Metadata-Version: 2.4
Name: peerlink
Version: 1.0.0
Summary: Zero-config P2P RPC over LAN/WiFi using mDNS + UDP
Author: PeerLink Authors
License: MIT
License-File: LICENSE
Keywords: mdns,networking,p2p,rpc,udp
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: System :: Networking
Requires-Python: >=3.10
Requires-Dist: click>=8.0
Requires-Dist: zeroconf>=0.39
Provides-Extra: dev
Requires-Dist: click; extra == 'dev'
Requires-Dist: mypy; extra == 'dev'
Requires-Dist: pytest; extra == 'dev'
Requires-Dist: ruff; extra == 'dev'
Description-Content-Type: text/markdown

# PeerLink

Zero-config P2P RPC over LAN/WiFi, built on:

- mDNS service discovery (via `zeroconf`)
- UDP transport
- JSON message framing

PeerLink lets you expose Python functions on one node and call them from another node on the same network with a simple, type-hinted API.

## Installation

```bash
pip install peerlink
```

Requires Python 3.10+.

## Core Concepts

- **Node**: a `PeerLink` instance with a unique name (e.g. `"Phone1"`).
- **Discovery**: nodes announce themselves over mDNS and discover each other automatically.
- **RPC**: you `register()` Python callables on a node and call them remotely by name.
- **Peers**: other nodes discovered on the network; you can address them by name or via a `PeerProxy`.

## Quick Start (two devices)

Run these on **two separate machines or terminals** on the same LAN.

### Phone1 (server)

```python
from peerlink import PeerLink

with PeerLink("Phone1") as node:
    node.register("add", lambda x, y: x + y)
    print("Phone1 ready; waiting for RPC calls...")

    import time
    while True:
        time.sleep(1)
```

### Phone2 (client)

```python
from peerlink import PeerLink

with PeerLink("Phone2") as node:
    if not node.wait_for_peers(1, timeout=5):
        raise SystemExit("No peers discovered within 5 seconds")

    result = node.peer("Phone1").add(25, 17)
    print(result)  # 42
```

## Python API

The public API is fully type-annotated; the most important entrypoints are:

| Method | Description |
|---|---|
| `PeerLink(name: str, verbose: bool = False)` | Create a named node |
| `.register(name: str, func: Callable) -> PeerLink` | Expose a function as RPC (chainable) |
| `.start() / .stop()` | Bootstrap / graceful shutdown (also used by the context manager) |
| `.call(peer: str, func: str, *args, timeout: float = 5)` | Unicast RPC call, returns the remote result |
| `.call("ALL", func, *args, timeout=5)` | Broadcast to all peers, returns `dict[str, Any]` mapping peer name → result/exception |
| `.peer(name: str) -> PeerProxy` | Get a peer proxy so you can call `peer.add(1, 2)` |
| `.wait_for_peers(n: int, timeout: float = 30)` | Block until at least `n` peers are discovered |
| `.peer_names() -> list[str]` | List discovered peer names |

### Broadcast semantics

When you call:

```python
results = node.call("ALL", "some_func", 123)
```

you get a `dict[str, Any]` where:

- Successful peers map to their return value.
- Peers that failed map to an **exception instance** (typically `RemoteError`, `PeerTimeoutError`, or `PeerNotFound`), so your code can inspect or log per-peer failures without aborting the whole call.

## Error Handling

PeerLink uses standard Python exceptions so failures can be handled idiomatically:

| Error | Raised when |
|---|---|
| `PeerNotFound` (alias: `PeerNotFoundError`) | Target peer is unknown or has expired from the discovery cache |
| `PeerTimeoutError` (subclass of `TimeoutError`) | No reply within the given timeout for a unicast call |
| `RemoteError` | The remote function raised an exception; contains the remote type and message |

Broadcast calls never raise directly; instead they return exceptions per-peer in the result dict as described above.

## CLI Usage

Installing `peerlink` also installs a `peerlink` command-line tool via `[project.scripts]` in `pyproject.toml`:

```bash
peerlink discover          # list peers on the local network
peerlink ping Phone1       # test connectivity to peer "Phone1"
```

Under the hood this is equivalent to `python -m peerlink.cli` but using the entry point is the recommended interface.

## Example: Swarm Demo

The repository includes `swarm_demo.py`, which you can run on multiple devices:

```bash
# Terminal 1–N (same WiFi, any device)
python swarm_demo.py Phone1
python swarm_demo.py Laptop2
python swarm_demo.py RPi4
```

One node periodically broadcasts RPC calls to all others, demonstrating `.call("ALL", ...)` and parallel fan-out.

## Development & Release

Build and publish to PyPI:

```bash
python -m pip install --upgrade build twine
python -m build
python -m twine upload dist/*
```

The project is licensed under the MIT License (see `LICENSE`).

