Merge pull request #429 from goodboy/multiaddr_support
Multiaddresses: a novel `libp2p` peep's idea worth embracingsubint_spawner_backend
commit
5c98ab1fb6
|
|
@ -27,7 +27,8 @@
|
||||||
"Write(.claude/git_commit_msg_LATEST.md)",
|
"Write(.claude/git_commit_msg_LATEST.md)",
|
||||||
"Skill(run-tests)",
|
"Skill(run-tests)",
|
||||||
"Skill(close-wkt)",
|
"Skill(close-wkt)",
|
||||||
"Skill(open-wkt)"
|
"Skill(open-wkt)",
|
||||||
|
"Skill(prompt-io)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ treat it as the test target. Examples:
|
||||||
|
|
||||||
- `/run-tests` → full suite
|
- `/run-tests` → full suite
|
||||||
- `/run-tests test_local.py` → single file
|
- `/run-tests test_local.py` → single file
|
||||||
- `/run-tests test_discovery -v` → file + verbose
|
- `/run-tests test_registrar -v` → file + verbose
|
||||||
- `/run-tests -k cancel` → keyword filter
|
- `/run-tests -k cancel` → keyword filter
|
||||||
- `/run-tests tests/ipc/ --tpt-proto uds` → subdir + UDS
|
- `/run-tests tests/ipc/ --tpt-proto uds` → subdir + UDS
|
||||||
|
|
||||||
|
|
@ -81,7 +81,7 @@ python -m pytest tests/test_local.py tests/test_rpc.py -x --tb=short --no-header
|
||||||
python -m pytest tests/ -x --tb=short --no-header
|
python -m pytest tests/ -x --tb=short --no-header
|
||||||
|
|
||||||
# specific test with debug
|
# specific test with debug
|
||||||
python -m pytest tests/test_discovery.py::test_reg_then_unreg -x -s --tpdb --ll debug
|
python -m pytest tests/discovery/test_registrar.py::test_reg_then_unreg -x -s --tpdb --ll debug
|
||||||
|
|
||||||
# run with UDS transport
|
# run with UDS transport
|
||||||
python -m pytest tests/ -x --tb=short --no-header --tpt-proto uds
|
python -m pytest tests/ -x --tb=short --no-header --tpt-proto uds
|
||||||
|
|
@ -173,8 +173,10 @@ tests/
|
||||||
├── devx/ # debugger/tooling tests
|
├── devx/ # debugger/tooling tests
|
||||||
├── ipc/ # transport protocol tests
|
├── ipc/ # transport protocol tests
|
||||||
├── msg/ # messaging layer tests
|
├── msg/ # messaging layer tests
|
||||||
|
├── discovery/ # discovery subsystem tests
|
||||||
|
│ ├── test_multiaddr.py # multiaddr construction
|
||||||
|
│ └── test_registrar.py # registry/discovery protocol
|
||||||
├── test_local.py # registrar + local actor basics
|
├── test_local.py # registrar + local actor basics
|
||||||
├── test_discovery.py # registry/discovery protocol
|
|
||||||
├── test_rpc.py # RPC error handling
|
├── test_rpc.py # RPC error handling
|
||||||
├── test_spawning.py # subprocess spawning
|
├── test_spawning.py # subprocess spawning
|
||||||
├── test_multi_program.py # multi-process tree tests
|
├── test_multi_program.py # multi-process tree tests
|
||||||
|
|
@ -193,7 +195,7 @@ test subset first for fast feedback:
|
||||||
| Changed module(s) | Run these tests first |
|
| Changed module(s) | Run these tests first |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `runtime/_runtime.py`, `runtime/_state.py` | `test_local.py test_rpc.py test_spawning.py test_root_runtime.py` |
|
| `runtime/_runtime.py`, `runtime/_state.py` | `test_local.py test_rpc.py test_spawning.py test_root_runtime.py` |
|
||||||
| `discovery/` (`_registry`, `_discovery`, `_addr`) | `test_discovery.py test_multi_program.py test_local.py` |
|
| `discovery/` (`_registry`, `_discovery`, `_addr`) | `tests/discovery/ test_multi_program.py test_local.py` |
|
||||||
| `_context.py`, `_streaming.py` | `test_context_stream_semantics.py test_advanced_streaming.py` |
|
| `_context.py`, `_streaming.py` | `test_context_stream_semantics.py test_advanced_streaming.py` |
|
||||||
| `ipc/` (`_chan`, `_server`, `_transport`) | `tests/ipc/ test_2way.py` |
|
| `ipc/` (`_chan`, `_server`, `_transport`) | `tests/ipc/ test_2way.py` |
|
||||||
| `runtime/_portal.py`, `runtime/_rpc.py` | `test_rpc.py test_cancellation.py` |
|
| `runtime/_portal.py`, `runtime/_rpc.py` | `test_rpc.py test_cancellation.py` |
|
||||||
|
|
@ -212,7 +214,7 @@ test subset first for fast feedback:
|
||||||
python -c 'import tractor' && python -m pytest tests/ -x -q --co 2>&1 | tail -3
|
python -c 'import tractor' && python -m pytest tests/ -x -q --co 2>&1 | tail -3
|
||||||
|
|
||||||
# core subset (~10s)
|
# core subset (~10s)
|
||||||
python -m pytest tests/test_local.py tests/test_rpc.py tests/test_spawning.py tests/test_discovery.py -x --tb=short --no-header
|
python -m pytest tests/test_local.py tests/test_rpc.py tests/test_spawning.py tests/discovery/test_registrar.py -x --tb=short --no-header
|
||||||
```
|
```
|
||||||
|
|
||||||
### Re-run last failures only:
|
### Re-run last failures only:
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ model: claude-opus-4-6
|
||||||
service: claude
|
service: claude
|
||||||
timestamp: 2026-04-06T22:26:48Z
|
timestamp: 2026-04-06T22:26:48Z
|
||||||
git_ref: f086222
|
git_ref: f086222
|
||||||
|
diff_cmd: git diff f086222~1..f086222
|
||||||
---
|
---
|
||||||
|
|
||||||
Here's the problem. The per-key locking doesn't fix this race because the **lock lifetime is decoupled from `run_ctx`'s lifetime**. Trace through what happens:
|
Here's the problem. The per-key locking doesn't fix this race because the **lock lifetime is decoupled from `run_ctx`'s lifetime**. Trace through what happens:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
---
|
||||||
|
model: claude-opus-4-6
|
||||||
|
service: claude
|
||||||
|
session: (ad-hoc, not tracked via conf.toml)
|
||||||
|
timestamp: 2026-04-13T19:21:16Z
|
||||||
|
git_ref: f851f28
|
||||||
|
scope: code
|
||||||
|
substantive: true
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prompt
|
||||||
|
|
||||||
|
User provided a detailed implementation plan for
|
||||||
|
a new `test_tpt_bind_addrs.py` test suite covering
|
||||||
|
`open_root_actor(tpt_bind_addrs=...)` — the three
|
||||||
|
runtime code paths in `_root.py:385-450`:
|
||||||
|
|
||||||
|
1. Non-registrar, no explicit bind -> random addrs
|
||||||
|
2. Registrar, no explicit bind -> registry_addrs
|
||||||
|
3. Explicit bind given -> `wrap_address()` + merge
|
||||||
|
|
||||||
|
Plan specified 6 test functions (~10 parametrized
|
||||||
|
variants), predicted a type-mixing bug at line 446,
|
||||||
|
and asked for an in-flight fix if confirmed.
|
||||||
|
|
||||||
|
## Response summary
|
||||||
|
|
||||||
|
Created `tests/discovery/test_tpt_bind_addrs.py`
|
||||||
|
with 9 collected test variants across 6 functions:
|
||||||
|
|
||||||
|
- `test_registrar_root_tpt_bind_addrs` (3 variants:
|
||||||
|
`bind-eq-reg`, `bind-subset-reg`,
|
||||||
|
`bind-disjoint-reg`)
|
||||||
|
- `test_non_registrar_root_tpt_bind_addrs`
|
||||||
|
(2 variants: `bind-same-bindspace`,
|
||||||
|
`bind-disjoint`)
|
||||||
|
- `test_non_registrar_default_random_bind`
|
||||||
|
(baseline, no explicit bind)
|
||||||
|
- `test_tpt_bind_addrs_as_maddr_str`
|
||||||
|
(multiaddr string input)
|
||||||
|
- `test_registrar_merge_binds_union`
|
||||||
|
(registrar + disjoint bind -> union)
|
||||||
|
- `test_open_nursery_forwards_tpt_bind_addrs`
|
||||||
|
(`open_nursery(**kwargs)` forwarding)
|
||||||
|
|
||||||
|
Confirmed and fixed the predicted bug at
|
||||||
|
`_root.py:446`: the registrar merge path mixed
|
||||||
|
`Address` objects (`tpt_bind_addrs`) with raw tuples
|
||||||
|
(`uw_reg_addrs`) inside `set()`, preventing
|
||||||
|
deduplication and causing double-bind `OSError`.
|
||||||
|
|
||||||
|
Fix: wrap `uw_reg_addrs` before the set union:
|
||||||
|
```python
|
||||||
|
# before (broken)
|
||||||
|
tpt_bind_addrs = list(set(
|
||||||
|
tpt_bind_addrs + uw_reg_addrs
|
||||||
|
))
|
||||||
|
# after (fixed)
|
||||||
|
tpt_bind_addrs = list(set(
|
||||||
|
tpt_bind_addrs
|
||||||
|
+ [wrap_address(a) for a in uw_reg_addrs]
|
||||||
|
))
|
||||||
|
```
|
||||||
|
|
||||||
|
All 9 tests pass after the fix.
|
||||||
|
|
||||||
|
## Files changed
|
||||||
|
|
||||||
|
- `tests/discovery/test_tpt_bind_addrs.py` (new)
|
||||||
|
- `tractor/_root.py:446` (bug fix, 1 line)
|
||||||
|
|
||||||
|
## Human edits
|
||||||
|
|
||||||
|
N/A — pending review.
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
---
|
||||||
|
model: claude-opus-4-6
|
||||||
|
service: claude
|
||||||
|
session: 76154e65-d8e1-4b5f-9275-0ea45ba7e98a
|
||||||
|
timestamp: 2026-04-13T20:50:48Z
|
||||||
|
git_ref: 269d939c
|
||||||
|
scope: code
|
||||||
|
substantive: true
|
||||||
|
raw_file: 20260413T205048Z_269d939c_prompt_io.raw.md
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prompt
|
||||||
|
|
||||||
|
Implement a `parse_endpoints()` API in
|
||||||
|
`tractor.discovery._multiaddr` that lets downstream
|
||||||
|
projects declare per-actor transport bind addresses
|
||||||
|
as a service table of actor-name -> multiaddr
|
||||||
|
strings (loaded from config, e.g. TOML `[network]`
|
||||||
|
section). Add type aliases `EndpointsTable` and
|
||||||
|
`ParsedEndpoints`, plus 7 unit tests covering TCP,
|
||||||
|
UDS, tuples, mixed input, unsupported protocols, and
|
||||||
|
edge cases.
|
||||||
|
|
||||||
|
A detailed implementation plan was provided
|
||||||
|
specifying insertion points, type signatures, test
|
||||||
|
names, and verification commands.
|
||||||
|
|
||||||
|
## Response summary
|
||||||
|
|
||||||
|
Add `parse_endpoints()` function with two supporting
|
||||||
|
type aliases to `_multiaddr.py`. The function
|
||||||
|
iterates a `dict[str, list[str|tuple]]` service
|
||||||
|
table and delegates each entry to the existing
|
||||||
|
`wrap_address()` helper, which already handles maddr
|
||||||
|
strings, raw tuples, and pre-wrapped `Address`
|
||||||
|
objects. Seven pure unit tests (no trio runtime)
|
||||||
|
validate all input variants and edge cases. All 22
|
||||||
|
multiaddr tests pass.
|
||||||
|
|
||||||
|
## Files changed
|
||||||
|
|
||||||
|
- `tractor/discovery/_multiaddr.py` — add
|
||||||
|
`EndpointsTable`, `ParsedEndpoints` type aliases
|
||||||
|
and `parse_endpoints()` function after line 133
|
||||||
|
- `tests/discovery/test_multiaddr.py` — add
|
||||||
|
`parse_endpoints` import and 7 new test functions
|
||||||
|
|
||||||
|
## Human edits
|
||||||
|
|
||||||
|
None — committed as generated.
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
---
|
||||||
|
model: claude-opus-4-6
|
||||||
|
service: claude
|
||||||
|
timestamp: 2026-04-13T20:50:48Z
|
||||||
|
git_ref: 269d939c
|
||||||
|
diff_cmd: git diff 269d939c~1..269d939c
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation output
|
||||||
|
|
||||||
|
### `tractor/discovery/_multiaddr.py`
|
||||||
|
|
||||||
|
> `git diff 269d939c~1..269d939c -- tractor/discovery/_multiaddr.py`
|
||||||
|
|
||||||
|
Type aliases `EndpointsTable` and `ParsedEndpoints`
|
||||||
|
plus `parse_endpoints()` helper that wraps each
|
||||||
|
entry via `wrap_address()`.
|
||||||
|
|
||||||
|
### `tests/discovery/test_multiaddr.py`
|
||||||
|
|
||||||
|
> `git diff 269d939c~1..269d939c -- tests/discovery/test_multiaddr.py`
|
||||||
|
|
||||||
|
Add import of `parse_endpoints` to existing import
|
||||||
|
block, then 7 new test functions.
|
||||||
|
|
||||||
|
### Test run output
|
||||||
|
|
||||||
|
```
|
||||||
|
22 passed, 1 warning in 0.05s
|
||||||
|
```
|
||||||
|
|
||||||
|
All 22 tests pass (15 existing + 7 new).
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
---
|
||||||
|
model: claude-opus-4-6
|
||||||
|
service: claude
|
||||||
|
session: multiaddr-support-rename-prefer
|
||||||
|
timestamp: 2026-04-14T16:33:00Z
|
||||||
|
git_ref: befedc49
|
||||||
|
scope: code
|
||||||
|
substantive: true
|
||||||
|
raw_file: 20260414T163300Z_befedc49_prompt_io.raw.md
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prompt
|
||||||
|
|
||||||
|
Create a helper function that determines the best transport given
|
||||||
|
actor locality (distributed vs same host). Use PID/hostname
|
||||||
|
comparison for locality detection, apply at registry addr selection
|
||||||
|
only (not spawn-time).
|
||||||
|
|
||||||
|
## Response summary
|
||||||
|
|
||||||
|
New `prefer_addr()` + `_is_local_addr()` helpers
|
||||||
|
in `_api.py` using `socket.getaddrinfo()` and
|
||||||
|
`ipaddress` for PID/hostname locality detection.
|
||||||
|
Preference: UDS > local TCP > remote TCP.
|
||||||
|
Integrated into `query_actor()` and
|
||||||
|
`wait_for_actor()`. Also changed
|
||||||
|
`Registrar.find_actor()` to return full addr list
|
||||||
|
so callers can apply preference.
|
||||||
|
|
||||||
|
## Files changed
|
||||||
|
|
||||||
|
- `tractor/discovery/_discovery.py` → `_api.py`
|
||||||
|
— renamed + added `prefer_addr()`,
|
||||||
|
`_is_local_addr()`; updated `query_actor()` and
|
||||||
|
`wait_for_actor()` call sites
|
||||||
|
- `tractor/discovery/_registry.py`
|
||||||
|
— `Registrar.find_actor()` returns
|
||||||
|
`list[UnwrappedAddress]|None`
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
---
|
||||||
|
model: claude-opus-4-6
|
||||||
|
service: claude
|
||||||
|
timestamp: 2026-04-14T16:33:00Z
|
||||||
|
git_ref: befedc49
|
||||||
|
diff_cmd: git diff befedc49~1..befedc49
|
||||||
|
---
|
||||||
|
|
||||||
|
### `tractor/discovery/_api.py`
|
||||||
|
|
||||||
|
> `git diff befedc49~1..befedc49 -- tractor/discovery/_api.py`
|
||||||
|
|
||||||
|
Add `_is_local_addr()` and `prefer_addr()` transport
|
||||||
|
preference helpers.
|
||||||
|
|
||||||
|
#### `_is_local_addr(addr: Address) -> bool`
|
||||||
|
|
||||||
|
Determines whether an `Address` is reachable on the
|
||||||
|
local host:
|
||||||
|
|
||||||
|
- `UDSAddress`: always returns `True`
|
||||||
|
(filesystem-bound, inherently local)
|
||||||
|
- `TCPAddress`: checks if `._host` is a loopback IP
|
||||||
|
via `ipaddress.ip_address().is_loopback`, then
|
||||||
|
falls back to comparing against the machine's own
|
||||||
|
interface IPs via
|
||||||
|
`socket.getaddrinfo(socket.gethostname(), None)`
|
||||||
|
|
||||||
|
#### `prefer_addr(addrs: list[UnwrappedAddress]) -> UnwrappedAddress`
|
||||||
|
|
||||||
|
Selects the "best" transport address from a
|
||||||
|
multihomed actor's address list. Wraps each
|
||||||
|
candidate via `wrap_address()` to get typed
|
||||||
|
`Address` objects, then classifies into three tiers:
|
||||||
|
|
||||||
|
1. **UDS** (same-host guaranteed, lowest overhead)
|
||||||
|
2. **TCP loopback / same-host IP** (local network)
|
||||||
|
3. **TCP remote** (only option for distributed)
|
||||||
|
|
||||||
|
Within each tier, the last-registered (latest) entry
|
||||||
|
is preferred. Falls back to `addrs[-1]` if no
|
||||||
|
heuristic matches.
|
||||||
|
|
||||||
|
### `tractor/discovery/_registry.py`
|
||||||
|
|
||||||
|
> `git diff befedc49~1..befedc49 -- tractor/discovery/_registry.py`
|
||||||
|
|
||||||
|
`Registrar.find_actor()` return type broadened from
|
||||||
|
single addr to `list[UnwrappedAddress]|None` — full
|
||||||
|
addr list lets callers apply transport preference.
|
||||||
|
|
||||||
|
#### Integration
|
||||||
|
|
||||||
|
`query_actor()` and `wait_for_actor()` now call
|
||||||
|
`prefer_addr(addrs)` instead of `addrs[-1]`.
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
|
||||||
|
All discovery tests pass (13/13 non-daemon).
|
||||||
|
`test_local.py` and `test_multi_program.py` also
|
||||||
|
pass (daemon fixture teardown failures are
|
||||||
|
pre-existing and unrelated).
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
ok now i want you to take a look at the most recent commit adding
|
||||||
|
a `tpt_bind_addrs` to `open_root_actor()` and extend the existing
|
||||||
|
tests/discovery/test_multiaddr* and friends to use this new param in
|
||||||
|
at least one suite with parametrizations over,
|
||||||
|
|
||||||
|
- `registry_addrs == tpt_bind_addrs`, as in both inputs are the same.
|
||||||
|
- `set(registry_addrs) >= set(tpt_bind_addrs)`, as in the registry
|
||||||
|
addrs include the bind set.
|
||||||
|
- `registry_addrs != tpt_bind_addrs`, where the reg set is disjoint from
|
||||||
|
the bind set in all possible combos you can imagine.
|
||||||
|
|
||||||
|
All of the ^above cases should further be parametrized over,
|
||||||
|
- the root being the registrar,
|
||||||
|
- a non-registrar root using our bg `daemon` fixture.
|
||||||
|
|
||||||
|
once we have a fairly thorough test suite and have flushed out all
|
||||||
|
bugs and edge cases we want to design a wrapping API which allows
|
||||||
|
declaring full tree's of actors tpt endpoints using multiaddrs such
|
||||||
|
that a `dict[str, list[str]]` of actor-name -> multiaddr can be used
|
||||||
|
to configure a tree of actors-as-services given such an input
|
||||||
|
"endpoints-table" can be matched with the number of appropriately
|
||||||
|
named subactore spawns in a `tractor` user-app.
|
||||||
|
|
||||||
|
Here is a small example from piker,
|
||||||
|
|
||||||
|
- in piker's root conf.toml we define a `[network]` section which can
|
||||||
|
define various actor-service-daemon names set to a maddr
|
||||||
|
(multiaddress str).
|
||||||
|
|
||||||
|
- each actor whether part of the `pikerd` tree (as a sub) or spawned
|
||||||
|
in other non-registrar rooted trees (such as `piker chart`) should
|
||||||
|
configurable in terms of its `tractor` tpt bind addresses via
|
||||||
|
a simple service lookup table,
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[network]
|
||||||
|
pikerd = [
|
||||||
|
'/ip4/127.0.0.1/tcp/6116', # std localhost daemon-actor tree
|
||||||
|
'/uds/run/user/1000/piker/pikerd@6116.sock', # same but serving UDS
|
||||||
|
]
|
||||||
|
chart = [
|
||||||
|
'/ip4/127.0.0.1/tcp/3333', # std localhost daemon-actor tree
|
||||||
|
'/uds/run/user/1000/piker/chart@3333.sock',
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
We should take whatever common API is needed to support this and
|
||||||
|
distill it into a
|
||||||
|
```python
|
||||||
|
tractor.discovery.parse_endpoints(
|
||||||
|
) -> dict[
|
||||||
|
str,
|
||||||
|
list[Address]
|
||||||
|
|dict[str, list[Address]]
|
||||||
|
# ^recursive case, see below
|
||||||
|
]:
|
||||||
|
```
|
||||||
|
|
||||||
|
style API which can,
|
||||||
|
|
||||||
|
- be re-used easily across dependent projects.
|
||||||
|
- correctly raise tpt-backend support errors when a maddr specifying
|
||||||
|
a unsupport proto is passed.
|
||||||
|
- be used to handle "tunnelled" maddrs per
|
||||||
|
https://github.com/multiformats/py-multiaddr/#tunneling such that
|
||||||
|
for any such tunneled maddr-`str`-entry we deliver a data-structure
|
||||||
|
which can easily be passed to nested `@acm`s which consecutively
|
||||||
|
setup nested net bindspaces for binding the endpoint addrs using
|
||||||
|
a combo of our `.ipc.*` machinery and, say for example something like
|
||||||
|
https://github.com/svinota/pyroute2, more precisely say for
|
||||||
|
managing tunnelled wireguard eps within network-namespaces,
|
||||||
|
* https://docs.pyroute2.org/
|
||||||
|
* https://docs.pyroute2.org/netns.html
|
||||||
|
|
||||||
|
remember to include use of all default `.claude/skills` throughout
|
||||||
|
this work!
|
||||||
|
|
@ -49,6 +49,7 @@ dependencies = [
|
||||||
"msgspec>=0.19.0",
|
"msgspec>=0.19.0",
|
||||||
"cffi>=1.17.1",
|
"cffi>=1.17.1",
|
||||||
"bidict>=0.23.1",
|
"bidict>=0.23.1",
|
||||||
|
"multiaddr>=0.2.0",
|
||||||
"platformdirs>=4.4.0",
|
"platformdirs>=4.4.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -199,15 +199,39 @@ def ci_env() -> bool:
|
||||||
def sig_prog(
|
def sig_prog(
|
||||||
proc: subprocess.Popen,
|
proc: subprocess.Popen,
|
||||||
sig: int,
|
sig: int,
|
||||||
canc_timeout: float = 0.1,
|
canc_timeout: float = 0.2,
|
||||||
|
tries: int = 3,
|
||||||
) -> int:
|
) -> int:
|
||||||
"Kill the actor-process with ``sig``."
|
'''
|
||||||
proc.send_signal(sig)
|
Kill the actor-process with `sig`.
|
||||||
time.sleep(canc_timeout)
|
|
||||||
if not proc.poll():
|
Prefer to kill with the provided signal and
|
||||||
|
failing a `canc_timeout`, send a `SIKILL`-like
|
||||||
|
to ensure termination.
|
||||||
|
|
||||||
|
'''
|
||||||
|
for i in range(tries):
|
||||||
|
proc.send_signal(sig)
|
||||||
|
if proc.poll() is None:
|
||||||
|
print(
|
||||||
|
f'WARNING, proc still alive after,\n'
|
||||||
|
f'canc_timeout={canc_timeout!r}\n'
|
||||||
|
f'sig={sig!r}\n'
|
||||||
|
f'\n'
|
||||||
|
f'{proc.args!r}\n'
|
||||||
|
)
|
||||||
|
time.sleep(canc_timeout)
|
||||||
|
else:
|
||||||
# TODO: why sometimes does SIGINT not work on teardown?
|
# TODO: why sometimes does SIGINT not work on teardown?
|
||||||
# seems to happen only when trace logging enabled?
|
# seems to happen only when trace logging enabled?
|
||||||
proc.send_signal(_KILL_SIGNAL)
|
if proc.poll() is None:
|
||||||
|
print(
|
||||||
|
f'XXX WARNING KILLING PROG WITH SIGINT XXX\n'
|
||||||
|
f'canc_timeout={canc_timeout!r}\n'
|
||||||
|
f'{proc.args!r}\n'
|
||||||
|
)
|
||||||
|
proc.send_signal(_KILL_SIGNAL)
|
||||||
|
|
||||||
ret: int = proc.wait()
|
ret: int = proc.wait()
|
||||||
assert ret
|
assert ret
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -785,17 +785,28 @@ def test_multi_nested_subactors_error_through_nurseries(
|
||||||
|
|
||||||
# timed_out_early: bool = False
|
# timed_out_early: bool = False
|
||||||
|
|
||||||
for send_char in itertools.cycle(['c', 'q']):
|
for (
|
||||||
|
i,
|
||||||
|
send_char,
|
||||||
|
) in enumerate(itertools.cycle(['c', 'q'])):
|
||||||
|
|
||||||
|
timeout: float = -1
|
||||||
|
if (
|
||||||
|
_non_linux
|
||||||
|
and
|
||||||
|
ci_env
|
||||||
|
):
|
||||||
|
timeout: float = 6
|
||||||
|
|
||||||
|
# XXX linux but the first crash sequence
|
||||||
|
# can take longer to arrive at a prompt.
|
||||||
|
elif i == 0:
|
||||||
|
timeout = 5
|
||||||
|
|
||||||
try:
|
try:
|
||||||
child.expect(
|
child.expect(
|
||||||
PROMPT,
|
PROMPT,
|
||||||
timeout=(
|
timeout=timeout,
|
||||||
6 if (
|
|
||||||
_non_linux
|
|
||||||
and
|
|
||||||
ci_env
|
|
||||||
) else -1
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
child.sendline(send_char)
|
child.sendline(send_char)
|
||||||
time.sleep(0.01)
|
time.sleep(0.01)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,376 @@
|
||||||
|
'''
|
||||||
|
Multiaddr construction, parsing, and round-trip tests for
|
||||||
|
`tractor.discovery._multiaddr.mk_maddr()` and
|
||||||
|
`tractor.discovery._multiaddr.parse_maddr()`.
|
||||||
|
|
||||||
|
'''
|
||||||
|
from pathlib import Path
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from multiaddr import Multiaddr
|
||||||
|
|
||||||
|
from tractor.ipc._tcp import TCPAddress
|
||||||
|
from tractor.ipc._uds import UDSAddress
|
||||||
|
from tractor.discovery._multiaddr import (
|
||||||
|
mk_maddr,
|
||||||
|
parse_maddr,
|
||||||
|
parse_endpoints,
|
||||||
|
_tpt_proto_to_maddr,
|
||||||
|
_maddr_to_tpt_proto,
|
||||||
|
)
|
||||||
|
from tractor.discovery._addr import wrap_address
|
||||||
|
|
||||||
|
|
||||||
|
def test_tpt_proto_to_maddr_mapping():
|
||||||
|
'''
|
||||||
|
`_tpt_proto_to_maddr` maps all supported `proto_key`
|
||||||
|
values to their correct multiaddr protocol names.
|
||||||
|
|
||||||
|
'''
|
||||||
|
assert _tpt_proto_to_maddr['tcp'] == 'tcp'
|
||||||
|
assert _tpt_proto_to_maddr['uds'] == 'unix'
|
||||||
|
assert len(_tpt_proto_to_maddr) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_mk_maddr_tcp_ipv4():
|
||||||
|
'''
|
||||||
|
`mk_maddr()` on a `TCPAddress` with an IPv4 host
|
||||||
|
produces the correct `/ip4/<host>/tcp/<port>` multiaddr.
|
||||||
|
|
||||||
|
'''
|
||||||
|
addr = TCPAddress('127.0.0.1', 1234)
|
||||||
|
result: Multiaddr = mk_maddr(addr)
|
||||||
|
|
||||||
|
assert isinstance(result, Multiaddr)
|
||||||
|
assert str(result) == '/ip4/127.0.0.1/tcp/1234'
|
||||||
|
|
||||||
|
protos = result.protocols()
|
||||||
|
assert protos[0].name == 'ip4'
|
||||||
|
assert protos[1].name == 'tcp'
|
||||||
|
|
||||||
|
assert result.value_for_protocol('ip4') == '127.0.0.1'
|
||||||
|
assert result.value_for_protocol('tcp') == '1234'
|
||||||
|
|
||||||
|
|
||||||
|
def test_mk_maddr_tcp_ipv6():
|
||||||
|
'''
|
||||||
|
`mk_maddr()` on a `TCPAddress` with an IPv6 host
|
||||||
|
produces the correct `/ip6/<host>/tcp/<port>` multiaddr.
|
||||||
|
|
||||||
|
'''
|
||||||
|
addr = TCPAddress('::1', 5678)
|
||||||
|
result: Multiaddr = mk_maddr(addr)
|
||||||
|
|
||||||
|
assert str(result) == '/ip6/::1/tcp/5678'
|
||||||
|
|
||||||
|
protos = result.protocols()
|
||||||
|
assert protos[0].name == 'ip6'
|
||||||
|
assert protos[1].name == 'tcp'
|
||||||
|
|
||||||
|
|
||||||
|
def test_mk_maddr_uds():
|
||||||
|
'''
|
||||||
|
`mk_maddr()` on a `UDSAddress` produces a `/unix/<path>`
|
||||||
|
multiaddr containing the full socket path.
|
||||||
|
|
||||||
|
'''
|
||||||
|
# NOTE, use an absolute `filedir` to match real runtime
|
||||||
|
# UDS paths; `mk_maddr()` strips the leading `/` to avoid
|
||||||
|
# the double-slash `/unix//run/..` that py-multiaddr
|
||||||
|
# rejects as "empty protocol path".
|
||||||
|
filedir = '/tmp/tractor_test'
|
||||||
|
filename = 'test_sock.sock'
|
||||||
|
addr = UDSAddress(
|
||||||
|
filedir=filedir,
|
||||||
|
filename=filename,
|
||||||
|
)
|
||||||
|
result: Multiaddr = mk_maddr(addr)
|
||||||
|
|
||||||
|
assert isinstance(result, Multiaddr)
|
||||||
|
|
||||||
|
result_str: str = str(result)
|
||||||
|
assert result_str.startswith('/unix/')
|
||||||
|
# verify the leading `/` was stripped to avoid double-slash
|
||||||
|
assert '/unix/tmp/tractor_test/' in result_str
|
||||||
|
|
||||||
|
sockpath_rel: str = str(
|
||||||
|
Path(filedir) / filename
|
||||||
|
).lstrip('/')
|
||||||
|
unix_val: str = result.value_for_protocol('unix')
|
||||||
|
assert unix_val.endswith(sockpath_rel)
|
||||||
|
|
||||||
|
|
||||||
|
def test_mk_maddr_unsupported_proto_key():
|
||||||
|
'''
|
||||||
|
`mk_maddr()` raises `ValueError` for an unsupported
|
||||||
|
`proto_key`.
|
||||||
|
|
||||||
|
'''
|
||||||
|
fake_addr = SimpleNamespace(proto_key='quic')
|
||||||
|
with pytest.raises(
|
||||||
|
ValueError,
|
||||||
|
match='Unsupported proto_key',
|
||||||
|
):
|
||||||
|
mk_maddr(fake_addr)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'addr',
|
||||||
|
[
|
||||||
|
pytest.param(
|
||||||
|
TCPAddress('127.0.0.1', 9999),
|
||||||
|
id='tcp-ipv4',
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
UDSAddress(
|
||||||
|
filedir='/tmp/tractor_rt',
|
||||||
|
filename='roundtrip.sock',
|
||||||
|
),
|
||||||
|
id='uds',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_mk_maddr_roundtrip(addr):
|
||||||
|
'''
|
||||||
|
`mk_maddr()` output is valid multiaddr syntax that the
|
||||||
|
library can re-parse back into an equivalent `Multiaddr`.
|
||||||
|
|
||||||
|
'''
|
||||||
|
maddr: Multiaddr = mk_maddr(addr)
|
||||||
|
reparsed = Multiaddr(str(maddr))
|
||||||
|
|
||||||
|
assert reparsed == maddr
|
||||||
|
assert str(reparsed) == str(maddr)
|
||||||
|
|
||||||
|
|
||||||
|
# ------ parse_maddr() tests ------
|
||||||
|
|
||||||
|
def test_maddr_to_tpt_proto_mapping():
|
||||||
|
'''
|
||||||
|
`_maddr_to_tpt_proto` is the exact inverse of
|
||||||
|
`_tpt_proto_to_maddr`.
|
||||||
|
|
||||||
|
'''
|
||||||
|
assert _maddr_to_tpt_proto == {
|
||||||
|
'tcp': 'tcp',
|
||||||
|
'unix': 'uds',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_maddr_tcp_ipv4():
|
||||||
|
'''
|
||||||
|
`parse_maddr()` on an IPv4 TCP multiaddr string
|
||||||
|
produce a `TCPAddress` with the correct host and port.
|
||||||
|
|
||||||
|
'''
|
||||||
|
result = parse_maddr('/ip4/127.0.0.1/tcp/1234')
|
||||||
|
|
||||||
|
assert isinstance(result, TCPAddress)
|
||||||
|
assert result.unwrap() == ('127.0.0.1', 1234)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_maddr_tcp_ipv6():
|
||||||
|
'''
|
||||||
|
`parse_maddr()` on an IPv6 TCP multiaddr string
|
||||||
|
produce a `TCPAddress` with the correct host and port.
|
||||||
|
|
||||||
|
'''
|
||||||
|
result = parse_maddr('/ip6/::1/tcp/5678')
|
||||||
|
|
||||||
|
assert isinstance(result, TCPAddress)
|
||||||
|
assert result.unwrap() == ('::1', 5678)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_maddr_uds():
|
||||||
|
'''
|
||||||
|
`parse_maddr()` on a `/unix/...` multiaddr string
|
||||||
|
produce a `UDSAddress` with the correct dir and filename,
|
||||||
|
preserving absolute path semantics.
|
||||||
|
|
||||||
|
'''
|
||||||
|
result = parse_maddr('/unix/tmp/tractor_test/test.sock')
|
||||||
|
|
||||||
|
assert isinstance(result, UDSAddress)
|
||||||
|
filedir, filename = result.unwrap()
|
||||||
|
assert filename == 'test.sock'
|
||||||
|
assert str(filedir) == '/tmp/tractor_test'
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_maddr_unsupported():
|
||||||
|
'''
|
||||||
|
`parse_maddr()` raise `ValueError` for an unsupported
|
||||||
|
protocol combination like UDP.
|
||||||
|
|
||||||
|
'''
|
||||||
|
with pytest.raises(
|
||||||
|
ValueError,
|
||||||
|
match='Unsupported multiaddr protocol combo',
|
||||||
|
):
|
||||||
|
parse_maddr('/ip4/127.0.0.1/udp/1234')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'addr',
|
||||||
|
[
|
||||||
|
pytest.param(
|
||||||
|
TCPAddress('127.0.0.1', 9999),
|
||||||
|
id='tcp-ipv4',
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
UDSAddress(
|
||||||
|
filedir='/tmp/tractor_rt',
|
||||||
|
filename='roundtrip.sock',
|
||||||
|
),
|
||||||
|
id='uds',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_parse_maddr_roundtrip(addr):
|
||||||
|
'''
|
||||||
|
Full round-trip: `addr -> mk_maddr -> str -> parse_maddr`
|
||||||
|
produce an `Address` whose `.unwrap()` matches the original.
|
||||||
|
|
||||||
|
'''
|
||||||
|
maddr: Multiaddr = mk_maddr(addr)
|
||||||
|
maddr_str: str = str(maddr)
|
||||||
|
parsed = parse_maddr(maddr_str)
|
||||||
|
|
||||||
|
assert type(parsed) is type(addr)
|
||||||
|
assert parsed.unwrap() == addr.unwrap()
|
||||||
|
|
||||||
|
|
||||||
|
def test_wrap_address_maddr_str():
|
||||||
|
'''
|
||||||
|
`wrap_address()` accept a multiaddr-format string and
|
||||||
|
return the correct `Address` type.
|
||||||
|
|
||||||
|
'''
|
||||||
|
result = wrap_address('/ip4/127.0.0.1/tcp/9999')
|
||||||
|
|
||||||
|
assert isinstance(result, TCPAddress)
|
||||||
|
assert result.unwrap() == ('127.0.0.1', 9999)
|
||||||
|
|
||||||
|
|
||||||
|
# ------ parse_endpoints() tests ------
|
||||||
|
|
||||||
|
def test_parse_endpoints_tcp_only():
|
||||||
|
'''
|
||||||
|
`parse_endpoints()` with a single TCP maddr per actor
|
||||||
|
produce the correct `TCPAddress` instances.
|
||||||
|
|
||||||
|
'''
|
||||||
|
table = {
|
||||||
|
'registry': ['/ip4/127.0.0.1/tcp/1616'],
|
||||||
|
'data_feed': ['/ip4/0.0.0.0/tcp/5555'],
|
||||||
|
}
|
||||||
|
result = parse_endpoints(table)
|
||||||
|
|
||||||
|
assert set(result.keys()) == {'registry', 'data_feed'}
|
||||||
|
|
||||||
|
reg_addr = result['registry'][0]
|
||||||
|
assert isinstance(reg_addr, TCPAddress)
|
||||||
|
assert reg_addr.unwrap() == ('127.0.0.1', 1616)
|
||||||
|
|
||||||
|
feed_addr = result['data_feed'][0]
|
||||||
|
assert isinstance(feed_addr, TCPAddress)
|
||||||
|
assert feed_addr.unwrap() == ('0.0.0.0', 5555)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_endpoints_mixed_tpts():
|
||||||
|
'''
|
||||||
|
`parse_endpoints()` with both TCP and UDS maddrs for
|
||||||
|
the same actor produce the correct mixed `Address` list.
|
||||||
|
|
||||||
|
'''
|
||||||
|
table = {
|
||||||
|
'broker': [
|
||||||
|
'/ip4/127.0.0.1/tcp/4040',
|
||||||
|
'/unix/tmp/tractor/broker.sock',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
result = parse_endpoints(table)
|
||||||
|
addrs = result['broker']
|
||||||
|
|
||||||
|
assert len(addrs) == 2
|
||||||
|
assert isinstance(addrs[0], TCPAddress)
|
||||||
|
assert addrs[0].unwrap() == ('127.0.0.1', 4040)
|
||||||
|
|
||||||
|
assert isinstance(addrs[1], UDSAddress)
|
||||||
|
filedir, filename = addrs[1].unwrap()
|
||||||
|
assert filename == 'broker.sock'
|
||||||
|
assert str(filedir) == '/tmp/tractor'
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_endpoints_unwrapped_tuples():
|
||||||
|
'''
|
||||||
|
`parse_endpoints()` accept raw `(host, port)` tuples
|
||||||
|
and wrap them as `TCPAddress`.
|
||||||
|
|
||||||
|
'''
|
||||||
|
table = {
|
||||||
|
'ems': [('127.0.0.1', 6666)],
|
||||||
|
}
|
||||||
|
result = parse_endpoints(table)
|
||||||
|
|
||||||
|
addr = result['ems'][0]
|
||||||
|
assert isinstance(addr, TCPAddress)
|
||||||
|
assert addr.unwrap() == ('127.0.0.1', 6666)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_endpoints_mixed_str_and_tuple():
|
||||||
|
'''
|
||||||
|
`parse_endpoints()` accept a mix of maddr strings and
|
||||||
|
raw tuples in the same actor entry list.
|
||||||
|
|
||||||
|
'''
|
||||||
|
table = {
|
||||||
|
'quoter': [
|
||||||
|
'/ip4/127.0.0.1/tcp/7777',
|
||||||
|
('127.0.0.1', 8888),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
result = parse_endpoints(table)
|
||||||
|
addrs = result['quoter']
|
||||||
|
|
||||||
|
assert len(addrs) == 2
|
||||||
|
assert isinstance(addrs[0], TCPAddress)
|
||||||
|
assert addrs[0].unwrap() == ('127.0.0.1', 7777)
|
||||||
|
|
||||||
|
assert isinstance(addrs[1], TCPAddress)
|
||||||
|
assert addrs[1].unwrap() == ('127.0.0.1', 8888)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_endpoints_unsupported_proto():
|
||||||
|
'''
|
||||||
|
`parse_endpoints()` raise `ValueError` when a maddr
|
||||||
|
string uses an unsupported protocol like `/udp/`.
|
||||||
|
|
||||||
|
'''
|
||||||
|
table = {
|
||||||
|
'bad_actor': ['/ip4/127.0.0.1/udp/9999'],
|
||||||
|
}
|
||||||
|
with pytest.raises(
|
||||||
|
ValueError,
|
||||||
|
match='Unsupported multiaddr protocol combo',
|
||||||
|
):
|
||||||
|
parse_endpoints(table)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_endpoints_empty_table():
|
||||||
|
'''
|
||||||
|
`parse_endpoints()` on an empty table return an empty
|
||||||
|
dict.
|
||||||
|
|
||||||
|
'''
|
||||||
|
assert parse_endpoints({}) == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_endpoints_empty_actor_list():
|
||||||
|
'''
|
||||||
|
`parse_endpoints()` with an actor mapped to an empty
|
||||||
|
list preserve the key with an empty list value.
|
||||||
|
|
||||||
|
'''
|
||||||
|
result = parse_endpoints({'x': []})
|
||||||
|
assert result == {'x': []}
|
||||||
|
|
@ -16,6 +16,8 @@ import subprocess
|
||||||
import tractor
|
import tractor
|
||||||
from tractor.trionics import collapse_eg
|
from tractor.trionics import collapse_eg
|
||||||
from tractor._testing import tractor_test
|
from tractor._testing import tractor_test
|
||||||
|
from tractor.discovery._addr import wrap_address
|
||||||
|
from tractor.discovery._multiaddr import mk_maddr
|
||||||
import trio
|
import trio
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -53,6 +55,49 @@ async def test_reg_then_unreg(
|
||||||
assert not sockaddrs
|
assert not sockaddrs
|
||||||
|
|
||||||
|
|
||||||
|
@tractor_test
|
||||||
|
async def test_reg_then_unreg_maddr(
|
||||||
|
reg_addr: tuple,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Same as `test_reg_then_unreg` but pass the registry
|
||||||
|
address as a multiaddr string to verify `wrap_address()`
|
||||||
|
multiaddr parsing end-to-end through the runtime.
|
||||||
|
|
||||||
|
'''
|
||||||
|
# tuple -> Address -> multiaddr string
|
||||||
|
addr_obj = wrap_address(reg_addr)
|
||||||
|
maddr_str: str = str(mk_maddr(addr_obj))
|
||||||
|
|
||||||
|
actor = tractor.current_actor()
|
||||||
|
assert actor.is_registrar
|
||||||
|
|
||||||
|
async with tractor.open_nursery(
|
||||||
|
registry_addrs=[maddr_str],
|
||||||
|
) as n:
|
||||||
|
|
||||||
|
portal = await n.start_actor(
|
||||||
|
'actor_maddr',
|
||||||
|
enable_modules=[__name__],
|
||||||
|
)
|
||||||
|
uid = portal.channel.aid.uid
|
||||||
|
|
||||||
|
async with tractor.get_registry(maddr_str) as aportal:
|
||||||
|
assert actor is aportal.actor
|
||||||
|
|
||||||
|
async with tractor.wait_for_actor('actor_maddr'):
|
||||||
|
assert uid in aportal.actor._registry
|
||||||
|
sockaddrs = actor._registry[uid]
|
||||||
|
assert sockaddrs
|
||||||
|
|
||||||
|
await n.cancel()
|
||||||
|
|
||||||
|
await trio.sleep(0.1)
|
||||||
|
assert uid not in aportal.actor._registry
|
||||||
|
sockaddrs = actor._registry.get(uid)
|
||||||
|
assert not sockaddrs
|
||||||
|
|
||||||
|
|
||||||
the_line = 'Hi my name is {}'
|
the_line = 'Hi my name is {}'
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -507,7 +552,7 @@ def test_stale_entry_is_deleted(
|
||||||
registry_addrs=[reg_addr],
|
registry_addrs=[reg_addr],
|
||||||
) as maybe_portal:
|
) as maybe_portal:
|
||||||
# because the transitive
|
# because the transitive
|
||||||
# `._discovery.maybe_open_portal()` call should
|
# `._api.maybe_open_portal()` call should
|
||||||
# fail and implicitly call `.delete_addr()`
|
# fail and implicitly call `.delete_addr()`
|
||||||
assert maybe_portal is None
|
assert maybe_portal is None
|
||||||
registry: dict = await unpack_reg(_reg_ptl)
|
registry: dict = await unpack_reg(_reg_ptl)
|
||||||
|
|
@ -0,0 +1,345 @@
|
||||||
|
'''
|
||||||
|
`open_root_actor(tpt_bind_addrs=...)` test suite.
|
||||||
|
|
||||||
|
Verify all three runtime code paths for explicit IPC-server
|
||||||
|
bind-address selection in `_root.py`:
|
||||||
|
|
||||||
|
1. Non-registrar, no explicit bind -> random addrs from registry proto
|
||||||
|
2. Registrar, no explicit bind -> binds to registry_addrs
|
||||||
|
3. Explicit bind given -> wraps via `wrap_address()` and uses them
|
||||||
|
|
||||||
|
'''
|
||||||
|
import pytest
|
||||||
|
import trio
|
||||||
|
import tractor
|
||||||
|
from tractor.discovery._addr import (
|
||||||
|
wrap_address,
|
||||||
|
)
|
||||||
|
from tractor.discovery._multiaddr import mk_maddr
|
||||||
|
from tractor._testing.addr import get_rando_addr
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# helpers
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _bound_bindspaces(
|
||||||
|
actor: tractor.Actor,
|
||||||
|
) -> set[str]:
|
||||||
|
'''
|
||||||
|
Collect the set of bindspace strings from the actor's
|
||||||
|
currently bound IPC-server accept addresses.
|
||||||
|
|
||||||
|
'''
|
||||||
|
return {
|
||||||
|
wrap_address(a).bindspace
|
||||||
|
for a in actor.accept_addrs
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _bound_wrapped(
|
||||||
|
actor: tractor.Actor,
|
||||||
|
) -> list:
|
||||||
|
'''
|
||||||
|
Return the actor's accept addrs as wrapped `Address` objects.
|
||||||
|
|
||||||
|
'''
|
||||||
|
return [
|
||||||
|
wrap_address(a)
|
||||||
|
for a in actor.accept_addrs
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 1) Registrar + explicit tpt_bind_addrs
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'addr_combo',
|
||||||
|
[
|
||||||
|
'bind-eq-reg',
|
||||||
|
'bind-subset-reg',
|
||||||
|
'bind-disjoint-reg',
|
||||||
|
],
|
||||||
|
ids=lambda v: v,
|
||||||
|
)
|
||||||
|
def test_registrar_root_tpt_bind_addrs(
|
||||||
|
reg_addr: tuple,
|
||||||
|
tpt_proto: str,
|
||||||
|
debug_mode: bool,
|
||||||
|
addr_combo: str,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Registrar root-actor with explicit `tpt_bind_addrs`:
|
||||||
|
bound set must include all registry + all bind addr bindspaces
|
||||||
|
(merge behavior).
|
||||||
|
|
||||||
|
'''
|
||||||
|
reg_wrapped = wrap_address(reg_addr)
|
||||||
|
|
||||||
|
if addr_combo == 'bind-eq-reg':
|
||||||
|
bind_addrs = [reg_addr]
|
||||||
|
# extra secondary reg addr for subset test
|
||||||
|
extra_reg = []
|
||||||
|
|
||||||
|
elif addr_combo == 'bind-subset-reg':
|
||||||
|
second_reg = get_rando_addr(tpt_proto)
|
||||||
|
bind_addrs = [reg_addr]
|
||||||
|
extra_reg = [second_reg]
|
||||||
|
|
||||||
|
elif addr_combo == 'bind-disjoint-reg':
|
||||||
|
# port=0 on same host -> completely different addr
|
||||||
|
rando = wrap_address(reg_addr).get_random(
|
||||||
|
bindspace=reg_wrapped.bindspace,
|
||||||
|
)
|
||||||
|
bind_addrs = [rando.unwrap()]
|
||||||
|
extra_reg = []
|
||||||
|
|
||||||
|
all_reg = [reg_addr] + extra_reg
|
||||||
|
|
||||||
|
async def _main():
|
||||||
|
async with tractor.open_root_actor(
|
||||||
|
registry_addrs=all_reg,
|
||||||
|
tpt_bind_addrs=bind_addrs,
|
||||||
|
debug_mode=debug_mode,
|
||||||
|
):
|
||||||
|
actor = tractor.current_actor()
|
||||||
|
assert actor.is_registrar
|
||||||
|
|
||||||
|
bound = actor.accept_addrs
|
||||||
|
bound_bs = _bound_bindspaces(actor)
|
||||||
|
|
||||||
|
# all registry bindspaces must appear in bound set
|
||||||
|
for ra in all_reg:
|
||||||
|
assert wrap_address(ra).bindspace in bound_bs
|
||||||
|
|
||||||
|
# all bind-addr bindspaces must appear
|
||||||
|
for ba in bind_addrs:
|
||||||
|
assert wrap_address(ba).bindspace in bound_bs
|
||||||
|
|
||||||
|
# registry addr must appear verbatim in bound
|
||||||
|
# (after wrapping both sides for comparison)
|
||||||
|
bound_w = _bound_wrapped(actor)
|
||||||
|
assert reg_wrapped in bound_w
|
||||||
|
|
||||||
|
if addr_combo == 'bind-disjoint-reg':
|
||||||
|
assert len(bound) >= 2
|
||||||
|
|
||||||
|
trio.run(_main)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'addr_combo',
|
||||||
|
[
|
||||||
|
'bind-same-bindspace',
|
||||||
|
'bind-disjoint',
|
||||||
|
],
|
||||||
|
ids=lambda v: v,
|
||||||
|
)
|
||||||
|
def test_non_registrar_root_tpt_bind_addrs(
|
||||||
|
daemon,
|
||||||
|
reg_addr: tuple,
|
||||||
|
tpt_proto: str,
|
||||||
|
debug_mode: bool,
|
||||||
|
addr_combo: str,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Non-registrar root with explicit `tpt_bind_addrs`:
|
||||||
|
bound set must exactly match the requested bind addrs
|
||||||
|
(no merge with registry).
|
||||||
|
|
||||||
|
'''
|
||||||
|
reg_wrapped = wrap_address(reg_addr)
|
||||||
|
|
||||||
|
if addr_combo == 'bind-same-bindspace':
|
||||||
|
# same bindspace as reg but port=0 so we get a random port
|
||||||
|
rando = reg_wrapped.get_random(
|
||||||
|
bindspace=reg_wrapped.bindspace,
|
||||||
|
)
|
||||||
|
bind_addrs = [rando.unwrap()]
|
||||||
|
|
||||||
|
elif addr_combo == 'bind-disjoint':
|
||||||
|
rando = reg_wrapped.get_random(
|
||||||
|
bindspace=reg_wrapped.bindspace,
|
||||||
|
)
|
||||||
|
bind_addrs = [rando.unwrap()]
|
||||||
|
|
||||||
|
async def _main():
|
||||||
|
async with tractor.open_root_actor(
|
||||||
|
registry_addrs=[reg_addr],
|
||||||
|
tpt_bind_addrs=bind_addrs,
|
||||||
|
debug_mode=debug_mode,
|
||||||
|
):
|
||||||
|
actor = tractor.current_actor()
|
||||||
|
assert not actor.is_registrar
|
||||||
|
|
||||||
|
bound = actor.accept_addrs
|
||||||
|
assert len(bound) == len(bind_addrs)
|
||||||
|
|
||||||
|
# bindspaces must match
|
||||||
|
bound_bs = _bound_bindspaces(actor)
|
||||||
|
for ba in bind_addrs:
|
||||||
|
assert wrap_address(ba).bindspace in bound_bs
|
||||||
|
|
||||||
|
# TCP port=0 should resolve to a real port
|
||||||
|
for uw_addr in bound:
|
||||||
|
w = wrap_address(uw_addr)
|
||||||
|
if w.proto_key == 'tcp':
|
||||||
|
_host, port = uw_addr
|
||||||
|
assert port > 0
|
||||||
|
|
||||||
|
trio.run(_main)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 3) Non-registrar, default random bind (baseline)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_non_registrar_default_random_bind(
|
||||||
|
daemon,
|
||||||
|
reg_addr: tuple,
|
||||||
|
debug_mode: bool,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Baseline: no `tpt_bind_addrs`, daemon running.
|
||||||
|
Bound bindspace matches registry bindspace,
|
||||||
|
but bound addr differs from reg_addr (random).
|
||||||
|
|
||||||
|
'''
|
||||||
|
reg_wrapped = wrap_address(reg_addr)
|
||||||
|
|
||||||
|
async def _main():
|
||||||
|
async with tractor.open_root_actor(
|
||||||
|
registry_addrs=[reg_addr],
|
||||||
|
debug_mode=debug_mode,
|
||||||
|
):
|
||||||
|
actor = tractor.current_actor()
|
||||||
|
assert not actor.is_registrar
|
||||||
|
|
||||||
|
bound_bs = _bound_bindspaces(actor)
|
||||||
|
assert reg_wrapped.bindspace in bound_bs
|
||||||
|
|
||||||
|
# bound addr should differ from the registry addr
|
||||||
|
# (the runtime picks a random port/path)
|
||||||
|
bound_w = _bound_wrapped(actor)
|
||||||
|
assert reg_wrapped not in bound_w
|
||||||
|
|
||||||
|
trio.run(_main)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 4) Multiaddr string input
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_tpt_bind_addrs_as_maddr_str(
|
||||||
|
reg_addr: tuple,
|
||||||
|
debug_mode: bool,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Pass multiaddr strings as `tpt_bind_addrs`.
|
||||||
|
Runtime should parse and bind successfully.
|
||||||
|
|
||||||
|
'''
|
||||||
|
reg_wrapped = wrap_address(reg_addr)
|
||||||
|
# build a port-0 / random maddr string for binding
|
||||||
|
rando = reg_wrapped.get_random(
|
||||||
|
bindspace=reg_wrapped.bindspace,
|
||||||
|
)
|
||||||
|
maddr_str: str = str(mk_maddr(rando))
|
||||||
|
|
||||||
|
async def _main():
|
||||||
|
async with tractor.open_root_actor(
|
||||||
|
registry_addrs=[reg_addr],
|
||||||
|
tpt_bind_addrs=[maddr_str],
|
||||||
|
debug_mode=debug_mode,
|
||||||
|
):
|
||||||
|
actor = tractor.current_actor()
|
||||||
|
assert actor.is_registrar
|
||||||
|
|
||||||
|
for uw_addr in actor.accept_addrs:
|
||||||
|
w = wrap_address(uw_addr)
|
||||||
|
if w.proto_key == 'tcp':
|
||||||
|
_host, port = uw_addr
|
||||||
|
assert port > 0
|
||||||
|
|
||||||
|
trio.run(_main)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 5) Registrar merge produces union of binds
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_registrar_merge_binds_union(
|
||||||
|
tpt_proto: str,
|
||||||
|
debug_mode: bool,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Registrar + disjoint bind addr: bound set must include
|
||||||
|
both registry and explicit bind addresses.
|
||||||
|
|
||||||
|
'''
|
||||||
|
reg_addr = get_rando_addr(tpt_proto)
|
||||||
|
reg_wrapped = wrap_address(reg_addr)
|
||||||
|
|
||||||
|
rando = reg_wrapped.get_random(
|
||||||
|
bindspace=reg_wrapped.bindspace,
|
||||||
|
)
|
||||||
|
bind_addrs = [rando.unwrap()]
|
||||||
|
|
||||||
|
# NOTE: for UDS, `get_random()` produces the same
|
||||||
|
# filename for the same pid+actor-state, so the
|
||||||
|
# "disjoint" premise only holds when the addrs
|
||||||
|
# actually differ (always true for TCP, may
|
||||||
|
# collide for UDS).
|
||||||
|
expect_disjoint: bool = (
|
||||||
|
tuple(reg_addr) != rando.unwrap()
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _main():
|
||||||
|
async with tractor.open_root_actor(
|
||||||
|
registry_addrs=[reg_addr],
|
||||||
|
tpt_bind_addrs=bind_addrs,
|
||||||
|
debug_mode=debug_mode,
|
||||||
|
):
|
||||||
|
actor = tractor.current_actor()
|
||||||
|
assert actor.is_registrar
|
||||||
|
|
||||||
|
bound = actor.accept_addrs
|
||||||
|
bound_w = _bound_wrapped(actor)
|
||||||
|
|
||||||
|
if expect_disjoint:
|
||||||
|
# must have at least 2 (registry + bind)
|
||||||
|
assert len(bound) >= 2
|
||||||
|
|
||||||
|
# registry addr must appear in bound set
|
||||||
|
assert reg_wrapped in bound_w
|
||||||
|
|
||||||
|
trio.run(_main)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 6) open_nursery forwards tpt_bind_addrs
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_open_nursery_forwards_tpt_bind_addrs(
|
||||||
|
reg_addr: tuple,
|
||||||
|
debug_mode: bool,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
`open_nursery(tpt_bind_addrs=...)` forwards through
|
||||||
|
`**kwargs` to `open_root_actor()`.
|
||||||
|
|
||||||
|
'''
|
||||||
|
reg_wrapped = wrap_address(reg_addr)
|
||||||
|
rando = reg_wrapped.get_random(
|
||||||
|
bindspace=reg_wrapped.bindspace,
|
||||||
|
)
|
||||||
|
bind_addrs = [rando.unwrap()]
|
||||||
|
|
||||||
|
async def _main():
|
||||||
|
async with tractor.open_nursery(
|
||||||
|
registry_addrs=[reg_addr],
|
||||||
|
tpt_bind_addrs=bind_addrs,
|
||||||
|
debug_mode=debug_mode,
|
||||||
|
):
|
||||||
|
actor = tractor.current_actor()
|
||||||
|
bound_bs = _bound_bindspaces(actor)
|
||||||
|
|
||||||
|
for ba in bind_addrs:
|
||||||
|
assert wrap_address(ba).bindspace in bound_bs
|
||||||
|
|
||||||
|
trio.run(_main)
|
||||||
|
|
@ -24,6 +24,8 @@ from tractor._testing import (
|
||||||
expect_ctxc,
|
expect_ctxc,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .conftest import cpu_scaling_factor
|
||||||
|
|
||||||
# XXX TODO cases:
|
# XXX TODO cases:
|
||||||
# - [x] WE cancelled the peer and thus should not see any raised
|
# - [x] WE cancelled the peer and thus should not see any raised
|
||||||
# `ContextCancelled` as it should be reaped silently?
|
# `ContextCancelled` as it should be reaped silently?
|
||||||
|
|
@ -1030,6 +1032,7 @@ def test_peer_spawns_and_cancels_service_subactor(
|
||||||
reg_addr: tuple[str, int],
|
reg_addr: tuple[str, int],
|
||||||
raise_sub_spawn_error_after: float|None,
|
raise_sub_spawn_error_after: float|None,
|
||||||
loglevel: str,
|
loglevel: str,
|
||||||
|
test_log: tractor.log.StackLevelAdapter,
|
||||||
# ^XXX, set to 'warning' to see masked-exc warnings
|
# ^XXX, set to 'warning' to see masked-exc warnings
|
||||||
# that may transpire during actor-nursery teardown.
|
# that may transpire during actor-nursery teardown.
|
||||||
):
|
):
|
||||||
|
|
@ -1250,9 +1253,20 @@ def test_peer_spawns_and_cancels_service_subactor(
|
||||||
|
|
||||||
# assert spawn_ctx.cancelled_caught
|
# assert spawn_ctx.cancelled_caught
|
||||||
|
|
||||||
|
|
||||||
async def _main():
|
async def _main():
|
||||||
|
headroom: float = cpu_scaling_factor()
|
||||||
|
this_fast_on_linux: float = 3
|
||||||
|
this_fast = this_fast_on_linux * headroom
|
||||||
|
if headroom != 1.:
|
||||||
|
test_log.warning(
|
||||||
|
f'Adding latency headroom on linux bc CPU scaling,\n'
|
||||||
|
f'headroom: {headroom}\n'
|
||||||
|
f'this_fast_on_linux: {this_fast_on_linux} -> {this_fast}\n'
|
||||||
|
)
|
||||||
with trio.fail_after(
|
with trio.fail_after(
|
||||||
3 if not debug_mode
|
this_fast
|
||||||
|
if not debug_mode
|
||||||
else 999
|
else 999
|
||||||
):
|
):
|
||||||
await main()
|
await main()
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,7 @@ async def get_root_portal(
|
||||||
|
|
||||||
# connect back to our immediate parent which should also
|
# connect back to our immediate parent which should also
|
||||||
# be the actor-tree's root.
|
# be the actor-tree's root.
|
||||||
from tractor.discovery._discovery import get_root
|
from tractor.discovery._api import get_root
|
||||||
ptl: Portal
|
ptl: Portal
|
||||||
async with get_root() as ptl:
|
async with get_root() as ptl:
|
||||||
root_aid: Aid = ptl.chan.aid
|
root_aid: Aid = ptl.chan.aid
|
||||||
|
|
|
||||||
|
|
@ -213,9 +213,12 @@ def test_open_local_sub_to_stream(
|
||||||
N local tasks using `trionics.maybe_open_context()`.
|
N local tasks using `trionics.maybe_open_context()`.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
timeout: float = 3.6
|
from .conftest import cpu_scaling_factor
|
||||||
if platform.system() == "Windows":
|
timeout: float = (
|
||||||
timeout: float = 10
|
4
|
||||||
|
if not platform.system() == "Windows"
|
||||||
|
else 10
|
||||||
|
) * cpu_scaling_factor()
|
||||||
|
|
||||||
if debug_mode:
|
if debug_mode:
|
||||||
timeout = 999
|
timeout = 999
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ async def spawn(
|
||||||
assert actor is None # no runtime yet
|
assert actor is None # no runtime yet
|
||||||
async with (
|
async with (
|
||||||
tractor.open_root_actor(
|
tractor.open_root_actor(
|
||||||
arbiter_addr=reg_addr,
|
registry_addrs=[reg_addr],
|
||||||
),
|
),
|
||||||
tractor.open_nursery() as an,
|
tractor.open_nursery() as an,
|
||||||
):
|
):
|
||||||
|
|
@ -203,7 +203,7 @@ def test_loglevel_propagated_to_subactor(
|
||||||
async with tractor.open_nursery(
|
async with tractor.open_nursery(
|
||||||
name='registrar',
|
name='registrar',
|
||||||
start_method=start_method,
|
start_method=start_method,
|
||||||
arbiter_addr=reg_addr,
|
registry_addrs=[reg_addr],
|
||||||
|
|
||||||
) as tn:
|
) as tn:
|
||||||
await tn.run_in_actor(
|
await tn.run_in_actor(
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ async def open_sequence_streamer(
|
||||||
) -> tractor.MsgStream:
|
) -> tractor.MsgStream:
|
||||||
|
|
||||||
async with tractor.open_nursery(
|
async with tractor.open_nursery(
|
||||||
arbiter_addr=reg_addr,
|
registry_addrs=[reg_addr],
|
||||||
start_method=start_method,
|
start_method=start_method,
|
||||||
) as an:
|
) as an:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ from ._streaming import (
|
||||||
MsgStream as MsgStream,
|
MsgStream as MsgStream,
|
||||||
stream as stream,
|
stream as stream,
|
||||||
)
|
)
|
||||||
from .discovery._discovery import (
|
from .discovery._api import (
|
||||||
get_registry as get_registry,
|
get_registry as get_registry,
|
||||||
find_actor as find_actor,
|
find_actor as find_actor,
|
||||||
wait_for_actor as wait_for_actor,
|
wait_for_actor as wait_for_actor,
|
||||||
|
|
|
||||||
|
|
@ -145,11 +145,16 @@ async def maybe_block_bp(
|
||||||
@acm
|
@acm
|
||||||
async def open_root_actor(
|
async def open_root_actor(
|
||||||
*,
|
*,
|
||||||
# defaults are above
|
tpt_bind_addrs: list[
|
||||||
registry_addrs: list[UnwrappedAddress]|None = None,
|
Address # `Address.get_random()` case
|
||||||
|
|UnwrappedAddress # registrar case `= uw_reg_addrs`
|
||||||
|
]|None = None,
|
||||||
|
|
||||||
# defaults are above
|
# defaults are above
|
||||||
arbiter_addr: tuple[UnwrappedAddress]|None = None,
|
registry_addrs: list[
|
||||||
|
Address
|
||||||
|
|UnwrappedAddress
|
||||||
|
]|None = None,
|
||||||
|
|
||||||
enable_transports: list[
|
enable_transports: list[
|
||||||
# TODO, this should eventually be the pairs as
|
# TODO, this should eventually be the pairs as
|
||||||
|
|
@ -268,15 +273,7 @@ async def open_root_actor(
|
||||||
if start_method is not None:
|
if start_method is not None:
|
||||||
_spawn.try_set_start_method(start_method)
|
_spawn.try_set_start_method(start_method)
|
||||||
|
|
||||||
if arbiter_addr is not None:
|
# XXX expect pre-unwrapped registrar addrs.
|
||||||
warnings.warn(
|
|
||||||
'`arbiter_addr` is now deprecated\n'
|
|
||||||
'Use `registry_addrs: list[tuple]` instead..',
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
uw_reg_addrs = [arbiter_addr]
|
|
||||||
|
|
||||||
uw_reg_addrs = registry_addrs
|
uw_reg_addrs = registry_addrs
|
||||||
if not uw_reg_addrs:
|
if not uw_reg_addrs:
|
||||||
uw_reg_addrs: list[UnwrappedAddress] = default_lo_addrs(
|
uw_reg_addrs: list[UnwrappedAddress] = default_lo_addrs(
|
||||||
|
|
@ -289,7 +286,6 @@ async def open_root_actor(
|
||||||
wrap_address(uw_addr)
|
wrap_address(uw_addr)
|
||||||
for uw_addr in uw_reg_addrs
|
for uw_addr in uw_reg_addrs
|
||||||
]
|
]
|
||||||
|
|
||||||
loglevel: str = (
|
loglevel: str = (
|
||||||
loglevel
|
loglevel
|
||||||
or
|
or
|
||||||
|
|
@ -386,10 +382,14 @@ async def open_root_actor(
|
||||||
addr,
|
addr,
|
||||||
)
|
)
|
||||||
|
|
||||||
tpt_bind_addrs: list[
|
if tpt_bind_addrs is None:
|
||||||
Address # `Address.get_random()` case
|
tpt_bind_addrs: list[Address] = []
|
||||||
|UnwrappedAddress # registrar case `= uw_reg_addrs`
|
else:
|
||||||
] = []
|
input_bind_addrs = list(tpt_bind_addrs)
|
||||||
|
tpt_bind_addrs: list[Address] = []
|
||||||
|
for addr in input_bind_addrs:
|
||||||
|
addr: Address = wrap_address(addr)
|
||||||
|
tpt_bind_addrs.append(addr)
|
||||||
|
|
||||||
# ------ NON-REGISTRAR ------
|
# ------ NON-REGISTRAR ------
|
||||||
# create a new root-actor instance.
|
# create a new root-actor instance.
|
||||||
|
|
@ -417,19 +417,22 @@ async def open_root_actor(
|
||||||
# a new NON-registrar, ROOT-actor.
|
# a new NON-registrar, ROOT-actor.
|
||||||
#
|
#
|
||||||
# XXX INSTEAD, bind random addrs using the same tpt
|
# XXX INSTEAD, bind random addrs using the same tpt
|
||||||
# proto.
|
# proto if not already provided.
|
||||||
for addr in ponged_addrs:
|
if not tpt_bind_addrs:
|
||||||
tpt_bind_addrs.append(
|
for addr in ponged_addrs:
|
||||||
# XXX, these are `Address` NOT `UnwrappedAddress`.
|
tpt_bind_addrs.append(
|
||||||
#
|
# XXX, these are `Address` NOT `UnwrappedAddress`.
|
||||||
# NOTE, in the case of posix/berkley socket
|
#
|
||||||
# protos we allocate port=0 such that the system
|
# NOTE, in the case of posix/berkley socket
|
||||||
# allocates a random value at bind time; this
|
# protos we allocate port=0 such that the system
|
||||||
# happens in the `.ipc.*` stack's backend.
|
# allocates a random value at bind time; this
|
||||||
addr.get_random(
|
# happens in the `.ipc.*` stack's backend.
|
||||||
bindspace=addr.bindspace,
|
addr.get_random(
|
||||||
|
bindspace=addr.bindspace,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
header: str = '-> Contacting existing registry @ '
|
||||||
|
|
||||||
# ------ REGISTRAR ------
|
# ------ REGISTRAR ------
|
||||||
# create a new "registry providing" root-actor instance.
|
# create a new "registry providing" root-actor instance.
|
||||||
|
|
@ -442,7 +445,11 @@ async def open_root_actor(
|
||||||
# following init steps are taken:
|
# following init steps are taken:
|
||||||
# - the tranport layer server is bound to each addr
|
# - the tranport layer server is bound to each addr
|
||||||
# pair defined in provided registry_addrs, or the default.
|
# pair defined in provided registry_addrs, or the default.
|
||||||
tpt_bind_addrs = uw_reg_addrs
|
tpt_bind_addrs = list(set(
|
||||||
|
tpt_bind_addrs
|
||||||
|
+
|
||||||
|
[wrap_address(a) for a in uw_reg_addrs]
|
||||||
|
))
|
||||||
|
|
||||||
# - it is normally desirable for any registrar to stay up
|
# - it is normally desirable for any registrar to stay up
|
||||||
# indefinitely until either all registered (child/sub)
|
# indefinitely until either all registered (child/sub)
|
||||||
|
|
@ -464,6 +471,7 @@ async def open_root_actor(
|
||||||
# `tractor.to_asyncio.run_as_asyncio_guest()` and NOT
|
# `tractor.to_asyncio.run_as_asyncio_guest()` and NOT
|
||||||
# `.trio.run()`.
|
# `.trio.run()`.
|
||||||
actor._infected_aio = _state._runtime_vars['_is_infected_aio']
|
actor._infected_aio = _state._runtime_vars['_is_infected_aio']
|
||||||
|
header: str = '-> Opening new registry @ '
|
||||||
|
|
||||||
# Start up main task set via core actor-runtime nurseries.
|
# Start up main task set via core actor-runtime nurseries.
|
||||||
try:
|
try:
|
||||||
|
|
@ -475,7 +483,7 @@ async def open_root_actor(
|
||||||
report: str = f'Starting actor-runtime for {actor.aid.reprol()!r}\n'
|
report: str = f'Starting actor-runtime for {actor.aid.reprol()!r}\n'
|
||||||
if reg_addrs := actor.registry_addrs:
|
if reg_addrs := actor.registry_addrs:
|
||||||
report += (
|
report += (
|
||||||
'-> Opening new registry @ '
|
header
|
||||||
+
|
+
|
||||||
'\n'.join(
|
'\n'.join(
|
||||||
f'{addr}' for addr in reg_addrs
|
f'{addr}' for addr in reg_addrs
|
||||||
|
|
|
||||||
|
|
@ -1013,7 +1013,7 @@ async def request_root_stdio_lock(
|
||||||
DebugStatus.req_task = current_task()
|
DebugStatus.req_task = current_task()
|
||||||
req_err: BaseException|None = None
|
req_err: BaseException|None = None
|
||||||
try:
|
try:
|
||||||
from tractor.discovery._discovery import get_root
|
from tractor.discovery._api import get_root
|
||||||
# NOTE: we need this to ensure that this task exits
|
# NOTE: we need this to ensure that this task exits
|
||||||
# BEFORE the REPl instance raises an error like
|
# BEFORE the REPl instance raises an error like
|
||||||
# `bdb.BdbQuit` directly, OW you get a trio cs stack
|
# `bdb.BdbQuit` directly, OW you get a trio cs stack
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,15 @@
|
||||||
Discovery (protocols) API for automatic addressing
|
Discovery (protocols) API for automatic addressing
|
||||||
and location management of (service) actors.
|
and location management of (service) actors.
|
||||||
|
|
||||||
NOTE: to avoid circular imports, this ``__init__``
|
NOTE: this ``__init__`` only eagerly imports the
|
||||||
does NOT eagerly import submodules. Use direct
|
``._multiaddr`` submodule (for public re-exports).
|
||||||
module paths like ``tractor.discovery._addr`` or
|
Heavier submodules like ``._addr`` and ``._api``
|
||||||
``tractor.discovery._discovery`` instead.
|
are NOT imported here to avoid circular imports;
|
||||||
|
use direct module paths for those.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
from ._multiaddr import (
|
||||||
|
parse_endpoints as parse_endpoints,
|
||||||
|
parse_maddr as parse_maddr,
|
||||||
|
mk_maddr as mk_maddr,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -206,7 +206,7 @@ def mk_uuid() -> str:
|
||||||
|
|
||||||
|
|
||||||
def wrap_address(
|
def wrap_address(
|
||||||
addr: UnwrappedAddress
|
addr: UnwrappedAddress|str,
|
||||||
) -> Address:
|
) -> Address:
|
||||||
'''
|
'''
|
||||||
Wrap an `UnwrappedAddress` as an `Address`-type based
|
Wrap an `UnwrappedAddress` as an `Address`-type based
|
||||||
|
|
@ -257,6 +257,14 @@ def wrap_address(
|
||||||
cls: Type[Address] = get_address_cls(_def_tpt_proto)
|
cls: Type[Address] = get_address_cls(_def_tpt_proto)
|
||||||
addr: UnwrappedAddress = cls.get_root().unwrap()
|
addr: UnwrappedAddress = cls.get_root().unwrap()
|
||||||
|
|
||||||
|
# multiaddr-format string, e.g.
|
||||||
|
# '/ip4/127.0.0.1/tcp/1616'
|
||||||
|
case str() if addr.startswith('/'):
|
||||||
|
from tractor.discovery._multiaddr import (
|
||||||
|
parse_maddr,
|
||||||
|
)
|
||||||
|
return parse_maddr(addr)
|
||||||
|
|
||||||
case _:
|
case _:
|
||||||
# import pdbp; pdbp.set_trace()
|
# import pdbp; pdbp.set_trace()
|
||||||
# from tractor.devx import mk_pdb
|
# from tractor.devx import mk_pdb
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ management of (service) actors.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
import ipaddress
|
||||||
|
import socket
|
||||||
from typing import (
|
from typing import (
|
||||||
AsyncGenerator,
|
AsyncGenerator,
|
||||||
AsyncContextManager,
|
AsyncContextManager,
|
||||||
|
|
@ -33,10 +35,12 @@ from ..trionics import (
|
||||||
collapse_eg,
|
collapse_eg,
|
||||||
)
|
)
|
||||||
from ..ipc import _connect_chan, Channel
|
from ..ipc import _connect_chan, Channel
|
||||||
|
from ..ipc._tcp import TCPAddress
|
||||||
|
from ..ipc._uds import UDSAddress
|
||||||
from ._addr import (
|
from ._addr import (
|
||||||
UnwrappedAddress,
|
UnwrappedAddress,
|
||||||
Address,
|
Address,
|
||||||
wrap_address
|
wrap_address,
|
||||||
)
|
)
|
||||||
from ..runtime._portal import (
|
from ..runtime._portal import (
|
||||||
Portal,
|
Portal,
|
||||||
|
|
@ -56,6 +60,94 @@ if TYPE_CHECKING:
|
||||||
log = get_logger()
|
log = get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
def _is_local_addr(addr: Address) -> bool:
|
||||||
|
'''
|
||||||
|
Determine whether `addr` is reachable on the
|
||||||
|
local host by inspecting address type and
|
||||||
|
comparing hostnames/PIDs.
|
||||||
|
|
||||||
|
- `UDSAddress` is always local (filesystem-bound)
|
||||||
|
- `TCPAddress` is local when its host is a
|
||||||
|
loopback IP or matches one of the machine's
|
||||||
|
own interface addresses.
|
||||||
|
|
||||||
|
'''
|
||||||
|
if isinstance(addr, UDSAddress):
|
||||||
|
return True
|
||||||
|
|
||||||
|
if isinstance(addr, TCPAddress):
|
||||||
|
try:
|
||||||
|
ip = ipaddress.ip_address(addr._host)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if ip.is_loopback:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# check if this IP belongs to any of our
|
||||||
|
# local network interfaces.
|
||||||
|
try:
|
||||||
|
local_ips: set[str] = {
|
||||||
|
info[4][0]
|
||||||
|
for info in socket.getaddrinfo(
|
||||||
|
socket.gethostname(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return addr._host in local_ips
|
||||||
|
except socket.gaierror:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def prefer_addr(
|
||||||
|
addrs: list[UnwrappedAddress],
|
||||||
|
) -> UnwrappedAddress:
|
||||||
|
'''
|
||||||
|
Select the "best" transport address from a
|
||||||
|
multihomed actor's address list based on
|
||||||
|
locality heuristics.
|
||||||
|
|
||||||
|
Preference order (highest -> lowest):
|
||||||
|
1. UDS (same-host guaranteed, lowest overhead)
|
||||||
|
2. TCP loopback / same-host IP
|
||||||
|
3. TCP remote (only option for distributed)
|
||||||
|
|
||||||
|
When multiple addrs share the same priority
|
||||||
|
tier, the last-registered (latest) entry is
|
||||||
|
preferred.
|
||||||
|
|
||||||
|
'''
|
||||||
|
if len(addrs) == 1:
|
||||||
|
return addrs[0]
|
||||||
|
|
||||||
|
local_uds: list[UnwrappedAddress] = []
|
||||||
|
local_tcp: list[UnwrappedAddress] = []
|
||||||
|
remote: list[UnwrappedAddress] = []
|
||||||
|
|
||||||
|
for unwrapped in addrs:
|
||||||
|
wrapped: Address = wrap_address(unwrapped)
|
||||||
|
if isinstance(wrapped, UDSAddress):
|
||||||
|
local_uds.append(unwrapped)
|
||||||
|
elif _is_local_addr(wrapped):
|
||||||
|
local_tcp.append(unwrapped)
|
||||||
|
else:
|
||||||
|
remote.append(unwrapped)
|
||||||
|
|
||||||
|
# prefer UDS > local TCP > remote TCP;
|
||||||
|
# within each tier take the latest entry.
|
||||||
|
if local_uds:
|
||||||
|
return local_uds[-1]
|
||||||
|
if local_tcp:
|
||||||
|
return local_tcp[-1]
|
||||||
|
if remote:
|
||||||
|
return remote[-1]
|
||||||
|
|
||||||
|
# fallback: last registered addr
|
||||||
|
return addrs[-1]
|
||||||
|
|
||||||
|
|
||||||
@acm
|
@acm
|
||||||
async def get_registry(
|
async def get_registry(
|
||||||
addr: UnwrappedAddress|None = None,
|
addr: UnwrappedAddress|None = None,
|
||||||
|
|
@ -187,13 +279,17 @@ async def query_actor(
|
||||||
reg_portal: Portal|LocalPortal
|
reg_portal: Portal|LocalPortal
|
||||||
regaddr: Address = wrap_address(regaddr) or actor.reg_addrs[0]
|
regaddr: Address = wrap_address(regaddr) or actor.reg_addrs[0]
|
||||||
async with get_registry(regaddr) as reg_portal:
|
async with get_registry(regaddr) as reg_portal:
|
||||||
# TODO: return portals to all available actors - for now
|
addrs: list[UnwrappedAddress]|None = (
|
||||||
# just the last one that registered
|
await reg_portal.run_from_ns(
|
||||||
addr: UnwrappedAddress = await reg_portal.run_from_ns(
|
'self',
|
||||||
'self',
|
'find_actor',
|
||||||
'find_actor',
|
name=name,
|
||||||
name=name,
|
)
|
||||||
)
|
)
|
||||||
|
if addrs:
|
||||||
|
addr: UnwrappedAddress = prefer_addr(addrs)
|
||||||
|
else:
|
||||||
|
addr = None
|
||||||
yield addr, reg_portal
|
yield addr, reg_portal
|
||||||
|
|
||||||
@acm
|
@acm
|
||||||
|
|
@ -370,9 +466,9 @@ async def wait_for_actor(
|
||||||
name=name,
|
name=name,
|
||||||
)
|
)
|
||||||
|
|
||||||
# get latest registered addr by default?
|
# select the best transport addr from
|
||||||
# TODO: offer multi-portal yields in multi-homed case?
|
# the (possibly multihomed) addr list.
|
||||||
addr: UnwrappedAddress = addrs[-1]
|
addr: UnwrappedAddress = prefer_addr(addrs)
|
||||||
|
|
||||||
async with _connect_chan(addr) as chan:
|
async with _connect_chan(addr) as chan:
|
||||||
async with open_portal(chan) as portal:
|
async with open_portal(chan) as portal:
|
||||||
|
|
@ -15,137 +15,176 @@
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Multiaddress parser and utils according the spec(s) defined by
|
Multiaddress support using the upstream `py-multiaddr` lib
|
||||||
`libp2p` and used in dependent project such as `ipfs`:
|
(a `libp2p` community standard) instead of our own NIH parser.
|
||||||
|
|
||||||
- https://docs.libp2p.io/concepts/fundamentals/addressing/
|
- https://github.com/multiformats/multiaddr
|
||||||
- https://github.com/libp2p/specs/blob/master/addressing/README.md
|
- https://github.com/multiformats/py-multiaddr
|
||||||
|
- https://github.com/multiformats/multiaddr/blob/master/protocols.csv
|
||||||
|
- https://github.com/multiformats/multiaddr/blob/master/protocols/unix.md
|
||||||
|
|
||||||
'''
|
'''
|
||||||
from typing import Iterator
|
import ipaddress
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from bidict import bidict
|
from multiaddr import Multiaddr
|
||||||
|
|
||||||
# TODO: see if we can leverage libp2p ecosys projects instead of
|
if TYPE_CHECKING:
|
||||||
# rolling our own (parser) impls of the above addressing specs:
|
from tractor.discovery._addr import Address
|
||||||
# - https://github.com/libp2p/py-libp2p
|
|
||||||
# - https://docs.libp2p.io/concepts/nat/circuit-relay/#relay-addresses
|
|
||||||
# prots: bidict[int, str] = bidict({
|
|
||||||
prots: bidict[int, str] = {
|
|
||||||
'ipv4': 3,
|
|
||||||
'ipv6': 3,
|
|
||||||
'wg': 3,
|
|
||||||
|
|
||||||
'tcp': 4,
|
# map from tractor-internal `proto_key` identifiers
|
||||||
'udp': 4,
|
# to the standard multiaddr protocol name strings.
|
||||||
|
_tpt_proto_to_maddr: dict[str, str] = {
|
||||||
# TODO: support the next-gen shite Bo
|
'tcp': 'tcp',
|
||||||
# 'quic': 4,
|
'uds': 'unix',
|
||||||
# 'ssh': 7, # via rsyscall bootstrapping
|
|
||||||
}
|
}
|
||||||
|
|
||||||
prot_params: dict[str, tuple[str]] = {
|
# reverse mapping: multiaddr protocol name -> tractor proto_key
|
||||||
'ipv4': ('addr',),
|
_maddr_to_tpt_proto: dict[str, str] = {
|
||||||
'ipv6': ('addr',),
|
v: k for k, v in _tpt_proto_to_maddr.items()
|
||||||
'wg': ('addr', 'port', 'pubkey'),
|
|
||||||
|
|
||||||
'tcp': ('port',),
|
|
||||||
'udp': ('port',),
|
|
||||||
|
|
||||||
# 'quic': ('port',),
|
|
||||||
# 'ssh': ('port',),
|
|
||||||
}
|
}
|
||||||
|
# -> {'tcp': 'tcp', 'unix': 'uds'}
|
||||||
|
|
||||||
|
|
||||||
def iter_prot_layers(
|
def mk_maddr(
|
||||||
multiaddr: str,
|
addr: 'Address',
|
||||||
) -> Iterator[
|
) -> Multiaddr:
|
||||||
tuple[
|
|
||||||
int,
|
|
||||||
list[str]
|
|
||||||
]
|
|
||||||
]:
|
|
||||||
'''
|
'''
|
||||||
Unpack a libp2p style "multiaddress" into multiple "segments"
|
Construct a `Multiaddr` from a tractor `Address` instance,
|
||||||
for each "layer" of the protocoll stack (in OSI terms).
|
dispatching on the `.proto_key` to build the correct
|
||||||
|
multiaddr-spec-compliant protocol path.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
tokens: list[str] = multiaddr.split('/')
|
proto_key: str = addr.proto_key
|
||||||
root, tokens = tokens[0], tokens[1:]
|
maddr_proto: str|None = _tpt_proto_to_maddr.get(proto_key)
|
||||||
assert not root # there is a root '/' on LHS
|
if maddr_proto is None:
|
||||||
itokens = iter(tokens)
|
raise ValueError(
|
||||||
|
f'Unsupported proto_key: {proto_key!r}'
|
||||||
|
)
|
||||||
|
|
||||||
prot: str | None = None
|
match proto_key:
|
||||||
params: list[str] = []
|
case 'tcp':
|
||||||
for token in itokens:
|
host, port = addr.unwrap()
|
||||||
# every prot path should start with a known
|
ip = ipaddress.ip_address(host)
|
||||||
# key-str.
|
net_proto: str = (
|
||||||
if token in prots:
|
'ip4' if ip.version == 4
|
||||||
if prot is None:
|
else 'ip6'
|
||||||
prot: str = token
|
)
|
||||||
else:
|
return Multiaddr(
|
||||||
yield prot, params
|
f'/{net_proto}/{host}/{maddr_proto}/{port}'
|
||||||
prot = token
|
)
|
||||||
|
|
||||||
params = []
|
case 'uds':
|
||||||
|
filedir, filename = addr.unwrap()
|
||||||
elif token not in prots:
|
filepath = Path(filedir) / filename
|
||||||
params.append(token)
|
# NOTE, strip any leading `/` to avoid
|
||||||
|
# double-slash `/unix//run/..` which the
|
||||||
else:
|
# multiaddr parser rejects as "empty
|
||||||
yield prot, params
|
# protocol path".
|
||||||
|
fpath_str: str = str(filepath).lstrip('/')
|
||||||
|
return Multiaddr(
|
||||||
|
f'/{maddr_proto}/{fpath_str}'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def parse_maddr(
|
def parse_maddr(
|
||||||
multiaddr: str,
|
maddr_str: str,
|
||||||
) -> dict[str, str | int | dict]:
|
) -> 'Address':
|
||||||
'''
|
'''
|
||||||
Parse a libp2p style "multiaddress" into its distinct protocol
|
Parse a multiaddr string into a tractor `Address`.
|
||||||
segments where each segment is of the form:
|
|
||||||
|
|
||||||
`../<protocol>/<param0>/<param1>/../<paramN>`
|
Inverse of `mk_maddr()`.
|
||||||
|
|
||||||
and is loaded into a (order preserving) `layers: dict[str,
|
|
||||||
dict[str, Any]` which holds each protocol-layer-segment of the
|
|
||||||
original `str` path as a separate entry according to its approx
|
|
||||||
OSI "layer number".
|
|
||||||
|
|
||||||
Any `paramN` in the path must be distinctly defined by a str-token in the
|
|
||||||
(module global) `prot_params` table.
|
|
||||||
|
|
||||||
For eg. for wireguard which requires an address, port number and publickey
|
|
||||||
the protocol params are specified as the entry:
|
|
||||||
|
|
||||||
'wg': ('addr', 'port', 'pubkey'),
|
|
||||||
|
|
||||||
and are thus parsed from a maddr in that order:
|
|
||||||
`'/wg/1.1.1.1/51820/<pubkey>'`
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
layers: dict[str, str | int | dict] = {}
|
# lazy imports to avoid circular deps
|
||||||
|
from tractor.ipc._tcp import TCPAddress
|
||||||
|
from tractor.ipc._uds import UDSAddress
|
||||||
|
|
||||||
|
maddr = Multiaddr(maddr_str)
|
||||||
|
proto_names: list[str] = [
|
||||||
|
p.name for p in maddr.protocols()
|
||||||
|
]
|
||||||
|
|
||||||
|
match proto_names:
|
||||||
|
case [('ip4' | 'ip6') as net_proto, 'tcp']:
|
||||||
|
host: str = maddr.value_for_protocol(net_proto)
|
||||||
|
port: int = int(maddr.value_for_protocol('tcp'))
|
||||||
|
return TCPAddress(host, port)
|
||||||
|
|
||||||
|
case ['unix']:
|
||||||
|
# NOTE, the multiaddr lib prepends a `/` to the
|
||||||
|
# unix protocol value which effectively restores
|
||||||
|
# the absolute-path semantics that `mk_maddr()`
|
||||||
|
# strips when building the multiaddr string.
|
||||||
|
raw: str = maddr.value_for_protocol('unix')
|
||||||
|
sockpath = Path(raw)
|
||||||
|
return UDSAddress(
|
||||||
|
filedir=sockpath.parent,
|
||||||
|
filename=sockpath.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
case _:
|
||||||
|
raise ValueError(
|
||||||
|
f'Unsupported multiaddr protocol combo: '
|
||||||
|
f'{proto_names!r}\n'
|
||||||
|
f'from maddr: {maddr_str!r}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# type aliases for service-endpoint config tables
|
||||||
|
#
|
||||||
|
# input table: actor/service name -> list of maddr strings
|
||||||
|
# or raw unwrapped-address tuples (as accepted by
|
||||||
|
# `wrap_address()`).
|
||||||
|
EndpointsTable = dict[
|
||||||
|
str, # actor/service name
|
||||||
|
list[str|tuple], # maddr strs or UnwrappedAddress
|
||||||
|
]
|
||||||
|
|
||||||
|
# output table: actor/service name -> list of wrapped
|
||||||
|
# `Address` instances ready for transport binding.
|
||||||
|
ParsedEndpoints = dict[
|
||||||
|
str, # actor/service name
|
||||||
|
list['Address'],
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_endpoints(
|
||||||
|
service_table: EndpointsTable,
|
||||||
|
) -> ParsedEndpoints:
|
||||||
|
'''
|
||||||
|
Parse a service-endpoint config table into wrapped
|
||||||
|
`Address` instances suitable for transport binding.
|
||||||
|
|
||||||
|
Each key is an actor/service name and each value is
|
||||||
|
a list of addresses in any format accepted by
|
||||||
|
`wrap_address()`:
|
||||||
|
|
||||||
|
- multiaddr strings: ``'/ip4/127.0.0.1/tcp/1616'``
|
||||||
|
- UDS multiaddr strings using the **multiaddr spec
|
||||||
|
name** ``/unix/...`` (NOT the tractor-internal
|
||||||
|
``/uds/`` proto_key)
|
||||||
|
- raw unwrapped tuples: ``('127.0.0.1', 1616)``
|
||||||
|
- pre-wrapped `Address` objects (passed through)
|
||||||
|
|
||||||
|
Returns a new `dict` with the same keys, where each
|
||||||
|
value list contains the corresponding `Address`
|
||||||
|
instances.
|
||||||
|
|
||||||
|
Raises `ValueError` for unsupported multiaddr
|
||||||
|
protocols (e.g. ``/udp/``).
|
||||||
|
|
||||||
|
'''
|
||||||
|
from tractor.discovery._addr import wrap_address
|
||||||
|
|
||||||
|
parsed: ParsedEndpoints = {}
|
||||||
for (
|
for (
|
||||||
prot_key,
|
actor_name,
|
||||||
params,
|
addr_entries,
|
||||||
) in iter_prot_layers(multiaddr):
|
) in service_table.items():
|
||||||
|
parsed[actor_name] = [
|
||||||
layer: int = prots[prot_key] # OSI layer used for sorting
|
wrap_address(entry)
|
||||||
ep: dict[str, int | str] = {'layer': layer}
|
for entry in addr_entries
|
||||||
layers[prot_key] = ep
|
]
|
||||||
|
return parsed
|
||||||
# TODO; validation and resolving of names:
|
|
||||||
# - each param via a validator provided as part of the
|
|
||||||
# prot_params def? (also see `"port"` case below..)
|
|
||||||
# - do a resolv step that will check addrs against
|
|
||||||
# any loaded network.resolv: dict[str, str]
|
|
||||||
rparams: list = list(reversed(params))
|
|
||||||
for key in prot_params[prot_key]:
|
|
||||||
val: str | int = rparams.pop()
|
|
||||||
|
|
||||||
# TODO: UGHH, dunno what we should do for validation
|
|
||||||
# here, put it in the params spec somehow?
|
|
||||||
if key == 'port':
|
|
||||||
val = int(val)
|
|
||||||
|
|
||||||
ep[key] = val
|
|
||||||
|
|
||||||
return layers
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,6 @@ name-to-address mappings so peers can discover each other.
|
||||||
'''
|
'''
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from bidict import bidict
|
|
||||||
import trio
|
import trio
|
||||||
|
|
||||||
from ..runtime._runtime import Actor
|
from ..runtime._runtime import Actor
|
||||||
|
|
@ -83,10 +82,10 @@ class Registrar(Actor):
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
self._registry: bidict[
|
self._registry: dict[
|
||||||
tuple[str, str],
|
tuple[str, str],
|
||||||
UnwrappedAddress,
|
list[UnwrappedAddress],
|
||||||
] = bidict({})
|
] = {}
|
||||||
self._waiters: dict[
|
self._waiters: dict[
|
||||||
str,
|
str,
|
||||||
# either an event to sync to receiving an
|
# either an event to sync to receiving an
|
||||||
|
|
@ -102,18 +101,17 @@ class Registrar(Actor):
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
|
|
||||||
) -> UnwrappedAddress|None:
|
) -> list[UnwrappedAddress]|None:
|
||||||
|
|
||||||
for uid, addr in self._registry.items():
|
for uid, addrs in self._registry.items():
|
||||||
if name in uid:
|
if name in uid:
|
||||||
return addr
|
return addrs if addrs else None
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def get_registry(
|
async def get_registry(
|
||||||
self
|
self
|
||||||
|
) -> dict[str, list[UnwrappedAddress]]:
|
||||||
) -> dict[str, UnwrappedAddress]:
|
|
||||||
'''
|
'''
|
||||||
Return current name registry.
|
Return current name registry.
|
||||||
|
|
||||||
|
|
@ -144,18 +142,17 @@ class Registrar(Actor):
|
||||||
|
|
||||||
'''
|
'''
|
||||||
addrs: list[UnwrappedAddress] = []
|
addrs: list[UnwrappedAddress] = []
|
||||||
addr: UnwrappedAddress
|
|
||||||
|
|
||||||
mailbox_info: str = (
|
mailbox_info: str = (
|
||||||
'Actor registry contact infos:\n'
|
'Actor registry contact infos:\n'
|
||||||
)
|
)
|
||||||
for uid, addr in self._registry.items():
|
for uid, uid_addrs in self._registry.items():
|
||||||
mailbox_info += (
|
mailbox_info += (
|
||||||
f'|_uid: {uid}\n'
|
f'|_uid: {uid}\n'
|
||||||
f'|_addr: {addr}\n\n'
|
f'|_addrs: {uid_addrs}\n\n'
|
||||||
)
|
)
|
||||||
if name == uid[0]:
|
if name == uid[0]:
|
||||||
addrs.append(addr)
|
addrs.extend(uid_addrs)
|
||||||
|
|
||||||
if not addrs:
|
if not addrs:
|
||||||
waiter = trio.Event()
|
waiter = trio.Event()
|
||||||
|
|
@ -166,7 +163,7 @@ class Registrar(Actor):
|
||||||
|
|
||||||
for uid in self._waiters[name]:
|
for uid in self._waiters[name]:
|
||||||
if not isinstance(uid, trio.Event):
|
if not isinstance(uid, trio.Event):
|
||||||
addrs.append(
|
addrs.extend(
|
||||||
self._registry[uid]
|
self._registry[uid]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -187,13 +184,26 @@ class Registrar(Actor):
|
||||||
# should never be 0-dynamic-os-alloc
|
# should never be 0-dynamic-os-alloc
|
||||||
await debug.pause()
|
await debug.pause()
|
||||||
|
|
||||||
# XXX NOTE, value must also be hashable AND since
|
addr_tup: tuple = tuple(addr)
|
||||||
# `._registry` is a `bidict` values must be unique;
|
|
||||||
# use `.forceput()` to replace any prior (stale)
|
# Evict stale entries: if a *different* uid claims
|
||||||
# entries that might map a different uid to the same
|
# this addr (e.g. after unclean shutdown or
|
||||||
# addr (e.g. after an unclean shutdown or
|
# actor-restart reusing the same address), remove
|
||||||
# actor-restart reusing the same address).
|
# it from the old uid's addr list.
|
||||||
self._registry.forceput(uid, tuple(addr))
|
for other_uid, other_addrs in self._registry.items():
|
||||||
|
if (
|
||||||
|
other_uid != uid
|
||||||
|
and addr_tup in other_addrs
|
||||||
|
):
|
||||||
|
other_addrs.remove(addr_tup)
|
||||||
|
if not other_addrs:
|
||||||
|
del self._registry[other_uid]
|
||||||
|
break
|
||||||
|
|
||||||
|
# Append to this uid's addr list (avoid duplicates)
|
||||||
|
entry: list = self._registry.setdefault(uid, [])
|
||||||
|
if addr_tup not in entry:
|
||||||
|
entry.append(addr_tup)
|
||||||
|
|
||||||
# pop and signal all waiter events
|
# pop and signal all waiter events
|
||||||
events = self._waiters.pop(name, [])
|
events = self._waiters.pop(name, [])
|
||||||
|
|
@ -210,7 +220,7 @@ class Registrar(Actor):
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
uid = (str(uid[0]), str(uid[1]))
|
uid = (str(uid[0]), str(uid[1]))
|
||||||
entry: tuple = self._registry.pop(
|
entry: list|None = self._registry.pop(
|
||||||
uid, None
|
uid, None
|
||||||
)
|
)
|
||||||
if entry is None:
|
if entry is None:
|
||||||
|
|
@ -225,13 +235,20 @@ class Registrar(Actor):
|
||||||
) -> tuple[str, str]|None:
|
) -> tuple[str, str]|None:
|
||||||
# NOTE: `addr` arrives as a `list` over IPC
|
# NOTE: `addr` arrives as a `list` over IPC
|
||||||
# (msgpack deserializes tuples -> lists) so
|
# (msgpack deserializes tuples -> lists) so
|
||||||
# coerce to `tuple` for the bidict hash lookup.
|
# coerce to `tuple` for the linear scan.
|
||||||
uid: tuple[str, str]|None = (
|
addr = tuple(addr)
|
||||||
self._registry.inverse.pop(
|
uid: tuple[str, str]|None = None
|
||||||
tuple(addr),
|
|
||||||
None,
|
for _uid, addrs in self._registry.items():
|
||||||
)
|
if addr in addrs:
|
||||||
)
|
addrs.remove(addr)
|
||||||
|
uid = _uid
|
||||||
|
# remove the uid entry entirely when it
|
||||||
|
# has no remaining addrs.
|
||||||
|
if not addrs:
|
||||||
|
del self._registry[_uid]
|
||||||
|
break
|
||||||
|
|
||||||
if uid:
|
if uid:
|
||||||
report: str = (
|
report: str = (
|
||||||
'Deleting registry-entry for,\n'
|
'Deleting registry-entry for,\n'
|
||||||
|
|
|
||||||
|
|
@ -274,7 +274,7 @@ async def maybe_wait_on_canced_subs(
|
||||||
# ephemeral `.register_actor()` request!
|
# ephemeral `.register_actor()` request!
|
||||||
# -[ ] also, that should be avoidable by
|
# -[ ] also, that should be avoidable by
|
||||||
# re-using any existing chan from the
|
# re-using any existing chan from the
|
||||||
# `._discovery.get_registry()` call as
|
# `._api.get_registry()` call as
|
||||||
# well..
|
# well..
|
||||||
log.runtime(
|
log.runtime(
|
||||||
f'Peer IPC broke but subproc is alive?\n\n'
|
f'Peer IPC broke but subproc is alive?\n\n'
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,10 @@ from trio import (
|
||||||
open_tcp_listeners,
|
open_tcp_listeners,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from multiaddr import Multiaddr
|
||||||
from tractor.msg import MsgCodec
|
from tractor.msg import MsgCodec
|
||||||
from tractor.log import get_logger
|
from tractor.log import get_logger
|
||||||
|
from tractor.discovery._multiaddr import mk_maddr
|
||||||
from tractor.ipc._transport import (
|
from tractor.ipc._transport import (
|
||||||
MsgTransport,
|
MsgTransport,
|
||||||
MsgpackTransport,
|
MsgpackTransport,
|
||||||
|
|
@ -198,21 +200,8 @@ class MsgpackTCPStream(MsgpackTransport):
|
||||||
layer_key: int = 4
|
layer_key: int = 4
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def maddr(self) -> str:
|
def maddr(self) -> Multiaddr:
|
||||||
host, port = self.raddr.unwrap()
|
return mk_maddr(self.raddr)
|
||||||
return (
|
|
||||||
# TODO, use `ipaddress` from stdlib to handle
|
|
||||||
# first detecting which of `ipv4/6` before
|
|
||||||
# choosing the routing prefix part.
|
|
||||||
f'/ipv4/{host}'
|
|
||||||
|
|
||||||
f'/{self.address_type.proto_key}/{port}'
|
|
||||||
# f'/{self.chan.uid[0]}'
|
|
||||||
# f'/{self.cid}'
|
|
||||||
|
|
||||||
# f'/cid={cid_head}..{cid_tail}'
|
|
||||||
# TODO: ? not use this ^ right ?
|
|
||||||
)
|
|
||||||
|
|
||||||
def connected(self) -> bool:
|
def connected(self) -> bool:
|
||||||
return self.stream.socket.fileno() != -1
|
return self.stream.socket.fileno() != -1
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ from typing import (
|
||||||
ClassVar,
|
ClassVar,
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
)
|
)
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from multiaddr import Multiaddr
|
||||||
from collections.abc import (
|
from collections.abc import (
|
||||||
AsyncGenerator,
|
AsyncGenerator,
|
||||||
AsyncIterator,
|
AsyncIterator,
|
||||||
|
|
@ -118,7 +120,7 @@ class MsgTransport(Protocol):
|
||||||
...
|
...
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def maddr(self) -> str:
|
def maddr(self) -> Multiaddr|str:
|
||||||
...
|
...
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,10 @@ from trio._highlevel_open_unix_stream import (
|
||||||
has_unix,
|
has_unix,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from multiaddr import Multiaddr
|
||||||
from tractor.msg import MsgCodec
|
from tractor.msg import MsgCodec
|
||||||
from tractor.log import get_logger
|
from tractor.log import get_logger
|
||||||
|
from tractor.discovery._multiaddr import mk_maddr
|
||||||
from tractor.ipc._transport import (
|
from tractor.ipc._transport import (
|
||||||
MsgpackTransport,
|
MsgpackTransport,
|
||||||
)
|
)
|
||||||
|
|
@ -230,7 +232,7 @@ class UDSAddress(
|
||||||
pid: str = '<unknown-peer-pid>'
|
pid: str = '<unknown-peer-pid>'
|
||||||
|
|
||||||
body: str = (
|
body: str = (
|
||||||
f'({self.filedir}, {self.filename}, {pid})'
|
f'({self.filedir}/, {self.filename}, {pid})'
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
f'{type(self).__name__}'
|
f'{type(self).__name__}'
|
||||||
|
|
@ -298,7 +300,23 @@ async def start_listener(
|
||||||
):
|
):
|
||||||
await sock.bind(str(bindpath))
|
await sock.bind(str(bindpath))
|
||||||
|
|
||||||
sock.listen(1)
|
# NOTE, the backlog must be large enough to handle
|
||||||
|
# concurrent connection attempts during actor teardown.
|
||||||
|
# Previously this was `listen(1)` which caused
|
||||||
|
# deregistration failures in the remote-daemon registrar
|
||||||
|
# case: when multiple sub-actors simultaneously try to
|
||||||
|
# connect to deregister, a backlog of 1 overflows and
|
||||||
|
# connections get ECONNREFUSED. This matches the TCP
|
||||||
|
# transport which uses `trio.open_tcp_listeners()` with
|
||||||
|
# a default backlog of ~128.
|
||||||
|
#
|
||||||
|
# For details see the `close_listener()` below which
|
||||||
|
# `os.unlink()`s the socket file on teardown — meaning
|
||||||
|
# any NEW connection attempts after that point will fail
|
||||||
|
# with `FileNotFoundError` regardless of backlog size.
|
||||||
|
# The backlog only matters while the listener is alive
|
||||||
|
# and accepting.
|
||||||
|
sock.listen(128)
|
||||||
log.info(
|
log.info(
|
||||||
f'Listening on UDS socket\n'
|
f'Listening on UDS socket\n'
|
||||||
f'[>\n'
|
f'[>\n'
|
||||||
|
|
@ -314,6 +332,16 @@ def close_listener(
|
||||||
'''
|
'''
|
||||||
Close and remove the listening unix socket's path.
|
Close and remove the listening unix socket's path.
|
||||||
|
|
||||||
|
NOTE, the `os.unlink()` here removes the socket file from
|
||||||
|
the filesystem immediately, which means any subsequent
|
||||||
|
connection attempts (e.g. sub-actors trying to deregister
|
||||||
|
with a registrar whose listener is tearing down) will fail
|
||||||
|
with `FileNotFoundError`. For the local-registrar case
|
||||||
|
(parent IS the registrar), `_runtime.async_main()` works
|
||||||
|
around this by reusing the existing `_parent_chan` instead
|
||||||
|
of opening a new connection; see the `parent_is_reg` logic
|
||||||
|
in the deregistration path.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
lstnr.socket.close()
|
lstnr.socket.close()
|
||||||
os.unlink(addr.sockpath)
|
os.unlink(addr.sockpath)
|
||||||
|
|
@ -442,19 +470,11 @@ class MsgpackUDSStream(MsgpackTransport):
|
||||||
layer_key: int = 4
|
layer_key: int = 4
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def maddr(self) -> str:
|
def maddr(self) -> Multiaddr|str:
|
||||||
if not self.raddr:
|
if not self.raddr:
|
||||||
return '<unknown-peer>'
|
return '<unknown-peer>'
|
||||||
|
|
||||||
filepath: Path = Path(self.raddr.unwrap()[0])
|
return mk_maddr(self.raddr)
|
||||||
return (
|
|
||||||
f'/{self.address_type.proto_key}/{filepath}'
|
|
||||||
# f'/{self.chan.uid[0]}'
|
|
||||||
# f'/{self.cid}'
|
|
||||||
|
|
||||||
# f'/cid={cid_head}..{cid_tail}'
|
|
||||||
# TODO: ? not use this ^ right ?
|
|
||||||
)
|
|
||||||
|
|
||||||
def connected(self) -> bool:
|
def connected(self) -> bool:
|
||||||
return self.stream.socket.fileno() != -1
|
return self.stream.socket.fileno() != -1
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ from ..devx import (
|
||||||
debug,
|
debug,
|
||||||
pformat as _pformat
|
pformat as _pformat
|
||||||
)
|
)
|
||||||
from ..discovery._discovery import get_registry
|
from ..discovery._api import get_registry
|
||||||
from ._portal import Portal
|
from ._portal import Portal
|
||||||
from . import _state
|
from . import _state
|
||||||
from ..spawn import _mp_fixup_main
|
from ..spawn import _mp_fixup_main
|
||||||
|
|
@ -1379,7 +1379,7 @@ class Actor:
|
||||||
# - `Channel.maddr() -> str:` obvi!
|
# - `Channel.maddr() -> str:` obvi!
|
||||||
# - `Context.maddr() -> str:`
|
# - `Context.maddr() -> str:`
|
||||||
tasks_str += (
|
tasks_str += (
|
||||||
f' |_@ /ipv4/tcp/cid="{ctx.cid[-16:]} .."\n'
|
f' |_@ /ip4/tcp/cid="{ctx.cid[-16:]} .."\n'
|
||||||
f' |>> {ctx._nsf}() -> dict:\n'
|
f' |>> {ctx._nsf}() -> dict:\n'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1564,7 +1564,15 @@ async def async_main(
|
||||||
addr: Address = transport_cls.get_random()
|
addr: Address = transport_cls.get_random()
|
||||||
accept_addrs.append(addr.unwrap())
|
accept_addrs.append(addr.unwrap())
|
||||||
|
|
||||||
assert accept_addrs
|
# XXX, either passed in by caller or delivered
|
||||||
|
# in post spawn-spec handshake for subs.
|
||||||
|
if not accept_addrs:
|
||||||
|
raise RuntimeError(
|
||||||
|
f'No tpt bind addresses provided to actor!?\n'
|
||||||
|
f'parent_addr={parent_addr!r}\n'
|
||||||
|
f'accept_addrs={accept_addrs!r}\n'
|
||||||
|
f'enable_transports={enable_transports!r}\n'
|
||||||
|
)
|
||||||
|
|
||||||
ya_root_tn: bool = bool(actor._root_tn)
|
ya_root_tn: bool = bool(actor._root_tn)
|
||||||
ya_service_tn: bool = bool(actor._service_tn)
|
ya_service_tn: bool = bool(actor._service_tn)
|
||||||
|
|
@ -1837,7 +1845,41 @@ async def async_main(
|
||||||
and
|
and
|
||||||
not actor.is_registrar
|
not actor.is_registrar
|
||||||
):
|
):
|
||||||
failed: bool = False
|
failed_unreg: bool = False
|
||||||
|
rent_chan: Channel|None = actor._parent_chan
|
||||||
|
|
||||||
|
# XXX, detect whether the parent IS the registrar
|
||||||
|
# so we can FALL BACK to `_parent_chan` when a new
|
||||||
|
# connection attempt fails (e.g. UDS transport
|
||||||
|
# `os.unlink()`s the socket file during teardown).
|
||||||
|
#
|
||||||
|
# IMPORTANT: we do NOT eagerly reuse `_parent_chan`
|
||||||
|
# because it may still be carrying context/stream
|
||||||
|
# teardown protocol traffic — sending an
|
||||||
|
# `unregister_actor` RPC over it concurrently
|
||||||
|
# causes protocol-level conflicts. Instead we try
|
||||||
|
# a fresh `get_registry()` connection first and
|
||||||
|
# only fall back to the parent channel on failure.
|
||||||
|
#
|
||||||
|
# See `ipc._uds.close_listener()` for details on
|
||||||
|
# the UDS socket-file lifecycle.
|
||||||
|
parent_is_reg: bool = False
|
||||||
|
if (
|
||||||
|
rent_chan is not None
|
||||||
|
and
|
||||||
|
rent_chan.connected()
|
||||||
|
):
|
||||||
|
pchan_raddr: Address|None = rent_chan.raddr
|
||||||
|
if pchan_raddr is not None:
|
||||||
|
for reg_addr in actor.reg_addrs:
|
||||||
|
if (
|
||||||
|
pchan_raddr.unwrap()
|
||||||
|
==
|
||||||
|
tuple(reg_addr)
|
||||||
|
):
|
||||||
|
parent_is_reg = True
|
||||||
|
break
|
||||||
|
|
||||||
for addr in actor.reg_addrs:
|
for addr in actor.reg_addrs:
|
||||||
waddr = wrap_address(addr)
|
waddr = wrap_address(addr)
|
||||||
assert waddr.is_valid
|
assert waddr.is_valid
|
||||||
|
|
@ -1853,14 +1895,38 @@ async def async_main(
|
||||||
uid=actor.aid.uid,
|
uid=actor.aid.uid,
|
||||||
)
|
)
|
||||||
except OSError:
|
except OSError:
|
||||||
failed = True
|
# Connection to registrar failed
|
||||||
if cs.cancelled_caught:
|
# (listener socket likely already
|
||||||
failed = True
|
# closed/unlinked). Fall back to
|
||||||
|
# parent channel if parent IS the
|
||||||
|
# registrar.
|
||||||
|
if (
|
||||||
|
parent_is_reg
|
||||||
|
and
|
||||||
|
rent_chan.connected()
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
reg_portal = Portal(rent_chan)
|
||||||
|
await reg_portal.run_from_ns(
|
||||||
|
'self',
|
||||||
|
'unregister_actor',
|
||||||
|
uid=actor.aid.uid,
|
||||||
|
)
|
||||||
|
except (
|
||||||
|
OSError,
|
||||||
|
trio.ClosedResourceError,
|
||||||
|
):
|
||||||
|
failed_unreg = True
|
||||||
|
else:
|
||||||
|
failed_unreg = True
|
||||||
|
|
||||||
if failed:
|
if cs.cancelled_caught:
|
||||||
|
failed_unreg = True
|
||||||
|
|
||||||
|
if failed_unreg:
|
||||||
teardown_report += (
|
teardown_report += (
|
||||||
f'-> Failed to unregister {actor.name} from '
|
f'-> Failed to unregister {actor.name} from '
|
||||||
f'registar @ {addr}\n'
|
f'registrar @ {addr}\n'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Ensure all peers (actors connected to us as clients) are finished
|
# Ensure all peers (actors connected to us as clients) are finished
|
||||||
|
|
|
||||||
284
uv.lock
284
uv.lock
|
|
@ -2,6 +2,15 @@ version = 1
|
||||||
revision = 3
|
revision = 3
|
||||||
requires-python = ">=3.12, <3.14"
|
requires-python = ">=3.12, <3.14"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-generator"
|
||||||
|
version = "1.10"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ce/b6/6fa6b3b598a03cba5e80f829e0dadbb49d7645f523d209b2fb7ea0bbb02a/async_generator-1.10.tar.gz", hash = "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144", size = 29870, upload-time = "2018-08-01T03:36:21.69Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/52/39d20e03abd0ac9159c162ec24b93fbcaa111e8400308f2465432495ca2b/async_generator-1.10-py3-none-any.whl", hash = "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b", size = 18857, upload-time = "2018-08-01T03:36:20.029Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "attrs"
|
name = "attrs"
|
||||||
version = "24.3.0"
|
version = "24.3.0"
|
||||||
|
|
@ -11,6 +20,15 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308", size = 63397, upload-time = "2024-12-16T06:59:26.977Z" },
|
{ url = "https://files.pythonhosted.org/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308", size = 63397, upload-time = "2024-12-16T06:59:26.977Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[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]]
|
[[package]]
|
||||||
name = "bidict"
|
name = "bidict"
|
||||||
version = "0.23.1"
|
version = "0.23.1"
|
||||||
|
|
@ -20,6 +38,50 @@ 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" },
|
{ 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.8"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/75/aa/abcd75e9600987a0bc6cfe9b6b2ff3f0e2cb08c170addc6e76035b5c4cb3/blake3-1.0.8.tar.gz", hash = "sha256:513cc7f0f5a7c035812604c2c852a0c1468311345573de647e310aca4ab165ba", size = 117308, upload-time = "2025-10-14T06:47:48.83Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/a0/b7b6dff04012cfd6e665c09ee446f749bd8ea161b00f730fe1bdecd0f033/blake3-1.0.8-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d8da4233984d51471bd4e4366feda1d90d781e712e0a504ea54b1f2b3577557b", size = 347983, upload-time = "2025-10-14T06:45:47.214Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5b/a2/264091cac31d7ae913f1f296abc20b8da578b958ffb86100a7ce80e8bf5c/blake3-1.0.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1257be19f2d381c868a34cc822fc7f12f817ddc49681b6d1a2790bfbda1a9865", size = 325415, upload-time = "2025-10-14T06:45:48.482Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ee/7d/85a4c0782f613de23d114a7a78fcce270f75b193b3ff3493a0de24ba104a/blake3-1.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:269f255b110840e52b6ce9db02217e39660ebad3e34ddd5bca8b8d378a77e4e1", size = 371296, upload-time = "2025-10-14T06:45:49.674Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/20/488475254976ed93fab57c67aa80d3b40df77f7d9db6528c9274bff53e08/blake3-1.0.8-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:66ca28a673025c40db3eba21a9cac52f559f83637efa675b3f6bd8683f0415f3", size = 374516, upload-time = "2025-10-14T06:45:51.23Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/21/2a1c47fedb77fb396512677ec6d46caf42ac6e9a897db77edd0a2a46f7bb/blake3-1.0.8-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcb04966537777af56c1f399b35525aa70a1225816e121ff95071c33c0f7abca", size = 447911, upload-time = "2025-10-14T06:45:52.637Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/7d/db0626df16029713e7e61b67314c4835e85c296d82bd907c21c6ea271da2/blake3-1.0.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5b5da177d62cc4b7edf0cea08fe4dec960c9ac27f916131efa890a01f747b93", size = 505420, upload-time = "2025-10-14T06:45:54.445Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5b/55/6e737850c2d58a6d9de8a76dad2ae0f75b852a23eb4ecb07a0b165e6e436/blake3-1.0.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:38209b10482c97e151681ea3e91cc7141f56adbbf4820a7d701a923124b41e6a", size = 394189, upload-time = "2025-10-14T06:45:55.719Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5b/94/eafaa5cdddadc0c9c603a6a6d8339433475e1a9f60c8bb9c2eed2d8736b6/blake3-1.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:504d1399b7fb91dfe5c25722d2807990493185faa1917456455480c36867adb5", size = 388001, upload-time = "2025-10-14T06:45:57.067Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/81/735fa00d13de7f68b25e1b9cb36ff08c6f165e688d85d8ec2cbfcdedccc5/blake3-1.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c84af132aa09abeadf9a0118c8fb26f4528f3f42c10ef8be0fcf31c478774ec4", size = 550302, upload-time = "2025-10-14T06:45:58.657Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/c6/d1fe8bdea4a6088bd54b5a58bc40aed89a4e784cd796af7722a06f74bae7/blake3-1.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a25db3d36b55f5ed6a86470155cc749fc9c5b91c949b8d14f48658f9d960d9ec", size = 554211, upload-time = "2025-10-14T06:46:00.269Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/55/d1/ca74aa450cbe10e396e061f26f7a043891ffa1485537d6b30d3757e20995/blake3-1.0.8-cp312-cp312-win32.whl", hash = "sha256:e0fee93d5adcd44378b008c147e84f181f23715307a64f7b3db432394bbfce8b", size = 228343, upload-time = "2025-10-14T06:46:01.533Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4d/42/bbd02647169e3fbed27558555653ac2578c6f17ccacf7d1956c58ef1d214/blake3-1.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:6a6eafc29e4f478d365a87d2f25782a521870c8514bb43734ac85ae9be71caf7", size = 215704, upload-time = "2025-10-14T06:46:02.79Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/55/b8/11de9528c257f7f1633f957ccaff253b706838d22c5d2908e4735798ec01/blake3-1.0.8-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:46dc20976bd6c235959ef0246ec73420d1063c3da2839a9c87ca395cf1fd7943", size = 347771, upload-time = "2025-10-14T06:46:04.248Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/26/f7668be55c909678b001ecacff11ad7016cd9b4e9c7cc87b5971d638c5a9/blake3-1.0.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d17eb6382634b3a5bc0c0e0454d5265b0becaeeadb6801ed25150b39a999d0cc", size = 325431, upload-time = "2025-10-14T06:46:06.136Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/57/e8a85fa261894bf7ce7af928ff3408aab60287ab8d58b55d13a3f700b619/blake3-1.0.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19fc6f2b7edab8acff6895fc6e38c19bd79f4c089e21153020c75dfc7397d52d", size = 370994, upload-time = "2025-10-14T06:46:07.398Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/cd/765b76bb48b8b294fea94c9008b0d82b4cfa0fa2f3c6008d840d01a597e4/blake3-1.0.8-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f54cff7f15d91dc78a63a2dd02a3dccdc932946f271e2adb4130e0b4cf608ba", size = 374372, upload-time = "2025-10-14T06:46:08.698Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/7a/32084eadbb28592bb07298f0de316d2da586c62f31500a6b1339a7e7b29b/blake3-1.0.8-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7e12a777f6b798eb8d06f875d6e108e3008bd658d274d8c676dcf98e0f10537", size = 447627, upload-time = "2025-10-14T06:46:10.002Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/f4/3788a1d86e17425eea147e28d7195d7053565fc279236a9fd278c2ec495e/blake3-1.0.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddfc59b0176fb31168f08d5dd536e69b1f4f13b5a0f4b0c3be1003efd47f9308", size = 507536, upload-time = "2025-10-14T06:46:11.614Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/01/4639cba48513b94192681b4da472cdec843d3001c5344d7051ee5eaef606/blake3-1.0.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2336d5b2a801a7256da21150348f41610a6c21dae885a3acb1ebbd7333d88d8", size = 394105, upload-time = "2025-10-14T06:46:12.808Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/ae/6e55c19c8460fada86cd1306a390a09b0c5a2e2e424f9317d2edacea439f/blake3-1.0.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4072196547484c95a5a09adbb952e9bb501949f03f9e2a85e7249ef85faaba8", size = 386928, upload-time = "2025-10-14T06:46:16.284Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ee/6c/05b7a5a907df1be53a8f19e7828986fc6b608a44119641ef9c0804fbef15/blake3-1.0.8-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0eab3318ec02f8e16fe549244791ace2ada2c259332f0c77ab22cf94dfff7130", size = 550003, upload-time = "2025-10-14T06:46:17.791Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/03/f0ea4adfedc1717623be6460b3710fcb725ca38082c14274369803f727e1/blake3-1.0.8-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a33b9a1fb6d1d559a8e0d04b041e99419a6bb771311c774f6ff57ed7119c70ed", size = 553857, upload-time = "2025-10-14T06:46:19.088Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/6f/e5410d2e2a30c8aba8389ffc1c0061356916bf5ecd0a210344e7b69b62ab/blake3-1.0.8-cp313-cp313-win32.whl", hash = "sha256:e171b169cb7ea618e362a4dddb7a4d4c173bbc08b9ba41ea3086dd1265530d4f", size = 228315, upload-time = "2025-10-14T06:46:20.391Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/ef/d9c297956dfecd893f29f59e7b22445aba5b47b7f6815d9ba5dcd73fcae6/blake3-1.0.8-cp313-cp313-win_amd64.whl", hash = "sha256:3168c457255b5d2a2fc356ba696996fcaff5d38284f968210d54376312107662", size = 215477, upload-time = "2025-10-14T06:46:21.542Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/ba/eaa7723d66dd8ab762a3e85e139bb9c46167b751df6e950ad287adb8fb61/blake3-1.0.8-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:b4d672c24dc15ec617d212a338a4ca14b449829b6072d09c96c63b6e6b621aed", size = 347289, upload-time = "2025-10-14T06:46:22.772Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/b3/6957f6ee27f0d5b8c4efdfda68a1298926a88c099f4dd89c711049d16526/blake3-1.0.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:1af0e5a29aa56d4fba904452ae784740997440afd477a15e583c38338e641f41", size = 324444, upload-time = "2025-10-14T06:46:24.729Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/da/722cebca11238f3b24d3cefd2361c9c9ea47cfa0ad9288eeb4d1e0b7cf93/blake3-1.0.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef153c5860d5bf1cc71aece69b28097d2a392913eb323d6b52555c875d0439fc", size = 370441, upload-time = "2025-10-14T06:46:26.29Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/d5/2f7440c8e41c0af995bad3a159e042af0f4ed1994710af5b4766ca918f65/blake3-1.0.8-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e8ae3689f0c7bfa6ce6ae45cab110e4c3442125c4c23b28f1f097856de26e4d1", size = 374312, upload-time = "2025-10-14T06:46:27.451Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/6c/fb6a7812e60ce3e110bcbbb11f167caf3e975c589572c41e1271f35f2c41/blake3-1.0.8-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fb83532f7456ddeb68dae1b36e1f7c52f9cb72852ac01159bbcb1a12b0f8be0", size = 447007, upload-time = "2025-10-14T06:46:29.056Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/3b/c99b43fae5047276ea9d944077c190fc1e5f22f57528b9794e21f7adedc6/blake3-1.0.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ae7754c7d96e92a70a52e07c732d594cf9924d780f49fffd3a1e9235e0f5ba7", size = 507323, upload-time = "2025-10-14T06:46:30.661Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/bb/ba90eddd592f8c074a0694cb0a744b6bd76bfe67a14c2b490c8bdfca3119/blake3-1.0.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4bacaae75e98dee3b7da6c5ee3b81ee21a3352dd2477d6f1d1dbfd38cdbf158a", size = 393449, upload-time = "2025-10-14T06:46:31.805Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/ed/58a2acd0b9e14459cdaef4344db414d4a36e329b9720921b442a454dd443/blake3-1.0.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9456c829601d72852d8ba0af8dae0610f7def1d59f5942efde1e2ef93e8a8b57", size = 386844, upload-time = "2025-10-14T06:46:33.195Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/04/fed09845b18d90862100c8e48308261e2f663aab25d3c71a6a0bdda6618b/blake3-1.0.8-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:497ef8096ec4ac1ffba9a66152cee3992337cebf8ea434331d8fd9ce5423d227", size = 549550, upload-time = "2025-10-14T06:46:35.23Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/65/1859fddfabc1cc72548c2269d988819aad96d854e25eae00531517925901/blake3-1.0.8-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:511133bab85ff60ed143424ce484d08c60894ff7323f685d7a6095f43f0c85c3", size = 553805, upload-time = "2025-10-14T06:46:36.532Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/c7/2969352017f62378e388bb07bb2191bc9a953f818dc1cd6b9dd5c24916e1/blake3-1.0.8-cp313-cp313t-win32.whl", hash = "sha256:9c9fbdacfdeb68f7ca53bb5a7a5a593ec996eaf21155ad5b08d35e6f97e60877", size = 228068, upload-time = "2025-10-14T06:46:37.826Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d8/fc/923e25ac9cadfff1cd20038bcc0854d0f98061eb6bc78e42c43615f5982d/blake3-1.0.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3cec94ed5676821cf371e9c9d25a41b4f3ebdb5724719b31b2749653b7cc1dfa", size = 215369, upload-time = "2025-10-14T06:46:39.054Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cffi"
|
name = "cffi"
|
||||||
version = "1.17.1"
|
version = "1.17.1"
|
||||||
|
|
@ -74,6 +136,15 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff", size = 11424, upload-time = "2024-10-29T18:34:49.815Z" },
|
{ url = "https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff", size = 11424, upload-time = "2024-10-29T18:34:49.815Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[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]]
|
[[package]]
|
||||||
name = "greenback"
|
name = "greenback"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
|
|
@ -130,6 +201,18 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "importlib-metadata"
|
||||||
|
version = "9.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "zipp" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", size = 27789, upload-time = "2026-03-20T06:42:55.665Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iniconfig"
|
name = "iniconfig"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
|
|
@ -139,6 +222,59 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" },
|
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mmh3"
|
||||||
|
version = "5.2.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/91/1a/edb23803a168f070ded7a3014c6d706f63b90c84ccc024f89d794a3b7a6d/mmh3-5.2.1.tar.gz", hash = "sha256:bbea5b775f0ac84945191fb83f845a6fd9a21a03ea7f2e187defac7e401616ad", size = 33775, upload-time = "2026-03-05T15:55:57.716Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/94/bc5c3b573b40a328c4d141c20e399039ada95e5e2a661df3425c5165fd84/mmh3-5.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0cc21533878e5586b80d74c281d7f8da7932bc8ace50b8d5f6dbf7e3935f63f1", size = 56087, upload-time = "2026-03-05T15:54:21.92Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/80/64a02cc3e95c3af0aaa2590849d9ed24a9f14bb93537addde688e039b7c3/mmh3-5.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4eda76074cfca2787c8cf1bec603eaebdddd8b061ad5502f85cddae998d54f00", size = 40500, upload-time = "2026-03-05T15:54:22.953Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/72/e6d6602ce18adf4ddcd0e48f2e13590cc92a536199e52109f46f259d3c46/mmh3-5.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:eee884572b06bbe8a2b54f424dbd996139442cf83c76478e1ec162512e0dd2c7", size = 40034, upload-time = "2026-03-05T15:54:23.943Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/c2/bf4537a8e58e21886ef16477041238cab5095c836496e19fafc34b7445d2/mmh3-5.2.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d0b7e803191db5f714d264044e06189c8ccd3219e936cc184f07106bd17fd7b", size = 97292, upload-time = "2026-03-05T15:54:25.335Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/e2/51ed62063b44d10b06d975ac87af287729eeb5e3ed9772f7584a17983e90/mmh3-5.2.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e6c219e375f6341d0959af814296372d265a8ca1af63825f65e2e87c618f006", size = 103274, upload-time = "2026-03-05T15:54:26.44Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/ce/12a7524dca59eec92e5b31fdb13ede1e98eda277cf2b786cf73bfbc24e81/mmh3-5.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26fb5b9c3946bf7f1daed7b37e0c03898a6f062149127570f8ede346390a0825", size = 106158, upload-time = "2026-03-05T15:54:28.578Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/1f/d3ba6dd322d01ab5d44c46c8f0c38ab6bbbf9b5e20e666dfc05bf4a23604/mmh3-5.2.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3c38d142c706201db5b2345166eeef1e7740e3e2422b470b8ba5c8727a9b4c7a", size = 113005, upload-time = "2026-03-05T15:54:29.767Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/a9/15d6b6f913294ea41b44d901741298e3718e1cb89ee626b3694625826a43/mmh3-5.2.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50885073e2909251d4718634a191c49ae5f527e5e1736d738e365c3e8be8f22b", size = 120744, upload-time = "2026-03-05T15:54:30.931Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/b3/70b73923fd0284c439860ff5c871b20210dfdbe9a6b9dd0ee6496d77f174/mmh3-5.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3f99e1756fc48ad507b95e5d86f2fb21b3d495012ff13e6592ebac14033f166", size = 99111, upload-time = "2026-03-05T15:54:32.353Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/38/99f7f75cd27d10d8b899a1caafb9d531f3903e4d54d572220e3d8ac35e89/mmh3-5.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62815d2c67f2dd1be76a253d88af4e1da19aeaa1820146dec52cf8bee2958b16", size = 98623, upload-time = "2026-03-05T15:54:33.801Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/68/6e292c0853e204c44d2f03ea5f090be3317a0e2d9417ecb62c9eb27687df/mmh3-5.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8f767ba0911602ddef289404e33835a61168314ebd3c729833db2ed685824211", size = 106437, upload-time = "2026-03-05T15:54:35.177Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/c6/fedd7284c459cfb58721d461fcf5607a4c1f5d9ab195d113d51d10164d16/mmh3-5.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:67e41a497bac88cc1de96eeba56eeb933c39d54bc227352f8455aa87c4ca4000", size = 110002, upload-time = "2026-03-05T15:54:36.673Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/ac/ca8e0c19a34f5b71390171d2ff0b9f7f187550d66801a731bb68925126a4/mmh3-5.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d74a03fb57757ece25aa4b3c1c60157a1cece37a020542785f942e2f827eed5", size = 97507, upload-time = "2026-03-05T15:54:37.804Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/94/6ebb9094cfc7ac5e7950776b9d13a66bb4a34f83814f32ba2abc9494fc68/mmh3-5.2.1-cp312-cp312-win32.whl", hash = "sha256:7374d6e3ef72afe49697ecd683f3da12f4fc06af2d75433d0580c6746d2fa025", size = 40773, upload-time = "2026-03-05T15:54:40.077Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5b/3c/cd3527198cf159495966551c84a5f36805a10ac17b294f41f67b83f6a4d6/mmh3-5.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:3a9fed49c6ce4ed7e73f13182760c65c816da006debe67f37635580dfb0fae00", size = 41560, upload-time = "2026-03-05T15:54:41.148Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/96/6fe5ebd0f970a076e3ed5512871ce7569447b962e96c125528a2f9724470/mmh3-5.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:bbfcb95d9a744e6e2827dfc66ad10e1020e0cac255eb7f85652832d5a264c2fc", size = 39313, upload-time = "2026-03-05T15:54:42.171Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/a5/9daa0508a1569a54130f6198d5462a92deda870043624aa3ea72721aa765/mmh3-5.2.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:723b2681ed4cc07d3401bbea9c201ad4f2a4ca6ba8cddaff6789f715dd2b391e", size = 40832, upload-time = "2026-03-05T15:54:43.212Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/6b/3230c6d80c1f4b766dedf280a92c2241e99f87c1504ff74205ec8cebe451/mmh3-5.2.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:3619473a0e0d329fd4aec8075628f8f616be2da41605300696206d6f36920c3d", size = 41964, upload-time = "2026-03-05T15:54:44.204Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/fb/648bfddb74a872004b6ee751551bfdda783fe6d70d2e9723bad84dbe5311/mmh3-5.2.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:e48d4dbe0f88e53081da605ae68644e5182752803bbc2beb228cca7f1c4454d6", size = 39114, upload-time = "2026-03-05T15:54:45.205Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/c2/ab7901f87af438468b496728d11264cb397b3574d41506e71b92128e0373/mmh3-5.2.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a482ac121de6973897c92c2f31defc6bafb11c83825109275cffce54bb64933f", size = 39819, upload-time = "2026-03-05T15:54:46.509Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/ed/6f88dda0df67de1612f2e130ffea34cf84aaee5bff5b0aff4dbff2babe34/mmh3-5.2.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:17fbb47f0885ace8327ce1235d0416dc86a211dcd8cc1e703f41523be32cfec8", size = 40330, upload-time = "2026-03-05T15:54:47.864Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/66/7516d23f53cdf90f43fce24ab80c28f45e6851d78b46bef8c02084edf583/mmh3-5.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d51fde50a77f81330523562e3c2734ffdca9c4c9e9d355478117905e1cfe16c6", size = 56078, upload-time = "2026-03-05T15:54:48.9Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/34/4d152fdf4a91a132cb226b671f11c6b796eada9ab78080fb5ce1e95adaab/mmh3-5.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:19bbd3b841174ae6ed588536ab5e1b1fe83d046e668602c20266547298d939a9", size = 40498, upload-time = "2026-03-05T15:54:49.942Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d4/4c/8e3af1b6d85a299767ec97bd923f12b06267089c1472c27c1696870d1175/mmh3-5.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be77c402d5e882b6fbacfd90823f13da8e0a69658405a39a569c6b58fdb17b03", size = 40033, upload-time = "2026-03-05T15:54:50.994Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/f2/966ea560e32578d453c9e9db53d602cbb1d0da27317e232afa7c38ceba11/mmh3-5.2.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fd96476f04db5ceba1cfa0f21228f67c1f7402296f0e73fee3513aa680ad237b", size = 97320, upload-time = "2026-03-05T15:54:52.072Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/0d/2c5f9893b38aeb6b034d1a44ecd55a010148054f6a516abe53b5e4057297/mmh3-5.2.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:707151644085dd0f20fe4f4b573d28e5130c4aaa5f587e95b60989c5926653b5", size = 103299, upload-time = "2026-03-05T15:54:53.569Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1c/fc/2ebaef4a4d4376f89761274dc274035ffd96006ab496b4ee5af9b08f21a9/mmh3-5.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3737303ca9ea0f7cb83028781148fcda4f1dac7821db0c47672971dabcf63593", size = 106222, upload-time = "2026-03-05T15:54:55.092Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/09/ea7ffe126d0ba0406622602a2d05e1e1a6841cc92fc322eb576c95b27fad/mmh3-5.2.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2778fed822d7db23ac5008b181441af0c869455b2e7d001f4019636ac31b6fe4", size = 113048, upload-time = "2026-03-05T15:54:56.305Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/85/57/9447032edf93a64aa9bef4d9aa596400b1756f40411890f77a284f6293ca/mmh3-5.2.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d57dea657357230cc780e13920d7fa7db059d58fe721c80020f94476da4ca0a1", size = 120742, upload-time = "2026-03-05T15:54:57.453Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/53/82/a86cc87cc88c92e9e1a598fee509f0409435b57879a6129bf3b3e40513c7/mmh3-5.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:169e0d178cb59314456ab30772429a802b25d13227088085b0d49b9fe1533104", size = 99132, upload-time = "2026-03-05T15:54:58.583Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/f7/6b16eb1b40ee89bb740698735574536bc20d6cdafc65ae702ea235578e05/mmh3-5.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7e4e1f580033335c6f76d1e0d6b56baf009d1a64d6a4816347e4271ba951f46d", size = 98686, upload-time = "2026-03-05T15:55:00.078Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/88/a601e9f32ad1410f438a6d0544298ea621f989bd34a0731a7190f7dec799/mmh3-5.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2bd9f19f7f1fcebd74e830f4af0f28adad4975d40d80620be19ffb2b2af56c9f", size = 106479, upload-time = "2026-03-05T15:55:01.532Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/5c/ce29ae3dfc4feec4007a437a1b7435fb9507532a25147602cd5b52be86db/mmh3-5.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c88653877aeb514c089d1b3d473451677b8b9a6d1497dbddf1ae7934518b06d2", size = 110030, upload-time = "2026-03-05T15:55:02.934Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/30/ae444ef2ff87c805d525da4fa63d27cda4fe8a48e77003a036b8461cfd5c/mmh3-5.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fceef7fe67c81e1585198215e42ad3fdba3a25644beda8fbdaf85f4d7b93175a", size = 97536, upload-time = "2026-03-05T15:55:04.135Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/f9/dc3787ee5c813cc27fe79f45ad4500d9b5437f23a7402435cc34e07c7718/mmh3-5.2.1-cp313-cp313-win32.whl", hash = "sha256:54b64fb2433bc71488e7a449603bf8bd31fbcf9cb56fbe1eb6d459e90b86c37b", size = 40769, upload-time = "2026-03-05T15:55:05.277Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/67/850e0b5a1e97799822ebfc4ca0e8c6ece3ed8baf7dcdf64de817dfdda2ca/mmh3-5.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:cae6383181f1e345317742d2ddd88f9e7d2682fa4c9432e3a74e47d92dce0229", size = 41563, upload-time = "2026-03-05T15:55:06.283Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/cc/98c90b28e1da5458e19fbfaf4adb5289208d3bfccd45dd14eab216a2f0bb/mmh3-5.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:022aa1a528604e6c83d0a7705fdef0b5355d897a9e0fa3a8d26709ceaa06965d", size = 39310, upload-time = "2026-03-05T15:55:07.323Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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]]
|
[[package]]
|
||||||
name = "msgspec"
|
name = "msgspec"
|
||||||
version = "0.19.0"
|
version = "0.19.0"
|
||||||
|
|
@ -161,6 +297,47 @@ wheels = [
|
||||||
{ 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" },
|
{ 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]]
|
||||||
|
name = "multiaddr"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
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" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c7/10/4e26a8577cfce1c0febc8d83087e1373e93c695c6e73ad010546fb67e229/multiaddr-0.2.0.tar.gz", hash = "sha256:acb6b25c332ec1b2f1f8fef8d03a8c63385d34a87d690df0f4bba43cdf6efe8d", size = 58356, upload-time = "2026-03-17T21:51:00.274Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/13/56e503d01218d1ca27ea9fda862045a4b400cae5e756f47315f5aaba0eee/multiaddr-0.2.0-py3-none-any.whl", hash = "sha256:bcff7bf3d7de3d6da0b865b25423bcb411de1d20d70cc6abfacf75170d17866c", size = 40424, upload-time = "2026-03-17T21:50:58.833Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy-extensions"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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]]
|
[[package]]
|
||||||
name = "outcome"
|
name = "outcome"
|
||||||
version = "1.3.0.post0"
|
version = "1.3.0.post0"
|
||||||
|
|
@ -262,6 +439,64 @@ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "pycparser"
|
name = "pycparser"
|
||||||
version = "2.22"
|
version = "2.22"
|
||||||
|
|
@ -310,6 +545,12 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" },
|
{ url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[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]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.14.14"
|
version = "0.14.14"
|
||||||
|
|
@ -336,6 +577,15 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" },
|
{ url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "six"
|
||||||
|
version = "1.17.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sniffio"
|
name = "sniffio"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
|
|
@ -384,6 +634,7 @@ dependencies = [
|
||||||
{ name = "cffi" },
|
{ name = "cffi" },
|
||||||
{ name = "colorlog" },
|
{ name = "colorlog" },
|
||||||
{ name = "msgspec" },
|
{ name = "msgspec" },
|
||||||
|
{ name = "multiaddr" },
|
||||||
{ name = "pdbp" },
|
{ name = "pdbp" },
|
||||||
{ name = "platformdirs" },
|
{ name = "platformdirs" },
|
||||||
{ name = "tricycle" },
|
{ name = "tricycle" },
|
||||||
|
|
@ -428,6 +679,7 @@ requires-dist = [
|
||||||
{ name = "cffi", specifier = ">=1.17.1" },
|
{ name = "cffi", specifier = ">=1.17.1" },
|
||||||
{ name = "colorlog", specifier = ">=6.8.2,<7" },
|
{ name = "colorlog", specifier = ">=6.8.2,<7" },
|
||||||
{ name = "msgspec", specifier = ">=0.19.0" },
|
{ name = "msgspec", specifier = ">=0.19.0" },
|
||||||
|
{ name = "multiaddr", specifier = ">=0.2.0" },
|
||||||
{ name = "pdbp", specifier = ">=1.8.2,<2" },
|
{ name = "pdbp", specifier = ">=1.8.2,<2" },
|
||||||
{ name = "platformdirs", specifier = ">=4.4.0" },
|
{ name = "platformdirs", specifier = ">=4.4.0" },
|
||||||
{ name = "tricycle", specifier = ">=0.4.1,<0.5" },
|
{ name = "tricycle", specifier = ">=0.4.1,<0.5" },
|
||||||
|
|
@ -493,6 +745,23 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/c9/55/c4d9bea8b3d7937901958f65124123512419ab0eb73695e5f382521abbfb/trio-0.29.0-py3-none-any.whl", hash = "sha256:d8c463f1a9cc776ff63e331aba44c125f423a5a13c684307e828d930e625ba66", size = 492920, upload-time = "2025-02-14T07:13:48.696Z" },
|
{ url = "https://files.pythonhosted.org/packages/c9/55/c4d9bea8b3d7937901958f65124123512419ab0eb73695e5f382521abbfb/trio-0.29.0-py3-none-any.whl", hash = "sha256:d8c463f1a9cc776ff63e331aba44c125f423a5a13c684307e828d930e625ba66", size = 492920, upload-time = "2025-02-14T07:13:48.696Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "trio-typing"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "async-generator" },
|
||||||
|
{ name = "importlib-metadata" },
|
||||||
|
{ name = "mypy-extensions" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "trio" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b5/74/a87aafa40ec3a37089148b859892cbe2eef08d132c816d58a60459be5337/trio-typing-0.10.0.tar.gz", hash = "sha256:065ee684296d52a8ab0e2374666301aec36ee5747ac0e7a61f230250f8907ac3", size = 38747, upload-time = "2023-12-01T02:54:55.508Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/ff/9bd795273eb14fac7f6a59d16cc8c4d0948a619a1193d375437c7f50f3eb/trio_typing-0.10.0-py3-none-any.whl", hash = "sha256:6d0e7ec9d837a2fe03591031a172533fbf4a1a95baf369edebfc51d5a49f0264", size = 42224, upload-time = "2023-12-01T02:54:54.1Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.14.1"
|
version = "4.14.1"
|
||||||
|
|
@ -502,6 +771,12 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
|
{ url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[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]]
|
[[package]]
|
||||||
name = "wcwidth"
|
name = "wcwidth"
|
||||||
version = "0.2.13"
|
version = "0.2.13"
|
||||||
|
|
@ -563,3 +838,12 @@ wheels = [
|
||||||
{ 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/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" },
|
{ 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]]
|
||||||
|
name = "zipp"
|
||||||
|
version = "3.23.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
|
||||||
|
]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue