190 lines
6.0 KiB
Python
190 lines
6.0 KiB
Python
import os
|
|
import functools
|
|
import logging
|
|
|
|
from . import exceptions
|
|
from . import protocol
|
|
from . import routing
|
|
from . import streaming
|
|
from . import rendering
|
|
|
|
class SiteCompositionPiece:
|
|
|
|
"""
|
|
Piece of a site, the composition of a multiple of which makes
|
|
up an entire site. Base class for Application and Subsite
|
|
handling the functionality both have in common.
|
|
"""
|
|
|
|
request_class = protocol.Request
|
|
response_class = protocol.Response
|
|
router_class = routing.Router
|
|
#renderer_class = rendering.JinjaRenderer
|
|
renderer_class = rendering.DOMinionRenderer
|
|
|
|
def __init__(self):
|
|
|
|
self.ooze_path = os.path.dirname(os.path.realpath(__file__))
|
|
self.project_path = os.getcwd()
|
|
|
|
self.logger = logging.getLogger(__name__)
|
|
self.logger.setLevel('DEBUG')
|
|
|
|
theme = 'default' # TODO: Read from config
|
|
self.theme = theme
|
|
self.theme_directories = []
|
|
|
|
ooze_default_path = f'{self.ooze_path}/themes/default'
|
|
project_default_path = f'{self.project_path}/themes/default'
|
|
|
|
if theme != 'default':
|
|
|
|
ooze_theme_path = f'{self.ooze_path}/themes/{theme}'
|
|
project_theme_path = f'{self.project_path}/themes/{theme}'
|
|
|
|
self.theme_directories.append(project_theme_path)
|
|
self.theme_directories.append(ooze_theme_path)
|
|
|
|
self.theme_directories.append(project_default_path)
|
|
self.theme_directories.append(ooze_default_path)
|
|
|
|
self.theme_directories = tuple(self.theme_directories)
|
|
self.subsites = {}
|
|
self.router = self.router_class(self)
|
|
self.renderer = self.renderer_class(self)
|
|
|
|
@property
|
|
def root(self):
|
|
raise exceptions.OozeProgrammingError("SiteCompositionPiece MUST be subclassed and can't be used directly!")
|
|
|
|
@property
|
|
def absolute_root(self):
|
|
raise exceptions.OozeProgrammingError("SiteCompositionPiece MUST be subclassed and can't be used directly!")
|
|
|
|
def route(self, rule):
|
|
|
|
def decorator(func):
|
|
|
|
self.router.add_route(rule, func)
|
|
|
|
return func
|
|
|
|
return decorator
|
|
|
|
def expose(self, rule, mode='full'):
|
|
|
|
def decorator(cls):
|
|
|
|
self.router.add_route(rule, functools.partial(cls.view, mode=mode))
|
|
|
|
return cls
|
|
|
|
return decorator
|
|
|
|
def register_subsite(self, root, subsite):
|
|
|
|
if root.startswith('/') or root.startswith('/'):
|
|
raise exceptions.OozeProgrammingError(f"Subsite path MUST NOT start or end with '/', but {path} does!")
|
|
|
|
self.subsites[root] = subsite
|
|
subsite._register(self, root)
|
|
|
|
def handle_request(self, request):
|
|
|
|
relative_path = request.path[len(self.absolute_root):]
|
|
for subsite_root, subsite in self.subsites.items():
|
|
if relative_path.startswith(subsite_root):
|
|
self.logger.debug(f'Dispatching request to subsite of {self}: /{relative_path}')
|
|
return subsite.handle_request(request)
|
|
|
|
self.logger.debug(f"Trying to resolve request to {request.path}…")
|
|
route, view_kwargs = self.router.find_route(request.path)
|
|
|
|
self.logger.debug(f"Found matching route: {route}!")
|
|
|
|
rv = route.view(request, **view_kwargs)
|
|
|
|
response = self.response_class(request)
|
|
|
|
if isinstance(rv, self.response_class):
|
|
response = rv
|
|
|
|
elif isinstance(rv, tuple): # string + HTTPStatus
|
|
rv, response.status = rv
|
|
|
|
if isinstance(rv, rendering.Renderable):
|
|
response.body = rv.render(self, format='html', mode='full')
|
|
|
|
if isinstance(rv, streaming.FileStream):
|
|
|
|
if request.ranges:
|
|
response.body = rv.stream(request.ranges)
|
|
|
|
else:
|
|
response.body = rv.stream()
|
|
|
|
else:
|
|
response.body = str(rv)
|
|
|
|
return response
|
|
|
|
class Subsite(SiteCompositionPiece):
|
|
|
|
"""
|
|
Composable piece of a site that can be bound to a path and will handle any
|
|
requests going to the "directory" denoted by its path.
|
|
|
|
Essentially the Ooze equivalent of Flasks Blueprints.
|
|
|
|
Can be used to organize sites as well as to implement re-usable
|
|
subsites independent from any particular Ooze application or to
|
|
use different request, response, router and renderer classes in
|
|
parallel within the same project.
|
|
|
|
## Class attributes ##
|
|
|
|
* request_class: See [Application.request_class]
|
|
* response_class: See [Application.response_class]
|
|
* router_class: See [Application.router_class]
|
|
* renderer_class: See [Application.renderer_class]
|
|
|
|
## Object attributes ##
|
|
* router: See [Application.router]
|
|
* renderer: See [Application.renderer]
|
|
* dispatcher: The object dispatching calls to this class.
|
|
For non-nested Subsites, this is the [Application].
|
|
* root: The path relative to the app or parent Subsite acting
|
|
as "root directory" for this Subsite.
|
|
"""
|
|
|
|
dispatcher = None
|
|
root = None
|
|
absolute_root = None
|
|
|
|
def __init__(self):
|
|
|
|
super().__init__()
|
|
|
|
def __repr__(self):
|
|
if self.absolute_root == None:
|
|
return f"<{self.__class__.__name__}, unregistered>"
|
|
return f"<{self.__class__.__name__} at {self.absolute_root}>"
|
|
|
|
def _register(self, dispatcher, root):
|
|
|
|
if dispatcher.absolute_root == None:
|
|
raise exceptions.OozeProgrammingError(f"Tried to register a subsite to an unregistered dispatcher. You likely want to do app.register_subsite(<parent of {root}>. 'some/path').")
|
|
|
|
self.dispatcher = dispatcher
|
|
self.root = root
|
|
self.absolute_root = f'{dispatcher.absolute_root}{root}/'
|
|
|
|
self.logger.debug(f"Registered subsite at '{self.absolute_root}'.")
|
|
|
|
def handle_request(self, request):
|
|
|
|
if self.dispatcher == None:
|
|
raise exceptions.OozeProgrammingError(f"Tried to dispatch request to unregistered Subsite: { request.path }. Call app.register_subsite(<subsite>).")
|
|
|
|
return super().handle_request(request)
|