2023-09-29 18:11:31 +00:00
|
|
|
# tractor: structured concurrent "actors".
|
|
|
|
|
# Copyright 2018-eternity Tyler Goodlet.
|
|
|
|
|
|
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
|
# (at your option) any later version.
|
|
|
|
|
|
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
# GNU Affero General Public License for more details.
|
|
|
|
|
|
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
|
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
|
|
'''
|
2026-03-25 23:04:13 +00:00
|
|
|
Multiaddress support using the upstream `py-multiaddr` lib
|
|
|
|
|
(a `libp2p` community standard) instead of our own NIH parser.
|
2023-09-29 18:11:31 +00:00
|
|
|
|
2026-03-25 23:04:13 +00:00
|
|
|
- https://github.com/multiformats/multiaddr
|
|
|
|
|
- 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
|
2023-09-29 18:11:31 +00:00
|
|
|
|
|
|
|
|
'''
|
2026-03-25 23:04:13 +00:00
|
|
|
import ipaddress
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import TYPE_CHECKING
|
2023-09-29 18:11:31 +00:00
|
|
|
|
2026-03-25 23:04:13 +00:00
|
|
|
from multiaddr import Multiaddr
|
2023-09-29 18:11:31 +00:00
|
|
|
|
2026-03-25 23:04:13 +00:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
from tractor.discovery._addr import Address
|
2023-09-29 18:11:31 +00:00
|
|
|
|
2026-03-25 23:04:13 +00:00
|
|
|
# map from tractor-internal `proto_key` identifiers
|
|
|
|
|
# to the standard multiaddr protocol name strings.
|
|
|
|
|
_tpt_proto_to_maddr: dict[str, str] = {
|
|
|
|
|
'tcp': 'tcp',
|
|
|
|
|
'uds': 'unix',
|
2023-09-29 18:11:31 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 20:55:10 +00:00
|
|
|
# reverse mapping: multiaddr protocol name -> tractor proto_key
|
|
|
|
|
_maddr_to_tpt_proto: dict[str, str] = {
|
|
|
|
|
v: k for k, v in _tpt_proto_to_maddr.items()
|
|
|
|
|
}
|
2026-04-13 22:33:39 +00:00
|
|
|
# -> {'tcp': 'tcp', 'unix': 'uds'}
|
2026-03-26 20:55:10 +00:00
|
|
|
|
2023-09-29 18:11:31 +00:00
|
|
|
|
2026-03-25 23:04:13 +00:00
|
|
|
def mk_maddr(
|
|
|
|
|
addr: 'Address',
|
|
|
|
|
) -> Multiaddr:
|
2023-09-29 18:11:31 +00:00
|
|
|
'''
|
2026-03-25 23:04:13 +00:00
|
|
|
Construct a `Multiaddr` from a tractor `Address` instance,
|
|
|
|
|
dispatching on the `.proto_key` to build the correct
|
|
|
|
|
multiaddr-spec-compliant protocol path.
|
2023-09-29 18:11:31 +00:00
|
|
|
|
|
|
|
|
'''
|
2026-03-25 23:38:21 +00:00
|
|
|
proto_key: str = addr.proto_key
|
|
|
|
|
maddr_proto: str|None = _tpt_proto_to_maddr.get(proto_key)
|
|
|
|
|
if maddr_proto is None:
|
|
|
|
|
raise ValueError(
|
|
|
|
|
f'Unsupported proto_key: {proto_key!r}'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
match proto_key:
|
2026-03-25 23:04:13 +00:00
|
|
|
case 'tcp':
|
|
|
|
|
host, port = addr.unwrap()
|
|
|
|
|
ip = ipaddress.ip_address(host)
|
|
|
|
|
net_proto: str = (
|
|
|
|
|
'ip4' if ip.version == 4
|
|
|
|
|
else 'ip6'
|
|
|
|
|
)
|
|
|
|
|
return Multiaddr(
|
2026-03-25 23:38:21 +00:00
|
|
|
f'/{net_proto}/{host}/{maddr_proto}/{port}'
|
2026-03-25 23:04:13 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
case 'uds':
|
|
|
|
|
filedir, filename = addr.unwrap()
|
|
|
|
|
filepath = Path(filedir) / filename
|
2026-03-27 01:56:33 +00:00
|
|
|
# NOTE, strip any leading `/` to avoid
|
|
|
|
|
# double-slash `/unix//run/..` which the
|
|
|
|
|
# multiaddr parser rejects as "empty
|
|
|
|
|
# protocol path".
|
|
|
|
|
fpath_str: str = str(filepath).lstrip('/')
|
2026-03-25 23:04:13 +00:00
|
|
|
return Multiaddr(
|
2026-03-27 01:56:33 +00:00
|
|
|
f'/{maddr_proto}/{fpath_str}'
|
2026-03-25 23:04:13 +00:00
|
|
|
)
|
2026-03-26 20:55:10 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_maddr(
|
|
|
|
|
maddr_str: str,
|
|
|
|
|
) -> 'Address':
|
|
|
|
|
'''
|
|
|
|
|
Parse a multiaddr string into a tractor `Address`.
|
|
|
|
|
|
|
|
|
|
Inverse of `mk_maddr()`.
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
# 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
|
2026-03-27 15:21:20 +00:00
|
|
|
# unix protocol value which effectively restores
|
|
|
|
|
# the absolute-path semantics that `mk_maddr()`
|
|
|
|
|
# strips when building the multiaddr string.
|
2026-03-26 20:55:10 +00:00
|
|
|
raw: str = maddr.value_for_protocol('unix')
|
2026-03-27 15:21:20 +00:00
|
|
|
sockpath = Path(raw)
|
2026-03-26 20:55:10 +00:00
|
|
|
return UDSAddress(
|
2026-03-27 15:21:20 +00:00
|
|
|
filedir=sockpath.parent,
|
|
|
|
|
filename=sockpath.name,
|
2026-03-26 20:55:10 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
case _:
|
|
|
|
|
raise ValueError(
|
|
|
|
|
f'Unsupported multiaddr protocol combo: '
|
|
|
|
|
f'{proto_names!r}\n'
|
|
|
|
|
f'from maddr: {maddr_str!r}\n'
|
|
|
|
|
)
|
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
2026-04-13 21:36:21 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# 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
|