diff --git a/armctl/universal_robots/protocols/codes.py b/armctl/universal_robots/protocols/codes.py new file mode 100644 index 0000000..6ce240d --- /dev/null +++ b/armctl/universal_robots/protocols/codes.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from enum import IntEnum + + +class RobotMode(IntEnum): + NO_CONTROLLER = -1 + DISCONNECTED = 0 + CONFIRM_SAFETY = 1 + BOOTING = 2 + POWER_OFF = 3 + POWER_ON = 4 + IDLE = 5 + BACKDRIVE = 6 + RUNNING = 7 + UPDATING_FIRMWARE = 8 + + +class SafetyMode(IntEnum): + NORMAL = 1 + REDUCED = 2 + PROTECTIVE_STOP = 3 + RECOVERY = 4 + SAFEGUARD_STOP = 5 + SYSTEM_EMERGENCY_STOP = 6 + ROBOT_EMERGENCY_STOP = 7 + VIOLATION = 8 + FAULT = 9 + + +class RuntimeState(IntEnum): + STOPPED = 0 + PLAYING = 1 + PAUSED = 2 + PAUSING = 3 + RESUMING = 4 diff --git a/armctl/universal_robots/protocols/config.xml b/armctl/universal_robots/protocols/config.xml index ce429b7..2a901ad 100644 --- a/armctl/universal_robots/protocols/config.xml +++ b/armctl/universal_robots/protocols/config.xml @@ -1,11 +1,51 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - \ No newline at end of file + diff --git a/armctl/universal_robots/protocols/rtde.py b/armctl/universal_robots/protocols/rtde.py index e9167ac..9be5372 100644 --- a/armctl/universal_robots/protocols/rtde.py +++ b/armctl/universal_robots/protocols/rtde.py @@ -1,8 +1,11 @@ from __future__ import annotations +import time +from ctypes import c_uint32 from pathlib import Path from typing import NewType -from ctypes import c_uint32 + +from .codes import RobotMode, RuntimeState, SafetyMode try: from rtde import RTDE as _RTDE @@ -13,22 +16,26 @@ u32 = NewType("u32", c_uint32) +_MOVING_VELOCITY_THRESHOLD = 1e-3 # rad/s — below this on all joints = stopped + class RTDE: def __init__(self, ip: str): if _RTDE is None or ConfigFile is None: raise ImportError( - "Universal Robots RTDE support requires additional dependencies." + "Universal Robots RTDE support requires additional dependencies. " "Install it with: pip install armctl[ur]" ) config_file = Path(__file__).parent / "config.xml" config = ConfigFile(str(config_file)) - state_names, state_types = config.get_recipe("out") + out_names, out_types = config.get_recipe("out") + in_names, in_types = config.get_recipe("in") self.c = _RTDE(ip) self.c.connect() - self.c.send_output_setup(state_names, state_types) + self.c.send_output_setup(out_names, out_types) + self._input = self.c.send_input_setup(in_names, in_types) self.controller_version = ( self.c.get_controller_version() ) # (MAJOR, MINOR, BUGFIX, BUILD) @@ -40,29 +47,71 @@ def _get_data(self): return self.c.receive() def joint_angles(self) -> list[float]: - """Return joint angles in radians.""" + """Return actual joint angles in radians.""" return list(self._get_data().actual_q) - def tcp_pose(self) -> list[float]: - """Return TCP pose [x, y, z, rx, ry, rz].""" - return list(self._get_data().actual_TCP_pose) + def joint_velocities(self) -> list[float]: + """Return actual joint velocities in rad/s.""" + return list(self._get_data().actual_qd) + + def joint_currents(self) -> list[float]: + """Return actual joint currents in Amperes.""" + return list(self._get_data().actual_current) def joint_torques(self) -> list[float]: - """Return joint torques in Nm converted from current.""" - # See: https://www.universal-robots.com/articles/ur/release-notes/release-note-software-version-523x/ + """Return joint torques in Nm converted from current. + + Requires controller >= 5.23.0.0. + See: https://www.universal-robots.com/articles/ur/release-notes/release-note-software-version-523x/ + """ if self.controller_version >= (5, 23, 0, 0): return list(self._get_data().actual_current_as_torque) - else: - raise NotImplementedError( - "Joint torques not available for controller versions below 5.23.0.0" - ) + raise NotImplementedError( + "Joint torques not available for controller versions below 5.23.0.0" + ) + + def tcp_pose(self) -> list[float]: + """Return actual TCP pose [x, y, z, rx, ry, rz] in metres and radians.""" + return list(self._get_data().actual_TCP_pose) + + def tcp_speed(self) -> list[float]: + """Return actual TCP speed [vx, vy, vz, wx, wy, wz] in m/s and rad/s.""" + return list(self._get_data().actual_TCP_speed) def tcp_force(self) -> list[float]: - """Return TCP force [Fx, Fy, Fz, Tx, Ty, Tz] in Newton and Newton-meters.""" + """Return TCP force [Fx, Fy, Fz, Tx, Ty, Tz] in Newton and Newton-metres.""" return list(self._get_data().actual_TCP_force) + def target_joint_positions(self) -> list[float]: + """Return target joint positions in radians.""" + return list(self._get_data().target_q) + + def target_joint_velocities(self) -> list[float]: + """Return target joint velocities in rad/s.""" + return list(self._get_data().target_qd) + + def target_tcp_pose(self) -> list[float]: + """Return target TCP pose [x, y, z, rx, ry, rz] in metres and radians.""" + return list(self._get_data().target_TCP_pose) + + def target_tcp_speed(self) -> list[float]: + """Return target TCP speed [vx, vy, vz, wx, wy, wz] in m/s and rad/s.""" + return list(self._get_data().target_TCP_speed) + + def robot_mode(self) -> RobotMode: + """Return robot mode as a RobotMode enum.""" + return RobotMode(self._get_data().robot_mode) + + def safety_mode(self) -> SafetyMode: + """Return safety mode as a SafetyMode enum.""" + return SafetyMode(self._get_data().safety_mode) + + def runtime_state(self) -> RuntimeState: + """Return runtime state as a RuntimeState enum.""" + return RuntimeState(self._get_data().runtime_state) + def robot_status(self) -> dict[str, bool]: - """Return robot status. + """Return robot and safety status bit fields. Robot status bits (UINT32): - **`Bit 0`**: Is power on @@ -108,3 +157,148 @@ def robot_status(self) -> dict[str, bool]: "Fault": bit(ssb, 9), "Stopped Due to Safety": bit(ssb, 10), } + + def speed_scaling(self) -> float: + """Return current speed scaling factor in [0, 1].""" + return float(self._get_data().speed_scaling) + + def payload(self) -> dict: + """Return payload mass (kg) and centre of gravity (m).""" + data = self._get_data() + return { + "mass_kg": float(data.payload), + "cog": list(data.payload_cog), + } + + def analog_inputs(self) -> dict[str, float]: + """Return standard and tool analog input values (V or mA).""" + data = self._get_data() + return { + "standard_0": float(data.standard_analog_input0), + "standard_1": float(data.standard_analog_input1), + "tool_0": float(data.tool_analog_input0), + "tool_1": float(data.tool_analog_input1), + } + + def analog_outputs(self) -> dict[str, float]: + """Return standard analog output values (V or mA).""" + data = self._get_data() + return { + "standard_0": float(data.standard_analog_output0), + "standard_1": float(data.standard_analog_output1), + } + + def digital_inputs(self) -> dict[str, bool]: + """Return digital input states. + + Bits 0-7: Standard DI 0-7 + Bits 8-15: Configurable DI 0-7 + Bits 16-17: Tool DI 0-1 + """ + bits = self._get_data().standard_digital_input_bits + bit = lambda n: bool(bits & (1 << n)) + return { + **{f"DI{i}": bit(i) for i in range(8)}, + **{f"CDI{i}": bit(i + 8) for i in range(8)}, + "Tool_DI0": bit(16), + "Tool_DI1": bit(17), + } + + def digital_outputs(self) -> dict[str, bool]: + """Return digital output states. + + Bits 0-7: Standard DO 0-7 + Bits 8-15: Configurable DO 0-7 + Bits 16-17: Tool DO 0-1 + """ + bits = self._get_data().standard_digital_output_bits + bit = lambda n: bool(bits & (1 << n)) + return { + **{f"DO{i}": bit(i) for i in range(8)}, + **{f"CDO{i}": bit(i + 8) for i in range(8)}, + "Tool_DO0": bit(16), + "Tool_DO1": bit(17), + } + + def tool_io(self) -> dict: + """Return tool I/O state.""" + data = self._get_data() + return { + "analog_input_0": float(data.tool_analog_input0), + "analog_input_1": float(data.tool_analog_input1), + "output_voltage": int(data.tool_output_voltage), + "output_current": float(data.tool_output_current), + "temperature": float(data.tool_temperature), + } + + def is_moving(self, threshold: float = _MOVING_VELOCITY_THRESHOLD) -> bool: + """Return True if any joint velocity exceeds threshold (rad/s).""" + return any(abs(v) > threshold for v in self._get_data().actual_qd) + + def wait_until_stopped( + self, timeout: float = 120.0, poll_interval: float = 0.05 + ) -> None: + """Block until all joints are stopped or timeout expires. + + Waits for motion to begin, then polls joint velocities via RTDE + until all fall below _MOVING_VELOCITY_THRESHOLD. + + Parameters + ---------- + timeout : float + Maximum seconds to wait after motion starts. + poll_interval : float + RTDE poll interval in seconds. + + Raises + ------ + TimeoutError + If the robot does not stop within timeout. + """ + deadline = time.time() + timeout + while time.time() < deadline: + if self._get_data().output_int_registers_0 == 1: + return + time.sleep(poll_interval) + raise TimeoutError(f"Robot did not stop within {timeout}s") + + def set_speed_slider(self, fraction: float) -> None: + """Set speed override slider fraction in [0.0, 1.0].""" + if not 0.0 <= fraction <= 1.0: + raise ValueError( + f"Speed fraction must be in [0, 1], got {fraction}" + ) + self._input.speed_slider_mask = 1 + self._input.speed_slider_fraction = fraction + self.c.send(self._input) + + def set_digital_output(self, pin: int, value: bool) -> None: + """Set a standard digital output pin (0-7) high or low.""" + if not 0 <= pin <= 7: + raise ValueError(f"Digital output pin must be 0-7, got {pin}") + mask = 1 << pin + self._input.standard_digital_output_mask = mask + self._input.standard_digital_output = mask if value else 0 + self.c.send(self._input) + + def set_analog_output(self, channel: int, value: float) -> None: + """Set standard analog output voltage (channel 0 or 1). + + Parameters + ---------- + channel : int + Output channel — 0 or 1. + value : float + Output value in Volts [0, 10] (voltage mode). + """ + if channel not in (0, 1): + raise ValueError( + f"Analog output channel must be 0 or 1, got {channel}" + ) + self._input.standard_analog_output_mask = 1 << channel + self._input.standard_analog_output_type = 0 # 0 = voltage mode + if channel == 0: + self._input.standard_analog_output_0 = value + else: + self._input.standard_analog_output_1 = value + self.c.send(self._input) diff --git a/armctl/universal_robots/universal_robots.py b/armctl/universal_robots/universal_robots.py index 9a0c1df..e662cc1 100644 --- a/armctl/universal_robots/universal_robots.py +++ b/armctl/universal_robots/universal_robots.py @@ -1,11 +1,14 @@ from __future__ import annotations import math +import time as _time from armctl.templates import Commands, Properties from armctl.templates import SocketController as SCT from armctl.templates.logger import logger from armctl.utils import CommandCheck as cc + +from .protocols.codes import RobotMode, RuntimeState, SafetyMode from .protocols.rtde import RTDE ### Notes ### @@ -28,19 +31,26 @@ def __init__(self, ip: str, port: int | tuple[int, int] = 30_002): self.MAX_JOINT_VELOCITY = 2.0 # rad/s # Source: https://forum.universal-robots.com/t/maximum-axis-speed-acceleration/13338/4 self.MAX_JOINT_ACCELERATION = 10.0 # rad/s^2 + self.rtde: RTDE | None = None def connect(self): super().connect() - - self.rtde = RTDE(self.ip) # Initialize RTDE connection + self.rtde = RTDE(self.ip) def disconnect(self): - self.rtde.c.disconnect() # Disconnect RTDE connection + try: + self.stop_motion() + except Exception: + pass + if self.rtde is not None: + self.rtde.c.disconnect() + self.rtde = None super().disconnect() - def sleep(self, seconds): + def sleep(self, seconds: float) -> None: cc.sleep(seconds) self.send_command(f"sleep({seconds})\n") + _time.sleep(seconds) # block Python side while controller sleeps def move_joints( self, @@ -50,33 +60,34 @@ def move_joints( t: float = 0.0, radius: float = 0.0, ) -> None: - """ - MoveJ - -------- - Move the robot to the specified joint positions. + """MoveJ — move to joint positions. Parameters ---------- pos : list of float - Joint positions in radians [j1, j2, j3, j4, j5, j6]. - speed : float, optional - Speed of the movement in rad/s. - acceleration : float, optional - Acceleration of the movement in rad/s^2. - t : float, optional - The time in seconds to make the move. If specified, the command will ignore the speed and acceleration values. - radius : float, optional - Blend radius in meters. + Target joint positions in radians [j1..j6]. + speed : float + Speed in rad/s. + acceleration : float + Acceleration in rad/s^2. + t : float + Move duration in seconds (overrides speed/acceleration when > 0). + radius : float + Blend radius in metres. Non-zero disables blocking at this waypoint. """ cc.move_joints(self, pos, speed, acceleration) - - command = f"movej([{','.join(map(str, pos))}], a={acceleration}, v={speed}, t={t}, r={radius})\n" + command = ( + f"write_output_integer_register(0,0)\n" + f"movej([{','.join(map(str, pos))}]," + f" a={acceleration}, v={speed}, t={t}, r={radius})\n" + f"write_output_integer_register(0,1)\n" + ) self.send_command( command, timeout=t + 10, suppress_output=True, raw_response=False ) - - # while not all(round(a, 2) == round(b, 2) for a, b in zip(self.get_joint_positions(), joint_positions)): - # _sleep(2) + if radius == 0.0: + timeout = (t + 15.0) if t > 0 else 120.0 + self.rtde.wait_until_stopped(timeout=timeout) def move_cartesian( self, @@ -87,23 +98,22 @@ def move_cartesian( time: float = 0.0, radius: float = 0.0, ) -> None: - """ - Move the robot to the specified Cartesian position. + """Move to Cartesian pose. Parameters ---------- pose : list of float - Cartesian position and orientation [x, y, z, rx, ry, rz] in meters and radians. - move_type : str, optional - Type of movement: "movel" for linear Cartesian pathing, "movep" for circular Cartesian pathing, or "movej" for flexible real-time path tracking. - speed : float, optional - Velocity of the movement in m/s. - acceleration : float, optional - Acceleration of the movement in m/s^2. - time : float, optional - The time in seconds to make the move. If specified, the command will ignore the speed and acceleration values. - radius : float, optional - Blend radius in meters. + Target pose [x, y, z, rx, ry, rz] in metres and radians. + move_type : str + "movel" (linear), "movep" (circular), or "movej" (joint-space). + speed : float + Velocity in m/s. + acceleration : float + Acceleration in m/s^2. + time : float + Move duration in seconds (overrides speed/acceleration when > 0). + radius : float + Blend radius in metres. Non-zero disables blocking at this waypoint. """ if move_type not in {"movel", "movep", "movej"}: raise ValueError( @@ -112,80 +122,93 @@ def move_cartesian( cc.move_cartesian(self, pose) - # Construct the command based on the move type - base_command = f"{move_type}(p[{','.join(map(str, pose))}], a={acceleration}, v={speed}" + base = ( + f"{move_type}(p[{','.join(map(str, pose))}]," + f" a={acceleration}, v={speed}" + ) if move_type in {"movel", "movej"}: - command = f"{base_command}, t={time}, r={radius})\n" - else: # move_type == "movep" - command = f"{base_command}, r={radius})\n" + command = f"write_output_integer_register(0,0)\n{base}, t={time}, r={radius})\nwrite_output_integer_register(0,1)\n" + else: # movep + command = f"write_output_integer_register(0,0)\n{base}, r={radius})\nwrite_output_integer_register(0,1)\n" self.send_command(command, suppress_output=True) + if radius == 0.0: + timeout = (time + 15.0) if time > 0 else 120.0 + self.rtde.wait_until_stopped(timeout=timeout) - # while not all(round(a, 2) == round(b, 2) for a, b in zip(self.get_cartesian_position(), pose)): - # _sleep(2) + def stop_motion(self) -> None: + deceleration = 2.0 # rad/s^2 + self.send_command(f"stopj({deceleration})\n", suppress_output=True) def get_joint_positions(self) -> list[float]: - """ - Get the current joint positions of the robot. - - Returns - ------- - list of float - Joint positions in radians [j1, j2, j3, j4, j5, j6]. - """ + """Return actual joint positions in radians.""" angles = self.rtde.joint_angles() logger.receive(f"Received response: {angles}") return angles - def get_cartesian_position(self) -> list[float]: - """ - Retrieves the current Cartesian position of the robot's tool center point (TCP). + def get_joint_velocities(self) -> list[float]: + """Return actual joint velocities in rad/s.""" + vels = self.rtde.joint_velocities() + logger.receive(f"Received response: {vels}") + return vels - This method sends a command to the robot to obtain the actual TCP pose and - processes the response to return the Cartesian position. - - Returns: - list: A list representing the Cartesian position of the TCP in the format - [X, Y, Z, Rx, Ry, Rz], where X, Y, Z are the position coordinates in meters, - and Rx, Ry, Rz are the rotation vector components in radians. - """ - pose = self.rtde.tcp_pose() - logger.receive(f"Received response: {pose}") - return pose + def get_joint_currents(self) -> list[float]: + """Return actual joint currents in Amperes.""" + currents = self.rtde.joint_currents() + logger.receive(f"Received response: {currents}") + return currents def get_joint_torques(self) -> list[float]: - """ - Get the current joint torques of the robot. - - Returns - ------- - list of float - Joint torques in Nm [T1, T2, T3, T4, T5, T6]. - """ + """Return joint torques in Nm. Requires controller >= 5.23.0.0.""" torques = self.rtde.joint_torques() logger.receive(f"Received response: {torques}") return torques - def get_tcp_forces(self) -> list[float]: - """ - Get the current TCP forces of the robot. + def get_cartesian_position(self) -> list[float]: + """Return actual TCP pose [x, y, z, rx, ry, rz] in metres and radians.""" + pose = self.rtde.tcp_pose() + logger.receive(f"Received response: {pose}") + return pose - Returns - ------- - list of float - TCP forces [Fx, Fy, Fz, Tx, Ty, Tz] in Newton and Newton-meters. - """ + def get_tcp_speed(self) -> list[float]: + """Return actual TCP speed [vx, vy, vz, wx, wy, wz] in m/s and rad/s.""" + speed = self.rtde.tcp_speed() + logger.receive(f"Received response: {speed}") + return speed + + def get_tcp_forces(self) -> list[float]: + """Return TCP force [Fx, Fy, Fz, Tx, Ty, Tz] in Newton and Newton-metres.""" forces = self.rtde.tcp_force() logger.receive(f"Received response: {forces}") return forces - def stop_motion(self) -> None: - deceleration = 2.0 # rad/s^2 - self.send_command(f"stopj({deceleration})\n", suppress_output=True) + def get_target_joint_positions(self) -> list[float]: + """Return target joint positions in radians.""" + target = self.rtde.target_joint_positions() + logger.receive(f"Received response: {target}") + return target + + def get_target_joint_velocities(self) -> list[float]: + """Return target joint velocities in rad/s.""" + target = self.rtde.target_joint_velocities() + logger.receive(f"Received response: {target}") + return target + + def get_target_tcp_pose(self) -> list[float]: + """Return target TCP pose [x, y, z, rx, ry, rz] in metres and radians.""" + target = self.rtde.target_tcp_pose() + logger.receive(f"Received response: {target}") + return target + + def get_target_tcp_speed(self) -> list[float]: + """Return target TCP speed [vx, vy, vz, wx, wy, wz] in m/s and rad/s.""" + target = self.rtde.target_tcp_speed() + logger.receive(f"Received response: {target}") + return target def get_robot_state(self) -> dict[str, bool]: + """Return robot and safety status bit fields.""" status = self.rtde.robot_status() - key_out = [ "Power On", "Program Running", @@ -198,3 +221,82 @@ def get_robot_state(self) -> dict[str, bool]: + " ..." ) return status + + def get_robot_mode(self) -> RobotMode: + """Return robot mode as a RobotMode enum.""" + mode = self.rtde.robot_mode() + logger.receive(f"Received response: {mode}") + return mode + + def get_safety_mode(self) -> SafetyMode: + """Return safety mode as a SafetyMode enum.""" + mode = self.rtde.safety_mode() + logger.receive(f"Received response: {mode}") + return mode + + def get_runtime_state(self) -> RuntimeState: + """Return runtime state as a RuntimeState enum.""" + state = self.rtde.runtime_state() + logger.receive(f"Received response: {state}") + return state + + def get_speed_scaling(self) -> float: + """Return current speed scaling factor in [0, 1].""" + scaling = self.rtde.speed_scaling() + logger.receive(f"Received response: {scaling}") + return scaling + + def get_payload(self) -> dict: + """Return payload mass (kg) and centre of gravity (m).""" + payload = self.rtde.payload() + logger.receive(f"Received response: {payload}") + return payload + + def is_moving(self) -> bool: + """Return True if the robot is currently in motion.""" + return self.rtde.is_moving() + + def get_analog_inputs(self) -> dict[str, float]: + """Return standard and tool analog input values.""" + inputs = self.rtde.analog_inputs() + logger.receive(f"Received response: {inputs}") + return inputs + + def get_analog_outputs(self) -> dict[str, float]: + """Return standard analog output values.""" + outputs = self.rtde.analog_outputs() + logger.receive(f"Received response: {outputs}") + return outputs + + def get_digital_inputs(self) -> dict[str, bool]: + """Return digital input states (standard, configurable, tool).""" + inputs = self.rtde.digital_inputs() + logger.receive(f"Received response: {inputs}") + return inputs + + def get_digital_outputs(self) -> dict[str, bool]: + """Return digital output states (standard, configurable, tool).""" + outputs = self.rtde.digital_outputs() + logger.receive(f"Received response: {outputs}") + return outputs + + def get_tool_io(self) -> dict: + """Return tool I/O state (analog inputs, voltage, current, temperature).""" + tool = self.rtde.tool_io() + logger.receive(f"Received response: {tool}") + return tool + + def set_speed_slider(self, fraction: float) -> None: + """Set speed override slider fraction in [0.0, 1.0].""" + logger.send(f"Setting speed slider: {fraction}") + self.rtde.set_speed_slider(fraction) + + def set_digital_output(self, pin: int, value: bool) -> None: + """Set a standard digital output pin (0-7) high or low.""" + logger.send(f"Setting digital output DO{pin} = {value}") + self.rtde.set_digital_output(pin, value) + + def set_analog_output(self, channel: int, value: float) -> None: + """Set standard analog output voltage on channel 0 or 1.""" + logger.send(f"Setting analog output AO{channel} = {value}V") + self.rtde.set_analog_output(channel, value) diff --git a/uv.lock b/uv.lock index 3dbdd65..c57fe10 100644 --- a/uv.lock +++ b/uv.lock @@ -29,7 +29,7 @@ dependencies = [ { name = "typer", version = "0.23.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, { name = "typer", version = "0.24.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "zaber-motion", version = "3.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, - { name = "zaber-motion", version = "9.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8'" }, + { name = "zaber-motion", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8'" }, ] [package.optional-dependencies] @@ -42,7 +42,7 @@ dev = [ { name = "pytest", version = "7.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, - { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest", version = "9.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest-cov", version = "4.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, { name = "pytest-cov", version = "5.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, { name = "pytest-cov", version = "7.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, @@ -55,7 +55,7 @@ test = [ { name = "pytest", version = "7.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, - { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest", version = "9.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest-cov", version = "4.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, { name = "pytest-cov", version = "5.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, { name = "pytest-cov", version = "7.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, @@ -102,7 +102,7 @@ wheels = [ [[package]] name = "click" -version = "8.3.1" +version = "8.3.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.11'", @@ -111,9 +111,9 @@ resolution-markers = [ dependencies = [ { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, ] [[package]] @@ -664,7 +664,7 @@ wheels = [ [[package]] name = "packaging" -version = "26.0" +version = "26.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.11'", @@ -672,9 +672,9 @@ resolution-markers = [ "python_full_version == '3.9.*'", "python_full_version == '3.8.*'", ] -sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, ] [[package]] @@ -818,7 +818,7 @@ dependencies = [ { name = "colorama", marker = "python_full_version == '3.8.*' and sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version == '3.8.*'" }, { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, - { name = "packaging", version = "26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "packaging", version = "26.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, { name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, ] @@ -838,7 +838,7 @@ dependencies = [ { name = "colorama", marker = "python_full_version == '3.9.*' and sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version == '3.9.*'" }, { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, - { name = "packaging", version = "26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "packaging", version = "26.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, { name = "pygments", version = "2.20.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, @@ -850,7 +850,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.2" +version = "9.0.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.11'", @@ -860,14 +860,14 @@ dependencies = [ { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "packaging", version = "26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", version = "26.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pygments", version = "2.20.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] [[package]] @@ -916,7 +916,7 @@ dependencies = [ { name = "coverage", version = "7.13.5", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, - { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest", version = "9.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } wheels = [ @@ -955,48 +955,62 @@ wheels = [ [[package]] name = "rich" -version = "14.3.3" +version = "14.3.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "pygments", version = "2.19.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/67/cae617f1351490c25a4b8ac3b8b63a4dda609295d8222bad12242dfdc629/rich-14.3.4.tar.gz", hash = "sha256:817e02727f2b25b40ef56f5aa2217f400c8489f79ca8f46ea2b70dd5e14558a9", size = 230524, upload-time = "2026-04-11T02:57:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/76/6d163cfac87b632216f71879e6b2cf17163f773ff59c00b5ff4900a80fa3/rich-14.3.4-py3-none-any.whl", hash = "sha256:07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952", size = 310480, upload-time = "2026-04-11T02:57:47.484Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.11'", "python_full_version == '3.10.*'", "python_full_version == '3.9.*'", - "python_full_version == '3.8.*'", ] dependencies = [ - { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8' and python_full_version < '3.10'" }, + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "pygments", version = "2.19.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, { name = "pygments", version = "2.20.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, ] [[package]] name = "ruff" -version = "0.15.8" +version = "0.15.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/14/b0/73cf7550861e2b4824950b8b52eebdcc5adc792a00c514406556c5b80817/ruff-0.15.8.tar.gz", hash = "sha256:995f11f63597ee362130d1d5a327a87cb6f3f5eae3094c620bcc632329a4d26e", size = 4610921, upload-time = "2026-03-26T18:39:38.675Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264, upload-time = "2026-04-16T18:46:26.58Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/92/c445b0cd6da6e7ae51e954939cb69f97e008dbe750cfca89b8cedc081be7/ruff-0.15.8-py3-none-linux_armv6l.whl", hash = "sha256:cbe05adeba76d58162762d6b239c9056f1a15a55bd4b346cfd21e26cd6ad7bc7", size = 10527394, upload-time = "2026-03-26T18:39:41.566Z" }, - { url = "https://files.pythonhosted.org/packages/eb/92/f1c662784d149ad1414cae450b082cf736430c12ca78367f20f5ed569d65/ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d3e3d0b6ba8dca1b7ef9ab80a28e840a20070c4b62e56d675c24f366ef330570", size = 10905693, upload-time = "2026-03-26T18:39:30.364Z" }, - { url = "https://files.pythonhosted.org/packages/ca/f2/7a631a8af6d88bcef997eb1bf87cc3da158294c57044aafd3e17030613de/ruff-0.15.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ee3ae5c65a42f273f126686353f2e08ff29927b7b7e203b711514370d500de3", size = 10323044, upload-time = "2026-03-26T18:39:33.37Z" }, - { url = "https://files.pythonhosted.org/packages/67/18/1bf38e20914a05e72ef3b9569b1d5c70a7ef26cd188d69e9ca8ef588d5bf/ruff-0.15.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdce027ada77baa448077ccc6ebb2fa9c3c62fd110d8659d601cf2f475858d94", size = 10629135, upload-time = "2026-03-26T18:39:44.142Z" }, - { url = "https://files.pythonhosted.org/packages/d2/e9/138c150ff9af60556121623d41aba18b7b57d95ac032e177b6a53789d279/ruff-0.15.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12e617fc01a95e5821648a6df341d80456bd627bfab8a829f7cfc26a14a4b4a3", size = 10348041, upload-time = "2026-03-26T18:39:52.178Z" }, - { url = "https://files.pythonhosted.org/packages/02/f1/5bfb9298d9c323f842c5ddeb85f1f10ef51516ac7a34ba446c9347d898df/ruff-0.15.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:432701303b26416d22ba696c39f2c6f12499b89093b61360abc34bcc9bf07762", size = 11121987, upload-time = "2026-03-26T18:39:55.195Z" }, - { url = "https://files.pythonhosted.org/packages/10/11/6da2e538704e753c04e8d86b1fc55712fdbdcc266af1a1ece7a51fff0d10/ruff-0.15.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d910ae974b7a06a33a057cb87d2a10792a3b2b3b35e33d2699fdf63ec8f6b17a", size = 11951057, upload-time = "2026-03-26T18:39:19.18Z" }, - { url = "https://files.pythonhosted.org/packages/83/f0/c9208c5fd5101bf87002fed774ff25a96eea313d305f1e5d5744698dc314/ruff-0.15.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2033f963c43949d51e6fdccd3946633c6b37c484f5f98c3035f49c27395a8ab8", size = 11464613, upload-time = "2026-03-26T18:40:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/f8/22/d7f2fabdba4fae9f3b570e5605d5eb4500dcb7b770d3217dca4428484b17/ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f29b989a55572fb885b77464cf24af05500806ab4edf9a0fd8977f9759d85b1", size = 11257557, upload-time = "2026-03-26T18:39:57.972Z" }, - { url = "https://files.pythonhosted.org/packages/71/8c/382a9620038cf6906446b23ce8632ab8c0811b8f9d3e764f58bedd0c9a6f/ruff-0.15.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ac51d486bf457cdc985a412fb1801b2dfd1bd8838372fc55de64b1510eff4bec", size = 11169440, upload-time = "2026-03-26T18:39:22.205Z" }, - { url = "https://files.pythonhosted.org/packages/4d/0d/0994c802a7eaaf99380085e4e40c845f8e32a562e20a38ec06174b52ef24/ruff-0.15.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c9861eb959edab053c10ad62c278835ee69ca527b6dcd72b47d5c1e5648964f6", size = 10605963, upload-time = "2026-03-26T18:39:46.682Z" }, - { url = "https://files.pythonhosted.org/packages/19/aa/d624b86f5b0aad7cef6bbf9cd47a6a02dfdc4f72c92a337d724e39c9d14b/ruff-0.15.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8d9a5b8ea13f26ae90838afc33f91b547e61b794865374f114f349e9036835fb", size = 10357484, upload-time = "2026-03-26T18:39:49.176Z" }, - { url = "https://files.pythonhosted.org/packages/35/c3/e0b7835d23001f7d999f3895c6b569927c4d39912286897f625736e1fd04/ruff-0.15.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c2a33a529fb3cbc23a7124b5c6ff121e4d6228029cba374777bd7649cc8598b8", size = 10830426, upload-time = "2026-03-26T18:40:03.702Z" }, - { url = "https://files.pythonhosted.org/packages/f0/51/ab20b322f637b369383adc341d761eaaa0f0203d6b9a7421cd6e783d81b9/ruff-0.15.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:75e5cd06b1cf3f47a3996cfc999226b19aa92e7cce682dcd62f80d7035f98f49", size = 11345125, upload-time = "2026-03-26T18:39:27.799Z" }, - { url = "https://files.pythonhosted.org/packages/37/e6/90b2b33419f59d0f2c4c8a48a4b74b460709a557e8e0064cf33ad894f983/ruff-0.15.8-py3-none-win32.whl", hash = "sha256:bc1f0a51254ba21767bfa9a8b5013ca8149dcf38092e6a9eb704d876de94dc34", size = 10571959, upload-time = "2026-03-26T18:39:36.117Z" }, - { url = "https://files.pythonhosted.org/packages/1f/a2/ef467cb77099062317154c63f234b8a7baf7cb690b99af760c5b68b9ee7f/ruff-0.15.8-py3-none-win_amd64.whl", hash = "sha256:04f79eff02a72db209d47d665ba7ebcad609d8918a134f86cb13dd132159fc89", size = 11743893, upload-time = "2026-03-26T18:39:25.01Z" }, - { url = "https://files.pythonhosted.org/packages/15/e2/77be4fff062fa78d9b2a4dea85d14785dac5f1d0c1fb58ed52331f0ebe28/ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2", size = 11048175, upload-time = "2026-03-26T18:40:01.06Z" }, + { url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943, upload-time = "2026-04-16T18:46:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592, upload-time = "2026-04-16T18:46:00.742Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501, upload-time = "2026-04-16T18:46:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693, upload-time = "2026-04-16T18:46:41.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177, upload-time = "2026-04-16T18:46:21.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/71/504d79abfd3d92532ba6bbe3d1c19fada03e494332a59e37c7c2dabae427/ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d", size = 11221886, upload-time = "2026-04-16T18:46:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183, upload-time = "2026-04-16T18:46:07.944Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575, upload-time = "2026-04-16T18:46:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537, upload-time = "2026-04-16T18:46:36.988Z" }, + { url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813, upload-time = "2026-04-16T18:46:24.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136, upload-time = "2026-04-16T18:46:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701, upload-time = "2026-04-16T18:46:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/d3/57/bf0dfb32241b56c83bb663a826133da4bf17f682ba8c096973065f6e6a68/ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0", size = 10873887, upload-time = "2026-04-16T18:46:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316, upload-time = "2026-04-16T18:46:19.462Z" }, + { url = "https://files.pythonhosted.org/packages/88/27/0195d15fe7a897cbcba0904792c4b7c9fdd958456c3a17d2ea6093716a9a/ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3", size = 10655535, upload-time = "2026-04-16T18:46:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5e/c927b325bd4c1d3620211a4b96f47864633199feed60fa936025ab27e090/ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3", size = 11779692, upload-time = "2026-04-16T18:46:17.268Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614, upload-time = "2026-04-16T18:46:34.487Z" }, ] [[package]] @@ -1116,7 +1130,7 @@ resolution-markers = [ ] dependencies = [ { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, - { name = "rich", version = "14.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "rich", version = "14.3.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, { name = "shellingham", marker = "python_full_version == '3.8.*'" }, { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, ] @@ -1135,7 +1149,7 @@ resolution-markers = [ dependencies = [ { name = "annotated-doc", marker = "python_full_version == '3.9.*'" }, { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, - { name = "rich", version = "14.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "rich", version = "15.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, { name = "shellingham", marker = "python_full_version == '3.9.*'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d3/ae/93d16574e66dfe4c2284ffdaca4b0320ade32858cb2cc586c8dd79f127c5/typer-0.23.2.tar.gz", hash = "sha256:a99706a08e54f1aef8bb6a8611503808188a4092808e86addff1828a208af0de", size = 120162, upload-time = "2026-02-16T18:52:40.354Z" } @@ -1153,8 +1167,8 @@ resolution-markers = [ ] dependencies = [ { name = "annotated-doc", marker = "python_full_version >= '3.10'" }, - { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "rich", version = "14.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "click", version = "8.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "rich", version = "15.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "shellingham", marker = "python_full_version >= '3.10'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } @@ -1226,7 +1240,7 @@ wheels = [ [[package]] name = "zaber-motion" -version = "9.0.0" +version = "9.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.11'", @@ -1237,15 +1251,15 @@ resolution-markers = [ dependencies = [ { name = "reactivex", marker = "python_full_version >= '3.8'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/91/f8/78abe38a2f87e46c7a92652397c1fc5bfb2005c4e1e99ff6def64016941d/zaber_motion-9.0.0.tar.gz", hash = "sha256:7c0815cf9d329005154eebef14a2b15a58903c616f3eadfd916638d2889b740a", size = 289538, upload-time = "2026-03-27T18:05:51.018Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/65/5640219f251b1bcd1e5db86722a6dedfcf12a6ba421641661e9b50ff9373/zaber_motion-9.1.0.tar.gz", hash = "sha256:fa6d8e34e8f7a6ee6fe6e629ac524e1da3388710848ef0efb77502cc50450b68", size = 289832, upload-time = "2026-04-15T20:31:30.456Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/1e/f486ab7cc709c9468a09d368cf67c39e0d8a44e96ce31de4b91071b6964a/zaber_motion-9.0.0-py3-none-macosx_10_4_universal2.whl", hash = "sha256:3ca63e4d05b2f5af9c63970c4bf6172f01245be8f1f502705affbd52888fec2d", size = 14242832, upload-time = "2026-03-27T18:05:35.72Z" }, - { url = "https://files.pythonhosted.org/packages/a7/3d/ab66cbf528752b1bc053df01c0db2af63d90b70375b3431ee28502c6557e/zaber_motion-9.0.0-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:1fba29e95a69de75102765a031d0697b9e75d8c24691b5f3213b6e01983e2de8", size = 7168157, upload-time = "2026-03-27T18:05:38.381Z" }, - { url = "https://files.pythonhosted.org/packages/01/8c/fc0be225393401238a5b5ee011e513b66ca4ecefc4348f4f43a5d5fadfcb/zaber_motion-9.0.0-py3-none-manylinux_2_27_armv7l.whl", hash = "sha256:482c74b97db00b22a8a57cc32e11b4f47aae84178f1ed300a58300a2d4109ab9", size = 7470746, upload-time = "2026-03-27T18:05:40.458Z" }, - { url = "https://files.pythonhosted.org/packages/84/20/d2988a13733a58bb3c7817c298677948271d6c88ce069992a718a0b87fd0/zaber_motion-9.0.0-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4a8d338269a9ff17f324a590722cbe6581ba6fffdf1f65fc6a444f27d493a9ba", size = 7904790, upload-time = "2026-03-27T18:05:43.65Z" }, - { url = "https://files.pythonhosted.org/packages/89/24/a19c8ca18306e7ce79294af8d614b98df21337de7e750e0646ce04423990/zaber_motion-9.0.0-py3-none-win32.whl", hash = "sha256:ced1a6d2e8fecb475a726c2e32ea8fb21d9b86f4084fb6cf12b3f64cb85ba735", size = 7522738, upload-time = "2026-03-27T18:05:45.477Z" }, - { url = "https://files.pythonhosted.org/packages/cc/1a/f0448385741ea2ed175b716dc8071586b86f57b1784a9c2e58c30942c83a/zaber_motion-9.0.0-py3-none-win_amd64.whl", hash = "sha256:2d7b99975ece3f169c7aede9b8ac2b0f22164cb8f69f015f5cc4f9f75eb4919f", size = 7695820, upload-time = "2026-03-27T18:05:47.551Z" }, - { url = "https://files.pythonhosted.org/packages/8e/c7/2183999ca20634edb7e9e94c83cbe6817116119a7c5fd63977acd34e7a84/zaber_motion-9.0.0-py3-none-win_arm64.whl", hash = "sha256:883a72ecfed429bd05d374efb3ccb7cb56d006730bc93075dca3e9154f51269b", size = 6971742, upload-time = "2026-03-27T18:05:49.66Z" }, + { url = "https://files.pythonhosted.org/packages/ad/7e/eb8b9c7c869c0c7a0136c7a6cc9b034f0802b4ae0b1ec063c57141fcf2df/zaber_motion-9.1.0-py3-none-macosx_10_4_universal2.whl", hash = "sha256:8e47700a0bf9828db4e041252fdcfec4dc217edca3d602da43cd1defe6abc841", size = 14289215, upload-time = "2026-04-15T20:31:14.45Z" }, + { url = "https://files.pythonhosted.org/packages/57/c3/43d5615d1b8ff0d2ebd957c18ab618667d3b64c398be47836d817882d8aa/zaber_motion-9.1.0-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b0643110f530a4b417d232600bb1a482df5a09fb8c31e3d43edd57eb00863589", size = 7184455, upload-time = "2026-04-15T20:31:17.384Z" }, + { url = "https://files.pythonhosted.org/packages/db/45/d28a864439297d4d22354a8c7537f2554e8aecd294db76229b15ef8d3aa1/zaber_motion-9.1.0-py3-none-manylinux_2_27_armv7l.whl", hash = "sha256:4365187c3820418525041ebf1709e1d8b10031bb6fc9e8582546338b360fd52c", size = 7486231, upload-time = "2026-04-15T20:31:20.275Z" }, + { url = "https://files.pythonhosted.org/packages/95/65/fce959f0e16026d06ea23381b6c1d3d629bd47ce40f2fffcfe08f590f6b5/zaber_motion-9.1.0-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:2d7aa02885adecf674ae83820ff5f6aa385fe78ed82e19455f6bf340d0a43cfd", size = 7920766, upload-time = "2026-04-15T20:31:22.601Z" }, + { url = "https://files.pythonhosted.org/packages/a6/38/888257e8dee2296ad8055c014daea324780b98314b6cfa4d8d1f69acc1a9/zaber_motion-9.1.0-py3-none-win32.whl", hash = "sha256:3ffa6409687214043bff8c4bcd8d7a1cc1110965c33d9f5f796d79ff212ba247", size = 7537809, upload-time = "2026-04-15T20:31:24.671Z" }, + { url = "https://files.pythonhosted.org/packages/90/93/59dbcb3e052eed20c46075341bf36e31b87c7551c1ad115c13d0ff81dc4e/zaber_motion-9.1.0-py3-none-win_amd64.whl", hash = "sha256:baa2867d5208364de7fdf3b0f44d789bf8d1b47e2503e5ddbe890e640cd712a4", size = 7711906, upload-time = "2026-04-15T20:31:26.628Z" }, + { url = "https://files.pythonhosted.org/packages/48/3a/b1619dfe976bc31a7ea4b8df4f4bada6d69f47415e184abc797fe021551c/zaber_motion-9.1.0-py3-none-win_arm64.whl", hash = "sha256:9af3208a5bcbe51eef79f709f6c6d64b0ba5e889658eba494d4a32c930e787c6", size = 6987533, upload-time = "2026-04-15T20:31:28.73Z" }, ] [[package]]