fixes for python 3.7 on REST

This commit is contained in:
Adolfo Gómez García 2019-09-05 11:29:57 +02:00
parent f464d78f99
commit 79f41b3e1a
7 changed files with 105 additions and 84 deletions

View File

@ -29,8 +29,12 @@
"""
@author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import os.path
import pkgutil
import sys
import time
import logging
import typing
from django import http
from django.views.generic.base import View
@ -51,7 +55,6 @@ from .handlers import (
from . import processors
logger = logging.getLogger(__name__)
__all__ = [str(v) for v in ['Handler', 'Dispatcher']]
@ -63,64 +66,62 @@ class Dispatcher(View):
"""
This class is responsible of dispatching REST requests
"""
# This attribute will contain all paths-->handler relations, added at Initialized method
services = {'': None} # Will include a default /rest handler, but rigth now this will be fine
# This attribute will contain all paths--> handler relations, filled at Initialized method
services: typing.ClassVar[typing.Dict[str, typing.Any]] = {'': None} # Will include a default /rest handler, but rigth now this will be fine
# pylint: disable=too-many-locals, too-many-return-statements, too-many-branches, too-many-statements
@method_decorator(csrf_exempt)
def dispatch(self, request, *args, **kwargs):
def dispatch(self, request: http.HttpRequest, *args, **kwargs):
"""
Processes the REST request and routes it wherever it needs to be routed
"""
# Remove session, so response middleware do nothing with this
# Remove session from request, so response middleware do nothing with this
del request.session
# Now we extract method and possible variables from path
path = kwargs['arguments'].split('/')
path: typing.List[str] = kwargs['arguments'].split('/')
del kwargs['arguments']
# Transverse service nodes too look for path
# Transverse service nodes, so we can locate class processing this path
service = Dispatcher.services
full_path = []
content_type = None
full_path_lst: typing.List[str] = []
# Guess content type from content type header (post) or ".xxx" to method
content_type: str = request.META.get('CONTENT_TYPE', 'json')
cls = None
while len(path) > 0:
# .json, .xml, ... will break path recursion
while path:
# .json, .xml, .anything will break path recursion
if path[0].find('.') != -1:
content_type = path[0].split('.')[1]
clean_path = path[0].split('.')[0]
if clean_path in service:
service = service[clean_path]
full_path.append(path[0])
full_path_lst.append(path[0])
path = path[1:]
else:
break
full_path = '/'.join(full_path)
full_path = '/'.join(full_path_lst)
logger.debug("REST request: %s (%s)", full_path, content_type)
# Here, service points to the path
cls = service['']
cls: typing.Optional[typing.Type[Handler]] = service['']
if cls is None:
return http.HttpResponseNotFound('method not found', content_type="text/plain")
return http.HttpResponseNotFound('Method not found', content_type="text/plain")
# Guess content type from content type header (post) or ".xxx" to method
try:
processor = processors.available_processors_ext_dict[content_type](request)
except Exception:
processor = processors.available_processors_mime_dict.get(request.META.get('CONTENT_TYPE', 'json'), processors.default_processor)(request)
processor = processors.available_processors_ext_dict.get(content_type, processors.default_processor)(request)
# Obtain method to be invoked
http_method = request.method.lower()
http_method: str = request.method.lower()
args = path
# Path here has "remaining" path, that is, method part has been removed
args = tuple(path)
handler = None
try:
handler = cls(request, full_path, http_method, processor.processParameters(), *args, **kwargs)
operation = getattr(handler, http_method)
operation: typing.Callable[[], typing.Any] = getattr(handler, http_method)
except processors.ParametersException as e:
logger.debug('Path: %s', full_path)
logger.debug('Error: %s', e)
@ -144,6 +145,7 @@ class Dispatcher(View):
if not handler.raw: # Raw handlers will return an HttpResponse Object
response = processor.getResponse(response)
# Set response headers
for k, val in handler.headers().items():
response[k] = val
return response
@ -164,25 +166,24 @@ class Dispatcher(View):
return http.HttpResponseServerError(str(e), content_type="text/plain")
@staticmethod
def registerSubclasses(classes):
def registerSubclasses(classes: typing.List[typing.Type[Handler]]):
"""
Try to register Handler subclasses that have not been inherited
"""
for cls in classes:
if len(cls.__subclasses__()) == 0: # Only classes that has not been inherited will be registered as Handlers
logger.debug('Found class %s', cls)
if cls.name is None:
if not cls.__subclasses__(): # Only classes that has not been inherited will be registered as Handlers
if not cls.name:
name = cls.__name__.lower()
else:
name = cls.name
logger.debug('Adding handler %s for method %s in path %s', cls, name, cls.path)
service_node = Dispatcher.services
if cls.path is not None:
service_node = Dispatcher.services # Root path
if cls.path:
for k in cls.path.split('/'):
if service_node.get(k) is None:
if k not in service_node:
service_node[k] = {'': None}
service_node = service_node[k]
if service_node.get(name) is None:
if name not in service_node:
service_node[name] = {'': None}
service_node[name][''] = cls
@ -196,11 +197,7 @@ class Dispatcher(View):
This imports all packages that are descendant of this package, and, after that,
it register all subclases of Handler. (In fact, it looks for packages inside "methods" package, child of this)
"""
import os.path
import pkgutil
import sys
logger.debug('Loading Handlers')
logger.info('Initializing REST Handlers')
# Dinamycally import children of this package.
package = 'methods'

