pyrpc-codegen 0.7.4__tar.gz → 0.7.6__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.
- {pyrpc_codegen-0.7.4 → pyrpc_codegen-0.7.6}/PKG-INFO +2 -2
- {pyrpc_codegen-0.7.4 → pyrpc_codegen-0.7.6}/pyproject.toml +2 -2
- {pyrpc_codegen-0.7.4 → pyrpc_codegen-0.7.6}/src/pyrpc_codegen/ts_codegen.py +35 -5
- pyrpc_codegen-0.7.6/tests/test_codegen.py +255 -0
- pyrpc_codegen-0.7.4/tests/test_codegen.py +0 -91
- {pyrpc_codegen-0.7.4 → pyrpc_codegen-0.7.6}/.gitignore +0 -0
- {pyrpc_codegen-0.7.4 → pyrpc_codegen-0.7.6}/README.md +0 -0
- {pyrpc_codegen-0.7.4 → pyrpc_codegen-0.7.6}/src/pyrpc_codegen/__init__.py +0 -0
- {pyrpc_codegen-0.7.4 → pyrpc_codegen-0.7.6}/src/pyrpc_codegen/templates/client.ts.j2 +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pyrpc-codegen"
|
|
3
|
-
version = "0.7.
|
|
3
|
+
version = "0.7.6"
|
|
4
4
|
description = "Codegen and CLI tools for pyRPC"
|
|
5
5
|
requires-python = ">=3.11"
|
|
6
6
|
dependencies = [
|
|
7
7
|
"jinja2>=3.1.0",
|
|
8
|
-
"jsonschema-ts>=0.
|
|
8
|
+
"jsonschema-ts>=0.2.0",
|
|
9
9
|
]
|
|
10
10
|
|
|
11
11
|
[build-system]
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import re
|
|
3
|
+
import unicodedata
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
from typing import Any, Dict
|
|
5
6
|
|
|
6
7
|
from jinja2 import Environment, FileSystemLoader
|
|
7
8
|
from jsonschema_ts import Options as JsonschemaTsOptions
|
|
8
|
-
from jsonschema_ts import assemble, collect_defs, convert_all
|
|
9
|
+
from jsonschema_ts import assemble, collect_defs, convert_all, ensure_inline_models
|
|
9
10
|
|
|
10
11
|
DEFAULT_OUTPUT = "node_modules/@pyrpc/types/src/index.ts"
|
|
11
12
|
|
|
@@ -20,6 +21,27 @@ _TYPE_MAP: Dict[str, str] = {
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
def _to_safe_name(name: str) -> str:
|
|
25
|
+
s = unicodedata.normalize("NFKD", name).encode("ascii", "ignore").decode("ascii")
|
|
26
|
+
s = re.sub(r"[^a-zA-Z0-9]", " ", s)
|
|
27
|
+
s = _to_pascal_case(s)
|
|
28
|
+
return s or "GeneratedType"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _to_pascal_case(s: str) -> str:
|
|
32
|
+
parts = s.split()
|
|
33
|
+
result: list[str] = []
|
|
34
|
+
for part in parts:
|
|
35
|
+
sub_parts = re.findall(
|
|
36
|
+
r"[A-Z]?[a-z]+|[A-Z]+(?=[A-Z][a-z]|\d|\b)|[A-Z]+|\d+",
|
|
37
|
+
part,
|
|
38
|
+
)
|
|
39
|
+
if not sub_parts:
|
|
40
|
+
sub_parts = [part]
|
|
41
|
+
result.extend(sub_parts)
|
|
42
|
+
return "".join(seg.capitalize() for seg in result if seg)
|
|
43
|
+
|
|
44
|
+
|
|
23
45
|
def _pytype_to_ts(type_str: str) -> str:
|
|
24
46
|
if not type_str:
|
|
25
47
|
return "any"
|
|
@@ -31,7 +53,7 @@ def _pytype_to_ts(type_str: str) -> str:
|
|
|
31
53
|
name = name.rsplit('.', 1)[1]
|
|
32
54
|
if name in _TYPE_MAP:
|
|
33
55
|
return _TYPE_MAP[name]
|
|
34
|
-
return name
|
|
56
|
+
return _to_safe_name(name)
|
|
35
57
|
|
|
36
58
|
if type_str.startswith("typing."):
|
|
37
59
|
type_str = type_str[7:]
|
|
@@ -106,13 +128,21 @@ def _collect_schema_defs(schemas: Dict[str, Any]) -> dict:
|
|
|
106
128
|
else:
|
|
107
129
|
params = schema.parameters
|
|
108
130
|
for param in params:
|
|
109
|
-
|
|
131
|
+
if isinstance(param, dict):
|
|
132
|
+
js = param.get("schema") or param.get("schema_")
|
|
133
|
+
else:
|
|
134
|
+
js = param.schema_
|
|
110
135
|
if js:
|
|
111
136
|
schema_sources.append(js)
|
|
112
|
-
|
|
137
|
+
if isinstance(schema, dict):
|
|
138
|
+
rs = schema.get("return_schema")
|
|
139
|
+
else:
|
|
140
|
+
rs = schema.return_schema
|
|
113
141
|
if rs:
|
|
114
142
|
schema_sources.append(rs)
|
|
115
|
-
|
|
143
|
+
|
|
144
|
+
processed = ensure_inline_models(*schema_sources)
|
|
145
|
+
return collect_defs(*processed)
|
|
116
146
|
|
|
117
147
|
|
|
118
148
|
def generate_typescript_client(schemas: Dict[str, Any]) -> str:
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import tempfile
|
|
5
|
+
import pytest
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
from pydantic.dataclasses import dataclass
|
|
8
|
+
from pyrpc_core import rpc, default_router, get_registry_schema
|
|
9
|
+
from pyrpc_codegen import generate_typescript_client, save_typescript_client
|
|
10
|
+
from pyrpc_codegen.ts_codegen import _to_safe_name, _to_pascal_case, _collect_schema_defs
|
|
11
|
+
|
|
12
|
+
def _npx_works() -> bool:
|
|
13
|
+
"""Check if npx works via subprocess.run without shell=True (how jsonschema_ts calls it)."""
|
|
14
|
+
path = shutil.which("npx")
|
|
15
|
+
if not path:
|
|
16
|
+
return False
|
|
17
|
+
try:
|
|
18
|
+
import subprocess
|
|
19
|
+
result = subprocess.run(
|
|
20
|
+
["npx", "--version"],
|
|
21
|
+
capture_output=True, text=True, timeout=5, shell=False,
|
|
22
|
+
)
|
|
23
|
+
return result.returncode == 0
|
|
24
|
+
except (OSError, subprocess.SubprocessError):
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
npx_available = _npx_works()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.fixture(autouse=True)
|
|
31
|
+
def clear_registry():
|
|
32
|
+
default_router._procedures.clear()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_generate_typescript_client():
|
|
36
|
+
@rpc
|
|
37
|
+
def add(a: int, b: int) -> int:
|
|
38
|
+
"""Add two numbers."""
|
|
39
|
+
return a + b
|
|
40
|
+
|
|
41
|
+
schemas = get_registry_schema(default_router)
|
|
42
|
+
content = generate_typescript_client(schemas)
|
|
43
|
+
|
|
44
|
+
assert "export interface Types" in content
|
|
45
|
+
assert "add(a: number, b: number): Promise<number>;" in content
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_generate_typescript_client_empty():
|
|
49
|
+
schemas = get_registry_schema(default_router)
|
|
50
|
+
content = generate_typescript_client(schemas)
|
|
51
|
+
assert "export interface Types {" in content
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_save_typescript_client_from_file():
|
|
55
|
+
schemas = {
|
|
56
|
+
"greet": {
|
|
57
|
+
"name": "greet",
|
|
58
|
+
"doc": "Say hello",
|
|
59
|
+
"parameters": [
|
|
60
|
+
{"name": "name", "type": "<class 'str'>", "required": True, "default": None}
|
|
61
|
+
],
|
|
62
|
+
"return_type": "<class 'str'>",
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
67
|
+
schema_file = os.path.join(tmpdir, "schema.json")
|
|
68
|
+
with open(schema_file, "w") as f:
|
|
69
|
+
json.dump(schemas, f)
|
|
70
|
+
|
|
71
|
+
from pyrpc_core.cli import _load_schema
|
|
72
|
+
loaded = _load_schema(schema_file)
|
|
73
|
+
assert loaded == schemas
|
|
74
|
+
|
|
75
|
+
output_file = os.path.join(tmpdir, "types.ts")
|
|
76
|
+
save_typescript_client(schemas, output_file)
|
|
77
|
+
|
|
78
|
+
with open(output_file) as f:
|
|
79
|
+
content = f.read()
|
|
80
|
+
|
|
81
|
+
assert "export interface Types" in content
|
|
82
|
+
assert "greet(name: string): Promise<string>;" in content
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_save_typescript_client_serialized_schema():
|
|
86
|
+
@rpc
|
|
87
|
+
def add(a: int) -> int:
|
|
88
|
+
return a
|
|
89
|
+
|
|
90
|
+
schemas = get_registry_schema(default_router)
|
|
91
|
+
|
|
92
|
+
serializable = {}
|
|
93
|
+
for name, schema in schemas.items():
|
|
94
|
+
serializable[name] = {
|
|
95
|
+
"name": schema.name,
|
|
96
|
+
"doc": schema.doc or "",
|
|
97
|
+
"parameters": [
|
|
98
|
+
{"name": p.name, "type": p.type, "required": p.required, "default": p.default}
|
|
99
|
+
for p in schema.parameters
|
|
100
|
+
],
|
|
101
|
+
"return_type": schema.return_type,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
105
|
+
output_file = os.path.join(tmpdir, "types.ts")
|
|
106
|
+
save_typescript_client(serializable, output_file)
|
|
107
|
+
|
|
108
|
+
with open(output_file) as f:
|
|
109
|
+
content = f.read()
|
|
110
|
+
|
|
111
|
+
assert "export interface Types" in content
|
|
112
|
+
assert "add(a: number): Promise<number>;" in content
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_to_pascal_case():
|
|
116
|
+
assert _to_pascal_case("User") == "User"
|
|
117
|
+
assert _to_pascal_case("user") == "User"
|
|
118
|
+
assert _to_pascal_case("MyClass") == "MyClass"
|
|
119
|
+
assert _to_pascal_case("my_model") == "MyModel"
|
|
120
|
+
assert _to_pascal_case("") == ""
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_to_safe_name():
|
|
124
|
+
assert _to_safe_name("User") == "User"
|
|
125
|
+
assert _to_safe_name("my_model") == "MyModel"
|
|
126
|
+
assert _to_safe_name("MyClass") == "MyClass"
|
|
127
|
+
assert _to_safe_name("") == "GeneratedType"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_collect_defs_base_model():
|
|
131
|
+
class UserModel(BaseModel):
|
|
132
|
+
name: str
|
|
133
|
+
age: int
|
|
134
|
+
|
|
135
|
+
@rpc
|
|
136
|
+
def get_user(u: UserModel) -> UserModel:
|
|
137
|
+
return u
|
|
138
|
+
|
|
139
|
+
schemas = get_registry_schema(default_router)
|
|
140
|
+
defs = _collect_schema_defs(schemas)
|
|
141
|
+
assert "UserModel" in defs
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_collect_defs_at_model():
|
|
145
|
+
@dataclass
|
|
146
|
+
class Item:
|
|
147
|
+
name: str
|
|
148
|
+
price: float
|
|
149
|
+
|
|
150
|
+
@rpc
|
|
151
|
+
def buy(item: Item) -> Item:
|
|
152
|
+
return item
|
|
153
|
+
|
|
154
|
+
schemas = get_registry_schema(default_router)
|
|
155
|
+
defs = _collect_schema_defs(schemas)
|
|
156
|
+
assert "Item" in defs
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_collect_defs_nested_base_model():
|
|
160
|
+
class Address(BaseModel):
|
|
161
|
+
city: str
|
|
162
|
+
zip: str
|
|
163
|
+
|
|
164
|
+
class UserNested(BaseModel):
|
|
165
|
+
name: str
|
|
166
|
+
address: Address
|
|
167
|
+
|
|
168
|
+
@rpc
|
|
169
|
+
def get_user(u: UserNested) -> UserNested:
|
|
170
|
+
return u
|
|
171
|
+
|
|
172
|
+
schemas = get_registry_schema(default_router)
|
|
173
|
+
defs = _collect_schema_defs(schemas)
|
|
174
|
+
assert "UserNested" in defs
|
|
175
|
+
assert "Address" in defs
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_collect_defs_nested_at_model():
|
|
179
|
+
@dataclass
|
|
180
|
+
class Address:
|
|
181
|
+
city: str
|
|
182
|
+
zip: str
|
|
183
|
+
|
|
184
|
+
@dataclass
|
|
185
|
+
class Person:
|
|
186
|
+
name: str
|
|
187
|
+
address: Address
|
|
188
|
+
|
|
189
|
+
@rpc
|
|
190
|
+
def get_person(p: Person) -> Person:
|
|
191
|
+
return p
|
|
192
|
+
|
|
193
|
+
schemas = get_registry_schema(default_router)
|
|
194
|
+
defs = _collect_schema_defs(schemas)
|
|
195
|
+
assert "Person" in defs
|
|
196
|
+
assert "Address" in defs
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_collect_defs_nested_mixed():
|
|
200
|
+
class Address(BaseModel):
|
|
201
|
+
city: str
|
|
202
|
+
zip: str
|
|
203
|
+
|
|
204
|
+
@dataclass
|
|
205
|
+
class Person:
|
|
206
|
+
name: str
|
|
207
|
+
address: Address
|
|
208
|
+
|
|
209
|
+
class Organization(BaseModel):
|
|
210
|
+
name: str
|
|
211
|
+
owner: Person
|
|
212
|
+
|
|
213
|
+
@rpc
|
|
214
|
+
def get_org(o: Organization) -> Organization:
|
|
215
|
+
return o
|
|
216
|
+
|
|
217
|
+
schemas = get_registry_schema(default_router)
|
|
218
|
+
defs = _collect_schema_defs(schemas)
|
|
219
|
+
assert "Organization" in defs
|
|
220
|
+
assert "Person" in defs
|
|
221
|
+
assert "Address" in defs
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@pytest.mark.skipif(not npx_available, reason="requires npx (json-schema-to-typescript)")
|
|
225
|
+
def test_generate_typescript_client_with_base_model():
|
|
226
|
+
class UserModel(BaseModel):
|
|
227
|
+
name: str
|
|
228
|
+
age: int
|
|
229
|
+
email: str
|
|
230
|
+
|
|
231
|
+
@rpc
|
|
232
|
+
def create_user(user: UserModel) -> UserModel:
|
|
233
|
+
return user
|
|
234
|
+
|
|
235
|
+
schemas = get_registry_schema(default_router)
|
|
236
|
+
content = generate_typescript_client(schemas)
|
|
237
|
+
assert "export interface Types" in content
|
|
238
|
+
assert "UserModel" in content
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@pytest.mark.skipif(not npx_available, reason="requires npx (json-schema-to-typescript)")
|
|
242
|
+
def test_generate_typescript_client_with_at_model():
|
|
243
|
+
@dataclass
|
|
244
|
+
class Item:
|
|
245
|
+
name: str
|
|
246
|
+
price: float
|
|
247
|
+
|
|
248
|
+
@rpc
|
|
249
|
+
def buy_item(item: Item) -> Item:
|
|
250
|
+
return item
|
|
251
|
+
|
|
252
|
+
schemas = get_registry_schema(default_router)
|
|
253
|
+
content = generate_typescript_client(schemas)
|
|
254
|
+
assert "export interface Types" in content
|
|
255
|
+
assert "Item" in content
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import os
|
|
3
|
-
import tempfile
|
|
4
|
-
import pytest
|
|
5
|
-
from pyrpc_core import rpc, default_router, get_registry_schema
|
|
6
|
-
from pyrpc_codegen import generate_typescript_client, save_typescript_client
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@pytest.fixture(autouse=True)
|
|
10
|
-
def clear_registry():
|
|
11
|
-
default_router._procedures.clear()
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def test_generate_typescript_client():
|
|
15
|
-
@rpc
|
|
16
|
-
def add(a: int, b: int) -> int:
|
|
17
|
-
"""Add two numbers."""
|
|
18
|
-
return a + b
|
|
19
|
-
|
|
20
|
-
schemas = get_registry_schema(default_router)
|
|
21
|
-
content = generate_typescript_client(schemas)
|
|
22
|
-
|
|
23
|
-
assert "export interface Types" in content
|
|
24
|
-
assert "add(a: number, b: number): Promise<number>;" in content
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def test_generate_typescript_client_empty():
|
|
28
|
-
schemas = get_registry_schema(default_router)
|
|
29
|
-
content = generate_typescript_client(schemas)
|
|
30
|
-
assert "export interface Types {" in content
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def test_save_typescript_client_from_file():
|
|
34
|
-
schemas = {
|
|
35
|
-
"greet": {
|
|
36
|
-
"name": "greet",
|
|
37
|
-
"doc": "Say hello",
|
|
38
|
-
"parameters": [
|
|
39
|
-
{"name": "name", "type": "<class 'str'>", "required": True, "default": None}
|
|
40
|
-
],
|
|
41
|
-
"return_type": "<class 'str'>",
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
with tempfile.TemporaryDirectory() as tmpdir:
|
|
46
|
-
schema_file = os.path.join(tmpdir, "schema.json")
|
|
47
|
-
with open(schema_file, "w") as f:
|
|
48
|
-
json.dump(schemas, f)
|
|
49
|
-
|
|
50
|
-
from pyrpc_core.cli import _load_schema
|
|
51
|
-
loaded = _load_schema(schema_file)
|
|
52
|
-
assert loaded == schemas
|
|
53
|
-
|
|
54
|
-
output_file = os.path.join(tmpdir, "types.ts")
|
|
55
|
-
save_typescript_client(schemas, output_file)
|
|
56
|
-
|
|
57
|
-
with open(output_file) as f:
|
|
58
|
-
content = f.read()
|
|
59
|
-
|
|
60
|
-
assert "export interface Types" in content
|
|
61
|
-
assert "greet(name: string): Promise<string>;" in content
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def test_save_typescript_client_serialized_schema():
|
|
65
|
-
@rpc
|
|
66
|
-
def add(a: int) -> int:
|
|
67
|
-
return a
|
|
68
|
-
|
|
69
|
-
schemas = get_registry_schema(default_router)
|
|
70
|
-
|
|
71
|
-
serializable = {}
|
|
72
|
-
for name, schema in schemas.items():
|
|
73
|
-
serializable[name] = {
|
|
74
|
-
"name": schema.name,
|
|
75
|
-
"doc": schema.doc or "",
|
|
76
|
-
"parameters": [
|
|
77
|
-
{"name": p.name, "type": p.type, "required": p.required, "default": p.default}
|
|
78
|
-
for p in schema.parameters
|
|
79
|
-
],
|
|
80
|
-
"return_type": schema.return_type,
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
with tempfile.TemporaryDirectory() as tmpdir:
|
|
84
|
-
output_file = os.path.join(tmpdir, "types.ts")
|
|
85
|
-
save_typescript_client(serializable, output_file)
|
|
86
|
-
|
|
87
|
-
with open(output_file) as f:
|
|
88
|
-
content = f.read()
|
|
89
|
-
|
|
90
|
-
assert "export interface Types" in content
|
|
91
|
-
assert "add(a: number): Promise<number>;" in content
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|