Compare commits

...

4 commits

Author SHA1 Message Date
927e2513e3 add uv stuff 2025-09-24 21:44:23 +02:00
5d73bf125f add .gitignore 2025-09-24 21:43:44 +02:00
dbdf3a226f mixer.py: ran ruff 2025-09-24 21:38:34 +02:00
cce93ce2cb mixer.py: rename recv -> recv_sys_ex 2025-09-24 20:31:06 +02:00
6 changed files with 516 additions and 45 deletions

361
.gitignore vendored Normal file
View file

@ -0,0 +1,361 @@
# Created by https://www.toptal.com/developers/gitignore/api/linux,windows,macos,python,pycharm+all,direnv,vim
# Edit at https://www.toptal.com/developers/gitignore?templates=linux,windows,macos,python,pycharm+all,direnv,vim
### direnv ###
.direnv
.envrc
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### PyCharm+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### PyCharm+all Patch ###
# Ignore everything but code style settings and run configurations
# that are supposed to be shared within teams.
.idea/*
!.idea/codeStyles
!.idea/runConfigurations
### Python ###
# 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
# 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/#use-with-ide
.pdm.toml
# 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/
### Python Patch ###
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
poetry.toml
# ruff
.ruff_cache/
# LSP config files
pyrightconfig.json
### Vim ###
# Swap
[._]*.s[a-v][a-z]
!*.svg # comment out if you don't need vector files
[._]*.sw[a-p]
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]
[._]sw[a-p]
# Session
Session.vim
Sessionx.vim
# Temporary
.netrwhist
# Auto-generated tag files
tags
# Persistent undo
[._]*.un~
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/linux,windows,macos,python,pycharm+all,direnv,vim

1
.python-version Normal file
View file

@ -0,0 +1 @@
3.13

View file

@ -8,6 +8,7 @@ from enum import Enum
MIXER_PORT = 51325 MIXER_PORT = 51325
class Mixer: class Mixer:
def __init__(self, ip, port): def __init__(self, ip, port):
self.sock = socket.create_connection((ip, port)) self.sock = socket.create_connection((ip, port))
@ -27,7 +28,7 @@ class Mixer:
GET_METER_DATA_REQUEST = 0x12 GET_METER_DATA_REQUEST = 0x12
GET_METER_DATA_RESPONSE = 0x13 GET_METER_DATA_RESPONSE = 0x13
def recv(self, response_msg_filter: SysExMessageId = None): def recv_sys_ex(self, response_msg_filter: SysExMessageId = None):
p = mido.Parser() p = mido.Parser()
while True: while True:
@ -47,7 +48,7 @@ class Mixer:
print("Hex: ", " ".join(f"{b:02X}" for b in msg.data)) print("Hex: ", " ".join(f"{b:02X}" for b in msg.data))
if response_msg_filter is not None: if response_msg_filter is not None:
if msg.type != 'sysex': if msg.type != "sysex":
continue continue
msg_id = int(msg.data[8]) msg_id = int(msg.data[8])
@ -63,13 +64,13 @@ class Mixer:
data = self.SYSEX_ALL_CALL + [msg_id, i_pad_flag] data = self.SYSEX_ALL_CALL + [msg_id, i_pad_flag]
msg = mido.Message('sysex', data=data) msg = mido.Message("sysex", data=data)
msg_bytes = bytes(msg.bytes()) msg_bytes = bytes(msg.bytes())
self.sock.sendall(msg_bytes) self.sock.sendall(msg_bytes)
print("Sent:", " ".join(f"{b:02X}" for b in msg_bytes)) print("Sent:", " ".join(f"{b:02X}" for b in msg_bytes))
response = self.recv() response = self.recv_sys_ex(self.SysExMessageId.GET_SYSTEM_STATE_RESPONSE)
class QuModel(Enum): class QuModel(Enum):
QU16 = 1 QU16 = 1
@ -109,15 +110,15 @@ class Mixer:
data = self.SYSEX_HEADER + [int(0), msg_id, int(channel_no)] data = self.SYSEX_HEADER + [int(0), msg_id, int(channel_no)]
if name_to_set: if name_to_set:
data += list(name_to_set.encode('utf-8')) data += list(name_to_set.encode("utf-8"))
msg = mido.Message('sysex', data=data) msg = mido.Message("sysex", data=data)
msg_bytes = bytes(msg.bytes()) msg_bytes = bytes(msg.bytes())
print("Sent:", " ".join(f"{b:02X}" for b in msg_bytes)) print("Sent:", " ".join(f"{b:02X}" for b in msg_bytes))
self.sock.sendall(msg_bytes) self.sock.sendall(msg_bytes)
response = self.recv(self.SysExMessageId.GET_NAME_FROM_QU_RESPONSE) response = self.recv_sys_ex(self.SysExMessageId.GET_NAME_FROM_QU_RESPONSE)
sysex_header = response.data[:8] sysex_header = response.data[:8]
@ -131,61 +132,77 @@ class Mixer:
print(f"Channel Name: {channel_name}") print(f"Channel Name: {channel_name}")
def nrpn_parameter_control(self, midi_ch, mixer_ch, id, va, vx): def nrpn_parameter_control(self, midi_ch, mixer_ch, id, va, vx):
a = mido.Message('control_change', channel=midi_ch, control=0x63, value=mixer_ch) a = mido.Message(
b = mido.Message('control_change', channel=midi_ch, control=0x62, value=id) "control_change", channel=midi_ch, control=0x63, value=mixer_ch
c = mido.Message('control_change', channel=midi_ch, control=0x06, value=va) )
d = mido.Message('control_change', channel=midi_ch, control=0x26, value=vx) b = mido.Message("control_change", channel=midi_ch, control=0x62, value=id)
c = mido.Message("control_change", channel=midi_ch, control=0x06, value=va)
d = mido.Message("control_change", channel=midi_ch, control=0x26, value=vx)
msg_bytes = bytes(a.bytes() + b.bytes() + c.bytes() + d.bytes()) msg_bytes = bytes(a.bytes() + b.bytes() + c.bytes() + d.bytes())
print(' '.join(f"{b:02X}" for b in msg_bytes)) print(" ".join(f"{b:02X}" for b in msg_bytes))
self.sock.sendall(msg_bytes) self.sock.sendall(msg_bytes)
def shutdown(self): def shutdown(self):
self.nrpn_parameter_control(midi_ch=0, mixer_ch=0, id=0x5F, va=0x00, vx=0x00) self.nrpn_parameter_control(midi_ch=0, mixer_ch=0, id=0x5F, va=0x00, vx=0x00)
def set_bank_1(self): def set_bank_1(self):
msb = mido.Message('control_change', channel=0, control=0x00, value=0x00) msb = mido.Message("control_change", channel=0, control=0x00, value=0x00)
lsb = mido.Message('control_change', channel=0, control=0x20, value=0x00) lsb = mido.Message("control_change", channel=0, control=0x20, value=0x00)
msg_bytes = bytes(msb.bytes() + lsb.bytes()) msg_bytes = bytes(msb.bytes() + lsb.bytes())
print(' '.join(f"{b:02X}" for b in msg_bytes)) print(" ".join(f"{b:02X}" for b in msg_bytes))
self.sock.sendall(msg_bytes) self.sock.sendall(msg_bytes)
def scene_recall(self, scene_id): def scene_recall(self, scene_id):
print(f"scene_recall: scene_id={scene_id}", scene_id) print(f"scene_recall: scene_id={scene_id}", scene_id)
self.set_bank_1() self.set_bank_1()
msb = mido.Message('program_change', channel=0, program=scene_id) msb = mido.Message("program_change", channel=0, program=scene_id)
msg_bytes = bytes(msb.bytes()) msg_bytes = bytes(msb.bytes())
print(' '.join(f"{b:02X}" for b in msg_bytes)) print(" ".join(f"{b:02X}" for b in msg_bytes))
self.sock.sendall(msg_bytes) self.sock.sendall(msg_bytes)
def watch(self): def watch(self):
while True: while True:
self.recv() self.recv_sys_ex()
def main(): def main():
parser = argparse.ArgumentParser(description="Allen & Heath Qu Remote Control") parser = argparse.ArgumentParser(description="Allen & Heath Qu Remote Control")
parser.add_argument("ip", help="IP of the mixer") parser.add_argument("ip", help="IP of the mixer")
subparsers = parser.add_subparsers(dest="command", required=True, help="Available commands") subparsers = parser.add_subparsers(
dest="command", required=True, help="Available commands"
)
channel_naming_parser = subparsers.add_parser("get_name_from_qu", help="Channel naming") channel_naming_parser = subparsers.add_parser(
channel_naming_parser.add_argument("channel_id", type=int, choices=range(0, 16), help="Number of channel to get or set its name") "get_name_from_qu", help="Channel naming"
)
channel_naming_parser.add_argument(
"channel_id",
type=int,
choices=range(0, 16),
help="Number of channel to get or set its name",
)
channel_naming_parser.add_argument("-n", "--name", help="Channel name to set") channel_naming_parser.add_argument("-n", "--name", help="Channel name to set")
subparsers.add_parser("get_system_state", help="Get system information") subparsers.add_parser("get_system_state", help="Get system information")
subparsers.add_parser("shutdown", help="Shut down the mixer") subparsers.add_parser("shutdown", help="Shut down the mixer")
scene_parser = subparsers.add_parser("scene_recall", help="Recall a specific scene") scene_parser = subparsers.add_parser("scene_recall", help="Recall a specific scene")
scene_parser.add_argument("scene_number", type=int, choices=range(0, 100), help="Scene number to recall") scene_parser.add_argument(
"scene_number", type=int, choices=range(0, 100), help="Scene number to recall"
)
subparsers.add_parser("scene_recall_default", help="Set the default scene 0") subparsers.add_parser("scene_recall_default", help="Set the default scene 0")
subparsers.add_parser("watch", help="Just receive data from mixer and print it to console") subparsers.add_parser(
"watch", help="Just receive data from mixer and print it to console"
)
args = parser.parse_args() args = parser.parse_args()
@ -197,12 +214,19 @@ def main():
print(f"Args: {vars(args)}") print(f"Args: {vars(args)}")
match args.command: match args.command:
case 'get_name_from_qu': mixer.get_name_from_qu(args.channel_id, args.name) case "get_name_from_qu":
case 'get_system_state': mixer.get_system_state() mixer.get_name_from_qu(args.channel_id, args.name)
case 'shutdown': mixer.shutdown() case "get_system_state":
case 'scene_recall': mixer.scene_recall(args.scene_number) mixer.get_system_state()
case 'scene_recall_default': mixer.scene_recall(0) case "shutdown":
case 'watch': mixer.watch() mixer.shutdown()
case "scene_recall":
mixer.scene_recall(args.scene_number)
case "scene_recall_default":
mixer.scene_recall(0)
case "watch":
mixer.watch()
if __name__ == '__main__':
if __name__ == "__main__":
main() main()

18
pyproject.toml Normal file
View file

@ -0,0 +1,18 @@
[project]
name = "mixer"
version = "0.1.0"
description = "talks to atten & heath mixer via mqtt"
readme = "README.md"
authors = [
{ name = "coon", email = "coon@mailbox.org" }
]
requires-python = ">=3.13"
dependencies = [
"mido>=1.3.3",
"paho-mqtt>=2.1.0",
]
[dependency-groups]
dev = [
"ruff>=0.13.1",
]

View file

@ -1,15 +0,0 @@
blessed==1.21.0
bpython==0.25
certifi==2025.8.3
charset-normalizer==3.4.3
curtsies==0.4.3
cwcwidth==0.1.10
greenlet==3.2.4
idna==3.10
mido==1.3.3
packaging==25.0
Pygments==2.19.2
pyxdg==0.28
requests==2.32.5
urllib3==2.5.0
wcwidth==0.2.13

82
uv.lock generated Normal file
View file

@ -0,0 +1,82 @@
version = 1
revision = 3
requires-python = ">=3.13"
[[package]]
name = "mido"
version = "1.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
]
sdist = { url = "https://files.pythonhosted.org/packages/23/14/cfda3fe61ce4c0f50a9f707ae02b46cb53211732b2cd4522bf06272848f4/mido-1.3.3.tar.gz", hash = "sha256:1aecb30b7f282404f17e43768cbf74a6a31bf22b3b783bdd117a1ce9d22cb74c", size = 124288, upload-time = "2024-10-25T15:05:21.847Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/28/45deb15c11859d2f10702b32e71de9328a9fa494f989626916db39a9617f/mido-1.3.3-py3-none-any.whl", hash = "sha256:01033c9b10b049e4436fca2762194ca839b09a4334091dd3c34e7f4ae674fd8a", size = 54614, upload-time = "2024-10-25T15:05:20.349Z" },
]
[[package]]
name = "mixer"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "mido" },
{ name = "paho-mqtt" },
]
[package.dev-dependencies]
dev = [
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
{ name = "mido", specifier = ">=1.3.3" },
{ name = "paho-mqtt", specifier = ">=2.1.0" },
]
[package.metadata.requires-dev]
dev = [{ name = "ruff", specifier = ">=0.13.1" }]
[[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 = "paho-mqtt"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/39/15/0a6214e76d4d32e7f663b109cf71fb22561c2be0f701d67f93950cd40542/paho_mqtt-2.1.0.tar.gz", hash = "sha256:12d6e7511d4137555a3f6ea167ae846af2c7357b10bc6fa4f7c3968fc1723834", size = 148848, upload-time = "2024-04-29T19:52:55.591Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c4/cb/00451c3cf31790287768bb12c6bec834f5d292eaf3022afc88e14b8afc94/paho_mqtt-2.1.0-py3-none-any.whl", hash = "sha256:6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee", size = 67219, upload-time = "2024-04-29T19:52:48.345Z" },
]
[[package]]
name = "ruff"
version = "0.13.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ab/33/c8e89216845615d14d2d42ba2bee404e7206a8db782f33400754f3799f05/ruff-0.13.1.tar.gz", hash = "sha256:88074c3849087f153d4bb22e92243ad4c1b366d7055f98726bc19aa08dc12d51", size = 5397987, upload-time = "2025-09-18T19:52:44.33Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f3/41/ca37e340938f45cfb8557a97a5c347e718ef34702546b174e5300dbb1f28/ruff-0.13.1-py3-none-linux_armv6l.whl", hash = "sha256:b2abff595cc3cbfa55e509d89439b5a09a6ee3c252d92020bd2de240836cf45b", size = 12304308, upload-time = "2025-09-18T19:51:56.253Z" },
{ url = "https://files.pythonhosted.org/packages/ff/84/ba378ef4129415066c3e1c80d84e539a0d52feb250685091f874804f28af/ruff-0.13.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4ee9f4249bf7f8bb3984c41bfaf6a658162cdb1b22e3103eabc7dd1dc5579334", size = 12937258, upload-time = "2025-09-18T19:52:00.184Z" },
{ url = "https://files.pythonhosted.org/packages/8d/b6/ec5e4559ae0ad955515c176910d6d7c93edcbc0ed1a3195a41179c58431d/ruff-0.13.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5c5da4af5f6418c07d75e6f3224e08147441f5d1eac2e6ce10dcce5e616a3bae", size = 12214554, upload-time = "2025-09-18T19:52:02.753Z" },
{ url = "https://files.pythonhosted.org/packages/70/d6/cb3e3b4f03b9b0c4d4d8f06126d34b3394f6b4d764912fe80a1300696ef6/ruff-0.13.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80524f84a01355a59a93cef98d804e2137639823bcee2931f5028e71134a954e", size = 12448181, upload-time = "2025-09-18T19:52:05.279Z" },
{ url = "https://files.pythonhosted.org/packages/d2/ea/bf60cb46d7ade706a246cd3fb99e4cfe854efa3dfbe530d049c684da24ff/ruff-0.13.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff7f5ce8d7988767dd46a148192a14d0f48d1baea733f055d9064875c7d50389", size = 12104599, upload-time = "2025-09-18T19:52:07.497Z" },
{ url = "https://files.pythonhosted.org/packages/2d/3e/05f72f4c3d3a69e65d55a13e1dd1ade76c106d8546e7e54501d31f1dc54a/ruff-0.13.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c55d84715061f8b05469cdc9a446aa6c7294cd4bd55e86a89e572dba14374f8c", size = 13791178, upload-time = "2025-09-18T19:52:10.189Z" },
{ url = "https://files.pythonhosted.org/packages/81/e7/01b1fc403dd45d6cfe600725270ecc6a8f8a48a55bc6521ad820ed3ceaf8/ruff-0.13.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ac57fed932d90fa1624c946dc67a0a3388d65a7edc7d2d8e4ca7bddaa789b3b0", size = 14814474, upload-time = "2025-09-18T19:52:12.866Z" },
{ url = "https://files.pythonhosted.org/packages/fa/92/d9e183d4ed6185a8df2ce9faa3f22e80e95b5f88d9cc3d86a6d94331da3f/ruff-0.13.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c366a71d5b4f41f86a008694f7a0d75fe409ec298685ff72dc882f882d532e36", size = 14217531, upload-time = "2025-09-18T19:52:15.245Z" },
{ url = "https://files.pythonhosted.org/packages/3b/4a/6ddb1b11d60888be224d721e01bdd2d81faaf1720592858ab8bac3600466/ruff-0.13.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4ea9d1b5ad3e7a83ee8ebb1229c33e5fe771e833d6d3dcfca7b77d95b060d38", size = 13265267, upload-time = "2025-09-18T19:52:17.649Z" },
{ url = "https://files.pythonhosted.org/packages/81/98/3f1d18a8d9ea33ef2ad508f0417fcb182c99b23258ec5e53d15db8289809/ruff-0.13.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0f70202996055b555d3d74b626406476cc692f37b13bac8828acff058c9966a", size = 13243120, upload-time = "2025-09-18T19:52:20.332Z" },
{ url = "https://files.pythonhosted.org/packages/8d/86/b6ce62ce9c12765fa6c65078d1938d2490b2b1d9273d0de384952b43c490/ruff-0.13.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:f8cff7a105dad631085d9505b491db33848007d6b487c3c1979dd8d9b2963783", size = 13443084, upload-time = "2025-09-18T19:52:23.032Z" },
{ url = "https://files.pythonhosted.org/packages/a1/6e/af7943466a41338d04503fb5a81b2fd07251bd272f546622e5b1599a7976/ruff-0.13.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9761e84255443316a258dd7dfbd9bfb59c756e52237ed42494917b2577697c6a", size = 12295105, upload-time = "2025-09-18T19:52:25.263Z" },
{ url = "https://files.pythonhosted.org/packages/3f/97/0249b9a24f0f3ebd12f007e81c87cec6d311de566885e9309fcbac5b24cc/ruff-0.13.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:3d376a88c3102ef228b102211ef4a6d13df330cb0f5ca56fdac04ccec2a99700", size = 12072284, upload-time = "2025-09-18T19:52:27.478Z" },
{ url = "https://files.pythonhosted.org/packages/f6/85/0b64693b2c99d62ae65236ef74508ba39c3febd01466ef7f354885e5050c/ruff-0.13.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cbefd60082b517a82c6ec8836989775ac05f8991715d228b3c1d86ccc7df7dae", size = 12970314, upload-time = "2025-09-18T19:52:30.212Z" },
{ url = "https://files.pythonhosted.org/packages/96/fc/342e9f28179915d28b3747b7654f932ca472afbf7090fc0c4011e802f494/ruff-0.13.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dd16b9a5a499fe73f3c2ef09a7885cb1d97058614d601809d37c422ed1525317", size = 13422360, upload-time = "2025-09-18T19:52:32.676Z" },
{ url = "https://files.pythonhosted.org/packages/37/54/6177a0dc10bce6f43e392a2192e6018755473283d0cf43cc7e6afc182aea/ruff-0.13.1-py3-none-win32.whl", hash = "sha256:55e9efa692d7cb18580279f1fbb525146adc401f40735edf0aaeabd93099f9a0", size = 12178448, upload-time = "2025-09-18T19:52:35.545Z" },
{ url = "https://files.pythonhosted.org/packages/64/51/c6a3a33d9938007b8bdc8ca852ecc8d810a407fb513ab08e34af12dc7c24/ruff-0.13.1-py3-none-win_amd64.whl", hash = "sha256:3a3fb595287ee556de947183489f636b9f76a72f0fa9c028bdcabf5bab2cc5e5", size = 13286458, upload-time = "2025-09-18T19:52:38.198Z" },
{ url = "https://files.pythonhosted.org/packages/fd/04/afc078a12cf68592345b1e2d6ecdff837d286bac023d7a22c54c7a698c5b/ruff-0.13.1-py3-none-win_arm64.whl", hash = "sha256:c0bae9ffd92d54e03c2bf266f466da0a65e145f298ee5b5846ed435f6a00518a", size = 12437893, upload-time = "2025-09-18T19:52:41.283Z" },
]