fastgrpc3 0.1.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.
- fastgrpc3-0.1.0/LICENSE +21 -0
- fastgrpc3-0.1.0/PKG-INFO +214 -0
- fastgrpc3-0.1.0/README.md +187 -0
- fastgrpc3-0.1.0/pyproject.toml +93 -0
- fastgrpc3-0.1.0/pyproject.toml.orig +91 -0
- fastgrpc3-0.1.0/src/fastgrpc3/__init__.py +75 -0
- fastgrpc3-0.1.0/src/fastgrpc3/__main__.py +7 -0
- fastgrpc3-0.1.0/src/fastgrpc3/_proto/__init__.py +1 -0
- fastgrpc3-0.1.0/src/fastgrpc3/_proto/_descriptor.py +130 -0
- fastgrpc3-0.1.0/src/fastgrpc3/_proto/google/__init__.py +1 -0
- fastgrpc3-0.1.0/src/fastgrpc3/_proto/google/rpc/__init__.py +1 -0
- fastgrpc3-0.1.0/src/fastgrpc3/_proto/google/rpc/code_models.py +155 -0
- fastgrpc3-0.1.0/src/fastgrpc3/_proto/google/rpc/error_details_models.py +332 -0
- fastgrpc3-0.1.0/src/fastgrpc3/_proto/google/rpc/status_models.py +33 -0
- fastgrpc3-0.1.0/src/fastgrpc3/_proto/grpc/__init__.py +1 -0
- fastgrpc3-0.1.0/src/fastgrpc3/_proto/grpc/health/__init__.py +1 -0
- fastgrpc3-0.1.0/src/fastgrpc3/_proto/grpc/health/v1/__init__.py +1 -0
- fastgrpc3-0.1.0/src/fastgrpc3/_proto/grpc/health/v1/health_models.py +45 -0
- fastgrpc3-0.1.0/src/fastgrpc3/_proto/grpc/health/v1/health_service.py +62 -0
- fastgrpc3-0.1.0/src/fastgrpc3/_proto/grpc/reflection/__init__.py +1 -0
- fastgrpc3-0.1.0/src/fastgrpc3/_proto/grpc/reflection/v1/__init__.py +1 -0
- fastgrpc3-0.1.0/src/fastgrpc3/_proto/grpc/reflection/v1/reflection_models.py +145 -0
- fastgrpc3-0.1.0/src/fastgrpc3/_proto/grpc/reflection/v1/reflection_service.py +37 -0
- fastgrpc3-0.1.0/src/fastgrpc3/_proto/grpc/reflection/v1alpha/__init__.py +1 -0
- fastgrpc3-0.1.0/src/fastgrpc3/_proto/grpc/reflection/v1alpha/reflection_models.py +147 -0
- fastgrpc3-0.1.0/src/fastgrpc3/_proto/grpc/reflection/v1alpha/reflection_service.py +37 -0
- fastgrpc3-0.1.0/src/fastgrpc3/api/__init__.py +0 -0
- fastgrpc3-0.1.0/src/fastgrpc3/api/app.py +452 -0
- fastgrpc3-0.1.0/src/fastgrpc3/api/client.py +360 -0
- fastgrpc3-0.1.0/src/fastgrpc3/api/config.py +110 -0
- fastgrpc3-0.1.0/src/fastgrpc3/api/context.py +63 -0
- fastgrpc3-0.1.0/src/fastgrpc3/api/deps.py +314 -0
- fastgrpc3-0.1.0/src/fastgrpc3/api/errors.py +144 -0
- fastgrpc3-0.1.0/src/fastgrpc3/api/metadata.py +78 -0
- fastgrpc3-0.1.0/src/fastgrpc3/api/security.py +107 -0
- fastgrpc3-0.1.0/src/fastgrpc3/api/service.py +200 -0
- fastgrpc3-0.1.0/src/fastgrpc3/cli.py +299 -0
- fastgrpc3-0.1.0/src/fastgrpc3/codegen/__init__.py +5 -0
- fastgrpc3-0.1.0/src/fastgrpc3/codegen/generate.py +144 -0
- fastgrpc3-0.1.0/src/fastgrpc3/codegen/models.py +386 -0
- fastgrpc3-0.1.0/src/fastgrpc3/codegen/naming.py +97 -0
- fastgrpc3-0.1.0/src/fastgrpc3/codegen/source.py +165 -0
- fastgrpc3-0.1.0/src/fastgrpc3/ops/__init__.py +0 -0
- fastgrpc3-0.1.0/src/fastgrpc3/ops/docs.py +348 -0
- fastgrpc3-0.1.0/src/fastgrpc3/ops/health.py +88 -0
- fastgrpc3-0.1.0/src/fastgrpc3/ops/openapi.py +198 -0
- fastgrpc3-0.1.0/src/fastgrpc3/ops/reflection.py +90 -0
- fastgrpc3-0.1.0/src/fastgrpc3/py.typed +0 -0
- fastgrpc3-0.1.0/src/fastgrpc3/runtime/__init__.py +0 -0
- fastgrpc3-0.1.0/src/fastgrpc3/runtime/deadline.py +31 -0
- fastgrpc3-0.1.0/src/fastgrpc3/runtime/dispatch.py +261 -0
- fastgrpc3-0.1.0/src/fastgrpc3/runtime/workers.py +92 -0
- fastgrpc3-0.1.0/src/fastgrpc3/schema/__init__.py +20 -0
- fastgrpc3-0.1.0/src/fastgrpc3/schema/annotations.py +248 -0
- fastgrpc3-0.1.0/src/fastgrpc3/schema/breaking.py +184 -0
- fastgrpc3-0.1.0/src/fastgrpc3/schema/codec.py +242 -0
- fastgrpc3-0.1.0/src/fastgrpc3/schema/lock.py +224 -0
- fastgrpc3-0.1.0/src/fastgrpc3/schema/registry.py +393 -0
- fastgrpc3-0.1.0/src/fastgrpc3/schema/render.py +286 -0
- fastgrpc3-0.1.0/src/fastgrpc3/schema/schema.py +70 -0
- fastgrpc3-0.1.0/src/fastgrpc3/schema/verify.py +93 -0
- fastgrpc3-0.1.0/src/fastgrpc3/testing.py +52 -0
- fastgrpc3-0.1.0/src/fastgrpc3/transport/__init__.py +50 -0
- fastgrpc3-0.1.0/src/fastgrpc3/transport/base.py +68 -0
- fastgrpc3-0.1.0/src/fastgrpc3/transport/grpc_aio.py +86 -0
- fastgrpc3-0.1.0/src/fastgrpc3/transport/tonic.py +291 -0
- fastgrpc3-0.1.0/src/fastgrpc3/types.py +294 -0
fastgrpc3-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 William Nauroy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
fastgrpc3-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastgrpc3
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: FastAPI-like gRPC framework for Python: contracts from .proto files or Pydantic models, powered by protoruf (Rust)
|
|
5
|
+
Author: William Nauroy
|
|
6
|
+
Author-email: William Nauroy <william.nauroy@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 1 - Planning
|
|
10
|
+
Classifier: Framework :: AsyncIO
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Topic :: System :: Networking
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Dist: grpcio>=1.75.1
|
|
19
|
+
Requires-Dist: protoruf>=0.5.3
|
|
20
|
+
Requires-Dist: pydantic>=2.11
|
|
21
|
+
Requires-Dist: uvicorn>=0.30 ; extra == 'docs'
|
|
22
|
+
Requires-Dist: fastgrpc3-transport>=0.1,<0.2 ; extra == 'tonic'
|
|
23
|
+
Requires-Python: >=3.12
|
|
24
|
+
Provides-Extra: docs
|
|
25
|
+
Provides-Extra: tonic
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# fastgrpc3
|
|
29
|
+
|
|
30
|
+
A FastAPI-like gRPC framework for Python: async handlers, Pydantic models, dependency
|
|
31
|
+
injection and typed clients, on top of `grpc.aio`.
|
|
32
|
+
|
|
33
|
+
> **Status: early development.** Contracts, server, client, health checking, reflection and
|
|
34
|
+
> multi-process serving work; documentation is still to come. The API may change.
|
|
35
|
+
|
|
36
|
+
On a 20-field message, the codec is 4× faster than fastgrpc2's and the framework costs ~7 %
|
|
37
|
+
over plain `grpc.aio` with generated stubs, validation included (`benches/results/`).
|
|
38
|
+
|
|
39
|
+
- **Two ways to declare a contract.** Start from `.proto` files and generate the Pydantic
|
|
40
|
+
models, services and clients, or write Pydantic models and get the `.proto` generated.
|
|
41
|
+
- **Fast protobuf codec.** Encoding and decoding run in Rust through
|
|
42
|
+
[protoruf](https://github.com/EdwinAlkins/protoruf), straight between protobuf bytes and
|
|
43
|
+
Python objects, without any JSON step.
|
|
44
|
+
- **Standard gRPC.** Real protobuf on the wire, standard error details, health checking and
|
|
45
|
+
server reflection, so clients in any language can talk to it.
|
|
46
|
+
- **Two transports.** `grpc.aio` by default, or tonic, a Rust server 6 to 10× faster per
|
|
47
|
+
core under concurrency (`--transport tonic`, extra `fastgrpc3[tonic]`, experimental).
|
|
48
|
+
- **Interactive docs.** Swagger UI generated from the models, with a JSON gateway to try the
|
|
49
|
+
RPCs from the browser (`app.enable_docs()`, extra `fastgrpc3[docs]`).
|
|
50
|
+
|
|
51
|
+
Requires Python 3.12 or later (tested on 3.12, 3.13 and 3.14).
|
|
52
|
+
|
|
53
|
+
A complete example, a bookstore service with its client and tests, is in
|
|
54
|
+
[`examples/`](examples/README.md).
|
|
55
|
+
|
|
56
|
+
## Server and client
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
import asyncio
|
|
60
|
+
from collections.abc import AsyncIterator
|
|
61
|
+
|
|
62
|
+
from pydantic import BaseModel
|
|
63
|
+
from fastgrpc3 import Context, Depends, GrpcApp, GrpcError, GrpcService, Insecure, StatusCode
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class GetOrder(BaseModel):
|
|
67
|
+
order_number: str
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Order(BaseModel):
|
|
71
|
+
order_number: str
|
|
72
|
+
paid: bool = False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
service = GrpcService("Orders", package="orders.v1")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_repo() -> dict[str, Order]:
|
|
79
|
+
return {"A": Order(order_number="A", paid=True)}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@service.unary()
|
|
83
|
+
async def Get(req: GetOrder, ctx: Context, repo: dict[str, Order] = Depends(get_repo)) -> Order:
|
|
84
|
+
if req.order_number not in repo:
|
|
85
|
+
raise GrpcError(StatusCode.NOT_FOUND, "no such order")
|
|
86
|
+
return repo[req.order_number]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@service.unary_stream()
|
|
90
|
+
async def Watch(req: GetOrder) -> AsyncIterator[Order]:
|
|
91
|
+
yield Order(order_number=req.order_number)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
app = GrpcApp(address="[::]:50051", tls=Insecure()) # or TLS(cert=..., key=...)
|
|
95
|
+
app.add_service(service)
|
|
96
|
+
app.enable_health() # grpc.health.v1: Check, Watch, List (grpc-health-probe, Kubernetes)
|
|
97
|
+
app.enable_reflection() # grpc.reflection v1 and v1alpha (grpcurl list / describe / call)
|
|
98
|
+
app.enable_docs(gateway=True) # Swagger UI on 127.0.0.1:8080/docs (fastgrpc3[docs])
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
asyncio.run(app.run()) # stops cleanly on SIGINT / SIGTERM
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from fastgrpc3 import Channel, GrpcClient, Insecure
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class OrdersClient(GrpcClient):
|
|
109
|
+
package = "orders.v1"
|
|
110
|
+
service = "Orders"
|
|
111
|
+
get = GrpcClient.unary("Get", GetOrder, Order)
|
|
112
|
+
watch = GrpcClient.unary_stream("Watch", GetOrder, Order)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
async def main() -> None:
|
|
116
|
+
async with Channel("localhost:50051", tls=Insecure()) as channel:
|
|
117
|
+
orders = OrdersClient(channel)
|
|
118
|
+
order = await orders.get(GetOrder(order_number="A"))
|
|
119
|
+
async for update in orders.watch(GetOrder(order_number="A")):
|
|
120
|
+
print(update)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Also: dependencies with sub-dependencies, per-call cache and `yield` cleanup;
|
|
124
|
+
`app.add_interceptor` (pre-call hooks), `@app.middleware` (around the handler) and
|
|
125
|
+
`@app.exception_handler`; rich errors with the standard `google.rpc` details (`ErrorInfo`,
|
|
126
|
+
`BadRequest`…); deadline propagation to calls made from a handler; `TLS` / `MTLS` /
|
|
127
|
+
`Insecure` always explicit; `fastgrpc3.testing.TestClient` for tests.
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
fastgrpc3 run myapp.main:app --address [::]:50051 --workers 4 # one process per core
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Workers share the port with `SO_REUSEPORT` (Linux); otherwise a busy port makes the server
|
|
134
|
+
fail to start instead of silently sharing it.
|
|
135
|
+
|
|
136
|
+
## Code first: Pydantic models -> `.proto`
|
|
137
|
+
|
|
138
|
+
`fastgrpc3 lock`, `check` and `export` take an app (`myapp.main:app`) or a `SchemaRegistry`.
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from pydantic import BaseModel
|
|
142
|
+
from fastgrpc3 import MethodDef, SchemaRegistry, ServiceDef, field
|
|
143
|
+
from fastgrpc3.types import UInt32
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class GetOrder(BaseModel):
|
|
147
|
+
order_number: str
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class Order(BaseModel):
|
|
151
|
+
order_number: str
|
|
152
|
+
qty: UInt32 = 0 # proto uint32, bounds checked by Pydantic
|
|
153
|
+
note: str | None = field(5, default=None) # explicit number
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
registry = SchemaRegistry()
|
|
157
|
+
registry.add_service(ServiceDef("Orders", "orders.v1", (MethodDef("Get", GetOrder, Order),)))
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
fastgrpc3 lock myapp.contract:registry # number new fields, write proto.lock (commit it)
|
|
162
|
+
fastgrpc3 check myapp.contract:registry # CI: fail if proto.lock is out of date
|
|
163
|
+
fastgrpc3 export myapp.contract:registry -o proto/ # one .proto file per package
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Fields without an explicit number get one the first time they are locked, wherever they are
|
|
167
|
+
declared; a removed field's number is reserved for good. Types without a proto equivalent
|
|
168
|
+
(`Decimal`, `UUID`, unions…) travel as a JSON string, marked `// json` in the `.proto`.
|
|
169
|
+
|
|
170
|
+
## Proto first: `.proto` -> Pydantic models
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
fastgrpc3 generate proto/ -o src/myapp/gen # -I third_party/ for extra imports
|
|
174
|
+
fastgrpc3 generate proto/ -o src/myapp/gen --check # CI: fail if not up to date
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`orders/v1/orders.proto` gives `orders/v1/orders_models.py` (one `ProtoModel` per message,
|
|
178
|
+
one open `ProtoEnum` per enum) and `orders/v1/orders_service.py`; `_descriptor.py` embeds the
|
|
179
|
+
compiled descriptor, so nothing is compiled at startup. Defaults can live in `pyproject.toml`:
|
|
180
|
+
|
|
181
|
+
```toml
|
|
182
|
+
[tool.fastgrpc3.generate]
|
|
183
|
+
proto_paths = ["proto"]
|
|
184
|
+
output = "src/myapp/gen"
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Each service module also holds the contract (`Orders`, to serve with
|
|
188
|
+
`GrpcService.implement(Orders)`) and a typed client (`OrdersClient`). Generated code passes
|
|
189
|
+
`mypy --strict` and `ruff`. Well-known types become Python types
|
|
190
|
+
(`Timestamp` -> `datetime`, `Struct` -> `dict[str, Any]`, wrappers -> `X | None`…).
|
|
191
|
+
|
|
192
|
+
## Documentation
|
|
193
|
+
|
|
194
|
+
The guide and the reference live in [`docs/`](docs/index.md) (MkDocs). To build the site
|
|
195
|
+
into `site/`, or to serve it locally with live reload:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
uv run --project docs mkdocs build --strict
|
|
199
|
+
uv run --project docs mkdocs serve # http://127.0.0.1:8000
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Development
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
uv sync # install the package and the dev tools
|
|
206
|
+
uv run pytest # tests
|
|
207
|
+
uv run ruff check . # lint
|
|
208
|
+
uv run ruff format --check # formatting
|
|
209
|
+
uv run mypy src tests # type checking
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## License
|
|
213
|
+
|
|
214
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# fastgrpc3
|
|
2
|
+
|
|
3
|
+
A FastAPI-like gRPC framework for Python: async handlers, Pydantic models, dependency
|
|
4
|
+
injection and typed clients, on top of `grpc.aio`.
|
|
5
|
+
|
|
6
|
+
> **Status: early development.** Contracts, server, client, health checking, reflection and
|
|
7
|
+
> multi-process serving work; documentation is still to come. The API may change.
|
|
8
|
+
|
|
9
|
+
On a 20-field message, the codec is 4× faster than fastgrpc2's and the framework costs ~7 %
|
|
10
|
+
over plain `grpc.aio` with generated stubs, validation included (`benches/results/`).
|
|
11
|
+
|
|
12
|
+
- **Two ways to declare a contract.** Start from `.proto` files and generate the Pydantic
|
|
13
|
+
models, services and clients, or write Pydantic models and get the `.proto` generated.
|
|
14
|
+
- **Fast protobuf codec.** Encoding and decoding run in Rust through
|
|
15
|
+
[protoruf](https://github.com/EdwinAlkins/protoruf), straight between protobuf bytes and
|
|
16
|
+
Python objects, without any JSON step.
|
|
17
|
+
- **Standard gRPC.** Real protobuf on the wire, standard error details, health checking and
|
|
18
|
+
server reflection, so clients in any language can talk to it.
|
|
19
|
+
- **Two transports.** `grpc.aio` by default, or tonic, a Rust server 6 to 10× faster per
|
|
20
|
+
core under concurrency (`--transport tonic`, extra `fastgrpc3[tonic]`, experimental).
|
|
21
|
+
- **Interactive docs.** Swagger UI generated from the models, with a JSON gateway to try the
|
|
22
|
+
RPCs from the browser (`app.enable_docs()`, extra `fastgrpc3[docs]`).
|
|
23
|
+
|
|
24
|
+
Requires Python 3.12 or later (tested on 3.12, 3.13 and 3.14).
|
|
25
|
+
|
|
26
|
+
A complete example, a bookstore service with its client and tests, is in
|
|
27
|
+
[`examples/`](examples/README.md).
|
|
28
|
+
|
|
29
|
+
## Server and client
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import asyncio
|
|
33
|
+
from collections.abc import AsyncIterator
|
|
34
|
+
|
|
35
|
+
from pydantic import BaseModel
|
|
36
|
+
from fastgrpc3 import Context, Depends, GrpcApp, GrpcError, GrpcService, Insecure, StatusCode
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class GetOrder(BaseModel):
|
|
40
|
+
order_number: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Order(BaseModel):
|
|
44
|
+
order_number: str
|
|
45
|
+
paid: bool = False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
service = GrpcService("Orders", package="orders.v1")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_repo() -> dict[str, Order]:
|
|
52
|
+
return {"A": Order(order_number="A", paid=True)}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@service.unary()
|
|
56
|
+
async def Get(req: GetOrder, ctx: Context, repo: dict[str, Order] = Depends(get_repo)) -> Order:
|
|
57
|
+
if req.order_number not in repo:
|
|
58
|
+
raise GrpcError(StatusCode.NOT_FOUND, "no such order")
|
|
59
|
+
return repo[req.order_number]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@service.unary_stream()
|
|
63
|
+
async def Watch(req: GetOrder) -> AsyncIterator[Order]:
|
|
64
|
+
yield Order(order_number=req.order_number)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
app = GrpcApp(address="[::]:50051", tls=Insecure()) # or TLS(cert=..., key=...)
|
|
68
|
+
app.add_service(service)
|
|
69
|
+
app.enable_health() # grpc.health.v1: Check, Watch, List (grpc-health-probe, Kubernetes)
|
|
70
|
+
app.enable_reflection() # grpc.reflection v1 and v1alpha (grpcurl list / describe / call)
|
|
71
|
+
app.enable_docs(gateway=True) # Swagger UI on 127.0.0.1:8080/docs (fastgrpc3[docs])
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
asyncio.run(app.run()) # stops cleanly on SIGINT / SIGTERM
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from fastgrpc3 import Channel, GrpcClient, Insecure
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class OrdersClient(GrpcClient):
|
|
82
|
+
package = "orders.v1"
|
|
83
|
+
service = "Orders"
|
|
84
|
+
get = GrpcClient.unary("Get", GetOrder, Order)
|
|
85
|
+
watch = GrpcClient.unary_stream("Watch", GetOrder, Order)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def main() -> None:
|
|
89
|
+
async with Channel("localhost:50051", tls=Insecure()) as channel:
|
|
90
|
+
orders = OrdersClient(channel)
|
|
91
|
+
order = await orders.get(GetOrder(order_number="A"))
|
|
92
|
+
async for update in orders.watch(GetOrder(order_number="A")):
|
|
93
|
+
print(update)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Also: dependencies with sub-dependencies, per-call cache and `yield` cleanup;
|
|
97
|
+
`app.add_interceptor` (pre-call hooks), `@app.middleware` (around the handler) and
|
|
98
|
+
`@app.exception_handler`; rich errors with the standard `google.rpc` details (`ErrorInfo`,
|
|
99
|
+
`BadRequest`…); deadline propagation to calls made from a handler; `TLS` / `MTLS` /
|
|
100
|
+
`Insecure` always explicit; `fastgrpc3.testing.TestClient` for tests.
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
fastgrpc3 run myapp.main:app --address [::]:50051 --workers 4 # one process per core
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Workers share the port with `SO_REUSEPORT` (Linux); otherwise a busy port makes the server
|
|
107
|
+
fail to start instead of silently sharing it.
|
|
108
|
+
|
|
109
|
+
## Code first: Pydantic models -> `.proto`
|
|
110
|
+
|
|
111
|
+
`fastgrpc3 lock`, `check` and `export` take an app (`myapp.main:app`) or a `SchemaRegistry`.
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from pydantic import BaseModel
|
|
115
|
+
from fastgrpc3 import MethodDef, SchemaRegistry, ServiceDef, field
|
|
116
|
+
from fastgrpc3.types import UInt32
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class GetOrder(BaseModel):
|
|
120
|
+
order_number: str
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class Order(BaseModel):
|
|
124
|
+
order_number: str
|
|
125
|
+
qty: UInt32 = 0 # proto uint32, bounds checked by Pydantic
|
|
126
|
+
note: str | None = field(5, default=None) # explicit number
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
registry = SchemaRegistry()
|
|
130
|
+
registry.add_service(ServiceDef("Orders", "orders.v1", (MethodDef("Get", GetOrder, Order),)))
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
fastgrpc3 lock myapp.contract:registry # number new fields, write proto.lock (commit it)
|
|
135
|
+
fastgrpc3 check myapp.contract:registry # CI: fail if proto.lock is out of date
|
|
136
|
+
fastgrpc3 export myapp.contract:registry -o proto/ # one .proto file per package
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Fields without an explicit number get one the first time they are locked, wherever they are
|
|
140
|
+
declared; a removed field's number is reserved for good. Types without a proto equivalent
|
|
141
|
+
(`Decimal`, `UUID`, unions…) travel as a JSON string, marked `// json` in the `.proto`.
|
|
142
|
+
|
|
143
|
+
## Proto first: `.proto` -> Pydantic models
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
fastgrpc3 generate proto/ -o src/myapp/gen # -I third_party/ for extra imports
|
|
147
|
+
fastgrpc3 generate proto/ -o src/myapp/gen --check # CI: fail if not up to date
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`orders/v1/orders.proto` gives `orders/v1/orders_models.py` (one `ProtoModel` per message,
|
|
151
|
+
one open `ProtoEnum` per enum) and `orders/v1/orders_service.py`; `_descriptor.py` embeds the
|
|
152
|
+
compiled descriptor, so nothing is compiled at startup. Defaults can live in `pyproject.toml`:
|
|
153
|
+
|
|
154
|
+
```toml
|
|
155
|
+
[tool.fastgrpc3.generate]
|
|
156
|
+
proto_paths = ["proto"]
|
|
157
|
+
output = "src/myapp/gen"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Each service module also holds the contract (`Orders`, to serve with
|
|
161
|
+
`GrpcService.implement(Orders)`) and a typed client (`OrdersClient`). Generated code passes
|
|
162
|
+
`mypy --strict` and `ruff`. Well-known types become Python types
|
|
163
|
+
(`Timestamp` -> `datetime`, `Struct` -> `dict[str, Any]`, wrappers -> `X | None`…).
|
|
164
|
+
|
|
165
|
+
## Documentation
|
|
166
|
+
|
|
167
|
+
The guide and the reference live in [`docs/`](docs/index.md) (MkDocs). To build the site
|
|
168
|
+
into `site/`, or to serve it locally with live reload:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
uv run --project docs mkdocs build --strict
|
|
172
|
+
uv run --project docs mkdocs serve # http://127.0.0.1:8000
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Development
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
uv sync # install the package and the dev tools
|
|
179
|
+
uv run pytest # tests
|
|
180
|
+
uv run ruff check . # lint
|
|
181
|
+
uv run ruff format --check # formatting
|
|
182
|
+
uv run mypy src tests # type checking
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "fastgrpc3"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "FastAPI-like gRPC framework for Python: contracts from .proto files or Pydantic models, powered by protoruf (Rust)"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.12"
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 1 - Planning",
|
|
11
|
+
"Framework :: AsyncIO",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python :: 3.13",
|
|
16
|
+
"Programming Language :: Python :: 3.14",
|
|
17
|
+
"Topic :: System :: Networking",
|
|
18
|
+
"Typing :: Typed",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"grpcio>=1.75.1",
|
|
22
|
+
"protoruf>=0.5.3",
|
|
23
|
+
"pydantic>=2.11",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[[project.authors]]
|
|
27
|
+
name = "William Nauroy"
|
|
28
|
+
email = "william.nauroy@gmail.com"
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
fastgrpc3 = "fastgrpc3.cli:main"
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
docs = ["uvicorn>=0.30"]
|
|
35
|
+
tonic = ["fastgrpc3-transport>=0.1,<0.2"]
|
|
36
|
+
|
|
37
|
+
[dependency-groups]
|
|
38
|
+
bench = [
|
|
39
|
+
"fastgrpc2==0.5.0",
|
|
40
|
+
"protobuf>=7.36.2",
|
|
41
|
+
]
|
|
42
|
+
dev = [
|
|
43
|
+
"mypy>=2.4",
|
|
44
|
+
"pytest>=9.2",
|
|
45
|
+
"pytest-asyncio>=1.4",
|
|
46
|
+
"ruff>=0.17",
|
|
47
|
+
"types-grpcio>=1.85.0.20261028",
|
|
48
|
+
"uvicorn>=0.30",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[build-system]
|
|
52
|
+
requires = ["uv_build>=0.13.4,<0.14.0"]
|
|
53
|
+
build-backend = "uv_build"
|
|
54
|
+
|
|
55
|
+
[tool.ruff]
|
|
56
|
+
line-length = 100
|
|
57
|
+
target-version = "py312"
|
|
58
|
+
extend-exclude = ["src/fastgrpc3/_proto"]
|
|
59
|
+
|
|
60
|
+
[tool.ruff.lint]
|
|
61
|
+
select = [
|
|
62
|
+
"E",
|
|
63
|
+
"F",
|
|
64
|
+
"W",
|
|
65
|
+
"I",
|
|
66
|
+
"UP",
|
|
67
|
+
"B",
|
|
68
|
+
"SIM",
|
|
69
|
+
"RUF",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
[tool.ruff.lint.flake8-bugbear]
|
|
73
|
+
extend-immutable-calls = [
|
|
74
|
+
"fastgrpc3.Depends",
|
|
75
|
+
"fastgrpc3.api.deps.Depends",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
[tool.mypy]
|
|
79
|
+
python_version = "3.12"
|
|
80
|
+
strict = true
|
|
81
|
+
|
|
82
|
+
[[tool.mypy.overrides]]
|
|
83
|
+
module = ["fastgrpc3_transport"]
|
|
84
|
+
ignore_missing_imports = true
|
|
85
|
+
|
|
86
|
+
[[tool.mypy.overrides]]
|
|
87
|
+
module = ["fastgrpc3.transport.tonic"]
|
|
88
|
+
warn_return_any = false
|
|
89
|
+
|
|
90
|
+
[tool.pytest.ini_options]
|
|
91
|
+
testpaths = ["tests"]
|
|
92
|
+
asyncio_mode = "auto"
|
|
93
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "fastgrpc3"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "FastAPI-like gRPC framework for Python: contracts from .proto files or Pydantic models, powered by protoruf (Rust)"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "William Nauroy", email = "william.nauroy@gmail.com" }
|
|
10
|
+
]
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 1 - Planning",
|
|
14
|
+
"Framework :: AsyncIO",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Programming Language :: Python :: 3.14",
|
|
20
|
+
"Topic :: System :: Networking",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
# First release shipping CPython 3.14 wheels.
|
|
25
|
+
"grpcio>=1.75.1",
|
|
26
|
+
"protoruf>=0.5.3",
|
|
27
|
+
# `model_validate(..., by_name=True)` appeared in 2.11.
|
|
28
|
+
"pydantic>=2.11",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
fastgrpc3 = "fastgrpc3.cli:main"
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
docs = [
|
|
36
|
+
"uvicorn>=0.30",
|
|
37
|
+
]
|
|
38
|
+
# The tonic transport (`GrpcApp(transport="tonic")`), a Rust server.
|
|
39
|
+
tonic = [
|
|
40
|
+
"fastgrpc3-transport>=0.1,<0.2",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[dependency-groups]
|
|
44
|
+
bench = [
|
|
45
|
+
"fastgrpc2==0.5.0",
|
|
46
|
+
"protobuf>=7.36.2",
|
|
47
|
+
]
|
|
48
|
+
dev = [
|
|
49
|
+
"mypy>=2.4",
|
|
50
|
+
"pytest>=9.2",
|
|
51
|
+
"pytest-asyncio>=1.4",
|
|
52
|
+
"ruff>=0.17",
|
|
53
|
+
"types-grpcio>=1.85.0.20261028",
|
|
54
|
+
"uvicorn>=0.30",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
[build-system]
|
|
58
|
+
requires = ["uv_build>=0.13.4,<0.14.0"]
|
|
59
|
+
build-backend = "uv_build"
|
|
60
|
+
|
|
61
|
+
[tool.ruff]
|
|
62
|
+
line-length = 100
|
|
63
|
+
target-version = "py312"
|
|
64
|
+
# Generated by scripts/generate_standard_protos.py (proto comments make long lines).
|
|
65
|
+
extend-exclude = ["src/fastgrpc3/_proto"]
|
|
66
|
+
|
|
67
|
+
[tool.ruff.lint]
|
|
68
|
+
select = ["E", "F", "W", "I", "UP", "B", "SIM", "RUF"]
|
|
69
|
+
|
|
70
|
+
[tool.ruff.lint.flake8-bugbear]
|
|
71
|
+
# `x: Repo = Depends(get_repo)` is the documented way to declare a dependency.
|
|
72
|
+
extend-immutable-calls = ["fastgrpc3.Depends", "fastgrpc3.api.deps.Depends"]
|
|
73
|
+
|
|
74
|
+
[tool.mypy]
|
|
75
|
+
python_version = "3.12"
|
|
76
|
+
strict = true
|
|
77
|
+
|
|
78
|
+
[[tool.mypy.overrides]]
|
|
79
|
+
# The tonic transport, optional (`fastgrpc3-transport`, typed when installed).
|
|
80
|
+
module = ["fastgrpc3_transport"]
|
|
81
|
+
ignore_missing_imports = true
|
|
82
|
+
|
|
83
|
+
[[tool.mypy.overrides]]
|
|
84
|
+
# Returns what the extension gives, typed only when fastgrpc3-transport is installed.
|
|
85
|
+
module = ["fastgrpc3.transport.tonic"]
|
|
86
|
+
warn_return_any = false
|
|
87
|
+
|
|
88
|
+
[tool.pytest.ini_options]
|
|
89
|
+
testpaths = ["tests"]
|
|
90
|
+
asyncio_mode = "auto"
|
|
91
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""fastgrpc3: a FastAPI-like gRPC framework for Python, powered by protoruf."""
|
|
2
|
+
|
|
3
|
+
from fastgrpc3.api.app import GrpcApp
|
|
4
|
+
from fastgrpc3.api.client import Channel, GrpcClient
|
|
5
|
+
from fastgrpc3.api.config import Compression, Keepalive, RetryPolicy
|
|
6
|
+
from fastgrpc3.api.context import Context
|
|
7
|
+
from fastgrpc3.api.deps import Depends, blocking, non_blocking
|
|
8
|
+
from fastgrpc3.api.errors import (
|
|
9
|
+
BadRequest,
|
|
10
|
+
ErrorInfo,
|
|
11
|
+
FieldViolation,
|
|
12
|
+
GrpcError,
|
|
13
|
+
StatusCode,
|
|
14
|
+
register_detail,
|
|
15
|
+
)
|
|
16
|
+
from fastgrpc3.api.metadata import Metadata
|
|
17
|
+
from fastgrpc3.api.security import MTLS, TLS, BearerToken, Insecure
|
|
18
|
+
from fastgrpc3.api.service import GrpcService
|
|
19
|
+
from fastgrpc3.runtime.dispatch import CallNext
|
|
20
|
+
from fastgrpc3.schema import (
|
|
21
|
+
Lock,
|
|
22
|
+
LockError,
|
|
23
|
+
MethodDef,
|
|
24
|
+
Schema,
|
|
25
|
+
SchemaError,
|
|
26
|
+
SchemaRegistry,
|
|
27
|
+
ServiceDef,
|
|
28
|
+
)
|
|
29
|
+
from fastgrpc3.transport import Transport, TransportCapabilities, available_transports
|
|
30
|
+
from fastgrpc3.types import ProtoEnum, ProtoField, ProtoModel, field, key, proto_enum
|
|
31
|
+
|
|
32
|
+
__version__ = "0.1.0"
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"MTLS",
|
|
36
|
+
"TLS",
|
|
37
|
+
"BadRequest",
|
|
38
|
+
"BearerToken",
|
|
39
|
+
"CallNext",
|
|
40
|
+
"Channel",
|
|
41
|
+
"Compression",
|
|
42
|
+
"Context",
|
|
43
|
+
"Depends",
|
|
44
|
+
"ErrorInfo",
|
|
45
|
+
"FieldViolation",
|
|
46
|
+
"GrpcApp",
|
|
47
|
+
"GrpcClient",
|
|
48
|
+
"GrpcError",
|
|
49
|
+
"GrpcService",
|
|
50
|
+
"Insecure",
|
|
51
|
+
"Keepalive",
|
|
52
|
+
"Lock",
|
|
53
|
+
"LockError",
|
|
54
|
+
"Metadata",
|
|
55
|
+
"MethodDef",
|
|
56
|
+
"ProtoEnum",
|
|
57
|
+
"ProtoField",
|
|
58
|
+
"ProtoModel",
|
|
59
|
+
"RetryPolicy",
|
|
60
|
+
"Schema",
|
|
61
|
+
"SchemaError",
|
|
62
|
+
"SchemaRegistry",
|
|
63
|
+
"ServiceDef",
|
|
64
|
+
"StatusCode",
|
|
65
|
+
"Transport",
|
|
66
|
+
"TransportCapabilities",
|
|
67
|
+
"__version__",
|
|
68
|
+
"available_transports",
|
|
69
|
+
"blocking",
|
|
70
|
+
"field",
|
|
71
|
+
"key",
|
|
72
|
+
"non_blocking",
|
|
73
|
+
"proto_enum",
|
|
74
|
+
"register_detail",
|
|
75
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Generated by fastgrpc3: do not edit.
|