View File

@ -40,6 +40,11 @@ from uds.core.auths.auth import getRootUser
from uds.models import Authenticator
from uds.core.managers import cryptoManager
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from django.http import HttpRequest # pylint: disable=ungrouped-imports
from uds.models import User
logger = logging.getLogger(__name__)
AUTH_TOKEN_HEADER = 'HTTP_X_AUTH_TOKEN'
@ -88,19 +93,26 @@ class Handler:
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
path: typing.ClassVar[typing.Optional[str]] = None # Path for this method, so we can do /auth/login, /auth/logout, /auth/auths in a simple way
authenticated: typing.ClassVar[bool] = True # By default, all handlers needs authentication
authenticated: bool = True # By default, all handlers needs authentication. Will be overwriten if needs_admin or needs_staff
needs_admin: typing.ClassVar[bool] = False # By default, the methods will be accessible by anyone if nothing else indicated
needs_staff: typing.ClassVar[bool] = False # By default, staff
_request: 'HttpRequest'
_path: str
_operation: str
_params: typing.Any
_args: typing.Tuple[str, ...]
_headers: typing.Dict[str, str]
_authToken: typing.Optional[str]
_user: typing.Optional['User']
# method names: 'get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'
def __init__(self, request, path, operation, params, *args, **kwargs):
def __init__(self, request: 'HttpRequest', path: str, operation: str, params: typing.Any, *args, **kwargs):
if self.needs_admin:
if self.needs_admin or self.needs_staff:
self.authenticated = True # If needs_admin, must also be authenticated
if self.needs_staff:
self.authenticated = True # Same for staff members
self._request = request
self._path = path
self._operation = operation
@ -137,14 +149,14 @@ class Handler:
"""
return self._headers
def header(self, headerName):
def header(self, headerName) -> typing.Optional[str]:
"""
Get's an specific header name from REST request
:param headerName: name of header to get
"""
return self._headers.get(headerName)
def addHeader(self, header, value):
def addHeader(self, header: str, value: str) -> None:
"""
Inserts a new header inside the headers list
:param header: name of header to insert
@ -152,7 +164,7 @@ class Handler:
"""
self._headers[header] = value
def removeHeader(self, header):
def removeHeader(self, header: str) -> None:
"""
Removes an specific header from the headers list
:param header: Name of header to remove
@ -163,14 +175,24 @@ class Handler:
pass # If not found, just ignore it
# Auth related
def getAuthToken(self):
def getAuthToken(self) -> typing.Optional[str]:
"""
Returns the authentication token for this REST request
"""
return self._authToken
@staticmethod
def storeSessionAuthdata(session, id_auth, username, password, locale, platform, is_admin, staff_member, scrambler):
def storeSessionAuthdata(
session: typing.MutableMapping[str, typing.Any],
id_auth: str,
username: str,
password: str,
locale: str,
platform: str,
is_admin: bool,
staff_member: bool,
scrambler: str
):
"""
Stores the authentication data inside current session
:param session: session handler (Djano user session object)
@ -193,7 +215,17 @@ class Handler:
'staff_member': staff_member
}
def genAuthToken(self, id_auth, username, password, locale, platform, is_admin, staf_member, scrambler):
def genAuthToken(
self,
id_auth: str,
username: str,
password: str,
locale: str,
platform: str,
is_admin: bool,
staf_member: bool,
scrambler: str
):
"""
Generates the authentication token from a session, that is basically
the session key itself
@ -211,7 +243,7 @@ class Handler:
self._session = session
return self._authToken
def cleanAuthToken(self):
def cleanAuthToken(self) -> None:
"""
Cleans up the authentication token
"""
@ -221,7 +253,7 @@ class Handler:
self._session = None
# Session related (from auth token)
def getValue(self, key):
def getValue(self, key) -> typing.Optional[str]:
"""
Get REST session related value for a key
"""
@ -230,7 +262,7 @@ class Handler:
except Exception:
return None # _session['REST'] does not exists?
def setValue(self, key, value):
def setValue(self, key: str, value: str) -> None:
"""
Set a session key value
"""
@ -241,19 +273,19 @@ class Handler:
except Exception:
logger.exception('Got an exception setting session value %s to %s', key, value)
def is_admin(self):
def is_admin(self) -> bool:
"""
True if user of this REST request is administrator
"""
return bool(self.getValue('is_admin'))
def is_staff_member(self):
def is_staff_member(self) -> bool:
"""
True if user of this REST request is member of staff
"""
return bool(self.getValue('staff_member'))
def getUser(self):
def getUser(self) -> 'User':
"""
If user is staff member, returns his Associated user on auth
"""

