generate website
This commit is contained in:
58
app.py
Normal file
58
app.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from flask import Flask, render_template, url_for, jsonify, send_file
|
||||
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
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route("/favicon.ico")
|
||||
def favicon():
|
||||
return send_file("static/images/favicon.ico")
|
||||
|
||||
@app.route("/")
|
||||
def main_page():
|
||||
# print(compile_javascript())
|
||||
|
||||
return render_template(
|
||||
'base.html',
|
||||
title="testin",
|
||||
station="Gröbenzell",
|
||||
current_time_str=datetime.now().strftime(r"%H:%M:%S"),
|
||||
js_files=compile_javascript()
|
||||
)
|
||||
|
||||
# TODO: add a text entry field to change the EVA
|
||||
# @app.route("/setup")
|
||||
# def setup():
|
||||
# return ""
|
||||
|
||||
@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)
|
||||
296
main.py
296
main.py
@@ -1,271 +1,47 @@
|
||||
from dataclasses import dataclass, field
|
||||
from src.api import Timetable, Stop
|
||||
|
||||
from flask import Flask
|
||||
import fire
|
||||
|
||||
import cv2
|
||||
from PIL import ImageFont, ImageDraw, Image
|
||||
import numpy as np
|
||||
import time
|
||||
from htpy import body, h1, head, html, title, table, tr, td, div, script
|
||||
|
||||
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"},
|
||||
}
|
||||
app = Flask(__name__)
|
||||
|
||||
@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),
|
||||
}
|
||||
def run_server(debug=False):
|
||||
app.run(debug=debug)
|
||||
|
||||
@app.route("/")
|
||||
def main_page():
|
||||
return build_page(None)
|
||||
|
||||
def build_page(tt, debug=True):
|
||||
_ = tt
|
||||
from datetime import datetime
|
||||
|
||||
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"),
|
||||
)
|
||||
|
||||
text_colors: dict = field(
|
||||
default_factory=lambda: {
|
||||
"title": "#899194",
|
||||
"column_title": "#555A5C",
|
||||
"default": "#899194"
|
||||
}
|
||||
)
|
||||
margins: tuple = (60, 60, 60, 60) # t, b, l, r
|
||||
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)
|
||||
],
|
||||
]
|
||||
]
|
||||
|
||||
retval = str(retval)
|
||||
|
||||
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
|
||||
|
||||
|
||||
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 debug:
|
||||
print(retval)
|
||||
|
||||
return retval
|
||||
|
||||
if __name__ == "__main__":
|
||||
fire.Fire(main)
|
||||
fire.Fire(run_server)
|
||||
@@ -8,6 +8,8 @@ dependencies = [
|
||||
"cachetools>=6.1.0",
|
||||
"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",
|
||||
|
||||
238
src/image.py
Normal file
238
src/image.py
Normal 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
|
||||
@@ -62,6 +62,26 @@ class Stop:
|
||||
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}"
|
||||
@@ -90,6 +110,18 @@ class Timetable:
|
||||
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()
|
||||
stops.append(stop_dict)
|
||||
return stops
|
||||
|
||||
|
||||
def fetch_actual(self, stop: Stop):
|
||||
changes = self.get_changes()
|
||||
for event in changes:
|
||||
BIN
static/images/favicon.ico
Normal file
BIN
static/images/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
199
static/styles.css
Normal file
199
static/styles.css
Normal file
@@ -0,0 +1,199 @@
|
||||
/* .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: #899194;
|
||||
}
|
||||
|
||||
.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: 79%;
|
||||
}
|
||||
|
||||
/* .header-clock {
|
||||
width: 15%;
|
||||
} */
|
||||
|
||||
.line {
|
||||
width: 6%;
|
||||
/* text-align: left; */
|
||||
/* padding: 0 0 0 0; */
|
||||
vertical-align: 25%;
|
||||
}
|
||||
|
||||
.destination{
|
||||
padding: 100em;
|
||||
}
|
||||
|
||||
.departure_time {
|
||||
width: 12%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.departure_delay {
|
||||
width: 10%;
|
||||
text-align: left;
|
||||
font-size: 50%;
|
||||
color: #646a6c
|
||||
}
|
||||
|
||||
.until_departure {
|
||||
width: 16%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.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%;
|
||||
}
|
||||
|
||||
|
||||
.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: 0.75em;
|
||||
border-radius: 9999px; /* pill shape adjusts automatically */
|
||||
height: 100%; /* takes most of cell height */
|
||||
max-height: 100%; /* don’t overflow cell */
|
||||
width: 100%;
|
||||
max-width: 100%; /* stay within cell */
|
||||
aspect-ratio: 2 / 1;
|
||||
box-sizing: border-box;
|
||||
white-space: nowrap; /* prevent text wrapping */
|
||||
overflow: hidden; /* hide overflow if text is too long */
|
||||
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-low {
|
||||
color: #dc4d21
|
||||
}
|
||||
|
||||
.until-medium {
|
||||
color: #dc9e21
|
||||
}
|
||||
171
static/update_time.js
Normal file
171
static/update_time.js
Normal file
@@ -0,0 +1,171 @@
|
||||
(function () {
|
||||
|
||||
|
||||
function updateClock ( clock ) {
|
||||
clock.innerHTML = new Date().toLocaleTimeString();
|
||||
}
|
||||
|
||||
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 redraw_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, 8); i++) {
|
||||
const row = table.rows[i];
|
||||
// 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]]; // Get the cell once
|
||||
cell.classList.add(key); // Add class based on column name
|
||||
if (key == "line") {
|
||||
// cell.textContent = String(value);
|
||||
const lineClass = `line-${value}`;
|
||||
const appliedClass = cssClassExists(lineClass) ? lineClass : "line-default";
|
||||
cell.innerHTML = `<span class="line-pill ${appliedClass}">${value}</span>`;
|
||||
const pill = document.querySelector('.line-pill');
|
||||
|
||||
const cellHeight = cell.clientHeight;
|
||||
const cellWidth = cell.clientWidth;
|
||||
|
||||
// Set maximum sizes dynamically
|
||||
pill.style.maxWidth = `${cellHeight * 2}px`;
|
||||
pill.style.maxHeight = `${cellWidth * 0.5}px`;
|
||||
}
|
||||
if (key == "destination") {
|
||||
dest = String(value);
|
||||
dest = dest.replace(/\s*\(.*?\)\s*/g, "")
|
||||
.replace(/^München\s*/i, "")
|
||||
.replace(/^[\s-]+|[\s-]+$/g, "");
|
||||
cell.textContent = dest;
|
||||
}
|
||||
if (key == "departure_time") {
|
||||
if (train["canceled"]) {
|
||||
cell.textContent = "Fällt aus :(";
|
||||
cell.classList.add("train-canceled");
|
||||
} else {
|
||||
const date = new Date(value*1000);
|
||||
cell.textContent = date.toLocaleTimeString("de-DE",
|
||||
{
|
||||
hour: "2-digit",
|
||||
minute: "2-digit"
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
if (key == "departure_delay") {
|
||||
if (train["canceled"]) {
|
||||
cell.textContent = "";
|
||||
} else {
|
||||
cell.textContent = formatWithSign(value);
|
||||
if (value >= 5) {
|
||||
cell.classList.add("delay-high");
|
||||
} else if (value < 0) {
|
||||
cell.classList.add("delay-neg");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (key == "until_departure") {
|
||||
if (train["canceled"]) {
|
||||
cell.textContent = "";
|
||||
} else {
|
||||
const minutes = Math.round(value/60);
|
||||
cell.textContent = String(minutes) + " min";
|
||||
if (minutes <= 6) {
|
||||
cell.classList.add("until-low");
|
||||
} else if (minutes <= 10) {
|
||||
cell.classList.add("until-medium");
|
||||
}
|
||||
}
|
||||
}
|
||||
// cell.innerHTML.classList.add("scrolling-text")
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
redraw_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);
|
||||
|
||||
}());
|
||||
|
||||
40
templates/base.html
Normal file
40
templates/base.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<!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"></table>
|
||||
</div>
|
||||
</body>
|
||||
{% for file in js_files %}
|
||||
<script type="text/javascript" src="{{file}}"></script>
|
||||
{% endfor %}
|
||||
</html>
|
||||
37
tt_to_image.py
Normal file
37
tt_to_image.py
Normal 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)
|
||||
124
uv.lock
generated
124
uv.lock
generated
@@ -10,6 +10,8 @@ dependencies = [
|
||||
{ name = "cachetools" },
|
||||
{ name = "datetime" },
|
||||
{ name = "fire" },
|
||||
{ name = "flask" },
|
||||
{ name = "htpy" },
|
||||
{ name = "opencv-contrib-python" },
|
||||
{ name = "opencv-python" },
|
||||
{ name = "pillow" },
|
||||
@@ -23,6 +25,8 @@ requires-dist = [
|
||||
{ name = "cachetools", specifier = ">=6.1.0" },
|
||||
{ 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" },
|
||||
@@ -31,6 +35,15 @@ requires-dist = [
|
||||
{ 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]]
|
||||
name = "cachetools"
|
||||
version = "6.1.0"
|
||||
@@ -80,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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "datetime"
|
||||
version = "5.5"
|
||||
@@ -105,6 +139,35 @@ 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]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
@@ -114,6 +177,27 @@ 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" },
|
||||
]
|
||||
|
||||
[[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"
|
||||
@@ -126,6 +210,34 @@ 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"
|
||||
@@ -325,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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "xmltodict"
|
||||
version = "0.14.2"
|
||||
|
||||
Reference in New Issue
Block a user