pydantic-rpc 0.2.0__tar.gz → 0.2.2__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.
Files changed (45) hide show
  1. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/PKG-INFO +37 -3
  2. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/README.md +36 -2
  3. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/agent_aio_grpc.py +11 -0
  4. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/agent_connecpy.py +11 -0
  5. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/pyproject.toml +1 -1
  6. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/src/pydantic_rpc/core.py +75 -37
  7. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/.gitignore +0 -0
  8. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/.python-version +0 -0
  9. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/LICENSE +0 -0
  10. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/README.md +0 -0
  11. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/asyncio_greeting.py +0 -0
  12. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/barservice.proto +0 -0
  13. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/barservice_pb2.py +0 -0
  14. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/barservice_pb2.pyi +0 -0
  15. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/barservice_pb2_grpc.py +0 -0
  16. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/foobar.py +0 -0
  17. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/foobar_client.py +0 -0
  18. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/fooservice.proto +0 -0
  19. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/fooservice_pb2.py +0 -0
  20. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/fooservice_pb2.pyi +0 -0
  21. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/fooservice_pb2_grpc.py +0 -0
  22. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/google/protobuf/duration.proto +0 -0
  23. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/google/protobuf/timestamp.proto +0 -0
  24. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/greeter.proto +0 -0
  25. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/greeter_client.py +0 -0
  26. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/greeter_connecpy.py +0 -0
  27. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/greeter_connecpy_client.py +0 -0
  28. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/greeter_pb2.py +0 -0
  29. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/greeter_pb2.pyi +0 -0
  30. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/greeter_pb2_grpc.py +0 -0
  31. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/greeter_sonora_client.py +0 -0
  32. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/greeting.py +0 -0
  33. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/greeting_asgi.py +0 -0
  34. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/greeting_connecpy.py +0 -0
  35. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/greeting_using_exsiting_pb2_modules.py +0 -0
  36. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/greeting_wsgi.py +0 -0
  37. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/olympicslocationagent.proto +0 -0
  38. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/olympicslocationagent_connecpy.py +0 -0
  39. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/olympicslocationagent_pb2.py +0 -0
  40. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/olympicslocationagent_pb2.pyi +0 -0
  41. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/examples/olympicslocationagent_pb2_grpc.py +0 -0
  42. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/requirements-dev.lock +0 -0
  43. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/requirements.lock +0 -0
  44. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/src/pydantic_rpc/__init__.py +0 -0
  45. {pydantic_rpc-0.2.0 → pydantic_rpc-0.2.2}/src/pydantic_rpc/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pydantic-rpc
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: A Python library for building gRPC/ConnectRPC services with Pydantic models.
5
5
  Author: Yasushi Itoh
6
6
  Requires-Python: >=3.11
@@ -14,10 +14,10 @@ Description-Content-Type: text/markdown
14
14
 
15
15
  # 🚀 PydanticRPC
16
16
 
