pyrpc-codegen 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,154 @@
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
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyrpc-codegen
3
+ Version: 0.2.0
4
+ Summary: Codegen and CLI tools for pyRPC
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: httpx>=0.28.1
7
+ Requires-Dist: jinja2>=3.1.0
8
+ Requires-Dist: pyrpc-core
9
+ Requires-Dist: rich>=13.0.0
10
+ Requires-Dist: typer[all]>=0.9.0
11
+ Requires-Dist: uvicorn>=0.23.0
@@ -0,0 +1,23 @@
1
+ [project]
2
+ name = "pyrpc-codegen"
3
+ version = "0.2.0"
4
+ description = "Codegen and CLI tools for pyRPC"
5
+ requires-python = ">=3.11"
6
+ dependencies = [
7
+ "pyrpc-core",
8
+ "typer[all]>=0.9.0",
9
+ "rich>=13.0.0",
10
+ "jinja2>=3.1.0",
11
+ "uvicorn>=0.23.0",
12
+ "httpx>=0.28.1",
13
+ ]
14
+
15
+ [project.scripts]
16
+ pyrpc = "pyrpc_codegen.main:app"
17
+
18
+ [build-system]
19
+ requires = ["hatchling"]
20
+ build-backend = "hatchling.build"
21
+
22
+ [tool.hatch.build.targets.wheel]
23
+ packages = ["src/pyrpc_codegen"]
@@ -0,0 +1,4 @@
1
+ from .ts_codegen import generate_typescript_client, save_typescript_client
2
+ from .main import app
3
+
4
+ __all__ = ["generate_typescript_client", "save_typescript_client", "app"]
@@ -0,0 +1,178 @@
1
+ import importlib
2
+ import json
3
+ import os
4
+ import sys
5
+
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.table import Table
10
+
11
+ from .ts_codegen import save_typescript_client, DEFAULT_OUTPUT
12
+
13
+
14
+ __version__ = "0.2.0"
15
+
16
+
17
+ app = typer.Typer(
18
+ name="pyrpc",
19
+ help="pyRPC CLI - type-safe Python-to-TypeScript RPC",
20
+ add_completion=False,
21
+ )
22
+ console = Console()
23
+
24
+
25
+ def _lazy_import_pyrpc_core():
26
+ """Import pyrpc-core lazily so codegen-only usage avoids the dep."""
27
+ global default_router, get_registry_schema
28
+ from pyrpc_core import default_router, get_registry_schema
29
+ return default_router, get_registry_schema
30
+
31
+
32
+ def _import_module(module_path: str):
33
+ sys.path.append(os.getcwd())
34
+ try:
35
+ return importlib.import_module(module_path)
36
+ except ImportError as e:
37
+ console.print(f"[bold red]Error:[/bold red] Could not import module '{module_path}': {e}")
38
+ raise typer.Exit(code=1)
39
+
40
+
41
+ def _fetch_schema(url: str) -> dict:
42
+ import httpx
43
+ clean_url = url.rstrip("/")
44
+ if not clean_url.endswith("/rpc"):
45
+ clean_url += "/rpc"
46
+ response = httpx.get(clean_url)
47
+ response.raise_for_status()
48
+ return response.json()
49
+
50
+
51
+ def _load_schema(path_or_url: str) -> dict:
52
+ if path_or_url.startswith("http://") or path_or_url.startswith("https://"):
53
+ console.print(f"Fetching schema from [bold yellow]{path_or_url}[/bold yellow]...")
54
+ return _fetch_schema(path_or_url)
55
+ path = os.path.abspath(path_or_url)
56
+ console.print(f"Reading schema from [bold yellow]{path}[/bold yellow]...")
57
+ with open(path, "r") as f:
58
+ return json.load(f)
59
+
60
+
61
+ @app.command()
62
+ def version():
63
+ """Show pyRPC version."""
64
+ console.print(f"pyRPC version: [bold cyan]{__version__}[/bold cyan]")
65
+
66
+
67
+ @app.command()
68
+ def pull(
69
+ module: str = typer.Argument(..., help="Python module path (e.g. 'app.main')"),
70
+ output: str = typer.Option("pyrpc-schema.json", "--output", "-o", help="Output JSON schema file path"),
71
+ ):
72
+ """Extract RPC schema from a Python module and save as JSON."""
73
+ _lazy_import_pyrpc_core()
74
+ _import_module(module)
75
+
76
+ schemas = get_registry_schema(default_router)
77
+
78
+ if not schemas:
79
+ console.print("[yellow]No procedures found in registry for this module.[/yellow]")
80
+ raise typer.Exit(code=1)
81
+
82
+ serializable = {}
83
+ for name, schema in schemas.items():
84
+ serializable[name] = {
85
+ "name": schema.name,
86
+ "doc": schema.doc or "",
87
+ "parameters": [
88
+ {"name": p.name, "type": p.type, "required": p.required, "default": p.default}
89
+ for p in schema.parameters
90
+ ],
91
+ "return_type": schema.return_type,
92
+ }
93
+
94
+ output_path = os.path.abspath(output)
95
+ os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
96
+ with open(output_path, "w") as f:
97
+ json.dump(serializable, f, indent=2)
98
+
99
+ console.print(f"[bold green]OK Schema extracted to {output_path}[/bold green]")
100
+ console.print(f" ({len(serializable)} procedure(s) written)")
101
+
102
+
103
+ @app.command()
104
+ def serve(
105
+ module: str = typer.Argument(..., help="Module containing the pyRPC application (e.g. 'app.main')"),
106
+ host: str = typer.Option("127.0.0.1", "--host", "-h", help="Bind socket to this host"),
107
+ port: int = typer.Option(8000, "--port", "-p", help="Bind socket to this port"),
108
+ reload: bool = typer.Option(False, "--reload", help="Enable auto-reload"),
109
+ ):
110
+ """Start the pyRPC ASGI server."""
111
+ _lazy_import_pyrpc_core()
112
+ import uvicorn
113
+ _import_module(module)
114
+
115
+ console.print(Panel(
116
+ f"Starting pyRPC server for [bold cyan]{module}[/bold cyan]\n"
117
+ f"Endpoint: [bold green]http://{host}:{port}/rpc[/bold green]",
118
+ title="pyRPC Serve",
119
+ border_style="blue"
120
+ ))
121
+
122
+ uvicorn.run("pyrpc:asgi_app", host=host, port=port, reload=reload)
123
+
124
+
125
+ @app.command()
126
+ def codegen(
127
+ source: str = typer.Argument(..., help="Schema JSON file path or URL of a running pyRPC server (e.g. pyrpc-schema.json or http://localhost:8000)"),
128
+ output: str = typer.Option(DEFAULT_OUTPUT, "--output", "-o", help="Output file path for generated types"),
129
+ ):
130
+ """Generate TypeScript type definitions from a schema file or a running server."""
131
+ try:
132
+ schemas = _load_schema(source)
133
+ except Exception as e:
134
+ console.print(f"[bold red]Error:[/bold red] Could not load schema from '{source}': {e}")
135
+ raise typer.Exit(code=1)
136
+
137
+ console.print(f"Generating TypeScript contracts [dim]({len(schemas)} procedures)[/dim]...")
138
+ save_typescript_client(schemas, output)
139
+ console.print(f"[bold green]OK Types written to {output}[/bold green]")
140
+
141
+ if os.path.exists(output):
142
+ console.print(f" Import: [bold]import type {{ Types }} from \"@pyrpc/types\"[/bold]")
143
+
144
+
145
+ @app.command()
146
+ def inspect(
147
+ module: str = typer.Argument(..., help="Module to inspect")
148
+ ):
149
+ """List all registered RPC procedures in a module."""
150
+ _lazy_import_pyrpc_core()
151
+ _import_module(module)
152
+
153
+ schemas = get_registry_schema(default_router)
154
+
155
+ if not schemas:
156
+ console.print("[yellow]No procedures found in registry for this module.[/yellow]")
157
+ return
158
+
159
+ table = Table(title=f"pyRPC Registry: {module}")
160
+ table.add_column("Method", style="cyan")
161
+ table.add_column("Params", style="green")
162
+ table.add_column("Returns", style="magenta")
163
+ table.add_column("Doc", style="white", no_wrap=False)
164
+
165
+ for name, schema in schemas.items():
166
+ params = ", ".join([f"{p.name}: {p.type}" for p in schema.parameters])
167
+ table.add_row(
168
+ name,
169
+ params or "None",
170
+ schema.return_type,
171
+ schema.doc or ""
172
+ )
173
+
174
+ console.print(table)
175
+
176
+
177
+ if __name__ == "__main__":
178
+ app()
@@ -0,0 +1,22 @@
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
+
15
+ export interface Types {
16
+ {% for name, schema in schemas.items() %}
17
+ /**
18
+ * {{ schema.doc or "No documentation available." }}
19
+ */
20
+ {{ 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 }}>;
21
+ {% endfor %}
22
+ }
@@ -0,0 +1,115 @@
1
+ import os
2
+ import re
3
+ from pathlib import Path
4
+ from typing import Any, Dict
5
+
6
+ from jinja2 import Environment, FileSystemLoader
7
+
8
+ DEFAULT_OUTPUT = "node_modules/@pyrpc/types/src/index.ts"
9
+
10
+ _TYPE_MAP: Dict[str, str] = {
11
+ "int": "number",
12
+ "float": "number",
13
+ "str": "string",
14
+ "bool": "boolean",
15
+ "None": "null",
16
+ "NoneType": "null",
17
+ "Any": "any",
18
+ }
19
+
20
+
21
+ def _pytype_to_ts(type_str: str) -> str:
22
+ if not type_str:
23
+ return "any"
24
+
25
+ m = re.match(r"<class\s+'([^']+)'>", type_str)
26
+ if m:
27
+ name = m.group(1)
28
+ if name in _TYPE_MAP:
29
+ return _TYPE_MAP[name]
30
+ if name[0].isupper():
31
+ return name
32
+ return "any"
33
+
34
+ if type_str.startswith("typing."):
35
+ type_str = type_str[7:]
36
+
37
+ if type_str.startswith("Optional["):
38
+ inner = type_str[9:-1]
39
+ return f"{_pytype_to_ts(inner)} | null"
40
+
41
+ if type_str.startswith("Union["):
42
+ inner = type_str[6:-1]
43
+ parts = _split_type_args(inner)
44
+ ts_parts = [_pytype_to_ts(p.strip()) for p in parts]
45
+ non_null = [p for p in ts_parts if p != "null"]
46
+ if len(non_null) < len(ts_parts):
47
+ return f"{' | '.join(non_null)} | null"
48
+ return " | ".join(ts_parts)
49
+
50
+ if type_str.startswith("List[") or type_str.startswith("list["):
51
+ inner = type_str[5:-1]
52
+ return f"{_pytype_to_ts(inner.strip())}[]"
53
+
54
+ if type_str.startswith("Dict[") or type_str.startswith("dict["):
55
+ inner = type_str[5:-1]
56
+ parts = _split_type_args(inner)
57
+ if len(parts) >= 2:
58
+ return f"Record<{_pytype_to_ts(parts[0].strip())}, {_pytype_to_ts(parts[1].strip())}>"
59
+ return "Record<string, any>"
60
+
61
+ if type_str.startswith("Tuple[") or type_str.startswith("tuple["):
62
+ inner = type_str[6:-1]
63
+ parts = _split_type_args(inner)
64
+ ts_parts = [_pytype_to_ts(p.strip()) for p in parts]
65
+ return f"[{', '.join(ts_parts)}]"
66
+
67
+ if type_str.startswith("Set[") or type_str.startswith("set["):
68
+ inner = type_str[4:-1]
69
+ return f"Set<{_pytype_to_ts(inner.strip())}>"
70
+
71
+ return "any"
72
+
73
+
74
+ def _split_type_args(s: str) -> list:
75
+ parts = []
76
+ depth = 0
77
+ current = ""
78
+ for c in s:
79
+ if c in "[(":
80
+ depth += 1
81
+ current += c
82
+ elif c in "])":
83
+ depth -= 1
84
+ current += c
85
+ elif c == "," and depth == 0:
86
+ parts.append(current)
87
+ current = ""
88
+ else:
89
+ current += c
90
+ if current:
91
+ parts.append(current)
92
+ return parts
93
+
94
+
95
+ def _return_type_to_ts(return_type: str) -> str:
96
+ return _pytype_to_ts(return_type)
97
+
98
+
99
+ def generate_typescript_client(schemas: Dict[str, Any]) -> str:
100
+ template_dir = Path(__file__).parent / "templates"
101
+ env = Environment(loader=FileSystemLoader(template_dir))
102
+ env.filters["pytype_to_ts"] = _pytype_to_ts
103
+ env.filters["return_type_to_ts"] = _return_type_to_ts
104
+ template = env.get_template("client.ts.j2")
105
+
106
+ return template.render(schemas=schemas)
107
+
108
+
109
+ def save_typescript_client(schemas: Dict[str, Any], output_path: str = DEFAULT_OUTPUT):
110
+ content = generate_typescript_client(schemas)
111
+ if not os.path.isabs(output_path):
112
+ output_path = os.path.join(os.getcwd(), output_path)
113
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
114
+ with open(output_path, "w", encoding="utf-8") as f:
115
+ f.write(content)