ephaptic 0.1.3__py3-none-any.whl → 0.2.0__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.
@@ -0,0 +1,3 @@
1
+ from .__main__ import app
2
+
3
+ __all__ = ["app"]
@@ -0,0 +1,106 @@
1
+ import sys, os, json, inspect, importlib, typing, typer
2
+
3
+ from pathlib import Path
4
+ from pydantic import TypeAdapter
5
+ from pydantic.json_schema import models_json_schema
6
+
7
+ from ephaptic import Ephaptic
8
+
9
+ app = typer.Typer(help="Ephaptic CLI tool.")
10
+
11
+ def load_ephaptic(import_name: str) -> Ephaptic:
12
+ try:
13
+ from dotenv import load_dotenv; load_dotenv()
14
+ except: ...
15
+
16
+ sys.path.insert(0, os.getcwd())
17
+
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
21
+
22
+ module_name, var_name = import_name.split(":", 1)
23
+
24
+ try:
25
+ typer.secho(f"Attempting to import `{var_name}` from `{module_name}`...")
26
+ module = importlib.import_module(module_name)
27
+ except ImportError as e:
28
+ typer.secho(f"Error: Can't import '{module_name}'.\n{e}", fg=typer.colors.RED)
29
+ raise typer.Exit(1)
30
+
31
+ try:
32
+ instance = getattr(module, var_name)
33
+ except AttributeError:
34
+ typer.secho(f"Error: Variable '{var_name}' not found in module '{module_name}'.", fg=typer.colors.RED)
35
+ raise typer.Exit(1)
36
+
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)
39
+ raise typer.Exit(1)
40
+
41
+ return instance
42
+
43
+ def create_schema(adapter: TypeAdapter, definitions: dict) -> dict:
44
+ schema = adapter.json_schema(ref_template='#/definitions/{model}')
45
+
46
+ if '$defs' in schema:
47
+ definitions.update(schema.pop('$defs'))
48
+
49
+ if schema.get('type') == 'object' and 'title' in schema:
50
+ model = schema['title']
51
+ definitions[model] = schema
52
+ return { '$ref': f'#/definitions/{model}' }
53
+
54
+ return schema
55
+
56
+ @app.command()
57
+ 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.")
60
+ ):
61
+ ephaptic = load_ephaptic(app)
62
+
63
+ typer.secho(f"Found {len(ephaptic._exposed_functions)} functions.", fg=typer.colors.GREEN)
64
+
65
+ schema_output = {
66
+ "methods": {},
67
+ "definitions": {},
68
+ }
69
+
70
+ for name, func in ephaptic._exposed_functions.items():
71
+ typer.secho(f" - {name}")
72
+
73
+ hints = typing.get_type_hints(func)
74
+ sig = inspect.signature(func)
75
+
76
+ method_schema = {
77
+ "args": {},
78
+ "return": None
79
+ }
80
+
81
+ for param_name in sig.parameters:
82
+ hint = hints.get(param_name, typing.Any)
83
+ adapter = TypeAdapter(hint)
84
+
85
+ method_schema["args"][param_name] = create_schema(
86
+ adapter,
87
+ schema_output["definitions"],
88
+ )
89
+
90
+ return_hint = hints.get("return", typing.Any)
91
+ if return_hint is not type(None):
92
+ adapter = TypeAdapter(return_hint)
93
+ method_schema["return"] = create_schema(
94
+ adapter,
95
+ schema_output["definitions"],
96
+ )
97
+
98
+ schema_output["methods"][name] = method_schema
99
+
100
+ with open(output, "w") as f:
101
+ json.dump(schema_output, f, indent=2)
102
+
103
+ typer.secho(f"Schema generated to `{output}`.", fg=typer.colors.GREEN, bold=True)
104
+
105
+ if __name__ == "__main__":
106
+ app()
ephaptic/ephaptic.py CHANGED
@@ -1,12 +1,14 @@
1
1
  import asyncio
2
2
  import msgpack
3
3
  import redis.asyncio as redis
4
+ import pydantic
4
5
 
5
6
  from contextvars import ContextVar
6
7
  from .localproxy import LocalProxy
7
8
 
8
9
  from .transports import Transport
9
10
 
11
+ import typing
10
12
  from typing import Optional, Callable, Any, List, Set, Dict
11
13
  import inspect
12
14
 
@@ -91,12 +93,11 @@ class Ephaptic:
91
93
 
92
94
  @classmethod
93
95
  def from_app(cls, app, path="/_ephaptic", redis_url=None):
94
- # `app` could be Flask, Quart, FastAPI, etc.
96
+ # `app` could be ~Flask~, Quart, FastAPI, etc.
95
97
  instance = cls()
96
98
 
97
99
  if redis_url:
98
100
  manager.init_redis(redis_url)
99
- # TODO: framework-specific hooks for the background listener.
100
101
 
