pydantic-rpc 0.13.0__tar.gz → 0.15.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.
- {pydantic_rpc-0.13.0 → pydantic_rpc-0.15.0}/PKG-INFO +10 -17
- {pydantic_rpc-0.13.0 → pydantic_rpc-0.15.0}/README.md +9 -16
- {pydantic_rpc-0.13.0 → pydantic_rpc-0.15.0}/pyproject.toml +1 -1
- {pydantic_rpc-0.13.0 → pydantic_rpc-0.15.0}/src/pydantic_rpc/__init__.py +2 -0
- {pydantic_rpc-0.13.0 → pydantic_rpc-0.15.0}/src/pydantic_rpc/core.py +202 -48
- {pydantic_rpc-0.13.0 → pydantic_rpc-0.15.0}/src/pydantic_rpc/decorators.py +47 -13
- {pydantic_rpc-0.13.0 → pydantic_rpc-0.15.0}/src/pydantic_rpc/mcp/__init__.py +0 -0
- {pydantic_rpc-0.13.0 → pydantic_rpc-0.15.0}/src/pydantic_rpc/mcp/converter.py +0 -0
- {pydantic_rpc-0.13.0 → pydantic_rpc-0.15.0}/src/pydantic_rpc/mcp/exporter.py +0 -0
- {pydantic_rpc-0.13.0 → pydantic_rpc-0.15.0}/src/pydantic_rpc/options.py +0 -0
- {pydantic_rpc-0.13.0 → pydantic_rpc-0.15.0}/src/pydantic_rpc/py.typed +0 -0
- {pydantic_rpc-0.13.0 → pydantic_rpc-0.15.0}/src/pydantic_rpc/tls.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pydantic-rpc
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.15.0
|
|
4
4
|
Summary: A Python library for building gRPC/ConnectRPC services with Pydantic models.
|
|
5
5
|
Author: Yasushi Itoh
|
|
6
6
|
Requires-Dist: annotated-types==0.7.0
|
|
@@ -125,7 +125,7 @@ app = ASGIApp(service=OlympicsLocationAgent())
|
|
|
125
125
|
- 🔎 **Server Reflection:** Built-in support for gRPC server reflection.
|
|
126
126
|
- ⚡ **Asynchronous Support:** Easily create asynchronous gRPC services with `AsyncIOServer`.
|
|
127
127
|
- **For Connect-RPC:**
|
|
128
|
-
- 🌐 **Full Protocol Support:** Native Connect-RPC support via `
|
|
128
|
+
- 🌐 **Full Protocol Support:** Native Connect-RPC support via `connect-python`
|
|
129
129
|
- 🔄 **All Streaming Patterns:** Unary, server streaming, client streaming, and bidirectional streaming
|
|
130
130
|
- 🌐 **WSGI/ASGI Applications:** Run as standard WSGI or ASGI applications for easy deployment
|
|
131
131
|
- 🛠️ **Pre-generated Protobuf Files and Code:** Pre-generate proto files and corresponding code via the CLI. By setting the environment variable (PYDANTIC_RPC_SKIP_GENERATION), you can skip runtime generation.
|
|
@@ -304,21 +304,21 @@ app.mount(Greeter())
|
|
|
304
304
|
|
|
305
305
|
### 🏆 Connect-RPC with Streaming Example
|
|
306
306
|
|
|
307
|
-
PydanticRPC provides native Connect-RPC support via
|
|
307
|
+
PydanticRPC provides native Connect-RPC support via connect-python, including full streaming capabilities and PEP 8 naming conventions. Check out our ASGI examples:
|
|
308
308
|
|
|
309
309
|
```bash
|
|
310
310
|
# Run with uvicorn
|
|
311
311
|
uv run uvicorn greeting_asgi:app --port 3000
|
|
312
312
|
|
|
313
313
|
# Or run streaming example
|
|
314
|
-
uv run python examples/
|
|
314
|
+
uv run python examples/streaming_connect_python.py
|
|
315
315
|
```
|
|
316
316
|
|
|
317
|
-
This will launch a
|
|
317
|
+
This will launch a connect-python-based ASGI application that uses the same Pydantic models to serve Connect-RPC requests.
|
|
318
318
|
|
|
319
|
-
#### Streaming Support with
|
|
319
|
+
#### Streaming Support with connect-python
|
|
320
320
|
|
|
321
|
-
|
|
321
|
+
connect-python provides full support for streaming RPCs with automatic PEP 8 naming (snake_case):
|
|
322
322
|
|
|
323
323
|
```python
|
|
324
324
|
from typing import AsyncIterator
|
|
@@ -359,14 +359,7 @@ app.mount(StreamingService())
|
|
|
359
359
|
```
|
|
360
360
|
|
|
361
361
|
> [!NOTE]
|
|
362
|
-
> Please install `protoc-gen-
|
|
363
|
-
>
|
|
364
|
-
> 1. Install Go.
|
|
365
|
-
> - Please follow the instruction described in https://go.dev/doc/install.
|
|
366
|
-
> 2. Install `protoc-gen-connecpy`:
|
|
367
|
-
> ```bash
|
|
368
|
-
> go install github.com/i2y/connecpy/v2/protoc-gen-connecpy@latest
|
|
369
|
-
> ```
|
|
362
|
+
> Please install `protoc-gen-connect-python` to run the connect-python example.
|
|
370
363
|
|
|
371
364
|
## ♻️ Skipping Protobuf Generation
|
|
372
365
|
By default, PydanticRPC generates .proto files and code at runtime. If you wish to skip the code-generation step (for example, in production environment), set the environment variable below:
|
|
@@ -375,9 +368,9 @@ By default, PydanticRPC generates .proto files and code at runtime. If you wish
|
|
|
375
368
|
export PYDANTIC_RPC_SKIP_GENERATION=true
|
|
376
369
|
```
|
|
377
370
|
|
|
378
|
-
When this variable is set to "true", PydanticRPC will load existing pre-generated modules rather than generating
|
|
371
|
+
When this variable is set to "true", PydanticRPC will load existing pre-generated modules rather than generating theƒm on the fly.
|
|
379
372
|
|
|
380
|
-
## 🪧 Setting Protobuf and
|
|
373
|
+
## 🪧 Setting Protobuf and Connect RPC/gRPC generation directory
|
|
381
374
|
By default your files will be generated in the current working directory where you ran the code from, but you can set a custom specific directory by setting the environment variable below:
|
|
382
375
|
|
|
383
376
|
```bash
|
|
@@ -107,7 +107,7 @@ app = ASGIApp(service=OlympicsLocationAgent())
|
|
|
107
107
|
- 🔎 **Server Reflection:** Built-in support for gRPC server reflection.
|
|
108
108
|
- ⚡ **Asynchronous Support:** Easily create asynchronous gRPC services with `AsyncIOServer`.
|
|
109
109
|
- **For Connect-RPC:**
|
|
110
|
-
- 🌐 **Full Protocol Support:** Native Connect-RPC support via `
|
|
110
|
+
- 🌐 **Full Protocol Support:** Native Connect-RPC support via `connect-python`
|
|
111
111
|
- 🔄 **All Streaming Patterns:** Unary, server streaming, client streaming, and bidirectional streaming
|
|
112
112
|
- 🌐 **WSGI/ASGI Applications:** Run as standard WSGI or ASGI applications for easy deployment
|
|
113
113
|
- 🛠️ **Pre-generated Protobuf Files and Code:** Pre-generate proto files and corresponding code via the CLI. By setting the environment variable (PYDANTIC_RPC_SKIP_GENERATION), you can skip runtime generation.
|
|
@@ -286,21 +286,21 @@ app.mount(Greeter())
|
|
|
286
286
|
|
|
287
287
|
### 🏆 Connect-RPC with Streaming Example
|
|
288
288
|
|
|
289
|
-
PydanticRPC provides native Connect-RPC support via
|
|
289
|
+
PydanticRPC provides native Connect-RPC support via connect-python, including full streaming capabilities and PEP 8 naming conventions. Check out our ASGI examples:
|
|
290
290
|
|
|
291
291
|
```bash
|
|
292
292
|
# Run with uvicorn
|
|
293
293
|
uv run uvicorn greeting_asgi:app --port 3000
|
|
294
294
|
|
|
295
295
|
# Or run streaming example
|
|
296
|
-
uv run python examples/
|
|
296
|
+
uv run python examples/streaming_connect_python.py
|
|
297
297
|
```
|
|
298
298
|
|
|
299
|
-
This will launch a
|
|
299
|
+
This will launch a connect-python-based ASGI application that uses the same Pydantic models to serve Connect-RPC requests.
|
|
300
300
|
|
|
301
|
-
#### Streaming Support with
|
|
301
|
+
#### Streaming Support with connect-python
|
|
302
302
|
|
|
303
|
-
|
|
303
|
+
connect-python provides full support for streaming RPCs with automatic PEP 8 naming (snake_case):
|
|
304
304
|
|
|
305
305
|
```python
|
|
306
306
|
from typing import AsyncIterator
|
|
@@ -341,14 +341,7 @@ app.mount(StreamingService())
|
|
|
341
341
|
```
|
|
342
342
|
|
|
343
343
|
> [!NOTE]
|
|
344
|
-
> Please install `protoc-gen-
|
|
345
|
-
>
|
|
346
|
-
> 1. Install Go.
|
|
347
|
-
> - Please follow the instruction described in https://go.dev/doc/install.
|
|
348
|
-
> 2. Install `protoc-gen-connecpy`:
|
|
349
|
-
> ```bash
|
|
350
|
-
> go install github.com/i2y/connecpy/v2/protoc-gen-connecpy@latest
|
|
351
|
-
> ```
|
|
344
|
+
> Please install `protoc-gen-connect-python` to run the connect-python example.
|
|
352
345
|
|
|
353
346
|
## ♻️ Skipping Protobuf Generation
|
|
354
347
|
By default, PydanticRPC generates .proto files and code at runtime. If you wish to skip the code-generation step (for example, in production environment), set the environment variable below:
|
|
@@ -357,9 +350,9 @@ By default, PydanticRPC generates .proto files and code at runtime. If you wish
|
|
|
357
350
|
export PYDANTIC_RPC_SKIP_GENERATION=true
|
|
358
351
|
```
|
|
359
352
|
|
|
360
|
-
When this variable is set to "true", PydanticRPC will load existing pre-generated modules rather than generating
|
|
353
|
+
When this variable is set to "true", PydanticRPC will load existing pre-generated modules rather than generating theƒm on the fly.
|
|
361
354
|
|
|
362
|
-
## 🪧 Setting Protobuf and
|
|
355
|
+
## 🪧 Setting Protobuf and Connect RPC/gRPC generation directory
|
|
363
356
|
By default your files will be generated in the current working directory where you ran the code from, but you can set a custom specific directory by setting the environment variable below:
|
|
364
357
|
|
|
365
358
|
```bash
|
|
@@ -15,6 +15,7 @@ from .decorators import (
|
|
|
15
15
|
has_http_option,
|
|
16
16
|
error_handler,
|
|
17
17
|
get_error_handlers,
|
|
18
|
+
invoke_error_handler,
|
|
18
19
|
)
|
|
19
20
|
from .tls import (
|
|
20
21
|
GrpcTLSConfig,
|
|
@@ -35,6 +36,7 @@ __all__ = [
|
|
|
35
36
|
"has_http_option",
|
|
36
37
|
"error_handler",
|
|
37
38
|
"get_error_handlers",
|
|
39
|
+
"invoke_error_handler",
|
|
38
40
|
"GrpcTLSConfig",
|
|
39
41
|
"extract_peer_identity",
|
|
40
42
|
"extract_peer_certificate_chain",
|
|
@@ -11,9 +11,11 @@ import signal
|
|
|
11
11
|
import sys
|
|
12
12
|
import time
|
|
13
13
|
import types
|
|
14
|
-
from collections.abc import AsyncIterator, Awaitable, Callable, Iterable
|
|
14
|
+
from collections.abc import AsyncIterator, Awaitable, Callable, Iterable, Sequence
|
|
15
15
|
from concurrent import futures
|
|
16
16
|
from connectrpc.code import Code as Errors
|
|
17
|
+
from connectrpc.errors import ConnectError
|
|
18
|
+
|
|
17
19
|
# Protobuf Python modules for Timestamp, Duration (requires protobuf / grpcio)
|
|
18
20
|
from google.protobuf import duration_pb2, timestamp_pb2, empty_pb2
|
|
19
21
|
from grpc import ServicerContext
|
|
@@ -32,12 +34,17 @@ from typing import (
|
|
|
32
34
|
get_origin,
|
|
33
35
|
cast,
|
|
34
36
|
TypeGuard,
|
|
37
|
+
Union,
|
|
38
|
+
Tuple,
|
|
35
39
|
)
|
|
36
|
-
from typing import Union
|
|
37
|
-
from typing import Union, Sequence, Tuple
|
|
38
40
|
from concurrent.futures import Executor
|
|
39
41
|
|
|
40
|
-
from .decorators import
|
|
42
|
+
from .decorators import (
|
|
43
|
+
get_method_options,
|
|
44
|
+
has_http_option,
|
|
45
|
+
get_error_handlers,
|
|
46
|
+
invoke_error_handler,
|
|
47
|
+
)
|
|
41
48
|
from .tls import GrpcTLSConfig
|
|
42
49
|
|
|
43
50
|
###############################################################################
|
|
@@ -309,6 +316,116 @@ def generate_message_converter(
|
|
|
309
316
|
return converter
|
|
310
317
|
|
|
311
318
|
|
|
319
|
+
def handle_validation_error_sync(
|
|
320
|
+
exc: ValidationError,
|
|
321
|
+
method: Callable,
|
|
322
|
+
context: Any,
|
|
323
|
+
request: Any = None,
|
|
324
|
+
is_grpc: bool = True,
|
|
325
|
+
) -> Any:
|
|
326
|
+
"""
|
|
327
|
+
Handle ValidationError with custom error handlers or default behavior (sync version).
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
exc: The ValidationError that was raised
|
|
331
|
+
method: The RPC method being called
|
|
332
|
+
context: The gRPC or Connect context
|
|
333
|
+
request: Optional raw request data
|
|
334
|
+
is_grpc: True for gRPC, False for Connect RPC
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
Result of context.abort() call
|
|
338
|
+
"""
|
|
339
|
+
error_handlers = get_error_handlers(method)
|
|
340
|
+
|
|
341
|
+
if error_handlers:
|
|
342
|
+
# Check if there's a handler for ValidationError
|
|
343
|
+
for handler_config in error_handlers:
|
|
344
|
+
if isinstance(exc, handler_config["exception_type"]):
|
|
345
|
+
if handler_config["handler"]:
|
|
346
|
+
# Custom handler function
|
|
347
|
+
try:
|
|
348
|
+
msg, _details = invoke_error_handler(
|
|
349
|
+
handler_config["handler"], exc, request
|
|
350
|
+
)
|
|
351
|
+
except Exception:
|
|
352
|
+
# Handler failed, fall back to default
|
|
353
|
+
msg = str(exc)
|
|
354
|
+
else:
|
|
355
|
+
# No custom handler, use default message
|
|
356
|
+
msg = str(exc)
|
|
357
|
+
|
|
358
|
+
# Use the configured status code
|
|
359
|
+
if is_grpc:
|
|
360
|
+
status_code = handler_config["status_code"]
|
|
361
|
+
return context.abort(status_code, msg)
|
|
362
|
+
else:
|
|
363
|
+
status_code = handler_config["connect_code"]
|
|
364
|
+
raise ConnectError(code=status_code, message=msg)
|
|
365
|
+
|
|
366
|
+
# No handler found, use default behavior
|
|
367
|
+
if is_grpc:
|
|
368
|
+
return context.abort(grpc.StatusCode.INVALID_ARGUMENT, str(exc))
|
|
369
|
+
else:
|
|
370
|
+
raise ConnectError(code=Errors.INVALID_ARGUMENT, message=str(exc))
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
async def handle_validation_error_async(
|
|
374
|
+
exc: ValidationError,
|
|
375
|
+
method: Callable,
|
|
376
|
+
context: Any,
|
|
377
|
+
request: Any = None,
|
|
378
|
+
is_grpc: bool = True,
|
|
379
|
+
) -> Any:
|
|
380
|
+
"""
|
|
381
|
+
Handle ValidationError with custom error handlers or default behavior (async version).
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
exc: The ValidationError that was raised
|
|
385
|
+
method: The RPC method being called
|
|
386
|
+
context: The gRPC or Connect context
|
|
387
|
+
request: Optional raw request data
|
|
388
|
+
is_grpc: True for gRPC, False for Connect RPC
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
Result of context.abort() call
|
|
392
|
+
"""
|
|
393
|
+
error_handlers = get_error_handlers(method)
|
|
394
|
+
|
|
395
|
+
if error_handlers:
|
|
396
|
+
# Check if there's a handler for ValidationError
|
|
397
|
+
for handler_config in error_handlers:
|
|
398
|
+
if isinstance(exc, handler_config["exception_type"]):
|
|
399
|
+
# Found a matching handler
|
|
400
|
+
if handler_config["handler"]:
|
|
401
|
+
# Custom handler function
|
|
402
|
+
try:
|
|
403
|
+
msg, _details = invoke_error_handler(
|
|
404
|
+
handler_config["handler"], exc, request
|
|
405
|
+
)
|
|
406
|
+
except Exception:
|
|
407
|
+
# Handler failed, fall back to default
|
|
408
|
+
msg = str(exc)
|
|
409
|
+
else:
|
|
410
|
+
# No custom handler, use default message
|
|
411
|
+
msg = str(exc)
|
|
412
|
+
|
|
413
|
+
# Use the configured status code
|
|
414
|
+
if is_grpc:
|
|
415
|
+
status_code = handler_config["status_code"]
|
|
416
|
+
await context.abort(status_code, msg)
|
|
417
|
+
return
|
|
418
|
+
else:
|
|
419
|
+
status_code = handler_config["connect_code"]
|
|
420
|
+
raise ConnectError(code=status_code, message=msg)
|
|
421
|
+
|
|
422
|
+
# No handler found, use default behavior
|
|
423
|
+
if is_grpc:
|
|
424
|
+
await context.abort(grpc.StatusCode.INVALID_ARGUMENT, str(exc))
|
|
425
|
+
else:
|
|
426
|
+
raise ConnectError(code=Errors.INVALID_ARGUMENT, message=str(exc))
|
|
427
|
+
|
|
428
|
+
|
|
312
429
|
def python_value_to_proto_value(field_type: type[Any], value: Any) -> Any:
|
|
313
430
|
"""
|
|
314
431
|
Converts Python values to protobuf values.
|
|
@@ -394,7 +511,9 @@ def connect_obj_with_stub(
|
|
|
394
511
|
resp_obj, response_type, pb2_module
|
|
395
512
|
)
|
|
396
513
|
except ValidationError as e:
|
|
397
|
-
return
|
|
514
|
+
return handle_validation_error_sync(
|
|
515
|
+
e, original, context, request, is_grpc=True
|
|
516
|
+
)
|
|
398
517
|
except Exception as e:
|
|
399
518
|
return context.abort(grpc.StatusCode.INTERNAL, str(e))
|
|
400
519
|
|
|
@@ -431,7 +550,9 @@ def connect_obj_with_stub(
|
|
|
431
550
|
resp_obj, response_type, pb2_module
|
|
432
551
|
)
|
|
433
552
|
except ValidationError as e:
|
|
434
|
-
return
|
|
553
|
+
return handle_validation_error_sync(
|
|
554
|
+
e, original, context, request, is_grpc=True
|
|
555
|
+
)
|
|
435
556
|
except Exception as e:
|
|
436
557
|
return context.abort(grpc.StatusCode.INTERNAL, str(e))
|
|
437
558
|
|
|
@@ -513,8 +634,8 @@ def connect_obj_with_stub_async(
|
|
|
513
634
|
resp_obj, output_item_type, pb2_module
|
|
514
635
|
)
|
|
515
636
|
except ValidationError as e:
|
|
516
|
-
await
|
|
517
|
-
|
|
637
|
+
await handle_validation_error_async(
|
|
638
|
+
e, method, context, None, is_grpc=True
|
|
518
639
|
)
|
|
519
640
|
except Exception as e:
|
|
520
641
|
await context.abort(grpc.StatusCode.INTERNAL, str(e))
|
|
@@ -534,8 +655,8 @@ def connect_obj_with_stub_async(
|
|
|
534
655
|
resp_obj, output_item_type, pb2_module
|
|
535
656
|
)
|
|
536
657
|
except ValidationError as e:
|
|
537
|
-
await
|
|
538
|
-
|
|
658
|
+
await handle_validation_error_async(
|
|
659
|
+
e, method, context, None, is_grpc=True
|
|
539
660
|
)
|
|
540
661
|
except Exception as e:
|
|
541
662
|
await context.abort(grpc.StatusCode.INTERNAL, str(e))
|
|
@@ -559,8 +680,8 @@ def connect_obj_with_stub_async(
|
|
|
559
680
|
resp_obj, response_type, pb2_module
|
|
560
681
|
)
|
|
561
682
|
except ValidationError as e:
|
|
562
|
-
await
|
|
563
|
-
|
|
683
|
+
await handle_validation_error_async(
|
|
684
|
+
e, method, context, None, is_grpc=True
|
|
564
685
|
)
|
|
565
686
|
except Exception as e:
|
|
566
687
|
await context.abort(grpc.StatusCode.INTERNAL, str(e))
|
|
@@ -580,8 +701,8 @@ def connect_obj_with_stub_async(
|
|
|
580
701
|
resp_obj, response_type, pb2_module
|
|
581
702
|
)
|
|
582
703
|
except ValidationError as e:
|
|
583
|
-
await
|
|
584
|
-
|
|
704
|
+
await handle_validation_error_async(
|
|
705
|
+
e, method, context, None, is_grpc=True
|
|
585
706
|
)
|
|
586
707
|
except Exception as e:
|
|
587
708
|
await context.abort(grpc.StatusCode.INTERNAL, str(e))
|
|
@@ -611,8 +732,8 @@ def connect_obj_with_stub_async(
|
|
|
611
732
|
resp_obj, output_item_type, pb2_module
|
|
612
733
|
)
|
|
613
734
|
except ValidationError as e:
|
|
614
|
-
await
|
|
615
|
-
|
|
735
|
+
await handle_validation_error_async(
|
|
736
|
+
e, method, context, request, is_grpc=True
|
|
616
737
|
)
|
|
617
738
|
except Exception as e:
|
|
618
739
|
await context.abort(grpc.StatusCode.INTERNAL, str(e))
|
|
@@ -632,8 +753,8 @@ def connect_obj_with_stub_async(
|
|
|
632
753
|
resp_obj, output_item_type, pb2_module
|
|
633
754
|
)
|
|
634
755
|
except ValidationError as e:
|
|
635
|
-
await
|
|
636
|
-
|
|
756
|
+
await handle_validation_error_async(
|
|
757
|
+
e, method, context, request, is_grpc=True
|
|
637
758
|
)
|
|
638
759
|
except Exception as e:
|
|
639
760
|
await context.abort(grpc.StatusCode.INTERNAL, str(e))
|
|
@@ -674,8 +795,8 @@ def connect_obj_with_stub_async(
|
|
|
674
795
|
resp_obj, response_type, pb2_module
|
|
675
796
|
)
|
|
676
797
|
except ValidationError as e:
|
|
677
|
-
await
|
|
678
|
-
|
|
798
|
+
await handle_validation_error_async(
|
|
799
|
+
e, method, context, request, is_grpc=True
|
|
679
800
|
)
|
|
680
801
|
except Exception as e:
|
|
681
802
|
await context.abort(grpc.StatusCode.INTERNAL, str(e))
|
|
@@ -709,8 +830,8 @@ def connect_obj_with_stub_async(
|
|
|
709
830
|
resp_obj, response_type, pb2_module
|
|
710
831
|
)
|
|
711
832
|
except ValidationError as e:
|
|
712
|
-
await
|
|
713
|
-
|
|
833
|
+
await handle_validation_error_async(
|
|
834
|
+
e, method, context, request, is_grpc=True
|
|
714
835
|
)
|
|
715
836
|
except Exception as e:
|
|
716
837
|
await context.abort(grpc.StatusCode.INTERNAL, str(e))
|
|
@@ -769,9 +890,11 @@ def connect_obj_with_stub_connect_python(
|
|
|
769
890
|
resp_obj, response_type, pb2_module
|
|
770
891
|
)
|
|
771
892
|
except ValidationError as e:
|
|
772
|
-
return
|
|
893
|
+
return handle_validation_error_sync(
|
|
894
|
+
e, method, context, request, is_grpc=False
|
|
895
|
+
)
|
|
773
896
|
except Exception as e:
|
|
774
|
-
|
|
897
|
+
raise ConnectError(code=Errors.INTERNAL, message=str(e))
|
|
775
898
|
|
|
776
899
|
return stub_method0
|
|
777
900
|
|
|
@@ -798,9 +921,11 @@ def connect_obj_with_stub_connect_python(
|
|
|
798
921
|
resp_obj, response_type, pb2_module
|
|
799
922
|
)
|
|
800
923
|
except ValidationError as e:
|
|
801
|
-
return
|
|
924
|
+
return handle_validation_error_sync(
|
|
925
|
+
e, method, context, request, is_grpc=False
|
|
926
|
+
)
|
|
802
927
|
except Exception as e:
|
|
803
|
-
|
|
928
|
+
raise ConnectError(code=Errors.INTERNAL, message=str(e))
|
|
804
929
|
|
|
805
930
|
return stub_method1
|
|
806
931
|
|
|
@@ -827,9 +952,11 @@ def connect_obj_with_stub_connect_python(
|
|
|
827
952
|
resp_obj, response_type, pb2_module
|
|
828
953
|
)
|
|
829
954
|
except ValidationError as e:
|
|
830
|
-
return
|
|
955
|
+
return handle_validation_error_sync(
|
|
956
|
+
e, method, context, request, is_grpc=False
|
|
957
|
+
)
|
|
831
958
|
except Exception as e:
|
|
832
|
-
|
|
959
|
+
raise ConnectError(code=Errors.INTERNAL, message=str(e))
|
|
833
960
|
|
|
834
961
|
return stub_method2
|
|
835
962
|
|
|
@@ -889,9 +1016,11 @@ def connect_obj_with_stub_async_connect_python(
|
|
|
889
1016
|
resp_obj, response_type, pb2_module
|
|
890
1017
|
)
|
|
891
1018
|
except ValidationError as e:
|
|
892
|
-
await
|
|
1019
|
+
await handle_validation_error_async(
|
|
1020
|
+
e, method, context, request, is_grpc=False
|
|
1021
|
+
)
|
|
893
1022
|
except Exception as e:
|
|
894
|
-
|
|
1023
|
+
raise ConnectError(code=Errors.INTERNAL, message=str(e))
|
|
895
1024
|
|
|
896
1025
|
return stub_method0
|
|
897
1026
|
|
|
@@ -931,9 +1060,11 @@ def connect_obj_with_stub_async_connect_python(
|
|
|
931
1060
|
resp_obj, output_item_type, pb2_module
|
|
932
1061
|
)
|
|
933
1062
|
except ValidationError as e:
|
|
934
|
-
await
|
|
1063
|
+
await handle_validation_error_async(
|
|
1064
|
+
e, method, context, None, is_grpc=False
|
|
1065
|
+
)
|
|
935
1066
|
except Exception as e:
|
|
936
|
-
|
|
1067
|
+
raise ConnectError(code=Errors.INTERNAL, message=str(e))
|
|
937
1068
|
else: # size_of_parameters == 2
|
|
938
1069
|
|
|
939
1070
|
async def stub_method(
|
|
@@ -949,9 +1080,11 @@ def connect_obj_with_stub_async_connect_python(
|
|
|
949
1080
|
resp_obj, output_item_type, pb2_module
|
|
950
1081
|
)
|
|
951
1082
|
except ValidationError as e:
|
|
952
|
-
await
|
|
1083
|
+
await handle_validation_error_async(
|
|
1084
|
+
e, method, context, None, is_grpc=False
|
|
1085
|
+
)
|
|
953
1086
|
except Exception as e:
|
|
954
|
-
|
|
1087
|
+
raise ConnectError(code=Errors.INTERNAL, message=str(e))
|
|
955
1088
|
|
|
956
1089
|
return stub_method
|
|
957
1090
|
else:
|
|
@@ -973,9 +1106,11 @@ def connect_obj_with_stub_async_connect_python(
|
|
|
973
1106
|
resp_obj, response_type, pb2_module
|
|
974
1107
|
)
|
|
975
1108
|
except ValidationError as e:
|
|
976
|
-
await
|
|
1109
|
+
await handle_validation_error_async(
|
|
1110
|
+
e, method, context, None, is_grpc=False
|
|
1111
|
+
)
|
|
977
1112
|
except Exception as e:
|
|
978
|
-
|
|
1113
|
+
raise ConnectError(code=Errors.INTERNAL, message=str(e))
|
|
979
1114
|
else: # size_of_parameters == 2
|
|
980
1115
|
|
|
981
1116
|
async def stub_method(
|
|
@@ -993,9 +1128,11 @@ def connect_obj_with_stub_async_connect_python(
|
|
|
993
1128
|
resp_obj, response_type, pb2_module
|
|
994
1129
|
)
|
|
995
1130
|
except ValidationError as e:
|
|
996
|
-
await
|
|
1131
|
+
await handle_validation_error_async(
|
|
1132
|
+
e, method, context, None, is_grpc=False
|
|
1133
|
+
)
|
|
997
1134
|
except Exception as e:
|
|
998
|
-
|
|
1135
|
+
raise ConnectError(code=Errors.INTERNAL, message=str(e))
|
|
999
1136
|
|
|
1000
1137
|
return stub_method
|
|
1001
1138
|
else:
|
|
@@ -1024,9 +1161,11 @@ def connect_obj_with_stub_async_connect_python(
|
|
|
1024
1161
|
resp_obj, output_item_type, pb2_module
|
|
1025
1162
|
)
|
|
1026
1163
|
except ValidationError as e:
|
|
1027
|
-
await
|
|
1164
|
+
await handle_validation_error_async(
|
|
1165
|
+
e, method, context, request, is_grpc=False
|
|
1166
|
+
)
|
|
1028
1167
|
except Exception as e:
|
|
1029
|
-
|
|
1168
|
+
raise ConnectError(code=Errors.INTERNAL, message=str(e))
|
|
1030
1169
|
else: # size_of_parameters == 2
|
|
1031
1170
|
|
|
1032
1171
|
async def stub_method(
|
|
@@ -1045,9 +1184,11 @@ def connect_obj_with_stub_async_connect_python(
|
|
|
1045
1184
|
resp_obj, output_item_type, pb2_module
|
|
1046
1185
|
)
|
|
1047
1186
|
except ValidationError as e:
|
|
1048
|
-
await
|
|
1187
|
+
await handle_validation_error_async(
|
|
1188
|
+
e, method, context, request, is_grpc=False
|
|
1189
|
+
)
|
|
1049
1190
|
except Exception as e:
|
|
1050
|
-
|
|
1191
|
+
raise ConnectError(code=Errors.INTERNAL, message=str(e))
|
|
1051
1192
|
|
|
1052
1193
|
return stub_method
|
|
1053
1194
|
else:
|
|
@@ -1074,9 +1215,11 @@ def connect_obj_with_stub_async_connect_python(
|
|
|
1074
1215
|
resp_obj, response_type, pb2_module
|
|
1075
1216
|
)
|
|
1076
1217
|
except ValidationError as e:
|
|
1077
|
-
await
|
|
1218
|
+
await handle_validation_error_async(
|
|
1219
|
+
e, method, context, request, is_grpc=False
|
|
1220
|
+
)
|
|
1078
1221
|
except Exception as e:
|
|
1079
|
-
|
|
1222
|
+
raise ConnectError(code=Errors.INTERNAL, message=str(e))
|
|
1080
1223
|
else: # size_of_parameters == 2
|
|
1081
1224
|
|
|
1082
1225
|
async def stub_method(
|
|
@@ -1099,9 +1242,11 @@ def connect_obj_with_stub_async_connect_python(
|
|
|
1099
1242
|
resp_obj, response_type, pb2_module
|
|
1100
1243
|
)
|
|
1101
1244
|
except ValidationError as e:
|
|
1102
|
-
await
|
|
1245
|
+
await handle_validation_error_async(
|
|
1246
|
+
e, method, context, request, is_grpc=False
|
|
1247
|
+
)
|
|
1103
1248
|
except Exception as e:
|
|
1104
|
-
|
|
1249
|
+
raise ConnectError(code=Errors.INTERNAL, message=str(e))
|
|
1105
1250
|
|
|
1106
1251
|
return stub_method
|
|
1107
1252
|
|
|
@@ -2611,11 +2756,20 @@ class Server:
|
|
|
2611
2756
|
port: int = 50051,
|
|
2612
2757
|
package_name: str = "",
|
|
2613
2758
|
max_workers: int = 8,
|
|
2614
|
-
*interceptors: Any,
|
|
2615
2759
|
tls: Optional["GrpcTLSConfig"] = None,
|
|
2760
|
+
interceptors: Optional[Sequence[grpc.ServerInterceptor]] = None,
|
|
2761
|
+
handlers: Optional[Sequence[grpc.GenericRpcHandler]] = None,
|
|
2762
|
+
options: Optional[Sequence[Tuple[str, Any]]] = None,
|
|
2763
|
+
maximum_concurrent_rpcs: Optional[int] = None,
|
|
2764
|
+
compression: Optional[grpc.Compression] = None,
|
|
2616
2765
|
) -> None:
|
|
2617
2766
|
self._server: grpc.Server = grpc.server(
|
|
2618
|
-
futures.ThreadPoolExecutor(max_workers),
|
|
2767
|
+
futures.ThreadPoolExecutor(max_workers),
|
|
2768
|
+
handlers=handlers,
|
|
2769
|
+
interceptors=interceptors,
|
|
2770
|
+
options=options,
|
|
2771
|
+
maximum_concurrent_rpcs=maximum_concurrent_rpcs,
|
|
2772
|
+
compression=compression,
|
|
2619
2773
|
)
|
|
2620
2774
|
self._service_names: list[str] = []
|
|
2621
2775
|
self._package_name: str = package_name
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Any, Callable, Dict, List, Optional, TypeVar, Type
|
|
4
4
|
from functools import wraps
|
|
5
|
+
import inspect
|
|
5
6
|
import grpc
|
|
6
7
|
from connectrpc.code import Code as ConnectErrors
|
|
7
8
|
|
|
@@ -145,7 +146,10 @@ def error_handler(
|
|
|
145
146
|
exception_type: Type[Exception],
|
|
146
147
|
status_code: Optional[grpc.StatusCode] = None,
|
|
147
148
|
connect_code: Optional[ConnectErrors] = None,
|
|
148
|
-
handler: Optional[
|
|
149
|
+
handler: Optional[
|
|
150
|
+
Callable[[Exception], tuple[str, Any]]
|
|
151
|
+
| Callable[[Exception, Any], tuple[str, Any]]
|
|
152
|
+
] = None,
|
|
149
153
|
) -> Callable[[F], F]:
|
|
150
154
|
"""
|
|
151
155
|
Decorator to add automatic error handling to an RPC method.
|
|
@@ -154,13 +158,22 @@ def error_handler(
|
|
|
154
158
|
exception_type: The type of exception to handle
|
|
155
159
|
status_code: The gRPC status code to return (for gRPC services)
|
|
156
160
|
connect_code: The Connect error code to return (for Connect services)
|
|
157
|
-
handler: Optional custom handler function that returns (message, details)
|
|
161
|
+
handler: Optional custom handler function that returns (message, details).
|
|
162
|
+
Can accept either (exception) or (exception, request_data) as parameters.
|
|
158
163
|
|
|
159
164
|
Example:
|
|
160
165
|
@error_handler(ValidationError, status_code=grpc.StatusCode.INVALID_ARGUMENT)
|
|
161
166
|
@error_handler(KeyError, status_code=grpc.StatusCode.NOT_FOUND)
|
|
162
167
|
async def get_user(self, request: GetUserRequest) -> User:
|
|
163
168
|
...
|
|
169
|
+
|
|
170
|
+
# With custom handler that accesses request data
|
|
171
|
+
def validation_handler(exc: ValidationError, request_data: Any) -> tuple[str, dict]:
|
|
172
|
+
return f"Validation failed for {request_data}", {"errors": exc.errors()}
|
|
173
|
+
|
|
174
|
+
@error_handler(ValidationError, handler=validation_handler)
|
|
175
|
+
async def create_user(self, request: CreateUserRequest) -> User:
|
|
176
|
+
...
|
|
164
177
|
"""
|
|
165
178
|
|
|
166
179
|
def decorator(func: F) -> F:
|
|
@@ -180,18 +193,11 @@ def error_handler(
|
|
|
180
193
|
}
|
|
181
194
|
)
|
|
182
195
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
# Preserve the error handlers on the wrapper
|
|
188
|
-
setattr(wrapper, ERROR_HANDLER_ATTR, handlers)
|
|
196
|
+
# Set the error handlers directly on the function
|
|
197
|
+
# No need for a wrapper - we're just storing metadata
|
|
198
|
+
setattr(func, ERROR_HANDLER_ATTR, handlers)
|
|
189
199
|
|
|
190
|
-
#
|
|
191
|
-
if hasattr(func, OPTION_METADATA_ATTR):
|
|
192
|
-
setattr(wrapper, OPTION_METADATA_ATTR, getattr(func, OPTION_METADATA_ATTR))
|
|
193
|
-
|
|
194
|
-
return wrapper # type: ignore
|
|
200
|
+
return func # type: ignore
|
|
195
201
|
|
|
196
202
|
return decorator
|
|
197
203
|
|
|
@@ -207,3 +213,31 @@ def get_error_handlers(method: Callable) -> Optional[List[Dict[str, Any]]]:
|
|
|
207
213
|
List of error handler configurations if present, None otherwise
|
|
208
214
|
"""
|
|
209
215
|
return getattr(method, ERROR_HANDLER_ATTR, None)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def invoke_error_handler(
|
|
219
|
+
handler_func: Callable, exception: Exception, request_data: Any = None
|
|
220
|
+
) -> tuple[str, Any]:
|
|
221
|
+
"""
|
|
222
|
+
Invoke an error handler function with appropriate parameters based on its signature.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
handler_func: The error handler function to invoke
|
|
226
|
+
exception: The exception that was raised
|
|
227
|
+
request_data: Optional request data (raw protobuf request)
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Tuple of (error_message, error_details)
|
|
231
|
+
"""
|
|
232
|
+
sig = inspect.signature(handler_func)
|
|
233
|
+
param_count = len(sig.parameters)
|
|
234
|
+
|
|
235
|
+
if param_count == 1:
|
|
236
|
+
# Handler only accepts exception
|
|
237
|
+
return handler_func(exception)
|
|
238
|
+
elif param_count == 2:
|
|
239
|
+
# Handler accepts exception and request_data
|
|
240
|
+
return handler_func(exception, request_data)
|
|
241
|
+
else:
|
|
242
|
+
# Invalid signature, fall back to exception only
|
|
243
|
+
return handler_func(exception)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|