ephaptic 0.2.1__py3-none-any.whl → 0.2.3__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/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,12 +53,31 @@ 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
 
@@ -97,8 +116,15 @@ def generate(
97
116
 
98
117
  schema_output["methods"][name] = method_schema
99
118
 
119
+ new = json.dumps(schema_output, indent=2)
120
+
121
+ if output.exists():
122
+ old = output.read_text()
123
+ if old == new:
124
+ return
125
+
100
126
  with open(output, "w") as f:
101
- json.dump(schema_output, f, indent=2)
127
+ f.write(new)
102
128
 
103
129
  typer.secho(f"Schema generated to `{output}`.", fg=typer.colors.GREEN, bold=True)
104
130
 
ephaptic/ephaptic.py CHANGED
@@ -51,9 +51,12 @@ class ConnectionManager:
51
51
  for user_id in user_ids:
52
52
  if user_id in self.active:
53
53
  for transport in list(self.active[user_id]):
54
- try:
55
- await transport.send(payload)
56
- except: pass
54
+ asyncio.create_task(self._safe_send(transport, payload))
55
+
56
+ async def _safe_send(self, transport: Transport, payload: bytes):
57
+ try:
58
+ await transport.send(payload)
59
+ except: ...
57
60
 
58
61
  async def start_redis(self):
59
62
  if not self.redis: return
@@ -192,7 +195,7 @@ class Ephaptic:
192
195
 
193
196
  if func_name in self._exposed_functions:
194
197
  target_func = self._exposed_functions[func_name]
195
- sig = inspect.signature(target_func)
198
+ sig = inspect.signature(target_func)
196
199
  try:
197
200
  bound = sig.bind(*args, **kwargs)
198
201
  bound.apply_defaults()
@@ -202,6 +205,8 @@ class Ephaptic:
202
205
 
203
206
  hints = typing.get_type_hints(target_func)
204
207
 
208
+ return_type = hints.get("return", typing.Any)
209
+
205
210
  errors = []
206
211
 
207
212
  for name, val in bound.arguments.items():
@@ -216,7 +221,11 @@ class Ephaptic:
216
221
  if errors:
217
222
  await transport.send(msgpack.dumps({
218
223
  "id": call_id,
219
- "error": errors,
224
+ "error": {
225
+ "code": "VALIDATION_ERROR",
226
+ "message": "Validation failed.",
227
+ "data": errors,
228
+ },
220
229
  }))
221
230
  continue
222
231
 
@@ -225,9 +234,21 @@ class Ephaptic:
225
234
 
226
235
  try:
227
236
  result = await self._async(target_func)(**bound.arguments)
228
- if isinstance(result, pydantic.BaseModel):
229
- result = result.model_dump()
237
+
238
+ if return_type is not inspect.Signature.empty:
239
+ try:
240
+ adapter = pydantic.TypeAdapter(return_type)
241
+ validated = adapter.validate_python(result, from_attributes=True)
242
+ result = adapter.dump_python(validated, mode='json')
243
+ except: ...
244
+ elif isinstance(result, pydantic.BaseModel):
245
+ result = result.model_dump(mode='json')
246
+
230
247
  await transport.send(msgpack.dumps({"id": call_id, "result": result}))
248
+ # except pydantic.ValidationError as e:
249
+ # Should we really treat this separately?
250
+ # For input it's understandable, but for server responses it feels like a server issue.
251
+ # Ok, let's treat this like any other server error.
231
252
  except Exception as e:
232
253
  await transport.send(msgpack.dumps({"id": call_id, "error": str(e)}))
233
254
  finally:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ephaptic
3
- Version: 0.2.1
3
+ Version: 0.2.3
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
@@ -132,8 +133,7 @@ async def add(num1: int, num2: int) -> int:
132
133
  return num1 + num2
133
134
  ```
134
135
 
135
- > [!TIP]
136
- > If you're experiencing circular imports, 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 - this is mainly for people importing RPC functions from another file. The same thing can be done with the global `identity_loader` decorator.
136
+ ###### 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.
137
137
 
138
138
  Yep, it's really that simple.
139
139
 
@@ -1,19 +1,19 @@
1
1
  ephaptic/__init__.py,sha256=3GmVqhbye3snH7KfnyqIt8t_w6ULnWOvDw07eB13iDA,126
2
- ephaptic/ephaptic.py,sha256=K9cOUk8guI6IesBACkdUxebI2UXpN-0yJfrhjHUC3iI,9034
2
+ ephaptic/ephaptic.py,sha256=9PKqiUKWl5MWlBvqsVbpsEfBmJmZJFes0RbbeqFkX4I,10224
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=fbfEGUnufc7vO65ZHnCV7YSshlHyif1c3kzR7vSp9GM,4218
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.1.dist-info/licenses/LICENSE,sha256=kMpJjLKMj6zsAhf4uHApO4q0r31Ye1VyfBOl9cFW13M,1065
15
- ephaptic-0.2.1.dist-info/METADATA,sha256=9-6m-C21Y01fry6UHDGoQ-2LtUqYpI_rOxJZv_fzaGM,7716
16
- ephaptic-0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
- ephaptic-0.2.1.dist-info/entry_points.txt,sha256=lis9vIDIrMVJM43r9ooFkb07KhrX4946duvYru3VfT4,46
18
- ephaptic-0.2.1.dist-info/top_level.txt,sha256=nNhdhcz2o_IuwZ9I2uWQuLZrRmSW0dQVU3qwGrb35Io,9
19
- ephaptic-0.2.1.dist-info/RECORD,,
14
+ ephaptic-0.2.3.dist-info/licenses/LICENSE,sha256=kMpJjLKMj6zsAhf4uHApO4q0r31Ye1VyfBOl9cFW13M,1065
15
+ ephaptic-0.2.3.dist-info/METADATA,sha256=ZokQJXZp3r4icuY3F8IRpqALNpEZXrCOLkgv8l2lHvk,7783
16
+ ephaptic-0.2.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
+ ephaptic-0.2.3.dist-info/entry_points.txt,sha256=lis9vIDIrMVJM43r9ooFkb07KhrX4946duvYru3VfT4,46
18
+ ephaptic-0.2.3.dist-info/top_level.txt,sha256=nNhdhcz2o_IuwZ9I2uWQuLZrRmSW0dQVU3qwGrb35Io,9
19
+ ephaptic-0.2.3.dist-info/RECORD,,