Compare commits

..

6 Commits

Author SHA1 Message Date
21a11211c3 edit tt display, tweak css 2025-08-25 23:57:12 +02:00
ddef1be220 generate website 2025-08-24 22:32:24 +02:00
10df613dd7 remove unused package 2025-08-23 22:28:38 +02:00
393cf58f9f adjust colors 2025-08-23 22:28:04 +02:00
de5acc223e update gitignore 2025-08-23 19:42:50 +02:00
8b5fd7e1fe generate pretty table from Timetable class, display with cv2 2025-08-23 19:42:43 +02:00
27 changed files with 1506 additions and 136 deletions

4
.gitignore vendored
View File

@@ -1,4 +1,5 @@
# ---> Python # ---> Python
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
@@ -174,3 +175,6 @@ cython_debug/
# PyPI configuration file # PyPI configuration file
.pypirc .pypirc
####
.vscode/
stations/stations.json

57
app.py Normal file
View File

@@ -0,0 +1,57 @@
from flask import Flask, render_template, url_for, jsonify, send_file, request
from datetime import datetime
from pathlib import Path
from src.iris_api import Timetable
def compile_javascript(base_path = "static"):
js_file_ptrs = list(Path(base_path).glob("*.js"))
js_files = []
print(js_file_ptrs)
for file in js_file_ptrs:
print(file.name)
fname = f"{file.name}"
js_files.append(url_for('static', filename=fname))
return js_files
eva = 8002377
# eva = 8004158
app = Flask(__name__)
@app.route("/favicon.ico")
def favicon():
return send_file("static/images/favicon.ico")
@app.route("/")
def main_page():
# print(compile_javascript())
tt = Timetable(eva)
tt.get_changes()
return render_template(
'base.html',
station=tt.station,
current_time_str=datetime.now().strftime(r"%H:%M:%S"),
js_files=compile_javascript()
)
@app.route("/update", methods=['POST'])
def update():
tt = Timetable(eva)
tt.get_stops()
table_data = tt.str_stops()
# print(table_data[0])
result = {'status': 'success'}
# result.update(data)
result["table_data"] = table_data
return jsonify(result)

BIN
fonts/Rubik-Black.ttf Normal file

Binary file not shown.

BIN
fonts/Rubik-BlackItalic.ttf Normal file

Binary file not shown.

BIN
fonts/Rubik-Bold.ttf Normal file

Binary file not shown.

BIN
fonts/Rubik-BoldItalic.ttf Normal file

Binary file not shown.

BIN
fonts/Rubik-ExtraBold.ttf Normal file

Binary file not shown.

Binary file not shown.

BIN
fonts/Rubik-Italic.ttf Normal file

Binary file not shown.

BIN
fonts/Rubik-Light.ttf Normal file

Binary file not shown.

BIN
fonts/Rubik-LightItalic.ttf Normal file

Binary file not shown.

BIN
fonts/Rubik-Medium.ttf Normal file

Binary file not shown.

Binary file not shown.

BIN
fonts/Rubik-Regular.ttf Normal file

Binary file not shown.

BIN
fonts/Rubik-SemiBold.ttf Normal file

Binary file not shown.

Binary file not shown.

171
main.py
View File

@@ -1,148 +1,47 @@
import requests from flask import Flask
from datetime import datetime, timedelta import fire
import xmltodict
from cachetools import cached, TTLCache
eva = 8002377 from htpy import body, h1, head, html, title, table, tr, td, div, script
base_url = "https://iris.noncd.db.de/iris-tts/timetable" app = Flask(__name__)
@cached(cache=TTLCache(maxsize=128, ttl=60)) def run_server(debug=False):
def _get_changes(eva): app.run(debug=debug)
url = f"{base_url}/fchg/{eva}"
x = requests.get(url)
changes = xmltodict.parse(x.text)['timetable']['s']
return changes
class Departure: @app.route("/")
def __init__(self, event, station=None, eva=None): def main_page():
dp = event['dp'] return build_page(None)
self.id = event['@id']
try:
self.line = f"{event['tl']['@c']}{dp['@l']}"
except KeyError:
self.line = '??'
self.time = datetime.strptime(dp['@pt'], "%y%m%d%H%M")
self.old_time = self.time
self.delayed = None
self.platform = dp['@pp']
path = dp['@ppth']
self.target = path.split('|')[-1]
self.path = path.split('|')
self.station = station
self.eva = eva
def build_page(tt, debug=True):
def fetch_actual(self, eva): _ = tt
changes = _get_changes(eva) from datetime import datetime
for event in changes:
if event['@id'] == self.id:
self.time = datetime.strptime(event['dp']['@ct'], "%y%m%d%H%M")
self.delayed = self.time - self.old_time
self.delayed = int(self.delayed.total_seconds()/60)
if self.delayed == 0:
self.delayed = None
pass
def __repr__(self): lines = (
("S1", "somewhere", datetime.now().strftime(r"%H:%M:%S"), 4, "3 min"),
("S2", "else", datetime.now().strftime(r"%H:%M:%S"), 4, "3 min"),
)
return f"{self.line} to {self.target}: {self.time.strftime("%Y-%m-%d %H:%M")}{f" ({self.delayed:+d})" if self.delayed is not None else ""} @ platform {self.platform}" retval = html[
head[title["today's menu"]],
body[
div(style="clear: both")[
h1(style="float: left")["Gröbenzell"],
h1(style="float: right", id="clock")[datetime.now().strftime(r"%H:%M:%S")],
],
table[
(tr[
(td[element] for element in line)
] for line in lines)
],
]
]
def main(): retval = str(retval)
# current_timestamp = datetime(year=2025, month=8, day=22, hour=22, minute=51, second=0) if debug:
retrieve_and_print_schedule(eva, number_future_deps=100) print(retval)
pass
def retrieve_and_print_schedule(eva, target_timestamp = None, future_only = True, number_future_deps=6):
current_timestamp = datetime.now() if target_timestamp is None else target_timestamp
timetable = fetch_timetable(current_timestamp, eva)
departures = extract_departures(timetable, eva)
if current_timestamp.minute < 10 and not future_only:
timetable = fetch_timetable(current_timestamp - timedelta(hours=1), eva)
departures = extract_departures(timetable, eva, departures)
# elif current_timestamp.minute > 50:
# # current_timestamp = timedelta(hours=1)
# timetable = fetch_timetable(current_timestamp + timedelta(hours=1), eva)
# departures = extract_departures(timetable, eva, departures)
if future_only:
split_index = -1
for di, dep in enumerate(departures):
if dep.time < current_timestamp:
split_index = di
departures = departures[split_index+1:]
cnt = 0
while len(departures) < number_future_deps and cnt <= 24:
current_timestamp += timedelta(hours=1)
timetable = fetch_timetable(current_timestamp, eva)
departures = extract_departures(timetable, eva, departures)
cnt += 1
# departures = departures[:number_future_deps]
print(f"{departures[0].station} ({departures[0].station})")
for abf in departures:
# if abf.time >= current_timestamp:
print(abf)
def fetch_timetable(target_datetime: datetime, eva):
"""
timetable:
@station: // station name
s: // stop, contains list of the following
@id // id
tl: // trip label
@f // distance class, F: Fern, N: Nah, S: Stadt
@t // trip type: e, p, z, s, h, n -> normally p
@o // owner: EVU-Number 800725 is S-Bahn München
@c // category: CE, IC, EC, IRE, RE, RB, S, MEX, TGJ, NJ, Bus
@n // train number
ar: // arrival
see dp
dp: // departure
@pt // yyMMddHHmm
@pp // platform number
@l // line. -> tl.@c + dp.@l make up the train line (eg S3)
@ppth // path, stations separated by |. for the last station in dp.@ppth is the destination, the first station in ar.@ppth is the origin
"""
daystamp = target_datetime.strftime(r"%y%m%d")
hourstamp = target_datetime.strftime(r"%H")
url = f"{base_url}/plan/{eva}/{daystamp}/{hourstamp}"
x = requests.get(url)
if x.status_code != 200:
return None
timetable = xmltodict.parse(x.text)['timetable']
return timetable
def extract_departures(timetable, eva, departures=None):
if departures is None:
departures = []
if timetable is None:
return departures
events = timetable['s']
if not isinstance(events, list):
events = [events]
for event in events:
if event['tl']['@f'] != 'S':
# only s-bahnen are supported for now
continue
departure = Departure(event, station=timetable['@station'], eva=eva)
departure.fetch_actual(eva)
departures.append(departure)
departures.sort(key=lambda abf: abf.time)
return departures
return retval
if __name__ == "__main__": if __name__ == "__main__":
main() fire.Fire(run_server)

