From e90241baaa637c4799d62a183519ed8ec0260f6b Mon Sep 17 00:00:00 2001 From: goodboy Date: Mon, 13 Apr 2026 17:36:21 -0400 Subject: [PATCH] Add `parse_endpoints()` to `_multiaddr` Provide a service-table parsing API for downstream projects (like `piker`) to declare per-actor transport bind addresses as a config map of actor-name -> multiaddr strings (e.g. from a TOML `[network]` section). Deats, - `EndpointsTable` type alias: input `dict[str, list[str|tuple]]`. - `ParsedEndpoints` type alias: output `dict[str, list[Address]]`. - `parse_endpoints()` iterates the table and delegates each entry to the existing `tractor.discovery._discovery.wrap_address()` helper, which handles maddr strings, raw `(host, port)` tuples, and pre-wrapped `Address` objs. - UDS maddrs use the multiaddr spec name `/unix/...` (not tractor's internal `/uds/` proto_key) Also add new tests, - 7 new pure unit tests (no trio runtime): TCP-only, mixed tpts, unwrapped tuples, mixed str+tuple, unsupported proto (`/udp/`), empty table, empty actor list - all 22 multiaddr tests pass rn. Prompt-IO: ai/prompt-io/claude/20260413T205048Z_269d939c_prompt_io.md (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- .../20260413T205048Z_269d939c_prompt_io.md | 50 +++++ ...20260413T205048Z_269d939c_prompt_io.raw.md | 207 ++++++++++++++++++ tests/discovery/test_multiaddr.py | 125 +++++++++++ tractor/discovery/_multiaddr.py | 58 +++++ 4 files changed, 440 insertions(+) create mode 100644 ai/prompt-io/claude/20260413T205048Z_269d939c_prompt_io.md create mode 100644 ai/prompt-io/claude/20260413T205048Z_269d939c_prompt_io.raw.md diff --git a/ai/prompt-io/claude/20260413T205048Z_269d939c_prompt_io.md b/ai/prompt-io/claude/20260413T205048Z_269d939c_prompt_io.md new file mode 100644 index 00000000..f6ec66d0 --- /dev/null +++ b/ai/prompt-io/claude/20260413T205048Z_269d939c_prompt_io.md @@ -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. diff --git a/ai/prompt-io/claude/20260413T205048Z_269d939c_prompt_io.raw.md b/ai/prompt-io/claude/20260413T205048Z_269d939c_prompt_io.raw.md new file mode 100644 index 00000000..71aff348 --- /dev/null +++ b/ai/prompt-io/claude/20260413T205048Z_269d939c_prompt_io.raw.md @@ -0,0 +1,207 @@ +--- +model: claude-opus-4-6 +service: claude +timestamp: 2026-04-13T20:50:48Z +git_ref: 269d939c +--- + +## Implementation output + +### `tractor/discovery/_multiaddr.py` — appended after line 133 + +```python +# 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 ( + actor_name, + addr_entries, + ) in service_table.items(): + parsed[actor_name] = [ + wrap_address(entry) + for entry in addr_entries + ] + return parsed +``` + +### `tests/discovery/test_multiaddr.py` — appended after line 252 + +Added import of `parse_endpoints` to existing import block, then +7 new test functions: + +```python +# ------ 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': []} +``` + +### Test run output + +``` +22 passed, 1 warning in 0.05s +``` + +All 22 tests pass (15 existing + 7 new). diff --git a/tests/discovery/test_multiaddr.py b/tests/discovery/test_multiaddr.py index 83da84ea..9b1520d6 100644 --- a/tests/discovery/test_multiaddr.py +++ b/tests/discovery/test_multiaddr.py @@ -15,6 +15,7 @@ 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, ) @@ -249,3 +250,127 @@ def test_wrap_address_maddr_str(): 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': []} diff --git a/tractor/discovery/_multiaddr.py b/tractor/discovery/_multiaddr.py index e310cf1c..da6c17d1 100644 --- a/tractor/discovery/_multiaddr.py +++ b/tractor/discovery/_multiaddr.py @@ -130,3 +130,61 @@ def parse_maddr( 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 ( + actor_name, + addr_entries, + ) in service_table.items(): + parsed[actor_name] = [ + wrap_address(entry) + for entry in addr_entries + ] + return parsed