python-jsonrpc-lib 0.3.1__tar.gz → 0.3.2__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.
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/.gitignore +2 -1
- python_jsonrpc_lib-0.3.2/CHANGELOG.md +26 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/LICENSE +1 -1
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/PKG-INFO +1 -1
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/docs/api-reference.md +16 -4
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/docs/tutorial/07-openapi.md +55 -26
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/pyproject.toml +1 -1
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/src/jsonrpc/__init__.py +1 -1
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/src/jsonrpc/jsonrpc.py +8 -6
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/src/jsonrpc/method.py +19 -2
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/src/jsonrpc/openapi.py +18 -12
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/src/jsonrpc/validation.py +1 -1
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/tests/test_internal_api.py +54 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/tests/test_jsonrpc_v2.py +7 -18
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/tests/test_openapi.py +58 -7
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/tests/test_utils.py +20 -0
- {python_jsonrpc_lib-0.3.1/.claude/skills/jsonrpc-lib → python_jsonrpc_lib-0.3.2/.claude/skills/python-jsonrpc-lib}/SKILL.md +0 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/.github/workflows/docs.yml +0 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/MANIFEST.in +0 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/README.md +0 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/docs/advanced/async.md +0 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/docs/advanced/batch.md +0 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/docs/advanced/middleware.md +0 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/docs/advanced/protocols.md +0 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/docs/index.md +0 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/docs/integrations/custom.md +0 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/docs/integrations/fastapi.md +0 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/docs/integrations/flask.md +0 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/docs/philosophy.md +0 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/docs/tutorial/01-hello-world.md +0 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/docs/tutorial/02-method-classes.md +0 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/docs/tutorial/03-parameters.md +0 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/docs/tutorial/04-nested-types.md +0 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/docs/tutorial/05-context.md +0 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/docs/tutorial/06-groups.md +0 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/mkdocs.yml +0 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/src/jsonrpc/errors.py +0 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/src/jsonrpc/py.typed +0 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/src/jsonrpc/request.py +0 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/src/jsonrpc/response.py +0 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/src/jsonrpc/types.py +0 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/tests/__init__.py +0 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/tests/fixtures.py +0 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/tests/test_context.py +0 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/tests/test_decorator.py +0 -0
- {python_jsonrpc_lib-0.3.1 → python_jsonrpc_lib-0.3.2}/tests/test_jsonrpc_v1.py +0 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.2
|
|
4
|
+
|
|
5
|
+
**Bug fixes:**
|
|
6
|
+
|
|
7
|
+
- `add_security_scheme`: replaced `**kwargs` with `options: dict` parameter, fixing inability to create `apiKey` schemes (conflicting `name` parameter, `in` as Python reserved word)
|
|
8
|
+
- `_convert_value`: Union types containing multiple dataclasses now correctly try all variants instead of crashing on the first mismatch
|
|
9
|
+
- `simplify_id` flag now consistently applies to JSONRPCError schema in OpenAPI output
|
|
10
|
+
- `unregister()` now clears `.rpc` attribute, allowing re-registration of the same Method instance
|
|
11
|
+
- `max_concurrent` parameter is now validated (`-1` or `>= 1`); previously `0` caused a silent deadlock
|
|
12
|
+
- `version` parameter is now validated at init; invalid values like `'3.0'` raise `ValueError`
|
|
13
|
+
- Fixed `bearer_format` typo in tests (should be `bearerFormat` per OpenAPI spec)
|
|
14
|
+
- Fixed OpenAPI tutorial example to match actual generated output
|
|
15
|
+
|
|
16
|
+
## 0.3.1 (First Public Release)
|
|
17
|
+
|
|
18
|
+
- JSON-RPC 1.0 and 2.0 support
|
|
19
|
+
- Dataclass-based parameter validation
|
|
20
|
+
- Built-in OpenAPI generation
|
|
21
|
+
- Hierarchical context support
|
|
22
|
+
- Decorator API for prototyping
|
|
23
|
+
- Async/sync methods
|
|
24
|
+
- Batch request handling
|
|
25
|
+
- Strict mode by default
|
|
26
|
+
- Zero external dependencies
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-jsonrpc-lib
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Simple, yet solid - Type-safe JSON-RPC 1.0/2.0 with OpenAPI support
|
|
5
5
|
Project-URL: Homepage, https://github.com/uandysmith/python-jsonrpc-lib
|
|
6
6
|
Project-URL: Documentation, https://uandysmith.github.io/python-jsonrpc-lib/
|
|
@@ -279,7 +279,7 @@ yaml_str: str = generator.generate_yaml()
|
|
|
279
279
|
generator.add_security_scheme(
|
|
280
280
|
name: str,
|
|
281
281
|
scheme_type: Literal["apiKey", "http", "oauth2", "openIdConnect"],
|
|
282
|
-
|
|
282
|
+
options: dict[str, Any] | None = None,
|
|
283
283
|
)
|
|
284
284
|
|
|
285
285
|
# Add global header parameter
|
|
@@ -309,8 +309,7 @@ generator = OpenAPIGenerator(
|
|
|
309
309
|
generator.add_security_scheme(
|
|
310
310
|
"BearerAuth",
|
|
311
311
|
scheme_type="http",
|
|
312
|
-
|
|
313
|
-
bearerFormat="JWT",
|
|
312
|
+
options={"scheme": "bearer", "bearerFormat": "JWT"},
|
|
314
313
|
)
|
|
315
314
|
generator.add_security_requirement("BearerAuth")
|
|
316
315
|
spec = generator.generate()
|
|
@@ -504,7 +503,20 @@ response = rpc.handle(
|
|
|
504
503
|
|
|
505
504
|
## Changelog
|
|
506
505
|
|
|
507
|
-
###
|
|
506
|
+
### 0.3.2
|
|
507
|
+
|
|
508
|
+
**Bug fixes:**
|
|
509
|
+
|
|
510
|
+
- `add_security_scheme`: replaced `**kwargs` with `options: dict` parameter, fixing inability to create `apiKey` schemes (conflicting `name` parameter, `in` as Python reserved word)
|
|
511
|
+
- `_convert_value`: Union types containing multiple dataclasses now correctly try all variants instead of crashing on the first mismatch
|
|
512
|
+
- `simplify_id` flag now consistently applies to JSONRPCError schema in OpenAPI output
|
|
513
|
+
- `unregister()` now clears `.rpc` attribute, allowing re-registration of the same Method instance
|
|
514
|
+
- `max_concurrent` parameter is now validated (`-1` or `>= 1`); previously `0` caused a silent deadlock
|
|
515
|
+
- `version` parameter is now validated at init; invalid values like `'3.0'` raise `ValueError`
|
|
516
|
+
- Fixed `bearer_format` typo in tests (should be `bearerFormat` per OpenAPI spec)
|
|
517
|
+
- Fixed OpenAPI tutorial example to match actual generated output
|
|
518
|
+
|
|
519
|
+
### 0.3.1 (First Public Release)
|
|
508
520
|
|
|
509
521
|
- JSON-RPC 1.0 and 2.0 support
|
|
510
522
|
- Dataclass-based parameter validation
|
|
@@ -67,28 +67,47 @@ import json
|
|
|
67
67
|
print(json.dumps(spec, indent=2))
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
**Generated OpenAPI Schema:**
|
|
70
|
+
**Generated OpenAPI Schema (abbreviated):**
|
|
71
|
+
|
|
72
|
+
Each method gets its own path using fragment syntax (`#method.name`), with separate
|
|
73
|
+
request and response schemas in `components/schemas`:
|
|
71
74
|
|
|
72
75
|
```json title="openapi_output.json"
|
|
73
76
|
{
|
|
74
|
-
"openapi": "3.0.
|
|
77
|
+
"openapi": "3.0.3",
|
|
75
78
|
"info": {
|
|
76
79
|
"title": "Calculator API",
|
|
77
80
|
"version": "1.0.0",
|
|
78
81
|
"description": "Simple calculator with JSON-RPC 2.0"
|
|
79
82
|
},
|
|
80
83
|
"paths": {
|
|
81
|
-
"/": {
|
|
84
|
+
"/jsonrpc#math.calculate": {
|
|
82
85
|
"post": {
|
|
83
|
-
"
|
|
86
|
+
"operationId": "math_calculate",
|
|
87
|
+
"summary": "Perform a math operation.",
|
|
88
|
+
"tags": ["math"],
|
|
84
89
|
"requestBody": {
|
|
90
|
+
"required": true,
|
|
85
91
|
"content": {
|
|
86
92
|
"application/json": {
|
|
87
|
-
"schema": {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
93
|
+
"schema": {"$ref": "#/components/schemas/math.calculate_request"}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"responses": {
|
|
98
|
+
"200": {
|
|
99
|
+
"description": "Successful response",
|
|
100
|
+
"content": {
|
|
101
|
+
"application/json": {
|
|
102
|
+
"schema": {"$ref": "#/components/schemas/math.calculate_response"}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"default": {
|
|
107
|
+
"description": "JSON-RPC Error",
|
|
108
|
+
"content": {
|
|
109
|
+
"application/json": {
|
|
110
|
+
"schema": {"$ref": "#/components/schemas/JSONRPCError"}
|
|
92
111
|
}
|
|
93
112
|
}
|
|
94
113
|
}
|
|
@@ -98,11 +117,12 @@ print(json.dumps(spec, indent=2))
|
|
|
98
117
|
},
|
|
99
118
|
"components": {
|
|
100
119
|
"schemas": {
|
|
101
|
-
"
|
|
120
|
+
"math.calculate_request": {
|
|
102
121
|
"type": "object",
|
|
103
122
|
"properties": {
|
|
104
|
-
"jsonrpc": {"
|
|
105
|
-
"method": {"
|
|
123
|
+
"jsonrpc": {"const": "2.0"},
|
|
124
|
+
"method": {"const": "math.calculate"},
|
|
125
|
+
"id": {"type": "integer"},
|
|
106
126
|
"params": {
|
|
107
127
|
"type": "object",
|
|
108
128
|
"properties": {
|
|
@@ -111,10 +131,9 @@ print(json.dumps(spec, indent=2))
|
|
|
111
131
|
"operation": {"type": "string"}
|
|
112
132
|
},
|
|
113
133
|
"required": ["x", "y", "operation"]
|
|
114
|
-
}
|
|
115
|
-
"id": {"type": "integer"}
|
|
134
|
+
}
|
|
116
135
|
},
|
|
117
|
-
"required": ["jsonrpc", "method", "
|
|
136
|
+
"required": ["jsonrpc", "method", "id", "params"]
|
|
118
137
|
}
|
|
119
138
|
}
|
|
120
139
|
}
|
|
@@ -412,25 +431,35 @@ generator = OpenAPIGenerator(rpc, title="Secure API", version="1.0.0")
|
|
|
412
431
|
generator.add_security_scheme(
|
|
413
432
|
"BearerAuth",
|
|
414
433
|
scheme_type="http",
|
|
415
|
-
|
|
416
|
-
bearerFormat="JWT",
|
|
434
|
+
options={"scheme": "bearer", "bearerFormat": "JWT"},
|
|
417
435
|
)
|
|
418
436
|
generator.add_security_requirement("BearerAuth")
|
|
419
437
|
|
|
438
|
+
# API Key
|
|
439
|
+
generator = OpenAPIGenerator(rpc, title="API Key API", version="1.0.0")
|
|
440
|
+
generator.add_security_scheme(
|
|
441
|
+
"ApiKeyAuth",
|
|
442
|
+
scheme_type="apiKey",
|
|
443
|
+
options={"name": "X-API-Key", "in": "header"},
|
|
444
|
+
)
|
|
445
|
+
generator.add_security_requirement("ApiKeyAuth")
|
|
446
|
+
|
|
420
447
|
# OAuth2
|
|
421
448
|
generator = OpenAPIGenerator(rpc, title="OAuth API", version="1.0.0")
|
|
422
449
|
generator.add_security_scheme(
|
|
423
450
|
"OAuth2",
|
|
424
451
|
scheme_type="oauth2",
|
|
425
|
-
|
|
426
|
-
"
|
|
427
|
-
"
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
"
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
452
|
+
options={
|
|
453
|
+
"flows": {
|
|
454
|
+
"authorizationCode": {
|
|
455
|
+
"authorizationUrl": "https://example.com/oauth/authorize",
|
|
456
|
+
"tokenUrl": "https://example.com/oauth/token",
|
|
457
|
+
"scopes": {
|
|
458
|
+
"read": "Read access",
|
|
459
|
+
"write": "Write access",
|
|
460
|
+
},
|
|
461
|
+
}
|
|
462
|
+
},
|
|
434
463
|
},
|
|
435
464
|
)
|
|
436
465
|
generator.add_security_requirement("OAuth2", scopes=["read", "write"])
|
|
@@ -46,7 +46,7 @@ from .response import build_error_response, build_response, parse_response
|
|
|
46
46
|
from .types import ErrorResponse, Request, Response, Version
|
|
47
47
|
from .validation import validate_params, validate_result_type
|
|
48
48
|
|
|
49
|
-
__version__ = '0.3.
|
|
49
|
+
__version__ = '0.3.2'
|
|
50
50
|
|
|
51
51
|
__all__ = [
|
|
52
52
|
# Main classes
|
|
@@ -190,6 +190,12 @@ class JSONRPC:
|
|
|
190
190
|
v1.0: allow_batch=False, allow_dict_params=False, allow_list_params=True
|
|
191
191
|
v2.0: allow_batch=True, allow_dict_params=True, allow_list_params=True
|
|
192
192
|
"""
|
|
193
|
+
if version not in ('1.0', '2.0'):
|
|
194
|
+
raise ValueError(f"version must be '1.0' or '2.0', got {version!r}")
|
|
195
|
+
|
|
196
|
+
if max_concurrent is not None and max_concurrent != -1 and max_concurrent < 1:
|
|
197
|
+
raise ValueError(f'max_concurrent must be -1 (unlimited) or >= 1, got {max_concurrent}')
|
|
198
|
+
|
|
193
199
|
self.version = version
|
|
194
200
|
self.validate_results = validate_results
|
|
195
201
|
self.context_type = context_type
|
|
@@ -615,9 +621,7 @@ class JSONRPC:
|
|
|
615
621
|
return self.serialize(response)
|
|
616
622
|
|
|
617
623
|
if self.max_batch != -1 and len(data) > self.max_batch:
|
|
618
|
-
error = InvalidRequestError(
|
|
619
|
-
f'Batch too large: {len(data)} requests, maximum is {self.max_batch}'
|
|
620
|
-
)
|
|
624
|
+
error = InvalidRequestError(f'Batch too large: {len(data)} requests, maximum is {self.max_batch}')
|
|
621
625
|
response = build_error_response(error, None, self.version)
|
|
622
626
|
return self.serialize(response)
|
|
623
627
|
|
|
@@ -646,9 +650,7 @@ class JSONRPC:
|
|
|
646
650
|
return self.serialize(response)
|
|
647
651
|
|
|
648
652
|
if self.max_batch != -1 and len(data) > self.max_batch:
|
|
649
|
-
error = InvalidRequestError(
|
|
650
|
-
f'Batch too large: {len(data)} requests, maximum is {self.max_batch}'
|
|
651
|
-
)
|
|
653
|
+
error = InvalidRequestError(f'Batch too large: {len(data)} requests, maximum is {self.max_batch}')
|
|
652
654
|
response = build_error_response(error, None, self.version)
|
|
653
655
|
return self.serialize(response)
|
|
654
656
|
|
|
@@ -303,6 +303,8 @@ class MethodGroup:
|
|
|
303
303
|
def unregister(self, name: str) -> None:
|
|
304
304
|
"""Unregister a method or subgroup by name.
|
|
305
305
|
|
|
306
|
+
Clears the `.rpc` attribute so the instance can be re-registered.
|
|
307
|
+
|
|
306
308
|
Args:
|
|
307
309
|
name: Method or subgroup name (not a path)
|
|
308
310
|
|
|
@@ -310,9 +312,12 @@ class MethodGroup:
|
|
|
310
312
|
KeyError: If name not found in either methods or subgroups
|
|
311
313
|
"""
|
|
312
314
|
if name in self._methods:
|
|
313
|
-
|
|
315
|
+
method = self._methods.pop(name)
|
|
316
|
+
if hasattr(method, 'rpc'):
|
|
317
|
+
del method.rpc
|
|
314
318
|
elif name in self._subgroups:
|
|
315
|
-
|
|
319
|
+
subgroup = self._subgroups.pop(name)
|
|
320
|
+
subgroup._clear_rpc()
|
|
316
321
|
else:
|
|
317
322
|
raise KeyError(f"'{name}' not found in group '{self._name}'")
|
|
318
323
|
|
|
@@ -559,3 +564,15 @@ class MethodGroup:
|
|
|
559
564
|
|
|
560
565
|
for subgroup in self._subgroups.values():
|
|
561
566
|
subgroup._inject_rpc(rpc)
|
|
567
|
+
|
|
568
|
+
def _clear_rpc(self) -> None:
|
|
569
|
+
"""Clear RPC reference from group and all children (recursive)."""
|
|
570
|
+
if hasattr(self, 'rpc'):
|
|
571
|
+
del self.rpc
|
|
572
|
+
|
|
573
|
+
for method in self._methods.values():
|
|
574
|
+
if hasattr(method, 'rpc'):
|
|
575
|
+
del method.rpc
|
|
576
|
+
|
|
577
|
+
for subgroup in self._subgroups.values():
|
|
578
|
+
subgroup._clear_rpc()
|
|
@@ -174,25 +174,31 @@ class OpenAPIGenerator:
|
|
|
174
174
|
self,
|
|
175
175
|
name: str,
|
|
176
176
|
scheme_type: Literal['apiKey', 'http', 'oauth2', 'openIdConnect'],
|
|
177
|
-
|
|
177
|
+
options: dict[str, Any] | None = None,
|
|
178
178
|
) -> None:
|
|
179
179
|
"""Add authentication/security scheme.
|
|
180
180
|
|
|
181
181
|
Args:
|
|
182
182
|
name: Scheme name (used in security requirements)
|
|
183
183
|
scheme_type: Type of security scheme
|
|
184
|
-
|
|
184
|
+
options: Additional scheme properties as a dict.
|
|
185
|
+
Keys are passed directly into the OpenAPI security scheme object.
|
|
185
186
|
|
|
186
187
|
Example:
|
|
187
188
|
>>> openapi.add_security_scheme(
|
|
188
189
|
... "bearerAuth",
|
|
189
190
|
... scheme_type="http",
|
|
190
|
-
...
|
|
191
|
-
...
|
|
191
|
+
... options={"scheme": "bearer", "bearerFormat": "JWT"},
|
|
192
|
+
... )
|
|
193
|
+
>>> openapi.add_security_scheme(
|
|
194
|
+
... "apiKeyAuth",
|
|
195
|
+
... scheme_type="apiKey",
|
|
196
|
+
... options={"name": "X-API-Key", "in": "header"},
|
|
192
197
|
... )
|
|
193
198
|
"""
|
|
194
199
|
scheme: dict[str, Any] = {'type': scheme_type}
|
|
195
|
-
|
|
200
|
+
if options:
|
|
201
|
+
scheme.update(options)
|
|
196
202
|
self.security_schemes[name] = scheme
|
|
197
203
|
|
|
198
204
|
def add_header(
|
|
@@ -390,13 +396,7 @@ class OpenAPIGenerator:
|
|
|
390
396
|
},
|
|
391
397
|
'required': ['code', 'message'],
|
|
392
398
|
},
|
|
393
|
-
'id':
|
|
394
|
-
'oneOf': [
|
|
395
|
-
{'type': 'string'},
|
|
396
|
-
{'type': 'integer'},
|
|
397
|
-
{'type': 'null'},
|
|
398
|
-
]
|
|
399
|
-
},
|
|
399
|
+
'id': self._error_id_schema(),
|
|
400
400
|
},
|
|
401
401
|
'required': ['jsonrpc', 'error', 'id'],
|
|
402
402
|
},
|
|
@@ -421,6 +421,12 @@ class OpenAPIGenerator:
|
|
|
421
421
|
return {'type': 'integer'}
|
|
422
422
|
return {'oneOf': [{'type': 'string'}, {'type': 'integer'}]}
|
|
423
423
|
|
|
424
|
+
def _error_id_schema(self) -> dict[str, Any]:
|
|
425
|
+
"""Return JSON Schema for the error response id field (includes null)."""
|
|
426
|
+
if self.simplify_id:
|
|
427
|
+
return {'oneOf': [{'type': 'integer'}, {'type': 'null'}]}
|
|
428
|
+
return {'oneOf': [{'type': 'string'}, {'type': 'integer'}, {'type': 'null'}]}
|
|
429
|
+
|
|
424
430
|
def _generate_method_request_schema(
|
|
425
431
|
self,
|
|
426
432
|
full_name: str,
|
|
@@ -226,7 +226,7 @@ def _convert_value(value: Any, expected_type: type, _depth: int = 0) -> Any:
|
|
|
226
226
|
if arg is not type(None):
|
|
227
227
|
try:
|
|
228
228
|
return _convert_value(value, arg, _depth=_depth)
|
|
229
|
-
except (TypeError, ValueError):
|
|
229
|
+
except (TypeError, ValueError, InvalidParamsError):
|
|
230
230
|
continue
|
|
231
231
|
return value
|
|
232
232
|
|
|
@@ -261,6 +261,60 @@ class TestJSONRPCFacade(unittest.TestCase):
|
|
|
261
261
|
with self.assertRaises(MethodNotFoundError):
|
|
262
262
|
rpc.call_method('math.add', {'a': 1, 'b': 2})
|
|
263
263
|
|
|
264
|
+
def test_unregister_method_then_reregister_same_instance(self):
|
|
265
|
+
"""Test re-registering the same Method instance after unregister."""
|
|
266
|
+
rpc = JSONRPC(version='2.0')
|
|
267
|
+
math = MethodGroup()
|
|
268
|
+
add = AddMethod()
|
|
269
|
+
math.register('add', add)
|
|
270
|
+
rpc.register('math', math)
|
|
271
|
+
|
|
272
|
+
self.assertEqual(rpc.call_method('math.add', {'a': 1, 'b': 2}), 3)
|
|
273
|
+
|
|
274
|
+
rpc.unregister('math.add')
|
|
275
|
+
# Same instance should be re-registerable after unregister
|
|
276
|
+
math.register('add', add)
|
|
277
|
+
self.assertEqual(rpc.call_method('math.add', {'a': 5, 'b': 3}), 8)
|
|
278
|
+
|
|
279
|
+
def test_unregister_subgroup_clears_rpc_on_children(self):
|
|
280
|
+
"""Test unregistering a subgroup clears .rpc on all nested methods."""
|
|
281
|
+
rpc = JSONRPC(version='2.0')
|
|
282
|
+
math = MethodGroup()
|
|
283
|
+
add = AddMethod()
|
|
284
|
+
sub = SubtractMethod()
|
|
285
|
+
math.register('add', add)
|
|
286
|
+
math.register('subtract', sub)
|
|
287
|
+
rpc.register('math', math)
|
|
288
|
+
|
|
289
|
+
self.assertTrue(hasattr(add, 'rpc'))
|
|
290
|
+
self.assertTrue(hasattr(sub, 'rpc'))
|
|
291
|
+
|
|
292
|
+
rpc.unregister('math')
|
|
293
|
+
self.assertFalse(hasattr(add, 'rpc'))
|
|
294
|
+
self.assertFalse(hasattr(sub, 'rpc'))
|
|
295
|
+
|
|
296
|
+
def test_invalid_version_raises_value_error(self):
|
|
297
|
+
"""Test that invalid version raises ValueError."""
|
|
298
|
+
with self.assertRaises(ValueError) as ctx:
|
|
299
|
+
JSONRPC(version='3.0')
|
|
300
|
+
self.assertIn("'1.0' or '2.0'", str(ctx.exception))
|
|
301
|
+
|
|
302
|
+
def test_max_concurrent_zero_raises_value_error(self):
|
|
303
|
+
"""Test that max_concurrent=0 raises ValueError."""
|
|
304
|
+
with self.assertRaises(ValueError) as ctx:
|
|
305
|
+
JSONRPC(max_concurrent=0)
|
|
306
|
+
self.assertIn('max_concurrent', str(ctx.exception))
|
|
307
|
+
|
|
308
|
+
def test_max_concurrent_negative_two_raises_value_error(self):
|
|
309
|
+
"""Test that max_concurrent=-2 raises ValueError."""
|
|
310
|
+
with self.assertRaises(ValueError):
|
|
311
|
+
JSONRPC(max_concurrent=-2)
|
|
312
|
+
|
|
313
|
+
def test_max_concurrent_minus_one_is_valid(self):
|
|
314
|
+
"""Test that max_concurrent=-1 (unlimited) is accepted."""
|
|
315
|
+
rpc = JSONRPC(max_concurrent=-1)
|
|
316
|
+
self.assertEqual(rpc._effective_max_concurrent, -1)
|
|
317
|
+
|
|
264
318
|
|
|
265
319
|
class TestCallMethod(unittest.TestCase):
|
|
266
320
|
"""Comprehensive tests for call_method() internal API."""
|
|
@@ -871,9 +871,7 @@ class TestJSONRPCV2DefensiveExceptionHandling(unittest.TestCase):
|
|
|
871
871
|
|
|
872
872
|
# Patch _handle_single to raise — bypasses all internal error handling.
|
|
873
873
|
with patch.object(self.rpc, '_handle_single', side_effect=RuntimeError('internal crash')):
|
|
874
|
-
response = self.rpc.handle(
|
|
875
|
-
'{"jsonrpc":"2.0","method":"math.add","params":{"a":1,"b":2},"id":1}'
|
|
876
|
-
)
|
|
874
|
+
response = self.rpc.handle('{"jsonrpc":"2.0","method":"math.add","params":{"a":1,"b":2},"id":1}')
|
|
877
875
|
data = json.loads(response)
|
|
878
876
|
self.assertEqual(data['error']['code'], -32603)
|
|
879
877
|
self.assertIn('internal crash', data['error']['message'])
|
|
@@ -884,9 +882,7 @@ class TestJSONRPCV2DefensiveExceptionHandling(unittest.TestCase):
|
|
|
884
882
|
|
|
885
883
|
with patch.object(self.rpc, '_handle_single_async', side_effect=RuntimeError('async crash')):
|
|
886
884
|
response = asyncio.run(
|
|
887
|
-
self.rpc.handle_async(
|
|
888
|
-
'{"jsonrpc":"2.0","method":"math.add","params":{"a":1,"b":2},"id":1}'
|
|
889
|
-
)
|
|
885
|
+
self.rpc.handle_async('{"jsonrpc":"2.0","method":"math.add","params":{"a":1,"b":2},"id":1}')
|
|
890
886
|
)
|
|
891
887
|
data = json.loads(response)
|
|
892
888
|
self.assertEqual(data['error']['code'], -32603)
|
|
@@ -1136,9 +1132,7 @@ class TestJSONRPCV2SerializationHooks(unittest.TestCase):
|
|
|
1136
1132
|
|
|
1137
1133
|
rpc = self._make_rpc()
|
|
1138
1134
|
with patch.object(rpc, 'serialize', side_effect=self._failing_serialize(fail_on_call=1)):
|
|
1139
|
-
response = rpc.handle(
|
|
1140
|
-
'{"jsonrpc":"2.0","method":"math.add","params":{"a":1,"b":2},"id":42}'
|
|
1141
|
-
)
|
|
1135
|
+
response = rpc.handle('{"jsonrpc":"2.0","method":"math.add","params":{"a":1,"b":2},"id":42}')
|
|
1142
1136
|
data = json.loads(response)
|
|
1143
1137
|
self.assertEqual(data['error']['code'], -32603)
|
|
1144
1138
|
self.assertIn('cannot serialize', data['error']['message'])
|
|
@@ -1151,9 +1145,7 @@ class TestJSONRPCV2SerializationHooks(unittest.TestCase):
|
|
|
1151
1145
|
rpc = self._make_rpc()
|
|
1152
1146
|
with patch.object(rpc, 'serialize', side_effect=self._failing_serialize(fail_on_call=1)):
|
|
1153
1147
|
response = asyncio.run(
|
|
1154
|
-
rpc.handle_async(
|
|
1155
|
-
'{"jsonrpc":"2.0","method":"math.add","params":{"a":1,"b":2},"id":42}'
|
|
1156
|
-
)
|
|
1148
|
+
rpc.handle_async('{"jsonrpc":"2.0","method":"math.add","params":{"a":1,"b":2},"id":42}')
|
|
1157
1149
|
)
|
|
1158
1150
|
data = json.loads(response)
|
|
1159
1151
|
self.assertEqual(data['error']['code'], -32603)
|
|
@@ -1164,9 +1156,7 @@ class TestJSONRPCV2SerializationHooks(unittest.TestCase):
|
|
|
1164
1156
|
from unittest.mock import patch
|
|
1165
1157
|
|
|
1166
1158
|
rpc = self._make_rpc()
|
|
1167
|
-
batch = json.dumps(
|
|
1168
|
-
[{'jsonrpc': '2.0', 'method': 'math.add', 'params': {'a': 1, 'b': 2}, 'id': 1}]
|
|
1169
|
-
)
|
|
1159
|
+
batch = json.dumps([{'jsonrpc': '2.0', 'method': 'math.add', 'params': {'a': 1, 'b': 2}, 'id': 1}])
|
|
1170
1160
|
with patch.object(rpc, 'serialize', side_effect=self._failing_serialize(fail_on_call=1)):
|
|
1171
1161
|
response = rpc.handle(batch)
|
|
1172
1162
|
data = json.loads(response)
|
|
@@ -1177,9 +1167,7 @@ class TestJSONRPCV2SerializationHooks(unittest.TestCase):
|
|
|
1177
1167
|
from unittest.mock import patch
|
|
1178
1168
|
|
|
1179
1169
|
rpc = self._make_rpc()
|
|
1180
|
-
batch = json.dumps(
|
|
1181
|
-
[{'jsonrpc': '2.0', 'method': 'math.add', 'params': {'a': 1, 'b': 2}, 'id': 1}]
|
|
1182
|
-
)
|
|
1170
|
+
batch = json.dumps([{'jsonrpc': '2.0', 'method': 'math.add', 'params': {'a': 1, 'b': 2}, 'id': 1}])
|
|
1183
1171
|
with patch.object(rpc, 'serialize', side_effect=self._failing_serialize(fail_on_call=1)):
|
|
1184
1172
|
response = asyncio.run(rpc.handle_async(batch))
|
|
1185
1173
|
data = json.loads(response)
|
|
@@ -1219,6 +1207,7 @@ class TestV2Logging(unittest.TestCase):
|
|
|
1219
1207
|
|
|
1220
1208
|
def test_async_notification_error_logged_at_debug(self):
|
|
1221
1209
|
"""Suppressed async notification errors are logged at DEBUG level."""
|
|
1210
|
+
|
|
1222
1211
|
class AsyncBrokenMethod(Method):
|
|
1223
1212
|
async def execute(self, params: None) -> str:
|
|
1224
1213
|
raise RuntimeError('Async error!')
|
|
@@ -649,6 +649,11 @@ class TestOpenAPISchemaValidation(unittest.TestCase):
|
|
|
649
649
|
id_schema = add_request['properties']['id']
|
|
650
650
|
self.assertEqual(id_schema, {'type': 'integer'})
|
|
651
651
|
|
|
652
|
+
# Error schema id should also respect simplify_id
|
|
653
|
+
error_schema = schemas['JSONRPCError']
|
|
654
|
+
error_id = error_schema['properties']['id']
|
|
655
|
+
self.assertEqual(error_id, {'oneOf': [{'type': 'integer'}, {'type': 'null'}]})
|
|
656
|
+
|
|
652
657
|
def test_simplify_id_false_uses_oneof(self):
|
|
653
658
|
"""simplify_id=False produces oneOf[string, integer] for id fields."""
|
|
654
659
|
generator = OpenAPIGenerator(self.rpc, simplify_id=False)
|
|
@@ -665,6 +670,12 @@ class TestOpenAPISchemaValidation(unittest.TestCase):
|
|
|
665
670
|
add_response = schemas['math.add_response']
|
|
666
671
|
self.assertEqual(add_response['properties']['id'], expected_id)
|
|
667
672
|
|
|
673
|
+
# Error schema id should include null
|
|
674
|
+
error_schema = schemas['JSONRPCError']
|
|
675
|
+
error_id = error_schema['properties']['id']
|
|
676
|
+
expected_error_id = {'oneOf': [{'type': 'string'}, {'type': 'integer'}, {'type': 'null'}]}
|
|
677
|
+
self.assertEqual(error_id, expected_error_id)
|
|
678
|
+
|
|
668
679
|
|
|
669
680
|
class TestOpenAPITypeConversionEdgeCases(unittest.TestCase):
|
|
670
681
|
"""Tests for edge cases in type conversion to JSON Schema."""
|
|
@@ -817,7 +828,11 @@ class TestOpenAPISecurityAndHeaders(unittest.TestCase):
|
|
|
817
828
|
generator = OpenAPIGenerator(rpc)
|
|
818
829
|
|
|
819
830
|
# Add security scheme
|
|
820
|
-
generator.add_security_scheme(
|
|
831
|
+
generator.add_security_scheme(
|
|
832
|
+
name='bearerAuth',
|
|
833
|
+
scheme_type='http',
|
|
834
|
+
options={'scheme': 'bearer', 'bearerFormat': 'JWT'},
|
|
835
|
+
)
|
|
821
836
|
|
|
822
837
|
# Add security requirement
|
|
823
838
|
generator.add_security_requirement('bearerAuth')
|
|
@@ -828,10 +843,38 @@ class TestOpenAPISecurityAndHeaders(unittest.TestCase):
|
|
|
828
843
|
self.assertIn('securitySchemes', spec['components'])
|
|
829
844
|
self.assertIn('bearerAuth', spec['components']['securitySchemes'])
|
|
830
845
|
|
|
846
|
+
# Check security scheme content
|
|
847
|
+
scheme = spec['components']['securitySchemes']['bearerAuth']
|
|
848
|
+
self.assertEqual(scheme['type'], 'http')
|
|
849
|
+
self.assertEqual(scheme['scheme'], 'bearer')
|
|
850
|
+
self.assertEqual(scheme['bearerFormat'], 'JWT')
|
|
851
|
+
|
|
831
852
|
# Check security requirement is applied globally
|
|
832
853
|
self.assertIn('security', spec)
|
|
833
854
|
self.assertEqual(spec['security'], [{'bearerAuth': []}])
|
|
834
855
|
|
|
856
|
+
def test_add_apikey_security_scheme(self):
|
|
857
|
+
"""Test adding apiKey security scheme with name and in fields."""
|
|
858
|
+
rpc = JSONRPC()
|
|
859
|
+
math_group = MethodGroup()
|
|
860
|
+
math_group.register('add', AddMethod())
|
|
861
|
+
rpc.register('math', math_group)
|
|
862
|
+
|
|
863
|
+
generator = OpenAPIGenerator(rpc)
|
|
864
|
+
generator.add_security_scheme(
|
|
865
|
+
name='apiKeyAuth',
|
|
866
|
+
scheme_type='apiKey',
|
|
867
|
+
options={'name': 'X-API-Key', 'in': 'header'},
|
|
868
|
+
)
|
|
869
|
+
generator.add_security_requirement('apiKeyAuth')
|
|
870
|
+
|
|
871
|
+
spec = generator.generate()
|
|
872
|
+
|
|
873
|
+
scheme = spec['components']['securitySchemes']['apiKeyAuth']
|
|
874
|
+
self.assertEqual(scheme['type'], 'apiKey')
|
|
875
|
+
self.assertEqual(scheme['name'], 'X-API-Key')
|
|
876
|
+
self.assertEqual(scheme['in'], 'header')
|
|
877
|
+
|
|
835
878
|
def test_add_oauth2_security_with_scopes(self):
|
|
836
879
|
"""Test adding OAuth2 security with scopes."""
|
|
837
880
|
rpc = JSONRPC()
|
|
@@ -845,12 +888,14 @@ class TestOpenAPISecurityAndHeaders(unittest.TestCase):
|
|
|
845
888
|
generator.add_security_scheme(
|
|
846
889
|
name='oauth2',
|
|
847
890
|
scheme_type='oauth2',
|
|
848
|
-
|
|
849
|
-
'
|
|
850
|
-
'
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
891
|
+
options={
|
|
892
|
+
'flows': {
|
|
893
|
+
'authorizationCode': {
|
|
894
|
+
'authorizationUrl': 'https://example.com/oauth/authorize',
|
|
895
|
+
'tokenUrl': 'https://example.com/oauth/token',
|
|
896
|
+
'scopes': {'read': 'Read access', 'write': 'Write access'},
|
|
897
|
+
}
|
|
898
|
+
},
|
|
854
899
|
},
|
|
855
900
|
)
|
|
856
901
|
|
|
@@ -859,6 +904,12 @@ class TestOpenAPISecurityAndHeaders(unittest.TestCase):
|
|
|
859
904
|
|
|
860
905
|
spec = generator.generate()
|
|
861
906
|
|
|
907
|
+
# Check security scheme content
|
|
908
|
+
scheme = spec['components']['securitySchemes']['oauth2']
|
|
909
|
+
self.assertEqual(scheme['type'], 'oauth2')
|
|
910
|
+
self.assertIn('flows', scheme)
|
|
911
|
+
self.assertIn('authorizationCode', scheme['flows'])
|
|
912
|
+
|
|
862
913
|
# Check security requirement includes scopes
|
|
863
914
|
self.assertIn('security', spec)
|
|
864
915
|
self.assertEqual(spec['security'], [{'oauth2': ['read', 'write']}])
|
|
@@ -843,6 +843,26 @@ class TestAdvancedTypeEdgeCases(unittest.TestCase):
|
|
|
843
843
|
result = _convert_value(value, TypeA | TypeB)
|
|
844
844
|
self.assertIs(result, value)
|
|
845
845
|
|
|
846
|
+
def test_convert_value_union_dataclass_field_mismatch(self):
|
|
847
|
+
"""Union fallback: if first dataclass has wrong fields (InvalidParamsError), try next."""
|
|
848
|
+
from dataclasses import dataclass
|
|
849
|
+
|
|
850
|
+
@dataclass
|
|
851
|
+
class TypeA:
|
|
852
|
+
name: str
|
|
853
|
+
age: int
|
|
854
|
+
|
|
855
|
+
@dataclass
|
|
856
|
+
class TypeB:
|
|
857
|
+
x: int
|
|
858
|
+
y: int
|
|
859
|
+
|
|
860
|
+
# Value matches TypeB fields but not TypeA → should fall through to TypeB
|
|
861
|
+
result = _convert_value({'x': 1, 'y': 2}, TypeA | TypeB)
|
|
862
|
+
self.assertIsInstance(result, TypeB)
|
|
863
|
+
self.assertEqual(result.x, 1)
|
|
864
|
+
self.assertEqual(result.y, 2)
|
|
865
|
+
|
|
846
866
|
def test_convert_value_plain_list_returns_as_is(self):
|
|
847
867
|
"""Test _convert_value with plain list (no args) - line 246."""
|
|
848
868
|
from jsonrpc.validation import _convert_value
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|