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 CHANGED
@@ -3,6 +3,7 @@ from .ephaptic import (
3
3
  active_user,
4
4
  expose,
5
5
  identity_loader,
6
+ event,
6
7
  )
7
8
 
8
9
  from .client import (
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 app name. Defaulting to `app`.", fg=typer.colors.YELLOW)
20
- import_name += ":app" # default: expect app to be named `app` inside the file
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 instance. It is type: {type(instance)}", fg=typer.colors.RED)
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
- app: str = typer.Argument('app:app', help="The import string. (Default: `app:app`)"),
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
- ephaptic = load_ephaptic(app)
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
- json.dump(schema_output, f, indent=2)
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
- cls._exposed_functions = _EXPOSED_FUNCTIONS.copy()
129
- cls._identity_loader = _IDENTITY_LOADER
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.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
- #### Client:
95
+ <h4>Client:</h4>
95
96
 
96
97
  ```
97
- pip install ephaptic
98
+ $ pip install ephaptic
98
99
  ```
99
100
 
100
- #### Server:
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
- ###### If you're trying to expose functions statelessly, e.g. in a different file, feel free to instead import and use the `expose` function from the library instead of the instance. Please note that if you do this, you must define all exposed functions *before* 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 `identity_loader` decorator.
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 (Svelt, React, Angular, Vite, etc.)</summary>
174
+ <summary>JavaScript/TypeScript — Browser (Svelte, React, Angular, Vite, etc.)</summary>
159
175
 
160
- #### To use with a framework / Vite:
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
- #### Or, to use in your browser:
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=3GmVqhbye3snH7KfnyqIt8t_w6ULnWOvDw07eB13iDA,126
2
- ephaptic/ephaptic.py,sha256=9PKqiUKWl5MWlBvqsVbpsEfBmJmZJFes0RbbeqFkX4I,10224
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=RNEL4c_MkEKV7kY9pswC1ihNToiaVb06t1aZ4dCfk9s,3389
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.2.dist-info/licenses/LICENSE,sha256=kMpJjLKMj6zsAhf4uHApO4q0r31Ye1VyfBOl9cFW13M,1065
15
- ephaptic-0.2.2.dist-info/METADATA,sha256=FvhGA1GURxKF9pygzB9SQ7uuKaDCYq3mxQ6UfLMpZIg,7757
16
- ephaptic-0.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
- ephaptic-0.2.2.dist-info/entry_points.txt,sha256=lis9vIDIrMVJM43r9ooFkb07KhrX4946duvYru3VfT4,46
18
- ephaptic-0.2.2.dist-info/top_level.txt,sha256=nNhdhcz2o_IuwZ9I2uWQuLZrRmSW0dQVU3qwGrb35Io,9
19
- ephaptic-0.2.2.dist-info/RECORD,,
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,,