View File

@@ -7,6 +7,13 @@ requires-python = ">=3.13"
dependencies = [ dependencies = [
"cachetools>=6.1.0", "cachetools>=6.1.0",
"datetime>=5.5", "datetime>=5.5",
"fire>=0.7.1",
"flask>=3.1.2",
"htpy>=25.8.1",
"opencv-contrib-python>=4.12.0.88",
"opencv-python>=4.12.0.88",
"pillow>=11.3.0",
"requests>=2.32.5", "requests>=2.32.5",
"rich>=14.1.0",
"xmltodict>=0.14.2", "xmltodict>=0.14.2",
] ]

238
src/image.py Normal file
View File

@@ -0,0 +1,238 @@
from dataclasses import dataclass, field
from iris_api import Timetable, Stop
import cv2
from PIL import ImageFont, ImageDraw, Image
import numpy as np
badge_colors = {
"S1": {"fill": "#18bae7", "text": "#C6D3D7"},
"S2": {"fill": "#74b72b", "text": "#C6D3D7"},
"S3": {"fill": "#941d81", "text": "#C6D3D7"},
"S4": {"fill": "#e20b1c", "text": "#C6D3D7"},
"S5": {"fill": "#005280", "text": "#C6D3D7"},
"S6": {"fill": "#008c59", "text": "#C6D3D7"},
"S7": {"fill": "#892f24", "text": "#C6D3D7"},
"S8": {"fill": "#0d0d11", "text": "#eeaa00"},
"S20": {"fill": "#e8526d", "text": "#C6D3D7"},
}
@dataclass
class ImageSetup:
width: int = 1920
height: int = 1080
bg_color: str = "#282a35"
fonts: dict = field(
default_factory=lambda: {
"title": ("./fonts/Rubik-Bold.ttf", 80),
"badge": ("./fonts/Rubik-SemiBold.ttf", 70),
"column_title": ("./fonts/Rubik-Regular.ttf", 50),
"default": ("./fonts/Rubik-Regular.ttf", 60),
}
)
text_colors: dict = field(
default_factory=lambda: {
"title": "#899194",
"column_title": "#555A5C",
"default": "#899194"
}
)
margins: tuple = (60, 60, 60, 60) # t, b, l, r
def denormalize(coords, setup: ImageSetup, use_margins=True):
(x, y) = coords
margins = setup.margins if use_margins else (0, 0, 0, 0)
return (
int(x * (setup.width - margins[2] - margins[3]) + margins[2]),
int(y * (setup.height - margins[0] - margins[1]) + margins[0]),
)
def create_image(tt: Timetable, setup: ImageSetup | None = None):
if setup is None:
setup = ImageSetup()
# img = np.zeros((setup.height, setup.width, 3), dtype=np.uint8)
img = Image.new("RGBA", (setup.width, setup.height), setup.bg_color)
# # fill with background color
# for i, c in enumerate(hex2color(setup.bg_color)):
# img[:, :, i] = int(c * 255)
### setup done ###
draw = ImageDraw.Draw(img)
title_font = ImageFont.truetype(*setup.fonts["title"])
ctitle_font = ImageFont.truetype(*setup.fonts["column_title"])
badge_font = ImageFont.truetype(*setup.fonts["badge"])
default_font = ImageFont.truetype(*setup.fonts["default"])
draw.text(
xy=denormalize((0, 0), setup),
text=tt.station,
font=title_font,
anchor="la",
fill=setup.text_colors["title"]
)
draw.text(
xy=denormalize((0.815, 0), setup),
text=tt.timestamp.strftime(r"%H:%M:%S"),
font=title_font,
anchor="la",
fill=setup.text_colors["title"]
)
line_badge_size = (2*setup.fonts["badge"][1], setup.fonts["badge"][1])
line_height = setup.fonts["badge"][1]*1.75
xy_start = denormalize((0,0.15), setup)
dest_x = denormalize((0.12, 0), setup)[0]
dep_x = denormalize((0.5, 0), setup)[0]
delay_x = denormalize((0.6, 0), setup)[0]
max_dest_len = dep_x - dest_x - 60
until_x = denormalize((0.5+0.5-0.12, 0), setup)[0]
# col_title_y = denormalize((0, 0.15), setup)[1]
# draw.text(
# xy=(dest_x, col_title_y),
# text="Richtung",
# font=ctitle_font,
# anchor="lm",
# fill=setup.text_colors["column_title"],
# )
# draw.text(
# xy=(dep_x, col_title_y),
# text="Zeit",
# font=ctitle_font,
# anchor="lm",
# fill=setup.text_colors["column_title"],
# )
# draw.text(
# xy=(until_x, col_title_y),
# text="fährt in",
# font=ctitle_font,
# anchor="lm",
# fill=setup.text_colors["column_title"],
# )
current_xys_line_badge = [*xy_start, xy_start[0]+line_badge_size[0], xy_start[1]+line_badge_size[1]]
for stop in tt.stops[:min(tt.min_stop_count, len(tt.stops), 7)]:
stop: Stop = stop
draw.rounded_rectangle(
current_xys_line_badge,
radius=min(current_xys_line_badge[2]-current_xys_line_badge[0], current_xys_line_badge[3]-current_xys_line_badge[1])//2,
fill=badge_colors[stop.line]["fill"],
outline=badge_colors[stop.line]["fill"],
width=10
)
center_xy = ((current_xys_line_badge[0]+current_xys_line_badge[2])//2, (current_xys_line_badge[1]+current_xys_line_badge[3])//2)
draw.text(
xy=center_xy,
text=stop.line,
font=badge_font,
anchor="mm",
fill=badge_colors[stop.line]["text"],
)
dest_text :str = stop.destination
dest_text = dest_text.replace("München-", "")
dest_text = dest_text.replace("München ", "")
paren_index = dest_text.find("(")
if paren_index != -1:
dest_text = dest_text[:paren_index]
cutoff = 0
while default_font.getlength(dest_text) > max_dest_len:
dest_text = (dest_text[:len(dest_text)-cutoff+1]+ ("." if cutoff > 0 else "")).replace(" .", "")
cutoff += 1
dep_text = stop.departure_time.strftime(r"%H:%M")
time_until = round((stop.departure_time - tt.timestamp).total_seconds()/60)
until_text = f"{time_until:d} min"
until_color = setup.text_colors["default"]
delay_color = setup.text_colors["column_title"]
dep_color = setup.text_colors["default"]
dest_color = setup.text_colors["default"]
delay_text = ""
if stop.departure_delay is not None:
delay_text = f"{"+" if stop.departure_delay >= 0 else "-"} {abs(stop.departure_delay):d}"
if stop.departure_delay > 5:
delay_color = "#c8683f"
elif stop.departure_delay < 0:
delay_color = "#48c144"
if time_until <= 6:
until_color = "#c8683f"
elif 6 < time_until <= 10:
until_color = "#b79b46"
if stop.departure_canceled:
dep_color = "#c83f4e"
dest_color = "#c83f4e"
dep_text = "Fällt aus :("
until_text = "----"
delay_text = ""
draw.text(
xy=(dest_x, center_xy[1]),
text=dest_text,
font=default_font,
anchor="lm",
fill=dest_color,
)
draw.text(
xy=(dep_x, center_xy[1]),
text=dep_text,
font=default_font,
anchor="lm",
fill=dep_color,
)
draw.text(
xy=(delay_x, center_xy[1]),
text=delay_text,
font=ctitle_font,
anchor="lm",
fill=delay_color,
)
draw.text(
xy=(until_x, center_xy[1]),
text=until_text,
font=default_font,
anchor="lm",
fill=until_color,
)
current_xys_line_badge[1] += line_height
current_xys_line_badge[3] += line_height
# if gif is not None:
# gif = Image.open(gif)
# gif.seek(gif_frame % gif.n_frames)
# frame = gif.convert("RGBA")
# frame_size = frame.size
# frame_anchor = denormalize((1, 1), setup, use_margins=False)
# img.paste(frame, (frame_anchor[0]-frame_size[0], frame_anchor[1]-frame_size[1]), frame)
# last step, convert from rgb to bgr
img = img.convert("RGB")
img = np.array(img)
img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
return img

303
src/iris_api.py Normal file
View File

@@ -0,0 +1,303 @@
import requests
from datetime import datetime, timedelta
import xmltodict
from cachetools import cached, TTLCache, LRUCache
from rich.console import Console
from rich.table import Table
import fire
class Stop:
line_colors = {
"S1": "[turquoise2]",
"S2": "[green3]",
"S3": "[dark_magenta]",
"S4": "[red1]",
"S5": "[deep_sky_blue4]",
"S6": "[green3]",
"S7": "[dark_red]",
"S8": "[gold1]",
"S20": "[light_coral]"
}
def __init__(self, event):
dp = event['dp']
self.id = event['@id']
try:
self.line = f"{event['tl']['@c']}{dp['@l']}"
except KeyError:
self.line = '??'
self.departure_time = datetime.strptime(dp['@pt'], "%y%m%d%H%M")
self.old_departure_time = self.departure_time
self.departure_delay = None
self.departure_canceled = False
self.departure_cancellation_time = None
self.platform = dp['@pp']
path = dp['@ppth']
self.path = path.split('|')
self.destination = self.path[-1]
self.origin = None
self.arrival_time = None
if 'ar' in event:
ar = event['ar']
self.arrival_time = datetime.strptime(ar['@pt'], "%y%m%d%H%M")
path = ar['@ppth']
add_path:list = path.split('|')[:-1]
add_path.extend(self.path)
self.path = add_path
self.origin = self.path[0]
self.old_arrival_time = self.arrival_time
self.arrival_delay = None
self.arrival_canceled = False
self.arrival_cancellation_time = None
def lookup_line_color(self):
if self.line in Stop.line_colors:
return Stop.line_colors[self.line]
else:
return ""
def to_dict(self):
return {
"id": self.id,
"line": self.line,
"departure_time": self.departure_time.timestamp(),
"old_departure_time": self.old_departure_time.timestamp(),
"departure_delay": 0 if self.departure_delay is None else self.departure_delay,
"departure_canceled": self.departure_canceled,
"departure_cancellation_time": -1 if self.departure_cancellation_time is None else self.departure_cancellation_time.timestamp(),
"platform": self.platform,
"path": self.path,
"destination": self.destination,
"origin": "" if self.origin is None else self.origin,
"arrival_time": -1 if self.arrival_time is None else self.arrival_time.timestamp(),
"old_arrival_time": -1 if self.old_arrival_time is None else self.old_arrival_time.timestamp(),
"arrival_delay": 0 if self.arrival_delay is None else self.arrival_delay,
"arrival_canceled": self.arrival_canceled,
"arrival_cancellation_time": -1 if self.arrival_cancellation_time is None else self.arrival_cancellation_time.timestamp(),
}
def __repr__(self):
if not self.departure_canceled:
return f"{self.line} to {self.destination}: {self.departure_time.strftime(r"%Y-%m-%d %H:%M")}{f" ({self.departure_delay:+d})" if self.departure_delay is not None else ""} @ platform {self.platform}"
else:
return f"{self.line} to {self.destination}: {self.departure_time.strftime(r"%Y-%m-%d %H:%M")} @ platform {self.platform} CANCELLED at {self.cancellation_time.strftime(r"%Y-%m-%d %H:%M")}"
class Timetable:
base_url = "https://iris.noncd.db.de/iris-tts/timetable"
def __init__(self, eva=None, number_departures=10):
self.eva = eva
self.station = None
self.get_changes()
self.stops = []
self.min_stop_count = number_departures
self.timestamp = None
self._timetable = None
pass
@cached(cache=TTLCache(maxsize=128, ttl=30))
def get_changes(self):
url = f"{Timetable.base_url}/fchg/{self.eva}"
x = requests.get(url)
changes = xmltodict.parse(x.text)
if self.station is None:
self.station = changes['timetable']['@station']
return changes['timetable']['s']
def str_stops(self):
stops = []
for stop in self.stops:
stop_dict = stop.to_dict()
stop_dict["until_departure"] = stop_dict["departure_time"] - self.timestamp.timestamp()
stop_dict["until_arrival"] = stop_dict["arrival_time"] - self.timestamp.timestamp()
stop_dict["timestamp"] = self.timestamp.timestamp()
stop_dict["station"] = self.station
stops.append(stop_dict)
return stops
def fetch_actual(self, stop: Stop):
changes = self.get_changes()
for event in changes:
if event['@id'] == stop.id:
try:
changed_status = event['ar']['@cs']
if changed_status == 'c':
stop.arrival_canceled = True
stop.arrival_cancellation_time = datetime.strptime(event['ar']['@clt'], r"%y%m%d%H%M")
except KeyError:
pass
try:
changed_status = event['dp']['@cs']
if changed_status == 'c':
stop.departure_canceled = True
stop.departure_cancellation_time = datetime.strptime(event['dp']['@clt'], r"%y%m%d%H%M")
except KeyError:
pass
if stop.arrival_canceled or stop.departure_canceled:
return
try:
stop.arrival_time = datetime.strptime(event['ar']['@ct'], r"%y%m%d%H%M")
stop.arrival_delay = stop.arrival_time - stop.old_arrival_time
stop.arrival_delay = int(stop.arrival_delay.total_seconds()/60)
if stop.arrival_delay == 0:
stop.arrival_delay = None
except KeyError:
pass
try:
stop.departure_time = datetime.strptime(event['dp']['@ct'], r"%y%m%d%H%M")
stop.departure_delay = stop.departure_time - stop.old_departure_time
stop.departure_delay = int(stop.departure_delay.total_seconds()/60)
if stop.departure_delay == 0:
stop.departure_delay = None
except KeyError:
pass
@staticmethod
# planned timetable doesn't change, so just cache a lot
@cached(LRUCache(maxsize=1024))
def _fetch_timetable_cached(daystamp, hourstamp, eva):
url = f"{Timetable.base_url}/plan/{eva}/{daystamp}/{hourstamp}"
x = requests.get(url)
if x.status_code != 200:
return None
timetable = xmltodict.parse(x.text)['timetable']
return timetable
def set_timestamp(self, ts: datetime|None):
if ts is None:
ts = datetime.now()
self.timestamp = ts
def fetch_timetable(self, ts: datetime|None = None):
if ts is None:
ts = self.timestamp
daystamp = ts.strftime(r"%y%m%d")
hourstamp = ts.strftime(r"%H")
self._timetable = self._fetch_timetable_cached(daystamp, hourstamp, self.eva)
def get_stops(self, ts=None):
self.set_timestamp(ts)
# get departures for current hour
self.fetch_timetable()
self.extract_stops()
# get departures for previous hour in case there is a long delay
self.fetch_timetable(self.timestamp - timedelta(hours=1))
self.extract_stops()
# filter out departures earlier than now
split_index = -1
for di, dep in enumerate(self.stops):
if dep.departure_time < self.timestamp:
split_index = di
self.stops = self.stops[split_index+1:]
delta = timedelta(hours=1)
while len(self.stops) < self.min_stop_count and delta.seconds < 86400:
self.fetch_timetable(self.timestamp + delta)
self.extract_stops()
delta += timedelta(hours=1)
def extract_stops(self):
if self._timetable is None:
return
events = self._timetable['s']
if not isinstance(events, list):
events = [events]
for event in events:
if event['tl']['@f'] != 'S':
# only s-bahnen are supported for now
continue
# check if there is a member 'dp' in event -> train does not end at the station
if 'dp' not in event:
continue
stop = Stop(event)
self.fetch_actual(stop)
# check if ID is already in list
# (inefficient, but there aren't that many trips available anyways as there aren't that many hours available)
add = True
for stp in self.stops:
if stp.id == stop.id:
add = False
if add:
self.stops.append(stop)
self.stops.sort(key=lambda stp: stp.departure_time)
def print_stops(self):
table = Table(title = f"{self.station} - {self.timestamp.strftime(r"%Y-%m-%d %H:%M:%S")}")
table.add_column("Line", justify="center")
# table.add_column("Origin", justify="left")
table.add_column("Destination", justify="left")
# table.add_column("Arrival", justify="center")
# table.add_column("Arrives in", justify="right")
# table.add_column("Arr. Delay", justify="center")
# table.add_column("Orig. Arr.", justify="center")
table.add_column("Departure", justify="center")
table.add_column("Departs in", justify="right")
table.add_column("Delay", justify="center")
# table.add_column("Orig. Dep.", justify="center")
# table.add_column("Pl", justify="left")
for stop in self.stops[:min(self.min_stop_count, len(self.stops))]:
stop: Stop = stop
line_tag = stop.lookup_line_color()
# arr_color_tag = "[yellow]" if stop.arrival_time is not None and int((stop.arrival_time - self.timestamp).total_seconds())/60 <= 10 else ""
# arr_timestr = (f"{stop.arrival_time.strftime(r"%H:%M")}" if not stop.arrival_canceled else "[red]cancelled") if stop.arrival_time is not None else ""
# arr_until = f"{int((stop.arrival_time - self.timestamp).total_seconds()/60)} min" if stop.arrival_time is not None else ""
dep_color_tag = ""
mins_until_departure = int((stop.departure_time - self.timestamp).total_seconds()/60)
if mins_until_departure <= 10:
dep_color_tag = "[yellow]"
if mins_until_departure <= 6:
dep_color_tag = "[red]"
dep_timestr = f"{stop.departure_time.strftime(r"%H:%M")}" if not stop.departure_canceled else "[red]cancelled"
dep_until = f"{int((stop.departure_time - self.timestamp).total_seconds()/60)} min"
table.add_row(
f"{line_tag}{stop.line}",
# f"{self.station if stop.origin is None else stop.origin}",
f"{stop.destination}",
# f"{arr_timestr}",
# f"{arr_color_tag}{arr_until}",
# f"{f"{stop.arrival_delay:+d}" if stop.arrival_delay is not None else ""}",
# f"{stop.old_arrival_time.strftime(r"%H:%M") if stop.arrival_delay is not None and stop.old_arrival_time is not None else ""}",
f"{dep_timestr}",
f"{dep_color_tag}{dep_until}",
f"{f"{stop.departure_delay:+d}" if stop.departure_delay is not None else ""}",
# f"{stop.old_departure_time.strftime(r"%H:%M") if stop.departure_delay is not None else ""}",
# f"{stop.platform}"
)
console = Console()
console.print(table)
def fetch_and_print_departures(eva):
tt = Timetable(eva=eva)
tt.get_stops()
tt.print_stops()
if __name__ == "__main__":
fire.Fire(fetch_and_print_departures)

BIN
static/images/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

205
static/styles.css Normal file
View File

@@ -0,0 +1,205 @@
/* .header-box {
display: flex;
justify-content: space-between; /* pushes items to opposite sides
align-items: top; /* vertically center them
width: 100%;
height: 12%;
font-size: 120%;
}
.header-box h1 {
margin: 0 2.5% 2.5% 2.5%;
} */
html, body {
/* margin: 0; */
padding: 0;
height: 100%;
overflow: hidden; /* no scrollbars */
font-size: 150%;
font-family: "Rubik", sans-serif;
font-optical-sizing: auto;
font-weight: 500;
font-style: normal;
background: #282a35;
color: #b5b5b3;
}
.header-table-container {
width: 95%;
margin: 0 2.5% 1% 2.5%;
height: 12%;
display: flex;
flex-direction: column;
justify-content: center;
}
.header-table {
width: 100%;
height: 100%;
border-collapse: collapse;
table-layout: fixed;
font-size: 120%;
}
.header-station {
width: 86.5%;
}
/* .header-clock {
width: 15%;
} */
.line {
width: 8%;
vertical-align: inherit;
}
/* .destination{
padding: 100em;
} */
.departure_time {
width: 12%;
text-align: left;
vertical-align: inherit;
}
.departure_delay {
width: 10%;
text-align: left;
font-size: 50%;
color: #646a6c;
vertical-align: inherit;
}
.until_departure {
width: 16%;
text-align: right;
vertical-align: inherit;
}
.departure-table-container {
width: 95%;
margin: 0 2.5% 2.5% 2.5%;
height: 83%;
display: flex;
flex-direction: column;
justify-content: center;
}
.departure-table {
width: 100%;
height: 100%;
border-collapse: collapse;
table-layout: fixed;
font-size: 200%;
vertical-align: center;
}
.departure-table th,
.departure-table td {
padding: 0 0.25em 0 0;
/* vertical-align: middle; */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
/* border: 1px solid #8de8fa; */
/* height: 12.5%; */
}
.departure-table td:last-child,
.departure-table th:last-child {
padding-right: 0;
}
.line-pill {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.2em 0.5em; /* relative padding */
font-weight: bold;
font-size: 100%;
border-radius: 9999px; /* pill shape adjusts automatically */
/* height: 50%; */
max-height: 50%;
/* width: 100%; */
max-width: 100%;
aspect-ratio: 2 / 1;
box-sizing: border-box;
white-space: nowrap; /* prevent text wrapping */
overflow: hidden;
text-overflow: ellipsis; /* show "..." if necessary */
}
/* Colors for different lines */
.line-S1 {
background-color: #14bae7;
/* color: #b5b5b5; */
}
.line-S2 {
background-color: #75b728;
/* color: #b5b5b5; */
}
.line-S3 {
background-color: #951781;
/* color: #b5b5b5; */
}
.line-S4 {
background-color: #e30b1b;
/* color: #b5b5b5; */
}
.line-S5 {
background-color: #00517f;
/* color: #b5b5b5; */
}
.line-S6 {
background-color: #008c58;
/* color: #b5b5b5; */
}
.line-S7 {
background-color: #882d22;
/* color: #b5b5b5; */
}
.line-S8 {
background-color: #000000;
color: #f0a901;
}
.line-S20 {
background-color: #ea516c;
/* color: #b5b5b5; */
}
.line-default {
background-color: #bdc3c7;
color: #1f2831;
}
.delay-high {
color: #dc4d21;
}
.delay-neg {
color: #5bc812
}
.until-medium {
color: #dc9e21
}
.until-low {
color: #dc4d21
}

177
static/update.js Normal file
View File

@@ -0,0 +1,177 @@
(function () {
function updateClock(clock) {
clock.innerHTML = new Date().toLocaleTimeString("de-DE", {
hour: "2-digit",
minute: "2-digit",
});
}
function ensureTableSize(table, x, y) {
// Adjust number of rows
while (table.rows.length < x) {
table.insertRow();
}
while (table.rows.length > x) {
table.deleteRow(table.rows.length - 1);
}
// Ensure each row has y cells
for (let row of table.rows) {
while (row.cells.length < y) {
row.insertCell();
}
while (row.cells.length > y) {
row.deleteCell(row.cells.length - 1);
}
}
}
function formatWithSign(num) {
if (num > 0) {
return `+ ${num}`;
} else if (num < 0) {
return `- ${Math.abs(num)}`;
} else {
return "";
}
}
function cssClassExists(className) {
return Array.from(document.styleSheets).some((sheet) => {
try {
return Array.from(sheet.cssRules).some(
(rule) => rule.selectorText === `.${className}`
);
} catch (e) {
// Ignore CORS-restricted stylesheets
return false;
}
});
}
function fill_table(data, table) {
// const res = JSON.parse(result)
const columns = {
line: 0,
destination: 1,
departure_time: 2,
departure_delay: 3,
until_departure: 4,
};
// console.log(data)
// ensureTableSize(table, Math.min(data.length, 8), 5);
for (let i = 0; i < Math.min(data.length, table.rows.length); i++) {
const row = table.rows[i];
console.log(row);
// row.classList.add("departure-table-row")
// row.style.height = `$10%`;
const train = Object.entries(data[i]);
for (let [key, value] of train) {
if (key in columns) {
const cell = row.cells[columns[key]];
let newContent = "";
let newClassList = [];
if (key in columns) {
newClassList.push(key);
}
if (key == "line") {
const lineClass = `line-${value}`;
const appliedClass = cssClassExists(lineClass)
? lineClass
: "line-default";
newContent = `<span class="line-pill ${appliedClass}">${value}</span>`;
}
if (key == "destination") {
let dest = String(value)
.replace(/\s*\(.*?\)\s*/g, "")
.replace(/^München\s*/i, "")
.replace(/^[\s-]+|[\s-]+$/g, "")
.replace("Ost", "Ostbahnhof")
.replace("Hbf", "Hauptbahnhof")
.replace("Gl.", "Gl. ");
newContent = dest;
}
if (key == "departure_time") {
if (train["canceled"]) {
newContent = "Fällt aus :(";
newClassList.push("train-canceled");
} else {
const date = new Date(value * 1000);
newContent = date.toLocaleTimeString("de-DE", {
hour: "2-digit",
minute: "2-digit",
});
}
}
if (key == "departure_delay") {
if (!train["canceled"]) {
newContent = formatWithSign(value);
if (value >= 5) newClassList.push("delay-high");
else if (value < 0) newClassList.push("delay-neg");
}
}
if (key == "until_departure") {
if (!train["canceled"]) {
const minutes = Math.round((value + 0) / 60);
newContent = String(minutes) + " min";
if (minutes <= 6) newClassList.push("until-low");
else if (minutes <= 10) newClassList.push("until-medium");
}
}
// Only update if content changed
if (
(cell.innerHTML !== newContent && key === "line") ||
(cell.textContent !== newContent && key !== "line")
) {
if (key === "line") {
cell.innerHTML = newContent;
} else {
cell.textContent = newContent;
}
}
// Update classes only if changed
cell.className = ""; // Clear all classes
for (const cls of newClassList) cell.classList.add(cls);
}
}
}
// document.querySelector('.departure-table')
// .style.setProperty('--row-count', table.rows.length);
}
function updateTable(table) {
fetch("/update", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
// TODO: pass number of rows we want
})
.then((response) => response.json())
.then((result) => {
// console.log(result);
fill_table(result.table_data, table);
// const res = JSON.parse(result);
// console.log(res.table_data);
// for (const elem of res.table_data) {
// console.log(elem);
// }
});
}
var clockElement = document.getElementById("clock");
var tableElement = document.getElementById("departures");
setInterval(function () {
updateClock(clockElement);
updateTable(tableElement);
}, 1000);
})();

