class Content(object): template = None template_engine = None _json_dict = None _loc = None '''Template engine to render this content. Overwritten my metadata. If not available, the application default engine is used''' mandatory_properties = () def __init__(self, app, content, metadata, src, path=None, context=None, **params): self._app = app self._content = content self._context_for = context self._additional_context = {} self._src = src self._path = path or src self._meta = AttributeDictionary(params) if src: self._meta.modified = modified_datetime(src) else: self._meta.modified = datetime.now() # Get the site meta data dictionary. # Used to render Content metadata self._update_meta(metadata) meta = self._meta if self.is_html: dir, slug = os.path.split(self._path) if not slug: slug = self._path dir = None if not meta.slug: meta.slug = slugify(slug, separator='_') if dir: meta.slug = '%s/%s' % (dir, meta.slug) else: if self.suffix: # Any other file suffix = '.%s' % self.suffix if not self._path.endswith(suffix): self._path = self._path + suffix if not meta.slug: meta.slug = self._path meta.name = slugify(meta.slug, separator='_') for name in self.mandatory_properties: if not meta.get(name): raise BuildError("Property '%s' not available in %s" % (name, self)) @property def name(self): return self._meta.name @property def src(self): return self._src @property def loc(self): return self._loc or self._src @loc.setter def loc(self, value): self._loc = value @property def content_type(self): return self._meta.content_type @property def is_text(self): return self._meta.content_type in CONTENT_EXTENSIONS @property def is_html(self): return is_html(self._meta.content_type) @property def suffix(self): return CONTENT_EXTENSIONS.get(self._meta.content_type) @property def path(self): return self._path @property def reldate(self): return self._meta.date or self._meta.modified @property def year(self): return self.reldate.year @property def month(self): return self.reldate.month @property def month2(self): return self.reldate.strftime('%m') @property def month3(self): return self.reldate.strftime('%b').lower() @property def id(self): if self.is_html: return '%s.json' % self._meta.slug @property def context_for(self): '''A list of contents names for which this snippet is required in the context dictionary ''' return self._context_for @property def additional_context(self): '''Dictionary of key and :class:`.Snippet` providing additional keys for this content ''' return self._additional_context def __repr__(self): return self._src __str__ = __repr__ def key(self, name=None): '''The key for a context dictionary ''' name = name or self.name return 'html_%s' % name if self.is_html else name def context(self, context=None): '''Extract the context dictionary for server side template rendering ''' ctx = dict(self._flatten(self._meta)) if context: ctx.update(context) return ctx def urlparams(self, names=None): urlparams = {} if names: for name in names: value = self._meta.get(name) or getattr(self, name, None) if value in (None, ''): if name == 'id': raise SkipBuild elif names: raise KeyError("%s could not obtain url variable '%s'" % (self, name)) urlparams[name] = value return urlparams def render(self, context=None): '''Render the content ''' if self.is_html: context = self.context(context) content = self._engine(self._content, context) if self.template: template = self._app.template_full_path(self.template) if template: context[self.key('main')] = content with open(template, 'r') as file: template_str = file.read() raw = self._engine(template_str, context) reader = get_reader(self._app, template) ct = reader.process(raw, template) content = ct._content return content else: return self._content def json(self, request): '''Convert the content into a Json dictionary for the API ''' if not self._json_dict and self.is_html: context = self._app.context(request) context = self.context(context) # Add additional context keys if self.additional_context: for key, ct in self.additional_context.items(): if isinstance(ct, Content): key = ct.key(key) ct = ct.render(context) context[key] = ct # assert self.suffix data = self._to_json(self._meta) text = data.get(self.suffix) or {} data[self.suffix] = text text['main'] = self.render(context) # head = {} for key in HEAD_META: value = data.get(key) if value: head[key] = value # require_css = data.get('require_css') if require_css: data['require_css'] = [] cfg = request.config links = Links(cfg['MEDIA_URL'], minified=cfg['MINIFIED_MEDIA']) for css in require_css: css = CssLibraries.get(css, css) links.append(css) for link in links.children: link = link.split("href=") if len(link) == 2: href = link[1] c = href[0] href = href[1:] link = href[:href.find(c)] data['require_css'].append(link) # if 'head' in data: head.update(data['head']) data['head'] = head self._json_dict = data return self._json_dict def html(self, request): '''Build the ``html_main`` key for this content and set content specific values to the ``head`` tag of the HTML5 document. ''' if not self.is_html: raise Unsupported # The JSON data for this page data = self.json(request) doc = request.html_document doc.jscontext['page'] = dict(page_info(data)) # doc.meta.update({'og:image': data.get('image'), 'og:published_time': data.get('date'), 'og:modified_time': data.get('modified')}) doc.meta.update(data['head']) # if not request.config['ANGULAR_UI_ROUTER']: for css in data.get('require_css') or (): doc.head.links.append(css) doc.head.scripts.require.extend(data.get('require_js') or ()) # if request.cache.uirouter is False: doc.head.scripts.require.extend(data.get('require_js') or ()) self.on_html(doc) return data[self.suffix]['main'] def on_html(self, doc): pass @classmethod def as_draft(cls): mp = tuple((a for a in cls.mandatory_properties if a not in no_draft_field)) return cls.__class__('Draft', (cls,), {'mandatory_properties': mp}) # INTERNALS def _update_meta(self, metadata): meta = self._meta meta.site = self._app.extensions['static'].build_info(self._app) context = self.context() for name in ('template_engine', 'template'): default = getattr(self, name) value = metadata.pop(name, default) meta.site[name] = value setattr(self, name, value) self._engine = engine = self._app.template_engine(self.template_engine) meta.update(((key, self._render_meta(value, context)) for key, value in metadata.items())) def _flatten(self, meta): for key, value in mapping_iterator(meta): if isinstance(value, Mapping): for child, value in self._flatten(value): yield '%s_%s' % (key, child), value else: yield key, self._to_string(value) def _to_string(self, value): if isinstance(value, Mapping): raise BuildError('A dictionary found when coverting to string') elif isinstance(value, (list, tuple)): return ', '.join(self._to_string(v) for v in value) elif isinstance(value, date): return iso8601(value) else: return to_string(value) def _render_meta(self, value, context): if isinstance(value, Mapping): return dict(((k, self._render_meta(v, context)) for k, v in value.items())) elif isinstance(value, (list, tuple)): return [self._render_meta(v, context) for v in value] elif isinstance(value, date): return value elif value is not None: return self._engine(to_string(value), context) def _to_json(self, value): if isinstance(value, Mapping): return dict(((k, self._to_json(v)) for k, v in value.items())) elif isinstance(value, (list, tuple)): return [self._to_json(v) for v in value] elif isinstance(value, date): return iso8601(value) else: return value
class Router(RouterType('RouterBase', (object,), {})): '''A :ref:`WSGI middleware <wsgi-middleware>` to handle client requests on multiple :ref:`routes <apps-wsgi-route>`. The user must implement the HTTP methods required by the application. For example if the route needs to serve a ``GET`` request, the ``get(self, request)`` method must be implemented. :param rule: String used for creating the :attr:`route` of this :class:`Router`. :param routes: Optional :class:`Router` instances which are added to the children :attr:`routes` of this router. :param parameters: Optional parameters for this router. They are stored in the :attr:`parameters` attribute. If a ``response_content_types`` value is passed, it overrides the :attr:`response_content_types` attribute. .. attribute:: routes List of children :class:`Router` of this :class:`Router`. .. attribute:: parent The parent :class:`Router` of this :class:`Router`. .. attribute:: response_content_types a list/tuple of possible content types of a response to a client request. The client request must accept at least one of the response content types, otherwise an HTTP ``415`` exception occurs. .. attribute:: allows_redirects boolean indicating if this router can redirect requests to valid urls within this router and its children. For example, if a router serves the '/echo' url but not the ``/echo/`` one, a request on ``/echo/`` will be redirected to ``/echo``. Default: ``False`` .. attribute:: parameters A :class:`.AttributeDictionary` of parameters for this :class:`Router`. Parameters are created at initialisation from the ``parameters`` class attribute and the key-valued parameters passed to the ``__init__`` method for which the value is not callable. ''' _creation_count = 0 _parent = None _name = None response_content_types = RouterParam(None) allows_redirects = RouterParam(False) def __init__(self, rule, *routes, **parameters): Router._creation_count += 1 self._creation_count = Router._creation_count if not isinstance(rule, Route): rule = Route(rule) self._route = rule self._name = parameters.pop('name', rule.rule) self.routes = [] # add routes specified via the initialiser for router in routes: self.add_child(router) # copy parameters self.parameters = AttributeDictionary(self.parameters) for name, rule_method in self.rule_methods.items(): rule, method, params, _, _ = rule_method rparameters = params.copy() handler = getattr(self, name) router = self.add_child(Router(rule, **rparameters)) setattr(router, method, handler) for name, value in parameters.items(): if name in self.parameters: self.parameters[name] = value else: setattr(self, name, value) @property def route(self): '''The relative :class:`.Route` served by this :class:`Router`. ''' parent = self._parent if parent and parent._route.is_leaf: return parent.route + self._route else: return self._route @property def full_route(self): '''The full :attr:`route` for this :class:`.Router`. It includes the :attr:`parent` portion of the route if a parent router is available. ''' if self._parent: return self._parent.full_route + self._route else: return self._route @property def name(self): '''The name of this :class:`Router`. This attribute can be specified during initialisation. If available, it can be used to retrieve a child router by name via the :meth:`get_route` method. ''' return self._name @property def root(self): '''The root :class:`Router` for this :class:`Router`.''' if self.parent: return self.parent.root else: return self @property def parent(self): return self._parent @property def default_content_type(self): '''The default content type for responses. This is the first element in the :attr:`response_content_types` list. ''' ct = self.response_content_types return ct[0] if ct else None @property def creation_count(self): '''Integer for sorting :class:`Router` by creation. Auto-generated during initialisation.''' return self._creation_count @property def rule(self): '''The full ``rule`` string for this :class:`Router`. It includes the :attr:`parent` portion of the rule if a :attr:`parent` router is available. ''' return self.full_route.rule def path(self, **urlargs): '''The full path of this :class:`Router`. It includes the :attr:`parent` portion of url if a parent router is available. ''' return self.full_route.url(**urlargs) def __getattr__(self, name): '''Check the value of a :attr:`parameters` ``name``. If the parameter is not available, retrieve the parameter from the :attr:`parent` :class:`Router` if it exists. ''' if not name.startswith('_'): return self.get_parameter(name, False) self.no_param(name) def get_parameter(self, name, safe=True): value = self.parameters.get(name) if value is None: if self._parent: return self._parent.get_parameter(name, safe) elif name in self.parameters: return value elif not safe: self.no_param(name) else: return value def no_param(self, name): raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, name)) def content_type(self, request): '''Evaluate the content type for the response to a client ``request``. The method uses the :attr:`response_content_types` parameter of accepted content types and the content types accepted by the client and figure out the best match. ''' return request.content_types.best_match(self.response_content_types) def accept_content_type(self, content_type): '''Check if ``content_type`` is accepted by this :class:`Router`. Return the best mach or ``None`` if not accepted.''' response_content_types = self.response_content_types if response_content_types: return ContentAccept( [(content_type, 1)]).best_match(response_content_types) def __repr__(self): return self.route.__repr__() def __call__(self, environ, start_response=None): path = environ.get('PATH_INFO') or '/' path = path[1:] router_args = self.resolve(path) if router_args: router, args = router_args return router.response(environ, args) elif self.allows_redirects: if self.route.is_leaf: if path.endswith('/'): router_args = self.resolve(path[:-1]) if router_args is not None: return self.redirect(environ, '/%s' % path[:-1]) else: if not path.endswith('/'): router_args = self.resolve('%s/' % path) if router_args is not None: return self.redirect(environ, '/%s/' % path) def resolve(self, path, urlargs=None): '''Resolve a path and return a ``(handler, urlargs)`` tuple or ``None`` if the path could not be resolved. ''' match = self.route.match(path) if match is None: if not self.route.is_leaf: # no match return elif '__remaining__' in match: path = match.pop('__remaining__') urlargs = update_args(urlargs, match) else: return self, update_args(urlargs, match) # for handler in self.routes: view_args = handler.resolve(path, urlargs) if view_args is None: continue return view_args def response(self, environ, args): '''Once the :meth:`resolve` method has matched the correct :class:`Router` for serving the request, this matched router invokes this method to produce the WSGI response. ''' request = wsgi_request(environ, self, args) # Set the response content type request.response.content_type = self.content_type(request) method = request.method.lower() callable = getattr(self, method, None) if callable is None: raise HttpException(status=405, msg='Method "%s" not allowed' % method) return callable(request) def redirect(self, environ, path): raise HttpRedirect(path) def add_child(self, router): '''Add a new :class:`Router` to the :attr:`routes` list. ''' assert isinstance(router, Router), 'Not a valid Router' assert router is not self, 'cannot add self to children' # Loop over available routers to check it the router # is already available for r in self.routes: if r.route == router.route: r.parameters.update(router.parameters) return r if router.parent: router.parent.remove_child(router) router._parent = self self.routes.append(router) return router def remove_child(self, router): '''remove a :class:`Router` from the :attr:`routes` list.''' if router in self.routes: self.routes.remove(router) router._parent = None def get_route(self, name): '''Get a child :class:`Router` by its :attr:`name`.''' for route in self.routes: if route.name == name: return route def link(self, *args, **urlargs): '''Return an anchor :class:`Html` element with the `href` attribute set to the url of this :class:`Router`.''' if len(args) > 1: raise ValueError url = self.route.url(**urlargs) if len(args) == 1: text = args[0] else: text = url return Html('a', text, href=url) def sitemap(self, root=None): '''This utility method returns a sitemap starting at root. If *root* is ``None`` it starts from this :class:`Router`. :param request: a :ref:`wsgi request wrapper <app-wsgi-request>` :param root: Optional url path where to start the sitemap. By default it starts from this :class:`Router`. Pass `"/"` to start from the root :class:`Router`. :param levels: Number of nested levels to include. :return: A list of children ''' if not root: root = self else: handler_urlargs = self.root.resolve(root[1:]) if handler_urlargs: root, urlargs = handler_urlargs else: return [] return list(self.routes) def encoding(self, request): '''The encoding to use for the response. By default it returns ``utf-8``.''' return 'utf-8'
class Content(Cacheable): '''A class for managing a file-based content ''' template = None template_engine = None def __init__(self, app, content, metadata, path, src=None, **params): self._app = app self._content = content self._path = path self._src = src self._meta = AttributeDictionary(params) self._update_meta(metadata) if not self._meta.modified: if src: self._meta.modified = modified_datetime(src) else: self._meta.modified = datetime.now() self._meta.name = slugify(self._path, separator='_') @property def app(self): return self._app @property def content_type(self): return self._meta.content_type @property def is_text(self): return self._meta.content_type in CONTENT_EXTENSIONS @property def is_html(self): return is_html(self._meta.content_type) @property def suffix(self): return CONTENT_EXTENSIONS.get(self._meta.content_type) @property def path(self): return self._path @property def reldate(self): return self._meta.date or self._meta.modified @property def year(self): return self.reldate.year @property def month(self): return self.reldate.month @property def month2(self): return self.reldate.strftime('%m') @property def month3(self): return self.reldate.strftime('%b').lower() @property def id(self): if self.is_html: return '%s.json' % self._path def cache_key(self, app): return self._meta.name def __repr__(self): return self._path __str__ = __repr__ def key(self, name=None): '''The key for a context dictionary ''' name = name or self.name suffix = self.suffix return '%s_%s' % (suffix, name) if suffix else name def context(self, context=None): '''Extract the context dictionary for server side template rendering ''' ctx = dict(self._flatten(self._meta)) if context: ctx.update(context) return ctx def urlparams(self, names=None): urlparams = {} if names: for name in names: value = self._meta.get(name) or getattr(self, name, None) if value in (None, ''): if name == 'id': raise SkipBuild elif names: raise KeyError("%s could not obtain url variable '%s'" % (self, name)) urlparams[name] = value return urlparams def render(self, context=None): '''Render the content ''' if self.is_html: context = self.context(context) content = self._engine(self._content, context) if self.template: template = self._app.template_full_path(self.template) if template: context[self.key('main')] = content with open(template, 'r') as file: template_str = file.read() content = self._engine(template_str, context) return content else: return self._content def raw(self, request): return self._content @cached def json(self, request): '''Convert the content into a Json dictionary for the API ''' if self.is_html: context = self._app.context(request) context = self.context(context) # data = self._to_json(request, self._meta) text = data.get(self.suffix) or {} data[self.suffix] = text text['main'] = self.render(context) # head = {} for key in HEAD_META: value = data.get(key) if value: head[key] = value # if 'head' in data: head.update(data['head']) data['url'] = request.absolute_uri(self._path) data['head'] = head return data def html(self, request): '''Build the ``html_main`` key for this content and set content specific values to the ``head`` tag of the HTML5 document. ''' if not self.is_html: raise Unsupported # The JSON data for this page data = self.json(request) doc = request.html_document doc.jscontext['page'] = dict(page_info(data)) # image = absolute_uri(request, data.get('image')) doc.meta.update({'og:image': image, 'og:published_time': data.get('date'), 'og:modified_time': data.get('modified')}) doc.meta.update(data['head']) # if not request.config.get('HTML5_NAVIGATION'): for css in data.get('require_css') or (): doc.head.links.append(css) doc.head.scripts.require.extend(data.get('require_js') or ()) # if request.cache.uirouter is False: doc.head.scripts.require.extend(data.get('require_js') or ()) self.on_html(doc) return data[self.suffix]['main'] def on_html(self, doc): pass @classmethod def as_draft(cls): mp = tuple((a for a in cls.mandatory_properties if a not in no_draft_field)) return cls.__class__('Draft', (cls,), {'mandatory_properties': mp}) # INTERNALS def _update_meta(self, metadata): meta = self._meta meta.site = {} for name in ('template_engine', 'template'): default = getattr(self, name) value = metadata.pop(name, default) meta.site[name] = value setattr(self, name, value) context = self.context(self.app.config) self._engine = self._app.template_engine(self.template_engine) meta.update(((key, self._render_meta(value, context)) for key, value in metadata.items())) def _flatten(self, meta): for key, value in mapping_iterator(meta): if isinstance(value, Mapping): for child, value in self._flatten(value): yield '%s_%s' % (key, child), value else: yield key, self._to_string(value) def _to_string(self, value): if isinstance(value, Mapping): raise BuildError('A dictionary found when coverting to string') elif isinstance(value, (list, tuple)): return ', '.join(self._to_string(v) for v in value) elif isinstance(value, date): return iso8601(value) else: return to_string(value) def _render_meta(self, value, context): if isinstance(value, Mapping): return dict(((k, self._render_meta(v, context)) for k, v in value.items())) elif isinstance(value, (list, tuple)): return [self._render_meta(v, context) for v in value] elif isinstance(value, str): return self._engine(to_string(value), context) else: return value def _to_json(self, request, value): if isinstance(value, Mapping): return dict(((k, self._to_json(request, v)) for k, v in value.items())) elif isinstance(value, (list, tuple)): return [self._to_json(request, v) for v in value] elif isinstance(value, date): return iso8601(value) elif isinstance(value, URLWrapper): return value.to_json(request) else: return value
class Router(RouterType('RouterBase', (object, ), {})): '''A :ref:`WSGI middleware <wsgi-middleware>` to handle client requests on multiple :ref:`routes <apps-wsgi-route>`. The user must implement the HTTP methods required by the application. For example if the route needs to serve a ``GET`` request, the ``get(self, request)`` method must be implemented. :param rule: String used for creating the :attr:`route` of this :class:`Router`. :param routes: Optional :class:`Router` instances which are added to the children :attr:`routes` of this router. :param parameters: Optional parameters for this router. They are stored in the :attr:`parameters` attribute with the exception of :attr:`response_content_types` and :attr:`response_wrapper` .. attribute:: rule_methods A class attribute built during class creation. It is an ordered dictionary mapping method names with a five-elements tuple containing information about a child route (See the :class:`.route` decorator). .. attribute:: routes List of children :class:`Router` of this :class:`Router`. .. attribute:: parent The parent :class:`Router` of this :class:`Router`. .. attribute:: response_content_types A list/tuple of possible content types of a response to a client request. The client request must accept at least one of the response content types, otherwise an HTTP ``415`` exception occurs. .. attribute:: response_wrapper Optional function which wraps all handlers of this :class:`.Router`. The function must accept two parameters, the original handler and the :class:`.WsgiRequest`:: def response_wrapper(handler, request): ... return handler(request) .. attribute:: parameters A :class:`.AttributeDictionary` of parameters for this :class:`Router`. Parameters are created at initialisation from the ``parameters`` class attribute and the key-valued parameters passed to the ``__init__`` method which are available in the class ``parameters`` attribute. ''' _creation_count = 0 _parent = None _name = None response_content_types = RouterParam(None) response_wrapper = RouterParam(None) def __init__(self, rule, *routes, **parameters): Router._creation_count += 1 self._creation_count = Router._creation_count if not isinstance(rule, Route): rule = Route(rule) self._route = rule self._name = parameters.pop('name', rule.name) self.routes = [] # add routes specified via the initialiser first for router in routes: self.add_child(router) # copy parameters self.parameters = AttributeDictionary(self.parameters) for name, rule_method in self.rule_methods.items(): rule, method, params, _, _ = rule_method rparameters = params.copy() handler = getattr(self, name) router = self.add_child(self.make_router(rule, **rparameters)) setattr(router, method, handler) for name, value in parameters.items(): if name in self.parameters: self.parameters[name] = value else: setattr(self, slugify(name, separator='_'), value) @property def route(self): '''The relative :class:`.Route` served by this :class:`Router`. ''' parent = self._parent if parent and parent._route.is_leaf: return parent.route + self._route else: return self._route @property def full_route(self): '''The full :attr:`route` for this :class:`.Router`. It includes the :attr:`parent` portion of the route if a parent router is available. ''' if self._parent: return self._parent.full_route + self._route else: return self._route @property def name(self): '''The name of this :class:`Router`. This attribute can be specified during initialisation. If available, it can be used to retrieve a child router by name via the :meth:`get_route` method. ''' return self._name @property def root(self): '''The root :class:`Router` for this :class:`Router`.''' if self.parent: return self.parent.root else: return self @property def parent(self): return self._parent @property def creation_count(self): '''Integer for sorting :class:`Router` by creation. Auto-generated during initialisation.''' return self._creation_count @property def rule(self): '''The full ``rule`` string for this :class:`Router`. It includes the :attr:`parent` portion of the rule if a :attr:`parent` router is available. ''' return self.full_route.rule def path(self, **urlargs): '''The full path of this :class:`Router`. It includes the :attr:`parent` portion of url if a parent router is available. ''' return self.full_route.url(**urlargs) def getparam(self, name, default=None): '''A parameter in this :class:`.Router` ''' try: return getattr(self, name) except AttributeError: return default def __getattr__(self, name): '''Check the value of a :attr:`parameters` ``name``. If the parameter is not available, retrieve the parameter from the :attr:`parent` :class:`Router` if it exists. ''' if not name.startswith('_'): return self._get_router_parameter(name, False) self._no_param(name) def content_type(self, request): '''Evaluate the content type for the response to a client ``request``. The method uses the :attr:`response_content_types` parameter of accepted content types and the content types accepted by the client ``request`` and figures out the best match. ''' content_types = self.response_content_types ct = request.content_types.best_match(content_types) if ct and '*' in ct: ct = None if not ct and content_types: raise HttpException(status=415, msg=request.content_types) return ct def __repr__(self): return self.route.__repr__() def __call__(self, environ, start_response=None): path = environ.get('PATH_INFO') or '/' path = path[1:] router_args = self.resolve(path) if router_args: router, args = router_args return router.response(environ, args) def resolve(self, path, urlargs=None): '''Resolve a path and return a ``(handler, urlargs)`` tuple or ``None`` if the path could not be resolved. ''' match = self.route.match(path) if match is None: if not self.route.is_leaf: # no match return elif '__remaining__' in match: path = match.pop('__remaining__') urlargs = update_args(urlargs, match) else: return self, update_args(urlargs, match) # for handler in self.routes: view_args = handler.resolve(path, urlargs) if view_args is None: continue return view_args def response(self, environ, args): '''Once the :meth:`resolve` method has matched the correct :class:`Router` for serving the request, this matched router invokes this method to produce the WSGI response. ''' request = wsgi_request(environ, self, args) request.response.content_type = self.content_type(request) method = request.method.lower() callable = getattr(self, method, None) if callable is None: raise HttpException(status=405) response_wrapper = self.response_wrapper if response_wrapper: return response_wrapper(callable, request) return callable(request) def add_child(self, router): '''Add a new :class:`Router` to the :attr:`routes` list. ''' assert isinstance(router, Router), 'Not a valid Router' assert router is not self, 'cannot add self to children' # # Remove from previous parent if router.parent: router.parent.remove_child(router) router._parent = self # Loop over available routers to check it the router # is already available for r in self.routes: if r.route == router.route: r.parameters.update(router.parameters) return r self.routes.append(router) return router def remove_child(self, router): '''remove a :class:`Router` from the :attr:`routes` list.''' if router in self.routes: self.routes.remove(router) router._parent = None def get_route(self, name): '''Get a child :class:`Router` by its :attr:`name`. This method search child routes recursively. ''' for route in self.routes: if route.name == name: return route for child in self.routes: route = child.get_route(name) if route: return route def link(self, *args, **urlargs): '''Return an anchor :class:`Html` element with the `href` attribute set to the url of this :class:`Router`.''' if len(args) > 1: raise ValueError url = self.route.url(**urlargs) if len(args) == 1: text = args[0] else: text = url return Html('a', text, href=url) def has_parent(self, router): '''Check if ``router`` is ``self`` or a parent or ``self`` ''' parent = self while parent and parent is not router: parent = parent._parent return parent is not None def make_router(self, rule, cls=None, **params): '''Create a new :class:`.Router` from a ``rule`` and parameters. This method is used during initialisation when building child Routers from the :attr:`rule_methods`. ''' cls = cls or Router return cls(rule, **params) def _no_param(self, name): raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, name)) def _get_router_parameter(self, name, safe=True): value = self.parameters.get(name) if value is None: if self._parent: return self._parent._get_router_parameter(name, safe) elif name in self.parameters: return value elif not safe: self._no_param(name) else: return value
class Router(RouterType('RouterBase', (object, ), {})): '''A :ref:`WSGI middleware <wsgi-middleware>` to handle client requests on multiple :ref:`routes <apps-wsgi-route>`. The user must implement the HTTP methods required by the application. For example if the route needs to serve a ``GET`` request, the ``get(self, request)`` method must be implemented. :param rule: String used for creating the :attr:`route` of this :class:`Router`. :param routes: Optional :class:`Router` instances which are added to the children :attr:`routes` of this router. :param parameters: Optional parameters for this router. They are stored in the :attr:`parameters` attribute. If a ``response_content_types`` value is passed, it overrides the :attr:`response_content_types` attribute. .. attribute:: route The :ref:`Route <apps-wsgi-route>` served by this :class:`Router`. .. attribute:: routes List of children :class:`Router` of this :class:`Router`. .. attribute:: parent The parent :class:`Router` of this :class:`Router`. .. attribute:: response_content_types a list/tuple of possible content types of a response to a client request. The client request must accept at least one of the response content types, otherwise an HTTP ``415`` exception occurs. .. attribute:: parameters A :class:`.AttributeDictionary` of parameters for this :class:`Router`. Parameters are created at initialisation from the ``parameters`` class attribute and the key-valued parameters passed to the ``__init__`` method for which the value is not callable. ''' _creation_count = 0 _parent = None _name = None response_content_types = RouterParam(None) def __init__(self, rule, *routes, **parameters): Router._creation_count += 1 self._creation_count = Router._creation_count if not isinstance(rule, Route): rule = Route(rule) self.route = rule self._name = parameters.pop('name', rule.rule) self.routes = [] for router in routes: self.add_child(router) # copy parameters self.parameters = AttributeDictionary(self.parameters) for name, rule_method in self.rule_methods.items(): rule, method, params, _, _ = rule_method rparameters = params.copy() handler = getattr(self, name) if rparameters.pop('async', False): # asynchronous method handler = async ()(handler) handler.rule_method = rule_method router = self.add_child(Router(rule, **rparameters)) setattr(router, method, handler) for name, value in parameters.items(): if name in self.parameters: self.parameters[name] = value else: setattr(self, name, value) @property def name(self): '''The name of this :class:`Router`. This attribute can be specified during initialisation. If available, it can be used to retrieve a child router by name via the :meth:`get_route` method. ''' return self._name @property def root(self): '''The root :class:`Router` for this :class:`Router`.''' if self.parent: return self.parent.root else: return self @property def parent(self): return self._parent @property def default_content_type(self): '''The default content type for responses. This is the first element in the :attr:`response_content_types` list.''' ct = self.response_content_types return ct[0] if ct else None @property def creation_count(self): '''Integer for sorting :class:`Router` by creation. Auto-generated during initialisation.''' return self._creation_count @property def full_route(self): '''The full :attr:`route` for this :class:`Router`. It includes the :attr:`parent` portion of the route if a parent router is available.''' route = self.route if self._parent: route = self._parent.route + route return route @property def rule(self): '''The full ``rule`` string for this :class:`Router`. It includes the :attr:`parent` portion of rule if a parent router is available.''' return self.full_route.rule def path(self, **urlargs): '''The full path of this :class:`Router`. It includes the :attr:`parent` portion of url if a parent router is available.''' route = self.route if self._parent: route = self._parent.route + route return route.url(**urlargs) def __getattr__(self, name): '''Check the value of a :attr:`parameters` ``name``. If the parameter is not available, retrieve the parameter from the :attr:`parent` :class:`Router` if it exists. ''' if not name.startswith('_'): return self.get_parameter(name, False) self.no_param(name) def get_parameter(self, name, safe=True): value = self.parameters.get(name) if value is None: if self._parent: return self._parent.get_parameter(name, safe) elif name in self.parameters: return value elif not safe: self.no_param(name) else: return value def no_param(self, name): raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, name)) def content_type(self, request): '''Evaluate the content type for the response to a client ``request``. The method uses the :attr:`response_content_types` parameter of accepted content types and the content types accepted by the client and figure out the best match. ''' response_content_types = self.response_content_types if response_content_types: return request.content_types.best_match(response_content_types) def accept_content_type(self, content_type): '''Check if ``content_type`` is accepted by this :class:`Router`. Return the best mach or ``None`` if not accepted.''' response_content_types = self.response_content_types if response_content_types: return ContentAccept([(content_type, 1) ]).best_match(response_content_types) def __repr__(self): return self.route.__repr__() def __call__(self, environ, start_response=None): path = environ.get('PATH_INFO') or '/' path = path[1:] router_args = self.resolve(path) if router_args: router, args = router_args return router.response(environ, args) else: if self.route.is_leaf: if path.endswith('/'): router_args = self.resolve(path[:-1]) if router_args is not None: return self.redirect(environ, '/%s' % path[:-1]) else: if not path.endswith('/'): router_args = self.resolve('%s/' % path) if router_args is not None: return self.redirect(environ, '/%s/' % path) def resolve(self, path, urlargs=None): '''Resolve a path and return a ``(handler, urlargs)`` tuple or ``None`` if the path could not be resolved.''' urlargs = urlargs if urlargs is not None else {} match = self.route.match(path) if match is None: return if '__remaining__' in match: remaining_path = match['__remaining__'] for handler in self.routes: view_args = handler.resolve(remaining_path, urlargs) if view_args is None: continue #remaining_path = match.pop('__remaining__','') #urlargs.update(match) return view_args else: return self, match @async (get_result=True) def response(self, environ, args): '''Once the :meth:`resolve` method has matched the correct :class:`Router` for serving the request, this matched router invokes this method to produce the WSGI response.''' request = wsgi_request(environ, self, args) # Set the response content type request.response.content_type = self.content_type(request) method = request.method.lower() callable = getattr(self, method, None) if callable is None: raise HttpException(status=405, msg='Method "%s" not allowed' % method) # make sure cache does not contain asynchronous data async_cache = multi_async(request.cache, raise_on_error=False) cache = yield async_cache if async_cache.num_failures: for key, value in list(cache.items()): if isinstance(value, Failure): cache.pop(key) environ['pulsar.cache'] = cache yield async_cache.failures else: environ['pulsar.cache'] = cache yield callable(request) @async (get_result=True) def redirect(self, environ, path): request = wsgi_request(environ, self) environ['pulsar.cache'] = yield multi_async(request.cache) raise HttpRedirect(path) def add_child(self, router): '''Add a new :class:`Router` to the :attr:`routes` list. If this :class:`Router` is a leaf route, add a slash to the url.''' assert isinstance(router, Router), 'Not a valid Router' assert router is not self, 'cannot add self to children' if self.route.is_leaf: self.route = Route('%s/' % self.route.rule) for r in self.routes: if r.route == router.route: r.parameters.update(router.parameters) return r if router.parent: router.parent.remove_child(router) router._parent = self self.routes.append(router) return router def remove_child(self, router): '''remove a :class:`Router` from the :attr:`routes` list.''' if router in self.routes: self.routes.remove(router) router._parent = None def get_route(self, name): '''Get a child :class:`Router` by its :attr:`name`.''' for route in self.routes: if route.name == name: return route def link(self, *args, **urlargs): '''Return an anchor :class:`Html` element with the `href` attribute set to the url of this :class:`Router`.''' if len(args) > 1: raise ValueError url = self.route.url(**urlargs) if len(args) == 1: text = args[0] else: text = url return Html('a', text, href=url) def sitemap(self, root=None): '''This utility method returns a sitemap starting at root. If *root* is ``None`` it starts from this :class:`Router`. :param request: a :ref:`wsgi request wrapper <app-wsgi-request>` :param root: Optional url path where to start the sitemap. By default it starts from this :class:`Router`. Pass `"/"` to start from the root :class:`Router`. :param levels: Number of nested levels to include. :return: A list of children ''' if not root: root = self else: handler_urlargs = self.root.resolve(root[1:]) if handler_urlargs: root, urlargs = handler_urlargs else: return [] return list(self.routes) def encoding(self, request): '''The encoding to use for the response. By default it returns ``utf-8``.''' return 'utf-8'
class Router(RouterType('RouterBase', (object,), {})): '''A :ref:`WSGI middleware <wsgi-middleware>` to handle client requests on multiple :ref:`routes <apps-wsgi-route>`. The user must implement the HTTP methods required by the application. For example if the route needs to serve a ``GET`` request, the ``get(self, request)`` method must be implemented. :param rule: String used for creating the :attr:`route` of this :class:`Router`. :param routes: Optional :class:`Router` instances which are added to the children :attr:`routes` of this router. :param parameters: Optional parameters for this router. They are stored in the :attr:`parameters` attribute with the exception of :attr:`response_content_types` and :attr:`response_wrapper` .. attribute:: rule_methods A class attribute built during class creation. It is an ordered dictionary mapping method names with a five-elements tuple containing information about a child route (See the :class:`.route` decorator). .. attribute:: routes List of children :class:`Router` of this :class:`Router`. .. attribute:: parent The parent :class:`Router` of this :class:`Router`. .. attribute:: response_content_types A list/tuple of possible content types of a response to a client request. The client request must accept at least one of the response content types, otherwise an HTTP ``415`` exception occurs. .. attribute:: response_wrapper Optional function which wraps all handlers of this :class:`.Router`. The function must accept two parameters, the original handler and the :class:`.WsgiRequest`:: def response_wrapper(handler, request): ... return handler(request) .. attribute:: parameters A :class:`.AttributeDictionary` of parameters for this :class:`Router`. Parameters are created at initialisation from the ``parameters`` class attribute and the key-valued parameters passed to the ``__init__`` method which are available in the class ``parameters`` attribute. ''' _creation_count = 0 _parent = None _name = None response_content_types = RouterParam(None) response_wrapper = RouterParam(None) def __init__(self, rule, *routes, **parameters): Router._creation_count += 1 self._creation_count = Router._creation_count if not isinstance(rule, Route): rule = Route(rule) self._route = rule self._name = parameters.pop('name', rule.name) self.routes = [] # add routes specified via the initialiser first for router in routes: self.add_child(router) # copy parameters self.parameters = AttributeDictionary(self.parameters) for name, rule_method in self.rule_methods.items(): rule, method, params, _, _ = rule_method rparameters = params.copy() handler = getattr(self, name) router = self.add_child(self.make_router(rule, **rparameters)) setattr(router, method, handler) for name, value in parameters.items(): if name in self.parameters: self.parameters[name] = value else: setattr(self, slugify(name, separator='_'), value) @property def route(self): '''The relative :class:`.Route` served by this :class:`Router`. ''' parent = self._parent if parent and parent._route.is_leaf: return parent.route + self._route else: return self._route @property def full_route(self): '''The full :attr:`route` for this :class:`.Router`. It includes the :attr:`parent` portion of the route if a parent router is available. ''' if self._parent: return self._parent.full_route + self._route else: return self._route @property def name(self): '''The name of this :class:`Router`. This attribute can be specified during initialisation. If available, it can be used to retrieve a child router by name via the :meth:`get_route` method. ''' return self._name @property def root(self): '''The root :class:`Router` for this :class:`Router`.''' if self.parent: return self.parent.root else: return self @property def parent(self): return self._parent @property def creation_count(self): '''Integer for sorting :class:`Router` by creation. Auto-generated during initialisation.''' return self._creation_count @property def rule(self): '''The full ``rule`` string for this :class:`Router`. It includes the :attr:`parent` portion of the rule if a :attr:`parent` router is available. ''' return self.full_route.rule def path(self, **urlargs): '''The full path of this :class:`Router`. It includes the :attr:`parent` portion of url if a parent router is available. ''' return self.full_route.url(**urlargs) def getparam(self, name, default=None): '''A parameter in this :class:`.Router` ''' try: return getattr(self, name) except AttributeError: return default def __getattr__(self, name): '''Check the value of a :attr:`parameters` ``name``. If the parameter is not available, retrieve the parameter from the :attr:`parent` :class:`Router` if it exists. ''' if not name.startswith('_'): return self._get_router_parameter(name, False) self._no_param(name) def content_type(self, request): '''Evaluate the content type for the response to a client ``request``. The method uses the :attr:`response_content_types` parameter of accepted content types and the content types accepted by the client ``request`` and figures out the best match. ''' response_content_types = self.response_content_types request_content_types = request.content_types if request_content_types: ct = request_content_types.best_match(response_content_types) if ct and '*' in ct: ct = None if not ct and response_content_types: raise HttpException(status=415, msg=request_content_types) return ct def __repr__(self): return self.route.__repr__() def __call__(self, environ, start_response=None): path = environ.get('PATH_INFO') or '/' path = path[1:] router_args = self.resolve(path) if router_args: router, args = router_args return router.response(environ, args) def resolve(self, path, urlargs=None): '''Resolve a path and return a ``(handler, urlargs)`` tuple or ``None`` if the path could not be resolved. ''' match = self.route.match(path) if match is None: if not self.route.is_leaf: # no match return elif '__remaining__' in match: path = match.pop('__remaining__') urlargs = update_args(urlargs, match) else: return self, update_args(urlargs, match) # for handler in self.routes: view_args = handler.resolve(path, urlargs) if view_args is None: continue return view_args def response(self, environ, args): '''Once the :meth:`resolve` method has matched the correct :class:`Router` for serving the request, this matched router invokes this method to produce the WSGI response. ''' request = wsgi_request(environ, self, args) request.response.content_type = self.content_type(request) method = request.method.lower() callable = getattr(self, method, None) if callable is None: raise HttpException(status=405) response_wrapper = self.response_wrapper if response_wrapper: return response_wrapper(callable, request) return callable(request) def add_child(self, router): '''Add a new :class:`Router` to the :attr:`routes` list. ''' assert isinstance(router, Router), 'Not a valid Router' assert router is not self, 'cannot add self to children' # # Remove from previous parent if router.parent: router.parent.remove_child(router) router._parent = self # Loop over available routers to check it the router # is already available for r in self.routes: if r.route == router.route: r.parameters.update(router.parameters) return r self.routes.append(router) return router def remove_child(self, router): '''remove a :class:`Router` from the :attr:`routes` list.''' if router in self.routes: self.routes.remove(router) router._parent = None def get_route(self, name): '''Get a child :class:`Router` by its :attr:`name`. This method search child routes recursively. ''' for route in self.routes: if route.name == name: return route for child in self.routes: route = child.get_route(name) if route: return route def link(self, *args, **urlargs): '''Return an anchor :class:`Html` element with the `href` attribute set to the url of this :class:`Router`.''' if len(args) > 1: raise ValueError url = self.route.url(**urlargs) if len(args) == 1: text = args[0] else: text = url return Html('a', text, href=url) def has_parent(self, router): '''Check if ``router`` is ``self`` or a parent or ``self`` ''' parent = self while parent and parent is not router: parent = parent._parent return parent is not None def make_router(self, rule, cls=None, **params): '''Create a new :class:`.Router` from a ``rule`` and parameters. This method is used during initialisation when building child Routers from the :attr:`rule_methods`. ''' cls = cls or Router return cls(rule, **params) def _no_param(self, name): raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, name)) def _get_router_parameter(self, name, safe=True): value = self.parameters.get(name) if value is None: if self._parent: return self._parent._get_router_parameter(name, safe) elif name in self.parameters: return value elif not safe: self._no_param(name) else: return value
class Content(Cacheable): '''A class for managing a file-based content ''' template = None template_engine = None def __init__(self, app, content, metadata, path, src=None, **params): self._app = app self._content = content self._path = path self._src = src self._meta = AttributeDictionary(params) self._update_meta(metadata) if not self._meta.modified: if src: self._meta.modified = modified_datetime(src) else: self._meta.modified = datetime.now() self._meta.name = slugify(self._path, separator='_') @property def app(self): return self._app @property def content_type(self): return self._meta.content_type @property def is_text(self): return self._meta.content_type in CONTENT_EXTENSIONS @property def is_html(self): return is_html(self._meta.content_type) @property def suffix(self): return CONTENT_EXTENSIONS.get(self._meta.content_type) @property def path(self): return self._path @property def reldate(self): return self._meta.date or self._meta.modified @property def year(self): return self.reldate.year @property def month(self): return self.reldate.month @property def month2(self): return self.reldate.strftime('%m') @property def month3(self): return self.reldate.strftime('%b').lower() @property def id(self): if self.is_html: return '%s.json' % self._path def cache_key(self, app): return self._meta.name def __repr__(self): return self._path __str__ = __repr__ def key(self, name=None): '''The key for a context dictionary ''' name = name or self.name suffix = self.suffix return '%s_%s' % (suffix, name) if suffix else name def context(self, context=None): '''Extract the context dictionary for server side template rendering ''' ctx = dict(self._flatten(self._meta)) if context: ctx.update(context) return ctx def urlparams(self, names=None): urlparams = {} if names: for name in names: value = self._meta.get(name) or getattr(self, name, None) if value in (None, ''): if name == 'id': raise SkipBuild elif names: raise KeyError( "%s could not obtain url variable '%s'" % (self, name)) urlparams[name] = value return urlparams def render(self, context=None): '''Render the content ''' if self.is_html: context = self.context(context) content = self._engine(self._content, context) if self.template: template = self._app.template_full_path(self.template) if template: context[self.key('main')] = content with open(template, 'r') as file: template_str = file.read() content = self._engine(template_str, context) return content else: return self._content def raw(self, request): return self._content @cached def json(self, request): '''Convert the content into a Json dictionary for the API ''' if self.is_html: context = self._app.context(request) context = self.context(context) # data = self._to_json(request, self._meta) text = data.get(self.suffix) or {} data[self.suffix] = text text['main'] = self.render(context) # head = {} for key in HEAD_META: value = data.get(key) if value: head[key] = value # if 'head' in data: head.update(data['head']) data['url'] = request.absolute_uri(self._path) data['head'] = head return data def html(self, request): '''Build the ``html_main`` key for this content and set content specific values to the ``head`` tag of the HTML5 document. ''' if not self.is_html: raise Unsupported # The JSON data for this page data = self.json(request) doc = request.html_document doc.jscontext['page'] = dict(page_info(data)) # image = absolute_uri(request, data.get('image')) doc.meta.update({ 'og:image': image, 'og:published_time': data.get('date'), 'og:modified_time': data.get('modified') }) doc.meta.update(data['head']) # if not request.config.get('HTML5_NAVIGATION'): for css in data.get('require_css') or (): doc.head.links.append(css) doc.head.scripts.require.extend(data.get('require_js') or ()) # if request.cache.uirouter is False: doc.head.scripts.require.extend(data.get('require_js') or ()) self.on_html(doc) return data[self.suffix]['main'] def on_html(self, doc): pass @classmethod def as_draft(cls): mp = tuple( (a for a in cls.mandatory_properties if a not in no_draft_field)) return cls.__class__('Draft', (cls, ), {'mandatory_properties': mp}) # INTERNALS def _update_meta(self, metadata): meta = self._meta meta.site = {} for name in ('template_engine', 'template'): default = getattr(self, name) value = metadata.pop(name, default) meta.site[name] = value setattr(self, name, value) context = self.context(self.app.config) self._engine = self._app.template_engine(self.template_engine) meta.update(((key, self._render_meta(value, context)) for key, value in metadata.items())) def _flatten(self, meta): for key, value in mapping_iterator(meta): if isinstance(value, Mapping): for child, value in self._flatten(value): yield '%s_%s' % (key, child), value else: yield key, self._to_string(value) def _to_string(self, value): if isinstance(value, Mapping): raise BuildError('A dictionary found when coverting to string') elif isinstance(value, (list, tuple)): return ', '.join(self._to_string(v) for v in value) elif isinstance(value, date): return iso8601(value) else: return to_string(value) def _render_meta(self, value, context): if isinstance(value, Mapping): return dict( ((k, self._render_meta(v, context)) for k, v in value.items())) elif isinstance(value, (list, tuple)): return [self._render_meta(v, context) for v in value] elif isinstance(value, str): return self._engine(to_string(value), context) else: return value def _to_json(self, request, value): if isinstance(value, Mapping): return dict( ((k, self._to_json(request, v)) for k, v in value.items())) elif isinstance(value, (list, tuple)): return [self._to_json(request, v) for v in value] elif isinstance(value, date): return iso8601(value) elif isinstance(value, URLWrapper): return value.to_json(request) else: return value