1
0
mirror of https://github.com/dkmstr/openuds.git synced 2024-12-22 13:34:04 +03:00

Added xperimental support for streaming some kind of results.

Currently no one streams anything, way to go to prepare everything for this.
also, removed non-unsed non-maintained filtering
This commit is contained in:
Adolfo Gómez García 2024-03-27 22:44:11 +01:00
parent 1780e6c2d9
commit c8f402a419
No known key found for this signature in database
GPG Key ID: DD1ABF20724CDA23
5 changed files with 70 additions and 76 deletions

View File

@ -56,6 +56,7 @@ logger = logging.getLogger(__name__)
__all__ = ['Handler', 'Dispatcher']
@dataclasses.dataclass(frozen=True)
class HandlerNode:
"""
@ -91,7 +92,9 @@ class Dispatcher(View):
services: typing.ClassVar[HandlerNode] = HandlerNode('', None, {})
@method_decorator(csrf_exempt)
def dispatch(self, request: 'http.request.HttpRequest', *args: typing.Any, **kwargs: typing.Any) -> 'http.HttpResponse':
def dispatch(
self, request: 'http.request.HttpRequest', *args: typing.Any, **kwargs: typing.Any
) -> 'http.HttpResponse':
"""
Processes the REST request and routes it wherever it needs to be routed
"""
@ -183,8 +186,19 @@ class Dispatcher(View):
try:
response = operation()
if not handler.raw: # Raw handlers will return an HttpResponse Object
response = processor.get_response(response)
# If response is an HttpResponse object, return it directly
if not isinstance(response, http.HttpResponse):
# If it is a generator, produce an streamed incremental response
if isinstance(response, collections.abc.Generator):
response = typing.cast(
'http.HttpResponse',
http.StreamingHttpResponse(
processor.as_incremental(response),
content_type="application/json",
),
)
else:
response = processor.get_response(response)
# Set response headers
response['UDS-Version'] = f'{consts.system.VERSION};{consts.system.VERSION_STAMP}'
for k, val in handler.headers().items():
@ -249,9 +263,7 @@ class Dispatcher(View):
if name not in service_node.children:
service_node.children[name] = HandlerNode(name, None, {})
service_node.children[name] = dataclasses.replace(
service_node.children[name], handler=type_
)
service_node.children[name] = dataclasses.replace(service_node.children[name], handler=type_)
# Initializes the dispatchers
@staticmethod
@ -280,4 +292,5 @@ class Dispatcher(View):
package_name='methods',
)
Dispatcher.initialize()

View File

@ -58,7 +58,6 @@ class Handler:
REST requests handler base class
"""
raw: typing.ClassVar[bool] = False # If true, Handler will return directly an HttpResponse Object
name: typing.ClassVar[typing.Optional[str]] = (
None # If name is not used, name will be the class name in lower case
)

View File

