ephaptic 0.2.7__tar.gz → 0.3.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.2.7 → ephaptic-0.3.0}/PKG-INFO +19 -25
- {ephaptic-0.2.7 → ephaptic-0.3.0}/README.md +11 -22
- {ephaptic-0.2.7 → ephaptic-0.3.0}/ephaptic/__init__.py +7 -2
- ephaptic-0.3.0/ephaptic/cli/__main__.py +486 -0
- ephaptic-0.3.0/ephaptic/ctx.py +10 -0
- ephaptic-0.3.0/ephaptic/decorators.py +87 -0
- {ephaptic-0.2.7 → ephaptic-0.3.0}/ephaptic/ephaptic.py +142 -78
- ephaptic-0.3.0/ephaptic/ext/fastapi/__init__.py +1 -0
- ephaptic-0.2.7/ephaptic/adapters/fastapi_.py → ephaptic-0.3.0/ephaptic/ext/fastapi/adapter.py +4 -1
- ephaptic-0.3.0/ephaptic/ext/fastapi/middleware.py +24 -0
- ephaptic-0.3.0/ephaptic/ext/fastapi/router.py +82 -0
- ephaptic-0.2.7/ephaptic/adapters/quart_.py → ephaptic-0.3.0/ephaptic/ext/quart/adapter.py +1 -1
- ephaptic-0.3.0/ephaptic/transports/__init__.py +10 -0
- ephaptic-0.3.0/ephaptic/transports/fastapi_ws.py +17 -0
- {ephaptic-0.2.7 → ephaptic-0.3.0}/ephaptic/transports/websocket.py +1 -0
- ephaptic-0.3.0/ephaptic/utils.py +21 -0
- {ephaptic-0.2.7 → ephaptic-0.3.0}/ephaptic.egg-info/PKG-INFO +19 -25
- {ephaptic-0.2.7 → ephaptic-0.3.0}/ephaptic.egg-info/SOURCES.txt +9 -3
- {ephaptic-0.2.7 → ephaptic-0.3.0}/ephaptic.egg-info/requires.txt +7 -2
- {ephaptic-0.2.7 → ephaptic-0.3.0}/pyproject.toml +9 -2
- ephaptic-0.2.7/ephaptic/cli/__main__.py +0 -152
- ephaptic-0.2.7/ephaptic/transports/__init__.py +0 -5
- ephaptic-0.2.7/ephaptic/transports/fastapi_ws.py +0 -8
- {ephaptic-0.2.7 → ephaptic-0.3.0}/LICENSE +0 -0
- {ephaptic-0.2.7 → ephaptic-0.3.0}/ephaptic/cli/__init__.py +0 -0
- {ephaptic-0.2.7 → ephaptic-0.3.0}/ephaptic/client/__init__.py +0 -0
- {ephaptic-0.2.7 → ephaptic-0.3.0}/ephaptic/client/client.py +0 -0
- {ephaptic-0.2.7/ephaptic/adapters → ephaptic-0.3.0/ephaptic/ext}/__init__.py +0 -0
- {ephaptic-0.2.7 → ephaptic-0.3.0}/ephaptic/localproxy.py +0 -0
- {ephaptic-0.2.7 → ephaptic-0.3.0}/ephaptic.egg-info/dependency_links.txt +0 -0
- {ephaptic-0.2.7 → ephaptic-0.3.0}/ephaptic.egg-info/entry_points.txt +0 -0
- {ephaptic-0.2.7 → ephaptic-0.3.0}/ephaptic.egg-info/top_level.txt +0 -0
- {ephaptic-0.2.7 → ephaptic-0.3.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ephaptic
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: The Python client/server package for ephaptic.
|
|
5
5
|
Author-email: uukelele <robustrobot11@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -37,8 +37,13 @@ Requires-Dist: pydantic>=2.0
|
|
|
37
37
|
Requires-Dist: typer[standard]>=0.20.0
|
|
38
38
|
Requires-Dist: python-dotenv
|
|
39
39
|
Requires-Dist: watchfiles
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
Requires-Dist: redis
|
|
41
|
+
Provides-Extra: test
|
|
42
|
+
Requires-Dist: pytest; extra == "test"
|
|
43
|
+
Requires-Dist: pytest-asyncio; extra == "test"
|
|
44
|
+
Requires-Dist: fastapi; extra == "test"
|
|
45
|
+
Requires-Dist: uvicorn; extra == "test"
|
|
46
|
+
Requires-Dist: httpx; extra == "test"
|
|
42
47
|
Dynamic: license-file
|
|
43
48
|
|
|
44
49
|
<div align="center">
|
|
@@ -51,15 +56,7 @@ Dynamic: license-file
|
|
|
51
56
|
<br>
|
|
52
57
|
<h1>ephaptic</h1>
|
|
53
58
|
<br>
|
|
54
|
-
<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"
|
|
55
|
-
<img alt="PyPI - Version"
|
|
56
|
-
src="https://img.shields.io/pypi/v/ephaptic?style=for-the-badge&labelColor=%23222222">
|
|
57
|
-
</a>
|
|
58
|
-
|
|
59
|
-
<a href="https://www.npmjs.com/package/@ephaptic/client">
|
|
60
|
-
<img alt="NPM - Version"
|
|
61
|
-
src="https://img.shields.io/npm/v/%40ephaptic%2Fclient?style=for-the-badge&labelColor=%23222222">
|
|
62
|
-
</a>
|
|
59
|
+
<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" /> <img alt="GitHub Actions Workflow Status" src="https://img.shields.io/github/actions/workflow/status/ephaptic/ephaptic/tests.yml?style=for-the-badge&label=tests&labelColor=%23222222" /> <a href="https://pypi.org/project/ephaptic/"><img alt="PyPI - Version" src="https://img.shields.io/pypi/v/ephaptic?style=for-the-badge&labelColor=%23222222" /></a> <a href="https://www.npmjs.com/package/@ephaptic/client"><img alt="NPM - Version" src="https://img.shields.io/npm/v/%40ephaptic%2Fclient?style=for-the-badge&labelColor=%23222222" /></a>
|
|
63
60
|
|
|
64
61
|
|
|
65
62
|
</div>
|
|
@@ -92,18 +89,10 @@ What are you waiting for? **Let's go.**
|
|
|
92
89
|
<details>
|
|
93
90
|
<summary>Python</summary>
|
|
94
91
|
|
|
95
|
-
<h4>Client:</h4>
|
|
96
|
-
|
|
97
92
|
```
|
|
98
93
|
$ pip install ephaptic
|
|
99
94
|
```
|
|
100
95
|
|
|
101
|
-
<h4>Server:</h4>
|
|
102
|
-
|
|
103
|
-
```
|
|
104
|
-
$ pip install ephaptic[server]
|
|
105
|
-
```
|
|
106
|
-
|
|
107
96
|
```python
|
|
108
97
|
from fastapi import FastAPI # or `from quart import Quart`
|
|
109
98
|
from ephaptic import Ephaptic
|
|
@@ -142,13 +131,19 @@ But what if your code throws an error? No sweat, it just throws up on the fronte
|
|
|
142
131
|
And, want to say something to the frontend?
|
|
143
132
|
|
|
144
133
|
```python
|
|
145
|
-
|
|
134
|
+
class Notification(BaseModel):
|
|
135
|
+
message: str
|
|
136
|
+
priority: Literal["high", "low", "default"]
|
|
137
|
+
|
|
138
|
+
await ephaptic.to(user1, user2).emit(Notification(message="Hello, world!", priority="high"))
|
|
146
139
|
```
|
|
147
140
|
|
|
148
141
|
To create a schema of your RPC endpoints:
|
|
149
142
|
|
|
150
143
|
```
|
|
151
|
-
$ ephaptic src.app:app -o schema.json # --watch to run in background and auto-reload on file change.
|
|
144
|
+
$ ephaptic generate src.app:app -o schema.json # --watch to run in background and auto-reload on file change.
|
|
145
|
+
$ # Or:
|
|
146
|
+
$ ephaptic generate src.app:app -o ephaptic.d.ts # converts directly to typescript
|
|
152
147
|
```
|
|
153
148
|
|
|
154
149
|
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.
|
|
@@ -166,7 +161,7 @@ async def load_identity(auth): # You can use synchronous functions here too.
|
|
|
166
161
|
return user_id
|
|
167
162
|
```
|
|
168
163
|
|
|
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.)
|
|
164
|
+
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
165
|
|
|
171
166
|
</details>
|
|
172
167
|
|
|
@@ -206,8 +201,7 @@ const client = connect({ url: '...', auth: { token: window.localStorage.getItem(
|
|
|
206
201
|
And you can load types, too.
|
|
207
202
|
|
|
208
203
|
```
|
|
209
|
-
$
|
|
210
|
-
$ npx @ephaptic/type-gen ./schema.json -o schema.d.ts # --watch to auto-reload upon changes
|
|
204
|
+
$ ephaptic from-schema ./schema.json -o schema.d.ts # --watch to auto-reload upon changes
|
|
211
205
|
```
|
|
212
206
|
|
|
213
207
|
```typescript
|
|
@@ -8,15 +8,7 @@
|
|
|
8
8
|
<br>
|
|
9
9
|
<h1>ephaptic</h1>
|
|
10
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"
|
|
12
|
-
<img alt="PyPI - Version"
|
|
13
|
-
src="https://img.shields.io/pypi/v/ephaptic?style=for-the-badge&labelColor=%23222222">
|
|
14
|
-
</a>
|
|
15
|
-
|
|
16
|
-
<a href="https://www.npmjs.com/package/@ephaptic/client">
|
|
17
|
-
<img alt="NPM - Version"
|
|
18
|
-
src="https://img.shields.io/npm/v/%40ephaptic%2Fclient?style=for-the-badge&labelColor=%23222222">
|
|
19
|
-
</a>
|
|
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" /> <img alt="GitHub Actions Workflow Status" src="https://img.shields.io/github/actions/workflow/status/ephaptic/ephaptic/tests.yml?style=for-the-badge&label=tests&labelColor=%23222222" /> <a href="https://pypi.org/project/ephaptic/"><img alt="PyPI - Version" src="https://img.shields.io/pypi/v/ephaptic?style=for-the-badge&labelColor=%23222222" /></a> <a href="https://www.npmjs.com/package/@ephaptic/client"><img alt="NPM - Version" src="https://img.shields.io/npm/v/%40ephaptic%2Fclient?style=for-the-badge&labelColor=%23222222" /></a>
|
|
20
12
|
|
|
21
13
|
|
|
22
14
|
</div>
|
|
@@ -49,18 +41,10 @@ What are you waiting for? **Let's go.**
|
|
|
49
41
|
<details>
|
|
50
42
|
<summary>Python</summary>
|
|
51
43
|
|
|
52
|
-
<h4>Client:</h4>
|
|
53
|
-
|
|
54
44
|
```
|
|
55
45
|
$ pip install ephaptic
|
|
56
46
|
```
|
|
57
47
|
|
|
58
|
-
<h4>Server:</h4>
|
|
59
|
-
|
|
60
|
-
```
|
|
61
|
-
$ pip install ephaptic[server]
|
|
62
|
-
```
|
|
63
|
-
|
|
64
48
|
```python
|
|
65
49
|
from fastapi import FastAPI # or `from quart import Quart`
|
|
66
50
|
from ephaptic import Ephaptic
|
|
@@ -99,13 +83,19 @@ But what if your code throws an error? No sweat, it just throws up on the fronte
|
|
|
99
83
|
And, want to say something to the frontend?
|
|
100
84
|
|
|
101
85
|
```python
|
|
102
|
-
|
|
86
|
+
class Notification(BaseModel):
|
|
87
|
+
message: str
|
|
88
|
+
priority: Literal["high", "low", "default"]
|
|
89
|
+
|
|
90
|
+
await ephaptic.to(user1, user2).emit(Notification(message="Hello, world!", priority="high"))
|
|
103
91
|
```
|
|
104
92
|
|
|
105
93
|
To create a schema of your RPC endpoints:
|
|
106
94
|
|
|
107
95
|
```
|
|
108
|
-
$ ephaptic src.app:app -o schema.json # --watch to run in background and auto-reload on file change.
|
|
96
|
+
$ ephaptic generate src.app:app -o schema.json # --watch to run in background and auto-reload on file change.
|
|
97
|
+
$ # Or:
|
|
98
|
+
$ ephaptic generate src.app:app -o ephaptic.d.ts # converts directly to typescript
|
|
109
99
|
```
|
|
110
100
|
|
|
111
101
|
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.
|
|
@@ -123,7 +113,7 @@ async def load_identity(auth): # You can use synchronous functions here too.
|
|
|
123
113
|
return user_id
|
|
124
114
|
```
|
|
125
115
|
|
|
126
|
-
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.)
|
|
116
|
+
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.)
|
|
127
117
|
|
|
128
118
|
</details>
|
|
129
119
|
|
|
@@ -163,8 +153,7 @@ const client = connect({ url: '...', auth: { token: window.localStorage.getItem(
|
|
|
163
153
|
And you can load types, too.
|
|
164
154
|
|
|
165
155
|
```
|
|
166
|
-
$
|
|
167
|
-
$ npx @ephaptic/type-gen ./schema.json -o schema.d.ts # --watch to auto-reload upon changes
|
|
156
|
+
$ ephaptic from-schema ./schema.json -o schema.d.ts # --watch to auto-reload upon changes
|
|
168
157
|
```
|
|
169
158
|
|
|
170
159
|
```typescript
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import sys, os, subprocess as sp
|
|
2
|
+
import json, re
|
|
3
|
+
import inspect, importlib, typing
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from pydantic import TypeAdapter
|
|
10
|
+
|
|
11
|
+
from ephaptic import Ephaptic
|
|
12
|
+
from ephaptic.decorators import META_KEY
|
|
13
|
+
|
|
14
|
+
from typing import *
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(help="Ephaptic CLI tool.")
|
|
17
|
+
|
|
18
|
+
IDENTIFIER_REGEX = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]*$')
|
|
19
|
+
|
|
20
|
+
LOG: List[str] = []
|
|
21
|
+
|
|
22
|
+
def log(*data):
|
|
23
|
+
global LOG
|
|
24
|
+
LOG.append(' '.join(data))
|
|
25
|
+
|
|
26
|
+
def clear_log():
|
|
27
|
+
global LOG
|
|
28
|
+
LOG = []
|
|
29
|
+
|
|
30
|
+
def key_name(key: str) -> str:
|
|
31
|
+
if IDENTIFIER_REGEX.match(key): return key
|
|
32
|
+
else: return json.dumps(key)
|
|
33
|
+
|
|
34
|
+
def validate(name: str) -> str:
|
|
35
|
+
if IDENTIFIER_REGEX.match(name):
|
|
36
|
+
safe = re.sub(r'[^a-zA-Z0-9_$]', '_', name)
|
|
37
|
+
log(typer.style("[warning] '{name}' is not a valid identifier. sanitizing to '{safe}'", fg=typer.colors.YELLOW))
|
|
38
|
+
return safe
|
|
39
|
+
return name
|
|
40
|
+
|
|
41
|
+
def load_schema(input_path: str | Path):
|
|
42
|
+
return json.loads(Path(input_path).read_text())
|
|
43
|
+
# errors can be handled by typer
|
|
44
|
+
|
|
45
|
+
def load_ephaptic(import_name: str) -> Ephaptic:
|
|
46
|
+
try:
|
|
47
|
+
from dotenv import load_dotenv; load_dotenv()
|
|
48
|
+
except: ...
|
|
49
|
+
|
|
50
|
+
sys.path.insert(0, os.getcwd())
|
|
51
|
+
|
|
52
|
+
if ":" not in import_name:
|
|
53
|
+
log(typer.style(f"Warning: Import name did not specify client name. Defaulting to `client`.", fg=typer.colors.YELLOW))
|
|
54
|
+
import_name += ":client" # default: expect client to be named `client` inside the file
|
|
55
|
+
|
|
56
|
+
module_name, var_name = import_name.split(":", 1)
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
log(typer.style(f"Attempting to import `{var_name}` from `{module_name}`..."))
|
|
60
|
+
module = importlib.import_module(module_name)
|
|
61
|
+
except ImportError as e:
|
|
62
|
+
typer.echo(typer.style(f"Error: Can't import '{module_name}'.\n{e}", fg=typer.colors.RED))
|
|
63
|
+
raise typer.Exit(1)
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
instance = getattr(module, var_name)
|
|
67
|
+
except AttributeError:
|
|
68
|
+
typer.echo(typer.style(f"Error: Variable '{var_name}' not found in module '{module_name}'.", fg=typer.colors.RED))
|
|
69
|
+
raise typer.Exit(1)
|
|
70
|
+
|
|
71
|
+
if not isinstance(instance, Ephaptic):
|
|
72
|
+
typer.echo(typer.style(f"Error: '{var_name}' is not an Ephaptic client. It is type: {type(instance)}", fg=typer.colors.RED))
|
|
73
|
+
raise typer.Exit(1)
|
|
74
|
+
|
|
75
|
+
return instance
|
|
76
|
+
|
|
77
|
+
def TS_resolve_type(schema: Dict[str, Any]) -> str:
|
|
78
|
+
if not schema: return 'any'
|
|
79
|
+
if schema.get('$ref'): return validate(schema['$ref'].split('/').pop() or 'any')
|
|
80
|
+
if schema.get('enum'): return ' | '.join([json.dumps(val) for val in schema['enum']])
|
|
81
|
+
if schema.get('anyOf'): return ' | '.join({TS_resolve_type(s) for s in schema['anyOf']})
|
|
82
|
+
if schema.get('type') == 'array': return f"{TS_resolve_type(schema['items']) if schema.get('items') else 'any'}[]"
|
|
83
|
+
if schema.get('type') in ('integer', 'number'): return 'number'
|
|
84
|
+
if schema.get('type') == 'boolean': return 'boolean'
|
|
85
|
+
if schema.get('type') == 'string': return 'string'
|
|
86
|
+
if schema.get('type') == 'null': return 'null'
|
|
87
|
+
|
|
88
|
+
if schema['type'] == 'object':
|
|
89
|
+
if not schema.get('properties'): return 'Record<string, any>'
|
|
90
|
+
props = [
|
|
91
|
+
f"{key_name(key)}{'' if key in schema.get('required', []) else '?'}: {TS_resolve_type(prop_schema)}"
|
|
92
|
+
for key, prop_schema in schema['properties'].items()
|
|
93
|
+
]
|
|
94
|
+
return '{ ' + '; '.join(props) + ' }'
|
|
95
|
+
|
|
96
|
+
return 'any'
|
|
97
|
+
|
|
98
|
+
def TS_generate(data: dict):
|
|
99
|
+
lines: List[str] = []
|
|
100
|
+
|
|
101
|
+
lines.extend([
|
|
102
|
+
'/**',
|
|
103
|
+
' * Auto-generated by ephaptic',
|
|
104
|
+
' * Do not edit this file manually.',
|
|
105
|
+
' * */',
|
|
106
|
+
'',
|
|
107
|
+
'import { type EphapticClientBase } from "@ephaptic/client";',
|
|
108
|
+
'',
|
|
109
|
+
'export type EphapticQuery<TArgs extends any[], TReturn> = { queryKey: [string, ...TArgs]; queryFn: () => Promise<TReturn>; }'
|
|
110
|
+
'',
|
|
111
|
+
])
|
|
112
|
+
|
|
113
|
+
for name, schema in data.get('definitions', {}).items():
|
|
114
|
+
name = validate(name)
|
|
115
|
+
if schema['type'] == 'object':
|
|
116
|
+
lines.append(f'export interface {name} {{')
|
|
117
|
+
lines.extend([
|
|
118
|
+
f" {validate(prop_name)}{'' if prop_name in schema.get('required', []) else '?'}: {TS_resolve_type(prop_schema)};"
|
|
119
|
+
for prop_name, prop_schema in schema['properties'].items()
|
|
120
|
+
])
|
|
121
|
+
lines.append('}')
|
|
122
|
+
lines.append('')
|
|
123
|
+
else:
|
|
124
|
+
lines.append(f'export type {name} = {TS_resolve_type(schema)};')
|
|
125
|
+
lines.append('')
|
|
126
|
+
|
|
127
|
+
lines.append('export interface EphapticEvents {')
|
|
128
|
+
lines.extend([
|
|
129
|
+
f" {validate(event_name)}: {TS_resolve_type(event_schema)};"
|
|
130
|
+
for event_name, event_schema in data.get('events', {}).items()
|
|
131
|
+
])
|
|
132
|
+
lines.append('}')
|
|
133
|
+
lines.append('')
|
|
134
|
+
|
|
135
|
+
lines.append('export interface EphapticService extends EphapticClientBase {')
|
|
136
|
+
lines.append('')
|
|
137
|
+
|
|
138
|
+
for method_name, method_data in data.get('methods', {}).items():
|
|
139
|
+
args: List[str] = []
|
|
140
|
+
|
|
141
|
+
args.extend([
|
|
142
|
+
f"{validate(arg_name)}{'' if arg_name in method_data.get('required', []) else '?'}: {TS_resolve_type(arg_schema)}"
|
|
143
|
+
for arg_name, arg_schema in method_data.get('args', {}).items()
|
|
144
|
+
])
|
|
145
|
+
|
|
146
|
+
return_type = TS_resolve_type(method_data['return']) if method_data.get('return') else 'void'
|
|
147
|
+
|
|
148
|
+
lines.append(f" {validate(method_name)}({', '.join(args)}): Promise<{return_type}>;")
|
|
149
|
+
|
|
150
|
+
lines.append('')
|
|
151
|
+
lines.append(' queries: {')
|
|
152
|
+
|
|
153
|
+
for method_name, method_data in data.get('methods', {}).items():
|
|
154
|
+
args: List[str] = []
|
|
155
|
+
|
|
156
|
+
args.extend([
|
|
157
|
+
f"{validate(arg_name)}{'' if arg_name in method_data.get('required', {}) else '?'}: {TS_resolve_type(arg_schema)}"
|
|
158
|
+
for arg_name, arg_schema in method_data.get('args', {}).items()
|
|
159
|
+
])
|
|
160
|
+
|
|
161
|
+
arg_types = [TS_resolve_type(arg_schema) for arg_schema in method_data.get('args', {}).values()]
|
|
162
|
+
|
|
163
|
+
return_type = TS_resolve_type(method_data['return']) if method_data.get('return') else 'void'
|
|
164
|
+
|
|
165
|
+
lines.append(f" {validate(method_name)}({', '.join(args)}): EphapticQuery<[{', '.join(arg_types)}], {return_type}>;")
|
|
166
|
+
|
|
167
|
+
lines.append(' };')
|
|
168
|
+
|
|
169
|
+
lines.append('')
|
|
170
|
+
|
|
171
|
+
lines.append(' on<K extends keyof EphapticEvents>(event: K, callback: (data: EphapticEvents[K]) => void): void;')
|
|
172
|
+
lines.append(' off<K extends keyof EphapticEvents>(event: K, callback: (data: EphapticEvents[K]) => void): void;')
|
|
173
|
+
lines.append(' once<K extends keyof EphapticEvents>(event: K, callback: (data: EphapticEvents[K]) => void): void;')
|
|
174
|
+
|
|
175
|
+
lines.append('}');
|
|
176
|
+
lines.append('');
|
|
177
|
+
|
|
178
|
+
lines.extend([
|
|
179
|
+
'/**',
|
|
180
|
+
' * Usage:',
|
|
181
|
+
' * import { connect } from "@ephaptic/client";',
|
|
182
|
+
' * import { type EphapticService } from "./ephaptic";',
|
|
183
|
+
' * ',
|
|
184
|
+
' * const client = connect(...) as unknown as EphapticService;',
|
|
185
|
+
' */',
|
|
186
|
+
])
|
|
187
|
+
|
|
188
|
+
return lines
|
|
189
|
+
|
|
190
|
+
def KT_resolve_type(schema: Dict[str, Any]) -> str:
|
|
191
|
+
if not schema: return 'Any?'
|
|
192
|
+
if schema.get('$ref'): return validate(schema['$ref'].split('/').pop() or 'Any')
|
|
193
|
+
if schema.get('enum') and len(schema['enum']) > 0:
|
|
194
|
+
first = schema['enum'][0]
|
|
195
|
+
if isinstance(first, str): return 'String'
|
|
196
|
+
if isinstance(first, int): return 'Int'
|
|
197
|
+
if isinstance(first, float): return 'Double'
|
|
198
|
+
if isinstance(first, bool): return 'Boolean'
|
|
199
|
+
return 'Any?'
|
|
200
|
+
if schema.get('anyOf'):
|
|
201
|
+
nonNull = [t for t in schema['anyOf'] if t['type'] != 'null']
|
|
202
|
+
if len(nonNull) == 1:
|
|
203
|
+
type = KT_resolve_type(nonNull[0])
|
|
204
|
+
if not type.endswith('?'): type += '?'
|
|
205
|
+
return type
|
|
206
|
+
return 'Any?'
|
|
207
|
+
if schema.get('type') == 'array': return f"List<{KT_resolve_type(schema['items']) if schema.get('items') else 'Any?'}>"
|
|
208
|
+
if schema.get('type') == 'integer': return 'Long'
|
|
209
|
+
if schema.get('type') == 'boolean': return 'Boolean'
|
|
210
|
+
if schema.get('type') == 'string': return 'String'
|
|
211
|
+
if schema.get('type') == 'null': return 'Nothing?'
|
|
212
|
+
if schema.get('type') == 'object': return 'Map<String, Any?>'
|
|
213
|
+
return 'Any?'
|
|
214
|
+
|
|
215
|
+
def KT_generate(data: dict, package_name: str):
|
|
216
|
+
lines: List[str] = []
|
|
217
|
+
|
|
218
|
+
lines.extend([
|
|
219
|
+
'/**',
|
|
220
|
+
' * Auto-generated by ephaptic',
|
|
221
|
+
' * Do not edit this file manually.',
|
|
222
|
+
' * */',
|
|
223
|
+
'',
|
|
224
|
+
f'package {package_name}',
|
|
225
|
+
'',
|
|
226
|
+
'import com.squareup.moshi.JsonClass',
|
|
227
|
+
'import com.ephaptic.android.EphapticClient',
|
|
228
|
+
'import com.ephaptic.android.EphapticException',
|
|
229
|
+
'',
|
|
230
|
+
])
|
|
231
|
+
|
|
232
|
+
for name, schema in data.get('definitions', {}).items():
|
|
233
|
+
name = validate(name)
|
|
234
|
+
if schema['type'] == 'object':
|
|
235
|
+
lines.append('@JsonClass(generateAdapter = true)')
|
|
236
|
+
lines.append(f'data class {name}(')
|
|
237
|
+
|
|
238
|
+
for prop_name, prop_schema in schema['properties'].items():
|
|
239
|
+
kt_type = KT_resolve_type(prop_schema)
|
|
240
|
+
|
|
241
|
+
required = prop_name in schema.get('required', [])
|
|
242
|
+
explicit_null = prop_schema.get('type') == 'null'
|
|
243
|
+
union_null = any(t.get('type') == 'null' for t in prop_schema.get('anyOf', []))
|
|
244
|
+
|
|
245
|
+
nullable = not required or explicit_null or union_null
|
|
246
|
+
if nullable and not kt_type.endswith('?'): kt_type += '?'
|
|
247
|
+
|
|
248
|
+
lines.append(f" val {validate(prop_name)}: {kt_type}{' = null' if nullable else ''},")
|
|
249
|
+
|
|
250
|
+
lines.append(')')
|
|
251
|
+
lines.append('')
|
|
252
|
+
else:
|
|
253
|
+
lines.append(f'typealias {name} = {KT_resolve_type(schema)}')
|
|
254
|
+
lines.append('')
|
|
255
|
+
|
|
256
|
+
lines.append('sealed class EphapticEvent')
|
|
257
|
+
lines.extend([
|
|
258
|
+
f" data class {validate(event_name)}(val data: {KT_resolve_type(event_schema)}): EphapticEvent()"
|
|
259
|
+
for event_name, event_schema in data.get('events', {}).items()
|
|
260
|
+
])
|
|
261
|
+
lines.append('}')
|
|
262
|
+
lines.append('')
|
|
263
|
+
|
|
264
|
+
lines.append('class EphapticService(private val client: EphapticClient) {')
|
|
265
|
+
lines.append('')
|
|
266
|
+
|
|
267
|
+
for method_name, method_data in data.get('methods', {}).items():
|
|
268
|
+
args: List[str] = []
|
|
269
|
+
params: List[str] = []
|
|
270
|
+
|
|
271
|
+
for arg_name, arg_schema in method_data.get('args', {}).items():
|
|
272
|
+
is_req = arg_name in method_data.get('required', [])
|
|
273
|
+
kt_type = KT_resolve_type(arg_schema)
|
|
274
|
+
|
|
275
|
+
if not is_req and not kt_type.endswith('?'): kt_type += '?'
|
|
276
|
+
|
|
277
|
+
args.append(f"{validate(arg_name)}: {kt_type}")
|
|
278
|
+
params.append(validate(arg_name))
|
|
279
|
+
|
|
280
|
+
return_type = KT_resolve_type(method_data['return']) if method_data.get('return') else 'Any?'
|
|
281
|
+
|
|
282
|
+
lines.append(f" suspend fun {validate(method_name)}({', '.join(args)}): {return_type} {{")
|
|
283
|
+
lines.append(f' return client.request<{return_type}>("{method_name}", {", ".join(params)})')
|
|
284
|
+
lines.append(' }')
|
|
285
|
+
lines.append('')
|
|
286
|
+
|
|
287
|
+
lines.append('}')
|
|
288
|
+
lines.append('')
|
|
289
|
+
|
|
290
|
+
return lines
|
|
291
|
+
|
|
292
|
+
def create_schema(adapter: TypeAdapter, definitions: dict) -> dict:
|
|
293
|
+
schema = adapter.json_schema(ref_template='#/definitions/{model}')
|
|
294
|
+
|
|
295
|
+
if '$defs' in schema:
|
|
296
|
+
definitions.update(schema.pop('$defs'))
|
|
297
|
+
|
|
298
|
+
if schema.get('type') == 'object' and 'title' in schema:
|
|
299
|
+
model = schema['title']
|
|
300
|
+
definitions[model] = schema
|
|
301
|
+
return { '$ref': f'#/definitions/{model}' }
|
|
302
|
+
|
|
303
|
+
return schema
|
|
304
|
+
|
|
305
|
+
def run_subprocess():
|
|
306
|
+
cmd = [sys.executable]
|
|
307
|
+
cmd += [arg for arg in sys.argv if arg not in {'--watch', '-w'}]
|
|
308
|
+
sp.run(cmd)
|
|
309
|
+
|
|
310
|
+
def calculate_language(lang: str, output: Path):
|
|
311
|
+
if lang is None:
|
|
312
|
+
if not output or str(output) == '-': raise ValueError("You must specify a language or an output path.")
|
|
313
|
+
lang = os.path.splitext(output)[-1]
|
|
314
|
+
|
|
315
|
+
map = {
|
|
316
|
+
'kotlin': 'kt',
|
|
317
|
+
'typescript': 'ts',
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if lang in map: lang = map[lang]
|
|
321
|
+
|
|
322
|
+
if output is None:
|
|
323
|
+
match lang:
|
|
324
|
+
case 'ts':
|
|
325
|
+
output = Path('ephaptic.d.ts')
|
|
326
|
+
case 'kt':
|
|
327
|
+
output = Path('Ephaptic.kt')
|
|
328
|
+
case _:
|
|
329
|
+
output = Path('schema.json')
|
|
330
|
+
|
|
331
|
+
return lang, output
|
|
332
|
+
|
|
333
|
+
class NothingToChange(Exception): ...
|
|
334
|
+
|
|
335
|
+
def generate_output(lang, schema_output, package_name, output: Path):
|
|
336
|
+
content = None
|
|
337
|
+
match lang:
|
|
338
|
+
case 'json':
|
|
339
|
+
content = json.dumps(schema_output, indent=2)
|
|
340
|
+
case 'ts':
|
|
341
|
+
content = '\n'.join(TS_generate(schema_output))
|
|
342
|
+
case 'kt':
|
|
343
|
+
content = '\n'.join(KT_generate(schema_output, package_name))
|
|
344
|
+
|
|
345
|
+
if str(output) == '-':
|
|
346
|
+
print(content)
|
|
347
|
+
return
|
|
348
|
+
|
|
349
|
+
if output.exists():
|
|
350
|
+
if output.read_text() == content:
|
|
351
|
+
return
|
|
352
|
+
else:
|
|
353
|
+
output.write_text(content)
|
|
354
|
+
|
|
355
|
+
for line in LOG:
|
|
356
|
+
typer.echo(line)
|
|
357
|
+
|
|
358
|
+
clear_log()
|
|
359
|
+
|
|
360
|
+
typer.secho(f"Schema generated to `{output}`.", fg=typer.colors.GREEN, bold=True)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
@app.command()
|
|
364
|
+
def generate(
|
|
365
|
+
client: str = typer.Argument('app:client', help="The import string for the Ephaptic client."),
|
|
366
|
+
output: Path = typer.Option(None, '--output', '-o', help="Output path for the generated file (default: schema.json / ephaptic.d.ts / Ephaptic.kt)."),
|
|
367
|
+
watch: bool = typer.Option(False, '--watch', '-w', help="Watch for changes in `.py` files and regenerate schema file automatically."),
|
|
368
|
+
lang: str = typer.Option(None, '--lang', '-l', help="Output language ('json', 'kotlin', 'kt', 'typescript', 'ts') (default: autodetected from output path)"),
|
|
369
|
+
package_name: str = typer.Option('com.example.app', '--package-name', '-p', help="Package name (required for Kotlin)")
|
|
370
|
+
):
|
|
371
|
+
lang, output = calculate_language(lang, output)
|
|
372
|
+
|
|
373
|
+
if watch:
|
|
374
|
+
import watchfiles
|
|
375
|
+
|
|
376
|
+
cwd = os.getcwd()
|
|
377
|
+
typer.secho(f"Watching for changes ({cwd})...", fg=typer.colors.GREEN)
|
|
378
|
+
|
|
379
|
+
run_subprocess()
|
|
380
|
+
|
|
381
|
+
for changes in watchfiles.watch(cwd):
|
|
382
|
+
if any(f.endswith('.py') for _, f in changes):
|
|
383
|
+
typer.secho("Detected changes, regenerating...")
|
|
384
|
+
run_subprocess()
|
|
385
|
+
|
|
386
|
+
return
|
|
387
|
+
|
|
388
|
+
ephaptic = load_ephaptic(client)
|
|
389
|
+
|
|
390
|
+
schema_output = {
|
|
391
|
+
"methods": {},
|
|
392
|
+
"events": {},
|
|
393
|
+
"definitions": {},
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
log(typer.style("--- Functions ---"))
|
|
397
|
+
|
|
398
|
+
for name, func in ephaptic._exposed_functions.items():
|
|
399
|
+
log(typer.style(f" - {name}"))
|
|
400
|
+
|
|
401
|
+
meta = getattr(func, META_KEY, {})
|
|
402
|
+
|
|
403
|
+
hints = meta.get('hints') or typing.get_type_hints(func)
|
|
404
|
+
sig = meta.get('sig') or inspect.signature(func)
|
|
405
|
+
|
|
406
|
+
method_schema = {
|
|
407
|
+
"args": {},
|
|
408
|
+
"return": None,
|
|
409
|
+
"required": [],
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
for param_name, param in sig.parameters.items():
|
|
413
|
+
hint = hints.get(param_name, typing.Any)
|
|
414
|
+
adapter = TypeAdapter(hint)
|
|
415
|
+
|
|
416
|
+
method_schema["args"][param_name] = create_schema(
|
|
417
|
+
adapter,
|
|
418
|
+
schema_output["definitions"],
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
log(typer.style(f" - {param_name}: {hint} = {param.default}"))
|
|
422
|
+
|
|
423
|
+
if param.default == inspect.Parameter.empty:
|
|
424
|
+
method_schema["required"].append(param_name)
|
|
425
|
+
else:
|
|
426
|
+
method_schema["args"][param_name]["default"] = str(param.default)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
return_hint = meta.get('response_model') or hints.get("return", typing.Any)
|
|
431
|
+
if return_hint and return_hint is not type(None):
|
|
432
|
+
adapter = TypeAdapter(return_hint)
|
|
433
|
+
method_schema["return"] = create_schema(
|
|
434
|
+
adapter,
|
|
435
|
+
schema_output["definitions"],
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
schema_output["methods"][name] = method_schema
|
|
439
|
+
|
|
440
|
+
log(typer.style("--- Events ---"))
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
for name, model in ephaptic._exposed_events.items():
|
|
444
|
+
log(typer.style(f" - {name}"))
|
|
445
|
+
adapter = TypeAdapter(model)
|
|
446
|
+
|
|
447
|
+
schema_output["events"][name] = create_schema(
|
|
448
|
+
adapter,
|
|
449
|
+
schema_output["definitions"],
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
generate_output(lang, schema_output, package_name, output)
|
|
453
|
+
|
|
454
|
+
@app.command()
|
|
455
|
+
def from_schema(
|
|
456
|
+
schema_path: Path = typer.Option('schema.json', help="Path to the schema file."),
|
|
457
|
+
output: Path = typer.Option(None, '--output', '-o', help="Output path for the generated file (default: ephaptic.d.ts / Ephaptic.kt)."),
|
|
458
|
+
watch: bool = typer.Option(False, '--watch', '-w', help="Watch for changes in `.py` files and regenerate schema file automatically."),
|
|
459
|
+
lang: str = typer.Option(None, '--lang', '-l', help="Output language ('kotlin', 'kt', 'typescript', 'ts') (default: autodetected from output path)"),
|
|
460
|
+
package_name: str = typer.Option('com.example.app', '--package-name', '-p', help="Package name (required for Kotlin)")
|
|
461
|
+
):
|
|
462
|
+
lang, output = calculate_language(lang, output)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
if watch:
|
|
466
|
+
import watchfiles
|
|
467
|
+
|
|
468
|
+
typer.secho(f"Watching for changes ({schema_path})...", fg=typer.colors.GREEN)
|
|
469
|
+
|
|
470
|
+
run_subprocess()
|
|
471
|
+
|
|
472
|
+
for changes in watchfiles.watch(schema_path):
|
|
473
|
+
if any(Path(f).name == schema_path.name for _, f in changes):
|
|
474
|
+
typer.secho("Detected changes, regenerating...")
|
|
475
|
+
run_subprocess()
|
|
476
|
+
|
|
477
|
+
return
|
|
478
|
+
|
|
479
|
+
schema_output = json.loads(schema_path.read_text())
|
|
480
|
+
|
|
481
|
+
generate_output(lang, schema_output, package_name, output)
|
|
482
|
+
|
|
483
|
+
click = typer.main.get_command(app)
|
|
484
|
+
|
|
485
|
+
if __name__ == "__main__":
|
|
486
|
+
app()
|