import gevent
import logging
import re
import types
from jadi import interface
[docs]def url(pattern):
"""
Exposes the decorated method of your :class:`HttpPlugin` via HTTP.
Will be deprecated in favor of new decorators ( @get, @post, ... )
:param pattern: URL regex (``^`` and ``$`` are implicit)
:type pattern: str
:rtype: function
Named capture groups will be fed to function as ``**kwargs``
"""
def decorator(f):
f.url_pattern = re.compile(f'^{pattern}$')
return f
return decorator
[docs]def requests_decorator_generator(method):
"""
Factorization to generate request decorators like @get or @post.
:param method: Request method decorator to generate, like get or post
:type method: basestring
:return:
:rtype:
"""
def request_decorator(pattern):
"""
Exposes the decorated method of your :class:`HttpPlugin` via HTTP
:param pattern: URL regex (``^`` and ``$`` are implicit)
:type pattern: str
:rtype: function
Named capture groups will be fed to function as ``**kwargs``
"""
def decorator(f):
# Request method involved, like get or post
f.method = method
f.url_pattern = re.compile(f'^{pattern}$')
return f
return decorator
return request_decorator
# Decorators like @get and @post are defined here
http_methods = ['get', 'post', 'delete', 'head', 'put', 'patch']
webdav_methods = ['propfind', 'mkcol', 'options', 'proppatch', 'copy', 'move', 'lock', 'unlock']
for method in http_methods + webdav_methods:
globals()[method] = requests_decorator_generator(method)
[docs]class BaseHttpHandler():
"""
Base class for everything that can process HTTP requests
"""
[docs] def handle(self, http_context):
"""
Should create a HTTP response in the given ``http_context`` and return
the plain output
:param http_context: HTTP context
:type http_context: :class:`aj.http.HttpContext`
"""
[docs]@interface
class HttpMiddleware(BaseHttpHandler):
def __init__(self, context):
self.context = context
[docs] def handle(self, http_context):
pass
[docs]@interface
class HttpMasterMiddleware(BaseHttpHandler):
def __init__(self, context):
self.context = context
[docs] def handle(self, http_context):
pass
[docs]@interface
class HttpPlugin():
"""
A base interface for HTTP request handling::
@component
class HelloHttp(HttpPlugin):
@get('/hello/(?P<name>.+)')
def get_page(self, http_context, name=None):
context.add_header('Content-Type', 'text/plain')
context.respond_ok()
return 'Hello, f"{name}"!'
"""
def __init__(self, context):
self.context = context
[docs] def handle(self, http_context):
"""
Finds and executes the handler for given request context
(handlers were methods decorated with :func:`url` and will be
decorated with e.g. @get and @post in the future)
:param http_context: HTTP context
:type http_context: :class:`aj.http.HttpContext`
:returns: reponse data
"""
def check_method(handle_function, http_context):
"""
Check if the requested method is supported by the function,
e.g. avoid accept a get request in a post method
:param handle_function:
:type handle_function:
:param http_context:
:type http_context:
:return:
:rtype:
"""
# Right http method called
if handle_function.method == http_context.method.lower():
return True
# Allow head request on get targets
if http_context.method.lower() == 'head' and handle_function.method == 'get':
return True
return False
for name, handle_function in self.__class__.__dict__.items():
if hasattr(handle_function, 'url_pattern'):
handle_function = getattr(self, name)
match = handle_function.url_pattern.match(http_context.path)
if match:
# New decorators @get, @post, @delete ... used
if hasattr(handle_function, 'method'):
if check_method(handle_function, http_context):
http_context.route_data = match.groupdict()
data = handle_function(http_context, **http_context.route_data)
if isinstance(data, str):
data = data.encode('utf-8')
if isinstance(data, types.GeneratorType):
return data
return [data]
else:
# Ensure compatibility with old @url decorator
logging.warning(f'Backward @url compatibility for {handle_function.__name__}')
http_context.route_data = match.groupdict()
data = handle_function(http_context, **http_context.route_data)
if isinstance(data, str):
data = data.encode('utf-8')
if isinstance(data, types.GeneratorType):
return data
return [data]
[docs]@interface
class SocketEndpoint():
"""
Base interface for Socket.IO endpoints.
"""
plugin = None
"""arbitrary plugin ID for socket message routing"""
def __init__(self, context):
self.context = context
self.greenlets = []
[docs] def on_connect(self, message):
"""
Called on a successful client connection
"""
[docs] def on_disconnect(self, message):
"""
Called on a client disconnect
"""
[docs] def destroy(self):
"""
Destroys endpoint, killing the running greenlets
"""
for gl in self.greenlets:
gl.kill(block=False)
[docs] def on_message(self, message, *args):
"""
Called when a socket message arrives to this endpoint
"""
[docs] def spawn(self, target, *args, **kwargs):
"""
Spawns a greenlet in this endpoint, which will be auto-killed when the client disconnects
:param target: target function
"""
logging.debug(
f'Spawning sub-Socket Greenlet (in a namespace): {target.__name__}'
)
greenlet = gevent.spawn(target, *args, **kwargs)
self.greenlets.append(greenlet)
return greenlet
[docs] def send(self, data, plugin=None):
"""
Sends a message to the client.the
:param data: message object
:param plugin: routing ID (this endpoint's ID if not specified)
:type plugin: str
"""
self.context.worker.send_to_upstream({
'type': 'socket',
'message': {
'plugin': plugin or self.plugin,
'data': data,
},
})