django-jsonrpc-framework 0.1.0__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.
Files changed (65) hide show
  1. django_jsonrpc_framework-0.1.0.dist-info/METADATA +101 -0
  2. django_jsonrpc_framework-0.1.0.dist-info/RECORD +65 -0
  3. django_jsonrpc_framework-0.1.0.dist-info/WHEEL +4 -0
  4. jsonrpc_framework/__init__.py +8 -0
  5. jsonrpc_framework/controller/__init__.py +7 -0
  6. jsonrpc_framework/controller/_base.py +103 -0
  7. jsonrpc_framework/controller/_route.py +71 -0
  8. jsonrpc_framework/controller/decor.py +81 -0
  9. jsonrpc_framework/controller/openrpc/__init__.py +0 -0
  10. jsonrpc_framework/controller/openrpc/_openrpc.py +101 -0
  11. jsonrpc_framework/controller/openrpc/collectors/__init__.py +5 -0
  12. jsonrpc_framework/controller/openrpc/collectors/_main_collector.py +57 -0
  13. jsonrpc_framework/controller/openrpc/collectors/_method_collector.py +198 -0
  14. jsonrpc_framework/core/__init__.py +3 -0
  15. jsonrpc_framework/core/error.py +37 -0
  16. jsonrpc_framework/core/models.py +41 -0
  17. jsonrpc_framework/logic/__init__.py +9 -0
  18. jsonrpc_framework/logic/dispatcher.py +140 -0
  19. jsonrpc_framework/logic/responser.py +27 -0
  20. jsonrpc_framework/logic/validator.py +56 -0
  21. jsonrpc_framework/management/commands/generate_openrpc.py +49 -0
  22. jsonrpc_framework/openrpc/__init__.py +0 -0
  23. jsonrpc_framework/openrpc/builder/__init__.py +0 -0
  24. jsonrpc_framework/openrpc/builder/builder.py +76 -0
  25. jsonrpc_framework/openrpc/document/__init__.py +0 -0
  26. jsonrpc_framework/openrpc/document/_base.py +8 -0
  27. jsonrpc_framework/openrpc/document/_openrpc_document.py +71 -0
  28. jsonrpc_framework/openrpc/document/common/__init__.py +27 -0
  29. jsonrpc_framework/openrpc/document/common/_descriptor.py +68 -0
  30. jsonrpc_framework/openrpc/document/common/_error.py +41 -0
  31. jsonrpc_framework/openrpc/document/common/_example.py +55 -0
  32. jsonrpc_framework/openrpc/document/common/_link.py +60 -0
  33. jsonrpc_framework/openrpc/document/common/_link_server.py +75 -0
  34. jsonrpc_framework/openrpc/document/common/_pairing_object.py +52 -0
  35. jsonrpc_framework/openrpc/document/common/_reference.py +26 -0
  36. jsonrpc_framework/openrpc/document/common/_schema.py +55 -0
  37. jsonrpc_framework/openrpc/document/common/_tag.py +45 -0
  38. jsonrpc_framework/openrpc/document/common/_utils.py +15 -0
  39. jsonrpc_framework/openrpc/document/components/__init__.py +5 -0
  40. jsonrpc_framework/openrpc/document/components/_components.py +87 -0
  41. jsonrpc_framework/openrpc/document/external_docs/__init__.py +5 -0
  42. jsonrpc_framework/openrpc/document/external_docs/_external_docs.py +27 -0
  43. jsonrpc_framework/openrpc/document/info/__init__.py +7 -0
  44. jsonrpc_framework/openrpc/document/info/_info.py +110 -0
  45. jsonrpc_framework/openrpc/document/method/__init__.py +5 -0
  46. jsonrpc_framework/openrpc/document/method/_method.py +134 -0
  47. jsonrpc_framework/openrpc/document/server/__init__.py +6 -0
  48. jsonrpc_framework/openrpc/document/server/_server.py +83 -0
  49. jsonrpc_framework/openrpc/tests/__init__.py +0 -0
  50. jsonrpc_framework/openrpc/tests/_conftest/__init__.py +6 -0
  51. jsonrpc_framework/openrpc/tests/_conftest/builder_conftest.py +0 -0
  52. jsonrpc_framework/openrpc/tests/_conftest/components_conftest.py +113 -0
  53. jsonrpc_framework/openrpc/tests/_conftest/external_doc_conftest.py +30 -0
  54. jsonrpc_framework/openrpc/tests/_conftest/info_conftest.py +59 -0
  55. jsonrpc_framework/openrpc/tests/_conftest/method_conftest.py +261 -0
  56. jsonrpc_framework/openrpc/tests/_conftest/server_conftest.py +49 -0
  57. jsonrpc_framework/openrpc/tests/conftest.py +1 -0
  58. jsonrpc_framework/openrpc/tests/test_builder.py +40 -0
  59. jsonrpc_framework/openrpc/tests/test_components.py +11 -0
  60. jsonrpc_framework/openrpc/tests/test_external_doc.py +24 -0
  61. jsonrpc_framework/openrpc/tests/test_openrcp_info.py +24 -0
  62. jsonrpc_framework/openrpc/tests/test_openrcp_method.py +26 -0
  63. jsonrpc_framework/openrpc/tests/test_server.py +24 -0
  64. jsonrpc_framework/py.typed +0 -0
  65. jsonrpc_framework/templates/openrpc_docs.html +91 -0