101
102
  module = app.__class__.__module__.split(".")[0]
102
103
 
@@ -177,11 +178,41 @@ class Ephaptic:
177
178
 
178
179
  if func_name in self._exposed_functions:
179
180
  target_func = self._exposed_functions[func_name]
181
+ sig = inspect.signature(target_func)
182
+ try:
183
+ bound = sig.bind(*args, **kwargs)
184
+ bound.apply_defaults()
185
+ except TypeError as e:
186
+ await transport.send(msgpack.dumps({"id": call_id, "error": str(e)}))
187
+ continue
188
+
189
+ hints = typing.get_type_hints(target_func)
190
+
191
+ errors = []
192
+
193
+ for name, val in bound.arguments.items():
194
+ hint = hints.get(name)
195
+
196
+ if hint and inspect.isclass(hint) and issubclass(hint, pydantic.BaseModel):
197
+ try:
198
+ bound.arguments[name] = hint.model_validate(val)
199
+ except pydantic.ValidationError as e:
200
+ errors.extend(e.errors())
201
+
202
+ if errors:
203
+ await transport.send(msgpack.dumps({
204
+ "id": call_id,
205
+ "error": errors,
206
+ }))
207
+ continue
208
+
180
209
  token_transport = _active_transport_ctx.set(transport)
181
210
  token_user = _active_user_ctx.set(current_uid)
182
211
 
183
212
  try:
184
- result = await self._async(target_func)(*args, **kwargs)
213
+ result = await self._async(target_func)(**bound.arguments)
214
+ if isinstance(result, pydantic.BaseModel):
215
+ result = result.model_dump()
185
216
  await transport.send(msgpack.dumps({"id": call_id, "result": result}))
186
217
  except Exception as e:
187
218
  await transport.send(msgpack.dumps({"id": call_id, "error": str(e)}))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ephaptic
3
- Version: 0.1.3
3
+ Version: 0.2.0
4
4
  Summary: The Python client/server package for ephaptic.
5
5
  Author-email: uukelele <robustrobot11@gmail.com>
6
6
  License: MIT License
@@ -33,6 +33,9 @@ Description-Content-Type: text/markdown
33
33
  License-File: LICENSE
34
34
  Requires-Dist: msgpack>=1.0.0
35
35
  Requires-Dist: websockets>=12.0
36
+ Requires-Dist: pydantic>=2.0
37
+ Requires-Dist: typer[standard]>=0.20.0
38
+ Requires-Dist: python-dotenv
36
39
  Provides-Extra: server
37
40
  Requires-Dist: redis; extra == "server"
38
41
  Dynamic: license-file
@@ -58,8 +61,6 @@ Dynamic: license-file
58
61
  </a>
59
62
 
60
63
 
61
-
62
-
63
64
  </div>
64
65
 
65
66
  ## What is `ephaptic`?
@@ -73,7 +74,7 @@ Dynamic: license-file
73
74
 
74
75
  Nah, just kidding. It's an RPC framework.
75
76
 
76
- > **ephaptic** — Call your backend straight from your frontend. No JSON. No latency. No middleware.
77
+ > **ephaptic** — Call your backend straight from your frontend. No JSON. Low latency. Invisible middleware.
77
78
 
78
79
  ## Getting Started
79
80
 
@@ -83,7 +84,9 @@ Nah, just kidding. It's an RPC framework.
83
84
 
84
85
  - 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.
85
86
 
86
- What are you waiting for? **Let's go.**
87
+ - Saved the best for last: it's type-safe. Don't believe me? Try it out for yourself. Simply type hint return values and parameters on the backend, and watch those very Python types transform into interfaces and types on the TypeScript frontend. Plus, you can use Pydantic - which means, for those of you who are FastAPI users, this is going to be great.
88
+
89
+ What are you waiting for? **Let's go.**
87
90
 
88
91
  <details>
89
92
  <summary>Python</summary>
@@ -125,13 +128,13 @@ Now, how do you expose your function to the frontend?
125
128
 
126
129
  ```python
127
130
  @ephaptic.expose
128
- async def add(num1, num2):
131
+ async def add(num1: int, num2: int) -> int:
129
132
  return num1 + num2
130
133
  ```
131
134
 
132
135
  Yep, it's really that simple.
133
136
 
134
- But what if your code throws an error? No sweat, it just throws up on the frontend with the same details.
137
+ But what if your code throws an error? No sweat, it just throws up on the frontend, with the error name.
135
138
 
136
139
  And, want to say something to the frontend?
137
140
 
@@ -139,6 +142,13 @@ And, want to say something to the frontend?
139
142
  await ephaptic.to(user1, user2).notification("Hello, world!", priority="high")
