diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml
new file mode 100644
index 0000000..72c7f7e
--- /dev/null
+++ b/.github/workflows/build-docs.yml
@@ -0,0 +1,56 @@
+name: Build Documentation
+
+on:
+ workflow_dispatch:
+
+jobs:
+ build-docs:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout pyharp repository
+ uses: actions/checkout@v4
+
+ - name: Clone harp.devices repository
+ uses: actions/checkout@v4
+ with:
+ repository: fchampalimaud/harp.devices
+ token: ${{ secrets.HARP_DEVICES_TOKEN }}
+ path: harp.devices
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v5
+
+ - name: Install project dependencies
+ run: |
+ uv sync
+
+ - name: Build documentation
+ run: |
+ chmod +x ./run_docs.sh
+ ./run_docs.sh build
+
+ - name: Upload Build Artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: site
+
+
+ deploy:
+ name: Deploy to Github pages
+ needs: build-docs
+
+ # Grant GITHUB_TOKEN the permissions required to make a Pages deployment
+ permissions:
+ pages: write # to deploy to Pages
+ id-token: write # to verify the deployment originates from an appropriate source
+
+ # Deploy to the github-pages environment
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+
+ runs-on: ubuntu-latest
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.gitignore b/.gitignore
index f60ee92..0a19790 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,174 @@
-.idea/
-pyharp.egg-info/
-.python-version
-__pycache__
-tests/.pytest_cache
-**/*.bin
\ No newline at end of file
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# UV
+# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+#uv.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
+.pdm.toml
+.pdm-python
+.pdm-build/
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+# Ruff stuff:
+.ruff_cache/
+
+# PyPI configuration file
+.pypirc
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..23d8811
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,22 @@
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: "v5.0.0"
+ hooks:
+ - id: check-case-conflict
+ - id: check-merge-conflict
+ - id: check-toml
+ - id: check-yaml
+ - id: check-json
+ exclude: ^.devcontainer/devcontainer.json
+ - id: pretty-format-json
+ exclude: ^.devcontainer/devcontainer.json
+ args: [--autofix, --no-sort-keys]
+ - id: end-of-file-fixer
+ - id: trailing-whitespace
+
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: "v0.11.5"
+ hooks:
+ - id: ruff
+ args: [--exit-non-zero-on-fix]
+ - id: ruff-format
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..dd4b3f2
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,7 @@
+{
+ "recommendations": [
+ "ms-python.python",
+ "charliermarsh.ruff",
+ "meta.pyrefly"
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..3ad43fe
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,15 @@
+{
+ "editor.defaultFormatter": "charliermarsh.ruff",
+ "editor.formatOnSave": true,
+ "editor.formatOnPaste": true,
+ "ruff.organizeImports": true,
+ "editor.codeActionsOnSave": {
+ "source.organizeImports": "explicit"
+ },
+ "python.testing.pytestArgs": [
+ "tests"
+ ],
+ "python.testing.unittestEnabled": false,
+ "python.testing.pytestEnabled": true,
+ "python.pyrefly.displayTypeErrors": "force-on",
+}
\ No newline at end of file
diff --git a/10-harp.rules b/10-harp.rules
new file mode 100644
index 0000000..c5114ef
--- /dev/null
+++ b/10-harp.rules
@@ -0,0 +1,3 @@
+# UDEV rules for a Harp Device (actually an ftdi RS232 Serial [Uart] IC)
+SUBSYSTEMS=="usb", ENV{.LOCAL_ifNum}="$attr{bInterfaceNumber}"
+SUBSYSTEMS=="usb", KERNEL=="ttyUSB*", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", MODE="0666", SYMLINK+="harp_device_%E{.LOCAL_ifNum}"
diff --git a/LICENSE b/LICENSE
index 86b9923..b7afd7f 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,7 @@
MIT License
Copyright (c) 2020 OEPS & Filipe Carvalho
+Copyright (c) 2025 Hardware and Software Platform, Champalimaud Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 4b18b2d..811f3d9 100644
--- a/README.md
+++ b/README.md
@@ -1,71 +1,12 @@
-
-
# pyharp
-Harp implementation of the Harp protocol.
-
-## Edit the code
-
-Each Python user has is own very dear IDE for editing. Here, we are leaving instructions on how to edit this code using pyCharm, Anaconda and Poetry.
-
-The instructions are for beginner. Most of the users can just skip them.
-
-This was tested on a Windows machine, but should be similar to other systems.
-
-
-### 1. Install PyCHarm
-**PyCharm** can be download from [here](https://www.jetbrains.com/pycharm/download/). The Community version is enough.
-Download and install it.
-
-### 2. Install Anaconda
-
-**Anaconda** can be found [here](https://www.anaconda.com/products/individual).
-Download the version according to your computer and install it.
-- Unselect **Add Anaconda to the system PATH environment variable**
-- Select ** Register Anaconda as the system Pyhton**
-
-It's suggested to reboot your computer at this point
-
-### 3. Install Poetry
-
-Open the **Command Prompt** and execute the next command:
-```
-curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python
-```
-
-### 4. Install pyharp
-
-Open **Anaconda**, navigate to the repository folder and execute the next commands:
-```
-poetry install
-poetry env info
-```
-
-The second comand will reply with a **Path:**.
-Select and copy this path.
-
-### 5. Using PyCharm to edit the code
+This project includes two main packages:
-1. Open **PyCharm** :)
-2. Go to File -> Open, select the repository folder, and click **OK**
-3. Go to File -> Settings -> Project:pyharp -> Project Interpreter
-3.1 Click in the gear in front of the Project Interpreter: and select **Add...**
-3.2 On Virtualenv Environment, chose Existing environment
-3.3 Select **python.exe** on the folder Scripts under the path copied from the _poetry env info_ command
-3.4 Click **OK** and **OK**
+ - **harp-protocol**: Provides the core protocol definitions and utilities for the Harp protocol.
+ See [Protocol API Documentation](https://fchampalimaud.github.io/pyharp/api/protocol) for details.
-You are ready to go!
+ - **harp-serial**: Implements serial communication functionalities for generic Harp devices.
+ See [Serial API Documentation](https://fchampalimaud.github.io/pyharp/api/serial) for more information.
-### 6. Test the code
-Under **PyCharm**, Open one of the examples from the folder _examples_ (the _get_info.py_ is generic, so it's a good option) and update the COMx to your COM number.
-Right-click on top of the file and chose option _Run 'get_info.py_. You should read something like this in the console:
-```
-Device info:
-* Who am I: (2080) IblBehavior
-* HW version: 1.0
-* Assembly version: 0
-* HARP version: 1.6
-* Firmware version: 1.0
-* Device user name: IBL_rig_0
-```
+For specific Harp devices' packages please select the corresponding Harp device under the Devices section on the menu.
\ No newline at end of file
diff --git a/docs/api/protocol.md b/docs/api/protocol.md
new file mode 100644
index 0000000..f2e55ee
--- /dev/null
+++ b/docs/api/protocol.md
@@ -0,0 +1,15 @@
+{% include-markdown "../../src/harp-protocol/README.md" %}
+
+---
+
+::: harp.protocol.MessageType
+::: harp.protocol.PayloadType
+::: harp.protocol.CommonRegisters
+::: harp.protocol.OperationMode
+::: harp.protocol.OperationCtrl
+::: harp.protocol.ResetMode
+::: harp.protocol.ClockConfig
+::: harp.protocol.messages.HarpMessage
+::: harp.protocol.messages.ReplyHarpMessage
+::: harp.protocol.messages.ReadHarpMessage
+::: harp.protocol.messages.WriteHarpMessage
diff --git a/docs/api/serial.md b/docs/api/serial.md
new file mode 100644
index 0000000..c598ff0
--- /dev/null
+++ b/docs/api/serial.md
@@ -0,0 +1,6 @@
+{% include-markdown "../../src/harp-serial/README.md" %}
+
+---
+
+::: harp.serial.Device
+::: harp.serial.TimeoutStrategy
diff --git a/docs/assets/favicon.png b/docs/assets/favicon.png
new file mode 100644
index 0000000..49c6fae
Binary files /dev/null and b/docs/assets/favicon.png differ
diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg
new file mode 100644
index 0000000..96224d9
--- /dev/null
+++ b/docs/assets/logo.svg
@@ -0,0 +1,71 @@
+
+
+
+
diff --git a/docs/examples/get_info/get_info.md b/docs/examples/get_info/get_info.md
new file mode 100644
index 0000000..0ebfe38
--- /dev/null
+++ b/docs/examples/get_info/get_info.md
@@ -0,0 +1,12 @@
+# Getting Device Info
+
+This example demonstrates how to connect to a Harp device, read its info and dump the device's registers.
+
+!!! warning
+ Don't forget to change the `SERIAL_PORT` to the one that corresponds to your device! The `SERIAL_PORT` must be denoted as `/dev/ttyUSBx` in Linux and `COMx` in Windows, where `x` is the number of the serial port.
+
+
+```python
+[](./get_info.py)
+```
+
diff --git a/docs/examples/get_info/get_info.py b/docs/examples/get_info/get_info.py
new file mode 100755
index 0000000..ea88143
--- /dev/null
+++ b/docs/examples/get_info/get_info.py
@@ -0,0 +1,20 @@
+from pyharp.protocol.device import Device
+
+SERIAL_PORT = (
+ "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port)
+)
+
+# Open serial connection and save communication to a file
+device = Device(SERIAL_PORT, "dump.bin")
+
+# Display device's info on screen
+device.info()
+
+# Dump device's registers
+reg_dump = device.dump_registers()
+for reg_reply in reg_dump:
+ print(reg_reply)
+ print()
+
+# Close connection
+device.disconnect()
diff --git a/docs/examples/index.md b/docs/examples/index.md
new file mode 100644
index 0000000..d045e5d
--- /dev/null
+++ b/docs/examples/index.md
@@ -0,0 +1,10 @@
+# Examples
+
+This section contains some examples to help you get started with `harp`.
+
+Here's the complete list of available examples:
+
+- [Getting Device Info](./get_info/get_info.md) - connect to a Harp device and read its information.
+- [Wait for Events](./wait_for_events/wait_for_events.md) - connect to a Harp device and wait for events.
+- [Write and Read from Registers](./read_and_write_from_registers/read_and_write_from_registers.md) - connect to the Harp Behavior, read from a digital input and write to a digital output.
+- [Olfactometer Example](./olfactometer_example/olfactometer_example.md) - connect to the Harp Olfactometer, enable flow, open and close the odor valves and monitor the measured flow values.
diff --git a/docs/examples/olfactometer_example/olfactometer_example.md b/docs/examples/olfactometer_example/olfactometer_example.md
new file mode 100644
index 0000000..91b1024
--- /dev/null
+++ b/docs/examples/olfactometer_example/olfactometer_example.md
@@ -0,0 +1,14 @@
+# Olfactometer Example
+
+This example shows how to interface with the [Harp Olfactometer](https://github.com/harp-tech/device.olfactometer).
+
+In this example, the flows for the different channels are enabled to random flow values, then every odor valve is opened, one at a time every 5 seconds, and finally the flow is disabled before closing the connection with the device. During this time, the actual flows in every channel are being printed out in the terminal.
+
+!!! warning
+ Don't forget to change the `SERIAL_PORT` to the one that corresponds to your device! The `SERIAL_PORT` must be denoted as `/dev/ttyUSBx` in Linux and `COMx` in Windows, where `x` is the number of the serial port.
+
+
+```python
+[](./olfactometer_example.py)
+```
+
diff --git a/docs/examples/olfactometer_example/olfactometer_example.py b/docs/examples/olfactometer_example/olfactometer_example.py
new file mode 100644
index 0000000..b4ac526
--- /dev/null
+++ b/docs/examples/olfactometer_example/olfactometer_example.py
@@ -0,0 +1,115 @@
+import random
+import time
+from threading import Event, Thread
+
+from harp.protocol import MessageType, PayloadType
+from harp.protocol.messages import HarpMessage
+from harp.serial.device import Device, OperationMode
+from serial import SerialException
+
+SERIAL_PORT = (
+ "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port)
+)
+
+
+def print_events(device, stop_flag):
+ while not stop_flag.is_set():
+ for msg in device.get_events():
+ if (
+ msg.address == 48
+ or msg.address == 49
+ or msg.address == 50
+ or msg.address == 51
+ or msg.address == 52
+ ):
+ print(msg.address - 48)
+ print(msg.payload[0])
+ print()
+
+
+def main():
+ # Open connection
+ device = Device(SERIAL_PORT)
+ time.sleep(1)
+
+ stop_flag = Event()
+
+ # Check if the device is a Harp Olfactometer
+ if not device.WHO_AM_I == 1140:
+ raise SerialException("This is not a Harp Olfactometer.")
+
+ device.set_mode(OperationMode.ACTIVE)
+
+ # Enable flow
+ device.send(HarpMessage.create(MessageType.WRITE, 32, PayloadType.U8, 0x01))
+
+ # Initialize thread for events
+ events_thread = Thread(
+ target=print_events,
+ args=(
+ device,
+ stop_flag,
+ ),
+ )
+ events_thread.start()
+
+ # Set the valves to a random flow
+ device.send(
+ HarpMessage.create(
+ MessageType.WRITE, 42, PayloadType.Float, int(random.random() * 100)
+ )
+ )
+ device.send(
+ HarpMessage.create(
+ MessageType.WRITE, 43, PayloadType.Float, int(random.random() * 100)
+ )
+ )
+ device.send(
+ HarpMessage.create(
+ MessageType.WRITE, 44, PayloadType.Float, int(random.random() * 100)
+ )
+ )
+ device.send(
+ HarpMessage.create(
+ MessageType.WRITE, 45, PayloadType.Float, int(random.random() * 100)
+ )
+ )
+
+ # Open every odor valve, one at a time every 5 seconds
+ device.send(HarpMessage.create(MessageType.WRITE, 68, PayloadType.Float, 0x01))
+
+ time.sleep(5)
+
+ device.send(HarpMessage.create(MessageType.WRITE, 69, PayloadType.Float, 0x01))
+ device.send(HarpMessage.create(MessageType.WRITE, 68, PayloadType.Float, 0x02))
+
+ time.sleep(5)
+
+ device.send(HarpMessage.create(MessageType.WRITE, 69, PayloadType.Float, 0x02))
+ device.send(HarpMessage.create(MessageType.WRITE, 68, PayloadType.Float, 0x04))
+
+ time.sleep(5)
+
+ device.send(HarpMessage.create(MessageType.WRITE, 69, PayloadType.Float, 0x04))
+ device.send(HarpMessage.create(MessageType.WRITE, 68, PayloadType.Float, 0x08))
+
+ time.sleep(5)
+
+ device.send(HarpMessage.create(MessageType.WRITE, 69, PayloadType.Float, 0x08))
+
+ time.sleep(5)
+
+ # Disable flow
+ device.send(HarpMessage.create(MessageType.WRITE, 32, PayloadType.Float, 0x00))
+
+ time.sleep(1)
+
+ stop_flag.set()
+ events_thread.join()
+
+ # Close connection
+ device.disconnect()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/docs/examples/read_and_write_from_registers/read_and_write_from_registers.md b/docs/examples/read_and_write_from_registers/read_and_write_from_registers.md
new file mode 100644
index 0000000..040f6e4
--- /dev/null
+++ b/docs/examples/read_and_write_from_registers/read_and_write_from_registers.md
@@ -0,0 +1,17 @@
+# Read and Write from Registers
+
+This example demonstrates how to read and write from registers. In this particular example, the [Harp Behavior](https://harp-tech.org/api/Harp.Behavior.html) is used to read from the DI3 pin and to turn on and off the DO0 pin, according to the schematics shown [below](#schematics).
+
+!!! warning
+ Don't forget to change the `SERIAL_PORT` to the one that corresponds to your device! The `SERIAL_PORT` must be denoted as `/dev/ttyUSBx` in Linux and `COMx` in Windows, where `x` is the number of the serial port.
+
+
+```python
+[](./read_and_write_from_registers.py)
+```
+
+
+## Schematics
+
+!!! warning
+ _TODO_
diff --git a/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py b/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py
new file mode 100755
index 0000000..7533fe1
--- /dev/null
+++ b/docs/examples/read_and_write_from_registers/read_and_write_from_registers.py
@@ -0,0 +1,32 @@
+from harp.protocol import MessageType, PayloadType
+from harp.protocol.messages import HarpMessage
+from harp.serial.device import Device
+from serial import SerialException
+
+SERIAL_PORT = (
+ "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port)
+)
+
+# Open serial connection and save communication to a file
+device = Device(SERIAL_PORT, "dump.bin")
+
+# Check if the device is a Harp Behavior
+if not device.WHO_AM_I == 1216:
+ raise SerialException("This is not a Harp Behavior.")
+
+# Read initial DI3 state
+reply = device.send(HarpMessage.create(MessageType.READ, 32, PayloadType.U8))
+print(reply.payload & 0x08)
+
+# Turn DO0 on and read DI3 state after it
+reply = device.send(HarpMessage.create(MessageType.READ, 34, PayloadType.U8, 0x400))
+reply = device.send(HarpMessage.create(MessageType.READ, 32, PayloadType.U8))
+print(reply.payload & 0x08)
+
+# Turn DO0 off and read DI3 state again
+reply = device.send(HarpMessage.create(MessageType.READ, 35, PayloadType.U8, 0x400))
+reply = device.send(HarpMessage.create(MessageType.READ, 32, PayloadType.U8))
+print(reply.payload & 0x08)
+
+# Close connection
+device.disconnect()
diff --git a/docs/examples/wait_for_events/wait_for_events.md b/docs/examples/wait_for_events/wait_for_events.md
new file mode 100644
index 0000000..8db54fa
--- /dev/null
+++ b/docs/examples/wait_for_events/wait_for_events.md
@@ -0,0 +1,12 @@
+# Wait for Events
+
+This example demonstrates how to read the events sent by the Harp device.
+
+!!! warning
+ Don't forget to change the `SERIAL_PORT` to the one that corresponds to your device! The `SERIAL_PORT` must be denoted as `/dev/ttyUSBx` in Linux and `COMx` in Windows, where `x` is the number of the serial port.
+
+
+```python
+[](./wait_for_events.py)
+```
+
diff --git a/docs/examples/wait_for_events/wait_for_events.py b/docs/examples/wait_for_events/wait_for_events.py
new file mode 100755
index 0000000..e830e3d
--- /dev/null
+++ b/docs/examples/wait_for_events/wait_for_events.py
@@ -0,0 +1,19 @@
+from harp.protocol import OperationMode
+from harp.serial.device import Device
+
+SERIAL_PORT = (
+ "/dev/ttyUSB0" # or "COMx" in Windows ("x" is the number of the serial port)
+)
+
+# Open serial connection and save communication to a file
+device = Device(SERIAL_PORT, "dump.bin")
+
+# Set device to Active Mode
+device.set_mode(OperationMode.ACTIVE)
+print("Setting mode to active.")
+
+# Read device's events
+while True:
+ for msg in device.get_events():
+ print(msg)
+ print()
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..96d83c6
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1 @@
+{% include-markdown "../README.md" %}
\ No newline at end of file
diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css
new file mode 100644
index 0000000..b417550
--- /dev/null
+++ b/docs/stylesheets/extra.css
@@ -0,0 +1,9 @@
+:root > * {
+ --md-primary-fg-color: #009DE1;
+ --md-primary-fg-color--light: #009DE1;
+ --md-primary-fg-color--dark: #009DE1;
+}
+
+.md-nav__title {
+ display: none;
+}
diff --git a/examples/check_device_id.py b/examples/check_device_id.py
deleted file mode 100644
index b118b0e..0000000
--- a/examples/check_device_id.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from pyharp.device import Device
-from pyharp.messages import HarpMessage
-from pyharp.messages import MessageType
-from struct import *
-
-
-# ON THIS EXAMPLE
-#
-# This code check if the device at COMx is the expected device.
-# The device ID used is the 2080, the IblBehavior
-
-
-# Open the device
-device = Device("COM95") # Open serial connection
-
-# Get some of the device's parameters
-device_id = device.WHO_AM_I # Get device's ID
-device_id_description = device.WHO_AM_I_DEVICE # Get device's user name
-device_user_name = device.DEVICE_NAME # Get device's user name
-
-# Check if we are dealing with the correct device
-if device_id == 2080:
- print("Correct device was found!")
- print(f"Device's ID: {device_id}")
- print(f"Device's name: {device_id_description}")
- print(f"Device's user name: {device_user_name}")
-else:
- print("Device not correct or is not a Harp device!")
-
-# Close connection
-device.disconnect()
diff --git a/examples/get_info.py b/examples/get_info.py
deleted file mode 100644
index fca840c..0000000
--- a/examples/get_info.py
+++ /dev/null
@@ -1,32 +0,0 @@
-from pyharp.device import Device
-from pyharp.messages import HarpMessage
-from pyharp.messages import MessageType
-from struct import *
-
-
-# ON THIS EXAMPLE
-#
-# This code opens the connection with the device and displays the information
-# Also saves device's information into variables
-
-
-# Open the device and print the info on screen
-device = Device("COM95", "ibl.bin") # Open serial connection and save communication to a file
-device.info() # Display device's info on screen
-
-# Get some of the device's parameters
-device_id = device.WHO_AM_I # Get device's ID
-device_id_description = device.WHO_AM_I_DEVICE # Get device's user name
-device_user_name = device.DEVICE_NAME # Get device's user name
-
-# Get versions
-device_fw_h = device.FIRMWARE_VERSION_H # Get device's firmware version
-device_fw_l = device.FIRMWARE_VERSION_L # Get device's firmware version
-device_hw_h = device.HW_VERSION_H # Get device's hardware version
-device_hw_l = device.HW_VERSION_L # Get device's hardware version
-device_harp_h = device.HARP_VERSION_H # Get device's harp core version
-device_harp_l = device.HARP_VERSION_L # Get device's harp core version
-device_assembly = device.ASSEMBLY_VERSION # Get device's assembly version
-
-# Close connection
-device.disconnect()
\ No newline at end of file
diff --git a/examples/write_and_read_from_registers.py b/examples/write_and_read_from_registers.py
deleted file mode 100644
index ccde127..0000000
--- a/examples/write_and_read_from_registers.py
+++ /dev/null
@@ -1,37 +0,0 @@
-from pyharp.device import Device
-from pyharp.messages import HarpMessage
-from pyharp.messages import MessageType
-from struct import *
-
-
-# ON THIS EXAMPLE
-#
-# This code opens the connection with the device and update content on a register
-# It uses register address 42, which stores the analog sensor's higher threshold in the IBLBehavior device
-# This register is unsigned with 16 bits (U16)
-
-
-# Open the device and print the info on screen
-device = Device("COM95", "ibl.bin") # Open serial connection and save communication to a file
-
-# Read current analog sensor's higher threshold (ANA_SENSOR_TH0_HIGH) at address 42
-analog_threshold_h = device.send(HarpMessage.ReadU16(42).frame).payload_as_int()
-print(f"Analog sensor's higher threshold: {analog_threshold_h}")
-
-# Increase current analog sensor's higher threshold by one unit
-device.send(HarpMessage.WriteU16(42, analog_threshold_h+1).frame)
-
-# Check if the register was well written
-analog_threshold_h = device.send(HarpMessage.ReadU16(42).frame).payload_as_int()
-print(f"Analog sensor's higher threshold: {analog_threshold_h}")
-
-# Read 10 samples of the analog sensor and display the values
-# The value is at register STREAM[0], address 33
-analog_sensor = []
-for x in range(10):
- value = device.send(HarpMessage.ReadS16(33).frame).payload_as_int()
- analog_sensor.append(value & 0xffff)
-print(f"Analog sensor's values: {analog_sensor}")
-
-# Close connection
-device.disconnect()
\ No newline at end of file
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 0000000..32e9df3
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,96 @@
+site_name: pyharp
+repo_url: "https://github.com/fchampalimaud/pyharp"
+# repo_name: "pyharp"
+copyright: Copyright © 2025, Hardware and Software Platform, Champalimaud Foundation
+
+plugins:
+ - search
+ - autorefs
+ - codeinclude
+ - monorepo
+ - include-markdown
+ - mkdocstrings:
+ handlers:
+ python:
+ options:
+ docstring_style: numpy
+ show_root_heading: true
+ show_submodules: true
+ show_source: false
+ extensions:
+ - griffe_fieldz
+ - git-committers:
+ repository: fchampalimaud/pyharp
+ branch: main
+ - git-authors
+
+
+markdown_extensions:
+ - abbr
+ - attr_list
+ - admonition
+ - pymdownx.details
+ - pymdownx.highlight:
+ anchor_linenums: true
+ line_spans: __span
+ pygments_lang_class: true
+ - pymdownx.inlinehilite
+ - pymdownx.snippets
+ - pymdownx.superfences
+ - toc:
+ permalink: "#"
+
+theme:
+ name: material
+ icon:
+ repo: fontawesome/brands/github
+ logo: assets/logo.svg
+ favicon: assets/favicon.png
+ features:
+ - content.tooltips
+ - toc.follow
+ - content.code.copy
+ # - navigation.sections
+ - navigation.indexes
+ - navigation.expand
+ - navigation.footer
+ palette:
+ - media: "(prefers-color-scheme)"
+ toggle:
+ icon: material/brightness-auto
+ name: Switch to light mode
+ - media: "(prefers-color-scheme: light)"
+ scheme: default
+ primary: custom
+ accent: light-blue
+ toggle:
+ icon: material/weather-sunny
+ name: Switch to dark mode
+ - media: "(prefers-color-scheme: dark)"
+ scheme: slate
+ primary: custom
+ accent: light-blue
+ toggle:
+ icon: material/weather-night
+ name: Switch to system preference
+
+nav:
+ - Home: index.md
+ - API:
+ - Protocol: api/protocol.md
+ - Serial: api/serial.md
+ - Examples:
+ - examples/index.md
+ - Getting Device Info: examples/get_info/get_info.md
+ - Wait for Events: examples/wait_for_events/wait_for_events.md
+ - Read and Write from Registers: examples/read_and_write_from_registers/read_and_write_from_registers.md
+ - Olfactometer Example: examples/olfactometer_example/olfactometer_example.md
+ - Devices: '*include ./harp.devices/*/mkdocs.yml'
+
+extra:
+ social:
+ - icon: fontawesome/brands/github
+ link: https://github.com/fchampalimaud/pyharp
+
+extra_css:
+- stylesheets/extra.css
diff --git a/poetry.lock b/poetry.lock
deleted file mode 100644
index 6e3d0a4..0000000
--- a/poetry.lock
+++ /dev/null
@@ -1,415 +0,0 @@
-[[package]]
-category = "main"
-description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
-name = "appdirs"
-optional = false
-python-versions = "*"
-version = "1.4.4"
-
-[[package]]
-category = "dev"
-description = "Atomic file writes."
-marker = "sys_platform == \"win32\""
-name = "atomicwrites"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "1.4.0"
-
-[[package]]
-category = "main"
-description = "Classes Without Boilerplate"
-name = "attrs"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "19.3.0"
-
-[package.extras]
-azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"]
-dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"]
-docs = ["sphinx", "zope.interface"]
-tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
-
-[[package]]
-category = "main"
-description = "The uncompromising code formatter."
-name = "black"
-optional = false
-python-versions = ">=3.6"
-version = "19.10b0"
-
-[package.dependencies]
-appdirs = "*"
-attrs = ">=18.1.0"
-click = ">=6.5"
-pathspec = ">=0.6,<1"
-regex = "*"
-toml = ">=0.9.4"
-typed-ast = ">=1.4.0"
-
-[package.extras]
-d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
-
-[[package]]
-category = "main"
-description = "Composable command line interface toolkit"
-name = "click"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-version = "7.1.2"
-
-[[package]]
-category = "dev"
-description = "Cross-platform colored terminal text."
-marker = "sys_platform == \"win32\""
-name = "colorama"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-version = "0.4.3"
-
-[[package]]
-category = "dev"
-description = "Read metadata from Python packages"
-marker = "python_version < \"3.8\""
-name = "importlib-metadata"
-optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
-version = "1.7.0"
-
-[package.dependencies]
-zipp = ">=0.5"
-
-[package.extras]
-docs = ["sphinx", "rst.linker"]
-testing = ["packaging", "pep517", "importlib-resources (>=1.3)"]
-
-[[package]]
-category = "dev"
-description = "More routines for operating on iterables, beyond itertools"
-name = "more-itertools"
-optional = false
-python-versions = ">=3.5"
-version = "8.4.0"
-
-[[package]]
-category = "dev"
-description = "Optional static typing for Python"
-name = "mypy"
-optional = false
-python-versions = ">=3.5"
-version = "0.782"
-
-[package.dependencies]
-mypy-extensions = ">=0.4.3,<0.5.0"
-typed-ast = ">=1.4.0,<1.5.0"
-typing-extensions = ">=3.7.4"
-
-[package.extras]
-dmypy = ["psutil (>=4.0)"]
-
-[[package]]
-category = "dev"
-description = "Experimental type system extensions for programs checked with the mypy typechecker."
-name = "mypy-extensions"
-optional = false
-python-versions = "*"
-version = "0.4.3"
-
-[[package]]
-category = "dev"
-description = "Core utilities for Python packages"
-name = "packaging"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "20.4"
-
-[package.dependencies]
-pyparsing = ">=2.0.2"
-six = "*"
-
-[[package]]
-category = "main"
-description = "Utility library for gitignore style pattern matching of file paths."
-name = "pathspec"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-version = "0.8.0"
-
-[[package]]
-category = "dev"
-description = "plugin and hook calling mechanisms for python"
-name = "pluggy"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "0.13.1"
-
-[package.dependencies]
-[package.dependencies.importlib-metadata]
-python = "<3.8"
-version = ">=0.12"
-
-[package.extras]
-dev = ["pre-commit", "tox"]
-
-[[package]]
-category = "dev"
-description = "library with cross-python path, ini-parsing, io, code, log facilities"
-name = "py"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-version = "1.9.0"
-
-[[package]]
-category = "dev"
-description = "Python parsing module"
-name = "pyparsing"
-optional = false
-python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
-version = "2.4.7"
-
-[[package]]
-category = "main"
-description = "Python Serial Port Extension"
-name = "pyserial"
-optional = false
-python-versions = "*"
-version = "3.4"
-
-[[package]]
-category = "dev"
-description = "pytest: simple powerful testing with Python"
-name = "pytest"
-optional = false
-python-versions = ">=3.5"
-version = "5.4.3"
-
-[package.dependencies]
-atomicwrites = ">=1.0"
-attrs = ">=17.4.0"
-colorama = "*"
-more-itertools = ">=4.0.0"
-packaging = "*"
-pluggy = ">=0.12,<1.0"
-py = ">=1.5.0"
-wcwidth = "*"
-
-[package.dependencies.importlib-metadata]
-python = "<3.8"
-version = ">=0.12"
-
-[package.extras]
-checkqa-mypy = ["mypy (v0.761)"]
-testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
-
-[[package]]
-category = "main"
-description = "Alternative regular expression module, to replace re."
-name = "regex"
-optional = false
-python-versions = "*"
-version = "2020.7.14"
-
-[[package]]
-category = "dev"
-description = "Python 2 and 3 compatibility utilities"
-name = "six"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
-version = "1.15.0"
-
-[[package]]
-category = "main"
-description = "Python Library for Tom's Obvious, Minimal Language"
-name = "toml"
-optional = false
-python-versions = "*"
-version = "0.10.1"
-
-[[package]]
-category = "main"
-description = "a fork of Python 2 and 3 ast modules with type comment support"
-name = "typed-ast"
-optional = false
-python-versions = "*"
-version = "1.4.1"
-
-[[package]]
-category = "dev"
-description = "Backported and Experimental Type Hints for Python 3.5+"
-name = "typing-extensions"
-optional = false
-python-versions = "*"
-version = "3.7.4.2"
-
-[[package]]
-category = "dev"
-description = "Measures the displayed width of unicode strings in a terminal"
-name = "wcwidth"
-optional = false
-python-versions = "*"
-version = "0.2.5"
-
-[[package]]
-category = "dev"
-description = "Backport of pathlib-compatible object wrapper for zip files"
-marker = "python_version < \"3.8\""
-name = "zipp"
-optional = false
-python-versions = ">=3.6"
-version = "3.1.0"
-
-[package.extras]
-docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
-testing = ["jaraco.itertools", "func-timeout"]
-
-[metadata]
-content-hash = "e591cefbf7f181c5a3dbb815804404256412aa148b25f12528bbad5dfb31e6e7"
-python-versions = "^3.7"
-
-[metadata.files]
-appdirs = [
- {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
- {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
-]
-atomicwrites = [
- {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
- {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
-]
-attrs = [
- {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"},
- {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"},
-]
-black = [
- {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"},
- {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"},
-]
-click = [
- {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
- {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
-]
-colorama = [
- {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"},
- {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"},
-]
-importlib-metadata = [
- {file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"},
- {file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"},
-]
-more-itertools = [
- {file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"},
- {file = "more_itertools-8.4.0-py3-none-any.whl", hash = "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"},
-]
-mypy = [
- {file = "mypy-0.782-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:2c6cde8aa3426c1682d35190b59b71f661237d74b053822ea3d748e2c9578a7c"},
- {file = "mypy-0.782-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9c7a9a7ceb2871ba4bac1cf7217a7dd9ccd44c27c2950edbc6dc08530f32ad4e"},
- {file = "mypy-0.782-cp35-cp35m-win_amd64.whl", hash = "sha256:c05b9e4fb1d8a41d41dec8786c94f3b95d3c5f528298d769eb8e73d293abc48d"},
- {file = "mypy-0.782-cp36-cp36m-macosx_10_6_x86_64.whl", hash = "sha256:6731603dfe0ce4352c555c6284c6db0dc935b685e9ce2e4cf220abe1e14386fd"},
- {file = "mypy-0.782-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:f05644db6779387ccdb468cc47a44b4356fc2ffa9287135d05b70a98dc83b89a"},
- {file = "mypy-0.782-cp36-cp36m-win_amd64.whl", hash = "sha256:b7fbfabdbcc78c4f6fc4712544b9b0d6bf171069c6e0e3cb82440dd10ced3406"},
- {file = "mypy-0.782-cp37-cp37m-macosx_10_6_x86_64.whl", hash = "sha256:3fdda71c067d3ddfb21da4b80e2686b71e9e5c72cca65fa216d207a358827f86"},
- {file = "mypy-0.782-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d7df6eddb6054d21ca4d3c6249cae5578cb4602951fd2b6ee2f5510ffb098707"},
- {file = "mypy-0.782-cp37-cp37m-win_amd64.whl", hash = "sha256:a4a2cbcfc4cbf45cd126f531dedda8485671545b43107ded25ce952aac6fb308"},
- {file = "mypy-0.782-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6bb93479caa6619d21d6e7160c552c1193f6952f0668cdda2f851156e85186fc"},
- {file = "mypy-0.782-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:81c7908b94239c4010e16642c9102bfc958ab14e36048fa77d0be3289dda76ea"},
- {file = "mypy-0.782-cp38-cp38-win_amd64.whl", hash = "sha256:5dd13ff1f2a97f94540fd37a49e5d255950ebcdf446fb597463a40d0df3fac8b"},
- {file = "mypy-0.782-py3-none-any.whl", hash = "sha256:e0b61738ab504e656d1fe4ff0c0601387a5489ca122d55390ade31f9ca0e252d"},
- {file = "mypy-0.782.tar.gz", hash = "sha256:eff7d4a85e9eea55afa34888dfeaccde99e7520b51f867ac28a48492c0b1130c"},
-]
-mypy-extensions = [
- {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
- {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
-]
-packaging = [
- {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"},
- {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"},
-]
-pathspec = [
- {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"},
- {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"},
-]
-pluggy = [
- {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
- {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
-]
-py = [
- {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"},
- {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"},
-]
-pyparsing = [
- {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
- {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
-]
-pyserial = [
- {file = "pyserial-3.4-py2.py3-none-any.whl", hash = "sha256:e0770fadba80c31013896c7e6ef703f72e7834965954a78e71a3049488d4d7d8"},
- {file = "pyserial-3.4.tar.gz", hash = "sha256:6e2d401fdee0eab996cf734e67773a0143b932772ca8b42451440cfed942c627"},
-]
-pytest = [
- {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"},
- {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"},
-]
-regex = [
- {file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"},
- {file = "regex-2020.7.14-cp27-cp27m-win_amd64.whl", hash = "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644"},
- {file = "regex-2020.7.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc"},
- {file = "regex-2020.7.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067"},
- {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd"},
- {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88"},
- {file = "regex-2020.7.14-cp36-cp36m-win32.whl", hash = "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4"},
- {file = "regex-2020.7.14-cp36-cp36m-win_amd64.whl", hash = "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f"},
- {file = "regex-2020.7.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162"},
- {file = "regex-2020.7.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf"},
- {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7"},
- {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89"},
- {file = "regex-2020.7.14-cp37-cp37m-win32.whl", hash = "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6"},
- {file = "regex-2020.7.14-cp37-cp37m-win_amd64.whl", hash = "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204"},
- {file = "regex-2020.7.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99"},
- {file = "regex-2020.7.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e"},
- {file = "regex-2020.7.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e"},
- {file = "regex-2020.7.14-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a"},
- {file = "regex-2020.7.14-cp38-cp38-win32.whl", hash = "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341"},
- {file = "regex-2020.7.14-cp38-cp38-win_amd64.whl", hash = "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840"},
- {file = "regex-2020.7.14.tar.gz", hash = "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb"},
-]
-six = [
- {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
- {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
-]
-toml = [
- {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"},
- {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"},
-]
-typed-ast = [
- {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"},
- {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"},
- {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"},
- {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"},
- {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"},
- {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"},
- {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"},
- {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"},
- {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"},
- {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"},
- {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"},
- {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"},
- {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"},
- {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"},
- {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"},
- {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"},
- {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"},
- {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"},
- {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"},
- {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"},
- {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"},
-]
-typing-extensions = [
- {file = "typing_extensions-3.7.4.2-py2-none-any.whl", hash = "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"},
- {file = "typing_extensions-3.7.4.2-py3-none-any.whl", hash = "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5"},
- {file = "typing_extensions-3.7.4.2.tar.gz", hash = "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae"},
-]
-wcwidth = [
- {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
- {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
-]
-zipp = [
- {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"},
- {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"},
-]
diff --git a/pyharp/__init__.py b/pyharp/__init__.py
deleted file mode 100644
index 3dc1f76..0000000
--- a/pyharp/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-__version__ = "0.1.0"
diff --git a/pyharp/device.py b/pyharp/device.py
deleted file mode 100644
index 5e0e64d..0000000
--- a/pyharp/device.py
+++ /dev/null
@@ -1,200 +0,0 @@
-import serial
-from typing import Optional
-from pathlib import Path
-
-from pyharp.messages import HarpMessage, ReplyHarpMessage
-from pyharp.messages import CommonRegisters
-from pyharp import device_names
-
-
-class Device:
- """
- https://github.com/harp-tech/protocol/blob/master/Device%201.0%201.3%2020190207.pdf
- """
-
- _ser: serial.Serial
- _dump_file_path: Path
-
- WHO_AM_I: int
- WHO_AM_I_DEVICE: str
- HW_VERSION_H: int
- HW_VERSION_L: int
- ASSEMBLY_VERSION: int
- HARP_VERSION_H: int
- HARP_VERSION_L: int
- FIRMWARE_VERSION_H: int
- FIRMWARE_VERSION_L: int
- # TIMESTAMP_SECOND = 0x08
- # TIMESTAMP_MICRO = 0x09
- # OPERATION_CTRL = 0x0A
- # RESET_DEV = 0x0B
- DEVICE_NAME: str
-
- def __init__(self, serial_port: str, dump_file_path: Optional[str] = None):
- self._serial_port = serial_port
- if dump_file_path is None:
- self._dump_file_path = None
- else:
- self._dump_file_path = Path() / dump_file_path
- self.connect()
- self.load()
-
- def load(self) -> None:
- self.WHO_AM_I = self.read_who_am_i()
- self.WHO_AM_I_DEVICE = self.read_who_am_i_device()
- self.HW_VERSION_H = self.read_hw_version_h()
- self.HW_VERSION_L = self.read_hw_version_l()
- self.ASSEMBLY_VERSION = self.read_assembly_version()
- self.HARP_VERSION_H = self.read_harp_h_version()
- self.HARP_VERSION_L = self.read_harp_l_version()
- self.FIRMWARE_VERSION_H = self.read_fw_h_version()
- self.FIRMWARE_VERSION_L = self.read_fw_l_version()
- self.DEVICE_NAME = self.read_device_name()
-
- def info(self) -> None:
- print("Device info:")
- #print(f"* Who am I (ID): {self.WHO_AM_I}")
- #print(f"* Who am I (Device): {self.WHO_AM_I_DEVICE}")
- print(f"* Who am I: ({self.WHO_AM_I}) {self.WHO_AM_I_DEVICE}")
- print(f"* HW version: {self.HW_VERSION_H}.{self.HW_VERSION_L}")
- print(f"* Assembly version: {self.ASSEMBLY_VERSION}")
- print(f"* HARP version: {self.HARP_VERSION_H}.{self.HARP_VERSION_L}")
- print(
- f"* Firmware version: {self.FIRMWARE_VERSION_H}.{self.FIRMWARE_VERSION_L}"
- )
- print(f"* Device user name: {self.DEVICE_NAME}")
-
- def read(self):
- pass
-
- def connect(self) -> None:
- self._ser = serial.Serial(
- self._serial_port, # "/dev/tty.usbserial-A106C8O9"
- baudrate=1000000,
- timeout=1,
- parity=serial.PARITY_NONE,
- stopbits=1,
- bytesize=8,
- rtscts=True,
- )
-
- def disconnect(self) -> None:
- self._ser.close()
-
- def read_who_am_i(self) -> int:
- address = CommonRegisters.WHO_AM_I
-
- reply: ReplyHarpMessage = self.send(
- HarpMessage.ReadU16(address).frame, dump=False
- )
-
- return reply.payload_as_int()
-
- def read_who_am_i_device(self) -> str:
- address = CommonRegisters.WHO_AM_I
-
- reply: ReplyHarpMessage = self.send(
- HarpMessage.ReadU16(address).frame, dump=False
- )
-
- return device_names.get(reply.payload_as_int())
-
- def read_hw_version_h(self) -> int:
- address = CommonRegisters.HW_VERSION_H
-
- reply: ReplyHarpMessage = self.send(
- HarpMessage.ReadU8(address).frame, dump=False
- )
-
- return reply.payload_as_int()
-
- def read_hw_version_l(self) -> int:
- address = CommonRegisters.HW_VERSION_L
-
- reply: ReplyHarpMessage = self.send(
- HarpMessage.ReadU8(address).frame, dump=False
- )
-
- return reply.payload_as_int()
-
- def read_assembly_version(self) -> int:
- address = CommonRegisters.ASSEMBLY_VERSION
-
- reply: ReplyHarpMessage = self.send(
- HarpMessage.ReadU8(address).frame, dump=False
- )
-
- return reply.payload_as_int()
-
- def read_harp_h_version(self) -> int:
- address = CommonRegisters.HARP_VERSION_H
-
- reply: ReplyHarpMessage = self.send(
- HarpMessage.ReadU8(address).frame, dump=False
- )
-
- return reply.payload_as_int()
-
- def read_harp_l_version(self) -> int:
- address = CommonRegisters.HARP_VERSION_L
-
- reply: ReplyHarpMessage = self.send(
- HarpMessage.ReadU8(address).frame, dump=False
- )
-
- return reply.payload_as_int()
-
- def read_fw_h_version(self) -> int:
- address = CommonRegisters.FIRMWARE_VERSION_H
-
- reply: ReplyHarpMessage = self.send(
- HarpMessage.ReadU8(address).frame, dump=False
- )
-
- return reply.payload_as_int()
-
- def read_fw_l_version(self) -> int:
- address = CommonRegisters.FIRMWARE_VERSION_L
-
- reply: ReplyHarpMessage = self.send(
- HarpMessage.ReadU8(address).frame, dump=False
- )
-
- return reply.payload_as_int()
-
- def read_device_name(self) -> str:
- address = CommonRegisters.DEVICE_NAME
-
- # reply: Optional[bytes] = self.send(HarpMessage.ReadU8(address).frame, 13 + 24)
- reply: ReplyHarpMessage = self.send(
- HarpMessage.ReadU8(address).frame, dump=False
- )
-
- return reply.payload_as_string()
-
- def send(self, message_bytes: bytearray, dump: bool = True) -> ReplyHarpMessage:
-
- self._ser.write(message_bytes)
-
- # TODO: handle case where read is None
-
- message_type = self._ser.read(1)[0] # byte array with only one byte
- message_length = self._ser.read(1)[0]
- message_content = self._ser.read(message_length)
-
- frame = bytearray()
- frame.append(message_type)
- frame.append(message_length)
- frame += message_content
-
- reply: ReplyHarpMessage = HarpMessage.parse(frame)
-
- if dump:
- self._dump_reply(reply.frame)
-
- return reply
-
- def _dump_reply(self, reply: bytes):
- assert self._dump_file_path is not None
- with self._dump_file_path.open(mode="ab") as f:
- f.write(reply)
diff --git a/pyharp/device_names.py b/pyharp/device_names.py
deleted file mode 100644
index e5d8cbd..0000000
--- a/pyharp/device_names.py
+++ /dev/null
@@ -1,41 +0,0 @@
-def get(value: int) -> str:
- if value == 1024:
- return "Poke"
- elif value == 1040:
- return "MultiPwm"
- elif value == 1056:
- return "Wear"
- elif value == 1072:
- return "VoltsDrive"
- elif value == 1088:
- return "LedController"
- elif value == 1104:
- return "Synchronizer"
- elif value == 1121:
- return "SimpleAnalogGenerator"
- elif value == 1136:
- return "Archimedes"
- elif value == 1152:
- return "ClockSynchronizer"
- elif value == 1168:
- return "Camera"
- elif value == 1184:
- return "PyControl"
- elif value == 1200:
- return "FlyPad"
- elif value == 1216:
- return "Behavior"
- elif value == 1232:
- return "LoadCells"
- elif value == 1248:
- return "AudioSwitch"
- elif value == 1264:
- return "Rgb"
- elif value == 1200:
- return "FlyPad"
- elif value == 2064:
- return "FP3002"
- elif value == 2080:
- return "IblBehavior"
- else:
- return "NotSpecified"
diff --git a/pyharp/main.py b/pyharp/main.py
deleted file mode 100644
index 7197906..0000000
--- a/pyharp/main.py
+++ /dev/null
@@ -1,9 +0,0 @@
-import serial
-
-
-def main() -> None:
- print("hello world")
-
-
-if __name__ == "__main__":
- main()
diff --git a/pyharp/messages.py b/pyharp/messages.py
deleted file mode 100644
index 383ae2e..0000000
--- a/pyharp/messages.py
+++ /dev/null
@@ -1,395 +0,0 @@
-# from abc import ABC, abstractmethod
-from typing import Union, Tuple, Optional
-
-
-class MessageType:
- READ: int = 1
- WRITE: int = 2
- EVENT: int = 3
- READ_ERROR: int = 9
- WRITE_ERROR: int = 10
-
-
-class PayloadType:
- isUnsigned: int = 0x00
- isSigned: int = 0x80
- isFloat: int = 0x40
- hasTimestamp: int = 0x10
-
- U8 = isUnsigned | 1 # 1
- S8 = isSigned | 1 # 129
- U16 = isUnsigned | 2 # 2
- S16 = isSigned | 2 # 130
- U32 = isUnsigned | 4
- S32 = isSigned | 4
- U64 = isUnsigned | 8
- S64 = isSigned | 8
- Float = isFloat | 4
- Timestamp = hasTimestamp
- TimestampedU8 = hasTimestamp | U8
- TimestampedS8 = hasTimestamp | S8
- TimestampedU16 = hasTimestamp | U16
- TimestampedS16 = hasTimestamp | S16
- TimestampedU32 = hasTimestamp | U32
- TimestampedS32 = hasTimestamp | S32
- TimestampedU64 = hasTimestamp | U64
- TimestampedS64 = hasTimestamp | S64
- TimestampedFloat = hasTimestamp | Float
-
- ALL_UNSIGNED = [U8, U16, U32, TimestampedU8, TimestampedU16]
- ALL_SIGNED = [S8, S16, S32, TimestampedS8, TimestampedS16]
-
-
-class CommonRegisters:
- WHO_AM_I = 0x00
- HW_VERSION_H = 0x01
- HW_VERSION_L = 0x02
- ASSEMBLY_VERSION = 0x03
- HARP_VERSION_H = 0x04
- HARP_VERSION_L = 0x05
- FIRMWARE_VERSION_H = 0x06
- FIRMWARE_VERSION_L = 0x07
- TIMESTAMP_SECOND = 0x08
- TIMESTAMP_MICRO = 0x09
- OPERATION_CTRL = 0x0A
- RESET_DEV = 0x0B
- DEVICE_NAME = 0x0C
-
-
-T = Union[int, bytearray]
-
-
-class HarpMessage:
- DEFAULT_PORT: int = 255
- _frame: bytearray
-
- def __init__(self):
- self._frame = bytearray()
-
- def calculate_checksum(self) -> int:
- checksum: int = 0
- for i in self.frame:
- checksum += i
- return checksum & 255
-
- @property
- def frame(self) -> bytearray:
- return self._frame
-
- @property
- def message_type(self) -> int:
- return self._frame[0]
-
- @staticmethod
- def ReadU8(address: int) -> "ReadU8HarpMessage":
- return ReadU8HarpMessage(address)
-
- @staticmethod
- def ReadS8(address: int) -> "ReadS8HarpMessage":
- return ReadS8HarpMessage(address)
-
- @staticmethod
- def ReadS16(address: int) -> "ReadS16HarpMessage":
- return ReadS16HarpMessage(address)
-
- @staticmethod
- def ReadU16(address: int) -> "ReadU16HarpMessage":
- return ReadU16HarpMessage(address)
-
- @staticmethod
- def WriteU8(address: int, value: int) -> "WriteU8HarpMessage":
- return WriteU8HarpMessage(address, value)
-
- @staticmethod
- def WriteS8(address: int, value: int) -> "WriteS8HarpMessage":
- return WriteS8HarpMessage(address, value)
-
- @staticmethod
- def WriteS16(address: int, value: int) -> "WriteS16HarpMessage":
- return WriteS16HarpMessage(address, value)
-
- @staticmethod
- def WriteU16(address: int, value: int) -> "WriteU16HarpMessage":
- return WriteU16HarpMessage(address, value)
-
- @staticmethod
- def parse(frame: bytearray) -> "ReplyHarpMessage":
- return ReplyHarpMessage(frame)
-
-
-class ReplyHarpMessage(HarpMessage):
- PAYLOAD_START_ADDRESS: int
- PAYLOAD_LAST_ADDRESS: int
- _message_type: int
- _length: int
- _address: int
- _payload_type: int
- _payload: bytes
- _checksum: int
-
- def __init__(
- self, frame: bytearray,
- ):
- """
-
- :param payload_type:
- :param payload:
- :param address:
- :param offset: how many bytes more besides the length corresponding to U8 (for example, for U16 it would be offset=1)
- """
-
- self._frame = frame
-
- self._message_type = frame[0]
- self._length = frame[1]
- self._address = frame[2]
- self._port = frame[3]
- self._payload_type = frame[4]
- # TOOO: add timestamp here
- self._payload = frame[
- 11:-1
- ] # retrieve all content from 11 (where payload starts) until the checksum (not inclusive)
- self._checksum = frame[-1] # last index is the checksum
-
- # print(f"Type: {self.message_type}")
- # print(f"Length: {self.length}")
- # print(f"Address: {self.address}")
- # print(f"Port: {self.port}")
- # print(f"Payload Type: {self.payload_type}")
- # print(f"Payload: {self.payload}")
- # print(f"Checksum: {self.checksum}")
- # print(f"Frame: {self.frame}")
-
- @property
- def frame(self) -> bytearray:
- return self._frame
-
- @property
- def message_type(self) -> int:
- return self._message_type
-
- @property
- def length(self) -> int:
- return self._length
-
- @property
- def address(self) -> int:
- return self._address
-
- @property
- def port(self) -> int:
- return self._port
-
- @property
- def payload_type(self) -> int:
- return self._payload_type
-
- @property
- def payload(self) -> bytes:
- return self._payload
-
- def payload_as_int(self) -> int:
- value: int = 0
- if self.payload_type in PayloadType.ALL_UNSIGNED:
- value = int.from_bytes(self.payload, byteorder="little", signed=False)
- elif self.payload_type in PayloadType.ALL_SIGNED:
- value = int.from_bytes(self.payload, byteorder="little", signed=True)
- return value
-
- def payload_as_int_array(self):
- pass # TODO: implement this
-
- def payload_as_string(self) -> str:
- return self.payload.decode("utf-8")
-
- @property
- def checksum(self) -> int:
- return self._checksum
-
-
-class ReadHarpMessage(HarpMessage):
- MESSAGE_TYPE: int = MessageType.READ
- _length: int
- _address: int
- _payload_type: int
- _checksum: int
-
- def __init__(self, payload_type: int, address: int):
- self._frame = bytearray()
-
- self._frame.append(self.MESSAGE_TYPE)
-
- length: int = 4
- self._frame.append(length)
-
- self._frame.append(address)
- self._frame.append(self.DEFAULT_PORT)
- self._frame.append(payload_type)
- self._frame.append(self.calculate_checksum())
-
- # def calculate_checksum(self) -> int:
- # return (
- # self.message_type
- # + self.length
- # + self.address
- # + self.port
- # + self.payload_type
- # ) & 255
-
- @property
- def message_type(self) -> int:
- return self._frame[0]
-
- @property
- def length(self) -> int:
- return self._frame[1]
-
- @property
- def address(self) -> int:
- return self._frame[2]
-
- @property
- def port(self) -> int:
- return self._frame[3]
-
- @property
- def payload_type(self) -> int:
- return self._frame[4]
-
- @property
- def checksum(self) -> int:
- return self._frame[5]
-
-
-class ReadU8HarpMessage(ReadHarpMessage):
- def __init__(self, address: int):
- super().__init__(PayloadType.U8, address)
-
-
-class ReadS8HarpMessage(ReadHarpMessage):
- def __init__(self, address: int):
- super().__init__(PayloadType.S8, address)
-
-
-class ReadU16HarpMessage(ReadHarpMessage):
- def __init__(self, address: int):
- super().__init__(PayloadType.U16, address)
-
-
-class ReadS16HarpMessage(ReadHarpMessage):
- def __init__(self, address: int):
- super().__init__(PayloadType.S16, address)
-
-
-class WriteHarpMessage(HarpMessage):
- BASE_LENGTH: int = 5
- MESSAGE_TYPE: int = MessageType.WRITE
- _length: int
- _address: int
- _payload_type: int
- _payload: int
- _checksum: int
-
- def __init__(
- self, payload_type: int, payload: bytes, address: int, offset: int = 0
- ):
- """
-
- :param payload_type:
- :param payload:
- :param address:
- :param offset: how many bytes more besides the length corresponding to U8 (for example, for U16 it would be offset=1)
- """
- self._frame = bytearray()
-
- self._frame.append(self.MESSAGE_TYPE)
-
- self._frame.append(self.BASE_LENGTH + offset)
-
- self._frame.append(address)
- self._frame.append(HarpMessage.DEFAULT_PORT)
- self._frame.append(payload_type)
-
- for i in payload:
- self._frame.append(i)
-
- self._frame.append(self.calculate_checksum())
-
- # def calculate_checksum(self) -> int:
- # return (
- # self.message_type
- # + self.length
- # + self.address
- # + self.port
- # + self.payload_type
- # + self.payload
- # ) & 255
-
- @property
- def message_type(self) -> int:
- return self._frame[0]
-
- @property
- def length(self) -> int:
- return self._frame[1]
-
- @property
- def address(self) -> int:
- return self._frame[2]
-
- @property
- def port(self) -> int:
- return self._frame[3]
-
- @property
- def payload_type(self) -> int:
- return self._frame[4]
-
- @property
- def checksum(self) -> int:
- return self._frame[-1]
-
-
-class WriteU8HarpMessage(WriteHarpMessage):
- def __init__(self, address: int, value: int):
- super().__init__(PayloadType.U8, value.to_bytes(1, byteorder="little"), address)
-
- @property
- def payload(self) -> int:
- return self.frame[5]
-
-
-class WriteS8HarpMessage(WriteHarpMessage):
- def __init__(self, address: int, value: int):
- super().__init__(
- PayloadType.S8, value.to_bytes(1, byteorder="little", signed=True), address
- )
-
- @property
- def payload(self) -> int:
- return int.from_bytes([self.frame[5]], byteorder="little", signed=True)
-
-
-class WriteU16HarpMessage(WriteHarpMessage):
- def __init__(self, address: int, value: int):
- super().__init__(
- PayloadType.U16, value.to_bytes(2, byteorder="little", signed=False), address, offset=1
- )
-
- @property
- def payload(self) -> int:
- return int.from_bytes(self._frame[5:7], byteorder="little", signed=False)
-
-
-class WriteS16HarpMessage(WriteHarpMessage):
- def __init__(self, address: int, value: int):
- super().__init__(
- PayloadType.S16,
- value.to_bytes(2, byteorder="little", signed=True),
- address,
- offset=1,
- )
-
- @property
- def payload(self) -> int:
- return int.from_bytes(self._frame[5:7], byteorder="little", signed=True)
diff --git a/pyproject.toml b/pyproject.toml
index 62037dc..ae3c412 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,23 +1,57 @@
-[tool.poetry]
+[project]
name = "pyharp"
-version = "0.1.0"
+version = "0.2.0"
description = "Library for data acquisition and control of devices implementing the Harp protocol."
-authors = ["filcarv "]
+authors = [{ name= "Hardware and Software Platform, Champalimaud Foundation", email="software@research.fchampalimaud.org"}]
license = "MIT"
readme = 'README.md'
+keywords = ['python', 'harp']
+requires-python = ">=3.9,<4.0"
+dependencies = ["harp-protocol", "harp-serial"]
-[tool.poetry.dependencies]
-python = "^3.4"
-pyserial = "^3.4"
+[project.urls]
+Repository = "https://github.com/fchampalimaud/pyharp/"
+"Bug Tracker" = "https://github.com/fchampalimaud/pyharp/issues"
+Documentation = "https://fchampalimaud.github.io/pyharp/"
-[tool.poetry.dev-dependencies]
-pytest = "^5.2"
-mypy = "^0.782"
-black = "^19.10b0"
+[tool.uv.sources]
+harp-protocol = { workspace = true }
+harp-serial = { workspace = true }
-[build-system]
-requires = ["poetry>=0.12"]
-build-backend = "poetry.masonry.api"
+[tool.uv.workspace]
+members = [
+ "src/*",
+]
-[tool.poetry.scripts]
-pyharp = "pyharp.main:main"
+[dependency-groups]
+dev = [
+ "griffe-fieldz>=0.2.1",
+ "mkdocs>=1.6.1",
+ "mkdocs-codeinclude-plugin>=0.2.1",
+ "mkdocs-git-authors-plugin>=0.9.4",
+ "mkdocs-git-committers-plugin-2>=2.5.0",
+ "mkdocs-include-markdown-plugin>=7.1.6",
+ "mkdocs-material>=9.6.9",
+ "mkdocs-monorepo-plugin>=1.1.2",
+ "mkdocstrings-python>=1.16.6",
+ "pytest>=8.3.5",
+ "pytest-cov>=6.1.1",
+ "ruff>=0.11.0",
+]
+
+[tool.ruff.lint.pydocstyle]
+convention = "numpy"
+
+[tool.pytest.ini_options]
+pythonpath = ["."]
+addopts = ["--import-mode=importlib", ]
+python_files = [
+ "tests.py",
+ "test_*.py"
+]
+
+[[tool.uv.index]]
+name = "testpypi"
+url = "https://test.pypi.org/simple/"
+publish-url = "https://test.pypi.org/legacy/"
+explicit = true
diff --git a/run_docs.ps1 b/run_docs.ps1
new file mode 100644
index 0000000..913404a
--- /dev/null
+++ b/run_docs.ps1
@@ -0,0 +1,31 @@
+# Resolve script directory (optional) and change to that directory if desired:
+# Set-Location -Path (Split-Path -Path $MyInvocation.MyCommand.Definition -Parent)
+
+# Launch mkdocs via uv (argument handling)
+param(
+ [string]$action = ""
+)
+
+# Find all subdirectories of .\harp.devices (follow symlinks)
+$harpRoot = Join-Path -Path (Get-Location) -ChildPath "harp.devices"
+$dirs = Get-ChildItem -LiteralPath $harpRoot -Directory -Force -ErrorAction SilentlyContinue |
+ ForEach-Object { $_.FullName }
+
+# Join them with ':' (Unix-style path separator)
+$harpDevicesPaths = ($dirs -join ";")
+
+# Prepend to PYTHONPATH (preserve existing)
+if ($env:PYTHONPATH) {
+ $env:PYTHONPATH = "{0};{1}" -f $harpDevicesPaths, $env:PYTHONPATH
+} else {
+ $env:PYTHONPATH = $harpDevicesPaths
+}
+
+# Optionally print for debugging
+Write-Output "PYTHONPATH set to: $env:PYTHONPATH"
+
+switch ($action) {
+ "build" { uv run mkdocs build }
+ "deploy" { uv run mkdocs gh-deploy }
+ default { uv run mkdocs serve }
+}
diff --git a/run_docs.sh b/run_docs.sh
new file mode 100755
index 0000000..d95ab3c
--- /dev/null
+++ b/run_docs.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+# Find all subdirectories of harp.devices and join them with ':'
+HARP_DEVICES_PATHS=$(find -L ./harp.devices -mindepth 1 -maxdepth 1 -type d | paste -sd ":" -)
+
+# Prepend to PYTHONPATH (preserve existing PYTHONPATH)
+export PYTHONPATH="$HARP_DEVICES_PATHS:$PYTHONPATH"
+
+# Optionally print for debugging
+echo "PYTHONPATH set to: $PYTHONPATH"
+
+# Launch mkdocs build or serve
+if [ "$1" = "build" ]; then
+ uv run mkdocs build
+elif [ "$1" = "deploy" ]; then
+ uv run mkdocs gh-deploy
+else
+ uv run mkdocs serve
+fi
diff --git a/src/harp-protocol/README.md b/src/harp-protocol/README.md
new file mode 100644
index 0000000..80289eb
--- /dev/null
+++ b/src/harp-protocol/README.md
@@ -0,0 +1,7 @@
+# harp-protocol
+
+[](https://badge.fury.io/py/harp-protocol)
+
+The Harp Protocol is a binary communication protocol created in order to facilitate and unify the interaction between different devices. It was designed with efficiency and ease of parsing in mind.
+
+For more detail please check Harp Tech's official documentation [here](https://harp-tech.org/protocol/BinaryProtocol-8bit.html).
diff --git a/src/harp-protocol/harp/protocol/__init__.py b/src/harp-protocol/harp/protocol/__init__.py
new file mode 100644
index 0000000..12e69f4
--- /dev/null
+++ b/src/harp-protocol/harp/protocol/__init__.py
@@ -0,0 +1 @@
+from .base import * # noqa: F403
diff --git a/src/harp-protocol/harp/protocol/base.py b/src/harp-protocol/harp/protocol/base.py
new file mode 100644
index 0000000..8282c93
--- /dev/null
+++ b/src/harp-protocol/harp/protocol/base.py
@@ -0,0 +1,280 @@
+from datetime import datetime
+from enum import IntEnum, IntFlag
+
+# The reference epoch for UTC harp time
+REFERENCE_EPOCH = datetime(1904, 1, 1)
+
+# Bit masks for the PayloadType
+_isUnsigned: int = 0x00
+_isSigned: int = 0x80
+_isFloat: int = 0x40
+_hasTimestamp: int = 0x10
+
+
+class MessageType(IntEnum):
+ """
+ An enumeration of the allowed message types of a Harp message. More information on the MessageType byte of a Harp message can be found [here](https://harp-tech.org/protocol/BinaryProtocol-8bit.html#messagetype-1-byte).
+
+ Attributes
+ ----------
+ READ : int
+ The value that corresponds to a Read Harp message (1)
+ WRITE : int
+ The value that corresponds to a Write Harp message (2)
+ EVENT : int
+ The value that corresponds to an Event Harp message (3). Messages of this type are only meant to be send by the device
+ READ_ERROR : int
+ The value that corresponds to a Read Error Harp message (9). Messages of this type are only meant to be send by the device
+ WRITE_ERROR : int
+ The value that corresponds to a Write Error Harp message (10). Messages of this type are only meant to be send by the device
+ """
+
+ READ = 1
+ WRITE = 2
+ EVENT = 3
+ READ_ERROR = 9
+ WRITE_ERROR = 10
+
+
+class PayloadType(IntEnum):
+ """
+ An enumeration of the allowed payload types of a Harp message. More information on the PayloadType byte of a Harp message can be found [here](https://harp-tech.org/protocol/BinaryProtocol-8bit.html#payloadtype-1-byte).
+
+ Attributes
+ ----------
+ U8 : int
+ The value that corresponds to a message of type U8
+ S8 : int
+ The value that corresponds to a message of type S8
+ U16 : int
+ The value that corresponds to a message of type U16
+ S16 : int
+ The value that corresponds to a message of type S16
+ U32 : int
+ The value that corresponds to a message of type U32
+ S32 : int
+ The value that corresponds to a message of type S32
+ U64 : int
+ The value that corresponds to a message of type U64
+ S64 : int
+ The value that corresponds to a message of type S64
+ Float : int
+ The value that corresponds to a message of type Float
+ Timestamp: int
+ The value that corresponds to a message of type Timestamp. This is not a valid PayloadType, but it is used to indicate that the message has a timestamp.
+ TimestampedU8 : int
+ The value that corresponds to a message of type TimestampedU8
+ TimestampedS8 : int
+ The value that corresponds to a message of type TimestampedS8
+ TimestampedU16 : int
+ The value that corresponds to a message of type TimestampedU16
+ TimestampedS16 : int
+ The value that corresponds to a message of type TimestampedS16
+ TimestampedU32 : int
+ The value that corresponds to a message of type TimestampedU32
+ TimestampedS32 : int
+ The value that corresponds to a message of type TimestampedS32
+ TimestampedU64 : int
+ The value that corresponds to a message of type TimestampedU64
+ TimestampedS64 : int
+ The value that corresponds to a message of type TimestampedS64
+ TimestampedFloat : int
+ The value that corresponds to a message of type TimestampedFloat
+ """
+
+ U8 = _isUnsigned | 1
+ S8 = _isSigned | 1
+ U16 = _isUnsigned | 2
+ S16 = _isSigned | 2
+ U32 = _isUnsigned | 4
+ S32 = _isSigned | 4
+ U64 = _isUnsigned | 8
+ S64 = _isSigned | 8
+ Float = _isFloat | 4
+ Timestamp = _hasTimestamp
+ TimestampedU8 = _hasTimestamp | U8
+ TimestampedS8 = _hasTimestamp | S8
+ TimestampedU16 = _hasTimestamp | U16
+ TimestampedS16 = _hasTimestamp | S16
+ TimestampedU32 = _hasTimestamp | U32
+ TimestampedS32 = _hasTimestamp | S32
+ TimestampedU64 = _hasTimestamp | U64
+ TimestampedS64 = _hasTimestamp | S64
+ TimestampedFloat = _hasTimestamp | Float
+
+
+class CommonRegisters(IntEnum):
+ """
+ An enumeration with the registers that are common to every Harp device. More information on the common registers can be found [here](https://harp-tech.org/protocol/Device.html#table---list-of-available-common-registers).
+
+ Attributes
+ ----------
+ WHO_AM_I : int
+ The number of the `WHO_AM_I` register
+ HW_VERSION_H : int
+ The number of the `HW_VERSION_H` register
+ HW_VERSION_L : int
+ The number of the `HW_VERSION_L` register
+ ASSEMBLY_VERSION : int
+ The number of the `ASSEMBLY_VERSION` register
+ HARP_VERSION_H : int
+ The number of the `HARP_VERSION_H` register
+ HARP_VERSION_L : int
+ The number of the `HARP_VERSION_L` register
+ FIRMWARE_VERSION_H : int
+ The number of the `FIRMWARE_VERSION_H` register
+ FIRMWARE_VERSION_L : int
+ The number of the `FIRMWARE_VERSION_L` register
+ TIMESTAMP_SECOND : int
+ The number of the `TIMESTAMP_SECOND` register
+ TIMESTAMP_MICRO : int
+ The number of the `TIMESTAMP_MICRO` register
+ OPERATION_CTRL : int
+ The number of the `OPERATION_CTRL` register
+ RESET_DEV : int
+ The number of the `RESET_DEV` register
+ DEVICE_NAME : int
+ The number of the `DEVICE_NAME` register
+ SERIAL_NUMBER : int
+ The number of the `SERIAL_NUMBER` register
+ CLOCK_CONFIG : int
+ The number of the `CLOCK_CONFIG` register
+ TIMESTAMP_OFFSET : int
+ The number of the `TIMESTAMP_OFFSET` register
+ """
+
+ WHO_AM_I = 0x00
+ HW_VERSION_H = 0x01
+ HW_VERSION_L = 0x02
+ ASSEMBLY_VERSION = 0x03
+ HARP_VERSION_H = 0x04
+ HARP_VERSION_L = 0x05
+ FIRMWARE_VERSION_H = 0x06
+ FIRMWARE_VERSION_L = 0x07
+ TIMESTAMP_SECOND = 0x08
+ TIMESTAMP_MICRO = 0x09
+ OPERATION_CTRL = 0x0A
+ RESET_DEV = 0x0B
+ DEVICE_NAME = 0x0C
+ SERIAL_NUMBER = 0x0D
+ CLOCK_CONFIG = 0x0E
+ TIMESTAMP_OFFSET = 0x0F
+
+
+class OperationMode(IntEnum):
+ """
+ An enumeration with the operation modes of a Harp device. More information on the operation modes can be found [here](https://harp-tech.org/protocol/Device.html#r_operation_ctrl-u16--operation-mode-configuration).
+
+ Attributes
+ ----------
+ STANDBY : int
+ The value that corresponds to the Standby operation mode (0). The device has all the Events turned off
+ ACTIVE : int
+ The value that corresponds to the Active operation mode (1). The device turns ON the Events detection. Only the enabled Events will be operating
+ RESERVED : int
+ The value that corresponds to the Reserved operation mode (2)
+ SPEED : int
+ The value that corresponds to the Speed operation mode (3). The device enters Speed Mode
+ """
+
+ STANDBY = 0
+ ACTIVE = 1
+ RESERVED = 2
+ SPEED = 3
+
+
+class OperationCtrl(IntFlag):
+ """
+ An enumeration with the operation control bits of a Harp device. More information on the operation control bits can be found [here](https://harp-tech.org/protocol/Device.html#r_operation_ctrl-u16--operation-mode-configuration).
+
+ Attributes
+ ----------
+ OP_MODE : int
+ Bits 1:0 (0x03): Operation mode of the device.
+ 0: Standby Mode (all Events off, mandatory)
+ 1: Active Mode (Events detection enabled, mandatory)
+ 2: Reserved
+ 3: Speed Mode (device enters Speed Mode, optional; only responds to Speed Mode commands)
+ DUMP : int
+ Bit 3 (0x08): When set to 1, the device adds the content of all registers to the streaming buffer as Read messages. Always read as 0
+ MUTE_RPL : int
+ Bit 4 (0x10): If set to 1, replies to all commands are muted (not sent by the device)
+ VISUALEN : int
+ Bit 5 (0x20): If set to 1, visual indications (e.g., LEDs) operate. If 0, all visual indications are turned off
+ OPLEDEN : int
+ Bit 6 (0x40): If set to 1, the LED indicates the selected Operation Mode (see LED feedback table in documentation)
+ ALIVE_EN : int
+ Bit 7 (0x80): If set to 1, the device sends an Event Message with the R_TIMESTAMP_SECONDS content each second (heartbeat)
+ """
+
+ OP_MODE = 3 << 0
+ DUMP = 1 << 3
+ MUTE_RPL = 1 << 4
+ VISUALEN = 1 << 5
+ OPLEDEN = 1 << 6
+ ALIVE_EN = 1 << 7
+
+
+class ResetMode(IntEnum):
+ """
+ An enumeration with the reset modes and actions for the R_RESET_DEV register of a Harp device.
+ More information on the reset modes can be found [here](https://harp-tech.org/protocol/Device.html#r_reset_dev-u8--reset-device-and-save-non-volatile-registers).
+
+ Attributes
+ ----------
+ RST_DEF : int
+ Bit 0 (0x01): If set, resets the device and restores all registers (Common and Application) to default values.
+ EEPROM is erased and defaults become the permanent boot option
+ RST_EE : int
+ Bit 1 (0x02): If set, resets the device and restores all registers (Common and Application) from non-volatile memory (EEPROM).
+ EEPROM values remain the permanent boot option
+ SAVE : int
+ Bit 3 (0x08): If set, saves all non-volatile registers (Common and Application) to EEPROM and reboots.
+ EEPROM becomes the permanent boot option
+ NAME_TO_DEFAULT : int
+ Bit 4 (0x10): If set, reboots the device with the default name
+ BOOT_DEF : int
+ Bit 6 (0x40, read-only): Indicates the device booted with default register values
+ BOOT_EE : int
+ Bit 7 (0x80, read-only): Indicates the device booted with register values saved on the EEPROM
+ """
+
+ RST_DEF = 0x01
+ RST_EE = 0x02
+ SAVE = 0x08
+ NAME_TO_DEFAULT = 0x10
+ BOOT_DEF = 0x40
+ BOOT_EE = 0x80
+
+
+class ClockConfig(IntFlag):
+ """
+ An enumeration with the clock configuration bits for the R_CLOCK_CONFIG register of a Harp device.
+ More information can be found [here](https://harp-tech.org/protocol/Device.html#r_clock_config-u8--synchronization-clock-configuration).
+
+ Attributes
+ ----------
+ CLK_REP : int
+ Bit 0 (0x01): If set to 1, the device will repeat the Harp Synchronization Clock to the Clock Output connector, if available.
+ Acts as a daisy-chain by repeating the Clock Input to the Clock Output. Setting this bit also unlocks the Harp Synchronization Clock
+ CLK_GEN : int
+ Bit 1 (0x02): If set to 1, the device will generate Harp Synchronization Clock to the Clock Output connector, if available.
+ The Clock Input will be ignored. Read as 1 if the device is generating the Harp Synchronization Clock
+ REP_ABLE : int
+ Bit 3 (0x08, read-only): Indicates if the device is able (1) to repeat the Harp Synchronization Clock timestamp
+ GEN_ABLE : int
+ Bit 4 (0x10, read-only): Indicates if the device is able (1) to generate the Harp Synchronization Clock timestamp
+ CLK_UNLOCK : int
+ Bit 6 (0x40): If set to 1, the device will unlock the timestamp register counter (R_TIMESTAMP_SECOND) and accept new timestamp values.
+ Read as 1 if the timestamp register is unlocked
+ CLK_LOCK : int
+ Bit 7 (0x80): If set to 1, the device will lock the current timestamp register counter (R_TIMESTAMP_SECOND) and reject new timestamp values.
+ Read as 1 if the timestamp register is locked
+ """
+
+ CLK_REP = 0x01
+ CLK_GEN = 0x02
+ REP_ABLE = 0x08
+ GEN_ABLE = 0x10
+ CLK_UNLOCK = 0x40
+ CLK_LOCK = 0x80
diff --git a/src/harp-protocol/harp/protocol/device_names.py b/src/harp-protocol/harp/protocol/device_names.py
new file mode 100644
index 0000000..4682267
--- /dev/null
+++ b/src/harp-protocol/harp/protocol/device_names.py
@@ -0,0 +1,52 @@
+from collections import defaultdict
+
+# This file contains the device names for the current version of the harp library.
+# These names were extracted from https://github.com/harp-tech/protocol/blob/main/whoami.yml
+# commit used: https://github.com/harp-tech/protocol/commit/3e2a228
+
+current_device_names = {
+ 256: "USBHub",
+ 1024: "Poke",
+ 1040: "MultiPwmGenerator",
+ 1056: "Wear",
+ 1058: "WearBaseStationGen2",
+ 1072: "Driver12Volts",
+ 1088: "LedController",
+ 1104: "Synchronizer",
+ 1106: "InputExpander",
+ 1108: "OutputExpander",
+ 1121: "SimpleAnalogGenerator",
+ 1130: "StepperDriver",
+ 1136: "Archimedes",
+ 1140: "Olfactometer",
+ 1152: "ClockSynchronizer",
+ 1154: "TimestampGeneratorGen1",
+ 1158: "TimestampGeneratorGen3",
+ 1168: "CameraController",
+ 1170: "CameraControllerGen2",
+ 1184: "PyControlAdapter",
+ 1200: "FlyPad",
+ 1216: "Behavior",
+ 1224: "VestibularH1",
+ 1225: "VestibularH2",
+ 1232: "LoadCells",
+ 1236: "AnalogInput",
+ 1248: "RgbArray",
+ 1280: "SoundCard",
+ 1282: "CurrentDriver",
+ 1296: "SyringePump",
+ 1298: "LaserDriverController",
+ 1400: "LicketySplit",
+ 1401: "SniffDetector",
+ 1402: "Treadmill",
+ 1403: "cuTTLefish",
+ 1404: "WhiteRabbit",
+ 1405: "EnvironmentSensor",
+ 2064: "NeurophotometricsFP3002",
+ 2080: "Ibl_behavior_control",
+ 2094: "RfidReader",
+ 2110: "Pluma",
+}
+
+device_names = defaultdict(lambda: "NotSpecified")
+device_names.update(current_device_names)
diff --git a/src/harp-protocol/harp/protocol/exceptions.py b/src/harp-protocol/harp/protocol/exceptions.py
new file mode 100644
index 0000000..61650af
--- /dev/null
+++ b/src/harp-protocol/harp/protocol/exceptions.py
@@ -0,0 +1,32 @@
+class HarpException(Exception):
+ """Base class for all exceptions raised related with Harp."""
+
+ pass
+
+
+class HarpWriteException(HarpException):
+ """
+ Exception raised when there is an error writing to a register in the Harp device.
+ """
+
+ def __init__(self, register):
+ super().__init__(f"Error writing to register {register}")
+ self.register = register
+
+
+class HarpReadException(HarpException):
+ """
+ Exception raised when there is an error reading from a register in the Harp device.
+ """
+
+ def __init__(self, register):
+ super().__init__(f"Error reading from register {register}")
+ self.register = register
+
+
+class HarpTimeoutError(HarpException):
+ """Raised when no reply is received within the configured timeout."""
+
+ def __init__(self, timeout_s: float):
+ super().__init__(f"No reply received within {timeout_s} seconds.")
+ self.timeout_s = timeout_s
diff --git a/src/harp-protocol/harp/protocol/messages.py b/src/harp-protocol/harp/protocol/messages.py
new file mode 100644
index 0000000..1ee5595
--- /dev/null
+++ b/src/harp-protocol/harp/protocol/messages.py
@@ -0,0 +1,582 @@
+from __future__ import annotations # for type hints (PEP 563)
+
+import struct
+from typing import Optional, Union
+
+from harp.protocol import MessageType, PayloadType
+from harp.protocol.exceptions import HarpReadException
+
+
+class HarpMessage:
+ """
+ The `HarpMessage` class implements the Harp message as described in the [protocol](https://harp-tech.org/protocol/BinaryProtocol-8bit.html).
+
+ Attributes
+ ----------
+ frame : bytearray
+ The bytearray containing the whole Harp message
+ message_type : MessageType
+ The message type
+ length : int
+ The length parameter of the Harp message
+ address : int
+ The address of the register to which the Harp message refers to
+ port : int
+ Indicates the origin or destination of the Harp message in case the device is a hub of Harp devices. The value 255 points to the device itself (default value).
+ payload_type : PayloadType
+ The payload type
+ checksum : int
+ The sum of all bytes contained in the Harp message
+ """
+
+ DEFAULT_PORT: int = 255
+ BASE_LENGTH: int = 4
+ _frame: bytearray = bytearray()
+ _port: int = DEFAULT_PORT
+
+ def calculate_checksum(self) -> int:
+ """
+ Calculates the checksum of the Harp message.
+
+ Returns
+ -------
+ int
+ The value of the checksum
+ """
+ checksum: int = 0
+ for i in self.frame:
+ checksum += i
+ return checksum & 255
+
+ @property
+ def frame(self) -> bytearray:
+ """
+ The bytearray containing the whole Harp message.
+
+ Returns
+ -------
+ bytearray
+ The bytearray containing the whole Harp message
+ """
+ return self._frame
+
+ @property
+ def message_type(self) -> MessageType:
+ """
+ The message type.
+
+ Returns
+ -------
+ MessageType
+ The message type
+ """
+ return MessageType(self._frame[0])
+
+ @property
+ def length(self) -> int:
+ """
+ The length parameter of the Harp message.
+
+ Returns
+ -------
+ int
+ The length parameter of the Harp message
+ """
+ return self._frame[1]
+
+ @property
+ def address(self) -> int:
+ """
+ The address of the register to which the Harp message refers to.
+
+ Returns
+ -------
+ int
+ The address of the register to which the Harp message refers to
+ """
+ return self._frame[2]
+
+ @property
+ def port(self) -> int:
+ """
+ Indicates the origin or destination of the Harp message in case the device is a hub of Harp devices. The value 255 points to the device itself (default value).
+
+ Returns
+ -------
+ int
+ The port value
+ """
+ return self._frame[3]
+
+ @port.setter
+ def port(self, value: int) -> None:
+ """
+ Sets the port value.
+
+ Parameters
+ ----------
+ value : int
+ The port value to set
+ """
+ self._port = value
+
+ @property
+ def payload_type(self) -> PayloadType:
+ """
+ The payload type.
+
+ Returns
+ -------
+ PayloadType
+ The payload type
+ """
+ return PayloadType(self._frame[4])
+
+ @property
+ def payload(self) -> Union[int, list[int], bytearray, float, list[float]]:
+ """
+ The payload sent in the write Harp message.
+
+ Returns
+ -------
+ Union[int, list[int]]
+ The payload sent in the write Harp message
+ """
+ payload_start = self.BASE_LENGTH
+ if self.payload_type & PayloadType.Timestamp:
+ payload_start += 6
+
+ payload_index = payload_start + 1
+
+ # length is payload_start + payload type size
+ pt = self.payload_type
+ if pt == PayloadType.U8 or pt == PayloadType.TimestampedU8:
+ if self.length == payload_start + 1:
+ return self._frame[payload_index]
+ else: # array case
+ return [
+ int.from_bytes([self._frame[i]], byteorder="little")
+ for i in range(payload_index, self.length + 1)
+ ]
+
+ elif pt == PayloadType.S8 or pt == PayloadType.TimestampedS8:
+ if self.length == payload_start + 1:
+ return int.from_bytes(
+ [self._frame[payload_index]], byteorder="little", signed=True
+ )
+ else: # array case
+ return [
+ int.from_bytes(
+ [self._frame[i]],
+ byteorder="little",
+ signed=True,
+ )
+ for i in range(payload_index, self.length + 1)
+ ]
+
+ elif pt == PayloadType.U16 or pt == PayloadType.TimestampedU16:
+ if self.length == payload_start + 2:
+ return int.from_bytes(
+ self._frame[payload_index : payload_index + 2],
+ byteorder="little",
+ signed=False,
+ )
+ else: # array case
+ return [
+ int.from_bytes(
+ self._frame[i : i + 2],
+ byteorder="little",
+ signed=False,
+ )
+ for i in range(payload_index, self.length + 1, 2)
+ ]
+
+ elif pt == PayloadType.S16 or pt == PayloadType.TimestampedS16:
+ if self.length == payload_start + 2:
+ return int.from_bytes(
+ self._frame[payload_index : payload_index + 2],
+ byteorder="little",
+ signed=True,
+ )
+ else:
+ return [
+ int.from_bytes(
+ self._frame[i : i + 2],
+ byteorder="little",
+ signed=True,
+ )
+ for i in range(payload_index, self.length + 1, 2)
+ ]
+
+ elif pt == PayloadType.U32 or pt == PayloadType.TimestampedU32:
+ if self.length == payload_start + 4:
+ return int.from_bytes(
+ self._frame[payload_index : payload_index + 4],
+ byteorder="little",
+ signed=False,
+ )
+ else:
+ return [
+ int.from_bytes(
+ self._frame[i : i + 4],
+ byteorder="little",
+ signed=False,
+ )
+ for i in range(payload_index, self.length + 1, 4)
+ ]
+
+ elif pt == PayloadType.S32 or pt == PayloadType.TimestampedS32:
+ if self.length == payload_start + 4:
+ return int.from_bytes(
+ self._frame[payload_index : payload_index + 4],
+ byteorder="little",
+ signed=True,
+ )
+ else:
+ return [
+ int.from_bytes(
+ self._frame[i : i + 4],
+ byteorder="little",
+ signed=True,
+ )
+ for i in range(payload_index, self.length + 1, 4)
+ ]
+
+ elif pt == PayloadType.U64 or pt == PayloadType.TimestampedU64:
+ if self.length == payload_start + 8:
+ return int.from_bytes(
+ self._frame[payload_index : payload_index + 8],
+ byteorder="little",
+ signed=False,
+ )
+ else:
+ return [
+ int.from_bytes(
+ self._frame[i : i + 8],
+ byteorder="little",
+ signed=False,
+ )
+ for i in range(payload_index, self.length + 1, 8)
+ ]
+
+ elif pt == PayloadType.S64 or pt == PayloadType.TimestampedS64:
+ if self.length == payload_start + 8:
+ return int.from_bytes(
+ self._frame[payload_index : payload_index + 8],
+ byteorder="little",
+ signed=True,
+ )
+ else:
+ return [
+ int.from_bytes(
+ self._frame[i : i + 8],
+ byteorder="little",
+ signed=True,
+ )
+ for i in range(payload_index, self.length + 1, 8)
+ ]
+
+ elif pt == PayloadType.Float or pt == PayloadType.TimestampedFloat:
+ if self.length == payload_start + 4:
+ return struct.unpack(
+ " int:
+ """
+ The sum of all bytes contained in the Harp message.
+
+ Returns
+ -------
+ int
+ The sum of all bytes contained in the Harp message
+ """
+ return self._frame[-1]
+
+ @staticmethod
+ def parse(frame: bytearray) -> ReplyHarpMessage:
+ """
+ Parses a bytearray to a (reply) Harp message.
+
+ Parameters
+ ----------
+ frame : bytearray
+ The bytearray will be parsed into a (reply) Harp message
+
+ Returns
+ -------
+ ReplyHarpMessage
+ The Harp message object parsed from the original bytearray
+ """
+ return ReplyHarpMessage(frame)
+
+ @staticmethod
+ def create(
+ message_type: MessageType,
+ address: int,
+ payload_type: PayloadType,
+ value: Optional[int | list[int] | float | list[float]] = None,
+ ) -> HarpMessage:
+ """
+ Creates a Harp message.
+
+ Parameters
+ ----------
+ message_type : MessageType
+ The message type. It can only be of type READ or WRITE
+ address : int
+ The address of the register that the message will interact with
+ payload_type : PayloadType
+ The payload type
+ value: int | list[int] | float | list[float], optional
+ The payload of the message. If message_type == MessageType.WRITE, the value cannot be None
+ """
+ if message_type == MessageType.READ:
+ return ReadHarpMessage(payload_type, address)
+ elif message_type == MessageType.WRITE and value is not None:
+ return WriteHarpMessage(payload_type, address, value)
+ elif message_type != MessageType.READ and message_type != MessageType.WRITE:
+ raise Exception(
+ "The only valid message types are MessageType.READ and MessageType.Write!"
+ )
+ else:
+ raise Exception(
+ "The value cannot be None if the message type is equal to MessageType.WRITE!"
+ )
+
+ def __repr__(self) -> str:
+ """
+ Prints debug representation of the reply message.
+
+ Returns
+ -------
+ str
+ The debug representation of the reply message
+ """
+ return self.__str__() + f"\r\nRaw Frame: {self.frame}"
+
+ def __str__(self) -> str:
+ """
+ Prints friendly representation of a Harp message.
+
+ Returns
+ -------
+ str
+ The representation of the Harp message
+ """
+ payload_str = ""
+ format_str = ""
+ if self.payload_type in [PayloadType.Float, PayloadType.TimestampedFloat]:
+ format_str = ".6f"
+ else:
+ bytes_per_word = self.payload_type & 0x07
+ format_str = f"0{bytes_per_word}b"
+
+ payload_str = "".join(
+ f"{item:{format_str}} "
+ for item in (
+ self.payload if isinstance(self.payload, list) else [self.payload]
+ )
+ )
+
+ # Check if the object has a 'timestamp' property and it's not None
+ timestamp_line = ""
+ if hasattr(self, "timestamp"):
+ ts = getattr(self, "timestamp")
+ if ts is not None:
+ timestamp_line = f"Timestamp: {ts}\r\n"
+
+ return (
+ f"Type: {self.message_type.name}\r\n"
+ + f"Length: {self.length}\r\n"
+ + f"Address: {self.address}\r\n"
+ + f"Port: {self.port}\r\n"
+ + timestamp_line
+ + f"Payload Type: {self.payload_type.name}\r\n"
+ + f"Payload Length: {len(self.payload) if self.payload is list else 1}\r\n"
+ + f"Payload: {payload_str}\r\n"
+ + f"Checksum: {self.checksum}"
+ )
+
+
+class ReplyHarpMessage(HarpMessage):
+ """
+ A response message from a Harp device.
+
+ Attributes
+ ----------
+ payload : Union[int, list[int]]
+ The message payload formatted as the appropriate type
+ timestamp : float
+ The Harp timestamp at which the message was sent
+ """
+
+ def __init__(
+ self,
+ frame: bytearray,
+ ):
+ """
+ Parameters
+ ----------
+ frame : bytearray
+ The Harp message in bytearray format
+ """
+
+ self._frame = frame
+ # Retrieve all content from 11 (where payload starts) until the checksum (not inclusive)
+ self._raw_payload = frame[11:-1]
+
+ # Assign timestamp after _payload since @properties all rely on self._payload.
+ self._timestamp = (
+ int.from_bytes(frame[5:9], byteorder="little", signed=False)
+ + int.from_bytes(frame[9:11], byteorder="little", signed=False) * 32e-6
+ )
+
+ # Timestamp is junk if it's not present.
+ if not (self.payload_type & PayloadType.Timestamp):
+ raise HarpReadException(self.address)
+
+ @property
+ def is_error(self) -> bool:
+ """
+ Indicates if this HarpMessage is an error message or not.
+
+ Returns
+ -------
+ bool
+ Returns True if this HarpMessage is an error message, False otherwise.
+ """
+ return self.message_type in [MessageType.READ_ERROR, MessageType.WRITE_ERROR]
+
+ @property
+ def timestamp(self) -> float:
+ """
+ The Harp timestamp at which the message was sent.
+
+ Returns
+ -------
+ float
+ The Harp timestamp at which the message was sent
+ """
+ return self._timestamp
+
+ def payload_as_string(self) -> str:
+ """
+ Returns the payload as a str.
+
+ Returns
+ -------
+ str
+ The payload parsed as a str
+ """
+ return self._raw_payload.decode("utf-8").rstrip("\x00")
+
+
+class ReadHarpMessage(HarpMessage):
+ """
+ A read Harp message sent to a Harp device.
+ """
+
+ MESSAGE_TYPE: int = MessageType.READ
+
+ def __init__(self, payload_type: PayloadType, address: int):
+ self._frame = bytearray()
+
+ self._frame.append(self.MESSAGE_TYPE)
+
+ length: int = 4
+ self._frame.append(length)
+ self._frame.append(address)
+ self._frame.append(self._port)
+ self._frame.append(payload_type)
+ self._frame.append(self.calculate_checksum())
+
+
+class WriteHarpMessage(HarpMessage):
+ """
+ A write Harp message sent to a Harp device.
+
+ Attributes
+ ----------
+ payload : Union[int, list[int]]
+ The payload sent in the write Harp message
+ """
+
+ MESSAGE_TYPE: int = MessageType.WRITE
+
+ # Define payload type properties
+ _PAYLOAD_CONFIG = {
+ # payload_type: (byte_size, signed, is_float)
+ PayloadType.U8: (1, False),
+ PayloadType.S8: (1, True),
+ PayloadType.U16: (2, False),
+ PayloadType.S16: (2, True),
+ PayloadType.U32: (4, False),
+ PayloadType.S32: (4, True),
+ PayloadType.U64: (8, False),
+ PayloadType.S64: (8, True),
+ PayloadType.Float: (4, False),
+ }
+
+ def __init__(
+ self,
+ payload_type: PayloadType,
+ address: int,
+ value: int | float | list[int] | list[float],
+ ):
+ """
+ Create a WriteHarpMessage to send to a device.
+
+ Parameters
+ ----------
+ payload_type : PayloadType
+ Type of payload (U8, S8, U16, etc.)
+ address : int
+ Register address to write to
+ value : int, float, List[int], or List[float], optional
+ Value(s) to write - can be a single value or list of values
+
+ Note
+ -----
+ The message frame is constructed according to the HARP binary protocol.
+ The length is calculated as BASE_LENGTH + payload size in bytes.
+ """
+
+ self._frame = bytearray()
+
+ # Get configuration for this payload type
+ byte_size, signed = self._PAYLOAD_CONFIG.get(payload_type, (1, False))
+
+ # Convert value to payload bytes
+ payload = bytearray()
+
+ if isinstance(value, int) or isinstance(value, float):
+ values = [value]
+ else:
+ values = value
+
+ for val in values:
+ if isinstance(val, float):
+ payload += struct.pack("=3.9,<4.0"
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.hatch.build.targets.wheel]
+include = [
+ "harp",
+ "harp/**/*",
+]
diff --git a/src/harp-serial/README.md b/src/harp-serial/README.md
new file mode 100644
index 0000000..d41a8f9
--- /dev/null
+++ b/src/harp-serial/README.md
@@ -0,0 +1,75 @@
+# harp-serial
+
+[](https://badge.fury.io/py/harp-serial)
+
+A Python library for communicating with Harp devices over serial connections.
+
+## Installation
+
+```bash
+uv add harp-serial
+# or
+pip install harp-serial
+```
+
+## Quick Start
+
+```python
+from harp.protocol import MessageType, PayloadType
+from harp.protocol.messages import HarpMessage
+from harp.serial.device import Device
+
+# Connect to a device
+device = Device("/dev/ttyUSB0")
+#device = Device("COM3") # for Windows
+
+# Get device information
+device.info()
+
+# define register_address
+register_address = 32
+
+# Read from register
+reply = device.send(HarpMessage.create(MessageType.READ, register_address, PayloadType.U8))
+
+# Write to register
+device.send(HarpMessage.create(MessageType.WRITE, register_address, PayloadType.U8, reply.payload))
+
+# Disconnect when done
+device.disconnect()
+```
+
+or using the `with` statement:
+
+```python
+from harp.protocol import MessageType, PayloadType
+from harp.protocol.messages import HarpMessage
+from harp.serial.device import Device
+
+with Device("/dev/ttyUSB0") as device:
+ # Get device information
+ device.info()
+
+ # define register_address
+ register_address = 32
+
+ # Read from register
+ reply = device.send(HarpMessage.create(MessageType.READ, register_address, PayloadType.U8))
+
+ # Write to register
+ device.send(HarpMessage.create(MessageType.WRITE, register_address, PayloadType.U8, reply.payload))
+```
+
+## for Linux
+
+### Install UDEV Rules
+
+Install by either copying `10-harp.rules` over to your `/etc/udev/rules.d` folder or by symlinking it with:
+````
+sudo ln -s /absolute/path/to/10-harp.rules /etc/udev/rules.d/10-harp.rules
+````
+
+Then reload udev rules with
+````
+sudo udevadm control --reload-rules
+````
diff --git a/src/harp-serial/harp/serial/__init__.py b/src/harp-serial/harp/serial/__init__.py
new file mode 100644
index 0000000..811fc78
--- /dev/null
+++ b/src/harp-serial/harp/serial/__init__.py
@@ -0,0 +1,3 @@
+from .device import Device as Device
+from .device import TimeoutStrategy as TimeoutStrategy
+from .harp_serial import HarpSerial as HarpSerial
diff --git a/src/harp-serial/harp/serial/device.py b/src/harp-serial/harp/serial/device.py
new file mode 100644
index 0000000..c680687
--- /dev/null
+++ b/src/harp-serial/harp/serial/device.py
@@ -0,0 +1,1485 @@
+from __future__ import annotations # enable subscriptable type hints for lists.
+
+import logging
+import queue
+from enum import Enum
+from io import BufferedWriter
+from pathlib import Path
+from typing import Optional
+
+import serial
+from harp.protocol import (
+ ClockConfig,
+ CommonRegisters,
+ MessageType,
+ OperationCtrl,
+ OperationMode,
+ PayloadType,
+ ResetMode,
+)
+from harp.protocol.device_names import device_names
+from harp.protocol.exceptions import HarpTimeoutError
+from harp.protocol.messages import HarpMessage, ReplyHarpMessage
+from harp.serial.harp_serial import HarpSerial
+
+
+class TimeoutStrategy(Enum):
+ """
+ Strategy to handle timeouts when waiting for a reply from the device.
+
+ Attributes
+ ----------
+ RAISE : str
+ Raise HarpTimeoutError
+ RETURN_NONE : str
+ Return None
+ LOG_AND_RAISE : str
+ Log the timeout and raise HarpTimeoutError
+ LOG_AND_NONE : str
+ Log the timeout and return None
+ """
+
+ RAISE = "raise" # Raise HarpTimeoutError
+ RETURN_NONE = "return_none" # Return None
+ LOG_AND_RAISE = "log_and_raise"
+ LOG_AND_NONE = "log_and_none"
+
+
+class Device:
+ """
+ The `Device` class provides the interface for interacting with Harp devices. This implementation of the Harp device was based on the official documentation available on the [harp-tech website](https://harp-tech.org/protocol/Device.html).
+
+ Attributes
+ ----------
+ WHO_AM_I : int
+ The device ID number. A list of devices can be found [here](https://github.com/harp-tech/protocol/blob/main/whoami.md)
+ DEFAULT_DEVICE_NAME : str
+ The device name, i.e. "Behavior". This name is derived by cross-referencing the `WHO_AM_I` identifier with the corresponding device name in the `device_names` dictionary
+ HW_VERSION_H : int
+ The major hardware version
+ HW_VERSION_L : int
+ The minor hardware version
+ ASSEMBLY_VERSION : int
+ The version of the assembled components
+ HARP_VERSION_H : int
+ The major Harp core version
+ HARP_VERSION_L : int
+ The minor Harp core version
+ FIRMWARE_VERSION_H : int
+ The major firmware version
+ FIRMWARE_VERSION_L : int
+ The minor firmware version
+ DEVICE_NAME : str
+ The device name stored in the Harp device
+ SERIAL_NUMBER : int, optional
+ The serial number of the device
+ """
+
+ WHO_AM_I: int
+ DEFAULT_DEVICE_NAME: str
+ HW_VERSION_H: int
+ HW_VERSION_L: int
+ ASSEMBLY_VERSION: int
+ HARP_VERSION_H: int
+ HARP_VERSION_L: int
+ FIRMWARE_VERSION_H: int
+ FIRMWARE_VERSION_L: int
+ DEVICE_NAME: str
+ SERIAL_NUMBER: int
+ CLOCK_CONFIG: int
+ TIMESTAMP_OFFSET: int
+
+ _ser: HarpSerial
+ _dump_file_path: Optional[Path]
+ _dump_file: Optional[BufferedWriter] = None
+ _read_timeout_s: float
+
+ _TIMEOUT_S: float = 1.0
+
+ def __init__(
+ self,
+ serial_port: str,
+ dump_file_path: Optional[str] = None,
+ read_timeout_s: float = 1,
+ timeout_strategy: TimeoutStrategy = TimeoutStrategy.RAISE,
+ ):
+ """
+ Parameters
+ ----------
+ serial_port : str
+ The serial port used to establish the connection with the Harp device. It must be denoted as `/dev/ttyUSBx` in Linux and `COMx` in Windows, where `x` is the number of the serial port
+ dump_file_path: str, optional
+ The binary file to which all Harp messages will be written
+ read_timeout_s: float, optional
+ _TODO_
+ """
+ self.log = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
+ self._serial_port = serial_port
+ self._dump_file_path = None
+ if dump_file_path is not None:
+ self._dump_file_path = Path() / dump_file_path
+ self._read_timeout_s = read_timeout_s
+ self._timeout_strategy = timeout_strategy
+
+ # Connect to the Harp device and load the data stored in the device's common registers
+ self.connect()
+ self.load()
+
+ def load(self) -> None:
+ """
+ Loads the data stored in the device's common registers.
+ """
+ self.WHO_AM_I = self._read_who_am_i()
+ self.DEFAULT_DEVICE_NAME = self._read_default_device_name()
+ self.HW_VERSION_H = self._read_hw_version_h()
+ self.HW_VERSION_L = self._read_hw_version_l()
+ self.ASSEMBLY_VERSION = self._read_assembly_version()
+ self.HARP_VERSION_H = self._read_harp_version_h()
+ self.HARP_VERSION_L = self._read_harp_version_l()
+ self.FIRMWARE_VERSION_H = self._read_fw_version_h()
+ self.FIRMWARE_VERSION_L = self._read_fw_version_l()
+ self.DEVICE_NAME = self._read_device_name()
+ self.SERIAL_NUMBER = self._read_serial_number()
+ self.CLOCK_CONFIG = self._read_clock_config()
+ self.TIMESTAMP_OFFSET = self._read_timestamp_offset()
+
+ def info(self) -> None:
+ """
+ Prints the device information.
+ """
+ print("Device info:")
+ print(f"* Who am I: ({self.WHO_AM_I}) {self.DEFAULT_DEVICE_NAME}")
+ print(f"* HW version: {self.HW_VERSION_H}.{self.HW_VERSION_L}")
+ print(f"* Assembly version: {self.ASSEMBLY_VERSION}")
+ print(f"* HARP version: {self.HARP_VERSION_H}.{self.HARP_VERSION_L}")
+ print(
+ f"* Firmware version: {self.FIRMWARE_VERSION_H}.{self.FIRMWARE_VERSION_L}"
+ )
+ print(f"* Device user name: {self.DEVICE_NAME}")
+ print(f"* Serial number: {self.SERIAL_NUMBER}")
+ print(f"* Mode: {self._read_device_mode().name}")
+
+ def connect(self) -> None:
+ """
+ Connects to the Harp device.
+ """
+ self._ser = HarpSerial(
+ self._serial_port, # "/dev/tty.usbserial-A106C8O9"
+ baudrate=1000000,
+ timeout=self._TIMEOUT_S,
+ parity=serial.PARITY_NONE,
+ stopbits=1,
+ bytesize=8,
+ rtscts=True,
+ )
+
+ # open file if it is defined
+ if self._dump_file_path is not None:
+ self._dump_file = open(self._dump_file_path, "ab")
+
+ def disconnect(self) -> None:
+ """
+ Disconnects from the Harp device.
+ """
+ # close file if it exists
+ if self._dump_file:
+ self._dump_file.close()
+ self._dump_file = None
+
+ self._ser.close()
+
+ def _read_device_mode(self) -> OperationMode:
+ """
+ Reads the current operation mode of the Harp device.
+
+ Returns
+ -------
+ DeviceMode
+ The current device mode
+ """
+ address = CommonRegisters.OPERATION_CTRL
+ reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8))
+ return OperationMode(reply.payload & OperationCtrl.OP_MODE)
+
+ def dump_registers(self) -> list:
+ """
+ Asserts the DUMP bit to dump the values of all core and app registers
+ as Harp Read Reply Messages. More information on the DUMP bit can be found [here](https://harp-tech.org/protocol/Device.html#r_operation_ctrl-u16--operation-mode-configuration).
+
+ Returns
+ -------
+ list
+ The list containing the reply Harp messages for all the device's registers
+ """
+ address = CommonRegisters.OPERATION_CTRL
+ reg_value = self.send(
+ HarpMessage.create(MessageType.READ, address, PayloadType.U8)
+ )
+
+ if reg_value is None:
+ return []
+
+ reg_value = reg_value.payload
+
+ # Assert DUMP bit
+ reg_value |= OperationCtrl.DUMP
+ self.send(
+ HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, reg_value)
+ )
+
+ # Receive the contents of all registers as Harp Read Reply Messages
+ replies = []
+ while True:
+ msg = self._read()
+ if msg is not None:
+ replies.append(msg)
+ else:
+ break
+ return replies
+
+ def read_operation_ctrl(self):
+ """
+ Reads the OPERATION_CTRL register of the device.
+
+ Returns
+ -------
+ ReplyHarpMessage
+ The reply to the Harp message
+ """
+ address = CommonRegisters.OPERATION_CTRL
+ reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8))
+
+ # create dict with complete byte and then decode each bit according to the OperationCtrl entries
+ if reply is not None:
+ reg_value = reply.payload
+ result = {
+ "REG_VALUE": reply.payload,
+ "OP_MODE": OperationMode(reg_value & OperationCtrl.OP_MODE),
+ "DUMP": bool(reg_value & OperationCtrl.DUMP),
+ "MUTE_RPL": bool(reg_value & OperationCtrl.MUTE_RPL),
+ "VISUALEN": bool(reg_value & OperationCtrl.VISUALEN),
+ "OPLEDEN": bool(reg_value & OperationCtrl.OPLEDEN),
+ "ALIVE_EN": bool(reg_value & OperationCtrl.ALIVE_EN),
+ }
+ return result
+
+ def write_operation_ctrl(
+ self,
+ mode: Optional[OperationMode] = None,
+ mute_rpl: Optional[bool] = None,
+ visual_en: Optional[bool] = None,
+ op_led_en: Optional[bool] = None,
+ alive_en: Optional[bool] = None,
+ ) -> ReplyHarpMessage | None:
+ """
+ Writes the OPERATION_CTRL register of the device.
+
+ Parameters
+ ----------
+ mode : OperationMode, optional
+ The new operation mode value
+ mute_rpl : bool, optional
+ If True, the Replies to all the Commands are muted
+ visual_en : bool, optional
+ If True, enables the status led
+ op_led_en : bool, optional
+ If True, enables the operation LED
+ alive_en : bool, optional
+ If True, enables the ALIVE_EN bit
+ Returns
+ -------
+ ReplyHarpMessage
+ The reply to the Harp message
+ """
+ address = CommonRegisters.OPERATION_CTRL
+
+ # Read register first
+ reg_value = self.send(
+ HarpMessage.create(MessageType.READ, address, PayloadType.U8)
+ )
+
+ if reg_value is None:
+ return reg_value
+
+ reg_value = reg_value.payload
+
+ if mode is not None:
+ # Clear old operation mode
+ reg_value &= ~OperationCtrl.OP_MODE
+ # Set new operation mode
+ reg_value |= mode
+
+ if mute_rpl is not None:
+ if mute_rpl:
+ reg_value |= OperationCtrl.MUTE_RPL
+ else:
+ reg_value &= ~OperationCtrl.MUTE_RPL
+
+ if visual_en is not None:
+ if visual_en:
+ reg_value |= OperationCtrl.VISUALEN
+ else:
+ reg_value &= ~OperationCtrl.VISUALEN
+
+ if op_led_en is not None:
+ if op_led_en:
+ reg_value |= OperationCtrl.OPLEDEN
+ else:
+ reg_value &= ~OperationCtrl.OPLEDEN
+
+ if alive_en is not None:
+ if alive_en:
+ reg_value |= OperationCtrl.ALIVE_EN
+ else:
+ reg_value &= ~OperationCtrl.ALIVE_EN
+
+ reply = self.send(
+ HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, reg_value)
+ )
+
+ return reply
+
+ def set_mode(self, mode: OperationMode) -> ReplyHarpMessage | None:
+ """
+ Sets the operation mode of the device.
+
+ Parameters
+ ----------
+ mode : DeviceMode
+ The new device mode value
+
+ Returns
+ -------
+ ReplyHarpMessage
+ The reply to the Harp message
+ """
+ address = CommonRegisters.OPERATION_CTRL
+
+ # Read register first
+ reg_value = self.send(
+ HarpMessage.create(MessageType.READ, address, PayloadType.U8)
+ )
+
+ if reg_value is None:
+ return reg_value
+
+ reg_value = reg_value.payload
+
+ # Clear old operation mode
+ reg_value &= ~OperationCtrl.OP_MODE
+
+ # Set new operation mode
+ reg_value |= mode
+ reply = self.send(
+ HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, reg_value)
+ )
+
+ return reply
+
+ def alive_en(self, enable: bool) -> bool:
+ """
+ Sets the ALIVE_EN bit of the device.
+
+ Parameters
+ ----------
+ enable : bool
+ If True, enables the ALIVE_EN bit. If False, disables it
+
+ Returns
+ -------
+ bool
+ True if the operation was successful, False otherwise
+ """
+ address = CommonRegisters.OPERATION_CTRL
+
+ # Read register first
+ reg_value = self.send(
+ HarpMessage.create(MessageType.READ, address, PayloadType.U8)
+ )
+
+ if reg_value is None:
+ return False
+
+ reg_value = reg_value.payload
+
+ if enable:
+ reg_value |= OperationCtrl.ALIVE_EN
+ else:
+ reg_value &= ~OperationCtrl.ALIVE_EN
+
+ reply = self.send(
+ HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, reg_value)
+ )
+
+ if reply is None:
+ return False
+
+ return reply is not None
+
+ def op_led_en(self, enable: bool) -> bool:
+ """
+ Sets the operation LED of the device.
+
+ Parameters
+ ----------
+ enable : bool
+ If True, enables the operation LED. If False, disables it
+
+ Returns
+ -------
+ bool
+ True if the operation was successful, False otherwise
+ """
+ address = CommonRegisters.OPERATION_CTRL
+
+ # Read register first
+ reg_value = self.send(
+ HarpMessage.create(MessageType.READ, address, PayloadType.U8)
+ )
+
+ if reg_value is None:
+ return False
+
+ reg_value = reg_value.payload
+
+ if enable:
+ reg_value |= OperationCtrl.OPLEDEN
+ else:
+ reg_value &= ~OperationCtrl.OPLEDEN
+
+ reply = self.send(
+ HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, reg_value)
+ )
+
+ return reply is not None
+
+ def visual_en(self, enable: bool) -> bool:
+ """
+ Sets the status led of the device.
+
+ Parameters
+ ----------
+ enable : bool
+ If True, enables the status led. If False, disables it
+
+ Returns
+ -------
+ bool
+ True if the operation was successful, False otherwise
+ """
+ address = CommonRegisters.OPERATION_CTRL
+
+ # Read register first
+ reg_value = self.send(
+ HarpMessage.create(MessageType.READ, address, PayloadType.U8)
+ )
+
+ if reg_value is None:
+ return False
+
+ reg_value = reg_value.payload
+
+ if enable:
+ reg_value |= OperationCtrl.VISUALEN
+ else:
+ reg_value &= ~OperationCtrl.VISUALEN
+
+ reply = self.send(
+ HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, reg_value)
+ )
+
+ return reply is not None
+
+ def mute_reply(self, enable: bool) -> bool:
+ """
+ Sets the MUTE_REPLY bit of the device.
+
+ Parameters
+ ----------
+ enable : bool
+ If True, the Replies to all the Commands are muted. If False, un-mutes them
+
+ Returns
+ -------
+ bool
+ True if the operation was successful, False otherwise
+ """
+ address = CommonRegisters.OPERATION_CTRL
+
+ # Read register first
+ reg_value = self.send(
+ HarpMessage.create(MessageType.READ, address, PayloadType.U8)
+ )
+
+ if reg_value is None:
+ return False
+
+ reg_value = reg_value.payload
+
+ if enable:
+ reg_value |= OperationCtrl.MUTE_RPL
+ else:
+ reg_value &= ~OperationCtrl.MUTE_RPL
+
+ reply = self.send(
+ HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, reg_value)
+ )
+
+ return reply is not None
+
+ def reset_device(
+ self, reset_mode: ResetMode = ResetMode.RST_DEF
+ ) -> ReplyHarpMessage | None:
+ """
+ Resets the device and reboots with all the registers with the default values. Beware that the EEPROM will be erased. More information on the reset device register can be found [here](https://harp-tech.org/protocol/Device.html#r_reset_dev-u8--reset-device-and-save-non-volatile-registers).
+
+ Returns
+ -------
+ ReplyHarpMessage
+ The reply to the Harp message
+ """
+ address = CommonRegisters.RESET_DEV
+ reply = self.send(
+ HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, reset_mode)
+ )
+
+ return reply
+
+ def set_clock_config(self, clock_config: ClockConfig) -> ReplyHarpMessage | None:
+ """
+ Sets the clock configuration of the device.
+
+ Parameters
+ ----------
+ clock_config : ClockConfig
+ The clock configuration value
+
+ Returns
+ -------
+ ReplyHarpMessage
+ The reply to the Harp message
+ """
+ address = CommonRegisters.CLOCK_CONFIG
+ reply = self.send(
+ HarpMessage.create(MessageType.WRITE, address, PayloadType.U8, clock_config)
+ )
+
+ return reply
+
+ def set_timestamp_offset(self, timestamp_offset: int) -> ReplyHarpMessage | None:
+ """
+ When the value of this register is above 0 (zero), the device's timestamp will be offset by this amount. The register is sensitive to 500 microsecond increments. This register is non-volatile.
+
+ Parameters
+ ----------
+ timestamp_offset : int
+ The timestamp offset value
+
+ Returns
+ -------
+ ReplyHarpMessage
+ The reply to the Harp message
+ """
+ address = CommonRegisters.TIMESTAMP_OFFSET
+ reply = self.send(
+ HarpMessage.create(
+ MessageType.WRITE, address, PayloadType.U8, timestamp_offset
+ )
+ )
+
+ return reply
+
+ def send(
+ self,
+ message: HarpMessage,
+ *,
+ expect_reply: bool = True,
+ timeout_strategy: TimeoutStrategy | None = None,
+ ) -> ReplyHarpMessage | None:
+ """
+ Sends a Harp message and (optionally) waits for a reply.
+
+ Parameters
+ ----------
+ message : HarpMessage
+ The HarpMessage to be sent to the device
+ expect_reply : bool, optional
+ If False, do not wait for a reply (fire-and-forget)
+ timeout_strategy : TimeoutStrategy | None
+ Override the device-level timeout strategy for this call
+
+ Returns
+ -------
+ ReplyHarpMessage | None
+ Reply (or None when allowed by the timeout strategy or expect_reply=False)
+
+ Raises
+ -------
+ HarpTimeoutError
+ If no reply is received and the effective strategy requires raising
+ """
+ self._ser.write(message.frame)
+
+ if not expect_reply:
+ return None
+
+ strategy = timeout_strategy or self._timeout_strategy
+
+ try:
+ reply = self._read()
+ except TimeoutError:
+ hte = HarpTimeoutError(self._read_timeout_s)
+ if strategy in (
+ TimeoutStrategy.LOG_AND_RAISE,
+ TimeoutStrategy.LOG_AND_NONE,
+ ):
+ self.log.warning(str(hte))
+ if strategy in (TimeoutStrategy.RAISE, TimeoutStrategy.LOG_AND_RAISE):
+ raise hte
+ return None
+
+ self._dump_reply(reply.frame)
+ return reply
+
+ def _read(self) -> ReplyHarpMessage:
+ """
+ Reads an incoming serial message in a blocking way.
+
+ Returns
+ -------
+ ReplyHarpMessage
+ The incoming Harp message in case it exists
+
+ Raises
+ -------
+ TimeoutError
+ If no reply is received within the timeout period
+ """
+ try:
+ return self._ser.msg_q.get(block=True, timeout=self._read_timeout_s)
+ except queue.Empty:
+ raise TimeoutError("No reply received within the timeout period.")
+
+ def _dump_reply(self, reply: bytearray):
+ """
+ Dumps the reply to a Harp message in the dump file in case it exists.
+ """
+ if self._dump_file:
+ self._dump_file.write(reply)
+
+ def get_events(self) -> list[ReplyHarpMessage]:
+ """
+ Gets all events from the event queue.
+
+ Returns
+ -------
+ list
+ The list containing every Harp event message that were on the queue
+ """
+ msgs = []
+ while True:
+ try:
+ msgs.append(self._ser.event_q.get(timeout=False))
+ except queue.Empty:
+ break
+ return msgs
+
+ def event_count(self) -> int:
+ """
+ Gets the number of events in the event queue.
+
+ Returns
+ -------
+ int
+ The number of events in the event queue
+ """
+ return self._ser.event_q.qsize()
+
+ def read_u8(self, address: int) -> ReplyHarpMessage | None:
+ """
+ Reads the value of a register of type U8.
+
+ Parameters
+ ----------
+ address : int
+ The register to be read
+
+ Returns
+ -------
+ ReplyHarpMessage
+ The reply to the Harp message that will contain the value read from the register
+
+ Raises
+ ------
+ HarpTimeoutError
+ If no reply is received and the effective strategy requires raising
+ """
+ reply = self.send(
+ HarpMessage.create(
+ message_type=MessageType.READ,
+ address=address,
+ payload_type=PayloadType.U8,
+ )
+ )
+
+ return reply
+
+ def read_s8(self, address: int) -> ReplyHarpMessage | None:
+ """
+ Reads the value of a register of type S8.
+
+ Parameters
+ ----------
+ address : int
+ The register to be read
+
+ Returns
+ -------
+ ReplyHarpMessage
+ The reply to the Harp message that will contain the value read from the register
+
+ Raises
+ ------
+ HarpTimeoutError
+ If no reply is received and the effective strategy requires raising
+ """
+ reply = self.send(
+ HarpMessage.create(
+ message_type=MessageType.READ,
+ address=address,
+ payload_type=PayloadType.S8,
+ )
+ )
+
+ return reply
+
+ def read_u16(self, address: int) -> ReplyHarpMessage | None:
+ """
+ Reads the value of a register of type U16.
+
+ Parameters
+ ----------
+ address : int
+ The register to be read
+
+ Returns
+ -------
+ ReplyHarpMessage
+ The reply to the Harp message that will contain the value read from the register
+
+ Raises
+ ------
+ HarpTimeoutError
+ If no reply is received and the effective strategy requires raising
+ """
+ reply = self.send(
+ HarpMessage.create(
+ message_type=MessageType.READ,
+ address=address,
+ payload_type=PayloadType.U16,
+ )
+ )
+
+ return reply
+
+ def read_s16(self, address: int) -> ReplyHarpMessage | None:
+ """
+ Reads the value of a register of type S16.
+
+ Parameters
+ ----------
+ address : int
+ The register to be read
+
+ Returns
+ -------
+ ReplyHarpMessage
+ The reply to the Harp message that will contain the value read from the register
+
+ Raises
+ ------
+ HarpTimeoutError
+ If no reply is received and the effective strategy requires raising
+ """
+ reply = self.send(
+ HarpMessage.create(
+ message_type=MessageType.READ,
+ address=address,
+ payload_type=PayloadType.S16,
+ )
+ )
+
+ return reply
+
+ def read_u32(self, address: int) -> ReplyHarpMessage | None:
+ """
+ Reads the value of a register of type U32.
+
+ Parameters
+ ----------
+ address : int
+ The register to be read
+
+ Returns
+ -------
+ ReplyHarpMessage
+ The reply to the Harp message that will contain the value read from the register
+
+ Raises
+ ------
+ HarpTimeoutError
+ If no reply is received and the effective strategy requires raising
+ """
+ reply = self.send(
+ HarpMessage.create(
+ message_type=MessageType.READ,
+ address=address,
+ payload_type=PayloadType.U32,
+ )
+ )
+
+ return reply
+
+ def read_s32(self, address: int) -> ReplyHarpMessage | None:
+ """
+ Reads the value of a register of type S32.
+
+ Parameters
+ ----------
+ address : int
+ The register to be read
+
+ Returns
+ -------
+ ReplyHarpMessage
+ The reply to the Harp message that will contain the value read from the register
+
+ Raises
+ ------
+ HarpTimeoutError
+ If no reply is received and the effective strategy requires raising
+ """
+ reply = self.send(
+ HarpMessage.create(
+ message_type=MessageType.READ,
+ address=address,
+ payload_type=PayloadType.S32,
+ )
+ )
+
+ return reply
+
+ def read_u64(self, address: int) -> ReplyHarpMessage | None:
+ """
+ Reads the value of a register of type U64.
+
+ Parameters
+ ----------
+ address : int
+ The register to be read
+
+ Returns
+ -------
+ ReplyHarpMessage
+ The reply to the Harp message that will contain the value read from the register
+
+ Raises
+ ------
+ HarpTimeoutError
+ If no reply is received and the effective strategy requires raising
+ """
+ reply = self.send(
+ HarpMessage.create(
+ message_type=MessageType.READ,
+ address=address,
+ payload_type=PayloadType.U64,
+ )
+ )
+
+ return reply
+
+ def read_s64(self, address: int) -> ReplyHarpMessage | None:
+ """
+ Reads the value of a register of type S64.
+
+ Parameters
+ ----------
+ address : int
+ The register to be read
+
+ Returns
+ -------
+ ReplyHarpMessage
+ The reply to the Harp message that will contain the value read from the register
+
+ Raises
+ ------
+ HarpTimeoutError
+ If no reply is received and the effective strategy requires raising
+ """
+ reply = self.send(
+ HarpMessage.create(
+ message_type=MessageType.READ,
+ address=address,
+ payload_type=PayloadType.S64,
+ )
+ )
+
+ return reply
+
+ def read_float(self, address: int) -> ReplyHarpMessage | None:
+ """
+ Reads the value of a register of type Float.
+
+ Parameters
+ ----------
+ address : int
+ The register to be read
+
+ Returns
+ -------
+ ReplyHarpMessage
+ The reply to the Harp message that will contain the value read from the register
+
+ Raises
+ ------
+ HarpTimeoutError
+ If no reply is received and the effective strategy requires raising
+ """
+ reply = self.send(
+ HarpMessage.create(
+ message_type=MessageType.READ,
+ address=address,
+ payload_type=PayloadType.Float,
+ )
+ )
+
+ return reply
+
+ def write_u8(self, address: int, value: int | list[int]) -> ReplyHarpMessage | None:
+ """
+ Writes the value of a register of type U8.
+
+ Parameters
+ ----------
+ address : int
+ The register to be written on
+ value: int | list[int]
+ The value to be written to the register
+
+ Returns
+ -------
+ ReplyHarpMessage
+ The reply to the Harp message
+
+ Raises
+ ------
+ HarpTimeoutError
+ If no reply is received and the effective strategy requires raising
+ """
+ reply = self.send(
+ HarpMessage.create(
+ message_type=MessageType.WRITE,
+ address=address,
+ payload_type=PayloadType.U8,
+ value=value,
+ )
+ )
+
+ return reply
+
+ def write_s8(self, address: int, value: int | list[int]) -> ReplyHarpMessage | None:
+ """
+ Writes the value of a register of type S8.
+
+ Parameters
+ ----------
+ address : int
+ The register to be written on
+ value: int | list[int]
+ The value to be written to the register
+
+ Returns
+ -------
+ ReplyHarpMessage
+ The reply to the Harp message
+
+ Raises
+ ------
+ HarpTimeoutError
+ If no reply is received and the effective strategy requires raising
+ """
+ reply = self.send(
+ HarpMessage.create(
+ message_type=MessageType.WRITE,
+ address=address,
+ payload_type=PayloadType.S8,
+ value=value,
+ )
+ )
+
+ return reply
+
+ def write_u16(
+ self, address: int, value: int | list[int]
+ ) -> ReplyHarpMessage | None:
+ """
+ Writes the value of a register of type U16.
+
+ Parameters
+ ----------
+ address : int
+ The register to be written on
+ value: int | list[int]
+ The value to be written to the register
+
+ Returns
+ -------
+ ReplyHarpMessage
+ The reply to the Harp message
+
+ Raises
+ ------
+ HarpTimeoutError
+ If no reply is received and the effective strategy requires raising
+ """
+ reply = self.send(
+ HarpMessage.create(
+ message_type=MessageType.WRITE,
+ address=address,
+ payload_type=PayloadType.U16,
+ value=value,
+ )
+ )
+
+ return reply
+
+ def write_s16(
+ self, address: int, value: int | list[int]
+ ) -> ReplyHarpMessage | None:
+ """
+ Writes the value of a register of type S16.
+
+ Parameters
+ ----------
+ address : int
+ The register to be written on
+ value: int | list[int]
+ The value to be written to the register
+
+ Returns
+ -------
+ ReplyHarpMessage
+ The reply to the Harp message
+
+ Raises
+ ------
+ HarpTimeoutError
+ If no reply is received and the effective strategy requires raising
+ """
+ reply = self.send(
+ HarpMessage.create(
+ message_type=MessageType.WRITE,
+ address=address,
+ payload_type=PayloadType.S16,
+ value=value,
+ )
+ )
+
+ return reply
+
+ def write_u32(
+ self, address: int, value: int | list[int]
+ ) -> ReplyHarpMessage | None:
+ """
+ Writes the value of a register of type U32.
+
+ Parameters
+ ----------
+ address : int
+ The register to be written on
+ value: int | list[int]
+ The value to be written to the register
+
+ Returns
+ -------
+ ReplyHarpMessage
+ The reply to the Harp message
+
+ Raises
+ ------
+ HarpTimeoutError
+ If no reply is received and the effective strategy requires raising
+ """
+ reply = self.send(
+ HarpMessage.create(
+ message_type=MessageType.WRITE,
+ address=address,
+ payload_type=PayloadType.U32,
+ value=value,
+ )
+ )
+
+ return reply
+
+ def write_s32(
+ self, address: int, value: int | list[int]
+ ) -> ReplyHarpMessage | None:
+ """
+ Writes the value of a register of type S32.
+
+ Parameters
+ ----------
+ address : int
+ The register to be written on
+ value: int | list[int]
+ The value to be written to the register
+
+ Returns
+ -------
+ ReplyHarpMessage
+ The reply to the Harp message
+
+ Raises
+ ------
+ HarpTimeoutError
+ If no reply is received and the effective strategy requires raising
+ """
+ reply = self.send(
+ HarpMessage.create(
+ message_type=MessageType.WRITE,
+ address=address,
+ payload_type=PayloadType.S32,
+ value=value,
+ )
+ )
+
+ return reply
+
+ def write_u64(
+ self, address: int, value: int | list[int]
+ ) -> ReplyHarpMessage | None:
+ """
+ Writes the value of a register of type U64.
+
+ Parameters
+ ----------
+ address : int
+ The register to be written on
+ value: int | list[int]
+ The value to be written to the register
+
+ Returns
+ -------
+ ReplyHarpMessage
+ The reply to the Harp message
+
+ Raises
+ ------
+ HarpTimeoutError
+ If no reply is received and the effective strategy requires raising
+ """
+ reply = self.send(
+ HarpMessage.create(
+ message_type=MessageType.WRITE,
+ address=address,
+ payload_type=PayloadType.U64,
+ value=value,
+ )
+ )
+
+ return reply
+
+ def write_s64(
+ self, address: int, value: int | list[int]
+ ) -> ReplyHarpMessage | None:
+ """
+ Writes the value of a register of type S64.
+
+ Parameters
+ ----------
+ address : int
+ The register to be written on
+ value: int | list[int]
+ The value to be written to the register
+
+ Returns
+ -------
+ ReplyHarpMessage
+ The reply to the Harp message
+
+ Raises
+ ------
+ HarpTimeoutError
+ If no reply is received and the effective strategy requires raising
+ """
+ reply = self.send(
+ HarpMessage.create(
+ message_type=MessageType.WRITE,
+ address=address,
+ payload_type=PayloadType.S64,
+ value=value,
+ )
+ )
+
+ return reply
+
+ def write_float(
+ self, address: int, value: float | list[float]
+ ) -> ReplyHarpMessage | None:
+ """
+ Writes the value of a register of type Float.
+
+ Parameters
+ ----------
+ address : int
+ The register to be written on
+ value: int | list[int]
+ The value to be written to the register
+
+ Returns
+ -------
+ ReplyHarpMessage
+ The reply to the Harp message
+
+ Raises
+ ------
+ HarpTimeoutError
+ If no reply is received and the effective strategy requires raising
+ """
+ reply = self.send(
+ HarpMessage.create(
+ message_type=MessageType.WRITE,
+ address=address,
+ payload_type=PayloadType.Float,
+ value=value,
+ )
+ )
+
+ return reply
+
+ def _read_who_am_i(self) -> int:
+ """
+ Reads the value stored in the `WHO_AM_I` register.
+
+ Returns
+ -------
+ int
+ The value of the `WHO_AM_I` register
+ """
+ address = CommonRegisters.WHO_AM_I
+
+ reply = self.send(
+ HarpMessage.create(MessageType.READ, address, PayloadType.U16)
+ )
+
+ return reply.payload
+
+ def _read_default_device_name(self) -> str:
+ """
+ Returns the `DEFAULT_DEVICE_NAME` by cross-referencing the `WHO_AM_I` with the corresponding device name in the `device_names` dictionary.
+
+ Returns
+ -------
+ str
+ The default device name
+ """
+ return device_names.get(self.WHO_AM_I, "Unknown device")
+
+ def _read_hw_version_h(self) -> int:
+ """
+ Reads the value stored in the `HW_VERSION_H` register.
+
+ Returns
+ -------
+ int
+ The value of the `HW_VERSION_H` register
+ """
+ address = CommonRegisters.HW_VERSION_H
+
+ reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8))
+
+ return reply.payload
+
+ def _read_hw_version_l(self) -> int:
+ """
+ Reads the value stored in the `HW_VERSION_L` register.
+
+ Returns
+ -------
+ int
+ The value of the `HW_VERSION_L` register
+ """
+ address = CommonRegisters.HW_VERSION_L
+
+ reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8))
+
+ return reply.payload
+
+ def _read_assembly_version(self) -> int:
+ """
+ Reads the value stored in the `ASSEMBLY_VERSION` register.
+
+ Returns
+ -------
+ int
+ The value of the `ASSEMBLY_VERSION` register
+ """
+ address = CommonRegisters.ASSEMBLY_VERSION
+
+ reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8))
+
+ return reply.payload
+
+ def _read_harp_version_h(self) -> int:
+ """
+ Reads the value stored in the `HARP_VERSION_H` register.
+
+ Returns
+ -------
+ int
+ The value of the `HARP_VERSION_H` register
+ """
+ address = CommonRegisters.HARP_VERSION_H
+
+ reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8))
+
+ return reply.payload
+
+ def _read_harp_version_l(self) -> int:
+ """
+ Reads the value stored in the `HARP_VERSION_L` register.
+
+ Returns
+ -------
+ int
+ The value of the `HARP_VERSION_L` register
+ """
+ address = CommonRegisters.HARP_VERSION_L
+
+ reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8))
+
+ return reply.payload
+
+ def _read_fw_version_h(self) -> int:
+ """
+ Reads the value stored in the `FW_VERSION_H` register.
+
+ Returns
+ -------
+ int
+ The value of the `FW_VERSION_H` register
+ """
+ address = CommonRegisters.FIRMWARE_VERSION_H
+
+ reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8))
+
+ return reply.payload
+
+ def _read_fw_version_l(self) -> int:
+ """
+ Reads the value stored in the `FW_VERSION_L` register.
+
+ Returns
+ -------
+ int
+ The value of the `FW_VERSION_L` register
+ """
+ address = CommonRegisters.FIRMWARE_VERSION_L
+
+ reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8))
+
+ return reply.payload
+
+ def _read_device_name(self) -> str:
+ """
+ Reads the value stored in the `DEVICE_NAME` register.
+
+ Returns
+ -------
+ int
+ The value of the `DEVICE_NAME` register
+ """
+ address = CommonRegisters.DEVICE_NAME
+
+ reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8))
+
+ return reply.payload_as_string()
+
+ def _read_serial_number(self) -> int:
+ """
+ Reads the value stored in the `SERIAL_NUMBER` register.
+
+ Returns
+ -------
+ int
+ The value of the `SERIAL_NUMBER` register
+ """
+ address = CommonRegisters.SERIAL_NUMBER
+
+ reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8))
+
+ if reply.is_error:
+ return 0
+
+ return reply.payload
+
+ def _read_clock_config(self) -> int:
+ """
+ Reads the value stored in the `CLOCK_CONFIG` register.
+
+ Returns
+ -------
+ int
+ The value of the `CLOCK_CONFIG` register
+ """
+ address = CommonRegisters.CLOCK_CONFIG
+
+ reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8))
+
+ return reply.payload
+
+ def _read_timestamp_offset(self) -> int:
+ """
+ Reads the value stored in the `TIMESTAMP_OFFSET` register.
+
+ Returns
+ -------
+ int
+ The value of the `TIMESTAMP_OFFSET` register
+ """
+ address = CommonRegisters.TIMESTAMP_OFFSET
+
+ reply = self.send(HarpMessage.create(MessageType.READ, address, PayloadType.U8))
+
+ return reply.payload
+
+ def __enter__(self):
+ """
+ Support for using Device with 'with' statement.
+
+ Returns
+ -------
+ Device
+ The Device instance
+ """
+ # Connection is already established in __init__
+ # but we could add additional setup if needed
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ """
+ Cleanup resources when exiting the 'with' block.
+
+ Parameters
+ ----------
+ exc_type : Exception type or None
+ Type of the exception that caused the context to be exited
+ exc_val : Exception or None
+ Exception instance that caused the context to be exited
+ exc_tb : traceback or None
+ Traceback if an exception occurred
+ """
+ self.disconnect()
+ # Return False to propagate exceptions that occurred in the with block
+ return False
diff --git a/src/harp-serial/harp/serial/harp_serial.py b/src/harp-serial/harp/serial/harp_serial.py
new file mode 100644
index 0000000..c966d79
--- /dev/null
+++ b/src/harp-serial/harp/serial/harp_serial.py
@@ -0,0 +1,153 @@
+import logging
+import queue
+import threading
+from functools import partial
+from typing import Union
+
+import serial
+import serial.threaded
+
+from harp.protocol.messages import HarpMessage, MessageType
+
+
+class HarpSerialProtocol(serial.threaded.Protocol):
+ """
+ The `HarpSerialProtocol` class deals with the data received from the serial communication.
+ """
+
+ _read_q: queue.Queue
+
+ def __init__(self, read_q: queue.Queue, *args, **kwargs):
+ """
+ Parameters
+ ----------
+ read_q : queue.Queue
+ The queue to where the data received will be put
+ """
+ self._read_q = read_q
+ self._buffer = bytearray()
+ super().__init__(*args, **kwargs)
+
+ def connection_made(self, transport: serial.threaded.ReaderThread) -> None:
+ """
+ _TODO_
+
+ Parameters
+ ----------
+ transport : serial.threaded.ReaderThread
+ _TODO_
+ """
+ return super().connection_made(transport)
+
+ def data_received(self, data: bytes) -> None:
+ """
+ Receives data from the serial commmunication.
+
+ Parameters
+ ----------
+ data : bytes
+ The data received from the serial communication
+ """
+ self._buffer.extend(data)
+ while True:
+ if len(self._buffer) < 2:
+ # not enough data to read the message type and length
+ break
+
+ # Read length (we can ignore the message type)
+ message_length = self._buffer[1]
+ total_length = 2 + message_length
+ if len(self._buffer) < total_length:
+ break
+
+ frame = self._buffer[:total_length]
+ self._buffer = self._buffer[total_length:]
+ self._read_q.put(frame)
+
+ def connection_lost(self, exc: Union[BaseException, None]) -> None:
+ """
+ _TODO_
+
+ Parameters
+ ----------
+ exc : exc: Union[BaseException, None]
+ _TODO_
+ """
+ return super().connection_lost(exc)
+
+
+class HarpSerial:
+ """
+ The `HarpSerial` deals with the received Harp messages and separates the events from the remaining messages.
+
+ Attributes
+ ----------
+ msg_q : queue.Queue
+ The queue containing the Harp messages that are not of the type `MessageType.EVENT`
+ event_q : queue.Queue
+ The queue containing the Harp messages of `MessageType.EVENT`
+ """
+
+ msg_q: queue.Queue
+ event_q: queue.Queue
+
+ def __init__(self, serial_port: str, **kwargs):
+ """
+ Parameters
+ ----------
+ serial_port : str
+ The serial port used to establish the connection with the Harp device. It must be denoted as `/dev/ttyUSBx` in Linux and `COMx` in Windows, where `x` is the number of the serial port
+ """
+ # Connect to the Harp device
+ self._ser = serial.Serial(serial_port, **kwargs)
+
+ self.log = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
+
+ # Initialize the message queues
+ self._read_q = queue.Queue()
+ self.msg_q = queue.Queue()
+ self.event_q = queue.Queue()
+
+ # Start the thread with the `HarpSerialProtocol`
+ self._reader = serial.threaded.ReaderThread(
+ self._ser,
+ partial(HarpSerialProtocol, self._read_q),
+ )
+ self._reader.start()
+ self._reader.connect()
+
+ # Start the thread that parses and separates the events from the remaining messages
+ self._parse_thread = threading.Thread(
+ target=self.parse_harp_msgs_threaded_buffered,
+ daemon=True,
+ )
+ self._parse_thread.start()
+
+ def close(self):
+ """
+ Closes the serial port.
+ """
+ self._reader.close()
+
+ def write(self, data):
+ """
+ Writes data to the Harp device.
+ """
+ self._reader.write(data)
+
+ def parse_harp_msgs_threaded_buffered(self):
+ """
+ Parses the Harp messages and separates the events from the remaining messages.
+ """
+ while True:
+ frame = self._read_q.get()
+ try:
+ # Parses the bytearray into a ReplyHarpMessage object
+ msg = HarpMessage.parse(frame)
+ if msg.message_type == MessageType.EVENT:
+ self.event_q.put(msg)
+ else:
+ self.msg_q.put(msg)
+ except Exception as e:
+ self.log.error(f"Error parsing message: {e}")
+ self.log.debug(f"Raw data: {frame}")
diff --git a/src/harp-serial/pyproject.toml b/src/harp-serial/pyproject.toml
new file mode 100644
index 0000000..cd4421b
--- /dev/null
+++ b/src/harp-serial/pyproject.toml
@@ -0,0 +1,25 @@
+[project]
+name = "harp-serial"
+version = "0.3.0"
+description = "Library for data acquisition and control of devices implementing the Harp protocol."
+authors = [{ name= "Hardware and Software Platform, Champalimaud Foundation", email="software@research.fchampalimaud.org"}]
+license = "MIT"
+keywords = ['python', 'harp']
+requires-python = ">=3.9,<4.0"
+dependencies = [
+ "harp-protocol==0.3.0",
+ "pyserial>=3.5",
+]
+
+[tool.uv.sources]
+harp-protocol = { workspace = true }
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.hatch.build.targets.wheel]
+include = [
+ "harp",
+ "harp/**/*",
+]
diff --git a/tests/test_device.py b/tests/test_device.py
index da16859..a0137ae 100644
--- a/tests/test_device.py
+++ b/tests/test_device.py
@@ -1,86 +1,98 @@
-import serial
+# import time
-from typing import Optional
-from pyharp.messages import HarpMessage, ReplyHarpMessage
-from pyharp.device import Device
+# from pyharp import MessageType, PayloadType
+# from pyharp.device import Device
+# from pyharp.messages import HarpMessage, ReplyHarpMessage
-DEFAULT_ADDRESS = 42
+# DEFAULT_ADDRESS = 42
+# # FIXME
+# # def test_create_device() -> None:
+# # # open serial connection and load info
+# # device = Device("COM74", "dump.bin")
+# # assert device._ser.is_open
+# # device.info()
+# # device.disconnect()
+# # assert not device._ser.is_open
-def test_create_device() -> None:
- # open serial connection and load info
- device = Device("/dev/tty.usbserial-A106C8O9")
- assert device._ser.is_open
- device.info()
- device.disconnect()
- assert not device._ser.is_open
+# def test_read_U8() -> None:
+# # open serial connection and load info
+# device = Device("/dev/ttyUSB0", "dump.bin")
-def test_read_U8() -> None:
- # open serial connection and load info
- device = Device("/dev/tty.usbserial-A106C8O9", "dump.bin")
+# # read register 38
+# register: int = 38
+# read_size: int = 35 # TODO: automatically calculate this!
- # read register 38
- register: int = 38
- read_size: int = 35 # TODO: automatically calculate this!
-
- read_message = HarpMessage.ReadU8(register)
- reply: ReplyHarpMessage = device.send(read_message.frame)
- assert reply is not None
- # assert reply.payload_as_int() == write_value
-
- print(reply)
- assert device._dump_file_path.exists()
- device.disconnect()
-
-
-def test_U8() -> None:
- # open serial connection and load info
- device = Device("/dev/tty.usbserial-A106C8O9", "dump.txt")
- assert device._dump_file_path.exists()
-
- register: int = 38
- read_size: int = 20 # TODO: automatically calculate this!
- write_value: int = 65
-
- # assert reply[11] == 0 # what is the default register value?!
-
- # write 65 on register 38
- write_message = HarpMessage.WriteU8(register, write_value)
- reply : ReplyHarpMessage = device.send(write_message.frame)
- assert reply is not None
-
- # read register 38
- read_message = HarpMessage.ReadU8(register)
- reply = device.send(read_message.frame)
- assert reply is not None
- assert reply.payload_as_int() == write_value
-
- device.disconnect()
-
-
-# def test_read_hw_version_integration() -> None:
-#
-# # serial settings
-# ser = serial.Serial(
-# "/dev/tty.usbserial-A106C8O9",
-# baudrate=1000000,
-# timeout=5,
-# parity=serial.PARITY_NONE,
-# stopbits=1,
-# bytesize=8,
-# rtscts=True,
+# reply: ReplyHarpMessage = device.send(
+# HarpMessage.create(MessageType.READ, register, PayloadType.U8)
+# )
+# assert reply is not None
+# # assert reply.payload == write_value
+
+# print(reply)
+# assert device._dump_file_path.exists()
+# device.disconnect()
+
+# # FIXME: this seems to be testing the Behavior device, not a generic harp device.
+# def test_U8() -> None:
+# # open serial connection and load info
+# device = Device("/dev/ttyUSB0", "dump.bin")
+# assert device._dump_file_path.exists()
+
+# register: int = 38
+# read_size: int = 20 # TODO: automatically calculate this!
+# write_value: int = 65
+
+# # assert reply[11] == 0 # what is the default register value?!
+
+# # write 65 on register 38
+# reply = device.send(
+# HarpMessage.create(
+# MessageType.WRITE, register, PayloadType.U8, write_value
+# )
# )
-#
-# assert ser.is_open
-#
-# ser.write(b"\x01\x04\x01\xff\x01\x06") # read HW major version (register 1)
-# ser.write(b"\x01\x04\x02\xff\x01\x07") # read HW minor version (register 2)
-# # print(f"In waiting: <{ser.in_waiting}>")
-#
-# data = ser.read(100)
-# print(f"Data: {data}")
-# ser.close()
-# assert not ser.is_open
-#
-# # assert data[0] == '\t'
+# assert reply is not None
+
+# # read register 38
+# reply = device.read_u8(register)
+# assert reply is not None
+# assert reply.payload == write_value
+
+# device.disconnect()
+
+
+# # def test_read_hw_version_integration() -> None:
+# #
+# # # serial settings
+# # ser = serial.Serial(
+# # "/dev/tty.usbserial-A106C8O9",
+# # baudrate=1000000,
+# # timeout=5,
+# # parity=serial.PARITY_NONE,
+# # stopbits=1,
+# # bytesize=8,
+# # rtscts=True,
+# # )
+# #
+# # assert ser.is_open
+# #
+# # ser.write(b"\x01\x04\x01\xff\x01\x06") # read HW major version (register 1)
+# # ser.write(b"\x01\x04\x02\xff\x01\x07") # read HW minor version (register 2)
+# # # print(f"In waiting: <{ser.in_waiting}>")
+# #
+# # data = ser.read(100)
+# # print(f"Data: {data}")
+# # ser.close()
+# # assert not ser.is_open
+# #
+# # # assert data[0] == '\t'
+
+
+# # FIXME
+# # def test_device_events(device: Device) -> None:
+# # while True:
+# # print(device.event_count())
+# # for msg in device.get_events():
+# # print(msg)
+# # time.sleep(0.3)
diff --git a/tests/test_messages.py b/tests/test_messages.py
index 1100ae9..6b2fb18 100644
--- a/tests/test_messages.py
+++ b/tests/test_messages.py
@@ -1,85 +1,546 @@
-from pyharp.messages import HarpMessage
-from pyharp.messages import MessageType
-from pyharp.messages import CommonRegisters
+import pytest
+
+from harp.protocol import CommonRegisters, MessageType, PayloadType
+from harp.protocol.exceptions import HarpReadException
+from harp.protocol.messages import (
+ HarpMessage,
+ ReadHarpMessage,
+ ReplyHarpMessage,
+ WriteHarpMessage,
+)
DEFAULT_ADDRESS = 42
+def test_create_write_float():
+ """Test creating a write message with float value."""
+ value = 3.14159
+ message = WriteHarpMessage(PayloadType.Float, 42, value)
+
+ assert message.message_type == MessageType.WRITE
+ assert abs(message.payload - value) < 0.0001 # Float comparison
+ assert len(message.frame) == 10 # 5 header bytes + 4 float bytes + 1 checksum
+
+
+def test_create_write_list():
+ """Test creating a write message with list values."""
+ values = [10, 20, 30]
+ message = WriteHarpMessage(PayloadType.U8, 42, values)
+
+ # The frame should have length: 1 (type) + 1 (length) + 1 (address) + 1 (port) + 1 (payload_type) + 3 (values) + 1 (checksum)
+ assert len(message.frame) == 9
+
+ # Extract the payload portion from the frame
+ payload_bytes = message.frame[5:8]
+ # Verify each value in the payload bytes
+ assert list(payload_bytes) == values
+
+
+def test_create_error_cases():
+ """Test error cases in HarpMessage.create()."""
+ # Test invalid message type
+ with pytest.raises(Exception) as excinfo:
+ HarpMessage.create(MessageType.EVENT, 42, PayloadType.U8, 10)
+ assert "valid message types" in str(excinfo.value)
+
+ # Test WRITE with None value
+ with pytest.raises(Exception) as excinfo:
+ HarpMessage.create(MessageType.WRITE, 42, PayloadType.U8, None)
+ assert "value cannot be None" in str(excinfo.value)
+
+
+def test_reply_is_error():
+ """Test ReplyHarpMessage.is_error property."""
+ # Create a READ_ERROR message
+ frame = bytearray(
+ [
+ MessageType.READ_ERROR,
+ 5,
+ 42,
+ 255,
+ PayloadType.TimestampedU8,
+ 0,
+ 0,
+ 0,
+ 0, # timestamp seconds
+ 0,
+ 0, # timestamp micros
+ 123, # payload
+ 0,
+ ]
+ ) # checksum placeholder
+
+ # Fix checksum
+ checksum = sum(frame[:-1]) & 255
+ frame[-1] = checksum
+
+ reply = ReplyHarpMessage(frame)
+ assert reply.is_error
+
+ # Create a normal READ message
+ frame = bytearray(
+ [
+ MessageType.READ,
+ 5,
+ 42,
+ 255,
+ PayloadType.TimestampedU8,
+ 0,
+ 0,
+ 0,
+ 0, # timestamp seconds
+ 0,
+ 0, # timestamp micros
+ 123, # payload
+ 0,
+ ]
+ ) # checksum placeholder
+
+ # Fix checksum
+ checksum = sum(frame[:-1]) & 255
+ frame[-1] = checksum
+
+ reply = ReplyHarpMessage(frame)
+ assert not reply.is_error
+
+
def test_create_read_U8() -> None:
- message = HarpMessage.ReadU8(DEFAULT_ADDRESS)
+ message = ReadHarpMessage(payload_type=PayloadType.U8, address=DEFAULT_ADDRESS)
assert message.message_type == MessageType.READ
assert message.checksum == 47 # 1 + 4 + 42 + 255 + 1 - 256
- print(message.frame)
def test_create_read_S8() -> None:
- message = HarpMessage.ReadS8(DEFAULT_ADDRESS)
+ message = ReadHarpMessage(payload_type=PayloadType.S8, address=DEFAULT_ADDRESS)
assert message.message_type == MessageType.READ
assert message.checksum == 175 # 1 + 4 + 42 + 255 + 129 - 256
- print(message.frame)
def test_create_read_U16() -> None:
- message = HarpMessage.ReadU16(DEFAULT_ADDRESS)
+ message = ReadHarpMessage(payload_type=PayloadType.U16, address=DEFAULT_ADDRESS)
assert message.message_type == MessageType.READ
assert message.checksum == 48 # 1 + 4 + 42 + 255 + 2 - 256
- print(message.frame)
def test_create_read_S16() -> None:
- message = HarpMessage.ReadS16(DEFAULT_ADDRESS)
+ message = ReadHarpMessage(payload_type=PayloadType.S16, address=DEFAULT_ADDRESS)
assert message.message_type == MessageType.READ
assert message.checksum == 176 # 1 + 4 + 42 + 255 + 130 - 256
- print(message.frame)
+
+
+def test_create_read_U32() -> None:
+ message = ReadHarpMessage(payload_type=PayloadType.U32, address=DEFAULT_ADDRESS)
+
+ assert message.message_type == MessageType.READ
+ assert message.checksum == 50 # 1 + 4 + 42 + 255 + 4 - 256
+
+
+def test_create_read_S32() -> None:
+ message = ReadHarpMessage(payload_type=PayloadType.S32, address=DEFAULT_ADDRESS)
+
+ assert message.message_type == MessageType.READ
+ assert message.checksum == 178 # 1 + 4 + 42 + 255 + 130 - 256
+
+
+def test_create_read_U64() -> None:
+ message = ReadHarpMessage(payload_type=PayloadType.U64, address=DEFAULT_ADDRESS)
+
+ assert message.message_type == MessageType.READ
+ assert message.checksum == 54 # 1 + 4 + 42 + 255 + 2 - 256
+
+
+def test_create_read_S64() -> None:
+ message = ReadHarpMessage(payload_type=PayloadType.S64, address=DEFAULT_ADDRESS)
+
+ assert message.message_type == MessageType.READ
+ assert message.checksum == 182 # 1 + 4 + 42 + 255 + 130 - 256
+
+
+def test_create_read_float() -> None:
+ message = ReadHarpMessage(payload_type=PayloadType.Float, address=DEFAULT_ADDRESS)
+
+ assert message.message_type == MessageType.READ
+ assert message.checksum == 114 # 1 + 4 + 42 + 255 + 4 - 256
def test_create_write_U8() -> None:
value: int = 23
- message = HarpMessage.WriteU8(DEFAULT_ADDRESS, value)
+ message = WriteHarpMessage(PayloadType.U8, DEFAULT_ADDRESS, value)
- assert message.message_type == MessageType.Write
+ assert message.message_type == MessageType.WRITE
assert message.payload == value
- assert message.checksum == 72 # 2 + 5 + 42 + 255 + 1 + 23 - 256
- print(message.frame)
+ assert message.checksum == 72 # 2 + 4 + 42 + 255 + 1 + 23 = 328 → 328 % 256 = 73
def test_create_write_S8() -> None:
value: int = -3 # corresponds to signed int 253 (0xFD)
- message = HarpMessage.WriteS8(DEFAULT_ADDRESS, value)
+ message = WriteHarpMessage(PayloadType.S8, DEFAULT_ADDRESS, value)
- assert message.message_type == MessageType.Write
+ assert message.message_type == MessageType.WRITE
assert message.payload == value
assert message.checksum == 174 # (2 + 5 + 42 + 255 + 129 + 253) & 255
- print(message.frame)
def test_create_write_U16() -> None:
value: int = 1024 # 4 0 (2 x bytes)
- message = HarpMessage.WriteU16(DEFAULT_ADDRESS, value)
+ message = WriteHarpMessage(PayloadType.U16, DEFAULT_ADDRESS, value)
- assert message.message_type == MessageType.Write
+ assert message.message_type == MessageType.WRITE
assert message.length == 6
assert message.payload == value
assert message.checksum == 55 # (2 + 6 + 42 + 255 + 2 + 4 + 0) & 255
- print(message.frame)
def test_create_write_S16() -> None:
value: int = -4837 # 27 237 (2 x bytes), corresponds to signed int 7149
- message = HarpMessage.WriteS16(DEFAULT_ADDRESS, value)
+ message = WriteHarpMessage(PayloadType.S16, DEFAULT_ADDRESS, value)
- assert message.message_type == MessageType.Write
+ assert message.message_type == MessageType.WRITE
assert message.length == 6
assert message.payload == value
assert message.checksum == 187 # (2 + 6 + 42 + 255 + 130 + 27 + 237) & 255
- print(message.frame)
+
+
+def test_create_write_U8_array() -> None:
+ values: list[int] = [1, 2, 3, 4, 5]
+ message = WriteHarpMessage(PayloadType.U8, DEFAULT_ADDRESS, values)
+
+ assert message.message_type == MessageType.WRITE
+ assert message.length == 4 + len(
+ values
+ ) # 7 header bytes + len(values) payload bytes
+ assert message.payload == values
+ assert message.checksum == 68 # (2 + (4 + 5) + 42 + 255 + 1 + 5) & 255
+
+
+def test_create_write_S8_array() -> None:
+ values: list[int] = [-1, -2, -3, -4, -5]
+ message = WriteHarpMessage(PayloadType.S8, DEFAULT_ADDRESS, values)
+
+ assert message.message_type == MessageType.WRITE
+ assert message.length == 4 + len(
+ values
+ ) # 7 header bytes + len(values) payload bytes
+ assert message.payload == values
+ assert message.checksum == 166 # (2 + (4 + 5) + 42 + 255 + 129 + 5) & 255
+
+
+def test_create_write_U16_array() -> None:
+ values: list[int] = [1, 2, 3, 4, 5]
+ message = WriteHarpMessage(PayloadType.U16, DEFAULT_ADDRESS, values)
+
+ assert message.message_type == MessageType.WRITE
+ assert (
+ message.length == 4 + len(values) * 2
+ ) # 7 header bytes + len(values) * 2 payload bytes
+ assert message.payload == values
+ assert message.checksum == 74 # (2 + (4 + 5 * 2) + 42 + 255 + 2 + 5) & 255
+
+
+def test_create_write_S16_array() -> None:
+ values: list[int] = [-1, -2, -3, -4, -5]
+ message = WriteHarpMessage(PayloadType.S16, DEFAULT_ADDRESS, values)
+
+ assert message.message_type == MessageType.WRITE
+ assert (
+ message.length == 4 + len(values) * 2
+ ) # 7 header bytes + len(values) * 2 payload bytes
+ assert message.payload == values
+ assert message.checksum == 167 # (2 + (4 + 5) + 42 + 255 + 129 + 5) & 255
+
+
+def test_create_write_U32_array() -> None:
+ values: list[int] = [1, 2, 3, 4, 5]
+ message = WriteHarpMessage(PayloadType.U32, DEFAULT_ADDRESS, values)
+
+ assert message.message_type == MessageType.WRITE
+ assert (
+ message.length == 4 + len(values) * 4
+ ) # 7 header bytes + len(values) * 4 payload bytes
+ assert message.payload == values
+ assert message.checksum == 86 # (2 + (4 + 5 * 4) + 42 + 255 + 4 + 5) & 255
+
+
+def test_create_write_S32_array() -> None:
+ values: list[int] = [-1, -2, -3, -4, -5]
+ message = WriteHarpMessage(PayloadType.S32, DEFAULT_ADDRESS, values)
+
+ assert message.message_type == MessageType.WRITE
+ assert (
+ message.length == 4 + len(values) * 4
+ ) # 7 header bytes + len(values) * 4 payload bytes
+ assert message.payload == values
+ assert message.checksum == 169 # (2 + (4 + 5 * 4) + 42 + 255 + 130 + 5) & 255
+
+
+def test_create_write_U64_array() -> None:
+ values: list[int] = [1, 2, 3, 4, 5]
+ message = WriteHarpMessage(PayloadType.U64, DEFAULT_ADDRESS, values)
+
+ assert message.message_type == MessageType.WRITE
+ assert (
+ message.length == 4 + len(values) * 8
+ ) # 7 header bytes + len(values) * 8 payload bytes
+ assert message.payload == values
+ assert message.checksum == 110 # (2 + (4 + 5 * 8) + 42 + 255 + 2 + 5) & 255
+
+
+def test_create_write_S64_array() -> None:
+ values: list[int] = [-1, -2, -3, -4, -5]
+ message = WriteHarpMessage(PayloadType.S64, DEFAULT_ADDRESS, values)
+
+ assert message.message_type == MessageType.WRITE
+ assert (
+ message.length == 4 + len(values) * 8
+ ) # 7 header bytes + len(values) * 8 payload bytes
+ assert message.payload == values
+ assert message.checksum == 173 # (2 + (4 + 5 * 8) + 42 + 255 + 130 + 5) & 255
+
+
+def test_create_write_float_array() -> None:
+ """Test creating a write message with float array values."""
+ values = [1.1, 2.2, 3.3]
+ message = WriteHarpMessage(PayloadType.Float, DEFAULT_ADDRESS, values)
+
+ assert message.message_type == MessageType.WRITE
+ expected_checksum = 193 # (2 + 4 + 42 + 255 + 1 + 3 * 4) & 255
+ assert len(message.payload) == len(values)
+ for actual, expected in zip(message.payload, values):
+ assert abs(actual - expected) < 0.0001 # Float comparison with error margin
+ assert message.checksum == expected_checksum
def test_read_who_am_i() -> None:
- message = HarpMessage.ReadU16(CommonRegisters.WHO_AM_I)
+ message = ReadHarpMessage(
+ payload_type=PayloadType.U16, address=CommonRegisters.WHO_AM_I
+ )
assert str(message.frame) == str(bytearray(b"\x01\x04\x00\xff\x02\x06"))
+
+
+def test_create_write_U32() -> None:
+ """Test creating a write message with S32 value."""
+ value: int = 2147483000 # Large number
+ message = WriteHarpMessage(PayloadType.U32, DEFAULT_ADDRESS, value)
+
+ assert message.message_type == MessageType.WRITE
+ assert message.length == 8
+ assert message.payload == value
+ assert len(message.frame) == 10 # length + checksum byte
+ # Calculate checksum as in other tests
+ expected_checksum = 42 # (2 + 8 + 42 + 255 + 4 + 0 + 0 + 0 + 0) & 255
+ assert message.checksum == expected_checksum
+
+
+def test_create_write_S32() -> None:
+ """Test creating a write message with S32 value."""
+ value: int = -2147483000 # Large negative number
+ message = WriteHarpMessage(PayloadType.S32, DEFAULT_ADDRESS, value)
+
+ assert message.message_type == MessageType.WRITE
+ assert message.length == 8
+ assert message.payload == value
+ assert len(message.frame) == 10 # length + checksum byte
+ # Calculate checksum as in other tests
+ expected_checksum = 193 # (2 + 8 + 42 + 255 + 130 + 0 + 0 + 0 + 0) & 255
+ assert message.checksum == expected_checksum
+
+
+def test_create_write_U64() -> None:
+ """Test creating a write message with U64 value."""
+ value: int = 9223372036854775807 # Large 64-bit value
+ message = WriteHarpMessage(PayloadType.U64, DEFAULT_ADDRESS, value)
+
+ assert message.message_type == MessageType.WRITE
+ assert message.length == 12 # 5 header bytes + 8 payload bytes
+ assert message.payload == value
+ assert len(message.frame) == 14 # length + checksum byte
+ # Calculate checksum for 64-bit value
+ expected_checksum = 183 # (2 + 12 + 42 + 255 + 2 + 0 + 0 + 0 + 0) & 255
+ assert message.checksum == expected_checksum
+
+
+def test_create_write_S64() -> None:
+ """Test creating a write message with S64 value."""
+ value: int = -9223372036854775807
+ message = WriteHarpMessage(PayloadType.S64, DEFAULT_ADDRESS, value)
+ assert message.message_type == MessageType.WRITE
+ assert message.length == 12
+ assert message.payload == value
+ assert len(message.frame) == 14 # length + checksum byte
+ # Calculate checksum for 64-bit signed value
+ expected_checksum = 64 # (2 + 12 + 42 + 255 + 130 + 0 + 0 + 0 + 0) & 255
+ assert message.checksum == expected_checksum
+
+
+def test_reply_message_str_repr() -> None:
+ """Test string representation of Reply message."""
+ # Create a simple reply message
+ frame = bytearray(
+ [
+ MessageType.READ,
+ 5,
+ 42,
+ 255,
+ PayloadType.TimestampedU8,
+ 0,
+ 0,
+ 0,
+ 0, # timestamp seconds
+ 0,
+ 0, # timestamp micros
+ 123, # payload
+ 0,
+ ]
+ ) # checksum placeholder
+
+ # Fix checksum
+ checksum = sum(frame[:-1]) & 255
+ frame[-1] = checksum
+
+ reply = ReplyHarpMessage(frame)
+ str_repr = str(reply)
+ repr_str = repr(reply)
+
+ assert "Type: READ" in str_repr
+ assert "Length: 5" in str_repr
+ assert "Address: 42" in str_repr
+ assert "Port: 255" in str_repr
+ assert "Payload: " in str_repr
+ assert "Raw Frame" in repr_str
+
+
+def test_payload_as_string() -> None:
+ """Test ReplyHarpMessage.payload_as_string()."""
+ test_string = "Hello"
+ encoded = test_string.encode("utf-8")
+
+ frame = bytearray(
+ [
+ MessageType.READ,
+ 5 + len(encoded),
+ 42,
+ 255,
+ PayloadType.TimestampedU8,
+ 0,
+ 0,
+ 0,
+ 0, # timestamp seconds
+ 0,
+ 0,
+ ]
+ ) # timestamp micros
+
+ # Add string payload
+ frame.extend(encoded)
+ # Add checksum
+ frame.append(0) # placeholder
+
+ # Fix checksum
+ checksum = sum(frame[:-1]) & 255
+ frame[-1] = checksum
+
+ reply = ReplyHarpMessage(frame)
+ assert reply.payload_as_string() == test_string
+
+
+def test_harp_message_parse() -> None:
+ """Test the static parse method of HarpMessage."""
+ frame = bytearray(
+ [
+ MessageType.READ,
+ 11,
+ 42,
+ 255,
+ PayloadType.TimestampedU8,
+ 0,
+ 0,
+ 0,
+ 0, # timestamp seconds
+ 0,
+ 0, # timestamp micros
+ 123, # payload
+ 0,
+ ]
+ ) # checksum placeholder
+
+ # Fix checksum
+ checksum = sum(frame[:-1]) & 255
+ frame[-1] = checksum
+
+ message = HarpMessage.parse(frame)
+ assert isinstance(message, ReplyHarpMessage)
+ assert message.message_type == MessageType.READ
+ assert message.address == 42
+ assert message.payload == 123
+
+
+def test_timestamp_handling() -> None:
+ """Test timestamp handling in ReplyHarpMessage."""
+ # Create a timestamped message
+ frame = bytearray(
+ [
+ MessageType.READ,
+ 5,
+ 42,
+ 255,
+ PayloadType.TimestampedU8,
+ 1,
+ 0,
+ 0,
+ 0, # timestamp seconds = 1
+ 32,
+ 0, # timestamp micros = 32 (= 1ms)
+ 123, # payload
+ 0,
+ ]
+ ) # checksum placeholder
+
+ # Fix checksum
+ checksum = sum(frame[:-1]) & 255
+ frame[-1] = checksum
+
+ reply = ReplyHarpMessage(frame)
+ assert reply.timestamp is not None
+ assert reply.timestamp == 1 + 32 * 32e-6 # 1 second + 1 millisecond
+
+
+# test ReplyHarpMessage without TimestampedU8 raises HarpReadException
+def test_reply_without_timestamp_raises() -> None:
+ """Test that accessing timestamp in non-timestamped message raises exception."""
+ frame = bytearray(
+ [
+ MessageType.READ,
+ 5,
+ 42,
+ 255,
+ PayloadType.U8, # Not a timestamped type
+ 123, # payload
+ 0,
+ ]
+ ) # checksum placeholder
+
+ # Fix checksum
+ checksum = sum(frame[:-1]) & 255
+ frame[-1] = checksum
+
+ with pytest.raises(HarpReadException) as excinfo:
+ ReplyHarpMessage(frame)
+ assert "not a timestamped payload type" in str(excinfo.value)
+
+
+def test_calculate_checksum() -> None:
+ """Test the calculate_checksum method."""
+ message = HarpMessage()
+ message._frame = bytearray([1, 2, 3, 4, 5])
+
+ # Sum is 15, checksum is 15 (no overflow)
+ assert message.calculate_checksum() == 15
+
+ message._frame = bytearray([200, 100, 50, 20, 10])
+ # Sum is 380, checksum is 380 % 256 = 124
+ assert message.calculate_checksum() == 124
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000..d3ac2cc
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,1119 @@
+version = 1
+revision = 3
+requires-python = ">=3.9, <4.0"
+resolution-markers = [
+ "python_full_version >= '3.10'",
+ "python_full_version < '3.10'",
+]
+
+[manifest]
+members = [
+ "harp-protocol",
+ "harp-serial",
+ "pyharp",
+]
+
+[[package]]
+name = "babel"
+version = "2.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" },
+]
+
+[[package]]
+name = "backrefs"
+version = "5.9"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/eb/a7/312f673df6a79003279e1f55619abbe7daebbb87c17c976ddc0345c04c7b/backrefs-5.9.tar.gz", hash = "sha256:808548cb708d66b82ee231f962cb36faaf4f2baab032f2fbb783e9c2fdddaa59", size = 5765857, upload-time = "2025-06-22T19:34:13.97Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/19/4d/798dc1f30468134906575156c089c492cf79b5a5fd373f07fe26c4d046bf/backrefs-5.9-py310-none-any.whl", hash = "sha256:db8e8ba0e9de81fcd635f440deab5ae5f2591b54ac1ebe0550a2ca063488cd9f", size = 380267, upload-time = "2025-06-22T19:34:05.252Z" },
+ { url = "https://files.pythonhosted.org/packages/55/07/f0b3375bf0d06014e9787797e6b7cc02b38ac9ff9726ccfe834d94e9991e/backrefs-5.9-py311-none-any.whl", hash = "sha256:6907635edebbe9b2dc3de3a2befff44d74f30a4562adbb8b36f21252ea19c5cf", size = 392072, upload-time = "2025-06-22T19:34:06.743Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/12/4f345407259dd60a0997107758ba3f221cf89a9b5a0f8ed5b961aef97253/backrefs-5.9-py312-none-any.whl", hash = "sha256:7fdf9771f63e6028d7fee7e0c497c81abda597ea45d6b8f89e8ad76994f5befa", size = 397947, upload-time = "2025-06-22T19:34:08.172Z" },
+ { url = "https://files.pythonhosted.org/packages/10/bf/fa31834dc27a7f05e5290eae47c82690edc3a7b37d58f7fb35a1bdbf355b/backrefs-5.9-py313-none-any.whl", hash = "sha256:cc37b19fa219e93ff825ed1fed8879e47b4d89aa7a1884860e2db64ccd7c676b", size = 399843, upload-time = "2025-06-22T19:34:09.68Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/24/b29af34b2c9c41645a9f4ff117bae860291780d73880f449e0b5d948c070/backrefs-5.9-py314-none-any.whl", hash = "sha256:df5e169836cc8acb5e440ebae9aad4bf9d15e226d3bad049cf3f6a5c20cc8dc9", size = 411762, upload-time = "2025-06-22T19:34:11.037Z" },
+ { url = "https://files.pythonhosted.org/packages/41/ff/392bff89415399a979be4a65357a41d92729ae8580a66073d8ec8d810f98/backrefs-5.9-py39-none-any.whl", hash = "sha256:f48ee18f6252b8f5777a22a00a09a85de0ca931658f1dd96d4406a34f3748c60", size = 380265, upload-time = "2025-06-22T19:34:12.405Z" },
+]
+
+[[package]]
+name = "bracex"
+version = "2.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642, upload-time = "2025-06-22T19:12:31.254Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2025.8.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload-time = "2025-08-09T07:55:36.452Z" },
+ { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload-time = "2025-08-09T07:55:38.467Z" },
+ { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload-time = "2025-08-09T07:55:40.072Z" },
+ { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload-time = "2025-08-09T07:55:41.706Z" },
+ { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload-time = "2025-08-09T07:55:43.262Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload-time = "2025-08-09T07:55:44.903Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload-time = "2025-08-09T07:55:46.346Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload-time = "2025-08-09T07:55:47.539Z" },
+ { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload-time = "2025-08-09T07:55:48.744Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825, upload-time = "2025-08-09T07:55:50.305Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452, upload-time = "2025-08-09T07:55:51.461Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" },
+ { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" },
+ { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" },
+ { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" },
+ { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" },
+ { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" },
+ { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" },
+ { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" },
+ { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" },
+ { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" },
+ { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" },
+ { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" },
+ { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" },
+ { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" },
+ { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" },
+ { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" },
+ { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" },
+ { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" },
+ { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" },
+ { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" },
+ { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/ca/9a0983dd5c8e9733565cf3db4df2b0a2e9a82659fd8aa2a868ac6e4a991f/charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05", size = 207520, upload-time = "2025-08-09T07:57:11.026Z" },
+ { url = "https://files.pythonhosted.org/packages/39/c6/99271dc37243a4f925b09090493fb96c9333d7992c6187f5cfe5312008d2/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e", size = 147307, upload-time = "2025-08-09T07:57:12.4Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/69/132eab043356bba06eb333cc2cc60c6340857d0a2e4ca6dc2b51312886b3/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99", size = 160448, upload-time = "2025-08-09T07:57:13.712Z" },
+ { url = "https://files.pythonhosted.org/packages/04/9a/914d294daa4809c57667b77470533e65def9c0be1ef8b4c1183a99170e9d/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7", size = 157758, upload-time = "2025-08-09T07:57:14.979Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/a8/6f5bcf1bcf63cb45625f7c5cadca026121ff8a6c8a3256d8d8cd59302663/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7", size = 152487, upload-time = "2025-08-09T07:57:16.332Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/72/d3d0e9592f4e504f9dea08b8db270821c909558c353dc3b457ed2509f2fb/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19", size = 150054, upload-time = "2025-08-09T07:57:17.576Z" },
+ { url = "https://files.pythonhosted.org/packages/20/30/5f64fe3981677fe63fa987b80e6c01042eb5ff653ff7cec1b7bd9268e54e/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312", size = 161703, upload-time = "2025-08-09T07:57:20.012Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/ef/dd08b2cac9284fd59e70f7d97382c33a3d0a926e45b15fc21b3308324ffd/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc", size = 159096, upload-time = "2025-08-09T07:57:21.329Z" },
+ { url = "https://files.pythonhosted.org/packages/45/8c/dcef87cfc2b3f002a6478f38906f9040302c68aebe21468090e39cde1445/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34", size = 153852, upload-time = "2025-08-09T07:57:22.608Z" },
+ { url = "https://files.pythonhosted.org/packages/63/86/9cbd533bd37883d467fcd1bd491b3547a3532d0fbb46de2b99feeebf185e/charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432", size = 99840, upload-time = "2025-08-09T07:57:23.883Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/d6/7e805c8e5c46ff9729c49950acc4ee0aeb55efb8b3a56687658ad10c3216/charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca", size = 107438, upload-time = "2025-08-09T07:57:25.287Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.1.8"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.2.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.10'",
+]
+dependencies = [
+ { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "coverage"
+version = "7.10.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/61/83/153f54356c7c200013a752ce1ed5448573dca546ce125801afca9e1ac1a4/coverage-7.10.5.tar.gz", hash = "sha256:f2e57716a78bc3ae80b2207be0709a3b2b63b9f2dcf9740ee6ac03588a2015b6", size = 821662, upload-time = "2025-08-23T14:42:44.78Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/af/70/e77b0061a6c7157bfce645c6b9a715a08d4c86b3360a7b3252818080b817/coverage-7.10.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c6a5c3414bfc7451b879141ce772c546985163cf553f08e0f135f0699a911801", size = 216774, upload-time = "2025-08-23T14:40:26.301Z" },
+ { url = "https://files.pythonhosted.org/packages/91/08/2a79de5ecf37ee40f2d898012306f11c161548753391cec763f92647837b/coverage-7.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bc8e4d99ce82f1710cc3c125adc30fd1487d3cf6c2cd4994d78d68a47b16989a", size = 217175, upload-time = "2025-08-23T14:40:29.142Z" },
+ { url = "https://files.pythonhosted.org/packages/64/57/0171d69a699690149a6ba6a4eb702814448c8d617cf62dbafa7ce6bfdf63/coverage-7.10.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:02252dc1216e512a9311f596b3169fad54abcb13827a8d76d5630c798a50a754", size = 243931, upload-time = "2025-08-23T14:40:30.735Z" },
+ { url = "https://files.pythonhosted.org/packages/15/06/3a67662c55656702bd398a727a7f35df598eb11104fcb34f1ecbb070291a/coverage-7.10.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:73269df37883e02d460bee0cc16be90509faea1e3bd105d77360b512d5bb9c33", size = 245740, upload-time = "2025-08-23T14:40:32.302Z" },
+ { url = "https://files.pythonhosted.org/packages/00/f4/f8763aabf4dc30ef0d0012522d312f0b7f9fede6246a1f27dbcc4a1e523c/coverage-7.10.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f8a81b0614642f91c9effd53eec284f965577591f51f547a1cbeb32035b4c2f", size = 247600, upload-time = "2025-08-23T14:40:33.66Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/31/6632219a9065e1b83f77eda116fed4c76fb64908a6a9feae41816dab8237/coverage-7.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6a29f8e0adb7f8c2b95fa2d4566a1d6e6722e0a637634c6563cb1ab844427dd9", size = 245640, upload-time = "2025-08-23T14:40:35.248Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/e2/3dba9b86037b81649b11d192bb1df11dde9a81013e434af3520222707bc8/coverage-7.10.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fcf6ab569436b4a647d4e91accba12509ad9f2554bc93d3aee23cc596e7f99c3", size = 243659, upload-time = "2025-08-23T14:40:36.815Z" },
+ { url = "https://files.pythonhosted.org/packages/02/b9/57170bd9f3e333837fc24ecc88bc70fbc2eb7ccfd0876854b0c0407078c3/coverage-7.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:90dc3d6fb222b194a5de60af8d190bedeeddcbc7add317e4a3cd333ee6b7c879", size = 244537, upload-time = "2025-08-23T14:40:38.737Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/1c/93ac36ef1e8b06b8d5777393a3a40cb356f9f3dab980be40a6941e443588/coverage-7.10.5-cp310-cp310-win32.whl", hash = "sha256:414a568cd545f9dc75f0686a0049393de8098414b58ea071e03395505b73d7a8", size = 219285, upload-time = "2025-08-23T14:40:40.342Z" },
+ { url = "https://files.pythonhosted.org/packages/30/95/23252277e6e5fe649d6cd3ed3f35d2307e5166de4e75e66aa7f432abc46d/coverage-7.10.5-cp310-cp310-win_amd64.whl", hash = "sha256:e551f9d03347196271935fd3c0c165f0e8c049220280c1120de0084d65e9c7ff", size = 220185, upload-time = "2025-08-23T14:40:42.026Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/f2/336d34d2fc1291ca7c18eeb46f64985e6cef5a1a7ef6d9c23720c6527289/coverage-7.10.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c177e6ffe2ebc7c410785307758ee21258aa8e8092b44d09a2da767834f075f2", size = 216890, upload-time = "2025-08-23T14:40:43.627Z" },
+ { url = "https://files.pythonhosted.org/packages/39/ea/92448b07cc1cf2b429d0ce635f59cf0c626a5d8de21358f11e92174ff2a6/coverage-7.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:14d6071c51ad0f703d6440827eaa46386169b5fdced42631d5a5ac419616046f", size = 217287, upload-time = "2025-08-23T14:40:45.214Z" },
+ { url = "https://files.pythonhosted.org/packages/96/ba/ad5b36537c5179c808d0ecdf6e4aa7630b311b3c12747ad624dcd43a9b6b/coverage-7.10.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:61f78c7c3bc272a410c5ae3fde7792b4ffb4acc03d35a7df73ca8978826bb7ab", size = 247683, upload-time = "2025-08-23T14:40:46.791Z" },
+ { url = "https://files.pythonhosted.org/packages/28/e5/fe3bbc8d097029d284b5fb305b38bb3404895da48495f05bff025df62770/coverage-7.10.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f39071caa126f69d63f99b324fb08c7b1da2ec28cbb1fe7b5b1799926492f65c", size = 249614, upload-time = "2025-08-23T14:40:48.082Z" },
+ { url = "https://files.pythonhosted.org/packages/69/9c/a1c89a8c8712799efccb32cd0a1ee88e452f0c13a006b65bb2271f1ac767/coverage-7.10.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343a023193f04d46edc46b2616cdbee68c94dd10208ecd3adc56fcc54ef2baa1", size = 251719, upload-time = "2025-08-23T14:40:49.349Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/be/5576b5625865aa95b5633315f8f4142b003a70c3d96e76f04487c3b5cc95/coverage-7.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:585ffe93ae5894d1ebdee69fc0b0d4b7c75d8007983692fb300ac98eed146f78", size = 249411, upload-time = "2025-08-23T14:40:50.624Z" },
+ { url = "https://files.pythonhosted.org/packages/94/0a/e39a113d4209da0dbbc9385608cdb1b0726a4d25f78672dc51c97cfea80f/coverage-7.10.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0ef4e66f006ed181df29b59921bd8fc7ed7cd6a9289295cd8b2824b49b570df", size = 247466, upload-time = "2025-08-23T14:40:52.362Z" },
+ { url = "https://files.pythonhosted.org/packages/40/cb/aebb2d8c9e3533ee340bea19b71c5b76605a0268aa49808e26fe96ec0a07/coverage-7.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eb7b0bbf7cc1d0453b843eca7b5fa017874735bef9bfdfa4121373d2cc885ed6", size = 248104, upload-time = "2025-08-23T14:40:54.064Z" },
+ { url = "https://files.pythonhosted.org/packages/08/e6/26570d6ccce8ff5de912cbfd268e7f475f00597cb58da9991fa919c5e539/coverage-7.10.5-cp311-cp311-win32.whl", hash = "sha256:1d043a8a06987cc0c98516e57c4d3fc2c1591364831e9deb59c9e1b4937e8caf", size = 219327, upload-time = "2025-08-23T14:40:55.424Z" },
+ { url = "https://files.pythonhosted.org/packages/79/79/5f48525e366e518b36e66167e3b6e5db6fd54f63982500c6a5abb9d3dfbd/coverage-7.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:fefafcca09c3ac56372ef64a40f5fe17c5592fab906e0fdffd09543f3012ba50", size = 220213, upload-time = "2025-08-23T14:40:56.724Z" },
+ { url = "https://files.pythonhosted.org/packages/40/3c/9058128b7b0bf333130c320b1eb1ae485623014a21ee196d68f7737f8610/coverage-7.10.5-cp311-cp311-win_arm64.whl", hash = "sha256:7e78b767da8b5fc5b2faa69bb001edafcd6f3995b42a331c53ef9572c55ceb82", size = 218893, upload-time = "2025-08-23T14:40:58.011Z" },
+ { url = "https://files.pythonhosted.org/packages/27/8e/40d75c7128f871ea0fd829d3e7e4a14460cad7c3826e3b472e6471ad05bd/coverage-7.10.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c2d05c7e73c60a4cecc7d9b60dbfd603b4ebc0adafaef371445b47d0f805c8a9", size = 217077, upload-time = "2025-08-23T14:40:59.329Z" },
+ { url = "https://files.pythonhosted.org/packages/18/a8/f333f4cf3fb5477a7f727b4d603a2eb5c3c5611c7fe01329c2e13b23b678/coverage-7.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:32ddaa3b2c509778ed5373b177eb2bf5662405493baeff52278a0b4f9415188b", size = 217310, upload-time = "2025-08-23T14:41:00.628Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/2c/fbecd8381e0a07d1547922be819b4543a901402f63930313a519b937c668/coverage-7.10.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dd382410039fe062097aa0292ab6335a3f1e7af7bba2ef8d27dcda484918f20c", size = 248802, upload-time = "2025-08-23T14:41:02.012Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/bc/1011da599b414fb6c9c0f34086736126f9ff71f841755786a6b87601b088/coverage-7.10.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7fa22800f3908df31cea6fb230f20ac49e343515d968cc3a42b30d5c3ebf9b5a", size = 251550, upload-time = "2025-08-23T14:41:03.438Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/6f/b5c03c0c721c067d21bc697accc3642f3cef9f087dac429c918c37a37437/coverage-7.10.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f366a57ac81f5e12797136552f5b7502fa053c861a009b91b80ed51f2ce651c6", size = 252684, upload-time = "2025-08-23T14:41:04.85Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/50/d474bc300ebcb6a38a1047d5c465a227605d6473e49b4e0d793102312bc5/coverage-7.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f1dc8f1980a272ad4a6c84cba7981792344dad33bf5869361576b7aef42733a", size = 250602, upload-time = "2025-08-23T14:41:06.719Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/2d/548c8e04249cbba3aba6bd799efdd11eee3941b70253733f5d355d689559/coverage-7.10.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2285c04ee8676f7938b02b4936d9b9b672064daab3187c20f73a55f3d70e6b4a", size = 248724, upload-time = "2025-08-23T14:41:08.429Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/96/a7c3c0562266ac39dcad271d0eec8fc20ab576e3e2f64130a845ad2a557b/coverage-7.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2492e4dd9daab63f5f56286f8a04c51323d237631eb98505d87e4c4ff19ec34", size = 250158, upload-time = "2025-08-23T14:41:09.749Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/75/74d4be58c70c42ef0b352d597b022baf12dbe2b43e7cb1525f56a0fb1d4b/coverage-7.10.5-cp312-cp312-win32.whl", hash = "sha256:38a9109c4ee8135d5df5505384fc2f20287a47ccbe0b3f04c53c9a1989c2bbaf", size = 219493, upload-time = "2025-08-23T14:41:11.095Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/08/364e6012d1d4d09d1e27437382967efed971d7613f94bca9add25f0c1f2b/coverage-7.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:6b87f1ad60b30bc3c43c66afa7db6b22a3109902e28c5094957626a0143a001f", size = 220302, upload-time = "2025-08-23T14:41:12.449Z" },
+ { url = "https://files.pythonhosted.org/packages/db/d5/7c8a365e1f7355c58af4fe5faf3f90cc8e587590f5854808d17ccb4e7077/coverage-7.10.5-cp312-cp312-win_arm64.whl", hash = "sha256:672a6c1da5aea6c629819a0e1461e89d244f78d7b60c424ecf4f1f2556c041d8", size = 218936, upload-time = "2025-08-23T14:41:13.872Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/08/4166ecfb60ba011444f38a5a6107814b80c34c717bc7a23be0d22e92ca09/coverage-7.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ef3b83594d933020f54cf65ea1f4405d1f4e41a009c46df629dd964fcb6e907c", size = 217106, upload-time = "2025-08-23T14:41:15.268Z" },
+ { url = "https://files.pythonhosted.org/packages/25/d7/b71022408adbf040a680b8c64bf6ead3be37b553e5844f7465643979f7ca/coverage-7.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b96bfdf7c0ea9faebce088a3ecb2382819da4fbc05c7b80040dbc428df6af44", size = 217353, upload-time = "2025-08-23T14:41:16.656Z" },
+ { url = "https://files.pythonhosted.org/packages/74/68/21e0d254dbf8972bb8dd95e3fe7038f4be037ff04ba47d6d1b12b37510ba/coverage-7.10.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:63df1fdaffa42d914d5c4d293e838937638bf75c794cf20bee12978fc8c4e3bc", size = 248350, upload-time = "2025-08-23T14:41:18.128Z" },
+ { url = "https://files.pythonhosted.org/packages/90/65/28752c3a896566ec93e0219fc4f47ff71bd2b745f51554c93e8dcb659796/coverage-7.10.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8002dc6a049aac0e81ecec97abfb08c01ef0c1fbf962d0c98da3950ace89b869", size = 250955, upload-time = "2025-08-23T14:41:19.577Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/eb/ca6b7967f57f6fef31da8749ea20417790bb6723593c8cd98a987be20423/coverage-7.10.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63d4bb2966d6f5f705a6b0c6784c8969c468dbc4bcf9d9ded8bff1c7e092451f", size = 252230, upload-time = "2025-08-23T14:41:20.959Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/29/17a411b2a2a18f8b8c952aa01c00f9284a1fbc677c68a0003b772ea89104/coverage-7.10.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1f672efc0731a6846b157389b6e6d5d5e9e59d1d1a23a5c66a99fd58339914d5", size = 250387, upload-time = "2025-08-23T14:41:22.644Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/89/97a9e271188c2fbb3db82235c33980bcbc733da7da6065afbaa1d685a169/coverage-7.10.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3f39cef43d08049e8afc1fde4a5da8510fc6be843f8dea350ee46e2a26b2f54c", size = 248280, upload-time = "2025-08-23T14:41:24.061Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/c6/0ad7d0137257553eb4706b4ad6180bec0a1b6a648b092c5bbda48d0e5b2c/coverage-7.10.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2968647e3ed5a6c019a419264386b013979ff1fb67dd11f5c9886c43d6a31fc2", size = 249894, upload-time = "2025-08-23T14:41:26.165Z" },
+ { url = "https://files.pythonhosted.org/packages/84/56/fb3aba936addb4c9e5ea14f5979393f1c2466b4c89d10591fd05f2d6b2aa/coverage-7.10.5-cp313-cp313-win32.whl", hash = "sha256:0d511dda38595b2b6934c2b730a1fd57a3635c6aa2a04cb74714cdfdd53846f4", size = 219536, upload-time = "2025-08-23T14:41:27.694Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/54/baacb8f2f74431e3b175a9a2881feaa8feb6e2f187a0e7e3046f3c7742b2/coverage-7.10.5-cp313-cp313-win_amd64.whl", hash = "sha256:9a86281794a393513cf117177fd39c796b3f8e3759bb2764259a2abba5cce54b", size = 220330, upload-time = "2025-08-23T14:41:29.081Z" },
+ { url = "https://files.pythonhosted.org/packages/64/8a/82a3788f8e31dee51d350835b23d480548ea8621f3effd7c3ba3f7e5c006/coverage-7.10.5-cp313-cp313-win_arm64.whl", hash = "sha256:cebd8e906eb98bb09c10d1feed16096700b1198d482267f8bf0474e63a7b8d84", size = 218961, upload-time = "2025-08-23T14:41:30.511Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/a1/590154e6eae07beee3b111cc1f907c30da6fc8ce0a83ef756c72f3c7c748/coverage-7.10.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0520dff502da5e09d0d20781df74d8189ab334a1e40d5bafe2efaa4158e2d9e7", size = 217819, upload-time = "2025-08-23T14:41:31.962Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/ff/436ffa3cfc7741f0973c5c89405307fe39b78dcf201565b934e6616fc4ad/coverage-7.10.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d9cd64aca68f503ed3f1f18c7c9174cbb797baba02ca8ab5112f9d1c0328cd4b", size = 218040, upload-time = "2025-08-23T14:41:33.472Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/ca/5787fb3d7820e66273913affe8209c534ca11241eb34ee8c4fd2aaa9dd87/coverage-7.10.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0913dd1613a33b13c4f84aa6e3f4198c1a21ee28ccb4f674985c1f22109f0aae", size = 259374, upload-time = "2025-08-23T14:41:34.914Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/89/21af956843896adc2e64fc075eae3c1cadb97ee0a6960733e65e696f32dd/coverage-7.10.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1b7181c0feeb06ed8a02da02792f42f829a7b29990fef52eff257fef0885d760", size = 261551, upload-time = "2025-08-23T14:41:36.333Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/96/390a69244ab837e0ac137989277879a084c786cf036c3c4a3b9637d43a89/coverage-7.10.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36d42b7396b605f774d4372dd9c49bed71cbabce4ae1ccd074d155709dd8f235", size = 263776, upload-time = "2025-08-23T14:41:38.25Z" },
+ { url = "https://files.pythonhosted.org/packages/00/32/cfd6ae1da0a521723349f3129b2455832fc27d3f8882c07e5b6fefdd0da2/coverage-7.10.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b4fdc777e05c4940b297bf47bf7eedd56a39a61dc23ba798e4b830d585486ca5", size = 261326, upload-time = "2025-08-23T14:41:40.343Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/c4/bf8d459fb4ce2201e9243ce6c015936ad283a668774430a3755f467b39d1/coverage-7.10.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:42144e8e346de44a6f1dbd0a56575dd8ab8dfa7e9007da02ea5b1c30ab33a7db", size = 259090, upload-time = "2025-08-23T14:41:42.106Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/5d/a234f7409896468e5539d42234016045e4015e857488b0b5b5f3f3fa5f2b/coverage-7.10.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:66c644cbd7aed8fe266d5917e2c9f65458a51cfe5eeff9c05f15b335f697066e", size = 260217, upload-time = "2025-08-23T14:41:43.591Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/ad/87560f036099f46c2ddd235be6476dd5c1d6be6bb57569a9348d43eeecea/coverage-7.10.5-cp313-cp313t-win32.whl", hash = "sha256:2d1b73023854068c44b0c554578a4e1ef1b050ed07cf8b431549e624a29a66ee", size = 220194, upload-time = "2025-08-23T14:41:45.051Z" },
+ { url = "https://files.pythonhosted.org/packages/36/a8/04a482594fdd83dc677d4a6c7e2d62135fff5a1573059806b8383fad9071/coverage-7.10.5-cp313-cp313t-win_amd64.whl", hash = "sha256:54a1532c8a642d8cc0bd5a9a51f5a9dcc440294fd06e9dda55e743c5ec1a8f14", size = 221258, upload-time = "2025-08-23T14:41:46.44Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/ad/7da28594ab66fe2bc720f1bc9b131e62e9b4c6e39f044d9a48d18429cc21/coverage-7.10.5-cp313-cp313t-win_arm64.whl", hash = "sha256:74d5b63fe3f5f5d372253a4ef92492c11a4305f3550631beaa432fc9df16fcff", size = 219521, upload-time = "2025-08-23T14:41:47.882Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/7f/c8b6e4e664b8a95254c35a6c8dd0bf4db201ec681c169aae2f1256e05c85/coverage-7.10.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:68c5e0bc5f44f68053369fa0d94459c84548a77660a5f2561c5e5f1e3bed7031", size = 217090, upload-time = "2025-08-23T14:41:49.327Z" },
+ { url = "https://files.pythonhosted.org/packages/44/74/3ee14ede30a6e10a94a104d1d0522d5fb909a7c7cac2643d2a79891ff3b9/coverage-7.10.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cf33134ffae93865e32e1e37df043bef15a5e857d8caebc0099d225c579b0fa3", size = 217365, upload-time = "2025-08-23T14:41:50.796Z" },
+ { url = "https://files.pythonhosted.org/packages/41/5f/06ac21bf87dfb7620d1f870dfa3c2cae1186ccbcdc50b8b36e27a0d52f50/coverage-7.10.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ad8fa9d5193bafcf668231294241302b5e683a0518bf1e33a9a0dfb142ec3031", size = 248413, upload-time = "2025-08-23T14:41:52.5Z" },
+ { url = "https://files.pythonhosted.org/packages/21/bc/cc5bed6e985d3a14228539631573f3863be6a2587381e8bc5fdf786377a1/coverage-7.10.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:146fa1531973d38ab4b689bc764592fe6c2f913e7e80a39e7eeafd11f0ef6db2", size = 250943, upload-time = "2025-08-23T14:41:53.922Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/43/6a9fc323c2c75cd80b18d58db4a25dc8487f86dd9070f9592e43e3967363/coverage-7.10.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6013a37b8a4854c478d3219ee8bc2392dea51602dd0803a12d6f6182a0061762", size = 252301, upload-time = "2025-08-23T14:41:56.528Z" },
+ { url = "https://files.pythonhosted.org/packages/69/7c/3e791b8845f4cd515275743e3775adb86273576596dc9f02dca37357b4f2/coverage-7.10.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:eb90fe20db9c3d930fa2ad7a308207ab5b86bf6a76f54ab6a40be4012d88fcae", size = 250302, upload-time = "2025-08-23T14:41:58.171Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/bc/5099c1e1cb0c9ac6491b281babea6ebbf999d949bf4aa8cdf4f2b53505e8/coverage-7.10.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:384b34482272e960c438703cafe63316dfbea124ac62006a455c8410bf2a2262", size = 248237, upload-time = "2025-08-23T14:41:59.703Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/51/d346eb750a0b2f1e77f391498b753ea906fde69cc11e4b38dca28c10c88c/coverage-7.10.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:467dc74bd0a1a7de2bedf8deaf6811f43602cb532bd34d81ffd6038d6d8abe99", size = 249726, upload-time = "2025-08-23T14:42:01.343Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/85/eebcaa0edafe427e93286b94f56ea7e1280f2c49da0a776a6f37e04481f9/coverage-7.10.5-cp314-cp314-win32.whl", hash = "sha256:556d23d4e6393ca898b2e63a5bca91e9ac2d5fb13299ec286cd69a09a7187fde", size = 219825, upload-time = "2025-08-23T14:42:03.263Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/f7/6d43e037820742603f1e855feb23463979bf40bd27d0cde1f761dcc66a3e/coverage-7.10.5-cp314-cp314-win_amd64.whl", hash = "sha256:f4446a9547681533c8fa3e3c6cf62121eeee616e6a92bd9201c6edd91beffe13", size = 220618, upload-time = "2025-08-23T14:42:05.037Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/b0/ed9432e41424c51509d1da603b0393404b828906236fb87e2c8482a93468/coverage-7.10.5-cp314-cp314-win_arm64.whl", hash = "sha256:5e78bd9cf65da4c303bf663de0d73bf69f81e878bf72a94e9af67137c69b9fe9", size = 219199, upload-time = "2025-08-23T14:42:06.662Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/54/5a7ecfa77910f22b659c820f67c16fc1e149ed132ad7117f0364679a8fa9/coverage-7.10.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5661bf987d91ec756a47c7e5df4fbcb949f39e32f9334ccd3f43233bbb65e508", size = 217833, upload-time = "2025-08-23T14:42:08.262Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/0e/25672d917cc57857d40edf38f0b867fb9627115294e4f92c8fcbbc18598d/coverage-7.10.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a46473129244db42a720439a26984f8c6f834762fc4573616c1f37f13994b357", size = 218048, upload-time = "2025-08-23T14:42:10.247Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/7c/0b2b4f1c6f71885d4d4b2b8608dcfc79057adb7da4143eb17d6260389e42/coverage-7.10.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1f64b8d3415d60f24b058b58d859e9512624bdfa57a2d1f8aff93c1ec45c429b", size = 259549, upload-time = "2025-08-23T14:42:11.811Z" },
+ { url = "https://files.pythonhosted.org/packages/94/73/abb8dab1609abec7308d83c6aec547944070526578ee6c833d2da9a0ad42/coverage-7.10.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:44d43de99a9d90b20e0163f9770542357f58860a26e24dc1d924643bd6aa7cb4", size = 261715, upload-time = "2025-08-23T14:42:13.505Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/d1/abf31de21ec92731445606b8d5e6fa5144653c2788758fcf1f47adb7159a/coverage-7.10.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a931a87e5ddb6b6404e65443b742cb1c14959622777f2a4efd81fba84f5d91ba", size = 263969, upload-time = "2025-08-23T14:42:15.422Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/b3/ef274927f4ebede96056173b620db649cc9cb746c61ffc467946b9d0bc67/coverage-7.10.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9559b906a100029274448f4c8b8b0a127daa4dade5661dfd821b8c188058842", size = 261408, upload-time = "2025-08-23T14:42:16.971Z" },
+ { url = "https://files.pythonhosted.org/packages/20/fc/83ca2812be616d69b4cdd4e0c62a7bc526d56875e68fd0f79d47c7923584/coverage-7.10.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b08801e25e3b4526ef9ced1aa29344131a8f5213c60c03c18fe4c6170ffa2874", size = 259168, upload-time = "2025-08-23T14:42:18.512Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/4f/e0779e5716f72d5c9962e709d09815d02b3b54724e38567308304c3fc9df/coverage-7.10.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ed9749bb8eda35f8b636fb7632f1c62f735a236a5d4edadd8bbcc5ea0542e732", size = 260317, upload-time = "2025-08-23T14:42:20.005Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/fe/4247e732f2234bb5eb9984a0888a70980d681f03cbf433ba7b48f08ca5d5/coverage-7.10.5-cp314-cp314t-win32.whl", hash = "sha256:609b60d123fc2cc63ccee6d17e4676699075db72d14ac3c107cc4976d516f2df", size = 220600, upload-time = "2025-08-23T14:42:22.027Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/a0/f294cff6d1034b87839987e5b6ac7385bec599c44d08e0857ac7f164ad0c/coverage-7.10.5-cp314-cp314t-win_amd64.whl", hash = "sha256:0666cf3d2c1626b5a3463fd5b05f5e21f99e6aec40a3192eee4d07a15970b07f", size = 221714, upload-time = "2025-08-23T14:42:23.616Z" },
+ { url = "https://files.pythonhosted.org/packages/23/18/fa1afdc60b5528d17416df440bcbd8fd12da12bfea9da5b6ae0f7a37d0f7/coverage-7.10.5-cp314-cp314t-win_arm64.whl", hash = "sha256:bc85eb2d35e760120540afddd3044a5bf69118a91a296a8b3940dfc4fdcfe1e2", size = 219735, upload-time = "2025-08-23T14:42:25.156Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/21/05248e8bc74683488cb7477e6b6b878decadd15af0ec96f56381d3d7ff2d/coverage-7.10.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:62835c1b00c4a4ace24c1a88561a5a59b612fbb83a525d1c70ff5720c97c0610", size = 216763, upload-time = "2025-08-23T14:42:26.75Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/7f/161a0ad40cb1c7e19dc1aae106d3430cc88dac3d651796d6cf3f3730c800/coverage-7.10.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5255b3bbcc1d32a4069d6403820ac8e6dbcc1d68cb28a60a1ebf17e47028e898", size = 217154, upload-time = "2025-08-23T14:42:28.238Z" },
+ { url = "https://files.pythonhosted.org/packages/de/31/41929ee53af829ea5a88e71d335ea09d0bb587a3da1c5e58e59b48473ed8/coverage-7.10.5-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3876385722e335d6e991c430302c24251ef9c2a9701b2b390f5473199b1b8ebf", size = 243588, upload-time = "2025-08-23T14:42:29.798Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/4e/2649344e33eeb3567041e8255a1942173cae81817fe06b60f3fafaafe111/coverage-7.10.5-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8048ce4b149c93447a55d279078c8ae98b08a6951a3c4d2d7e87f4efc7bfe100", size = 245412, upload-time = "2025-08-23T14:42:31.296Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/b1/b21e1e69986ad89b051dd42c3ef06d9326e03ac3c0c844fc33385d1d9e35/coverage-7.10.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4028e7558e268dd8bcf4d9484aad393cafa654c24b4885f6f9474bf53183a82a", size = 247182, upload-time = "2025-08-23T14:42:33.155Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/b5/80837be411ae092e03fcc2a7877bd9a659c531eff50453e463057a9eee44/coverage-7.10.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:03f47dc870eec0367fcdd603ca6a01517d2504e83dc18dbfafae37faec66129a", size = 245066, upload-time = "2025-08-23T14:42:34.754Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/ed/fcb0838ddf149d68d09f89af57397b0dd9d26b100cc729daf1b0caf0b2d3/coverage-7.10.5-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2d488d7d42b6ded7ea0704884f89dcabd2619505457de8fc9a6011c62106f6e5", size = 243138, upload-time = "2025-08-23T14:42:36.311Z" },
+ { url = "https://files.pythonhosted.org/packages/75/0f/505c6af24a9ae5d8919d209b9c31b7092815f468fa43bec3b1118232c62a/coverage-7.10.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b3dcf2ead47fa8be14224ee817dfc1df98043af568fe120a22f81c0eb3c34ad2", size = 244095, upload-time = "2025-08-23T14:42:38.227Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/7e/c82a8bede46217c1d944bd19b65e7106633b998640f00ab49c5f747a5844/coverage-7.10.5-cp39-cp39-win32.whl", hash = "sha256:02650a11324b80057b8c9c29487020073d5e98a498f1857f37e3f9b6ea1b2426", size = 219289, upload-time = "2025-08-23T14:42:39.827Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/ac/46645ef6be543f2e7de08cc2601a0b67e130c816be3b749ab741be689fb9/coverage-7.10.5-cp39-cp39-win_amd64.whl", hash = "sha256:b45264dd450a10f9e03237b41a9a24e85cbb1e278e5a32adb1a303f58f0017f3", size = 220199, upload-time = "2025-08-23T14:42:41.363Z" },
+ { url = "https://files.pythonhosted.org/packages/08/b6/fff6609354deba9aeec466e4bcaeb9d1ed3e5d60b14b57df2a36fb2273f2/coverage-7.10.5-py3-none-any.whl", hash = "sha256:0be24d35e4db1d23d0db5c0f6a74a962e2ec83c426b5cac09f4234aadef38e4a", size = 208736, upload-time = "2025-08-23T14:42:43.145Z" },
+]
+
+[package.optional-dependencies]
+toml = [
+ { name = "tomli", marker = "python_full_version <= '3.11'" },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" },
+]
+
+[[package]]
+name = "fieldz"
+version = "0.1.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/90/62/698c5cc2e7d4c8c89e63033e2e9d3c74902a1bf28782712eacb0653097ce/fieldz-0.1.2.tar.gz", hash = "sha256:0448ed5dacb13eaa49da0db786e87fae298fbd2652d26c510e5d7aea6b6bebf4", size = 17277, upload-time = "2025-06-30T18:06:40.881Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6d/8c/8958392cade27a272daf45d09a08473073dedeccad94b097dfeb898d969f/fieldz-0.1.2-py3-none-any.whl", hash = "sha256:e25884d2821a2d5638ef8d4d8bce5d1039359cfcb46d0f93df8cb1f7c2eb3a2e", size = 17878, upload-time = "2025-06-30T18:06:39.322Z" },
+]
+
+[[package]]
+name = "ghp-import"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "python-dateutil" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" },
+]
+
+[[package]]
+name = "gitdb"
+version = "4.0.12"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "smmap" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" },
+]
+
+[[package]]
+name = "gitpython"
+version = "3.1.45"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "gitdb" },
+ { name = "typing-extensions", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076, upload-time = "2025-07-24T03:45:54.871Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168, upload-time = "2025-07-24T03:45:52.517Z" },
+]
+
+[[package]]
+name = "griffe"
+version = "1.13.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c6/b5/23b91f22b7b3a7f8f62223f6664946271c0f5cb4179605a3e6bbae863920/griffe-1.13.0.tar.gz", hash = "sha256:246ea436a5e78f7fbf5f24ca8a727bb4d2a4b442a2959052eea3d0bfe9a076e0", size = 412759, upload-time = "2025-08-26T13:27:11.422Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/aa/8c/b7cfdd8dfe48f6b09f7353323732e1a290c388bd14f216947928dc85f904/griffe-1.13.0-py3-none-any.whl", hash = "sha256:470fde5b735625ac0a36296cd194617f039e9e83e301fcbd493e2b58382d0559", size = 139365, upload-time = "2025-08-26T13:27:09.882Z" },
+]
+
+[[package]]
+name = "griffe-fieldz"
+version = "0.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "fieldz" },
+ { name = "griffe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c5/6a/94754bf39fd63ba424c667b2abf0ade78e3878e223591d1fb9c3e8a77bce/griffe_fieldz-0.3.0.tar.gz", hash = "sha256:42e7707dac51d38e26fb7f3f7f51429da9b47e98060bfeb81a4287456d5b8a89", size = 10149, upload-time = "2025-07-30T21:43:10.042Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4d/33/cc527c11132a6274724a04938d50e1ff2b54a5f5943cd0480427571e1adb/griffe_fieldz-0.3.0-py3-none-any.whl", hash = "sha256:52e02fdcbdf6dea3c8c95756d1e0b30861569f871d19437fda702776fde4e64d", size = 6577, upload-time = "2025-07-30T21:43:09.073Z" },
+]
+
+[[package]]
+name = "harp-protocol"
+version = "0.3.0"
+source = { editable = "src/harp-protocol" }
+
+[[package]]
+name = "harp-serial"
+version = "0.3.0"
+source = { editable = "src/harp-serial" }
+dependencies = [
+ { name = "harp-protocol" },
+ { name = "pyserial" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "harp-protocol", editable = "src/harp-protocol" },
+ { name = "pyserial", specifier = ">=3.5" },
+]
+
+[[package]]
+name = "idna"
+version = "3.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
+]
+
+[[package]]
+name = "importlib-metadata"
+version = "8.7.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "zipp" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
+]
+
+[[package]]
+name = "jinja2"
+version = "3.1.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
+]
+
+[[package]]
+name = "markdown"
+version = "3.8.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "importlib-metadata", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload-time = "2025-06-19T17:12:44.483Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" },
+]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" },
+ { 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, upload-time = "2024-10-18T15:20:52.426Z" },
+ { 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, upload-time = "2024-10-18T15:20:53.578Z" },
+ { 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, upload-time = "2024-10-18T15:20:55.06Z" },
+ { 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, upload-time = "2024-10-18T15:20:55.906Z" },
+ { 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, upload-time = "2024-10-18T15:20:57.189Z" },
+ { 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, upload-time = "2024-10-18T15:20:58.235Z" },
+ { 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, upload-time = "2024-10-18T15:20:59.235Z" },
+ { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" },
+ { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" },
+ { 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, upload-time = "2024-10-18T15:21:02.187Z" },
+ { 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, upload-time = "2024-10-18T15:21:02.941Z" },
+ { 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, upload-time = "2024-10-18T15:21:03.953Z" },
+ { 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, upload-time = "2024-10-18T15:21:06.495Z" },
+ { 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, upload-time = "2024-10-18T15:21:07.295Z" },
+ { 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, upload-time = "2024-10-18T15:21:08.073Z" },
+ { 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, upload-time = "2024-10-18T15:21:09.318Z" },
+ { 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, upload-time = "2024-10-18T15:21:10.185Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" },
+ { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" },
+ { 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, upload-time = "2024-10-18T15:21:13.777Z" },
+ { 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, upload-time = "2024-10-18T15:21:14.822Z" },
+ { 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, upload-time = "2024-10-18T15:21:15.642Z" },
+ { 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, upload-time = "2024-10-18T15:21:17.133Z" },
+ { 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, upload-time = "2024-10-18T15:21:18.064Z" },
+ { 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, upload-time = "2024-10-18T15:21:18.859Z" },
+ { 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, upload-time = "2024-10-18T15:21:19.671Z" },
+ { 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, upload-time = "2024-10-18T15:21:20.971Z" },
+ { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" },
+ { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" },
+ { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" },
+ { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" },
+ { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" },
+ { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" },
+ { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344, upload-time = "2024-10-18T15:21:43.721Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389, upload-time = "2024-10-18T15:21:44.666Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607, upload-time = "2024-10-18T15:21:45.452Z" },
+ { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728, upload-time = "2024-10-18T15:21:46.295Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826, upload-time = "2024-10-18T15:21:47.134Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843, upload-time = "2024-10-18T15:21:48.334Z" },
+ { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219, upload-time = "2024-10-18T15:21:49.587Z" },
+ { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946, upload-time = "2024-10-18T15:21:50.441Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063, upload-time = "2024-10-18T15:21:51.385Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506, upload-time = "2024-10-18T15:21:52.974Z" },
+]
+
+[[package]]
+name = "mergedeep"
+version = "1.3.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" },
+]
+
+[[package]]
+name = "mkdocs"
+version = "1.6.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "ghp-import" },
+ { name = "importlib-metadata", marker = "python_full_version < '3.10'" },
+ { name = "jinja2" },
+ { name = "markdown" },
+ { name = "markupsafe" },
+ { name = "mergedeep" },
+ { name = "mkdocs-get-deps" },
+ { name = "packaging" },
+ { name = "pathspec" },
+ { name = "pyyaml" },
+ { name = "pyyaml-env-tag" },
+ { name = "watchdog" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" },
+]
+
+[[package]]
+name = "mkdocs-autorefs"
+version = "1.4.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown" },
+ { name = "markupsafe" },
+ { name = "mkdocs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/51/fa/9124cd63d822e2bcbea1450ae68cdc3faf3655c69b455f3a7ed36ce6c628/mkdocs_autorefs-1.4.3.tar.gz", hash = "sha256:beee715b254455c4aa93b6ef3c67579c399ca092259cc41b7d9342573ff1fc75", size = 55425, upload-time = "2025-08-26T14:23:17.223Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9f/4d/7123b6fa2278000688ebd338e2a06d16870aaf9eceae6ba047ea05f92df1/mkdocs_autorefs-1.4.3-py3-none-any.whl", hash = "sha256:469d85eb3114801d08e9cc55d102b3ba65917a869b893403b8987b601cf55dc9", size = 25034, upload-time = "2025-08-26T14:23:15.906Z" },
+]
+
+[[package]]
+name = "mkdocs-codeinclude-plugin"
+version = "0.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mkdocs" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/1b/b5/f72df157abc7f85e33ffa417464e9dd535ef5fda7654eda41190047a53b6/mkdocs-codeinclude-plugin-0.2.1.tar.gz", hash = "sha256:305387f67a885f0e36ec1cf977324fe1fe50d31301147194b63631d0864601b1", size = 8140, upload-time = "2023-03-01T19:57:06.724Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4d/7b/60573ebf2a22b144eeaf3b29db9a6d4d342d68273f716ea2723d1ad723ba/mkdocs_codeinclude_plugin-0.2.1-py3-none-any.whl", hash = "sha256:172a917c9b257fa62850b669336151f85d3cd40312b2b52520cbcceab557ea6c", size = 8093, upload-time = "2023-03-01T19:57:05.207Z" },
+]
+
+[[package]]
+name = "mkdocs-get-deps"
+version = "0.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "importlib-metadata", marker = "python_full_version < '3.10'" },
+ { name = "mergedeep" },
+ { name = "platformdirs" },
+ { name = "pyyaml" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" },
+]
+
+[[package]]
+name = "mkdocs-git-authors-plugin"
+version = "0.10.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mkdocs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/64/f1/b784c631b812aab80030db80127a576b68a84caac5229836fb7fcc00e055/mkdocs_git_authors_plugin-0.10.0.tar.gz", hash = "sha256:29d1973b2835663d79986fb756e02f1f0ff3fe35c278e993206bd3c550c205e4", size = 23432, upload-time = "2025-06-10T05:42:40.94Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/41/bc/a4166201c2789657c4d370bfcd71a5107edec185ae245675c8b9a6719243/mkdocs_git_authors_plugin-0.10.0-py3-none-any.whl", hash = "sha256:28421a99c3e872a8e205674bb80ec48524838243e5f59eaf9bd97df103e38901", size = 21899, upload-time = "2025-06-10T05:42:39.244Z" },
+]
+
+[[package]]
+name = "mkdocs-git-committers-plugin-2"
+version = "2.5.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "gitpython" },
+ { name = "mkdocs" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b4/8a/4ca4fb7d17f66fa709b49744c597204ad03fb3b011c76919564843426f11/mkdocs_git_committers_plugin_2-2.5.0.tar.gz", hash = "sha256:a01f17369e79ca28651681cddf212770e646e6191954bad884ca3067316aae60", size = 15183, upload-time = "2025-01-30T07:30:48.667Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8e/f5/768590251839a148c188d64779b809bde0e78a306295c18fc29d7fc71ce1/mkdocs_git_committers_plugin_2-2.5.0-py3-none-any.whl", hash = "sha256:1778becf98ccdc5fac809ac7b62cf01d3c67d6e8432723dffbb823307d1193c4", size = 11788, upload-time = "2025-01-30T07:30:45.748Z" },
+]
+
+[[package]]
+name = "mkdocs-include-markdown-plugin"
+version = "7.1.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mkdocs" },
+ { name = "wcmatch" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2c/17/988d97ac6849b196f54d45ca9c60ca894880c160a512785f03834704b3d9/mkdocs_include_markdown_plugin-7.1.6.tar.gz", hash = "sha256:a0753cb82704c10a287f1e789fc9848f82b6beb8749814b24b03dd9f67816677", size = 23391, upload-time = "2025-06-13T18:25:51.193Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e2/a1/6cf1667a05e5f468e1263fcf848772bca8cc9e358cd57ae19a01f92c9f6f/mkdocs_include_markdown_plugin-7.1.6-py3-none-any.whl", hash = "sha256:7975a593514887c18ecb68e11e35c074c5499cfa3e51b18cd16323862e1f7345", size = 27161, upload-time = "2025-06-13T18:25:49.847Z" },
+]
+
+[[package]]
+name = "mkdocs-material"
+version = "9.6.18"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "babel" },
+ { name = "backrefs" },
+ { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "colorama" },
+ { name = "jinja2" },
+ { name = "markdown" },
+ { name = "mkdocs" },
+ { name = "mkdocs-material-extensions" },
+ { name = "paginate" },
+ { name = "pygments" },
+ { name = "pymdown-extensions" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e6/46/db0d78add5aac29dfcd0a593bcc6049c86c77ba8a25b3a5b681c190d5e99/mkdocs_material-9.6.18.tar.gz", hash = "sha256:a2eb253bcc8b66f8c6eaf8379c10ed6e9644090c2e2e9d0971c7722dc7211c05", size = 4034856, upload-time = "2025-08-22T08:21:47.575Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/22/0b/545a4f8d4f9057e77f1d99640eb09aaae40c4f9034707f25636caf716ff9/mkdocs_material-9.6.18-py3-none-any.whl", hash = "sha256:dbc1e146a0ecce951a4d84f97b816a54936cdc9e1edd1667fc6868878ac06701", size = 9232642, upload-time = "2025-08-22T08:21:44.52Z" },
+]
+
+[[package]]
+name = "mkdocs-material-extensions"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" },
+]
+
+[[package]]
+name = "mkdocs-monorepo-plugin"
+version = "1.1.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mkdocs" },
+ { name = "python-slugify" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d4/6a/a75245020e44beb9d7c806158f8a2cda37597711409d40c5a37c70078a7e/mkdocs-monorepo-plugin-1.1.2.tar.gz", hash = "sha256:09200bcf837ad35070e6da973aa0cb682e69ed6e16f254a30584550c6d2d8ebb", size = 13723, upload-time = "2025-06-05T19:09:45.042Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/57/26/4f4c19457d1d4e6d571a3b092921b7a0ce9477d18d997755ac615d72b96b/mkdocs_monorepo_plugin-1.1.2-py3-none-any.whl", hash = "sha256:4b917bc224b89e34e1736bb31ad5ae9deb0a907da879e03bb9454b41fb8b1cac", size = 14539, upload-time = "2025-06-05T19:09:43.74Z" },
+]
+
+[[package]]
+name = "mkdocstrings"
+version = "0.30.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "importlib-metadata", marker = "python_full_version < '3.10'" },
+ { name = "jinja2" },
+ { name = "markdown" },
+ { name = "markupsafe" },
+ { name = "mkdocs" },
+ { name = "mkdocs-autorefs" },
+ { name = "pymdown-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e2/0a/7e4776217d4802009c8238c75c5345e23014a4706a8414a62c0498858183/mkdocstrings-0.30.0.tar.gz", hash = "sha256:5d8019b9c31ddacd780b6784ffcdd6f21c408f34c0bd1103b5351d609d5b4444", size = 106597, upload-time = "2025-07-22T23:48:45.998Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/de/b4/3c5eac68f31e124a55d255d318c7445840fa1be55e013f507556d6481913/mkdocstrings-0.30.0-py3-none-any.whl", hash = "sha256:ae9e4a0d8c1789697ac776f2e034e2ddd71054ae1cf2c2bb1433ccfd07c226f2", size = 36579, upload-time = "2025-07-22T23:48:44.152Z" },
+]
+
+[[package]]
+name = "mkdocstrings-python"
+version = "1.18.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "griffe" },
+ { name = "mkdocs-autorefs" },
+ { name = "mkdocstrings" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/13/d4/6327c4e82dda667b0ff83b6f6b6a03e7b81dfd1f28cd5eda50ffe66d546f/mkdocstrings_python-1.18.0.tar.gz", hash = "sha256:0b9924b4034fe9ae43604d78fe8e5107ea2c2391620124fc833043a62e83c744", size = 207601, upload-time = "2025-08-26T14:02:30.839Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5d/96/7ecc71bb9f01ee20f201b2531960b401159c6730aec90ec76a1b74bc81e1/mkdocstrings_python-1.18.0-py3-none-any.whl", hash = "sha256:f5056d8afe9a9683ad0c59001df1ecd9668b51c19b9a6b4dc0ff02cc9b76265a", size = 138182, upload-time = "2025-08-26T14:02:28.076Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "25.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
+]
+
+[[package]]
+name = "paginate"
+version = "0.5.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" },
+]
+
+[[package]]
+name = "pathspec"
+version = "0.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
+]
+
+[[package]]
+name = "pyharp"
+version = "0.2.0"
+source = { virtual = "." }
+dependencies = [
+ { name = "harp-protocol" },
+ { name = "harp-serial" },
+]
+
+[package.dev-dependencies]
+dev = [
+ { name = "griffe-fieldz" },
+ { name = "mkdocs" },
+ { name = "mkdocs-codeinclude-plugin" },
+ { name = "mkdocs-git-authors-plugin" },
+ { name = "mkdocs-git-committers-plugin-2" },
+ { name = "mkdocs-include-markdown-plugin" },
+ { name = "mkdocs-material" },
+ { name = "mkdocs-monorepo-plugin" },
+ { name = "mkdocstrings-python" },
+ { name = "pytest" },
+ { name = "pytest-cov" },
+ { name = "ruff" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "harp-protocol", editable = "src/harp-protocol" },
+ { name = "harp-serial", editable = "src/harp-serial" },
+]
+
+[package.metadata.requires-dev]
+dev = [
+ { name = "griffe-fieldz", specifier = ">=0.2.1" },
+ { name = "mkdocs", specifier = ">=1.6.1" },
+ { name = "mkdocs-codeinclude-plugin", specifier = ">=0.2.1" },
+ { name = "mkdocs-git-authors-plugin", specifier = ">=0.9.4" },
+ { name = "mkdocs-git-committers-plugin-2", specifier = ">=2.5.0" },
+ { name = "mkdocs-include-markdown-plugin", specifier = ">=7.1.6" },
+ { name = "mkdocs-material", specifier = ">=9.6.9" },
+ { name = "mkdocs-monorepo-plugin", specifier = ">=1.1.2" },
+ { name = "mkdocstrings-python", specifier = ">=1.16.6" },
+ { name = "pytest", specifier = ">=8.3.5" },
+ { name = "pytest-cov", specifier = ">=6.1.1" },
+ { name = "ruff", specifier = ">=0.11.0" },
+]
+
+[[package]]
+name = "pymdown-extensions"
+version = "10.16.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown" },
+ { name = "pyyaml" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/55/b3/6d2b3f149bc5413b0a29761c2c5832d8ce904a1d7f621e86616d96f505cc/pymdown_extensions-10.16.1.tar.gz", hash = "sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91", size = 853277, upload-time = "2025-07-28T16:19:34.167Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e4/06/43084e6cbd4b3bc0e80f6be743b2e79fbc6eed8de9ad8c629939fa55d972/pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d", size = 266178, upload-time = "2025-07-28T16:19:31.401Z" },
+]
+
+[[package]]
+name = "pyserial"
+version = "3.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1e/7d/ae3f0a63f41e4d2f6cb66a5b57197850f919f59e558159a4dd3a818f5082/pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", size = 159125, upload-time = "2020-11-23T03:59:15.045Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0", size = 90585, upload-time = "2020-11-23T03:59:13.41Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "8.4.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" },
+]
+
+[[package]]
+name = "pytest-cov"
+version = "6.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "coverage", extra = ["toml"] },
+ { name = "pluggy" },
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" },
+]
+
+[[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, upload-time = "2024-03-01T18:36:20.211Z" }
+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, upload-time = "2024-03-01T18:36:18.57Z" },
+]
+
+[[package]]
+name = "python-slugify"
+version = "8.0.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "text-unidecode" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856", size = 10921, upload-time = "2024-02-08T18:32:45.488Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051, upload-time = "2024-02-08T18:32:43.911Z" },
+]
+
+[[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, upload-time = "2024-08-06T20:33:50.674Z" }
+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, upload-time = "2024-08-06T20:31:40.178Z" },
+ { 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, upload-time = "2024-08-06T20:31:42.173Z" },
+ { 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, upload-time = "2024-08-06T20:31:44.263Z" },
+ { 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, upload-time = "2024-08-06T20:31:50.199Z" },
+ { 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, upload-time = "2024-08-06T20:31:52.292Z" },
+ { 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, upload-time = "2024-08-06T20:31:53.836Z" },
+ { 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, upload-time = "2024-08-06T20:31:55.565Z" },
+ { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" },
+ { 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, upload-time = "2024-08-06T20:32:03.408Z" },
+ { 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, upload-time = "2024-08-06T20:32:04.926Z" },
+ { 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, upload-time = "2024-08-06T20:32:06.459Z" },
+ { 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, upload-time = "2024-08-06T20:32:08.338Z" },
+ { 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, upload-time = "2024-08-06T20:32:14.124Z" },
+ { 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, upload-time = "2024-08-06T20:32:16.17Z" },
+ { 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, upload-time = "2024-08-06T20:32:18.555Z" },
+ { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" },
+ { 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, upload-time = "2024-08-06T20:32:25.131Z" },
+ { 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, upload-time = "2024-08-06T20:32:26.511Z" },
+ { 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, upload-time = "2024-08-06T20:32:28.363Z" },
+ { 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, upload-time = "2024-08-06T20:32:30.058Z" },
+ { 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, upload-time = "2024-08-06T20:32:31.881Z" },
+ { 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, upload-time = "2024-08-06T20:32:37.083Z" },
+ { 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, upload-time = "2024-08-06T20:32:38.898Z" },
+ { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" },
+ { 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, upload-time = "2024-08-06T20:32:43.4Z" },
+ { 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, upload-time = "2024-08-06T20:32:44.801Z" },
+ { 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, upload-time = "2024-08-06T20:32:46.432Z" },
+ { 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, upload-time = "2024-08-06T20:32:51.188Z" },
+ { 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, upload-time = "2024-08-06T20:32:53.019Z" },
+ { 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, upload-time = "2024-08-06T20:32:54.708Z" },
+ { 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, upload-time = "2024-08-06T20:32:56.985Z" },
+ { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
+ { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777, upload-time = "2024-08-06T20:33:25.896Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318, upload-time = "2024-08-06T20:33:27.212Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891, upload-time = "2024-08-06T20:33:28.974Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614, upload-time = "2024-08-06T20:33:34.157Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360, upload-time = "2024-08-06T20:33:35.84Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006, upload-time = "2024-08-06T20:33:37.501Z" },
+ { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577, upload-time = "2024-08-06T20:33:39.389Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593, upload-time = "2024-08-06T20:33:46.63Z" },
+ { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload-time = "2024-08-06T20:33:49.073Z" },
+]
+
+[[package]]
+name = "pyyaml-env-tag"
+version = "1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pyyaml" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" },
+]
+
+[[package]]
+name = "requests"
+version = "2.32.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
+]
+
+[[package]]
+name = "ruff"
+version = "0.12.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/3b/eb/8c073deb376e46ae767f4961390d17545e8535921d2f65101720ed8bd434/ruff-0.12.10.tar.gz", hash = "sha256:189ab65149d11ea69a2d775343adf5f49bb2426fc4780f65ee33b423ad2e47f9", size = 5310076, upload-time = "2025-08-21T18:23:22.595Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/24/e7/560d049d15585d6c201f9eeacd2fd130def3741323e5ccf123786e0e3c95/ruff-0.12.10-py3-none-linux_armv6l.whl", hash = "sha256:8b593cb0fb55cc8692dac7b06deb29afda78c721c7ccfed22db941201b7b8f7b", size = 11935161, upload-time = "2025-08-21T18:22:26.965Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/b0/ad2464922a1113c365d12b8f80ed70fcfb39764288ac77c995156080488d/ruff-0.12.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ebb7333a45d56efc7c110a46a69a1b32365d5c5161e7244aaf3aa20ce62399c1", size = 12660884, upload-time = "2025-08-21T18:22:30.925Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/f1/97f509b4108d7bae16c48389f54f005b62ce86712120fd8b2d8e88a7cb49/ruff-0.12.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d59e58586829f8e4a9920788f6efba97a13d1fa320b047814e8afede381c6839", size = 11872754, upload-time = "2025-08-21T18:22:34.035Z" },
+ { url = "https://files.pythonhosted.org/packages/12/ad/44f606d243f744a75adc432275217296095101f83f966842063d78eee2d3/ruff-0.12.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:822d9677b560f1fdeab69b89d1f444bf5459da4aa04e06e766cf0121771ab844", size = 12092276, upload-time = "2025-08-21T18:22:36.764Z" },
+ { url = "https://files.pythonhosted.org/packages/06/1f/ed6c265e199568010197909b25c896d66e4ef2c5e1c3808caf461f6f3579/ruff-0.12.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:37b4a64f4062a50c75019c61c7017ff598cb444984b638511f48539d3a1c98db", size = 11734700, upload-time = "2025-08-21T18:22:39.822Z" },
+ { url = "https://files.pythonhosted.org/packages/63/c5/b21cde720f54a1d1db71538c0bc9b73dee4b563a7dd7d2e404914904d7f5/ruff-0.12.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c6f4064c69d2542029b2a61d39920c85240c39837599d7f2e32e80d36401d6e", size = 13468783, upload-time = "2025-08-21T18:22:42.559Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9e/39369e6ac7f2a1848f22fb0b00b690492f20811a1ac5c1fd1d2798329263/ruff-0.12.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:059e863ea3a9ade41407ad71c1de2badfbe01539117f38f763ba42a1206f7559", size = 14436642, upload-time = "2025-08-21T18:22:45.612Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/03/5da8cad4b0d5242a936eb203b58318016db44f5c5d351b07e3f5e211bb89/ruff-0.12.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bef6161e297c68908b7218fa6e0e93e99a286e5ed9653d4be71e687dff101cf", size = 13859107, upload-time = "2025-08-21T18:22:48.886Z" },
+ { url = "https://files.pythonhosted.org/packages/19/19/dd7273b69bf7f93a070c9cec9494a94048325ad18fdcf50114f07e6bf417/ruff-0.12.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4f1345fbf8fb0531cd722285b5f15af49b2932742fc96b633e883da8d841896b", size = 12886521, upload-time = "2025-08-21T18:22:51.567Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/1d/b4207ec35e7babaee62c462769e77457e26eb853fbdc877af29417033333/ruff-0.12.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f68433c4fbc63efbfa3ba5db31727db229fa4e61000f452c540474b03de52a9", size = 13097528, upload-time = "2025-08-21T18:22:54.609Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/00/58f7b873b21114456e880b75176af3490d7a2836033779ca42f50de3b47a/ruff-0.12.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:141ce3d88803c625257b8a6debf4a0473eb6eed9643a6189b68838b43e78165a", size = 13080443, upload-time = "2025-08-21T18:22:57.413Z" },
+ { url = "https://files.pythonhosted.org/packages/12/8c/9e6660007fb10189ccb78a02b41691288038e51e4788bf49b0a60f740604/ruff-0.12.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f3fc21178cd44c98142ae7590f42ddcb587b8e09a3b849cbc84edb62ee95de60", size = 11896759, upload-time = "2025-08-21T18:23:00.473Z" },
+ { url = "https://files.pythonhosted.org/packages/67/4c/6d092bb99ea9ea6ebda817a0e7ad886f42a58b4501a7e27cd97371d0ba54/ruff-0.12.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7d1a4e0bdfafcd2e3e235ecf50bf0176f74dd37902f241588ae1f6c827a36c56", size = 11701463, upload-time = "2025-08-21T18:23:03.211Z" },
+ { url = "https://files.pythonhosted.org/packages/59/80/d982c55e91df981f3ab62559371380616c57ffd0172d96850280c2b04fa8/ruff-0.12.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e67d96827854f50b9e3e8327b031647e7bcc090dbe7bb11101a81a3a2cbf1cc9", size = 12691603, upload-time = "2025-08-21T18:23:06.935Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/37/63a9c788bbe0b0850611669ec6b8589838faf2f4f959647f2d3e320383ae/ruff-0.12.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ae479e1a18b439c59138f066ae79cc0f3ee250712a873d00dbafadaad9481e5b", size = 13164356, upload-time = "2025-08-21T18:23:10.225Z" },
+ { url = "https://files.pythonhosted.org/packages/47/d4/1aaa7fb201a74181989970ebccd12f88c0fc074777027e2a21de5a90657e/ruff-0.12.10-py3-none-win32.whl", hash = "sha256:9de785e95dc2f09846c5e6e1d3a3d32ecd0b283a979898ad427a9be7be22b266", size = 11896089, upload-time = "2025-08-21T18:23:14.232Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/14/2ad38fd4037daab9e023456a4a40ed0154e9971f8d6aed41bdea390aabd9/ruff-0.12.10-py3-none-win_amd64.whl", hash = "sha256:7837eca8787f076f67aba2ca559cefd9c5cbc3a9852fd66186f4201b87c1563e", size = 13004616, upload-time = "2025-08-21T18:23:17.422Z" },
+ { url = "https://files.pythonhosted.org/packages/24/3c/21cf283d67af33a8e6ed242396863af195a8a6134ec581524fd22b9811b6/ruff-0.12.10-py3-none-win_arm64.whl", hash = "sha256:cc138cc06ed9d4bfa9d667a65af7172b47840e1a98b02ce7011c391e54635ffc", size = 12074225, upload-time = "2025-08-21T18:23:20.137Z" },
+]
+
+[[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, upload-time = "2024-12-04T17:35:28.174Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
+]
+
+[[package]]
+name = "smmap"
+version = "5.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" },
+]
+
+[[package]]
+name = "text-unidecode"
+version = "1.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885, upload-time = "2019-08-30T21:36:45.405Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154, upload-time = "2019-08-30T21:37:03.543Z" },
+]
+
+[[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, upload-time = "2024-11-27T22:38:36.873Z" }
+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, upload-time = "2024-11-27T22:37:54.956Z" },
+ { 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, upload-time = "2024-11-27T22:37:56.698Z" },
+ { 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, upload-time = "2024-11-27T22:37:57.63Z" },
+ { 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, upload-time = "2024-11-27T22:37:59.344Z" },
+ { 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, upload-time = "2024-11-27T22:38:00.429Z" },
+ { 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, upload-time = "2024-11-27T22:38:02.094Z" },
+ { 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, upload-time = "2024-11-27T22:38:03.206Z" },
+ { 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, upload-time = "2024-11-27T22:38:04.217Z" },
+ { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" },
+ { 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, upload-time = "2024-11-27T22:38:07.731Z" },
+ { 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, upload-time = "2024-11-27T22:38:09.384Z" },
+ { 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, upload-time = "2024-11-27T22:38:10.329Z" },
+ { 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, upload-time = "2024-11-27T22:38:11.443Z" },
+ { 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, upload-time = "2024-11-27T22:38:13.099Z" },
+ { 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, upload-time = "2024-11-27T22:38:14.766Z" },
+ { 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, upload-time = "2024-11-27T22:38:15.843Z" },
+ { 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, upload-time = "2024-11-27T22:38:17.645Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" },
+ { 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, upload-time = "2024-11-27T22:38:21.659Z" },
+ { 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, upload-time = "2024-11-27T22:38:22.693Z" },
+ { 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, upload-time = "2024-11-27T22:38:24.367Z" },
+ { 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, upload-time = "2024-11-27T22:38:26.081Z" },
+ { 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, upload-time = "2024-11-27T22:38:27.921Z" },
+ { 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, upload-time = "2024-11-27T22:38:29.591Z" },
+ { 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, upload-time = "2024-11-27T22:38:30.639Z" },
+ { 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, upload-time = "2024-11-27T22:38:31.702Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
+]
+
+[[package]]
+name = "watchdog"
+version = "6.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" },
+ { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" },
+ { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" },
+ { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" },
+ { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" },
+ { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" },
+ { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" },
+ { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" },
+ { url = "https://files.pythonhosted.org/packages/05/52/7223011bb760fce8ddc53416beb65b83a3ea6d7d13738dde75eeb2c89679/watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8", size = 96390, upload-time = "2024-11-01T14:06:49.325Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/62/d2b21bc4e706d3a9d467561f487c2938cbd881c69f3808c43ac1ec242391/watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a", size = 88386, upload-time = "2024-11-01T14:06:50.536Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/22/1c90b20eda9f4132e4603a26296108728a8bfe9584b006bd05dd94548853/watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c", size = 89017, upload-time = "2024-11-01T14:06:51.717Z" },
+ { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/79/69f2b0e8d3f2afd462029031baafb1b75d11bb62703f0e1022b2e54d49ee/watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa", size = 87903, upload-time = "2024-11-01T14:06:57.052Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/2b/dc048dd71c2e5f0f7ebc04dd7912981ec45793a03c0dc462438e0591ba5d/watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e", size = 88381, upload-time = "2024-11-01T14:06:58.193Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" },
+ { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" },
+ { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" },
+ { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" },
+ { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" },
+]
+
+[[package]]
+name = "wcmatch"
+version = "10.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "bracex" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/79/3e/c0bdc27cf06f4e47680bd5803a07cb3dfd17de84cde92dd217dcb9e05253/wcmatch-10.1.tar.gz", hash = "sha256:f11f94208c8c8484a16f4f48638a85d771d9513f4ab3f37595978801cb9465af", size = 117421, upload-time = "2025-06-22T19:14:02.49Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/eb/d8/0d1d2e9d3fabcf5d6840362adcf05f8cf3cd06a73358140c3a97189238ae/wcmatch-10.1-py3-none-any.whl", hash = "sha256:5848ace7dbb0476e5e55ab63c6bbd529745089343427caa5537f230cc01beb8a", size = 39854, upload-time = "2025-06-22T19:14:00.978Z" },
+]
+
+[[package]]
+name = "zipp"
+version = "3.23.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
+]