diff --git a/README.md b/README.md index dcfb807..6956810 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ Shepherd is designed to manage local workflows that involve both actions that ru Shepherd is particularly useful for applications that require persistent services as part of a traditional task-based workflow. For example, consider a scenario where a web server should start only after the database service has completed its initialization and is ready to handle queries. The database does not perform a single action that completes; instead, it reaches an internal state that should dynamically trigger the launch of the web server. Shepherd can monitor the database service's logs for a message indicating successful startup. Upon detecting this state, Shepherd triggers the initiation of the web server, ensuring efficient workflow execution. Moreover, Shepherd wraps the entire service workflow into a single task that terminates upon completion, making it easy to integrate into larger task-based workflows. ## Key Features + - **Services as Tasks:** Shepherd treats persistent services as first-class tasks within a workflow, enabling them to be seamlessly integrated into traditional task-based workflow managers. - **Dependency Management:** Shepherd allows tasks (both actions and services) to start based on the internal states of other services or actions, enabling complex state-based dependencies. It supports `any` and `all` dependency modes, allowing for flexible dependency configurations. @@ -17,37 +18,42 @@ Shepherd is particularly useful for applications that require persistent service - **Integration with Larger Workflows:** By encapsulating service workflows into single tasks, Shepherd enables easy integration with larger distributed workflow managers like Makeflow, enhancing workflow flexibility and reliability. - ## Program State Transition Overview The Shepherd tool manages program execution through a series of defined states, ensuring dependencies are met and states are recorded. Every program has default states (`Initialized`, `Started`, and `Final`) and can have optional user-defined states. Programs transition from `Initialized` to `Started` once dependencies are satisfied, then move through user-defined states. Actions return `Action Success` on a zero return code and `Action Failure` otherwise, while services transition to `Service Failure` if they stop unexpectedly. Any program receiving a stop signal is marked as `Stopped`, and all programs ultimately transition to a `Final` state, reflecting their execution outcome. ![Test](diagram/dot/shepherd-state-machine.svg) -## Installation +## Quick start -To install Shepherd, clone the repository and install using pip: +### Using `uv` (easiest and fastest) ```bash git clone https://github.com/cooperative-computing-lab/shepherd.git -cd shepherd -pip install . +uv sync --dev +uv run examples/example3/run_test.sh ``` -Optionally, create a virtual environment before installing Shepherd to avoid conflicts with other Python packages: +### Using pip ```bash -python3 -m venv venv +git clone https://github.com/cooperative-computing-lab/shepherd.git +cd shepherd +python --version # must be >=3.10 +# use uv or pyenv to install a more recent python version if needed + +python -m venv venv source venv/bin/activate pip install . +examples/example3/run_test.sh ``` - ## Getting Started with Shepherd: A Hello World Example -Shepherd simplifies complex application workflows. Here’s a simple example to demonstrate how to use Shepherd for + +Shepherd simplifies complex application workflows. Here’s a simple example to demonstrate how to use Shepherd for scheduling dependent programs. In this example, we have two shell scripts: `program1.sh` and `program2.sh`. `program2` should start only after `program1` has successfully completed its execution. -#### 1. Create Sample Scripts +### 1. Create Sample Scripts Create two shell scripts named `program1.sh` and `program2.sh` with the following content: @@ -60,49 +66,56 @@ echo "Program completed" ``` Make sure to make the scripts executable: - - ```shell - chmod +x program1.sh program2.sh - ``` -#### 2. Create a Shepherd Configuration File -Create a Shepherd configuration file named `shepherd-config.yml` with the following content: +```shell +chmod +x program1.sh program2.sh +``` + +### 2. Create a Shepherd Configuration File + +Create a Shepherd configuration file named `shepherd-config.yaml` with the following content: ```yaml -tasks: - program1: - command: "./program1.sh" - program2: - command: "./program2.sh" - dependency: - items: - program1: "action_success" # Start program2 only after program1 succeeds +services: + program1: + command: "./program1.sh" + program2: + command: "./program2.sh" + dependency: + items: + program1: "action_success" # Start program2 only after program1 succeeds output: - state_times: "state_transition_times.json" + state_times: "state_transition_times.json" max_run_time: 60 # Optional: Limit total runtime to 60 seconds ``` -#### 3. Run Shepherd +### 3. Run Shepherd + Run Shepherd with the configuration file: + ```shell -shepherd -c shepherd-config.yml +shepherd -c shepherd-config.yaml ``` If you are running the python source, then run + ```shell -python3 shepherd.py -c shepherd-config.yml +python3 run_shepherd.py -c shepherd-config.yaml ``` If you are running shepherd executable, then run + ```shell -shepherd -c shepherd-config.yml +shepherd -c shepherd-config.yaml ``` -#### Understanding the workflow +### Understanding the workflow + With this simple configuration, Shepherd will: + 1. Execute `program1.sh`. 2. Monitor the internal states of the program. -3. Start `program2.sh` only after `program1.sh `succeeds. +3. Start `program2.sh` only after `program1.sh` succeeds. 4. Create `state_transition_times.json`, which will look similar to this: ```json @@ -124,9 +137,11 @@ With this simple configuration, Shepherd will: ``` ## Monitoring User-Defined States in Shepherd + Shepherd can monitor standard output (stdout) or any other file to detect user-defined states. These states can then be used as dependencies for other programs. This feature allows you to define complex workflows based on custom application states. -#### Example Scenario: Dynamic Dependencies +### Example Scenario: Dynamic Dependencies + Suppose you have a service that becomes 'ready' after some initialization, and other tasks depend on it being ready. Example Service script (`service.sh`): @@ -141,6 +156,7 @@ tail -f /dev/null # Keep the service running ``` Action script (`action.sh`): + ```bash #!/bin/bash @@ -150,16 +166,17 @@ echo "Action completed" ``` Make sure to make the scripts executable: - - ```shell + + ```shell chmod +x service.sh action.sh ``` -#### Shepherd Configuration with user-defined states +### Shepherd Configuration with user-defined states + Below is a Shepherd configuration file that monitors the standard output of the service script to detect the 'ready' state. The action script starts only after the service is ready. ```yaml -tasks: +services: my_service: type: "service" command: "./service.sh" @@ -177,7 +194,8 @@ output: max_run_time: 60 ``` -#### How This Configuration Works +### How This Configuration Works + 1. Shepherd starts the service script `service.sh`. 2. Shepherd monitors the standard output of the service script for the message "Service is ready". 3. Once the service is ready, Shepherd starts the action script `action.sh`. @@ -202,9 +220,11 @@ max_run_time: 60 ``` ## Configuration Options + Shepherd uses a YAML configuration file to define the workflow. Here are some key configuration options: ### Defining Tasks + Tasks are defined under the tasks section. Each task can be an action or a service: - **Action:** A task that runs to completion and exits. @@ -213,7 +233,7 @@ Tasks are defined under the tasks section. Each task can be an action or a servi The default type is action. Here is an example configuration with an action and a service: ```yaml -tasks: +services: my_action: type: "action" command: "python process_data.py" @@ -223,12 +243,13 @@ tasks: ``` ### Dependencies + Dependencies specify when a task should start, based on the states of other tasks. - **Mode:** Specifies whether all dependencies must be met (`all`, the default) or any one (`any`). ```yaml -tasks: +services: task2: type: "action" command: "./task2.sh" @@ -240,11 +261,13 @@ tasks: ``` ### Monitoring User-Defined States + Shepherd can monitor standard output or files to detect user-defined states. This allows you to control the workflow based on custom application states. Example of monitoring standard output: + ```yaml -tasks: +services: my_program: command: "./my_program.sh" state: @@ -256,7 +279,7 @@ tasks: Example of monitoring a file: ```yaml -tasks: +services: my_task: type: "action" command: "./my_task.sh" @@ -268,6 +291,7 @@ tasks: ``` ### Output Options + Shepherd can generate output files containing state transition times and other logs. You can specify the output file paths in the configuration: ```yaml @@ -278,6 +302,7 @@ output: ``` ### Shutdown Conditions + Shepherd can be configured to stop all tasks based on specific conditions, such as a stop signal, maximum runtime, or success criteria: - **Stop Signal:** A file that, when created, triggers a controlled shutdown. diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 0000000..549d4bd --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1,3 @@ +*.log +**/outputs/** +state_transition_times.json diff --git a/examples/example1/program1.sh b/examples/example1/program1.sh index dbdaf43..f406ffc 100755 --- a/examples/example1/program1.sh +++ b/examples/example1/program1.sh @@ -2,4 +2,4 @@ echo "Starting program..." sleep 5 -echo "Program completed" \ No newline at end of file +echo "Program completed" diff --git a/examples/example1/program2.sh b/examples/example1/program2.sh index dbdaf43..f406ffc 100755 --- a/examples/example1/program2.sh +++ b/examples/example1/program2.sh @@ -2,4 +2,4 @@ echo "Starting program..." sleep 5 -echo "Program completed" \ No newline at end of file +echo "Program completed" diff --git a/examples/example1/shepherd-config.yaml b/examples/example1/shepherd-config.yaml new file mode 100644 index 0000000..ecbbd33 --- /dev/null +++ b/examples/example1/shepherd-config.yaml @@ -0,0 +1,11 @@ +services: + program1: + command: "./program1.sh" + program2: + command: "./program2.sh" + dependency: + items: + program1: "action_success" # Start program2 only after program1 succeeds +output: + state_times: "state_transition_times.json" +max_run_time: 60 # Optional: Limit total runtime to 60 seconds diff --git a/examples/example1/shepherd-config.yml b/examples/example1/shepherd-config.yml deleted file mode 100644 index 0ab2219..0000000 --- a/examples/example1/shepherd-config.yml +++ /dev/null @@ -1,11 +0,0 @@ -tasks: - program1: - command: "./program1.sh" - program2: - command: "./program2.sh" - dependency: - items: - program1: "action_success" # Start program2 only after program1 succeeds -output: - state_times: "state_transition_times.json" -max_run_time: 60 # Optional: Limit total runtime to 60 seconds \ No newline at end of file diff --git a/examples/example2/action.sh b/examples/example2/action.sh index e69d1cb..45f051c 100755 --- a/examples/example2/action.sh +++ b/examples/example2/action.sh @@ -2,4 +2,4 @@ echo "Action is running..." sleep 5 -echo "Action completed" \ No newline at end of file +echo "Action completed" diff --git a/examples/example2/service.sh b/examples/example2/service.sh index 1692ccb..6f338a1 100755 --- a/examples/example2/service.sh +++ b/examples/example2/service.sh @@ -3,4 +3,4 @@ echo "Service is starting..." sleep 5 echo "Service is ready" -tail -f /dev/null # Keep the service running \ No newline at end of file +tail -f /dev/null # Keep the service running diff --git a/examples/example2/shepherd-config.yaml b/examples/example2/shepherd-config.yaml new file mode 100644 index 0000000..11fa129 --- /dev/null +++ b/examples/example2/shepherd-config.yaml @@ -0,0 +1,16 @@ +services: + my_service: + type: "service" + command: "./service.sh" + state: + log: + ready: "Service is ready" + my_action: + type: "action" + command: "./action.sh" + dependency: + items: + my_service: "ready" +output: + state_times: "state_transition_times.json" +max_run_time: 60 diff --git a/examples/example2/shepherd-config.yml b/examples/example2/shepherd-config.yml deleted file mode 100644 index b6e5fae..0000000 --- a/examples/example2/shepherd-config.yml +++ /dev/null @@ -1,16 +0,0 @@ -tasks: - my_service: - type: "service" - command: "./service.sh" - state: - log: - ready: "Service is ready" - my_action: - type: "action" - command: "./action.sh" - dependency: - items: - my_service: "ready" -output: - state_times: "state_transition_times.json" -max_run_time: 60 \ No newline at end of file diff --git a/examples/example3/cleanup.sh b/examples/example3/cleanup.sh index 7c2e0d6..fbfdbdd 100755 --- a/examples/example3/cleanup.sh +++ b/examples/example3/cleanup.sh @@ -1 +1,13 @@ -rm file-created-by-program3.log \ No newline at end of file +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" +cd "${SCRIPT_DIR}" + +rm -f ./*.log +rm -f ./outputs/file-created-by-program3.log +rm -f ./outputs/logs/shepherd.log +rm -f ./state_transition_times.json + +rmdir ./outputs/logs/ || true +rmdir ./outputs/ || true diff --git a/examples/example3/program3.sh b/examples/example3/program3.sh index 85d1add..bd18d2f 100755 --- a/examples/example3/program3.sh +++ b/examples/example3/program3.sh @@ -18,7 +18,7 @@ run_duration=30 while true; do echo "$(date +%s) - program is running" sleep 0.5 - echo "File created by program3" > ./file-created-by-program3.log + echo "File created by program3" >"./outputs/file-created-by-program3.log" if [[ $(date +%s) -gt $((READY_TIME + run_duration)) ]]; then echo "$(date +%s) - program is completed" break diff --git a/examples/example3/run_test.sh b/examples/example3/run_test.sh old mode 100644 new mode 100755 index 39aeb5d..0e66aab --- a/examples/example3/run_test.sh +++ b/examples/example3/run_test.sh @@ -1,7 +1,12 @@ #!/bin/bash +set -euo pipefail echo "Starting test ..." -shepherd -c shepherd-config.yml +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" +echo "Script directory: ${SCRIPT_DIR}" +shepherd --config "${SCRIPT_DIR}/shepherd-config.yaml" \ + --work-dir "${SCRIPT_DIR}" \ + --run-dir "${SCRIPT_DIR}/outputs" echo "Completed test" diff --git a/examples/example3/shepherd-config.yaml b/examples/example3/shepherd-config.yaml new file mode 100644 index 0000000..3d4e599 --- /dev/null +++ b/examples/example3/shepherd-config.yaml @@ -0,0 +1,31 @@ +services: + program1: + command: "./program1.sh" + state: + log: + ready: "program is ready" + complete: "program is completed" + program2: + command: "./program2.sh" + state: + log: + ready: "program is ready" + complete: "program is completed" + dependency: + items: + program1: "ready" + file_dependency: + mode: "all" + items: + - path: "file-created-by-program3.log" + min_size: 1 + program3: + command: "./program3.sh" + state: + log: + ready: "program is ready" + complete: "program is completed" +output: + state_times: "state_transition_times.json" +cleanup_command: "./cleanup.sh" +max_run_time: 120 diff --git a/examples/example3/shepherd-config.yml b/examples/example3/shepherd-config.yml deleted file mode 100644 index c3c2b0c..0000000 --- a/examples/example3/shepherd-config.yml +++ /dev/null @@ -1,31 +0,0 @@ -tasks: - program1: - command: "./program1.sh" - state: - log: - ready: "program is ready" - complete: "program is completed" - program2: - command: "./program2.sh" - state: - log: - ready: "program is ready" - complete: "program is completed" - dependency: - items: - program1: "ready" - file_dependency: - mode: "all" - items: - - path: "file-created-by-program3.log" - min_size: 1 - program3: - command: "./program3.sh" - state: - log: - ready: "program is ready" - complete: "program is completed" -output: - state_times: "state_transition_times.json" -cleanup_command: "./cleanup.sh" -max_run_time: 120 diff --git a/examples/sade/gen_shepherd_config_for_suas.py b/examples/sade/gen_shepherd_config_for_suas.py index 4d1a415..995c084 100644 --- a/examples/sade/gen_shepherd_config_for_suas.py +++ b/examples/sade/gen_shepherd_config_for_suas.py @@ -1,18 +1,24 @@ import sys -from jinja2 import Environment, FileSystemLoader + +from jinja2 import Environment +from jinja2 import FileSystemLoader + def generate_config(template_file, output_file, num_px4_instances): - env = Environment(loader=FileSystemLoader('.')) + env = Environment(loader=FileSystemLoader(".")) template = env.get_template(template_file) config = template.render(num_px4_instances=num_px4_instances) - with open(output_file, 'w') as f: + with open(output_file, "w") as f: f.write(config) -if __name__ == '__main__': + +if __name__ == "__main__": if len(sys.argv) != 4: - print("Usage: python generate_shepherd_config.py ") + print( + "Usage: python generate_shepherd_config.py " + ) sys.exit(1) template_file = sys.argv[1] @@ -20,4 +26,3 @@ def generate_config(template_file, output_file, num_px4_instances): num_px4_instances = int(sys.argv[3]) generate_config(template_file, output_file, num_px4_instances) - diff --git a/examples/sade/sade-config.yaml b/examples/sade/sade-config.yaml new file mode 100644 index 0000000..39c8615 --- /dev/null +++ b/examples/sade/sade-config.yaml @@ -0,0 +1,97 @@ +services: + reserve_port: + type: "action" + command: "python3 /tmp/scripts/reserve_ports.py /home/user/Firmware/build/px4_sitl_default/etc/init.d-posix/ports.config 2 20010 simulator,gcs_local,gcs_remote,offboard_local,onboard_payload_local,onboard_gimbal_local,typhoon_offboard_local,gazebo_master" + stdout_path: "/tmp/log/reserve_ports.log" + stderr_path: "/tmp/log/reserve_ports_error.log" + state: + log: + complete: "ports and their PIDs written to" + chmod_port_config: + type: "action" + command: "chmod +x /home/user/Firmware/build/px4_sitl_default/etc/init.d-posix/ports.config" + stdout_path: "/tmp/log/chmod_port_config.log" + stderr_path: "/tmp/log/chmod_port_config_error.log" + dependency: + items: + reserve_port: "final" + + copy_ports_config: + command: "cp /home/user/Firmware/build/px4_sitl_default/etc/init.d-posix/ports.config /tmp/log/ports.config" + stdout_path: "/tmp/log/copy_ports_config.log" + stderr_path: "/tmp/log/copy_ports_config_error.log" + monitor_log: false + dependency: + items: + chmod_port_config: "final" + + gazebo_server: + type: "service" + command: "/tmp/scripts/start_gazebo_server.sh" + stdout_path: "/tmp/log/gazebo_server.log" + stderr_path: "/tmp/log/gazebo_server_error.log" + state: + log: + ready: "Connected to gazebo master" + dependency: + items: + chmod_port_config: "final" + + px4_instance_0: + type: "service" + command: "/tmp/scripts/start_px4_instance.sh 0" + stdout_path: "/tmp/log/px4_0.log" + stderr_path: "/tmp/log/px4_0_error.log" + state: + log: + waiting_for_simulator: "Waiting for simulator to accept connection" + ready: "Startup script returned successfully" + dependency: + items: + gazebo_server: "ready" + + spawn_model_0: + type: "action" + command: "/tmp/scripts/spawn_model.sh 0" + stdout_path: "/tmp/log/spawn_model_0.log" + stderr_path: "/tmp/log/spawn_model_0_error.log" + dependency: + items: + px4_instance_0: "waiting_for_simulator" + + px4_instance_1: + type: "service" + command: "/tmp/scripts/start_px4_instance.sh 1" + stdout_path: "/tmp/log/px4_1.log" + stderr_path: "/tmp/log/px4_1_error.log" + state: + log: + waiting_for_simulator: "Waiting for simulator to accept connection" + ready: "Startup script returned successfully" + dependency: + items: + gazebo_server: "ready" + + spawn_model_1: + type: "action" + command: "/tmp/scripts/spawn_model.sh 1" + stdout_path: "/tmp/log/spawn_model_1.log" + stderr_path: "/tmp/log/spawn_model_1_error.log" + dependency: + items: + px4_instance_1: "waiting_for_simulator" + + pose_sender: + type: "service" + command: "/tmp/scripts/start_pose_sender.sh" + stdout_path: "/tmp/log/pose_sender.log" + stderr_path: "/tmp/log/pose_sender_error.log" + dependency: + items: + px4_instance_0: "ready" + + px4_instance_1: "ready" + +output: + state_times: "/tmp/log/state_times.json" +stop_signal: "/tmp/log/stop.txt" diff --git a/examples/sade/sade-config.yml b/examples/sade/sade-config.yml deleted file mode 100644 index 9d9bf17..0000000 --- a/examples/sade/sade-config.yml +++ /dev/null @@ -1,98 +0,0 @@ -services: - reserve_port: - type: "action" - command: "python3 /tmp/scripts/reserve_ports.py /home/user/Firmware/build/px4_sitl_default/etc/init.d-posix/ports.config 2 20010 simulator,gcs_local,gcs_remote,offboard_local,onboard_payload_local,onboard_gimbal_local,typhoon_offboard_local,gazebo_master" - stdout_path: "/tmp/log/reserve_ports.log" - stderr_path: "/tmp/log/reserve_ports_error.log" - state: - log: - complete: "ports and their PIDs written to" - chmod_port_config: - type: "action" - command: "chmod +x /home/user/Firmware/build/px4_sitl_default/etc/init.d-posix/ports.config" - stdout_path: "/tmp/log/chmod_port_config.log" - stderr_path: "/tmp/log/chmod_port_config_error.log" - dependency: - items: - reserve_port: "final" - - copy_ports_config: - command: "cp /home/user/Firmware/build/px4_sitl_default/etc/init.d-posix/ports.config /tmp/log/ports.config" - stdout_path: "/tmp/log/copy_ports_config.log" - stderr_path: "/tmp/log/copy_ports_config_error.log" - monitor_log: false - dependency: - items: - chmod_port_config: "final" - - gazebo_server: - type: "service" - command: "/tmp/scripts/start_gazebo_server.sh" - stdout_path: "/tmp/log/gazebo_server.log" - stderr_path: "/tmp/log/gazebo_server_error.log" - state: - log: - ready: "Connected to gazebo master" - dependency: - items: - chmod_port_config: "final" - - px4_instance_0: - type: "service" - command: "/tmp/scripts/start_px4_instance.sh 0" - stdout_path: "/tmp/log/px4_0.log" - stderr_path: "/tmp/log/px4_0_error.log" - state: - log: - waiting_for_simulator: "Waiting for simulator to accept connection" - ready: "Startup script returned successfully" - dependency: - items: - gazebo_server: "ready" - - spawn_model_0: - type: "action" - command: "/tmp/scripts/spawn_model.sh 0" - stdout_path: "/tmp/log/spawn_model_0.log" - stderr_path: "/tmp/log/spawn_model_0_error.log" - dependency: - items: - px4_instance_0: "waiting_for_simulator" - - px4_instance_1: - type: "service" - command: "/tmp/scripts/start_px4_instance.sh 1" - stdout_path: "/tmp/log/px4_1.log" - stderr_path: "/tmp/log/px4_1_error.log" - state: - log: - waiting_for_simulator: "Waiting for simulator to accept connection" - ready: "Startup script returned successfully" - dependency: - items: - gazebo_server: "ready" - - spawn_model_1: - type: "action" - command: "/tmp/scripts/spawn_model.sh 1" - stdout_path: "/tmp/log/spawn_model_1.log" - stderr_path: "/tmp/log/spawn_model_1_error.log" - dependency: - items: - px4_instance_1: "waiting_for_simulator" - - pose_sender: - type: "service" - command: "/tmp/scripts/start_pose_sender.sh" - stdout_path: "/tmp/log/pose_sender.log" - stderr_path: "/tmp/log/pose_sender_error.log" - dependency: - items: - - px4_instance_0: "ready" - - px4_instance_1: "ready" - -output: - state_times: "/tmp/log/state_times.json" -stop_signal: "/tmp/log/stop.txt" diff --git a/examples/sade/sade-wf.yaml b/examples/sade/sade-wf.yaml new file mode 100644 index 0000000..4679701 --- /dev/null +++ b/examples/sade/sade-wf.yaml @@ -0,0 +1,96 @@ +services: + reserve_port: + type: "action" + command: "python3 /tmp/sade/reserve_ports.py /home/user/Firmware/build/px4_sitl_default/etc/init.d-posix/ports.config 8 4600 simulator,gcs_local,gcs_remote,offboard_local,onboard_payload_local,onboard_gimbal_local,typhoon_offboard_local,gazebo_master" + stdout_path: "/tmp/log/reserve_ports.log" + stderr_path: "/tmp/log/reserve_ports_error.log" + state: + log: + complete: "ports and their PIDs written to" + chmod_port_config: + type: "action" + command: "chmod +x /home/user/Firmware/build/px4_sitl_default/etc/init.d-posix/ports.config" + stdout_path: "/tmp/log/chmod_port_config.log" + stderr_path: "/tmp/log/chmod_port_config_error.log" + dependency: + items: + reserve_port: "final" + + copy_ports_config: + command: "cp /home/user/Firmware/build/px4_sitl_default/etc/init.d-posix/ports.config /tmp/log/ports.config" + stdout_path: "/tmp/log/copy_ports_config.log" + stderr_path: "/tmp/log/copy_ports_config_error.log" + monitor_log: false + dependency: + items: + chmod_port_config: "final" + + gazebo_server: + type: "service" + command: "/tmp/sade/start_gazebo_server.sh" + stdout_path: "/tmp/log/gazebo_server.log" + stderr_path: "/tmp/log/gazebo_server_error.log" + state: + log: + ready: "Connected to gazebo master" + dependency: + items: + chmod_port_config: "final" + + px4_instance_0: + type: "service" + command: "/tmp/sade/start_px4_instance.sh 0" + stdout_path: "/tmp/log/px4_0.log" + stderr_path: "/tmp/log/px4_0_error.log" + state: + log: + waiting_for_simulator: "Waiting for simulator to accept connection" + ready: "Startup script returned successfully" + dependency: + items: + gazebo_server: "ready" + + spawn_model_0: + type: "action" + command: "/tmp/sade/spawn_model.sh 0" + stdout_path: "/tmp/log/spawn_model_0.log" + stderr_path: "/tmp/log/spawn_model_0_error.log" + dependency: + items: + px4_instance_0: "ready_for_simulator" + + px4_instance_1: + type: "service" + command: "/tmp/sade/start_px4_instance.sh 0" + stdout_path: "/tmp/log/px4_0.log" + stderr_path: "/tmp/log/px4_0_error.log" + state: + log: + waiting_for_simulator: "Waiting for simulator to accept connection" + ready: "Startup script returned successfully" + dependency: + items: + gazebo_server: "ready" + + spawn_model_1: + type: "action" + command: "/tmp/sade/spawn_model.sh 0" + stdout_path: "/tmp/log/spawn_model_0.log" + stderr_path: "/tmp/log/spawn_model_0_error.log" + dependency: + items: + px4_instance_1: "ready_for_simulator" + + pose_sender: + type: "service" + command: "/tmp/sade/start_pose_sender.sh" + stdout_path: "/tmp/log/pose_sender.log" + stderr_path: "/tmp/log/pose_sender_error.log" + dependency: + items: + px4_instance_0: "ready" + px4_instance_1: "ready" + +output: + state_times: "/tmp/log/state_times.json" +stop_signal: "/tmp/log/stop.txt" diff --git a/examples/sade/sade-wf.yml b/examples/sade/sade-wf.yml deleted file mode 100644 index bf3b16c..0000000 --- a/examples/sade/sade-wf.yml +++ /dev/null @@ -1,96 +0,0 @@ -services: - reserve_port: - type: "action" - command: "python3 /tmp/sade/reserve_ports.py /home/user/Firmware/build/px4_sitl_default/etc/init.d-posix/ports.config 8 4600 simulator,gcs_local,gcs_remote,offboard_local,onboard_payload_local,onboard_gimbal_local,typhoon_offboard_local,gazebo_master" - stdout_path: "/tmp/log/reserve_ports.log" - stderr_path: "/tmp/log/reserve_ports_error.log" - state: - log: - complete: "ports and their PIDs written to" - chmod_port_config: - type: "action" - command: "chmod +x /home/user/Firmware/build/px4_sitl_default/etc/init.d-posix/ports.config" - stdout_path: "/tmp/log/chmod_port_config.log" - stderr_path: "/tmp/log/chmod_port_config_error.log" - dependency: - items: - reserve_port: "final" - - copy_ports_config: - command: "cp /home/user/Firmware/build/px4_sitl_default/etc/init.d-posix/ports.config /tmp/log/ports.config" - stdout_path: "/tmp/log/copy_ports_config.log" - stderr_path: "/tmp/log/copy_ports_config_error.log" - monitor_log: false - dependency: - items: - chmod_port_config: "final" - - gazebo_server: - type: "service" - command: "/tmp/sade/start_gazebo_server.sh" - stdout_path: "/tmp/log/gazebo_server.log" - stderr_path: "/tmp/log/gazebo_server_error.log" - state: - log: - ready: "Connected to gazebo master" - dependency: - items: - chmod_port_config: "final" - - px4_instance_0: - type: "service" - command: "/tmp/sade/start_px4_instance.sh 0" - stdout_path: "/tmp/log/px4_0.log" - stderr_path: "/tmp/log/px4_0_error.log" - state: - log: - waiting_for_simulator: "Waiting for simulator to accept connection" - ready: "Startup script returned successfully" - dependency: - items: - gazebo_server: "ready" - - spawn_model_0: - type: "action" - command: "/tmp/sade/spawn_model.sh 0" - stdout_path: "/tmp/log/spawn_model_0.log" - stderr_path: "/tmp/log/spawn_model_0_error.log" - dependency: - items: - px4_instance_0: "ready_for_simulator" - - px4_instance_1: - type: "service" - command: "/tmp/sade/start_px4_instance.sh 0" - stdout_path: "/tmp/log/px4_0.log" - stderr_path: "/tmp/log/px4_0_error.log" - state: - log: - waiting_for_simulator: "Waiting for simulator to accept connection" - ready: "Startup script returned successfully" - dependency: - items: - gazebo_server: "ready" - - spawn_model_1: - type: "action" - command: "/tmp/sade/spawn_model.sh 0" - stdout_path: "/tmp/log/spawn_model_0.log" - stderr_path: "/tmp/log/spawn_model_0_error.log" - dependency: - items: - px4_instance_1: "ready_for_simulator" - - pose_sender: - type: "service" - command: "/tmp/sade/start_pose_sender.sh" - stdout_path: "/tmp/log/pose_sender.log" - stderr_path: "/tmp/log/pose_sender_error.log" - dependency: - items: - px4_instance_0: "ready" - px4_instance_1: "ready" - -output: - state_times: "/tmp/log/state_times.json" -stop_signal: "/tmp/log/stop.txt" diff --git a/examples/sade/simple.sh b/examples/sade/simple.sh index 88053eb..d95ec46 100644 --- a/examples/sade/simple.sh +++ b/examples/sade/simple.sh @@ -1,67 +1,66 @@ #!/bin/bash # Reserve port -python3 /tmp/sade/reserve_ports.py /home/user/Firmware/build/px4_sitl_default/etc/init.d-posix/ports.config 8 4600 simulator,gcs_local,gcs_remote,offboard_local,onboard_payload_local,onboard_gimbal_local,typhoon_offboard_local,gazebo_master > /tmp/log/reserve_ports.log 2> /tmp/log/reserve_ports_error.log +python3 /tmp/sade/reserve_ports.py /home/user/Firmware/build/px4_sitl_default/etc/init.d-posix/ports.config 8 4600 simulator,gcs_local,gcs_remote,offboard_local,onboard_payload_local,onboard_gimbal_local,typhoon_offboard_local,gazebo_master >/tmp/log/reserve_ports.log 2>/tmp/log/reserve_ports_error.log # Check if the ports were reserved successfully if grep -q "ports and their PIDs written to" /tmp/log/reserve_ports.log; then - echo "Ports reserved successfully." + echo "Ports reserved successfully." else - echo "Failed to reserve ports." >&2 - exit 1 + echo "Failed to reserve ports." >&2 + exit 1 fi # Set permissions -chmod +x /home/user/Firmware/build/px4_sitl_default/etc/init.d-posix/ports.config > /tmp/log/chmod_port_config.log 2> /tmp/log/chmod_port_config_error.log +chmod +x /home/user/Firmware/build/px4_sitl_default/etc/init.d-posix/ports.config >/tmp/log/chmod_port_config.log 2>/tmp/log/chmod_port_config_error.log # Copy ports config -cp /home/user/Firmware/build/px4_sitl_default/etc/init.d-posix/ports.config /tmp/log/ports.config > /tmp/log/copy_ports_config.log 2> /tmp/log/copy_ports_config_error.log - +cp /home/user/Firmware/build/px4_sitl_default/etc/init.d-posix/ports.config /tmp/log/ports.config >/tmp/log/copy_ports_config.log 2>/tmp/log/copy_ports_config_error.log #!/bin/bash # Start Gazebo server -/tmp/sade/start_gazebo_server.sh > /tmp/log/gazebo_server.log 2> /tmp/log/gazebo_server_error.log & -GAZEBO_PID=$! +/tmp/sade/start_gazebo_server.sh >/tmp/log/gazebo_server.log 2>/tmp/log/gazebo_server_error.log & +# GAZEBO_PID=$! # Wait for Gazebo to be ready while ! grep -q "Connected to gazebo master" /tmp/log/gazebo_server.log; do - sleep 0.1 + sleep 0.1 done echo "Gazebo server is ready." #!/bin/bash # Start PX4 instance 0 -/tmp/sade/start_px4_instance.sh 0 > /tmp/log/px4_0.log 2> /tmp/log/px4_0_error.log & -PX4_0_PID=$! +/tmp/sade/start_px4_instance.sh 0 >/tmp/log/px4_0.log 2>/tmp/log/px4_0_error.log & +# PX4_0_PID=$! # Wait for PX4 instance 0 to be ready while ! grep -q "Startup script returned successfully" /tmp/log/px4_0.log; do - sleep 0.1 + sleep 0.1 done echo "PX4 instance 0 is ready." # Start PX4 instance 1 -/tmp/sade/start_px4_instance.sh 1 > /tmp/log/px4_1.log 2> /tmp/log/px4_1_error.log & -PX4_1_PID=$! +/tmp/sade/start_px4_instance.sh 1 >/tmp/log/px4_1.log 2>/tmp/log/px4_1_error.log & +# PX4_1_PID=$! # Wait for PX4 instance 1 to be ready while ! grep -q "Startup script returned successfully" /tmp/log/px4_1.log; do - sleep 0.1 + sleep 0.1 done echo "PX4 instance 1 is ready." #!/bin/bash # Spawn model for PX4 instance 0 -/tmp/sade/spawn_model.sh 0 > /tmp/log/spawn_model_0.log 2> /tmp/log/spawn_model_0_error.log +/tmp/sade/spawn_model.sh 0 >/tmp/log/spawn_model_0.log 2>/tmp/log/spawn_model_0_error.log # Spawn model for PX4 instance 1 -/tmp/sade/spawn_model.sh 1 > /tmp/log/spawn_model_1.log 2> /tmp/log/spawn_model_1_error.log +/tmp/sade/spawn_model.sh 1 >/tmp/log/spawn_model_1.log 2>/tmp/log/spawn_model_1_error.log #!/bin/bash # Start pose sender after all PX4 instances are ready -/tmp/sade/start_pose_sender.sh > /tmp/log/pose_sender.log 2> /tmp/log/pose_sender_error.log & -POSE_SENDER_PID=$! +/tmp/sade/start_pose_sender.sh >/tmp/log/pose_sender.log 2>/tmp/log/pose_sender_error.log & +# POSE_SENDER_PID=$! echo "Pose sender started." diff --git a/examples/sade/spawn_model.sh b/examples/sade/spawn_model.sh index b219e73..dd25c28 100755 --- a/examples/sade/spawn_model.sh +++ b/examples/sade/spawn_model.sh @@ -21,7 +21,7 @@ export GAZEBO_MASTER_URI="http://localhost:${drone_0_gazebo_master_port}" echo "GAZEBO_MASTER_URI set to $GAZEBO_MASTER_URI" function spawn_model() { - local N=$1 # Instance Number + local N=$1 # Instance Number local MODEL=$2 local X=0.0 local Y=$((3 * N)) @@ -30,15 +30,15 @@ function spawn_model() { echo "Starting PX4 instance $N with simulator port $simulator_port" python3 "${FIRMWARE_PATH}/Tools/sitl_gazebo/scripts/jinja_gen.py" \ - "${FIRMWARE_PATH}/Tools/sitl_gazebo/models/${MODEL}/${MODEL}.sdf.jinja" \ - "${FIRMWARE_PATH}/Tools/sitl_gazebo" \ - --mavlink_tcp_port $simulator_port \ - --mavlink_udp_port $((14560 + N)) \ - --mavlink_id $((1 + N)) \ - --gst_udp_port $((5600 + N)) \ - --video_uri $((5600 + N)) \ - --mavlink_cam_udp_port $((14530 + N)) \ - --output-file "/tmp/${MODEL}_${N}.sdf" + "${FIRMWARE_PATH}/Tools/sitl_gazebo/models/${MODEL}/${MODEL}.sdf.jinja" \ + "${FIRMWARE_PATH}/Tools/sitl_gazebo" \ + --mavlink_tcp_port $simulator_port \ + --mavlink_udp_port $((14560 + N)) \ + --mavlink_id $((1 + N)) \ + --gst_udp_port $((5600 + N)) \ + --video_uri $((5600 + N)) \ + --mavlink_cam_udp_port $((14530 + N)) \ + --output-file "/tmp/${MODEL}_${N}.sdf" echo "Spawning ${MODEL}_${N} at X=${X}, Y=${Y}" gz model --spawn-file="/tmp/${MODEL}_${N}.sdf" --model-name="${MODEL}_${N}" -x $X -y $Y -z 0.83 @@ -46,4 +46,3 @@ function spawn_model() { # Spawn model spawn_model $instance_number $VEHICLE_MODEL - diff --git a/examples/sade/suas_template.yml.j2 b/examples/sade/suas_template.yaml.j2 similarity index 100% rename from examples/sade/suas_template.yml.j2 rename to examples/sade/suas_template.yaml.j2 diff --git a/pyproject.toml b/pyproject.toml index 7964629..e9ee91d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,24 +1,239 @@ [build-system] -requires = ["setuptools>=61.0", "wheel"] -build-backend = "setuptools.build_meta" + build-backend = "setuptools.build_meta" + requires = ["setuptools>=61.0", "wheel"] [project] -name = "shepherd" -authors = [ - { name = "Your Name", email = "your.email@example.com" }, -] -description = "Shepherd - Service Orchestration and Monitoring Tool" -readme = "README.md" -requires-python = ">=3.6" -classifiers = [ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", - "Operating System :: MacOS", - "Operating System :: POSIX :: Linux", -] - -dynamic = ["version", "scripts", "dependencies"] - -[project.urls] -Homepage = "https://github.com/cooperative-computing-lab/shepherd" -Issues = "https://github.com/cooperative-computing-lab/shepherd/issues" + authors = [{ name = "Your Name", email = "your.email@example.com" }] + classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", + "Operating System :: MacOS", + "Operating System :: POSIX :: Linux", + ] + description = "Shepherd - Service Orchestration and Monitoring Tool" + name = "shepherd" + readme = "README.md" + requires-python = ">=3.10" + + dependencies = [ + "graphviz>=0.20.3", + "jinja2>=3.1.4", + "loguru>=0.7.3", + "matplotlib>=3.9.4", + "numpy>=2.0.2", + "pyyaml>=6.0.2", + ] + dynamic = ["version"] + + [project.scripts] + shepherd = "shepherd.main:main" + shepherd_viz = "shepherd.shepherd_viz:main" + + [project.urls] + Homepage = "https://github.com/cooperative-computing-lab/shepherd" + Issues = "https://github.com/cooperative-computing-lab/shepherd/issues" + +[dependency-groups] + dev = ["deptry>=0.21.2", "rich>=13.9.4", "ruff>=0.8.4", "setuptools>=75.6.0"] + +# deptry detects unused and missing dependencies. +[tool.deptry] + + extend_exclude = ["docs/"] + + [tool.deptry.per_rule_ignores] + # https://deptry.com/usage/#per-rule-ignores + # DEP003 = ["shepherd"] + DEP004 = ["rich"] + + +[tool.pylint] + + [tool.pylint.messages_control] + # https://pylint.readthedocs.io/en/stable/user_guide/messages/index.html + disable = [ + "R0903", # too few public methods + ] + +[tool.pyright] + # https://github.com/microsoft/pyright/blob/main/docs/configuration.md + exclude = [ + "*.proto", + "*.pyc", + "**/__pycache__", + "**/.cache/**", + "**/.dvc/**", + "**/.git/**", + "**/.git/objects/**", + "**/.git/subtree-cache/**", + "**/.ipynb_checkpoints/**", + "**/.neptune/**", + "**/.ruff_cache/**", + "**/.tox/**", + "**/.trash/**", + "**/.venv/**", + "**/build/**", + "**/cache/**", + "**/data/**", + "**/datalakes/**", + "**/dataset/**", + "**/datasets/**", + "**/dist/**", + "**/docs/**", + "**/node_modules/**", + "**/outputs/**", + "**/previews/**", + "**/temp/**", + "**/tmp/**", + "**/venv/**", + ] + include = [".", "src", "src/spectrumx"] + # Pylint rule list: + # https://pylint.readthedocs.io/en/stable/user_guide/checkers/features.html + lint.ignore = [ + # "E501", # line too long + "R0903", # too few public methods + ] + pythonPlatform = "Linux" + pythonVersion = "3.13" + # "none", "warning", "information", or "error" + reportMissingTypeArgument = "information" + reportPrivateUsage = "information" + stubPath = "./typings" # defaults to ./typings + typeCheckingMode = "standard" # "off", "basic", "standard", "strict" + + # Reports: + # https://github.com/microsoft/pyright/blob/main/docs/configuration.md#type-check-diagnostics-settings + # place ignored rules here + +[tool.ruff] + # Exclude a variety of commonly ignored directories. + exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "*/migrations/*.py", + "docs/", + "node_modules", + "staticfiles/*", + "venv", + ] + include = ["**/*.py", "shepherd/"] + indent-width = 4 + line-length = 88 + # IMPORTANT: in src, use paths from the root of the repository (starting with /) + src = ["/shepherd/"] + target-version = "py313" + + [tool.ruff.lint] + ignore = [ + # https://docs.astral.sh/ruff/settings/#lint_ignore + "COM812", # disabled following ruff's recommendation + "ISC001", # disabled following ruff's recommendation + "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` + "S101", # Use of assert detected https://docs.astral.sh/ruff/rules/assert/ + "S104", # Possible binding to all interfaces + "SIM102", # sometimes it's better to nest + "UP038", # Checks for uses of isinstance/issubclass that take a tuple + # of types for comparison. + # UP038 deactivated because it can make the code slow: + # https://github.com/astral-sh/ruff/issues/7871 + ] + select = [ + # https://docs.astral.sh/ruff/settings/#lint_select + # RULE SETS: https://docs.astral.sh/ruff/rules/ + "F", # Pyflakes + "E", # Pycodestyle errors + "W", # Pycodestyle warnings + "C90", # Cyclomatic complexity + "I", # Isort (import order) + "N", # PEP-8 Naming + "UP", # PyUpgrade (modernize code) + "YTT", # flake8-2020 (modernize code) + "ASYNC", # flake8-async + "S", # flake8-bandit (security) + "BLE", # flake8-blind-except + "FBT", # flake8-boolean-trap + "B", # flake8-bugbear (common errors) + "A", # flake8-builtins + "COM", # flake8-commas + "C4", # flake8-comprehensions + "DTZ", # flake8-date-time-zones + "T10", # flake8-debugger + "DJ", # flake8-django + "EM", # flake8-errmsg + "EXE", # flake8-executable + "FA", # flake8-flask + 'ISC', # flake8-implicit-str-concat + "ICN", # flake8-import-conventions + "LOG", # flake8-logging + "G", # flake8-logging-format + 'INP', # flake8-no-pep-420 + 'PIE', # flake8-pie (misc lints) + "T20", # flake8-print + 'PYI', # flake8-pyi (type hints) + 'PT', # flake8-pytest-style + "Q", # flake8-quotes + "RSE", # flake8-raise + "RET", # flake8-return + "SLF", # flake8-self + "SLOT", # flake8-slot + "SIM", # flake8-simplify + "TID", # flake8-tidy-imports + "TCH", # flake8-type-checking + "INT", # flake8-gettext + "PTH", # flake8-use-pathlib + "ERA", # eradicate + "PD", # pandas-vet + "PGH", # pygrep-hooks + "PL", # pylint + "R", # refactor + "TRY", # tryceratops (try/except) + "FLY", # flynt (f-string) + "PERF", # perflint (performance) + "FURB", # refurb (refactorings for older code) + "RUF", # ruff-specific rules + "AIR", # airflow + # "ANN", # flake8-annotations (type annotations; 100+ errors atm from cookiecutter-django) + # "ARG", # unused function argument + # "DOC", # docstrings + # "FAST", # fastapi + # "NPY", # numpy-specific (unused) + ] + # Allow fix for all enabled rules (when `--fix`) is provided. + fixable = ["ALL"] + unfixable = [] + # The fixes in extend-unsafe-fixes will require + # provide the `--unsafe-fixes` flag when fixing. + extend-unsafe-fixes = ["UP038"] + # Allow unused variables when underscore-prefixed. + dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + + [tool.ruff.lint.isort] + force-single-line = true + + [tool.ruff.lint.pylint] + # Controls PLR0913 + max-args = 9 + + [tool.ruff.format] + indent-style = "space" + line-ending = "auto" + quote-style = "double" + skip-magic-trailing-comma = false diff --git a/setup.py b/setup.py index abc25a2..4f9a917 100644 --- a/setup.py +++ b/setup.py @@ -1,31 +1,24 @@ -import os -from setuptools import setup, find_packages +from pathlib import Path + +from setuptools import find_packages +from setuptools import setup # Read the version from _version.py version = {} -with open(os.path.join("shepherd", "_version.py")) as f: - exec(f.read(), version) +version_path = Path(__file__).parent / "shepherd" / "_version.py" +with version_path.open(mode="r", encoding="utf-8") as fp: + exec(fp.read(), version) setup( - name='shepherd', - version=version['__version__'], # Use the imported version - description='Shepherd - Service Orchestration and Monitoring Tool', - long_description=open('README.md').read(), - long_description_content_type='text/markdown', + version=version["__version__"], # Use the imported version + description="Shepherd - Service Orchestration and Monitoring Tool", packages=find_packages(), include_package_data=True, install_requires=[ - 'pyyaml', - 'jinja2', - 'graphviz', - 'matplotlib', - 'pandas', + "pyyaml", + "jinja2", + "graphviz", + "matplotlib", + "pandas", ], - entry_points={ - 'console_scripts': [ - 'shepherd=shepherd.shepherd:main', - 'shepherd_viz=shepherd.shepherd_viz:main', - ], - }, - python_requires='>=3.6', ) diff --git a/shepherd.code-workspace b/shepherd.code-workspace new file mode 100644 index 0000000..8bc4156 --- /dev/null +++ b/shepherd.code-workspace @@ -0,0 +1,9 @@ +{ + "folders": [ + { + "name": "shepherd", + "path": "." + } + ], + "settings": {} +} diff --git a/shepherd/config_loader.py b/shepherd/config_loader.py index 28b8869..d0ef687 100644 --- a/shepherd/config_loader.py +++ b/shepherd/config_loader.py @@ -1,81 +1,101 @@ -import os -import logging +"""Helpers to load and preprocess Shepherd configuration.""" + +from pathlib import Path +from typing import Any + import yaml +from loguru import logger as log -def load_and_preprocess_config(filepath): +def load_and_preprocess_config(filepath: Path) -> dict[str, Any]: """Loads and preprocesses configuration from a YAML file.""" - if filepath is None or not os.path.exists(filepath): + if filepath is None: return None - - with open(filepath, 'r') as file: + if not filepath.exists(): + msg = f"Config file not found: {filepath}" + raise FileNotFoundError(msg) + if not filepath.is_file(): + msg = f"Config path is not a file: {filepath}" + raise ValueError(msg) + + with filepath.open(mode="r", encoding="utf-8") as file: config = yaml.safe_load(file) - preprocess_config(config, filepath) + preprocess_config(config, config_path=filepath) - logging.debug(f"Loaded and preprocessed config from {filepath}") + log.debug(f"Loaded and preprocessed config from {filepath}") return config -def preprocess_config(config, config_path): +def preprocess_config(config: dict[str, Any], config_path: Path) -> None: """Automatically fills in missing stdout_path and stderr_path paths.""" - tasks = config.get('tasks', {}) - stdout_dir = config.get('output', {}).get('stdout_dir', '') - working_dir = os.path.dirname(os.path.abspath(config_path)) + services = config.get("services", {}) + stdout_dir = config.get("output", {}).get("stdout_dir", "") + stdout_dir = Path(stdout_dir) if stdout_dir else None + working_dir = config_path.parent - for task_name, details in tasks.items(): + for service_name, details in services.items(): # Auto-fill log and error files if not specified - if 'stdout_path' not in details: - details['stdout_path'] = f"{task_name}_stdout.log" - if 'stderr_path' not in details: - details['stderr_path'] = f"{task_name}_stderr.log" + if "stdout_path" not in details: + details["stdout_path"] = f"{service_name}_stdout.log" + if "stderr_path" not in details: + details["stderr_path"] = f"{service_name}_stderr.log" - if stdout_dir: - details['stdout_path'] = os.path.join(stdout_dir, details['stdout_path']) - details['stderr_path'] = os.path.join(stdout_dir, details['stderr_path']) - else: - details['stdout_path'] = os.path.join(working_dir, details['stdout_path']) - details['stderr_path'] = os.path.join(working_dir, details['stderr_path']) + if not stdout_dir: + log.warning(f"Service '{service_name}' has no 'stdout_dir' specified.") + dir_used = stdout_dir if stdout_dir else working_dir + details["stdout_path"] = str(dir_used / details["stdout_path"]) + details["stderr_path"] = str(dir_used / details["stderr_path"]) - state_file_path = details.get('state', {}).get('file', {}).get('path', "") + state_file_path = details.get("state", {}).get("file", {}).get("path", "") if state_file_path: - details['state']['file']['path'] = os.path.join(working_dir, state_file_path) + details["state"]["file"]["path"] = str(working_dir / state_file_path) -def validate_and_sort_programs(config): - logging.debug("Validating and sorting programs") - required_keys = ['tasks'] +def validate_and_sort_services(config: dict[str, Any]) -> list[str]: + """Validates and sorts services based on their dependencies.""" + log.debug("Validating and sorting services") + required_keys = ["services"] for key in required_keys: if key not in config: - raise ValueError(f"Missing required key: {key}") + msg = f"Missing required key: {key}" + raise ValueError(msg) - tasks = config['tasks'] + services = config["services"] - for task, details in tasks.items(): - if 'command' not in details: - raise ValueError(f"Program {task} is missing the 'command' key") - if 'stdout_path' not in details: - raise ValueError(f"Program {task} is missing the 'stdout_path' key") + for service, details in services.items(): + if "command" not in details: + msg = f"Service '{service}' is missing the 'command' key" + raise ValueError(msg) + if "stdout_path" not in details: + msg = f"Service '{service}' is missing the 'stdout_path' key" + raise ValueError(msg) - sorted_tasks = topological_sort(tasks) - logging.debug(f"Sorted tasks: {sorted_tasks}") - return sorted_tasks + sorted_services = topological_sort(services) + log.debug(f"Sorted services: {sorted_services}") + return sorted_services -def topological_sort(programs): - logging.debug("Performing topological sort") +def topological_sort(services: dict[str, dict[str, Any]]) -> list[str]: + """Forms a graph of services and dependencies, sorting them topologically.""" + log.debug("Performing topological sort") - graph = {program: details.get('dependency', {}).get('items', {}) for program, details in programs.items()} + graph = { + service: details.get("dependency", {}).get("items", {}) + for service, details in services.items() + } - visited = set() - visiting = set() - stack = [] + visited: set[str] = set() + visiting: set[str] = set() + stack: list[str] = [] - def dfs(node): + def dfs(node) -> None: + """Depth-first search for topological sort.""" if node in visiting: - raise ValueError(f"Cyclic dependency on {node}") + msg = f"Cyclic dependency on {node}" + raise ValueError(msg) visiting.add(node) @@ -87,7 +107,7 @@ def dfs(node): visiting.remove(node) - for program in graph: - dfs(program) - logging.debug(f"Topological sort result: {stack}") + for service in graph: + dfs(service) + log.debug(f"Topological sort result: {stack}") return stack diff --git a/shepherd/log_monitor.py b/shepherd/log_monitor.py index cda18a0..f0c9c84 100644 --- a/shepherd/log_monitor.py +++ b/shepherd/log_monitor.py @@ -1,50 +1,83 @@ -import os +"""Monitors log files for specific keywords and updates the state dictionary.""" + import time -import logging +from multiprocessing.synchronize import Condition +from multiprocessing.synchronize import Event +from pathlib import Path + +from loguru import logger as log -def monitor_log_file(log_path, state_dict, task_name, state_keywords, cond, state_times, start_time, stop_event): - logging.debug(f"Starting to monitor file '{log_path}' for {task_name}") +def monitor_log_file( + log_path: str | Path, + state_dict: dict[str, str], + service_name: str, + state_keywords: dict[str, str], + cond: Condition, + state_times: dict[str, dict[str, float]], + start_time: float, + stop_event: Event, +) -> None: + """Monitors a log file for specific keywords and updates the state dictionary.""" if not state_keywords: - logging.debug(f"No state keywords for {task_name}, exiting monitor") + log.warning( + f"No state keywords for '{service_name}'; " + "exiting log monitor for this service." + ) return - while not os.path.exists(log_path): + log_path = Path(log_path) + while not log_path.exists(): if stop_event.is_set(): - logging.debug(f"Stop event set, exiting monitor for {task_name}") + log.info( + f"Stop event set for service '{service_name}'; exiting log monitor" + ) return time.sleep(0.1) + if not log_path.is_file(): + log.warning( + f"Log path '{log_path}' for service '{service_name}' " + "is not a file; exiting log monitor" + ) + return + + log.debug(f"Started monitoring '{log_path}' for service '{service_name}'") last_state = list(state_keywords.keys())[-1] + reached_last_state: bool = False - with open(log_path, 'r') as file: + with log_path.open(mode="r", encoding="utf-8") as log_file: while not stop_event.is_set(): - line = file.readline() + line = log_file.readline() if not line: - time.sleep(0.01) + time.sleep(0.05) continue current_time = time.time() - start_time - reached_last_state = False - for state in state_keywords: - if state_keywords[state] in line: - with cond: - state_dict[task_name] = state - local_state_times = state_times[task_name] - local_state_times[state] = current_time - state_times[task_name] = local_state_times - cond.notify_all() + for state, value in state_keywords.items(): + if value not in line: + continue + with cond: + state_dict[service_name] = state + local_state_times = state_times[service_name] + local_state_times[state] = current_time + state_times[service_name] = local_state_times + cond.notify_all() - logging.debug(f"{task_name} reached state '{state}' at {current_time}") + msg = ( + f"Service '{service_name}' reached state " + f"'{state.upper()}' at {current_time}" + ) + log.info(msg) - if state == last_state: - reached_last_state = True - break + if state == last_state: + reached_last_state = True + break if reached_last_state: break - logging.debug(f"Finished monitoring file '{log_path}' for {task_name}") + log.info(f"Finished monitoring '{log_path}' for service '{service_name}'") diff --git a/shepherd/logging_setup.py b/shepherd/logging_setup.py index 1c28806..b3d6b55 100644 --- a/shepherd/logging_setup.py +++ b/shepherd/logging_setup.py @@ -1,28 +1,36 @@ +"""Logging setup for the main process and the listener process.""" + import logging import logging.config import logging.handlers +import multiprocessing import sys +import traceback +from pathlib import Path + +from loguru import logger as log -def setup_logging(queue): +def setup_logging(queue: multiprocessing.Queue) -> None: + """Sets up logging for the main process.""" root = logging.getLogger() handler = logging.handlers.QueueHandler(queue) root.addHandler(handler) root.setLevel(logging.DEBUG) -def configure_listener(log_file=None): +def configure_listener(log_file: str | Path | None = None) -> None: + """Configures the listener process to write log messages to a file.""" root = logging.getLogger() - if log_file: - handler = logging.FileHandler(log_file) - else: - handler = logging.StreamHandler() - formatter = logging.Formatter('%(asctime)s %(name)-12s %(levelname)-8s %(message)s') + # default handler + handler = logging.FileHandler(log_file) if log_file else logging.StreamHandler() + formatter = logging.Formatter("%(asctime)s %(name)-12s %(levelname)-8s %(message)s") handler.setFormatter(formatter) root.addHandler(handler) - #Todo: make it optional? + # stream handler + # TODO: make it optional? stream_handler = logging.StreamHandler(sys.stdout) stream_handler.setFormatter(formatter) root.addHandler(stream_handler) @@ -30,7 +38,8 @@ def configure_listener(log_file=None): root.setLevel(logging.DEBUG) -def listener_process(queue, log_file=None): +def logger_daemon(queue: multiprocessing.Queue, log_file: str | Path | None) -> None: + """Listens for log messages and writes them to a file.""" configure_listener(log_file) while True: try: @@ -39,7 +48,6 @@ def listener_process(queue, log_file=None): break logger = logging.getLogger(record.name) logger.handle(record) - except Exception: - import sys, traceback - print('Problem:', file=sys.stderr) + except Exception as err: # pylint: disable=broad-except # noqa: BLE001 traceback.print_exc(file=sys.stderr) + log.error(f"Problem in logging listener: {err}") diff --git a/shepherd/main.py b/shepherd/main.py new file mode 100755 index 0000000..1ab3751 --- /dev/null +++ b/shepherd/main.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""Script to start a Shepherd Workflow Manager instance.""" + +import argparse +import multiprocessing +import time +from pathlib import Path + +from loguru import logger as log + +from shepherd import logging_setup +from shepherd.service_manager import ServiceManager + +console = None + +try: + from rich.console import Console + from rich.traceback import install + + install() + console = Console() +except ImportError: + log.info("Install Rich to get better tracebacks.") + + +def main() -> None: + """Main entry point for the Shepherd Workflow Manager.""" + log.debug("Starting Shepherd Workflow Manager") + parser = argparse.ArgumentParser(description="Run Shepherd Workflow Manager") + parser.add_argument( + "--run-dir", + type=str, + help="Writeable directory exclusive to this simulation run. Must exist.", + required=True, + ) + parser.add_argument( + "--config", + type=str, + help="Path to the Shepherd config (YAML) file. Must exist.", + required=True, + ) + parser.add_argument( + "--work-dir", + type=str, + default=None, + required=True, + help="Directory to where call the services / scripts from. Must exist.", + ) + parser.add_argument( + "--log", + type=str, + default=None, + help="Path to Shepherd's log file. File and parents will be created if not exist.", + ) + + args = parser.parse_args() + + run_dir = Path(args.run_dir) + if not run_dir.is_dir(): + msg = f"Run directory not found: {run_dir}" + raise NotADirectoryError(msg) + config_path = Path(args.config) + working_dir = Path(args.work_dir) + if not working_dir.is_dir(): + msg = ( + f"Working directory not found: {working_dir}. " + "Either create it or set --work-dir when calling Shepherd." + ) + raise NotADirectoryError(msg) + log_file = Path(args.log) if args.log else run_dir / "logs" / "shepherd.log" + log_file.parent.mkdir(parents=True, exist_ok=True) + + start_time = time.time() + start_time_fmt = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(start_time)) + log.debug(f"Start time: {start_time_fmt}") + + logging_queue = multiprocessing.Queue() + listener = multiprocessing.Process( + target=logging_setup.logger_daemon, + kwargs={"queue": logging_queue, "log_file": log_file}, + ) + listener.start() + logging_setup.setup_logging(logging_queue) + + log.debug("Starting main") + service_manager = ServiceManager( + config_path=config_path, + logging_queue=logging_queue, + run_dir=run_dir, + working_dir=working_dir, + ) + service_manager.start_services(start_time) + log.debug("Exiting main") + + logging_queue.put(None) # Send None to the listener to stop it + listener.join() + + +if __name__ == "__main__": + main() diff --git a/shepherd/program_executor.py b/shepherd/program_executor.py index 3825072..3a92e6a 100644 --- a/shepherd/program_executor.py +++ b/shepherd/program_executor.py @@ -1,77 +1,94 @@ +import multiprocessing import os import signal import subprocess import threading import time -import logging +from multiprocessing.synchronize import Condition +from multiprocessing.synchronize import Event +from pathlib import Path +from typing import Any + +from loguru import logger as log from shepherd.log_monitor import monitor_log_file from shepherd.logging_setup import setup_logging - -def execute_program(config, working_dir, state_dict, task_name, cond, state_times, start_time, pgid_dict, - stop_event, logging_queue): +console = None + +try: + from rich.console import Console + from rich.traceback import install + + install() + console = Console() +except ImportError: + log.info("Install Rich to get better tracebacks.") + + +def execute_service( + *, + config: dict[str, Any], + working_dir: str, + state_dict: dict[str, str], + service_name: str, + cond: Condition, + state_times: dict[str, dict[str, int]], + start_time: float, + pgid_dict: dict[str, int], + stop_event: Event, + logging_queue: multiprocessing.Queue, +): + """Executes a service and updates the state dictionary.""" setup_logging(logging_queue) - def signal_handler(signum, frame): - logging.debug(f"Received signal {signum} in {task_name}") + def signal_handler(signum, frame) -> None: # pylint: disable=unused-argument + log.debug(f"Received signal {signum} in {service_name}") stop_event.set() - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGINT, handler=signal_handler) + signal.signal(signal.SIGTERM, handler=signal_handler) - command = config['command'] - stdout_path = config['stdout_path'] - stderr_path = config['stderr_path'] + command = config["command"] - dependencies = config.get('dependency', {}).get('items', {}) - dependency_mode = config.get('dependency', {}).get('mode', 'all') - stdout_states = config.get('state', {}).get('log', {}) - file_path_to_monitor = config.get('state', {}).get('file', {}).get('path', '') - file_states = config.get('state', {}).get('file', {}).get('states', {}) + # make sure out dirs exist + stdout_path = Path(config["stdout_path"]) + stderr_path = Path(config["stderr_path"]) + stdout_path.parent.mkdir(parents=True, exist_ok=True) + stderr_path.parent.mkdir(parents=True, exist_ok=True) - file_dependencies = config.get('file_dependency', {}) - file_dependency_mode = file_dependencies.get('mode', 'all') - file_dependency_items = file_dependencies.get('items', []) + dependencies: dict[str, str] = config.get("dependency", {}).get("items", {}) + dependency_mode: str = config.get("dependency", {}).get("mode", "all") + stdout_states: dict[str, str] = config.get("state", {}).get("log", {}) + file_path_to_monitor: str = config.get("state", {}).get("file", {}).get("path", "") + file_states: dict[str, str] = ( + config.get("state", {}).get("file", {}).get("states", {}) + ) - task_type = config.get('type', 'action') + service_type: str = config.get("type", "action") with cond: - state_dict[task_name] = "initialized" - update_state_time(task_name, "initialized", start_time, state_times) + state_dict[service_name] = "initialized" + update_state_time( + service_name=service_name, state="initialized", state_times=state_times + ) cond.notify_all() - if file_dependencies: - for file_dep in file_dependency_items: - file_path = file_dep['path'] - min_size = file_dep.get('min_size', 1) - - while (not os.path.exists(file_path) - or (os.path.getsize(file_path) < min_size)) and not stop_event.is_set(): - - time.sleep(0.5) - - if stop_event.is_set(): - with cond: - state_dict[task_name] = "stopped_before_execution" - update_state_time(task_name, "stopped_before_execution", start_time, state_times) - cond.notify_all() - return - - logging.debug(f"Dependant file {file_path} found for task {task_name}") - try: with cond: - if dependency_mode == 'all': - for dep_task, required_state in dependencies.items(): - while required_state not in state_times.get(dep_task, {}) and not stop_event.is_set(): + if dependency_mode == "all": + for dep_service, required_state in dependencies.items(): + while ( + required_state not in state_times.get(dep_service, {}) + and not stop_event.is_set() + ): cond.wait() - elif dependency_mode == 'any': + elif dependency_mode == "any": satisfied = False while not satisfied and not stop_event.is_set(): - for dep_task, required_state in dependencies.items(): - if required_state in state_times.get(dep_task, {}): + for dep_service, required_state in dependencies.items(): + if required_state in state_times.get(dep_service, {}): satisfied = True break if not satisfied: @@ -79,93 +96,156 @@ def signal_handler(signum, frame): if stop_event.is_set(): with cond: - state_dict[task_name] = "stopped_before_execution" - update_state_time(task_name, "stopped_before_execution", start_time, state_times) + state_dict[service_name] = "stopped_before_execution" + update_state_time( + service_name=service_name, + state="stopped_before_execution", + state_times=state_times, + ) cond.notify_all() return - logging.debug(f"Starting execution of '{task_type}' {task_name}") + log.debug(f"Starting execution of '{service_type}' {service_name}") with cond: - state_dict[task_name] = "started" - update_state_time(task_name, "started", start_time, state_times) + state_dict[service_name] = "started" + update_state_time( + service_name=service_name, state="started", state_times=state_times + ) cond.notify_all() # Start the main log monitoring thread - log_thread = threading.Thread(target=monitor_log_file, - args=(stdout_path, state_dict, task_name, stdout_states, cond, state_times, - start_time, stop_event)) - log_thread.start() + log_monitor_thread = threading.Thread( + target=monitor_log_file, + kwargs={ + "log_path": stdout_path, + "state_dict": state_dict, + "service_name": service_name, + "state_keywords": stdout_states, + "cond": cond, + "state_times": state_times, + "start_time": start_time, + "stop_event": stop_event, + }, + ) + log_monitor_thread.start() # Optional: Start additional file monitoring thread if a file path is specified file_monitor_thread = None if file_path_to_monitor: - file_monitor_thread = threading.Thread(target=monitor_log_file, - args=( - file_path_to_monitor, state_dict, task_name, file_states, - cond, - state_times, start_time, stop_event)) + file_monitor_thread = threading.Thread( + target=monitor_log_file, + kwargs={ + "log_path": Path(file_path_to_monitor), + "state_dict": state_dict, + "service_name": service_name, + "state_keywords": file_states, + "cond": cond, + "state_times": state_times, + "start_time": start_time, + "stop_event": stop_event, + }, + ) file_monitor_thread.start() # Execute the process - with open(stdout_path, 'w') as out, open(stderr_path, 'w') as err: - process = subprocess.Popen(command, shell=True, cwd=working_dir, stdout=out, stderr=err, - preexec_fn=os.setsid) - pgid_dict[task_name] = os.getpgid(process.pid) + with ( + stdout_path.open(mode="w", encoding="utf-8") as out, + stderr_path.open(mode="w", encoding="utf-8") as err, + ): + process = subprocess.Popen( + command, + shell=True, + cwd=working_dir, + stdout=out, + stderr=err, + start_new_session=True, + ) + pgid_dict[service_name] = os.getpgid(process.pid) while process.poll() is None: time.sleep(0.1) return_code = process.returncode - logging.debug(f"Returned with code {return_code}") + msg = f"Service '{service_name}' returned with code {return_code}" + _ = log.debug(msg) if return_code == 0 else log.error(msg) with cond: if stop_event.is_set() and return_code == -signal.SIGTERM: - state_dict[task_name] = "stopped" - update_state_time(task_name, "stopped", start_time, state_times) + state_dict[service_name] = "stopped" + update_state_time( + service_name=service_name, state="stopped", state_times=state_times + ) cond.notify_all() - if task_type == 'service' and not stop_event.is_set(): - logging.debug(f"Stopping execution of '{task_type}' {task_name}") + if service_type == "service" and not stop_event.is_set(): + log.debug(f"Stopping execution of '{service_type}' {service_name}") # If a service stops before receiving a stop event, mark it as failed with cond: - state_dict[task_name] = "failure" - update_state_time(task_name, "failure", start_time, state_times) + state_dict[service_name] = "failure" + update_state_time( + service_name=service_name, state="failure", state_times=state_times + ) cond.notify_all() - logging.debug(f"ERROR: Task {task_name} stopped unexpectedly, marked as failure.") - - elif task_type == 'action': + log.error( + f"Service '{service_name}' stopped unexpectedly, marked as failure.", + ) + if process.stderr: + log.error( + process.stderr.read().decode("utf-8", errors="ignore"), + ) + if process.stdout: + log.error( + process.stdout.read().decode("utf-8", errors="ignore"), + ) + + elif service_type == "action": action_state = "action_success" if return_code == 0 else "action_failure" with cond: - state_dict[task_name] = action_state - update_state_time(task_name, action_state, start_time, state_times) + state_dict[service_name] = action_state + update_state_time( + service_name=service_name, + state=action_state, + state_times=state_times, + ) cond.notify_all() with cond: - state_dict[task_name] = "final" - update_state_time(task_name, "final", start_time, state_times) + state_dict[service_name] = "final" + update_state_time( + service_name=service_name, state="final", state_times=state_times + ) cond.notify_all() - if log_thread.is_alive(): - log_thread.join() + if log_monitor_thread.is_alive(): + log_monitor_thread.join() if file_monitor_thread and file_monitor_thread.is_alive(): file_monitor_thread.join() - except Exception as e: - logging.debug(f"Exception in executing {task_name}: {e}") + except Exception as err: # pylint: disable=broad-except + log.error(f"Exception in service '{service_name}': {err}") + if console: + console.print_exception(show_locals=True) + else: + log.warning("Install Rich to get better tracebacks.") + log.exception(err) - logging.debug(f"Finished execution of {task_name}") + log.debug(f"Finished execution of service '{service_name}'") -def update_state_time(task_name, state, start_time, state_times): - current_time = time.time() - start_time +def update_state_time(*, service_name, state, state_times) -> None: + """Updates the state time for a service.""" + current_time = int(time.time() * 1000) - local_state_times = state_times[task_name] + local_state_times = state_times[service_name] local_state_times[state] = current_time - state_times[task_name] = local_state_times + state_times[service_name] = local_state_times - logging.debug(f"Task '{task_name}' reached the state '{state}' at time {current_time:.3f}") + log.debug( + f"Service '{service_name}' reached state '{state.upper()}' at " + f"{time.strftime('%H:%M:%S', time.localtime(current_time // 1_000))}", + ) diff --git a/shepherd/service_manager.py b/shepherd/service_manager.py index dd7222f..055e5df 100644 --- a/shepherd/service_manager.py +++ b/shepherd/service_manager.py @@ -1,78 +1,132 @@ +"""Manages the execution of services and the stop conditions.""" + import json -import logging import multiprocessing import os import signal -import subprocess import threading import time from multiprocessing import Process +from pathlib import Path +from typing import Any + +from loguru import logger as log + +from shepherd.config_loader import load_and_preprocess_config +from shepherd.config_loader import validate_and_sort_services +from shepherd.program_executor import execute_service -from shepherd.config_loader import load_and_preprocess_config, validate_and_sort_programs -from shepherd.program_executor import execute_program +def save_state_times(state_times: dict[str, Any], output_file: Path) -> None: + """Saves the state times to a JSON file.""" -def save_state_times(state_times, output_file): - logging.debug(state_times) + output_file = Path(output_file) + log.debug(state_times) state_times_dict = dict(state_times) for key, value in state_times_dict.items(): state_times_dict[key] = dict(value) - with open(output_file, 'w') as f: - json.dump(state_times_dict, f, indent=2) + with output_file.open("w", encoding="utf-8") as fp: + json.dump(state_times_dict, fp, indent=2) + + +class ServiceManager: + """Service Manager for the Shepherd Workflow Manager. + + Args: + run_dir: The directory exclusive to this simulation run. + config_path: Path to the Shepherd config (YAML) file. + working_dir: Directory to where call the services / scripts from. + logging_queue: Multiprocessing queue for logging messages. + """ + + def __init__( + self, + run_dir: str | Path, + config_path: str | Path, + working_dir: str | Path, + logging_queue, + ) -> None: + log.debug("Initializing ServiceManager") + self.run_dir = Path(run_dir) + if not self.run_dir.is_dir(): + msg = f"Run directory not found: {self.run_dir}" + raise NotADirectoryError(msg) + self.config_path = Path(config_path) + self.config = load_and_preprocess_config(self.config_path) - -class TaskManager: - def __init__(self, config_path, logging_queue): - logging.debug("Initializing TaskManager") - self.config = load_and_preprocess_config(config_path) - self.tasks = self.config['tasks'] - self.sorted_tasks = validate_and_sort_programs(self.config) - self.working_dir = os.path.dirname(os.path.abspath(config_path)) - self.output = self.config['output'] - self.stop_signal_path = os.path.join(self.working_dir, self.config.get('stop_signal', '')) - self.max_run_time = self.config.get('max_run_time', None) - self.stop_event = multiprocessing.Event() + self.cond = multiprocessing.Condition() + self.logging_queue = logging_queue + self.max_run_time = self.config.get("max_run_time", None) + self.output = self.config["output"] self.pgid_dict = multiprocessing.Manager().dict() + self.process_timeout = self.config.get("process_timeout", 10) + self.processes: dict[str, Process] = {} + self.services: dict[str, dict[str, Any]] = self.config["services"] + self.sorted_services = validate_and_sort_services(self.config) self.state_dict = multiprocessing.Manager().dict() self.state_times = multiprocessing.Manager().dict() - self.cond = multiprocessing.Condition() - self.processes = {} - self.logging_queue = logging_queue - self.cleanup_command = self.config.get('cleanup_command', None) - logging.debug("TaskManager initialized") - - def setup_signal_handlers(self): - logging.debug("Setting up signal handlers") + self.stop_event = multiprocessing.Event() + self.stop_signal_path: Path = ( + self.run_dir / "control" / self.config.get("stop_signal", "stop.txt") + ) + self.working_dir = Path(working_dir) + log.info("ServiceManager initialized") + config = json.dumps(self.config, indent=4) + msg = f"Shepherd (service mgr) configuration:\n{config}" + log.debug(msg) + + def setup_signal_handlers(self) -> None: + """Sets up signal handlers for SIGTERM and SIGINT.""" + log.debug("Setting up signal handlers") signal.signal(signal.SIGTERM, self.signal_handler) signal.signal(signal.SIGINT, self.signal_handler) - def signal_handler(self, signum, frame): - logging.debug(f"Received signal {signum} in pid {os.getpid()}, stopping all tasks...") + def signal_handler(self, signum, frame) -> None: # pylint: disable=unused-argument + """Handles SIGTERM and SIGINT signals.""" + log.debug( + f"Received signal {signum} in pid {os.getpid()}, stopping all services...", + ) self.stop_event.set() - def start_tasks(self, start_time): - logging.debug("Starting tasks") + def start_services(self, start_time) -> None: + """Starts all services in the workflow, clearing stop signal file if present.""" + log.debug("Starting Shepherd workflow services") self.setup_signal_handlers() - for task in self.sorted_tasks: - task_config = self.tasks[task] - - self.state_dict[task] = "" - self.state_times[task] = {} - - p_exec = Process(target=execute_program, args=( - task_config, self.working_dir, self.state_dict, task, self.cond, self.state_times, start_time, - self.pgid_dict, self.stop_event, self.logging_queue)) + for service in self.sorted_services: + service_config = self.services[service] + + self.state_dict[service] = "" + self.state_times[service] = {} + + log.info(f"Starting service '{service}'") + p_exec = Process( + target=execute_service, + kwargs={ + "config": service_config, + "working_dir": self.working_dir, + "state_dict": self.state_dict, + "service_name": service, + "cond": self.cond, + "state_times": self.state_times, + "start_time": start_time, + "pgid_dict": self.pgid_dict, + "stop_event": self.stop_event, + "logging_queue": self.logging_queue, + }, + ) p_exec.start() - self.processes[task] = p_exec + self.processes[service] = p_exec - logging.debug("All tasks initialized") + log.debug("All services initialized") - stop_thread = threading.Thread(target=self.check_stop_conditions, args=(start_time,)) + stop_thread = threading.Thread( + target=self.check_stop_conditions, args=(start_time,) + ) stop_thread.start() for p in self.processes.values(): @@ -80,79 +134,112 @@ def start_tasks(self, start_time): stop_thread.join() - if os.path.isfile(self.stop_signal_path) and os.path.exists(self.stop_signal_path): - os.remove(self.stop_signal_path) + if self.stop_signal_path.is_file(): + self.stop_signal_path.unlink() - save_state_times(self.state_times, os.path.join(self.working_dir, self.output['state_times'])) + save_state_times( + state_times=self.state_times, # pyright: ignore[reportArgumentType] + output_file=self.working_dir / self.output["state_times"], + ) - def check_stop_conditions(self, start_time): - logging.debug("Checking stop conditions") + def check_stop_conditions(self, start_time: float) -> None: + """Issues a stop event if any of the stop conditions are met.""" + log.debug("Checking stop conditions") while not self.stop_event.is_set(): - if (self.check_stop_signal_file() - or self.check_max_run_time(start_time) or self.check_all_tasks_final()): + if ( + self.is_stop_signal_present() + or self.is_runtime_exceeded(start_time) + or self.is_all_services_final() + ): self.stop_event.set() else: self.stop_event.wait(timeout=1) - self.stop_all_tasks() - - logging.debug("Finished checking stop conditions") + self.stop_all_services() - def stop_all_tasks(self): - logging.debug("Stopping all tasks") + log.debug("Finished checking stop conditions") - if self.cleanup_command: + def stop_all_services(self) -> None: + """Stops all services in this workflow.""" + log.debug("Stopping all services") + for service_name in self.processes: + pgid = self.pgid_dict.get(service_name) + if not pgid: + continue try: - logging.debug(f"Executing system cleanup command: {self.cleanup_command}") - - subprocess.run(self.cleanup_command, shell=True, check=True) - logging.debug("System cleanup command executed successfully") - except subprocess.CalledProcessError as e: - logging.error(f"System cleanup command failed: {e}") - - for task_name, process in self.processes.items(): - pgid = self.pgid_dict.get(task_name) + os.killpg(pgid, signal.SIGTERM) + msg = ( + f"Sent SIGTERM to process group '{pgid}'" + f" for service '{service_name}'" + ) + log.debug(msg) + except (ProcessLookupError, OSError): + msg = ( + f"Process group '{pgid}' for service " + f"'{service_name}' not found or already terminated." + ) + log.info(msg) + + failed_to_stop: list[str] = [] + for service_name, process in self.processes.items(): + process.join(timeout=self.process_timeout) + if not process.is_alive(): + continue + pgid = self.pgid_dict.get(service_name) if pgid: - try: - os.killpg(pgid, signal.SIGTERM) - except ProcessLookupError: - logging.debug(f"Process group {pgid} for task {task_name} not found.") - # process.terminate() - - for process in self.processes.values(): - process.join() - - logging.debug("All tasks have been stopped") + os.killpg(pgid, signal.SIGKILL) + msg = ( + f"Service '{service_name}' did not terminate in " + "time and was forcefully killed." + ) + log.warning(msg) + else: + failed_to_stop.append(service_name) + msg = ( + f"Service '{service_name}' did not terminate in " + f"time and could not be forcefully killed." + ) + log.warning(msg) + + if failed_to_stop: + log.warning("Some services could not be stopped cleanly:") + for service_name in failed_to_stop: + log.warning(f"\tService '{service_name}' could not be stopped.") + else: + log.info("All services have been stopped") - def stop_task(self, task_name): - if task_name in self.processes: - process = self.processes[task_name] + def stop_service(self, service_name: str) -> None: + """Stops a specific service by name.""" + if service_name in self.processes: + process = self.processes[service_name] if process.is_alive(): - pgid = self.pgid_dict.get(task_name) + pgid = self.pgid_dict.get(service_name) if pgid: os.killpg(pgid, signal.SIGTERM) # Terminate the process group process.terminate() process.join() - logging.debug(f"Task {task_name} has been stopped.") + log.info(f"Service '{service_name}' has been stopped.") else: - logging.debug(f"Task {task_name} not found.") + log.warning(f"Service '{service_name}' not found.") - def check_stop_signal_file(self): - if os.path.exists(self.stop_signal_path) and os.path.isfile(self.stop_signal_path): - logging.debug("Received stop signal") + def is_stop_signal_present(self) -> bool: + """Checks if the stop signal file exists and is a file.""" + if self.stop_signal_path.is_file(): + log.debug("Received stop signal") return True + return False - def check_max_run_time(self, start_time): - if self.max_run_time: - current_time = time.time() - if (current_time - start_time) > self.max_run_time: - logging.debug("Maximum runtime exceeded. Stopping all tasks.") - return True + def is_runtime_exceeded(self, start_time: float) -> bool: + """Checks if the maximum runtime has been exceeded the maximum allowed.""" + if not self.max_run_time: + return False + current_time = time.time() + if (current_time - start_time) > self.max_run_time: + log.debug("Maximum runtime exceeded. Stopping all services.") + return True return False - def check_all_tasks_final(self): - for state in self.state_dict.values(): - if state != 'final': - return False - return True + def is_all_services_final(self) -> bool: + """Checks if all services have reached the final state.""" + return all(state == "final" for state in self.state_dict.values()) diff --git a/shepherd/shepherd.py b/shepherd/shepherd.py deleted file mode 100644 index 4511f55..0000000 --- a/shepherd/shepherd.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python3 -import multiprocessing -import time -import logging -import logging.config -import logging.handlers -import argparse - -from shepherd.service_manager import TaskManager -from shepherd.logging_setup import setup_logging, listener_process -from shepherd._version import __version__ # Import the version - - -def main(): - parser = argparse.ArgumentParser(description='Run Shepherd Workflow Manager') - - parser.add_argument('--config', '-c', type=str, help='Path to the program config YAML file') - parser.add_argument('--log', '-l', type=str, default='shepherd.log', help='Path to the log file') - parser.add_argument('--version', '-v', action='version', version=f'Shepherd {__version__}', - help='Show the version and exit') - - args = parser.parse_args() - - config_path = args.config - log_file = args.log - - start_time = time.time() - - logging_queue = multiprocessing.Queue() - listener = multiprocessing.Process(target=listener_process, args=(logging_queue, log_file)) - listener.start() - setup_logging(logging_queue) - - logging.debug("Starting main") - service_manager = TaskManager(config_path, logging_queue) - service_manager.start_tasks(start_time) - logging.debug("Exiting main") - - logging_queue.put(None) # Send None to the listener to stop it - listener.join() - - -if __name__ == '__main__': - main() diff --git a/shepherd/shepherd_viz.py b/shepherd/shepherd_viz.py index fc6497b..630cc66 100755 --- a/shepherd/shepherd_viz.py +++ b/shepherd/shepherd_viz.py @@ -1,44 +1,66 @@ #!/usr/bin/env python3 -import os +"""Visualize Shepherd workflow manager configurations and state transitions.""" + import argparse import json +from pathlib import Path + import matplotlib.pyplot as plt import numpy as np -from graphviz import Digraph, Source +from graphviz import Digraph +from graphviz import Source -from shepherd.config_loader import load_and_preprocess_config from shepherd._version import __version__ # Import the version +from shepherd.config_loader import load_and_preprocess_config def load_json_from_file(filename): - if filename is None or not os.path.exists(filename): + if filename is None: + return None + filename = Path(filename) + if not filename.exists(): return None - with open(filename) as f: - return json.load(f) + with filename.open(mode="r", encoding="utf-8") as fp: + return json.load(fp) -def render_dot(dot_source, output_filename='workflow_visualization', output_format='png'): +def render_dot( + dot_source, output_filename="workflow_visualization", output_format="png" +): # Todo: try optional unflatten method src = Source(dot_source) src.format = output_format src.render(output_filename) -def generate_state_times_graph(state_transition_times, output_prefix, output_format='png', fig_size=(15, 12)): +def generate_state_times_graph( + state_transition_times, output_prefix, output_format="png", fig_size=(15, 12) +): sorted_state_transition_times = dict( - sorted(state_transition_times.items(), key=lambda x: x[1]['started'], reverse=True)) + sorted( + state_transition_times.items(), key=lambda x: x[1]["started"], reverse=True + ) + ) unique_services = list(sorted_state_transition_times.keys()) unique_states = set( - {state for times in state_transition_times.values() for state in times.keys() if state != 'initialized'}) + { + state + for times in state_transition_times.values() + for state in times + if state != "initialized" + } + ) state_colors = plt.cm.tab20(np.linspace(0, 1, len(unique_states))) - color_map = dict(zip(unique_states, state_colors)) + color_map = dict(zip(unique_states, state_colors, strict=False)) fig, ax = plt.subplots(figsize=fig_size) - for idx, (service, state_times) in enumerate(sorted_state_transition_times.items()): + for idx, (_service, state_times) in enumerate( + sorted_state_transition_times.items() + ): sorted_states = sorted(state_times.items(), key=lambda x: x[1]) previous_time = sorted_states[1][1] # skipping initialized state @@ -46,98 +68,136 @@ def generate_state_times_graph(state_transition_times, output_prefix, output_for for state, state_time in sorted_states[2:]: duration = state_time - previous_time - ax.broken_barh([(previous_time, duration)], (idx - 0.4, 0.8), facecolors=color_map[previous_state], - label=previous_state) + ax.broken_barh( + [(previous_time, duration)], + (idx - 0.4, 0.8), + facecolors=color_map[previous_state], + label=previous_state, + ) previous_time = state_time previous_state = state handles, labels = ax.get_legend_handles_labels() - by_label = dict(zip(labels, handles)) + by_label = dict(zip(labels, handles, strict=False)) ax.legend(by_label.values(), by_label.keys()) ax.set_yticks(range(len(unique_services))) ax.set_yticklabels(unique_services) - ax.set_xlabel('Time (seconds)') - ax.grid(True) + ax.set_xlabel("Time (seconds)") + ax.grid(visible=True) - filename = output_prefix + '_timeline.' + output_format + filename = output_prefix + "_timeline." + output_format plt.savefig(filename) -def generate_state_transition_graph(config, state_transition, output_prefix, output_format='png'): - output_filename = output_prefix + '_workflow_transition' +def generate_state_transition_graph( + config, state_transition, output_prefix, output_format="png" +): + output_filename = output_prefix + "_workflow_transition" - dot = Digraph(comment='Workflow Visualization') + dot = Digraph(comment="Workflow Visualization") colors = { - 'service': 'lightblue', - 'action': 'lightblue', + "service": "lightblue", + "action": "lightblue", } # Add subgraphs for each service with their state transitions - for service, details in config['tasks'].items(): - service_type = details.get('type', 'action') # Default to 'action' if type is not specified - node_color = colors.get(service_type, 'lightgrey') # Default color if no specific type is found - - with dot.subgraph(name=f'cluster_{service}') as sub: - sub.attr(style='filled', color='lightgrey') - sub.node_attr.update(style='filled', color=node_color) + for service, details in config["tasks"].items(): + service_type = details.get( + "type", "action" + ) # Default to 'action' if type is not specified + node_color = colors.get( + service_type, "lightgrey" + ) # Default color if no specific type is found + + with dot.subgraph(name=f"cluster_{service}") as sub: + sub.attr(style="filled", color="lightgrey") + sub.node_attr.update(style="filled", color=node_color) states = state_transition.get(service, {}) # Add nodes for each state for state, time in states.items(): - sub.node(f'{service}_{state}', f'{state}\n{time:.2f}s') + sub.node(f"{service}_{state}", f"{state}\n{time:.2f}s") # Add edges between states state_list = list(states.keys()) for i in range(len(state_list) - 1): - sub.edge(f'{service}_{state_list[i]}', f'{service}_{state_list[i + 1]}') + sub.edge(f"{service}_{state_list[i]}", f"{service}_{state_list[i + 1]}") sub.attr(label=service) # Add edges based on dependencies - for service, details in config['tasks'].items(): - dependencies = details.get('dependency', {}).get('items', {}) + for service, details in config["tasks"].items(): + dependencies = details.get("dependency", {}).get("items", {}) for dep, state in dependencies.items(): - dot.edge(f'{dep}_{state}', f'{service}_started', label=f'{dep} {state}') + dot.edge(f"{dep}_{state}", f"{service}_started", label=f"{dep} {state}") return render_dot(dot.source, output_filename, output_format) -def generate_workflow_graph(config, output_prefix, output_format='png'): - output_filename = output_prefix + '_workflow' +def generate_workflow_graph(config, output_prefix, output_format="png"): + output_filename = output_prefix + "_workflow" - dot = Digraph(comment='Workflow Visualization') + dot = Digraph(comment="Workflow Visualization") colors = { - 'service': 'lightblue', - 'action': 'lightgreen', + "service": "lightblue", + "action": "lightgreen", } # Add nodes with color based on service type - for service, details in config['tasks'].items(): - service_type = details.get('type', 'action') # Default to 'service' if type is not specified - node_color = colors.get(service_type, 'lightgrey') # Default color if no specific type is found - dot.node(service, service, shape='box', style='filled', color=node_color) - - dependencies = details.get('dependency', {}).get('items', {}) + for service, details in config["tasks"].items(): + service_type = details.get( + "type", "action" + ) # Default to 'service' if type is not specified + node_color = colors.get( + service_type, "lightgrey" + ) # Default color if no specific type is found + dot.node(service, service, shape="box", style="filled", color=node_color) + + dependencies = details.get("dependency", {}).get("items", {}) for dep, state in dependencies.items(): - edge_color = colors.get(state, '') + edge_color = colors.get(state, "") dot.edge(dep, service, label=state, color=edge_color, fontcolor=edge_color) return render_dot(dot.source, output_filename, output_format) def main(): - parser = argparse.ArgumentParser(description='Generate visualizations for Shepherd workflow manger.') - parser.add_argument('--config', '-c', type=str, help='Path to the program config YAML file') - parser.add_argument('--state_transition', '-s', type=str, help='Path to the state transition JSON file') - parser.add_argument('--output_prefix', '-p', type=str, default='shepherd', - help='Output filename prefix for the visualization') - parser.add_argument('--output_format', '-f', type=str, default='png', - help='Output format for the visualization (e.g., svg, png)') - parser.add_argument('--version', '-v', action='version', version=f'Shepherd Visualization Tool {__version__}', - help='Show the version and exit') + parser = argparse.ArgumentParser( + description="Generate visualizations for Shepherd workflow manger." + ) + parser.add_argument( + "--config", "-c", type=str, help="Path to the program config YAML file" + ) + parser.add_argument( + "--state_transition", + "-s", + type=str, + help="Path to the state transition JSON file", + ) + parser.add_argument( + "--output_prefix", + "-p", + type=str, + default="shepherd", + help="Output filename prefix for the visualization", + ) + parser.add_argument( + "--output_format", + "-f", + type=str, + default="png", + help="Output format for the visualization (e.g., svg, png)", + ) + parser.add_argument( + "--version", + "-v", + action="version", + version=f"Shepherd Visualization Tool {__version__}", + help="Show the version and exit", + ) args = parser.parse_args() @@ -151,8 +211,10 @@ def main(): if state_transition is not None: generate_state_times_graph(state_transition, output_prefix, output_format) if config is not None and state_transition is not None: - generate_state_transition_graph(config, state_transition, output_prefix, output_format) + generate_state_transition_graph( + config, state_transition, output_prefix, output_format + ) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..391666d --- /dev/null +++ b/uv.lock @@ -0,0 +1,768 @@ +version = 1 +requires-python = ">=3.10" + +[[package]] +name = "click" +version = "8.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, +] + +[[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 = "contourpy" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/f6/31a8f28b4a2a4fa0e01085e542f3081ab0588eff8e589d39d775172c9792/contourpy-1.3.0.tar.gz", hash = "sha256:7ffa0db17717a8ffb127efd0c95a4362d996b892c2904db72428d5b52e1938a4", size = 13464370 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/e0/be8dcc796cfdd96708933e0e2da99ba4bb8f9b2caa9d560a50f3f09a65f3/contourpy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:880ea32e5c774634f9fcd46504bf9f080a41ad855f4fef54f5380f5133d343c7", size = 265366 }, + { url = "https://files.pythonhosted.org/packages/50/d6/c953b400219443535d412fcbbc42e7a5e823291236bc0bb88936e3cc9317/contourpy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:76c905ef940a4474a6289c71d53122a4f77766eef23c03cd57016ce19d0f7b42", size = 249226 }, + { url = "https://files.pythonhosted.org/packages/6f/b4/6fffdf213ffccc28483c524b9dad46bb78332851133b36ad354b856ddc7c/contourpy-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92f8557cbb07415a4d6fa191f20fd9d2d9eb9c0b61d1b2f52a8926e43c6e9af7", size = 308460 }, + { url = "https://files.pythonhosted.org/packages/cf/6c/118fc917b4050f0afe07179a6dcbe4f3f4ec69b94f36c9e128c4af480fb8/contourpy-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36f965570cff02b874773c49bfe85562b47030805d7d8360748f3eca570f4cab", size = 347623 }, + { url = "https://files.pythonhosted.org/packages/f9/a4/30ff110a81bfe3abf7b9673284d21ddce8cc1278f6f77393c91199da4c90/contourpy-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cacd81e2d4b6f89c9f8a5b69b86490152ff39afc58a95af002a398273e5ce589", size = 317761 }, + { url = "https://files.pythonhosted.org/packages/99/e6/d11966962b1aa515f5586d3907ad019f4b812c04e4546cc19ebf62b5178e/contourpy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69375194457ad0fad3a839b9e29aa0b0ed53bb54db1bfb6c3ae43d111c31ce41", size = 322015 }, + { url = "https://files.pythonhosted.org/packages/4d/e3/182383743751d22b7b59c3c753277b6aee3637049197624f333dac5b4c80/contourpy-1.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a52040312b1a858b5e31ef28c2e865376a386c60c0e248370bbea2d3f3b760d", size = 1262672 }, + { url = "https://files.pythonhosted.org/packages/78/53/974400c815b2e605f252c8fb9297e2204347d1755a5374354ee77b1ea259/contourpy-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3faeb2998e4fcb256542e8a926d08da08977f7f5e62cf733f3c211c2a5586223", size = 1321688 }, + { url = "https://files.pythonhosted.org/packages/52/29/99f849faed5593b2926a68a31882af98afbeac39c7fdf7de491d9c85ec6a/contourpy-1.3.0-cp310-cp310-win32.whl", hash = "sha256:36e0cff201bcb17a0a8ecc7f454fe078437fa6bda730e695a92f2d9932bd507f", size = 171145 }, + { url = "https://files.pythonhosted.org/packages/a9/97/3f89bba79ff6ff2b07a3cbc40aa693c360d5efa90d66e914f0ff03b95ec7/contourpy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:87ddffef1dbe5e669b5c2440b643d3fdd8622a348fe1983fad7a0f0ccb1cd67b", size = 216019 }, + { url = "https://files.pythonhosted.org/packages/b3/1f/9375917786cb39270b0ee6634536c0e22abf225825602688990d8f5c6c19/contourpy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fa4c02abe6c446ba70d96ece336e621efa4aecae43eaa9b030ae5fb92b309ad", size = 266356 }, + { url = "https://files.pythonhosted.org/packages/05/46/9256dd162ea52790c127cb58cfc3b9e3413a6e3478917d1f811d420772ec/contourpy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:834e0cfe17ba12f79963861e0f908556b2cedd52e1f75e6578801febcc6a9f49", size = 250915 }, + { url = "https://files.pythonhosted.org/packages/e1/5d/3056c167fa4486900dfbd7e26a2fdc2338dc58eee36d490a0ed3ddda5ded/contourpy-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbc4c3217eee163fa3984fd1567632b48d6dfd29216da3ded3d7b844a8014a66", size = 310443 }, + { url = "https://files.pythonhosted.org/packages/ca/c2/1a612e475492e07f11c8e267ea5ec1ce0d89971be496c195e27afa97e14a/contourpy-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4865cd1d419e0c7a7bf6de1777b185eebdc51470800a9f42b9e9decf17762081", size = 348548 }, + { url = "https://files.pythonhosted.org/packages/45/cf/2c2fc6bb5874158277b4faf136847f0689e1b1a1f640a36d76d52e78907c/contourpy-1.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:303c252947ab4b14c08afeb52375b26781ccd6a5ccd81abcdfc1fafd14cf93c1", size = 319118 }, + { url = "https://files.pythonhosted.org/packages/03/33/003065374f38894cdf1040cef474ad0546368eea7e3a51d48b8a423961f8/contourpy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637f674226be46f6ba372fd29d9523dd977a291f66ab2a74fbeb5530bb3f445d", size = 323162 }, + { url = "https://files.pythonhosted.org/packages/42/80/e637326e85e4105a802e42959f56cff2cd39a6b5ef68d5d9aee3ea5f0e4c/contourpy-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76a896b2f195b57db25d6b44e7e03f221d32fe318d03ede41f8b4d9ba1bff53c", size = 1265396 }, + { url = "https://files.pythonhosted.org/packages/7c/3b/8cbd6416ca1bbc0202b50f9c13b2e0b922b64be888f9d9ee88e6cfabfb51/contourpy-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e1fd23e9d01591bab45546c089ae89d926917a66dceb3abcf01f6105d927e2cb", size = 1324297 }, + { url = "https://files.pythonhosted.org/packages/4d/2c/021a7afaa52fe891f25535506cc861c30c3c4e5a1c1ce94215e04b293e72/contourpy-1.3.0-cp311-cp311-win32.whl", hash = "sha256:d402880b84df3bec6eab53cd0cf802cae6a2ef9537e70cf75e91618a3801c20c", size = 171808 }, + { url = "https://files.pythonhosted.org/packages/8d/2f/804f02ff30a7fae21f98198828d0857439ec4c91a96e20cf2d6c49372966/contourpy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:6cb6cc968059db9c62cb35fbf70248f40994dfcd7aa10444bbf8b3faeb7c2d67", size = 217181 }, + { url = "https://files.pythonhosted.org/packages/c9/92/8e0bbfe6b70c0e2d3d81272b58c98ac69ff1a4329f18c73bd64824d8b12e/contourpy-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:570ef7cf892f0afbe5b2ee410c507ce12e15a5fa91017a0009f79f7d93a1268f", size = 267838 }, + { url = "https://files.pythonhosted.org/packages/e3/04/33351c5d5108460a8ce6d512307690b023f0cfcad5899499f5c83b9d63b1/contourpy-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:da84c537cb8b97d153e9fb208c221c45605f73147bd4cadd23bdae915042aad6", size = 251549 }, + { url = "https://files.pythonhosted.org/packages/51/3d/aa0fe6ae67e3ef9f178389e4caaaa68daf2f9024092aa3c6032e3d174670/contourpy-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0be4d8425bfa755e0fd76ee1e019636ccc7c29f77a7c86b4328a9eb6a26d0639", size = 303177 }, + { url = "https://files.pythonhosted.org/packages/56/c3/c85a7e3e0cab635575d3b657f9535443a6f5d20fac1a1911eaa4bbe1aceb/contourpy-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c0da700bf58f6e0b65312d0a5e695179a71d0163957fa381bb3c1f72972537c", size = 341735 }, + { url = "https://files.pythonhosted.org/packages/dd/8d/20f7a211a7be966a53f474bc90b1a8202e9844b3f1ef85f3ae45a77151ee/contourpy-1.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb8b141bb00fa977d9122636b16aa67d37fd40a3d8b52dd837e536d64b9a4d06", size = 314679 }, + { url = "https://files.pythonhosted.org/packages/6e/be/524e377567defac0e21a46e2a529652d165fed130a0d8a863219303cee18/contourpy-1.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3634b5385c6716c258d0419c46d05c8aa7dc8cb70326c9a4fb66b69ad2b52e09", size = 320549 }, + { url = "https://files.pythonhosted.org/packages/0f/96/fdb2552a172942d888915f3a6663812e9bc3d359d53dafd4289a0fb462f0/contourpy-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0dce35502151b6bd35027ac39ba6e5a44be13a68f55735c3612c568cac3805fd", size = 1263068 }, + { url = "https://files.pythonhosted.org/packages/2a/25/632eab595e3140adfa92f1322bf8915f68c932bac468e89eae9974cf1c00/contourpy-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea348f053c645100612b333adc5983d87be69acdc6d77d3169c090d3b01dc35", size = 1322833 }, + { url = "https://files.pythonhosted.org/packages/73/e3/69738782e315a1d26d29d71a550dbbe3eb6c653b028b150f70c1a5f4f229/contourpy-1.3.0-cp312-cp312-win32.whl", hash = "sha256:90f73a5116ad1ba7174341ef3ea5c3150ddf20b024b98fb0c3b29034752c8aeb", size = 172681 }, + { url = "https://files.pythonhosted.org/packages/0c/89/9830ba00d88e43d15e53d64931e66b8792b46eb25e2050a88fec4a0df3d5/contourpy-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b11b39aea6be6764f84360fce6c82211a9db32a7c7de8fa6dd5397cf1d079c3b", size = 218283 }, + { url = "https://files.pythonhosted.org/packages/53/a1/d20415febfb2267af2d7f06338e82171824d08614084714fb2c1dac9901f/contourpy-1.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3e1c7fa44aaae40a2247e2e8e0627f4bea3dd257014764aa644f319a5f8600e3", size = 267879 }, + { url = "https://files.pythonhosted.org/packages/aa/45/5a28a3570ff6218d8bdfc291a272a20d2648104815f01f0177d103d985e1/contourpy-1.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:364174c2a76057feef647c802652f00953b575723062560498dc7930fc9b1cb7", size = 251573 }, + { url = "https://files.pythonhosted.org/packages/39/1c/d3f51540108e3affa84f095c8b04f0aa833bb797bc8baa218a952a98117d/contourpy-1.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32b238b3b3b649e09ce9aaf51f0c261d38644bdfa35cbaf7b263457850957a84", size = 303184 }, + { url = "https://files.pythonhosted.org/packages/00/56/1348a44fb6c3a558c1a3a0cd23d329d604c99d81bf5a4b58c6b71aab328f/contourpy-1.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d51fca85f9f7ad0b65b4b9fe800406d0d77017d7270d31ec3fb1cc07358fdea0", size = 340262 }, + { url = "https://files.pythonhosted.org/packages/2b/23/00d665ba67e1bb666152131da07e0f24c95c3632d7722caa97fb61470eca/contourpy-1.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:732896af21716b29ab3e988d4ce14bc5133733b85956316fb0c56355f398099b", size = 313806 }, + { url = "https://files.pythonhosted.org/packages/5a/42/3cf40f7040bb8362aea19af9a5fb7b32ce420f645dd1590edcee2c657cd5/contourpy-1.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d73f659398a0904e125280836ae6f88ba9b178b2fed6884f3b1f95b989d2c8da", size = 319710 }, + { url = "https://files.pythonhosted.org/packages/05/32/f3bfa3fc083b25e1a7ae09197f897476ee68e7386e10404bdf9aac7391f0/contourpy-1.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c6c7c2408b7048082932cf4e641fa3b8ca848259212f51c8c59c45aa7ac18f14", size = 1264107 }, + { url = "https://files.pythonhosted.org/packages/1c/1e/1019d34473a736664f2439542b890b2dc4c6245f5c0d8cdfc0ccc2cab80c/contourpy-1.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f317576606de89da6b7e0861cf6061f6146ead3528acabff9236458a6ba467f8", size = 1322458 }, + { url = "https://files.pythonhosted.org/packages/22/85/4f8bfd83972cf8909a4d36d16b177f7b8bdd942178ea4bf877d4a380a91c/contourpy-1.3.0-cp313-cp313-win32.whl", hash = "sha256:31cd3a85dbdf1fc002280c65caa7e2b5f65e4a973fcdf70dd2fdcb9868069294", size = 172643 }, + { url = "https://files.pythonhosted.org/packages/cc/4a/fb3c83c1baba64ba90443626c228ca14f19a87c51975d3b1de308dd2cf08/contourpy-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4553c421929ec95fb07b3aaca0fae668b2eb5a5203d1217ca7c34c063c53d087", size = 218301 }, + { url = "https://files.pythonhosted.org/packages/76/65/702f4064f397821fea0cb493f7d3bc95a5d703e20954dce7d6d39bacf378/contourpy-1.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:345af746d7766821d05d72cb8f3845dfd08dd137101a2cb9b24de277d716def8", size = 278972 }, + { url = "https://files.pythonhosted.org/packages/80/85/21f5bba56dba75c10a45ec00ad3b8190dbac7fd9a8a8c46c6116c933e9cf/contourpy-1.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3bb3808858a9dc68f6f03d319acd5f1b8a337e6cdda197f02f4b8ff67ad2057b", size = 263375 }, + { url = "https://files.pythonhosted.org/packages/0a/64/084c86ab71d43149f91ab3a4054ccf18565f0a8af36abfa92b1467813ed6/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:420d39daa61aab1221567b42eecb01112908b2cab7f1b4106a52caaec8d36973", size = 307188 }, + { url = "https://files.pythonhosted.org/packages/3d/ff/d61a4c288dc42da0084b8d9dc2aa219a850767165d7d9a9c364ff530b509/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d63ee447261e963af02642ffcb864e5a2ee4cbfd78080657a9880b8b1868e18", size = 345644 }, + { url = "https://files.pythonhosted.org/packages/ca/aa/00d2313d35ec03f188e8f0786c2fc61f589306e02fdc158233697546fd58/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:167d6c890815e1dac9536dca00828b445d5d0df4d6a8c6adb4a7ec3166812fa8", size = 317141 }, + { url = "https://files.pythonhosted.org/packages/8d/6a/b5242c8cb32d87f6abf4f5e3044ca397cb1a76712e3fa2424772e3ff495f/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:710a26b3dc80c0e4febf04555de66f5fd17e9cf7170a7b08000601a10570bda6", size = 323469 }, + { url = "https://files.pythonhosted.org/packages/6f/a6/73e929d43028a9079aca4bde107494864d54f0d72d9db508a51ff0878593/contourpy-1.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:75ee7cb1a14c617f34a51d11fa7524173e56551646828353c4af859c56b766e2", size = 1260894 }, + { url = "https://files.pythonhosted.org/packages/2b/1e/1e726ba66eddf21c940821df8cf1a7d15cb165f0682d62161eaa5e93dae1/contourpy-1.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:33c92cdae89ec5135d036e7218e69b0bb2851206077251f04a6c4e0e21f03927", size = 1314829 }, + { url = "https://files.pythonhosted.org/packages/d1/09/60e486dc2b64c94ed33e58dcfb6f808192c03dfc5574c016218b9b7680dc/contourpy-1.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe41b41505a5a33aeaed2a613dccaeaa74e0e3ead6dd6fd3a118fb471644fd6c", size = 261886 }, + { url = "https://files.pythonhosted.org/packages/19/20/b57f9f7174fcd439a7789fb47d764974ab646fa34d1790551de386457a8e/contourpy-1.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca7e17a65f72a5133bdbec9ecf22401c62bcf4821361ef7811faee695799779", size = 311008 }, + { url = "https://files.pythonhosted.org/packages/74/fc/5040d42623a1845d4f17a418e590fd7a79ae8cb2bad2b2f83de63c3bdca4/contourpy-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1ec4dc6bf570f5b22ed0d7efba0dfa9c5b9e0431aeea7581aa217542d9e809a4", size = 215690 }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321 }, +] + +[[package]] +name = "deptry" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "packaging" }, + { name = "requirements-parser" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/71/b7bd84b50aa898f50e0af5e30f50e621ff21d1d83951d2303516a3217fce/deptry-0.21.2.tar.gz", hash = "sha256:4e870553c7a1fafcd99a83ba4137259525679eecabeff61bc669741efa201541", size = 160990 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/b2/66c6d5709edc8cd04c0f1987e868b75e6681f525b5e9c703074bad14b521/deptry-0.21.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e3b9e0c5ee437240b65e61107b5777a12064f78f604bf9f181a96c9b56eb896d", size = 1741649 }, + { url = "https://files.pythonhosted.org/packages/b5/d4/a391784f9e323a50c461878f193848e187f24573085e56aa4e2557eafd25/deptry-0.21.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:d76bbf48bd62ecc44ca3d414769bd4b7956598d23d9ccb42fd359b831a31cab2", size = 1642885 }, + { url = "https://files.pythonhosted.org/packages/6c/4c/c6edfa6860174ca0d929059874c903503084c9eff12776eb17571da4ff71/deptry-0.21.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3080bb88c16ebd35f59cba7688416115b7aaf4630dc5a051dff2649cbf129a1b", size = 1746733 }, + { url = "https://files.pythonhosted.org/packages/a8/4e/36f5ee2adc14a5ee8e46fe749f409f4fc423354de641fd9b0ddf0c912bab/deptry-0.21.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adb12d6678fb5dbd320a0a2e37881059d0a45bec6329df4250c977d803fe7f96", size = 1823962 }, + { url = "https://files.pythonhosted.org/packages/be/cb/a45aa163be1386dade6ca6f61664578662c638254c89bbef81d4e1e80126/deptry-0.21.2-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7479d3079be69c3bbf5913d8e21090749c1139ee91f81520ffce90b5322476b0", size = 1927315 }, + { url = "https://files.pythonhosted.org/packages/a4/fa/84a396a5605fa3dd98c66ba9aa0d25665dd24bab1e42458bdd9ffbd3232f/deptry-0.21.2-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:019167b35301edd2bdd4719c8b8f44769be4507cb8a1cd46fff4393cdbe8d31b", size = 1992695 }, + { url = "https://files.pythonhosted.org/packages/30/87/858bc92296313fe15761ad4c37e64ebda6dee2670c63da6da45fa235e8c1/deptry-0.21.2-cp39-abi3-win_amd64.whl", hash = "sha256:d8add495f0dd19a38aa6d1e09b14b1441bca47c9d945bc7b322efb084313eea3", size = 1601293 }, + { url = "https://files.pythonhosted.org/packages/2c/42/58383595b0062da10f9ed3628748b93d3e6af5a131aead530c86186a3bb4/deptry-0.21.2-cp39-abi3-win_arm64.whl", hash = "sha256:06d48e9fa460aad02f9e1b079d9f5a69d622d291b3a0525b722fc91c88032042", size = 1518159 }, + { url = "https://files.pythonhosted.org/packages/34/32/a60d25162d8005440474adaf52d7c8fe7a474a70db5876c0e4ec8ef727c1/deptry-0.21.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3ef8aed33a2eac357f9565063bc1257bcefa03a37038299c08a4222e28f3cd34", size = 1741596 }, + { url = "https://files.pythonhosted.org/packages/84/14/e4d13e1e7685b65a47a592ec40d12da82cf7a1bacc036a57ebf1b909fc41/deptry-0.21.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:917745db5f8295eb5048e43d9073a9a675ffdba865e9b294d2e7aa455730cb06", size = 1642238 }, + { url = "https://files.pythonhosted.org/packages/94/d9/ac332104378cd28d5149dfaea2d38d1afbe7eecb15d445a25688a9271ab7/deptry-0.21.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:186ddbc69c1f70e684e83e202795e1054d0c2dfc03b8acc077f65dc3b6a7f4ce", size = 1745994 }, + { url = "https://files.pythonhosted.org/packages/3c/dc/187b30b9f339b5a5b5c34ef8445189109778e7493fa9faaa8e07931a7eab/deptry-0.21.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3686e86ad7063b5a6e5253454f9d9e4a7a6b1511a99bd4306fda5424480be48", size = 1823561 }, + { url = "https://files.pythonhosted.org/packages/c3/53/5f6d88e6a9827b67fc8879e5d96f587066c48b35faeb2377ebe9f628fd08/deptry-0.21.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:1012a88500f242489066f811f6ec0c93328d9340bbf0f87f0c7d2146054d197e", size = 1926706 }, + { url = "https://files.pythonhosted.org/packages/e1/59/0151989dbdc5d0a4b3b1c48e25f0a324c0c9ca01aa1c99fbb3506dbcba76/deptry-0.21.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:769bb658172586d1b03046bdc6b6c94f6a98ecfbac04ff7f77ec61768c75e1c2", size = 1992302 }, + { url = "https://files.pythonhosted.org/packages/88/fc/694b7ba5e814bcd2006517e08363721137c556039acd83a1f18fd9d2aa59/deptry-0.21.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:fb2f43747b58abeec01dc277ef22859342f3bca2ac677818c94940a009b436c0", size = 1601732 }, +] + +[[package]] +name = "fonttools" +version = "4.55.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/61/a300d1574dc381393424047c0396a0e213db212e28361123af9830d71a8d/fonttools-4.55.3.tar.gz", hash = "sha256:3983313c2a04d6cc1fe9251f8fc647754cf49a61dac6cb1e7249ae67afaafc45", size = 3498155 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/f3/9ac8c6705e4a0ff3c29e524df1caeee6f2987b02fb630129f21cc99a8212/fonttools-4.55.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1dcc07934a2165ccdc3a5a608db56fb3c24b609658a5b340aee4ecf3ba679dc0", size = 2769857 }, + { url = "https://files.pythonhosted.org/packages/d8/24/e8b8edd280bdb7d0ecc88a5d952b1dec2ee2335be71cc5a33c64871cdfe8/fonttools-4.55.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f7d66c15ba875432a2d2fb419523f5d3d347f91f48f57b8b08a2dfc3c39b8a3f", size = 2299705 }, + { url = "https://files.pythonhosted.org/packages/f8/9e/e1ba20bd3b71870207fd45ca3b90208a7edd8ae3b001081dc31c45adb017/fonttools-4.55.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e4ae3592e62eba83cd2c4ccd9462dcfa603ff78e09110680a5444c6925d841", size = 4576104 }, + { url = "https://files.pythonhosted.org/packages/34/db/d423bc646e6703fe3e6aea0edd22a2df47b9d188c5f7f1b49070be4d2205/fonttools-4.55.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62d65a3022c35e404d19ca14f291c89cc5890032ff04f6c17af0bd1927299674", size = 4618282 }, + { url = "https://files.pythonhosted.org/packages/75/a0/e5062ac960a385b984ba74e7b55132e7f2c65e449e8330ab0f595407a3de/fonttools-4.55.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d342e88764fb201286d185093781bf6628bbe380a913c24adf772d901baa8276", size = 4570539 }, + { url = "https://files.pythonhosted.org/packages/1f/33/0d744ff518ebe50020b63e5018b8b278efd6a930c1d2eedda7defc42153b/fonttools-4.55.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dd68c87a2bfe37c5b33bcda0fba39b65a353876d3b9006fde3adae31f97b3ef5", size = 4742411 }, + { url = "https://files.pythonhosted.org/packages/7e/6c/2f768652dba6b801f1567fc5d1829cda369bcd6e95e315a91e628f91c702/fonttools-4.55.3-cp310-cp310-win32.whl", hash = "sha256:1bc7ad24ff98846282eef1cbeac05d013c2154f977a79886bb943015d2b1b261", size = 2175132 }, + { url = "https://files.pythonhosted.org/packages/19/d1/4dcd865360fb2c499749a913fe80e41c26e8ae18629d87dfffa3de27e831/fonttools-4.55.3-cp310-cp310-win_amd64.whl", hash = "sha256:b54baf65c52952db65df39fcd4820668d0ef4766c0ccdf32879b77f7c804d5c5", size = 2219430 }, + { url = "https://files.pythonhosted.org/packages/4b/18/14be25545600bd100e5b74a3ac39089b7c1cb403dc513b7ca348be3381bf/fonttools-4.55.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8c4491699bad88efe95772543cd49870cf756b019ad56294f6498982408ab03e", size = 2771005 }, + { url = "https://files.pythonhosted.org/packages/b2/51/2e1a5d3871cd7c2ae2054b54e92604e7d6abc3fd3656e9583c399648fe1c/fonttools-4.55.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5323a22eabddf4b24f66d26894f1229261021dacd9d29e89f7872dd8c63f0b8b", size = 2300654 }, + { url = "https://files.pythonhosted.org/packages/73/1a/50109bb2703bc6f774b52ea081db21edf2a9fa4b6d7485faadf9d1b997e9/fonttools-4.55.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5480673f599ad410695ca2ddef2dfefe9df779a9a5cda89503881e503c9c7d90", size = 4877541 }, + { url = "https://files.pythonhosted.org/packages/5d/52/c0b9857fa075da1b8806c5dc2d8342918a8cc2065fd14fbddb3303282693/fonttools-4.55.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da9da6d65cd7aa6b0f806556f4985bcbf603bf0c5c590e61b43aa3e5a0f822d0", size = 4906304 }, + { url = "https://files.pythonhosted.org/packages/0b/1b/55f85c7e962d295e456d5209581c919620ee3e877b95cd86245187a5050f/fonttools-4.55.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e894b5bd60d9f473bed7a8f506515549cc194de08064d829464088d23097331b", size = 4888087 }, + { url = "https://files.pythonhosted.org/packages/83/13/6f2809c612ea2ac51391f92468ff861c63473601530fca96458b453212bf/fonttools-4.55.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aee3b57643827e237ff6ec6d28d9ff9766bd8b21e08cd13bff479e13d4b14765", size = 5056958 }, + { url = "https://files.pythonhosted.org/packages/c1/28/d0ea9e872fa4208b9dfca686e1dd9ca22f6c9ef33ecff2f0ebc2dbe7c29b/fonttools-4.55.3-cp311-cp311-win32.whl", hash = "sha256:eb6ca911c4c17eb51853143624d8dc87cdcdf12a711fc38bf5bd21521e79715f", size = 2173939 }, + { url = "https://files.pythonhosted.org/packages/be/36/d74ae1020bc41a1dff3e6f5a99f646563beecb97e386d27abdac3ba07650/fonttools-4.55.3-cp311-cp311-win_amd64.whl", hash = "sha256:6314bf82c54c53c71805318fcf6786d986461622dd926d92a465199ff54b1b72", size = 2220363 }, + { url = "https://files.pythonhosted.org/packages/89/58/fbcf5dff7e3ea844bb00c4d806ca1e339e1f2dce5529633bf4842c0c9a1f/fonttools-4.55.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f9e736f60f4911061235603a6119e72053073a12c6d7904011df2d8fad2c0e35", size = 2765380 }, + { url = "https://files.pythonhosted.org/packages/81/dd/da6e329e51919b4f421c8738f3497e2ab08c168e76aaef7b6d5351862bdf/fonttools-4.55.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a8aa2c5e5b8b3bcb2e4538d929f6589a5c6bdb84fd16e2ed92649fb5454f11c", size = 2297940 }, + { url = "https://files.pythonhosted.org/packages/00/44/f5ee560858425c99ef07e04919e736db09d6416408e5a8d3bbfb4a6623fd/fonttools-4.55.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07f8288aacf0a38d174445fc78377a97fb0b83cfe352a90c9d9c1400571963c7", size = 4793327 }, + { url = "https://files.pythonhosted.org/packages/24/da/0a001926d791c55e29ac3c52964957a20dbc1963615446b568b7432891c3/fonttools-4.55.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8d5e8916c0970fbc0f6f1bece0063363bb5857a7f170121a4493e31c3db3314", size = 4865624 }, + { url = "https://files.pythonhosted.org/packages/3d/d8/1edd8b13a427a9fb6418373437caa586c0caa57f260af8e0548f4d11e340/fonttools-4.55.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae3b6600565b2d80b7c05acb8e24d2b26ac407b27a3f2e078229721ba5698427", size = 4774166 }, + { url = "https://files.pythonhosted.org/packages/9c/ec/ade054097976c3d6debc9032e09a351505a0196aa5493edf021be376f75e/fonttools-4.55.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:54153c49913f45065c8d9e6d0c101396725c5621c8aee744719300f79771d75a", size = 5001832 }, + { url = "https://files.pythonhosted.org/packages/e2/cd/233f0e31ad799bb91fc78099c8b4e5ec43b85a131688519640d6bae46f6a/fonttools-4.55.3-cp312-cp312-win32.whl", hash = "sha256:827e95fdbbd3e51f8b459af5ea10ecb4e30af50221ca103bea68218e9615de07", size = 2162228 }, + { url = "https://files.pythonhosted.org/packages/46/45/a498b5291f6c0d91b2394b1ed7447442a57d1c9b9cf8f439aee3c316a56e/fonttools-4.55.3-cp312-cp312-win_amd64.whl", hash = "sha256:e6e8766eeeb2de759e862004aa11a9ea3d6f6d5ec710551a88b476192b64fd54", size = 2209118 }, + { url = "https://files.pythonhosted.org/packages/9c/9f/00142a19bad96eeeb1aed93f567adc19b7f2c1af6f5bc0a1c3de90b4b1ac/fonttools-4.55.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a430178ad3e650e695167cb53242dae3477b35c95bef6525b074d87493c4bf29", size = 2752812 }, + { url = "https://files.pythonhosted.org/packages/b0/20/14b8250d63ba65e162091fb0dda07730f90c303bbf5257e9ddacec7230d9/fonttools-4.55.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:529cef2ce91dc44f8e407cc567fae6e49a1786f2fefefa73a294704c415322a4", size = 2291521 }, + { url = "https://files.pythonhosted.org/packages/34/47/a681cfd10245eb74f65e491a934053ec75c4af639655446558f29818e45e/fonttools-4.55.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e75f12c82127486fac2d8bfbf5bf058202f54bf4f158d367e41647b972342ca", size = 4770980 }, + { url = "https://files.pythonhosted.org/packages/d2/6c/a7066afc19db0705a12efd812e19c32cde2b9514eb714659522f2ebd60b6/fonttools-4.55.3-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:859c358ebf41db18fb72342d3080bce67c02b39e86b9fbcf1610cca14984841b", size = 4845534 }, + { url = "https://files.pythonhosted.org/packages/0c/a2/3c204fbabbfd845d9bdcab9ae35279d41e9a4bf5c80a0a2708f9c5a195d6/fonttools-4.55.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:546565028e244a701f73df6d8dd6be489d01617863ec0c6a42fa25bf45d43048", size = 4753910 }, + { url = "https://files.pythonhosted.org/packages/6e/8c/b4cb3592880340b89e4ef6601b531780bba73862332a6451d78fe135d6cb/fonttools-4.55.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:aca318b77f23523309eec4475d1fbbb00a6b133eb766a8bdc401faba91261abe", size = 4976411 }, + { url = "https://files.pythonhosted.org/packages/fc/a8/4bf98840ff89fcc188470b59daec57322178bf36d2f4f756cd19a42a826b/fonttools-4.55.3-cp313-cp313-win32.whl", hash = "sha256:8c5ec45428edaa7022f1c949a632a6f298edc7b481312fc7dc258921e9399628", size = 2160178 }, + { url = "https://files.pythonhosted.org/packages/e6/57/4cc35004605416df3225ff362f3455cf09765db00df578ae9e46d0fefd23/fonttools-4.55.3-cp313-cp313-win_amd64.whl", hash = "sha256:11e5de1ee0d95af4ae23c1a138b184b7f06e0b6abacabf1d0db41c90b03d834b", size = 2206102 }, + { url = "https://files.pythonhosted.org/packages/99/3b/406d17b1f63e04a82aa621936e6e1c53a8c05458abd66300ac85ea7f9ae9/fonttools-4.55.3-py3-none-any.whl", hash = "sha256:f412604ccbeee81b091b420272841e5ec5ef68967a9790e80bffd0e30b8e2977", size = 1111638 }, +] + +[[package]] +name = "graphviz" +version = "0.20.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/83/5a40d19b8347f017e417710907f824915fba411a9befd092e52746b63e9f/graphviz-0.20.3.zip", hash = "sha256:09d6bc81e6a9fa392e7ba52135a9d49f1ed62526f96499325930e87ca1b5925d", size = 256455 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/be/d59db2d1d52697c6adc9eacaf50e8965b6345cc143f671e1ed068818d5cf/graphviz-0.20.3-py3-none-any.whl", hash = "sha256:81f848f2904515d8cd359cc611faba817598d2feaac4027b266aa3eda7b3dde5", size = 47126 }, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/4d/2255e1c76304cbd60b48cee302b66d1dde4468dc5b1160e4b7cb43778f2a/kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60", size = 97286 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/14/fc943dd65268a96347472b4fbe5dcc2f6f55034516f80576cd0dd3a8930f/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6", size = 122440 }, + { url = "https://files.pythonhosted.org/packages/1e/46/e68fed66236b69dd02fcdb506218c05ac0e39745d696d22709498896875d/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17", size = 65758 }, + { url = "https://files.pythonhosted.org/packages/ef/fa/65de49c85838681fc9cb05de2a68067a683717321e01ddafb5b8024286f0/kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9", size = 64311 }, + { url = "https://files.pythonhosted.org/packages/42/9c/cc8d90f6ef550f65443bad5872ffa68f3dee36de4974768628bea7c14979/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9", size = 1637109 }, + { url = "https://files.pythonhosted.org/packages/55/91/0a57ce324caf2ff5403edab71c508dd8f648094b18cfbb4c8cc0fde4a6ac/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c", size = 1617814 }, + { url = "https://files.pythonhosted.org/packages/12/5d/c36140313f2510e20207708adf36ae4919416d697ee0236b0ddfb6fd1050/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599", size = 1400881 }, + { url = "https://files.pythonhosted.org/packages/56/d0/786e524f9ed648324a466ca8df86298780ef2b29c25313d9a4f16992d3cf/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05", size = 1512972 }, + { url = "https://files.pythonhosted.org/packages/67/5a/77851f2f201e6141d63c10a0708e996a1363efaf9e1609ad0441b343763b/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407", size = 1444787 }, + { url = "https://files.pythonhosted.org/packages/06/5f/1f5eaab84355885e224a6fc8d73089e8713dc7e91c121f00b9a1c58a2195/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278", size = 2199212 }, + { url = "https://files.pythonhosted.org/packages/b5/28/9152a3bfe976a0ae21d445415defc9d1cd8614b2910b7614b30b27a47270/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5", size = 2346399 }, + { url = "https://files.pythonhosted.org/packages/26/f6/453d1904c52ac3b400f4d5e240ac5fec25263716723e44be65f4d7149d13/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad", size = 2308688 }, + { url = "https://files.pythonhosted.org/packages/5a/9a/d4968499441b9ae187e81745e3277a8b4d7c60840a52dc9d535a7909fac3/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895", size = 2445493 }, + { url = "https://files.pythonhosted.org/packages/07/c9/032267192e7828520dacb64dfdb1d74f292765f179e467c1cba97687f17d/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3", size = 2262191 }, + { url = "https://files.pythonhosted.org/packages/6c/ad/db0aedb638a58b2951da46ddaeecf204be8b4f5454df020d850c7fa8dca8/kiwisolver-1.4.7-cp310-cp310-win32.whl", hash = "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc", size = 46644 }, + { url = "https://files.pythonhosted.org/packages/12/ca/d0f7b7ffbb0be1e7c2258b53554efec1fd652921f10d7d85045aff93ab61/kiwisolver-1.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c", size = 55877 }, + { url = "https://files.pythonhosted.org/packages/97/6c/cfcc128672f47a3e3c0d918ecb67830600078b025bfc32d858f2e2d5c6a4/kiwisolver-1.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a", size = 48347 }, + { url = "https://files.pythonhosted.org/packages/e9/44/77429fa0a58f941d6e1c58da9efe08597d2e86bf2b2cce6626834f49d07b/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54", size = 122442 }, + { url = "https://files.pythonhosted.org/packages/e5/20/8c75caed8f2462d63c7fd65e16c832b8f76cda331ac9e615e914ee80bac9/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95", size = 65762 }, + { url = "https://files.pythonhosted.org/packages/f4/98/fe010f15dc7230f45bc4cf367b012d651367fd203caaa992fd1f5963560e/kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935", size = 64319 }, + { url = "https://files.pythonhosted.org/packages/8b/1b/b5d618f4e58c0675654c1e5051bcf42c776703edb21c02b8c74135541f60/kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb", size = 1334260 }, + { url = "https://files.pythonhosted.org/packages/b8/01/946852b13057a162a8c32c4c8d2e9ed79f0bb5d86569a40c0b5fb103e373/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02", size = 1426589 }, + { url = "https://files.pythonhosted.org/packages/70/d1/c9f96df26b459e15cf8a965304e6e6f4eb291e0f7a9460b4ad97b047561e/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51", size = 1541080 }, + { url = "https://files.pythonhosted.org/packages/d3/73/2686990eb8b02d05f3de759d6a23a4ee7d491e659007dd4c075fede4b5d0/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052", size = 1470049 }, + { url = "https://files.pythonhosted.org/packages/a7/4b/2db7af3ed3af7c35f388d5f53c28e155cd402a55432d800c543dc6deb731/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18", size = 1426376 }, + { url = "https://files.pythonhosted.org/packages/05/83/2857317d04ea46dc5d115f0df7e676997bbd968ced8e2bd6f7f19cfc8d7f/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545", size = 2222231 }, + { url = "https://files.pythonhosted.org/packages/0d/b5/866f86f5897cd4ab6d25d22e403404766a123f138bd6a02ecb2cdde52c18/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b", size = 2368634 }, + { url = "https://files.pythonhosted.org/packages/c1/ee/73de8385403faba55f782a41260210528fe3273d0cddcf6d51648202d6d0/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36", size = 2329024 }, + { url = "https://files.pythonhosted.org/packages/a1/e7/cd101d8cd2cdfaa42dc06c433df17c8303d31129c9fdd16c0ea37672af91/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3", size = 2468484 }, + { url = "https://files.pythonhosted.org/packages/e1/72/84f09d45a10bc57a40bb58b81b99d8f22b58b2040c912b7eb97ebf625bf2/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523", size = 2284078 }, + { url = "https://files.pythonhosted.org/packages/d2/d4/71828f32b956612dc36efd7be1788980cb1e66bfb3706e6dec9acad9b4f9/kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d", size = 46645 }, + { url = "https://files.pythonhosted.org/packages/a1/65/d43e9a20aabcf2e798ad1aff6c143ae3a42cf506754bcb6a7ed8259c8425/kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b", size = 56022 }, + { url = "https://files.pythonhosted.org/packages/35/b3/9f75a2e06f1b4ca00b2b192bc2b739334127d27f1d0625627ff8479302ba/kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376", size = 48536 }, + { url = "https://files.pythonhosted.org/packages/97/9c/0a11c714cf8b6ef91001c8212c4ef207f772dd84540104952c45c1f0a249/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2", size = 121808 }, + { url = "https://files.pythonhosted.org/packages/f2/d8/0fe8c5f5d35878ddd135f44f2af0e4e1d379e1c7b0716f97cdcb88d4fd27/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a", size = 65531 }, + { url = "https://files.pythonhosted.org/packages/80/c5/57fa58276dfdfa612241d640a64ca2f76adc6ffcebdbd135b4ef60095098/kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee", size = 63894 }, + { url = "https://files.pythonhosted.org/packages/8b/e9/26d3edd4c4ad1c5b891d8747a4f81b1b0aba9fb9721de6600a4adc09773b/kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640", size = 1369296 }, + { url = "https://files.pythonhosted.org/packages/b6/67/3f4850b5e6cffb75ec40577ddf54f7b82b15269cc5097ff2e968ee32ea7d/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f", size = 1461450 }, + { url = "https://files.pythonhosted.org/packages/52/be/86cbb9c9a315e98a8dc6b1d23c43cffd91d97d49318854f9c37b0e41cd68/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483", size = 1579168 }, + { url = "https://files.pythonhosted.org/packages/0f/00/65061acf64bd5fd34c1f4ae53f20b43b0a017a541f242a60b135b9d1e301/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258", size = 1507308 }, + { url = "https://files.pythonhosted.org/packages/21/e4/c0b6746fd2eb62fe702118b3ca0cb384ce95e1261cfada58ff693aeec08a/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e", size = 1464186 }, + { url = "https://files.pythonhosted.org/packages/0a/0f/529d0a9fffb4d514f2782c829b0b4b371f7f441d61aa55f1de1c614c4ef3/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107", size = 2247877 }, + { url = "https://files.pythonhosted.org/packages/d1/e1/66603ad779258843036d45adcbe1af0d1a889a07af4635f8b4ec7dccda35/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948", size = 2404204 }, + { url = "https://files.pythonhosted.org/packages/8d/61/de5fb1ca7ad1f9ab7970e340a5b833d735df24689047de6ae71ab9d8d0e7/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038", size = 2352461 }, + { url = "https://files.pythonhosted.org/packages/ba/d2/0edc00a852e369827f7e05fd008275f550353f1f9bcd55db9363d779fc63/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383", size = 2501358 }, + { url = "https://files.pythonhosted.org/packages/84/15/adc15a483506aec6986c01fb7f237c3aec4d9ed4ac10b756e98a76835933/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520", size = 2314119 }, + { url = "https://files.pythonhosted.org/packages/36/08/3a5bb2c53c89660863a5aa1ee236912269f2af8762af04a2e11df851d7b2/kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b", size = 46367 }, + { url = "https://files.pythonhosted.org/packages/19/93/c05f0a6d825c643779fc3c70876bff1ac221f0e31e6f701f0e9578690d70/kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb", size = 55884 }, + { url = "https://files.pythonhosted.org/packages/d2/f9/3828d8f21b6de4279f0667fb50a9f5215e6fe57d5ec0d61905914f5b6099/kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a", size = 48528 }, + { url = "https://files.pythonhosted.org/packages/c4/06/7da99b04259b0f18b557a4effd1b9c901a747f7fdd84cf834ccf520cb0b2/kiwisolver-1.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2e6039dcbe79a8e0f044f1c39db1986a1b8071051efba3ee4d74f5b365f5226e", size = 121913 }, + { url = "https://files.pythonhosted.org/packages/97/f5/b8a370d1aa593c17882af0a6f6755aaecd643640c0ed72dcfd2eafc388b9/kiwisolver-1.4.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a1ecf0ac1c518487d9d23b1cd7139a6a65bc460cd101ab01f1be82ecf09794b6", size = 65627 }, + { url = "https://files.pythonhosted.org/packages/2a/fc/6c0374f7503522539e2d4d1b497f5ebad3f8ed07ab51aed2af988dd0fb65/kiwisolver-1.4.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ab9ccab2b5bd5702ab0803676a580fffa2aa178c2badc5557a84cc943fcf750", size = 63888 }, + { url = "https://files.pythonhosted.org/packages/bf/3e/0b7172793d0f41cae5c923492da89a2ffcd1adf764c16159ca047463ebd3/kiwisolver-1.4.7-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f816dd2277f8d63d79f9c8473a79fe54047bc0467754962840782c575522224d", size = 1369145 }, + { url = "https://files.pythonhosted.org/packages/77/92/47d050d6f6aced2d634258123f2688fbfef8ded3c5baf2c79d94d91f1f58/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf8bcc23ceb5a1b624572a1623b9f79d2c3b337c8c455405ef231933a10da379", size = 1461448 }, + { url = "https://files.pythonhosted.org/packages/9c/1b/8f80b18e20b3b294546a1adb41701e79ae21915f4175f311a90d042301cf/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dea0bf229319828467d7fca8c7c189780aa9ff679c94539eed7532ebe33ed37c", size = 1578750 }, + { url = "https://files.pythonhosted.org/packages/a4/fe/fe8e72f3be0a844f257cadd72689c0848c6d5c51bc1d60429e2d14ad776e/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c06a4c7cf15ec739ce0e5971b26c93638730090add60e183530d70848ebdd34", size = 1507175 }, + { url = "https://files.pythonhosted.org/packages/39/fa/cdc0b6105d90eadc3bee525fecc9179e2b41e1ce0293caaf49cb631a6aaf/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:913983ad2deb14e66d83c28b632fd35ba2b825031f2fa4ca29675e665dfecbe1", size = 1463963 }, + { url = "https://files.pythonhosted.org/packages/6e/5c/0c03c4e542720c6177d4f408e56d1c8315899db72d46261a4e15b8b33a41/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5337ec7809bcd0f424c6b705ecf97941c46279cf5ed92311782c7c9c2026f07f", size = 2248220 }, + { url = "https://files.pythonhosted.org/packages/3d/ee/55ef86d5a574f4e767df7da3a3a7ff4954c996e12d4fbe9c408170cd7dcc/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c26ed10c4f6fa6ddb329a5120ba3b6db349ca192ae211e882970bfc9d91420b", size = 2404463 }, + { url = "https://files.pythonhosted.org/packages/0f/6d/73ad36170b4bff4825dc588acf4f3e6319cb97cd1fb3eb04d9faa6b6f212/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c619b101e6de2222c1fcb0531e1b17bbffbe54294bfba43ea0d411d428618c27", size = 2352842 }, + { url = "https://files.pythonhosted.org/packages/0b/16/fa531ff9199d3b6473bb4d0f47416cdb08d556c03b8bc1cccf04e756b56d/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a", size = 2501635 }, + { url = "https://files.pythonhosted.org/packages/78/7e/aa9422e78419db0cbe75fb86d8e72b433818f2e62e2e394992d23d23a583/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3ce6b2b0231bda412463e152fc18335ba32faf4e8c23a754ad50ffa70e4091ee", size = 2314556 }, + { url = "https://files.pythonhosted.org/packages/a8/b2/15f7f556df0a6e5b3772a1e076a9d9f6c538ce5f05bd590eca8106508e06/kiwisolver-1.4.7-cp313-cp313-win32.whl", hash = "sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07", size = 46364 }, + { url = "https://files.pythonhosted.org/packages/0b/db/32e897e43a330eee8e4770bfd2737a9584b23e33587a0812b8e20aac38f7/kiwisolver-1.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76", size = 55887 }, + { url = "https://files.pythonhosted.org/packages/c8/a4/df2bdca5270ca85fd25253049eb6708d4127be2ed0e5c2650217450b59e9/kiwisolver-1.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650", size = 48530 }, + { url = "https://files.pythonhosted.org/packages/ac/59/741b79775d67ab67ced9bb38552da688c0305c16e7ee24bba7a2be253fb7/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643", size = 59491 }, + { url = "https://files.pythonhosted.org/packages/58/cc/fb239294c29a5656e99e3527f7369b174dd9cc7c3ef2dea7cb3c54a8737b/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706", size = 57648 }, + { url = "https://files.pythonhosted.org/packages/3b/ef/2f009ac1f7aab9f81efb2d837301d255279d618d27b6015780115ac64bdd/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6", size = 84257 }, + { url = "https://files.pythonhosted.org/packages/81/e1/c64f50987f85b68b1c52b464bb5bf73e71570c0f7782d626d1eb283ad620/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2", size = 80906 }, + { url = "https://files.pythonhosted.org/packages/fd/71/1687c5c0a0be2cee39a5c9c389e546f9c6e215e46b691d00d9f646892083/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4", size = 79951 }, + { url = "https://files.pythonhosted.org/packages/ea/8b/d7497df4a1cae9367adf21665dd1f896c2a7aeb8769ad77b662c5e2bcce7/kiwisolver-1.4.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a", size = 55715 }, +] + +[[package]] +name = "loguru" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "win32-setctime", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595 }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +] + +[[package]] +name = "matplotlib" +version = "3.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/17/1747b4154034befd0ed33b52538f5eb7752d05bb51c5e2a31470c3bc7d52/matplotlib-3.9.4.tar.gz", hash = "sha256:1e00e8be7393cbdc6fedfa8a6fba02cf3e83814b285db1c60b906a023ba41bc3", size = 36106529 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/94/27d2e2c30d54b56c7b764acc1874a909e34d1965a427fc7092bb6a588b63/matplotlib-3.9.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fdd7abfb706dfa8d307af64a87f1a862879ec3cd8d0ec8637458f0885b9c50", size = 7885089 }, + { url = "https://files.pythonhosted.org/packages/c6/25/828273307e40a68eb8e9df832b6b2aaad075864fdc1de4b1b81e40b09e48/matplotlib-3.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d89bc4e85e40a71d1477780366c27fb7c6494d293e1617788986f74e2a03d7ff", size = 7770600 }, + { url = "https://files.pythonhosted.org/packages/f2/65/f841a422ec994da5123368d76b126acf4fc02ea7459b6e37c4891b555b83/matplotlib-3.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddf9f3c26aae695c5daafbf6b94e4c1a30d6cd617ba594bbbded3b33a1fcfa26", size = 8200138 }, + { url = "https://files.pythonhosted.org/packages/07/06/272aca07a38804d93b6050813de41ca7ab0e29ba7a9dd098e12037c919a9/matplotlib-3.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18ebcf248030173b59a868fda1fe42397253f6698995b55e81e1f57431d85e50", size = 8312711 }, + { url = "https://files.pythonhosted.org/packages/98/37/f13e23b233c526b7e27ad61be0a771894a079e0f7494a10d8d81557e0e9a/matplotlib-3.9.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974896ec43c672ec23f3f8c648981e8bc880ee163146e0312a9b8def2fac66f5", size = 9090622 }, + { url = "https://files.pythonhosted.org/packages/4f/8c/b1f5bd2bd70e60f93b1b54c4d5ba7a992312021d0ddddf572f9a1a6d9348/matplotlib-3.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:4598c394ae9711cec135639374e70871fa36b56afae17bdf032a345be552a88d", size = 7828211 }, + { url = "https://files.pythonhosted.org/packages/74/4b/65be7959a8fa118a3929b49a842de5b78bb55475236fcf64f3e308ff74a0/matplotlib-3.9.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d4dd29641d9fb8bc4492420c5480398dd40a09afd73aebe4eb9d0071a05fbe0c", size = 7894430 }, + { url = "https://files.pythonhosted.org/packages/e9/18/80f70d91896e0a517b4a051c3fd540daa131630fd75e02e250365353b253/matplotlib-3.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30e5b22e8bcfb95442bf7d48b0d7f3bdf4a450cbf68986ea45fca3d11ae9d099", size = 7780045 }, + { url = "https://files.pythonhosted.org/packages/a2/73/ccb381026e3238c5c25c3609ba4157b2d1a617ec98d65a8b4ee4e1e74d02/matplotlib-3.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bb0030d1d447fd56dcc23b4c64a26e44e898f0416276cac1ebc25522e0ac249", size = 8209906 }, + { url = "https://files.pythonhosted.org/packages/ab/33/1648da77b74741c89f5ea95cbf42a291b4b364f2660b316318811404ed97/matplotlib-3.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aca90ed222ac3565d2752b83dbb27627480d27662671e4d39da72e97f657a423", size = 8322873 }, + { url = "https://files.pythonhosted.org/packages/57/d3/8447ba78bc6593c9044c372d1609f8ea10fb1e071e7a9e0747bea74fc16c/matplotlib-3.9.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a181b2aa2906c608fcae72f977a4a2d76e385578939891b91c2550c39ecf361e", size = 9099566 }, + { url = "https://files.pythonhosted.org/packages/23/e1/4f0e237bf349c02ff9d1b6e7109f1a17f745263809b9714a8576dc17752b/matplotlib-3.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:1f6882828231eca17f501c4dcd98a05abb3f03d157fbc0769c6911fe08b6cfd3", size = 7838065 }, + { url = "https://files.pythonhosted.org/packages/1a/2b/c918bf6c19d6445d1cefe3d2e42cb740fb997e14ab19d4daeb6a7ab8a157/matplotlib-3.9.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dfc48d67e6661378a21c2983200a654b72b5c5cdbd5d2cf6e5e1ece860f0cc70", size = 7891131 }, + { url = "https://files.pythonhosted.org/packages/c1/e5/b4e8fc601ca302afeeabf45f30e706a445c7979a180e3a978b78b2b681a4/matplotlib-3.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47aef0fab8332d02d68e786eba8113ffd6f862182ea2999379dec9e237b7e483", size = 7776365 }, + { url = "https://files.pythonhosted.org/packages/99/06/b991886c506506476e5d83625c5970c656a491b9f80161458fed94597808/matplotlib-3.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fba1f52c6b7dc764097f52fd9ab627b90db452c9feb653a59945de16752e965f", size = 8200707 }, + { url = "https://files.pythonhosted.org/packages/c3/e2/556b627498cb27e61026f2d1ba86a78ad1b836fef0996bef5440e8bc9559/matplotlib-3.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:173ac3748acaac21afcc3fa1633924609ba1b87749006bc25051c52c422a5d00", size = 8313761 }, + { url = "https://files.pythonhosted.org/packages/58/ff/165af33ec766ff818306ea88e91f9f60d2a6ed543be1eb122a98acbf3b0d/matplotlib-3.9.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320edea0cadc07007765e33f878b13b3738ffa9745c5f707705692df70ffe0e0", size = 9095284 }, + { url = "https://files.pythonhosted.org/packages/9f/8b/3d0c7a002db3b1ed702731c2a9a06d78d035f1f2fb0fb936a8e43cc1e9f4/matplotlib-3.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a4a4cfc82330b27042a7169533da7991e8789d180dd5b3daeaee57d75cd5a03b", size = 7841160 }, + { url = "https://files.pythonhosted.org/packages/49/b1/999f89a7556d101b23a2f0b54f1b6e140d73f56804da1398f2f0bc0924bc/matplotlib-3.9.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37eeffeeca3c940985b80f5b9a7b95ea35671e0e7405001f249848d2b62351b6", size = 7891499 }, + { url = "https://files.pythonhosted.org/packages/87/7b/06a32b13a684977653396a1bfcd34d4e7539c5d55c8cbfaa8ae04d47e4a9/matplotlib-3.9.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3e7465ac859ee4abcb0d836137cd8414e7bb7ad330d905abced457217d4f0f45", size = 7776802 }, + { url = "https://files.pythonhosted.org/packages/65/87/ac498451aff739e515891bbb92e566f3c7ef31891aaa878402a71f9b0910/matplotlib-3.9.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4c12302c34afa0cf061bea23b331e747e5e554b0fa595c96e01c7b75bc3b858", size = 8200802 }, + { url = "https://files.pythonhosted.org/packages/f8/6b/9eb761c00e1cb838f6c92e5f25dcda3f56a87a52f6cb8fdfa561e6cf6a13/matplotlib-3.9.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b8c97917f21b75e72108b97707ba3d48f171541a74aa2a56df7a40626bafc64", size = 8313880 }, + { url = "https://files.pythonhosted.org/packages/d7/a2/c8eaa600e2085eec7e38cbbcc58a30fc78f8224939d31d3152bdafc01fd1/matplotlib-3.9.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0229803bd7e19271b03cb09f27db76c918c467aa4ce2ae168171bc67c3f508df", size = 9094637 }, + { url = "https://files.pythonhosted.org/packages/71/1f/c6e1daea55b7bfeb3d84c6cb1abc449f6a02b181e7e2a5e4db34c3afb793/matplotlib-3.9.4-cp313-cp313-win_amd64.whl", hash = "sha256:7c0d8ef442ebf56ff5e206f8083d08252ee738e04f3dc88ea882853a05488799", size = 7841311 }, + { url = "https://files.pythonhosted.org/packages/c0/3a/2757d3f7d388b14dd48f5a83bea65b6d69f000e86b8f28f74d86e0d375bd/matplotlib-3.9.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a04c3b00066a688834356d196136349cb32f5e1003c55ac419e91585168b88fb", size = 7919989 }, + { url = "https://files.pythonhosted.org/packages/24/28/f5077c79a4f521589a37fe1062d6a6ea3534e068213f7357e7cfffc2e17a/matplotlib-3.9.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04c519587f6c210626741a1e9a68eefc05966ede24205db8982841826af5871a", size = 7809417 }, + { url = "https://files.pythonhosted.org/packages/36/c8/c523fd2963156692916a8eb7d4069084cf729359f7955cf09075deddfeaf/matplotlib-3.9.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308afbf1a228b8b525fcd5cec17f246bbbb63b175a3ef6eb7b4d33287ca0cf0c", size = 8226258 }, + { url = "https://files.pythonhosted.org/packages/f6/88/499bf4b8fa9349b6f5c0cf4cead0ebe5da9d67769129f1b5651e5ac51fbc/matplotlib-3.9.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddb3b02246ddcffd3ce98e88fed5b238bc5faff10dbbaa42090ea13241d15764", size = 8335849 }, + { url = "https://files.pythonhosted.org/packages/b8/9f/20a4156b9726188646a030774ee337d5ff695a965be45ce4dbcb9312c170/matplotlib-3.9.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8a75287e9cb9eee48cb79ec1d806f75b29c0fde978cb7223a1f4c5848d696041", size = 9102152 }, + { url = "https://files.pythonhosted.org/packages/10/11/237f9c3a4e8d810b1759b67ff2da7c32c04f9c80aa475e7beb36ed43a8fb/matplotlib-3.9.4-cp313-cp313t-win_amd64.whl", hash = "sha256:488deb7af140f0ba86da003e66e10d55ff915e152c78b4b66d231638400b1965", size = 7896987 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[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 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "pillow" +version = "11.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/26/0d95c04c868f6bdb0c447e3ee2de5564411845e36a858cfd63766bc7b563/pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739", size = 46737780 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/fb/a6ce6836bd7fd93fbf9144bf54789e02babc27403b50a9e1583ee877d6da/pillow-11.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947", size = 3154708 }, + { url = "https://files.pythonhosted.org/packages/6a/1d/1f51e6e912d8ff316bb3935a8cda617c801783e0b998bf7a894e91d3bd4c/pillow-11.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba", size = 2979223 }, + { url = "https://files.pythonhosted.org/packages/90/83/e2077b0192ca8a9ef794dbb74700c7e48384706467067976c2a95a0f40a1/pillow-11.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086", size = 4183167 }, + { url = "https://files.pythonhosted.org/packages/0e/74/467af0146970a98349cdf39e9b79a6cc8a2e7558f2c01c28a7b6b85c5bda/pillow-11.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9", size = 4283912 }, + { url = "https://files.pythonhosted.org/packages/85/b1/d95d4f7ca3a6c1ae120959605875a31a3c209c4e50f0029dc1a87566cf46/pillow-11.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488", size = 4195815 }, + { url = "https://files.pythonhosted.org/packages/41/c3/94f33af0762ed76b5a237c5797e088aa57f2b7fa8ee7932d399087be66a8/pillow-11.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f", size = 4366117 }, + { url = "https://files.pythonhosted.org/packages/ba/3c/443e7ef01f597497268899e1cca95c0de947c9bbf77a8f18b3c126681e5d/pillow-11.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb", size = 4278607 }, + { url = "https://files.pythonhosted.org/packages/26/95/1495304448b0081e60c0c5d63f928ef48bb290acee7385804426fa395a21/pillow-11.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97", size = 4410685 }, + { url = "https://files.pythonhosted.org/packages/45/da/861e1df971ef0de9870720cb309ca4d553b26a9483ec9be3a7bf1de4a095/pillow-11.0.0-cp310-cp310-win32.whl", hash = "sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50", size = 2249185 }, + { url = "https://files.pythonhosted.org/packages/d5/4e/78f7c5202ea2a772a5ab05069c1b82503e6353cd79c7e474d4945f4b82c3/pillow-11.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c", size = 2566726 }, + { url = "https://files.pythonhosted.org/packages/77/e4/6e84eada35cbcc646fc1870f72ccfd4afacb0fae0c37ffbffe7f5dc24bf1/pillow-11.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1", size = 2254585 }, + { url = "https://files.pythonhosted.org/packages/f0/eb/f7e21b113dd48a9c97d364e0915b3988c6a0b6207652f5a92372871b7aa4/pillow-11.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc", size = 3154705 }, + { url = "https://files.pythonhosted.org/packages/25/b3/2b54a1d541accebe6bd8b1358b34ceb2c509f51cb7dcda8687362490da5b/pillow-11.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a", size = 2979222 }, + { url = "https://files.pythonhosted.org/packages/20/12/1a41eddad8265c5c19dda8fb6c269ce15ee25e0b9f8f26286e6202df6693/pillow-11.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3", size = 4190220 }, + { url = "https://files.pythonhosted.org/packages/a9/9b/8a8c4d07d77447b7457164b861d18f5a31ae6418ef5c07f6f878fa09039a/pillow-11.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5", size = 4291399 }, + { url = "https://files.pythonhosted.org/packages/fc/e4/130c5fab4a54d3991129800dd2801feeb4b118d7630148cd67f0e6269d4c/pillow-11.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b", size = 4202709 }, + { url = "https://files.pythonhosted.org/packages/39/63/b3fc299528d7df1f678b0666002b37affe6b8751225c3d9c12cf530e73ed/pillow-11.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa", size = 4372556 }, + { url = "https://files.pythonhosted.org/packages/c6/a6/694122c55b855b586c26c694937d36bb8d3b09c735ff41b2f315c6e66a10/pillow-11.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306", size = 4287187 }, + { url = "https://files.pythonhosted.org/packages/ba/a9/f9d763e2671a8acd53d29b1e284ca298bc10a595527f6be30233cdb9659d/pillow-11.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9", size = 4418468 }, + { url = "https://files.pythonhosted.org/packages/6e/0e/b5cbad2621377f11313a94aeb44ca55a9639adabcaaa073597a1925f8c26/pillow-11.0.0-cp311-cp311-win32.whl", hash = "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5", size = 2249249 }, + { url = "https://files.pythonhosted.org/packages/dc/83/1470c220a4ff06cd75fc609068f6605e567ea51df70557555c2ab6516b2c/pillow-11.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291", size = 2566769 }, + { url = "https://files.pythonhosted.org/packages/52/98/def78c3a23acee2bcdb2e52005fb2810ed54305602ec1bfcfab2bda6f49f/pillow-11.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9", size = 2254611 }, + { url = "https://files.pythonhosted.org/packages/1c/a3/26e606ff0b2daaf120543e537311fa3ae2eb6bf061490e4fea51771540be/pillow-11.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923", size = 3147642 }, + { url = "https://files.pythonhosted.org/packages/4f/d5/1caabedd8863526a6cfa44ee7a833bd97f945dc1d56824d6d76e11731939/pillow-11.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903", size = 2978999 }, + { url = "https://files.pythonhosted.org/packages/d9/ff/5a45000826a1aa1ac6874b3ec5a856474821a1b59d838c4f6ce2ee518fe9/pillow-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4", size = 4196794 }, + { url = "https://files.pythonhosted.org/packages/9d/21/84c9f287d17180f26263b5f5c8fb201de0f88b1afddf8a2597a5c9fe787f/pillow-11.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f", size = 4300762 }, + { url = "https://files.pythonhosted.org/packages/84/39/63fb87cd07cc541438b448b1fed467c4d687ad18aa786a7f8e67b255d1aa/pillow-11.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9", size = 4210468 }, + { url = "https://files.pythonhosted.org/packages/7f/42/6e0f2c2d5c60f499aa29be14f860dd4539de322cd8fb84ee01553493fb4d/pillow-11.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7", size = 4381824 }, + { url = "https://files.pythonhosted.org/packages/31/69/1ef0fb9d2f8d2d114db982b78ca4eeb9db9a29f7477821e160b8c1253f67/pillow-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6", size = 4296436 }, + { url = "https://files.pythonhosted.org/packages/44/ea/dad2818c675c44f6012289a7c4f46068c548768bc6c7f4e8c4ae5bbbc811/pillow-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc", size = 4429714 }, + { url = "https://files.pythonhosted.org/packages/af/3a/da80224a6eb15bba7a0dcb2346e2b686bb9bf98378c0b4353cd88e62b171/pillow-11.0.0-cp312-cp312-win32.whl", hash = "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6", size = 2249631 }, + { url = "https://files.pythonhosted.org/packages/57/97/73f756c338c1d86bb802ee88c3cab015ad7ce4b838f8a24f16b676b1ac7c/pillow-11.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47", size = 2567533 }, + { url = "https://files.pythonhosted.org/packages/0b/30/2b61876e2722374558b871dfbfcbe4e406626d63f4f6ed92e9c8e24cac37/pillow-11.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25", size = 2254890 }, + { url = "https://files.pythonhosted.org/packages/63/24/e2e15e392d00fcf4215907465d8ec2a2f23bcec1481a8ebe4ae760459995/pillow-11.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699", size = 3147300 }, + { url = "https://files.pythonhosted.org/packages/43/72/92ad4afaa2afc233dc44184adff289c2e77e8cd916b3ddb72ac69495bda3/pillow-11.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38", size = 2978742 }, + { url = "https://files.pythonhosted.org/packages/9e/da/c8d69c5bc85d72a8523fe862f05ababdc52c0a755cfe3d362656bb86552b/pillow-11.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2", size = 4194349 }, + { url = "https://files.pythonhosted.org/packages/cd/e8/686d0caeed6b998351d57796496a70185376ed9c8ec7d99e1d19ad591fc6/pillow-11.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2", size = 4298714 }, + { url = "https://files.pythonhosted.org/packages/ec/da/430015cec620d622f06854be67fd2f6721f52fc17fca8ac34b32e2d60739/pillow-11.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527", size = 4208514 }, + { url = "https://files.pythonhosted.org/packages/44/ae/7e4f6662a9b1cb5f92b9cc9cab8321c381ffbee309210940e57432a4063a/pillow-11.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa", size = 4380055 }, + { url = "https://files.pythonhosted.org/packages/74/d5/1a807779ac8a0eeed57f2b92a3c32ea1b696e6140c15bd42eaf908a261cd/pillow-11.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f", size = 4296751 }, + { url = "https://files.pythonhosted.org/packages/38/8c/5fa3385163ee7080bc13026d59656267daaaaf3c728c233d530e2c2757c8/pillow-11.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb", size = 4430378 }, + { url = "https://files.pythonhosted.org/packages/ca/1d/ad9c14811133977ff87035bf426875b93097fb50af747793f013979facdb/pillow-11.0.0-cp313-cp313-win32.whl", hash = "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798", size = 2249588 }, + { url = "https://files.pythonhosted.org/packages/fb/01/3755ba287dac715e6afdb333cb1f6d69740a7475220b4637b5ce3d78cec2/pillow-11.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de", size = 2567509 }, + { url = "https://files.pythonhosted.org/packages/c0/98/2c7d727079b6be1aba82d195767d35fcc2d32204c7a5820f822df5330152/pillow-11.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84", size = 2254791 }, + { url = "https://files.pythonhosted.org/packages/eb/38/998b04cc6f474e78b563716b20eecf42a2fa16a84589d23c8898e64b0ffd/pillow-11.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b", size = 3150854 }, + { url = "https://files.pythonhosted.org/packages/13/8e/be23a96292113c6cb26b2aa3c8b3681ec62b44ed5c2bd0b258bd59503d3c/pillow-11.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003", size = 2982369 }, + { url = "https://files.pythonhosted.org/packages/97/8a/3db4eaabb7a2ae8203cd3a332a005e4aba00067fc514aaaf3e9721be31f1/pillow-11.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2", size = 4333703 }, + { url = "https://files.pythonhosted.org/packages/28/ac/629ffc84ff67b9228fe87a97272ab125bbd4dc462745f35f192d37b822f1/pillow-11.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a", size = 4412550 }, + { url = "https://files.pythonhosted.org/packages/d6/07/a505921d36bb2df6868806eaf56ef58699c16c388e378b0dcdb6e5b2fb36/pillow-11.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8", size = 4461038 }, + { url = "https://files.pythonhosted.org/packages/d6/b9/fb620dd47fc7cc9678af8f8bd8c772034ca4977237049287e99dda360b66/pillow-11.0.0-cp313-cp313t-win32.whl", hash = "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8", size = 2253197 }, + { url = "https://files.pythonhosted.org/packages/df/86/25dde85c06c89d7fc5db17940f07aae0a56ac69aa9ccb5eb0f09798862a8/pillow-11.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904", size = 2572169 }, + { url = "https://files.pythonhosted.org/packages/51/85/9c33f2517add612e17f3381aee7c4072779130c634921a756c97bc29fb49/pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3", size = 2256828 }, + { url = "https://files.pythonhosted.org/packages/36/57/42a4dd825eab762ba9e690d696d894ba366e06791936056e26e099398cda/pillow-11.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2", size = 3119239 }, + { url = "https://files.pythonhosted.org/packages/98/f7/25f9f9e368226a1d6cf3507081a1a7944eddd3ca7821023377043f5a83c8/pillow-11.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2", size = 2950803 }, + { url = "https://files.pythonhosted.org/packages/59/01/98ead48a6c2e31e6185d4c16c978a67fe3ccb5da5c2ff2ba8475379bb693/pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b", size = 3281098 }, + { url = "https://files.pythonhosted.org/packages/51/c0/570255b2866a0e4d500a14f950803a2ec273bac7badc43320120b9262450/pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2", size = 3323665 }, + { url = "https://files.pythonhosted.org/packages/0e/75/689b4ec0483c42bfc7d1aacd32ade7a226db4f4fac57c6fdcdf90c0731e3/pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830", size = 3310533 }, + { url = "https://files.pythonhosted.org/packages/3d/30/38bd6149cf53da1db4bad304c543ade775d225961c4310f30425995cb9ec/pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734", size = 3414886 }, + { url = "https://files.pythonhosted.org/packages/ec/3d/c32a51d848401bd94cabb8767a39621496491ee7cd5199856b77da9b18ad/pillow-11.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316", size = 2567508 }, +] + +[[package]] +name = "pygments" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, +] + +[[package]] +name = "pyparsing" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/d5/e5aeee5387091148a19e1145f63606619cb5f20b83fccb63efae6474e7b2/pyparsing-3.2.0.tar.gz", hash = "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c", size = 920984 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/ec/2eb3cd785efd67806c46c13a17339708ddc346cbb684eade7a6e6f79536a/pyparsing-3.2.0-py3-none-any.whl", hash = "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84", size = 106921 }, +] + +[[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 = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "requirements-parser" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "types-setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/70/80ed53ebd21853855aad552d4ed6c4934df62cd32fe9a3669fcdef59429c/requirements_parser-0.11.0.tar.gz", hash = "sha256:35f36dc969d14830bf459803da84f314dc3d17c802592e9e970f63d0359e5920", size = 23663 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/33/190393a7d36872e237cbc99e6c44d9a078a1ba7b406462fe6eafd5a28e04/requirements_parser-0.11.0-py3-none-any.whl", hash = "sha256:50379eb50311834386c2568263ae5225d7b9d0867fb55cf4ecc93959de2c2684", size = 14800 }, +] + +[[package]] +name = "rich" +version = "13.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, +] + +[[package]] +name = "ruff" +version = "0.8.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/37/9c02181ef38d55b77d97c68b78e705fd14c0de0e5d085202bb2b52ce5be9/ruff-0.8.4.tar.gz", hash = "sha256:0d5f89f254836799af1615798caa5f80b7f935d7a670fad66c5007928e57ace8", size = 3402103 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/67/f480bf2f2723b2e49af38ed2be75ccdb2798fca7d56279b585c8f553aaab/ruff-0.8.4-py3-none-linux_armv6l.whl", hash = "sha256:58072f0c06080276804c6a4e21a9045a706584a958e644353603d36ca1eb8a60", size = 10546415 }, + { url = "https://files.pythonhosted.org/packages/eb/7a/5aba20312c73f1ce61814e520d1920edf68ca3b9c507bd84d8546a8ecaa8/ruff-0.8.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ffb60904651c00a1e0b8df594591770018a0f04587f7deeb3838344fe3adabac", size = 10346113 }, + { url = "https://files.pythonhosted.org/packages/76/f4/c41de22b3728486f0aa95383a44c42657b2db4062f3234ca36fc8cf52d8b/ruff-0.8.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ddf5d654ac0d44389f6bf05cee4caeefc3132a64b58ea46738111d687352296", size = 9943564 }, + { url = "https://files.pythonhosted.org/packages/0e/f0/afa0d2191af495ac82d4cbbfd7a94e3df6f62a04ca412033e073b871fc6d/ruff-0.8.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e248b1f0fa2749edd3350a2a342b67b43a2627434c059a063418e3d375cfe643", size = 10805522 }, + { url = "https://files.pythonhosted.org/packages/12/57/5d1e9a0fd0c228e663894e8e3a8e7063e5ee90f8e8e60cf2085f362bfa1a/ruff-0.8.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf197b98ed86e417412ee3b6c893f44c8864f816451441483253d5ff22c0e81e", size = 10306763 }, + { url = "https://files.pythonhosted.org/packages/04/df/f069fdb02e408be8aac6853583572a2873f87f866fe8515de65873caf6b8/ruff-0.8.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c41319b85faa3aadd4d30cb1cffdd9ac6b89704ff79f7664b853785b48eccdf3", size = 11359574 }, + { url = "https://files.pythonhosted.org/packages/d3/04/37c27494cd02e4a8315680debfc6dfabcb97e597c07cce0044db1f9dfbe2/ruff-0.8.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9f8402b7c4f96463f135e936d9ab77b65711fcd5d72e5d67597b543bbb43cf3f", size = 12094851 }, + { url = "https://files.pythonhosted.org/packages/81/b1/c5d7fb68506cab9832d208d03ea4668da9a9887a4a392f4f328b1bf734ad/ruff-0.8.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4e56b3baa9c23d324ead112a4fdf20db9a3f8f29eeabff1355114dd96014604", size = 11655539 }, + { url = "https://files.pythonhosted.org/packages/ef/38/8f8f2c8898dc8a7a49bc340cf6f00226917f0f5cb489e37075bcb2ce3671/ruff-0.8.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:736272574e97157f7edbbb43b1d046125fce9e7d8d583d5d65d0c9bf2c15addf", size = 12912805 }, + { url = "https://files.pythonhosted.org/packages/06/dd/fa6660c279f4eb320788876d0cff4ea18d9af7d9ed7216d7bd66877468d0/ruff-0.8.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fe710ab6061592521f902fca7ebcb9fabd27bc7c57c764298b1c1f15fff720", size = 11205976 }, + { url = "https://files.pythonhosted.org/packages/a8/d7/de94cc89833b5de455750686c17c9e10f4e1ab7ccdc5521b8fe911d1477e/ruff-0.8.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:13e9ec6d6b55f6da412d59953d65d66e760d583dd3c1c72bf1f26435b5bfdbae", size = 10792039 }, + { url = "https://files.pythonhosted.org/packages/6d/15/3e4906559248bdbb74854af684314608297a05b996062c9d72e0ef7c7097/ruff-0.8.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:97d9aefef725348ad77d6db98b726cfdb075a40b936c7984088804dfd38268a7", size = 10400088 }, + { url = "https://files.pythonhosted.org/packages/a2/21/9ed4c0e8133cb4a87a18d470f534ad1a8a66d7bec493bcb8bda2d1a5d5be/ruff-0.8.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ab78e33325a6f5374e04c2ab924a3367d69a0da36f8c9cb6b894a62017506111", size = 10900814 }, + { url = "https://files.pythonhosted.org/packages/0d/5d/122a65a18955bd9da2616b69bc839351f8baf23b2805b543aa2f0aed72b5/ruff-0.8.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8ef06f66f4a05c3ddbc9121a8b0cecccd92c5bf3dd43b5472ffe40b8ca10f0f8", size = 11268828 }, + { url = "https://files.pythonhosted.org/packages/43/a9/1676ee9106995381e3d34bccac5bb28df70194167337ed4854c20f27c7ba/ruff-0.8.4-py3-none-win32.whl", hash = "sha256:552fb6d861320958ca5e15f28b20a3d071aa83b93caee33a87b471f99a6c0835", size = 8805621 }, + { url = "https://files.pythonhosted.org/packages/10/98/ed6b56a30ee76771c193ff7ceeaf1d2acc98d33a1a27b8479cbdb5c17a23/ruff-0.8.4-py3-none-win_amd64.whl", hash = "sha256:f21a1143776f8656d7f364bd264a9d60f01b7f52243fbe90e7670c0dfe0cf65d", size = 9660086 }, + { url = "https://files.pythonhosted.org/packages/13/9f/026e18ca7d7766783d779dae5e9c656746c6ede36ef73c6d934aaf4a6dec/ruff-0.8.4-py3-none-win_arm64.whl", hash = "sha256:9183dd615d8df50defa8b1d9a074053891ba39025cf5ae88e8bcb52edcc4bf08", size = 9074500 }, +] + +[[package]] +name = "setuptools" +version = "75.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/54/292f26c208734e9a7f067aea4a7e282c080750c4546559b58e2e45413ca0/setuptools-75.6.0.tar.gz", hash = "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6", size = 1337429 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/21/47d163f615df1d30c094f6c8bbb353619274edccf0327b185cc2493c2c33/setuptools-75.6.0-py3-none-any.whl", hash = "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d", size = 1224032 }, +] + +[[package]] +name = "shepherd" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "graphviz" }, + { name = "jinja2" }, + { name = "loguru" }, + { name = "matplotlib" }, + { name = "numpy" }, + { name = "pyyaml" }, +] + +[package.dev-dependencies] +dev = [ + { name = "deptry" }, + { name = "rich" }, + { name = "ruff" }, + { name = "setuptools" }, +] + +[package.metadata] +requires-dist = [ + { name = "graphviz", specifier = ">=0.20.3" }, + { name = "jinja2", specifier = ">=3.1.4" }, + { name = "loguru", specifier = ">=0.7.3" }, + { name = "matplotlib", specifier = ">=3.9.4" }, + { name = "numpy", specifier = ">=2.0.2" }, + { name = "pyyaml", specifier = ">=6.0.2" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "deptry", specifier = ">=0.21.2" }, + { name = "rich", specifier = ">=13.9.4" }, + { name = "ruff", specifier = ">=0.8.4" }, + { name = "setuptools", specifier = ">=75.6.0" }, +] + +[[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 = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + +[[package]] +name = "types-setuptools" +version = "75.6.0.20241126" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/d2/15ede73bc3faf647af2c7bfefa90dde563a4b6bb580b1199f6255463c272/types_setuptools-75.6.0.20241126.tar.gz", hash = "sha256:7bf25ad4be39740e469f9268b6beddda6e088891fa5a27e985c6ce68bf62ace0", size = 48569 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/a0/898a1363592d372d4103b76b7c723d84fcbde5fa4ed0c3a29102805ed7db/types_setuptools-75.6.0.20241126-py3-none-any.whl", hash = "sha256:aaae310a0e27033c1da8457d4d26ac673b0c8a0de7272d6d4708e263f2ea3b9b", size = 72732 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083 }, +]