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/response.py ADDED
@@ -0,0 +1,169 @@
1
+ """Response building and parsing for JSON-RPC protocol."""
2
+
3
+ import json
4
+ from dataclasses import asdict, is_dataclass
5
+ from typing import Any
6
+
7
+ from .errors import (
8
+ InvalidRequestError,
9
+ JSONRPCError,
10
+ ParseError,
11
+ RPCError,
12
+ )
13
+ from .types import ErrorResponse, Response, Version
14
+
15
+
16
+ def _dataclass_to_dict(value: Any) -> Any:
17
+ """Convert dataclass instances to dicts recursively for JSON serialization.
18
+
19
+ Args:
20
+ value: Any value (dataclass, list, dict, or primitive)
21
+
22
+ Returns:
23
+ Value with all dataclasses converted to dicts
24
+ """
25
+ if is_dataclass(value) and not isinstance(value, type):
26
+ return asdict(value)
27
+
28
+ if isinstance(value, list):
29
+ return [_dataclass_to_dict(item) for item in value]
30
+
31
+ if isinstance(value, dict):
32
+ return {key: _dataclass_to_dict(val) for key, val in value.items()}
33
+
34
+ return value
35
+
36
+
37
+ def build_response(
38
+ result: Any,
39
+ id: str | int,
40
+ version: Version = '2.0',
41
+ ) -> dict[str, Any]:
42
+ """Build a JSON-RPC success response dict.
43
+
44
+ Args:
45
+ result: Method result (can be dataclass, which will be converted to dict)
46
+ id: Request ID
47
+ version: Protocol version
48
+
49
+ Returns:
50
+ Response dict ready for JSON serialization
51
+ """
52
+ if version == '2.0':
53
+ return {
54
+ 'jsonrpc': '2.0',
55
+ 'result': result,
56
+ 'id': id,
57
+ }
58
+ else:
59
+ # v1: always has both result and error fields
60
+ return {
61
+ 'result': result,
62
+ 'error': None,
63
+ 'id': id,
64
+ }
65
+
66
+
67
+ def build_error_response(
68
+ error: JSONRPCError | RPCError,
69
+ id: str | int | None,
70
+ version: Version = '2.0',
71
+ ) -> dict[str, Any]:
72
+ """Build a JSON-RPC error response dict.
73
+
74
+ Args:
75
+ error: Error object or exception
76
+ id: Request ID (can be None for parse errors)
77
+ version: Protocol version
78
+
79
+ Returns:
80
+ Error response dict ready for JSON serialization
81
+ """
82
+ error_dict = error.to_dict()
83
+
84
+ if version == '2.0':
85
+ return {
86
+ 'jsonrpc': '2.0',
87
+ 'error': error_dict,
88
+ 'id': id,
89
+ }
90
+ else:
91
+ # v1: always has both result and error fields
92
+ return {
93
+ 'result': None,
94
+ 'error': error_dict,
95
+ 'id': id,
96
+ }
97
+
98
+
99
+ def parse_response(
100
+ data: str | bytes | dict[str, Any],
101
+ ) -> Response | ErrorResponse:
102
+ """Parse JSON-RPC response (for client-side usage).
103
+
104
+ Args:
105
+ data: JSON string, bytes, or already parsed dict
106
+
107
+ Returns:
108
+ Response or ErrorResponse object
109
+
110
+ Raises:
111
+ ParseError: If JSON is invalid
112
+ InvalidRequestError: If response structure is invalid
113
+ """
114
+ if isinstance(data, (str, bytes)):
115
+ try:
116
+ parsed = json.loads(data)
117
+ except json.JSONDecodeError as e:
118
+ raise ParseError(f'Invalid JSON: {e}') from e
119
+ else:
120
+ parsed = data
121
+
122
+ if not isinstance(parsed, dict):
123
+ raise InvalidRequestError(f'Response must be object, got {type(parsed).__name__}')
124
+
125
+ if 'jsonrpc' in parsed:
126
+ if parsed['jsonrpc'] != '2.0':
127
+ raise InvalidRequestError(f'Invalid jsonrpc version: {parsed["jsonrpc"]!r}')
128
+ version: Version = '2.0'
129
+ else:
130
+ version = '1.0'
131
+
132
+ response_id = parsed.get('id')
133
+
134
+ if 'error' in parsed and parsed['error'] is not None:
135
+ error_data = parsed['error']
136
+ if not isinstance(error_data, dict):
137
+ raise InvalidRequestError(f"Field 'error' must be object, got {type(error_data).__name__}")
138
+
139
+ code = error_data.get('code')
140
+ if not isinstance(code, int):
141
+ raise InvalidRequestError(f"Error 'code' must be integer, got {type(code).__name__}")
142
+
143
+ message = error_data.get('message', '')
144
+ if not isinstance(message, str):
145
+ raise InvalidRequestError(f"Error 'message' must be string, got {type(message).__name__}")
146
+
147
+ error = RPCError(
148
+ code=code,
149
+ message=message,
150
+ data=error_data.get('data'),
151
+ )
152
+
153
+ return ErrorResponse(
154
+ error=error,
155
+ id=response_id,
156
+ version=version,
157
+ )
158
+
159
+ if 'result' not in parsed:
160
+ raise InvalidRequestError("Response must have 'result' or 'error' field")
161
+
162
+ if response_id is None:
163
+ raise InvalidRequestError("Success response must have 'id' field")
164
+
165
+ return Response(
166
+ result=parsed['result'],
167
+ id=response_id,
168
+ version=version,
169
+ )
jsonrpc/types.py ADDED
@@ -0,0 +1,53 @@
1
+ """Core type definitions for JSON-RPC protocol."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Literal
5
+
6
+ from .errors import RPCError
7
+
8
+ Version = Literal['1.0', '2.0']
9
+
10
+
11
+ @dataclass
12
+ class Request:
13
+ """JSON-RPC request object."""
14
+
15
+ method: str
16
+ params: list[Any] | dict[str, Any] | None
17
+ id: str | int | None
18
+ version: Version
19
+ id_was_present: bool = True
20
+
21
+ @property
22
+ def is_notification(self) -> bool:
23
+ """Check if this request is a notification (no response expected).
24
+
25
+ JSON-RPC 2.0: notification when id field is missing (not just null).
26
+ JSON-RPC 1.0: NO notifications - all requests get responses.
27
+
28
+ Note: v2.0 distinguishes:
29
+ - Missing id field → notification (no response)
30
+ - id=null → normal request (returns response with id=null)
31
+ """
32
+ if self.version == '2.0':
33
+ return not self.id_was_present
34
+ else:
35
+ return False
36
+
37
+
38
+ @dataclass
39
+ class Response:
40
+ """JSON-RPC success response object."""
41
+
42
+ result: Any
43
+ id: str | int
44
+ version: Version
45
+
46
+
47
+ @dataclass
48
+ class ErrorResponse:
49
+ """JSON-RPC error response object."""
50
+
51
+ error: RPCError
52
+ id: str | int | None
53
+ version: Version
jsonrpc/validation.py ADDED
@@ -0,0 +1,297 @@
1
+ """Type validation, conversion, and utility functions for JSON-RPC."""
2
+
3
+ import types
4
+ from dataclasses import MISSING, fields, is_dataclass
5
+ from typing import (
6
+ Any,
7
+ Literal,
8
+ Union,
9
+ get_args,
10
+ get_origin,
11
+ get_type_hints,
12
+ )
13
+
14
+ from .errors import InvalidParamsError, InvalidResultError
15
+
16
+ MAX_NESTING_DEPTH = 64
17
+
18
+ # Cache for dataclass introspection data: type_hints, field_list, field_names, required_fields
19
+ _params_type_cache: dict[type, tuple[dict[str, Any], tuple[Any, ...], list[str], list[str]]] = {}
20
+
21
+
22
+ def _get_params_type_info(params_type: type) -> tuple[dict[str, Any], tuple[Any, ...], list[str], list[str]]:
23
+ """Get cached introspection data for a params dataclass type."""
24
+ info = _params_type_cache.get(params_type)
25
+ if info is not None:
26
+ return info
27
+ type_hints = get_type_hints(params_type)
28
+ field_list = fields(params_type)
29
+ field_names = [f.name for f in field_list]
30
+ required_fields = [f.name for f in field_list if f.default is MISSING and f.default_factory is MISSING]
31
+ info = (type_hints, field_list, field_names, required_fields)
32
+ _params_type_cache[params_type] = info
33
+ return info
34
+
35
+
36
+ def is_batch(data: Any) -> bool:
37
+ """Check if data represents a batch request.
38
+
39
+ Args:
40
+ data: Parsed JSON data
41
+
42
+ Returns:
43
+ True if data is a list (batch request)
44
+ """
45
+ return isinstance(data, list)
46
+
47
+
48
+ def _type_name(t: type) -> str:
49
+ """Get human-readable name for a type."""
50
+ origin = get_origin(t)
51
+ if origin is Union:
52
+ args = get_args(t)
53
+ # Handle Optional (T | None)
54
+ if len(args) == 2 and type(None) in args:
55
+ other = args[0] if args[1] is type(None) else args[1]
56
+ return f'{_type_name(other)} | None'
57
+ return ' | '.join(_type_name(a) for a in args)
58
+ if origin is list:
59
+ args = get_args(t)
60
+ if args:
61
+ return f'list[{_type_name(args[0])}]'
62
+ return 'list'
63
+ if origin is dict:
64
+ args = get_args(t)
65
+ if args:
66
+ return f'dict[{_type_name(args[0])}, {_type_name(args[1])}]'
67
+ return 'dict'
68
+ if origin is Literal:
69
+ args = get_args(t)
70
+ return f'Literal{list(args)}'
71
+ if hasattr(t, '__name__'):
72
+ return t.__name__
73
+ return str(t)
74
+
75
+
76
+ def _check_type(value: Any, expected_type: type) -> bool:
77
+ """Check if value matches expected type.
78
+
79
+ Args:
80
+ value: Value to check
81
+ expected_type: Expected type annotation
82
+
83
+ Returns:
84
+ True if value matches type, False otherwise
85
+ """
86
+ if value is None:
87
+ origin = get_origin(expected_type)
88
+ # Check for Union (typing.Union) or UnionType (T | None syntax)
89
+ if origin is Union or isinstance(expected_type, types.UnionType):
90
+ return type(None) in get_args(expected_type)
91
+ return expected_type is type(None)
92
+
93
+ origin = get_origin(expected_type)
94
+
95
+ if origin is Union or isinstance(expected_type, types.UnionType):
96
+ args = get_args(expected_type)
97
+ return any(_check_type(value, arg) for arg in args)
98
+
99
+ if origin is Literal:
100
+ return value in get_args(expected_type)
101
+
102
+ if origin is list:
103
+ if not isinstance(value, list):
104
+ return False
105
+ args = get_args(expected_type)
106
+ if args:
107
+ item_type = args[0]
108
+ return all(_check_type(item, item_type) for item in value)
109
+ return True
110
+
111
+ if origin is dict:
112
+ if not isinstance(value, dict):
113
+ return False
114
+ args = get_args(expected_type)
115
+ if args:
116
+ key_type, val_type = args
117
+ return all(_check_type(k, key_type) and _check_type(v, val_type) for k, v in value.items())
118
+ return True
119
+
120
+ if expected_type is Any:
121
+ return True
122
+
123
+ if expected_type is int:
124
+ # int should not match bool (bool is subclass of int)
125
+ return isinstance(value, int) and not isinstance(value, bool)
126
+ if expected_type is float:
127
+ # float accepts int values
128
+ return isinstance(value, (int, float)) and not isinstance(value, bool)
129
+ if expected_type is bool:
130
+ return isinstance(value, bool)
131
+ if expected_type is str:
132
+ return isinstance(value, str)
133
+
134
+ if is_dataclass(expected_type) and isinstance(expected_type, type):
135
+ # Accept both dict (named params) and list (positional params)
136
+ return isinstance(value, (dict, list)) # Will be converted
137
+
138
+ try:
139
+ return isinstance(value, expected_type)
140
+ except TypeError:
141
+ raise InvalidParamsError(
142
+ f"Cannot validate type '{_type_name(expected_type)}' for value of type '{type(value).__name__}'. "
143
+ f'Unsupported type annotation.'
144
+ )
145
+
146
+
147
+ def validate_params(
148
+ params: list[Any] | dict[str, Any] | None,
149
+ params_type: type | None,
150
+ _depth: int = 0,
151
+ ) -> Any:
152
+ """Validate and convert params to dataclass instance.
153
+
154
+ Args:
155
+ params: Raw params from JSON-RPC request
156
+ params_type: Target dataclass type (or None for no-params methods)
157
+ _depth: Internal recursion depth counter (do not set manually)
158
+
159
+ Returns:
160
+ Validated dataclass instance, or None if params_type is None
161
+
162
+ Raises:
163
+ InvalidParamsError: With descriptive message on validation failure
164
+ """
165
+ if _depth > MAX_NESTING_DEPTH:
166
+ raise InvalidParamsError(f'Maximum nesting depth ({MAX_NESTING_DEPTH}) exceeded')
167
+
168
+ if params_type is None or params_type is type(None):
169
+ if params is not None and params != [] and params != {}:
170
+ raise InvalidParamsError(f'Method accepts no parameters, but received: {params}')
171
+ return None
172
+
173
+ if not is_dataclass(params_type):
174
+ raise InvalidParamsError(f'params_type must be a dataclass, got {type(params_type).__name__}')
175
+
176
+ type_hints, field_list, field_names, required_fields = _get_params_type_info(params_type)
177
+
178
+ if params is None:
179
+ if required_fields:
180
+ raise InvalidParamsError(f'Missing required parameters: {required_fields}')
181
+ return params_type()
182
+
183
+ if isinstance(params, list):
184
+ if len(params) > len(field_names):
185
+ raise InvalidParamsError(f'Too many positional parameters: expected {len(field_names)}, got {len(params)}')
186
+ params = dict(zip(field_names, params))
187
+
188
+ if not isinstance(params, dict):
189
+ raise InvalidParamsError(f'Parameters must be object or array, got {type(params).__name__}')
190
+
191
+ for field_name in params:
192
+ if field_name not in type_hints:
193
+ raise InvalidParamsError(f"Unknown parameter: '{field_name}'")
194
+
195
+ for field_name, value in params.items():
196
+ expected_type = type_hints[field_name]
197
+ if not _check_type(value, expected_type):
198
+ raise InvalidParamsError(
199
+ f"Parameter '{field_name}' expected type '{_type_name(expected_type)}', got '{type(value).__name__}'"
200
+ )
201
+
202
+ for f in field_list:
203
+ if f.name not in params:
204
+ if f.default is MISSING and f.default_factory is MISSING:
205
+ raise InvalidParamsError(f"Missing required parameter: '{f.name}'")
206
+
207
+ converted_params = {}
208
+ for field_name, value in params.items():
209
+ expected_type = type_hints[field_name]
210
+ converted_params[field_name] = _convert_value(value, expected_type, _depth=_depth)
211
+
212
+ return params_type(**converted_params)
213
+
214
+
215
+ def _convert_value(value: Any, expected_type: type, _depth: int = 0) -> Any:
216
+ """Convert value to expected type, handling nested dataclasses."""
217
+ if value is None:
218
+ return None
219
+
220
+ origin = get_origin(expected_type)
221
+
222
+ if origin is Union or isinstance(expected_type, types.UnionType):
223
+ args = get_args(expected_type)
224
+ # Try non-None types first
225
+ for arg in args:
226
+ if arg is not type(None):
227
+ try:
228
+ return _convert_value(value, arg, _depth=_depth)
229
+ except (TypeError, ValueError):
230
+ continue
231
+ return value
232
+
233
+ if origin is list and isinstance(value, list):
234
+ args = get_args(expected_type)
235
+ if args:
236
+ item_type = args[0]
237
+ return [_convert_value(item, item_type, _depth=_depth) for item in value]
238
+ return value
239
+
240
+ if origin is dict and isinstance(value, dict):
241
+ args = get_args(expected_type)
242
+ if args and len(args) == 2:
243
+ val_type = args[1]
244
+ return {k: _convert_value(v, val_type, _depth=_depth) for k, v in value.items()}
245
+ return value
246
+
247
+ if is_dataclass(expected_type) and isinstance(expected_type, type):
248
+ if isinstance(value, (dict, list)):
249
+ # Recursively validate nested dataclass
250
+ return validate_params(value, expected_type, _depth=_depth + 1)
251
+
252
+ return value
253
+
254
+
255
+ def validate_result_type(result: Any, result_type: type) -> None:
256
+ """Validate that result matches expected type.
257
+
258
+ Args:
259
+ result: Actual return value from execute()
260
+ result_type: Expected type (basic type or dataclass)
261
+
262
+ Raises:
263
+ InvalidResultError: If result doesn't match expected type
264
+ """
265
+ if not _check_type(result, result_type):
266
+ raise InvalidResultError(f"Expected return type '{_type_name(result_type)}', got '{type(result).__name__}'")
267
+
268
+
269
+ def _unwrap_optional(type_hint: type) -> type:
270
+ """Unwrap Optional[T] or T | None to T.
271
+
272
+ For params, we want the actual type, not Optional wrapper.
273
+
274
+ Args:
275
+ type_hint: Type annotation that may be Optional
276
+
277
+ Returns:
278
+ Unwrapped type (T from Optional[T] or T | None)
279
+
280
+ Examples:
281
+ Optional[AddParams] -> AddParams
282
+ AddParams | None -> AddParams
283
+ AddParams -> AddParams
284
+ None -> type(None)
285
+ """
286
+ origin = get_origin(type_hint)
287
+
288
+ # Handle Union types (including T | None syntax)
289
+ if origin is Union or isinstance(type_hint, types.UnionType):
290
+ args = get_args(type_hint)
291
+
292
+ # If it's a 2-arg Union with None, return the non-None type
293
+ if len(args) == 2 and type(None) in args:
294
+ result: type = args[0] if args[1] is type(None) else args[1]
295
+ return result
296
+
297
+ return type_hint
@@ -0,0 +1,141 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-jsonrpc-lib
3
+ Version: 0.3.1
4
+ Summary: Simple, yet solid - Type-safe JSON-RPC 1.0/2.0 with OpenAPI support
5
+ Project-URL: Homepage, https://github.com/uandysmith/python-jsonrpc-lib
6
+ Project-URL: Documentation, https://uandysmith.github.io/python-jsonrpc-lib/
7
+ Project-URL: Repository, https://github.com/uandysmith/python-jsonrpc-lib
8
+ Project-URL: Issues, https://github.com/uandysmith/python-jsonrpc-lib/issues
9
+ Author: Andy Smith
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: dataclass,json-rpc,jsonrpc,protocol,rpc,validation
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.11
24
+ Provides-Extra: dev
25
+ Requires-Dist: mypy>=1.0; extra == 'dev'
26
+ Requires-Dist: ruff>=0.15.0; extra == 'dev'
27
+ Provides-Extra: docs
28
+ Requires-Dist: mkdocs-material>=9.5.0; extra == 'docs'
29
+ Requires-Dist: mkdocs>=1.5.0; extra == 'docs'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # python-jsonrpc-lib
33
+
34
+ **Simple, yet solid.** JSON-RPC 1.0/2.0 for Python.
35
+
36
+ JSON-RPC is a small protocol: a method name, some parameters, a result. python-jsonrpc-lib keeps it that way. You write ordinary Python functions and dataclasses; the library handles validation, routing, error responses, and API documentation. No framework lock-in, no external dependencies, no boilerplate.
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install python-jsonrpc-lib
42
+ ```
43
+
44
+ ## Quickstart
45
+
46
+ Define methods as classes with typed parameters. The library validates inputs, routes calls, and builds responses automatically.
47
+
48
+ ```python
49
+ from dataclasses import dataclass
50
+ from jsonrpc import JSONRPC, Method, MethodGroup
51
+
52
+ @dataclass
53
+ class AddParams:
54
+ a: int
55
+ b: int
56
+
57
+ class Add(Method):
58
+ def execute(self, params: AddParams) -> int:
59
+ return params.a + params.b
60
+
61
+ @dataclass
62
+ class GreetParams:
63
+ name: str
64
+ greeting: str = 'Hello'
65
+
66
+ class Greet(Method):
67
+ def execute(self, params: GreetParams) -> str:
68
+ return f'{params.greeting}, {params.name}!'
69
+
70
+ rpc = JSONRPC(version='2.0')
71
+ rpc.register('add', Add())
72
+ rpc.register('greet', Greet())
73
+
74
+ response = rpc.handle('{"jsonrpc": "2.0", "method": "add", "params": {"a": 5, "b": 3}, "id": 1}')
75
+ # '{"jsonrpc": "2.0", "result": 8, "id": 1}'
76
+ ```
77
+
78
+ Pass in a JSON string, get a JSON string back. What carries it over the wire is up to you.
79
+
80
+ If `a` is `"five"` instead of `5`, the caller receives a `-32602 Invalid params` error immediately — no exception handling on your end.
81
+
82
+ The same `AddParams` dataclass drives validation, IDE autocomplete, and the OpenAPI schema.
83
+
84
+ ## Why python-jsonrpc-lib?
85
+
86
+ - **Zero dependencies** — pure Python 3.11+. Nothing to pin, nothing to audit beyond the library itself.
87
+ - **Type validation from dataclasses** — declare parameters as a dataclass, get automatic validation and clear error messages for free.
88
+ - **OpenAPI docs auto-generated** — type hints and docstrings you already wrote become a full OpenAPI 3.0 spec. Point any Swagger-compatible UI at it and your API is self-documented.
89
+ - **Transport-agnostic** — `rpc.handle(json_string)` returns a string. HTTP, WebSocket, TCP, message queue: your choice.
90
+ - **Spec-compliant by default** — v1.0 and v2.0 rules enforced out of the box, configurable when you need to support legacy clients.
91
+
92
+ ## Namespacing and Middleware
93
+
94
+ Use `MethodGroup` to organize methods into namespaces and add cross-cutting concerns:
95
+
96
+ ```python
97
+ math = MethodGroup()
98
+ math.register('add', Add())
99
+
100
+ rpc = JSONRPC(version='2.0')
101
+ rpc.register('math', math)
102
+
103
+ # "math.add" is now available
104
+ ```
105
+
106
+ ## Quick Prototyping
107
+
108
+ For scripts and throwaway code, the `@rpc.method` decorator registers functions directly (v2.0 only):
109
+
110
+ ```python
111
+ rpc = JSONRPC(version='2.0')
112
+
113
+ @rpc.method
114
+ def add(a: int, b: int) -> int:
115
+ return a + b
116
+ ```
117
+
118
+ For production use, prefer `Method` classes — they support context, middleware, and groups.
119
+
120
+ ## Documentation
121
+
122
+ Full documentation with tutorials, integration guides, and API reference:
123
+
124
+ - [Tutorial: Hello World](docs/tutorial/01-hello-world.md) — first method, explained
125
+ - [Tutorial: Parameters](docs/tutorial/03-parameters.md) — dataclass validation in detail
126
+ - [Tutorial: Context](docs/tutorial/05-context.md) — authentication and per-request data
127
+ - [Tutorial: OpenAPI](docs/tutorial/07-openapi.md) — interactive API documentation
128
+ - [Flask integration](docs/integrations/flask.md)
129
+ - [FastAPI integration](docs/integrations/fastapi.md)
130
+ - [Philosophy](docs/philosophy.md) — design decisions and trade-offs
131
+ - [API Reference](docs/api-reference.md)
132
+
133
+ ## Claude Code Integration
134
+
135
+ If you use [Claude Code](https://claude.ai/claude-code), a skill for this library is available. It gives Claude built-in knowledge of jsonrpc-lib's API: creating methods, registering them, organizing with groups, handling errors, and adding context and middleware — without having to look up docs.
136
+
137
+ To use it, add the skill file to your project's `.claude/skills/` directory.
138
+
139
+ ## License
140
+
141
+ MIT
@@ -0,0 +1,14 @@
1
+ jsonrpc/__init__.py,sha256=dzCKUITaRkigRAf_n84Ah_ffHgf9sGYK7Z2h9nB4-Dc,2106
2
+ jsonrpc/errors.py,sha256=fJ1HBSKtvgSKmZym7-7BXZQfYfBhXtnRu0vXM07JKwI,3401
3
+ jsonrpc/jsonrpc.py,sha256=Yd1mx6ftzAWV78zEKcXGXxlDje6BfignfCP8qGfltyQ,34432
4
+ jsonrpc/method.py,sha256=aLGj3QkpckEoGQT5PPt6hgSHWKoLBQjwjMwZXkIWsdg,20245
5
+ jsonrpc/openapi.py,sha256=gPnjNmcum7seZ_QtVWHaNsqQ3DYGHS1l46v461tRMcQ,16455
6
+ jsonrpc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ jsonrpc/request.py,sha256=ki9L0dv8H1Gxffp5OEAaWgsFHih1P476CC0OZdE33ms,5106
8
+ jsonrpc/response.py,sha256=JPLD-xXo9SR0LtmHaa2dTn7Sp0L4H2ZvHI_eR578Uzw,4506
9
+ jsonrpc/types.py,sha256=qk73MfkyyXMOOYmJAg6I45FZcFzDmgUQajj4eqbtiZU,1231
10
+ jsonrpc/validation.py,sha256=zm2TAyTx8Hp3oJR77_7D9d-lRI1QD5BPlAnf3s2XCjw,10130
11
+ python_jsonrpc_lib-0.3.1.dist-info/METADATA,sha256=merT9rsOAFkY70K8I99CH44x0E6q_oEHaXWBrEt7y-4,5326
12
+ python_jsonrpc_lib-0.3.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
13
+ python_jsonrpc_lib-0.3.1.dist-info/licenses/LICENSE,sha256=NQXsc3PwCv4Fetw7R3qXdaoLue9xiSYf_dMAzKU2jYA,1063
14
+ python_jsonrpc_lib-0.3.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any