Compare commits

..

No commits in common. "wkt/land_binance_usdm_feed" and "main" have entirely different histories.

94 changed files with 898 additions and 6315 deletions

View File

@ -1,233 +0,0 @@
# Bounded IB recovery candidate
Prepared 2026-10-22 on `wkt/land_ib_recovery`, in the main checkout
`/home/goodboy/repos/piker`. Base:
`dc618229bd8c9c90aa3160e26677b553a7d472c7`.
Changes are unstaged and uncommitted. No acceptance is implied.
## Patch selection and provenance
Keep these existing landing commits:
| Commit | Reason |
| --- | --- |
| `4133f073` and its ancestors | Packaging, Nix, runtime API and pin work |
| `e8e0730a` | Correlate MethodProxy requests and responses |
| `7bef50b0` | One response reader, per-request waiters |
| `d1571ddf` | Propagate daemon-handler exceptions |
| `dc618229` | Isolate cached feed stream consumers |
Proposed separate implementation boundaries, in this order:
1. Back out the code and original test from
`635314c757c74a1cd95c5cf3dc7dfd03be984b00`.
This restores the direct-owner `_quote_streams` implementation.
Retain the historical prompt records. Replace the removed
mocked-cache regression with the real-bridge restart reproducer.
This backout is not a claim that arbitrary concurrent direct
callers have safe broadcast ownership.
2. Backport **only** `piker/brokers/ib/feed.py` from
`0846cbd4a4197e47415689cf5e9d146ba3174c32`.
Retain its seven history-adapter regressions in
`tests/test_ib_history.py`. This includes reset serialization,
completion/result signalling and bounded cancellation/failure.
It does not include the original SHM/storage orchestration patch.
3. Backport **only** `piker/brokers/ib/feed.py` from
`1da9b760a3bc6b96501110cab2b5d09846d42a1d`.
Retain `test_empty_history_preserves_request_boundary` and its
six cases. This preserves no-data request boundaries and wakes
the waiter. It does not include the original history-layer
endpoint/filtering changes. Add the real-bridge history recovery
case after these adapter changes are present.
4. Import the code and original regression from
`12b0d66e854247e92cb5a8668be71d816c72d856`.
Add a separate real-broadcaster/production-loop regression.
5. Remove the single-null-sample debugger stop in `get_null_segs()`.
This is a local UX cleanup retaining the existing `None` result.
It does not backport the null-grouping/deduplication rewrite in
`976db6c33c4b8ed07eee3047226417d3928d9cca`.
6. Record cross-boundary regression results and live qualification
evidence, distinguishing the known callback failure from passes.
All three imported source commits were authored by
`goodboy <goodboy_foss@protonmail.com>`; preserve that authorship
and the full source SHA when committing each partial backport.
Use explicit partial-backport wording, not a whole-commit cherry
claim. New real-runtime tests are this session's additional work.
The current aggregate working diff contains these boundaries in
the same feed file. Staging that entire file would combine the
first three boundaries. Reconstruct/stage the original per-source
patches in the above order when preparing commits.
## Quote-cache history correction
`4af885d90f6e4bcfa963f2f7a1d40436d3c928f3` and landing commit
`635314c757c74a1cd95c5cf3dc7dfd03be984b00` have the same stable
patch ID: `bc54892c25e957be112b135bb9e383ec4dd123c5`.
They are equivalent replays, not different implementations.
`wkt/ib_downstream_pin_eval@e929fdc2` lacks that quote-cache
change and retains direct ownership. The original qualification
did not validate the combination assembled for the landing.
After this candidate's edits, `piker/brokers/ib/feed.py` matches
the source at `1da9b760` byte-for-byte. Compared with the evaluated
tip, only the later schedule/reservation APIs are absent.
The EMS code patch has the same stable patch ID as the original
`12b0d66e` EMS code patch:
`50fee047e94fd67c8d9feab66859fbe3ab25b64c`.
## Real-runtime evidence
`tests/test_ib_quote_restart.py` uses real TCP Tractor actors,
an asyncio-infected child, the installed pinned Tractor bridge,
production Piker quote setup, and real IB/eventkit ticker events.
Only the external IB connection/input is replaced with replay data.
Before the quote-cache backout, cancellation after ticker delivery
raised `RemoteActorError[TrioTaskExited]` through
`maybe_open_context()` / `_Cache.run_ctx` teardown, matching the
reported fatal path.
After the backout:
- Two quote generations finish; each subscription is cancelled,
each asyncio producer is done and each ticker handler removed.
- A second RPC repeats the sequence in the same actor successfully.
- The strict callback-cleanliness case still fails: Tractor's
`signal_trio_when_done()` raises `TrioTaskExited` into asyncio's
exception handler even though the Trio consumer recovers.
The history integration case keeps `Client.bars()`, MethodProxy,
both relays, `get_bars()`, `wait_on_data_reset()` and
`data_reset_hack()` real. It replaces external contract discovery,
IB history replies and VNC input. A real IB error event announces
farm recovery. The replay rejects retries before recovery and
proves one reset followed by completion for both 1s and 60s data.
The new EMS case runs the production dark-clearing loop over a
real Tractor broadcaster: both EMS and a sibling receive a tick,
EMS cancellation removes only its subscription, and the sibling
continues with an unchanged strict lag policy.
## Verification
Unique selected cases: **38 passed, 1 failed**.
The failure is intentionally visible, not skipped or xfailed.
Run from the checkout's working Nix environment:
```xsh
timeout -k 5 45 py313/bin/python -m pytest -p no:xonsh -q -x --tb=short tests/test_ib_history.py tests/test_ems_broadcast.py tests/test_ib_method_proxy.py tests/test_feed_broadcast.py
timeout -k 5 45 py313/bin/python -m pytest -p no:xonsh -q --tb=short tests/test_ib_quote_restart.py
timeout -k 5 45 py313/bin/python -m pytest -p no:xonsh -q -x --tb=short tests/test_logging.py tests/test_startup.py
timeout -k 5 45 py313/bin/python -m pytest -p no:xonsh -q -x --tb=short tests/test_services.py::test_runtime_boot
git diff --check
```
The adapter/proxy/feed/EMS selection initially passed 21 cases;
the added real EMS case then passed with its imported case.
The bridge file passed two cases and failed callback cleanliness.
Logging/startup passed 13 cases; runtime boot passed one with the
existing `Registrar.uid` deprecation warning.
The agent shell inherited another project's environment and lacked
native libraries. Tests used the existing Piker `py313` via
`/tmp/nix-shell.zfKbeB/opencode/piker_test_env.py`, which supplies
libraries from the pinned flake's existing Qt dependency closure,
excluding Nix Qt itself and glibc. No environment sync or shellHook
ran. Piker resolved to this checkout; Tractor resolved to its
installed package in `py313`.
Workflow-state preparation initially reported changed legacy
files/modes after migration. On 2026-10-23 the human authorized
reconciliation. Inspection found only two missing legacy guidance
snapshots; the neutral copies matched their saved hashes and modes.
Restoring those local snapshots resolved preparation without
changing archived messages or the original migration receipt.
## Human live qualification, 2026-10-23
The human ran these journeys with the installed Tractor pin,
the existing IB Gateway and pre-existing NativeDB. The agent did
not operate the broker connection, VNC, orders or database.
- Initial QQQ startup/backfill recovered after pacing error 162
with one reset attempt, then displayed live updates. The reset
waiter appears to have timed out; the log did not establish an
acknowledged farm reset or live quote-stream restart.
- Sequential chart exits and reattachment preserved all four
daemon PIDs; a read-only process/listener check confirmed this.
- The overlapping two-chart journey used `pikerd@961108`,
`brokerd.ib@964765`, `samplerd@965900` and `emsd@969015`.
B started at 08:16:31; C at 08:17:33. B closed at 08:18:11,
replacement B started at 08:18:24 and had FSPs up by 08:18:28,
then C closed at 08:18:37. The human reported the exercise worked;
survivor updates are a manual observation, while timestamps and
unchanged daemon identities corroborate overlap and service reuse.
- Startup in that journey hit the explicit single-null-sample
breakpoint several times before the charts overlapped. The small
debugger cleanup above was applied afterward; this live run does
not itself verify that later edit.
- Departing charts still report cancellation groups and an FSP
receiver-close `ClosedResourceError`, followed by the Qt close
handler's self-SIGINT. Peer-disconnected final result warnings
remain contained in the persistent services.
- Daemon SIGINT at 08:19:43 reproduced cached shared-client teardown:
`load_clients_for_trio()` / `_Cache.run_ctx` promoted
`TrioTaskExited` through `open_trade_dialog()` and entered PDB.
After the human continued, all three children were reported
exited with status 0 and the root terminated at 08:19:45.
Cancellation-only RPC error reporting and late cancel-ack
diagnostics also remain.
The human explicitly accepted deferring this daemon teardown fix
from #94 and requested loud attribution for the bounded backports.
This is an operational smoke result, not clean-shutdown coverage.
## Downstream reconciliation requirements
Before landing #94, the human wants an easy replay of the downstream
histories intended to follow it. Rehearse against the agreed #94
tree in disposable worktrees, then inspect range-diffs, final code
and relevant regressions. This note does not authorize rewriting
the original branches or changing acceptance markers.
- `0846cbd4` and `1da9b760` are partial backports. Their imported
adapter hunks may already be present, but their history/SHM/storage
changes and omitted integration tests remain necessary downstream.
Never drop either whole source commit merely because `feed.py`
matches or its adapter hunk becomes empty.
- `12b0d66e` has source-equivalent EMS code and its imported test,
plus an additional local real-broadcaster test. Preserve that extra
regression when resolving test-file overlap.
- The quote-cache backout intentionally restores direct ownership
for this landing. Reintroducing original `4af885d9` downstream
requires qualifying cached-owner teardown with the matching
Tractor revision; the backout is not a permanent ownership design.
- The local single-null breakpoint removal retains legacy behavior.
Prefer the full downstream `976db6c3` algorithm when it is replayed;
preserving this `return None` guard would lose that later fix.
- Real-bridge tests and manual qualification notes are local work,
not evidence that omitted portions of the source commits landed.
Commit messages must name full source SHAs, original authorship,
imported paths/tests, omissions, local additions and replay guidance.
Use explicit partial-backport language for adapter subsets; do not
claim a full cherry-pick or reuse the source's broader fix claims.
## Remaining decision
This candidate removes the reproduced fatal quote-restart
propagation and imports existing reset fixes. The human exercised
live QQQ startup, backfill and concurrent chart consumer lifetimes.
Completion-callback and teardown errors remain; the human accepts
them as follow-ups outside the bounded #94 landing.
The storage/SHM portions of the downstream fixes are not covered by
the adapter backport or its integration tests.
`ddac3553` is Deribit-specific and omitted. Qt lifecycle commit
`1a2d4893` stays a separate follow-up. Further Tractor teardown
changes or storage imports require reassessing the bounded landing
rather than silently expanding this candidate.

View File

@ -1,52 +0,0 @@
---
created_at: 2026-10-07T16:54:53Z
generated_by:
harness: opencode
provider: openai
model: gpt-6.1-sol
git_ref: bde2457b
scope: code
substantive: true
raw_file: 20261007T165453Z_bde2457b_prompt_io.raw.md
---
## Prompt
The user requested fixing all seven local PR94 review findings and
exporting the review for human inspection in Tuicr. They asked that a
Tractor pin adjustment be considered before adding Piker workarounds.
## Response summary
Correct the endpoint defaults, Click argument shape, daemon binding and
registrar option handling, chart fallback, and stray debugger pause.
Keep the absent datad implementation's test outside this landing
branch. Add offline startup coverage and export original findings
without modifying review states. The checked newer Tractor address
branch still rejects mixed transports, so leave the dependency pin.
## Files changed
- `config/conf.toml`: TCP-only default endpoints.
- `piker/cli/__init__.py`: repeatable and intact multiaddresses.
- `piker/service/_actor_runtime.py`: validate daemon listeners.
- `piker/service/_daemon.py`: initialize and consume registrar options.
- `piker/ui/cli.py`: fallback to automatic chart binding.
- `piker/brokers/binance/feed.py`: remove unconditional debugger entry.
- `tests/test_services.py`: defer test requiring missing datad API.
- `tests/test_startup.py`: CLI/config and service lifecycle regressions.
## Human edits
The human identified that datad tests arrived through the agent's
mixed cherry-picks and explicitly requested deferring that test. They
requested a chart default for existing configs, expanded the task to
all review findings, and directed checking whether a newer Tractor pin
could reduce the fix scope. No direct manual source edits were observed
during these changes; acceptance remains with the human.
During the human review pass, the human confirmed removing the Binance
pause and correcting CLI endpoint parsing. They requested an explicit
template note about this pin's single-transport limitation and detailed
comments distinguishing registrar discovery endpoints from daemon bind
endpoints. Those explanations were added at the human's direction.

View File

@ -1,61 +0,0 @@
---
created_at: 2026-10-07T16:54:53Z
generated_by:
harness: opencode
provider: openai
model: gpt-6.1-sol
git_ref: bde2457b
diff_cmd: git diff HEAD~1..HEAD
---
## Prompt
Fix all findings from the local PR94 review, prefer a Tractor pin
change if it reduces work, and export the original review for Tuicr.
## Generated changes
> `git diff HEAD~1..HEAD -- config/conf.toml`
Use TCP-only defaults, retaining commented UDS alternatives. Both
checked Tractor address branch tips still reject mixed transports.
> `git diff HEAD~1..HEAD -- piker/cli/__init__.py`
Make Click multiaddresses repeatable and normalize single strings.
> `git diff HEAD~1..HEAD -- piker/service/_actor_runtime.py`
Validate requested daemon bind addresses instead of remote registrars.
Normalize accepted endpoints and allow TCP ephemeral port requests.
> `git diff HEAD~1..HEAD -- piker/service/_daemon.py`
Resolve explicit or inherited registrars independently of other daemon
options and consume the explicit keyword before forwarding kwargs.
> `git diff HEAD~1..HEAD -- piker/ui/cli.py`
Default chart bind endpoints to None when network config is absent.
> `git diff HEAD~1..HEAD -- piker/brokers/binance/feed.py`
Remove the unconditional market-info debugger pause.
> `git diff HEAD~1..HEAD -- tests/test_services.py tests/test_startup.py`
Remove the prematurely cherry-picked datad test and add offline startup
regressions, including a real registrar/child daemon lifecycle check.
## Verification
- Startup and logging tests: 13 passed.
- Existing runtime boot and tagged registrar tests: 2 passed.
- Scoped native Ruff and diff checks pass.
- The first new actor test used a removed nursery API; it was corrected
to the installed start_actor/Portal.run interface before passing.
- Import initially failed without native libraries. Tests use the
existing flake's GCC, zlib, Qt and PyQt paths without executing its
environment-rebuilding shell hook.
- Original seven findings exported locally as Tuicr review-add payloads
bound to bde2457b; no session comments or remote PR were changed.

View File

@ -1,45 +0,0 @@
---
created_at: 2026-10-10T23:28:52Z
generated_by:
harness: opencode
provider: openai
model: openai/gpt-6.1-sol
git_ref: 1fa45dfa
scope: config
substantive: true
raw_file: 20261010T232852Z_1fa45dfa_prompt_io.raw.md
---
## Prompt
The human rejected keeping the April Tractor pin as a landing baseline.
They requested the reviewed `wkt/addr_unpacking` branch, removal of the
new rollback adaptations, reuse of original Piker histories where
possible, and a candidate prepared within their gateway relocation
window. They asked specifically whether the runtime validation review
hunk duplicated older endpoint history.
## Response summary
Select the reviewed address branch at an immutable locked revision and
preserve all committed application/test compatibility code. Verify the
minimal dependency boundary offline and preview existing consumer
fixes without importing unrelated datad/backfill changes. Distinguish
original-source test results from live teardown acceptance, which is
not available in this preparation window.
## Files changed
- `pyproject.toml`: select Tractor `wkt/addr_unpacking`.
- `uv.lock`: resolve reviewed `a966928d` and setproctitle 1.3.8 while
preserving Polars 1.36.0 and the rest of the locked graph.
## Human edits
The human redirected the task from an older known-good runtime to a
modern landing stack and insisted on reusing original history rather
than building new compatibility adapters. They proposed stricter
exception-group behavior as a hypothesis and set the live-test/time
constraints. Those directions materially changed the dependency
boundary. No direct manual edits to this final boundary were observed;
runtime acceptance and landing remain human-owned.

View File

@ -1,52 +0,0 @@
---
created_at: 2026-10-10T23:28:52Z
generated_by:
harness: opencode
provider: openai
model: openai/gpt-6.1-sol
git_ref: 1fa45dfa
diff_cmd: git diff HEAD~1..HEAD
---
## Prompt
Undo the April Tractor rollback and select the reviewed
`wkt/addr_unpacking` branch. Prefer original downstream compatibility
commits to new adaptations, and prepare an offline-verified candidate
while live IB verification is deferred for gateway relocation.
## Generated changes
> `git diff HEAD~1..HEAD -- pyproject.toml uv.lock`
Change Tractor's source from `piker_pin` to `wkt/addr_unpacking` and
lock it at `a966928dead4cbf369ad244834ce9dbe988ac8ca`. Restore application
and test sources byte-for-byte to the committed PR94 tree. Keep the
rejected rollback journal only in ignored local experiment reports.
Preserve Polars 1.36.0 by retaining its exact existing lock blocks;
setproctitle re-resolves from 1.3.7 to 1.3.8. `uv sync --locked` accepts
the resulting graph. Actual imports confirm the reviewed Tractor pin
and Trio 0.32.0, not the hypothesized Trio 0.33.
## Original-history previews
The existing MethodProxy stack is retained. Read-only merge previews
show original `4af885d9` and `b691422b` can be applied without the datad
cutover. Original-source regressions run through temporary in-memory
overlays against the real pinned runtime: seven cases pass and one
cache-hit case fails because `log.at_least_level('info')` looks up INFO
in a custom-level mapping. A separate standard logging correction
produces eight passing cases. Its patch and execution handoff are saved
under ignored `.ai/code-review/reports/pr94_modern_stack/`; no consumer
cherry-picks or source edits are performed by this boundary.
## Verification
- Startup and logging: 13 passed.
- Runtime boot and tagged registrar normalization: 2 passed.
- Locked sync and source whitespace checks pass.
- Original application/test trees are unchanged from PR94 HEAD.
- Consumer overlay with explicit logging correction: 8 passed.
- Correction patch validates against its exact preview tree using a
private temporary index; the real index is untouched.
- Live IB startup, SIGINT and surviving-pikerd reattachment are pending.

View File

@ -1,59 +0,0 @@
---
model: claude-fable-5[1m]
service: claude
session: 32d15f9a-b2d3-4c26-bdc9-190219141a25
timestamp: 2026-06-10T17:08:59Z
git_ref: datad_service
diff_cmd: git log -1 -p --follow -- ai/prompt-io/claude/20260610T170859Z_75cefe10_prompt_io.md
scope: code
substantive: true
raw_file: 20260610T170859Z_75cefe10_prompt_io.raw.md
---
## Prompt
Session-initiating instruction (driving all 7 commits in
this series):
> ok i want you to become the distributed runtime and
> concurrency expert for this project - namely acquire
> a deep understanding of tractor and how it's used.
> then i want you to attempt to factor our current
> brokerd service daemon into 2 daemons: a brokerd
> which only hosts live/paper trading endpoint tasks
> [..] a new `datad` subdaemon which in a separate
> subactor serves all the data feed service tasks [..]
> give me a mega detailed plan on how to approach this,
> and a staged approach for the implementation.
Proximate driver for THIS commit: during the approved
plan's stage-0 test gate the agent discovered test
subactors were writing into the user's REAL
`~/.config/piker/accounting/` files (a bogus paper fill
landed in `account.kraken.paper.toml`) because the old
test-dir override in `config.get_app_dir()` was
commented out and could never work in spawned
subactors anyway. Fix applied autonomously under the
approved plan; no explicit per-fix user prompt.
## Response summary
Restore `pytest` config-dir isolation by resolving the
per-test tmp dir lazily (at conf-path access time) from
`tractor` runtime-vars, which propagate down the actor
tree; route all conf-path derivation through
`config.get_conf_dir()` so the override is effective in
every (sub)actor.
## Files changed
- `piker/config.py` — add `_maybe_use_test_dir()`;
hook in `get_conf_dir()`; route `get_conf_path()` +
`load()` mkdir through it
- `piker/accounting/_ledger.py` — derive ledger dir
via `config.get_conf_dir()` (not the global)
- `piker/accounting/_pos.py` — same for account files
## Human edits
None — committed as generated.

View File

@ -1,46 +0,0 @@
---
model: claude-fable-5[1m]
service: claude
timestamp: 2026-06-10T17:08:59Z
git_ref: datad_service
diff_cmd: git log -1 -p --follow -- ai/prompt-io/claude/20260610T170859Z_75cefe10_prompt_io.md
---
NOTE: diff-ref mode entry (code committed in the same
commit as this log); backfilled from the live dev
session transcript per the `/prompt-io` skill rules.
> `git log -1 -p --follow -- piker/config.py`
Generated: `config._maybe_use_test_dir()` — lazily
reads `piker_test_dir` from
`tractor.runtime._state._runtime_vars['piker_vars']`
(pre-loaded by `open_piker_runtime()` from the
`tests.conftest._open_test_pikerd()` overrides) and
calls `_override_config_dir()` when set. Hooked at the
top of `get_conf_dir()`; `get_conf_path()` and
`load()`'s dir-creation rerouted through
`get_conf_dir()`.
> `git log -1 -p --follow -- piker/accounting/_ledger.py`
> `git log -1 -p --follow -- piker/accounting/_pos.py`
Generated: ledger/account dir derivation switched from
the `config._config_dir` module global to
`config.get_conf_dir()` + `mkdir(parents=True,
exist_ok=True)` for nested tmp-dir creation.
Key diagnostic reasoning (verbatim from session):
The old (commented-out) `get_app_dir()` override gated
on `'pytest' in sys.modules` which can NEVER work in
spawned subactors (fresh procs, no pytest import); as
a result test `paperboi`/daemon actors were writing
into the user's REAL `~/.config/piker/accounting/`
files. Evidence: `account.kraken.paper.toml` gained a
single 0.001 xbtusdt clear stamped 2026-06-09T18:47Z
(a test fill), and `trades_kraken_paper.toml` (252B)
contained only that fill. The resolution must be
checked lazily at config-path access time (NOT import
time) since sub-actors only receive runtime-vars once
their `tractor` runtime has fully booted.

View File

@ -1,53 +0,0 @@
---
model: claude-fable-5[1m]
service: claude
session: 32d15f9a-b2d3-4c26-bdc9-190219141a25
timestamp: 2026-06-10T17:10:22Z
git_ref: datad_service
diff_cmd: git log -1 -p --follow -- ai/prompt-io/claude/20260610T171022Z_4485f2b9_prompt_io.md
scope: code
substantive: true
raw_file: 20260610T171022Z_4485f2b9_prompt_io.raw.md
---
## Prompt
Same session-initiating `brokerd`-split instruction (see
`20260610T170859Z_75cefe10_prompt_io.md`). Proximate
driver: the approved plan's per-stage test gates could
not run AT ALL — the branch base was broken vs.
`tractor` git `main` (`AttributeError: 'tuple' object
has no attribute 'unwrap'` at `pikerd` boot, stale
`arbiter_sockaddr`/`.type`/`.size` API refs). The agent
fixed forward autonomously to (re)establish the gate
baseline, continuing the `repair_tests` branch lineage
(verified already merged in ancestry via `git cherry`
during a user-requested branch-overlap survey).
## Response summary
Port the service layer + test suites to current
`tractor` APIs: addr-type normalization in
`open_pikerd()`, `query_actor()`/`wait_for_actor()`
kwarg renames, modern self-cancel absorption semantics
in the cancel-method test harness, exc/position attr
renames, a paper-EMS startup-budget bump and a syntax
fix in `.deribit.api`.
## Files changed
- `piker/service/_actor_runtime.py` — `wrap_address()`
normalize before `.unwrap()` in `open_pikerd()`
- `piker/service/_registry.py` — `check_for_service()`
-> `query_actor(regaddr=)` + 2-tuple yield +
`open_registry(addrs=)`
- `piker/brokers/deribit/api.py` — missing comma in
`tractor.trionics` import tuple
- `tests/test_services.py` — `registry_addr=` kwarg,
raddr unwraps, cancel-semantics harness rewrite,
`fail_after` 9 -> 19s
- `tests/test_ems.py` — `.boxed_type`, `pp.cumsize`
## Human edits
None — committed as generated.

View File

@ -1,56 +0,0 @@
---
model: claude-fable-5[1m]
service: claude
timestamp: 2026-06-10T17:10:22Z
git_ref: datad_service
diff_cmd: git log -1 -p --follow -- ai/prompt-io/claude/20260610T171022Z_4485f2b9_prompt_io.md
---
NOTE: diff-ref mode entry (code committed in the same
commit as this log); backfilled from the live dev
session transcript per the `/prompt-io` skill rules.
> `git log -1 -p --follow -- piker/service/_actor_runtime.py`
Generated: normalize each registry addr via
`tractor.discovery._addr.wrap_address()` before
`.unwrap()`-ing for the `accept_addrs` bind check —
entries may be raw `tuple`s when passed in from (test)
client code. Import-path precedent taken from
`piker/cli/__init__.py:336`.
> `git log -1 -p --follow -- piker/service/_registry.py`
Generated: `check_for_service()` ported to
`tractor.query_actor(name, regaddr=...)` (kwarg was
`arbiter_sockaddr=`), unpacking the new
`(sockaddr, portal)` yield, and passing the
now-required `open_registry(addrs=Registry.addrs)`.
> `git log -1 -p --follow -- tests/test_services.py`
> `git log -1 -p --follow -- tests/test_ems.py`
> `git log -1 -p --follow -- piker/brokers/deribit/api.py`
Key diagnostic reasoning (verbatim from session):
- the "DID NOT RAISE ContextCancelled" failure: in this
test the client actor IS pikerd (in-proc), and
current `tractor` main absorbs a `ContextCancelled`
whose canceller is your own actor — self-requested
cancels now exit cleanly instead of raising; the
'sigint' variant propagates a bare collapsed
`KeyboardInterrupt` rather than a
`BaseExceptionGroup`.
- the hard-coded `trio.fail_after(9)` startup budget is
marginal — full stack boot (pikerd -> emsd ->
brokerd.kraken -> paperboi + live kraken symbology
fetch) occasionally exceeds 9s -> bumped to 19s.
- `RemoteActorError.type` -> `.boxed_type`;
`Position.size` -> `.cumsize` (the paper engine
populates `BrokerdPosition.size` from `pp.cumsize`).
- overlap survey (user-requested): all of the
`repair_tests` branch commits are already in this
stack's ancestry; this commit finishes that branch's
port mission (its f4c4f1e2 fixed `conftest.py`'s
`arbiter_sockaddr` usage; this fixes the remaining
`test_services.py` + `check_for_service()` sites).

View File

@ -1,34 +0,0 @@
# AI Prompt I/O Log — claude
This directory tracks prompt inputs and model
outputs for AI-assisted development using
`claude` (claude-code CLI).
## Policy
Prompt logging follows the
[NLNet generative AI policy][nlnet-ai].
All substantive AI contributions are logged
with:
- Model name and version
- Timestamps
- The prompts that produced the output
- Unedited model output (`.raw.md` files)
[nlnet-ai]: https://nlnet.nl/foundation/policies/generativeAI/
## Usage
Entries are created by the `/prompt-io` skill
or automatically via `/commit-msg` integration.
Each commit carrying AI-generated changes links
to its provenance entry via a `Prompt-IO:`
commit-msg trailer; entries use "diff-ref mode"
(pointers into `git log -p` instead of verbatim
code copies) to avoid duplicating committed
code.
Human contributors remain accountable for all
code decisions. AI-generated content is never
presented as human-authored work.

View File

@ -1,36 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: ses_0799212ebffe42arY96czXn89F
timestamp: 2026-08-11T20:58:26Z
git_ref: 06d5ea5d
scope: code
substantive: true
raw_file: 20260811T205826Z_06d5ea5d_prompt_io.raw.md
---
## Prompt
Diagnose and fix an IB live-order failure where lazy NVDA contract
qualification raised `tractor.trionics.Lagged` after the order reached
`brokerd.ib`. Determine whether recent history changes or cancelled symbol
searches caused the stale state.
## Response summary
Traced the failure to competing consumers of a one-slot Trio broadcast, not
the recent SHM change or stale asyncio client state. Replaced broadcast
fan-out with one response/status demultiplexer, retained request-ID
correlation, handled cancellation and teardown, documented the task/channel
ownership model, warned on diagnostic metadata eviction, and added
deterministic proxy regressions.
## Files changed
- `piker/brokers/ib/api.py` - sole-reader IB proxy response routing.
- `tests/test_ib_method_proxy.py` - status, concurrency, cancellation and EOF regressions.
- `.claude/skills/run-tests/test-harness-reference.md` - proxy test mapping.
## Human edits
None - generated output follows the user's live failure diagnosis.

View File

@ -1,66 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-11T20:58:26Z
git_ref: 06d5ea5d
diff_cmd: git diff HEAD~1..HEAD
---
The user reported that certain IB order entries might have broken and
provided a live failure for a first NVDA sell order. EMS delivered the
request to `brokerd.ib`, but lazy `get_mkt_info('nvda.nasdaq')` contract
qualification failed in `MethodProxy._run_method()` with:
```text
tractor.trionics._broadcast.Lagged:
Task `piker.brokers.ib.broker.handle_order_requests` overrun and
dropped `0` values
```
The user suspected a cancelled symbol-search task might have left stale
asyncio state. Cancellation can leave an orphaned method response, but the
observed failure was stale Trio broadcast state. `open_client_proxy()`
broadcast a one-slot `LinkedTaskChannel` to an always-running IB status relay
and an idle method receiver. Status traffic advanced only the relay, so the
first method call for an uncached symbol entered an already-lagged receiver.
Orders for eagerly cached contracts bypassed this path.
The immediately preceding sparse-history commit did not modify IB order,
symbol, or proxy code; it exposed no direct dependency on this failure.
> `git diff HEAD~1..HEAD -- piker/brokers/ib/api.py`
Generated a sole-reader message demultiplexer for each IB client proxy.
`relay_client_proxy_messages()` owns every asyncio-to-Trio receive, handles
status/error traffic, and dispatches `mid`-tagged responses into per-call
`MethodProxy` wait slots. Cancelled calls remove their pending slot, causing
late responses to be logged and dropped without skewing later calls. Clean
channel EOF is accepted during normal proxy teardown.
Follow-up review restored the masked reconnect TODO and per-message case
rationale, documented the one-reader/many-writer task hierarchy and separate
per-proxy channels, and added a warning with request method metadata when the
bounded unresolved-request diagnostic table evicts its oldest entry.
> `git diff HEAD~1..HEAD -- tests/test_ib_method_proxy.py`
Generated deterministic regressions for idle status traffic before first
qualification, two concurrent calls with reverse-order responses, cancelled
calls with late responses followed by a successful call, and graceful
channel EOF. Every synchronization wait is timeout-bounded.
> `git diff HEAD~1..HEAD -- .claude/skills/run-tests/test-harness-reference.md`
Registered the deterministic IB method-proxy target and source mapping.
Verification output:
```text
........................ [100%]
25 passed, 4 warnings in 3.44s
```
The warnings are existing Tractor/Trio deprecations from the EMS test.
Adversarial review drove a clean-EOF guard and bounded polling. Final review
found no actionable issues. Live IB order validation remains pending and
requires restarting `brokerd.ib`.

View File

@ -1,34 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: unavailable
timestamp: 2026-08-13T20:24:02Z
git_ref: 12b0d66e
scope: code
substantive: true
raw_file: 20260813T202402Z_12b0d66e_prompt_io.raw.md
---
## Prompt
Continue the paired piker broadcast-consumer work after Tractor PR 490
stabilization. Stop cached feed callers from mutating or consuming shared
quote-stream roots, then review, verify and prepare a commit boundary.
## Response summary
Made provider root streams private to the cached `Feed`, gave every caller a
local subscribed view, corrected feed cache identity, and made multi-provider
relays own synchronized child subscriptions and independent send-channel
clones.
## Files changed
- `piker/data/feed.py` - caller-local feed views, complete cache keys, and
subscribed multi-provider relays with independent channel lifetimes.
- `tests/test_feed_broadcast.py` - real broadcast ownership, lifecycle and
cache-identity regressions, including sibling survival after provider EOC.
## Human edits
None - generated output follows the requested cached-feed ownership item.

View File

@ -1,51 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-13T20:24:02Z
git_ref: 12b0d66e
diff_cmd: git diff HEAD~1..HEAD
---
The user asked to continue the paired piker broadcast-consumer work after
Tractor PR 490 stabilization. The next item was to stop cached feed callers
from mutating or consuming shared `Feed`/`Flume` quote-stream roots, then
review, verify and prepare a commit boundary.
> `git diff HEAD~1..HEAD -- piker/data/feed.py`
Changed `maybe_open_feed()` so the cached `Feed` exclusively owns provider
root streams and every caller, including the first cache owner, receives a
caller-local `Feed`/`Flume` view backed by one child subscription per provider.
The cache identity now includes normalized full symbol shape and stream policy.
Changed `Feed.open_multi_stream()` so every provider relay subscribes instead
of directly consuming a shared stream, and synchronizes relay startup before
yielding the merged channel. Each relay owns a cloned send channel so one
provider EOC cannot terminate its siblings. The module-level relay returns its
live child receiver through `TaskStatus` for typed retention and introspection.
Documented `Feed.pause()`/`Feed.resume()` as provider-context-wide controls
shared by local cache users.
> `git diff HEAD~1..HEAD -- tests/test_feed_broadcast.py`
Added real `Feed`, `Flume`, and Tractor broadcast regressions covering both
cache-miss owner and cache-hit paths, one-provider-many-market mapping,
multi-provider relay fan-out and startup, duplex controls, lexical child
closure, sibling survival after provider EOC, cached-root immutability, and
full cache-key differentiation.
Verification output:
```text
... [100%]
3 passed in 0.02s
. [100%]
1 passed in 0.01s
```
The second result is the existing EMS broadcast ownership regression. Ruff,
E501, compilation and whitespace checks passed. Repeated adversarial review
found and resolved shared `Feed.streams`, incomplete cache-key, fake-lifecycle,
relay-startup and first-owner asymmetry issues; final review reported no
findings.

View File

@ -1,32 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: unavailable
timestamp: 2026-08-13T23:07:44Z
git_ref: ddac3553
scope: code
substantive: true
raw_file: 20260813T230744Z_ddac3553_prompt_io.raw.md
---
## Prompt
Continue the piker broadcast-consumer audit after the Deribit ownership commit
and prepare the next smallest ownership fix.
## Response summary
Replaced IB's manual raw quote-channel registry with a symbol-keyed cached
context and gave every caller a lexical child from one retained
`LinkedTaskChannel` broadcaster root.
## Files changed
- `piker/brokers/ib/feed.py` - cached raw-channel lifetime and lexical child
ownership.
- `tests/test_ib_broadcast.py` - concurrent fan-out, cache-key and child
lifetime regression.
## Human edits
None - generated output follows the requested broadcast-consumer audit.

View File

@ -1,39 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-13T23:07:44Z
git_ref: ddac3553
diff_cmd: git diff HEAD~1..HEAD
---
The user asked to continue the piker broadcast-consumer audit after committing
the Deribit ownership fix. The next isolated item was IB's actor-local
`_quote_streams` cache, which retained a raw channel owned by its first caller
and constructed competing broadcaster roots for later callers.
> `git diff HEAD~1..HEAD -- piker/brokers/ib/feed.py`
Factored `_open_aio_quote_channel()` as the raw asyncio-channel context and
made `open_aio_quote_stream()` cache that context by symbol through
`maybe_open_context()`. Every caller now receives a lexical child from the
retained `LinkedTaskChannel` broadcaster, while the raw source remains alive
until its final cache user exits. Removed the manual `_quote_streams` registry
and teardown mutation.
> `git diff HEAD~1..HEAD -- tests/test_ib_broadcast.py`
Added a concurrent ownership regression using real Tractor broadcast state.
It proves miss and hit callers use the same symbol/context factory, receive the
same quote through distinct children, close independently, and leave the owner
live after the hit exits.
Verification output:
```text
. [100%]
1 passed in 0.01s
```
The new test passes through a source-isolated IB feed import. Test Ruff, E501,
Python compilation and whitespace checks pass. Normal package collection
remains blocked by the existing Qt environment mismatch.

View File

@ -1,43 +0,0 @@
---
model: gpt-5.6-sol
service: opencode
session: tractor-addr-unpacking
timestamp: 2026-08-20T16:35:19Z
git_ref: 5f631e19
scope: code
substantive: true
raw_file: 20260820T163519Z_5f631e19_prompt_io.raw.md
---
## Prompt
The human asked to implement the Piker compatibility work caused by
tractor's canonical tagged-address migration, run the previously
deferred tractor verification, and keep runtime-managed WireGuard
bindspaces in a later separate PR.
## Response summary
Made Piker normalize legacy and tagged registry inputs before semantic
comparison and process-global storage. Updated runtime address typing and
test assertions for canonical output, retained legacy caller input, and
removed a duplicate pytest log-level option that blocked downstream test
collection with current tractor.
## Files changed
- `piker/service/_registry.py` - canonical registry normalization and
shared address typing.
- `piker/service/_actor_runtime.py` - tagged-compatible runtime API
annotations.
- `tests/conftest.py` - canonical fixture comparison and shared pytest
option ownership.
- `tests/test_services.py` - registry regression and canonical runtime
assertions.
## Human edits
The human explicitly requested the downstream implementation and chose
to keep WireGuard bindspace lifecycle work in a separate future tractor
PR. The agent implemented and tested those directions; no direct manual
source edits were observed.

View File

@ -1,25 +0,0 @@
---
model: gpt-5.6-sol
service: opencode
timestamp: 2026-08-20T16:35:19Z
git_ref: 5f631e19
diff_cmd: git diff HEAD~1..HEAD
---
# Raw output - tractor tagged-address compatibility
The human requested downstream Piker qualification after tractor PR
#505 switched transport address emission to canonical tagged tuples.
> `git diff HEAD~1..HEAD -- piker/service/_registry.py piker/service/_actor_runtime.py tests/conftest.py tests/test_services.py`
Normalized legacy and tagged registry addresses through tractor's
address codec before Piker stores or compares them. Extended service
runtime annotations to cover both tuple shapes, updated runtime tests to
compare canonical serialized values, and removed Piker's duplicate
registration of tractor's shared `--ll` pytest option.
Ruff passed. The focused normalization regression and Piker runtime boot
passed against the local tractor checkout at `5d92595f`. The wider
service module reached an unrelated pre-existing logger-name assertion
in `maybe_spawn_daemon()` after both address-focused tests passed.

View File

@ -1,39 +0,0 @@
---
model: gpt-5.6-sol
service: opencode
session: tractor-addr-unpacking
timestamp: 2026-08-21T04:20:58Z
git_ref: b7aca4c0
scope: code
substantive: true
raw_file: 20260821T042058Z_b7aca4c0_prompt_io.raw.md
---
## Prompt
The human approved refining Piker's logger-name handling after the
wider service test exposed a stale parent-logger assertion during the
tractor tagged-address compatibility work.
## Response summary
Changed Piker's logger helpers to remove only an exact `piker.` prefix,
preserving complete requested submodule names. Removed daemon startup
assertions that incorrectly required parent logger names and added a
regression covering both logger factories and names that the old
character-set stripping silently corrupted.
## Files changed
- `piker/log.py` - exact relative logger-name derivation.
- `piker/service/_daemon.py` - remove stale parent-name assertion.
- `piker/data/_daemon.py` - remove stale parent-name assertion.
- `piker/brokers/_daemon.py` - remove stale parent-name assertion.
- `piker/clearing/_ems.py` - remove redundant logger assertion.
- `tests/test_logging.py` - package logger namespace regressions.
## Human edits
The human explicitly approved this follow-up after reviewing the
residual Piker service-suite gap. The agent implemented and verified
the refinement; no direct manual source edits were observed.

View File

@ -1,24 +0,0 @@
---
model: gpt-5.6-sol
service: opencode
timestamp: 2026-08-21T04:20:58Z
git_ref: b7aca4c0
diff_cmd: git diff HEAD~1..HEAD
---
# Raw output - preserve Piker logger namespaces
The human approved a follow-up refinement after the full Piker service
module exposed a stale logger-name assertion while qualifying tractor
PR #505.
> `git diff HEAD~1..HEAD -- piker/log.py piker/service/_daemon.py piker/data/_daemon.py piker/brokers/_daemon.py piker/clearing/_ems.py tests/test_logging.py`
Replaced character-set `str.lstrip()` calls with exact package-prefix
removal, removed assertion-only parent-logger expectations from daemon
startup paths, and added regression coverage for both logger factories
and prefix-sensitive submodule names.
Ruff passed after removing newly unused logger assignments. All four
logger regressions and all six service integration tests passed against
the local tractor PR #505 checkout.

View File

@ -1,46 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: unavailable
timestamp: 2026-08-26T22:26:00Z
git_ref: 5f631e19
scope: code
substantive: true
raw_file: 20260826T222600Z_5f631e19_prompt_io.raw.md
---
## Prompt
Diagnose and fix the hanging Binance USD-M perpetual feed for
`NVDAUSDT.USDTM.PERP.BINANCE` by comparing piker with Binance's current
API and changelog. Ensure unsupported or silent websocket feeds fail
loudly into `datad` and chart consumers instead of blocking startup.
## Response summary
Migrates Binance USD-M market data from the retired legacy websocket to
the split public and market routes, handles Binance's new aggregate-trade
schema, and introduces bounded connection and first-data startup. Direct
stream URLs avoid reconnect-time subscription-ACK races, while targeted
tests preserve routing and fail-loud behavior.
## Files changed
- `piker/brokers/binance/feed.py` - route, merge, normalize, and bound
Binance quote streams.
- `piker/brokers/binance/venues.py` - define current USD-M websocket
endpoints.
- `piker/data/_web_bs.py` - support bounded websocket context startup.
- `tests/test_binance.py` - cover routes, schemas, and startup failures.
- `ai/prompt-io/opencode/20260826T222600Z_5f631e19_prompt_io.raw.md` -
retain the generated-output record.
- `ai/prompt-io/opencode/20260826T222600Z_5f631e19_prompt_io.md` - record
structured provenance for the code changes.
## Human edits
The human explicitly required fail-loud behavior rather than merely
updating the endpoint, and supplied a direct CLI reproduction proving
both `datad.binance` and the chart caller blocked. Those requirements
drove the connection and first-data deadlines and their regressions. The
changes remain uncommitted for human review.

View File

@ -1,57 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-26T22:26:00Z
git_ref: 5f631e19
diff_cmd: git diff HEAD~1..HEAD
---
## Prompt
The user reported that selecting
`NVDAUSDT.USDTM.PERP.BINANCE` connected Binance's USD-M
websocket but never loaded the feed or downstream chart. They asked
OpenCode to compare Binance's API changelog with piker's FQME and feed
code, diagnose the failure, and solve it.
The user clarified that future provider failures must raise loudly
instead of silently blocking feed and chart startup. They also confirmed
the same hang from
`piker -l info --pdb chart nvdausdt.usdtm.perp.binance`.
## Generated code
> `git diff HEAD~1..HEAD -- piker/brokers/binance/feed.py`
Routes USD-M book-ticker and aggregate-trade data through Binance's new
`/public` and `/market` websocket paths, merges both sockets into the
existing quote stream, accepts the new `st` aggregate-trade field, and
requires both stream classes before declaring startup complete. It also
removes the misleading missing-provider warning, derives feed mode from
resolved pair metadata, and raises `DataUnavailable` for connection or
live-data startup timeouts.
> `git diff HEAD~1..HEAD -- piker/brokers/binance/venues.py`
Defines Binance's current public and market USD-M websocket endpoints
and makes the market endpoint the regular futures websocket API root.
> `git diff HEAD~1..HEAD -- piker/data/_web_bs.py`
Adds an optional connection deadline to `open_autorecon_ws()` so callers
can fail context entry when repeated websocket handshakes never become
connected.
> `git diff HEAD~1..HEAD -- tests/test_binance.py`
Adds regressions for exact Binance stream routing, silent live-data
startup, silent websocket connection startup, and the current USD-M
aggregate-trade payload schema.
## Verification output
`pytest -q tests/test_binance.py` passes all four tests. Compile checks,
focused Ruff fatal-error checks, and `git diff --check` pass. Live probes
through piker's merged stream received both required stream classes and
returned normalized trade quotes for `NVDAUSDT` USD-M and `BTCUSDT`
spot.

View File

@ -1,37 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: 3e2eb8c9-6cfb-49a5-bf1a-1c0c2c2a4ddd
timestamp: 2026-08-28T20:03:52Z
git_ref: dd676857
scope: config
substantive: true
raw_file: 20260828T200352Z_dd676857_prompt_io.raw.md
---
## Prompt
Update the xonsh version used by the repository dev shell before stacking
the Binance work, while retaining the dotrc compatibility shim as an
immediate workaround for older shells.
## Response summary
Raises the xonsh dependency floor to `0.24.2` and refreshes its lock entry.
The new release supports Python 3.13, handles Git alias completion natively,
and works in the updated Nix development shell.
## Files changed
- `pyproject.toml` - require xonsh `0.24.2` or newer.
- `uv.lock` - lock xonsh `0.24.2` wheels and source metadata.
- `ai/prompt-io/opencode/20260828T200352Z_dd676857_prompt_io.raw.md` - retain
the triggering prompt, generated configuration summary, and checks.
- `ai/prompt-io/opencode/20260828T200352Z_dd676857_prompt_io.md` - record
structured provenance for the dependency update.
## Human edits
The user identified old xonsh behavior as the root cause, reported the
separate dotrc compatibility shim, and directed that the repository update
remain a distinct flake-layer change before the Binance stack continues.

View File

@ -1,36 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-28T20:03:52Z
git_ref: dd676857
diff_cmd: git diff HEAD~1..HEAD
---
## Prompt
The user traced broken multiline conflict-resolution pastes to the old
xonsh release installed by the repository dev shell. A separate dotrc
agent added a compatibility shim, and the user directed that xonsh still
be updated alongside the flake work before continuing the Binance stack.
## Generated configuration
> `git diff HEAD~1..HEAD -- pyproject.toml uv.lock`
Raises the repository's xonsh requirement from `0.22.2` to `0.24.2` and
refreshes only xonsh's locked distribution metadata. The selected release
supports Python 3.13, includes native Git-config alias completion, and fixes
Nix metadata checks.
## Verification output
- `uv lock --upgrade-package xonsh` resolves 111 packages and updates only
xonsh from `0.22.4` to `0.24.2`.
- Fresh `nix develop` replaces the worktree-local xonsh installation.
- `xonsh --version` reports `0.24.2`.
- A multi-command xonsh invocation executes Git commands successfully.
- `nix flake check --no-build` passes for `x86_64-linux`.
- `git diff --check` passes.
The existing Polars `1.36.0` lock remains unchanged despite uv reporting
that its distributions are yanked for an `unpivot` regression.

View File

@ -1,46 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: c3a1b1ba-65c6-4c8e-bd12-e596031b0eb0
timestamp: 2026-08-29T17:55:42Z
git_ref: e09e306a
scope: code
substantive: true
raw_file: 20260829T175542Z_e09e306a_prompt_io.raw.md
---
## Prompt
Preserve the existing Binance USD-M commit above the reviewed flake/xonsh
stack, then add missing deterministic tests and correctness fixes as a
separate follow-up with a complete editor-gated commit plan.
## Response summary
Hardens Binance feed startup around its supported one-symbol contract,
direct websocket routes, current provider schemas, and first-quote
readiness. Adds deterministic Binance regressions and separates generic
reconnect behavior into its own tested commit boundary.
## Files changed
- `piker/brokers/binance/feed.py` - validate feed cardinality, route
USD-M stream classes, and declare readiness after either quote class.
- `tests/test_binance.py` - cover routes, first-quote startup, cardinality,
and documented book-ticker and aggregate-trade schemas.
- `ai/prompt-io/opencode/20260829T175542Z_e09e306a_prompt_io.raw.md` - retain
the triggering prompt, generated-code summary, and verification output.
- `ai/prompt-io/opencode/20260829T175542Z_e09e306a_prompt_io.md` - record
structured provenance for the follow-up.
## Human edits
The user rejected reconstructing already committed Binance work, required
preservation through cherry-pick, selected the flake/xonsh stack base, and
required the review fixes to remain a separate tested boundary. The user also
corrected the workflow to require an automatic `/commit-plan` at every stack
review gate. During Tuicr review, the user rejected trade-gated startup,
requested a feed-specific initialization error and async-generator TODO,
and required generic reconnect changes and tests to land independently.
The user then clarified that either L1 or trade data must unblock startup,
rather than preferring one stream class or waiting independently for both.

View File

@ -1,43 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-29T17:55:42Z
git_ref: e09e306a
diff_cmd: git diff HEAD~1..HEAD
---
## Prompt
The user asked to land recent branch work in small chronological patch sets,
including missing tests. For Binance, the user required preserving the
existing committed topic patch through cherry-pick and placing newly found
correctness repairs in a separate follow-up boundary above the flake/xonsh
stack. The user also required every yielded stack layer to include a complete
`/commit-plan` with patch-specific checks.
## Generated code
> `git diff HEAD~1..HEAD -- piker/brokers/binance/feed.py`
Restricts Binance feeds to the supported one-symbol contract, renders direct
quote routes, separates L1 liveness from first-trade readiness, and derives
the contextual trade timeout from Trio's enclosing effective deadline.
> `git diff HEAD~1..HEAD -- piker/data/_web_bs.py`
Lets structured cancellation bypass reconnect throttling and ensures handled
handshake rejections retry before accessing connection-local cancel state.
> `git diff HEAD~1..HEAD -- tests/test_binance.py`
Adds deterministic regressions for symbol cardinality, route selection,
silent startup, immediate rejection, cancellation cleanup, aggregate-trade
schema evolution, delayed trades, and finite/infinite/tiny deadlines.
## Verification output
- Targeted Binance tests: `10 passed in 0.26s`.
- Ruff passes for all three changed Python files.
- `git diff --check` passes.
- Independent final review reports no actionable findings.
- No live Binance network probe was run.

View File

@ -1,42 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: c3a1b1ba-65c6-4c8e-bd12-e596031b0eb0
timestamp: 2026-09-01T02:42:35Z
git_ref: e09e306a
scope: code
substantive: true
raw_file: 20260901T024235Z_e09e306a_prompt_io.raw.md
---
## Prompt
Address the Tuicr review by separating generic websocket reconnect
changes from Binance-specific work and testing the real reconnect task
hierarchy rather than a reimplementation.
## Response summary
Preserves structured cancellation and safe first-handshake retries in
`_web_bs`. Adds three focused regressions in a dedicated websocket test
module and packages them as an independent commit boundary.
## Files changed
- `piker/data/_web_bs.py` - restart handled handshake failures and let
cancellation bypass the reconnect throttle.
- `tests/test_websocket.py` - exercise real reconnect startup, retry,
and cancellation task paths with controlled transport boundaries.
- `ai/prompt-io/opencode/20260901T024235Z_e09e306a_prompt_io.raw.md`
- retain the review prompt, generated-code summary, and verification.
- `ai/prompt-io/opencode/20260901T024235Z_e09e306a_prompt_io.md` - record
structured provenance for the generic reconnect boundary.
## Human edits
The user identified that these changes were generic rather than
Binance-specific, required a separate commit and test module, requested
coverage of the real reconnect hierarchy, and corrected code prose to
refer to `trio` consistently. A follow-up review required the production
code to explain why `trio.Cancelled` bypasses the broad reconnect-error
handler and cite the regression which audits that teardown invariant.

View File

@ -1,33 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-09-01T02:42:35Z
git_ref: e09e306a
diff_cmd: git diff HEAD~1..HEAD
---
## Prompt
The user requested that generic `_web_bs` reconnect fixes and tests be
separated from the Binance backend patch. The review also required tests
to exercise the real reconnect task hierarchy, cite the previous faulty
branches, and consistently name `trio` in code prose.
## Generated code
> `git diff HEAD~1..HEAD -- piker/data/_web_bs.py`
Ensures handled handshake failures immediately restart the reconnect
loop and lets structured cancellation bypass shielded error throttling.
> `git diff HEAD~1..HEAD -- tests/test_websocket.py`
Adds deterministic scheduler-real regressions for handshake startup
timeouts, immediate rejection retries, and cancellation propagation.
## Verification output
- Generic and Binance targeted tests: `9 passed in 0.06s`.
- Ruff passes for all four changed Python files.
- `git diff --check` passes.
- No live network probe was run.

View File

@ -1,41 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: 4dc920cf-e82e-4c1e-83db-75a0a9efe82b
timestamp: 2026-09-01T23:00:43Z
git_ref: e91f41a6
scope: config
substantive: true
raw_file: 20260901T230043Z_e91f41a6_prompt_io.raw.md
---
## Prompt
Update `wkt/land_flake_update` to Tractor's restored `piker_pin` after
reusing the existing runtime compatibility commits. Keep stale
historical lock snapshots out of the final dependency state.
## Response summary
Updates piker's Python and `msgspec` requirements for current Tractor,
then generates one fresh lock at Tractor `piker_pin@0d95a9e6`. The
dependency commit completes the compatibility stack already
cherry-picked onto the flake branch.
## Files changed
- `pyproject.toml` - require Python 3.13 and `msgspec>=0.20.0`.
- `uv.lock` - resolve Tractor `piker_pin@0d95a9e6`, `msgspec 0.21.1`,
and current transport dependencies.
- `ai/prompt-io/opencode/20260901T230043Z_e91f41a6_prompt_io.raw.md`
- preserve the prompt, generated-change summary, and resolution notes.
- `ai/prompt-io/opencode/20260901T230043Z_e91f41a6_prompt_io.md` - record
structured provenance for the dependency update.
## Human edits
The user identified that Tractor compatibility should land with the
flake update rather than the Binance patch, supplied four known source
commits, required a broader local-history audit for missing ports, and
directed a fresh lock from the restored `piker_pin` instead of replaying
historical pins.

View File

@ -1,35 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-09-01T23:00:43Z
git_ref: e91f41a6
diff_cmd: git diff HEAD~1..HEAD
---
## Prompt
The user requested that the flake-update branch absorb the Tractor
runtime compatibility work before the Binance branch lands. They
directed reuse of existing compatibility commits, exclusion of stale
historical lock pins, and a fresh update to Tractor's restored
`piker_pin`.
## Generated changes
> `git diff HEAD~1..HEAD -- pyproject.toml`
Narrows piker support to Python 3.13 and raises the direct `msgspec`
requirement to the version family required by current Tractor.
> `git diff --cached -- uv.lock`
Relocks against Tractor `piker_pin@0d95a9e6`, `msgspec 0.21.1`, and the
current transport dependency graph while retaining the existing Xonsh
update.
## Resolution notes
- Historical lock hunks from reused commits were intentionally omitted.
- Tractor runtime compatibility remains represented by the preceding
individually preserved commits on `wkt/land_flake_update`.
- Project checks are rendered in the commit plan for human execution.

View File

@ -1,52 +0,0 @@
---
created_at: 2026-10-21T22:44:21Z
generated_by:
harness: opencode
provider: openai
model: openai/gpt-6.1-sol
git_ref: 507c75c6
scope: config
substantive: true
raw_file: 20261021T224421Z_507c75c6_piker_pin_prompt_io.raw.md
---
## Prompt
The human asked to sanity-check the current PR94 landing state against
the qualified new Tractor pin before deduplicating branch history. After
the initial offline checks passed, the human authorized changing the
selector to piker_pin, resolving the lock, and verifying the installed
dependency without the temporary local-source PYTHONPATH override.
The human explicitly authorized network resolution and a scoped update
of Tractor/runtime dependencies while preserving unrelated packages.
## Response summary
Updated the branch selector and lock to published piker_pin@b64c89f0.
Installed the exact locked Tractor revision, verified its installed
source metadata, and reran 14 offline logger/startup cases without a
local Tractor source override. Preserved the rest of the environment.
## Files changed
- `pyproject.toml`: select piker_pin and bound Polars below 2.0.
- `uv.lock`: lock b64c89f0 and Polars/runtime 1.44.2.
- This provenance pair.
## Verification and limits
14 distinct offline cases passed against the installed Tractor update
with the existing Polars 1.36 installation. The actor.uid deprecation
warning remains. No live provider/order testing occurred.
The initial resolver selected Polars 2.0 and rejected retaining 1.36
for its Windows target. The human then requested the latest pre-2.0
release instead. PyPI metadata identified non-yanked stable 1.44.2;
adding `<2.0.0` to the existing lower bound and relocking succeeded
with both polars and polars-runtime-32 at 1.44.2, without platform edits.
The raw partner retains the original, superseded 2.0 generation history.
The scoped install changed only Tractor. Polars 1.44.2 is locked but
not installed or data-path qualified yet. The human's existing staged
checkpoint is preserved; the pre-2.0 correction is in the worktree.
No history cleanup, staging or commit was performed by this agent.

View File

@ -1,45 +0,0 @@
---
created_at: 2026-10-21T22:44:21Z
generated_by:
harness: opencode
provider: openai
model: openai/gpt-6.1-sol
git_ref: 507c75c6
diff_cmd: git diff HEAD~1..HEAD
---
> `git diff HEAD~1..HEAD -- pyproject.toml uv.lock`
Changed the landing branch's Tractor selector from wkt/addr_unpacking
to piker_pin and resolved the published branch to
b64c89f05a9fc8ac2878551eb5eda36574ebef6a. An offline branch lookup lacked
the cached name, so the human explicitly authorized network resolution
and a scoped installed-runtime update preserving unrelated packages.
The resolver advanced Polars and polars-runtime-32 from 1.36.0 to 2.0.0.
An attempt to retain both exact 1.36.0 versions was rejected for the
resolver's Windows target. The valid lock with the visible Polars bump
is retained for review; no platform restriction or source constraint was
introduced to bypass the failure.
Installed only Tractor and its required dependencies with uv pip rather
than syncing/pruning the whole environment. The installer reported one
package replacement: Tractor a966928d to b64c89f0. Qt distribution versions
remained PyQt6 6.10.1, PyQt6-Qt6 6.10.1 and PyQt6-sip 13.10.3.
Removed PYTHONPATH from verification subprocesses. Piker resolved inside
land_flake_update and Tractor resolved from that checkout's py313
site-packages. Installed VCS metadata matched the exact locked revision.
The temporary LD_LIBRARY_PATH used the binding's bundled Qt libraries
and matching native dependencies; no global environment setup changed.
Verification output:
- Logger/non-actor startup: 12 passed, 1 deselected in 0.05s.
- test_runtime_boot: 1 passed, 1 warning in 0.04s.
- test_daemon_bind_distinct_from_registrar: 1 passed in 0.77s.
- Existing warning: actor.uid deprecation; no failure.
- git diff --check passed.
This verifies the installed Tractor pin, not a full sync to all new lock
versions. Polars was not upgraded in the environment by the scoped install.
No live broker feed, order, branch rewrite, staging or commit was run.

View File

@ -1,67 +0,0 @@
---
created_at: 2026-10-22T21:22:48Z
generated_by:
harness: opencode
provider: openai
model: openai/gpt-6-astra
git_ref: dc618229bd8c9c90aa3160e26677b553a7d472c7
scope: code
substantive: true
raw_file: 20261022T212248Z_dc618229_ib_recovery_prompt_io.raw.md
---
## Prompt
The human approved trying the bounded candidate: back out the IB
quote-context cache, import adapter-only reset/boundary fixes and
the missing EMS subscription fix, preserve source authorship and
commit boundaries, and exercise the real Tractor bridge. They
required an explicit keep/backout audit and clarification that
the evaluated downstream lineage lacked the broadcast fix.
## Response summary
Verified identical stable patch IDs for original 4af885d9 and
landing replay 635314c7. Created `wkt/land_ib_recovery` at dc618229
in the main checkout. Applied source-equivalent adapter backports
from 0846cbd4 and 1da9b760 and the EMS code from 12b0d66e, with
source attribution recorded in `ai/ib-recovery-candidate.md`.
The real-runtime quote test first reproduced the reported fatal
TrioTaskExited propagation with caching enabled. The direct-owner
backout restores restart and subsequent RPC survival, but a strict
callback-cleanliness test remains red. Real bridge history pacing
recovery and real broadcaster dark clearing pass. No live broker,
VNC, orders or TSDB operations were performed by the agent.
On 2026-10-23 the human supplied successful live startup/backfill,
sequential reattachment and overlapping chart-close/reopen results,
plus the still-failing daemon SIGINT teardown. The human accepted
deferring teardown from #94, requested loud partial-backport
attribution and required downstream rebase rehearsals before landing.
The agent removed only the explicit single-null debugger stop,
retaining its legacy result. Downstream `976db6c3` instead rewrites
null grouping; the local cleanup does not claim that algorithmic fix.
## Files changed
- `piker/brokers/ib/feed.py`: source-equivalent backout/backports.
- `piker/clearing/_ems.py`: original EMS subscription patch.
- `tests/test_ib_broadcast.py`: removed with the reverted behavior.
- `tests/test_ib_history.py`: original adapter regressions only.
- `tests/test_ems_broadcast.py`: imported and real-broadcaster tests.
- `tests/test_ib_quote_restart.py`: real Tractor boundary tests.
- `piker/tsp/_anal.py`: local single-null breakpoint removal only.
- `ai/ib-recovery-candidate.md`: provenance and qualification limits.
## Human edits
The human required original fixes and authorship, rejected broad
speculative integration work, required real concurrency boundaries
in tests, and corrected the interpretation of live qualification.
They performed the live tests, chose to defer known teardown defects
from #94, permitted the minimal debugger cleanup, and requested
explicit downstream discrepancies and rebase compatibility checks.
The agent applied those directed edits; no direct manual source edits
are known in this candidate. The raw partner retains the originally
captured output; this structured record includes later contributions.

View File

@ -1,69 +0,0 @@
---
created_at: 2026-10-22T21:22:48Z
generated_by:
harness: opencode
provider: openai
model: openai/gpt-6-astra
git_ref: dc618229bd8c9c90aa3160e26677b553a7d472c7
diff_cmd: git diff dc618229 -- piker/brokers/ib/feed.py piker/clearing/_ems.py tests
---
## Human direction
> yeah i guess this is our best approach strat for sure.
> so you're saying these 2 are not the same patch?
> please tell me no lol
> so detail which commits to drop vs. keep please in detail so i can
> audit your choices this time..
> let's try it and consider bailing and re-trying if this leads us into
> a further (and deeper) sinkhole, bc as it stands right now this whole
> endevour has turned into a complete shitfest XD
> you didn't respond to my ask about the wkt/ib_downstream_pin_eval stuff and it missing the bcast rx commit either
## Unedited diagnostic response
The real-bridge test reproduced the exact fatal path:
`maybe_open_context()` teardown raises `TrioTaskExited` back into
the quote consumer.
With only the quote-cache backout, both quote generations complete
and their asyncio tasks finish. Tractor still emits
`TrioTaskExited` callback errors, so the strict test remains red.
That narrows the result: the backout removes the fatal propagation
in this reproducer, but doesn’t make bridge teardown clean.
## Code output pointers
> `git diff dc618229 -- piker/brokers/ib/feed.py`
Back out 635314c7's quote owner and import the original adapter-only
deltas from 0846cbd4 and 1da9b760.
> `git diff dc618229 -- piker/clearing/_ems.py`
Import 12b0d66e's dedicated dark-clearing quote subscription.
> `git diff dc618229 -- tests/test_ib_broadcast.py tests/test_ib_quote_restart.py tests/test_ib_history.py tests/test_ems_broadcast.py`
Replace the reverted mock-cache test with a real-runtime restart
reproducer; retain original history/EMS tests and add real bridge
history recovery and real broadcaster EMS coverage. Newly added
test files remain untracked until the human stages them, so the
diff pointers include them only after staging or committing.
## Verification output
```text
21 passed in 0.53s
1 failed, 2 passed in 5.53s
13 passed in 1.41s
1 passed, 1 warning in 0.04s
2 passed in 0.03s
```
The last run includes the original EMS case again and one additional
real-broadcaster case: 38 unique passes and one strict callback failure.

View File

@ -1,77 +0,0 @@
---
created_at: 2026-10-28T16:08:46Z
generated_by:
harness: opencode
provider: openai
model: openai/gpt-6.1-sol
git_ref: 7242dcb9
scope: code
substantive: true
raw_file: 20261028T160846Z_7242dcb9_websocket_close.raw.md
---
## Prompt
The human reported repeated live Binance perp crashes after correct
worktree startup: the real trio-websocket reader attempted a Pong
while wsproto was LOCAL_CLOSING. They asked whether an existing
downstream websocket fix should be reused, then requested:
> ok add the fix now, follow by a /commit-plan and then i'll test
> before saving it ya?
They approved a real websocket reader/close regression covering
recovery and intentional shutdown, and separately authorized
reconciling this worktree's legacy workflow bookkeeping while
preserving existing archives.
## Response summary
The older `3d83b61f3f7b982e84a4230dba5777f1407c78f1` wrapper
suppressed escaped communication failures. The landing replay
`97eb8347` of `d50ab6124cd0e1646c82fa4a85d7c13bfff82611`
added propagation; it did not restart a reader-context failure.
Existing `LocalProtocolError` membership in NoBsWs.recon_errors
only protected the receive relay, not reader-nursery context exit.
Classify these errors around the complete websocket context and
retry instead of restoring outer suppression. Place collapse_eg
outside that context, invalidate stale connection readiness and
socket references, and leave the retry wait cancellable. Preserve
unexpected failures and mixed exception groups for propagation.
After review, use RuntimeError rather than ValueError for the
test-only unexpected-failure sentinel. Its injected exception has
no underlying cause. Production recovery logs retain the original
traceback and now name the URL; unexpected exceptions use bare
raise to preserve identity, existing causes and notes.
The deterministic seam controls protocol-state ordering in the real
client Ping handler. It does not replace the websocket transport,
reader, context manager, reconnect task or wsproto exception.
## Files changed
- `piker/data/_web_bs.py`: reader-context failure recovery.
- `tests/test_websocket.py`: loopback recovery and two shutdown
variants; retain unexpected-failure propagation control.
- This provenance pair: live report, lineage and test scope.
## Verification
Baseline uses new tests against original HEAD websocket code in an
isolated exact-tree checkout. The recovery case fails with the
reported Pong-in-LOCAL_CLOSING LocalProtocolError.
Fixed code passes 13 cases across tests/test_websocket.py and
tests/test_binance.py. Exact-boundary verification and native-library
configuration are retained in the ignored commit-plan package.
Live testing remains human-owned and pending.
## Human edits
No direct manual source edits observed in this follow-up. The human
supplied live failures, corrected the earlier daemon/worktree setup,
set the narrow landing scope, requested source-fix reuse, and will
test before reviewing and committing. Existing commits are retained.
The human requested the RuntimeError sentinel and preservation of
underlying error context. The raw partner retains the initial output.

View File

@ -1,36 +0,0 @@
---
created_at: 2026-10-28T16:08:46Z
generated_by:
harness: opencode
provider: openai
model: openai/gpt-6.1-sol
git_ref: 7242dcb9
diff_cmd: git diff HEAD~1..HEAD
---
Implemented a focused websocket-context reconnect fix and real
loopback reader/close regressions in the Binance landing worktree.
> `git diff HEAD~1..HEAD -- piker/data/_web_bs.py`
Move exception-group collapse outside the websocket context so
reader-nursery exit errors are classified by the reconnect loop.
Retry NoBsWs.recon_errors there, clear the stale connected event
and socket reference, and honor cancellation through unshielded
retry sleep. Retain unexpected-error propagation at the outer API.
> `git diff HEAD~1..HEAD -- tests/test_websocket.py`
Exercise real TCP, handshake, Ping frames, reader dispatch and
wsproto Pong validation. A narrow handler gate advances the real
protocol state to LOCAL_CLOSING without sending the Close frame,
preventing a peer reply from racing the controlled state. Require
fresh application data from the replacement connection. Separate
variants cancel at the reader gate and at the actual retry wait.
Update the existing unexpected-failure control to use ValueError
because LocalProtocolError is now intentionally recoverable.
The isolated original-code/new-test baseline fails with the exact
Pong-in-LOCAL_CLOSING error. The fixed isolated tree passes all 13
selected Binance/websocket cases. No live provider, broker orders
or production daemon operations were performed by the agent.

View File

@ -1,70 +0,0 @@
---
created_at: 2026-10-28T16:56:38Z
generated_by:
harness: opencode
provider: openai
model: openai/gpt-6.1-sol
git_ref: b95e7c79
scope: code
substantive: true
raw_file: 20261028T165638Z_b95e7c79_reconnect_logging.raw.md
---
## Prompt
The human reported that the live Binance Pong-in-LOCAL_CLOSING
error now logged a retry without crashing. They requested visible
re-establishment reporting under `pikerd -l info --pdb`, and asked
to check for existing downstream work before adding a fix.
## Response summary
The existing `Connection success` INFO message already identifies
handshake completion. Feed startup configured piker.data.feed,
while shared websocket diagnostics use ._util.log on piker.data.
Configure that actual emitter and prefer the daemon's verbosity
over a quieter client request for these shared transport tasks.
Add a readiness INFO message after fixture setup and a first-message
INFO report once per connection after actual relay. Include the URL
in the receive-timeout reset error. Do not call first raw websocket
traffic a parsed quote or proof of restored market-data freshness.
## Existing-work audit
Compared `_web_bs.py`, `_util.py`, `piker.log` and daemon logging
against the evaluated downstream tip and relevant all-ref histories.
`54da2973` contains logging/type cleanup, not this emitter fix.
`5adf77a9` / landing equivalents address package-name correctness,
but not configuration of the shared data emitter at feed startup.
All-ref searches for this actual emitter/readiness/first-relay
implementation found no existing commit. The dirty IB history
worktree has only an unrelated f-string correction in `_web_bs.py`;
the TIPC worktree has no dirty delta in these two feed/websocket
files. No downstream work was overwritten or imported wholesale.
## Files changed
- `piker/data/feed.py`: shared transport logger startup/verbosity.
- `piker/data/_web_bs.py`: readiness, first-relay and timeout URL.
- `tests/test_logging.py`: actual emitter/console startup regression.
- `tests/test_websocket.py`: recovery milestones on real loopback.
- This prompt provenance pair.
## Verification
The new logging regression against original b95e7c79 source fails:
the actual websocket emitter remains at WARNING and rejects INFO.
It passes with the logger wiring fix. Provider lookup is stopped
before network work, while logger factories and console emission
are real. Global logger state is restored after the check.
18 cases pass across tests/test_logging.py, tests/test_websocket.py
and tests/test_binance.py. git diff --check passes. Live verification
after restarting the daemon with these edits remains human-owned.
## Human edits
No direct manual source edits observed. The human supplied live
recovery evidence, selected INFO verbosity, requested downstream
reuse inspection, and retains live-test and commit ownership.

View File

@ -1,38 +0,0 @@
---
created_at: 2026-10-28T16:56:38Z
generated_by:
harness: opencode
provider: openai
model: openai/gpt-6.1-sol
git_ref: b95e7c79
diff_cmd: git diff HEAD~1..HEAD
---
Confirmed the existing INFO handshake-success log was hidden by
logger wiring: websocket code emits on piker.data, while feed
startup configured piker.data.feed. The baseline regression leaves
the emitter at WARNING for an INFO daemon and ERROR client.
> `git diff HEAD~1..HEAD -- piker/data/feed.py`
Configure the actual shared ._util.log namespace, with daemon
verbosity taking priority over client verbosity for actor-wide
transport diagnostics.
> `git diff HEAD~1..HEAD -- piker/data/_web_bs.py`
Keep the existing handshake-success message. Add INFO readiness
after fixture setup and first-message relay once per connection.
Include the URL in the receive-timeout reset diagnostic.
> `git diff HEAD~1..HEAD -- tests/test_logging.py tests/test_websocket.py`
Prove production startup enables real console INFO on the websocket
emitter without contacting a provider. Extend the real loopback
recovery test to require readiness for both connections and one
first-message report for the connection delivering fresh data.
All 18 targeted logging/Binance/websocket cases passed. No live
provider/daemon operations were performed by the agent. First relay
is raw websocket traffic; parsed market-data recovery remains a
separate live observation.

View File

@ -1,54 +0,0 @@
---
created_at: 2026-10-28T18:08:56Z
generated_by:
harness: opencode
provider: openai
model: openai/gpt-6.1-sol
git_ref: 032580bf
scope: code
substantive: true
raw_file: 20261028T180856Z_032580bf_timeout_phase.raw.md
---
## Prompt
The human reported repeated readiness without first-message relay,
specifically after first attempting an invalid .usdm. FQME and then
the corrected .usdtm. one. They requested distinct log messages for
receive starvation versus local backpressure.
## Response summary
Add phase-specific WARNING messages for each elapsed relay-loop
deadline, with URL, timeout duration, accumulated count and reset
threshold. Leave the existing shared receive/send deadline and
counter/reset semantics intact. Do not claim that connected/ready
sockets alone establish parsed quote recovery.
Read-only invalid-venue tracing found the usd substring check in
Binance get_mkt_info() accepts USDM while retaining it in MktPair.
Client.bars() later uses that market identity against canonical
USDTM keys. Backend stream startup precedes history startup and
sampler consumption. A relationship to the corrected-feed stall
still needs runtime evidence; neither venue handling nor task
lifetime behavior was changed by this diagnostic patch.
## Files changed
- `piker/data/_web_bs.py`: track and report the timed-out operation.
- `tests/test_websocket.py`: real silent-source and blocked-sink cases.
- This prompt provenance pair.
## Verification
15 selected Binance/websocket cases passed. The new cases exercise
real TCP, websocket handshake and relay behavior, replacing only
log observation. A zero-capacity relay deliberately forces the
local-send case; the source-silent case never receives a payload.
git diff --check passed. No live provider/daemon operations run.
## Human edits
No direct source edits observed. The human supplied the critical
invalid-symbol-then-corrected-symbol trigger and requested the
phase distinction; live testing and task acceptance remain theirs.

View File

@ -1,34 +0,0 @@
---
created_at: 2026-10-28T18:08:56Z
generated_by:
harness: opencode
provider: openai
model: openai/gpt-6.1-sol
git_ref: 032580bf
diff_cmd: git diff HEAD~1..HEAD
---
Added distinct timeout diagnostics for websocket receive starvation
versus local relay backpressure, preserving the existing timeout,
counter and reconnect threshold policy.
> `git diff HEAD~1..HEAD -- piker/data/_web_bs.py`
Track whether proxy_msgs() is awaiting ws.get_message() or
snd.send(). Report that phase, URL, configured deadline, timeout
count and reset threshold for every timeout, before the existing
threshold/reset diagnostic. Capture the configured deadline once
per operation so the report matches the deadline that elapsed.
> `git diff HEAD~1..HEAD -- tests/test_websocket.py`
Two real loopback variants use a silent server versus one payload
and an unconsumed zero-capacity Trio relay. Require the appropriate
phase and URL on the actual first timeout. All 15 selected
Binance/websocket cases pass. No live provider was contacted.
Read-only tracing found a permissive invalid-venue path: get_mkt_info
accepts USDM via its usd substring check and preserves that venue
in MktPair, whereas Client.bars() indexes canonical USDTM pair keys.
This identifies a potential late failed-startup path, not proof of
the corrected-chart stall or of leaked producer ownership.

View File

@ -1,29 +1,8 @@
[network]
# This Tractor pin supports one transport per actor, not mixed TCP/UDS.
# Use the single TCP endpoint shown in each list for this version.
# Mixed-transport configs need a future Tractor pin with that support.
pikerd = [
# TCP localhost loopback
'/ip4/127.0.0.1/tcp/6116',
# UDS alternative: replace TCP and select the UDS transport.
# '/unix/run/user/1000/piker/pikerd.sock',
'/ipv4/127.0.0.1/tcp/6116', # std localhost daemon-actor tree
# '/uds/6116', # TODO std uds socket file
]
chart = [
'/ip4/127.0.0.1/tcp/3003',
# '/unix/run/user/1000/piker/chart.sock',
]
# the service-actor registry endpoint;
# other pikerd trees contact this to discover
# actors.
# XXX if absent, pikerd binds the registry
# on its own tpt_bind_addrs.
# regd = ['/ip4/127.0.0.1/tcp/6116']
# chart = [
# '/ip4/127.0.0.1/tcp/3333',
# '/unix/run/user/1000/piker/chart@3333.sock',
# ]
[ui]

View File

@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1784497964,
"narHash": "sha256-vlHUuqAcbcH2RKmHbPiuQzbv1pnzzavXnI62RD0bqCU=",
"lastModified": 1765779637,
"narHash": "sha256-KJ2wa/BLSrTqDjbfyNx70ov/HdgNBCBBSQP3BIzKnv4=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "241313f4e8e508cb9b13278c2b0fa25b9ca27163",
"rev": "1306659b587dc277866c7b69eb97e5f07864d8c4",
"type": "github"
},
"original": {

View File

@ -22,14 +22,12 @@
# do store-path extractions
qt6baseStorePath = lib.getLib pkgs.qt6.qtbase;
zlibStorePath = lib.getLib pkgs.zlib;
# ?TODO? can remove below since manual linking not needed?
# qt6QtWaylandStorePath = lib.getLib pkgs.qt6.qtwayland;
# XXX NOTE XXX, for now we overlay specific pkgs via
# a major-version-pinned-`cpython`
cpython = "python313";
python = pkgs.${cpython};
pypkgs = pkgs."${cpython}Packages";
in
{
@ -48,7 +46,7 @@
qt6.qtbase
uv
python
python313 # ?TODO^ how to set from `cpython` above?
pypkgs.pyqt6
pypkgs.pyqt6-sip
pypkgs.qtpy
@ -72,7 +70,6 @@
# link-in c++ stdlib for various AOT-ext-pkgs (numpy, etc.)
LD_LIBRARY_PATH="${pkgs.stdenv.cc.cc.lib}/lib:$LD_LIBRARY_PATH"
LD_LIBRARY_PATH="${zlibStorePath}/lib:$LD_LIBRARY_PATH"
export LD_LIBRARY_PATH
@ -89,31 +86,15 @@
export QT_WAYLAND_SHELL_INTEGRATION="xdg-shell"
# ------ uv ------
# ignore any venv inherited from an outer checkout
unset VIRTUAL_ENV
# - always use the ./py313/ venv-subdir
export UV_PROJECT_ENVIRONMENT="py313"
# Rebuild after a flake update changes the Nix Python closure.
venv_python="$UV_PROJECT_ENVIRONMENT/bin/python"
nix_python="$(readlink -f "${python}/bin/python3")"
if test -e "$venv_python" \
&& test "$(readlink -f "$venv_python")" != "$nix_python"
then
uv venv \
--clear \
--python "$nix_python" \
"$UV_PROJECT_ENVIRONMENT"
fi
# sync project-env with all extras
uv sync --dev --all-extras --no-group lint
source "$UV_PROJECT_ENVIRONMENT/bin/activate"
# ------ TIPS ------
# NOTE, to launch the py-venv installed `xonsh` (like @goodboy)
# run the `nix develop` cmd with,
# >> nix develop -c xonsh
# >> nix develop -c uv run xonsh
'';
};
}

View File

@ -324,13 +324,10 @@ def load_ledger(
ldir: Path = (
dirpath
or
config.get_conf_dir() / 'accounting' / 'ledgers'
config._config_dir / 'accounting' / 'ledgers'
)
if not ldir.is_dir():
ldir.mkdir(
parents=True,
exist_ok=True,
)
ldir.mkdir()
fname = f'trades_{brokername}_{acctid}.toml'
fpath: Path = ldir / fname

View File

@ -785,16 +785,9 @@ def load_account(
legacy_fn: str = f'pps.{brokername}.{acctid}.toml'
fn: str = f'account.{brokername}.{acctid}.toml'
dirpath: Path = (
dirpath
or
(config.get_conf_dir() / 'accounting')
)
dirpath: Path = dirpath or (config._config_dir / 'accounting')
if not dirpath.is_dir():
dirpath.mkdir(
parents=True,
exist_ok=True,
)
dirpath.mkdir()
conf, path = config.load(
path=dirpath / fn,

View File

@ -80,11 +80,12 @@ async def _setup_persistent_brokerd(
# any further (level) configuration on their own B)
actor: tractor.Actor = tractor.current_actor()
tll: str = actor.loglevel
get_console_log(
log = get_console_log(
level=loglevel or tll,
name=f'{_util.subsys}.{brokername}',
with_tractor_log=bool(tll),
)
assert log.name == _util.subsys
# further, set the log level on any broker broker specific
# logger instance.

View File

@ -20,17 +20,10 @@ Handy cross-broker utils.
"""
from __future__ import annotations
# from functools import partial
from typing import (
Type,
)
import json
import httpx
import logging
from msgspec import Struct
from tractor._exceptions import (
reg_err_types,
)
from piker.log import (
colorize_json,
@ -104,19 +97,6 @@ class DataThrottle(BrokerError):
'''
# TODO: add in throttle metrics/feedback
class SchemaMismatch(BrokerError):
'''
Market `Pair` fields mismatch, likely due to provider API update.
'''
# auto-register all `BrokerError` subtypes for
# tractor IPC exc-marshalling.
reg_err_types([
BrokerError,
*BrokerError.__subclasses__(),
])
def resproc(
resp: httpx.Response,
@ -143,45 +123,3 @@ def resproc(
log.debug(f"Received json contents:\n{colorize_json(msg)}")
return msg if return_json else resp
def get_or_raise_on_pair_schema_mismatch(
pair_type: Type[Struct],
fields_data: dict,
provider_name: str,
api_url: str|None = None,
) -> Struct:
'''
Boilerplate helper around assset-`Pair` field schema mismatches,
normally due to provider API updates.
'''
try:
pair: Struct = pair_type(**fields_data)
return pair
except TypeError as err:
from tractor.devx.pformat import ppfmt
repr_data: str = ppfmt(fields_data)
report: str = (
f'Field mismatch we need to codify!\n'
f'\n'
f'{pair_type!r}({repr_data})'
f'\n'
f'^^^ {err.args[0]!r} ^^^\n'
f'\n'
f"Don't panic, prolly {provider_name!r} "
f"changed their symbology schema..\n"
)
if (
api_url
or
(api_url := pair_type._api_url)
):
report += (
f'\n'
f'Check out their API docs here:\n'
f'{api_url}\n'
)
raise SchemaMismatch(report) from err

View File

@ -49,9 +49,6 @@ from piker import config
from piker.clearing._messages import (
Order,
)
from piker.brokers._util import (
get_or_raise_on_pair_schema_mismatch,
)
from piker.accounting import (
Asset,
digits_to_dec,
@ -373,12 +370,20 @@ class Client:
item['filters'] = filters
pair_type: Type = PAIRTYPES[venue]
pair: Pair = get_or_raise_on_pair_schema_mismatch(
pair_type=pair_type,
fields_data=item,
provider_name='binance',
api_url='https://binance-docs.github.io/apidocs/spot/en/#exchange-information',
)
try:
pair: Pair = pair_type(**item)
except Exception as e:
e.add_note(
f'\n'
f'New or removed field we need to codify!\n'
f'pair-type: {pair_type!r}\n'
f'\n'
f"Don't panic, prolly stupid binance changed their symbology schema again..\n"
f'Check out their API docs here:\n'
f'\n'
f'https://binance-docs.github.io/apidocs/spot/en/#exchange-information\n'
)
raise
pair_table[pair.symbol.upper()] = pair
# update an additional top-level-cross-venue-table
@ -576,8 +581,8 @@ class Client:
self,
mkt: MktPair,
start_dt: datetime|None = None,
end_dt: datetime|None = None,
start_dt: datetime | None = None,
end_dt: datetime | None = None,
as_np: bool = True,
@ -604,11 +609,7 @@ class Client:
start_time = binance_timestamp(start_dt)
end_time = binance_timestamp(end_dt)
import tractor
with tractor.devx.maybe_open_crash_handler():
bs_pair: Pair = self._pairs[
mkt.bs_fqme.upper()
]
bs_pair: Pair = self._pairs[mkt.bs_fqme.upper()]
# https://binance-docs.github.io/apidocs/spot/en/#kline-candlestick-data
bars = await self._api(

View File

@ -22,14 +22,18 @@ from __future__ import annotations
from contextlib import (
asynccontextmanager as acm,
aclosing,
AsyncExitStack,
)
from datetime import datetime
from functools import (
partial,
)
import itertools
from pprint import pformat
from typing import (
Any,
AsyncGenerator,
Callable,
Generator,
)
import time
@ -44,7 +48,6 @@ import tractor
from piker.brokers import (
open_cached_client,
NoData,
SymbolNotFound,
)
from piker._cacheables import (
async_lifo_cache,
@ -56,10 +59,7 @@ from piker.accounting import (
unpack_fqme,
)
from piker.types import Struct
from piker.data.validate import (
FeedInit,
FeedInitializationError,
)
from piker.data.validate import FeedInit
from piker.data._web_bs import (
open_autorecon_ws,
NoBsWs,
@ -76,15 +76,13 @@ from .venues import (
Pair,
FutesPair,
get_api_eps,
_futes_market_ws,
_futes_public_ws,
)
log = get_logger(name=__name__)
class L1(Struct):
# https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Individual-Symbol-Book-Ticker-Streams
# https://binance-docs.github.io/apidocs/spot/en/#individual-symbol-book-ticker-streams
update_id: int
sym: str
@ -109,15 +107,14 @@ class AggTrade(Struct, frozen=True):
T: int # Trade time
m: bool # Is the buyer the market maker?
M: bool|None = None # Ignore
nq: float|None = None # Normal quantity without RPI-order trades
# ^XXX: https://developers.binance.com/docs/derivatives/change-log#2025-12-29
st: int|None = None # 1 for USD-M, 2 for COIN-M
nq: float|None = None # Normal quantity without the trades involving RPI orders
# ^XXX https://developers.binance.com/docs/derivatives/change-log#2025-12-29
async def stream_messages(
ws: NoBsWs,
) -> AsyncGenerator[tuple[str, dict], None]:
) -> AsyncGenerator[NoBsWs, dict]:
# TODO: match syntax here!
msg: dict[str, Any]
@ -206,6 +203,27 @@ async def stream_messages(
yield 'trade', piker_quote
def make_sub(pairs: list[str], sub_name: str, uid: int) -> dict[str, str]:
'''
Create a request subscription packet dict.
- spot:
https://binance-docs.github.io/apidocs/spot/en/#live-subscribing-unsubscribing-to-streams
- futes:
https://binance-docs.github.io/apidocs/futures/en/#websocket-market-streams
'''
return {
'method': 'SUBSCRIBE',
'params': [
f'{pair.lower()}@{sub_name}'
for pair in pairs
],
'id': uid
}
# TODO, why aren't frame resp `log.info()`s showing in upstream
# code?!
@acm
@ -283,10 +301,6 @@ async def get_mkt_info(
# uppercase since kraken bs_mktid is always upper
if 'binance' not in fqme.lower():
log.warning(
f'Missing `.<provider>` part in fqme ??\n'
f'fqme: {fqme!r}\n'
)
fqme += '.binance'
mkt_mode: str = ''
@ -301,12 +315,6 @@ async def get_mkt_info(
venue: str = venue.upper()
venue_lower: str = venue.lower()
if not venue:
raise SymbolNotFound(
f'Invalid or missing .<venue> part in fqme?\n'
f'fqme: {fqme!r}\n'
)
# XXX TODO: we should change the usdtm_futes name to just
# usdm_futes (dropping the tether part) since it turns out that
# there are indeed USD-tokens OTHER THEN tether being used as
@ -398,153 +406,54 @@ async def get_mkt_info(
return mkt, pair
def get_quote_routes(
mkt_mode: str,
symbols: list[str],
) -> tuple[str, ...]:
'''
Render websocket URLs for each required Binance stream class.
'''
if len(symbols) != 1:
raise ValueError(
'Binance quote routes require exactly one symbol; '
f'got {symbols!r}'
)
symbol: str = symbols[0]
sub_names: tuple[str, str] = ('bookTicker', 'aggTrade')
if mkt_mode == 'usdtm_futes':
return tuple(
f'{wss_url}/{symbol.lower()}@{sub_name}'
for wss_url, sub_name in (
(_futes_public_ws, sub_names[0]),
(_futes_market_ws, sub_names[1]),
)
)
wss_url: str = get_api_eps(mkt_mode)[1]
return tuple(
f'{wss_url}/{symbol.lower()}@{sub_name}'
for sub_name in sub_names
)
async def relay_messages(
ws: NoBsWs,
send_chan: trio.MemorySendChannel,
) -> None:
'''
Relay one Binance websocket into a merged quote channel.
'''
async with (
send_chan,
aclosing(stream_messages(ws)) as msg_gen,
):
async for msg in msg_gen:
await send_chan.send(msg)
# TODO: Encapsulate ``relay_messages()`` and ``open_quote_stream()``
# inside ``stream_quotes()`` so async-generator closure is owned by
# the backend feed task.
@acm
async def open_quote_stream(
mkt_mode: str,
async def subscribe(
ws: NoBsWs,
symbols: list[str],
connect_timeout: float = 16,
) -> AsyncGenerator[trio.MemoryReceiveChannel, None]:
'''
Open and merge the websocket routes required by a market.
'''
quote_routes: tuple[str, ...] = get_quote_routes(
mkt_mode=mkt_mode,
symbols=symbols,
)
# defined once at import time to keep a global state B)
iter_subids: Generator[int, None, None] = itertools.count(),
send, recv = trio.open_memory_channel(616)
async with AsyncExitStack() as stack:
websockets: list[NoBsWs] = []
try:
for wss_url in quote_routes:
ws: NoBsWs = await stack.enter_async_context(
open_autorecon_ws(
url=wss_url,
connect_timeout=connect_timeout,
)
)
websockets.append(ws)
):
# setup subs
except (
TimeoutError,
*NoBsWs.recon_errors,
) as err:
routes_str: str = '\n'.join(quote_routes)
raise DataUnavailable(
f'Binance websocket connection failed for '
f'{symbols!r}:\n'
f'{routes_str}'
) from err
subid: int = next(iter_subids)
async with trio.open_nursery() as tn:
for ws in websockets:
tn.start_soon(
relay_messages,
ws,
send.clone(),
)
# trade data (aka L1)
# https://binance-docs.github.io/apidocs/spot/en/#symbol-order-book-ticker
l1_sub = make_sub(symbols, 'bookTicker', subid)
await ws.send_msg(l1_sub)
await send.aclose()
async with recv:
try:
yield recv
finally:
tn.cancel_scope.cancel()
# aggregate (each order clear by taker **not** by maker)
# trades data:
# https://binance-docs.github.io/apidocs/spot/en/#aggregate-trade-streams
agg_trades_sub = make_sub(symbols, 'aggTrade', subid)
await ws.send_msg(agg_trades_sub)
# might get ack from ws server, or maybe some
# other msg still in transit..
res = await ws.recv_msg()
subid: str|None = res.get('id')
if subid:
assert res['id'] == subid
async def wait_for_live_quotes(
msg_stream: AsyncGenerator | trio.MemoryReceiveChannel,
symbols: list[str],
startup_timeout: float = 16,
) -> dict:
'''
Require L1 or trade data before declaring a feed live.
yield
'''
if len(symbols) != 1:
raise ValueError(
'Binance quote readiness requires exactly one symbol; '
f'got {symbols!r}'
)
subs = []
for sym in symbols:
subs.append("{sym}@aggTrade")
subs.append("{sym}@bookTicker")
symbol: str = symbols[0]
msg_iter = aiter(msg_stream)
# unsub from all pairs on teardown
if ws.connected():
await ws.send_msg({
"method": "UNSUBSCRIBE",
"params": subs,
"id": subid,
})
try:
with trio.fail_after(startup_timeout):
while True:
typ, quote = await anext(msg_iter)
if (
typ in {'l1', 'trade'}
and
quote['symbol'] == symbol
):
return quote
except trio.TooSlowError as err:
raise DataUnavailable(
f'Binance quote startup timed out after '
f'{startup_timeout}s; '
f'no live data received for {symbol}'
) from err
except StopAsyncIteration as err:
raise DataUnavailable(
f'Binance quote streams closed during startup; '
f'no live data received for {symbol}'
) from err
# XXX: do we need to ack the unsub?
# await ws.recv_msg()
async def stream_quotes(
@ -556,46 +465,49 @@ async def stream_quotes(
# startup sync
task_status: TaskStatus[tuple[dict, dict]] = trio.TASK_STATUS_IGNORED,
) -> None:
if len(symbols) != 1:
# ``data.feed.allocate_persistent_feed()`` currently starts
# one backend task per symbol. Binance multiplexing remains
# deferred until that caller contract can represent
# multi-symbol startup.
raise FeedInitializationError(
'Binance feeds require exactly one symbol; '
f'got {symbols!r}'
)
) -> None:
async with (
tractor.trionics.maybe_raise_from_masking_exc(),
send_chan as send_chan,
open_cached_client('binance') as client,
):
mkt: MktPair
pair: Pair
mkt, pair = await get_mkt_info(symbols[0])
mkt_mode: str = (
'usdtm_futes'
if isinstance(pair, FutesPair)
else 'spot'
)
init_msgs: list[FeedInit] = []
for sym in symbols:
mkt: MktPair
pair: Pair
mkt, pair = await get_mkt_info(sym)
# build out init msgs according to latest spec
init_msgs: list[FeedInit] = [
FeedInit(mkt_info=mkt),
]
ws_symbols: list[str] = [mkt.bs_mktid]
async with open_quote_stream(
mkt_mode=mkt_mode,
symbols=ws_symbols,
) as msg_stream:
quote: dict = await wait_for_live_quotes(
msg_stream=msg_stream,
symbols=ws_symbols,
# build out init msgs according to latest spec
init_msgs.append(
FeedInit(mkt_info=mkt)
)
wss_url: str = get_api_eps(client.mkt_mode)[1] # 2nd elem is wss url
# TODO: for sanity, but remove eventually Xp
if 'future' in mkt.type_key:
assert 'fstream' in wss_url
async with (
open_autorecon_ws(
url=wss_url,
fixture=partial(
subscribe,
symbols=[mkt.bs_mktid],
),
) as ws,
# avoid stream-gen closure from breaking trio..
aclosing(stream_messages(ws)) as msg_gen,
):
# log.info('WAITING ON FIRST LIVE QUOTE..')
typ, quote = await anext(msg_gen)
# pull a first quote and deliver
while typ != 'trade':
typ, quote = await anext(msg_gen)
task_status.started((init_msgs, quote))
# signal to caller feed is ready for consumption
@ -611,7 +523,7 @@ async def stream_quotes(
topic: str = mkt.bs_fqme
# start streaming
async for typ, quote in msg_stream:
async for typ, quote in msg_gen:
# period = time.time() - last
# hz = 1/period if period else float('inf')
# if hz > 60:

View File

@ -20,7 +20,6 @@ Per market data-type definitions and schemas types.
"""
from __future__ import annotations
from typing import (
ClassVar,
Literal,
)
from decimal import Decimal
@ -42,10 +41,7 @@ _spot_ws: str = 'wss://stream.binance.com/ws'
# or this one? ..
# 'wss://ws-api.binance.com:443/ws-api/v3',
# https://developers.binance.com/docs/derivatives/usds-margined-futures/
# websocket-market-streams/Important-WebSocket-Change-Notice
_futes_public_ws: str = f'wss://fstream.{_domain}/public/ws'
_futes_market_ws: str = f'wss://fstream.{_domain}/market/ws'
# https://binance-docs.github.io/apidocs/futures/en/#websocket-market-streams
_futes_ws: str = f'wss://fstream.{_domain}/ws'
_auth_futes_ws: str = 'wss://fstream-auth.{_domain}/ws'
@ -82,7 +78,7 @@ def get_api_eps(venue: MarketType) -> tuple[str, str]:
),
'usdtm_futes': (
_futes_url,
_futes_market_ws,
_futes_ws,
),
}[venue]
@ -207,8 +203,6 @@ class FutesPair(Pair):
# NOTE: see `.data._symcache.SymbologyCache.load()` for why
ns_path: str = 'piker.brokers.binance:FutesPair'
_api_url: ClassVar[str] = 'https://binance-docs.github.io/apidocs/spot/en/#exchange-information'
# NOTE: for compat with spot pairs and `MktPair.src: Asset`
# processing..
@property

View File

@ -425,7 +425,7 @@ class DataFeed:
async def stream_to_file(
watchlist_name: str,
filename: str,
portal: tractor.Portal,
portal: tractor._portal.Portal,
tickers: List[str],
brokermod: ModuleType,
rate: int,

View File

@ -23,6 +23,7 @@ from contextlib import (
asynccontextmanager as acm,
)
from datetime import datetime
from functools import partial
import time
from typing import (
Any,
@ -37,7 +38,7 @@ from rapidfuzz import process as fuzzy
import numpy as np
from tractor.trionics import (
broadcast_receiver,
maybe_open_context,
maybe_open_context
collapse_eg,
)
from tractor import to_asyncio
@ -523,12 +524,13 @@ async def maybe_open_feed_handler() -> trio.abc.ReceiveStream:
async def aio_price_feed_relay(
chan: to_asyncio.LinkedTaskChannel,
fh: FeedHandler,
instrument: Symbol,
from_trio: asyncio.Queue,
to_trio: trio.abc.SendChannel,
) -> None:
async def _trade(data: dict, receipt_timestamp):
chan.send_nowait(('trade', {
to_trio.send_nowait(('trade', {
'symbol': cb_sym_to_deribit_inst(
str_to_cb_sym(data.symbol)).lower(),
'last': data,
@ -538,7 +540,7 @@ async def aio_price_feed_relay(
}))
async def _l1(data: dict, receipt_timestamp):
chan.send_nowait(('l1', {
to_trio.send_nowait(('l1', {
'symbol': cb_sym_to_deribit_inst(
str_to_cb_sym(data.symbol)).lower(),
'ticks': [
@ -568,7 +570,7 @@ async def aio_price_feed_relay(
install_signal_handlers=False)
# sync with trio
chan.started_nowait(None)
to_trio.send_nowait(None)
await asyncio.sleep(float('inf'))
@ -579,9 +581,11 @@ async def open_price_feed(
) -> trio.abc.ReceiveStream:
async with maybe_open_feed_handler() as fh:
async with to_asyncio.open_channel_from(
aio_price_feed_relay,
fh=fh,
instrument=instrument,
partial(
aio_price_feed_relay,
fh,
instrument
)
) as (chan, first):
yield chan
@ -607,9 +611,10 @@ async def maybe_open_price_feed(
async def aio_order_feed_relay(
chan: to_asyncio.LinkedTaskChannel,
fh: FeedHandler,
instrument: Symbol,
from_trio: asyncio.Queue,
to_trio: trio.abc.SendChannel,
) -> None:
async def _fill(data: dict, receipt_timestamp):
breakpoint()
@ -632,7 +637,7 @@ async def aio_order_feed_relay(
install_signal_handlers=False)
# sync with trio
chan.started_nowait(None)
to_trio.send_nowait(None)
await asyncio.sleep(float('inf'))
@ -643,9 +648,11 @@ async def open_order_feed(
) -> trio.abc.ReceiveStream:
async with maybe_open_feed_handler() as fh:
async with to_asyncio.open_channel_from(
aio_order_feed_relay,
fh=fh,
instrument=instrument,
partial(
aio_order_feed_relay,
fh,
instrument
)
) as (chan, first):
yield chan

View File

@ -1584,72 +1584,6 @@ class MethodProxy:
self.event_table = event_table
self._aio_ns = asyncio_ns
# request-id counter for correlating each method
# call to its (eventual) response; necessary since
# the (single, shared) chan has NO other ordering
# guarantee whenever a caller task is CANCELLED
# (eg. by a search-req timeout) after sending its
# request but before consuming the response: the
# "orphaned" response otherwise gets mis-delivered
# to the next caller causing an off-by-one skew of
# every result thereafter!
self._mids = itertools.count()
self._pending: dict[
int,
tuple[trio.Event, list[dict]],
] = {}
self._request_methods: dict[int, str|None] = {}
def _deliver_method_response(
self,
msg: dict,
) -> bool:
'''
Deliver one correlated response to its waiting caller.
'''
mid: int = msg['mid']
meth: str|None = self._request_methods.pop(mid, None)
pending = self._pending.get(mid)
if pending is None:
log.warning(
f'Dropping stale method-resp,\n'
f'mid: {mid}\n'
f'meth: {meth!r}\n'
f'(caller prolly got cancelled '
f'before its resp?)\n'
)
return False
event, slot = pending
slot.append(msg)
event.set()
return True
def _track_request_method(
self,
mid: int,
meth: str|None,
) -> None:
'''
Retain bounded request metadata for stale-response logs.
'''
self._request_methods[mid] = meth
if len(self._request_methods) > 256:
oldest_mid: int = next(iter(self._request_methods))
oldest_meth: str|None = self._request_methods.pop(
oldest_mid
)
log.warning(
f'Evicting unresolved method-request metadata,\n'
f'mid: {oldest_mid}\n'
f'meth: {oldest_meth!r}\n'
f'pending metadata limit: 256\n'
)
async def _run_method(
self,
*,
@ -1662,30 +1596,38 @@ class MethodProxy:
``tractor.to_asyncio`` layer.
'''
mid: int = next(self._mids)
event = trio.Event()
slot: list[dict] = []
self._pending[mid] = event, slot
self._track_request_method(mid, meth)
try:
await self.chan.send((meth, kwargs, mid))
await event.wait()
finally:
self._pending.pop(mid, None)
chan = self.chan
await chan.send((meth, kwargs))
msg: dict = slot[0]
match msg:
# OUR method-call response B)
case {'result': res}:
while not chan.closed():
# send through method + ``kwargs: dict`` as pair
msg = await chan.receive()
# TODO: implement reconnect functionality like
# in our `.data._web_bs.NoBsWs`
# try:
# msg = await chan.receive()
# except ConnectionError:
# self.reset()
# print(f'NEXT MSG: {msg}')
# TODO: py3.10 ``match:`` syntax B)
if 'result' in msg:
res = msg.get('result')
return res
case {'exception': err}:
elif 'exception' in msg:
err = msg.get('exception')
raise err
case _:
raise RuntimeError(
f'Invalid method response for mid={mid}: {msg!r}'
)
elif 'error' in msg:
etype, emsg = msg
log.warning(f'IB error relay: {emsg}')
continue
else:
log.warning(f'UNKNOWN IB MSG: {msg}')
def status_event(
self,
@ -1738,31 +1680,22 @@ async def open_aio_client_method_relay(
log.info('asyncio `Client` method-proxy SHUTDOWN!')
break
case (meth_name, kwargs, mid):
case (meth_name, kwargs):
meth_name, kwargs = msg
meth = getattr(client, meth_name)
try:
resp = await meth(**kwargs)
# echo the msg back tagged with the
# req-id for caller-side correlation.
chan.send_nowait({
'mid': mid,
'result': resp,
})
# echo the msg back
chan.send_nowait({'result': resp})
# XXX: relay ALL (non-cancel) errors to the
# `trio`-side caller (which re-raises in the
# `MethodProxy._run_method()` frame) instead
# of crashing this relay task and thus the
# whole proxy/dialog; critical post the
# (datad|brokerd)-split where eg. contract
# qualification failures are expected to be
# caught per-request by order/warmup code!
except Exception as err:
chan.send_nowait({
'mid': mid,
'exception': err,
})
except (
RequestError,
# TODO: relay all errors to trio?
# BaseException,
) as err:
chan.send_nowait({'exception': err})
case {'error': content}:
chan.send_nowait({'exception': content})
@ -1771,73 +1704,6 @@ async def open_aio_client_method_relay(
raise ValueError(f'Unhandled msg {msg}')
async def relay_client_proxy_messages(
chan: tractor.to_asyncio.LinkedTaskChannel,
proxy: MethodProxy,
) -> None:
'''
Route one proxy's asyncio-client msgs from a single reader.
Each `open_client_proxy()` allocates its own linked channel and
starts this task in its relay nursery. Any number of Trio caller
tasks may concurrently write tagged requests through
`MethodProxy._run_method()` and wait on their per-`mid` slots.
This task alone reads the channel and dispatches each response to
its waiting caller.
Separate proxy contexts own separate channels and reader tasks;
no cross-proxy response broadcast is intended. IB status events
still reach each proxy through its own `Client.inline_errors()`
handler.
'''
try:
while not chan.closed():
# TODO: implement reconnect functionality like
# in our `.data._web_bs.NoBsWs`
# try:
msg = await chan.receive()
# except ConnectionError:
# proxy.reset()
match msg:
# Correlated response from a method call.
case {'mid': _}:
proxy._deliver_method_response(msg)
# routine (api-farm conn) status events
# relayed inline by `Client.inline_errors()`;
# not a response to a method call so just
# log at info and keep waiting.
case ('event', status_msg):
reason = status_msg['reason']
event = proxy.event_table.pop(reason, None)
if (
event
and
event.statistics().tasks_waiting
):
log.info(f'Relaying ib status message: {msg}')
event.set()
# Inline API error with no request correlation.
case ('error', emsg):
log.warning(f'IB error relay: {emsg}')
# Out-of-band client error with no method `mid`.
case {'exception': err}:
log.error(
f'Uncorrelated IB client error: {err!r}'
)
# Preserve visibility into unexpected relay traffic.
case _:
log.warning(f'UNKNOWN IB MSG: {msg}')
except trio.EndOfChannel:
log.info('IB client proxy relay closed')
@acm
async def open_client_proxy(
client: Client,
@ -1874,11 +1740,28 @@ async def open_client_proxy(
continue
setattr(proxy, name, partial(proxy._run_method, meth=name))
relay_tn.start_soon(
relay_client_proxy_messages,
chan,
proxy,
)
async def relay_events():
async with chan.subscribe() as msg_stream:
async for msg in msg_stream:
if 'event' not in msg:
continue
# if 'event' in msg:
# wake up any system event waiters.
etype, status_msg = msg
reason = status_msg['reason']
ev = proxy.event_table.pop(reason, None)
if ev and ev.statistics().tasks_waiting:
log.info(f'Relaying ib status message: {msg}')
ev.set()
continue
relay_tn.start_soon(relay_events)
yield proxy

View File

@ -231,21 +231,20 @@ async def handle_order_requests(
async def recv_trade_updates(
chan: tractor.to_asyncio.LinkedTaskChannel,
client: Client,
to_trio: trio.abc.SendChannel,
) -> None:
'''
Receive and relay order control and positioning
related events from `ib_async`, pack as tuples and
push over mem-chan to our trio relay task for
processing and relay to EMS.
Receive and relay order control and positioning related events
from `ib_async`, pack as tuples and push over mem-chan to our
trio relay task for processing and relay to EMS.
'''
client.inline_errors(chan)
client.inline_errors(to_trio)
# sync with trio task
chan.started_nowait(client.ib)
to_trio.send_nowait(client.ib)
def push_tradesies(
eventkit_obj,
@ -283,7 +282,7 @@ async def recv_trade_updates(
try:
# emit event name + relevant ibis internal objects
chan.send_nowait((event_name, emit))
to_trio.send_nowait((event_name, emit))
except trio.BrokenResourceError:
log.exception(f'Disconnected from {eventkit_obj} updates')
eventkit_obj.disconnect(push_tradesies)

View File

@ -32,12 +32,14 @@ import time
from typing import (
Any,
Callable,
TYPE_CHECKING,
)
from async_generator import aclosing
import ib_async as ibis
import numpy as np
from pendulum import (
now,
from_timestamp,
Duration,
duration as mk_duration,
@ -73,6 +75,9 @@ from ._util import (
)
from .symbols import get_mkt_info
if TYPE_CHECKING:
from trio._core._run import Task
log = get_logger(
name=__name__,
)
@ -220,11 +225,7 @@ async def open_history_client(
f'mean: {mean}'
)
# get_bars() uses None internally for an empty reply.
# The backfiller never receives that sentinel: translate
# it into NoData, or DataUnavailable at the known start
# of the instrument's history. A venue closure can also
# yield an empty reply without exhausting all history.
# could be trying to retreive bars over weekend
if out is None:
log.error(
f"No bars starting at {end_dt!r} !?!?"
@ -352,7 +353,6 @@ async def wait_on_data_reset(
tuple[
trio.CancelScope,
trio.Event,
list[bool],
]
] = trio.TASK_STATUS_IGNORED,
) -> bool:
@ -396,9 +396,8 @@ async def wait_on_data_reset(
client: Client = proxy._aio_ns
done = trio.Event()
result: list[bool] = []
with trio.move_on_after(timeout) as cs:
task_status.started((cs, done, result))
task_status.started((cs, done))
log.warning(
'Sending DATA RESET request:\n'
@ -414,7 +413,6 @@ async def wait_on_data_reset(
'NO VNC DETECTED!\n'
'Manually press ctrl-alt-f on your IB java app'
)
result.append(False)
done.set()
return False
@ -429,7 +427,6 @@ async def wait_on_data_reset(
]:
await ev.wait()
log.info(f"{name} DATA RESET")
result.append(True)
done.set()
return True
@ -438,12 +435,12 @@ async def wait_on_data_reset(
'Data reset task canceled?'
)
result.append(False)
done.set()
return False
_data_reset_lock = trio.Lock()
_data_resetter_task: Task | None = None
_failed_resets: int = 0
async def get_bars(
@ -459,6 +456,10 @@ async def get_bars(
# how long before we trigger a feed reset (seconds)
feed_reset_timeout: float = 3,
# how many days to subtract before giving up on further
# history queries for instrument, presuming that most don't
# not trade for a week XD
max_nodatas: int = 6,
max_failed_resets: int = 6,
task_status: TaskStatus[trio.CancelScope] = trio.TASK_STATUS_IGNORED,
@ -478,10 +479,8 @@ async def get_bars(
`.ib.api.Client` methods.
'''
cancelled_count: int = 0
failed_resets: int = 0
pacing_reset_pending: bool = False
pacing_reset_completed: bool = False
global _data_resetter_task, _failed_resets
nodatas_count: int = 0
data_cs: trio.CancelScope | None = None
result: tuple[
@ -494,16 +493,10 @@ async def get_bars(
async def query():
nonlocal result, data_cs
nonlocal cancelled_count, failed_resets
nonlocal pacing_reset_pending, pacing_reset_completed
global _failed_resets
nonlocal result, data_cs, end_dt, nodatas_count
while True:
if failed_resets >= max_failed_resets:
raise DataUnavailable(
f'IB history reset failed {failed_resets} '
f'times for {fqme}'
)
while _failed_resets < max_failed_resets:
try:
(
bars,
@ -535,10 +528,6 @@ async def get_bars(
)
# NOTE: REQUIRED to pass back value..
result = None
failed_resets = 0
result_ready.set()
if data_cs:
data_cs.cancel()
return None
# not enough bars signal, likely due to venue
@ -598,7 +587,6 @@ async def get_bars(
first_dt,
last_dt,
)
failed_resets = 0
# signal data reset loop parent task
result_ready.set()
@ -623,21 +611,27 @@ async def get_bars(
# the frame dt index since the upper layer may
# be doing so concurrently and we don't want to
# be delivering frames that weren't asked for.
# try to decrement start point and look further back
# end_dt = end_dt.subtract(seconds=2000)
logmsg = "SUBTRACTING DAY from DT index"
if end_dt is not None:
end_dt = end_dt.subtract(days=1)
elif end_dt is None:
end_dt = now().subtract(days=1)
log.warning(
f'NO DATA found ending @ {end_dt}\n'
+ logmsg
)
# Match get_bars()'s empty-bars return: None is
# internal to the IB adapter, not its backfill
# protocol. open_history_client() converts it
# to NoData (or DataUnavailable at history start).
# Wake the waiting get_bars() task before exiting
# so it does not mistake this reply for a timeout.
result = None
failed_resets = 0
result_ready.set()
if data_cs:
data_cs.cancel()
return None
if nodatas_count >= max_nodatas:
raise DataUnavailable(
f'Presuming {fqme} has no further history '
f'after {max_nodatas} tries..'
)
nodatas_count += 1
continue
elif (
'API historical data query cancelled'
@ -648,12 +642,6 @@ async def get_bars(
'Query cancelled by IB (:eyeroll:):\n'
f'{err.message}'
)
cancelled_count += 1
if cancelled_count >= max_failed_resets:
raise DataUnavailable(
f'IB cancelled {cancelled_count} history '
f'queries for {fqme}'
)
continue
elif (
@ -674,7 +662,6 @@ async def get_bars(
)
client = proxy._aio_ns.ib.client
pacing_reset_pending = True
# cancel any existing reset task
if data_cs:
@ -682,81 +669,69 @@ async def get_bars(
data_cs.cancel()
# spawn new data reset task
async with _data_reset_lock:
(
data_cs,
reset_done,
reset_result,
) = await tn.start(
partial(
wait_on_data_reset,
proxy,
reset_type='connection'
)
data_cs, reset_done = await tn.start(
partial(
wait_on_data_reset,
proxy,
reset_type='connection'
)
await reset_done.wait()
if reset_result[0]:
failed_resets = 0
)
if reset_done:
_failed_resets = 0
else:
failed_resets += 1
pacing_reset_pending = False
pacing_reset_completed = True
_failed_resets += 1
continue
else:
raise
# TODO: make this global across all history task/requests
# such that simultaneous symbol queries don't try data resettingn
# too fast..
unset_resetter: bool = False
async with (
tractor.trionics.collapse_eg(),
trio.open_nursery() as tn
):
# Run history until a result or bounded reset failure.
# start history request that we allow
# to run indefinitely until a result is acquired
tn.start_soon(query)
# Serialize farm resets across every history requester.
# start history reset loop which waits up to the timeout
# for a result before triggering a data feed reset.
while not result_ready.is_set():
with trio.move_on_after(feed_reset_timeout):
await result_ready.wait()
break
async with _data_reset_lock:
if result_ready.is_set():
break
if pacing_reset_completed:
pacing_reset_completed = False
continue
if _data_resetter_task:
# don't double invoke the reset hack if another
# requester task already has it covered.
continue
(
data_cs,
reset_done,
reset_result,
) = await tn.start(
partial(
wait_on_data_reset,
proxy,
reset_type='data',
)
else:
_data_resetter_task = trio.lowlevel.current_task()
unset_resetter: bool = True
# spawn new data reset task
data_cs, reset_done = await tn.start(
partial(
wait_on_data_reset,
proxy,
reset_type='data',
)
await reset_done.wait()
)
# sync wait on reset to complete
await reset_done.wait()
if (
result_ready.is_set()
or
reset_result[0]
):
failed_resets = 0
elif pacing_reset_pending:
# Pacing recovery deliberately cancelled this reset
# and will consume the retry itself.
pass
else:
failed_resets += 1
if failed_resets >= max_failed_resets:
raise DataUnavailable(
f'IB history reset failed '
f'{failed_resets} times for {fqme}'
)
_data_resetter_task = (
None
if unset_resetter
else _data_resetter_task
)
assert result
return (
result,
data_cs is not None,

View File

@ -52,7 +52,6 @@ from piker.brokers._util import (
SymbolNotFound,
BrokerError,
DataThrottle,
get_or_raise_on_pair_schema_mismatch,
)
from piker.accounting import Transaction
from piker.log import get_logger
@ -118,9 +117,6 @@ class InvalidKey(ValueError):
'''
from tractor._exceptions import reg_err_types
reg_err_types([InvalidKey])
class Client:
@ -506,16 +502,7 @@ class Client:
# NOTE: always cache in pairs tables for faster lookup
with tractor.devx.maybe_open_crash_handler(): # as bxerr:
# pair = Pair(xname=xkey, **data)
pair: Pair = get_or_raise_on_pair_schema_mismatch(
pair_type=Pair,
fields_data=dict(
xname=xkey,
**data,
),
provider_name='kraken',
# api_url='https://binance-docs.github.io/apidocs/spot/en/#exchange-information',
)
pair = Pair(xname=xkey, **data)
# register the above `Pair` structs for all
# key-sets/monikers: a set of 4 (frickin) tables

View File

@ -97,9 +97,6 @@ MsgUnion = Union[
class TooFastEdit(Exception):
'Edit requests faster then api submissions'
from tractor._exceptions import reg_err_types
reg_err_types([TooFastEdit])
# TODO: make this wrap the `Client` and `ws` instances
# and give it methods to submit cancel vs. add vs. edit

View File

@ -19,9 +19,6 @@ Symbology defs and search.
'''
from decimal import Decimal
from typing import (
ClassVar,
)
import tractor
@ -89,14 +86,9 @@ class Pair(Struct):
short_position_limit: float = 0
long_position_limit: float = float('inf')
# TODO, add API note when this was added!
execution_venue: str|None = None
# TODO: should we make this a literal NamespacePath ref?
ns_path: str = 'piker.brokers.kraken:Pair'
_api_url: ClassVar[str] = 'https://docs.kraken.com/api/docs/rest-api/get-tradable-asset-pairs'
@property
def bs_mktid(self) -> str:
'''

View File

@ -95,9 +95,6 @@ _time_frames = {
class QuestradeError(Exception):
"Non-200 OK response code"
from tractor._exceptions import reg_err_types
reg_err_types([QuestradeError])
class ContractsKey(NamedTuple):
symbol: str

View File

@ -159,11 +159,11 @@ class DarkBook(Struct):
_DEFAULT_SIZE: float = 1.0
async def _clear_dark_triggers(
async def clear_dark_triggers(
router: Router,
brokerd_orders_stream: tractor.MsgStream,
quote_stream: trionics.AsyncReceiver,
quote_stream: tractor.MsgStream,
broker: str,
fqme: str,
@ -182,6 +182,7 @@ async def _clear_dark_triggers(
# - port to the new ringbuf stuff in `tractor.ipc`!
# - numba all this!
# - this stream may eventually contain multiple symbols
quote_stream._raise_on_lag = False
async for quotes in quote_stream:
# start = time.time()
for sym, quote in quotes.items():
@ -313,32 +314,6 @@ async def _clear_dark_triggers(
# print(f'execs scan took: {time.time() - start}')
async def clear_dark_triggers(
router: Router,
brokerd_orders_stream: tractor.MsgStream,
quote_stream: tractor.MsgStream,
broker: str,
fqme: str,
book: DarkBook,
) -> None:
'''
Run dark clearing on a dedicated non-strict quote subscription.
'''
async with quote_stream.subscribe(
raise_on_lag=False,
) as quotes_stream:
await _clear_dark_triggers(
router,
brokerd_orders_stream,
quotes_stream,
broker,
fqme,
book,
)
class TradesRelay(Struct):
# for now we keep only a single connection open with
@ -810,10 +785,11 @@ async def _setup_persistent_emsd(
) -> None:
if loglevel:
get_console_log(
_log = get_console_log(
level=loglevel,
name=subsys,
)
assert _log.name == 'piker.clearing'
global _router

View File

@ -27,7 +27,7 @@ from types import ModuleType
import click
import trio
import tractor
from tractor.discovery import parse_endpoints
from tractor._multiaddr import parse_maddr
from ..log import (
get_console_log,
@ -46,27 +46,48 @@ log = get_logger('piker.cli')
def load_trans_eps(
network: dict|None = None,
maddrs: str|tuple[str, ...]|list[str]|None = None,
network: dict | None = None,
maddrs: list[tuple] | None = None,
) -> dict[str, list]:
'''
Load transport endpoints from a `[network]` config
table or CLI-provided multiaddr strings, delegating
to `tractor.discovery.parse_endpoints()`.
) -> dict[str, dict[str, dict]]:
'''
if network and not maddrs:
return parse_endpoints(network)
# transport-oriented endpoint multi-addresses
eps: dict[
str, # service name, eg. `pikerd`, `emsd`..
# libp2p style multi-addresses parsed into prot layers
list[dict[str, str | int]]
] = {}
if (
network
and
not maddrs
):
# load network section and (attempt to) connect all endpoints
# which are reachable B)
for key, maddrs in network.items():
match key:
# TODO: resolve table across multiple discov
# prots Bo
case 'resolv':
pass
case 'pikerd':
dname: str = key
for maddr in maddrs:
layers: dict = parse_maddr(maddr)
eps.setdefault(
dname,
[],
).append(layers)
elif maddrs:
if isinstance(maddrs, str):
maddrs = [maddrs]
return parse_endpoints(
{'pikerd': list(maddrs)}
)
# presume user is manually specifying the root actor ep.
eps['pikerd'] = [parse_maddr(maddr)]
return {}
return eps
@click.command()
@ -87,13 +108,13 @@ def load_trans_eps(
help='Enable tractor debug mode',
)
@click.option(
'--maddrs',
'--maddr',
'-m',
multiple=True,
default=None,
help='Multiaddrs to bind or contact',
)
def pikerd(
maddrs: tuple[str, ...],
maddr: list[str] | None,
loglevel: str,
tl: bool,
pdb: bool,
@ -124,8 +145,7 @@ def pikerd(
))
# service-actor registry endpoint socket-address set
regaddrs: list = []
tpt_bind_addrs: list|None = None
regaddrs: list[tuple[str, int]] = []
conf, _ = config.load(
conf_name='conf',
@ -133,8 +153,7 @@ def pikerd(
network: dict = conf.get('network')
if (
network is None
and
not maddrs
and not maddr
):
regaddrs = [(
_default_registry_host,
@ -142,23 +161,15 @@ def pikerd(
)]
else:
from tractor.devx import maybe_open_crash_handler
with maybe_open_crash_handler(pdb=pdb):
eps: dict[str, list] = load_trans_eps(
network,
maddrs,
)
# pikerd bind addresses (may differ from
# registry if regd is separate)
tpt_bind_addrs = eps.get('pikerd')
# registry: dedicated `regd` key, or fall
# back to pikerd addrs (pikerd IS the registry)
regaddrs = eps.get(
'regd',
tpt_bind_addrs,
)
eps: dict = load_trans_eps(
network,
maddr,
)
for layers in eps['pikerd']:
regaddrs.append((
layers['ipv4']['addr'],
layers['tcp']['port'],
))
from .. import service
@ -167,7 +178,6 @@ def pikerd(
async with (
service.open_pikerd(
registry_addrs=regaddrs,
tpt_bind_addrs=tpt_bind_addrs,
loglevel=loglevel,
debug_mode=pdb,
# enable_transports=['uds'],
@ -335,7 +345,7 @@ def services(
if not ports:
ports: list[int] = [_default_registry_port]
addr = tractor.discovery._addr.wrap_address(
addr = tractor._addr.wrap_address(
addr=(host, ports[0])
)

View File

@ -36,7 +36,6 @@ except ModuleNotFoundError:
import tomli as tomllib
from tractor._exceptions import reg_err_types
from .log import get_logger
log = get_logger('broker-config')
@ -173,12 +172,6 @@ class ConfigurationError(Exception):
class NoSignature(ConfigurationError):
'No credentials setup for broker backend!'
# auto-register for tractor IPC exc-marshalling.
reg_err_types([
ConfigurationError,
*ConfigurationError.__subclasses__(),
])
def _override_config_dir(
path: str
@ -187,48 +180,6 @@ def _override_config_dir(
_config_dir = path
def _maybe_use_test_dir() -> None:
'''
When running under the `pytest` harness, override
the config dir to the per-test-tmp dir "passed down"
the actor tree via `tractor`'s runtime-vars
inheritance mechanism.
See the `tractor_runtime_overrides` usage in our
`tests.conftest._open_test_pikerd()` as well as
`.service._actor_runtime.open_piker_runtime()` for
the root-actor's pre-loading of the var state.
NOTE: this must be checked lazily at config-path
access time (NOT import time) since sub-actors only
receive runtime-vars once their `tractor` runtime
has fully booted.
'''
global _config_dir
import tractor
actor = tractor.current_actor(
err_on_no_runtime=False,
)
if actor is None:
return
rvs: dict = tractor.runtime._state._runtime_vars
pvars: dict|None = rvs.get('piker_vars')
if (
pvars
and
(testdir := pvars.get('piker_test_dir'))
):
testdirpath = Path(testdir)
assert testdirpath.exists(), (
f'piker test harness might be borked!?\n'
f'testdirpath: {testdirpath!r}\n'
)
if _config_dir != testdirpath:
_override_config_dir(testdirpath)
def _conf_fn_w_ext(
name: str,
) -> str:
@ -242,7 +193,6 @@ def get_conf_dir() -> Path:
on the local filesystem.
'''
_maybe_use_test_dir()
return _config_dir
@ -268,7 +218,7 @@ def get_conf_path(
assert str(conf_name) in _conf_names
fn = _conf_fn_w_ext(conf_name)
return get_conf_dir() / Path(fn)
return _config_dir / Path(fn)
def repodir() -> Path:
@ -313,9 +263,8 @@ def load(
'''
# create the $HOME/.config/piker dir if dne
conf_dir: Path = get_conf_dir()
if not conf_dir.is_dir():
conf_dir.mkdir(
if not _config_dir.is_dir():
_config_dir.mkdir(
parents=True,
exist_ok=True,
)

View File

@ -194,24 +194,15 @@ async def _reconnect_forever(
'''
# after so many msg recv timeouts, reset the connection
timeouts: int = 0
first_message: bool = True
while True:
waiting_on: str = 'websocket receive'
timeout: float = nobsws._timeout
with trio.move_on_after(
# can be dynamically changed by user code
timeout,
nobsws._timeout,
) as cs:
try:
msg: Any = await ws.get_message()
waiting_on = 'local relay send'
await snd.send(msg)
if first_message:
log.info(
f'Websocket first message relayed: {url!r}'
)
first_message = False
except nobsws.recon_errors:
log.exception(
f'{src_mod}\n'
@ -227,17 +218,10 @@ async def _reconnect_forever(
if cs.cancelled_caught:
timeouts += 1
log.warning(
f'Websocket timeout waiting on {waiting_on}\n'
f'url: {url!r}\n'
f'timeout: {timeout}s; count: {timeouts}; '
f'reset after: {reset_after}\n'
)
if timeouts > reset_after:
log.error(
f'{src_mod}\n'
f'WS feed seems down and slow af.. reconnecting\n'
f'{url!r}\n'
'WS feed seems down and slow af.. reconnecting\n'
)
rent_cs.cancel()
@ -277,8 +261,8 @@ async def _reconnect_forever(
ws: WebSocketConnection
try:
async with (
tractor.trionics.collapse_eg(),
open_websocket_url(url) as ws,
tractor.trionics.collapse_eg(),
trio.open_nursery() as tn,
):
cs = nobsws._cs = tn.cancel_scope
@ -311,38 +295,24 @@ async def _reconnect_forever(
# indicate to wrapper / opener that we are up and block
# to let tasks run **inside** the ws open block above.
log.info(f'Websocket ready: {url!r}')
nobsws._connected.set()
await trio.sleep_forever()
except nobsws.recon_errors:
# The websocket reader can fail while its context exits,
# outside proxy_msgs() (e.g. Pong during LOCAL_CLOSING).
# Collapse its nursery group before classifying the error.
nobsws._connected = trio.Event()
nobsws._ws = None
# Unlike the old outer wrapper suppression, restart the
# connection. This checkpoint also honors parent shutdown;
# never shield a reconnect retry from cancellation.
log.exception(f'Retrying connection: {url!r}')
except (
HandshakeError,
ConnectionRejected,
):
log.exception('Retrying connection')
await trio.sleep(0.5) # throttle
continue
# ``trio.Cancelled`` controls task-tree teardown; it is not a
# reconnect failure. Let it escape before ``BaseException``
# so nursery cancellation never enters the shielded 200ms
# error throttle. Audited by
# ``test_reconnect_cancellation_bypasses_error_throttle()``.
except trio.Cancelled:
raise
except BaseException:
except BaseException as _berr:
berr = _berr
log.exception(
'Reconnect-attempt failed ??\n'
)
with trio.CancelScope(shield=True):
await trio.sleep(0.2) # throttle
raise
raise berr
#|_ws & nursery block ends
nobsws._connected = trio.Event()
@ -372,7 +342,6 @@ async def open_autorecon_ws(
url: str,
fixture: AsyncContextManager | None = None,
connect_timeout: float | None = None,
# time in sec between msgs received before
# we presume connection might need a reset.
@ -422,19 +391,7 @@ async def open_autorecon_ws(
reset_after=reset_after,
)
)
try:
if connect_timeout is None:
await nobsws._connected.wait()
else:
with trio.fail_after(connect_timeout):
await nobsws._connected.wait()
except trio.TooSlowError as err:
raise TimeoutError(
f'Websocket connection timed out after '
f'{connect_timeout}s: {url}'
) from err
await nobsws._connected.wait()
assert nobsws._cs
assert nobsws.connected()
try:
@ -447,7 +404,6 @@ async def open_autorecon_ws(
f'Entire ws-channel disconnect due to,\n'
f'con_err: {con_err!r}\n'
)
raise
'''

View File

@ -30,7 +30,6 @@ from collections import (
defaultdict,
abc,
)
from copy import copy
from contextlib import asynccontextmanager as acm
from functools import partial
import time
@ -78,7 +77,7 @@ from ._sampling import (
if TYPE_CHECKING:
from .flows import Flume
from tractor.discovery._addr import Address
from tractor._addr import Address
from tractor.msg.types import Aid
@ -485,15 +484,14 @@ async def open_feed_bus(
if loglevel is None:
loglevel: str = tractor.current_actor().loglevel
# Configure the shared piker.data logger used by ._util.log,
# including websocket tasks. A per-client verbosity must not
# demote actor-wide transport diagnostics below daemon verbosity.
# XXX: required to propagate ``tractor`` loglevel to piker
# logging
get_console_log(
level=(tractor.current_actor().loglevel
level=(loglevel
or
loglevel
tractor.current_actor().loglevel
),
name=log.name,
name=__name__,
)
# local state sanity checks
@ -676,36 +674,6 @@ async def open_feed_bus(
bus.remove_subs(bs_fqme, subs)
async def relay_to_common_memchan(
stream: (
tractor.MsgStream
|trionics.BroadcastReceiver
),
tx: trio.MemorySendChannel[dict[str, Any]],
task_status: TaskStatus[
trionics.BroadcastReceiver,
] = trio.TASK_STATUS_IGNORED,
) -> None:
'''
Relay one provider subscription into a common memory channel.
Deliver the live child receiver through `task_status` so the
caller can retain and inspect each provider relay's cursor.
'''
bstream: trionics.BroadcastReceiver
async with (
stream.subscribe() as bstream,
tx,
):
task_status.started(bstream)
msg: dict[str, Any]
async for msg in bstream:
await tx.send(msg)
class Feed(Struct):
'''
A per-provider API for client-side consumption from real-time data
@ -755,51 +723,29 @@ class Feed(Struct):
if len(mods) == 1:
# just pass the brokerd stream directly if only one provider
# was detected.
stream: (
tractor.MsgStream
|trionics.BroadcastReceiver
) = self.streams[list(brokers)[0]]
bstream: trionics.BroadcastReceiver
stream = self.streams[list(brokers)[0]]
async with stream.subscribe() as bstream:
yield bstream
return
# start multiplexing task tree
tx: trio.MemorySendChannel[dict[str, Any]]
rx: trio.MemoryReceiveChannel[dict[str, Any]]
tx, rx = trio.open_memory_channel(616)
async def relay_to_common_memchan(stream: tractor.MsgStream):
async with tx:
async for msg in stream:
await tx.send(msg)
async with (
tractor.trionics.collapse_eg(),
trio.open_nursery() as nurse
):
# spawn a relay task for each stream so that they all
# multiplex to a common channel.
broker2bstreams: dict[
str,
trionics.BroadcastReceiver,
] = {}
broker: str
for broker in mods:
stream: (
tractor.MsgStream
|trionics.BroadcastReceiver
) = self.streams[broker]
for brokername in mods:
stream = self.streams[brokername]
nurse.start_soon(relay_to_common_memchan, stream)
relay_tx: trio.MemorySendChannel[
dict[str, Any]
] = tx.clone()
bstream: trionics.BroadcastReceiver = await nurse.start(
relay_to_common_memchan,
stream,
relay_tx,
)
broker2bstreams[broker] = bstream
# Each relay now owns one `tx` clone. Closing the original
# lets `rx` reach EOC only after every provider relay exits.
await tx.aclose()
try:
yield rx
finally:
@ -808,23 +754,10 @@ class Feed(Struct):
_max_sample_rate: int = 1
async def pause(self) -> None:
'''
Pause the shared remote provider context for this cached feed.
This controls provider publication for every local cache user;
it does not pause only this caller's receive cursor.
'''
stream: tractor.MsgStream|trionics.BroadcastReceiver
for stream in set(self.streams.values()):
await stream.send('pause')
async def resume(self) -> None:
'''
Resume the shared remote provider context for this cached feed.
'''
stream: tractor.MsgStream|trionics.BroadcastReceiver
for stream in set(self.streams.values()):
await stream.send('resume')
@ -884,17 +817,7 @@ async def maybe_open_feed(
in a tractor broadcast receiver.
'''
cache_key: tuple[
tuple[str, ...],
float|None,
bool,
bool,
] = (
tuple(sorted(fqmes)),
kwargs.get('tick_throttle'),
kwargs.get('allow_overruns', True),
kwargs.get('start_stream', True),
)
fqme = fqmes[0]
async with trionics.maybe_open_context(
acm_func=open_feed,
@ -907,73 +830,27 @@ async def maybe_open_feed(
'allow_overruns': kwargs.get('allow_overruns', True),
'start_stream': kwargs.get('start_stream', True),
},
key=cache_key,
key=fqme,
) as (cache_hit, feed):
feed: Feed
if (
cache_hit
and
log.at_least_level('info')
):
log.info(
f'Using cached feed for key:\n'
f'{cache_key!r}'
)
if cache_hit:
log.info(f'Using cached feed for {fqme}')
# add a new broadcast subscription for the quote stream
# if this feed is likely already in use
# The cached `Feed` exclusively owns provider root streams.
# Every caller, including the first cache owner, gets a local
# child cursor and copied descriptors for its lexical lifetime.
broker_names: tuple[str, ...] = tuple(feed.streams)
bstreams: tuple[
trionics.BroadcastReceiver,
...,
]
async with trionics.gather_contexts(
mngrs=[
feed.streams[broker].subscribe()
for broker in broker_names
]
) as bstreams:
broker2bstreams: dict[
str,
trionics.BroadcastReceiver,
] = dict(zip(
broker_names,
bstreams,
strict=True,
))
flumes: dict[str, Flume] = {}
fqme: str
flume: Flume
for fqme, flume in feed.flumes.items():
broker: str = flume.mkt.broker
async with trionics.gather_contexts(
mngrs=[stream.subscribe() for stream in feed.streams.values()]
) as bstreams:
for bstream, flume in zip(bstreams, feed.flumes.values()):
# XXX: TODO: horrible hackery that needs fixing..
# i guess we have to create context proxies?
bstream._ctx = flume.stream._ctx
flume.stream = bstream
bstream: trionics.BroadcastReceiver = (
broker2bstreams[broker]
)
# XXX: TODO: horrible hackery that needs fixing..
# i guess we have to create context proxies?
root_stream: tractor.MsgStream = feed.streams[broker]
ctx: tractor.Context = root_stream.ctx
bstream._ctx = ctx
local_flume: Flume = copy(flume)
local_flume.stream = bstream
flumes[fqme] = local_flume
# `feed` is the cache-owned resource and must retain only
# provider root streams. A shallow `Feed` copy gives this
# caller its own descriptor mappings while intentionally
# sharing immutable market metadata, SHM handles and
# provider controls for the cache resource's lifetime.
local_feed: Feed = copy(feed)
local_feed.flumes = flumes
local_feed.streams = broker2bstreams
yield local_feed
yield feed
else:
yield feed
@acm

View File

@ -34,7 +34,6 @@ from piker.accounting import (
Asset,
MktPair,
)
from tractor._exceptions import reg_err_types
from ._util import log
@ -44,8 +43,6 @@ class FeedInitializationError(ValueError):
'''
reg_err_types([FeedInitializationError])
class FeedInit(Struct, frozen=True):
'''

View File

@ -36,15 +36,6 @@ from pygments import (
_proj_name: str = 'piker'
def _relative_name(
name: str|None,
) -> str|None:
if name:
return name.removeprefix(f'{_proj_name}.')
return None
def get_logger(
name: str|None = None,
**tractor_log_kwargs,
@ -56,9 +47,17 @@ def get_logger(
See `tractor.log.get_logger()` for details.
'''
pkg_name: str = _proj_name
if (
name
and
pkg_name in name
):
name: str = name.lstrip(f'{_proj_name}.')
return tractor.log.get_logger(
name=_relative_name(name),
pkg_name=_proj_name,
name=name,
pkg_name=pkg_name,
**tractor_log_kwargs,
)
@ -82,6 +81,14 @@ def get_console_log(
You do it.. Bp
'''
pkg_name: str = _proj_name
if (
name
and
pkg_name in name
):
name: str = name.lstrip(f'{_proj_name}.')
tll: str|None = None
if (
with_tractor_log is not False
@ -104,8 +111,8 @@ def get_console_log(
return tractor.log.get_console_log(
level=level,
name=_relative_name(name),
pkg_name=_proj_name,
name=name,
pkg_name=pkg_name,
**tractor_log_kwargs,
)

View File

@ -44,7 +44,6 @@ from ._registry import ( # noqa
_tractor_kwargs,
_default_reg_addr,
open_registry,
RegistryAddress,
)
@ -59,8 +58,7 @@ def get_runtime_vars() -> dict[str, Any]:
@acm
async def open_piker_runtime(
name: str,
registry_addrs: list[RegistryAddress] = [],
tpt_bind_addrs: list|None = None,
registry_addrs: list[tuple[str, int]] = [],
enable_modules: list[str] = [],
loglevel: str|None = None,
@ -78,7 +76,7 @@ async def open_piker_runtime(
) -> tuple[
tractor.Actor,
list[RegistryAddress],
list[tuple[str, int]],
]:
'''
Start a piker actor who's runtime will automatically sync with
@ -93,7 +91,7 @@ async def open_piker_runtime(
try:
actor = tractor.current_actor()
except tractor._exceptions.NoRuntime:
tractor.runtime._state._runtime_vars[
tractor._state._runtime_vars[
'piker_vars'
] = tractor_runtime_overrides
@ -114,7 +112,6 @@ async def open_piker_runtime(
# passed through to `open_root_actor`
registry_addrs=registry_addrs,
tpt_bind_addrs=tpt_bind_addrs,
name=name,
start_method=start_method,
loglevel=loglevel,
@ -168,8 +165,7 @@ _root_modules: list[str] = [
@acm
async def open_pikerd(
registry_addrs: list[RegistryAddress],
tpt_bind_addrs: list|None = None,
registry_addrs: list[tuple[str, int]],
loglevel: str|None = None,
# XXX: you should pretty much never want debug mode
@ -202,7 +198,6 @@ async def open_pikerd(
loglevel=loglevel,
debug_mode=debug_mode,
registry_addrs=registry_addrs,
tpt_bind_addrs=tpt_bind_addrs,
**kwargs,
@ -214,44 +209,8 @@ async def open_pikerd(
tractor.trionics.collapse_eg(),
trio.open_nursery() as service_tn,
):
# `reg_addrs` locates the registrar used for actor discovery;
# `tpt_bind_addrs` requests this daemon's own IPC listeners.
# An existing registrar can live elsewhere, so its endpoints
# must not be checked against `root_actor.accept_addrs`.
bind_addrs = tpt_bind_addrs
if bind_addrs is None:
# Only a registrar actor owns the discovery endpoints.
# A client with no explicit bind request lets Tractor pick
# its listener; there is no caller-specified bind to check.
bind_addrs = (
reg_addrs if root_actor.is_registrar else []
)
# `root_actor.accept_addrs` contains the actual bound endpoints.
# Normalize both sides with Tractor's address API so legacy
# pairs and tagged transport tuples compare in the same form.
accept_addrs = [
tractor.discovery._addr.wrap_address(addr).unwrap()
for addr in root_actor.accept_addrs
]
for addr in bind_addrs:
# normalize to a wrapped `tractor` addr-type;
# entries may be raw `tuple`s when passed in
# from (test) client code.
wladdr = tractor.discovery._addr.wrap_address(
addr,
)
uaddr: tuple = wladdr.unwrap()
bound = uaddr in accept_addrs
if wladdr.proto_key == 'tcp' and uaddr[-1] == 0:
# TCP port zero asks the OS for an ephemeral listener.
# Its actual port differs from the request, but the
# tagged transport and host must still match exactly.
bound = any(
actual[:-1] == uaddr[:-1]
for actual in accept_addrs
)
if not bound:
for addr in reg_addrs:
if addr not in root_actor.accept_addrs:
raise RuntimeError(
f'`pikerd` failed to bind on {addr}!\n'
'Maybe you have another daemon already running?'
@ -299,13 +258,13 @@ async def open_pikerd(
@acm
async def maybe_open_pikerd(
registry_addrs: list[RegistryAddress] | None = None,
registry_addrs: list[tuple[str, int]] | None = None,
loglevel: str | None = None,
**kwargs,
) -> (
tractor.Portal
tractor._portal.Portal
|ClassVar[Services]
):
'''
@ -333,7 +292,7 @@ async def maybe_open_pikerd(
# async with open_portal(chan) as arb_portal:
# yield arb_portal
registry_addrs: list[RegistryAddress] = (
registry_addrs: list[tuple[str, int]] = (
registry_addrs
or
[_default_reg_addr]

View File

@ -49,7 +49,6 @@ from requests.exceptions import (
ReadTimeout,
)
from tractor._exceptions import reg_err_types
from piker.log import (
get_console_log,
get_logger,
@ -67,11 +66,6 @@ class DockerNotStarted(Exception):
class ApplicationLogError(Exception):
'App in container reported an error in logs'
reg_err_types([
DockerNotStarted,
ApplicationLogError,
])
@acm
async def open_docker(

View File

@ -72,6 +72,7 @@ async def maybe_spawn_daemon(
level=loglevel,
name=__name__,
)
assert log.name == 'piker.service'
# serialize access to this section to avoid
# 2 or more tasks racing to create a daemon
@ -79,21 +80,9 @@ async def maybe_spawn_daemon(
await lock.acquire()
try:
# Discovery and `maybe_open_pikerd()` must use the same
# registrar list, independently of other runtime options.
# Consume an override here: startup forwards it explicitly
# below, so leaving it in `pikerd_kwargs` duplicates the keyword.
registry_addrs = pikerd_kwargs.pop('registry_addrs', None)
if registry_addrs is None:
# Use the registrar addresses inherited by this actor.
rtvs: dict = tractor.get_runtime_vars()
registry_addrs: list[tuple] = list(
map(tuple, rtvs['_registry_addrs'])
)
async with find_service(
service_name,
registry_addrs=registry_addrs,
registry_addrs=[('127.0.0.1', 6116)],
) as portal:
if portal is not None:
lock.release()
@ -110,7 +99,6 @@ async def maybe_spawn_daemon(
# process tree
async with maybe_open_pikerd(
loglevel=loglevel,
registry_addrs=registry_addrs,
**pikerd_kwargs,
) as pikerd_portal:
@ -154,65 +142,7 @@ async def maybe_spawn_daemon(
async with tractor.wait_for_actor(service_name) as portal:
lock.release()
yield portal
# --- ---- ---
# XXX NOTE XXX
# --- ---- ---
# DO NOT PUT A `portal.cancel_actor()` here (as was prior)!
#
# Doing so will cause an "out-of-band" ctxc
# (`tractor.ContextCancelled`) to be raised inside the
# `ServiceMngr.open_context_in_task()`'s call to
# `ctx.wait_for_result()` AND the internal self-ctxc
# "graceful capture" WILL NOT CATCH IT!
#
# This can cause certain types of operations to raise
# that ctxc BEFORE THEY `return`, resulting in
# a "false-negative" ctxc being raised when really
# nothing actually failed, other then our semantic
# "failure" to suppress an expected, graceful,
# self-cancel scenario..
#
# bUt wHy duZ It WorK lIKe dis..
# ------------------------------
# from the perspective of the `tractor.Context` this
# cancel request was conducted "out of band" since
# `Context.cancel()` was never called and thus the
# `._cancel_called: bool` was never set. Despite the
# remote `.canceller` being set to `pikerd` (i.e. the
# same `Actor.uid` of the raising service-mngr task) the
# service-task's ctx itself was never marked as having
# requested cancellation and thus still raises the ctxc
# bc it was unaware of any such request.
#
# How to make grokin these cases easier tho?
# ------------------------------------------
# Because `Portal.cancel_actor()` was called it requests
# "full-`Actor`-runtime-cancellation" of it's peer
# process which IS NOT THE SAME as a single inter-actor
# RPC task cancelling its local context with a remote
# peer `Task` in that same peer process.
#
# ?TODO? It might be better if we do one (or all) of the
# following:
#
# -[ ] at least set a special message for the
# `ContextCancelled` when raised locally by the
# unaware ctx task such that we check for the
# `.canceller` being *our `Actor`* and in the case
# where `Context._cancel_called == False` we specially
# note that this is likely an "out-of-band"
# runtime-cancel request triggered by some call to
# `Portal.cancel_actor()`, possibly even reporting the
# exact LOC of that caller by tracking it inside our
# portal-type?
# -[ ] possibly add another field `ContextCancelled` like
# maybe a,
# `.request_type: Literal['os', 'proc', 'actor',
# 'ctx']` type thing which would allow immediately
# being able to tell what kind of cancellation caused
# the unexpected ctxc?
# -[ ] REMOVE THIS COMMENT, once we've settled on how to
# better augment `tractor` to be more explicit on this!
await portal.cancel_actor()
except BaseException as _err:
err = _err
@ -222,13 +152,11 @@ async def maybe_spawn_daemon(
lock.statistics().owner is current_task()
):
log.exception(
f'Releasing stale lock after crash..?\n'
f'\n'
f'Releasing stale lock after crash..?'
f'{err!r}\n'
)
lock.release()
raise
raise err
async def spawn_emsd(

View File

@ -48,7 +48,7 @@ log = get_logger(name=__name__)
# new actors and supervises them to completion?
class Services:
actor_n: tractor.ActorNursery
actor_n: tractor._supervise.ActorNursery
service_n: trio.Nursery
debug_mode: bool # tractor sub-actor debug mode flag
service_tasks: dict[

View File

@ -24,7 +24,6 @@ from contextlib import (
)
from typing import (
Any,
TypeAlias,
)
import tractor
@ -33,23 +32,11 @@ from tractor import (
Actor,
Portal,
)
from tractor.discovery._addr import (
LegacyUnwrappedAddress,
UnwrappedAddress,
wrap_address,
)
from piker.log import get_logger
log = get_logger(name=__name__)
# Piker config still accepts legacy socket pairs while tractor now
# emits tagged `UnwrappedAddress` values.
RegistryAddress: TypeAlias = (
LegacyUnwrappedAddress
|UnwrappedAddress
)
# TODO? default path-space for UDS registry?
# [ ] needs to be Xplatform tho!
# _default_registry_path: Path = (
@ -59,7 +46,10 @@ RegistryAddress: TypeAlias = (
_default_registry_host: str = '127.0.0.1'
_default_registry_port: int = 6116
_default_reg_addr: RegistryAddress = (
_default_reg_addr: tuple[
str,
int, # |str TODO, once we support UDS, see above.
] = (
_default_registry_host,
_default_registry_port,
)
@ -73,37 +63,24 @@ _registry: Registry | None = None
class Registry:
# TODO: should this be a set or should we complain
# on duplicates?
addrs: list[RegistryAddress] = []
addrs: list[tuple[str, int]] = []
# TODO: table of uids to sockaddrs
peers: dict[
tuple[str, str],
RegistryAddress,
tuple[str, int],
] = {}
_tractor_kwargs: dict[str, Any] = {}
def _normalize_addresses(
addrs: list[RegistryAddress],
) -> list[RegistryAddress]:
'''
Normalize legacy and tagged addresses through tractor's codec.
'''
return [
wrap_address(addr).unwrap()
for addr in addrs
]
@acm
async def open_registry(
addrs: list[RegistryAddress],
addrs: list[tuple[str, int]],
ensure_exists: bool = True,
) -> list[RegistryAddress]:
) -> list[tuple[str, int]]:
'''
Open the service-actor-discovery registry by returning a set of
tranport socket-addrs to registrar actors which may be
@ -115,12 +92,9 @@ async def open_registry(
actor: Actor = tractor.current_actor()
aid: msg.Aid = actor.aid
uid: tuple[str, str] = aid.uid
addrs = _normalize_addresses(addrs)
preset_reg_addrs: list[RegistryAddress] = (
_normalize_addresses(Registry.addrs)
)
if preset_reg_addrs:
Registry.addrs = preset_reg_addrs
preset_reg_addrs: list[
tuple[str, int]
] = Registry.addrs
if (
preset_reg_addrs
and
@ -128,10 +102,9 @@ async def open_registry(
):
if preset_reg_addrs != addrs:
# if any(addr in preset_reg_addrs for addr in addrs):
diff: set[RegistryAddress] = (
set(preset_reg_addrs)
-set(addrs)
)
diff: set[
tuple[str, int]
] = set(preset_reg_addrs) - set(addrs)
if diff:
log.warning(
f'`{uid}` requested only subset of registrars: {addrs}\n'
@ -150,9 +123,7 @@ async def open_registry(
and
not Registry.addrs
):
Registry.addrs.extend(
_normalize_addresses(actor.reg_addrs)
)
Registry.addrs.extend(actor.reg_addrs)
if (
ensure_exists
@ -168,13 +139,13 @@ async def open_registry(
Registry.addrs = (
addrs
or
_normalize_addresses([_default_reg_addr])
[_default_reg_addr]
)
# NOTE: only spot this seems currently used is inside
# `.ui._exec` which is the (eventual qtloops) bootstrapping
# with guest mode.
reg_addrs: list[RegistryAddress] = Registry.addrs
reg_addrs: list[tuple[str, str|int]] = Registry.addrs
# !TODO, a struct-API to stringently allow this only in special
# cases?
# -> better would be to have some way to (atomically) rewrite
@ -188,13 +159,13 @@ async def open_registry(
# next (set of) calls will apply whatever new one is passed
# in.
if was_set:
Registry.addrs = []
Registry.addrs = None
@acm
async def find_service(
service_name: str,
registry_addrs: list[RegistryAddress] | None = None,
registry_addrs: list[tuple[str, int]] | None = None,
first_only: bool = True,
@ -204,7 +175,7 @@ async def find_service(
| None
):
# try:
reg_addrs: list[RegistryAddress]
reg_addrs: list[tuple[str, int|str]]
async with open_registry(
addrs=(
registry_addrs
@ -248,19 +219,16 @@ async def find_service(
async def check_for_service(
service_name: str,
) -> None|RegistryAddress:
) -> None|tuple[str, int]:
'''
Service daemon "liveness" predicate.
'''
async with (
open_registry(
addrs=Registry.addrs,
ensure_exists=False,
) as reg_addrs,
open_registry(ensure_exists=False) as reg_addr,
tractor.query_actor(
service_name,
regaddr=reg_addrs[0],
) as (sockaddr, _),
arbiter_sockaddr=reg_addr,
) as sockaddr,
):
return sockaddr

View File

@ -382,9 +382,6 @@ def quote_to_marketstore_structarray(
class MarketStoreError(Exception):
"Generic marketstore client error"
from tractor._exceptions import reg_err_types
reg_err_types([MarketStoreError])
# def err_on_resp(response: dict) -> None:
# """Raise any errors found in responses from client request.

View File

@ -151,13 +151,6 @@ class StorageConnectionError(ConnectionError):
'''
from tractor._exceptions import reg_err_types
reg_err_types([
TimeseriesNotFound,
StorageConnectionError,
])
def get_storagemod(
name: str,

View File

@ -301,7 +301,7 @@ def ldshm(
# TODO: call null-seg fixer somehow?
if null_segs:
if tractor.runtime._state.is_debug_mode():
if tractor._state.is_debug_mode():
await tractor.pause()
# async with (
# trio.open_nursery() as tn,

View File

@ -276,8 +276,15 @@ def get_null_segs(
absi_zdiff: np.ndarray = np.diff(absi_zeros)
if zero_t.size < 2:
# Keep the legacy single-null result without a REPL stop.
# Full null grouping is handled downstream in 976db6c3.
try:
breakpoint()
except RuntimeError:
# XXX, if greenback not active from
# piker store ldshm cmd..
log.exception(
"Can't debug single-sample null!\n"
)
return None
# scan for all frame-indices where the

View File

@ -168,7 +168,7 @@ async def markup_gaps(
prev_r: pl.DataFrame = prev_row_by_i
# debug any missing pre-row
if tractor.runtime._state.is_debug_mode():
if tractor._state.is_debug_mode():
await tractor.pause()
istart: int = prev_r['index'][0]

View File

@ -22,10 +22,7 @@ import os
import click
import tractor
from ..cli import (
cli,
load_trans_eps,
)
from ..cli import cli
from .. import watchlists as wl
from ..service import maybe_spawn_brokerd
@ -162,63 +159,67 @@ def chart(
needed if not discovered via [network] config.
'''
# eg. ``--profile 3`` reports profiling for anything slower then 3 ms.
if profile is not None:
from .. import _profile
_profile._pg_profile = True
_profile.ms_slower_then = float(profile)
# Qt UI entrypoint
from ._app import _main
for symbol in symbols:
if '.' not in symbol:
click.echo(click.style(
f'symbol: {symbol} must have a {symbol}.<provider> suffix',
fg='red',
))
return
# global opts
# brokernames: list[str] = config['brokers']
brokermods = config['brokermods']
assert brokermods
tractorloglevel = config['tractorloglevel']
pikerloglevel = config['loglevel']
maddrs: list[tuple[str, int]] = config.get(
'maddrs',
[],
)
# if maddrs:
# from tractor._multiaddr import parse_maddr
# for addr in maddrs:
# breakpoint()
# layers: dict = parse_maddr(addr)
regaddrs: list[tuple[str, int]] = config.get(
'registry_addrs',
[],
)
from ..config import load
conf, _ = load(
conf_name='conf',
)
network: dict = conf.get('network')
if network:
from ..cli import load_trans_eps
eps: dict = load_trans_eps(
network,
maddrs,
)
for layers in eps['pikerd']:
regaddrs.append((
layers['ipv4']['addr'],
layers['tcp']['port'],
))
# breakpoint()
from tractor.devx import maybe_open_crash_handler
pdb: bool = config['pdb']
with maybe_open_crash_handler(pdb=pdb):
# eg. ``--profile 3`` reports profiling for anything slower then 3 ms.
if profile is not None:
from .. import _profile
_profile._pg_profile = True
_profile.ms_slower_then = float(profile)
# Qt UI entrypoint
from ._app import _main
for symbol in symbols:
if '.' not in symbol:
click.echo(click.style(
f'symbol: {symbol} must have a {symbol}.<provider> suffix',
fg='red',
))
return
# global opts
# brokernames: list[str] = config['brokers']
brokermods = config['brokermods']
assert brokermods
tractorloglevel = config['tractorloglevel']
pikerloglevel = config['loglevel']
# !TODO, allow declaring bind addr via maddr!
from ..config import load
conf, _ = load(
conf_name='conf',
)
regaddrs: list = config.get(
'registry_addrs',
[],
)
chart_eps: list|None = None
network: dict = conf.get('network')
if network:
eps: dict = load_trans_eps(
network,
maddrs=None,
)
# registry addrs: prefer `regd`, fall back
# to `pikerd` eps
reg_eps = eps.get(
'regd',
eps.get('pikerd')
)
assert reg_eps
regaddrs = [
addr.unwrap() for addr in reg_eps
]
chart_eps = eps.get('chart')
_main(
syms=symbols,
brokermods=brokermods,
@ -228,7 +229,6 @@ def chart(
'loglevel': tractorloglevel,
'name': 'chart',
'registry_addrs': list(set(regaddrs)),
'tpt_bind_addrs': chart_eps,
'enable_modules': [
# remote data-view annotations Bo

View File

@ -167,7 +167,7 @@ async def stream_symbol_selection():
async def _async_main(
name: str,
portal: tractor.Portal,
portal: tractor._portal.Portal,
symbols: List[str],
brokermod: ModuleType,
loglevel: str = 'info',

View File

@ -436,7 +436,7 @@ class OptionChain(object):
async def new_chain_ui(
portal: tractor.Portal,
portal: tractor._portal.Portal,
symbol: str,
brokermod: types.ModuleType,
rate: int = 1,

View File

@ -23,7 +23,7 @@ name = "piker"
version = "0.1.0a0dev0"
description = "trading gear for hackers"
authors = [{ name = "Tyler Goodlet", email = "goodboy_foss@protonmail.com" }]
requires-python = ">=3.13, <3.14"
requires-python = ">=3.12, <3.14"
license = "AGPL-3.0-or-later"
readme = "README.rst"
keywords = [
@ -39,6 +39,7 @@ classifiers = [
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Intended Audience :: Financial and Insurance Industry",
"Intended Audience :: Science/Research",
@ -52,7 +53,7 @@ dependencies = [
"colorama >=0.4.6, <0.5.0",
"colorlog >=6.7.0, <7.0.0",
"numpy>=2.0",
"polars >=0.20.6, <2.0.0",
"polars >=0.20.6",
"polars-fuzzy-match>=0.1.5",
"pygments >=2.16.1, <3.0.0",
"rich >=13.5.2, <14.0.0",
@ -67,7 +68,7 @@ dependencies = [
"cryptofeed >=2.4.0, <3.0.0",
"pyarrow>=18.0.0",
"websockets ==12.0",
"msgspec>=0.20.0",
"msgspec>=0.19.0,<0.20",
"tractor",
"tomlkit",
"trio-typing>=0.10.0",
@ -131,7 +132,7 @@ repl = [
"greenback >=1.1.1, <2.0.0",
# @goodboy's preferred console toolz
"xonsh>=0.24.2",
"xonsh>=0.22.2",
"prompt-toolkit ==3.0.40",
"pyperclip>=1.9.0",
@ -202,7 +203,7 @@ pyvnc = { git = "https://github.com/regulad/pyvnc.git" }
# xonsh = { git = 'https://github.com/xonsh/xonsh.git', branch = 'main' }
# XXX since, we're like, always hacking new shite all-the-time. Bp
tractor = { git = "https://github.com/goodboy/tractor.git", branch = "piker_pin" }
tractor = { git = "https://github.com/goodboy/tractor.git", branch ="main" }
# tractor = { git = "https://pikers.dev/goodboy/tractor", branch = "piker_pin" }
# ------ goodboy ------
# hackin dev-envs, usually there's something new he's hackin in..

View File

@ -22,6 +22,8 @@ pytest_plugins: tuple[str] = (
def pytest_addoption(parser):
parser.addoption("--ll", action="store", dest='loglevel',
default=None, help="logging level to set when testing")
parser.addoption("--confdir", default=None,
help="Use a practice API account")
@ -90,7 +92,8 @@ def log(
@acm
async def _open_test_pikerd(
tmpconfdir: str,
reg_addr: tuple[str, int|str],
reg_addr: tuple[str, int] | None = None,
loglevel: str = 'warning',
debug_mode: bool = False,
@ -110,10 +113,16 @@ async def _open_test_pikerd(
to boot the root actor / tractor runtime.
'''
import random
from piker.service import maybe_open_pikerd
if reg_addr is None:
port = random.randint(6e3, 7e3)
reg_addr = ('127.0.0.1', port)
async with (
maybe_open_pikerd(
registry_addrs=[reg_addr],
registry_addr=reg_addr,
loglevel=loglevel,
tractor_runtime_overrides={
@ -130,19 +139,13 @@ async def _open_test_pikerd(
async with tractor.wait_for_actor(
'pikerd',
registry_addr=reg_addr,
arbiter_sockaddr=reg_addr,
) as portal:
raddr = portal.chan.raddr
uw_raddr: tuple = raddr.unwrap()
canonical_addr: tuple = (
tractor.discovery._addr.wrap_address(
reg_addr,
).unwrap()
)
assert uw_raddr == canonical_addr
raddr = portal.channel.raddr
assert raddr == reg_addr
yield (
raddr._host,
raddr._port,
raddr[0],
raddr[1],
portal,
service_manager,
)
@ -199,10 +202,7 @@ def open_test_pikerd(
request: pytest.FixtureRequest,
tmp_path: Path,
tmpconfdir: Path,
# XXX from `tractor._testing.pytest` plugin
loglevel: str,
reg_addr: tuple,
):
tmpconfdir_str: str = str(tmpconfdir)
@ -236,13 +236,10 @@ def open_test_pikerd(
# bwitout clobbering each other's config state.
tmpconfdir=tmpconfdir_str,
# NOTE these come verbatim from `tractor`'s builtin plugin!
#
# per-tpt compat registrar address.
reg_addr=reg_addr,
# bind in level from fixture.
# (can be set with `--ll <value>` flag to `pytest`).
# bind in level from fixture, which is itself set by
# `--ll <value>` cli flag.
loglevel=loglevel,
debug_mode=debug_mode,
)

View File

@ -1,251 +0,0 @@
'''
Binance websocket routing and feed-startup regressions.
'''
import pytest
import trio
from piker.brokers._util import DataUnavailable
from piker.brokers.binance import feed as binance_feed
from piker.brokers.binance.feed import (
get_quote_routes,
stream_messages,
wait_for_live_quotes,
)
from piker.brokers.binance.venues import (
_futes_market_ws,
_futes_public_ws,
_spot_ws,
)
from piker.data.validate import FeedInitializationError
def test_usdtm_websocket_routes() -> None:
'''
Binance retired its legacy USD-M ``/ws`` route in April 2026.
Market and high-frequency public traffic now use separate base
paths. Verify piker retains both routes so a
future endpoint edit cannot silently send a valid subscription to
a socket which never publishes that traffic class.
'''
routes: tuple[str, ...] = get_quote_routes(
mkt_mode='usdtm_futes',
symbols=['NVDAUSDT'],
)
assert routes == (
f'{_futes_public_ws}/nvdausdt@bookTicker',
f'{_futes_market_ws}/nvdausdt@aggTrade',
)
assert get_quote_routes(
mkt_mode='spot',
symbols=['BTCUSDT'],
) == (
f'{_spot_ws}/btcusdt@bookTicker',
f'{_spot_ws}/btcusdt@aggTrade',
)
def test_silent_binance_feed_startup_fails() -> None:
'''
A retired Binance websocket accepted piker's connection but sent
no first data, leaving ``stream_quotes()`` blocked before
``task_status.started()`` and hanging every chart consumer.
Keep an empty receive channel open to reproduce a connected but
silent socket. The controlled deadline proves startup raises
``DataUnavailable`` with the absent stream classes instead of
waiting indefinitely for a first quote.
'''
async def main() -> None:
send, recv = trio.open_memory_channel(1)
async with send, recv:
with pytest.raises(
DataUnavailable,
match='no live data received for NVDAUSDT',
):
await wait_for_live_quotes(
msg_stream=recv,
symbols=['NVDAUSDT'],
startup_timeout=0.01,
)
trio.run(main)
def test_usdtm_aggregate_trade_stream_type() -> None:
'''
Binance added ``st`` to USD-M aggregate trades during its June
2026 UM/CM websocket integration. The first valid NVDAUSDT trade
then raised ``TypeError`` while constructing piker's strict
``AggTrade`` struct, terminating the feed relay during startup.
Feed one representative USD-M frame through ``stream_messages()``
and assert its trade quote is emitted. This proves the new stream
discriminator is accepted without weakening parsing of the fields
used by downstream consumers.
'''
class MockWs:
def __init__(self) -> None:
self._msgs = iter([{
'e': 'aggTrade',
'E': 1787782452314,
'a': 8430770,
's': 'NVDAUSDT',
'p': '217.91000',
'q': '0.92',
'nq': '0.92',
'f': 20677382,
'l': 20677382,
'T': 1787782452162,
'm': True,
'st': 1,
}])
def __aiter__(self):
return self
async def __anext__(self) -> dict:
try:
return next(self._msgs)
except StopIteration:
raise StopAsyncIteration from None
async def main() -> None:
msg_gen = stream_messages(MockWs())
typ, quote = await anext(msg_gen)
assert typ == 'trade'
assert quote['symbol'] == 'NVDAUSDT'
assert quote['last'] == 217.91
trio.run(main)
def test_usdtm_book_ticker_stream_type() -> None:
'''
Binance USD-M book-ticker frames include event and transaction
metadata in addition to the update ID and four L1 value fields.
Feed the documented USD-M response shape through
``stream_messages()``. The emitted bid and ask ticks prove the
parser recognizes the real provider schema rather than a reduced
test-only event which could conceal route incompatibility.
'''
class MockWs:
def __init__(self) -> None:
self._msgs = iter([{
'e': 'bookTicker',
'u': 400900217,
'E': 1568014460893,
'T': 1568014460891,
's': 'BNBUSDT',
'b': '25.35190000',
'B': '31.21000000',
'a': '25.36520000',
'A': '40.66000000',
}])
def __aiter__(self):
return self
async def __anext__(self) -> dict:
try:
return next(self._msgs)
except StopIteration:
raise StopAsyncIteration from None
async def main() -> None:
msg_gen = stream_messages(MockWs())
typ, quote = await anext(msg_gen)
assert typ == 'l1'
assert quote['symbol'] == 'BNBUSDT'
assert quote['ticks'][0] == {
'type': 'bid',
'price': 25.3519,
'size': 31.21,
}
assert quote['ticks'][2] == {
'type': 'ask',
'price': 25.3652,
'size': 40.66,
}
trio.run(main)
def test_binance_feed_rejects_non_single_symbol_requests() -> None:
'''
Piker currently allocates one backend feed task per symbol, but
Binance previously accepted empty or multi-symbol requests and
either rendered invalid routes or silently retained one market.
Exercise both invalid cardinalities against the pure route helper
and public backend boundary. The helper ``ValueError`` proves no
malformed URLs can be rendered, while contextual
``FeedInitializationError`` errors prove ``stream_quotes()``
rejects each request before market lookup, websocket opening, or
task startup.
'''
async def main() -> None:
for symbols in (
[],
['NVDAUSDT', 'TSLAUSDT'],
):
with pytest.raises(
ValueError,
match='exactly one symbol',
):
get_quote_routes(
mkt_mode='usdtm_futes',
symbols=symbols,
)
with pytest.raises(
FeedInitializationError,
match='exactly one symbol',
):
await binance_feed.stream_quotes(
send_chan=None,
symbols=symbols,
feed_is_live=trio.Event(),
)
trio.run(main)
@pytest.mark.parametrize(
'typ',
['l1', 'trade'],
)
def test_first_quote_starts_feed(typ: str) -> None:
'''
Waiting independently for both L1 and trade data blocked startup
whenever either stream class remained quiet after the other proved
the merged Binance route was live.
Parameterize the first frame across both stream classes and keep
the merged channel open without a second producer. Returning that
exact frame under the startup timeout proves either L1-first or
trade-first delivery independently establishes feed readiness.
'''
async def main() -> None:
send, recv = trio.open_memory_channel(1)
first_quote: dict = {
'symbol': 'NVDAUSDT',
}
async with send, recv:
await send.send((typ, first_quote))
quote: dict = await wait_for_live_quotes(
msg_stream=recv,
symbols=['NVDAUSDT'],
startup_timeout=0.01,
)
assert quote is first_quote
trio.run(main)

View File

@ -151,7 +151,7 @@ def load_and_check_pos(
# is the same the fqme.
pp: Position = table.pps[ppmsg.symbol]
assert ppmsg.size == pp.cumsize
assert ppmsg.size == pp.size
assert ppmsg.avg_price == pp.ppu
yield pp
@ -179,7 +179,7 @@ def test_ems_err_on_bad_broker(
# NOTE: emsd should error on the actor's enabled modules
# import phase, when looking for a backend named `doggy`.
except tractor.RemoteActorError as re:
assert re.boxed_type is ModuleNotFoundError
assert re.type is ModuleNotFoundError
run_and_tollerate_cancels(load_bad_fqme)

View File

@ -1,133 +0,0 @@
'''
EMS broadcast ownership regressions.
'''
from contextlib import asynccontextmanager as acm
import trio
from trio.testing import wait_all_tasks_blocked
from tractor.trionics import broadcast_receiver
from piker.clearing import _ems
def test_dark_clearing_owns_non_strict_subscription(
monkeypatch,
) -> None:
'''
Dark clearing must not mutate another feed consumer's lag policy.
`clear_dark_triggers()` previously assigned `_raise_on_lag` on
whichever root or temporary receiver was stored in `Flume.stream`.
That changed private shared state and made behavior depend on feed
cache ownership. Supply a stream whose subscription records the
public lag-policy argument and yields a distinct child. Replace
the hot trigger loop with a checkpointing probe, then prove the
non-strict child is entered, passed to the core, and closed only
after processing completes while the source remains untouched.
'''
events: list[tuple] = []
child = object()
class QuoteStream:
'''
Record public subscription ownership without quote traffic.
'''
@acm
async def subscribe(self, raise_on_lag: bool = True):
'''
Yield one dedicated child for the wrapper lifetime.
'''
events.append(('enter', raise_on_lag))
try:
yield child
finally:
events.append(('exit', raise_on_lag))
async def clear_core(*args) -> None:
'''
Prove the child remains owned while processing runs.
'''
events.append(('core', args[2] is child))
await trio.lowlevel.checkpoint()
monkeypatch.setattr(
_ems,
'_clear_dark_triggers',
clear_core,
)
async def main() -> None:
stream = QuoteStream()
await _ems.clear_dark_triggers(
object(),
object(),
stream,
'ib',
'nvda.nasdaq.ib',
object(),
)
assert not hasattr(stream, '_raise_on_lag')
trio.run(main)
assert events == [
('enter', False),
('core', True),
('exit', False),
]
def test_dark_clearing_with_real_broadcast() -> None:
'''
Dark clearing must consume quotes without owning sibling lifetime.
Exercise the production trigger loop and real Tractor broadcaster
together. Deliver one trade tick to both EMS and a sibling cursor;
DarkBook.lasts proves EMS processed it. Cancel only the EMS task
and verify its subscription disappears while the sibling still
receives the next tick with its strict lag policy intact.
The book has no order triggers, so this requires no broker or
account. Unlike the imported wrapper test, no stream or clearing
function is replaced.
'''
async def main() -> None:
symbol = 'qqq.nasdaq.ib'
book = _ems.DarkBook(broker='ib', triggers={}, lasts={})
tx, rx = trio.open_memory_channel(8)
root = broadcast_receiver(rx, 8)
quote = {symbol: {'ticks': [{'type': 'trade', 'price': 100}]}}
with trio.fail_after(2):
async with tx, rx, root, root.subscribe() as sibling:
async with trio.open_nursery() as nursery:
nursery.start_soon(
_ems.clear_dark_triggers,
None,
None,
root,
'ib',
symbol,
book,
)
await wait_all_tasks_blocked()
assert len(root._state.subs) == 3
await tx.send(quote)
assert await sibling.receive() == quote
await wait_all_tasks_blocked()
assert book.lasts[symbol] == 100
assert root._raise_on_lag
assert sibling._raise_on_lag
nursery.cancel_scope.cancel()
assert len(root._state.subs) == 2
assert not sibling._closed
await tx.send(quote)
assert await sibling.receive() == quote
trio.run(main)

View File

@ -1,347 +0,0 @@
'''
Cached feed broadcast ownership regressions.
'''
from contextlib import asynccontextmanager as acm
from types import SimpleNamespace
import tractor
import trio
import pytest
from piker.data import feed as feed_mod
from piker.data.flows import Flume
class FakeMsgStream:
'''
Duplex test stream backed by a real Tractor broadcaster.
'''
def __init__(self, broker: str) -> None:
self.broker: str = broker
self._ctx: object = object()
self._tx, rx = trio.open_memory_channel(8)
self._broadcaster = tractor.trionics.broadcast_receiver(
rx,
8,
)
self.children: list[
tractor.trionics.BroadcastReceiver,
] = []
self.controls: list[str] = []
@property
def ctx(self) -> object:
'''
Expose the public context ref provided by `MsgStream.ctx`.
'''
return self._ctx
@acm
async def subscribe(self):
'''
Yield a real child receiver with duplex send delegation.
'''
child: tractor.trionics.BroadcastReceiver
async with self._broadcaster.subscribe() as child:
child.send = self.send
self.children.append(child)
yield child
async def send(self, msg: str) -> None:
'''
Record a pause or resume control sent through a child.
'''
self.controls.append(msg)
async def push(self, msg: dict) -> None:
'''
Send one provider quote into the broadcast source.
'''
await self._tx.send(msg)
async def close_source(self) -> None:
'''
End this provider without closing sibling relay channels.
'''
await self._tx.aclose()
def make_feed() -> tuple[
feed_mod.Feed,
dict[str, FakeMsgStream],
]:
'''
Build a real `Feed` and `Flume` graph for two providers.
'''
roots: dict[str, FakeMsgStream] = {
broker: FakeMsgStream(broker)
for broker in ('ib', 'deribit')
}
flumes: dict[str, Flume] = {
fqme: Flume(
mkt=SimpleNamespace(broker=broker),
first_quote={},
_rt_shm_token=SimpleNamespace(),
stream=roots[broker],
)
for fqme, broker in (
('nvda.nasdaq.ib', 'ib'),
('aapl.nasdaq.ib', 'ib'),
('btc-usd.deribit', 'deribit'),
)
}
return (
feed_mod.Feed(
mods={
broker: SimpleNamespace(name=broker)
for broker in roots
},
portals={},
flumes=flumes,
streams=roots,
status={},
),
roots,
)
@pytest.mark.parametrize('cache_hit', [False, True])
def test_cached_feed_returns_local_streams(
monkeypatch,
cache_hit: bool,
) -> None:
'''
A cached feed caller must not consume or replace shared streams.
`maybe_open_feed()` previously paired provider subscriptions with
`Feed.flumes.values()` using `zip()` and installed each child on the
cached `Flume`. One caller therefore exposed its lexical receiver
to every holder of the shared `Feed`; after exit those descriptors
pointed at closed children. The positional pairing also dropped a
market whenever one provider supplied multiple flumes.
Model two real provider broadcasters with two IB markets and one
Deribit market. Enter both first-owner and cache-hit paths, then
prove the yielded `Feed`, its `Feed.streams` mapping and all
`Flume`s are caller-local views. Exercise provider-context-wide
pause/resume and `Feed.open_multi_stream()` to show duplex sends
still reach the roots while each relay owns another child. Finally
prove every lexical child closes and all cached descriptors still
point at live roots.
'''
cached_feed: feed_mod.Feed
roots: dict[str, FakeMsgStream]
cached_feed, roots = make_feed()
cached_flumes: dict[str, Flume] = cached_feed.flumes
@acm
async def maybe_open_context(**kwargs):
'''
Return the controlled shared feed as a cache hit.
'''
yield cache_hit, cached_feed
monkeypatch.setattr(
feed_mod.trionics,
'maybe_open_context',
maybe_open_context,
)
local_feed: feed_mod.Feed|None = None
local_streams: dict[
str,
tractor.trionics.BroadcastReceiver,
]|None = None
async def main() -> None:
nonlocal local_feed, local_streams
feed: feed_mod.Feed
async with feed_mod.maybe_open_feed(
list(cached_flumes),
) as feed:
local_feed = feed
local_streams = feed.streams
assert feed is not cached_feed
assert feed.streams is not roots
assert feed.flumes is not cached_flumes
broker: str
stream: tractor.trionics.BroadcastReceiver
for broker, stream in feed.streams.items():
assert stream is roots[broker].children[0]
assert stream._ctx is roots[broker].ctx
fqme: str
flume: Flume
for fqme, flume in feed.flumes.items():
cached: Flume = cached_flumes[fqme]
stream: tractor.trionics.BroadcastReceiver = (
feed.streams[flume.mkt.broker]
)
assert flume is not cached
assert flume.stream is stream
assert cached.stream is roots[flume.mkt.broker]
assert (
feed.flumes['nvda.nasdaq.ib'].stream
is feed.flumes['aapl.nasdaq.ib'].stream
)
await feed.pause()
await feed.resume()
assert all(
root.controls == ['pause', 'resume']
for root in roots.values()
)
stream: trio.MemoryReceiveChannel
async with feed.open_multi_stream() as stream:
assert all(
len(child._state.subs) == 3
for child in feed.streams.values()
)
await roots['ib'].push({'provider': 'ib'})
await roots['deribit'].push({
'provider': 'deribit',
})
with trio.fail_after(1):
msgs = {
(await stream.receive())['provider'],
(await stream.receive())['provider'],
}
assert msgs == {'ib', 'deribit'}
# One provider's EOC must close only its `tx` clone;
# the sibling relay continues publishing to `stream`.
await roots['ib'].close_source()
with trio.fail_after(1):
while (
len(feed.streams['ib']._state.subs)
!= 2
):
await trio.lowlevel.checkpoint()
await roots['deribit'].push({
'provider': 'deribit-after-ib-eoc',
})
msg: dict
msg = await stream.receive()
assert msg['provider'] == 'deribit-after-ib-eoc'
assert all(
len(child._state.subs) == 2
for child in feed.streams.values()
)
trio.run(main)
assert local_feed is not None
assert local_streams is not None
assert all(stream._closed for stream in local_streams.values())
assert all(
flume.stream is roots[flume.mkt.broker]
for flume in cached_flumes.values()
)
assert all(
len(root._broadcaster._state.subs) == 1
for root in roots.values()
)
def test_feed_cache_key_includes_shape_and_policy(
monkeypatch,
) -> None:
'''
Feed cache identity must include every resource-defining input.
The old `fqmes[0]` key aliased `[A]` with `[A, B]` and ignored
`tick_throttle`, `allow_overruns` and `start_stream`. Whichever
caller entered first silently determined later feed shape and
stream behavior, including reuse of a history-only remote feed for
a live quote request.
Capture the key passed to `maybe_open_context()` for reordered and
varied requests. Symbol order must normalize to one key, while the
full symbol set and each stream policy must produce distinct keys.
'''
feed: feed_mod.Feed = feed_mod.Feed(
mods={},
portals={},
flumes={},
streams={},
status={},
)
keys: list[tuple] = []
@acm
async def maybe_open_context(**kwargs):
'''
Capture cache identity without opening a remote feed.
'''
keys.append(kwargs['key'])
yield False, feed
@acm
async def gather_contexts(mngrs):
'''
Permit the metadata-only empty feed used by this key probe.
'''
assert not mngrs
yield ()
monkeypatch.setattr(
feed_mod.trionics,
'maybe_open_context',
maybe_open_context,
)
monkeypatch.setattr(
feed_mod.trionics,
'gather_contexts',
gather_contexts,
)
async def open_once(
fqmes: list[str],
**kwargs,
) -> None:
async with feed_mod.maybe_open_feed(
fqmes,
**kwargs,
):
pass
async def main() -> None:
await open_once(['a.ib', 'b.ib'])
await open_once(['b.ib', 'a.ib'])
await open_once(['a.ib'])
await open_once(['a.ib', 'b.ib'], tick_throttle=10)
await open_once(
['a.ib', 'b.ib'],
allow_overruns=False,
)
await open_once(
['a.ib', 'b.ib'],
start_stream=False,
)
trio.run(main)
assert keys[0] == keys[1]
assert len(set(keys[2:])) == 4
assert keys[0] not in keys[2:]

View File

@ -23,35 +23,13 @@ from piker.accounting import (
'fqmes',
[
# binance
(100, {
# !TODO, write a suite which validates raising against
# bad/legacy fqmes such as this!
# 'btcusdt.binance',
'btcusdt.spot.binance',
'ethusdt.spot.binance',
}, False),
(100, {'btcusdt.binance', 'ethusdt.binance'}, False),
# kraken
(20, {
# !TODO, write a suite which validates raising against
# bad/legacy fqmes such as this!
# 'ethusdt.kraken',
# 'xbtusd.kraken',
'ethusdt.spot.kraken',
'xbtusd.spot.kraken',
}, True),
(20, {'ethusdt.kraken', 'xbtusd.kraken'}, True),
# binance + kraken
(100, {
# !TODO, write a suite which validates raising against
# bad/legacy fqmes such as this!
# 'btcusdt.binance',
# 'xbtusd.kraken',
'btcusdt.spot.binance',
'xbtusd.spot.kraken',
}, False),
(100, {'btcusdt.binance', 'xbtusd.kraken'}, False),
],
ids=lambda param: f'quotes={param[0]}@fqmes={param[1]}',
)
@ -70,17 +48,12 @@ def test_multi_fqsn_feed(
if (
ci_env
and
not run_in_ci
and not run_in_ci
):
pytest.skip(
'CI-disabled-test due to live-feed restrictions'
)
pytest.skip('Skipping CI disabled test due to feed restrictions')
brokers = set()
for fqme in fqmes:
# ?TODO, add this unpack + normalize check to a symbology
# helper fn?
brokername, *_ = unpack_fqme(fqme)
brokers.add(brokername)

View File

@ -1,586 +0,0 @@
'''
IB history adapter regressions backported from 0846cbd4 and 1da9b760.
'''
from types import SimpleNamespace
from ib_async import RequestError
import numpy as np
from pendulum import (
datetime as pdatetime,
duration,
)
import pytest
import trio
from piker.brokers import DataUnavailable
from piker.brokers.ib import feed as ib_feed
from piker.brokers.ib.feed import get_bars
def test_empty_history_frame_completes() -> None:
'''
A blank IB response must release the history request slot.
``get_bars()`` previously returned from its query task without
signalling the parent, which then reset the feed forever. That
kept serialized SHM repair from running. Return IB's empty
response and prove the request completes explicitly
with no result instead of relying on a reset timeout.
'''
class Proxy:
'''
Return one empty IB history response.
'''
async def bars(self, **kwargs):
'''
Model ``Client.bars()`` during a venue data gap.
'''
return (
[],
np.empty(0),
duration(seconds=2000),
)
async def main() -> None:
with trio.fail_after(0.1):
result, timedout = await get_bars(
Proxy(),
'qqq.nasdaq.ib',
1,
)
assert result is None
assert not timedout
trio.run(main)
def test_repeated_ib_cancellation_is_bounded() -> None:
'''
Repeated IB cancellation must not monopolize history forever.
IB can answer a historical request with error 162 cancellation.
The old retry loop had no limit, so reverse backfill could retain
the backend's sole request slot and prevent final null repair.
Always raise that response and prove the configured retry bound
terminates with ``DataUnavailable`` deterministically.
'''
class Proxy:
'''
Cancel every IB history request.
'''
async def bars(self, **kwargs):
'''
Raise IB's historical-query cancellation response.
'''
raise RequestError(
1,
162,
'API historical data query cancelled',
)
async def main() -> None:
with (
trio.fail_after(0.1),
pytest.raises(DataUnavailable),
):
await get_bars(
Proxy(),
'qqq.nasdaq.ib',
1,
max_failed_resets=2,
)
trio.run(main)
def test_query_error_cancels_stalled_reset(
monkeypatch: pytest.MonkeyPatch,
) -> None:
'''
Exceptional history exit must cancel its in-flight reset task.
A timed-out request acquires the reset lock before launching
IB's VNC reset task. If the query then raises while that task is
blocked, stale work must not retain the reset lock. Force that
ordering with explicit Trio checkpoints, then prove bounded query
failure cancels the reset task before propagating.
'''
reset_cancelled: list[bool] = []
class Proxy:
'''
Delay once, then return IB cancellation.
'''
async def bars(self, **kwargs):
'''
Let the parent acquire the reset lock before failing.
'''
await trio.sleep(0.02)
raise RequestError(
1,
162,
'API historical data query cancelled',
)
async def stalled_reset(
proxy,
reset_type='data',
task_status=trio.TASK_STATUS_IGNORED,
) -> bool:
'''
Publish reset state, then block until sibling failure.
'''
done = trio.Event()
result: list[bool] = []
with trio.CancelScope() as cs:
task_status.started((cs, done, result))
try:
await trio.sleep_forever()
finally:
reset_cancelled.append(True)
return False
monkeypatch.setattr(
ib_feed,
'wait_on_data_reset',
stalled_reset,
)
async def main() -> None:
with (
trio.fail_after(0.1),
pytest.raises(DataUnavailable),
):
await get_bars(
Proxy(),
'qqq.nasdaq.ib',
1,
feed_reset_timeout=0.001,
max_failed_resets=1,
)
trio.run(main)
assert reset_cancelled == [True]
def test_failed_pacing_resets_reach_retry_limit(
monkeypatch: pytest.MonkeyPatch,
) -> None:
'''
Failed pacing resets must increment the request's retry count.
The reset API returns an ``Event`` and result box. The old
code tested the always-truthy event and reset the failure counter
after each failure. Return immediate failures and prove two
pacing responses terminate instead of looping forever.
'''
reset_calls: list[bool] = []
class Proxy:
'''
Return a pacing violation for every request.
'''
_aio_ns = SimpleNamespace(
ib=SimpleNamespace(
client=SimpleNamespace(),
),
)
async def bars(self, **kwargs):
'''
Raise IB's pacing response.
'''
raise RequestError(
1,
162,
ib_feed._pacing,
)
async def failed_reset(
proxy,
reset_type='data',
task_status=trio.TASK_STATUS_IGNORED,
) -> bool:
'''
Publish one completed, unsuccessful reset.
'''
done = trio.Event()
result = [False]
reset_calls.append(True)
with trio.CancelScope() as cs:
task_status.started((cs, done, result))
done.set()
return False
monkeypatch.setattr(
ib_feed,
'wait_on_data_reset',
failed_reset,
)
async def main() -> None:
with (
trio.fail_after(0.1),
pytest.raises(DataUnavailable),
):
await get_bars(
Proxy(),
'qqq.nasdaq.ib',
1,
max_failed_resets=2,
)
trio.run(main)
assert reset_calls == [True, True]
def test_pacing_resets_are_serialized_across_requests(
monkeypatch: pytest.MonkeyPatch,
) -> None:
'''
Concurrent history requests must never reset IB farms together.
Pacing recovery bypassed reset ownership, allowing 1s and 60s
requests to launch the VNC hack concurrently. Start two pacing
failures in one nursery and hold each fake reset across a Trio
checkpoint. Active-count assertions prove the actor-global lock
serializes reset lifetimes without depending on scheduler timing.
'''
active: int = 0
max_active: int = 0
reset_calls: int = 0
class Proxy:
'''
Return a pacing violation for every request.
'''
_aio_ns = SimpleNamespace(
ib=SimpleNamespace(
client=SimpleNamespace(),
),
)
async def bars(self, **kwargs):
'''
Raise IB's pacing response.
'''
raise RequestError(
1,
162,
ib_feed._pacing,
)
async def failed_reset(
proxy,
reset_type='data',
task_status=trio.TASK_STATUS_IGNORED,
) -> bool:
'''
Hold one failed reset long enough to expose overlap.
'''
nonlocal active, max_active, reset_calls
done = trio.Event()
result: list[bool] = []
with trio.CancelScope() as cs:
task_status.started((cs, done, result))
active += 1
reset_calls += 1
max_active = max(max_active, active)
await trio.sleep(0.01)
active -= 1
result.append(False)
done.set()
return False
monkeypatch.setattr(
ib_feed,
'wait_on_data_reset',
failed_reset,
)
async def request() -> None:
'''
Run one independently bounded history request.
'''
with pytest.raises(DataUnavailable):
await get_bars(
Proxy(),
'qqq.nasdaq.ib',
1,
max_failed_resets=1,
)
async def main() -> None:
with trio.fail_after(0.1):
async with trio.open_nursery() as nursery:
nursery.start_soon(request)
nursery.start_soon(request)
trio.run(main)
assert reset_calls == 2
assert max_active == 1
def test_pacing_supersedes_timeout_reset_once(
monkeypatch: pytest.MonkeyPatch,
) -> None:
'''
Pacing handoff must not count cancellation as another failure.
A slow request can trigger timeout recovery immediately before IB
returns a pacing violation. Query-side recovery then deliberately
cancels that data reset and waits for the same actor-global lock.
Reproduce this with blocked first and failed second resets. A
one-attempt limit proves cancellation releases the lock
without aborting before the intended connection reset runs.
'''
reset_types: list[str] = []
class Proxy:
'''
Return pacing only after timeout recovery starts.
'''
_aio_ns = SimpleNamespace(
ib=SimpleNamespace(
client=SimpleNamespace(),
),
)
async def bars(self, **kwargs):
'''
Delay past the reset timeout, then report pacing.
'''
await trio.sleep(0.02)
raise RequestError(
1,
162,
ib_feed._pacing,
)
async def reset(
proxy,
reset_type='data',
task_status=trio.TASK_STATUS_IGNORED,
) -> bool:
'''
Block data reset and fail connection reset immediately.
'''
done = trio.Event()
result: list[bool] = []
reset_types.append(reset_type)
with trio.CancelScope() as cs:
task_status.started((cs, done, result))
if reset_type == 'data':
await trio.sleep_forever()
result.append(False)
done.set()
return False
monkeypatch.setattr(
ib_feed,
'wait_on_data_reset',
reset,
)
async def main() -> None:
with (
trio.fail_after(0.1),
pytest.raises(DataUnavailable),
):
await get_bars(
Proxy(),
'qqq.nasdaq.ib',
1,
feed_reset_timeout=0.001,
max_failed_resets=1,
)
trio.run(main)
assert reset_types == ['data', 'connection']
def test_completed_pacing_reset_restarts_timeout_wait(
monkeypatch: pytest.MonkeyPatch,
) -> None:
'''
A queued timeout waiter must consume completed pacing recovery.
While query-side pacing recovery holds the reset lock, the outer
timeout task can already queue behind it. Without a completion
handoff, that waiter launches a redundant data reset immediately.
Return a blank frame just after a successful connection reset and
prove no third reset runs before the restarted timeout observes
completion.
'''
reset_types: list[str] = []
class Proxy:
'''
Pace the first query and return blank data on retry.
'''
_aio_ns = SimpleNamespace(
ib=SimpleNamespace(
client=SimpleNamespace(),
),
)
def __init__(self) -> None:
self.calls: int = 0
async def bars(self, **kwargs):
'''
Control the pacing-to-success handoff ordering.
'''
self.calls += 1
if self.calls == 1:
await trio.sleep(0.02)
raise RequestError(
1,
162,
ib_feed._pacing,
)
await trio.sleep(0.0005)
return (
[],
np.empty(0),
duration(seconds=2000),
)
async def reset(
proxy,
reset_type='data',
task_status=trio.TASK_STATUS_IGNORED,
) -> bool:
'''
Cancel timeout recovery and complete pacing recovery.
'''
done = trio.Event()
result: list[bool] = []
reset_types.append(reset_type)
with trio.CancelScope() as cs:
task_status.started((cs, done, result))
if reset_type == 'data':
await trio.sleep_forever()
result.append(reset_type == 'connection')
done.set()
return result[0]
monkeypatch.setattr(
ib_feed,
'wait_on_data_reset',
reset,
)
async def main() -> None:
with trio.fail_after(0.1):
result, _ = await get_bars(
Proxy(),
'qqq.nasdaq.ib',
1,
feed_reset_timeout=0.001,
max_failed_resets=2,
)
assert result is None
trio.run(main)
assert reset_types == ['data', 'connection']
@pytest.mark.parametrize('empty_reply', ['hmds_error', 'empty_bars'])
@pytest.mark.parametrize(
'boundary', ['latest', 'after_head', 'head'],
)
def test_empty_history_preserves_request_boundary(
monkeypatch: pytest.MonkeyPatch,
empty_reply: str,
boundary: str,
) -> None:
'''
Empty IB responses retain their original request metadata.
Exercise open_history_client() and get_bars() together. The
latest request must not fall back to yesterday; a dated request
must not issue another date or become historical exhaustion
unless it has actually reached the known first timestamp.
'''
from contextlib import asynccontextmanager
from piker.brokers import NoData
head = pdatetime(2026, 9, 18, tz='UTC')
end = {
'latest': None,
'after_head': head.add(days=4),
'head': head,
}[boundary]
requests: list = []
class Proxy:
async def maybe_get_head_time(self, **kwargs):
return head
async def bars(self, **kwargs):
requests.append(kwargs['end_dt'])
if empty_reply == 'hmds_error':
raise RequestError(
1, 162, 'HMDS query returned no data',
)
return [], np.empty(0), duration(seconds=2000)
@asynccontextmanager
async def fake_data_client():
yield Proxy()
monkeypatch.setattr(
ib_feed, 'open_data_client', fake_data_client,
)
mkt = SimpleNamespace(
dst=SimpleNamespace(atype='future'),
get_bs_fqme=lambda **kwargs: 'mnq.cme.20261218',
)
async def main() -> None:
with trio.fail_after(1):
async with ib_feed.open_history_client(mkt) as pair:
get_hist, _ = pair
expected = (
DataUnavailable if boundary == 'head' else NoData
)
with pytest.raises(expected) as exc:
await get_hist(1, end_dt=end)
if expected is NoData:
assert exc.value.info['end_dt'] == end
assert requests == [end]
trio.run(main)

View File

@ -1,271 +0,0 @@
'''
IB asyncio method-proxy regressions.
'''
from types import SimpleNamespace
from typing import Any
import pytest
import trio
from piker.brokers.ib import api as ib_api
from piker.brokers.ib.api import (
MethodProxy,
relay_client_proxy_messages,
)
class FakeChannel:
'''
Model the Trio side of an asyncio linked-task channel.
'''
def __init__(self) -> None:
self.requests: list[tuple] = []
(
self._tx,
self._rx,
) = trio.open_memory_channel[Any](10)
async def send(self, msg: tuple) -> None:
'''
Capture one Trio-to-asyncio method request.
'''
self.requests.append(msg)
async def receive(self) -> Any:
'''
Receive one simulated asyncio-to-Trio response.
'''
return await self._rx.receive()
def closed(self) -> bool:
'''
Report an open channel for the lifetime of each test.
'''
return False
async def respond(self, msg: Any) -> None:
'''
Send one response or status event to the proxy relay.
'''
await self._tx.send(msg)
async def close_responses(self) -> None:
'''
Close the simulated asyncio-to-Trio response channel.
'''
await self._tx.aclose()
def test_method_proxy_routes_after_idle_status_events() -> None:
'''
Idle IB status traffic must not break first-symbol qualification.
``open_client_proxy()`` previously broadcast a one-slot channel
to an always-running status relay and an idle method receiver.
Status events advanced only the relay, so a first order for an
uncached symbol raised ``Lagged`` before receiving
``get_sym_details()``. Feed several idle events through the
sole-reader relay, then start two method calls and return their
responses in reverse order. Both exact results prove status
traffic cannot overrun a dormant caller and concurrent responses
remain correlated by request ID.
'''
async def main() -> None:
chan = FakeChannel()
proxy = MethodProxy(
chan,
event_table={},
asyncio_ns=SimpleNamespace(),
)
results: dict[str, str] = {}
async def call(name: str) -> None:
results[name] = await proxy._run_method(meth=name)
async with trio.open_nursery() as nursery:
nursery.start_soon(
relay_client_proxy_messages,
chan,
proxy,
)
for i in range(3):
await chan.respond((
'event',
{'reason': f'farm-status-{i}'},
))
nursery.start_soon(call, 'first')
nursery.start_soon(call, 'second')
with trio.fail_after(0.1):
while len(chan.requests) < 2:
await trio.lowlevel.checkpoint()
mids: dict[str, int] = {
meth: mid
for meth, _kwargs, mid in chan.requests
}
await chan.respond({
'mid': mids['second'],
'result': 'second-result',
})
await chan.respond({
'mid': mids['first'],
'result': 'first-result',
})
with trio.fail_after(0.1):
while len(results) < 2:
await trio.lowlevel.checkpoint()
nursery.cancel_scope.cancel()
assert results == {
'first': 'first-result',
'second': 'second-result',
}
trio.run(main)
def test_method_proxy_relay_accepts_clean_eof() -> None:
'''
Normal asyncio relay completion must not fail proxy teardown.
The sole-reader relay blocks in ``LinkedTaskChannel.receive()``
until the asyncio task exits and closes its Trio send channel.
Close the fake response sender before entering the relay and
prove the resulting ``EndOfChannel`` is graceful completion.
'''
async def main() -> None:
chan = FakeChannel()
proxy = MethodProxy(
chan,
event_table={},
asyncio_ns=SimpleNamespace(),
)
await chan.close_responses()
with trio.fail_after(0.1):
await relay_client_proxy_messages(chan, proxy)
trio.run(main)
def test_method_proxy_drops_cancelled_call_response() -> None:
'''
A cancelled symbol search must not skew the next method response.
Search timeouts can cancel a Trio caller after its request
reaches the asyncio client. Wait until one request is sent,
cancel its caller, and then deliver the orphaned response. The
sole-reader relay must drop that stale ``mid`` without failing; a
subsequent call must still receive its own exact response.
'''
async def main() -> None:
chan = FakeChannel()
proxy = MethodProxy(
chan,
event_table={},
asyncio_ns=SimpleNamespace(),
)
call_scope = trio.CancelScope()
cancelled = trio.Event()
result: list[str] = []
async def cancelled_call() -> None:
with call_scope:
try:
await proxy._run_method(meth='cancelled')
finally:
cancelled.set()
async def next_call() -> None:
value = await proxy._run_method(meth='next')
result.append(value)
async with trio.open_nursery() as nursery:
nursery.start_soon(
relay_client_proxy_messages,
chan,
proxy,
)
nursery.start_soon(cancelled_call)
with trio.fail_after(0.1):
while len(chan.requests) < 1:
await trio.lowlevel.checkpoint()
cancelled_mid: int = chan.requests[0][2]
call_scope.cancel()
with trio.fail_after(0.1):
await cancelled.wait()
assert proxy._pending == {}
assert (
proxy._request_methods[cancelled_mid]
==
'cancelled'
)
await chan.respond({
'mid': cancelled_mid,
'result': 'stale',
})
with trio.fail_after(0.1):
while cancelled_mid in proxy._request_methods:
await trio.lowlevel.checkpoint()
nursery.start_soon(next_call)
with trio.fail_after(0.1):
while len(chan.requests) < 2:
await trio.lowlevel.checkpoint()
next_mid: int = chan.requests[1][2]
await chan.respond({
'mid': next_mid,
'result': 'fresh',
})
with trio.fail_after(0.1):
while not result:
await trio.lowlevel.checkpoint()
nursery.cancel_scope.cancel()
assert result == ['fresh']
trio.run(main)
def test_method_proxy_warns_when_metadata_is_evicted(
monkeypatch: pytest.MonkeyPatch,
) -> None:
'''
Unresolved metadata eviction must remain operator-visible.
Cancelled calls whose asyncio responses never arrive retain a
bounded method name solely for later stale-response diagnostics.
Fill that table beyond its limit and prove the oldest entry is
evicted with its request ID, method name, and configured bound in
the warning.
'''
proxy = MethodProxy(
FakeChannel(),
event_table={},
asyncio_ns=SimpleNamespace(),
)
warnings: list[str] = []
monkeypatch.setattr(ib_api.log, 'warning', warnings.append)
for mid in range(257):
proxy._track_request_method(mid, f'method-{mid}')
assert len(proxy._request_methods) == 256
assert 0 not in proxy._request_methods
assert warnings == [
'Evicting unresolved method-request metadata,\n'
'mid: 0\n'
"meth: 'method-0'\n"
'pending metadata limit: 256\n'
]

View File

@ -1,291 +0,0 @@
'''
IB quote restart regressions using the real Tractor asyncio bridge.
'''
import asyncio
from contextlib import asynccontextmanager as acm
from types import SimpleNamespace
from async_generator import aclosing
from ib_async import Contract, IB, RequestError, Ticker
import pytest
import tractor
import trio
from piker.brokers.ib import feed
from piker.brokers.ib import _util
from piker.brokers.ib.api import Client, open_client_proxy
async def exercise_quote_restart() -> list[str]:
'''
Cancel an active quote consumer, then reopen in the same actor.
Only the external IB connection is replaced. The production quote
producer, eventkit callback, bridge, broadcaster and context cache
all run unchanged in an asyncio-infected Tractor child. The farm
OK event in stream_quotes() cancels its local scope after a reset;
reproduce that cancellation after an actual ticker delivery.
A cached owner can turn this into a normal bridge exit, feeding
TrioTaskExited back into the consumer and killing brokerd. Two
generations must close their IB subscriptions and asyncio tasks.
Return callback failures separately so actor survival cannot be
mistaken for clean teardown. This proves local restart semantics,
not connectivity to a real Gateway.
'''
contract = Contract(symbol='QQQ')
tasks: list[asyncio.Task] = []
tickers: list[Ticker] = []
cancelled: list[Contract] = []
callback_errors: list[dict] = []
class ReplayIB:
'''
Deliver one real eventkit ticker per subscription.
'''
def reqMktData(self, con: Contract, opts: str) -> Ticker:
'''
Emit once the production producer installs its callback.
'''
task = asyncio.current_task()
assert task is not None
tasks.append(task)
ticker = Ticker(contract=con)
tickers.append(ticker)
asyncio.get_running_loop().call_soon(
ticker.updateEvent.emit,
ticker,
)
return ticker
def cancelMktData(self, con: Contract) -> None:
'''
Record the production producer's subscription teardown.
'''
cancelled.append(con)
client = SimpleNamespace(ib=ReplayIB())
@acm
async def load_clients(**kwargs):
'''
Supply an offline IB peer without opening broker connections.
'''
yield {'ib.replay': client}
loop = asyncio.get_running_loop()
previous_handler = loop.get_exception_handler()
loop.set_exception_handler(
lambda loop, context: callback_errors.append(context)
)
try:
with pytest.MonkeyPatch.context() as patch:
patch.setattr(feed, 'load_aio_clients', load_clients)
patch.setattr(
feed,
'get_preferred_data_client',
lambda clients: ('ib.replay', client),
)
for generation in range(2):
with trio.CancelScope() as scope:
async with feed.open_aio_quote_stream(
'qqq.nasdaq',
contract=contract,
) as quotes:
async with aclosing(quotes):
ticker = await quotes.receive()
assert ticker is tickers[generation]
scope.cancel()
await trio.lowlevel.checkpoint()
assert scope.cancelled_caught
assert len(cancelled) == generation + 1
assert tasks[generation].done()
assert len(tickers[generation].updateEvent) == 0
# Flush done callbacks before accepting a clean restart.
await tractor.to_asyncio.run_task(asyncio.sleep, delay=0)
return [
repr(context['exception'])
for context in callback_errors
]
finally:
loop.set_exception_handler(previous_handler)
@pytest.mark.parametrize(
'check_callbacks',
[False, True],
ids=['actor-survival', 'callback-cleanliness'],
)
def test_quote_restart_preserves_actor(
reg_addr,
check_callbacks: bool,
) -> None:
'''
A farm-style quote restart must leave its broker actor usable.
Use a real TCP actor nursery and an asyncio-infected child. Run
two subscription generations, then invoke another RPC on the same
actor. Both cases reject a propagated bridge exception, leaked
quote task or lost actor. The strict case also rejects asyncio
callback failures; the survival case reports those independently.
The backout can restore restart survival while the strict case
still exposes Tractor's completion-callback defect.
'''
async def main() -> None:
with trio.fail_after(10):
async with (
tractor.open_root_actor(registry_addrs=[reg_addr]),
tractor.open_nursery() as nursery,
):
portal = await nursery.start_actor(
'ib_quote_replay',
enable_modules=[__name__],
infect_asyncio=True,
)
errors = await portal.run(exercise_quote_restart)
errors += await portal.run(exercise_quote_restart)
await portal.cancel_actor()
if check_callbacks:
assert not errors, errors
trio.run(main)
async def exercise_history_recovery(timeframe: int) -> None:
'''
Replay pacing and farm recovery across the production IB adapter.
Keep Client.bars(), MethodProxy, both relay tasks, get_bars(),
wait_on_data_reset() and data_reset_hack() intact. Replace only
external contract discovery, historical replies and VNC input.
Deliver the farm event through IB.errorEvent after the real reset
task is waiting, and reject a retry before that recovery signal.
This covers the bridge missing from the imported policy tests;
it neither sends VNC keys nor connects to a broker or storage.
'''
requests: int = 0
reset_calls: int = 0
recovery_sent: bool = False
reset_started = trio.Event()
contract = Contract(symbol='QQQ')
reason = 'HMDS data farm connection is OK:ushmds'
class ReplayIB(IB):
'''
Use real IB events with scripted historical responses.
'''
async def reqHistoricalDataAsync(self, **kwargs) -> list:
'''
Reject the first request; return empty data after reset.
'''
nonlocal requests
requests += 1
if requests == 1:
raise RequestError(1, 162, feed._pacing)
assert recovery_sent
assert requests == 2
return []
ib = ReplayIB()
client = Client(ib=ib, config={'vnc_addrs': []})
async def find_contracts(fqme: str) -> list[Contract]:
'''
Return the replay contract without network qualification.
'''
return [contract]
async def vnc_input(client: Client) -> None:
'''
Record completion of external reset input on asyncio.
'''
nonlocal reset_calls
reset_calls += 1
assert reset_calls == 1
reset_started.set()
await asyncio.sleep(0)
async def emit_recovery() -> None:
'''
Deliver the Gateway status through Client.inline_errors().
'''
nonlocal recovery_sent
recovery_sent = True
ib.errorEvent.emit(-1, 2106, reason, contract)
with pytest.MonkeyPatch.context() as patch:
patch.setattr(client, 'find_contracts', find_contracts)
patch.setattr(_util, '_reset_tech', 'vnc')
patch.setattr(_util, 'vnc_click_hack', vnc_input)
async with open_client_proxy(client) as proxy:
async def gateway_status() -> None:
'''
Wait for the actual farm-event consumer to be ready.
'''
await reset_started.wait()
event = proxy.event_table[reason]
while not event.statistics().tasks_waiting:
await trio.lowlevel.checkpoint()
await tractor.to_asyncio.run_task(emit_recovery)
async with trio.open_nursery() as nursery:
nursery.start_soon(gateway_status)
result, reset_used = await feed.get_bars(
proxy,
'qqq.nasdaq',
timeframe,
)
assert result is None
assert reset_used
assert not feed._data_reset_lock.locked()
assert requests == 2
assert reset_calls == 1
def test_history_pacing_through_real_bridge(reg_addr) -> None:
'''
Pacing recovery must complete before retrying either bar period.
Run the production proxy and reset tasks in an infected actor for
both 1s and 60s history. A returned empty frame must release the
request rather than trigger more resets. Repeat on the same actor
to expose leaked ownership from the first request.
'''
async def main() -> None:
with trio.fail_after(10):
async with (
tractor.open_root_actor(registry_addrs=[reg_addr]),
tractor.open_nursery() as nursery,
):
portal = await nursery.start_actor(
'ib_history_replay',
enable_modules=[__name__],
infect_asyncio=True,
)
for timeframe in (1, 60):
await portal.run(
exercise_history_recovery,
timeframe=timeframe,
)
await portal.cancel_actor()
trio.run(main)

View File

@ -1,112 +0,0 @@
'''
Package logger namespace regression tests.
'''
import logging
from types import SimpleNamespace
import pytest
import trio
from piker.log import (
get_console_log,
get_logger,
)
@pytest.mark.parametrize(
'factory',
[
get_logger,
get_console_log,
],
)
@pytest.mark.parametrize(
'name',
[
'piker.service._daemon',
'piker.paper',
],
)
def test_package_logger_name(
factory,
name: str,
):
'''
`str.lstrip('piker.')` treated the package prefix as a set of
removable characters. A submodule beginning with any of those
characters, such as `piker.paper`, was silently renamed to
`piker.aper`, while service code asserted a stale parent name.
Exercise both logger factories with service and prefix-sensitive
submodule names, proving they retain the exact requested package
namespace.
'''
log = factory(name=name)
assert log.name == name
def test_feed_startup_configures_transport_logger(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture,
) -> None:
'''
Daemon INFO did not enable websocket reconnection diagnostics.
Feed startup configured piker.data.feed, but ._util.log and
._web_bs.log emit on piker.data. Errors fell back to bare stderr
while connection-success INFO stayed hidden. Client verbosity
must also not demote that shared actor-wide transport logger.
Run production feed startup and logger configuration with an INFO
actor and ERROR client. Stop at feed-bus lookup before touching
any provider. Use real logging/console emission, not mocked logger
methods, to prove INFO from the websocket emitter is visible.
'''
from piker.data import feed, _web_bs
saved = {}
for name in ('piker.data', 'piker.data.feed', 'tractor'):
logger = logging.getLogger(name)
saved[logger] = (logger.level, list(logger.handlers))
boundary = RuntimeError('stop before provider lookup')
def stop_at_bus(brokername: str):
raise boundary
monkeypatch.setattr(
feed.tractor,
'current_actor',
lambda **kwargs: SimpleNamespace(
name='brokerd.binance', loglevel='info',
),
)
monkeypatch.setattr(feed, 'get_feed_bus', stop_at_bus)
async def main() -> None:
with pytest.raises(RuntimeError) as caught:
await feed.open_feed_bus(
ctx=None, brokername='binance', symbols=[],
loglevel='error',
)
assert caught.value is boundary
try:
logging.getLogger('piker.data').setLevel(logging.WARNING)
trio.run(main)
assert _web_bs.log.isEnabledFor(logging.INFO)
_web_bs.log.info('websocket INFO visibility probe')
assert 'websocket INFO visibility probe' in (
capsys.readouterr().err
)
finally:
for logger, (level, handlers) in saved.items():
for handler in list(logger.handlers):
if handler not in handlers:
logger.removeHandler(handler)
handler.close()
logger.setLevel(level)

View File

@ -7,7 +7,6 @@ from typing import (
Callable,
)
from contextlib import asynccontextmanager as acm
from types import SimpleNamespace
from exceptiongroup import BaseExceptionGroup
import pytest
@ -18,7 +17,6 @@ from piker.service import (
find_service,
Services,
)
from piker.service import _registry
from piker.data import (
open_feed,
)
@ -55,17 +53,11 @@ def test_runtime_boot(
tractor.wait_for_actor(
'pikerd',
registry_addr=daemon_addr,
arbiter_sockaddr=daemon_addr,
) as portal,
):
uw_raddr: tuple = pikerd_portal.chan.raddr.unwrap()
canonical_addr: tuple = (
_registry._normalize_addresses(
[daemon_addr],
)[0]
)
assert uw_raddr == canonical_addr
assert uw_raddr == portal.chan.raddr.unwrap()
assert pikerd_portal.channel.raddr == daemon_addr
assert pikerd_portal.channel.raddr == portal.channel.raddr
# no service tasks should be started
assert not services.service_tasks
@ -73,53 +65,6 @@ def test_runtime_boot(
trio.run(main)
def test_registry_normalizes_tagged_addresses(
monkeypatch: pytest.MonkeyPatch,
):
'''
Tractor now emits tagged registry addresses while existing Piker
callers still pass legacy socket pairs. Direct list and set
comparisons treated the two forms as different and retained the
legacy value in `Registry.addrs`.
Seed `Registry.addrs` with a legacy pair, request the equivalent
tagged address, and enter `open_registry()` as a root actor. The
yielded and stored values must both use tractor's canonical form.
'''
legacy = ('127.0.0.1', 6116)
tagged = ('tcp', '127.0.0.1', 6116)
actor = SimpleNamespace(
aid=SimpleNamespace(uid=('test', 'uid')),
)
monkeypatch.setattr(
tractor,
'current_actor',
lambda: actor,
)
monkeypatch.setattr(
tractor,
'is_root_process',
lambda: True,
)
_registry.Registry.addrs = [legacy]
async def main():
async with _registry.open_registry(
[tagged],
) as addrs:
assert addrs == [tagged]
assert _registry.Registry.addrs == [tagged]
assert _registry._tractor_kwargs[
'registry_addrs'
] == [tagged]
try:
trio.run(main)
finally:
_registry.Registry.addrs = []
def test_ensure_datafeed_actors(
open_test_pikerd: AsyncContextManager,
loglevel: str,
@ -163,7 +108,7 @@ async def ensure_service(
sockaddr: tuple[str, int] | None = None,
) -> None:
async with find_service(name) as portal:
remote_sockaddr: tuple = portal.chan.raddr.unwrap()
remote_sockaddr = portal.channel.raddr
print(f'FOUND `{name}` @ {remote_sockaddr}')
if sockaddr:
@ -186,49 +131,40 @@ def run_test_w_cancel_method(
"was remotely cancelled by remote actor (\'pikerd\'")
if cancel_method == 'sigint':
# XXX: with modern `tractor` the (single-exc)
# group is collapsed so a bare KBI normally
# propagates; tolerate either form.
with pytest.raises((
KeyboardInterrupt,
with pytest.raises(
BaseExceptionGroup,
)) as exc_info:
) as exc_info:
trio.run(main)
err = exc_info.value
match err:
case BaseExceptionGroup():
for suberr in err.exceptions:
match suberr:
# ensure we receive a remote
# cancellation error caused by the
# pikerd root actor.
case tractor.ContextCancelled():
assert (
cancelled_msg
in
suberr.args[0]
)
multi = exc_info.value
case KeyboardInterrupt():
pass
for suberr in multi.exceptions:
match suberr:
# ensure we receive a remote cancellation error caused
# by the pikerd root actor since we used the
# `.cancel_service()` API above B)
case tractor.ContextCancelled():
assert cancelled_msg in suberr.args[0]
case _:
pytest.fail(
f'Unexpected error {suberr}'
)
case KeyboardInterrupt():
pass
case KeyboardInterrupt():
pass
case _:
pytest.fail(f'Unexpected error {suberr}')
elif cancel_method == 'services':
# XXX: cancelling our own sub-service via
# `Services.cancel_service()` is a *self*
# requested cancel: modern `tractor` absorbs the
# resulting `ContextCancelled` (canceller is our
# own actor) so the runtime tears down gracefully
# with NO error raised to the opener.
trio.run(main)
# XXX NOTE: oddly, when you pass --pdb to pytest, i think since
# we also use that to enable the underlying tractor debug mode,
# it causes this to not raise for some reason? So if you see
# that while changing this test.. it's prolly that.
with pytest.raises(
tractor.ContextCancelled
) as exc_info:
trio.run(main)
assert cancelled_msg in exc_info.value.args[0]
else:
pytest.fail(f'Test is broken due to {cancel_method}')
@ -261,9 +197,7 @@ def test_ensure_ems_in_paper_actors(
# ensure we timeout after is startup is too slow.
# TODO: something like this should be our start point for
# benchmarking end-to-end startup B)
# NOTE: includes a live (kraken) symbology fetch so
# the budget needs some headroom for net latency..
with trio.fail_after(19):
with trio.fail_after(9):
async with (
open_test_pikerd() as (_, _, _, services),

View File

@ -1,214 +0,0 @@
'''
Offline regressions for endpoint configuration and service startup.
'''
from contextlib import asynccontextmanager as acm
from pathlib import Path
import sys
import tomllib
from types import ModuleType
from click.testing import CliRunner
import pytest
import tractor
import trio
from piker import config
from piker.cli import load_trans_eps, pikerd
from piker.service import _daemon, _actor_runtime
@pytest.mark.parametrize('repeat', [False, True])
def test_maddrs_click_collection(repeat):
'''
Click previously produced a string for `--maddrs`, which endpoint
loading split into individual characters. Parse one or repeated
options through Click and then the real Tractor parser, without
entering the indefinitely running daemon callback. Every address
must survive intact and in the declared order.
'''
addresses = ['/ip4/127.0.0.1/tcp/6116']
if repeat:
addresses.append('/ip4/127.0.0.1/tcp/6117')
args = [
arg for address in addresses for arg in ['--maddrs', address]
]
with pikerd.make_context('pikerd', args) as ctx:
endpoints = load_trans_eps(maddrs=ctx.params['maddrs'])
assert [addr.unwrap() for addr in endpoints['pikerd']] == [
('tcp', '127.0.0.1', 6116 + index)
for index in range(len(addresses))
]
def test_single_string_multiaddr():
'''
Direct endpoint callers may provide one string, independently of
Click's collection conversion. That input must denote a single
endpoint rather than a sequence of invalid character endpoints.
'''
endpoints = load_trans_eps(maddrs='/ip4/127.0.0.1/tcp/6116')
assert endpoints['pikerd'][0].unwrap() == (
'tcp', '127.0.0.1', 6116,
)
def test_template_single_transport():
'''
The shipped template enabled TCP and UDS together although the
locked Tractor runtime permits only one transport per actor.
Parse the real template and ensure both daemon and chart defaults
use the TCP transport selected by their startup paths.
'''
path = Path(__file__).parents[1] / 'config' / 'conf.toml'
conf = tomllib.loads(path.read_text())
endpoints = load_trans_eps(conf['network'])
for name in ['pikerd', 'chart']:
assert endpoints[name]
assert {addr.proto_key for addr in endpoints[name]} == {'tcp'}
@pytest.mark.parametrize('network', [None, {}])
def test_chart_without_network(monkeypatch, network):
'''
Existing configs without a populated network table left chart
bind endpoints unbound, crashing before the UI entrypoint. Invoke
the actual Click callback with absent and empty tables, replacing
only config IO and the UI entrypoint. Automatic binding must be
passed as None while preserving the configured registrar address.
'''
from piker.ui.cli import chart
conf = {} if network is None else {'network': network}
monkeypatch.setattr(config, 'load', lambda **_: (conf, None))
app = ModuleType('piker.ui._app')
calls = []
app._main = lambda **kwargs: calls.append(kwargs)
monkeypatch.setitem(sys.modules, 'piker.ui._app', app)
result = CliRunner().invoke(
chart,
['btcusdt.binance'],
obj={
'pdb': False,
'brokermods': [object()],
'tractorloglevel': 'warning',
'loglevel': 'warning',
'registry_addrs': [('127.0.0.1', 6116)],
},
)
assert result.exit_code == 0, result.exception
runtime = calls[0]['tractor_kwargs']
assert runtime['tpt_bind_addrs'] is None
assert runtime['registry_addrs'] == [('127.0.0.1', 6116)]
@pytest.mark.parametrize('explicit_registry', [False, True])
def test_daemon_options_resolve_registrars(
monkeypatch,
explicit_registry,
):
'''
Any nonempty pikerd kwargs previously skipped registrar address
initialization. Exercise the full discovery and spawn path with
runtime options, using protocol-boundary fakes instead of remote
actors. An explicit override must reach discovery and startup
exactly once; otherwise the inherited registrar list is used.
The service lock must be released after leaving the context.
'''
inherited = [('tcp', '127.0.0.1', 6116)]
override = [('tcp', '127.0.0.1', 6117)]
expected = override if explicit_registry else inherited
calls = []
portal = object()
@acm
async def find(name, registry_addrs):
calls.append(('find', registry_addrs))
yield None
@acm
async def open_root(**kwargs):
calls.append(('open', kwargs))
yield None
@acm
async def wait(name):
yield portal
async def start(**kwargs):
return True
monkeypatch.setattr(_daemon, 'find_service', find)
monkeypatch.setattr(_daemon, 'maybe_open_pikerd', open_root)
monkeypatch.setattr(tractor, 'wait_for_actor', wait)
monkeypatch.setattr(
tractor,
'get_runtime_vars',
lambda: {'_registry_addrs': inherited},
)
async def main():
lock = trio.Lock()
monkeypatch.setitem(_daemon.Services.locks, 'probe', lock)
kwargs = {'debug_mode': False}
if explicit_registry:
kwargs['registry_addrs'] = override
async with _daemon.maybe_spawn_daemon(
'probe', start, {}, **kwargs,
) as found:
assert found is portal
assert not lock.locked()
trio.run(main)
assert calls[0] == ('find', expected)
assert calls[1][1]['registry_addrs'] == expected
assert calls[1][1]['debug_mode'] is False
async def _check_distinct_daemon_bind():
'''
Open Piker's service context on a non-registrar child actor.
'''
actor = tractor.current_actor()
assert not actor.is_registrar
assert actor.accept_addrs != actor.reg_addrs
async with _actor_runtime.open_pikerd(
registry_addrs=actor.reg_addrs,
tpt_bind_addrs=[('127.0.0.1', 0)],
):
return actor.accept_addrs
def test_daemon_bind_distinct_from_registrar(reg_addr):
'''
Separate daemon and registrar endpoints were rejected because
startup checked registrar addresses against daemon listeners.
Run a real TCP registrar and a distinct child daemon with an
ephemeral listener. Its service context must start and unwind
without taking ownership of the registrar's listening address.
'''
async def main():
async with (
tractor.open_root_actor(
registry_addrs=[reg_addr],
enable_transports=['tcp'],
),
tractor.open_nursery() as nursery,
):
portal = await nursery.start_actor(
'daemon_probe',
enable_modules=[__name__],
bind_addrs=[('127.0.0.1', 0)],
)
addresses = await portal.run(_check_distinct_daemon_bind)
assert addresses
await nursery.cancel()
trio.run(main)

View File

@ -1,374 +0,0 @@
'''
Generic websocket connection and teardown regressions.
'''
from contextlib import asynccontextmanager as acm
import pytest
import trio
from trio_websocket import (
ConnectionClosed,
ConnectionRejected,
WebSocketConnection,
serve_websocket,
)
from wsproto.events import CloseConnection
from piker.data import _web_bs
def test_websocket_connection_startup_fails(
monkeypatch: pytest.MonkeyPatch,
) -> None:
'''
``open_autorecon_ws()`` previously had no bound while the real
``_reconnect_forever()`` task awaited its first websocket
handshake, so provider startup inherited a 616-second deadline.
Replace only ``_web_bs.open_websocket_url()`` with a context
entry that never completes. The URL-bearing ``TimeoutError``
proves the wrapper bounds the real reconnect task and cancels its
nursery instead of waiting indefinitely for
``NoBsWs._connected``.
'''
@acm
async def never_connect(url: str):
await trio.sleep_forever()
yield
monkeypatch.setattr(
_web_bs,
'open_websocket_url',
never_connect,
)
async def main() -> None:
with pytest.raises(
TimeoutError,
match='wss://silent.invalid/ws',
):
async with _web_bs.open_autorecon_ws(
url='wss://silent.invalid/ws',
connect_timeout=0.01,
):
pytest.fail(
'silent websocket unexpectedly connected'
)
trio.run(main)
def test_immediate_websocket_failure_retries(
monkeypatch: pytest.MonkeyPatch,
) -> None:
'''
``_reconnect_forever()`` previously fell through after a handled
first-handshake rejection and read ``cs`` before any websocket
nursery had assigned it, masking the transport failure with an
unbound-local error.
Make the real reconnect loop receive ``ConnectionRejected`` on
its first context entry and ``RuntimeError`` on its second.
Removing retry sleeps fixes their order. Two attempts and the
propagated second error prove the first rejection restarts the
loop without touching connection-local cancellation state.
Require the exact error object so wrapping cannot discard its
existing diagnostic context.
'''
attempts: int = 0
failure = RuntimeError('unexpected websocket failure')
@acm
async def reject_url(url: str):
nonlocal attempts
attempts += 1
if attempts == 1:
raise ConnectionRejected(503, [], b'unavailable')
raise failure
yield
async def no_throttle(delay: float) -> None:
await trio.lowlevel.checkpoint()
monkeypatch.setattr(
_web_bs,
'open_websocket_url',
reject_url,
)
monkeypatch.setattr(
_web_bs.trio,
'sleep',
no_throttle,
)
async def main() -> None:
with pytest.raises(
RuntimeError,
match='unexpected websocket',
) as caught:
async with _web_bs.open_autorecon_ws(
url='wss://reject.invalid/ws',
connect_timeout=0.1,
):
pytest.fail(
'failed websocket unexpectedly connected'
)
assert caught.value is failure
trio.run(main)
assert attempts == 2
def test_reconnect_cancellation_bypasses_error_throttle(
monkeypatch: pytest.MonkeyPatch,
) -> None:
'''
``_reconnect_forever()`` previously caught ``trio`` cancellation
in its broad ``BaseException`` handler and entered a shielded
200ms error throttle, delaying websocket and feed-task teardown.
Block the real reconnect task in the patched websocket context,
wait until that cancellation point is active, and cancel its
nursery. The tracked ``trio.sleep()`` replacement records any
retry throttle without wall time. No calls after nursery exit
proves structured cancellation bypasses normal error throttling.
'''
entered = trio.Event()
sleep_calls: list[float] = []
@acm
async def blocked_url(url: str):
entered.set()
await trio.sleep_forever()
yield
async def record_sleep(delay: float) -> None:
sleep_calls.append(delay)
await trio.lowlevel.checkpoint()
monkeypatch.setattr(
_web_bs,
'open_websocket_url',
blocked_url,
)
monkeypatch.setattr(
_web_bs.trio,
'sleep',
record_sleep,
)
async def main() -> None:
send, recv = trio.open_memory_channel(1)
nobsws = _web_bs.NoBsWs(
'wss://cancel.invalid/ws',
recv,
16,
)
async with (
send,
recv,
trio.open_nursery() as tn,
):
await tn.start(
_web_bs._reconnect_forever,
'wss://cancel.invalid/ws',
send,
nobsws,
3,
)
await entered.wait()
tn.cancel_scope.cancel()
trio.run(main)
assert sleep_calls == []
@pytest.mark.parametrize('shutdown', ['recover', 'reader', 'retry'])
def test_reader_ping_close_recovery(
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
shutdown: str,
) -> None:
'''
A real websocket reader's Pong error escaped reconnect ownership.
Binance pings arriving during local closure raised wsproto's
LocalProtocolError in trio-websocket's reader nursery, outside
proxy_msgs(). 97eb8347 propagated that error from the outer
wrapper, terminating the persistent broker instead of reconnecting.
Use real loopback TCP, handshake, Ping frames and reader dispatch.
At the first client Ping, synchronously advance its real wsproto
connection to LOCAL_CLOSING. Withhold that Close frame from the
wire so a peer Close reply cannot race this deterministic gate.
The original Ping handler then generates the actual Pong error.
Require a replacement connection to deliver fresh application data.
For shutdown, cancel either at the held Ping boundary or after
the actual error reaches the reconnect throttle. Require bounded
exit and exactly one connection, proving teardown never starts a
retry. Only the Ping handler ordering is controlled; contexts,
readers, cancellation, reconnect tasks and delivery remain real.
'''
caplog.set_level('INFO', logger=_web_bs.log.name)
async def main() -> None:
connections: int = 0
ping_entered = trio.Event()
handler_exited = trio.Event()
retry_entered = trio.Event()
ping_handler = WebSocketConnection._handle_ping_event
faulted: list[WebSocketConnection] = []
log_exception = _web_bs.log.exception
def observe_retry(message, *args, **kwargs) -> None:
if message.startswith('Retrying connection:'):
retry_entered.set()
log_exception(message, *args, **kwargs)
monkeypatch.setattr(_web_bs.log, 'exception', observe_retry)
async def gate_ping(ws, event) -> None:
if ws.is_client and not faulted:
faulted.append(ws)
ping_entered.set()
try:
if shutdown == 'reader':
await trio.sleep_forever()
ws._wsproto.send(CloseConnection(code=1000))
await ping_handler(ws, event)
finally:
handler_exited.set()
else:
await ping_handler(ws, event)
monkeypatch.setattr(
WebSocketConnection, '_handle_ping_event', gate_ping,
)
async def serve(request) -> None:
nonlocal connections
ws = await request.accept()
connections += 1
if connections == 1:
try:
await ws.ping(b'close-race')
except ConnectionClosed:
pass
else:
await ws.send_message('{"generation": 2}')
await trio.sleep_forever()
async with trio.open_nursery() as tn:
server = await tn.start(
serve_websocket, serve, '127.0.0.1', 0, None,
)
url = f'ws://127.0.0.1:{server.port}/'
with trio.fail_after(5):
with trio.CancelScope() as owner:
async with _web_bs.open_autorecon_ws(
url, connect_timeout=2,
) as ws:
await ping_entered.wait()
if shutdown == 'retry':
await retry_entered.wait()
if shutdown != 'recover':
owner.cancel()
await trio.lowlevel.checkpoint()
else:
assert await ws.recv_msg() == {
'generation': 2,
}
await handler_exited.wait()
assert len(faulted) == 1
assert connections == (
2 if shutdown == 'recover' else 1
)
tn.cancel_scope.cancel()
trio.run(main)
if shutdown == 'recover':
messages = [record.getMessage() for record in caplog.records]
assert sum(
message.startswith('Websocket ready:')
for message in messages
) == 2
assert sum(
message.startswith('Websocket first message relayed:')
for message in messages
) == 1
@pytest.mark.parametrize('message_arrives', [False, True])
def test_timeout_identifies_blocked_operation(
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
message_arrives: bool,
) -> None:
'''
Receive starvation and local backpressure shared one reset log.
The reconnect deadline covers both ws.get_message() and the
Trio relay send, so a connected-but-stalled feed could not be
diagnosed from the old timeout report. Use real loopback sockets
with either a silent server or a server sending one payload into
a zero-capacity local channel whose consumer never receives.
Require the actual timed-out operation and URL in the diagnostic.
Stop after the first timeout, before the unchanged reset budget.
'''
async def main() -> None:
observed = trio.Event()
warning = _web_bs.log.warning
def observe_timeout(message, *args, **kwargs) -> None:
warning(message, *args, **kwargs)
if message.startswith('Websocket timeout waiting on '):
observed.set()
monkeypatch.setattr(_web_bs.log, 'warning', observe_timeout)
async def serve(request) -> None:
ws = await request.accept()
if message_arrives:
await ws.send_message('{"sequence": 1}')
await trio.sleep_forever()
async with trio.open_nursery() as tn:
server = await tn.start(
serve_websocket, serve, '127.0.0.1', 0, None,
)
url = f'ws://127.0.0.1:{server.port}/'
send, receive = trio.open_memory_channel(0)
async with send, receive:
wrapper = _web_bs.NoBsWs(
url, receive, msg_recv_timeout=0.05,
)
await tn.start(
_web_bs._reconnect_forever,
url, send, wrapper, 3,
)
with trio.fail_after(5):
await observed.wait()
waiting_on = (
'local relay send'
if message_arrives else 'websocket receive'
)
messages = [
record.getMessage() for record in caplog.records
if record.getMessage().startswith(
'Websocket timeout waiting on ',
)
]
assert len(messages) == 1
assert f'waiting on {waiting_on}' in messages[0]
assert f'url: {url!r}' in messages[0]
assert 'count: 1; reset after: 3' in messages[0]
tn.cancel_scope.cancel()
trio.run(main)

552
uv.lock
View File

@ -1,6 +1,6 @@
version = 1
revision = 3
requires-python = "==3.13.*"
requires-python = ">=3.12, <3.14"
resolution-markers = [
"sys_platform == 'win32'",
"sys_platform == 'emscripten'",
@ -55,6 +55,23 @@ dependencies = [
{ name = "yarl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/54/07/9467d3f8dae29b14f423b414d9e67512a76743c5bb7686fb05fe10c9cc3e/aiohttp-3.9.1.tar.gz", hash = "sha256:8fc49a87ac269d4529da45871e2ffb6874e87779c3d0e2ccd813c0899221239d", size = 7482118, upload-time = "2023-11-26T17:55:44.763Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/41/d6/e4f5eadff5e4523f75b56183f474f7d5f54fc495e80ee875843d7b264492/aiohttp-3.9.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f800164276eec54e0af5c99feb9494c295118fc10a11b997bbb1348ba1a52065", size = 591673, upload-time = "2023-11-26T17:53:39.376Z" },
{ url = "https://files.pythonhosted.org/packages/70/de/9cfb42190a946df5179375a8e59110faf8188e2c19f58a6f8f6846414c8f/aiohttp-3.9.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:500f1c59906cd142d452074f3811614be04819a38ae2b3239a48b82649c08821", size = 392831, upload-time = "2023-11-26T17:53:41.583Z" },
{ url = "https://files.pythonhosted.org/packages/cf/45/580b5a6abb70530cea7f6e697227c61e0001eff75d50b897a62b66c6d3b7/aiohttp-3.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0b0a6a36ed7e164c6df1e18ee47afbd1990ce47cb428739d6c99aaabfaf1b3af", size = 388850, upload-time = "2023-11-26T17:53:44.157Z" },
{ url = "https://files.pythonhosted.org/packages/02/3a/9aa79bc010bb8af6020f8e70937710d01622b97a7e04b8f8fbea97b04ff8/aiohttp-3.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69da0f3ed3496808e8cbc5123a866c41c12c15baaaead96d256477edf168eb57", size = 1319339, upload-time = "2023-11-26T17:53:47.152Z" },
{ url = "https://files.pythonhosted.org/packages/3c/2a/6db78762123f368d97a38694b75d1942fcff6d476cb633dbca84c93c7221/aiohttp-3.9.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:176df045597e674fa950bf5ae536be85699e04cea68fa3a616cf75e413737eb5", size = 1359461, upload-time = "2023-11-26T17:53:50.59Z" },
{ url = "https://files.pythonhosted.org/packages/8c/4b/fec8718e62106fa0362c5109f362ce45f6985d14283678e5c82cc9dfb0af/aiohttp-3.9.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b796b44111f0cab6bbf66214186e44734b5baab949cb5fb56154142a92989aeb", size = 1401205, upload-time = "2023-11-26T17:53:53.275Z" },
{ url = "https://files.pythonhosted.org/packages/75/5f/90a2869ad3d1fb9bd984bfc1b02d8b19e381467b238bd3668a09faa69df5/aiohttp-3.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f27fdaadce22f2ef950fc10dcdf8048407c3b42b73779e48a4e76b3c35bca26c", size = 1314647, upload-time = "2023-11-26T17:53:56.884Z" },
{ url = "https://files.pythonhosted.org/packages/a4/56/f5064eb44914235591b372b04420fd9e80b21110ae718ba72387f49ee9c0/aiohttp-3.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcb6532b9814ea7c5a6a3299747c49de30e84472fa72821b07f5a9818bce0f66", size = 1266651, upload-time = "2023-11-26T17:53:59.833Z" },
{ url = "https://files.pythonhosted.org/packages/59/86/f759ee047d87cff52028e90679a2f5c15c08f1b816cd1c16eb06db65276f/aiohttp-3.9.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:54631fb69a6e44b2ba522f7c22a6fb2667a02fd97d636048478db2fd8c4e98fe", size = 1321196, upload-time = "2023-11-26T17:54:03.058Z" },
{ url = "https://files.pythonhosted.org/packages/b6/ae/30c8962df269f86912be9e3ec59b51dd8eaeccb5d23695f63177a0e21d1b/aiohttp-3.9.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4b4c452d0190c5a820d3f5c0f3cd8a28ace48c54053e24da9d6041bf81113183", size = 1267553, upload-time = "2023-11-26T17:54:05.551Z" },
{ url = "https://files.pythonhosted.org/packages/f3/1a/6452aa5ab519e79c43831e59fcef6f76426b51810d9772e03addc3efd958/aiohttp-3.9.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:cae4c0c2ca800c793cae07ef3d40794625471040a87e1ba392039639ad61ab5b", size = 1347571, upload-time = "2023-11-26T17:54:08.408Z" },
{ url = "https://files.pythonhosted.org/packages/20/43/19a597a7e50ea99d04509ea82659c52149fefec45b5005d2e1f67b68ac0d/aiohttp-3.9.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:565760d6812b8d78d416c3c7cfdf5362fbe0d0d25b82fed75d0d29e18d7fc30f", size = 1395459, upload-time = "2023-11-26T17:54:11.071Z" },
{ url = "https://files.pythonhosted.org/packages/d0/89/5cdbebbdfe91c1f937ef4cc2836152cce0d2a0138029b53703d0c3f13199/aiohttp-3.9.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54311eb54f3a0c45efb9ed0d0a8f43d1bc6060d773f6973efd90037a51cd0a3f", size = 1316162, upload-time = "2023-11-26T17:54:13.789Z" },
{ url = "https://files.pythonhosted.org/packages/21/66/114bf1d9f0a38a50bf1b7a5c8315a44fd1f35bd1fee025a230907a2cb4b7/aiohttp-3.9.1-cp312-cp312-win32.whl", hash = "sha256:85c3e3c9cb1d480e0b9a64c658cd66b3cfb8e721636ab8b0e746e2d79a7a9eed", size = 341652, upload-time = "2023-11-26T17:54:16.771Z" },
{ url = "https://files.pythonhosted.org/packages/4e/13/e929a6a50288e60ade3961b294d2f5aeb251b6579e4290a5397e484d0df9/aiohttp-3.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:11cb254e397a82efb1805d12561e80124928e04e9c4483587ce7390b3866d213", size = 362885, upload-time = "2023-11-26T17:54:18.884Z" },
]
[[package]]
name = "aiosignal"
@ -62,6 +79,7 @@ version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "frozenlist" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" }
wheels = [
@ -74,6 +92,7 @@ version = "4.12.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" }
wheels = [
@ -98,15 +117,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/44/827b2a91a5816512fcaf3cc4ebc465ccd5d598c45cefa6703fcf4a79018f/attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1", size = 60752, upload-time = "2023-12-31T06:30:30.772Z" },
]
[[package]]
name = "base58"
version = "2.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7f/45/8ae61209bb9015f516102fa559a2914178da1d5868428bd86a1b4421141d/base58-2.1.1.tar.gz", hash = "sha256:c5d0cb3f5b6e81e8e35da5754388ddcc6d0d14b6c6a132cb93d69ed580a7278c", size = 6528, upload-time = "2021-10-30T22:12:17.858Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4a/45/ec96b29162a402fc4c1c5512d114d7b3787b9d1c2ec241d9568b4816ee23/base58-2.1.1-py3-none-any.whl", hash = "sha256:11a36f4d3ce51dfc1043f3218591ac4eb1ceb172919cebe05b52a5bcc8d245c2", size = 5621, upload-time = "2021-10-30T22:12:16.658Z" },
]
[[package]]
name = "bidict"
version = "0.23.1"
@ -116,33 +126,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764, upload-time = "2024-02-18T19:09:04.156Z" },
]
[[package]]
name = "blake3"
version = "1.0.9"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/26/6a/4cc5a9dd40fd8a6d283fd3761e5f59c490109571ef8e3c73245417e5a305/blake3-1.0.9.tar.gz", hash = "sha256:5fa374fa5070ca084368776c19b420157eb0f2d3f091343d6bc59189929d62e2", size = 116872, upload-time = "2026-06-22T18:02:25.366Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/41/1b/95b473d649f5322e69674622a307ffdb4f0b63adb0a0adcbc5cb8a8833c2/blake3-1.0.9-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:69ff5aebc7650954443aa701feff2028d7c7ea5b5e18ee265f15e2104e892328", size = 343869, upload-time = "2026-06-22T18:00:51.936Z" },
{ url = "https://files.pythonhosted.org/packages/4b/9d/adec22c719d8451af1dc9e624bf5907008ef1e0afa51aa69fd1e8c91e60e/blake3-1.0.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0cdfeff65488089ef86f7587c76055ff72b28d28d10e427b547f5711477c376d", size = 328482, upload-time = "2026-06-22T18:00:53.39Z" },
{ url = "https://files.pythonhosted.org/packages/5e/aa/0a6967ff9a6ae182419a681aed54f7338b34a1f71372e90f787a2afa42e6/blake3-1.0.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:766f1555cbe614f14f399c2fbec0983568d20edb36837ba04040807eb9e1a609", size = 373616, upload-time = "2026-06-22T18:00:54.701Z" },
{ url = "https://files.pythonhosted.org/packages/1c/51/5d4e198bf3ae902c6697ad6ec77d7210736ad8f680980e8b648dcfcd09a0/blake3-1.0.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:128a62136c9a39c7cb9fdaa5fb38471f2418853da7f5a89f31495735d0ba6f2c", size = 374149, upload-time = "2026-06-22T18:00:56.015Z" },
{ url = "https://files.pythonhosted.org/packages/7e/62/d3c7c364925b3f10828e5137376f3947f112c32188e899b42f09c2fde98a/blake3-1.0.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1ea0bf17b184b03444007646d902207d2b4d4f3e91a0cac3836552d83db74b9", size = 446151, upload-time = "2026-06-22T18:00:57.378Z" },
{ url = "https://files.pythonhosted.org/packages/b1/01/55b89389c5036c9d24b1d762d6265e91552e10b76a3c99fece3c4a7a4783/blake3-1.0.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73a48f7e9f0e047f51a445d9b0361ab1907bdc72b6857815a84dacd2e59556f8", size = 487256, upload-time = "2026-06-22T18:00:58.763Z" },
{ url = "https://files.pythonhosted.org/packages/b2/7a/a21b52253292ad3e4df63ea4a01ce11d3ee8f4a8a8d80eaf0c7ce92a62bd/blake3-1.0.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b27550ada40f839aca64c66127940e4318bb6ef3e291890ef913017f6f637448", size = 383977, upload-time = "2026-06-22T18:01:00.192Z" },
{ url = "https://files.pythonhosted.org/packages/f3/f0/fe7188201a29ee9b042616c786a98afd864d537ca96198e64c3fe4ff13a9/blake3-1.0.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66c84dbc2a31eda88b55bbf5c5b711037bf0698eba0fd1faf06bdaf313c39048", size = 383615, upload-time = "2026-06-22T18:01:01.557Z" },
{ url = "https://files.pythonhosted.org/packages/22/08/f6a213b950e30fe9ef7d7fc061ec388e66ed62643570226882e6f7136ea3/blake3-1.0.9-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:dab59b324aa65c09e937d6c43de5de85ec9581627f4e79dcc9806d85b54a1c34", size = 380288, upload-time = "2026-06-22T18:01:03.025Z" },
{ url = "https://files.pythonhosted.org/packages/f1/fb/b171e47c1b835483bcf1545ebc289458165f8dc0f5c7f74a9176d7e9af03/blake3-1.0.9-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:eca281fedcbe5c56655bd5a4176e6036eddbbe57df96114a03838fce08b1e0ca", size = 549122, upload-time = "2026-06-22T18:01:04.486Z" },
{ url = "https://files.pythonhosted.org/packages/a7/d8/7bf71c2c85a0951e406971f151435e0751716907e3924c6c48a2d6dae0db/blake3-1.0.9-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3cbe7f190164896dc3908e920716ee66bc31d40f1a0fb603ed59ac53290fb9cf", size = 591183, upload-time = "2026-06-22T18:01:06.259Z" },
{ url = "https://files.pythonhosted.org/packages/20/85/34c3ea03cc90b2516628494ab3e0a98aec4ca8b04d037840ccd390e480ca/blake3-1.0.9-cp313-cp313-win32.whl", hash = "sha256:508ccaf8f9377cc47e6026c2897fdc37de61faeb1420dc023b6379cc2474eb65", size = 229053, upload-time = "2026-06-22T18:01:07.638Z" },
{ url = "https://files.pythonhosted.org/packages/db/2e/f09e8ed426f360aa2005206466ceab2f707486eb5d9db7051dbcbae056d1/blake3-1.0.9-cp313-cp313-win_amd64.whl", hash = "sha256:caded2806d2cbeed638c5e2517ed8b2a94165b3452fda35e72896142d22070e0", size = 217589, upload-time = "2026-06-22T18:01:08.922Z" },
]
[[package]]
name = "caio"
version = "0.9.24"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/98/04/ec9b6864135032fd454f6cd1d9444e0bb01040196ad0cd776c061fc92c6b/caio-0.9.24.tar.gz", hash = "sha256:5bcdecaea02a9aa8e3acf0364eff8ad9903d57d70cdb274a42270126290a77f1", size = 27174, upload-time = "2025-04-23T16:31:19.191Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5f/76/b33a89dc2516aae045ef509cf2febe7ffb2a36c4eebb8f301a7ef2093385/caio-0.9.24-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7ae3566228383175265a7583107f21a7cb044a752ea29ba84fce7c1a49a05903", size = 42212, upload-time = "2025-04-23T16:31:08.457Z" },
{ url = "https://files.pythonhosted.org/packages/a9/8c/cb62483e69309bbad503c2ace29c4ac3466558a20e9aed840d313e1dcacd/caio-0.9.24-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:a306b0dda91cb4ca3170f066c114597f8ea41b3da578574a9d2b54f86963de68", size = 81517, upload-time = "2025-04-23T16:31:09.686Z" },
{ url = "https://files.pythonhosted.org/packages/64/80/8a8cdfd4b47e06d1e9de6d5431c2603e0741282fa06f757f10c04e619d8f/caio-0.9.24-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:8ee158e56128d865fb7d57a9c9c22fca4e8aa8d8664859c977a36fff3ccb3609", size = 80216, upload-time = "2025-04-23T16:31:10.98Z" },
{ url = "https://files.pythonhosted.org/packages/66/35/06e77837fc5455d330c5502460fc3743989d4ff840b61aa79af3a7ec5b19/caio-0.9.24-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1d47ef8d76aca74c17cb07339a441c5530fc4b8dd9222dfb1e1abd7f9f9b814f", size = 42214, upload-time = "2025-04-23T16:31:12.272Z" },
{ url = "https://files.pythonhosted.org/packages/e0/e2/c16aeaea4b2103e04fdc2e7088ede6313e1971704c87fcd681b58ab1c6b4/caio-0.9.24-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:d15fc746c4bf0077d75df05939d1e97c07ccaa8e580681a77021d6929f65d9f4", size = 81557, upload-time = "2025-04-23T16:31:13.526Z" },
{ url = "https://files.pythonhosted.org/packages/78/3b/adeb0cffe98dbe60661f316ec0060037a5209a5ed8be38ac8e79fdbc856d/caio-0.9.24-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:9368eae0a9badd5f31264896c51b47431d96c0d46f1979018fb1d20c49f56156", size = 80242, upload-time = "2025-04-23T16:31:14.365Z" },
@ -166,6 +158,18 @@ dependencies = [
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
{ url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
{ url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
{ url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
{ url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
{ url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
{ url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
{ url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
{ url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
{ url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
{ url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
{ url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
@ -186,6 +190,22 @@ version = "3.4.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
{ url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
{ url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
{ url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
{ url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
{ url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
{ url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
{ url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
{ url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
{ url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
{ url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
{ url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
{ url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
{ url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
{ url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
{ url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
@ -296,6 +316,10 @@ version = "3.2.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/29/17/55fc687ba986f2210298fa2f60fec265fa3004c3f9a1e958ea1fe2d4e061/cython-3.2.2.tar.gz", hash = "sha256:c3add3d483acc73129a61d105389344d792c17e7c1cee24863f16416bd071634", size = 3275797, upload-time = "2025-11-30T12:48:20.942Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/57/0f/6fd78dc581373722bb9dedfc90c35b59ba88af988756315af227a877c7a2/cython-3.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:692a41c8fe06fb2dc55ca2c8d71c80c469fd16fe69486ed99f3b3cbb2d3af83f", size = 2968037, upload-time = "2025-11-30T12:48:47.279Z" },
{ url = "https://files.pythonhosted.org/packages/b0/52/50b6263c2fbad73aae8911ce54641ee1739d430a0592d3b3510591d7842b/cython-3.2.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:098590c1dc309f8a0406ade031963a95a87714296b425539f9920aebf924560d", size = 3223137, upload-time = "2025-11-30T12:48:48.951Z" },
{ url = "https://files.pythonhosted.org/packages/d6/44/4e34d161674c9162c6eb9ddef0cd69d41d92ae7e6dee3945fed3a6871ebe/cython-3.2.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3898c076e9c458bcb3e4936187919fda5f5365fe4c567d35d2b003444b6f3fe", size = 3390943, upload-time = "2025-11-30T12:48:51.125Z" },
{ url = "https://files.pythonhosted.org/packages/62/8a/ffc2df024c1341737008fbaf0fbea51ef983a7146b43b84a239f197cf005/cython-3.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:2b910b89a2a71004064c5e890b9512a251eda63fae252caa0feb9835057035f9", size = 2756403, upload-time = "2025-11-30T12:48:52.929Z" },
{ url = "https://files.pythonhosted.org/packages/a2/4f/b5355918962ec28b376eb8e357c718d58baf32d6df7017be8d147dd4ba29/cython-3.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa24cd0bdab27ca099b2467806c684404add597c1108e07ddf7b6471653c85d7", size = 2958578, upload-time = "2025-11-30T12:48:55.354Z" },
{ url = "https://files.pythonhosted.org/packages/46/21/a8038c8253e7a5241ed1db6d031bac586f7a502d92f487124abbc3f3e94f/cython-3.2.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60f4aa425e1ff98abf8d965ae7020f06dd2cbc01dbd945137d2f9cca4ff0524a", size = 3212479, upload-time = "2025-11-30T12:48:57.567Z" },
{ url = "https://files.pythonhosted.org/packages/57/c1/76928c07176a4402c74d5b304936ad8ee167dd04a07cf7dca545e8c25f9b/cython-3.2.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a473df474ba89e9fee81ee82b31062a267f9e598096b222783477e56d02ad12c", size = 3374773, upload-time = "2025-11-30T12:48:59.318Z" },
@ -312,15 +336,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/76/f2/98fd8d0b514622a789fd2824b59bd6041b799aaeeba14a8d92d52f6654dd/cython-3.2.2-py3-none-any.whl", hash = "sha256:13b99ecb9482aff6a6c12d1ca6feef6940c507af909914b49f568de74fa965fb", size = 1255106, upload-time = "2025-11-30T12:48:18.454Z" },
]
[[package]]
name = "dnspython"
version = "2.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" },
]
[[package]]
name = "elastic-transport"
version = "8.17.1"
@ -352,6 +367,9 @@ wheels = [
name = "exceptiongroup"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
@ -380,6 +398,22 @@ 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/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" },
{ url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" },
{ url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" },
{ url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" },
{ url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" },
{ url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" },
{ url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" },
{ url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" },
{ url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" },
{ url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" },
{ url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" },
{ url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" },
{ url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" },
{ url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" },
{ url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" },
{ url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" },
{ url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" },
@ -435,6 +469,14 @@ version = "3.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651, upload-time = "2025-12-04T14:49:44.05Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" },
{ url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" },
{ url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" },
{ url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297, upload-time = "2025-12-04T15:07:13.552Z" },
{ url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" },
{ url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" },
{ url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" },
{ url = "https://files.pythonhosted.org/packages/6c/79/3912a94cf27ec503e51ba493692d6db1e3cd8ac7ac52b0b47c8e33d7f4f9/greenlet-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7a34b13d43a6b78abf828a6d0e87d3385680eaf830cd60d20d52f249faabf39", size = 301964, upload-time = "2025-12-04T14:36:58.316Z" },
{ url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" },
{ url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" },
{ url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" },
@ -562,6 +604,11 @@ version = "0.45.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/99/8d/5baf1cef7f9c084fb35a8afbde88074f0d6a727bc63ef764fe0e7543ba40/llvmlite-0.45.1.tar.gz", hash = "sha256:09430bb9d0bb58fc45a45a57c7eae912850bedc095cd0810a57de109c69e1c32", size = 185600, upload-time = "2025-10-01T17:59:52.046Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e2/7c/82cbd5c656e8991bcc110c69d05913be2229302a92acb96109e166ae31fb/llvmlite-0.45.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:28e763aba92fe9c72296911e040231d486447c01d4f90027c8e893d89d49b20e", size = 43043524, upload-time = "2025-10-01T18:03:30.666Z" },
{ url = "https://files.pythonhosted.org/packages/9d/bc/5314005bb2c7ee9f33102c6456c18cc81745d7055155d1218f1624463774/llvmlite-0.45.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1a53f4b74ee9fd30cb3d27d904dadece67a7575198bd80e687ee76474620735f", size = 37253123, upload-time = "2025-10-01T18:04:18.177Z" },
{ url = "https://files.pythonhosted.org/packages/96/76/0f7154952f037cb320b83e1c952ec4a19d5d689cf7d27cb8a26887d7bbc1/llvmlite-0.45.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b3796b1b1e1c14dcae34285d2f4ea488402fbd2c400ccf7137603ca3800864f", size = 56288211, upload-time = "2025-10-01T18:01:24.079Z" },
{ url = "https://files.pythonhosted.org/packages/00/b1/0b581942be2683ceb6862d558979e87387e14ad65a1e4db0e7dd671fa315/llvmlite-0.45.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:779e2f2ceefef0f4368548685f0b4adde34e5f4b457e90391f570a10b348d433", size = 55140958, upload-time = "2025-10-01T18:02:30.482Z" },
{ url = "https://files.pythonhosted.org/packages/33/94/9ba4ebcf4d541a325fd8098ddc073b663af75cc8b065b6059848f7d4dce7/llvmlite-0.45.1-cp312-cp312-win_amd64.whl", hash = "sha256:9e6c9949baf25d9aa9cd7cf0f6d011b9ca660dd17f5ba2b23bdbdb77cc86b116", size = 38132231, upload-time = "2025-10-01T18:05:03.664Z" },
{ url = "https://files.pythonhosted.org/packages/1d/e2/c185bb7e88514d5025f93c6c4092f6120c6cea8fe938974ec9860fb03bbb/llvmlite-0.45.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:d9ea9e6f17569a4253515cc01dade70aba536476e3d750b2e18d81d7e670eb15", size = 43043524, upload-time = "2025-10-01T18:03:43.249Z" },
{ url = "https://files.pythonhosted.org/packages/09/b8/b5437b9ecb2064e89ccf67dccae0d02cd38911705112dd0dcbfa9cd9a9de/llvmlite-0.45.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:c9f3cadee1630ce4ac18ea38adebf2a4f57a89bd2740ce83746876797f6e0bfb", size = 37253121, upload-time = "2025-10-01T18:04:30.557Z" },
{ url = "https://files.pythonhosted.org/packages/f7/97/ad1a907c0173a90dd4df7228f24a3ec61058bc1a9ff8a0caec20a0cc622e/llvmlite-0.45.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:57c48bf2e1083eedbc9406fb83c4e6483017879714916fe8be8a72a9672c995a", size = 56288210, upload-time = "2025-10-01T18:01:40.26Z" },
@ -590,76 +637,26 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
[[package]]
name = "mmh3"
version = "5.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/aa/69/d00269fee7a3102fcc0f04f0a312e41c6b237762bdcad4c19426f18e697c/mmh3-5.3.0.tar.gz", hash = "sha256:95832419b87b882bec9dcd7d041d74887ba7745b3659c14be1ae1db5cfa35cad", size = 33607, upload-time = "2026-08-26T04:58:20.042Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/fa/e07f6b9e2fba550fe539b6c66ee7fc28e44f5bd445a7203ae4c169d4aa72/mmh3-5.3.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:6576400e7a748ec5c7ea72f38d626939876dd1756f4a0ccf552b8646dcf6f3e9", size = 41040, upload-time = "2026-08-26T04:56:29.766Z" },
{ url = "https://files.pythonhosted.org/packages/62/9c/cb0f23e71bddcc519331c9787ec029e0d2fef64ed1bc490ad84b00a43950/mmh3-5.3.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:2b4cd2fcf1b517872530d9ef1a2de2ef9b86e5a0f8927539ea0b68337618244e", size = 42153, upload-time = "2026-08-26T04:56:30.954Z" },
{ url = "https://files.pythonhosted.org/packages/27/1c/99f2ff480922046a496c2e53728a13a467b68f01a0f48370577c0825a763/mmh3-5.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:f401a82d80c53d88605b82a80623edd95d922732d2c513c1c5f8e4b5e10c2913", size = 38796, upload-time = "2026-08-26T04:56:32.207Z" },
{ url = "https://files.pythonhosted.org/packages/ad/05/e137452583b33a56d053ad48643a7c56e4cf466efc8a145da6a736913ae3/mmh3-5.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:86238bb78ff65c9fc1e6b371b78f271e23c5d61898222c201122209dc8eadc76", size = 39474, upload-time = "2026-08-26T04:56:33.196Z" },
{ url = "https://files.pythonhosted.org/packages/74/e8/a9031fe6ed0eb06ab1c98eb76182eb01dc484dc46873a8e5ef3097d9bdf4/mmh3-5.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:222ea0a485e23bdcb29e28d15b8b01ebe34e8720bad4b5f92b645ed86e3fc715", size = 39983, upload-time = "2026-08-26T04:56:34.195Z" },
{ url = "https://files.pythonhosted.org/packages/f9/ae/11459b39af5341de2a048998a717f57b3a1c4e6a9edf8fe09e314b2c263c/mmh3-5.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d95ee6696aa5b7283f4a27b67eb7db1c4fb5bb7a9117205d29ebaaa7f6294d7b", size = 55464, upload-time = "2026-08-26T04:56:35.23Z" },
{ url = "https://files.pythonhosted.org/packages/49/b7/841b580415a614a3ad836db1cb8d57c425bbeb717c80263a9d979b1a4eac/mmh3-5.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e2f439ffd4fd7d64b77f6a287d4605700bad26fe12bb1b63b4ee45211344e2fc", size = 40084, upload-time = "2026-08-26T04:56:36.44Z" },
{ url = "https://files.pythonhosted.org/packages/02/f6/f5cb0e2f7bb7df876847dab63ee984c3f6173569d5b892edc04bc797c1b7/mmh3-5.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f9cb34c661454f73432112a81ac522ebe69500feeb8d77f744f6bd3e8b2f2ba", size = 39693, upload-time = "2026-08-26T04:56:37.428Z" },
{ url = "https://files.pythonhosted.org/packages/b5/61/9506d0b30d7388846cc3b884ec63b8d67f3ad2d521e61775315111e70ec3/mmh3-5.3.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b6b7397804e9299bd0c01ea426245fa3d730d3e9c31f583f51aa87bed399c481", size = 97247, upload-time = "2026-08-26T04:56:38.439Z" },
{ url = "https://files.pythonhosted.org/packages/da/6f/c6a4acf4715fede1e0acb17d6080ebe9b88290d113bbf9513a8728c65b4d/mmh3-5.3.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4a19b00097fcc8e3008bb006cd6bfaf0544e9fa2abc4cc77fbad57971a37dcc0", size = 103256, upload-time = "2026-08-26T04:56:39.52Z" },
{ url = "https://files.pythonhosted.org/packages/a3/2a/f4fe4ebf2e44f49953ed1ee6b90d329d6843ebb949e60e881dbfde84e17d/mmh3-5.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83d93abf6a68d54b4e2c4c041ffcffeb94b1c9ab3171443fda3f5f19024be517", size = 106172, upload-time = "2026-08-26T04:56:40.67Z" },
{ url = "https://files.pythonhosted.org/packages/1b/61/23168dc6fa92e1d9b57905b89c694a521a00570f0fe325bc3f6422fe6119/mmh3-5.3.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:598350a6adefb5799c800fd00bbeacbc115ee560e2fd7b35f703608c1037a2ed", size = 113020, upload-time = "2026-08-26T04:56:42.035Z" },
{ url = "https://files.pythonhosted.org/packages/6e/f6/68dbb5461727bb14fbca2342a7bba4521400d679d64e00dd98a992eb0be4/mmh3-5.3.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c0772d6bba19a5601d24b3c6ce6627484fd5a3fd1d402913e1578b1447d51a0", size = 120648, upload-time = "2026-08-26T04:56:43.339Z" },
{ url = "https://files.pythonhosted.org/packages/92/18/a879ce4c26e8c1741b575e6c854b2323fd7e116000965c8904428e66fba6/mmh3-5.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00105934e7d52f80b4364282918c37e2cc3cf9868ef4052016cbc39d8711c3f4", size = 99034, upload-time = "2026-08-26T04:56:44.771Z" },
{ url = "https://files.pythonhosted.org/packages/1b/28/b0548d78133f8e79bb16d6487f2df504ff4e16ea1330954f26be83b645ca/mmh3-5.3.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:99a5b0a908beb01b0e134b7b085d0ea6bfb7ed28ba3ed0737365aa2ce9bda0e4", size = 98587, upload-time = "2026-08-26T04:56:46.089Z" },
{ url = "https://files.pythonhosted.org/packages/97/10/abfa952c6a443d07efb286b53b439d4418f7cfbac49fca8974cd78c17427/mmh3-5.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:da4ad7a0d4c589069c46101dcb55ee304616293bcf614f4c445b3ecc961fa836", size = 106407, upload-time = "2026-08-26T04:56:47.16Z" },
{ url = "https://files.pythonhosted.org/packages/a6/61/3b974a1cfe683c6cb01d2d3f2d5eddfca7e0a175d976d2307269143f4e0e/mmh3-5.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:8962a67c314f1da82957aee5b940698aaffff13e41b3298baa59d30cbddb23e2", size = 109922, upload-time = "2026-08-26T04:56:48.451Z" },
{ url = "https://files.pythonhosted.org/packages/0e/d2/ac7e18d6fcee3b26e06b3d33ecab4459b7aa6aff9859b13eb13e353a69ab/mmh3-5.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f61f2850b318c043961662f6cdd08e69b05f1d25d0e321782a3995d39f811548", size = 97456, upload-time = "2026-08-26T04:56:49.559Z" },
{ url = "https://files.pythonhosted.org/packages/ee/80/7911629b930d46ef8cb3165bce44f021d31361a2d7c56e35efe7b0d90493/mmh3-5.3.0-cp313-cp313-win32.whl", hash = "sha256:d7eec1b09bde3a9b6e2102717a587b9c9a96c360a1ef478b5668414619cac606", size = 40458, upload-time = "2026-08-26T04:56:50.785Z" },
{ url = "https://files.pythonhosted.org/packages/45/c7/138d77c740f9d33ceffa968786fe23abe24ce442c210772b5b7cb6e0c198/mmh3-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:b1829bfe98d1f6e7bd79646b78e73dcef92c5aa32aaa622b9e07bf39df98c9b5", size = 41879, upload-time = "2026-08-26T04:56:51.785Z" },
{ url = "https://files.pythonhosted.org/packages/8b/e1/4312e5c18c8ef4060c9b50f1a645c9af7c3b79fad012c18fbe64a1b17103/mmh3-5.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:3fb0d4918b7d827ac804069849fde03d516628cbbf7bffe0b957ba6f1440cca6", size = 39203, upload-time = "2026-08-26T04:56:52.746Z" },
]
[[package]]
name = "morphys"
version = "1.0"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/4f/cb781d0ac5d079adabc77dc4f0bc99fc81c390029bd33c6e70552139e762/morphys-1.0-py2.py3-none-any.whl", hash = "sha256:76d6dbaa4d65f597e59d332c81da786d83e4669387b9b2a750cfec74e7beec20", size = 5618, upload-time = "2017-01-10T20:08:56.872Z" },
]
[[package]]
name = "msgspec"
version = "0.21.1"
version = "0.19.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e3/60/f79b9b013a16fa3a58350c9295ddc6789f2e335f36ea61ed10a21b215364/msgspec-0.21.1.tar.gz", hash = "sha256:2313508e394b0d208f8f56892ca9b2799e2561329de9763b19619595a6c0f72c", size = 319193, upload-time = "2026-04-12T21:44:50.394Z" }
sdist = { url = "https://files.pythonhosted.org/packages/cf/9b/95d8ce458462b8b71b8a70fa94563b2498b89933689f3a7b8911edfae3d7/msgspec-0.19.0.tar.gz", hash = "sha256:604037e7cd475345848116e89c553aa9a233259733ab51986ac924ab1b976f8e", size = 216934, upload-time = "2024-12-27T17:40:28.597Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/74/f11ede02839b19ff459f88e3145df5d711626ca84da4e23520cebf819367/msgspec-0.21.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:764173717a01743f007e9f74520ed281f24672c604514f7d76c1c3a10e8edb66", size = 196176, upload-time = "2026-04-12T21:44:17.613Z" },
{ url = "https://files.pythonhosted.org/packages/bb/40/4476c1bd341418a046c4955aff632ec769315d1e3cb94e6acf86d461f9ed/msgspec-0.21.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:344c7cd0eaed1fb81d7959f99100ef71ec9b536881a376f11b9a6c4803365697", size = 188524, upload-time = "2026-04-12T21:44:18.815Z" },
{ url = "https://files.pythonhosted.org/packages/ca/d9/9e9d7d7e5061b47540d03d640fab9b3965ba7ae49c1b2154861c8f007518/msgspec-0.21.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48943e278b3854c2f89f955ddc6f9f430d3f0784b16e47d10604ee0463cd21f5", size = 218880, upload-time = "2026-04-12T21:44:20.028Z" },
{ url = "https://files.pythonhosted.org/packages/74/66/2bb344f34abb4b57e60c7c9c761994e0417b9718ec1460bf00c296f2a7ea/msgspec-0.21.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9aa659ebb0101b1cbc31461212b87e341d961f0ab0772aaf068a99e001ec4aa", size = 225050, upload-time = "2026-04-12T21:44:21.577Z" },
{ url = "https://files.pythonhosted.org/packages/1a/84/7c1e412f76092277bf760cef12b7979d03314d259ab5b5cafde5d0c1722d/msgspec-0.21.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7b27d1a8ead2b6f5b0c4f2d07b8be1ccfcc041c8a0e704781edebe3ae13c484", size = 222713, upload-time = "2026-04-12T21:44:22.83Z" },
{ url = "https://files.pythonhosted.org/packages/4e/27/0bba04b2b4ef05f3d068429410bc71d2cea925f1596a8f41152cccd5edb8/msgspec-0.21.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:38fe93e86b61328fe544cb7fd871fad5a27c8734bfda90f65e5dbe288ae50f61", size = 227259, upload-time = "2026-04-12T21:44:24.11Z" },
{ url = "https://files.pythonhosted.org/packages/b0/2d/09574b0eea02fed2c2c1383dbaae2c7f79dc16dcd6487a886000afb5d7c4/msgspec-0.21.1-cp313-cp313-win_amd64.whl", hash = "sha256:8bc666331c35fcce05a7cd2d6221adbe0f6058f8e750711413d22793c080ac6a", size = 189857, upload-time = "2026-04-12T21:44:25.359Z" },
{ url = "https://files.pythonhosted.org/packages/46/34/105b1576ad182879914f0c821f17ee1d13abb165cb060448f96fe2aff078/msgspec-0.21.1-cp313-cp313-win_arm64.whl", hash = "sha256:42bb1241e0750c1a4346f2aa84db26c5ffd99a4eb3a954927d9f149ff2f42898", size = 175403, upload-time = "2026-04-12T21:44:26.608Z" },
]
[[package]]
name = "multiaddr"
version = "0.2.0"
source = { git = "https://github.com/multiformats/py-multiaddr.git?rev=f86519daaa21699023d0037c58cdff600313dd09#f86519daaa21699023d0037c58cdff600313dd09" }
dependencies = [
{ name = "base58" },
{ name = "dnspython" },
{ name = "idna" },
{ name = "netaddr" },
{ name = "psutil" },
{ name = "py-cid" },
{ name = "py-multibase" },
{ name = "py-multicodec" },
{ name = "py-multihash" },
{ name = "trio" },
{ name = "trio-typing" },
{ name = "varint" },
{ url = "https://files.pythonhosted.org/packages/b2/5f/a70c24f075e3e7af2fae5414c7048b0e11389685b7f717bb55ba282a34a7/msgspec-0.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f98bd8962ad549c27d63845b50af3f53ec468b6318400c9f1adfe8b092d7b62f", size = 190485, upload-time = "2024-12-27T17:39:44.974Z" },
{ url = "https://files.pythonhosted.org/packages/89/b0/1b9763938cfae12acf14b682fcf05c92855974d921a5a985ecc197d1c672/msgspec-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:43bbb237feab761b815ed9df43b266114203f53596f9b6e6f00ebd79d178cdf2", size = 183910, upload-time = "2024-12-27T17:39:46.401Z" },
{ url = "https://files.pythonhosted.org/packages/87/81/0c8c93f0b92c97e326b279795f9c5b956c5a97af28ca0fbb9fd86c83737a/msgspec-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cfc033c02c3e0aec52b71710d7f84cb3ca5eb407ab2ad23d75631153fdb1f12", size = 210633, upload-time = "2024-12-27T17:39:49.099Z" },
{ url = "https://files.pythonhosted.org/packages/d0/ef/c5422ce8af73928d194a6606f8ae36e93a52fd5e8df5abd366903a5ca8da/msgspec-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d911c442571605e17658ca2b416fd8579c5050ac9adc5e00c2cb3126c97f73bc", size = 213594, upload-time = "2024-12-27T17:39:51.204Z" },
{ url = "https://files.pythonhosted.org/packages/19/2b/4137bc2ed45660444842d042be2cf5b18aa06efd2cda107cff18253b9653/msgspec-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:757b501fa57e24896cf40a831442b19a864f56d253679f34f260dcb002524a6c", size = 214053, upload-time = "2024-12-27T17:39:52.866Z" },
{ url = "https://files.pythonhosted.org/packages/9d/e6/8ad51bdc806aac1dc501e8fe43f759f9ed7284043d722b53323ea421c360/msgspec-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5f0f65f29b45e2816d8bded36e6b837a4bf5fb60ec4bc3c625fa2c6da4124537", size = 219081, upload-time = "2024-12-27T17:39:55.142Z" },
{ url = "https://files.pythonhosted.org/packages/b1/ef/27dd35a7049c9a4f4211c6cd6a8c9db0a50647546f003a5867827ec45391/msgspec-0.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:067f0de1c33cfa0b6a8206562efdf6be5985b988b53dd244a8e06f993f27c8c0", size = 187467, upload-time = "2024-12-27T17:39:56.531Z" },
{ url = "https://files.pythonhosted.org/packages/3c/cb/2842c312bbe618d8fefc8b9cedce37f773cdc8fa453306546dba2c21fd98/msgspec-0.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f12d30dd6266557aaaf0aa0f9580a9a8fbeadfa83699c487713e355ec5f0bd86", size = 190498, upload-time = "2024-12-27T17:40:00.427Z" },
{ url = "https://files.pythonhosted.org/packages/58/95/c40b01b93465e1a5f3b6c7d91b10fb574818163740cc3acbe722d1e0e7e4/msgspec-0.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82b2c42c1b9ebc89e822e7e13bbe9d17ede0c23c187469fdd9505afd5a481314", size = 183950, upload-time = "2024-12-27T17:40:04.219Z" },
{ url = "https://files.pythonhosted.org/packages/e8/f0/5b764e066ce9aba4b70d1db8b087ea66098c7c27d59b9dd8a3532774d48f/msgspec-0.19.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19746b50be214a54239aab822964f2ac81e38b0055cca94808359d779338c10e", size = 210647, upload-time = "2024-12-27T17:40:05.606Z" },
{ url = "https://files.pythonhosted.org/packages/9d/87/bc14f49bc95c4cb0dd0a8c56028a67c014ee7e6818ccdce74a4862af259b/msgspec-0.19.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60ef4bdb0ec8e4ad62e5a1f95230c08efb1f64f32e6e8dd2ced685bcc73858b5", size = 213563, upload-time = "2024-12-27T17:40:10.516Z" },
{ url = "https://files.pythonhosted.org/packages/53/2f/2b1c2b056894fbaa975f68f81e3014bb447516a8b010f1bed3fb0e016ed7/msgspec-0.19.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac7f7c377c122b649f7545810c6cd1b47586e3aa3059126ce3516ac7ccc6a6a9", size = 213996, upload-time = "2024-12-27T17:40:12.244Z" },
{ url = "https://files.pythonhosted.org/packages/aa/5a/4cd408d90d1417e8d2ce6a22b98a6853c1b4d7cb7669153e4424d60087f6/msgspec-0.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5bc1472223a643f5ffb5bf46ccdede7f9795078194f14edd69e3aab7020d327", size = 219087, upload-time = "2024-12-27T17:40:14.881Z" },
{ url = "https://files.pythonhosted.org/packages/23/d8/f15b40611c2d5753d1abb0ca0da0c75348daf1252220e5dda2867bd81062/msgspec-0.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:317050bc0f7739cb30d257ff09152ca309bf5a369854bbf1e57dffc310c1f20f", size = 187432, upload-time = "2024-12-27T17:40:16.256Z" },
]
[[package]]
@ -668,6 +665,24 @@ version = "6.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877, upload-time = "2025-10-06T14:49:20.884Z" },
{ url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467, upload-time = "2025-10-06T14:49:22.054Z" },
{ url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834, upload-time = "2025-10-06T14:49:23.566Z" },
{ url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545, upload-time = "2025-10-06T14:49:24.882Z" },
{ url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305, upload-time = "2025-10-06T14:49:26.778Z" },
{ url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363, upload-time = "2025-10-06T14:49:28.562Z" },
{ url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375, upload-time = "2025-10-06T14:49:29.96Z" },
{ url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346, upload-time = "2025-10-06T14:49:31.404Z" },
{ url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107, upload-time = "2025-10-06T14:49:32.974Z" },
{ url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592, upload-time = "2025-10-06T14:49:34.52Z" },
{ url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024, upload-time = "2025-10-06T14:49:35.956Z" },
{ url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484, upload-time = "2025-10-06T14:49:37.631Z" },
{ url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579, upload-time = "2025-10-06T14:49:39.502Z" },
{ url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654, upload-time = "2025-10-06T14:49:41.32Z" },
{ url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511, upload-time = "2025-10-06T14:49:46.021Z" },
{ url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895, upload-time = "2025-10-06T14:49:48.718Z" },
{ url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073, upload-time = "2025-10-06T14:49:50.28Z" },
{ url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226, upload-time = "2025-10-06T14:49:52.304Z" },
{ url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135, upload-time = "2025-10-06T14:49:54.26Z" },
{ url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117, upload-time = "2025-10-06T14:49:55.82Z" },
{ url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472, upload-time = "2025-10-06T14:49:57.048Z" },
@ -725,15 +740,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" },
]
[[package]]
name = "netaddr"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/54/90/188b2a69654f27b221fba92fda7217778208532c962509e959a9cee5229d/netaddr-1.3.0.tar.gz", hash = "sha256:5c3c3d9895b551b763779ba7db7a03487dc1f8e3b385af819af341ae9ef6e48a", size = 2260504, upload-time = "2024-05-28T21:30:37.743Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/cc/f4fe2c7ce68b92cbf5b2d379ca366e1edae38cccaad00f69f529b460c3ef/netaddr-1.3.0-py3-none-any.whl", hash = "sha256:c2c6a8ebe5554ce33b7d5b3a306b71bbb373e000bbbf2350dd5213cc56e3dbbe", size = 2262023, upload-time = "2024-05-28T21:30:34.191Z" },
]
[[package]]
name = "numba"
version = "0.62.1"
@ -744,6 +750,11 @@ dependencies = [
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/20/33dbdbfe60e5fd8e3dbfde299d106279a33d9f8308346022316781368591/numba-0.62.1.tar.gz", hash = "sha256:7b774242aa890e34c21200a1fc62e5b5757d5286267e71103257f4e2af0d5161", size = 2749817, upload-time = "2025-09-29T10:46:31.551Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5e/fa/30fa6873e9f821c0ae755915a3ca444e6ff8d6a7b6860b669a3d33377ac7/numba-0.62.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:1b743b32f8fa5fff22e19c2e906db2f0a340782caf024477b97801b918cf0494", size = 2685346, upload-time = "2025-09-29T10:43:43.677Z" },
{ url = "https://files.pythonhosted.org/packages/a9/d5/504ce8dc46e0dba2790c77e6b878ee65b60fe3e7d6d0006483ef6fde5a97/numba-0.62.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:90fa21b0142bcf08ad8e32a97d25d0b84b1e921bc9423f8dda07d3652860eef6", size = 2688139, upload-time = "2025-09-29T10:44:04.894Z" },
{ url = "https://files.pythonhosted.org/packages/50/5f/6a802741176c93f2ebe97ad90751894c7b0c922b52ba99a4395e79492205/numba-0.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6ef84d0ac19f1bf80431347b6f4ce3c39b7ec13f48f233a48c01e2ec06ecbc59", size = 3796453, upload-time = "2025-09-29T10:42:52.771Z" },
{ url = "https://files.pythonhosted.org/packages/7e/df/efd21527d25150c4544eccc9d0b7260a5dec4b7e98b5a581990e05a133c0/numba-0.62.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9315cc5e441300e0ca07c828a627d92a6802bcbf27c5487f31ae73783c58da53", size = 3496451, upload-time = "2025-09-29T10:43:19.279Z" },
{ url = "https://files.pythonhosted.org/packages/80/44/79bfdab12a02796bf4f1841630355c82b5a69933b1d50eb15c7fa37dabe8/numba-0.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:44e3aa6228039992f058f5ebfcfd372c83798e9464297bdad8cc79febcf7891e", size = 2745552, upload-time = "2025-09-29T10:44:26.399Z" },
{ url = "https://files.pythonhosted.org/packages/22/76/501ea2c07c089ef1386868f33dff2978f43f51b854e34397b20fc55e0a58/numba-0.62.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:b72489ba8411cc9fdcaa2458d8f7677751e94f0109eeb53e5becfdc818c64afb", size = 2685766, upload-time = "2025-09-29T10:43:49.161Z" },
{ url = "https://files.pythonhosted.org/packages/80/68/444986ed95350c0611d5c7b46828411c222ce41a0c76707c36425d27ce29/numba-0.62.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:44a1412095534a26fb5da2717bc755b57da5f3053965128fe3dc286652cc6a92", size = 2688741, upload-time = "2025-09-29T10:44:10.07Z" },
{ url = "https://files.pythonhosted.org/packages/78/7e/bf2e3634993d57f95305c7cee4c9c6cb3c9c78404ee7b49569a0dfecfe33/numba-0.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8c9460b9e936c5bd2f0570e20a0a5909ee6e8b694fd958b210e3bde3a6dba2d7", size = 3804576, upload-time = "2025-09-29T10:42:59.53Z" },
@ -757,6 +768,17 @@ version = "2.3.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" },
{ url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" },
{ url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" },
{ url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" },
{ url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" },
{ url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" },
{ url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" },
{ url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" },
{ url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" },
{ url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" },
{ url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" },
{ url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" },
{ url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652, upload-time = "2025-11-16T22:50:21.487Z" },
{ url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172, upload-time = "2025-11-16T22:50:24.562Z" },
@ -786,6 +808,10 @@ name = "order-book"
version = "0.6.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/7e/a118618ff5cb05040a27f3f1c6325c7324e998a90d3635b6f4b557f89f06/order_book-0.6.1.tar.gz", hash = "sha256:636cee1e6b9b74afd623797ff62a8decbf6de28db398d4a818ff9e73128f9f21", size = 34054, upload-time = "2024-04-23T00:15:05.66Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/4c/1b9854b19dc9738d6398786089f5f7a411d28b55b3942ac950a6f7e3ea03/order_book-0.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2c64369d4404614b5a7fdd3687c52e79515d9bac737d70b222527554930542e", size = 26747, upload-time = "2024-04-23T00:15:04.289Z" },
{ url = "https://files.pythonhosted.org/packages/9b/b4/8e4cf9fe1c9c73ce21963b6d5c1005551d4fa6bb4093cadad4c0875574d1/order_book-0.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:285e4046751e2aaf0edc3b180fc229c2b316723d9332bcac4575a84ae90090b6", size = 66768, upload-time = "2024-04-23T00:14:46.898Z" },
]
[[package]]
name = "outcome"
@ -819,6 +845,14 @@ dependencies = [
]
sdist = { url = "https://files.pythonhosted.org/packages/de/da/b1dc0481ab8d55d0f46e343cfe67d4551a0e14fcee52bd38ca1bd73258d8/pandas-3.0.0.tar.gz", hash = "sha256:0facf7e87d38f721f0af46fe70d97373a37701b1c09f7ed7aeeb292ade5c050f", size = 4633005, upload-time = "2026-01-21T15:52:04.726Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/38/db33686f4b5fa64d7af40d96361f6a4615b8c6c8f1b3d334eee46ae6160e/pandas-3.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9803b31f5039b3c3b10cc858c5e40054adb4b29b4d81cb2fd789f4121c8efbcd", size = 10334013, upload-time = "2026-01-21T15:50:34.771Z" },
{ url = "https://files.pythonhosted.org/packages/a5/7b/9254310594e9774906bacdd4e732415e1f86ab7dbb4b377ef9ede58cd8ec/pandas-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14c2a4099cd38a1d18ff108168ea417909b2dea3bd1ebff2ccf28ddb6a74d740", size = 9874154, upload-time = "2026-01-21T15:50:36.67Z" },
{ url = "https://files.pythonhosted.org/packages/63/d4/726c5a67a13bc66643e66d2e9ff115cead482a44fc56991d0c4014f15aaf/pandas-3.0.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d257699b9a9960e6125686098d5714ac59d05222bef7a5e6af7a7fd87c650801", size = 10384433, upload-time = "2026-01-21T15:50:39.132Z" },
{ url = "https://files.pythonhosted.org/packages/bf/2e/9211f09bedb04f9832122942de8b051804b31a39cfbad199a819bb88d9f3/pandas-3.0.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:69780c98f286076dcafca38d8b8eee1676adf220199c0a39f0ecbf976b68151a", size = 10864519, upload-time = "2026-01-21T15:50:41.043Z" },
{ url = "https://files.pythonhosted.org/packages/00/8d/50858522cdc46ac88b9afdc3015e298959a70a08cd21e008a44e9520180c/pandas-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4a66384f017240f3858a4c8a7cf21b0591c3ac885cddb7758a589f0f71e87ebb", size = 11394124, upload-time = "2026-01-21T15:50:43.377Z" },
{ url = "https://files.pythonhosted.org/packages/86/3f/83b2577db02503cd93d8e95b0f794ad9d4be0ba7cb6c8bcdcac964a34a42/pandas-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be8c515c9bc33989d97b89db66ea0cececb0f6e3c2a87fcc8b69443a6923e95f", size = 11920444, upload-time = "2026-01-21T15:50:45.932Z" },
{ url = "https://files.pythonhosted.org/packages/64/2d/4f8a2f192ed12c90a0aab47f5557ece0e56b0370c49de9454a09de7381b2/pandas-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:a453aad8c4f4e9f166436994a33884442ea62aa8b27d007311e87521b97246e1", size = 9730970, upload-time = "2026-01-21T15:50:47.962Z" },
{ url = "https://files.pythonhosted.org/packages/d4/64/ff571be435cf1e643ca98d0945d76732c0b4e9c37191a89c8550b105eed1/pandas-3.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:da768007b5a33057f6d9053563d6b74dd6d029c337d93c6d0d22a763a5c2ecc0", size = 9041950, upload-time = "2026-01-21T15:50:50.422Z" },
{ url = "https://files.pythonhosted.org/packages/6f/fa/7f0ac4ca8877c57537aaff2a842f8760e630d8e824b730eb2e859ffe96ca/pandas-3.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b78d646249b9a2bc191040988c7bb524c92fa8534fb0898a0741d7e6f2ffafa6", size = 10307129, upload-time = "2026-01-21T15:50:52.877Z" },
{ url = "https://files.pythonhosted.org/packages/6f/11/28a221815dcea4c0c9414dfc845e34a84a6a7dabc6da3194498ed5ba4361/pandas-3.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bc9cba7b355cb4162442a88ce495e01cb605f17ac1e27d6596ac963504e0305f", size = 9850201, upload-time = "2026-01-21T15:50:54.807Z" },
{ url = "https://files.pythonhosted.org/packages/ba/da/53bbc8c5363b7e5bd10f9ae59ab250fc7a382ea6ba08e4d06d8694370354/pandas-3.0.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c9a1a149aed3b6c9bf246033ff91e1b02d529546c5d6fb6b74a28fea0cf4c70", size = 10354031, upload-time = "2026-01-21T15:50:57.463Z" },
@ -860,6 +894,16 @@ dependencies = [
]
sdist = { url = "https://files.pythonhosted.org/packages/23/7c/009c12b86c7cc6c403aec80f8a4308598dfc5995e5c523a5491faaa3952e/pendulum-3.1.0.tar.gz", hash = "sha256:66f96303560f41d097bee7d2dc98ffca716fbb3a832c4b3062034c2d45865015", size = 85930, upload-time = "2025-04-19T14:30:01.675Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7a/d7/b1bfe15a742f2c2713acb1fdc7dc3594ff46ef9418ac6a96fcb12a6ba60b/pendulum-3.1.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:4dfd53e7583ccae138be86d6c0a0b324c7547df2afcec1876943c4d481cf9608", size = 336209, upload-time = "2025-04-19T14:01:27.815Z" },
{ url = "https://files.pythonhosted.org/packages/eb/87/0392da0c603c828b926d9f7097fbdddaafc01388cb8a00888635d04758c3/pendulum-3.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6a6e06a28f3a7d696546347805536f6f38be458cb79de4f80754430696bea9e6", size = 323130, upload-time = "2025-04-19T14:01:29.336Z" },
{ url = "https://files.pythonhosted.org/packages/c0/61/95f1eec25796be6dddf71440ee16ec1fd0c573fc61a73bd1ef6daacd529a/pendulum-3.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e68d6a51880708084afd8958af42dc8c5e819a70a6c6ae903b1c4bfc61e0f25", size = 341509, upload-time = "2025-04-19T14:01:31.1Z" },
{ url = "https://files.pythonhosted.org/packages/b5/7b/eb0f5e6aa87d5e1b467a1611009dbdc92f0f72425ebf07669bfadd8885a6/pendulum-3.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e3f1e5da39a7ea7119efda1dd96b529748c1566f8a983412d0908455d606942", size = 378674, upload-time = "2025-04-19T14:01:32.974Z" },
{ url = "https://files.pythonhosted.org/packages/29/68/5a4c1b5de3e54e16cab21d2ec88f9cd3f18599e96cc90a441c0b0ab6b03f/pendulum-3.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9af1e5eeddb4ebbe1b1c9afb9fd8077d73416ade42dd61264b3f3b87742e0bb", size = 436133, upload-time = "2025-04-19T14:01:34.349Z" },
{ url = "https://files.pythonhosted.org/packages/87/5d/f7a1d693e5c0f789185117d5c1d5bee104f5b0d9fbf061d715fb61c840a8/pendulum-3.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20f74aa8029a42e327bfc150472e0e4d2358fa5d795f70460160ba81b94b6945", size = 351232, upload-time = "2025-04-19T14:01:35.669Z" },
{ url = "https://files.pythonhosted.org/packages/30/77/c97617eb31f1d0554edb073201a294019b9e0a9bd2f73c68e6d8d048cd6b/pendulum-3.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:cf6229e5ee70c2660148523f46c472e677654d0097bec010d6730f08312a4931", size = 521562, upload-time = "2025-04-19T14:01:37.05Z" },
{ url = "https://files.pythonhosted.org/packages/76/22/0d0ef3393303877e757b848ecef8a9a8c7627e17e7590af82d14633b2cd1/pendulum-3.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:350cabb23bf1aec7c7694b915d3030bff53a2ad4aeabc8c8c0d807c8194113d6", size = 523221, upload-time = "2025-04-19T14:01:38.444Z" },
{ url = "https://files.pythonhosted.org/packages/99/f3/aefb579aa3cebd6f2866b205fc7a60d33e9a696e9e629024752107dc3cf5/pendulum-3.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:42959341e843077c41d47420f28c3631de054abd64da83f9b956519b5c7a06a7", size = 260502, upload-time = "2025-04-19T14:01:39.814Z" },
{ url = "https://files.pythonhosted.org/packages/02/74/4332b5d6e34c63d4df8e8eab2249e74c05513b1477757463f7fdca99e9be/pendulum-3.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:006758e2125da2e624493324dfd5d7d1b02b0c44bc39358e18bf0f66d0767f5f", size = 253089, upload-time = "2025-04-19T14:01:41.171Z" },
{ url = "https://files.pythonhosted.org/packages/8e/1f/af928ba4aa403dac9569f787adcf024005e7654433d71f7a84e608716837/pendulum-3.1.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:28658b0baf4b30eb31d096a375983cfed033e60c0a7bbe94fa23f06cd779b50b", size = 336209, upload-time = "2025-04-19T14:01:42.775Z" },
{ url = "https://files.pythonhosted.org/packages/b6/16/b010643007ba964c397da7fa622924423883c1bbff1a53f9d1022cd7f024/pendulum-3.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b114dcb99ce511cb8f5495c7b6f0056b2c3dba444ef1ea6e48030d7371bd531a", size = 323132, upload-time = "2025-04-19T14:01:44.577Z" },
{ url = "https://files.pythonhosted.org/packages/64/19/c3c47aeecb5d9bceb0e89faafd800d39809b696c5b7bba8ec8370ad5052c/pendulum-3.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2404a6a54c80252ea393291f0b7f35525a61abae3d795407f34e118a8f133a18", size = 341509, upload-time = "2025-04-19T14:01:46.084Z" },
@ -977,11 +1021,11 @@ requires-dist = [
{ name = "exchange-calendars", specifier = ">=4.13.1" },
{ name = "httpx", specifier = ">=0.27.0,<0.28.0" },
{ name = "ib-async", specifier = ">=2.1.0" },
{ name = "msgspec", specifier = ">=0.20.0" },
{ name = "msgspec", specifier = ">=0.19.0,<0.20" },
{ name = "numba", specifier = ">=0.61.0" },
{ name = "numpy", specifier = ">=2.0" },
{ name = "pendulum" },
{ name = "polars", specifier = ">=0.20.6,<2.0.0" },
{ name = "polars", specifier = ">=0.20.6" },
{ name = "polars-fuzzy-match", specifier = ">=0.1.5" },
{ name = "pyarrow", specifier = ">=18.0.0" },
{ name = "pygments", specifier = ">=2.16.1,<3.0.0" },
@ -990,7 +1034,7 @@ requires-dist = [
{ name = "tomli", specifier = ">=2.0.1,<3.0.0" },
{ name = "tomli-w", specifier = ">=1.0.0,<2.0.0" },
{ name = "tomlkit", git = "https://github.com/pikers/tomlkit.git?branch=piker_pin" },
{ name = "tractor", git = "https://github.com/goodboy/tractor.git?branch=piker_pin" },
{ name = "tractor", git = "https://github.com/goodboy/tractor.git?branch=main" },
{ name = "trio", specifier = ">=0.27" },
{ name = "trio-typing", specifier = ">=0.10.0" },
{ name = "trio-util", specifier = ">=0.7.0,<0.8.0" },
@ -1015,7 +1059,7 @@ dev = [
{ name = "pytest" },
{ name = "qdarkstyle", specifier = ">=3.0.2,<4.0.0" },
{ name = "rapidfuzz", specifier = ">=3.2.0,<4.0.0" },
{ name = "xonsh", specifier = ">=0.24.2" },
{ name = "xonsh", specifier = ">=0.22.2" },
]
lint = [{ name = "ruff", specifier = ">=0.9.6" }]
repl = [
@ -1024,7 +1068,7 @@ repl = [
{ name = "pexpect", specifier = ">=4.9.0" },
{ name = "prompt-toolkit", specifier = "==3.0.40" },
{ name = "pyperclip", specifier = ">=1.9.0" },
{ name = "xonsh", specifier = ">=0.24.2" },
{ name = "xonsh", specifier = ">=0.22.2" },
]
testing = [{ name = "pytest" }]
uis = [
@ -1054,14 +1098,14 @@ wheels = [
[[package]]
name = "polars"
version = "1.44.2"
version = "1.36.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "polars-runtime-32" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a4/15/e8541eefc22fbc7ca89bcb5112298a153729f73cfbc0cf6a668e509f975c/polars-1.44.2.tar.gz", hash = "sha256:86c8e26b6c2de8c8d344bb910b74dfc47b118ac3fe0f19b44909467990a0b281", size = 766272, upload-time = "2026-09-09T07:42:08.859Z" }
sdist = { url = "https://files.pythonhosted.org/packages/7b/ce/285beb40d17c4fc846aea8781d11e8031a3d8e51cedc94858977333e1f0c/polars-1.36.0.tar.gz", hash = "sha256:68af01d0c2dd2a4d95a1ddcd9d34d34587b77222434b824d3827282c0ac39941", size = 711540, upload-time = "2025-12-08T17:11:50.34Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/51/6d/3014112c7f717d1253223faa13b6db3ac3a64ed00ab2a3bc1b942bc9cdd4/polars-1.44.2-py3-none-any.whl", hash = "sha256:1bb331f17a40d9d931101533dcd33637b66edc61eb377b07020dac16a0f0377b", size = 865768, upload-time = "2026-09-09T07:40:12.053Z" },
{ url = "https://files.pythonhosted.org/packages/24/d8/71544ade68423f75c1ffc0fc31022aa71e4bc2522005db16a4461597d382/polars-1.36.0-py3-none-any.whl", hash = "sha256:32d6f0a033420122874fff8f3d7cb2c0d758624d4def8e1ff2023b061c2ad70e", size = 801937, upload-time = "2025-12-08T17:10:54.619Z" },
]
[[package]]
@ -1082,18 +1126,16 @@ wheels = [
[[package]]
name = "polars-runtime-32"
version = "1.44.2"
version = "1.36.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d4/a1/a7eace6587b56f22cf2a21ab4d5e695db372dc23fd96accb68b1ec12660b/polars_runtime_32-1.44.2.tar.gz", hash = "sha256:b84842f7d621aaca7a52e165e19a24f89db45f8aa13744941430218419a14a67", size = 3172205, upload-time = "2026-09-09T07:42:10.851Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a6/0b/7e3b4c3845a994f8bd471b66ec505f053f88cad47a1d6aec2a16dc15707b/polars_runtime_32-1.36.0.tar.gz", hash = "sha256:29d3b3282f3c6ca54b147053d8c0da9c7ae23a7cd80c08ecbefa4f1c266310d3", size = 2786015, upload-time = "2025-12-08T17:11:51.78Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/99/5b/a5215f82c3dd443dc5d6911b0d3e937f97056e0ef7753f7e123422481a18/polars_runtime_32-1.44.2-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:1fd536720668ba203a16a20b08cd6b23057e407a0279cf36b2f35f879d6e3208", size = 48334014, upload-time = "2026-09-09T07:40:16.43Z" },
{ url = "https://files.pythonhosted.org/packages/c2/e0/f3dc93fce4b4e99370db6a89001a1b8d3c606e3560d0d91dda809d6c6324/polars_runtime_32-1.44.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0fd43720c8222ae39919c8ff891636d53b352706087120e62f83544dd3ff782", size = 43331099, upload-time = "2026-09-09T07:40:21.029Z" },
{ url = "https://files.pythonhosted.org/packages/4e/4f/076626ce93ddd622203c4b27be2a96d034cf5b24110c52e96e6029f0ea33/polars_runtime_32-1.44.2-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbf9b45040291dc1c6c588c837019c33557bde25ec536562a9cca9e1f6dfcc45", size = 46590853, upload-time = "2026-09-09T07:40:24.934Z" },
{ url = "https://files.pythonhosted.org/packages/e9/24/ed9982657c446dd5491b089370eea196725673570cfc61f7225a9fdd7ef0/polars_runtime_32-1.44.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1bafb441e99199a62c63bf1bbdc0ea09ee9776dbac2bf31452b5000fb1df2f7", size = 49912258, upload-time = "2026-09-09T07:40:29.238Z" },
{ url = "https://files.pythonhosted.org/packages/71/42/5490ab360aa2406119825ad82203a5e2ff27a3a5893ca8e0b93c053a59a3/polars_runtime_32-1.44.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:10c0c695a418407617b5159db7d9a21074a733e4c6d61275b6762f25cb31ca99", size = 46737310, upload-time = "2026-09-09T07:40:33.143Z" },
{ url = "https://files.pythonhosted.org/packages/06/8f/d741afb1dcd1848161189e017d27972e7e78556d8dce66b94d4235093706/polars_runtime_32-1.44.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c4a09fb14aad711526346efc0cb2015c2fd0555ce4118b6524e5debbaea65ff5", size = 49887654, upload-time = "2026-09-09T07:40:37.455Z" },
{ url = "https://files.pythonhosted.org/packages/ba/e7/c61c1c7eea37705920fe7c1302d1dd80d1165db2b928f0da3eae6d1ebb75/polars_runtime_32-1.44.2-cp310-abi3-win_amd64.whl", hash = "sha256:8598e7a20efba70bb74978c7df7af7c606ff4d79b9b48fdd808250b189bc9a13", size = 51297967, upload-time = "2026-09-09T07:40:41.993Z" },
{ url = "https://files.pythonhosted.org/packages/e7/a0/d0dd0d2ec95fa328dd47055905fae53ba3cd79f11c8973326ebe75a49e4c/polars_runtime_32-1.44.2-cp310-abi3-win_arm64.whl", hash = "sha256:d51040d3ab40157f6db3c62be59cab5b80fb3c8d158924769c4982a1c8eef730", size = 44303345, upload-time = "2026-09-09T07:40:47.081Z" },
{ url = "https://files.pythonhosted.org/packages/98/06/f18703120e8656f0852710a70f301204c8531f5dd7aa4b57e3e4498c1b04/polars_runtime_32-1.36.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6060d6ca990ee76d7f56aa5377c323e842b2c5f2a3eb98422670e393908a3569", size = 42514626, upload-time = "2025-12-08T17:10:56.601Z" },
{ url = "https://files.pythonhosted.org/packages/73/51/96a4129d8754a49c1a3aae550a25859432338899ed522e7bd34902c6493d/polars_runtime_32-1.36.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ab2f15b9de1b2ea78d421703b829a9ffb0b65889a4e6e1dec75c785cebef45f9", size = 38370421, upload-time = "2025-12-08T17:10:59.639Z" },
{ url = "https://files.pythonhosted.org/packages/9e/d2/3d47a9904ac4d71fa1110594dd99de7c298e5bd6f6a602a42f35c81bebf5/polars_runtime_32-1.36.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be0aedf1e2c37b2463923f356d688459843e9436fc47e55fa281ce474ddefacc", size = 43489077, upload-time = "2025-12-08T17:11:02.273Z" },
{ url = "https://files.pythonhosted.org/packages/00/45/ebd063d4d41500cf8140feb49fce9f3b2386103df2ff2cc34569273b9196/polars_runtime_32-1.36.0-cp39-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:2cf5dee05322394fbff6b1e66e9f2653abee1c067a94a3ac5bd1ea389df28b40", size = 39710563, upload-time = "2025-12-08T17:11:04.956Z" },
{ url = "https://files.pythonhosted.org/packages/cc/30/ddcce6b988f88b445f365ec793e54bc8f843a175eb3b580aebfa6c732976/polars_runtime_32-1.36.0-cp39-abi3-win_amd64.whl", hash = "sha256:cf2be8e2fe2a89e8985845cf25a67e233211c7b6a29256b647df6a261b987374", size = 43547512, upload-time = "2025-12-08T17:11:08.286Z" },
{ url = "https://files.pythonhosted.org/packages/92/4f/b2350a98a767de31b7872bd1511530cb7eefe68132f64ba1105679346ca4/polars_runtime_32-1.36.0-cp39-abi3-win_arm64.whl", hash = "sha256:b21cb377c86fe5d0b4de6010dfc3f62c65c4b3b34dcbca77cd1a47548f0f1cba", size = 38938352, upload-time = "2025-12-08T17:11:10.922Z" },
]
[[package]]
@ -1114,6 +1156,21 @@ version = "0.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" },
{ url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" },
{ url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" },
{ url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" },
{ url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" },
{ url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" },
{ url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" },
{ url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" },
{ url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" },
{ url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" },
{ url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" },
{ url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" },
{ url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" },
{ url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" },
{ url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" },
{ url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" },
{ url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" },
{ url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" },
@ -1147,28 +1204,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" },
]
[[package]]
name = "psutil"
version = "7.2.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" },
{ url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" },
{ url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" },
{ url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" },
{ url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" },
{ url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" },
{ url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" },
{ url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" },
{ url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" },
{ url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" },
{ url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" },
{ url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" },
{ url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" },
{ url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" },
]
[[package]]
name = "ptyprocess"
version = "0.7.0"
@ -1178,70 +1213,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" },
]
[[package]]
name = "py-cid"
version = "0.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "morphys" },
{ name = "py-multibase" },
{ name = "py-multicodec" },
{ name = "py-multihash" },
]
sdist = { url = "https://files.pythonhosted.org/packages/96/8e/68c2bd0346247570e8e01e8c170a0237884e95cdfa43989527b71adaa978/py_cid-0.5.0.tar.gz", hash = "sha256:93c62586c672353a9862f3fce13c9848ea39a00378e0980e2f0eed91631f3d28", size = 38028, upload-time = "2026-02-13T19:03:28.603Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2b/18/eaea1571ae8b4fa490793a4b78a9641c4579a884f7a26f3d1b019d7e91c2/py_cid-0.5.0-py3-none-any.whl", hash = "sha256:2fbad437384534e2a0ab0c4068aac3e510c4cb710c89c8f6bf98f4b07ed54e3e", size = 16046, upload-time = "2026-02-13T19:03:27.516Z" },
]
[[package]]
name = "py-multibase"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "morphys" },
{ name = "python-baseconv" },
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bc/52/5ed393ab49df7e3b03995d3c4e53bae1e8c2ca40909cf25a41b346c09a38/py_multibase-2.0.0.tar.gz", hash = "sha256:58c1a264195fa1ae29ea707c6fc8196446f4bdb92e0f9a0f131e0f280b238839", size = 26857, upload-time = "2025-12-18T02:24:49.132Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/36/c7/38035079d9978b32b962f996f1cccaa166ecfe38723ab4349ab32166c037/py_multibase-2.0.0-py3-none-any.whl", hash = "sha256:b29ce489b556134e73998a11712c406b70950812955df64084754e0774e40900", size = 10608, upload-time = "2025-12-18T02:24:47.827Z" },
]
[[package]]
name = "py-multicodec"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "varint" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5e/26/ef24db0fbfec080b72c5ac4a1000da3a4d696a1e31862c695d683097a1b5/py_multicodec-1.0.0.tar.gz", hash = "sha256:78e4e3e47b6288cf635c3ca987152e6cb5510bdcdab307e7690c76ec3d5bbfeb", size = 44668, upload-time = "2025-12-18T20:41:37.976Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/da/768d07490faeae88ac361184164be9c262fececc3c6241b5fc471be4f659/py_multicodec-1.0.0-py3-none-any.whl", hash = "sha256:ae2e687bac8fdf54e3f5b3feded36b61a304d5e3c3af9438f7481f543ec15b8d", size = 26200, upload-time = "2025-12-18T20:41:37.055Z" },
]
[[package]]
name = "py-multihash"
version = "3.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "base58" },
{ name = "blake3" },
{ name = "mmh3" },
{ name = "morphys" },
{ name = "six" },
{ name = "varint" },
]
sdist = { url = "https://files.pythonhosted.org/packages/11/3d/ed68b0eccd0654f7f3c163d9b3d428f903e5e3e884ab1f0d0a16ba6a4f11/py_multihash-3.0.0.tar.gz", hash = "sha256:2e848941de5ef0533ca26b81940e2ffcf7b4322a3f803e8c97f4f0eca8767aa7", size = 41630, upload-time = "2025-12-17T19:30:00.596Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/24/e2/d65606db8369916fb5a9b4fe14df7e6072970d919300f3fb1c989a1d8e7d/py_multihash-3.0.0-py3-none-any.whl", hash = "sha256:3863ec1313b4eac1e5169137c143d40bf77456e57388f839441deba089f87326", size = 21215, upload-time = "2025-12-17T19:29:59.322Z" },
]
[[package]]
name = "pyarrow"
version = "22.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/30/53/04a7fdc63e6056116c9ddc8b43bc28c12cdd181b85cbeadb79278475f3ae/pyarrow-22.0.0.tar.gz", hash = "sha256:3d600dc583260d845c7d8a6db540339dd883081925da2bd1c5cb808f720b3cd9", size = 1151151, upload-time = "2025-10-24T12:30:00.762Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/af/63/ba23862d69652f85b615ca14ad14f3bcfc5bf1b99ef3f0cd04ff93fdad5a/pyarrow-22.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:bea79263d55c24a32b0d79c00a1c58bb2ee5f0757ed95656b01c0fb310c5af3d", size = 34211578, upload-time = "2025-10-24T10:05:21.583Z" },
{ url = "https://files.pythonhosted.org/packages/b1/d0/f9ad86fe809efd2bcc8be32032fa72e8b0d112b01ae56a053006376c5930/pyarrow-22.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:12fe549c9b10ac98c91cf791d2945e878875d95508e1a5d14091a7aaa66d9cf8", size = 35989906, upload-time = "2025-10-24T10:05:29.485Z" },
{ url = "https://files.pythonhosted.org/packages/b4/a8/f910afcb14630e64d673f15904ec27dd31f1e009b77033c365c84e8c1e1d/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:334f900ff08ce0423407af97e6c26ad5d4e3b0763645559ece6fbf3747d6a8f5", size = 45021677, upload-time = "2025-10-24T10:05:38.274Z" },
{ url = "https://files.pythonhosted.org/packages/13/95/aec81f781c75cd10554dc17a25849c720d54feafb6f7847690478dcf5ef8/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c6c791b09c57ed76a18b03f2631753a4960eefbbca80f846da8baefc6491fcfe", size = 47726315, upload-time = "2025-10-24T10:05:47.314Z" },
{ url = "https://files.pythonhosted.org/packages/bb/d4/74ac9f7a54cfde12ee42734ea25d5a3c9a45db78f9def949307a92720d37/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c3200cb41cdbc65156e5f8c908d739b0dfed57e890329413da2748d1a2cd1a4e", size = 47990906, upload-time = "2025-10-24T10:05:58.254Z" },
{ url = "https://files.pythonhosted.org/packages/2e/71/fedf2499bf7a95062eafc989ace56572f3343432570e1c54e6599d5b88da/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ac93252226cf288753d8b46280f4edf3433bf9508b6977f8dd8526b521a1bbb9", size = 50306783, upload-time = "2025-10-24T10:06:08.08Z" },
{ url = "https://files.pythonhosted.org/packages/68/ed/b202abd5a5b78f519722f3d29063dda03c114711093c1995a33b8e2e0f4b/pyarrow-22.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:44729980b6c50a5f2bfcc2668d36c569ce17f8b17bccaf470c4313dcbbf13c9d", size = 27972883, upload-time = "2025-10-24T10:06:14.204Z" },
{ url = "https://files.pythonhosted.org/packages/a6/d6/d0fac16a2963002fc22c8fa75180a838737203d558f0ed3b564c4a54eef5/pyarrow-22.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e6e95176209257803a8b3d0394f21604e796dadb643d2f7ca21b66c9c0b30c9a", size = 34204629, upload-time = "2025-10-24T10:06:20.274Z" },
{ url = "https://files.pythonhosted.org/packages/c6/9c/1d6357347fbae062ad3f17082f9ebc29cc733321e892c0d2085f42a2212b/pyarrow-22.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:001ea83a58024818826a9e3f89bf9310a114f7e26dfe404a4c32686f97bd7901", size = 35985783, upload-time = "2025-10-24T10:06:27.301Z" },
{ url = "https://files.pythonhosted.org/packages/ff/c0/782344c2ce58afbea010150df07e3a2f5fdad299cd631697ae7bd3bac6e3/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ce20fe000754f477c8a9125543f1936ea5b8867c5406757c224d745ed033e691", size = 45020999, upload-time = "2025-10-24T10:06:35.387Z" },
@ -1267,6 +1251,19 @@ dependencies = [
]
sdist = { url = "https://files.pythonhosted.org/packages/8d/ad/9d1e96486d2eb5a2672c4d9a2dd372d015b8d7a332c6ac2722c4c8e6bbbf/pycares-4.11.0.tar.gz", hash = "sha256:c863d9003ca0ce7df26429007859afd2a621d3276ed9fef154a9123db9252557", size = 654473, upload-time = "2025-09-09T15:18:21.849Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e2/4e/4821b66feefaaa8ec03494c1a11614c430983572e54ff062b4589441e199/pycares-4.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b93d624560ba52287873bacff70b42c99943821ecbc810b959b0953560f53c36", size = 145906, upload-time = "2025-09-09T15:16:53.204Z" },
{ url = "https://files.pythonhosted.org/packages/e8/81/93a505dcbb7533254b0ce1da519591dcda889d6a66dcdfa5737e3280e18a/pycares-4.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:775d99966e28c8abd9910ddef2de0f1e173afc5a11cea9f184613c747373ab80", size = 141972, upload-time = "2025-09-09T15:16:54.43Z" },
{ url = "https://files.pythonhosted.org/packages/7d/d6/76994c8b21316e48ea6c3ce3298574c28f90c9c41428a3349a57104621c9/pycares-4.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:84fde689557361764f052850a2d68916050adbfd9321f6105aca1d8f1a9bd49b", size = 637832, upload-time = "2025-09-09T15:16:55.523Z" },
{ url = "https://files.pythonhosted.org/packages/bb/a4/5ca7e316d0edb714d78974cb34f4883f63fe9f580644c2db99fb62b05f56/pycares-4.11.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:30ceed06f3bf5eff865a34d21562c25a7f3dad0ed336b9dd415330e03a6c50c4", size = 687751, upload-time = "2025-09-09T15:16:57.55Z" },
{ url = "https://files.pythonhosted.org/packages/cb/8d/c5c578fdd335d7b1dcaea88fae3497390095b5b05a1ba34a29f62d037abb/pycares-4.11.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:97d971b3a88a803bb95ff8a40ea4d68da59319eb8b59e924e318e2560af8c16d", size = 678362, upload-time = "2025-09-09T15:16:58.859Z" },
{ url = "https://files.pythonhosted.org/packages/b9/96/9be4d838a9348dd2e72a90c34d186b918b66d499af5be79afa18a6ba2808/pycares-4.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:2d5cac829da91ade70ce1af97dad448c6cd4778b48facbce1b015e16ced93642", size = 641069, upload-time = "2025-09-09T15:17:00.046Z" },
{ url = "https://files.pythonhosted.org/packages/39/d6/8ea9b5dcef6b566cde034aa2b68743f7b0a19fa0fba9ea01a4f98b8a57fb/pycares-4.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee1ea367835eb441d246164c09d1f9703197af4425fc6865cefcde9e2ca81f85", size = 622357, upload-time = "2025-09-09T15:17:01.205Z" },
{ url = "https://files.pythonhosted.org/packages/07/f8/3401e89b5d2970e30e02f9beb29ad59e2a8f19ef2c68c978de2b764cacb0/pycares-4.11.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3139ec1f4450a4b253386035c5ecd2722582ae3320a456df5021ffe3f174260a", size = 670290, upload-time = "2025-09-09T15:17:02.413Z" },
{ url = "https://files.pythonhosted.org/packages/a2/c4/ff6a166e1d1d1987339548a19d0b1d52ec3ead8b3a8a2247a0d96e56013c/pycares-4.11.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5d70324ca1d82c6c4b00aa678347f7560d1ef2ce1d181978903459a97751543a", size = 652958, upload-time = "2025-09-09T15:17:04.203Z" },
{ url = "https://files.pythonhosted.org/packages/b8/7c/fc084b395921c9b862d31a83f809fe649c24314b51b527ad0ab0df33edd4/pycares-4.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e2f8d9cfe0eb3a2997fde5df99b1aaea5a46dabfcfcac97b2d05f027c2cd5e28", size = 629239, upload-time = "2025-09-09T15:17:05.477Z" },
{ url = "https://files.pythonhosted.org/packages/b0/7f/2f26062bea95ab657f979217d50df563dc9fd9cc4c5dd21a6e7323e9efe7/pycares-4.11.0-cp312-cp312-win32.whl", hash = "sha256:1571a7055c03a95d5270c914034eac7f8bfa1b432fc1de53d871b821752191a4", size = 118918, upload-time = "2025-09-09T15:17:06.882Z" },
{ url = "https://files.pythonhosted.org/packages/a5/86/277473d20f3df4e00fa7e0ebb21955b2830b15247462aaf8f3fc8c4950be/pycares-4.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:7570e0b50db619b2ee370461c462617225dc3a3f63f975c6f117e2f0c94f82ca", size = 144560, upload-time = "2025-09-09T15:17:07.891Z" },
{ url = "https://files.pythonhosted.org/packages/f0/f9/d65ad17ec921d8b7eb42161dec2024ee2f5c9f1c44cabf0dd1b7f4fac6c5/pycares-4.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:f199702740f3b766ed8c70efb885538be76cb48cd0cb596b948626f0b825e07a", size = 115695, upload-time = "2025-09-09T15:17:09.333Z" },
{ url = "https://files.pythonhosted.org/packages/dc/a9/62fea7ad72ac1fed2ac9dd8e9a7379b7eb0288bf2b3ea5731642c3a6f7de/pycares-4.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c296ab94d1974f8d2f76c499755a9ce31ffd4986e8898ef19b90e32525f7d84", size = 145909, upload-time = "2025-09-09T15:17:10.491Z" },
{ url = "https://files.pythonhosted.org/packages/f4/ac/0317d6d0d3bd7599c53b8f1db09ad04260647d2f6842018e322584791fd5/pycares-4.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e0fcd3a8bac57a0987d9b09953ba0f8703eb9dca7c77f7051d8c2ed001185be8", size = 141974, upload-time = "2025-09-09T15:17:11.634Z" },
{ url = "https://files.pythonhosted.org/packages/63/11/731b565ae1e81c43dac247a248ee204628186f6df97c9927bd06c62237f8/pycares-4.11.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:bac55842047567ddae177fb8189b89a60633ac956d5d37260f7f71b517fd8b87", size = 637796, upload-time = "2025-09-09T15:17:12.815Z" },
@ -1354,6 +1351,11 @@ version = "13.10.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0d/e9/d1b97154cec1d6c8a3d93fb6565d1463bc528fa5103491d626d07a451c7c/pyqt6_sip-13.10.3.tar.gz", hash = "sha256:630895b3827e2c3b4e072089157985691fe4210d64340e71141f93775ea4ae51", size = 92621, upload-time = "2025-12-06T13:19:44.569Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/61/46/c44d1956a2a6bae272883b276125964736adc0e0a87f95a4af0f7876ba08/pyqt6_sip-13.10.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:61e4e935f1d80dd107b0a97fbcbbf27e07046666f72663fa4b0d700514e8201c", size = 112365, upload-time = "2025-12-06T13:19:27.79Z" },
{ url = "https://files.pythonhosted.org/packages/11/fd/04adac969ba70bb042d52e13c99c968fce0e1fa6a52146f03a974168a848/pyqt6_sip-13.10.3-cp312-cp312-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3f3e2a79738319b795f0d1b2a555b1ea669b1a306b604bac876c84833cabb008", size = 301147, upload-time = "2025-12-06T13:19:30.279Z" },
{ url = "https://files.pythonhosted.org/packages/74/83/7ba660ddd7070090bcd387140865474affd901861ba8f6dfcb18504f7f26/pyqt6_sip-13.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748758dfd7f77aeb1c5becfc934a722ce10de51bfdf9902f9cad19c27ba146e7", size = 323336, upload-time = "2025-12-06T13:19:28.961Z" },
{ url = "https://files.pythonhosted.org/packages/cc/0b/6c77989542751c5ec3d829ff6f65b13c646606560c72b96aeb4dfae843b0/pyqt6_sip-13.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:7361b7005a375cd647f2d1e3ca7000967406831bef466003e6ead2af27d84a2b", size = 53459, upload-time = "2025-12-06T13:19:31.353Z" },
{ url = "https://files.pythonhosted.org/packages/9b/73/74df7a24c75719ee36e94d97e147c3c260c1a6268e48d692f561f9d5b9dc/pyqt6_sip-13.10.3-cp312-cp312-win_arm64.whl", hash = "sha256:dd21e6f70f7cfe81e1d9b96800652ffeb5947b41354c4fd58a5e3d3f02499a7a", size = 48647, upload-time = "2025-12-06T13:19:32.271Z" },
{ url = "https://files.pythonhosted.org/packages/0c/a9/25a07fb16308e9405ac01369013943ae58bef72c8700d8a6100182b8d937/pyqt6_sip-13.10.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a8b5532398c0e6d0064d4dce4c096ff20bf710507dafefb036eff61c3f59cda8", size = 112348, upload-time = "2025-12-06T13:19:33.323Z" },
{ url = "https://files.pythonhosted.org/packages/4a/f1/38b625b0638681659bc3c7eaa548b65862a305d26b48835b67cdd6add720/pyqt6_sip-13.10.3-cp313-cp313-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d02c138c6eacb13ef668bfe6becfb6ab40bb40135f34a36ef31b7dc860976493", size = 301470, upload-time = "2025-12-06T13:19:35.824Z" },
{ url = "https://files.pythonhosted.org/packages/cd/8d/a2eaccc88cc53e6370e3728593ea80d10a132f87078ce7cbcfc8c33d9b3f/pyqt6_sip-13.10.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e234a3af9539f71bb566e7136317b92f189a89553970284d833cd63cca4dafdd", size = 323466, upload-time = "2025-12-06T13:19:34.445Z" },
@ -1394,12 +1396,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
]
[[package]]
name = "python-baseconv"
version = "1.2.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/33/d0/9297d7d8dd74767b4d5560d834b30b2fff17d39987c23ed8656f476e0d9b/python-baseconv-1.2.2.tar.gz", hash = "sha256:0539f8bd0464013b05ad62e0a1673f0ac9086c76b43ebf9f833053527cd9931b", size = 4929, upload-time = "2019-04-04T19:28:57.17Z" }
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
@ -1440,6 +1436,16 @@ 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/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" },
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
@ -1482,6 +1488,17 @@ version = "3.14.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d3/28/9d808fe62375b9aab5ba92fa9b29371297b067c2790b2d7cda648b1e2f8d/rapidfuzz-3.14.3.tar.gz", hash = "sha256:2491937177868bc4b1e469087601d53f925e8d270ccc21e07404b4b5814b7b5f", size = 57863900, upload-time = "2025-11-01T11:54:52.321Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/8e/3c215e860b458cfbedb3ed73bc72e98eb7e0ed72f6b48099604a7a3260c2/rapidfuzz-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:685c93ea961d135893b5984a5a9851637d23767feabe414ec974f43babbd8226", size = 1945306, upload-time = "2025-11-01T11:53:06.452Z" },
{ url = "https://files.pythonhosted.org/packages/36/d9/31b33512015c899f4a6e6af64df8dfe8acddf4c8b40a4b3e0e6e1bcd00e5/rapidfuzz-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fa7c8f26f009f8c673fbfb443792f0cf8cf50c4e18121ff1e285b5e08a94fbdb", size = 1390788, upload-time = "2025-11-01T11:53:08.721Z" },
{ url = "https://files.pythonhosted.org/packages/a9/67/2ee6f8de6e2081ccd560a571d9c9063184fe467f484a17fa90311a7f4a2e/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57f878330c8d361b2ce76cebb8e3e1dc827293b6abf404e67d53260d27b5d941", size = 1374580, upload-time = "2025-11-01T11:53:10.164Z" },
{ url = "https://files.pythonhosted.org/packages/30/83/80d22997acd928eda7deadc19ccd15883904622396d6571e935993e0453a/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c5f545f454871e6af05753a0172849c82feaf0f521c5ca62ba09e1b382d6382", size = 3154947, upload-time = "2025-11-01T11:53:12.093Z" },
{ url = "https://files.pythonhosted.org/packages/5b/cf/9f49831085a16384695f9fb096b99662f589e30b89b4a589a1ebc1a19d34/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:07aa0b5d8863e3151e05026a28e0d924accf0a7a3b605da978f0359bb804df43", size = 1223872, upload-time = "2025-11-01T11:53:13.664Z" },
{ url = "https://files.pythonhosted.org/packages/c8/0f/41ee8034e744b871c2e071ef0d360686f5ccfe5659f4fd96c3ec406b3c8b/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73b07566bc7e010e7b5bd490fb04bb312e820970180df6b5655e9e6224c137db", size = 2392512, upload-time = "2025-11-01T11:53:15.109Z" },
{ url = "https://files.pythonhosted.org/packages/da/86/280038b6b0c2ccec54fb957c732ad6b41cc1fd03b288d76545b9cf98343f/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6de00eb84c71476af7d3110cf25d8fe7c792d7f5fa86764ef0b4ca97e78ca3ed", size = 2521398, upload-time = "2025-11-01T11:53:17.146Z" },
{ url = "https://files.pythonhosted.org/packages/fa/7b/05c26f939607dca0006505e3216248ae2de631e39ef94dd63dbbf0860021/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d7843a1abf0091773a530636fdd2a49a41bcae22f9910b86b4f903e76ddc82dc", size = 4259416, upload-time = "2025-11-01T11:53:19.34Z" },
{ url = "https://files.pythonhosted.org/packages/40/eb/9e3af4103d91788f81111af1b54a28de347cdbed8eaa6c91d5e98a889aab/rapidfuzz-3.14.3-cp312-cp312-win32.whl", hash = "sha256:dea97ac3ca18cd3ba8f3d04b5c1fe4aa60e58e8d9b7793d3bd595fdb04128d7a", size = 1709527, upload-time = "2025-11-01T11:53:20.949Z" },
{ url = "https://files.pythonhosted.org/packages/b8/63/d06ecce90e2cf1747e29aeab9f823d21e5877a4c51b79720b2d3be7848f8/rapidfuzz-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:b5100fd6bcee4d27f28f4e0a1c6b5127bc8ba7c2a9959cad9eab0bf4a7ab3329", size = 1538989, upload-time = "2025-11-01T11:53:22.428Z" },
{ url = "https://files.pythonhosted.org/packages/fc/6d/beee32dcda64af8128aab3ace2ccb33d797ed58c434c6419eea015fec779/rapidfuzz-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:4e49c9e992bc5fc873bd0fff7ef16a4405130ec42f2ce3d2b735ba5d3d4eb70f", size = 811161, upload-time = "2025-11-01T11:53:23.811Z" },
{ url = "https://files.pythonhosted.org/packages/e4/4f/0d94d09646853bd26978cb3a7541b6233c5760687777fa97da8de0d9a6ac/rapidfuzz-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dbcb726064b12f356bf10fffdb6db4b6dce5390b23627c08652b3f6e49aa56ae", size = 1939646, upload-time = "2025-11-01T11:53:25.292Z" },
{ url = "https://files.pythonhosted.org/packages/b6/eb/f96aefc00f3bbdbab9c0657363ea8437a207d7545ac1c3789673e05d80bd/rapidfuzz-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1704fc70d214294e554a2421b473779bcdeef715881c5e927dc0f11e1692a0ff", size = 1385512, upload-time = "2025-11-01T11:53:27.594Z" },
{ url = "https://files.pythonhosted.org/packages/26/34/71c4f7749c12ee223dba90017a5947e8f03731a7cc9f489b662a8e9e643d/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc65e72790ddfd310c2c8912b45106e3800fefe160b0c2ef4d6b6fec4e826457", size = 1373571, upload-time = "2025-11-01T11:53:29.096Z" },
@ -1560,26 +1577,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" },
]
[[package]]
name = "setproctitle"
version = "1.3.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/49/b0/6b8a516c5a9e9630bd5293db78314ac012f690305fe93beadea388626efb/setproctitle-1.3.8.tar.gz", hash = "sha256:cafe209d064a6efb88cb45a03e97981ff8832802b2b5d009dde0197a3b7b41c8", size = 27214, upload-time = "2026-10-01T21:09:27.121Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/58/46/125791eb19c85340c6d43de8c821b143aecbfa0882343f244f5744474229/setproctitle-1.3.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a4a782f9ab26e662a890c1a7d865c452c83ed554de44aa83da35fa5bdcc79921", size = 18100, upload-time = "2026-10-01T20:59:33.41Z" },
{ url = "https://files.pythonhosted.org/packages/47/51/73b1e62c3e019e683e58d31f9f8af57e6750f36ce079004d29b7790e22ce/setproctitle-1.3.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d63adfa4be8f9dbac4be6b359e2a8eb15a733545ef613d02a12ae27a4d40e975", size = 13116, upload-time = "2026-10-01T20:59:35.159Z" },
{ url = "https://files.pythonhosted.org/packages/78/ae/095c6ad159e3a7b95bc6d9a3eae18abfea1ebc79ed3377617406a1668150/setproctitle-1.3.8-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d4289e209e805d7d276a534f062092974fbe6814889d4c132c247205ebb49a65", size = 33275, upload-time = "2026-10-01T20:59:37.235Z" },
{ url = "https://files.pythonhosted.org/packages/f7/6e/5196258efd941cb874b3f4da91460601b867fdc434eba48a70455736d4be/setproctitle-1.3.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8bb543b4b58ad839f7f0d335a7f4b40cef6989952e7239de3532de0b7771607e", size = 34124, upload-time = "2026-10-01T20:59:39.529Z" },
{ url = "https://files.pythonhosted.org/packages/36/01/e9754e0916e6dda232a8c337bda69d2fea33acee0c5d3bdf580b8155a50d/setproctitle-1.3.8-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b705f01a531dba8fbbab90e30d432af4cc70a79c0a33b238e2c061816140cad0", size = 36006, upload-time = "2026-10-01T20:59:41.501Z" },
{ url = "https://files.pythonhosted.org/packages/ee/a9/c63d2d745f18423937429a8611743e7b3e17259173150fbcdaa59daa4a51/setproctitle-1.3.8-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8e286e4c4e90063072a6e088a8d6526a7ffceb47429db88ba50ba58eeebcc758", size = 32268, upload-time = "2026-10-01T20:59:43.795Z" },
{ url = "https://files.pythonhosted.org/packages/36/0c/a72818902f32ea9b848db5ccc828eb9cd9bc8661975610e1d46ea4aacbe2/setproctitle-1.3.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7b77c8671974288cf192e481ed63e928f1086762e97e8845d09d65ba121f8aaa", size = 33179, upload-time = "2026-10-01T20:59:45.809Z" },
{ url = "https://files.pythonhosted.org/packages/bb/55/fa59463ae6b957048e0e842d4b5513eced1b71554d8af0f5d91cd23cde23/setproctitle-1.3.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0e85b55eac9abfa3166ab00dac21e718ac2624a1df7d0b75559a2d3e3781cb33", size = 34472, upload-time = "2026-10-01T20:59:47.858Z" },
{ url = "https://files.pythonhosted.org/packages/b7/77/1f0cf55055788bf7eb9a146040a64d7ec42cd56153894856effc70e943ec/setproctitle-1.3.8-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f215ec9237d5f8e96d0205be6e2141292c20b133343c2153b6d4934f523ef422", size = 31428, upload-time = "2026-10-01T20:59:49.753Z" },
{ url = "https://files.pythonhosted.org/packages/32/98/d1d8a6cd620d72a04f5d3335125e5b2fc5667bd06a76bb5bbc50b101e804/setproctitle-1.3.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b0e70a5d294e4c8cf7dc94e015fca6d4913779a424937e66b429fba82d8eb5e2", size = 32776, upload-time = "2026-10-01T20:59:51.763Z" },
{ url = "https://files.pythonhosted.org/packages/4b/34/8b9eea1e80ac7a7f622eead8a0e4d73a60098344ed6f373e111f7ce8d29d/setproctitle-1.3.8-cp313-cp313-win32.whl", hash = "sha256:f85b4d87273e0ec63568433bbb8ccf160b548c49b53576fe010bad9f698b8586", size = 12105, upload-time = "2026-10-01T20:59:53.444Z" },
{ url = "https://files.pythonhosted.org/packages/bf/f0/98883c658859d331faf91eccac8946cb2f8feb2590905015b129fdeb09cd/setproctitle-1.3.8-cp313-cp313-win_amd64.whl", hash = "sha256:bcdf74fe8a6e3b31bed1bc7d3e2a38cfbda3263d2750492ea0725dbc2db5aacf", size = 12933, upload-time = "2026-10-01T20:59:55.045Z" },
]
[[package]]
name = "shellingham"
version = "1.5.4"
@ -1634,6 +1631,14 @@ version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" },
{ url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" },
{ url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" },
{ url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" },
{ url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" },
{ url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" },
{ url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" },
{ url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" },
{ url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" },
{ url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" },
{ url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" },
@ -1671,16 +1676,14 @@ wheels = [
[[package]]
name = "tractor"
version = "0.1.0a6.dev0"
source = { git = "https://github.com/goodboy/tractor.git?branch=piker_pin#b64c89f05a9fc8ac2878551eb5eda36574ebef6a" }
source = { git = "https://github.com/goodboy/tractor.git?branch=main#e77198bb64f0467a50e251ed140daee439752354" }
dependencies = [
{ name = "bidict" },
{ name = "cffi" },
{ name = "colorlog" },
{ name = "msgspec" },
{ name = "multiaddr" },
{ name = "pdbp" },
{ name = "platformdirs" },
{ name = "py-multibase" },
{ name = "setproctitle" },
{ name = "tricycle" },
{ name = "trio" },
{ name = "wrapt" },
@ -1807,6 +1810,12 @@ version = "0.22.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" },
{ url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" },
{ url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" },
{ url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" },
{ url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" },
{ url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" },
{ url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" },
{ url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" },
{ url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" },
@ -1815,12 +1824,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" },
]
[[package]]
name = "varint"
version = "1.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a8/fe/1ea0ba0896dfa47186692655b86db3214c4b7c9e0e76c7b1dc257d101ab1/varint-1.0.2.tar.gz", hash = "sha256:a6ecc02377ac5ee9d65a6a8ad45c9ff1dac8ccee19400a5950fb51d594214ca5", size = 1886, upload-time = "2016-02-24T20:42:38.5Z" }
[[package]]
name = "wcwidth"
version = "0.2.14"
@ -1836,6 +1839,17 @@ version = "12.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2e/62/7a7874b7285413c954a4cca3c11fd851f11b2fe5b4ae2d9bee4f6d9bdb10/websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b", size = 104994, upload-time = "2023-10-21T14:21:11.88Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/6d/23cc898647c8a614a0d9ca703695dd04322fb5135096a20c2684b7c852b6/websockets-12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df", size = 124061, upload-time = "2023-10-21T14:20:02.221Z" },
{ url = "https://files.pythonhosted.org/packages/39/34/364f30fdf1a375e4002a26ee3061138d1571dfda6421126127d379d13930/websockets-12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc", size = 121296, upload-time = "2023-10-21T14:20:03.591Z" },
{ url = "https://files.pythonhosted.org/packages/2e/00/96ae1c9dcb3bc316ef683f2febd8c97dde9f254dc36c3afc65c7645f734c/websockets-12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b", size = 121326, upload-time = "2023-10-21T14:20:04.956Z" },
{ url = "https://files.pythonhosted.org/packages/af/f1/bba1e64430685dd456c1a1fd6b0c791ae33104967b928aefeff261761e8d/websockets-12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb", size = 131807, upload-time = "2023-10-21T14:20:06.153Z" },
{ url = "https://files.pythonhosted.org/packages/62/3b/98ee269712f37d892b93852ce07b3e6d7653160ca4c0d4f8c8663f8021f8/websockets-12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92", size = 130751, upload-time = "2023-10-21T14:20:07.753Z" },
{ url = "https://files.pythonhosted.org/packages/f1/00/d6f01ca2b191f8b0808e4132ccd2e7691f0453cbd7d0f72330eb97453c3a/websockets-12.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed", size = 131176, upload-time = "2023-10-21T14:20:09.212Z" },
{ url = "https://files.pythonhosted.org/packages/af/9c/703ff3cd8109dcdee6152bae055d852ebaa7750117760ded697ab836cbcf/websockets-12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5", size = 136246, upload-time = "2023-10-21T14:20:10.423Z" },
{ url = "https://files.pythonhosted.org/packages/0b/a5/1a38fb85a456b9dc874ec984f3ff34f6550eafd17a3da28753cd3c1628e8/websockets-12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2", size = 135466, upload-time = "2023-10-21T14:20:11.826Z" },
{ url = "https://files.pythonhosted.org/packages/3c/98/1261f289dff7e65a38d59d2f591de6ed0a2580b729aebddec033c4d10881/websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113", size = 136083, upload-time = "2023-10-21T14:20:13.451Z" },
{ url = "https://files.pythonhosted.org/packages/a9/1c/f68769fba63ccb9c13fe0a25b616bd5aebeef1c7ddebc2ccc32462fb784d/websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d", size = 124460, upload-time = "2023-10-21T14:20:14.719Z" },
{ url = "https://files.pythonhosted.org/packages/20/52/8915f51f9aaef4e4361c89dd6cf69f72a0159f14e0d25026c81b6ad22525/websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f", size = 124985, upload-time = "2023-10-21T14:20:15.817Z" },
{ url = "https://files.pythonhosted.org/packages/79/4d/9cc401e7b07e80532ebc8c8e993f42541534da9e9249c59ee0139dcb0352/websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e", size = 118370, upload-time = "2023-10-21T14:21:10.075Z" },
]
@ -1845,6 +1859,16 @@ version = "1.17.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" },
{ url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" },
{ url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" },
{ url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" },
{ url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" },
{ url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" },
{ url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" },
{ url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" },
{ url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" },
{ url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" },
{ url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" },
{ url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" },
{ url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" },
@ -1872,13 +1896,13 @@ wheels = [
[[package]]
name = "xonsh"
version = "0.24.2"
version = "0.22.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c1/28/974f44afd5c05bcfb012d52a91b514e629706048c85d669a29eab2f0369a/xonsh-0.24.2.tar.gz", hash = "sha256:461244cf1de8ed28d0c07cc548342b9e7969d46e17ef8d71b5d3064307930a25", size = 1217004, upload-time = "2026-08-23T15:41:03.929Z" }
sdist = { url = "https://files.pythonhosted.org/packages/48/df/1fc9ed62b3d7c14612e1713e9eb7bd41d54f6ad1028a8fbb6b7cddebc345/xonsh-0.22.4.tar.gz", hash = "sha256:6be346563fec2db75778ba5d2caee155525e634e99d9cc8cc347626025c0b3fa", size = 826665, upload-time = "2026-02-17T07:53:39.424Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/16/32/2dc9f6142d6744fa584a59507030a851971e4088af86f187437344f60315/xonsh-0.24.2-py311-none-any.whl", hash = "sha256:60015217f74663d52c4038e495546710ecdffeb0beae96834b4ac3ea970a3927", size = 821746, upload-time = "2026-08-23T15:41:02.525Z" },
{ url = "https://files.pythonhosted.org/packages/b0/77/9a9b1e1fcf377b412bb45626be1af9a97414f8a3aa96df566e06b8ebb092/xonsh-0.24.2-py312-none-any.whl", hash = "sha256:68e93b77bbf9e2a26f70bf323a1c311014dc2aed9ebcf036e68e7fe59ed0849c", size = 821749, upload-time = "2026-08-23T15:40:25.865Z" },
{ url = "https://files.pythonhosted.org/packages/08/6a/aaae1f889eec7f015aa8b7041f807871cb18a2c2bc8f0564c5ca70f4bc16/xonsh-0.24.2-py313-none-any.whl", hash = "sha256:da19e84ad0361c0c8052f5119fd08bd111995df60fa233b8a61a3d66ea253d78", size = 822213, upload-time = "2026-08-23T15:40:24.572Z" },
{ url = "https://files.pythonhosted.org/packages/2e/00/7cbc0c1fb64365a0a317c54ce3a151c9644eea5a509d9cbaae61c9fd1426/xonsh-0.22.4-py311-none-any.whl", hash = "sha256:38b29b29fa85aa756462d9d9bbcaa1d85478c2108da3de6cc590a69a4bcd1a01", size = 654375, upload-time = "2026-02-17T07:53:37.702Z" },
{ url = "https://files.pythonhosted.org/packages/2e/c2/3dd498dc28d8f89cdd52e39950c5e591499ae423f61694c0bb4d03ed1d82/xonsh-0.22.4-py312-none-any.whl", hash = "sha256:4e538fac9f4c3d866ddbdeca068f0c0515469c997ed58d3bfee963878c6df5a5", size = 654300, upload-time = "2026-02-17T07:53:35.813Z" },
{ url = "https://files.pythonhosted.org/packages/82/7d/1f9c7147518e9f03f6ce081b5bfc4f1aceb6ec5caba849024d005e41d3be/xonsh-0.22.4-py313-none-any.whl", hash = "sha256:cc5fabf0ad0c56a2a11bed1e6a43c4ec6416a5b30f24f126b8e768547c3793e2", size = 654818, upload-time = "2026-02-17T07:53:33.477Z" },
]
[[package]]
@ -1886,6 +1910,10 @@ name = "yapic-json"
version = "1.9.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/3d/35/5d52934c020bfbffaff2c09b311f65103655a25dd4182cd23984187b9872/yapic.json-1.9.5.tar.gz", hash = "sha256:d4c7a533b0670709fca9623f243e6bc5b6c0f67adbb4de508af2381c271e98b4", size = 105372, upload-time = "2024-09-29T18:43:37.038Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/3d/eedd25e7497d7e761907705b99f8fb452e27a24f54be9d4d4cd505eec1b6/yapic.json-1.9.5-cp312-cp312-win32.whl", hash = "sha256:63036af738ccd4d3cf5a3446a2867ad62e78f66af21dcd73b77992a19ee43e15", size = 77845, upload-time = "2024-09-29T18:47:38.216Z" },
{ url = "https://files.pythonhosted.org/packages/cb/6e/664b9557cf99e34783da9f2cb384817379cea2b8d5f02f73fe119023c4dd/yapic.json-1.9.5-cp312-cp312-win_amd64.whl", hash = "sha256:3db5a5828efd0d207a74f71bd41ab7db995fe26920bff1e413b756b367691db2", size = 84473, upload-time = "2024-09-29T18:47:28.002Z" },
]
[[package]]
name = "yarl"
@ -1898,6 +1926,22 @@ dependencies = [
]
sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" },
{ url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" },
{ url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" },
{ url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" },
{ url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" },
{ url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" },
{ url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" },
{ url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" },
{ url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" },
{ url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" },
{ url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" },
{ url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" },
{ url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" },
{ url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" },
{ url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" },
{ url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" },
{ url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" },
{ url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" },
{ url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" },