@@ -0,0 +1,101 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-jsonrpc-framework
3
+ Version: 0.1.0
4
+ Summary: Django jsonrpc implementation
5
+ Classifier: Framework :: Django
6
+ Classifier: Programming Language :: Python :: 3
7
+ Requires-Python: >=3.13
8
+ Requires-Dist: django>=6.0.5
9
+ Requires-Dist: pydantic>=2.13.4
10
+ Description-Content-Type: text/markdown
11
+
12
+ # Django jsonrpc implementation
13
+
14
+ ## Advantages
15
+
16
+ - Complete support jsonrpc 2.0 (Request, Notificatin, Batch)
17
+ - Auto generation openrpc.json 1.3.2 version
18
+ - Auto generation OpenRPC documentation (like swagger)
19
+ - Async support
20
+
21
+ ## Create methods
22
+
23
+ We provide several methods creating methods.
24
+
25
+ - Using `method_` prefix
26
+ - Using `jsonrpc_method` decorator
27
+ - Rename existing func to new name
28
+
29
+ ``` python
30
+
31
+ from jsonrpc_framework import BaseController
32
+ from jsonrpc_framework.controller.decor import jsonrpc_method
33
+
34
+ class EchoController(BaseController):
35
+
36
+ def method_echo_hello(self, name: str) -> str:
37
+ return f"hello {name}"
38
+
39
+ @jsonrpc_method
40
+ def echo_goodbye(self, name: str) -> str:
41
+ return f"goodbye {name}"
42
+
43
+ @jsonrpc_method("echo_see_you")
44
+ def wrong_name(self, name) -> str:
45
+ return "See you fron echo_see_you method"
46
+
47
+ ```
48
+
49
+ ## Adding several controllers to one controller
50
+
51
+ ``` python
52
+ from jsonrpc_framework import RouteController
53
+
54
+ class PrintController(BaseController):
55
+
56
+ async def method_print_hello(self, name) -> None:
57
+ print(f"hello {name}")
58
+
59
+ @jsonrpc_method
60
+ async def print_goodbye(self, name) -> None:
61
+ print(f"goodbye {name}")
62
+
63
+
64
+ route = RouteController(
65
+ 'jsonrpc',
66
+ controllers=[
67
+ PrintController,
68
+ EchoController,
69
+ ]
70
+
71
+ )
72
+ ```
73
+
74
+ ## Generation openrpc.json and OpenRpc documentation
75
+
76
+ ``` python
77
+ from jsonrpc_framework.controller.openrpc.collectors import OpenRpcCollector
78
+
79
+ collector = OpenRpcCollector(
80
+ PrintController,
81
+ EchoController,
82
+ title='My mini API'
83
+ )
84
+
85
+
86
+ urlpatterns = [
87
+ path('echorpc', EchoController,as_view),
88
+ path('jsonrpc', route.as_view()),
89
+ path('openrpc.json', OpenRpcJsonView.as_view(collector=collector)),
90
+ path('docs', OpenRpcDocView.as_view()),
91
+ ]
92
+
93
+ ```
94
+
95
+ ### Openrpc.json example
96
+
97
+ ![OpenRPC docs](docs/docs/openrpcjson.png)
98
+
99
+ ## Openrpc doc example
100
+
101
+ ![OpenRPC docs](docs/docs/docs.png)
@@ -0,0 +1,65 @@
1
+ jsonrpc_framework/__init__.py,sha256=X6E4p4lLRgxj719HI1OqnzXx5chipNKylno-qy7R-Vk,183
2
+ jsonrpc_framework/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ jsonrpc_framework/controller/__init__.py,sha256=_GdmPAgDZ35sAxn-lfSG_E5atL6trGSc1PmR2LKAI6s,129
4
+ jsonrpc_framework/controller/_base.py,sha256=VVixZa1WC6eRAQCPGC2djubOftO3SJ6YlKVzi5w7TxA,2980
5
+ jsonrpc_framework/controller/_route.py,sha256=Ts0-nfVQuzjnz1KZzE9FCAC_9PoQ3oPdKhbKHLXXMTs,2303
6
+ jsonrpc_framework/controller/decor.py,sha256=ULIsqmxQV0Kytvzx0n9m4ee8VrIjBpR04T8sVPWH6h8,2223
7
+ jsonrpc_framework/controller/openrpc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ jsonrpc_framework/controller/openrpc/_openrpc.py,sha256=k-eZeQzWpf3bvoeyFxI2vMrKX3eITbavUB9x3tH2R88,3498
9
+ jsonrpc_framework/controller/openrpc/collectors/__init__.py,sha256=DgCgfIjnbzwO902OquiDABFUfQz-QX_sBC0v8LeQfio,84
10
+ jsonrpc_framework/controller/openrpc/collectors/_main_collector.py,sha256=ZDfUPaOXkoArzv5NXuZx7qybL5cJ-yNxO4ctn8xpFYY,1762
11
+ jsonrpc_framework/controller/openrpc/collectors/_method_collector.py,sha256=vL8a2sEQRo1onx5RwRuK64F5-_uXpJnHIGq3i_IN_0I,7189
12
+ jsonrpc_framework/core/__init__.py,sha256=-offSuXGPBgoDOXP5w0Rabg3agv1XgF2QZXPTFj_Np8,239
13
+ jsonrpc_framework/core/error.py,sha256=JiGji2BP5i8HS6KL4mEY8G_TV41gQZFFqHT6zOIvWOk,864
14
+ jsonrpc_framework/core/models.py,sha256=XIz5VdkhBoFm_49nLu_6_24V3yoAfcuy8UyLDt0S_sY,923
15
+ jsonrpc_framework/logic/__init__.py,sha256=9WpK_qxeNUkpoI-Wy5rhaHODzHDQZ9dbxK9755G10OE,200
16
+ jsonrpc_framework/logic/dispatcher.py,sha256=G4PwVyzoHmIurVkgnQTeXeGkMtctNBz9BG4ZS69vsJw,4377
17
+ jsonrpc_framework/logic/responser.py,sha256=SuROgaMTl5FUgUKGk3P2it1I2j0Sq0J7qiIxUUTrj4c,837
18
+ jsonrpc_framework/logic/validator.py,sha256=URxgSMtKx5Vv5JwVIzrVHDTGAMBVPH4C0uWKFfqTXU8,1869
19
+ jsonrpc_framework/management/commands/generate_openrpc.py,sha256=OCJqXRBKQThOpSwMIa0AwyO4VlwOfjIsHacWug4J7dM,2000
20
+ jsonrpc_framework/openrpc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
+ jsonrpc_framework/openrpc/builder/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
+ jsonrpc_framework/openrpc/builder/builder.py,sha256=RChn14Y_LMXT6ocffCLZ8cGkGbNR_zFEZvyPI1sJQ8M,2545
23
+ jsonrpc_framework/openrpc/document/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ jsonrpc_framework/openrpc/document/_base.py,sha256=A3_YUF45ntSoC4yJvksNp6eVnwQnemrg1h06SYtthnU,174
25
+ jsonrpc_framework/openrpc/document/_openrpc_document.py,sha256=7kwopKwLUEsGhqjoqcWtU7NP9Vco4HgmHi6c_GHkYFs,2618
26
+ jsonrpc_framework/openrpc/document/common/__init__.py,sha256=f2lPC_J7ySZ-y6s-kcxZMol_je9aeGMHeIUeiSEzDJU,880
27
+ jsonrpc_framework/openrpc/document/common/_descriptor.py,sha256=lcjcIvYXnmmk51FUFvEca7SVPOV2ZiZ7Fb6jGtsX4nE,2185
28
+ jsonrpc_framework/openrpc/document/common/_error.py,sha256=_sqL49WhBSu-ENJfoJqSlJfkCLcMZ1zDODOKpMHaFSE,1253
29
+ jsonrpc_framework/openrpc/document/common/_example.py,sha256=7Dc16LhVrY-5OALFg2HmZKgB9vEi2zuETQZP68TD58s,1572
30
+ jsonrpc_framework/openrpc/document/common/_link.py,sha256=xCf-U18HrsXiaelMnGMLmg0Y-KOeXVuoofJZsbRVI-g,1868
31
+ jsonrpc_framework/openrpc/document/common/_link_server.py,sha256=GOkCFAZbzF2GpLeozVc9p7MbKhU7Ot7NudWx6vOy2ts,2281
32
+ jsonrpc_framework/openrpc/document/common/_pairing_object.py,sha256=dihAs6OGaWAvhzYrGM91nEf2rmefocDphKAFdNFUHbk,1818
33
+ jsonrpc_framework/openrpc/document/common/_reference.py,sha256=FW2lRN_U0soWSh0oR3XjLBhh3JJF6xrIKW8divzx3FA,489
34
+ jsonrpc_framework/openrpc/document/common/_schema.py,sha256=g_cglrAsSDUzojC5M7TZj_Wrv3f69ExBnSL7jJTEtvM,1546
35
+ jsonrpc_framework/openrpc/document/common/_tag.py,sha256=AkxN6IhImyMd8_ftqk7tV5eA4ZrHHa6UAbyvKss1SbA,1194
36
+ jsonrpc_framework/openrpc/document/common/_utils.py,sha256=rCuuexL7cu4aEoJdQ6GbtMdENkqTjhdtYR8p66PHtJs,403
37
+ jsonrpc_framework/openrpc/document/components/__init__.py,sha256=kXo5OBxjfHMWRbt8irCO8wpZG-4GVtCUE6pgF5L_nF0,127
38
+ jsonrpc_framework/openrpc/document/components/_components.py,sha256=0oNxA1Hbu1ge62oryTa58-inW3rTgP3SKPT-kDl8aeY,2962
39
+ jsonrpc_framework/openrpc/document/external_docs/__init__.py,sha256=gVyRBn9VbWR5UljuhxdYLYmFLjrMpu_8IaVq7c5LsSk,136
40
+ jsonrpc_framework/openrpc/document/external_docs/_external_docs.py,sha256=af7NTLK4nv9NSlsKwknID0FEg9vfS3yD_Dt3cJVrgrM,738
41
+ jsonrpc_framework/openrpc/document/info/__init__.py,sha256=6MNAtJ1M6yoGZjnEdYqVPpeQz3Iv-_2zhhNIQ9cIyyU,179
42
+ jsonrpc_framework/openrpc/document/info/_info.py,sha256=e_rmaccnyrrrE_VFSkKKQcKsRPXPvwQ3y2efg5qLPt4,2908
43
+ jsonrpc_framework/openrpc/document/method/__init__.py,sha256=O3TzgWKg9QpKb61TB2cLOAdLRM1IKEYkndUQVu1rops,111
44
+ jsonrpc_framework/openrpc/document/method/_method.py,sha256=n7kK0FHnzSAPSmyEONmpvVjVy_dVgSMtAh0afEjbglA,5404
45
+ jsonrpc_framework/openrpc/document/server/__init__.py,sha256=XJwc5s7oT5FiKSqm9XSLi057HNxhicb9DXC0SBSrqVg,175
46
+ jsonrpc_framework/openrpc/document/server/_server.py,sha256=_DYjWONMc3PaZJSxQaYZvBjkd43iQruSPpH5vGFqxa8,2788
47
+ jsonrpc_framework/openrpc/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
+ jsonrpc_framework/openrpc/tests/conftest.py,sha256=OcF-nvDZ34823vT5ViqD52n3fVuKpOlLszujHlRwjJo,68
49
+ jsonrpc_framework/openrpc/tests/test_builder.py,sha256=HAn2wGqBzjGwSC6BuStZVrRxIAMZE9JCY5IaJX1HMcA,1791
50
+ jsonrpc_framework/openrpc/tests/test_components.py,sha256=TKIGZQj3R-WFGdxk4oQg6v3qAwTDI6DrOgqKr5RuRio,454
51
+ jsonrpc_framework/openrpc/tests/test_external_doc.py,sha256=881BGXkEFmPMXzPsI-x1vhpml-ToLDt7SULvQPFD7SA,947
52
+ jsonrpc_framework/openrpc/tests/test_openrcp_info.py,sha256=we8RC5MPonQNUyP_eG3-0E-7i6LKGEUmN9wlrnwVAXU,707
53
+ jsonrpc_framework/openrpc/tests/test_openrcp_method.py,sha256=09oJJdRm_iCAwjQMGysCCSJlHKQ3-q3y8FaisQ2U4y4,798
54
+ jsonrpc_framework/openrpc/tests/test_server.py,sha256=WaH16ml1NIFCewmxOIPzUBYrMgnsplr2aJ2FCR7Ga60,784
55
+ jsonrpc_framework/openrpc/tests/_conftest/__init__.py,sha256=Z-mbYwidG9VCLrVjHdb58X1OwXy67wLJHX18H6m7bBs,519
56
+ jsonrpc_framework/openrpc/tests/_conftest/builder_conftest.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
+ jsonrpc_framework/openrpc/tests/_conftest/components_conftest.py,sha256=-ShdT61-KikgTGVfaksVeWfTnfidlKOlj_Gd8VapeZY,4343
58
+ jsonrpc_framework/openrpc/tests/_conftest/external_doc_conftest.py,sha256=w4Ag5hCQZ0qVhFucsk_JsbqZsLEmViHclaZidsMb6xs,917
59
+ jsonrpc_framework/openrpc/tests/_conftest/info_conftest.py,sha256=LXJf2pxCfc3EsmAjoAsBIkv221tw4bcGr-4fpE8xnDs,1562
60
+ jsonrpc_framework/openrpc/tests/_conftest/method_conftest.py,sha256=Hk9X4N964jX9yQE5q843rDyGfA2P7ktXeee8yO2AiAc,8604
61
+ jsonrpc_framework/openrpc/tests/_conftest/server_conftest.py,sha256=72vXOzmTOAfsJnYxCfO10TbGY2OgKZlVUwWUlAsCjKA,1501
62
+ jsonrpc_framework/templates/openrpc_docs.html,sha256=duo_IbWipuDYbUM-x2Aa3OcohiBrDkvBJMBoL4iVr6c,2670
63
+ django_jsonrpc_framework-0.1.0.dist-info/METADATA,sha256=jSPpKcdj_1r6vOIK1Zs5gzhYdGl-V6JtM_tzFA49nx4,2236
64
+ django_jsonrpc_framework-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
65
+ django_jsonrpc_framework-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,8 @@
1
+ from .controller import BaseController, RouteController
2
+ from .controller.decor import jsonrpc_method
3
+
4
+ __all__ = [
5
+ "BaseController",
6
+ "RouteController",
7
+ "jsonrpc_method",
8
+ ]
@@ -0,0 +1,7 @@
1
+ from ._base import BaseController
2
+ from ._route import RouteController
3
+
4
+ __all__ = [
5
+ "BaseController",
6
+ "RouteController",
7
+ ]
@@ -0,0 +1,103 @@
1
+ import logging
2
+ from typing import Any
3
+ from collections.abc import Callable
4
+
5
+ from django.views import View
6
+ from django.http import HttpRequest, HttpResponse
7
+
8
+ from jsonrpc_framework.logic.dispatcher import RpcDispatcher
9
+ from jsonrpc_framework.logic.validator import RequestValidator, RequestType, BatchType
10
+ from jsonrpc_framework.logic.responser import ResponseBuilder
11
+
12
+ from jsonrpc_framework.core.error import RpcError
13
+ from jsonrpc_framework.core.models import MethodType
14
+
15
+
16
+
17
+ logger = logging.getLogger("django.server")
18
+
19
+
20
+ class BaseController(View):
21
+ http_method_names = ["post"]
22
+ path: str = "jsonrpc"
23
+
24
+ registry: dict[MethodType, Callable[..., Any]]
25
+
26
+ dispatcher: RpcDispatcher
27
+ validator: RequestValidator
28
+ response_builder: ResponseBuilder
29
+
30
+
31
+ def __init__(self, *args: tuple[Any], **kwargs: dict[str, Any]):
32
+ super().__init__(*args, **kwargs)
33
+
34
+ self.registry = self._collect_declared_methods()
35
+
36
+ self.dispatcher = RpcDispatcher()
37
+ self.validator = RequestValidator()
38
+ self.response_builder = ResponseBuilder()
39
+
40
+
41
+ def _collect_declared_methods(self) -> dict[MethodType, Callable[..., Any]]:
42
+ registry: dict[MethodType, Callable[..., Any]] = {}
43
+
44
+ for name, value in vars(self.__class__).items():
45
+ if not callable(value):
46
+ continue
47
+
48
+ rpc_name = getattr(value, "__rpc_method_name__", None)
49
+
50
+ if rpc_name is not None:
51
+ method_name = rpc_name
52
+ elif name.startswith("method_"):
53
+ method_name = name.replace("method_", "")
54
+ else:
55
+ continue
56
+
57
+ if method_name in registry:
58
+ raise ValueError(f"Method {method_name} already registered in {self.__class__.__name__}")
59
+
60
+ registry[method_name] = getattr(self, name)
61
+
62
+ return registry
63
+
64
+ async def post(
65
+ self,
66
+ request: HttpRequest,
67
+ *args: tuple[Any],
68
+ **kwargs: dict[str, Any],
69
+ ) -> HttpResponse:
70
+ body = self.validator.validate_body(request.body)
71
+
72
+ result = await self.dispatcher.dispatch(body, registry=self.registry)
73
+ self._log_jsonrpc_methods(request, body)
74
+
75
+ return self.response_builder.build_response(result)
76
+
77
+ def _log_jsonrpc_methods(
78
+ self,
79
+ request: HttpRequest,
80
+ body: RequestType | BatchType | RpcError,
81
+ ) -> None:
82
+ methods = self._extract_method_names(body)
83
+
84
+ if methods:
85
+ logger.info(f"JSONRPC {request.path} {methods}")
86
+
87
+ def _extract_method_names(
88
+ self,
89
+ body: RequestType | BatchType | RpcError,
90
+ ) -> list[str] | str:
91
+ if isinstance(body, RpcError):
92
+ return []
93
+
94
+ if isinstance(body, list):
95
+ methods: list[str] = []
96
+
97
+ for item in body:
98
+ if isinstance(item, RpcError):
99
+ continue
100
+ methods.append(item.method)
101
+ return methods
102
+
103
+ return body.method
@@ -0,0 +1,71 @@
1
+ from collections import defaultdict
2
+ from collections.abc import Callable
3
+ from typing import Any
4
+
5
+
6
+ from jsonrpc_framework.controller._base import BaseController
7
+
8
+ class RouteController(BaseController):
9
+ """The conroller that collects BaseControllers to merge
10
+ their methods into a single controller.
11
+
12
+ Args:
13
+ path: The path to use in urlconfig.
14
+ controllers: The list of BaseControllers to merge.
15
+ """
16
+ controllers: list[type[BaseController] | BaseController] = []
17
+
18
+ def __init__(
19
+ self,
20
+ path: str,
21
+ controllers: list[type[BaseController] | BaseController],
22
+ ):
23
+ super().__init__()
24
+ self.path = path
25
+ self.controllers = controllers
26
+
27
+ merged_registry: dict[str, Callable[..., Any]] = {}
28
+ key_sources: dict[str, list[str]] = defaultdict(list)
29
+
30
+ for raw_controller in controllers:
31
+
32
+ controller = (
33
+ raw_controller()
34
+ if isinstance(raw_controller, type)
35
+ else raw_controller
36
+ )
37
+
38
+ if not isinstance(controller, BaseController):
39
+ raise TypeError(
40
+ "Each controller must be a BaseController instance or subclass"
41
+ )
42
+
43
+ controller_name = controller.__class__.__name__
44
+ for method_name, callback in controller.registry.items():
45
+ key_sources[method_name].append(controller_name)
46
+ if method_name in merged_registry:
47
+ continue
48
+ merged_registry[method_name] = callback
49
+
50
+ conflicts = {
51
+ method_name: names
52
+ for method_name, names in key_sources.items()
53
+ if len(names) > 1
54
+ }
55
+ if conflicts:
56
+ details = "; ".join(
57
+ f"{method!r} -> {sources}"
58
+ for method, sources in sorted(conflicts.items())
59
+ )
60
+ raise ValueError(f"Duplicate JSON-RPC methods detected: {details}")
61
+
62
+ self.registry = merged_registry
63
+
64
+
65
+ def as_view(self, **initkwargs: Any) -> Any: # type: ignore[override]
66
+ return BaseController.as_view.__func__( # type: ignore[attr-defined]
67
+ self.__class__,
68
+ path=self.path,
69
+ controllers=self.controllers,
70
+ **initkwargs,
71
+ )
@@ -0,0 +1,81 @@
1
+ from collections.abc import Callable
2
+ from functools import wraps
3
+ from typing import Any
4
+
5
+
6
+ def _decorate[R, **P](
7
+ func: Callable[P, R],
8
+ rpc_name: str,
9
+ summary: str | None = None,
10
+ description: str | None = None,
11
+ tags: list[str] | None = None,
12
+ ) -> Callable[P, R]:
13
+ # Keep an explicit RPC alias on the callable, because class attribute
14
+ # names are used during registry collection and cannot be renamed here.
15
+ setattr(func, "__rpc_method_name__", rpc_name)
16
+ setattr(func, "__rpc_method_summary__", summary)
17
+ setattr(func, "__rpc_method_description__", description)
18
+ setattr(func, "__rpc_method_tags__", tags)
19
+
20
+ @wraps(func)
21
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
22
+ return func(*args, **kwargs)
23
+
24
+ setattr(wrapper, "__rpc_method_name__", rpc_name)
25
+ setattr(wrapper, "__rpc_method_summary__", summary)
26
+ setattr(wrapper, "__rpc_method_description__", description)
27
+ setattr(wrapper, "__rpc_method_tags__", tags)
28
+
29
+ return wrapper
30
+
31
+
32
+ def simple_decorator[R, **P](
33
+ func: Callable[P, R],
34
+ ) -> Callable[P, R]:
35
+ return _decorate(
36
+ func,
37
+ rpc_name=func.__name__,
38
+ description=func.__doc__,
39
+ )
40
+
41
+
42
+ def parametrized_decorator[R, **P](
43
+ func: Callable[P, R],
44
+ *,
45
+ name: str | None = None,
46
+ summary: str | None = None,
47
+ description: str | None = None,
48
+ tags: list[str] | None = None,
49
+ ) -> Callable[P, R]:
50
+ rpc_name = name if isinstance(name, str) else func.__name__
51
+ return _decorate(
52
+ func,
53
+ rpc_name,
54
+ summary,
55
+ description,
56
+ tags,
57
+ )
58
+
59
+
60
+ def jsonrpc_method(
61
+ name_or_func: str | Callable[..., Any] | None = None,
62
+ *,
63
+ summary: str | None = None,
64
+ description: str | None = None,
65
+ tags: list[str] | None = None,
66
+ ) -> Callable[..., Any]:
67
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
68
+ return parametrized_decorator(
69
+ func,
70
+ name=name_or_func if isinstance(name_or_func, str) else None,
71
+ summary=summary,
72
+ description=description,
73
+ tags=tags,
74
+ )
75
+
76
+ if callable(name_or_func):
77
+ return simple_decorator(
78
+ name_or_func,
79
+ )
80
+
81
+ return decorator
File without changes
@@ -0,0 +1,101 @@
1
+ from pathlib import Path
2
+ from typing import Callable, override, Any
3
+
4
+ from django.views.generic.base import TemplateView
5
+ from django.conf import settings
6
+ from django.http import HttpRequest, HttpResponse, HttpResponseBase
7
+ from django.http.response import FileResponse
8
+
9
+ from django.views import View
10
+
11
+ from jsonrpc_framework.controller.openrpc.collectors import OpenRpcCollector
12
+
13
+ class OpenRpcJsonView(View):
14
+
15
+ http_method_names = ["get"]
16
+ path = "openrpc.json"
17
+ collector: OpenRpcCollector = OpenRpcCollector()
18
+
19
+ def __init__(
20
+ self,
21
+ *args: tuple[Any],
22
+ **kwargs: dict[str, Any]
23
+ ):
24
+ super().__init__(*args, **kwargs)
25
+
26
+ def get(self, request: HttpRequest, *args: tuple[Any], **kwargs: dict[str, Any]) -> HttpResponse | FileResponse:
27
+ file_path = getattr(
28
+ settings, "DJANGO_JSONRPC_DOCS", {}
29
+ ).get("FILE_PATH", None)
30
+
31
+ if file_path is not None and Path(file_path).exists():
32
+ return FileResponse(
33
+ Path(file_path).open("rb"),
34
+ filename=Path(file_path).name,
35
+ )
36
+
37
+ if not self.collector.is_collected:
38
+ self.collector.collect()
39
+
40
+ document = self.collector.build_document()
41
+
42
+ return HttpResponse(
43
+ content=document,
44
+ content_type="application/json; charset=utf-8",
45
+ )
46
+
47
+ @classmethod
48
+ @override
49
+ def as_view(
50
+ cls,
51
+ *,
52
+ collector: OpenRpcCollector | None = None,
53
+ **initkwargs: Any,
54
+ ) -> Callable[..., HttpResponseBase]:
55
+ return super().as_view(collector=collector, **initkwargs)
56
+
57
+
58
+
59
+ class OpenRpcDocView(TemplateView):
60
+ template_name = "openrpc_docs.html"
61
+
62
+ def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
63
+ ctx = super().get_context_data(**kwargs)
64
+
65
+ docs_cfg = getattr(settings, "DJANGO_JSONRPC_DOCS", {})
66
+
67
+ schema_path = docs_cfg.get("SCHEMA_PATH", "/openrpc.json")
68
+ schema_url = self.request.build_absolute_uri(schema_path)
69
+
70
+ ctx.update(
71
+ {
72
+ "openrpc_schema_url": schema_url,
73
+ "page_title": docs_cfg.get("TITLE", "OpenRPC Docs"),
74
+ "docs_react_css_url": docs_cfg.get(
75
+ "DOCS_REACT_CSS_URL",
76
+ "https://cdn.jsdelivr.net/npm/@open-rpc/docs-react@2.1.1/dist/docs-react.css",
77
+ ),
78
+ "esm_react_url": docs_cfg.get(
79
+ "ESM_REACT_URL",
80
+ "https://esm.sh/react@18.3.1",
81
+ ),
82
+ "esm_react_dom_url": docs_cfg.get(
83
+ "ESM_REACT_DOM_URL",
84
+ "https://esm.sh/react-dom@18.3.1/client?deps=react@18.3.1",
85
+ ),
86
+ "esm_openrpc_docs_url": docs_cfg.get(
87
+ "ESM_OPENRPC_DOCS_URL",
88
+ "https://esm.sh/@open-rpc/docs-react@2.1.1?deps=react@18.3.1,react-dom@18.3.1",
89
+ ),
90
+ "esm_mui_styles_url": docs_cfg.get(
91
+ "ESM_MUI_STYLES_URL",
92
+ "https://esm.sh/@mui/material@6.3.1/styles?deps=react@18.3.1,react-dom@18.3.1",
93
+ ),
94
+ "esm_mui_css_baseline_url": docs_cfg.get(
95
+ "ESM_MUI_CSS_BASELINE_URL",
96
+ "https://esm.sh/@mui/material@6.3.1/CssBaseline?deps=react@18.3.1,react-dom@18.3.1",
97
+ ),
98
+ "openrpc_theme_mode": docs_cfg.get("THEME_MODE", "dark"),
99
+ }
100
+ )
101
+ return ctx
@@ -0,0 +1,5 @@
1
+ from ._main_collector import OpenRpcCollector
2
+
3
+ __all__ = [
4
+ "OpenRpcCollector",
5
+ ]
@@ -0,0 +1,57 @@
1
+
2
+ from jsonrpc_framework.controller._base import BaseController
3
+ from jsonrpc_framework.openrpc.builder.builder import OpenRpcBuilder
4
+ from jsonrpc_framework.openrpc.document.info import OpenRpcContact, OpenRpcLicense
5
+
6
+ from jsonrpc_framework.controller.openrpc.collectors._method_collector import MethodsCollector
7
+
8
+ type ControllerType = type[BaseController] | BaseController
9
+
10
+ class OpenRpcCollector:
11
+
12
+ controllers: tuple[type[BaseController] | BaseController, ...]
13
+ builder: OpenRpcBuilder
14
+ is_collected: bool
15
+
16
+ method_collector: MethodsCollector
17
+
18
+ def __init__(
19
+ self,
20
+ *controllers: ControllerType,
21
+ title: str = "Django-jsonrpc API",
22
+ version: str = "1.0.0",
23
+ description: str | None = None,
24
+ terms_of_service: str | None = None,
25
+ contact: OpenRpcContact | None = None,
26
+ license: OpenRpcLicense | None = None,
27
+ ):
28
+ self.controllers = controllers
29
+
30
+ self.method_collector = MethodsCollector()
31
+ self.builder = OpenRpcBuilder(
32
+ title=title,
33
+ version=version,
34
+ description=description,
35
+ terms_of_service=terms_of_service,
36
+ contact=contact,
37
+ license=license,
38
+ )
39
+ self.is_collected = False
40
+
41
+ def collect(self) -> None:
42
+ for controller in self.controllers:
43
+ if isinstance(controller, type):
44
+ controller = controller()
45
+
46
+ self.method_collector.collect(controller)
47
+
48
+ for method in self.method_collector.methods:
49
+ self.builder.add_method(method)
50
+
51
+ self.builder.add_components(self.method_collector.components)
52
+ self.is_collected = True
53
+
54
+
55
+ def build_document(self) -> str:
56
+
57
+ return self.builder.build_json()