diff --git a/.gitignore b/.gitignore index 0a19790..e4b14a2 100644 --- a/.gitignore +++ b/.gitignore @@ -172,3 +172,7 @@ cython_debug/ # PyPI configuration file .pypirc + + +# csv containing personal information +test-config.csv \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..643b148 --- /dev/null +++ b/.pre-commit-config.yaml @@ -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/" \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8a1c371 --- /dev/null +++ b/pyproject.toml @@ -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" diff --git a/running_dinner/running_dinner.py b/running_dinner/running_dinner.py new file mode 100644 index 0000000..25b729c --- /dev/null +++ b/running_dinner/running_dinner.py @@ -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() diff --git a/src/tatami/__init__.py b/src/tatami/__init__.py new file mode 100644 index 0000000..9dc64e1 --- /dev/null +++ b/src/tatami/__init__.py @@ -0,0 +1,2 @@ +def main() -> None: + print("Hello from tatami-core!") diff --git a/src/tatami/classes.py b/src/tatami/classes.py new file mode 100644 index 0000000..4efd33d --- /dev/null +++ b/src/tatami/classes.py @@ -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 diff --git a/src/tatami/tatami_masterplan.py b/src/tatami/tatami_masterplan.py new file mode 100644 index 0000000..c9254e3 --- /dev/null +++ b/src/tatami/tatami_masterplan.py @@ -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.") diff --git a/src/tatami/traveltimes.py b/src/tatami/traveltimes.py new file mode 100644 index 0000000..44f353d --- /dev/null +++ b/src/tatami/traveltimes.py @@ -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) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..9433f8e --- /dev/null +++ b/uv.lock @@ -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 }, +]