1
0
This commit is contained in:
2024-01-15 20:20:10 +00:00
parent c75ea4ea9d
commit 38cd64ea9a
87 changed files with 3946 additions and 2040 deletions

View File

@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# django-jsonrpc-aiohttp | (c) 2022-present Tomek Wójcik | MIT License
from .views import JSONRPCView # noqa
# bthlabs-jsonrpc-aiohttp | (c) 2022-present Tomek Wójcik | MIT License
from .executor import AioHttpExecutor # noqa: F401
from .views import JSONRPCView # noqa: F401
__version__ = '1.0.0'
__version__ = '1.1.0b1'

View File

@@ -1,38 +1,85 @@
# -*- coding: utf-8 -*-
# django-jsonrpc-aiohttp | (c) 2022-present Tomek Wójcik | MIT License
import logging
# bthlabs-jsonrpc-aiohttp | (c) 2022-present Tomek Wójcik | MIT License
from __future__ import annotations
from bthlabs_jsonrpc_core import Executor, JSONRPCAccessDeniedError
import logging
import inspect
import typing
from aiohttp import web
from bthlabs_jsonrpc_core import Codec, Executor, JSONRPCAccessDeniedError
from bthlabs_jsonrpc_core.exceptions import JSONRPCParseError
from bthlabs_jsonrpc_core.serializer import JSONRPCSerializer
LOGGER = logging.getLogger('bthlabs_jsonrpc.aiohttp.executor')
TCanCall = typing.Callable[[web.Request, str, list, dict], typing.Awaitable[bool]]
class AioHttpExecutor(Executor):
def __init__(self, request, can_call, namespace=None):
super().__init__(namespace=namespace)
"""AioHttp-specific executor."""
def __init__(self,
request: web.Request,
can_call: TCanCall,
namespace: str | None = None,
codec: Codec | None = None,
):
super().__init__(namespace=namespace, codec=codec)
self.request = request
self.can_call = can_call
async def list_methods(self, *args, **kwargs):
# pragma mark - Public interface
async def list_methods(self, *args, **kwargs) -> list[str]: # type: ignore[override]
return super().list_methods()
async def deserialize_data(self, request):
try:
return await request.json()
except Exception as exception:
LOGGER.error('Error deserializing RPC call!', exc_info=exception)
raise JSONRPCParseError()
async def deserialize_data(self, request: web.Request) -> typing.Any: # type: ignore[override]
"""
Deserializes *data* and returns the result.
def enrich_args(self, args):
Raises :py:exc:`JSONRPCParseError` if there was an error in the process.
"""
try:
payload = await request.text()
result = self.codec.decode(payload)
if inspect.isawaitable(result):
return await result
return result
except Exception as exception:
LOGGER.error(
'Unhandled exception when deserializing RPC call: %s',
exception,
exc_info=exception,
)
raise JSONRPCParseError() from exception
def enrich_args(self, args: list) -> list:
"""
Injects the current :py:class:`aiohttp.web.Request` as the first
argument.
"""
return [self.request, *super().enrich_args(args)]
async def before_call(self, method, args, kwargs):
async def before_call(self, method: str, args: list, kwargs: dict):
"""
Executes *can_call* and raises :py:exc:`JSONRPCAccessDeniedError`
accordingly.
"""
can_call = await self.can_call(self.request, method, args, kwargs)
if can_call is False:
raise JSONRPCAccessDeniedError(data='can_call')
async def execute(self):
async def execute(self) -> JSONRPCSerializer | None: # type: ignore[override]
"""
Executes the JSONRPC request.
Returns an instance of :py:class:`JSONRPCSerializer` or ``None`` if
the list of responses is empty.
"""
with self.execute_context() as execute_context:
data = await self.deserialize_data(self.request)

View File

@@ -1,8 +1,11 @@
# -*- coding: utf-8 -*-
# django-jsonrpc-aiohttp | (c) 2022-present Tomek Wójcik | MIT License
import typing
# bthlabs-jsonrpc-aiohttp | (c) 2022-present Tomek Wójcik | MIT License
from __future__ import annotations
import inspect
from aiohttp import web
from bthlabs_jsonrpc_core.codecs import Codec, JSONCodec
from bthlabs_jsonrpc_aiohttp.executor import AioHttpExecutor
@@ -20,14 +23,17 @@ class JSONRPCView:
app.add_routes([
web.post('/rpc', JSONRPCView()),
web.post('/example/rpc', JSONRPCView(namespace='examnple')),
web.post('/example/rpc', JSONRPCView(namespace='example')),
])
"""
# pragma mark - Public interface
def __init__(self,
namespace: str | None = None,
codec: type[Codec] | None = None):
self.namespace: str | None = namespace
self.codec: type[Codec] = codec or JSONCodec
def __init__(self, namespace: typing.Optional[str] = None):
self.namespace: typing.Optional[str] = namespace
# pragma mark - Public interface
async def can_call(self,
request: web.Request,
@@ -40,14 +46,33 @@ class JSONRPCView:
"""
return True
async def get_codec(self, request: web.Request) -> Codec:
"""Returns a codec configured for the *request*."""
return self.codec()
async def get_executor(self, request: web.Request) -> AioHttpExecutor:
"""Returns an executor configured for the *request*."""
codec = await self.get_codec(request)
return AioHttpExecutor(
request, self.can_call, namespace=self.namespace, codec=codec,
)
async def __call__(self, request: web.Request) -> web.Response:
"""The request handler."""
executor = AioHttpExecutor(
request, self.can_call, namespace=self.namespace,
)
executor = await self.get_executor(request)
serializer = await executor.execute()
if serializer is None:
return web.Response(body='')
return web.json_response(serializer.data)
codec = await self.get_codec(request)
body = codec.encode(serializer.data)
if inspect.isawaitable(body):
body = await body
return web.Response(
body=body,
content_type=codec.get_content_type(),
)