Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Some of the cool and cutting-edge [transport protocols](https://connectivity.lib
| [`js-peer`](./js-peer/) | Browser Chat Peer in TypeScript | ✅ | ✅ | ✅ | ❌ | ❌ |
| [`go-peer`](./go-peer/) | Chat peer implemented in Go | ✅ | ❌ | ✅ | ✅ | ✅ |
| [`rust-peer`](./rust-peer/) | Chat peer implemented in Rust | ❌ | ❌ | ✅ | ✅ | ❌ |
| [`py-peer`](./py-peer/) | Chat peer implemented in Python | ❌ | ❌ | ❌ | ✅ | ✅ |

✅ - Protocol supported
❌ - Protocol not supported
Expand Down Expand Up @@ -82,3 +83,13 @@ cargo run -- --help
cd go-peer
go run .
```

## Getting started: Python

Make sure you have the [uv package manager](https://github.com/astral-sh/uv)
installed first. Follow the instructions on their Github to install it.

```
cd py-peer
uv run hello.py
```
1 change: 1 addition & 0 deletions py-peer/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12
8 changes: 8 additions & 0 deletions py-peer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Python Peer (py-peer) of Universal Connectivity

This is the Python implementation of the [Universal Connectivity][UNIV_CONN] app showcasing the [Gossipsub][GOSSIPSUB], and eventually [QUIC][QUIC], features of the core libp2p protocol as found in the [py-libp2p][PYLIBP2P] Python libp2p implementation.

[GOSSIPSUB]: https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/README.md
[PYLIBP2P]: https://github.com/libp2p/py-libp2p
[QUIC]: https://github.com/libp2p/specs/blob/master/quic/README.md
[UNIV_CONN]: https://github.com/libp2p/universal-connectivity
6 changes: 6 additions & 0 deletions py-peer/hello.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
def main():
print("Hello from py-peer!")


if __name__ == "__main__":
main()
25 changes: 25 additions & 0 deletions py-peer/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[project]
name = "py-peer"
version = "0.1.0"
description = "Python implementation of the Universal Connectivity peer and p2p chat experience."
readme = "README.md"
requires-python = ">=3.12"
dependencies = []

[tool.uv]
dev-dependencies = [
"pytest>=8.0",
"ruff>=0.5"
]

[tool.ruff]
line-length = 100
select = ["E", "F", "I", "W", "Q"]
# E = Errors
# F = Pyflakes
# I = Imports
# W = Warnings
# Q = Quality
ignore = [
"E501", # Line too long
]
22 changes: 22 additions & 0 deletions py-peer/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from setuptools import setup, find_packages

setup(
name="py-peer",
version="0.1.0",
packages=find_packages(),
install_requires=[
"trio",
"multiaddr",
"base58",
"py-libp2p",
],
entry_points={
"console_scripts": [
"py-peer=py_peer.main:main",
],
},
description="A modular libp2p peer implementation in Python",
author="Your Name",
author_email="[email protected]",
url="https://github.com/yourusername/py-peer",
)
5 changes: 5 additions & 0 deletions py-peer/src/py_peer/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""
py-peer: A modular libp2p peer implementation in Python.
"""

__version__ = "0.1.0"
91 changes: 91 additions & 0 deletions py-peer/src/py_peer/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""
Configuration module for py-peer.
"""
import argparse
import socket
from dataclasses import dataclass
from typing import Optional

from libp2p.crypto.rsa import create_new_key_pair
from libp2p.crypto.keys import KeyPair
from libp2p.custom_types import TProtocol

# Default values
DEFAULT_TOPIC = "pubsub-chat"
DEFAULT_PORT = 8080
GOSSIPSUB_PROTOCOL_ID = TProtocol("/meshsub/1.0.0")
NOISE_PROTOCOL_ID = TProtocol("/noise")
MPLEX_PROTOCOL_ID = TProtocol("/mplex/6.7.0")


@dataclass
class PeerConfig:
"""Configuration for a libp2p peer."""
topic: str
destination: Optional[str]
port: int
verbose: bool
key_pair: KeyPair

@classmethod
def from_args(cls, args: argparse.Namespace) -> 'PeerConfig':
"""Create a PeerConfig from command line arguments."""
# Generate a key pair for the node
key_pair = create_new_key_pair()

return cls(
topic=args.topic,
destination=args.destination,
port=args.port if args.port != 0 else find_free_port(),
verbose=args.verbose,
key_pair=key_pair,
)


def find_free_port() -> int:
"""Find a free port on localhost."""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("", 0)) # Bind to a free port provided by the OS
return s.getsockname()[1]


def parse_args() -> argparse.Namespace:
"""Parse command line arguments."""
description = """
This program demonstrates a modular pubsub p2p application using libp2p with
the gossipsub protocol as the pubsub router.
"""

parser = argparse.ArgumentParser(description=description)
parser.add_argument(
"-t",
"--topic",
type=str,
help="topic name to subscribe",
default=DEFAULT_TOPIC,
)

parser.add_argument(
"-d",
"--destination",
type=str,
help="Address of peer to connect to",
default=None,
)

parser.add_argument(
"-p",
"--port",
type=int,
help="Port to listen on",
default=DEFAULT_PORT,
)

parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="Enable debug logging",
)

return parser.parse_args()
37 changes: 37 additions & 0 deletions py-peer/src/py_peer/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""
Main entry point for py-peer.
"""
import trio
import logging

from py_peer.config import parse_args, PeerConfig
from py_peer.peer import Peer
from py_peer.utils.logging import configure_logging


def main() -> None:
"""Main entry point for the application."""
# Parse command line arguments
args = parse_args()

# Configure logging
logger = configure_logging(args.verbose)

# Create peer configuration
config = PeerConfig.from_args(args)

logger.info("Running py-peer...")
logger.info(f"Your selected topic is: {config.topic}")
logger.info(f"Your peer ID is: {config.key_pair.public_key}")

# Create and start the peer
peer = Peer(config)

try:
trio.run(peer.start)
except KeyboardInterrupt:
logger.info("Application terminated by user")


if __name__ == "__main__":
main()
3 changes: 3 additions & 0 deletions py-peer/src/py_peer/network/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""
Network modules for py-peer.
"""
60 changes: 60 additions & 0 deletions py-peer/src/py_peer/network/discovery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""
Peer discovery mechanisms for py-peer.
"""
import multiaddr
import trio

from libp2p.peer.peerinfo import info_from_p2p_addr
from libp2p.host.host_interface import (
IHost,
)
from libp2p.pubsub.pubsub import Pubsub

from py_peer.utils.logging import get_logger

logger = get_logger(__name__)


async def connect_to_peer(host: IHost, peer_addr: str) -> bool:
"""
Connect to a peer using its multiaddress.

Args:
host: The libp2p host
peer_addr: The multiaddress of the peer to connect to

Returns:
True if connection was successful, False otherwise
"""
try:
maddr = multiaddr.Multiaddr(peer_addr)
protocols_in_maddr = maddr.protocols()
info = info_from_p2p_addr(maddr)

logger.debug(f"Multiaddr protocols: {protocols_in_maddr}")
logger.info(
f"Connecting to peer: {info.peer_id} "
f"using protocols: {protocols_in_maddr}"
)

await host.connect(info)
logger.info(f"Connected to peer: {info.peer_id}")
return True
except Exception:
logger.exception(f"Failed to connect to peer: {peer_addr}")
return False


async def monitor_peers(pubsub: Pubsub, interval: float = 30.0) -> None:
"""
Monitor connected peers and log information periodically.

Args:
host: The libp2p host
interval: The interval in seconds between checks
"""
while True:
# peers = host.get_network().get_peer_id()
peers = pubsub.peers
logger.debug(f"Connected to {len(peers)} peers: {peers}")
await trio.sleep(interval)
71 changes: 71 additions & 0 deletions py-peer/src/py_peer/network/host.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""
Host creation and management for py-peer.
"""
import multiaddr
from typing import List, Dict, Any

from libp2p import new_host
from libp2p.crypto.keys import KeyPair
from libp2p.custom_types import TProtocol
from libp2p.stream_muxer.mplex.mplex import Mplex
from libp2p.tools.factories import security_options_factory_factory
from libp2p.host.host_interface import IHost

from py_peer.config import NOISE_PROTOCOL_ID, MPLEX_PROTOCOL_ID
from py_peer.utils.logging import get_logger

logger = get_logger(__name__)


def create_host(
key_pair: KeyPair,
listen_addrs: List[multiaddr.Multiaddr] = None,
security_protocol: TProtocol = NOISE_PROTOCOL_ID,
muxer_protocol: TProtocol = MPLEX_PROTOCOL_ID,
) -> IHost:
"""
Create a new libp2p host.

Args:
key_pair: The key pair for the host
listen_addrs: List of multiaddresses to listen on
security_protocol: The security protocol to use
muxer_protocol: The stream multiplexer protocol to use

Returns:
A new libp2p host
"""
if listen_addrs is None:
listen_addrs = []

# Security options
security_options_factory = security_options_factory_factory(security_protocol)
security_options = security_options_factory(key_pair)

# Create a new libp2p host
host = new_host(
key_pair=key_pair,
muxer_opt={muxer_protocol: Mplex},
sec_opt=security_options,
)

logger.debug(f"Host ID: {host.get_id()}")
logger.debug(
f"Host multiselect protocols: "
f"{host.get_mux().get_protocols() if hasattr(host, 'get_mux') else 'N/A'}"
)

return host


def get_listen_multiaddr(port: int) -> multiaddr.Multiaddr:
"""
Get a multiaddress for listening on all interfaces with the given port.

Args:
port: The port to listen on

Returns:
A multiaddress
"""
return multiaddr.Multiaddr(f"/ip4/0.0.0.0/tcp/{port}")
Loading