pyrpc-codegen 0.7.5__tar.gz → 0.7.7__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.
@@ -1,161 +1,163 @@
1
- # Byte-compiled / optimized / DLL files
2
- __pycache__/
3
- *.py[cod]
4
- *$py.class
5
-
6
- # C extensions
7
- *.so
8
-
9
- # Distribution / packaging
10
- .Python
11
- build/
12
- develop-eggs/
13
- dist/
14
- downloads/
15
- eggs/
16
- .eggs/
17
- lib/
18
- !docs/lib/
19
- lib64/
20
- parts/
21
- sdist/
22
- var/
23
- wheels/
24
- share/python-wheels/
25
- *.egg-info/
26
- .installed.cfg
27
- *.egg
28
- MANIFEST
29
-
30
- # PyInstaller
31
- # Used for packaging Python scripts into standalone executables
32
- *.manifest
33
- *.spec
34
-
35
- # Installer logs
36
- pip-log.txt
37
- pip-delete-this-directory.txt
38
-
39
- # Unit test / coverage reports
40
- htmlcov/
41
- .tox/
42
- .nox/
43
- .coverage
44
- .coverage.*
45
- .cache
46
- nosetests.xml
47
- coverage.xml
48
- *.cover
49
- *.py,cover
50
- .stats
51
- .hypothesis/
52
- .pytest_cache/
53
- pytestdebug.log
54
-
55
- # Translations
56
- *.mo
57
- *.pot
58
-
59
- # Django stuff:
60
- *.log
61
- local_settings.py
62
- db.sqlite3
63
- db.sqlite3-journal
64
-
65
- # Flask stuff:
66
- instance/
67
- .webassets-cache
68
-
69
- # Scrapy stuff:
70
- .scrapy
71
-
72
- # Sphinx documentation
73
- docs/_build/
74
-
75
- # PyBuilder
76
- .pybuilder/
77
- target/
78
-
79
- # Jupyter Notebook
80
- .ipynb_checkpoints
81
-
82
- # IPython
83
- profile_default/
84
- ipython_config.py
85
-
86
- # pyenv
87
- # Project-specific python versions
88
- # .python-version
89
-
90
- # pipenv
91
- # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
- # However, in case of collaboration, if Pipfile.lock (and requirements.txt) are not
93
- # preferred, then add them into the ignore list.
94
- # Pipfile.lock
95
-
96
- # poetry
97
- # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
98
- # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
99
- # poetry.lock
100
-
101
- # pdm
102
- # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
103
- # pdm.lock
104
- # .pdm-python
105
-
106
- # PEP 582; used by e.g. github.com/fannheyward/coc-pyright
107
- __pypackages__/
108
-
109
- # Celery stuff
110
- celerybeat-schedule
111
- celerybeat.pid
112
-
113
- # SageMath parsed files
114
- *.sage.py
115
-
116
- # Environments
117
- .env
118
- .venv
119
- env/
120
- venv/
121
- ENV/
122
- env.bak/
123
- venv.bak/
124
-
125
- # Spyder project settings
126
- .spyderproject
127
- .spyderformpoint
128
-
129
- # Rope project settings
130
- .ropeproject
131
-
132
- # mkdocs documentation
133
- /site
134
-
135
- # mypy
136
- .mypy_cache/
137
- .dmypy.json
138
- dmypy.json
139
-
140
- # Pyre type checker
141
- .pyre/
142
-
143
- # pytype static type analyzer
144
- .pytype/
145
-
146
- # Cython debug symbols
147
- cython_debug/
148
-
149
- # OS X
150
- .DS_Store
151
-
152
- # Node modules
153
- node_modules
154
- dist
155
-
156
-
157
- # System design docs (local developer documentation)
158
- system-design/
159
-
160
- # Scripts
161
- scripts/seed_downloads.py
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ !docs/lib/
19
+ lib64/
20
+ parts/
21
+ sdist/
22
+ var/
23
+ wheels/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Used for packaging Python scripts into standalone executables
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .stats
51
+ .hypothesis/
52
+ .pytest_cache/
53
+ pytestdebug.log
54
+
55
+ # Translations
56
+ *.mo
57
+ *.pot
58
+
59
+ # Django stuff:
60
+ *.log
61
+ local_settings.py
62
+ db.sqlite3
63
+ db.sqlite3-journal
64
+
65
+ # Flask stuff:
66
+ instance/
67
+ .webassets-cache
68
+
69
+ # Scrapy stuff:
70
+ .scrapy
71
+
72
+ # Sphinx documentation
73
+ docs/_build/
74
+
75
+ # PyBuilder
76
+ .pybuilder/
77
+ target/
78
+
79
+ # Jupyter Notebook
80
+ .ipynb_checkpoints
81
+
82
+ # IPython
83
+ profile_default/
84
+ ipython_config.py
85
+
86
+ # pyenv
87
+ # Project-specific python versions
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if Pipfile.lock (and requirements.txt) are not
93
+ # preferred, then add them into the ignore list.
94
+ # Pipfile.lock
95
+
96
+ # poetry
97
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
98
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
99
+ # poetry.lock
100
+
101
+ # pdm
102
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
103
+ # pdm.lock
104
+ # .pdm-python
105
+
106
+ # PEP 582; used by e.g. github.com/fannheyward/coc-pyright
107
+ __pypackages__/
108
+
109
+ # Celery stuff
110
+ celerybeat-schedule
111
+ celerybeat.pid
112
+
113
+ # SageMath parsed files
114
+ *.sage.py
115
+
116
+ # Environments
117
+ .env
118
+ .venv
119
+ env/
120
+ venv/
121
+ ENV/
122
+ env.bak/
123
+ venv.bak/
124
+
125
+ # Spyder project settings
126
+ .spyderproject
127
+ .spyderformpoint
128
+
129
+ # Rope project settings
130
+ .ropeproject
131
+
132
+ # mkdocs documentation
133
+ /site
134
+
135
+ # mypy
136
+ .mypy_cache/
137
+ .dmypy.json
138
+ dmypy.json
139
+
140
+ # Pyre type checker
141
+ .pyre/
142
+
143
+ # pytype static type analyzer
144
+ .pytype/
145
+
146
+ # Cython debug symbols
147
+ cython_debug/
148
+
149
+ # OS X
150
+ .DS_Store
151
+
152
+ # Node modules
153
+ node_modules
154
+ dist
155
+
156
+
157
+ # System design docs (local developer documentation)
158
+ system-design/
159
+
160
+ # Scripts
161
+ scripts/seed_downloads.py
162
+ # pyrpc client config
163
+ pyrpc-client.json
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyrpc-codegen
3
- Version: 0.7.5
3
+ Version: 0.7.7
4
4
  Summary: Codegen and CLI tools for pyRPC
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: jinja2>=3.1.0
7
- Requires-Dist: jsonschema-ts>=0.2.0
7
+ Requires-Dist: jsonschema-ts>=0.2.1
@@ -1,21 +1,21 @@
1
- # pyrpc-codegen
2
-
3
- Code generation utilities for [pyRPC](https://pyrpc.com). Generates TypeScript type definitions from Python procedure schemas.
4
-
5
- ## What it does
6
-
7
- - `generate_typescript_client(schemas)` - generates TypeScript interface source code from a schema dict
8
- - `save_typescript_client(schemas, output_path)` - generates and writes TypeScript types to a file
9
- - `DEFAULT_OUTPUT` - default output filename constant
10
-
11
- ## Installation
12
-
13
- Installed automatically as a dependency of `pyrpc-core`:
14
-
15
- ```bash
16
- uv add pyrpc-core
17
- ```
18
-
19
- ## License
20
-
21
- MIT
1
+ # pyrpc-codegen
2
+
3
+ Code generation utilities for [pyRPC](https://pyrpc.com). Generates TypeScript type definitions from Python procedure schemas.
4
+
5
+ ## What it does
6
+
7
+ - `generate_typescript_client(schemas)` - generates TypeScript interface source code from a schema dict
8
+ - `save_typescript_client(schemas, output_path)` - generates and writes TypeScript types to a file
9
+ - `DEFAULT_OUTPUT` - default output filename constant
10
+
11
+ ## Installation
12
+
13
+ Installed automatically as a dependency of `pyrpc-core`:
14
+
15
+ ```bash
16
+ uv add pyrpc-core
17
+ ```
18
+
19
+ ## License
20
+
21
+ MIT
@@ -1,16 +1,16 @@
1
- [project]
2
- name = "pyrpc-codegen"
3
- version = "0.7.5"
4
- description = "Codegen and CLI tools for pyRPC"
5
- requires-python = ">=3.11"
6
- dependencies = [
7
- "jinja2>=3.1.0",
8
- "jsonschema-ts>=0.2.0",
9
- ]
10
-
11
- [build-system]
12
- requires = ["hatchling"]
13
- build-backend = "hatchling.build"
14
-
15
- [tool.hatch.build.targets.wheel]
16
- packages = ["src/pyrpc_codegen"]
1
+ [project]
2
+ name = "pyrpc-codegen"
3
+ version = "0.7.7"
4
+ description = "Codegen and CLI tools for pyRPC"
5
+ requires-python = ">=3.11"
6
+ dependencies = [
7
+ "jinja2>=3.1.0",
8
+ "jsonschema-ts>=0.2.1",
9
+ ]
10
+
11
+ [build-system]
12
+ requires = ["hatchling"]
13
+ build-backend = "hatchling.build"
14
+
15
+ [tool.hatch.build.targets.wheel]
16
+ packages = ["src/pyrpc_codegen"]
@@ -1,3 +1,3 @@
1
- from .ts_codegen import DEFAULT_OUTPUT, generate_typescript_client, save_typescript_client
2
-
3
- __all__ = ["DEFAULT_OUTPUT", "generate_typescript_client", "save_typescript_client"]
1
+ from .ts_codegen import DEFAULT_OUTPUT, generate_typescript_client, save_typescript_client
2
+
3
+ __all__ = ["DEFAULT_OUTPUT", "generate_typescript_client", "save_typescript_client"]
@@ -1,24 +1,24 @@
1
- /**
2
- * @pyrpc/types - Auto-generated by `pyrpc codegen`.
3
- * @see https://pyrpc.io/docs/plugins/prpc-codegen
4
- *
5
- * Import `Types` and pass to `createClient<Types>()` for full type safety.
6
- *
7
- * ```ts
8
- * import { createClient } from "@pyrpc/client"
9
- * import type { Types } from "@pyrpc/types"
10
- * const client = createClient<Types>({ baseUrl: "https://..." })
11
- * const result = await client.someProcedure(42)
12
- * ```
13
- */
14
- /* eslint-disable */
15
-
16
- // ── Procedures ──────────────────────────────────────────
17
- export interface Types {
18
- {% for name, schema in schemas.items() %}
19
- /**
20
- * {{ schema.doc or "No documentation available." }}
21
- */
22
- {{ name }}({% for param in schema.parameters %}{{ param.name }}: {{ param.type | pytype_to_ts }}{% if not loop.last %}, {% endif %}{% endfor %}): Promise<{{ schema.return_type | return_type_to_ts }}>;
23
- {% endfor %}
24
- }
1
+ /**
2
+ * @pyrpc/types - Auto-generated by `pyrpc codegen`.
3
+ * @see https://pyrpc.io/docs/plugins/prpc-codegen
4
+ *
5
+ * Import `Types` and pass to `createClient<Types>()` for full type safety.
6
+ *
7
+ * ```ts
8
+ * import { createClient } from "@pyrpc/client"
9
+ * import type { Types } from "@pyrpc/types"
10
+ * const client = createClient<Types>({ baseUrl: "https://..." })
11
+ * const result = await client.someProcedure(42)
12
+ * ```
13
+ */
14
+ /* eslint-disable */
15
+
16
+ // ── Procedures ──────────────────────────────────────────
17
+ export interface Types {
18
+ {% for name, schema in schemas.items() %}
19
+ /**
20
+ * {{ schema.doc or "No documentation available." }}
21
+ */
22
+ {{ name }}({% for param in schema.parameters %}{{ param.name }}: {{ param.type | pytype_to_ts }}{% if not loop.last %}, {% endif %}{% endfor %}): Promise<{{ schema.return_type | return_type_to_ts }}>;
23
+ {% endfor %}
24
+ }
@@ -1,182 +1,182 @@
1
- import os
2
- import re
3
- import unicodedata
4
- from pathlib import Path
5
- from typing import Any, Dict
6
-
7
- from jinja2 import Environment, FileSystemLoader
8
- from jsonschema_ts import Options as JsonschemaTsOptions
9
- from jsonschema_ts import assemble, collect_defs, convert_all, ensure_inline_models
10
-
11
- DEFAULT_OUTPUT = "node_modules/@pyrpc/types/src/index.ts"
12
-
13
- _TYPE_MAP: Dict[str, str] = {
14
- "int": "number",
15
- "float": "number",
16
- "str": "string",
17
- "bool": "boolean",
18
- "None": "null",
19
- "NoneType": "null",
20
- "Any": "any",
21
- }
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
-
45
- def _pytype_to_ts(type_str: str) -> str:
46
- if not type_str:
47
- return "any"
48
-
49
- m = re.match(r"<class\s+'([^']+)'>", type_str)
50
- if m:
51
- name = m.group(1)
52
- if '.' in name:
53
- name = name.rsplit('.', 1)[1]
54
- if name in _TYPE_MAP:
55
- return _TYPE_MAP[name]
56
- return _to_safe_name(name)
57
-
58
- if type_str.startswith("typing."):
59
- type_str = type_str[7:]
60
-
61
- if type_str.startswith("Optional["):
62
- inner = type_str[9:-1]
63
- return f"{_pytype_to_ts(inner)} | null"
64
-
65
- if type_str.startswith("Union["):
66
- inner = type_str[6:-1]
67
- parts = _split_type_args(inner)
68
- ts_parts = [_pytype_to_ts(p.strip()) for p in parts]
69
- non_null = [p for p in ts_parts if p != "null"]
70
- if len(non_null) < len(ts_parts):
71
- return f"{' | '.join(non_null)} | null"
72
- return " | ".join(ts_parts)
73
-
74
- if type_str.startswith("List[") or type_str.startswith("list["):
75
- inner = type_str[5:-1]
76
- return f"{_pytype_to_ts(inner.strip())}[]"
77
-
78
- if type_str.startswith("Dict[") or type_str.startswith("dict["):
79
- inner = type_str[5:-1]
80
- parts = _split_type_args(inner)
81
- if len(parts) >= 2:
82
- return f"Record<{_pytype_to_ts(parts[0].strip())}, {_pytype_to_ts(parts[1].strip())}>"
83
- return "Record<string, any>"
84
-
85
- if type_str.startswith("Tuple[") or type_str.startswith("tuple["):
86
- inner = type_str[6:-1]
87
- parts = _split_type_args(inner)
88
- ts_parts = [_pytype_to_ts(p.strip()) for p in parts]
89
- return f"[{', '.join(ts_parts)}]"
90
-
91
- if type_str.startswith("Set[") or type_str.startswith("set["):
92
- inner = type_str[4:-1]
93
- return f"Set<{_pytype_to_ts(inner.strip())}>"
94
-
95
- return "any"
96
-
97
-
98
- def _split_type_args(s: str) -> list:
99
- parts = []
100
- depth = 0
101
- current = ""
102
- for c in s:
103
- if c in "[(":
104
- depth += 1
105
- current += c
106
- elif c in "])":
107
- depth -= 1
108
- current += c
109
- elif c == "," and depth == 0:
110
- parts.append(current)
111
- current = ""
112
- else:
113
- current += c
114
- if current:
115
- parts.append(current)
116
- return parts
117
-
118
-
119
- def _return_type_to_ts(return_type: str) -> str:
120
- return _pytype_to_ts(return_type)
121
-
122
-
123
- def _collect_schema_defs(schemas: Dict[str, Any]) -> dict:
124
- schema_sources = []
125
- for _name, schema in schemas.items():
126
- if isinstance(schema, dict):
127
- params = schema.get("parameters", [])
128
- else:
129
- params = schema.parameters
130
- for param in params:
131
- if isinstance(param, dict):
132
- js = param.get("schema") or param.get("schema_")
133
- else:
134
- js = param.schema_
135
- if js:
136
- schema_sources.append(js)
137
- if isinstance(schema, dict):
138
- rs = schema.get("return_schema")
139
- else:
140
- rs = schema.return_schema
141
- if rs:
142
- schema_sources.append(rs)
143
-
144
- processed = ensure_inline_models(*schema_sources)
145
- return collect_defs(*processed)
146
-
147
-
148
- def generate_typescript_client(schemas: Dict[str, Any]) -> str:
149
- defs = _collect_schema_defs(schemas)
150
-
151
- opts = JsonschemaTsOptions(
152
- banner_comment="",
153
- format=True,
154
- unknown_any=True,
155
- )
156
-
157
- model_interfaces = convert_all(defs, opts=opts)
158
-
159
- template_dir = Path(__file__).parent / "templates"
160
- env = Environment(loader=FileSystemLoader(template_dir))
161
- env.filters["pytype_to_ts"] = _pytype_to_ts
162
- env.filters["return_type_to_ts"] = _return_type_to_ts
163
- template = env.get_template("client.ts.j2")
164
-
165
- procedure_types = template.render(schemas=schemas)
166
-
167
- return assemble(
168
- models=model_interfaces,
169
- procedures=procedure_types,
170
- banner="",
171
- )
172
-
173
-
174
- def save_typescript_client(schemas: Dict[str, Any], output_path: str):
175
- if not os.path.isabs(output_path):
176
- raise ValueError(
177
- "save_typescript_client requires an absolute path"
178
- )
179
- content = generate_typescript_client(schemas)
180
- os.makedirs(os.path.dirname(output_path), exist_ok=True)
181
- with open(output_path, "w", encoding="utf-8") as f:
182
- f.write(content)
1
+ import os
2
+ import re
3
+ import unicodedata
4
+ from pathlib import Path
5
+ from typing import Any, Dict
6
+
7
+ from jinja2 import Environment, FileSystemLoader
8
+ from jsonschema_ts import Options as JsonschemaTsOptions
9
+ from jsonschema_ts import assemble, collect_defs, convert_all, ensure_inline_models
10
+
11
+ DEFAULT_OUTPUT = "node_modules/@pyrpc/types/src/index.ts"
12
+
13
+ _TYPE_MAP: Dict[str, str] = {
14
+ "int": "number",
15
+ "float": "number",
16
+ "str": "string",
17
+ "bool": "boolean",
18
+ "None": "null",
19
+ "NoneType": "null",
20
+ "Any": "any",
21
+ }
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
+
45
+ def _pytype_to_ts(type_str: str) -> str:
46
+ if not type_str:
47
+ return "any"
48
+
49
+ m = re.match(r"<class\s+'([^']+)'>", type_str)
50
+ if m:
51
+ name = m.group(1)
52
+ if '.' in name:
53
+ name = name.rsplit('.', 1)[1]
54
+ if name in _TYPE_MAP:
55
+ return _TYPE_MAP[name]
56
+ return _to_safe_name(name)
57
+
58
+ if type_str.startswith("typing."):
59
+ type_str = type_str[7:]
60
+
61
+ if type_str.startswith("Optional["):
62
+ inner = type_str[9:-1]
63
+ return f"{_pytype_to_ts(inner)} | null"
64
+
65
+ if type_str.startswith("Union["):
66
+ inner = type_str[6:-1]
67
+ parts = _split_type_args(inner)
68
+ ts_parts = [_pytype_to_ts(p.strip()) for p in parts]
69
+ non_null = [p for p in ts_parts if p != "null"]
70
+ if len(non_null) < len(ts_parts):
71
+ return f"{' | '.join(non_null)} | null"
72
+ return " | ".join(ts_parts)
73
+
74
+ if type_str.startswith("List[") or type_str.startswith("list["):
75
+ inner = type_str[5:-1]
76
+ return f"{_pytype_to_ts(inner.strip())}[]"
77
+
78
+ if type_str.startswith("Dict[") or type_str.startswith("dict["):
79
+ inner = type_str[5:-1]
80
+ parts = _split_type_args(inner)
81
+ if len(parts) >= 2:
82
+ return f"Record<{_pytype_to_ts(parts[0].strip())}, {_pytype_to_ts(parts[1].strip())}>"
83
+ return "Record<string, any>"
84
+
85
+ if type_str.startswith("Tuple[") or type_str.startswith("tuple["):
86
+ inner = type_str[6:-1]
87
+ parts = _split_type_args(inner)
88
+ ts_parts = [_pytype_to_ts(p.strip()) for p in parts]
89
+ return f"[{', '.join(ts_parts)}]"
90
+
91
+ if type_str.startswith("Set[") or type_str.startswith("set["):
92
+ inner = type_str[4:-1]
93
+ return f"Set<{_pytype_to_ts(inner.strip())}>"
94
+
95
+ return "any"
96
+
97
+
98
+ def _split_type_args(s: str) -> list:
99
+ parts = []
100
+ depth = 0
101
+ current = ""
102
+ for c in s:
103
+ if c in "[(":
104
+ depth += 1
105
+ current += c
106
+ elif c in "])":
107
+ depth -= 1
108
+ current += c
109
+ elif c == "," and depth == 0:
110
+ parts.append(current)
111
+ current = ""
112
+ else:
113
+ current += c
114
+ if current:
115
+ parts.append(current)
116
+ return parts
117
+
118
+
119
+ def _return_type_to_ts(return_type: str) -> str:
120
+ return _pytype_to_ts(return_type)
121
+
122
+
123
+ def _collect_schema_defs(schemas: Dict[str, Any]) -> dict:
124
+ schema_sources = []
125
+ for _name, schema in schemas.items():
126
+ if isinstance(schema, dict):
127
+ params = schema.get("parameters", [])
128
+ else:
129
+ params = schema.parameters
130
+ for param in params:
131
+ if isinstance(param, dict):
132
+ js = param.get("schema") or param.get("schema_")
133
+ else:
134
+ js = param.schema_
135
+ if js:
136
+ schema_sources.append(js)
137
+ if isinstance(schema, dict):
138
+ rs = schema.get("return_schema")
139
+ else:
140
+ rs = schema.return_schema
141
+ if rs:
142
+ schema_sources.append(rs)
143
+
144
+ processed = ensure_inline_models(*schema_sources)
145
+ return collect_defs(*processed)
146
+
147
+
148
+ def generate_typescript_client(schemas: Dict[str, Any]) -> str:
149
+ defs = _collect_schema_defs(schemas)
150
+
151
+ opts = JsonschemaTsOptions(
152
+ banner_comment="",
153
+ format=True,
154
+ unknown_any=True,
155
+ )
156
+
157
+ model_interfaces = convert_all(defs, opts=opts)
158
+
159
+ template_dir = Path(__file__).parent / "templates"
160
+ env = Environment(loader=FileSystemLoader(template_dir))
161
+ env.filters["pytype_to_ts"] = _pytype_to_ts
162
+ env.filters["return_type_to_ts"] = _return_type_to_ts
163
+ template = env.get_template("client.ts.j2")
164
+
165
+ procedure_types = template.render(schemas=schemas)
166
+
167
+ return assemble(
168
+ models=model_interfaces,
169
+ procedures=procedure_types,
170
+ banner="",
171
+ )
172
+
173
+
174
+ def save_typescript_client(schemas: Dict[str, Any], output_path: str):
175
+ if not os.path.isabs(output_path):
176
+ raise ValueError(
177
+ "save_typescript_client requires an absolute path"
178
+ )
179
+ content = generate_typescript_client(schemas)
180
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
181
+ with open(output_path, "w", encoding="utf-8") as f:
182
+ f.write(content)
@@ -1,255 +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
+ 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