45
stations/get_stations.py Normal file
View File

@@ -0,0 +1,45 @@
import requests
import xmltodict
import json
import os
import time
def update_station_data(jsonpath, max_age=86400):
if not update_file(jsonpath, max_age=max_age):
print(f"file {jsonpath} was modified less than {max_age}s ago, skipping")
return
url = "https://iris.noncd.db.de/iris-tts/timetable/station/*"
x = requests.get(url)
if x.status_code != 200:
raise ConnectionError(f"status code {x.status_code}")
xmlstr = x.text
stations_list = xmltodict.parse(xmlstr, )['stations']['station']
stations_dict = {}
for station in stations_list:
name = station.pop("@name")
local_station = {k.strip('@'): v for k,v in station.items()}
stations_dict[name] = local_station
with open(jsonpath, 'w') as f:
f.write(json.dumps(stations_dict, indent=4, ensure_ascii=False))
def update_file(fpath, max_age=86400):
if os.path.exists(fpath):
mtime = os.path.getmtime(fpath)
ctime = time.time()
if ctime - mtime < max_age:
return False
return True
def main():
update_station_data("./stations/stations2.json")
if __name__ == "__main__":
main()

83
templates/base.html Normal file
View File

@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html>
<head>
<title>Abfahrten {{ station }}</title>
<link
rel="stylesheet"
href="{{ url_for('static', filename='styles.css') }}"
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,300..900;1,300..900&display=swap"
rel="stylesheet"
/>
</head>
<body>
<div class="header-table-container">
<table id="header" class="header-table">
<tr>
<td class="header-station">
<h1>{{ station }}</h1>
</td>
<td class="header-clock">
<h1 id="clock" >{{ current_time_str }}</h1>
</td>
</tr>
</table>
</div>
<!-- <div class="header-box">
<h1>{{ station }}</h1>
<h1 id="clock", class="clock">{{ current_time_str }}</h1>
</div> -->
<div class="departure-table-container">
<table id="departures" class="departure-table">
<tr>
<td class="line"></td>
<td class="destination"></td>
<td class="departure_time"></td>
<td class="departure_delay"></td>
<td class="until_departure"></td>
</tr>
<tr>
<td class="line"></td>
<td class="destination"></td>
<td class="departure_time"></td>
<td class="departure_delay"></td>
<td class="until_departure"></td>
</tr>
<tr>
<td class="line"></td>
<td class="destination"></td>
<td class="departure_time"></td>
<td class="departure_delay"></td>
<td class="until_departure"></td>
</tr>
<tr>
<td class="line"></td>
<td class="destination"></td>
<td class="departure_time"></td>
<td class="departure_delay"></td>
<td class="until_departure"></td>
</tr>
<tr>
<td class="line"></td>
<td class="destination"></td>
<td class="departure_time"></td>
<td class="departure_delay"></td>
<td class="until_departure"></td>
</tr>
<tr>
<td class="line"></td>
<td class="destination"></td>
<td class="departure_time"></td>
<td class="departure_delay"></td>
<td class="until_departure"></td>
</tr>
</table>
</div>
</body>
{% for file in js_files %}
<script type="text/javascript" src="{{file}}"></script>
{% endfor %}
</html>