17
- **PydanticRPC** is a Python library that enables you to rapidly expose Pydantic models via gRPC/ConnectRPC 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.
17
+ **PydanticRPC** is a Python library that enables you to rapidly expose [Pydantic](https://docs.pydantic.dev/) models via [gRPC](https://grpc.io/)/[ConnectRPC](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.
18
18
 
19
19
 
20
- Below is an example of a simple gRPC service that exposes a PydanticAI agent:
20
+ Below is an example of a simple gRPC service that exposes a [PydanticAI](https://ai.pydantic.dev/) agent:
21
21
 
22
22
  ```python
23
23
  import asyncio
@@ -54,6 +54,40 @@ if __name__ == "__main__":
54
54
  loop.run_until_complete(s.run(OlympicsLocationAgent()))
55
55
  ```
56
56
 
57
+ And here is an example of a simple ConnectRPC service that exposes the same agent as an ASGI application:
58
+
59
+ ```python
60
+ import asyncio
61
+
62
+ from pydantic_ai import Agent
63
+ from pydantic_rpc import ConnecpyASGIApp, Message
64
+
65
+
66
+ class CityLocation(Message):
67
+ city: str
68
+ country: str
69
+
70
+
71
+ class Olympics(Message):
72
+ year: int
73
+
74
+ def prompt(self):
75
+ return f"Where were the Olympics held in {self.year}?"
76
+
77
+
78
+ class OlympicsLocationAgent:
79
+ def __init__(self):
80
+ self._agent = Agent("ollama:llama3.2", result_type=CityLocation)
81
+
82
+ async def ask(self, req: Olympics) -> CityLocation:
83
+ result = await self._agent.run(req.prompt())
84
+ return result.data
85
+
86
+ app = ConnecpyASGIApp()
87
+ app.mount(OlympicsLocationAgent())
88
+
89
+ ```
90
+
57
91
 
58
92
  ## 💡 Key Features
59
93
 
@@ -1,9 +1,9 @@
1
1
  # 🚀 PydanticRPC
2
2
 
3
- **PydanticRPC** is a Python library that enables you to rapidly expose Pydantic models via gRPC/ConnectRPC 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.
3
+ **PydanticRPC** is a Python library that enables you to rapidly expose [Pydantic](https://docs.pydantic.dev/) models via [gRPC](https://grpc.io/)/[ConnectRPC](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.
4
4
 
5
5
 
6
- Below is an example of a simple gRPC service that exposes a PydanticAI agent:
6
+ Below is an example of a simple gRPC service that exposes a [PydanticAI](https://ai.pydantic.dev/) agent:
7
7
 
8
8
  ```python
9
9
  import asyncio
@@ -40,6 +40,40 @@ if __name__ == "__main__":
40
40
  loop.run_until_complete(s.run(OlympicsLocationAgent()))
41
41
  ```
42
42
 
43
+ And here is an example of a simple ConnectRPC service that exposes the same agent as an ASGI application:
44
+
45
+ ```python
46
+ import asyncio
47
+
48
+ from pydantic_ai import Agent
49
+ from pydantic_rpc import ConnecpyASGIApp, Message
50
+
51
+
52
+ class CityLocation(Message):
53
+ city: str
54
+ country: str
55
+
56
+
57
+ class Olympics(Message):
58
+ year: int
59
+
60
+ def prompt(self):
61
+ return f"Where were the Olympics held in {self.year}?"
62
+
63
+
64
+ class OlympicsLocationAgent:
65
+ def __init__(self):
66
+ self._agent = Agent("ollama:llama3.2", result_type=CityLocation)
67
+
68
+ async def ask(self, req: Olympics) -> CityLocation:
69
+ result = await self._agent.run(req.prompt())
70
+ return result.data
71
+
72
+ app = ConnecpyASGIApp()
73
+ app.mount(OlympicsLocationAgent())
74
+
75
+ ```
76
+
43
77
 
44
78
  ## 💡 Key Features
45
79
 
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
2
 
3
+ from pydantic import field_validator
3
4
  from pydantic_ai import Agent
4
5
  from pydantic_rpc import AsyncIOServer, Message
5
6
 
@@ -16,6 +17,16 @@ class Olympics(Message):
16
17
  def prompt(self):
17
18
  return f"Where were the Olympics held in {self.year}?"
18
19
 
20
+ @field_validator("year")
21
+ def validate_year(cls, value):
22
+ if value < 1896:
23
+ raise ValueError("The first modern Olympics was held in 1896.")
24
+
25
+ if value % 4 != 0:
26
+ raise ValueError("The Olympics are held every 4 years.")
27
+
28
+ return value
29
+
19
30
 
20
31
  class OlympicsLocationAgent:
21
32
  def __init__(self):
@@ -2,6 +2,7 @@ import asyncio
2
2
 
3
3
  from hypercorn.asyncio import serve
4
4
  from hypercorn.config import Config
5
+ from pydantic import field_validator
5
6
  from pydantic_ai import Agent
6
7
  from pydantic_rpc import ConnecpyASGIApp, Message
7
8
 
@@ -17,6 +18,16 @@ class Olympics(Message):
17
18
  def prompt(self):
18
19
  return f"Where were the Olympics held in {self.year}?"
19
20
 
21
+ @field_validator("year")
22
+ def validate_year(cls, value):
23
+ if value < 1896:
24
+ raise ValueError("The first modern Olympics was held in 1896.")
25
+
26
+ if value % 4 != 0:
27
+ raise ValueError("The Olympics are held every 4 years.")
28
+
29
+ return value
30
+
20
31
 
21
32
  class OlympicsLocationAgent:
22
33
  def __init__(self):
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pydantic-rpc"
3
- version = "0.2.0"
3
+ version = "0.2.2"
4
4
  description = "A Python library for building gRPC/ConnectRPC services with Pydantic models."
5
5
  authors = [
6
6
  { name = "Yasushi Itoh" }
@@ -24,10 +24,11 @@ from grpc_health.v1 import health_pb2, health_pb2_grpc
24
24
  from grpc_health.v1.health import HealthServicer
25
25
  from grpc_reflection.v1alpha import reflection
26
26
  from grpc_tools import protoc
27
- from pydantic import BaseModel
27
+ from pydantic import BaseModel, ValidationError
28
28
  from sonora.wsgi import grpcWSGI
29
29
  from sonora.asgi import grpcASGI
30
30
  from connecpy.asgi import ConnecpyASGIApp as ConnecpyASGI
31
+ from connecpy.errors import Errors
31
32
 
32
33
  # Protobuf Python modules for Timestamp, Duration (requires protobuf / grpcio)
33
34
  from google.protobuf import timestamp_pb2, duration_pb2
@@ -198,25 +199,35 @@ def connect_obj_with_stub(pb2_grpc_module, pb2_module, service_obj: object) -> t
198
199
  case 1:
199
200
 
200
201
  def stub_method1(self, request, context, method=method):
201
- # Convert request to Python object
202
- arg = converter(request)
203
- # Invoke the actual method
204
- resp_obj = method(arg)
205
- # Convert the returned Python Message to a protobuf message
206
- return convert_python_message_to_proto(
207
- resp_obj, response_type, pb2_module
208
- )
202
+ try:
203
+ # Convert request to Python object
204
+ arg = converter(request)
205
+ # Invoke the actual method
206
+ resp_obj = method(arg)
207
+ # Convert the returned Python Message to a protobuf message
208
+ return convert_python_message_to_proto(
209
+ resp_obj, response_type, pb2_module
210
+ )
211
+ except ValidationError as e:
212
+ return context.abort(grpc.StatusCode.INVALID_ARGUMENT, str(e))
213
+ except Exception as e:
214
+ return context.abort(grpc.StatusCode.INTERNAL, str(e))
209
215
 
210
216
  return stub_method1
211
217
 
212
218
  case 2:
213
219
 
214
220
  def stub_method2(self, request, context, method=method):
215
- arg = converter(request)
216
- resp_obj = method(arg, context)
217
- return convert_python_message_to_proto(
218
- resp_obj, response_type, pb2_module
219
- )
221
+ try:
222
+ arg = converter(request)
223
+ resp_obj = method(arg, context)
224
+ return convert_python_message_to_proto(
225
+ resp_obj, response_type, pb2_module
226
+ )
227
+ except ValidationError as e:
228
+ return context.abort(grpc.StatusCode.INVALID_ARGUMENT, str(e))
229
+ except Exception as e:
230
+ return context.abort(grpc.StatusCode.INTERNAL, str(e))
220
231
 
221
232
  return stub_method2
222
233
 
@@ -255,22 +266,32 @@ def connect_obj_with_stub_async(pb2_grpc_module, pb2_module, obj: object) -> typ
255
266
  case 1:
256
267
 
257
268
  async def stub_method1(self, request, context, method=method):
258
- arg = converter(request)
259
- resp_obj = await method(arg)
260
- return convert_python_message_to_proto(
261
- resp_obj, response_type, pb2_module
262
- )
269
+ try:
270
+ arg = converter(request)
271
+ resp_obj = await method(arg)
272
+ return convert_python_message_to_proto(
273
+ resp_obj, response_type, pb2_module
274
+ )
275
+ except ValidationError as e:
276
+ await context.abort(grpc.StatusCode.INVALID_ARGUMENT, str(e))
277
+ except Exception as e:
278
+ await context.abort(grpc.StatusCode.INTERNAL, str(e))
263
279
 
264
280
  return stub_method1
265
281
 
266
282
  case 2:
267
283
 
268
284
  async def stub_method2(self, request, context, method=method):
269
- arg = converter(request)
270
- resp_obj = await method(arg, context)
271
- return convert_python_message_to_proto(
272
- resp_obj, response_type, pb2_module
273
- )
285
+ try:
286
+ arg = converter(request)
287
+ resp_obj = await method(arg, context)
288
+ return convert_python_message_to_proto(
289
+ resp_obj, response_type, pb2_module
290
+ )
291
+ except ValidationError as e:
292
+ await context.abort(grpc.StatusCode.INVALID_ARGUMENT, str(e))
293
+ except Exception as e:
294
+ await context.abort(grpc.StatusCode.INTERNAL, str(e))
274
295
 
275
296
  return stub_method2
276
297
 
@@ -302,7 +323,6 @@ def connect_obj_with_stub_async_connecpy(
302
323
 
303
324
  def implement_stub_method(method):
304
325
  sig = inspect.signature(method)
305
- print(type(method))
306
326
  arg_type = get_request_arg_type(sig)
307
327
  converter = generate_message_converter(arg_type)
308
328
  response_type = sig.return_annotation
@@ -312,22 +332,32 @@ def connect_obj_with_stub_async_connecpy(
312
332
  case 1:
313
333
 
314
334
  async def stub_method1(self, request, context, method=method):
315
- arg = converter(request)
316
- resp_obj = await method(arg)
317
- return convert_python_message_to_proto(
318
- resp_obj, response_type, pb2_module
319
- )
335
+ try:
336
+ arg = converter(request)
337
+ resp_obj = await method(arg)
338
+ return convert_python_message_to_proto(
339
+ resp_obj, response_type, pb2_module
340
+ )
341
+ except ValidationError as e:
342
+ await context.abort(Errors.InvalidArgument, str(e))
343
+ except Exception as e:
344
+ await context.abort(Errors.Internal, str(e))
320
345
 
321
346
  return stub_method1
322
347
 
323
348
  case 2:
324
349
 
325
350
  async def stub_method2(self, request, context, method=method):
326
- arg = converter(request)
327
- resp_obj = await method(arg, context)
328
- return convert_python_message_to_proto(
329
- resp_obj, response_type, pb2_module
330
- )
351
+ try:
352
+ arg = converter(request)
353
+ resp_obj = await method(arg, context)
354
+ return convert_python_message_to_proto(
355
+ resp_obj, response_type, pb2_module
356
+ )
357
+ except ValidationError as e:
358
+ await context.abort(Errors.InvalidArgument, str(e))
359
+ except Exception as e:
360
+ await context.abort(Errors.Internal, str(e))
331
361
 
332
362
  return stub_method2
333
363
 
@@ -865,7 +895,11 @@ def generate_and_compile_proto(obj: object, package_name: str = ""):
865
895
  pb2_grpc_module = importlib.import_module(
866
896
  f"{obj.__class__.__name__.lower()}_pb2_grpc"
867
897
  )
868
- return pb2_grpc_module, pb2_module
898
+
899
+ if pb2_grpc_module is not None and pb2_module is not None:
900
+ return pb2_grpc_module, pb2_module
901
+
902
+ # If the modules are not found, generate and compile the proto files.
869
903
 
870
904
  klass = obj.__class__
871
905
  proto_file = generate_proto(obj, package_name)
@@ -892,7 +926,11 @@ def generate_and_compile_proto_using_connecpy(obj: object, package_name: str = "
892
926
  connecpy_module = importlib.import_module(
893
927
  f"{obj.__class__.__name__.lower()}_connecpy"
894
928
  )
895
- return connecpy_module, pb2_module
929
+
930
+ if connecpy_module is not None and pb2_module is not None:
931
+ return connecpy_module, pb2_module
932
+
933
+ # If the modules are not found, generate and compile the proto files.
896
934
 
897
935
  klass = obj.__class__
898
936
  proto_file = generate_proto(obj, package_name)
File without changes
File without changes