diff --git a/.gitignore b/.gitignore index 8e8f5b54..f099dde8 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,4 @@ yarn.lock .vscode/ .DS_Store go-peer/go-peer -**/.idea +**/.idea \ No newline at end of file diff --git a/go-peer/chatroom.go b/go-peer/chatroom.go index f5fcd6a6..9bae56ec 100644 --- a/go-peer/chatroom.go +++ b/go-peer/chatroom.go @@ -207,4 +207,4 @@ func (cr *ChatRoom) requestFile(toPeer peer.ID, fileID []byte) ([]byte, error) { } return fileBody, nil -} +} \ No newline at end of file diff --git a/go-peer/identity.go b/go-peer/identity.go index 5fdedd53..535c50f8 100644 --- a/go-peer/identity.go +++ b/go-peer/identity.go @@ -47,4 +47,4 @@ func GenerateIdentity(path string) (crypto.PrivKey, error) { err = os.WriteFile(path, bytes, 0400) return privk, err -} +} \ No newline at end of file diff --git a/go-peer/main.go b/go-peer/main.go index 5ec66ab5..a2daaf18 100644 --- a/go-peer/main.go +++ b/go-peer/main.go @@ -379,4 +379,4 @@ func getResourceManager() network.ResourceManager { panic(err) } return rcmgr -} +} \ No newline at end of file diff --git a/py-libp2p b/py-libp2p new file mode 160000 index 00000000..f9f8cea7 --- /dev/null +++ b/py-libp2p @@ -0,0 +1 @@ +Subproject commit f9f8cea7a9accec3d524f96cb2cf1b86cd994f08 diff --git a/py-peer/src/py_peer/__init__.py b/py-peer/.gitignore similarity index 100% rename from py-peer/src/py_peer/__init__.py rename to py-peer/.gitignore diff --git a/py-peer/README.md b/py-peer/README.md index 010f8532..91aa9741 100644 --- a/py-peer/README.md +++ b/py-peer/README.md @@ -1,6 +1,598 @@ -# Python Peer (py-peer) of Universal Connectivity +# py-peer 🌐 -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. +A Python implementation of the Universal Connectivity peer-to-peer chat application using libp2p networking with multiple UI options including a modern mobile-friendly interface. + +This is the Python implementation of the [Universal Connectivity][UNIV_CONN] app showcasing the [Gossipsub][GOSSIPSUB] features of the core libp2p protocol as found in the [py-libp2p][PYLIBP2P] Python libp2p implementation. The implementation currently uses TCP. + +## πŸ“‹ Table of Contents + +- [Overview](#overview) +- [Features](#features) +- [Screenshots](#screenshots) +- [Architecture](#architecture) +- [Prerequisites](#prerequisites) +- [Installation](#installation) +- [Usage](#usage) +- [User Interfaces](#user-interfaces) +- [Configuration](#configuration) +- [Development](#development) +- [Troubleshooting](#troubleshooting) +- [Contributing](#contributing) + +## πŸš€ Overview + +py-peer is a decentralized chat application that enables peer-to-peer communication without requiring central servers. Built on libp2p, it provides secure, direct communication between participants using modern networking protocols. It offers three distinct UI modes to suit different use cases: a mobile-friendly Kivy interface, a terminal-based Textual TUI, and a simple CLI mode. + +## πŸ“Έ Screenshots + +### Kivy Mobile-Friendly Interface (NEW!) +![py-peer Kivy UI](assets/images/py-peer-kivy.png) + +*The modern Kivy/KivyMD interface featuring WhatsApp-style navigation, topic-based conversations, and one-tap copy functionality for peer IDs and multiaddresses.* + +### Textual TUI Interface +![py-peer Textual UI](assets/images/py-peer-textual-ui.png) + +*The Textual Terminal User Interface showing a live chat session with multiple connected peers. The interface features a main chat area, connected peers panel, system messages, and input field.* + +## 🎯 Key Technologies + +- **[libp2p](https://libp2p.io/)** - Modular peer-to-peer networking stack +- **[Trio](https://trio.readthedocs.io/)** - Async/await framework for Python +- **[KivyMD](https://kivymd.readthedocs.io/)** - Material Design mobile UI framework +- **[Textual](https://textual.textualize.io/)** - Modern Terminal User Interface framework +- **[GossipSub](https://docs.libp2p.io/concepts/pubsub/overview/)** - Pub/sub messaging protocol + +## ✨ Features + +### Core Features +- **Peer-to-Peer Chat** - Direct communication without central servers +- **Multiple UI Modes** - Kivy mobile UI, Textual TUI, or simple CLI mode +- **Real-time Messaging** - Instant message delivery through GossipSub +- **Peer Discovery** - Automatic discovery of other peers in the network +- **Cross-Platform** - Works on Linux, macOS, Windows, and mobile platforms +- **Secure Communication** - Built-in encryption and peer authentication + +### Advanced Features (NEW!) +- **Topic-Based Conversations** - Subscribe to and chat in multiple topics simultaneously +- **WhatsApp-Style Interface** - Intuitive topic selector with unread message counts +- **Dynamic Topic Management** - Add new topics on the fly without restarting +- **Per-Topic Message Storage** - Messages organized by topic with read/unread tracking +- **Dynamic Peer Connection** - Connect to peers using their multiaddress through the UI +- **One-Tap Copy** - Easy sharing of Peer ID and Multiaddr with clickable copy +- **Message Filtering** - View messages only for the selected topic +- **Custom Topics** - Use custom topic names via command line or UI +- **Persistent Connections** - Automatic peer connection maintenance + +### UI-Specific Features + +#### Kivy UI Features +- Topic list with unread counts +- Per-topic conversation views +- Tap-to-copy for Peer ID and Multiaddr +- Dynamic peer connection dialog +- Connection info display +- Material Design aesthetic + +#### Textual TUI Features +- Live peer count display +- System message logging +- Interactive command support +- Real-time chat updates + +#### CLI Features +- Simple text-based interface +- Command history +- Minimal resource usage + +## πŸ—οΈ Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ UI Layer (Choose One) β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Kivy UI β”‚ Textual TUI β”‚ CLI Mode β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β€’ Topics List β”‚ β€’ Chat Window β”‚ β€’ Simple Input/Output β”‚ +β”‚ β€’ Chat Screens β”‚ β€’ Peers Panel β”‚ β€’ Commands β”‚ +β”‚ β€’ Copy Features β”‚ β€’ System Log β”‚ β€’ Direct Messaging β”‚ +β”‚ β€’ Peer Connect β”‚ β€’ Commands β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Headless Service β”‚ + β”‚ β”‚ + β”‚ β€’ Message Queue β”‚ + β”‚ β€’ Topic Storage β”‚ + β”‚ β€’ Event Loop β”‚ + β”‚ β€’ State Mgmt β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Chat Room β”‚ + β”‚ β”‚ + β”‚ β€’ libp2p Host β”‚ + β”‚ β€’ PubSub/GossipSubβ”‚ + β”‚ β€’ DHT β”‚ + β”‚ β€’ Topic Handlers β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ P2P Network β”‚ + β”‚ β”‚ + β”‚ β€’ Peer Discovery β”‚ + β”‚ β€’ Message Relay β”‚ + β”‚ β€’ Topic Routing β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Components + +- **main.py** - Application entry point and argument parsing +- **kivy_ui.py** - Modern mobile-friendly Kivy interface (NEW!) +- **ui.py** - Textual-based terminal user interface +- **headless.py** - Background service managing libp2p operations +- **chatroom.py** - Chat room logic, topic management, and message handling + +### Message Flow + +``` +User Input β†’ UI Thread β†’ Queue β†’ Async Thread (Trio) + ↓ + HeadlessService + ↓ + ChatRoom + ↓ + GossipSub/PubSub + ↓ + P2P Network + ↓ + Other Peers +``` + +## πŸ“‹ Prerequisites + +- **Python 3.12+** +- **uv** (recommended) or pip package manager +- Network connectivity for peer-to-peer communication +- For Kivy UI: Additional system dependencies (see Installation) + +## πŸ› οΈ Installation + +### Option 1: Using uv (Recommended) + +```bash +# Clone the repository +git clone https://github.com/sumanjeet0012/universal-connectivity.git +cd universal-connectivity + +# Switch to the py-peer development branch +git checkout py-peer-development +cd py-peer + +# Create virtual environment +uv venv + +# Activate virtual environment +source .venv/bin/activate # Linux/macOS +# or +.venv\Scripts\activate # Windows + +# Install the package +uv pip install -e . +``` + +### Option 2: Using pip + +```bash +# Clone the repository +git clone https://github.com/sumanjeet0012/universal-connectivity.git +cd universal-connectivity + +# Switch to the py-peer development branch +git checkout py-peer-development +cd py-peer + +# Create virtual environment +python -m venv .venv + +# Activate virtual environment +source .venv/bin/activate # Linux/macOS +# or +.venv\Scripts\activate # Windows + +# Install the package +pip install -e . +``` + +### Kivy UI Dependencies + +For the Kivy mobile interface, you may need additional system dependencies: + +**Linux (Ubuntu/Debian):** +```bash +sudo apt-get install -y \ + python3-dev \ + build-essential \ + git \ + ffmpeg \ + libsdl2-dev \ + libsdl2-image-dev \ + libsdl2-mixer-dev \ + libsdl2-ttf-dev \ + libportmidi-dev \ + libswscale-dev \ + libavformat-dev \ + libavcodec-dev \ + zlib1g-dev +``` + +**macOS:** +```bash +brew install sdl2 sdl2_image sdl2_ttf sdl2_mixer +``` + +## 🎯 Usage + +### Quick Start + +```bash +# Kivy UI (Mobile-friendly interface) +python main.py --kivy --nick Alice + +# Textual TUI (Terminal interface) +python main.py --ui --nick Bob + +# Simple CLI mode +python main.py --nick Charlie + +# Headless mode (no UI) +python main.py --headless --nick Dave +``` + +### Command Line Options + +| Option | Description | Example | +|--------|-------------|---------| +| `--nick NAME` | Set your nickname (required) | `--nick Alice` | +| `--kivy` | Use Kivy mobile-friendly UI | `--kivy` | +| `--ui` | Use Textual TUI interface | `--ui` | +| `--headless` | Run without UI (logs only) | `--headless` | +| `-c, --connect ADDR` | Connect to specific peer | `-c /ip4/...` | +| `-p, --port PORT` | Set listening port | `-p 8080` | +| `-t, --topic TOPIC` | Set custom default topic | `-t my-topic` | +| `-s, --seed SEED` | Seed for deterministic peer ID | `-s 12345` | +| `-v, --verbose` | Enable debug logging | `-v` | +| `--no-strict-signing` | Allow unsigned messages | `--no-strict-signing` | + +### Connecting Peers + +**Peer 1:** +```bash +python main.py --kivy --nick Alice --port 9095 +# Get your multiaddr from the info dialog (tap ℹ️ icon) +``` + +**Peer 2:** +```bash +# Use the multiaddr from Peer 1 +python main.py --kivy --nick Bob --connect /ip4/192.168.1.100/tcp/9095/p2p/QmXXX... +``` + +## πŸ–₯️ User Interfaces + +### 1. Kivy UI (Recommended for Mobile/Desktop) + +**Features:** +- WhatsApp-style topic selector +- Per-topic conversations +- Unread message counts +- Tap-to-copy for sharing +- Dynamic peer connection +- Material Design + +**Navigation:** +``` +Topics Screen (Main) + β”œβ”€ Tap topic β†’ Open conversation + β”œβ”€ + button β†’ Subscribe to new topic + β”œβ”€ πŸ”— button β†’ Connect to peer + └─ ℹ️ button β†’ View connection info + +Chat Screen + β”œβ”€ Back β†’ Return to topics + β”œβ”€ ℹ️ button β†’ Show status + └─ Type & send messages +``` + +**Copying Peer Info:** +1. Tap ℹ️ (info) icon +2. Tap on Peer ID or Multiaddr card +3. Value copied to clipboard automatically + +### 2. Textual TUI + +**Features:** +- Terminal-based interface +- Live peer count +- System messages panel +- Command support + +**Commands:** +- `/quit` - Exit the chat +- `/peers` - Show connected peers +- `/status` - Display connection status +- `/multiaddr` - Show your multiaddress + +**Note:** Copy multiaddr from `system_messages.txt` file for sharing. + +### 3. Simple CLI Mode + +**Features:** +- Minimal interface +- Direct input/output +- Command history +- Low resource usage + +**Usage:** +Just type messages and press Enter. Use `/quit` to exit. + +## βš™οΈ Configuration + +### Custom Topics + +**Via Command Line:** +```bash +python main.py --kivy --nick Alice --topic my-custom-topic +``` + +**Via UI (Kivy):** +1. Tap + button in Topics screen +2. Enter topic name +3. Tap SUBSCRIBE + +### Dynamic Peer Connection (Kivy UI) + +1. Get peer's multiaddr: + - Tap ℹ️ icon in Topics screen + - Tap on Multiaddr card to copy + +2. Connect to peer: + - Tap πŸ”— icon in Topics screen + - Paste multiaddr + - Tap CONNECT + +### Message Storage + +Messages are stored per-topic with read/unread tracking: +- Unread messages appear in topic list +- Opening a topic marks messages as read +- Messages persist during session only + +### Log Files + +- **`system_messages.txt`** - System events and connection logs + - Format: `[HH:MM:SS] message` + - Contains: startup, peer connections, errors, multiaddr + +## πŸ”§ Development + +### Project Structure + +``` +py-peer/ +β”œβ”€β”€ main.py # Entry point & argument parsing +β”œβ”€β”€ kivy_ui.py # Kivy mobile UI (NEW!) +β”œβ”€β”€ ui.py # Textual TUI implementation +β”œβ”€β”€ headless.py # Background service & state management +β”œβ”€β”€ chatroom.py # Chat room logic & topic handling +β”œβ”€β”€ pyproject.toml # Project configuration & dependencies +β”œβ”€β”€ uv.lock # Dependency lock file +β”œβ”€β”€ system_messages.txt # System logs +└── README.md # This file +``` + +### Key Classes + +**kivy_ui.py:** +- `ChatScreen` - Per-topic conversation view +- `TopicsScreen` - Main topic selector (WhatsApp-style) +- `PeersScreen` - Connected peers list +- `ChatApp` - Main Kivy application + +**headless.py:** +- `HeadlessService` - Background libp2p service +- Manages queues, message storage, peer connections +- Per-topic message tracking with read/unread status + +**chatroom.py:** +- `ChatRoom` - Core libp2p chat functionality +- Dynamic topic subscription +- Unified message handler for all topics +- Message validation & signing + +### Running from Source + +```bash +# Development mode with debug logging +python main.py --kivy --nick TestUser --verbose + +# Test with multiple topics +python main.py --kivy --nick Alice --topic dev-chat + +# Run with specific port +python main.py --kivy --nick Bob --port 8080 +``` + +### Code Style + +The project follows Python best practices: +- Type hints where applicable +- Async/await patterns with Trio +- Modular architecture +- Thread-safe queue-based communication +- Comprehensive logging +- Material Design UI principles (Kivy) + +## πŸ› Troubleshooting + +### Common Issues + +**1. Port Already in Use** +```bash +# Solution: Specify a different port +python main.py --kivy --nick YourName --port 8081 +``` + +**2. No Peers Found** +- Ensure other peers are running on the same network +- Check firewall settings +- Use `--connect` or UI connect button to manually connect +- Verify multiaddr format is correct + +**3. Kivy UI Not Starting** +```bash +# Install system dependencies (Linux) +sudo apt-get install libsdl2-dev + +# Reinstall Kivy +pip install --force-reinstall kivy kivymd +``` + +**4. Messages Not Appearing** +- Check if you're subscribed to the correct topic +- Verify peer connection in info dialog +- Look for errors in system_messages.txt + +**5. Clipboard Copy Not Working (Kivy)** +- Ensure clipboard permissions (mobile) +- Try restarting the app +- Check system logs for errors + +### Debug Mode + +Enable verbose logging to diagnose issues: +```bash +python main.py --kivy --nick DebugUser --verbose +``` + +Check logs: +```bash +tail -f system_messages.txt +``` + +### Network Testing + +Test peer connectivity: +```bash +# Terminal 1 +python main.py --kivy --nick Peer1 --port 8080 + +# Get multiaddr from Peer1's info dialog + +# Terminal 2 (connect to Peer1) +python main.py --kivy --nick Peer2 --connect /ip4/127.0.0.1/tcp/8080/p2p/PEER_ID +``` + +### Topic Issues + +**Can't receive messages on new topic:** +- Ensure both peers are subscribed to the same topic name +- Topic names are case-sensitive +- Check spelling in both peers + +**Unread counts not updating:** +- Open the chat screen for that topic +- Messages are marked read when you view the topic + +## πŸ“š Advanced Usage + +### Multiple Topics + +Subscribe to multiple topics and switch between conversations: +```bash +# Start with default topic +python main.py --kivy --nick Alice + +# In UI: +# 1. Tap + to add "project-updates" +# 2. Tap + to add "random-chat" +# 3. Switch between topics by tapping on them +``` + +### Custom Topic Names + +```bash +# Use your own topic namespace +python main.py --kivy --nick Alice --topic my-company-chat +``` + +### Peer Groups + +Create private peer groups by using unique topic names: +```bash +# All peers use the same custom topic +python main.py --kivy --nick Alice --topic secret-group-2024 +python main.py --kivy --nick Bob --topic secret-group-2024 +``` + +## 🀝 Contributing + +We welcome contributions! Here's how: + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Make your changes +4. Test thoroughly (all three UI modes) +5. Commit your changes (`git commit -m 'Add amazing feature'`) +6. Push to the branch (`git push origin feature/amazing-feature`) +7. Open a Pull Request + +### Development Guidelines + +- Follow existing code style +- Add type hints +- Update README for new features +- Test on multiple platforms if possible +- Ensure backward compatibility + +## πŸ™ Acknowledgments + +- [libp2p](https://libp2p.io/) - Peer-to-peer networking framework +- [Trio](https://trio.readthedocs.io/) - Async framework +- [Kivy](https://kivy.org/) & [KivyMD](https://kivymd.readthedocs.io/) - Mobile UI framework +- [Textual](https://textual.textualize.io/) - Terminal UI framework +- Universal Connectivity project contributors + +## πŸ“„ License + +This project is part of the Universal Connectivity project. See LICENSE files for details. + +--- + +## πŸ“ž Support + +For support and questions: +- Create an issue in the [GitHub repository](https://github.com/sumanjeet0012/universal-connectivity) +- Check the troubleshooting section above +- Review the system logs in `system_messages.txt` +- Join our community discussions + +## πŸ—ΊοΈ Roadmap + +- [ ] Message persistence across sessions +- [ ] File sharing support +- [ ] Voice/video chat +- [ ] End-to-end encryption +- [ ] Mobile app packaging (Android/iOS) +- [ ] Bootstrap node configuration persistence +- [ ] Message search functionality +- [ ] Notification system +- [ ] Custom themes + +**Happy chatting! πŸŽ‰** [GOSSIPSUB]: https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/README.md [PYLIBP2P]: https://github.com/libp2p/py-libp2p diff --git a/py-peer/assets/images/py-peer-kivy.png b/py-peer/assets/images/py-peer-kivy.png new file mode 100644 index 00000000..fff85f0a Binary files /dev/null and b/py-peer/assets/images/py-peer-kivy.png differ diff --git a/py-peer/assets/images/py-peer-textual-ui.png b/py-peer/assets/images/py-peer-textual-ui.png new file mode 100644 index 00000000..08b635bf Binary files /dev/null and b/py-peer/assets/images/py-peer-textual-ui.png differ diff --git a/py-peer/chatroom.py b/py-peer/chatroom.py new file mode 100644 index 00000000..5cca1e73 --- /dev/null +++ b/py-peer/chatroom.py @@ -0,0 +1,437 @@ +""" +ChatRoom module for Universal Connectivity Python Peer + +This module handles chat room functionality including message handling, +pubsub subscriptions, and peer discovery. +""" + +import base58 +import logging +import time +import trio +from dataclasses import dataclass +from typing import Set, Optional, AsyncIterator + +from libp2p.host.basic_host import BasicHost +from libp2p.pubsub.pb.rpc_pb2 import Message +from libp2p.pubsub.pubsub import Pubsub + +logger = logging.getLogger("chatroom") + +# Create a separate logger for system messages +system_logger = logging.getLogger("system_messages") +system_handler = logging.FileHandler("system_messages.txt", mode='a') +system_handler.setFormatter(logging.Formatter("[%(asctime)s] %(message)s", datefmt="%H:%M:%S")) +system_logger.addHandler(system_handler) +system_logger.setLevel(logging.INFO) +system_logger.propagate = False # Don't send to parent loggers + +# Chat room buffer size for incoming messages +CHAT_ROOM_BUF_SIZE = 128 + +# Topics used in the chat system +PUBSUB_DISCOVERY_TOPIC = "universal-connectivity-browser-peer-discovery" +CHAT_TOPIC = "universal-connectivity" + + +@dataclass +class ChatMessage: + """Represents a chat message.""" + message: str + sender_id: str + sender_nick: str + topic: str = None # Topic the message was received on + timestamp: Optional[float] = None + + def __post_init__(self): + if self.timestamp is None: + self.timestamp = time.time() + + +class ChatRoom: + """ + Represents a subscription to PubSub topics for chat functionality. + Messages can be published to topics and received messages are handled + through callback functions. + """ + + def __init__(self, host: BasicHost, pubsub: Pubsub, nickname: str, multiaddr: str = None, headless_service=None, topic: str = None): + self.host = host + self.pubsub = pubsub + self.nickname = nickname + self.peer_id = str(host.get_id()) + self.multiaddr = multiaddr or f"unknown/{self.peer_id}" + self.headless_service = headless_service # Reference for identify protocol + + # Use custom topic if provided, otherwise use default + self.chat_topic = topic if topic else CHAT_TOPIC + + # Subscriptions - now a dictionary to track all subscriptions + self.subscriptions = {} # topic_name -> subscription object + self.chat_subscription = None + self.discovery_subscription = None + + # Message handlers + self.message_handlers = [] + self.system_message_handlers = [] + + # Topic handlers - stores (topic_name, subscription) for dynamic topics + self.topic_handlers = [] + self.active_topic_handlers = set() # Track which topics already have handlers running + + # Running state + self.running = False + self.nursery = None # Store nursery reference for spawning new handlers + + logger.info(f"ChatRoom initialized for peer {self.peer_id[:8]}... with nickname '{nickname}'") + logger.info(f"Chat topic: {self.chat_topic}") + self._log_system_message("Universal Connectivity Chat Started") + self._log_system_message(f"Nickname: {nickname}") + self._log_system_message(f"Topic: {self.chat_topic}") + self._log_system_message(f"Multiaddr: {self.multiaddr}") + self._log_system_message("Commands: /quit, /peers, /status, /multiaddr") + + def _log_system_message(self, message: str): + """Log system message to file.""" + system_logger.info(message) + + @classmethod + async def join_chat_room(cls, host: BasicHost, pubsub: Pubsub, nickname: str, multiaddr: str = None, headless_service=None, topic: str = None) -> "ChatRoom": + """Create and join a chat room.""" + chat_room = cls(host, pubsub, nickname, multiaddr, headless_service, topic) + await chat_room._subscribe_to_topics() + chat_room._log_system_message(f"Joined chat room as '{nickname}'") + return chat_room + + async def _subscribe_to_topics(self): + """Subscribe to all necessary topics.""" + try: + # Subscribe to chat topic (either custom or default) + self.chat_subscription = await self.pubsub.subscribe(self.chat_topic) + self.subscriptions[self.chat_topic] = self.chat_subscription + logger.info(f"Subscribed to chat topic: {self.chat_topic}") + + # Add chat topic to handlers list + self.topic_handlers.append((self.chat_topic, self.chat_subscription)) + + # Subscribe to discovery topic + self.discovery_subscription = await self.pubsub.subscribe(PUBSUB_DISCOVERY_TOPIC) + self.subscriptions[PUBSUB_DISCOVERY_TOPIC] = self.discovery_subscription + logger.info(f"Subscribed to discovery topic: {PUBSUB_DISCOVERY_TOPIC}") + + # Add discovery topic to handlers list + self.topic_handlers.append((PUBSUB_DISCOVERY_TOPIC, self.discovery_subscription)) + + except Exception as e: + logger.error(f"Failed to subscribe to topics: {e}") + self._log_system_message(f"ERROR: Failed to subscribe to topics: {e}") + raise + + async def publish_message(self, message: str): + """Publish a chat message in plain text format (Go-compatible).""" + try: + # Check if we have any peers connected + peer_count = len(self.pubsub.peers) + logger.info(f"πŸ“€ Publishing message to {peer_count} peers: {message}") + logger.info(f"Total pubsub peers: {list(self.pubsub.peers.keys())}") + + # Send plain text message (Go-compatible format) to the custom topic + print(f"Sending message {message}") + await self.pubsub.publish(self.chat_topic, message.encode()) + logger.info(f"βœ… Message published successfully to topic '{self.chat_topic}'") + + if peer_count == 0: + print(f"⚠️ No peers connected - message sent to topic but no one will receive it") + else: + print(f"βœ“ Message sent to {peer_count} peer(s)") + + except Exception as e: + logger.error(f"❌ Failed to publish message: {e}") + print(f"❌ Error sending message: {e}") + self._log_system_message(f"ERROR: Failed to publish message: {e}") + + async def publish_to_topic(self, topic: str, message: str): + """Publish a message to a specific topic.""" + try: + # Check if we're subscribed to this topic + if topic not in self.subscriptions: + logger.warning(f"Not subscribed to topic: {topic}") + return False + + peer_count = len(self.pubsub.peers) + logger.info(f"πŸ“€ Publishing message to topic '{topic}' with {peer_count} peers: {message}") + + # Send plain text message + await self.pubsub.publish(topic, message.encode()) + logger.info(f"βœ… Message published successfully to topic '{topic}'") + + return True + + except Exception as e: + logger.error(f"❌ Failed to publish message to topic '{topic}': {e}") + self._log_system_message(f"ERROR: Failed to publish message to topic '{topic}': {e}") + return False + + async def _validate_message_with_identify(self, message, sender_id): + """Validate message using identify protocol to get sender's public key. + + This should only be called for messages from OTHER peers that don't include + a public key in the message data. + """ + # Safety check: never try to identify ourselves + if sender_id == self.peer_id: + logger.debug(f"⏭️ Skipping identify for own peer ID {sender_id}") + return True + + if not self.headless_service: + logger.warning("No headless service available for identify protocol") + return True # Default to accepting message if no identify available + + try: + # Get peer info via identify protocol (this will cache it) + peer_info = await self.headless_service.get_cached_peer_info(sender_id) + + if peer_info and peer_info.get('public_key'): + logger.info(f"βœ… Retrieved public key for {sender_id} via identify protocol") + # Here you could add actual message signature validation + # For now, we just log that we got the public key + return True + else: + logger.warning(f"⚠️ Could not get public key for {sender_id} via identify protocol") + return True # Still accept message but log the issue + + except Exception as e: + logger.error(f"❌ Error validating message with identify: {e}") + return True # Default to accepting message on error + + async def _handle_topic_messages(self, topic_name: str, subscription): + """Handle incoming messages for any subscribed topic (including chat and discovery).""" + logger.debug(f"πŸ“¨ Starting message handler for topic: {topic_name}") + + try: + async for message in self._message_stream(subscription): + try: + # Handle messages in the same way as chat messages + raw_data = message.data.decode() + sender_id = base58.b58encode(message.from_id).decode() if message.from_id else "unknown" + + # Check if this is our own message + is_own_message = sender_id == self.peer_id + + # Only validate messages from other peers + if not is_own_message: + if not message.key: + logger.debug(f"πŸ” Message from {sender_id} has no public key, using identify protocol") + is_valid = await self._validate_message_with_identify(message, sender_id) + if not is_valid: + logger.warning(f"⚠️ Message validation failed for {sender_id}, skipping") + continue + else: + logger.debug(f"βœ… Message from {sender_id} includes public key") + else: + logger.debug(f"πŸ“ Processing own message from {sender_id} (no validation needed)") + + # Format sender nickname + if is_own_message: + sender_nick = f"{self.nickname}" + else: + sender_nick = sender_id[-8:] if len(sender_id) > 8 else sender_id + + actual_message = raw_data + + logger.info(f"πŸ“¨ Received message on topic '{topic_name}' from {sender_id} ({sender_nick}): {actual_message}") + + # Create ChatMessage object for handlers + chat_msg = ChatMessage( + message=actual_message, + sender_id=sender_id, + sender_nick=sender_nick, + topic=topic_name + ) + + # Call message handlers + for handler in self.message_handlers: + try: + await handler(chat_msg) + except Exception as e: + logger.error(f"❌ Error in message handler: {e}") + + # Default console output if no handlers + if not self.message_handlers: + print(f"[{topic_name}][{chat_msg.sender_nick}]: {chat_msg.message}") + + except Exception as e: + logger.error(f"❌ Error processing message on topic '{topic_name}': {e}") + + except Exception as e: + logger.error(f"❌ Error in message handler for topic '{topic_name}': {e}") + + async def _message_stream(self, subscription) -> AsyncIterator[Message]: + """Create an async iterator for subscription messages.""" + while self.running: + try: + message = await subscription.get() + yield message + except Exception as e: + logger.error(f"Error getting message from subscription: {e}") + await trio.sleep(1) # Avoid tight loop on error + + async def start_message_handlers(self): + """Start all message handler tasks.""" + self.running = True + + async with trio.open_nursery() as nursery: + # Store nursery reference for dynamic task spawning + self.nursery = nursery + + # Start background task to monitor for new topic subscriptions + nursery.start_soon(self._monitor_new_topics) + + async def _monitor_new_topics(self): + """Monitor for new topic subscriptions and start handlers for them.""" + while self.running: + try: + # Check if there are any new topics that need handlers + for topic_name, subscription in self.topic_handlers: + if topic_name not in self.active_topic_handlers: + logger.info(f"Starting message handler for topic: {topic_name}") + + # Use generic handler for all topics (including chat and discovery) + self.nursery.start_soon(self._handle_topic_messages, topic_name, subscription) + self.active_topic_handlers.add(topic_name) + + # Check periodically (every 0.5 seconds) + await trio.sleep(0.5) + + except Exception as e: + logger.error(f"Error in topic monitor: {e}") + await trio.sleep(1) + + def add_message_handler(self, handler): + """Add a custom message handler.""" + self.message_handlers.append(handler) + + def add_system_message_handler(self, handler): + """Add a custom system message handler.""" + self.system_message_handlers.append(handler) + + async def run_interactive(self): + """Run interactive chat mode.""" + print(f"\n=== Universal Connectivity Chat ===") + print(f"Nickname: {self.nickname}") + print(f"Peer ID: {self.peer_id}") + print(f"Type messages and press Enter to send. Type 'quit' to exit.") + print(f"Commands: /peers, /status, /multiaddr") + print() + + async with trio.open_nursery() as nursery: + # Start message handlers + nursery.start_soon(self.start_message_handlers) + + # Start input handler + nursery.start_soon(self._input_handler) + + async def _input_handler(self): + """Handle user input in interactive mode.""" + try: + while self.running: + try: + # Use trio's to_thread to avoid blocking the event loop + message = await trio.to_thread.run_sync(input) + + if message.lower() in ["quit", "exit", "q"]: + print("Goodbye!") + self.running = False + break + + # Handle special commands + elif message.strip() == "/peers": + peers = self.get_connected_peers() + if peers: + print(f"πŸ“‘ Connected peers ({len(peers)}):") + for peer in peers: + print(f" - {peer[:8]}...") + else: + print("πŸ“‘ No peers connected") + continue + + elif message.strip() == "/multiaddr": + print(f"\nπŸ“‹ Copy this multiaddress:") + print(f"{self.multiaddr}") + print() + continue + + elif message.strip() == "/status": + peer_count = self.get_peer_count() + subscribed_topics = ", ".join(sorted(self.get_subscribed_topics())) + print(f"πŸ“Š Status:") + print(f" - Multiaddr: {self.multiaddr}") + print(f" - Nickname: {self.nickname}") + print(f" - Connected peers: {peer_count}") + print(f" - Chat topic: {self.chat_topic}") + print(f" - Subscribed topics: {subscribed_topics}") + continue + + if message.strip(): + await self.publish_message(message) + + except EOFError: + print("\nGoodbye!") + self.running = False + break + except Exception as e: + logger.error(f"Error in input handler: {e}") + await trio.sleep(0.1) + + except Exception as e: + logger.error(f"Fatal error in input handler: {e}") + self.running = False + + async def stop(self): + """Stop the chat room.""" + self.running = False + logger.info("ChatRoom stopped") + + def get_connected_peers(self) -> Set[str]: + """Get list of connected peer IDs.""" + return set(str(peer_id) for peer_id in self.pubsub.peers.keys()) + + def get_peer_count(self) -> int: + """Get number of connected peers.""" + return len(self.pubsub.peers) + + def get_subscribed_topics(self) -> Set[str]: + """Get list of all subscribed topics.""" + return set(self.subscriptions.keys()) + + async def subscribe_to_topic(self, topic_name: str) -> bool: + """ + Subscribe to a new topic dynamically. + + Args: + topic_name: The name of the topic to subscribe to + + Returns: + True if subscription was successful, False otherwise + """ + try: + if topic_name in self.subscriptions: + logger.warning(f"Already subscribed to topic: {topic_name}") + return False + + logger.info(f"Subscribing to new topic: {topic_name}") + subscription = await self.pubsub.subscribe(topic_name) + self.subscriptions[topic_name] = subscription + logger.info(f"Successfully subscribed to topic: {topic_name}") + self._log_system_message(f"Subscribed to topic: {topic_name}") + + # Add to topic_handlers list - will be started in start_message_handlers + self.topic_handlers.append((topic_name, subscription)) + logger.info(f"Added handler for topic: {topic_name}") + + return True + + except Exception as e: + logger.error(f"Failed to subscribe to topic {topic_name}: {e}") + self._log_system_message(f"ERROR: Failed to subscribe to topic {topic_name}: {e}") + return False diff --git a/py-peer/headless.py b/py-peer/headless.py new file mode 100644 index 00000000..501207f0 --- /dev/null +++ b/py-peer/headless.py @@ -0,0 +1,901 @@ +""" +Headless Service for Universal Connectivity Python Peer + +This module provides a headless service that manages libp2p host, pubsub, and chat functionality +without any UI. It communicates with the UI through queues and events. +""" + +import logging +import random +import socket +import time +import traceback +import multiaddr +import janus +import trio +import trio_asyncio +import hashlib +from queue import Empty +from typing import List, Dict, Any, Set +from libp2p.discovery.bootstrap import BootstrapDiscovery +from libp2p.kad_dht.kad_dht import ( + DHTMode, + KadDHT, +) +from libp2p import new_host +from libp2p.crypto.rsa import create_new_key_pair +from libp2p.pubsub.gossipsub import GossipSub +from libp2p.pubsub.pubsub import Pubsub +from libp2p.tools.async_service.trio_service import background_trio_service +from libp2p.peer.peerinfo import info_from_p2p_addr +from libp2p.peer.peerinfo import PeerInfo +from libp2p.identity.identify.identify import identify_handler_for, parse_identify_response, ID as IDENTIFY_PROTOCOL_ID +from libp2p.utils.varint import read_length_prefixed_protobuf +from libp2p.peer.id import ID +from libp2p.custom_types import TProtocol +from libp2p.pubsub.gossipsub import PROTOCOL_ID, PROTOCOL_ID_V11 +from libp2p.protocol_muxer.exceptions import ( + MultiselectClientError, +) +from libp2p.host.exceptions import ( + StreamFailure, +) +from chatroom import ChatRoom, ChatMessage + +logger = logging.getLogger("headless") + +# Constants +DISCOVERY_SERVICE_TAG = "universal-connectivity" +PROTOCOL_ID_LIST = [PROTOCOL_ID, PROTOCOL_ID_V11] +DEFAULT_PORT = 9095 + +# Bootstrap nodes for peer discovery +BOOTSTRAP_PEERS = [ + # "/ip4/139.178.65.157/tcp/4001/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa", + # "/ip4/139.178.91.71/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", + # "/ip4/145.40.118.135/tcp/4001/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt" + # "/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", + # "/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa", + # "/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zp7ykQCj2gRNdrFeqQ1vG13rMb4sPS", + # "/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt", + # "/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ" + # "/ip4/0.0.0.0/tcp/52972/p2p/QmVZZrUGuyicD5eig2a5yhi2dLDH5uMS3mXfxnR6uYuFZz" + # "/ip4/127.0.0.1/tcp/9095/p2p/QmbXUUZ4LoDE59Hx9zjiH88S9YY77ft9b3pFtPsyH2xeZJ" +] + + +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 filter_compatible_peer_info(peer_info) -> bool: + """Filter peer info to check if it has compatible addresses (TCP + IPv4).""" + if not hasattr(peer_info, "addrs") or not peer_info.addrs: + return False + + for addr in peer_info.addrs: + addr_str = str(addr) + if "/tcp/" in addr_str and "/ip4/" in addr_str and "/quic" not in addr_str: + return True + return False + +async def maintain_connections(host) -> None: + """Maintain connections to ensure the host remains connected to healthy peers.""" + while True: + try: + connected_peers = host.get_connected_peers() + list_peers = host.get_peerstore().peers_with_addrs() + + if len(connected_peers) < 20: + logger.debug("Reconnecting to maintain peer connections...") + + # Find compatible peers + compatible_peers = [] + for peer_id in list_peers: + try: + peer_info = host.get_peerstore().peer_info(peer_id) + if filter_compatible_peer_info(peer_info): + compatible_peers.append(peer_id) + except Exception: + continue + + # Connect to random subset of compatible peers + if compatible_peers: + random_peers = random.sample( + compatible_peers, min(50, len(compatible_peers)) + ) + for peer_id in random_peers: + if peer_id not in connected_peers: + try: + with trio.move_on_after(5): + peer_info = host.get_peerstore().peer_info(peer_id) + await host.connect(peer_info) + logger.debug(f"Connected to peer: {peer_id}") + except Exception as e: + logger.debug(f"Failed to connect to {peer_id}: {e}") + + await trio.sleep(15) + except Exception as e: + logger.error(f"Error maintaining connections: {e}") + + +class HeadlessService: + """ + Headless service that manages libp2p components and provides data to UI through queues. + """ + + def __init__(self, nickname: str, port: int = 0, connect_addrs: List[str] = None, ui_mode: bool = False, strict_signing: bool = True, seed: int = None, topic: str = None): + self.nickname = nickname + self.port = port if port != 0 else find_free_port() + self.connect_addrs = connect_addrs or [] + self.ui_mode = ui_mode # Flag to control logging behavior + self.strict_signing = strict_signing # Flag to control message signing + self.seed = seed + self.topic = topic # Custom topic to use instead of default + + # libp2p components + self.host = None + self.pubsub = None + self.gossipsub = None + self.dht = None + self.chat_room = None + + # Service state + self.running = False + self.ready = False + self.full_multiaddr = None + + # Communication with UI + self.message_queue = None # UI receives messages from headless + self.system_queue = None # UI receives system messages from headless + self.outgoing_queue = None # UI sends messages to headless + self.topic_subscription_queue = None # UI sends topic subscription requests + self.peer_connection_queue = None # UI sends peer connection requests + + # Per-topic message storage + self.topic_messages = {} # {topic: [{'message': msg, 'timestamp': ts, 'read': bool}]} + self.topic_unread_counts = {} # {topic: int} + + # Peer information storage for identify protocol + self.peer_info_cache = {} # Store peer info retrieved through identify + + # Events for synchronization + self.ready_event = trio.Event() + self.stop_event = trio.Event() + + if not ui_mode: # Only log initialization if not in UI mode + logger.info(f"HeadlessService initialized - nickname: {nickname}, port: {self.port}, strict_signing: {strict_signing}") + + async def monitor_peers(self): + while True: + print("testing print") + logger.info("testing status") + logger.info(f"Connected peers are: len{self.host.get_connected_peers()}") + logger.info(f"peers in peer store are: len{self.host.get_peerstore().peers_with_addrs()}") + logger.info(f"peers in routing table are: len{self.dht.routing_table.get_peer_ids()}") + logger.info(f"peers in pubsub are: {len(self.pubsub.peers.keys())}") + await trio.sleep(5) + + async def start(self): + """Start the headless service.""" + logger.info("Starting headless service...") + + try: + # Create queues for communication with UI + logger.debug("Creating message queues...") + self.message_queue = janus.Queue() # Messages from headless to UI + self.system_queue = janus.Queue() # System messages from headless to UI + self.outgoing_queue = janus.Queue() # Messages from UI to headless + self.topic_subscription_queue = janus.Queue() # Topic subscription requests from UI + self.peer_connection_queue = janus.Queue() # Peer connection requests from UI + logger.debug("Message queues created successfully") + + # Enable trio-asyncio mode + async with trio_asyncio.open_loop(): + # Send initial system message to test queue inside trio context + await self._send_system_message("Headless service starting...") + await self._run_service() + + except Exception as e: + logger.error(f"Failed to start headless service: {e}") + logger.error(f"Traceback:\n{traceback.format_exc()}") + raise + + async def _run_service(self): + """Run the main service loop.""" + key_pair = create_new_key_pair() + + # Create listen address + listen_addr = multiaddr.Multiaddr(f"/ip4/0.0.0.0/tcp/{self.port}") + + # Create libp2p host WITHOUT bootstrap nodes initially + # We'll connect to bootstrap nodes after pubsub is running + self.host = new_host( + key_pair=key_pair + # bootstrap = BOOTSTRAP_PEERS + ) + + # Register identify protocol handler + logger.info("πŸ“‹ Registering identify protocol handler (raw protobuf format for go-libp2p compatibility)") + identify_handler = identify_handler_for(self.host, use_varint_format=True) + self.host.set_stream_handler(IDENTIFY_PROTOCOL_ID, identify_handler) + logger.info(f"βœ… Identify protocol handler registered for {IDENTIFY_PROTOCOL_ID} (raw format)") + + # Create DHT with random walk enabled + self.dht = KadDHT(self.host, DHTMode.SERVER, enable_random_walk=True) + logger.info("βœ… DHT created with random walk enabled") + + self.full_multiaddr = f"{listen_addr}/p2p/{self.host.get_id()}" + logger.info(f"Host created with PeerID: {self.host.get_id()}") + logger.info(f"Listening on: {listen_addr}") + logger.info(f"Full multiaddr: {self.full_multiaddr}") + + # Log GossipSub protocol configuration + logger.info(f"πŸ“‹ Configuring GossipSub with protocols: {PROTOCOL_ID_LIST}") + logger.info(f" Protocol 1: {PROTOCOL_ID}") + logger.info(f" Protocol 2: {PROTOCOL_ID_V11}") + + # Create GossipSub with optimized parameters (matching working pubsub.py) + self.gossipsub = GossipSub( + protocols=PROTOCOL_ID_LIST, + degree=3, + degree_low=2, + degree_high=4, + gossip_window=2, # Smaller window for faster gossip + gossip_history=5, # Keep more history + heartbeat_initial_delay=2.0, # Start heartbeats sooner + heartbeat_interval=5, # More frequent heartbeats for testing + ) + logger.info("βœ… GossipSub router created successfully") + + # Create PubSub + logger.info(f"πŸ” Creating PubSub with strict_signing={self.strict_signing}") + self.pubsub = Pubsub(self.host, self.gossipsub, strict_signing=self.strict_signing) + logger.info("βœ… PubSub service created successfully") + + # Start host and pubsub services + async with self.host.run(listen_addrs=[listen_addr]): + logger.info("πŸ“‘ Initializing PubSub, GossipSub, and DHT services...") + try: + async with background_trio_service(self.pubsub): + async with background_trio_service(self.gossipsub): + async with background_trio_service(self.dht): + logger.info("βœ… Pubsub, GossipSub, and DHT services started.") + await self.pubsub.wait_until_ready() + logger.info("βœ… Pubsub ready and operational.") + logger.info("βœ… DHT service started with random walk enabled.") + bootstrap = None + if BOOTSTRAP_PEERS: + bootstrap = BootstrapDiscovery(self.host.get_network(), BOOTSTRAP_PEERS) + await bootstrap.start() + # Setup connections and chat room + await self._setup_connections() + await self._setup_chat_room() + + # Setup connection event handlers for DHT + + # Mark service as ready + self.ready = True + self.ready_event.set() + logger.info("βœ… Headless service is ready") + + # Start message processing and wait for stop + async with trio.open_nursery() as nursery: + nursery.start_soon(self._process_messages) + nursery.start_soon(self._process_outgoing_messages) + nursery.start_soon(self._process_topic_subscriptions) + nursery.start_soon(self._process_peer_connections) + nursery.start_soon(self._wait_for_stop) + nursery.start_soon(self.monitor_peers) + nursery.start_soon(maintain_connections, self.host) + + except (MultiselectClientError, StreamFailure) as e: + logger.log(f"The protocol negotitaion failed: {e}") + pass + + async def _setup_connections(self): + """Setup connections to specified peers with detailed protocol logging.""" + if not self.connect_addrs: + return + + for addr_str in self.connect_addrs: + try: + logger.info(f"πŸ”— Attempting to connect to: {addr_str}") + maddr = multiaddr.Multiaddr(addr_str) + info = info_from_p2p_addr(maddr) + logger.info(f"πŸ”— Parsed peer info - ID: {info.peer_id}, Addrs: {info.addrs}") + + # Log connection attempt + logger.info(f"πŸ”— Initiating connection to peer: {info.peer_id}") + await self.host.connect(info) + logger.info(f"βœ… TCP connection established to peer: {info.peer_id}") + + # Wait for initial protocol negotiation + await trio.sleep(1) + + # Detailed protocol inspection + logger.info(f"πŸ” Starting protocol inspection for peer: {info.peer_id}") + await self._inspect_peer_protocols(info.peer_id) + + # Check connection status + try: + # In py-libp2p, we can check if peer is connected via the swarm + swarm = self.host.get_network() + if hasattr(swarm, 'connections') and info.peer_id in swarm.connections: + connections = [swarm.connections[info.peer_id]] + logger.info(f"πŸ“Š Active connections to peer {info.peer_id}: {len(connections)}") + else: + logger.info(f"πŸ“Š No direct connection info available for peer {info.peer_id}") + except Exception as conn_err: + logger.warning(f"⚠️ Could not check connection status: {conn_err}") + + # Wait for PubSub protocol negotiation + logger.info(f"⏳ Waiting for PubSub protocol negotiation...") + await trio.sleep(3) + + # Check final PubSub status + await self._check_pubsub_status(info.peer_id) + + await self._send_system_message(f"Connected to peer: {str(info.peer_id)[:8]}") + + except Exception as e: + logger.error(f"❌ Failed to connect to {addr_str}: {e}") + await self._send_system_message(f"Failed to connect to {addr_str}: {e}") + + async def _inspect_peer_protocols(self, peer_id): + """Inspect and log all protocols supported by a peer.""" + try: + logger.info(f"πŸ” Checking peerstore for peer: {peer_id}") + + # Get peer's protocols from peerstore (simplified approach) + peerstore = self.host.get_peerstore() + + # Check if we can access protocols - different py-libp2p versions have different APIs + try: + if hasattr(peerstore, 'get_protocols'): + protocols = peerstore.get_protocols(peer_id) + elif hasattr(peerstore, 'protocols'): + protocols = peerstore.protocols(peer_id) + else: + # Fallback - just log that we connected successfully + logger.info(f"βœ… Successfully connected to peer {peer_id}") + logger.info(f"πŸ” Protocol inspection not available in this py-libp2p version") + return + + if protocols: + logger.info(f"πŸ“‹ Peer {peer_id} supports {len(protocols)} protocols:") + for i, protocol in enumerate(protocols, 1): + logger.info(f" {i}: {protocol}") + if "meshsub" in str(protocol) or "gossipsub" in str(protocol): + logger.info(f" 🎯 Found PubSub protocol: {protocol}") + else: + logger.info(f"πŸ“‹ No protocols found for peer {peer_id} yet (may still be negotiating)") + + except Exception as proto_err: + logger.info(f"πŸ” Protocol details not accessible: {proto_err}") + logger.info(f"βœ… Peer {peer_id} connected successfully") + + except Exception as e: + logger.warning(f"⚠️ Error inspecting peer protocols: {e}") + logger.info(f"βœ… Peer {peer_id} connected successfully") + + async def _check_pubsub_status(self, peer_id): + """Check the PubSub connection status with a specific peer.""" + try: + logger.info(f"πŸ” Checking PubSub status for peer: {peer_id}") + + # Check if peer is in pubsub.peers + pubsub_peers = list(self.pubsub.peers.keys()) + logger.info(f"πŸ“‘ Total PubSub peers: {len(pubsub_peers)}") + for i, p in enumerate(pubsub_peers, 1): + logger.info(f" PubSub peer {i}: {p}") + + if peer_id in self.pubsub.peers: + logger.info(f"βœ… Peer {peer_id} is in PubSub mesh") + + # Check GossipSub specific status + if hasattr(self.pubsub, 'router') and hasattr(self.pubsub.router, 'mesh'): + mesh = self.pubsub.router.mesh + logger.info(f"πŸ•ΈοΈ GossipSub mesh status:") + logger.info(f" Mesh topics: {list(mesh.keys())}") + for topic, topic_peers in mesh.items(): + logger.info(f" Topic '{topic}': {len(topic_peers)} peers") + if peer_id in topic_peers: + logger.info(f" βœ… Peer {peer_id} is in mesh for topic '{topic}'") + else: + logger.warning(f" ❌ Peer {peer_id} is NOT in mesh for topic '{topic}'") + else: + logger.warning(f"❌ Peer {peer_id} is NOT in PubSub mesh") + logger.info("πŸ”§ Possible reasons:") + logger.info(" 1. PubSub protocol negotiation failed") + logger.info(" 2. Peer doesn't support compatible GossipSub version") + logger.info(" 3. Network issues preventing PubSub handshake") + + except Exception as e: + logger.error(f"❌ Error checking PubSub status: {e}") + + async def _setup_chat_room(self): + """Setup the chat room.""" + logger.info("Setting up chat room...") + + self.chat_room = await ChatRoom.join_chat_room( + host=self.host, + pubsub=self.pubsub, + nickname=self.nickname, + multiaddr=self.full_multiaddr, + headless_service=self, + topic=self.topic + ) + + # Add custom message handler to forward messages to UI + self.chat_room.add_message_handler(self._handle_chat_message) + + # Start message handlers + self.running = True + + logger.info(f"Chat room setup complete for '{self.nickname}'") + await self._send_system_message(f"Joined chat room as '{self.nickname}'") + + async def _handle_chat_message(self, message: ChatMessage): + """Handle incoming chat messages and store them per-topic.""" + try: + topic = message.topic or "default" + + # Initialize topic storage if needed + if topic not in self.topic_messages: + self.topic_messages[topic] = [] + self.topic_unread_counts[topic] = 0 + + # Store message with unread flag + message_data = { + 'type': 'chat_message', + 'message': message.message, + 'sender_nick': message.sender_nick, + 'sender_id': message.sender_id, + 'timestamp': message.timestamp, + 'topic': topic, + 'read': False # New messages are unread by default + } + + self.topic_messages[topic].append(message_data) + self.topic_unread_counts[topic] += 1 + + # Log in simplified format only if not in UI mode + if not self.ui_mode: + logger.info(f"[{topic}] {message.sender_nick}: {message.message}") + + # Still put message in queue for UI updates + await self.message_queue.async_q.put(message_data) + + except Exception as e: + logger.error(f"Error handling chat message: {e}") + logger.exception("Full traceback:") + + async def _send_system_message(self, message: str): + """Send system message to UI queue.""" + logger.debug(f"_send_system_message called with: {message}") + try: + if self.system_queue: + logger.debug(f"System queue available, sending message: {message}") + await self.system_queue.async_q.put({ + 'type': 'system_message', + 'message': message, + 'timestamp': trio.current_time() + }) + logger.debug(f"System message sent successfully: {message}") + else: + logger.warning(f"System queue not available, cannot send message: {message}") + except Exception as e: + logger.error(f"Error sending system message: {e}") + logger.exception("Full traceback:") + + async def _process_messages(self): + """Process messages from chat room.""" + try: + # Start chat room message handlers + await self.chat_room.start_message_handlers() + except Exception as e: + logger.error(f"Error in message processing: {e}") + + async def _process_outgoing_messages(self): + """Process outgoing messages from UI to chat room.""" + + while self.running: + try: + # Check for messages from UI (non-blocking) + try: + outgoing_data = self.outgoing_queue.sync_q.get_nowait() + if outgoing_data and 'message' in outgoing_data: + message = outgoing_data['message'] + topic = outgoing_data.get('topic') # Optional topic parameter + + # Send message through chat room + if self.chat_room and self.running: + if topic: + # Send to specific topic + success = await self.chat_room.publish_to_topic(topic, message) + if not self.ui_mode: + logger.info(f"{self.nickname} (you) to {topic}: {message}") + else: + # Send to default chat topic + await self.chat_room.publish_message(message) + if not self.ui_mode: + logger.info(f"{self.nickname} (you): {message}") + else: + logger.warning("Cannot send message: chat room not ready") + await self._send_system_message("Cannot send message: chat room not ready") + + except Empty: + # No message available, that's fine + await trio.sleep(0.1) # Brief pause to avoid busy loop + except Exception as e: + logger.error(f"Error processing outgoing message: {e}") + await trio.sleep(0.1) + + except Exception as e: + logger.error(f"Error in outgoing message processing: {e}") + await trio.sleep(0.1) + + async def _process_topic_subscriptions(self): + """Process topic subscription requests from UI.""" + + while self.running: + try: + # Check for subscription requests from UI (non-blocking) + try: + subscription_data = self.topic_subscription_queue.sync_q.get_nowait() + if subscription_data and 'topic' in subscription_data: + topic_name = subscription_data['topic'] + + # Subscribe to the topic through chat room + if self.chat_room and self.running: + success = await self.chat_room.subscribe_to_topic(topic_name) + if success: + logger.info(f"Successfully subscribed to topic: {topic_name}") + await self._send_system_message(f"Subscribed to topic: {topic_name}") + else: + logger.warning(f"Failed to subscribe to topic: {topic_name}") + await self._send_system_message(f"Failed to subscribe to topic: {topic_name}") + else: + logger.warning("Cannot subscribe to topic: chat room not ready") + await self._send_system_message("Cannot subscribe to topic: chat room not ready") + + except Empty: + # No request available, that's fine + await trio.sleep(0.1) # Brief pause to avoid busy loop + except Exception as e: + logger.error(f"Error processing topic subscription: {e}") + await trio.sleep(0.1) + + except Exception as e: + logger.error(f"Error in topic subscription processing: {e}") + await trio.sleep(0.1) + + async def _process_peer_connections(self): + """Process peer connection requests from UI.""" + + while self.running: + try: + # Check for connection requests from UI (non-blocking) + try: + multiaddr_str = self.peer_connection_queue.sync_q.get_nowait() + if multiaddr_str: + logger.info(f"Processing peer connection request: {multiaddr_str}") + + # Parse and connect to the peer + try: + # Parse the multiaddress + maddr = multiaddr.Multiaddr(multiaddr_str) + + # Try to get peer info from the multiaddress + peer_info = info_from_p2p_addr(maddr) + + if peer_info: + # Connect to the peer + logger.info(f"Attempting to connect to peer: {peer_info.peer_id}") + await self.host.connect(peer_info) + logger.info(f"βœ… Successfully connected to peer: {peer_info.peer_id}") + await self._send_system_message(f"Connected to peer: {peer_info.peer_id}") + else: + logger.error(f"Could not extract peer info from multiaddress: {multiaddr_str}") + await self._send_system_message(f"Invalid multiaddress format") + + except Exception as e: + logger.error(f"Failed to connect to peer {multiaddr_str}: {e}") + await self._send_system_message(f"Connection failed: {str(e)}") + + except Empty: + # No request available, that's fine + await trio.sleep(0.1) # Brief pause to avoid busy loop + except Exception as e: + logger.error(f"Error processing peer connection: {e}") + await trio.sleep(0.1) + + except Exception as e: + logger.error(f"Error in peer connection processing: {e}") + await trio.sleep(0.1) + + async def _wait_for_stop(self): + """Wait for stop signal.""" + await self.stop_event.wait() + logger.info("Stop signal received, shutting down...") + self.running = False + + def send_message(self, message: str): + """Send a message through the chat room (thread-safe).""" + if self.outgoing_queue and self.running: + try: + # Put message in outgoing queue (sync call, safe from UI thread) + self.outgoing_queue.sync_q.put({ + 'message': message, + 'timestamp': time.time() + }) + except Exception as e: + logger.error(f"Failed to queue message: {e}") + else: + logger.warning("Cannot send message: outgoing queue not ready or service not running") + + def send_message_to_topic(self, topic: str, message: str): + """Send a message to a specific topic (thread-safe).""" + if self.outgoing_queue and self.running: + try: + # Put message with topic in outgoing queue + self.outgoing_queue.sync_q.put({ + 'message': message, + 'topic': topic, + 'timestamp': time.time() + }) + except Exception as e: + logger.error(f"Failed to queue message to topic {topic}: {e}") + else: + logger.warning("Cannot send message: outgoing queue not ready or service not running") + + def get_connection_info(self) -> Dict[str, Any]: + """Get connection information for UI.""" + if not self.ready: + return {} + + return { + 'peer_id': str(self.host.get_id()), + 'nickname': self.nickname, + 'multiaddr': self.full_multiaddr, + 'connected_peers': self.chat_room.get_connected_peers() if self.chat_room else set(), + 'peer_count': self.chat_room.get_peer_count() if self.chat_room else 0 + } + + def get_subscribed_topics(self) -> Set[str]: + """Get list of all subscribed topics.""" + if not self.chat_room: + return set() + return self.chat_room.get_subscribed_topics() + + def subscribe_to_topic(self, topic_name: str) -> bool: + """ + Subscribe to a new topic (thread-safe wrapper). + + Args: + topic_name: The name of the topic to subscribe to + + Returns: + True if subscription request was queued, False otherwise + """ + if not self.chat_room or not self.running: + logger.warning("Cannot subscribe to topic: chat room not ready or service not running") + return False + + try: + # Put subscription request in queue (sync call, safe from UI thread) + self.topic_subscription_queue.sync_q.put({ + 'topic': topic_name, + 'timestamp': time.time() + }) + logger.info(f"Queued subscription request for topic: {topic_name}") + return True + + except Exception as e: + logger.error(f"Failed to queue topic subscription: {e}") + return False + + def connect_to_peer(self, multiaddr: str) -> bool: + """ + Connect to a peer using multiaddress (thread-safe wrapper). + + Args: + multiaddr: The multiaddress of the peer to connect to + + Returns: + True if connection request was queued, False otherwise + """ + if not self.host or not self.running: + logger.warning("Cannot connect to peer: host not ready or service not running") + return False + + try: + # Put connection request in queue (sync call, safe from UI thread) + self.peer_connection_queue.sync_q.put(multiaddr) + logger.info(f"Queued peer connection request: {multiaddr}") + return True + + except Exception as e: + logger.error(f"Failed to queue peer connection: {e}") + return False + + def get_message_queue(self): + """Get the message queue for UI.""" + return self.message_queue + + def get_system_queue(self): + """Get the system queue for UI.""" + return self.system_queue + + def get_topic_messages(self, topic: str) -> List[Dict[str, Any]]: + """ + Get all messages for a specific topic. + + Args: + topic: The topic name + + Returns: + List of message dictionaries + """ + return self.topic_messages.get(topic, []) + + def get_all_topics_with_info(self) -> Dict[str, Dict[str, Any]]: + """ + Get all subscribed topics with their message counts and unread status. + + Returns: + Dict mapping topic names to info dicts containing: + - unread_count: Number of unread messages + - total_count: Total number of messages + - last_message: Most recent message (if any) + """ + result = {} + subscribed_topics = self.get_subscribed_topics() + + for topic in subscribed_topics: + messages = self.topic_messages.get(topic, []) + unread_count = self.topic_unread_counts.get(topic, 0) + + info = { + 'unread_count': unread_count, + 'total_count': len(messages), + 'last_message': messages[-1] if messages else None + } + result[topic] = info + + return result + + def mark_topic_as_read(self, topic: str): + """ + Mark all messages in a topic as read. + + Args: + topic: The topic name + """ + if topic in self.topic_messages: + for message in self.topic_messages[topic]: + message['read'] = True + self.topic_unread_counts[topic] = 0 + logger.debug(f"Marked all messages in topic '{topic}' as read") + + def get_unread_count(self, topic: str) -> int: + """ + Get the count of unread messages for a topic. + + Args: + topic: The topic name + + Returns: + Number of unread messages + """ + return self.topic_unread_counts.get(topic, 0) + + def get_outgoing_queue(self): + """Get the outgoing queue for UI to send messages.""" + return self.outgoing_queue + + async def get_peer_info_via_identify(self, peer_id): + """Get peer information using official identify protocol implementation.""" + try: + logger.info(f"πŸ” Requesting identify info from peer: {peer_id}") + logger.info(f"peers in peer store are: {self.host.get_peerstore().peers_with_addrs()}") + logger.info(f"address of peer {peer_id} is {self.host.get_peerstore().peer_info(peer_id).addrs} ") + + # Create a stream to the peer for identify protocol - use tuple format as in example + stream = await self.host.new_stream(peer_id, (IDENTIFY_PROTOCOL_ID,)) + + try: + # Use official py-libp2p utilities to read the response + # Use raw protobuf format (use_varint_format=False) for go-libp2p compatibility + # go-libp2p uses the old/raw format, not the newer varint length-prefixed format + response_bytes = await read_length_prefixed_protobuf(stream, use_varint_format=True) + + if not response_bytes: + logger.warning(f"Empty identify response from peer: {peer_id}") + return None + + # Parse the identify response using official parser + identify_info = parse_identify_response(response_bytes) + + logger.info(f"βœ… Received identify info from {peer_id}") + logger.info(f" - Protocol Version: {identify_info.protocol_version}") + logger.info(f" - Agent Version: {identify_info.agent_version}") + logger.info(f" - Public Key: {len(identify_info.public_key)} bytes") + logger.info(f" - Listen Addresses: {len(identify_info.listen_addrs)} addresses") + logger.info(f" - Protocols: {len(identify_info.protocols)} protocols") + + # Store the peer info in our cache + self.peer_info_cache[str(peer_id)] = { + 'public_key': identify_info.public_key, + 'protocol_version': identify_info.protocol_version, + 'agent_version': identify_info.agent_version, + 'listen_addrs': identify_info.listen_addrs, + 'protocols': identify_info.protocols, + 'timestamp': time.time() + } + + return identify_info + + finally: + await stream.close() + + except Exception as e: + logger.error(f"❌ Failed to get identify info from peer {peer_id}: {e}") + return None + + async def get_cached_peer_info(self, peer_id: str): + """Get cached peer info, or fetch it if not available.""" + peer_id_str = str(peer_id) + + # Check if we have cached info + if peer_id_str in self.peer_info_cache: + cached_info = self.peer_info_cache[peer_id_str] + # Check if cache is not too old (5 minutes) + if time.time() - cached_info['timestamp'] < 300: + return cached_info + else: + logger.debug(f"Cached info for {peer_id_str} is stale, refreshing") + + # Fetch fresh info + try: + peer_id_obj = ID.from_base58(peer_id_str) if isinstance(peer_id, str) else peer_id + identify_info = await self.get_peer_info_via_identify(peer_id_obj) + + if identify_info: + return self.peer_info_cache[peer_id_str] + except Exception as e: + logger.error(f"❌ Failed to get peer info for {peer_id_str}: {e}") + + return None + + def get_public_key_for_peer(self, peer_id: str): + """Get public key for a peer (synchronous access to cache).""" + peer_id_str = str(peer_id) + if peer_id_str in self.peer_info_cache: + return self.peer_info_cache[peer_id_str]['public_key'] + return None + + async def stop(self): + """Stop the headless service.""" + logger.info("Stopping headless service...") + self.stop_event.set() + + if self.chat_room: + await self.chat_room.stop() + + # Close queues + if self.message_queue: + self.message_queue.close() + if self.system_queue: + self.system_queue.close() + if self.outgoing_queue: + self.outgoing_queue.close() + if self.topic_subscription_queue: + self.topic_subscription_queue.close() + if self.peer_connection_queue: + self.peer_connection_queue.close() + + logger.info("Headless service stopped") diff --git a/py-peer/hello.py b/py-peer/hello.py deleted file mode 100644 index 279c6898..00000000 --- a/py-peer/hello.py +++ /dev/null @@ -1,6 +0,0 @@ -def main(): - print("Hello from py-peer!") - - -if __name__ == "__main__": - main() diff --git a/py-peer/kivy_ui.py b/py-peer/kivy_ui.py new file mode 100644 index 00000000..6c9cc16e --- /dev/null +++ b/py-peer/kivy_ui.py @@ -0,0 +1,1007 @@ +""" +Kivy UI module for Universal Connectivity Python Peer + +This module provides a modern mobile-friendly UI using Kivy and KivyMD. +It works with the headless service and uses queues for communication. +Design inspired by WhatsApp/Telegram for a familiar chat experience. +""" + +import os +# Disable Kivy argument parsing to avoid conflicts with our app's arguments +os.environ['KIVY_NO_ARGS'] = '1' + +import logging +import time +import threading +from queue import Empty +from typing import Optional + +from kivy.app import App +from kivy.clock import Clock +from kivy.core.window import Window +from kivy.metrics import dp +from kivy.properties import StringProperty, NumericProperty +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.screenmanager import ScreenManager, Screen + +from kivymd.app import MDApp +from kivymd.uix.list import OneLineAvatarIconListItem, IconLeftWidget, IconRightWidget +from kivymd.uix.label import MDLabel +from kivymd.uix.textfield import MDTextField +from kivymd.uix.button import MDIconButton, MDFlatButton +from kivymd.uix.toolbar import MDTopAppBar +from kivymd.uix.scrollview import MDScrollView +from kivymd.uix.card import MDCard +from kivymd.uix.dialog import MDDialog +from kivymd.uix.navigationdrawer import MDNavigationDrawer, MDNavigationDrawerMenu + +logger = logging.getLogger("kivy_ui") + + +class MessageBubble(MDCard): + """A message bubble similar to WhatsApp/Telegram.""" + + def __init__(self, message: str, sender: str, is_self: bool = False, timestamp: str = "", **kwargs): + super().__init__(**kwargs) + + # Set bubble properties + self.orientation = 'vertical' + self.size_hint_y = None + self.height = dp(80) + self.padding = dp(10) + self.spacing = dp(5) + + # Set different colors for sent/received messages + if is_self: + self.md_bg_color = (0.85, 0.95, 0.85, 1) # Light green for sent + self.pos_hint = {'right': 0.98} + self.size_hint_x = 0.75 + else: + self.md_bg_color = (1, 1, 1, 1) # White for received + self.pos_hint = {'x': 0.02} + self.size_hint_x = 0.75 + + # Sender label (only for received messages) + if not is_self: + sender_label = MDLabel( + text=sender, + font_style='Caption', + theme_text_color='Secondary', + size_hint_y=None, + height=dp(15) + ) + self.add_widget(sender_label) + + # Message content + message_label = MDLabel( + text=message, + size_hint_y=None, + height=dp(40) + ) + self.add_widget(message_label) + + # Timestamp + time_label = MDLabel( + text=timestamp, + font_style='Caption', + theme_text_color='Hint', + size_hint_y=None, + height=dp(15), + halign='right' + ) + self.add_widget(time_label) + + +class ChatScreen(Screen): + """Chat screen for a specific topic conversation.""" + + def __init__(self, headless_service, **kwargs): + super().__init__(**kwargs) + self.headless_service = headless_service + self.message_queue = headless_service.get_message_queue() + self.system_queue = headless_service.get_system_queue() + self.connection_info = headless_service.get_connection_info() + self.current_topic = None # The topic this chat screen is currently showing + + # Main layout + layout = BoxLayout(orientation='vertical') + + # Top app bar + self.toolbar = MDTopAppBar( + title="Select a Topic", + left_action_items=[["arrow-left", lambda x: self.go_back()]], + right_action_items=[ + ["information", lambda x: self.show_info()] + ], + elevation=2 + ) + layout.add_widget(self.toolbar) + + # Messages container + self.messages_layout = BoxLayout( + orientation='vertical', + spacing=dp(10), + padding=dp(10), + size_hint_y=None + ) + self.messages_layout.bind(minimum_height=self.messages_layout.setter('height')) + + # Scroll view for messages + self.scroll = MDScrollView() + self.scroll.add_widget(self.messages_layout) + layout.add_widget(self.scroll) + + # Input area + input_layout = BoxLayout( + orientation='horizontal', + size_hint_y=None, + height=dp(60), + padding=dp(10), + spacing=dp(10) + ) + + # Text input + self.message_input = MDTextField( + hint_text="Select a topic first...", + multiline=False, + size_hint_x=0.85, + disabled=True + ) + self.message_input.bind(on_text_validate=self.send_message) + input_layout.add_widget(self.message_input) + + # Send button + self.send_btn = MDIconButton( + icon="send", + on_release=self.send_message, + disabled=True + ) + input_layout.add_widget(self.send_btn) + + layout.add_widget(input_layout) + + self.add_widget(layout) + + # Start queue checking + Clock.schedule_interval(self.check_queues, 0.1) + + def go_back(self): + """Go back to topics list.""" + self.manager.current = 'topics' + + def set_topic(self, topic: str): + """ + Set the topic for this chat screen and load its messages. + + Args: + topic: The topic name to display + """ + self.current_topic = topic + self.toolbar.title = f"# {topic}" + self.message_input.hint_text = f"Message in {topic}..." + self.message_input.disabled = False + self.send_btn.disabled = False + + # Mark topic as read + self.headless_service.mark_topic_as_read(topic) + + # Clear and reload messages + self.messages_layout.clear_widgets() + self.load_topic_messages() + + def load_topic_messages(self): + """Load all messages for the current topic.""" + if not self.current_topic: + return + + messages = self.headless_service.get_topic_messages(self.current_topic) + our_peer_id = self.connection_info.get('peer_id', '') + + for msg_data in messages: + sender_id = msg_data['sender_id'] + sender_nick = msg_data['sender_nick'] + message = msg_data['message'] + timestamp = time.strftime("%H:%M", time.localtime(msg_data['timestamp'])) + + is_self = (sender_id == our_peer_id or sender_id == "self") + self.add_message_bubble(message, sender_nick, is_self=is_self, timestamp=timestamp) + + def send_message(self, *args): + """Send a message to the current topic.""" + if not self.current_topic: + return + + message = self.message_input.text.strip() + + if not message: + return + + # Clear input + self.message_input.text = "" + + # Handle commands + if message.startswith("/"): + self.handle_command(message) + return + + # Send message through headless service + try: + self.headless_service.send_message_to_topic(self.current_topic, message) + logger.info(f"Sending message to topic {self.current_topic}: {message}") + + # Display message immediately as sent + timestamp = time.strftime("%H:%M") + self.add_message_bubble(message, "You", is_self=True, timestamp=timestamp) + + except Exception as e: + logger.error(f"Failed to send message: {e}") + self.show_system_message(f"Error: {e}") + + def handle_command(self, command: str): + """Handle special commands.""" + cmd = command.lower().strip() + + if cmd in ["/quit", "/exit"]: + MDApp.get_running_app().stop() + elif cmd == "/status": + self.show_info() + else: + self.show_system_message(f"Unknown command: {command}") + + def check_queues(self, dt): + """Check message queues for new messages for the current topic.""" + # Check message queue + try: + while True: + try: + message_data = self.message_queue.sync_q.get_nowait() + if message_data.get('type') == 'chat_message': + # Only show messages for the current topic + msg_topic = message_data.get('topic', 'default') + if msg_topic != self.current_topic: + # Message is for a different topic, skip it + continue + + sender_nick = message_data['sender_nick'] + sender_id = message_data['sender_id'] + msg = message_data['message'] + + # Don't display our own messages again + our_peer_id = self.connection_info.get('peer_id', '') + if sender_id != our_peer_id and sender_id != "self": + timestamp = time.strftime("%H:%M") + self.add_message_bubble(msg, sender_nick, is_self=False, timestamp=timestamp) + + except Empty: + break + except Exception as e: + logger.error(f"Error checking message queue: {e}") + + # Check system queue + try: + while True: + try: + system_data = self.system_queue.sync_q.get_nowait() + if system_data.get('type') == 'system_message': + self.show_system_message(system_data['message']) + except Empty: + break + except Exception as e: + logger.error(f"Error checking system queue: {e}") + + def add_message_bubble(self, message: str, sender: str, is_self: bool = False, timestamp: str = ""): + """Add a message bubble to the chat.""" + bubble = MessageBubble( + message=message, + sender=sender, + is_self=is_self, + timestamp=timestamp + ) + self.messages_layout.add_widget(bubble) + + def show_system_message(self, message: str): + """Show a system message.""" + timestamp = time.strftime("%H:%M") + + # Create a centered system message + system_card = MDCard( + orientation='vertical', + size_hint=(0.8, None), + height=dp(40), + pos_hint={'center_x': 0.5}, + md_bg_color=(0.95, 0.95, 0.95, 1), + padding=dp(10) + ) + + label = MDLabel( + text=f"[{timestamp}] {message}", + font_style='Caption', + theme_text_color='Secondary', + halign='center' + ) + system_card.add_widget(label) + self.messages_layout.add_widget(system_card) + + def show_peers(self, *args): + """Show connected peers dialog.""" + info = self.headless_service.get_connection_info() + peers = info.get('connected_peers', set()) + + if peers: + peer_list = "\n".join([f"β€’ {peer[:16]}..." for peer in sorted(peers)]) + content = f"Connected Peers ({len(peers)}):\n\n{peer_list}" + else: + content = "No peers connected yet." + + dialog = MDDialog( + title="Connected Peers", + text=content, + buttons=[ + MDFlatButton( + text="CLOSE", + on_release=lambda x: dialog.dismiss() + ) + ] + ) + dialog.open() + + def show_info(self, *args): + """Show connection info dialog with clickable text to copy.""" + info = self.headless_service.get_connection_info() + peer_id = info.get('peer_id', 'Unknown') + multiaddr = info.get('multiaddr', 'Unknown') + topics = self.headless_service.get_subscribed_topics() + topics_list = ", ".join(sorted(topics)) if topics else "None" + + # Create content layout + content = BoxLayout( + orientation='vertical', + spacing=dp(10), + padding=dp(10), + size_hint_y=None, + height=dp(320) + ) + + # Info text + info_label = MDLabel( + text=f"""Nickname: {info.get('nickname', 'Unknown')} +Connected Peers: {info.get('peer_count', 0)} +Subscribed Topics: {topics_list} +""", + size_hint_y=None, + height=dp(100) + ) + content.add_widget(info_label) + + # Peer ID section + peer_id_label = MDLabel( + text="Peer ID:", + font_style='Caption', + theme_text_color='Secondary', + size_hint_y=None, + height=dp(20) + ) + content.add_widget(peer_id_label) + + # Clickable Peer ID card + peer_id_card = MDCard( + orientation='vertical', + size_hint_y=None, + height=dp(50), + padding=dp(10), + md_bg_color=(0.9, 0.95, 1, 1), # Light blue tint + on_release=lambda x: self.copy_to_clipboard(peer_id, "Peer ID copied!") + ) + + peer_id_text = MDLabel( + text=peer_id, + font_style='Body2', + halign='left', + valign='middle', + size_hint_y=1 + ) + peer_id_card.add_widget(peer_id_text) + content.add_widget(peer_id_card) + + # Multiaddr section + multiaddr_label = MDLabel( + text="Multiaddr:", + font_style='Caption', + theme_text_color='Secondary', + size_hint_y=None, + height=dp(20) + ) + content.add_widget(multiaddr_label) + + # Clickable Multiaddr card + multiaddr_card = MDCard( + orientation='vertical', + size_hint_y=None, + height=dp(70), + padding=dp(10), + md_bg_color=(0.9, 0.95, 1, 1), # Light blue tint + on_release=lambda x: self.copy_to_clipboard(multiaddr, "Multiaddr copied!") + ) + + multiaddr_text = MDLabel( + text=multiaddr, + font_style='Body2', + halign='left', + valign='middle', + size_hint_y=1 + ) + multiaddr_card.add_widget(multiaddr_text) + content.add_widget(multiaddr_card) + + dialog = MDDialog( + title="Connection Status", + type="custom", + content_cls=content, + buttons=[ + MDFlatButton( + text="CLOSE", + on_release=lambda x: dialog.dismiss() + ) + ] + ) + dialog.open() + + def copy_to_clipboard(self, text, success_message): + """Copy text to clipboard and show confirmation.""" + try: + from kivy.core.clipboard import Clipboard + Clipboard.copy(text) + logger.info(f"Copied to clipboard: {text[:50]}...") + self.show_system_message(success_message) + except Exception as e: + logger.error(f"Failed to copy to clipboard: {e}") + self.show_system_message("Failed to copy to clipboard") + + def show_multiaddr(self, *args): + """Show multiaddr dialog with clickable text to copy.""" + info = self.headless_service.get_connection_info() + multiaddr = info.get('multiaddr', 'Unknown') + + # Create content layout + content = BoxLayout( + orientation='vertical', + spacing=dp(10), + padding=dp(10), + size_hint_y=None, + height=dp(100) + ) + + # Hint text + hint_label = MDLabel( + text="Tap multiaddr to copy:", + font_style='Caption', + theme_text_color='Secondary', + size_hint_y=None, + height=dp(20) + ) + content.add_widget(hint_label) + + # Clickable Multiaddr card + multiaddr_card = MDCard( + orientation='vertical', + size_hint_y=None, + height=dp(70), + padding=dp(10), + md_bg_color=(0.9, 0.95, 1, 1), # Light blue tint + on_release=lambda x: self.copy_to_clipboard(multiaddr, "Multiaddr copied!") + ) + + multiaddr_text = MDLabel( + text=multiaddr, + font_style='Body2', + halign='left', + valign='middle', + size_hint_y=1 + ) + multiaddr_card.add_widget(multiaddr_text) + content.add_widget(multiaddr_card) + + dialog = MDDialog( + title="My Multiaddress", + type="custom", + content_cls=content, + buttons=[ + MDFlatButton( + text="CLOSE", + on_release=lambda x: dialog.dismiss() + ) + ] + ) + dialog.open() + + +class PeersScreen(Screen): + """Screen showing list of connected peers.""" + + def __init__(self, headless_service, **kwargs): + super().__init__(**kwargs) + self.headless_service = headless_service + + layout = BoxLayout(orientation='vertical') + + # Top app bar + toolbar = MDTopAppBar( + title="Connected Peers", + left_action_items=[["arrow-left", lambda x: self.go_back()]], + elevation=2 + ) + layout.add_widget(toolbar) + + # Peers list + self.peers_layout = BoxLayout( + orientation='vertical', + spacing=dp(5), + padding=dp(10), + size_hint_y=None + ) + self.peers_layout.bind(minimum_height=self.peers_layout.setter('height')) + + scroll = MDScrollView() + scroll.add_widget(self.peers_layout) + layout.add_widget(scroll) + + self.add_widget(layout) + + # Update peers periodically + Clock.schedule_interval(self.update_peers, 1.0) + + def go_back(self): + """Go back to chat screen.""" + self.manager.current = 'chat' + + def update_peers(self, dt): + """Update the peers list.""" + self.peers_layout.clear_widgets() + + info = self.headless_service.get_connection_info() + peers = info.get('connected_peers', set()) + + if not peers: + label = MDLabel( + text="No peers connected", + halign='center', + theme_text_color='Hint' + ) + self.peers_layout.add_widget(label) + return + + for peer in sorted(peers): + peer_item = OneLineAvatarIconListItem( + text=f"{peer[:16]}...", + on_release=lambda x, p=peer: self.show_peer_info(p) + ) + peer_item.add_widget(IconLeftWidget(icon="account")) + self.peers_layout.add_widget(peer_item) + + def show_peer_info(self, peer_id): + """Show information about a specific peer.""" + dialog = MDDialog( + title="Peer Information", + text=f"Peer ID:\n{peer_id}", + buttons=[ + MDFlatButton( + text="CLOSE", + on_release=lambda x: dialog.dismiss() + ) + ] + ) + dialog.open() + + +class TopicsScreen(Screen): + """Main screen showing list of subscribed topics - WhatsApp style selector.""" + + def __init__(self, headless_service, **kwargs): + super().__init__(**kwargs) + self.headless_service = headless_service + self.new_topic_dialog = None + + layout = BoxLayout(orientation='vertical') + + # Top app bar + toolbar = MDTopAppBar( + title="Universal Chat", + right_action_items=[ + ["plus", lambda x: self.show_add_topic_dialog()], + ["connection", lambda x: self.show_connect_dialog()], + ["information", lambda x: self.show_app_info()] + ], + elevation=2 + ) + layout.add_widget(toolbar) + + # Topics list + self.topics_layout = BoxLayout( + orientation='vertical', + spacing=dp(5), + padding=dp(10), + size_hint_y=None + ) + self.topics_layout.bind(minimum_height=self.topics_layout.setter('height')) + + scroll = MDScrollView() + scroll.add_widget(self.topics_layout) + layout.add_widget(scroll) + + self.add_widget(layout) + + # Update topics periodically + Clock.schedule_interval(self.update_topics, 1.0) + + def go_back(self): + """Not used - Topics is the main screen now.""" + pass + + def update_topics(self, dt): + """Update the topics list with unread counts.""" + self.topics_layout.clear_widgets() + + # Get all topics with their info + topics_info = self.headless_service.get_all_topics_with_info() + + if not topics_info: + label = MDLabel( + text="No topics subscribed\nTap + to add a topic", + halign='center', + theme_text_color='Hint' + ) + self.topics_layout.add_widget(label) + return + + # Sort topics by unread count (most unread first), then alphabetically + sorted_topics = sorted( + topics_info.items(), + key=lambda x: (-x[1]['unread_count'], x[0]) + ) + + for topic, info in sorted_topics: + unread_count = info['unread_count'] + last_message = info.get('last_message') + + # Create topic item with two lines (topic name + last message preview) + from kivymd.uix.list import TwoLineAvatarIconListItem + + # Preview of last message + preview = "" + if last_message: + preview = last_message['message'][:50] + if len(last_message['message']) > 50: + preview += "..." + + topic_item = TwoLineAvatarIconListItem( + text=topic, + secondary_text=preview or "No messages yet", + on_release=lambda x, t=topic: self.open_topic_chat(t) + ) + topic_item.add_widget(IconLeftWidget(icon="pound")) + + # Add unread badge if there are unread messages + if unread_count > 0: + # Show unread count in the secondary text + unread_text = f" ({unread_count} unread)" + topic_item.secondary_text = (preview or "No messages yet") + unread_text + + self.topics_layout.add_widget(topic_item) + + def open_topic_chat(self, topic): + """Open the chat screen for a specific topic.""" + # Switch to chat screen + chat_screen = self.manager.get_screen('chat') + chat_screen.set_topic(topic) + self.manager.current = 'chat' + + def show_topic_info(self, topic): + """Show information about a specific topic.""" + info = self.headless_service.get_all_topics_with_info().get(topic, {}) + unread = info.get('unread_count', 0) + total = info.get('total_count', 0) + + dialog = MDDialog( + title=f"Topic: {topic}", + text=f"Total messages: {total}\nUnread messages: {unread}", + buttons=[ + MDFlatButton( + text="CLOSE", + on_release=lambda x: dialog.dismiss() + ), + MDFlatButton( + text="OPEN CHAT", + on_release=lambda x: (dialog.dismiss(), self.open_topic_chat(topic)) + ) + ] + ) + dialog.open() + + def show_add_topic_dialog(self): + """Show dialog to add a new topic.""" + # Text field for topic name + self.topic_input = MDTextField( + hint_text="Enter topic name", + size_hint_x=0.9, + pos_hint={'center_x': 0.5} + ) + + content = BoxLayout( + orientation='vertical', + spacing=dp(10), + padding=dp(20), + size_hint_y=None, + height=dp(100) + ) + content.add_widget(self.topic_input) + + self.new_topic_dialog = MDDialog( + title="Subscribe to New Topic", + type="custom", + content_cls=content, + buttons=[ + MDFlatButton( + text="CANCEL", + on_release=lambda x: self.new_topic_dialog.dismiss() + ), + MDFlatButton( + text="SUBSCRIBE", + on_release=self.add_topic + ) + ] + ) + self.new_topic_dialog.open() + + def show_app_info(self): + """Show app connection information with clickable text to copy.""" + info = self.headless_service.get_connection_info() + peer_id = info.get('peer_id', 'Unknown') + multiaddr = info.get('multiaddr', 'Unknown') + + # Create content layout + content = BoxLayout( + orientation='vertical', + spacing=dp(15), + padding=dp(10), + size_hint_y=None, + height=dp(200) + ) + + # Peer ID section + peer_id_label = MDLabel( + text="Peer ID:", + font_style='Caption', + theme_text_color='Secondary', + size_hint_y=None, + height=dp(20) + ) + content.add_widget(peer_id_label) + + # Clickable Peer ID card + peer_id_card = MDCard( + orientation='vertical', + size_hint_y=None, + height=dp(50), + padding=dp(10), + md_bg_color=(0.9, 0.95, 1, 1), # Light blue tint + on_release=lambda x: self.copy_to_clipboard(peer_id, "Peer ID copied!") + ) + + peer_id_text = MDLabel( + text=peer_id, + font_style='Body2', + halign='left', + valign='middle', + size_hint_y=1 + ) + peer_id_card.add_widget(peer_id_text) + content.add_widget(peer_id_card) + + # Multiaddr section + multiaddr_label = MDLabel( + text="Multiaddr:", + font_style='Caption', + theme_text_color='Secondary', + size_hint_y=None, + height=dp(20) + ) + content.add_widget(multiaddr_label) + + # Clickable Multiaddr card + multiaddr_card = MDCard( + orientation='vertical', + size_hint_y=None, + height=dp(70), + padding=dp(10), + md_bg_color=(0.9, 0.95, 1, 1), # Light blue tint + on_release=lambda x: self.copy_to_clipboard(multiaddr, "Multiaddr copied!") + ) + + multiaddr_text = MDLabel( + text=multiaddr, + font_style='Body2', + halign='left', + valign='middle', + size_hint_y=1 + ) + multiaddr_card.add_widget(multiaddr_text) + content.add_widget(multiaddr_card) + + dialog = MDDialog( + title="Connection Info", + type="custom", + content_cls=content, + buttons=[ + MDFlatButton( + text="CLOSE", + on_release=lambda x: dialog.dismiss() + ) + ] + ) + dialog.open() + + def copy_to_clipboard(self, text, success_message): + """Copy text to clipboard and show confirmation.""" + try: + from kivy.core.clipboard import Clipboard + Clipboard.copy(text) + logger.info(f"Copied to clipboard: {text[:50]}...") + self.show_status_dialog("Success", success_message) + except Exception as e: + logger.error(f"Failed to copy to clipboard: {e}") + self.show_status_dialog("Error", "Failed to copy to clipboard") + + def show_connect_dialog(self): + """Show dialog to connect to a peer.""" + # Text field for multiaddress + self.connect_input = MDTextField( + hint_text="Enter peer multiaddress", + helper_text="e.g., /ip4/127.0.0.1/tcp/9095/p2p/QmXXXXXXXXXX...", + helper_text_mode="persistent", + multiline=True, + size_hint_y=None, + height=dp(120) + ) + + content = BoxLayout( + orientation='vertical', + spacing=dp(10), + padding=dp(20), + size_hint_y=None, + height=dp(140) + ) + content.add_widget(self.connect_input) + + self.connect_dialog = MDDialog( + title="Connect to Peer", + type="custom", + content_cls=content, + buttons=[ + MDFlatButton( + text="CANCEL", + on_release=lambda x: self.connect_dialog.dismiss() + ), + MDFlatButton( + text="CONNECT", + on_release=self.connect_to_peer + ) + ] + ) + self.connect_dialog.open() + + def connect_to_peer(self, *args): + """Connect to a peer using the provided multiaddress.""" + multiaddr = self.connect_input.text.strip() + + if not multiaddr: + self.show_status_dialog("Error", "Please enter a multiaddress") + return + + # Close the dialog + if self.connect_dialog: + self.connect_dialog.dismiss() + + try: + # Call the headless service to connect + success = self.headless_service.connect_to_peer(multiaddr) + if success: + self.show_status_dialog("Success", f"Connection request sent!\n\n{multiaddr[:60]}...") + else: + self.show_status_dialog("Error", "Failed to queue connection request") + except Exception as e: + logger.error(f"Error connecting to peer: {e}") + self.show_status_dialog("Error", f"Connection failed: {str(e)}") + + def show_status_dialog(self, title, text): + """Show a status/error dialog.""" + dialog = MDDialog( + title=title, + text=text, + buttons=[ + MDFlatButton( + text="OK", + on_release=lambda x: dialog.dismiss() + ) + ] + ) + dialog.open() + + def add_topic(self, *args): + """Add a new topic subscription.""" + topic_name = self.topic_input.text.strip() + + if not topic_name: + return + + # Subscribe to the topic + success = self.headless_service.subscribe_to_topic(topic_name) + + if success: + logger.info(f"Successfully subscribed to topic: {topic_name}") + else: + logger.error(f"Failed to subscribe to topic: {topic_name}") + + # Close dialog + if self.new_topic_dialog: + self.new_topic_dialog.dismiss() + + # Update the topics list immediately + self.update_topics(0) + + +class ChatApp(MDApp): + """Main Kivy application for the chat.""" + + def __init__(self, headless_service, **kwargs): + super().__init__(**kwargs) + self.headless_service = headless_service + self.theme_cls.primary_palette = "Green" + self.theme_cls.theme_style = "Light" + + def build(self): + """Build the application.""" + # Screen manager + sm = ScreenManager() + + # Add screens + topics_screen = TopicsScreen(self.headless_service, name='topics') + chat_screen = ChatScreen(self.headless_service, name='chat') + peers_screen = PeersScreen(self.headless_service, name='peers') + + sm.add_widget(topics_screen) + sm.add_widget(chat_screen) + sm.add_widget(peers_screen) + + # Set initial screen to topics (main WhatsApp-style selector) + sm.current = 'topics' + + return sm + + def on_start(self): + """Called when the app starts.""" + logger.info("Kivy Chat UI started") + + def on_stop(self): + """Called when the app stops.""" + logger.info("Kivy Chat UI stopped") + # Cleanup if needed + return True + + +def run_kivy_ui(headless_service): + """ + Run the Kivy UI with the given headless service. + + Args: + headless_service: The HeadlessService instance to use for communication + """ + logger.info("Starting Kivy UI...") + + # Set window size for desktop (will be ignored on mobile) + Window.size = (400, 600) + + # Create and run app + app = ChatApp(headless_service) + app.run() + + logger.info("Kivy UI stopped") diff --git a/py-peer/main.py b/py-peer/main.py new file mode 100644 index 00000000..2b7f11b5 --- /dev/null +++ b/py-peer/main.py @@ -0,0 +1,423 @@ +#!/usr/bin/env python3 +""" +Universal Connectivity Python Peer - Modular Main Entry Point + +This is the main entry point for the Python implementation of the universal connectivity peer. +It handles argument parsing and coordinates between the headless service and UI components. +""" + +import argparse +import logging +import sys +import time +import traceback +import trio +import threading + +from headless import HeadlessService +from ui import ChatUI + +DEFAULT_SEED = "py-peer" + +# Configure logging +def setup_logging(ui_mode=False): + """Setup logging configuration based on whether UI is active.""" + handlers = [] + + # Only add console handler if not in UI mode + if not ui_mode: + handlers.append(logging.StreamHandler()) + + # If no handlers, add a null handler to prevent logging errors + if not handlers: + handlers.append(logging.NullHandler()) + + logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s - %(name)s - %(message)s", + handlers=handlers, + force=True # Force reconfiguration + ) + +logger = logging.getLogger("main") +logging.getLogger("headless").setLevel(logging.DEBUG) # Enable debug for headless service +logging.getLogger("chatroom").setLevel(logging.DEBUG) # Enable debug for chatroom +logging.getLogger("libp2p.transport").setLevel(logging.DEBUG) +logging.getLogger("libp2p.security").setLevel(logging.DEBUG) +logging.getLogger("libp2p.mux").setLevel(logging.DEBUG) +logging.getLogger("libp2p.stream").setLevel(logging.DEBUG) +logging.getLogger("libp2p.pubsub").setLevel(logging.DEBUG) +logging.getLogger("libp2p.discovery").setLevel(logging.DEBUG) +logging.getLogger("libp2p.kad_dht").setLevel(logging.DEBUG) + +def run_headless_in_thread(headless_service, ready_event): + """Run headless service in a separate thread.""" + def run_service(): + try: + trio.run(headless_service.start) + except Exception as e: + logger.error(f"Error in headless service thread: {e}") + logger.error(f"Traceback:\n{traceback.format_exc()}") + + # Start the service in a daemon thread + thread = threading.Thread(target=run_service, daemon=True) + thread.start() + + # Wait for the service to be ready + max_wait = 30 # Maximum wait time in seconds + waited = 0 + while not headless_service.ready and waited < max_wait: + time.sleep(0.1) + waited += 0.1 + + if not headless_service.ready: + raise RuntimeError("Headless service failed to start within timeout") + + logger.info("βœ… Headless service is ready in background thread") + return thread + + +async def main_async(args): + """Main async function.""" + logger.info("Starting Universal Connectivity Python Peer...") + + # Create nickname + nickname = args.nick or f"peer-{time.time():.0f}" + + # Create headless service + strict_signing = not args.no_strict_signing # Default True, False if --no-strict-signing is used + headless_service = HeadlessService( + nickname=nickname, + port=args.port, + connect_addrs=args.connect, + strict_signing=strict_signing, + seed=args.seed, + topic=args.topic + ) + + try: + if args.headless: + # Run in headless mode + logger.info("Starting headless service...") + await headless_service.start() + elif args.ui: + # Return service configuration for UI mode + return headless_service + else: + # Run with simple interactive mode + logger.info("Starting headless service in background...") + + async with trio.open_nursery() as nursery: + # Start headless service in background + nursery.start_soon(headless_service.start) + + # Wait for service to be ready + await headless_service.ready_event.wait() + logger.info("βœ… Headless service is ready, starting UI...") + + # Run simple interactive mode + await run_simple_interactive(headless_service) + + except Exception as e: + logger.error(f"Application error: {e}") + logger.error(f"Traceback:\n{traceback.format_exc()}") + await headless_service.stop() + raise + + return None + + +async def run_simple_interactive(headless_service): + """Run simple interactive mode.""" + connection_info = headless_service.get_connection_info() + + print(f"\n=== Universal Connectivity Chat ===") + print(f"Nickname: {connection_info.get('nickname', 'Unknown')}") + print(f"Peer ID: {connection_info.get('peer_id', 'Unknown')}") + print(f"Multiaddr: {connection_info.get('multiaddr', 'Unknown')}") + print(f"Type messages and press Enter to send. Type 'quit' to exit.") + print(f"Commands: /peers, /status, /multiaddr") + print() + + # Start background task to monitor message queues + async with trio.open_nursery() as nursery: + nursery.start_soon(monitor_message_queues, headless_service) + nursery.start_soon(handle_user_input, headless_service) + + +async def monitor_message_queues(headless_service): + """Monitor message queues and display incoming messages.""" + logger.debug("monitor_message_queues function started") + + message_queue = headless_service.get_message_queue() + system_queue = headless_service.get_system_queue() + + logger.debug(f"Message queue: {message_queue}") + logger.debug(f"System queue: {system_queue}") + + if not message_queue or not system_queue: + logger.warning("Message queues not available") + return + + logger.info("πŸ“‘ Starting message queue monitoring...") + + while True: + try: + # Check message queue + try: + message_data = message_queue.sync_q.get_nowait() + logger.info(f"πŸ“¨ Got message from queue: {message_data}") + + if message_data.get('type') == 'chat_message': + sender_nick = message_data['sender_nick'] + sender_id = message_data['sender_id'] + msg = message_data['message'] + + # Display incoming message + sender_short = sender_id[:8] if len(sender_id) > 8 else sender_id + print(f"[{sender_nick}({sender_short})]: {msg}") + + except: + pass # Empty queue is normal, no need to log + + # Check system queue + try: + system_data = system_queue.sync_q.get_nowait() + logger.info(f"πŸ“‘ Got system message from queue: {system_data}") + + if system_data.get('type') == 'system_message': + print(f"πŸ“‘ {system_data['message']}") + + except: + pass # Empty queue is normal, no need to log + + await trio.sleep(0.1) # Small delay to prevent busy waiting + + except Exception as e: + logger.error(f"Error monitoring message queues: {e}") + await trio.sleep(1) + + +async def handle_user_input(headless_service): + """Handle user input in interactive mode.""" + try: + while True: + message = await trio.to_thread.run_sync(input) + + if message.lower() in ["quit", "exit", "q"]: + print("Goodbye!") + break + + # Handle special commands + elif message.strip() == "/peers": + info = headless_service.get_connection_info() + peers = info.get('connected_peers', set()) + if peers: + print(f"πŸ“‘ Connected peers ({len(peers)}):") + for peer in peers: + print(f" - {peer[:8]}...") + else: + print("πŸ“‘ No peers connected") + continue + + elif message.strip() == "/multiaddr": + info = headless_service.get_connection_info() + print(f"\nπŸ“‹ Copy this multiaddress:") + print(f"{info.get('multiaddr', 'Unknown')}") + print() + continue + + elif message.strip() == "/status": + info = headless_service.get_connection_info() + print(f"πŸ“Š Status:") + print(f" - Multiaddr: {info.get('multiaddr', 'Unknown')}") + print(f" - Nickname: {info.get('nickname', 'Unknown')}") + print(f" - Connected peers: {info.get('peer_count', 0)}") + print(f" - Subscribed topics: chat, discovery") + continue + + if message.strip(): + # Send message through headless service + headless_service.send_message(message) + + except (EOFError, KeyboardInterrupt): + print("\nGoodbye!") + + await headless_service.stop() + + +def main(): + """Main entry point.""" + parser = argparse.ArgumentParser(description="Universal Connectivity Python Peer") + + parser.add_argument( + "--nick", + type=str, + help="Nickname to use for the chat" + ) + + parser.add_argument( + "--headless", + action="store_true", + help="Run without chat UI" + ) + + parser.add_argument( + "--ui", + action="store_true", + help="Use Textual TUI instead of simple interactive mode" + ) + + parser.add_argument( + "--kivy", + action="store_true", + help="Use Kivy UI (mobile-friendly interface)" + ) + + parser.add_argument( + "-c", "--connect", + action="append", + help="Address to connect to (can be used multiple times)", + default=[] + ) + + parser.add_argument( + "-p", "--port", + type=int, + help="Port to listen on", + default=0 + ) + + parser.add_argument( + "-v", "--verbose", + action="store_true", + help="Enable debug logging" + ) + + parser.add_argument( + "--no-strict-signing", + action="store_true", + help="Disable strict message signing (allows unsigned messages)" + ) + parser.add_argument( + "-s", + "--seed", + type=str, + default=DEFAULT_SEED, + help="seed for deterministic peer ID generation", + ) + parser.add_argument( + "-t", + "--topic", + type=str, + help="Custom topic to subscribe.", + ) + + args = parser.parse_args() + + # Default logging setup (will be reconfigured based on mode) + setup_logging(ui_mode=False) + + # Set debug level if verbose flag is provided + if args.verbose: + logger.setLevel(logging.DEBUG) + logging.getLogger("libp2p").setLevel(logging.DEBUG) + logging.getLogger("headless").setLevel(logging.DEBUG) + logger.debug("Debug logging enabled") + + try: + if args.kivy: + # Configure logging for Kivy mode (no console output) + setup_logging(ui_mode=True) + + # Special handling for Kivy mode + logger.info("Starting in Kivy mode...") + + # Create nickname + nickname = args.nick or f"peer-{time.time():.0f}" + + # Create headless service + strict_signing = not args.no_strict_signing # Default True, False if --no-strict-signing is used + headless_service = HeadlessService( + nickname=nickname, + port=args.port, + connect_addrs=args.connect, + strict_signing=strict_signing, + seed=args.seed, + topic=args.topic + ) + + # Start headless service in background thread + logger.info("Starting headless service in background thread...") + ready_event = threading.Event() + headless_thread = run_headless_in_thread(headless_service, ready_event) + + logger.info("Starting Kivy UI in main thread...") + + # Import kivy_ui here to avoid issues if kivy is not installed + try: + from kivy_ui import run_kivy_ui + except ImportError as e: + logger.error("Failed to import kivy_ui. Make sure Kivy and KivyMD are installed.") + logger.error(f"Error: {e}") + logger.error("Install with: pip install kivy kivymd") + sys.exit(1) + + # Run Kivy UI - this will block until UI exits + run_kivy_ui(headless_service) + + elif args.ui: + # Configure logging for UI mode (no console output) + setup_logging(ui_mode=True) + + # Special handling for UI mode + logger.info("Starting in UI mode...") + + # Create nickname + nickname = args.nick or f"peer-{time.time():.0f}" + + # Create headless service + strict_signing = not args.no_strict_signing # Default True, False if --no-strict-signing is used + headless_service = HeadlessService( + nickname=nickname, + port=args.port, + connect_addrs=args.connect, + strict_signing=strict_signing, + seed=args.seed, + topic=args.topic + ) + + # Start headless service in background thread + logger.info("Starting headless service in background thread...") + ready_event = threading.Event() + headless_thread = run_headless_in_thread(headless_service, ready_event) + + logger.info("Starting Textual UI in main thread...") + + # Create and run UI in main thread + ui = ChatUI( + headless_service=headless_service, + message_queue=headless_service.get_message_queue(), + system_queue=headless_service.get_system_queue() + ) + + # Run UI - this will block until UI exits + ui.run() + + else: + # Configure logging for non-UI mode (console output enabled) + setup_logging(ui_mode=False) + + # Run the main async function for other modes + trio.run(main_async, args) + + except KeyboardInterrupt: + logger.info("Application terminated by user") + except Exception as e: + logger.error(f"Application error: {e}") + logger.error(f"Traceback:\n{traceback.format_exc()}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/py-peer/pyproject.toml b/py-peer/pyproject.toml index 622657ca..53b8d0f6 100644 --- a/py-peer/pyproject.toml +++ b/py-peer/pyproject.toml @@ -4,22 +4,24 @@ 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" +dependencies = [ + "textual>=0.47.0", + "libp2p>=0.3.0", + "trio>=0.22.0", + "base58", + "protobuf", + "janus>=1.0.0", + "trio_asyncio", + "kivy>=2.3.0", + "kivymd>=1.2.0", ] -[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 -] +[project.scripts] +py-peer = "main:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["."] \ No newline at end of file diff --git a/py-peer/ui.py b/py-peer/ui.py new file mode 100644 index 00000000..48eb61ae --- /dev/null +++ b/py-peer/ui.py @@ -0,0 +1,323 @@ +""" +UI module for Universal Connectivity Python Peer + +This module provides a Text User Interface (TUI) using Textual for the chat application. +It works with the headless service and uses queues for communication. +""" + +import logging +import time +from typing import Optional +from queue import Empty + +from textual.app import App, ComposeResult +from textual.containers import Container, Horizontal +from textual.widgets import Input, Log, Static +from textual.reactive import reactive +from textual.binding import Binding + +logger = logging.getLogger("ui") + + +class ChatUI(App[None]): + """ + A Textual-based Text User Interface (TUI) that works with the headless service. + + The UI provides: + - A main chat message area (left side) + - A peers list panel (right side) + - A system messages area (bottom) + - An input field for typing messages + """ + + CSS = """ + #chat-container { + height: 3fr; + } + + #chat-messages { + border: solid $primary; + border-title-align: left; + height: 1fr; + margin: 1; + } + + #peers-list { + border: solid $primary; + border-title-align: left; + height: 1fr; + margin: 1; + width: 30%; + } + + #system-messages { + border: solid $primary; + border-title-align: left; + height: 2fr; + margin: 1; + } + + #input-container { + height: 3; + margin: 1; + } + + #message-input { + border: solid $primary; + } + + .system-message { + color: $accent; + } + + Log { + scrollbar-size: 0 0; + } + """ + + BINDINGS = [ + Binding("ctrl+c", "quit", "Quit", show=True), + Binding("ctrl+q", "quit", "Quit", show=False), + ] + + # Reactive attributes + peer_count = reactive(0) + + def __init__(self, headless_service, message_queue, system_queue): + super().__init__() + self.headless_service = headless_service + self.message_queue = message_queue + self.system_queue = system_queue + self.running = False + + # Get connection info + self.connection_info = self.headless_service.get_connection_info() + + # Widgets (will be set in compose) + self.chat_log: Optional[Log] = None + self.peers_log: Optional[Log] = None + self.system_log: Optional[Log] = None + self.message_input: Optional[Input] = None + + logger.info(f"ModularChatUI initialized for peer {self.connection_info.get('peer_id', 'Unknown')[:8]}...") + + def compose(self) -> ComposeResult: + """Create the UI layout.""" + + with Container(id="chat-container"): + with Horizontal(): + # Main chat messages area + yield Log( + id="chat-messages", + name="chat-messages", + highlight=True, + auto_scroll=True, + max_lines=1000, + ).add_class("chat-messages") + + # Peers list + yield Log( + id="peers-list", + name="peers-list", + highlight=True, + auto_scroll=False, + max_lines=100, + ).add_class("peers-list") + + # System messages area + yield Log( + id="system-messages", + name="system-messages", + highlight=True, + auto_scroll=True, + max_lines=200, + ).add_class("system-messages") + + # Input field + with Container(id="input-container"): + nickname = self.connection_info.get('nickname', 'Unknown') + yield Input( + placeholder=f"{nickname} > Type your message...", + id="message-input", + name="message-input", + ) + + def on_mount(self) -> None: + """Called when the app is mounted.""" + # Get widget references + self.chat_log = self.query_one("#chat-messages", Log) + self.peers_log = self.query_one("#peers-list", Log) + self.system_log = self.query_one("#system-messages", Log) + self.message_input = self.query_one("#message-input", Input) + + # Set titles + self.chat_log.border_title = "Room: universal-connectivity" + self.peers_log.border_title = "Peers" + self.system_log.border_title = "System" + + # Focus the input field + self.message_input.focus() + + # Start the UI + self.running = True + + # Display welcome message + self.display_system_message("Universal Connectivity Chat Started") + self.display_system_message(f"Nickname: {self.connection_info.get('nickname', 'Unknown')}") + self.display_system_message(f"Multiaddr: {self.connection_info.get('multiaddr', 'Unknown')}") + self.display_system_message("Commands: /quit, /peers, /status, /multiaddr") + + # Start background tasks + self.set_interval(1.0, self.refresh_peers) + self.set_interval(0.1, self._check_queues) + + logger.info("UI mounted and running") + + async def on_input_submitted(self, event: Input.Submitted) -> None: + """Handle input submission.""" + message = event.value.strip() + + if not message: + return + + # Clear the input + self.message_input.clear() + + # Handle commands + if message.startswith("/"): + await self._handle_command(message) + return + + # Send message through headless service + try: + self.headless_service.send_message(message) # Now synchronous + + except Exception as e: + logger.error(f"Failed to send message: {e}") + self.display_system_message(f"Error sending message: {e}") + + async def _handle_command(self, command: str) -> None: + """Handle special commands.""" + cmd = command.lower().strip() + + if cmd in ["/quit", "/exit", "/q"]: + self.display_system_message("Goodbye!") + self.exit() + + elif cmd == "/peers": + self.refresh_peers() + + elif cmd == "/status": + info = self.headless_service.get_connection_info() + self.display_system_message(f"Status:") + self.display_system_message(f" - Multiaddr: {info.get('multiaddr', 'Unknown')}") + self.display_system_message(f" - Nickname: {info.get('nickname', 'Unknown')}") + self.display_system_message(f" - Connected peers: {info.get('peer_count', 0)}") + self.display_system_message(f" - Subscribed topics: chat, discovery") + + elif cmd == "/multiaddr": + info = self.headless_service.get_connection_info() + self.display_system_message("Copy this multiaddress:") + self.display_system_message(f"{info.get('multiaddr', 'Unknown')}") + + else: + self.display_system_message(f"Unknown command: {command}") + + def _check_queues(self) -> None: + """Check queues for new messages.""" + if not self.running: + return + + # Check message queue + try: + while True: + try: + message_data = self.message_queue.sync_q.get_nowait() + if message_data.get('type') == 'chat_message': + self.display_chat_message( + message_data['message'], + message_data['sender_nick'], + message_data['sender_id'] + ) + except Empty: + break + except Exception as e: + logger.error(f"Error checking message queue: {e}") + + # Check system queue + try: + while True: + try: + system_data = self.system_queue.sync_q.get_nowait() + if system_data.get('type') == 'system_message': + self.display_system_message(system_data['message']) + except Empty: + break + except Exception as e: + logger.error(f"Error checking system queue: {e}") + + def display_chat_message(self, message: str, sender_nick: str, sender_id: str) -> None: + """Display a chat message.""" + if not self.chat_log: + return + + # Determine if it's our own message + our_peer_id = self.connection_info.get('peer_id', '') + is_self = sender_id == our_peer_id or sender_id == "self" + + # Format message + timestamp = time.strftime("%H:%M:%S") + sender_display = sender_nick if not is_self else f"{sender_nick} (You)" + + formatted_message = f"[{timestamp}] {sender_display}: {message}" + + self.chat_log.write_line(formatted_message) + + def display_system_message(self, message: str) -> None: + """Display a system message.""" + if not self.system_log: + return + + timestamp = time.strftime("%H:%M:%S") + formatted_message = f"[{timestamp}] {message}" + + self.system_log.write_line(formatted_message) + + def refresh_peers(self) -> None: + """Refresh the peers list.""" + if not self.peers_log: + return + + try: + info = self.headless_service.get_connection_info() + peers = info.get('connected_peers', set()) + peer_count = len(peers) + + # Update reactive peer count + self.peer_count = peer_count + + # Clear and update peers list + self.peers_log.clear() + self.peers_log.write_line(f"Connected: {peer_count}") + + if peers: + for peer in sorted(peers): + peer_short = peer[:8] if len(peer) > 8 else peer + self.peers_log.write_line(f" β€’ {peer_short}...") + else: + self.peers_log.write_line(" (No peers connected)") + + except Exception as e: + logger.error(f"Error refreshing peers: {e}") + + def action_quit(self) -> None: + """Handle quit action.""" + self.display_system_message("Goodbye!") + self.running = False + self.exit() + + def on_unmount(self) -> None: + """Called when the app is unmounted.""" + self.running = False + logger.info("UI unmounted") + diff --git a/py-peer/uv.lock b/py-peer/uv.lock index 099cfb91..97f33a3d 100644 --- a/py-peer/uv.lock +++ b/py-peer/uv.lock @@ -1,46 +1,438 @@ version = 1 +revision = 2 requires-python = ">=3.12" +[[package]] +name = "async-generator" +version = "1.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/b6/6fa6b3b598a03cba5e80f829e0dadbb49d7645f523d209b2fb7ea0bbb02a/async_generator-1.10.tar.gz", hash = "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144", size = 29870, upload-time = "2018-08-01T03:36:21.69Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/52/39d20e03abd0ac9159c162ec24b93fbcaa111e8400308f2465432495ca2b/async_generator-1.10-py3-none-any.whl", hash = "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b", size = 18857, upload-time = "2018-08-01T03:36:20.029Z" }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, +] + +[[package]] +name = "base58" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/45/8ae61209bb9015f516102fa559a2914178da1d5868428bd86a1b4421141d/base58-2.1.1.tar.gz", hash = "sha256:c5d0cb3f5b6e81e8e35da5754388ddcc6d0d14b6c6a132cb93d69ed580a7278c", size = 6528, upload-time = "2021-10-30T22:12:17.858Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/45/ec96b29162a402fc4c1c5512d114d7b3787b9d1c2ec241d9568b4816ee23/base58-2.1.1-py3-none-any.whl", hash = "sha256:11a36f4d3ce51dfc1043f3218591ac4eb1ceb172919cebe05b52a5bcc8d245c2", size = 5621, upload-time = "2021-10-30T22:12:16.658Z" }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, +] + +[[package]] +name = "coincurve" +version = "21.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/a2/f2a38eb05b747ed3e54e1be33be339d4a14c1f5cc6a6e2b342b5e8160d51/coincurve-21.0.0.tar.gz", hash = "sha256:8b37ce4265a82bebf0e796e21a769e56fdbf8420411ccbe3fafee4ed75b6a6e5", size = 128986, upload-time = "2025-03-08T15:31:24.266Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/61/a2d9e109f99b6f5e65e653ac998b0944c5b82c568ac142fcbb381a4803be/coincurve-21.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f60ad56113f08e8c540bb89f4f35f44d434311433195ffff22893ccfa335070c", size = 1391948, upload-time = "2025-03-08T15:30:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/24/5a/2da75ee00a722ef1fa068ada3bc34c564595ead86fef573434e2f0cb0a5c/coincurve-21.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1cb1cd19fb0be22e68ecb60ad950b41f18b9b02eebeffaac9391dc31f74f08f2", size = 1384958, upload-time = "2025-03-08T15:30:34.705Z" }, + { url = "https://files.pythonhosted.org/packages/dc/50/6bf0bf7e8a9a9dd419ecc1e479dcb9fbfe657029276ad703806a25a2bef2/coincurve-21.0.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05d7e255a697b3475d7ae7640d3bdef3d5bc98ce9ce08dd387f780696606c33b", size = 1606576, upload-time = "2025-03-08T15:30:36.796Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ab/9e89908fdd09ad522938085587aaa821b022f4def16c286c5580cfc85811/coincurve-21.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a366c314df7217e3357bb8c7d2cda540b0bce180705f7a0ce2d1d9e28f62ad4", size = 1613642, upload-time = "2025-03-08T15:30:38.416Z" }, + { url = "https://files.pythonhosted.org/packages/b7/75/050b6fd08978de85a7b480f0f220ab6a30967c0910119f3096a8dd40befc/coincurve-21.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b04778b75339c6e46deb9ae3bcfc2250fbe48d1324153e4310fc4996e135715", size = 1616974, upload-time = "2025-03-08T15:30:39.939Z" }, + { url = "https://files.pythonhosted.org/packages/d7/62/2740ba0cafebf45708633635fecadcbe582d7a3ed1ce8b4637921feceaf8/coincurve-21.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8efcbdcd50cc219989a2662e6c6552f455efc000a15dd6ab3ebf4f9b187f41a3", size = 1644133, upload-time = "2025-03-08T15:30:41.733Z" }, + { url = "https://files.pythonhosted.org/packages/94/14/1f27c3048c4084fa85ef65f42a4ca631f2b184336e6d9446fecec20e0a7f/coincurve-21.0.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6df44b4e3b7acdc1453ade52a52e3f8a5b53ecdd5a06bd200f1ec4b4e250f7d9", size = 1619918, upload-time = "2025-03-08T15:30:43.284Z" }, + { url = "https://files.pythonhosted.org/packages/ca/22/7ec3ec4c8e7764daa25767d6674cb5741ea2d9b39ff758e9918d22a4b49b/coincurve-21.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bcc0831f07cb75b91c35c13b1362e7b9dc76c376b27d01ff577bec52005e22a8", size = 1645797, upload-time = "2025-03-08T15:30:44.974Z" }, + { url = "https://files.pythonhosted.org/packages/fb/60/87982b7499943ab12605df7b14f6001fff331aca0881b260682461e2309d/coincurve-21.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:5dd7b66b83b143f3ad3861a68fc0279167a0bae44fe3931547400b7a200e90b1", size = 1329255, upload-time = "2025-03-08T15:30:46.4Z" }, + { url = "https://files.pythonhosted.org/packages/62/c0/65b60b371579570931daca8a3f67debfc1482908b8ed03432297274a27da/coincurve-21.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:78dbe439e8cb22389956a4f2f2312813b4bd0531a0b691d4f8e868c7b366555d", size = 1325973, upload-time = "2025-03-08T15:30:48.056Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/cce55adaec37a588eb24b67da8eb68926546458e12ed2c4c2a21deb93d4c/coincurve-21.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9df5ceb5de603b9caf270629996710cf5ed1d43346887bc3895a11258644b65b", size = 1391762, upload-time = "2025-03-08T15:30:49.586Z" }, + { url = "https://files.pythonhosted.org/packages/ca/7a/628a30281d246ce98aea56592e0c8e79b03a93ee8b85d688db3388130c2d/coincurve-21.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:154467858d23c48f9e5ab380433bc2625027b50617400e2984cc16f5799ab601", size = 1384921, upload-time = "2025-03-08T15:30:51.103Z" }, + { url = "https://files.pythonhosted.org/packages/61/cc/719c5da31e6ba07e438abcf962f7a365eb69a06a0621ca4f2a484f344e09/coincurve-21.0.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f57f07c44d14d939bed289cdeaba4acb986bba9f729a796b6a341eab1661eedc", size = 1606559, upload-time = "2025-03-08T15:30:53.218Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ee/dd14237013d732e7fc3248c0c33a1d36b88b5378dfa3e624a50a23fb6f19/coincurve-21.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fb03e3a388a93d31ed56a442bdec7983ea404490e21e12af76fb1dbf097082a", size = 1613684, upload-time = "2025-03-08T15:30:55.087Z" }, + { url = "https://files.pythonhosted.org/packages/f0/05/eaa7f36a03376ced1c19e0cb563341cc83fe48f5734b2effe8f16d0ee0ab/coincurve-21.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d09ba4fd9d26b00b06645fcd768c5ad44832a1fa847ebe8fb44970d3204c3cb7", size = 1617001, upload-time = "2025-03-08T15:30:57.036Z" }, + { url = "https://files.pythonhosted.org/packages/39/32/fc75f1dd914ac95eb2704425c7ca1a9f509f982e15d05e0ca895b9e6ea9c/coincurve-21.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1a1e7ee73bc1b3bcf14c7b0d1f44e6485785d3b53ef7b16173c36d3cefa57f93", size = 1643924, upload-time = "2025-03-08T15:30:58.737Z" }, + { url = "https://files.pythonhosted.org/packages/1a/4b/8c6e65b5755e26fc02077803879747615c1c327047328d1784bccb4ff4c3/coincurve-21.0.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ad05952b6edc593a874df61f1bc79db99d716ec48ba4302d699e14a419fe6f51", size = 1619964, upload-time = "2025-03-08T15:31:00.275Z" }, + { url = "https://files.pythonhosted.org/packages/64/bc/d0a743305ff9fa26e72b4c77b534d5958ec8030b3772555a7172a0c134e5/coincurve-21.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4d2bf350ced38b73db9efa1ff8fd16a67a1cb35abb2dda50d89661b531f03fd3", size = 1645526, upload-time = "2025-03-08T15:31:01.952Z" }, + { url = "https://files.pythonhosted.org/packages/9d/44/ab082e2dc8c9a45774f1bb9961f58b43c0882b866f5c469ead932d45a35d/coincurve-21.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:54d9500c56d5499375e579c3917472ffcf804c3584dd79052a79974280985c74", size = 1329285, upload-time = "2025-03-08T15:31:03.591Z" }, + { url = "https://files.pythonhosted.org/packages/f3/94/407f6fc811310f15b1fc7255f436f6a9040854213beeb10093f56b5b7fd3/coincurve-21.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:773917f075ec4b94a7a742637d303a3a082616a115c36568eb6c873a8d950d18", size = 1326027, upload-time = "2025-03-08T15:31:05.318Z" }, +] + [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cryptography" +version = "45.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/1e/49527ac611af559665f71cbb8f92b332b5ec9c6fbc4e88b0f8e92f5e85df/cryptography-45.0.5.tar.gz", hash = "sha256:72e76caa004ab63accdf26023fccd1d087f6d90ec6048ff33ad0445abf7f605a", size = 744903, upload-time = "2025-07-02T13:06:25.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/fb/09e28bc0c46d2c547085e60897fea96310574c70fb21cd58a730a45f3403/cryptography-45.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:101ee65078f6dd3e5a028d4f19c07ffa4dd22cce6a20eaa160f8b5219911e7d8", size = 7043092, upload-time = "2025-07-02T13:05:01.514Z" }, + { url = "https://files.pythonhosted.org/packages/b1/05/2194432935e29b91fb649f6149c1a4f9e6d3d9fc880919f4ad1bcc22641e/cryptography-45.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3a264aae5f7fbb089dbc01e0242d3b67dffe3e6292e1f5182122bdf58e65215d", size = 4205926, upload-time = "2025-07-02T13:05:04.741Z" }, + { url = "https://files.pythonhosted.org/packages/07/8b/9ef5da82350175e32de245646b1884fc01124f53eb31164c77f95a08d682/cryptography-45.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e74d30ec9c7cb2f404af331d5b4099a9b322a8a6b25c4632755c8757345baac5", size = 4429235, upload-time = "2025-07-02T13:05:07.084Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e1/c809f398adde1994ee53438912192d92a1d0fc0f2d7582659d9ef4c28b0c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3af26738f2db354aafe492fb3869e955b12b2ef2e16908c8b9cb928128d42c57", size = 4209785, upload-time = "2025-07-02T13:05:09.321Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8b/07eb6bd5acff58406c5e806eff34a124936f41a4fb52909ffa4d00815f8c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e6c00130ed423201c5bc5544c23359141660b07999ad82e34e7bb8f882bb78e0", size = 3893050, upload-time = "2025-07-02T13:05:11.069Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ef/3333295ed58d900a13c92806b67e62f27876845a9a908c939f040887cca9/cryptography-45.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:dd420e577921c8c2d31289536c386aaa30140b473835e97f83bc71ea9d2baf2d", size = 4457379, upload-time = "2025-07-02T13:05:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9d/44080674dee514dbb82b21d6fa5d1055368f208304e2ab1828d85c9de8f4/cryptography-45.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d05a38884db2ba215218745f0781775806bde4f32e07b135348355fe8e4991d9", size = 4209355, upload-time = "2025-07-02T13:05:15.017Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d8/0749f7d39f53f8258e5c18a93131919ac465ee1f9dccaf1b3f420235e0b5/cryptography-45.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ad0caded895a00261a5b4aa9af828baede54638754b51955a0ac75576b831b27", size = 4456087, upload-time = "2025-07-02T13:05:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/09/d7/92acac187387bf08902b0bf0699816f08553927bdd6ba3654da0010289b4/cryptography-45.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9024beb59aca9d31d36fcdc1604dd9bbeed0a55bface9f1908df19178e2f116e", size = 4332873, upload-time = "2025-07-02T13:05:18.743Z" }, + { url = "https://files.pythonhosted.org/packages/03/c2/840e0710da5106a7c3d4153c7215b2736151bba60bf4491bdb421df5056d/cryptography-45.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91098f02ca81579c85f66df8a588c78f331ca19089763d733e34ad359f474174", size = 4564651, upload-time = "2025-07-02T13:05:21.382Z" }, + { url = "https://files.pythonhosted.org/packages/2e/92/cc723dd6d71e9747a887b94eb3827825c6c24b9e6ce2bb33b847d31d5eaa/cryptography-45.0.5-cp311-abi3-win32.whl", hash = "sha256:926c3ea71a6043921050eaa639137e13dbe7b4ab25800932a8498364fc1abec9", size = 2929050, upload-time = "2025-07-02T13:05:23.39Z" }, + { url = "https://files.pythonhosted.org/packages/1f/10/197da38a5911a48dd5389c043de4aec4b3c94cb836299b01253940788d78/cryptography-45.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:b85980d1e345fe769cfc57c57db2b59cff5464ee0c045d52c0df087e926fbe63", size = 3403224, upload-time = "2025-07-02T13:05:25.202Z" }, + { url = "https://files.pythonhosted.org/packages/fe/2b/160ce8c2765e7a481ce57d55eba1546148583e7b6f85514472b1d151711d/cryptography-45.0.5-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3562c2f23c612f2e4a6964a61d942f891d29ee320edb62ff48ffb99f3de9ae8", size = 7017143, upload-time = "2025-07-02T13:05:27.229Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e7/2187be2f871c0221a81f55ee3105d3cf3e273c0a0853651d7011eada0d7e/cryptography-45.0.5-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3fcfbefc4a7f332dece7272a88e410f611e79458fab97b5efe14e54fe476f4fd", size = 4197780, upload-time = "2025-07-02T13:05:29.299Z" }, + { url = "https://files.pythonhosted.org/packages/b9/cf/84210c447c06104e6be9122661159ad4ce7a8190011669afceeaea150524/cryptography-45.0.5-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:460f8c39ba66af7db0545a8c6f2eabcbc5a5528fc1cf6c3fa9a1e44cec33385e", size = 4420091, upload-time = "2025-07-02T13:05:31.221Z" }, + { url = "https://files.pythonhosted.org/packages/3e/6a/cb8b5c8bb82fafffa23aeff8d3a39822593cee6e2f16c5ca5c2ecca344f7/cryptography-45.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9b4cf6318915dccfe218e69bbec417fdd7c7185aa7aab139a2c0beb7468c89f0", size = 4198711, upload-time = "2025-07-02T13:05:33.062Z" }, + { url = "https://files.pythonhosted.org/packages/04/f7/36d2d69df69c94cbb2473871926daf0f01ad8e00fe3986ac3c1e8c4ca4b3/cryptography-45.0.5-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2089cc8f70a6e454601525e5bf2779e665d7865af002a5dec8d14e561002e135", size = 3883299, upload-time = "2025-07-02T13:05:34.94Z" }, + { url = "https://files.pythonhosted.org/packages/82/c7/f0ea40f016de72f81288e9fe8d1f6748036cb5ba6118774317a3ffc6022d/cryptography-45.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0027d566d65a38497bc37e0dd7c2f8ceda73597d2ac9ba93810204f56f52ebc7", size = 4450558, upload-time = "2025-07-02T13:05:37.288Z" }, + { url = "https://files.pythonhosted.org/packages/06/ae/94b504dc1a3cdf642d710407c62e86296f7da9e66f27ab12a1ee6fdf005b/cryptography-45.0.5-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:be97d3a19c16a9be00edf79dca949c8fa7eff621763666a145f9f9535a5d7f42", size = 4198020, upload-time = "2025-07-02T13:05:39.102Z" }, + { url = "https://files.pythonhosted.org/packages/05/2b/aaf0adb845d5dabb43480f18f7ca72e94f92c280aa983ddbd0bcd6ecd037/cryptography-45.0.5-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:7760c1c2e1a7084153a0f68fab76e754083b126a47d0117c9ed15e69e2103492", size = 4449759, upload-time = "2025-07-02T13:05:41.398Z" }, + { url = "https://files.pythonhosted.org/packages/91/e4/f17e02066de63e0100a3a01b56f8f1016973a1d67551beaf585157a86b3f/cryptography-45.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ff8728d8d890b3dda5765276d1bc6fb099252915a2cd3aff960c4c195745dd0", size = 4319991, upload-time = "2025-07-02T13:05:43.64Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2e/e2dbd629481b499b14516eed933f3276eb3239f7cee2dcfa4ee6b44d4711/cryptography-45.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7259038202a47fdecee7e62e0fd0b0738b6daa335354396c6ddebdbe1206af2a", size = 4554189, upload-time = "2025-07-02T13:05:46.045Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ea/a78a0c38f4c8736287b71c2ea3799d173d5ce778c7d6e3c163a95a05ad2a/cryptography-45.0.5-cp37-abi3-win32.whl", hash = "sha256:1e1da5accc0c750056c556a93c3e9cb828970206c68867712ca5805e46dc806f", size = 2911769, upload-time = "2025-07-02T13:05:48.329Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/28ac139109d9005ad3f6b6f8976ffede6706a6478e21c889ce36c840918e/cryptography-45.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:90cb0a7bb35959f37e23303b7eed0a32280510030daba3f7fdfbb65defde6a97", size = 3390016, upload-time = "2025-07-02T13:05:50.811Z" }, +] + +[[package]] +name = "fastecdsa" +version = "1.7.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/9a/dbc950929ba56731588c68f34e5a7a1f507eb51f2a59d943020ec8ba399a/fastecdsa-1.7.5.tar.gz", hash = "sha256:bd3b7808cc2bea1e8b3c4dd5928ecfc14072403ab2a47580a7a8350800e6fedd", size = 40426, upload-time = "2019-11-05T06:14:45.006Z" } + +[[package]] +name = "grpcio" +version = "1.73.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/e8/b43b851537da2e2f03fa8be1aef207e5cbfb1a2e014fbb6b40d24c177cd3/grpcio-1.73.1.tar.gz", hash = "sha256:7fce2cd1c0c1116cf3850564ebfc3264fba75d3c74a7414373f1238ea365ef87", size = 12730355, upload-time = "2025-06-26T01:53:24.622Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/41/456caf570c55d5ac26f4c1f2db1f2ac1467d5bf3bcd660cba3e0a25b195f/grpcio-1.73.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:921b25618b084e75d424a9f8e6403bfeb7abef074bb6c3174701e0f2542debcf", size = 5334621, upload-time = "2025-06-26T01:52:23.602Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c2/9a15e179e49f235bb5e63b01590658c03747a43c9775e20c4e13ca04f4c4/grpcio-1.73.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:277b426a0ed341e8447fbf6c1d6b68c952adddf585ea4685aa563de0f03df887", size = 10601131, upload-time = "2025-06-26T01:52:25.691Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1d/1d39e90ef6348a0964caa7c5c4d05f3bae2c51ab429eb7d2e21198ac9b6d/grpcio-1.73.1-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:96c112333309493c10e118d92f04594f9055774757f5d101b39f8150f8c25582", size = 5759268, upload-time = "2025-06-26T01:52:27.631Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2b/2dfe9ae43de75616177bc576df4c36d6401e0959833b2e5b2d58d50c1f6b/grpcio-1.73.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f48e862aed925ae987eb7084409a80985de75243389dc9d9c271dd711e589918", size = 6409791, upload-time = "2025-06-26T01:52:29.711Z" }, + { url = "https://files.pythonhosted.org/packages/6e/66/e8fe779b23b5a26d1b6949e5c70bc0a5fd08f61a6ec5ac7760d589229511/grpcio-1.73.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83a6c2cce218e28f5040429835fa34a29319071079e3169f9543c3fbeff166d2", size = 6003728, upload-time = "2025-06-26T01:52:31.352Z" }, + { url = "https://files.pythonhosted.org/packages/a9/39/57a18fcef567784108c4fc3f5441cb9938ae5a51378505aafe81e8e15ecc/grpcio-1.73.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:65b0458a10b100d815a8426b1442bd17001fdb77ea13665b2f7dc9e8587fdc6b", size = 6103364, upload-time = "2025-06-26T01:52:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/c5/46/28919d2aa038712fc399d02fa83e998abd8c1f46c2680c5689deca06d1b2/grpcio-1.73.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0a9f3ea8dce9eae9d7cb36827200133a72b37a63896e0e61a9d5ec7d61a59ab1", size = 6749194, upload-time = "2025-06-26T01:52:34.734Z" }, + { url = "https://files.pythonhosted.org/packages/3d/56/3898526f1fad588c5d19a29ea0a3a4996fb4fa7d7c02dc1be0c9fd188b62/grpcio-1.73.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:de18769aea47f18e782bf6819a37c1c528914bfd5683b8782b9da356506190c8", size = 6283902, upload-time = "2025-06-26T01:52:36.503Z" }, + { url = "https://files.pythonhosted.org/packages/dc/64/18b77b89c5870d8ea91818feb0c3ffb5b31b48d1b0ee3e0f0d539730fea3/grpcio-1.73.1-cp312-cp312-win32.whl", hash = "sha256:24e06a5319e33041e322d32c62b1e728f18ab8c9dbc91729a3d9f9e3ed336642", size = 3668687, upload-time = "2025-06-26T01:52:38.678Z" }, + { url = "https://files.pythonhosted.org/packages/3c/52/302448ca6e52f2a77166b2e2ed75f5d08feca4f2145faf75cb768cccb25b/grpcio-1.73.1-cp312-cp312-win_amd64.whl", hash = "sha256:303c8135d8ab176f8038c14cc10d698ae1db9c480f2b2823f7a987aa2a4c5646", size = 4334887, upload-time = "2025-06-26T01:52:40.743Z" }, + { url = "https://files.pythonhosted.org/packages/37/bf/4ca20d1acbefabcaba633ab17f4244cbbe8eca877df01517207bd6655914/grpcio-1.73.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:b310824ab5092cf74750ebd8a8a8981c1810cb2b363210e70d06ef37ad80d4f9", size = 5335615, upload-time = "2025-06-26T01:52:42.896Z" }, + { url = "https://files.pythonhosted.org/packages/75/ed/45c345f284abec5d4f6d77cbca9c52c39b554397eb7de7d2fcf440bcd049/grpcio-1.73.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:8f5a6df3fba31a3485096ac85b2e34b9666ffb0590df0cd044f58694e6a1f6b5", size = 10595497, upload-time = "2025-06-26T01:52:44.695Z" }, + { url = "https://files.pythonhosted.org/packages/a4/75/bff2c2728018f546d812b755455014bc718f8cdcbf5c84f1f6e5494443a8/grpcio-1.73.1-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:052e28fe9c41357da42250a91926a3e2f74c046575c070b69659467ca5aa976b", size = 5765321, upload-time = "2025-06-26T01:52:46.871Z" }, + { url = "https://files.pythonhosted.org/packages/70/3b/14e43158d3b81a38251b1d231dfb45a9b492d872102a919fbf7ba4ac20cd/grpcio-1.73.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c0bf15f629b1497436596b1cbddddfa3234273490229ca29561209778ebe182", size = 6415436, upload-time = "2025-06-26T01:52:49.134Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3f/81d9650ca40b54338336fd360f36773be8cb6c07c036e751d8996eb96598/grpcio-1.73.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ab860d5bfa788c5a021fba264802e2593688cd965d1374d31d2b1a34cacd854", size = 6007012, upload-time = "2025-06-26T01:52:51.076Z" }, + { url = "https://files.pythonhosted.org/packages/55/f4/59edf5af68d684d0f4f7ad9462a418ac517201c238551529098c9aa28cb0/grpcio-1.73.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:ad1d958c31cc91ab050bd8a91355480b8e0683e21176522bacea225ce51163f2", size = 6105209, upload-time = "2025-06-26T01:52:52.773Z" }, + { url = "https://files.pythonhosted.org/packages/e4/a8/700d034d5d0786a5ba14bfa9ce974ed4c976936c2748c2bd87aa50f69b36/grpcio-1.73.1-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:f43ffb3bd415c57224c7427bfb9e6c46a0b6e998754bfa0d00f408e1873dcbb5", size = 6753655, upload-time = "2025-06-26T01:52:55.064Z" }, + { url = "https://files.pythonhosted.org/packages/1f/29/efbd4ac837c23bc48e34bbaf32bd429f0dc9ad7f80721cdb4622144c118c/grpcio-1.73.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:686231cdd03a8a8055f798b2b54b19428cdf18fa1549bee92249b43607c42668", size = 6287288, upload-time = "2025-06-26T01:52:57.33Z" }, + { url = "https://files.pythonhosted.org/packages/d8/61/c6045d2ce16624bbe18b5d169c1a5ce4d6c3a47bc9d0e5c4fa6a50ed1239/grpcio-1.73.1-cp313-cp313-win32.whl", hash = "sha256:89018866a096e2ce21e05eabed1567479713ebe57b1db7cbb0f1e3b896793ba4", size = 3668151, upload-time = "2025-06-26T01:52:59.405Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/77ac689216daee10de318db5aa1b88d159432dc76a130948a56b3aa671a2/grpcio-1.73.1-cp313-cp313-win_amd64.whl", hash = "sha256:4a68f8c9966b94dff693670a5cf2b54888a48a5011c5d9ce2295a1a1465ee84f", size = 4335747, upload-time = "2025-06-26T01:53:01.233Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, ] [[package]] name = "iniconfig" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" }, +] + +[[package]] +name = "janus" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/7f/69884b6618be4baf6ebcacc716ee8680a842428a19f403db6d1c0bb990aa/janus-2.0.0.tar.gz", hash = "sha256:0970f38e0e725400496c834a368a67ee551dc3b5ad0a257e132f5b46f2e77770", size = 22910, upload-time = "2024-12-13T12:59:08.622Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/34/65604740edcb20e1bda6a890348ed7d282e7dd23aa00401cbe36fd0edbd9/janus-2.0.0-py3-none-any.whl", hash = "sha256:7e6449d34eab04cd016befbd7d8c0d8acaaaab67cb59e076a69149f9031745f9", size = 12161, upload-time = "2024-12-13T12:59:06.106Z" }, +] + +[[package]] +name = "libp2p" +version = "0.2.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "base58" }, + { name = "coincurve" }, + { name = "fastecdsa" }, + { name = "grpcio" }, + { name = "lru-dict" }, + { name = "multiaddr" }, + { name = "mypy-protobuf" }, + { name = "noiseprotocol" }, + { name = "protobuf" }, + { name = "pycryptodome" }, + { name = "pymultihash" }, + { name = "pynacl" }, + { name = "rpcudp" }, + { name = "trio" }, + { name = "trio-typing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/9d/d83923b2e00f64acc3da3e0e7c4ca9058f6c8012ead9c56649dd12d3a27c/libp2p-0.2.7.tar.gz", hash = "sha256:ec0277aa9c3a4210585a39e6544c85f4112fe97ea299104372994348eba8876c", size = 178240, upload-time = "2025-05-22T21:27:21.641Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/ed/d635d5b836b2c6ef69eac1a5c414df60450c140092a42c04a394b50a62ac/libp2p-0.2.7-py3-none-any.whl", hash = "sha256:6d342e8ba1a6cc6daf975b0c8cabf0b2a1e85fbd5a1f80db83c51a4361316117", size = 158039, upload-time = "2025-05-22T21:27:19.985Z" }, +] + +[[package]] +name = "linkify-it-py" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/ae/bb56c6828e4797ba5a4821eec7c43b8bf40f69cda4d4f5f8c8a2810ec96a/linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048", size = 27946, upload-time = "2024-02-04T14:48:04.179Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79", size = 19820, upload-time = "2024-02-04T14:48:02.496Z" }, +] + +[[package]] +name = "lru-dict" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/e3/42c87871920602a3c8300915bd0292f76eccc66c38f782397acbf8a62088/lru-dict-1.3.0.tar.gz", hash = "sha256:54fd1966d6bd1fcde781596cb86068214edeebff1db13a2cea11079e3fd07b6b", size = 13123, upload-time = "2023-11-06T01:40:12.951Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/5c/385f080747eb3083af87d8e4c9068f3c4cab89035f6982134889940dafd8/lru_dict-1.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c279068f68af3b46a5d649855e1fb87f5705fe1f744a529d82b2885c0e1fc69d", size = 17174, upload-time = "2023-11-06T01:39:07.923Z" }, + { url = "https://files.pythonhosted.org/packages/3c/de/5ef2ed75ce55d7059d1b96177ba04fa7ee1f35564f97bdfcd28fccfbe9d2/lru_dict-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:350e2233cfee9f326a0d7a08e309372d87186565e43a691b120006285a0ac549", size = 10742, upload-time = "2023-11-06T01:39:08.871Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/f69a6abb0062d2cf2ce0aaf0284b105b97d1da024ca6d3d0730e6151242e/lru_dict-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4eafb188a84483b3231259bf19030859f070321b00326dcb8e8c6cbf7db4b12f", size = 11079, upload-time = "2023-11-06T01:39:09.766Z" }, + { url = "https://files.pythonhosted.org/packages/ea/59/cf891143abe58a455b8eaa9175f0e80f624a146a2bf9a1ca842ee0ef930a/lru_dict-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73593791047e36b37fdc0b67b76aeed439fcea80959c7d46201240f9ec3b2563", size = 32469, upload-time = "2023-11-06T01:39:11.091Z" }, + { url = "https://files.pythonhosted.org/packages/59/88/d5976e9f70107ce11e45d93c6f0c2d5eaa1fc30bb3c8f57525eda4510dff/lru_dict-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1958cb70b9542773d6241974646e5410e41ef32e5c9e437d44040d59bd80daf2", size = 33496, upload-time = "2023-11-06T01:39:12.463Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f8/94d6e910d54fc1fa05c0ee1cd608c39401866a18cf5e5aff238449b33c11/lru_dict-1.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc1cd3ed2cee78a47f11f3b70be053903bda197a873fd146e25c60c8e5a32cd6", size = 29914, upload-time = "2023-11-06T01:39:13.395Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b9/9db79780c8a3cfd66bba6847773061e5cf8a3746950273b9985d47bbfe53/lru_dict-1.3.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82eb230d48eaebd6977a92ddaa6d788f14cf4f4bcf5bbffa4ddfd60d051aa9d4", size = 32241, upload-time = "2023-11-06T01:39:14.612Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b6/08a623019daec22a40c4d6d2c40851dfa3d129a53b2f9469db8eb13666c1/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5ad659cbc349d0c9ba8e536b5f40f96a70c360f43323c29f4257f340d891531c", size = 37320, upload-time = "2023-11-06T01:39:15.875Z" }, + { url = "https://files.pythonhosted.org/packages/70/0b/d3717159c26155ff77679cee1b077d22e1008bf45f19921e193319cd8e46/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ba490b8972531d153ac0d4e421f60d793d71a2f4adbe2f7740b3c55dce0a12f1", size = 35054, upload-time = "2023-11-06T01:39:17.063Z" }, + { url = "https://files.pythonhosted.org/packages/04/74/f2ae00de7c27984a19b88d2b09ac877031c525b01199d7841ec8fa657fd6/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:c0131351b8a7226c69f1eba5814cbc9d1d8daaf0fdec1ae3f30508e3de5262d4", size = 38613, upload-time = "2023-11-06T01:39:18.136Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0b/e30236aafe31b4247aa9ae61ba8aac6dde75c3ea0e47a8fb7eef53f6d5ce/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0e88dba16695f17f41701269fa046197a3fd7b34a8dba744c8749303ddaa18df", size = 37143, upload-time = "2023-11-06T01:39:19.571Z" }, + { url = "https://files.pythonhosted.org/packages/1c/28/b59bcebb8d76ba8147a784a8be7eab6a4ad3395b9236e73740ff675a5a52/lru_dict-1.3.0-cp312-cp312-win32.whl", hash = "sha256:6ffaf595e625b388babc8e7d79b40f26c7485f61f16efe76764e32dce9ea17fc", size = 12653, upload-time = "2023-11-06T01:39:20.574Z" }, + { url = "https://files.pythonhosted.org/packages/bd/18/06d9710cb0a0d3634f8501e4bdcc07abe64a32e404d82895a6a36fab97f6/lru_dict-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf9da32ef2582434842ab6ba6e67290debfae72771255a8e8ab16f3e006de0aa", size = 13811, upload-time = "2023-11-06T01:39:21.599Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[package.optional-dependencies] +linkify = [ + { name = "linkify-it-py" }, +] +plugins = [ + { name = "mdit-py-plugins" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542, upload-time = "2024-09-09T20:27:49.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", size = 55316, upload-time = "2024-09-09T20:27:48.397Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "multiaddr" +version = "0.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "base58" }, + { name = "netaddr" }, + { name = "six" }, + { name = "varint" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/f4/fa5353022ad8e0fd364bfa8b474f9562c36ce1305fad31fe52b849e30795/multiaddr-0.0.9.tar.gz", hash = "sha256:30b2695189edc3d5b90f1c303abb8f02d963a3a4edf2e7178b975eb417ab0ecf", size = 24726, upload-time = "2019-12-23T07:06:21.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/59/df732566d951c33f00a4022fc5bf9c5d1661b1c2cdaf56e75a1a5fa8f829/multiaddr-0.0.9-py2.py3-none-any.whl", hash = "sha256:5c0f862cbcf19aada2a899f80ef896ddb2e85614e0c8f04dd287c06c69dac95b", size = 16281, upload-time = "2019-12-23T07:06:18.915Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "mypy-protobuf" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, + { name = "types-protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/59/48f1df10f87cf87d0638dd38f54549e98cb43fcf7ce0ab6c159816e85f23/mypy-protobuf-3.3.0.tar.gz", hash = "sha256:24f3b0aecb06656e983f58e07c732a90577b9d7af3e1066fc2b663bbf0370248", size = 23640, upload-time = "2022-08-24T11:18:16.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/7b/b00434b3f508eb4bc637b83c2238cacaf1ce3cc5e540a2b76f5f99302446/mypy_protobuf-3.3.0-py3-none-any.whl", hash = "sha256:15604f6943b16c05db646903261e3b3e775cf7f7990b7c37b03d043a907b650d", size = 16001, upload-time = "2022-08-24T11:18:14.462Z" }, +] + +[[package]] +name = "netaddr" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/90/188b2a69654f27b221fba92fda7217778208532c962509e959a9cee5229d/netaddr-1.3.0.tar.gz", hash = "sha256:5c3c3d9895b551b763779ba7db7a03487dc1f8e3b385af819af341ae9ef6e48a", size = 2260504, upload-time = "2024-05-28T21:30:37.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/cc/f4fe2c7ce68b92cbf5b2d379ca366e1edae38cccaad00f69f529b460c3ef/netaddr-1.3.0-py3-none-any.whl", hash = "sha256:c2c6a8ebe5554ce33b7d5b3a306b71bbb373e000bbbf2350dd5213cc56e3dbbe", size = 2262023, upload-time = "2024-05-28T21:30:34.191Z" }, +] + +[[package]] +name = "noiseprotocol" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/17/fcf8a90dcf36fe00b475e395f34d92f42c41379c77b25a16066f63002f95/noiseprotocol-0.3.1.tar.gz", hash = "sha256:b092a871b60f6a8f07f17950dc9f7098c8fe7d715b049bd4c24ee3752b90d645", size = 16890, upload-time = "2020-11-25T19:06:48.938Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + { url = "https://files.pythonhosted.org/packages/9d/e1/76e4694201d67b93a6f1644b2588b4a3d965419fe189416e3496cf415db5/noiseprotocol-0.3.1-py3-none-any.whl", hash = "sha256:2e1a603a38439636cf0ffd8b3e8b12cee27d368a28b41be7dbe568b2abb23111", size = 20546, upload-time = "2020-03-03T18:51:28.095Z" }, +] + +[[package]] +name = "outcome" +version = "1.3.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060, upload-time = "2023-10-26T04:26:04.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692, upload-time = "2023-10-26T04:26:02.532Z" }, ] [[package]] name = "packaging" version = "24.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950, upload-time = "2024-11-08T09:47:47.202Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, ] [[package]] name = "pluggy" version = "1.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, +] + +[[package]] +name = "protobuf" +version = "6.31.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/f3/b9655a711b32c19720253f6f06326faf90580834e2e83f840472d752bc8b/protobuf-6.31.1.tar.gz", hash = "sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a", size = 441797, upload-time = "2025-05-28T19:25:54.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/6f/6ab8e4bf962fd5570d3deaa2d5c38f0a363f57b4501047b5ebeb83ab1125/protobuf-6.31.1-cp310-abi3-win32.whl", hash = "sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9", size = 423603, upload-time = "2025-05-28T19:25:41.198Z" }, + { url = "https://files.pythonhosted.org/packages/44/3a/b15c4347dd4bf3a1b0ee882f384623e2063bb5cf9fa9d57990a4f7df2fb6/protobuf-6.31.1-cp310-abi3-win_amd64.whl", hash = "sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447", size = 435283, upload-time = "2025-05-28T19:25:44.275Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c9/b9689a2a250264a84e66c46d8862ba788ee7a641cdca39bccf64f59284b7/protobuf-6.31.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:6f1227473dc43d44ed644425268eb7c2e488ae245d51c6866d19fe158e207402", size = 425604, upload-time = "2025-05-28T19:25:45.702Z" }, + { url = "https://files.pythonhosted.org/packages/76/a1/7a5a94032c83375e4fe7e7f56e3976ea6ac90c5e85fac8576409e25c39c3/protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39", size = 322115, upload-time = "2025-05-28T19:25:47.128Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/b59d405d64d31999244643d88c45c8241c58f17cc887e73bcb90602327f8/protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6", size = 321070, upload-time = "2025-05-28T19:25:50.036Z" }, + { url = "https://files.pythonhosted.org/packages/f7/af/ab3c51ab7507a7325e98ffe691d9495ee3d3aa5f589afad65ec920d39821/protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e", size = 168724, upload-time = "2025-05-28T19:25:53.926Z" }, ] [[package]] name = "py-peer" version = "0.1.0" -source = { virtual = "." } +source = { editable = "." } +dependencies = [ + { name = "base58" }, + { name = "janus" }, + { name = "libp2p" }, + { name = "protobuf" }, + { name = "textual" }, + { name = "trio" }, +] [package.dev-dependencies] dev = [ @@ -49,6 +441,14 @@ dev = [ ] [package.metadata] +requires-dist = [ + { name = "base58", specifier = ">=2.1.0" }, + { name = "janus", specifier = ">=1.0.0" }, + { name = "libp2p", specifier = ">=0.2.0" }, + { name = "protobuf", specifier = ">=4.25.0" }, + { name = "textual", specifier = ">=0.47.0" }, + { name = "trio", specifier = ">=0.22.0" }, +] [package.metadata.requires-dev] dev = [ @@ -56,6 +456,83 @@ dev = [ { name = "ruff", specifier = ">=0.5" }, ] +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, +] + +[[package]] +name = "pycryptodome" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" }, + { url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" }, + { url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" }, + { url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" }, + { url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" }, + { url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" }, + { url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" }, + { url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" }, + { url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" }, + { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" }, + { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" }, + { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" }, + { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" }, + { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" }, + { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pymultihash" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/32/64/53a5c75b11fa697d74ae5c52b38ee6c98fa3bf3bfd50e064ae11ef1a0db3/pymultihash-0.8.2.tar.gz", hash = "sha256:49c75a1ae9ecc6d22d259064d4597b3685da3f0258f4ded632e03a3a79af215b", size = 17471, upload-time = "2016-06-12T18:06:00.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/1c/570697fafd87adcec479b9ad34dc8371a363aa14bd9edaece5f0b8066903/pymultihash-0.8.2-py3-none-any.whl", hash = "sha256:f7fa840b24bd6acbd6b073fcd330f10e15619387297babf1dd13ca4dae6e8209", size = 13505, upload-time = "2016-06-12T18:05:55.562Z" }, +] + +[[package]] +name = "pynacl" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/22/27582568be639dfe22ddb3902225f91f2f17ceff88ce80e4db396c8986da/PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba", size = 3392854, upload-time = "2022-01-07T22:05:41.134Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/75/0b8ede18506041c0bf23ac4d8e2971b4161cd6ce630b177d0a08eb0d8857/PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1", size = 349920, upload-time = "2022-01-07T22:05:49.156Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/fddf10acd09637327a97ef89d2a9d621328850a72f1fdc8c08bdf72e385f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92", size = 601722, upload-time = "2022-01-07T22:05:50.989Z" }, + { url = "https://files.pythonhosted.org/packages/5d/70/87a065c37cca41a75f2ce113a5a2c2aa7533be648b184ade58971b5f7ccc/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394", size = 680087, upload-time = "2022-01-07T22:05:52.539Z" }, + { url = "https://files.pythonhosted.org/packages/ee/87/f1bb6a595f14a327e8285b9eb54d41fef76c585a0edef0a45f6fc95de125/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d", size = 856678, upload-time = "2022-01-07T22:05:54.251Z" }, + { url = "https://files.pythonhosted.org/packages/66/28/ca86676b69bf9f90e710571b67450508484388bfce09acf8a46f0b8c785f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858", size = 1133660, upload-time = "2022-01-07T22:05:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/3d/85/c262db650e86812585e2bc59e497a8f59948a005325a11bbbc9ecd3fe26b/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b", size = 663824, upload-time = "2022-01-07T22:05:57.434Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1a/cc308a884bd299b651f1633acb978e8596c71c33ca85e9dc9fa33a5399b9/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff", size = 1117912, upload-time = "2022-01-07T22:05:58.665Z" }, + { url = "https://files.pythonhosted.org/packages/25/2d/b7df6ddb0c2a33afdb358f8af6ea3b8c4d1196ca45497dd37a56f0c122be/PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543", size = 204624, upload-time = "2022-01-07T22:06:00.085Z" }, + { url = "https://files.pythonhosted.org/packages/5e/22/d3db169895faaf3e2eda892f005f433a62db2decbcfbc2f61e6517adfa87/PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", size = 212141, upload-time = "2022-01-07T22:06:01.861Z" }, +] + [[package]] name = "pytest" version = "8.3.4" @@ -66,32 +543,184 @@ dependencies = [ { name = "packaging" }, { name = "pluggy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } +sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919, upload-time = "2024-12-01T12:54:25.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083, upload-time = "2024-12-01T12:54:19.735Z" }, +] + +[[package]] +name = "rich" +version = "14.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, +] + +[[package]] +name = "rpcudp" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "u-msgpack-python" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/5b/99ee4dd6080d857f029ad209860d461305f5fba9fef2316548a1d131e4c2/rpcudp-5.0.1.tar.gz", hash = "sha256:b6793b9b3e84e9c8510fa78e259cc3204c1b03fd2bb4fbf0f457cd391933bb78", size = 8777, upload-time = "2025-03-31T01:01:56.602Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/8b/33f35f2a730f848b5e5ac24dcb50c97387b98756e0f199ee8da30ef19fe7/rpcudp-5.0.1-py3-none-any.whl", hash = "sha256:8bf7cf1caed687acbbf2a37b67a92b02d109483a2da28fa13cca93b9ee873b9a", size = 5594, upload-time = "2025-03-31T01:01:55.296Z" }, ] [[package]] name = "ruff" version = "0.9.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/39/8b/a86c300359861b186f18359adf4437ac8e4c52e42daa9eedc731ef9d5b53/ruff-0.9.7.tar.gz", hash = "sha256:643757633417907510157b206e490c3aa11cab0c087c912f60e07fbafa87a4c6", size = 3669813 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/f3/3a1d22973291226df4b4e2ff70196b926b6f910c488479adb0eeb42a0d7f/ruff-0.9.7-py3-none-linux_armv6l.whl", hash = "sha256:99d50def47305fe6f233eb8dabfd60047578ca87c9dcb235c9723ab1175180f4", size = 11774588 }, - { url = "https://files.pythonhosted.org/packages/8e/c9/b881f4157b9b884f2994fd08ee92ae3663fb24e34b0372ac3af999aa7fc6/ruff-0.9.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d59105ae9c44152c3d40a9c40d6331a7acd1cdf5ef404fbe31178a77b174ea66", size = 11746848 }, - { url = "https://files.pythonhosted.org/packages/14/89/2f546c133f73886ed50a3d449e6bf4af27d92d2f960a43a93d89353f0945/ruff-0.9.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f313b5800483770bd540cddac7c90fc46f895f427b7820f18fe1822697f1fec9", size = 11177525 }, - { url = "https://files.pythonhosted.org/packages/d7/93/6b98f2c12bf28ab9def59c50c9c49508519c5b5cfecca6de871cf01237f6/ruff-0.9.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:042ae32b41343888f59c0a4148f103208bf6b21c90118d51dc93a68366f4e903", size = 11996580 }, - { url = "https://files.pythonhosted.org/packages/8e/3f/b3fcaf4f6d875e679ac2b71a72f6691a8128ea3cb7be07cbb249f477c061/ruff-0.9.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87862589373b33cc484b10831004e5e5ec47dc10d2b41ba770e837d4f429d721", size = 11525674 }, - { url = "https://files.pythonhosted.org/packages/f0/48/33fbf18defb74d624535d5d22adcb09a64c9bbabfa755bc666189a6b2210/ruff-0.9.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a17e1e01bee0926d351a1ee9bc15c445beae888f90069a6192a07a84af544b6b", size = 12739151 }, - { url = "https://files.pythonhosted.org/packages/63/b5/7e161080c5e19fa69495cbab7c00975ef8a90f3679caa6164921d7f52f4a/ruff-0.9.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7c1f880ac5b2cbebd58b8ebde57069a374865c73f3bf41f05fe7a179c1c8ef22", size = 13416128 }, - { url = "https://files.pythonhosted.org/packages/4e/c8/b5e7d61fb1c1b26f271ac301ff6d9de5e4d9a9a63f67d732fa8f200f0c88/ruff-0.9.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e63fc20143c291cab2841dbb8260e96bafbe1ba13fd3d60d28be2c71e312da49", size = 12870858 }, - { url = "https://files.pythonhosted.org/packages/da/cb/2a1a8e4e291a54d28259f8fc6a674cd5b8833e93852c7ef5de436d6ed729/ruff-0.9.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91ff963baed3e9a6a4eba2a02f4ca8eaa6eba1cc0521aec0987da8d62f53cbef", size = 14786046 }, - { url = "https://files.pythonhosted.org/packages/ca/6c/c8f8a313be1943f333f376d79724260da5701426c0905762e3ddb389e3f4/ruff-0.9.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88362e3227c82f63eaebf0b2eff5b88990280fb1ecf7105523883ba8c3aaf6fb", size = 12550834 }, - { url = "https://files.pythonhosted.org/packages/9d/ad/f70cf5e8e7c52a25e166bdc84c082163c9c6f82a073f654c321b4dff9660/ruff-0.9.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0372c5a90349f00212270421fe91874b866fd3626eb3b397ede06cd385f6f7e0", size = 11961307 }, - { url = "https://files.pythonhosted.org/packages/52/d5/4f303ea94a5f4f454daf4d02671b1fbfe2a318b5fcd009f957466f936c50/ruff-0.9.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d76b8ab60e99e6424cd9d3d923274a1324aefce04f8ea537136b8398bbae0a62", size = 11612039 }, - { url = "https://files.pythonhosted.org/packages/eb/c8/bd12a23a75603c704ce86723be0648ba3d4ecc2af07eecd2e9fa112f7e19/ruff-0.9.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0c439bdfc8983e1336577f00e09a4e7a78944fe01e4ea7fe616d00c3ec69a3d0", size = 12168177 }, - { url = "https://files.pythonhosted.org/packages/cc/57/d648d4f73400fef047d62d464d1a14591f2e6b3d4a15e93e23a53c20705d/ruff-0.9.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:115d1f15e8fdd445a7b4dc9a30abae22de3f6bcabeb503964904471691ef7606", size = 12610122 }, - { url = "https://files.pythonhosted.org/packages/49/79/acbc1edd03ac0e2a04ae2593555dbc9990b34090a9729a0c4c0cf20fb595/ruff-0.9.7-py3-none-win32.whl", hash = "sha256:e9ece95b7de5923cbf38893f066ed2872be2f2f477ba94f826c8defdd6ec6b7d", size = 9988751 }, - { url = "https://files.pythonhosted.org/packages/6d/95/67153a838c6b6ba7a2401241fd8a00cd8c627a8e4a0491b8d853dedeffe0/ruff-0.9.7-py3-none-win_amd64.whl", hash = "sha256:3770fe52b9d691a15f0b87ada29c45324b2ace8f01200fb0c14845e499eb0c2c", size = 11002987 }, - { url = "https://files.pythonhosted.org/packages/63/6a/aca01554949f3a401991dc32fe22837baeaccb8a0d868256cbb26a029778/ruff-0.9.7-py3-none-win_arm64.whl", hash = "sha256:b075a700b2533feb7a01130ff656a4ec0d5f340bb540ad98759b8401c32c2037", size = 10177763 }, +sdist = { url = "https://files.pythonhosted.org/packages/39/8b/a86c300359861b186f18359adf4437ac8e4c52e42daa9eedc731ef9d5b53/ruff-0.9.7.tar.gz", hash = "sha256:643757633417907510157b206e490c3aa11cab0c087c912f60e07fbafa87a4c6", size = 3669813, upload-time = "2025-02-20T13:26:52.111Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/f3/3a1d22973291226df4b4e2ff70196b926b6f910c488479adb0eeb42a0d7f/ruff-0.9.7-py3-none-linux_armv6l.whl", hash = "sha256:99d50def47305fe6f233eb8dabfd60047578ca87c9dcb235c9723ab1175180f4", size = 11774588, upload-time = "2025-02-20T13:25:52.253Z" }, + { url = "https://files.pythonhosted.org/packages/8e/c9/b881f4157b9b884f2994fd08ee92ae3663fb24e34b0372ac3af999aa7fc6/ruff-0.9.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d59105ae9c44152c3d40a9c40d6331a7acd1cdf5ef404fbe31178a77b174ea66", size = 11746848, upload-time = "2025-02-20T13:25:57.279Z" }, + { url = "https://files.pythonhosted.org/packages/14/89/2f546c133f73886ed50a3d449e6bf4af27d92d2f960a43a93d89353f0945/ruff-0.9.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f313b5800483770bd540cddac7c90fc46f895f427b7820f18fe1822697f1fec9", size = 11177525, upload-time = "2025-02-20T13:26:00.007Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/6b98f2c12bf28ab9def59c50c9c49508519c5b5cfecca6de871cf01237f6/ruff-0.9.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:042ae32b41343888f59c0a4148f103208bf6b21c90118d51dc93a68366f4e903", size = 11996580, upload-time = "2025-02-20T13:26:03.274Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3f/b3fcaf4f6d875e679ac2b71a72f6691a8128ea3cb7be07cbb249f477c061/ruff-0.9.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87862589373b33cc484b10831004e5e5ec47dc10d2b41ba770e837d4f429d721", size = 11525674, upload-time = "2025-02-20T13:26:06.073Z" }, + { url = "https://files.pythonhosted.org/packages/f0/48/33fbf18defb74d624535d5d22adcb09a64c9bbabfa755bc666189a6b2210/ruff-0.9.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a17e1e01bee0926d351a1ee9bc15c445beae888f90069a6192a07a84af544b6b", size = 12739151, upload-time = "2025-02-20T13:26:08.964Z" }, + { url = "https://files.pythonhosted.org/packages/63/b5/7e161080c5e19fa69495cbab7c00975ef8a90f3679caa6164921d7f52f4a/ruff-0.9.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7c1f880ac5b2cbebd58b8ebde57069a374865c73f3bf41f05fe7a179c1c8ef22", size = 13416128, upload-time = "2025-02-20T13:26:12.54Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c8/b5e7d61fb1c1b26f271ac301ff6d9de5e4d9a9a63f67d732fa8f200f0c88/ruff-0.9.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e63fc20143c291cab2841dbb8260e96bafbe1ba13fd3d60d28be2c71e312da49", size = 12870858, upload-time = "2025-02-20T13:26:16.794Z" }, + { url = "https://files.pythonhosted.org/packages/da/cb/2a1a8e4e291a54d28259f8fc6a674cd5b8833e93852c7ef5de436d6ed729/ruff-0.9.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91ff963baed3e9a6a4eba2a02f4ca8eaa6eba1cc0521aec0987da8d62f53cbef", size = 14786046, upload-time = "2025-02-20T13:26:19.85Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6c/c8f8a313be1943f333f376d79724260da5701426c0905762e3ddb389e3f4/ruff-0.9.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88362e3227c82f63eaebf0b2eff5b88990280fb1ecf7105523883ba8c3aaf6fb", size = 12550834, upload-time = "2025-02-20T13:26:23.082Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ad/f70cf5e8e7c52a25e166bdc84c082163c9c6f82a073f654c321b4dff9660/ruff-0.9.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0372c5a90349f00212270421fe91874b866fd3626eb3b397ede06cd385f6f7e0", size = 11961307, upload-time = "2025-02-20T13:26:26.738Z" }, + { url = "https://files.pythonhosted.org/packages/52/d5/4f303ea94a5f4f454daf4d02671b1fbfe2a318b5fcd009f957466f936c50/ruff-0.9.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d76b8ab60e99e6424cd9d3d923274a1324aefce04f8ea537136b8398bbae0a62", size = 11612039, upload-time = "2025-02-20T13:26:30.26Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c8/bd12a23a75603c704ce86723be0648ba3d4ecc2af07eecd2e9fa112f7e19/ruff-0.9.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0c439bdfc8983e1336577f00e09a4e7a78944fe01e4ea7fe616d00c3ec69a3d0", size = 12168177, upload-time = "2025-02-20T13:26:33.452Z" }, + { url = "https://files.pythonhosted.org/packages/cc/57/d648d4f73400fef047d62d464d1a14591f2e6b3d4a15e93e23a53c20705d/ruff-0.9.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:115d1f15e8fdd445a7b4dc9a30abae22de3f6bcabeb503964904471691ef7606", size = 12610122, upload-time = "2025-02-20T13:26:37.365Z" }, + { url = "https://files.pythonhosted.org/packages/49/79/acbc1edd03ac0e2a04ae2593555dbc9990b34090a9729a0c4c0cf20fb595/ruff-0.9.7-py3-none-win32.whl", hash = "sha256:e9ece95b7de5923cbf38893f066ed2872be2f2f477ba94f826c8defdd6ec6b7d", size = 9988751, upload-time = "2025-02-20T13:26:40.366Z" }, + { url = "https://files.pythonhosted.org/packages/6d/95/67153a838c6b6ba7a2401241fd8a00cd8c627a8e4a0491b8d853dedeffe0/ruff-0.9.7-py3-none-win_amd64.whl", hash = "sha256:3770fe52b9d691a15f0b87ada29c45324b2ace8f01200fb0c14845e499eb0c2c", size = 11002987, upload-time = "2025-02-20T13:26:43.762Z" }, + { url = "https://files.pythonhosted.org/packages/63/6a/aca01554949f3a401991dc32fe22837baeaccb8a0d868256cbb26a029778/ruff-0.9.7-py3-none-win_arm64.whl", hash = "sha256:b075a700b2533feb7a01130ff656a4ec0d5f340bb540ad98759b8401c32c2037", size = 10177763, upload-time = "2025-02-20T13:26:48.92Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "textual" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", extra = ["linkify", "plugins"] }, + { name = "platformdirs" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/63/16cdf4b9efb47366940d8315118c5c6dd6309f5eb2c159d7195b60e2e221/textual-3.5.0.tar.gz", hash = "sha256:c4a440338694672acf271c74904f1cf1e4a64c6761c056b02a561774b81a04f4", size = 1590084, upload-time = "2025-06-20T14:46:58.263Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/36/2597036cb80e40f71555bf59741471f7bd76ebed112f10ae0549650a12bf/textual-3.5.0-py3-none-any.whl", hash = "sha256:7c960efb70391b754e66201776793de2b26d699d51fb91f5f78401d13cec79a1", size = 688740, upload-time = "2025-06-20T14:46:56.484Z" }, +] + +[[package]] +name = "trio" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cffi", marker = "implementation_name != 'pypy' and os_name == 'nt'" }, + { name = "idna" }, + { name = "outcome" }, + { name = "sniffio" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/c1/68d582b4d3a1c1f8118e18042464bb12a7c1b75d64d75111b297687041e3/trio-0.30.0.tar.gz", hash = "sha256:0781c857c0c81f8f51e0089929a26b5bb63d57f927728a5586f7e36171f064df", size = 593776, upload-time = "2025-04-21T00:48:19.507Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/8e/3f6dfda475ecd940e786defe6df6c500734e686c9cd0a0f8ef6821e9b2f2/trio-0.30.0-py3-none-any.whl", hash = "sha256:3bf4f06b8decf8d3cf00af85f40a89824669e2d033bb32469d34840edcfc22a5", size = 499194, upload-time = "2025-04-21T00:48:17.167Z" }, +] + +[[package]] +name = "trio-typing" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-generator" }, + { name = "importlib-metadata" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "trio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/74/a87aafa40ec3a37089148b859892cbe2eef08d132c816d58a60459be5337/trio-typing-0.10.0.tar.gz", hash = "sha256:065ee684296d52a8ab0e2374666301aec36ee5747ac0e7a61f230250f8907ac3", size = 38747, upload-time = "2023-12-01T02:54:55.508Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/ff/9bd795273eb14fac7f6a59d16cc8c4d0948a619a1193d375437c7f50f3eb/trio_typing-0.10.0-py3-none-any.whl", hash = "sha256:6d0e7ec9d837a2fe03591031a172533fbf4a1a95baf369edebfc51d5a49f0264", size = 42224, upload-time = "2023-12-01T02:54:54.1Z" }, +] + +[[package]] +name = "types-protobuf" +version = "6.30.2.20250703" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/54/d63ce1eee8e93c4d710bbe2c663ec68e3672cf4f2fca26eecd20981c0c5d/types_protobuf-6.30.2.20250703.tar.gz", hash = "sha256:609a974754bbb71fa178fc641f51050395e8e1849f49d0420a6281ed8d1ddf46", size = 62300, upload-time = "2025-07-03T03:14:05.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/2b/5d0377c3d6e0f49d4847ad2c40629593fee4a5c9ec56eba26a15c708fbc0/types_protobuf-6.30.2.20250703-py3-none-any.whl", hash = "sha256:fa5aff9036e9ef432d703abbdd801b436a249b6802e4df5ef74513e272434e57", size = 76489, upload-time = "2025-07-03T03:14:04.453Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, +] + +[[package]] +name = "u-msgpack-python" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/9d/a40411a475e7d4838994b7f6bcc6bfca9acc5b119ce3a7503608c4428b49/u-msgpack-python-2.8.0.tar.gz", hash = "sha256:b801a83d6ed75e6df41e44518b4f2a9c221dc2da4bcd5380e3a0feda520bc61a", size = 18167, upload-time = "2023-05-18T09:28:12.187Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/5e/512aeb40fd819f4660d00f96f5c7371ee36fc8c6b605128c5ee59e0b28c6/u_msgpack_python-2.8.0-py2.py3-none-any.whl", hash = "sha256:1d853d33e78b72c4228a2025b4db28cda81214076e5b0422ed0ae1b1b2bb586a", size = 10590, upload-time = "2023-05-18T09:28:10.323Z" }, +] + +[[package]] +name = "uc-micro-py" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/7a/146a99696aee0609e3712f2b44c6274566bc368dfe8375191278045186b8/uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a", size = 6043, upload-time = "2024-02-09T16:52:01.654Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/87/1f677586e8ac487e29672e4b17455758fce261de06a0d086167bb760361a/uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5", size = 6229, upload-time = "2024-02-09T16:52:00.371Z" }, +] + +[[package]] +name = "varint" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/fe/1ea0ba0896dfa47186692655b86db3214c4b7c9e0e76c7b1dc257d101ab1/varint-1.0.2.tar.gz", hash = "sha256:a6ecc02377ac5ee9d65a6a8ad45c9ff1dac8ccee19400a5950fb51d594214ca5", size = 1886, upload-time = "2016-02-24T20:42:38.5Z" } + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, ]