View File

@ -71,7 +71,7 @@ class Accounts(ModelHandler):
'tags': [tag.tag for tag in item.tags.all()],
'comments': item.comments,
'time_mark': item.time_mark,
'permission': permissions.getEffectivePermission(self._user, account)
'permission': permissions.getEffectivePermission(self._user, item)
}
def getGui(self, type_):
@ -84,4 +84,3 @@ class Accounts(ModelHandler):
def clear(self, item):
self.ensureAccess(item, permissions.PERMISSION_MANAGEMENT)
return item.usages.filter(user_service=None).delete()

View File

@ -566,8 +566,8 @@ class ModelHandler(BaseModelHandler):
The only detail that has types within is "Service", child of "Provider"
"""
# Authentication related
authenticated: typing.ClassVar[bool] = True
needs_staff: typing.ClassVar[bool] = True
authenticated = True
needs_staff = True
# Which model does this manage
model: models.Model

View File

@ -52,13 +52,15 @@ class ContentProcessor:
"""
Process contents (request/response) so Handlers can manage them
"""
mime_type: typing.ClassVar[typing.Optional[str]] = None
mime_type: typing.ClassVar[str] = ''
extensions: typing.ClassVar[typing.Iterable[str]] = []
def __init__(self, request):
_request: http.HttpRequest
def __init__(self, request: http.HttpRequest):
self._request = request
def processGetParameters(self):
def processGetParameters(self) -> typing.MutableMapping[str, typing.Any]:
"""
returns parameters based on request method
GET parameters are understood
@ -68,7 +70,7 @@ class ContentProcessor:
return self._request.GET.copy()
def processParameters(self):
def processParameters(self) -> typing.Any:
"""
Returns the parameter from the request
"""
@ -81,14 +83,14 @@ class ContentProcessor:
"""
return http.HttpResponse(content=self.render(obj), content_type=self.mime_type + "; charset=utf-8")
def render(self, obj):
def render(self, obj: typing.Any):
"""
Renders an obj to the spefific type
"""
return str(obj)
@staticmethod
def procesForRender(obj):
def procesForRender(obj: typing.Any):
"""
Helper for renderers. Alters some types so they can be serialized correctly (as we want them to be)
"""
@ -96,16 +98,10 @@ class ContentProcessor:
return obj
if isinstance(obj, dict):
res = {}
for k, v in obj.items():
res[k] = ContentProcessor.procesForRender(v)
return res
return {k:ContentProcessor.procesForRender(v) for k, v in obj.items()}
if isinstance(obj, (list, tuple, types.GeneratorType)):
res = []
for v in obj:
res.append(ContentProcessor.procesForRender(v))
return res
return [ContentProcessor.procesForRender(v) for v in obj]
if isinstance(obj, (datetime.datetime, datetime.date)):
return int(time.mktime(obj.timetuple()))
@ -149,7 +145,7 @@ class JsonProcessor(MarshallerProcessor):
"""
mime_type = 'application/json'
extensions = ['json']
marshaller = json
marshaller = json # type: ignore
# ---------------
# XML Processor
@ -166,9 +162,9 @@ class JsonProcessor(MarshallerProcessor):
processors_list = (JsonProcessor,)
default_processor = JsonProcessor
available_processors_mime_dict = dict((cls.mime_type, cls) for cls in processors_list)
available_processors_ext_dict = {}
default_processor: typing.Type[ContentProcessor] = JsonProcessor
available_processors_mime_dict: typing.Dict[str, typing.Type[ContentProcessor]] = {cls.mime_type: cls for cls in processors_list}
available_processors_ext_dict: typing.Dict[str, typing.Type[ContentProcessor]] = {}
for cls in processors_list:
for ext in cls.extensions:
available_processors_ext_dict[ext] = cls

View File

@ -26,7 +26,6 @@
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from __future__ import unicode_literals
import logging
logger = logging.getLogger(__name__)
@ -34,9 +33,7 @@ logger = logging.getLogger(__name__)
class RequestDebug:
"""
Add a X-UA-Compatible header to the response
This header tells to Internet Explorer to render page with latest
possible version or to use chrome frame if it is installed.
Used for logging some request data on develeopment
"""
def __init__(self, get_response):
self.get_response = get_response

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2013 Virtual Cable S.L.
# Copyright (c) 2013-2019 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,