aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore5
-rw-r--r--.gitlab-ci.yml58
-rw-r--r--.woodpecker/eslint.yml9
-rw-r--r--.woodpecker/lint.yml9
-rw-r--r--.woodpecker/mypy.yml9
-rw-r--r--.woodpecker/test.yml14
-rw-r--r--CHANGELOG.rst70
-rw-r--r--Dockerfile2
-rw-r--r--README.md4
-rw-r--r--asset-sources/fietsboek.ts26
-rw-r--r--asset-sources/theme.scss87
-rw-r--r--babel.cfg1
-rwxr-xr-xci/run_tests.sh48
-rw-r--r--doc/administration/configuration.rst14
-rw-r--r--doc/administration/installation.rst38
-rw-r--r--doc/developer/module/hittekaart_py.rst149
-rw-r--r--doc/developer/module/modules.rst8
-rw-r--r--doc/man/fietsctl.rst22
-rw-r--r--fietsboek/__init__.py50
-rw-r--r--fietsboek/actions.py97
-rw-r--r--fietsboek/alembic/versions/20220808_d085998b49ca.py11
-rw-r--r--fietsboek/alembic/versions/20230203_3149aa2d0114.py2
-rw-r--r--fietsboek/alembic/versions/20250607_2ebe1bf66430.py46
-rw-r--r--fietsboek/alembic/versions/20251019_90b39fdf6e4b.py46
-rw-r--r--fietsboek/alembic/versions/20251230_f9ca03541351.py43
-rw-r--r--fietsboek/config.py39
-rw-r--r--fietsboek/convert.py136
-rw-r--r--fietsboek/data.py365
-rw-r--r--fietsboek/fstrans.py417
-rw-r--r--fietsboek/geo.py236
-rw-r--r--fietsboek/hittekaart.py67
-rw-r--r--fietsboek/locale/de/LC_MESSAGES/messages.mobin16196 -> 20960 bytes
-rw-r--r--fietsboek/locale/de/LC_MESSAGES/messages.po576
-rw-r--r--fietsboek/locale/en/LC_MESSAGES/messages.mobin15148 -> 19742 bytes
-rw-r--r--fietsboek/locale/en/LC_MESSAGES/messages.po576
-rw-r--r--fietsboek/locale/fietslog.pot558
-rw-r--r--fietsboek/models/__init__.py17
-rw-r--r--fietsboek/models/badge.py10
-rw-r--r--fietsboek/models/comment.py17
-rw-r--r--fietsboek/models/image.py12
-rw-r--r--fietsboek/models/journey.py190
-rw-r--r--fietsboek/models/meta.py14
-rw-r--r--fietsboek/models/track.py297
-rw-r--r--fietsboek/models/user.py77
-rw-r--r--fietsboek/pdf-assets/COPYRIGHT.md104
-rw-r--r--fietsboek/pdf-assets/Nunito.ttfbin0 -> 132200 bytes
-rw-r--r--fietsboek/pdf-assets/Twemoji.Mozilla.ttfbin0 -> 1474284 bytes
-rw-r--r--fietsboek/pdf-assets/overview.typ29
-rw-r--r--fietsboek/pdf.py291
-rw-r--r--fietsboek/routes.py48
-rw-r--r--fietsboek/scripts/fietscron.py49
-rw-r--r--fietsboek/scripts/fietsctl.py105
-rw-r--r--fietsboek/static/DeadEnd.svg44
-rw-r--r--fietsboek/static/NoEntry.svg37
-rw-r--r--fietsboek/static/fietsboek.js25
-rw-r--r--fietsboek/static/fietsboek.js.map2
-rw-r--r--fietsboek/static/theme.css101
-rw-r--r--fietsboek/static/theme.css.map2
-rw-r--r--fietsboek/summaries.py175
-rw-r--r--fietsboek/templates/403.jinja216
-rw-r--r--fietsboek/templates/404.jinja214
-rw-r--r--fietsboek/templates/admin.jinja249
-rw-r--r--fietsboek/templates/admin_badges.jinja245
-rw-r--r--fietsboek/templates/admin_overview.jinja297
-rw-r--r--fietsboek/templates/browse.jinja2159
-rw-r--r--fietsboek/templates/details.jinja226
-rw-r--r--fietsboek/templates/edit.jinja24
-rw-r--r--fietsboek/templates/journey_details.jinja2180
-rw-r--r--fietsboek/templates/journey_edit.jinja221
-rw-r--r--fietsboek/templates/journey_form.jinja2261
-rw-r--r--fietsboek/templates/journey_list.jinja232
-rw-r--r--fietsboek/templates/journey_new.jinja221
-rw-r--r--fietsboek/templates/layout.jinja24
-rw-r--r--fietsboek/templates/profile_overview.jinja2139
-rw-r--r--fietsboek/trackmap.py148
-rw-r--r--fietsboek/transformers/__init__.py9
-rw-r--r--fietsboek/transformers/breaks.py57
-rw-r--r--fietsboek/transformers/elevation.py28
-rw-r--r--fietsboek/updater/__init__.py21
-rw-r--r--fietsboek/updater/cli.py19
-rw-r--r--fietsboek/updater/scripts/upd_20230103_lu8w3rwlz4ddcpms.py68
-rw-r--r--fietsboek/updater/scripts/upd_20250618_v0.11.0.py27
-rw-r--r--fietsboek/updater/scripts/upd_20251109_nm561argcq1s8w27.py163
-rw-r--r--fietsboek/updater/scripts/upd_20260103_v0.12.0.py27
-rw-r--r--fietsboek/updater/scripts/upd_20260103_v0.12.1.py27
-rw-r--r--fietsboek/updater/scripts/upd_30ppwg8zi4ujb46f.py46
-rw-r--r--fietsboek/util.py94
-rw-r--r--fietsboek/views/admin.py158
-rw-r--r--fietsboek/views/browse.py139
-rw-r--r--fietsboek/views/default.py2
-rw-r--r--fietsboek/views/detail.py136
-rw-r--r--fietsboek/views/edit.py65
-rw-r--r--fietsboek/views/errors.py32
-rw-r--r--fietsboek/views/journey.py267
-rw-r--r--fietsboek/views/notfound.py19
-rw-r--r--fietsboek/views/profile.py20
-rw-r--r--fietsboek/views/tileproxy.py109
-rw-r--r--fietsboek/views/upload.py41
-rw-r--r--poetry.lock3442
-rw-r--r--pylint.tests.toml4
-rw-r--r--pylint.toml6
-rw-r--r--pyproject.toml98
-rw-r--r--release-checklist.md2
-rw-r--r--testing.ini2
-rw-r--r--tests/assets/picture01.jpgbin0 -> 58545 bytes
-rw-r--r--tests/assets/picture02.jpgbin0 -> 52997 bytes
-rw-r--r--tests/bootstrap/test_new_instance.py90
-rw-r--r--tests/cli/conftest.py14
-rw-r--r--tests/cli/test_fietsctl.py241
-rw-r--r--tests/conftest.py26
-rw-r--r--tests/integration/test_browse.py118
-rw-r--r--tests/integration/test_pdf.py59
-rw-r--r--tests/playwright/conftest.py18
-rw-r--r--tests/playwright/test_basic.py34
-rw-r--r--tests/playwright/test_journeys.py170
-rw-r--r--tests/playwright/test_profiles.py2
-rw-r--r--tests/playwright/test_share.py3
-rw-r--r--tests/playwright/test_tileproxy.py4
-rw-r--r--tests/playwright/test_transformers.py19
-rw-r--r--tests/testutils.py159
-rw-r--r--tests/unit/test_pdf.py58
-rw-r--r--tests/unit/test_util.py25
-rw-r--r--tox.ini6
123 files changed, 10234 insertions, 2640 deletions
diff --git a/.gitignore b/.gitignore
index 56f6db9..99e9602 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,5 +26,8 @@ test
poetry.toml
/language-packs
# The module docs are regenerated using sphinx-apidoc
-doc/developer/module/
+doc/developer/module/*
+# However, we manually document hittekaart_py:
+!doc/developer/module/modules.rst
+!doc/developer/module/hittekaart_py.rst
/node_modules
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
deleted file mode 100644
index 9263e91..0000000
--- a/.gitlab-ci.yml
+++ /dev/null
@@ -1,58 +0,0 @@
-# Change pip's cache directory to be inside the project directory since we can
-# only cache local items.
-variables:
- PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
-
-default:
- image: python:bullseye
- # Pip's cache doesn't store the python packages
- # https://pip.pypa.io/en/stable/topics/caching/
- #
- # If you want to also cache the installed packages, you have to install
- # them in a virtualenv and cache it as well.
- cache:
- paths:
- - .cache/pip
- - .tox
-
- before_script:
- - python --version # For debugging
- - pip install tox
-
-test:
- script:
- - pip install poetry && pip install "playwright=="$(poetry show playwright | grep version | cut -f 2 -d ":" | tr -d " ")
- - playwright install firefox
- - playwright install-deps
- - apt install -y redis-server
- - redis-server >/dev/null 2>&1 &
- - tox -e python -- --browser firefox
-
-# test-pypy:
-# image: pypy:3
-# # nh3 does not have wheels prepared for pypy, so we need a working Rust
-# # compiler to compile the module:
-# script:
-# - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh /dev/stdin -y --profile=minimal
-# - source "$HOME/.cargo/env"
-# - pip install poetry && pip install "playwright=="$(poetry show playwright | grep version | cut -f 2 -d ":" | tr -d " ")
-# - playwright install firefox
-# - playwright install-deps
-# - apt install -y redis-server
-# - redis-server >/dev/null 2>&1 &
-# - tox -e pypy3 -- --browser firefox
-
-lint:
- script:
- - tox -e pylint,pylint-tests,flake,black,isort
-
-mypy:
- script:
- - tox -e mypy
-
-eslint:
- image: node
- before_script: []
- script:
- - npm install
- - npx eslint asset-sources/*.ts
diff --git a/.woodpecker/eslint.yml b/.woodpecker/eslint.yml
new file mode 100644
index 0000000..52fb895
--- /dev/null
+++ b/.woodpecker/eslint.yml
@@ -0,0 +1,9 @@
+when:
+ - event: push
+
+steps:
+ - name: eslint
+ image: node
+ commands:
+ - npm install
+ - npx eslint asset-sources/*.ts
diff --git a/.woodpecker/lint.yml b/.woodpecker/lint.yml
new file mode 100644
index 0000000..37eebba
--- /dev/null
+++ b/.woodpecker/lint.yml
@@ -0,0 +1,9 @@
+when:
+ - event: push
+
+steps:
+ - name: lint
+ image: python:bookworm
+ commands:
+ - pip install tox
+ - tox -e pylint,pylint-tests,flake,black,isort
diff --git a/.woodpecker/mypy.yml b/.woodpecker/mypy.yml
new file mode 100644
index 0000000..92ad9cf
--- /dev/null
+++ b/.woodpecker/mypy.yml
@@ -0,0 +1,9 @@
+when:
+ - event: push
+
+steps:
+ - name: mypy
+ image: python:bookworm
+ commands:
+ - pip install tox
+ - tox -e mypy
diff --git a/.woodpecker/test.yml b/.woodpecker/test.yml
new file mode 100644
index 0000000..f50057a
--- /dev/null
+++ b/.woodpecker/test.yml
@@ -0,0 +1,14 @@
+when:
+ - event: push
+
+matrix:
+ DB:
+ - sqlite
+ - postgres
+
+steps:
+ - name: test
+ image: python:bookworm
+ commands:
+ - pip install tox
+ - ci/run_tests.sh $DB
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index a49f7b6..21a0f4e 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -4,6 +4,76 @@ Changelog
Unreleased
----------
+Added
+^^^^^
+
+- The ``fietsctl track add`` subcommand.
+- The yearly tracks ("contributions") view.
+- Filtering and sorting by uphill amount (thanks Christoph).
+
+0.12.1 - 2026-01-03
+-------------------
+
+Fixed
+^^^^^
+
+- Fixed ``fietsctl maintenance-mode``.
+
+0.12.0 - 2026-01-03
+-------------------
+
+Added
+^^^^^
+
+- Added opengraph tags for better previews.
+- Added PDF renderings of tracks.
+- Added journeys.
+
+Changed
+^^^^^^^
+
+- ``hittekaart`` is now built as a dependency, no longer an external binary.
+- Tracks are now stored in the database.
+
+Fixed
+^^^^^
+
+- Backup GPX files are now included in the storage breakdown.
+- No more warnings about ``pkg_resources`` being deprecated.
+- Proper deletion of (partial) track data if something fails during the upload.
+
+Removed
+^^^^^^^
+
+- The ``hittekaart.bin`` configuration setting (not needed anymore, since it's
+ a dependency now).
+- ``Content-Encoding: br`` for GPX files (not worth anymore, as we need to
+ compress on-the-fly).
+
+0.11.0 - 2025-06-18
+-------------------
+
+Added
+^^^^^
+
+- The admin overview with storage statistics and some other information.
+- Track preview images in the browse/profile views.
+- The ability to replace a track's GPX file with a new one.
+
+Changed
+^^^^^^^
+
+- Fietsboek now requires at least Python 3.11.
+- There is now a proper 404 (Not Found) page.
+- There is now a proper 403 (Forbidden) page.
+- The heat map is now preselected on the profile page.
+- The browse page now paginates its results.
+
+Fixed
+^^^^^
+
+- The alignment of image/comment counts on the home view has been fixed.
+
0.10.0 - 2025-02-05
-------------------
diff --git a/Dockerfile b/Dockerfile
index 53eb2cc..f485f2e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM python:3.11
+FROM python:3.14
RUN pip install gunicorn psycopg2-binary mysqlclient
RUN mkdir /package
diff --git a/README.md b/README.md
index 9f508e5..895c838 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
Fietsboek
=========
-<a href="https://gitlab.com/dunj3/fietsboek/-/pipelines">
- <img src="https://img.shields.io/gitlab/pipeline-status/dunj3/fietsboek?branch=master" alt="Pipeline status">
+<a href="https://ci.kingdread.de/repos/2">
+ <img src="https://ci.kingdread.de/api/badges/2/status.svg" alt="Pipeline status">
</a>
<a href="https://fietsboek.readthedocs.io">
<img src="https://img.shields.io/readthedocs/fietsboek" alt="Documentation status">
diff --git a/asset-sources/fietsboek.ts b/asset-sources/fietsboek.ts
index 66caa4f..bcce661 100644
--- a/asset-sources/fietsboek.ts
+++ b/asset-sources/fietsboek.ts
@@ -540,6 +540,23 @@ function loadProfileStats() {
/* Used via in-page scripts, so make eslint happy */
loadProfileStats;
+/**
+ * Formats the given timestamp to the user's locale.
+ *
+ * @param timestamp - The timestamp in milliseconds since the epoch.
+ * @return The formatted string.
+ */
+function formatTimestamp(timestamp: number): string {
+ const date = new Date(timestamp);
+ // TypeScript complains about this, but according to MDN it is fine, at
+ // least in "somewhat modern" browsers
+ const intl = new Intl.DateTimeFormat(LOCALE, {
+ dateStyle: "medium",
+ timeStyle: "medium",
+ } as any);
+ return intl.format(date);
+}
+
document.addEventListener('DOMContentLoaded', function() {
window.fietsboekImageIndex = 0;
@@ -567,13 +584,6 @@ document.addEventListener('DOMContentLoaded', function() {
/* Format all datetimes to the local timezone */
document.querySelectorAll(".fietsboek-local-datetime").forEach((obj) => {
const timestamp = parseFloat(obj.attributes.getNamedItem("data-utc-timestamp")!.value);
- const date = new Date(timestamp * 1000);
- // TypeScript complains about this, but according to MDN it is fine, at
- // least in "somewhat modern" browsers
- const intl = new Intl.DateTimeFormat(LOCALE, {
- dateStyle: "medium",
- timeStyle: "medium",
- } as any);
- obj.innerHTML = intl.format(date);
+ obj.innerHTML = formatTimestamp(timestamp * 1000);
});
});
diff --git a/asset-sources/theme.scss b/asset-sources/theme.scss
index 7f89bf6..374f5fe 100644
--- a/asset-sources/theme.scss
+++ b/asset-sources/theme.scss
@@ -271,6 +271,40 @@ strong {
}
}
+.contribs-cell {
+ div {
+ border: 1px solid rgba(0, 0, 0, 0.175);
+ border-radius: calc(0.375rem - (1px));
+
+ width: 1.1em;
+ height: 1.1em;
+ }
+}
+
+.contribs-rowhead {
+ font-size: 80%;
+ padding: 0 1px;
+}
+
+.contribs-length-0 div {
+ background-color: #FFFFFF;
+}
+.contribs-length-1 div {
+ background-color: #8FF0A4;
+}
+.contribs-length-2 div {
+ background-color: #75DD95;
+}
+.contribs-length-3 div {
+ background-color: #5BC987;
+}
+.contribs-length-4 div {
+ background-color: #40B678;
+}
+.contribs-length-5 div {
+ background-color: #26A269;
+}
+
.browse-summary {
font-variant: lining-nums;
}
@@ -280,10 +314,63 @@ strong {
width: 25%;
}
+.browse-track-card {
+ display: grid;
+ grid-template: 'preview data' / 300px auto;
+
+ @media (max-width: 768px) {
+ grid-template:
+ 'preview'
+ 'data';
+ }
+
+ &.card-body {
+ padding: 0px;
+ }
+
+ .browse-track-preview {
+ grid-area: preview;
+
+ @media (max-width: 768px) {
+ text-align: center;
+ }
+
+ img {
+ width: 300px;
+ height: 300px;
+ }
+ }
+
+ .browse-track-data {
+ grid-area: data;
+ padding: var(--bs-card-spacer-y) var(--bs-card-spacer-x);
+ }
+}
+
.chart-title {
text-align: center;
}
+/* Admin view layout: We have an extra sidebar for the navigation */
+#adminContainer {
+ display: grid;
+ grid-template-areas: "sidebar main";
+ grid-template-columns: 1fr 5fr;
+ gap: 1rem;
+}
+
+#adminNavigation {
+ grid-area: sidebar;
+}
+
+#adminContent {
+ grid-area: main;
+}
+
+.admin-stat {
+ font-size: 120%;
+}
+
.list-group.list-group-root {
padding: 0;
overflow: hidden;
diff --git a/babel.cfg b/babel.cfg
index 6de4aaa..650a00a 100644
--- a/babel.cfg
+++ b/babel.cfg
@@ -4,3 +4,4 @@ jinja2 = jinja2.ext:babel_extract
[jinja2: **.jinja2]
[python: **.py]
+keywords = _ pluralize:1,2
diff --git a/ci/run_tests.sh b/ci/run_tests.sh
new file mode 100755
index 0000000..200f794
--- /dev/null
+++ b/ci/run_tests.sh
@@ -0,0 +1,48 @@
+#!/bin/bash
+set -euxo pipefail
+
+DB=$1
+
+PPATH="/usr/lib/postgresql/15/bin/"
+
+setup_postgres() {
+ apt update
+ apt install -y postgresql postgresql-client sudo
+ echo -n "postgres" >/tmp/pw
+ mkdir /tmp/postgres-db
+ chown postgres:postgres /tmp/postgres-db
+ sudo -u postgres "$PPATH/initdb" --pwfile /tmp/pw -U postgres /tmp/postgres-db
+ sudo -u postgres "$PPATH/postgres" -D /tmp/postgres-db >/dev/null 2>&1 &
+}
+
+setup_redis() {
+ apt install -y redis-server
+ redis-server >/dev/null 2>&1 &
+}
+
+setup_playwright() {
+ pip install poetry
+ pip install "playwright=="$(poetry show playwright | grep version | cut -f 2 -d ":" | tr -d " ")
+ playwright install firefox
+ playwright install-deps
+}
+
+case "$DB" in
+ "sqlite")
+ ;;
+
+ "postgres")
+ setup_postgres
+ sed -i 's|^sqlalchemy.url = .*$|sqlalchemy.url = postgresql://postgres:postgres@localhost/postgres|' testing.ini
+ ;;
+
+ *)
+ echo "Unknown database: $DB"
+ exit 1
+ ;;
+esac
+
+pip install tox
+setup_playwright
+setup_redis
+tox -e python -- --browser firefox
diff --git a/doc/administration/configuration.rst b/doc/administration/configuration.rst
index 24c5fdb..98f3592 100644
--- a/doc/administration/configuration.rst
+++ b/doc/administration/configuration.rst
@@ -221,15 +221,12 @@ true``. This will cause the tiles to be loaded directly by the client.
Hittekaart Integration
----------------------
-Fietsboek can use hittekaart_ to generate heat maps for users. For that, you
-can set ``hittekaart.bin`` to the path to the ``hittekaart`` binary. If unset,
-it is assumed that the binary can be found in your ``$PATH``.
+Fietsboek can use hittekaart_ to generate heat maps for users.
-In addition, you can set ``hittekaart.autogenerate`` to the list of overlay
-maps you want to automatically generate and update. By default, this list is
-empty, which means that Fietsboek will not generate any overlays on its own.
-You can add ``heatmap`` and/or ``tilehunter`` to generate those maps
-automatically.
+You can set ``hittekaart.autogenerate`` to the list of overlay maps you want to
+automatically generate and update. By default, this list is empty, which means
+that Fietsboek will not generate any overlays on its own. You can add
+``heatmap`` and/or ``tilehunter`` to generate those maps automatically.
By default, ``hittekaart`` will use as many threads as CPU cores are available.
This leads to the fastest heatmap generation, but might be undesired on shared
@@ -253,7 +250,6 @@ An example configuration excerpt can look like this:
.. code-block:: ini
- hittekaart.bin = /usr/local/bin/hittekaart
hittekaart.autogenerate = heatmap tilehunter
hittekaart.threads = 2
diff --git a/doc/administration/installation.rst b/doc/administration/installation.rst
index b207784..8c9c26e 100644
--- a/doc/administration/installation.rst
+++ b/doc/administration/installation.rst
@@ -6,14 +6,17 @@ 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``):
+Fietsboek has the following requirements:
-* Python 3.10 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)
+* A Linux system
+* Python 3.11 or later
+* A `redis <https://redis.com/>`__ server
+* (Optionally) an SQL database server:
+
+ * `PostgreSQL <https://www.postgresql.org/>`__
+
+Other systems (such as BSD as operating system, or MariaDB as SQL server) might
+work, but are not tested.
In addition, if you run on a different interpreter than CPython, you might need
a working Rust toolchain (``rustc`` and ``cargo``) installed. This is because
@@ -97,6 +100,18 @@ parser to process the GPX files:
.. _issue #7: https://gitlab.com/dunj3/fietsboek/-/issues/7
+Optional: Install Database Drivers
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+If you decide to use a database other than SQLite, you must install the
+required drivers:
+
+**PostgreSQL**:
+
+.. code:: bash
+
+ .venv/bin/pip install psycopg2
+
Configuring Fietsboek
---------------------
@@ -119,7 +134,7 @@ other update tasks. You can use it to set up the initial database schema:
instead of a specific version, you must also run ``.venv/bin/alembic -c
production.ini upgrade head``.
- See :doc:`../developer/updater` ("Furhter notes") for more information.
+ See :doc:`../developer/updater` ("Further notes") for more information.
Adding an Administrator User
----------------------------
@@ -164,6 +179,13 @@ performance. For those, refer to the Pyramid documentation for more information:
* `mod_wsgi
<https://docs.pylonsproject.org/projects/pyramid/en/latest/tutorials/modwsgi/index.html>`__
+.. note::
+
+ When you use ``gunicorn``, make sure to set ``forward_allow_ips`` properly.
+ This also applies to ``gunicorn`` in the Docker container:
+
+ https://gunicorn.org/reference/settings/#forwarded_allow_ips
+
Maintaining Fietsboek
---------------------
diff --git a/doc/developer/module/hittekaart_py.rst b/doc/developer/module/hittekaart_py.rst
new file mode 100644
index 0000000..052c2b0
--- /dev/null
+++ b/doc/developer/module/hittekaart_py.rst
@@ -0,0 +1,149 @@
+hittekaart_py package
+=====================
+
+.. module:: hittekaart_py
+
+ The ``hittekaart_py`` module provides a Python interface for hittekaart__
+ heatmap generation. Unlike previous modules, this is a binding using
+ PyO3__, and not a subprocess wrapper.
+
+.. __: https://gitlab.com/dunj3/hittekaart
+.. __: https://crates.io/crates/pyo3
+
+Example
+-------
+
+.. code-block:: python
+
+ from hittekaart_py import (
+ Track, HeatmapRenderer, Settings, Storage, generate
+ )
+
+ settings = Settings(threads=3)
+ tracks = [
+ Track.from_file(b"Documents/track.gpx", None),
+ Track.from_coordinates([(45.0, 47.0)]),
+ ]
+ storage = Storage.Sqlite(b"/tmp/tiles.sqlite")
+ generate(settings, tracks, HeatmapRenderer(), storage)
+
+Input and output
+----------------
+
+.. class:: Track
+
+ An in-memory representation of a track.
+
+ .. staticmethod:: from_file(path, compression)
+
+ Loads a track from the given file.
+
+ :param path: Path to the file.
+ :type path: bytes
+ :param compression: Decompression algorithm to use when reading the
+ file. Can be :obj:`None`, ``"gzip"`` or ``"brotli"``.
+ :type compression: str | None
+ :return: The created track.
+ :rtype: Track
+
+ .. staticmethod:: from_coordinates(coordinates)
+
+ Directly represents the given coordinates as a track.
+
+ :param coordinates: The coordinates as a list of longitude-latitude
+ pairs.
+ :type coordinates: list[tuple[float, float]]
+ :return: The created track.
+ :rtype: Track
+
+
+.. class:: Storage
+
+ Represents the output storage.
+
+ .. staticmethod:: Folder(path)
+
+ Output the tiles to the given folder. This will create a subdirectory
+ for every zoom level, then a directory for the x coordinate, then a
+ file ``y.png``.
+
+ Note that this will create many small files, which can waste space.
+
+ :param path: Path to the folder.
+ :type path: bytes
+ :return: The created storage.
+ :rtype: Storage
+
+ .. staticmethod:: Sqlite(path)
+
+ Output the tiles to the given SQLite database.
+
+ :param path: Path to the database.
+ :type path: bytes
+ :return: The created storage.
+ :rtype: Storage
+
+Renderers
+---------
+
+.. class:: HeatmapRenderer()
+
+ The renderer that generates a heatmap.
+
+.. class:: MarktileRenderer()
+
+ The renderer that will only mark visited tiles.
+
+.. class:: TilehuntRenderer(zoom)
+
+ The renderer that will mark visisted tiles at a fixed zoom level.
+
+ :param zoom: The zoom level.
+ :type zoom: int
+
+Tile generation
+---------------
+
+.. class:: Settings(min_zoom=1, max_zoom=19, threads=0)
+
+ Settings that apply to all renderers.
+
+ .. attribute:: min_zoom
+ :type: int
+
+ Smalles zoom level to generate tiles for.
+
+ .. attribute:: max_zoom
+ :type: int
+
+ Largest zoom level to generate tiles for.
+
+ .. attribute:: threads
+ :type: int
+
+ Number of threads to use for tile generation.
+
+ Setting this to 0 will automatically determine the number of available
+ cores and use that many threads.
+
+.. function:: generate(settings, items, renderer, storage)
+
+ Generates the tiles using the given renderer, and saves them to the given
+ storage.
+
+ :param settings: The settings.
+ :type settings: Settings
+ :param items: The tracks to render.
+ :type items: ~typing.Iterable[Track]
+ :param renderer: The renderer to use.
+ :type renderer: HeatmapRenderer | MarktileRenderer | TilehuntRenderer
+ :param storage: The storage to output to.
+ :type storage: Storage
+
+Errors
+------
+
+.. exception:: HitteError
+
+ Catch-all error for underlying hittekaart errors. See the string
+ description for the error cause.
diff --git a/doc/developer/module/modules.rst b/doc/developer/module/modules.rst
new file mode 100644
index 0000000..9585a5f
--- /dev/null
+++ b/doc/developer/module/modules.rst
@@ -0,0 +1,8 @@
+fietsboek
+=========
+
+.. toctree::
+ :maxdepth: 1
+
+ fietsboek
+ hittekaart_py
diff --git a/doc/man/fietsctl.rst b/doc/man/fietsctl.rst
index 1d30b20..eb3dfae 100644
--- a/doc/man/fietsctl.rst
+++ b/doc/man/fietsctl.rst
@@ -11,7 +11,7 @@ SYNPOSIS
.. code-block:: text
fietsctl maintenance-mode
- fietsctl track {del|list}
+ fietsctl track {add|del|list}
fietsctl user {add|del|hittekaart|list|modify|passwd}
fietsctl version
@@ -186,6 +186,26 @@ For each track, the following information is shown:
* The track's owner (both name and email address)
* The track's title.
+ADDING A TRACK
+##############
+
+.. code-block:: text
+
+ fietsctl track add [-c CONFIG] [-i/--id ID] [-e/--email] [--visibility PRIVATE|FRIENDS|FRIENDS_TAGGED|LOGGED_IN|PUBLIC] [--track-type ORGANIC|SYNTHETIC] FILENAME...
+
+Adds a track to the system. You specify the owner via the ``--id`` or
+``--email`` options, similar to the ``user modify`` command.
+
+One or multiple ``FILENAME`` might be given. Any supported input format may be
+used.
+
+The titles and dates are attempted to be read from the files. Other metadata
+(tags, images, ...) is left empty. The ``--visibility`` and ``--track-type``
+options can be used to set the visibility and type of the added tracks.
+
+For each file read, the resulting ID in the database is printed. If an error
+occurs for one file, the error is shown and the next track is read.
+
REMOVING A TRACK
################
diff --git a/fietsboek/__init__.py b/fietsboek/__init__.py
index 3d6c125..1f21c5f 100644
--- a/fietsboek/__init__.py
+++ b/fietsboek/__init__.py
@@ -20,6 +20,7 @@ from pathlib import Path
from typing import Callable, Optional
import redis
+import sqlalchemy
from pyramid.config import Configurator
from pyramid.csrf import CookieCSRFStoragePolicy
from pyramid.httpexceptions import HTTPServiceUnavailable
@@ -30,6 +31,7 @@ from pyramid.response import Response
from pyramid.session import SignedCookieSessionFactory
from . import config as mod_config
+from . import fstrans
from . import jinja2 as mod_jinja2
from . import transformers
from .data import DataManager
@@ -37,7 +39,10 @@ from .pages import Pages
from .security import SecurityPolicy
from .updater import Updater, UpdateState
-__VERSION__ = importlib.metadata.version("fietsboek")
+try:
+ __VERSION__ = importlib.metadata.version("fietsboek")
+except importlib.metadata.PackageNotFoundError:
+ __VERSION__ = "<unknown>"
LOGGER = logging.getLogger(__name__)
@@ -88,7 +93,10 @@ def maintenance_mode(
"""
def tween(request: Request) -> Response:
- maintenance = request.data_manager.maintenance_mode()
+ # We don't want to mess around with transactioned data mangers here,
+ # so we create a new one without transaction
+ data_manager = DataManager(Path(request.config.data_dir))
+ maintenance = data_manager.maintenance_mode()
if maintenance is None:
return handler(request)
@@ -119,6 +127,35 @@ def check_update_state(config_uri: str):
LOGGER.warning("Could not determine version state of the data - check `fietsupdate status`")
+def check_db_engine(sqlalchemy_uri: str):
+ """Checks whether we "support" the given SQL engine.
+
+ :param sqlalchemy_uri: The configured SQLAlchemy URL.
+ """
+ engine = sqlalchemy.create_engine(sqlalchemy_uri)
+ match engine.name:
+ case "sqlite":
+ pass
+ case _:
+ LOGGER.warning(
+ "The configured SQL backend is not well tested in combination with fietsboek. "
+ "Use it at your own risk."
+ )
+
+
+def create_data_folders(data_dir: Path):
+ """Creates the subfolders of the data directory.
+
+ :param data_dir: Path to the data directory, from the config.
+ """
+ LOGGER.debug("Creating %s/tracks/", data_dir)
+ (data_dir / "tracks").mkdir(exist_ok=True)
+ LOGGER.debug("Creating %s/users/", data_dir)
+ (data_dir / "users").mkdir(exist_ok=True)
+ LOGGER.debug("Creating %s/journeys/", data_dir)
+ (data_dir / "journeys").mkdir(exist_ok=True)
+
+
def main(global_config, **settings):
"""This function returns a Pyramid WSGI application."""
# Avoid a circular import by not importing at the top level
@@ -132,8 +169,13 @@ def main(global_config, **settings):
parsed_config = mod_config.parse(settings)
settings["jinja2.newstyle"] = True
+ check_db_engine(parsed_config.sqlalchemy_url)
+ create_data_folders(parsed_config.data_dir)
+
def data_manager(request):
- return DataManager(Path(request.config.data_dir))
+ data_dir = Path(request.config.data_dir)
+ lock_file = data_dir / "lock"
+ return DataManager(data_dir, txn=fstrans.begin(lock_file, request.tm))
def redis_(request):
return redis.from_url(request.config.redis_url)
@@ -174,7 +216,7 @@ def main(global_config, **settings):
config.add_request_method(redis_, name="redis", reify=True)
config.add_request_method(config_, name="config", reify=True)
- config.registry.registerUtility(TileRequester())
+ config.registry.registerUtility(TileRequester(redis.from_url(parsed_config.redis_url)))
jinja2_env = config.get_jinja2_environment()
jinja2_env.filters["format_decimal"] = mod_jinja2.filter_format_decimal
diff --git a/fietsboek/actions.py b/fietsboek/actions.py
index a20ca2e..cdddaa2 100644
--- a/fietsboek/actions.py
+++ b/fietsboek/actions.py
@@ -7,23 +7,24 @@ the test functions.
"""
import datetime
+import io
import logging
import re
from typing import Optional
-import brotli
-import gpxpy
from pyramid.i18n import TranslationString as _
from pyramid.request import Request
from sqlalchemy import select
from sqlalchemy.orm.session import Session
-from . import email, models
+from . import convert, email, models, trackmap
from . import transformers as mod_transformers
from . import util
+from .config import TileLayerConfig
from .data import DataManager, TrackDataDir
from .models.track import TrackType, Visibility
from .models.user import TokenType
+from .views.tileproxy import TileRequester
LOGGER = logging.getLogger(__name__)
@@ -31,6 +32,8 @@ LOGGER = logging.getLogger(__name__)
def add_track(
dbsession: Session,
data_manager: DataManager,
+ tile_requester: TileRequester,
+ layer: TileLayerConfig,
owner: models.User,
title: str,
date: datetime.datetime,
@@ -69,16 +72,19 @@ def add_track(
"""
# pylint: disable=too-many-positional-arguments,too-many-locals,too-many-arguments
LOGGER.debug("Inserting new track...")
- track = models.Track(
- owner=owner,
- title=title,
- visibility=visibility,
- type=track_type,
- description=description,
- badges=badges,
- link_secret=util.random_link_secret(),
- tagged_people=tagged_people,
- )
+ track = convert.smart_convert(gpx_data)
+ path = track.path()
+ # We set the path later using fast_set_path. By setting the empty list
+ # here, we avoid doing an insert of many thousand points prematurely.
+ track.points = []
+ track.owner = owner
+ track.title = title
+ track.visibility = visibility
+ track.type = track_type
+ track.description = description
+ track.badges = badges
+ track.link_secret = util.random_link_secret()
+ track.tagged_people = tagged_people
track.date = date
track.sync_tags(tags)
dbsession.add(track)
@@ -87,31 +93,27 @@ def add_track(
# Save the GPX data
LOGGER.debug("Creating a new data folder for %d", track.id)
assert track.id is not None
- with data_manager.initialize(track.id) as manager:
- LOGGER.debug("Saving GPX to %s", manager.gpx_path())
- manager.compress_gpx(gpx_data)
- manager.backup()
-
- gpx = gpxpy.parse(gpx_data)
- for transformer in transformers:
- LOGGER.debug("Running %s with %r", transformer, transformer.parameters)
- transformer.execute(gpx)
- track.transformers = [
- [tfm.identifier(), tfm.parameters.model_dump()] for tfm in transformers
- ]
-
- # Best time to build the cache is right after the upload, but *after* the
- # transformers have been applied!
- track.ensure_cache(gpx)
- dbsession.add(track.cache)
-
- manager.engrave_metadata(
- title=track.title,
- description=track.description,
- author_name=track.owner.name,
- time=track.date,
- gpx=gpx,
- )
+ manager = data_manager.initialize(track.id)
+ LOGGER.debug("Saving backup to %s", manager.backup_path())
+ manager.compress_backup(gpx_data)
+
+ for transformer in transformers:
+ LOGGER.debug("Running %s with %r", transformer, transformer.parameters)
+ transformer.execute(path)
+ track.transformers = [[tfm.identifier(), tfm.parameters.model_dump()] for tfm in transformers]
+
+ track.fast_set_path(path)
+
+ # Best time to build the cache is right after the upload, but *after* the
+ # transformers have been applied!
+ track.ensure_cache(path)
+ dbsession.add(track.cache)
+
+ LOGGER.debug("Building preview image for %s", track.id)
+ preview_image = trackmap.render(path, layer, tile_requester)
+ image_io = io.BytesIO()
+ preview_image.save(image_io, "PNG")
+ manager.set_preview(image_io.getvalue())
return track
@@ -187,7 +189,7 @@ def edit_images(request: Request, track: models.Track, *, manager: Optional[Trac
request.dbsession.add(image_meta)
-def execute_transformers(request: Request, track: models.Track) -> Optional[gpxpy.gpx.GPX]:
+def execute_transformers(request: Request, track: models.Track):
"""Execute the transformers for the given track.
Note that this function "short circuits" if the saved transformer settings
@@ -198,7 +200,6 @@ def execute_transformers(request: Request, track: models.Track) -> Optional[gpxp
:param request: The request.
:param track: The track.
- :return: The transformed track.
"""
# pylint: disable=too-many-locals
LOGGER.debug("Executing transformers for %d", track.id)
@@ -208,22 +209,21 @@ def execute_transformers(request: Request, track: models.Track) -> Optional[gpxp
serialized = [[tfm.identifier(), tfm.parameters.model_dump()] for tfm in settings]
if serialized == track.transformers:
LOGGER.debug("Applied transformations match on %d, skipping", track.id)
- return None
+ return
# We always start with the backup, that way we don't get "deepfried GPX"
# files by having the same filters run multiple times on the same input.
# They are not idempotent after all.
manager = request.data_manager.open(track.id)
- gpx_bytes = manager.backup_path().read_bytes()
- gpx_bytes = brotli.decompress(gpx_bytes)
- gpx = gpxpy.parse(gpx_bytes)
+ backup_bytes = manager.decompress_backup()
+ reloaded = convert.smart_convert(backup_bytes)
+ path = reloaded.path()
for transformer in settings:
LOGGER.debug("Running %s with %r", transformer, transformer.parameters)
- transformer.execute(gpx)
+ transformer.execute(path)
- LOGGER.debug("Saving transformed file for %d", track.id)
- manager.compress_gpx(util.encode_gpx(gpx))
+ track.fast_set_path(path)
LOGGER.debug("Saving new transformers on %d", track.id)
track.transformers = serialized
@@ -231,9 +231,8 @@ def execute_transformers(request: Request, track: models.Track) -> Optional[gpxp
LOGGER.debug("Rebuilding cache for %d", track.id)
request.dbsession.delete(track.cache)
track.cache = None
- track.ensure_cache(gpx)
+ track.ensure_cache()
request.dbsession.add(track.cache)
- return gpx
def send_verification_token(request: Request, user: models.User):
diff --git a/fietsboek/alembic/versions/20220808_d085998b49ca.py b/fietsboek/alembic/versions/20220808_d085998b49ca.py
index d6353d2..5b47668 100644
--- a/fietsboek/alembic/versions/20220808_d085998b49ca.py
+++ b/fietsboek/alembic/versions/20220808_d085998b49ca.py
@@ -14,9 +14,18 @@ down_revision = '091ce24409fe'
branch_labels = None
depends_on = None
+is_postgres = lambda: op.get_bind().dialect.name == "postgresql"
+
def upgrade():
- op.add_column('tracks', sa.Column('type', sa.Enum('ORGANIC', 'SYNTHETIC', name='tracktype'), nullable=True))
+ if is_postgres():
+ tracktype = sa.dialects.postgresql.ENUM("ORGANIC", "SYNTHETIC", name="tracktype")
+ tracktype.create(op.get_bind())
+ op.add_column("tracks", sa.Column("type", tracktype, nullable=True))
+ else:
+ op.add_column('tracks', sa.Column('type', sa.Enum('ORGANIC', 'SYNTHETIC', name='tracktype'), nullable=True))
op.execute("UPDATE tracks SET type='ORGANIC';")
def downgrade():
op.drop_column('tracks', 'type')
+ if is_postgres():
+ op.execute("DROP TYPE tracktype;")
diff --git a/fietsboek/alembic/versions/20230203_3149aa2d0114.py b/fietsboek/alembic/versions/20230203_3149aa2d0114.py
index eb9ef78..ced8639 100644
--- a/fietsboek/alembic/versions/20230203_3149aa2d0114.py
+++ b/fietsboek/alembic/versions/20230203_3149aa2d0114.py
@@ -16,7 +16,7 @@ depends_on = None
def upgrade():
op.add_column('tracks', sa.Column('transformers', sa.JSON(), nullable=True))
- op.execute('UPDATE tracks SET transformers="[]";')
+ op.execute("UPDATE tracks SET transformers='[]';")
def downgrade():
op.drop_column('tracks', 'transformers')
diff --git a/fietsboek/alembic/versions/20250607_2ebe1bf66430.py b/fietsboek/alembic/versions/20250607_2ebe1bf66430.py
new file mode 100644
index 0000000..d1c2c2f
--- /dev/null
+++ b/fietsboek/alembic/versions/20250607_2ebe1bf66430.py
@@ -0,0 +1,46 @@
+"""switch transfomers from JSON to TEXT
+
+Revision ID: 2ebe1bf66430
+Revises: 4566843039d6
+Create Date: 2025-06-07 23:24:33.182649
+
+"""
+import logging
+
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = '2ebe1bf66430'
+down_revision = '4566843039d6'
+branch_labels = None
+depends_on = None
+
+is_sqlite = lambda: op.get_bind().dialect.name == "sqlite"
+
+def upgrade():
+ if is_sqlite():
+ op.add_column('tracks', sa.Column('transformers_text', sa.Text, nullable=True))
+ op.execute('UPDATE tracks SET transformers_text=transformers;')
+ try:
+ op.drop_column('tracks', 'transformers')
+ except sa.exc.OperationalError as exc:
+ logging.getLogger(__name__).warning(
+ "Your SQLite version does not support dropping a column. "
+ "We're setting the content to NULL instead: %s",
+ exc,
+ )
+ op.execute("UPDATE tracks SET transformers = NULL;")
+ op.alter_column("tracks", "transformers", new_column_name="transformers_old_delete_this_column")
+ op.alter_column('tracks', 'transformers_text', new_column_name='transformers')
+ else:
+ op.alter_column('tracks', 'transformers', type_=sa.Text)
+
+def downgrade():
+ if is_sqlite():
+ op.add_column('tracks', sa.Column('transfomers_json', sa.JSON, nullable=True))
+ op.execute('UPDATE tracks SET transformers_json=transformers;')
+ op.drop_column('tracks', 'transformers')
+ op.alter_column('tracks', 'transformers_json', new_column_name='transformers')
+ else:
+ op.alter_column('tracks', 'transformers', type_=sa.JSON)
diff --git a/fietsboek/alembic/versions/20251019_90b39fdf6e4b.py b/fietsboek/alembic/versions/20251019_90b39fdf6e4b.py
new file mode 100644
index 0000000..825f774
--- /dev/null
+++ b/fietsboek/alembic/versions/20251019_90b39fdf6e4b.py
@@ -0,0 +1,46 @@
+"""add table for track points
+
+Revision ID: 90b39fdf6e4b
+Revises: 2ebe1bf66430
+Create Date: 2025-10-19 20:17:12.562653
+
+"""
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = '90b39fdf6e4b'
+down_revision = '2ebe1bf66430'
+branch_labels = None
+depends_on = None
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('track_points',
+ sa.Column('track_id', sa.Integer(), nullable=False),
+ sa.Column('index', sa.Integer(), nullable=False),
+ sa.Column('longitude', sa.Float(), nullable=False),
+ sa.Column('latitude', sa.Float(), nullable=False),
+ sa.Column('elevation', sa.Float(), nullable=True),
+ sa.Column('time_offset', sa.Float(), nullable=True),
+ sa.ForeignKeyConstraint(['track_id'], ['tracks.id'], name=op.f('fk_track_points_track_id_tracks')),
+ sa.PrimaryKeyConstraint('track_id', 'index', name=op.f('pk_track_points'))
+ )
+ op.create_table('waypoints',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('track_id', sa.Integer(), nullable=False),
+ sa.Column('longitude', sa.Float(), nullable=False),
+ sa.Column('latitude', sa.Float(), nullable=False),
+ sa.Column('elevation', sa.Float(), nullable=True),
+ sa.Column('name', sa.Text(), nullable=True),
+ sa.Column('description', sa.Text(), nullable=True),
+ sa.ForeignKeyConstraint(['track_id'], ['tracks.id'], name=op.f('fk_waypoints_track_id_tracks')),
+ sa.PrimaryKeyConstraint('id', name=op.f('pk_waypoints'))
+ )
+ # ### end Alembic commands ###
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_table('track_points')
+ op.drop_table('waypoints')
+ # ### end Alembic commands ###
diff --git a/fietsboek/alembic/versions/20251230_f9ca03541351.py b/fietsboek/alembic/versions/20251230_f9ca03541351.py
new file mode 100644
index 0000000..363c6c1
--- /dev/null
+++ b/fietsboek/alembic/versions/20251230_f9ca03541351.py
@@ -0,0 +1,43 @@
+"""add journeys table
+
+Revision ID: f9ca03541351
+Revises: 90b39fdf6e4b
+Create Date: 2025-12-30 22:23:17.765361
+
+"""
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = 'f9ca03541351'
+down_revision = '90b39fdf6e4b'
+branch_labels = None
+depends_on = None
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('journeys',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('owner_id', sa.Integer(), nullable=False),
+ sa.Column('title', sa.Text(), nullable=False),
+ sa.Column('description', sa.Text(), nullable=False),
+ sa.Column('visibility', sa.Enum('PRIVATE', 'FRIENDS', 'LOGGED_IN', 'PUBLIC', name='journey_visibility'), nullable=False),
+ sa.Column('link_secret', sa.Text(), nullable=True),
+ sa.ForeignKeyConstraint(['owner_id'], ['users.id'], name=op.f('fk_journeys_owner_id_users')),
+ sa.PrimaryKeyConstraint('id', name=op.f('pk_journeys'))
+ )
+ op.create_table('journey_track_assoc',
+ sa.Column('journey_id', sa.Integer(), nullable=False),
+ sa.Column('track_id', sa.Integer(), nullable=False),
+ sa.Column('sort_index', sa.Integer(), nullable=False),
+ sa.ForeignKeyConstraint(['journey_id'], ['journeys.id'], name=op.f('fk_journey_track_assoc_journey_id_journeys')),
+ sa.ForeignKeyConstraint(['track_id'], ['tracks.id'], name=op.f('fk_journey_track_assoc_track_id_tracks')),
+ sa.PrimaryKeyConstraint('journey_id', 'track_id', name=op.f('pk_journey_track_assoc'))
+ )
+ # ### end Alembic commands ###
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_table('journey_track_assoc')
+ op.drop_table('journeys')
+ # ### end Alembic commands ###
diff --git a/fietsboek/config.py b/fietsboek/config.py
index af946b4..bd2347a 100644
--- a/fietsboek/config.py
+++ b/fietsboek/config.py
@@ -19,6 +19,7 @@ import re
import typing
import urllib.parse
from enum import Enum
+from itertools import chain
import pydantic
from pydantic import (
@@ -63,6 +64,11 @@ KNOWN_TILE_LAYERS = [
"hiking",
]
+WARNINGS = {
+ "hittekaart.bin": "hittekaart is now used via a Python module. "
+ "Enable extra `hittekaart` to install the dependency.",
+}
+
class ValidationError(Exception):
"""Exception for malformed configurations.
@@ -202,9 +208,6 @@ class Config(BaseModel):
tile_layers: list[TileLayerConfig] = []
"""Tile layers."""
- hittekaart_bin: str = Field("", alias="hittekaart.bin")
- """Path to the hittekaart binary."""
-
hittekaart_autogenerate: PyramidList = Field([], alias="hittekaart.autogenerate")
"""Overlay maps to automatically generate."""
@@ -277,6 +280,27 @@ class Config(BaseModel):
hasher.update(what_for.encode("utf-8"))
return hasher.hexdigest()
+ def public_tile_layers(self) -> list[TileLayerConfig]:
+ """Returns all tile layer configs that are public.
+
+ :return: A list of public :class:`TileLayerConfig`s.
+ """
+ # pylint: disable=import-outside-toplevel,cyclic-import
+ from .views.tileproxy import DEFAULT_TILE_LAYERS, extract_tile_layers
+
+ return [
+ source
+ for source in chain(
+ (
+ default_layer
+ for default_layer in DEFAULT_TILE_LAYERS
+ if default_layer.layer_id in self.default_tile_layers
+ ),
+ extract_tile_layers(self),
+ )
+ if source.access == LayerAccess.PUBLIC
+ ]
+
def parse(config: dict) -> Config:
"""Parses the configuration into a :class:`Config`.
@@ -325,10 +349,17 @@ def parse(config: dict) -> Config:
keys.discard(_field_name(field_name, field))
keys -= KNOWN_PYRAMID_SETTINGS
+ _warn_unknown(keys)
+
+ return parsed_config
+
+
+def _warn_unknown(keys):
for key in keys:
LOGGER.warning("Unknown configuration key: %r", key)
- return parsed_config
+ if warning := WARNINGS.get(key):
+ LOGGER.warning(warning)
def _field_name(field_name, field):
diff --git a/fietsboek/convert.py b/fietsboek/convert.py
index 3c8208b..5d7b43e 100644
--- a/fietsboek/convert.py
+++ b/fietsboek/convert.py
@@ -1,11 +1,27 @@
"""Conversion functions to convert between various recording formats."""
+import datetime
+
import fitparse
-from gpxpy.gpx import GPX, GPXTrack, GPXTrackPoint, GPXTrackSegment
+import gpxpy
+
+from . import geo, util
+from .models import Track, Waypoint
FIT_RECORD_FIELDS = ["position_lat", "position_long", "altitude", "timestamp"]
+class ConversionError(Exception):
+ """Error that occurred when loading a track from a file."""
+
+
+class UnknownFormat(ConversionError):
+ """The format of the source file could not be identified."""
+
+ def __str__(self):
+ return type(self).__doc__
+
+
def semicircles_to_deg(circles: int) -> float:
"""Convert semicircles coordinate to degree coordinate.
@@ -15,8 +31,8 @@ def semicircles_to_deg(circles: int) -> float:
return circles * (180 / 2**31)
-def from_fit(data: bytes) -> GPX:
- """Reads a .fit as GPX data.
+def from_fit(data: bytes) -> Track:
+ """Reads a .fit as track data.
This uses the fitparse_ library under the hood.
@@ -24,29 +40,127 @@ def from_fit(data: bytes) -> GPX:
:param data: The input bytes.
:return: The converted structure.
+ :raises ConversionError: If conversion failed.
"""
fitfile = fitparse.FitFile(data)
+ start_time = None
points = []
for record in fitfile.get_messages("record"):
values = record.get_values()
try:
if any(values[field] is None for field in FIT_RECORD_FIELDS):
continue
- point = GPXTrackPoint(
+ time = values["timestamp"]
+ if start_time is None:
+ start_time = time
+ point = geo.Point(
latitude=semicircles_to_deg(values["position_lat"]),
longitude=semicircles_to_deg(values["position_long"]),
elevation=values["altitude"],
- time=values["timestamp"],
+ time_offset=(time - start_time).total_seconds(),
)
except KeyError:
pass
else:
points.append(point)
- track = GPXTrack()
- track.segments = [GPXTrackSegment(points)]
- gpx = GPX()
- gpx.tracks = [track]
- return gpx
+ path = geo.Path(points)
+ track = Track()
+ track.set_path(path)
+ track.date = start_time
+ return track
+
+
+def from_gpx(data: bytes) -> Track:
+ """Reads a .gpx as track data.
+
+ This uses the gpxpy_ library under the hood.
+
+ .. _gpxpy: https://github.com/tkrajina/gpxpy
+
+ :param data: The input bytes.
+ :return: The converted structure.
+ :raises ConversionError: If conversion failed.
+ """
+ # pylint: disable=too-many-locals
+ gpx = gpxpy.parse(data)
+ points = []
+ start_time = None
+
+ for track in gpx.tracks:
+ for segment in track.segments:
+ for point in segment.points:
+ if start_time is None:
+ start_time = point.time
+
+ if point.time is not None and start_time is not None:
+ time_offset = (point.time - start_time).total_seconds()
+ else:
+ time_offset = 0
+ points.append(
+ geo.Point(
+ longitude=point.longitude,
+ latitude=point.latitude,
+ elevation=point.elevation or 0.0,
+ time_offset=time_offset,
+ )
+ )
+
+ timezone = util.guess_gpx_timezone(gpx)
+ date = gpx.time or gpx.get_time_bounds().start_time or datetime.datetime.now()
+ date = date.astimezone(timezone)
+ track_name = gpx.name
+ track_desc = gpx.description
+ for track in gpx.tracks:
+ if not track_name and track.name:
+ track_name = track.name
+ if not track_desc and track.description:
+ track_desc = track.description
+
+ path = geo.Path(points)
+ track = Track()
+ track.set_path(path)
+ track.title = track_name
+ track.description = track_desc
+ track.date = date
+
+ for waypoint in gpx.waypoints:
+ desc = None
+ # GPX waypoints can have both description and comment. It seems like
+ # comment is what is usually used (GPXViewer only shows the comment),
+ # so we'll prioritize that.
+ if waypoint.comment:
+ desc = waypoint.comment
+ if not desc and waypoint.description:
+ desc = waypoint.description
+ wpt = Waypoint(
+ longitude=waypoint.longitude,
+ latitude=waypoint.latitude,
+ elevation=waypoint.elevation,
+ name=waypoint.name,
+ description=desc,
+ )
+ track.waypoints.append(wpt)
+
+ return track
+
+
+def smart_convert(data: bytes) -> Track:
+ """Tries to be smart in converting the input bytes.
+
+ This function automatically applies the correct conversion if possible.
+
+ Note that this function is not guaranteed to return valid GPX bytes. In the worst case,
+ invalid bytes are simply passed through.
+
+ :param data: The input bytes.
+ :return: The converted content.
+ :raises ConversionError: When conversion fails.
+ """
+ if len(data) > 11 and data[9:12] == b"FIT":
+ return from_fit(data)
+ if data.startswith(b"<?xml") and b"<gpx" in data[:200]:
+ return from_gpx(data)
+ raise UnknownFormat()
-__all__ = ["from_fit"]
+__all__ = ["ConversionError", "from_fit", "from_gpx", "smart_convert"]
diff --git a/fietsboek/data.py b/fietsboek/data.py
index a7e9b19..a6492c3 100644
--- a/fietsboek/data.py
+++ b/fietsboek/data.py
@@ -7,21 +7,19 @@ the database itself. This module makes access to such data objects easier.
# We don't have onexc yet in all supported versions, so let's ignore the
# deprecation for now and stick with onerror:
# pylint: disable=deprecated-argument
-import datetime
import logging
-import os
import random
import shutil
import string
import uuid
from pathlib import Path
-from typing import BinaryIO, Literal, Optional
+from typing import BinaryIO, Optional
import brotli
-import gpxpy
from filelock import FileLock
from . import util
+from .fstrans import Transaction
LOGGER = logging.getLogger(__name__)
@@ -44,6 +42,10 @@ def generate_filename(filename: Optional[str]) -> str:
return str(uuid.uuid4())
+def _log_deletion_error(_, path, exc_info):
+ LOGGER.warning("Failed to remove %s", path, exc_info=exc_info)
+
+
class DataManager:
"""Data manager.
@@ -53,8 +55,9 @@ class DataManager:
:ivar data_dir: Path to the data folder.
"""
- def __init__(self, data_dir: Path):
+ def __init__(self, data_dir: Path, *, txn: Transaction | None = None):
self.data_dir: Path = data_dir
+ self.txn = txn
def _track_data_dir(self, track_id):
return self.data_dir / "tracks" / str(track_id)
@@ -62,6 +65,9 @@ class DataManager:
def _user_data_dir(self, user_id):
return self.data_dir / "users" / str(user_id)
+ def _journey_data_dir(self, journey_id):
+ return self.data_dir / "journeys" / str(journey_id)
+
def maintenance_mode(self) -> Optional[str]:
"""Checks whether the maintenance mode is enabled.
@@ -84,8 +90,11 @@ class DataManager:
:return: The manager that can be used to manage this track's data.
"""
path = self._track_data_dir(track_id)
- path.mkdir(parents=True)
- return TrackDataDir(track_id, path, journal=True, is_fresh=True)
+ if self.txn:
+ self.txn.make_dir(path)
+ else:
+ path.mkdir(parents=True)
+ return TrackDataDir(track_id, path, txn=self.txn)
def initialize_user(self, user_id: int) -> "UserDataDir":
"""Creates the data directory for a user.
@@ -95,8 +104,22 @@ class DataManager:
:return: The manager that can be used to manage this user's data.
"""
path = self._user_data_dir(user_id)
+ if self.txn:
+ self.txn.make_dir(path)
+ else:
+ path.mkdir(parents=True)
+ return UserDataDir(user_id, path, txn=self.txn)
+
+ def initialize_journey(self, journey_id: int) -> "JourneyDataDir":
+ """Creates the data directory for a journey.
+
+ :raises FileExistsError: If the directory already exists.
+ :param journey_id: ID of the journey.
+ :return: The manager that can be used to manage this journey's data.
+ """
+ path = self._journey_data_dir(journey_id)
path.mkdir(parents=True)
- return UserDataDir(user_id, path)
+ return JourneyDataDir(journey_id, path)
def purge(self, track_id: int):
"""Forcefully purges all data from the given track.
@@ -104,9 +127,9 @@ class DataManager:
This function logs errors but raises no exception, as such it can
always be used to clean up after a track.
"""
- TrackDataDir(track_id, self._track_data_dir(track_id)).purge()
+ TrackDataDir(track_id, self._track_data_dir(track_id), txn=self.txn).purge()
- def open(self, track_id: int) -> "TrackDataDir":
+ def open(self, track_id: int, *, force: bool = False) -> "TrackDataDir":
"""Opens a track's data directory.
:raises FileNotFoundError: If the track directory does not exist.
@@ -114,9 +137,9 @@ class DataManager:
:return: The manager that can be used to manage this track's data.
"""
path = self._track_data_dir(track_id)
- if not path.is_dir():
+ if not force and not path.is_dir():
raise FileNotFoundError(f"The path {path} is not a directory") from None
- return TrackDataDir(track_id, path)
+ return TrackDataDir(track_id, path, txn=self.txn)
def open_user(self, user_id: int) -> "UserDataDir":
"""Opens a user's data directory.
@@ -128,98 +151,71 @@ class DataManager:
path = self._user_data_dir(user_id)
if not path.is_dir():
raise FileNotFoundError(f"The path {path} is not a directory") from None
- return UserDataDir(user_id, path)
+ return UserDataDir(user_id, path, txn=self.txn)
+ def open_journey(self, journey_id: int) -> "JourneyDataDir":
+ """Open a journey's data directory.
-class TrackDataDir:
- """Manager for a single track's data.
+ :raises FileNotFoundError: If the journey directory does not exist.
+ :param journey_id: ID of the journey.
+ :return: The manager that can be used to manage this journey's data.
+ """
+ path = self._journey_data_dir(journey_id)
+ if not path.is_dir():
+ raise FileNotFoundError(f"The path {path} is not a directory") from None
+ return JourneyDataDir(journey_id, path)
- If initialized with ``journal = True``, then you can use :meth:`rollback`
- to roll back the changes in case of an error. In case of no error, use
- :meth:`commit` to commit the changes. If you don't want the "journalling"
- semantics, use ``journal = False``.
- """
+ def size(self) -> int:
+ """Returns the size of all data.
- def __init__(self, track_id: int, path: Path, *, journal: bool = False, is_fresh: bool = False):
- self.track_id: int = track_id
- self.path: Path = path
- self.journal: Optional[list] = [] if journal else None
- self.is_fresh = is_fresh
+ :return: The size of all data in bytes.
+ """
+ return util.recursive_size(self.data_dir)
- def __enter__(self) -> "TrackDataDir":
- if self.journal is None:
- self.journal = []
- return self
+ def list_tracks(self) -> list[int]:
+ """Returns a list of all tracks.
- def __exit__(self, exc_type, exc_val, exc_tb) -> Literal[False]:
- if exc_type is None and exc_val is None and exc_tb is None:
- self.commit()
- else:
- self.rollback()
- return False
+ :return: A list of all track IDs.
+ """
+ try:
+ return [int(track.name) for track in self._track_data_dir(".").iterdir()]
+ except FileNotFoundError:
+ return []
- def rollback(self):
- """Rolls back the journal, e.g. in case of error.
+ def list_users(self) -> list[int]:
+ """Returns a list of all users.
- :raises ValueError: If the data directory was opened without the
- journal, this raises :exc:`ValueError`.
+ :return: A list of all user IDs.
"""
- LOGGER.debug("Rolling back state of %s", self.path)
-
- if self.journal is None:
- raise ValueError("Rollback on a non-journalling data directory")
+ try:
+ return [int(user.name) for user in self._user_data_dir(".").iterdir()]
+ except FileNotFoundError:
+ return []
- if self.is_fresh:
- # Shortcut if the directory is fresh, simply remove everything
- self.journal = None
- self.purge()
- return
+ def list_journeys(self) -> list[int]:
+ """Returns a list of all journeys.
- for action, *rest in reversed(self.journal):
- if action == "purge":
- (new_name,) = rest
- shutil.move(new_name, self.path)
- elif action == "compress_gpx":
- (old_data,) = rest
- if old_data is None:
- self.gpx_path().unlink()
- else:
- self.gpx_path().write_bytes(old_data)
- elif action == "add_image":
- (image_path,) = rest
- image_path.unlink()
- elif action == "delete_image":
- path, data = rest
- path.write_bytes(data)
-
- self.journal = None
-
- def commit(self):
- """Commits all changed and deletes the journal.
-
- Note that this function will do nothing if the journal is disabled,
- meaning it can always be called.
+ :return: A list of all journey IDs.
"""
- LOGGER.debug("Committing journal for %s", self.path)
+ try:
+ return [int(journey.name) for journey in self._journey_data_dir(".").iterdir()]
+ except FileNotFoundError:
+ return []
- if self.journal is None:
- return
- for action, *rest in reversed(self.journal):
- if action == "purge":
- (new_name,) = rest
- shutil.rmtree(new_name, ignore_errors=False, onerror=self._log_deletion_error)
- elif action == "compress_gpx":
- # Nothing to do here, the new data is already on the disk
- pass
- elif action == "add_image":
- # Nothing to do here, the image is already saved
- pass
- elif action == "delete_image":
- # Again, nothing to do here, we simply discard the in-memory image data
- pass
-
- self.journal = None
+class TrackDataDir:
+ """Manager for a single track's data.
+
+ If initialized with ``journal = True``, then you can use :meth:`rollback`
+ to roll back the changes in case of an error. In case of no error, use
+ :meth:`commit` to commit the changes. If you don't want the "journalling"
+ semantics, use ``journal = False``.
+ """
+
+ def __init__(self, track_id: int, path: Path, *, txn: Transaction | None = None):
+ self.track_id: int = track_id
+ self.path: Path = path
+ self.txn = txn
def lock(self) -> FileLock:
"""Returns a FileLock that can be used to lock access to the track's
@@ -229,121 +225,54 @@ class TrackDataDir:
"""
return FileLock(self.path / "lock")
- @staticmethod
- def _log_deletion_error(_, path, exc_info):
- LOGGER.warning("Failed to remove %s", path, exc_info=exc_info)
-
def purge(self):
"""Purge all data pertaining to the track.
This function logs errors but raises no exception, as such it can
always be used to clean up after a track.
"""
- if self.journal is None:
- if self.path.is_dir():
- shutil.rmtree(self.path, ignore_errors=False, onerror=self._log_deletion_error)
+ if self.txn:
+ self.txn.purge(self.path)
else:
- new_name = self.path.with_name("trash-" + self.path.name)
- shutil.move(self.path, new_name)
- self.journal.append(("purge", new_name))
+ if self.path.is_dir():
+ shutil.rmtree(self.path, ignore_errors=False, onerror=_log_deletion_error)
def size(self) -> int:
"""Returns the size of the data that this track entails.
:return: The size of bytes that this track consumes.
"""
- size = 0
- for root, _, files in os.walk(self.path):
- size += sum(os.path.getsize(os.path.join(root, fname)) for fname in files)
- return size
-
- def gpx_path(self) -> Path:
- """Returns the path of the GPX file.
-
- This file contains the (brotli) compressed GPX data.
-
- :return: The path where the GPX is supposed to be.
- """
- return self.path / "track.gpx.br"
+ return util.recursive_size(self.path)
- def compress_gpx(self, data: bytes, quality: int = 4):
- """Set the GPX content to the compressed form of data.
+ def compress_backup(self, data: bytes, quality: int = 4):
+ """Set the content of the backup to the compressed form of data.
- If you want to write compressed data directly, use :meth:`gpx_path` to
+ If you want to write compressed data directly, use :meth:`backup_path` to
get the path of the GPX file.
:param data: The GPX data (uncompressed).
:param quality: Compression quality, from 0 to 11 - 11 is highest
quality but slowest compression speed.
"""
- if self.journal is not None:
- # First, we check if we already saved an old state of the GPX data
- for action, *_ in self.journal:
- if action == "compress_gpx":
- break
- else:
- # We did not save a state yet
- old_data = None if not self.gpx_path().is_file() else self.gpx_path().read_bytes()
- self.journal.append(("compress_gpx", old_data))
-
compressed = brotli.compress(data, quality=quality)
- self.gpx_path().write_bytes(compressed)
+ if self.txn:
+ self.txn.write_bytes(self.backup_path(), compressed)
+ else:
+ self.backup_path().write_bytes(compressed)
- def decompress_gpx(self) -> bytes:
- """Returns the GPX bytes decompressed.
+ def decompress_backup(self) -> bytes:
+ """Returns the backup bytes decompressed.
:return: The saved GPX file, decompressed.
"""
- return brotli.decompress(self.gpx_path().read_bytes())
-
- def engrave_metadata(
- self,
- title: Optional[str],
- description: Optional[str],
- author_name: Optional[str],
- time: Optional[datetime.datetime],
- *,
- gpx: Optional[gpxpy.gpx.GPX] = None,
- ):
- """Engrave the given metadata into the GPX file.
-
- Note that this will overwrite all existing metadata in the given
- fields.
-
- If ``None`` is given, it will erase that specific part of the metadata.
-
- :param title: The title of the track.
- :param description: The description of the track.
- :param creator: Name of the track's creator.
- :param time: Time of the track.
- :param gpx: The pre-parsed GPX track, to save time if it is already parsed.
- """
- # pylint: disable=too-many-arguments
- if gpx is None:
- gpx = gpxpy.parse(self.decompress_gpx())
- # First we delete the existing metadata
- for track in gpx.tracks:
- track.name = None
- track.description = None
-
- # Now we add the new metadata
- gpx.author_name = author_name
- gpx.name = title
- gpx.description = description
- gpx.time = time
-
- self.compress_gpx(util.encode_gpx(gpx))
-
- def backup(self):
- """Create a backup of the GPX file."""
- shutil.copy(self.gpx_path(), self.backup_path())
+ return brotli.decompress(self.backup_path().read_bytes())
def backup_path(self) -> Path:
"""Path of the GPX backup file.
:return: The path of the backup file.
"""
- return self.path / "track.bck.gpx.br"
+ return self.path / "track.bck.br"
def images(self) -> list[str]:
"""Returns a list of images that belong to the track.
@@ -377,15 +306,18 @@ class TrackDataDir:
:return: The ID of the saved image.
"""
image_dir = self.path / "images"
- image_dir.mkdir(parents=True, exist_ok=True)
+ if self.txn:
+ self.txn.make_dir(image_dir, exist_ok=True)
+ else:
+ image_dir.mkdir(parents=True, exist_ok=True)
filename = generate_filename(filename)
path = image_dir / filename
- with open(path, "wb") as fobj:
- shutil.copyfileobj(image, fobj)
-
- if self.journal is not None:
- self.journal.append(("add_image", path))
+ if self.txn:
+ self.txn.write_bytes(path, image.read())
+ else:
+ with open(path, "wb") as fobj:
+ shutil.copyfileobj(image, fobj)
return filename
@@ -401,18 +333,36 @@ class TrackDataDir:
return
path = self.image_path(image_id)
- if self.journal is not None:
- self.journal.append(("delete_image", path, path.read_bytes()))
+ if self.txn:
+ self.txn.unlink(path)
+ else:
+ path.unlink()
- path.unlink()
+ def preview_path(self) -> Path:
+ """Gets the path to the "preview image".
+
+ :return: The path to the preview image.
+ """
+ return self.path / "preview.png"
+
+ def set_preview(self, data: bytes):
+ """Sets the preview image to the given data.
+
+ :param data: The data of the preview image.
+ """
+ if self.txn:
+ self.txn.write_bytes(self.preview_path(), data)
+ else:
+ self.preview_path().write_bytes(data)
class UserDataDir:
"""Manager for a single user's data."""
- def __init__(self, user_id: int, path: Path):
+ def __init__(self, user_id: int, path: Path, *, txn: Transaction | None = None):
self.user_id = user_id
self.path = path
+ self.txn = txn
def heatmap_path(self) -> Path:
"""Returns the path for the heatmap tile file.
@@ -429,4 +379,51 @@ class UserDataDir:
return self.path / "tilehunt.sqlite"
-__all__ = ["generate_filename", "DataManager", "TrackDataDir", "UserDataDir"]
+class JourneyDataDir:
+ """Manager for a single journey's data."""
+
+ def __init__(self, journey_id: int, path: Path, *, txn: Transaction | None = None):
+ self.journey_id = journey_id
+ self.path = path
+ self.txn = txn
+
+ def purge(self):
+ """Purge all data pertaining to the journey.
+
+ This function logs errors but raises no exception, as such it can
+ always be used to clean up after a track.
+ """
+ if self.txn:
+ self.txn.purge(self.path)
+ else:
+ if self.path.is_dir():
+ shutil.rmtree(self.path, ignore_errors=False, onerror=_log_deletion_error)
+
+ def preview_path(self) -> Path:
+ """Gets the path to the "preview image".
+
+ :return: The path to the preview image.
+ """
+ return self.path / "preview.png"
+
+ def set_preview(self, data: bytes):
+ """Sets the preview image to the given data.
+
+ :param data: The data of the preview image.
+ """
+ if self.txn:
+ self.txn.write_bytes(self.preview_path(), data)
+ else:
+ self.preview_path().write_bytes(data)
+
+ def remove_preview(self):
+ """Deletes the preview image."""
+ if not self.preview_path().exists():
+ return
+ if self.txn:
+ self.txn.unlink(self.preview_path())
+ else:
+ self.preview_path().unlink()
+
+
+__all__ = ["generate_filename", "DataManager", "TrackDataDir", "UserDataDir", "JourneyDataDir"]
diff --git a/fietsboek/fstrans.py b/fietsboek/fstrans.py
new file mode 100644
index 0000000..d402266
--- /dev/null
+++ b/fietsboek/fstrans.py
@@ -0,0 +1,417 @@
+"""Filesystem transactions.
+
+Motivation
+##########
+
+Fietsboek does a lot of filesystem stuff, such as saving images and track
+backups. Like database actions, we want to ensure that these actions happen
+"atomically" -- if an error occurs during one action, we want to undo the
+previous ones. Similarly, if an error occurs after things have been sent to the
+database/filesystem, we want to ensure that we "clean up" (see `issue 98`_).
+
+.. _issue 98: https://gitlab.com/dunj3/fietsboek/-/issues/98
+
+By having "transactionized" file system actions, we can ensure that we do not
+not have such issues:
+
+* Actions are reversible in case changes need to be rolled back.
+* Actions are done all-or-nothing.
+* Transactions are combined with other transactions (such as SQLAlchemy); if
+ one fails, the others will be rolled back.
+
+Implementation
+##############
+
+The main mechanism is :class:`Transaction`. It provides the commit/abort
+interface as required by the transaction_ module, and user-facing methods to
+enqueue filesystem modifications.
+
+.. _transaction: https://github.com/zopefoundation/transaction
+
+A transaction is started by :func:`begin`, which also joins the transaction
+to the other transactions managed by ``transaction``.
+
+A transaction records :class:`Action`, which represent modifications to be done
+to the filesystem. Each action knows how it should be executed. Additionally,
+each action records how to undo it -- the :class:`WriteBytes` action, for
+example, records the previous file state so it can be restored.
+
+This implementation has some drawbacks:
+
+* Modifications are kept in-memory, which might be an issue for larger files.
+* Extra time is needed to record previous states (read a file before it is
+ overwritten).
+* Changes are not visible to other programs until they are committed.
+
+But the advantage is the ease of implementation: Cancelling a transaction just
+involves clearing the in-memory buffer, and there are no additional temporary
+files needed.
+
+To further ensure that the filesystem is in a consistent state, the
+transactions use a lock file to get exclusive access. Currently, this lock
+spans the complete data directory. In the future, more fine-grained locking can
+be implemented.
+
+Usage
+#####
+
+The transaction is automatically used by the
+:class:`fietsboek.data.DataManager`.
+
+Interface
+#########
+"""
+
+import enum
+import logging
+import shutil
+import uuid
+from pathlib import Path
+
+import transaction # type: ignore
+from filelock import FileLock
+
+LOGGER = logging.getLogger(__name__)
+
+
+def _log_deletion_error(_, path, exc_info):
+ LOGGER.warning("Failed to remove %s", path, exc_info=exc_info)
+
+
+class TransactionalError(Exception):
+ """An exception that occurs when committing filesystem transactions."""
+
+
+class State(enum.Enum):
+ """State of the transaction."""
+
+ OPEN = enum.auto()
+ """Transaction is open for further actions."""
+
+ COMMITTING = enum.auto()
+ """Transaction is in the process of being committed."""
+
+ COMMITTED = enum.auto()
+ """Transaction has been committed."""
+
+ TAINTED = enum.auto()
+ """Transaction is tainted."""
+
+
+class Action:
+ """Base class for any actions that can be applied to the filesystem."""
+
+ def commit_1(self):
+ """Commit this action (phase 1).
+
+ This corresponds to ``tpc_vote``, and may raise an exception if
+ committing should be cancelled.
+ """
+
+ def commit_2(self):
+ """Commit this action (phase 2).
+
+ This corresponds to ``tpc_finish``, and should not raise an exception.
+ """
+
+ def undo(self):
+ """Undo this action.
+
+ This is called if commiting fails, to undo any changes that were
+ already committed.
+ """
+
+
+class WriteBytes(Action):
+ """Write bytes to the given file."""
+
+ def __init__(self, path: Path, data: bytes):
+ self.path = path
+ self.data = data
+ self.old: bytes | None = None
+
+ def __repr__(self):
+ return f"<WriteBytes {len(self.data)} to {self.path}>"
+
+ def commit_1(self):
+ try:
+ self.old = self.path.read_bytes()
+ except FileNotFoundError:
+ self.old = None
+ self.path.write_bytes(self.data)
+
+ def undo(self):
+ if self.old is None:
+ self.path.unlink()
+ else:
+ self.path.write_bytes(self.old)
+
+
+class Unlink(Action):
+ """Remove the given file."""
+
+ def __init__(self, path: Path):
+ self.path = path
+ self.old: bytes | None = None
+
+ def __repr__(self):
+ return f"<Unlink {self.path}>"
+
+ def commit_1(self):
+ self.old = self.path.read_bytes()
+ self.path.unlink()
+
+ def undo(self):
+ # This should not happen, unless an exception occurs when we read the
+ # file
+ if self.old is not None:
+ self.path.write_bytes(self.old)
+
+
+class MakeDir(Action):
+ """Create the given directory."""
+
+ def __init__(self, path: Path, exist_ok: bool = False):
+ self.path = path
+ self.exist_ok = exist_ok
+ self.existed: bool | None = None
+
+ def __repr__(self):
+ return f"<MakeDir {self.path}>"
+
+ def commit_1(self):
+ self.existed = self.path.is_dir()
+ self.path.mkdir(exist_ok=self.exist_ok)
+
+ def undo(self):
+ if not self.existed:
+ self.path.rmdir()
+
+
+class RemoveDir(Action):
+ """Remove the given (empty) directory."""
+
+ def __init__(self, path: Path):
+ self.path = path
+
+ def __repr__(self):
+ return f"<RemoveDir {self.path}>"
+
+ def commit_1(self):
+ self.path.rmdir()
+
+ def undo(self):
+ self.path.mkdir()
+
+
+class Purge(Action):
+ """Purge (recursively remove) the given directory."""
+
+ def __init__(self, path: Path):
+ self.path = path
+
+ def __repr__(self):
+ return f"<Purge {self.path}>"
+
+ def commit_2(self):
+ # pylint: disable=deprecated-argument
+ shutil.rmtree(self.path, ignore_errors=False, onerror=_log_deletion_error)
+
+
+class Transaction:
+ """A transaction, recording pending filesystem changes."""
+
+ def __init__(self, lock_path: Path):
+ self.actions: list[Action] = []
+ self.actions_done: list[Action] = []
+ self.state = State.OPEN
+ self.lock_path = lock_path
+ self.id = uuid.uuid4().hex
+
+ def tpc_begin(self, _trans):
+ """Begin the transaction.
+
+ This is required by the two-phase commit protocol of ``transaction``.
+ """
+
+ def tpc_vote(self, _trans):
+ """Commit (phase 1) the pending transaction.
+
+ This is required by the two-phase commit protocol of ``transaction``.
+
+ This method may raise exceptions to signal that the transaction (and
+ all linked transactions) should be aborted.
+ """
+ if not self.actions:
+ return
+
+ with FileLock(self.lock_path):
+ self.commit_1()
+
+ def tpc_finish(self, _trans):
+ """Commit (phase 2) the pending transaction.
+
+ This is required by the two-phase commit protocol of ``transaction``.
+
+ This method should not raise an exception.
+ """
+ if not self.actions:
+ return
+
+ with FileLock(self.lock_path):
+ self.commit_2()
+
+ def tpc_abort(self, _trans):
+ """Abort the transaction, undoing all previously done changes.
+
+ This is required by the two-phase commit protocol of ``transaction``.
+ """
+ if not self.actions_done:
+ return
+
+ with FileLock(self.lock_path):
+ self.undo()
+
+ # Needs to conform to transaction API:
+ # pylint: disable=invalid-name
+ def sortKey(self):
+ """Returns the sort key to sort this transaction in relation to others."""
+ return f"filesystem:{self.id}"
+
+ def undo(self):
+ """Undo all actions that have already been applied."""
+ # pylint: disable=broad-exception-caught
+ for action in reversed(self.actions_done):
+ LOGGER.debug("Undoing %s", action)
+ try:
+ action.undo()
+ except Exception as exc_inner:
+ # Hide "during the handling of ... another exception occurred"
+ exc_inner.__context__ = None
+ LOGGER.exception(
+ "Exception ignored during rollback of %s",
+ action,
+ exc_info=exc_inner,
+ )
+
+ def commit(self, _trans):
+ """Commit this transaction.
+
+ This is required by the interface of ``transaction``.
+ """
+
+ def commit_1(self):
+ """Start the first phase of committing.
+
+ This method is called automatically by the transaction manager.
+ """
+ if self.state != State.OPEN:
+ raise TransactionalError(f"Transaction is not open but {self.state}")
+
+ self.state = State.COMMITTING
+
+ try:
+ for action in self.actions:
+ LOGGER.debug("Executing 1st phase %s", action)
+ action.commit_1()
+ self.actions_done.append(action)
+ except Exception as exc:
+ LOGGER.debug("Exception while committing")
+ self.state = State.TAINTED
+ raise exc
+
+ def commit_2(self):
+ """Start the second phase of committing.
+
+ This method is called automatically by the transaction manager.
+ """
+ if self.state != State.COMMITTING:
+ raise TransactionalError(f"Transaction is not committing but {self.state}")
+
+ self.state = State.TAINTED
+ for action in self.actions:
+ LOGGER.debug("Executing 2nd phase %s", action)
+ action.commit_2()
+ self.state = State.COMMITTED
+
+ def abort(self, _trans=None):
+ """Abort this transaction."""
+ self.actions.clear()
+ self.state = State.TAINTED
+
+ def write_bytes(self, path: Path, data: bytes):
+ """Write the given bytes to the given path.
+
+ This is a transactioned version of :meth:`Path.write_bytes`.
+
+ :param path: Path where to write the bytes to.
+ :param data: The data to write.
+ """
+ if self.state != State.OPEN:
+ raise TransactionalError(f"Transaction is not open but {self.state}")
+ self.actions.append(WriteBytes(path, data))
+
+ def unlink(self, path: Path):
+ """Unlinks (removes) the given file.
+
+ This is a transactioned version of :math:`Path.unlink`.
+
+ :param path: The path to the file to unlink.
+ """
+ if self.state != State.OPEN:
+ raise TransactionalError(f"Transaction is not open but {self.state}")
+ self.actions.append(Unlink(path))
+
+ def make_dir(self, path: Path, *, exist_ok: bool = False):
+ """Creates the directory.
+
+ This is a transactioned version of :meth:`Path.mkdir`.
+
+ :param path: The directory to create.
+ :param exist_ok: If ``True``, no error will be raised if the directory
+ already exists.
+ """
+ if self.state != State.OPEN:
+ raise TransactionalError(f"Transaction is not open but {self.state}")
+ self.actions.append(MakeDir(path, exist_ok=exist_ok))
+
+ def remove_dir(self, path: Path):
+ """Removes the (empty) directory.
+
+ This is a transactioned version of :meth:`Path.rmdir`.
+
+ :param path: The directory to remove.
+ """
+ if self.state != State.OPEN:
+ raise TransactionalError(f"Transaction is not open but {self.state}")
+ self.actions.append(RemoveDir(path))
+
+ def purge(self, path: Path):
+ """Completely remove (recursively) the given path.
+
+ This uses :func:`shutil.rmtree` to delete the path.
+
+ Unlike other actions, this cannot be undone!
+
+ :param path: The directory to remove.
+ """
+ if self.state != State.OPEN:
+ raise TransactionalError(f"Transaction is not open but {self.state}")
+ self.actions.append(Purge(path))
+
+
+def begin(lock_path: Path, tm=None) -> Transaction:
+ """Begin a new transaction and register it.
+
+ :param lock_path: The path to the lock file to use in order to synchronize
+ the transaction.
+ :param tm: The transaction manager from ``transaction`` in which to join
+ this transaction.
+ :return: The :class:`Transaction`.
+ """
+ trans = Transaction(lock_path)
+ if tm:
+ tm.get().join(trans)
+ else:
+ transaction.manager.get().join(trans)
+ return trans
diff --git a/fietsboek/geo.py b/fietsboek/geo.py
new file mode 100644
index 0000000..e6abb71
--- /dev/null
+++ b/fietsboek/geo.py
@@ -0,0 +1,236 @@
+"""This module implements GPS related functionality."""
+
+import datetime
+import io
+from dataclasses import dataclass
+from itertools import islice
+from math import cos, radians, sin, sqrt
+
+from . import util
+
+# WGS-84 equatorial radius, also called the semi-major axis.
+# https://en.wikipedia.org/wiki/Earth_radius
+EARTH_RADIUS = 6378137.0
+"""Radius of the earth, in meters."""
+
+# https://en.wikipedia.org/wiki/Preferred_walking_speed
+MOVING_THRESHOLD = 1.1
+"""Speed which is considered to be the moving threshold, in m/s."""
+
+
+@dataclass
+class Waypoint:
+ """A waypoint, a special landmark marked in the track."""
+
+ longitude: float
+ latitude: float
+ elevation: float | None
+ name: str | None
+ description: str | None
+
+
+@dataclass
+class MovementData:
+ """Movement statistics for a path."""
+
+ # pylint: disable=too-many-instance-attributes
+
+ duration: float = 0.0
+ """Duration of the path, in seconds."""
+
+ moving_duration: float = 0.0
+ """Duration spent moving, in seconds."""
+
+ stopped_duration: float = 0.0
+ """Duration spent stopped, in seconds."""
+
+ length: float = 0.0
+ """Length of the path, in meters."""
+
+ average_speed: float = 0.0
+ """Average speed, in m/s."""
+
+ maximum_speed: float = 0.0
+ """Maximum speed, in m/s."""
+
+ uphill: float = 0.0
+ """Uphill elevation, in meters."""
+
+ downhill: float = 0.0
+ """Downhill elevation, in meters."""
+
+
+@dataclass(slots=True)
+class Point:
+ """A GPS point, represented as longitude/latitude/elevation."""
+
+ longitude: float
+ latitude: float
+ elevation: float
+ time_offset: float
+
+ def distance(self, other: "Point") -> float:
+ """Returns the distance between this point and the given other point in
+ meters.
+ """
+ r_1 = EARTH_RADIUS + self.elevation
+ r_2 = EARTH_RADIUS + other.elevation
+ # The formula assumes that 0° is straight upward, but 0° in geo
+ # coordinates is actually on the equator plane.
+ t_1 = radians(90 - self.latitude)
+ t_2 = radians(90 - other.latitude)
+ p_1 = radians(self.longitude)
+ p_2 = radians(other.longitude)
+ # See
+ # https://en.wikipedia.org/wiki/Spherical_coordinate_system#Distance_in_spherical_coordinates
+ # While this is not the Haversine formula for distances along the
+ # circle curvature, it allows us to take the elevation into account,
+ # and for most GPS point differences that we encounter it should be
+ # enough.
+ radicand = (
+ r_1**2
+ + r_2**2
+ - 2 * r_1 * r_2 * (sin(t_1) * sin(t_2) * cos(p_1 - p_2) + cos(t_1) * cos(t_2))
+ )
+ if radicand < 0.0:
+ return 0.0
+ return sqrt(radicand)
+
+ def flat_distance(self, other: "Point") -> float:
+ """Returns the distance between this point and the other point in
+ meters.
+
+ This does not take elevation into account, and only looks at the 2d distance.
+ """
+ r = EARTH_RADIUS
+ # The formula assumes that 0° is straight upward, but 0° in geo
+ # coordinates is actually on the equator plane.
+ t_1 = radians(90 - self.latitude)
+ t_2 = radians(90 - other.latitude)
+ p_1 = radians(self.longitude)
+ p_2 = radians(other.longitude)
+ # See
+ # https://en.wikipedia.org/wiki/Spherical_coordinate_system#Distance_in_spherical_coordinates
+ # While this is not the Haversine formula for distances along the
+ # circle curvature, it allows us to take the elevation into account,
+ # and for most GPS point differences that we encounter it should be
+ # enough.
+ radicand = 2 * r**2 * (1 - (sin(t_1) * sin(t_2) * cos(p_1 - p_2) + cos(t_1) * cos(t_2)))
+ if radicand < 0.0:
+ return 0.0
+ return sqrt(radicand)
+
+
+class Path:
+ """A GPS path, that is a series of GPS points."""
+
+ # pylint: disable=too-few-public-methods
+
+ def __init__(self, points: list[Point]):
+ self.points = points
+
+ def _point_pairs(self):
+ return zip(self.points, islice(self.points, 1, None))
+
+ def movement_data(self) -> MovementData:
+ """Returns the movement data."""
+ movement_data = MovementData()
+ for a, b in self._point_pairs():
+ distance = a.distance(b)
+ time = b.time_offset - a.time_offset
+ if time != 0:
+ speed = distance / time
+ else:
+ speed = 0.0
+ elevation = b.elevation - a.elevation
+
+ movement_data.length += distance
+ if speed >= MOVING_THRESHOLD:
+ movement_data.moving_duration += time
+ else:
+ movement_data.stopped_duration += time
+ movement_data.maximum_speed = max(movement_data.maximum_speed, speed)
+ if elevation > 0.0:
+ movement_data.uphill += elevation
+ else:
+ movement_data.downhill += -elevation
+ movement_data.duration = b.time_offset
+
+ if movement_data.moving_duration > 0:
+ movement_data.average_speed = movement_data.length / movement_data.moving_duration
+ else:
+ movement_data.average_speed = 0.0
+ return movement_data
+
+
+def gpx_xml(
+ title: str | None,
+ description: str | None,
+ date: datetime.datetime,
+ points: list[Point],
+ waypoints: list[Waypoint],
+) -> bytes:
+ """Returns an XML representation of the given path.
+
+ :param title: The title of the resulting track.
+ :param description: The description of the resulting track.
+ :param points: The points that make up this track.
+ :param waypoints: The waypoints that should be included.
+ :return: The XML representation (a GPX file).
+ """
+ # This is a cumbersome way to do it, as we're re-implementing XML
+ # serialization logic. However, recreating the track in gpxpy and
+ # letting it serialize it is much slower:
+ # For a track with around 50,000 points, the gpxpy method takes
+ # ~5.9 seconds here, while the "manual" buffer takes only ~2.4 seconds.
+ # This is a speed-up we're happy to take!
+ buf = io.BytesIO()
+ buf.write(b'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>')
+ buf.write(b'<gpx version="1.1" xmlns="http://www.topografix.com/GPX/1/1">')
+
+ buf.write(b"<metadata>")
+ if title:
+ buf.write(b"<name>%s</name>" % util.xml_escape(title))
+ if description:
+ buf.write(b"<desc>%s</desc>" % util.xml_escape(description))
+ buf.write(b"</metadata>")
+
+ # Cache for fast access
+ write = buf.write
+
+ write(b"<trk>")
+ write(b"<trkseg>")
+ for point in points:
+ write(b'<trkpt lat="')
+ write(str(point.latitude).encode("ascii"))
+ write(b'" lon="')
+ write(str(point.longitude).encode("ascii"))
+ write(b'">')
+ write(b"<ele>")
+ write(str(point.elevation).encode("ascii"))
+ write(b"</ele>")
+ write(b"<time>")
+ write(str(date + datetime.timedelta(seconds=point.time_offset)).encode("ascii"))
+ write(b"</time>")
+ write(b"</trkpt>\n")
+ write(b"</trkseg>")
+ write(b"</trk>")
+
+ # This loop is not as hot:
+ for wpt in waypoints:
+ write(
+ b'<wpt lat="%s" lon="%s">'
+ % (util.xml_escape(str(wpt.latitude)), util.xml_escape(str(wpt.longitude)))
+ )
+ if wpt.elevation is not None:
+ write(b"<ele>%s</ele>" % util.xml_escape(str(wpt.elevation)))
+ if wpt.name is not None:
+ write(b"<name>%s</name>" % util.xml_escape(wpt.name))
+ if wpt.description is not None:
+ write(b"<cmt>%s</cmt>" % util.xml_escape(wpt.description))
+ write(b"<desc>%s</desc>" % util.xml_escape(wpt.description))
+ write(b"</wpt>")
+
+ write(b"</gpx>")
+
+ return buf.getvalue()
diff --git a/fietsboek/hittekaart.py b/fietsboek/hittekaart.py
index 15f2855..8a48d60 100644
--- a/fietsboek/hittekaart.py
+++ b/fietsboek/hittekaart.py
@@ -6,21 +6,25 @@
import enum
import logging
import shutil
-import subprocess
import tempfile
from pathlib import Path
-from typing import Optional
+try:
+ import hittekaart_py
+except ImportError:
+ pass
from sqlalchemy import select
from sqlalchemy.orm import aliased
from sqlalchemy.orm.session import Session
-from . import models
+from . import geo, models
from .data import DataManager
from .models.track import TrackType
LOGGER = logging.getLogger(__name__)
+TILEHUNTER_ZOOM = 14
+
class Mode(enum.Enum):
"""Heatmap generation mode.
@@ -36,9 +40,8 @@ class Mode(enum.Enum):
def generate(
output: Path,
mode: Mode,
- input_files: list[Path],
+ input_files: list[geo.Path],
*,
- exe_path: Optional[Path] = None,
threads: int = 0,
):
"""Calls hittekaart with the given arguments.
@@ -46,14 +49,32 @@ def generate(
:param output: Output filename. Note that this function always uses the
sqlite output mode.
:param mode: What to generate.
- :param input_files: List of paths to the input files.
- :param exe_path: Path to the hittekaart binary. If not given,
- ``hittekaart`` is searched in the path.
+ :param input_files: List of input paths.
:param threads: Number of threads that ``hittekaart`` should use. Defaults
to 0, which uses all available cores.
"""
+ try:
+ hittekaart_py
+ except NameError:
+ raise RuntimeError("hittekaart not available") from None
+
if not input_files:
return
+
+ renderer: hittekaart_py.HeatmapRenderer | hittekaart_py.TilehuntRenderer
+ if mode == Mode.HEATMAP:
+ renderer = hittekaart_py.HeatmapRenderer()
+ elif mode == Mode.TILEHUNTER:
+ renderer = hittekaart_py.TilehuntRenderer(TILEHUNTER_ZOOM)
+
+ LOGGER.debug("Loading tracks ...")
+ tracks = [
+ hittekaart_py.Track.from_coordinates(
+ [(point.longitude, point.latitude) for point in input_file.points]
+ )
+ for input_file in input_files
+ ]
+ LOGGER.debug("Tracks loaded!")
# There are two reasons why we do the tempfile dance:
# 1. hittekaart refuses to overwrite existing files
# 2. This way we can (hope for?) an atomic move (at least if temporary file
@@ -61,23 +82,12 @@ def generate(
# this, but for now, it's alright.
with tempfile.TemporaryDirectory() as tempdir:
tmpfile = Path(tempdir) / "hittekaart.sqlite"
- binary = str(exe_path) if exe_path else "hittekaart"
- cmdline = [
- binary,
- "--sqlite",
- "-o",
- str(tmpfile),
- "-m",
- mode.value,
- "-t",
- str(threads),
- "--",
- ]
- cmdline.extend(map(str, input_files))
- LOGGER.debug("Running %r", cmdline)
- subprocess.run(cmdline, check=True, stdout=subprocess.DEVNULL)
-
- LOGGER.debug("Moving temporary file")
+ sink = hittekaart_py.Storage.OsmAnd(bytes(tmpfile))
+ settings = hittekaart_py.Settings(threads=threads)
+ LOGGER.debug("Running hittekaart (renderer %r) to %r", renderer, tmpfile)
+ hittekaart_py.generate(settings, tracks, renderer, sink)
+
+ LOGGER.debug("Moving temporary file %r to %r", tmpfile, output)
shutil.move(tmpfile, output)
@@ -87,7 +97,6 @@ def generate_for(
data_manager: DataManager,
mode: Mode,
*,
- exe_path: Optional[Path] = None,
threads: int = 0,
):
"""Uses :meth:`generate` to generate a heatmap for the given user.
@@ -104,7 +113,6 @@ def generate_for(
:param dbsession: The database session.
:param data_manager: The data manager.
:param mode: The mode of the heatmap.
- :param exe_path: See :meth:`generate`.
:param threads: See :meth:`generate`.
"""
# pylint: disable=too-many-arguments
@@ -117,8 +125,7 @@ def generate_for(
for track in dbsession.execute(query).scalars():
if track.id is None:
continue
- path = data_manager.open(track.id).gpx_path()
- input_paths.append(path)
+ input_paths.append(track.path())
if not input_paths:
return
@@ -133,7 +140,7 @@ def generate_for(
Mode.TILEHUNTER: user_dir.tilehunt_path(),
}
- generate(output_paths[mode], mode, input_paths, exe_path=exe_path, threads=threads)
+ generate(output_paths[mode], mode, input_paths, threads=threads)
__all__ = ["Mode", "generate", "generate_for"]
diff --git a/fietsboek/locale/de/LC_MESSAGES/messages.mo b/fietsboek/locale/de/LC_MESSAGES/messages.mo
index 9de152c..ec717d2 100644
--- a/fietsboek/locale/de/LC_MESSAGES/messages.mo
+++ b/fietsboek/locale/de/LC_MESSAGES/messages.mo
Binary files differ
diff --git a/fietsboek/locale/de/LC_MESSAGES/messages.po b/fietsboek/locale/de/LC_MESSAGES/messages.po
index ccb46a3..3d6c4aa 100644
--- a/fietsboek/locale/de/LC_MESSAGES/messages.po
+++ b/fietsboek/locale/de/LC_MESSAGES/messages.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2025-01-30 21:50+0100\n"
+"POT-Creation-Date: 2026-08-13 17:40+0200\n"
"PO-Revision-Date: 2022-07-02 17:35+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: de\n"
@@ -16,87 +16,231 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 2.15.0\n"
+"Generated-By: Babel 2.18.0\n"
-#: fietsboek/actions.py:267
+#: fietsboek/actions.py:266
msgid "email.verify_mail.subject"
msgstr "Fietsboek Konto Bestätigung"
-#: fietsboek/actions.py:270
+#: fietsboek/actions.py:269
msgid "email.verify.text"
msgstr ""
"Um Dein Fietsboek-Konto zu bestätigen, nutze diesen Link: {}\n"
"\n"
"Falls Du kein Konto angelegt hast, ignoriere diese E-Mail."
-#: fietsboek/util.py:333
+#: fietsboek/pdf.py:233
+msgid "pdf.table.date"
+msgstr "Datum"
+
+#: fietsboek/pdf.py:235
+msgid "pdf.table.length"
+msgstr "Länge"
+
+#: fietsboek/pdf.py:239
+msgid "pdf.table.uphill"
+msgstr "Bergauf"
+
+#: fietsboek/pdf.py:243
+msgid "pdf.table.downhill"
+msgstr "Bergab"
+
+#: fietsboek/pdf.py:246
+msgid "pdf.table.moving_time"
+msgstr "Fahrzeit"
+
+#: fietsboek/pdf.py:247
+msgid "pdf.table.stopped_time"
+msgstr "Haltezeit"
+
+#: fietsboek/pdf.py:249
+msgid "pdf.table.max_speed"
+msgstr "Maximalgeschwindigkeit"
+
+#: fietsboek/pdf.py:253
+msgid "pdf.table.avg_speed"
+msgstr "Durchschnittsgeschwindigkeit"
+
+#: fietsboek/summaries.py:429
+msgid "page.profile.contribution_card.tooltip"
+msgid_plural "page.profile.contribution_card.tooltips"
+msgstr[0] "${count} Strecke am ${date}"
+msgstr[1] "${count} Strecken am ${date}"
+
+#: fietsboek/util.py:299
msgid "password_constraint.mismatch"
msgstr "Passwörter stimmen nicht überein"
-#: fietsboek/util.py:335
+#: fietsboek/util.py:301
msgid "password_constraint.length"
msgstr "Passwort zu kurz"
-#: fietsboek/models/track.py:603
+#: fietsboek/models/track.py:774
msgid "tooltip.table.length"
msgstr "Länge"
-#: fietsboek/models/track.py:604
+#: fietsboek/models/track.py:775
msgid "tooltip.table.people"
msgstr "# Personen"
-#: fietsboek/models/track.py:605
+#: fietsboek/models/track.py:776
msgid "tooltip.table.uphill"
msgstr "Bergauf"
-#: fietsboek/models/track.py:606
+#: fietsboek/models/track.py:777
msgid "tooltip.table.downhill"
msgstr "Bergab"
-#: fietsboek/models/track.py:607 fietsboek/templates/home.jinja2:7
+#: fietsboek/models/track.py:778 fietsboek/templates/home.jinja2:7
msgid "tooltip.table.moving_time"
msgstr "Fahrzeit"
-#: fietsboek/models/track.py:608 fietsboek/templates/home.jinja2:8
+#: fietsboek/models/track.py:779 fietsboek/templates/home.jinja2:8
msgid "tooltip.table.stopped_time"
msgstr "Haltezeit"
-#: fietsboek/models/track.py:610
+#: fietsboek/models/track.py:781
msgid "tooltip.table.max_speed"
msgstr "Maximalgeschwindigkeit"
-#: fietsboek/models/track.py:614
+#: fietsboek/models/track.py:785
msgid "tooltip.table.avg_speed"
msgstr "Durchschnittsgeschwindigkeit"
+#: fietsboek/templates/403.jinja2:5
+msgid "403.title"
+msgstr "Zugang verboten"
+
+#: fietsboek/templates/403.jinja2:9
+msgid "403.no_access"
+msgstr "Du hast keinen Zugang zu dieser Ressource."
+
+#: fietsboek/templates/403.jinja2:12
+msgid "403.try_log_in"
+msgstr ""
+"Falls Du Zugang haben solltest, stelle sicher, dass du korrekt angemeldet"
+" bist."
+
+#: fietsboek/templates/404.jinja2:5
+msgid "404.title"
+msgstr "Sackgasse"
+
+#: fietsboek/templates/404.jinja2:9
+msgid "404.path_not_found"
+msgstr "Der gesuchte Weg wurde nicht gefunden."
+
+#: fietsboek/templates/404.jinja2:12
+msgid "404.choose_different"
+msgstr "Bitte such einen anderen Weg."
+
#: fietsboek/templates/admin.jinja2:5
msgid "page.admin.title"
msgstr "Administration"
-#: fietsboek/templates/admin.jinja2:7
+#: fietsboek/templates/admin.jinja2:10
+msgid "page.admin.nav.overview"
+msgstr "Übersicht"
+
+#: fietsboek/templates/admin.jinja2:11
+msgid "page.admin.nav.badges"
+msgstr "Wappen"
+
+#: fietsboek/templates/admin_badges.jinja2:5
msgid "page.admin.badges"
msgstr "Wappen"
-#: fietsboek/templates/admin.jinja2:23
+#: fietsboek/templates/admin_badges.jinja2:21
msgid "page.admin.badge.edit"
msgstr "Bearbeiten"
-#: fietsboek/templates/admin.jinja2:29
+#: fietsboek/templates/admin_badges.jinja2:22
msgid "page.admin.badge.delete_badge"
msgstr "Löschen"
-#: fietsboek/templates/admin.jinja2:37
+#: fietsboek/templates/admin_badges.jinja2:35
msgid "page.admin.badges.badge_title"
msgstr "Titel"
-#: fietsboek/templates/admin.jinja2:41
+#: fietsboek/templates/admin_badges.jinja2:39
msgid "page.admin.badges.badge_image"
msgstr "Bild"
-#: fietsboek/templates/admin.jinja2:45
+#: fietsboek/templates/admin_badges.jinja2:43
msgid "page.admin.badges.add_badge"
msgstr "Hinzufügen"
+#: fietsboek/templates/admin_overview.jinja2:5
+msgid "admin.overview.instance_has"
+msgstr "Diese Instanz hat"
+
+#: fietsboek/templates/admin_overview.jinja2:9
+msgid "admin.overview.stat.user"
+msgid_plural "admin.overview.stat.users"
+msgstr[0] "%(num)d Nutzer:in"
+msgstr[1] "%(num)d Nutzer:innen"
+
+#: fietsboek/templates/admin_overview.jinja2:13
+msgid "admin.overview.stat.track"
+msgid_plural "admin.overview.stat.tracks"
+msgstr[0] "%(num)d Strecke"
+msgstr[1] "%(num)d Strecken"
+
+#: fietsboek/templates/admin_overview.jinja2:17
+msgid "admin.overview.stats.mib"
+msgstr "MiB an Daten"
+
+#: fietsboek/templates/admin_overview.jinja2:24
+msgid "admin.overview.system_overview"
+msgstr "Systemübersicht"
+
+#: fietsboek/templates/admin_overview.jinja2:28
+msgid "admin.overview.fietsboek_version"
+msgstr "Fietsboek-Version"
+
+#: fietsboek/templates/admin_overview.jinja2:32
+msgid "admin.overview.python_version"
+msgstr "Python-Version"
+
+#: fietsboek/templates/admin_overview.jinja2:36
+msgid "admin.overview.kernel_version"
+msgstr "Kernel-Version"
+
+#: fietsboek/templates/admin_overview.jinja2:40
+msgid "admin.overview.distro_version"
+msgstr "Distribution"
+
+#: fietsboek/templates/admin_overview.jinja2:44
+msgid "admin.overview.last_cronjob"
+msgstr "Letzter Cronjob"
+
+#: fietsboek/templates/admin_overview.jinja2:55
+msgid "admin.overview.storage_graph.label.track_data"
+msgstr "Streckendaten"
+
+#: fietsboek/templates/admin_overview.jinja2:56
+msgid "admin.overview.storage_graph.label.backups"
+msgstr "Sicherungskopien"
+
+#: fietsboek/templates/admin_overview.jinja2:57
+msgid "admin.overview.storage_graph.label.images"
+msgstr "Bilder"
+
+#: fietsboek/templates/admin_overview.jinja2:58
+msgid "admin.overview.storage_graph.label.track_previews"
+msgstr "Streckenvoransichten"
+
+#: fietsboek/templates/admin_overview.jinja2:59
+msgid "admin.overview.storage_graph.label.journey_previews"
+msgstr "Reisenvoransichten"
+
+#: fietsboek/templates/admin_overview.jinja2:60
+msgid "admin.overview.storage_graph.label.user_maps"
+msgstr "Nutzerkarten"
+
+#: fietsboek/templates/admin_overview.jinja2:88
+msgid "admin.overview.storage_graph.title"
+msgstr "Speicherübersicht"
+
#: fietsboek/templates/browse.jinja2:4
msgid "page.browse.title"
msgstr "Stöbern"
@@ -125,6 +269,18 @@ msgstr "Längenbeschränkungen"
msgid "page.browse.filter.length_maximum"
msgstr "Maximale Länge"
+#: fietsboek/templates/browse.jinja2:34
+msgid "page.browse.filter.uphill_minimum"
+msgstr "Minimaler Bergauf"
+
+#: fietsboek/templates/browse.jinja2:40
+msgid "page.browse.filter.uphill_boundaries"
+msgstr "Bergaufbeschränkungen"
+
+#: fietsboek/templates/browse.jinja2:46
+msgid "page.browse.filter.uphill_maximum"
+msgstr "Maximaler Bergauf"
+
#: fietsboek/templates/browse.jinja2:59
msgid "page.browse.filter.date_boundaries"
msgstr "Datumsgrenzen"
@@ -182,6 +338,10 @@ msgid "page.browse.sort.length"
msgstr "Nach Länge sortieren"
#: fietsboek/templates/browse.jinja2:125 fietsboek/templates/browse.jinja2:126
+msgid "page.browse.sort.uphill"
+msgstr "Nach Bergauf sortieren"
+
+#: fietsboek/templates/browse.jinja2:127 fietsboek/templates/browse.jinja2:128
msgid "page.browse.sort.duration"
msgstr "Nach Dauer sortieren"
@@ -193,73 +353,100 @@ msgstr "Dies ist eine Aufnahme einer Strecke"
msgid "page.browse.synthetic_tooltip"
msgstr "Dies ist eine geplante Strecke"
-#: fietsboek/templates/browse.jinja2:158 fietsboek/templates/details.jinja2:103
-#: fietsboek/templates/profile.jinja2:15
+#: fietsboek/templates/browse.jinja2:162 fietsboek/templates/details.jinja2:127
+#: fietsboek/templates/journey_details.jinja2:124
+#: fietsboek/templates/profile_overview.jinja2:20
msgid "page.details.date"
msgstr "Datum"
-#: fietsboek/templates/browse.jinja2:160 fietsboek/templates/details.jinja2:117
-#: fietsboek/templates/profile.jinja2:17
+#: fietsboek/templates/browse.jinja2:164 fietsboek/templates/details.jinja2:141
+#: fietsboek/templates/journey_details.jinja2:76
+#: fietsboek/templates/journey_details.jinja2:126
+#: fietsboek/templates/profile_overview.jinja2:22
msgid "page.details.length"
msgstr "Länge"
-#: fietsboek/templates/browse.jinja2:165 fietsboek/templates/details.jinja2:108
-#: fietsboek/templates/profile.jinja2:21
+#: fietsboek/templates/browse.jinja2:169 fietsboek/templates/details.jinja2:132
+#: fietsboek/templates/journey_details.jinja2:131
+#: fietsboek/templates/profile_overview.jinja2:26
msgid "page.details.start_time"
msgstr "Startzeit"
-#: fietsboek/templates/browse.jinja2:167 fietsboek/templates/details.jinja2:112
-#: fietsboek/templates/profile.jinja2:23
+#: fietsboek/templates/browse.jinja2:171 fietsboek/templates/details.jinja2:136
+#: fietsboek/templates/journey_details.jinja2:133
+#: fietsboek/templates/profile_overview.jinja2:28
msgid "page.details.end_time"
msgstr "Endzeit"
-#: fietsboek/templates/browse.jinja2:172 fietsboek/templates/details.jinja2:121
-#: fietsboek/templates/profile.jinja2:27
+#: fietsboek/templates/browse.jinja2:176 fietsboek/templates/details.jinja2:145
+#: fietsboek/templates/journey_details.jinja2:80
+#: fietsboek/templates/journey_details.jinja2:138
+#: fietsboek/templates/profile_overview.jinja2:32
msgid "page.details.uphill"
msgstr "Bergauf"
-#: fietsboek/templates/browse.jinja2:174 fietsboek/templates/details.jinja2:125
-#: fietsboek/templates/profile.jinja2:29
+#: fietsboek/templates/browse.jinja2:178 fietsboek/templates/details.jinja2:149
+#: fietsboek/templates/journey_details.jinja2:84
+#: fietsboek/templates/journey_details.jinja2:140
+#: fietsboek/templates/profile_overview.jinja2:34
msgid "page.details.downhill"
msgstr "Bergab"
-#: fietsboek/templates/browse.jinja2:179 fietsboek/templates/details.jinja2:130
-#: fietsboek/templates/profile.jinja2:33
+#: fietsboek/templates/browse.jinja2:183 fietsboek/templates/details.jinja2:154
+#: fietsboek/templates/journey_details.jinja2:88
+#: fietsboek/templates/journey_details.jinja2:145
+#: fietsboek/templates/profile_overview.jinja2:38
msgid "page.details.moving_time"
msgstr "Fahrzeit"
-#: fietsboek/templates/browse.jinja2:181 fietsboek/templates/details.jinja2:134
-#: fietsboek/templates/profile.jinja2:35
+#: fietsboek/templates/browse.jinja2:185 fietsboek/templates/details.jinja2:158
+#: fietsboek/templates/journey_details.jinja2:92
+#: fietsboek/templates/journey_details.jinja2:147
+#: fietsboek/templates/profile_overview.jinja2:40
msgid "page.details.stopped_time"
msgstr "Haltezeit"
-#: fietsboek/templates/browse.jinja2:185 fietsboek/templates/details.jinja2:138
-#: fietsboek/templates/profile.jinja2:39
+#: fietsboek/templates/browse.jinja2:189 fietsboek/templates/details.jinja2:162
+#: fietsboek/templates/journey_details.jinja2:96
+#: fietsboek/templates/journey_details.jinja2:151
+#: fietsboek/templates/profile_overview.jinja2:44
msgid "page.details.max_speed"
msgstr "maximale Geschwindigkeit"
-#: fietsboek/templates/browse.jinja2:187 fietsboek/templates/details.jinja2:142
-#: fietsboek/templates/profile.jinja2:41
+#: fietsboek/templates/browse.jinja2:191 fietsboek/templates/details.jinja2:166
+#: fietsboek/templates/journey_details.jinja2:100
+#: fietsboek/templates/journey_details.jinja2:153
+#: fietsboek/templates/profile_overview.jinja2:46
msgid "page.details.avg_speed"
msgstr "durchschnittliche Geschwindigkeit"
-#: fietsboek/templates/browse.jinja2:192
+#: fietsboek/templates/browse.jinja2:196
+#: fietsboek/templates/journey_details.jinja2:158
msgid "page.browse.card.comments"
msgstr "Kommentare"
-#: fietsboek/templates/browse.jinja2:194
+#: fietsboek/templates/browse.jinja2:198
+#: fietsboek/templates/journey_details.jinja2:160
msgid "page.browse.card.images"
msgstr "Bilder"
-#: fietsboek/templates/browse.jinja2:211
+#: fietsboek/templates/browse.jinja2:216
msgid "page.browse.download_multiple"
msgstr "ausgewählte Herunterladen"
-#: fietsboek/templates/browse.jinja2:213
+#: fietsboek/templates/browse.jinja2:222 fietsboek/templates/browse.jinja2:226
+msgid "pagination.previous"
+msgstr "Vorherige"
+
+#: fietsboek/templates/browse.jinja2:231 fietsboek/templates/browse.jinja2:235
+msgid "pagination.next"
+msgstr "Nächste"
+
+#: fietsboek/templates/browse.jinja2:242
msgid "page.browse.no_results"
msgstr "Es wurden keine Strecken gefunden, die den Filtern entsprechen."
-#: fietsboek/templates/browse.jinja2:215
+#: fietsboek/templates/browse.jinja2:244
msgid "page.browse.no_tracks"
msgstr ""
"Es wurden keine Strecken gefunden, auf die Du Zugriff hast. Versuche, "
@@ -305,90 +492,95 @@ msgstr "Passwort wiederholen"
msgid "page.create_account.create"
msgstr "Erstellen"
-#: fietsboek/templates/details.jinja2:7
+#: fietsboek/templates/details.jinja2:24
msgid "page.details.title"
msgstr "Details"
-#: fietsboek/templates/details.jinja2:20
+#: fietsboek/templates/details.jinja2:37
msgid "page.details.edit"
msgstr "Bearbeiten"
-#: fietsboek/templates/details.jinja2:21
+#: fietsboek/templates/details.jinja2:38
msgid "page.details.share"
msgstr "Teilen"
-#: fietsboek/templates/details.jinja2:22
+#: fietsboek/templates/details.jinja2:39
msgid "page.details.delete"
msgstr "Löschen"
-#: fietsboek/templates/details.jinja2:28
+#: fietsboek/templates/details.jinja2:45
msgid "page.details.sharelink.title"
msgstr "Link zum Teilen"
-#: fietsboek/templates/details.jinja2:32
+#: fietsboek/templates/details.jinja2:49
msgid "page.details.sharelink.info"
msgstr "Jeder mit Zugang zu diesem Link kann die Strecke ansehen!"
-#: fietsboek/templates/details.jinja2:39
+#: fietsboek/templates/details.jinja2:56
msgid "page.details.sharelink.invalidate"
msgstr "Link invalidieren"
-#: fietsboek/templates/details.jinja2:41
+#: fietsboek/templates/details.jinja2:58
msgid "page.details.sharelink.close"
msgstr "Schließen"
-#: fietsboek/templates/details.jinja2:51
+#: fietsboek/templates/details.jinja2:68
msgid "page.details.delete.title"
msgstr "Strecke Löschen"
-#: fietsboek/templates/details.jinja2:55
+#: fietsboek/templates/details.jinja2:72
msgid "page.details.delete.info"
msgstr "Das Löschen der Strecke wird alle damit verbundenen Informationen löschen!"
-#: fietsboek/templates/details.jinja2:60
+#: fietsboek/templates/details.jinja2:77
msgid "page.details.delete.delete"
msgstr "Löschen"
-#: fietsboek/templates/details.jinja2:62
+#: fietsboek/templates/details.jinja2:79
msgid "page.details.delete.close"
msgstr "Abbrechen"
-#: fietsboek/templates/details.jinja2:81
+#: fietsboek/templates/details.jinja2:98
msgid "page.details.tags"
msgstr "Schlagwörter"
-#: fietsboek/templates/details.jinja2:91 fietsboek/templates/edit.jinja2:10
+#: fietsboek/templates/details.jinja2:108 fietsboek/templates/edit.jinja2:10
#: fietsboek/templates/finish_upload.jinja2:10
+#: fietsboek/templates/journey_details.jinja2:66
msgid "page.noscript"
msgstr ""
"JavaScript ist deaktiviert, zum Nutzen aller Funktionen bitte JavaScript "
"aktivieren"
-#: fietsboek/templates/details.jinja2:97
+#: fietsboek/templates/details.jinja2:115
msgid "page.details.download"
msgstr "Herunterladen"
-#: fietsboek/templates/details.jinja2:187
+#: fietsboek/templates/details.jinja2:120
+msgid "page.details.download_pdf"
+msgstr "PDF Übersicht"
+
+#: fietsboek/templates/details.jinja2:211
msgid "page.details.comments"
msgstr "Kommentare"
-#: fietsboek/templates/details.jinja2:191
+#: fietsboek/templates/details.jinja2:215
msgid "page.details.comments.author"
msgstr "Kommentar von {}"
-#: fietsboek/templates/details.jinja2:208
+#: fietsboek/templates/details.jinja2:232
msgid "page.details.comments.new.title"
msgstr "Kommentar erstellen"
-#: fietsboek/templates/details.jinja2:211
+#: fietsboek/templates/details.jinja2:235
msgid "page.details.comments.new.input_title"
msgstr "Titel"
-#: fietsboek/templates/details.jinja2:212
+#: fietsboek/templates/details.jinja2:236
msgid "page.details.comments.new.input_comment"
msgstr "Kommentar"
-#: fietsboek/templates/details.jinja2:215
+#: fietsboek/templates/details.jinja2:239
msgid "page.details.comments.new.submit"
msgstr "Absenden"
@@ -396,11 +588,15 @@ msgstr "Absenden"
msgid "page.edit.title"
msgstr "Strecke Bearbeiten"
-#: fietsboek/templates/edit.jinja2:16
+#: fietsboek/templates/edit.jinja2:14
+msgid "page.edit.form.new_track"
+msgstr "Neue Streckendatei auswählen"
+
+#: fietsboek/templates/edit.jinja2:20
msgid "page.edit.form.submit"
msgstr "Speichern"
-#: fietsboek/templates/edit.jinja2:17
+#: fietsboek/templates/edit.jinja2:21
msgid "page.edit.form.cancel"
msgstr "Abbrechen"
@@ -544,61 +740,177 @@ msgstr ""
"Links, um sie fortzusetzen:"
#: fietsboek/templates/home.jinja2:44 fietsboek/templates/home.jinja2:53
-#: fietsboek/templates/home.jinja2:97
+#: fietsboek/templates/home.jinja2:99
msgid "page.home.summary.track"
msgid_plural "page.home.summary.tracks"
msgstr[0] "%(num)d Strecke"
msgstr[1] "%(num)d Strecken"
-#: fietsboek/templates/home.jinja2:97
+#: fietsboek/templates/home.jinja2:99
msgid "page.home.total"
msgstr "Gesamt"
-#: fietsboek/templates/layout.jinja2:43
+#: fietsboek/templates/journey_details.jinja2:10
+msgid "journey.edit"
+msgstr "Bearbeiten"
+
+#: fietsboek/templates/journey_details.jinja2:11
+msgid "journey.share"
+msgstr "Teilen"
+
+#: fietsboek/templates/journey_details.jinja2:12
+msgid "journey.delete"
+msgstr "Löschen"
+
+#: fietsboek/templates/journey_details.jinja2:18
+msgid "journey.sharelink.title"
+msgstr "Link zum Teilen"
+
+#: fietsboek/templates/journey_details.jinja2:22
+msgid "journey.sharelink.info"
+msgstr "Jeder mit Zugang zu diesem Link kann die Reise ansehen!"
+
+#: fietsboek/templates/journey_details.jinja2:29
+msgid "journey.sharelink.invalidate"
+msgstr "Link invalidieren"
+
+#: fietsboek/templates/journey_details.jinja2:31
+msgid "journey.sharelink.close"
+msgstr "Schließen"
+
+#: fietsboek/templates/journey_details.jinja2:41
+msgid "journey.delete.title"
+msgstr "Reise Löschen"
+
+#: fietsboek/templates/journey_details.jinja2:45
+msgid "journey.delete.info"
+msgstr "Das Löschen der Reise wird die einzelnen Strecken nicht löschen."
+
+#: fietsboek/templates/journey_details.jinja2:50
+msgid "journey.delete.delete"
+msgstr "Löschen"
+
+#: fietsboek/templates/journey_details.jinja2:52
+msgid "journey.delete.close"
+msgstr "Abbrechen"
+
+#: fietsboek/templates/journey_details.jinja2:108
+msgid "journey.tracks"
+msgstr "Strecken"
+
+#: fietsboek/templates/journey_details.jinja2:174
+msgid "journeys.track.hidden"
+msgstr "Du hast nicht die Rechte, diese Strecke zu sehen. Sie ist versteckt."
+
+#: fietsboek/templates/journey_form.jinja2:40
+msgid "journeys.new.form.title"
+msgstr "Titel"
+
+#: fietsboek/templates/journey_form.jinja2:43
+msgid "journeys.new.form.requires_title"
+msgstr "Ein Titel wird benötigt"
+
+#: fietsboek/templates/journey_form.jinja2:47
+msgid "journeys.new.form.description"
+msgstr "Beschreibung"
+
+#: fietsboek/templates/journey_form.jinja2:51
+msgid "journeys.new.form.visibility"
+msgstr "Sichtbarkeit"
+
+#: fietsboek/templates/journey_form.jinja2:54
+msgid "journeys.new.form.visibility.private"
+msgstr "Privat"
+
+#: fietsboek/templates/journey_form.jinja2:55
+msgid "journeys.new.form.visibility.friends"
+msgstr "Nur Freunde"
+
+#: fietsboek/templates/journey_form.jinja2:56
+msgid "journeys.new.form.visibility.logged_in"
+msgstr "Angemeldete Nutzer"
+
+#: fietsboek/templates/journey_form.jinja2:57
+msgid "journeys.new.form.visibility.public"
+msgstr "Öffentlich"
+
+#: fietsboek/templates/journey_form.jinja2:62
+msgid "journeys.new.form.tracksearch"
+msgstr "Nach Strecken suchen"
+
+#: fietsboek/templates/journey_form.jinja2:71
+msgid "journeys.new.form.tracks"
+msgstr "Strecken (ziehen zum Ordnen)"
+
+#: fietsboek/templates/journey_form.jinja2:90
+msgid "journeys.new.form.submit"
+msgstr "Speichern"
+
+#: fietsboek/templates/journey_form.jinja2:93
+msgid "journeys.new.form.requires_tracks"
+msgstr "Es muss mindestens eine Strecke vorhanden sein"
+
+#: fietsboek/templates/journey_list.jinja2:4
+msgid "journeys.overview.title"
+msgstr "Reisen"
+
+#: fietsboek/templates/journey_list.jinja2:10
+msgid "journeys.overview.new"
+msgstr "Neue Reise"
+
+#: fietsboek/templates/journey_new.jinja2:10
+msgid "journeys.new.title"
+msgstr "Neue Reise"
+
+#: fietsboek/templates/layout.jinja2:44
msgid "page.navbar.toggle"
msgstr "Navigation umschalten"
-#: fietsboek/templates/layout.jinja2:54
+#: fietsboek/templates/layout.jinja2:55
msgid "page.navbar.home"
msgstr "Startseite"
-#: fietsboek/templates/layout.jinja2:57
+#: fietsboek/templates/layout.jinja2:58
msgid "page.navbar.browse"
msgstr "Stöbern"
#: fietsboek/templates/layout.jinja2:61
+msgid "page.navbar.journeys"
+msgstr "Reisen"
+
+#: fietsboek/templates/layout.jinja2:65
msgid "page.navbar.upload"
msgstr "Hochladen"
-#: fietsboek/templates/layout.jinja2:70
+#: fietsboek/templates/layout.jinja2:74
msgid "page.navbar.user"
msgstr "Nutzer"
-#: fietsboek/templates/layout.jinja2:74
+#: fietsboek/templates/layout.jinja2:78
msgid "page.navbar.welcome_user"
msgstr "Willkommen, {}!"
-#: fietsboek/templates/layout.jinja2:77
+#: fietsboek/templates/layout.jinja2:81
msgid "page.navbar.logout"
msgstr "Abmelden"
-#: fietsboek/templates/layout.jinja2:80
+#: fietsboek/templates/layout.jinja2:84
msgid "page.navbar.profile"
msgstr "Profil"
-#: fietsboek/templates/layout.jinja2:83
+#: fietsboek/templates/layout.jinja2:87
msgid "page.navbar.user_data"
msgstr "Persönliche Daten"
-#: fietsboek/templates/layout.jinja2:87
+#: fietsboek/templates/layout.jinja2:91
msgid "page.navbar.admin"
msgstr "Admin"
-#: fietsboek/templates/layout.jinja2:93
+#: fietsboek/templates/layout.jinja2:97
msgid "page.navbar.login"
msgstr "Anmelden"
-#: fietsboek/templates/layout.jinja2:97
+#: fietsboek/templates/layout.jinja2:101
msgid "page.navbar.create_account"
msgstr "Konto Erstellen"
@@ -654,91 +966,103 @@ msgstr "Passwörter stimmen nicht überein"
msgid "page.password_reset.reset"
msgstr "Zurücksetzen"
-#: fietsboek/templates/profile.jinja2:64
+#: fietsboek/templates/profile.jinja2:10
msgid "page.profile.tabbar.overview"
msgstr "Übersicht"
-#: fietsboek/templates/profile.jinja2:69
+#: fietsboek/templates/profile.jinja2:15
msgid "page.profile.tabbar.graphs"
msgstr "Diagramme"
-#: fietsboek/templates/profile.jinja2:74
+#: fietsboek/templates/profile.jinja2:20
msgid "page.profile.tabbar.calendar"
msgstr "Kalender"
-#: fietsboek/templates/profile.jinja2:88
+#: fietsboek/templates/profile_calendar.jinja2:9
+msgid "page.profile.calendar.previous"
+msgstr "Vorheriger Monat"
+
+#: fietsboek/templates/profile_calendar.jinja2:11
+msgid "page.profile.calendar.next"
+msgstr "Nächster Monat"
+
+#: fietsboek/templates/profile_graphs.jinja2:6
+msgid "page.profile.graph.km_per_month"
+msgstr "Kilometer pro Monat"
+
+#: fietsboek/templates/profile_overview.jinja2:71
msgid "page.profile.length"
msgstr "Länge"
-#: fietsboek/templates/profile.jinja2:92
+#: fietsboek/templates/profile_overview.jinja2:75
msgid "page.profile.avg_length"
msgstr "durchschnittliche Länge"
-#: fietsboek/templates/profile.jinja2:96
+#: fietsboek/templates/profile_overview.jinja2:79
msgid "page.profile.uphill"
msgstr "Bergauf"
-#: fietsboek/templates/profile.jinja2:100
+#: fietsboek/templates/profile_overview.jinja2:83
msgid "page.profile.downhill"
msgstr "Bergab"
-#: fietsboek/templates/profile.jinja2:104
+#: fietsboek/templates/profile_overview.jinja2:87
msgid "page.profile.moving_time"
msgstr "Fahrzeit"
-#: fietsboek/templates/profile.jinja2:108
+#: fietsboek/templates/profile_overview.jinja2:91
msgid "page.profile.stopped_time"
msgstr "Haltezeit"
-#: fietsboek/templates/profile.jinja2:112
+#: fietsboek/templates/profile_overview.jinja2:95
msgid "page.profile.avg_duration"
msgstr "durchschnittliche Dauer"
-#: fietsboek/templates/profile.jinja2:116
+#: fietsboek/templates/profile_overview.jinja2:99
msgid "page.profile.max_speed"
msgstr "maximale Geschwindigkeit"
-#: fietsboek/templates/profile.jinja2:120
+#: fietsboek/templates/profile_overview.jinja2:103
msgid "page.profile.avg_speed"
msgstr "durchschnittliche Geschwindigkeit"
-#: fietsboek/templates/profile.jinja2:124
+#: fietsboek/templates/profile_overview.jinja2:107
msgid "page.profile.number_of_tracks"
msgstr "Anzahl der Strecken"
-#: fietsboek/templates/profile.jinja2:130
+#: fietsboek/templates/profile_overview.jinja2:112
+msgid "page.profile.contribution_card.title"
+msgstr "Streckenaktivität"
+
+#: fietsboek/templates/profile_overview.jinja2:147
+msgid "page.profile.contribution_card.legend.less"
+msgstr "Weniger km"
+
+#: fietsboek/templates/profile_overview.jinja2:154
+msgid "page.profile.contribution_card.legend.more"
+msgstr "Mehr km"
+
+#: fietsboek/templates/profile_overview.jinja2:161
msgid "page.profile.longest_distance_track"
msgstr "Weiteste Strecke"
-#: fietsboek/templates/profile.jinja2:135
+#: fietsboek/templates/profile_overview.jinja2:166
msgid "page.profile.shortest_distance_track"
msgstr "Kürzeste Strecke"
-#: fietsboek/templates/profile.jinja2:140
+#: fietsboek/templates/profile_overview.jinja2:171
msgid "page.profile.longest_duration_track"
msgstr "Am Längsten Dauernde Strecke"
-#: fietsboek/templates/profile.jinja2:145
+#: fietsboek/templates/profile_overview.jinja2:176
msgid "page.profile.shortest_duration_track"
msgstr "Am Kürzesten Dauernde Strecke"
-#: fietsboek/templates/profile.jinja2:152
-msgid "page.profile.graph.km_per_month"
-msgstr "Kilometer pro Monat"
-
-#: fietsboek/templates/profile.jinja2:161
-msgid "page.profile.calendar.previous"
-msgstr "Vorheriger Monat"
-
-#: fietsboek/templates/profile.jinja2:163
-msgid "page.profile.calendar.next"
-msgstr "Nächster Monat"
-
-#: fietsboek/templates/profile.jinja2:218
+#: fietsboek/templates/profile_overview.jinja2:197
msgid "page.profile.heatmap"
msgstr "Heatmap"
-#: fietsboek/templates/profile.jinja2:223
+#: fietsboek/templates/profile_overview.jinja2:205
msgid "page.profile.tilehunt"
msgstr "Kacheljäger"
@@ -866,11 +1190,11 @@ msgstr ""
"Diese Transformation passt die Höhenangabe für Punkte an, bei denen die "
"Höhenangabe fehlt."
-#: fietsboek/transformers/elevation.py:116
+#: fietsboek/transformers/elevation.py:109
msgid "transformers.fix-elevation-jumps"
msgstr "Höhensprünge beheben"
-#: fietsboek/transformers/elevation.py:120
+#: fietsboek/transformers/elevation.py:113
msgid "transformers.fix-elevation-jumps.description"
msgstr ""
"Diese Transformation passt die Höhenangabe für Punkte an, bei denen die "
@@ -888,15 +1212,15 @@ msgstr "Ungültige E-Mail-Adresse"
msgid "flash.a_confirmation_link_has_been_sent"
msgstr "Ein Bestätigungslink wurde versandt"
-#: fietsboek/views/admin.py:49
+#: fietsboek/views/admin.py:189
msgid "flash.badge_added"
msgstr "Wappen hinzugefügt"
-#: fietsboek/views/admin.py:73
+#: fietsboek/views/admin.py:213
msgid "flash.badge_modified"
msgstr "Wappen bearbeitet"
-#: fietsboek/views/admin.py:93
+#: fietsboek/views/admin.py:233
msgid "flash.badge_deleted"
msgstr "Wappen gelöscht"
@@ -956,23 +1280,27 @@ msgstr "E-Mail-Adresse bestätigt"
msgid "flash.password_updated"
msgstr "Passwort aktualisiert"
-#: fietsboek/views/detail.py:162
+#: fietsboek/views/detail.py:187
msgid "flash.track_deleted"
msgstr "Strecke gelöscht"
+#: fietsboek/views/edit.py:97 fietsboek/views/upload.py:63
+msgid "flash.invalid_file"
+msgstr "Ungültige GPX-Datei gesendet"
+
+#: fietsboek/views/journey.py:251
+msgid "flash.journey_deleted"
+msgstr "Reise gelöscht"
+
#: fietsboek/views/upload.py:53
msgid "flash.no_file_selected"
msgstr "Keine Datei ausgewählt"
-#: fietsboek/views/upload.py:66
-msgid "flash.invalid_file"
-msgstr "Ungültige GPX-Datei gesendet"
-
-#: fietsboek/views/upload.py:192
+#: fietsboek/views/upload.py:177
msgid "flash.upload_success"
msgstr "Hochladen erfolgreich"
-#: fietsboek/views/upload.py:211
+#: fietsboek/views/upload.py:196
msgid "flash.upload_cancelled"
msgstr "Hochladen abgebrochen"
diff --git a/fietsboek/locale/en/LC_MESSAGES/messages.mo b/fietsboek/locale/en/LC_MESSAGES/messages.mo
index 5f8edc6..95464f3 100644
--- a/fietsboek/locale/en/LC_MESSAGES/messages.mo
+++ b/fietsboek/locale/en/LC_MESSAGES/messages.mo
Binary files differ
diff --git a/fietsboek/locale/en/LC_MESSAGES/messages.po b/fietsboek/locale/en/LC_MESSAGES/messages.po
index 981d134..b94eb76 100644
--- a/fietsboek/locale/en/LC_MESSAGES/messages.po
+++ b/fietsboek/locale/en/LC_MESSAGES/messages.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2025-01-30 21:50+0100\n"
+"POT-Creation-Date: 2026-08-13 17:40+0200\n"
"PO-Revision-Date: 2023-04-03 20:42+0200\n"
"Last-Translator: \n"
"Language: en\n"
@@ -16,87 +16,231 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 2.15.0\n"
+"Generated-By: Babel 2.18.0\n"
-#: fietsboek/actions.py:267
+#: fietsboek/actions.py:266
msgid "email.verify_mail.subject"
msgstr "Fietsboek Account Verification"
-#: fietsboek/actions.py:270
+#: fietsboek/actions.py:269
msgid "email.verify.text"
msgstr ""
"To verify your Fietsboek account, please use this link: {}\n"
"\n"
"If you did not create an account, ignore this email."
-#: fietsboek/util.py:333
+#: fietsboek/pdf.py:233
+msgid "pdf.table.date"
+msgstr "Date"
+
+#: fietsboek/pdf.py:235
+msgid "pdf.table.length"
+msgstr "Length"
+
+#: fietsboek/pdf.py:239
+msgid "pdf.table.uphill"
+msgstr "Uphill"
+
+#: fietsboek/pdf.py:243
+msgid "pdf.table.downhill"
+msgstr "Downhill"
+
+#: fietsboek/pdf.py:246
+msgid "pdf.table.moving_time"
+msgstr "Moving Time"
+
+#: fietsboek/pdf.py:247
+msgid "pdf.table.stopped_time"
+msgstr "Stopped Time"
+
+#: fietsboek/pdf.py:249
+msgid "pdf.table.max_speed"
+msgstr "Max Speed"
+
+#: fietsboek/pdf.py:253
+msgid "pdf.table.avg_speed"
+msgstr "Average Speed"
+
+#: fietsboek/summaries.py:429
+msgid "page.profile.contribution_card.tooltip"
+msgid_plural "page.profile.contribution_card.tooltips"
+msgstr[0] "${count} track on ${date}"
+msgstr[1] "${count} tracks on ${date}"
+
+#: fietsboek/util.py:299
msgid "password_constraint.mismatch"
msgstr "Passwords don't match"
-#: fietsboek/util.py:335
+#: fietsboek/util.py:301
msgid "password_constraint.length"
msgstr "Password not long enough"
-#: fietsboek/models/track.py:603
+#: fietsboek/models/track.py:774
msgid "tooltip.table.length"
msgstr "Length"
-#: fietsboek/models/track.py:604
+#: fietsboek/models/track.py:775
msgid "tooltip.table.people"
msgstr "# People"
-#: fietsboek/models/track.py:605
+#: fietsboek/models/track.py:776
msgid "tooltip.table.uphill"
msgstr "Uphill"
-#: fietsboek/models/track.py:606
+#: fietsboek/models/track.py:777
msgid "tooltip.table.downhill"
msgstr "Downhill"
-#: fietsboek/models/track.py:607 fietsboek/templates/home.jinja2:7
+#: fietsboek/models/track.py:778 fietsboek/templates/home.jinja2:7
msgid "tooltip.table.moving_time"
msgstr "Moving Time"
-#: fietsboek/models/track.py:608 fietsboek/templates/home.jinja2:8
+#: fietsboek/models/track.py:779 fietsboek/templates/home.jinja2:8
msgid "tooltip.table.stopped_time"
msgstr "Stopped Time"
-#: fietsboek/models/track.py:610
+#: fietsboek/models/track.py:781
msgid "tooltip.table.max_speed"
msgstr "Max Speed"
-#: fietsboek/models/track.py:614
+#: fietsboek/models/track.py:785
msgid "tooltip.table.avg_speed"
msgstr "Average Speed"
+#: fietsboek/templates/403.jinja2:5
+msgid "403.title"
+msgstr "No entry"
+
+#: fietsboek/templates/403.jinja2:9
+msgid "403.no_access"
+msgstr "You are not allowed to access this resource."
+
+#: fietsboek/templates/403.jinja2:12
+msgid "403.try_log_in"
+msgstr ""
+"If you should have access, make sure you are logged in with the right "
+"credentials."
+
+#: fietsboek/templates/404.jinja2:5
+msgid "404.title"
+msgstr "Dead end"
+
+#: fietsboek/templates/404.jinja2:9
+msgid "404.path_not_found"
+msgstr "The path you have chosen was not found."
+
+#: fietsboek/templates/404.jinja2:12
+msgid "404.choose_different"
+msgstr "Please choose a different path."
+
#: fietsboek/templates/admin.jinja2:5
msgid "page.admin.title"
msgstr "Administration"
-#: fietsboek/templates/admin.jinja2:7
+#: fietsboek/templates/admin.jinja2:10
+msgid "page.admin.nav.overview"
+msgstr "Overview"
+
+#: fietsboek/templates/admin.jinja2:11
+msgid "page.admin.nav.badges"
+msgstr "Badges"
+
+#: fietsboek/templates/admin_badges.jinja2:5
msgid "page.admin.badges"
msgstr "Badges"
-#: fietsboek/templates/admin.jinja2:23
+#: fietsboek/templates/admin_badges.jinja2:21
msgid "page.admin.badge.edit"
msgstr "Edit"
-#: fietsboek/templates/admin.jinja2:29
+#: fietsboek/templates/admin_badges.jinja2:22
msgid "page.admin.badge.delete_badge"
msgstr "Delete badge"
-#: fietsboek/templates/admin.jinja2:37
+#: fietsboek/templates/admin_badges.jinja2:35
msgid "page.admin.badges.badge_title"
msgstr "Badge Title"
-#: fietsboek/templates/admin.jinja2:41
+#: fietsboek/templates/admin_badges.jinja2:39
msgid "page.admin.badges.badge_image"
msgstr "Badge Image"
-#: fietsboek/templates/admin.jinja2:45
+#: fietsboek/templates/admin_badges.jinja2:43
msgid "page.admin.badges.add_badge"
msgstr "Add Badge"
+#: fietsboek/templates/admin_overview.jinja2:5
+msgid "admin.overview.instance_has"
+msgstr "This instance has"
+
+#: fietsboek/templates/admin_overview.jinja2:9
+msgid "admin.overview.stat.user"
+msgid_plural "admin.overview.stat.users"
+msgstr[0] "%(num)d user"
+msgstr[1] "%(num)d users"
+
+#: fietsboek/templates/admin_overview.jinja2:13
+msgid "admin.overview.stat.track"
+msgid_plural "admin.overview.stat.tracks"
+msgstr[0] "%(num)d track"
+msgstr[1] "%(num)d tracks"
+
+#: fietsboek/templates/admin_overview.jinja2:17
+msgid "admin.overview.stats.mib"
+msgstr "MiB of data"
+
+#: fietsboek/templates/admin_overview.jinja2:24
+msgid "admin.overview.system_overview"
+msgstr "System information"
+
+#: fietsboek/templates/admin_overview.jinja2:28
+msgid "admin.overview.fietsboek_version"
+msgstr "Fietsboek version"
+
+#: fietsboek/templates/admin_overview.jinja2:32
+msgid "admin.overview.python_version"
+msgstr "Python version"
+
+#: fietsboek/templates/admin_overview.jinja2:36
+msgid "admin.overview.kernel_version"
+msgstr "Linux version"
+
+#: fietsboek/templates/admin_overview.jinja2:40
+msgid "admin.overview.distro_version"
+msgstr "Distribution"
+
+#: fietsboek/templates/admin_overview.jinja2:44
+msgid "admin.overview.last_cronjob"
+msgstr "Last cronjob"
+
+#: fietsboek/templates/admin_overview.jinja2:55
+msgid "admin.overview.storage_graph.label.track_data"
+msgstr "Track data"
+
+#: fietsboek/templates/admin_overview.jinja2:56
+msgid "admin.overview.storage_graph.label.backups"
+msgstr "File backups"
+
+#: fietsboek/templates/admin_overview.jinja2:57
+msgid "admin.overview.storage_graph.label.images"
+msgstr "Images"
+
+#: fietsboek/templates/admin_overview.jinja2:58
+msgid "admin.overview.storage_graph.label.track_previews"
+msgstr "Track previews"
+
+#: fietsboek/templates/admin_overview.jinja2:59
+msgid "admin.overview.storage_graph.label.journey_previews"
+msgstr "Journey previews"
+
+#: fietsboek/templates/admin_overview.jinja2:60
+msgid "admin.overview.storage_graph.label.user_maps"
+msgstr "User maps"
+
+#: fietsboek/templates/admin_overview.jinja2:88
+msgid "admin.overview.storage_graph.title"
+msgstr "Storage breakdown"
+
#: fietsboek/templates/browse.jinja2:4
msgid "page.browse.title"
msgstr "Browse"
@@ -125,6 +269,18 @@ msgstr "Length limits"
msgid "page.browse.filter.length_maximum"
msgstr "Maximum length"
+#: fietsboek/templates/browse.jinja2:34
+msgid "page.browse.filter.uphill_minimum"
+msgstr "Minimum uphill"
+
+#: fietsboek/templates/browse.jinja2:40
+msgid "page.browse.filter.uphill_boundaries"
+msgstr "Uphill limits"
+
+#: fietsboek/templates/browse.jinja2:46
+msgid "page.browse.filter.uphill_maximum"
+msgstr "Maximum uphill"
+
#: fietsboek/templates/browse.jinja2:59
msgid "page.browse.filter.date_boundaries"
msgstr "Date limits"
@@ -182,6 +338,10 @@ msgid "page.browse.sort.length"
msgstr "Sort by length"
#: fietsboek/templates/browse.jinja2:125 fietsboek/templates/browse.jinja2:126
+msgid "page.browse.sort.uphill"
+msgstr "Sort by uphill"
+
+#: fietsboek/templates/browse.jinja2:127 fietsboek/templates/browse.jinja2:128
msgid "page.browse.sort.duration"
msgstr "Sort by duration"
@@ -193,73 +353,100 @@ msgstr "This is a recording of a track"
msgid "page.browse.synthetic_tooltip"
msgstr "This is a pre-planned track"
-#: fietsboek/templates/browse.jinja2:158 fietsboek/templates/details.jinja2:103
-#: fietsboek/templates/profile.jinja2:15
+#: fietsboek/templates/browse.jinja2:162 fietsboek/templates/details.jinja2:127
+#: fietsboek/templates/journey_details.jinja2:124
+#: fietsboek/templates/profile_overview.jinja2:20
msgid "page.details.date"
msgstr "Date"
-#: fietsboek/templates/browse.jinja2:160 fietsboek/templates/details.jinja2:117
-#: fietsboek/templates/profile.jinja2:17
+#: fietsboek/templates/browse.jinja2:164 fietsboek/templates/details.jinja2:141
+#: fietsboek/templates/journey_details.jinja2:76
+#: fietsboek/templates/journey_details.jinja2:126
+#: fietsboek/templates/profile_overview.jinja2:22
msgid "page.details.length"
msgstr "Length"
-#: fietsboek/templates/browse.jinja2:165 fietsboek/templates/details.jinja2:108
-#: fietsboek/templates/profile.jinja2:21
+#: fietsboek/templates/browse.jinja2:169 fietsboek/templates/details.jinja2:132
+#: fietsboek/templates/journey_details.jinja2:131
+#: fietsboek/templates/profile_overview.jinja2:26
msgid "page.details.start_time"
msgstr "Record Start"
-#: fietsboek/templates/browse.jinja2:167 fietsboek/templates/details.jinja2:112
-#: fietsboek/templates/profile.jinja2:23
+#: fietsboek/templates/browse.jinja2:171 fietsboek/templates/details.jinja2:136
+#: fietsboek/templates/journey_details.jinja2:133
+#: fietsboek/templates/profile_overview.jinja2:28
msgid "page.details.end_time"
msgstr "Record End"
-#: fietsboek/templates/browse.jinja2:172 fietsboek/templates/details.jinja2:121
-#: fietsboek/templates/profile.jinja2:27
+#: fietsboek/templates/browse.jinja2:176 fietsboek/templates/details.jinja2:145
+#: fietsboek/templates/journey_details.jinja2:80
+#: fietsboek/templates/journey_details.jinja2:138
+#: fietsboek/templates/profile_overview.jinja2:32
msgid "page.details.uphill"
msgstr "Uphill"
-#: fietsboek/templates/browse.jinja2:174 fietsboek/templates/details.jinja2:125
-#: fietsboek/templates/profile.jinja2:29
+#: fietsboek/templates/browse.jinja2:178 fietsboek/templates/details.jinja2:149
+#: fietsboek/templates/journey_details.jinja2:84
+#: fietsboek/templates/journey_details.jinja2:140
+#: fietsboek/templates/profile_overview.jinja2:34
msgid "page.details.downhill"
msgstr "Downhill"
-#: fietsboek/templates/browse.jinja2:179 fietsboek/templates/details.jinja2:130
-#: fietsboek/templates/profile.jinja2:33
+#: fietsboek/templates/browse.jinja2:183 fietsboek/templates/details.jinja2:154
+#: fietsboek/templates/journey_details.jinja2:88
+#: fietsboek/templates/journey_details.jinja2:145
+#: fietsboek/templates/profile_overview.jinja2:38
msgid "page.details.moving_time"
msgstr "Moving Time"
-#: fietsboek/templates/browse.jinja2:181 fietsboek/templates/details.jinja2:134
-#: fietsboek/templates/profile.jinja2:35
+#: fietsboek/templates/browse.jinja2:185 fietsboek/templates/details.jinja2:158
+#: fietsboek/templates/journey_details.jinja2:92
+#: fietsboek/templates/journey_details.jinja2:147
+#: fietsboek/templates/profile_overview.jinja2:40
msgid "page.details.stopped_time"
msgstr "Stopped Time"
-#: fietsboek/templates/browse.jinja2:185 fietsboek/templates/details.jinja2:138
-#: fietsboek/templates/profile.jinja2:39
+#: fietsboek/templates/browse.jinja2:189 fietsboek/templates/details.jinja2:162
+#: fietsboek/templates/journey_details.jinja2:96
+#: fietsboek/templates/journey_details.jinja2:151
+#: fietsboek/templates/profile_overview.jinja2:44
msgid "page.details.max_speed"
msgstr "Max Speed"
-#: fietsboek/templates/browse.jinja2:187 fietsboek/templates/details.jinja2:142
-#: fietsboek/templates/profile.jinja2:41
+#: fietsboek/templates/browse.jinja2:191 fietsboek/templates/details.jinja2:166
+#: fietsboek/templates/journey_details.jinja2:100
+#: fietsboek/templates/journey_details.jinja2:153
+#: fietsboek/templates/profile_overview.jinja2:46
msgid "page.details.avg_speed"
msgstr "Average Speed"
-#: fietsboek/templates/browse.jinja2:192
+#: fietsboek/templates/browse.jinja2:196
+#: fietsboek/templates/journey_details.jinja2:158
msgid "page.browse.card.comments"
msgstr "Comments"
-#: fietsboek/templates/browse.jinja2:194
+#: fietsboek/templates/browse.jinja2:198
+#: fietsboek/templates/journey_details.jinja2:160
msgid "page.browse.card.images"
msgstr "Images"
-#: fietsboek/templates/browse.jinja2:211
+#: fietsboek/templates/browse.jinja2:216
msgid "page.browse.download_multiple"
msgstr "Download selected"
-#: fietsboek/templates/browse.jinja2:213
+#: fietsboek/templates/browse.jinja2:222 fietsboek/templates/browse.jinja2:226
+msgid "pagination.previous"
+msgstr "Previous"
+
+#: fietsboek/templates/browse.jinja2:231 fietsboek/templates/browse.jinja2:235
+msgid "pagination.next"
+msgstr "Next"
+
+#: fietsboek/templates/browse.jinja2:242
msgid "page.browse.no_results"
msgstr "No results matching the filters were found."
-#: fietsboek/templates/browse.jinja2:215
+#: fietsboek/templates/browse.jinja2:244
msgid "page.browse.no_tracks"
msgstr "You currently do not have access to any tracks. Try logging in."
@@ -303,88 +490,93 @@ msgstr "Repeat password"
msgid "page.create_account.create"
msgstr "Create"
-#: fietsboek/templates/details.jinja2:7
+#: fietsboek/templates/details.jinja2:24
msgid "page.details.title"
msgstr "Track Details"
-#: fietsboek/templates/details.jinja2:20
+#: fietsboek/templates/details.jinja2:37
msgid "page.details.edit"
msgstr "Edit"
-#: fietsboek/templates/details.jinja2:21
+#: fietsboek/templates/details.jinja2:38
msgid "page.details.share"
msgstr "Share"
-#: fietsboek/templates/details.jinja2:22
+#: fietsboek/templates/details.jinja2:39
msgid "page.details.delete"
msgstr "Delete"
-#: fietsboek/templates/details.jinja2:28
+#: fietsboek/templates/details.jinja2:45
msgid "page.details.sharelink.title"
msgstr "Share Link"
-#: fietsboek/templates/details.jinja2:32
+#: fietsboek/templates/details.jinja2:49
msgid "page.details.sharelink.info"
msgstr "Everyone with access to this link can view the track!"
-#: fietsboek/templates/details.jinja2:39
+#: fietsboek/templates/details.jinja2:56
msgid "page.details.sharelink.invalidate"
msgstr "Invalidate link"
-#: fietsboek/templates/details.jinja2:41
+#: fietsboek/templates/details.jinja2:58
msgid "page.details.sharelink.close"
msgstr "Close"
-#: fietsboek/templates/details.jinja2:51
+#: fietsboek/templates/details.jinja2:68
msgid "page.details.delete.title"
msgstr "Delete Track"
-#: fietsboek/templates/details.jinja2:55
+#: fietsboek/templates/details.jinja2:72
msgid "page.details.delete.info"
msgstr "Deleting this track will remove all associated information with it!"
-#: fietsboek/templates/details.jinja2:60
+#: fietsboek/templates/details.jinja2:77
msgid "page.details.delete.delete"
msgstr "Delete"
-#: fietsboek/templates/details.jinja2:62
+#: fietsboek/templates/details.jinja2:79
msgid "page.details.delete.close"
msgstr "Abort"
-#: fietsboek/templates/details.jinja2:81
+#: fietsboek/templates/details.jinja2:98
msgid "page.details.tags"
msgstr "Tagged as"
-#: fietsboek/templates/details.jinja2:91 fietsboek/templates/edit.jinja2:10
+#: fietsboek/templates/details.jinja2:108 fietsboek/templates/edit.jinja2:10
#: fietsboek/templates/finish_upload.jinja2:10
+#: fietsboek/templates/journey_details.jinja2:66
msgid "page.noscript"
msgstr "JavaScript is disabled, please enable JavaScript"
-#: fietsboek/templates/details.jinja2:97
+#: fietsboek/templates/details.jinja2:115
msgid "page.details.download"
msgstr "Download Tour"
-#: fietsboek/templates/details.jinja2:187
+#: fietsboek/templates/details.jinja2:120
+msgid "page.details.download_pdf"
+msgstr "PDF overview"
+
+#: fietsboek/templates/details.jinja2:211
msgid "page.details.comments"
msgstr "Comments"
-#: fietsboek/templates/details.jinja2:191
+#: fietsboek/templates/details.jinja2:215
msgid "page.details.comments.author"
msgstr "Comment by {}"
-#: fietsboek/templates/details.jinja2:208
+#: fietsboek/templates/details.jinja2:232
msgid "page.details.comments.new.title"
msgstr "Create a new comment"
-#: fietsboek/templates/details.jinja2:211
+#: fietsboek/templates/details.jinja2:235
msgid "page.details.comments.new.input_title"
msgstr "Title"
-#: fietsboek/templates/details.jinja2:212
+#: fietsboek/templates/details.jinja2:236
msgid "page.details.comments.new.input_comment"
msgstr "Comment"
-#: fietsboek/templates/details.jinja2:215
+#: fietsboek/templates/details.jinja2:239
msgid "page.details.comments.new.submit"
msgstr "Submit"
@@ -392,11 +584,15 @@ msgstr "Submit"
msgid "page.edit.title"
msgstr "Edit Track"
-#: fietsboek/templates/edit.jinja2:16
+#: fietsboek/templates/edit.jinja2:14
+msgid "page.edit.form.new_track"
+msgstr "New file for this track"
+
+#: fietsboek/templates/edit.jinja2:20
msgid "page.edit.form.submit"
msgstr "Save"
-#: fietsboek/templates/edit.jinja2:17
+#: fietsboek/templates/edit.jinja2:21
msgid "page.edit.form.cancel"
msgstr "Cancel"
@@ -538,61 +734,177 @@ msgid "page.home.unfinished_uploads"
msgstr "You have unfinished uploads. Click on the links below to resume them:"
#: fietsboek/templates/home.jinja2:44 fietsboek/templates/home.jinja2:53
-#: fietsboek/templates/home.jinja2:97
+#: fietsboek/templates/home.jinja2:99
msgid "page.home.summary.track"
msgid_plural "page.home.summary.tracks"
msgstr[0] "%(num)d track"
msgstr[1] "%(num)d tracks"
-#: fietsboek/templates/home.jinja2:97
+#: fietsboek/templates/home.jinja2:99
msgid "page.home.total"
msgstr "Total"
-#: fietsboek/templates/layout.jinja2:43
+#: fietsboek/templates/journey_details.jinja2:10
+msgid "journey.edit"
+msgstr "Edit"
+
+#: fietsboek/templates/journey_details.jinja2:11
+msgid "journey.share"
+msgstr "Share"
+
+#: fietsboek/templates/journey_details.jinja2:12
+msgid "journey.delete"
+msgstr "Delete"
+
+#: fietsboek/templates/journey_details.jinja2:18
+msgid "journey.sharelink.title"
+msgstr "Share Link"
+
+#: fietsboek/templates/journey_details.jinja2:22
+msgid "journey.sharelink.info"
+msgstr "Everyone with access to this link can view the journey!"
+
+#: fietsboek/templates/journey_details.jinja2:29
+msgid "journey.sharelink.invalidate"
+msgstr "Invalidate link"
+
+#: fietsboek/templates/journey_details.jinja2:31
+msgid "journey.sharelink.close"
+msgstr "Close"
+
+#: fietsboek/templates/journey_details.jinja2:41
+msgid "journey.delete.title"
+msgstr "Delete Journey"
+
+#: fietsboek/templates/journey_details.jinja2:45
+msgid "journey.delete.info"
+msgstr "Deleting this journey will not remove the individual tracks."
+
+#: fietsboek/templates/journey_details.jinja2:50
+msgid "journey.delete.delete"
+msgstr "Delete"
+
+#: fietsboek/templates/journey_details.jinja2:52
+msgid "journey.delete.close"
+msgstr "Abort"
+
+#: fietsboek/templates/journey_details.jinja2:108
+msgid "journey.tracks"
+msgstr "Tracks"
+
+#: fietsboek/templates/journey_details.jinja2:174
+msgid "journeys.track.hidden"
+msgstr "This track is hidden, you don't have permission to view it."
+
+#: fietsboek/templates/journey_form.jinja2:40
+msgid "journeys.new.form.title"
+msgstr "Title"
+
+#: fietsboek/templates/journey_form.jinja2:43
+msgid "journeys.new.form.requires_title"
+msgstr "A title is required"
+
+#: fietsboek/templates/journey_form.jinja2:47
+msgid "journeys.new.form.description"
+msgstr "Description"
+
+#: fietsboek/templates/journey_form.jinja2:51
+msgid "journeys.new.form.visibility"
+msgstr "Visibility"
+
+#: fietsboek/templates/journey_form.jinja2:54
+msgid "journeys.new.form.visibility.private"
+msgstr "Private"
+
+#: fietsboek/templates/journey_form.jinja2:55
+msgid "journeys.new.form.visibility.friends"
+msgstr "Friends only"
+
+#: fietsboek/templates/journey_form.jinja2:56
+msgid "journeys.new.form.visibility.logged_in"
+msgstr "Logged in users"
+
+#: fietsboek/templates/journey_form.jinja2:57
+msgid "journeys.new.form.visibility.public"
+msgstr "Public"
+
+#: fietsboek/templates/journey_form.jinja2:62
+msgid "journeys.new.form.tracksearch"
+msgstr "Search for tracks"
+
+#: fietsboek/templates/journey_form.jinja2:71
+msgid "journeys.new.form.tracks"
+msgstr "Tracks (drag to re-order)"
+
+#: fietsboek/templates/journey_form.jinja2:90
+msgid "journeys.new.form.submit"
+msgstr "Save"
+
+#: fietsboek/templates/journey_form.jinja2:93
+msgid "journeys.new.form.requires_tracks"
+msgstr "A journey must have at least one track"
+
+#: fietsboek/templates/journey_list.jinja2:4
+msgid "journeys.overview.title"
+msgstr "Journeys"
+
+#: fietsboek/templates/journey_list.jinja2:10
+msgid "journeys.overview.new"
+msgstr "New journey"
+
+#: fietsboek/templates/journey_new.jinja2:10
+msgid "journeys.new.title"
+msgstr "New Journey"
+
+#: fietsboek/templates/layout.jinja2:44
msgid "page.navbar.toggle"
msgstr "Toggle navigation"
-#: fietsboek/templates/layout.jinja2:54
+#: fietsboek/templates/layout.jinja2:55
msgid "page.navbar.home"
msgstr "Home"
-#: fietsboek/templates/layout.jinja2:57
+#: fietsboek/templates/layout.jinja2:58
msgid "page.navbar.browse"
msgstr "Browse"
#: fietsboek/templates/layout.jinja2:61
+msgid "page.navbar.journeys"
+msgstr "Journeys"
+
+#: fietsboek/templates/layout.jinja2:65
msgid "page.navbar.upload"
msgstr "Upload"
-#: fietsboek/templates/layout.jinja2:70
+#: fietsboek/templates/layout.jinja2:74
msgid "page.navbar.user"
msgstr "User"
-#: fietsboek/templates/layout.jinja2:74
+#: fietsboek/templates/layout.jinja2:78
msgid "page.navbar.welcome_user"
msgstr "Welcome, {}!"
-#: fietsboek/templates/layout.jinja2:77
+#: fietsboek/templates/layout.jinja2:81
msgid "page.navbar.logout"
msgstr "Logout"
-#: fietsboek/templates/layout.jinja2:80
+#: fietsboek/templates/layout.jinja2:84
msgid "page.navbar.profile"
msgstr "Profile"
-#: fietsboek/templates/layout.jinja2:83
+#: fietsboek/templates/layout.jinja2:87
msgid "page.navbar.user_data"
msgstr "Personal Data"
-#: fietsboek/templates/layout.jinja2:87
+#: fietsboek/templates/layout.jinja2:91
msgid "page.navbar.admin"
msgstr "Admin"
-#: fietsboek/templates/layout.jinja2:93
+#: fietsboek/templates/layout.jinja2:97
msgid "page.navbar.login"
msgstr "Login"
-#: fietsboek/templates/layout.jinja2:97
+#: fietsboek/templates/layout.jinja2:101
msgid "page.navbar.create_account"
msgstr "Create Account"
@@ -648,91 +960,103 @@ msgstr "Passwords must match"
msgid "page.password_reset.reset"
msgstr "Reset"
-#: fietsboek/templates/profile.jinja2:64
+#: fietsboek/templates/profile.jinja2:10
msgid "page.profile.tabbar.overview"
msgstr "Overview"
-#: fietsboek/templates/profile.jinja2:69
+#: fietsboek/templates/profile.jinja2:15
msgid "page.profile.tabbar.graphs"
msgstr "Graphs"
-#: fietsboek/templates/profile.jinja2:74
+#: fietsboek/templates/profile.jinja2:20
msgid "page.profile.tabbar.calendar"
msgstr "Calendar"
-#: fietsboek/templates/profile.jinja2:88
+#: fietsboek/templates/profile_calendar.jinja2:9
+msgid "page.profile.calendar.previous"
+msgstr "Previous month"
+
+#: fietsboek/templates/profile_calendar.jinja2:11
+msgid "page.profile.calendar.next"
+msgstr "Next month"
+
+#: fietsboek/templates/profile_graphs.jinja2:6
+msgid "page.profile.graph.km_per_month"
+msgstr "Kilometers per month"
+
+#: fietsboek/templates/profile_overview.jinja2:71
msgid "page.profile.length"
msgstr "Length"
-#: fietsboek/templates/profile.jinja2:92
+#: fietsboek/templates/profile_overview.jinja2:75
msgid "page.profile.avg_length"
msgstr "Average Length"
-#: fietsboek/templates/profile.jinja2:96
+#: fietsboek/templates/profile_overview.jinja2:79
msgid "page.profile.uphill"
msgstr "Uphill"
-#: fietsboek/templates/profile.jinja2:100
+#: fietsboek/templates/profile_overview.jinja2:83
msgid "page.profile.downhill"
msgstr "Downhill"
-#: fietsboek/templates/profile.jinja2:104
+#: fietsboek/templates/profile_overview.jinja2:87
msgid "page.profile.moving_time"
msgstr "Moving Time"
-#: fietsboek/templates/profile.jinja2:108
+#: fietsboek/templates/profile_overview.jinja2:91
msgid "page.profile.stopped_time"
msgstr "Stopped Time"
-#: fietsboek/templates/profile.jinja2:112
+#: fietsboek/templates/profile_overview.jinja2:95
msgid "page.profile.avg_duration"
msgstr "Average Duration"
-#: fietsboek/templates/profile.jinja2:116
+#: fietsboek/templates/profile_overview.jinja2:99
msgid "page.profile.max_speed"
msgstr "Max Speed"
-#: fietsboek/templates/profile.jinja2:120
+#: fietsboek/templates/profile_overview.jinja2:103
msgid "page.profile.avg_speed"
msgstr "Average Speed"
-#: fietsboek/templates/profile.jinja2:124
+#: fietsboek/templates/profile_overview.jinja2:107
msgid "page.profile.number_of_tracks"
msgstr "Number of tracks"
-#: fietsboek/templates/profile.jinja2:130
+#: fietsboek/templates/profile_overview.jinja2:112
+msgid "page.profile.contribution_card.title"
+msgstr "Track Activity"
+
+#: fietsboek/templates/profile_overview.jinja2:147
+msgid "page.profile.contribution_card.legend.less"
+msgstr "Fewer km"
+
+#: fietsboek/templates/profile_overview.jinja2:154
+msgid "page.profile.contribution_card.legend.more"
+msgstr "More km"
+
+#: fietsboek/templates/profile_overview.jinja2:161
msgid "page.profile.longest_distance_track"
msgstr "Longest Track"
-#: fietsboek/templates/profile.jinja2:135
+#: fietsboek/templates/profile_overview.jinja2:166
msgid "page.profile.shortest_distance_track"
msgstr "Shortest Track"
-#: fietsboek/templates/profile.jinja2:140
+#: fietsboek/templates/profile_overview.jinja2:171
msgid "page.profile.longest_duration_track"
msgstr "Most Time-Consuming Track"
-#: fietsboek/templates/profile.jinja2:145
+#: fietsboek/templates/profile_overview.jinja2:176
msgid "page.profile.shortest_duration_track"
msgstr "Quickest Track"
-#: fietsboek/templates/profile.jinja2:152
-msgid "page.profile.graph.km_per_month"
-msgstr "Kilometers per month"
-
-#: fietsboek/templates/profile.jinja2:161
-msgid "page.profile.calendar.previous"
-msgstr "Previous month"
-
-#: fietsboek/templates/profile.jinja2:163
-msgid "page.profile.calendar.next"
-msgstr "Next month"
-
-#: fietsboek/templates/profile.jinja2:218
+#: fietsboek/templates/profile_overview.jinja2:197
msgid "page.profile.heatmap"
msgstr "Heat Map"
-#: fietsboek/templates/profile.jinja2:223
+#: fietsboek/templates/profile_overview.jinja2:205
msgid "page.profile.tilehunt"
msgstr "Tilehunt"
@@ -858,11 +1182,11 @@ msgstr "Fix null elevation"
msgid "transformers.fix-null-elevation.description"
msgstr "This transformer fixes the elevation of points whose elevation is unset."
-#: fietsboek/transformers/elevation.py:116
+#: fietsboek/transformers/elevation.py:109
msgid "transformers.fix-elevation-jumps"
msgstr "Fix elevation jumps"
-#: fietsboek/transformers/elevation.py:120
+#: fietsboek/transformers/elevation.py:113
msgid "transformers.fix-elevation-jumps.description"
msgstr "This transformer fixes abrupt jumps in the elevation value."
@@ -878,15 +1202,15 @@ msgstr "Invalid email"
msgid "flash.a_confirmation_link_has_been_sent"
msgstr "A confirmation link has been sent"
-#: fietsboek/views/admin.py:49
+#: fietsboek/views/admin.py:189
msgid "flash.badge_added"
msgstr "Badge has been added"
-#: fietsboek/views/admin.py:73
+#: fietsboek/views/admin.py:213
msgid "flash.badge_modified"
msgstr "Badge has been modified"
-#: fietsboek/views/admin.py:93
+#: fietsboek/views/admin.py:233
msgid "flash.badge_deleted"
msgstr "Badge has been deleted"
@@ -945,23 +1269,27 @@ msgstr "Your email address has been verified"
msgid "flash.password_updated"
msgstr "Password has been updated"
-#: fietsboek/views/detail.py:162
+#: fietsboek/views/detail.py:187
msgid "flash.track_deleted"
msgstr "Track has been deleted"
+#: fietsboek/views/edit.py:97 fietsboek/views/upload.py:63
+msgid "flash.invalid_file"
+msgstr "Invalid GPX file selected"
+
+#: fietsboek/views/journey.py:251
+msgid "flash.journey_deleted"
+msgstr "Journey has been deleted"
+
#: fietsboek/views/upload.py:53
msgid "flash.no_file_selected"
msgstr "No file selected"
-#: fietsboek/views/upload.py:66
-msgid "flash.invalid_file"
-msgstr "Invalid GPX file selected"
-
-#: fietsboek/views/upload.py:192
+#: fietsboek/views/upload.py:177
msgid "flash.upload_success"
msgstr "Upload successful"
-#: fietsboek/views/upload.py:211
+#: fietsboek/views/upload.py:196
msgid "flash.upload_cancelled"
msgstr "Upload cancelled"
diff --git a/fietsboek/locale/fietslog.pot b/fietsboek/locale/fietslog.pot
index 60c77a5..53f697f 100644
--- a/fietsboek/locale/fietslog.pot
+++ b/fietsboek/locale/fietslog.pot
@@ -1,98 +1,240 @@
# Translations template for PROJECT.
-# Copyright (C) 2025 ORGANIZATION
+# Copyright (C) 2026 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
-# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2026.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2025-01-30 21:50+0100\n"
+"POT-Creation-Date: 2026-08-13 17:40+0200\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.15.0\n"
+"Generated-By: Babel 2.18.0\n"
-#: fietsboek/actions.py:267
+#: fietsboek/actions.py:266
msgid "email.verify_mail.subject"
msgstr ""
-#: fietsboek/actions.py:270
+#: fietsboek/actions.py:269
msgid "email.verify.text"
msgstr ""
-#: fietsboek/util.py:333
+#: fietsboek/pdf.py:233
+msgid "pdf.table.date"
+msgstr ""
+
+#: fietsboek/pdf.py:235
+msgid "pdf.table.length"
+msgstr ""
+
+#: fietsboek/pdf.py:239
+msgid "pdf.table.uphill"
+msgstr ""
+
+#: fietsboek/pdf.py:243
+msgid "pdf.table.downhill"
+msgstr ""
+
+#: fietsboek/pdf.py:246
+msgid "pdf.table.moving_time"
+msgstr ""
+
+#: fietsboek/pdf.py:247
+msgid "pdf.table.stopped_time"
+msgstr ""
+
+#: fietsboek/pdf.py:249
+msgid "pdf.table.max_speed"
+msgstr ""
+
+#: fietsboek/pdf.py:253
+msgid "pdf.table.avg_speed"
+msgstr ""
+
+#: fietsboek/summaries.py:429
+msgid "page.profile.contribution_card.tooltip"
+msgid_plural "page.profile.contribution_card.tooltips"
+msgstr[0] ""
+msgstr[1] ""
+
+#: fietsboek/util.py:299
msgid "password_constraint.mismatch"
msgstr ""
-#: fietsboek/util.py:335
+#: fietsboek/util.py:301
msgid "password_constraint.length"
msgstr ""
-#: fietsboek/models/track.py:603
+#: fietsboek/models/track.py:774
msgid "tooltip.table.length"
msgstr ""
-#: fietsboek/models/track.py:604
+#: fietsboek/models/track.py:775
msgid "tooltip.table.people"
msgstr ""
-#: fietsboek/models/track.py:605
+#: fietsboek/models/track.py:776
msgid "tooltip.table.uphill"
msgstr ""
-#: fietsboek/models/track.py:606
+#: fietsboek/models/track.py:777
msgid "tooltip.table.downhill"
msgstr ""
-#: fietsboek/models/track.py:607 fietsboek/templates/home.jinja2:7
+#: fietsboek/models/track.py:778 fietsboek/templates/home.jinja2:7
msgid "tooltip.table.moving_time"
msgstr ""
-#: fietsboek/models/track.py:608 fietsboek/templates/home.jinja2:8
+#: fietsboek/models/track.py:779 fietsboek/templates/home.jinja2:8
msgid "tooltip.table.stopped_time"
msgstr ""
-#: fietsboek/models/track.py:610
+#: fietsboek/models/track.py:781
msgid "tooltip.table.max_speed"
msgstr ""
-#: fietsboek/models/track.py:614
+#: fietsboek/models/track.py:785
msgid "tooltip.table.avg_speed"
msgstr ""
+#: fietsboek/templates/403.jinja2:5
+msgid "403.title"
+msgstr ""
+
+#: fietsboek/templates/403.jinja2:9
+msgid "403.no_access"
+msgstr ""
+
+#: fietsboek/templates/403.jinja2:12
+msgid "403.try_log_in"
+msgstr ""
+
+#: fietsboek/templates/404.jinja2:5
+msgid "404.title"
+msgstr ""
+
+#: fietsboek/templates/404.jinja2:9
+msgid "404.path_not_found"
+msgstr ""
+
+#: fietsboek/templates/404.jinja2:12
+msgid "404.choose_different"
+msgstr ""
+
#: fietsboek/templates/admin.jinja2:5
msgid "page.admin.title"
msgstr ""
-#: fietsboek/templates/admin.jinja2:7
+#: fietsboek/templates/admin.jinja2:10
+msgid "page.admin.nav.overview"
+msgstr ""
+
+#: fietsboek/templates/admin.jinja2:11
+msgid "page.admin.nav.badges"
+msgstr ""
+
+#: fietsboek/templates/admin_badges.jinja2:5
msgid "page.admin.badges"
msgstr ""
-#: fietsboek/templates/admin.jinja2:23
+#: fietsboek/templates/admin_badges.jinja2:21
msgid "page.admin.badge.edit"
msgstr ""
-#: fietsboek/templates/admin.jinja2:29
+#: fietsboek/templates/admin_badges.jinja2:22
msgid "page.admin.badge.delete_badge"
msgstr ""
-#: fietsboek/templates/admin.jinja2:37
+#: fietsboek/templates/admin_badges.jinja2:35
msgid "page.admin.badges.badge_title"
msgstr ""
-#: fietsboek/templates/admin.jinja2:41
+#: fietsboek/templates/admin_badges.jinja2:39
msgid "page.admin.badges.badge_image"
msgstr ""
-#: fietsboek/templates/admin.jinja2:45
+#: fietsboek/templates/admin_badges.jinja2:43
msgid "page.admin.badges.add_badge"
msgstr ""
+#: fietsboek/templates/admin_overview.jinja2:5
+msgid "admin.overview.instance_has"
+msgstr ""
+
+#: fietsboek/templates/admin_overview.jinja2:9
+msgid "admin.overview.stat.user"
+msgid_plural "admin.overview.stat.users"
+msgstr[0] ""
+msgstr[1] ""
+
+#: fietsboek/templates/admin_overview.jinja2:13
+msgid "admin.overview.stat.track"
+msgid_plural "admin.overview.stat.tracks"
+msgstr[0] ""
+msgstr[1] ""
+
+#: fietsboek/templates/admin_overview.jinja2:17
+msgid "admin.overview.stats.mib"
+msgstr ""
+
+#: fietsboek/templates/admin_overview.jinja2:24
+msgid "admin.overview.system_overview"
+msgstr ""
+
+#: fietsboek/templates/admin_overview.jinja2:28
+msgid "admin.overview.fietsboek_version"
+msgstr ""
+
+#: fietsboek/templates/admin_overview.jinja2:32
+msgid "admin.overview.python_version"
+msgstr ""
+
+#: fietsboek/templates/admin_overview.jinja2:36
+msgid "admin.overview.kernel_version"
+msgstr ""
+
+#: fietsboek/templates/admin_overview.jinja2:40
+msgid "admin.overview.distro_version"
+msgstr ""
+
+#: fietsboek/templates/admin_overview.jinja2:44
+msgid "admin.overview.last_cronjob"
+msgstr ""
+
+#: fietsboek/templates/admin_overview.jinja2:55
+msgid "admin.overview.storage_graph.label.track_data"
+msgstr ""
+
+#: fietsboek/templates/admin_overview.jinja2:56
+msgid "admin.overview.storage_graph.label.backups"
+msgstr ""
+
+#: fietsboek/templates/admin_overview.jinja2:57
+msgid "admin.overview.storage_graph.label.images"
+msgstr ""
+
+#: fietsboek/templates/admin_overview.jinja2:58
+msgid "admin.overview.storage_graph.label.track_previews"
+msgstr ""
+
+#: fietsboek/templates/admin_overview.jinja2:59
+msgid "admin.overview.storage_graph.label.journey_previews"
+msgstr ""
+
+#: fietsboek/templates/admin_overview.jinja2:60
+msgid "admin.overview.storage_graph.label.user_maps"
+msgstr ""
+
+#: fietsboek/templates/admin_overview.jinja2:88
+msgid "admin.overview.storage_graph.title"
+msgstr ""
+
#: fietsboek/templates/browse.jinja2:4
msgid "page.browse.title"
msgstr ""
@@ -189,73 +331,100 @@ msgstr ""
msgid "page.browse.synthetic_tooltip"
msgstr ""
-#: fietsboek/templates/browse.jinja2:158 fietsboek/templates/details.jinja2:103
-#: fietsboek/templates/profile.jinja2:15
+#: fietsboek/templates/browse.jinja2:162 fietsboek/templates/details.jinja2:127
+#: fietsboek/templates/journey_details.jinja2:124
+#: fietsboek/templates/profile_overview.jinja2:20
msgid "page.details.date"
msgstr ""
-#: fietsboek/templates/browse.jinja2:160 fietsboek/templates/details.jinja2:117
-#: fietsboek/templates/profile.jinja2:17
+#: fietsboek/templates/browse.jinja2:164 fietsboek/templates/details.jinja2:141
+#: fietsboek/templates/journey_details.jinja2:76
+#: fietsboek/templates/journey_details.jinja2:126
+#: fietsboek/templates/profile_overview.jinja2:22
msgid "page.details.length"
msgstr ""
-#: fietsboek/templates/browse.jinja2:165 fietsboek/templates/details.jinja2:108
-#: fietsboek/templates/profile.jinja2:21
+#: fietsboek/templates/browse.jinja2:169 fietsboek/templates/details.jinja2:132
+#: fietsboek/templates/journey_details.jinja2:131
+#: fietsboek/templates/profile_overview.jinja2:26
msgid "page.details.start_time"
msgstr ""
-#: fietsboek/templates/browse.jinja2:167 fietsboek/templates/details.jinja2:112
-#: fietsboek/templates/profile.jinja2:23
+#: fietsboek/templates/browse.jinja2:171 fietsboek/templates/details.jinja2:136
+#: fietsboek/templates/journey_details.jinja2:133
+#: fietsboek/templates/profile_overview.jinja2:28
msgid "page.details.end_time"
msgstr ""
-#: fietsboek/templates/browse.jinja2:172 fietsboek/templates/details.jinja2:121
-#: fietsboek/templates/profile.jinja2:27
+#: fietsboek/templates/browse.jinja2:176 fietsboek/templates/details.jinja2:145
+#: fietsboek/templates/journey_details.jinja2:80
+#: fietsboek/templates/journey_details.jinja2:138
+#: fietsboek/templates/profile_overview.jinja2:32
msgid "page.details.uphill"
msgstr ""
-#: fietsboek/templates/browse.jinja2:174 fietsboek/templates/details.jinja2:125
-#: fietsboek/templates/profile.jinja2:29
+#: fietsboek/templates/browse.jinja2:178 fietsboek/templates/details.jinja2:149
+#: fietsboek/templates/journey_details.jinja2:84
+#: fietsboek/templates/journey_details.jinja2:140
+#: fietsboek/templates/profile_overview.jinja2:34
msgid "page.details.downhill"
msgstr ""
-#: fietsboek/templates/browse.jinja2:179 fietsboek/templates/details.jinja2:130
-#: fietsboek/templates/profile.jinja2:33
+#: fietsboek/templates/browse.jinja2:183 fietsboek/templates/details.jinja2:154
+#: fietsboek/templates/journey_details.jinja2:88
+#: fietsboek/templates/journey_details.jinja2:145
+#: fietsboek/templates/profile_overview.jinja2:38
msgid "page.details.moving_time"
msgstr ""
-#: fietsboek/templates/browse.jinja2:181 fietsboek/templates/details.jinja2:134
-#: fietsboek/templates/profile.jinja2:35
+#: fietsboek/templates/browse.jinja2:185 fietsboek/templates/details.jinja2:158
+#: fietsboek/templates/journey_details.jinja2:92
+#: fietsboek/templates/journey_details.jinja2:147
+#: fietsboek/templates/profile_overview.jinja2:40
msgid "page.details.stopped_time"
msgstr ""
-#: fietsboek/templates/browse.jinja2:185 fietsboek/templates/details.jinja2:138
-#: fietsboek/templates/profile.jinja2:39
+#: fietsboek/templates/browse.jinja2:189 fietsboek/templates/details.jinja2:162
+#: fietsboek/templates/journey_details.jinja2:96
+#: fietsboek/templates/journey_details.jinja2:151
+#: fietsboek/templates/profile_overview.jinja2:44
msgid "page.details.max_speed"
msgstr ""
-#: fietsboek/templates/browse.jinja2:187 fietsboek/templates/details.jinja2:142
-#: fietsboek/templates/profile.jinja2:41
+#: fietsboek/templates/browse.jinja2:191 fietsboek/templates/details.jinja2:166
+#: fietsboek/templates/journey_details.jinja2:100
+#: fietsboek/templates/journey_details.jinja2:153
+#: fietsboek/templates/profile_overview.jinja2:46
msgid "page.details.avg_speed"
msgstr ""
-#: fietsboek/templates/browse.jinja2:192
+#: fietsboek/templates/browse.jinja2:196
+#: fietsboek/templates/journey_details.jinja2:158
msgid "page.browse.card.comments"
msgstr ""
-#: fietsboek/templates/browse.jinja2:194
+#: fietsboek/templates/browse.jinja2:198
+#: fietsboek/templates/journey_details.jinja2:160
msgid "page.browse.card.images"
msgstr ""
-#: fietsboek/templates/browse.jinja2:211
+#: fietsboek/templates/browse.jinja2:216
msgid "page.browse.download_multiple"
msgstr ""
-#: fietsboek/templates/browse.jinja2:213
+#: fietsboek/templates/browse.jinja2:222 fietsboek/templates/browse.jinja2:226
+msgid "pagination.previous"
+msgstr ""
+
+#: fietsboek/templates/browse.jinja2:231 fietsboek/templates/browse.jinja2:235
+msgid "pagination.next"
+msgstr ""
+
+#: fietsboek/templates/browse.jinja2:242
msgid "page.browse.no_results"
msgstr ""
-#: fietsboek/templates/browse.jinja2:215
+#: fietsboek/templates/browse.jinja2:244
msgid "page.browse.no_tracks"
msgstr ""
@@ -299,88 +468,93 @@ msgstr ""
msgid "page.create_account.create"
msgstr ""
-#: fietsboek/templates/details.jinja2:7
+#: fietsboek/templates/details.jinja2:24
msgid "page.details.title"
msgstr ""
-#: fietsboek/templates/details.jinja2:20
+#: fietsboek/templates/details.jinja2:37
msgid "page.details.edit"
msgstr ""
-#: fietsboek/templates/details.jinja2:21
+#: fietsboek/templates/details.jinja2:38
msgid "page.details.share"
msgstr ""
-#: fietsboek/templates/details.jinja2:22
+#: fietsboek/templates/details.jinja2:39
msgid "page.details.delete"
msgstr ""
-#: fietsboek/templates/details.jinja2:28
+#: fietsboek/templates/details.jinja2:45
msgid "page.details.sharelink.title"
msgstr ""
-#: fietsboek/templates/details.jinja2:32
+#: fietsboek/templates/details.jinja2:49
msgid "page.details.sharelink.info"
msgstr ""
-#: fietsboek/templates/details.jinja2:39
+#: fietsboek/templates/details.jinja2:56
msgid "page.details.sharelink.invalidate"
msgstr ""
-#: fietsboek/templates/details.jinja2:41
+#: fietsboek/templates/details.jinja2:58
msgid "page.details.sharelink.close"
msgstr ""
-#: fietsboek/templates/details.jinja2:51
+#: fietsboek/templates/details.jinja2:68
msgid "page.details.delete.title"
msgstr ""
-#: fietsboek/templates/details.jinja2:55
+#: fietsboek/templates/details.jinja2:72
msgid "page.details.delete.info"
msgstr ""
-#: fietsboek/templates/details.jinja2:60
+#: fietsboek/templates/details.jinja2:77
msgid "page.details.delete.delete"
msgstr ""
-#: fietsboek/templates/details.jinja2:62
+#: fietsboek/templates/details.jinja2:79
msgid "page.details.delete.close"
msgstr ""
-#: fietsboek/templates/details.jinja2:81
+#: fietsboek/templates/details.jinja2:98
msgid "page.details.tags"
msgstr ""
-#: fietsboek/templates/details.jinja2:91 fietsboek/templates/edit.jinja2:10
+#: fietsboek/templates/details.jinja2:108 fietsboek/templates/edit.jinja2:10
#: fietsboek/templates/finish_upload.jinja2:10
+#: fietsboek/templates/journey_details.jinja2:66
msgid "page.noscript"
msgstr ""
-#: fietsboek/templates/details.jinja2:97
+#: fietsboek/templates/details.jinja2:115
msgid "page.details.download"
msgstr ""
-#: fietsboek/templates/details.jinja2:187
+#: fietsboek/templates/details.jinja2:120
+msgid "page.details.download_pdf"
+msgstr ""
+
+#: fietsboek/templates/details.jinja2:211
msgid "page.details.comments"
msgstr ""
-#: fietsboek/templates/details.jinja2:191
+#: fietsboek/templates/details.jinja2:215
msgid "page.details.comments.author"
msgstr ""
-#: fietsboek/templates/details.jinja2:208
+#: fietsboek/templates/details.jinja2:232
msgid "page.details.comments.new.title"
msgstr ""
-#: fietsboek/templates/details.jinja2:211
+#: fietsboek/templates/details.jinja2:235
msgid "page.details.comments.new.input_title"
msgstr ""
-#: fietsboek/templates/details.jinja2:212
+#: fietsboek/templates/details.jinja2:236
msgid "page.details.comments.new.input_comment"
msgstr ""
-#: fietsboek/templates/details.jinja2:215
+#: fietsboek/templates/details.jinja2:239
msgid "page.details.comments.new.submit"
msgstr ""
@@ -388,11 +562,15 @@ msgstr ""
msgid "page.edit.title"
msgstr ""
-#: fietsboek/templates/edit.jinja2:16
+#: fietsboek/templates/edit.jinja2:14
+msgid "page.edit.form.new_track"
+msgstr ""
+
+#: fietsboek/templates/edit.jinja2:20
msgid "page.edit.form.submit"
msgstr ""
-#: fietsboek/templates/edit.jinja2:17
+#: fietsboek/templates/edit.jinja2:21
msgid "page.edit.form.cancel"
msgstr ""
@@ -532,61 +710,177 @@ msgid "page.home.unfinished_uploads"
msgstr ""
#: fietsboek/templates/home.jinja2:44 fietsboek/templates/home.jinja2:53
-#: fietsboek/templates/home.jinja2:97
+#: fietsboek/templates/home.jinja2:99
msgid "page.home.summary.track"
msgid_plural "page.home.summary.tracks"
msgstr[0] ""
msgstr[1] ""
-#: fietsboek/templates/home.jinja2:97
+#: fietsboek/templates/home.jinja2:99
msgid "page.home.total"
msgstr ""
-#: fietsboek/templates/layout.jinja2:43
+#: fietsboek/templates/journey_details.jinja2:10
+msgid "journey.edit"
+msgstr ""
+
+#: fietsboek/templates/journey_details.jinja2:11
+msgid "journey.share"
+msgstr ""
+
+#: fietsboek/templates/journey_details.jinja2:12
+msgid "journey.delete"
+msgstr ""
+
+#: fietsboek/templates/journey_details.jinja2:18
+msgid "journey.sharelink.title"
+msgstr ""
+
+#: fietsboek/templates/journey_details.jinja2:22
+msgid "journey.sharelink.info"
+msgstr ""
+
+#: fietsboek/templates/journey_details.jinja2:29
+msgid "journey.sharelink.invalidate"
+msgstr ""
+
+#: fietsboek/templates/journey_details.jinja2:31
+msgid "journey.sharelink.close"
+msgstr ""
+
+#: fietsboek/templates/journey_details.jinja2:41
+msgid "journey.delete.title"
+msgstr ""
+
+#: fietsboek/templates/journey_details.jinja2:45
+msgid "journey.delete.info"
+msgstr ""
+
+#: fietsboek/templates/journey_details.jinja2:50
+msgid "journey.delete.delete"
+msgstr ""
+
+#: fietsboek/templates/journey_details.jinja2:52
+msgid "journey.delete.close"
+msgstr ""
+
+#: fietsboek/templates/journey_details.jinja2:108
+msgid "journey.tracks"
+msgstr ""
+
+#: fietsboek/templates/journey_details.jinja2:174
+msgid "journeys.track.hidden"
+msgstr ""
+
+#: fietsboek/templates/journey_form.jinja2:40
+msgid "journeys.new.form.title"
+msgstr ""
+
+#: fietsboek/templates/journey_form.jinja2:43
+msgid "journeys.new.form.requires_title"
+msgstr ""
+
+#: fietsboek/templates/journey_form.jinja2:47
+msgid "journeys.new.form.description"
+msgstr ""
+
+#: fietsboek/templates/journey_form.jinja2:51
+msgid "journeys.new.form.visibility"
+msgstr ""
+
+#: fietsboek/templates/journey_form.jinja2:54
+msgid "journeys.new.form.visibility.private"
+msgstr ""
+
+#: fietsboek/templates/journey_form.jinja2:55
+msgid "journeys.new.form.visibility.friends"
+msgstr ""
+
+#: fietsboek/templates/journey_form.jinja2:56
+msgid "journeys.new.form.visibility.logged_in"
+msgstr ""
+
+#: fietsboek/templates/journey_form.jinja2:57
+msgid "journeys.new.form.visibility.public"
+msgstr ""
+
+#: fietsboek/templates/journey_form.jinja2:62
+msgid "journeys.new.form.tracksearch"
+msgstr ""
+
+#: fietsboek/templates/journey_form.jinja2:71
+msgid "journeys.new.form.tracks"
+msgstr ""
+
+#: fietsboek/templates/journey_form.jinja2:90
+msgid "journeys.new.form.submit"
+msgstr ""
+
+#: fietsboek/templates/journey_form.jinja2:93
+msgid "journeys.new.form.requires_tracks"
+msgstr ""
+
+#: fietsboek/templates/journey_list.jinja2:4
+msgid "journeys.overview.title"
+msgstr ""
+
+#: fietsboek/templates/journey_list.jinja2:10
+msgid "journeys.overview.new"
+msgstr ""
+
+#: fietsboek/templates/journey_new.jinja2:10
+msgid "journeys.new.title"
+msgstr ""
+
+#: fietsboek/templates/layout.jinja2:44
msgid "page.navbar.toggle"
msgstr ""
-#: fietsboek/templates/layout.jinja2:54
+#: fietsboek/templates/layout.jinja2:55
msgid "page.navbar.home"
msgstr ""
-#: fietsboek/templates/layout.jinja2:57
+#: fietsboek/templates/layout.jinja2:58
msgid "page.navbar.browse"
msgstr ""
#: fietsboek/templates/layout.jinja2:61
+msgid "page.navbar.journeys"
+msgstr ""
+
+#: fietsboek/templates/layout.jinja2:65
msgid "page.navbar.upload"
msgstr ""
-#: fietsboek/templates/layout.jinja2:70
+#: fietsboek/templates/layout.jinja2:74
msgid "page.navbar.user"
msgstr ""
-#: fietsboek/templates/layout.jinja2:74
+#: fietsboek/templates/layout.jinja2:78
msgid "page.navbar.welcome_user"
msgstr ""
-#: fietsboek/templates/layout.jinja2:77
+#: fietsboek/templates/layout.jinja2:81
msgid "page.navbar.logout"
msgstr ""
-#: fietsboek/templates/layout.jinja2:80
+#: fietsboek/templates/layout.jinja2:84
msgid "page.navbar.profile"
msgstr ""
-#: fietsboek/templates/layout.jinja2:83
+#: fietsboek/templates/layout.jinja2:87
msgid "page.navbar.user_data"
msgstr ""
-#: fietsboek/templates/layout.jinja2:87
+#: fietsboek/templates/layout.jinja2:91
msgid "page.navbar.admin"
msgstr ""
-#: fietsboek/templates/layout.jinja2:93
+#: fietsboek/templates/layout.jinja2:97
msgid "page.navbar.login"
msgstr ""
-#: fietsboek/templates/layout.jinja2:97
+#: fietsboek/templates/layout.jinja2:101
msgid "page.navbar.create_account"
msgstr ""
@@ -642,91 +936,103 @@ msgstr ""
msgid "page.password_reset.reset"
msgstr ""
-#: fietsboek/templates/profile.jinja2:64
+#: fietsboek/templates/profile.jinja2:10
msgid "page.profile.tabbar.overview"
msgstr ""
-#: fietsboek/templates/profile.jinja2:69
+#: fietsboek/templates/profile.jinja2:15
msgid "page.profile.tabbar.graphs"
msgstr ""
-#: fietsboek/templates/profile.jinja2:74
+#: fietsboek/templates/profile.jinja2:20
msgid "page.profile.tabbar.calendar"
msgstr ""
-#: fietsboek/templates/profile.jinja2:88
+#: fietsboek/templates/profile_calendar.jinja2:9
+msgid "page.profile.calendar.previous"
+msgstr ""
+
+#: fietsboek/templates/profile_calendar.jinja2:11
+msgid "page.profile.calendar.next"
+msgstr ""
+
+#: fietsboek/templates/profile_graphs.jinja2:6
+msgid "page.profile.graph.km_per_month"
+msgstr ""
+
+#: fietsboek/templates/profile_overview.jinja2:71
msgid "page.profile.length"
msgstr ""
-#: fietsboek/templates/profile.jinja2:92
+#: fietsboek/templates/profile_overview.jinja2:75
msgid "page.profile.avg_length"
msgstr ""
-#: fietsboek/templates/profile.jinja2:96
+#: fietsboek/templates/profile_overview.jinja2:79
msgid "page.profile.uphill"
msgstr ""
-#: fietsboek/templates/profile.jinja2:100
+#: fietsboek/templates/profile_overview.jinja2:83
msgid "page.profile.downhill"
msgstr ""
-#: fietsboek/templates/profile.jinja2:104
+#: fietsboek/templates/profile_overview.jinja2:87
msgid "page.profile.moving_time"
msgstr ""
-#: fietsboek/templates/profile.jinja2:108
+#: fietsboek/templates/profile_overview.jinja2:91
msgid "page.profile.stopped_time"
msgstr ""
-#: fietsboek/templates/profile.jinja2:112
+#: fietsboek/templates/profile_overview.jinja2:95
msgid "page.profile.avg_duration"
msgstr ""
-#: fietsboek/templates/profile.jinja2:116
+#: fietsboek/templates/profile_overview.jinja2:99
msgid "page.profile.max_speed"
msgstr ""
-#: fietsboek/templates/profile.jinja2:120
+#: fietsboek/templates/profile_overview.jinja2:103
msgid "page.profile.avg_speed"
msgstr ""
-#: fietsboek/templates/profile.jinja2:124
+#: fietsboek/templates/profile_overview.jinja2:107
msgid "page.profile.number_of_tracks"
msgstr ""
-#: fietsboek/templates/profile.jinja2:130
-msgid "page.profile.longest_distance_track"
+#: fietsboek/templates/profile_overview.jinja2:112
+msgid "page.profile.contribution_card.title"
msgstr ""
-#: fietsboek/templates/profile.jinja2:135
-msgid "page.profile.shortest_distance_track"
+#: fietsboek/templates/profile_overview.jinja2:147
+msgid "page.profile.contribution_card.legend.less"
msgstr ""
-#: fietsboek/templates/profile.jinja2:140
-msgid "page.profile.longest_duration_track"
+#: fietsboek/templates/profile_overview.jinja2:154
+msgid "page.profile.contribution_card.legend.more"
msgstr ""
-#: fietsboek/templates/profile.jinja2:145
-msgid "page.profile.shortest_duration_track"
+#: fietsboek/templates/profile_overview.jinja2:161
+msgid "page.profile.longest_distance_track"
msgstr ""
-#: fietsboek/templates/profile.jinja2:152
-msgid "page.profile.graph.km_per_month"
+#: fietsboek/templates/profile_overview.jinja2:166
+msgid "page.profile.shortest_distance_track"
msgstr ""
-#: fietsboek/templates/profile.jinja2:161
-msgid "page.profile.calendar.previous"
+#: fietsboek/templates/profile_overview.jinja2:171
+msgid "page.profile.longest_duration_track"
msgstr ""
-#: fietsboek/templates/profile.jinja2:163
-msgid "page.profile.calendar.next"
+#: fietsboek/templates/profile_overview.jinja2:176
+msgid "page.profile.shortest_duration_track"
msgstr ""
-#: fietsboek/templates/profile.jinja2:218
+#: fietsboek/templates/profile_overview.jinja2:197
msgid "page.profile.heatmap"
msgstr ""
-#: fietsboek/templates/profile.jinja2:223
+#: fietsboek/templates/profile_overview.jinja2:205
msgid "page.profile.tilehunt"
msgstr ""
@@ -846,11 +1152,11 @@ msgstr ""
msgid "transformers.fix-null-elevation.description"
msgstr ""
-#: fietsboek/transformers/elevation.py:116
+#: fietsboek/transformers/elevation.py:109
msgid "transformers.fix-elevation-jumps"
msgstr ""
-#: fietsboek/transformers/elevation.py:120
+#: fietsboek/transformers/elevation.py:113
msgid "transformers.fix-elevation-jumps.description"
msgstr ""
@@ -866,15 +1172,15 @@ msgstr ""
msgid "flash.a_confirmation_link_has_been_sent"
msgstr ""
-#: fietsboek/views/admin.py:49
+#: fietsboek/views/admin.py:189
msgid "flash.badge_added"
msgstr ""
-#: fietsboek/views/admin.py:73
+#: fietsboek/views/admin.py:213
msgid "flash.badge_modified"
msgstr ""
-#: fietsboek/views/admin.py:93
+#: fietsboek/views/admin.py:233
msgid "flash.badge_deleted"
msgstr ""
@@ -930,23 +1236,27 @@ msgstr ""
msgid "flash.password_updated"
msgstr ""
-#: fietsboek/views/detail.py:162
+#: fietsboek/views/detail.py:187
msgid "flash.track_deleted"
msgstr ""
-#: fietsboek/views/upload.py:53
-msgid "flash.no_file_selected"
+#: fietsboek/views/edit.py:97 fietsboek/views/upload.py:63
+msgid "flash.invalid_file"
msgstr ""
-#: fietsboek/views/upload.py:66
-msgid "flash.invalid_file"
+#: fietsboek/views/journey.py:251
+msgid "flash.journey_deleted"
+msgstr ""
+
+#: fietsboek/views/upload.py:53
+msgid "flash.no_file_selected"
msgstr ""
-#: fietsboek/views/upload.py:192
+#: fietsboek/views/upload.py:177
msgid "flash.upload_success"
msgstr ""
-#: fietsboek/views/upload.py:211
+#: fietsboek/views/upload.py:196
msgid "flash.upload_cancelled"
msgstr ""
diff --git a/fietsboek/models/__init__.py b/fietsboek/models/__init__.py
index 6f91eae..004ed8e 100644
--- a/fietsboek/models/__init__.py
+++ b/fietsboek/models/__init__.py
@@ -5,13 +5,14 @@ access the submodules if you need some of the auxiliary definitions.
"""
import zope.sqlalchemy
-from sqlalchemy import engine_from_config
+from sqlalchemy import engine_from_config, event
from sqlalchemy.orm import configure_mappers, sessionmaker
from .badge import Badge # flake8: noqa
from .comment import Comment # flake8: noqa
from .image import ImageMetadata # flake8: noqa
-from .track import Tag, Track, TrackCache, Upload # flake8: noqa
+from .journey import Journey
+from .track import Tag, Track, TrackCache, Upload, Waypoint # flake8: noqa
# Import or define all models here to ensure they are attached to the
# ``Base.metadata`` prior to any initialization routines.
@@ -24,7 +25,17 @@ configure_mappers()
def get_engine(settings, prefix="sqlalchemy."):
"""Create an SQL Engine from the given settings."""
- return engine_from_config(settings, prefix)
+ engine = engine_from_config(settings, prefix)
+
+ # SQLite is quite loose with foreign keys by default, so make sure it
+ # checks them.
+ def _fk_pragma_on_connect(dbapi_con, _con_record):
+ dbapi_con.execute("PRAGMA foreign_keys=ON;")
+ dbapi_con.execute("PRAGMA journal_mode=WAL;")
+
+ if engine.dialect.name == "sqlite":
+ event.listen(engine, "connect", _fk_pragma_on_connect)
+ return engine
def get_session_factory(engine):
diff --git a/fietsboek/models/badge.py b/fietsboek/models/badge.py
index 2a6ef95..d80d9fb 100644
--- a/fietsboek/models/badge.py
+++ b/fietsboek/models/badge.py
@@ -3,8 +3,8 @@
from typing import TYPE_CHECKING
from pyramid.httpexceptions import HTTPNotFound
-from sqlalchemy import Column, Integer, LargeBinary, Text, select
-from sqlalchemy.orm import Mapped, relationship
+from sqlalchemy import Integer, LargeBinary, Text, select
+from sqlalchemy.orm import Mapped, mapped_column, relationship
from .meta import Base
@@ -29,9 +29,9 @@ class Badge(Base):
# pylint: disable=too-few-public-methods
__tablename__ = "badges"
- id = Column(Integer, primary_key=True)
- title = Column(Text)
- image = Column(LargeBinary)
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
+ title: Mapped[str] = mapped_column(Text)
+ image: Mapped[bytes] = mapped_column(LargeBinary)
tracks: Mapped[list["Track"]] = relationship(
"Track", secondary="track_badge_assoc", back_populates="badges"
diff --git a/fietsboek/models/comment.py b/fietsboek/models/comment.py
index e1762d5..a06b595 100644
--- a/fietsboek/models/comment.py
+++ b/fietsboek/models/comment.py
@@ -1,9 +1,10 @@
"""Comment model."""
+import datetime
from typing import TYPE_CHECKING
-from sqlalchemy import Column, DateTime, ForeignKey, Integer, Text
-from sqlalchemy.orm import Mapped, relationship
+from sqlalchemy import DateTime, ForeignKey, Integer, Text
+from sqlalchemy.orm import Mapped, mapped_column, relationship
from .meta import Base
@@ -35,12 +36,12 @@ class Comment(Base):
# pylint: disable=too-few-public-methods
__tablename__ = "comments"
- id = Column(Integer, primary_key=True)
- author_id = Column(Integer, ForeignKey("users.id"))
- track_id = Column(Integer, ForeignKey("tracks.id"))
- date = Column(DateTime(False))
- title = Column(Text)
- text = Column(Text)
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
+ author_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("users.id"))
+ track_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("tracks.id"))
+ date: Mapped[datetime.datetime | None] = mapped_column(DateTime(False))
+ title: Mapped[str | None] = mapped_column(Text)
+ text: Mapped[str | None] = mapped_column(Text)
author: Mapped["User"] = relationship("User", back_populates="comments")
track: Mapped["Track"] = relationship("Track", back_populates="comments")
diff --git a/fietsboek/models/image.py b/fietsboek/models/image.py
index dfa9ffb..1f0d4a9 100644
--- a/fietsboek/models/image.py
+++ b/fietsboek/models/image.py
@@ -6,8 +6,8 @@ image description here.
from typing import TYPE_CHECKING
-from sqlalchemy import Column, ForeignKey, Integer, Text, UniqueConstraint, select
-from sqlalchemy.orm import Mapped, relationship
+from sqlalchemy import ForeignKey, Integer, Text, UniqueConstraint, select
+from sqlalchemy.orm import Mapped, mapped_column, relationship
from .meta import Base
@@ -32,10 +32,10 @@ class ImageMetadata(Base):
# pylint: disable=too-few-public-methods
__tablename__ = "image_metadata"
- id = Column(Integer, primary_key=True)
- track_id = Column(Integer, ForeignKey("tracks.id"), nullable=False)
- image_name = Column(Text, nullable=False)
- description = Column(Text)
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
+ track_id: Mapped[int] = mapped_column(Integer, ForeignKey("tracks.id"), nullable=False)
+ image_name: Mapped[str] = mapped_column(Text, nullable=False)
+ description: Mapped[str] = mapped_column(Text)
track: Mapped["Track"] = relationship("Track", back_populates="images")
diff --git a/fietsboek/models/journey.py b/fietsboek/models/journey.py
new file mode 100644
index 0000000..0f1639e
--- /dev/null
+++ b/fietsboek/models/journey.py
@@ -0,0 +1,190 @@
+"""Journey model definition.
+
+A Journey is an ordered collection of tracks, with a title and (optionally) a description.
+"""
+
+import dataclasses
+import datetime
+import enum
+import logging
+from typing import TYPE_CHECKING, Self
+
+from pyramid.authorization import (
+ ALL_PERMISSIONS,
+ Allow,
+ Authenticated,
+ Everyone,
+)
+from pyramid.httpexceptions import HTTPNotFound
+from pyramid.request import Request
+from sqlalchemy import (
+ Column,
+ Enum,
+ ForeignKey,
+ Integer,
+ Table,
+ Text,
+ delete,
+ insert,
+ inspect,
+ select,
+)
+from sqlalchemy.orm import Mapped, mapped_column, relationship
+
+from .. import geo
+from .meta import Base
+
+if TYPE_CHECKING:
+ from .. import models
+
+
+LOGGER = logging.getLogger(__name__)
+
+
+class Visibility(enum.Enum):
+ """An enum representing the visibility of Journeys."""
+
+ PRIVATE = enum.auto()
+ """Only the owner can see the journey."""
+
+ FRIENDS = enum.auto()
+ """Friends can see the journey."""
+
+ LOGGED_IN = enum.auto()
+ """Logged in users can see the journey."""
+
+ PUBLIC = enum.auto()
+ """Everybody can see the journey."""
+
+
+journey_track_assoc = Table(
+ "journey_track_assoc",
+ Base.metadata,
+ Column("journey_id", ForeignKey("journeys.id"), primary_key=True),
+ Column("track_id", ForeignKey("tracks.id"), primary_key=True),
+ Column("sort_index", Integer, nullable=False),
+)
+
+
+class Journey(Base):
+ """A :class:`Journey` represents a collection of tracks, with a title and description."""
+
+ __tablename__ = "journeys"
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
+ owner_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False)
+ title: Mapped[str] = mapped_column(Text, nullable=False)
+ description: Mapped[str] = mapped_column(Text, nullable=False)
+ visibility: Mapped[Visibility] = mapped_column(
+ Enum(Visibility, name="journey_visibility"),
+ nullable=False,
+ )
+ link_secret: Mapped[str | None] = mapped_column(Text)
+
+ owner: Mapped["models.User"] = relationship("User", back_populates="journeys")
+ tracks: Mapped[list["models.Track"]] = relationship(
+ "Track",
+ back_populates="journeys",
+ secondary=journey_track_assoc,
+ order_by=journey_track_assoc.c.sort_index,
+ )
+
+ @classmethod
+ def factory(cls, request: Request) -> Self:
+ """Factory method to pass to a route definition.
+
+ This factory retrieves the journey based on the ``journey_id`` matched
+ route parameter, and returns the journey. If the journey is not found,
+ ``HTTPNotFound`` is raised.
+
+ :raises pyramid.httpexception.NotFound: If the journey is not found.
+ :param request: The pyramid request.
+ :type request: ~pyramid.request.Request
+ :return: The journey.
+ :type: Track
+ """
+ journey_id = request.matchdict["journey_id"]
+ query = select(cls).filter_by(id=journey_id)
+ journey = request.dbsession.execute(query).scalar_one_or_none()
+ if journey is None:
+ raise HTTPNotFound()
+ return journey
+
+ def __acl__(self):
+ # Basic ACL: Permissions for the admin, the owner and the share link
+ acl = [
+ (Allow, "group:admins", ALL_PERMISSIONS),
+ (
+ Allow,
+ f"user:{self.owner_id}",
+ [
+ "journey.view",
+ "journey.edit",
+ "journey.unshare",
+ "journey.comment",
+ "journey.delete",
+ ],
+ ),
+ (Allow, f"secret:{self.link_secret}", "journey.view"),
+ ]
+
+ if self.visibility == Visibility.PUBLIC:
+ acl.append((Allow, Everyone, "journey.view"))
+ acl.append((Allow, Authenticated, "journey.comment"))
+ elif self.visibility == Visibility.LOGGED_IN:
+ acl.append((Allow, Authenticated, ["journey.view", "journey.comment"]))
+ elif self.visibility == Visibility.FRIENDS:
+ acl.extend(
+ (Allow, f"user:{friend.id}", ["journey.view", "journey.comment"])
+ for friend in self.owner.get_friends()
+ )
+ return acl
+
+ def set_track_ids(self, track_ids: list[int]):
+ """Sets the IDs of the contained tracks.
+
+ The order is relevant and will be saved.
+
+ Needs to have a session, as it will directly issue INSERT statements.
+
+ :param track_ids: The IDs of the tracks that should be in this journey.
+ """
+ session = inspect(self).session
+ assert session is not None, "Can only use set_track_ids() if journey is in a session"
+ del_stmt = delete(journey_track_assoc).where(journey_track_assoc.c.journey_id == self.id)
+ session.execute(del_stmt)
+ for index, track_id in enumerate(track_ids, 1):
+ ins_stmt = insert(journey_track_assoc).values(
+ journey_id=self.id,
+ track_id=track_id,
+ sort_index=index,
+ )
+ session.execute(ins_stmt)
+
+ def path(self) -> geo.Path:
+ """Returns the concatenated path of all contained tracks."""
+ offset = 0.0
+ points = []
+ for track in self.tracks:
+ point = None
+ for point in track.path().points:
+ new_point = dataclasses.replace(point, time_offset=point.time_offset + offset)
+ points.append(new_point)
+ if point:
+ offset += point.time_offset
+ return geo.Path(points)
+
+ def gpx_xml(self) -> bytes:
+ """Returns a GPX XML that represents this journey.
+
+ :return: The XML file.
+ """
+ return geo.gpx_xml(
+ self.title,
+ self.description,
+ datetime.datetime.fromtimestamp(0).replace(tzinfo=datetime.UTC),
+ self.path().points,
+ [],
+ )
+
+
+__all__ = ["Journey", "Visibility"]
diff --git a/fietsboek/models/meta.py b/fietsboek/models/meta.py
index 45723fd..0e7dd15 100644
--- a/fietsboek/models/meta.py
+++ b/fietsboek/models/meta.py
@@ -1,6 +1,6 @@
"""Base metadata definition for the SQLAlchemy models."""
-from sqlalchemy.orm import declarative_base
+from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.schema import MetaData
# Recommended naming convention used by Alembic, as various different database
@@ -14,8 +14,14 @@ NAMING_CONVENTION = {
"pk": "pk_%(table_name)s",
}
-metadata = MetaData(naming_convention=NAMING_CONVENTION)
-Base = declarative_base(metadata=metadata)
+sqla_metadata = MetaData(naming_convention=NAMING_CONVENTION)
-__all__ = ["NAMING_CONVENTION", "metadata", "Base"]
+class Base(DeclarativeBase):
+ """Base class for SQLAlchemy model definitions."""
+
+ # pylint: disable=too-few-public-methods
+ metadata = sqla_metadata
+
+
+__all__ = ["NAMING_CONVENTION", "sqla_metadata", "Base"]
diff --git a/fietsboek/models/track.py b/fietsboek/models/track.py
index 0737982..95e341f 100644
--- a/fietsboek/models/track.py
+++ b/fietsboek/models/track.py
@@ -12,14 +12,17 @@ example all cached data to be re-computed without interfering with the other
meta information.
"""
+# pylint: disable=too-many-lines
+
import datetime
import enum
import gzip
+import json
import logging
from itertools import chain
-from typing import TYPE_CHECKING, Optional, Union
+from typing import TYPE_CHECKING, Optional
-import gpxpy
+import sqlalchemy.types
from babel.numbers import format_decimal
from markupsafe import Markup
from pyramid.authorization import (
@@ -34,7 +37,6 @@ from pyramid.httpexceptions import HTTPNotFound
from pyramid.i18n import Localizer
from pyramid.i18n import TranslationString as _
from sqlalchemy import (
- JSON,
Column,
DateTime,
Enum,
@@ -44,11 +46,14 @@ from sqlalchemy import (
LargeBinary,
Table,
Text,
+ delete,
+ insert,
+ inspect,
select,
)
-from sqlalchemy.orm import Mapped, relationship
+from sqlalchemy.orm import Mapped, mapped_column, relationship
-from .. import util
+from .. import geo, util
from .meta import Base
if TYPE_CHECKING:
@@ -58,6 +63,24 @@ if TYPE_CHECKING:
LOGGER = logging.getLogger(__name__)
+class JsonText(sqlalchemy.types.TypeDecorator):
+ """Saves objects serialized as JSON but keeps the column as a Text."""
+
+ # This is straight from the SQLAlchemy documentation, so the non-overriden
+ # methods should be fine.
+ # pylint: disable=too-many-ancestors,abstract-method
+
+ impl = sqlalchemy.types.Text
+
+ cache_ok = True
+
+ def process_bind_param(self, value, dialect):
+ return json.dumps(value)
+
+ def process_result_value(self, value, dialect):
+ return json.loads(value)
+
+
class Tag(Base):
"""A tag is a single keyword associated with a track.
@@ -71,8 +94,8 @@ class Tag(Base):
# pylint: disable=too-few-public-methods
__tablename__ = "tags"
- track_id = Column(Integer, ForeignKey("tracks.id"), primary_key=True)
- tag = Column(Text, primary_key=True)
+ track_id: Mapped[int] = mapped_column(Integer, ForeignKey("tracks.id"), primary_key=True)
+ tag: Mapped[str] = mapped_column(Text, primary_key=True)
track: Mapped["Track"] = relationship("Track", back_populates="tags")
@@ -132,6 +155,67 @@ track_favourite_assoc = Table(
Column("user_id", ForeignKey("users.id"), primary_key=True),
)
+
+class Waypoint(Base):
+ """A waypoint represents a "point of interest" along a path.
+
+ Waypoints can have a name and description set. They exist outside of the
+ actual route.
+ """
+
+ # pylint: disable=too-few-public-methods
+ __tablename__ = "waypoints"
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
+ track_id: Mapped[int] = mapped_column(Integer, ForeignKey("tracks.id"), nullable=False)
+ longitude: Mapped[float] = mapped_column(Float, nullable=False)
+ latitude: Mapped[float] = mapped_column(Float, nullable=False)
+ elevation: Mapped[float | None] = mapped_column(Float)
+ name: Mapped[str | None] = mapped_column(Text)
+ description: Mapped[str | None] = mapped_column(Text)
+
+ track: Mapped["Track"] = relationship("Track", back_populates="waypoints")
+
+ def to_geo_waypoint(self) -> geo.Waypoint:
+ """Converts this waypoint (a database object) to a plain waypoint.
+
+ :return: The converted point.
+ """
+ return geo.Waypoint(
+ latitude=self.latitude,
+ longitude=self.longitude,
+ elevation=self.elevation,
+ name=self.name,
+ description=self.description,
+ )
+
+
+class TrackPoint(Base):
+ """A track point represents a single GPS point along a path."""
+
+ # pylint: disable=too-few-public-methods
+ __tablename__ = "track_points"
+ track_id: Mapped[int] = mapped_column(Integer, ForeignKey("tracks.id"), primary_key=True)
+ index: Mapped[int] = mapped_column(Integer, primary_key=True)
+ longitude: Mapped[float] = mapped_column(Float, nullable=False)
+ latitude: Mapped[float] = mapped_column(Float, nullable=False)
+ elevation: Mapped[float | None] = mapped_column(Float)
+ time_offset: Mapped[float | None] = mapped_column(Float)
+
+ track: Mapped["Track"] = relationship("Track", back_populates="points")
+
+ def to_geo_point(self) -> geo.Point:
+ """Converts this point (a database object) to a plain point.
+
+ :return: The converted point.
+ """
+ return geo.Point(
+ latitude=self.latitude,
+ longitude=self.longitude,
+ elevation=self.elevation or 0.0,
+ time_offset=self.time_offset or 0.0,
+ )
+
+
# Some words about timezone handling in saved tracks:
# https://www.youtube.com/watch?v=-5wpm-gesOY
#
@@ -204,18 +288,24 @@ class Track(Base):
"""
__tablename__ = "tracks"
- id = Column(Integer, primary_key=True)
- owner_id = Column(Integer, ForeignKey("users.id"))
- title = Column(Text)
- description = Column(Text)
- date_raw = Column(DateTime(False))
- date_tz = Column(Integer)
- visibility = Column(Enum(Visibility))
- link_secret = Column(Text)
- type = Column(Enum(TrackType))
- transformers = Column(JSON)
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
+ owner_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"))
+ title: Mapped[str | None] = mapped_column(Text)
+ description: Mapped[str | None] = mapped_column(Text)
+ date_raw: Mapped[datetime.datetime | None] = mapped_column(DateTime(False))
+ date_tz: Mapped[int | None] = mapped_column(Integer)
+ visibility: Mapped[Visibility | None] = mapped_column(Enum(Visibility))
+ link_secret: Mapped[str | None] = mapped_column(Text)
+ type: Mapped[TrackType | None] = mapped_column(Enum(TrackType))
+ transformers: Mapped[list | dict | bool | str | int | float | None] = mapped_column(JsonText)
owner: Mapped["models.User"] = relationship("User", back_populates="tracks")
+ points: Mapped[list["TrackPoint"]] = relationship(
+ "TrackPoint", back_populates="track", cascade="all, delete-orphan"
+ )
+ waypoints: Mapped[list["Waypoint"]] = relationship(
+ "Waypoint", back_populates="track", cascade="all, delete-orphan"
+ )
cache: Mapped[Optional["TrackCache"]] = relationship(
"TrackCache", back_populates="track", uselist=False, cascade="all, delete-orphan"
)
@@ -237,6 +327,9 @@ class Track(Base):
favourees: Mapped[list["models.User"]] = relationship(
"User", secondary=track_favourite_assoc, back_populates="favourite_tracks"
)
+ journeys: Mapped[list["models.Journey"]] = relationship(
+ "Journey", secondary="journey_track_assoc", back_populates="tracks"
+ )
@classmethod
def factory(cls, request):
@@ -297,6 +390,74 @@ class Track(Base):
)
return acl
+ def set_path(self, path: geo.Path):
+ """Sets this track's represented path to the given path.
+
+ If the track is in the database session, you can use
+ :meth:`fast_set_path`.
+
+ :param path: The new GPS path of this track.
+ """
+ self.points = [
+ TrackPoint(
+ track=self,
+ index=i,
+ longitude=point.longitude,
+ latitude=point.latitude,
+ elevation=point.elevation,
+ time_offset=point.time_offset,
+ )
+ for i, point in enumerate(path.points)
+ ]
+
+ def fast_set_path(self, path: geo.Path):
+ """Sets this track's represented path to the given path.
+
+ This method is faster than using :meth:`set_path`, as it does a bulk
+ insert of the path points. However, this requires the path to be in the
+ database and the session.
+
+ :param path: The new GPS path of this track.
+ """
+ session = inspect(self).session
+ assert session
+ points = [
+ {
+ "track_id": self.id,
+ "index": index,
+ "longitude": p.longitude,
+ "latitude": p.latitude,
+ "elevation": p.elevation,
+ "time_offset": p.time_offset,
+ }
+ for index, p in enumerate(path.points)
+ ]
+ session.execute(delete(TrackPoint).where(TrackPoint.track_id == self.id))
+ session.execute(insert(TrackPoint), points)
+ session.expire(self, ["points"])
+
+ def path(self) -> geo.Path:
+ """Returns the path of this track.
+
+ :return: The GPS path of this track.
+ """
+ return geo.Path(
+ [point.to_geo_point() for point in sorted(self.points, key=lambda p: p.index or 0.0)]
+ )
+
+ def gpx_xml(self) -> bytes:
+ """Returns an XML representation of this track.
+
+ :return: The XML representation (a GPX file).
+ """
+ return geo.gpx_xml(
+ self.title,
+ self.description,
+ self.date,
+ self.path().points,
+ [wpt.to_geo_waypoint() for wpt in self.waypoints],
+ )
+
@property
def date(self):
"""The time-zone-aware date this track has set.
@@ -355,24 +516,35 @@ class Track(Base):
result = ACLHelper().permits(self, principals, "track.view")
return isinstance(result, ACLAllowed)
- def ensure_cache(self, gpx_data: Union[str, bytes, gpxpy.gpx.GPX]):
+ def ensure_cache(self, path: geo.Path | None = None):
"""Ensure that a cached version of this track's metadata exists.
- :param gpx_data: GPX data (uncompressed) from which to build the cache.
+ If ``path`` is given, it is used as the basis for the calculation. This
+ is useful if you have already loaded the path, to avoid a database
+ access.
+
+ :param path: The path, if preloaded. Otherwise, ``self.path()`` will be
+ used.
"""
if self.cache is not None:
return
- self.cache = TrackCache(track=self)
- meta = util.tour_metadata(gpx_data)
- self.cache.length = meta["length"]
- self.cache.uphill = meta["uphill"]
- self.cache.downhill = meta["downhill"]
- self.cache.moving_time = meta["moving_time"]
- self.cache.stopped_time = meta["stopped_time"]
- self.cache.max_speed = meta["max_speed"]
- self.cache.avg_speed = meta["avg_speed"]
- self.cache.start_time = meta["start_time"]
- self.cache.end_time = meta["end_time"]
+ self.cache = TrackCache()
+ if path is None:
+ path = self.path()
+ meta = path.movement_data()
+ self.cache.length = meta.length
+ self.cache.uphill = meta.uphill
+ self.cache.downhill = meta.downhill
+ self.cache.moving_time = meta.moving_duration
+ self.cache.stopped_time = meta.stopped_duration
+ self.cache.max_speed = meta.maximum_speed
+ self.cache.avg_speed = meta.average_speed
+ self.cache.start_time = self.date
+ self.cache.end_time = self.date + datetime.timedelta(seconds=meta.duration)
+
+ def with_metadata(self) -> "TrackWithMetadata":
+ """Returns this track with attached path metadata."""
+ return TrackWithMetadata(self)
def text_tags(self):
"""Returns a set of textual tags.
@@ -443,6 +615,9 @@ class Track(Base):
if not self.transformers:
return None
+ if not isinstance(self.transformers, dict):
+ return None
+
for t_id, settings in self.transformers:
if t_id == transformer_id:
return settings
@@ -452,7 +627,7 @@ class Track(Base):
class TrackWithMetadata:
"""A class to add metadata to a :class:`Track`.
- This basically caches the result of :func:`fietsboek.util.tour_metadata`,
+ This basically caches the result of :func:`fietsboek.geo.Path.movement_data`,
or uses the track's cache if possible.
Loading of the metadata is lazy on first access. The track is accessible as
@@ -461,19 +636,17 @@ class TrackWithMetadata:
# pylint: disable=too-many-public-methods
- def __init__(self, track: Track, data_manager):
+ def __init__(self, track: Track):
self.track = track
self.cache = track.cache
- self.data_manager = data_manager
- self._cached_meta: Optional[dict] = None
+ self._cached_meta: Optional[geo.MovementData] = None
def _meta(self):
# Already loaded, we're done
if self._cached_meta:
return self._cached_meta
- data = self.data_manager.open(self.track.id).decompress_gpx()
- self._cached_meta = util.tour_metadata(data)
+ self._cached_meta = self.track.path().movement_data()
return self._cached_meta
@property
@@ -483,7 +656,7 @@ class TrackWithMetadata:
:return: Length of the track in meters.
"""
if self.cache is None or self.cache.length is None:
- return self._meta()["length"]
+ return self._meta().length
return float(self.cache.length)
@property
@@ -493,7 +666,7 @@ class TrackWithMetadata:
:return: Downhill in meters.
"""
if self.cache is None or self.cache.downhill is None:
- return self._meta()["downhill"]
+ return self._meta().downhill
return float(self.cache.downhill)
@property
@@ -503,7 +676,7 @@ class TrackWithMetadata:
:return: Uphill in meters.
"""
if self.cache is None or self.cache.uphill is None:
- return self._meta()["uphill"]
+ return self._meta().uphill
return float(self.cache.uphill)
@property
@@ -513,7 +686,7 @@ class TrackWithMetadata:
:return: Moving time in seconds.
"""
if self.cache is None or self.cache.moving_time is None:
- value = self._meta()["moving_time"]
+ value = self._meta().moving_duration
else:
value = self.cache.moving_time
return datetime.timedelta(seconds=value)
@@ -525,7 +698,7 @@ class TrackWithMetadata:
:return: Stopped time in seconds.
"""
if self.cache is None or self.cache.stopped_time is None:
- value = self._meta()["stopped_time"]
+ value = self._meta().stopped_duration
else:
value = self.cache.stopped_time
return datetime.timedelta(seconds=value)
@@ -537,7 +710,7 @@ class TrackWithMetadata:
:return: Maximum speed in meters/second.
"""
if self.cache is None or self.cache.max_speed is None:
- return self._meta()["max_speed"]
+ return self._meta().maximum_speed
return float(self.cache.max_speed)
@property
@@ -547,7 +720,7 @@ class TrackWithMetadata:
:return: Average speed in meters/second.
"""
if self.cache is None or self.cache.avg_speed is None:
- return self._meta()["avg_speed"]
+ return self._meta().average_speed
return float(self.cache.avg_speed)
@property
@@ -558,9 +731,7 @@ class TrackWithMetadata:
:return: Start time.
"""
- if self.cache is None or self.cache.start_time is None:
- return self._meta()["start_time"]
- return self.cache.start_time
+ return self.track.date
@property
def end_time(self) -> datetime.datetime:
@@ -571,7 +742,7 @@ class TrackWithMetadata:
:return: End time.
"""
if self.cache is None or self.cache.end_time is None:
- return self._meta()["end_time"]
+ return self.track.date + datetime.timedelta(seconds=self._meta().duration)
return self.cache.end_time
@property
@@ -754,18 +925,18 @@ class TrackCache(Base):
# pylint: disable=too-many-instance-attributes,too-few-public-methods
__tablename__ = "track_cache"
- track_id = Column(Integer, ForeignKey("tracks.id"), primary_key=True)
- length = Column(Float)
- uphill = Column(Float)
- downhill = Column(Float)
- moving_time = Column(Float)
- stopped_time = Column(Float)
- max_speed = Column(Float)
- avg_speed = Column(Float)
- start_time_raw = Column(DateTime(False))
- start_time_tz = Column(Integer)
- end_time_raw = Column(DateTime(False))
- end_time_tz = Column(Integer)
+ track_id: Mapped[int] = mapped_column(Integer, ForeignKey("tracks.id"), primary_key=True)
+ length: Mapped[float | None] = mapped_column(Float)
+ uphill: Mapped[float | None] = mapped_column(Float)
+ downhill: Mapped[float | None] = mapped_column(Float)
+ moving_time: Mapped[float | None] = mapped_column(Float)
+ stopped_time: Mapped[float | None] = mapped_column(Float)
+ max_speed: Mapped[float | None] = mapped_column(Float)
+ avg_speed: Mapped[float | None] = mapped_column(Float)
+ start_time_raw: Mapped[datetime.datetime | None] = mapped_column(DateTime(False))
+ start_time_tz: Mapped[int | None] = mapped_column(Integer)
+ end_time_raw: Mapped[datetime.datetime | None] = mapped_column(DateTime(False))
+ end_time_tz: Mapped[int | None] = mapped_column(Integer)
track: Mapped["Track"] = relationship("Track", back_populates="cache")
@@ -846,10 +1017,10 @@ class Upload(Base):
# pylint: disable=too-many-instance-attributes,too-few-public-methods
__tablename__ = "uploads"
- id = Column(Integer, primary_key=True)
- uploaded_at = Column(DateTime(False))
- owner_id = Column(Integer, ForeignKey("users.id"))
- gpx = Column(LargeBinary)
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
+ uploaded_at: Mapped[datetime.datetime | None] = mapped_column(DateTime(False))
+ owner_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("users.id"))
+ gpx: Mapped[bytes | None] = mapped_column(LargeBinary)
owner: Mapped["models.User"] = relationship("User", back_populates="uploads")
diff --git a/fietsboek/models/user.py b/fietsboek/models/user.py
index 725fb3a..551d920 100644
--- a/fietsboek/models/user.py
+++ b/fietsboek/models/user.py
@@ -31,14 +31,16 @@ from sqlalchemy import (
union,
)
from sqlalchemy.exc import NoResultFound
-from sqlalchemy.orm import Mapped, Session, relationship, with_parent
+from sqlalchemy.orm import Mapped, Session, mapped_column, relationship, with_parent
from sqlalchemy.orm.attributes import flag_dirty
from sqlalchemy.orm.session import object_session
+from sqlalchemy.sql.expression import CompoundSelect
from .meta import Base
if TYPE_CHECKING:
from .comment import Comment
+ from .journey import Journey
from .track import Track, Upload
@@ -112,14 +114,14 @@ class User(Base):
"""
__tablename__ = "users"
- id = Column(Integer, primary_key=True)
- name = Column(Text)
- password = Column(LargeBinary)
- salt = Column(LargeBinary)
- email = Column(Text)
- session_secret = Column(LargeBinary(SESSION_SECRET_LENGTH))
- is_admin = Column(Boolean, default=False)
- is_verified = Column(Boolean, default=False)
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
+ name: Mapped[str | None] = mapped_column(Text)
+ password: Mapped[bytes | None] = mapped_column(LargeBinary)
+ salt: Mapped[bytes | None] = mapped_column(LargeBinary)
+ email: Mapped[str | None] = mapped_column(Text)
+ session_secret: Mapped[bytes | None] = mapped_column(LargeBinary(SESSION_SECRET_LENGTH))
+ is_admin: Mapped[bool | None] = mapped_column(Boolean, default=False)
+ is_verified: Mapped[bool | None] = mapped_column(Boolean, default=False)
tracks: Mapped[list["Track"]] = relationship(
"Track", back_populates="owner", cascade="all, delete-orphan"
@@ -139,6 +141,7 @@ class User(Base):
comments: Mapped[list["Comment"]] = relationship(
"Comment", back_populates="author", cascade="all, delete-orphan"
)
+ journeys: Mapped[list["Journey"]] = relationship("Journey", back_populates="owner")
# We don't use them, but include them to ensure our cascading works
friends_1: Mapped[list["User"]] = relationship(
@@ -395,6 +398,44 @@ class User(Base):
return union(*queries)
+ @staticmethod
+ def visible_journeys_query(user: Optional["User"] = None) -> CompoundSelect:
+ """Returns a query that returns the visible journeys for a user.
+
+ If the user is ``None``, only public journeys will be returned.
+
+ :param user: The user which to query the journeys for.
+ :return: The query that selects all fitting journeys.
+ """
+ # Late import to avoid cycles
+ # pylint: disable=import-outside-toplevel,protected-access
+ from .journey import Journey, Visibility
+
+ queries = []
+
+ # Step 1: Own journeys
+ if user:
+ queries.append(select(Journey).where(with_parent(user, User.journeys)))
+
+ # Step 2: Public journeys
+ queries.append(select(Journey).filter_by(visibility=Visibility.PUBLIC))
+
+ # Step 3: Journeys for logged in users
+ if user:
+ queries.append(select(Journey).filter_by(visibility=Visibility.LOGGED_IN))
+
+ # Step 4: Friends' journeys
+ if user:
+ friend_query = user._friend_query().subquery()
+ friend_ids = select(friend_query.c.id)
+ queries.append(
+ select(Journey)
+ .filter_by(visibility=Visibility.FRIENDS)
+ .where(Journey.owner_id.in_(friend_ids))
+ )
+
+ return union(*queries)
+
def _friend_query(self):
qry1 = select(User).filter(
friends_assoc.c.user_1_id == self.id, friends_assoc.c.user_2_id == User.id
@@ -486,10 +527,10 @@ class FriendRequest(Base):
# pylint: disable=too-few-public-methods
__tablename__ = "friend_requests"
- id = Column(Integer, primary_key=True)
- sender_id = Column(Integer, ForeignKey("users.id"))
- recipient_id = Column(Integer, ForeignKey("users.id"))
- date = Column(DateTime(False))
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
+ sender_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("users.id"))
+ recipient_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("users.id"))
+ date: Mapped[datetime.datetime | None] = mapped_column(DateTime(False))
sender: Mapped["User"] = relationship(
"User", primaryjoin="User.id == FriendRequest.sender_id", backref="outgoing_requests"
@@ -540,11 +581,11 @@ class Token(Base):
# pylint: disable=too-few-public-methods
__tablename__ = "tokens"
- id = Column(Integer, primary_key=True)
- user_id = Column(Integer, ForeignKey("users.id"))
- uuid = Column(Text)
- token_type = Column(Enum(TokenType))
- date = Column(DateTime(False))
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
+ user_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("users.id"))
+ uuid: Mapped[str | None] = mapped_column(Text)
+ token_type: Mapped[TokenType | None] = mapped_column(Enum(TokenType))
+ date: Mapped[datetime.datetime | None] = mapped_column(DateTime(False))
user: Mapped["User"] = relationship("User", back_populates="tokens")
diff --git a/fietsboek/pdf-assets/COPYRIGHT.md b/fietsboek/pdf-assets/COPYRIGHT.md
new file mode 100644
index 0000000..9b9e652
--- /dev/null
+++ b/fietsboek/pdf-assets/COPYRIGHT.md
@@ -0,0 +1,104 @@
+# Nunito
+
+`Nunito.ttf` from https://fonts.google.com/specimen/Nunito
+
+Designed by Vernon Adams, Cyreal, Jacques Le Bailly
+
+## License
+
+Copyright 2014 The Nunito Project Authors (https://github.com/googlefonts/nunito)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1 . This license is copied below, and is also available with a FAQ at: https://openfontlicense.org
+
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+
+### PREAMBLE
+
+The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives.
+
+### DEFINITIONS
+
+"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the copyright statement(s).
+"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment.
+
+"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software.
+
+### PERMISSION & CONDITIONS
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions:
+
+* Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.
+* Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user.
+* No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users.
+* The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission.
+* The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software.
+
+### TERMINATION
+
+This license becomes null and void if any of the above conditions are not met.
+
+### DISCLAIMER
+
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
+
+
+# Mozilla's Twemoji
+
+`Twemoji.Mozilla.ttf` from https://github.com/mozilla/twemoji-colr
+
+## License for the Code
+
+Copyright 2016-2018, Mozilla Foundation
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+
+
+## License for the Visual Design
+
+The Emoji art in the twe-svg.zip archive comes from [Twemoji](https://twitter.github.io/twemoji),
+and is used and redistributed under the CC-BY-4.0 [license terms](https://github.com/twitter/twemoji#license)
+offered by the Twemoji project.
+
+### Creative Commons Attribution 4.0 International (CC BY 4.0)
+https://creativecommons.org/licenses/by/4.0/legalcode
+or for the human readable summary: https://creativecommons.org/licenses/by/4.0/
+
+
+#### You are free to:
+**Share** — copy and redistribute the material in any medium or format
+
+**Adapt** — remix, transform, and build upon the material for any purpose, even commercially.
+
+The licensor cannot revoke these freedoms as long as you follow the license terms.
+
+
+#### Under the following terms:
+**Attribution** — You must give appropriate credit, provide a link to the license,
+and indicate if changes were made.
+You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
+
+**No additional restrictions** — You may not apply legal terms or **technological measures**
+that legally restrict others from doing anything the license permits.
+
+#### Notices:
+You do not have to comply with the license for elements of the material in the public domain
+or where your use is permitted by an applicable exception or limitation. No warranties are given.
+The license may not give you all of the permissions necessary for your intended use.
+For example, other rights such as publicity, privacy, or moral rights may limit how you use the material.
diff --git a/fietsboek/pdf-assets/Nunito.ttf b/fietsboek/pdf-assets/Nunito.ttf
new file mode 100644
index 0000000..be80c3f
--- /dev/null
+++ b/fietsboek/pdf-assets/Nunito.ttf
Binary files differ
diff --git a/fietsboek/pdf-assets/Twemoji.Mozilla.ttf b/fietsboek/pdf-assets/Twemoji.Mozilla.ttf
new file mode 100644
index 0000000..9f45178
--- /dev/null
+++ b/fietsboek/pdf-assets/Twemoji.Mozilla.ttf
Binary files differ
diff --git a/fietsboek/pdf-assets/overview.typ b/fietsboek/pdf-assets/overview.typ
new file mode 100644
index 0000000..dd0d3b0
--- /dev/null
+++ b/fietsboek/pdf-assets/overview.typ
@@ -0,0 +1,29 @@
+#set page(margin: 1cm)
+#set text(font: ("Nunito", "Twemoji Mozilla"))
+
+#show heading.where(level: 1): set align(center)
+#show heading.where(level: 1): set text(size: 20pt)
+
+#let rowHead(body) = strong(body)
+
+#heading[{{ title | typst_escape }}]
+
+#text(baseline: -1pt, emoji.person)
+{% for person in people -%}
+#strong[{{ person | typst_escape }}]{% if not loop.last %}, {% endif %}
+{%- endfor %}
+
+#image("mapimage.png")
+
+#image(width: 100%, "height_profile.pdf")
+
+#table(
+ columns: (50%, 50%),
+ stroke: none,
+ fill: (_, y) => if calc.odd(y) { rgb("#efefef") } else { none },
+ {% for name, value in table -%}
+ rowHead[{{ name | typst_escape }}], [{{ value | typst_escape }}],
+ {% endfor %}
+)
+
+{{ description | md_to_typst }}
diff --git a/fietsboek/pdf.py b/fietsboek/pdf.py
new file mode 100644
index 0000000..5b2a3ba
--- /dev/null
+++ b/fietsboek/pdf.py
@@ -0,0 +1,291 @@
+"""PDF generation for tracks.
+
+This module implements functionality that renders a PDF overview for a track.
+The PDF overview consists of a map using OSM tiles, and a table with the
+computed metadata.
+
+PDF generation is done using Typst_ via the `Python bindings`_. Typst provides
+layouting and good typography without too much effort from our side. The Typst
+file is generated from a Jinja2 template, saved to a temporary directory
+together with the track image, and then compiled.
+
+.. _Typst: https://typst.app/
+.. _Python bindings: https://pypi.org/project/typst/
+"""
+
+import html.parser
+import importlib.resources
+import io
+import logging
+import tempfile
+from itertools import chain
+from pathlib import Path
+
+import jinja2
+import matplotlib
+import typst
+from babel.dates import format_datetime
+from babel.numbers import format_decimal
+from matplotlib import pyplot as plt
+from pyramid.i18n import Localizer
+from pyramid.i18n import TranslationString as _
+
+from . import trackmap, util
+from .config import TileLayerConfig
+from .models import Track
+from .models.track import TrackWithMetadata
+from .views.tileproxy import TileRequester
+
+LOGGER = logging.getLogger(__name__)
+TEMP_PREFIX = "fietsboek-typst-"
+IMAGE_WIDTH = 1500
+IMAGE_HEIGHT = 500
+# See https://typst.app/docs/reference/syntax/
+TO_ESCAPE = {
+ "$",
+ "#",
+ "[",
+ "]",
+ "*",
+ "_",
+ "`",
+ "<",
+ ">",
+ "@",
+ "=",
+ "-",
+ "+",
+ "/",
+ "\\",
+}
+
+
+class HtmlToTypst(html.parser.HTMLParser):
+ """A parser that converts HTML to Typst syntax.
+
+ This is adjusted for the HTML output that the markdown converted produces.
+ It will not work for arbitrary HTML.
+ """
+
+ # pylint: disable=too-many-branches
+ def __init__(self, out):
+ super().__init__()
+ self.out = out
+
+ def handle_data(self, data):
+ self.out.write(typst_escape(data))
+
+ def handle_starttag(self, tag, attrs):
+ if tag == "strong":
+ self.out.write("#strong[")
+ elif tag == "em":
+ self.out.write("#emph[")
+ elif tag == "a":
+ href = ""
+ for key, val in attrs:
+ if key == "href":
+ href = val
+ self.out.write(f"#link({typst_string(href)})[")
+ elif tag == "ul":
+ self.out.write("#list(")
+ elif tag == "ol":
+ self.out.write("#enum(")
+ elif tag == "li":
+ self.out.write("[")
+ elif tag == "h1":
+ self.out.write("#heading(level: 1)[")
+ elif tag == "h2":
+ self.out.write("#heading(level: 2)[")
+ elif tag == "h3":
+ self.out.write("#heading(level: 3)[")
+ elif tag == "h4":
+ self.out.write("#heading(level: 4)[")
+ elif tag == "h5":
+ self.out.write("#heading(level: 5)[")
+ elif tag == "h6":
+ self.out.write("#heading(level: 6)[")
+
+ def handle_endtag(self, tag):
+ if tag == "p":
+ self.out.write("\n\n")
+ elif tag == "strong":
+ self.out.write("]")
+ elif tag == "em":
+ self.out.write("]")
+ elif tag == "a":
+ self.out.write("]")
+ elif tag == "ul":
+ self.out.write(")")
+ elif tag == "ol":
+ self.out.write(")")
+ elif tag == "li":
+ self.out.write("],")
+ elif tag in {"h1", "h2", "h3", "h4", "h5", "h6"}:
+ self.out.write("]")
+
+
+def typst_string(value: str) -> str:
+ """Serializes a string to a string that can be embedded in Typst source.
+
+ This wraps the value in quotes, and escapes all characters.
+
+ :param value: The value to serialize.
+ :return: The serialized string, ready to be embedded.
+ """
+ return '"' + "".join(f"\\u{{{ord(char):x}}}" for char in value) + '"'
+
+
+def typst_escape(value: str) -> str:
+ """Escapes Typst markup in the given value.
+
+ :param value: The value to escape.
+ :return: The value with formatting characters escaped.
+ """
+ return "".join("\\" + char if char in TO_ESCAPE else char for char in value)
+
+
+def md_to_typst(value: str) -> str:
+ """Convert Markdown-formatted text to Typst source code.
+
+ :param value: The Markdown source to convert.
+ :return: The Typst code.
+ """
+ html_source = util.safe_markdown(value).unescape()
+ buffer = io.StringIO()
+ parser = HtmlToTypst(buffer)
+ parser.feed(html_source)
+ return buffer.getvalue()
+
+
+def draw_map(track: Track, requester: TileRequester, tile_layer: TileLayerConfig, outfile: Path):
+ """Renders the track map.
+
+ :param track: The track.
+ :param requester: The requester which is used to retrieve tiles.
+ :param tile_layer: The OSM tile layer configuration.
+ :param outfile: Path to the output file.
+ """
+ map_image = trackmap.render(
+ track.path(),
+ tile_layer,
+ requester,
+ size=(IMAGE_WIDTH, IMAGE_HEIGHT),
+ )
+ map_image.save(str(outfile))
+
+
+def draw_height_profile(track: Track, outfile: Path):
+ """Renders the height graph.
+
+ :param track: The track.
+ :param outfile: The output file.
+ """
+ x, y = [], []
+ cur = 0.0
+ last = None
+ for point in track.path().points:
+ x.append(cur / 1000)
+ y.append(point.elevation)
+ if last:
+ cur += point.distance(last)
+ last = point
+
+ matplotlib.use("pdf")
+ fig, ax = plt.subplots(1, 1, figsize=(11, 3))
+ ax.plot(x, y)
+ ax.grid()
+ ax.xaxis.set_major_formatter("{x} km")
+ ax.yaxis.set_major_formatter("{x} m")
+ fig.savefig(outfile, bbox_inches="tight")
+
+
+def generate(
+ track: Track, requester: TileRequester, tile_layer: TileLayerConfig, localizer: Localizer
+) -> bytes:
+ """Generate a PDF representation for the given track.
+
+ :param track: The track for which to generate a PDF overview.
+ :param requester: The tile requester to render the track map.
+ :param tile_layer: The tile layer to use for the track map.
+ :param localizer: The localizer.
+ :return: The PDF bytes.
+ """
+ # Yes, we could use f-strings, but I don't like embedding the huge
+ # expressions below in f-strings:
+ # pylint: disable=consider-using-f-string
+ # And this is a false positive for typst.compile:
+ # pylint: disable=no-member
+ env = jinja2.Environment(
+ loader=jinja2.PackageLoader("fietsboek", "pdf-assets"),
+ autoescape=False,
+ )
+ env.filters["typst_escape"] = typst_escape
+ env.filters["md_to_typst"] = md_to_typst
+
+ twm = TrackWithMetadata(track)
+ template = env.get_template("overview.typ")
+ translate = localizer.translate
+ locale = localizer.locale_name
+ placeholders = {
+ "title": track.title or track.date.strftime("%Y-%m-%d %H:%M"),
+ "people": [person.name for person in chain([track.owner], track.tagged_people)],
+ "table": [
+ (translate(_("pdf.table.date")), format_datetime(track.date, locale=locale)),
+ (
+ translate(_("pdf.table.length")),
+ "{} km".format(format_decimal(twm.length / 1000, locale=locale)),
+ ),
+ (
+ translate(_("pdf.table.uphill")),
+ "{} m".format(format_decimal(twm.uphill, locale=locale)),
+ ),
+ (
+ translate(_("pdf.table.downhill")),
+ "{} m".format(format_decimal(twm.downhill, locale=locale)),
+ ),
+ (translate(_("pdf.table.moving_time")), str(twm.moving_time)),
+ (translate(_("pdf.table.stopped_time")), str(twm.stopped_time)),
+ (
+ translate(_("pdf.table.max_speed")),
+ "{} km/h".format(format_decimal(util.mps_to_kph(twm.max_speed), locale=locale)),
+ ),
+ (
+ translate(_("pdf.table.avg_speed")),
+ "{} km/h".format(format_decimal(util.mps_to_kph(twm.avg_speed), locale=locale)),
+ ),
+ ],
+ "description": track.description,
+ }
+
+ with tempfile.TemporaryDirectory(prefix=TEMP_PREFIX) as temp_dir_name:
+ temp_dir = Path(temp_dir_name)
+ LOGGER.debug("New PDF generation in %s", temp_dir)
+
+ _copy_font("Nunito.ttf", temp_dir)
+ _copy_font("Twemoji.Mozilla.ttf", temp_dir)
+
+ draw_map(track, requester, tile_layer, temp_dir / "mapimage.png")
+ LOGGER.debug("%s: map drawn", temp_dir)
+
+ draw_height_profile(track, temp_dir / "height_profile.pdf")
+ LOGGER.debug("%s: height profile drawn", temp_dir)
+
+ rendered = template.render(placeholders)
+ LOGGER.debug("%s: typst template rendered", temp_dir)
+
+ (temp_dir / "overview.typ").write_text(rendered)
+ pdf_bytes = typst.compile(
+ str(temp_dir / "overview.typ"),
+ font_paths=[str(temp_dir)],
+ )
+ LOGGER.debug("%s: PDF rendering complete", temp_dir)
+
+ return pdf_bytes
+
+
+def _copy_font(name: str, outdir: Path):
+ data = importlib.resources.files("fietsboek").joinpath(f"pdf-assets/{name}").read_bytes()
+ (outdir / name).write_bytes(data)
+
+
+__all__ = ["generate"]
diff --git a/fietsboek/routes.py b/fietsboek/routes.py
index d5caef8..7042415 100644
--- a/fietsboek/routes.py
+++ b/fietsboek/routes.py
@@ -2,7 +2,7 @@
def includeme(config):
- # pylint: disable=missing-function-docstring
+ # pylint: disable=missing-function-docstring,too-many-statements
config.add_static_view("static", "static", cache_max_age=3600)
config.add_route("home", "/")
config.add_route("login", "/login")
@@ -46,13 +46,51 @@ def includeme(config):
config.add_route(
"image", "/track/{track_id}/images/{image_name}", factory="fietsboek.models.Track.factory"
)
+ config.add_route(
+ "track-map",
+ "/track/{track_id}/preview",
+ factory="fietsboek.models.Track.factory",
+ )
+ config.add_route(
+ "track-pdf",
+ "/track/{track_id}/index.pdf",
+ factory="fietsboek.models.Track.factory",
+ )
+
+ config.add_route("journey-list", "/journey/")
+ config.add_route(
+ "journey-map",
+ "/journey/{journey_id}/preview",
+ factory="fietsboek.models.Journey.factory",
+ )
+ config.add_route(
+ "journey-gpx", "/journey/{journey_id}/gpx", factory="fietsboek.models.Journey.factory"
+ )
+ config.add_route(
+ "journey-details", "/journey/{journey_id}/", factory="fietsboek.models.Journey.factory"
+ )
+ config.add_route(
+ "journey-edit", "/journey/{journey_id}/edit", factory="fietsboek.models.Journey.factory"
+ )
+ config.add_route(
+ "journey-invalidate-share",
+ "/journey/{journey_id}/invalidate-link",
+ factory="fietsboek.models.Journey.factory",
+ )
+ config.add_route(
+ "delete-journey",
+ "/journey/{journey_id}/delete",
+ factory="fietsboek.models.Journey.factory",
+ )
+ config.add_route("journey-new", "/journey/new")
config.add_route("badge", "/badge/{badge_id}", factory="fietsboek.models.Badge.factory")
- config.add_route("admin", "/admin")
- config.add_route("admin-badge-add", "/admin/add-badge")
- config.add_route("admin-badge-edit", "/admin/edit-badge")
- config.add_route("admin-badge-delete", "/admin/delete-badge")
+ config.add_route("admin", "/admin/")
+ config.add_route("admin-badge", "/admin/badges/")
+ config.add_route("admin-badge-add", "/admin/badges/add")
+ config.add_route("admin-badge-edit", "/admin/badges/edit")
+ config.add_route("admin-badge-delete", "/admin/badges/delete")
config.add_route("user-data", "/me")
config.add_route("add-friend", "/me/send-friend-request")
diff --git a/fietsboek/scripts/fietscron.py b/fietsboek/scripts/fietscron.py
index 7687e12..ea05f15 100644
--- a/fietsboek/scripts/fietscron.py
+++ b/fietsboek/scripts/fietscron.py
@@ -3,7 +3,6 @@
import datetime
import logging
import logging.config
-from pathlib import Path
import click
import pyramid.paster
@@ -14,10 +13,11 @@ from sqlalchemy.engine import Engine
from sqlalchemy.orm import Session
from .. import config as mod_config
-from .. import hittekaart, models
+from .. import hittekaart, models, trackmap
from ..config import Config
from ..data import DataManager
from ..models.user import TOKEN_LIFETIME
+from ..views.tileproxy import TileRequester
from . import config_option
LOGGER = logging.getLogger(__name__)
@@ -33,6 +33,7 @@ def cli(config):
\b
* Deletes pending uploads that are older than 24 hours.
* Rebuilds the cache for missing tracks.
+ * Builds preview images for tracks.
* (optional) Runs ``hittekaart`` to generate heatmaps
"""
logging.config.fileConfig(config)
@@ -47,16 +48,20 @@ def cli(config):
return
engine = create_engine(config.sqlalchemy_url)
+ redis = mod_redis.from_url(config.redis_url)
LOGGER.debug("Starting maintenance tasks")
remove_old_uploads(engine)
remove_old_tokens(engine)
- rebuild_cache(engine, data_manager)
+ rebuild_cache(engine)
+ build_previews(engine, data_manager, redis, config)
+ redis = mod_redis.from_url(config.redis_url)
if config.hittekaart_autogenerate:
- redis = mod_redis.from_url(config.redis_url)
run_hittekaart(engine, data_manager, redis, config)
+ redis.set("last-cronjob", datetime.datetime.now(datetime.UTC).timestamp())
+
def remove_old_uploads(engine: Engine):
"""Removes old uploads from the database."""
@@ -78,7 +83,7 @@ def remove_old_tokens(engine: Engine):
session.commit()
-def rebuild_cache(engine: Engine, data_manager: DataManager):
+def rebuild_cache(engine: Engine):
"""Rebuilds the cache entries that are currently missing."""
LOGGER.debug("Rebuilding caches")
session = Session(engine)
@@ -89,12 +94,38 @@ def rebuild_cache(engine: Engine, data_manager: DataManager):
for track in session.execute(needed_rebuilds).scalars():
assert track.id is not None
LOGGER.info("Rebuilding cache for track %d", track.id)
- gpx_data = data_manager.open(track.id).decompress_gpx()
- track.ensure_cache(gpx_data)
+ track.ensure_cache()
session.add(track)
session.commit()
+def build_previews(
+ engine: Engine,
+ data_manager: DataManager,
+ redis: Redis,
+ config: mod_config.Config,
+):
+ """Builds track preview images if they are missing."""
+ LOGGER.info("Building track preview images")
+ session = Session(engine)
+ tile_requester = TileRequester(redis)
+ layer = config.public_tile_layers()[0]
+ tracks = select(models.Track)
+ for track in session.execute(tracks).scalars():
+ if track.id is None:
+ continue
+
+ track_dir = data_manager.open(track.id)
+ if track_dir.preview_path().exists():
+ continue
+
+ LOGGER.debug("Building preview for %s", track.id)
+ preview = trackmap.render(track.path(), layer, tile_requester)
+ with track_dir.lock():
+ with open(track_dir.preview_path(), "wb") as preview_file:
+ preview.save(preview_file, "PNG")
+
+
def run_hittekaart(engine: Engine, data_manager: DataManager, redis: Redis, config: Config):
"""Run outstanding hittekaart requests."""
# The logic here is as follows:
@@ -107,7 +138,6 @@ def run_hittekaart(engine: Engine, data_manager: DataManager, redis: Redis, conf
# re-generate all maps over time (e.g. if the hittekaart version changes or
# we miss an update).
modes = [hittekaart.Mode(mode) for mode in config.hittekaart_autogenerate]
- exe_path = Path(config.hittekaart_bin) if config.hittekaart_bin else None
session = Session(engine)
had_hq_item = False
@@ -129,7 +159,6 @@ def run_hittekaart(engine: Engine, data_manager: DataManager, redis: Redis, conf
session,
data_manager,
mode,
- exe_path=exe_path,
threads=config.hittekaart_threads,
)
@@ -153,7 +182,7 @@ def run_hittekaart(engine: Engine, data_manager: DataManager, redis: Redis, conf
for mode in modes:
LOGGER.info("Generating %s for user %d (low-priority)", mode.value, user.id)
hittekaart.generate_for(
- user, session, data_manager, mode, exe_path=exe_path, threads=config.hittekaart_threads
+ user, session, data_manager, mode, threads=config.hittekaart_threads
)
diff --git a/fietsboek/scripts/fietsctl.py b/fietsboek/scripts/fietsctl.py
index ad06e91..9fff6ee 100644
--- a/fietsboek/scripts/fietsctl.py
+++ b/fietsboek/scripts/fietsctl.py
@@ -2,6 +2,7 @@
# pylint: disable=too-many-positional-arguments,too-many-arguments
import logging
+from pathlib import Path
from typing import Optional
import click
@@ -11,8 +12,9 @@ from pyramid.scripting import AppEnvironment
from sqlalchemy import select
from sqlalchemy.exc import NoResultFound
-from .. import __VERSION__, hittekaart, models, util
+from .. import __VERSION__, actions, convert, hittekaart, models, util
from ..data import DataManager
+from ..views.tileproxy import ITileRequester
from . import config_option
LOGGER = logging.getLogger("fietsctl")
@@ -313,11 +315,14 @@ def cmd_user_hittekaart(
else:
query = models.User.query_by_email(email)
- exe_path = env["request"].config.hittekaart_bin
threads = env["request"].config.hittekaart_threads
with env["request"].tm:
dbsession = env["request"].dbsession
data_manager: DataManager = env["request"].data_manager
+ # We disable the transaction here to not cause issues with delayed
+ # folder creation. Heatmap generation is atomic anyway, as we only move
+ # one SQLite file.
+ data_manager.txn = None
user = dbsession.execute(query).scalar_one_or_none()
if user is None:
click.echo("Error: No such user found.", err=True)
@@ -337,9 +342,7 @@ def cmd_user_hittekaart(
click.echo(f"Generating overlay maps for {click.style(user.name, fg=FG_USER_NAME)}...")
for mode in modes:
- hittekaart.generate_for(
- user, dbsession, data_manager, mode, exe_path=exe_path, threads=threads
- )
+ hittekaart.generate_for(user, dbsession, data_manager, mode, threads=threads)
click.echo(f"Generated {mode.value}")
@@ -379,6 +382,93 @@ def cmd_track_list(config: str):
)
+@cmd_track.command("add")
+@config_option
+@click.option(
+ "--visibility",
+ help="Visibility of the added tracks.",
+ type=click.Choice([vis.name for vis in models.track.Visibility]),
+ default="PUBLIC",
+)
+@click.option(
+ "--track-type",
+ help="Type of the added tracks.",
+ type=click.Choice([typ.name for typ in models.track.TrackType]),
+ default="ORGANIC",
+)
+@optgroup.group("User selection", cls=RequiredMutuallyExclusiveOptionGroup)
+@optgroup.option("--id", "-i", "id_", help="Database ID of the user.", type=int)
+@optgroup.option("--email", "-e", help="Email address of the user.")
+@click.argument("filename", nargs=-1)
+@click.pass_context
+def cmd_track_add(
+ ctx: click.Context,
+ config: str,
+ visibility: str,
+ track_type: str,
+ id_: int | None,
+ email: str | None,
+ filename: list[str],
+):
+ """Add a track.
+
+ The track is added for the specified user (via --id/--email).
+ """
+ # pylint: disable=too-many-locals,broad-exception-caught
+ env = setup(config)
+ if id_ is not None:
+ query = select(models.User).filter_by(id=id_)
+ else:
+ query = models.User.query_by_email(email)
+ visibility = models.track.Visibility[visibility]
+ track_type = models.track.TrackType[track_type]
+ with env["request"].tm:
+ request = env["request"]
+ dbsession = request.dbsession
+ user = dbsession.execute(query).scalar_one_or_none()
+ if user is None:
+ click.echo("Error: No such user found.", err=True)
+ ctx.exit(EXIT_FAILURE)
+
+ for fname in filename:
+ fpath = Path(fname)
+ try:
+ gpx_bytes = fpath.read_bytes()
+ except Exception as exc:
+ click.secho("Error", fg="red", nl=False)
+ click.echo(f" in {fname}: {exc}")
+ continue
+
+ try:
+ track = convert.smart_convert(gpx_bytes)
+ except Exception as exc:
+ click.secho("Error", fg="red", nl=False)
+ click.echo(f" in {fname}: {exc}")
+ continue
+
+ title = track.title or fpath.name
+ saved = actions.add_track(
+ dbsession,
+ request.data_manager,
+ request.registry.getUtility(ITileRequester),
+ request.config.public_tile_layers()[0],
+ user,
+ title,
+ track.date,
+ visibility,
+ track_type,
+ "",
+ [],
+ [],
+ [],
+ [],
+ gpx_bytes,
+ )
+
+ click.secho("Success", fg="green", nl=False)
+ click.echo(f" {fname} saved as track {saved.id}")
+
+
@cmd_track.command("del")
@config_option
@click.option("--force", "-f", help="Override the safety check.", is_flag=True)
@@ -435,7 +525,9 @@ def cmd_maintenance_mode(ctx: click.Context, config: str, disable: bool, reason:
maintenance mode.
"""
env = setup(config)
- data_manager = env["request"].data_manager
+ # Create a fresh one to avoid creating (and having to deal with)
+ # transactions
+ data_manager = DataManager(env["request"].config.data_dir)
if disable and reason:
click.echo("Cannot enable and disable maintenance mode at the same time", err=True)
ctx.exit(EXIT_FAILURE)
@@ -448,6 +540,7 @@ def cmd_maintenance_mode(ctx: click.Context, config: str, disable: bool, reason:
elif disable:
(data_manager.data_dir / "MAINTENANCE").unlink()
else:
+ assert reason is not None
(data_manager.data_dir / "MAINTENANCE").write_text(reason, encoding="utf-8")
diff --git a/fietsboek/static/DeadEnd.svg b/fietsboek/static/DeadEnd.svg
new file mode 100644
index 0000000..b65f171
--- /dev/null
+++ b/fietsboek/static/DeadEnd.svg
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ viewBox="0 0 421.0267 630.72443"
+ height="630.72443"
+ width="421.0267"
+ xml:space="preserve"
+ id="svg5733"
+ version="1.1"><metadata
+ id="metadata5739"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs5737" /><g
+ transform="matrix(1.3333333,0,0,-1.3333333,0,630.72445)"
+ id="g5741"><g
+ id="g5743"><path
+ id="path5745"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="M 21.594,0.375 H 294.59 c 20.906,0 20.805,17.051 20.805,17.051 v 438.41 c 0,0 0.136,16.828 -17.977,16.828 H 18.945 c 0,0 -18.531,0.309 -18.57,-18.507 V 19.11 C 0.375,0.328 21.594,0.375 21.594,0.375 Z" /><path
+ id="path5747"
+ style="fill:#154889;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 18.422,8.723 h 279.019 c 0,0 10.153,0.027 10.153,10.27 V 454.86 c 0,0 -0.008,10.058 -11.235,10.058 H 19.504 c 0,0 -11.152,0 -11.152,-10.054 V 18.352 C 8.391,8.621 18.422,8.723 18.422,8.723 Z" /><path
+ id="path5749"
+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 18.945,473.043 c -0.007,0 -4.711,0.078 -9.433,-2.238 C 4.781,468.485 0.02,463.688 0,454.161 V 19.11 C 0,9.582 5.441,4.731 10.832,2.356 16.223,-0.019 21.594,0 21.594,0 H 294.59 c 10.555,0 15.918,4.348 18.562,8.707 2.645,4.356 2.618,8.715 2.618,8.715 v 438.414 c 0,0.004 0.035,4.293 -2.243,8.594 -2.277,4.297 -6.937,8.613 -16.109,8.613 z m 0,-0.379 h 278.473 c 18.113,0 17.977,-16.828 17.977,-16.828 V 17.422 c 0,0 0.101,-17.047 -20.805,-17.047 H 21.594 c 0,0 -21.219,-0.047 -21.219,18.731 v 435.051 c 0.039,18.816 18.57,18.507 18.57,18.507 z" /><path
+ id="path5751"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="M 53.969,418.61 V 265.614 h 96.164 v -34.368 h -66 V 181.43 H 137.16 V 6.571 h 41.59 l -0.066,174.859 h 53.027 l -0.066,49.816 h -66 v 34.368 h 96.156 V 418.61 Z" /><path
+ id="path5753"
+ style="fill:#cc0000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="M 88.68,226.7 H 227.098 V 186.442 H 88.68 Z" /><path
+ id="path5755"
+ style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 165.359,390.379 c 0,-5.152 -4.175,-9.332 -9.332,-9.332 -5.152,0 -9.328,4.18 -9.328,9.332 0,5.153 4.176,9.332 9.328,9.332 5.157,0 9.332,-4.179 9.332,-9.332 z" /><path
+ id="path5757"
+ style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 128.367,336.723 c 25.278,39.441 22.879,40.543 32.328,40.824 5.5,-0.164 5.664,-1.664 14.996,-8.996 11.5,-8.449 10.664,-8.551 10.5,-17.832 -0.05,-11.164 0.715,-10.328 -8.168,-14.496 v 18.832 l -7.832,6.996 v -26.496 l -18.66,0.168 -0.168,20.996 c -15.332,-23.66 -14.332,-22.328 -22.996,-19.996 z" /><path
+ id="path5759"
+ style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
+ d="m 151.863,332.891 h 18.164 c 0,-4.168 1.332,-3.832 10.996,-21.996 10.165,-18.715 10.332,-17.445 4.5,-28.16 l -25.16,45.156 c -23.832,-46.105 -20.832,-44.707 -33.996,-43.824 l 13.164,24.66 c 11.282,20.383 11.883,20.449 12.332,24.164 z" /></g></g></svg> \ No newline at end of file
diff --git a/fietsboek/static/NoEntry.svg b/fietsboek/static/NoEntry.svg
new file mode 100644
index 0000000..1cc18a4
--- /dev/null
+++ b/fietsboek/static/NoEntry.svg
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" width="600.99628" height="600.99628">
+ <defs>
+ <path id="a" d="M23.809 456.512h.051v-.047h-.051v.047z"/>
+ <path id="b" d="M23.809 456.465v.047l.05-.047h-.05zm.05 0-.05.047v-.047h.05z"/>
+ <path id="c" d="M23.859 456.465h-.05.023v.047l.027-.047z"/>
+ <path id="d" d="M23.809 456.465v.047-.047l.05.047-.05-.047z"/>
+ <path id="e" d="M23.859 456.512v-.047h-.05l.05.047z"/>
+ </defs>
+ <g transform="matrix(1.25 0 0 -1.25 0 600.99628)">
+ <path fill="#fff" d="M480.398 240.399c0-132.551-107.449-240-240-240-132.55 0-240 107.449-240 240 0 132.55 107.45 240 240 240 132.551 0 240-107.45 240-240z"/>
+ <path fill="#c1121c" d="M240.402 472.402c-127.75 0-232-104.25-232-232s104.25-232 232-232c127.746 0 232 104.25 232 232s-104.254 232-232 232zm-208-196h416v-72h-416v72z"/>
+ <path d="M240.398 480.797C107.633 480.797 0 373.164 0 240.399 0 107.633 107.633 0 240.398 0c132.766 0 240.399 107.633 240.399 240.399 0 132.765-107.633 240.398-240.399 240.398zm0-.398c132.551 0 240-107.45 240-240 0-132.551-107.449-240-240-240-132.55 0-240 107.449-240 240 0 132.55 107.45 240 240 240zM23.832 456.512v-.024h-.023l.023.024z"/>
+ <path d="M23.809 456.512h.023v-.023h-.023v.023z"/>
+ <path d="M23.809 456.488v.024l.023-.024h-.023zm.023 0-.023.024v-.024h.023z"/>
+ <path d="M23.859 456.488h-.05.023v.024l.027-.024z"/>
+ <path d="M23.809 456.488v.024-.024l.023.024-.023-.024z"/>
+ <path d="M23.809 456.512h.023v-.023h-.023v.023zm.05 0v-.047h-.05l.05.047z"/>
+ <use xlink:href="#a"/>
+ <use xlink:href="#b"/>
+ <use xlink:href="#c"/>
+ <use xlink:href="#d"/>
+ <use xlink:href="#a"/>
+ <use xlink:href="#e"/>
+ <use xlink:href="#a"/>
+ <use xlink:href="#b"/>
+ <use xlink:href="#c"/>
+ <use xlink:href="#d"/>
+ <use xlink:href="#a"/>
+ <use xlink:href="#e"/>
+ <use xlink:href="#a"/>
+ <use xlink:href="#b"/>
+ <use xlink:href="#c"/>
+ <use xlink:href="#d"/>
+ <use xlink:href="#a"/>
+ </g>
+</svg> \ No newline at end of file
diff --git a/fietsboek/static/fietsboek.js b/fietsboek/static/fietsboek.js
index bbdf6e9..d7d903e 100644
--- a/fietsboek/static/fietsboek.js
+++ b/fietsboek/static/fietsboek.js
@@ -445,6 +445,22 @@ function loadProfileStats() {
}
/* Used via in-page scripts, so make eslint happy */
loadProfileStats;
+/**
+ * Formats the given timestamp to the user's locale.
+ *
+ * @param timestamp - The timestamp in milliseconds since the epoch.
+ * @return The formatted string.
+ */
+function formatTimestamp(timestamp) {
+ const date = new Date(timestamp);
+ // TypeScript complains about this, but according to MDN it is fine, at
+ // least in "somewhat modern" browsers
+ const intl = new Intl.DateTimeFormat(LOCALE, {
+ dateStyle: "medium",
+ timeStyle: "medium",
+ });
+ return intl.format(date);
+}
document.addEventListener('DOMContentLoaded', function () {
window.fietsboekImageIndex = 0;
/* Enable tooltips */
@@ -466,14 +482,7 @@ document.addEventListener('DOMContentLoaded', function () {
/* Format all datetimes to the local timezone */
document.querySelectorAll(".fietsboek-local-datetime").forEach((obj) => {
const timestamp = parseFloat(obj.attributes.getNamedItem("data-utc-timestamp").value);
- const date = new Date(timestamp * 1000);
- // TypeScript complains about this, but according to MDN it is fine, at
- // least in "somewhat modern" browsers
- const intl = new Intl.DateTimeFormat(LOCALE, {
- dateStyle: "medium",
- timeStyle: "medium",
- });
- obj.innerHTML = intl.format(date);
+ obj.innerHTML = formatTimestamp(timestamp * 1000);
});
});
//# sourceMappingURL=fietsboek.js.map \ No newline at end of file
diff --git a/fietsboek/static/fietsboek.js.map b/fietsboek/static/fietsboek.js.map
index 2299118..0dc6ff9 100644
--- a/fietsboek/static/fietsboek.js.map
+++ b/fietsboek/static/fietsboek.js.map
@@ -1 +1 @@
-{"version":3,"file":"fietsboek.js","sourceRoot":"","sources":["../../asset-sources/fietsboek.ts"],"names":[],"mappings":";AAqBA,kDAAkD;AAClD,CAAC,CAAS,EAAE,EAAE,CAAC,IAAI,CAAC;AAQpB;;;;;GAKG;AACH,SAAS,SAAS,CAAC,IAAY;;IAC3B,OAAO,MAAA,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC;SAC7B,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,0CACxC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;AACxB,CAAC;AAGD;;;;;;;GAOG;AACH,SAAS,OAAO,CAAC,IAAY;IACzB,OAAO,IAAI,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AACnC,CAAC;AAED;;;;;;GAMG;AACH,SAAS,UAAU,CACf,QAAkB,EAClB,KAAQ,EACR,OAAoD;IAEpD,QAAQ,CAAC,gBAAgB,CAAC,QAAQ,CAAC;QAC/B,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,gBAAgB,CAAC,KAAK,EAAE,OAAwB,CAAC,CAAC,CAAC;AAChF,CAAC;AAED;;;;GAIG;AACH,SAAS,eAAe,CAAC,KAAiB;IACtC,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC;IACxC,MAAM,QAAQ,GAAI,KAAK,CAAC,MAAsB,CAAC,YAAY,CAAC,eAAe,CAAC,CAAC;IAC7E,QAAQ,CAAC,MAAM,GAAG,oBAAoB,QAAQ,UAAU,IAAI,EAAE,CAAC;IAC/D,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;IACzB,KAAK,CAAC,cAAc,EAAE,CAAC;AAC3B,CAAC;AAED,UAAU,CAAC,kBAAkB,EAAE,OAAO,EAAE,eAAe,CAAC,CAAC;AAEzD;;;;GAIG;AACH,SAAS,UAAU,CAAC,KAAiB;IACjC,MAAM,IAAI,GAAI,KAAK,CAAC,MAAsB,CAAC,OAAO,CAAC,MAAM,CAAE,CAAC;IAC5D,IAAI,CAAC,UAAW,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;AACvC,CAAC;AAED,UAAU,CAAC,YAAY,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;AAE9C;;GAEG;AACH,SAAS,MAAM;;IACX,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,UAAU,CAAqB,CAAC;IACtE,IAAI,MAAM,CAAC,KAAK,KAAK,EAAE,EAAE;QACrB,OAAO;KACV;IACD,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;IAC5C,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAChC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAC5B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IACnC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAC9B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAChC,IAAI,CAAC,gBAAgB,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IAC3C,MAAM,IAAI,GAAG,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACnD,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IACvB,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;IACzC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IACvB,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;IAC9C,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,CAAC,IAAI,GAAG,OAAO,CAAC;IACrB,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;IAC3B,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IACxB,MAAA,QAAQ,CAAC,aAAa,CAAC,WAAW,CAAC,0CAAE,WAAW,CAAC,IAAI,CAAC,CAAC;IACvD,MAAM,KAAK,GAAG,QAAQ,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC;IAC3C,MAAA,QAAQ,CAAC,aAAa,CAAC,WAAW,CAAC,0CAAE,WAAW,CAAC,KAAK,CAAC,CAAC;IACxD,MAAM,CAAC,KAAK,GAAG,EAAE,CAAC;AACtB,CAAC;AAED,UAAU,CAAC,cAAc,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;AAC5C,uCAAuC;AACvC,UAAU,CAAC,UAAU,EAAE,UAAU,EAAE,CAAC,KAAK,EAAE,EAAE;IACzC,IAAI,KAAK,CAAC,IAAI,IAAI,OAAO,EAAE;QACvB,KAAK,CAAC,cAAc,EAAE,CAAC;QACvB,MAAM,EAAE,CAAC;KACZ;AACL,CAAC,CAAC,CAAC;AAEH;;;;;GAKG;AACH,SAAS,qBAAqB,CAAC,IAAc,EAAE,MAAgB;IAC3D,MAAM,YAAY,GAAG,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAqB,CAAC;IACtE,MAAM,cAAc,GAAG,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAqB,CAAC;IAE1E,MAAM,IAAI,GAAG,YAAY,CAAC,OAAO,CAAC,MAAM,CAAE,CAAC;IAC3C,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;IAEvC,sEAAsE;IACtE,sEAAsE;IACtE,IAAI,YAAY,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,IAAI,YAAY,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE;QACjE,YAAY,CAAC,iBAAiB,CAAC,WAAW,CAAC,CAAC;KAC/C;SAAM;QACH,YAAY,CAAC,iBAAiB,CAAC,EAAE,CAAC,CAAC;KACtC;IAED,IAAI,YAAY,CAAC,KAAK,IAAI,cAAc,CAAC,KAAK,EAAE;QAC5C,cAAc,CAAC,iBAAiB,CAAC,gBAAgB,CAAC,CAAC;KACtD;SAAM;QACH,cAAc,CAAC,iBAAiB,CAAC,EAAE,CAAC,CAAC;KACxC;AACL,CAAC;AAED,2EAA2E;AAC3E,qBAAqB,CAAC;AAEtB;;;;GAIG;AACH,SAAS,iBAAiB,CAAC,IAAc;IACrC,MAAM,SAAS,GAAG,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAqB,CAAC;IACnE,IAAI,SAAS,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,EAAE;QAC7B,SAAS,CAAC,iBAAiB,CAAC,cAAc,CAAC,CAAC;KAC/C;AACL,CAAC;AAED,2EAA2E;AAC3E,iBAAiB,CAAC;AAElB;;;;;;;;GAQG;AACH,SAAS,cAAc,CAAC,QAAgB;IACpC,OAAO,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,gBAAgB,CAAC,0BAA0B,CAAC,CAAC;SACnE,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAuB,CAAC;SACrC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC;SAC9B,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC;SACvB,QAAQ,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;AACvC,CAAC;AAED;;;GAGG;AACH,SAAS,aAAa;IAClB,MAAM,aAAa,GAAI,QAAQ,CAAC,aAAa,CAAC,oBAAoB,CAAsB;QACpF,KAAK,CAAC,WAAW,EAAE,CAAC;IACxB,MAAM,YAAY,GAAG,QAAQ,CAAC,aAAa,CAAC,eAAe,CAAE,CAAC;IAC9D,YAAY,CAAC,SAAS,GAAG,EAAE,CAAC;IAC5B,KAAK,CAAC,WAAW,CAAC;SACb,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;SACnC,IAAI,CAAC,CAAC,QAAsB,EAAE,EAAE;QAC7B,MAAM,SAAS,GACV,QAAQ,CAAC,aAAa,CAAC,wBAAwB,CAAyB,CAAC,OAAO,CAAC;QAEtF,yCAAyC;QACzC,IAAI,OAAO,GAAG,QAAQ,CAAC,MAAM,CACzB,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAC/D,CAAC;QAEF,8CAA8C;QAC9C,OAAO,GAAG,OAAO,CAAC,MAAM,CACpB,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC,CACnC,CAAC;QAEF,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE;;YACvB,MAAM,IAAI,GAAG,SAAS,CAAC,SAAS,CAAC,IAAI,CAAqB,CAAC;YAC1D,IAAI,CAAC,aAAa,CAAC,cAAc,CAAqB,CAAC,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC;YAClF,MAAA,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,0CAAE,gBAAgB,CAAC,OAAO,EAAE,CAAC,KAAiB,EAAE,EAAE;gBAC1E,MAAM,MAAM,GAAI,KAAK,CAAC,MAAsB,CAAC,OAAO,CAAC,QAAQ,CAAE,CAAC;gBAChE,MAAM,CAAC,UAAW,CAAC,UAAW,CAAC,WAAW,CAAC,MAAM,CAAC,UAAW,CAAC,CAAC;gBAE/D,MAAM,KAAK,GACN,QAAQ,CAAC,aAAa,CAAC,uBAAuB,CAA0B;qBACxE,OAAO;qBACP,SAAS,CAAC,IAAI,CAAqB,CAAC;gBACxC,KAAK,CAAC,aAAa,CAAC,cAAc,CAAqB;oBACpD,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC;gBAC9B,KAAK,CAAC,aAAa,CAAC,OAAO,CAAE,CAAC,KAAK,GAAG,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC;gBAC3D,KAAK,CAAC,aAAa,CAAC,OAAO,CAAE,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC;gBAC1D,KAAK,CAAC,aAAa,CAAC,QAAQ,CAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAC;gBAC9E,QAAQ,CAAC,aAAa,CAAC,gBAAgB,CAAE,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;YACjE,CAAC,CAAC,CAAC;YACH,YAAY,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QACnC,CAAC,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;AACX,CAAC;AAED,UAAU,CAAC,iBAAiB,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,aAAa,EAAE,CAAC,CAAC;AAC9D,4CAA4C;AAC5C,UAAU,CAAC,oBAAoB,EAAE,UAAU,EAAE,CAAC,KAAK,EAAE,EAAE;IACnD,IAAI,KAAK,CAAC,IAAI,IAAI,OAAO,EAAE;QACvB,KAAK,CAAC,cAAc,EAAE,CAAC;QACvB,aAAa,EAAE,CAAC;KACnB;AACL,CAAC,CAAC,CAAC;AAEH;;;;GAIG;AACH,SAAS,mBAAmB,CAAC,KAAiB;IAC1C,MAAM,MAAM,GAAI,KAAK,CAAC,MAAsB,CAAC,OAAO,CAAC,QAAQ,CAAE,CAAC;IAChE,MAAM,CAAC,UAAW,CAAC,UAAW,CAAC,WAAW,CAAC,MAAM,CAAC,UAAW,CAAC,CAAC;AACnE,CAAC;AAED,UAAU,CAAC,uBAAuB,EAAE,OAAO,EAAE,mBAAmB,CAAC,CAAC;AAElE;;;;;;;;GAQG;AACH,SAAS,oBAAoB,CAAC,KAAY;;IACtC,MAAM,MAAM,GAAG,KAAK,CAAC,MAA0B,CAAC;IAChD,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,MAAA,MAAM,CAAC,KAAK,mCAAI,EAAE,CAAC,EAAE;QAC/C,MAAM,CAAC,mBAAmB,EAAE,CAAC;QAE7B,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QAC9C,KAAK,CAAC,IAAI,GAAG,MAAM,CAAC;QACpB,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC;QACpB,KAAK,CAAC,IAAI,GAAG,SAAS,MAAM,CAAC,mBAAmB,GAAG,CAAC;QAEpD,MAAM,QAAQ,GAAG,IAAI,YAAY,EAAE,CAAC;QACpC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACzB,KAAK,CAAC,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC;QAE7B,MAAM,OAAO,GACR,QAAQ,CAAC,aAAa,CAAC,6BAA6B,CAAyB;YAC9E,OAAO;YACP,SAAS,CAAC,IAAI,CAAqB,CAAC;QACxC,OAAO,CAAC,aAAa,CAAC,KAAK,CAAE,CAAC,GAAG,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;QAC9D,OAAO,CAAC,aAAa,CAAC,qBAAqB,CAAE;YACzC,gBAAgB,CAAC,OAAO,EAAE,wBAAyC,CAAC,CAAC;QACzE,OAAO,CAAC,aAAa,CAAC,+BAA+B,CAAE;YACnD,gBAAgB,CAAC,OAAO,EAAE,2BAA4C,CAAC,CAAC;QAC3E,OAAO,CAAC,aAAa,CAAC,+BAA+B,CAAsB;YACxE,IAAI,GAAG,qBAAqB,MAAM,CAAC,mBAAmB,GAAG,CAAC;QAC9D,OAAO,CAAC,aAAa,CAAC,KAAK,CAAE,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QAEjD,QAAQ,CAAC,aAAa,CAAC,iBAAiB,CAAE,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;KACnE;IAED,MAAM,CAAC,KAAK,GAAG,EAAE,CAAC;AACtB,CAAC;AAED,UAAU,CAAC,gBAAgB,EAAE,QAAQ,EAAE,oBAAoB,CAAC,CAAC;AAE7D;;;;GAIG;AACH,SAAS,wBAAwB,CAAC,KAAiB;IAC/C,MAAM,OAAO,GAAI,KAAK,CAAC,MAAsB,CAAC,OAAO,CAAC,yBAAyB,CAAE,CAAC;IAClF,8DAA8D;IAC9D,MAAM,KAAK,GAAG,OAAO,CAAC,aAAa,CAAC,kBAAkB,CAAC,CAAC;IACxD,IAAI,KAAK,EAAE;QACP,OAAO,CAAC,UAAW,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QACzC,OAAO;KACV;IAED,4EAA4E;IAC5E,MAAM,OAAO,GAAG,OAAO,CAAC,aAAa,CAAC,2BAA2B,CAAE,CAAC;IACpE,OAAO,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC;IACpC,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IAC7B,OAAO,CAAC,UAAW,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IACzC,OAAO,CAAC,UAAW,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;AAC7C,CAAC;AAED,UAAU,CAAC,qBAAqB,EAAE,OAAO,EAAE,wBAAwB,CAAC,CAAC;AAErE;;;;GAIG;AACH,SAAS,2BAA2B,CAAC,KAAiB;IAClD,MAAM,CAAC,qBAAqB,GAAI,KAAK,CAAC,MAAsB,CAAC,OAAO,CAAC,KAAK,CAAE,CAAC;IAE7E,MAAM,UAAU,GAEZ,MAAM,CAAC,qBAAqB,CAAC,aAAa,CAAC,+BAA+B,CAC7E,CAAC;IACF,MAAM,kBAAkB,GAAG,UAAU,CAAC,KAAK,CAAC;IAC5C,MAAM,QAAQ,GAAG,QAAQ,CAAC,cAAc,CAAC,uBAAuB,CAAE,CAAC;IACnE,QAAQ,CAAC,aAAa,CAAC,UAAU,CAAE,CAAC,KAAK,GAAG,kBAAkB,CAAC;IAE/D,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,mBAAmB,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAChE,KAAK,CAAC,IAAI,EAAE,CAAC;AACjB,CAAC;AAED,UAAU,CAAC,+BAA+B,EAAE,OAAO,EAAE,2BAA2B,CAAC,CAAC;AAElF;;;;GAIG;AACH,SAAS,2BAA2B,CAAC,MAAkB;IACnD,MAAM,QAAQ,GAAG,QAAQ,CAAC,cAAc,CAAC,uBAAuB,CAAE,CAAC;IACnE,MAAM,iBAAiB,GAAG,QAAQ,CAAC,aAAa,CAAC,UAAU,CAAE,CAAC,KAAK,CAAC;IACnE,MAAM,CAAC,qBAAsB;QAC1B,aAAa,CAAC,+BAA+B,CAAsB;QACnE,KAAK,GAAG,iBAAiB,CAAC;IAC9B,MAAM,CAAC,qBAAsB;QACzB,aAAa,CAAC,KAAK,CAAE,CAAC,KAAK,GAAG,iBAAiB,CAAC;IAEpD,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,mBAAmB,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAChE,KAAK,CAAC,IAAI,EAAE,CAAC;IAEb,MAAM,CAAC,qBAAqB,GAAG,IAAI,CAAC;AACxC,CAAC;AAED,UAAU,CAAC,2CAA2C,EAAE,OAAO,EAAE,2BAA2B,CAAC,CAAC;AAE9F;;;;GAIG;AACH,SAAS,aAAa,CAAC,KAAiB;IACpC,MAAM,OAAO,GAAG,KAAK,CAAC,MAAqB,CAAC;IAC5C,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAE,CAAC;IACzC,MAAM,OAAO,GAAG,UAAU,CAAC,kBAAmB,CAAC;IAC/C,SAAS,CAAC,QAAQ,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,CAAC;IACzD,IAAI,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,iBAAiB,CAAC,EAAE;QAC/C,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC;QAC5C,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;KAC7C;SAAM;QACH,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC;QAC7C,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;KAC5C;AACL,CAAC;AAED,UAAU,CAAC,kBAAkB,EAAE,OAAO,EAAE,aAAa,CAAC,CAAC;AAEvD;;GAEG;AACH,UAAU,CAAC,wBAAwB,EAAE,OAAO,EAAE,GAAG,EAAE;IAC/C,MAAM,OAAO,GAAG,QAAQ,CAAC,gBAAgB,CAAC,2BAA2B,CAAC,CAAC;IACvE,MAAM,GAAG,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IACrC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE;QAClB,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,YAAY,EAAG,CAAsB,CAAC,KAAK,CAAC,CAAC;IACzE,CAAC,CAAC,CAAC;IACH,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;AAChC,CAAC,CAAC,CAAC;AACH;;GAEG;AACH,UAAU,CAAC,mBAAmB,EAAE,QAAQ,EAAE,GAAG,EAAE;IAC3C,MAAM,OAAO,GAAG,QAAQ,CAAC,gBAAgB,CAAC,2BAA2B,CAAC,CAAC;IACvE,MAAM,cAAc,GAAG,QAAQ,CAAC,aAAa,CAAC,wBAAwB,CAAsB,CAAC;IAC7F,cAAc,CAAC,QAAQ,GAAG,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC;AACpD,CAAC,CAAC,CAAC;AAEH;;;;;;GAMG;AACH,SAAS,uBAAuB,CAAC,KAAiB;IAC9C,MAAM,MAAM,GAAG,KAAK,CAAC,MAAqB,CAAC;IAC3C,MAAM,CAAC,OAAO,CAAC,cAAc,CAAE,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC;IACvF,MAAM,CAAC,OAAO,CAAC,cAAc,CAAE,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC;AAC5F,CAAC;AAED,UAAU,CAAC,qBAAqB,EAAE,OAAO,EAAE,uBAAuB,CAAC,CAAC;AAGpE;;;;;;;GAOG;AACH,SAAS,iBAAiB,CAAC,MAAkB;;IACzC,MAAM,cAAc,GAAG,MAAA,SAAS,CAAC,cAAc,CAAC,mCAAI,KAAK,CAAC;IAC1D,MAAM,UAAU,GAAG,cAAc,IAAI,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC;IAC5D,QAAQ,CAAC,MAAM,GAAG,gBAAgB,UAAU,gBAAgB,CAAC;IAC7D,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;AAC7B,CAAC;AAED,UAAU,CAAC,oBAAoB,EAAE,OAAO,EAAE,iBAAiB,CAAC,CAAC;AAG7D;;;;;;;GAOG;AACH,SAAS,oBAAoB,CAAC,KAAiB;;IAC3C,MAAM,MAAM,GAAG,KAAK,CAAC,MAAqB,CAAC;IAC3C,MAAM,OAAO,GAAG,MAAM,CAAC,YAAY,CAAC,eAAe,CAAC,CAAC;IACrD,IAAI,OAAO,KAAK,IAAI,EAAE;QAClB,OAAO;KACV;IACD,MAAM,GAAG,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAC3C,MAAM,QAAQ,GAAG,IAAI,eAAe,EAAE,CAAC;IACvC,QAAQ,CAAC,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IACrC,QAAQ,CAAC,MAAM,CAAC,YAAY,EAAE,MAAA,SAAS,CAAC,YAAY,CAAC,mCAAI,EAAE,CAAC,CAAC;IAC7D,KAAK,CAAC,GAAG,EAAE;QACP,QAAQ,EAAE,MAAM;QAChB,MAAM,EAAE,QAAQ;KACnB,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;QAC5C,MAAM,cAAc,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC;QACzC,IAAI,cAAc,EAAE;YAChB,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC;SACvD;aAAM;YACH,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,cAAc,EAAE,SAAS,CAAC,CAAC;SACvD;IACL,CAAC,CAAC,CAAC,CAAC;AACR,CAAC;AAED,UAAU,CAAC,iBAAiB,EAAE,OAAO,EAAE,oBAAoB,CAAC,CAAC;AAE7D;;GAEG;AACH,SAAS,sBAAsB;IAC3B,MAAM,UAAU,GAAa,EAAE,CAAC;IAChC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IAC/B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE;QACzB,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,EAAC,KAAK,EAAE,MAAM,EAAC,CAAC,CAAC,CAAC;QAC9D,IAAI,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;KACxB;IACD,OAAO,UAAU,CAAC;AACtB,CAAC;AAED;;GAEG;AACH,SAAS,gBAAgB;IACrB,MAAM,UAAU,GAAG,sBAAsB,EAAE,CAAC;IAE5C,MAAM,GAAG,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACvC,KAAK,CAAC,GAAG,CAAC;SACL,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;SACjC,IAAI,CAAC,CAAC,QAAqB,EAAE,EAAE;QAC5B,MAAM,QAAQ,GAAG,EAAE,CAAC;QACpB,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE;YACnD,MAAM,IAAI,GAAG,EAAE,CAAC;YAChB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,EAAE;gBAC1B,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;aACrD;YACD,QAAQ,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,IAAI;gBACV,KAAK,EAAE,IAAI;aACd,CAAC,CAAC;SACN;QAED,IAAI,KAAK,CACL,qBAAqB,EACrB;YACI,IAAI,EAAE,KAAK;YACX,IAAI,EAAE;gBACF,QAAQ,EAAE,QAAQ;gBAClB,MAAM,EAAE,UAAU;aACrB;SACJ,CACJ,CAAC;IACN,CAAC,CAAC,CAAC;AACX,CAAC;AAED,oDAAoD;AACpD,gBAAgB,CAAC;AAEjB,QAAQ,CAAC,gBAAgB,CAAC,kBAAkB,EAAE;IAC1C,MAAM,CAAC,mBAAmB,GAAG,CAAC,CAAC;IAE/B,qBAAqB;IACrB,MAAM,kBAAkB,GAAG,EAAE,CAAC,KAAK,CAAC,IAAI,CACpC,QAAQ,CAAC,gBAAgB,CAAC,4BAA4B,CAAC,CAC1D,CAAC;IACF,kBAAkB,CAAC,GAAG,CAAC,CAAC,gBAAgB,EAAE,EAAE;QACxC,OAAO,IAAI,SAAS,CAAC,OAAO,CAAC,gBAAgB,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;IACxE,CAAC,CAAC,CAAC;IAEH,sCAAsC;IACtC,MAAM,KAAK,GAAG,QAAQ,CAAC,gBAAgB,CAAC,mBAAmB,CAAC,CAAC;IAC7D,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;QAC/B,IAAI,CAAC,gBAAgB,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE;YACtC,IAAI,CAAE,IAAwB,CAAC,aAAa,EAAE,EAAE;gBAC5C,KAAK,CAAC,cAAc,EAAE,CAAC;gBACvB,KAAK,CAAC,eAAe,EAAE,CAAC;aAC3B;YAED,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;QACxC,CAAC,EAAE,KAAK,CAAC,CAAC;IACd,CAAC,CAAC,CAAC;IAEH,gDAAgD;IAChD,QAAQ,CAAC,gBAAgB,CAAC,2BAA2B,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;QACnE,MAAM,SAAS,GAAG,UAAU,CAAC,GAAG,CAAC,UAAU,CAAC,YAAY,CAAC,oBAAoB,CAAE,CAAC,KAAK,CAAC,CAAC;QACvF,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;QACxC,uEAAuE;QACvE,sCAAsC;QACtC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE;YACzC,SAAS,EAAE,QAAQ;YACnB,SAAS,EAAE,QAAQ;SACf,CAAC,CAAC;QACV,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC"} \ No newline at end of file
+{"version":3,"file":"fietsboek.js","sourceRoot":"","sources":["../../asset-sources/fietsboek.ts"],"names":[],"mappings":";AAqBA,kDAAkD;AAClD,CAAC,CAAS,EAAE,EAAE,CAAC,IAAI,CAAC;AAQpB;;;;;GAKG;AACH,SAAS,SAAS,CAAC,IAAY;;IAC3B,OAAO,MAAA,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC;SAC7B,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,0CACxC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;AACxB,CAAC;AAGD;;;;;;;GAOG;AACH,SAAS,OAAO,CAAC,IAAY;IACzB,OAAO,IAAI,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AACnC,CAAC;AAED;;;;;;GAMG;AACH,SAAS,UAAU,CACf,QAAkB,EAClB,KAAQ,EACR,OAAoD;IAEpD,QAAQ,CAAC,gBAAgB,CAAC,QAAQ,CAAC;QAC/B,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,gBAAgB,CAAC,KAAK,EAAE,OAAwB,CAAC,CAAC,CAAC;AAChF,CAAC;AAED;;;;GAIG;AACH,SAAS,eAAe,CAAC,KAAiB;IACtC,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC;IACxC,MAAM,QAAQ,GAAI,KAAK,CAAC,MAAsB,CAAC,YAAY,CAAC,eAAe,CAAC,CAAC;IAC7E,QAAQ,CAAC,MAAM,GAAG,oBAAoB,QAAQ,UAAU,IAAI,EAAE,CAAC;IAC/D,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;IACzB,KAAK,CAAC,cAAc,EAAE,CAAC;AAC3B,CAAC;AAED,UAAU,CAAC,kBAAkB,EAAE,OAAO,EAAE,eAAe,CAAC,CAAC;AAEzD;;;;GAIG;AACH,SAAS,UAAU,CAAC,KAAiB;IACjC,MAAM,IAAI,GAAI,KAAK,CAAC,MAAsB,CAAC,OAAO,CAAC,MAAM,CAAE,CAAC;IAC5D,IAAI,CAAC,UAAW,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;AACvC,CAAC;AAED,UAAU,CAAC,YAAY,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;AAE9C;;GAEG;AACH,SAAS,MAAM;;IACX,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,UAAU,CAAqB,CAAC;IACtE,IAAI,MAAM,CAAC,KAAK,KAAK,EAAE,EAAE;QACrB,OAAO;KACV;IACD,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;IAC5C,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAChC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAC5B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IACnC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAC9B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAChC,IAAI,CAAC,gBAAgB,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IAC3C,MAAM,IAAI,GAAG,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACnD,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IACvB,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;IACzC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IACvB,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;IAC9C,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,CAAC,IAAI,GAAG,OAAO,CAAC;IACrB,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;IAC3B,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IACxB,MAAA,QAAQ,CAAC,aAAa,CAAC,WAAW,CAAC,0CAAE,WAAW,CAAC,IAAI,CAAC,CAAC;IACvD,MAAM,KAAK,GAAG,QAAQ,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC;IAC3C,MAAA,QAAQ,CAAC,aAAa,CAAC,WAAW,CAAC,0CAAE,WAAW,CAAC,KAAK,CAAC,CAAC;IACxD,MAAM,CAAC,KAAK,GAAG,EAAE,CAAC;AACtB,CAAC;AAED,UAAU,CAAC,cAAc,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;AAC5C,uCAAuC;AACvC,UAAU,CAAC,UAAU,EAAE,UAAU,EAAE,CAAC,KAAK,EAAE,EAAE;IACzC,IAAI,KAAK,CAAC,IAAI,IAAI,OAAO,EAAE;QACvB,KAAK,CAAC,cAAc,EAAE,CAAC;QACvB,MAAM,EAAE,CAAC;KACZ;AACL,CAAC,CAAC,CAAC;AAEH;;;;;GAKG;AACH,SAAS,qBAAqB,CAAC,IAAc,EAAE,MAAgB;IAC3D,MAAM,YAAY,GAAG,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAqB,CAAC;IACtE,MAAM,cAAc,GAAG,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAqB,CAAC;IAE1E,MAAM,IAAI,GAAG,YAAY,CAAC,OAAO,CAAC,MAAM,CAAE,CAAC;IAC3C,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;IAEvC,sEAAsE;IACtE,sEAAsE;IACtE,IAAI,YAAY,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,IAAI,YAAY,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE;QACjE,YAAY,CAAC,iBAAiB,CAAC,WAAW,CAAC,CAAC;KAC/C;SAAM;QACH,YAAY,CAAC,iBAAiB,CAAC,EAAE,CAAC,CAAC;KACtC;IAED,IAAI,YAAY,CAAC,KAAK,IAAI,cAAc,CAAC,KAAK,EAAE;QAC5C,cAAc,CAAC,iBAAiB,CAAC,gBAAgB,CAAC,CAAC;KACtD;SAAM;QACH,cAAc,CAAC,iBAAiB,CAAC,EAAE,CAAC,CAAC;KACxC;AACL,CAAC;AAED,2EAA2E;AAC3E,qBAAqB,CAAC;AAEtB;;;;GAIG;AACH,SAAS,iBAAiB,CAAC,IAAc;IACrC,MAAM,SAAS,GAAG,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAqB,CAAC;IACnE,IAAI,SAAS,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,EAAE;QAC7B,SAAS,CAAC,iBAAiB,CAAC,cAAc,CAAC,CAAC;KAC/C;AACL,CAAC;AAED,2EAA2E;AAC3E,iBAAiB,CAAC;AAElB;;;;;;;;GAQG;AACH,SAAS,cAAc,CAAC,QAAgB;IACpC,OAAO,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,gBAAgB,CAAC,0BAA0B,CAAC,CAAC;SACnE,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAuB,CAAC;SACrC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC;SAC9B,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC;SACvB,QAAQ,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;AACvC,CAAC;AAED;;;GAGG;AACH,SAAS,aAAa;IAClB,MAAM,aAAa,GAAI,QAAQ,CAAC,aAAa,CAAC,oBAAoB,CAAsB;QACpF,KAAK,CAAC,WAAW,EAAE,CAAC;IACxB,MAAM,YAAY,GAAG,QAAQ,CAAC,aAAa,CAAC,eAAe,CAAE,CAAC;IAC9D,YAAY,CAAC,SAAS,GAAG,EAAE,CAAC;IAC5B,KAAK,CAAC,WAAW,CAAC;SACb,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;SACnC,IAAI,CAAC,CAAC,QAAsB,EAAE,EAAE;QAC7B,MAAM,SAAS,GACV,QAAQ,CAAC,aAAa,CAAC,wBAAwB,CAAyB,CAAC,OAAO,CAAC;QAEtF,yCAAyC;QACzC,IAAI,OAAO,GAAG,QAAQ,CAAC,MAAM,CACzB,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAC/D,CAAC;QAEF,8CAA8C;QAC9C,OAAO,GAAG,OAAO,CAAC,MAAM,CACpB,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC,CACnC,CAAC;QAEF,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE;;YACvB,MAAM,IAAI,GAAG,SAAS,CAAC,SAAS,CAAC,IAAI,CAAqB,CAAC;YAC1D,IAAI,CAAC,aAAa,CAAC,cAAc,CAAqB,CAAC,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC;YAClF,MAAA,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,0CAAE,gBAAgB,CAAC,OAAO,EAAE,CAAC,KAAiB,EAAE,EAAE;gBAC1E,MAAM,MAAM,GAAI,KAAK,CAAC,MAAsB,CAAC,OAAO,CAAC,QAAQ,CAAE,CAAC;gBAChE,MAAM,CAAC,UAAW,CAAC,UAAW,CAAC,WAAW,CAAC,MAAM,CAAC,UAAW,CAAC,CAAC;gBAE/D,MAAM,KAAK,GACN,QAAQ,CAAC,aAAa,CAAC,uBAAuB,CAA0B;qBACxE,OAAO;qBACP,SAAS,CAAC,IAAI,CAAqB,CAAC;gBACxC,KAAK,CAAC,aAAa,CAAC,cAAc,CAAqB;oBACpD,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC;gBAC9B,KAAK,CAAC,aAAa,CAAC,OAAO,CAAE,CAAC,KAAK,GAAG,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC;gBAC3D,KAAK,CAAC,aAAa,CAAC,OAAO,CAAE,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC;gBAC1D,KAAK,CAAC,aAAa,CAAC,QAAQ,CAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAC;gBAC9E,QAAQ,CAAC,aAAa,CAAC,gBAAgB,CAAE,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;YACjE,CAAC,CAAC,CAAC;YACH,YAAY,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QACnC,CAAC,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;AACX,CAAC;AAED,UAAU,CAAC,iBAAiB,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,aAAa,EAAE,CAAC,CAAC;AAC9D,4CAA4C;AAC5C,UAAU,CAAC,oBAAoB,EAAE,UAAU,EAAE,CAAC,KAAK,EAAE,EAAE;IACnD,IAAI,KAAK,CAAC,IAAI,IAAI,OAAO,EAAE;QACvB,KAAK,CAAC,cAAc,EAAE,CAAC;QACvB,aAAa,EAAE,CAAC;KACnB;AACL,CAAC,CAAC,CAAC;AAEH;;;;GAIG;AACH,SAAS,mBAAmB,CAAC,KAAiB;IAC1C,MAAM,MAAM,GAAI,KAAK,CAAC,MAAsB,CAAC,OAAO,CAAC,QAAQ,CAAE,CAAC;IAChE,MAAM,CAAC,UAAW,CAAC,UAAW,CAAC,WAAW,CAAC,MAAM,CAAC,UAAW,CAAC,CAAC;AACnE,CAAC;AAED,UAAU,CAAC,uBAAuB,EAAE,OAAO,EAAE,mBAAmB,CAAC,CAAC;AAElE;;;;;;;;GAQG;AACH,SAAS,oBAAoB,CAAC,KAAY;;IACtC,MAAM,MAAM,GAAG,KAAK,CAAC,MAA0B,CAAC;IAChD,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,MAAA,MAAM,CAAC,KAAK,mCAAI,EAAE,CAAC,EAAE;QAC/C,MAAM,CAAC,mBAAmB,EAAE,CAAC;QAE7B,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QAC9C,KAAK,CAAC,IAAI,GAAG,MAAM,CAAC;QACpB,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC;QACpB,KAAK,CAAC,IAAI,GAAG,SAAS,MAAM,CAAC,mBAAmB,GAAG,CAAC;QAEpD,MAAM,QAAQ,GAAG,IAAI,YAAY,EAAE,CAAC;QACpC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACzB,KAAK,CAAC,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC;QAE7B,MAAM,OAAO,GACR,QAAQ,CAAC,aAAa,CAAC,6BAA6B,CAAyB;YAC9E,OAAO;YACP,SAAS,CAAC,IAAI,CAAqB,CAAC;QACxC,OAAO,CAAC,aAAa,CAAC,KAAK,CAAE,CAAC,GAAG,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;QAC9D,OAAO,CAAC,aAAa,CAAC,qBAAqB,CAAE;YACzC,gBAAgB,CAAC,OAAO,EAAE,wBAAyC,CAAC,CAAC;QACzE,OAAO,CAAC,aAAa,CAAC,+BAA+B,CAAE;YACnD,gBAAgB,CAAC,OAAO,EAAE,2BAA4C,CAAC,CAAC;QAC3E,OAAO,CAAC,aAAa,CAAC,+BAA+B,CAAsB;YACxE,IAAI,GAAG,qBAAqB,MAAM,CAAC,mBAAmB,GAAG,CAAC;QAC9D,OAAO,CAAC,aAAa,CAAC,KAAK,CAAE,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QAEjD,QAAQ,CAAC,aAAa,CAAC,iBAAiB,CAAE,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;KACnE;IAED,MAAM,CAAC,KAAK,GAAG,EAAE,CAAC;AACtB,CAAC;AAED,UAAU,CAAC,gBAAgB,EAAE,QAAQ,EAAE,oBAAoB,CAAC,CAAC;AAE7D;;;;GAIG;AACH,SAAS,wBAAwB,CAAC,KAAiB;IAC/C,MAAM,OAAO,GAAI,KAAK,CAAC,MAAsB,CAAC,OAAO,CAAC,yBAAyB,CAAE,CAAC;IAClF,8DAA8D;IAC9D,MAAM,KAAK,GAAG,OAAO,CAAC,aAAa,CAAC,kBAAkB,CAAC,CAAC;IACxD,IAAI,KAAK,EAAE;QACP,OAAO,CAAC,UAAW,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QACzC,OAAO;KACV;IAED,4EAA4E;IAC5E,MAAM,OAAO,GAAG,OAAO,CAAC,aAAa,CAAC,2BAA2B,CAAE,CAAC;IACpE,OAAO,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC;IACpC,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IAC7B,OAAO,CAAC,UAAW,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IACzC,OAAO,CAAC,UAAW,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;AAC7C,CAAC;AAED,UAAU,CAAC,qBAAqB,EAAE,OAAO,EAAE,wBAAwB,CAAC,CAAC;AAErE;;;;GAIG;AACH,SAAS,2BAA2B,CAAC,KAAiB;IAClD,MAAM,CAAC,qBAAqB,GAAI,KAAK,CAAC,MAAsB,CAAC,OAAO,CAAC,KAAK,CAAE,CAAC;IAE7E,MAAM,UAAU,GAEZ,MAAM,CAAC,qBAAqB,CAAC,aAAa,CAAC,+BAA+B,CAC7E,CAAC;IACF,MAAM,kBAAkB,GAAG,UAAU,CAAC,KAAK,CAAC;IAC5C,MAAM,QAAQ,GAAG,QAAQ,CAAC,cAAc,CAAC,uBAAuB,CAAE,CAAC;IACnE,QAAQ,CAAC,aAAa,CAAC,UAAU,CAAE,CAAC,KAAK,GAAG,kBAAkB,CAAC;IAE/D,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,mBAAmB,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAChE,KAAK,CAAC,IAAI,EAAE,CAAC;AACjB,CAAC;AAED,UAAU,CAAC,+BAA+B,EAAE,OAAO,EAAE,2BAA2B,CAAC,CAAC;AAElF;;;;GAIG;AACH,SAAS,2BAA2B,CAAC,MAAkB;IACnD,MAAM,QAAQ,GAAG,QAAQ,CAAC,cAAc,CAAC,uBAAuB,CAAE,CAAC;IACnE,MAAM,iBAAiB,GAAG,QAAQ,CAAC,aAAa,CAAC,UAAU,CAAE,CAAC,KAAK,CAAC;IACnE,MAAM,CAAC,qBAAsB;QAC1B,aAAa,CAAC,+BAA+B,CAAsB;QACnE,KAAK,GAAG,iBAAiB,CAAC;IAC9B,MAAM,CAAC,qBAAsB;QACzB,aAAa,CAAC,KAAK,CAAE,CAAC,KAAK,GAAG,iBAAiB,CAAC;IAEpD,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,mBAAmB,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAChE,KAAK,CAAC,IAAI,EAAE,CAAC;IAEb,MAAM,CAAC,qBAAqB,GAAG,IAAI,CAAC;AACxC,CAAC;AAED,UAAU,CAAC,2CAA2C,EAAE,OAAO,EAAE,2BAA2B,CAAC,CAAC;AAE9F;;;;GAIG;AACH,SAAS,aAAa,CAAC,KAAiB;IACpC,MAAM,OAAO,GAAG,KAAK,CAAC,MAAqB,CAAC;IAC5C,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAE,CAAC;IACzC,MAAM,OAAO,GAAG,UAAU,CAAC,kBAAmB,CAAC;IAC/C,SAAS,CAAC,QAAQ,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,CAAC;IACzD,IAAI,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,iBAAiB,CAAC,EAAE;QAC/C,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC;QAC5C,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;KAC7C;SAAM;QACH,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC;QAC7C,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;KAC5C;AACL,CAAC;AAED,UAAU,CAAC,kBAAkB,EAAE,OAAO,EAAE,aAAa,CAAC,CAAC;AAEvD;;GAEG;AACH,UAAU,CAAC,wBAAwB,EAAE,OAAO,EAAE,GAAG,EAAE;IAC/C,MAAM,OAAO,GAAG,QAAQ,CAAC,gBAAgB,CAAC,2BAA2B,CAAC,CAAC;IACvE,MAAM,GAAG,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IACrC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE;QAClB,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,YAAY,EAAG,CAAsB,CAAC,KAAK,CAAC,CAAC;IACzE,CAAC,CAAC,CAAC;IACH,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;AAChC,CAAC,CAAC,CAAC;AACH;;GAEG;AACH,UAAU,CAAC,mBAAmB,EAAE,QAAQ,EAAE,GAAG,EAAE;IAC3C,MAAM,OAAO,GAAG,QAAQ,CAAC,gBAAgB,CAAC,2BAA2B,CAAC,CAAC;IACvE,MAAM,cAAc,GAAG,QAAQ,CAAC,aAAa,CAAC,wBAAwB,CAAsB,CAAC;IAC7F,cAAc,CAAC,QAAQ,GAAG,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC;AACpD,CAAC,CAAC,CAAC;AAEH;;;;;;GAMG;AACH,SAAS,uBAAuB,CAAC,KAAiB;IAC9C,MAAM,MAAM,GAAG,KAAK,CAAC,MAAqB,CAAC;IAC3C,MAAM,CAAC,OAAO,CAAC,cAAc,CAAE,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC;IACvF,MAAM,CAAC,OAAO,CAAC,cAAc,CAAE,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC;AAC5F,CAAC;AAED,UAAU,CAAC,qBAAqB,EAAE,OAAO,EAAE,uBAAuB,CAAC,CAAC;AAGpE;;;;;;;GAOG;AACH,SAAS,iBAAiB,CAAC,MAAkB;;IACzC,MAAM,cAAc,GAAG,MAAA,SAAS,CAAC,cAAc,CAAC,mCAAI,KAAK,CAAC;IAC1D,MAAM,UAAU,GAAG,cAAc,IAAI,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC;IAC5D,QAAQ,CAAC,MAAM,GAAG,gBAAgB,UAAU,gBAAgB,CAAC;IAC7D,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;AAC7B,CAAC;AAED,UAAU,CAAC,oBAAoB,EAAE,OAAO,EAAE,iBAAiB,CAAC,CAAC;AAG7D;;;;;;;GAOG;AACH,SAAS,oBAAoB,CAAC,KAAiB;;IAC3C,MAAM,MAAM,GAAG,KAAK,CAAC,MAAqB,CAAC;IAC3C,MAAM,OAAO,GAAG,MAAM,CAAC,YAAY,CAAC,eAAe,CAAC,CAAC;IACrD,IAAI,OAAO,KAAK,IAAI,EAAE;QAClB,OAAO;KACV;IACD,MAAM,GAAG,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAC3C,MAAM,QAAQ,GAAG,IAAI,eAAe,EAAE,CAAC;IACvC,QAAQ,CAAC,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IACrC,QAAQ,CAAC,MAAM,CAAC,YAAY,EAAE,MAAA,SAAS,CAAC,YAAY,CAAC,mCAAI,EAAE,CAAC,CAAC;IAC7D,KAAK,CAAC,GAAG,EAAE;QACP,QAAQ,EAAE,MAAM;QAChB,MAAM,EAAE,QAAQ;KACnB,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;QAC5C,MAAM,cAAc,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC;QACzC,IAAI,cAAc,EAAE;YAChB,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC;SACvD;aAAM;YACH,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,cAAc,EAAE,SAAS,CAAC,CAAC;SACvD;IACL,CAAC,CAAC,CAAC,CAAC;AACR,CAAC;AAED,UAAU,CAAC,iBAAiB,EAAE,OAAO,EAAE,oBAAoB,CAAC,CAAC;AAE7D;;GAEG;AACH,SAAS,sBAAsB;IAC3B,MAAM,UAAU,GAAa,EAAE,CAAC;IAChC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IAC/B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE;QACzB,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,EAAC,KAAK,EAAE,MAAM,EAAC,CAAC,CAAC,CAAC;QAC9D,IAAI,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;KACxB;IACD,OAAO,UAAU,CAAC;AACtB,CAAC;AAED;;GAEG;AACH,SAAS,gBAAgB;IACrB,MAAM,UAAU,GAAG,sBAAsB,EAAE,CAAC;IAE5C,MAAM,GAAG,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACvC,KAAK,CAAC,GAAG,CAAC;SACL,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;SACjC,IAAI,CAAC,CAAC,QAAqB,EAAE,EAAE;QAC5B,MAAM,QAAQ,GAAG,EAAE,CAAC;QACpB,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE;YACnD,MAAM,IAAI,GAAG,EAAE,CAAC;YAChB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,EAAE;gBAC1B,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;aACrD;YACD,QAAQ,CAAC,IAAI,CAAC;gBACV,IAAI,EAAE,IAAI;gBACV,KAAK,EAAE,IAAI;aACd,CAAC,CAAC;SACN;QAED,IAAI,KAAK,CACL,qBAAqB,EACrB;YACI,IAAI,EAAE,KAAK;YACX,IAAI,EAAE;gBACF,QAAQ,EAAE,QAAQ;gBAClB,MAAM,EAAE,UAAU;aACrB;SACJ,CACJ,CAAC;IACN,CAAC,CAAC,CAAC;AACX,CAAC;AAED,oDAAoD;AACpD,gBAAgB,CAAC;AAEjB;;;;;GAKG;AACH,SAAS,eAAe,CAAC,SAAiB;IACtC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC;IACjC,uEAAuE;IACvE,sCAAsC;IACtC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE;QACzC,SAAS,EAAE,QAAQ;QACnB,SAAS,EAAE,QAAQ;KACf,CAAC,CAAC;IACV,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AAC7B,CAAC;AAED,QAAQ,CAAC,gBAAgB,CAAC,kBAAkB,EAAE;IAC1C,MAAM,CAAC,mBAAmB,GAAG,CAAC,CAAC;IAE/B,qBAAqB;IACrB,MAAM,kBAAkB,GAAG,EAAE,CAAC,KAAK,CAAC,IAAI,CACpC,QAAQ,CAAC,gBAAgB,CAAC,4BAA4B,CAAC,CAC1D,CAAC;IACF,kBAAkB,CAAC,GAAG,CAAC,CAAC,gBAAgB,EAAE,EAAE;QACxC,OAAO,IAAI,SAAS,CAAC,OAAO,CAAC,gBAAgB,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;IACxE,CAAC,CAAC,CAAC;IAEH,sCAAsC;IACtC,MAAM,KAAK,GAAG,QAAQ,CAAC,gBAAgB,CAAC,mBAAmB,CAAC,CAAC;IAC7D,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;QAC/B,IAAI,CAAC,gBAAgB,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE;YACtC,IAAI,CAAE,IAAwB,CAAC,aAAa,EAAE,EAAE;gBAC5C,KAAK,CAAC,cAAc,EAAE,CAAC;gBACvB,KAAK,CAAC,eAAe,EAAE,CAAC;aAC3B;YAED,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;QACxC,CAAC,EAAE,KAAK,CAAC,CAAC;IACd,CAAC,CAAC,CAAC;IAEH,gDAAgD;IAChD,QAAQ,CAAC,gBAAgB,CAAC,2BAA2B,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;QACnE,MAAM,SAAS,GAAG,UAAU,CAAC,GAAG,CAAC,UAAU,CAAC,YAAY,CAAC,oBAAoB,CAAE,CAAC,KAAK,CAAC,CAAC;QACvF,GAAG,CAAC,SAAS,GAAG,eAAe,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IACtD,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC"} \ No newline at end of file
diff --git a/fietsboek/static/theme.css b/fietsboek/static/theme.css
index 2298b49..1005e75 100644
--- a/fietsboek/static/theme.css
+++ b/fietsboek/static/theme.css
@@ -80,14 +80,6 @@ strong {
flex-wrap: wrap;
}
-.track-image-preview {
- position: relative;
- width: min(317px, 100%);
- height: 178.3125px;
- border: 1px solid grey;
- margin-right: 5px;
- margin-bottom: 5px;
-}
.track-image-preview .delete-image {
position: absolute;
z-index: 5;
@@ -120,6 +112,14 @@ strong {
left: 50%;
transform: translate(-50%, -50%);
}
+.track-image-preview {
+ position: relative;
+ width: min(317px, 100%);
+ height: 178.3125px;
+ border: 1px solid grey;
+ margin-right: 5px;
+ margin-bottom: 5px;
+}
.track-description img {
max-width: 100%;
@@ -259,6 +259,42 @@ strong {
color: inherit;
}
+.contribs-cell div {
+ border: 1px solid rgba(0, 0, 0, 0.175);
+ border-radius: calc(0.375rem - 1px);
+ width: 1.1em;
+ height: 1.1em;
+}
+
+.contribs-rowhead {
+ font-size: 80%;
+ padding: 0 1px;
+}
+
+.contribs-length-0 div {
+ background-color: #FFFFFF;
+}
+
+.contribs-length-1 div {
+ background-color: #8FF0A4;
+}
+
+.contribs-length-2 div {
+ background-color: #75DD95;
+}
+
+.contribs-length-3 div {
+ background-color: #5BC987;
+}
+
+.contribs-length-4 div {
+ background-color: #40B678;
+}
+
+.contribs-length-5 div {
+ background-color: #26A269;
+}
+
.browse-summary {
font-variant: lining-nums;
}
@@ -268,10 +304,59 @@ strong {
width: 25%;
}
+.browse-track-card {
+ display: grid;
+ grid-template: "preview data"/300px auto;
+}
+@media (max-width: 768px) {
+ .browse-track-card {
+ grid-template: "preview" "data";
+ }
+}
+.browse-track-card.card-body {
+ padding: 0px;
+}
+.browse-track-card .browse-track-preview {
+ grid-area: preview;
+}
+@media (max-width: 768px) {
+ .browse-track-card .browse-track-preview {
+ text-align: center;
+ }
+}
+.browse-track-card .browse-track-preview img {
+ width: 300px;
+ height: 300px;
+}
+.browse-track-card .browse-track-data {
+ grid-area: data;
+ padding: var(--bs-card-spacer-y) var(--bs-card-spacer-x);
+}
+
.chart-title {
text-align: center;
}
+/* Admin view layout: We have an extra sidebar for the navigation */
+#adminContainer {
+ display: grid;
+ grid-template-areas: "sidebar main";
+ grid-template-columns: 1fr 5fr;
+ gap: 1rem;
+}
+
+#adminNavigation {
+ grid-area: sidebar;
+}
+
+#adminContent {
+ grid-area: main;
+}
+
+.admin-stat {
+ font-size: 120%;
+}
+
.list-group.list-group-root {
padding: 0;
overflow: hidden;
diff --git a/fietsboek/static/theme.css.map b/fietsboek/static/theme.css.map
index 08ac64f..262e299 100644
--- a/fietsboek/static/theme.css.map
+++ b/fietsboek/static/theme.css.map
@@ -1 +1 @@
-{"version":3,"sourceRoot":"","sources":["../../asset-sources/theme.scss"],"names":[],"mappings":"AAAA;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;;AAEA;EACE;EACA;EACA;;;AAIJ;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EAqCE;EACA;EACA;EACA;EACA;EACA;;AAzCA;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAWJ;EACI;;;AAGJ;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;;;AAIJ;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;;AAEA;EACE;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;;AAIJ;EACE;;AAEF;EACE;;AACA;EACE;;AAGJ;EACE;;AACA;EACE;;AAGJ;EACE;;AACA;EACE;;AAGJ;EACE;;AACA;EACE;;AAGJ;EACE;EACA;;AACA;EACE;;AAIJ;EACE;EACA;;;AAIJ;EACE;;;AAGF;AACA;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE","file":"theme.css"} \ No newline at end of file
+{"version":3,"sourceRoot":"","sources":["../../asset-sources/theme.scss"],"names":[],"mappings":"AAAA;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;;AAEA;EACE;EACA;EACA;;;AAIJ;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAIA;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAlCJ;EAqCE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACI;;;AAGJ;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;;;AAIJ;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;;AAEA;EACE;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;;AAIJ;EACE;;AAEF;EACE;;AACA;EACE;;AAGJ;EACE;;AACA;EACE;;AAGJ;EACE;;AACA;EACE;;AAGJ;EACE;;AACA;EACE;;AAGJ;EACE;EACA;;AACA;EACE;;AAIJ;EACE;EACA;;;AAKF;EACE;EACA;EAEA;EACA;;;AAIJ;EACE;EACA;;;AAGF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAGF;EACE;;;AAGF;AACA;EACE;;;AAGF;EACE;EACA;;AAEA;EAJF;IAKI,eACE;;;AAIJ;EACE;;AAGF;EACE;;AAEA;EAHF;IAII;;;AAGF;EACE;EACA;;AAIJ;EACE;EACA;;;AAIJ;EACE;;;AAGF;AACA;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE","file":"theme.css"} \ No newline at end of file
diff --git a/fietsboek/summaries.py b/fietsboek/summaries.py
index 0bee6ca..502e2f9 100644
--- a/fietsboek/summaries.py
+++ b/fietsboek/summaries.py
@@ -2,11 +2,18 @@
import datetime
from dataclasses import dataclass
-from typing import Optional
+from typing import Iterator, Optional
+
+from babel import Locale
+from babel.dates import format_date
+from pyramid.i18n import Localizer
+from pyramid.request import Request
from . import util
from .models.track import TrackWithMetadata
+ONE_WEEK = datetime.timedelta(days=7)
+
class Summary:
"""A summary of a user's tracks.
@@ -294,4 +301,168 @@ class CumulativeStats:
return util.round_to_seconds((self.moving_time + self.stopped_time) / self.count)
-__all__ = ["Summary", "YearSummary", "MonthSummary", "CumulativeStats"]
+class YearContributions:
+ """Github-contributions-tracker-like overview for tracks."""
+
+ def __init__(self, end_date: datetime.date):
+ self.end_date = end_date
+ self.start_date = end_date - 52 * ONE_WEEK
+ # Make sure the start of the range is on a Monday
+ self.start_date -= datetime.timedelta(days=end_date.weekday())
+ assert (self.start_date - self.end_date).days <= 53 * 7
+
+ self.tracks: dict[datetime.date, list[TrackWithMetadata]] = {}
+
+ def add(self, track: TrackWithMetadata):
+ """Adds a track to this overview.
+
+ :param track: The track to add.
+ """
+ if track.date is None:
+ return
+ date = track.date.date()
+ if self.start_date <= date <= self.end_date:
+ self.tracks.setdefault(date, []).append(track)
+
+ def rows(self) -> Iterator[Iterator[DayContributions]]:
+ """Returns the rows for the overview table.
+
+ Each row is a list of the cells. Seven rows will be returned, one for each weekday.
+
+ :return: An iterator over the rows to render.
+ """
+ for weekday in range(7):
+ row = []
+ day = self.start_date + datetime.timedelta(days=weekday)
+ while day <= self.end_date:
+ row.append(DayContributions(day, self.tracks.get(day, [])))
+ day += ONE_WEEK
+ yield iter(row)
+
+ def _rowheads(self, localizer: Localizer) -> Iterator[str | None]:
+ """Returns the row heads (i.e., weekday names) to be prepended to the rows.
+
+ :param localizer: The localizer to use.
+ :return: An iterator over the column headers.
+ """
+ loc = Locale.parse(localizer.locale_name)
+ days = loc.days["stand-alone"]["abbreviated"]
+ return iter(
+ [
+ days[0],
+ None,
+ None,
+ days[3],
+ None,
+ None,
+ days[6],
+ ]
+ )
+
+ def rows_with_heads(
+ self, localizer: Localizer
+ ) -> Iterator[tuple[str | None, Iterator[DayContributions]]]:
+ """Returns each row with its associated heading (weekday name).
+
+ :param localizer: The localizer to use.
+ :return: An iterator over (header, cells) pairs.
+ """
+ return zip(self._rowheads(localizer), self.rows())
+
+ def header_row(self, localizer: Localizer) -> Iterator[str | None]:
+ """Returns the header row.
+
+ Each string marks a month name (spanning three columns), and a ``None`` is an empty column.
+
+ :param localizer: The localizer to use.
+ :return: An iterator over the column headers.
+ """
+ locale = localizer.locale_name
+ day = self.start_date
+ while day <= self.end_date:
+ diq = day + datetime.timedelta(days=6)
+ if diq.day <= 7:
+ yield format_date(diq, "MMM", locale=locale)
+ day += 3 * ONE_WEEK
+ else:
+ yield None
+ day += ONE_WEEK
+
+
+class DayContributions:
+ """A single cell (representing one day) for the YearContributions."""
+
+ def __init__(self, day: datetime.date, tracks: list[TrackWithMetadata]):
+ self.day = day
+ self.tracks = tracks
+
+ def total_length(self) -> float:
+ """The total length of all tracks on this day, in meters."""
+ return sum(track.length for track in self.tracks)
+
+ def count(self) -> int:
+ """The number of tracks in this day."""
+ return len(self.tracks)
+
+ def cell_style(self) -> str:
+ """Returns the CSS style that should be applied to the cell.
+
+ :return: The CSS class.
+ """
+ length = self.total_length() / 1000
+ # Arbitrary cutoffs, should maybe be the same as for the calendar
+ if length == 0:
+ return "contribs-length-0"
+ if length <= 25:
+ return "contribs-length-1"
+ if length <= 50:
+ return "contribs-length-2"
+ if length <= 75:
+ return "contribs-length-3"
+ if length <= 100:
+ return "contribs-length-4"
+ return "contribs-length-5"
+
+ def tooltip(self, localizer: Localizer) -> str:
+ """Generates the tooltip for the cell, localized to the given language.
+
+ :param localizer: The localizer to use.
+ :return: The tooltip text.
+ """
+ formatted_date = format_date(self.day, locale=localizer.locale_name)
+ return localizer.pluralize(
+ "page.profile.contribution_card.tooltip",
+ "page.profile.contribution_card.tooltips",
+ self.count(),
+ mapping={
+ "count": self.count(),
+ "date": formatted_date,
+ },
+ )
+
+ def browse_url(self, request: Request) -> str:
+ """Returns a URL to the browse page with filters for this day applied.
+
+ :param request: The pyramid request.
+ :return: The URL.
+ """
+ datestring = self.day.strftime("%Y-%m-%d")
+ return request.route_url(
+ "browse",
+ _query={
+ "type[]": "ORGANIC",
+ "show-only[]": "user-tagged",
+ "min-date": datestring,
+ "max-date": datestring,
+ },
+ )
+
+
+__all__ = [
+ "Summary",
+ "YearSummary",
+ "MonthSummary",
+ "CumulativeStats",
+ "YearContributions",
+ "DayContributions",
+]
diff --git a/fietsboek/templates/403.jinja2 b/fietsboek/templates/403.jinja2
new file mode 100644
index 0000000..9f4478e
--- /dev/null
+++ b/fietsboek/templates/403.jinja2
@@ -0,0 +1,16 @@
+{% extends "layout.jinja2" %}
+
+{% block content %}
+<div class="container">
+ <h1>{{ _("403.title") }}</h1>
+ <div style="text-align: center;">
+ <img src="{{ request.static_url('fietsboek:static/NoEntry.svg') }}" style="width: min(100%, 300px); margin: auto;">
+ <p>
+ {{ _("403.no_access") }}
+ </p>
+ <p>
+ {{ _("403.try_log_in") }}
+ </p>
+ </div>
+</div>
+{% endblock content %}
diff --git a/fietsboek/templates/404.jinja2 b/fietsboek/templates/404.jinja2
index aaf1241..9c7cc72 100644
--- a/fietsboek/templates/404.jinja2
+++ b/fietsboek/templates/404.jinja2
@@ -1,8 +1,16 @@
{% extends "layout.jinja2" %}
{% block content %}
-<div class="content">
- <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Starter project</span></h1>
- <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p>
+<div class="container">
+ <h1>{{ _("404.title") }}</h1>
+ <div style="text-align: center;">
+ <img src="{{ request.static_url('fietsboek:static/DeadEnd.svg') }}" style="width: min(100%, 300px); margin: auto;">
+ <p>
+ {{ _("404.path_not_found") }}
+ </p>
+ <p>
+ {{ _("404.choose_different") }}
+ </p>
+ </div>
</div>
{% endblock content %}
diff --git a/fietsboek/templates/admin.jinja2 b/fietsboek/templates/admin.jinja2
index 3201d8d..e05e9f0 100644
--- a/fietsboek/templates/admin.jinja2
+++ b/fietsboek/templates/admin.jinja2
@@ -4,45 +4,18 @@
<div class="container">
<h1>{{ _("page.admin.title") }}</h1>
- <h2>{{ _("page.admin.badges") }}</h2>
+ <div id="adminContainer">
+ <aside id="adminNavigation">
+ <nav class="nav nav-pills nav-fill flex-column">
+ <a class="nav-link{% if admin_index == 0 %} active text-bg-dark{% endif %}" href="{{ request.route_url('admin') }}">{{ _("page.admin.nav.overview") }}</a>
+ <a class="nav-link{% if admin_index == 1 %} active text-bg-dark{% endif %}" href="{{ request.route_url('admin-badge') }}">{{ _("page.admin.nav.badges") }}</a>
+ </nav>
+ </aside>
- <div class="list-group">
- {% for badge in badges %}
- <span href="#" class="list-group-item list-group-item-action d-flex admin-badge-list">
- {{ util.render_badge(badge) }}
- <form method="POST" enctype="multipart/form-data" action="{{ request.route_path('admin-badge-edit') }}">
- <input type="hidden" name="badge-edit-id" value="{{ badge.id }}">
- <div class="mb-3">
- <input type="text" class="form-control" name="badge-title" value="{{ badge.title }}">
- </div>
- <div class="mb-3">
- <input class="form-control" type="file" name="badge-image">
- </div>
- {{ util.hidden_csrf_input() }}
- <div class="mb-3">
- <button class="btn btn-primary">{{ _("page.admin.badge.edit") }}</button>
- </div>
- </form>
- <form method="POST" action="{{ request.route_path('admin-badge-delete') }}">
- <input type="hidden" name="badge-delete-id" value="{{ badge.id }}">
- {{ util.hidden_csrf_input() }}
- <button class="btn btn-danger"><i class="bi bi-trash"></i> {{ _("page.admin.badge.delete_badge") }}</button>
- </form>
- </span>
- {% endfor %}
- </div>
-
- <form method="POST" enctype="multipart/form-data" action="{{ request.route_path('admin-badge-add') }}">
- <div class="mb-3">
- <label for="badge-title" class="form-label">{{ _("page.admin.badges.badge_title") }}</label>
- <input type="text" class="form-control" id="badge-title" name="badge-title">
+ <div id="adminContent">
+ {% block admin_content %}
+ {% endblock %}
</div>
- <div class="mb-3">
- <label for="badge-image" class="form-label">{{ _("page.admin.badges.badge_image") }}</label>
- <input class="form-control" type="file" name="badge-image">
- </div>
- {{ util.hidden_csrf_input() }}
- <button type="submit" class="btn btn-primary">{{ _("page.admin.badges.add_badge") }}</button>
- </form>
+ </div>
</div>
{% endblock %}
diff --git a/fietsboek/templates/admin_badges.jinja2 b/fietsboek/templates/admin_badges.jinja2
new file mode 100644
index 0000000..efe8d2c
--- /dev/null
+++ b/fietsboek/templates/admin_badges.jinja2
@@ -0,0 +1,45 @@
+{% set admin_index = 1 %}
+{% extends "admin.jinja2" %}
+{% import "util.jinja2" as util with context %}
+{% block admin_content %}
+<h2>{{ _("page.admin.badges") }}</h2>
+
+<div class="list-group">
+ {% for badge in badges %}
+ <span href="#" class="list-group-item list-group-item-action d-flex admin-badge-list">
+ {{ util.render_badge(badge) }}
+ <form method="POST" enctype="multipart/form-data" action="{{ request.route_path('admin-badge-edit') }}">
+ <input type="hidden" name="badge-edit-id" value="{{ badge.id }}">
+ <div class="mb-3">
+ <input type="text" class="form-control" name="badge-title" value="{{ badge.title }}">
+ </div>
+ <div class="mb-3">
+ <input class="form-control" type="file" name="badge-image">
+ </div>
+ {{ util.hidden_csrf_input() }}
+ <div class="mb-3">
+ <button class="btn btn-success"><i class="bi bi-pencil"></i> {{ _("page.admin.badge.edit") }}</button>
+ <button class="btn btn-danger" form="deleteBadge{{ badge.id }}"><i class="bi bi-trash"></i> {{ _("page.admin.badge.delete_badge") }}</button>
+ </div>
+ </form>
+ <form method="POST" id="deleteBadge{{ badge.id }}" action="{{ request.route_path('admin-badge-delete') }}">
+ <input type="hidden" name="badge-delete-id" value="{{ badge.id }}">
+ {{ util.hidden_csrf_input() }}
+ </form>
+ </span>
+ {% endfor %}
+</div>
+
+<form method="POST" enctype="multipart/form-data" action="{{ request.route_path('admin-badge-add') }}">
+ <div class="mb-3">
+ <label for="badge-title" class="form-label">{{ _("page.admin.badges.badge_title") }}</label>
+ <input type="text" class="form-control" id="badge-title" name="badge-title">
+ </div>
+ <div class="mb-3">
+ <label for="badge-image" class="form-label">{{ _("page.admin.badges.badge_image") }}</label>
+ <input class="form-control" type="file" name="badge-image">
+ </div>
+ {{ util.hidden_csrf_input() }}
+ <button type="submit" class="btn btn-primary">{{ _("page.admin.badges.add_badge") }}</button>
+</form>
+{% endblock %}
diff --git a/fietsboek/templates/admin_overview.jinja2 b/fietsboek/templates/admin_overview.jinja2
new file mode 100644
index 0000000..fbb626b
--- /dev/null
+++ b/fietsboek/templates/admin_overview.jinja2
@@ -0,0 +1,97 @@
+{% set admin_index = 0 %}
+{% extends "admin.jinja2" %}
+{% block admin_content %}
+<p class="admin-stat">
+ {{ _("admin.overview.instance_has") }}&hellip;
+</p>
+
+<p class="admin-stat">
+ &hellip; {{ ngettext("admin.overview.stat.user", "admin.overview.stat.users", user_count) }}
+</p>
+
+<p class="admin-stat">
+ &hellip; {{ ngettext("admin.overview.stat.track", "admin.overview.stat.tracks", track_count) }}
+</p>
+
+<p class="admin-stat">
+ &hellip; {{ (total_size / 1024 / 1024) | round(2) }} {{ _("admin.overview.stats.mib") }}
+</p>
+
+<div style="position: relative; height: 500px; margin: auto; width: 75%;">
+ <canvas id="graph-size-breakdown"></canvas>
+</div>
+
+<h2>{{ _("admin.overview.system_overview") }}</h2>
+
+<table class="table">
+ <tr>
+ <td>{{ _("admin.overview.fietsboek_version") }}</td>
+ <td>{{ versions["fietsboek"] }}</td>
+ </tr>
+ <tr>
+ <td>{{ _("admin.overview.python_version") }}</td>
+ <td>{{ versions["python"] }}</td>
+ </tr>
+ <tr>
+ <td>{{ _("admin.overview.kernel_version") }}</td>
+ <td>{{ versions["linux"] }}</td>
+ </tr>
+ <tr>
+ <td>{{ _("admin.overview.distro_version") }}</td>
+ <td>{{ versions["distro"] }}</td>
+ </tr>
+ <tr class="{% if cron_good %}table-success{% else %}table-warning{% endif %}">
+ <td>{{ _("admin.overview.last_cronjob") }} {% if not cron_good %}<i class="bi bi-exclamation-triangle-fill"></i>{% endif %}</td>
+ <td>{{ last_cronjob }}</td>
+ </tr>
+</table>
+{% endblock %}
+
+{% block latescripts %}
+<script>
+ (function() {
+ const data = {
+ labels: [
+ {{ _("admin.overview.storage_graph.label.track_data") | tojson }},
+ {{ _("admin.overview.storage_graph.label.backups") | tojson }},
+ {{ _("admin.overview.storage_graph.label.images") | tojson }},
+ {{ _("admin.overview.storage_graph.label.track_previews") | tojson }},
+ {{ _("admin.overview.storage_graph.label.journey_previews") | tojson }},
+ {{ _("admin.overview.storage_graph.label.user_maps") | tojson }}
+ ],
+ datasets: [
+ {
+ label: "MiB",
+ data: [
+ {{ (size_breakdown.track_data / 1024 / 1024) | tojson }},
+ {{ (size_breakdown.backups / 1024 / 1024) | tojson }},
+ {{ (size_breakdown.image_files / 1024 / 1024) | tojson }},
+ {{ (size_breakdown.track_previews / 1024 / 1024) | tojson }},
+ {{ (size_breakdown.journey_previews / 1024 / 1024) | tojson }},
+ {{ (size_breakdown.user_maps / 1024 / 1024) | tojson }}
+ ]
+ }
+ ]
+ };
+
+ const config = {
+ type: 'pie',
+ data: data,
+ options: {
+ responsive: true,
+ plugins: {
+ legend: {
+ position: 'top',
+ },
+ title: {
+ display: true,
+ text: {{ _("admin.overview.storage_graph.title") | tojson }}
+ }
+ }
+ }
+ };
+
+ new Chart("graph-size-breakdown", config);
+ })();
+</script>
+{% endblock %}
diff --git a/fietsboek/templates/browse.jinja2 b/fietsboek/templates/browse.jinja2
index 8877229..b4a99b2 100644
--- a/fietsboek/templates/browse.jinja2
+++ b/fietsboek/templates/browse.jinja2
@@ -51,6 +51,26 @@
<div class="col-md-4">
<div class="input-group">
<button type="button" class="btn btn-outline-secondary button-clear-input"><i class="bi bi-eraser-fill"></i></button>
+ <input name="min-uphill" type="number" class="form-control ui-element" placeholder="{{ _("page.browse.filter.uphill_minimum") }}" value="{{ request.params.get('min-uphill', '') }}">
+ <span class="input-group-text ui-element">m</span>
+ </div>
+ </div>
+
+ <div class="col-md-4 fs-5 d-flex align-items-center justify-content-center ui-element">
+ {{ _("page.browse.filter.uphill_boundaries") }}
+ </div>
+
+ <div class="col-md-4">
+ <div class="input-group">
+ <button type="button" class="btn btn-outline-secondary button-clear-input"><i class="bi bi-eraser-fill"></i></button>
+ <input name="max-uphill" type="number" class="form-control ui-element" placeholder="{{ _("page.browse.filter.uphill_maximum") }}" value="{{ request.params.get('max-uphill', '') }}">
+ <span class="input-group-text ui-element">m</span>
+ </div>
+ </div>
+
+ <div class="col-md-4">
+ <div class="input-group">
+ <button type="button" class="btn btn-outline-secondary button-clear-input"><i class="bi bi-eraser-fill"></i></button>
<input name="min-date" type="date" class="form-control ui-element" value="{{ request.params.get('min-date', '') }}">
</div>
</div>
@@ -122,6 +142,8 @@
<option value="DATE_ASC" {% if request.params.get("sort") == "DATE_ASC" %}selected{% endif %}>{{ _("page.browse.sort.date") }} &ShortUpArrow;</option>
<option value="LENGTH_DESC" {% if request.params.get("sort") == "LENGTH_DESC" %}selected{% endif %}>{{ _("page.browse.sort.length") }} &ShortDownArrow;</option>
<option value="LENGTH_ASC" {% if request.params.get("sort") == "LENGTH_ASC" %}selected{% endif %}>{{ _("page.browse.sort.length") }} &ShortUpArrow;</option>
+ <option value="UPHILL_DESC" {% if request.params.get("sort") == "UPHILL_DESC" %}selected{% endif %}>{{ _("page.browse.sort.uphill") }} &ShortDownArrow;</option>
+ <option value="UPHILL_ASC" {% if request.params.get("sort") == "UPHILL_ASC" %}selected{% endif %}>{{ _("page.browse.sort.uphill") }} &ShortUpArrow;</option>
<option value="DURATION_DESC" {% if request.params.get("sort") == "DURATION_DESC" %}selected{% endif %}>{{ _("page.browse.sort.duration") }} &ShortDownArrow;</option>
<option value="DURATION_ASC" {% if request.params.get("sort") == "DURATION_ASC" %}selected{% endif %}>{{ _("page.browse.sort.duration") }} &ShortUpArrow;</option>
</select>
@@ -151,64 +173,93 @@
{% endif %}
</span>
</h5>
- <div class="card-body">
- <table class="table table-hover table-sm browse-summary">
- <tbody>
- <tr>
- <th scope="row">{{ _("page.details.date") }}</th>
- <td>{{ track.date | format_datetime }}</td>
- <th scope="row">{{ _("page.details.length") }}</th>
- <td>{{ (track.length / 1000) | round(2) | format_decimal }} km</td>
- </tr>
- {% if track.show_organic_data() %}
- <tr>
- <th scope="row">{{ _("page.details.start_time") }}</th>
- <td>{{ track.start_time | format_datetime }}</td>
- <th scope="row">{{ _("page.details.end_time") }}</th>
- <td>{{ track.end_time | format_datetime }}</td>
- </tr>
- {% endif %}
- <tr>
- <th scope="row">{{ _("page.details.uphill") }}</th>
- <td>{{ track.uphill | round(2) | format_decimal }} m</td>
- <th scope="row">{{ _("page.details.downhill") }}</th>
- <td>{{ track.downhill | round(2) | format_decimal }} m</td>
- </tr>
- {% if track.show_organic_data() %}
- <tr>
- <th scope="row">{{ _("page.details.moving_time") }}</th>
- <td>{{ track.moving_time }}</td>
- <th scope="row">{{ _("page.details.stopped_time") }}</th>
- <td>{{ track.stopped_time }}</td>
- </tr>
- <tr>
- <th scope="row">{{ _("page.details.max_speed") }}</th>
- <td>{{ mps_to_kph(track.max_speed) | round(2) | format_decimal }} km/h</td>
- <th scope="row">{{ _("page.details.avg_speed") }}</th>
- <td>{{ mps_to_kph(track.avg_speed) | round(2) | format_decimal }} km/h</td>
- </tr>
- {% endif %}
- <tr>
- <th scope="row"><i class="bi bi-chat-left-text-fill"></i> {{ _("page.browse.card.comments") }}</th>
- <td>{{ track.comments | length }}</td>
- <th scope="row"><i class="bi bi-images"></i> {{ _("page.browse.card.images") }}</th>
- <td>{{ track.images | length }}</td>
- </tr>
- </tbody>
- </table>
-
- {% if track.show_organic_data() %}
- <ul>
- <li>{{ track.owner.name }}</li>
- {% for user in track.tagged_people %}
- <li>{{ user.name }}</li>
- {% endfor %}
- </ul>
- {% endif %}
+ <div class="card-body browse-track-card">
+ <div class="browse-track-preview">
+ <img src="{{ request.route_url('track-map', track_id=track.id) }}">
+ </div>
+ <div class="browse-track-data">
+ <table class="table table-hover table-sm browse-summary">
+ <tbody>
+ <tr>
+ <th scope="row">{{ _("page.details.date") }}</th>
+ <td>{{ track.date | format_datetime }}</td>
+ <th scope="row">{{ _("page.details.length") }}</th>
+ <td>{{ (track.length / 1000) | round(2) | format_decimal }} km</td>
+ </tr>
+ {% if track.show_organic_data() %}
+ <tr>
+ <th scope="row">{{ _("page.details.start_time") }}</th>
+ <td>{{ track.start_time | format_datetime }}</td>
+ <th scope="row">{{ _("page.details.end_time") }}</th>
+ <td>{{ track.end_time | format_datetime }}</td>
+ </tr>
+ {% endif %}
+ <tr>
+ <th scope="row">{{ _("page.details.uphill") }}</th>
+ <td>{{ track.uphill | round(2) | format_decimal }} m</td>
+ <th scope="row">{{ _("page.details.downhill") }}</th>
+ <td>{{ track.downhill | round(2) | format_decimal }} m</td>
+ </tr>
+ {% if track.show_organic_data() %}
+ <tr>
+ <th scope="row">{{ _("page.details.moving_time") }}</th>
+ <td>{{ track.moving_time }}</td>
+ <th scope="row">{{ _("page.details.stopped_time") }}</th>
+ <td>{{ track.stopped_time }}</td>
+ </tr>
+ <tr>
+ <th scope="row">{{ _("page.details.max_speed") }}</th>
+ <td>{{ mps_to_kph(track.max_speed) | round(2) | format_decimal }} km/h</td>
+ <th scope="row">{{ _("page.details.avg_speed") }}</th>
+ <td>{{ mps_to_kph(track.avg_speed) | round(2) | format_decimal }} km/h</td>
+ </tr>
+ {% endif %}
+ <tr>
+ <th scope="row"><i class="bi bi-chat-left-text-fill"></i> {{ _("page.browse.card.comments") }}</th>
+ <td>{{ track.comments | length }}</td>
+ <th scope="row"><i class="bi bi-images"></i> {{ _("page.browse.card.images") }}</th>
+ <td>{{ track.images | length }}</td>
+ </tr>
+ </tbody>
+ </table>
+
+ {% if track.show_organic_data() %}
+ <ul>
+ <li>{{ track.owner.name }}</li>
+ {% for user in track.tagged_people %}
+ <li>{{ user.name }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+ </div>
</div>
</div>
{% endfor %}
<button type="button" class="btn btn-primary ui-element" id="archiveDownloadButton" disabled><i class="bi bi-file-earmark-zip"></i> {{ _("page.browse.download_multiple") }}</button>
+
+ <nav aria-label="Page navigation">
+ <ul class="pagination justify-content-center">
+ {% if page_previous is none %}
+ <li class="page-item disabled">
+ <span class="page-link">{{ _("pagination.previous") }}</span>
+ </li>
+ {% else %}
+ <li class="page-item">
+ <a class="page-link" href="{{ page_previous | safe }}">{{ _("pagination.previous") }}</a>
+ </li>
+ {% endif %}
+ {% if page_next is none %}
+ <li class="page-item disabled">
+ <span class="page-link">{{ _("pagination.next") }}</span>
+ </li>
+ {% else %}
+ <li class="page-item">
+ <a class="page-link" href="{{ page_next | safe }}">{{ _("pagination.next") }}</a>
+ </li>
+ {% endif %}
+ </ul>
+ </nav>
+
{% elif used_filters %}
<p>{{ _("page.browse.no_results") }}</p>
{% else %}
diff --git a/fietsboek/templates/details.jinja2 b/fietsboek/templates/details.jinja2
index d8c9250..f97f01f 100644
--- a/fietsboek/templates/details.jinja2
+++ b/fietsboek/templates/details.jinja2
@@ -1,6 +1,23 @@
{% extends "layout.jinja2" %}
{% import "util.jinja2" as util with context %}
+{% block extrahead %}
+{% if 'secret' in request.GET %}
+{% set preview_url = request.route_url('track-map', track_id=track.id, _query=[('secret', request.GET['secret'])]) %}
+{% else %}
+{% set preview_url = request.route_url('track-map', track_id=track.id) %}
+{% endif %}
+<meta property="og:title" content="{{ track.title | default(track.date | format_datetime, true) }}">
+<meta property="og:type" content="website">
+<meta property="og:url" content="{{ request.url }}">
+<meta property="og:image" content="{{ preview_url }}">
+<meta property="og:image:url" content="{{ preview_url }}">
+<meta property="og:image:type" content="image/png">
+<meta property="og:image:alt" content="Track overview">
+<meta property="og:description" content="{{ (track.length / 1000) | round(2) | format_decimal }} km{% if og_description %}: {{ og_description }}{% endif %}">
+<meta property="og:site_name" content="Fietsboek">
+{% endblock %}
+
{% block content %}
<div class="container">
<h1>
@@ -94,7 +111,14 @@
</div>
<div class="mb-3">
- <a class="btn btn-primary ui-element" href="{{ gpx_url }}"><i class="bi bi-download"></i> {{ _("page.details.download") }}</a>
+ <div class="btn-group" role="group">
+ <a class="btn btn-primary ui-element" href="{{ gpx_url }}"><i class="bi bi-download"></i> {{ _("page.details.download") }}</a>
+ <a class="btn btn-secondary ui-element" href="{% if 'secret' in request.GET %}
+ {{ request.route_path('track-pdf', track_id=track.id, _query=[('secret', request.GET['secret'])]) }}
+ {% else %}
+ {{ request.route_path('track-pdf', track_id=track.id) }}
+ {% endif %}"><i class="bi bi-file-earmark-pdf"></i> {{ _("page.details.download_pdf") }}</a>
+ </div>
</div>
<table class="table table-hover table-sm">
diff --git a/fietsboek/templates/edit.jinja2 b/fietsboek/templates/edit.jinja2
index 6347ae6..26c520f 100644
--- a/fietsboek/templates/edit.jinja2
+++ b/fietsboek/templates/edit.jinja2
@@ -10,6 +10,10 @@
<noscript><p>{{ _("page.noscript") }}<p></noscript>
</div>
<form method="POST" enctype="multipart/form-data">
+ <div class="mb-3">
+ <label class="form-label ui-element" for="newGpx">{{ _("page.edit.form.new_track") }}</label>
+ <input id="newGpx" name="gpx" type="file" class="form-control ui-element">
+ </div>
{{ edit_form.edit_track(track.title, track.date_raw, track.date_tz or 0, track.visibility, track.type, track.description, track.text_tags(), badges, track.tagged_people, images) }}
{{ util.hidden_csrf_input() }}
<div class="btn-group" role="group">
diff --git a/fietsboek/templates/journey_details.jinja2 b/fietsboek/templates/journey_details.jinja2
new file mode 100644
index 0000000..a4941e5
--- /dev/null
+++ b/fietsboek/templates/journey_details.jinja2
@@ -0,0 +1,180 @@
+{% extends "layout.jinja2" %}
+{% import "util.jinja2" as util with context %}
+
+{% block content %}
+<div class="container">
+ <h1>{{ journey.title }}</h1>
+
+ {% if show_edit_link %}
+ <div class="btn-group mb-3" role="group">
+ <a class="btn btn-success ui-element" href="{{ request.route_path('journey-edit', journey_id=journey.id) }}"><i class="bi-pencil-square"></i> {{ _("journey.edit") }}</a>
+ <button type="button" class="btn btn-info ui-element" id="showShareLink" data-bs-toggle="modal" data-bs-target="#shareLinkModal"><i class="bi-share"></i> {{ _("journey.share") }}</button>
+ <button type="button" class="btn btn-danger ui-element" id="deleteLink" data-bs-toggle="modal" data-bs-target="#deleteModal"><i class="bi bi-trash"></i> {{ _("journey.delete") }}</button>
+ </div>
+ <div class="modal fade" id="shareLinkModal" tabindex="-1" aria-hidden="true">
+ <div class="modal-dialog">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h5 class="modal-title">{{ _("journey.sharelink.title") }}</h5>
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+ </div>
+ <div class="modal-body">
+ <p>{{ _("journey.sharelink.info") }}</p>
+ {% set share_link = request.route_url('journey-details', journey_id=journey.id, _query=[("secret", journey.link_secret)]) %}
+ <a href="{{ share_link }}">{{ share_link }}</a>
+ </div>
+ <div class="modal-footer">
+ <form method="POST" action="{{ request.route_url('journey-invalidate-share', journey_id=journey.id) }}">
+ {{ util.hidden_csrf_input() }}
+ <button type="submit" class="btn btn-warning ui-element">{{ _("journey.sharelink.invalidate") }}</button>
+ </form>
+ <button type="button" class="btn btn-secondary ui-element" data-bs-dismiss="modal">{{ _("journey.sharelink.close") }}</button>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="modal fade" id="deleteModal" tabindex="-1" aria-hidden="true">
+ <div class="modal-dialog">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h5 class="modal-title">{{ _("journey.delete.title") }}</h5>
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+ </div>
+ <div class="modal-body">
+ <p>{{ _("journey.delete.info") }}</p>
+ </div>
+ <div class="modal-footer">
+ <form method="POST" action="{{ request.route_url('delete-journey', journey_id=journey.id) }}">
+ {{ util.hidden_csrf_input() }}
+ <button type="submit" class="btn btn-danger ui-element">{{ _("journey.delete.delete") }}</button>
+ </form>
+ <button type="button" class="btn btn-secondary ui-element" data-bs-dismiss="modal">{{ _("journey.delete.close") }}</button>
+ </div>
+ </div>
+ </div>
+ </div>
+ {% endif %}
+
+ {% if 'secret' in request.GET %}
+ {% set gpx_url = request.route_path("journey-gpx", journey_id=journey.id, _query=[('secret', request.GET['secret'])]) %}
+ {% else %}
+ {% set gpx_url = request.route_path("journey-gpx", journey_id=journey.id) %}
+ {% endif %}
+ <div class="mb-3">
+ <div id="mainmap" class="gpxview:{{ gpx_url }}:OSM" style="width:100%;height:600px">
+ <noscript><p>{{ _("page.noscript") }}<p></noscript>
+ </div>
+ </div>
+ <div class="mb-3">
+ <div id="mainmap_hp" style="width:100%;height:300px"></div>
+ </div>
+
+ <table class="table table-hover" style="margin-top: 10px;">
+ <tbody>
+ <tr>
+ <th scope="row">{{ _("page.details.length") }}</th>
+ <td id="detailsLength">{{ (movement_data.length / 1000) | round(2) | format_decimal }} km</td>
+ </tr>
+ <tr>
+ <th scope="row">{{ _("page.details.uphill") }}</th>
+ <td id="detailsUphill">{{ movement_data.uphill | round(2) | format_decimal }} m</td>
+ </tr>
+ <tr>
+ <th scope="row">{{ _("page.details.downhill") }}</th>
+ <td id="detailsDownhill">{{ movement_data.downhill | round(2) | format_decimal }} m</td>
+ </tr>
+ <tr>
+ <th scope="row">{{ _("page.details.moving_time") }}</th>
+ <td id="detailsDownhill">{{ timedelta(seconds=movement_data.moving_duration) }}</td>
+ </tr>
+ <tr>
+ <th scope="row">{{ _("page.details.stopped_time") }}</th>
+ <td id="detailsDownhill">{{ timedelta(seconds=movement_data.stopped_duration) }}</td>
+ </tr>
+ <tr>
+ <th scope="row">{{ _("page.details.max_speed") }}</th>
+ <td id="detailsDownhill">{{ mps_to_kph(movement_data.maximum_speed) | round(2) | format_decimal }} km/h</td>
+ </tr>
+ <tr>
+ <th scope="row">{{ _("page.details.avg_speed") }}</th>
+ <td id="detailsDownhill">{{ mps_to_kph(movement_data.average_speed) | round(2) | format_decimal }} km/h</td>
+ </tr>
+ </tbody>
+ </table>
+
+ {{ md_to_html(journey.description) }}
+
+ <h2>{{ _("journey.tracks") }}</h2>
+
+ {% for track in tracks %}
+ {% if track.track.is_visible_to(request.identity) %}
+ <div class="card mb-3">
+ <h5 class="card-header">
+ <a href="{{ request.route_url('details', track_id=track.id) }}">{{ track.title | default(track.date, true) }}</a>
+ </h5>
+ <div class="card-body browse-track-card">
+ <div class="browse-track-preview">
+ <img src="{{ request.route_url('track-map', track_id=track.id) }}">
+ </div>
+ <div class="browse-track-data">
+ <table class="table table-hover table-sm browse-summary">
+ <tbody>
+ <tr>
+ <th scope="row">{{ _("page.details.date") }}</th>
+ <td>{{ track.date | format_datetime }}</td>
+ <th scope="row">{{ _("page.details.length") }}</th>
+ <td>{{ (track.length / 1000) | round(2) | format_decimal }} km</td>
+ </tr>
+ {% if track.show_organic_data() %}
+ <tr>
+ <th scope="row">{{ _("page.details.start_time") }}</th>
+ <td>{{ track.start_time | format_datetime }}</td>
+ <th scope="row">{{ _("page.details.end_time") }}</th>
+ <td>{{ track.end_time | format_datetime }}</td>
+ </tr>
+ {% endif %}
+ <tr>
+ <th scope="row">{{ _("page.details.uphill") }}</th>
+ <td>{{ track.uphill | round(2) | format_decimal }} m</td>
+ <th scope="row">{{ _("page.details.downhill") }}</th>
+ <td>{{ track.downhill | round(2) | format_decimal }} m</td>
+ </tr>
+ {% if track.show_organic_data() %}
+ <tr>
+ <th scope="row">{{ _("page.details.moving_time") }}</th>
+ <td>{{ track.moving_time }}</td>
+ <th scope="row">{{ _("page.details.stopped_time") }}</th>
+ <td>{{ track.stopped_time }}</td>
+ </tr>
+ <tr>
+ <th scope="row">{{ _("page.details.max_speed") }}</th>
+ <td>{{ mps_to_kph(track.max_speed) | round(2) | format_decimal }} km/h</td>
+ <th scope="row">{{ _("page.details.avg_speed") }}</th>
+ <td>{{ mps_to_kph(track.avg_speed) | round(2) | format_decimal }} km/h</td>
+ </tr>
+ {% endif %}
+ <tr>
+ <th scope="row"><i class="bi bi-chat-left-text-fill"></i> {{ _("page.browse.card.comments") }}</th>
+ <td>{{ track.comments | length }}</td>
+ <th scope="row"><i class="bi bi-images"></i> {{ _("page.browse.card.images") }}</th>
+ <td>{{ track.images | length }}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ {% else %}
+ <div class="card mb-3">
+ <h5 class="card-header">
+ {{ track.title | default(track.date, true) }}
+ </h5>
+ <div class="card-body">
+ {{ _("journeys.track.hidden") }}
+ </div>
+ </div>
+ {% endif %}
+ {% endfor %}
+</div>
+{% endblock %}
diff --git a/fietsboek/templates/journey_edit.jinja2 b/fietsboek/templates/journey_edit.jinja2
new file mode 100644
index 0000000..fa39228
--- /dev/null
+++ b/fietsboek/templates/journey_edit.jinja2
@@ -0,0 +1,21 @@
+{% extends "layout.jinja2" %}
+{% import "journey_form.jinja2" as form with context %}
+
+{% block extrahead %}
+{{ form.journey_css() }}
+{% endblock %}
+
+{% block content %}
+<div class="container">
+ <h1>{{ journey.title }}</h1>
+
+ <form method="POST">
+ {{ form.journey_form(journey) }}
+ </form>
+</div>
+
+{% endblock %}
+
+{% block latescripts %}
+{{ form.journey_js() }}
+{% endblock %}
diff --git a/fietsboek/templates/journey_form.jinja2 b/fietsboek/templates/journey_form.jinja2
new file mode 100644
index 0000000..3776b6c
--- /dev/null
+++ b/fietsboek/templates/journey_form.jinja2
@@ -0,0 +1,261 @@
+{% import "util.jinja2" as util with context %}
+
+{% macro journey_css() %}
+<style>
+.track-query-response, .journey-track {
+ background-color: var(--bs-body-bg);
+ padding: 0.375rem;
+ margin-bottom: 0.1rem;
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+
+ .track-title {
+ font-weight: 450;
+ font-size: 110%;
+ }
+
+ .track-date {
+ color: #808080;
+ }
+
+ .track-length {
+ color: #808080;
+ }
+}
+
+.journey-track {
+ cursor: grab;
+}
+
+.dragging {
+ opacity: 0.7;
+}
+</style>
+{% endmacro %}
+
+
+{% macro journey_form(journey) %}
+<div class="mb-3">
+ <label for="journeyTitle" class="form-label">{{ _("journeys.new.form.title") }}</label>
+ <input type="text" class="form-control" id="journeyTitle" name="journeyTitle" value="{{ journey.title }}" onchange="checkTitleValidity()">
+ <div class="invalid-feedback">
+ {{ _("journeys.new.form.requires_title") }}
+ </div>
+</div>
+<div class="mb-3">
+ <label for="journeyDescription" class="form-label">{{ _("journeys.new.form.description") }}</label>
+ <textarea class="form-control" id="journeyDescription" name="journeyDescription">{{ journey.description }}</textarea>
+</div>
+<div class="mb-3">
+ <label for="journeyVisibility" class="form-label">{{ _("journeys.new.form.visibility") }}</label>
+ <select class="form-select" id="journeyVisibility" name="journeyVisibility">
+ {% set visibility = journey.visibility.name if journey else "" %}
+ <option value="PRIVATE"{% if visibility== "PRIVATE" %} selected{% endif %}>{{ _("journeys.new.form.visibility.private") }}</option>
+ <option value="FRIENDS"{% if visibility== "FRIENDS" %} selected{% endif %}>{{ _("journeys.new.form.visibility.friends") }}</option>
+ <option value="LOGGED_IN"{% if visibility== "LOGGED_IN" %} selected{% endif %}>{{ _("journeys.new.form.visibility.logged_in") }}</option>
+ <option value="PUBLIC"{% if visibility== "PUBLIC" %} selected{% endif %}>{{ _("journeys.new.form.visibility.public") }}</option>
+ </select>
+</div>
+<div class="mb-3">
+ <p>
+ {{ _("journeys.new.form.tracksearch") }}
+ </p>
+ <div class="input-group">
+ <input type="text" id="trackSearch" placeholder="Title" class="form-control">
+ <button class="btn btn-secondary" id="trackSearchButton"><i class="bi bi-search"></i></button>
+ </div>
+</div>
+<div class="mb-3" id="trackSearchResults"></div>
+<div class="mb-3">
+ <p>{{ _("journeys.new.form.tracks") }}<p>
+</div>
+<div class="mb-3" id="journeyTracks">
+ {% for track in journey.tracks %}
+ <div class="journey-track" draggable="true">
+ <input type="hidden" name="journeyTrack[]" value="{{ track.id }}">
+ <button class="btn btn-danger btn-sm"><i class="bi bi-x-circle-fill"></i></button>
+ <div class="track-title">{{ track.title }}</div>
+ <div class="track-length">{{ (track.with_metadata().length / 1000) | round(2) }} km</div>
+ <div class="track-date">{{ track.date | format_datetime }}</div>
+ </div>
+ {% endfor %}
+</div>
+
+{{ util.hidden_csrf_input() }}
+
+<div>
+ <button class="btn btn-primary" type="submit" id="journeySubmit">
+ <i class="bi bi-save"></i>
+ {{ _("journeys.new.form.submit") }}
+ </button>
+ <div class="invalid-feedback">
+ {{ _("journeys.new.form.requires_tracks") }}
+ </div>
+</div>
+
+<template id="queryResponse">
+ <div class="track-query-response">
+ <button class="btn btn-success btn-sm"><i class="bi bi-plus-square-fill"></i></button>
+ <div class="track-title"></div>
+ <div class="track-length"></div>
+ <div class="track-date"></div>
+ </div>
+</template>
+
+<template id="journeyTrack">
+ <div class="journey-track" draggable="true">
+ <input type="hidden" name="journeyTrack[]">
+ <button class="btn btn-danger btn-sm"><i class="bi bi-x-circle-fill"></i></button>
+ <div class="track-title"></div>
+ <div class="track-length"></div>
+ <div class="track-date"></div>
+ </div>
+</template>
+{% endmacro %}
+
+
+{% macro journey_js() %}
+<script>
+// Make sure the mouse pointer stays "grab", even when leaving the list of
+// tracks.
+document.addEventListener("dragover", (event) => event.preventDefault());
+
+let trDrag;
+
+function trDragStart(event) {
+ trDrag = event.target;
+ event.target.closest(".journey-track").classList.add("dragging");
+ event.dataTransfer.effectAllowed = "move";
+}
+
+function trDragOver(event) {
+ let target = event.target.closest(".journey-track");
+
+ // Check whether we are in the top of bottom half of the element
+ let rect = target.getBoundingClientRect();
+ let is_top_half = event.clientY < rect.top + rect.height / 2;
+
+ if (is_top_half) {
+ target.insertAdjacentElement("beforebegin", trDrag);
+ } else {
+ target.insertAdjacentElement("afterend", trDrag);
+ }
+ event.preventDefault();
+}
+
+function trDragLeave(event) {
+ let target = event.target.closest(".journey-track");
+ target.style.marginTop = "";
+ target.style.marginBottom = "";
+ event.preventDefault();
+}
+
+function trDragEnd(event) {
+ trDrag.closest(".journey-track").classList.remove("dragging");
+ trDrag = null;
+}
+
+function removeTrackFromJourney(event) {
+ let track = event.target.closest("div");
+ track.parentNode.removeChild(track);
+
+ checkJourneyValidity();
+
+ event.preventDefault();
+}
+
+addHandler(".journey-track button", "click", removeTrackFromJourney);
+
+function addTrackToJourney(event) {
+ let track = event.target.closest("div");
+ let template = document.getElementById("journeyTrack");
+ let clone = document.importNode(template.content, true);
+
+ clone.querySelector("input").setAttribute("value", track.getAttribute("data-track-id"));
+ for (let sel of [".track-title", ".track-length", ".track-date"]) {
+ clone.querySelector(sel).textContent = track.querySelector(sel).textContent;
+ }
+ clone.querySelector("button").addEventListener("click", removeTrackFromJourney);
+ clone.querySelector(".journey-track").addEventListener("dragstart", trDragStart);
+ clone.querySelector(".journey-track").addEventListener("dragover", trDragOver);
+ clone.querySelector(".journey-track").addEventListener("dragleave", trDragLeave);
+ clone.querySelector(".journey-track").addEventListener("dragend", trDragEnd);
+
+ document.getElementById("journeyTracks").appendChild(clone);
+ track.parentElement.removeChild(track);
+
+ checkJourneyValidity();
+
+ event.preventDefault();
+}
+
+addHandler(".journey-track", "dragstart", trDragStart);
+addHandler(".journey-track", "dragover", trDragOver);
+addHandler(".journey-track", "dragleave", trDragLeave);
+addHandler(".journey-track", "dragend", trDragEnd);
+
+function hasTrack(id) {
+ for (let track of document.querySelectorAll(".journey-track")) {
+ let tid = track.querySelector("input").value;
+ if (parseInt(tid) == id) {
+ return true;
+ }
+ }
+ return false;
+}
+
+function searchTracks() {
+ let template = document.getElementById("queryResponse");
+ let results = document.getElementById("trackSearchResults");
+ let pattern = document.getElementById("trackSearch").value;
+ let url = makeUrl(`/track/?format=json&search-terms=${encodeURIComponent(pattern)}`);
+ fetch(url)
+ .then((response) => response.json())
+ .then((response) => {
+ results.replaceChildren();
+ for (let track of response) {
+ if (hasTrack(track.id)) {
+ continue;
+ }
+ let clone = document.importNode(template.content, true);
+ clone.firstElementChild.setAttribute("data-track-id", track.id);
+ clone.querySelector(".track-title").textContent = track.title.length > 0 ? track.title : formatTimestamp(track.date * 1000);
+ clone.querySelector(".track-date").textContent = formatTimestamp(track.date * 1000);
+ clone.querySelector(".track-length").textContent = `${(track.length / 1000).toFixed(2)} km`;
+ clone.querySelector("button").addEventListener("click", addTrackToJourney);
+ results.appendChild(clone);
+ }
+ });
+}
+
+function checkTitleValidity() {
+ let title = document.querySelector("#journeyTitle");
+ if (title.value.length > 0) {
+ title.setCustomValidity("");
+ } else {
+ title.setCustomValidity("title missing");
+ }
+}
+
+checkTitleValidity();
+
+function checkJourneyValidity() {
+ let btn = document.querySelector("#journeySubmit");
+ let track_count = document.querySelectorAll(".journey-track").length;
+
+ if (track_count == 0) {
+ btn.setCustomValidity("no tracks");
+ } else {
+ btn.setCustomValidity("");
+ }
+}
+
+checkJourneyValidity();
+
+document.querySelector("#trackSearchButton").addEventListener("click", (event) => {
+ searchTracks();
+ event.preventDefault();
+});
+</script>
+{% endmacro %}
diff --git a/fietsboek/templates/journey_list.jinja2 b/fietsboek/templates/journey_list.jinja2
new file mode 100644
index 0000000..8c7bfe9
--- /dev/null
+++ b/fietsboek/templates/journey_list.jinja2
@@ -0,0 +1,32 @@
+{% extends "layout.jinja2" %}
+{% block content %}
+<div class="container">
+ <h1>{{ _("journeys.overview.title") }}</h1>
+
+ {% if show_new_button %}
+ <div class="mb-3">
+ <a href="{{ request.route_url('journey-new') }}" class="btn btn-primary">
+ <i class="bi bi-plus-circle"></i>
+ {{ _("journeys.overview.new") }}
+ </a>
+ </div>
+ {% endif %}
+
+ {% for journey in journeys %}
+ <div class="card mb-5">
+ <img src="{{ request.route_url('journey-map', journey_id=journey.id) }}" class="card-img-top" alt="Rendered map of the journey">
+ <div class="card-body">
+ <h5 class="card-title">
+ <a href="{{ request.route_url('journey-details', journey_id=journey.id) }}">{{ journey.title }}</a>
+ </h5>
+ {{ md_to_html(journey.description) }}
+ </div>
+ <ul class="list-group list-group-flush">
+ {% for track in journey.tracks %}
+ <li class="list-group-item">{{ track.title | default(track.date, true) }}</li>
+ {% endfor %}
+ </ul>
+ </div>
+ {% endfor %}
+</div>
+{% endblock %}
diff --git a/fietsboek/templates/journey_new.jinja2 b/fietsboek/templates/journey_new.jinja2
new file mode 100644
index 0000000..b1cfffb
--- /dev/null
+++ b/fietsboek/templates/journey_new.jinja2
@@ -0,0 +1,21 @@
+{% extends "layout.jinja2" %}
+{% import "journey_form.jinja2" as form with context %}
+
+{% block extrahead %}
+{{ form.journey_css() }}
+{% endblock %}
+
+{% block content %}
+<div class="container">
+ <h1>{{ _("journeys.new.title") }}</h1>
+
+ <form method="POST" class="needs-validation" novalidate>
+ {{ form.journey_form(none) }}
+ </form>
+</div>
+
+{% endblock %}
+
+{% block latescripts %}
+{{ form.journey_js() }}
+{% endblock %}
diff --git a/fietsboek/templates/layout.jinja2 b/fietsboek/templates/layout.jinja2
index c96716d..3058553 100644
--- a/fietsboek/templates/layout.jinja2
+++ b/fietsboek/templates/layout.jinja2
@@ -32,6 +32,7 @@ const Fullscreenbutton = true;
const Legende = false;
</script>
+ {% block extrahead %}{% endblock %}
</head>
<body>
@@ -56,6 +57,9 @@ const Legende = false;
<li class="nav-item">
<a class="nav-link" href="{{ request.route_url('browse') }}">{{ _("page.navbar.browse") }}</a>
</li>
+ <li class="nav-item">
+ <a class="nav-link" href="{{ request.route_url('journey-list') }}">{{ _("page.navbar.journeys") }}</a>
+ </li>
{% if request.identity is not none %}
<li class="nav-item">
<a class="nav-link" href="{{ request.route_url('upload') }}">{{ _("page.navbar.upload") }}</a>
diff --git a/fietsboek/templates/profile_overview.jinja2 b/fietsboek/templates/profile_overview.jinja2
index aa2333c..a088ba5 100644
--- a/fietsboek/templates/profile_overview.jinja2
+++ b/fietsboek/templates/profile_overview.jinja2
@@ -9,48 +9,53 @@
{% for tag in track.tags %}<span class="badge bg-info text-dark">{{ tag.tag }}</span> {% endfor %}
{% endif %}
</h5>
- <div class="card-body">
- <table class="table table-hover table-sm browse-summary">
- <tbody>
- <tr>
- <th scope="row">{{ _("page.details.date") }}</th>
- <td>{{ track.date | format_datetime }}</td>
- <th scope="row">{{ _("page.details.length") }}</th>
- <td>{{ (track.length / 1000) | round(2) | format_decimal }} km</td>
- </tr>
- <tr>
- <th scope="row">{{ _("page.details.start_time") }}</th>
- <td>{{ track.start_time | format_datetime }}</td>
- <th scope="row">{{ _("page.details.end_time") }}</th>
- <td>{{ track.end_time | format_datetime }}</td>
- </tr>
- <tr>
- <th scope="row">{{ _("page.details.uphill") }}</th>
- <td>{{ track.uphill | round(2) | format_decimal }} m</td>
- <th scope="row">{{ _("page.details.downhill") }}</th>
- <td>{{ track.downhill | round(2) | format_decimal }} m</td>
- </tr>
- <tr>
- <th scope="row">{{ _("page.details.moving_time") }}</th>
- <td>{{ track.moving_time }}</td>
- <th scope="row">{{ _("page.details.stopped_time") }}</th>
- <td>{{ track.stopped_time }}</td>
- </tr>
- <tr>
- <th scope="row">{{ _("page.details.max_speed") }}</th>
- <td>{{ mps_to_kph(track.max_speed) | round(2) | format_decimal }} km/h</td>
- <th scope="row">{{ _("page.details.avg_speed") }}</th>
- <td>{{ mps_to_kph(track.avg_speed) | round(2) | format_decimal }} km/h</td>
- </tr>
- </tbody>
- </table>
-
- <ul>
- <li>{{ track.owner.name }}</li>
- {% for user in track.tagged_people %}
- <li>{{ user.name }}</li>
- {% endfor %}
- </ul>
+ <div class="card-body browse-track-card">
+ <div class="browse-track-preview">
+ <img src="{{ request.route_url('track-map', track_id=track.id) }}">
+ </div>
+ <div class="browse-track-data">
+ <table class="table table-hover table-sm browse-summary">
+ <tbody>
+ <tr>
+ <th scope="row">{{ _("page.details.date") }}</th>
+ <td>{{ track.date | format_datetime }}</td>
+ <th scope="row">{{ _("page.details.length") }}</th>
+ <td>{{ (track.length / 1000) | round(2) | format_decimal }} km</td>
+ </tr>
+ <tr>
+ <th scope="row">{{ _("page.details.start_time") }}</th>
+ <td>{{ track.start_time | format_datetime }}</td>
+ <th scope="row">{{ _("page.details.end_time") }}</th>
+ <td>{{ track.end_time | format_datetime }}</td>
+ </tr>
+ <tr>
+ <th scope="row">{{ _("page.details.uphill") }}</th>
+ <td>{{ track.uphill | round(2) | format_decimal }} m</td>
+ <th scope="row">{{ _("page.details.downhill") }}</th>
+ <td>{{ track.downhill | round(2) | format_decimal }} m</td>
+ </tr>
+ <tr>
+ <th scope="row">{{ _("page.details.moving_time") }}</th>
+ <td>{{ track.moving_time }}</td>
+ <th scope="row">{{ _("page.details.stopped_time") }}</th>
+ <td>{{ track.stopped_time }}</td>
+ </tr>
+ <tr>
+ <th scope="row">{{ _("page.details.max_speed") }}</th>
+ <td>{{ mps_to_kph(track.max_speed) | round(2) | format_decimal }} km/h</td>
+ <th scope="row">{{ _("page.details.avg_speed") }}</th>
+ <td>{{ mps_to_kph(track.avg_speed) | round(2) | format_decimal }} km/h</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <ul>
+ <li>{{ track.owner.name }}</li>
+ {% for user in track.tagged_people %}
+ <li>{{ user.name }}</li>
+ {% endfor %}
+ </ul>
+ </div>
</div>
</div>
{% endmacro %}
@@ -104,6 +109,56 @@
</tr>
</table>
+<h2>{{ _("page.profile.contribution_card.title") }}</h2>
+<div class="d-xl-flex justify-content-center" style="overflow: scroll;">
+ <table class="contributions">
+ <thead>
+ <tr>
+ <th></th>
+ {% for cell in year_contributions.header_row(request.localizer) %}
+ {% if cell is none %}
+ <th></th>
+ {% else %}
+ <th colspan="3" class="text-muted">{{ cell }}</th>
+ {% endif %}
+ {% endfor %}
+ </tr>
+ </thead>
+ <tbody>
+ {% for rowhead, row in year_contributions.rows_with_heads(request.localizer) %}
+ <tr>
+ <td class="text-muted contribs-rowhead">{{ rowhead | default("", true) }}</td>
+ {% for cell in row %}
+ <td class="contribs-cell {{ cell.cell_style() }}">
+ <a href="{{ cell.browse_url(request) }}">
+ <!-- Use a div because border-radius does not work on td -->
+ <div data-bs-toggle="tooltip" title="{{ cell.tooltip(request.localizer) }}"></div>
+ </a>
+ </td>
+ {% endfor %}
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+</div>
+
+<div class="d-flex justify-content-end mb-3">
+ <table>
+ <tbody>
+ <tr>
+ <td>{{ _("page.profile.contribution_card.legend.less") }}</td>
+ <td class="contribs-cell contribs-length-0"><div></div></td>
+ <td class="contribs-cell contribs-length-1"><div></div></td>
+ <td class="contribs-cell contribs-length-2"><div></div></td>
+ <td class="contribs-cell contribs-length-3"><div></div></td>
+ <td class="contribs-cell contribs-length-4"><div></div></td>
+ <td class="contribs-cell contribs-length-5"><div></div></td>
+ <td>{{ _("page.profile.contribution_card.legend.more") }}</td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+
{% if total.longest_distance_track %}
<h2>{{ _("page.profile.longest_distance_track") }}</h2>
{{ render_track_card(total.longest_distance_track) }}
diff --git a/fietsboek/trackmap.py b/fietsboek/trackmap.py
new file mode 100644
index 0000000..4cb13aa
--- /dev/null
+++ b/fietsboek/trackmap.py
@@ -0,0 +1,148 @@
+"""Module to render tracks to static images on OSM tiles."""
+
+import io
+import math
+
+from PIL import Image, ImageDraw
+
+from . import geo
+from .config import TileLayerConfig
+from .views.tileproxy import TileRequester
+
+TILE_SIZE = 256
+
+# This is arbitrarily set to provide some image in case a render is requested
+# for a track without points. I've arbitrarily chosen Berlin as the represented
+# area.
+DEFAULT_ZOOM = 9
+DEFAULT_BBOX = (70062, 70611, 42824, 43179)
+
+
+def to_web_mercator(lat: float, lon: float, zoom: int) -> tuple[int, int]:
+ """Convert a pari of latitude/longitude coordinates to web mercator form.
+
+ :param lat: Latitude (in degrees).
+ :param lon: Longitude (in degrees).
+ :param zoom: Zoom level.
+ :return: The web mercator x/y coordinates. Both will be between 0 and
+ 2**zoom * 256.
+ """
+ width = height = TILE_SIZE
+
+ la = math.radians(lon)
+ phi = math.radians(lat)
+
+ x = float(2**zoom) / (2 * math.pi) * width * (la + math.pi)
+ y = (
+ float(2**zoom)
+ / (2 * math.pi)
+ * height
+ * (math.pi - math.log(math.tan((math.pi / 4 + phi / 2))))
+ )
+
+ return (int(math.floor(x)), int(math.floor(y)))
+
+
+class TrackMapRenderer:
+ """A renderer that renders GPX tracks onto small map excerpts."""
+
+ # pylint: disable=too-few-public-methods
+
+ def __init__(
+ self,
+ track: geo.Path,
+ requester: TileRequester,
+ size: tuple[int, int],
+ layer: TileLayerConfig,
+ ):
+ self.track = track
+ self.requester = requester
+ self.size = size
+ self.layer = layer
+ self.maxzoom = layer.zoom
+ self.color = (0, 0, 255)
+ self.line_width = 5
+
+ def render(self) -> Image.Image:
+ """Render the track.
+
+ :return: The image containing the rendered preview.
+ """
+ zoom, bbox = self._find_zoom()
+ image = Image.new("RGB", self.size)
+ start_x, start_y = self._draw_base(image, zoom, bbox)
+ self._draw_lines(image, zoom, start_x, start_y)
+ return image
+
+ def _find_zoom(self) -> tuple[int, tuple[int, int, int, int]]:
+ if not self.track.points:
+ return DEFAULT_ZOOM, DEFAULT_BBOX
+
+ for zoom in range(self.maxzoom or 19, 0, -1):
+ min_x, max_x = 2**zoom * TILE_SIZE, 0
+ min_y, max_y = 2**zoom * TILE_SIZE, 0
+
+ for point in self.track.points:
+ x, y = to_web_mercator(point.latitude, point.longitude, zoom)
+ min_x = min(min_x, x)
+ max_x = max(max_x, x)
+ min_y = min(min_y, y)
+ max_y = max(max_y, y)
+
+ if max_x - min_x > self.size[0] or max_y - min_y > self.size[1]:
+ break
+ else:
+ return zoom, (min_x, max_x, min_y, max_y)
+
+ return DEFAULT_ZOOM, DEFAULT_BBOX
+
+ def _draw_base(self, image, zoom, bbox):
+ min_x, max_x, min_y, max_y = bbox
+ # We center the track by centering its bounding box
+ start_x = min_x - (self.size[0] - (max_x - min_x)) // 2
+ start_y = min_y - (self.size[1] - (max_y - min_y)) // 2
+
+ offset_x = start_x // TILE_SIZE * TILE_SIZE - start_x
+ offset_y = start_y // TILE_SIZE * TILE_SIZE - start_y
+
+ for i in range(int(math.ceil(self.size[0] / TILE_SIZE)) + 1):
+ for j in range(int(math.ceil(self.size[1] / TILE_SIZE)) + 1):
+ tile = self._load_tile(zoom, start_x // TILE_SIZE + i, start_y // TILE_SIZE + j)
+ image.paste(tile, (i * TILE_SIZE + offset_x, j * TILE_SIZE + offset_y))
+
+ return start_x, start_y
+
+ def _load_tile(self, zoom, x, y) -> Image.Image:
+ tile_data = self.requester.load_tile(self.layer, zoom, x, y)
+ if not tile_data:
+ return Image.new("RGB", (TILE_SIZE, TILE_SIZE))
+ return Image.open(io.BytesIO(tile_data))
+
+ def _draw_lines(self, image, zoom, start_x, start_y):
+ coords = (
+ to_web_mercator(point.latitude, point.longitude, zoom) for point in self.track.points
+ )
+ coords = [(x - start_x, y - start_y) for x, y in coords]
+
+ if coords:
+ draw = ImageDraw.Draw(image)
+ draw.line(coords, fill=self.color, width=self.line_width, joint="curve")
+
+
+def render(
+ track: geo.Path,
+ layer: TileLayerConfig,
+ requester: TileRequester,
+ size: tuple[int, int] = (300, 300),
+) -> Image.Image:
+ """Shorthand to construct a :class:`TrackMapRenderer` and render the preview.
+
+ :param track: Track to render.
+ :param layer: The tile layer to take the map tiles from.
+ :param requester: The requester which will be used to request the tiles.
+ :return: The image containing the rendered preview.
+ """
+ return TrackMapRenderer(track, requester, size, layer).render()
+
+
+__all__ = ["to_web_mercator", "TrackMapRenderer", "render"]
diff --git a/fietsboek/transformers/__init__.py b/fietsboek/transformers/__init__.py
index b1a0245..097fbaf 100644
--- a/fietsboek/transformers/__init__.py
+++ b/fietsboek/transformers/__init__.py
@@ -13,11 +13,12 @@ from abc import ABC, abstractmethod
from collections.abc import Mapping
from typing import Literal, NamedTuple, TypeVar
-from gpxpy.gpx import GPX
from pydantic import BaseModel
from pyramid.i18n import TranslationString
from pyramid.request import Request
+from .. import geo
+
_ = TranslationString
T = TypeVar("T", bound="Transformer")
@@ -117,12 +118,12 @@ class Transformer(ABC):
pass
@abstractmethod
- def execute(self, gpx: GPX):
+ def execute(self, path: geo.Path):
"""Run the transformation on the input gpx.
- This is expected to modify the GPX object to represent the new state.
+ This is expected to modify the path to represent the new state.
- :param gpx: The GPX object to transform. Note that this object will be
+ :param path: The path to transform. Note that this object will be
mutated!
"""
diff --git a/fietsboek/transformers/breaks.py b/fietsboek/transformers/breaks.py
index 789fdfd..1072eef 100644
--- a/fietsboek/transformers/breaks.py
+++ b/fietsboek/transformers/breaks.py
@@ -2,9 +2,9 @@
import datetime
-from gpxpy.gpx import GPX, GPXTrack
from pyramid.i18n import TranslationString
+from .. import geo
from . import Parameters, Transformer
_ = TranslationString
@@ -47,34 +47,27 @@ class RemoveBreaks(Transformer):
def parameters(self, value):
pass
- def execute(self, gpx: GPX):
- for track in gpx.tracks:
- self._clean(track)
-
- def _clean(self, track: GPXTrack):
- if not track.get_points_no():
+ def execute(self, path: geo.Path):
+ if not path.points:
return
i = 0
- while i < track.get_points_no():
- segment_idx, point_idx = index(track, i)
- point = track.segments[segment_idx].points[point_idx]
+ while i < len(path.points):
+ point = path.points[i]
# We check if the following points constitute a break, and if yes,
# how many of them
count = 0
current_length = 0.0
last_point = point
- while True:
- try:
- j_segment, j_point = index(track, i + count + 1)
- except IndexError:
- break
- current_point = track.segments[j_segment].points[j_point]
- current_length += last_point.distance_3d(current_point) or 0.0
+ while i + count + 1 < len(path.points):
+ current_point = path.points[i + count + 1]
+ current_length += last_point.distance(current_point) or 0.0
last_point = current_point
- delta_t = datetime.timedelta(seconds=point.time_difference(last_point) or 0.0)
+ delta_t = datetime.timedelta(
+ seconds=last_point.time_offset - point.time_offset or 0.0
+ )
if not delta_t or current_length / delta_t.total_seconds() > STOPPED_SPEED_LIMIT:
break
count += 1
@@ -85,7 +78,7 @@ class RemoveBreaks(Transformer):
continue
# At this point, check if the break is long enough to be removed
- delta_t = datetime.timedelta(seconds=point.time_difference(last_point) or 0.0)
+ delta_t = datetime.timedelta(seconds=last_point.time_offset - point.time_offset or 0.0)
if delta_t < MIN_BREAK_TO_REMOVE:
i += 1
continue
@@ -93,32 +86,12 @@ class RemoveBreaks(Transformer):
# Here, we have a proper break to remove
# Delete the points belonging to the break ...
for _ in range(count):
- j_segment, j_point = index(track, i + 1)
- del track.segments[j_segment].points[j_point]
+ del path.points[i + 1]
# ... and shift the time of the following points
j = i + 1
- while j < track.get_points_no():
- j_segment, j_point = index(track, j)
- track.segments[j_segment].points[j_point].adjust_time(-delta_t)
- j += 1
-
-
-def index(track: GPXTrack, idx: int) -> tuple[int, int]:
- """Takes a one-dimensional index (the point index) and returns an index
- into the segment/segment points.
-
- :raises IndexError: When the given index is out of bounds.
- :param track: The track for which to get the index.
- :param idx: The "1D" index.
- :return: A tuple with the segment index, and the index of the point within
- the segment.
- """
- for segment_idx, segment in enumerate(track.segments):
- if idx < len(segment.points):
- return (segment_idx, idx)
- idx -= len(segment.points)
- raise IndexError
+ for j_point in path.points[j:]:
+ j_point.time_offset -= delta_t.total_seconds()
__all__ = ["RemoveBreaks"]
diff --git a/fietsboek/transformers/elevation.py b/fietsboek/transformers/elevation.py
index e1f7c7c..27683bb 100644
--- a/fietsboek/transformers/elevation.py
+++ b/fietsboek/transformers/elevation.py
@@ -3,9 +3,9 @@
from collections.abc import Callable, Iterable
from itertools import islice, zip_longest
-from gpxpy.gpx import GPX, GPXTrackPoint
from pyramid.i18n import TranslationString
+from .. import geo
from . import Parameters, Transformer
_ = TranslationString
@@ -13,7 +13,7 @@ _ = TranslationString
MAX_ORGANIC_SLOPE: float = 1.0
-def slope(point_a: GPXTrackPoint, point_b: GPXTrackPoint) -> float:
+def slope(point_a: geo.Point, point_b: geo.Point) -> float:
"""Returns the slope between two GPX points.
This is defined as delta_h / euclid_distance.
@@ -25,7 +25,7 @@ def slope(point_a: GPXTrackPoint, point_b: GPXTrackPoint) -> float:
if point_a.elevation is None or point_b.elevation is None:
return 0.0
delta_h = abs(point_a.elevation - point_b.elevation)
- dist = point_a.distance_2d(point_b)
+ dist = point_a.flat_distance(point_b)
if dist == 0.0 or dist is None:
return 0.0
return delta_h / dist
@@ -58,19 +58,12 @@ class FixNullElevation(Transformer):
def parameters(self, value):
pass
- def execute(self, gpx: GPX):
+ def execute(self, path: geo.Path):
def all_points():
- return gpx.walk(only_points=True)
+ return iter(path.points)
def rev_points():
- # We cannot use reversed(gpx.walk(...)) since that is not a
- # generator, so we do it manually.
- return (
- point
- for track in reversed(gpx.tracks)
- for segment in reversed(track.segments)
- for point in reversed(segment.points)
- )
+ return reversed(path.points)
# First, from the front
self.fixup(all_points)
@@ -78,7 +71,7 @@ class FixNullElevation(Transformer):
self.fixup(rev_points)
@classmethod
- def fixup(cls, points: Callable[[], Iterable[GPXTrackPoint]]):
+ def fixup(cls, points: Callable[[], Iterable[geo.Point]]):
"""Fixes the given GPX points.
This iterates over the points and checks for the first point that has a
@@ -131,17 +124,16 @@ class FixElevationJumps(Transformer):
def parameters(self, value):
pass
- def execute(self, gpx: GPX):
+ def execute(self, path: geo.Path):
current_adjustment = 0.0
- points = gpx.walk(only_points=True)
- next_points = gpx.walk(only_points=True)
+ points = iter(path.points)
+ next_points = iter(path.points)
for current_point, next_point in zip_longest(points, islice(next_points, 1, None)):
point_adjustment = current_adjustment
if next_point and slope(current_point, next_point) > MAX_ORGANIC_SLOPE:
current_adjustment += current_point.elevation - next_point.elevation
- print(f"{current_adjustment=}")
current_point.elevation += point_adjustment
diff --git a/fietsboek/updater/__init__.py b/fietsboek/updater/__init__.py
index 42e40f4..4d3b0ee 100644
--- a/fietsboek/updater/__init__.py
+++ b/fietsboek/updater/__init__.py
@@ -331,6 +331,27 @@ class Updater:
return UpdateState.OUTDATED
return state
+ def check_connectivity(self) -> str | None:
+ """Checks whether the data directory and the SQL server accessible.
+
+ Returns ``None`` if there are no problems, or a string describing the
+ error.
+
+ :return: Whether there is a connection error.
+ """
+ data_dir = Path(self.settings["fietsboek.data_dir"])
+ if not data_dir.exists():
+ return "data directory does not exist"
+
+ engine = sqlalchemy.create_engine(self.settings["sqlalchemy.url"])
+ try:
+ with engine.connect():
+ pass
+ except sqlalchemy.exc.OperationalError as exc:
+ return f"could not connect to database\n\n{exc}"
+
+ return None
+
class UpdateScript:
"""Represents an update script."""
diff --git a/fietsboek/updater/cli.py b/fietsboek/updater/cli.py
index 9b7d92e..271e7a1 100644
--- a/fietsboek/updater/cli.py
+++ b/fietsboek/updater/cli.py
@@ -8,6 +8,7 @@ migrating the configuration.
"""
import logging.config
+import sys
import click
@@ -32,6 +33,21 @@ def user_confirm(verb):
click.confirm("Proceed?", abort=True)
+def check_connectivity(updater: Updater):
+ """Makes sure that the updater can connect to the database.
+
+ Aborts the program if not.
+
+ :param updater: The updater.
+ """
+ error = updater.check_connectivity()
+ if error is None:
+ return
+ click.secho("Error: ", fg="red", nl=False)
+ click.echo(error)
+ sys.exit(1)
+
+
@click.group(
help=__doc__,
context_settings={"help_option_names": ["-h", "--help"]},
@@ -62,6 +78,7 @@ def update(ctx, config, version, force):
logging.config.fileConfig(config)
updater = Updater(config)
updater.load()
+ check_connectivity(updater)
if version and not updater.exists(version):
ctx.fail(f"Version {version!r} not found")
@@ -107,6 +124,7 @@ def downgrade(ctx, config, version, force):
logging.config.fileConfig(config)
updater = Updater(config)
updater.load()
+ check_connectivity(updater)
if version and not updater.exists(version):
ctx.fail(f"Version {version!r} not found")
@@ -158,6 +176,7 @@ def status(config):
logging.config.fileConfig(config)
updater = Updater(config)
updater.load()
+ check_connectivity(updater)
current = updater.current_versions()
heads = updater.heads()
click.secho("Current versions:", fg="yellow")
diff --git a/fietsboek/updater/scripts/upd_20230103_lu8w3rwlz4ddcpms.py b/fietsboek/updater/scripts/upd_20230103_lu8w3rwlz4ddcpms.py
index 4362c2d..10c42d0 100644
--- a/fietsboek/updater/scripts/upd_20230103_lu8w3rwlz4ddcpms.py
+++ b/fietsboek/updater/scripts/upd_20230103_lu8w3rwlz4ddcpms.py
@@ -29,42 +29,42 @@ alembic_revision = 'c939800af428'
class Up(UpdateScript):
def pre_alembic(self, config):
engine = create_engine(config["sqlalchemy.url"])
- connection = engine.connect()
data_dir = Path(config["fietsboek.data_dir"])
- sql = (
- "SELECT tracks.id, tracks.title, tracks.description, tracks.date_raw, "
- "tracks.date_tz, users.name "
- "FROM tracks, users "
- "WHERE tracks.owner_id = users.id;"
- )
- for row in connection.execute(text(sql)):
- track_id, title, description, date_raw, date_tz, author_name = row
- if isinstance(date_raw, str):
- date_raw = datetime.datetime.strptime(date_raw, "%Y-%m-%d %H:%M:%S.%f")
- if date_tz is None:
- timezone = datetime.timezone.utc
- else:
- timezone = datetime.timezone(datetime.timedelta(minutes=date_tz))
- date = date_raw.replace(tzinfo=timezone)
-
- self.tell(f"Embedding metadata for track {track_id}")
- track_dir = data_dir / "tracks" / str(track_id)
- gpx_path = track_dir / "track.gpx.br"
-
- raw_gpx = brotli.decompress(gpx_path.read_bytes())
- gpx = gpxpy.parse(raw_gpx)
-
- for track in gpx.tracks:
- track.name = None
- track.description = None
-
- gpx.author_name = author_name
- gpx.name = title
- gpx.description = description
- gpx.time = date
-
- gpx_path.write_bytes(brotli.compress(gpx.to_xml().encode("utf-8"), quality=4))
+ with engine.connect() as connection:
+ sql = (
+ "SELECT tracks.id, tracks.title, tracks.description, tracks.date_raw, "
+ "tracks.date_tz, users.name "
+ "FROM tracks, users "
+ "WHERE tracks.owner_id = users.id;"
+ )
+ for row in connection.execute(text(sql)):
+ track_id, title, description, date_raw, date_tz, author_name = row
+ if isinstance(date_raw, str):
+ date_raw = datetime.datetime.strptime(date_raw, "%Y-%m-%d %H:%M:%S.%f")
+ if date_tz is None:
+ timezone = datetime.timezone.utc
+ else:
+ timezone = datetime.timezone(datetime.timedelta(minutes=date_tz))
+ date = date_raw.replace(tzinfo=timezone)
+
+ self.tell(f"Embedding metadata for track {track_id}")
+ track_dir = data_dir / "tracks" / str(track_id)
+ gpx_path = track_dir / "track.gpx.br"
+
+ raw_gpx = brotli.decompress(gpx_path.read_bytes())
+ gpx = gpxpy.parse(raw_gpx)
+
+ for track in gpx.tracks:
+ track.name = None
+ track.description = None
+
+ gpx.author_name = author_name
+ gpx.name = title
+ gpx.description = description
+ gpx.time = date
+
+ gpx_path.write_bytes(brotli.compress(gpx.to_xml().encode("utf-8"), quality=4))
def post_alembic(self, config):
pass
diff --git a/fietsboek/updater/scripts/upd_20250618_v0.11.0.py b/fietsboek/updater/scripts/upd_20250618_v0.11.0.py
new file mode 100644
index 0000000..11837f7
--- /dev/null
+++ b/fietsboek/updater/scripts/upd_20250618_v0.11.0.py
@@ -0,0 +1,27 @@
+"""Revision upgrade script v0.11.0
+
+Date created: 2025-06-18 13:14:07.849714
+"""
+from fietsboek.updater.script import UpdateScript
+
+update_id = 'v0.11.0'
+previous = [
+ 'v0.10.0',
+]
+alembic_revision = '2ebe1bf66430'
+
+
+class Up(UpdateScript):
+ def pre_alembic(self, config):
+ pass
+
+ def post_alembic(self, config):
+ pass
+
+
+class Down(UpdateScript):
+ def pre_alembic(self, config):
+ pass
+
+ def post_alembic(self, config):
+ pass
diff --git a/fietsboek/updater/scripts/upd_20251109_nm561argcq1s8w27.py b/fietsboek/updater/scripts/upd_20251109_nm561argcq1s8w27.py
new file mode 100644
index 0000000..ae6c29a
--- /dev/null
+++ b/fietsboek/updater/scripts/upd_20251109_nm561argcq1s8w27.py
@@ -0,0 +1,163 @@
+"""Revision upgrade script nm561argcq1s8w27
+
+This script moves data from the GPX files in the data directory to the SQL
+database.
+
+Date created: 2025-11-09 18:27:48.493007
+"""
+import datetime
+import logging
+import shutil
+from pathlib import Path
+
+import brotli
+import gpxpy
+from sqlalchemy import create_engine
+from sqlalchemy.sql import text
+
+from fietsboek import convert
+from fietsboek.updater.script import UpdateScript
+
+LOGGER = logging.getLogger(__name__)
+
+update_id = 'nm561argcq1s8w27'
+previous = [
+ 'v0.11.0',
+]
+alembic_revision = '90b39fdf6e4b'
+
+
+class Up(UpdateScript):
+ def pre_alembic(self, config):
+ pass
+
+ def post_alembic(self, config):
+ engine = create_engine(config["sqlalchemy.url"])
+ connection = engine.connect()
+ data_dir = Path(config["fietsboek.data_dir"])
+
+ with engine.connect() as connection:
+ # This can happen in a fresh instance
+ if not (data_dir / "tracks").exists():
+ return
+
+ for track_dir in (data_dir / "tracks").iterdir():
+ track_id = int(track_dir.name)
+ self.tell(f"Loading track {track_id}")
+
+ gpx_path = track_dir / "track.gpx.br"
+
+ # We're careful here, in case a previous update was interrupted
+ if not gpx_path.exists():
+ continue
+
+ gpx_bytes = brotli.decompress(gpx_path.read_bytes())
+
+ track = convert.smart_convert(gpx_bytes)
+ with connection.begin():
+ connection.execute(
+ text("DELETE FROM track_points WHERE track_id = :id;"),
+ {"id": track_id},
+ )
+ connection.execute(
+ text("DELETE FROM waypoints WHERE track_id = :id;"),
+ {"id": track_id},
+ )
+ for index, point in enumerate(track.path().points):
+ connection.execute(
+ text("""INSERT INTO track_points (
+ track_id, "index", longitude, latitude, elevation, time_offset
+ ) VALUES (
+ :track_id, :index, :longitude, :latitude, :elevation, :time_offset
+ );"""),
+ {
+ "track_id": track_id,
+ "index": index,
+ "longitude": point.longitude,
+ "latitude": point.latitude,
+ "elevation": point.elevation,
+ "time_offset": point.time_offset,
+ },
+ )
+ for waypoint in track.waypoints:
+ connection.execute(
+ text("""INSERT INTO waypoints (
+ track_id, longitude, latitude, elevation, name, description
+ ) VALUES (
+ :track_id, :longitude, :latitude, :elevation, :name, :description
+ );"""),
+ {
+ "track_id": track_id,
+ "longitude": waypoint.longitude,
+ "latitude": waypoint.latitude,
+ "elevation": waypoint.elevation,
+ "name": waypoint.name,
+ "description": waypoint.description,
+ },
+ )
+
+ gpx_path.unlink()
+ shutil.move(
+ track_dir / "track.bck.gpx.br",
+ track_dir / "track.bck.br",
+ )
+
+
+class Down(UpdateScript):
+ def pre_alembic(self, config):
+ engine = create_engine(config["sqlalchemy.url"])
+ data_dir = Path(config["fietsboek.data_dir"])
+
+ query = text("SELECT id, title, description, date_raw FROM tracks;")
+
+ with engine.connect() as connection:
+ for row in connection.execute(query):
+ gpx = gpxpy.gpx.GPX()
+ gpx.description = row.description
+ gpx.name = row.title
+
+ start_date = row.date_raw
+ if isinstance(start_date, str):
+ start_date = datetime.datetime.fromisoformat(start_date)
+
+ segment = gpxpy.gpx.GPXTrackSegment()
+ points_query = text("""
+ SELECT longitude, latitude, elevation, time_offset
+ FROM track_points WHERE track_id = :track_id ORDER BY "index";
+ """)
+ for point in connection.execute(points_query, {"track_id": row.id}):
+ segment.points.append(
+ gpxpy.gpx.GPXTrackPoint(
+ latitude=point.latitude,
+ longitude=point.longitude,
+ elevation=point.elevation,
+ time=start_date + datetime.timedelta(seconds=point.time_offset),
+ )
+ )
+ track = gpxpy.gpx.GPXTrack()
+ track.segments.append(segment)
+ gpx.tracks.append(track)
+
+ waypoints_query = text("""
+ SELECT longitude, latitude, elevation, name, description
+ FROM waypoints WHERE track_id = :track_id;
+ """)
+ for wpt in connection.execute(waypoints_query, {"track_id": row.id}):
+ gpx.waypoints.append(
+ gpxpy.gpx.GPXWaypoint(
+ longitude=wpt.longitude,
+ latitude=wpt.latitude,
+ elevation=wpt.elevation,
+ name=wpt.name,
+ comment=wpt.description,
+ description=wpt.description,
+ )
+ )
+
+ xml_data = gpx.to_xml(prettyprint=False).encode("utf-8")
+ track_dir = data_dir / "tracks" / str(row.id)
+ (track_dir / "track.gpx.br").write_bytes(brotli.compress(xml_data))
+ shutil.move(track_dir / "track.bck.br", track_dir / "track.bck.gpx.br")
+
+ def post_alembic(self, config):
+ pass
diff --git a/fietsboek/updater/scripts/upd_20260103_v0.12.0.py b/fietsboek/updater/scripts/upd_20260103_v0.12.0.py
new file mode 100644
index 0000000..697421e
--- /dev/null
+++ b/fietsboek/updater/scripts/upd_20260103_v0.12.0.py
@@ -0,0 +1,27 @@
+"""Revision upgrade script v0.12.0
+
+Date created: 2026-01-03 20:38:01.341899
+"""
+from fietsboek.updater.script import UpdateScript
+
+update_id = 'v0.12.0'
+previous = [
+ 'nm561argcq1s8w27',
+]
+alembic_revision = 'f9ca03541351'
+
+
+class Up(UpdateScript):
+ def pre_alembic(self, config):
+ pass
+
+ def post_alembic(self, config):
+ pass
+
+
+class Down(UpdateScript):
+ def pre_alembic(self, config):
+ pass
+
+ def post_alembic(self, config):
+ pass
diff --git a/fietsboek/updater/scripts/upd_20260103_v0.12.1.py b/fietsboek/updater/scripts/upd_20260103_v0.12.1.py
new file mode 100644
index 0000000..2c46c24
--- /dev/null
+++ b/fietsboek/updater/scripts/upd_20260103_v0.12.1.py
@@ -0,0 +1,27 @@
+"""Revision upgrade script v0.12.1
+
+Date created: 2026-01-03 23:48:08.321881
+"""
+from fietsboek.updater.script import UpdateScript
+
+update_id = 'v0.12.1'
+previous = [
+ 'v0.12.0',
+]
+alembic_revision = 'f9ca03541351'
+
+
+class Up(UpdateScript):
+ def pre_alembic(self, config):
+ pass
+
+ def post_alembic(self, config):
+ pass
+
+
+class Down(UpdateScript):
+ def pre_alembic(self, config):
+ pass
+
+ def post_alembic(self, config):
+ pass
diff --git a/fietsboek/updater/scripts/upd_30ppwg8zi4ujb46f.py b/fietsboek/updater/scripts/upd_30ppwg8zi4ujb46f.py
index e900c7a..cdc09f6 100644
--- a/fietsboek/updater/scripts/upd_30ppwg8zi4ujb46f.py
+++ b/fietsboek/updater/scripts/upd_30ppwg8zi4ujb46f.py
@@ -25,18 +25,18 @@ alembic_revision = 'c939800af428'
class Up(UpdateScript):
def pre_alembic(self, config):
engine = create_engine(config["sqlalchemy.url"])
- connection = engine.connect()
- data_dir = Path(config["fietsboek.data_dir"])
+ with engine.connect() as connection:
+ data_dir = Path(config["fietsboek.data_dir"])
- for row in connection.execute(text("SELECT id, gpx FROM tracks;")):
- self.tell(f"Moving GPX data for track {row.id} from database to disk")
- track_dir = data_dir / "tracks" / str(row.id)
- track_dir.mkdir(parents=True, exist_ok=True)
+ for row in connection.execute(text("SELECT id, gpx FROM tracks;")):
+ self.tell(f"Moving GPX data for track {row.id} from database to disk")
+ track_dir = data_dir / "tracks" / str(row.id)
+ track_dir.mkdir(parents=True, exist_ok=True)
- raw_gpx = gzip.decompress(row.gpx)
- gpx_path = track_dir / "track.gpx.br"
- gpx_path.write_bytes(brotli.compress(raw_gpx, quality=5))
- shutil.copy(gpx_path, track_dir / "track.bck.gpx.br")
+ raw_gpx = gzip.decompress(row.gpx)
+ gpx_path = track_dir / "track.gpx.br"
+ gpx_path.write_bytes(brotli.compress(raw_gpx, quality=5))
+ shutil.copy(gpx_path, track_dir / "track.bck.gpx.br")
def post_alembic(self, config):
pass
@@ -48,18 +48,18 @@ class Down(UpdateScript):
def post_alembic(self, config):
engine = create_engine(config["sqlalchemy.url"])
- connection = engine.connect()
- data_dir = Path(config["fietsboek.data_dir"])
+ with engine.connect() as connection:
+ data_dir = Path(config["fietsboek.data_dir"])
- for track_path in (data_dir / "tracks").iterdir():
- track_id = int(track_path.name)
- self.tell(f"Moving GPX data for track {track_id} from disk to database")
- brotli_data = (track_path / "track.gpx.br").read_bytes()
- gzip_data = gzip.compress(brotli.decompress(brotli_data))
- connection.execute(
- text("UPDATE tracks SET gpx = :gpx WHERE id = :id;"),
- gpx=gzip_data, id=track_id
- )
+ for track_path in (data_dir / "tracks").iterdir():
+ track_id = int(track_path.name)
+ self.tell(f"Moving GPX data for track {track_id} from disk to database")
+ brotli_data = (track_path / "track.gpx.br").read_bytes()
+ gzip_data = gzip.compress(brotli.decompress(brotli_data))
+ connection.execute(
+ text("UPDATE tracks SET gpx = :gpx WHERE id = :id;"),
+ gpx=gzip_data, id=track_id
+ )
- (track_path / "track.gpx.br").unlink()
- (track_path / "track.bck.gpx.br").unlink(missing_ok=True)
+ (track_path / "track.gpx.br").unlink()
+ (track_path / "track.bck.gpx.br").unlink(missing_ok=True)
diff --git a/fietsboek/util.py b/fietsboek/util.py
index 9284ce2..ecb3f43 100644
--- a/fietsboek/util.py
+++ b/fietsboek/util.py
@@ -1,13 +1,13 @@
"""Various utility functions."""
import datetime
-import html
import importlib.resources
import os
import re
import secrets
import unicodedata
-from typing import Optional, TypeVar, Union
+from pathlib import Path
+from typing import Optional, TypeVar
import babel
import gpxpy
@@ -63,6 +63,9 @@ _windows_device_files = (
)
+_valid_xml_chars = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,-: ")
+
+
def safe_markdown(md_source: str) -> Markup:
"""Transform a markdown document into a safe HTML document.
@@ -172,43 +175,6 @@ def guess_gpx_timezone(gpx: gpxpy.gpx.GPX) -> datetime.tzinfo:
return datetime.timezone.utc
-def tour_metadata(gpx_data: Union[str, bytes, gpxpy.gpx.GPX]) -> dict:
- """Calculate the metadata of the tour.
-
- Returns a dict with ``length``, ``uphill``, ``downhill``, ``moving_time``,
- ``stopped_time``, ``max_speed``, ``avg_speed``, ``start_time`` and
- ``end_time``.
-
- :param gpx_data: The GPX data of the tour. Can be pre-parsed to save time.
- :return: A dictionary with the computed values.
- """
- if isinstance(gpx_data, bytes):
- gpx_data = gpx_data.decode("utf-8")
- if isinstance(gpx_data, gpxpy.gpx.GPX):
- gpx = gpx_data
- else:
- gpx = gpxpy.parse(gpx_data)
- timezone = guess_gpx_timezone(gpx)
- uphill, downhill = gpx.get_uphill_downhill()
- moving_data = gpx.get_moving_data()
- time_bounds = gpx.get_time_bounds()
- try:
- avg_speed = moving_data.moving_distance / moving_data.moving_time
- except ZeroDivisionError:
- avg_speed = 0.0
- return {
- "length": gpx.length_3d(),
- "uphill": uphill,
- "downhill": downhill,
- "moving_time": moving_data.moving_time,
- "stopped_time": moving_data.stopped_time,
- "max_speed": moving_data.max_speed,
- "avg_speed": avg_speed,
- "start_time": (time_bounds.start_time or DEFAULT_START_TIME).astimezone(timezone),
- "end_time": (time_bounds.end_time or DEFAULT_END_TIME).astimezone(timezone),
- }
-
-
def mps_to_kph(mps: float) -> float:
"""Converts meters/second to kilometers/hour.
@@ -442,22 +408,6 @@ def tile_url(request: Request, route_name: str, **kwargs: str) -> str:
return route.replace("__x__", "{x}").replace("__y__", "{y}").replace("__z__", "{z}")
-def encode_gpx(gpx: gpxpy.gpx.GPX) -> bytes:
- """Encodes a GPX in-memory representation to the XML representation.
-
- This ensures that the resulting XML file is valid.
-
- Returns the contents of the XML as bytes, ready to be written to disk.
-
- :param gpx: The GPX file to encode. Might be modified!
- :return: The encoded GPX content.
- """
- for track in gpx.tracks:
- if track.link:
- track.link = html.escape(track.link)
- return gpx.to_xml().encode("utf-8")
-
-
def secure_filename(filename: str) -> str:
r"""Pass it a filename and it will return a secure version of it. This
filename can then safely be stored on a regular file system and passed
@@ -504,6 +454,36 @@ def secure_filename(filename: str) -> str:
return filename
+def recursive_size(path: Path) -> int:
+ """Recursively determines the size of the given directory.
+
+ :param path: The directory.
+ :return: The combined size, in bytes.
+ """
+ size = 0
+ for root, _folders, files in os.walk(path):
+ size += sum(os.path.getsize(os.path.join(root, fname)) for fname in files)
+ return size
+
+
+def xml_escape(value: str) -> bytes:
+ """Escapes and encodes a string to be embedded in a XML document.
+
+ This replaces characters like < and > with their entities.
+
+ :param value: The value.
+ :return: The escaped and encoded string.
+ """
+ return b"".join(
+ (
+ char.encode("ascii")
+ if char in _valid_xml_chars
+ else b"&#x%s;" % hex(ord(char))[2:].encode("ascii")
+ )
+ for char in value
+ )
+
+
__all__ = [
"ALLOWED_TAGS",
"ALLOWED_ATTRIBUTES",
@@ -515,7 +495,6 @@ __all__ = [
"round_timedelta_to_multiple",
"round_to_seconds",
"guess_gpx_timezone",
- "tour_metadata",
"mps_to_kph",
"human_size",
"month_name",
@@ -527,6 +506,7 @@ __all__ = [
"locale_display_name",
"list_locales",
"tile_url",
- "encode_gpx",
"secure_filename",
+ "recursive_size",
+ "xml_escape",
]
diff --git a/fietsboek/views/admin.py b/fietsboek/views/admin.py
index d078794..f0aa271 100644
--- a/fietsboek/views/admin.py
+++ b/fietsboek/views/admin.py
@@ -1,21 +1,161 @@
"""Admin views."""
+import datetime
+import platform
+import stat
+from dataclasses import dataclass
+from pathlib import Path
+
from pyramid.httpexceptions import HTTPFound
from pyramid.i18n import TranslationString as _
+from pyramid.request import Request
from pyramid.view import view_config
-from sqlalchemy import select
+from sqlalchemy import func, select, text
+
+from .. import models, util
+from ..data import DataManager
+
+GOOD_CRON_THRESHOLD = datetime.timedelta(hours=1)
-from .. import models
+
+def _safe_size(path: Path) -> int:
+ try:
+ res = path.stat()
+ if stat.S_ISDIR(res.st_mode):
+ return util.recursive_size(path)
+ if stat.S_ISREG(res.st_mode):
+ return res.st_size
+ return 0
+ except FileNotFoundError:
+ return 0
+
+
+@dataclass
+class SizeBreakdown:
+ """A breakdown of what objects take how much storage."""
+
+ track_data: int = 0
+ backups: int = 0
+ image_files: int = 0
+ track_previews: int = 0
+ journey_previews: int = 0
+ user_maps: int = 0
+
+
+def _get_size_breakdown(dbsession, data_manager: DataManager):
+ breakdown = SizeBreakdown()
+
+ dialect = dbsession.bind.dialect.name
+ if dialect == "sqlite":
+ query = text("""SELECT SUM("pgsize") FROM "dbstat" WHERE name='track_points';""")
+ result = dbsession.execute(query).scalar_one()
+ breakdown.track_data += result
+ elif dialect == "postgresql":
+ query = text("""SELECT pg_relation_size('track_points');""")
+ result = dbsession.execute(query).scalar_one()
+ breakdown.track_data += result
+
+ for track_id in data_manager.list_tracks():
+ track = data_manager.open(track_id)
+ breakdown.backups += _safe_size(track.backup_path())
+ breakdown.track_previews += _safe_size(track.preview_path())
+ for image_id in track.images():
+ breakdown.image_files += _safe_size(track.image_path(image_id))
+
+ for user_id in data_manager.list_users():
+ user = data_manager.open_user(user_id)
+ breakdown.user_maps += _safe_size(user.heatmap_path())
+ breakdown.user_maps += _safe_size(user.tilehunt_path())
+
+ for journey_id in data_manager.list_journeys():
+ journey = data_manager.open_journey(journey_id)
+ breakdown.journey_previews += _safe_size(journey.preview_path())
+
+ return breakdown
+
+
+def _get_db_size(dbsession):
+ dialect = dbsession.bind.dialect.name
+ if dialect == "sqlite":
+ query = text(
+ """SELECT page_size * page_count FROM pragma_page_count(), pragma_page_size();"""
+ )
+ result = dbsession.execute(query).scalar_one()
+ return result
+ if dialect == "postgresql":
+ database_name = dbsession.bind.url.database
+ query = text(f"""SELECT pg_database_size('{database_name}');""")
+ result = dbsession.execute(query).scalar_one()
+ return result
+ return 0
+
+
+def _get_fietsboek_version():
+ # pylint: disable=import-outside-toplevel
+ from fietsboek import __VERSION__
+
+ return __VERSION__
@view_config(
route_name="admin",
- renderer="fietsboek:templates/admin.jinja2",
+ renderer="fietsboek:templates/admin_overview.jinja2",
+ request_method="GET",
+ permission="admin",
+)
+def admin(request: Request):
+ """Renders the admin overview.
+
+ :param request: The Pyramid request.
+ :return: The HTTP response.
+ """
+ # False-positive with func.count()
+ # pylint: disable=not-callable
+ user_count = request.dbsession.execute(select(func.count()).select_from(models.User)).scalar()
+ track_count = request.dbsession.execute(select(func.count()).select_from(models.Track)).scalar()
+ size_total = request.data_manager.size() + _get_db_size(request.dbsession)
+ size_breakdown = _get_size_breakdown(request.dbsession, request.data_manager)
+
+ try:
+ distro = platform.freedesktop_os_release()["PRETTY_NAME"]
+ except OSError:
+ distro = None
+
+ try:
+ last_cronjob_timestamp = float(request.redis.get("last-cronjob"))
+ except (TypeError, ValueError):
+ last_cronjob = None
+ cron_good = False
+ else:
+ last_cronjob = datetime.datetime.fromtimestamp(last_cronjob_timestamp, datetime.UTC)
+ cron_good = (datetime.datetime.now(datetime.UTC) - last_cronjob) < GOOD_CRON_THRESHOLD
+
+ versions = {
+ "fietsboek": _get_fietsboek_version(),
+ "python": platform.python_version(),
+ "linux": platform.platform(),
+ "distro": distro,
+ }
+
+ return {
+ "user_count": user_count,
+ "track_count": track_count,
+ "total_size": size_total,
+ "size_breakdown": size_breakdown,
+ "versions": versions,
+ "last_cronjob": last_cronjob,
+ "cron_good": cron_good,
+ }
+
+
+@view_config(
+ route_name="admin-badge",
+ renderer="fietsboek:templates/admin_badges.jinja2",
request_method="GET",
permission="admin",
)
-def admin(request):
- """Renders the main admin overview.
+def admin_badges(request):
+ """Renders the badges editor.
:param request: The Pyramid request.
:type request: pyramid.request.Request
@@ -47,7 +187,7 @@ def do_badge_add(request):
request.dbsession.add(badge)
request.session.flash(request.localizer.translate(_("flash.badge_added")))
- return HTTPFound(request.route_url("admin"))
+ return HTTPFound(request.route_url("admin-badge"))
@view_config(route_name="admin-badge-edit", permission="admin", request_method="POST")
@@ -71,7 +211,7 @@ def do_badge_edit(request):
badge.title = request.params["badge-title"]
request.session.flash(request.localizer.translate(_("flash.badge_modified")))
- return HTTPFound(request.route_url("admin"))
+ return HTTPFound(request.route_url("admin-badge"))
@view_config(route_name="admin-badge-delete", permission="admin", request_method="POST")
@@ -91,7 +231,7 @@ def do_badge_delete(request):
request.dbsession.delete(badge)
request.session.flash(request.localizer.translate(_("flash.badge_deleted")))
- return HTTPFound(request.route_url("admin"))
+ return HTTPFound(request.route_url("admin-badge"))
-__all__ = ["admin", "do_badge_add", "do_badge_edit", "do_badge_delete"]
+__all__ = ["admin", "admin_badges", "do_badge_add", "do_badge_edit", "do_badge_delete"]
diff --git a/fietsboek/views/browse.py b/fietsboek/views/browse.py
index 97bee35..490b9b4 100644
--- a/fietsboek/views/browse.py
+++ b/fietsboek/views/browse.py
@@ -1,10 +1,12 @@
"""Views for browsing all tracks."""
import datetime
+import json
+import urllib.parse
from collections.abc import Callable, Iterable
from enum import Enum
from io import RawIOBase
-from typing import TypeVar
+from typing import Iterator, TypeVar
from zipfile import ZIP_DEFLATED, ZipFile
from pyramid.httpexceptions import HTTPBadRequest, HTTPForbidden, HTTPNotFound
@@ -12,12 +14,13 @@ from pyramid.request import Request
from pyramid.response import Response
from pyramid.view import view_config
from sqlalchemy import func, not_, or_, select
-from sqlalchemy.orm import aliased
+from sqlalchemy.orm import Session, aliased
from sqlalchemy.sql import Select
from .. import models, util
from ..models.track import TrackType, TrackWithMetadata
+TRACKS_PER_PAGE = 20
T = TypeVar("T", bound=Enum)
AliasedTrack = type[models.Track]
@@ -69,6 +72,14 @@ def _get_enum(enum: type[T], value: str) -> T:
raise HTTPBadRequest(f"Invalid enum value {value!r}") from exc
+def _with_page(url: str, num: int) -> str:
+ parsed = urllib.parse.urlparse(url)
+ query = urllib.parse.parse_qs(parsed.query)
+ query["page"] = [str(num)]
+ new_qs = urllib.parse.urlencode(query, doseq=True)
+ return urllib.parse.urlunparse(parsed._replace(query=new_qs))
+
+
class ResultOrder(Enum):
"""Enum representing the different ways in which the tracks can be sorted
in the result."""
@@ -79,6 +90,8 @@ class ResultOrder(Enum):
LENGTH_DESC = "length-desc"
DURATION_ASC = "duration-asc"
DURATION_DESC = "duration-desc"
+ UPHILL_ASC = "uphill-asc"
+ UPHILL_DESC = "uphill-desc"
class Filter:
@@ -284,7 +297,7 @@ class FilterCollection(Filter):
:param request: The request.
:return: The parsed filter.
"""
- # pylint: disable=singleton-comparison
+ # pylint: disable=singleton-comparison,too-many-branches
filters: list[Filter] = []
if request.params.get("search-terms"):
term = request.params.get("search-terms").strip()
@@ -329,6 +342,34 @@ class FilterCollection(Filter):
)
)
+ if request.params.get("min-uphill"):
+ min_uphill = _get_int(request, "min-uphill")
+ filters.append(
+ LambdaFilter(
+ lambda query, track: query.where(
+ or_(
+ models.TrackCache.uphill >= min_uphill,
+ models.TrackCache.uphill == None, # noqa: E711
+ )
+ ),
+ lambda track: track.uphill >= min_uphill,
+ )
+ )
+
+ if request.params.get("max-uphill"):
+ max_uphill = _get_int(request, "max-uphill")
+ filters.append(
+ LambdaFilter(
+ lambda query, track: query.where(
+ or_(
+ models.TrackCache.uphill <= max_uphill,
+ models.TrackCache.uphill == None, # noqa: E711
+ )
+ ),
+ lambda track: track.uphill <= max_uphill,
+ )
+ )
+
if request.params.get("min-date"):
min_date = _get_date(request, "min-date")
min_date = datetime.datetime.combine(min_date, datetime.time.min)
@@ -404,6 +445,10 @@ def apply_order(query: Select, track: AliasedTrack, order: ResultOrder) -> Selec
query = query.order_by(models.TrackCache.length.desc())
elif order == ResultOrder.LENGTH_ASC:
query = query.order_by(models.TrackCache.length.asc())
+ elif order == ResultOrder.UPHILL_DESC:
+ query = query.order_by(models.TrackCache.uphill.desc())
+ elif order == ResultOrder.UPHILL_ASC:
+ query = query.order_by(models.TrackCache.uphill.asc())
elif order == ResultOrder.DURATION_DESC:
query = query.order_by(
(models.TrackCache.moving_time + models.TrackCache.stopped_time).desc()
@@ -415,6 +460,47 @@ def apply_order(query: Select, track: AliasedTrack, order: ResultOrder) -> Selec
return query
+def paginate(
+ dbsession: Session,
+ query: Select,
+ filters: Filter,
+ start: int,
+ num: int,
+) -> Iterator[TrackWithMetadata]:
+ """Paginates a query.
+
+ Unlike a simple OFFSET/LIMIT solution, this generator will request more
+ elements if the filters end up throwing tracks out.
+
+ :param dbsession: The current database session.
+ :param query: The (filtered and ordered) query.
+ :param filters: The filters to apply after retrieving elements from the
+ database.
+ :param track: The aliased ``Track`` class.
+ :param start: The offset from which to start the pagination.
+ :param num: How many items to retrieve at maximum.
+ :return: An iterable over ``num`` tracks (or fewer).
+ """
+ # pylint: disable=too-many-arguments,too-many-positional-arguments
+ num_retrieved = 0
+ offset = start
+ while num_retrieved < num:
+ # Best to try and get all at once
+ num_query = num - num_retrieved
+ this_query = query.offset(offset).limit(num_query)
+ offset += num_query
+
+ tracks = list(dbsession.execute(this_query).scalars())
+ if not tracks:
+ break
+
+ for track in tracks:
+ track = TrackWithMetadata(track)
+ if filters.apply(track):
+ num_retrieved += 1
+ yield track
+
+
@view_config(
route_name="browse", renderer="fietsboek:templates/browse.jinja2", request_method="GET"
)
@@ -431,18 +517,45 @@ def browse(request: Request) -> Response:
query = select(track).join(models.TrackCache, isouter=True)
query = filters.compile(query, track)
+ if "page" in request.params:
+ page = _get_int(request, "page")
+ else:
+ page = 1
+
order = ResultOrder.DATE_DESC
if request.params.get("sort"):
order = _get_enum(ResultOrder, request.params.get("sort"))
query = apply_order(query, track, order)
- tracks = request.dbsession.execute(query).scalars()
- tracks = (TrackWithMetadata(track, request.data_manager) for track in tracks)
- tracks = [track for track in tracks if filters.apply(track)]
+ tracks = list(
+ paginate(
+ request.dbsession,
+ query,
+ filters,
+ (page - 1) * TRACKS_PER_PAGE,
+ # We request one more so we can tell easily if there is a next page
+ TRACKS_PER_PAGE + 1,
+ )
+ )
+
+ if request.params.get("format") == "json":
+ obj = [
+ {
+ "id": track.id,
+ "title": track.title,
+ "date": (track.date or datetime.datetime.fromtimestamp(0)).timestamp(),
+ "length": track.length,
+ }
+ for track in tracks
+ ]
+ return Response(json.dumps(obj).encode("ascii"), content_type="application/json")
+
return {
- "tracks": tracks,
+ "tracks": tracks[:TRACKS_PER_PAGE],
"mps_to_kph": util.mps_to_kph,
"used_filters": bool(filters),
+ "page_previous": None if page == 1 else _with_page(request.url, page - 1),
+ "page_next": None if len(tracks) <= TRACKS_PER_PAGE else _with_page(request.url, page + 1),
}
@@ -467,12 +580,18 @@ def archive(request: Request) -> Response:
if not track.is_visible_to(request.identity):
return HTTPForbidden()
+ # Since we stream the data, we need to ensure it's loaded before we close
+ # the session
+ for track in tracks:
+ request.dbsession.refresh(track, ["points", "waypoints"])
+ request.dbsession.expunge(track)
+
def generate():
stream = Stream()
with ZipFile(stream, "w", ZIP_DEFLATED) as zipfile: # type: ignore
- for track_id in track_ids:
- data = request.data_manager.open(track_id).decompress_gpx()
- zipfile.writestr(f"track_{track_id}.gpx", data)
+ for track in tracks:
+ data = track.gpx_xml()
+ zipfile.writestr(f"track_{track.id}.gpx", data)
yield stream.readall()
yield stream.readall()
diff --git a/fietsboek/views/default.py b/fietsboek/views/default.py
index 8a9718d..320d02d 100644
--- a/fietsboek/views/default.py
+++ b/fietsboek/views/default.py
@@ -61,7 +61,7 @@ def home(request: Request) -> Response:
gpx_data = request.data_manager.open(track.id).decompress_gpx()
track.ensure_cache(gpx_data)
request.dbsession.add(track.cache)
- summary.add(TrackWithMetadata(track, request.data_manager))
+ summary.add(TrackWithMetadata(track))
unfinished_uploads = request.identity.uploads
diff --git a/fietsboek/views/detail.py b/fietsboek/views/detail.py
index 2bc5d9a..ca3a0af 100644
--- a/fietsboek/views/detail.py
+++ b/fietsboek/views/detail.py
@@ -2,21 +2,25 @@
import datetime
import gzip
+import io
import logging
+from html.parser import HTMLParser
+from markupsafe import Markup
from pyramid.httpexceptions import (
HTTPFound,
- HTTPInternalServerError,
HTTPNotAcceptable,
HTTPNotFound,
)
from pyramid.i18n import TranslationString as _
+from pyramid.request import Request
from pyramid.response import FileResponse, Response
from pyramid.view import view_config
from sqlalchemy import select
-from .. import models, util
+from .. import models, pdf, trackmap, util
from ..models.track import Track, TrackWithMetadata
+from .tileproxy import ITileRequester
LOGGER = logging.getLogger(__name__)
@@ -32,6 +36,34 @@ def _sort_key(image_name: str) -> str:
return image_name.split("_", 1)[1]
+class _DescriptionParser(HTMLParser):
+ # We don't go overboard with the parsing here, as we expect to only be fed
+ # output from the markdown conversion (so simple documents)
+ def __init__(self):
+ super().__init__()
+ self.par = False
+ self.text = ""
+
+ def handle_starttag(self, tag, attrs):
+ if tag == "p":
+ self.par = True
+
+ def handle_endtag(self, tag):
+ if tag == "p":
+ self.par = False
+
+ def handle_data(self, data):
+ if self.par:
+ self.text += data
+
+
+def _og_description(description: Markup) -> str:
+ """Returns a truncated, non-marked up description of the given input description."""
+ parser = _DescriptionParser()
+ parser.feed(str(description))
+ return parser.text
+
+
@view_config(
route_name="details", renderer="fietsboek:templates/details.jinja2", permission="track.view"
)
@@ -45,6 +77,7 @@ def details(request):
"""
track = request.context
description = util.safe_markdown(track.description)
+ og_description = _og_description(description)
show_edit_link = track.owner == request.identity
on_disk_images = []
@@ -71,7 +104,7 @@ def details(request):
# Strip off the sort key again
images = [(image[1], image[2]) for image in images]
- with_meta = TrackWithMetadata(track, request.data_manager)
+ with_meta = TrackWithMetadata(track)
return {
"track": with_meta,
"show_organic": track.show_organic_data(),
@@ -79,6 +112,7 @@ def details(request):
"mps_to_kph": util.mps_to_kph,
"comment_md_to_html": util.safe_markdown,
"description": description,
+ "og_description": og_description,
"images": images,
}
@@ -93,37 +127,28 @@ def gpx(request):
:rtype: pyramid.response.Response
"""
track: Track = request.context
- try:
- manager = request.data_manager.open(track.id)
- except FileNotFoundError:
- LOGGER.error("Track exists in database, but not on disk: %d", track.id)
- return HTTPInternalServerError()
if track.title:
wanted_filename = f"{track.id} - {util.secure_filename(track.title)}.gpx"
else:
wanted_filename = f"{track.id}.gpx"
content_disposition = f'attachment; filename="{wanted_filename}"'
- # We can be nice to the client if they support it, and deliver the gzipped
- # data straight. This saves decompression time on the server and saves a
- # lot of bandwidth.
- accepted = request.accept_encoding.acceptable_offers(["br", "gzip", "identity"])
+ gpx_data = track.gpx_xml()
+ # We used to offer brotli compression here as well, but brotli is too
+ # inefficient to do on-the-fly. That was only useful while we had the data
+ # stored brotli-compressed. For comparison, a track with ~50k points:
+ # identity -- 2.4s -- 5.49 MiB
+ # gzip -- 2.5s -- 657.53 KiB
+ # brotli -- 12s -- 389.45 KiB
+ # So yes, brotli does give better compression than gzip, but the time is
+ # not worth paying for.
+ accepted = request.accept_encoding.acceptable_offers(["gzip", "identity"])
for encoding, _qvalue in accepted:
- if encoding == "br":
- response = FileResponse(
- str(manager.gpx_path()),
- request,
- content_type="application/gpx+xml",
- content_encoding="br",
- )
- break
if encoding == "gzip":
- # gzip'ed GPX files are so much smaller than uncompressed ones, it
- # is worth re-compressing them for the client
- data = gzip.compress(manager.decompress_gpx())
+ data = gzip.compress(gpx_data)
response = Response(data, content_type="application/gpx+xml", content_encoding="gzip")
break
if encoding == "identity":
- response = Response(manager.decompress_gpx(), content_type="application/gpx+xml")
+ response = Response(gpx_data, content_type="application/gpx+xml")
break
else:
return HTTPNotAcceptable("No data with acceptable encoding found")
@@ -222,4 +247,65 @@ def add_comment(request):
return HTTPFound(request.route_url("details", track_id=track.id))
-__all__ = ["details", "gpx", "invalidate_share", "delete_track", "badge", "image", "add_comment"]
+@view_config(route_name="track-map", http_cache=3600, permission="track.view")
+def track_map(request: Request):
+ """Endpoint to provide the track's preview image.
+
+ Will use the cached version if available. Otherwise, will create the
+ preview and cache it.
+
+ :param request: The pyramid request.
+ :return: The HTTP response.
+ """
+ track = request.context
+ manager = request.data_manager.open(track.id)
+ preview_path = manager.preview_path()
+ try:
+ response = Response(preview_path.read_bytes(), content_type="image/png")
+ response.md5_etag()
+ return response
+ except FileNotFoundError:
+ pass
+
+ loader: ITileRequester = request.registry.getUtility(ITileRequester)
+ layer = request.config.public_tile_layers()[0]
+
+ track_image = trackmap.render(track.path(), layer, loader)
+
+ imageio = io.BytesIO()
+ track_image.save(imageio, "png")
+ tile_data = imageio.getvalue()
+
+ with manager.lock():
+ manager.set_preview(tile_data)
+
+ response = Response(tile_data, content_type="image/png")
+ response.md5_etag()
+ return response
+
+
+@view_config(route_name="track-pdf", permission="track.view")
+def track_pdf(request: Request):
+ """Endpoint to provide the track's PDF overview.
+
+ :param request: The pyramid request.
+ :return: The HTTP response.
+ """
+ loader: ITileRequester = request.registry.getUtility(ITileRequester)
+ layer = request.config.public_tile_layers()[0]
+ pdf_bytes = pdf.generate(request.context, loader, layer, request.localizer)
+ response = Response(pdf_bytes, content_type="application/pdf")
+ return response
+
+
+__all__ = [
+ "details",
+ "gpx",
+ "invalidate_share",
+ "delete_track",
+ "badge",
+ "image",
+ "add_comment",
+ "track_map",
+ "track_pdf",
+]
diff --git a/fietsboek/views/edit.py b/fietsboek/views/edit.py
index c3a4fc5..2b559d4 100644
--- a/fietsboek/views/edit.py
+++ b/fietsboek/views/edit.py
@@ -5,10 +5,11 @@ import logging
from collections import namedtuple
from pyramid.httpexceptions import HTTPBadRequest, HTTPFound
+from pyramid.i18n import TranslationString as _
from pyramid.view import view_config
from sqlalchemy import select
-from .. import actions, models, util
+from .. import actions, convert, models, util
from ..data import TrackDataDir
from ..models.track import TrackType, Visibility
@@ -68,7 +69,7 @@ def do_edit(request):
:return: The HTTP response.
:rtype: pyramid.response.Response
"""
- # pylint: disable=duplicate-code
+ # pylint: disable=duplicate-code,broad-exception-caught
track = request.context
user_friends = request.identity.get_friends()
@@ -83,27 +84,45 @@ def do_edit(request):
data: TrackDataDir = request.data_manager.open(track.id)
tz_offset = datetime.timedelta(minutes=int(request.params["date-tz"]))
date = datetime.datetime.fromisoformat(request.params["date"])
- with data, data.lock():
- track.date = date.replace(tzinfo=datetime.timezone(tz_offset))
-
- track.tagged_people = tagged_people
- track.title = request.params["title"]
- track.visibility = Visibility[request.params["visibility"]]
- track.type = TrackType[request.params["type"]]
- track.description = request.params["description"]
- track.badges = badges
- tags = request.params.getall("tag[]")
- track.sync_tags(tags)
-
- actions.edit_images(request, request.context, manager=data)
- gpx = actions.execute_transformers(request, request.context)
- data.engrave_metadata(
- title=track.title,
- description=track.description,
- author_name=track.owner.name,
- time=track.date,
- gpx=gpx,
- )
+ redo_cache = False
+ try:
+ gpx_bytes = request.POST["gpx"].file.read()
+ except AttributeError:
+ pass
+ else:
+ LOGGER.info("Setting new track for %s", track.id)
+ try:
+ new_track = convert.smart_convert(gpx_bytes)
+ except convert.ConversionError as exc:
+ request.session.flash(request.localizer.translate(_("flash.invalid_file")))
+ LOGGER.info("Could not parse gpx: %s", exc)
+ return HTTPFound(request.route_url("edit", track_id=track.id))
+ data.compress_backup(gpx_bytes)
+ track.fast_set_path(new_track.path())
+ track.transformers = []
+ redo_cache = True
+
+ track.date = date.replace(tzinfo=datetime.timezone(tz_offset))
+
+ track.tagged_people = tagged_people
+ track.title = request.params["title"]
+ track.visibility = Visibility[request.params["visibility"]]
+ track.type = TrackType[request.params["type"]]
+ track.description = request.params["description"]
+ track.badges = badges
+ tags = request.params.getall("tag[]")
+ track.sync_tags(tags)
+
+ actions.edit_images(request, request.context, manager=data)
+ actions.execute_transformers(request, request.context)
+
+ # actions.execute_transformers automatically rebuilds the cache, so we only need to do
+ # this if execute_transformers didn't do it
+ if redo_cache:
+ LOGGER.info("New file detected, rebuilding cache for %s", track.id)
+ track.cache = None
+ track.ensure_cache()
+ request.dbsession.add(track.cache)
return HTTPFound(request.route_url("details", track_id=track.id))
diff --git a/fietsboek/views/errors.py b/fietsboek/views/errors.py
new file mode 100644
index 0000000..39af14e
--- /dev/null
+++ b/fietsboek/views/errors.py
@@ -0,0 +1,32 @@
+"""Error views."""
+
+from pyramid.view import forbidden_view_config, notfound_view_config
+
+
+@notfound_view_config(renderer="fietsboek:templates/404.jinja2")
+def notfound_view(request):
+ """Renders the 404 response.
+
+ :param request: The Pyramid request.
+ :type request: pyramid.request.Request
+ :return: The HTTP response.
+ :rtype: pyramid.response.Response
+ """
+ request.response.status = 404
+ return {}
+
+
+@forbidden_view_config(renderer="fietsboek:templates/403.jinja2")
+def forbidden_view(request):
+ """Renders the 403 response.
+
+ :param request: The Pyramid request.
+ :type request: pyramid.request.Request
+ :return: The HTTP response.
+ :rtype: pyramid.response.Response
+ """
+ request.response.status = 403
+ return {}
+
+
+__all__ = ["notfound_view", "forbidden_view"]
diff --git a/fietsboek/views/journey.py b/fietsboek/views/journey.py
new file mode 100644
index 0000000..74b0fad
--- /dev/null
+++ b/fietsboek/views/journey.py
@@ -0,0 +1,267 @@
+"""Views relating to journeys."""
+
+import io
+import logging
+from datetime import timedelta
+
+from pyramid.httpexceptions import HTTPBadRequest, HTTPFound
+from pyramid.i18n import TranslationString as _
+from pyramid.request import Request
+from pyramid.response import Response
+from pyramid.view import view_config
+from sqlalchemy import select
+from sqlalchemy.orm import aliased
+
+from .. import trackmap, util
+from ..data import JourneyDataDir
+from ..models import User
+from ..models.journey import Journey, Visibility
+from ..models.track import Track, TrackWithMetadata
+from .tileproxy import ITileRequester
+
+LOGGER = logging.getLogger(__name__)
+
+
+@view_config(
+ route_name="journey-list",
+ renderer="fietsboek:templates/journey_list.jinja2",
+)
+def journey_list(request: Request):
+ """Lists the available journeys.
+
+ :param request: The pyramid request.
+ """
+ query = select(aliased(Journey, User.visible_journeys_query(request.identity).subquery()))
+ journeys = request.dbsession.execute(query).scalars()
+ show_new_button = request.identity is not None
+ return {
+ "journeys": journeys,
+ "md_to_html": util.safe_markdown,
+ "show_new_button": show_new_button,
+ }
+
+
+@view_config(
+ route_name="journey-details",
+ renderer="fietsboek:templates/journey_details.jinja2",
+ permission="journey.view",
+)
+def journey_details(request: Request):
+ """Shows details for a single journey.
+
+ :param request: The pyramid request.
+ """
+ journey: Journey = request.context
+ tracks = [TrackWithMetadata(track) for track in journey.tracks]
+ movement_data = journey.path().movement_data()
+ show_edit_link = request.identity == journey.owner
+ return {
+ "journey": journey,
+ "tracks": tracks,
+ "movement_data": movement_data,
+ "mps_to_kph": util.mps_to_kph,
+ "md_to_html": util.safe_markdown,
+ "timedelta": timedelta,
+ "show_edit_link": show_edit_link,
+ }
+
+
+@view_config(route_name="journey-gpx", http_cache=3600, permission="journey.view")
+def journey_gpx(request: Request):
+ """The view that returns the journey's GPX.
+
+ :param request: The pyramid request.
+ """
+ gpx_xml = request.context.gpx_xml()
+ response = Response(gpx_xml, content_type="application/gpx+xml")
+ response.md5_etag()
+ return response
+
+
+@view_config(route_name="journey-map", http_cache=3600, permission="journey.view")
+def journey_map(request: Request):
+ """The journey preview map image.
+
+ :param request: The pyramid request.
+ """
+ journey: Journey = request.context
+ journey_data: JourneyDataDir = request.data_manager.open_journey(journey.id)
+ preview_path = journey_data.preview_path()
+
+ if preview_path.exists():
+ response = Response(preview_path.read_bytes(), content_type="image/png")
+ response.md5_etag()
+ return response
+
+ loader: ITileRequester = request.registry.getUtility(ITileRequester)
+ layer = request.config.public_tile_layers()[0]
+
+ track_image = trackmap.render(journey.path(), layer, loader, size=(1300, 300))
+
+ imageio = io.BytesIO()
+ track_image.save(imageio, "png")
+ tile_data = imageio.getvalue()
+
+ if not preview_path.exists():
+ LOGGER.debug("Setting preview at %s", preview_path)
+ journey_data.set_preview(tile_data)
+
+ response = Response(tile_data, content_type="image/png")
+ response.md5_etag()
+ return response
+
+
+@view_config(
+ route_name="journey-new",
+ renderer="fietsboek:templates/journey_new.jinja2",
+ permission="new-journey",
+)
+def journey_new(_request: Request):
+ """The form to add a new journey.
+
+ :param request: The pyramid request.
+ """
+ return {}
+
+
+@view_config(
+ route_name="journey-new",
+ permission="new-journey",
+ request_method="POST",
+)
+def do_journey_new(request: Request):
+ """Handler for submitting the new-journey form.
+
+ :param request: The pyramid request.
+ """
+ journey = Journey(
+ owner=request.identity,
+ title=request.params.get("journeyTitle"),
+ description=request.params.get("journeyDescription"),
+ visibility=_extract_visibility(request),
+ link_secret=util.random_link_secret(),
+ tracks=[],
+ )
+
+ request.dbsession.add(journey)
+ request.dbsession.flush()
+
+ track_ids = _extract_valid_tracks(request, set())
+ journey.set_track_ids(track_ids)
+
+ request.data_manager.initialize_journey(journey.id)
+
+ return HTTPFound(request.route_url("journey-details", journey_id=journey.id))
+
+
+@view_config(
+ route_name="journey-edit",
+ renderer="fietsboek:templates/journey_edit.jinja2",
+ permission="journey.edit",
+)
+def journey_edit(request: Request):
+ """The form to edit a journey.
+
+ :param request: The pyramid request.
+ """
+ journey: Journey = request.context
+ return {
+ "journey": journey,
+ }
+
+
+@view_config(
+ route_name="journey-edit",
+ permission="journey.edit",
+ request_method="POST",
+)
+def do_journey_edit(request: Request):
+ """Handler for submitting the edit-journey form.
+
+ :param request: The pyramid request.
+ """
+ journey: Journey = request.context
+ request.data_manager.open_journey(journey.id).remove_preview()
+
+ journey.title = request.params.get("journeyTitle")
+ journey.description = request.params.get("journeyDescription")
+ journey.visibility = _extract_visibility(request)
+
+ track_ids = _extract_valid_tracks(request, {track.id for track in journey.tracks})
+ journey.set_track_ids(track_ids)
+
+ request.dbsession.add(journey)
+
+ return HTTPFound(request.route_url("journey-details", journey_id=journey.id))
+
+
+def _extract_visibility(request: Request) -> Visibility:
+ key = request.params.get("journeyVisibility")
+ try:
+ return Visibility[key]
+ except KeyError:
+ raise HTTPBadRequest("Invalid visibility") from None
+
+
+def _extract_valid_tracks(request: Request, current_ids: set[int]) -> list[int]:
+ user: User = request.identity
+
+ if not request.params.get("journeyTitle"):
+ raise HTTPBadRequest("Needs a title")
+
+ try:
+ track_ids = [int(tid) for tid in request.params.getall("journeyTrack[]")]
+ except ValueError:
+ # Shouldn't happen if users don't tamper with the requests manually, so we don't translate
+ raise HTTPBadRequest("Invalid track ID") from None
+
+ if not track_ids:
+ raise HTTPBadRequest("No track IDs given")
+
+ for track_id in track_ids:
+ query = select(Track).filter_by(id=track_id)
+ track: Track = request.dbsession.execute(query).scalar_one_or_none()
+ if track is None:
+ raise HTTPBadRequest("Invalid track ID")
+ # We don't really want users to add tracks to journeys that they can't
+ # see, because that leaks information (e.g., you create a journey and
+ # add a single tracks, that gives you the clear path).
+ # However, if a track used to be visible and now is no longer, we don't
+ # want editing to fail, so we allow a non-visible track if it is already
+ # in the journey.
+ if not track.is_visible_to(user) and track_id not in current_ids:
+ raise HTTPBadRequest("Invalid track ID")
+
+ return track_ids
+
+
+@view_config(
+ route_name="delete-journey",
+ permission="journey.delete",
+ request_method="POST",
+)
+def do_journey_delete(request: Request):
+ """Handler to delete a journey.
+
+ :param request: The pyramid request.
+ """
+ journey: Journey = request.context
+ request.data_manager.open_journey(journey.id).purge()
+ request.dbsession.delete(journey)
+ request.session.flash(request.localizer.translate(_("flash.journey_deleted")))
+ return HTTPFound(request.route_url("journey-list"))
+
+
+@view_config(
+ route_name="journey-invalidate-share",
+ permission="journey.edit",
+ request_method="POST",
+)
+def do_journey_invalidate_share(request: Request):
+ """Handler to invalidate a journey share link.
+
+ :param request: The pyramid request.
+ """
+ journey: Journey = request.context
+ journey.link_secret = util.random_link_secret()
+ return HTTPFound(request.route_url("journey-details", journey_id=journey.id))
diff --git a/fietsboek/views/notfound.py b/fietsboek/views/notfound.py
deleted file mode 100644
index 2ec6c6c..0000000
--- a/fietsboek/views/notfound.py
+++ /dev/null
@@ -1,19 +0,0 @@
-"""Error views."""
-
-from pyramid.view import notfound_view_config
-
-
-@notfound_view_config(renderer="fietsboek:templates/404.jinja2")
-def notfound_view(request):
- """Renders the 404 response.
-
- :param request: The Pyramid request.
- :type request: pyramid.request.Request
- :return: The HTTP response.
- :rtype: pyramid.response.Response
- """
- request.response.status = 404
- return {}
-
-
-__all__ = ["notfound_view"]
diff --git a/fietsboek/views/profile.py b/fietsboek/views/profile.py
index 15bc46c..4f9abca 100644
--- a/fietsboek/views/profile.py
+++ b/fietsboek/views/profile.py
@@ -14,9 +14,9 @@ from sqlalchemy import select
from sqlalchemy.orm import aliased
from .. import models, util
-from ..data import DataManager, UserDataDir
+from ..data import UserDataDir
from ..models.track import TrackType, TrackWithMetadata
-from ..summaries import CumulativeStats, Summary
+from ..summaries import CumulativeStats, Summary, YearContributions
# A well-made transparent tile is actually pretty small (only 116 bytes), which
# is even smaller than our HTTP 404 page. So not only is it more efficient
@@ -49,13 +49,15 @@ LOGGER = logging.getLogger(__name__)
def profile_data(request: Request) -> dict:
"""Retrieves the profile data for the given request."""
total = CumulativeStats()
+ year_contribs = YearContributions(datetime.date.today())
query = request.context.all_tracks_query()
query = select(aliased(models.Track, query)).where(query.c.type == TrackType.ORGANIC)
track: models.Track
for track in request.dbsession.execute(query).scalars():
- meta = TrackWithMetadata(track, request.data_manager)
+ meta = TrackWithMetadata(track)
total.add(meta)
+ year_contribs.add(meta)
total.moving_time = util.round_to_seconds(total.moving_time)
total.stopped_time = util.round_to_seconds(total.stopped_time)
@@ -79,6 +81,7 @@ def profile_data(request: Request) -> dict:
"mps_to_kph": util.mps_to_kph,
"heatmap_url": heatmap_url,
"tilehunt_url": tilehunt_url,
+ "year_contributions": year_contribs,
}
@@ -132,7 +135,6 @@ def profile_calendar(request: Request) -> dict:
data["user"] = request.context
data["calendar_rows"] = calendar_rows(
request.dbsession,
- request.data_manager,
request.context,
date.year,
date.month,
@@ -161,7 +163,6 @@ def profile_calendar_ym(request: Request) -> dict:
data["user"] = request.context
data["calendar_rows"] = calendar_rows(
request.dbsession,
- request.data_manager,
request.context,
date.year,
date.month,
@@ -200,7 +201,6 @@ def cell_style(tracks: list[TrackWithMetadata]) -> str:
def calendar_rows(
dbsession: "sqlalchemy.orm.session.Session",
- data_manager: DataManager,
user: models.User,
year: int,
month: int,
@@ -222,9 +222,7 @@ def calendar_rows(
# Step 1: Retrieve all tracks
query = user.all_tracks_query()
query = select(aliased(models.Track, query)).where(query.c.type == TrackType.ORGANIC)
- tracks = [
- TrackWithMetadata(track, data_manager) for track in dbsession.execute(query).scalars()
- ]
+ tracks = [TrackWithMetadata(track) for track in dbsession.execute(query).scalars()]
# Step 2: Build the calendar
days = []
@@ -313,7 +311,7 @@ def user_tile(request: Request) -> Response:
cursor = connection.cursor()
result = cursor.execute(
- "SELECT data FROM tiles WHERE zoom = ? AND x = ? AND y = ?;",
+ "SELECT image FROM tiles WHERE z = ? AND x = ? AND y = ?;",
(int(request.matchdict["z"]), int(request.matchdict["x"]), int(request.matchdict["y"])),
)
result = result.fetchone()
@@ -340,7 +338,7 @@ def json_summary(request: Request) -> Response:
if track.cache is None:
LOGGER.debug("Skipping track %d as it has no cached metadata", track.id)
continue
- summary.add(TrackWithMetadata(track, request.data_manager))
+ summary.add(TrackWithMetadata(track))
return {y.year: {m.month: m.total_length for m in y} for y in summary}
diff --git a/fietsboek/views/tileproxy.py b/fietsboek/views/tileproxy.py
index 0efe4de..f472d6d 100644
--- a/fietsboek/views/tileproxy.py
+++ b/fietsboek/views/tileproxy.py
@@ -242,18 +242,44 @@ Note that new requests reset the timeout.
class ITileRequester(Interface): # pylint: disable=inherit-non-class
"""An interface to define the tile requester."""
- def load_tile(self, url: str, headers: Optional[dict[str, str]] = None) -> requests.Response:
+ # pylint: disable=too-many-arguments
+
+ def load_url(self, url: str, headers: Optional[dict[str, str]] = None) -> requests.Response:
"""Loads a tile at the given URL.
+ This is a low-level version of :meth:`~ITileRequester.load_tile`.
+
:param url: The URL of the tile to load.
:param headers: Additional headers to send.
:return: The response.
"""
raise NotImplementedError()
+ def load_tile(
+ self,
+ layer: TileLayerConfig,
+ zoom: int,
+ x: int,
+ y: int,
+ *,
+ headers: Optional[dict[str, str]] = None,
+ use_cache: bool = True,
+ ) -> bytes:
+ """Loads a tile from the given layer.
+
+ :param layer: The configured tile layer.
+ :param zoom: The zoom level.
+ :param x: The tile's x coordinate.
+ :param y: The tile's y coordinate.
+ :param headers: Additional headers.
+ :param use_cache: Whether to use the cache (if available).
+ :return: The bytes of the tile.
+ """
+ raise NotImplementedError()
+
@implementer(ITileRequester)
-class TileRequester: # pylint: disable=too-few-public-methods
+class TileRequester:
"""Implementation of the tile requester using requests sessions.
The benefit of this over doing ``requests.get`` is that we can re-use
@@ -261,7 +287,9 @@ class TileRequester: # pylint: disable=too-few-public-methods
servers by not hammering them with too many connections.
"""
- def __init__(self):
+ # pylint: disable=too-many-arguments
+
+ def __init__(self, redis):
self.session = requests.Session()
adapter = requests.adapters.HTTPAdapter(
pool_maxsize=MAX_CONCURRENT_CONNECTIONS,
@@ -271,14 +299,56 @@ class TileRequester: # pylint: disable=too-few-public-methods
self.session.mount("https://", adapter)
self.lock = threading.Lock()
self.closer = None
-
- def load_tile(self, url: str, headers: Optional[dict[str, str]] = None) -> requests.Response:
- """Implementation of :meth:`ITileRequester.load_tile`."""
+ self.redis = redis
+
+ def load_url(self, url: str, headers: Optional[dict[str, str]] = None) -> requests.Response:
+ """Implementation of :meth:`ITileRequester.load_url`."""
+ if headers is None:
+ headers = {}
+ if "user-agent" not in headers:
+ headers["user-agent"] = f"Fietsboek/{__VERSION__}"
response = self.session.get(url, headers=headers, timeout=TIMEOUT.total_seconds())
response.raise_for_status()
self._schedule_session_close()
return response
+ def load_tile(
+ self,
+ layer: TileLayerConfig,
+ zoom: int,
+ x: int,
+ y: int,
+ *,
+ headers: Optional[dict[str, str]] = None,
+ use_cache: bool = True,
+ ) -> bytes:
+ """Implementation of :meth:`ITileRequester.load_tile`."""
+ cache_key = f"tile:{layer.layer_id}-{x}-{y}-{zoom}"
+
+ if use_cache and self.redis is not None:
+ cached = self.redis.get(cache_key)
+ if cached is not None:
+ LOGGER.debug("Cache hit for %s/z:%s/x:%s/y:%s", layer.layer_id, zoom, x, y)
+ return cached
+
+ url = (
+ layer.url.unicode_string()
+ .replace(quote("{x}"), str(x))
+ .replace(quote("{y}"), str(y))
+ .replace(quote("{z}"), str(zoom))
+ )
+
+ # Avoid doing actual requests during tests
+ if url.startswith("http://localhost:0"):
+ LOGGER.debug("Skipping tile request for testing URL")
+ return b""
+
+ response = self.load_url(url, headers=headers)
+ tile_data = response.content
+ if use_cache and self.redis is not None:
+ self.redis.set(cache_key, tile_data, ex=TTL)
+ return tile_data
+
def _schedule_session_close(self):
with self.lock:
if self.closer:
@@ -332,29 +402,17 @@ def tile_proxy(request):
LOGGER.debug("Aborted attempt to contact %s due to previous timeouts", provider)
raise HTTPGatewayTimeout(f"Avoiding request to {provider}")
- url = (
- tile_sources[provider]
- .url.unicode_string()
- .replace(quote("{x}"), str(x))
- .replace(quote("{y}"), str(y))
- .replace(quote("{z}"), str(z))
- )
- # Avoid doing actual requests during tests
- if url.startswith("http://localhost:0"):
- LOGGER.debug("Skipping tile proxy request for testing URL")
- return Response(b"", content_type="image/png")
- headers = {
- "user-agent": f"Fietsboek-Tile-Proxy/{__VERSION__}",
- }
+ headers = {}
from_mail = request.config.email_from
if from_mail:
headers["from"] = from_mail
loader: ITileRequester = request.registry.getUtility(ITileRequester)
try:
- resp = loader.load_tile(url, headers=headers)
+ # We already tried the cache, so bypass it here
+ resp = loader.load_tile(tile_sources[provider], z, x, y, headers=headers, use_cache=False)
except ReadTimeout:
- LOGGER.debug("Proxy timeout when accessing %r", url)
+ LOGGER.debug("Proxy timeout when accessing z:%s/x:%s/y:%s from %s", z, x, y, provider)
request.redis.incr(timeout_tracker)
request.redis.expire(timeout_tracker, PUNISHMENT_TTL)
raise HTTPGatewayTimeout(f"No response in time from {provider}") from None
@@ -365,8 +423,8 @@ def tile_proxy(request):
status_code = exc.response.status_code
return Response(f"Failed to get tile from {provider}", status_code=status_code)
else:
- request.redis.set(cache_key, resp.content, ex=TTL)
- return Response(resp.content, content_type=resp.headers.get("Content-type", content_type))
+ request.redis.set(cache_key, resp, ex=TTL)
+ return Response(resp, content_type=content_type)
def sources_for(request: Request) -> list[TileLayerConfig]:
@@ -375,6 +433,9 @@ def sources_for(request: Request) -> list[TileLayerConfig]:
:param request: The Pyramid request.
:return: A list of tile sources.
"""
+ # This code is similar to Config.public_tile_layers. Maybe it's worth
+ # refactoring it when we refactor this module?
+ # pylint: disable=duplicate-code
return [
source
for source in chain(
diff --git a/fietsboek/views/upload.py b/fietsboek/views/upload.py
index c40319c..7be5a42 100644
--- a/fietsboek/views/upload.py
+++ b/fietsboek/views/upload.py
@@ -3,7 +3,6 @@
import datetime
import logging
-import gpxpy
from pyramid.httpexceptions import HTTPBadRequest, HTTPFound
from pyramid.i18n import TranslationString as _
from pyramid.response import Response
@@ -12,6 +11,7 @@ from sqlalchemy import select
from .. import actions, convert, models, transformers, util
from ..models.track import TrackType, Visibility
+from ..views.tileproxy import ITileRequester
LOGGER = logging.getLogger(__name__)
@@ -53,15 +53,12 @@ def do_upload(request):
request.session.flash(request.localizer.translate(_("flash.no_file_selected")))
return HTTPFound(request.route_url("upload"))
- if len(gpx) > 11 and gpx[9:12] == b"FIT":
- gpx = convert.from_fit(gpx).to_xml().encode("utf-8")
-
# Before we do anything, we check if we can parse the file.
# gpxpy might throw different exceptions, so we simply catch `Exception`
# here - if we can't parse it, we don't care too much why at this point.
# pylint: disable=broad-except
try:
- gpxpy.parse(gpx)
+ track = convert.smart_convert(gpx)
except Exception as exc:
request.session.flash(request.localizer.translate(_("flash.invalid_file")))
LOGGER.info("Could not parse gpx: %s", exc)
@@ -73,7 +70,7 @@ def do_upload(request):
owner=request.identity,
uploaded_at=now,
)
- upload.gpx_data = gpx
+ upload.gpx_data = track.gpx_xml()
request.dbsession.add(upload)
request.dbsession.flush()
@@ -111,28 +108,19 @@ def finish_upload(request):
upload = request.context
badges = request.dbsession.execute(select(models.Badge)).scalars()
badges = [(False, badge) for badge in badges]
- gpx = gpxpy.parse(upload.gpx_data)
- timezone = util.guess_gpx_timezone(gpx)
- date = gpx.time or gpx.get_time_bounds().start_time or datetime.datetime.now()
- date = date.astimezone(timezone)
- tz_offset = timezone.utcoffset(date)
+ track = convert.smart_convert(upload.gpx_data)
+ timezone = track.date.tzinfo
+ tz_offset = timezone.utcoffset(track.date)
tz_offset = 0 if tz_offset is None else tz_offset.total_seconds()
- track_name = ""
- track_desc = ""
- for track in gpx.tracks:
- if not track_name and track.name:
- track_name = track.name
- if not track_desc and track.description:
- track_desc = track.description
return {
"preview_id": upload.id,
- "upload_title": gpx.name or track_name,
- "upload_date": date,
+ "upload_title": track.title,
+ "upload_date": track.date,
"upload_date_tz": int(tz_offset // 60),
"upload_visibility": Visibility.PRIVATE,
"upload_type": TrackType.ORGANIC,
- "upload_description": gpx.description or track_desc,
+ "upload_description": track.description,
"upload_tags": set(),
"upload_tagged_people": [],
"badges": badges,
@@ -165,6 +153,8 @@ def do_finish_upload(request):
track = actions.add_track(
request.dbsession,
request.data_manager,
+ request.registry.getUtility(ITileRequester),
+ request.config.public_tile_layers()[0],
owner=request.identity,
title=request.params["title"],
visibility=Visibility[request.params["visibility"]],
@@ -180,14 +170,9 @@ def do_finish_upload(request):
request.dbsession.delete(upload)
# Don't forget to add the images
+ manager = request.data_manager.open(track.id, force=True)
LOGGER.debug("Starting to edit images for the upload")
- try:
- actions.edit_images(request, track)
- except Exception:
- # We just created the folder, so we'll be fine deleting it
- LOGGER.info("Deleting partially created folder for track %d", track.id)
- request.data_manager.open(track.id).purge()
- raise
+ actions.edit_images(request, track, manager=manager)
request.session.flash(request.localizer.translate(_("flash.upload_success")))
diff --git a/poetry.lock b/poetry.lock
index c1347df..3fc3a7a 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand.
+# This file is automatically @generated by Poetry 2.3.3 and should not be changed by hand.
[[package]]
name = "alabaster"
@@ -6,6 +6,7 @@ version = "1.0.0"
description = "A light, configurable Sphinx theme"
optional = false
python-versions = ">=3.10"
+groups = ["docs"]
files = [
{file = "alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"},
{file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"},
@@ -13,22 +14,23 @@ files = [
[[package]]
name = "alembic"
-version = "1.14.1"
+version = "1.18.4"
description = "A database migration tool for SQLAlchemy."
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.10"
+groups = ["main"]
files = [
- {file = "alembic-1.14.1-py3-none-any.whl", hash = "sha256:1acdd7a3a478e208b0503cd73614d5e4c6efafa4e73518bb60e4f2846a37b1c5"},
- {file = "alembic-1.14.1.tar.gz", hash = "sha256:496e888245a53adf1498fcab31713a469c65836f8de76e01399aa1c3e90dd213"},
+ {file = "alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a"},
+ {file = "alembic-1.18.4.tar.gz", hash = "sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc"},
]
[package.dependencies]
Mako = "*"
-SQLAlchemy = ">=1.3.0"
-typing-extensions = ">=4"
+SQLAlchemy = ">=1.4.23"
+typing-extensions = ">=4.12"
[package.extras]
-tz = ["backports.zoneinfo", "tzdata"]
+tz = ["tzdata"]
[[package]]
name = "annotated-types"
@@ -36,6 +38,7 @@ version = "0.7.0"
description = "Reusable constraint types to use with typing.Annotated"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
@@ -43,24 +46,24 @@ files = [
[[package]]
name = "astroid"
-version = "3.3.8"
+version = "4.0.4"
description = "An abstract syntax tree for Python with inference support."
optional = false
-python-versions = ">=3.9.0"
+python-versions = ">=3.10.0"
+groups = ["linters"]
files = [
- {file = "astroid-3.3.8-py3-none-any.whl", hash = "sha256:187ccc0c248bfbba564826c26f070494f7bc964fd286b6d9fff4420e55de828c"},
- {file = "astroid-3.3.8.tar.gz", hash = "sha256:a88c7994f914a4ea8572fac479459f4955eeccc877be3f2d959a33273b0cf40b"},
+ {file = "astroid-4.0.4-py3-none-any.whl", hash = "sha256:52f39653876c7dec3e3afd4c2696920e05c83832b9737afc21928f2d2eb7a753"},
+ {file = "astroid-4.0.4.tar.gz", hash = "sha256:986fed8bcf79fb82c78b18a53352a0b287a73817d6dbcfba3162da36667c49a0"},
]
-[package.dependencies]
-typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""}
-
[[package]]
name = "async-timeout"
version = "5.0.1"
description = "Timeout context manager for asyncio programs"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
+markers = "python_version == \"3.11\" and python_full_version < \"3.11.3\""
files = [
{file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"},
{file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"},
@@ -68,31 +71,34 @@ files = [
[[package]]
name = "babel"
-version = "2.16.0"
+version = "2.18.0"
description = "Internationalization utilities"
optional = false
python-versions = ">=3.8"
+groups = ["main", "docs"]
files = [
- {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"},
- {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"},
+ {file = "babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35"},
+ {file = "babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d"},
]
[package.extras]
-dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"]
+dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""]
[[package]]
name = "beautifulsoup4"
-version = "4.12.3"
+version = "4.14.3"
description = "Screen-scraping library"
optional = false
-python-versions = ">=3.6.0"
+python-versions = ">=3.7.0"
+groups = ["testing"]
files = [
- {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"},
- {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"},
+ {file = "beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb"},
+ {file = "beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86"},
]
[package.dependencies]
-soupsieve = ">1.2"
+soupsieve = ">=1.6.1"
+typing-extensions = ">=4.0.0"
[package.extras]
cchardet = ["cchardet"]
@@ -103,33 +109,39 @@ lxml = ["lxml"]
[[package]]
name = "black"
-version = "25.1.0"
+version = "25.12.0"
description = "The uncompromising code formatter."
optional = false
-python-versions = ">=3.9"
-files = [
- {file = "black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32"},
- {file = "black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da"},
- {file = "black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7"},
- {file = "black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9"},
- {file = "black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0"},
- {file = "black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299"},
- {file = "black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096"},
- {file = "black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2"},
- {file = "black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b"},
- {file = "black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc"},
- {file = "black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f"},
- {file = "black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba"},
- {file = "black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f"},
- {file = "black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3"},
- {file = "black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171"},
- {file = "black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18"},
- {file = "black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0"},
- {file = "black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f"},
- {file = "black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e"},
- {file = "black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355"},
- {file = "black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717"},
- {file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"},
+python-versions = ">=3.10"
+groups = ["linters"]
+files = [
+ {file = "black-25.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f85ba1ad15d446756b4ab5f3044731bf68b777f8f9ac9cdabd2425b97cd9c4e8"},
+ {file = "black-25.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:546eecfe9a3a6b46f9d69d8a642585a6eaf348bcbbc4d87a19635570e02d9f4a"},
+ {file = "black-25.12.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17dcc893da8d73d8f74a596f64b7c98ef5239c2cd2b053c0f25912c4494bf9ea"},
+ {file = "black-25.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:09524b0e6af8ba7a3ffabdfc7a9922fb9adef60fed008c7cd2fc01f3048e6e6f"},
+ {file = "black-25.12.0-cp310-cp310-win_arm64.whl", hash = "sha256:b162653ed89eb942758efeb29d5e333ca5bb90e5130216f8369857db5955a7da"},
+ {file = "black-25.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0cfa263e85caea2cff57d8f917f9f51adae8e20b610e2b23de35b5b11ce691a"},
+ {file = "black-25.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a2f578ae20c19c50a382286ba78bfbeafdf788579b053d8e4980afb079ab9be"},
+ {file = "black-25.12.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e1b65634b0e471d07ff86ec338819e2ef860689859ef4501ab7ac290431f9b"},
+ {file = "black-25.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a3fa71e3b8dd9f7c6ac4d818345237dfb4175ed3bf37cd5a581dbc4c034f1ec5"},
+ {file = "black-25.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:51e267458f7e650afed8445dc7edb3187143003d52a1b710c7321aef22aa9655"},
+ {file = "black-25.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:31f96b7c98c1ddaeb07dc0f56c652e25bdedaac76d5b68a059d998b57c55594a"},
+ {file = "black-25.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05dd459a19e218078a1f98178c13f861fe6a9a5f88fc969ca4d9b49eb1809783"},
+ {file = "black-25.12.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1f68c5eff61f226934be6b5b80296cf6939e5d2f0c2f7d543ea08b204bfaf59"},
+ {file = "black-25.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:274f940c147ddab4442d316b27f9e332ca586d39c85ecf59ebdea82cc9ee8892"},
+ {file = "black-25.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:169506ba91ef21e2e0591563deda7f00030cb466e747c4b09cb0a9dae5db2f43"},
+ {file = "black-25.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a05ddeb656534c3e27a05a29196c962877c83fa5503db89e68857d1161ad08a5"},
+ {file = "black-25.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9ec77439ef3e34896995503865a85732c94396edcc739f302c5673a2315e1e7f"},
+ {file = "black-25.12.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e509c858adf63aa61d908061b52e580c40eae0dfa72415fa47ac01b12e29baf"},
+ {file = "black-25.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:252678f07f5bac4ff0d0e9b261fbb029fa530cfa206d0a636a34ab445ef8ca9d"},
+ {file = "black-25.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc5b1c09fe3c931ddd20ee548511c64ebf964ada7e6f0763d443947fd1c603ce"},
+ {file = "black-25.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0a0953b134f9335c2434864a643c842c44fba562155c738a2a37a4d61f00cad5"},
+ {file = "black-25.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2355bbb6c3b76062870942d8cc450d4f8ac71f9c93c40122762c8784df49543f"},
+ {file = "black-25.12.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9678bd991cc793e81d19aeeae57966ee02909877cb65838ccffef24c3ebac08f"},
+ {file = "black-25.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:97596189949a8aad13ad12fcbb4ae89330039b96ad6742e6f6b45e75ad5cfd83"},
+ {file = "black-25.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:778285d9ea197f34704e3791ea9404cd6d07595745907dd2ce3da7a13627b29b"},
+ {file = "black-25.12.0-py3-none-any.whl", hash = "sha256:48ceb36c16dbc84062740049eef990bb2ce07598272e673c17d1a7720c71c828"},
+ {file = "black-25.12.0.tar.gz", hash = "sha256:8d3dd9cea14bff7ddc0eb243c811cdb1a011ebb4800a5f0335a01a68654796a7"},
]
[package.dependencies]
@@ -138,8 +150,7 @@ mypy-extensions = ">=0.4.3"
packaging = ">=22.0"
pathspec = ">=0.9.0"
platformdirs = ">=2"
-tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
-typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
+pytokens = ">=0.3.0"
[package.extras]
colorama = ["colorama (>=0.4.3)"]
@@ -149,296 +160,373 @@ uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "brotli"
-version = "1.1.0"
+version = "1.2.0"
description = "Python bindings for the Brotli compression library"
optional = false
python-versions = "*"
-files = [
- {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752"},
- {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9"},
- {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3"},
- {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d"},
- {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e"},
- {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da"},
- {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80"},
- {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d"},
- {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0"},
- {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e"},
- {file = "Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2"},
- {file = "Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128"},
- {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc"},
- {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6"},
- {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd"},
- {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf"},
- {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61"},
- {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327"},
- {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd"},
- {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9"},
- {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265"},
- {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8"},
- {file = "Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50"},
- {file = "Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1"},
- {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409"},
- {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2"},
- {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451"},
- {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91"},
- {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408"},
- {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0"},
- {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc"},
- {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180"},
- {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248"},
- {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966"},
- {file = "Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0"},
- {file = "Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951"},
- {file = "Brotli-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1"},
- {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d"},
- {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b"},
- {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112"},
- {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064"},
- {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914"},
- {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2"},
- {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354"},
- {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2"},
- {file = "Brotli-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460"},
- {file = "Brotli-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579"},
- {file = "Brotli-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c"},
- {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985"},
- {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60"},
- {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a"},
- {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84"},
- {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643"},
- {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74"},
- {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b"},
- {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438"},
- {file = "Brotli-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95"},
- {file = "Brotli-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68"},
- {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3"},
- {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208"},
- {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7"},
- {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751"},
- {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48"},
- {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619"},
- {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97"},
- {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a"},
- {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088"},
- {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596"},
- {file = "Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b"},
- {file = "Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0"},
- {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a"},
- {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f"},
- {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9"},
- {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf"},
- {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac"},
- {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578"},
- {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474"},
- {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c"},
- {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d"},
- {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59"},
- {file = "Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64"},
- {file = "Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467"},
- {file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"},
+groups = ["main"]
+files = [
+ {file = "brotli-1.2.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:99cfa69813d79492f0e5d52a20fd18395bc82e671d5d40bd5a91d13e75e468e8"},
+ {file = "brotli-1.2.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:3ebe801e0f4e56d17cd386ca6600573e3706ce1845376307f5d2cbd32149b69a"},
+ {file = "brotli-1.2.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:a387225a67f619bf16bd504c37655930f910eb03675730fc2ad69d3d8b5e7e92"},
+ {file = "brotli-1.2.0-cp27-cp27m-win32.whl", hash = "sha256:b908d1a7b28bc72dfb743be0d4d3f8931f8309f810af66c906ae6cd4127c93cb"},
+ {file = "brotli-1.2.0-cp27-cp27m-win_amd64.whl", hash = "sha256:d206a36b4140fbb5373bf1eb73fb9de589bb06afd0d22376de23c5e91d0ab35f"},
+ {file = "brotli-1.2.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7e9053f5fb4e0dfab89243079b3e217f2aea4085e4d58c5c06115fc34823707f"},
+ {file = "brotli-1.2.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:4735a10f738cb5516905a121f32b24ce196ab82cfc1e4ba2e3ad1b371085fd46"},
+ {file = "brotli-1.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3b90b767916ac44e93a8e28ce6adf8d551e43affb512f2377c732d486ac6514e"},
+ {file = "brotli-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6be67c19e0b0c56365c6a76e393b932fb0e78b3b56b711d180dd7013cb1fd984"},
+ {file = "brotli-1.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0bbd5b5ccd157ae7913750476d48099aaf507a79841c0d04a9db4415b14842de"},
+ {file = "brotli-1.2.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3f3c908bcc404c90c77d5a073e55271a0a498f4e0756e48127c35d91cf155947"},
+ {file = "brotli-1.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1b557b29782a643420e08d75aea889462a4a8796e9a6cf5621ab05a3f7da8ef2"},
+ {file = "brotli-1.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81da1b229b1889f25adadc929aeb9dbc4e922bd18561b65b08dd9343cfccca84"},
+ {file = "brotli-1.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ff09cd8c5eec3b9d02d2408db41be150d8891c5566addce57513bf546e3d6c6d"},
+ {file = "brotli-1.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a1778532b978d2536e79c05dac2d8cd857f6c55cd0c95ace5b03740824e0e2f1"},
+ {file = "brotli-1.2.0-cp310-cp310-win32.whl", hash = "sha256:b232029d100d393ae3c603c8ffd7e3fe6f798c5e28ddca5feabb8e8fdb732997"},
+ {file = "brotli-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:ef87b8ab2704da227e83a246356a2b179ef826f550f794b2c52cddb4efbd0196"},
+ {file = "brotli-1.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:15b33fe93cedc4caaff8a0bd1eb7e3dab1c61bb22a0bf5bdfdfd97cd7da79744"},
+ {file = "brotli-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:898be2be399c221d2671d29eed26b6b2713a02c2119168ed914e7d00ceadb56f"},
+ {file = "brotli-1.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:350c8348f0e76fff0a0fd6c26755d2653863279d086d3aa2c290a6a7251135dd"},
+ {file = "brotli-1.2.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1ad3fda65ae0d93fec742a128d72e145c9c7a99ee2fcd667785d99eb25a7fe"},
+ {file = "brotli-1.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:40d918bce2b427a0c4ba189df7a006ac0c7277c180aee4617d99e9ccaaf59e6a"},
+ {file = "brotli-1.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2a7f1d03727130fc875448b65b127a9ec5d06d19d0148e7554384229706f9d1b"},
+ {file = "brotli-1.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9c79f57faa25d97900bfb119480806d783fba83cd09ee0b33c17623935b05fa3"},
+ {file = "brotli-1.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:844a8ceb8483fefafc412f85c14f2aae2fb69567bf2a0de53cdb88b73e7c43ae"},
+ {file = "brotli-1.2.0-cp311-cp311-win32.whl", hash = "sha256:aa47441fa3026543513139cb8926a92a8e305ee9c71a6209ef7a97d91640ea03"},
+ {file = "brotli-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:022426c9e99fd65d9475dce5c195526f04bb8be8907607e27e747893f6ee3e24"},
+ {file = "brotli-1.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:35d382625778834a7f3061b15423919aa03e4f5da34ac8e02c074e4b75ab4f84"},
+ {file = "brotli-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a61c06b334bd99bc5ae84f1eeb36bfe01400264b3c352f968c6e30a10f9d08b"},
+ {file = "brotli-1.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acec55bb7c90f1dfc476126f9711a8e81c9af7fb617409a9ee2953115343f08d"},
+ {file = "brotli-1.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:260d3692396e1895c5034f204f0db022c056f9e2ac841593a4cf9426e2a3faca"},
+ {file = "brotli-1.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:072e7624b1fc4d601036ab3f4f27942ef772887e876beff0301d261210bca97f"},
+ {file = "brotli-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adedc4a67e15327dfdd04884873c6d5a01d3e3b6f61406f99b1ed4865a2f6d28"},
+ {file = "brotli-1.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7a47ce5c2288702e09dc22a44d0ee6152f2c7eda97b3c8482d826a1f3cfc7da7"},
+ {file = "brotli-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:af43b8711a8264bb4e7d6d9a6d004c3a2019c04c01127a868709ec29962b6036"},
+ {file = "brotli-1.2.0-cp312-cp312-win32.whl", hash = "sha256:e99befa0b48f3cd293dafeacdd0d191804d105d279e0b387a32054c1180f3161"},
+ {file = "brotli-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:b35c13ce241abdd44cb8ca70683f20c0c079728a36a996297adb5334adfc1c44"},
+ {file = "brotli-1.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9e5825ba2c9998375530504578fd4d5d1059d09621a02065d1b6bfc41a8e05ab"},
+ {file = "brotli-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0cf8c3b8ba93d496b2fae778039e2f5ecc7cff99df84df337ca31d8f2252896c"},
+ {file = "brotli-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8565e3cdc1808b1a34714b553b262c5de5fbda202285782173ec137fd13709f"},
+ {file = "brotli-1.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:26e8d3ecb0ee458a9804f47f21b74845cc823fd1bb19f02272be70774f56e2a6"},
+ {file = "brotli-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67a91c5187e1eec76a61625c77a6c8c785650f5b576ca732bd33ef58b0dff49c"},
+ {file = "brotli-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ecdb3b6dc36e6d6e14d3a1bdc6c1057c8cbf80db04031d566eb6080ce283a48"},
+ {file = "brotli-1.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3e1b35d56856f3ed326b140d3c6d9db91740f22e14b06e840fe4bb1923439a18"},
+ {file = "brotli-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54a50a9dad16b32136b2241ddea9e4df159b41247b2ce6aac0b3276a66a8f1e5"},
+ {file = "brotli-1.2.0-cp313-cp313-win32.whl", hash = "sha256:1b1d6a4efedd53671c793be6dd760fcf2107da3a52331ad9ea429edf0902f27a"},
+ {file = "brotli-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:b63daa43d82f0cdabf98dee215b375b4058cce72871fd07934f179885aad16e8"},
+ {file = "brotli-1.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6c12dad5cd04530323e723787ff762bac749a7b256a5bece32b2243dd5c27b21"},
+ {file = "brotli-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3219bd9e69868e57183316ee19c84e03e8f8b5a1d1f2667e1aa8c2f91cb061ac"},
+ {file = "brotli-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:963a08f3bebd8b75ac57661045402da15991468a621f014be54e50f53a58d19e"},
+ {file = "brotli-1.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9322b9f8656782414b37e6af884146869d46ab85158201d82bab9abbcb971dc7"},
+ {file = "brotli-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf9cba6f5b78a2071ec6fb1e7bd39acf35071d90a81231d67e92d637776a6a63"},
+ {file = "brotli-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7547369c4392b47d30a3467fe8c3330b4f2e0f7730e45e3103d7d636678a808b"},
+ {file = "brotli-1.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361"},
+ {file = "brotli-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888"},
+ {file = "brotli-1.2.0-cp314-cp314-win32.whl", hash = "sha256:832c115a020e463c2f67664560449a7bea26b0c1fdd690352addad6d0a08714d"},
+ {file = "brotli-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e7c0af964e0b4e3412a0ebf341ea26ec767fa0b4cf81abb5e897c9338b5ad6a3"},
+ {file = "brotli-1.2.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:82676c2781ecf0ab23833796062786db04648b7aae8be139f6b8065e5e7b1518"},
+ {file = "brotli-1.2.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c16ab1ef7bb55651f5836e8e62db1f711d55b82ea08c3b8083ff037157171a69"},
+ {file = "brotli-1.2.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e85190da223337a6b7431d92c799fca3e2982abd44e7b8dec69938dcc81c8e9e"},
+ {file = "brotli-1.2.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d8c05b1dfb61af28ef37624385b0029df902ca896a639881f594060b30ffc9a7"},
+ {file = "brotli-1.2.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:465a0d012b3d3e4f1d6146ea019b5c11e3e87f03d1676da1cc3833462e672fb0"},
+ {file = "brotli-1.2.0-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:96fbe82a58cdb2f872fa5d87dedc8477a12993626c446de794ea025bbda625ea"},
+ {file = "brotli-1.2.0-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:1b71754d5b6eda54d16fbbed7fce2d8bc6c052a1b91a35c320247946ee103502"},
+ {file = "brotli-1.2.0-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:66c02c187ad250513c2f4fce973ef402d22f80e0adce734ee4e4efd657b6cb64"},
+ {file = "brotli-1.2.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:ba76177fd318ab7b3b9bf6522be5e84c2ae798754b6cc028665490f6e66b5533"},
+ {file = "brotli-1.2.0-cp36-cp36m-win32.whl", hash = "sha256:c1702888c9f3383cc2f09eb3e88b8babf5965a54afb79649458ec7c3c7a63e96"},
+ {file = "brotli-1.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f8d635cafbbb0c61327f942df2e3f474dde1cff16c3cd0580564774eaba1ee13"},
+ {file = "brotli-1.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e80a28f2b150774844c8b454dd288be90d76ba6109670fe33d7ff54d96eb5cb8"},
+ {file = "brotli-1.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b1b799f45da91292ffaa21a473ab3a3054fa78560e8ff67082a185274431c8"},
+ {file = "brotli-1.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29b7e6716ee4ea0c59e3b241f682204105f7da084d6254ec61886508efeb43bc"},
+ {file = "brotli-1.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:640fe199048f24c474ec6f3eae67c48d286de12911110437a36a87d7c89573a6"},
+ {file = "brotli-1.2.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:92edab1e2fd6cd5ca605f57d4545b6599ced5dea0fd90b2bcdf8b247a12bd190"},
+ {file = "brotli-1.2.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7274942e69b17f9cef76691bcf38f2b2d4c8a5f5dba6ec10958363dcb3308a0a"},
+ {file = "brotli-1.2.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:a56ef534b66a749759ebd091c19c03ef81eb8cd96f0d1d16b59127eaf1b97a12"},
+ {file = "brotli-1.2.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:5732eff8973dd995549a18ecbd8acd692ac611c5c0bb3f59fa3541ae27b33be3"},
+ {file = "brotli-1.2.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:598e88c736f63a0efec8363f9eb34e5b5536b7b6b1821e401afcb501d881f59a"},
+ {file = "brotli-1.2.0-cp37-cp37m-win32.whl", hash = "sha256:7ad8cec81f34edf44a1c6a7edf28e7b7806dfb8886e371d95dcf789ccd4e4982"},
+ {file = "brotli-1.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:865cedc7c7c303df5fad14a57bc5db1d4f4f9b2b4d0a7523ddd206f00c121a16"},
+ {file = "brotli-1.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ac27a70bda257ae3f380ec8310b0a06680236bea547756c277b5dfe55a2452a8"},
+ {file = "brotli-1.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e813da3d2d865e9793ef681d3a6b66fa4b7c19244a45b817d0cceda67e615990"},
+ {file = "brotli-1.2.0-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fe11467c42c133f38d42289d0861b6b4f9da31e8087ca2c0d7ebb4543625526"},
+ {file = "brotli-1.2.0-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c0d6770111d1879881432f81c369de5cde6e9467be7c682a983747ec800544e2"},
+ {file = "brotli-1.2.0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:eda5a6d042c698e28bda2507a89b16555b9aa954ef1d750e1c20473481aff675"},
+ {file = "brotli-1.2.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3173e1e57cebb6d1de186e46b5680afbd82fd4301d7b2465beebe83ed317066d"},
+ {file = "brotli-1.2.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:71a66c1c9be66595d628467401d5976158c97888c2c9379c034e1e2312c5b4f5"},
+ {file = "brotli-1.2.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:1e68cdf321ad05797ee41d1d09169e09d40fdf51a725bb148bff892ce04583d7"},
+ {file = "brotli-1.2.0-cp38-cp38-win32.whl", hash = "sha256:f16dace5e4d3596eaeb8af334b4d2c820d34b8278da633ce4a00020b2eac981c"},
+ {file = "brotli-1.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:14ef29fc5f310d34fc7696426071067462c9292ed98b5ff5a27ac70a200e5470"},
+ {file = "brotli-1.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8d4f47f284bdd28629481c97b5f29ad67544fa258d9091a6ed1fda47c7347cd1"},
+ {file = "brotli-1.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2881416badd2a88a7a14d981c103a52a23a276a553a8aacc1346c2ff47c8dc17"},
+ {file = "brotli-1.2.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d39b54b968f4b49b5e845758e202b1035f948b0561ff5e6385e855c96625971"},
+ {file = "brotli-1.2.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:95db242754c21a88a79e01504912e537808504465974ebb92931cfca2510469e"},
+ {file = "brotli-1.2.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bba6e7e6cfe1e6cb6eb0b7c2736a6059461de1fa2c0ad26cf845de6c078d16c8"},
+ {file = "brotli-1.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:88ef7d55b7bcf3331572634c3fd0ed327d237ceb9be6066810d39020a3ebac7a"},
+ {file = "brotli-1.2.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:7fa18d65a213abcfbb2f6cafbb4c58863a8bd6f2103d65203c520ac117d1944b"},
+ {file = "brotli-1.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:09ac247501d1909e9ee47d309be760c89c990defbb2e0240845c892ea5ff0de4"},
+ {file = "brotli-1.2.0-cp39-cp39-win32.whl", hash = "sha256:c25332657dee6052ca470626f18349fc1fe8855a56218e19bd7a8c6ad4952c49"},
+ {file = "brotli-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:1ce223652fd4ed3eb2b7f78fbea31c52314baecfac68db44037bb4167062a937"},
+ {file = "brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a"},
]
[[package]]
name = "certifi"
-version = "2024.12.14"
+version = "2026.4.22"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
+groups = ["main", "docs", "testing"]
files = [
- {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"},
- {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"},
+ {file = "certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a"},
+ {file = "certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580"},
]
[[package]]
name = "cffi"
-version = "1.17.1"
+version = "2.0.0"
description = "Foreign Function Interface for Python calling C code."
optional = false
-python-versions = ">=3.8"
-files = [
- {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
- {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
- {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"},
- {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"},
- {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"},
- {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"},
- {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"},
- {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"},
- {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"},
- {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"},
- {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"},
- {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"},
- {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"},
- {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"},
- {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"},
- {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"},
- {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"},
- {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"},
- {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"},
- {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"},
- {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"},
- {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"},
- {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"},
- {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"},
- {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"},
- {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"},
- {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"},
- {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"},
- {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"},
- {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"},
- {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"},
- {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"},
- {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"},
- {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"},
- {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"},
- {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"},
- {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"},
- {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"},
- {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"},
- {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"},
- {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"},
- {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"},
- {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"},
- {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"},
- {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"},
- {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"},
- {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"},
- {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"},
- {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"},
- {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"},
- {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"},
- {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"},
- {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"},
- {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"},
- {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"},
- {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"},
- {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"},
- {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"},
- {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"},
- {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"},
- {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"},
- {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"},
- {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"},
- {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"},
- {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"},
- {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"},
- {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"},
+python-versions = ">=3.9"
+groups = ["main", "types"]
+markers = "platform_python_implementation != \"PyPy\""
+files = [
+ {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"},
+ {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"},
+ {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"},
+ {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"},
+ {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"},
+ {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"},
+ {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"},
+ {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"},
+ {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"},
+ {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"},
+ {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"},
+ {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"},
+ {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"},
+ {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"},
+ {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"},
+ {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"},
+ {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"},
+ {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"},
+ {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"},
+ {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"},
+ {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"},
+ {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"},
+ {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"},
+ {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"},
+ {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"},
+ {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"},
+ {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"},
+ {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"},
+ {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"},
+ {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"},
+ {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"},
+ {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"},
+ {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"},
+ {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"},
+ {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"},
+ {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"},
+ {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"},
+ {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"},
+ {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"},
+ {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"},
+ {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"},
+ {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"},
+ {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"},
+ {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"},
+ {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"},
+ {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"},
+ {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"},
+ {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"},
+ {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"},
+ {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"},
+ {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"},
+ {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"},
+ {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"},
+ {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"},
+ {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"},
+ {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"},
+ {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"},
+ {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"},
+ {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"},
+ {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"},
+ {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"},
+ {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"},
+ {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"},
+ {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"},
+ {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"},
+ {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"},
+ {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"},
+ {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"},
+ {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"},
+ {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"},
+ {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"},
+ {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"},
+ {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"},
+ {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"},
+ {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"},
+ {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"},
+ {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"},
+ {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"},
+ {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"},
+ {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"},
+ {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"},
+ {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"},
+ {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"},
+ {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"},
]
[package.dependencies]
-pycparser = "*"
+pycparser = {version = "*", markers = "implementation_name != \"PyPy\""}
[[package]]
name = "charset-normalizer"
-version = "3.4.1"
+version = "3.4.7"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false
python-versions = ">=3.7"
-files = [
- {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"},
- {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"},
- {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"},
- {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"},
- {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"},
- {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"},
- {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"},
- {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"},
- {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"},
- {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"},
- {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"},
- {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"},
- {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"},
- {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"},
- {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"},
- {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"},
- {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"},
- {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"},
- {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"},
- {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"},
- {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"},
- {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"},
- {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"},
- {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"},
- {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"},
- {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"},
- {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"},
- {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"},
- {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"},
- {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"},
- {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"},
- {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"},
- {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"},
- {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"},
- {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"},
- {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"},
- {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"},
- {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"},
- {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"},
- {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"},
- {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"},
- {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"},
- {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"},
- {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"},
- {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"},
- {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"},
- {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"},
- {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"},
- {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"},
- {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"},
- {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"},
- {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"},
- {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"},
- {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"},
- {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"},
- {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"},
- {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"},
- {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"},
- {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"},
- {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"},
- {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"},
- {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"},
- {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"},
- {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"},
- {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"},
- {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"},
- {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"},
- {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"},
- {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"},
- {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"},
- {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"},
- {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"},
- {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"},
- {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"},
- {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"},
- {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"},
- {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"},
- {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"},
- {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"},
- {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"},
- {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"},
- {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"},
- {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"},
- {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"},
- {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"},
- {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"},
- {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"},
- {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"},
- {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"},
- {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"},
- {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"},
- {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"},
+groups = ["main", "docs", "testing"]
+files = [
+ {file = "charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d"},
+ {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8"},
+ {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790"},
+ {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc"},
+ {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393"},
+ {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153"},
+ {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af"},
+ {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34"},
+ {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1"},
+ {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752"},
+ {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53"},
+ {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616"},
+ {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a"},
+ {file = "charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374"},
+ {file = "charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943"},
+ {file = "charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008"},
+ {file = "charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7"},
+ {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7"},
+ {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e"},
+ {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c"},
+ {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df"},
+ {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265"},
+ {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4"},
+ {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e"},
+ {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38"},
+ {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c"},
+ {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b"},
+ {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c"},
+ {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d"},
+ {file = "charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad"},
+ {file = "charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00"},
+ {file = "charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1"},
+ {file = "charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46"},
+ {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2"},
+ {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b"},
+ {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a"},
+ {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116"},
+ {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb"},
+ {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1"},
+ {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15"},
+ {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5"},
+ {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d"},
+ {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7"},
+ {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464"},
+ {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49"},
+ {file = "charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c"},
+ {file = "charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6"},
+ {file = "charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d"},
+ {file = "charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063"},
+ {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c"},
+ {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66"},
+ {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18"},
+ {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd"},
+ {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215"},
+ {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859"},
+ {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8"},
+ {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5"},
+ {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832"},
+ {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6"},
+ {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48"},
+ {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a"},
+ {file = "charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e"},
+ {file = "charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110"},
+ {file = "charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c"},
+ {file = "charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d"},
+ {file = "charset_normalizer-3.4.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e5f4d355f0a2b1a31bc3edec6795b46324349c9cb25eed068049e4f472fb4259"},
+ {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16d971e29578a5e97d7117866d15889a4a07befe0e87e703ed63cd90cb348c01"},
+ {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dca4bbc466a95ba9c0234ef56d7dd9509f63da22274589ebd4ed7f1f4d4c54e3"},
+ {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e80c8378d8f3d83cd3164da1ad2df9e37a666cdde7b1cb2298ed0b558064be30"},
+ {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36836d6ff945a00b88ba1e4572d721e60b5b8c98c155d465f56ad19d68f23734"},
+ {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:bd9b23791fe793e4968dba0c447e12f78e425c59fc0e3b97f6450f4781f3ee60"},
+ {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aef65cd602a6d0e0ff6f9930fcb1c8fec60dd2cfcb6facaf4bdb0e5873042db0"},
+ {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:82b271f5137d07749f7bf32f70b17ab6eaabedd297e75dce75081a24f76eb545"},
+ {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:1efde3cae86c8c273f1eb3b287be7d8499420cf2fe7585c41d370d3e790054a5"},
+ {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:c593052c465475e64bbfe5dbd81680f64a67fdc752c56d7a0ae205dc8aeefe0f"},
+ {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:af21eb4409a119e365397b2adbaca4c9ccab56543a65d5dbd9f920d6ac29f686"},
+ {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:84c018e49c3bf790f9c2771c45e9313a08c2c2a6342b162cd650258b57817706"},
+ {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dd915403e231e6b1809fe9b6d9fc55cf8fb5e02765ac625d9cd623342a7905d7"},
+ {file = "charset_normalizer-3.4.7-cp38-cp38-win32.whl", hash = "sha256:320ade88cfb846b8cd6b4ddf5ee9e80ee0c1f52401f2456b84ae1ae6a1a5f207"},
+ {file = "charset_normalizer-3.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:1dc8b0ea451d6e69735094606991f32867807881400f808a106ee1d963c46a83"},
+ {file = "charset_normalizer-3.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217"},
+ {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5"},
+ {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9"},
+ {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a"},
+ {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc"},
+ {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00"},
+ {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776"},
+ {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319"},
+ {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24"},
+ {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42"},
+ {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4"},
+ {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67"},
+ {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274"},
+ {file = "charset_normalizer-3.4.7-cp39-cp39-win32.whl", hash = "sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366"},
+ {file = "charset_normalizer-3.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444"},
+ {file = "charset_normalizer-3.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c"},
+ {file = "charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d"},
+ {file = "charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5"},
]
[[package]]
name = "click"
-version = "8.1.8"
+version = "8.3.3"
description = "Composable command line interface toolkit"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.10"
+groups = ["main", "linters"]
files = [
- {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"},
- {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"},
+ {file = "click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613"},
+ {file = "click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2"},
]
[package.dependencies]
@@ -446,22 +534,24 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "click-option-group"
-version = "0.5.6"
+version = "0.5.9"
description = "Option groups missing in Click"
optional = false
-python-versions = ">=3.6,<4"
+python-versions = ">=3.7"
+groups = ["main"]
files = [
- {file = "click-option-group-0.5.6.tar.gz", hash = "sha256:97d06703873518cc5038509443742b25069a3c7562d1ea72ff08bfadde1ce777"},
- {file = "click_option_group-0.5.6-py3-none-any.whl", hash = "sha256:38a26d963ee3ad93332ddf782f9259c5bdfe405e73408d943ef5e7d0c3767ec7"},
+ {file = "click_option_group-0.5.9-py3-none-any.whl", hash = "sha256:ad2599248bd373e2e19bec5407967c3eec1d0d4fc4a5e77b08a0481e75991080"},
+ {file = "click_option_group-0.5.9.tar.gz", hash = "sha256:f94ed2bc4cf69052e0f29592bd1e771a1789bd7bfc482dd0bc482134aff95823"},
]
[package.dependencies]
-Click = ">=7.0,<9"
+click = ">=7.0"
[package.extras]
-docs = ["Pallets-Sphinx-Themes", "m2r2", "sphinx"]
-tests = ["pytest"]
-tests-cov = ["coverage", "coveralls", "pytest", "pytest-cov"]
+dev = ["pre-commit", "pytest"]
+docs = ["m2r2", "pallets-sphinx-themes", "sphinx"]
+test = ["pytest"]
+test-cov = ["pytest", "pytest-cov"]
[[package]]
name = "colorama"
@@ -469,209 +559,443 @@ version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+groups = ["main", "docs", "linters", "testing"]
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
+markers = {main = "platform_system == \"Windows\"", docs = "sys_platform == \"win32\"", linters = "platform_system == \"Windows\" or sys_platform == \"win32\"", testing = "sys_platform == \"win32\""}
+
+[[package]]
+name = "contourpy"
+version = "1.3.3"
+description = "Python library for calculating contours of 2D quadrilateral grids"
+optional = false
+python-versions = ">=3.11"
+groups = ["main"]
+files = [
+ {file = "contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1"},
+ {file = "contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381"},
+ {file = "contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7"},
+ {file = "contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1"},
+ {file = "contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a"},
+ {file = "contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db"},
+ {file = "contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620"},
+ {file = "contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f"},
+ {file = "contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff"},
+ {file = "contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42"},
+ {file = "contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470"},
+ {file = "contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb"},
+ {file = "contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6"},
+ {file = "contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7"},
+ {file = "contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8"},
+ {file = "contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea"},
+ {file = "contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1"},
+ {file = "contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7"},
+ {file = "contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411"},
+ {file = "contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69"},
+ {file = "contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b"},
+ {file = "contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc"},
+ {file = "contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5"},
+ {file = "contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1"},
+ {file = "contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286"},
+ {file = "contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5"},
+ {file = "contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67"},
+ {file = "contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9"},
+ {file = "contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659"},
+ {file = "contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7"},
+ {file = "contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d"},
+ {file = "contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263"},
+ {file = "contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9"},
+ {file = "contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d"},
+ {file = "contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216"},
+ {file = "contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae"},
+ {file = "contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20"},
+ {file = "contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99"},
+ {file = "contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b"},
+ {file = "contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a"},
+ {file = "contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e"},
+ {file = "contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3"},
+ {file = "contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8"},
+ {file = "contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301"},
+ {file = "contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a"},
+ {file = "contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77"},
+ {file = "contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5"},
+ {file = "contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4"},
+ {file = "contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36"},
+ {file = "contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3"},
+ {file = "contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b"},
+ {file = "contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36"},
+ {file = "contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d"},
+ {file = "contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd"},
+ {file = "contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339"},
+ {file = "contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772"},
+ {file = "contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77"},
+ {file = "contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13"},
+ {file = "contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe"},
+ {file = "contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f"},
+ {file = "contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0"},
+ {file = "contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4"},
+ {file = "contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f"},
+ {file = "contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae"},
+ {file = "contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc"},
+ {file = "contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b"},
+ {file = "contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497"},
+ {file = "contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8"},
+ {file = "contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e"},
+ {file = "contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989"},
+ {file = "contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77"},
+ {file = "contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880"},
+]
+
+[package.dependencies]
+numpy = ">=1.25"
+
+[package.extras]
+bokeh = ["bokeh", "selenium"]
+docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"]
+mypy = ["bokeh", "contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.17.0)", "types-Pillow"]
+test = ["Pillow", "contourpy[test-no-images]", "matplotlib"]
+test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist", "wurlitzer"]
[[package]]
name = "coverage"
-version = "7.6.10"
+version = "7.14.0"
description = "Code coverage measurement for Python"
optional = false
-python-versions = ">=3.9"
-files = [
- {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"},
- {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"},
- {file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"},
- {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"},
- {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"},
- {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"},
- {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"},
- {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"},
- {file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"},
- {file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"},
- {file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"},
- {file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"},
- {file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"},
- {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"},
- {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"},
- {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"},
- {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"},
- {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"},
- {file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"},
- {file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"},
- {file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"},
- {file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"},
- {file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"},
- {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"},
- {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"},
- {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"},
- {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"},
- {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"},
- {file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"},
- {file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"},
- {file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"},
- {file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"},
- {file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"},
- {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"},
- {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"},
- {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"},
- {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"},
- {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"},
- {file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"},
- {file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"},
- {file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"},
- {file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"},
- {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"},
- {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"},
- {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"},
- {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"},
- {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"},
- {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"},
- {file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"},
- {file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"},
- {file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a"},
- {file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27"},
- {file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4"},
- {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f"},
- {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25"},
- {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315"},
- {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90"},
- {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d"},
- {file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18"},
- {file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59"},
- {file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"},
- {file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"},
+python-versions = ">=3.10"
+groups = ["testing"]
+files = [
+ {file = "coverage-7.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84c32d90bf4537f0e7b4dec9aaa9a938fb8205136b9d2ecf4d7629d5262dc075"},
+ {file = "coverage-7.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7c843572c605ab51cfdb5c6b5f2586e2a8467c0d28eca4bdef4ec70c5fecbd82"},
+ {file = "coverage-7.14.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0c451757d3fa2603354fdc789b5e58a0e327a117c370a40e3476ba4eabab228c"},
+ {file = "coverage-7.14.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3fd43f0616e765ab78d069cf8358def7363957a45cee446d65c502dcfeea7893"},
+ {file = "coverage-7.14.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:731e535b1498b27d13594a0527a79b0510867b0ad891532be41cb883f2128e20"},
+ {file = "coverage-7.14.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c7492f2d493b976941c7ca050f273cbda2f43c381124f7586a3e3c16d1804fec"},
+ {file = "coverage-7.14.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc38367eaa2abb1b766ac333142bce7655335a73537f5c8b75aaa89c2b987757"},
+ {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0a951308cde22cf77f953955a754d04dccb57fe3bb8e345d685778ed9fc1632a"},
+ {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fab3877e4ebb06bd9d4d4d00ee53309ee5478e66873c66a382272e3ee33eb7ea"},
+ {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b812eb847b19876ebf33fb6c4f11819af05ab6050b0bfa1bc53412ae81779adb"},
+ {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d9c8ef6ed820c433de075657d72dda1f89a2984955e58b8a75feb3f184250218"},
+ {file = "coverage-7.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d128b1bba9361fbaaf6a19e179e6cfd6a9103ce0c0555876f72780acc93efd85"},
+ {file = "coverage-7.14.0-cp310-cp310-win32.whl", hash = "sha256:65f267ca1370726ec2c1aa38bbe4df9a71a740f22878d2d4bf59d71a4cd8d323"},
+ {file = "coverage-7.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:b34ece8065914f938ed7f2c5872bb865336977a52919149846eac3744327267a"},
+ {file = "coverage-7.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a78e2a9d9c5e3b8d4ab9b9d28c985ea66fced0a7d7c2aec1f216e03a2011480"},
+ {file = "coverage-7.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1816c505187592dcd1c5a5f226601a549f70365fbd00930ac88b0c225b76bb4"},
+ {file = "coverage-7.14.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d8e1762f0e9cbc26ec315471e7b47855218e833cd5a032d706fbf43845d878c7"},
+ {file = "coverage-7.14.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9336e23e8bb3a3925398261385e2a1533957d3e760e91070dcb0e98bfa514eed"},
+ {file = "coverage-7.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd1169b2230f9cbe9c638ba38022ed7a2b1e641cc07f7cea0365e4be2a74980"},
+ {file = "coverage-7.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1bb3543b58fea74d2cd1abc4054cc927e4724687cb4560cd2ed88d2c7d820c0"},
+ {file = "coverage-7.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a93bac2cb577ef60074999ed56d8a1535894398e2ed920d4185c3ec0c8864742"},
+ {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5904abf7e18cddc463219b17552229650c6b79e061d31a1059283051169cf7d5"},
+ {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:741f57cddc9004a8c81b084660215f33a6b597dbe62c31386b983ee26310e327"},
+ {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:664123feb0929d7affc135717dbd70d61d98688a08ab1e5ba464739620c6252d"},
+ {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c83d2399a51bbec8429266905d33616f04bc5726b1138c35844d5fcd896b2e20"},
+ {file = "coverage-7.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb2e855b87321259a037429288ae85216d191c74de3e79bf57cd2bc0761992c"},
+ {file = "coverage-7.14.0-cp311-cp311-win32.whl", hash = "sha256:731dc15b385ac52289743d476245b61e1a2927e803bef655b52bc3b2a75a21f3"},
+ {file = "coverage-7.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:bfb0ed8ec5d25e93face268115d7964db9df8b9aae8edcde9ec6b16c726a7cc1"},
+ {file = "coverage-7.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:7ebb1c6df9f78046a1b1e0a89674cd4bf73b7c648914eebcf976a57fd99a5627"},
+ {file = "coverage-7.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7ffd19fc8aed057fd686a17a4935eef5f9859d69208f96310e893e64b9b6ccf5"},
+ {file = "coverage-7.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:829994cfe1aeb773ca27bf246d4badc1e764893e3bfb98fff820fcecd1ca4662"},
+ {file = "coverage-7.14.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b4f07cf7edcb7ec39431a5074d7ea83b29a9f71fcfc494f0f40af4e65180420f"},
+ {file = "coverage-7.14.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca3d9cf2c32b521bd9518385608787fa86f38daf993695307531822c3430ed67"},
+ {file = "coverage-7.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92af52828e7f29d827346b0294e5a0853fa206db77db0395b282918d41e28db9"},
+ {file = "coverage-7.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7b2bb6c9d7e769360d0f20a0f219603fd64f0c8f97de17ab25853261602be0fb"},
+ {file = "coverage-7.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c9ed6ef99f88fb8c14aa8e2bf8eb0fe55fa2edfea68f8675d78741df1a5ac0e"},
+ {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8231ade007f37959fbf58acc677f26b922c02eda6f0428ea307da0fd39681bf3"},
+ {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d8b013632cc1ce1d09dbe4f32667b4d320ec2f54fc326ebeffcd0b0bcc2bb6c4"},
+ {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1733198802d71ec4c524f322e2867ee05c62e9e75df86bdca545407a221827d1"},
+ {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:72a305291fa8ee01332f1aaf38b348ca34097f6aa0b0ef627eef2837e57bbba5"},
+ {file = "coverage-7.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcaba850dd317c65423a9d63d88f9573c53b00354d6dd95724576cc98a131595"},
+ {file = "coverage-7.14.0-cp312-cp312-win32.whl", hash = "sha256:5ac83957a80d0701310e96d8bec68cdcf4f90a7674b7d13f15a344315b41ab27"},
+ {file = "coverage-7.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:70390b0da32cb90b501953716302906e8bcce087cb283e70d8c97729f22e92b2"},
+ {file = "coverage-7.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:91b993743d959b8be85b4abf9d5478216a69329c321efe5be0433c1a841d691d"},
+ {file = "coverage-7.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f2bbb8254370eb4c628ff3d6fa8a7f74ddc40565394d4f7ab791d1fe568e37ef"},
+ {file = "coverage-7.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23b81107f46d3f21d0cbce30664fcec0f5d9f585638a67081750f99738f6bf66"},
+ {file = "coverage-7.14.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:22a7e06a5f11a757cdfe79018e9095f9f69ae283c5cd8123774c788deec8717b"},
+ {file = "coverage-7.14.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9d1aa57a1dc8e05bdc42e81c5d671d849577aeedf279f4c449d6d286f9ed88ca"},
+ {file = "coverage-7.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90c1a51bcfddf645b3bb7ec333d9e94393a8e94f55642380fa8a9a5a9e636cb7"},
+ {file = "coverage-7.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a841fae2fadcae4f438d43b6ccc4aac2ad609f47cdb6cfdce60cbb3fe5ca7bc2"},
+ {file = "coverage-7.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c79d2319cabef1fe8e86df73371126931550804738f78ad7d31e3aad85a67367"},
+ {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b23b0c6f0b1db6ad769b7050c8b641c0bf215ded26c1816955b17b7f26edfa9"},
+ {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:55d3089079ce181a4566b1065ab28d2575eb76d8ac8f81f4fcda2bf037fee087"},
+ {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:49c005cba1e2f9677fb2845dcdf9a2e72a52a17d63e8231aaaae35d9f50215ef"},
+ {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9117377b823daa28aa8635fbb08cda1cd6be3d7143257345459559aeef852d52"},
+ {file = "coverage-7.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b79d646cf46d5cf9a9f40281d4441df5849e445726e369006d2b117710b33fe"},
+ {file = "coverage-7.14.0-cp313-cp313-win32.whl", hash = "sha256:fb609b3658479e33f9516d46f1a89dbb9b6c261366e3a11844a96ec487533dae"},
+ {file = "coverage-7.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0773d8329cf32b6fd222e4b52622c61fe8d503eb966cfc8d3c3c10c96266d50e"},
+ {file = "coverage-7.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:b4e26a0f1b696faf283bffe5b8569e44e336c582439df5d53281ab89ee0cba96"},
+ {file = "coverage-7.14.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:953f521ca9445300397e65fda3dca58b2dbd68fee983777420b57ac3c77e9f90"},
+ {file = "coverage-7.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:98af83fd65ae24b1fdd03aaead967a9f523bcd2f1aab2d4f3ffda65bb568a6f1"},
+ {file = "coverage-7.14.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:668b92e6958c4db7cf92e81caac328dfbbdbb215db2850ad28f0cbe1eea0bfbd"},
+ {file = "coverage-7.14.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9fbd898551762dea00d3fef2b1c4f99afd2c6a3ff952ea07d60a9bd5ed4f34bc"},
+ {file = "coverage-7.14.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68af363c07ecd8d4b7d4043d85cb376d7d227eceb54e5323ee45da73dbd3e426"},
+ {file = "coverage-7.14.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e57054a583da8ac55edf24117ea4c9133032cfc4cf72aa2d48c1e5d4b52f899"},
+ {file = "coverage-7.14.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3499459bbcdd51a65b64c35ab7ed2764eaf3cba826e0df3f1d7fe2e102b70b"},
+ {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:45899ec2138a4346ed34d601dedf5076fb74edf2d1dd9dc76a78e82397edee90"},
+ {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8767486808c436f05b23ab98eb963fb29185e32a9357a166971685cb3459900f"},
+ {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a3b5ddfd6aa7ddad53ee3edb231e88a2151507a43229b7d71b953916deca127d"},
+ {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:63df0fe568e698e1045792399f8ab6da3a6c2dce3182813fb92afa2641087b47"},
+ {file = "coverage-7.14.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:827d6397dbd95144939b18f89edf31f63e1f99633e8d5f32f22ba8bdda567477"},
+ {file = "coverage-7.14.0-cp313-cp313t-win32.whl", hash = "sha256:7bf43e000d24012599b879791cff41589af90674722421ef11b11a5431920bab"},
+ {file = "coverage-7.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3f5549365af25d770e06b1f8f5682d9a5637d06eb494db91c6fa75d3950cc917"},
+ {file = "coverage-7.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6d160217ec6fe890f16ad3a9531761589443749e448f91986c972714fad361c8"},
+ {file = "coverage-7.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9aed9fa983514ca032790f3fe0d1c0e42ca7e16b42432af1706b50a9a46bef5d"},
+ {file = "coverage-7.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ba3b8390db29296dbbf49e91b6fe08f990743a90c8f447ba4c2ffc29670dfa63"},
+ {file = "coverage-7.14.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3a5d8e876dfa2f102e970b183863d6dedd023d3c0eeca1fe7a9787bc5f28b212"},
+ {file = "coverage-7.14.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ebb8f4614a3787d567e610bbfdf96a4798dd69a1afb1bd8ad228d4111fe6ff3"},
+ {file = "coverage-7.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b9bf47223dd8db3d4c4b2e443b02bace480d428f0822c3f991600448a176c97"},
+ {file = "coverage-7.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3485a836550b303d006d57cc06e3d5afaabc642c77050b7c985a97b13e3776b8"},
+ {file = "coverage-7.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e7e88110bae996d199d1693ca8ec3fd52441d426401ae963437598667b4c5eb"},
+ {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15228a6800ce7bdf1b74800595e56db7138cecb338fdbf044806e10dcf182dfe"},
+ {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d26ac7f5398bafc5b57421ad994e8a4749e8a7a0e62d05ec7d53014d5963bfa"},
+ {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb73254ff43c911c967a899e1359bc5049b4b115d6e8fbdde4937d0a2246cd5"},
+ {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:454a380af72c6adada298ed270d38c7a391288198dbfb8467f786f588751a90c"},
+ {file = "coverage-7.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:65c86fb646d2bd2972e96bd1a8b45817ed907cee68655d6295fe7ec031d04cca"},
+ {file = "coverage-7.14.0-cp314-cp314-win32.whl", hash = "sha256:6a6516b02a6101398e19a3f44820f69bab2590697f7def4331f668b14adaf828"},
+ {file = "coverage-7.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:45e0f79d8351fa76e256716df91eab12890d32678b9590df7ae1042e4bd4cf5d"},
+ {file = "coverage-7.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:4b899594a8b2d81e5cc064a0d7f9cac2081fed91049456cae7676787e41549c9"},
+ {file = "coverage-7.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f580f8c80acd94ac72e863efe2cab791d8c38d153e0b463b92dfa000d5c84cd1"},
+ {file = "coverage-7.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a2bd259c442cd43c49b30fbafc51776eb19ea396faf159d26a83e6a0a5f13b0c"},
+ {file = "coverage-7.14.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a706b908dfa85538863504c624b237a3cc34232bf403c057414ebfdb3b4d9f84"},
+ {file = "coverage-7.14.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7333cd944ee4393b9b3d3c1b598c936d4fc8d70573a4c7dacfec5590dd50e436"},
+ {file = "coverage-7.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f162bc9a15b82d947b02651b0c7e1609d6f7a8735ca330cfadec8481dd97d5a"},
+ {file = "coverage-7.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:362cb78e01a5dc82009d88004cf60f2e6b6d6fcbfdec05b05af73b0abf40118f"},
+ {file = "coverage-7.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:acebd068fca5512c3a6fde9c045f901613478781a73f0e82b307b214daef23fb"},
+ {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:29fe3da551dface75deb2ccbf87b6b66e2e7ef38f6d89050b428be94afff3490"},
+ {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b4cc4fce8672fffcb09b0eafc167b396b3ba53c4a7230f54b7aaffbf6c835fa9"},
+ {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5d4a51aad8ba8bdcd2b8bd8f03d4aca19693fa2327a3470e4718a25b03481020"},
+ {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9f323af3e1e4f68b60b7b247e37b8515563a61375518fa59de1af48ba28a3db6"},
+ {file = "coverage-7.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1a0abc7342ea9711c469dd8b821c6c311e6bc6aac1442e5fbd6b27fae0a8f3db"},
+ {file = "coverage-7.14.0-cp314-cp314t-win32.whl", hash = "sha256:a9f864ef57b7172e2db87a096642dd51e179e085ab6b2c371c29e885f65c8fb2"},
+ {file = "coverage-7.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29943e552fdc08e082eb51400fb2f58e118a83b5542bd06531214e084399b644"},
+ {file = "coverage-7.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:742a73ea621953b012f2c4c2219b512180dd84489acf5b1596b0aafc55b9100b"},
+ {file = "coverage-7.14.0-py3-none-any.whl", hash = "sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1"},
+ {file = "coverage-7.14.0.tar.gz", hash = "sha256:057a6af2f160a85384cde4ab36f0d2777bae1057bae255f95413cdd382aa5c74"},
]
-[package.dependencies]
-tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
-
[package.extras]
-toml = ["tomli"]
+toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
[[package]]
name = "cryptography"
-version = "44.0.0"
+version = "46.0.7"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false
-python-versions = "!=3.9.0,!=3.9.1,>=3.7"
-files = [
- {file = "cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123"},
- {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092"},
- {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f"},
- {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"},
- {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"},
- {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"},
- {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"},
- {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"},
- {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"},
- {file = "cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd"},
- {file = "cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591"},
- {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7"},
- {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc"},
- {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"},
- {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"},
- {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"},
- {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"},
- {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"},
- {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"},
- {file = "cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede"},
- {file = "cryptography-44.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731"},
- {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4"},
- {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756"},
- {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c"},
- {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa"},
- {file = "cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c"},
- {file = "cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02"},
+python-versions = "!=3.9.0,!=3.9.1,>=3.8"
+groups = ["main", "types"]
+files = [
+ {file = "cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4"},
+ {file = "cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325"},
+ {file = "cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308"},
+ {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77"},
+ {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1"},
+ {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef"},
+ {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de"},
+ {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83"},
+ {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb"},
+ {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b"},
+ {file = "cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85"},
+ {file = "cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e"},
+ {file = "cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457"},
+ {file = "cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b"},
+ {file = "cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842"},
+ {file = "cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c"},
+ {file = "cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902"},
+ {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d"},
+ {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022"},
+ {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce"},
+ {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f"},
+ {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99"},
+ {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1"},
+ {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2"},
+ {file = "cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e"},
+ {file = "cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee"},
+ {file = "cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298"},
+ {file = "cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb"},
+ {file = "cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4"},
+ {file = "cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7"},
+ {file = "cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832"},
+ {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163"},
+ {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2"},
+ {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067"},
+ {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0"},
+ {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba"},
+ {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006"},
+ {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0"},
+ {file = "cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85"},
+ {file = "cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e"},
+ {file = "cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246"},
+ {file = "cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3"},
+ {file = "cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f"},
+ {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15"},
+ {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455"},
+ {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65"},
+ {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968"},
+ {file = "cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4"},
+ {file = "cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5"},
]
[package.dependencies]
-cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
+cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""}
[package.extras]
-docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0)"]
+docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"]
docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"]
-nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"]
-pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"]
+nox = ["nox[uv] (>=2024.4.15)"]
+pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"]
sdist = ["build (>=1.0.0)"]
ssh = ["bcrypt (>=3.1.5)"]
-test = ["certifi (>=2024)", "cryptography-vectors (==44.0.0)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
+test = ["certifi (>=2024)", "cryptography-vectors (==46.0.7)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
test-randomorder = ["pytest-randomly"]
[[package]]
-name = "dill"
-version = "0.3.9"
-description = "serialize all of Python"
+name = "cycler"
+version = "0.12.1"
+description = "Composable style cycles"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
- {file = "dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a"},
- {file = "dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c"},
+ {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"},
+ {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"},
]
[package.extras]
-graph = ["objgraph (>=1.7.2)"]
-profile = ["gprof2dot (>=2022.7.29)"]
+docs = ["ipython", "matplotlib", "numpydoc", "sphinx"]
+tests = ["pytest", "pytest-cov", "pytest-xdist"]
[[package]]
-name = "docutils"
-version = "0.21.2"
-description = "Docutils -- Python Documentation Utilities"
+name = "dill"
+version = "0.4.1"
+description = "serialize all of Python"
optional = false
python-versions = ">=3.9"
+groups = ["linters"]
files = [
- {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"},
- {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"},
+ {file = "dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d"},
+ {file = "dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa"},
]
+[package.extras]
+graph = ["objgraph (>=1.7.2)"]
+profile = ["gprof2dot (>=2022.7.29)"]
+
[[package]]
-name = "exceptiongroup"
-version = "1.2.2"
-description = "Backport of PEP 654 (exception groups)"
+name = "docutils"
+version = "0.22.4"
+description = "Docutils -- Python Documentation Utilities"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.9"
+groups = ["docs"]
files = [
- {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
- {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
+ {file = "docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de"},
+ {file = "docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968"},
]
-[package.extras]
-test = ["pytest (>=6)"]
-
[[package]]
name = "filelock"
-version = "3.17.0"
+version = "3.29.0"
description = "A platform independent file lock."
optional = false
-python-versions = ">=3.9"
+python-versions = ">=3.10"
+groups = ["main"]
files = [
- {file = "filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338"},
- {file = "filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e"},
+ {file = "filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258"},
+ {file = "filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90"},
]
-[package.extras]
-docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"]
-testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"]
-typing = ["typing-extensions (>=4.12.2)"]
-
[[package]]
name = "fitparse"
version = "1.2.0"
description = "Python library to parse ANT/Garmin .FIT files"
optional = false
python-versions = "*"
+groups = ["main"]
files = [
{file = "fitparse-1.2.0.tar.gz", hash = "sha256:2d691022452dea6dabad13cc6e017ca467fe8a3a895cd3ac67a50a7bb716b4a9"},
]
[[package]]
+name = "fonttools"
+version = "4.63.0"
+description = "Tools to manipulate font files"
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "fonttools-4.63.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e3297a6a4059b4acc3a1e9a8b04741f240a80044eef08ebd32e8b5bcdddce75b"},
+ {file = "fonttools-4.63.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1cd75a03ad8cb5bc40c90bfde68c0c47de423aa19e5c0f362b43520645eea94"},
+ {file = "fonttools-4.63.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0425b277a59cff3d80ca42162a8de360f318438a2ac83570842a678d826d579"},
+ {file = "fonttools-4.63.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d7e5c9973aa04c95650c96e5f5ad865fbf42d62079163ecfab1e01cbc2504c22"},
+ {file = "fonttools-4.63.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cb014d58140a38135f16064c74c652ed57aa0b75cbf8bb59cac821f7edb5334e"},
+ {file = "fonttools-4.63.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:032038247a96c1690f9f31e377c389383c902531b085aa4e4dabd6f57f870e69"},
+ {file = "fonttools-4.63.0-cp310-cp310-win32.whl", hash = "sha256:a8b33a82979e0a6a34ff435cc81317be1f95ec1ebb7a3a2d1c8a6a54f02ae44e"},
+ {file = "fonttools-4.63.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c18358a155d75034911c5ee397a5b44cd19dd325dbb8b35fb60bf421d6a72ac"},
+ {file = "fonttools-4.63.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b8ae05d9eacf6081414d759c0a352769ac28ce31280d6bb8e77b03f9e3c449f"},
+ {file = "fonttools-4.63.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79cdc9f567aec74a72918fd060283911406750cbc9fd28c1316023deb6ce31a9"},
+ {file = "fonttools-4.63.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c14b4fd138c4bafcca294765c547914e1aa431ae1ca94ab99d8db08c958bd3b"},
+ {file = "fonttools-4.63.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76ac49f929aecaf82d83250b8347e099d7aecba0f4726c1d9b6df3b8bb5fe18"},
+ {file = "fonttools-4.63.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dcf076a4474fe0d7367e5bbf5b052c7284fa1feca729c04176ce513521afd8a0"},
+ {file = "fonttools-4.63.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7dd683fef0663e9f0f45cf541d788d24caa3ec9db50796b588e1757d8b3bc007"},
+ {file = "fonttools-4.63.0-cp311-cp311-win32.whl", hash = "sha256:afefc1ed0a59785a7fb06ea7e1678e849c193e1e387db783579bc7b3056fcfcb"},
+ {file = "fonttools-4.63.0-cp311-cp311-win_amd64.whl", hash = "sha256:063e08bd17bd5a90127a14123de0d6a952dbc847695fd98b63c043d58057f90c"},
+ {file = "fonttools-4.63.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02"},
+ {file = "fonttools-4.63.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9faff9e0c1f76f9fd55899d2ce785832efebab37eb8ae13995853aef178bef0"},
+ {file = "fonttools-4.63.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af"},
+ {file = "fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8"},
+ {file = "fonttools-4.63.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b"},
+ {file = "fonttools-4.63.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:27fdc65af8da6f88b9c6121c47a464cbe359fcfff7ff6fc2d37a1f395d755b78"},
+ {file = "fonttools-4.63.0-cp312-cp312-win32.whl", hash = "sha256:af2fd1664d00a397d75f806985ddb36282091c2131a73a6485c23b4a34722263"},
+ {file = "fonttools-4.63.0-cp312-cp312-win_amd64.whl", hash = "sha256:59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272"},
+ {file = "fonttools-4.63.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd"},
+ {file = "fonttools-4.63.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59"},
+ {file = "fonttools-4.63.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d"},
+ {file = "fonttools-4.63.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68"},
+ {file = "fonttools-4.63.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be"},
+ {file = "fonttools-4.63.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27"},
+ {file = "fonttools-4.63.0-cp313-cp313-win32.whl", hash = "sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380"},
+ {file = "fonttools-4.63.0-cp313-cp313-win_amd64.whl", hash = "sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b"},
+ {file = "fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745"},
+ {file = "fonttools-4.63.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6e528da43bc3791085f8cb6141b1d13e459226790240340fcbb4625649238b03"},
+ {file = "fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b2248c5decb223562f7902ff6325077a073f608ee8e33e88ad88db734eb9f49"},
+ {file = "fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b"},
+ {file = "fonttools-4.63.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bf00f21eb5fb721dbaf73d1e9da6d02a1af7768f2ebcf9798be98beab8ba90f6"},
+ {file = "fonttools-4.63.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c1aaa4b9c75798400ac043ce04d74e7830376c85095a5a6ed7cba2f17a266bf4"},
+ {file = "fonttools-4.63.0-cp314-cp314-win32.whl", hash = "sha256:22693918177bd9ceabec4736d338045f357769416fc6b0b2508eefef75b08616"},
+ {file = "fonttools-4.63.0-cp314-cp314-win_amd64.whl", hash = "sha256:7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5"},
+ {file = "fonttools-4.63.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6db5140a60a5d731d21ec076745b40a310607731b0a565b50776393188649001"},
+ {file = "fonttools-4.63.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d76edbff9014094dbf03bd2d074709dfa6ec7aba13d838c937a2b33d2d6a86e"},
+ {file = "fonttools-4.63.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eac00b9118c3c2f87d272e45341871c5b3066baa3c86897fa634a7c3fb59096"},
+ {file = "fonttools-4.63.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51394295f1a51de8b5f30bdb1e1b9a4231536c7064ef5c6e211eec19fa36036f"},
+ {file = "fonttools-4.63.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9e12f105d2b6342c559c298afb674006bb2893afc7102dcf8a1b55b0486b4e40"},
+ {file = "fonttools-4.63.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:796f27556dbe094c4824f75ca85267e4df776c79036c8441469a4df37038c196"},
+ {file = "fonttools-4.63.0-cp314-cp314t-win32.whl", hash = "sha256:948428a275741f0b64b113c955425a953314f4b9ab9997f73a72c83e68e569c8"},
+ {file = "fonttools-4.63.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6d4741eb179121cab9eea4cb2393d24492373a260d7945006358c08cfbf45419"},
+ {file = "fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d"},
+ {file = "fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0"},
+]
+
+[package.extras]
+all = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\"", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.45.0)", "unicodedata2 (>=17.0.0) ; python_version <= \"3.14\"", "xattr ; sys_platform == \"darwin\"", "zopfli (>=0.1.4)"]
+graphite = ["lz4 (>=1.7.4.2)"]
+interpolatable = ["munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\""]
+lxml = ["lxml (>=4.0)"]
+pathops = ["skia-pathops (>=0.5.0)"]
+plot = ["matplotlib"]
+repacker = ["uharfbuzz (>=0.45.0)"]
+symfont = ["sympy"]
+type1 = ["xattr ; sys_platform == \"darwin\""]
+unicode = ["unicodedata2 (>=17.0.0) ; python_version <= \"3.14\""]
+woff = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "zopfli (>=0.1.4)"]
+
+[[package]]
name = "gpxpy"
version = "1.6.2"
description = "GPX file parser and GPS track manipulation library"
optional = false
python-versions = ">=3.6"
+groups = ["main"]
files = [
{file = "gpxpy-1.6.2-py3-none-any.whl", hash = "sha256:289bc2d80f116c988d0a1e763fda22838f83005573ece2bbc6521817b26fb40a"},
{file = "gpxpy-1.6.2.tar.gz", hash = "sha256:a72c484b97ec42b80834353b029cc8ee1b79f0ffca1179b2210bb3baf26c01ae"},
@@ -679,89 +1003,94 @@ files = [
[[package]]
name = "greenlet"
-version = "3.1.1"
+version = "3.5.0"
description = "Lightweight in-process concurrent programming"
optional = false
-python-versions = ">=3.7"
-files = [
- {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"},
- {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"},
- {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0"},
- {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120"},
- {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc"},
- {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617"},
- {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7"},
- {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6"},
- {file = "greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80"},
- {file = "greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70"},
- {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159"},
- {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e"},
- {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1"},
- {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383"},
- {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a"},
- {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511"},
- {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395"},
- {file = "greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39"},
- {file = "greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d"},
- {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79"},
- {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa"},
- {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441"},
- {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36"},
- {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9"},
- {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0"},
- {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942"},
- {file = "greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01"},
- {file = "greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1"},
- {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff"},
- {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a"},
- {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e"},
- {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4"},
- {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e"},
- {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1"},
- {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c"},
- {file = "greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761"},
- {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011"},
- {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13"},
- {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475"},
- {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b"},
- {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822"},
- {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01"},
- {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6"},
- {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47da355d8687fd65240c364c90a31569a133b7b60de111c255ef5b606f2ae291"},
- {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98884ecf2ffb7d7fe6bd517e8eb99d31ff7855a840fa6d0d63cd07c037f6a981"},
- {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1d4aeb8891338e60d1ab6127af1fe45def5259def8094b9c7e34690c8858803"},
- {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db32b5348615a04b82240cc67983cb315309e88d444a288934ee6ceaebcad6cc"},
- {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcc62f31eae24de7f8dce72134c8651c58000d3b1868e01392baea7c32c247de"},
- {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1d3755bcb2e02de341c55b4fca7a745a24a9e7212ac953f6b3a48d117d7257aa"},
- {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b8da394b34370874b4572676f36acabac172602abf054cbc4ac910219f3340af"},
- {file = "greenlet-3.1.1-cp37-cp37m-win32.whl", hash = "sha256:a0dfc6c143b519113354e780a50381508139b07d2177cb6ad6a08278ec655798"},
- {file = "greenlet-3.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:54558ea205654b50c438029505def3834e80f0869a70fb15b871c29b4575ddef"},
- {file = "greenlet-3.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9"},
- {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111"},
- {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81"},
- {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba"},
- {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8"},
- {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1"},
- {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd"},
- {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7"},
- {file = "greenlet-3.1.1-cp38-cp38-win32.whl", hash = "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef"},
- {file = "greenlet-3.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d"},
- {file = "greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3"},
- {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42"},
- {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f"},
- {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437"},
- {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145"},
- {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c"},
- {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e"},
- {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e"},
- {file = "greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c"},
- {file = "greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22"},
- {file = "greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467"},
-]
+python-versions = ">=3.10"
+groups = ["main", "testing"]
+files = [
+ {file = "greenlet-3.5.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:29ea813b2e1f45fa9649a17853b2b5465c4072fbcb072e5af6cd3a288216574a"},
+ {file = "greenlet-3.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:804a70b328e706b785c6ef16187051c394a63dd1a906d89be24b6ad77759f13f"},
+ {file = "greenlet-3.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:884f649de075b84739713d41dd4dfd41e2b910bfb769c4a3ea02ec1da52cd9bb"},
+ {file = "greenlet-3.5.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4d0eadc7e4d9ffb2af4247b606cae307be8e448911e5a0d0b16d72fc3d224cfd"},
+ {file = "greenlet-3.5.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b28037cb07768933c54d81bfe47a85f9f402f57d7d69743b991a713b63954eb"},
+ {file = "greenlet-3.5.0-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:f8c30c2225f40dd76c50790f0eb3b5c7c18431efb299e2782083e1981feed243"},
+ {file = "greenlet-3.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cda05425526240807408156b6960a17a79a0c760b813573b67027823be760977"},
+ {file = "greenlet-3.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9c615f869163e14bb1ced20322d8038fb680b08236521ac3f30cd4c1288785a0"},
+ {file = "greenlet-3.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:ba8f0bdc2fae6ce915dfd0c16d2d00bca7e4247c1eae4416e06430e522137858"},
+ {file = "greenlet-3.5.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8f1cc966c126639cd152fdaa52624d2655f492faa79e013fea161de3e6dda082"},
+ {file = "greenlet-3.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:362624e6a8e5bca3b8233e45eef33903a100e9539a2b995c364d595dbc4018b3"},
+ {file = "greenlet-3.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5ecd83806b0f4c2f53b1018e0005cd82269ea01d42befc0368730028d850ed1c"},
+ {file = "greenlet-3.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa94cb2288681e3a11645958f1871d48ee9211bd2f66628fdace505927d6e564"},
+ {file = "greenlet-3.5.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ff251e9a0279522e62f6176412869395a64ddf2b5c5f782ff609a8216a4e662"},
+ {file = "greenlet-3.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:64d6ac45f7271f48e45f67c95b54ef73534c52ec041fcda8edf520c6d811f4bc"},
+ {file = "greenlet-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d874e79afd41a96e11ff4c5d0bc90a80973e476fda1c2c64985667397df432b"},
+ {file = "greenlet-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0ed006e4b86c59de7467eb2601cd1b77b5a7d657d1ee55e30fe30d76451edba4"},
+ {file = "greenlet-3.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:703cb211b820dbffbbc55a16bfc6e4583a6e6e990f33a119d2cc8b83211119c8"},
+ {file = "greenlet-3.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:6c18dfb59c70f5a94acd271c72e90128c3c776e41e5f07767908c8c1b74ad339"},
+ {file = "greenlet-3.5.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:db2910d3c809444e0a20147361f343fe2798e106af8d9d8506f5305302655a9f"},
+ {file = "greenlet-3.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ec9ea74e7268ace7f9aab1b1a4e730193fc661b39a993cd91c606c32d4a3628"},
+ {file = "greenlet-3.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54d243512da35485fc7a6bf3c178fdda6327a9d6506fcdd62b1abd1e41b2927b"},
+ {file = "greenlet-3.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:41353ec2ecedf7aa8f682753a41919f8718031a6edac46b8d3dc7ed9e1ceb136"},
+ {file = "greenlet-3.5.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d280a7f5c331622c69f97eb167f33577ff2d1df282c41cd15907fc0a3ca198c"},
+ {file = "greenlet-3.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:58c1c374fe2b3d852f9b6b11a7dff4c85404e51b9a596fd9e89cf904eb09866d"},
+ {file = "greenlet-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1eb67d5adefb5bd2e182d42678a328979a209e4e82eb93575708185d31d1f588"},
+ {file = "greenlet-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2628d6c86f6cb0cb45e0c3c54058bbec559f57eaae699447748cb3928150577e"},
+ {file = "greenlet-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:d4d9f0624c775f2dfc56ba54d515a8c771044346852a918b405914f6b19d7fd8"},
+ {file = "greenlet-3.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:83ed9f27f1680b50e89f40f6df348a290ea234b249a4003d366663a12eab94f2"},
+ {file = "greenlet-3.5.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:5a5ed18de6a0f6cc7087f1563f6bd93fc7df1c19165ca01e9bde5a5dc281d106"},
+ {file = "greenlet-3.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a717fbc46d8a354fa675f7c1e813485b6ba3885f9bef0cd56e5ba27d758ff5b"},
+ {file = "greenlet-3.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ddc090c5c1792b10246a78e8c2163ebbe04cf877f9d785c230a7b27b39ad038e"},
+ {file = "greenlet-3.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4964101b8585c144cbda5532b1aa644255126c08a265dae90c16e7a0e63aaa9d"},
+ {file = "greenlet-3.5.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2094acd54b272cb6eae8c03dd87b3fa1820a4cef18d6889c378d503500a1dc13"},
+ {file = "greenlet-3.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:7022615368890680e67b9965d33f5773aade330d5343bbe25560135aaa849eae"},
+ {file = "greenlet-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5e05ba267789ea87b5a155cf0e810b1ab88bf18e9e8740813945ceb8ee4350ba"},
+ {file = "greenlet-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0ecec963079cd58cbd14723582384f11f166fd58883c15dcbfb342e0bc9b5846"},
+ {file = "greenlet-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:728d9667d8f2f586644b748dbd9bb67e50d6a9381767d1357714ea6825bb3bf5"},
+ {file = "greenlet-3.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:47422135b1d308c14b2c6e758beedb1acd33bb91679f5670edf77bf46244722b"},
+ {file = "greenlet-3.5.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:f35807464c4c58c55f0d31dfa83c541a5615d825c2fe3d2b95360cf7c4e3c0a8"},
+ {file = "greenlet-3.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55fa7ea52771be44af0de27d8b80c02cd18c2c3cddde6c847ecebdf72418b6a1"},
+ {file = "greenlet-3.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a97e4821aa710603f94de0da25f25096454d78ffdace5dc77f3a006bc01abba3"},
+ {file = "greenlet-3.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf2d8a80bec89ab46221ae45c5373d5ba0bd36c19aa8508e85c6cd7e5106cd37"},
+ {file = "greenlet-3.5.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f52a464e4ed91780bdfbbdd2b97197f3accaa629b98c200f4dffada759f3ae7"},
+ {file = "greenlet-3.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:1bae92a1dd94c5f9d9493c3a212dd874c202442047cf96446412c862feca83a2"},
+ {file = "greenlet-3.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:762612baf1161ccb8437c0161c668a688223cba28e1bf038f4eb47b13e39ccdf"},
+ {file = "greenlet-3.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:57a43c6079a89713522bc4bcb9f75070ecf5d3dbad7792bfe42239362cbf2a16"},
+ {file = "greenlet-3.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bc59be3945ae9750b9e7d45067d01ae3fe90ea5f9ade99239dabdd6e28a5033"},
+ {file = "greenlet-3.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:a96fcee45e03fe30a62669fd16ab5c9d3c172660d3085605cb1e2d1280d3c988"},
+ {file = "greenlet-3.5.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:a10a732421ab4fec934783ce3e54763470d0181db6e3468f9103a275c3ed1853"},
+ {file = "greenlet-3.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fc391b1566f2907d17aaebe78f8855dc45675159a775fcf9e61f8ee0078e87f"},
+ {file = "greenlet-3.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:680bd0e7ad5e8daa8a4aa89f68fd6adc834b8a8036dc256533f7e08f4a4b01f7"},
+ {file = "greenlet-3.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1aa4ce8debcd4ea7fb2e150f3036588c41493d1d52c43538924ae1819003f4ce"},
+ {file = "greenlet-3.5.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddb36c7d6c9c0a65f18c7258634e0c416c6ab59caac8c987b96f80c2ebda0112"},
+ {file = "greenlet-3.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:728a73687e39ae9ca34e4694cbf2f049d3fbc7174639468d0f67200a97d8f9e2"},
+ {file = "greenlet-3.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e5ddf316ced87539144621453c3aef229575825fe60c604e62bedc4003f372b2"},
+ {file = "greenlet-3.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4a448128607be0de65342dc9b31be7f948ef4cc0bc8832069350abefd310a8f2"},
+ {file = "greenlet-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d60097128cb0a1cab9ea541186ea13cd7b847b8449a7787c2e2350da0cb82d86"},
+ {file = "greenlet-3.5.0.tar.gz", hash = "sha256:d419647372241bc68e957bf38d5c1f98852155e4146bd1e4121adea81f4f01e4"},
+]
+markers = {main = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""}
[package.extras]
docs = ["Sphinx", "furo"]
-test = ["objgraph", "psutil"]
+test = ["objgraph", "psutil", "setuptools"]
+
+[[package]]
+name = "hittekaart-py"
+version = "0.2.0"
+description = ""
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = []
+develop = false
+
+[package.source]
+type = "git"
+url = "https://gitlab.com/dunj3/hittekaart"
+reference = "9a576a8d3a8cb1d143be0ade0310f71b490cacb9"
+resolved_reference = "9a576a8d3a8cb1d143be0ade0310f71b490cacb9"
+subdirectory = "hittekaart-py"
[[package]]
name = "hupper"
@@ -769,6 +1098,7 @@ version = "1.12.1"
description = "Integrated process monitor for developing and reloading daemons."
optional = false
python-versions = ">=3.7"
+groups = ["main"]
files = [
{file = "hupper-1.12.1-py3-none-any.whl", hash = "sha256:e872b959f09d90be5fb615bd2e62de89a0b57efc037bdf9637fb09cdf8552b19"},
{file = "hupper-1.12.1.tar.gz", hash = "sha256:06bf54170ff4ecf4c84ad5f188dee3901173ab449c2608ad05b9bfd6b13e32eb"},
@@ -780,64 +1110,82 @@ testing = ["mock", "pytest", "pytest-cov", "watchdog"]
[[package]]
name = "idna"
-version = "3.10"
+version = "3.15"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.8"
+groups = ["main", "docs", "testing"]
files = [
- {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
- {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
+ {file = "idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8"},
+ {file = "idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc"},
]
[package.extras]
-all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
+all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]]
name = "imagesize"
-version = "1.4.1"
+version = "1.5.0"
description = "Getting image size from png/jpeg/jpeg2000/gif file"
optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
+groups = ["docs"]
+markers = "python_version >= \"3.15\""
files = [
- {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"},
- {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"},
+ {file = "imagesize-1.5.0-py2.py3-none-any.whl", hash = "sha256:32677681b3f434c2cb496f00e89c5a291247b35b1f527589909e008057da5899"},
+ {file = "imagesize-1.5.0.tar.gz", hash = "sha256:8bfc5363a7f2133a89f0098451e0bcb1cd71aba4dc02bbcecb39d99d40e1b94f"},
]
[[package]]
-name = "iniconfig"
+name = "imagesize"
version = "2.0.0"
+description = "Get image size from headers (BMP/PNG/JPEG/JPEG2000/GIF/TIFF/SVG/Netpbm/WebP/AVIF/HEIC/HEIF)"
+optional = false
+python-versions = "<3.15,>=3.10"
+groups = ["docs"]
+markers = "python_version < \"3.15\""
+files = [
+ {file = "imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96"},
+ {file = "imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3"},
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
description = "brain-dead simple config-ini parsing"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.10"
+groups = ["testing"]
files = [
- {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
- {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
+ {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"},
+ {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"},
]
[[package]]
name = "isort"
-version = "6.0.0"
+version = "8.0.1"
description = "A Python utility / library to sort Python imports."
optional = false
-python-versions = ">=3.9.0"
+python-versions = ">=3.10.0"
+groups = ["linters"]
files = [
- {file = "isort-6.0.0-py3-none-any.whl", hash = "sha256:567954102bb47bb12e0fae62606570faacddd441e45683968c8d1734fb1af892"},
- {file = "isort-6.0.0.tar.gz", hash = "sha256:75d9d8a1438a9432a7d7b54f2d3b45cad9a4a0fdba43617d9873379704a8bdf1"},
+ {file = "isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75"},
+ {file = "isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d"},
]
[package.extras]
colors = ["colorama"]
-plugins = ["setuptools"]
[[package]]
name = "jinja2"
-version = "3.1.5"
+version = "3.1.6"
description = "A very fast and expressive template engine."
optional = false
python-versions = ">=3.7"
+groups = ["main", "docs"]
files = [
- {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"},
- {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"},
+ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"},
+ {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"},
]
[package.dependencies]
@@ -847,25 +1195,256 @@ MarkupSafe = ">=2.0"
i18n = ["Babel (>=2.7)"]
[[package]]
-name = "legacy-cgi"
-version = "2.6.2"
-description = "Fork of the standard library cgi and cgitb modules, being deprecated in PEP-594"
+name = "kiwisolver"
+version = "1.5.0"
+description = "A fast implementation of the Cassowary constraint solver"
optional = false
python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "kiwisolver-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32cc0a5365239a6ea0c6ed461e8838d053b57e397443c0ca894dcc8e388d4374"},
+ {file = "kiwisolver-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc0b66c1eec9021353a4b4483afb12dfd50e3669ffbb9152d6842eb34c7e29fd"},
+ {file = "kiwisolver-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86e0287879f75621ae85197b0877ed2f8b7aa57b511c7331dce2eb6f4de7d476"},
+ {file = "kiwisolver-1.5.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:62f59da443c4f4849f73a51a193b1d9d258dcad0c41bc4d1b8fb2bcc04bfeb22"},
+ {file = "kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9190426b7aa26c5229501fa297b8d0653cfd3f5a36f7990c264e157cbf886b3b"},
+ {file = "kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c8277104ded0a51e699c8c3aff63ce2c56d4ed5519a5f73e0fd7057f959a2b9e"},
+ {file = "kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8f9baf6f0a6e7571c45c8863010b45e837c3ee1c2c77fcd6ef423be91b21fedb"},
+ {file = "kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cff8e5383db4989311f99e814feeb90c4723eb4edca425b9d5d9c3fefcdd9537"},
+ {file = "kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ebae99ed6764f2b5771c522477b311be313e8841d2e0376db2b10922daebbba4"},
+ {file = "kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d5cd5189fc2b6a538b75ae45433140c4823463918f7b1617c31e68b085c0022c"},
+ {file = "kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f42c23db5d1521218a3276bb08666dcb662896a0be7347cba864eca45ff64ede"},
+ {file = "kiwisolver-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:94eff26096eb5395136634622515b234ecb6c9979824c1f5004c6e3c3c85ccd2"},
+ {file = "kiwisolver-1.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:dd952e03bfbb096cfe2dd35cd9e00f269969b67536cb4370994afc20ff2d0875"},
+ {file = "kiwisolver-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c"},
+ {file = "kiwisolver-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb"},
+ {file = "kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac"},
+ {file = "kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27"},
+ {file = "kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398"},
+ {file = "kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db"},
+ {file = "kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc"},
+ {file = "kiwisolver-1.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679"},
+ {file = "kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309"},
+ {file = "kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2"},
+ {file = "kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c"},
+ {file = "kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08"},
+ {file = "kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4"},
+ {file = "kiwisolver-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b"},
+ {file = "kiwisolver-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac"},
+ {file = "kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9"},
+ {file = "kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588"},
+ {file = "kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819"},
+ {file = "kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f"},
+ {file = "kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf"},
+ {file = "kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d"},
+ {file = "kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083"},
+ {file = "kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6"},
+ {file = "kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1"},
+ {file = "kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0"},
+ {file = "kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15"},
+ {file = "kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314"},
+ {file = "kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9"},
+ {file = "kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384"},
+ {file = "kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7"},
+ {file = "kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09"},
+ {file = "kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3"},
+ {file = "kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd"},
+ {file = "kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3"},
+ {file = "kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96"},
+ {file = "kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099"},
+ {file = "kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8"},
+ {file = "kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87"},
+ {file = "kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23"},
+ {file = "kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859"},
+ {file = "kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902"},
+ {file = "kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167"},
+ {file = "kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0"},
+ {file = "kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276"},
+ {file = "kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c"},
+ {file = "kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1"},
+ {file = "kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e"},
+ {file = "kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7"},
+ {file = "kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c"},
+ {file = "kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368"},
+ {file = "kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489"},
+ {file = "kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1"},
+ {file = "kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3"},
+ {file = "kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18"},
+ {file = "kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021"},
+ {file = "kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310"},
+ {file = "kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3"},
+ {file = "kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2"},
+ {file = "kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53"},
+ {file = "kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615"},
+ {file = "kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02"},
+ {file = "kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e"},
+ {file = "kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac"},
+ {file = "kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05"},
+ {file = "kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd"},
+ {file = "kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a"},
+ {file = "kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554"},
+ {file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581"},
+ {file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303"},
+ {file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9"},
+ {file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79"},
+ {file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796"},
+ {file = "kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e"},
+ {file = "kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df"},
+ {file = "kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e"},
+ {file = "kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4"},
+ {file = "kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028"},
+ {file = "kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657"},
+ {file = "kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920"},
+ {file = "kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9"},
+ {file = "kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d"},
+ {file = "kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65"},
+ {file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa"},
+ {file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0"},
+ {file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9"},
+ {file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f"},
+ {file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646"},
+ {file = "kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681"},
+ {file = "kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57"},
+ {file = "kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797"},
+ {file = "kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203"},
+ {file = "kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7"},
+ {file = "kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57"},
+ {file = "kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:295d9ffe712caa9f8a3081de8d32fc60191b4b51c76f02f951fd8407253528f4"},
+ {file = "kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:51e8c4084897de9f05898c2c2a39af6318044ae969d46ff7a34ed3f96274adca"},
+ {file = "kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b83af57bdddef03c01a9138034c6ff03181a3028d9a1003b301eb1a55e161a3f"},
+ {file = "kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf4679a3d71012a7c2bf360e5cd878fbd5e4fcac0896b56393dec239d81529ed"},
+ {file = "kiwisolver-1.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:41024ed50e44ab1a60d3fe0a9d15a4ccc9f5f2b1d814ff283c8d01134d5b81bc"},
+ {file = "kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232"},
+ {file = "kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a"},
+ {file = "kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737"},
+ {file = "kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16"},
+ {file = "kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1"},
+ {file = "kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a"},
+]
+
+[[package]]
+name = "legacy-cgi"
+version = "2.6.4"
+description = "Fork of the standard library cgi and cgitb modules removed in Python 3.13"
+optional = false
+python-versions = ">=3.8"
+groups = ["main", "testing"]
+markers = "python_version >= \"3.13\""
files = [
- {file = "legacy_cgi-2.6.2-py3-none-any.whl", hash = "sha256:a7b83afb1baf6ebeb56522537c5943ef9813cf933f6715e88a803f7edbce0bff"},
- {file = "legacy_cgi-2.6.2.tar.gz", hash = "sha256:9952471ceb304043b104c22d00b4f333cac27a6abe446d8a528fc437cf13c85f"},
+ {file = "legacy_cgi-2.6.4-py3-none-any.whl", hash = "sha256:7e235ce58bf1e25d1fc9b2d299015e4e2cd37305eccafec1e6bac3fc04b878cd"},
+ {file = "legacy_cgi-2.6.4.tar.gz", hash = "sha256:abb9dfc7835772f7c9317977c63253fd22a7484b5c9bbcdca60a29dcce97c577"},
+]
+
+[[package]]
+name = "librt"
+version = "0.11.0"
+description = "Mypyc runtime library"
+optional = false
+python-versions = ">=3.9"
+groups = ["types"]
+markers = "platform_python_implementation != \"PyPy\""
+files = [
+ {file = "librt-0.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e94ebfcfa2d5e9926d6c3b9aa4617ffc42a845b4321fb84021b872358c82a0f"},
+ {file = "librt-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae627397a2f351560440d872d6f7c8dbb4072e57868e7b2fc5b8b430fe489d45"},
+ {file = "librt-0.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc329359321b67d24efdf4bc69012b0597001649544db662c001db5a0184794c"},
+ {file = "librt-0.11.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:7e82e642ab0f7608ce2fe53d76ca2280a9ee33a1b06556142c7c6fe80a86fc33"},
+ {file = "librt-0.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88145c15c67731d54283d135b03244028c750cc9edc334a96a4f5950ebdb2884"},
+ {file = "librt-0.11.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d36a51b3d93320b686588e27123f4995804dbf1bce81df78c02fc3c6eea9280"},
+ {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3ac06a2a8b246327f11e186a53a100a4d5c7ed52346367e5ec751d51586c"},
+ {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:461bbceede621f1ffb8839755f8663e886087ee7af16294cab7fb4d782c62eeb"},
+ {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0cad8a4d6a8ff03c9b76f9414caccd78e7cfbc8a2e12fa334d8e1d9932753783"},
+ {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f37aa505b3cf60701562eddb32df74b12a9e380c207fd8b06dd157a943ac7ea0"},
+ {file = "librt-0.11.0-cp310-cp310-win32.whl", hash = "sha256:94663a21534637f0e787ec2a2a756022df6e5b7b2335a5cdd7d8e33d68a2af89"},
+ {file = "librt-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:dec7db73758c2b54953fd8b7fe348c45188fe26b39ee18446196edd08453a5d4"},
+ {file = "librt-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93d95bd45b7d58343d8b90d904450a545144eec19a002511163426f8ab1fae29"},
+ {file = "librt-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ee278c769a713638cdacd4c0436d72156e75df3ebc0166ab2b9dc43acc386c9"},
+ {file = "librt-0.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f230cb1cbc9faaa616f9a678f530ebcf186e414b6bcbd88b960e4ba1b92428d5"},
+ {file = "librt-0.11.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5d63c855d86938d9de93e265c9bd8c705b51ec494de5738340ee93767a686e4b"},
+ {file = "librt-0.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f028be9e96a08d31df3479ac80d99be374d17f3b78e4796b3fd3c913d4e89"},
+ {file = "librt-0.11.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:258d73a0aa66a055e65b2e4d1b8cdb23b9d132c5bb915d9547d804fcaed116cc"},
+ {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0827efe7854718f04aaddf6496e96960a956e676fe1d0f04eb41511fd8ad06d5"},
+ {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7753e57d6e12d019c0d8786f1c09c709f4c3fcc57c3887b24e36e6c06ec938b7"},
+ {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11bd19822431cc21af9f27374e7ae2e58103c7d98bda823536a6c47f6bb2bb3d"},
+ {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:22bdf239b219d3993761a148ffa134b19e52e9989c84f845d5d7b71d70a17412"},
+ {file = "librt-0.11.0-cp311-cp311-win32.whl", hash = "sha256:46c60b61e308eb535fbd6fa622b1ee1bb2815691c1ad9c98bf7b84952ec3bc8d"},
+ {file = "librt-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:902e546ff044f579ff1c953ff5fce97b636fe9e3943996b2177710c6ef076f73"},
+ {file = "librt-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:65ac3bc20f78aa0ee5ae84baa68917f89fef4af63e941084dd019a0d0e749f0c"},
+ {file = "librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46"},
+ {file = "librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3"},
+ {file = "librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67"},
+ {file = "librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a"},
+ {file = "librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a"},
+ {file = "librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f"},
+ {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b"},
+ {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766"},
+ {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d"},
+ {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8"},
+ {file = "librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a"},
+ {file = "librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9"},
+ {file = "librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c"},
+ {file = "librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894"},
+ {file = "librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c"},
+ {file = "librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea"},
+ {file = "librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230"},
+ {file = "librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2"},
+ {file = "librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3"},
+ {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21"},
+ {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930"},
+ {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be"},
+ {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e"},
+ {file = "librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e"},
+ {file = "librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47"},
+ {file = "librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44"},
+ {file = "librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd"},
+ {file = "librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4"},
+ {file = "librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8"},
+ {file = "librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b"},
+ {file = "librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175"},
+ {file = "librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03"},
+ {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c"},
+ {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3"},
+ {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96"},
+ {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe"},
+ {file = "librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f"},
+ {file = "librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7"},
+ {file = "librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1"},
+ {file = "librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72"},
+ {file = "librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa"},
+ {file = "librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548"},
+ {file = "librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2"},
+ {file = "librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f"},
+ {file = "librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51"},
+ {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2"},
+ {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085"},
+ {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3"},
+ {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd"},
+ {file = "librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8"},
+ {file = "librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c"},
+ {file = "librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253"},
+ {file = "librt-0.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6bd72d903911d995ab666dbd1871f8b1e80925a699af8063fbf50053329fb05f"},
+ {file = "librt-0.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ef69ac715f3cd8e5cd252cb2aebfa72c015492aacc339d5d7bf8fef3c62c677"},
+ {file = "librt-0.11.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:624a40c4a4ad7773315c287276cd024509b2c66ff5904f504bfc08d2c70293ab"},
+ {file = "librt-0.11.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:41dc19fe150b69716c8ece4f76773a9e8813fe3e35e032a58b4d46423fb8d7c0"},
+ {file = "librt-0.11.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e8bd98ea9c47ae90b319a087ab28dac493f1ffbc1ecd1f28fcdbf3b7e1108d1"},
+ {file = "librt-0.11.0-cp39-cp39-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84308fc49423ce6475d1c5d1985cd69a8ca9f0325fc7d5f81bb690a3f3625d4e"},
+ {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ff0fbaf5f44a21beeb0110f2ab64f45135a9536a834b79c0d1ef018f2786bbfa"},
+ {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9c028a9442a18e266955d364ce42259136e79a7ba14d773e0d778d5f70cd56f1"},
+ {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:9f1692105a02bcf853f355032a5fdc5494358ef83d8fd22d16de375c85cec3f5"},
+ {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7a80a71e1fda83cc752a9141e87aae7fef279538597564d670e9ce513f286192"},
+ {file = "librt-0.11.0-cp39-cp39-win32.whl", hash = "sha256:140695816ddf3c86eb972981a26f35efd871c44b0c3aed44c8cd01749386617f"},
+ {file = "librt-0.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:92f7ff819c197fc30473190a12c2856f325ac90aabfccbeb2072d28cc2e234e3"},
+ {file = "librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1"},
]
[[package]]
name = "mako"
-version = "1.3.8"
+version = "1.3.12"
description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
- {file = "Mako-1.3.8-py3-none-any.whl", hash = "sha256:42f48953c7eb91332040ff567eb7eea69b22e7a4affbc5ba8e845e8f730f6627"},
- {file = "mako-1.3.8.tar.gz", hash = "sha256:577b97e414580d3e088d47c2dbbe9594aa7a5146ed2875d4dfa9075af2dd3cc8"},
+ {file = "mako-1.3.12-py3-none-any.whl", hash = "sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9"},
+ {file = "mako-1.3.12.tar.gz", hash = "sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a"},
]
[package.dependencies]
@@ -878,95 +1457,205 @@ testing = ["pytest"]
[[package]]
name = "markdown"
-version = "3.7"
+version = "3.10.2"
description = "Python implementation of John Gruber's Markdown."
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.10"
+groups = ["main"]
files = [
- {file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"},
- {file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"},
+ {file = "markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36"},
+ {file = "markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950"},
]
[package.extras]
-docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"]
+docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python] (>=0.28.3)"]
testing = ["coverage", "pyyaml"]
[[package]]
name = "markupsafe"
-version = "3.0.2"
+version = "3.0.3"
description = "Safely add untrusted strings to HTML/XML markup."
optional = false
python-versions = ">=3.9"
-files = [
- {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"},
- {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"},
- {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"},
- {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"},
- {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"},
- {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"},
- {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"},
- {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"},
- {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"},
- {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"},
- {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"},
- {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"},
- {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"},
- {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"},
- {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"},
- {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"},
- {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"},
- {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"},
- {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"},
- {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"},
- {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"},
- {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"},
- {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"},
- {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"},
- {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"},
- {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"},
- {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"},
- {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"},
- {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"},
- {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"},
- {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"},
- {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"},
- {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"},
- {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"},
- {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"},
- {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"},
- {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"},
- {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"},
- {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"},
- {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"},
- {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"},
- {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"},
- {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"},
- {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"},
- {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"},
- {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"},
- {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"},
- {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"},
- {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"},
- {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"},
- {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"},
- {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"},
- {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"},
- {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"},
- {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"},
- {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"},
- {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"},
- {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"},
- {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"},
- {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"},
- {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"},
+groups = ["main", "docs"]
+files = [
+ {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"},
+ {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"},
+ {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"},
+ {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"},
+ {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"},
+ {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"},
+ {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"},
+ {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"},
+ {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"},
+ {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"},
+ {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"},
+ {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"},
+ {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"},
+ {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"},
+ {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"},
+ {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"},
+ {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"},
+ {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"},
+ {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"},
+ {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"},
+ {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"},
+ {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"},
+ {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"},
+ {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"},
+ {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"},
+ {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"},
+ {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"},
+ {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"},
+ {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"},
+ {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"},
+ {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"},
+ {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"},
+ {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"},
+ {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"},
+ {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"},
+ {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"},
+ {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"},
+ {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"},
+ {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"},
+ {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"},
+ {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"},
+ {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"},
+ {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"},
+ {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"},
+ {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"},
+ {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"},
+ {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"},
+ {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"},
+ {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"},
+ {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"},
+ {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"},
+ {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"},
+ {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"},
+ {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"},
+ {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"},
+ {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"},
+ {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"},
+ {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"},
+ {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"},
+ {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"},
+ {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"},
+ {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"},
+ {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"},
+ {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"},
+ {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"},
+ {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"},
+ {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"},
+ {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"},
+ {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"},
+]
+
+[[package]]
+name = "matplotlib"
+version = "3.10.9"
+description = "Python plotting package"
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "matplotlib-3.10.9-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77210dce9cb8153dffc967efaae990543392563d5a376d4dd8539bebcb0ed217"},
+ {file = "matplotlib-3.10.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1e7698ac9868428e84d2c967424803b2472ff7167d9d6590d4204ed775343c3b"},
+ {file = "matplotlib-3.10.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1aa972116abb4c9d201bf245620b433726cb6856f3bef6a78f776a00f5c92d37"},
+ {file = "matplotlib-3.10.9-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae2f11957b27ce53497dd4d7b235c4d4f1faf383dfb39d0c5beb833bff883294"},
+ {file = "matplotlib-3.10.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b049278ddce116aaa1c1377ebf58adea909132dfce0281cf7e3a1ea9fc2e2c65"},
+ {file = "matplotlib-3.10.9-cp310-cp310-win_amd64.whl", hash = "sha256:82834c3c292d24d3a8aae77cd2d20019de69d692a34a970e4fdb8d33e2ea3dda"},
+ {file = "matplotlib-3.10.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:68cfdcede415f7c8f5577b03303dd94526cdb6d11036cecdc205e08733b2d2bb"},
+ {file = "matplotlib-3.10.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfca0129678bd56379db26c52b5d77ed7de314c047492fbdc763aa7501710cfb"},
+ {file = "matplotlib-3.10.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e436d155fa8a3399dc62683f8f5d0e2e50d25d0144a73edd73f82eec8f4abfb"},
+ {file = "matplotlib-3.10.9-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56fc0bd271b00025c6edfdc7c2dcd247372c8e1544971d62e1dc7c17367e8bf9"},
+ {file = "matplotlib-3.10.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5a6104ed666402ba5106d7f36e0e0cdca4e8d7fa4d39708ca88019e2835a2eb"},
+ {file = "matplotlib-3.10.9-cp311-cp311-win_amd64.whl", hash = "sha256:d730e984eddf56974c3e72b6129c7ca462ac38dc624338f4b0b23eb23ecba00f"},
+ {file = "matplotlib-3.10.9-cp311-cp311-win_arm64.whl", hash = "sha256:51bf0ddbdc598e060d46c16b5590708f81a1624cefbaaf62f6a81bf9285b8c80"},
+ {file = "matplotlib-3.10.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1"},
+ {file = "matplotlib-3.10.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320"},
+ {file = "matplotlib-3.10.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285"},
+ {file = "matplotlib-3.10.9-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2"},
+ {file = "matplotlib-3.10.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf"},
+ {file = "matplotlib-3.10.9-cp312-cp312-win_amd64.whl", hash = "sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6"},
+ {file = "matplotlib-3.10.9-cp312-cp312-win_arm64.whl", hash = "sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42"},
+ {file = "matplotlib-3.10.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b580440f1ff81a0e34122051a3dfabb7e4b7f9e380629929bde0eff9af72165f"},
+ {file = "matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e"},
+ {file = "matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f"},
+ {file = "matplotlib-3.10.9-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a8d66a55def891c33147ba3ba9bfcabf0b526a43764c818acbb4525e5ed0838"},
+ {file = "matplotlib-3.10.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d843374407c4017a6403b59c6c81606773d136f3259d5b6da3131bc814542cc2"},
+ {file = "matplotlib-3.10.9-cp313-cp313-win_amd64.whl", hash = "sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921"},
+ {file = "matplotlib-3.10.9-cp313-cp313-win_arm64.whl", hash = "sha256:ba7b3b8ef09eab7df0e86e9ae086faa433efbfbdb46afcb3aa16aabf779469a8"},
+ {file = "matplotlib-3.10.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9"},
+ {file = "matplotlib-3.10.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:82368699727bfb7b0182e1aa13082e3c08e092fa1a25d3e1fd92405bff96f6d4"},
+ {file = "matplotlib-3.10.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3225f4e1edcb8c86c884ddf79ebe20ecd0a67d30188f279897554ccd8fded4dc"},
+ {file = "matplotlib-3.10.9-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de2445a0c6690d21b7eb6ce071cebad6d40a2e9bdf10d039074a96ba19797b99"},
+ {file = "matplotlib-3.10.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d"},
+ {file = "matplotlib-3.10.9-cp313-cp313t-win_amd64.whl", hash = "sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8"},
+ {file = "matplotlib-3.10.9-cp313-cp313t-win_arm64.whl", hash = "sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38"},
+ {file = "matplotlib-3.10.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d"},
+ {file = "matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f"},
+ {file = "matplotlib-3.10.9-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b"},
+ {file = "matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2"},
+ {file = "matplotlib-3.10.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716"},
+ {file = "matplotlib-3.10.9-cp314-cp314-win_amd64.whl", hash = "sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f"},
+ {file = "matplotlib-3.10.9-cp314-cp314-win_arm64.whl", hash = "sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456"},
+ {file = "matplotlib-3.10.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe"},
+ {file = "matplotlib-3.10.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6"},
+ {file = "matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c"},
+ {file = "matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4"},
+ {file = "matplotlib-3.10.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf"},
+ {file = "matplotlib-3.10.9-cp314-cp314t-win_amd64.whl", hash = "sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39"},
+ {file = "matplotlib-3.10.9-cp314-cp314t-win_arm64.whl", hash = "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c"},
+ {file = "matplotlib-3.10.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1872fb212a05b729e649754a72d5da61d03e0554d76e80303b6f83d1d2c0552b"},
+ {file = "matplotlib-3.10.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:985f2238880e2e69093f588f5fe2e46771747febf0649f3cf7f7b7480875317f"},
+ {file = "matplotlib-3.10.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6640f75af2c6148293caa0a2b39dd806a492dd66c8a8b04035813e33d0fd2585"},
+ {file = "matplotlib-3.10.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:42fb814efabe95c06c1994d8ab5a8385f43a249e23badd3ba931d4308e5bca20"},
+ {file = "matplotlib-3.10.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f76e640a5268850bfda54b5131b1b1941cc685e42c5fa98ed9f2d64038308cba"},
+ {file = "matplotlib-3.10.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3fc0364dfbe1d07f6d15c5ebd0c5bf89e126916e5a8667dd4a7a6e84c36653d4"},
+ {file = "matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358"},
]
+[package.dependencies]
+contourpy = ">=1.0.1"
+cycler = ">=0.10"
+fonttools = ">=4.22.0"
+kiwisolver = ">=1.3.1"
+numpy = ">=1.23"
+packaging = ">=20.0"
+pillow = ">=8"
+pyparsing = ">=3"
+python-dateutil = ">=2.7"
+
+[package.extras]
+dev = ["meson-python (>=0.13.1,<0.17.0)", "pybind11 (>=2.13.2,!=2.13.3)", "setuptools (>=64)", "setuptools_scm (>=7,<10)"]
+
[[package]]
name = "mccabe"
version = "0.7.0"
description = "McCabe checker, plugin for flake8"
optional = false
python-versions = ">=3.6"
+groups = ["linters"]
files = [
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
@@ -974,116 +1663,216 @@ files = [
[[package]]
name = "mypy"
-version = "1.14.1"
+version = "1.20.2"
description = "Optional static typing for Python"
optional = false
-python-versions = ">=3.8"
-files = [
- {file = "mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"},
- {file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"},
- {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d"},
- {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b"},
- {file = "mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427"},
- {file = "mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f"},
- {file = "mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c"},
- {file = "mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1"},
- {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8"},
- {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f"},
- {file = "mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1"},
- {file = "mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae"},
- {file = "mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14"},
- {file = "mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9"},
- {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11"},
- {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e"},
- {file = "mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89"},
- {file = "mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b"},
- {file = "mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255"},
- {file = "mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34"},
- {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a"},
- {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9"},
- {file = "mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd"},
- {file = "mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107"},
- {file = "mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31"},
- {file = "mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6"},
- {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319"},
- {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac"},
- {file = "mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b"},
- {file = "mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837"},
- {file = "mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35"},
- {file = "mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc"},
- {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9"},
- {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb"},
- {file = "mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60"},
- {file = "mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c"},
- {file = "mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1"},
- {file = "mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6"},
+python-versions = ">=3.10"
+groups = ["types"]
+files = [
+ {file = "mypy-1.20.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cf5a4db6dca263010e2c7bff081c89383c72d187ba2cf4c44759aac970e2f0c4"},
+ {file = "mypy-1.20.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7b0e817b518bff7facd7f85ea05b643ad8bdcce684cf29784987b0a7c8e1f997"},
+ {file = "mypy-1.20.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97d7b9a485b40f8ca425460e89bf1da2814625b2da627c0dcc6aa46c92631d14"},
+ {file = "mypy-1.20.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e1c12f6d2db3d78b909b5f77513c11eb7f2dd2782b96a3ab6dffc7d44575c99"},
+ {file = "mypy-1.20.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:89dce27e142d25ffbc154c1819383b69f2e9234dc4ed4766f42e0e8cb264ab5c"},
+ {file = "mypy-1.20.2-cp310-cp310-win_amd64.whl", hash = "sha256:f376e37f9bf2a946872fc5fd1199c99310748e3c26c7a26683f13f8bdb756cbd"},
+ {file = "mypy-1.20.2-cp310-cp310-win_arm64.whl", hash = "sha256:6e2b469efd811707bc530fd1effef0f5d6eebcb7fe376affae69025da4b979a2"},
+ {file = "mypy-1.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4077797a273e56e8843d001e9dfe4ba10e33323d6ade647ff260e5cd97d9758c"},
+ {file = "mypy-1.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cdecf62abcc4292500d7858aeae87a1f8f1150f4c4dd08fb0b336ee79b2a6df3"},
+ {file = "mypy-1.20.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c566c3a88b6ece59b3d70f65bedef17304f48eb52ff040a6a18214e1917b3254"},
+ {file = "mypy-1.20.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0deb80d062b2479f2c87ae568f89845afc71d11bc41b04179e58165fd9f31e98"},
+ {file = "mypy-1.20.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bba9ad231e92a3e424b3e56b65aa17704993425bba97e302c832f9466bb85bac"},
+ {file = "mypy-1.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:baf593f2765fa3a6b1ef95807dbaa3d25b594f6a52adcc506a6b9cb115e1be67"},
+ {file = "mypy-1.20.2-cp311-cp311-win_arm64.whl", hash = "sha256:20175a1c0f49863946ec20b7f63255768058ac4f07d2b9ded6a6b46cfb5a9100"},
+ {file = "mypy-1.20.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4dbfcf869f6b0517f70cf0030ba6ea1d6645e132337a7d5204a18d8d5636c02b"},
+ {file = "mypy-1.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b6481b228d072315b053210b01ac320e1be243dc17f9e5887ef167f23f5fae4"},
+ {file = "mypy-1.20.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34397cdced6b90b836e38182076049fdb41424322e0b0728c946b0939ebdf9f6"},
+ {file = "mypy-1.20.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5da6976f20cae27059ea8d0c86e7cef3de720e04c4bb9ee18e3690fdb792066"},
+ {file = "mypy-1.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56908d7e08318d39f85b1f0c6cfd47b0cac1a130da677630dac0de3e0623e102"},
+ {file = "mypy-1.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:d52ad8d78522da1d308789df651ee5379088e77c76cb1994858d40a426b343b9"},
+ {file = "mypy-1.20.2-cp312-cp312-win_arm64.whl", hash = "sha256:785b08db19c9f214dc37d65f7c165d19a30fcecb48abfa30f31b01b5acaabb58"},
+ {file = "mypy-1.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:edfbfca868cdd6bd8d974a60f8a3682f5565d3f5c99b327640cedd24c4264026"},
+ {file = "mypy-1.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e2877a02380adfcdbc69071a0f74d6e9dbbf593c0dc9d174e1f223ffd5281943"},
+ {file = "mypy-1.20.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7488448de6007cd5177c6cea0517ac33b4c0f5ee9b5e9f2be51ce75511a85517"},
+ {file = "mypy-1.20.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb9c2fa06887e21d6a3a868762acb82aec34e2c6fd0174064f27c93ede68ad15"},
+ {file = "mypy-1.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d56a78b646f2e3daa865bc70cd5ec5a46c50045801ca8ff17a0c43abc97e3ee"},
+ {file = "mypy-1.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:2a4102b03bb7481d9a91a6da8d174740c9c8c4401024684b9ca3b7cc5e49852f"},
+ {file = "mypy-1.20.2-cp313-cp313-win_arm64.whl", hash = "sha256:a95a9248b0c6fd933a442c03c3b113c3b61320086b88e2c444676d3fd1ca3330"},
+ {file = "mypy-1.20.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:419413398fe250aae057fd2fe50166b61077083c9b82754c341cf4fd73038f30"},
+ {file = "mypy-1.20.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e73c07f23009962885c197ccb9b41356a30cc0e5a1d0c2ea8fd8fb1362d7f924"},
+ {file = "mypy-1.20.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c64e5973df366b747646fc98da921f9d6eba9716d57d1db94a83c026a08e0fb"},
+ {file = "mypy-1.20.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a65aa591af023864fd08a97da9974e919452cfe19cb146c8a5dc692626445dc"},
+ {file = "mypy-1.20.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fef51b01e638974a6e69885687e9bd40c8d1e09a6cd291cca0619625cf1f558"},
+ {file = "mypy-1.20.2-cp314-cp314-win_amd64.whl", hash = "sha256:913485a03f1bcf5d279409a9d2b9ed565c151f61c09f29991e5faa14033da4c8"},
+ {file = "mypy-1.20.2-cp314-cp314-win_arm64.whl", hash = "sha256:c3bae4f855d965b5453784300c12ffc63a548304ac7f99e55d4dc7c898673aa3"},
+ {file = "mypy-1.20.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2de3dcea53babc1c3237a19002bc3d228ce1833278f093b8d619e06e7cc79609"},
+ {file = "mypy-1.20.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:52b176444e2e5054dfcbcb8c75b0b719865c96247b37407184bbfca5c353f2c2"},
+ {file = "mypy-1.20.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:688c3312e5dadb573a2c69c82af3a298d43ecf9e6d264e0f95df960b5f6ac19c"},
+ {file = "mypy-1.20.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29752dbbf8cc53f89f6ac096d363314333045c257c9c75cbd189ca2de0455744"},
+ {file = "mypy-1.20.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:803203d2b6ea644982c644895c2f78b28d0e208bba7b27d9b921e0ec5eb207c6"},
+ {file = "mypy-1.20.2-cp314-cp314t-win_amd64.whl", hash = "sha256:9bcb8aa397ff0093c824182fd76a935a9ba7ad097fcbef80ae89bf6c1731d8ec"},
+ {file = "mypy-1.20.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e061b58443f1736f8a37c48978d7ab581636d6ab03e3d4f99e3fa90463bb9382"},
+ {file = "mypy-1.20.2-py3-none-any.whl", hash = "sha256:a94c5a76ab46c5e6257c7972b6c8cff0574201ca7dc05647e33e795d78680563"},
+ {file = "mypy-1.20.2.tar.gz", hash = "sha256:e8222c26daaafd9e8626dec58ae36029f82585890589576f769a650dd20fd665"},
]
[package.dependencies]
+librt = {version = ">=0.8.0", markers = "platform_python_implementation != \"PyPy\""}
mypy_extensions = ">=1.0.0"
-tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
-typing_extensions = ">=4.6.0"
+pathspec = ">=1.0.0"
+typing_extensions = [
+ {version = ">=4.6.0", markers = "python_version < \"3.15\""},
+ {version = ">=4.14.0", markers = "python_version >= \"3.15\""},
+]
[package.extras]
dmypy = ["psutil (>=4.0)"]
faster-cache = ["orjson"]
install-types = ["pip"]
mypyc = ["setuptools (>=50)"]
+native-parser = ["ast-serialize (>=0.1.1,<1.0.0)"]
reports = ["lxml"]
[[package]]
name = "mypy-extensions"
-version = "1.0.0"
+version = "1.1.0"
description = "Type system extensions for programs checked with the mypy type checker."
optional = false
-python-versions = ">=3.5"
+python-versions = ">=3.8"
+groups = ["linters", "types"]
files = [
- {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
- {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
+ {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"},
+ {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"},
]
[[package]]
name = "nh3"
-version = "0.2.20"
+version = "0.3.5"
description = "Python binding to Ammonia HTML sanitizer Rust crate"
optional = false
python-versions = ">=3.8"
-files = [
- {file = "nh3-0.2.20-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e1061a4ab6681f6bdf72b110eea0c4e1379d57c9de937db3be4202f7ad6043db"},
- {file = "nh3-0.2.20-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb4254b1dac4a1ee49919a5b3f1caf9803ea8dada1816d9e8289e63d3cd0dd9a"},
- {file = "nh3-0.2.20-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0ae9cbd713524cdb81e64663d0d6aae26f678db9f2cd9db0bf162606f1f9f20c"},
- {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e1f7370b4e14cc03f5ae141ef30a1caf81fa5787711f80be9081418dd9eb79d2"},
- {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:ac4d27dc836a476efffc6eb661994426b8b805c951b29c9cf2ff36bc9ad58bc5"},
- {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4fd2e9248725ebcedac3997a8d3da0d90a12a28c9179c6ba51f1658938ac30d0"},
- {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f7d564871833ddbe54df3aa59053b1110729d3a800cb7628ae8f42adb3d75208"},
- {file = "nh3-0.2.20-cp313-cp313t-win32.whl", hash = "sha256:d2a176fd4306b6f0f178a3f67fac91bd97a3a8d8fafb771c9b9ef675ba5c8886"},
- {file = "nh3-0.2.20-cp313-cp313t-win_amd64.whl", hash = "sha256:6ed834c68452a600f517dd3e1534dbfaff1f67f98899fecf139a055a25d99150"},
- {file = "nh3-0.2.20-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:76e2f603b30c02ff6456b233a83fc377dedab6a50947b04e960a6b905637b776"},
- {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:181063c581defe683bd4bb78188ac9936d208aebbc74c7f7c16b6a32ae2ebb38"},
- {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:231addb7643c952cd6d71f1c8702d703f8fe34afcb20becb3efb319a501a12d7"},
- {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1b9a8340a0aab991c68a5ca938d35ef4a8a3f4bf1b455da8855a40bee1fa0ace"},
- {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10317cd96fe4bbd4eb6b95f3920b71c902157ad44fed103fdcde43e3b8ee8be6"},
- {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8698db4c04b140800d1a1cd3067fda399e36e1e2b8fc1fe04292a907350a3e9b"},
- {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3eb04b9c3deb13c3a375ea39fd4a3c00d1f92e8fb2349f25f1e3e4506751774b"},
- {file = "nh3-0.2.20-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92f3f1c4f47a2c6f3ca7317b1d5ced05bd29556a75d3a4e2715652ae9d15c05d"},
- {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ddefa9fd6794a87e37d05827d299d4b53a3ec6f23258101907b96029bfef138a"},
- {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ce3731c8f217685d33d9268362e5b4f770914e922bba94d368ab244a59a6c397"},
- {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:09f037c02fc2c43b211ff1523de32801dcfb0918648d8e651c36ef890f1731ec"},
- {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:813f1c8012dd64c990514b795508abb90789334f76a561fa0fd4ca32d2275330"},
- {file = "nh3-0.2.20-cp38-abi3-win32.whl", hash = "sha256:47b2946c0e13057855209daeffb45dc910bd0c55daf10190bb0b4b60e2999784"},
- {file = "nh3-0.2.20-cp38-abi3-win_amd64.whl", hash = "sha256:da87573f03084edae8eb87cfe811ec338606288f81d333c07d2a9a0b9b976c0b"},
- {file = "nh3-0.2.20.tar.gz", hash = "sha256:9705c42d7ff88a0bea546c82d7fe5e59135e3d3f057e485394f491248a1f8ed5"},
+groups = ["main"]
+files = [
+ {file = "nh3-0.3.5-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:23a312224875f72cd16bde417f49071451877e29ef646a60e50fcb69407cc18a"},
+ {file = "nh3-0.3.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:387abd011e81959d5a35151a11350a0795c6edeb53ebfa02d2e882dc01299263"},
+ {file = "nh3-0.3.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:48f45e3e914be93a596431aa143dedf1582557bf41a58153c296048d6e3798c9"},
+ {file = "nh3-0.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0a09f51806fd51b4fedbf9ea2b61fef388f19aef0d62fe51199d41648be14588"},
+ {file = "nh3-0.3.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c357f1d042c67f135a5e6babb2b0e3b9d9224ff4a3543240f597767b01384ffd"},
+ {file = "nh3-0.3.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:38748140bf76383ab7ce2dce0ad4cb663855d8fbc9098f7f3483673d09616a17"},
+ {file = "nh3-0.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:84bdeb082544fbcb77a12c034dd77d7da0556fdc0727b787eb6214b958c15e29"},
+ {file = "nh3-0.3.5-cp314-cp314t-win32.whl", hash = "sha256:c3aae321f67ae66cff2a627115f106a377d4475d10b0e13d97959a13486b9a88"},
+ {file = "nh3-0.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c88605d8d468f7fc1b31e06129bc91d6c96f6c621776c9b504a0da9beac9df5f"},
+ {file = "nh3-0.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:72c5bdedec27fa33de6a5326346ea8aa3fe54f6ac294d54c4b204fb66a9f1e79"},
+ {file = "nh3-0.3.5-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:3bb854485c9b33e5bb143ff3e49e577073bc6bc320f0ff8fc316dd89c0d3c101"},
+ {file = "nh3-0.3.5-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50d401ab2d8e86d59e2126e3ab2a2f45840c405842b626d9a51624b3a33b6878"},
+ {file = "nh3-0.3.5-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:acfd354e61accbe4c74f8017c6e397a776916dfe47c48643cf7fd84ade826f93"},
+ {file = "nh3-0.3.5-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:52d877980d7ca01dc3baf3936bf844828bc6f332962227a684ed79c18cce14c3"},
+ {file = "nh3-0.3.5-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:207c01801d3e9bb8ec08f08689346bdd30ce15b8bf60013a925d08b5388962a4"},
+ {file = "nh3-0.3.5-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea232933394d1d58bf7c4bb348dc4660eae6604e1ae81cd2ba6d9ed80d390f3b"},
+ {file = "nh3-0.3.5-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe3a787dc76b50de6bee54ef242f26c41dfe47654428e3e94f0fae5bb6dd2cc1"},
+ {file = "nh3-0.3.5-cp38-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:488928988caad25ba14b1eb5bc74e25e21f3b5e40341d956f3ce4a8bc19460dc"},
+ {file = "nh3-0.3.5-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2c069570b06aa848457713ad7af4a9905691291548c4466a9ad78ee95808382b"},
+ {file = "nh3-0.3.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:eeedc90ed8c42c327e8e10e621ccfa314fc6cce35d5929f4297ff1cdb89667c4"},
+ {file = "nh3-0.3.5-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:de8e8621853b6470fe928c684ee0d3f39ea8086cebafe4c416486488dea7b68d"},
+ {file = "nh3-0.3.5-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:6ea58cc44d274c643b83547ca9654a0b1a817609b160601356f76a2b744c49ad"},
+ {file = "nh3-0.3.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e49c9b564e6bcb03ecd2f057213df9a0de15a95812ac9db9600b590db23d3ae9"},
+ {file = "nh3-0.3.5-cp38-abi3-win32.whl", hash = "sha256:559e4c73b689e9a7aa97ac9760b1bc488038d7c1a575aa4ab5a0e19ee9630c0f"},
+ {file = "nh3-0.3.5-cp38-abi3-win_amd64.whl", hash = "sha256:45e6a65dc88a300a2e3502cb9c8e6d1d6b831d6fba7470643333609c6aab1f30"},
+ {file = "nh3-0.3.5-cp38-abi3-win_arm64.whl", hash = "sha256:8f85285700a18e9f3fc5bff41fe573fa84f81542ef13b48a89f9fecca0474d3b"},
+ {file = "nh3-0.3.5.tar.gz", hash = "sha256:45855e14ff056064fec77133bfcf7cd691838168e5e17bbef075394954dc9dc8"},
+]
+
+[[package]]
+name = "numpy"
+version = "2.4.4"
+description = "Fundamental package for array computing in Python"
+optional = false
+python-versions = ">=3.11"
+groups = ["main"]
+files = [
+ {file = "numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db"},
+ {file = "numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0"},
+ {file = "numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015"},
+ {file = "numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40"},
+ {file = "numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d"},
+ {file = "numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502"},
+ {file = "numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd"},
+ {file = "numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5"},
+ {file = "numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e"},
+ {file = "numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e"},
+ {file = "numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e"},
+ {file = "numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b"},
+ {file = "numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e"},
+ {file = "numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842"},
+ {file = "numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8"},
+ {file = "numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121"},
+ {file = "numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e"},
+ {file = "numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44"},
+ {file = "numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d"},
+ {file = "numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827"},
+ {file = "numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a"},
+ {file = "numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec"},
+ {file = "numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50"},
+ {file = "numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115"},
+ {file = "numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af"},
+ {file = "numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c"},
+ {file = "numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103"},
+ {file = "numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83"},
+ {file = "numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed"},
+ {file = "numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959"},
+ {file = "numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed"},
+ {file = "numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf"},
+ {file = "numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d"},
+ {file = "numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5"},
+ {file = "numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7"},
+ {file = "numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93"},
+ {file = "numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e"},
+ {file = "numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40"},
+ {file = "numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e"},
+ {file = "numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392"},
+ {file = "numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008"},
+ {file = "numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8"},
+ {file = "numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233"},
+ {file = "numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0"},
+ {file = "numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a"},
+ {file = "numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a"},
+ {file = "numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b"},
+ {file = "numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a"},
+ {file = "numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d"},
+ {file = "numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252"},
+ {file = "numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f"},
+ {file = "numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc"},
+ {file = "numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74"},
+ {file = "numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb"},
+ {file = "numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e"},
+ {file = "numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113"},
+ {file = "numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d"},
+ {file = "numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d"},
+ {file = "numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f"},
+ {file = "numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0"},
+ {file = "numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150"},
+ {file = "numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871"},
+ {file = "numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e"},
+ {file = "numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7"},
+ {file = "numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4"},
+ {file = "numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e"},
+ {file = "numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c"},
+ {file = "numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3"},
+ {file = "numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7"},
+ {file = "numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f"},
+ {file = "numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119"},
+ {file = "numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0"},
]
[[package]]
name = "packaging"
-version = "24.2"
+version = "26.2"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
+groups = ["main", "docs", "linters", "testing"]
files = [
- {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
- {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
+ {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"},
+ {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"},
]
[[package]]
@@ -1092,6 +1881,7 @@ version = "3.1.0"
description = "Load, configure, and compose WSGI applications and servers"
optional = false
python-versions = ">=3.7"
+groups = ["main"]
files = [
{file = "PasteDeploy-3.1.0-py3-none-any.whl", hash = "sha256:76388ad53a661448d436df28c798063108f70e994ddc749540d733cdbd1b38cf"},
{file = "PasteDeploy-3.1.0.tar.gz", hash = "sha256:9ddbaf152f8095438a9fe81f82c78a6714b92ae8e066bed418b6a7ff6a095a95"},
@@ -1104,21 +1894,137 @@ testing = ["Paste", "pytest", "pytest-cov"]
[[package]]
name = "pathspec"
-version = "0.12.1"
+version = "1.1.1"
description = "Utility library for gitignore style pattern matching of file paths."
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
+groups = ["linters", "types"]
files = [
- {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
- {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
+ {file = "pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189"},
+ {file = "pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a"},
+]
+
+[package.extras]
+hyperscan = ["hyperscan (>=0.7)"]
+optional = ["typing-extensions (>=4)"]
+re2 = ["google-re2 (>=1.1)"]
+
+[[package]]
+name = "pillow"
+version = "12.2.0"
+description = "Python Imaging Library (fork)"
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f"},
+ {file = "pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97"},
+ {file = "pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff"},
+ {file = "pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec"},
+ {file = "pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136"},
+ {file = "pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c"},
+ {file = "pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3"},
+ {file = "pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa"},
+ {file = "pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032"},
+ {file = "pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5"},
+ {file = "pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024"},
+ {file = "pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab"},
+ {file = "pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65"},
+ {file = "pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7"},
+ {file = "pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e"},
+ {file = "pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705"},
+ {file = "pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176"},
+ {file = "pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b"},
+ {file = "pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909"},
+ {file = "pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808"},
+ {file = "pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60"},
+ {file = "pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe"},
+ {file = "pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5"},
+ {file = "pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421"},
+ {file = "pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987"},
+ {file = "pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76"},
+ {file = "pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005"},
+ {file = "pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780"},
+ {file = "pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5"},
+ {file = "pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5"},
+ {file = "pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940"},
+ {file = "pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5"},
+ {file = "pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414"},
+ {file = "pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c"},
+ {file = "pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2"},
+ {file = "pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c"},
+ {file = "pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795"},
+ {file = "pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f"},
+ {file = "pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed"},
+ {file = "pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9"},
+ {file = "pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed"},
+ {file = "pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3"},
+ {file = "pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9"},
+ {file = "pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795"},
+ {file = "pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e"},
+ {file = "pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b"},
+ {file = "pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06"},
+ {file = "pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b"},
+ {file = "pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f"},
+ {file = "pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612"},
+ {file = "pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c"},
+ {file = "pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea"},
+ {file = "pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4"},
+ {file = "pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4"},
+ {file = "pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea"},
+ {file = "pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24"},
+ {file = "pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98"},
+ {file = "pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453"},
+ {file = "pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8"},
+ {file = "pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b"},
+ {file = "pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295"},
+ {file = "pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed"},
+ {file = "pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae"},
+ {file = "pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601"},
+ {file = "pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be"},
+ {file = "pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f"},
+ {file = "pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286"},
+ {file = "pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50"},
+ {file = "pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104"},
+ {file = "pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7"},
+ {file = "pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150"},
+ {file = "pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1"},
+ {file = "pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463"},
+ {file = "pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3"},
+ {file = "pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166"},
+ {file = "pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe"},
+ {file = "pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd"},
+ {file = "pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e"},
+ {file = "pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06"},
+ {file = "pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43"},
+ {file = "pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354"},
+ {file = "pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1"},
+ {file = "pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb"},
+ {file = "pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f"},
+ {file = "pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d"},
+ {file = "pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f"},
+ {file = "pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e"},
+ {file = "pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0"},
+ {file = "pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1"},
+ {file = "pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e"},
+ {file = "pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5"},
]
+[package.extras]
+docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-autobuild", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"]
+fpx = ["olefile"]
+mic = ["olefile"]
+test-arrow = ["arro3-compute", "arro3-core", "nanoarrow", "pyarrow"]
+tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma (>=5)", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"]
+xmp = ["defusedxml"]
+
[[package]]
name = "plaster"
version = "1.1.2"
description = "A loader interface around multiple config file formats."
optional = false
python-versions = ">=3.7"
+groups = ["main"]
files = [
{file = "plaster-1.1.2-py2.py3-none-any.whl", hash = "sha256:42992ab1f4865f1278e2ad740e8ad145683bb4022e03534265528f0c23c0df2d"},
{file = "plaster-1.1.2.tar.gz", hash = "sha256:f8befc54bf8c1147c10ab40297ec84c2676fa2d4ea5d6f524d9436a80074ef98"},
@@ -1134,6 +2040,7 @@ version = "1.0.1"
description = "A loader implementing the PasteDeploy syntax to be used by plaster."
optional = false
python-versions = ">=3.7"
+groups = ["main"]
files = [
{file = "plaster_pastedeploy-1.0.1-py2.py3-none-any.whl", hash = "sha256:ad3550cc744648969ed3b810f33c9344f515ee8d8a8cec18e8f2c4a643c2181f"},
{file = "plaster_pastedeploy-1.0.1.tar.gz", hash = "sha256:be262e6d2e41a7264875daa2fe2850cbb0615728bcdc92828fdc72736e381412"},
@@ -1148,224 +2055,250 @@ testing = ["pytest", "pytest-cov"]
[[package]]
name = "platformdirs"
-version = "4.3.6"
+version = "4.9.6"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.10"
+groups = ["linters"]
files = [
- {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"},
- {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"},
+ {file = "platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917"},
+ {file = "platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a"},
]
-[package.extras]
-docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"]
-test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"]
-type = ["mypy (>=1.11.2)"]
-
[[package]]
name = "playwright"
-version = "1.49.1"
+version = "1.59.0"
description = "A high-level API to automate web browsers"
optional = false
python-versions = ">=3.9"
+groups = ["testing"]
files = [
- {file = "playwright-1.49.1-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:1041ffb45a0d0bc44d698d3a5aa3ac4b67c9bd03540da43a0b70616ad52592b8"},
- {file = "playwright-1.49.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9f38ed3d0c1f4e0a6d1c92e73dd9a61f8855133249d6f0cec28648d38a7137be"},
- {file = "playwright-1.49.1-py3-none-macosx_11_0_universal2.whl", hash = "sha256:3be48c6d26dc819ca0a26567c1ae36a980a0303dcd4249feb6f59e115aaddfb8"},
- {file = "playwright-1.49.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:753ca90ee31b4b03d165cfd36e477309ebf2b4381953f2a982ff612d85b147d2"},
- {file = "playwright-1.49.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd9bc8dab37aa25198a01f555f0a2e2c3813fe200fef018ac34dfe86b34994b9"},
- {file = "playwright-1.49.1-py3-none-win32.whl", hash = "sha256:43b304be67f096058e587dac453ece550eff87b8fbed28de30f4f022cc1745bb"},
- {file = "playwright-1.49.1-py3-none-win_amd64.whl", hash = "sha256:47b23cb346283278f5b4d1e1990bcb6d6302f80c0aa0ca93dd0601a1400191df"},
+ {file = "playwright-1.59.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:bfc6940100b57423175c819ce2422ec5880d55fa2769987f62ab7a1f5fe6783e"},
+ {file = "playwright-1.59.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:af068143a0c045ec11608b67d6c42e58db7e9cf65a742dd21fddedc1a9802c47"},
+ {file = "playwright-1.59.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:4a4a2d4842b0e4120de3fa48636e4b69085a05b81d8a35ad4353f530ade72ed6"},
+ {file = "playwright-1.59.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:c5792aad9e22b91a09264b9edbc18553cf05ea5a39404d65dc19a012c6b2e51d"},
+ {file = "playwright-1.59.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c881a19377d2b900af855fb525b5f22a27bf3cfbecba6d1edb36766d56cb100"},
+ {file = "playwright-1.59.0-py3-none-win32.whl", hash = "sha256:6989c476be2b9cd3e24a18cc9dcf202e266fb3d91e3e5395cd668c54ea54b119"},
+ {file = "playwright-1.59.0-py3-none-win_amd64.whl", hash = "sha256:d5a5cc064b82ca92996080025710844e417f44df8fda9001102c28f44174171c"},
+ {file = "playwright-1.59.0-py3-none-win_arm64.whl", hash = "sha256:93581ad515728cadc8af39b288a5633ba6d36e7d72048e79d890ce01ea2156f9"},
]
[package.dependencies]
-greenlet = "3.1.1"
-pyee = "12.0.0"
+greenlet = ">=3.1.1,<4.0.0"
+pyee = ">=13,<14"
[[package]]
name = "pluggy"
-version = "1.5.0"
+version = "1.6.0"
description = "plugin and hook calling mechanisms for python"
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
+groups = ["testing"]
files = [
- {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
- {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
+ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"},
+ {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"},
]
[package.extras]
dev = ["pre-commit", "tox"]
-testing = ["pytest", "pytest-benchmark"]
+testing = ["coverage", "pytest", "pytest-benchmark"]
[[package]]
name = "pycparser"
-version = "2.22"
+version = "3.0"
description = "C parser in Python"
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.10"
+groups = ["main", "types"]
+markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\""
files = [
- {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
- {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
+ {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"},
+ {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"},
]
[[package]]
name = "pydantic"
-version = "2.10.6"
+version = "2.13.4"
description = "Data validation using Python type hints"
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
+groups = ["main"]
files = [
- {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"},
- {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"},
+ {file = "pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba"},
+ {file = "pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6"},
]
[package.dependencies]
annotated-types = ">=0.6.0"
-pydantic-core = "2.27.2"
-typing-extensions = ">=4.12.2"
+pydantic-core = "2.46.4"
+typing-extensions = ">=4.14.1"
+typing-inspection = ">=0.4.2"
[package.extras]
email = ["email-validator (>=2.0.0)"]
-timezone = ["tzdata"]
+timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""]
[[package]]
name = "pydantic-core"
-version = "2.27.2"
+version = "2.46.4"
description = "Core functionality for Pydantic validation and serialization"
optional = false
-python-versions = ">=3.8"
-files = [
- {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"},
- {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"},
- {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"},
- {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"},
- {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"},
- {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"},
- {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"},
- {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"},
- {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"},
- {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"},
- {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"},
- {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"},
- {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"},
- {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"},
- {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"},
- {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"},
- {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"},
- {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"},
- {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"},
- {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"},
- {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"},
- {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"},
- {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"},
- {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"},
- {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"},
- {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"},
- {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"},
- {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"},
- {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"},
- {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"},
- {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"},
- {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"},
- {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"},
- {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"},
- {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"},
- {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"},
- {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"},
- {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"},
- {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"},
- {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"},
- {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"},
- {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"},
- {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"},
- {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"},
- {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"},
- {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"},
- {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"},
- {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"},
- {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"},
- {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"},
- {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"},
- {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"},
- {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"},
- {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"},
- {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"},
- {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"},
- {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"},
- {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"},
- {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"},
- {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"},
- {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"},
- {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"},
- {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"},
- {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"},
- {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"},
- {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"},
- {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"},
- {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"},
- {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"},
- {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"},
- {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"},
- {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"},
- {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"},
- {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"},
- {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"},
- {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"},
- {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"},
- {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"},
- {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"},
- {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"},
- {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"},
- {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"},
- {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"},
- {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"},
- {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"},
- {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"},
- {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"},
- {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"},
- {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"},
- {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"},
- {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"},
- {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"},
- {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"},
- {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"},
- {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"},
- {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"},
- {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"},
- {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"},
- {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"},
- {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"},
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4"},
+ {file = "pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5"},
+ {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba"},
+ {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b"},
+ {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c"},
+ {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50"},
+ {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd"},
+ {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01"},
+ {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d"},
+ {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4"},
+ {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f"},
+ {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39"},
+ {file = "pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d"},
+ {file = "pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf"},
+ {file = "pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594"},
+ {file = "pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c"},
+ {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826"},
+ {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04"},
+ {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e"},
+ {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3"},
+ {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4"},
+ {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398"},
+ {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3"},
+ {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848"},
+ {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3"},
+ {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109"},
+ {file = "pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda"},
+ {file = "pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33"},
+ {file = "pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d"},
+ {file = "pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2"},
+ {file = "pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f"},
+ {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7"},
+ {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7"},
+ {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712"},
+ {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4"},
+ {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce"},
+ {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987"},
+ {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b"},
+ {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458"},
+ {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b"},
+ {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c"},
+ {file = "pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894"},
+ {file = "pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89"},
+ {file = "pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a"},
+ {file = "pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008"},
+ {file = "pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4"},
+ {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76"},
+ {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3"},
+ {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76"},
+ {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4"},
+ {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a"},
+ {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262"},
+ {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e"},
+ {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd"},
+ {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be"},
+ {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d"},
+ {file = "pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb"},
+ {file = "pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292"},
+ {file = "pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d"},
+ {file = "pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb"},
+ {file = "pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462"},
+ {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9"},
+ {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4"},
+ {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914"},
+ {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28"},
+ {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b"},
+ {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c"},
+ {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb"},
+ {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898"},
+ {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e"},
+ {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519"},
+ {file = "pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4"},
+ {file = "pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac"},
+ {file = "pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a"},
+ {file = "pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0"},
+ {file = "pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d"},
+ {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b"},
+ {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000"},
+ {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e"},
+ {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd"},
+ {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3"},
+ {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7"},
+ {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff"},
+ {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424"},
+ {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6"},
+ {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565"},
+ {file = "pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02"},
+ {file = "pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5"},
+ {file = "pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596"},
+ {file = "pydantic_core-2.46.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae"},
+ {file = "pydantic_core-2.46.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201"},
+ {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0"},
+ {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15"},
+ {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76"},
+ {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49"},
+ {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928"},
+ {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066"},
+ {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6"},
+ {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9"},
+ {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29"},
+ {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9"},
+ {file = "pydantic_core-2.46.4-cp39-cp39-win32.whl", hash = "sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1"},
+ {file = "pydantic_core-2.46.4-cp39-cp39-win_amd64.whl", hash = "sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac"},
+ {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c"},
+ {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b"},
+ {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b"},
+ {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea"},
+ {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7"},
+ {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df"},
+ {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526"},
+ {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0"},
+ {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0"},
+ {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7"},
+ {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2"},
+ {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9"},
+ {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf"},
+ {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30"},
+ {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc"},
+ {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983"},
+ {file = "pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1"},
]
[package.dependencies]
-typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
+typing-extensions = ">=4.14.1"
[[package]]
name = "pyee"
-version = "12.0.0"
+version = "13.0.1"
description = "A rough port of Node.js's EventEmitter to Python with a few tricks of its own"
optional = false
python-versions = ">=3.8"
+groups = ["testing"]
files = [
- {file = "pyee-12.0.0-py3-none-any.whl", hash = "sha256:7b14b74320600049ccc7d0e0b1becd3b4bd0a03c745758225e31a59f4095c990"},
- {file = "pyee-12.0.0.tar.gz", hash = "sha256:c480603f4aa2927d4766eb41fa82793fe60a82cbfdb8d688e0d08c55a534e145"},
+ {file = "pyee-13.0.1-py3-none-any.whl", hash = "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228"},
+ {file = "pyee-13.0.1.tar.gz", hash = "sha256:0b931f7c14535667ed4c7e0d531716368715e860b988770fc7eb8578d1f67fc8"},
]
[package.dependencies]
typing-extensions = "*"
[package.extras]
-dev = ["black", "build", "flake8", "flake8-black", "isort", "jupyter-console", "mkdocs", "mkdocs-include-markdown-plugin", "mkdocstrings[python]", "pytest", "pytest-asyncio", "pytest-trio", "sphinx", "toml", "tox", "trio", "trio", "trio-typing", "twine", "twisted", "validate-pyproject[all]"]
+dev = ["black", "build", "flake8", "flake8-black", "isort", "jupyter-console", "mkdocs", "mkdocs-include-markdown-plugin", "mkdocstrings[python]", "mypy", "pytest", "pytest-asyncio ; python_version >= \"3.4\"", "pytest-trio ; python_version >= \"3.7\"", "sphinx", "toml", "tox", "trio", "trio ; python_version > \"3.6\"", "trio-typing ; python_version > \"3.6\"", "twine", "twisted", "validate-pyproject[all]"]
[[package]]
name = "pygments"
-version = "2.19.1"
+version = "2.20.0"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
+groups = ["main", "docs", "testing"]
files = [
- {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"},
- {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"},
+ {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"},
+ {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"},
]
[package.extras]
@@ -1373,27 +2306,26 @@ windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "pylint"
-version = "3.3.4"
+version = "4.0.5"
description = "python code static checker"
optional = false
-python-versions = ">=3.9.0"
+python-versions = ">=3.10.0"
+groups = ["linters"]
files = [
- {file = "pylint-3.3.4-py3-none-any.whl", hash = "sha256:289e6a1eb27b453b08436478391a48cd53bb0efb824873f949e709350f3de018"},
- {file = "pylint-3.3.4.tar.gz", hash = "sha256:74ae7a38b177e69a9b525d0794bd8183820bfa7eb68cc1bee6e8ed22a42be4ce"},
+ {file = "pylint-4.0.5-py3-none-any.whl", hash = "sha256:00f51c9b14a3b3ae08cff6b2cdd43f28165c78b165b628692e428fb1f8dc2cf2"},
+ {file = "pylint-4.0.5.tar.gz", hash = "sha256:8cd6a618df75deb013bd7eb98327a95f02a6fb839205a6bbf5456ef96afb317c"},
]
[package.dependencies]
-astroid = ">=3.3.8,<=3.4.0-dev0"
+astroid = ">=4.0.2,<=4.1.dev0"
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
dill = [
- {version = ">=0.2", markers = "python_version < \"3.11\""},
{version = ">=0.3.7", markers = "python_version >= \"3.12\""},
- {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""},
+ {version = ">=0.3.6", markers = "python_version == \"3.11\""},
]
-isort = ">=4.2.5,<5.13.0 || >5.13.0,<7"
+isort = ">=5,<5.13 || >5.13,<9"
mccabe = ">=0.6,<0.8"
-platformdirs = ">=2.2.0"
-tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+platformdirs = ">=2.2"
tomlkit = ">=0.10.1"
[package.extras]
@@ -1401,20 +2333,34 @@ spelling = ["pyenchant (>=3.2,<4.0)"]
testutils = ["gitpython (>3)"]
[[package]]
-name = "pyramid"
-version = "2.0.2"
-description = "The Pyramid Web Framework, a Pylons project"
+name = "pyparsing"
+version = "3.3.2"
+description = "pyparsing - Classes and methods to define and execute parsing grammars"
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.9"
+groups = ["main"]
files = [
- {file = "pyramid-2.0.2-py3-none-any.whl", hash = "sha256:2e6585ac55c147f0a51bc00dadf72075b3bdd9a871b332ff9e5e04117ccd76fa"},
- {file = "pyramid-2.0.2.tar.gz", hash = "sha256:372138a738e4216535cc76dcce6eddd5a1aaca95130f2354fb834264c06f18de"},
+ {file = "pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d"},
+ {file = "pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc"},
]
+[package.extras]
+diagrams = ["jinja2", "railroad-diagrams"]
+
+[[package]]
+name = "pyramid"
+version = "2.1.dev0"
+description = "The Pyramid Web Framework, a Pylons project"
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = []
+develop = false
+
[package.dependencies]
hupper = ">=1.5"
plaster = "*"
-plaster-pastedeploy = "*"
+plaster_pastedeploy = "*"
setuptools = "*"
translationstring = ">=0.4"
venusian = ">=1.0"
@@ -1423,15 +2369,22 @@ webob = ">=1.8.3"
"zope.interface" = ">=3.8.0"
[package.extras]
-docs = ["Sphinx (>=3.0.0)", "docutils", "pylons-sphinx-latesturl", "pylons-sphinx-themes (>=1.0.8)", "repoze.sphinx.autointerface", "sphinx-copybutton", "sphinxcontrib-autoprogram"]
+docs = ["Sphinx (>=3.0.0)", "docutils", "pylons-sphinx-themes (>=1.0.8)", "pylons_sphinx_latesturl", "repoze.sphinx.autointerface", "sphinx-copybutton", "sphinxcontrib-autoprogram"]
testing = ["coverage", "pytest (>=5.4.2)", "pytest-cov", "webtest (>=1.3.1)", "zope.component (>=4.0)"]
+[package.source]
+type = "git"
+url = "https://github.com/Kingdread/pyramid.git"
+reference = "7f5a499ccc63a10302fcc8021dcfae90ee97866f"
+resolved_reference = "7f5a499ccc63a10302fcc8021dcfae90ee97866f"
+
[[package]]
name = "pyramid-debugtoolbar"
version = "4.12.1"
description = "A package which provides an interactive HTML debugger for Pyramid application development"
optional = false
python-versions = ">=3.7"
+groups = ["main"]
files = [
{file = "pyramid_debugtoolbar-4.12.1-py3-none-any.whl", hash = "sha256:1d13a82444b3396d5a76d1e611d0de1b38da096f04440041e8e889536103864b"},
{file = "pyramid_debugtoolbar-4.12.1.tar.gz", hash = "sha256:71e888d349c85fcca12b3e6dc4c7ae8e3f02a1d5acc05154fd9ba8c7f661b43d"},
@@ -1452,6 +2405,7 @@ version = "2.10.1"
description = "Jinja2 template bindings for the Pyramid web framework"
optional = false
python-versions = ">=3.7.0"
+groups = ["main"]
files = [
{file = "pyramid_jinja2-2.10.1-py3-none-any.whl", hash = "sha256:425a52a0c1cc2a83e183d22bb15cdd999112aa4c7b1e0f4e21c49a6b523ad6e1"},
{file = "pyramid_jinja2-2.10.1.tar.gz", hash = "sha256:8c508cb35c135f95149ca236110f9c8875343575740d16c5cb73a50ef1c21677"},
@@ -1473,6 +2427,7 @@ version = "1.1.0"
description = "Mako template bindings for the Pyramid web framework"
optional = false
python-versions = "*"
+groups = ["main"]
files = [
{file = "pyramid_mako-1.1.0-py2.py3-none-any.whl", hash = "sha256:76104592d292b6974cf7080aa52405c51f396a621535f01e274d7fe546e85a43"},
{file = "pyramid_mako-1.1.0.tar.gz", hash = "sha256:0066c863441f1c3ddea60cee1ccc50d00a91a317a8052ca44131da1a12a840e2"},
@@ -1492,6 +2447,7 @@ version = "2.1.1"
description = "An execution policy for Pyramid that supports retrying requests after certain failure exceptions."
optional = false
python-versions = "*"
+groups = ["main"]
files = [
{file = "pyramid_retry-2.1.1-py2.py3-none-any.whl", hash = "sha256:b5129a60eb9d7409234ea52839006426d2ae887b4a1f0530c75ec336cabf2476"},
{file = "pyramid_retry-2.1.1.tar.gz", hash = "sha256:baa8276ae68babad09e5f2f94efc4f7421f3b8fb526151df522052f8cd3ec0c9"},
@@ -1511,6 +2467,7 @@ version = "2.6"
description = "A package which allows Pyramid requests to join the active transaction"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "pyramid_tm-2.6-py3-none-any.whl", hash = "sha256:665a4ee1d6f41f0c7ffa5e54d9b01d70cd3e8e5bd76277529acbdd6b6bd6fe9e"},
{file = "pyramid_tm-2.6.tar.gz", hash = "sha256:8148d2191285280c9a0c23e6df1018b3514b4cef02115b872dd0350a4d78709c"},
@@ -1526,25 +2483,25 @@ testing = ["WebTest", "coverage (>=5.0)", "pytest", "pytest-cov"]
[[package]]
name = "pytest"
-version = "8.3.4"
+version = "9.0.3"
description = "pytest: simple powerful testing with Python"
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.10"
+groups = ["testing"]
files = [
- {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"},
- {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"},
+ {file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"},
+ {file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"},
]
[package.dependencies]
-colorama = {version = "*", markers = "sys_platform == \"win32\""}
-exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
-iniconfig = "*"
-packaging = "*"
+colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""}
+iniconfig = ">=1.0.1"
+packaging = ">=22"
pluggy = ">=1.5,<2"
-tomli = {version = ">=1", markers = "python_version < \"3.11\""}
+pygments = ">=2.7.2"
[package.extras]
-dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
+dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"]
[[package]]
name = "pytest-base-url"
@@ -1552,6 +2509,7 @@ version = "2.1.0"
description = "pytest plugin for URL based testing"
optional = false
python-versions = ">=3.8"
+groups = ["testing"]
files = [
{file = "pytest_base_url-2.1.0-py3-none-any.whl", hash = "sha256:3ad15611778764d451927b2a53240c1a7a591b521ea44cebfe45849d2d2812e6"},
{file = "pytest_base_url-2.1.0.tar.gz", hash = "sha256:02748589a54f9e63fcbe62301d6b0496da0d10231b753e950c63e03aee745d45"},
@@ -1566,45 +2524,64 @@ test = ["black (>=22.1.0)", "flake8 (>=4.0.1)", "pre-commit (>=2.17.0)", "pytest
[[package]]
name = "pytest-cov"
-version = "6.0.0"
+version = "7.1.0"
description = "Pytest plugin for measuring coverage."
optional = false
python-versions = ">=3.9"
+groups = ["testing"]
files = [
- {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"},
- {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"},
+ {file = "pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678"},
+ {file = "pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2"},
]
[package.dependencies]
-coverage = {version = ">=7.5", extras = ["toml"]}
-pytest = ">=4.6"
+coverage = {version = ">=7.10.6", extras = ["toml"]}
+pluggy = ">=1.2"
+pytest = ">=7"
[package.extras]
-testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"]
+testing = ["process-tests", "pytest-xdist", "virtualenv"]
[[package]]
name = "pytest-playwright"
-version = "0.6.2"
+version = "0.7.2"
description = "A pytest wrapper with fixtures for Playwright to automate web browsers"
optional = false
-python-versions = ">=3.9"
+python-versions = ">=3.10"
+groups = ["testing"]
files = [
- {file = "pytest_playwright-0.6.2-py3-none-any.whl", hash = "sha256:0eff73bebe497b0158befed91e2f5fe94cfa17181f8b3acf575beed84e7e9043"},
- {file = "pytest_playwright-0.6.2.tar.gz", hash = "sha256:ff4054b19aa05df096ac6f74f0572591566aaf0f6d97f6cb9674db8a4d4ed06c"},
+ {file = "pytest_playwright-0.7.2-py3-none-any.whl", hash = "sha256:8084e015b2b3ecff483c2160f1c8219b38b66c0d4578b23c0f700d1b0240ea38"},
+ {file = "pytest_playwright-0.7.2.tar.gz", hash = "sha256:247b61123b28c7e8febb993a187a07e54f14a9aa04edc166f7a976d88f04c770"},
]
[package.dependencies]
playwright = ">=1.18"
-pytest = ">=6.2.4,<9.0.0"
+pytest = ">=6.2.4,<10.0.0"
pytest-base-url = ">=1.0.0,<3.0.0"
python-slugify = ">=6.0.0,<9.0.0"
[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+description = "Extensions to the standard Python datetime module"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+groups = ["main"]
+files = [
+ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
+ {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
+]
+
+[package.dependencies]
+six = ">=1.5"
+
+[[package]]
name = "python-slugify"
version = "8.0.4"
description = "A Python slugify application that also handles Unicode"
optional = false
python-versions = ">=3.7"
+groups = ["testing"]
files = [
{file = "python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856"},
{file = "python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8"},
@@ -1617,107 +2594,198 @@ text-unidecode = ">=1.3"
unidecode = ["Unidecode (>=1.1.1)"]
[[package]]
+name = "pytokens"
+version = "0.4.1"
+description = "A Fast, spec compliant Python 3.14+ tokenizer that runs on older Pythons."
+optional = false
+python-versions = ">=3.8"
+groups = ["linters"]
+files = [
+ {file = "pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5"},
+ {file = "pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe"},
+ {file = "pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c"},
+ {file = "pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7"},
+ {file = "pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2"},
+ {file = "pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440"},
+ {file = "pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc"},
+ {file = "pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d"},
+ {file = "pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16"},
+ {file = "pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6"},
+ {file = "pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083"},
+ {file = "pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1"},
+ {file = "pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1"},
+ {file = "pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9"},
+ {file = "pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68"},
+ {file = "pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b"},
+ {file = "pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f"},
+ {file = "pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1"},
+ {file = "pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4"},
+ {file = "pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78"},
+ {file = "pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321"},
+ {file = "pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa"},
+ {file = "pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d"},
+ {file = "pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324"},
+ {file = "pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9"},
+ {file = "pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb"},
+ {file = "pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3"},
+ {file = "pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975"},
+ {file = "pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a"},
+ {file = "pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918"},
+ {file = "pytokens-0.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:da5baeaf7116dced9c6bb76dc31ba04a2dc3695f3d9f74741d7910122b456edc"},
+ {file = "pytokens-0.4.1-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11edda0942da80ff58c4408407616a310adecae1ddd22eef8c692fe266fa5009"},
+ {file = "pytokens-0.4.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0fc71786e629cef478cbf29d7ea1923299181d0699dbe7c3c0f4a583811d9fc1"},
+ {file = "pytokens-0.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dcafc12c30dbaf1e2af0490978352e0c4041a7cde31f4f81435c2a5e8b9cabb6"},
+ {file = "pytokens-0.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:42f144f3aafa5d92bad964d471a581651e28b24434d184871bd02e3a0d956037"},
+ {file = "pytokens-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:34bcc734bd2f2d5fe3b34e7b3c0116bfb2397f2d9666139988e7a3eb5f7400e3"},
+ {file = "pytokens-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941d4343bf27b605e9213b26bfa1c4bf197c9c599a9627eb7305b0defcfe40c1"},
+ {file = "pytokens-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3ad72b851e781478366288743198101e5eb34a414f1d5627cdd585ca3b25f1db"},
+ {file = "pytokens-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:682fa37ff4d8e95f7df6fe6fe6a431e8ed8e788023c6bcc0f0880a12eab80ad1"},
+ {file = "pytokens-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:30f51edd9bb7f85c748979384165601d028b84f7bd13fe14d3e065304093916a"},
+ {file = "pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de"},
+ {file = "pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a"},
+]
+
+[package.extras]
+dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "twine", "wheel"]
+
+[[package]]
name = "redis"
-version = "5.2.1"
+version = "7.4.0"
description = "Python client for Redis database and key-value store"
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.10"
+groups = ["main"]
files = [
- {file = "redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4"},
- {file = "redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f"},
+ {file = "redis-7.4.0-py3-none-any.whl", hash = "sha256:a9c74a5c893a5ef8455a5adb793a31bb70feb821c86eccb62eebef5a19c429ec"},
+ {file = "redis-7.4.0.tar.gz", hash = "sha256:64a6ea7bf567ad43c964d2c30d82853f8df927c5c9017766c55a1d1ed95d18ad"},
]
[package.dependencies]
async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""}
[package.extras]
-hiredis = ["hiredis (>=3.0.0)"]
-ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==23.2.1)", "requests (>=2.31.0)"]
+circuit-breaker = ["pybreaker (>=1.4.0)"]
+hiredis = ["hiredis (>=3.2.0)"]
+jwt = ["pyjwt (>=2.9.0)"]
+ocsp = ["cryptography (>=36.0.1)", "pyopenssl (>=20.0.1)", "requests (>=2.31.0)"]
+otel = ["opentelemetry-api (>=1.39.1)", "opentelemetry-exporter-otlp-proto-http (>=1.39.1)", "opentelemetry-sdk (>=1.39.1)"]
+xxhash = ["xxhash (>=3.6.0,<3.7.0)"]
[[package]]
name = "requests"
-version = "2.32.3"
+version = "2.34.1"
description = "Python HTTP for Humans."
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.10"
+groups = ["main", "docs", "testing"]
files = [
- {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
- {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
+ {file = "requests-2.34.1-py3-none-any.whl", hash = "sha256:bf38a3ff993960d3dd819c08862c40b3c703306eb7c744fcd9f4ddbb95b548f0"},
+ {file = "requests-2.34.1.tar.gz", hash = "sha256:0fc5669f2b69704449fe1552360bd2a73a54512dfd03e65529157f1513322beb"},
]
[package.dependencies]
-certifi = ">=2017.4.17"
-charset-normalizer = ">=2,<4"
+certifi = ">=2023.5.7"
+charset_normalizer = ">=2,<4"
idna = ">=2.5,<4"
-urllib3 = ">=1.21.1,<3"
+urllib3 = ">=1.26,<3"
[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
-use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
+use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"]
+
+[[package]]
+name = "roman-numerals"
+version = "4.1.0"
+description = "Manipulate well-formed Roman numerals"
+optional = false
+python-versions = ">=3.10"
+groups = ["docs"]
+files = [
+ {file = "roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7"},
+ {file = "roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2"},
+]
[[package]]
name = "setuptools"
-version = "75.8.0"
-description = "Easily download, build, install, upgrade, and uninstall Python packages"
+version = "82.0.1"
+description = "Most extensible Python build backend with support for C/C++ extension modules"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
- {file = "setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3"},
- {file = "setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6"},
+ {file = "setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb"},
+ {file = "setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9"},
]
[package.extras]
-check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"]
-core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"]
+check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.13.0) ; sys_platform != \"cygwin\""]
+core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"]
cover = ["pytest-cov"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
enabler = ["pytest-enabler (>=2.2)"]
-test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
-type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"]
+test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
+type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.18.*)", "pytest-mypy"]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+description = "Python 2 and 3 compatibility utilities"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+groups = ["main"]
+files = [
+ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
+ {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
+]
[[package]]
name = "snowballstemmer"
-version = "2.2.0"
-description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms."
+version = "3.0.1"
+description = "This package provides 32 stemmers for 30 languages generated from Snowball algorithms."
optional = false
-python-versions = "*"
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*"
+groups = ["docs"]
files = [
- {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"},
- {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"},
+ {file = "snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064"},
+ {file = "snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895"},
]
[[package]]
name = "soupsieve"
-version = "2.6"
+version = "2.8.3"
description = "A modern CSS selector implementation for Beautiful Soup."
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
+groups = ["testing"]
files = [
- {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"},
- {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"},
+ {file = "soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95"},
+ {file = "soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349"},
]
[[package]]
name = "sphinx"
-version = "8.1.3"
+version = "9.0.4"
description = "Python documentation generator"
optional = false
-python-versions = ">=3.10"
+python-versions = ">=3.11"
+groups = ["docs"]
+markers = "python_version == \"3.11\""
files = [
- {file = "sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2"},
- {file = "sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927"},
+ {file = "sphinx-9.0.4-py3-none-any.whl", hash = "sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb"},
+ {file = "sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3"},
]
[package.dependencies]
alabaster = ">=0.7.14"
babel = ">=2.13"
colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""}
-docutils = ">=0.20,<0.22"
+docutils = ">=0.20,<0.23"
imagesize = ">=1.3"
Jinja2 = ">=3.1"
packaging = ">=23.0"
Pygments = ">=2.17"
requests = ">=2.30.0"
+roman-numerals = ">=1.0.0"
snowballstemmer = ">=2.2"
sphinxcontrib-applehelp = ">=1.0.7"
sphinxcontrib-devhelp = ">=1.0.6"
@@ -1725,30 +2793,74 @@ sphinxcontrib-htmlhelp = ">=2.0.6"
sphinxcontrib-jsmath = ">=1.0.1"
sphinxcontrib-qthelp = ">=1.0.6"
sphinxcontrib-serializinghtml = ">=1.1.9"
-tomli = {version = ">=2", markers = "python_version < \"3.11\""}
-[package.extras]
-docs = ["sphinxcontrib-websupport"]
-lint = ["flake8 (>=6.0)", "mypy (==1.11.1)", "pyright (==1.1.384)", "pytest (>=6.0)", "ruff (==0.6.9)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-Pillow (==10.2.0.20240822)", "types-Pygments (==2.18.0.20240506)", "types-colorama (==0.4.15.20240311)", "types-defusedxml (==0.7.0.20240218)", "types-docutils (==0.21.0.20241005)", "types-requests (==2.32.0.20240914)", "types-urllib3 (==1.26.25.14)"]
-test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"]
+[[package]]
+name = "sphinx"
+version = "9.1.0"
+description = "Python documentation generator"
+optional = false
+python-versions = ">=3.12"
+groups = ["docs"]
+markers = "python_version >= \"3.12\""
+files = [
+ {file = "sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978"},
+ {file = "sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb"},
+]
+
+[package.dependencies]
+alabaster = ">=0.7.14"
+babel = ">=2.13"
+colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""}
+docutils = ">=0.21,<0.23"
+imagesize = ">=1.3"
+Jinja2 = ">=3.1"
+packaging = ">=23.0"
+Pygments = ">=2.17"
+requests = ">=2.30.0"
+roman-numerals = ">=1.0.0"
+snowballstemmer = ">=2.2"
+sphinxcontrib-applehelp = ">=1.0.7"
+sphinxcontrib-devhelp = ">=1.0.6"
+sphinxcontrib-htmlhelp = ">=2.0.6"
+sphinxcontrib-jsmath = ">=1.0.1"
+sphinxcontrib-qthelp = ">=1.0.6"
+sphinxcontrib-serializinghtml = ">=1.1.9"
[[package]]
name = "sphinx-autodoc-typehints"
-version = "3.0.1"
+version = "3.6.1"
description = "Type hints (PEP 484) support for the Sphinx autodoc extension"
optional = false
-python-versions = ">=3.10"
+python-versions = ">=3.11"
+groups = ["docs"]
+markers = "python_version == \"3.11\""
files = [
- {file = "sphinx_autodoc_typehints-3.0.1-py3-none-any.whl", hash = "sha256:4b64b676a14b5b79cefb6628a6dc8070e320d4963e8ff640a2f3e9390ae9045a"},
- {file = "sphinx_autodoc_typehints-3.0.1.tar.gz", hash = "sha256:b9b40dd15dee54f6f810c924f863f9cf1c54f9f3265c495140ea01be7f44fa55"},
+ {file = "sphinx_autodoc_typehints-3.6.1-py3-none-any.whl", hash = "sha256:dd818ba31d4c97f219a8c0fcacef280424f84a3589cedcb73003ad99c7da41ca"},
+ {file = "sphinx_autodoc_typehints-3.6.1.tar.gz", hash = "sha256:fa0b686ae1b85965116c88260e5e4b82faec3687c2e94d6a10f9b36c3743e2fe"},
]
[package.dependencies]
-sphinx = ">=8.1.3"
+sphinx = ">=9.0.4"
[package.extras]
-docs = ["furo (>=2024.8.6)"]
-testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "defusedxml (>=0.7.1)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "sphobjinv (>=2.3.1.2)", "typing-extensions (>=4.12.2)"]
+docs = ["furo (>=2025.9.25)"]
+testing = ["covdefaults (>=2.3)", "coverage (>=7.13)", "defusedxml (>=0.7.1)", "diff-cover (>=9.7.2)", "pytest (>=9.0.2)", "pytest-cov (>=7)", "sphobjinv (>=2.3.1.3)", "typing-extensions (>=4.15)"]
+
+[[package]]
+name = "sphinx-autodoc-typehints"
+version = "3.10.2"
+description = "Type hints (PEP 484) support for the Sphinx autodoc extension"
+optional = false
+python-versions = ">=3.12"
+groups = ["docs"]
+markers = "python_version >= \"3.12\""
+files = [
+ {file = "sphinx_autodoc_typehints-3.10.2-py3-none-any.whl", hash = "sha256:2d1bcac8f62b8d0d210f6ca44b8e016e314d07cc8c30d0512cf6986a0b042b26"},
+ {file = "sphinx_autodoc_typehints-3.10.2.tar.gz", hash = "sha256:34db651eb14343ba16bd9c03e0f5093971f9bbd3cc6b0a1470adecd578940b9c"},
+]
+
+[package.dependencies]
+sphinx = ">=9.1"
[[package]]
name = "sphinxcontrib-applehelp"
@@ -1756,6 +2868,7 @@ version = "2.0.0"
description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books"
optional = false
python-versions = ">=3.9"
+groups = ["docs"]
files = [
{file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"},
{file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"},
@@ -1772,6 +2885,7 @@ version = "2.0.0"
description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents"
optional = false
python-versions = ">=3.9"
+groups = ["docs"]
files = [
{file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"},
{file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"},
@@ -1788,6 +2902,7 @@ version = "2.1.0"
description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files"
optional = false
python-versions = ">=3.9"
+groups = ["docs"]
files = [
{file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"},
{file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"},
@@ -1804,6 +2919,7 @@ version = "1.0.1"
description = "A sphinx extension which renders display math in HTML via JavaScript"
optional = false
python-versions = ">=3.5"
+groups = ["docs"]
files = [
{file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"},
{file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"},
@@ -1818,6 +2934,7 @@ version = "2.0.0"
description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents"
optional = false
python-versions = ">=3.9"
+groups = ["docs"]
files = [
{file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"},
{file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"},
@@ -1834,6 +2951,7 @@ version = "2.0.0"
description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)"
optional = false
python-versions = ">=3.9"
+groups = ["docs"]
files = [
{file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"},
{file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"},
@@ -1846,81 +2964,87 @@ test = ["pytest"]
[[package]]
name = "sqlalchemy"
-version = "2.0.37"
+version = "2.0.49"
description = "Database Abstraction Library"
optional = false
python-versions = ">=3.7"
-files = [
- {file = "SQLAlchemy-2.0.37-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da36c3b0e891808a7542c5c89f224520b9a16c7f5e4d6a1156955605e54aef0e"},
- {file = "SQLAlchemy-2.0.37-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e7402ff96e2b073a98ef6d6142796426d705addd27b9d26c3b32dbaa06d7d069"},
- {file = "SQLAlchemy-2.0.37-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6f5d254a22394847245f411a2956976401e84da4288aa70cbcd5190744062c1"},
- {file = "SQLAlchemy-2.0.37-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41296bbcaa55ef5fdd32389a35c710133b097f7b2609d8218c0eabded43a1d84"},
- {file = "SQLAlchemy-2.0.37-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bedee60385c1c0411378cbd4dc486362f5ee88deceea50002772912d798bb00f"},
- {file = "SQLAlchemy-2.0.37-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6c67415258f9f3c69867ec02fea1bf6508153709ecbd731a982442a590f2b7e4"},
- {file = "SQLAlchemy-2.0.37-cp310-cp310-win32.whl", hash = "sha256:650dcb70739957a492ad8acff65d099a9586b9b8920e3507ca61ec3ce650bb72"},
- {file = "SQLAlchemy-2.0.37-cp310-cp310-win_amd64.whl", hash = "sha256:93d1543cd8359040c02b6614421c8e10cd7a788c40047dbc507ed46c29ae5636"},
- {file = "SQLAlchemy-2.0.37-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:78361be6dc9073ed17ab380985d1e45e48a642313ab68ab6afa2457354ff692c"},
- {file = "SQLAlchemy-2.0.37-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b661b49d0cb0ab311a189b31e25576b7ac3e20783beb1e1817d72d9d02508bf5"},
- {file = "SQLAlchemy-2.0.37-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d57bafbab289e147d064ffbd5cca2d7b1394b63417c0636cea1f2e93d16eb9e8"},
- {file = "SQLAlchemy-2.0.37-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa2c0913f02341d25fb858e4fb2031e6b0813494cca1ba07d417674128ce11b"},
- {file = "SQLAlchemy-2.0.37-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9df21b8d9e5c136ea6cde1c50d2b1c29a2b5ff2b1d610165c23ff250e0704087"},
- {file = "SQLAlchemy-2.0.37-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db18ff6b8c0f1917f8b20f8eca35c28bbccb9f83afa94743e03d40203ed83de9"},
- {file = "SQLAlchemy-2.0.37-cp311-cp311-win32.whl", hash = "sha256:46954173612617a99a64aee103bcd3f078901b9a8dcfc6ae80cbf34ba23df989"},
- {file = "SQLAlchemy-2.0.37-cp311-cp311-win_amd64.whl", hash = "sha256:7b7e772dc4bc507fdec4ee20182f15bd60d2a84f1e087a8accf5b5b7a0dcf2ba"},
- {file = "SQLAlchemy-2.0.37-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2952748ecd67ed3b56773c185e85fc084f6bdcdec10e5032a7c25a6bc7d682ef"},
- {file = "SQLAlchemy-2.0.37-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3151822aa1db0eb5afd65ccfafebe0ef5cda3a7701a279c8d0bf17781a793bb4"},
- {file = "SQLAlchemy-2.0.37-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eaa8039b6d20137a4e02603aba37d12cd2dde7887500b8855356682fc33933f4"},
- {file = "SQLAlchemy-2.0.37-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cdba1f73b64530c47b27118b7053b8447e6d6f3c8104e3ac59f3d40c33aa9fd"},
- {file = "SQLAlchemy-2.0.37-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1b2690456528a87234a75d1a1644cdb330a6926f455403c8e4f6cad6921f9098"},
- {file = "SQLAlchemy-2.0.37-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf5ae8a9dcf657fd72144a7fd01f243236ea39e7344e579a121c4205aedf07bb"},
- {file = "SQLAlchemy-2.0.37-cp312-cp312-win32.whl", hash = "sha256:ea308cec940905ba008291d93619d92edaf83232ec85fbd514dcb329f3192761"},
- {file = "SQLAlchemy-2.0.37-cp312-cp312-win_amd64.whl", hash = "sha256:635d8a21577341dfe4f7fa59ec394b346da12420b86624a69e466d446de16aff"},
- {file = "SQLAlchemy-2.0.37-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8c4096727193762e72ce9437e2a86a110cf081241919ce3fab8e89c02f6b6658"},
- {file = "SQLAlchemy-2.0.37-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e4fb5ac86d8fe8151966814f6720996430462e633d225497566b3996966b9bdb"},
- {file = "SQLAlchemy-2.0.37-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e56a139bfe136a22c438478a86f8204c1eb5eed36f4e15c4224e4b9db01cb3e4"},
- {file = "SQLAlchemy-2.0.37-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f95fc8e3f34b5f6b3effb49d10ac97c569ec8e32f985612d9b25dd12d0d2e94"},
- {file = "SQLAlchemy-2.0.37-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c505edd429abdfe3643fa3b2e83efb3445a34a9dc49d5f692dd087be966020e0"},
- {file = "SQLAlchemy-2.0.37-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:12b0f1ec623cccf058cf21cb544f0e74656618165b083d78145cafde156ea7b6"},
- {file = "SQLAlchemy-2.0.37-cp313-cp313-win32.whl", hash = "sha256:293f9ade06b2e68dd03cfb14d49202fac47b7bb94bffcff174568c951fbc7af2"},
- {file = "SQLAlchemy-2.0.37-cp313-cp313-win_amd64.whl", hash = "sha256:d70f53a0646cc418ca4853da57cf3ddddbccb8c98406791f24426f2dd77fd0e2"},
- {file = "SQLAlchemy-2.0.37-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:44f569d0b1eb82301b92b72085583277316e7367e038d97c3a1a899d9a05e342"},
- {file = "SQLAlchemy-2.0.37-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2eae3423e538c10d93ae3e87788c6a84658c3ed6db62e6a61bb9495b0ad16bb"},
- {file = "SQLAlchemy-2.0.37-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfff7be361048244c3aa0f60b5e63221c5e0f0e509f4e47b8910e22b57d10ae7"},
- {file = "SQLAlchemy-2.0.37-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:5bc3339db84c5fb9130ac0e2f20347ee77b5dd2596ba327ce0d399752f4fce39"},
- {file = "SQLAlchemy-2.0.37-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:84b9f23b0fa98a6a4b99d73989350a94e4a4ec476b9a7dfe9b79ba5939f5e80b"},
- {file = "SQLAlchemy-2.0.37-cp37-cp37m-win32.whl", hash = "sha256:51bc9cfef83e0ac84f86bf2b10eaccb27c5a3e66a1212bef676f5bee6ef33ebb"},
- {file = "SQLAlchemy-2.0.37-cp37-cp37m-win_amd64.whl", hash = "sha256:8e47f1af09444f87c67b4f1bb6231e12ba6d4d9f03050d7fc88df6d075231a49"},
- {file = "SQLAlchemy-2.0.37-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6b788f14c5bb91db7f468dcf76f8b64423660a05e57fe277d3f4fad7b9dcb7ce"},
- {file = "SQLAlchemy-2.0.37-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521ef85c04c33009166777c77e76c8a676e2d8528dc83a57836b63ca9c69dcd1"},
- {file = "SQLAlchemy-2.0.37-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75311559f5c9881a9808eadbeb20ed8d8ba3f7225bef3afed2000c2a9f4d49b9"},
- {file = "SQLAlchemy-2.0.37-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cce918ada64c956b62ca2c2af59b125767097ec1dca89650a6221e887521bfd7"},
- {file = "SQLAlchemy-2.0.37-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9d087663b7e1feabea8c578d6887d59bb00388158e8bff3a76be11aa3f748ca2"},
- {file = "SQLAlchemy-2.0.37-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cf95a60b36997dad99692314c4713f141b61c5b0b4cc5c3426faad570b31ca01"},
- {file = "SQLAlchemy-2.0.37-cp38-cp38-win32.whl", hash = "sha256:d75ead7dd4d255068ea0f21492ee67937bd7c90964c8f3c2bea83c7b7f81b95f"},
- {file = "SQLAlchemy-2.0.37-cp38-cp38-win_amd64.whl", hash = "sha256:74bbd1d0a9bacf34266a7907d43260c8d65d31d691bb2356f41b17c2dca5b1d0"},
- {file = "SQLAlchemy-2.0.37-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:648ec5acf95ad59255452ef759054f2176849662af4521db6cb245263ae4aa33"},
- {file = "SQLAlchemy-2.0.37-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:35bd2df269de082065d4b23ae08502a47255832cc3f17619a5cea92ce478b02b"},
- {file = "SQLAlchemy-2.0.37-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f581d365af9373a738c49e0c51e8b18e08d8a6b1b15cc556773bcd8a192fa8b"},
- {file = "SQLAlchemy-2.0.37-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82df02816c14f8dc9f4d74aea4cb84a92f4b0620235daa76dde002409a3fbb5a"},
- {file = "SQLAlchemy-2.0.37-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94b564e38b344d3e67d2e224f0aec6ba09a77e4582ced41e7bfd0f757d926ec9"},
- {file = "SQLAlchemy-2.0.37-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:955a2a765aa1bd81aafa69ffda179d4fe3e2a3ad462a736ae5b6f387f78bfeb8"},
- {file = "SQLAlchemy-2.0.37-cp39-cp39-win32.whl", hash = "sha256:03f0528c53ca0b67094c4764523c1451ea15959bbf0a8a8a3096900014db0278"},
- {file = "SQLAlchemy-2.0.37-cp39-cp39-win_amd64.whl", hash = "sha256:4b12885dc85a2ab2b7d00995bac6d967bffa8594123b02ed21e8eb2205a7584b"},
- {file = "SQLAlchemy-2.0.37-py3-none-any.whl", hash = "sha256:a8998bf9f8658bd3839cbc44ddbe982955641863da0c1efe5b00c1ab4f5c16b1"},
- {file = "sqlalchemy-2.0.37.tar.gz", hash = "sha256:12b28d99a9c14eaf4055810df1001557176716de0167b91026e648e65229bffb"},
+groups = ["main"]
+files = [
+ {file = "sqlalchemy-2.0.49-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:42e8804962f9e6f4be2cbaedc0c3718f08f60a16910fa3d86da5a1e3b1bfe60f"},
+ {file = "sqlalchemy-2.0.49-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc992c6ed024c8c3c592c5fc9846a03dd68a425674900c70122c77ea16c5fb0b"},
+ {file = "sqlalchemy-2.0.49-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eb188b84269f357669b62cb576b5b918de10fb7c728a005fa0ebb0b758adce1"},
+ {file = "sqlalchemy-2.0.49-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:62557958002b69699bdb7f5137c6714ca1133f045f97b3903964f47db97ea339"},
+ {file = "sqlalchemy-2.0.49-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da9b91bca419dc9b9267ffadde24eae9b1a6bffcd09d0a207e5e3af99a03ce0d"},
+ {file = "sqlalchemy-2.0.49-cp310-cp310-win32.whl", hash = "sha256:5e61abbec255be7b122aa461021daa7c3f310f3e743411a67079f9b3cc91ece3"},
+ {file = "sqlalchemy-2.0.49-cp310-cp310-win_amd64.whl", hash = "sha256:0c98c59075b890df8abfcc6ad632879540f5791c68baebacb4f833713b510e75"},
+ {file = "sqlalchemy-2.0.49-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5070135e1b7409c4161133aa525419b0062088ed77c92b1da95366ec5cbebbe"},
+ {file = "sqlalchemy-2.0.49-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ac7a3e245fd0310fd31495eb61af772e637bdf7d88ee81e7f10a3f271bff014"},
+ {file = "sqlalchemy-2.0.49-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d4e5a0ceba319942fa6b585cf82539288a61e314ef006c1209f734551ab9536"},
+ {file = "sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ddcb27fb39171de36e207600116ac9dfd4ae46f86c82a9bf3934043e80ebb88"},
+ {file = "sqlalchemy-2.0.49-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:32fe6a41ad97302db2931f05bb91abbcc65b5ce4c675cd44b972428dd2947700"},
+ {file = "sqlalchemy-2.0.49-cp311-cp311-win32.whl", hash = "sha256:46d51518d53edfbe0563662c96954dc8fcace9832332b914375f45a99b77cc9a"},
+ {file = "sqlalchemy-2.0.49-cp311-cp311-win_amd64.whl", hash = "sha256:951d4a210744813be63019f3df343bf233b7432aadf0db54c75802247330d3af"},
+ {file = "sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbccb45260e4ff1b7db0be80a9025bb1e6698bdb808b83fff0000f7a90b2c0b"},
+ {file = "sqlalchemy-2.0.49-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb37f15714ec2652d574f021d479e78cd4eb9d04396dca36568fdfffb3487982"},
+ {file = "sqlalchemy-2.0.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb9ec6436a820a4c006aad1ac351f12de2f2dbdaad171692ee457a02429b672"},
+ {file = "sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8d6efc136f44a7e8bc8088507eaabbb8c2b55b3dbb63fe102c690da0ddebe55e"},
+ {file = "sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e06e617e3d4fd9e51d385dfe45b077a41e9d1b033a7702551e3278ac597dc750"},
+ {file = "sqlalchemy-2.0.49-cp312-cp312-win32.whl", hash = "sha256:83101a6930332b87653886c01d1ee7e294b1fe46a07dd9a2d2b4f91bcc88eec0"},
+ {file = "sqlalchemy-2.0.49-cp312-cp312-win_amd64.whl", hash = "sha256:618a308215b6cececb6240b9abde545e3acdabac7ae3e1d4e666896bf5ba44b4"},
+ {file = "sqlalchemy-2.0.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df2d441bacf97022e81ad047e1597552eb3f83ca8a8f1a1fdd43cd7fe3898120"},
+ {file = "sqlalchemy-2.0.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e20e511dc15265fb433571391ba313e10dd8ea7e509d51686a51313b4ac01a2"},
+ {file = "sqlalchemy-2.0.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47604cb2159f8bbd5a1ab48a714557156320f20871ee64d550d8bf2683d980d3"},
+ {file = "sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:22d8798819f86720bc646ab015baff5ea4c971d68121cb36e2ebc2ee43ead2b7"},
+ {file = "sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1c058c171b739e7c330760044803099c7fff11511e3ab3573e5327116a9c33"},
+ {file = "sqlalchemy-2.0.49-cp313-cp313-win32.whl", hash = "sha256:a143af2ea6672f2af3f44ed8f9cd020e9cc34c56f0e8db12019d5d9ecf41cb3b"},
+ {file = "sqlalchemy-2.0.49-cp313-cp313-win_amd64.whl", hash = "sha256:12b04d1db2663b421fe072d638a138460a51d5a862403295671c4f3987fb9148"},
+ {file = "sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24bd94bb301ec672d8f0623eba9226cc90d775d25a0c92b5f8e4965d7f3a1518"},
+ {file = "sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a51d3db74ba489266ef55c7a4534eb0b8db9a326553df481c11e5d7660c8364d"},
+ {file = "sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:55250fe61d6ebfd6934a272ee16ef1244e0f16b7af6cd18ab5b1fc9f08631db0"},
+ {file = "sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:46796877b47034b559a593d7e4b549aba151dae73f9e78212a3478161c12ab08"},
+ {file = "sqlalchemy-2.0.49-cp313-cp313t-win32.whl", hash = "sha256:9c4969a86e41454f2858256c39bdfb966a20961e9b58bf8749b65abf447e9a8d"},
+ {file = "sqlalchemy-2.0.49-cp313-cp313t-win_amd64.whl", hash = "sha256:b9870d15ef00e4d0559ae10ee5bc71b654d1f20076dbe8bc7ed19b4c0625ceba"},
+ {file = "sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:233088b4b99ebcbc5258c755a097aa52fbf90727a03a5a80781c4b9c54347a2e"},
+ {file = "sqlalchemy-2.0.49-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57ca426a48eb2c682dae8204cd89ea8ab7031e2675120a47924fabc7caacbc2a"},
+ {file = "sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685e93e9c8f399b0c96a624799820176312f5ceef958c0f88215af4013d29066"},
+ {file = "sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e0400fa22f79acc334d9a6b185dc00a44a8e6578aa7e12d0ddcd8434152b187"},
+ {file = "sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a05977bffe9bffd2229f477fa75eabe3192b1b05f408961d1bebff8d1cd4d401"},
+ {file = "sqlalchemy-2.0.49-cp314-cp314-win32.whl", hash = "sha256:0f2fa354ba106eafff2c14b0cc51f22801d1e8b2e4149342023bd6f0955de5f5"},
+ {file = "sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl", hash = "sha256:77641d299179c37b89cf2343ca9972c88bb6eef0d5fc504a2f86afd15cd5adf5"},
+ {file = "sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1dc3368794d522f43914e03312202523cc89692f5389c32bea0233924f8d977"},
+ {file = "sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c821c47ecfe05cc32140dcf8dc6fd5d21971c86dbd56eabfe5ba07a64910c01"},
+ {file = "sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9c04bff9a5335eb95c6ecf1c117576a0aa560def274876fd156cfe5510fccc61"},
+ {file = "sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7f605a456948c35260e7b2a39f8952a26f077fd25653c37740ed186b90aaa68a"},
+ {file = "sqlalchemy-2.0.49-cp314-cp314t-win32.whl", hash = "sha256:6270d717b11c5476b0cbb21eedc8d4dbb7d1a956fd6c15a23e96f197a6193158"},
+ {file = "sqlalchemy-2.0.49-cp314-cp314t-win_amd64.whl", hash = "sha256:275424295f4256fd301744b8f335cff367825d270f155d522b30c7bf49903ee7"},
+ {file = "sqlalchemy-2.0.49-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8a97ac839c2c6672c4865e48f3cbad7152cee85f4233fb4ca6291d775b9b954a"},
+ {file = "sqlalchemy-2.0.49-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c338ec6ec01c0bc8e735c58b9f5d51e75bacb6ff23296658826d7cfdfdb8678a"},
+ {file = "sqlalchemy-2.0.49-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:566df36fd0e901625523a5a1835032f1ebdd7f7886c54584143fa6c668b4df3b"},
+ {file = "sqlalchemy-2.0.49-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d99945830a6f3e9638d89a28ed130b1eb24c91255e4f24366fbe699b983f29e4"},
+ {file = "sqlalchemy-2.0.49-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:01146546d84185f12721a1d2ce0c6673451a7894d1460b592d378ca4871a0c72"},
+ {file = "sqlalchemy-2.0.49-cp38-cp38-win32.whl", hash = "sha256:69469ce8ce7a8df4d37620e3163b71238719e1e2e5048d114a1b6ce0fbf8c662"},
+ {file = "sqlalchemy-2.0.49-cp38-cp38-win_amd64.whl", hash = "sha256:b95b2f470c1b2683febd2e7eab1d3f0e078c91dbdd0b00e9c645d07a413bb99f"},
+ {file = "sqlalchemy-2.0.49-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43d044780732d9e0381ac8d5316f95d7f02ef04d6e4ef6dc82379f09795d993f"},
+ {file = "sqlalchemy-2.0.49-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d6be30b2a75362325176c036d7fb8d19e8846c77e87683ffaa8177b35135613"},
+ {file = "sqlalchemy-2.0.49-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d898cc2c76c135ef65517f4ddd7a3512fb41f23087b0650efb3418b8389a3cd1"},
+ {file = "sqlalchemy-2.0.49-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:059d7151fff513c53a4638da8778be7fce81a0c4854c7348ebd0c4078ddf28fe"},
+ {file = "sqlalchemy-2.0.49-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:334edbcff10514ad1d66e3a70b339c0a29886394892490119dbb669627b17717"},
+ {file = "sqlalchemy-2.0.49-cp39-cp39-win32.whl", hash = "sha256:74ab4ee7794d7ed1b0c37e7333640e0f0a626fc7b398c07a7aef52f484fddde3"},
+ {file = "sqlalchemy-2.0.49-cp39-cp39-win_amd64.whl", hash = "sha256:88690f4e1f0fbf5339bedbb127e240fec1fd3070e9934c0b7bef83432f779d2f"},
+ {file = "sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0"},
+ {file = "sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f"},
]
[package.dependencies]
-greenlet = {version = "!=0.4.17", markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"}
-mypy = {version = ">=0.910", optional = true, markers = "extra == \"mypy\""}
+greenlet = {version = ">=1", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""}
typing-extensions = ">=4.6.0"
[package.extras]
-aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"]
-aioodbc = ["aioodbc", "greenlet (!=0.4.17)"]
-aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"]
-asyncio = ["greenlet (!=0.4.17)"]
-asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"]
+aiomysql = ["aiomysql (>=0.2.0)", "greenlet (>=1)"]
+aioodbc = ["aioodbc", "greenlet (>=1)"]
+aiosqlite = ["aiosqlite", "greenlet (>=1)", "typing_extensions (!=3.10.0.1)"]
+asyncio = ["greenlet (>=1)"]
+asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (>=1)"]
mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"]
mssql = ["pyodbc"]
mssql-pymssql = ["pymssql"]
@@ -1931,7 +3055,7 @@ mysql-connector = ["mysql-connector-python"]
oracle = ["cx_oracle (>=8)"]
oracle-oracledb = ["oracledb (>=1.0.1)"]
postgresql = ["psycopg2 (>=2.7)"]
-postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"]
+postgresql-asyncpg = ["asyncpg", "greenlet (>=1)"]
postgresql-pg8000 = ["pg8000 (>=1.29.1)"]
postgresql-psycopg = ["psycopg (>=3.0.7)"]
postgresql-psycopg2binary = ["psycopg2-binary"]
@@ -1942,13 +3066,14 @@ sqlcipher = ["sqlcipher3_binary"]
[[package]]
name = "termcolor"
-version = "2.5.0"
+version = "3.3.0"
description = "ANSI color formatting for output in terminal"
optional = false
-python-versions = ">=3.9"
+python-versions = ">=3.10"
+groups = ["main"]
files = [
- {file = "termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8"},
- {file = "termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f"},
+ {file = "termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5"},
+ {file = "termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5"},
]
[package.extras]
@@ -1960,72 +3085,34 @@ version = "1.3"
description = "The most basic Text::Unidecode port"
optional = false
python-versions = "*"
+groups = ["testing"]
files = [
{file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"},
{file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"},
]
[[package]]
-name = "tomli"
-version = "2.2.1"
-description = "A lil' TOML parser"
-optional = false
-python-versions = ">=3.8"
-files = [
- {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
- {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
- {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"},
- {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"},
- {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"},
- {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"},
- {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"},
- {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"},
- {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"},
- {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"},
- {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"},
- {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"},
- {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"},
- {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"},
- {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"},
- {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"},
- {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"},
- {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"},
- {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"},
- {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"},
- {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"},
- {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"},
- {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"},
- {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"},
- {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"},
- {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"},
- {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"},
- {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"},
- {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"},
- {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"},
- {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"},
- {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"},
-]
-
-[[package]]
name = "tomlkit"
-version = "0.13.2"
+version = "0.15.0"
description = "Style preserving TOML library"
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
+groups = ["linters"]
files = [
- {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"},
- {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"},
+ {file = "tomlkit-0.15.0-py3-none-any.whl", hash = "sha256:4dbc8f0fc024412b57ced8757ac7461305126a648ff8c2c807fcb8e133a78738"},
+ {file = "tomlkit-0.15.0.tar.gz", hash = "sha256:7d1a9ecba3086638211b13814ea79c90dd54dd11993564376f3aa92271f5c7a3"},
]
[[package]]
name = "transaction"
-version = "5.0"
+version = "5.1"
description = "Transaction management for Python"
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.10"
+groups = ["main"]
files = [
- {file = "transaction-5.0-py3-none-any.whl", hash = "sha256:b4c0b2d49a042d86235fa76531c3356b66d7635bb0e9f29ba2512915fc7b7a42"},
- {file = "transaction-5.0.tar.gz", hash = "sha256:106e7bd782bcc0cb5119fc9225b0c9a71dfc53adb938be905223adaef22b1174"},
+ {file = "transaction-5.1-py3-none-any.whl", hash = "sha256:05aa77c4543cec8b541565ad8b304857490878605ed727851bc5a531b9060789"},
+ {file = "transaction-5.1.tar.gz", hash = "sha256:f6ca8639a3e54f6991de0d9c0983a7cd91acb21a825cb544c168269be956bd41"},
]
[package.dependencies]
@@ -2041,6 +3128,7 @@ version = "1.4"
description = "Utility library for i18n relied on by various Repoze and Pyramid packages"
optional = false
python-versions = "*"
+groups = ["main"]
files = [
{file = "translationstring-1.4-py2.py3-none-any.whl", hash = "sha256:5f4dc4d939573db851c8d840551e1a0fb27b946afe3b95aafc22577eed2d6262"},
{file = "translationstring-1.4.tar.gz", hash = "sha256:bf947538d76e69ba12ab17283b10355a9ecfbc078e6123443f43f2107f6376f3"},
@@ -2055,6 +3143,7 @@ version = "2.11.0.15"
description = "Typing stubs for babel"
optional = false
python-versions = "*"
+groups = ["types"]
files = [
{file = "types-babel-2.11.0.15.tar.gz", hash = "sha256:282c184c8c9d81e8269212c1b8fa0d39ee88fb8bc43be47980412781c9c85f7e"},
{file = "types_babel-2.11.0.15-py3-none-any.whl", hash = "sha256:d0579f2e8adeaef3fbe2eb63e5a2ecf01767fc018e5f3f36a3c9d8b723bd62c7"},
@@ -2066,13 +3155,14 @@ types-setuptools = "*"
[[package]]
name = "types-cffi"
-version = "1.16.0.20241221"
+version = "2.0.0.20260508"
description = "Typing stubs for cffi"
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.10"
+groups = ["types"]
files = [
- {file = "types_cffi-1.16.0.20241221-py3-none-any.whl", hash = "sha256:e5b76b4211d7a9185f6ab8d06a106d56c7eb80af7cdb8bfcb4186ade10fb112f"},
- {file = "types_cffi-1.16.0.20241221.tar.gz", hash = "sha256:1c96649618f4b6145f58231acb976e0b448be6b847f7ab733dabe62dfbff6591"},
+ {file = "types_cffi-2.0.0.20260508-py3-none-any.whl", hash = "sha256:d094065daf4edcfbdd3e11c37d2fa9511eaf7c509da7a9d9573c276398a8e745"},
+ {file = "types_cffi-2.0.0.20260508.tar.gz", hash = "sha256:746b081b4bf84f9d8855c517a67c2dff717f3c18657fcff8e9c251fb5778f311"},
]
[package.dependencies]
@@ -2080,13 +3170,14 @@ types-setuptools = "*"
[[package]]
name = "types-markdown"
-version = "3.7.0.20241204"
+version = "3.10.2.20260508"
description = "Typing stubs for Markdown"
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.10"
+groups = ["types"]
files = [
- {file = "types_Markdown-3.7.0.20241204-py3-none-any.whl", hash = "sha256:f96146c367ea9c82bfe9903559d72706555cc2a1a3474c58ebba03b418ab18da"},
- {file = "types_markdown-3.7.0.20241204.tar.gz", hash = "sha256:ecca2b25cd23163fd28ed5ba34d183d731da03e8a5ed3a20b60daded304c5410"},
+ {file = "types_markdown-3.10.2.20260508-py3-none-any.whl", hash = "sha256:2ee5e462c7b821c27c3d8f75f4da873d3e3c87d24d7f2d741f8f4261e4024a99"},
+ {file = "types_markdown-3.10.2.20260508.tar.gz", hash = "sha256:64c405a30decd46cf78443bbcde8331f561f83752cc6d962ff3f5ccaabe26a8e"},
]
[[package]]
@@ -2095,6 +3186,7 @@ version = "24.1.0.20240722"
description = "Typing stubs for pyOpenSSL"
optional = false
python-versions = ">=3.8"
+groups = ["types"]
files = [
{file = "types-pyOpenSSL-24.1.0.20240722.tar.gz", hash = "sha256:47913b4678a01d879f503a12044468221ed8576263c1540dcb0484ca21b08c39"},
{file = "types_pyOpenSSL-24.1.0.20240722-py3-none-any.whl", hash = "sha256:6a7a5d2ec042537934cfb4c9d4deb0e16c4c6250b09358df1f083682fe6fda54"},
@@ -2106,13 +3198,14 @@ types-cffi = "*"
[[package]]
name = "types-pytz"
-version = "2024.2.0.20241221"
+version = "2026.2.0.20260506"
description = "Typing stubs for pytz"
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.10"
+groups = ["types"]
files = [
- {file = "types_pytz-2024.2.0.20241221-py3-none-any.whl", hash = "sha256:8fc03195329c43637ed4f593663df721fef919b60a969066e22606edf0b53ad5"},
- {file = "types_pytz-2024.2.0.20241221.tar.gz", hash = "sha256:06d7cde9613e9f7504766a0554a270c369434b50e00975b3a4a0f6eed0f2c1a9"},
+ {file = "types_pytz-2026.2.0.20260506-py3-none-any.whl", hash = "sha256:58ab5307c20885f9bcd42ff106616eb0e32710791f8cbdc770aee2ea0c4f01fb"},
+ {file = "types_pytz-2026.2.0.20260506.tar.gz", hash = "sha256:fc6a0de6a1b7da82a748fb4065e152372dac3016559cb1eef5e8af1e338eb627"},
]
[[package]]
@@ -2121,6 +3214,7 @@ version = "4.6.0.20241004"
description = "Typing stubs for redis"
optional = false
python-versions = ">=3.8"
+groups = ["types"]
files = [
{file = "types-redis-4.6.0.20241004.tar.gz", hash = "sha256:5f17d2b3f9091ab75384153bfa276619ffa1cf6a38da60e10d5e6749cc5b902e"},
{file = "types_redis-4.6.0.20241004-py3-none-any.whl", hash = "sha256:ef5da68cb827e5f606c8f9c0b49eeee4c2669d6d97122f301d3a55dc6a63f6ed"},
@@ -2132,13 +3226,14 @@ types-pyOpenSSL = "*"
[[package]]
name = "types-requests"
-version = "2.32.0.20241016"
+version = "2.33.0.20260513"
description = "Typing stubs for requests"
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.10"
+groups = ["types"]
files = [
- {file = "types-requests-2.32.0.20241016.tar.gz", hash = "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95"},
- {file = "types_requests-2.32.0.20241016-py3-none-any.whl", hash = "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747"},
+ {file = "types_requests-2.33.0.20260513-py3-none-any.whl", hash = "sha256:d5a965f9d18b6e06b72039a69565de9027e58f36a7f709857da747fbe7521122"},
+ {file = "types_requests-2.33.0.20260513.tar.gz", hash = "sha256:bd845450e954e751373d5d33526742592f298808a3ee3bda7e858e46b839b57f"},
]
[package.dependencies]
@@ -2146,42 +3241,88 @@ urllib3 = ">=2"
[[package]]
name = "types-setuptools"
-version = "75.8.0.20250110"
+version = "82.0.0.20260508"
description = "Typing stubs for setuptools"
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.10"
+groups = ["types"]
files = [
- {file = "types_setuptools-75.8.0.20250110-py3-none-any.whl", hash = "sha256:a9f12980bbf9bcdc23ecd80755789085bad6bfce4060c2275bc2b4ca9f2bc480"},
- {file = "types_setuptools-75.8.0.20250110.tar.gz", hash = "sha256:96f7ec8bbd6e0a54ea180d66ad68ad7a1d7954e7281a710ea2de75e355545271"},
+ {file = "types_setuptools-82.0.0.20260508-py3-none-any.whl", hash = "sha256:ba1d863bbd11526d7232bca8d5a4aebe1d38fa1677a550f47a2692b7d5776900"},
+ {file = "types_setuptools-82.0.0.20260508.tar.gz", hash = "sha256:e76ade6f42ba9b4211636b84b65a8e55948a67ffe81f9a44e66b8af93d57e77e"},
]
[[package]]
name = "typing-extensions"
-version = "4.12.2"
-description = "Backported and Experimental Type Hints for Python 3.8+"
+version = "4.15.0"
+description = "Backported and Experimental Type Hints for Python 3.9+"
optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
+groups = ["main", "testing", "types"]
files = [
- {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
- {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
+ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
+ {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.2"
+description = "Runtime typing introspection tools"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"},
+ {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"},
+]
+
+[package.dependencies]
+typing-extensions = ">=4.12.0"
+
+[[package]]
+name = "typst"
+version = "0.14.9"
+description = "Python binding to Typst, a new markup-based typesetting system that is powerful and easy to learn."
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "typst-0.14.9-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:abc1348393390173ba7b993389b06b3df4b635cf69205aedaa05ce51de9be3b6"},
+ {file = "typst-0.14.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e3d62e29ce38c45e94e81716383c153272438964b76c343b9a2183fe52a9ff02"},
+ {file = "typst-0.14.9-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f054414502fd21b84474bdd1c92263be1910e010a5a6bdb0d546d7a00131f91"},
+ {file = "typst-0.14.9-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d2676e5b6548ae83b13ff3b769f5f30cf5d716b712502ea64a8e98fc3ae4577f"},
+ {file = "typst-0.14.9-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:456ecf0e9bf02748fb3d7b6f0176aff36027ee8c2e81056eca20b3d4ba54e82b"},
+ {file = "typst-0.14.9-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a67963bd4e3b6969eee412a2bf49753276cd95be0acc86c07fd0c06f188a41bc"},
+ {file = "typst-0.14.9-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eabac1449c75381828996eaf352bfd795857668834a4c9cc6308d7687352ed49"},
+ {file = "typst-0.14.9-cp314-cp314t-win_amd64.whl", hash = "sha256:b5637861645b0abb2529e1006a8984a831ecd9d24610a60e99d3ae0af4e4691a"},
+ {file = "typst-0.14.9-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:4c4404891b3c8bfbb01d9e9308a24b9637088fb41aea0f24b58a9dffdd43b164"},
+ {file = "typst-0.14.9-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:9ade0c99e853bf02bcc28f1bad8be3714de5ee5323e1a0f649abdb4e634240e9"},
+ {file = "typst-0.14.9-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1339a1d8beb05f098090a8a6c627e1aa7d4c7792b333e59694d30072f6f0ccdb"},
+ {file = "typst-0.14.9-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:792818bb147ea4a88b0555fd2066e1bb0b65353aecd01270a8acd3be018f3e39"},
+ {file = "typst-0.14.9-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4ecc5102125880baebbe454946a8492f6377680bf3d74f60650fae093958e3b"},
+ {file = "typst-0.14.9-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7be2e851ee3ff3561c79d244a2809089fb7cfc678912555ecabb031d5de74da9"},
+ {file = "typst-0.14.9-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be26d1f195b75d4c57e7411646124f8257d136417cc5e9c393dcd8ea3c3a11e8"},
+ {file = "typst-0.14.9-cp38-abi3-pyemscripten_2026_0_wasm32.whl", hash = "sha256:2f7008b41217194a41b9eec611cef358cc7c8d90193817b182f9abed62526984"},
+ {file = "typst-0.14.9-cp38-abi3-win_amd64.whl", hash = "sha256:6ebe6b433f71e9ec03c444b26f123363ab63ebab9a289fc4f26d6962b18fad4e"},
+ {file = "typst-0.14.9.tar.gz", hash = "sha256:53a7ee0825c127c100023685eeb8fb911226d7ad9ae861a370c11bb0f5c160cc"},
]
[[package]]
name = "urllib3"
-version = "2.3.0"
+version = "2.7.0"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
-python-versions = ">=3.9"
+python-versions = ">=3.10"
+groups = ["main", "docs", "testing", "types"]
files = [
- {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"},
- {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"},
+ {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"},
+ {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"},
]
[package.extras]
-brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
+brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""]
h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
-zstd = ["zstandard (>=0.18.0)"]
+zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""]
[[package]]
name = "venusian"
@@ -2189,6 +3330,7 @@ version = "3.1.1"
description = "A library for deferring decorator actions"
optional = false
python-versions = ">=3.7"
+groups = ["main"]
files = [
{file = "venusian-3.1.1-py3-none-any.whl", hash = "sha256:0845808a985976acbceaa1fbb871c7fac4fb28ae75453232970e9c2c2866dbf4"},
{file = "venusian-3.1.1.tar.gz", hash = "sha256:534fb3b355669283eb3954581931e5d1d071fce61d029d58f3219a5e3a6f0c41"},
@@ -2204,6 +3346,7 @@ version = "3.0.2"
description = "Waitress WSGI server"
optional = false
python-versions = ">=3.9.0"
+groups = ["main", "testing"]
files = [
{file = "waitress-3.0.2-py3-none-any.whl", hash = "sha256:c56d67fd6e87c2ee598b76abdd4e96cfad1f24cacdea5078d382b1f9d7b5ed2e"},
{file = "waitress-3.0.2.tar.gz", hash = "sha256:682aaaf2af0c44ada4abfb70ded36393f0e307f4ab9456a215ce0020baefc31f"},
@@ -2219,6 +3362,7 @@ version = "1.8.9"
description = "WSGI request and response object"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+groups = ["main", "testing"]
files = [
{file = "WebOb-1.8.9-py2.py3-none-any.whl", hash = "sha256:45e34c58ed0c7e2ecd238ffd34432487ff13d9ad459ddfd77895e67abba7c1f9"},
{file = "webob-1.8.9.tar.gz", hash = "sha256:ad6078e2edb6766d1334ec3dee072ac6a7f95b1e32ce10def8ff7f0f02d56589"},
@@ -2233,13 +3377,14 @@ testing = ["coverage", "pytest (>=3.1.0)", "pytest-cov", "pytest-xdist"]
[[package]]
name = "webtest"
-version = "3.0.3"
+version = "3.0.7"
description = "Helper to test WSGI applications"
optional = false
python-versions = ">=3.9"
+groups = ["testing"]
files = [
- {file = "WebTest-3.0.3-py3-none-any.whl", hash = "sha256:25a2f3b1ad273655ed22fdb4f9eff62d02f22d9d5ffeb0b58627759b1f62edff"},
- {file = "webtest-3.0.3.tar.gz", hash = "sha256:b635f6fe6584bc9737496b687155e9373f3d01bcb1b46169007da0f7ba6238f9"},
+ {file = "webtest-3.0.7-py3-none-any.whl", hash = "sha256:2f51a0844f3a8beaef89bc23d225fe05ad816f7e429ffcc655a13013a799ac6c"},
+ {file = "webtest-3.0.7.tar.gz", hash = "sha256:7aeab50f970d46c068e7a36dd162cb242591edf72a1d04efd21374772b931741"},
]
[package.dependencies]
@@ -2253,13 +3398,14 @@ tests = ["PasteDeploy", "WSGIProxy2", "coverage", "pyquery", "pytest", "pytest-c
[[package]]
name = "zope-deprecation"
-version = "5.1"
+version = "6.0"
description = "Zope Deprecation Infrastructure"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
- {file = "zope.deprecation-5.1-py3-none-any.whl", hash = "sha256:60f957b964d8f947a4a592c647d51ce0f4f844d1f041657956ddde0d9fa9a76a"},
- {file = "zope_deprecation-5.1.tar.gz", hash = "sha256:46bed4611fb53edc731aadeb64b28308bcb848f4cc150c60c948d078f7108721"},
+ {file = "zope_deprecation-6.0-py3-none-any.whl", hash = "sha256:ff72d51c88b516b9ddf2cfb826381cc49f99a6a89b7d35c97faca7bee3b46da6"},
+ {file = "zope_deprecation-6.0.tar.gz", hash = "sha256:18727ebda8e63a6d4bd28a290e8b46852e9f14473debb5cc40a0a2dccfadf15f"},
]
[package.dependencies]
@@ -2267,57 +3413,53 @@ setuptools = "*"
[package.extras]
docs = ["Sphinx"]
-test = ["zope.testrunner"]
+test = ["zope.testrunner (>=6.4)"]
[[package]]
name = "zope-interface"
-version = "7.2"
+version = "8.4"
description = "Interfaces for Python"
optional = false
-python-versions = ">=3.8"
-files = [
- {file = "zope.interface-7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce290e62229964715f1011c3dbeab7a4a1e4971fd6f31324c4519464473ef9f2"},
- {file = "zope.interface-7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05b910a5afe03256b58ab2ba6288960a2892dfeef01336dc4be6f1b9ed02ab0a"},
- {file = "zope.interface-7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:550f1c6588ecc368c9ce13c44a49b8d6b6f3ca7588873c679bd8fd88a1b557b6"},
- {file = "zope.interface-7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ef9e2f865721553c6f22a9ff97da0f0216c074bd02b25cf0d3af60ea4d6931d"},
- {file = "zope.interface-7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27f926f0dcb058211a3bb3e0e501c69759613b17a553788b2caeb991bed3b61d"},
- {file = "zope.interface-7.2-cp310-cp310-win_amd64.whl", hash = "sha256:144964649eba4c5e4410bb0ee290d338e78f179cdbfd15813de1a664e7649b3b"},
- {file = "zope.interface-7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1909f52a00c8c3dcab6c4fad5d13de2285a4b3c7be063b239b8dc15ddfb73bd2"},
- {file = "zope.interface-7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:80ecf2451596f19fd607bb09953f426588fc1e79e93f5968ecf3367550396b22"},
- {file = "zope.interface-7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:033b3923b63474800b04cba480b70f6e6243a62208071fc148354f3f89cc01b7"},
- {file = "zope.interface-7.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a102424e28c6b47c67923a1f337ede4a4c2bba3965b01cf707978a801fc7442c"},
- {file = "zope.interface-7.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25e6a61dcb184453bb00eafa733169ab6d903e46f5c2ace4ad275386f9ab327a"},
- {file = "zope.interface-7.2-cp311-cp311-win_amd64.whl", hash = "sha256:3f6771d1647b1fc543d37640b45c06b34832a943c80d1db214a37c31161a93f1"},
- {file = "zope.interface-7.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:086ee2f51eaef1e4a52bd7d3111a0404081dadae87f84c0ad4ce2649d4f708b7"},
- {file = "zope.interface-7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21328fcc9d5b80768bf051faa35ab98fb979080c18e6f84ab3f27ce703bce465"},
- {file = "zope.interface-7.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6dd02ec01f4468da0f234da9d9c8545c5412fef80bc590cc51d8dd084138a89"},
- {file = "zope.interface-7.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e7da17f53e25d1a3bde5da4601e026adc9e8071f9f6f936d0fe3fe84ace6d54"},
- {file = "zope.interface-7.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cab15ff4832580aa440dc9790b8a6128abd0b88b7ee4dd56abacbc52f212209d"},
- {file = "zope.interface-7.2-cp312-cp312-win_amd64.whl", hash = "sha256:29caad142a2355ce7cfea48725aa8bcf0067e2b5cc63fcf5cd9f97ad12d6afb5"},
- {file = "zope.interface-7.2-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:3e0350b51e88658d5ad126c6a57502b19d5f559f6cb0a628e3dc90442b53dd98"},
- {file = "zope.interface-7.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15398c000c094b8855d7d74f4fdc9e73aa02d4d0d5c775acdef98cdb1119768d"},
- {file = "zope.interface-7.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:802176a9f99bd8cc276dcd3b8512808716492f6f557c11196d42e26c01a69a4c"},
- {file = "zope.interface-7.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb23f58a446a7f09db85eda09521a498e109f137b85fb278edb2e34841055398"},
- {file = "zope.interface-7.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a71a5b541078d0ebe373a81a3b7e71432c61d12e660f1d67896ca62d9628045b"},
- {file = "zope.interface-7.2-cp313-cp313-win_amd64.whl", hash = "sha256:4893395d5dd2ba655c38ceb13014fd65667740f09fa5bb01caa1e6284e48c0cd"},
- {file = "zope.interface-7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d3a8ffec2a50d8ec470143ea3d15c0c52d73df882eef92de7537e8ce13475e8a"},
- {file = "zope.interface-7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:31d06db13a30303c08d61d5fb32154be51dfcbdb8438d2374ae27b4e069aac40"},
- {file = "zope.interface-7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e204937f67b28d2dca73ca936d3039a144a081fc47a07598d44854ea2a106239"},
- {file = "zope.interface-7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:224b7b0314f919e751f2bca17d15aad00ddbb1eadf1cb0190fa8175edb7ede62"},
- {file = "zope.interface-7.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baf95683cde5bc7d0e12d8e7588a3eb754d7c4fa714548adcd96bdf90169f021"},
- {file = "zope.interface-7.2-cp38-cp38-win_amd64.whl", hash = "sha256:7dc5016e0133c1a1ec212fc87a4f7e7e562054549a99c73c8896fa3a9e80cbc7"},
- {file = "zope.interface-7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bd449c306ba006c65799ea7912adbbfed071089461a19091a228998b82b1fdb"},
- {file = "zope.interface-7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a19a6cc9c6ce4b1e7e3d319a473cf0ee989cbbe2b39201d7c19e214d2dfb80c7"},
- {file = "zope.interface-7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72cd1790b48c16db85d51fbbd12d20949d7339ad84fd971427cf00d990c1f137"},
- {file = "zope.interface-7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52e446f9955195440e787596dccd1411f543743c359eeb26e9b2c02b077b0519"},
- {file = "zope.interface-7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ad9913fd858274db8dd867012ebe544ef18d218f6f7d1e3c3e6d98000f14b75"},
- {file = "zope.interface-7.2-cp39-cp39-win_amd64.whl", hash = "sha256:1090c60116b3da3bfdd0c03406e2f14a1ff53e5771aebe33fec1edc0a350175d"},
- {file = "zope.interface-7.2.tar.gz", hash = "sha256:8b49f1a3d1ee4cdaf5b32d2e738362c7f5e40ac8b46dd7d1a65e82a4872728fe"},
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "zope_interface-8.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:415de524326ddd61a78f0816f65942fa1aa35dced19e72579ad30dd106ce523e"},
+ {file = "zope_interface-8.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fa0a26d5767087170b3da9ff503221d535ea266bf61b522d0afa2590fd05db0a"},
+ {file = "zope_interface-8.4-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:8544081e32b515bbaf1c6339eef06b23ed470cf4876ff2f575803f82a744cc43"},
+ {file = "zope_interface-8.4-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:892b4b5350e58d6914858f58eb85d39fe9b992640ac6ece695f46c978046554d"},
+ {file = "zope_interface-8.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d683267a6243526869cb69677dcfc663416d5f87904c1576ddec6e420687d51"},
+ {file = "zope_interface-8.4-cp310-cp310-win_amd64.whl", hash = "sha256:f00fd65343d2a241a2b17688a12f5e815aa704ed64f9ca375de5f9e0ae9c9bda"},
+ {file = "zope_interface-8.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8b733af6e89a2b0b8edf5ff7a37988fe4e1788806e84e72127b88c47858f0da6"},
+ {file = "zope_interface-8.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:265bad2df2ec070f23ff863249a89b408b11908fd4207662781fd18e3c6fc912"},
+ {file = "zope_interface-8.4-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:e195e76767847afb5379ffd67690c17d3c6efdab58dc0e477cf81ac94d5a5a15"},
+ {file = "zope_interface-8.4-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ec1a56b6cf9a757cbbce9da38284a01473b92b96c1517eabd99150f51f1bb69"},
+ {file = "zope_interface-8.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:04c2c9b58e9c177628715d85e94834efa807c1f9f0a2f57ae0f7b553e8266ac4"},
+ {file = "zope_interface-8.4-cp311-cp311-win_amd64.whl", hash = "sha256:376d0ef005a131b349e2088e302aa094fa23c826d2ec8a7db4b00fb33c71e0d9"},
+ {file = "zope_interface-8.4-cp311-cp311-win_arm64.whl", hash = "sha256:caffd033b27e311b45e15f01923cc9e73c6bfd8e843b4532e29b59ee432bf893"},
+ {file = "zope_interface-8.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:84064876ed96ddd0744e3ad5d37134c758d77885e54113567792671405a02bac"},
+ {file = "zope_interface-8.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:81ed23698bfb588c48b1756129814b890febac971ff6c8a414f82601773145bb"},
+ {file = "zope_interface-8.4-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:e0b9d7e958657fad414f8272afcdf0b8a873fbbb2bb6a6287232d2f11a232bf8"},
+ {file = "zope_interface-8.4-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eef0a49e041f4dc4d2a6ab894b4fd0c5354e0e8037e731fb953531e59b0d3d33"},
+ {file = "zope_interface-8.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b302f955c36e924e1f4fe70dd9105ff06235857861c6ae72c3b10b016aeee99"},
+ {file = "zope_interface-8.4-cp312-cp312-win_amd64.whl", hash = "sha256:4ae6a1e111642dbf724f635424dcaf5a5c8abbde49eac3f452f5323ffaa10232"},
+ {file = "zope_interface-8.4-cp312-cp312-win_arm64.whl", hash = "sha256:2e9e4aa33b76877af903d5532545e64d24ade0f6f80d9d1a31e6efcea76a60bc"},
+ {file = "zope_interface-8.4-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:cd55965d715413038774aead54851bc3dbdd74a69f3ce30252182a94407b9905"},
+ {file = "zope_interface-8.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0d88c1f106a4f06e074a3ada2d20f4a602e3f2871c4f55726ed5d91e94ec19b1"},
+ {file = "zope_interface-8.4-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:36c575356732d59ffd3279ad67e302a6fe517e67db5b061b36b377ee0fa016c4"},
+ {file = "zope_interface-8.4-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:29f09ec8bda65f7b30294328070070a2590b90f252f834ee0817cdb0e2c35f6a"},
+ {file = "zope_interface-8.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2bc388cebcb753d21eaf2a0481fd6f0ce6840a47300a40dcec0b56bac27d0f97"},
+ {file = "zope_interface-8.4-cp313-cp313-win_amd64.whl", hash = "sha256:3e5866917ccb57d929e515a1136d729bd3fa4f367965fb16e38a4bc72cb05521"},
+ {file = "zope_interface-8.4-cp313-cp313-win_arm64.whl", hash = "sha256:f1f854bef8bc137519e4413bcc1322d55faad28b20b3ca39f7bec49d2f1b26df"},
+ {file = "zope_interface-8.4-cp314-cp314-macosx_10_9_x86_64.whl", hash = "sha256:7cbb887fdbfaacb4c362dbb487033551646e28013ad5ffe72e96eb260003a1a1"},
+ {file = "zope_interface-8.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a5638c6be715116d3453e6d099c299c6844d54810de7445ce116424e905ede06"},
+ {file = "zope_interface-8.4-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:b8147b40bfcd53803870a9519e0879ff066aeecc2fcff8295663c1b17fc38dc2"},
+ {file = "zope_interface-8.4-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:049ba3c7b38cc400ae08e011617635706e0f442e1d075db1b015246fcbf6091e"},
+ {file = "zope_interface-8.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9c4ac009c2c8e43283842f80387c4d4b41bcbc293391c3b9ab71532ae1ccc301"},
+ {file = "zope_interface-8.4-cp314-cp314-win_amd64.whl", hash = "sha256:4713bf651ec36e7eea49d2ace4f0e89bec2b33a339674874b1121f2537edc62a"},
+ {file = "zope_interface-8.4-cp314-cp314-win_arm64.whl", hash = "sha256:d934497c4b72d5f528d2b5ebe9b8b5a7004b5877948ebd4ea00c2432fb27178f"},
+ {file = "zope_interface-8.4.tar.gz", hash = "sha256:9dbee7925a23aa6349738892c911019d4095a96cff487b743482073ecbc174a8"},
]
-[package.dependencies]
-setuptools = "*"
-
[package.extras]
docs = ["Sphinx", "furo", "repoze.sphinx.autointerface"]
test = ["coverage[toml]", "zope.event", "zope.testing"]
@@ -2325,19 +3467,19 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"]
[[package]]
name = "zope-sqlalchemy"
-version = "3.1"
+version = "4.1"
description = "Minimal Zope/SQLAlchemy transaction integration"
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.10"
+groups = ["main"]
files = [
- {file = "zope.sqlalchemy-3.1-py3-none-any.whl", hash = "sha256:fdc7d65d8da335a34b90fb993e8217ef12808bad3025d2e3a6720db4138e4985"},
- {file = "zope.sqlalchemy-3.1.tar.gz", hash = "sha256:d9c2c3be695c213c5e22b7f7c6a4a214fa8eb5940b033465ba1c10a9d8b346db"},
+ {file = "zope_sqlalchemy-4.1-py3-none-any.whl", hash = "sha256:24c18895af4b962c0b214d84fbdf548df4ac504c9eb437b30d9bff25af0ae072"},
+ {file = "zope_sqlalchemy-4.1.tar.gz", hash = "sha256:1912525fa0b2b6ff0c3cab46aab59fed45302d4de4e4b03e0d11386e770f4531"},
]
[package.dependencies]
packaging = "*"
-setuptools = "*"
-SQLAlchemy = ">=1.1,<1.4.0 || >1.4.0,<1.4.1 || >1.4.1,<1.4.2 || >1.4.2,<1.4.3 || >1.4.3,<1.4.4 || >1.4.4,<1.4.5 || >1.4.5,<1.4.6 || >1.4.6"
+SQLAlchemy = ">=1.2,<1.4.0 || >1.4.0,<1.4.1 || >1.4.1,<1.4.2 || >1.4.2,<1.4.3 || >1.4.3,<1.4.4 || >1.4.4,<1.4.5 || >1.4.5,<1.4.6 || >1.4.6,<2.0.32 || >2.0.32,<2.0.33 || >2.0.33,<2.0.34 || >2.0.34,<2.0.35 || >2.0.35"
transaction = ">=1.6.0"
"zope.interface" = ">=3.6.0"
@@ -2345,6 +3487,6 @@ transaction = ">=1.6.0"
test = ["zope.testing"]
[metadata]
-lock-version = "2.0"
-python-versions = "^3.10"
-content-hash = "5862b6f9b19b0b451a9c0a01e37ed6a68bd241d43f16a7d2e40d3d25104880f5"
+lock-version = "2.1"
+python-versions = ">=3.11, <4"
+content-hash = "b4d90215cb83febb48f381f7eb9d8d2aa4fe159a34bfb93b96fbef54ea943881"
diff --git a/pylint.tests.toml b/pylint.tests.toml
index 7a68437..bfa2d9f 100644
--- a/pylint.tests.toml
+++ b/pylint.tests.toml
@@ -76,10 +76,6 @@ py-version = "3.10"
# Discover python modules and packages in the file system subtree.
# recursive =
-# When enabled, pylint would attempt to guess common misconfiguration and emit
-# user-friendly hints instead of false-positive error messages.
-suggestion-mode = true
-
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
# unsafe-load-any-extension =
diff --git a/pylint.toml b/pylint.toml
index d758ecf..27afd42 100644
--- a/pylint.toml
+++ b/pylint.toml
@@ -17,7 +17,7 @@
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code. (This is an alternative name to extension-pkg-allow-list
# for backward compatibility.)
-extension-pkg-whitelist = ["pydantic", "nh3"]
+extension-pkg-whitelist = ["pydantic", "nh3", "hittekaart_py"]
# Return non-zero exit code if any of these messages/categories are detected,
# even if score is above --fail-under value. Syntax same as enable. Messages
@@ -76,10 +76,6 @@ py-version = "3.10"
# Discover python modules and packages in the file system subtree.
# recursive =
-# When enabled, pylint would attempt to guess common misconfiguration and emit
-# user-friendly hints instead of false-positive error messages.
-suggestion-mode = true
-
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
# unsafe-load-any-extension =
diff --git a/pyproject.toml b/pyproject.toml
index 3338057..d3d22a1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -2,15 +2,58 @@
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
-[tool.poetry]
+[project]
name = "fietsboek"
description = "GPX file sharing website"
-version = "0.10.0"
+version = "0.12.1"
license = "AGPL-3.0-or-later"
readme = "README.md"
authors = [
- "Daniel Schadt <fietsboek@kingdread.de>",
+ { name = "Daniel Schadt", email = "fietsboek@kingdread.de>" },
+]
+keywords = ["web", "gpx"]
+requires-python = ">=3.11, <4"
+dependencies = [
+ "pyramid @ git+https://github.com/Kingdread/pyramid.git@7f5a499ccc63a10302fcc8021dcfae90ee97866f",
+ "pyramid_jinja2 (>=2.10, <3.0)",
+ "pyramid_debugtoolbar (>=4.9, <5.0)",
+ "pyramid_retry (>=2.1, <3.0)",
+ "pyramid_tm (>=2.5, <3.0)",
+ "waitress (>=3, <4)",
+
+ "SQLAlchemy (>=2.0.15, <3.0.0)",
+ "alembic (>=1.8, <2.0)",
+ "transaction (>=5, <6)",
+ "zope.sqlalchemy (>=4.0, <5.0)",
+ "redis (>=7, <8)",
+
+ "Babel (>=2.11, <3.0)",
+ "cryptography (>=46, <47)",
+ "gpxpy (>=1.5, <2.0)",
+ "markdown (>=3.4, <4.0)",
+ "nh3 (>=0.3.0,<0.4.0)",
+ "Click (>=8.1, <9.0)",
+ "requests (>=2.28.1, <3.0.0)",
+
+ "pydantic (>=2, <3)",
+ "termcolor (>=3.1.0, <4.0.0)",
+ "filelock (>=3.8.2, <4.0.0)",
+ "brotli (>=1.0.9, <2.0.0)",
+ "click-option-group (>=0.5.5, <0.6.0)",
+ "fitparse (>=1.2.0, <2.0.0)",
+ "pillow (>=12.0.0, <13.0.0)",
+ "typst (>=0.14.1,<0.15.0)",
+ "matplotlib (>=3.10.7,<4.0.0)",
+
+ "hittekaart-py @ git+https://gitlab.com/dunj3/hittekaart@9a576a8d3a8cb1d143be0ade0310f71b490cacb9#subdirectory=hittekaart-py",
]
+
+[project.urls]
+documentation = "https://docs.fietsboek.org/"
+homepage = "https://fietsboek.org/"
+repository = "https://gitlab.com/dunj3/fietsboek"
+
+[tool.poetry]
classifiers = [
'Development Status :: 3 - Alpha',
'Framework :: Pyramid',
@@ -19,63 +62,28 @@ classifiers = [
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
'Topic :: Internet :: WWW/HTTP :: WSGI :: Application',
]
-documentation = "https://docs.fietsboek.org/"
-homepage = "https://fietsboek.org/"
-repository = "https://gitlab.com/dunj3/fietsboek"
-keywords = ["web", "gpx"]
-
-[tool.poetry.dependencies]
-python = "^3.10"
-
-pyramid = "^2"
-pyramid_jinja2 = "^2.10"
-pyramid_debugtoolbar = "^4.9"
-pyramid_retry = "^2.1"
-pyramid_tm = "^2.5"
-waitress = "^3"
-
-SQLAlchemy = { version = "^2.0.15", extras = ["mypy"] }
-alembic = "^1.8"
-transaction = "^5"
-"zope.sqlalchemy" = "^3.0"
-redis = "^5"
-
-Babel = "^2.11"
-cryptography = "^44"
-gpxpy = "^1.5"
-markdown = "^3.4"
-nh3 = "^0.2.9"
-Click = "^8.1"
-requests = "^2.28.1"
-
-pydantic = "^2"
-termcolor = "^2.1.1"
-filelock = "^3.8.2"
-brotli = "^1.0.9"
-click-option-group = "^0.5.5"
-fitparse = "^1.2.0"
[tool.poetry.group.docs]
optional = true
[tool.poetry.group.docs.dependencies]
-Sphinx = "^8.0"
+Sphinx = "^9.0"
sphinx-autodoc-typehints = "^3"
[tool.poetry.group.testing]
optional = true
[tool.poetry.group.testing.dependencies]
-pytest = "^8.0.0"
+pytest = "^9.0.0"
webtest = "^3.0.0"
-pytest-cov = "^6.0.0"
-pytest-playwright = "^0.6.0"
+pytest-cov = "^7.0.0"
+pytest-playwright = "^0.7.0"
[tool.poetry.group.linters]
optional = true
[tool.poetry.group.linters.dependencies]
-pylint = "^3"
+pylint = "^4"
black = "^25.0.0"
[tool.poetry.group.types]
@@ -88,12 +96,12 @@ types-requests = "^2.28.11.5"
types-babel = "^2.11.0.7"
types-redis = "^4.3.21.6"
-[tool.poetry.scripts]
+[project.scripts]
fietsctl = "fietsboek.scripts.fietsctl:cli"
fietscron = "fietsboek.scripts.fietscron:cli"
fietsupdate = "fietsboek.updater.cli:cli"
-[tool.poetry.plugins."paste.app_factory"]
+[project.entry-points."paste.app_factory"]
main = "fietsboek:main"
[tool.black]
diff --git a/release-checklist.md b/release-checklist.md
index 3b9632f..67e4ca4 100644
--- a/release-checklist.md
+++ b/release-checklist.md
@@ -7,7 +7,7 @@
- [ ] Commit those changes (`git add ... && git commit -m 'bump version to X.Y.Z'`)
- [ ] Make sure the directory is clean (no uncommited changes)
- [ ] Make sure the tests & lints pass
- - [ ] Make sure they also do so on **Python 3.10** (current minimum version)
+ - [ ] Make sure they also do so on **Python 3.11** (current minimum version)
- [ ] Create a new git tag: `git tag -a vX.Y.Z`
- [ ] Push the tag to the remote repositories
- [ ] Publish to PyPI: `poetry publish`
diff --git a/testing.ini b/testing.ini
index 82fddfd..ed53bdc 100644
--- a/testing.ini
+++ b/testing.ini
@@ -12,6 +12,8 @@ pyramid.debug_notfound = false
pyramid.debug_routematch = false
pyramid.default_locale_name = en
+# The sqlalchemy.url is overwritten by the test setup script for different
+# database engines. We leave sqlite here as default so a local tox run works fine.
sqlalchemy.url = sqlite:///%(here)s/testing.sqlite
# The pytest tests usually overwrite this with a temporary directory. Since
# this is cleaned on test teardown, we don't want to accidentally delete data
diff --git a/tests/assets/picture01.jpg b/tests/assets/picture01.jpg
new file mode 100644
index 0000000..cdd6d1b
--- /dev/null
+++ b/tests/assets/picture01.jpg
Binary files differ
diff --git a/tests/assets/picture02.jpg b/tests/assets/picture02.jpg
new file mode 100644
index 0000000..3e38495
--- /dev/null
+++ b/tests/assets/picture02.jpg
Binary files differ
diff --git a/tests/bootstrap/test_new_instance.py b/tests/bootstrap/test_new_instance.py
index dc3076e..05076f4 100644
--- a/tests/bootstrap/test_new_instance.py
+++ b/tests/bootstrap/test_new_instance.py
@@ -2,6 +2,7 @@
script, as described in the documentation.
"""
+import configparser
import contextlib
import logging
import os
@@ -10,6 +11,8 @@ import subprocess
import venv
from pathlib import Path
+import sqlalchemy
+
LOGGER = logging.getLogger(__name__)
REPO_BASE = Path(__file__).parent.parent.parent
@@ -51,31 +54,66 @@ def create_config(config_name: Path):
Path("data").mkdir()
+def cleanup_database(config_name: Path):
+ """Connects to the database and ensures everything is reset.
+
+ :param config_name: Path to the config file.
+ """
+ if not config_name.exists():
+ return
+
+ parser = configparser.ConfigParser()
+ parser["DEFAULT"]["here"] = str(config_name.parent)
+ parser.read(config_name)
+
+ db_url = parser["app:main"]["sqlalchemy.url"]
+ engine = sqlalchemy.create_engine(db_url)
+
+ match engine.name:
+ case "sqlite":
+ pass
+ case "postgresql":
+ with engine.connect() as connection:
+ connection.execute(sqlalchemy.text("DROP SCHEMA public CASCADE;"))
+ connection.execute(sqlalchemy.text("CREATE SCHEMA public;"))
+ connection.commit()
+
+
def test_setup_via_fietsupdate(tmpdir):
with chdir(tmpdir):
- # We create a new temporary virtual environment with a fresh install, just
- # to be sure there's as little interference as possible.
- LOGGER.info("Installing Fietsboek into clean env")
- binaries_path = install_fietsboek(tmpdir / "venv")
-
- LOGGER.info("Creating a test configuration")
- create_config(Path("testing.ini"))
-
- # Try to run the migrations
- subprocess.check_call(
- [binaries_path / "fietsupdate", "update", "-c", "testing.ini", "-f"]
- )
-
- # Also try to add an administrator
- subprocess.check_call([
- binaries_path / "fietsctl",
- "user",
- "add",
- "-c", "testing.ini",
- "--email", "foobar@example.com",
- "--name", "Foo Bar",
- "--password", "raboof",
- "--admin",
- ])
-
- assert True
+ try:
+ # We create a new temporary virtual environment with a fresh install, just
+ # to be sure there's as little interference as possible.
+ LOGGER.info("Installing Fietsboek into clean env")
+ binaries_path = install_fietsboek(tmpdir / "venv")
+
+ LOGGER.info("Installing additional SQL engines")
+ subprocess.check_call(
+ [binaries_path / "pip", "install", "psycopg2"]
+ )
+
+ LOGGER.info("Creating a test configuration")
+ create_config(Path("testing.ini"))
+
+ # Try to run the migrations
+ subprocess.check_call(
+ [binaries_path / "fietsupdate", "update", "-c", "testing.ini", "-f"]
+ )
+
+ # Also try to add an administrator
+ subprocess.check_call([
+ binaries_path / "fietsctl",
+ "user",
+ "add",
+ "-c", "testing.ini",
+ "--email", "foobar@example.com",
+ "--name", "Foo Bar",
+ "--password", "raboof",
+ "--admin",
+ ])
+
+ assert True
+ finally:
+ # Clean up the database. This is important with anything but SQLite, as
+ # the tables would otherwise persist and interfere with the other tests.
+ cleanup_database(Path("testing.ini"))
diff --git a/tests/cli/conftest.py b/tests/cli/conftest.py
new file mode 100644
index 0000000..97193c9
--- /dev/null
+++ b/tests/cli/conftest.py
@@ -0,0 +1,14 @@
+import pytest
+
+fietsctl_results = []
+
+
+@pytest.hookimpl(hookwrapper=True)
+def pytest_runtest_call(item):
+ fietsctl_results.clear()
+
+ yield
+
+ item.add_report_section(
+ "call", "fietsctl", "\n\n".join(result.report() for result in fietsctl_results)
+ )
diff --git a/tests/cli/test_fietsctl.py b/tests/cli/test_fietsctl.py
new file mode 100644
index 0000000..c033b23
--- /dev/null
+++ b/tests/cli/test_fietsctl.py
@@ -0,0 +1,241 @@
+import os
+import re
+import secrets
+import subprocess
+from pathlib import Path
+
+import pytest
+from sqlalchemy import select
+from sqlalchemy.orm import Session
+
+from fietsboek import models
+from .conftest import fietsctl_results # pylint: disable=relative-beyond-top-level
+from ..testutils import populate # pylint: disable=relative-beyond-top-level
+
+
+class CliChecker:
+ """Result of a CLI tool invocation, with useful methods for tests."""
+
+ def __init__(self, result):
+ self.result = result
+
+ @property
+ def args(self):
+ """Arguments to the CLI invocation."""
+ return self.result.args
+
+ @property
+ def returncode(self) -> int:
+ """Return code."""
+ return self.result.returncode
+
+ def successful(self) -> bool:
+ """Whether the invocation was successful (based on the return code)."""
+ return self.returncode == os.EX_OK
+
+ @property
+ def stdout(self) -> bytes:
+ """Standard output."""
+ return self.result.stdout or b""
+
+ def has_line(self, regex: str) -> bool:
+ """Checks whether the output has a line matching the given regex."""
+ pattern = re.compile(regex)
+ for line in self.stdout.split(b"\n"):
+ ascii_line = line.decode("ascii", "ignore")
+ if pattern.search(ascii_line):
+ return True
+ return False
+
+ def report(self):
+ """Formats the output for reporting in pytest."""
+ return f"cmd: {self.args} -> {self.returncode}\n" + self.stdout.decode("ascii", "ignore")
+
+
+@pytest.fixture
+def fietsctl(app, dbengine, ini_file, data_manager):
+ """A fietsctl invocation helper."""
+ # Ideally, we want to use the ini file. However, the ini doesn't have a data
+ # dir defined (as we use a temporary path for that), so we need to create a
+ # new one.
+ # Ideally², we'd create that ini file in the temporary folder, but that
+ # gives us the wrong sqlite database (as that uses %(here)). So we could
+ # either figure out the actual SQLite path and patch that as well, or "give
+ # up" and create the ini in the correct working dir.
+ # To do that, we add some random bytes to avoid collisions, and we remove
+ # the file at the end.
+ data_dir = data_manager.data_dir
+ entropy = secrets.token_hex(4)
+ config_path = Path(ini_file).with_name(f"testing.{entropy}.ini")
+ assert not config_path.exists(), "file collision - rerun tests!"
+ config = Path(ini_file).read_text(encoding="ascii")
+ config = config.replace("# %% fietsboek.data_dir %%", f"fietsboek.data_dir = {data_dir}")
+ config_path.write_text(config, encoding="ascii")
+
+ def run_inner(args):
+ cmd = ["fietsctl"] + args + ["-c", str(config_path)]
+ result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False)
+ checker = CliChecker(result)
+ fietsctl_results.append(checker)
+ return checker
+
+ yield run_inner
+
+ config_path.unlink()
+
+
+def test_user_add(fietsctl, dbsession):
+ res = fietsctl(
+ ["user", "add", "--email", "foo@bar.com", "--name", "Remy", "--password", "hadley"]
+ )
+ assert res.successful()
+
+ res = fietsctl([
+ "user", "add",
+ "--email", "bar@foo.com",
+ "--name", "James",
+ "--password", "wilson",
+ "--admin",
+ ])
+ assert res.successful()
+
+ user: models.User = (
+ dbsession.execute(select(models.User).filter_by(email="foo@bar.com")).scalar_one()
+ )
+
+ assert user.name == "Remy"
+ user.check_password("hadley")
+ assert user.session_secret
+ assert not user.is_admin
+ assert user.is_verified
+
+ user: models.User = (
+ dbsession.execute(select(models.User).filter_by(email="bar@foo.com")).scalar_one()
+ )
+
+ assert user.name == "James"
+ user.check_password("wilson")
+ assert user.session_secret
+ assert user.is_admin
+ assert user.is_verified
+
+
+def test_user_list(fietsctl, dbengine, data_manager):
+ populate(dbengine, data_manager)
+
+ res = fietsctl(["user", "list"])
+ assert res.successful()
+ assert res.has_line("av.*jon\\.snow@nw\\.org.*Jon")
+ assert res.has_line("v.*davos@seaworth\\.com.*Davos")
+
+
+def test_user_del(fietsctl, dbengine, data_manager):
+ populate(dbengine, data_manager)
+
+ res = fietsctl(["user", "del", "-f", "--email", "jon.snow@nw.org"])
+ assert res.successful()
+
+ with dbengine.connect() as conn:
+ qry = select(models.User).filter_by(email="davos@seaworth.com")
+ row = conn.execute(qry).scalar_one_or_none()
+ assert row
+ qry = select(models.User).filter_by(email="jon.snow@nw.org")
+ row = conn.execute(qry).scalar_one_or_none()
+ assert row is None
+
+ res = fietsctl(["user", "del", "-f", "--email", "davos@seaworth.com"])
+ assert res.successful()
+
+ with dbengine.connect() as conn:
+ qry = select(models.User).filter_by(email="davos@seaworth.com")
+ row = conn.execute(qry).scalar_one_or_none()
+ assert row is None
+
+
+def test_user_passwd(fietsctl, dbengine, data_manager):
+ ids = populate(dbengine, data_manager)
+
+ res = fietsctl(["user", "passwd", "--email", "jon.snow@nw.org", "--password", "Ghost"])
+ assert res.successful()
+
+ with Session(dbengine) as session:
+ user = session.get(models.User, ids.jon)
+ user.check_password("Ghost")
+ with pytest.raises(models.user.PasswordMismatch):
+ user.check_password("ygritte")
+
+
+def test_user_modify(fietsctl, dbengine, data_manager):
+ ids = populate(dbengine, data_manager)
+
+ res = fietsctl([
+ "user", "modify",
+ "--email", "davos@seaworth.com",
+ "--admin",
+ "--no-verified",
+ "--set-email", "ser.davos@seaworth.com",
+ ])
+ assert res.successful()
+
+ with Session(dbengine) as session:
+ user: models.User = session.get(models.User, ids.davos)
+ assert not user.is_verified
+ assert user.is_admin
+ assert user.email == "ser.davos@seaworth.com"
+
+
+def test_track_list(fietsctl, dbengine, data_manager):
+ populate(dbengine, data_manager)
+
+ res = fietsctl(["track", "list"])
+ assert res.successful()
+ assert res.has_line("Jon.*Trip around Winterfell")
+ assert res.has_line("Jon.*Road to Riverrun")
+
+
+def test_track_del(fietsctl, dbengine, data_manager):
+ ids = populate(dbengine, data_manager)
+
+ res = fietsctl(["track", "del", "-i", str(ids.winterfell)])
+ assert not res.successful()
+
+ res = fietsctl(["track", "del", "-i", str(ids.winterfell), "-f"])
+ assert res.successful()
+
+ with Session(dbengine) as session:
+ track = session.get(models.Track, ids.winterfell)
+ assert track is None
+
+ with pytest.raises(FileNotFoundError):
+ data_manager.open(ids.winterfell)
+
+
+def test_maintenance_mode_get(fietsctl, data_manager):
+ res = fietsctl(["maintenance-mode"])
+ assert res.successful()
+ assert res.has_line("Maintenance mode is disabled")
+
+ (data_manager.data_dir / "MAINTENANCE").write_bytes(b"Updating")
+ res = fietsctl(["maintenance-mode"])
+ assert res.successful()
+ assert res.has_line("enabled: Updating")
+
+
+def test_maintenance_mode_set(fietsctl, data_manager):
+ assert data_manager.maintenance_mode() is None
+
+ res = fietsctl(["maintenance-mode", "Discombobulating"])
+ assert res.successful()
+ assert data_manager.maintenance_mode() == "Discombobulating"
+
+ res = fietsctl(["maintenance-mode", "Frobnicating"])
+ assert res.successful()
+ assert data_manager.maintenance_mode() == "Frobnicating"
+
+
+def test_maintenance_mode_disable(fietsctl, data_manager):
+ (data_manager.data_dir / "MAINTENANCE").write_bytes(b"Updating")
+
+ res = fietsctl(["maintenance-mode", "--disable"])
+ assert res.successful()
+ assert data_manager.maintenance_mode() is None
diff --git a/tests/conftest.py b/tests/conftest.py
index cd74b0b..c31554d 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -52,6 +52,7 @@ def dbengine(app_settings, ini_file):
yield engine
+ engine.dispose()
Base.metadata.drop_all(bind=engine)
alembic.command.stamp(alembic_cfg, None, purge=True)
@@ -59,29 +60,40 @@ def dbengine(app_settings, ini_file):
def data_manager(app_settings):
return DataManager(Path(app_settings["fietsboek.data_dir"]))
+
+def clean_directory_content(path: Path):
+ if path.is_dir():
+ shutil.rmtree(path)
+ path.mkdir()
+ elif path.is_file():
+ path.unlink()
+
+
@pytest.fixture(autouse=True)
def _cleanup_data(app_settings):
yield
engine = models.get_engine(app_settings)
- db_meta = inspect(engine)
+ # Load all table names beforehand, as has_table() would cause lock conflicts
+ tables = inspect(engine).get_table_names()
with engine.begin() as connection:
for table in reversed(Base.metadata.sorted_tables):
# The unit tests don't always set up the tables, so be gentle when
# tearing them down
- if db_meta.has_table(table.name):
+ if table.name in tables:
connection.execute(table.delete())
# The unit tests also often don't have a data directory, so be gentle here as well
if "fietsboek.data_dir" in app_settings:
data_dir = Path(app_settings["fietsboek.data_dir"])
- if (data_dir / "tracks").is_dir():
- shutil.rmtree(data_dir / "tracks")
- if (data_dir / "users").is_dir():
- shutil.rmtree(data_dir / "users")
+ clean_directory_content(data_dir / "tracks")
+ clean_directory_content(data_dir / "users")
+ clean_directory_content(data_dir / "journeys")
+ clean_directory_content(data_dir / "MAINTENANCE")
@pytest.fixture(scope='module')
def app(app_settings, dbengine, tmp_path_factory):
app_settings["fietsboek.data_dir"] = str(tmp_path_factory.mktemp("data"))
logging.getLogger().setLevel(logging.DEBUG)
+ logging.getLogger("matplotlib").setLevel(logging.INFO)
return main({}, dbengine=dbengine, **app_settings)
@pytest.fixture
@@ -189,7 +201,7 @@ def logged_in(testapp, route_path, dbsession, tm):
tm.abort()
with tm:
- user = models.User(email='foo@barre.com', is_verified=True)
+ user = models.User(name="Feu Barre", email='foo@barre.com', is_verified=True)
user.set_password("foobar")
dbsession.add(user)
dbsession.flush()
diff --git a/tests/integration/test_browse.py b/tests/integration/test_browse.py
index 875821d..1b96e2e 100644
--- a/tests/integration/test_browse.py
+++ b/tests/integration/test_browse.py
@@ -3,8 +3,10 @@ import zipfile
from contextlib import contextmanager
from datetime import datetime
+from sqlalchemy import delete, inspect
+
from testutils import load_gpx_asset
-from fietsboek import models
+from fietsboek import convert, models
from fietsboek.models.track import Visibility
@@ -21,7 +23,10 @@ def added_tracks(tm, dbsession, owner, data_manager):
# objects to the database.
tm.abort()
+ path = convert.smart_convert(load_gpx_asset("Teasi_1.gpx.gz")).path()
+
tracks = []
+ track_ids = []
with tm:
track = models.Track(
owner=owner,
@@ -35,8 +40,10 @@ def added_tracks(tm, dbsession, owner, data_manager):
track.date = datetime(2022, 3, 14, 9, 26, 54)
dbsession.add(track)
dbsession.flush()
- data_manager.initialize(track.id).compress_gpx(load_gpx_asset("MyTourbook_1.gpx.gz"))
+ track.fast_set_path(path)
+ data_manager.initialize(track.id)
tracks.append(track)
+ track_ids.append(track.id)
track = models.Track(
owner=owner,
@@ -50,14 +57,18 @@ def added_tracks(tm, dbsession, owner, data_manager):
track.date = datetime(2022, 10, 29, 13, 37, 11)
dbsession.add(track)
dbsession.flush()
- data_manager.initialize(track.id).compress_gpx(load_gpx_asset("Teasi_1.gpx.gz"))
+ track.fast_set_path(path)
+ track.ensure_cache(path)
+ dbsession.add(track.cache)
+ data_manager.initialize(track.id)
tracks.append(track)
+ track_ids.append(track.id)
tm.begin()
tm.doom()
try:
- yield tracks
+ yield track_ids
finally:
tm.abort()
with tm:
@@ -67,26 +78,113 @@ def added_tracks(tm, dbsession, owner, data_manager):
tm.doom()
+@contextmanager
+def a_lot_of_tracks(tm, dbsession, owner, data_manager):
+ """Adds some tracks to the database session.
+
+ This function should be used as a context manager and it ensures that the
+ added tracks are deleted again after the test, to make a clean slate for
+ the next test.
+ """
+ # The normal transaction is "doomed", so we need to abort it, start a fresh
+ # one, and then explicitely commit it, otherwise we will not persist the
+ # objects to the database.
+ tm.abort()
+
+ gpx_data = load_gpx_asset("MyTourbook_1.gpx.gz")
+ skel = convert.smart_convert(gpx_data)
+ path = skel.path()
+
+ tracks = []
+ track_ids = []
+ with tm:
+ for index in range(50):
+ track = models.Track(
+ owner=owner,
+ title=f"Traxi {index}",
+ visibility=Visibility.PUBLIC,
+ description="One of many",
+ badges=[],
+ link_secret="foobar",
+ tagged_people=[],
+ )
+ track.date = datetime(2022 - index, 3, 14, 9, 26, 59)
+ dbsession.add(track)
+ dbsession.flush()
+ track.fast_set_path(path)
+ track.ensure_cache(path)
+ dbsession.add(track.cache)
+ tracks.append(track)
+ track_ids.append(track.id)
+ data_manager.initialize(track.id)
+
+ tm.begin()
+ tm.doom()
+
+ try:
+ yield track_ids
+ finally:
+ tm.abort()
+ table = inspect(models.track.TrackPoint).tables[0]
+ with tm:
+ for track_id in track_ids:
+ dbsession.execute(delete(table).where(table.c.track_id == track_id))
+ dbsession.execute(
+ delete(models.TrackCache).where(models.TrackCache.track_id == track_id)
+ )
+ dbsession.execute(delete(models.Track).where(models.Track.id == track_id))
+ tm.begin()
+ tm.doom()
+
+
def test_browse(testapp, dbsession, route_path, logged_in, tm, data_manager):
# pylint: disable=too-many-positional-arguments
# Ensure there are some tracks in the database
with added_tracks(tm, dbsession, logged_in, data_manager):
# Now go to the browse page
- browse = testapp.get(route_path('browse'))
+ browse = testapp.get(route_path("browse"))
assert "Foobar" in browse.text
assert "Barfoo" in browse.text
+def test_browse_paged(testapp, dbsession, route_path, logged_in, tm, data_manager):
+ # pylint: disable=too-many-positional-arguments
+ with a_lot_of_tracks(tm, dbsession, logged_in, data_manager):
+ page_1 = testapp.get(route_path("browse", _query=[("page", 1)]))
+ assert "Traxi 0" in page_1.text
+ assert "Traxi 10" in page_1.text
+ assert "Traxi 20" not in page_1.text
+ assert "Traxi 30" not in page_1.text
+ assert "Traxi 40" not in page_1.text
+
+ page_2 = testapp.get(route_path("browse", _query=[("page", 2)]))
+ assert "Traxi 0" not in page_2.text
+ assert "Traxi 10" not in page_2.text
+ assert "Traxi 20" in page_2.text
+ assert "Traxi 30" in page_2.text
+ assert "Traxi 40" not in page_2.text
+
+ page_3 = testapp.get(route_path("browse", _query=[("page", 3)]))
+ assert "Traxi 0" not in page_3.text
+ assert "Traxi 10" not in page_3.text
+ assert "Traxi 20" not in page_3.text
+ assert "Traxi 30" not in page_3.text
+ assert "Traxi 40" in page_3.text
+
+
def test_archive(testapp, dbsession, route_path, logged_in, tm, data_manager):
# pylint: disable=too-many-positional-arguments
- with added_tracks(tm, dbsession, logged_in, data_manager):
+ with added_tracks(tm, dbsession, logged_in, data_manager) as tracks:
archive = testapp.get(
- route_path('track-archive', _query=[("track_id[]", "1"), ("track_id[]", "2")])
+ route_path(
+ "track-archive",
+ _query=[("track_id[]", tracks[0]), ("track_id[]", tracks[1])],
+ )
)
result = io.BytesIO(archive.body)
- with zipfile.ZipFile(result, 'r') as zipped:
+ with zipfile.ZipFile(result, "r") as zipped:
assert len(zipped.namelist()) == 2
- assert "track_1.gpx" in zipped.namelist()
- assert "track_2.gpx" in zipped.namelist()
+ assert f"track_{tracks[0]}.gpx" in zipped.namelist()
+ assert f"track_{tracks[1]}.gpx" in zipped.namelist()
diff --git a/tests/integration/test_pdf.py b/tests/integration/test_pdf.py
new file mode 100644
index 0000000..29cda02
--- /dev/null
+++ b/tests/integration/test_pdf.py
@@ -0,0 +1,59 @@
+from contextlib import contextmanager
+from datetime import datetime
+
+from testutils import load_gpx_asset
+from fietsboek import convert, models
+from fietsboek.models.track import Visibility
+
+
+@contextmanager
+def a_track(tm, dbsession, owner, data_manager):
+ """Adds some tracks to the database session.
+
+ This function should be used as a context manager and it ensures that the
+ added tracks are deleted again after the test, to make a clean slate for
+ the next test.
+ """
+ # The normal transaction is "doomed", so we need to abort it, start a fresh
+ # one, and then explicitely commit it, otherwise we will not persist the
+ # objects to the database.
+ tm.abort()
+
+ with tm:
+ track = models.Track(
+ owner=owner,
+ title="Goober",
+ visibility=Visibility.PUBLIC,
+ description="A bar'd track",
+ badges=[],
+ link_secret="raboof",
+ tagged_people=[],
+ )
+ track.date = datetime(2027, 3, 14, 9, 26, 54)
+ track.set_path(convert.smart_convert(load_gpx_asset("MyTourbook_1.gpx.gz")).path())
+ dbsession.add(track)
+ dbsession.flush()
+ data_manager.initialize(track.id)
+ track_id = track.id
+
+ tm.begin()
+ tm.doom()
+
+ try:
+ yield track_id
+ finally:
+ tm.abort()
+ with tm:
+ dbsession.delete(track)
+ data_manager.purge(track_id)
+ tm.begin()
+ tm.doom()
+
+
+def test_pdf(testapp, dbsession, route_path, logged_in, tm, data_manager):
+ # pylint: disable=too-many-positional-arguments
+ # Ensure there are some tracks in the database
+ with a_track(tm, dbsession, logged_in, data_manager) as track_id:
+ pdf = testapp.get(route_path("track-pdf", track_id=track_id))
+
+ assert pdf
diff --git a/tests/playwright/conftest.py b/tests/playwright/conftest.py
index 4e7f5a4..435daa6 100644
--- a/tests/playwright/conftest.py
+++ b/tests/playwright/conftest.py
@@ -7,10 +7,12 @@ from wsgiref import simple_server
from pyramid.authentication import AuthTktCookieHelper
from pyramid.testing import DummyRequest
+import fietsboek.config
from testutils import load_gpx_asset
from fietsboek import models, util, actions
from fietsboek.models.track import Visibility, TrackType
from fietsboek.config import Config
+from fietsboek.views.tileproxy import TileRequester
import pytest
@@ -55,7 +57,11 @@ def dbaccess(app):
through and the running WSGI app cannot read them.
"""
session_factory = app.registry["dbsession_factory"]
- return session_factory()
+ factory = session_factory()
+
+ yield factory
+
+ factory.close()
class Helper:
@@ -104,7 +110,10 @@ class Helper:
)
def add_track(
- self, user: Optional[models.User] = None, track_name: str = "Teasi_1.gpx.gz"
+ self,
+ user: Optional[models.User] = None,
+ track_name: str = "Teasi_1.gpx.gz",
+ title: str = "Another awesome track",
) -> models.Track:
"""Add a track to the given user.
@@ -112,13 +121,16 @@ class Helper:
"""
if user is None:
user = self.john_doe()
+ config = fietsboek.config.parse(self.app_settings)
with self.dbaccess:
user = self.dbaccess.merge(user)
track = actions.add_track(
self.dbaccess,
self.data_manager,
+ TileRequester(None),
+ config.public_tile_layers()[0],
owner=user,
- title="Another awesome track",
+ title=title,
visibility=Visibility.PRIVATE,
description="Another description",
track_type=TrackType.ORGANIC,
diff --git a/tests/playwright/test_basic.py b/tests/playwright/test_basic.py
index 231962e..19ef8ff 100644
--- a/tests/playwright/test_basic.py
+++ b/tests/playwright/test_basic.py
@@ -4,7 +4,7 @@ import pytest
from playwright.sync_api import Page, expect
from sqlalchemy import select
-from testutils import load_gpx_asset
+from testutils import asset_path, load_gpx_asset
from fietsboek import models
from fietsboek.models.track import Visibility
@@ -60,6 +60,10 @@ def test_upload(page: Page, playwright_helper, tmp_path, dbaccess):
page.get_by_role("button", name="Add Tag").click()
page.get_by_label("Description").fill("Beschreibung der tollen Tour")
+ page.locator("#imageSelector").set_input_files(
+ [asset_path("picture01.jpg"), asset_path("picture02.jpg")],
+ )
+
page.locator(".btn", has_text="Upload").click()
# Once we have finished the upload, extract the ID of the track and check
@@ -72,6 +76,7 @@ def test_upload(page: Page, playwright_helper, tmp_path, dbaccess):
assert track.visibility == Visibility.PUBLIC
assert track.text_tags() == {"Tolle Tour"}
assert track.description == "Beschreibung der tollen Tour"
+ assert len(track.images) == 2
def test_edit(page: Page, playwright_helper, dbaccess):
@@ -101,6 +106,33 @@ def test_edit(page: Page, playwright_helper, dbaccess):
assert track.description == "Not so descriptive anymore"
+def test_edit_change_gpx(page: Page, playwright_helper, tmp_path, dbaccess):
+ playwright_helper.login()
+ track_id = playwright_helper.add_track().id
+
+ track = dbaccess.execute(select(models.Track).filter_by(id=track_id)).scalar_one()
+ old_cache = track.cache.length, track.cache.uphill, track.cache.downhill
+
+ page.goto(f"/track/{track_id}")
+ page.locator(".btn", has_text="Edit").click()
+
+ gpx_data = load_gpx_asset("Synthetic_BRouter_1.gpx.gz")
+ gpx_path = tmp_path / "NewGPX.gpx"
+ with open(gpx_path, "wb") as gpx_fobj:
+ gpx_fobj.write(gpx_data)
+
+ page.get_by_label("New file for this track").set_input_files(gpx_path)
+ page.locator(".btn", has_text="Save").click()
+
+ track = dbaccess.execute(select(models.Track).filter_by(id=track_id)).scalar_one()
+ new_cache = track.cache
+ dbaccess.refresh(new_cache)
+
+ assert old_cache[0] != new_cache.length
+ assert old_cache[1] != new_cache.uphill
+ assert old_cache[2] != new_cache.downhill
+
+
def test_browse(page: Page, playwright_helper, dbaccess):
playwright_helper.login()
track = playwright_helper.add_track()
diff --git a/tests/playwright/test_journeys.py b/tests/playwright/test_journeys.py
new file mode 100644
index 0000000..2bfd271
--- /dev/null
+++ b/tests/playwright/test_journeys.py
@@ -0,0 +1,170 @@
+from playwright.sync_api import Page, expect
+from sqlalchemy import select
+
+from fietsboek import models
+
+
+def add_journey(playwright_helper, dbaccess, title):
+ """Adds a journey for testing purposes. Returns the journey ID."""
+ t_1 = playwright_helper.add_track(None, "Teasi_1.gpx.gz", "trayectoria uno")
+ t_2 = playwright_helper.add_track(None, "MyTourbook_1.gpx.gz", "trayectoria dos")
+
+ with dbaccess:
+ journey = models.Journey(
+ owner=playwright_helper.john_doe(),
+ title=title,
+ description="You saw sirens?",
+ visibility=models.journey.Visibility.PUBLIC,
+ )
+
+ dbaccess.add(journey)
+ dbaccess.flush()
+
+ journey.set_track_ids([t_1.id, t_2.id])
+ dbaccess.commit()
+ dbaccess.refresh(journey, ["id"])
+ dbaccess.expunge(journey)
+
+ playwright_helper.data_manager.initialize_journey(journey.id)
+
+ return journey.id
+
+
+def test_journey_list(page: Page, playwright_helper, dbaccess):
+ playwright_helper.login()
+
+ add_journey(playwright_helper, dbaccess, title="Our Journey")
+
+ page.goto("/journey/")
+ expect(page.locator("h5", has_text="Our Journey")).to_be_visible()
+ expect(page.locator("li", has_text="trayectoria uno")).to_be_visible()
+ expect(page.locator("li", has_text="trayectoria dos")).to_be_visible()
+
+
+def test_journey_new(page: Page, playwright_helper, dbaccess):
+ playwright_helper.login()
+
+ playwright_helper.add_track(None, "Teasi_1.gpx.gz", "trayectoria uno")
+ playwright_helper.add_track(None, "MyTourbook_1.gpx.gz", "trayectoria dos")
+ playwright_helper.add_track(None, "MyTourbook_1.gpx.gz", "trayectoria tres")
+
+ page.goto("/journey/")
+ page.get_by_text("New journey").click()
+
+ page.get_by_label("Title").fill("My Odyssey")
+ page.get_by_label("Description").fill("I saw sirens!")
+
+ page.locator("#trackSearch").fill("uno")
+ page.locator("#trackSearchButton").click()
+ page.locator(".track-query-response button").click()
+
+ page.locator("#trackSearch").fill("dos")
+ page.locator("#trackSearchButton").click()
+ page.locator(".track-query-response button").click()
+
+ page.locator("#trackSearch").fill("tres")
+ page.locator("#trackSearchButton").click()
+ page.locator(".track-query-response button").click()
+ page.locator(".journey-track", has_text="tres").locator(".btn").click()
+
+ page.locator(".btn", has_text="Save").click()
+
+ expect(page.locator("h1", has_text="My Odyssey")).to_be_visible()
+
+ expect(page.locator("h5", has_text="trayectoria uno")).to_be_visible()
+ expect(page.locator("h5", has_text="trayectoria dos")).to_be_visible()
+
+ journey = dbaccess.execute(select(models.Journey).filter_by(title="My Odyssey")).scalar_one()
+
+ assert journey.title == "My Odyssey"
+ assert journey.description == "I saw sirens!"
+ assert len(journey.tracks) == 2
+ assert journey.tracks[0].title == "trayectoria uno"
+ assert journey.tracks[1].title == "trayectoria dos"
+
+
+def test_journey_new_empty_title(page: Page, playwright_helper):
+ playwright_helper.login()
+
+ playwright_helper.add_track(None, "Teasi_1.gpx.gz", "trayectoria uno")
+
+ page.goto("/journey/")
+ page.get_by_text("New journey").click()
+
+ page.locator("#trackSearch").fill("uno")
+ page.locator("#trackSearchButton").click()
+ page.locator(".track-query-response button").click()
+ page.locator(".btn", has_text="Save").click()
+
+ expect(page.locator(".invalid-feedback", has_text="A title is required")).to_be_visible()
+
+
+def test_journey_new_no_tracks(page: Page, playwright_helper):
+ playwright_helper.login()
+
+ page.goto("/journey/")
+ page.get_by_text("New journey").click()
+
+ page.get_by_label("Title").fill("A title is there!")
+
+ page.locator(".btn", has_text="Save").click()
+
+ expect(page.locator(".invalid-feedback", has_text="A journey must have at least one track"))\
+ .to_be_visible()
+
+
+def test_journey_edit(page: Page, playwright_helper, dbaccess):
+ playwright_helper.login()
+
+ journey_id = add_journey(playwright_helper, dbaccess, title="Your Odyssey")
+
+ page.goto(f"/journey/{journey_id}/")
+
+ expect(page.locator("h1", has_text="Your Odyssey")).to_be_visible()
+
+ page.locator("a", has_text="Edit").click()
+
+ page.get_by_label("Title").fill("Their Odyssey")
+ page.get_by_label("Description").fill("Where is Homer?")
+
+ expect(page.locator(".track-title", has_text="trayectoria uno")).to_be_visible()
+ page.locator(".journey-track", has_text="uno").locator(".btn").click()
+ expect(page.locator(".track-title", has_text="trayectoria uno")).not_to_be_visible()
+
+ page.locator(".btn", has_text="Save").click()
+
+ expect(page.locator("h1", has_text="Their Odyssey")).to_be_visible()
+ expect(page.locator("h5", has_text="trayectoria uno")).not_to_be_visible()
+ expect(page.locator("h5", has_text="trayectoria dos")).to_be_visible()
+
+ journey = dbaccess.execute(select(models.Journey).filter_by(title="Their Odyssey")).scalar_one()
+
+ assert journey.title == "Their Odyssey"
+ assert journey.description == "Where is Homer?"
+ assert len(journey.tracks) == 1
+ assert journey.tracks[0].title == "trayectoria dos"
+
+
+def test_journey_reorder(page: Page, playwright_helper, dbaccess):
+ playwright_helper.login()
+
+ journey_id = add_journey(playwright_helper, dbaccess, title="Her Journey")
+
+ page.goto(f"/journey/{journey_id}/edit")
+
+ expect(page.locator("h1", has_text="Her Journey")).to_be_visible()
+
+ page.locator(".track-title", has_text="trayectoria uno").drag_to(
+ page.locator(".track-title", has_text="trayectoria dos"),
+ target_position={"x": 10, "y": 20},
+ )
+
+ page.locator(".btn", has_text="Save").click()
+
+ expect(page.locator("h1", has_text="Her Journey")).to_be_visible()
+
+ journey = dbaccess.execute(select(models.Journey).filter_by(id=journey_id)).scalar_one()
+
+ assert len(journey.tracks) == 2
+ assert journey.tracks[0].title == "trayectoria dos"
+ assert journey.tracks[1].title == "trayectoria uno"
diff --git a/tests/playwright/test_profiles.py b/tests/playwright/test_profiles.py
index 7e5fb3c..ffbaab0 100644
--- a/tests/playwright/test_profiles.py
+++ b/tests/playwright/test_profiles.py
@@ -5,7 +5,7 @@ def test_forbidden(page: Page, playwright_helper):
john = playwright_helper.john_doe()
with page.expect_response(lambda resp: resp.status == 403):
- page.goto(f"/user/{john.id}")
+ page.goto(f"/user/{john.id}/")
def test_profile(page: Page, playwright_helper):
diff --git a/tests/playwright/test_share.py b/tests/playwright/test_share.py
index de288a0..dcba899 100644
--- a/tests/playwright/test_share.py
+++ b/tests/playwright/test_share.py
@@ -29,7 +29,8 @@ def test_view_wrong_link(page: Page, playwright_helper, dbaccess):
with page.expect_response(lambda resp: resp.status == 403):
page.goto(f"/track/{track.id}?secret=foobar")
- assert "Forbidden" in page.content()
+ assert "No entry" in page.content()
+ assert "not allowed to access" in page.content()
def test_change_link(page: Page, playwright_helper, dbaccess):
diff --git a/tests/playwright/test_tileproxy.py b/tests/playwright/test_tileproxy.py
index d4d3389..2a2bdc0 100644
--- a/tests/playwright/test_tileproxy.py
+++ b/tests/playwright/test_tileproxy.py
@@ -14,7 +14,7 @@ def test_tileproxy(page: Page, playwright_helper, caplog):
# If we're too fast, the log entry might not be there yet, wait 2 more
# seconds
- if "Skipping tile proxy request for testing URL" not in caplog.messages:
+ if "Skipping tile request for testing URL" not in caplog.messages:
time.sleep(2)
- assert "Skipping tile proxy request for testing URL" in caplog.messages
+ assert "Skipping tile request for testing URL" in caplog.messages
diff --git a/tests/playwright/test_transformers.py b/tests/playwright/test_transformers.py
index fc89afb..d4e3456 100644
--- a/tests/playwright/test_transformers.py
+++ b/tests/playwright/test_transformers.py
@@ -26,7 +26,7 @@ def test_transformer_zero_elevation_disabled(page: Page, playwright_helper, tmp_
# Expect early (here and in the other tests) to ensure that the backend has
# caught up with executing the transformer. Otherwise it might happen that
# we read the database while the request is not finished yet.
- expect(page.locator("#detailsUphill")).to_contain_text("167.7 m")
+ expect(page.locator("#detailsUphill")).to_contain_text("167.79 m")
new_track_id = int(page.url.rsplit("/", 1)[1])
track = dbaccess.execute(select(models.Track).filter_by(id=new_track_id)).scalar_one()
@@ -90,7 +90,7 @@ def test_transformer_steep_slope_disabled(page: Page, playwright_helper, tmp_pat
page.locator(".btn", has_text="Upload").click()
- expect(page.locator("#detailsUphill")).to_contain_text("61.54 m")
+ expect(page.locator("#detailsUphill")).to_contain_text("64.4 m")
new_track_id = int(page.url.rsplit("/", 1)[1])
track = dbaccess.execute(select(models.Track).filter_by(id=new_track_id)).scalar_one()
@@ -111,11 +111,11 @@ def test_transformer_steep_slope_enabled(page: Page, playwright_helper, tmp_path
page.locator(".btn", has_text="Upload").click()
- expect(page.locator("#detailsUphill")).to_contain_text("1.2 m")
+ expect(page.locator("#detailsUphill")).to_contain_text("2.4 m")
new_track_id = int(page.url.rsplit("/", 1)[1])
track = dbaccess.execute(select(models.Track).filter_by(id=new_track_id)).scalar_one()
- assert track.cache.uphill < 2
+ assert track.cache.uphill < 3
def test_transformer_steep_slope_edited(page: Page, playwright_helper, tmp_path, dbaccess):
@@ -137,14 +137,14 @@ def test_transformer_steep_slope_edited(page: Page, playwright_helper, tmp_path,
page.locator(".btn", has_text="Save").click()
- expect(page.locator("#detailsUphill")).to_contain_text("1.2 m")
+ expect(page.locator("#detailsUphill")).to_contain_text("2.4 m")
track_id = int(page.url.rsplit("/", 1)[1])
track = dbaccess.execute(select(models.Track).filter_by(id=track_id)).scalar_one()
- assert track.cache.uphill < 2
+ assert track.cache.uphill < 3
-def test_transformer_elevation_jump_enabled(page: Page, playwright_helper, tmp_path, data_manager):
+def test_transformer_elevation_jump_enabled(page: Page, playwright_helper, tmp_path, dbaccess):
playwright_helper.login()
page.goto("/")
@@ -161,9 +161,10 @@ def test_transformer_elevation_jump_enabled(page: Page, playwright_helper, tmp_p
page.locator(".alert", has_text="Upload successful").wait_for()
new_track_id = int(page.url.rsplit("/", 1)[1])
- data = data_manager.open(new_track_id)
- gpx = gpxpy.parse(data.decompress_gpx())
+ gpx = gpxpy.parse(
+ dbaccess.execute(select(models.Track).filter_by(id=new_track_id)).scalar_one().gpx_xml()
+ )
points = iter(gpx.walk(only_points=True))
next(points)
for prev_point, point in zip(gpx.walk(only_points=True), points):
diff --git a/tests/testutils.py b/tests/testutils.py
index 810bdf7..e49a0b5 100644
--- a/tests/testutils.py
+++ b/tests/testutils.py
@@ -1,8 +1,45 @@
"""Various utility functions for testing."""
+import datetime
import gzip
+import io
from pathlib import Path
+from typing import NamedTuple
from playwright.sync_api import Page
+from sqlalchemy import Engine
+from sqlalchemy.orm import Session
+
+from fietsboek import convert, models, util
+from fietsboek.data import DataManager
+
+
+class PopulationIds(NamedTuple):
+ """Collection of database IDs that :func:`populate` returns."""
+ jon: int
+ davos: int
+ winterfell: int
+ riverrun: int
+
+
+def asset_path(filename: str) -> Path:
+ """Returns the path to the test asset given by the filename.
+
+ :param filename: The filename of the asset.
+ :return: The complete path.
+ """
+ asset_dir = Path(__file__).parent / "assets"
+ return asset_dir / filename
+
+
+def load_test_asset(filename: str) -> bytes:
+ """Load a test asset.
+
+ Unlike :func:`load_gpx_asset`, this function does not do gzip decompression.
+
+ :param filkename: Name of the asset to load.
+ :return: The content of the file as bytes.
+ """
+ return asset_path(filename).read_bytes()
def load_gpx_asset(filename: str) -> bytes:
@@ -14,8 +51,7 @@ def load_gpx_asset(filename: str) -> bytes:
:param filename: Name of the asset to load.
:return: The content of the asset as bytes.
"""
- asset_dir = Path(__file__).parent / 'assets'
- test_file = asset_dir / filename
+ test_file = asset_path(filename)
with gzip.open(test_file, 'rb') as fobj:
return fobj.read()
@@ -36,3 +72,122 @@ def extract_and_upload(page: Page, filename: str, tmp_path: Path):
page.get_by_label("GPX file").set_input_files(gpx_path)
page.locator(".bi-upload").click()
+
+
+def populate(dbengine: Engine, data_manager: DataManager) -> PopulationIds:
+ """Populates the database and data directory with some test data.
+
+ This adds:
+
+ Jon (jon.snow@nw.org, admin, password: ygritte)
+ Trip around Winterfell (2 images)
+ Road to Riverrun
+
+ Davos (davos@seaworth.com, password: 123456)
+
+ :return: An object carrying the database IDs of the added objects.
+ """
+ user_ids = []
+ with Session(dbengine) as session:
+ user = models.User(name="Jon", email="jon.snow@nw.org", is_verified=True, is_admin=True)
+ user.set_password("ygritte")
+ user.roll_session_secret()
+ session.add(user)
+ session.flush()
+ user_ids.append(user.id)
+
+ user = models.User(name="Davos", email="davos@seaworth.com", is_verified=True)
+ user.set_password("123456")
+ user.roll_session_secret()
+ session.add(user)
+ session.flush()
+ user_ids.append(user.id)
+
+ session.commit()
+
+ for user_id in user_ids:
+ data_manager.initialize_user(user_id)
+
+ track_ids = []
+ # First track for Jon:
+
+ gpx_data = load_gpx_asset("Teasi_1.gpx.gz")
+ track = convert.smart_convert(gpx_data)
+ path = track.path()
+ track.points = []
+ track.owner_id = user_ids[0]
+ track.title = "Trip around Winterfell"
+ track.visibility = models.track.Visibility.PUBLIC
+ track.type = models.track.TrackType.ORGANIC
+ track.description = "I took my sister for a quick trip around Winterfell"
+ track.badges = []
+ track.link_secret = util.random_link_secret()
+ track.tagged_people = []
+ track.date = datetime.datetime(1984, 1, 2, 10, 11, tzinfo=datetime.UTC)
+ track.transformers = []
+ track.sync_tags({"westeros"})
+
+ with Session(dbengine) as session:
+ session.add(track)
+ session.flush()
+
+ assert track.id is not None
+ track_ids.append(track.id)
+ manager = data_manager.initialize(track.id)
+ manager.compress_backup(gpx_data)
+
+ track.fast_set_path(path)
+ track.ensure_cache(path)
+ session.add(track.cache)
+
+ image_name = manager.add_image(io.BytesIO(load_test_asset("picture01.jpg")), "PIC001.jpg")
+ image_meta = models.ImageMetadata(track=track, image_name=image_name)
+ image_meta.description = "Beautiful sight out of the Hunter's Gate"
+ session.add(image_meta)
+
+ image_name = manager.add_image(io.BytesIO(load_test_asset("picture02.jpg")), "PIC002.jpg")
+ image_meta = models.ImageMetadata(track=track, image_name=image_name)
+ image_meta.description = "Our steel wire horses ready to gallop"
+ session.add(image_meta)
+
+ session.commit()
+
+ # Second track for Jon:
+
+ gpx_data = load_gpx_asset("MyTourbook_1.gpx.gz")
+ track = convert.smart_convert(gpx_data)
+ path = track.path()
+ track.points = []
+ track.owner_id = user_ids[0]
+ track.title = "Road to Riverrun"
+ track.visibility = models.track.Visibility.PUBLIC
+ track.type = models.track.TrackType.ORGANIC
+ track.description = "Got a wedding to attend!"
+ track.badges = []
+ track.link_secret = util.random_link_secret()
+ track.tagged_people = []
+ track.date = datetime.datetime(1985, 8, 5, 16, 41, tzinfo=datetime.UTC)
+ track.transformers = []
+ track.sync_tags({"westeros"})
+
+ with Session(dbengine) as session:
+ session.add(track)
+ session.flush()
+
+ assert track.id is not None
+ track_ids.append(track.id)
+ manager = data_manager.initialize(track.id)
+ manager.compress_backup(gpx_data)
+
+ track.fast_set_path(path)
+ track.ensure_cache(path)
+ session.add(track.cache)
+
+ session.commit()
+
+ return PopulationIds(
+ jon=user_ids[0],
+ davos=user_ids[1],
+ winterfell=track_ids[0],
+ riverrun=track_ids[1],
+ )
diff --git a/tests/unit/test_pdf.py b/tests/unit/test_pdf.py
new file mode 100644
index 0000000..aafa717
--- /dev/null
+++ b/tests/unit/test_pdf.py
@@ -0,0 +1,58 @@
+import pytest
+
+from fietsboek import pdf
+
+
+@pytest.mark.parametrize("value, expected", [
+ ('', '""'),
+ ('a', '"\\u{61}"'),
+ ('FOO', '"\\u{46}\\u{4f}\\u{4f}"'),
+ ('äß', '"\\u{e4}\\u{df}"'),
+ ('"', '"\\u{22}"'),
+ ("'", '"\\u{27}"'),
+])
+def test_typst_string(value, expected):
+ assert pdf.typst_string(value) == expected
+
+
+@pytest.mark.parametrize("value, expected", [
+ ("foo", "foo"),
+ ("*foo*", "\\*foo\\*"),
+ ("#strong[foo]", "\\#strong\\[foo\\]"),
+ ("= foo", "\\= foo"),
+ ("par 1\n\npar 2", "par 1\n\npar 2"),
+])
+def test_typst_escape(value, expected):
+ assert pdf.typst_escape(value) == expected
+
+
+@pytest.mark.parametrize("md_source, typst_source", [
+ ("*foo*", "#emph[foo]\n\n"),
+ ("**foo**", "#strong[foo]\n\n"),
+ ("***foo***", "#strong[#emph[foo]]\n\n"),
+ ("[Teksd](https://link)",
+ '#link("\\u{68}\\u{74}\\u{74}\\u{70}\\u{73}\\u{3a}'
+ '\\u{2f}\\u{2f}\\u{6c}\\u{69}\\u{6e}\\u{6b}")[Teksd]\n\n'),
+ ("""\
+# Uperschrift
+
+Teksd""", """\
+#heading(level: 1)[Uperschrift]
+Teksd\n\n"""),
+ ("""\
+* Eitem 1
+* Eitem 2""", """\
+#list(
+[Eitem 1],
+[Eitem 2],
+)"""),
+ ("""\
+1. Eitem 1
+1. Eitem 2""", """\
+#enum(
+[Eitem 1],
+[Eitem 2],
+)"""),
+])
+def test_md_to_typst(md_source, typst_source):
+ assert pdf.md_to_typst(md_source) == typst_source
diff --git a/tests/unit/test_util.py b/tests/unit/test_util.py
index 6dc8e7d..cc92058 100644
--- a/tests/unit/test_util.py
+++ b/tests/unit/test_util.py
@@ -71,19 +71,6 @@ def test_guess_gpx_timezone(gpx_file, offset):
assert timezone.utcoffset(None) == offset
-@pytest.mark.parametrize('gpx_file', [
- 'Teasi_1.gpx.gz',
- 'MyTourbook_1.gpx.gz',
- 'Synthetic_WT2.gpx.gz',
- 'Synthetic_BRouter_1.gpx.gz',
-])
-def test_tour_metadata(gpx_file):
- # Here we simply make sure that we do not crash the metadata extraction
- # function.
- gpx_data = load_gpx_asset(gpx_file)
- assert util.tour_metadata(gpx_data) is not None
-
-
@pytest.mark.parametrize('mps, kph', [(1, 3.6), (10, 36)])
def test_mps_to_kph(mps, kph):
assert util.mps_to_kph(mps) == pytest.approx(kph, 0.1)
@@ -112,3 +99,15 @@ def test_tile_url(app_request):
assert "{y}" in route_url
assert "{z}" in route_url
assert "bobby" in route_url
+
+
+@pytest.mark.parametrize("value, expected", [
+ ("", b""),
+ ("foo", b"foo"),
+ ("<foo>", b"&#x3c;foo&#x3e;"),
+ ("foo bar", b"foo bar"),
+ ("</gpx>", b"&#x3c;&#x2f;gpx&#x3e;"),
+ ("äÖß", b"&#xe4;&#xd6;&#xdf;"),
+])
+def test_xml_escape(value, expected):
+ assert util.xml_escape(value) == expected
diff --git a/tox.ini b/tox.ini
index 17d6bdb..57ea65b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -11,7 +11,9 @@ envlist = python,pylint,pylint-tests,flake,mypy,black,isort
isolated_build = true
[testenv]
-deps = poetry
+deps =
+ poetry
+ psycopg2
skip_install = true
passenv =
TERM
@@ -51,7 +53,7 @@ changedir={toxinidir}{/}doc
commands_pre =
poetry install -v --with docs
commands =
- sphinx-apidoc -d 1 -f -M -e -o developer/module/ ../fietsboek "upd_*"
+ sphinx-apidoc -d 1 -M -e -o developer/module/ ../fietsboek "upd_*"
make html
mkdir -p _build/man
rst2man man/fietsctl.rst _build/man/fietsctl.1