From b5ad370e757c3f36d077c886ba4eb52380187c55 Mon Sep 17 00:00:00 2001 From: Miguel Angel Ajo Pelayo Date: Tue, 4 Feb 2025 12:00:35 +0100 Subject: [PATCH 1/2] Update create_driver contrib guide details --- CONTRIB.md | 18 +++++++++--------- __templates__/create_driver.sh | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/CONTRIB.md b/CONTRIB.md index 7529ed4b1..49cc1abf5 100644 --- a/CONTRIB.md +++ b/CONTRIB.md @@ -14,16 +14,16 @@ To create a new driver scaffold, you can use the `create_driver.sh` script in th From the root directory of the project, run the following command: ```shell -$ ./contrib/__templates__/create_driver.sh shell Shell "Miguel Angel Ajo" miguelangel@ajo.es +$ ./__templates__/create_driver.sh yepkit Ykush "Miguel Angel Ajo" "miguelangel@ajo.es" -Creating: contrib/drivers/shell/jumpstarter_driver_shell/__init__.py -Creating: contrib/drivers/shell/jumpstarter_driver_shell/client.py -Creating: contrib/drivers/shell/jumpstarter_driver_shell/driver_test.py -Creating: contrib/drivers/shell/jumpstarter_driver_shell/driver.py -Creating: contrib/drivers/shell/.gitignore -Creating: contrib/drivers/shell/pyproject.toml -Creating: contrib/drivers/shell/README.md -Creating: contrib/drivers/shell/examples/exporter.yaml +Creating: packages/jumpstarter_driver_yepkit/jumpstarter_driver_yepkit/__init__.py +Creating: packages/jumpstarter_driver_yepkit/jumpstarter_driver_yepkit/client.py +Creating: packages/jumpstarter_driver_yepkit/jumpstarter_driver_yepkit/driver_test.py +Creating: packages/jumpstarter_driver_yepkit/jumpstarter_driver_yepkit/driver.py +Creating: packages/jumpstarter_driver_yepkit/.gitignore +Creating: packages/jumpstarter_driver_yepkit/pyproject.toml +Creating: packages/jumpstarter_driver_yepkit/README.md +Creating: packages/jumpstarter_driver_yepkit/examples/exporter.yaml $ make sync uv sync --all-packages --all-extras diff --git a/__templates__/create_driver.sh b/__templates__/create_driver.sh index ea0040e23..0f3475d66 100755 --- a/__templates__/create_driver.sh +++ b/__templates__/create_driver.sh @@ -21,7 +21,7 @@ export AUTHOR_NAME=$3 export AUTHOR_EMAIL=$4 # create the driver directory -DRIVER_DIRECTORY=packages/jumpstarter_driver_${DRIVER_NAME} +DRIVER_DIRECTORY=packages/jumpstarter-driver-${DRIVER_NAME} MODULE_DIRECTORY=${DRIVER_DIRECTORY}/jumpstarter_driver_${DRIVER_NAME} # create the module directories mkdir -p ${MODULE_DIRECTORY} @@ -30,10 +30,10 @@ mkdir -p ${DRIVER_DIRECTORY}/examples for f in __init__.py client.py driver_test.py driver.py; do echo "Creating: ${MODULE_DIRECTORY}/${f}" - envsubst < contrib/__templates__/driver/jumpstarter_driver/${f}.tmpl > ${MODULE_DIRECTORY}/${f} + envsubst < __templates__/driver/jumpstarter_driver/${f}.tmpl > ${MODULE_DIRECTORY}/${f} done for f in .gitignore pyproject.toml README.md examples/exporter.yaml; do echo "Creating: ${DRIVER_DIRECTORY}/${f}" - envsubst < packages/__templates__/driver/${f}.tmpl > ${DRIVER_DIRECTORY}/${f} + envsubst < __templates__/driver/${f}.tmpl > ${DRIVER_DIRECTORY}/${f} done From f933e09b749c2810996121d93c72dce36ba54737 Mon Sep 17 00:00:00 2001 From: Miguel Angel Ajo Pelayo Date: Tue, 4 Feb 2025 13:38:55 +0100 Subject: [PATCH 2/2] Add Yepkit YKUSH Usb driver This USB Hub enables power control of the ports, making it useful to control the power of different targets as long as those targets can be USB powered. --- docs/source/api-reference/drivers/yepkit.md | 74 +++++++++ packages/jumpstarter-driver-yepkit/README.md | 11 ++ .../examples/exporter.yaml | 29 ++++ .../jumpstarter_driver_yepkit/__init__.py | 0 .../jumpstarter_driver_yepkit/conftest.py | 13 ++ .../jumpstarter_driver_yepkit/driver.py | 145 ++++++++++++++++++ .../jumpstarter_driver_yepkit/driver_test.py | 10 ++ .../jumpstarter-driver-yepkit/pyproject.toml | 40 +++++ uv.lock | 74 ++++++--- 9 files changed, 375 insertions(+), 21 deletions(-) create mode 100644 docs/source/api-reference/drivers/yepkit.md create mode 100644 packages/jumpstarter-driver-yepkit/README.md create mode 100644 packages/jumpstarter-driver-yepkit/examples/exporter.yaml create mode 100644 packages/jumpstarter-driver-yepkit/jumpstarter_driver_yepkit/__init__.py create mode 100644 packages/jumpstarter-driver-yepkit/jumpstarter_driver_yepkit/conftest.py create mode 100644 packages/jumpstarter-driver-yepkit/jumpstarter_driver_yepkit/driver.py create mode 100644 packages/jumpstarter-driver-yepkit/jumpstarter_driver_yepkit/driver_test.py create mode 100644 packages/jumpstarter-driver-yepkit/pyproject.toml diff --git a/docs/source/api-reference/drivers/yepkit.md b/docs/source/api-reference/drivers/yepkit.md new file mode 100644 index 000000000..2d42f34be --- /dev/null +++ b/docs/source/api-reference/drivers/yepkit.md @@ -0,0 +1,74 @@ +# Yepkit + +Drivers for yepkit products. + +## Ykush driver + +This driver provides a client for the [Ykush USB switch](https://www.yepkit.com/products/ykush). +**driver**: `jumpstarter_driver_yepkit.driver.Ykush` + +### Driver configuration +```yaml +export: + power: + type: jumpstarter_driver_yepkit.driver.Ykush + config: + serial: "YK25838" + port: "1" + + power2: + type: jumpstarter_driver_yepkit.driver.Ykush + config: + serial: "YK25838" + port: "2" + +``` +### Config parameters + +| Parameter | Description | Type | Required | Default | +|-----------|-------------|------|----------|---------| +| serial | The serial number of the ykush hub, empty means auto-detection | no | None | | +| port | The port number to be managed, "0", "1", "2", "a" which means all | str | yes | "a" | + + +### PowerClient API + +The yepkit ykush driver provides a `PowerClient` with the following API: + +```{eval-rst} +.. autoclass:: jumpstarter_driver_power.client.PowerClient + :members: on, off +``` + +### Examples +Powering on and off a device +```{testcode} +client.power.on() +time.sleep(1) +client.power.off() +``` + +### CLI access +```bash +$ sudo ~/.cargo/bin/uv run jmp exporter shell -c ./packages/jumpstarter-driver-yepkit/examples/exporter.yaml +WARNING:Ykush:No serial number provided for ykush, using the first one found: YK25838 +INFO:Ykush:Power OFF for Ykush YK25838 on port 1 +INFO:Ykush:Power OFF for Ykush YK25838 on port 2 + +$$ j +Usage: j [OPTIONS] COMMAND [ARGS]... + + Generic composite device + +Options: + --help Show this message and exit. + +Commands: + power Generic power + power2 Generic power + +$$ j power on +INFO:Ykush:Power ON for Ykush YK25838 on port 1 + +$$ exit +``` diff --git a/packages/jumpstarter-driver-yepkit/README.md b/packages/jumpstarter-driver-yepkit/README.md new file mode 100644 index 000000000..6e6806d26 --- /dev/null +++ b/packages/jumpstarter-driver-yepkit/README.md @@ -0,0 +1,11 @@ +# Jumpstarter Driver for the ykush USB Hub from Yepkit + +This driver is for the ykush USB Hub from Yepkit. It allows you to control the power of each port of the hub. + +If you want to test this locally, you can use the following commands from the root of the repository: + +```bash +sudo $(which uv) run jmp exporter shell --config ./packages/jumpstarter-driver-yepkit/examples/exporter.yaml +``` + +Please note that sudo is necessary to gain access to the raw USB interfaces. \ No newline at end of file diff --git a/packages/jumpstarter-driver-yepkit/examples/exporter.yaml b/packages/jumpstarter-driver-yepkit/examples/exporter.yaml new file mode 100644 index 000000000..e29d54643 --- /dev/null +++ b/packages/jumpstarter-driver-yepkit/examples/exporter.yaml @@ -0,0 +1,29 @@ +apiVersion: jumpstarter.dev/v1alpha1 +kind: ExporterConfig +metadata: + name: exporter + namespace: default +endpoint: grpc.jumpstarter.192.168.0.203.nip.io:8082 +token: "" +export: + power: + type: jumpstarter_driver_yepkit.driver.Ykush + config: + port: "1" + + power2: + type: jumpstarter_driver_yepkit.driver.Ykush + config: + serial: "YK25838" + port: "2" + + power3: + type: jumpstarter_driver_yepkit.driver.Ykush + config: + port: "3" + + all: + type: jumpstarter_driver_yepkit.driver.Ykush + config: + port: "all" + diff --git a/packages/jumpstarter-driver-yepkit/jumpstarter_driver_yepkit/__init__.py b/packages/jumpstarter-driver-yepkit/jumpstarter_driver_yepkit/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/packages/jumpstarter-driver-yepkit/jumpstarter_driver_yepkit/conftest.py b/packages/jumpstarter-driver-yepkit/jumpstarter_driver_yepkit/conftest.py new file mode 100644 index 000000000..b1dac6b37 --- /dev/null +++ b/packages/jumpstarter-driver-yepkit/jumpstarter_driver_yepkit/conftest.py @@ -0,0 +1,13 @@ +import pytest +import usb + + +def pytest_runtest_call(item): + try: + item.runtest() + except FileNotFoundError: + pytest.skip("yepkit not available") + except usb.core.USBError: + pytest.skip("USB not available, could need root permissions") + except usb.core.NoBackendError: + pytest.skip("No USB backend") diff --git a/packages/jumpstarter-driver-yepkit/jumpstarter_driver_yepkit/driver.py b/packages/jumpstarter-driver-yepkit/jumpstarter_driver_yepkit/driver.py new file mode 100644 index 000000000..8d16820b9 --- /dev/null +++ b/packages/jumpstarter-driver-yepkit/jumpstarter_driver_yepkit/driver.py @@ -0,0 +1,145 @@ +import threading +from collections.abc import AsyncGenerator +from dataclasses import dataclass, field + +import usb.core +import usb.util +from jumpstarter_driver_power.driver import PowerInterface, PowerReading + +from jumpstarter.driver import Driver, export + +VID = 0x04D8 +PID = 0xF2F7 + +PORT_UP_COMMANDS = { + '1': 0x11, + '2': 0x12, + '3': 0x13, + 'all': 0x1A +} + +PORT_DOWN_COMMANDS = { + '1': 0x01, + '2': 0x02, + '3': 0x03, + 'all': 0x0A +} + +PORT_STATUS_COMMANDS = { + '1': 0x21, + '2': 0x22, + '3': 0x23 +} + +VALID_DEFAULTS = ["on", "off", "keep"] + +# static shared array of usb devices, interfaces on same device cannot be claimed multiple times +_USB_DEVS = {} +_USB_DEVS_LOCK = threading.Lock() # Lock for synchronizing access, we don't do multithread, but just in case.. + +@dataclass(kw_only=True) +class Ykush(PowerInterface, Driver): + """ driver for Yepkit Ykush USB Hub with Power control """ + serial: str | None = field(default=None) + default: str = "off" + port: str = "all" + + dev: usb.core.Device = field(init=False) + + def __post_init__(self): + if hasattr(super(), "__post_init__"): + super().__post_init__() + + keys = PORT_UP_COMMANDS.keys() + if self.port not in keys: + raise ValueError( + f"The ykush driver port must be any of the following values: {keys}") + + if self.default not in VALID_DEFAULTS: + raise ValueError( + f"The ykush driver default must be any of the following values: {VALID_DEFAULTS}") + + with _USB_DEVS_LOCK: + # another instance already claimed this device? + if self.serial is None and len(_USB_DEVS.keys()) > 0: + self.serial = list(_USB_DEVS.keys())[0] + self.dev = _USB_DEVS[self.serial] + return + + if self.serial in _USB_DEVS: + self.dev = _USB_DEVS[self.serial] + return + + for dev in usb.core.find(idVendor=VID, idProduct=PID, find_all=True): + serial = usb.util.get_string(dev, dev.iSerialNumber, 0) + if serial == self.serial or self.serial is None: + _USB_DEVS[serial] = dev + if self.serial is None: + self.logger.warning( + f"No serial number provided for ykush, using the first one found: {serial}") + self.serial = serial + self.dev = dev + return + + raise FileNotFoundError("failed to find ykush device") + + def _send_cmd(self, cmd, report_size=64): + out_ep, in_ep = self._get_endpoints(self.dev) + out_buf = [0x00] * report_size + out_buf[0] = cmd # YKUSH command + + # Write to the OUT endpoint + out_ep.write(out_buf) + + # Read from the IN endpoint + in_buf = in_ep.read(report_size, timeout=2000) + return list(in_buf) + + def _get_endpoints(self, dev): + """ + From the active configuration, find the first IN and OUT endpoints. + """ + cfg = self.dev.get_active_configuration() + interface = cfg[(0, 0)] + + out_endpoint = usb.util.find_descriptor( + interface, + custom_match=lambda e: \ + usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_OUT + ) + + in_endpoint = usb.util.find_descriptor( + interface, + custom_match=lambda e: \ + usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_IN + ) + + if not out_endpoint or not in_endpoint: + raise RuntimeError("Could not find both IN and OUT endpoints for ykush.") + + return out_endpoint, in_endpoint + + # reset function is called by the exporter to setup the default state + def reset(self): + if self.default == "on": + self.on() + elif self.default == "off": + self.off() + + @export + def on(self): + self.logger.info(f"Power ON for Ykush {self.serial} on port {self.port}") + cmd = PORT_UP_COMMANDS.get(self.port) + _ = self._send_cmd(cmd) + return + + @export + def off(self): + self.logger.info(f"Power OFF for Ykush {self.serial} on port {self.port}") + cmd = PORT_DOWN_COMMANDS.get(self.port) + _ = self._send_cmd(cmd) + return + + @export + def read(self) -> AsyncGenerator[PowerReading, None]: + raise NotImplementedError diff --git a/packages/jumpstarter-driver-yepkit/jumpstarter_driver_yepkit/driver_test.py b/packages/jumpstarter-driver-yepkit/jumpstarter_driver_yepkit/driver_test.py new file mode 100644 index 000000000..9b280134e --- /dev/null +++ b/packages/jumpstarter-driver-yepkit/jumpstarter_driver_yepkit/driver_test.py @@ -0,0 +1,10 @@ +from .driver import Ykush +from jumpstarter.common.utils import serve + + +def test_drivers_yepkit(): + instance = Ykush() + + with serve(instance) as client: + client.on() + client.off() diff --git a/packages/jumpstarter-driver-yepkit/pyproject.toml b/packages/jumpstarter-driver-yepkit/pyproject.toml new file mode 100644 index 000000000..cd8c69d56 --- /dev/null +++ b/packages/jumpstarter-driver-yepkit/pyproject.toml @@ -0,0 +1,40 @@ +[project] +name = "jumpstarter-driver-yepkit" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +authors = [ + { name = "Miguel Angel Ajo", email = "miguelangel@ajo.es" } +] +requires-python = ">=3.11" +dependencies = [ + "anyio>=4.6.2.post1", + "pyusb>=1.2.1", + "jumpstarter_driver_power", + "jumpstarter", +] + +[tool.hatch.version] +source = "vcs" +raw-options = { 'root' = '../../'} + +[tool.hatch.metadata.hooks.vcs.urls] +Homepage = "https://jumpstarter.dev" +source_archive = "https://github.com/jumpstarter-dev/repo/archive/{commit_hash}.zip" + +[tool.pytest.ini_options] +addopts = "--cov --cov-report=html --cov-report=xml" +log_cli = true +log_cli_level = "INFO" +testpaths = ["jumpstarter_driver_yepkit"] +asyncio_mode = "auto" + +[build-system] +requires = ["hatchling", "hatch-vcs"] +build-backend = "hatchling.build" + +[dependency-groups] +dev = [ + "pytest-cov>=6.0.0", + "pytest>=8.3.3", +] diff --git a/uv.lock b/uv.lock index 36c6fb3b1..fd6e98ecf 100644 --- a/uv.lock +++ b/uv.lock @@ -21,6 +21,7 @@ members = [ "jumpstarter-driver-sdwire", "jumpstarter-driver-tftp", "jumpstarter-driver-ustreamer", + "jumpstarter-driver-yepkit", "jumpstarter-example-automotive", "jumpstarter-example-soc-pytest", "jumpstarter-imagehash", @@ -1292,6 +1293,37 @@ dev = [ { name = "pytest-cov", specifier = ">=5.0.0" }, ] +[[package]] +name = "jumpstarter-driver-yepkit" +version = "0.1.0" +source = { editable = "packages/jumpstarter-driver-yepkit" } +dependencies = [ + { name = "anyio" }, + { name = "jumpstarter" }, + { name = "jumpstarter-driver-power" }, + { name = "pyusb" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-cov" }, +] + +[package.metadata] +requires-dist = [ + { name = "anyio", specifier = ">=4.6.2.post1" }, + { name = "jumpstarter", editable = "packages/jumpstarter" }, + { name = "jumpstarter-driver-power", editable = "packages/jumpstarter-driver-power" }, + { name = "pyusb", specifier = ">=1.2.1" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.3.3" }, + { name = "pytest-cov", specifier = ">=6.0.0" }, +] + [[package]] name = "jumpstarter-example-automotive" version = "0.1.0" @@ -2244,27 +2276,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.9.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1e/7f/60fda2eec81f23f8aa7cbbfdf6ec2ca11eb11c273827933fb2541c2ce9d8/ruff-0.9.3.tar.gz", hash = "sha256:8293f89985a090ebc3ed1064df31f3b4b56320cdfcec8b60d3295bddb955c22a", size = 3586740 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/77/4fb790596d5d52c87fd55b7160c557c400e90f6116a56d82d76e95d9374a/ruff-0.9.3-py3-none-linux_armv6l.whl", hash = "sha256:7f39b879064c7d9670197d91124a75d118d00b0990586549949aae80cdc16624", size = 11656815 }, - { url = "https://files.pythonhosted.org/packages/a2/a8/3338ecb97573eafe74505f28431df3842c1933c5f8eae615427c1de32858/ruff-0.9.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a187171e7c09efa4b4cc30ee5d0d55a8d6c5311b3e1b74ac5cb96cc89bafc43c", size = 11594821 }, - { url = "https://files.pythonhosted.org/packages/8e/89/320223c3421962762531a6b2dd58579b858ca9916fb2674874df5e97d628/ruff-0.9.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c59ab92f8e92d6725b7ded9d4a31be3ef42688a115c6d3da9457a5bda140e2b4", size = 11040475 }, - { url = "https://files.pythonhosted.org/packages/b2/bd/1d775eac5e51409535804a3a888a9623e87a8f4b53e2491580858a083692/ruff-0.9.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc153c25e715be41bb228bc651c1e9b1a88d5c6e5ed0194fa0dfea02b026439", size = 11856207 }, - { url = "https://files.pythonhosted.org/packages/7f/c6/3e14e09be29587393d188454064a4aa85174910d16644051a80444e4fd88/ruff-0.9.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:646909a1e25e0dc28fbc529eab8eb7bb583079628e8cbe738192853dbbe43af5", size = 11420460 }, - { url = "https://files.pythonhosted.org/packages/ef/42/b7ca38ffd568ae9b128a2fa76353e9a9a3c80ef19746408d4ce99217ecc1/ruff-0.9.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a5a46e09355695fbdbb30ed9889d6cf1c61b77b700a9fafc21b41f097bfbba4", size = 12605472 }, - { url = "https://files.pythonhosted.org/packages/a6/a1/3167023f23e3530fde899497ccfe239e4523854cb874458ac082992d206c/ruff-0.9.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c4bb09d2bbb394e3730d0918c00276e79b2de70ec2a5231cd4ebb51a57df9ba1", size = 13243123 }, - { url = "https://files.pythonhosted.org/packages/d0/b4/3c600758e320f5bf7de16858502e849f4216cb0151f819fa0d1154874802/ruff-0.9.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:96a87ec31dc1044d8c2da2ebbed1c456d9b561e7d087734336518181b26b3aa5", size = 12744650 }, - { url = "https://files.pythonhosted.org/packages/be/38/266fbcbb3d0088862c9bafa8b1b99486691d2945a90b9a7316336a0d9a1b/ruff-0.9.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb7554aca6f842645022fe2d301c264e6925baa708b392867b7a62645304df4", size = 14458585 }, - { url = "https://files.pythonhosted.org/packages/63/a6/47fd0e96990ee9b7a4abda62de26d291bd3f7647218d05b7d6d38af47c30/ruff-0.9.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cabc332b7075a914ecea912cd1f3d4370489c8018f2c945a30bcc934e3bc06a6", size = 12419624 }, - { url = "https://files.pythonhosted.org/packages/84/5d/de0b7652e09f7dda49e1a3825a164a65f4998175b6486603c7601279baad/ruff-0.9.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:33866c3cc2a575cbd546f2cd02bdd466fed65118e4365ee538a3deffd6fcb730", size = 11843238 }, - { url = "https://files.pythonhosted.org/packages/9e/be/3f341ceb1c62b565ec1fb6fd2139cc40b60ae6eff4b6fb8f94b1bb37c7a9/ruff-0.9.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:006e5de2621304c8810bcd2ee101587712fa93b4f955ed0985907a36c427e0c2", size = 11484012 }, - { url = "https://files.pythonhosted.org/packages/a3/c8/ff8acbd33addc7e797e702cf00bfde352ab469723720c5607b964491d5cf/ruff-0.9.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ba6eea4459dbd6b1be4e6bfc766079fb9b8dd2e5a35aff6baee4d9b1514ea519", size = 12038494 }, - { url = "https://files.pythonhosted.org/packages/73/b1/8d9a2c0efbbabe848b55f877bc10c5001a37ab10aca13c711431673414e5/ruff-0.9.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:90230a6b8055ad47d3325e9ee8f8a9ae7e273078a66401ac66df68943ced029b", size = 12473639 }, - { url = "https://files.pythonhosted.org/packages/cb/44/a673647105b1ba6da9824a928634fe23186ab19f9d526d7bdf278cd27bc3/ruff-0.9.3-py3-none-win32.whl", hash = "sha256:eabe5eb2c19a42f4808c03b82bd313fc84d4e395133fb3fc1b1516170a31213c", size = 9834353 }, - { url = "https://files.pythonhosted.org/packages/c3/01/65cadb59bf8d4fbe33d1a750103e6883d9ef302f60c28b73b773092fbde5/ruff-0.9.3-py3-none-win_amd64.whl", hash = "sha256:040ceb7f20791dfa0e78b4230ee9dce23da3b64dd5848e40e3bf3ab76468dcf4", size = 10821444 }, - { url = "https://files.pythonhosted.org/packages/69/cb/b3fe58a136a27d981911cba2f18e4b29f15010623b79f0f2510fd0d31fd3/ruff-0.9.3-py3-none-win_arm64.whl", hash = "sha256:800d773f6d4d33b0a3c60e2c6ae8f4c202ea2de056365acfa519aa48acf28e0b", size = 10038168 }, +version = "0.9.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/80/63/77ecca9d21177600f551d1c58ab0e5a0b260940ea7312195bd2a4798f8a8/ruff-0.9.2.tar.gz", hash = "sha256:b5eceb334d55fae5f316f783437392642ae18e16dcf4f1858d55d3c2a0f8f5d0", size = 3553799 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/b9/0e168e4e7fb3af851f739e8f07889b91d1a33a30fca8c29fa3149d6b03ec/ruff-0.9.2-py3-none-linux_armv6l.whl", hash = "sha256:80605a039ba1454d002b32139e4970becf84b5fee3a3c3bf1c2af6f61a784347", size = 11652408 }, + { url = "https://files.pythonhosted.org/packages/2c/22/08ede5db17cf701372a461d1cb8fdde037da1d4fa622b69ac21960e6237e/ruff-0.9.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b9aab82bb20afd5f596527045c01e6ae25a718ff1784cb92947bff1f83068b00", size = 11587553 }, + { url = "https://files.pythonhosted.org/packages/42/05/dedfc70f0bf010230229e33dec6e7b2235b2a1b8cbb2a991c710743e343f/ruff-0.9.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fbd337bac1cfa96be615f6efcd4bc4d077edbc127ef30e2b8ba2a27e18c054d4", size = 11020755 }, + { url = "https://files.pythonhosted.org/packages/df/9b/65d87ad9b2e3def67342830bd1af98803af731243da1255537ddb8f22209/ruff-0.9.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b35259b0cbf8daa22a498018e300b9bb0174c2bbb7bcba593935158a78054d", size = 11826502 }, + { url = "https://files.pythonhosted.org/packages/93/02/f2239f56786479e1a89c3da9bc9391120057fc6f4a8266a5b091314e72ce/ruff-0.9.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b6a9701d1e371bf41dca22015c3f89769da7576884d2add7317ec1ec8cb9c3c", size = 11390562 }, + { url = "https://files.pythonhosted.org/packages/c9/37/d3a854dba9931f8cb1b2a19509bfe59e00875f48ade632e95aefcb7a0aee/ruff-0.9.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9cc53e68b3c5ae41e8faf83a3b89f4a5d7b2cb666dff4b366bb86ed2a85b481f", size = 12548968 }, + { url = "https://files.pythonhosted.org/packages/fa/c3/c7b812bb256c7a1d5553433e95980934ffa85396d332401f6b391d3c4569/ruff-0.9.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8efd9da7a1ee314b910da155ca7e8953094a7c10d0c0a39bfde3fcfd2a015684", size = 13187155 }, + { url = "https://files.pythonhosted.org/packages/bd/5a/3c7f9696a7875522b66aa9bba9e326e4e5894b4366bd1dc32aa6791cb1ff/ruff-0.9.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3292c5a22ea9a5f9a185e2d131dc7f98f8534a32fb6d2ee7b9944569239c648d", size = 12704674 }, + { url = "https://files.pythonhosted.org/packages/be/d6/d908762257a96ce5912187ae9ae86792e677ca4f3dc973b71e7508ff6282/ruff-0.9.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a605fdcf6e8b2d39f9436d343d1f0ff70c365a1e681546de0104bef81ce88df", size = 14529328 }, + { url = "https://files.pythonhosted.org/packages/2d/c2/049f1e6755d12d9cd8823242fa105968f34ee4c669d04cac8cea51a50407/ruff-0.9.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c547f7f256aa366834829a08375c297fa63386cbe5f1459efaf174086b564247", size = 12385955 }, + { url = "https://files.pythonhosted.org/packages/91/5a/a9bdb50e39810bd9627074e42743b00e6dc4009d42ae9f9351bc3dbc28e7/ruff-0.9.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d18bba3d3353ed916e882521bc3e0af403949dbada344c20c16ea78f47af965e", size = 11810149 }, + { url = "https://files.pythonhosted.org/packages/e5/fd/57df1a0543182f79a1236e82a79c68ce210efb00e97c30657d5bdb12b478/ruff-0.9.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b338edc4610142355ccf6b87bd356729b62bf1bc152a2fad5b0c7dc04af77bfe", size = 11479141 }, + { url = "https://files.pythonhosted.org/packages/dc/16/bc3fd1d38974f6775fc152a0554f8c210ff80f2764b43777163c3c45d61b/ruff-0.9.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:492a5e44ad9b22a0ea98cf72e40305cbdaf27fac0d927f8bc9e1df316dcc96eb", size = 12014073 }, + { url = "https://files.pythonhosted.org/packages/47/6b/e4ca048a8f2047eb652e1e8c755f384d1b7944f69ed69066a37acd4118b0/ruff-0.9.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:af1e9e9fe7b1f767264d26b1075ac4ad831c7db976911fa362d09b2d0356426a", size = 12435758 }, + { url = "https://files.pythonhosted.org/packages/c2/40/4d3d6c979c67ba24cf183d29f706051a53c36d78358036a9cd21421582ab/ruff-0.9.2-py3-none-win32.whl", hash = "sha256:71cbe22e178c5da20e1514e1e01029c73dc09288a8028a5d3446e6bba87a5145", size = 9796916 }, + { url = "https://files.pythonhosted.org/packages/c3/ef/7f548752bdb6867e6939489c87fe4da489ab36191525fadc5cede2a6e8e2/ruff-0.9.2-py3-none-win_amd64.whl", hash = "sha256:c5e1d6abc798419cf46eed03f54f2e0c3adb1ad4b801119dedf23fcaf69b55b5", size = 10773080 }, + { url = "https://files.pythonhosted.org/packages/0e/4e/33df635528292bd2d18404e4daabcd74ca8a9853b2e1df85ed3d32d24362/ruff-0.9.2-py3-none-win_arm64.whl", hash = "sha256:a1b63fa24149918f8b37cef2ee6fff81f24f0d74b6f0bdc37bc3e1f2143e41c6", size = 10001738 }, ] [[package]]