radiodish 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pow4you
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: radiodish
3
+ Version: 1.0.0
4
+ Summary: A standalone, async RADIO-DISH implementation over UDP (ZeroMQ RFC 48), with automatic peer discovery and autowiring.
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/pow4you/radiodish
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Topic :: System :: Networking
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Dynamic: license-file
14
+
15
+ # radiodish
16
+
17
+ A standalone, dependency-free, **async** implementation of ZeroMQ's
18
+ [RADIO-DISH pattern](https://rfc.zeromq.org/spec/48/), built directly on
19
+ `asyncio`'s UDP transport rather than on libzmq/pyzmq — plus automatic
20
+ port assignment, peer discovery, and autowiring on top.
21
+
22
+ - **Standalone**: pure Python stdlib (`asyncio`, `struct`, `collections`,
23
+ `socket`). No libzmq, no third-party dependencies at all.
24
+ - **UDP**: each RADIO and DISH is a plain UDP endpoint. A UDP datagram is
25
+ already an atomic, non-multipart unit, which lines up naturally with
26
+ RFC 48's "no multipart messages" requirement for the thread-safe
27
+ socket family.
28
+ - **Async**: `RadioSocket.send()` never blocks; `DishSocket.recv()` is an
29
+ `await`-able coroutine fed by `asyncio`'s event loop — no threads.
30
+
31
+ Looking for the actor-model layer built on top of this? That's a separate
32
+ package, [`radiodish-actor`](../radiodish-actor/), which depends on this
33
+ one.
34
+
35
+ ## Install
36
+
37
+ ```
38
+ pip install radiodish
39
+ ```
40
+
41
+ For local/editable development against this repo specifically, see the
42
+ workspace root README — this package is a member of a `uv` workspace.
43
+
44
+ ## Documentation
45
+
46
+ Full documentation, with the reasoning behind the design decisions (not
47
+ just the API surface), lives in [`docs/`](docs/index.md):
48
+
49
+ - **[docs/index.md](docs/index.md)** — quick start, API sketch, package layout
50
+ - **[docs/protocol.md](docs/protocol.md)** — the RFC 48 wire format and how this implementation maps to it
51
+ - **[docs/autowire.md](docs/autowire.md)** — automatic port assignment, peer discovery (two tiers: same-machine and LAN), and autowiring; the real bugs found while building it and how they were fixed
52
+ - **[docs/examples/](docs/examples/)** — runnable examples, each documented with what it demonstrates and why
53
+
54
+ ## 30-second version
55
+
56
+ ```python
57
+ from radish import RadioSocket, DishSocket
58
+
59
+ radio = await RadioSocket().bind("0.0.0.0", 9000)
60
+ radio.connect("127.0.0.1", 9001)
61
+ await radio.send(b"hello", group="news")
62
+
63
+ dish = await DishSocket().bind("0.0.0.0", 9001)
64
+ dish.connect("127.0.0.1", 9000)
65
+ dish.join("news")
66
+ group, payload = await dish.recv()
67
+ ```
68
+
69
+ Or skip manual wiring entirely:
70
+
71
+ ```python
72
+ from radish.autowire import autowired_radish
73
+
74
+ radish, discoverer, wirer = await autowired_radish(groups=["sensors"])
75
+ radish.broadcast("21.5C", group="sensors")
76
+ group, payload = await radish.recv()
77
+ ```
78
+
79
+ See [docs/index.md](docs/index.md) for the full walkthrough.
@@ -0,0 +1,65 @@
1
+ # radiodish
2
+
3
+ A standalone, dependency-free, **async** implementation of ZeroMQ's
4
+ [RADIO-DISH pattern](https://rfc.zeromq.org/spec/48/), built directly on
5
+ `asyncio`'s UDP transport rather than on libzmq/pyzmq — plus automatic
6
+ port assignment, peer discovery, and autowiring on top.
7
+
8
+ - **Standalone**: pure Python stdlib (`asyncio`, `struct`, `collections`,
9
+ `socket`). No libzmq, no third-party dependencies at all.
10
+ - **UDP**: each RADIO and DISH is a plain UDP endpoint. A UDP datagram is
11
+ already an atomic, non-multipart unit, which lines up naturally with
12
+ RFC 48's "no multipart messages" requirement for the thread-safe
13
+ socket family.
14
+ - **Async**: `RadioSocket.send()` never blocks; `DishSocket.recv()` is an
15
+ `await`-able coroutine fed by `asyncio`'s event loop — no threads.
16
+
17
+ Looking for the actor-model layer built on top of this? That's a separate
18
+ package, [`radiodish-actor`](../radiodish-actor/), which depends on this
19
+ one.
20
+
21
+ ## Install
22
+
23
+ ```
24
+ pip install radiodish
25
+ ```
26
+
27
+ For local/editable development against this repo specifically, see the
28
+ workspace root README — this package is a member of a `uv` workspace.
29
+
30
+ ## Documentation
31
+
32
+ Full documentation, with the reasoning behind the design decisions (not
33
+ just the API surface), lives in [`docs/`](docs/index.md):
34
+
35
+ - **[docs/index.md](docs/index.md)** — quick start, API sketch, package layout
36
+ - **[docs/protocol.md](docs/protocol.md)** — the RFC 48 wire format and how this implementation maps to it
37
+ - **[docs/autowire.md](docs/autowire.md)** — automatic port assignment, peer discovery (two tiers: same-machine and LAN), and autowiring; the real bugs found while building it and how they were fixed
38
+ - **[docs/examples/](docs/examples/)** — runnable examples, each documented with what it demonstrates and why
39
+
40
+ ## 30-second version
41
+
42
+ ```python
43
+ from radish import RadioSocket, DishSocket
44
+
45
+ radio = await RadioSocket().bind("0.0.0.0", 9000)
46
+ radio.connect("127.0.0.1", 9001)
47
+ await radio.send(b"hello", group="news")
48
+
49
+ dish = await DishSocket().bind("0.0.0.0", 9001)
50
+ dish.connect("127.0.0.1", 9000)
51
+ dish.join("news")
52
+ group, payload = await dish.recv()
53
+ ```
54
+
55
+ Or skip manual wiring entirely:
56
+
57
+ ```python
58
+ from radish.autowire import autowired_radish
59
+
60
+ radish, discoverer, wirer = await autowired_radish(groups=["sensors"])
61
+ radish.broadcast("21.5C", group="sensors")
62
+ group, payload = await radish.recv()
63
+ ```
64
+
65
+ See [docs/index.md](docs/index.md) for the full walkthrough.
@@ -0,0 +1,35 @@
1
+ [project]
2
+ name = "radiodish"
3
+ version = "1.0.0"
4
+ description = "A standalone, async RADIO-DISH implementation over UDP (ZeroMQ RFC 48), with automatic peer discovery and autowiring."
5
+ requires-python = ">=3.9"
6
+ readme = "README.md"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ dependencies = []
10
+ classifiers = [
11
+ "Programming Language :: Python :: 3",
12
+ "Operating System :: OS Independent",
13
+ "Topic :: System :: Networking",
14
+ ]
15
+
16
+ [project.urls]
17
+ Homepage = "https://github.com/pow4you/radiodish"
18
+
19
+ [build-system]
20
+ requires = ["setuptools>=77.0"]
21
+ build-backend = "setuptools.build_meta"
22
+
23
+ [tool.setuptools]
24
+ packages = ["radish", "radish._internal", "radish.autowire"]
25
+
26
+ [tool.setuptools.package-dir]
27
+ radish = "src"
28
+ "radish._internal" = "src/_internal"
29
+ "radish.autowire" = "src/autowire"
30
+
31
+ [tool.pytest.ini_options]
32
+ testpaths = ["tests"]
33
+ asyncio_mode = "auto"
34
+ asyncio_default_fixture_loop_scope = "function"
35
+
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: radiodish
3
+ Version: 1.0.0
4
+ Summary: A standalone, async RADIO-DISH implementation over UDP (ZeroMQ RFC 48), with automatic peer discovery and autowiring.
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/pow4you/radiodish
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Topic :: System :: Networking
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Dynamic: license-file
14
+
15
+ # radiodish
16
+
17
+ A standalone, dependency-free, **async** implementation of ZeroMQ's
18
+ [RADIO-DISH pattern](https://rfc.zeromq.org/spec/48/), built directly on
19
+ `asyncio`'s UDP transport rather than on libzmq/pyzmq — plus automatic
20
+ port assignment, peer discovery, and autowiring on top.
21
+
22
+ - **Standalone**: pure Python stdlib (`asyncio`, `struct`, `collections`,
23
+ `socket`). No libzmq, no third-party dependencies at all.
24
+ - **UDP**: each RADIO and DISH is a plain UDP endpoint. A UDP datagram is
25
+ already an atomic, non-multipart unit, which lines up naturally with
26
+ RFC 48's "no multipart messages" requirement for the thread-safe
27
+ socket family.
28
+ - **Async**: `RadioSocket.send()` never blocks; `DishSocket.recv()` is an
29
+ `await`-able coroutine fed by `asyncio`'s event loop — no threads.
30
+
31
+ Looking for the actor-model layer built on top of this? That's a separate
32
+ package, [`radiodish-actor`](../radiodish-actor/), which depends on this
33
+ one.
34
+
35
+ ## Install
36
+
37
+ ```
38
+ pip install radiodish
39
+ ```
40
+
41
+ For local/editable development against this repo specifically, see the
42
+ workspace root README — this package is a member of a `uv` workspace.
43
+
44
+ ## Documentation
45
+
46
+ Full documentation, with the reasoning behind the design decisions (not
47
+ just the API surface), lives in [`docs/`](docs/index.md):
48
+
49
+ - **[docs/index.md](docs/index.md)** — quick start, API sketch, package layout
50
+ - **[docs/protocol.md](docs/protocol.md)** — the RFC 48 wire format and how this implementation maps to it
51
+ - **[docs/autowire.md](docs/autowire.md)** — automatic port assignment, peer discovery (two tiers: same-machine and LAN), and autowiring; the real bugs found while building it and how they were fixed
52
+ - **[docs/examples/](docs/examples/)** — runnable examples, each documented with what it demonstrates and why
53
+
54
+ ## 30-second version
55
+
56
+ ```python
57
+ from radish import RadioSocket, DishSocket
58
+
59
+ radio = await RadioSocket().bind("0.0.0.0", 9000)
60
+ radio.connect("127.0.0.1", 9001)
61
+ await radio.send(b"hello", group="news")
62
+
63
+ dish = await DishSocket().bind("0.0.0.0", 9001)
64
+ dish.connect("127.0.0.1", 9000)
65
+ dish.join("news")
66
+ group, payload = await dish.recv()
67
+ ```
68
+
69
+ Or skip manual wiring entirely:
70
+
71
+ ```python
72
+ from radish.autowire import autowired_radish
73
+
74
+ radish, discoverer, wirer = await autowired_radish(groups=["sensors"])
75
+ radish.broadcast("21.5C", group="sensors")
76
+ group, payload = await radish.recv()
77
+ ```
78
+
79
+ See [docs/index.md](docs/index.md) for the full walkthrough.
@@ -0,0 +1,27 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ radiodish.egg-info/PKG-INFO
5
+ radiodish.egg-info/SOURCES.txt
6
+ radiodish.egg-info/dependency_links.txt
7
+ radiodish.egg-info/top_level.txt
8
+ src/__init__.py
9
+ src/dish.py
10
+ src/errors.py
11
+ src/protocol.py
12
+ src/radio.py
13
+ src/transport.py
14
+ src/unified.py
15
+ src/_internal/__init__.py
16
+ src/_internal/errors.py
17
+ src/_internal/groups.py
18
+ src/autowire/__init__.py
19
+ src/autowire/compose.py
20
+ src/autowire/discovery.py
21
+ src/autowire/local_discovery.py
22
+ src/autowire/network.py
23
+ src/autowire/wiring.py
24
+ tests/test_autowire.py
25
+ tests/test_network_resilience.py
26
+ tests/test_radiodish.py
27
+ tests/test_radish_socket.py
@@ -0,0 +1 @@
1
+ radish
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,38 @@
1
+ """
2
+ radish — a standalone, asyncio-native RADIO/DISH implementation over UDP.
3
+
4
+ This is an independent implementation of the pattern described in ZeroMQ
5
+ RFC 48 (https://rfc.zeromq.org/spec/48/). It does not depend on libzmq or
6
+ pyzmq at all — it's built directly on asyncio's UDP transport.
7
+
8
+ Package layout:
9
+ radish.errors RadioDishError, InvalidGroupError (re-exported from radish._internal.errors)
10
+ radish.protocol wire format: encode_frame/decode_frame, MSG_DATA/JOIN/LEAVE
11
+ radish.transport shared plumbing: _UDPProtocol, _BoundedDropQueue, _Peer
12
+ radish.radio RadioSocket (RFC 48's RADIO type)
13
+ radish.dish DishSocket (RFC 48's DISH type)
14
+ radish.unified RadishSocket (radio+dish combined, one object)
15
+ radish.autowire automatic port assignment, peer discovery, and
16
+ peer connection on top of RadishSocket
17
+
18
+ For most uses, everything below is enough:
19
+
20
+ from radish import RadioSocket, DishSocket, RadishSocket
21
+ from radish.autowire import autowired_radish
22
+ """
23
+
24
+ from radish.errors import RadioDishError, InvalidGroupError
25
+ from radish.protocol import (
26
+ MSG_DATA, MSG_JOIN, MSG_LEAVE, MAX_GROUP_LEN, GroupLike, Address,
27
+ encode_frame, decode_frame,
28
+ )
29
+ from radish.radio import RadioSocket
30
+ from radish.dish import DishSocket
31
+ from radish.unified import RadishSocket
32
+
33
+ __all__ = [
34
+ "RadioSocket", "DishSocket", "RadishSocket",
35
+ "RadioDishError", "InvalidGroupError",
36
+ "MSG_DATA", "MSG_JOIN", "MSG_LEAVE", "MAX_GROUP_LEN", "GroupLike", "Address",
37
+ "encode_frame", "decode_frame",
38
+ ]
@@ -0,0 +1,19 @@
1
+ """
2
+ radish._internal — private implementation details of the radish package.
3
+ Leading underscore means exactly that: not public API, no stability
4
+ promise, nothing outside radish itself should import from here (and
5
+ nothing does -- radioactor only ever reaches RadioDishError via
6
+ `from radish import RadioDishError`, the public re-export).
7
+ """
8
+
9
+ from radish._internal.errors import RadioDishError, InvalidGroupError
10
+ from radish._internal.groups import GroupLike, MAX_GROUP_LEN, normalize_group, groups_as_str
11
+
12
+ __all__ = [
13
+ "RadioDishError",
14
+ "InvalidGroupError",
15
+ "GroupLike",
16
+ "MAX_GROUP_LEN",
17
+ "normalize_group",
18
+ "groups_as_str",
19
+ ]
@@ -0,0 +1,16 @@
1
+ """
2
+ radish/_internal/errors.py — the exception hierarchy for the radish
3
+ package. Private subpackage (leading underscore) -- not part of the
4
+ public API, and nothing outside radish itself needs it, including
5
+ radioactor, which only ever imports RadioDishError via `from radish
6
+ import RadioDishError` (radish.errors re-exports these under the public
7
+ namespace for that purpose).
8
+ """
9
+
10
+
11
+ class RadioDishError(Exception):
12
+ """Base error for the whole project."""
13
+
14
+
15
+ class InvalidGroupError(RadioDishError):
16
+ """Raised when a group name violates RFC 48's length constraint (0-255 bytes)."""
@@ -0,0 +1,50 @@
1
+ """
2
+ radish/_internal/groups.py — group-name handling shared between two
3
+ different representations of the same concept within radish itself:
4
+
5
+ - radish.protocol needs a STRICT, wire-format validator: a group is
6
+ 0-255 raw bytes, full stop, because that's what RFC 48's ABNF requires
7
+ and what the frame header's single length byte can represent.
8
+ - radish.autowire.discovery needs a LOOSE, JSON-safe converter: group
9
+ names have to survive being embedded in a discovery announcement
10
+ payload, so bytes get decoded to str (JSON can't carry raw bytes) and
11
+ anything already a plain string passes through untouched.
12
+
13
+ Both directions live here instead of being duplicated (or, worse, subtly
14
+ diverging) between the two modules that need them.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import Iterable, Union
20
+
21
+ from radish._internal.errors import InvalidGroupError
22
+
23
+ GroupLike = Union[str, bytes]
24
+ MAX_GROUP_LEN = 255
25
+
26
+
27
+ def normalize_group(group: GroupLike) -> bytes:
28
+ """Strict validator for the wire protocol: str or bytes in, exactly
29
+ validated bytes out. Raises InvalidGroupError for anything else or
30
+ anything over RFC 48's 255-byte limit."""
31
+ if isinstance(group, str):
32
+ group = group.encode("utf-8")
33
+ if not isinstance(group, (bytes, bytearray)):
34
+ raise InvalidGroupError(f"group must be str or bytes, got {type(group)!r}")
35
+ if len(group) > MAX_GROUP_LEN:
36
+ raise InvalidGroupError(
37
+ f"group is {len(group)} bytes, RFC 48 allows at most {MAX_GROUP_LEN}"
38
+ )
39
+ return bytes(group)
40
+
41
+
42
+ def groups_as_str(groups: Iterable) -> tuple[str, ...]:
43
+ """Loose converter for discovery announcements: decodes any bytes
44
+ entries to str (JSON can't carry raw bytes), passes plain strings
45
+ through untouched. Used for the groups/interest lists that go into a
46
+ Discoverer's JSON payload, not for wire-protocol frame encoding."""
47
+ out = []
48
+ for g in groups:
49
+ out.append(g.decode("utf-8") if isinstance(g, (bytes, bytearray)) else g)
50
+ return tuple(out)
@@ -0,0 +1,31 @@
1
+ """
2
+ radish.autowire — automatic port assignment, peer discovery, and peer
3
+ connection on top of radish.RadishSocket.
4
+
5
+ radish.autowire.network find_free_port_pair, detect_local_addresses
6
+ radish.autowire.local_discovery the "my machine" file-based tier
7
+ radish.autowire.discovery the "my LAN" multicast tier, and Discoverer
8
+ radish.autowire.wiring AutoWirer -- the actual autowiring
9
+ radish.autowire.compose autowired_radish() -- one-call entrypoint
10
+
11
+ For most uses:
12
+
13
+ from radish.autowire import autowired_radish
14
+ radish, discoverer, wirer = await autowired_radish(groups=["sensors"])
15
+ """
16
+
17
+ from radish.autowire.network import find_free_port_pair, detect_local_addresses
18
+ from radish.autowire.discovery import Discoverer, PeerInfo, MULTICAST_GROUP, DEFAULT_DISCOVERY_PORT
19
+ from radish.autowire.wiring import AutoWirer
20
+ from radish.autowire.compose import autowired_radish
21
+
22
+ __all__ = [
23
+ "autowired_radish",
24
+ "Discoverer",
25
+ "AutoWirer",
26
+ "PeerInfo",
27
+ "find_free_port_pair",
28
+ "detect_local_addresses",
29
+ "MULTICAST_GROUP",
30
+ "DEFAULT_DISCOVERY_PORT",
31
+ ]
@@ -0,0 +1,91 @@
1
+ """
2
+ radish/autowire/compose.py — autowired_radish(): "on the creation of a
3
+ radish, autowire its peers." One call that finds ports, binds, joins
4
+ groups, and starts discovery + autowiring together.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import uuid
10
+ from typing import Iterable, Optional
11
+
12
+ from radish.unified import RadishSocket
13
+ from radish.autowire.network import find_free_port_pair, detect_local_addresses
14
+ from radish.autowire.discovery import Discoverer, DEFAULT_DISCOVERY_PORT
15
+ from radish.autowire.wiring import AutoWirer
16
+
17
+
18
+ async def autowired_radish(
19
+ host: Optional[str] = None,
20
+ groups: Iterable = (),
21
+ interest_groups: Optional[Iterable] = None,
22
+ port_range: tuple[int, int] = (20000, 40000),
23
+ discovery_port: int = DEFAULT_DISCOVERY_PORT,
24
+ seeds: Iterable[tuple[str, int]] = (),
25
+ node_id: Optional[str] = None,
26
+ autowire_poll_interval: float = 0.3,
27
+ ) -> tuple[RadishSocket, Discoverer, AutoWirer]:
28
+ """
29
+ One call, "on the creation of a radish": finds a free port pair, binds a
30
+ RadishSocket on it, joins `groups`, starts announcing/listening for
31
+ peers, and continuously connects to any discovered peer whose groups
32
+ overlap `interest_groups` (defaults to the same as `groups` — "only wire
33
+ me to peers who care about what I care about").
34
+
35
+ Leave `host` unset for the normal case: binds on "0.0.0.0" (every local
36
+ interface at once, loopback included -- always bindable, no guessing
37
+ needed) and announces every address this machine actually has
38
+ (detect_local_addresses(): loopback first, then whatever LAN-facing
39
+ addresses this machine's own hostname resolves to). Peers connect back
40
+ over whichever of those is actually reachable from where they sit --
41
+ loopback for a same-machine peer, a LAN address for one elsewhere on
42
+ the network. Pass an explicit `host` only to override both bind and
43
+ announce with one specific address (e.g. pairing a container-internal
44
+ address with `seeds=` on the other end).
45
+
46
+ Returns (radish, discoverer, autowirer). Hold onto discoverer/autowirer
47
+ if you want to inspect known/wired peers later; to tear everything down,
48
+ stop() the autowirer and discoverer, then close() the radish (see
49
+ example_autowire.py).
50
+ """
51
+ if host is not None:
52
+ bind_host = host
53
+ announce_addresses = [host]
54
+ try:
55
+ port, radio_sock, dish_sock = find_free_port_pair(bind_host, *port_range)
56
+ except RuntimeError:
57
+ if bind_host == "127.0.0.1":
58
+ raise
59
+ # an explicitly-given host turned out not to be bindable --
60
+ # fall back to localhost rather than failing outright.
61
+ bind_host = "127.0.0.1"
62
+ announce_addresses = [bind_host]
63
+ port, radio_sock, dish_sock = find_free_port_pair(bind_host, *port_range)
64
+ else:
65
+ bind_host = "0.0.0.0"
66
+ announce_addresses = detect_local_addresses()
67
+ port, radio_sock, dish_sock = find_free_port_pair(bind_host, *port_range)
68
+
69
+ radish = RadishSocket()
70
+ await radish.bind(bind_host, port, radio_sock=radio_sock, dish_sock=dish_sock)
71
+ for g in groups:
72
+ radish.join(g)
73
+
74
+ node_id = node_id or uuid.uuid4().hex[:12]
75
+ interest = groups if interest_groups is None else interest_groups
76
+ discoverer = await Discoverer(
77
+ node_id=node_id,
78
+ addresses=announce_addresses,
79
+ port=port,
80
+ groups=groups,
81
+ interest=interest,
82
+ discovery_port=discovery_port,
83
+ seeds=seeds,
84
+ auto_refresh_addresses=(host is None),
85
+ ).start()
86
+
87
+ wirer = await AutoWirer(radish, discoverer, interest_groups=interest).start(
88
+ poll_interval=autowire_poll_interval
89
+ )
90
+
91
+ return radish, discoverer, wirer