konet 0.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- konet-0.1.3/PKG-INFO +43 -0
- konet-0.1.3/README.md +31 -0
- konet-0.1.3/konet/__init__.py +25 -0
- konet-0.1.3/konet/channel.py +86 -0
- konet-0.1.3/konet/client.py +148 -0
- konet-0.1.3/konet.egg-info/PKG-INFO +43 -0
- konet-0.1.3/konet.egg-info/SOURCES.txt +10 -0
- konet-0.1.3/konet.egg-info/dependency_links.txt +1 -0
- konet-0.1.3/konet.egg-info/requires.txt +5 -0
- konet-0.1.3/konet.egg-info/top_level.txt +1 -0
- konet-0.1.3/pyproject.toml +21 -0
- konet-0.1.3/setup.cfg +4 -0
konet-0.1.3/PKG-INFO
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: konet
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Python SDK for Konet realtime infrastructure
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: websockets>=12.0
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
11
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
12
|
+
|
|
13
|
+
# konet (Python SDK)
|
|
14
|
+
|
|
15
|
+
Async Python SDK for [Konet](https://github.com/raucheacho/konet), the
|
|
16
|
+
self-hosted realtime engine (channels, presence, broadcast).
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install konet
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import asyncio
|
|
28
|
+
from konet import KonetClient
|
|
29
|
+
|
|
30
|
+
async def main():
|
|
31
|
+
async with KonetClient("ws://localhost:4000/socket", token="<your-token>") as client:
|
|
32
|
+
channel = client.channel("room:lobby")
|
|
33
|
+
await channel.subscribe()
|
|
34
|
+
channel.on("message", lambda payload: print(payload))
|
|
35
|
+
await channel.send("message", {"text": "Hello!"})
|
|
36
|
+
await asyncio.sleep(60)
|
|
37
|
+
|
|
38
|
+
asyncio.run(main())
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## License
|
|
42
|
+
|
|
43
|
+
MIT
|
konet-0.1.3/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# konet (Python SDK)
|
|
2
|
+
|
|
3
|
+
Async Python SDK for [Konet](https://github.com/raucheacho/konet), the
|
|
4
|
+
self-hosted realtime engine (channels, presence, broadcast).
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install konet
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
import asyncio
|
|
16
|
+
from konet import KonetClient
|
|
17
|
+
|
|
18
|
+
async def main():
|
|
19
|
+
async with KonetClient("ws://localhost:4000/socket", token="<your-token>") as client:
|
|
20
|
+
channel = client.channel("room:lobby")
|
|
21
|
+
await channel.subscribe()
|
|
22
|
+
channel.on("message", lambda payload: print(payload))
|
|
23
|
+
await channel.send("message", {"text": "Hello!"})
|
|
24
|
+
await asyncio.sleep(60)
|
|
25
|
+
|
|
26
|
+
asyncio.run(main())
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## License
|
|
30
|
+
|
|
31
|
+
MIT
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
konet — Python SDK for Konet realtime infrastructure.
|
|
3
|
+
|
|
4
|
+
Quick start::
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
from konet import KonetClient
|
|
8
|
+
|
|
9
|
+
async def main():
|
|
10
|
+
async with KonetClient("ws://localhost:4000/socket", token="kt_anon_...") as client:
|
|
11
|
+
channel = client.channel("room:lobby")
|
|
12
|
+
await channel.subscribe()
|
|
13
|
+
|
|
14
|
+
channel.on("message", lambda p: print("received:", p))
|
|
15
|
+
await channel.send("message", {"text": "Hello from Python!"})
|
|
16
|
+
await asyncio.sleep(60)
|
|
17
|
+
|
|
18
|
+
asyncio.run(main())
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from .client import KonetClient
|
|
22
|
+
from .channel import Channel
|
|
23
|
+
|
|
24
|
+
__all__ = ["KonetClient", "Channel"]
|
|
25
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from typing import Any, Callable, Awaitable
|
|
7
|
+
|
|
8
|
+
EventHandler = Callable[[Any], Awaitable[None] | None]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Channel:
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
topic: str,
|
|
15
|
+
send_fn: Callable,
|
|
16
|
+
next_ref: Callable[[], str],
|
|
17
|
+
) -> None:
|
|
18
|
+
self.topic = topic
|
|
19
|
+
self._state: str = "idle" # idle | joining | joined | errored
|
|
20
|
+
self._send_fn = send_fn
|
|
21
|
+
self._next_ref = next_ref
|
|
22
|
+
self._join_ref: str | None = None
|
|
23
|
+
self._handlers: dict[str, list[EventHandler]] = defaultdict(list)
|
|
24
|
+
self._reply_futures: dict[str, asyncio.Future] = {}
|
|
25
|
+
|
|
26
|
+
async def subscribe(self) -> None:
|
|
27
|
+
if self._state in ("joined", "joining"):
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
self._state = "joining"
|
|
31
|
+
ref = self._next_ref()
|
|
32
|
+
self._join_ref = ref
|
|
33
|
+
|
|
34
|
+
loop = asyncio.get_event_loop()
|
|
35
|
+
fut: asyncio.Future = loop.create_future()
|
|
36
|
+
self._reply_futures[ref] = fut
|
|
37
|
+
|
|
38
|
+
await self._send_fn([ref, ref, self.topic, "phx_join", {}])
|
|
39
|
+
|
|
40
|
+
reply = await asyncio.wait_for(fut, timeout=10.0)
|
|
41
|
+
if reply.get("status") == "ok":
|
|
42
|
+
self._state = "joined"
|
|
43
|
+
else:
|
|
44
|
+
self._state = "errored"
|
|
45
|
+
raise RuntimeError(f"Failed to join {self.topic}: {reply.get('response')}")
|
|
46
|
+
|
|
47
|
+
async def unsubscribe(self) -> None:
|
|
48
|
+
if self._state != "joined":
|
|
49
|
+
return
|
|
50
|
+
ref = self._next_ref()
|
|
51
|
+
self._state = "idle"
|
|
52
|
+
await self._send_fn([self._join_ref, ref, self.topic, "phx_leave", {}])
|
|
53
|
+
self._join_ref = None
|
|
54
|
+
|
|
55
|
+
def on(self, event: str, handler: EventHandler) -> Callable[[], None]:
|
|
56
|
+
"""Register an event handler. Returns an unsubscribe callable."""
|
|
57
|
+
self._handlers[event].append(handler)
|
|
58
|
+
|
|
59
|
+
def off() -> None:
|
|
60
|
+
self._handlers[event].remove(handler)
|
|
61
|
+
|
|
62
|
+
return off
|
|
63
|
+
|
|
64
|
+
async def send(self, event: str, payload: Any = None) -> None:
|
|
65
|
+
if self._state != "joined":
|
|
66
|
+
raise RuntimeError(f"Channel {self.topic} is not joined")
|
|
67
|
+
|
|
68
|
+
ref = self._next_ref()
|
|
69
|
+
await self._send_fn(
|
|
70
|
+
[self._join_ref, ref, self.topic, "broadcast", {"event": event, "payload": payload or {}}]
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def _receive(self, frame: list) -> None:
|
|
74
|
+
"""Called by the client when a message arrives for this topic."""
|
|
75
|
+
_join_ref, ref, _topic, event, payload = frame
|
|
76
|
+
|
|
77
|
+
if event == "phx_reply":
|
|
78
|
+
fut = self._reply_futures.pop(ref, None)
|
|
79
|
+
if fut and not fut.done():
|
|
80
|
+
fut.set_result(payload if isinstance(payload, dict) else {})
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
for handler in list(self._handlers.get(event, [])):
|
|
84
|
+
result = handler(payload)
|
|
85
|
+
if asyncio.iscoroutine(result):
|
|
86
|
+
asyncio.create_task(result)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import math
|
|
6
|
+
from typing import Any
|
|
7
|
+
from urllib.parse import urlencode
|
|
8
|
+
|
|
9
|
+
import websockets
|
|
10
|
+
from websockets.exceptions import ConnectionClosed, WebSocketException
|
|
11
|
+
|
|
12
|
+
from .channel import Channel
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class KonetClient:
|
|
16
|
+
"""
|
|
17
|
+
Async Konet client.
|
|
18
|
+
|
|
19
|
+
Usage::
|
|
20
|
+
|
|
21
|
+
async with KonetClient("ws://localhost:4000/socket", token="kt_anon_...") as client:
|
|
22
|
+
channel = client.channel("room:lobby")
|
|
23
|
+
await channel.subscribe()
|
|
24
|
+
channel.on("message", lambda p: print(p))
|
|
25
|
+
await channel.send("message", {"text": "Hello!"})
|
|
26
|
+
await asyncio.sleep(60)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
url: str,
|
|
32
|
+
*,
|
|
33
|
+
token: str,
|
|
34
|
+
heartbeat_interval: float = 30.0,
|
|
35
|
+
reconnect_delay: float = 1.0,
|
|
36
|
+
max_reconnect_tries: int = 10,
|
|
37
|
+
) -> None:
|
|
38
|
+
self._url = url
|
|
39
|
+
self._token = token
|
|
40
|
+
self._heartbeat_interval = heartbeat_interval
|
|
41
|
+
self._reconnect_delay = reconnect_delay
|
|
42
|
+
self._max_reconnect_tries = max_reconnect_tries
|
|
43
|
+
|
|
44
|
+
self._ws: websockets.ClientConnection | None = None
|
|
45
|
+
self._channels: dict[str, Channel] = {}
|
|
46
|
+
self._ref_counter = 0
|
|
47
|
+
self._connected = False
|
|
48
|
+
self._tasks: list[asyncio.Task] = []
|
|
49
|
+
|
|
50
|
+
async def __aenter__(self) -> "KonetClient":
|
|
51
|
+
await self.connect()
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
async def __aexit__(self, *_: Any) -> None:
|
|
55
|
+
await self.disconnect()
|
|
56
|
+
|
|
57
|
+
def _websocket_url(self) -> str:
|
|
58
|
+
# Phoenix mounts the actual websocket transport at "<socket path>/websocket",
|
|
59
|
+
# not at the socket path itself (e.g. "/socket" -> "/socket/websocket").
|
|
60
|
+
base = self._url.rstrip("/")
|
|
61
|
+
params = urlencode({"token": self._token, "vsn": "2.0.0"})
|
|
62
|
+
return f"{base}/websocket?{params}"
|
|
63
|
+
|
|
64
|
+
async def connect(self) -> None:
|
|
65
|
+
self._ws = await websockets.connect(self._websocket_url())
|
|
66
|
+
self._connected = True
|
|
67
|
+
|
|
68
|
+
self._tasks = [
|
|
69
|
+
asyncio.create_task(self._read_loop()),
|
|
70
|
+
asyncio.create_task(self._heartbeat_loop()),
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
async def disconnect(self) -> None:
|
|
74
|
+
self._connected = False
|
|
75
|
+
for task in self._tasks:
|
|
76
|
+
task.cancel()
|
|
77
|
+
if self._ws:
|
|
78
|
+
await self._ws.close()
|
|
79
|
+
self._ws = None
|
|
80
|
+
|
|
81
|
+
def channel(self, topic: str) -> Channel:
|
|
82
|
+
if topic not in self._channels:
|
|
83
|
+
self._channels[topic] = Channel(topic, self._send, self._next_ref)
|
|
84
|
+
return self._channels[topic]
|
|
85
|
+
|
|
86
|
+
async def _send(self, frame: list) -> None:
|
|
87
|
+
if self._ws is None:
|
|
88
|
+
raise RuntimeError("Not connected")
|
|
89
|
+
await self._ws.send(json.dumps(frame))
|
|
90
|
+
|
|
91
|
+
def _next_ref(self) -> str:
|
|
92
|
+
self._ref_counter += 1
|
|
93
|
+
return str(self._ref_counter)
|
|
94
|
+
|
|
95
|
+
async def _read_loop(self) -> None:
|
|
96
|
+
reconnect_attempts = 0
|
|
97
|
+
while self._connected:
|
|
98
|
+
try:
|
|
99
|
+
if self._ws is None:
|
|
100
|
+
await asyncio.sleep(0.1)
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
raw = await self._ws.recv()
|
|
104
|
+
frame = json.loads(raw)
|
|
105
|
+
|
|
106
|
+
if not isinstance(frame, list) or len(frame) != 5:
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
_join_ref, _ref, topic, event, _payload = frame
|
|
110
|
+
|
|
111
|
+
if topic == "phoenix":
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
ch = self._channels.get(topic)
|
|
115
|
+
if ch:
|
|
116
|
+
ch._receive(frame)
|
|
117
|
+
|
|
118
|
+
reconnect_attempts = 0
|
|
119
|
+
|
|
120
|
+
except ConnectionClosed:
|
|
121
|
+
if not self._connected:
|
|
122
|
+
break
|
|
123
|
+
if reconnect_attempts >= self._max_reconnect_tries:
|
|
124
|
+
break
|
|
125
|
+
delay = min(self._reconnect_delay * (2 ** reconnect_attempts), 30.0)
|
|
126
|
+
reconnect_attempts += 1
|
|
127
|
+
await asyncio.sleep(delay)
|
|
128
|
+
await self._reconnect()
|
|
129
|
+
|
|
130
|
+
except asyncio.CancelledError:
|
|
131
|
+
break
|
|
132
|
+
|
|
133
|
+
async def _heartbeat_loop(self) -> None:
|
|
134
|
+
while self._connected:
|
|
135
|
+
try:
|
|
136
|
+
await asyncio.sleep(self._heartbeat_interval)
|
|
137
|
+
ref = self._next_ref()
|
|
138
|
+
await self._send([None, ref, "phoenix", "heartbeat", {}])
|
|
139
|
+
except asyncio.CancelledError:
|
|
140
|
+
break
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
async def _reconnect(self) -> None:
|
|
145
|
+
try:
|
|
146
|
+
self._ws = await websockets.connect(self._websocket_url())
|
|
147
|
+
except Exception:
|
|
148
|
+
self._ws = None
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: konet
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Python SDK for Konet realtime infrastructure
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: websockets>=12.0
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
11
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
12
|
+
|
|
13
|
+
# konet (Python SDK)
|
|
14
|
+
|
|
15
|
+
Async Python SDK for [Konet](https://github.com/raucheacho/konet), the
|
|
16
|
+
self-hosted realtime engine (channels, presence, broadcast).
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install konet
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import asyncio
|
|
28
|
+
from konet import KonetClient
|
|
29
|
+
|
|
30
|
+
async def main():
|
|
31
|
+
async with KonetClient("ws://localhost:4000/socket", token="<your-token>") as client:
|
|
32
|
+
channel = client.channel("room:lobby")
|
|
33
|
+
await channel.subscribe()
|
|
34
|
+
channel.on("message", lambda payload: print(payload))
|
|
35
|
+
await channel.send("message", {"text": "Hello!"})
|
|
36
|
+
await asyncio.sleep(60)
|
|
37
|
+
|
|
38
|
+
asyncio.run(main())
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## License
|
|
42
|
+
|
|
43
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
konet
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "konet"
|
|
7
|
+
version = "0.1.3"
|
|
8
|
+
description = "Python SDK for Konet realtime infrastructure"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"websockets>=12.0",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
dev = ["pytest>=8.0", "pytest-asyncio>=0.23"]
|
|
18
|
+
|
|
19
|
+
[tool.setuptools.packages.find]
|
|
20
|
+
where = ["."]
|
|
21
|
+
include = ["konet*"]
|
konet-0.1.3/setup.cfg
ADDED