diff --git a/.gitignore b/.gitignore index b6993c08..8f76ea40 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ junit.xml .DS_Store /vendor profile.json +*.egg-info diff --git a/.zed/settings.json b/.zed/settings.json index 477c7499..0a9fb00e 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -14,6 +14,13 @@ "features": [] } } + }, + "pyright": { + "settings": { + "python": { + "pythonPath": "scripts/.venv/bin/python" + } + } } } } diff --git a/crates/loona/src/h2/server.rs b/crates/loona/src/h2/server.rs index 8fb8a44c..7230e62e 100644 --- a/crates/loona/src/h2/server.rs +++ b/crates/loona/src/h2/server.rs @@ -1454,7 +1454,7 @@ where std::str::from_utf8(&value).unwrap_or(""), ); - if &key[..1] == b":" { + if key.first() == Some(&b':') { if saw_regular_header { req_error = Some(H2StreamError::BadRequest( "All pseudo-header fields MUST appear in a field block before all regular field lines (RFC 9113, section 8.3)" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..0d01a11f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,27 @@ +[project] +name = "loona" +version = "0.1.0" +description = "Benchmarking rig for the loona Rust HTTP implementation" +requires-python = ">=3.11" +dependencies = [ + "termcolor~=2.4.0", + "openpyxl~=3.1.5", + "psutil>=6.0.0", +] + +[tool.setuptools] +packages = ["scripts"] + +[tool.pyright] +include = ["scripts"] +exclude = [ + "**/__pycache__", + "**/node_modules", + ".venv", +] +venvPath = "." +venv = ".venv" + +[build-system] +requires = ["setuptools>=45", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/scripts/bench.py b/scripts/bench.py new file mode 100755 index 00000000..6128f22b --- /dev/null +++ b/scripts/bench.py @@ -0,0 +1,419 @@ +#!/usr/bin/env -S PYTHONUNBUFFERED=1 FORCE_COLOR=1 uv run + +import psutil +import ctypes +import os +import signal +import subprocess +import sys +import time +import openpyxl +import pprint +from openpyxl.styles import Alignment, Font + +from pathlib import Path +from termcolor import colored +import tempfile + +# Change to the script's directory +os.chdir(os.path.dirname(os.path.abspath(__file__))) + +PERF_EVENTS = [ + "cycles", + "instructions", + "branches", + "branch-misses", + "cache-references", + "cache-misses", + "page-faults", + "syscalls:sys_enter_write", + "syscalls:sys_enter_writev", + "syscalls:sys_enter_epoll_wait", + "syscalls:sys_enter_io_uring_enter" +] +PERF_EVENTS_STRING = ",".join(PERF_EVENTS) + +# Define constants for prctl +PR_SET_PDEATHSIG = 1 + +# Load the libc shared library +libc = ctypes.CDLL("libc.so.6") + +def set_pdeathsig(): + """Set the parent death signal to SIGKILL.""" + # Call prctl with PR_SET_PDEATHSIG and SIGKILL + libc.prctl(PR_SET_PDEATHSIG, signal.SIGKILL) + +# Set trap to kill the process group on script exit +def kill_group(): + os.killpg(0, signal.SIGKILL) + +def kill_group_except_self(): + current_pid = os.getpid() + for proc in psutil.process_iter(['pid']): + try: + if proc.pid != current_pid and os.getpgid(proc.pid) == os.getpgid(0): + print(f"Killing process {proc.pid}: {proc.name()}") + os.kill(proc.pid, signal.SIGKILL) + except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + pass + +def abort_and_cleanup(signum, frame): + print(colored("\nAborting and cleaning up...", "red")) + kill_group() + sys.exit(1) + +# Register signal handlers +signal.signal(signal.SIGINT, abort_and_cleanup) +signal.signal(signal.SIGTERM, abort_and_cleanup) + +# Create directory if it doesn't exist +Path("/tmp/loona-perfstat").mkdir(parents=True, exist_ok=True) + +# Kill older processes +for pidfile in Path("/tmp/loona-perfstat").glob("*.PID"): + if pidfile.is_file(): + with open(pidfile) as f: + pid = f.read().strip() + if pid != str(os.getpid()): + try: + os.kill(int(pid), signal.SIGTERM) + except ProcessLookupError: + pass + pidfile.unlink() + +# Kill older remote processes +subprocess.run(["ssh", "brat", "pkill -9 h2load"], check=False) + +LOONA_DIR = os.path.expanduser("~/bearcove/loona") + +# Build the servers +subprocess.run(["cargo", "build", "--release", "--manifest-path", f"{LOONA_DIR}/Cargo.toml", "-F", "tracing/release_max_level_info"], check=True) + +# Set protocol, default to h2c +PROTO = os.environ.get("PROTO", "h2c") +os.environ["PROTO"] = PROTO + +# Get the default route interface +default_route = subprocess.check_output(["ip", "route", "show", "0.0.0.0/0"]).decode().strip() +default_interface = default_route.split()[4] + +# Get the IP address of the default interface +ip_addr_output = subprocess.check_output(["ip", "addr", "show", "dev", default_interface]).decode().strip() + +OUR_PUBLIC_IP = None +for line in ip_addr_output.split('\n'): + if 'inet ' in line: + OUR_PUBLIC_IP = line.split()[1].split('/')[0] + break + +if not OUR_PUBLIC_IP: + print(colored("Error: Could not determine our public IP address", "red")) + sys.exit(1) + +print(f"📡 Our public IP address is {OUR_PUBLIC_IP}") + +# Declare h2load args based on PROTO +HTTP_OR_HTTPS = "https" if PROTO == "tls" else "http" + +# Launch hyper server +hyper_env = os.environ.copy() +hyper_env.update({"ADDR": "0.0.0.0", "PORT": "8001"}) +hyper_process = subprocess.Popen([f"{LOONA_DIR}/target/release/httpwg-hyper"], env=hyper_env, preexec_fn=set_pdeathsig) +HYPER_PID = hyper_process.pid +with open("/tmp/loona-perfstat/hyper.PID", "w") as f: + f.write(str(HYPER_PID)) + +# Launch loona server +loona_env = os.environ.copy() +loona_env.update({"ADDR": "0.0.0.0", "PORT": "8002"}) +loona_process = subprocess.Popen([f"{LOONA_DIR}/target/release/httpwg-loona"], env=loona_env, preexec_fn=set_pdeathsig) +LOONA_PID = loona_process.pid +with open("/tmp/loona-perfstat/loona.PID", "w") as f: + f.write(str(LOONA_PID)) + +HYPER_ADDR = f"{HTTP_OR_HTTPS}://{OUR_PUBLIC_IP}:8001" +LOONA_ADDR = f"{HTTP_OR_HTTPS}://{OUR_PUBLIC_IP}:8002" + +servers = { + "hyper": { + "pid": HYPER_PID, + "address": HYPER_ADDR + }, + "loona": { + "pid": LOONA_PID, + "address": LOONA_ADDR + } +} + +SERVER = os.environ.get("SERVER") +if SERVER: + if SERVER in servers: + servers = {SERVER: servers[SERVER]} + else: + print(f"Error: SERVER '{SERVER}' not found in the list of servers.") + sys.exit(1) + +H2LOAD = "/nix/var/nix/profiles/default/bin/h2load" + +endpoint = os.environ.get("ENDPOINT", "/repeat-4k-blocks/128") +rps = os.environ.get("RPS", "2") +clients = os.environ.get("CLIENTS", "20") +streams = os.environ.get("STREAMS", "2") +warmup = int(os.environ.get("WARMUP", "5")) +duration = int(os.environ.get("DURATION", "20")) +repeat = int(os.environ.get("REPEAT", "3")) + +# Mode can be 'perfstat' or 'samply' +mode = os.environ.get("MODE", "perfstat") + +loona_git_sha = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD'], cwd=os.path.expanduser('~/bearcove/loona')).decode().strip() + +benchmark_params = f"RPS={rps}, ENDPOINT={endpoint}, CLIENTS={clients}, STREAMS={streams}, WARMUP={warmup}, DURATION={duration}, REPEAT={repeat}" +print(colored(f"📊 Benchmark parameters: {benchmark_params}", "blue")) + +def gen_h2load_cmd(addr: str): + return [ + H2LOAD, + "--warm-up-time", str(warmup), + "--rps", rps, + "--clients", clients, + "--max-concurrent-streams", streams, + "--duration", str(duration), + f"{addr}{endpoint}", + ] + +def do_samply(): + if len(servers) > 1: + print(colored("Error: More than one server specified.", "red")) + print("Please use SERVER=[loona,hyper] to narrow down to a single server.") + sys.exit(1) + + for server_name, server in servers.items(): + pid = server["pid"] + address = server["address"] + + print(colored("Warning: Warmup period is not taken into account in samply mode yet.", "yellow")) + + samply_process = subprocess.Popen(["samply", "record", "-p", str(server["pid"])], preexec_fn=set_pdeathsig) + with open("/tmp/loona-perfstat/samply.PID", "w") as f: + f.write(str(samply_process.pid)) + h2load_cmd = gen_h2load_cmd(server["address"]) + subprocess.run(["ssh", "brat"] + h2load_cmd, check=True) + samply_process.send_signal(signal.SIGINT) + samply_process.wait() + +def do_perfstat(): + import pickle + data_filename = f"/tmp/loona-perfstat/all_data.pkl" + + all_data = None + if os.environ.get("FROM_DISK") == "1": + if os.path.exists(data_filename): + with open(data_filename, 'rb') as f: + all_data = pickle.load(f) + print(colored(f"Loaded all_data from {data_filename}", "green")) + else: + print(colored(f"Error: File {data_filename} does not exist.", "red")) + sys.exit(1) + + if all_data is None: + all_data = {} + + for server_name, server in servers.items(): + pid = server["pid"] + address = server["address"] + + print(colored(f"🏃 Measuring {server_name} 🏃 (will take {duration} seconds)", "magenta")) + output_path = tempfile.NamedTemporaryFile(delete=False, suffix='.csv').name + + perf_cmd = [ + "perf", "stat", + "--event", PERF_EVENTS_STRING, + "--field-separator", ",", + "--output", output_path, + "--pid", str(pid), + "--delay", str(warmup*1000), + "--repeat", str(repeat), + "--", + "ssh", "brat", + ] + gen_h2load_cmd(address) + if PROTO == "tls": + perf_cmd += ["--alpn-list", "h2"] + + subprocess.run(perf_cmd, preexec_fn=set_pdeathsig, check=True) + + # Read the CSV file, skipping comments and empty lines + data = [] + with open(output_path, 'r') as f: + for line in f: + if not line.startswith('#') and line.strip(): + print(f"Using CSV line: {line}") + + # Split the line by comma and strip whitespace + row = [item.strip() for item in line.split(',')] + + # Ensure we have exactly 4 columns + if len(row) >= 4: + data.append({ + 'value': row[0], + 'event': row[2], + 'stddev': row[3] + }) + + print("Parsed CSV data:") + pp = pprint.PrettyPrinter(indent=4) + pp.pprint(data) + + all_data[server_name] = data + + # Save `all_data` to disk + import pickle + with open(data_filename, 'wb') as f: + pickle.dump(all_data, f) + print(colored(f"Saved all_data to {data_filename}", "green")) + + print("All data:") + pp = pprint.PrettyPrinter(indent=4) + pp.pprint(all_data) + + # Create a new workbook and select the active sheet + wb = openpyxl.Workbook() + + ws = wb.active + + if ws is None: + print(colored("Error: Could not find active sheet", "red")) + sys.exit(1) + + default_font = Font(name='Courier New', size=11) + bold_font = Font(name='Courier New', size=11, bold=True) + + # Initialize row counter + current_row = 1 + + # Add informative rows + informative_data = f"date: {time.strftime('%Y-%m-%d')}, loona rev: {loona_git_sha}" + ws.cell(row=current_row, column=1, value=informative_data) + ws.merge_cells(start_row=current_row, start_column=1, end_row=current_row, end_column=8) + merged_cell = ws.cell(row=current_row, column=1) + merged_cell.alignment = Alignment(horizontal='left') + merged_cell.font = bold_font + current_row += 1 + + informative_data = f"{endpoint} over {PROTO}, {repeat} runs each, {rps} reqs/s" + ws.cell(row=current_row, column=1, value=informative_data) + ws.merge_cells(start_row=current_row, start_column=1, end_row=current_row, end_column=8) + merged_cell = ws.cell(row=current_row, column=1) + merged_cell.alignment = Alignment(horizontal='left') + merged_cell.font = bold_font + current_row += 1 + + # Add informative rows + informative_data = f"{clients} clients with {streams} streams each, {duration}s total duration (including {warmup}s warmup)" + ws.cell(row=current_row, column=1, value=informative_data) + ws.merge_cells(start_row=current_row, start_column=1, end_row=current_row, end_column=8) + merged_cell = ws.cell(row=current_row, column=1) + merged_cell.alignment = Alignment(horizontal='left') + merged_cell.font = bold_font + current_row += 1 + + last_informative_row = current_row + + # Add an empty row for spacing + current_row += 1 + + # Add header row + headers = ["event", "hyper", "", "loona", ""] + for col, header in enumerate(headers, start=1): + cell = ws.cell(row=current_row, column=col, value=header) + cell.font = bold_font + + # Merge cells for "hyper" and "loona" + ws.merge_cells(start_row=current_row, start_column=2, end_row=current_row, end_column=3) + ws.merge_cells(start_row=current_row, start_column=4, end_row=current_row, end_column=5) + + # Set alignment for merged cells + ws.cell(row=current_row, column=2).alignment = Alignment(horizontal='center') + ws.cell(row=current_row, column=4).alignment = Alignment(horizontal='center') + + # Add subheader row + current_row += 1 + subheaders = ["", "value", "stddev", "value", "stddev"] + for col, subheader in enumerate(subheaders, start=1): + cell = ws.cell(row=current_row, column=col, value=subheader) + cell.alignment = Alignment(horizontal='center') + cell.font = Font(bold=True) + + # Center-align the merged cells + for col in [2, 4]: + cell = ws.cell(row=current_row-1, column=col) + cell.alignment = Alignment(horizontal='center') + + # Process and add data rows + current_row += 1 + for event in PERF_EVENTS: + event_name = event.split(':')[-1] if ':' in event else event + ws.cell(row=current_row, column=1, value=event_name) + + for server in ['hyper', 'loona']: + col = 2 if server == 'hyper' else 4 + if server in all_data: + server_data = next((item for item in all_data[server] if item['event'] == event), None) + if server_data: + value_cell = ws.cell(row=current_row, column=col, value=float(server_data['value'].replace(',', ''))) + if event_name in ['cycles', 'instructions', 'branches', 'branch-misses', 'cache-references', 'cache-misses']: + value_cell.number_format = '#,##0.0,, "B"' + else: + value_cell.number_format = '#,##0' + + stddev = float(server_data['stddev'].rstrip('%')) / 100 + stddev_cell = ws.cell(row=current_row, column=col+1, value=stddev) + stddev_cell.number_format = '0.00%' + + current_row += 1 + + # Adjust column widths + for col in ws.columns: + max_length = 0 + column = None + for cell in col[last_informative_row+1:]: # Skip all informative rows + if cell.value is not None: + try: + cell_length = len(str(cell.value)) + if cell_length > max_length: + max_length = cell_length + column = cell.column_letter + except TypeError: + # Handle cases where len() is not applicable + pass + adjusted_width = (max_length + 2) + if column is not None: + ws.column_dimensions[column].width = adjusted_width + + for row in ws.iter_rows(min_row=1, max_row=ws.max_row, min_col=1, max_col=ws.max_column): + for cell in row: + cell.font = default_font + + # Save the workbook + excel_filename = f"/tmp/loona-perfstat/combined_performance.xlsx" + wb.save(excel_filename) + print(colored(f"Excel file saved as {excel_filename}", "green")) + +try: + if mode == 'perfstat': + do_perfstat() + elif mode == 'samply': + do_samply() + else: + print(colored(f"Error: Unknown mode '{mode}'", "red")) + print(colored("Known modes:", "yellow")) + print(colored("- perfstat", "yellow")) + print(colored("- samply", "yellow")) + sys.exit(1) +except KeyboardInterrupt: + print(colored("\nKeyboard interrupt detected. Cleaning up...", "red")) + abort_and_cleanup(None, None) + +print(colored("🎉 All done! Bye bye! 👋", "cyan")) +# kill_group_except_self() diff --git a/scripts/perfstat.sh b/scripts/perfstat.sh deleted file mode 100755 index 57cee3e3..00000000 --- a/scripts/perfstat.sh +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env -S bash -euo pipefail - -. /root/.cargo/env - -# Change to the script's directory -cd "$(dirname "$0")" - -# Create a new process group -set -m - -# Set trap to kill the process group on script exit -trap 'kill -TERM -$$' EXIT - -# Create directory if it doesn't exist -mkdir -p /tmp/loona-perfstat - -# Kill older processes -for pidfile in /tmp/loona-perfstat/*.PID; do - if [ -f "$pidfile" ]; then - pid=$(cat "$pidfile") - if [ "$pid" != "$$" ]; then - kill "$pid" 2>/dev/null || true - fi - rm -f "$pidfile" - fi -done - -#PERF_EVENTS="cpu-clock,context-switches,cycles,instructions,branches,branch-misses,cache-references,cache-misses,page-faults,$(paste -sd ',' syscalls)" -PERF_EVENTS="cpu-clock,cycles,branch-misses,cache-misses,page-faults,$(paste -sd ',' syscalls)" - -LOONA_DIR=~/bearcove/loona - -# Build the servers -cargo build --release --manifest-path="$LOONA_DIR/Cargo.toml" -F tracing/release_max_level_info - -# Set protocol, default to h2c -PROTO=${PROTO:-h2c} -export PROTO - -OUR_PUBLIC_IP=$(curl -4 ifconfig.me) -if [[ "$PROTO" == "tls" ]]; then - HTTP_OR_HTTPS="https" -else - HTTP_OR_HTTPS="http" -fi - -# Launch hyper server -ADDR=0.0.0.0 PORT=8001 "$LOONA_DIR/target/release/httpwg-hyper" & -HYPER_PID=$! -echo $HYPER_PID > /tmp/loona-perfstat/hyper.PID -echo "hyper PID: $HYPER_PID" - -# Launch loona server -ADDR=0.0.0.0 PORT=8002 "$LOONA_DIR/target/release/httpwg-loona" & -LOONA_PID=$! -echo $LOONA_PID > /tmp/loona-perfstat/loona.PID -echo "loona PID: $LOONA_PID" - -HYPER_ADDR="${HTTP_OR_HTTPS}://${OUR_PUBLIC_IP}:8001" -LOONA_ADDR="${HTTP_OR_HTTPS}://${OUR_PUBLIC_IP}:8002" - -# Declare h2load args based on PROTO -declare -a H2LOAD_ARGS -if [[ "$PROTO" == "h1" ]]; then - echo "Error: h1 is not supported" - exit 1 -elif [[ "$PROTO" == "h2c" ]]; then - H2LOAD_ARGS=() -elif [[ "$PROTO" == "tls" ]]; then - ALPN_LIST=${ALPN_LIST:-"h2,http/1.1"} - H2LOAD_ARGS=(--alpn-list="$ALPN_LIST") -else - echo "Error: Unknown PROTO '$PROTO'" - exit 1 -fi - -declare -A servers=( - [hyper]="$HYPER_PID $HYPER_ADDR" - [loona]="$LOONA_PID $LOONA_ADDR" -) - -if [[ -n "${SERVER:-}" ]]; then - # If SERVER is set, only benchmark that one - if [[ -v "servers[$SERVER]" ]]; then - servers=([${SERVER}]="${servers[$SERVER]}") - else - echo "Error: SERVER '$SERVER' not found in the list of servers." - exit 1 - fi -fi - -H2LOAD="/nix/var/nix/profiles/default/bin/h2load" - -ENDPOINT="${ENDPOINT:-/repeat-4k-blocks/128}" -RPS="${RPS:-2}" -CONNS="${CONNS:-40}" -STREAMS="${STREAMS:-8}" -WARM_UP_TIME="${WARM_UP_TIME:-5}" -DURATION="${DURATION:-20}" -TIMES="${TIMES:-1}" - -# Set MODE to 'stat' if not specified -MODE=${MODE:-stat} - -if [[ "$MODE" == "record" ]]; then - PERF_CMD="perf record -F 99 -e $PERF_EVENTS -p" -elif [[ "$MODE" == "stat" ]]; then - PERF_CMD="perf stat -e $PERF_EVENTS -p" -else - echo "Error: Unknown MODE '$MODE'" - exit 1 -fi - -echo -e "\033[1;34m📊 Benchmark parameters: RPS=$RPS, CONNS=$CONNS, STREAMS=$STREAMS, WARM_UP_TIME=$WARM_UP_TIME, DURATION=$DURATION, TIMES=$TIMES\033[0m" - -for server in "${!servers[@]}"; do - read -r PID ADDR <<< "${servers[$server]}" - echo -e "\033[1;36mLoona Git SHA: $(cd ~/bearcove/loona && git rev-parse --short HEAD)\033[0m" - echo -e "\033[1;33m🚀 Benchmarking \033[1;32m$(cat /proc/$PID/cmdline | tr '\0' ' ')\033[0m" - remote_command=("$H2LOAD" "${H2LOAD_ARGS[@]}" --rps "$RPS" -c "$CONNS" -m "$STREAMS" --duration "$DURATION" "${ADDR}${ENDPOINT}") - - if [[ "$MODE" == "record" ]]; then - samply record -p "$PID" & - SAMPLY_PID=$! - echo $SAMPLY_PID > /tmp/loona-perfstat/samply.PID - ssh brat "${remote_command[@]}" - kill -INT $SAMPLY_PID - wait $SAMPLY_PID - else - for ((i=1; i<=$TIMES; i++)); do - echo "===================================================" - echo -e "\033[1;35m🏃 Run $i of $TIMES for $server server 🏃\033[0m" - echo "===================================================" - ssh brat "${remote_command[@]}" & - SSH_PID=$! - sleep "$WARM_UP_TIME" - ACTUAL_DURATION=$((DURATION - WARM_UP_TIME - 1)) - echo "Starting perf for $ACTUAL_DURATION seconds..." - perf stat -e "$PERF_EVENTS" -p "$PID" -- sleep "$ACTUAL_DURATION" - echo "Starting perf... done!" - wait $SSH_PID - done - fi -done diff --git a/scripts/syscalls b/scripts/syscalls deleted file mode 100644 index 81e6fc06..00000000 --- a/scripts/syscalls +++ /dev/null @@ -1,4 +0,0 @@ -syscalls:sys_enter_write -syscalls:sys_enter_writev -syscalls:sys_enter_epoll_wait -syscalls:sys_enter_io_uring_enter diff --git a/scripts/uv.lock b/scripts/uv.lock new file mode 100644 index 00000000..cf786605 --- /dev/null +++ b/scripts/uv.lock @@ -0,0 +1,187 @@ +version = 1 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version < '3.11'", + "python_full_version == '3.11.*'", + "python_full_version >= '3.12'", +] + +[[package]] +name = "bencimy-projec" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "openpyxl" }, + { name = "pandas" }, + { name = "termcolor" }, +] + +[package.metadata] +requires-dist = [ + { name = "openpyxl", specifier = "~=3.1.5" }, + { name = "pandas", specifier = "~=2.2.2" }, + { name = "termcolor", specifier = "~=2.4.0" }, +] + +[[package]] +name = "et-xmlfile" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/5d/0413a31d184a20c763ad741cc7852a659bf15094c24840c5bdd1754765cd/et_xmlfile-1.1.0.tar.gz", hash = "sha256:8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c", size = 3218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/c2/3dd434b0108730014f1b96fd286040dc3bcb70066346f7e01ec2ac95865f/et_xmlfile-1.1.0-py3-none-any.whl", hash = "sha256:a2ba85d1d6a74ef63837eed693bcb89c3f752169b0e3e7ae5b16ca5e1b3deada", size = 4688 }, +] + +[[package]] +name = "numpy" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/91/3495b3237510f79f5d81f2508f9f13fea78ebfdf07538fc7444badda173d/numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece", size = 21165245 }, + { url = "https://files.pythonhosted.org/packages/05/33/26178c7d437a87082d11019292dce6d3fe6f0e9026b7b2309cbf3e489b1d/numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04", size = 13738540 }, + { url = "https://files.pythonhosted.org/packages/ec/31/cc46e13bf07644efc7a4bf68df2df5fb2a1a88d0cd0da9ddc84dc0033e51/numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66", size = 5300623 }, + { url = "https://files.pythonhosted.org/packages/6e/16/7bfcebf27bb4f9d7ec67332ffebee4d1bf085c84246552d52dbb548600e7/numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b", size = 6901774 }, + { url = "https://files.pythonhosted.org/packages/f9/a3/561c531c0e8bf082c5bef509d00d56f82e0ea7e1e3e3a7fc8fa78742a6e5/numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd", size = 13907081 }, + { url = "https://files.pythonhosted.org/packages/fa/66/f7177ab331876200ac7563a580140643d1179c8b4b6a6b0fc9838de2a9b8/numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318", size = 19523451 }, + { url = "https://files.pythonhosted.org/packages/25/7f/0b209498009ad6453e4efc2c65bcdf0ae08a182b2b7877d7ab38a92dc542/numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8", size = 19927572 }, + { url = "https://files.pythonhosted.org/packages/3e/df/2619393b1e1b565cd2d4c4403bdd979621e2c4dea1f8532754b2598ed63b/numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326", size = 14400722 }, + { url = "https://files.pythonhosted.org/packages/22/ad/77e921b9f256d5da36424ffb711ae79ca3f451ff8489eeca544d0701d74a/numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97", size = 6472170 }, + { url = "https://files.pythonhosted.org/packages/10/05/3442317535028bc29cf0c0dd4c191a4481e8376e9f0db6bcf29703cadae6/numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131", size = 15905558 }, + { url = "https://files.pythonhosted.org/packages/8b/cf/034500fb83041aa0286e0fb16e7c76e5c8b67c0711bb6e9e9737a717d5fe/numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448", size = 21169137 }, + { url = "https://files.pythonhosted.org/packages/4a/d9/32de45561811a4b87fbdee23b5797394e3d1504b4a7cf40c10199848893e/numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195", size = 13703552 }, + { url = "https://files.pythonhosted.org/packages/c1/ca/2f384720020c7b244d22508cb7ab23d95f179fcfff33c31a6eeba8d6c512/numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57", size = 5298957 }, + { url = "https://files.pythonhosted.org/packages/0e/78/a3e4f9fb6aa4e6fdca0c5428e8ba039408514388cf62d89651aade838269/numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a", size = 6905573 }, + { url = "https://files.pythonhosted.org/packages/a0/72/cfc3a1beb2caf4efc9d0b38a15fe34025230da27e1c08cc2eb9bfb1c7231/numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669", size = 13914330 }, + { url = "https://files.pythonhosted.org/packages/ba/a8/c17acf65a931ce551fee11b72e8de63bf7e8a6f0e21add4c937c83563538/numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951", size = 19534895 }, + { url = "https://files.pythonhosted.org/packages/ba/86/8767f3d54f6ae0165749f84648da9dcc8cd78ab65d415494962c86fac80f/numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9", size = 19937253 }, + { url = "https://files.pythonhosted.org/packages/df/87/f76450e6e1c14e5bb1eae6836478b1028e096fd02e85c1c37674606ab752/numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15", size = 14414074 }, + { url = "https://files.pythonhosted.org/packages/5c/ca/0f0f328e1e59f73754f06e1adfb909de43726d4f24c6a3f8805f34f2b0fa/numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4", size = 6470640 }, + { url = "https://files.pythonhosted.org/packages/eb/57/3a3f14d3a759dcf9bf6e9eda905794726b758819df4663f217d658a58695/numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc", size = 15910230 }, + { url = "https://files.pythonhosted.org/packages/45/40/2e117be60ec50d98fa08c2f8c48e09b3edea93cfcabd5a9ff6925d54b1c2/numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b", size = 20895803 }, + { url = "https://files.pythonhosted.org/packages/46/92/1b8b8dee833f53cef3e0a3f69b2374467789e0bb7399689582314df02651/numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e", size = 13471835 }, + { url = "https://files.pythonhosted.org/packages/7f/19/e2793bde475f1edaea6945be141aef6c8b4c669b90c90a300a8954d08f0a/numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c", size = 5038499 }, + { url = "https://files.pythonhosted.org/packages/e3/ff/ddf6dac2ff0dd50a7327bcdba45cb0264d0e96bb44d33324853f781a8f3c/numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c", size = 6633497 }, + { url = "https://files.pythonhosted.org/packages/72/21/67f36eac8e2d2cd652a2e69595a54128297cdcb1ff3931cfc87838874bd4/numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692", size = 13621158 }, + { url = "https://files.pythonhosted.org/packages/39/68/e9f1126d757653496dbc096cb429014347a36b228f5a991dae2c6b6cfd40/numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a", size = 19236173 }, + { url = "https://files.pythonhosted.org/packages/d1/e9/1f5333281e4ebf483ba1c888b1d61ba7e78d7e910fdd8e6499667041cc35/numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c", size = 19634174 }, + { url = "https://files.pythonhosted.org/packages/71/af/a469674070c8d8408384e3012e064299f7a2de540738a8e414dcfd639996/numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded", size = 14099701 }, + { url = "https://files.pythonhosted.org/packages/d0/3d/08ea9f239d0e0e939b6ca52ad403c84a2bce1bde301a8eb4888c1c1543f1/numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5", size = 6174313 }, + { url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179 }, + { url = "https://files.pythonhosted.org/packages/43/c1/41c8f6df3162b0c6ffd4437d729115704bd43363de0090c7f913cfbc2d89/numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c", size = 21169942 }, + { url = "https://files.pythonhosted.org/packages/39/bc/fd298f308dcd232b56a4031fd6ddf11c43f9917fbc937e53762f7b5a3bb1/numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd", size = 13711512 }, + { url = "https://files.pythonhosted.org/packages/96/ff/06d1aa3eeb1c614eda245c1ba4fb88c483bee6520d361641331872ac4b82/numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b", size = 5306976 }, + { url = "https://files.pythonhosted.org/packages/2d/98/121996dcfb10a6087a05e54453e28e58694a7db62c5a5a29cee14c6e047b/numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729", size = 6906494 }, + { url = "https://files.pythonhosted.org/packages/15/31/9dffc70da6b9bbf7968f6551967fc21156207366272c2a40b4ed6008dc9b/numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1", size = 13912596 }, + { url = "https://files.pythonhosted.org/packages/b9/14/78635daab4b07c0930c919d451b8bf8c164774e6a3413aed04a6d95758ce/numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd", size = 19526099 }, + { url = "https://files.pythonhosted.org/packages/26/4c/0eeca4614003077f68bfe7aac8b7496f04221865b3a5e7cb230c9d055afd/numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d", size = 19932823 }, + { url = "https://files.pythonhosted.org/packages/f1/46/ea25b98b13dccaebddf1a803f8c748680d972e00507cd9bc6dcdb5aa2ac1/numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d", size = 14404424 }, + { url = "https://files.pythonhosted.org/packages/c8/a6/177dd88d95ecf07e722d21008b1b40e681a929eb9e329684d449c36586b2/numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa", size = 6476809 }, + { url = "https://files.pythonhosted.org/packages/ea/2b/7fc9f4e7ae5b507c1a3a21f0f15ed03e794c1242ea8a242ac158beb56034/numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73", size = 15911314 }, + { url = "https://files.pythonhosted.org/packages/8f/3b/df5a870ac6a3be3a86856ce195ef42eec7ae50d2a202be1f5a4b3b340e14/numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8", size = 21025288 }, + { url = "https://files.pythonhosted.org/packages/2c/97/51af92f18d6f6f2d9ad8b482a99fb74e142d71372da5d834b3a2747a446e/numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4", size = 6762793 }, + { url = "https://files.pythonhosted.org/packages/12/46/de1fbd0c1b5ccaa7f9a005b66761533e2f6a3e560096682683a223631fe9/numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c", size = 19334885 }, + { url = "https://files.pythonhosted.org/packages/cc/dc/d330a6faefd92b446ec0f0dfea4c3207bb1fef3c4771d19cf4543efd2c78/numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385", size = 15828784 }, +] + +[[package]] +name = "openpyxl" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "et-xmlfile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910 }, +] + +[[package]] +name = "pandas" +version = "2.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/d9/ecf715f34c73ccb1d8ceb82fc01cd1028a65a5f6dbc57bfa6ea155119058/pandas-2.2.2.tar.gz", hash = "sha256:9e79019aba43cb4fda9e4d983f8e88ca0373adbb697ae9c6c43093218de28b54", size = 4398391 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/2d/39600d073ea70b9cafdc51fab91d69c72b49dd92810f24cb5ac6631f387f/pandas-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90c6fca2acf139569e74e8781709dccb6fe25940488755716d1d354d6bc58bce", size = 12551798 }, + { url = "https://files.pythonhosted.org/packages/fd/4b/0cd38e68ab690b9df8ef90cba625bf3f93b82d1c719703b8e1b333b2c72d/pandas-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7adfc142dac335d8c1e0dcbd37eb8617eac386596eb9e1a1b77791cf2498238", size = 11287392 }, + { url = "https://files.pythonhosted.org/packages/01/c6/d3d2612aea9b9f28e79a30b864835dad8f542dcf474eee09afeee5d15d75/pandas-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4abfe0be0d7221be4f12552995e58723c7422c80a659da13ca382697de830c08", size = 15634823 }, + { url = "https://files.pythonhosted.org/packages/89/1b/12521efcbc6058e2673583bb096c2b5046a9df39bd73eca392c1efed24e5/pandas-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8635c16bf3d99040fdf3ca3db669a7250ddf49c55dc4aa8fe0ae0fa8d6dcc1f0", size = 13032214 }, + { url = "https://files.pythonhosted.org/packages/e4/d7/303dba73f1c3a9ef067d23e5afbb6175aa25e8121be79be354dcc740921a/pandas-2.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:40ae1dffb3967a52203105a077415a86044a2bea011b5f321c6aa64b379a3f51", size = 16278302 }, + { url = "https://files.pythonhosted.org/packages/ba/df/8ff7c5ed1cc4da8c6ab674dc8e4860a4310c3880df1283e01bac27a4333d/pandas-2.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8e5a0b00e1e56a842f922e7fae8ae4077aee4af0acb5ae3622bd4b4c30aedf99", size = 13892866 }, + { url = "https://files.pythonhosted.org/packages/69/a6/81d5dc9a612cf0c1810c2ebc4f2afddb900382276522b18d128213faeae3/pandas-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:ddf818e4e6c7c6f4f7c8a12709696d193976b591cc7dc50588d3d1a6b5dc8772", size = 11621592 }, + { url = "https://files.pythonhosted.org/packages/1b/70/61704497903d43043e288017cb2b82155c0d41e15f5c17807920877b45c2/pandas-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:696039430f7a562b74fa45f540aca068ea85fa34c244d0deee539cb6d70aa288", size = 12574808 }, + { url = "https://files.pythonhosted.org/packages/16/c6/75231fd47afd6b3f89011e7077f1a3958441264aca7ae9ff596e3276a5d0/pandas-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e90497254aacacbc4ea6ae5e7a8cd75629d6ad2b30025a4a8b09aa4faf55151", size = 11304876 }, + { url = "https://files.pythonhosted.org/packages/97/2d/7b54f80b93379ff94afb3bd9b0cd1d17b48183a0d6f98045bc01ce1e06a7/pandas-2.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58b84b91b0b9f4bafac2a0ac55002280c094dfc6402402332c0913a59654ab2b", size = 15602548 }, + { url = "https://files.pythonhosted.org/packages/fc/a5/4d82be566f069d7a9a702dcdf6f9106df0e0b042e738043c0cc7ddd7e3f6/pandas-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2123dc9ad6a814bcdea0f099885276b31b24f7edf40f6cdbc0912672e22eee", size = 13031332 }, + { url = "https://files.pythonhosted.org/packages/92/a2/b79c48f530673567805e607712b29814b47dcaf0d167e87145eb4b0118c6/pandas-2.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2925720037f06e89af896c70bca73459d7e6a4be96f9de79e2d440bd499fe0db", size = 16286054 }, + { url = "https://files.pythonhosted.org/packages/40/c7/47e94907f1d8fdb4868d61bd6c93d57b3784a964d52691b77ebfdb062842/pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1", size = 13879507 }, + { url = "https://files.pythonhosted.org/packages/ab/63/966db1321a0ad55df1d1fe51505d2cdae191b84c907974873817b0a6e849/pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24", size = 11634249 }, + { url = "https://files.pythonhosted.org/packages/dd/49/de869130028fb8d90e25da3b7d8fb13e40f5afa4c4af1781583eb1ff3839/pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef", size = 12500886 }, + { url = "https://files.pythonhosted.org/packages/db/7c/9a60add21b96140e22465d9adf09832feade45235cd22f4cb1668a25e443/pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce", size = 11340320 }, + { url = "https://files.pythonhosted.org/packages/b0/85/f95b5f322e1ae13b7ed7e97bd999160fa003424711ab4dc8344b8772c270/pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad", size = 15204346 }, + { url = "https://files.pythonhosted.org/packages/40/10/79e52ef01dfeb1c1ca47a109a01a248754ebe990e159a844ece12914de83/pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad", size = 12733396 }, + { url = "https://files.pythonhosted.org/packages/35/9d/208febf8c4eb5c1d9ea3314d52d8bd415fd0ef0dd66bb24cc5bdbc8fa71a/pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76", size = 15858913 }, + { url = "https://files.pythonhosted.org/packages/99/d1/2d9bd05def7a9e08a92ec929b5a4c8d5556ec76fae22b0fa486cbf33ea63/pandas-2.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32", size = 13417786 }, + { url = "https://files.pythonhosted.org/packages/22/a5/a0b255295406ed54269814bc93723cfd1a0da63fb9aaf99e1364f07923e5/pandas-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23", size = 11498828 }, + { url = "https://files.pythonhosted.org/packages/1b/cc/eb6ce83667131667c6561e009823e72aa5c76698e75552724bdfc8d1ef0b/pandas-2.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0ca6377b8fca51815f382bd0b697a0814c8bda55115678cbc94c30aacbb6eff2", size = 12566406 }, + { url = "https://files.pythonhosted.org/packages/96/08/9ad65176f854fd5eb806a27da6e8b6c12d5ddae7ef3bd80d8b3009099333/pandas-2.2.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9057e6aa78a584bc93a13f0a9bf7e753a5e9770a30b4d758b8d5f2a62a9433cd", size = 11304008 }, + { url = "https://files.pythonhosted.org/packages/aa/30/5987c82fea318ac7d6bcd083c5b5259d4000e99dd29ae7a9357c65a1b17a/pandas-2.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:001910ad31abc7bf06f49dcc903755d2f7f3a9186c0c040b827e522e9cef0863", size = 15662279 }, + { url = "https://files.pythonhosted.org/packages/bb/30/f6f1f1ac36250f50c421b1b6af08c35e5a8b5a84385ef928625336b93e6f/pandas-2.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921", size = 13069490 }, + { url = "https://files.pythonhosted.org/packages/b5/27/76c1509f505d1f4cb65839352d099c90a13019371e90347166811aa6a075/pandas-2.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a77e9d1c386196879aa5eb712e77461aaee433e54c68cf253053a73b7e49c33a", size = 16299412 }, + { url = "https://files.pythonhosted.org/packages/5d/11/a5a2f52936fba3afc42de35b19cae941284d973649cb6949bc41cc2e5901/pandas-2.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92fd6b027924a7e178ac202cfbe25e53368db90d56872d20ffae94b96c7acc57", size = 13920884 }, + { url = "https://files.pythonhosted.org/packages/bf/2c/a0cee9c392a4c9227b835af27f9260582b994f9a2b5ec23993b596e5deb7/pandas-2.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:640cef9aa381b60e296db324337a554aeeb883ead99dc8f6c18e81a93942f5f4", size = 11637580 }, +] + +[[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 = "2024.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/26/9f1f00a5d021fff16dee3de13d43e5e978f3d58928e129c3a62cf7eb9738/pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812", size = 316214 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/3d/a121f284241f08268b21359bd425f7d4825cffc5ac5cd0e1b3d82ffd2b10/pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319", size = 505474 }, +] + +[[package]] +name = "six" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, +] + +[[package]] +name = "termcolor" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/10/56/d7d66a84f96d804155f6ff2873d065368b25a07222a6fd51c4f24ef6d764/termcolor-2.4.0.tar.gz", hash = "sha256:aab9e56047c8ac41ed798fa36d892a37aca6b3e9159f3e0c24bc64a9b3ac7b7a", size = 12664 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/5f/8c716e47b3a50cbd7c146f45881e11d9414def768b7cd9c5e6650ec2a80a/termcolor-2.4.0-py3-none-any.whl", hash = "sha256:9297c0df9c99445c2412e832e882a7884038a25617c60cea2ad69488d4040d63", size = 7719 }, +] + +[[package]] +name = "tzdata" +version = "2024.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/5b/e025d02cb3b66b7b76093404392d4b44343c69101cc85f4d180dd5784717/tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd", size = 190559 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/58/f9c9e6be752e9fcb8b6a0ee9fb87e6e7a1f6bcab2cdc73f02bb7ba91ada0/tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252", size = 345370 }, +] diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..9230540d --- /dev/null +++ b/uv.lock @@ -0,0 +1,71 @@ +version = 1 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version < '3.11'", + "python_full_version == '3.11.*'", + "python_full_version >= '3.12'", +] + +[[package]] +name = "et-xmlfile" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/5d/0413a31d184a20c763ad741cc7852a659bf15094c24840c5bdd1754765cd/et_xmlfile-1.1.0.tar.gz", hash = "sha256:8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c", size = 3218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/c2/3dd434b0108730014f1b96fd286040dc3bcb70066346f7e01ec2ac95865f/et_xmlfile-1.1.0-py3-none-any.whl", hash = "sha256:a2ba85d1d6a74ef63837eed693bcb89c3f752169b0e3e7ae5b16ca5e1b3deada", size = 4688 }, +] + +[[package]] +name = "loona" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "openpyxl" }, + { name = "psutil" }, + { name = "termcolor" }, +] + +[package.metadata] +requires-dist = [ + { name = "openpyxl", specifier = "~=3.1.5" }, + { name = "psutil", specifier = ">=6.0.0" }, + { name = "termcolor", specifier = "~=2.4.0" }, +] + +[[package]] +name = "openpyxl" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "et-xmlfile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910 }, +] + +[[package]] +name = "psutil" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/c7/8c6872f7372eb6a6b2e4708b88419fb46b857f7a2e1892966b851cc79fc9/psutil-6.0.0.tar.gz", hash = "sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2", size = 508067 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/66/78c9c3020f573c58101dc43a44f6855d01bbbd747e24da2f0c4491200ea3/psutil-6.0.0-cp27-none-win32.whl", hash = "sha256:02b69001f44cc73c1c5279d02b30a817e339ceb258ad75997325e0e6169d8b35", size = 249766 }, + { url = "https://files.pythonhosted.org/packages/e1/3f/2403aa9558bea4d3854b0e5e567bc3dd8e9fbc1fc4453c0aa9aafeb75467/psutil-6.0.0-cp27-none-win_amd64.whl", hash = "sha256:21f1fb635deccd510f69f485b87433460a603919b45e2a324ad65b0cc74f8fb1", size = 253024 }, + { url = "https://files.pythonhosted.org/packages/0b/37/f8da2fbd29690b3557cca414c1949f92162981920699cd62095a984983bf/psutil-6.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0", size = 250961 }, + { url = "https://files.pythonhosted.org/packages/35/56/72f86175e81c656a01c4401cd3b1c923f891b31fbcebe98985894176d7c9/psutil-6.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0", size = 287478 }, + { url = "https://files.pythonhosted.org/packages/19/74/f59e7e0d392bc1070e9a70e2f9190d652487ac115bb16e2eff6b22ad1d24/psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd", size = 290455 }, + { url = "https://files.pythonhosted.org/packages/cd/5f/60038e277ff0a9cc8f0c9ea3d0c5eb6ee1d2470ea3f9389d776432888e47/psutil-6.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132", size = 292046 }, + { url = "https://files.pythonhosted.org/packages/8b/20/2ff69ad9c35c3df1858ac4e094f20bd2374d33c8643cf41da8fd7cdcb78b/psutil-6.0.0-cp37-abi3-win32.whl", hash = "sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d", size = 253560 }, + { url = "https://files.pythonhosted.org/packages/73/44/561092313ae925f3acfaace6f9ddc4f6a9c748704317bad9c8c8f8a36a79/psutil-6.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3", size = 257399 }, + { url = "https://files.pythonhosted.org/packages/7c/06/63872a64c312a24fb9b4af123ee7007a306617da63ff13bcc1432386ead7/psutil-6.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0", size = 251988 }, +] + +[[package]] +name = "termcolor" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/10/56/d7d66a84f96d804155f6ff2873d065368b25a07222a6fd51c4f24ef6d764/termcolor-2.4.0.tar.gz", hash = "sha256:aab9e56047c8ac41ed798fa36d892a37aca6b3e9159f3e0c24bc64a9b3ac7b7a", size = 12664 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/5f/8c716e47b3a50cbd7c146f45881e11d9414def768b7cd9c5e6650ec2a80a/termcolor-2.4.0-py3-none-any.whl", hash = "sha256:9297c0df9c99445c2412e832e882a7884038a25617c60cea2ad69488d4040d63", size = 7719 }, +]