@ -31,9 +31,7 @@
"""
# pylint: disable=too-many-public-methods
import fnmatch
import logging
import re
import typing
import collections.abc
@ -189,58 +187,6 @@ class ModelHandler(BaseModelHandler):
# End overridable
def extract_filter(self) -> None:
# Extract filter from params if present
self.fltr = None
if 'filter' in self._params:
self.fltr = self._params['filter']
del self._params['filter'] # Remove parameter
logger.debug('Found a filter expression (%s)', self.fltr)
def filter(self, data: typing.Any) -> typing.Any:
# Right now, filtering only supports a single filter, in a future
# we may improve it
if self.fltr is None:
return data
# Filtering a non iterable (list or tuple)
if not isinstance(data, collections.abc.Iterable):
return data
logger.debug('data: %s, fltr: %s', typing.cast(typing.Any, data), self.fltr)
try:
fld, pattern = self.fltr.split('=')
s, e = '', ''
if pattern[0] == '^':
pattern = pattern[1:]
s = '^'
if pattern[-1] == '$':
pattern = pattern[:-1]
e = '$'
r = re.compile(s + fnmatch.translate(pattern) + e, re.RegexFlag.IGNORECASE)
def fltr_function(item: typing.Any) -> bool:
if not isinstance(item, dict):
return False
try:
if fld not in item or r.match(typing.cast(dict[str, typing.Any], item)[fld]) is None:
return False
except Exception:
return False
return True
res: list[dict[str, typing.Any]] = list(
filter(fltr_function, typing.cast(collections.abc.Iterable[dict[str, typing.Any]], data))
)
logger.debug('After filtering: %s', res)
return res
except Exception as e:
logger.exception('Exception:')
logger.info('Filtering expression %s is invalid!', self.fltr)
raise exceptions.rest.RequestError(f'Filtering expression {self.fltr} is invalid') from e
# Helper to process detail
# Details can be managed (writen) by any user that has MANAGEMENT permission over parent
def process_detail(self) -> typing.Any:
@ -338,9 +284,7 @@ class ModelHandler(BaseModelHandler):
"""
Wraps real get method so we can process filters if they exists
"""
# Extract filter from params if present
self.extract_filter()
return self.filter(self.process_get())
return self.process_get()
# pylint: disable=too-many-return-statements
def process_get(self) -> typing.Any:

View File

@ -42,6 +42,8 @@ from django.utils.functional import Promise as DjangoPromise
from uds.core import consts
from .utils import to_incremental_json
# from xml_marshaller import xml_marshaller
logger = logging.getLogger(__name__)
@ -96,6 +98,12 @@ class ContentProcessor:
"""
return str(obj)
def as_incremental(self, obj: typing.Any) -> collections.abc.Iterable[bytes]:
"""
Renders an obj to the specific type, but in an incremental way (if possible)
"""
yield self.render(obj).encode('utf8')
@staticmethod
def process_for_render(obj: typing.Any) -> typing.Any:
"""
@ -108,13 +116,19 @@ class ContentProcessor:
return str(obj) # This is for translations
if isinstance(obj, dict):
return {k: ContentProcessor.process_for_render(v) for k, v in typing.cast(dict[str, typing.Any], obj).items()}
return {
k: ContentProcessor.process_for_render(v)
for k, v in typing.cast(dict[str, typing.Any], obj).items()
}
if isinstance(obj, bytes):
return obj.decode('utf-8')
if isinstance(obj, collections.abc.Iterable):
return [ContentProcessor.process_for_render(v) for v in typing.cast(collections.abc.Iterable[typing.Any], obj)]
return [
ContentProcessor.process_for_render(v)
for v in typing.cast(collections.abc.Iterable[typing.Any], obj)
]
if isinstance(obj, (datetime.datetime,)): # Datetime as timestamp
return int(time.mktime(obj.timetuple()))
@ -166,6 +180,10 @@ class JsonProcessor(MarshallerProcessor):
extensions: typing.ClassVar[collections.abc.Iterable[str]] = ['json']
marshaller: typing.ClassVar[typing.Any] = json
def as_incremental(self, obj: typing.Any) -> collections.abc.Iterable[bytes]:
for i in to_incremental_json(obj):
yield i.encode('utf8')
# ---------------
# XML Processor

View File

@ -28,8 +28,10 @@
'''
Author: Adolfo Gómez, dkmaster at dkmon dot com
'''
import json
import typing
import re
import collections.abc
from uds.core.consts.system import VERSION
from uds.core.util.model import sql_stamp_seconds
@ -42,6 +44,7 @@ def rest_result(result: typing.Any, **kwargs: typing.Any) -> dict[str, typing.An
# A common possible value in kwargs is "error"
return {'result': result, 'stamp': sql_stamp_seconds(), 'version': VERSION, **kwargs}
def camel_and_snake_case_from(text: str) -> tuple[str, str]:
'''
Returns a tuple with the camel case and snake case of a text
@ -53,3 +56,20 @@ def camel_and_snake_case_from(text: str) -> tuple[str, str]:
camel_case_name = camel_case_name[0].lower() + camel_case_name[1:]
return camel_case_name, snake_case_name
def to_incremental_json(
source: collections.abc.Generator[typing.Any, None, None]
) -> typing.Generator[str, None, None]:
'''
Converts a generator to a json incremental string
'''
yield '['
first = True
for item in source:
if first:
first = False
else:
yield ','
yield json.dumps(item)
yield ']'