pyrpc-codegen 0.7.6__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.
- {pyrpc_codegen-0.7.6 → pyrpc_codegen-0.7.7}/.gitignore +163 -161
- {pyrpc_codegen-0.7.6 → pyrpc_codegen-0.7.7}/PKG-INFO +2 -2
- {pyrpc_codegen-0.7.6 → pyrpc_codegen-0.7.7}/README.md +21 -21
- {pyrpc_codegen-0.7.6 → pyrpc_codegen-0.7.7}/pyproject.toml +16 -16
- {pyrpc_codegen-0.7.6 → pyrpc_codegen-0.7.7}/src/pyrpc_codegen/__init__.py +3 -3
- {pyrpc_codegen-0.7.6 → pyrpc_codegen-0.7.7}/src/pyrpc_codegen/templates/client.ts.j2 +24 -24
- {pyrpc_codegen-0.7.6 → pyrpc_codegen-0.7.7}/src/pyrpc_codegen/ts_codegen.py +182 -182
- {pyrpc_codegen-0.7.6 → pyrpc_codegen-0.7.7}/tests/test_codegen.py +255 -255
|
@@ -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,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.
|
|
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.
|
|
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
|