Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed47d61780 | ||
|
|
e0f5e332ca | ||
|
|
b237f4cc9a | ||
|
|
9d7f31072d | ||
|
|
e3fc538e5d | ||
|
|
eede2e5774 | ||
|
|
7a0847893c | ||
|
|
7c1c33f5b1 |
@@ -46,16 +46,19 @@ uv sync --group dev # plus pytest and ruff
|
||||
|
||||
```bash
|
||||
dora run dataflow.yml --uv # this node plus a bench stand-in, no pad
|
||||
dora run dataflow-gamepad.yml --uv # the same, with dora-joy pulled in by git:
|
||||
dora run dataflow-robot.yml # the pad and a real arm, both pulled in by git:
|
||||
dora run dataflow-gamepad.yml # the same, with the probe declared and no console
|
||||
```
|
||||
|
||||
`dataflow.yml` is self-contained on purpose — it resolves from this checkout alone, which
|
||||
is what lets CI validate it. Keep it that way; the pad belongs in `dataflow-gamepad.yml`.
|
||||
is what lets CI validate it. Keep it that way; the pad belongs in the other two. Neither
|
||||
of those takes `--uv`: dora 1.0.1 installs a `git:` node into a venv inside its own clone
|
||||
and then looks for its entrypoint in the dataflow's venv. README.md has the detail.
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
uv run pytest # 112 tests, no gamepad, arm, browser or daemon needed
|
||||
uv run pytest # 149 tests, no gamepad, arm, browser or daemon needed
|
||||
uv run ruff check .
|
||||
uv run ruff format --check .
|
||||
```
|
||||
@@ -74,6 +77,14 @@ a source goes away**.
|
||||
- **`dora_teleop/joy.py`** — The pad as it arrives from `dora-joy`. `Sample.present` is
|
||||
the load-bearing field: an unplugged pad reads as *absent*, not as centred, so "the
|
||||
operator let go" and "the pad fell off the desk" stay distinguishable.
|
||||
**`Calibration` is the other half of that seam.** It measures a pad the first time it is
|
||||
seen at rest and hands the rest of the node a reading as the layout's ideal pad would
|
||||
have sent it, so a worn stick's tilt is subtracted rather than deadzoned and a trigger
|
||||
that rests at 0 stops being a special case. A pad whose resting axes do not fit `Layout`
|
||||
reads as *absent* and is never corrected: one reading cannot tell a trigger at -1.0 from
|
||||
a stick held hard over, and guessing wrong drives a real arm. It is not re-measured
|
||||
while the pad is in use — doing that under a held stick would record the deflection as
|
||||
centre, and the arm would move when the operator let go.
|
||||
**`JoyInput.read` is the other safety-critical function in this repo.** Moving the pad
|
||||
into another process added a way for it to go away that did not exist before — that
|
||||
process dying — and from here that looks like silence rather than like an unplugged pad.
|
||||
@@ -90,7 +101,12 @@ a source goes away**.
|
||||
units: mm/s and deg/s, base frame. `is_active` lives here rather than on `Sample`
|
||||
because it needs the layout: **a trigger rests at -1.0**, so the pad-is-in-use test has
|
||||
to normalise triggers exactly as `velocity` does, or every connected pad reads as in use
|
||||
for ever and `GAMEPAD_IDLE_RELEASE_S` never fires.
|
||||
for ever and `GAMEPAD_IDLE_RELEASE_S` never fires. `rest_zeros` and `canonical` are the
|
||||
same concern one layer out: they check a resting pad against `Layout` and map it onto
|
||||
the ideal one. They are the only defence against a wrong index, which is not a
|
||||
theoretical failure — an Xbox One S pad reporting its triggers on 4 and 5 against a
|
||||
layout expecting them on 2 and 5 read as a full-speed pitch command from an untouched
|
||||
pad, held the lease for ever, and moved a real arm into a controller error.
|
||||
- **`dora_teleop/arbiter.py`** — The lease. One place that answers "why did it not move",
|
||||
rather than one answer per input path. Ported from the monolithic repo's `RobotService`
|
||||
lease because the console is already built on it — it gates its controls on
|
||||
@@ -170,6 +186,13 @@ belongs here, and a port nothing wires reads as supported while having never run
|
||||
pair; nothing can enforce the second from here.
|
||||
- **An unplugged pad, a dead `dora-joy` or a departed browser stops the arm immediately**,
|
||||
without waiting out any timer beyond the one that detects it.
|
||||
- **A pad is driven from only after it has been seen at rest**, and a pad whose rest does
|
||||
not fit `Layout` is not driven from at all. This covers the axes and **cannot cover the
|
||||
buttons**: an unpressed button reads the same on every pad, so a wrong button index is
|
||||
silent until somebody presses it and gets another verb. Sweeping a new pad with the
|
||||
probe is the only check, and it is worth doing before the arm is powered. The cost is that a pad held when it appears
|
||||
is ignored until it is let go; the alternative is guessing a numbering from one reading,
|
||||
against a real arm.
|
||||
- **Both control sources go through `mapping`.** The web path used to bypass it and pass
|
||||
a velocity straight through, which is why the console had to speak mm/s. One conversion
|
||||
point or none.
|
||||
|
||||
@@ -1,433 +1,120 @@
|
||||
# dora-teleop
|
||||
|
||||
A dora-rs node that hands a robot arm to a human. Pick up a gamepad and move the arm, or
|
||||
open a browser and move it from there — one of the two at a time, decided here.
|
||||
dora-rs teleoperation node: normalises two control sources — a gamepad read from
|
||||
`dora-joy`, and a web console served by this node itself — into one owner's lease, and
|
||||
emits velocity set-points and robot-agnostic commands. It carries no hardware driver: pad
|
||||
I/O lives in `dora-joy`, arm I/O in a robot node.
|
||||
|
||||
It drives [`dora-ulite6`](https://gitea.liria.cl/liria/dora-ulite6) through that node's
|
||||
ordinary `jog` and `request` ports, reads the pad from
|
||||
[`dora-joy`](https://gitea.liria.cl/liria/dora-joy), and **serves the browser console
|
||||
itself** on one port. It contains no robot driver and no gamepad driver: it is the piece
|
||||
in the middle that decides what the arm should be doing, and that decision is the only
|
||||
thing it owns. It touches no hardware at all.
|
||||
**Requirements:** dora 1.0+ (verified with `dora-cli 1.0.1`), Python 3.12+, `uv`.
|
||||
|
||||
## The gamepad
|
||||
## System
|
||||
|
||||
The pad itself is [`dora-joy`](https://gitea.liria.cl/liria/dora-joy), which publishes raw
|
||||
axes and buttons and knows nothing about arms. Everything below — the deadzone, the
|
||||
response curve, which button is which, and what any of it means in mm/s — happens here.
|
||||
```mermaid
|
||||
flowchart LR
|
||||
pad([gamepad]) --> joy
|
||||
browser([browser]) -- "ws: held directions, verbs" --> web
|
||||
web -- "ui_state snapshot" --> browser
|
||||
joy["dora-joy — sibling repo"]
|
||||
subgraph teleop["dora-teleop"]
|
||||
web["web console — built in, HTTP + ws on one port"]
|
||||
core["command arbitration"]
|
||||
end
|
||||
joy -- "joy: axes, buttons, present" --> core
|
||||
web -- "held directions, ±1" --> core
|
||||
core -- "twist SI · command verbs (standard)" --> arm
|
||||
core -- "jog · request (deprecated)" --> arm
|
||||
arm -. "state · poses · response" .-> core
|
||||
arm["robot node"]
|
||||
```
|
||||
|
||||
Analog and blended — several axes at once, and how fast depends on how far you push. That
|
||||
is the whole reason a pad beats the console, whose ring sectors ask for one direction at
|
||||
the speed the dial is set to. The console can still hold two at once — a sector and a Z
|
||||
pill — but it cannot ask for a third of full speed on one axis and full on another.
|
||||
|
||||
| Control | Does | Units |
|
||||
|-------------|-------------------------------------------|-------|
|
||||
| Left stick | Move along X (up/down) and Y (left/right) | mm/s |
|
||||
| Triggers | Lift on the right, lower on the left | mm/s |
|
||||
| Right stick | Roll (left/right) and pitch (up/down) | deg/s |
|
||||
| Bumpers | Yaw, left and right | deg/s |
|
||||
| A | Close the tool | — |
|
||||
| B | Open the tool | — |
|
||||
| X | Release the E-STOP and clear the arm's error | — |
|
||||
| Y | Toggle freedrive | — |
|
||||
| Back | **E-STOP** — latches until X | — |
|
||||
|
||||
Up on the stick sends the arm away from you, along **+X** in the robot's base frame —
|
||||
the same convention the web console's dial uses, so the two agree.
|
||||
|
||||
Indices are SDL's, which is what most Xbox- and PlayStation-style pads report. A pad that
|
||||
disagrees is a change to `Layout` in `dora_teleop/mapping.py` rather than a new
|
||||
environment variable: an index nobody can verify from outside is worse than a line of code
|
||||
somebody can read. The layout lives here rather than in `dora-joy` for the same reason —
|
||||
`dora-joy` does not know what a button is for, which is what makes it reusable.
|
||||
|
||||
`home` is deliberately **not** on the pad. It is a large unattended motion and belongs
|
||||
behind a deliberate click, not a thumb resting on a face button.
|
||||
|
||||
**Back latches.** It stops the arm, tells the controller to stop, and then keeps the arm
|
||||
still no matter what the sticks do — a stop somebody had to reach for is not undone by the
|
||||
next thumb movement. X is the only way back, and it does both halves: releases the latch
|
||||
here and clears the error on the controller. Neither button is gated on the lease, because
|
||||
an idle pad hands the arm back two seconds after the stop it just made, and a latch you
|
||||
must first take the arm back to release is a trap.
|
||||
|
||||
### Checking your pad
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
dora up
|
||||
dora start dataflow-gamepad.yml --uv --name teleop-bench
|
||||
uv run dora-teleop-probe # in another terminal
|
||||
uv sync
|
||||
dora run dataflow.yml --uv # teleop + bench stand-in: no pad, no arm
|
||||
```
|
||||
|
||||
Prints the raw axes, the buttons it sees, and the velocity this node would send — live,
|
||||
from the same `joy` stream the node reads. **It cannot move anything.** It attaches as a
|
||||
dynamic node, publishes nothing and never talks to the arm; it only reads.
|
||||
|
||||
```text
|
||||
axes (6 reported)
|
||||
0 left stick X +0.600 ----------|-----#----
|
||||
1 left stick Y -0.800 --#-------|----------
|
||||
2 left trigger -1.000 #---------|----------
|
||||
...
|
||||
buttons (6 reported)
|
||||
5:yaw right
|
||||
|
||||
velocity this would send to the arm
|
||||
vx +38.89 mm/s forward / back <--
|
||||
vy +23.53 mm/s left / right <--
|
||||
vz +20.39 mm/s up / down <--
|
||||
wz +30.00 deg/s yaw <--
|
||||
|
||||
moving: yes
|
||||
asking for the arm: yes
|
||||
```
|
||||
|
||||
Run this before trusting the table above. The indices are SDL's documented layout, not
|
||||
observed ones, and a pad that numbers its axes differently is much better found here than
|
||||
by watching an arm move the wrong way. Two things to look for:
|
||||
|
||||
- **Every axis is listed, including ones the layout does not use** — an axis marked
|
||||
`unused` that moves when you push a stick means the layout needs changing.
|
||||
- **A bar that does not sit centred with the stick released** is a worn pad announcing
|
||||
itself, and a reason to raise `DEADZONE`.
|
||||
|
||||
It goes through the same `velocity()` the node runs, on the same readings, so what it
|
||||
prints is what the arm would be sent.
|
||||
|
||||
For a pad whose layout is not known at all — which index is even which — use
|
||||
`dora-joy-probe` from the `dora-joy` repo, which needs no dataflow and no coordinator.
|
||||
|
||||
## Speaking to any robot
|
||||
|
||||
Nothing above is specific to one arm. Both control sources are reduced to a **normalised**
|
||||
six-vector — `[-1, 1]` per degree of freedom, meaning a fraction of full speed — and only
|
||||
then scaled, in one function. A gamepad reaches that vector through the deadzone and the
|
||||
response curve, which exist to correct a worn physical stick; the browser sends it already
|
||||
clean, because a browser has no worn centre.
|
||||
|
||||
That is what lets the same node and the same console drive a robot in mm/s and one in m/s:
|
||||
neither source ever knew the difference. It is also why a gamepad and the console asking
|
||||
for the same deflection produce byte-identical output.
|
||||
|
||||
`twist` is SI — m/s and rad/s — because that is unambiguous and is what a UR or MoveIt
|
||||
takes. Configuration stays in mm/s and deg/s, because `60 mm/s` is a number a person
|
||||
reading a dataflow can judge.
|
||||
|
||||
`command` carries verbs rather than service names: `gripper_close` rather than `grasp`,
|
||||
so a robot that swaps a vacuum for a two-finger gripper needs no change at either end.
|
||||
|
||||
The full contract, including the reserved `joint_jog` port for joint-space robots, is in
|
||||
the vault at `resources/robotics/teleop-contract.md`.
|
||||
|
||||
## The lease
|
||||
|
||||
Two sources, one arm. Whoever holds the lease drives; the other is ignored.
|
||||
|
||||
- The **gamepad takes it implicitly**, the first time a stick moves, if nobody holds it.
|
||||
Nobody should have to reach for a browser to use the thing in their hands.
|
||||
- It **gives it back** after `GAMEPAD_IDLE_RELEASE_S` untouched, so a pad left on the
|
||||
bench does not lock the browser out forever.
|
||||
- The **browser takes it explicitly**, with the console's TAKE CONTROL button, and gives
|
||||
it back with RELEASE. It can preempt a held arm, because an operator who can see the arm
|
||||
is better placed than this code to decide they should have it.
|
||||
- **E-STOP is never gated.** From either source, held lease or not. Somebody reaching for
|
||||
the stop is the one moment when asking who owns the arm is the wrong question.
|
||||
|
||||
**The lease binds only what goes through this node.** `dora-ulite6` has no idea it exists.
|
||||
Another node publishing `request` will preempt an operator mid-motion and nothing here can
|
||||
stop it — if that matters for your dataflow, do not wire one.
|
||||
|
||||
## The deadman
|
||||
|
||||
An arm under a velocity command keeps moving until told otherwise, so the question is
|
||||
never whether to start but whether anything has happened that means it must stop. Four
|
||||
layers, outermost last:
|
||||
|
||||
1. **The repeat** (`JOG_KEEPALIVE_S`, 0.2 s) while a source is driving.
|
||||
2. **The pad's freshness** (`JOY_TTL_S`, 0.2 s). The pad is another process now, and that
|
||||
process dying looks like silence rather than like an unplugged pad — so silence means
|
||||
the same thing. `dora-joy` publishes on every tick whether or not the pad moved, which
|
||||
is what makes silence unambiguous.
|
||||
3. **This node's watchdog** (`JOG_TTL_S`, 0.4 s) stops the arm when the owner goes quiet —
|
||||
and stops it *immediately*, without waiting out the timer, when the gamepad reports
|
||||
itself gone or the last browser disconnects. An unplugged pad is not a centred pad.
|
||||
4. **The arm's own expiry** (`VELOCITY_COMMAND_TTL_S` in `dora-ulite6`, 0.5 s), enforced by
|
||||
the controller whether or not this process is alive.
|
||||
|
||||
Keep them in that order, each shorter than the one behind it. A backstop that fires before
|
||||
the thing it is backing up backs up nothing. `JOY_TTL_S >= JOG_TTL_S` stops the node at
|
||||
startup rather than letting that happen quietly.
|
||||
|
||||
On shutdown the node sends one all-zero velocity before exiting. A motion lives in the
|
||||
controller, not in this process.
|
||||
|
||||
The stop is sent **once**, not every tick. A stream of zeroes would keep `dora-ulite6`
|
||||
believing a jog is live and hold `robot_state` at `moving` forever.
|
||||
|
||||
### What goes on the wire is the change
|
||||
|
||||
A velocity is a set-point the arm holds, not a frame in a sequence. This node sends one
|
||||
when it **changes** — a new direction, or the stop — and otherwise repeats the current one
|
||||
only every `JOG_KEEPALIVE_S` (0.2 s), which exists solely so the arm's own expiry never
|
||||
fires mid-motion.
|
||||
|
||||
It used to repeat every tick. On a real xArm that means the controller is handed the same
|
||||
velocity twenty times a second and re-plans the same motion each time, which an operator
|
||||
feels as the arm moving in steps rather than travelling. Keep the repeat well under the
|
||||
arm's `VELOCITY_COMMAND_TTL_S` and no faster than it needs to be.
|
||||
|
||||
**A change never waits for the repeat.** Releasing a button is acted on the tick it
|
||||
happens, so sending less does not cost anything at the operator's end.
|
||||
|
||||
## Ports
|
||||
|
||||
| Direction | Name | Description |
|
||||
|-----------|----------------|------------------------------------------------------|
|
||||
| in | `tick` | 50 ms — change detection, deadman, telemetry |
|
||||
| in | `joy` | One pad reading from `dora-joy` |
|
||||
| in | `response` | An answer from the arm |
|
||||
| in | `motion_state` | How a motion ended |
|
||||
| in | `robot_state` | The arm's own state |
|
||||
| in | `tcp_pose` | Tool-tip pose, six float32, mm and degrees |
|
||||
| in | `joint_pose` | Joint angles, six float32, degrees |
|
||||
| out | `twist` | **Standard.** `{linear, angular, frame}`, SI |
|
||||
| out | `command` | **Standard.** `{action, value}`, robot-agnostic verbs |
|
||||
| out | `jog` | *Deprecated.* mm/s + deg/s, for `dora-ulite6` |
|
||||
| out | `request` | *Deprecated.* A service call, `request_id` in metadata |
|
||||
| out | `ui_state` | The console's telemetry snapshot, as JSON — see below |
|
||||
|
||||
Wire `twist` **or** `jog`, never both: they carry the same intent, and a robot reading
|
||||
both is driven twice. Same for `command` and `request`.
|
||||
|
||||
The browser is not on this table. It reaches the node over the node's own HTTP and
|
||||
WebSocket port rather than through the dataflow, so there is nothing to wire for it —
|
||||
`ui_state` is published for anyone else watching and has no consumer in the stacks below.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|--------------------------|---------|--------------------------------------------------------|
|
||||
| `SPEED_MM_S` | `60.0` | Translation speed at full deflection |
|
||||
| `SPEED_DEG_S` | `30.0` | Rotation speed at full deflection |
|
||||
| `DEADZONE` | `0.12` | Stick travel near centre to ignore |
|
||||
| `EXPO` | `0.4` | How much of the curve is cubic; 0 is linear |
|
||||
| `JOG_TTL_S` | `0.4` | Owner silence before the arm is stopped |
|
||||
| `JOY_TTL_S` | `0.2` | `joy` silence before the pad reads as gone |
|
||||
| `GAMEPAD_IDLE_RELEASE_S` | `2.0` | Untouched pad before it hands the arm back |
|
||||
| `STATE_POLL_S` | `0.5` | How often to ask the arm what telemetry does not carry |
|
||||
|
||||
A value that cannot be read, a `DEADZONE` outside the stick, an `EXPO` off the curve or a
|
||||
`JOY_TTL_S` at or above `JOG_TTL_S` stops the node at startup rather than the first time
|
||||
somebody picks up the pad.
|
||||
|
||||
`JOYSTICK_INDEX` moved to `dora-joy` along with the pad.
|
||||
|
||||
## Usage
|
||||
|
||||
The real stack is three nodes: this one, the pad and the arm. The console comes with
|
||||
this node.
|
||||
|
||||
```yaml
|
||||
- id: teleop
|
||||
git: ssh://git@gitea.liria.cl:5001/liria/dora-teleop.git
|
||||
tag: v0.3.0
|
||||
build: pip install .
|
||||
path: dora-teleop
|
||||
inputs:
|
||||
tick: dora/timer/millis/50
|
||||
joy:
|
||||
source: joy/joy
|
||||
queue_size: 1
|
||||
queue_policy: drop_oldest
|
||||
response: ulite6/response
|
||||
motion_state: ulite6/motion_state
|
||||
robot_state: ulite6/robot_state
|
||||
tcp_pose: ulite6/tcp_pose
|
||||
joint_pose: ulite6/joint_pose
|
||||
outputs: [jog, request, ui_state]
|
||||
env:
|
||||
WEB_PORT: "7000"
|
||||
```
|
||||
|
||||
Wire `ulite6`'s `request` and `jog` from this node, and add `dora-joy` on a 20 ms tick for
|
||||
the pad — `queue_size: 1` matters, because it ticks faster than this node does and an arm
|
||||
should be driven by the newest reading or by none.
|
||||
|
||||
Then open `http://<the machine running teleop>:7000` for the console. Nothing else needs
|
||||
wiring: the node serves the page and holds the browsers' sockets, and the page finds the
|
||||
socket on its own origin.
|
||||
|
||||
Leave `joy` unwired for a browser-only console; the node starts without it. Set
|
||||
`WEB_PORT: "0"` for a pad-only one, and no port is opened.
|
||||
|
||||
**Requirements:** dora 1.0+ (verified with `dora-cli 1.0.1`), Python 3.11+ and `uv`. No
|
||||
hardware: the pad and the arm are other nodes, and the browser is any browser.
|
||||
|
||||
### Driving a real arm
|
||||
|
||||
`dataflow-robot.yml` is the one that moves a robot: the pad, this node and
|
||||
[`dora-ulite6`](https://gitea.liria.cl/liria/dora-ulite6), with no bench and no browser.
|
||||
Both sibling nodes come in by `git:`, so it runs from a fresh clone with nothing else
|
||||
checked out.
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate # see the note below before reaching for --uv
|
||||
dora build dataflow-robot.yml # clones and installs both, needs the network
|
||||
dora run dataflow-robot.yml
|
||||
```
|
||||
|
||||
**`--uv` does not work for a dataflow with `git:` nodes** (dora 1.0.1). `dora build --uv`
|
||||
installs each `git:` node into a venv inside its own clone directory, while `dora run
|
||||
--uv` resolves the entrypoint by running `uv run which <name>` from the *dataflow's*
|
||||
directory — which only ever sees the dataflow's own venv. The node is installed where the
|
||||
spawner does not look, and the run fails with `Could not find source path dora-joy within
|
||||
uv`. Building into one activated environment, as above, puts every node's entrypoint
|
||||
somewhere the spawner finds it. The venv needs `pip` for that (`uv pip install pip`),
|
||||
since `uv` does not create one.
|
||||
|
||||
`dataflow.yml` is unaffected and `--uv` is fine there: its only Python node is this repo,
|
||||
which is already in this repo's venv.
|
||||
|
||||
Set `ROBOT_IP` and `TOOL_TYPE` in that file to match the arm in front of you; the defaults
|
||||
are `dora-ulite6`'s own, `192.168.1.192` and a vacuum tool. `INIT_MODE` is `none`, so
|
||||
nothing moves until somebody pushes a stick.
|
||||
|
||||
The deadman timings have to stay ordered, each below the one behind it, and the file pins
|
||||
all of them so the chain is visible in one place:
|
||||
|
||||
| Layer | Setting | Default |
|
||||
|---|---|---|
|
||||
| Pad reading goes stale | `JOY_TTL_S` (teleop) | 0.2 s |
|
||||
| Held velocity repeated | `JOG_KEEPALIVE_S` (teleop) | 0.2 s |
|
||||
| Owner goes quiet | `JOG_TTL_S` (teleop) | 0.4 s |
|
||||
| Jog sender goes quiet | `VELOCITY_COMMAND_TTL_S` (ulite6) | 0.5 s |
|
||||
|
||||
The last one is enforced by `dora-ulite6` itself from v0.3.1 on, not by the controller:
|
||||
the velocity is set once and held, so that node stops the arm rather than letting a
|
||||
controller-side timer do it.
|
||||
|
||||
`dora` type-checks a dataflow before it fetches a `git:` source, so it cannot read the
|
||||
arm's manifest from here. The `output_types:` block on the `ulite6` node repeats what
|
||||
`dora-ulite6` v0.3.1 annotates, which is also what pinning that tag asserts — re-check it
|
||||
when moving to a newer one.
|
||||
|
||||
The console is already there — `dataflow-robot.yml` sets `WEB_PORT` to 7000, so opening
|
||||
`http://<that machine>:7000` gets you the same arm the pad drives, under the same lease.
|
||||
|
||||
### Running it with no arm and no browser
|
||||
|
||||
`dataflow.yml` in this repo is **not** that stack. It is this node plus a bench stand-in,
|
||||
with **no pad**: pulling `dora-joy` in by `git:` would cost that file the one property it
|
||||
has, which is that it resolves and runs from this checkout alone, with no network and no
|
||||
sibling clones.
|
||||
|
||||
```bash
|
||||
dora run dataflow.yml --uv # bench only, browser path
|
||||
dora run dataflow-gamepad.yml --uv # the same bench with a real pad
|
||||
```
|
||||
|
||||
`dataflow-gamepad.yml` adds `dora-joy` by `git:` and declares the probe. It needs the
|
||||
network on its first build.
|
||||
|
||||
The bench answers every service, publishes plausible telemetry, and prints each velocity
|
||||
it is sent:
|
||||
|
||||
```text
|
||||
jog 1 [ +38.89, +23.53, +0.00, +0.00, +0.00, +0.00]
|
||||
jog 2 [ +38.89, +23.53, +0.00, +0.00, +0.00, +0.00]
|
||||
jog 3 [ +0.00, +0.00, +0.00, +0.00, +0.00, +0.00] <- stop
|
||||
```
|
||||
|
||||
It is **not a simulator**: it never integrates the velocity or moves the pose, and a jog
|
||||
is printed and forgotten. It closes the loop so this node's behaviour can be watched —
|
||||
the lease changing hands, the deadman firing when you let go — not so the arm can be
|
||||
pretended at.
|
||||
Open `http://localhost:7000` and drive the robot from the console. The bench answers
|
||||
services and publishes telemetry so the loop closes.
|
||||
|
||||
### Watching the wire
|
||||
|
||||
`dataflow.yml` sets `debug: enable_debug_inspection: true`, so dora will let you read what
|
||||
is actually on the wire without the bench printing anything. Run it under a coordinator
|
||||
rather than `dora run`:
|
||||
Every dataflow sets `debug: enable_debug_inspection: true`, so dora can inspect the wire
|
||||
without any node printing. This needs a coordinator, so it works for the arm-free dataflow:
|
||||
|
||||
```bash
|
||||
dora up
|
||||
dora start dataflow-gamepad.yml --uv --name teleop-bench
|
||||
```
|
||||
|
||||
Then, in another terminal:
|
||||
|
||||
```bash
|
||||
dora start dataflow.yml --uv --name teleop-bench
|
||||
dora topic list -d teleop-bench # every edge in the graph
|
||||
dora topic echo -d teleop-bench joy/joy # what the pad is reporting
|
||||
dora topic echo -d teleop-bench teleop/jog # every velocity, live
|
||||
dora topic echo -d teleop-bench teleop/ui_state # what the console would draw
|
||||
dora topic hz -d teleop-bench teleop/jog --duration 10 # the re-send cadence
|
||||
dora stop --name teleop-bench && dora down
|
||||
```
|
||||
|
||||
`hz` is the one that checks the timings this node promises. The `get_state` poll measures
|
||||
at 1.88 Hz against a configured `STATE_POLL_S` of 0.5, and with a pad held over the
|
||||
deadzone `teleop/jog` should sit near the 50 ms tick.
|
||||
## Running with hardware
|
||||
|
||||
Tear it down with `dora stop --name teleop-bench && dora down`.
|
||||
| Dataflow | Nodes | Needs |
|
||||
|------------------------|--------------------------------------|--------------------|
|
||||
| `dataflow.yml` | teleop + bench stand-in | nothing |
|
||||
| `dataflow-robot.yml` | joy + teleop + ulite6 | a pad and an arm |
|
||||
| `dataflow-gamepad.yml` | the same, plus the probe, no console | a pad and an arm |
|
||||
|
||||
## The console
|
||||
## Gamepad
|
||||
|
||||
`dora_teleop/web/index.html` is the browser half: two analog sticks, a lift lane and a
|
||||
yaw lane, plus gripper, freedrive, home, the lease buttons and E-STOP over a telemetry
|
||||
readout. Plain HTML with no build step — the node serves the file as it stands, and a
|
||||
console you cannot fix without a toolchain is the wrong thing to have between a person and
|
||||
a robot.
|
||||
| Control | Does (base frame) | Index | Units |
|
||||
|----------------------|----------------------------------------------------------------|--------------|-------|
|
||||
| Left stick X / Y | Translate in the horizontal plane: `+X` stick up, `+Y` stick right | axes 0, 1 | mm/s |
|
||||
| Left / right trigger | Translate along Z: right trigger lifts (`+Z`), left lowers | axes 4, 5 | mm/s |
|
||||
| Right stick X / Y | Rotate: `+roll` stick right, `+pitch` stick up | axes 2, 3 | deg/s |
|
||||
| LB / RB | Yaw: RB is `+yaw` | buttons 6, 7 | deg/s |
|
||||
| A | Close the tool | button 0 | — |
|
||||
| B | Open the tool | button 1 | — |
|
||||
| X | Release the E-STOP and clear the arm's error | button 3 | — |
|
||||
| Y | Toggle freedrive | button 4 | — |
|
||||
| Back | **E-STOP** — latches until X | button 11 | — |
|
||||
|
||||
**Laid out like the arm's own pendant.** Each ring is quartered into arc sectors you hold,
|
||||
with the out-of-plane axis as a pair of pills above it — the same shape as xArm Studio's
|
||||
jog control on the controller at `:18333`, and deliberately the same directions, because
|
||||
two consoles driving one arm that disagree about which way is `+Y` is a trap rather than a
|
||||
preference.
|
||||
The sticks move the arm in its horizontal X/Y plane only; the triggers are the one path
|
||||
to Z. Up on the stick sends the arm away from the operator, the same `+X` convention the
|
||||
console draws.
|
||||
|
||||
| Ring | Sectors | Pills above |
|
||||
|-----------|------------------------------------------------|----------------|
|
||||
| translate | `+X` north, `−X` south, `Y+` **west**, `Y−` east | `−Z` `+Z` |
|
||||
| rotate | `+pitch` north, `−pitch` south, `−roll` west, `+roll` east | `−yaw` `+yaw` |
|
||||
## Inputs and outputs
|
||||
|
||||
**`Y+` is on the left**, matching xArm Studio and the right-hand rule with `+X` away from
|
||||
the operator and `+Z` up. It is the one direction that does not read as "screen right is
|
||||
right", and it is that way on purpose.
|
||||
| Direction | Name | Description |
|
||||
|-----------|----------------|--------------------------------------------------------|
|
||||
| in | `tick` | 50 ms — change detection, deadman, telemetry |
|
||||
| in | `joy` | One pad reading from `dora-joy` |
|
||||
| in | `response` | An answer from the arm |
|
||||
| in | `motion_state` | How a motion ended |
|
||||
| in | `robot_state` | The arm's own state |
|
||||
| in | `tcp_pose` | Tool-tip pose, six float32, mm and degrees |
|
||||
| in | `joint_pose` | Joint angles, six float32, degrees |
|
||||
| out | `twist` | **Standard.** `{linear, angular, frame}`, SI |
|
||||
| out | `command` | **Standard.** `{action, value}`, robot-agnostic verbs |
|
||||
| out | `jog` | *Deprecated.* mm/s + deg/s, for `dora-ulite6` |
|
||||
| out | `request` | *Deprecated.* A service call, `request_id` in metadata |
|
||||
| out | `ui_state` | The console's telemetry snapshot, as JSON |
|
||||
|
||||
Directions are held, not analog: speed comes from the dial, and every held direction is a
|
||||
separate pointer, so two fingers work at once — driving `+X` while lifting on `+Z` is one
|
||||
gesture.
|
||||
## Environment variables
|
||||
|
||||
The node serves it on `WEB_PORT` (7000) and holds the WebSocket on `/ws` of that same
|
||||
port, so the console is one URL. That work happens on a thread of its own and meets the
|
||||
control loop at two points only — the messages the next tick drains, and the snapshot
|
||||
handed back — because a browser that stops reading must never be able to stall the thread
|
||||
that stops the arm.
|
||||
|
||||
**The browser's deadman is the socket itself.** The last one closing releases the lease
|
||||
and stops the arm, which is why the tab closing mid-jog does not leave it travelling.
|
||||
|
||||
Hold a direction to move, release to stop. The page re-sends its jog every 150 ms against
|
||||
the node's 400 ms deadline, so letting go stops the robot even if the release is lost. It
|
||||
sends a **fraction of full deflection**, never a speed, so it never learns the robot's
|
||||
units; the speed dial sets that fraction. Every control is dimmed and inert unless the
|
||||
console holds the lease, so one that would be refused looks refused.
|
||||
|
||||
If `WEB_PORT` is already taken the node **refuses to start** and says so, rather than
|
||||
carrying on with a console nobody can open.
|
||||
|
||||
`tests/test_console.py` checks the page against `ui.py` — every message type it sends is
|
||||
one the node serves, and every telemetry key it reads is one the node sends. Neither
|
||||
language catches a rename, and the wrong place to discover one is on a machine that moves.
|
||||
| Variable | Default | Description |
|
||||
|--------------------------|-----------|--------------------------------------------------------|
|
||||
| `SPEED_MM_S` | `60.0` | Translation speed at full deflection |
|
||||
| `SPEED_DEG_S` | `30.0` | Rotation speed at full deflection |
|
||||
| `DEADZONE` | `0.12` | Stick travel near centre to ignore |
|
||||
| `EXPO` | `0.4` | How much of the curve is cubic; 0 is linear |
|
||||
| `JOG_TTL_S` | `0.4` | Owner silence before the arm is stopped |
|
||||
| `JOG_KEEPALIVE_S` | `0.2` | How often an unchanged velocity is repeated |
|
||||
| `JOY_TTL_S` | `0.2` | `joy` silence before the pad reads as gone |
|
||||
| `GAMEPAD_IDLE_RELEASE_S` | `2.0` | Untouched pad before it hands the arm back |
|
||||
| `STATE_POLL_S` | `0.5` | How often to ask the arm what telemetry does not carry |
|
||||
| `WEB_HOST` | `0.0.0.0` | Address the console binds to |
|
||||
| `WEB_PORT` | `7000` | Console port; `0` opens no port at all |
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
uv sync --group dev
|
||||
uv run pytest # 112 tests, no gamepad, arm, browser or daemon needed
|
||||
uv run pytest # 149 tests, no gamepad, arm, browser or daemon needed
|
||||
uv run ruff check .
|
||||
uv run ruff format --check .
|
||||
```
|
||||
|
||||
Every file here is testable without hardware, because there is no hardware. The pad
|
||||
arrives on `joy` as a plain `Sample`.
|
||||
|
||||
+57
-26
@@ -1,23 +1,29 @@
|
||||
# The same bench, with a real gamepad and a probe.
|
||||
# The pad, a real arm and the probe. No bench and no browser.
|
||||
#
|
||||
# `dataflow.yml` is what CI validates, and it deliberately has no pad in it: the
|
||||
# pad is `dora-joy`, another repo, and pulling it in by `git:` would cost that
|
||||
# file its one property — that it resolves and runs from this checkout alone.
|
||||
# This file is the other half. It needs the network on first build, and a pad.
|
||||
# This file is the other half. It needs the network on first build, a pad, and
|
||||
# an arm.
|
||||
#
|
||||
# The bench answers services and publishes telemetry but never moves anything.
|
||||
# What this is for is watching the node decide: the lease changing hands, the
|
||||
# deadman firing when you let go, and the arm being stopped when the pad — or
|
||||
# the node reading it — goes away.
|
||||
# It is `dataflow-robot.yml` with the probe attached and the console shut, for
|
||||
# the one job the probe exists for: checking that the pad in your hands has the
|
||||
# layout `mapping.py` assumes, against the arm that will actually move. Use
|
||||
# `dataflow-robot.yml` for ordinary driving.
|
||||
#
|
||||
# Run it under a coordinator rather than `dora run`, so `dora topic` can read the
|
||||
# wire and so the probe has something to attach to:
|
||||
# source .venv/bin/activate # not --uv; README.md says why
|
||||
# dora build dataflow-gamepad.yml
|
||||
# dora run dataflow-gamepad.yml
|
||||
#
|
||||
# dora up
|
||||
# dora start dataflow-gamepad.yml --uv --name teleop-bench
|
||||
# dora-teleop-probe # in another terminal
|
||||
# dora topic echo -d teleop-bench teleop/jog
|
||||
# dora stop --name teleop-bench && dora down
|
||||
# `dora run`, not `dora up` + `dora start`: dora 1.0.1 refuses to start a
|
||||
# dataflow holding `git:` nodes from a coordinator — "node joy has git source,
|
||||
# but no `dora build` was run yet", straight after a build that succeeded. It is
|
||||
# the same shape of gap as the `--uv` one above, and it costs this file
|
||||
# `dora topic`, which needs a coordinator to read the wire.
|
||||
#
|
||||
# ROBOT_IP and TOOL_TYPE below name the arm in front of you: the bench arm at
|
||||
# 192.168.1.162, with a vacuum tool. Read
|
||||
# `The gamepad` in README.md before picking it up — Back is the E-STOP.
|
||||
|
||||
strict_types: true
|
||||
|
||||
@@ -47,10 +53,11 @@ nodes:
|
||||
source: joy/joy
|
||||
queue_size: 1
|
||||
queue_policy: drop_oldest
|
||||
response: bench/response
|
||||
robot_state: bench/robot_state
|
||||
tcp_pose: bench/tcp_pose
|
||||
joint_pose: bench/joint_pose
|
||||
response: ulite6/response
|
||||
motion_state: ulite6/motion_state
|
||||
robot_state: ulite6/robot_state
|
||||
tcp_pose: ulite6/tcp_pose
|
||||
joint_pose: ulite6/joint_pose
|
||||
outputs:
|
||||
- twist
|
||||
- command
|
||||
@@ -60,8 +67,13 @@ nodes:
|
||||
env:
|
||||
SPEED_MM_S: "60"
|
||||
SPEED_DEG_S: "30"
|
||||
# This one is about the pad, so the console stays shut. Set a port to
|
||||
# bring it up alongside and watch the lease move between the two.
|
||||
# Keep the deadman layers ordered: JOY_TTL_S < JOG_TTL_S < the arm's own
|
||||
# VELOCITY_COMMAND_TTL_S, which is 0.5 s below. Each backs up the one
|
||||
# inside it, and a backstop that fires first backs up nothing.
|
||||
JOY_TTL_S: "0.2"
|
||||
JOG_TTL_S: "0.4"
|
||||
# This one is about the pad, so the console stays shut and no port is
|
||||
# opened. `dataflow-robot.yml` is the one that serves it.
|
||||
WEB_PORT: "0"
|
||||
|
||||
# Declared, not spawned: `dora-teleop-probe` attaches to this id when you run
|
||||
@@ -71,15 +83,34 @@ nodes:
|
||||
inputs:
|
||||
joy: joy/joy
|
||||
|
||||
- id: bench
|
||||
path: examples/bench.py
|
||||
- id: ulite6
|
||||
git: https://gitea.liria.cl/liria/dora-ulite6.git
|
||||
tag: v0.3.1
|
||||
build: pip install .
|
||||
path: dora-ulite6
|
||||
inputs:
|
||||
tick: dora/timer/millis/200
|
||||
jog: teleop/jog
|
||||
tick: dora/timer/millis/100
|
||||
request: teleop/request
|
||||
ui_state: teleop/ui_state
|
||||
jog: teleop/jog
|
||||
outputs:
|
||||
- response
|
||||
- robot_state
|
||||
- tcp_pose
|
||||
- motion_state
|
||||
- joint_pose
|
||||
- tcp_pose
|
||||
- robot_state
|
||||
# dora type-checks before it fetches a `git:` source, so it cannot read the
|
||||
# arm's own manifest from here. These three repeat what `dora-ulite6`
|
||||
# v0.3.1 annotates; `response` and `motion_state` stay untyped there and so
|
||||
# stay untyped here. Re-check them when moving to a new tag.
|
||||
output_types:
|
||||
joint_pose: std/core/v1/Float32
|
||||
tcp_pose: std/core/v1/Float32
|
||||
robot_state: std/core/v1/String
|
||||
env:
|
||||
ROBOT_IP: "192.168.1.162"
|
||||
TOOL_TYPE: "vacuum"
|
||||
# The outermost deadman, enforced by the controller whether or not any of
|
||||
# this is alive. Every timer above must stay below it.
|
||||
VELOCITY_COMMAND_TTL_S: "0.5"
|
||||
# The pad does not home. Nothing moves until somebody pushes a stick.
|
||||
INIT_MODE: "none"
|
||||
|
||||
+3
-3
@@ -16,8 +16,8 @@
|
||||
# then looks for its entrypoint in this dataflow's venv instead. README.md has the
|
||||
# detail. `dataflow.yml` has no `git:` nodes and is fine with --uv.
|
||||
#
|
||||
# Set ROBOT_IP and TOOL_TYPE below to match the arm in front of you. The
|
||||
# defaults are `dora-ulite6`'s own: 192.168.1.192 and a vacuum tool.
|
||||
# ROBOT_IP and TOOL_TYPE below name the arm in front of you: the bench arm at
|
||||
# 192.168.1.162, with a vacuum tool.
|
||||
#
|
||||
# The web console comes with the node: it serves the page and holds the
|
||||
# browsers' sockets itself, so `http://<this machine>:7000` is a second way to
|
||||
@@ -102,7 +102,7 @@ nodes:
|
||||
tcp_pose: std/core/v1/Float32
|
||||
robot_state: std/core/v1/String
|
||||
env:
|
||||
ROBOT_IP: "192.168.1.192"
|
||||
ROBOT_IP: "192.168.1.162"
|
||||
TOOL_TYPE: "vacuum"
|
||||
# The outermost deadman, enforced by the controller whether or not any of
|
||||
# this is alive. Every timer above must stay below it.
|
||||
|
||||
+3
-2
@@ -5,8 +5,9 @@
|
||||
# manifest from `path:` and CI needs no gamepad, no robot and no sibling clones.
|
||||
#
|
||||
# There is **no gamepad here**, because the pad is another node and wiring it in
|
||||
# by `git:` would cost this file the one property it has. For a real pad on a
|
||||
# real bench, use `dataflow-gamepad.yml`.
|
||||
# by `git:` would cost this file the one property it has. For a real pad, use
|
||||
# `dataflow-robot.yml`, or `dataflow-gamepad.yml` to check the pad with the
|
||||
# probe — both of which drive a real arm.
|
||||
#
|
||||
# This is **not** the real stack. The bench answers services and publishes
|
||||
# telemetry but never moves anything. README.md carries the four-node wiring a
|
||||
|
||||
@@ -53,6 +53,12 @@ inputs:
|
||||
pad node is indistinguishable from an unplugged pad as far as the arm is
|
||||
concerned.
|
||||
|
||||
Each pad is measured the first time it is seen sitting still, so a stick
|
||||
that no longer centres has its tilt subtracted rather than deadzoned away.
|
||||
A pad whose resting axes do not fit the layout this node compiles in — a
|
||||
trigger where a stick should be, say — reads the same as no pad at all
|
||||
and is named in the log, rather than being guessed at against a real arm.
|
||||
|
||||
Leave it unwired for a browser-only console. The node says so and starts.
|
||||
response:
|
||||
required: false
|
||||
|
||||
+75
-3
@@ -12,11 +12,19 @@ unplugged pad. So silence past `JOY_TTL_S` reads as *absent*, which is the same
|
||||
answer, because a reading nobody is refreshing is not evidence of anything.
|
||||
`dora-joy` publishes on every tick whether or not the pad moved, so silence
|
||||
means one thing only.
|
||||
|
||||
The other thing this seam does is :class:`Calibration`: no two pads rest in
|
||||
quite the same place, and one that rests somewhere the layout does not expect
|
||||
is a pad this node must not drive an arm from. Every reading the loop sees has
|
||||
been through it, so the rest of the node only ever handles a pad that behaves.
|
||||
"""
|
||||
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from dora_teleop import mapping
|
||||
from dora_teleop.log import log
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Sample:
|
||||
@@ -52,17 +60,81 @@ def decode(event: dict) -> Sample:
|
||||
)
|
||||
|
||||
|
||||
class Calibration:
|
||||
"""Where the pad in front of us rests, and whether the layout fits it.
|
||||
|
||||
Captured from the first reading of a pad that is sitting still, and kept
|
||||
until that pad goes away. It is not re-taken while the pad is in use, which
|
||||
would be the one way to get this badly wrong: an operator holding a stick
|
||||
when it happened would have that deflection recorded as centre, and the arm
|
||||
would then move whenever they let go.
|
||||
|
||||
A pad whose resting axes do not fit the layout is reported **absent** rather
|
||||
than corrected. The numbering cannot be guessed from one reading — a trigger
|
||||
resting at -1.0 and a stick held hard over look identical — and guessing
|
||||
wrong drives a real arm. `dora-teleop-probe` shows what the pad reports, and
|
||||
`Layout` in :mod:`dora_teleop.mapping` is where a pad that disagrees is
|
||||
written down.
|
||||
"""
|
||||
|
||||
def __init__(self, layout: mapping.Layout = mapping.LAYOUT):
|
||||
self.layout = layout
|
||||
self.mismatch: str | None = None
|
||||
self._zeros: list[float] | None = None
|
||||
|
||||
@property
|
||||
def known(self) -> bool:
|
||||
"""Whether a pad has been measured at rest and may be driven from."""
|
||||
return self._zeros is not None
|
||||
|
||||
def forget(self) -> None:
|
||||
"""Drop what was learned, so the next pad to appear is measured afresh."""
|
||||
self._zeros = None
|
||||
self.mismatch = None
|
||||
|
||||
def apply(self, sample: Sample) -> Sample:
|
||||
"""Return the reading as the layout's ideal pad would have sent it.
|
||||
|
||||
An absent pad passes straight through and takes the calibration with
|
||||
it. A pad that does not fit the layout comes back absent, which every
|
||||
path in this node already treats as a reason to stop.
|
||||
"""
|
||||
if not sample.present:
|
||||
self.forget()
|
||||
return sample
|
||||
if self._zeros is None or len(self._zeros) != len(sample.axes):
|
||||
try:
|
||||
self._zeros = mapping.rest_zeros(sample.axes, self.layout)
|
||||
except ValueError as wrong:
|
||||
# Logged once per pad: this runs on every reading, and a pad
|
||||
# that does not fit the layout does not fit it fifty times a
|
||||
# second.
|
||||
if self.mismatch is None:
|
||||
log(
|
||||
f"ignoring the gamepad: {wrong}. Run dora-teleop-probe and check Layout in mapping.py"
|
||||
)
|
||||
self.mismatch = str(wrong)
|
||||
return ABSENT
|
||||
self.mismatch = None
|
||||
return Sample(
|
||||
axes=mapping.canonical(sample.axes, self._zeros, self.layout),
|
||||
buttons=sample.buttons,
|
||||
present=True,
|
||||
)
|
||||
|
||||
|
||||
class JoyInput:
|
||||
"""The latest reading, and how long ago it arrived."""
|
||||
|
||||
def __init__(self, ttl_s: float):
|
||||
def __init__(self, ttl_s: float, layout: mapping.Layout = mapping.LAYOUT):
|
||||
self.ttl_s = ttl_s
|
||||
self.calibration = Calibration(layout)
|
||||
self._sample = ABSENT
|
||||
self._seen_at = 0.0
|
||||
|
||||
def accept(self, sample: Sample, moment: float) -> None:
|
||||
"""Take one reading off the wire."""
|
||||
self._sample = sample
|
||||
"""Take one reading off the wire, calibrated to the pad that sent it."""
|
||||
self._sample = self.calibration.apply(sample)
|
||||
self._seen_at = moment
|
||||
|
||||
def read(self, moment: float) -> Sample:
|
||||
|
||||
+117
-16
@@ -34,26 +34,34 @@ MM_PER_M = 1000.0
|
||||
class Layout:
|
||||
"""Which axis and button index does what, in SDL's numbering.
|
||||
|
||||
The defaults are the layout SDL reports for an Xbox-style pad, which is also
|
||||
what it reports for most PlayStation-style ones. A pad that disagrees is a
|
||||
change here rather than a new environment variable: an index nobody can
|
||||
verify from the outside is worse than a line of code somebody can read.
|
||||
These are what an Xbox One S pad reports over Bluetooth, swept one control
|
||||
at a time: sticks on 0-3, both triggers last, and buttons with gaps at 2 and
|
||||
5. A pad that disagrees is a change here rather than a new environment
|
||||
variable: an index nobody can verify from the outside is worse than a line
|
||||
of code somebody can read. `dora-teleop-probe` is how you check.
|
||||
|
||||
**The axes can be checked at runtime and the buttons cannot.**
|
||||
:func:`rest_zeros` refuses a pad whose resting axes say this numbering is
|
||||
wrong, because an axis at rest has a value to compare. An unpressed button
|
||||
reads the same on every pad, so a wrong button index is silent — it was
|
||||
found here by `clear` toggling freedrive and `yaw_left` firing the E-STOP.
|
||||
The probe, and pressing each button once, is the only check there is.
|
||||
"""
|
||||
|
||||
left_x: int = 0
|
||||
left_y: int = 1
|
||||
left_trigger: int = 2
|
||||
right_x: int = 3
|
||||
right_y: int = 4
|
||||
right_x: int = 2
|
||||
right_y: int = 3
|
||||
left_trigger: int = 4
|
||||
right_trigger: int = 5
|
||||
|
||||
yaw_left: int = 4 # LB
|
||||
yaw_right: int = 5 # RB
|
||||
grasp: int = 0 # A
|
||||
release: int = 1 # B
|
||||
clear: int = 2 # X
|
||||
freedrive: int = 3 # Y
|
||||
estop: int = 6 # Back
|
||||
clear: int = 3 # X
|
||||
freedrive: int = 4 # Y
|
||||
yaw_left: int = 6 # LB
|
||||
yaw_right: int = 7 # RB
|
||||
estop: int = 11 # Back
|
||||
|
||||
|
||||
LAYOUT = Layout()
|
||||
@@ -69,6 +77,16 @@ TOOL_FRAME = "tool"
|
||||
# would mean a stick could claim the arm and then not move it.
|
||||
ACTIVITY = 0.3
|
||||
|
||||
# Where an untouched control sits once :func:`canonical` has had it: a stick at
|
||||
# centre, a trigger at the bottom of its travel.
|
||||
STICK_REST = 0.0
|
||||
TRIGGER_REST = -1.0
|
||||
|
||||
# How far an axis may rest from those and still be believed. Wide enough for a
|
||||
# worn stick's tilt and for a pad that reports a trigger it has never felt as 0,
|
||||
# and far short of the full deflection a wrongly numbered axis shows.
|
||||
REST_TOLERANCE = 0.25
|
||||
|
||||
|
||||
def deadzone(value: float, width: float) -> float:
|
||||
"""Ignore travel near centre, and rescale what is left to the full range.
|
||||
@@ -98,11 +116,13 @@ def expo(value: float, factor: float) -> float:
|
||||
def trigger(value: float) -> float:
|
||||
"""Return a trigger's travel as 0 at rest and 1 fully pressed.
|
||||
|
||||
SDL reports a trigger from -1 to 1, unlike a stick, and a trigger that has
|
||||
never been touched sometimes reads 0 instead of -1 until it first moves.
|
||||
Clamping keeps that from reading as half pressed.
|
||||
SDL reports a trigger from -1 to 1, unlike a stick, so `TRIGGER_REST` is
|
||||
where an untouched one sits. A trigger that has never moved sometimes reads
|
||||
0 instead, which this function alone cannot tell from half pressed —
|
||||
:func:`canonical` is what fixes that, by measuring the pad's own rest before
|
||||
anything reaches here.
|
||||
"""
|
||||
return max(0.0, min(1.0, (value + 1.0) / 2.0))
|
||||
return max(0.0, min(1.0, (value - TRIGGER_REST) / (1.0 - TRIGGER_REST)))
|
||||
|
||||
|
||||
def axis(values: list[float], index: int) -> float:
|
||||
@@ -115,6 +135,87 @@ def pressed(buttons: list[bool], index: int) -> bool:
|
||||
return bool(buttons[index]) if index < len(buttons) else False
|
||||
|
||||
|
||||
def axis_names(layout: Layout = LAYOUT) -> dict[int, str]:
|
||||
"""Name each axis index the layout uses."""
|
||||
return {
|
||||
layout.left_x: "left stick X",
|
||||
layout.left_y: "left stick Y",
|
||||
layout.left_trigger: "left trigger",
|
||||
layout.right_x: "right stick X",
|
||||
layout.right_y: "right stick Y",
|
||||
layout.right_trigger: "right trigger",
|
||||
}
|
||||
|
||||
|
||||
def rest_zeros(axes: list[float], layout: Layout = LAYOUT) -> list[float]:
|
||||
"""Return where each axis of an untouched pad sits, to measure deflection from.
|
||||
|
||||
An untouched pad is the one moment this node can check `layout` against the
|
||||
thing in front of it: a stick rests near centre and a trigger near the
|
||||
bottom of its travel, so an axis resting anywhere else means the numbering
|
||||
is wrong, or a control is being held, or the pad is broken. All three look
|
||||
the same from here and all three raise, because all three would otherwise
|
||||
drive the arm from a reading nobody made — a trigger read as a stick asks
|
||||
for full speed while sitting untouched on the desk.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
axes
|
||||
One reading, taken while nobody is touching the pad.
|
||||
layout
|
||||
Which index is which control.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list of float
|
||||
The resting value of each axis, in the order they were reported.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If any axis rests where the layout says it cannot, naming the index,
|
||||
what the layout calls it, and what it read.
|
||||
"""
|
||||
names = axis_names(layout)
|
||||
triggers = (layout.left_trigger, layout.right_trigger)
|
||||
for index, value in enumerate(axes):
|
||||
if index in triggers:
|
||||
# Either end is a resting trigger: -1 is what SDL reports, and 0 is
|
||||
# what some pads report until the trigger first moves.
|
||||
if abs(value - TRIGGER_REST) <= REST_TOLERANCE or abs(value - STICK_REST) <= REST_TOLERANCE:
|
||||
continue
|
||||
elif abs(value - STICK_REST) <= REST_TOLERANCE:
|
||||
continue
|
||||
name = names.get(index, "an axis the layout does not use")
|
||||
raise ValueError(f"axis {index} rests at {value:+.3f}, which is not where {name} sits")
|
||||
return list(axes)
|
||||
|
||||
|
||||
def canonical(axes: list[float], zeros: list[float], layout: Layout = LAYOUT) -> list[float]:
|
||||
"""Return this pad's axes as the layout's ideal pad would have reported them.
|
||||
|
||||
Sticks come back resting at `STICK_REST` and triggers at `TRIGGER_REST`,
|
||||
whatever this particular pad does when untouched, so everything downstream
|
||||
is written once for a pad that behaves. A stick that no longer centres, and
|
||||
a trigger that reads 0 until it first moves, both stop being special cases
|
||||
here rather than in every function that reads one.
|
||||
|
||||
Deflection is measured from `zeros` rather than from the ideal, which is the
|
||||
whole point: the tilt is subtracted, not deadzoned away, so the travel that
|
||||
is left still reaches full speed.
|
||||
"""
|
||||
triggers = (layout.left_trigger, layout.right_trigger)
|
||||
out = []
|
||||
for index, value in enumerate(axes):
|
||||
zero = zeros[index] if index < len(zeros) else STICK_REST
|
||||
if index in triggers:
|
||||
travel = (value - zero) / (1.0 - zero) if zero < 1.0 else 0.0
|
||||
out.append(TRIGGER_REST + (1.0 - TRIGGER_REST) * max(0.0, min(1.0, travel)))
|
||||
else:
|
||||
out.append(max(-1.0, min(1.0, value - zero)))
|
||||
return out
|
||||
|
||||
|
||||
def from_pad(axes: list[float], buttons: list[bool], config, layout: Layout = LAYOUT) -> list[float]:
|
||||
"""Map one gamepad reading to the normalised six-vector.
|
||||
|
||||
|
||||
+30
-18
@@ -35,7 +35,7 @@ from dora import Node
|
||||
|
||||
from dora_teleop import joy, mapping
|
||||
from dora_teleop.config import Config
|
||||
from dora_teleop.mapping import LAYOUT
|
||||
from dora_teleop.mapping import LAYOUT, axis_names
|
||||
|
||||
# What each velocity component is, in the order the arm takes them.
|
||||
COMPONENTS = (
|
||||
@@ -50,18 +50,6 @@ COMPONENTS = (
|
||||
CLEAR = "\033[H\033[J"
|
||||
|
||||
|
||||
def axis_names(layout=LAYOUT) -> dict[int, str]:
|
||||
"""Name each axis index the layout uses."""
|
||||
return {
|
||||
layout.left_x: "left stick X",
|
||||
layout.left_y: "left stick Y",
|
||||
layout.left_trigger: "left trigger",
|
||||
layout.right_x: "right stick X",
|
||||
layout.right_y: "right stick Y",
|
||||
layout.right_trigger: "right trigger",
|
||||
}
|
||||
|
||||
|
||||
def button_names(layout=LAYOUT) -> dict[int, str]:
|
||||
"""Name each button index the layout uses."""
|
||||
return {
|
||||
@@ -89,8 +77,15 @@ def bar(value: float, width: int = 21) -> str:
|
||||
return "".join(cells)
|
||||
|
||||
|
||||
def render(sample, velocity: list[float], config: Config) -> str:
|
||||
"""Build the whole display for one reading."""
|
||||
def render(sample, driven, velocity: list[float], config: Config, mismatch: str | None = None) -> str:
|
||||
"""Build the whole display for one reading.
|
||||
|
||||
``sample`` is the pad exactly as it was reported, because seeing the raw
|
||||
numbers is the point of this tool. ``driven`` is the same reading after
|
||||
calibration — what the node actually decides from — and ``velocity`` is what
|
||||
it would send. A pad this node refuses reaches here as an absent ``driven``
|
||||
and a ``mismatch`` saying why.
|
||||
"""
|
||||
lines = ["dora-teleop probe — Ctrl-C to stop", ""]
|
||||
|
||||
if not sample.present:
|
||||
@@ -102,6 +97,18 @@ def render(sample, velocity: list[float], config: Config) -> str:
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
if mismatch:
|
||||
lines += [
|
||||
" this pad does not match the layout, and the arm will not be driven from it.",
|
||||
"",
|
||||
f" {mismatch}",
|
||||
"",
|
||||
" Either something was being held when it was plugged in, or the indices",
|
||||
" below are numbered differently on this pad. The axes are raw, so they",
|
||||
" say which: fix `Layout` in dora_teleop/mapping.py to match them.",
|
||||
"",
|
||||
]
|
||||
|
||||
names = axis_names()
|
||||
lines.append(f" axes ({len(sample.axes)} reported)")
|
||||
for index, value in enumerate(sample.axes):
|
||||
@@ -121,7 +128,7 @@ def render(sample, velocity: list[float], config: Config) -> str:
|
||||
lines += [
|
||||
"",
|
||||
f" moving: {moving}",
|
||||
f" asking for the arm: {'yes' if mapping.is_active(sample) else 'no'}",
|
||||
f" asking for the arm: {'yes' if mapping.is_active(driven) else 'no'}",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
@@ -137,14 +144,19 @@ def main() -> None:
|
||||
args = parser.parse_args()
|
||||
|
||||
config = Config.from_env()
|
||||
calibration = joy.Calibration()
|
||||
node = Node(args.name)
|
||||
|
||||
for event in node:
|
||||
if event["type"] != "INPUT" or event["id"] != "joy":
|
||||
continue
|
||||
sample = joy.decode(event)
|
||||
velocity = mapping.velocity(sample.axes, sample.buttons, config) if sample.present else [0.0] * 6
|
||||
print(CLEAR + render(sample, velocity, config), flush=True)
|
||||
# Through the same calibration the node drives from, so what this prints
|
||||
# stays what the arm would get. What is displayed is still the raw
|
||||
# reading: the indices are what a pad is checked against.
|
||||
driven = calibration.apply(sample)
|
||||
velocity = mapping.velocity(driven.axes, driven.buttons, config) if driven.present else [0.0] * 6
|
||||
print(CLEAR + render(sample, driven, velocity, config, calibration.mismatch), flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+2
-2
@@ -6,8 +6,8 @@ readme = "README.md"
|
||||
license = { text = "MIT" }
|
||||
authors = [{ name = "Liria" }]
|
||||
|
||||
# dora-rs 1.0 requires 3.11; the node uses PEP 604 unions and zip(strict=).
|
||||
requires-python = ">=3.11"
|
||||
# dora-rs 1.0 requires 3.11; this node standardises on 3.12.
|
||||
requires-python = ">=3.12"
|
||||
|
||||
# No hardware dependency. The pad is `dora-joy` and the arm is `dora-ulite6`;
|
||||
# this node decides, and serves the console that asks it to.
|
||||
|
||||
+48
-2
@@ -7,6 +7,7 @@ import pytest
|
||||
|
||||
from dora_teleop.config import Config
|
||||
from dora_teleop.joy import Sample
|
||||
from dora_teleop.mapping import LAYOUT, TRIGGER_REST
|
||||
|
||||
# The shape `dora-joy` publishes. Declared here rather than inferred for the same
|
||||
# reason it is declared there: pyarrow reads an empty `axes` list as `list<null>`,
|
||||
@@ -66,13 +67,58 @@ def stick(axes=None, buttons=None, present=True):
|
||||
values = list(axes or [])
|
||||
values += [0.0] * (6 - len(values))
|
||||
# A trigger at rest reads -1, and a test that forgot to say so would look
|
||||
# like both triggers held down.
|
||||
for index in (2, 5):
|
||||
# like both triggers held down. Which indices those are comes from the
|
||||
# layout, so a test never has to be edited when a pad renumbers them.
|
||||
for index in (LAYOUT.left_trigger, LAYOUT.right_trigger):
|
||||
if axes is None or index >= len(axes):
|
||||
values[index] = -1.0
|
||||
return Sample(axes=values, buttons=list(buttons or []), present=present)
|
||||
|
||||
|
||||
def axes_for(
|
||||
left_x=0.0,
|
||||
left_y=0.0,
|
||||
right_x=0.0,
|
||||
right_y=0.0,
|
||||
left_trigger=TRIGGER_REST,
|
||||
right_trigger=TRIGGER_REST,
|
||||
):
|
||||
"""Lay out a pad's axes by what each control is rather than by its index.
|
||||
|
||||
A test that numbers its axes says nothing about which control it meant, and
|
||||
goes on passing while meaning something else once a pad renumbers them.
|
||||
"""
|
||||
values = [0.0] * 6
|
||||
values[LAYOUT.left_x] = left_x
|
||||
values[LAYOUT.left_y] = left_y
|
||||
values[LAYOUT.right_x] = right_x
|
||||
values[LAYOUT.right_y] = right_y
|
||||
values[LAYOUT.left_trigger] = left_trigger
|
||||
values[LAYOUT.right_trigger] = right_trigger
|
||||
return values
|
||||
|
||||
|
||||
def buttons_for(
|
||||
grasp=False, release=False, clear=False, freedrive=False, yaw_left=False, yaw_right=False, estop=False
|
||||
):
|
||||
"""Lay out a pad's buttons by what each one does rather than by its index.
|
||||
|
||||
The same reason as :func:`axes_for`, and more pressing: an unpressed button
|
||||
reads identically on every pad, so a test that numbers them is the only
|
||||
place a wrong index could ever have been caught.
|
||||
"""
|
||||
wanted = {
|
||||
LAYOUT.grasp: grasp,
|
||||
LAYOUT.release: release,
|
||||
LAYOUT.clear: clear,
|
||||
LAYOUT.freedrive: freedrive,
|
||||
LAYOUT.yaw_left: yaw_left,
|
||||
LAYOUT.yaw_right: yaw_right,
|
||||
LAYOUT.estop: estop,
|
||||
}
|
||||
return [wanted.get(index, False) for index in range(max(wanted) + 1)]
|
||||
|
||||
|
||||
def joy_message(sample):
|
||||
"""Build the Arrow row `dora-joy` would publish for one reading."""
|
||||
return pa.array(
|
||||
|
||||
+24
-17
@@ -6,7 +6,7 @@ from dora_teleop import main as loop
|
||||
from dora_teleop.arbiter import GAMEPAD, WEB
|
||||
from dora_teleop.config import Config
|
||||
from dora_teleop.joy import Sample
|
||||
from tests.conftest import RecordingNode, input_event, joy_message, stick
|
||||
from tests.conftest import RecordingNode, buttons_for, input_event, joy_message, stick
|
||||
|
||||
STOP = [0.0] * 6
|
||||
PUSHED = [0.0, -1.0]
|
||||
@@ -47,7 +47,14 @@ def ticks(node, state, count=1):
|
||||
|
||||
|
||||
def drive(node, state, sample, count=1):
|
||||
"""Hold one reading for `count` ticks, the way a real pad and joy node would."""
|
||||
"""Hold one reading for `count` ticks, the way a real pad and joy node would.
|
||||
|
||||
A pad this node has not yet seen at rest is sent one resting reading first,
|
||||
because a real one is always at rest before a thumb reaches it — and the
|
||||
node refuses to drive an arm from a pad it has never seen still.
|
||||
"""
|
||||
if not state.joy.calibration.known:
|
||||
send_joy(node, state, stick())
|
||||
for _ in range(count):
|
||||
send_joy(node, state, sample)
|
||||
loop._tick(node, state)
|
||||
@@ -271,7 +278,7 @@ def test_a_latched_estop_keeps_the_arm_still_under_a_pushed_stick(clock):
|
||||
drive(node, state, stick(axes=PUSHED))
|
||||
node.clear()
|
||||
|
||||
drive(node, state, stick(axes=PUSHED, buttons=[False] * 6 + [True]))
|
||||
drive(node, state, stick(axes=PUSHED, buttons=buttons_for(estop=True)))
|
||||
node.clear()
|
||||
jogs = drive(node, state, stick(axes=PUSHED), count=4)
|
||||
|
||||
@@ -282,11 +289,11 @@ def test_the_pad_can_release_the_latch_it_set(clock):
|
||||
# A pad-only dataflow has no console RESET, so a latch nothing on the pad
|
||||
# can release would strand the arm until the process was restarted.
|
||||
node, state = build()
|
||||
drive(node, state, stick(buttons=[False] * 6 + [True]))
|
||||
drive(node, state, stick(buttons=buttons_for(estop=True)))
|
||||
assert state.view.estop is True
|
||||
node.clear()
|
||||
|
||||
drive(node, state, stick(buttons=[False, False, True]))
|
||||
drive(node, state, stick(buttons=buttons_for(clear=True)))
|
||||
|
||||
assert state.view.estop is False
|
||||
assert "clear" in node.services()
|
||||
@@ -296,20 +303,20 @@ def test_the_latch_is_released_without_holding_the_arm(clock):
|
||||
# An idle pad hands the lease back two seconds after the stop it just made,
|
||||
# so gating the release on the lease would be a trap.
|
||||
node, state = build()
|
||||
drive(node, state, stick(buttons=[False] * 6 + [True]))
|
||||
drive(node, state, stick(buttons=buttons_for(estop=True)))
|
||||
state.arbiter.release(GAMEPAD)
|
||||
state.arbiter.take(WEB, clock.t)
|
||||
node.clear()
|
||||
|
||||
drive(node, state, stick(buttons=[False, False, True]))
|
||||
drive(node, state, stick(buttons=buttons_for(clear=True)))
|
||||
|
||||
assert state.view.estop is False
|
||||
|
||||
|
||||
def test_the_arm_drives_again_once_the_latch_is_released(clock):
|
||||
node, state = build()
|
||||
drive(node, state, stick(buttons=[False] * 6 + [True]))
|
||||
drive(node, state, stick(buttons=[False, False, True]))
|
||||
drive(node, state, stick(buttons=buttons_for(estop=True)))
|
||||
drive(node, state, stick(buttons=buttons_for(clear=True)))
|
||||
node.clear()
|
||||
|
||||
jogs = drive(node, state, stick(axes=PUSHED))
|
||||
@@ -322,7 +329,7 @@ def test_a_latched_estop_stops_the_browser_too(clock):
|
||||
# through a stop somebody made on the gamepad.
|
||||
node, state = build()
|
||||
state.clients = 1
|
||||
drive(node, state, stick(buttons=[False] * 6 + [True]))
|
||||
drive(node, state, stick(buttons=buttons_for(estop=True)))
|
||||
state.arbiter.release(GAMEPAD)
|
||||
state.arbiter.take(WEB, clock.t)
|
||||
node.clear()
|
||||
@@ -337,7 +344,7 @@ def test_the_estop_button_works_without_the_pad_holding_the_arm(clock):
|
||||
node, state = build()
|
||||
state.arbiter.take(WEB, clock.t)
|
||||
|
||||
drive(node, state, stick(buttons=[False] * 6 + [True]))
|
||||
drive(node, state, stick(buttons=buttons_for(estop=True)))
|
||||
|
||||
assert "stop" in node.services()
|
||||
|
||||
@@ -402,7 +409,7 @@ def test_a_button_asks_in_verbs_as_well_as_in_services(clock):
|
||||
# what dora-ulite6 reads until it does.
|
||||
node, state = build()
|
||||
|
||||
drive(node, state, stick(buttons=[True]))
|
||||
drive(node, state, stick(buttons=buttons_for(grasp=True)))
|
||||
|
||||
assert node.actions() == ["gripper_close"]
|
||||
assert "grasp" in node.services()
|
||||
@@ -410,10 +417,10 @@ def test_a_button_asks_in_verbs_as_well_as_in_services(clock):
|
||||
|
||||
def test_every_verb_the_pad_can_ask_for_is_robot_agnostic(clock):
|
||||
node, state = build()
|
||||
drive(node, state, stick(buttons=[False, True]))
|
||||
drive(node, state, stick(buttons=[False] * 3 + [True]))
|
||||
drive(node, state, stick(buttons=[False] * 6 + [True]))
|
||||
drive(node, state, stick(buttons=[False, False, True]))
|
||||
drive(node, state, stick(buttons=buttons_for(release=True)))
|
||||
drive(node, state, stick(buttons=buttons_for(freedrive=True)))
|
||||
drive(node, state, stick(buttons=buttons_for(estop=True)))
|
||||
drive(node, state, stick(buttons=buttons_for(clear=True)))
|
||||
|
||||
assert node.actions() == ["gripper_open", "freedrive", "stop", "clear"]
|
||||
|
||||
@@ -426,6 +433,6 @@ def test_the_first_press_after_the_pad_has_been_idle_is_not_dropped(clock):
|
||||
node, state = build()
|
||||
assert state.arbiter.owner is None
|
||||
|
||||
drive(node, state, stick(buttons=[True]), count=3)
|
||||
drive(node, state, stick(buttons=buttons_for(grasp=True)), count=3)
|
||||
|
||||
assert node.actions() == ["gripper_close"]
|
||||
|
||||
+93
-4
@@ -1,9 +1,12 @@
|
||||
"""The pad as it arrives on the wire, and what silence on that wire means."""
|
||||
|
||||
import pyarrow as pa
|
||||
import pytest
|
||||
|
||||
from dora_teleop import mapping
|
||||
from dora_teleop.joy import ABSENT, JoyInput, Sample, decode
|
||||
from tests.conftest import input_event, joy_message, stick
|
||||
from dora_teleop.mapping import LAYOUT
|
||||
from tests.conftest import axes_for, input_event, joy_message, stick
|
||||
|
||||
|
||||
def joy_event(sample):
|
||||
@@ -41,21 +44,107 @@ def test_a_message_with_no_rows_reads_as_absent():
|
||||
|
||||
def test_a_fresh_reading_is_handed_back():
|
||||
joy = JoyInput(ttl_s=0.2)
|
||||
sample = stick(axes=[0.5])
|
||||
joy.accept(sample, 100.0)
|
||||
joy.accept(stick(), 100.0)
|
||||
joy.accept(stick(axes=[0.5]), 100.0)
|
||||
|
||||
assert joy.read(100.1) is sample
|
||||
assert joy.read(100.1).axes[0] == 0.5
|
||||
|
||||
|
||||
def test_a_reading_nobody_is_refreshing_reads_as_absent():
|
||||
# dora-joy publishes every tick, so silence means it died — and a dead pad
|
||||
# node is indistinguishable from an unplugged pad as far as the arm cares.
|
||||
joy = JoyInput(ttl_s=0.2)
|
||||
joy.accept(stick(), 100.0)
|
||||
joy.accept(stick(axes=[0.5]), 100.0)
|
||||
|
||||
assert joy.read(100.5) is ABSENT
|
||||
|
||||
|
||||
# --- the pad this node is actually holding ---
|
||||
|
||||
|
||||
def test_a_pad_is_measured_where_it_rests_and_the_tilt_is_subtracted():
|
||||
# A stick that no longer centres is the ordinary kind of worn pad. Its rest
|
||||
# becomes zero, so it asks for nothing until it is actually pushed.
|
||||
joy = JoyInput(ttl_s=0.2)
|
||||
tilted = axes_for(left_x=0.2, left_y=-0.15)
|
||||
joy.accept(stick(axes=tilted), 100.0)
|
||||
|
||||
reading = joy.read(100.1)
|
||||
|
||||
assert reading.present is True
|
||||
assert reading.axes[LAYOUT.left_x] == pytest.approx(0.0)
|
||||
assert reading.axes[LAYOUT.left_y] == pytest.approx(0.0)
|
||||
assert mapping.is_active(reading) is False
|
||||
|
||||
|
||||
def test_the_tilt_is_subtracted_rather_than_deadzoned_so_full_travel_remains():
|
||||
joy = JoyInput(ttl_s=0.2)
|
||||
joy.accept(stick(axes=axes_for(left_x=0.2)), 100.0)
|
||||
joy.accept(stick(axes=axes_for(left_x=1.0)), 100.0)
|
||||
|
||||
assert joy.read(100.1).axes[LAYOUT.left_x] == pytest.approx(0.8)
|
||||
|
||||
|
||||
def test_a_trigger_that_rests_at_zero_does_not_read_as_half_pressed():
|
||||
# Some pads report a trigger as 0 until it first moves. Read as SDL's -1..1
|
||||
# that is half travel, which would lift the arm on its own.
|
||||
joy = JoyInput(ttl_s=0.2)
|
||||
joy.accept(stick(axes=axes_for(right_trigger=0.0)), 100.0)
|
||||
|
||||
reading = joy.read(100.1)
|
||||
|
||||
assert mapping.trigger(reading.axes[LAYOUT.right_trigger]) == pytest.approx(0.0)
|
||||
assert mapping.is_active(reading) is False
|
||||
|
||||
|
||||
def test_a_pad_that_does_not_fit_the_layout_reads_as_absent():
|
||||
# The bug this exists for: a trigger on an index the layout calls a stick
|
||||
# rests at -1.0, which is full deflection — an untouched pad on the desk
|
||||
# asking for full speed, and holding the lease so nothing can take it back.
|
||||
joy = JoyInput(ttl_s=0.2)
|
||||
wrong = list(axes_for())
|
||||
wrong[LAYOUT.right_y] = -1.0
|
||||
joy.accept(stick(axes=wrong), 100.0)
|
||||
|
||||
assert joy.read(100.1) is ABSENT
|
||||
assert "right stick Y" in joy.calibration.mismatch
|
||||
|
||||
|
||||
def test_a_pad_held_when_it_appears_is_refused_until_it_is_let_go():
|
||||
# A stick held at startup and a wrongly numbered axis look identical in one
|
||||
# reading, so the safe answer is the same for both: no arm until it rests.
|
||||
joy = JoyInput(ttl_s=0.2)
|
||||
joy.accept(stick(axes=axes_for(left_y=-1.0)), 100.0)
|
||||
|
||||
assert joy.read(100.1) is ABSENT
|
||||
|
||||
joy.accept(stick(), 100.2)
|
||||
|
||||
assert joy.read(100.3).present is True
|
||||
|
||||
|
||||
def test_rest_is_not_re_measured_while_the_pad_is_in_use():
|
||||
# Re-measuring under a held stick would record that deflection as centre,
|
||||
# and the arm would then move the moment the operator let go.
|
||||
joy = JoyInput(ttl_s=0.2)
|
||||
joy.accept(stick(), 100.0)
|
||||
joy.accept(stick(axes=axes_for(left_y=-1.0)), 100.1)
|
||||
joy.accept(stick(axes=axes_for(left_y=-1.0)), 100.2)
|
||||
|
||||
assert joy.read(100.3).axes[LAYOUT.left_y] == pytest.approx(-1.0)
|
||||
|
||||
|
||||
def test_a_pad_that_goes_away_is_measured_again_when_it_comes_back():
|
||||
# The second pad is not the first one, and may not rest where it did.
|
||||
joy = JoyInput(ttl_s=0.2)
|
||||
joy.accept(stick(axes=axes_for(left_x=0.2)), 100.0)
|
||||
joy.accept(Sample(present=False), 100.1)
|
||||
joy.accept(stick(axes=axes_for(left_x=-0.2)), 100.2)
|
||||
|
||||
assert joy.read(100.3).axes[LAYOUT.left_x] == pytest.approx(0.0)
|
||||
|
||||
|
||||
def test_nothing_ever_arriving_reads_as_absent():
|
||||
# A dataflow with no `joy` wired at all, which is a browser-only console.
|
||||
assert JoyInput(ttl_s=0.2).read(100.0) is ABSENT
|
||||
|
||||
+88
-12
@@ -3,8 +3,8 @@
|
||||
import pytest
|
||||
|
||||
from dora_teleop import mapping
|
||||
from dora_teleop.mapping import VX, VY, VZ, WX, WY, WZ
|
||||
from tests.conftest import stick
|
||||
from dora_teleop.mapping import LAYOUT, VX, VY, VZ, WX, WY, WZ
|
||||
from tests.conftest import axes_for, buttons_for, stick
|
||||
|
||||
|
||||
def test_a_stick_at_rest_asks_for_nothing(config):
|
||||
@@ -29,15 +29,15 @@ def test_pushing_the_stick_right_moves_the_arm_along_positive_y(config):
|
||||
|
||||
|
||||
def test_the_right_trigger_lifts_and_the_left_one_lowers(config):
|
||||
up = mapping.velocity(stick(axes=[0, 0, -1.0, 0, 0, 1.0]).axes, [], config)
|
||||
down = mapping.velocity(stick(axes=[0, 0, 1.0, 0, 0, -1.0]).axes, [], config)
|
||||
up = mapping.velocity(axes_for(right_trigger=1.0), [], config)
|
||||
down = mapping.velocity(axes_for(left_trigger=1.0), [], config)
|
||||
|
||||
assert up[VZ] == pytest.approx(config.speed_mm_s)
|
||||
assert down[VZ] == pytest.approx(-config.speed_mm_s)
|
||||
|
||||
|
||||
def test_both_triggers_together_cancel(config):
|
||||
velocity = mapping.velocity(stick(axes=[0, 0, 1.0, 0, 0, 1.0]).axes, [], config)
|
||||
velocity = mapping.velocity(axes_for(left_trigger=1.0, right_trigger=1.0), [], config)
|
||||
|
||||
assert velocity[VZ] == 0.0
|
||||
|
||||
@@ -51,15 +51,15 @@ def test_an_untouched_trigger_does_not_read_as_half_pressed(config):
|
||||
|
||||
|
||||
def test_the_right_stick_turns_the_tool_in_degrees_per_second(config):
|
||||
velocity = mapping.velocity(stick(axes=[0, 0, -1, 1.0, -1.0, -1]).axes, [], config)
|
||||
velocity = mapping.velocity(axes_for(right_x=1.0, right_y=-1.0), [], config)
|
||||
|
||||
assert velocity[WX] == pytest.approx(config.speed_deg_s)
|
||||
assert velocity[WY] == pytest.approx(config.speed_deg_s)
|
||||
|
||||
|
||||
def test_the_bumpers_yaw_in_opposite_directions(config):
|
||||
right = mapping.velocity(stick().axes, [False] * 5 + [True], config)
|
||||
left = mapping.velocity(stick().axes, [False] * 4 + [True, False], config)
|
||||
right = mapping.velocity(stick().axes, buttons_for(yaw_right=True), config)
|
||||
left = mapping.velocity(stick().axes, buttons_for(yaw_left=True), config)
|
||||
|
||||
assert right[WZ] == pytest.approx(config.speed_deg_s)
|
||||
assert left[WZ] == pytest.approx(-config.speed_deg_s)
|
||||
@@ -68,7 +68,7 @@ def test_the_bumpers_yaw_in_opposite_directions(config):
|
||||
def test_several_axes_move_at_once(config):
|
||||
# The whole reason a pad beats the console's four-sector dial: the dial can
|
||||
# only ever ask for one axis.
|
||||
velocity = mapping.velocity(stick(axes=[1.0, -1.0, -1, 0, 0, 1.0]).axes, [], config)
|
||||
velocity = mapping.velocity(axes_for(left_x=1.0, left_y=-1.0, right_trigger=1.0), [], config)
|
||||
|
||||
assert velocity[VX] > 0
|
||||
assert velocity[VY] > 0
|
||||
@@ -129,14 +129,90 @@ def test_a_nudged_stick_is_asking_for_the_arm():
|
||||
|
||||
|
||||
def test_a_held_trigger_is_asking_for_the_arm():
|
||||
assert mapping.is_active(stick(axes=[0.0, 0.0, 1.0])) is True
|
||||
assert mapping.is_active(stick(axes=axes_for(left_trigger=1.0))) is True
|
||||
|
||||
|
||||
def test_a_held_button_is_asking_for_the_arm():
|
||||
assert mapping.is_active(stick(buttons=[True])) is True
|
||||
assert mapping.is_active(stick(buttons=buttons_for(grasp=True))) is True
|
||||
|
||||
|
||||
def test_an_axis_the_layout_does_not_use_still_counts():
|
||||
# A pad reporting its d-pad as two extra axes is asking for the arm when
|
||||
# somebody presses it, whether or not this node maps it to anything.
|
||||
assert mapping.is_active(stick(axes=[0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 1.0])) is True
|
||||
assert mapping.is_active(stick(axes=axes_for() + [1.0])) is True
|
||||
|
||||
|
||||
# --- reading a pad that is not the layout's ideal one ---
|
||||
|
||||
|
||||
def test_a_resting_pad_measures_as_resting():
|
||||
zeros = mapping.rest_zeros(axes_for())
|
||||
|
||||
assert zeros == axes_for()
|
||||
|
||||
|
||||
def test_a_small_tilt_is_accepted_as_rest():
|
||||
# A worn stick is an ordinary pad, not a broken one.
|
||||
assert mapping.rest_zeros(axes_for(left_x=0.2, right_y=-0.2))
|
||||
|
||||
|
||||
def test_an_axis_resting_where_the_layout_says_it_cannot_is_refused():
|
||||
wrong = axes_for()
|
||||
wrong[LAYOUT.right_y] = -1.0
|
||||
|
||||
with pytest.raises(ValueError, match="right stick Y"):
|
||||
mapping.rest_zeros(wrong)
|
||||
|
||||
|
||||
def test_the_refusal_names_the_index_and_what_it_read():
|
||||
# It is read off a terminal by somebody deciding what to put in `Layout`.
|
||||
wrong = axes_for()
|
||||
wrong[LAYOUT.right_x] = 0.9
|
||||
|
||||
with pytest.raises(ValueError, match=r"axis 2 rests at \+0.900"):
|
||||
mapping.rest_zeros(wrong)
|
||||
|
||||
|
||||
def test_a_trigger_resting_at_either_end_is_accepted():
|
||||
# -1 is what SDL reports; 0 is what some pads report until it first moves.
|
||||
assert mapping.rest_zeros(axes_for(left_trigger=-1.0, right_trigger=0.0))
|
||||
|
||||
|
||||
def test_an_axis_the_layout_does_not_use_still_has_to_rest():
|
||||
# A d-pad reported as two extra axes counts towards `is_active`, so one
|
||||
# stuck at an extreme would hold the arm as surely as a named one.
|
||||
with pytest.raises(ValueError, match="axis 6"):
|
||||
mapping.rest_zeros(axes_for() + [1.0])
|
||||
|
||||
|
||||
def test_canonical_puts_a_tilted_stick_back_at_centre():
|
||||
tilted = axes_for(left_x=0.2)
|
||||
|
||||
canonical = mapping.canonical(tilted, mapping.rest_zeros(tilted))
|
||||
|
||||
assert canonical[LAYOUT.left_x] == pytest.approx(0.0)
|
||||
|
||||
|
||||
def test_canonical_puts_a_trigger_back_at_its_own_rest():
|
||||
zeroed = axes_for(left_trigger=0.0, right_trigger=0.0)
|
||||
|
||||
canonical = mapping.canonical(zeroed, mapping.rest_zeros(zeroed))
|
||||
|
||||
assert canonical[LAYOUT.left_trigger] == pytest.approx(mapping.TRIGGER_REST)
|
||||
assert canonical[LAYOUT.right_trigger] == pytest.approx(mapping.TRIGGER_REST)
|
||||
|
||||
|
||||
def test_a_trigger_measured_from_zero_still_reaches_full_travel():
|
||||
zeros = mapping.rest_zeros(axes_for(right_trigger=0.0))
|
||||
|
||||
canonical = mapping.canonical(axes_for(right_trigger=1.0), zeros)
|
||||
|
||||
assert mapping.trigger(canonical[LAYOUT.right_trigger]) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_a_calibrated_pad_at_rest_asks_for_nothing(config):
|
||||
tilted = axes_for(left_x=0.2, left_y=-0.15, right_trigger=0.0)
|
||||
|
||||
canonical = mapping.canonical(tilted, mapping.rest_zeros(tilted))
|
||||
|
||||
assert mapping.velocity(canonical, [], config) == [0.0] * 6
|
||||
|
||||
+39
-12
@@ -2,7 +2,7 @@
|
||||
|
||||
from dora_teleop import mapping, probe
|
||||
from dora_teleop.joy import Sample
|
||||
from tests.conftest import stick
|
||||
from tests.conftest import buttons_for, stick
|
||||
|
||||
|
||||
def test_a_centred_stick_puts_the_marker_in_the_middle():
|
||||
@@ -28,7 +28,7 @@ def test_a_reading_beyond_full_deflection_stays_on_the_bar():
|
||||
def test_no_gamepad_says_so_and_says_the_arm_is_being_held_still(config):
|
||||
# Absence covers both "no pad" and "dora-joy stopped sending", and the probe
|
||||
# must not imply the arm is waiting on something it is not.
|
||||
drawn = probe.render(Sample(present=False), [0.0] * 6, config)
|
||||
drawn = probe.render(Sample(present=False), Sample(present=False), [0.0] * 6, config)
|
||||
|
||||
assert "no gamepad" in drawn
|
||||
assert "hold still" in drawn
|
||||
@@ -39,30 +39,31 @@ def test_every_axis_the_pad_reports_is_shown_even_the_unused_ones(config):
|
||||
# for, so nothing may be hidden.
|
||||
sample = Sample(axes=[0.0] * 8, buttons=[], present=True)
|
||||
|
||||
drawn = probe.render(sample, [0.0] * 6, config)
|
||||
drawn = probe.render(sample, sample, [0.0] * 6, config)
|
||||
|
||||
assert "unused" in drawn
|
||||
assert "axes (8 reported)" in drawn
|
||||
|
||||
|
||||
def test_a_pressed_button_is_named(config):
|
||||
sample = stick(buttons=[True])
|
||||
sample = stick(buttons=buttons_for(grasp=True))
|
||||
|
||||
drawn = probe.render(sample, [0.0] * 6, config)
|
||||
drawn = probe.render(sample, sample, [0.0] * 6, config)
|
||||
|
||||
assert "grasp" in drawn
|
||||
|
||||
|
||||
def test_nothing_pressed_says_so_rather_than_showing_an_empty_line(config):
|
||||
drawn = probe.render(stick(), [0.0] * 6, config)
|
||||
drawn = probe.render(stick(), stick(), [0.0] * 6, config)
|
||||
|
||||
assert "none pressed" in drawn
|
||||
|
||||
|
||||
def test_the_velocity_is_shown_in_the_units_the_arm_takes(config):
|
||||
velocity = mapping.velocity(stick(axes=[0.0, -1.0]).axes, [], config)
|
||||
pushed = stick(axes=[0.0, -1.0])
|
||||
velocity = mapping.velocity(pushed.axes, [], config)
|
||||
|
||||
drawn = probe.render(stick(axes=[0.0, -1.0]), velocity, config)
|
||||
drawn = probe.render(pushed, pushed, velocity, config)
|
||||
|
||||
assert "mm/s" in drawn
|
||||
assert "deg/s" in drawn
|
||||
@@ -70,13 +71,14 @@ def test_the_velocity_is_shown_in_the_units_the_arm_takes(config):
|
||||
|
||||
|
||||
def test_a_still_arm_is_reported_as_still(config):
|
||||
drawn = probe.render(stick(), [0.0] * 6, config)
|
||||
drawn = probe.render(stick(), stick(), [0.0] * 6, config)
|
||||
|
||||
assert "told to hold still" in drawn
|
||||
|
||||
|
||||
def test_a_moving_arm_is_reported_as_moving(config):
|
||||
drawn = probe.render(stick(axes=[0.0, -1.0]), [10.0, 0, 0, 0, 0, 0], config)
|
||||
pushed = stick(axes=[0.0, -1.0])
|
||||
drawn = probe.render(pushed, pushed, [10.0, 0, 0, 0, 0, 0], config)
|
||||
|
||||
assert "moving: yes" in drawn
|
||||
|
||||
@@ -84,5 +86,30 @@ def test_a_moving_arm_is_reported_as_moving(config):
|
||||
def test_whether_the_pad_is_asking_for_the_arm_is_shown(config):
|
||||
# The probe is where a resting trigger claiming the arm would have been
|
||||
# visible, so it says what `is_active` thinks rather than leaving it implied.
|
||||
assert "asking for the arm: no" in probe.render(stick(), [0.0] * 6, config)
|
||||
assert "asking for the arm: yes" in probe.render(stick(axes=[0.0, -1.0]), [0.0] * 6, config)
|
||||
rest, pushed = stick(), stick(axes=[0.0, -1.0])
|
||||
|
||||
assert "asking for the arm: no" in probe.render(rest, rest, [0.0] * 6, config)
|
||||
assert "asking for the arm: yes" in probe.render(pushed, pushed, [0.0] * 6, config)
|
||||
|
||||
|
||||
def test_a_pad_that_does_not_fit_the_layout_is_called_out(config):
|
||||
# This is the tool for exactly that pad, so the reason goes on the screen
|
||||
# rather than only in the node's log.
|
||||
wrong = stick(axes=[0.0, 0.0, 0.0, -1.0, -1.0, -1.0])
|
||||
mismatch = "axis 3 rests at -1.000, which is not where right stick Y sits"
|
||||
|
||||
drawn = probe.render(wrong, Sample(present=False), [0.0] * 6, config, mismatch)
|
||||
|
||||
assert "does not match the layout" in drawn
|
||||
assert mismatch in drawn
|
||||
assert "asking for the arm: no" in drawn
|
||||
|
||||
|
||||
def test_the_raw_axes_are_still_shown_for_a_pad_that_does_not_fit(config):
|
||||
# The numbers are the evidence: they are what `Layout` gets corrected from.
|
||||
wrong = stick(axes=[0.0, 0.0, 0.0, -1.0, -1.0, -1.0])
|
||||
|
||||
drawn = probe.render(wrong, Sample(present=False), [0.0] * 6, config, "axis 3")
|
||||
|
||||
assert "-1.000" in drawn
|
||||
assert "axes (6 reported)" in drawn
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.11"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
"python_full_version < '3.12'",
|
||||
]
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[package]]
|
||||
name = "aiohappyeyeballs"
|
||||
@@ -31,24 +27,6 @@ dependencies = [
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/5c/b3e4ff8ad43a8afef9602c5e90285936da1beaea8b029016b793891f03c3/aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3", size = 764250, upload-time = "2026-07-23T01:52:48.525Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/da/f1b384465e51449d844056b75070461da03a9a23e6c1747003695bf4172a/aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a", size = 516281, upload-time = "2026-07-23T01:52:51.047Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/3f/01264f820ee2e3712a827892b1cd6ff80f3300c1fcbffbb45714a915d47a/aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8", size = 514742, upload-time = "2026-07-23T01:52:53.779Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/8d/a71c6f2db52ac1ed142b133f7feddaa6b70539c3f4de24d7e226c95b794c/aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239", size = 1780613, upload-time = "2026-07-23T01:52:56.948Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/11/3dd9b3fb3a170f6ec9011b5291d876a6fab4086714c9e158600edf01b4fd/aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f", size = 1737688, upload-time = "2026-07-23T01:52:59.294Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/3e/834c26918be7d88068822b40e0db30fca50b5f4fe79104aa16a93f1d74e6/aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06", size = 1845742, upload-time = "2026-07-23T01:53:01.641Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/c9/49ab8572df7d66bc13d11e31f781292badb04180dd87ba98733066c6aed7/aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929", size = 1928412, upload-time = "2026-07-23T01:53:04.018Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/b9/2b8f0c0ce09c87a1daf80fd483431b56b1435d3f62789bc86f572e1245de/aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db", size = 1786220, upload-time = "2026-07-23T01:53:06.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/00/9c45f81de11710460edfa1dc81317b6e882703b160926c879a9d20da9fcc/aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce", size = 1637231, upload-time = "2026-07-23T01:53:10.258Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/ce/967d628e910756f3539c6107cb7844a1b69440dcb3029a5ee7871b09ab63/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c", size = 1753161, upload-time = "2026-07-23T01:53:13.817Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/b2/0c3d4114f0aee4f580f5b3b4eb71b24d7a23b834ea506a4dfebe76513f35/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15", size = 1756356, upload-time = "2026-07-23T01:53:16.211Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/5d/99e7d91c82f1399d1ae2a854e080bd1493fbc31e5e959dbc4ec33dac3bec/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c", size = 1819846, upload-time = "2026-07-23T01:53:18.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/05/d5e1cb6480eeffd3f901d40a2c5e2d1e7effdc797837da3b490272699f13/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae", size = 1628531, upload-time = "2026-07-23T01:53:23.86Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/90/b934682bcaefae18a9e04f3dff5b68522ba810906358ae5029b68110ea3b/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910", size = 1832712, upload-time = "2026-07-23T01:53:27.551Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/df/6061679faaf81fac746e7307c7adb71e858071a5d34c27583afefc64f543/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7", size = 1775014, upload-time = "2026-07-23T01:53:30.223Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/1d/f854878bbc69b88faefe924b619a34a6f59ec05fd387c77690667eaa75eb/aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa", size = 456006, upload-time = "2026-07-23T01:53:34.97Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/0c/2af9d1674baccd1dbd47282a93d660a22e57ef6167c856deb24b4214fbab/aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d", size = 481069, upload-time = "2026-07-23T01:53:39.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/76/88401ff3fc95e85c5fc38d588f36f55e61ecb64343b2bc8d69326f453cc0/aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39", size = 453021, upload-time = "2026-07-23T01:53:43.749Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/d4/eb96299230e20acf2efae207cb8d69051f1f68e357e5ea5e479bf6fb097a/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5", size = 754690, upload-time = "2026-07-23T01:53:47.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/11/e7a70a209eb9a067c0d3212b518a0134e3484f5178c7533878b6b514d469/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228", size = 509484, upload-time = "2026-07-23T01:53:51.159Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/07/4bbc222cc8dbe31d4c3e8a5baad2286e4d42026ac0c570027b89afce6344/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee", size = 511949, upload-time = "2026-07-23T01:53:55.083Z" },
|
||||
@@ -169,8 +147,7 @@ name = "dora-rs"
|
||||
version = "1.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
|
||||
{ name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
||||
{ name = "numpy" },
|
||||
{ name = "pyarrow" },
|
||||
{ name = "pyyaml" },
|
||||
]
|
||||
@@ -223,22 +200,6 @@ version = "1.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" },
|
||||
@@ -346,26 +307,6 @@ version = "6.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/14/95/989c1b5ca17b72128661530cd6e351a0a83cda9a4d6c036e9ed976c18931/multidict-6.8.0.tar.gz", hash = "sha256:5cd4637ce76312ba1e05eb9c5193fec231f64fee0944e135fa1e951242355b37", size = 122412, upload-time = "2026-09-09T13:57:57.967Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/37/90216392620b6ef8704eb0bc055141745de396121067e98f1f72bdac33c3/multidict-6.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b8986d4313dcee7c932837d16a535f1840b827bac1ea7c5c4c80751d0423794", size = 85033, upload-time = "2026-09-09T13:53:18.786Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/bb/e01b8cf906479b2fa046e992b84a9d9f39c1ed4058acc353004cd9a04df9/multidict-6.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2622fe114c0bd66ca5c461859357587f5a5e35ee5ff49fc5643d1bc78dbb41c6", size = 51008, upload-time = "2026-09-09T13:53:20.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/da/35d70c920812d9ddc6f295f6426457665194335fbc35aa0b96716aba219f/multidict-6.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26a7aafc992e78872e2c8c1f7248c0e01139cf9020a7781b0c064fa566832712", size = 50232, upload-time = "2026-09-09T13:53:21.356Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/6b/4c988a7c0daa4fbffc6080ed3c37b3a67cf225ba1de69d10a19ca1dd8d0d/multidict-6.8.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:05c2e90c5289c5f7436ba2c25812a5fbdaa1c1bc11c8d8d3bbf64f5cd7c633dd", size = 271678, upload-time = "2026-09-09T13:53:22.716Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/2b/22c7de8a72fc5c36390e8049d86d842b032ac7c87ade035a3dafb7df4ffc/multidict-6.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7941ef106ca1f2c62314a13c7ed913bcf49641f3efdc12864d588e17870920ac", size = 270283, upload-time = "2026-09-09T13:53:24.235Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/f4/c1428318f945c57c016ba690338af41f87f18a7d3a7ef3227b1440a2c169/multidict-6.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a5a7ee1217949ddd43c6b7bcf70d5c22193bb50e8c695386de5905325e93ce9f", size = 245306, upload-time = "2026-09-09T13:53:25.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/92/a37f7519fb32b0bf43b0540292effe60edaf0691959214b227795bd3d56a/multidict-6.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b62b7e0025aa48dec11e125e655d1157985a5fdcec04b1ad500101ad072b891", size = 279567, upload-time = "2026-09-09T13:53:27.162Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/fe/a93c2ce417401863cc88ecf6561577625c140990d412e37f347a1c03a144/multidict-6.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea880d441be7c510106bc56064be39266d948aef94ad4955e8784690019a5d9f", size = 282526, upload-time = "2026-09-09T13:53:28.927Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/9d/6bb4f84fdd82acfa09dc312ac133e7f76cbf2370004447f2a90e65e5d63f/multidict-6.8.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ab83fdd8cf307353edba9c427c17a3a021c2522d690f5633dd9f72d28b48ccca", size = 272604, upload-time = "2026-09-09T13:53:30.595Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/97/df0a30a4d786d313f24b39cb96edaaa3bbfe83a0b309577c81a797783ec5/multidict-6.8.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3d1f48582686a0a3b81e9b43234766cc96697df72081af3f48107bd3f34d34e5", size = 250786, upload-time = "2026-09-09T13:53:32.15Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/72/e59a917680d00214ba41f9fec19a8bec48f3bdf62656bc4377f37ae30947/multidict-6.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:563661919f603374c40cf45ffcd25535c12b8954203569a2ab1cee5265871cf4", size = 265419, upload-time = "2026-09-09T13:53:33.943Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/21/0eb8868982ff07c1a2faaef7502ee0be32ef247dc1bf27881c51e7f4b20d/multidict-6.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2ba9933e8f35fe4a70f540b837254c4055da82dc3a9e500a8f95e61498083a15", size = 258934, upload-time = "2026-09-09T13:53:35.662Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/a6/0586396716faf950c10ffbe733e4a57b4eeda9f7073e60c09bd4a05a766e/multidict-6.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:68d40b2bace413f3231f5729d3fcfb1837fd31c4907e241b5d43211bfd76f3c2", size = 273168, upload-time = "2026-09-09T13:53:37.131Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/79/7197af20190d0d832be3b18582be46c224f64f5ba1cd35d32068d6af31ed/multidict-6.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a9e246f67ac038568b854ed7c5578e4c6af1f742359901a8fcc3603ff1358df6", size = 275884, upload-time = "2026-09-09T13:53:38.579Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/f7/af60573e25ffecc09e805464580d579338cf1a44332ab52757038435ed60/multidict-6.8.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:29b6e7bc4442a56cf8e0dc1cabf3fdc77cd533568d6829fc76a1effd2ce332ec", size = 246967, upload-time = "2026-09-09T13:53:40.172Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/02/4459f8c5025ab034d3d9af9a34bbde11319016cff2f327362c8ec43a80a1/multidict-6.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:82780eb8bf59e8fb25dd081fde6e058805045d6374a7f2f877effc826ca4434b", size = 272358, upload-time = "2026-09-09T13:53:41.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/99/680d3522ab51a77094d31d7958c9f5989499a01ffbe5aebe11d25212b385/multidict-6.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2196ba6df392c3574acadd14ef87550f3611349c8618564de324b806a7a31cee", size = 267450, upload-time = "2026-09-09T13:53:43.646Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/04/d0c773805b0aea171287b01a82e4c28c59ef2c2d93e8047394765181363f/multidict-6.8.0-cp311-cp311-win32.whl", hash = "sha256:b8b7aa75146266fd3e2a2437cf69ae188688c04ab8665b163d4257b46c1e0c83", size = 46847, upload-time = "2026-09-09T13:53:45.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/f8/1a959771a4dcd3224bd7bb40054f66b98ba5b20d6b74fd273f548f887e0a/multidict-6.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:b03ca066b47b18b205cc080dca6f76cbd159f8cdd33a02a0700164c13b37e463", size = 51549, upload-time = "2026-09-09T13:53:46.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/7c/3a74b11599a9d8f3cfbb78b9c5cac3ff3cdc17278e4c00329c7c18dcaff9/multidict-6.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:54af1266710cb0f305127ae0b970aff8d208057f8a29cd6e1db99b0114947035", size = 48020, upload-time = "2026-09-09T13:53:47.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/83/a4621577679149ea001806f5963f3fc687c391c1bd5217157be2278863f5/multidict-6.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:22a310ad37672a261e55a8b5e28d0ae08cfb68abb1f46418ccd19835c3b8e836", size = 84146, upload-time = "2026-09-09T13:53:49.163Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/00/236b063f3e606055a3a9ba8faa5d40e6c688b059a58056b055f213476f46/multidict-6.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bb8c7da8c861391f7ae48e3593762be2dabe405109e01aec520fbe1a6d15d14b", size = 51049, upload-time = "2026-09-09T13:53:50.46Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/9d/954b139bfa969855f2d4cb5ae7b7d44dd7106f754305b6e21a9068213aa7/multidict-6.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10456943903744ae1249728161c96bd9d2f7eb5ee17fcc2ffda2dc32e1bb36c7", size = 49362, upload-time = "2026-09-09T13:53:51.878Z" },
|
||||
@@ -498,95 +439,10 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/ee/be4e1a4b7a2b27f4fb6936510d4bebcb41b0562c946930ad26916e069cf9/multidict-6.8.0-py3-none-any.whl", hash = "sha256:75daa15ca16d6285eb2e104b2f05ee6f8d9836c68da3ce5c85f615a0450eed0e", size = 16297, upload-time = "2026-09-09T13:57:56.106Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.12'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.5.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/13/01/11703282db468b85f6f7b8c7f22d058de5970d5c7e60a3a8aaa313c3de36/numpy-2.5.3.tar.gz", hash = "sha256:df2d5874ff183595a4ba404edd04f6bd9b5505c1d7708573f6a6c17489a67563", size = 20791231, upload-time = "2026-09-06T16:27:47.073Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/50/8fdbb16af64895706a45f06a4068e29db732ec180f3c1375f14123359138/numpy-2.5.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb189f09db39283b26bfd061ec16189e14f71c6755207f72a0f7540867afe5b9", size = 16994982, upload-time = "2026-09-06T16:24:29.244Z" },
|
||||
@@ -680,23 +536,6 @@ version = "0.5.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b3/9a/9fbf4e4ec0c2d7f1c32519fff782ef467859b8faa9fbc5331a96f6395d43/propcache-0.5.4.tar.gz", hash = "sha256:ff6b113f50bc066a698db5d944d2c6dc7507168dd3341e255a8892fd0715a558", size = 61545, upload-time = "2026-09-16T00:17:14.386Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/40/14b21e505b7921617466576423f188a5c9caddfdaa1cf4b2b8a83d8fe216/propcache-0.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:897d1ddf6716e8f47200f7aad9a0efa6cc7586df66c6defa572f9eab379c078e", size = 86393, upload-time = "2026-09-16T00:14:06.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/4b/5a52e1a7b43563f7d408814194bb23cc8bf214eb6b86639b667a33a8d0d0/propcache-0.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9cbfff4423eef4cc6cafc021469641a2b835f610b2647a6c5281903e21b8670d", size = 50431, upload-time = "2026-09-16T00:14:08.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/cf/b5248180bf056cc76acc60c9c6e8c0ebbfdbd1c6cffd31fd14996927b7c8/propcache-0.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fc24f209c1b7f7f688b66b98293954f5504279760999b58920ee12dd8471c1d", size = 52116, upload-time = "2026-09-16T00:14:09.114Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/a8/7c6cd6bfead1a11f2e411e688640e6d26574cb0bde7dcaa7423b0b65ed7a/propcache-0.5.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62530ca89187827e4a4fe733f971abe81a7542eeea48ff61995f19b64d7199c8", size = 238729, upload-time = "2026-09-16T00:14:10.357Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/b4/442715b2e980df51be52d203549279e027728f24c80b00b5e525e31cd5ea/propcache-0.5.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fc3f7599528db40b1efa0889a620116e2704144495273d66066e8164e45838", size = 246121, upload-time = "2026-09-16T00:14:11.735Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/5d/df0684fc2b1732a01a7bec26d7897369022712422d25b09c37ce7dbc88a2/propcache-0.5.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f2d880ff60f45898f4acfa152aac8d04e3ee627d90ff4003491bf92239d5757", size = 251735, upload-time = "2026-09-16T00:14:13.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/06/519a5ebb48b6f94beb48396e55c905f12246a25c3a3608a7ec7bceabf50e/propcache-0.5.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e9368e87a3efc285e559131092c5db643eb8e56de4ee42064d5baec22ef2bb5", size = 235381, upload-time = "2026-09-16T00:14:14.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/07/1e0a9bb310830f2245edbd5cd3c6d24a783c053c4efd8e08e386e513c940/propcache-0.5.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:004e685b315646c410771836e72a44f143bbe624f29653a42687815069a303d5", size = 208973, upload-time = "2026-09-16T00:14:15.715Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/5c/9324fab27d6088eecc47fe4332bf7aaf8c1ded93c36f558391e8a06d41a7/propcache-0.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:594eb4c6ec35e7179b058481f4e9f02521b56de16fa577c4b85c76fb1bf8a9f8", size = 233897, upload-time = "2026-09-16T00:14:17.25Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/a3/570d92fc952eae93b676f3a1568f4b89264102abd3c982ab6a9ebec58dcf/propcache-0.5.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2dba2f02d2d5c09ef8a0e6c1a42aeaa451f4be9898cb00b04fe98717da2eb23b", size = 223512, upload-time = "2026-09-16T00:14:18.87Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/47/26810d889d89bba31db397e6a88f8984af775f5ed6bad0a29dce84324cff/propcache-0.5.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c3ef2818d63bc86071e9d2989ae75a1bc32b8f7059cfd9f5abbbee70c32e2ed6", size = 239043, upload-time = "2026-09-16T00:14:20.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/2d/f9c47691aa024c8299a3afacd78d22a01ab57eb627b481b6089708e71017/propcache-0.5.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:dd2ac8f5b643454c2cc6b6118b13da16e88f4a6434fc3ba61aca384029f04f36", size = 208218, upload-time = "2026-09-16T00:14:21.801Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/6b/d510c0c378cabbf9d0ac7b663af6d00f2e9074073d93b20c85f24aa5c071/propcache-0.5.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4054acf80d40456a0537f2913b349718649d8d6458a14ab7f48d0ce28c30869d", size = 240301, upload-time = "2026-09-16T00:14:23.121Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/80/c80f6adaaa1e51f0db2dce8c9b3714d94ec45e358a21f9a1910b10b40a80/propcache-0.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40e94adb1e7d39ff28a8bd8d8b8fbd1df6b9f40976dbe379134f1ce058e532dd", size = 230785, upload-time = "2026-09-16T00:14:24.458Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/e2/c32a7df3f39caa7f11b2eb37ea5b6960a6946f2bc7c4b8ff97bbdf6d6b6e/propcache-0.5.4-cp311-cp311-win32.whl", hash = "sha256:9f86f7259efe2c951f43e57d471c9b41daa5bfc7db9f67189059cf1ae6d77fd9", size = 42747, upload-time = "2026-09-16T00:14:25.715Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/8a/3db6a3543d8101263b4c52978b6276a04ead2caff2c5ab880d934f47bd89/propcache-0.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:e904d4d01f36bd6e197590be1533c44e06058771e0746dd073a8ebb3ef880858", size = 46268, upload-time = "2026-09-16T00:14:26.996Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/07/5222e2665bbf6e45847492ecbf3b9f3e4975a0ae300e5fd465df7d48ce55/propcache-0.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:d42a9a856a4a6e2f6c10f1318c07e7daa498d6593abe745c71dae4521a26ca39", size = 43547, upload-time = "2026-09-16T00:14:28.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/cd/348d58f142aebc4873345c6b31087629182ca6e0f2b3caeaa528cf882eba/propcache-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b28f41fa3b8c6900457f858ec5b03998f3a6d535fbc1bb2edec5961ea05ec429", size = 87285, upload-time = "2026-09-16T00:14:29.362Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/f4/f3ffaee281b276da854ac1d7a6a506d26cbc62ea2e623756f1d0a4a1ba1a/propcache-0.5.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dcbf346a318a5e30063f547630b02bb787ce2f45b6368d5da143660b6a3835d8", size = 50984, upload-time = "2026-09-16T00:14:30.473Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/88/1d7df7201750b37765ef2b23bc1c526c028dadde80afa0f57a118fc01182/propcache-0.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87a3caecf8095e48dc72f84bfa42e23a848cf410cc9cc13031fba4869b706a21", size = 52460, upload-time = "2026-09-16T00:14:31.692Z" },
|
||||
@@ -808,13 +647,6 @@ version = "25.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3d/e3/27f57f80141379d60defe6703eb50a707325706f07fedfd1312c7a751995/pyarrow-25.0.1.tar.gz", hash = "sha256:9150a83248bfed9813ea3c3af74c3856c1984d444aa28e58bf7733b9750ddf6a", size = 1201653, upload-time = "2026-08-10T12:40:53.904Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/8b/0d23b47702fcfe8b3618d5292035099675c5a1c48258932350c08020f7b5/pyarrow-25.0.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:51093dd9e10325fbdb3c10a2ae7c4806e5c822d94e74ae4938b26524a3323fee", size = 35946180, upload-time = "2026-08-10T12:37:18.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/17/707d17a5476c55a9541fde0db8213ac30979a792864d72415f176ba50c45/pyarrow-25.0.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:eb6203482ff3746a5632303a7279ae0b5a304c46985b49ed1378cb350ea6728d", size = 37644787, upload-time = "2026-08-10T12:37:25.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/b2/cdc98ecf1a6408280bc3a6a07054cdd99a3f4670acc0545d383ce113e87d/pyarrow-25.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:880523be3d29efcf83d3998835d206118ccf35e3871dbd2fb60408cf6b007a80", size = 46834633, upload-time = "2026-08-10T12:37:33.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/6e/d3fafc41f378b2c65be43b827798c0fae42049a641c8526633ed3eb573e2/pyarrow-25.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:25f8720bf6387d5dc2ebd2622112de630760419e4b66134405dd24110d15f37e", size = 50065507, upload-time = "2026-08-10T12:37:40.565Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/12/8d0698954b8c3001844a898e0a6900bebe83d7ee40c11195174c5122f324/pyarrow-25.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4facd65742a024a4a366328a1d2292062d72d6e023c1b7dda8d4c37544933a25", size = 49955690, upload-time = "2026-08-10T12:37:46.644Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/0b/1ecb936ac6409e90a34d58eea1c7cec09a9ae6d2141b9e49ad01a2b1ea47/pyarrow-25.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aa0559502e1cd6254d6814614085dd9c5a3dd0419362978a936a3f68a9e5c3df", size = 53128198, upload-time = "2026-08-10T12:37:52.531Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/1c/5236033550633c9b7377b2a53660b2bbb06cb06dc09c4356332d67643ca1/pyarrow-25.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:62cd0d785b8aa6675ee355f9fc02252a340f4441257c42674937826fd7594325", size = 27857263, upload-time = "2026-08-10T12:37:56.943Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/e2/9ab15b88cbfac28e16419ce5439ec29234c5172cb8259301b4ba639bdec0/pyarrow-25.0.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:df961f2e7ae9cf496459259d798652c70625f6c080650d6952f8c04053c58ee9", size = 35861559, upload-time = "2026-08-10T12:38:02.567Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/79/a0036dbe1eabe1f73127427342f1d99982584c4a2cde2651d6c93499c6f6/pyarrow-25.0.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:cc4aa407fde9fc660be3939e49ea31f50f3e9fec17c0ec63159f7711edd3efc9", size = 37628383, upload-time = "2026-08-10T12:38:09.083Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/49/d93a57d375f4bf0cf82913dd6bb54acafde83dd993be2282c81ac5616cad/pyarrow-25.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:4340f0ba6c1d2e13f21658de1d7c662ca2545018568d0030a1e9afca159d87e3", size = 46820190, upload-time = "2026-08-10T12:38:15.458Z" },
|
||||
@@ -876,15 +708,6 @@ version = "6.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
|
||||
@@ -970,23 +793,6 @@ dependencies = [
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/75/16/e8be8e2fb175bbf41a0680381a319f1199fae256588241a2ac8677eafb49/yarl-1.25.1.tar.gz", hash = "sha256:03dd38de09bc213e9a8b29761eec33ee1d5318dac0e49d8af36e4d27830e23a7", size = 246245, upload-time = "2026-09-15T19:35:02.264Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/83/b3/2cea721d495ca57f8f414aa4867ee263f486b274e07463158cf52f02ba7f/yarl-1.25.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9d693bf4bf534e9ba3ae2780cfd577f5135629f7b5ac653490859d0b77864865", size = 143794, upload-time = "2026-09-15T19:30:26.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/e6/cd145cff8e5cf60b8b3c41fbecfa2a45028a8dec3fbc52bec03595ff3d3b/yarl-1.25.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ab2054c5531af2a9ba7b69b8ec91e4f884420e83a8c5e579b013084cb57e5e5d", size = 103993, upload-time = "2026-09-15T19:30:28.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/7c/fbb40fe2d53747c40aa36a9e2bd2178a202f942bda0b670f3306d4aefbce/yarl-1.25.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:564fdc7085d2245ab84f88882fdb1d6ac0723124bff6ded35bfb1c00f812630d", size = 104010, upload-time = "2026-09-15T19:30:31.069Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/0b/5a516f70641092283f57cf3670bdb75e7327bcc0dcb5038697e4dfbfd569/yarl-1.25.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acae6b45d1ace09b6ba3876da43b88366ef368f73b988c7f57e14231753d4420", size = 116444, upload-time = "2026-09-15T19:30:32.894Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/8a/d1a627f827b0a404ae0f5647cab0534959081cde13b0756a783469fb3b5e/yarl-1.25.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1fb2a01ba8cd9c5d2c5dc1ec35e0fc951d04b4f037541d4ac090c993ce58b3d7", size = 107565, upload-time = "2026-09-15T19:30:35.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/cf/c8e0aaec886840a6c4480fe44eaec7cd4319f79a563b79321473be79c56f/yarl-1.25.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e92b6bcc741b86d67606c40d3cb9c7cc8e6c737f81e31f4a94efc204456c92e3", size = 125006, upload-time = "2026-09-15T19:30:37.1Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/26/0cce366d54a93cdc8342965dc7663385e4db161625cc1e8b18e786753d24/yarl-1.25.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:72c34ac7ad4314c19362d5ce27626dcc8429bd30bbf8c179f4234078851f9492", size = 128717, upload-time = "2026-09-15T19:30:38.904Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/0c/a71501bbc1a674ff72c4d6c2b75f4d9a5af819f5244c3a7558080a8802c5/yarl-1.25.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5add7b4ca7afeea91d52e4d4e4db3b1fe9885b71f07054560d8c4296b7441a2", size = 117728, upload-time = "2026-09-15T19:30:40.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/6f/c3267ca01defeed9ed9c4ff9b17bd54915432c405945233265b707475d1c/yarl-1.25.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:def538065f9e4d4cf1ae164bd59aba00dfa84f03923e0de4c3788f252d6bcd17", size = 116224, upload-time = "2026-09-15T19:30:42.818Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/69/fad57ee52d648431718ee0f1f68966a99c1352c3924688ffbdcc9d3fe51a/yarl-1.25.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a191bfdb30a79b98e5d175d75285f9fcb78bf0e46ba5efda042e1c72071a0de", size = 116299, upload-time = "2026-09-15T19:30:44.966Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/d4/6a8c1e29f33338687ca278cba0a8fbf6525a322c2c02a9a500ccbe041152/yarl-1.25.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:71f42c5b9a948c113bbdebfa544598321431d064ff959d32e99b1feb61d68345", size = 108625, upload-time = "2026-09-15T19:30:46.874Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/d2/3a35ae791c9cb6522c106923ff25c3230d999091e5e65511bca23bbd9914/yarl-1.25.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:72849d892954be4d09e569b8b831ac39ce58417fedc767d4308a0fe542018a40", size = 124515, upload-time = "2026-09-15T19:30:49.082Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/fd/2b022109a6b4af0f7dc371cf7500af380b0d4f034010243e1b0ce218dc93/yarl-1.25.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:efb01a106f971cb3752856bca2318bbdf7f01bd8823779c461586cbe5ffd5258", size = 115711, upload-time = "2026-09-15T19:30:51.208Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/59/f7586271136c3ddb0126bbfe661844699369b76b555fe38c4efe86870b2e/yarl-1.25.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a1daf47cd95a7c3a63456336bc5aaa8c86dd3a47d07ed3d0e76132ae4666a5a1", size = 122751, upload-time = "2026-09-15T19:30:53.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/0b/07f7a2d881f7e16c385b47fc1753382600847cad305dcc7fa0c25828acf8/yarl-1.25.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9489e6abf47ba37f332075a91444c7cfedb03e6ce99fbb2f116bfe1ce810da3b", size = 117983, upload-time = "2026-09-15T19:30:55.277Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/9d/8cdceec66a9b940700cb45931741403f045b162afd79bcb93c41cadd0972/yarl-1.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7306dee25b8a0e737363f347362b875094b4dc4e367311470656ae420fdbf8e", size = 102894, upload-time = "2026-09-15T19:30:57.606Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/f1/7ec357db1d3ad2863542d71e8fe64a126bbec17b134cd7d30951196809a5/yarl-1.25.1-cp311-cp311-win_arm64.whl", hash = "sha256:abb1384477f5901d436b5d2e5465954de46ea6098f59163d243660b5c4461d35", size = 98609, upload-time = "2026-09-15T19:30:59.705Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/b3/cd32ac66ae622b854c2df0ac52106dda220d361b65a64fde7d5b3684aa3f/yarl-1.25.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:94d7aa6debf92a1dd14cb5280b083a764169a13cfb23a452111160274ed989f4", size = 144798, upload-time = "2026-09-15T19:31:01.821Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/fb/a2c52a8007c2051ba74662afb112ecf3d00346af4c25e33df9d80fd14fb8/yarl-1.25.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:83d4a37e4b95da4d8bda930d6d35b75b4cdadbacbb4980cae290ea3100b5d51d", size = 104583, upload-time = "2026-09-15T19:31:04.05Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/dd/ee38aec8e09fdf957e50d4085453fbe202f56c6c3b4cf07b81cdb4f09ee9/yarl-1.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e029648f9c951db30e98a7d7ec90835db88ec4b32820efe2a9bdc2287e032eb6", size = 104325, upload-time = "2026-09-15T19:31:06.338Z" },
|
||||
|
||||
Reference in New Issue
Block a user