python-jsonrpc-lib 0.3.1__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.
- jsonrpc/__init__.py +83 -0
- jsonrpc/errors.py +131 -0
- jsonrpc/jsonrpc.py +881 -0
- jsonrpc/method.py +561 -0
- jsonrpc/openapi.py +505 -0
- jsonrpc/py.typed +0 -0
- jsonrpc/request.py +164 -0
- jsonrpc/response.py +169 -0
- jsonrpc/types.py +53 -0
- jsonrpc/validation.py +297 -0
- python_jsonrpc_lib-0.3.1.dist-info/METADATA +141 -0
- python_jsonrpc_lib-0.3.1.dist-info/RECORD +14 -0
- python_jsonrpc_lib-0.3.1.dist-info/WHEEL +4 -0
- python_jsonrpc_lib-0.3.1.dist-info/licenses/LICENSE +21 -0
jsonrpc/method.py
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
"""Method and MethodGroup classes for JSON-RPC methods."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
from dataclasses import is_dataclass
|
|
6
|
+
from typing import (
|
|
7
|
+
TYPE_CHECKING,
|
|
8
|
+
Any,
|
|
9
|
+
Union,
|
|
10
|
+
get_type_hints,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from .validation import (
|
|
14
|
+
_unwrap_optional,
|
|
15
|
+
validate_params,
|
|
16
|
+
validate_result_type,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from .jsonrpc import JSONRPC
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Method:
|
|
24
|
+
"""Base class for RPC methods.
|
|
25
|
+
|
|
26
|
+
Subclasses must:
|
|
27
|
+
- Implement `execute(self, params: ParamsType) -> ResultType` with type hints
|
|
28
|
+
- ParamsType must be a dataclass (or None for no params)
|
|
29
|
+
- ResultType can be any type
|
|
30
|
+
|
|
31
|
+
Type hints are REQUIRED and will be automatically extracted to set
|
|
32
|
+
params_type and result_type attributes.
|
|
33
|
+
|
|
34
|
+
Name is specified during registration, not in the class.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
params_type: Auto-extracted from execute() params hint
|
|
38
|
+
result_type: Auto-extracted from execute() return hint
|
|
39
|
+
rpc: Reference to JSONRPC instance (injected on registration)
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
params_type: type = type(None)
|
|
43
|
+
result_type: type = type(None)
|
|
44
|
+
rpc: 'JSONRPC'
|
|
45
|
+
accepts_context: bool = False
|
|
46
|
+
context_type: type | None = None
|
|
47
|
+
_is_async_method: bool = False
|
|
48
|
+
|
|
49
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
50
|
+
"""Extract params_type and result_type from execute() signature."""
|
|
51
|
+
super().__init_subclass__(**kwargs)
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
hints = get_type_hints(cls.execute)
|
|
55
|
+
|
|
56
|
+
sig = inspect.signature(cls.execute)
|
|
57
|
+
params_list = list(sig.parameters.values())
|
|
58
|
+
|
|
59
|
+
if len(params_list) < 2:
|
|
60
|
+
raise TypeError(f"{cls.__name__}.execute() must have 'params' parameter")
|
|
61
|
+
|
|
62
|
+
params_param = params_list[1] # Skip 'self'
|
|
63
|
+
|
|
64
|
+
if params_param.name != 'params':
|
|
65
|
+
raise TypeError(
|
|
66
|
+
f"{cls.__name__}.execute() must have 'params' as second parameter, got '{params_param.name}'"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if 'params' not in hints:
|
|
70
|
+
raise TypeError(f"{cls.__name__}.execute() must have type hint for 'params' parameter")
|
|
71
|
+
|
|
72
|
+
params_type = hints['params']
|
|
73
|
+
|
|
74
|
+
params_type = _unwrap_optional(params_type)
|
|
75
|
+
|
|
76
|
+
if params_type is not type(None) and not is_dataclass(params_type):
|
|
77
|
+
raise TypeError(f'{cls.__name__}.execute() params type must be a dataclass or None, got {params_type}')
|
|
78
|
+
|
|
79
|
+
cls.params_type = params_type
|
|
80
|
+
|
|
81
|
+
if 'context' in sig.parameters:
|
|
82
|
+
if 'context' not in hints:
|
|
83
|
+
raise TypeError(f"{cls.__name__}.execute() has 'context' parameter but no type hint")
|
|
84
|
+
|
|
85
|
+
cls.accepts_context = True
|
|
86
|
+
cls.context_type = hints['context']
|
|
87
|
+
|
|
88
|
+
context_param = params_list[2]
|
|
89
|
+
if context_param.name != 'context':
|
|
90
|
+
raise TypeError(
|
|
91
|
+
f"{cls.__name__}.execute() third parameter must be 'context', got '{context_param.name}'"
|
|
92
|
+
)
|
|
93
|
+
else:
|
|
94
|
+
cls.accepts_context = False
|
|
95
|
+
cls.context_type = None
|
|
96
|
+
|
|
97
|
+
if 'return' not in hints:
|
|
98
|
+
raise TypeError(f'{cls.__name__}.execute() must have return type annotation')
|
|
99
|
+
|
|
100
|
+
cls.result_type = hints['return']
|
|
101
|
+
cls._is_async_method = asyncio.iscoroutinefunction(cls.execute)
|
|
102
|
+
|
|
103
|
+
except TypeError:
|
|
104
|
+
raise
|
|
105
|
+
except Exception as e:
|
|
106
|
+
# Wrap other exceptions
|
|
107
|
+
raise TypeError(f'Failed to infer types for {cls.__name__}: {e}') from e
|
|
108
|
+
|
|
109
|
+
def execute(self, params: Any, context: Any = None) -> Any:
|
|
110
|
+
"""Execute the method with validated params and optional context.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
params: Validated dataclass instance, or None if params_type is None
|
|
114
|
+
context: Optional context object (only passed if accepts_context=True)
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Method result (any JSON-serializable value)
|
|
118
|
+
"""
|
|
119
|
+
raise NotImplementedError(f'Method {self.__class__.__name__} must implement execute()')
|
|
120
|
+
|
|
121
|
+
def _is_async(self) -> bool:
|
|
122
|
+
"""Check if execute() is an async method."""
|
|
123
|
+
return self._is_async_method
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class MethodGroup:
|
|
127
|
+
"""Hierarchical container for methods with support for nesting and middleware.
|
|
128
|
+
|
|
129
|
+
MethodGroup acts as a tree structure supporting:
|
|
130
|
+
- Nested subgroups (e.g., sudo -> user -> management)
|
|
131
|
+
- Method registration with arbitrary names
|
|
132
|
+
- Middleware via execute_method() override
|
|
133
|
+
- Built-in routing and dispatch (replaces Dispatcher)
|
|
134
|
+
|
|
135
|
+
Name is set during registration, not in constructor.
|
|
136
|
+
|
|
137
|
+
Attributes:
|
|
138
|
+
name: Group name (read-only property, None for root group)
|
|
139
|
+
rpc: Reference to JSONRPC instance (injected on registration)
|
|
140
|
+
|
|
141
|
+
Examples:
|
|
142
|
+
# Basic usage
|
|
143
|
+
math = MethodGroup()
|
|
144
|
+
math.register('add', AddMethod())
|
|
145
|
+
|
|
146
|
+
# Nested groups
|
|
147
|
+
sudo = MethodGroup()
|
|
148
|
+
user = MethodGroup()
|
|
149
|
+
user.register('adduser', AddUserMethod())
|
|
150
|
+
sudo.register('user', user)
|
|
151
|
+
|
|
152
|
+
# Custom names
|
|
153
|
+
math.register('add_2', AddXMethod(2))
|
|
154
|
+
math.register('add_5', AddXMethod(5))
|
|
155
|
+
|
|
156
|
+
# Middleware
|
|
157
|
+
class SudoGroup(MethodGroup):
|
|
158
|
+
def execute_method(self, method, params):
|
|
159
|
+
if not self._check_auth():
|
|
160
|
+
raise PermissionError("Requires sudo")
|
|
161
|
+
return super().execute_method(method, params)
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
rpc: 'JSONRPC'
|
|
165
|
+
context_type: type | None = None # Extracted from execute_method() signature
|
|
166
|
+
accepts_context_param: bool = True # Whether execute_method accepts context parameter
|
|
167
|
+
|
|
168
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
169
|
+
"""Extract context_type and accepts_context_param from execute_method() signature."""
|
|
170
|
+
super().__init_subclass__(**kwargs)
|
|
171
|
+
|
|
172
|
+
# Check if execute_method is overridden
|
|
173
|
+
if cls.execute_method is MethodGroup.execute_method:
|
|
174
|
+
# Not overridden, base implementation accepts context
|
|
175
|
+
cls.context_type = None
|
|
176
|
+
cls.accepts_context_param = True
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
sig = inspect.signature(cls.execute_method)
|
|
181
|
+
|
|
182
|
+
if 'context' in sig.parameters:
|
|
183
|
+
# MethodGroup has context parameter - can pass context to methods
|
|
184
|
+
cls.accepts_context_param = True
|
|
185
|
+
hints = get_type_hints(cls.execute_method)
|
|
186
|
+
|
|
187
|
+
if 'context' not in hints:
|
|
188
|
+
# context parameter without type hint → no validation (context_type = None)
|
|
189
|
+
# This allows: def execute_method(self, method, params, context=None)
|
|
190
|
+
cls.context_type = None
|
|
191
|
+
else:
|
|
192
|
+
# context parameter with type hint → validate type
|
|
193
|
+
# This allows: def execute_method(self, method, params, context: AdminContext)
|
|
194
|
+
cls.context_type = hints['context']
|
|
195
|
+
else:
|
|
196
|
+
# execute_method overridden but no context parameter
|
|
197
|
+
# Cannot pass context to methods!
|
|
198
|
+
cls.accepts_context_param = False
|
|
199
|
+
cls.context_type = None
|
|
200
|
+
|
|
201
|
+
except Exception as e:
|
|
202
|
+
raise TypeError(f'Failed to infer context_type for {cls.__name__}: {e}') from e
|
|
203
|
+
|
|
204
|
+
def __init__(self) -> None:
|
|
205
|
+
"""Initialize method group (name set during registration)."""
|
|
206
|
+
self._name: str | None = None # Internal, set by parent
|
|
207
|
+
self._methods: dict[str, Method] = {}
|
|
208
|
+
self._subgroups: dict[str, MethodGroup] = {}
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def name(self) -> str | None:
|
|
212
|
+
"""Get group name (read-only)."""
|
|
213
|
+
return self._name
|
|
214
|
+
|
|
215
|
+
def register(self, name: str, target: Union[Method, 'MethodGroup']) -> None:
|
|
216
|
+
"""Register method or subgroup instance.
|
|
217
|
+
|
|
218
|
+
Unified registration API supporting both methods and subgroups.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
name: Registration name (must not contain '.', cannot be empty or None)
|
|
222
|
+
target: Method instance or MethodGroup instance
|
|
223
|
+
|
|
224
|
+
Raises:
|
|
225
|
+
ValueError: If name is None, empty, contains '.', or already registered
|
|
226
|
+
TypeError: If target is a class or invalid type
|
|
227
|
+
|
|
228
|
+
Examples:
|
|
229
|
+
group.register('add', AddMethod())
|
|
230
|
+
group.register('user', user_subgroup)
|
|
231
|
+
"""
|
|
232
|
+
# Validate name
|
|
233
|
+
if name is None:
|
|
234
|
+
raise ValueError('Name cannot be None in MethodGroup.register()')
|
|
235
|
+
if not name or name == '':
|
|
236
|
+
raise ValueError('Name cannot be empty string. Use None only in JSONRPC.register()')
|
|
237
|
+
if '.' in name:
|
|
238
|
+
raise ValueError(f"Name cannot contain '.': '{name}'")
|
|
239
|
+
if name in self._methods:
|
|
240
|
+
raise ValueError(f"Method '{name}' already registered")
|
|
241
|
+
if name in self._subgroups:
|
|
242
|
+
raise ValueError(f"Subgroup '{name}' already registered")
|
|
243
|
+
|
|
244
|
+
if isinstance(target, type):
|
|
245
|
+
raise TypeError(
|
|
246
|
+
f"Cannot register class '{target.__name__}'. "
|
|
247
|
+
f"Must register instance: register('{name}', {target.__name__}())"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Validate context_type compatibility
|
|
251
|
+
if isinstance(target, Method):
|
|
252
|
+
# Fail-fast: check if method is already registered elsewhere
|
|
253
|
+
if hasattr(target, 'rpc'):
|
|
254
|
+
raise ValueError(
|
|
255
|
+
f"Method instance '{target.__class__.__name__}' is already registered. "
|
|
256
|
+
f'Create a new instance for each registration.'
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Check if group can pass context to method
|
|
260
|
+
if target.accepts_context and not self.accepts_context_param:
|
|
261
|
+
raise TypeError(
|
|
262
|
+
f'Cannot register {target.__class__.__name__}: '
|
|
263
|
+
f'method requires context but group execute_method() does not accept context parameter. '
|
|
264
|
+
f'Add context parameter to execute_method: '
|
|
265
|
+
f'def execute_method(self, method, params, context=None)'
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Check context type hierarchy
|
|
269
|
+
if target.accepts_context and target.context_type is not None and self.context_type is not None:
|
|
270
|
+
if not issubclass(target.context_type, self.context_type):
|
|
271
|
+
raise TypeError(
|
|
272
|
+
f'Cannot register {target.__class__.__name__}: '
|
|
273
|
+
f'method context_type {target.context_type.__name__} must be '
|
|
274
|
+
f'subclass of group context_type {self.context_type.__name__}'
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
elif isinstance(target, MethodGroup):
|
|
278
|
+
if target.context_type is not None and self.context_type is not None:
|
|
279
|
+
if not issubclass(target.context_type, self.context_type):
|
|
280
|
+
raise TypeError(
|
|
281
|
+
f'Cannot register {target.__class__.__name__}: '
|
|
282
|
+
f'group context_type {target.context_type.__name__} must be '
|
|
283
|
+
f'subclass of parent context_type {self.context_type.__name__}'
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Register based on type
|
|
287
|
+
if isinstance(target, MethodGroup):
|
|
288
|
+
# Subgroup
|
|
289
|
+
target._name = name
|
|
290
|
+
if hasattr(self, 'rpc') and self.rpc is not None:
|
|
291
|
+
target._inject_rpc(self.rpc)
|
|
292
|
+
self._subgroups[name] = target
|
|
293
|
+
|
|
294
|
+
elif isinstance(target, Method):
|
|
295
|
+
# Method instance
|
|
296
|
+
if hasattr(self, 'rpc') and self.rpc is not None:
|
|
297
|
+
target.rpc = self.rpc
|
|
298
|
+
self._methods[name] = target
|
|
299
|
+
|
|
300
|
+
else:
|
|
301
|
+
raise TypeError(f'Expected Method or MethodGroup instance, got {type(target).__name__}')
|
|
302
|
+
|
|
303
|
+
def unregister(self, name: str) -> None:
|
|
304
|
+
"""Unregister a method or subgroup by name.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
name: Method or subgroup name (not a path)
|
|
308
|
+
|
|
309
|
+
Raises:
|
|
310
|
+
KeyError: If name not found in either methods or subgroups
|
|
311
|
+
"""
|
|
312
|
+
if name in self._methods:
|
|
313
|
+
del self._methods[name]
|
|
314
|
+
elif name in self._subgroups:
|
|
315
|
+
del self._subgroups[name]
|
|
316
|
+
else:
|
|
317
|
+
raise KeyError(f"'{name}' not found in group '{self._name}'")
|
|
318
|
+
|
|
319
|
+
def resolve_path(self, path: str) -> tuple['MethodGroup', Method]:
|
|
320
|
+
"""Resolve method path to (group, method) tuple.
|
|
321
|
+
|
|
322
|
+
This is the core routing logic that replaces Dispatcher._resolve_method().
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
path: Dot-separated path (e.g., "add", "user.add", "sudo.user.addgroup")
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Tuple of (final_group, method)
|
|
329
|
+
|
|
330
|
+
Raises:
|
|
331
|
+
KeyError: If path not found (method or subgroup missing)
|
|
332
|
+
|
|
333
|
+
Examples:
|
|
334
|
+
"add" → (self, self._methods["add"])
|
|
335
|
+
"user.add" → (user_group, user_group._methods["add"])
|
|
336
|
+
"sudo.user.add" → (user_group, user_group._methods["add"])
|
|
337
|
+
"""
|
|
338
|
+
from .errors import MethodNotFoundError
|
|
339
|
+
|
|
340
|
+
parts = path.split('.')
|
|
341
|
+
|
|
342
|
+
# Single name - look in this group
|
|
343
|
+
if len(parts) == 1:
|
|
344
|
+
method = self._methods.get(path)
|
|
345
|
+
if method:
|
|
346
|
+
return (self, method)
|
|
347
|
+
raise MethodNotFoundError(f"Method '{path}' not found in group '{self._name}'")
|
|
348
|
+
|
|
349
|
+
# Multi-part path - navigate subgroups
|
|
350
|
+
first, *rest = parts
|
|
351
|
+
|
|
352
|
+
if first in self._subgroups:
|
|
353
|
+
return self._subgroups[first].resolve_path('.'.join(rest))
|
|
354
|
+
|
|
355
|
+
raise MethodNotFoundError(f"Path '{path}' not found: no subgroup '{first}' in group '{self._name}'")
|
|
356
|
+
|
|
357
|
+
def dispatch(
|
|
358
|
+
self,
|
|
359
|
+
path: str,
|
|
360
|
+
params: list[Any] | dict[str, Any] | None,
|
|
361
|
+
id: str | int | None,
|
|
362
|
+
validate_result: bool = False,
|
|
363
|
+
context: Any = None,
|
|
364
|
+
) -> Any:
|
|
365
|
+
"""Dispatch method call synchronously (with middleware support).
|
|
366
|
+
|
|
367
|
+
This method replaces Dispatcher.dispatch(). It can be overridden
|
|
368
|
+
by subclasses to implement middleware logic.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
path: Method path (e.g., "add", "user.add")
|
|
372
|
+
params: Method parameters
|
|
373
|
+
id: Request ID
|
|
374
|
+
validate_result: Whether to validate result type
|
|
375
|
+
context: Optional context object
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
Method result
|
|
379
|
+
|
|
380
|
+
Raises:
|
|
381
|
+
MethodNotFoundError: If method not found
|
|
382
|
+
InvalidParamsError: If params validation fails
|
|
383
|
+
RuntimeError: If method is async (use dispatch_async instead)
|
|
384
|
+
"""
|
|
385
|
+
group, method = self.resolve_path(path)
|
|
386
|
+
|
|
387
|
+
validated_params = validate_params(params, method.params_type)
|
|
388
|
+
|
|
389
|
+
if method._is_async():
|
|
390
|
+
raise RuntimeError(f"Method '{path}' is async, use dispatch_async() instead")
|
|
391
|
+
|
|
392
|
+
# Execute via middleware hook with context
|
|
393
|
+
result = group.execute_method(method, validated_params, context=context)
|
|
394
|
+
|
|
395
|
+
# Validate result if requested
|
|
396
|
+
if validate_result and method.result_type is not None:
|
|
397
|
+
validate_result_type(result, method.result_type)
|
|
398
|
+
|
|
399
|
+
return result
|
|
400
|
+
|
|
401
|
+
async def dispatch_async(
|
|
402
|
+
self,
|
|
403
|
+
path: str,
|
|
404
|
+
params: list[Any] | dict[str, Any] | None,
|
|
405
|
+
id: str | int | None,
|
|
406
|
+
validate_result: bool = False,
|
|
407
|
+
context: Any = None,
|
|
408
|
+
) -> Any:
|
|
409
|
+
"""Dispatch method call asynchronously (with middleware support).
|
|
410
|
+
|
|
411
|
+
Override this method to implement async middleware.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
path: Method path
|
|
415
|
+
params: Method parameters
|
|
416
|
+
id: Request ID
|
|
417
|
+
validate_result: Whether to validate result
|
|
418
|
+
context: Optional context object
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
Method result
|
|
422
|
+
"""
|
|
423
|
+
group, method = self.resolve_path(path)
|
|
424
|
+
|
|
425
|
+
validated_params = validate_params(params, method.params_type)
|
|
426
|
+
|
|
427
|
+
if method._is_async():
|
|
428
|
+
result = await group.execute_method_async(method, validated_params, context=context)
|
|
429
|
+
else:
|
|
430
|
+
result = group.execute_method(method, validated_params, context=context)
|
|
431
|
+
|
|
432
|
+
# Validate result if requested
|
|
433
|
+
if validate_result and method.result_type is not None:
|
|
434
|
+
validate_result_type(result, method.result_type)
|
|
435
|
+
|
|
436
|
+
return result
|
|
437
|
+
|
|
438
|
+
def execute_method(self, method: Method, params: Any, context: Any = None) -> Any:
|
|
439
|
+
"""Execute method synchronously (override for middleware).
|
|
440
|
+
|
|
441
|
+
Override this in subclasses to add pre/post processing:
|
|
442
|
+
|
|
443
|
+
Examples:
|
|
444
|
+
class SudoGroup(MethodGroup):
|
|
445
|
+
def execute_method(self, method, params, context):
|
|
446
|
+
if not self._check_sudo_rights():
|
|
447
|
+
raise PermissionError("Sudo required")
|
|
448
|
+
return super().execute_method(method, params, context)
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
method: Method instance to execute
|
|
452
|
+
params: Validated params
|
|
453
|
+
context: Optional context object
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
Method result
|
|
457
|
+
"""
|
|
458
|
+
# Runtime validation (ONLY here!)
|
|
459
|
+
if method.accepts_context and context is not None and method.context_type is not None:
|
|
460
|
+
if not isinstance(context, method.context_type):
|
|
461
|
+
raise TypeError(
|
|
462
|
+
f'Expected context of type {method.context_type.__name__}, got {type(context).__name__}'
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
# Conditional context passing
|
|
466
|
+
if method.accepts_context:
|
|
467
|
+
return method.execute(params, context)
|
|
468
|
+
else:
|
|
469
|
+
return method.execute(params)
|
|
470
|
+
|
|
471
|
+
async def execute_method_async(self, method: Method, params: Any, context: Any = None) -> Any:
|
|
472
|
+
"""Execute method asynchronously (override for middleware).
|
|
473
|
+
|
|
474
|
+
Override this in subclasses for async middleware.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
method: Method instance to execute
|
|
478
|
+
params: Validated params
|
|
479
|
+
context: Optional context object
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
Method result
|
|
483
|
+
"""
|
|
484
|
+
# Runtime validation (ONLY here!)
|
|
485
|
+
if method.accepts_context and context is not None and method.context_type is not None:
|
|
486
|
+
if not isinstance(context, method.context_type):
|
|
487
|
+
raise TypeError(
|
|
488
|
+
f'Expected context of type {method.context_type.__name__}, got {type(context).__name__}'
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
# Conditional context passing
|
|
492
|
+
if method.accepts_context:
|
|
493
|
+
return await method.execute(params, context)
|
|
494
|
+
else:
|
|
495
|
+
return await method.execute(params)
|
|
496
|
+
|
|
497
|
+
def list_methods(self, recursive: bool = False) -> list[str]:
|
|
498
|
+
"""List method names in this group.
|
|
499
|
+
|
|
500
|
+
Args:
|
|
501
|
+
recursive: If True, include subgroup methods with paths
|
|
502
|
+
|
|
503
|
+
Returns:
|
|
504
|
+
List of method names (without group prefix)
|
|
505
|
+
|
|
506
|
+
Examples:
|
|
507
|
+
recursive=False: ["add", "subtract"]
|
|
508
|
+
recursive=True: ["add", "subtract", "user.add", "user.delete"]
|
|
509
|
+
"""
|
|
510
|
+
result = list(self._methods.keys())
|
|
511
|
+
|
|
512
|
+
if recursive:
|
|
513
|
+
for subgroup_name, subgroup in self._subgroups.items():
|
|
514
|
+
for method_name in subgroup.list_methods(recursive=True):
|
|
515
|
+
result.append(f'{subgroup_name}.{method_name}')
|
|
516
|
+
|
|
517
|
+
return result
|
|
518
|
+
|
|
519
|
+
def get_method(self, name: str) -> Method | None:
|
|
520
|
+
"""Get method by name (without path, local only).
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
name: Method name (not a path)
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
Method instance or None
|
|
527
|
+
"""
|
|
528
|
+
return self._methods.get(name)
|
|
529
|
+
|
|
530
|
+
def get_subgroup(self, name: str) -> 'MethodGroup | None':
|
|
531
|
+
"""Get subgroup by name.
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
name: Subgroup name
|
|
535
|
+
|
|
536
|
+
Returns:
|
|
537
|
+
MethodGroup instance or None
|
|
538
|
+
"""
|
|
539
|
+
return self._subgroups.get(name)
|
|
540
|
+
|
|
541
|
+
def get_all_groups(self) -> dict[str, 'MethodGroup']:
|
|
542
|
+
"""Get all subgroups (shallow, not recursive).
|
|
543
|
+
|
|
544
|
+
Returns:
|
|
545
|
+
Dict of {name: group}
|
|
546
|
+
"""
|
|
547
|
+
return dict(self._subgroups)
|
|
548
|
+
|
|
549
|
+
def _inject_rpc(self, rpc: 'JSONRPC') -> None:
|
|
550
|
+
"""Inject RPC reference into group and all children (recursive).
|
|
551
|
+
|
|
552
|
+
Args:
|
|
553
|
+
rpc: JSONRPC instance
|
|
554
|
+
"""
|
|
555
|
+
self.rpc = rpc
|
|
556
|
+
|
|
557
|
+
for method in self._methods.values():
|
|
558
|
+
method.rpc = rpc
|
|
559
|
+
|
|
560
|
+
for subgroup in self._subgroups.values():
|
|
561
|
+
subgroup._inject_rpc(rpc)
|