140
143
  ```
141
144
 
145
+ To create a schema of your RPC endpoints:
146
+
147
+ ```
148
+ $ ephaptic src.app:app -o schema.json
149
+ ```
150
+
151
+ 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.
142
152
 
143
153
  </details>
144
154
 
@@ -148,7 +158,7 @@ await ephaptic.to(user1, user2).notification("Hello, world!", priority="high")
148
158
  #### To use with a framework / Vite:
149
159
 
150
160
  ```
151
- npm install @ephaptic/client
161
+ $ npm install @ephaptic/client
152
162
  ```
153
163
 
154
164
  Then:
@@ -175,13 +185,28 @@ You can even send auth objects to the server for identity loading.
175
185
  const client = connect({ url: '...', auth: { token: window.localStorage.getItem('jwtToken') } })
176
186
  ```
177
187
 
188
+ And you can load types, too.
189
+
190
+ ```
191
+ $ npm i --save-dev @ephaptic/type-gen
192
+ $ npx @ephaptic/type-gen ./schema.json -o schema.d.ts
193
+ ```
194
+
195
+ ```typescript
196
+ import { connect } from "@ephaptic/client";
197
+ import { type EphapticService } from './schema';
198
+
199
+ const client = connect(...) as unknown as EphapticService;
200
+ ```
201
+
202
+
178
203
  #### Or, to use in your browser:
179
204
 
180
205
  ```html
181
206
  <script type="module">
182
207
  import { connect } from 'https://cdn.jsdelivr.net/npm/@ephaptic/client@latest/+esm';
183
208
 
184
- const client = connect();
209
+ const client = connect(...);
185
210
  </script>
186
211
  ```
187
212
 
@@ -1,16 +1,19 @@
1
1
  ephaptic/__init__.py,sha256=1_7Rwb0WCZS2rbOOEVIctFGqF0Kt81JCWR7Mm3Ry4F4,92
2
- ephaptic/ephaptic.py,sha256=m8gKQ_sjDVkap1EpbimuMvFa74eiRRpWkJd43yph8_c,7355
2
+ ephaptic/ephaptic.py,sha256=6k62iCSRZmpqt8ka8JgZePeZqojxY6gj6_u1yTPWTeE,8681
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
+ ephaptic/cli/__init__.py,sha256=p_mYumuQLr3HZa-6I4QKut6khZv3WQjEX-B-aa4cdEE,44
8
+ ephaptic/cli/__main__.py,sha256=RNEL4c_MkEKV7kY9pswC1ihNToiaVb06t1aZ4dCfk9s,3389
7
9
  ephaptic/client/__init__.py,sha256=NeaPIzTFeozP54wlDYHIg_adHP3Z3LWVujsRUlpn4_U,35
8
10
  ephaptic/client/client.py,sha256=YYAlzA40xBvWsiDu0Gsd1EBJaqivLR-bSszepWdNODs,4181
9
11
  ephaptic/transports/__init__.py,sha256=kSAlgvm8sV9nHHu61LTjjTpv4bweah90xvFrwQMDQtQ,169
10
12
  ephaptic/transports/fastapi_ws.py,sha256=X0PMRcwM-KDpKA-zXShGTFhD1kHMSqrx3PBBKZtQ1W0,258
11
13
  ephaptic/transports/websocket.py,sha256=jwgclSDSq0lQCvgwjwUXe9MzPk7NH0FdmsLhWxYBh-4,261
12
- ephaptic-0.1.3.dist-info/licenses/LICENSE,sha256=kMpJjLKMj6zsAhf4uHApO4q0r31Ye1VyfBOl9cFW13M,1065
13
- ephaptic-0.1.3.dist-info/METADATA,sha256=Qs74Bl4B09bcz1d3UMlCoEgFSd3aDnU4X7uthC25qLg,6263
14
- ephaptic-0.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
- ephaptic-0.1.3.dist-info/top_level.txt,sha256=nNhdhcz2o_IuwZ9I2uWQuLZrRmSW0dQVU3qwGrb35Io,9
16
- ephaptic-0.1.3.dist-info/RECORD,,
14
+ ephaptic-0.2.0.dist-info/licenses/LICENSE,sha256=kMpJjLKMj6zsAhf4uHApO4q0r31Ye1VyfBOl9cFW13M,1065
15
+ ephaptic-0.2.0.dist-info/METADATA,sha256=EiURw4x9UBZiNxhRchj1oXOys-_HECdx_b2w6Vze3bs,7309
16
+ ephaptic-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
+ ephaptic-0.2.0.dist-info/entry_points.txt,sha256=lis9vIDIrMVJM43r9ooFkb07KhrX4946duvYru3VfT4,46
18
+ ephaptic-0.2.0.dist-info/top_level.txt,sha256=nNhdhcz2o_IuwZ9I2uWQuLZrRmSW0dQVU3qwGrb35Io,9
19
+ ephaptic-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ephaptic = ephaptic.cli:app