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 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.
@@ -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
+ &copy; ephaptic 2025
188
+ </p>
@@ -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
+ &copy; ephaptic 2025
149
+ </p>
@@ -0,0 +1,7 @@
1
+ from .ephaptic import (
2
+ Ephaptic,
3
+ )
4
+
5
+ from .client import (
6
+ connect
7
+ )
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,3 @@
1
+ from .client import (
2
+ connect
3
+ )
@@ -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