pydantic-rpc 0.11.0__tar.gz → 0.13.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.
@@ -1,3 +1,21 @@
1
+ Metadata-Version: 2.3
2
+ Name: pydantic-rpc
3
+ Version: 0.13.0
4
+ Summary: A Python library for building gRPC/ConnectRPC services with Pydantic models.
5
+ Author: Yasushi Itoh
6
+ Requires-Dist: annotated-types==0.7.0
7
+ Requires-Dist: pydantic>=2.1.1
8
+ Requires-Dist: grpcio>=1.56.2
9
+ Requires-Dist: grpcio-tools>=1.56.2
10
+ Requires-Dist: grpcio-reflection>=1.56.2
11
+ Requires-Dist: grpcio-health-checking>=1.56.2
12
+ Requires-Dist: connect-python>=0.5.0
13
+ Requires-Dist: protoc-gen-connect-python>=0.1.0
14
+ Requires-Dist: mcp>=1.9.4
15
+ Requires-Dist: starlette>=0.27.0
16
+ Requires-Python: >=3.11
17
+ Description-Content-Type: text/markdown
18
+
1
19
  # 🚀 PydanticRPC
2
20
 
3
21
  **PydanticRPC** is a Python library that enables you to rapidly expose [Pydantic](https://docs.pydantic.dev/) models via [gRPC](https://grpc.io/)/[Connect RPC](https://connectrpc.com/docs/protocol/) services without writing any protobuf files. Instead, it automatically generates protobuf files on the fly from the method signatures of your Python objects and the type signatures of your Pydantic models.
@@ -1230,6 +1248,64 @@ class MyMessage(Message):
1230
1248
 
1231
1249
  This approach works because protobuf allows message types within `oneof` fields, and the collections are contained within those messages.
1232
1250
 
1251
+ ## 🔧 Development
1252
+
1253
+ This project uses [`just`](https://github.com/casey/just) as a command runner for development tasks.
1254
+
1255
+ ### Installing just
1256
+
1257
+ **macOS:**
1258
+ ```bash
1259
+ brew install just
1260
+ ```
1261
+
1262
+ **Linux:**
1263
+ ```bash
1264
+ curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to ~/bin
1265
+ ```
1266
+
1267
+ **Windows:**
1268
+ Download from [GitHub releases](https://github.com/casey/just/releases)
1269
+
1270
+ ### Quick Start
1271
+
1272
+ ```bash
1273
+ # Install dependencies
1274
+ just install
1275
+
1276
+ # Run tests
1277
+ just test # or just t
1278
+
1279
+ # Format and lint code
1280
+ just format # or just f
1281
+ just lint # or just l
1282
+
1283
+ # Run all checks (lint + tests)
1284
+ just check # or just c
1285
+
1286
+ # See all available commands
1287
+ just --list
1288
+ ```
1289
+
1290
+ ### Running Examples
1291
+
1292
+ ```bash
1293
+ # Start servers
1294
+ just greeting-server # gRPC server on port 50051
1295
+ just greeting-asgi # Connect RPC ASGI on port 8000
1296
+ just greeting-wsgi # Connect RPC WSGI on port 3000
1297
+
1298
+ # Test with buf curl (in another terminal)
1299
+ just greet # gRPC request
1300
+ just connect-greet # Connect RPC request
1301
+ just wsgi-greet # WSGI request
1302
+
1303
+ # Custom names
1304
+ just greet-name Alice
1305
+ just connect-greet-name Bob
1306
+ ```
1307
+
1308
+ For more development commands and options, see the [Justfile](Justfile) or run `just --list`.
1233
1309
 
1234
1310
  ## TODO
1235
1311
  - [x] Streaming Support
@@ -1,20 +1,3 @@
1
- Metadata-Version: 2.3
2
- Name: pydantic-rpc
3
- Version: 0.11.0
4
- Summary: A Python library for building gRPC/ConnectRPC services with Pydantic models.
5
- Author: Yasushi Itoh
6
- Requires-Dist: annotated-types==0.7.0
7
- Requires-Dist: pydantic>=2.1.1
8
- Requires-Dist: grpcio>=1.56.2
9
- Requires-Dist: grpcio-tools>=1.56.2
10
- Requires-Dist: grpcio-reflection>=1.56.2
11
- Requires-Dist: grpcio-health-checking>=1.56.2
12
- Requires-Dist: connecpy>=2.2.0
13
- Requires-Dist: mcp>=1.9.4
14
- Requires-Dist: starlette>=0.27.0
15
- Requires-Python: >=3.11
16
- Description-Content-Type: text/markdown
17
-
18
1
  # 🚀 PydanticRPC
19
2
 
20
3
  **PydanticRPC** is a Python library that enables you to rapidly expose [Pydantic](https://docs.pydantic.dev/) models via [gRPC](https://grpc.io/)/[Connect RPC](https://connectrpc.com/docs/protocol/) services without writing any protobuf files. Instead, it automatically generates protobuf files on the fly from the method signatures of your Python objects and the type signatures of your Pydantic models.
@@ -1247,6 +1230,64 @@ class MyMessage(Message):
1247
1230
 
1248
1231
  This approach works because protobuf allows message types within `oneof` fields, and the collections are contained within those messages.
1249
1232
 
1233
+ ## 🔧 Development
1234
+
1235
+ This project uses [`just`](https://github.com/casey/just) as a command runner for development tasks.
1236
+
1237
+ ### Installing just
1238
+
1239
+ **macOS:**
1240
+ ```bash
1241
+ brew install just
1242
+ ```
1243
+
1244
+ **Linux:**
1245
+ ```bash
1246
+ curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to ~/bin
1247
+ ```
1248
+
1249
+ **Windows:**
1250
+ Download from [GitHub releases](https://github.com/casey/just/releases)
1251
+
1252
+ ### Quick Start
1253
+
1254
+ ```bash
1255
+ # Install dependencies
1256
+ just install
1257
+
1258
+ # Run tests
1259
+ just test # or just t
1260
+
1261
+ # Format and lint code
1262
+ just format # or just f
1263
+ just lint # or just l
1264
+
1265
+ # Run all checks (lint + tests)
1266
+ just check # or just c
1267
+
1268
+ # See all available commands
1269
+ just --list
1270
+ ```
1271
+
1272
+ ### Running Examples
1273
+
1274
+ ```bash
1275
+ # Start servers
1276
+ just greeting-server # gRPC server on port 50051
1277
+ just greeting-asgi # Connect RPC ASGI on port 8000
1278
+ just greeting-wsgi # Connect RPC WSGI on port 3000
1279
+
1280
+ # Test with buf curl (in another terminal)
1281
+ just greet # gRPC request
1282
+ just connect-greet # Connect RPC request
1283
+ just wsgi-greet # WSGI request
1284
+
1285
+ # Custom names
1286
+ just greet-name Alice
1287
+ just connect-greet-name Bob
1288
+ ```
1289
+
1290
+ For more development commands and options, see the [Justfile](Justfile) or run `just --list`.
1250
1291
 
1251
1292
  ## TODO
1252
1293
  - [x] Streaming Support
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pydantic-rpc"
3
- version = "0.11.0"
3
+ version = "0.13.0"
4
4
  description = "A Python library for building gRPC/ConnectRPC services with Pydantic models."
5
5
  authors = [
6
6
  { name = "Yasushi Itoh" }
@@ -12,7 +12,8 @@ dependencies = [
12
12
  "grpcio-tools>=1.56.2",
13
13
  "grpcio-reflection>=1.56.2",
14
14
  "grpcio-health-checking>=1.56.2",
15
- "connecpy>=2.2.0",
15
+ "connect-python>=0.5.0",
16
+ "protoc-gen-connect-python>=0.1.0",
16
17
  "mcp>=1.9.4",
17
18
  "starlette>=0.27.0",
18
19
  ]
@@ -32,6 +33,7 @@ dev-dependencies = [
32
33
  "pytest>=8.3.4",
33
34
  "pytest-asyncio>=0.20.3",
34
35
  "ruff>=0.9.4",
36
+ "uvicorn>=0.34.0",
35
37
  ]
36
38
 
37
39
  [tool.pytest.ini_options]
@@ -1,6 +1,9 @@
1
+ import annotated_types
1
2
  import asyncio
2
3
  import datetime
3
4
  import enum
5
+ import grpc
6
+ import grpc_tools
4
7
  import importlib.util
5
8
  import inspect
6
9
  import os
@@ -8,11 +11,19 @@ import signal
8
11
  import sys
9
12
  import time
10
13
  import types
11
- from typing import Union
12
14
  from collections.abc import AsyncIterator, Awaitable, Callable, Iterable
13
15
  from concurrent import futures
16
+ from connectrpc.code import Code as Errors
17
+ # Protobuf Python modules for Timestamp, Duration (requires protobuf / grpcio)
18
+ from google.protobuf import duration_pb2, timestamp_pb2, empty_pb2
19
+ from grpc import ServicerContext
20
+ from grpc_health.v1 import health_pb2, health_pb2_grpc
21
+ from grpc_health.v1.health import HealthServicer
22
+ from grpc_reflection.v1alpha import reflection
23
+ from grpc_tools import protoc
14
24
  from pathlib import Path
15
25
  from posixpath import basename
26
+ from pydantic import BaseModel, ValidationError
16
27
  from typing import (
17
28
  Any,
18
29
  Optional,
@@ -22,20 +33,10 @@ from typing import (
22
33
  cast,
23
34
  TypeGuard,
24
35
  )
36
+ from typing import Union
37
+ from typing import Union, Sequence, Tuple
38
+ from concurrent.futures import Executor
25
39
 
26
- import annotated_types
27
- import grpc
28
- from grpc import ServicerContext
29
- import grpc_tools
30
- from connecpy.code import Code as Errors
31
-
32
- # Protobuf Python modules for Timestamp, Duration (requires protobuf / grpcio)
33
- from google.protobuf import duration_pb2, timestamp_pb2, empty_pb2
34
- from grpc_health.v1 import health_pb2, health_pb2_grpc
35
- from grpc_health.v1.health import HealthServicer
36
- from grpc_reflection.v1alpha import reflection
37
- from grpc_tools import protoc
38
- from pydantic import BaseModel, ValidationError
39
40
  from .decorators import get_method_options, has_http_option
40
41
  from .tls import GrpcTLSConfig
41
42
 
@@ -726,15 +727,15 @@ def connect_obj_with_stub_async(
726
727
  return ConcreteServiceClass
727
728
 
728
729
 
729
- def connect_obj_with_stub_connecpy(
730
- connecpy_module: Any, pb2_module: Any, obj: object
730
+ def connect_obj_with_stub_connect_python(
731
+ connect_python_module: Any, pb2_module: Any, obj: object
731
732
  ) -> type:
732
733
  """
733
- Connect a Python service object to a Connecpy stub.
734
+ Connect a Python service object to a Connect Python stub.
734
735
  """
735
736
  service_class = obj.__class__
736
737
  stub_class_name = service_class.__name__
737
- stub_class = getattr(connecpy_module, stub_class_name)
738
+ stub_class = getattr(connect_python_module, stub_class_name)
738
739
 
739
740
  class ConcreteServiceClass(stub_class):
740
741
  pass
@@ -845,15 +846,15 @@ def connect_obj_with_stub_connecpy(
845
846
  return ConcreteServiceClass
846
847
 
847
848
 
848
- def connect_obj_with_stub_async_connecpy(
849
- connecpy_module: Any, pb2_module: Any, obj: object
849
+ def connect_obj_with_stub_async_connect_python(
850
+ connect_python_module: Any, pb2_module: Any, obj: object
850
851
  ) -> type:
851
852
  """
852
- Connect a Python service object to a Connecpy stub for async methods with streaming support.
853
+ Connect a Python service object to a Connect Python stub for async methods with streaming support.
853
854
  """
854
855
  service_class = obj.__class__
855
856
  stub_class_name = service_class.__name__
856
- stub_class = getattr(connecpy_module, stub_class_name)
857
+ stub_class = getattr(connect_python_module, stub_class_name)
857
858
 
858
859
  class ConcreteServiceClass(stub_class):
859
860
  pass
@@ -2030,10 +2031,10 @@ def generate_grpc_code(proto_path: Path) -> types.ModuleType | None:
2030
2031
  return module
2031
2032
 
2032
2033
 
2033
- def generate_connecpy_code(proto_path: Path) -> types.ModuleType | None:
2034
+ def generate_connect_python_code(proto_path: Path) -> types.ModuleType | None:
2034
2035
  """
2035
- Run protoc with the Connecpy plugin to generate Python Connecpy code from proto_path.
2036
- Writes foo_connecpy.py next to proto_path, then imports and returns that module.
2036
+ Run protoc with the Connect Python plugin to generate Python Connect code from proto_path.
2037
+ Writes foo_connect_python.py next to proto_path, then imports and returns that module.
2037
2038
  """
2038
2039
  # 1) Ensure the .proto exists
2039
2040
  if not proto_path.is_file():
@@ -2051,7 +2052,7 @@ def generate_connecpy_code(proto_path: Path) -> types.ModuleType | None:
2051
2052
  "protoc", # Dummy program name (required for protoc.main)
2052
2053
  "-I.",
2053
2054
  f"-I{well_known_path}",
2054
- f"--connecpy_out={out_str}",
2055
+ f"--connect-python_out={out_str}",
2055
2056
  proto_path.name,
2056
2057
  ]
2057
2058
 
@@ -2065,7 +2066,7 @@ def generate_connecpy_code(proto_path: Path) -> types.ModuleType | None:
2065
2066
 
2066
2067
  # 4) Locate the generated file
2067
2068
  base_name = proto_path.stem # "foo"
2068
- generated_filename = f"{base_name}_connecpy.py" # "foo_connecpy.py"
2069
+ generated_filename = f"{base_name}_connect.py" # "foo_connect.py"
2069
2070
  generated_filepath = out_dir / generated_filename
2070
2071
 
2071
2072
  # 5) Add out_dir to sys.path so we can import by filename
@@ -2074,7 +2075,7 @@ def generate_connecpy_code(proto_path: Path) -> types.ModuleType | None:
2074
2075
 
2075
2076
  # 6) Load and return the module
2076
2077
  spec = importlib.util.spec_from_file_location(
2077
- base_name + "_connecpy", str(generated_filepath)
2078
+ base_name + "_connect", str(generated_filepath)
2078
2079
  )
2079
2080
  if spec is None or spec.loader is None:
2080
2081
  return None
@@ -2259,7 +2260,7 @@ def get_proto_path(proto_filename: str) -> Path:
2259
2260
  return base / proto_filename
2260
2261
 
2261
2262
 
2262
- def generate_and_compile_proto_using_connecpy(
2263
+ def generate_and_compile_proto_using_connect_python(
2263
2264
  obj: object,
2264
2265
  package_name: str = "",
2265
2266
  existing_proto_path: Path | None = None,
@@ -2268,7 +2269,7 @@ def generate_and_compile_proto_using_connecpy(
2268
2269
  import importlib
2269
2270
 
2270
2271
  pb2_module = None
2271
- connecpy_module = None
2272
+ connect_python_module = None
2272
2273
 
2273
2274
  try:
2274
2275
  pb2_module = importlib.import_module(
@@ -2278,14 +2279,14 @@ def generate_and_compile_proto_using_connecpy(
2278
2279
  pass
2279
2280
 
2280
2281
  try:
2281
- connecpy_module = importlib.import_module(
2282
- f"{obj.__class__.__name__.lower()}_connecpy"
2282
+ connect_python_module = importlib.import_module(
2283
+ f"{obj.__class__.__name__.lower()}_connect"
2283
2284
  )
2284
2285
  except ImportError:
2285
2286
  pass
2286
2287
 
2287
- if connecpy_module is not None and pb2_module is not None:
2288
- return connecpy_module, pb2_module
2288
+ if connect_python_module is not None and pb2_module is not None:
2289
+ return connect_python_module, pb2_module
2289
2290
 
2290
2291
  # If the modules are not found, generate and compile the proto files.
2291
2292
 
@@ -2306,10 +2307,10 @@ def generate_and_compile_proto_using_connecpy(
2306
2307
  if gen_pb is None:
2307
2308
  raise Exception("Generating pb code")
2308
2309
 
2309
- gen_connecpy = generate_connecpy_code(proto_file_path)
2310
- if gen_connecpy is None:
2311
- raise Exception("Generating Connecpy code")
2312
- return gen_connecpy, gen_pb
2310
+ gen_connect_python = generate_connect_python_code(proto_file_path)
2311
+ if gen_connect_python is None:
2312
+ raise Exception("Generating Connect Python code")
2313
+ return gen_connect_python, gen_pb
2313
2314
 
2314
2315
 
2315
2316
  def is_combined_proto_enabled() -> bool:
@@ -2711,10 +2712,22 @@ class AsyncIOServer:
2711
2712
  service: Optional[object] = None,
2712
2713
  port: int = 50051,
2713
2714
  package_name: str = "",
2714
- *interceptors: grpc.ServerInterceptor,
2715
2715
  tls: Optional["GrpcTLSConfig"] = None,
2716
+ interceptors: Optional[Sequence[grpc.ServerInterceptor]] = None,
2717
+ migration_thread_pool: Optional[Executor] = None,
2718
+ handlers: Optional[Sequence[grpc.GenericRpcHandler]] = None,
2719
+ options: Optional[Sequence[Tuple[str, Any]]] = None,
2720
+ maximum_concurrent_rpcs: Optional[int] = None,
2721
+ compression: Optional[grpc.Compression] = None,
2716
2722
  ) -> None:
2717
- self._server: grpc.aio.Server = grpc.aio.server(interceptors=interceptors)
2723
+ self._server: grpc.aio.Server = grpc.aio.server(
2724
+ migration_thread_pool=migration_thread_pool,
2725
+ handlers=handlers,
2726
+ interceptors=interceptors,
2727
+ options=options,
2728
+ maximum_concurrent_rpcs=maximum_concurrent_rpcs,
2729
+ compression=compression,
2730
+ )
2718
2731
  self._service_names: list[str] = []
2719
2732
  self._package_name: str = package_name
2720
2733
  self._port: int = port
@@ -2804,20 +2817,24 @@ class AsyncIOServer:
2804
2817
  await self._server.stop(10)
2805
2818
  print("gRPC server shutdown.")
2806
2819
 
2820
+ async def stop(self, grace: float = 10.0):
2821
+ """Stop the gRPC server gracefully."""
2822
+ await self._server.stop(grace)
2807
2823
 
2808
- def get_connecpy_asgi_app_class(connecpy_module: Any, service_name: str):
2809
- """Get the ASGI application class from connecpy module (Connecpy v2.x)."""
2810
- return getattr(connecpy_module, f"{service_name}ASGIApplication")
2811
2824
 
2825
+ def get_connect_python_asgi_app_class(connect_python_module: Any, service_name: str):
2826
+ """Get the ASGI application class from connect-python module."""
2827
+ return getattr(connect_python_module, f"{service_name}ASGIApplication")
2812
2828
 
2813
- def get_connecpy_wsgi_app_class(connecpy_module: Any, service_name: str):
2814
- """Get the WSGI application class from connecpy module (Connecpy v2.x)."""
2815
- return getattr(connecpy_module, f"{service_name}WSGIApplication")
2829
+
2830
+ def get_connect_python_wsgi_app_class(connect_python_module: Any, service_name: str):
2831
+ """Get the WSGI application class from connect-python module."""
2832
+ return getattr(connect_python_module, f"{service_name}WSGIApplication")
2816
2833
 
2817
2834
 
2818
2835
  class ASGIApp:
2819
2836
  """
2820
- An ASGI-compatible application that can serve Connect-RPC via Connecpy.
2837
+ An ASGI-compatible application that can serve Connect-RPC via connect-python.
2821
2838
  """
2822
2839
 
2823
2840
  def __init__(self, service: Optional[object] = None, package_name: str = ""):
@@ -2828,23 +2845,25 @@ class ASGIApp:
2828
2845
 
2829
2846
  def mount(self, obj: object, package_name: str = ""):
2830
2847
  """Generate and compile proto files, then mount the async service implementation."""
2831
- connecpy_module, pb2_module = generate_and_compile_proto_using_connecpy(
2832
- obj, package_name
2848
+ connect_python_module, pb2_module = (
2849
+ generate_and_compile_proto_using_connect_python(obj, package_name)
2833
2850
  )
2834
- self.mount_using_pb2_modules(connecpy_module, pb2_module, obj)
2851
+ self.mount_using_pb2_modules(connect_python_module, pb2_module, obj)
2835
2852
 
2836
2853
  def mount_using_pb2_modules(
2837
- self, connecpy_module: Any, pb2_module: Any, obj: object
2854
+ self, connect_python_module: Any, pb2_module: Any, obj: object
2838
2855
  ):
2839
- """Connect the compiled connecpy and pb2 modules with the async service implementation."""
2840
- concreteServiceClass = connect_obj_with_stub_async_connecpy(
2841
- connecpy_module, pb2_module, obj
2856
+ """Connect the compiled connect-python and pb2 modules with the async service implementation."""
2857
+ concreteServiceClass = connect_obj_with_stub_async_connect_python(
2858
+ connect_python_module, pb2_module, obj
2842
2859
  )
2843
2860
  service_name = obj.__class__.__name__
2844
2861
  service_impl = concreteServiceClass()
2845
2862
 
2846
2863
  # Get the service-specific ASGI application class
2847
- app_class = get_connecpy_asgi_app_class(connecpy_module, service_name)
2864
+ app_class = get_connect_python_asgi_app_class(
2865
+ connect_python_module, service_name
2866
+ )
2848
2867
  app = app_class(service=service_impl)
2849
2868
 
2850
2869
  # Store the app and its path for routing
@@ -2899,7 +2918,7 @@ class ASGIApp:
2899
2918
 
2900
2919
  class WSGIApp:
2901
2920
  """
2902
- A WSGI-compatible application that can serve Connect-RPC via Connecpy.
2921
+ A WSGI-compatible application that can serve Connect-RPC via connect-python.
2903
2922
  """
2904
2923
 
2905
2924
  def __init__(self, service: Optional[object] = None, package_name: str = ""):
@@ -2910,23 +2929,25 @@ class WSGIApp:
2910
2929
 
2911
2930
  def mount(self, obj: object, package_name: str = ""):
2912
2931
  """Generate and compile proto files, then mount the sync service implementation."""
2913
- connecpy_module, pb2_module = generate_and_compile_proto_using_connecpy(
2914
- obj, package_name
2932
+ connect_python_module, pb2_module = (
2933
+ generate_and_compile_proto_using_connect_python(obj, package_name)
2915
2934
  )
2916
- self.mount_using_pb2_modules(connecpy_module, pb2_module, obj)
2935
+ self.mount_using_pb2_modules(connect_python_module, pb2_module, obj)
2917
2936
 
2918
2937
  def mount_using_pb2_modules(
2919
- self, connecpy_module: Any, pb2_module: Any, obj: object
2938
+ self, connect_python_module: Any, pb2_module: Any, obj: object
2920
2939
  ):
2921
- """Connect the compiled connecpy and pb2 modules with the sync service implementation."""
2922
- concreteServiceClass = connect_obj_with_stub_connecpy(
2923
- connecpy_module, pb2_module, obj
2940
+ """Connect the compiled connect-python and pb2 modules with the sync service implementation."""
2941
+ concreteServiceClass = connect_obj_with_stub_connect_python(
2942
+ connect_python_module, pb2_module, obj
2924
2943
  )
2925
2944
  service_name = obj.__class__.__name__
2926
2945
  service_impl = concreteServiceClass()
2927
2946
 
2928
2947
  # Get the service-specific WSGI application class
2929
- app_class = get_connecpy_wsgi_app_class(connecpy_module, service_name)
2948
+ app_class = get_connect_python_wsgi_app_class(
2949
+ connect_python_module, service_name
2950
+ )
2930
2951
  app = app_class(service=service_impl)
2931
2952
 
2932
2953
  # Store the app and its path for routing
@@ -3,7 +3,7 @@
3
3
  from typing import Any, Callable, Dict, List, Optional, TypeVar, Type
4
4
  from functools import wraps
5
5
  import grpc
6
- from connecpy.code import Code as ConnectErrors
6
+ from connectrpc.code import Code as ConnectErrors
7
7
 
8
8
  from .options import OptionMetadata, OPTION_METADATA_ATTR
9
9