fastapi-openrpc 0.1.3__py3-none-any.whl
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/__init__.py +104 -0
- fastapi_openrpc/authentication.py +248 -0
- fastapi_openrpc/authorization.py +79 -0
- fastapi_openrpc/constants.py +43 -0
- fastapi_openrpc/decorator.py +517 -0
- fastapi_openrpc/document.py +329 -0
- fastapi_openrpc/errors.py +125 -0
- fastapi_openrpc/registry.py +132 -0
- fastapi_openrpc/router.py +905 -0
- fastapi_openrpc/types.py +99 -0
- fastapi_openrpc-0.1.3.dist-info/METADATA +25 -0
- fastapi_openrpc-0.1.3.dist-info/RECORD +13 -0
- fastapi_openrpc-0.1.3.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
"""@method() decorator and type inference."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import types
|
|
7
|
+
import typing
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from typing import Annotated, Any, cast, get_args, get_origin, get_type_hints
|
|
10
|
+
|
|
11
|
+
from fastapi_openrpc.registry import add
|
|
12
|
+
from fastapi_openrpc.types import MethodMeta, Require, _is_rpc_context_param
|
|
13
|
+
|
|
14
|
+
# ----------------------------------------------------------------------
|
|
15
|
+
# Type Inference
|
|
16
|
+
# ----------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _get_json_schema_type(python_type: type[Any] | None) -> str:
|
|
20
|
+
"""Map Python type to JSON Schema type string."""
|
|
21
|
+
if python_type is None:
|
|
22
|
+
return "object"
|
|
23
|
+
# Check type aliases
|
|
24
|
+
type_name = getattr(python_type, "__name__", str(python_type))
|
|
25
|
+
if type_name in ("str", "String"):
|
|
26
|
+
return "string"
|
|
27
|
+
if type_name in ("int", "Integer", "int64", "int32"):
|
|
28
|
+
return "integer"
|
|
29
|
+
if type_name in ("float", "Float", "double", "Number"):
|
|
30
|
+
return "number"
|
|
31
|
+
if type_name in ("bool", "Boolean"):
|
|
32
|
+
return "boolean"
|
|
33
|
+
if type_name in ("list", "List", "tuple", "Tuple"):
|
|
34
|
+
return "array"
|
|
35
|
+
if type_name in ("dict", "Dict", "Mapping"):
|
|
36
|
+
return "object"
|
|
37
|
+
return "object"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _infer_param_schema(annotation: type[Any] | None) -> dict[str, Any]:
|
|
41
|
+
"""Infer JSON Schema from a single parameter annotation.
|
|
42
|
+
|
|
43
|
+
- Pydantic BaseModel -> recursively use model_json_schema()
|
|
44
|
+
- Basic types -> direct mapping
|
|
45
|
+
- Optional[T] -> extract inner type
|
|
46
|
+
- List[T] / dict etc -> type: array / object
|
|
47
|
+
- No annotation -> {"type": "object"}
|
|
48
|
+
"""
|
|
49
|
+
import typing
|
|
50
|
+
|
|
51
|
+
if annotation is None:
|
|
52
|
+
return {"type": "object"}
|
|
53
|
+
|
|
54
|
+
# Prefer typing.get_origin/get_args (Python 3.8+), overriding getattr version
|
|
55
|
+
# because get_origin correctly handles Optional[str] (__origin__=None but
|
|
56
|
+
# get_origin returns Union)
|
|
57
|
+
try:
|
|
58
|
+
origin = typing.get_origin(annotation)
|
|
59
|
+
args = typing.get_args(annotation)
|
|
60
|
+
except Exception:
|
|
61
|
+
origin = getattr(annotation, "__origin__", None)
|
|
62
|
+
args = getattr(annotation, "__args__", ())
|
|
63
|
+
|
|
64
|
+
# Optional[X] -> X (Union with None)
|
|
65
|
+
if origin is not None:
|
|
66
|
+
# typing.Optional / typing.Union[X, None]
|
|
67
|
+
none_type = type(None)
|
|
68
|
+
non_none = [a for a in args if a is not none_type]
|
|
69
|
+
if len(non_none) == 1 and none_type in args:
|
|
70
|
+
# Optional[T]
|
|
71
|
+
return _infer_param_schema(non_none[0])
|
|
72
|
+
|
|
73
|
+
# List[T] / tuple
|
|
74
|
+
if origin is list or origin is tuple:
|
|
75
|
+
if args:
|
|
76
|
+
return {
|
|
77
|
+
"type": "array",
|
|
78
|
+
"items": _infer_param_schema(args[0]),
|
|
79
|
+
}
|
|
80
|
+
return {"type": "array"}
|
|
81
|
+
|
|
82
|
+
# Dict[str, T]
|
|
83
|
+
if origin is dict:
|
|
84
|
+
if len(args) >= 2:
|
|
85
|
+
return {
|
|
86
|
+
"type": "object",
|
|
87
|
+
"additionalProperties": _infer_param_schema(args[1]),
|
|
88
|
+
}
|
|
89
|
+
return {"type": "object"}
|
|
90
|
+
|
|
91
|
+
# Set[T]
|
|
92
|
+
if origin is set:
|
|
93
|
+
if args:
|
|
94
|
+
return {
|
|
95
|
+
"type": "array",
|
|
96
|
+
"items": _infer_param_schema(args[0]),
|
|
97
|
+
"uniqueItems": True,
|
|
98
|
+
}
|
|
99
|
+
return {"type": "array"}
|
|
100
|
+
|
|
101
|
+
# Pydantic V2 BaseModel -> expand fields to top-level properties
|
|
102
|
+
if _is_pydantic_model(annotation):
|
|
103
|
+
return _expand_pydantic_model_to_params(annotation)
|
|
104
|
+
|
|
105
|
+
# typing.NewType -> extract underlying type
|
|
106
|
+
if hasattr(annotation, "__supertype__"):
|
|
107
|
+
return _infer_param_schema(annotation.__supertype__)
|
|
108
|
+
|
|
109
|
+
return {"type": _get_json_schema_type(annotation)}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _is_pydantic_model(annotation: type[Any]) -> bool:
|
|
113
|
+
"""Check if annotation is a Pydantic BaseModel subclass."""
|
|
114
|
+
# Avoid importing pydantic at module level (reduce import overhead)
|
|
115
|
+
try:
|
|
116
|
+
from pydantic import BaseModel
|
|
117
|
+
|
|
118
|
+
return isinstance(annotation, type) and issubclass(annotation, BaseModel)
|
|
119
|
+
except ImportError:
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _unwrap_union(annotation: type[Any]) -> type[Any] | None:
|
|
124
|
+
"""Extract the first non-None type from a Union type (e.g., Optional[T], T | None)."""
|
|
125
|
+
try:
|
|
126
|
+
origin = typing.get_origin(annotation)
|
|
127
|
+
args = typing.get_args(annotation)
|
|
128
|
+
except (TypeError, AttributeError):
|
|
129
|
+
origin = getattr(annotation, "__origin__", None)
|
|
130
|
+
args = getattr(annotation, "__args__", ())
|
|
131
|
+
|
|
132
|
+
# Handle typing.Union (Optional[T]) and types.UnionType (T | None)
|
|
133
|
+
if origin is typing.Union or origin is types.UnionType:
|
|
134
|
+
for arg in args:
|
|
135
|
+
if arg is not type(None):
|
|
136
|
+
return cast(type[Any], arg)
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _expand_pydantic_model_to_params(model_cls: type) -> dict[str, Any]:
|
|
141
|
+
"""Expand a Pydantic BaseModel's fields into an OpenRPC params SchemaObject.
|
|
142
|
+
|
|
143
|
+
When a Pydantic model is used to describe parameter structure (e.g., handler
|
|
144
|
+
parameter is GetSchemaRequest), each field corresponds to one OpenRPC parameter.
|
|
145
|
+
|
|
146
|
+
For example, GetSchemaRequest(qualified_name: str) generates:
|
|
147
|
+
{
|
|
148
|
+
"type": "object",
|
|
149
|
+
"properties": {"qualified_name": {"type": "string", "description": "..."}},
|
|
150
|
+
"required": ["qualified_name"]
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
This way the JSON-RPC request still uses a flat structure:
|
|
154
|
+
{"jsonrpc": "2.0", "method": "getSchema", "params": {"qualified_name": "..."}}
|
|
155
|
+
"""
|
|
156
|
+
schema = _pydantic_model_schema(model_cls)
|
|
157
|
+
properties = schema.get("properties", {})
|
|
158
|
+
|
|
159
|
+
# Collect required fields (Pydantic model_json_schema omits top-level required;
|
|
160
|
+
# derive from fields)
|
|
161
|
+
model_required = schema.get("required", [])
|
|
162
|
+
if not model_required:
|
|
163
|
+
# Get required info via model_fields
|
|
164
|
+
try:
|
|
165
|
+
if hasattr(model_cls, "model_fields"):
|
|
166
|
+
model_required = [
|
|
167
|
+
name
|
|
168
|
+
for name, field_info in model_cls.model_fields.items()
|
|
169
|
+
if field_info.is_required()
|
|
170
|
+
]
|
|
171
|
+
except Exception:
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
"type": "object",
|
|
176
|
+
"properties": properties,
|
|
177
|
+
"required": model_required,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _pydantic_model_schema(model_cls: type) -> dict[str, Any]:
|
|
182
|
+
"""Generate JSON Schema using Pydantic V2 model_json_schema()."""
|
|
183
|
+
try:
|
|
184
|
+
from pydantic import BaseModel
|
|
185
|
+
|
|
186
|
+
if not issubclass(model_cls, BaseModel):
|
|
187
|
+
return {"type": "object"}
|
|
188
|
+
# Pydantic V2
|
|
189
|
+
return model_cls.model_json_schema()
|
|
190
|
+
except (ImportError, AttributeError):
|
|
191
|
+
# Pydantic V1 fallback
|
|
192
|
+
try:
|
|
193
|
+
return cast("BaseModel", model_cls).schema()
|
|
194
|
+
except AttributeError:
|
|
195
|
+
return {"type": "object"}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def infer_params_schema(func: Callable[..., Any]) -> dict[str, Any]:
|
|
199
|
+
"""Infer parameter JSON Schema from function signature.
|
|
200
|
+
|
|
201
|
+
Uses typing.get_type_hints() to resolve PEP 563 deferred annotations
|
|
202
|
+
(from __future__ import annotations), ensuring correct type inference even in
|
|
203
|
+
modules with that future directive.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
func: The decorated handler function.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
OpenRPC params SchemaObject:
|
|
210
|
+
{
|
|
211
|
+
"type": "object",
|
|
212
|
+
"properties": {<name>: <schema>},
|
|
213
|
+
"required": [<name>, ...]
|
|
214
|
+
}
|
|
215
|
+
"""
|
|
216
|
+
# get_type_hints() resolves deferred annotation strings;
|
|
217
|
+
# include_extras=True preserves Annotated wrappers
|
|
218
|
+
type_hints: dict[str, Any]
|
|
219
|
+
try:
|
|
220
|
+
type_hints = get_type_hints(func, include_extras=True)
|
|
221
|
+
except NameError:
|
|
222
|
+
# Some annotations cannot be resolved (e.g., locally defined classes);
|
|
223
|
+
# fall back to inspect.signature, still try to resolve types accessible from
|
|
224
|
+
# func.__globals__ (e.g., RpcContext)
|
|
225
|
+
sig = inspect.signature(func)
|
|
226
|
+
type_hints = {}
|
|
227
|
+
for p in sig.parameters.values():
|
|
228
|
+
if p.annotation is inspect.Parameter.empty:
|
|
229
|
+
continue
|
|
230
|
+
ann: Any = p.annotation
|
|
231
|
+
# Try to resolve string annotations (only from function global scope,
|
|
232
|
+
# not eval local scope)
|
|
233
|
+
if isinstance(ann, str):
|
|
234
|
+
try:
|
|
235
|
+
ann = eval(ann, func.__globals__)
|
|
236
|
+
except Exception:
|
|
237
|
+
pass # Keep string; _is_rpc_context_param will handle it specially
|
|
238
|
+
if get_origin(ann) is Annotated:
|
|
239
|
+
ann = get_args(ann)[0]
|
|
240
|
+
type_hints[p.name] = ann
|
|
241
|
+
|
|
242
|
+
sig = inspect.signature(func)
|
|
243
|
+
properties: dict[str, Any] = {}
|
|
244
|
+
required: list[str] = []
|
|
245
|
+
|
|
246
|
+
for param_name, param in sig.parameters.items():
|
|
247
|
+
# Skip FastAPI Depends parameters (injected by OpenRpcRouter, not part of RPC params)
|
|
248
|
+
from fastapi.params import Depends as FastAPIDepends
|
|
249
|
+
|
|
250
|
+
if isinstance(param.default, FastAPIDepends):
|
|
251
|
+
continue
|
|
252
|
+
|
|
253
|
+
annotation = type_hints.get(param_name)
|
|
254
|
+
|
|
255
|
+
# Skip RpcContext parameters (injected by dispatcher, not part of RPC params)
|
|
256
|
+
if _is_rpc_context_param(annotation):
|
|
257
|
+
continue
|
|
258
|
+
|
|
259
|
+
# Pydantic BaseModel parameter: expand model fields to top-level.
|
|
260
|
+
# For example handler(params: GetSchemaRequest) -> params_schema only has
|
|
261
|
+
# qualified_name field, not nested in params object. This way JSON-RPC flat
|
|
262
|
+
# params {"qualified_name": "..."} can pass through _validate_params directly,
|
|
263
|
+
# and dispatcher packs it into a model instance at runtime.
|
|
264
|
+
if annotation is not None and _is_pydantic_model(cast(type[Any], annotation)):
|
|
265
|
+
expanded = _expand_pydantic_model_to_params(cast(type[Any], annotation))
|
|
266
|
+
# Merge fields into top-level properties and required
|
|
267
|
+
for fname, fschema in expanded.get("properties", {}).items():
|
|
268
|
+
properties[fname] = fschema
|
|
269
|
+
for fname in expanded.get("required", []):
|
|
270
|
+
required.append(fname)
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
schema = _infer_param_schema(annotation)
|
|
274
|
+
properties[param_name] = schema
|
|
275
|
+
|
|
276
|
+
# required: no default value and not Optional
|
|
277
|
+
if param.default is inspect.Parameter.empty:
|
|
278
|
+
required.append(param_name)
|
|
279
|
+
elif param.default is not None:
|
|
280
|
+
# Has default value -> set as optional, provide default
|
|
281
|
+
properties[param_name]["default"] = param.default
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
"type": "object",
|
|
285
|
+
"properties": properties,
|
|
286
|
+
"required": list(dict.fromkeys(required)),
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def infer_result_schema(func: Callable[..., Any]) -> dict[str, Any]:
|
|
291
|
+
"""Infer return value JSON Schema from function signature.
|
|
292
|
+
|
|
293
|
+
Uses typing.get_type_hints() to resolve PEP 563 deferred annotations.
|
|
294
|
+
|
|
295
|
+
- Pydantic BaseModel -> model_json_schema()
|
|
296
|
+
- Basic types -> direct mapping
|
|
297
|
+
- No annotation -> {"type": "object"}
|
|
298
|
+
"""
|
|
299
|
+
import typing
|
|
300
|
+
|
|
301
|
+
# Use get_type_hints to resolve PEP 563 deferred annotations
|
|
302
|
+
try:
|
|
303
|
+
type_hints = typing.get_type_hints(func)
|
|
304
|
+
return_type = type_hints.get("return")
|
|
305
|
+
except Exception:
|
|
306
|
+
sig = inspect.signature(func)
|
|
307
|
+
return_type = sig.return_annotation
|
|
308
|
+
|
|
309
|
+
if return_type is None:
|
|
310
|
+
return {"type": "object"}
|
|
311
|
+
|
|
312
|
+
# Pydantic BaseModel
|
|
313
|
+
if _is_pydantic_model(return_type):
|
|
314
|
+
return _pydantic_model_schema(return_type)
|
|
315
|
+
|
|
316
|
+
# typing.List[T]
|
|
317
|
+
try:
|
|
318
|
+
origin = typing.get_origin(return_type)
|
|
319
|
+
args = typing.get_args(return_type)
|
|
320
|
+
except Exception:
|
|
321
|
+
origin = getattr(return_type, "__origin__", None)
|
|
322
|
+
args = getattr(return_type, "__args__", ())
|
|
323
|
+
if origin is list or origin is tuple:
|
|
324
|
+
if args:
|
|
325
|
+
return {
|
|
326
|
+
"type": "array",
|
|
327
|
+
"items": _infer_param_schema(args[0]),
|
|
328
|
+
}
|
|
329
|
+
return {"type": "array"}
|
|
330
|
+
|
|
331
|
+
# Union type (e.g., str | None, int | None) -- extract non-None type
|
|
332
|
+
# _infer_param_schema already handles Optional[T], reuse it
|
|
333
|
+
if origin is not None:
|
|
334
|
+
none_type = type(None)
|
|
335
|
+
non_none = [a for a in args if a is not none_type]
|
|
336
|
+
if len(non_none) == 1 and none_type in args:
|
|
337
|
+
return _infer_param_schema(non_none[0])
|
|
338
|
+
|
|
339
|
+
return {"type": _get_json_schema_type(return_type)}
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
# ----------------------------------------------------------------------
|
|
343
|
+
# @method() Decorator
|
|
344
|
+
# ----------------------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def method(
|
|
348
|
+
*,
|
|
349
|
+
name: str | None = None,
|
|
350
|
+
tags: list[str] | None = None,
|
|
351
|
+
params_schema: dict[str, Any] | None = None,
|
|
352
|
+
params_model: type | None = None,
|
|
353
|
+
result_schema: dict[str, Any] | None = None,
|
|
354
|
+
result_model: type | None = None,
|
|
355
|
+
summary: str | None = None,
|
|
356
|
+
description: str | None = None,
|
|
357
|
+
deprecated: bool = False,
|
|
358
|
+
examples: list[dict[str, Any]] | None = None,
|
|
359
|
+
required_roles: list[str] | None = None,
|
|
360
|
+
require: Require | None = None,
|
|
361
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
362
|
+
"""Declaratively register a JSON-RPC method.
|
|
363
|
+
|
|
364
|
+
Example::
|
|
365
|
+
|
|
366
|
+
from pydantic import BaseModel
|
|
367
|
+
|
|
368
|
+
class User(BaseModel):
|
|
369
|
+
id: str
|
|
370
|
+
name: str
|
|
371
|
+
|
|
372
|
+
@method(name="getUser", result_model=User)
|
|
373
|
+
async def get_user(ctx: RpcContext, user_id: str) -> User:
|
|
374
|
+
'''Get user information.'''
|
|
375
|
+
return User(id=user_id, name="Alice")
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
name: RPC method name. Defaults to function __name__.
|
|
379
|
+
tags: OpenRPC method tag grouping.
|
|
380
|
+
params_schema: Explicit parameter schema, overrides automatic inference.
|
|
381
|
+
result_schema: Explicit return value schema, overrides automatic inference.
|
|
382
|
+
result_model: Pydantic BaseModel subclass; automatically infers result_schema and
|
|
383
|
+
calls .model_dump() to serialize the return value.
|
|
384
|
+
summary: Method short description (for documentation).
|
|
385
|
+
description: Method detailed description. Inferred from docstring by default.
|
|
386
|
+
deprecated: Whether to mark as deprecated.
|
|
387
|
+
examples: List of parameter and return value examples.
|
|
388
|
+
required_roles: List of roles required by the method; None means no permission check.
|
|
389
|
+
require: Role combination semantics, Require.ANY (default) or Require.ALL.
|
|
390
|
+
"""
|
|
391
|
+
# rpc.discover is a reserved method name
|
|
392
|
+
_name = name or ""
|
|
393
|
+
if _name == "rpc.discover":
|
|
394
|
+
raise ValueError("'rpc.discover' is a reserved method name and cannot be registered.")
|
|
395
|
+
|
|
396
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
397
|
+
# Infer schema (pass func directly; let get_type_hints resolve PEP 563 annotations)
|
|
398
|
+
inferred_params = infer_params_schema(func)
|
|
399
|
+
inferred_result = infer_result_schema(func)
|
|
400
|
+
|
|
401
|
+
# Infer description from docstring
|
|
402
|
+
inferred_description = inspect.getdoc(func)
|
|
403
|
+
|
|
404
|
+
final_name = name or func.__name__
|
|
405
|
+
final_description = description if description is not None else inferred_description
|
|
406
|
+
|
|
407
|
+
# result_model handling: generate schema + mark serialization type
|
|
408
|
+
final_result_schema = result_schema
|
|
409
|
+
resolved_result_model: type | None = None
|
|
410
|
+
|
|
411
|
+
# Priority: explicit argument > automatic inference
|
|
412
|
+
if resolved_result_model is None:
|
|
413
|
+
# Infer result_model from return type annotation
|
|
414
|
+
# (supports -> UnifiedSchemaResponse syntax)
|
|
415
|
+
try:
|
|
416
|
+
import typing
|
|
417
|
+
|
|
418
|
+
type_hints = typing.get_type_hints(func)
|
|
419
|
+
return_type = type_hints.get("return")
|
|
420
|
+
if return_type is not None:
|
|
421
|
+
# Handle Union types like Optional[User], User | None
|
|
422
|
+
unwrapped = _unwrap_union(return_type)
|
|
423
|
+
if _is_pydantic_model(return_type):
|
|
424
|
+
resolved_result_model = cast(type[Any], return_type)
|
|
425
|
+
if result_schema is None:
|
|
426
|
+
final_result_schema = _pydantic_model_schema(
|
|
427
|
+
cast(type[Any], return_type)
|
|
428
|
+
)
|
|
429
|
+
elif unwrapped is not None and _is_pydantic_model(unwrapped):
|
|
430
|
+
# Union type contains Pydantic model (e.g., User | None)
|
|
431
|
+
resolved_result_model = unwrapped
|
|
432
|
+
if result_schema is None:
|
|
433
|
+
final_result_schema = _pydantic_model_schema(unwrapped)
|
|
434
|
+
except Exception:
|
|
435
|
+
pass # Silent fallback on parse failure; maintain backward compatibility
|
|
436
|
+
|
|
437
|
+
# Explicit result_model passed but result_schema not passed -> generate schema
|
|
438
|
+
if result_model is not None and result_schema is None:
|
|
439
|
+
final_result_schema = _pydantic_model_schema(result_model)
|
|
440
|
+
|
|
441
|
+
# params_model inference: detect first Pydantic BaseModel parameter in handler signature
|
|
442
|
+
resolved_params_model: type | None = None
|
|
443
|
+
if params_model is None:
|
|
444
|
+
# params_model not explicitly passed; infer from function signature
|
|
445
|
+
import typing as _typing
|
|
446
|
+
|
|
447
|
+
try:
|
|
448
|
+
_hints = _typing.get_type_hints(func, include_extras=True)
|
|
449
|
+
except Exception:
|
|
450
|
+
_hints = {}
|
|
451
|
+
sig = inspect.signature(func)
|
|
452
|
+
|
|
453
|
+
for _param_name, _param in sig.parameters.items():
|
|
454
|
+
_annotation = _hints.get(_param_name)
|
|
455
|
+
# Try to resolve string annotations
|
|
456
|
+
if _annotation is None or isinstance(_annotation, str):
|
|
457
|
+
_raw = sig.parameters[_param_name].annotation
|
|
458
|
+
if not isinstance(_raw, str):
|
|
459
|
+
_raw = str(_raw)
|
|
460
|
+
try:
|
|
461
|
+
_resolved = eval(_raw, func.__globals__)
|
|
462
|
+
if _annotation is None:
|
|
463
|
+
_annotation = _resolved
|
|
464
|
+
except Exception:
|
|
465
|
+
pass
|
|
466
|
+
if _is_rpc_context_param(_annotation):
|
|
467
|
+
continue
|
|
468
|
+
from fastapi.params import Depends as _FastAPIDepends
|
|
469
|
+
|
|
470
|
+
if isinstance(_param.default, _FastAPIDepends):
|
|
471
|
+
continue
|
|
472
|
+
if _annotation is not None and _is_pydantic_model(cast(type[Any], _annotation)):
|
|
473
|
+
if resolved_params_model is None:
|
|
474
|
+
# Only take the first Pydantic parameter;
|
|
475
|
+
# subsequent params_model remain None
|
|
476
|
+
resolved_params_model = cast(type, _annotation)
|
|
477
|
+
else:
|
|
478
|
+
# Multiple Pydantic parameters: degrade to inline behavior (no params_model)
|
|
479
|
+
resolved_params_model = None
|
|
480
|
+
break
|
|
481
|
+
else:
|
|
482
|
+
resolved_params_model = params_model
|
|
483
|
+
|
|
484
|
+
# Handle required_roles: deduplicate + lowercase
|
|
485
|
+
final_required_roles: list[str] = []
|
|
486
|
+
if required_roles is not None:
|
|
487
|
+
final_required_roles = list(dict.fromkeys(r.lower() for r in required_roles))
|
|
488
|
+
|
|
489
|
+
# Handle require: default Require.ANY
|
|
490
|
+
final_require: Require = Require.ANY
|
|
491
|
+
if require is not None:
|
|
492
|
+
final_require = require
|
|
493
|
+
|
|
494
|
+
meta = MethodMeta(
|
|
495
|
+
name=final_name,
|
|
496
|
+
func=func,
|
|
497
|
+
params_schema=(params_schema if params_schema is not None else inferred_params),
|
|
498
|
+
result_schema=(
|
|
499
|
+
final_result_schema if final_result_schema is not None else inferred_result
|
|
500
|
+
),
|
|
501
|
+
result_model=resolved_result_model,
|
|
502
|
+
params_model=resolved_params_model,
|
|
503
|
+
tags=list(tags) if tags else [],
|
|
504
|
+
summary=summary,
|
|
505
|
+
description=final_description,
|
|
506
|
+
deprecated=deprecated,
|
|
507
|
+
examples=list(examples) if examples else [],
|
|
508
|
+
required_roles=final_required_roles,
|
|
509
|
+
require=final_require,
|
|
510
|
+
)
|
|
511
|
+
add(meta)
|
|
512
|
+
return func
|
|
513
|
+
|
|
514
|
+
return decorator
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
__all__ = ["method", "infer_params_schema", "infer_result_schema"]
|