ephaptic 0.1.0__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.
- ephaptic-0.1.0/LICENSE +21 -0
- ephaptic-0.1.0/PKG-INFO +188 -0
- ephaptic-0.1.0/README.md +149 -0
- ephaptic-0.1.0/ephaptic/__init__.py +7 -0
- ephaptic-0.1.0/ephaptic/adapters/__init__.py +0 -0
- ephaptic-0.1.0/ephaptic/adapters/fastapi_.py +30 -0
- ephaptic-0.1.0/ephaptic/adapters/quart_.py +16 -0
- ephaptic-0.1.0/ephaptic/client/__init__.py +3 -0
- ephaptic-0.1.0/ephaptic/client/client.py +114 -0
- ephaptic-0.1.0/ephaptic/ephaptic.py +200 -0
- ephaptic-0.1.0/ephaptic/localproxy.py +557 -0
- ephaptic-0.1.0/ephaptic/transports/__init__.py +5 -0
- ephaptic-0.1.0/ephaptic/transports/fastapi_ws.py +8 -0
- ephaptic-0.1.0/ephaptic/transports/websocket.py +11 -0
- ephaptic-0.1.0/ephaptic.egg-info/PKG-INFO +188 -0
- ephaptic-0.1.0/ephaptic.egg-info/SOURCES.txt +19 -0
- ephaptic-0.1.0/ephaptic.egg-info/dependency_links.txt +1 -0
- ephaptic-0.1.0/ephaptic.egg-info/requires.txt +5 -0
- ephaptic-0.1.0/ephaptic.egg-info/top_level.txt +1 -0
- ephaptic-0.1.0/pyproject.toml +27 -0
- ephaptic-0.1.0/setup.cfg +4 -0
ephaptic-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 ephaptic
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ephaptic-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ephaptic
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The Python client/server package for ephaptic.
|
|
5
|
+
Author-email: uukelele <robustrobot11@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 ephaptic
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/ephaptic/ephaptic
|
|
29
|
+
Project-URL: Repository, https://github.com/ephaptic/ephaptic
|
|
30
|
+
Project-URL: Issue Tracker, https://github.com/ephaptic/ephaptic/issues
|
|
31
|
+
Requires-Python: >=3.10
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
License-File: LICENSE
|
|
34
|
+
Requires-Dist: msgpack>=1.0.0
|
|
35
|
+
Requires-Dist: websockets>=12.0
|
|
36
|
+
Provides-Extra: server
|
|
37
|
+
Requires-Dist: redis; extra == "server"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
<div align="center">
|
|
41
|
+
<a href="https://github.com/ephaptic/ephaptic">
|
|
42
|
+
<picture>
|
|
43
|
+
<img src="https://raw.githubusercontent.com/ephaptic/ephaptic/refs/heads/main/.github/assets/logo.png" alt="ephaptic logo" height="200">
|
|
44
|
+
<!-- <img src="https://avatars.githubusercontent.com/u/248199226?s=256" alt="ephaptic logo" height="200> -->
|
|
45
|
+
</picture>
|
|
46
|
+
</a>
|
|
47
|
+
<br>
|
|
48
|
+
<h1>ephaptic</h1>
|
|
49
|
+
<br>
|
|
50
|
+
<a href="https://github.com/ephaptic/ephaptic/blob/main/LICENSE"><img alt="GitHub License" src="https://img.shields.io/github/license/ephaptic/ephaptic?style=for-the-badge&labelColor=%23222222"></a> <img alt="GitHub Actions Workflow Status" src="https://img.shields.io/github/actions/workflow/status/ephaptic/ephaptic/publish-js.yml?style=for-the-badge&label=NPM%20Build%20Status&labelColor=%23222222"> <img alt="GitHub Actions Workflow Status" src="https://img.shields.io/github/actions/workflow/status/ephaptic/ephaptic/publish-python.yml?style=for-the-badge&label=PyPI%20Build%20Status&labelColor=%23222222">
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
## What is `ephaptic`?
|
|
56
|
+
|
|
57
|
+
<br>
|
|
58
|
+
|
|
59
|
+
<blockquote>
|
|
60
|
+
<b>ephaptic (adj.)</b><br>
|
|
61
|
+
electrical conduction of a nerve impulse across an ephapse without the mediation of a neurotransmitter.
|
|
62
|
+
</blockquote>
|
|
63
|
+
|
|
64
|
+
Nah, just kidding. It's an RPC framework.
|
|
65
|
+
|
|
66
|
+
> **ephaptic** — Call your backend straight from your frontend. No JSON. No latency. No middleware.
|
|
67
|
+
|
|
68
|
+
## Getting Started
|
|
69
|
+
|
|
70
|
+
- Ephaptic is designed to be invisible. Write a function on the server, call it on the client. No extra boilerplate.
|
|
71
|
+
|
|
72
|
+
- Plus, it's horizontally scalable with Redis (optional), and features extremely low latency thanks to [msgpack](https://github.com/msgpack).
|
|
73
|
+
|
|
74
|
+
- Oh, and the client can also listen to events broadcasted by the server. No, like literally. You just need to add an `eventListener`. Did I mention? Events can be sent to specific targets, specific users - not just anyone online.
|
|
75
|
+
|
|
76
|
+
What are you waiting for? **Let's go.**
|
|
77
|
+
|
|
78
|
+
<details>
|
|
79
|
+
<summary>Python</summary>
|
|
80
|
+
|
|
81
|
+
#### Client:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
pip install ephaptic
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
#### Server:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
pip install ephaptic[server]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from fastapi import FastAPI # or `from quart import Quart`
|
|
95
|
+
from ephaptic import Ephaptic
|
|
96
|
+
|
|
97
|
+
app = FastAPI() # or `app = Quart(__name__)`
|
|
98
|
+
|
|
99
|
+
ephaptic = Ephaptic.from_app(app) # Finds which framework you're using, and creates an ephaptic server.
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
You can also specify a custom path:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
ephaptic = Ephaptic.from_app(app, path="/websocket")
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
And you can even use Redis for horizontal scaling!
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
ephaptic = Ephaptic.from_app(app, redis_url="redis://my-redis-container:6379/0")
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Now, how do you expose your function to the frontend?
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
@ephaptic.expose
|
|
118
|
+
async def add(num1, num2):
|
|
119
|
+
return num1 + num2
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Yep, it's really that simple.
|
|
123
|
+
|
|
124
|
+
But what if your code throws an error? No sweat, it just throws up on the frontend with the same details.
|
|
125
|
+
|
|
126
|
+
And, want to say something to the frontend?
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
await ephaptic.to(user1, user2).notification("Hello, world!", priority="high")
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
</details>
|
|
134
|
+
|
|
135
|
+
<details>
|
|
136
|
+
<summary>JavaScript/TypeScript — Browser (Svelt, React, Angular, Vite, etc.)</summary>
|
|
137
|
+
|
|
138
|
+
#### To use with a framework / Vite:
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
npm install @ephaptic/client
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Then:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import { connect } from "@ephaptic/client";
|
|
148
|
+
|
|
149
|
+
const client = connect(); // Defaults to `/_ephaptic`.
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Or, you can use it with a custom URL:
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
const client = connect({ url: '/ws' });
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
const client = connect({ url: 'wss://my-backend.deployment/ephaptic' });
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
You can even send auth objects to the server for identity loading.
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
const client = connect({ url: '...', auth: { token: window.localStorage.getItem('jwtToken') } })
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
#### Or, to use in your browser:
|
|
169
|
+
|
|
170
|
+
```html
|
|
171
|
+
<script type="module">
|
|
172
|
+
import { connect } from 'https://cdn.jsdelivr.net/npm/@ephaptic/client@latest/+esm';
|
|
173
|
+
|
|
174
|
+
const client = connect();
|
|
175
|
+
</script>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
<!-- TODO: Add extended documentation -->
|
|
179
|
+
|
|
180
|
+
</details>
|
|
181
|
+
|
|
182
|
+
## [License](https://github.com/ephaptic/ephaptic/blob/main/LICENSE)
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
<p align="center">
|
|
187
|
+
© ephaptic 2025
|
|
188
|
+
</p>
|
ephaptic-0.1.0/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<a href="https://github.com/ephaptic/ephaptic">
|
|
3
|
+
<picture>
|
|
4
|
+
<img src="https://raw.githubusercontent.com/ephaptic/ephaptic/refs/heads/main/.github/assets/logo.png" alt="ephaptic logo" height="200">
|
|
5
|
+
<!-- <img src="https://avatars.githubusercontent.com/u/248199226?s=256" alt="ephaptic logo" height="200> -->
|
|
6
|
+
</picture>
|
|
7
|
+
</a>
|
|
8
|
+
<br>
|
|
9
|
+
<h1>ephaptic</h1>
|
|
10
|
+
<br>
|
|
11
|
+
<a href="https://github.com/ephaptic/ephaptic/blob/main/LICENSE"><img alt="GitHub License" src="https://img.shields.io/github/license/ephaptic/ephaptic?style=for-the-badge&labelColor=%23222222"></a> <img alt="GitHub Actions Workflow Status" src="https://img.shields.io/github/actions/workflow/status/ephaptic/ephaptic/publish-js.yml?style=for-the-badge&label=NPM%20Build%20Status&labelColor=%23222222"> <img alt="GitHub Actions Workflow Status" src="https://img.shields.io/github/actions/workflow/status/ephaptic/ephaptic/publish-python.yml?style=for-the-badge&label=PyPI%20Build%20Status&labelColor=%23222222">
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
## What is `ephaptic`?
|
|
17
|
+
|
|
18
|
+
<br>
|
|
19
|
+
|
|
20
|
+
<blockquote>
|
|
21
|
+
<b>ephaptic (adj.)</b><br>
|
|
22
|
+
electrical conduction of a nerve impulse across an ephapse without the mediation of a neurotransmitter.
|
|
23
|
+
</blockquote>
|
|
24
|
+
|
|
25
|
+
Nah, just kidding. It's an RPC framework.
|
|
26
|
+
|
|
27
|
+
> **ephaptic** — Call your backend straight from your frontend. No JSON. No latency. No middleware.
|
|
28
|
+
|
|
29
|
+
## Getting Started
|
|
30
|
+
|
|
31
|
+
- Ephaptic is designed to be invisible. Write a function on the server, call it on the client. No extra boilerplate.
|
|
32
|
+
|
|
33
|
+
- Plus, it's horizontally scalable with Redis (optional), and features extremely low latency thanks to [msgpack](https://github.com/msgpack).
|
|
34
|
+
|
|
35
|
+
- Oh, and the client can also listen to events broadcasted by the server. No, like literally. You just need to add an `eventListener`. Did I mention? Events can be sent to specific targets, specific users - not just anyone online.
|
|
36
|
+
|
|
37
|
+
What are you waiting for? **Let's go.**
|
|
38
|
+
|
|
39
|
+
<details>
|
|
40
|
+
<summary>Python</summary>
|
|
41
|
+
|
|
42
|
+
#### Client:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
pip install ephaptic
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
#### Server:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
pip install ephaptic[server]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from fastapi import FastAPI # or `from quart import Quart`
|
|
56
|
+
from ephaptic import Ephaptic
|
|
57
|
+
|
|
58
|
+
app = FastAPI() # or `app = Quart(__name__)`
|
|
59
|
+
|
|
60
|
+
ephaptic = Ephaptic.from_app(app) # Finds which framework you're using, and creates an ephaptic server.
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
You can also specify a custom path:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
ephaptic = Ephaptic.from_app(app, path="/websocket")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
And you can even use Redis for horizontal scaling!
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
ephaptic = Ephaptic.from_app(app, redis_url="redis://my-redis-container:6379/0")
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Now, how do you expose your function to the frontend?
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
@ephaptic.expose
|
|
79
|
+
async def add(num1, num2):
|
|
80
|
+
return num1 + num2
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Yep, it's really that simple.
|
|
84
|
+
|
|
85
|
+
But what if your code throws an error? No sweat, it just throws up on the frontend with the same details.
|
|
86
|
+
|
|
87
|
+
And, want to say something to the frontend?
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
await ephaptic.to(user1, user2).notification("Hello, world!", priority="high")
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
</details>
|
|
95
|
+
|
|
96
|
+
<details>
|
|
97
|
+
<summary>JavaScript/TypeScript — Browser (Svelt, React, Angular, Vite, etc.)</summary>
|
|
98
|
+
|
|
99
|
+
#### To use with a framework / Vite:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
npm install @ephaptic/client
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Then:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { connect } from "@ephaptic/client";
|
|
109
|
+
|
|
110
|
+
const client = connect(); // Defaults to `/_ephaptic`.
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Or, you can use it with a custom URL:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
const client = connect({ url: '/ws' });
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
const client = connect({ url: 'wss://my-backend.deployment/ephaptic' });
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
You can even send auth objects to the server for identity loading.
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
const client = connect({ url: '...', auth: { token: window.localStorage.getItem('jwtToken') } })
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
#### Or, to use in your browser:
|
|
130
|
+
|
|
131
|
+
```html
|
|
132
|
+
<script type="module">
|
|
133
|
+
import { connect } from 'https://cdn.jsdelivr.net/npm/@ephaptic/client@latest/+esm';
|
|
134
|
+
|
|
135
|
+
const client = connect();
|
|
136
|
+
</script>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
<!-- TODO: Add extended documentation -->
|
|
140
|
+
|
|
141
|
+
</details>
|
|
142
|
+
|
|
143
|
+
## [License](https://github.com/ephaptic/ephaptic/blob/main/LICENSE)
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
<p align="center">
|
|
148
|
+
© ephaptic 2025
|
|
149
|
+
</p>
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from fastapi import FastAPI, WebSocket
|
|
2
|
+
from ..transports.fastapi_ws import FastAPIWebSocketTransport
|
|
3
|
+
|
|
4
|
+
class FastAPIAdapter:
|
|
5
|
+
def __init__(self, ephaptic, app: FastAPI, path, manager):
|
|
6
|
+
self.ephaptic = ephaptic
|
|
7
|
+
|
|
8
|
+
@app.websocket(path)
|
|
9
|
+
async def ephaptic_ws(websocket: WebSocket):
|
|
10
|
+
await websocket.accept()
|
|
11
|
+
transport = FastAPIWebSocketTransport(websocket)
|
|
12
|
+
await self.ephaptic.handle_transport(transport)
|
|
13
|
+
|
|
14
|
+
if manager.redis:
|
|
15
|
+
lifespan = app.router.lifespan_context
|
|
16
|
+
|
|
17
|
+
from contextlib import asynccontextmanager
|
|
18
|
+
import asyncio
|
|
19
|
+
|
|
20
|
+
@asynccontextmanager
|
|
21
|
+
async def ephaptic_lifespan_wrapper(app):
|
|
22
|
+
asyncio.create_task(manager.start_redis())
|
|
23
|
+
|
|
24
|
+
if lifespan:
|
|
25
|
+
async with lifespan(app) as state:
|
|
26
|
+
yield state
|
|
27
|
+
else:
|
|
28
|
+
yield
|
|
29
|
+
|
|
30
|
+
app.router.lifespan_context = ephaptic_lifespan_wrapper
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from quart import websocket, Quart
|
|
2
|
+
from ..transports.websocket import WebSocketTransport
|
|
3
|
+
|
|
4
|
+
class QuartAdapter:
|
|
5
|
+
def __init__(self, ephaptic, app: Quart, path, manager):
|
|
6
|
+
self.ephaptic = ephaptic
|
|
7
|
+
|
|
8
|
+
@app.websocket(path)
|
|
9
|
+
async def ephaptic_ws():
|
|
10
|
+
transport = WebSocketTransport(websocket)
|
|
11
|
+
await self.ephaptic.handle_transport(transport)
|
|
12
|
+
|
|
13
|
+
if manager.redis:
|
|
14
|
+
@app.before_serving
|
|
15
|
+
async def start_redis():
|
|
16
|
+
app.add_background_task(manager.start_redis)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import msgpack
|
|
3
|
+
import websockets
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from typing import Callable, Any
|
|
7
|
+
import inspect
|
|
8
|
+
|
|
9
|
+
class EphapticClient:
|
|
10
|
+
def __init__(self, url: str, auth = None):
|
|
11
|
+
self.url = url
|
|
12
|
+
self.auth = auth
|
|
13
|
+
self.ws = None
|
|
14
|
+
self._call_id = 0
|
|
15
|
+
self._pending_calls = {} # id -> asyncio.Future (asyncio.Future is a Python equivalent of a Promise)
|
|
16
|
+
self._event_handlers = {} # name: str -> Set(Callable)
|
|
17
|
+
self._listen_task = None
|
|
18
|
+
|
|
19
|
+
def _async(self, func: Callable):
|
|
20
|
+
async def wrapper(*args, **kwargs) -> Any:
|
|
21
|
+
if inspect.iscoroutinefunction(func):
|
|
22
|
+
return await func(*args, **kwargs)
|
|
23
|
+
else:
|
|
24
|
+
return await asyncio.to_thread(func, *args, **kwargs)
|
|
25
|
+
return wrapper
|
|
26
|
+
|
|
27
|
+
async def connect(self):
|
|
28
|
+
if self.ws: return
|
|
29
|
+
|
|
30
|
+
self.ws = await websockets.connect(self.url)
|
|
31
|
+
|
|
32
|
+
payload = {"type": "init"}
|
|
33
|
+
if self.auth: payload["auth"] = self.auth
|
|
34
|
+
|
|
35
|
+
await self.ws.send(msgpack.dumps(payload))
|
|
36
|
+
|
|
37
|
+
self._listen_task = asyncio.create_task(self._listener())
|
|
38
|
+
|
|
39
|
+
async def _listener(self):
|
|
40
|
+
try:
|
|
41
|
+
async for message in self.ws:
|
|
42
|
+
data = msgpack.loads(message)
|
|
43
|
+
|
|
44
|
+
if data.get('id') is not None:
|
|
45
|
+
call_id = data['id']
|
|
46
|
+
if call_id in self._pending_calls:
|
|
47
|
+
future = self._pending_calls.pop(call_id)
|
|
48
|
+
if 'error' in data:
|
|
49
|
+
future.set_exception(Exception(data['error']))
|
|
50
|
+
else:
|
|
51
|
+
future.set_result(data.get('result'))
|
|
52
|
+
|
|
53
|
+
elif data.get('type') == 'event':
|
|
54
|
+
name = data['name']
|
|
55
|
+
payload = data.get('payload', {})
|
|
56
|
+
args = payload.get('args', [])
|
|
57
|
+
kwargs = payload.get('kwargs', {})
|
|
58
|
+
|
|
59
|
+
if name in self._event_handlers:
|
|
60
|
+
for handler in self._event_handlers[name]:
|
|
61
|
+
try:
|
|
62
|
+
asyncio.create_task(self._async(handler)(*args, **kwargs))
|
|
63
|
+
# We don't await it, we want to execute all handlers in parallel.
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logging.error(f"Error in event handler {name}: {e}")
|
|
66
|
+
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logging.error(f"Connection error: {e}")
|
|
69
|
+
|
|
70
|
+
def on(self, event_name, func):
|
|
71
|
+
if event_name not in self._event_handlers: self._event_handlers[event_name] = set()
|
|
72
|
+
self._event_handlers[event_name].add(func)
|
|
73
|
+
|
|
74
|
+
def off(self, event_name, func):
|
|
75
|
+
if event_name not in self._event_handlers: return
|
|
76
|
+
s = self._event_handlers[event_name]
|
|
77
|
+
s.discard(func)
|
|
78
|
+
if not s: del self._event_handlers[event_name]
|
|
79
|
+
|
|
80
|
+
def once(self, event_name, func):
|
|
81
|
+
async def wrapper(*args, **kwargs):
|
|
82
|
+
self.off(event_name, wrapper)
|
|
83
|
+
func(*args, **kwargs)
|
|
84
|
+
self.on(event_name, wrapper)
|
|
85
|
+
|
|
86
|
+
def __getattr__(self, name):
|
|
87
|
+
async def remote_call(*args, **kwargs):
|
|
88
|
+
if not self.ws: await self.connect()
|
|
89
|
+
|
|
90
|
+
self._call_id += 1
|
|
91
|
+
call_id = self._call_id
|
|
92
|
+
|
|
93
|
+
future = asyncio.Future()
|
|
94
|
+
self._pending_calls[call_id] = future
|
|
95
|
+
|
|
96
|
+
payload = {
|
|
97
|
+
"type": "rpc",
|
|
98
|
+
"id": call_id,
|
|
99
|
+
"name": name,
|
|
100
|
+
"args": args,
|
|
101
|
+
"kwargs": kwargs,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
await self.ws.send(msgpack.dumps(payload))
|
|
105
|
+
return await future
|
|
106
|
+
|
|
107
|
+
return remote_call
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
async def connect(url: str = "ws://localhost:8000/_ephaptic", auth = None):
|
|
112
|
+
client = EphapticClient(url, auth)
|
|
113
|
+
await client.connect()
|
|
114
|
+
return client
|