fastapi-openrpc 0.1.3__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.
- fastapi_openrpc-0.1.3/.env.example +5 -0
- fastapi_openrpc-0.1.3/.gitignore +21 -0
- fastapi_openrpc-0.1.3/.pre-commit-config.yaml +46 -0
- fastapi_openrpc-0.1.3/CLAUDE.md +76 -0
- fastapi_openrpc-0.1.3/Justfile +73 -0
- fastapi_openrpc-0.1.3/PKG-INFO +25 -0
- fastapi_openrpc-0.1.3/README.md +206 -0
- fastapi_openrpc-0.1.3/README_zh.md +196 -0
- fastapi_openrpc-0.1.3/docs/agents/domain.md +47 -0
- fastapi_openrpc-0.1.3/docs/agents/issue-tracker.md +52 -0
- fastapi_openrpc-0.1.3/docs/agents/triage-labels.md +25 -0
- fastapi_openrpc-0.1.3/docs/brainstorms/2026-04-23-openrpc-components-schemas-requirements.md +127 -0
- fastapi_openrpc-0.1.3/docs/brainstorms/2026-04-23-rpc-context-type-injection-requirements.md +97 -0
- fastapi_openrpc-0.1.3/docs/brainstorms/2026-06-01-replay-protection-requirements.md +102 -0
- fastapi_openrpc-0.1.3/docs/en/index.md +844 -0
- fastapi_openrpc-0.1.3/docs/explanation-concurrency.md +233 -0
- fastapi_openrpc-0.1.3/docs/howto-error-handling.md +465 -0
- fastapi_openrpc-0.1.3/docs/index.md +11 -0
- fastapi_openrpc-0.1.3/docs/plans/2026-04-23-001-refactor-rpc-context-type-injection-plan.md +252 -0
- fastapi_openrpc-0.1.3/docs/plans/2026-04-23-002-refactor-openrpc-components-schemas-plan.md +474 -0
- fastapi_openrpc-0.1.3/docs/plans/2026-06-01-001-feat-replay-protection-plan.md +260 -0
- fastapi_openrpc-0.1.3/docs/plans/2026-06-01-002-fix-code-review-issues-plan.md +353 -0
- fastapi_openrpc-0.1.3/docs/tutorial-pydantic-models.md +353 -0
- fastapi_openrpc-0.1.3/docs/zh/index.md +852 -0
- fastapi_openrpc-0.1.3/examples/hmac_auth_dependency.py +66 -0
- fastapi_openrpc-0.1.3/examples/minimal_app.py +41 -0
- fastapi_openrpc-0.1.3/examples/pydantic_models.py +120 -0
- fastapi_openrpc-0.1.3/pyproject.toml +62 -0
- fastapi_openrpc-0.1.3/src/fastapi_openrpc/__init__.py +104 -0
- fastapi_openrpc-0.1.3/src/fastapi_openrpc/authentication.py +248 -0
- fastapi_openrpc-0.1.3/src/fastapi_openrpc/authorization.py +79 -0
- fastapi_openrpc-0.1.3/src/fastapi_openrpc/constants.py +43 -0
- fastapi_openrpc-0.1.3/src/fastapi_openrpc/decorator.py +517 -0
- fastapi_openrpc-0.1.3/src/fastapi_openrpc/document.py +329 -0
- fastapi_openrpc-0.1.3/src/fastapi_openrpc/errors.py +125 -0
- fastapi_openrpc-0.1.3/src/fastapi_openrpc/registry.py +132 -0
- fastapi_openrpc-0.1.3/src/fastapi_openrpc/router.py +905 -0
- fastapi_openrpc-0.1.3/src/fastapi_openrpc/types.py +99 -0
- fastapi_openrpc-0.1.3/tests/http/.httpyac.js +55 -0
- fastapi_openrpc-0.1.3/tests/http/api.http +150 -0
- fastapi_openrpc-0.1.3/tests/http/hmac.http +32 -0
- fastapi_openrpc-0.1.3/tests/http/minimal.http +25 -0
- fastapi_openrpc-0.1.3/tests/http/pydantic.http +38 -0
- fastapi_openrpc-0.1.3/tests/test_authentication.py +821 -0
- fastapi_openrpc-0.1.3/tests/test_authorization.py +179 -0
- fastapi_openrpc-0.1.3/tests/test_decorator.py +434 -0
- fastapi_openrpc-0.1.3/tests/test_document.py +470 -0
- fastapi_openrpc-0.1.3/tests/test_errors.py +126 -0
- fastapi_openrpc-0.1.3/tests/test_registry.py +160 -0
- fastapi_openrpc-0.1.3/tests/test_router.py +496 -0
- fastapi_openrpc-0.1.3/uv.lock +655 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# .pre-commit-config.yaml
|
|
2
|
+
# 使用方法:
|
|
3
|
+
# pre-commit install # 安装 hook(首次)
|
|
4
|
+
# pre-commit run --all-files # 手动运行所有文件
|
|
5
|
+
# pre-commit run # 仅运行 staged 文件
|
|
6
|
+
# pre-commit autoupdate # 更新 hook 版本
|
|
7
|
+
|
|
8
|
+
repos:
|
|
9
|
+
# ========================
|
|
10
|
+
# Ruff — Lint + Format
|
|
11
|
+
# ========================
|
|
12
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
13
|
+
rev: v0.11.5
|
|
14
|
+
hooks:
|
|
15
|
+
- id: ruff
|
|
16
|
+
args: [--fix]
|
|
17
|
+
- id: ruff-format
|
|
18
|
+
|
|
19
|
+
# ========================
|
|
20
|
+
# Pyright — 类型检查
|
|
21
|
+
# ========================
|
|
22
|
+
- repo: https://github.com/RobertCraigie/pyright-python
|
|
23
|
+
rev: v1.1.402
|
|
24
|
+
hooks:
|
|
25
|
+
- id: pyright
|
|
26
|
+
additional_dependencies:
|
|
27
|
+
- fastapi>=0.100.0
|
|
28
|
+
- pydantic>=2.0
|
|
29
|
+
- pytest>=8.0
|
|
30
|
+
- pytest-asyncio>=0.23
|
|
31
|
+
- httpx>=0.27
|
|
32
|
+
- starlette>=0.30.0
|
|
33
|
+
- uvicorn>=0.30.0
|
|
34
|
+
|
|
35
|
+
# ========================
|
|
36
|
+
# 通用工具
|
|
37
|
+
# ========================
|
|
38
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
39
|
+
rev: v5.0.0
|
|
40
|
+
hooks:
|
|
41
|
+
- id: check-yaml
|
|
42
|
+
- id: check-json
|
|
43
|
+
- id: check-added-large-files
|
|
44
|
+
args: [--maxkb=500]
|
|
45
|
+
- id: end-of-file-fixer
|
|
46
|
+
- id: trailing-whitespace
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
These rules apply to every task in this project unless explicitly overridden.
|
|
4
|
+
Bias: caution over speed on non-trivial work. Use judgment on trivial tasks.
|
|
5
|
+
|
|
6
|
+
## Agent Skills
|
|
7
|
+
|
|
8
|
+
### Issue tracker
|
|
9
|
+
|
|
10
|
+
Local markdown issues under `.scratch/<feature-slug>/`. See `docs/agents/issue-tracker.md`.
|
|
11
|
+
|
|
12
|
+
### Triage labels
|
|
13
|
+
|
|
14
|
+
Canonical labels: `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`. See `docs/agents/triage-labels.md`.
|
|
15
|
+
|
|
16
|
+
### Domain docs
|
|
17
|
+
|
|
18
|
+
Single-context: one `CONTEXT.md` + `docs/adr/` at repo root. See `docs/agents/domain.md`.
|
|
19
|
+
|
|
20
|
+
## Rule 1 — Think Before Coding
|
|
21
|
+
State assumptions explicitly. If uncertain, ask rather than guess.
|
|
22
|
+
Present multiple interpretations when ambiguity exists.
|
|
23
|
+
Push back when a simpler approach exists.
|
|
24
|
+
Stop when confused. Name what's unclear.
|
|
25
|
+
|
|
26
|
+
## Rule 2 — Simplicity First
|
|
27
|
+
Minimum code that solves the problem. Nothing speculative.
|
|
28
|
+
No features beyond what was asked. No abstractions for single-use code.
|
|
29
|
+
Test: would a senior engineer say this is overcomplicated? If yes, simplify.
|
|
30
|
+
|
|
31
|
+
## Rule 3 — Surgical Changes
|
|
32
|
+
Touch only what you must. Clean up only your own mess.
|
|
33
|
+
Don't "improve" adjacent code, comments, or formatting.
|
|
34
|
+
Don't refactor what isn't broken. Match existing style.
|
|
35
|
+
|
|
36
|
+
## Rule 4 — Goal-Driven Execution
|
|
37
|
+
Define success criteria. Loop until verified.
|
|
38
|
+
Don't follow steps. Define success and iterate.
|
|
39
|
+
Strong success criteria let you loop independently.
|
|
40
|
+
|
|
41
|
+
## Rule 5 — Use the model only for judgment calls
|
|
42
|
+
Use me for: classification, drafting, summarization, extraction.
|
|
43
|
+
Do NOT use me for: routing, retries, deterministic transforms.
|
|
44
|
+
If code can answer, code answers.
|
|
45
|
+
|
|
46
|
+
## Rule 6 — Token budgets are not advisory
|
|
47
|
+
Per-task: 4,000 tokens. Per-session: 30,000 tokens.
|
|
48
|
+
If approaching budget, summarize and start fresh.
|
|
49
|
+
Surface the breach. Do not silently overrun.
|
|
50
|
+
|
|
51
|
+
## Rule 7 — Surface conflicts, don't average them
|
|
52
|
+
If two patterns contradict, pick one (more recent / more tested).
|
|
53
|
+
Explain why. Flag the other for cleanup.
|
|
54
|
+
Don't blend conflicting patterns.
|
|
55
|
+
|
|
56
|
+
## Rule 8 — Read before you write
|
|
57
|
+
Before adding code, read exports, immediate callers, shared utilities.
|
|
58
|
+
"Looks orthogonal" is dangerous. If unsure why code is structured a way, ask.
|
|
59
|
+
|
|
60
|
+
## Rule 9 — Tests verify intent, not just behavior
|
|
61
|
+
Tests must encode WHY behavior matters, not just WHAT it does.
|
|
62
|
+
A test that can't fail when business logic changes is wrong.
|
|
63
|
+
|
|
64
|
+
## Rule 10 — Checkpoint after every significant step
|
|
65
|
+
Summarize what was done, what's verified, what's left.
|
|
66
|
+
Don't continue from a state you can't describe back.
|
|
67
|
+
If you lose track, stop and restate.
|
|
68
|
+
|
|
69
|
+
## Rule 11 — Match the codebase's conventions, even if you disagree
|
|
70
|
+
Conformance > taste inside the codebase.
|
|
71
|
+
If you genuinely think a convention is harmful, surface it. Don't fork silently.
|
|
72
|
+
|
|
73
|
+
## Rule 12 — Fail loud
|
|
74
|
+
"Completed" is wrong if anything was skipped silently.
|
|
75
|
+
"Tests pass" is wrong if any were skipped.
|
|
76
|
+
Default to surfacing uncertainty, not hiding it.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Justfile for fastapi-openrpc
|
|
2
|
+
|
|
3
|
+
# 默认目标:运行所有测试
|
|
4
|
+
default: test
|
|
5
|
+
|
|
6
|
+
# 安装依赖
|
|
7
|
+
install:
|
|
8
|
+
uv sync
|
|
9
|
+
|
|
10
|
+
# 开发依赖
|
|
11
|
+
install-dev:
|
|
12
|
+
uv sync --extra dev
|
|
13
|
+
|
|
14
|
+
# 运行所有测试
|
|
15
|
+
test:
|
|
16
|
+
uv run pytest tests/ -v
|
|
17
|
+
|
|
18
|
+
# 运行测试并显示覆盖率
|
|
19
|
+
test-cov:
|
|
20
|
+
uv run pytest tests/ -v --cov=src/fastapi_openrpc --cov-report=term-missing
|
|
21
|
+
|
|
22
|
+
# 启动 minimal 示例(PORT 环境变量可覆盖,如 PORT=9000 just run-minimal)
|
|
23
|
+
run-minimal:
|
|
24
|
+
uv run uvicorn examples.minimal_app:app --host 0.0.0.0 --port 10333 --reload
|
|
25
|
+
|
|
26
|
+
# 测试 minimal 示例(启动服务,退出时关闭)
|
|
27
|
+
test-minimal:
|
|
28
|
+
bash -c 'uv run uvicorn examples.minimal_app:app --host 0.0.0.0 --port 10333 & PID=$$!; trap "kill $PID 2>/dev/null; wait $PID 2>/dev/null; true" EXIT; npx httpyac tests/http/minimal.http -a'
|
|
29
|
+
|
|
30
|
+
# 启动 HMAC 认证示例(需先设置 HMAC_SECRET_KEY)
|
|
31
|
+
run-hmac:
|
|
32
|
+
HEADER_SIGNING_KEY=my-secret-key uv run uvicorn examples.hmac_auth_dependency:app --host 0.0.0.0 --port 10334 --reload
|
|
33
|
+
|
|
34
|
+
test-hmac:
|
|
35
|
+
bash -c 'HMAC_SECRET_KEY=my-secret-key uv run uvicorn examples.hmac_auth_dependency:app --host 0.0.0.0 --port 10334 & PID=$$!; trap "kill $PID 2>/dev/null; wait $PID 2>/dev/null; true" EXIT; npx httpyac tests/http/hmac.http -a'
|
|
36
|
+
|
|
37
|
+
# 启动 Pydantic 模型示例
|
|
38
|
+
run-pydantic:
|
|
39
|
+
uv run uvicorn examples.pydantic_models:app --host 0.0.0.0 --port 10335 --reload
|
|
40
|
+
|
|
41
|
+
test-pydantic:
|
|
42
|
+
bash -c 'uv run uvicorn examples.pydantic_models:app --host 0.0.0.0 --port 10335 & PID=$$!; trap "kill $PID 2>/dev/null; wait $PID 2>/dev/null; true" EXIT; npx httpyac tests/http/pydantic.http -a'
|
|
43
|
+
|
|
44
|
+
# Lint
|
|
45
|
+
lint:
|
|
46
|
+
uv run ruff check src/ tests/
|
|
47
|
+
|
|
48
|
+
# 格式化
|
|
49
|
+
fmt:
|
|
50
|
+
uv run ruff format src/ tests/
|
|
51
|
+
|
|
52
|
+
# 类型检查
|
|
53
|
+
typecheck:
|
|
54
|
+
uv run pyright
|
|
55
|
+
|
|
56
|
+
# 完整开发检查:lint + typecheck + test
|
|
57
|
+
check: lint typecheck test
|
|
58
|
+
|
|
59
|
+
# 运行 pre-commit(仅 staged 文件)
|
|
60
|
+
precommit:
|
|
61
|
+
uv run pre-commit run
|
|
62
|
+
|
|
63
|
+
# 运行 pre-commit 所有文件
|
|
64
|
+
precommit-all:
|
|
65
|
+
uv run pre-commit run --all-files
|
|
66
|
+
|
|
67
|
+
# 安装 pre-commit hook(首次克隆后运行一次)
|
|
68
|
+
precommit-install:
|
|
69
|
+
pre-commit install
|
|
70
|
+
|
|
71
|
+
# 列出所有可用目标
|
|
72
|
+
help:
|
|
73
|
+
just --list
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi-openrpc
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: FastAPI JSON-RPC 2.0 + OpenRPC 1.0 service discovery plugin
|
|
5
|
+
Author-email: Ye Wenbin <yewenbin@winwin-tech.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: ai-agent,discover,fastapi,json-rpc,openrpc,rpc
|
|
8
|
+
Classifier: Framework :: FastAPI
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Requires-Dist: fastapi>=0.100.0
|
|
15
|
+
Requires-Dist: pydantic>=2.0
|
|
16
|
+
Requires-Dist: typing-extensions>=4.8
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: fakeredis>=2.21; extra == 'dev'
|
|
19
|
+
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
20
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
24
|
+
Provides-Extra: redis
|
|
25
|
+
Requires-Dist: redis[hiredis]>=5.0; extra == 'redis'
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# fastapi-openrpc
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/fastapi-openrpc/)
|
|
4
|
+
[](https://pypi.org/project/fastapi-openrpc/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Declarative JSON-RPC 2.0 interface and OpenRPC 1.0 service discovery for FastAPI applications.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Declarative Registration** — `@method()` decorator with automatic JSON Schema inference
|
|
12
|
+
- **OpenRPC Service Discovery** — `GET /openrpc.json` returns complete OpenRPC document
|
|
13
|
+
- **Pydantic Support** — Automatic schema generation with `$ref` references
|
|
14
|
+
- **RpcContext Propagation** — Secure caller context injection via FastAPI `Depends()`
|
|
15
|
+
- **HMAC Authentication** — Built-in signature verification with Nonce replay protection
|
|
16
|
+
- **Role-Based Authorization** — Method-level role declarations with composition semantics
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install fastapi-openrpc
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Optional dependencies:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Redis support (Nonce replay protection)
|
|
28
|
+
pip install fastapi-openrpc[redis]
|
|
29
|
+
|
|
30
|
+
# Development dependencies
|
|
31
|
+
pip install fastapi-openrpc[dev]
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from fastapi import FastAPI
|
|
38
|
+
from fastapi_openrpc import OpenRpcRouter, RpcContext, method
|
|
39
|
+
|
|
40
|
+
app = FastAPI()
|
|
41
|
+
|
|
42
|
+
openrpc_router = OpenRpcRouter(
|
|
43
|
+
title="My API",
|
|
44
|
+
version="1.0.0",
|
|
45
|
+
description="My JSON-RPC service",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
@method(name="getUser")
|
|
49
|
+
async def get_user(ctx: RpcContext, user_id: str) -> dict:
|
|
50
|
+
"""Get user information by user_id."""
|
|
51
|
+
return {"id": user_id, "name": "Alice", "roles": ctx.roles}
|
|
52
|
+
|
|
53
|
+
@method(name="listUsers")
|
|
54
|
+
async def list_users(ctx: RpcContext, limit: int = 10) -> list[dict]:
|
|
55
|
+
"""Return a list of users."""
|
|
56
|
+
return [{"id": i, "name": f"User {i}"} for i in range(limit)]
|
|
57
|
+
|
|
58
|
+
app.include_router(openrpc_router)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
After starting the server, access `GET /openrpc.json` to view the OpenRPC service document.
|
|
62
|
+
|
|
63
|
+
## Authentication
|
|
64
|
+
|
|
65
|
+
### HMAC Authentication
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from fastapi_openrpc.authentication import HMACAuthentication
|
|
69
|
+
|
|
70
|
+
auth = HMACAuthentication(secret_key="your-secret-key")
|
|
71
|
+
openrpc_router = OpenRpcRouter(
|
|
72
|
+
authentication=auth,
|
|
73
|
+
)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Nonce Replay Protection
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
auth = HMACAuthentication(
|
|
80
|
+
secret_key="your-secret-key",
|
|
81
|
+
enable_nonce=True,
|
|
82
|
+
redis_url="redis://localhost:6379",
|
|
83
|
+
nonce_ttl=3600,
|
|
84
|
+
redis_required=True, # Reject requests when Redis is unavailable
|
|
85
|
+
connect_timeout=2.0, # Redis connection timeout (seconds)
|
|
86
|
+
operation_timeout=5.0, # Redis operation timeout (seconds)
|
|
87
|
+
cleanup_timeout=2.0, # Redis cleanup timeout (seconds)
|
|
88
|
+
)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Header format:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"roles": ["admin"],
|
|
96
|
+
"nonce": "550e8400-e29b-41d4-a716-446655440000"
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Authorization
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from fastapi_openrpc import Require
|
|
104
|
+
|
|
105
|
+
@method(name="adminAction", required_roles=["admin"])
|
|
106
|
+
async def admin_action(ctx: RpcContext) -> dict:
|
|
107
|
+
"""Admin only."""
|
|
108
|
+
return {"status": "done"}
|
|
109
|
+
|
|
110
|
+
@method(name="editContent", required_roles=["editor", "admin"], require=Require.ALL)
|
|
111
|
+
async def edit_content(ctx: RpcContext) -> dict:
|
|
112
|
+
"""Requires both editor and admin roles."""
|
|
113
|
+
return {"status": "edited"}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Error Handling
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from fastapi_openrpc import InvalidParams, MethodNotFound
|
|
120
|
+
|
|
121
|
+
@method(name="divide")
|
|
122
|
+
async def divide(ctx: RpcContext, a: float, b: float) -> float:
|
|
123
|
+
if b == 0:
|
|
124
|
+
raise InvalidParams("Division by zero")
|
|
125
|
+
return a / b
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Built-in error codes:
|
|
129
|
+
|
|
130
|
+
| Error | Code |
|
|
131
|
+
|-------|------|
|
|
132
|
+
| Parse error | -32700 |
|
|
133
|
+
| Invalid Request | -32600 |
|
|
134
|
+
| Method Not Found | -32601 |
|
|
135
|
+
| Invalid Params | -32602 |
|
|
136
|
+
| Internal Error | -32603 |
|
|
137
|
+
| Forbidden | -32000 |
|
|
138
|
+
| Replay Detected | -32002 |
|
|
139
|
+
|
|
140
|
+
Library reserves -32000 ~ -32099, application custom errors start from -32100.
|
|
141
|
+
|
|
142
|
+
## API Reference
|
|
143
|
+
|
|
144
|
+
### OpenRpcRouter
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
148
|
+
|
|
149
|
+
executor = ThreadPoolExecutor(max_workers=4)
|
|
150
|
+
openrpc_router = OpenRpcRouter(
|
|
151
|
+
title="My API",
|
|
152
|
+
version="1.0.0",
|
|
153
|
+
description="...",
|
|
154
|
+
servers=[{"url": "https://api.example.com"}],
|
|
155
|
+
authentication=HMACAuthentication(secret_key="..."), # Optional
|
|
156
|
+
executor=executor, # Optional, for sync handler thread pool
|
|
157
|
+
)
|
|
158
|
+
app.include_router(openrpc_router)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### @method() Decorator
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
@method(
|
|
165
|
+
name="myMethod", # RPC method name
|
|
166
|
+
tags=["admin"], # OpenRPC tags
|
|
167
|
+
summary="Short description",
|
|
168
|
+
deprecated=False,
|
|
169
|
+
required_roles=["admin"], # Required roles
|
|
170
|
+
require=Require.ALL, # Role composition semantics
|
|
171
|
+
)
|
|
172
|
+
async def my_handler(ctx: RpcContext, arg1: str, arg2: int = 10) -> dict:
|
|
173
|
+
return {"result": arg1}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
> **Note**: `ctx: RpcContext` is a reserved parameter name for injecting caller context.
|
|
177
|
+
|
|
178
|
+
## Usage Examples
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
# Call RPC method
|
|
182
|
+
curl -X POST http://localhost:8000/ \
|
|
183
|
+
-H "Content-Type: application/json" \
|
|
184
|
+
-d '{"jsonrpc": "2.0", "id": 1, "method": "getUser", "params": {"user_id": "42"}}'
|
|
185
|
+
|
|
186
|
+
# Service discovery
|
|
187
|
+
curl http://localhost:8000/openrpc.json
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Documentation
|
|
191
|
+
|
|
192
|
+
For detailed user documentation, see [docs/index.md](docs/index.md).
|
|
193
|
+
|
|
194
|
+
Additional guides:
|
|
195
|
+
|
|
196
|
+
- [Tutorial: Pydantic Models](docs/tutorial-pydantic-models.md) — 4 scenarios covering flat params, nested models, optional fields, and validation
|
|
197
|
+
- [How-to: Error Handling](docs/howto-error-handling.md) — custom errors, Nonce replay protection, best practices
|
|
198
|
+
- [Explanation: Concurrency Model](docs/explanation-concurrency.md) — ThreadPoolExecutor, sync handler wrapping, closure safety
|
|
199
|
+
|
|
200
|
+
## Contributing
|
|
201
|
+
|
|
202
|
+
Issues and Pull Requests are welcome.
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
MIT
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# fastapi-openrpc
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/fastapi-openrpc/)
|
|
4
|
+
[](https://pypi.org/project/fastapi-openrpc/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
为 FastAPI 应用提供声明式 JSON-RPC 2.0 接口和 OpenRPC 1.0 服务发现支持。
|
|
8
|
+
|
|
9
|
+
## 特性
|
|
10
|
+
|
|
11
|
+
- **声明式注册** — `@method()` 装饰器,自动推断 JSON Schema
|
|
12
|
+
- **OpenRPC 服务发现** — `GET /openrpc.json` 返回完整 OpenRPC 文档
|
|
13
|
+
- **Pydantic 支持** — 自动生成 schema,支持 `$ref` 引用
|
|
14
|
+
- **RpcContext 传递** — 通过 FastAPI `Depends()` 安全注入调用者上下文
|
|
15
|
+
- **HMAC 认证** — 内置签名验证,支持 Nonce 重放攻击防护
|
|
16
|
+
- **角色授权** — 方法级角色声明与组合语义支持
|
|
17
|
+
|
|
18
|
+
## 安装
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install fastapi-openrpc
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
可选依赖:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Redis 支持(Nonce 重放防护)
|
|
28
|
+
pip install fastapi-openrpc[redis]
|
|
29
|
+
|
|
30
|
+
# 开发依赖
|
|
31
|
+
pip install fastapi-openrpc[dev]
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 快速开始
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from fastapi import FastAPI
|
|
38
|
+
from fastapi_openrpc import OpenRpcRouter, RpcContext, method
|
|
39
|
+
|
|
40
|
+
app = FastAPI()
|
|
41
|
+
|
|
42
|
+
openrpc_router = OpenRpcRouter(
|
|
43
|
+
title="My API",
|
|
44
|
+
version="1.0.0",
|
|
45
|
+
description="My JSON-RPC service",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
@method(name="getUser")
|
|
49
|
+
async def get_user(ctx: RpcContext, user_id: str) -> dict:
|
|
50
|
+
"""根据 user_id 返回用户信息。"""
|
|
51
|
+
return {"id": user_id, "name": "Alice", "roles": ctx.roles}
|
|
52
|
+
|
|
53
|
+
@method(name="listUsers")
|
|
54
|
+
async def list_users(ctx: RpcContext, limit: int = 10) -> list[dict]:
|
|
55
|
+
"""返回用户列表。"""
|
|
56
|
+
return [{"id": i, "name": f"User {i}"} for i in range(limit)]
|
|
57
|
+
|
|
58
|
+
app.include_router(openrpc_router)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
启动后访问 `GET /openrpc.json` 查看 OpenRPC 服务文档。
|
|
62
|
+
|
|
63
|
+
## 认证
|
|
64
|
+
|
|
65
|
+
### HMAC 认证
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from fastapi_openrpc.authentication import HMACAuthentication
|
|
69
|
+
|
|
70
|
+
auth = HMACAuthentication(secret_key="your-secret-key")
|
|
71
|
+
openrpc_router = OpenRpcRouter(
|
|
72
|
+
authentication=auth,
|
|
73
|
+
)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Nonce 重放防护
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
auth = HMACAuthentication(
|
|
80
|
+
secret_key="your-secret-key",
|
|
81
|
+
enable_nonce=True,
|
|
82
|
+
redis_url="redis://localhost:6379",
|
|
83
|
+
nonce_ttl=3600,
|
|
84
|
+
redis_required=True, # Redis 不可用时拒绝请求
|
|
85
|
+
connect_timeout=2.0, # Redis 连接超时(秒)
|
|
86
|
+
operation_timeout=5.0, # Redis 操作超时(秒)
|
|
87
|
+
cleanup_timeout=2.0, # Redis 清理超时(秒)
|
|
88
|
+
)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Header 格式:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"roles": ["admin"],
|
|
96
|
+
"nonce": "550e8400-e29b-41d4-a716-446655440000"
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## 授权
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from fastapi_openrpc import Require
|
|
104
|
+
|
|
105
|
+
@method(name="adminAction", required_roles=["admin"])
|
|
106
|
+
async def admin_action(ctx: RpcContext) -> dict:
|
|
107
|
+
"""仅限 admin 角色。"""
|
|
108
|
+
return {"status": "done"}
|
|
109
|
+
|
|
110
|
+
@method(name="editContent", required_roles=["editor", "admin"], require=Require.ALL)
|
|
111
|
+
async def edit_content(ctx: RpcContext) -> dict:
|
|
112
|
+
"""需要同时拥有 editor 和 admin 角色。"""
|
|
113
|
+
return {"status": "edited"}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## 错误处理
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from fastapi_openrpc import InvalidParams, MethodNotFound
|
|
120
|
+
|
|
121
|
+
@method(name="divide")
|
|
122
|
+
async def divide(ctx: RpcContext, a: float, b: float) -> float:
|
|
123
|
+
if b == 0:
|
|
124
|
+
raise InvalidParams("Division by zero")
|
|
125
|
+
return a / b
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
内置错误码:
|
|
129
|
+
|
|
130
|
+
| 错误 | 码 |
|
|
131
|
+
|------|-----|
|
|
132
|
+
| Parse error | -32700 |
|
|
133
|
+
| Invalid Request | -32600 |
|
|
134
|
+
| Method Not Found | -32601 |
|
|
135
|
+
| Invalid Params | -32602 |
|
|
136
|
+
| Internal Error | -32603 |
|
|
137
|
+
| Forbidden | -32000 |
|
|
138
|
+
| Replay Detected | -32002 |
|
|
139
|
+
|
|
140
|
+
库保留 -32000 ~ -32099,应用自定义错误从 -32100 起。
|
|
141
|
+
|
|
142
|
+
## API 参考
|
|
143
|
+
|
|
144
|
+
### OpenRpcRouter
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
openrpc_router = OpenRpcRouter(
|
|
148
|
+
title="My API",
|
|
149
|
+
version="1.0.0",
|
|
150
|
+
description="...",
|
|
151
|
+
servers=[{"url": "https://api.example.com"}],
|
|
152
|
+
authentication=HMACAuthentication(secret_key="..."), # 可选
|
|
153
|
+
)
|
|
154
|
+
app.include_router(openrpc_router)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### @method() 装饰器
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
@method(
|
|
161
|
+
name="myMethod", # RPC 方法名
|
|
162
|
+
tags=["admin"], # OpenRPC 标签
|
|
163
|
+
summary="简短描述",
|
|
164
|
+
deprecated=False,
|
|
165
|
+
required_roles=["admin"], # 所需角色
|
|
166
|
+
require=Require.ALL, # 角色组合语义
|
|
167
|
+
)
|
|
168
|
+
async def my_handler(ctx: RpcContext, arg1: str, arg2: int = 10) -> dict:
|
|
169
|
+
return {"result": arg1}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
> **注意**: 参数 `ctx: RpcContext` 是保留参数名,用于注入调用者上下文。
|
|
173
|
+
|
|
174
|
+
## 调用示例
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
# 调用 RPC 方法
|
|
178
|
+
curl -X POST http://localhost:8000/ \
|
|
179
|
+
-H "Content-Type: application/json" \
|
|
180
|
+
-d '{"jsonrpc": "2.0", "id": 1, "method": "getUser", "params": {"user_id": "42"}}'
|
|
181
|
+
|
|
182
|
+
# 服务发现
|
|
183
|
+
curl http://localhost:8000/openrpc.json
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## 完整文档
|
|
187
|
+
|
|
188
|
+
详细的用户文档请参阅 [docs/index.md](docs/zh/index.md)。
|
|
189
|
+
|
|
190
|
+
## 贡献
|
|
191
|
+
|
|
192
|
+
欢迎提交 Issue 和 Pull Request。
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
MIT
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Domain Context
|
|
2
|
+
|
|
3
|
+
Single-context project: one `CONTEXT.md` at repo root + `docs/adr/` for architectural decisions.
|
|
4
|
+
|
|
5
|
+
## CONTEXT.md
|
|
6
|
+
|
|
7
|
+
Project-level context: what the project does, key concepts, terminology, and patterns.
|
|
8
|
+
|
|
9
|
+
## docs/adr/
|
|
10
|
+
|
|
11
|
+
Architecture Decision Records documenting significant choices.
|
|
12
|
+
|
|
13
|
+
### ADR format
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
docs/adr/<YYYY-MM-DD>-<short-title>.md
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Example: `docs/adr/2024-01-15-use-pydantic-v2-for-validation.md`
|
|
20
|
+
|
|
21
|
+
### ADR template
|
|
22
|
+
|
|
23
|
+
```markdown
|
|
24
|
+
# ADR-001: Use Pydantic V2 for validation
|
|
25
|
+
|
|
26
|
+
**Status:** Accepted
|
|
27
|
+
**Date:** 2024-01-15
|
|
28
|
+
|
|
29
|
+
## Context
|
|
30
|
+
|
|
31
|
+
What problem prompted this decision?
|
|
32
|
+
|
|
33
|
+
## Decision
|
|
34
|
+
|
|
35
|
+
What was decided?
|
|
36
|
+
|
|
37
|
+
## Consequences
|
|
38
|
+
|
|
39
|
+
### Positive
|
|
40
|
+
- Benefit 1
|
|
41
|
+
|
|
42
|
+
### Negative
|
|
43
|
+
- Tradeoff 1
|
|
44
|
+
|
|
45
|
+
### Neutral
|
|
46
|
+
- Side effect 1
|
|
47
|
+
```
|