ephaptic 0.2.2__py3-none-any.whl → 0.2.4__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.
- ephaptic/__init__.py +1 -0
- ephaptic/cli/__main__.py +44 -8
- ephaptic/ephaptic.py +43 -2
- {ephaptic-0.2.2.dist-info → ephaptic-0.2.4.dist-info}/METADATA +29 -13
- {ephaptic-0.2.2.dist-info → ephaptic-0.2.4.dist-info}/RECORD +9 -9
- {ephaptic-0.2.2.dist-info → ephaptic-0.2.4.dist-info}/WHEEL +0 -0
- {ephaptic-0.2.2.dist-info → ephaptic-0.2.4.dist-info}/entry_points.txt +0 -0
- {ephaptic-0.2.2.dist-info → ephaptic-0.2.4.dist-info}/licenses/LICENSE +0 -0
- {ephaptic-0.2.2.dist-info → ephaptic-0.2.4.dist-info}/top_level.txt +0 -0
ephaptic/__init__.py
CHANGED
ephaptic/cli/__main__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import sys, os, json, inspect, importlib, typing, typer
|
|
1
|
+
import sys, os, json, inspect, importlib, typing, typer, subprocess as sp
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from pydantic import TypeAdapter
|
|
@@ -16,8 +16,8 @@ def load_ephaptic(import_name: str) -> Ephaptic:
|
|
|
16
16
|
sys.path.insert(0, os.getcwd())
|
|
17
17
|
|
|
18
18
|
if ":" not in import_name:
|
|
19
|
-
typer.secho(f"Warning: Import name did not specify
|
|
20
|
-
import_name += ":
|
|
19
|
+
typer.secho(f"Warning: Import name did not specify client name. Defaulting to `client`.", fg=typer.colors.YELLOW)
|
|
20
|
+
import_name += ":client" # default: expect client to be named `client` inside the file
|
|
21
21
|
|
|
22
22
|
module_name, var_name = import_name.split(":", 1)
|
|
23
23
|
|
|
@@ -35,7 +35,7 @@ def load_ephaptic(import_name: str) -> Ephaptic:
|
|
|
35
35
|
raise typer.Exit(1)
|
|
36
36
|
|
|
37
37
|
if not isinstance(instance, Ephaptic):
|
|
38
|
-
typer.secho(f"Error: '{var_name}' is not an Ephaptic
|
|
38
|
+
typer.secho(f"Error: '{var_name}' is not an Ephaptic client. It is type: {type(instance)}", fg=typer.colors.RED)
|
|
39
39
|
raise typer.Exit(1)
|
|
40
40
|
|
|
41
41
|
return instance
|
|
@@ -53,17 +53,37 @@ def create_schema(adapter: TypeAdapter, definitions: dict) -> dict:
|
|
|
53
53
|
|
|
54
54
|
return schema
|
|
55
55
|
|
|
56
|
+
def run_subprocess():
|
|
57
|
+
cmd = [sys.executable]
|
|
58
|
+
cmd += [arg for arg in sys.argv if arg not in {'--watch', '-w'}]
|
|
59
|
+
sp.run(cmd)
|
|
60
|
+
|
|
56
61
|
@app.command()
|
|
57
62
|
def generate(
|
|
58
|
-
|
|
59
|
-
output: Path = typer.Option('schema.json', '--output', '-o', help="Output path for the JSON schema.")
|
|
63
|
+
client: str = typer.Argument('client:client', help="The import string for the Ephaptic client."),
|
|
64
|
+
output: Path = typer.Option('schema.json', '--output', '-o', help="Output path for the JSON schema."),
|
|
65
|
+
watch: bool = typer.Option(False, '--watch', '-w', help="Watch for changes in `.py` files and regenerate schema file automatically."),
|
|
60
66
|
):
|
|
61
|
-
|
|
67
|
+
if watch:
|
|
68
|
+
import watchfiles
|
|
69
|
+
|
|
70
|
+
cwd = os.getcwd()
|
|
71
|
+
typer.secho(f"Watching for changes ({cwd})...", fg=typer.colors.GREEN)
|
|
72
|
+
|
|
73
|
+
run_subprocess()
|
|
74
|
+
|
|
75
|
+
for changes in watchfiles.watch(cwd):
|
|
76
|
+
if any(f.endswith('.py') for _, f in changes):
|
|
77
|
+
typer.secho("Detected changes, regenerating...")
|
|
78
|
+
run_subprocess()
|
|
79
|
+
|
|
80
|
+
ephaptic = load_ephaptic(client)
|
|
62
81
|
|
|
63
82
|
typer.secho(f"Found {len(ephaptic._exposed_functions)} functions.", fg=typer.colors.GREEN)
|
|
64
83
|
|
|
65
84
|
schema_output = {
|
|
66
85
|
"methods": {},
|
|
86
|
+
"events": {},
|
|
67
87
|
"definitions": {},
|
|
68
88
|
}
|
|
69
89
|
|
|
@@ -97,8 +117,24 @@ def generate(
|
|
|
97
117
|
|
|
98
118
|
schema_output["methods"][name] = method_schema
|
|
99
119
|
|
|
120
|
+
for name, model in ephaptic._exposed_events.items():
|
|
121
|
+
typer.secho(f" - {name}")
|
|
122
|
+
adapter = TypeAdapter(model)
|
|
123
|
+
|
|
124
|
+
schema_output["events"][name] = create_schema(
|
|
125
|
+
adapter,
|
|
126
|
+
schema_output["definitions"],
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
new = json.dumps(schema_output, indent=2)
|
|
130
|
+
|
|
131
|
+
if output.exists():
|
|
132
|
+
old = output.read_text()
|
|
133
|
+
if old == new:
|
|
134
|
+
return
|
|
135
|
+
|
|
100
136
|
with open(output, "w") as f:
|
|
101
|
-
|
|
137
|
+
f.write(new)
|
|
102
138
|
|
|
103
139
|
typer.secho(f"Schema generated to `{output}`.", fg=typer.colors.GREEN, bold=True)
|
|
104
140
|
|
ephaptic/ephaptic.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
from warnings import deprecated
|
|
2
3
|
import msgpack
|
|
3
4
|
import redis.asyncio as redis
|
|
4
5
|
import pydantic
|
|
@@ -71,12 +72,23 @@ class ConnectionManager:
|
|
|
71
72
|
manager = ConnectionManager()
|
|
72
73
|
|
|
73
74
|
_EXPOSED_FUNCTIONS = {}
|
|
75
|
+
_EXPOSED_EVENTS = {}
|
|
74
76
|
_IDENTITY_LOADER: Optional[Callable] = None
|
|
75
77
|
|
|
76
78
|
class EphapticTarget:
|
|
77
79
|
def __init__(self, user_ids: list[str]):
|
|
78
80
|
self.user_ids = user_ids
|
|
79
81
|
|
|
82
|
+
async def emit(self, event_instance: pydantic.BaseModel):
|
|
83
|
+
event_name = event_instance.__class__.__name__
|
|
84
|
+
payload = event_instance.model_dump(mode='json')
|
|
85
|
+
await manager.broadcast(
|
|
86
|
+
self.user_ids,
|
|
87
|
+
event_name,
|
|
88
|
+
args=[],
|
|
89
|
+
kwargs=payload,
|
|
90
|
+
)
|
|
91
|
+
|
|
80
92
|
def __getattr__(self, name: str):
|
|
81
93
|
async def emitter(*args, **kwargs):
|
|
82
94
|
await manager.broadcast(self.user_ids, name, list(args), dict(kwargs))
|
|
@@ -90,8 +102,13 @@ def identity_loader(func: Callable):
|
|
|
90
102
|
_IDENTITY_LOADER = func
|
|
91
103
|
return func
|
|
92
104
|
|
|
105
|
+
def event(model: typing.Type[pydantic.BaseModel]):
|
|
106
|
+
_EXPOSED_EVENTS[model.__name__] = model
|
|
107
|
+
return model
|
|
108
|
+
|
|
93
109
|
class Ephaptic:
|
|
94
110
|
_exposed_functions: Dict[str, Callable] = {}
|
|
111
|
+
_exposed_events: Dict[str, typing.Type[pydantic.BaseModel]]
|
|
95
112
|
_identity_loader: Optional[Callable] = None
|
|
96
113
|
|
|
97
114
|
def _async(self, func: Callable):
|
|
@@ -125,8 +142,9 @@ class Ephaptic:
|
|
|
125
142
|
case _:
|
|
126
143
|
raise TypeError(f"Unsupported app type: {module}")
|
|
127
144
|
|
|
128
|
-
|
|
129
|
-
|
|
145
|
+
instance._exposed_functions = _EXPOSED_FUNCTIONS.copy()
|
|
146
|
+
instance._exposed_events = _EXPOSED_EVENTS.copy()
|
|
147
|
+
instance._identity_loader = _IDENTITY_LOADER
|
|
130
148
|
|
|
131
149
|
return instance
|
|
132
150
|
|
|
@@ -135,6 +153,10 @@ class Ephaptic:
|
|
|
135
153
|
self._exposed_functions[func.__name__] = func
|
|
136
154
|
return func
|
|
137
155
|
|
|
156
|
+
def event(self, model: typing.Type[pydantic.BaseModel]):
|
|
157
|
+
self._exposed_events[model.__name__] = model
|
|
158
|
+
return model
|
|
159
|
+
|
|
138
160
|
def identity_loader(self, func: Callable):
|
|
139
161
|
self._identity_loader = func
|
|
140
162
|
return func
|
|
@@ -147,6 +169,7 @@ class Ephaptic:
|
|
|
147
169
|
return EphapticTarget(targets)
|
|
148
170
|
|
|
149
171
|
def __getattr__(self, name: str):
|
|
172
|
+
@deprecated("Use `emit` and the new (typed) event system instead.")
|
|
150
173
|
async def emitter(*args, **kwargs):
|
|
151
174
|
transport: Transport = _active_transport_ctx.get()
|
|
152
175
|
if not transport:
|
|
@@ -163,6 +186,24 @@ class Ephaptic:
|
|
|
163
186
|
|
|
164
187
|
return emitter
|
|
165
188
|
|
|
189
|
+
async def emit(self, event_instance: pydantic.BaseModel):
|
|
190
|
+
event_name = event_instance.__class__.__name__
|
|
191
|
+
payload = event_instance.model_dump(mode='json')
|
|
192
|
+
transport: Transport = _active_transport_ctx.get()
|
|
193
|
+
if not transport:
|
|
194
|
+
raise RuntimeError(
|
|
195
|
+
f".emit({event_name}) called outside RPC context."
|
|
196
|
+
f"Use .to(...).emit({event_name}) to broadcast from background tasks, to specific user(s)."
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# NOTE: There is slight duplication here and in the EphapticTarget. Perhaps make these functions internally route to EphapticTargets but pass the transport to use?
|
|
200
|
+
|
|
201
|
+
await transport.send(msgpack.dumps({
|
|
202
|
+
'type': 'event',
|
|
203
|
+
'name': event_name,
|
|
204
|
+
'payload': {'args': [], 'kwargs': payload}
|
|
205
|
+
}))
|
|
206
|
+
|
|
166
207
|
async def handle_transport(self, transport: Transport):
|
|
167
208
|
current_uid = None
|
|
168
209
|
try:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ephaptic
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: The Python client/server package for ephaptic.
|
|
5
5
|
Author-email: uukelele <robustrobot11@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -36,6 +36,7 @@ Requires-Dist: websockets>=12.0
|
|
|
36
36
|
Requires-Dist: pydantic>=2.0
|
|
37
37
|
Requires-Dist: typer[standard]>=0.20.0
|
|
38
38
|
Requires-Dist: python-dotenv
|
|
39
|
+
Requires-Dist: watchfiles
|
|
39
40
|
Provides-Extra: server
|
|
40
41
|
Requires-Dist: redis; extra == "server"
|
|
41
42
|
Dynamic: license-file
|
|
@@ -91,16 +92,16 @@ What are you waiting for? **Let's go.**
|
|
|
91
92
|
<details>
|
|
92
93
|
<summary>Python</summary>
|
|
93
94
|
|
|
94
|
-
|
|
95
|
+
<h4>Client:</h4>
|
|
95
96
|
|
|
96
97
|
```
|
|
97
|
-
pip install ephaptic
|
|
98
|
+
$ pip install ephaptic
|
|
98
99
|
```
|
|
99
100
|
|
|
100
|
-
|
|
101
|
+
<h4>Server:</h4>
|
|
101
102
|
|
|
102
103
|
```
|
|
103
|
-
pip install ephaptic[server]
|
|
104
|
+
$ pip install ephaptic[server]
|
|
104
105
|
```
|
|
105
106
|
|
|
106
107
|
```python
|
|
@@ -132,7 +133,7 @@ async def add(num1: int, num2: int) -> int:
|
|
|
132
133
|
return num1 + num2
|
|
133
134
|
```
|
|
134
135
|
|
|
135
|
-
|
|
136
|
+
<h5>If you're trying to expose functions statelessly, e.g. in a different file, feel free to instead import and use the <code>expose</code> function from the library instead of the instance. Please note that if you do this, you must define all exposed functions <i>before</i> creating the ephaptic instance - easily done by simply placing your import line above the ephaptic constructor. The same thing can be done with the global <code>identity_loader</code> decorator.</h5>
|
|
136
137
|
|
|
137
138
|
Yep, it's really that simple.
|
|
138
139
|
|
|
@@ -147,17 +148,32 @@ await ephaptic.to(user1, user2).notification("Hello, world!", priority="high")
|
|
|
147
148
|
To create a schema of your RPC endpoints:
|
|
148
149
|
|
|
149
150
|
```
|
|
150
|
-
$ ephaptic src.app:app -o schema.json
|
|
151
|
+
$ ephaptic src.app:app -o schema.json # --watch to run in background and auto-reload on file change.
|
|
151
152
|
```
|
|
152
153
|
|
|
153
154
|
Pydantic is entirely supported. It's validated for arguments, it's auto-serialized when you return a pydantic model, and your models receive type definitions in the schema.
|
|
154
155
|
|
|
156
|
+
To receive authentication objects and handle them:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from ephaptic import identity_loader
|
|
160
|
+
|
|
161
|
+
@identity_loader
|
|
162
|
+
async def load_identity(auth): # You can use synchronous functions here too.
|
|
163
|
+
jwt = auth.get("token")
|
|
164
|
+
if not jwt: return None # unauthorized
|
|
165
|
+
... # app logic to retrieve user ID
|
|
166
|
+
return user_id
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
From here, you can use <code>ephaptic.active_user</code> within any exposed function, and it will give you the current active user ID / whatever else your identity loading function returns. (This is also how <code>ephaptic.to</code> works.)
|
|
170
|
+
|
|
155
171
|
</details>
|
|
156
172
|
|
|
157
173
|
<details>
|
|
158
|
-
<summary>JavaScript/TypeScript — Browser (
|
|
174
|
+
<summary>JavaScript/TypeScript — Browser (Svelte, React, Angular, Vite, etc.)</summary>
|
|
159
175
|
|
|
160
|
-
|
|
176
|
+
<h4>To use with a framework / Vite:</h4>
|
|
161
177
|
|
|
162
178
|
```
|
|
163
179
|
$ npm install @ephaptic/client
|
|
@@ -191,7 +207,7 @@ And you can load types, too.
|
|
|
191
207
|
|
|
192
208
|
```
|
|
193
209
|
$ npm i --save-dev @ephaptic/type-gen
|
|
194
|
-
$ npx @ephaptic/type-gen ./schema.json -o schema.d.ts
|
|
210
|
+
$ npx @ephaptic/type-gen ./schema.json -o schema.d.ts # --watch to auto-reload upon changes
|
|
195
211
|
```
|
|
196
212
|
|
|
197
213
|
```typescript
|
|
@@ -202,7 +218,7 @@ const client = connect(...) as unknown as EphapticService;
|
|
|
202
218
|
```
|
|
203
219
|
|
|
204
220
|
|
|
205
|
-
|
|
221
|
+
<h4>Or, to use in your browser:</h4>
|
|
206
222
|
|
|
207
223
|
```html
|
|
208
224
|
<script type="module">
|
|
@@ -212,10 +228,10 @@ const client = connect(...);
|
|
|
212
228
|
</script>
|
|
213
229
|
```
|
|
214
230
|
|
|
215
|
-
<!-- TODO: Add extended documentation -->
|
|
216
|
-
|
|
217
231
|
</details>
|
|
218
232
|
|
|
233
|
+
See more in the [docs](https://ephaptic.github.io/ephaptic/tutorial).
|
|
234
|
+
|
|
219
235
|
## [License](https://github.com/ephaptic/ephaptic/blob/main/LICENSE)
|
|
220
236
|
|
|
221
237
|
---
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
ephaptic/__init__.py,sha256=
|
|
2
|
-
ephaptic/ephaptic.py,sha256=
|
|
1
|
+
ephaptic/__init__.py,sha256=QIEfepOHMtM6Lrxg4AwZ28gEj0D5TY2UP8MGlrAb-DI,137
|
|
2
|
+
ephaptic/ephaptic.py,sha256=UtT3_KD5SQcVFZxW5I3rAuDkXaRkMhIYQDl8d1njwDQ,11904
|
|
3
3
|
ephaptic/localproxy.py,sha256=fJaaskkiD6C2zaOod0F0HNWIbdKs_JMuHFwd0-sdLIM,19477
|
|
4
4
|
ephaptic/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
ephaptic/adapters/fastapi_.py,sha256=yfSbJuA7Tgeh9EhZkfIve0Uj-cOZmTBljlBsCRKh2EE,1007
|
|
6
6
|
ephaptic/adapters/quart_.py,sha256=MBo9g6h_zI63mL4aGdrvV5yEXsHaOd0Iv5J8SAPHBoA,537
|
|
7
7
|
ephaptic/cli/__init__.py,sha256=p_mYumuQLr3HZa-6I4QKut6khZv3WQjEX-B-aa4cdEE,44
|
|
8
|
-
ephaptic/cli/__main__.py,sha256=
|
|
8
|
+
ephaptic/cli/__main__.py,sha256=79FQgQtsq09pWV-G-98oqGx7mYuVLvw42kUbNzEJvvA,4499
|
|
9
9
|
ephaptic/client/__init__.py,sha256=NeaPIzTFeozP54wlDYHIg_adHP3Z3LWVujsRUlpn4_U,35
|
|
10
10
|
ephaptic/client/client.py,sha256=YYAlzA40xBvWsiDu0Gsd1EBJaqivLR-bSszepWdNODs,4181
|
|
11
11
|
ephaptic/transports/__init__.py,sha256=kSAlgvm8sV9nHHu61LTjjTpv4bweah90xvFrwQMDQtQ,169
|
|
12
12
|
ephaptic/transports/fastapi_ws.py,sha256=X0PMRcwM-KDpKA-zXShGTFhD1kHMSqrx3PBBKZtQ1W0,258
|
|
13
13
|
ephaptic/transports/websocket.py,sha256=jwgclSDSq0lQCvgwjwUXe9MzPk7NH0FdmsLhWxYBh-4,261
|
|
14
|
-
ephaptic-0.2.
|
|
15
|
-
ephaptic-0.2.
|
|
16
|
-
ephaptic-0.2.
|
|
17
|
-
ephaptic-0.2.
|
|
18
|
-
ephaptic-0.2.
|
|
19
|
-
ephaptic-0.2.
|
|
14
|
+
ephaptic-0.2.4.dist-info/licenses/LICENSE,sha256=kMpJjLKMj6zsAhf4uHApO4q0r31Ye1VyfBOl9cFW13M,1065
|
|
15
|
+
ephaptic-0.2.4.dist-info/METADATA,sha256=x3-a9_RDMcsnYPrQWYW0HaX3YwwZcEFuoqryONFYIUk,8533
|
|
16
|
+
ephaptic-0.2.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
17
|
+
ephaptic-0.2.4.dist-info/entry_points.txt,sha256=lis9vIDIrMVJM43r9ooFkb07KhrX4946duvYru3VfT4,46
|
|
18
|
+
ephaptic-0.2.4.dist-info/top_level.txt,sha256=nNhdhcz2o_IuwZ9I2uWQuLZrRmSW0dQVU3qwGrb35Io,9
|
|
19
|
+
ephaptic-0.2.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|