From ebea732518a30e959d0a762929504327dbee97d4 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Thu, 20 Nov 2025 22:16:35 +0100 Subject: initial PDF generation This adds initial functionality to render PDF overviews of tracks. I was pondering to use reportlab and do it completely in Python, but the effort of doing proper layouting seemed to much. Add to that the fact that Typst has much nicer typesetting, it seems like a no-brainer to use it. --- fietsboek/pdf-assets/Nunito.ttf | Bin 0 -> 132200 bytes fietsboek/pdf-assets/overview.typ | 27 ++++++ fietsboek/pdf.py | 199 ++++++++++++++++++++++++++++++++++++++ fietsboek/routes.py | 5 + fietsboek/trackmap.py | 4 +- fietsboek/views/detail.py | 12 ++- poetry.lock | 31 +++++- pyproject.toml | 3 +- 8 files changed, 275 insertions(+), 6 deletions(-) create mode 100644 fietsboek/pdf-assets/Nunito.ttf create mode 100644 fietsboek/pdf-assets/overview.typ create mode 100644 fietsboek/pdf.py diff --git a/fietsboek/pdf-assets/Nunito.ttf b/fietsboek/pdf-assets/Nunito.ttf new file mode 100644 index 0000000..be80c3f Binary files /dev/null and b/fietsboek/pdf-assets/Nunito.ttf differ diff --git a/fietsboek/pdf-assets/overview.typ b/fietsboek/pdf-assets/overview.typ new file mode 100644 index 0000000..0054483 --- /dev/null +++ b/fietsboek/pdf-assets/overview.typ @@ -0,0 +1,27 @@ +#set page(margin: 1cm) +#set text(font: "Nunito") + +#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") + +#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..a6b40c9 --- /dev/null +++ b/fietsboek/pdf.py @@ -0,0 +1,199 @@ +import html.parser +import importlib.resources +import io +import logging +import tempfile +from dataclasses import dataclass +from itertools import chain +from pathlib import Path + +import jinja2 +import typst +from babel.dates import format_datetime +from babel.numbers import format_decimal +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 = 900 +IMAGE_HEIGHT = 300 +# See https://typst.app/docs/reference/syntax/ +TO_ESCAPE = { + "$", + "#", + "[", + "]", + "*", + "_", + "`", + "<", + ">", + "@", + "=", + "-", + "+", + "/", + "\\", +} + + +class HtmlToTypst(html.parser.HTMLParser): + 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: + return '"' + "".join(f"\\u{{{ord(char):x}}}" for char in value) + '"' + + +def typst_escape(value: str) -> str: + return "".join("\\" + char if char in TO_ESCAPE else char for char in value) + + +def md_to_typst(value: str) -> str: + html = util.safe_markdown(value).unescape() + buffer = io.StringIO() + parser = HtmlToTypst(buffer) + parser.feed(html) + return buffer.getvalue() + + +def draw_map(track: Track, requester: TileRequester, tile_layer: TileLayerConfig, outfile: Path): + map_image = trackmap.render( + track.path(), + tile_layer, + requester, + size=(IMAGE_WIDTH, IMAGE_HEIGHT), + ) + map_image.save(str(outfile)) + + +def generate( + track: Track, requester: TileRequester, tile_layer: TileLayerConfig, localizer: Localizer +) -> bytes: + """Generate a PDF representation for the given track.""" + 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: + LOGGER.debug("New PDF generation in %s", temp_dir) + temp_dir = Path(temp_dir) + + font_data = importlib.resources.read_binary("fietsboek", "pdf-assets/Nunito.ttf") + (temp_dir / "Nunito.ttf").write_bytes(font_data) + + draw_map(track, requester, tile_layer, temp_dir / "mapimage.png") + LOGGER.debug("%s: map 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 + + +__all__ = ["generate"] diff --git a/fietsboek/routes.py b/fietsboek/routes.py index 4286e92..b8a0113 100644 --- a/fietsboek/routes.py +++ b/fietsboek/routes.py @@ -51,6 +51,11 @@ def includeme(config): "/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("badge", "/badge/{badge_id}", factory="fietsboek.models.Badge.factory") diff --git a/fietsboek/trackmap.py b/fietsboek/trackmap.py index 9854211..4280e0c 100644 --- a/fietsboek/trackmap.py +++ b/fietsboek/trackmap.py @@ -119,7 +119,7 @@ class TrackMapRenderer: draw.line(coords, fill=self.color, width=self.line_width, joint="curve") -def render(track: geo.Path, layer: TileLayerConfig, requester: TileRequester) -> Image.Image: +def render(track: geo.Path, layer: TileLayerConfig, requester: TileRequester, size: (int, int) = (300, 300)) -> Image.Image: """Shorthand to construct a :class:`TrackMapRenderer` and render the preview. :param track: Track to render. @@ -127,7 +127,7 @@ def render(track: geo.Path, layer: TileLayerConfig, requester: TileRequester) -> :param requester: The requester which will be used to request the tiles. :return: The image containing the rendered preview. """ - return TrackMapRenderer(track, requester, (300, 300), layer).render() + return TrackMapRenderer(track, requester, size, layer).render() __all__ = ["to_web_mercator", "TrackMapRenderer", "render"] diff --git a/fietsboek/views/detail.py b/fietsboek/views/detail.py index 8ca7836..b1d3c74 100644 --- a/fietsboek/views/detail.py +++ b/fietsboek/views/detail.py @@ -19,7 +19,7 @@ from pyramid.response import FileResponse, Response from pyramid.view import view_config from sqlalchemy import select -from .. import models, trackmap, util +from .. import models, pdf, trackmap, util from ..models.track import Track, TrackWithMetadata from .tileproxy import ITileRequester @@ -286,6 +286,15 @@ def track_map(request: Request): return response +@view_config(route_name="track-pdf", permission="track.view") +def track_pdf(request: Request): + 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", @@ -295,4 +304,5 @@ __all__ = [ "image", "add_comment", "track_map", + "track_pdf", ] diff --git a/poetry.lock b/poetry.lock index c5ba468..6f9d615 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2493,6 +2493,33 @@ files = [ [package.dependencies] typing-extensions = ">=4.12.0" +[[package]] +name = "typst" +version = "0.14.1" +description = "" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "typst-0.14.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:259e31fac599b21187384942a806778be35d2391362e7f3bf3970b74a1581008"}, + {file = "typst-0.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b14efb183e71aac2f15d83a11c4d90d474c07ed4bb021a574cdfcffe5fdb3146"}, + {file = "typst-0.14.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f695316ff4aab056d49dec04c4f34a6a29a26694ef8fdc85b1eafe8c7b589ec4"}, + {file = "typst-0.14.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:af9dd117af2f0c808cb1937acb9c88dda4895d9f5670fdff8c8a67281775c49c"}, + {file = "typst-0.14.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e86d766f13590b67fb36764ff0e5e89710a7e2fea8ecdc41fc9bf38aa6de1c9"}, + {file = "typst-0.14.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2178364f3787cefd2bfc2473cb2f37b7142b2ef6db7a8909344a03de506577a5"}, + {file = "typst-0.14.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9824868688a011b645275f3252a217e18344f043683fa2a34a0c9e862e7cecd5"}, + {file = "typst-0.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:c6acfb92cb1cd55ca7ace1e3cb4244de7d6aea43def0f4fb0a86bc4acc546cb0"}, + {file = "typst-0.14.1-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:4498657165994502aaba75364c5830c44509bd06d28b1b1da2d0de29c5271a65"}, + {file = "typst-0.14.1-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:386fdd45862d69f53960576a9a8834079bef1c0cea7bcd6e31af8f7f9b3f1a2c"}, + {file = "typst-0.14.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8a249d78debf8446faa72d928762fbfab3ca934b549b6122405533bffc3666c"}, + {file = "typst-0.14.1-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77a16fd700fd1e9108b8e5b25529e09f42e9a9d73163bba14010ae7864d43c42"}, + {file = "typst-0.14.1-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b001ce9193650e3d4d743c1f098f5ad51ab3aa8fc22d7cf2a1078e3092d554a2"}, + {file = "typst-0.14.1-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6488e56b6991a7fa72dc76326420e48af88b8d5524f467c7ec9ac50058d0ae95"}, + {file = "typst-0.14.1-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ca7303acec9fcbf06d949d8c7c1d5ccb986c1ac96a8fbdf08e25ac338df4a92"}, + {file = "typst-0.14.1-cp38-abi3-win_amd64.whl", hash = "sha256:9a327226c639510d163578f454062f80505332a91f7599eb8f72d51f172a0f19"}, + {file = "typst-0.14.1.tar.gz", hash = "sha256:8424387c4ea709caf0dcd54c44f736f86cdde421ac86f9a5d2fa76ba4978b50e"}, +] + [[package]] name = "urllib3" version = "2.5.0" @@ -2675,5 +2702,5 @@ hittekaart = ["hittekaart-py"] [metadata] lock-version = "2.1" -python-versions = ">=3.11" -content-hash = "d845a9c1ae4a96f42c85dd2d73f364f22b4bfe14779bf0ad902557faffa6a7db" +python-versions = ">=3.11, <4" +content-hash = "1f96ea2a459f39a0179e0593f409b622199abecd494359be15b689ff18b13c3b" diff --git a/pyproject.toml b/pyproject.toml index 891b5e3..bb01629 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ authors = [ { name = "Daniel Schadt", email = "fietsboek@kingdread.de>" }, ] keywords = ["web", "gpx"] -requires-python = ">=3.11" +requires-python = ">=3.11, <4" dependencies = [ "pyramid (>=2, <3)", "pyramid_jinja2 (>=2.10, <3.0)", @@ -42,6 +42,7 @@ dependencies = [ "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)", ] [project.urls] -- cgit v1.2.3 From 7171fef21b065715f3ce48cad57ee522cdd53a1e Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Thu, 20 Nov 2025 22:23:48 +0100 Subject: add translation strings for PDF --- fietsboek/locale/de/LC_MESSAGES/messages.mo | Bin 17985 -> 18365 bytes fietsboek/locale/de/LC_MESSAGES/messages.po | 62 +++++++++++++++++++++------- fietsboek/locale/en/LC_MESSAGES/messages.mo | Bin 16894 -> 17252 bytes fietsboek/locale/en/LC_MESSAGES/messages.po | 62 +++++++++++++++++++++------- fietsboek/locale/fietslog.pot | 62 +++++++++++++++++++++------- 5 files changed, 141 insertions(+), 45 deletions(-) diff --git a/fietsboek/locale/de/LC_MESSAGES/messages.mo b/fietsboek/locale/de/LC_MESSAGES/messages.mo index 22982c0..20859a6 100644 Binary files a/fietsboek/locale/de/LC_MESSAGES/messages.mo and b/fietsboek/locale/de/LC_MESSAGES/messages.mo differ diff --git a/fietsboek/locale/de/LC_MESSAGES/messages.po b/fietsboek/locale/de/LC_MESSAGES/messages.po index e9c65c6..4e55f89 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-11-01 15:25+0100\n" +"POT-Creation-Date: 2025-11-20 22:19+0100\n" "PO-Revision-Date: 2022-07-02 17:35+0200\n" "Last-Translator: FULL NAME \n" "Language: de\n" @@ -18,17 +18,49 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.17.0\n" -#: fietsboek/actions.py:265 +#: fietsboek/actions.py:268 msgid "email.verify_mail.subject" msgstr "Fietsboek Konto Bestätigung" -#: fietsboek/actions.py:268 +#: fietsboek/actions.py:271 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/pdf.py:149 +msgid "pdf.table.date" +msgstr "Datum" + +#: fietsboek/pdf.py:151 +msgid "pdf.table.length" +msgstr "Länge" + +#: fietsboek/pdf.py:155 +msgid "pdf.table.uphill" +msgstr "Bergauf" + +#: fietsboek/pdf.py:159 +msgid "pdf.table.downhill" +msgstr "Bergab" + +#: fietsboek/pdf.py:162 +msgid "pdf.table.moving_time" +msgstr "Fahrzeit" + +#: fietsboek/pdf.py:163 +msgid "pdf.table.stopped_time" +msgstr "Haltezeit" + +#: fietsboek/pdf.py:165 +msgid "pdf.table.max_speed" +msgstr "Maximalgeschwindigkeit" + +#: fietsboek/pdf.py:169 +msgid "pdf.table.avg_speed" +msgstr "Durchschnittsgeschwindigkeit" + #: fietsboek/util.py:297 msgid "password_constraint.mismatch" msgstr "Passwörter stimmen nicht überein" @@ -37,35 +69,35 @@ msgstr "Passwörter stimmen nicht überein" msgid "password_constraint.length" msgstr "Passwort zu kurz" -#: fietsboek/models/track.py:725 +#: fietsboek/models/track.py:776 msgid "tooltip.table.length" msgstr "Länge" -#: fietsboek/models/track.py:726 +#: fietsboek/models/track.py:777 msgid "tooltip.table.people" msgstr "# Personen" -#: fietsboek/models/track.py:727 +#: fietsboek/models/track.py:778 msgid "tooltip.table.uphill" msgstr "Bergauf" -#: fietsboek/models/track.py:728 +#: fietsboek/models/track.py:779 msgid "tooltip.table.downhill" msgstr "Bergab" -#: fietsboek/models/track.py:729 fietsboek/templates/home.jinja2:7 +#: fietsboek/models/track.py:780 fietsboek/templates/home.jinja2:7 msgid "tooltip.table.moving_time" msgstr "Fahrzeit" -#: fietsboek/models/track.py:730 fietsboek/templates/home.jinja2:8 +#: fietsboek/models/track.py:781 fietsboek/templates/home.jinja2:8 msgid "tooltip.table.stopped_time" msgstr "Haltezeit" -#: fietsboek/models/track.py:732 +#: fietsboek/models/track.py:783 msgid "tooltip.table.max_speed" msgstr "Maximalgeschwindigkeit" -#: fietsboek/models/track.py:736 +#: fietsboek/models/track.py:787 msgid "tooltip.table.avg_speed" msgstr "Durchschnittsgeschwindigkeit" @@ -1002,15 +1034,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:168 +#: fietsboek/views/admin.py:183 msgid "flash.badge_added" msgstr "Wappen hinzugefügt" -#: fietsboek/views/admin.py:192 +#: fietsboek/views/admin.py:207 msgid "flash.badge_modified" msgstr "Wappen bearbeitet" -#: fietsboek/views/admin.py:212 +#: fietsboek/views/admin.py:227 msgid "flash.badge_deleted" msgstr "Wappen gelöscht" @@ -1074,7 +1106,7 @@ msgstr "Passwort aktualisiert" msgid "flash.track_deleted" msgstr "Strecke gelöscht" -#: fietsboek/views/edit.py:97 fietsboek/views/upload.py:63 +#: fietsboek/views/edit.py:98 fietsboek/views/upload.py:63 msgid "flash.invalid_file" msgstr "Ungültige GPX-Datei gesendet" diff --git a/fietsboek/locale/en/LC_MESSAGES/messages.mo b/fietsboek/locale/en/LC_MESSAGES/messages.mo index d54ad31..271510e 100644 Binary files a/fietsboek/locale/en/LC_MESSAGES/messages.mo and b/fietsboek/locale/en/LC_MESSAGES/messages.mo differ diff --git a/fietsboek/locale/en/LC_MESSAGES/messages.po b/fietsboek/locale/en/LC_MESSAGES/messages.po index 7ccdf1c..5696108 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-11-01 15:25+0100\n" +"POT-Creation-Date: 2025-11-20 22:19+0100\n" "PO-Revision-Date: 2023-04-03 20:42+0200\n" "Last-Translator: \n" "Language: en\n" @@ -18,17 +18,49 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.17.0\n" -#: fietsboek/actions.py:265 +#: fietsboek/actions.py:268 msgid "email.verify_mail.subject" msgstr "Fietsboek Account Verification" -#: fietsboek/actions.py:268 +#: fietsboek/actions.py:271 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/pdf.py:149 +msgid "pdf.table.date" +msgstr "Date" + +#: fietsboek/pdf.py:151 +msgid "pdf.table.length" +msgstr "Length" + +#: fietsboek/pdf.py:155 +msgid "pdf.table.uphill" +msgstr "Uphill" + +#: fietsboek/pdf.py:159 +msgid "pdf.table.downhill" +msgstr "Downhill" + +#: fietsboek/pdf.py:162 +msgid "pdf.table.moving_time" +msgstr "Moving Time" + +#: fietsboek/pdf.py:163 +msgid "pdf.table.stopped_time" +msgstr "Stopped Time" + +#: fietsboek/pdf.py:165 +msgid "pdf.table.max_speed" +msgstr "Max Speed" + +#: fietsboek/pdf.py:169 +msgid "pdf.table.avg_speed" +msgstr "Average Speed" + #: fietsboek/util.py:297 msgid "password_constraint.mismatch" msgstr "Passwords don't match" @@ -37,35 +69,35 @@ msgstr "Passwords don't match" msgid "password_constraint.length" msgstr "Password not long enough" -#: fietsboek/models/track.py:725 +#: fietsboek/models/track.py:776 msgid "tooltip.table.length" msgstr "Length" -#: fietsboek/models/track.py:726 +#: fietsboek/models/track.py:777 msgid "tooltip.table.people" msgstr "# People" -#: fietsboek/models/track.py:727 +#: fietsboek/models/track.py:778 msgid "tooltip.table.uphill" msgstr "Uphill" -#: fietsboek/models/track.py:728 +#: fietsboek/models/track.py:779 msgid "tooltip.table.downhill" msgstr "Downhill" -#: fietsboek/models/track.py:729 fietsboek/templates/home.jinja2:7 +#: fietsboek/models/track.py:780 fietsboek/templates/home.jinja2:7 msgid "tooltip.table.moving_time" msgstr "Moving Time" -#: fietsboek/models/track.py:730 fietsboek/templates/home.jinja2:8 +#: fietsboek/models/track.py:781 fietsboek/templates/home.jinja2:8 msgid "tooltip.table.stopped_time" msgstr "Stopped Time" -#: fietsboek/models/track.py:732 +#: fietsboek/models/track.py:783 msgid "tooltip.table.max_speed" msgstr "Max Speed" -#: fietsboek/models/track.py:736 +#: fietsboek/models/track.py:787 msgid "tooltip.table.avg_speed" msgstr "Average Speed" @@ -992,15 +1024,15 @@ msgstr "Invalid email" msgid "flash.a_confirmation_link_has_been_sent" msgstr "A confirmation link has been sent" -#: fietsboek/views/admin.py:168 +#: fietsboek/views/admin.py:183 msgid "flash.badge_added" msgstr "Badge has been added" -#: fietsboek/views/admin.py:192 +#: fietsboek/views/admin.py:207 msgid "flash.badge_modified" msgstr "Badge has been modified" -#: fietsboek/views/admin.py:212 +#: fietsboek/views/admin.py:227 msgid "flash.badge_deleted" msgstr "Badge has been deleted" @@ -1063,7 +1095,7 @@ msgstr "Password has been updated" msgid "flash.track_deleted" msgstr "Track has been deleted" -#: fietsboek/views/edit.py:97 fietsboek/views/upload.py:63 +#: fietsboek/views/edit.py:98 fietsboek/views/upload.py:63 msgid "flash.invalid_file" msgstr "Invalid GPX file selected" diff --git a/fietsboek/locale/fietslog.pot b/fietsboek/locale/fietslog.pot index cedd3ac..d42ffdc 100644 --- a/fietsboek/locale/fietslog.pot +++ b/fietsboek/locale/fietslog.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2025-11-01 15:25+0100\n" +"POT-Creation-Date: 2025-11-20 22:19+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,14 +17,46 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.17.0\n" -#: fietsboek/actions.py:265 +#: fietsboek/actions.py:268 msgid "email.verify_mail.subject" msgstr "" -#: fietsboek/actions.py:268 +#: fietsboek/actions.py:271 msgid "email.verify.text" msgstr "" +#: fietsboek/pdf.py:149 +msgid "pdf.table.date" +msgstr "" + +#: fietsboek/pdf.py:151 +msgid "pdf.table.length" +msgstr "" + +#: fietsboek/pdf.py:155 +msgid "pdf.table.uphill" +msgstr "" + +#: fietsboek/pdf.py:159 +msgid "pdf.table.downhill" +msgstr "" + +#: fietsboek/pdf.py:162 +msgid "pdf.table.moving_time" +msgstr "" + +#: fietsboek/pdf.py:163 +msgid "pdf.table.stopped_time" +msgstr "" + +#: fietsboek/pdf.py:165 +msgid "pdf.table.max_speed" +msgstr "" + +#: fietsboek/pdf.py:169 +msgid "pdf.table.avg_speed" +msgstr "" + #: fietsboek/util.py:297 msgid "password_constraint.mismatch" msgstr "" @@ -33,35 +65,35 @@ msgstr "" msgid "password_constraint.length" msgstr "" -#: fietsboek/models/track.py:725 +#: fietsboek/models/track.py:776 msgid "tooltip.table.length" msgstr "" -#: fietsboek/models/track.py:726 +#: fietsboek/models/track.py:777 msgid "tooltip.table.people" msgstr "" -#: fietsboek/models/track.py:727 +#: fietsboek/models/track.py:778 msgid "tooltip.table.uphill" msgstr "" -#: fietsboek/models/track.py:728 +#: fietsboek/models/track.py:779 msgid "tooltip.table.downhill" msgstr "" -#: fietsboek/models/track.py:729 fietsboek/templates/home.jinja2:7 +#: fietsboek/models/track.py:780 fietsboek/templates/home.jinja2:7 msgid "tooltip.table.moving_time" msgstr "" -#: fietsboek/models/track.py:730 fietsboek/templates/home.jinja2:8 +#: fietsboek/models/track.py:781 fietsboek/templates/home.jinja2:8 msgid "tooltip.table.stopped_time" msgstr "" -#: fietsboek/models/track.py:732 +#: fietsboek/models/track.py:783 msgid "tooltip.table.max_speed" msgstr "" -#: fietsboek/models/track.py:736 +#: fietsboek/models/track.py:787 msgid "tooltip.table.avg_speed" msgstr "" @@ -978,15 +1010,15 @@ msgstr "" msgid "flash.a_confirmation_link_has_been_sent" msgstr "" -#: fietsboek/views/admin.py:168 +#: fietsboek/views/admin.py:183 msgid "flash.badge_added" msgstr "" -#: fietsboek/views/admin.py:192 +#: fietsboek/views/admin.py:207 msgid "flash.badge_modified" msgstr "" -#: fietsboek/views/admin.py:212 +#: fietsboek/views/admin.py:227 msgid "flash.badge_deleted" msgstr "" @@ -1046,7 +1078,7 @@ msgstr "" msgid "flash.track_deleted" msgstr "" -#: fietsboek/views/edit.py:97 fietsboek/views/upload.py:63 +#: fietsboek/views/edit.py:98 fietsboek/views/upload.py:63 msgid "flash.invalid_file" msgstr "" -- cgit v1.2.3 From b456196f9a9d200bb2003c847836621fd39b43a5 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Thu, 20 Nov 2025 22:38:59 +0100 Subject: add docstrings & fix lint --- fietsboek/pdf.py | 69 ++++++++++++++++++++++++++++++++++++++++++----- fietsboek/trackmap.py | 4 ++- fietsboek/views/detail.py | 5 ++++ 3 files changed, 71 insertions(+), 7 deletions(-) diff --git a/fietsboek/pdf.py b/fietsboek/pdf.py index a6b40c9..97e1d7b 100644 --- a/fietsboek/pdf.py +++ b/fietsboek/pdf.py @@ -1,9 +1,23 @@ +"""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 dataclasses import dataclass from itertools import chain from pathlib import Path @@ -45,6 +59,13 @@ 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 @@ -102,22 +123,46 @@ class HtmlToTypst(html.parser.HTMLParser): 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: - html = util.safe_markdown(value).unescape() + """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) + 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, @@ -130,7 +175,19 @@ def draw_map(track: Track, requester: TileRequester, tile_layer: TileLayerConfig def generate( track: Track, requester: TileRequester, tile_layer: TileLayerConfig, localizer: Localizer ) -> bytes: - """Generate a PDF representation for the given track.""" + """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, @@ -173,9 +230,9 @@ def generate( "description": track.description, } - with tempfile.TemporaryDirectory(prefix=TEMP_PREFIX) as temp_dir: + 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) - temp_dir = Path(temp_dir) font_data = importlib.resources.read_binary("fietsboek", "pdf-assets/Nunito.ttf") (temp_dir / "Nunito.ttf").write_bytes(font_data) diff --git a/fietsboek/trackmap.py b/fietsboek/trackmap.py index 4280e0c..c767852 100644 --- a/fietsboek/trackmap.py +++ b/fietsboek/trackmap.py @@ -119,7 +119,9 @@ class TrackMapRenderer: draw.line(coords, fill=self.color, width=self.line_width, joint="curve") -def render(track: geo.Path, layer: TileLayerConfig, requester: TileRequester, size: (int, int) = (300, 300)) -> Image.Image: +def render( + track: geo.Path, layer: TileLayerConfig, requester: TileRequester, size: (int, int) = (300, 300) +) -> Image.Image: """Shorthand to construct a :class:`TrackMapRenderer` and render the preview. :param track: Track to render. diff --git a/fietsboek/views/detail.py b/fietsboek/views/detail.py index b1d3c74..16c896a 100644 --- a/fietsboek/views/detail.py +++ b/fietsboek/views/detail.py @@ -288,6 +288,11 @@ def track_map(request: Request): @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) -- cgit v1.2.3 From 7db50bc298a37aae2938fef361db1eae9f5ebc35 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Thu, 20 Nov 2025 23:11:15 +0100 Subject: add height profile to PDF --- fietsboek/pdf-assets/overview.typ | 2 + fietsboek/pdf.py | 30 +++ poetry.lock | 507 +++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 4 files changed, 539 insertions(+), 1 deletion(-) diff --git a/fietsboek/pdf-assets/overview.typ b/fietsboek/pdf-assets/overview.typ index 0054483..8628c81 100644 --- a/fietsboek/pdf-assets/overview.typ +++ b/fietsboek/pdf-assets/overview.typ @@ -15,6 +15,8 @@ #image("mapimage.png") +#image(width: 100%, "height_profile.pdf") + #table( columns: (50%, 50%), stroke: none, diff --git a/fietsboek/pdf.py b/fietsboek/pdf.py index 97e1d7b..1f11070 100644 --- a/fietsboek/pdf.py +++ b/fietsboek/pdf.py @@ -22,9 +22,11 @@ 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 _ @@ -172,6 +174,31 @@ def draw_map(track: Track, requester: TileRequester, tile_layer: TileLayerConfig 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 + 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: @@ -240,6 +267,9 @@ def generate( 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) diff --git a/poetry.lock b/poetry.lock index 6f9d615..4ee4af1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -528,6 +528,98 @@ files = [ ] 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.11.0" @@ -710,6 +802,22 @@ ssh = ["bcrypt (>=3.1.5)"] test = ["certifi (>=2024)", "cryptography-vectors (==46.0.2)", "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 = "cycler" +version = "0.12.1" +description = "Composable style cycles" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, + {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, +] + +[package.extras] +docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] +tests = ["pytest", "pytest-cov", "pytest-xdist"] + [[package]] name = "dill" version = "0.4.0" @@ -761,6 +869,87 @@ files = [ {file = "fitparse-1.2.0.tar.gz", hash = "sha256:2d691022452dea6dabad13cc6e017ca467fe8a3a895cd3ac67a50a7bb716b4a9"}, ] +[[package]] +name = "fonttools" +version = "4.60.1" +description = "Tools to manipulate font files" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "fonttools-4.60.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9a52f254ce051e196b8fe2af4634c2d2f02c981756c6464dc192f1b6050b4e28"}, + {file = "fonttools-4.60.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7420a2696a44650120cdd269a5d2e56a477e2bfa9d95e86229059beb1c19e15"}, + {file = "fonttools-4.60.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee0c0b3b35b34f782afc673d503167157094a16f442ace7c6c5e0ca80b08f50c"}, + {file = "fonttools-4.60.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:282dafa55f9659e8999110bd8ed422ebe1c8aecd0dc396550b038e6c9a08b8ea"}, + {file = "fonttools-4.60.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4ba4bd646e86de16160f0fb72e31c3b9b7d0721c3e5b26b9fa2fc931dfdb2652"}, + {file = "fonttools-4.60.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0b0835ed15dd5b40d726bb61c846a688f5b4ce2208ec68779bc81860adb5851a"}, + {file = "fonttools-4.60.1-cp310-cp310-win32.whl", hash = "sha256:1525796c3ffe27bb6268ed2a1bb0dcf214d561dfaf04728abf01489eb5339dce"}, + {file = "fonttools-4.60.1-cp310-cp310-win_amd64.whl", hash = "sha256:268ecda8ca6cb5c4f044b1fb9b3b376e8cd1b361cef275082429dc4174907038"}, + {file = "fonttools-4.60.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f"}, + {file = "fonttools-4.60.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2"}, + {file = "fonttools-4.60.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914"}, + {file = "fonttools-4.60.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1"}, + {file = "fonttools-4.60.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d"}, + {file = "fonttools-4.60.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa"}, + {file = "fonttools-4.60.1-cp311-cp311-win32.whl", hash = "sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258"}, + {file = "fonttools-4.60.1-cp311-cp311-win_amd64.whl", hash = "sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf"}, + {file = "fonttools-4.60.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc"}, + {file = "fonttools-4.60.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877"}, + {file = "fonttools-4.60.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c"}, + {file = "fonttools-4.60.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401"}, + {file = "fonttools-4.60.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903"}, + {file = "fonttools-4.60.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed"}, + {file = "fonttools-4.60.1-cp312-cp312-win32.whl", hash = "sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6"}, + {file = "fonttools-4.60.1-cp312-cp312-win_amd64.whl", hash = "sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383"}, + {file = "fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb"}, + {file = "fonttools-4.60.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4"}, + {file = "fonttools-4.60.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c"}, + {file = "fonttools-4.60.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77"}, + {file = "fonttools-4.60.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199"}, + {file = "fonttools-4.60.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c"}, + {file = "fonttools-4.60.1-cp313-cp313-win32.whl", hash = "sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272"}, + {file = "fonttools-4.60.1-cp313-cp313-win_amd64.whl", hash = "sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac"}, + {file = "fonttools-4.60.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3"}, + {file = "fonttools-4.60.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85"}, + {file = "fonttools-4.60.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537"}, + {file = "fonttools-4.60.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003"}, + {file = "fonttools-4.60.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08"}, + {file = "fonttools-4.60.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99"}, + {file = "fonttools-4.60.1-cp314-cp314-win32.whl", hash = "sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6"}, + {file = "fonttools-4.60.1-cp314-cp314-win_amd64.whl", hash = "sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987"}, + {file = "fonttools-4.60.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299"}, + {file = "fonttools-4.60.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01"}, + {file = "fonttools-4.60.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801"}, + {file = "fonttools-4.60.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc"}, + {file = "fonttools-4.60.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc"}, + {file = "fonttools-4.60.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed"}, + {file = "fonttools-4.60.1-cp314-cp314t-win32.whl", hash = "sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259"}, + {file = "fonttools-4.60.1-cp314-cp314t-win_amd64.whl", hash = "sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c"}, + {file = "fonttools-4.60.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:122e1a8ada290423c493491d002f622b1992b1ab0b488c68e31c413390dc7eb2"}, + {file = "fonttools-4.60.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a140761c4ff63d0cb9256ac752f230460ee225ccef4ad8f68affc723c88e2036"}, + {file = "fonttools-4.60.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eae96373e4b7c9e45d099d7a523444e3554360927225c1cdae221a58a45b856"}, + {file = "fonttools-4.60.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:596ecaca36367027d525b3b426d8a8208169d09edcf8c7506aceb3a38bfb55c7"}, + {file = "fonttools-4.60.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2ee06fc57512144d8b0445194c2da9f190f61ad51e230f14836286470c99f854"}, + {file = "fonttools-4.60.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b42d86938e8dda1cd9a1a87a6d82f1818eaf933348429653559a458d027446da"}, + {file = "fonttools-4.60.1-cp39-cp39-win32.whl", hash = "sha256:8b4eb332f9501cb1cd3d4d099374a1e1306783ff95489a1026bde9eb02ccc34a"}, + {file = "fonttools-4.60.1-cp39-cp39-win_amd64.whl", hash = "sha256:7473a8ed9ed09aeaa191301244a5a9dbe46fe0bf54f9d6cd21d83044c3321217"}, + {file = "fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb"}, + {file = "fonttools-4.60.1.tar.gz", hash = "sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9"}, +] + +[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.23.0)", "unicodedata2 (>=15.1.0) ; python_version <= \"3.12\"", "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.23.0)"] +symfont = ["sympy"] +type1 = ["xattr ; sys_platform == \"darwin\""] +unicode = ["unicodedata2 (>=15.1.0) ; python_version <= \"3.12\""] +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" @@ -949,6 +1138,117 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "kiwisolver" +version = "1.4.9" +description = "A fast implementation of the Cassowary constraint solver" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "kiwisolver-1.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b"}, + {file = "kiwisolver-1.4.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fb3b8132019ea572f4611d770991000d7f58127560c4889729248eb5852a102f"}, + {file = "kiwisolver-1.4.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84fd60810829c27ae375114cd379da1fa65e6918e1da405f356a775d49a62bcf"}, + {file = "kiwisolver-1.4.9-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b78efa4c6e804ecdf727e580dbb9cba85624d2e1c6b5cb059c66290063bd99a9"}, + {file = "kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4efec7bcf21671db6a3294ff301d2fc861c31faa3c8740d1a94689234d1b415"}, + {file = "kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90f47e70293fc3688b71271100a1a5453aa9944a81d27ff779c108372cf5567b"}, + {file = "kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fdca1def57a2e88ef339de1737a1449d6dbf5fab184c54a1fca01d541317154"}, + {file = "kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cf554f21be770f5111a1690d42313e140355e687e05cf82cb23d0a721a64a48"}, + {file = "kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1795ac5cd0510207482c3d1d3ed781143383b8cfd36f5c645f3897ce066220"}, + {file = "kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ccd09f20ccdbbd341b21a67ab50a119b64a403b09288c27481575105283c1586"}, + {file = "kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:540c7c72324d864406a009d72f5d6856f49693db95d1fbb46cf86febef873634"}, + {file = "kiwisolver-1.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:ede8c6d533bc6601a47ad4046080d36b8fc99f81e6f1c17b0ac3c2dc91ac7611"}, + {file = "kiwisolver-1.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:7b4da0d01ac866a57dd61ac258c5607b4cd677f63abaec7b148354d2b2cdd536"}, + {file = "kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16"}, + {file = "kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089"}, + {file = "kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543"}, + {file = "kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61"}, + {file = "kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1"}, + {file = "kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872"}, + {file = "kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26"}, + {file = "kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028"}, + {file = "kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771"}, + {file = "kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a"}, + {file = "kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464"}, + {file = "kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2"}, + {file = "kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7"}, + {file = "kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999"}, + {file = "kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2"}, + {file = "kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14"}, + {file = "kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04"}, + {file = "kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752"}, + {file = "kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77"}, + {file = "kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198"}, + {file = "kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d"}, + {file = "kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab"}, + {file = "kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2"}, + {file = "kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145"}, + {file = "kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54"}, + {file = "kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60"}, + {file = "kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8"}, + {file = "kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2"}, + {file = "kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f"}, + {file = "kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098"}, + {file = "kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed"}, + {file = "kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525"}, + {file = "kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78"}, + {file = "kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b"}, + {file = "kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799"}, + {file = "kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3"}, + {file = "kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c"}, + {file = "kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d"}, + {file = "kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2"}, + {file = "kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1"}, + {file = "kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1"}, + {file = "kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11"}, + {file = "kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c"}, + {file = "kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197"}, + {file = "kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c"}, + {file = "kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185"}, + {file = "kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748"}, + {file = "kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64"}, + {file = "kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff"}, + {file = "kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07"}, + {file = "kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c"}, + {file = "kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386"}, + {file = "kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552"}, + {file = "kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3"}, + {file = "kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58"}, + {file = "kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4"}, + {file = "kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df"}, + {file = "kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6"}, + {file = "kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5"}, + {file = "kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf"}, + {file = "kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5"}, + {file = "kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce"}, + {file = "kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7"}, + {file = "kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c"}, + {file = "kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548"}, + {file = "kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d"}, + {file = "kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c"}, + {file = "kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122"}, + {file = "kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64"}, + {file = "kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134"}, + {file = "kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370"}, + {file = "kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21"}, + {file = "kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a"}, + {file = "kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f"}, + {file = "kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369"}, + {file = "kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891"}, + {file = "kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32"}, + {file = "kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4d1d9e582ad4d63062d34077a9a1e9f3c34088a2ec5135b1f7190c07cf366527"}, + {file = "kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:deed0c7258ceb4c44ad5ec7d9918f9f14fd05b2be86378d86cf50e63d1e7b771"}, + {file = "kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a590506f303f512dff6b7f75fd2fd18e16943efee932008fe7140e5fa91d80e"}, + {file = "kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e09c2279a4d01f099f52d5c4b3d9e208e91edcbd1a175c9662a8b16e000fece9"}, + {file = "kiwisolver-1.4.9-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c9e7cdf45d594ee04d5be1b24dd9d49f3d1590959b2271fb30b5ca2b262c00fb"}, + {file = "kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5"}, + {file = "kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa"}, + {file = "kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2"}, + {file = "kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f"}, + {file = "kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1"}, + {file = "kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d"}, +] + [[package]] name = "legacy-cgi" version = "2.6.3" @@ -1097,6 +1397,85 @@ files = [ {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, ] +[[package]] +name = "matplotlib" +version = "3.10.7" +description = "Python plotting package" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "matplotlib-3.10.7-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7ac81eee3b7c266dd92cee1cd658407b16c57eed08c7421fa354ed68234de380"}, + {file = "matplotlib-3.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:667ecd5d8d37813a845053d8f5bf110b534c3c9f30e69ebd25d4701385935a6d"}, + {file = "matplotlib-3.10.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc1c51b846aca49a5a8b44fbba6a92d583a35c64590ad9e1e950dc88940a4297"}, + {file = "matplotlib-3.10.7-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a11c2e9e72e7de09b7b72e62f3df23317c888299c875e2b778abf1eda8c0a42"}, + {file = "matplotlib-3.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f19410b486fdd139885ace124e57f938c1e6a3210ea13dd29cab58f5d4bc12c7"}, + {file = "matplotlib-3.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:b498e9e4022f93de2d5a37615200ca01297ceebbb56fe4c833f46862a490f9e3"}, + {file = "matplotlib-3.10.7-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:53b492410a6cd66c7a471de6c924f6ede976e963c0f3097a3b7abfadddc67d0a"}, + {file = "matplotlib-3.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d9749313deb729f08207718d29c86246beb2ea3fdba753595b55901dee5d2fd6"}, + {file = "matplotlib-3.10.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2222c7ba2cbde7fe63032769f6eb7e83ab3227f47d997a8453377709b7fe3a5a"}, + {file = "matplotlib-3.10.7-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e91f61a064c92c307c5a9dc8c05dc9f8a68f0a3be199d9a002a0622e13f874a1"}, + {file = "matplotlib-3.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6f1851eab59ca082c95df5a500106bad73672645625e04538b3ad0f69471ffcc"}, + {file = "matplotlib-3.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:6516ce375109c60ceec579e699524e9d504cd7578506f01150f7a6bc174a775e"}, + {file = "matplotlib-3.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:b172db79759f5f9bc13ef1c3ef8b9ee7b37b0247f987fbbbdaa15e4f87fd46a9"}, + {file = "matplotlib-3.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a0edb7209e21840e8361e91ea84ea676658aa93edd5f8762793dec77a4a6748"}, + {file = "matplotlib-3.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c380371d3c23e0eadf8ebff114445b9f970aff2010198d498d4ab4c3b41eea4f"}, + {file = "matplotlib-3.10.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d5f256d49fea31f40f166a5e3131235a5d2f4b7f44520b1cf0baf1ce568ccff0"}, + {file = "matplotlib-3.10.7-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11ae579ac83cdf3fb72573bb89f70e0534de05266728740d478f0f818983c695"}, + {file = "matplotlib-3.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4c14b6acd16cddc3569a2d515cfdd81c7a68ac5639b76548cfc1a9e48b20eb65"}, + {file = "matplotlib-3.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:0d8c32b7ea6fb80b1aeff5a2ceb3fb9778e2759e899d9beff75584714afcc5ee"}, + {file = "matplotlib-3.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:5f3f6d315dcc176ba7ca6e74c7768fb7e4cf566c49cb143f6bc257b62e634ed8"}, + {file = "matplotlib-3.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1d9d3713a237970569156cfb4de7533b7c4eacdd61789726f444f96a0d28f57f"}, + {file = "matplotlib-3.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37a1fea41153dd6ee061d21ab69c9cf2cf543160b1b85d89cd3d2e2a7902ca4c"}, + {file = "matplotlib-3.10.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b3c4ea4948d93c9c29dc01c0c23eef66f2101bf75158c291b88de6525c55c3d1"}, + {file = "matplotlib-3.10.7-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22df30ffaa89f6643206cf13877191c63a50e8f800b038bc39bee9d2d4957632"}, + {file = "matplotlib-3.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b69676845a0a66f9da30e87f48be36734d6748024b525ec4710be40194282c84"}, + {file = "matplotlib-3.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:744991e0cc863dd669c8dc9136ca4e6e0082be2070b9d793cbd64bec872a6815"}, + {file = "matplotlib-3.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:fba2974df0bf8ce3c995fa84b79cde38326e0f7b5409e7a3a481c1141340bcf7"}, + {file = "matplotlib-3.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:932c55d1fa7af4423422cb6a492a31cbcbdbe68fd1a9a3f545aa5e7a143b5355"}, + {file = "matplotlib-3.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e38c2d581d62ee729a6e144c47a71b3f42fb4187508dbbf4fe71d5612c3433b"}, + {file = "matplotlib-3.10.7-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:786656bb13c237bbcebcd402f65f44dd61ead60ee3deb045af429d889c8dbc67"}, + {file = "matplotlib-3.10.7-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09d7945a70ea43bf9248f4b6582734c2fe726723204a76eca233f24cffc7ef67"}, + {file = "matplotlib-3.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d0b181e9fa8daf1d9f2d4c547527b167cb8838fc587deabca7b5c01f97199e84"}, + {file = "matplotlib-3.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:31963603041634ce1a96053047b40961f7a29eb8f9a62e80cc2c0427aa1d22a2"}, + {file = "matplotlib-3.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:aebed7b50aa6ac698c90f60f854b47e48cd2252b30510e7a1feddaf5a3f72cbf"}, + {file = "matplotlib-3.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d883460c43e8c6b173fef244a2341f7f7c0e9725c7fe68306e8e44ed9c8fb100"}, + {file = "matplotlib-3.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07124afcf7a6504eafcb8ce94091c5898bbdd351519a1beb5c45f7a38c67e77f"}, + {file = "matplotlib-3.10.7-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c17398b709a6cce3d9fdb1595c33e356d91c098cd9486cb2cc21ea2ea418e715"}, + {file = "matplotlib-3.10.7-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7146d64f561498764561e9cd0ed64fcf582e570fc519e6f521e2d0cfd43365e1"}, + {file = "matplotlib-3.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90ad854c0a435da3104c01e2c6f0028d7e719b690998a2333d7218db80950722"}, + {file = "matplotlib-3.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:4645fc5d9d20ffa3a39361fcdbcec731382763b623b72627806bf251b6388866"}, + {file = "matplotlib-3.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:9257be2f2a03415f9105c486d304a321168e61ad450f6153d77c69504ad764bb"}, + {file = "matplotlib-3.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1e4bbad66c177a8fdfa53972e5ef8be72a5f27e6a607cec0d8579abd0f3102b1"}, + {file = "matplotlib-3.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d8eb7194b084b12feb19142262165832fc6ee879b945491d1c3d4660748020c4"}, + {file = "matplotlib-3.10.7-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d41379b05528091f00e1728004f9a8d7191260f3862178b88e8fd770206318"}, + {file = "matplotlib-3.10.7-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a74f79fafb2e177f240579bc83f0b60f82cc47d2f1d260f422a0627207008ca"}, + {file = "matplotlib-3.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:702590829c30aada1e8cef0568ddbffa77ca747b4d6e36c6d173f66e301f89cc"}, + {file = "matplotlib-3.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:f79d5de970fc90cd5591f60053aecfce1fcd736e0303d9f0bf86be649fa68fb8"}, + {file = "matplotlib-3.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:cb783436e47fcf82064baca52ce748af71725d0352e1d31564cbe9c95df92b9c"}, + {file = "matplotlib-3.10.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5c09cf8f2793f81368f49f118b6f9f937456362bee282eac575cca7f84cda537"}, + {file = "matplotlib-3.10.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:de66744b2bb88d5cd27e80dfc2ec9f0517d0a46d204ff98fe9e5f2864eb67657"}, + {file = "matplotlib-3.10.7-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:53cc80662dd197ece414dd5b66e07370201515a3eaf52e7c518c68c16814773b"}, + {file = "matplotlib-3.10.7-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:15112bcbaef211bd663fa935ec33313b948e214454d949b723998a43357b17b0"}, + {file = "matplotlib-3.10.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d2a959c640cdeecdd2ec3136e8ea0441da59bcaf58d67e9c590740addba2cb68"}, + {file = "matplotlib-3.10.7-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3886e47f64611046bc1db523a09dd0a0a6bed6081e6f90e13806dd1d1d1b5e91"}, + {file = "matplotlib-3.10.7.tar.gz", hash = "sha256:a06ba7e2a2ef9131c79c49e63dad355d2d878413a0376c1727c8b9335ff731c7"}, +] + +[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)"] + [[package]] name = "mccabe" version = "0.7.0" @@ -1217,6 +1596,90 @@ files = [ {file = "nh3-0.3.1.tar.gz", hash = "sha256:6a854480058683d60bdc7f0456105092dae17bef1f300642856d74bd4201da93"}, ] +[[package]] +name = "numpy" +version = "2.3.5" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.11" +groups = ["main"] +files = [ + {file = "numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10"}, + {file = "numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218"}, + {file = "numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d"}, + {file = "numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5"}, + {file = "numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7"}, + {file = "numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4"}, + {file = "numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e"}, + {file = "numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748"}, + {file = "numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c"}, + {file = "numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c"}, + {file = "numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa"}, + {file = "numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e"}, + {file = "numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769"}, + {file = "numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5"}, + {file = "numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4"}, + {file = "numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d"}, + {file = "numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28"}, + {file = "numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b"}, + {file = "numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c"}, + {file = "numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952"}, + {file = "numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa"}, + {file = "numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013"}, + {file = "numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff"}, + {file = "numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188"}, + {file = "numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0"}, + {file = "numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903"}, + {file = "numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d"}, + {file = "numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017"}, + {file = "numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf"}, + {file = "numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce"}, + {file = "numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e"}, + {file = "numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b"}, + {file = "numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae"}, + {file = "numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd"}, + {file = "numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f"}, + {file = "numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a"}, + {file = "numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139"}, + {file = "numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e"}, + {file = "numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9"}, + {file = "numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946"}, + {file = "numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1"}, + {file = "numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3"}, + {file = "numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234"}, + {file = "numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7"}, + {file = "numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82"}, + {file = "numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0"}, + {file = "numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63"}, + {file = "numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9"}, + {file = "numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b"}, + {file = "numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520"}, + {file = "numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c"}, + {file = "numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8"}, + {file = "numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248"}, + {file = "numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e"}, + {file = "numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2"}, + {file = "numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41"}, + {file = "numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad"}, + {file = "numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39"}, + {file = "numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20"}, + {file = "numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52"}, + {file = "numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b"}, + {file = "numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3"}, + {file = "numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227"}, + {file = "numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5"}, + {file = "numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf"}, + {file = "numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42"}, + {file = "numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310"}, + {file = "numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c"}, + {file = "numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18"}, + {file = "numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff"}, + {file = "numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb"}, + {file = "numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7"}, + {file = "numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425"}, + {file = "numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0"}, +] + [[package]] name = "packaging" version = "25.0" @@ -1683,6 +2146,21 @@ tomlkit = ">=0.10.1" spelling = ["pyenchant (>=3.2,<4.0)"] testutils = ["gitpython (>3)"] +[[package]] +name = "pyparsing" +version = "3.2.5" +description = "pyparsing - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e"}, + {file = "pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + [[package]] name = "pyramid" version = "2.0.2" @@ -1892,6 +2370,21 @@ pytest = ">=6.2.4,<9.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" @@ -2004,6 +2497,18 @@ enabler = ["pytest-enabler (>=2.2)"] 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.14.*)", "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 = "3.0.1" @@ -2703,4 +3208,4 @@ hittekaart = ["hittekaart-py"] [metadata] lock-version = "2.1" python-versions = ">=3.11, <4" -content-hash = "1f96ea2a459f39a0179e0593f409b622199abecd494359be15b689ff18b13c3b" +content-hash = "50b2981e10dc1baa9bb0c91ef441a006efebd659b04fcf3d87c54cc058100108" diff --git a/pyproject.toml b/pyproject.toml index bb01629..cb173d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,7 @@ dependencies = [ "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)", ] [project.urls] -- cgit v1.2.3 From 168dc893167a8e940d280bb7a4dc66e2261c1c25 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Thu, 20 Nov 2025 23:12:53 +0100 Subject: fix types --- fietsboek/pdf.py | 2 +- fietsboek/trackmap.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/fietsboek/pdf.py b/fietsboek/pdf.py index 1f11070..5ac237b 100644 --- a/fietsboek/pdf.py +++ b/fietsboek/pdf.py @@ -181,7 +181,7 @@ def draw_height_profile(track: Track, outfile: Path): :param outfile: The output file. """ x, y = [], [] - cur = 0 + cur = 0.0 last = None for point in track.path().points: x.append(cur / 1000) diff --git a/fietsboek/trackmap.py b/fietsboek/trackmap.py index c767852..994b3dd 100644 --- a/fietsboek/trackmap.py +++ b/fietsboek/trackmap.py @@ -120,7 +120,10 @@ class TrackMapRenderer: def render( - track: geo.Path, layer: TileLayerConfig, requester: TileRequester, size: (int, int) = (300, 300) + 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. -- cgit v1.2.3 From d718f986af37d755feaa5a49460983194e6bf28d Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Tue, 25 Nov 2025 20:12:45 +0100 Subject: add button to download PDF --- fietsboek/locale/de/LC_MESSAGES/messages.mo | Bin 18365 -> 18422 bytes fietsboek/locale/de/LC_MESSAGES/messages.po | 56 +++++++++++++++------------- fietsboek/locale/en/LC_MESSAGES/messages.mo | Bin 17252 -> 17307 bytes fietsboek/locale/en/LC_MESSAGES/messages.po | 56 +++++++++++++++------------- fietsboek/locale/fietslog.pot | 56 +++++++++++++++------------- fietsboek/templates/details.jinja2 | 9 ++++- 6 files changed, 98 insertions(+), 79 deletions(-) diff --git a/fietsboek/locale/de/LC_MESSAGES/messages.mo b/fietsboek/locale/de/LC_MESSAGES/messages.mo index 20859a6..1175786 100644 Binary files a/fietsboek/locale/de/LC_MESSAGES/messages.mo and b/fietsboek/locale/de/LC_MESSAGES/messages.mo differ diff --git a/fietsboek/locale/de/LC_MESSAGES/messages.po b/fietsboek/locale/de/LC_MESSAGES/messages.po index 4e55f89..7b6a70e 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-11-20 22:19+0100\n" +"POT-Creation-Date: 2025-11-25 20:03+0100\n" "PO-Revision-Date: 2022-07-02 17:35+0200\n" "Last-Translator: FULL NAME \n" "Language: de\n" @@ -29,35 +29,35 @@ msgstr "" "\n" "Falls Du kein Konto angelegt hast, ignoriere diese E-Mail." -#: fietsboek/pdf.py:149 +#: fietsboek/pdf.py:233 msgid "pdf.table.date" msgstr "Datum" -#: fietsboek/pdf.py:151 +#: fietsboek/pdf.py:235 msgid "pdf.table.length" msgstr "Länge" -#: fietsboek/pdf.py:155 +#: fietsboek/pdf.py:239 msgid "pdf.table.uphill" msgstr "Bergauf" -#: fietsboek/pdf.py:159 +#: fietsboek/pdf.py:243 msgid "pdf.table.downhill" msgstr "Bergab" -#: fietsboek/pdf.py:162 +#: fietsboek/pdf.py:246 msgid "pdf.table.moving_time" msgstr "Fahrzeit" -#: fietsboek/pdf.py:163 +#: fietsboek/pdf.py:247 msgid "pdf.table.stopped_time" msgstr "Haltezeit" -#: fietsboek/pdf.py:165 +#: fietsboek/pdf.py:249 msgid "pdf.table.max_speed" msgstr "Maximalgeschwindigkeit" -#: fietsboek/pdf.py:169 +#: fietsboek/pdf.py:253 msgid "pdf.table.avg_speed" msgstr "Durchschnittsgeschwindigkeit" @@ -327,52 +327,52 @@ msgstr "Dies ist eine Aufnahme einer Strecke" msgid "page.browse.synthetic_tooltip" msgstr "Dies ist eine geplante Strecke" -#: fietsboek/templates/browse.jinja2:162 fietsboek/templates/details.jinja2:120 +#: fietsboek/templates/browse.jinja2:162 fietsboek/templates/details.jinja2:127 #: fietsboek/templates/profile_overview.jinja2:20 msgid "page.details.date" msgstr "Datum" -#: fietsboek/templates/browse.jinja2:164 fietsboek/templates/details.jinja2:134 +#: fietsboek/templates/browse.jinja2:164 fietsboek/templates/details.jinja2:141 #: fietsboek/templates/profile_overview.jinja2:22 msgid "page.details.length" msgstr "Länge" -#: fietsboek/templates/browse.jinja2:169 fietsboek/templates/details.jinja2:125 +#: fietsboek/templates/browse.jinja2:169 fietsboek/templates/details.jinja2:132 #: fietsboek/templates/profile_overview.jinja2:26 msgid "page.details.start_time" msgstr "Startzeit" -#: fietsboek/templates/browse.jinja2:171 fietsboek/templates/details.jinja2:129 +#: fietsboek/templates/browse.jinja2:171 fietsboek/templates/details.jinja2:136 #: fietsboek/templates/profile_overview.jinja2:28 msgid "page.details.end_time" msgstr "Endzeit" -#: fietsboek/templates/browse.jinja2:176 fietsboek/templates/details.jinja2:138 +#: fietsboek/templates/browse.jinja2:176 fietsboek/templates/details.jinja2:145 #: fietsboek/templates/profile_overview.jinja2:32 msgid "page.details.uphill" msgstr "Bergauf" -#: fietsboek/templates/browse.jinja2:178 fietsboek/templates/details.jinja2:142 +#: fietsboek/templates/browse.jinja2:178 fietsboek/templates/details.jinja2:149 #: fietsboek/templates/profile_overview.jinja2:34 msgid "page.details.downhill" msgstr "Bergab" -#: fietsboek/templates/browse.jinja2:183 fietsboek/templates/details.jinja2:147 +#: fietsboek/templates/browse.jinja2:183 fietsboek/templates/details.jinja2:154 #: fietsboek/templates/profile_overview.jinja2:38 msgid "page.details.moving_time" msgstr "Fahrzeit" -#: fietsboek/templates/browse.jinja2:185 fietsboek/templates/details.jinja2:151 +#: fietsboek/templates/browse.jinja2:185 fietsboek/templates/details.jinja2:158 #: fietsboek/templates/profile_overview.jinja2:40 msgid "page.details.stopped_time" msgstr "Haltezeit" -#: fietsboek/templates/browse.jinja2:189 fietsboek/templates/details.jinja2:155 +#: fietsboek/templates/browse.jinja2:189 fietsboek/templates/details.jinja2:162 #: fietsboek/templates/profile_overview.jinja2:44 msgid "page.details.max_speed" msgstr "maximale Geschwindigkeit" -#: fietsboek/templates/browse.jinja2:191 fietsboek/templates/details.jinja2:159 +#: fietsboek/templates/browse.jinja2:191 fietsboek/templates/details.jinja2:166 #: fietsboek/templates/profile_overview.jinja2:46 msgid "page.details.avg_speed" msgstr "durchschnittliche Geschwindigkeit" @@ -506,31 +506,35 @@ msgstr "" "JavaScript ist deaktiviert, zum Nutzen aller Funktionen bitte JavaScript " "aktivieren" -#: fietsboek/templates/details.jinja2:114 +#: fietsboek/templates/details.jinja2:115 msgid "page.details.download" msgstr "Herunterladen" -#: fietsboek/templates/details.jinja2:204 +#: 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:208 +#: fietsboek/templates/details.jinja2:215 msgid "page.details.comments.author" msgstr "Kommentar von {}" -#: fietsboek/templates/details.jinja2:225 +#: fietsboek/templates/details.jinja2:232 msgid "page.details.comments.new.title" msgstr "Kommentar erstellen" -#: fietsboek/templates/details.jinja2:228 +#: fietsboek/templates/details.jinja2:235 msgid "page.details.comments.new.input_title" msgstr "Titel" -#: fietsboek/templates/details.jinja2:229 +#: fietsboek/templates/details.jinja2:236 msgid "page.details.comments.new.input_comment" msgstr "Kommentar" -#: fietsboek/templates/details.jinja2:232 +#: fietsboek/templates/details.jinja2:239 msgid "page.details.comments.new.submit" msgstr "Absenden" diff --git a/fietsboek/locale/en/LC_MESSAGES/messages.mo b/fietsboek/locale/en/LC_MESSAGES/messages.mo index 271510e..980fbe0 100644 Binary files a/fietsboek/locale/en/LC_MESSAGES/messages.mo and b/fietsboek/locale/en/LC_MESSAGES/messages.mo differ diff --git a/fietsboek/locale/en/LC_MESSAGES/messages.po b/fietsboek/locale/en/LC_MESSAGES/messages.po index 5696108..2d62c14 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-11-20 22:19+0100\n" +"POT-Creation-Date: 2025-11-25 20:03+0100\n" "PO-Revision-Date: 2023-04-03 20:42+0200\n" "Last-Translator: \n" "Language: en\n" @@ -29,35 +29,35 @@ msgstr "" "\n" "If you did not create an account, ignore this email." -#: fietsboek/pdf.py:149 +#: fietsboek/pdf.py:233 msgid "pdf.table.date" msgstr "Date" -#: fietsboek/pdf.py:151 +#: fietsboek/pdf.py:235 msgid "pdf.table.length" msgstr "Length" -#: fietsboek/pdf.py:155 +#: fietsboek/pdf.py:239 msgid "pdf.table.uphill" msgstr "Uphill" -#: fietsboek/pdf.py:159 +#: fietsboek/pdf.py:243 msgid "pdf.table.downhill" msgstr "Downhill" -#: fietsboek/pdf.py:162 +#: fietsboek/pdf.py:246 msgid "pdf.table.moving_time" msgstr "Moving Time" -#: fietsboek/pdf.py:163 +#: fietsboek/pdf.py:247 msgid "pdf.table.stopped_time" msgstr "Stopped Time" -#: fietsboek/pdf.py:165 +#: fietsboek/pdf.py:249 msgid "pdf.table.max_speed" msgstr "Max Speed" -#: fietsboek/pdf.py:169 +#: fietsboek/pdf.py:253 msgid "pdf.table.avg_speed" msgstr "Average Speed" @@ -327,52 +327,52 @@ msgstr "This is a recording of a track" msgid "page.browse.synthetic_tooltip" msgstr "This is a pre-planned track" -#: fietsboek/templates/browse.jinja2:162 fietsboek/templates/details.jinja2:120 +#: fietsboek/templates/browse.jinja2:162 fietsboek/templates/details.jinja2:127 #: fietsboek/templates/profile_overview.jinja2:20 msgid "page.details.date" msgstr "Date" -#: fietsboek/templates/browse.jinja2:164 fietsboek/templates/details.jinja2:134 +#: fietsboek/templates/browse.jinja2:164 fietsboek/templates/details.jinja2:141 #: fietsboek/templates/profile_overview.jinja2:22 msgid "page.details.length" msgstr "Length" -#: fietsboek/templates/browse.jinja2:169 fietsboek/templates/details.jinja2:125 +#: fietsboek/templates/browse.jinja2:169 fietsboek/templates/details.jinja2:132 #: fietsboek/templates/profile_overview.jinja2:26 msgid "page.details.start_time" msgstr "Record Start" -#: fietsboek/templates/browse.jinja2:171 fietsboek/templates/details.jinja2:129 +#: fietsboek/templates/browse.jinja2:171 fietsboek/templates/details.jinja2:136 #: fietsboek/templates/profile_overview.jinja2:28 msgid "page.details.end_time" msgstr "Record End" -#: fietsboek/templates/browse.jinja2:176 fietsboek/templates/details.jinja2:138 +#: fietsboek/templates/browse.jinja2:176 fietsboek/templates/details.jinja2:145 #: fietsboek/templates/profile_overview.jinja2:32 msgid "page.details.uphill" msgstr "Uphill" -#: fietsboek/templates/browse.jinja2:178 fietsboek/templates/details.jinja2:142 +#: fietsboek/templates/browse.jinja2:178 fietsboek/templates/details.jinja2:149 #: fietsboek/templates/profile_overview.jinja2:34 msgid "page.details.downhill" msgstr "Downhill" -#: fietsboek/templates/browse.jinja2:183 fietsboek/templates/details.jinja2:147 +#: fietsboek/templates/browse.jinja2:183 fietsboek/templates/details.jinja2:154 #: fietsboek/templates/profile_overview.jinja2:38 msgid "page.details.moving_time" msgstr "Moving Time" -#: fietsboek/templates/browse.jinja2:185 fietsboek/templates/details.jinja2:151 +#: fietsboek/templates/browse.jinja2:185 fietsboek/templates/details.jinja2:158 #: fietsboek/templates/profile_overview.jinja2:40 msgid "page.details.stopped_time" msgstr "Stopped Time" -#: fietsboek/templates/browse.jinja2:189 fietsboek/templates/details.jinja2:155 +#: fietsboek/templates/browse.jinja2:189 fietsboek/templates/details.jinja2:162 #: fietsboek/templates/profile_overview.jinja2:44 msgid "page.details.max_speed" msgstr "Max Speed" -#: fietsboek/templates/browse.jinja2:191 fietsboek/templates/details.jinja2:159 +#: fietsboek/templates/browse.jinja2:191 fietsboek/templates/details.jinja2:166 #: fietsboek/templates/profile_overview.jinja2:46 msgid "page.details.avg_speed" msgstr "Average Speed" @@ -502,31 +502,35 @@ msgstr "Tagged as" msgid "page.noscript" msgstr "JavaScript is disabled, please enable JavaScript" -#: fietsboek/templates/details.jinja2:114 +#: fietsboek/templates/details.jinja2:115 msgid "page.details.download" msgstr "Download Tour" -#: fietsboek/templates/details.jinja2:204 +#: 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:208 +#: fietsboek/templates/details.jinja2:215 msgid "page.details.comments.author" msgstr "Comment by {}" -#: fietsboek/templates/details.jinja2:225 +#: fietsboek/templates/details.jinja2:232 msgid "page.details.comments.new.title" msgstr "Create a new comment" -#: fietsboek/templates/details.jinja2:228 +#: fietsboek/templates/details.jinja2:235 msgid "page.details.comments.new.input_title" msgstr "Title" -#: fietsboek/templates/details.jinja2:229 +#: fietsboek/templates/details.jinja2:236 msgid "page.details.comments.new.input_comment" msgstr "Comment" -#: fietsboek/templates/details.jinja2:232 +#: fietsboek/templates/details.jinja2:239 msgid "page.details.comments.new.submit" msgstr "Submit" diff --git a/fietsboek/locale/fietslog.pot b/fietsboek/locale/fietslog.pot index d42ffdc..6db79b7 100644 --- a/fietsboek/locale/fietslog.pot +++ b/fietsboek/locale/fietslog.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2025-11-20 22:19+0100\n" +"POT-Creation-Date: 2025-11-25 20:03+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -25,35 +25,35 @@ msgstr "" msgid "email.verify.text" msgstr "" -#: fietsboek/pdf.py:149 +#: fietsboek/pdf.py:233 msgid "pdf.table.date" msgstr "" -#: fietsboek/pdf.py:151 +#: fietsboek/pdf.py:235 msgid "pdf.table.length" msgstr "" -#: fietsboek/pdf.py:155 +#: fietsboek/pdf.py:239 msgid "pdf.table.uphill" msgstr "" -#: fietsboek/pdf.py:159 +#: fietsboek/pdf.py:243 msgid "pdf.table.downhill" msgstr "" -#: fietsboek/pdf.py:162 +#: fietsboek/pdf.py:246 msgid "pdf.table.moving_time" msgstr "" -#: fietsboek/pdf.py:163 +#: fietsboek/pdf.py:247 msgid "pdf.table.stopped_time" msgstr "" -#: fietsboek/pdf.py:165 +#: fietsboek/pdf.py:249 msgid "pdf.table.max_speed" msgstr "" -#: fietsboek/pdf.py:169 +#: fietsboek/pdf.py:253 msgid "pdf.table.avg_speed" msgstr "" @@ -321,52 +321,52 @@ msgstr "" msgid "page.browse.synthetic_tooltip" msgstr "" -#: fietsboek/templates/browse.jinja2:162 fietsboek/templates/details.jinja2:120 +#: fietsboek/templates/browse.jinja2:162 fietsboek/templates/details.jinja2:127 #: fietsboek/templates/profile_overview.jinja2:20 msgid "page.details.date" msgstr "" -#: fietsboek/templates/browse.jinja2:164 fietsboek/templates/details.jinja2:134 +#: fietsboek/templates/browse.jinja2:164 fietsboek/templates/details.jinja2:141 #: fietsboek/templates/profile_overview.jinja2:22 msgid "page.details.length" msgstr "" -#: fietsboek/templates/browse.jinja2:169 fietsboek/templates/details.jinja2:125 +#: fietsboek/templates/browse.jinja2:169 fietsboek/templates/details.jinja2:132 #: fietsboek/templates/profile_overview.jinja2:26 msgid "page.details.start_time" msgstr "" -#: fietsboek/templates/browse.jinja2:171 fietsboek/templates/details.jinja2:129 +#: fietsboek/templates/browse.jinja2:171 fietsboek/templates/details.jinja2:136 #: fietsboek/templates/profile_overview.jinja2:28 msgid "page.details.end_time" msgstr "" -#: fietsboek/templates/browse.jinja2:176 fietsboek/templates/details.jinja2:138 +#: fietsboek/templates/browse.jinja2:176 fietsboek/templates/details.jinja2:145 #: fietsboek/templates/profile_overview.jinja2:32 msgid "page.details.uphill" msgstr "" -#: fietsboek/templates/browse.jinja2:178 fietsboek/templates/details.jinja2:142 +#: fietsboek/templates/browse.jinja2:178 fietsboek/templates/details.jinja2:149 #: fietsboek/templates/profile_overview.jinja2:34 msgid "page.details.downhill" msgstr "" -#: fietsboek/templates/browse.jinja2:183 fietsboek/templates/details.jinja2:147 +#: fietsboek/templates/browse.jinja2:183 fietsboek/templates/details.jinja2:154 #: fietsboek/templates/profile_overview.jinja2:38 msgid "page.details.moving_time" msgstr "" -#: fietsboek/templates/browse.jinja2:185 fietsboek/templates/details.jinja2:151 +#: fietsboek/templates/browse.jinja2:185 fietsboek/templates/details.jinja2:158 #: fietsboek/templates/profile_overview.jinja2:40 msgid "page.details.stopped_time" msgstr "" -#: fietsboek/templates/browse.jinja2:189 fietsboek/templates/details.jinja2:155 +#: fietsboek/templates/browse.jinja2:189 fietsboek/templates/details.jinja2:162 #: fietsboek/templates/profile_overview.jinja2:44 msgid "page.details.max_speed" msgstr "" -#: fietsboek/templates/browse.jinja2:191 fietsboek/templates/details.jinja2:159 +#: fietsboek/templates/browse.jinja2:191 fietsboek/templates/details.jinja2:166 #: fietsboek/templates/profile_overview.jinja2:46 msgid "page.details.avg_speed" msgstr "" @@ -496,31 +496,35 @@ msgstr "" msgid "page.noscript" msgstr "" -#: fietsboek/templates/details.jinja2:114 +#: fietsboek/templates/details.jinja2:115 msgid "page.details.download" msgstr "" -#: fietsboek/templates/details.jinja2:204 +#: 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:208 +#: fietsboek/templates/details.jinja2:215 msgid "page.details.comments.author" msgstr "" -#: fietsboek/templates/details.jinja2:225 +#: fietsboek/templates/details.jinja2:232 msgid "page.details.comments.new.title" msgstr "" -#: fietsboek/templates/details.jinja2:228 +#: fietsboek/templates/details.jinja2:235 msgid "page.details.comments.new.input_title" msgstr "" -#: fietsboek/templates/details.jinja2:229 +#: fietsboek/templates/details.jinja2:236 msgid "page.details.comments.new.input_comment" msgstr "" -#: fietsboek/templates/details.jinja2:232 +#: fietsboek/templates/details.jinja2:239 msgid "page.details.comments.new.submit" msgstr "" diff --git a/fietsboek/templates/details.jinja2 b/fietsboek/templates/details.jinja2 index 631149c..f97f01f 100644 --- a/fietsboek/templates/details.jinja2 +++ b/fietsboek/templates/details.jinja2 @@ -111,7 +111,14 @@ -- cgit v1.2.3 From a91c8309c2f68a4053c402a4e3fbaef86086d257 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Tue, 25 Nov 2025 21:32:58 +0100 Subject: add unit tests for typst escaping --- tests/unit/test_pdf.py | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 tests/unit/test_pdf.py 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 -- cgit v1.2.3 From 5d751651a7e993de50f8fd1febadd02f58764d79 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Sat, 29 Nov 2025 22:32:10 +0100 Subject: adjust image size in pdf --- fietsboek/pdf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fietsboek/pdf.py b/fietsboek/pdf.py index 5ac237b..662798e 100644 --- a/fietsboek/pdf.py +++ b/fietsboek/pdf.py @@ -38,8 +38,8 @@ from .views.tileproxy import TileRequester LOGGER = logging.getLogger(__name__) TEMP_PREFIX = "fietsboek-typst-" -IMAGE_WIDTH = 900 -IMAGE_HEIGHT = 300 +IMAGE_WIDTH = 1500 +IMAGE_HEIGHT = 500 # See https://typst.app/docs/reference/syntax/ TO_ESCAPE = { "$", -- cgit v1.2.3 From fd2f22d73498caefc932d585cc2f826a43c48340 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Sat, 29 Nov 2025 23:30:19 +0100 Subject: fix TrackWithMetadata attribute access --- fietsboek/models/track.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fietsboek/models/track.py b/fietsboek/models/track.py index 3bf7eee..26de414 100644 --- a/fietsboek/models/track.py +++ b/fietsboek/models/track.py @@ -688,7 +688,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) @@ -700,7 +700,7 @@ class TrackWithMetadata: :return: Stopped time in seconds. """ if self.cache is None or self.cache.stopped_time is None: - value = self._meta().moving_duration + value = self._meta().stopped_duration else: value = self.cache.stopped_time return datetime.timedelta(seconds=value) -- cgit v1.2.3 From b78c3afa2944691822b90f58a3765eb6d634264b Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Sat, 29 Nov 2025 23:33:51 +0100 Subject: ignore matploblib debug logs in tests matplotlib procudes a lot of lines for the font manager, which really clutter the log output for everything else. --- tests/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/conftest.py b/tests/conftest.py index 652d443..d10a318 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -83,6 +83,7 @@ def _cleanup_data(app_settings): 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 -- cgit v1.2.3 From 0558b2394e17afeb224116bc232ba778f3eee59a Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Sat, 29 Nov 2025 23:40:49 +0100 Subject: add smoke test for pdf generation This doesn't check any content of the PDF, but ensures that running typst does not produce an error. --- tests/conftest.py | 2 +- tests/integration/test_pdf.py | 59 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 tests/integration/test_pdf.py diff --git a/tests/conftest.py b/tests/conftest.py index d10a318..b49dad2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -191,7 +191,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_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 -- cgit v1.2.3 From 019a3e9b7348a3a05e4e7d5e13d35e2362956d44 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Sat, 29 Nov 2025 23:41:21 +0100 Subject: speed up track adding in test_browse --- tests/integration/test_browse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_browse.py b/tests/integration/test_browse.py index 68ead8f..1b96e2e 100644 --- a/tests/integration/test_browse.py +++ b/tests/integration/test_browse.py @@ -38,9 +38,9 @@ def added_tracks(tm, dbsession, owner, data_manager): tagged_people=[], ) track.date = datetime(2022, 3, 14, 9, 26, 54) - track.set_path(convert.smart_convert(load_gpx_asset("MyTourbook_1.gpx.gz")).path()) dbsession.add(track) dbsession.flush() + track.fast_set_path(path) data_manager.initialize(track.id) tracks.append(track) track_ids.append(track.id) -- cgit v1.2.3