37
tt_to_image.py Normal file
View File

@@ -0,0 +1,37 @@
from iris_api import Timetable
from src.image import create_image
import fire
import cv2
import time
def main(eva=None):
eva = 8002377 if eva is None else eva
# eva = 8098263
# eva = 8004158
cv2.namedWindow("fs", cv2.WND_PROP_FULLSCREEN)
cv2.setWindowProperty("fs", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
# cv2.moveWindow("fs", -1920, 0)
tt = Timetable(eva=eva)
while True:
start = time.time()
tt.get_stops()
img = create_image(tt)
cv2.imshow("fs", img)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
elapsed = time.time() - start
time.sleep(max(0, 1.0 - elapsed))
cv2.destroyAllWindows()
if __name__ == "__main__":
fire.Fire(main)

315
uv.lock generated
View File

@@ -9,7 +9,14 @@ source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "cachetools" }, { name = "cachetools" },
{ name = "datetime" }, { name = "datetime" },
{ name = "fire" },
{ name = "flask" },
{ name = "htpy" },
{ name = "opencv-contrib-python" },
{ name = "opencv-python" },
{ name = "pillow" },
{ name = "requests" }, { name = "requests" },
{ name = "rich" },
{ name = "xmltodict" }, { name = "xmltodict" },
] ]
@@ -17,10 +24,26 @@ dependencies = [
requires-dist = [ requires-dist = [
{ name = "cachetools", specifier = ">=6.1.0" }, { name = "cachetools", specifier = ">=6.1.0" },
{ name = "datetime", specifier = ">=5.5" }, { name = "datetime", specifier = ">=5.5" },
{ name = "fire", specifier = ">=0.7.1" },
{ name = "flask", specifier = ">=3.1.2" },
{ name = "htpy", specifier = ">=25.8.1" },
{ name = "opencv-contrib-python", specifier = ">=4.12.0.88" },
{ name = "opencv-python", specifier = ">=4.12.0.88" },
{ name = "pillow", specifier = ">=11.3.0" },
{ name = "requests", specifier = ">=2.32.5" }, { name = "requests", specifier = ">=2.32.5" },
{ name = "rich", specifier = ">=14.1.0" },
{ name = "xmltodict", specifier = ">=0.14.2" }, { name = "xmltodict", specifier = ">=0.14.2" },
] ]
[[package]]
name = "blinker"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
]
[[package]] [[package]]
name = "cachetools" name = "cachetools"
version = "6.1.0" version = "6.1.0"
@@ -70,6 +93,27 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" },
] ]
[[package]]
name = "click"
version = "8.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]] [[package]]
name = "datetime" name = "datetime"
version = "5.5" version = "5.5"
@@ -83,6 +127,47 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f3/78/8e382b8cb4346119e2e04270b6eb4a01c5ee70b47a8a0244ecdb157204f7/DateTime-5.5-py3-none-any.whl", hash = "sha256:0abf6c51cb4ba7cee775ca46ccc727f3afdde463be28dbbe8803631fefd4a120", size = 52649, upload-time = "2024-03-21T07:26:47.849Z" }, { url = "https://files.pythonhosted.org/packages/f3/78/8e382b8cb4346119e2e04270b6eb4a01c5ee70b47a8a0244ecdb157204f7/DateTime-5.5-py3-none-any.whl", hash = "sha256:0abf6c51cb4ba7cee775ca46ccc727f3afdde463be28dbbe8803631fefd4a120", size = 52649, upload-time = "2024-03-21T07:26:47.849Z" },
] ]
[[package]]
name = "fire"
version = "0.7.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "termcolor" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c0/00/f8d10588d2019d6d6452653def1ee807353b21983db48550318424b5ff18/fire-0.7.1.tar.gz", hash = "sha256:3b208f05c736de98fb343310d090dcc4d8c78b2a89ea4f32b837c586270a9cbf", size = 88720, upload-time = "2025-08-16T20:20:24.175Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/4c/93d0f85318da65923e4b91c1c2ff03d8a458cbefebe3bc612a6693c7906d/fire-0.7.1-py3-none-any.whl", hash = "sha256:e43fd8a5033a9001e7e2973bab96070694b9f12f2e0ecf96d4683971b5ab1882", size = 115945, upload-time = "2025-08-16T20:20:22.87Z" },
]
[[package]]
name = "flask"
version = "3.1.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "blinker" },
{ name = "click" },
{ name = "itsdangerous" },
{ name = "jinja2" },
{ name = "markupsafe" },
{ name = "werkzeug" },
]
sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" },
]
[[package]]
name = "htpy"
version = "25.8.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/45/bc/58f4b835e1ffec9441aa92f97d3ce38148fff7e46507e05f2436098db6dc/htpy-25.8.1.tar.gz", hash = "sha256:ba4ac83307cbcbf5cb2b08111f75bb978693ede1624bac75da01208882b3c48d", size = 287611, upload-time = "2025-08-15T11:34:23.732Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f2/27/67b73c2399e1f9a27d4fa8ed6f0506302b927afc5e9c4d0e2c27ceeb0520/htpy-25.8.1-py3-none-any.whl", hash = "sha256:184dc98cb39e1157fcc66ce930e5dad222955b2d97c33612277a79d7b5d8ded3", size = 18945, upload-time = "2025-08-15T11:34:22.07Z" },
]
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.10" version = "3.10"
@@ -92,6 +177,202 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
] ]
[[package]]
name = "itsdangerous"
version = "2.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
]
[[package]]
name = "jinja2"
version = "3.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
]
[[package]]
name = "markdown-it-py"
version = "4.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mdurl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
]
[[package]]
name = "markupsafe"
version = "3.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" },
{ url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" },
{ url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" },
{ url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" },
{ url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" },
{ url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" },
{ url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" },
{ url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" },
{ url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" },
{ url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" },
{ url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" },
{ url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" },
{ url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" },
{ url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" },
{ url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" },
{ url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" },
{ url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" },
{ url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" },
{ url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" },
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
]
[[package]]
name = "mdurl"
version = "0.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
[[package]]
name = "numpy"
version = "2.2.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" },
{ url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" },
{ url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" },
{ url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" },
{ url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" },
{ url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" },
{ url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" },
{ url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" },
{ url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" },
{ url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" },
{ url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" },
{ url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" },
{ url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" },
{ url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" },
{ url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" },
{ url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" },
{ url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" },
{ url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" },
{ url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" },
{ url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" },
]
[[package]]
name = "opencv-contrib-python"
version = "4.12.0.88"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e0/b4/30fb53c33da02626b66dd40ad58dd4aa01eef834e422e098dfc056ed0873/opencv-contrib-python-4.12.0.88.tar.gz", hash = "sha256:0f1e22823aace09067b9a0e8e2b4ba6d7a1ef08807d6cebea315f3133f419a0e", size = 150785997, upload-time = "2025-07-07T09:20:16.755Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/c7/7cc80acd8a1ef9438542364b41751ecea2e95cf16b8ac4e48ebca643b203/opencv_contrib_python-4.12.0.88-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:decc3a2627e03e61ec398919a8647b7f78315bf285e64bcd7dd4501b653f22bc", size = 46854784, upload-time = "2025-07-07T09:16:09.739Z" },
{ url = "https://files.pythonhosted.org/packages/24/50/632534ad4a029aab4e1693664d9116223345af4499c72903e7711a1b44bb/opencv_contrib_python-4.12.0.88-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:e1209da6553b9c2b348fb5c878c2cd0e0121f9335981b7a9735c10d1ef1580ac", size = 67163714, upload-time = "2025-07-07T09:16:22.948Z" },
{ url = "https://files.pythonhosted.org/packages/aa/30/0829882b280f40544a83c69312da4d935ff3d6086c10351c7b1659107b5c/opencv_contrib_python-4.12.0.88-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7b3b834d5a2b84f26e5b62ff9ae07a287ab668d2343773372c3b97a335cd7c4f", size = 51290366, upload-time = "2025-07-07T09:16:33.078Z" },
{ url = "https://files.pythonhosted.org/packages/6a/67/905c2c9364dcd450a0997a489fd3976a10a83cd1ebcbd3d039bb2525b54c/opencv_contrib_python-4.12.0.88-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac2627dcdf5bd625706949ada7994524320b656a991315ff6ae70043fc983689", size = 73160002, upload-time = "2025-07-07T09:16:45.815Z" },
{ url = "https://files.pythonhosted.org/packages/5a/78/8110f1de88155241d9df6a701e2e589734a8bf3dbe1fd1a3cd29243c732a/opencv_contrib_python-4.12.0.88-cp37-abi3-win32.whl", hash = "sha256:fe3a5cbc1467c3620aeda3afd8d363886298a6fc85b48d62b2991afab65076c5", size = 36169774, upload-time = "2025-07-07T09:16:54.401Z" },
{ url = "https://files.pythonhosted.org/packages/7f/8c/ec631100261b0fca25cafd1e1a06592e50b3cda8aa08e7c4c14d7b4d7115/opencv_contrib_python-4.12.0.88-cp37-abi3-win_amd64.whl", hash = "sha256:62c20c14fdd794c9d0fbc780b3d52a74bc967d205664d25b3906951abedc9f65", size = 45262376, upload-time = "2025-07-07T09:17:02.868Z" },
]
[[package]]
name = "opencv-python"
version = "4.12.0.88"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ac/71/25c98e634b6bdeca4727c7f6d6927b056080668c5008ad3c8fc9e7f8f6ec/opencv-python-4.12.0.88.tar.gz", hash = "sha256:8b738389cede219405f6f3880b851efa3415ccd674752219377353f017d2994d", size = 95373294, upload-time = "2025-07-07T09:20:52.389Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/85/68/3da40142e7c21e9b1d4e7ddd6c58738feb013203e6e4b803d62cdd9eb96b/opencv_python-4.12.0.88-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:f9a1f08883257b95a5764bf517a32d75aec325319c8ed0f89739a57fae9e92a5", size = 37877727, upload-time = "2025-07-07T09:13:31.47Z" },
{ url = "https://files.pythonhosted.org/packages/33/7c/042abe49f58d6ee7e1028eefc3334d98ca69b030e3b567fe245a2b28ea6f/opencv_python-4.12.0.88-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:812eb116ad2b4de43ee116fcd8991c3a687f099ada0b04e68f64899c09448e81", size = 57326471, upload-time = "2025-07-07T09:13:41.26Z" },
{ url = "https://files.pythonhosted.org/packages/62/3a/440bd64736cf8116f01f3b7f9f2e111afb2e02beb2ccc08a6458114a6b5d/opencv_python-4.12.0.88-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:51fd981c7df6af3e8f70b1556696b05224c4e6b6777bdd2a46b3d4fb09de1a92", size = 45887139, upload-time = "2025-07-07T09:13:50.761Z" },
{ url = "https://files.pythonhosted.org/packages/68/1f/795e7f4aa2eacc59afa4fb61a2e35e510d06414dd5a802b51a012d691b37/opencv_python-4.12.0.88-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:092c16da4c5a163a818f120c22c5e4a2f96e0db4f24e659c701f1fe629a690f9", size = 67041680, upload-time = "2025-07-07T09:14:01.995Z" },
{ url = "https://files.pythonhosted.org/packages/02/96/213fea371d3cb2f1d537612a105792aa0a6659fb2665b22cad709a75bd94/opencv_python-4.12.0.88-cp37-abi3-win32.whl", hash = "sha256:ff554d3f725b39878ac6a2e1fa232ec509c36130927afc18a1719ebf4fbf4357", size = 30284131, upload-time = "2025-07-07T09:14:08.819Z" },
{ url = "https://files.pythonhosted.org/packages/fa/80/eb88edc2e2b11cd2dd2e56f1c80b5784d11d6e6b7f04a1145df64df40065/opencv_python-4.12.0.88-cp37-abi3-win_amd64.whl", hash = "sha256:d98edb20aa932fd8ebd276a72627dad9dc097695b3d435a4257557bbb49a79d2", size = 39000307, upload-time = "2025-07-07T09:14:16.641Z" },
]
[[package]]
name = "pillow"
version = "11.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" },
{ url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" },
{ url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" },
{ url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" },
{ url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" },
{ url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" },
{ url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" },
{ url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" },
{ url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" },
{ url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" },
{ url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" },
{ url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" },
{ url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" },
{ url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" },
{ url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" },
{ url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" },
{ url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" },
{ url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" },
{ url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" },
{ url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" },
{ url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" },
{ url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" },
{ url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" },
{ url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" },
{ url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" },
{ url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" },
{ url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" },
{ url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" },
{ url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" },
{ url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" },
{ url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" },
{ url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" },
{ url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" },
{ url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" },
{ url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" },
{ url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" },
{ url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" },
{ url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" },
{ url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" },
{ url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" },
{ url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" },
{ url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" },
{ url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" },
{ url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" },
{ url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" },
{ url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" },
{ url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" },
]
[[package]]
name = "pygments"
version = "2.19.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]] [[package]]
name = "pytz" name = "pytz"
version = "2025.2" version = "2025.2"
@@ -116,6 +397,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
] ]
[[package]]
name = "rich"
version = "14.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" },
]
[[package]] [[package]]
name = "setuptools" name = "setuptools"
version = "80.9.0" version = "80.9.0"
@@ -125,6 +419,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" },
] ]
[[package]]
name = "termcolor"
version = "3.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/6c/3d75c196ac07ac8749600b60b03f4f6094d54e132c4d94ebac6ee0e0add0/termcolor-3.1.0.tar.gz", hash = "sha256:6a6dd7fbee581909eeec6a756cff1d7f7c376063b14e4a298dc4980309e55970", size = 14324, upload-time = "2025-04-30T11:37:53.791Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/bd/de8d508070629b6d84a30d01d57e4a65c69aa7f5abe7560b8fad3b50ea59/termcolor-3.1.0-py3-none-any.whl", hash = "sha256:591dd26b5c2ce03b9e43f391264626557873ce1d379019786f99b0c2bee140aa", size = 7684, upload-time = "2025-04-30T11:37:52.382Z" },
]
[[package]] [[package]]
name = "urllib3" name = "urllib3"
version = "1.26.20" version = "1.26.20"
@@ -134,6 +437,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/33/cf/8435d5a7159e2a9c83a95896ed596f68cf798005fe107cc655b5c5c14704/urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", size = 144225, upload-time = "2024-08-29T15:43:08.921Z" }, { url = "https://files.pythonhosted.org/packages/33/cf/8435d5a7159e2a9c83a95896ed596f68cf798005fe107cc655b5c5c14704/urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", size = 144225, upload-time = "2024-08-29T15:43:08.921Z" },
] ]
[[package]]
name = "werkzeug"
version = "3.1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925, upload-time = "2024-11-08T15:52:18.093Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" },
]
[[package]] [[package]]
name = "xmltodict" name = "xmltodict"
version = "0.14.2" version = "0.14.2"