elo-node 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
elo/transport/tcp.py ADDED
@@ -0,0 +1,327 @@
1
+ """TCP connection manager — persistent peer connections with framing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ import time
8
+ from collections.abc import Callable, Awaitable
9
+ from typing import Any
10
+
11
+ from elo.transport.protocol import (
12
+ encode_frame,
13
+ read_frame,
14
+ write_frame,
15
+ bye_msg,
16
+ heartbeat_msg,
17
+ FrameError,
18
+ MessageType,
19
+ )
20
+
21
+ logger = logging.getLogger("elo.transport.tcp")
22
+
23
+ # Message handler signature: (peer_addr: str, message: dict) → None
24
+ MessageHandler = Callable[[str, dict[str, Any]], Awaitable[None]]
25
+
26
+
27
+ class PeerConnection:
28
+ """A single TCP connection to a peer."""
29
+
30
+ def __init__(self, addr: str, reader: asyncio.StreamReader,
31
+ writer: asyncio.StreamWriter):
32
+ self.addr = addr
33
+ self._reader = reader
34
+ self._writer = writer
35
+ self._alive = True
36
+ self._last_read = time.monotonic()
37
+
38
+ async def send(self, message: dict[str, Any]) -> None:
39
+ """Send a framed message to the peer."""
40
+ if not self._alive:
41
+ raise ConnectionError("Peer disconnected")
42
+ try:
43
+ await write_frame(self._writer, message)
44
+ except Exception:
45
+ self._alive = False
46
+ raise
47
+
48
+ async def recv(self) -> dict[str, Any] | None:
49
+ """Receive one framed message from the peer. Returns None on EOF."""
50
+ try:
51
+ msg = await read_frame(self._reader)
52
+ self._last_read = time.monotonic()
53
+ return msg
54
+ except (FrameError, asyncio.IncompleteReadError, ConnectionError):
55
+ self._alive = False
56
+ return None
57
+
58
+ def close(self) -> None:
59
+ """Close the connection."""
60
+ self._alive = False
61
+ try:
62
+ self._writer.close()
63
+ except Exception:
64
+ pass
65
+
66
+ @property
67
+ def alive(self) -> bool:
68
+ return self._alive
69
+
70
+ @property
71
+ def idle_seconds(self) -> float:
72
+ return time.monotonic() - self._last_read
73
+
74
+
75
+ class TCPManager:
76
+ """Manages all TCP peer connections: server + client connections."""
77
+
78
+ # Timeout configuration
79
+ CONNECT_TIMEOUT = 10 # seconds
80
+ HEARTBEAT_INTERVAL = 30 # seconds
81
+ HEARTBEAT_IDLE_TIMEOUT = 90 # 3 missed heartbeats
82
+
83
+ def __init__(self, node_id: str, host: str = "0.0.0.0", port: int = 7878):
84
+ self._node_id = node_id
85
+ self._host = host
86
+ self._port = port
87
+ self._server: asyncio.Server | None = None
88
+ self._peers: dict[str, PeerConnection] = {}
89
+ self._handler: MessageHandler | None = None
90
+ self._shutdown = asyncio.Event()
91
+ self._heartbeat_task: asyncio.Task | None = None
92
+ self._listen_task: asyncio.Task | None = None
93
+
94
+ # ── lifecycle ────────────────────────────────────────────
95
+
96
+ async def start(self) -> int:
97
+ """Start the TCP server. Returns the actual port."""
98
+ self._server = await asyncio.start_server(
99
+ self._handle_incoming,
100
+ host=self._host,
101
+ port=self._port,
102
+ )
103
+ # Get the actual port (useful if port=0 for random assignment)
104
+ if self._port == 0 and self._server.sockets:
105
+ self._port = self._server.sockets[0].getsockname()[1]
106
+
107
+ logger.info("[tcp] listening on %s:%d", self._host, self._port)
108
+ return self._port
109
+
110
+ async def stop(self) -> None:
111
+ """Stop the server and close all peer connections."""
112
+ self._shutdown.set()
113
+
114
+ if self._heartbeat_task:
115
+ self._heartbeat_task.cancel()
116
+
117
+ # Send BYE to all peers
118
+ for addr, peer in list(self._peers.items()):
119
+ try:
120
+ await peer.send(bye_msg())
121
+ except Exception:
122
+ pass
123
+ peer.close()
124
+
125
+ self._peers.clear()
126
+
127
+ if self._server:
128
+ self._server.close()
129
+ await self._server.wait_closed()
130
+
131
+ logger.info("[tcp] stopped")
132
+
133
+ # ── handlers ─────────────────────────────────────────────
134
+
135
+ def on_message(self, handler: MessageHandler) -> None:
136
+ """Register a message handler."""
137
+ self._handler = handler
138
+
139
+ async def _handle_incoming(self, reader: asyncio.StreamReader,
140
+ writer: asyncio.StreamWriter) -> None:
141
+ """Handle a new incoming TCP connection."""
142
+ peername = writer.get_extra_info("peername")
143
+ addr = f"{peername[0]}:{peername[1]}" if peername else "unknown"
144
+ logger.debug("[tcp] incoming from %s", addr)
145
+
146
+ peer = PeerConnection(addr, reader, writer)
147
+
148
+ try:
149
+ # Read HELLO as first message
150
+ hello = await peer.recv()
151
+ if hello is None or hello.get("type") != MessageType.HELLO:
152
+ logger.warning("[tcp] no hello from %s", addr)
153
+ peer.close()
154
+ return
155
+
156
+ remote_id = hello.get("node_id", "")
157
+ addr = f"{remote_id[:12]}@{addr}"
158
+ peer.addr = addr
159
+
160
+ self._peers[addr] = peer
161
+ logger.info("[tcp] peer connected: %s", addr)
162
+
163
+ # Notify handler about the peer + its HELLO
164
+ if self._handler:
165
+ await self._handler(addr, hello)
166
+
167
+ except Exception:
168
+ logger.debug("[tcp] connection error from %s", addr, exc_info=True)
169
+ peer.close()
170
+ return
171
+
172
+ # Start read loop (shared by inbound and outbound)
173
+ await self._read_loop(addr, peer)
174
+
175
+ # ── read loop (shared by inbound + outbound) ───────────
176
+
177
+ async def _read_loop(self, addr: str, peer: PeerConnection) -> None:
178
+ """Continuously read messages from a peer until disconnect."""
179
+ try:
180
+ while not self._shutdown.is_set():
181
+ msg = await peer.recv()
182
+ if msg is None:
183
+ break
184
+ if self._handler:
185
+ await self._handler(addr, msg)
186
+ except Exception:
187
+ logger.debug("[tcp] read error from %s", addr, exc_info=True)
188
+ finally:
189
+ peer.close()
190
+ self._peers.pop(addr, None)
191
+ if self._handler:
192
+ try:
193
+ await self._handler(addr, {"type": MessageType.BYE, "node_id": ""})
194
+ except Exception:
195
+ pass
196
+ logger.info("[tcp] peer disconnected: %s", addr)
197
+
198
+ # ── outbound connections ─────────────────────────────────
199
+
200
+ async def connect_to_peer(self, addr: str, node_id: str = "unknown",
201
+ hello_payload: dict | None = None) -> str | None:
202
+ """Connect to a remote peer. Returns the assigned peer address on success."""
203
+ if ":" not in addr:
204
+ logger.warning("[tcp] invalid address: %s", addr)
205
+ return None
206
+
207
+ # Normalize: if no port, use default
208
+ host, _, port_str = addr.partition(":")
209
+ port = int(port_str) if port_str else self._port
210
+ canonical = f"{host}:{port}"
211
+
212
+ if canonical in self._peers:
213
+ return canonical
214
+
215
+ try:
216
+ reader, writer = await asyncio.wait_for(
217
+ asyncio.open_connection(host, port),
218
+ timeout=self.CONNECT_TIMEOUT,
219
+ )
220
+ except Exception as e:
221
+ logger.debug("[tcp] connect to %s failed: %s", canonical, e)
222
+ return None
223
+
224
+ peer = PeerConnection(canonical, reader, writer)
225
+
226
+ # Send HELLO
227
+ if hello_payload:
228
+ await peer.send(hello_payload)
229
+
230
+ # Wait for HELLO_ACK
231
+ try:
232
+ ack = await asyncio.wait_for(peer.recv(), timeout=5)
233
+ except asyncio.TimeoutError:
234
+ peer.close()
235
+ return None
236
+
237
+ if ack is None or ack.get("type") != MessageType.HELLO_ACK:
238
+ peer.close()
239
+ return None
240
+
241
+ remote_id = ack.get("node_id", "")
242
+ display_addr = f"{remote_id[:12]}@{canonical}"
243
+ peer.addr = display_addr
244
+
245
+ self._peers[display_addr] = peer
246
+
247
+ # Notify handler with HELLO_ACK
248
+ if self._handler:
249
+ await self._handler(display_addr, ack)
250
+
251
+ logger.info("[tcp] connected to %s", display_addr)
252
+
253
+ # Start read loop in background
254
+ asyncio.create_task(self._read_loop(display_addr, peer))
255
+
256
+ return display_addr
257
+
258
+ async def send_to(self, peer_addr: str, message: dict[str, Any]) -> None:
259
+ """Send a message to a specific peer."""
260
+ peer = self._peers.get(peer_addr)
261
+ if peer is None:
262
+ raise ConnectionError(f"Peer not connected: {peer_addr}")
263
+ await peer.send(message)
264
+
265
+ # ── broadcast ────────────────────────────────────────────
266
+
267
+ async def broadcast(self, message: dict[str, Any],
268
+ exclude: set[str] | None = None) -> None:
269
+ """Send a message to all connected peers."""
270
+ exclude = exclude or set()
271
+ for addr, peer in list(self._peers.items()):
272
+ if addr in exclude:
273
+ continue
274
+ try:
275
+ await peer.send(message)
276
+ except Exception:
277
+ logger.debug("[tcp] broadcast failed to %s", addr)
278
+
279
+ # ── heartbeat ────────────────────────────────────────────
280
+
281
+ async def start_heartbeat(self) -> None:
282
+ """Periodically send heartbeats and purge dead peers."""
283
+ while not self._shutdown.is_set():
284
+ try:
285
+ await asyncio.wait_for(
286
+ self._shutdown.wait(),
287
+ timeout=self.HEARTBEAT_INTERVAL,
288
+ )
289
+ break
290
+ except asyncio.TimeoutError:
291
+ pass
292
+
293
+ hb = heartbeat_msg(self._node_id)
294
+ dead = []
295
+
296
+ for addr, peer in list(self._peers.items()):
297
+ try:
298
+ await peer.send(hb)
299
+ except Exception:
300
+ dead.append(addr)
301
+
302
+ # Check idle timeout
303
+ if peer.idle_seconds > self.HEARTBEAT_IDLE_TIMEOUT:
304
+ dead.append(addr)
305
+
306
+ for addr in set(dead):
307
+ peer = self._peers.pop(addr, None)
308
+ if peer:
309
+ peer.close()
310
+ if self._handler:
311
+ await self._handler(addr,
312
+ {"type": MessageType.BYE, "node_id": ""})
313
+ logger.info("[tcp] peer timed out: %s", addr)
314
+
315
+ # ── accessors ────────────────────────────────────────────
316
+
317
+ @property
318
+ def peer_count(self) -> int:
319
+ return len(self._peers)
320
+
321
+ @property
322
+ def peer_addresses(self) -> list[str]:
323
+ return list(self._peers.keys())
324
+
325
+ @property
326
+ def port(self) -> int:
327
+ return self._port
@@ -0,0 +1,97 @@
1
+ """Local tracker — each node publishes its own agent/tool registry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ class LocalTracker:
9
+ """A node's local registry of agents, models, and tools.
10
+
11
+ Visibility modes:
12
+ - "public": any connected peer can query capabilities
13
+ - "private": only peers in the allowlist can query
14
+ """
15
+
16
+ def __init__(self, visibility: str = "public"):
17
+ self._visibility = visibility
18
+ self._caps: dict[str, list[dict[str, str]]] = {
19
+ "agents": [],
20
+ "models": [],
21
+ "tools": [],
22
+ }
23
+ self._allowlist: set[str] = set() # node_ids (for private mode)
24
+ self._interests: list[str] = []
25
+
26
+ # ── visibility ───────────────────────────────────────────
27
+
28
+ @property
29
+ def visibility(self) -> str:
30
+ return self._visibility
31
+
32
+ def set_visibility(self, mode: str) -> None:
33
+ if mode not in ("public", "private"):
34
+ raise ValueError(f"Invalid visibility: {mode}")
35
+ self._visibility = mode
36
+
37
+ def allow_peer(self, node_id: str) -> None:
38
+ self._allowlist.add(node_id)
39
+
40
+ def revoke_peer(self, node_id: str) -> None:
41
+ self._allowlist.discard(node_id)
42
+
43
+ def is_allowed(self, node_id: str) -> bool:
44
+ if self._visibility == "public":
45
+ return True
46
+ return node_id in self._allowlist
47
+
48
+ # ── caps ─────────────────────────────────────────────────
49
+
50
+ def register(self, *, agents: list[dict[str, str]] | None = None,
51
+ models: list[dict[str, str]] | None = None,
52
+ tools: list[dict[str, str]] | None = None) -> None:
53
+ if agents:
54
+ self._caps["agents"] = agents
55
+ if models:
56
+ self._caps["models"] = models
57
+ if tools:
58
+ self._caps["tools"] = tools
59
+
60
+ def set_interests(self, interests: list[str]) -> None:
61
+ self._interests = interests
62
+
63
+ def get_caps_for_peer(self, node_id: str) -> dict[str, list[dict[str, str]]]:
64
+ """Return capabilities visible to the given peer."""
65
+ if self.is_allowed(node_id):
66
+ return self._caps
67
+ return {"agents": [], "models": [], "tools": []}
68
+
69
+ def get_public_caps(self) -> dict[str, list[dict[str, str]]]:
70
+ """Return capabilities for public advertisement (HELLO)."""
71
+ if self._visibility == "public":
72
+ return self._caps
73
+ return {"agents": [], "models": [], "tools": []}
74
+
75
+ @property
76
+ def caps(self) -> dict[str, list[dict[str, str]]]:
77
+ return self._caps
78
+
79
+ @property
80
+ def interests(self) -> list[str]:
81
+ return self._interests
82
+
83
+ def has_capability(self, name: str) -> bool:
84
+ """Check if this node has a specific capability."""
85
+ for category in ("agents", "models", "tools"):
86
+ for item in self._caps[category]:
87
+ if item.get("name") == name:
88
+ return True
89
+ return False
90
+
91
+ def match(self, capability: str) -> dict[str, Any] | None:
92
+ """Match a capability by name. Returns the matched item or None."""
93
+ for category in ("agents", "models", "tools"):
94
+ for item in self._caps[category]:
95
+ if item.get("name") == capability:
96
+ return {"category": category, **item}
97
+ return None
elo/types.py ADDED
@@ -0,0 +1,126 @@
1
+ """Tipos do protocolo Elo v1."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field, asdict
6
+ from datetime import datetime, timezone
7
+ from typing import Any
8
+ import uuid
9
+
10
+
11
+ @dataclass
12
+ class AgentCap:
13
+ name: str
14
+ description: str = ""
15
+ model: str = ""
16
+
17
+
18
+ @dataclass
19
+ class ModelCap:
20
+ name: str
21
+ provider: str = ""
22
+ context: int = 0
23
+
24
+
25
+ @dataclass
26
+ class ToolCap:
27
+ name: str
28
+ description: str = ""
29
+ version: str = ""
30
+
31
+
32
+ @dataclass
33
+ class Capabilities:
34
+ agents: list[AgentCap] = field(default_factory=list)
35
+ models: list[ModelCap] = field(default_factory=list)
36
+ tools: list[ToolCap] = field(default_factory=list)
37
+
38
+ def to_dict(self) -> dict:
39
+ return asdict(self)
40
+
41
+
42
+ @dataclass
43
+ class NodeInfo:
44
+ name: str
45
+ version: str = "0.1.0"
46
+ started_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
47
+ status: str = "online"
48
+ public_key: str = ""
49
+ nats_url: str = ""
50
+ labels: dict[str, str] = field(default_factory=dict)
51
+
52
+ def to_dict(self) -> dict:
53
+ return asdict(self)
54
+
55
+
56
+ @dataclass
57
+ class Task:
58
+ target: str
59
+ caller: str
60
+ capability: str
61
+ payload: dict[str, Any] = field(default_factory=dict)
62
+ id: str = field(default_factory=lambda: str(uuid.uuid4()))
63
+ protocol: str = "elo.v1"
64
+ type: str = "task"
65
+ timestamp: int = field(default_factory=lambda: int(datetime.now(timezone.utc).timestamp()))
66
+ ttl_s: int = 60
67
+ signature: str = "" # Assinatura ed25519 do caller
68
+
69
+ def to_dict(self) -> dict:
70
+ d = asdict(self)
71
+ d["type"] = "task"
72
+ return d
73
+
74
+ @classmethod
75
+ def from_dict(cls, data: dict) -> "Task":
76
+ return cls(
77
+ id=data.get("id", ""),
78
+ protocol=data.get("protocol", "elo.v1"),
79
+ timestamp=data.get("timestamp", 0),
80
+ target=data.get("target", ""),
81
+ caller=data.get("caller", ""),
82
+ capability=data.get("capability", ""),
83
+ payload=data.get("payload", {}),
84
+ ttl_s=data.get("ttl_s", 60),
85
+ signature=data.get("signature", ""),
86
+ )
87
+
88
+
89
+ @dataclass
90
+ class Result:
91
+ id: str
92
+ status: str # "success" | "error" | "timeout"
93
+ payload: dict[str, Any] = field(default_factory=dict)
94
+ error: dict[str, str] | None = None
95
+ protocol: str = "elo.v1"
96
+ type: str = "result"
97
+
98
+ def to_dict(self) -> dict:
99
+ d = asdict(self)
100
+ d["type"] = "result"
101
+ if self.error is None:
102
+ del d["error"]
103
+ return d
104
+
105
+ @classmethod
106
+ def success(cls, task_id: str, payload: dict[str, Any]) -> "Result":
107
+ return cls(id=task_id, status="success", payload=payload)
108
+
109
+ @classmethod
110
+ def make_error(cls, task_id: str, code: str, message: str) -> "Result":
111
+ return cls(id=task_id, status="error", error={"code": code, "message": message})
112
+
113
+
114
+ @dataclass
115
+ class Event:
116
+ event_type: str # "task.completed" | "node.online" | "node.offline" | "capability.changed"
117
+ data: dict[str, Any] = field(default_factory=dict)
118
+ id: str = field(default_factory=lambda: str(uuid.uuid4()))
119
+ protocol: str = "elo.v1"
120
+ type: str = "event"
121
+ timestamp: int = field(default_factory=lambda: int(datetime.now(timezone.utc).timestamp()))
122
+
123
+ def to_dict(self) -> dict:
124
+ d = asdict(self)
125
+ d["type"] = "event"
126
+ return d
@@ -0,0 +1,107 @@
1
+ Metadata-Version: 2.4
2
+ Name: elo-node
3
+ Version: 0.4.0
4
+ Summary: Elo — malha P2P de mensagens para agentes de IA. Zero infraestrutura.
5
+ Author: Elo Contributors
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/xalq/elo
8
+ Project-URL: Repository, https://github.com/xalq/elo
9
+ Keywords: p2p,agents,ai,distributed,messaging,mesh
10
+ Classifier: Development Status :: 2 - Pre-Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Requires-Python: >=3.11
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: cryptography>=42.0
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest>=8.0; extra == "dev"
21
+ Requires-Dist: pytest-asyncio>=0.25; extra == "dev"
22
+
23
+ # Elo Node — Malha P2P para Agentes de IA
24
+
25
+ **Zero infraestrutura. Um processo. Uma porta TCP. Uma chave ed25519.**
26
+
27
+ Elo é uma malha de mensagens P2P descentralizada para comunicação entre agentes de IA. Sem servidor central, sem Kafka, sem Redis, sem NATS. Apenas TCP direto entre nós.
28
+
29
+ ```bash
30
+ pip install elo-node
31
+ ```
32
+
33
+ ```python
34
+ import asyncio
35
+ from elo import Node
36
+
37
+ async def main():
38
+ node = Node("meu-agente", port=7878)
39
+ await node.connect()
40
+ await node.register(agents=["analyst"], tools=["web-search"])
41
+
42
+ @node.on_task
43
+ async def handle(task):
44
+ return {"result": f"processed by {node.node_id}"}
45
+
46
+ await node.run()
47
+
48
+ asyncio.run(main())
49
+ ```
50
+
51
+ ## Recursos
52
+
53
+ - **P2P descentralizado** — descoberta via tracker público ou DHT Kademlia
54
+ - **Assinatura ed25519** — identidade criptográfica, mensagens autenticadas
55
+ - **Capabilities** — publish/subscribe de capacidades entre nós
56
+ - **Zero infra** — sem Kafka, Redis, NATS, ou servidor central
57
+ - **CLI nativo** — `python -m elo serve`, `status`, `init`, `id`
58
+
59
+ ## CLI
60
+
61
+ ```bash
62
+ python -m elo status # Node ID, hash, chaves
63
+ python -m elo id # Apenas o node_id
64
+ python -m elo pubkey # Chave pública (hex + b64)
65
+ python -m elo init # Gerar identidade persistente
66
+ python -m elo serve # Iniciar nó interativo
67
+ ```
68
+
69
+ ## Arquitetura
70
+
71
+ ```
72
+ ┌──────────────────┐ TCP/JSON ┌──────────────────┐
73
+ │ Node A │◄──────────────►│ Node B │
74
+ │ ed25519 key │ │ ed25519 key │
75
+ │ Capabilities │ │ Capabilities │
76
+ │ Interests │ │ Interests │
77
+ └──────────────────┘ └──────────────────┘
78
+ │ │
79
+ │ Tracker (opcional) │
80
+ └───────────── DHT ────────────────┘
81
+ ```
82
+
83
+ Cada nó:
84
+ 1. Gera identidade ed25519 na primeira execução
85
+ 2. Escuta em uma porta TCP
86
+ 3. Anuncia capacidades (ex: "analyst", "web-search")
87
+ 4. Descobre outros nós via tracker compartilhado ou peers manuais
88
+ 5. Troca mensagens assinadas (tasks, results, events)
89
+
90
+ ## Compatibilidade
91
+
92
+ - Python 3.11+
93
+ - Linux, macOS, Windows
94
+
95
+ ## Desenvolvimento
96
+
97
+ ```bash
98
+ git clone https://github.com/xalq/elo
99
+ cd elo/py
100
+ pip install -e ".[dev]"
101
+ pytest
102
+ ```
103
+
104
+ ## Projetos Relacionados
105
+
106
+ - [Hermes Agent](https://hermes-agent.nousresearch.com) — runtime de agentes autônomos
107
+ - [Honcho](https://github.com/argmax-inc/honcho) — memória persistente para agentes
@@ -0,0 +1,15 @@
1
+ elo/__init__.py,sha256=2C5vFJ_VprJtHJU-hIjyjF3ZJ7A_A-UX_YKz-XnZuEg,756
2
+ elo/__main__.py,sha256=YMQli0HxHjng28aZJ48lafLjcDD6hEBmnr9B0KeNjVM,4418
3
+ elo/node.py,sha256=wInHu_gPUAMBNR2Pdp1B2We9ynxdUgSYpZXnLo_LLEo,19965
4
+ elo/security.py,sha256=fdrWtFVViGDf2SXWv_1O3OK4MKYBz83oxBxnyk5Xkkc,6096
5
+ elo/types.py,sha256=anbfTA1SagT3ARyYdLzc5yj-knAaIjSlY45z40vTfkI,3344
6
+ elo/transport/__init__.py,sha256=ZAPtJiG1wyuJv3mBhD305QlK59HHJ-vNnfLBmuQ2_jo,1012
7
+ elo/transport/protocol.py,sha256=T7O69vmdV1HB1bgq-j3-6n1ZmU1GyU0KCEQPG3ovhLk,4781
8
+ elo/transport/routing.py,sha256=Yfn2goRHwHKdaPHHV3IS-dug6lSG651cbSsoyeDSFSc,5142
9
+ elo/transport/tcp.py,sha256=O4J4ivs1r4oL8h3MTxwEhjPs_Ec2u_sRfYQESL1im0Q,11211
10
+ elo/transport/tracker.py,sha256=vIs_VVlJgWzTJBRkb81a28L0kX6NpHcnbIptUT6YBFM,3497
11
+ elo_node-0.4.0.dist-info/METADATA,sha256=wQDKUgyQWqEiUVq6UZw-Z39sdJaGyTu0c7UlckNc8sA,3615
12
+ elo_node-0.4.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
13
+ elo_node-0.4.0.dist-info/entry_points.txt,sha256=AwKAQRa5DkVuiU0137P1fyZ6_0Q_7Dwp_nhNngoUIRI,42
14
+ elo_node-0.4.0.dist-info/top_level.txt,sha256=krnAob5lCi3ezo5jJ0hZx5fZcVFQfTGwEG1kdpKCrjM,4
15
+ elo_node-0.4.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ elo = elo.__main__:main
@@ -0,0 +1 @@
1
+ elo