pydantic-rpc-cli 0.11.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,110 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: pydantic-rpc-cli
|
|
3
|
+
Version: 0.11.0
|
|
4
|
+
Summary: CLI tool for pydantic-rpc with server runtime support
|
|
5
|
+
Author: Yasushi Itoh
|
|
6
|
+
Requires-Dist: pydantic-rpc>=0.10.0
|
|
7
|
+
Requires-Dist: hypercorn>=0.17.3
|
|
8
|
+
Requires-Dist: gunicorn>=20.1.0
|
|
9
|
+
Requires-Dist: uvloop>=0.17.0
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# pydantic-rpc-cli
|
|
14
|
+
|
|
15
|
+
CLI tool for [pydantic-rpc](https://github.com/yourusername/pydantic-rpc) with built-in server runtime support.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install pydantic-rpc-cli
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This will install:
|
|
24
|
+
- `pydantic-rpc` (core library)
|
|
25
|
+
- `hypercorn` (ASGI server)
|
|
26
|
+
- `gunicorn` (WSGI server)
|
|
27
|
+
- `uvloop` (optional asyncio performance improvement)
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
### Generate Protobuf Files
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Generate .proto file from a service class
|
|
35
|
+
pydantic-rpc generate myapp.services.UserService --output ./proto/
|
|
36
|
+
|
|
37
|
+
# Also compile to Python code
|
|
38
|
+
pydantic-rpc generate myapp.services.UserService --compile
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Run Servers
|
|
42
|
+
|
|
43
|
+
#### gRPC Server (default)
|
|
44
|
+
```bash
|
|
45
|
+
# Start a gRPC server
|
|
46
|
+
pydantic-rpc serve myapp.services.UserService --port 50051
|
|
47
|
+
|
|
48
|
+
# The CLI automatically detects if your service has async methods
|
|
49
|
+
# and uses AsyncIOServer or Server accordingly
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
#### Connect RPC via ASGI (HTTP/2)
|
|
53
|
+
```bash
|
|
54
|
+
# Run with Hypercorn (built-in)
|
|
55
|
+
pydantic-rpc serve myapp.services.UserService --asgi --port 8000
|
|
56
|
+
|
|
57
|
+
# Service will be available at:
|
|
58
|
+
# http://localhost:8000/UserService/
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
#### Connect RPC via WSGI (HTTP/1.1)
|
|
62
|
+
```bash
|
|
63
|
+
# Run with Gunicorn (built-in)
|
|
64
|
+
pydantic-rpc serve myapp.services.UserService --wsgi --port 8000
|
|
65
|
+
|
|
66
|
+
# With multiple workers
|
|
67
|
+
pydantic-rpc serve myapp.services.UserService --wsgi --port 8000 --workers 4
|
|
68
|
+
|
|
69
|
+
# Service will be available at:
|
|
70
|
+
# http://localhost:8000/UserService/
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Example Service
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
# myapp/services.py
|
|
77
|
+
from pydantic_rpc import Message, AsyncIOServer
|
|
78
|
+
|
|
79
|
+
class HelloRequest(Message):
|
|
80
|
+
name: str
|
|
81
|
+
|
|
82
|
+
class HelloResponse(Message):
|
|
83
|
+
message: str
|
|
84
|
+
|
|
85
|
+
class GreeterService:
|
|
86
|
+
async def say_hello(self, request: HelloRequest) -> HelloResponse:
|
|
87
|
+
return HelloResponse(message=f"Hello, {request.name}!")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Run it:
|
|
91
|
+
```bash
|
|
92
|
+
# As gRPC
|
|
93
|
+
pydantic-rpc serve myapp.services.GreeterService
|
|
94
|
+
|
|
95
|
+
# As Connect RPC (HTTP/2)
|
|
96
|
+
pydantic-rpc serve myapp.services.GreeterService --asgi --port 8000
|
|
97
|
+
|
|
98
|
+
# As Connect RPC (HTTP/1.1)
|
|
99
|
+
pydantic-rpc serve myapp.services.GreeterService --wsgi --port 8000
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Benefits of the Separated CLI
|
|
103
|
+
|
|
104
|
+
- **Lightweight Core**: The main `pydantic-rpc` package remains lightweight without server dependencies
|
|
105
|
+
- **Full Server Support**: When you install `pydantic-rpc-cli`, you get everything needed to run services
|
|
106
|
+
- **Flexibility**: Choose between gRPC, ASGI (HTTP/2), or WSGI (HTTP/1.1) at runtime
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
Same as pydantic-rpc
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# pydantic-rpc-cli
|
|
2
|
+
|
|
3
|
+
CLI tool for [pydantic-rpc](https://github.com/yourusername/pydantic-rpc) with built-in server runtime support.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install pydantic-rpc-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This will install:
|
|
12
|
+
- `pydantic-rpc` (core library)
|
|
13
|
+
- `hypercorn` (ASGI server)
|
|
14
|
+
- `gunicorn` (WSGI server)
|
|
15
|
+
- `uvloop` (optional asyncio performance improvement)
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
### Generate Protobuf Files
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Generate .proto file from a service class
|
|
23
|
+
pydantic-rpc generate myapp.services.UserService --output ./proto/
|
|
24
|
+
|
|
25
|
+
# Also compile to Python code
|
|
26
|
+
pydantic-rpc generate myapp.services.UserService --compile
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Run Servers
|
|
30
|
+
|
|
31
|
+
#### gRPC Server (default)
|
|
32
|
+
```bash
|
|
33
|
+
# Start a gRPC server
|
|
34
|
+
pydantic-rpc serve myapp.services.UserService --port 50051
|
|
35
|
+
|
|
36
|
+
# The CLI automatically detects if your service has async methods
|
|
37
|
+
# and uses AsyncIOServer or Server accordingly
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
#### Connect RPC via ASGI (HTTP/2)
|
|
41
|
+
```bash
|
|
42
|
+
# Run with Hypercorn (built-in)
|
|
43
|
+
pydantic-rpc serve myapp.services.UserService --asgi --port 8000
|
|
44
|
+
|
|
45
|
+
# Service will be available at:
|
|
46
|
+
# http://localhost:8000/UserService/
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
#### Connect RPC via WSGI (HTTP/1.1)
|
|
50
|
+
```bash
|
|
51
|
+
# Run with Gunicorn (built-in)
|
|
52
|
+
pydantic-rpc serve myapp.services.UserService --wsgi --port 8000
|
|
53
|
+
|
|
54
|
+
# With multiple workers
|
|
55
|
+
pydantic-rpc serve myapp.services.UserService --wsgi --port 8000 --workers 4
|
|
56
|
+
|
|
57
|
+
# Service will be available at:
|
|
58
|
+
# http://localhost:8000/UserService/
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Example Service
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
# myapp/services.py
|
|
65
|
+
from pydantic_rpc import Message, AsyncIOServer
|
|
66
|
+
|
|
67
|
+
class HelloRequest(Message):
|
|
68
|
+
name: str
|
|
69
|
+
|
|
70
|
+
class HelloResponse(Message):
|
|
71
|
+
message: str
|
|
72
|
+
|
|
73
|
+
class GreeterService:
|
|
74
|
+
async def say_hello(self, request: HelloRequest) -> HelloResponse:
|
|
75
|
+
return HelloResponse(message=f"Hello, {request.name}!")
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Run it:
|
|
79
|
+
```bash
|
|
80
|
+
# As gRPC
|
|
81
|
+
pydantic-rpc serve myapp.services.GreeterService
|
|
82
|
+
|
|
83
|
+
# As Connect RPC (HTTP/2)
|
|
84
|
+
pydantic-rpc serve myapp.services.GreeterService --asgi --port 8000
|
|
85
|
+
|
|
86
|
+
# As Connect RPC (HTTP/1.1)
|
|
87
|
+
pydantic-rpc serve myapp.services.GreeterService --wsgi --port 8000
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Benefits of the Separated CLI
|
|
91
|
+
|
|
92
|
+
- **Lightweight Core**: The main `pydantic-rpc` package remains lightweight without server dependencies
|
|
93
|
+
- **Full Server Support**: When you install `pydantic-rpc-cli`, you get everything needed to run services
|
|
94
|
+
- **Flexibility**: Choose between gRPC, ASGI (HTTP/2), or WSGI (HTTP/1.1) at runtime
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
Same as pydantic-rpc
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pydantic-rpc-cli"
|
|
3
|
+
version = "0.11.0"
|
|
4
|
+
description = "CLI tool for pydantic-rpc with server runtime support"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "Yasushi Itoh" }
|
|
7
|
+
]
|
|
8
|
+
dependencies = [
|
|
9
|
+
"pydantic-rpc>=0.10.0",
|
|
10
|
+
"hypercorn>=0.17.3",
|
|
11
|
+
"gunicorn>=20.1.0",
|
|
12
|
+
"uvloop>=0.17.0", # Optional performance improvement for asyncio
|
|
13
|
+
]
|
|
14
|
+
readme = "README.md"
|
|
15
|
+
requires-python = ">= 3.11"
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
pydantic-rpc = "pydantic_rpc_cli.cli:main"
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["uv_build>=0.7.21,<0.8.0"]
|
|
22
|
+
build-backend = "uv_build"
|
|
23
|
+
|
|
24
|
+
[tool.uv]
|
|
25
|
+
managed = true
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Command-line interface for pydantic-rpc with server runtime support."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import sys
|
|
6
|
+
import importlib
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
import tempfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
import asyncio
|
|
13
|
+
|
|
14
|
+
from pydantic_rpc.core import generate_proto, generate_and_compile_proto
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def import_service_class(module_path: str, class_name: Optional[str] = None):
|
|
18
|
+
"""Import a service class from a module path.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
module_path: Python module path (e.g., 'myapp.services.UserService')
|
|
22
|
+
class_name: Optional class name if not included in module_path
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
The service class
|
|
26
|
+
"""
|
|
27
|
+
# Add current directory to Python path FIRST, before parsing
|
|
28
|
+
current_dir = os.getcwd()
|
|
29
|
+
if current_dir not in sys.path:
|
|
30
|
+
sys.path.insert(0, current_dir)
|
|
31
|
+
|
|
32
|
+
if "." in module_path and not class_name:
|
|
33
|
+
# Try to extract class name from module path
|
|
34
|
+
parts = module_path.split(".")
|
|
35
|
+
potential_class = parts[-1]
|
|
36
|
+
# Check if the last part starts with uppercase (likely a class)
|
|
37
|
+
if potential_class[0].isupper():
|
|
38
|
+
module_path = ".".join(parts[:-1])
|
|
39
|
+
class_name = potential_class
|
|
40
|
+
|
|
41
|
+
if not class_name:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
"Class name must be provided either in module_path or as separate argument"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
module = importlib.import_module(module_path)
|
|
48
|
+
service_class = getattr(module, class_name)
|
|
49
|
+
return service_class
|
|
50
|
+
except ImportError as e:
|
|
51
|
+
raise ImportError(f"Failed to import module '{module_path}': {e}")
|
|
52
|
+
except AttributeError:
|
|
53
|
+
raise AttributeError(
|
|
54
|
+
f"Class '{class_name}' not found in module '{module_path}'"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def cmd_generate(args):
|
|
59
|
+
"""Generate protobuf file from a service class."""
|
|
60
|
+
try:
|
|
61
|
+
# Import the service class
|
|
62
|
+
service_class = import_service_class(args.service, args.class_name)
|
|
63
|
+
|
|
64
|
+
# Create an instance of the service
|
|
65
|
+
service_instance = service_class()
|
|
66
|
+
|
|
67
|
+
# Generate proto content
|
|
68
|
+
proto_content = generate_proto(service_instance, args.package or "")
|
|
69
|
+
|
|
70
|
+
# Determine output file
|
|
71
|
+
if args.output:
|
|
72
|
+
output_path = Path(args.output)
|
|
73
|
+
if output_path.is_dir():
|
|
74
|
+
# If output is a directory, create file with service name
|
|
75
|
+
proto_filename = f"{service_class.__name__.lower()}.proto"
|
|
76
|
+
output_file = output_path / proto_filename
|
|
77
|
+
else:
|
|
78
|
+
# Use the provided file path
|
|
79
|
+
output_file = output_path
|
|
80
|
+
else:
|
|
81
|
+
# Default to current directory
|
|
82
|
+
proto_filename = f"{service_class.__name__.lower()}.proto"
|
|
83
|
+
output_file = Path.cwd() / proto_filename
|
|
84
|
+
|
|
85
|
+
# Ensure parent directory exists
|
|
86
|
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
|
|
88
|
+
# Write the proto file
|
|
89
|
+
with open(output_file, "w") as f:
|
|
90
|
+
f.write(proto_content)
|
|
91
|
+
|
|
92
|
+
print(f"✓ Generated protobuf file: {output_file}")
|
|
93
|
+
|
|
94
|
+
# Optionally compile the proto file
|
|
95
|
+
if args.compile:
|
|
96
|
+
print("Compiling protobuf file...")
|
|
97
|
+
pb2_grpc, pb2 = generate_and_compile_proto(
|
|
98
|
+
service_instance, args.package or "", existing_proto_path=output_file
|
|
99
|
+
)
|
|
100
|
+
if pb2_grpc and pb2:
|
|
101
|
+
print(
|
|
102
|
+
f"✓ Compiled protobuf modules: {output_file.stem}_pb2.py, {output_file.stem}_pb2_grpc.py"
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
print("✗ Failed to compile protobuf file")
|
|
106
|
+
return 1
|
|
107
|
+
|
|
108
|
+
return 0
|
|
109
|
+
|
|
110
|
+
except Exception as e:
|
|
111
|
+
print(f"✗ Error: {e}", file=sys.stderr)
|
|
112
|
+
return 1
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def create_asgi_wrapper(
|
|
116
|
+
service_module: str, service_class_name: str, package_name: str = ""
|
|
117
|
+
) -> str:
|
|
118
|
+
"""Create a Python module that instantiates and exports an ASGI app."""
|
|
119
|
+
return f"""
|
|
120
|
+
# Auto-generated ASGI wrapper for pydantic-rpc
|
|
121
|
+
import sys
|
|
122
|
+
import os
|
|
123
|
+
sys.path.insert(0, os.getcwd())
|
|
124
|
+
|
|
125
|
+
from {service_module} import {service_class_name}
|
|
126
|
+
from pydantic_rpc import ASGIApp
|
|
127
|
+
|
|
128
|
+
# Create service instance
|
|
129
|
+
service = {service_class_name}()
|
|
130
|
+
|
|
131
|
+
# Create ASGI app
|
|
132
|
+
app = ASGIApp(package_name="{package_name}")
|
|
133
|
+
|
|
134
|
+
# Mount the service (this triggers proto generation)
|
|
135
|
+
app.mount(service)
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def create_wsgi_wrapper(
|
|
140
|
+
service_module: str, service_class_name: str, package_name: str = ""
|
|
141
|
+
) -> str:
|
|
142
|
+
"""Create a Python module that instantiates and exports a WSGI app."""
|
|
143
|
+
return f"""
|
|
144
|
+
# Auto-generated WSGI wrapper for pydantic-rpc
|
|
145
|
+
import sys
|
|
146
|
+
import os
|
|
147
|
+
sys.path.insert(0, os.getcwd())
|
|
148
|
+
|
|
149
|
+
from {service_module} import {service_class_name}
|
|
150
|
+
from pydantic_rpc import WSGIApp
|
|
151
|
+
|
|
152
|
+
# Create service instance
|
|
153
|
+
service = {service_class_name}()
|
|
154
|
+
|
|
155
|
+
# Create WSGI app
|
|
156
|
+
application = WSGIApp(package_name="{package_name}")
|
|
157
|
+
|
|
158
|
+
# Mount the service (this triggers proto generation)
|
|
159
|
+
application.mount(service)
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def run_asgi_server(
|
|
164
|
+
service_module: str, service_class_name: str, port: int, package_name: str = ""
|
|
165
|
+
):
|
|
166
|
+
"""Run an ASGI server with hypercorn."""
|
|
167
|
+
# Create a temporary file with the ASGI wrapper
|
|
168
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
169
|
+
wrapper_code = create_asgi_wrapper(
|
|
170
|
+
service_module, service_class_name, package_name
|
|
171
|
+
)
|
|
172
|
+
f.write(wrapper_code)
|
|
173
|
+
wrapper_path = f.name
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
# Get the module name from the temp file
|
|
177
|
+
wrapper_module = Path(wrapper_path).stem
|
|
178
|
+
|
|
179
|
+
print(f"✓ Starting ASGI server with Hypercorn on port {port}...")
|
|
180
|
+
print(f" Service: {service_class_name}")
|
|
181
|
+
print(f" HTTP: http://localhost:{port}")
|
|
182
|
+
print(f" Connect RPC endpoint: http://localhost:{port}/{service_class_name}/")
|
|
183
|
+
print("\nPress Ctrl+C to stop the server\n")
|
|
184
|
+
|
|
185
|
+
# Run hypercorn
|
|
186
|
+
cmd = [
|
|
187
|
+
sys.executable,
|
|
188
|
+
"-m",
|
|
189
|
+
"hypercorn",
|
|
190
|
+
f"{wrapper_module}:app",
|
|
191
|
+
"--bind",
|
|
192
|
+
f"0.0.0.0:{port}",
|
|
193
|
+
"--access-logfile",
|
|
194
|
+
"-",
|
|
195
|
+
"--error-logfile",
|
|
196
|
+
"-",
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
# Set PYTHONPATH to include the temp file directory
|
|
200
|
+
env = os.environ.copy()
|
|
201
|
+
env["PYTHONPATH"] = f"{Path(wrapper_path).parent}:{env.get('PYTHONPATH', '')}"
|
|
202
|
+
|
|
203
|
+
proc = subprocess.Popen(cmd, env=env)
|
|
204
|
+
proc.wait()
|
|
205
|
+
|
|
206
|
+
finally:
|
|
207
|
+
# Clean up temp file
|
|
208
|
+
if os.path.exists(wrapper_path):
|
|
209
|
+
os.unlink(wrapper_path)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def run_wsgi_server(
|
|
213
|
+
service_module: str, service_class_name: str, port: int, package_name: str = ""
|
|
214
|
+
):
|
|
215
|
+
"""Run a WSGI server with gunicorn."""
|
|
216
|
+
# Create a temporary file with the WSGI wrapper
|
|
217
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
|
|
218
|
+
wrapper_code = create_wsgi_wrapper(
|
|
219
|
+
service_module, service_class_name, package_name
|
|
220
|
+
)
|
|
221
|
+
f.write(wrapper_code)
|
|
222
|
+
wrapper_path = f.name
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
# Get the module name from the temp file
|
|
226
|
+
wrapper_module = Path(wrapper_path).stem
|
|
227
|
+
|
|
228
|
+
print(f"✓ Starting WSGI server with Gunicorn on port {port}...")
|
|
229
|
+
print(f" Service: {service_class_name}")
|
|
230
|
+
print(f" HTTP: http://localhost:{port}")
|
|
231
|
+
print(f" Connect RPC endpoint: http://localhost:{port}/{service_class_name}/")
|
|
232
|
+
print("\nPress Ctrl+C to stop the server\n")
|
|
233
|
+
|
|
234
|
+
# Run gunicorn
|
|
235
|
+
cmd = [
|
|
236
|
+
sys.executable,
|
|
237
|
+
"-m",
|
|
238
|
+
"gunicorn",
|
|
239
|
+
f"{wrapper_module}:application",
|
|
240
|
+
"--bind",
|
|
241
|
+
f"0.0.0.0:{port}",
|
|
242
|
+
"--access-logfile",
|
|
243
|
+
"-",
|
|
244
|
+
"--error-logfile",
|
|
245
|
+
"-",
|
|
246
|
+
"--workers",
|
|
247
|
+
str(args.workers) if hasattr(args, "workers") else "1",
|
|
248
|
+
]
|
|
249
|
+
|
|
250
|
+
# Set PYTHONPATH to include the temp file directory
|
|
251
|
+
env = os.environ.copy()
|
|
252
|
+
env["PYTHONPATH"] = f"{Path(wrapper_path).parent}:{env.get('PYTHONPATH', '')}"
|
|
253
|
+
|
|
254
|
+
proc = subprocess.Popen(cmd, env=env)
|
|
255
|
+
proc.wait()
|
|
256
|
+
|
|
257
|
+
finally:
|
|
258
|
+
# Clean up temp file
|
|
259
|
+
if os.path.exists(wrapper_path):
|
|
260
|
+
os.unlink(wrapper_path)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def cmd_serve(args):
|
|
264
|
+
"""Start a server with the specified service."""
|
|
265
|
+
try:
|
|
266
|
+
# Parse module and class name
|
|
267
|
+
if "." in args.service:
|
|
268
|
+
parts = args.service.split(".")
|
|
269
|
+
if parts[-1][0].isupper():
|
|
270
|
+
service_module = ".".join(parts[:-1])
|
|
271
|
+
service_class_name = parts[-1]
|
|
272
|
+
else:
|
|
273
|
+
service_module = args.service
|
|
274
|
+
service_class_name = args.class_name
|
|
275
|
+
else:
|
|
276
|
+
service_module = args.service
|
|
277
|
+
service_class_name = args.class_name
|
|
278
|
+
|
|
279
|
+
if not service_class_name:
|
|
280
|
+
raise ValueError("Class name must be provided")
|
|
281
|
+
|
|
282
|
+
# Import the service class to validate it exists
|
|
283
|
+
service_class = import_service_class(service_module, service_class_name)
|
|
284
|
+
|
|
285
|
+
# Determine server type and run
|
|
286
|
+
if args.asgi:
|
|
287
|
+
# Run ASGI server with hypercorn
|
|
288
|
+
run_asgi_server(
|
|
289
|
+
service_module, service_class_name, args.port, args.package or ""
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
elif args.wsgi:
|
|
293
|
+
# Run WSGI server with gunicorn
|
|
294
|
+
run_wsgi_server(
|
|
295
|
+
service_module, service_class_name, args.port, args.package or ""
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
else:
|
|
299
|
+
# Default to gRPC server
|
|
300
|
+
from pydantic_rpc import Server, AsyncIOServer
|
|
301
|
+
|
|
302
|
+
# Create an instance of the service
|
|
303
|
+
service_instance = service_class()
|
|
304
|
+
|
|
305
|
+
# Check if service has async methods
|
|
306
|
+
has_async = any(
|
|
307
|
+
asyncio.iscoroutinefunction(getattr(service_instance, name))
|
|
308
|
+
for name in dir(service_instance)
|
|
309
|
+
if not name.startswith("_")
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
if has_async or args.asyncio:
|
|
313
|
+
server = AsyncIOServer(
|
|
314
|
+
service=service_instance,
|
|
315
|
+
port=args.port,
|
|
316
|
+
package_name=args.package or "",
|
|
317
|
+
)
|
|
318
|
+
print(f"✓ Starting AsyncIO gRPC server on port {args.port}...")
|
|
319
|
+
print(f" Service: {service_class_name}")
|
|
320
|
+
if args.reflection:
|
|
321
|
+
print(" Reflection: enabled")
|
|
322
|
+
print("\nPress Ctrl+C to stop the server\n")
|
|
323
|
+
|
|
324
|
+
loop = asyncio.get_event_loop()
|
|
325
|
+
loop.run_until_complete(server.run())
|
|
326
|
+
|
|
327
|
+
else:
|
|
328
|
+
server = Server(
|
|
329
|
+
service=service_instance,
|
|
330
|
+
port=args.port,
|
|
331
|
+
package_name=args.package or "",
|
|
332
|
+
)
|
|
333
|
+
print(f"✓ Starting gRPC server on port {args.port}...")
|
|
334
|
+
print(f" Service: {service_class_name}")
|
|
335
|
+
if args.reflection:
|
|
336
|
+
print(" Reflection: enabled")
|
|
337
|
+
print("\nPress Ctrl+C to stop the server\n")
|
|
338
|
+
|
|
339
|
+
server.run()
|
|
340
|
+
|
|
341
|
+
return 0
|
|
342
|
+
|
|
343
|
+
except KeyboardInterrupt:
|
|
344
|
+
print("\n✓ Server stopped")
|
|
345
|
+
return 0
|
|
346
|
+
except Exception as e:
|
|
347
|
+
print(f"✗ Error: {e}", file=sys.stderr)
|
|
348
|
+
import traceback
|
|
349
|
+
|
|
350
|
+
traceback.print_exc()
|
|
351
|
+
return 1
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def main():
|
|
355
|
+
"""Main entry point for the CLI."""
|
|
356
|
+
parser = argparse.ArgumentParser(
|
|
357
|
+
prog="pydantic-rpc",
|
|
358
|
+
description="CLI tool for pydantic-rpc: Generate and serve gRPC/Connect services from Pydantic models",
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
362
|
+
|
|
363
|
+
# Generate command
|
|
364
|
+
generate_parser = subparsers.add_parser(
|
|
365
|
+
"generate", help="Generate protobuf file from a service class"
|
|
366
|
+
)
|
|
367
|
+
generate_parser.add_argument(
|
|
368
|
+
"service",
|
|
369
|
+
help="Python module path to the service (e.g., myapp.services.UserService)",
|
|
370
|
+
)
|
|
371
|
+
generate_parser.add_argument(
|
|
372
|
+
"--class-name", help="Service class name (if not included in service path)"
|
|
373
|
+
)
|
|
374
|
+
generate_parser.add_argument(
|
|
375
|
+
"--output",
|
|
376
|
+
"-o",
|
|
377
|
+
help="Output file or directory path (default: current directory)",
|
|
378
|
+
)
|
|
379
|
+
generate_parser.add_argument("--package", "-p", help="Protobuf package name")
|
|
380
|
+
generate_parser.add_argument(
|
|
381
|
+
"--compile",
|
|
382
|
+
"-c",
|
|
383
|
+
action="store_true",
|
|
384
|
+
help="Also compile the generated proto file",
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# Serve command
|
|
388
|
+
serve_parser = subparsers.add_parser(
|
|
389
|
+
"serve", help="Start a server with the specified service"
|
|
390
|
+
)
|
|
391
|
+
serve_parser.add_argument(
|
|
392
|
+
"service",
|
|
393
|
+
help="Python module path to the service (e.g., myapp.services.UserService)",
|
|
394
|
+
)
|
|
395
|
+
serve_parser.add_argument(
|
|
396
|
+
"--class-name", help="Service class name (if not included in service path)"
|
|
397
|
+
)
|
|
398
|
+
serve_parser.add_argument(
|
|
399
|
+
"--port",
|
|
400
|
+
"-p",
|
|
401
|
+
type=int,
|
|
402
|
+
default=50051,
|
|
403
|
+
help="Port to listen on (default: 50051)",
|
|
404
|
+
)
|
|
405
|
+
serve_parser.add_argument("--package", help="Protobuf package name")
|
|
406
|
+
serve_parser.add_argument(
|
|
407
|
+
"--reflection",
|
|
408
|
+
"-r",
|
|
409
|
+
action="store_true",
|
|
410
|
+
default=True,
|
|
411
|
+
help="Enable gRPC reflection (default: True)",
|
|
412
|
+
)
|
|
413
|
+
serve_parser.add_argument(
|
|
414
|
+
"--asyncio", action="store_true", help="Force use of AsyncIO server"
|
|
415
|
+
)
|
|
416
|
+
serve_parser.add_argument(
|
|
417
|
+
"--asgi",
|
|
418
|
+
action="store_true",
|
|
419
|
+
help="Run as ASGI app with Hypercorn (Connect RPC over HTTP/2)",
|
|
420
|
+
)
|
|
421
|
+
serve_parser.add_argument(
|
|
422
|
+
"--wsgi",
|
|
423
|
+
action="store_true",
|
|
424
|
+
help="Run as WSGI app with Gunicorn (Connect RPC over HTTP/1.1)",
|
|
425
|
+
)
|
|
426
|
+
serve_parser.add_argument(
|
|
427
|
+
"--workers",
|
|
428
|
+
"-w",
|
|
429
|
+
type=int,
|
|
430
|
+
default=1,
|
|
431
|
+
help="Number of worker processes for WSGI server (default: 1)",
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
global args # Make args accessible to run_wsgi_server
|
|
435
|
+
args = parser.parse_args()
|
|
436
|
+
|
|
437
|
+
if not args.command:
|
|
438
|
+
parser.print_help()
|
|
439
|
+
return 1
|
|
440
|
+
|
|
441
|
+
if args.command == "generate":
|
|
442
|
+
return cmd_generate(args)
|
|
443
|
+
elif args.command == "serve":
|
|
444
|
+
return cmd_serve(args)
|
|
445
|
+
else:
|
|
446
|
+
parser.print_help()
|
|
447
|
+
return 1
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
if __name__ == "__main__":
|
|
451
|
+
sys.exit(main())
|