Initial Commit. I'm afraid of testing it.

This commit is contained in:
2025-05-01 15:48:11 +02:00
parent e4dae9ec35
commit 51bc491cc8
10 changed files with 1464 additions and 0 deletions
+4
View File
@@ -172,3 +172,7 @@ cython_debug/
# PyPI configuration file
.pypirc
# csv containing personal information
test-config.csv
+19
View File
@@ -0,0 +1,19 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.2 # Replace with the latest version
hooks:
- id: ruff
name: ruff check
args: ["check"]
- id: ruff
name: ruff format
args: ["format"]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.9.0 # Replace with the latest version
hooks:
- id: mypy
args:
- "--allow-redefinition"
- "import-untyped"
- "src/"
+1
View File
@@ -0,0 +1 @@
3.13
+25
View File
@@ -0,0 +1,25 @@
[project]
name = "tatami"
version = "0.1.0"
description = "Tool for Arranging Tasty Appointments, Meetings & Invitations"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"numpy>=2.2.4",
"pandas>=2.2.3",
"pandas-stubs>=2.2.3.250308",
"requests>=2.32.3",
"tqdm>=4.67.1",
"types-requests>=2.32.0.20250328",
"types-tqdm>=4.67.0.20250404",
]
[dependency-groups]
dev = [
"mypy>=1.15.0",
"ruff>=0.11.5",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
+659
View File
@@ -0,0 +1,659 @@
import copy
import datetime
import random
import os
import pandas
import requests
import xlsxwriter
from progress.bar import IncrementalBar
EXCEL_DIR = "C:/Daten/RunningDinner/running_dinner/running_dinner.xlsx"
# Set the base url for the Google Maps Distance Matrix API
base_url = "https://maps.googleapis.com/maps/api/distancematrix/json?"
# Google Maps API Key
api_key = os.environ["MAPS_API_KEY"]
# Excel Sheet muss 7 Spalten haben:
# Zeitstempel (Erzeugt von Google Forms, wird in Zeile 30 entfernt) | Name | | Zimmergröße | Handynummer | Adresse | Entfernung zu Afterparty
# Spalte mit Namen muss 'Namen' als Header haben
# Maximale Distanz von Nachspeise zu Afterparty
max_dist_to_ap = 2
rd = pandas.read_excel(EXCEL_DIR)
appendants = rd.set_index("Name").T.to_dict("list")
addresses = []
address_indices = []
for i, app in enumerate(appendants):
appendants[app].pop(0)
if "passen" in appendants[app][1]: # Zimmergröße sehr gut
appendants[app][1] = 2
elif "gehen" in appendants[app][1]: # Zimmergröße gut
appendants[app][1] = 1
else: # Zimmergröße passt nicht
appendants[app][1] = 0
if type(appendants[app][3]) == str:
adr = appendants[app][3]
addresses.append(adr)
address_indices.append(i)
names = list(appendants.keys())
# print(appendants['Fabio Rodrigues'])
ind = 0
distances = []
for _ in range(len(names)):
distances.append([float("inf")] * len(names))
for i, adr in enumerate(addresses):
addresses_wo_adr = addresses.copy()
addresses_wo_adr.pop(i)
address_indices_wo_adr = address_indices.copy()
address_indices_wo_adr.pop(i)
# Build the request parameters
params = {
"origins": [adr],
"destinations": "|".join(addresses_wo_adr),
"key": api_key,
"mode": "bicycling",
}
# Make the request to the Google Maps Distance Matrix API
response = requests.get(base_url, params=params)
# Extract the distance matrix from the response
distance_matrix = response.json()["rows"][0]["elements"]
distances[address_indices[i]][address_indices[i]] = 0.0
for j in range(len(distance_matrix)):
distances[address_indices[i]][address_indices_wo_adr[j]] = distance_matrix[j][
"duration"
]["value"]
top1_max_distance = float("inf")
top1_total_distance = float("inf")
top1_teams = {}
top1_loc = {}
top2_max_distance = float("inf")
top2_total_distance = float("inf")
top2_teams = {}
top2_loc = {}
top3_max_distance = float("inf")
top3_total_distance = float("inf")
top3_teams = {}
top3_loc = {}
top4_max_distance = float("inf")
top4_total_distance = float("inf")
top4_teams = {}
top4_loc = {}
top5_max_distance = float("inf")
top5_total_distance = float("inf")
top5_teams = {}
top5_loc = {}
iterations = 1000
bar = IncrementalBar("Searching optimal Route", max=iterations)
for _ in range(iterations):
searching = True
while searching == True:
random.shuffle(names)
teams = dict()
for i in range(len(names)):
if i % 2 == 0 and i < len(names) - (len(names) % 6):
teams["Team " + str(int(i / 2 + 1))] = {
"Namen": [names[i], names[i + 1]]
}
if i >= len(names) - (len(names) % 6):
teams["Team " + str(i - (len(names) - (len(names) % 6)) + 1)][
"Namen"
].append(names[i])
for t in teams:
teams[t]["Location"] = []
for n in teams[t]["Namen"]:
if type(appendants[n][3]) == str:
teams[t]["Location"].append(appendants[n][3])
num_teams = len(teams)
t = list(range(1, num_teams + 1))
num_groups = int(num_teams / 3)
Hauptspeisen = t[0:num_groups]
Vorspeisen = t[num_groups : num_groups * 2]
Nachspeisen = t[num_groups * 2 : num_groups * 3]
for i in Vorspeisen + Hauptspeisen + Nachspeisen:
if i in Vorspeisen:
index = Vorspeisen.index(i)
teams["Team " + str(i)]["Gang"] = "Vorspeise"
teams["Team " + str(i)]["Gäste"] = [
"Team " + str(Hauptspeisen[index]),
"Team " + str(Nachspeisen[index]),
]
teams["Team " + str(i)]["Route"] = ["Team " + str(i)]
teams["Team " + str(Hauptspeisen[index])]["Route"] = ["Team " + str(i)]
teams["Team " + str(Nachspeisen[index])]["Route"] = ["Team " + str(i)]
teams["Team " + str(i)]["Unverträglichkeiten der Gäste"] = []
for g in teams["Team " + str(i)]["Gäste"]:
for n in teams[g]["Namen"]:
teams["Team " + str(i)]["Unverträglichkeiten der Gäste"].append(
appendants[n][0]
)
elif i in Hauptspeisen:
index = Hauptspeisen.index(i)
teams["Team " + str(i)]["Gang"] = "Hauptspeise"
teams["Team " + str(i)]["Gäste"] = [
"Team " + str(Vorspeisen[(index - 1) % num_groups]),
"Team " + str(Nachspeisen[(index + 1) % num_groups]),
]
teams["Team " + str(i)]["Route"].append("Team " + str(i))
teams["Team " + str(Vorspeisen[(index - 1) % num_groups])][
"Route"
].append("Team " + str(i))
teams["Team " + str(Nachspeisen[(index + 1) % num_groups])][
"Route"
].append("Team " + str(i))
teams["Team " + str(i)]["Unverträglichkeiten der Gäste"] = []
for g in teams["Team " + str(i)]["Gäste"]:
for n in teams[g]["Namen"]:
teams["Team " + str(i)]["Unverträglichkeiten der Gäste"].append(
appendants[n][0]
)
elif i in Nachspeisen:
index = Nachspeisen.index(i)
teams["Team " + str(i)]["Gang"] = "Nachspeise"
teams["Team " + str(i)]["Gäste"] = [
"Team " + str(Vorspeisen[(index - 1) % num_groups]),
"Team " + str(Hauptspeisen[(index + 1) % num_groups]),
]
teams["Team " + str(i)]["Route"].append("Team " + str(i))
teams["Team " + str(Vorspeisen[(index - 1) % num_groups])][
"Route"
].append("Team " + str(i))
teams["Team " + str(Hauptspeisen[(index + 1) % num_groups])][
"Route"
].append("Team " + str(i))
teams["Team " + str(i)]["Unverträglichkeiten der Gäste"] = []
for g in teams["Team " + str(i)]["Gäste"]:
for n in teams[g]["Namen"]:
teams["Team " + str(i)]["Unverträglichkeiten der Gäste"].append(
appendants[n][0]
)
searching = False
for t in teams:
size = 0
for n in teams[t]["Namen"]:
size = size + appendants[n][1]
if size == 0:
# print(str(teams[t]['Namen']) + ' Raum zu klein')
searching = True
break
max_room = 0
for n in teams[t]["Namen"]:
max_room = max(max_room, appendants[n][1])
dist = 6
for n in teams[t]["Namen"]:
if appendants[n][1] == max_room:
dist = min(dist, int(appendants[n][4]))
if dist > max_dist_to_ap and teams[t]["Gang"] == "Nachspeise":
# print(str(teams[t]['Namen']) + ' Nachspeise, aber zu weit von Afterparty entfernt')
searching = True
break
# if 'Fabio Rodrigues' in teams[t]['Namen'] and len(teams[t]['Namen']) == 3:
# print('Fabio in 3er Gruppe')
# searching = True
# break
# if 'Fabio Rodrigues' in teams[t]['Namen'] and teams[t]['Gang'] != 'Nachspeise':
# print('Fabio macht nicht Nachspeise')
# searching = True
# break
# if 'Fabio Rodrigues' in teams[t]['Namen'] and 'Alex Peeters' not in teams[t]['Namen']:
# print('Fabio nicht mit Alex')
# searching = True
# break
possible_location_permutations = 1
number_locations = []
for t in teams:
n_loc = len(teams[t]["Location"])
number_locations.append(n_loc)
possible_location_permutations *= n_loc
perm = [0] * len(number_locations)
for j in range(possible_location_permutations):
if j > 0:
i = 0
adding = True
while adding:
if perm[i] + 1 < number_locations[i]:
perm[i] += 1
adding = False
elif perm[i] + 1 == number_locations[i] and perm[i] > 0:
perm[i] = 0
i += 1
loc = {}
for k, l in enumerate(teams):
loc[l] = perm[k]
max_distance = 0.0
total_distance = 0.0
for t in teams:
way1 = distances[
address_indices[
addresses.index(
teams[teams[t]["Route"][0]]["Location"][
loc[teams[t]["Route"][0]]
]
)
]
][
address_indices[
addresses.index(
teams[teams[t]["Route"][1]]["Location"][
loc[teams[t]["Route"][1]]
]
)
]
]
way2 = distances[
address_indices[
addresses.index(
teams[teams[t]["Route"][1]]["Location"][
loc[teams[t]["Route"][1]]
]
)
]
][
address_indices[
addresses.index(
teams[teams[t]["Route"][2]]["Location"][
loc[teams[t]["Route"][2]]
]
)
]
]
way3 = distances[
address_indices[
addresses.index(
teams[teams[t]["Route"][2]]["Location"][
loc[teams[t]["Route"][2]]
]
)
]
][
address_indices[
addresses.index("Sebastian-Kneipp-Straße 6, 76131 Karlsruhe")
]
]
team_distance = way1 + way2 + way3
total_distance += team_distance
max_distance = max(max_distance, way1, way2)
teams[t]["Zeit"] = str(datetime.timedelta(seconds=team_distance))
if total_distance < top1_total_distance:
top5_max_distance = top4_max_distance
top5_total_distance = top4_total_distance
top5_teams = copy.deepcopy(top4_teams)
top5_loc = copy.deepcopy(top4_loc)
top4_max_distance = top3_max_distance
top4_total_distance = top3_total_distance
top4_teams = copy.deepcopy(top3_teams)
top4_loc = copy.deepcopy(top3_loc)
top3_max_distance = top2_max_distance
top3_total_distance = top2_total_distance
top3_teams = copy.deepcopy(top2_teams)
top3_loc = copy.deepcopy(top2_loc)
top2_max_distance = top1_max_distance
top2_total_distance = top1_total_distance
top2_teams = copy.deepcopy(top1_teams)
top2_loc = copy.deepcopy(top1_loc)
top1_max_distance = max_distance
top1_total_distance = total_distance
top1_teams = copy.deepcopy(teams)
top1_loc = copy.deepcopy(loc)
elif total_distance < top2_total_distance:
top5_max_distance = top4_max_distance
top5_total_distance = top4_total_distance
top5_teams = copy.deepcopy(top4_teams)
top5_loc = copy.deepcopy(top4_loc)
top4_max_distance = top3_max_distance
top4_total_distance = top3_total_distance
top4_teams = copy.deepcopy(top3_teams)
top4_loc = copy.deepcopy(top3_loc)
top3_max_distance = top2_max_distance
top3_total_distance = top2_total_distance
top3_teams = copy.deepcopy(top2_teams)
top3_loc = copy.deepcopy(top2_loc)
top2_max_distance = max_distance
top2_total_distance = total_distance
top2_teams = copy.deepcopy(teams)
top2_loc = copy.deepcopy(loc)
elif total_distance < top3_total_distance:
top5_max_distance = top4_max_distance
top5_total_distance = top4_total_distance
top5_teams = copy.deepcopy(top4_teams)
top5_loc = copy.deepcopy(top4_loc)
top4_max_distance = top3_max_distance
top4_total_distance = top3_total_distance
top4_teams = copy.deepcopy(top3_teams)
top4_loc = copy.deepcopy(top3_loc)
top3_max_distance = max_distance
top3_total_distance = total_distance
top3_teams = copy.deepcopy(teams)
top3_loc = copy.deepcopy(loc)
elif total_distance < top4_total_distance:
top5_max_distance = top4_max_distance
top5_total_distance = top4_total_distance
top5_teams = copy.deepcopy(top4_teams)
top5_loc = copy.deepcopy(top4_loc)
top4_max_distance = max_distance
top4_total_distance = total_distance
top4_teams = copy.deepcopy(teams)
top4_loc = copy.deepcopy(loc)
elif total_distance < top5_total_distance:
top5_max_distance = max_distance
top5_total_distance = total_distance
top5_teams = copy.deepcopy(teams)
top5_loc = copy.deepcopy(loc)
bar.next()
bar.finish()
for t in top5_teams:
top5_teams[t]["Location"] = top5_teams[t]["Location"][top5_loc[t]]
for t in top4_teams:
top4_teams[t]["Location"] = top4_teams[t]["Location"][top4_loc[t]]
for t in top3_teams:
top3_teams[t]["Location"] = top3_teams[t]["Location"][top3_loc[t]]
for t in top2_teams:
top2_teams[t]["Location"] = top2_teams[t]["Location"][top2_loc[t]]
for t in top1_teams:
top1_teams[t]["Location"] = top1_teams[t]["Location"][top1_loc[t]]
print("######################################################")
print("Top 5 Team:")
print(top3_teams)
print(
"The max time of one single route is: {}".format(
str(datetime.timedelta(seconds=top5_max_distance))
)
)
print(
"The total time spent on bike is: {}".format(
str(datetime.timedelta(seconds=top5_total_distance))
)
)
print("Top 4 Team:")
print(top3_teams)
print(
"The max time of one single route is: {}".format(
str(datetime.timedelta(seconds=top4_max_distance))
)
)
print(
"The total time spent on bike is: {}".format(
str(datetime.timedelta(seconds=top4_total_distance))
)
)
print("Top 3 Team:")
print(top3_teams)
print(
"The max time of one single route is: {}".format(
str(datetime.timedelta(seconds=top3_max_distance))
)
)
print(
"The total time spent on bike is: {}".format(
str(datetime.timedelta(seconds=top3_total_distance))
)
)
print("Top 2 Team:")
print(top2_teams)
print(
"The max time of one single route is: {}".format(
str(datetime.timedelta(seconds=top2_max_distance))
)
)
print(
"The total time spent on bike is: {}".format(
str(datetime.timedelta(seconds=top2_total_distance))
)
)
print("Top 1 Team:")
print(top1_teams)
print(
"The max time of one single route is: {}".format(
str(datetime.timedelta(seconds=top1_max_distance))
)
)
print(
"The total time spent on bike is: {}".format(
str(datetime.timedelta(seconds=top1_total_distance))
)
)
print("Type 1, 2, 3, 4 or 5 to choose the teams configuration:")
inp = int(input())
if inp == 1:
print("You chose the 1st option.")
teams = copy.deepcopy(top1_teams)
elif inp == 2:
print("You chose the 2nd option.")
teams = copy.deepcopy(top2_teams)
elif inp == 3:
print("You chose the 3rd option.")
teams = copy.deepcopy(top3_teams)
elif inp == 4:
print("You chose the 4th option.")
teams = copy.deepcopy(top4_teams)
elif inp == 5:
print("You chose the 5th option.")
teams = copy.deepcopy(top5_teams)
else:
print("Input could not be resolved, choosing number 1.")
teams = copy.deepcopy(top1_teams)
for t in teams:
if "Fabio Rodrigues" in teams[t]["Namen"]:
print(t + ": " + str(teams[t]["Namen"]) + " machen " + str(teams[t]["Gang"]))
if "Arnold Resch" in teams[t]["Namen"]:
print(t + ": " + str(teams[t]["Namen"]) + " machen " + str(teams[t]["Gang"]))
print("######################################################")
# Creating xlsx file
workbook = xlsxwriter.Workbook("running_dinner_masterplan.xlsx")
bold = workbook.add_format({"bold": True})
worksheet = workbook.add_worksheet("Übersicht")
worksheet.set_column(0, 0, 20)
worksheet.set_column(1, 1, 30)
worksheet.set_column(2, 2, 20)
worksheet.set_column(3, 3, 40)
worksheet.set_column(4, 4, 80)
row = 0
col = 0
header = workbook.add_format(
{
"bold": 1,
"border": 1,
"align": "center",
"valign": "vcenter",
"fg_color": "yellow",
}
)
worksheet.merge_range("A1:E1", "Der große Running-Dinner Masterplan", header)
row += 2
worksheet.write(row, col, "Teams", bold)
worksheet.write(row, col + 1, "Namen", bold)
worksheet.write(row, col + 2, "Telefonnummer", bold)
worksheet.write(row, col + 3, "Adresse", bold)
worksheet.write(row, col + 4, "Unverträglichkeiten", bold)
row += 1
for t in teams:
worksheet.write(row, col, t, bold)
worksheet.write(row + 1, col, teams[t]["Gang"])
for name in teams[t]["Namen"]:
worksheet.write(row, col + 1, name)
worksheet.write(row, col + 2, appendants[name][2])
if type(appendants[name][3]) == str:
worksheet.write(row, col + 3, appendants[name][3])
worksheet.write(row, col + 4, appendants[name][0])
row += 1
row += 3
worksheet.merge_range("A" + str(row) + ":E" + str(row), "Die Routen", header)
row += 1
worksheet.write(row, col, "18:30 Uhr", bold)
worksheet.write(row + 6, col, "20:00 Uhr", bold)
worksheet.write(row + 12, col, "22:00 Uhr", bold)
row += 1
vor_g = 1
haupt_g = 1
nach_g = 1
for t in teams:
if teams[t]["Gang"] == "Vorspeise":
worksheet.write(row, col + vor_g - 1, "Vorspeise Gruppe " + str(vor_g), bold)
worksheet.write(row + 1, col + vor_g - 1, t, bold)
for i, g in enumerate(teams[t]["Gäste"]):
worksheet.write(row + 2 + i, col + vor_g - 1, g)
vor_g += 1
elif teams[t]["Gang"] == "Hauptspeise":
worksheet.write(
row + 6, col + haupt_g - 1, "Hauptspeise Gruppe " + str(haupt_g), bold
)
worksheet.write(row + 7, col + haupt_g - 1, t, bold)
for i, g in enumerate(teams[t]["Gäste"]):
worksheet.write(row + 8 + i, col + haupt_g - 1, g)
haupt_g += 1
elif teams[t]["Gang"] == "Nachspeise":
worksheet.write(
row + 12, col + nach_g - 1, "Nachspeise Gruppe " + str(nach_g), bold
)
worksheet.write(row + 13, col + nach_g - 1, t, bold)
for i, g in enumerate(teams[t]["Gäste"]):
worksheet.write(row + 14 + i, col + nach_g - 1, g)
nach_g += 1
for t in teams:
worksheet = workbook.add_worksheet(t)
worksheet.set_column(0, 0, 60)
worksheet.set_column(1, 1, 100)
row = 0
col = 0
worksheet.merge_range("A1:B1", t, header)
row += 2
for n in [
"Namen",
"Gang",
"Location",
"Route",
"Gäste",
"Unverträglichkeiten der Gäste",
"Zeit",
]:
if n == "Namen":
for i, name in enumerate(teams[t][n]):
if i == 0:
text = name
else:
text += " und " + name
worksheet.write(row, col, "Namen", bold)
worksheet.write(row, col + 1, text)
row += 1
elif n == "Gang":
worksheet.write(row, col, "Euer Gang", bold)
worksheet.write(row, col + 1, teams[t][n])
row += 1
elif n == "Location":
worksheet.write(row, col, "Location (automatisch optimal gewählt)", bold)
worksheet.write(row, col + 1, teams[t][n])
row += 1
elif n == "Gäste":
for i, gaeste in enumerate(teams[t][n]):
if i == 0:
text = gaeste
else:
text += " und " + gaeste
worksheet.write(row, col, "Eure Gäste", bold)
worksheet.write(row, col + 1, text)
row += 1
for i, gaeste in enumerate(teams[t][n]):
for j, namen in enumerate(teams[gaeste]["Namen"]):
if i == 0 and j == 0:
text = namen
elif (
i == len(teams[t][n]) - 1
and j == len(teams[gaeste]["Namen"]) - 1
):
text += " und " + namen
else:
text += ", " + namen
worksheet.write(row, col + 1, text)
row += 2
elif n == "Route":
for i, route in enumerate(teams[t][n]):
if i == 0:
text = "Zuerst bei " + route
else:
text += ", dann bei " + route
worksheet.write(row, col, "Eure Route", bold)
worksheet.write(row, col + 1, text)
row += 2
elif n == "Unverträglichkeiten der Gäste":
for i, unv in enumerate(teams[t][n]):
if i == 0:
text = unv
else:
text += " und " + unv
worksheet.write(row, col, "Achtet auf diese Eigenarten eurer Gäste", bold)
worksheet.write(row, col + 1, text)
row += 2
elif n == "Zeit":
worksheet.write(
row,
col,
"Gesamtzeit für eure Route von Vorspeise bis Afterparty (per Fahrrad)",
bold,
)
worksheet.write(row, col + 1, teams[t][n])
workbook.close()
+2
View File
@@ -0,0 +1,2 @@
def main() -> None:
print("Hello from tatami-core!")
+114
View File
@@ -0,0 +1,114 @@
import datetime as dt
import pandas as pd
from uuid import uuid4
class Participant:
def __init__(
self, name: str, address: str, phone: str, kitchen_size: float, allergies: str
):
self.uuid = str(uuid4())
self.name = name
self.address = address
self.phone = phone
self.kitchen_size = kitchen_size # bigger is better; range 0-10
self.allergies = allergies
def get_penalty(self) -> dt.timedelta:
return dt.timedelta(minutes=3 * (10 - self.kitchen_size))
def get_after_party_time(
self, distance_matrix: pd.DataFrame, after_party_group: "Group"
) -> dt.timedelta:
return (
self.get_penalty()
+ pd.to_timedelta(
distance_matrix.loc[self.uuid, after_party_group.main_member.uuid]
).to_pytimedelta()
)
def __repr__(self) -> str:
return f"Participant(name={self.name}, uuid={self.uuid})[address={self.address}, phone={self.phone}, kitchen_size={self.kitchen_size}, allergies={self.allergies}]"
def __str__(self) -> str:
return self.__repr__()
def dict(self) -> dict:
return {
"uuid": self.uuid,
"name": self.name,
"address": self.address,
"phone": self.phone,
"kitchen_size": self.kitchen_size,
"allergies": self.allergies,
}
class Group:
def __init__(self, members: list[Participant], main_member: int = 0):
self.uuid = "Group_" + str(uuid4())
self.members = members
self.main_member = members[main_member]
self.course: str | None = None # For ordering the groups allowed values: "starter", "main", "dessert"
self.hosts: list[Group] | None = None
def set_course(self, course: str):
self.course = course
def set_hosts(self, hosts: list["Group"]):
self.hosts = hosts
self.sort_hosts()
def sort_hosts(self):
if self.hosts is None:
return
courses = {"starter": 0, "main": 1, "dessert": 2}
self.hosts.sort(key=lambda x: courses[x.course] if x.course in courses else 3)
def add_host(self, host: "Group"):
if self.hosts is None:
self.hosts = []
self.hosts.append(host)
self.sort_hosts()
def add_member(self, member: Participant, main_member: bool = False):
if main_member:
self.main_member = member
self.members.append(member)
def get_total_time(
self, distance_matrix: pd.DataFrame, after_party_group: "Group"
) -> dt.timedelta:
if self.hosts is None:
raise ValueError("Hosts must be set.")
if after_party_group.uuid in [h.uuid for h in self.hosts]:
groups = self.hosts
else:
groups = self.hosts + [after_party_group]
total_time = dt.timedelta()
for group, next_group in zip(groups[:-1], groups[1:]):
total_time += pd.to_timedelta(
distance_matrix.loc[group.main_member.uuid, next_group.main_member.uuid]
).to_pytimedelta()
total_time += group.main_member.get_penalty()
return total_time
def dict(self) -> dict:
return {
"uuid": self.uuid,
"members": [member.uuid for member in self.members],
"main_member": self.main_member.uuid,
"course": self.course,
"hosts": [host.uuid for host in self.hosts] if self.hosts else None,
}
def get_guests(self, groups: list["Group"]) -> list[Participant]:
guests: list[Participant] = []
if self.hosts is None:
return guests
for group in groups:
if group.hosts is not None and self.uuid in [g.uuid for g in group.hosts]:
guests.extend(group.members)
return guests
+210
View File
@@ -0,0 +1,210 @@
from tatami.classes import Participant, Group
from tatami.traveltimes import reduce_distance_matrix, get_participant_distance_matrix
import pandas as pd
import numpy as np
import random
from tqdm import tqdm
from itertools import permutations
def get_after_party_group(address: str) -> Group:
"""
Create a group for the after party with a single participant.
"""
participant = Participant(
name="After Party", address=address, phone="", kitchen_size=10, allergies=""
)
return Group(members=[participant], main_member=0)
def get_masterplan(
participants: list[Participant],
distance_matrix: pd.DataFrame,
after_party_group: Group,
) -> tuple[list[dict], list[dict]]:
groups_per_course = np.floor(len(participants) / 6).astype(int)
participants.sort(
key=lambda x: x.get_after_party_time(distance_matrix, after_party_group)
)
hosts = participants[: 3 * groups_per_course]
semi_hosts = participants[3 * groups_per_course :]
courses = ["starter", "main", "dessert"] * groups_per_course
groups = []
for host in hosts:
group = Group(members=[host])
groups.append(group)
random.shuffle(semi_hosts)
for i, member in enumerate(semi_hosts):
groups[i % len(groups)].add_member(member)
distance_matrix = reduce_distance_matrix(
distance_matrix, [*groups, after_party_group]
)
best_order = run_simulated_annealing(
groups,
distance_matrix,
initial_temperature=1000,
cooling_rate=0.99,
max_iterations=10000,
multiprocessing=1,
)
assign_courses(best_order, courses)
group_dicts = [group.dict() for group in best_order]
participant_dicts = [participant.dict() for participant in participants]
return group_dicts, participant_dicts
def assign_courses(groups: list[Group], courses: list[str]) -> None:
"""
Assign courses to groups.
"""
for i, (group, course) in enumerate(zip(groups, courses)):
group.set_course(course)
hosts = [groups[i] for i in get_courses(i, len(groups))]
group.set_hosts(hosts)
def run_simulated_annealing(
groups: list[Group],
reduced_distance_matrix: pd.DataFrame,
initial_temperature: float,
cooling_rate: float,
max_iterations: int,
multiprocessing: int = 1,
) -> list[Group]:
"""
Run simulated annealing to find the optimal order of groups.
"""
group_indices = list(range(len(groups)))
distance_matrix = reduced_distance_matrix.to_numpy()
# Convert group indices to a list of integers
group_indices = [int(i) for i in group_indices]
if multiprocessing > 1:
raise NotImplementedError("Multiprocessing is not implemented yet.")
else:
# Run simulated annealing without multiprocessing
best_order = simulated_annealing(
distance_matrix,
group_indices,
initial_temperature,
cooling_rate,
max_iterations,
)
best_ordererd_groups = [groups[i] for i in best_order]
return best_ordererd_groups
def next_permutation(
group_indices: list[int],
distance_matrix: np.ndarray,
T: float,
) -> list[int]:
"""
Generate the next permutation of group indices that minimizes the total travel time using boltzmann annealing.
"""
all_permutations = list(permutations(group_indices))
times = {
i: fast_total_time(distance_matrix, perm)
for i, perm in enumerate(all_permutations)
}
min_time = min(times.values())
probabilities = {i: np.exp(-(time - min_time) / T) for i, time in times.items()}
key = np.random.choice(list(probabilities.keys()), p=list(probabilities.values()))
return all_permutations[key]
def simulated_annealing(
distance_matrix: np.ndarray,
group_indices: list[int],
initial_temperature: float,
cooling_rate: float,
max_iterations: int,
) -> list[int]:
"""
Simulated annealing algorithm to find the optimal order of groups.
"""
solution = group_indices.copy()
np.random.shuffle(solution)
current_temperature = initial_temperature
for iteration in tqdm(range(max_iterations)):
try:
solution = next_permutation(solution, distance_matrix, current_temperature)
time = fast_total_time(distance_matrix, solution)
if iteration % 100 == 0:
print(f"Iteration {iteration}: Time = {time}")
current_temperature *= cooling_rate
except KeyboardInterrupt:
print("Simulation interrupted. Returning current solution.")
break
return solution
def get_courses(
group_index: int,
total_meetings: int,
):
a = group_index
b = (group_index + 1) % total_meetings
c = (group_index - 4) % total_meetings
order = sorted((a, b, c), key=lambda x: x % 3)
return order
def fast_total_time(distance_matrix: np.ndarray, group_indices: list[int]):
"""Calculates the total travel time for all groups.
Args:
distance_matrix (np.ndarray): 2D array representing the distance matrix, where
distance_matrix[i][j] is the travel time from group i to group j in seconds. Should be of shape (n+1, n+1). The last column/row should be the after party group.
group_indices (list[int]): List of group indices for which to calculate the total travel time. Should be of length n.
"""
n = len(group_indices)
after_party_idx = n # Since distance_matrix is (n+1)x(n+1)
total_time = 0
total_meetings = n
for group_index in group_indices:
a, b, c = get_courses(group_index, total_meetings)
total_time += distance_matrix[a][b]
total_time += distance_matrix[b][c]
total_time += distance_matrix[c][after_party_idx]
return total_time
def load_csv_to_participants(file_path: str) -> list[Participant]:
"""
Load participants from a CSV file.
"""
df = pd.read_csv(file_path, sep="\t")
participants = []
for _, row in df.iterrows():
participant = Participant(
name=row["name"],
address=row["address"],
phone=row["phone"],
kitchen_size=row["kitchen_size"],
allergies=row["allergies"],
)
participants.append(participant)
return participants
if __name__ == "__main__":
# Example usage
participants = load_csv_to_participants("test-config.csv")
after_party_group = get_after_party_group(
"Sebastian-Kneipp-Straße 6, 76131 Karlsruhe"
)
distance_matrix = get_participant_distance_matrix(
[*participants, after_party_group.main_member], mode="BICYCLE"
)
masterplan = get_masterplan(participants, distance_matrix, after_party_group)
print(masterplan)
print("Masterplan generated successfully.")
+104
View File
@@ -0,0 +1,104 @@
import pandas as pd
import requests
import os
from tatami.classes import Participant, Group
GOOGLE_MAPS_API_URL = (
"https://routes.googleapis.com/distanceMatrix/v2:computeRouteMatrix"
)
GOOGLE_MAPS_API_KEY = os.getenv("GOOGLE_MAPS_API_KEY")
if not GOOGLE_MAPS_API_KEY:
raise ValueError("GOOGLE_MAPS_API_KEY environment variable is not set.")
def get_distance_matrix(
addresses: list[str], mode: str = "BICYCLE", value: str = "duration"
) -> pd.DataFrame:
if not addresses:
raise ValueError("The list of addresses cannot be empty.")
locations = [{"waypoint": {"address": address}} for address in addresses]
params = {
"origins": locations,
"destinations": locations,
"travelMode": mode,
}
headers = {
"Content-Type": "application/json",
"X-Goog-Api-Key": GOOGLE_MAPS_API_KEY,
"X-Goog-FieldMask": "originIndex,destinationIndex,duration,distanceMeters",
}
response = requests.post(GOOGLE_MAPS_API_URL, json=params, headers=headers)
if response.status_code != 200:
raise Exception(f"Error fetching data from Google Maps API: {response.text}")
data = response.json()
df = pd.DataFrame(
data
) # Keys: originIndex, destinationIndex, duration, distanceMeters
distance_matrix = pd.DataFrame(
index=df["originIndex"].unique(), columns=df["destinationIndex"].unique()
)
for index, row in df.iterrows():
origin = row["originIndex"]
destination = row["destinationIndex"]
if value == "duration":
distance_matrix.at[origin, destination] = pd.to_timedelta(row["duration"])
elif value == "distanceMeters":
distance_matrix.at[origin, destination] = float(row["distanceMeters"])
else:
raise ValueError(
"Invalid value specified. Use 'duration' or 'distanceMeters'."
)
# Sort the DataFrame by index and columns
distance_matrix = distance_matrix.sort_index().sort_index(axis=1)
return distance_matrix
def get_participant_distance_matrix(
participants: list[Participant], mode: str = "BICYCLE"
) -> pd.DataFrame:
addresses = [p.address for p in participants]
distance_matrix = get_distance_matrix(addresses, mode)
distance_matrix.index = pd.Index([p.uuid for p in participants])
distance_matrix.columns = pd.Index([p.uuid for p in participants])
return distance_matrix
def reduce_distance_matrix(
distance_matrix: pd.DataFrame, groups: list[Group]
) -> pd.DataFrame:
uuids = [g.main_member.uuid for g in groups]
reduced_distance_matrix = distance_matrix.loc[uuids, uuids]
reduced_distance_matrix.columns = pd.Index(uuids)
reduced_distance_matrix.index = pd.Index(uuids)
for group in groups:
reduced_distance_matrix.loc[group.main_member.uuid] += (
group.main_member.get_penalty()
)
return reduced_distance_matrix
if __name__ == "__main__":
# Example usage
participants = [
Participant("Alice", "Römerstr. 12 76189 Karlsruhe", "555-1234", 8.0, "None"),
Participant(
"Bob", "Gottesauerstr. 30 76131 Karlsruhe", "555-5678", 7.5, "Peanuts"
),
Participant(
"Charlie", "Hermann-Hesse-Str 50 76189 Karlsruhe", "555-8765", 9.0, "None"
),
]
distance_matrix = get_participant_distance_matrix(participants)
print(distance_matrix)
Generated
+326
View File
@@ -0,0 +1,326 @@
version = 1
requires-python = ">=3.13"
[[package]]
name = "certifi"
version = "2025.1.31"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
]
[[package]]
name = "charset-normalizer"
version = "3.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 },
{ url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 },
{ url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 },
{ url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 },
{ url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 },
{ url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 },
{ url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 },
{ url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 },
{ url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 },
{ url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 },
{ url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 },
{ url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 },
{ url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 },
{ url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 },
]
[[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 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
]
[[package]]
name = "mypy"
version = "1.15.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mypy-extensions" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592 },
{ url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611 },
{ url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443 },
{ url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541 },
{ url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348 },
{ url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648 },
{ url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 },
]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 },
]
[[package]]
name = "numpy"
version = "2.2.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e1/78/31103410a57bc2c2b93a3597340a8119588571f6a4539067546cb9a0bfac/numpy-2.2.4.tar.gz", hash = "sha256:9ba03692a45d3eef66559efe1d1096c4b9b75c0986b5dff5530c378fb8331d4f", size = 20270701 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/d0/bd5ad792e78017f5decfb2ecc947422a3669a34f775679a76317af671ffc/numpy-2.2.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cf4e5c6a278d620dee9ddeb487dc6a860f9b199eadeecc567f777daace1e9e7", size = 20933623 },
{ url = "https://files.pythonhosted.org/packages/c3/bc/2b3545766337b95409868f8e62053135bdc7fa2ce630aba983a2aa60b559/numpy-2.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1974afec0b479e50438fc3648974268f972e2d908ddb6d7fb634598cdb8260a0", size = 14148681 },
{ url = "https://files.pythonhosted.org/packages/6a/70/67b24d68a56551d43a6ec9fe8c5f91b526d4c1a46a6387b956bf2d64744e/numpy-2.2.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:79bd5f0a02aa16808fcbc79a9a376a147cc1045f7dfe44c6e7d53fa8b8a79392", size = 5148759 },
{ url = "https://files.pythonhosted.org/packages/1c/8b/e2fc8a75fcb7be12d90b31477c9356c0cbb44abce7ffb36be39a0017afad/numpy-2.2.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:3387dd7232804b341165cedcb90694565a6015433ee076c6754775e85d86f1fc", size = 6683092 },
{ url = "https://files.pythonhosted.org/packages/13/73/41b7b27f169ecf368b52533edb72e56a133f9e86256e809e169362553b49/numpy-2.2.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f527d8fdb0286fd2fd97a2a96c6be17ba4232da346931d967a0630050dfd298", size = 14081422 },
{ url = "https://files.pythonhosted.org/packages/4b/04/e208ff3ae3ddfbafc05910f89546382f15a3f10186b1f56bd99f159689c2/numpy-2.2.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bce43e386c16898b91e162e5baaad90c4b06f9dcbe36282490032cec98dc8ae7", size = 16132202 },
{ url = "https://files.pythonhosted.org/packages/fe/bc/2218160574d862d5e55f803d88ddcad88beff94791f9c5f86d67bd8fbf1c/numpy-2.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31504f970f563d99f71a3512d0c01a645b692b12a63630d6aafa0939e52361e6", size = 15573131 },
{ url = "https://files.pythonhosted.org/packages/a5/78/97c775bc4f05abc8a8426436b7cb1be806a02a2994b195945600855e3a25/numpy-2.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:81413336ef121a6ba746892fad881a83351ee3e1e4011f52e97fba79233611fd", size = 17894270 },
{ url = "https://files.pythonhosted.org/packages/b9/eb/38c06217a5f6de27dcb41524ca95a44e395e6a1decdc0c99fec0832ce6ae/numpy-2.2.4-cp313-cp313-win32.whl", hash = "sha256:f486038e44caa08dbd97275a9a35a283a8f1d2f0ee60ac260a1790e76660833c", size = 6308141 },
{ url = "https://files.pythonhosted.org/packages/52/17/d0dd10ab6d125c6d11ffb6dfa3423c3571befab8358d4f85cd4471964fcd/numpy-2.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:207a2b8441cc8b6a2a78c9ddc64d00d20c303d79fba08c577752f080c4007ee3", size = 12636885 },
{ url = "https://files.pythonhosted.org/packages/fa/e2/793288ede17a0fdc921172916efb40f3cbc2aa97e76c5c84aba6dc7e8747/numpy-2.2.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8120575cb4882318c791f839a4fd66161a6fa46f3f0a5e613071aae35b5dd8f8", size = 20961829 },
{ url = "https://files.pythonhosted.org/packages/3a/75/bb4573f6c462afd1ea5cbedcc362fe3e9bdbcc57aefd37c681be1155fbaa/numpy-2.2.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a761ba0fa886a7bb33c6c8f6f20213735cb19642c580a931c625ee377ee8bd39", size = 14161419 },
{ url = "https://files.pythonhosted.org/packages/03/68/07b4cd01090ca46c7a336958b413cdbe75002286295f2addea767b7f16c9/numpy-2.2.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:ac0280f1ba4a4bfff363a99a6aceed4f8e123f8a9b234c89140f5e894e452ecd", size = 5196414 },
{ url = "https://files.pythonhosted.org/packages/a5/fd/d4a29478d622fedff5c4b4b4cedfc37a00691079623c0575978d2446db9e/numpy-2.2.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:879cf3a9a2b53a4672a168c21375166171bc3932b7e21f622201811c43cdd3b0", size = 6709379 },
{ url = "https://files.pythonhosted.org/packages/41/78/96dddb75bb9be730b87c72f30ffdd62611aba234e4e460576a068c98eff6/numpy-2.2.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f05d4198c1bacc9124018109c5fba2f3201dbe7ab6e92ff100494f236209c960", size = 14051725 },
{ url = "https://files.pythonhosted.org/packages/00/06/5306b8199bffac2a29d9119c11f457f6c7d41115a335b78d3f86fad4dbe8/numpy-2.2.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f085ce2e813a50dfd0e01fbfc0c12bbe5d2063d99f8b29da30e544fb6483b8", size = 16101638 },
{ url = "https://files.pythonhosted.org/packages/fa/03/74c5b631ee1ded596945c12027649e6344614144369fd3ec1aaced782882/numpy-2.2.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:92bda934a791c01d6d9d8e038363c50918ef7c40601552a58ac84c9613a665bc", size = 15571717 },
{ url = "https://files.pythonhosted.org/packages/cb/dc/4fc7c0283abe0981e3b89f9b332a134e237dd476b0c018e1e21083310c31/numpy-2.2.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ee4d528022f4c5ff67332469e10efe06a267e32f4067dc76bb7e2cddf3cd25ff", size = 17879998 },
{ url = "https://files.pythonhosted.org/packages/e5/2b/878576190c5cfa29ed896b518cc516aecc7c98a919e20706c12480465f43/numpy-2.2.4-cp313-cp313t-win32.whl", hash = "sha256:05c076d531e9998e7e694c36e8b349969c56eadd2cdcd07242958489d79a7286", size = 6366896 },
{ url = "https://files.pythonhosted.org/packages/3e/05/eb7eec66b95cf697f08c754ef26c3549d03ebd682819f794cb039574a0a6/numpy-2.2.4-cp313-cp313t-win_amd64.whl", hash = "sha256:188dcbca89834cc2e14eb2f106c96d6d46f200fe0200310fc29089657379c58d", size = 12739119 },
]
[[package]]
name = "pandas"
version = "2.2.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
{ name = "python-dateutil" },
{ name = "pytz" },
{ name = "tzdata" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643 },
{ url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573 },
{ url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085 },
{ url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809 },
{ url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316 },
{ url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055 },
{ url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175 },
{ url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650 },
{ url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177 },
{ url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526 },
{ url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013 },
{ url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620 },
{ url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436 },
]
[[package]]
name = "pandas-stubs"
version = "2.2.3.250308"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
{ name = "types-pytz" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2e/5a/261f5c67a73e46df2d5984fe7129d66a3ed4864fd7aa9d8721abb3fc802e/pandas_stubs-2.2.3.250308.tar.gz", hash = "sha256:3a6e9daf161f00b85c83772ed3d5cff9522028f07a94817472c07b91f46710fd", size = 103986 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/64/ab61d9ca06ff66c07eb804ec27dec1a2be1978b3c3767caaa91e363438cc/pandas_stubs-2.2.3.250308-py3-none-any.whl", hash = "sha256:a377edff3b61f8b268c82499fdbe7c00fdeed13235b8b71d6a1dc347aeddc74d", size = 158053 },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
]
[[package]]
name = "pytz"
version = "2025.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 },
]
[[package]]
name = "requests"
version = "2.32.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
]
[[package]]
name = "ruff"
version = "0.11.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/45/71/5759b2a6b2279bb77fe15b1435b89473631c2cd6374d45ccdb6b785810be/ruff-0.11.5.tar.gz", hash = "sha256:cae2e2439cb88853e421901ec040a758960b576126dab520fa08e9de431d1bef", size = 3976488 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/23/db/6efda6381778eec7f35875b5cbefd194904832a1153d68d36d6b269d81a8/ruff-0.11.5-py3-none-linux_armv6l.whl", hash = "sha256:2561294e108eb648e50f210671cc56aee590fb6167b594144401532138c66c7b", size = 10103150 },
{ url = "https://files.pythonhosted.org/packages/44/f2/06cd9006077a8db61956768bc200a8e52515bf33a8f9b671ee527bb10d77/ruff-0.11.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac12884b9e005c12d0bd121f56ccf8033e1614f736f766c118ad60780882a077", size = 10898637 },
{ url = "https://files.pythonhosted.org/packages/18/f5/af390a013c56022fe6f72b95c86eb7b2585c89cc25d63882d3bfe411ecf1/ruff-0.11.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4bfd80a6ec559a5eeb96c33f832418bf0fb96752de0539905cf7b0cc1d31d779", size = 10236012 },
{ url = "https://files.pythonhosted.org/packages/b8/ca/b9bf954cfed165e1a0c24b86305d5c8ea75def256707f2448439ac5e0d8b/ruff-0.11.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0947c0a1afa75dcb5db4b34b070ec2bccee869d40e6cc8ab25aca11a7d527794", size = 10415338 },
{ url = "https://files.pythonhosted.org/packages/d9/4d/2522dde4e790f1b59885283f8786ab0046958dfd39959c81acc75d347467/ruff-0.11.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad871ff74b5ec9caa66cb725b85d4ef89b53f8170f47c3406e32ef040400b038", size = 9965277 },
{ url = "https://files.pythonhosted.org/packages/e5/7a/749f56f150eef71ce2f626a2f6988446c620af2f9ba2a7804295ca450397/ruff-0.11.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6cf918390cfe46d240732d4d72fa6e18e528ca1f60e318a10835cf2fa3dc19f", size = 11541614 },
{ url = "https://files.pythonhosted.org/packages/89/b2/7d9b8435222485b6aac627d9c29793ba89be40b5de11584ca604b829e960/ruff-0.11.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56145ee1478582f61c08f21076dc59153310d606ad663acc00ea3ab5b2125f82", size = 12198873 },
{ url = "https://files.pythonhosted.org/packages/00/e0/a1a69ef5ffb5c5f9c31554b27e030a9c468fc6f57055886d27d316dfbabd/ruff-0.11.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5f66f8f1e8c9fc594cbd66fbc5f246a8d91f916cb9667e80208663ec3728304", size = 11670190 },
{ url = "https://files.pythonhosted.org/packages/05/61/c1c16df6e92975072c07f8b20dad35cd858e8462b8865bc856fe5d6ccb63/ruff-0.11.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80b4df4d335a80315ab9afc81ed1cff62be112bd165e162b5eed8ac55bfc8470", size = 13902301 },
{ url = "https://files.pythonhosted.org/packages/79/89/0af10c8af4363304fd8cb833bd407a2850c760b71edf742c18d5a87bb3ad/ruff-0.11.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3068befab73620b8a0cc2431bd46b3cd619bc17d6f7695a3e1bb166b652c382a", size = 11350132 },
{ url = "https://files.pythonhosted.org/packages/b9/e1/ecb4c687cbf15164dd00e38cf62cbab238cad05dd8b6b0fc68b0c2785e15/ruff-0.11.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5da2e710a9641828e09aa98b92c9ebbc60518fdf3921241326ca3e8f8e55b8b", size = 10312937 },
{ url = "https://files.pythonhosted.org/packages/cf/4f/0e53fe5e500b65934500949361e3cd290c5ba60f0324ed59d15f46479c06/ruff-0.11.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ef39f19cb8ec98cbc762344921e216f3857a06c47412030374fffd413fb8fd3a", size = 9936683 },
{ url = "https://files.pythonhosted.org/packages/04/a8/8183c4da6d35794ae7f76f96261ef5960853cd3f899c2671961f97a27d8e/ruff-0.11.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b2a7cedf47244f431fd11aa5a7e2806dda2e0c365873bda7834e8f7d785ae159", size = 10950217 },
{ url = "https://files.pythonhosted.org/packages/26/88/9b85a5a8af21e46a0639b107fcf9bfc31da4f1d263f2fc7fbe7199b47f0a/ruff-0.11.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:81be52e7519f3d1a0beadcf8e974715b2dfc808ae8ec729ecfc79bddf8dbb783", size = 11404521 },
{ url = "https://files.pythonhosted.org/packages/fc/52/047f35d3b20fd1ae9ccfe28791ef0f3ca0ef0b3e6c1a58badd97d450131b/ruff-0.11.5-py3-none-win32.whl", hash = "sha256:e268da7b40f56e3eca571508a7e567e794f9bfcc0f412c4b607931d3af9c4afe", size = 10320697 },
{ url = "https://files.pythonhosted.org/packages/b9/fe/00c78010e3332a6e92762424cf4c1919065707e962232797d0b57fd8267e/ruff-0.11.5-py3-none-win_amd64.whl", hash = "sha256:6c6dc38af3cfe2863213ea25b6dc616d679205732dc0fb673356c2d69608f800", size = 11378665 },
{ url = "https://files.pythonhosted.org/packages/43/7c/c83fe5cbb70ff017612ff36654edfebec4b1ef79b558b8e5fd933bab836b/ruff-0.11.5-py3-none-win_arm64.whl", hash = "sha256:67e241b4314f4eacf14a601d586026a962f4002a475aa702c69980a38087aa4e", size = 10460287 },
]
[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
]
[[package]]
name = "tatami"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "numpy" },
{ name = "pandas" },
{ name = "pandas-stubs" },
{ name = "requests" },
{ name = "tqdm" },
{ name = "types-requests" },
{ name = "types-tqdm" },
]
[package.dev-dependencies]
dev = [
{ name = "mypy" },
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
{ name = "numpy", specifier = ">=2.2.4" },
{ name = "pandas", specifier = ">=2.2.3" },
{ name = "pandas-stubs", specifier = ">=2.2.3.250308" },
{ name = "requests", specifier = ">=2.32.3" },
{ name = "tqdm", specifier = ">=4.67.1" },
{ name = "types-requests", specifier = ">=2.32.0.20250328" },
{ name = "types-tqdm", specifier = ">=4.67.0.20250404" },
]
[package.metadata.requires-dev]
dev = [
{ name = "mypy", specifier = ">=1.15.0" },
{ name = "ruff", specifier = ">=0.11.5" },
]
[[package]]
name = "tqdm"
version = "4.67.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 },
]
[[package]]
name = "types-pytz"
version = "2025.2.0.20250326"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4b/66/38c89861242f2c61c8315ddbcc7d7bbf64979f4b0bdc48db0ba62aeec330/types_pytz-2025.2.0.20250326.tar.gz", hash = "sha256:deda02de24f527066fc8d6a19e284ab3f3ae716a42b4adb6b40e75e408c08d36", size = 10595 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4e/e0/17f3a6670db5c95dc195f346e2e7290f22ba8327c188133959389b578cbd/types_pytz-2025.2.0.20250326-py3-none-any.whl", hash = "sha256:3c397fd1b845cd2b3adc9398607764ced9e578a98a5d1fbb4a9bc9253edfb162", size = 10222 },
]
[[package]]
name = "types-requests"
version = "2.32.0.20250328"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/00/7d/eb174f74e3f5634eaacb38031bbe467dfe2e545bc255e5c90096ec46bc46/types_requests-2.32.0.20250328.tar.gz", hash = "sha256:c9e67228ea103bd811c96984fac36ed2ae8da87a36a633964a21f199d60baf32", size = 22995 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/15/3700282a9d4ea3b37044264d3e4d1b1f0095a4ebf860a99914fd544e3be3/types_requests-2.32.0.20250328-py3-none-any.whl", hash = "sha256:72ff80f84b15eb3aa7a8e2625fffb6a93f2ad5a0c20215fc1dcfa61117bcb2a2", size = 20663 },
]
[[package]]
name = "types-tqdm"
version = "4.67.0.20250404"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "types-requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f4/a1/75e0d6f96f8c34a1bad6c232566182f2ae53ffdf7ab9c75afb61b2a07354/types_tqdm-4.67.0.20250404.tar.gz", hash = "sha256:e9997c655ffbba3ab78f4418b5511c05a54e76824d073d212166dc73aa56c768", size = 17159 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/35/85/2c09e94d2554e8314cbc74f897c1b3012930b34a22f6905bb0b9fb79f40e/types_tqdm-4.67.0.20250404-py3-none-any.whl", hash = "sha256:4a9b897eb4036f757240f4cb4a794f296265c04de46fdd058e453891f0186eed", size = 24053 },
]
[[package]]
name = "typing-extensions"
version = "4.13.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 },
]
[[package]]
name = "tzdata"
version = "2025.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 },
]
[[package]]
name = "urllib3"
version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 },
]