def template_for_uri(self, uri, exc_if_not_found=True): """ :return: template for the uri provided :rtype: Template """ try: template = self.instances[uri] if config.get("smisk.mvc.template.autoreload", config.get("smisk.autoreload")): if template is not None: template = self._check(uri, template) else: raise KeyError("check again") if exc_if_not_found and template is None: raise exceptions.TopLevelLookupException("Failed to locate template for uri '%s'" % uri) return template except KeyError: u = re.sub(r"^\/+", "", uri) for dn in self.directories: srcfile = posixpath.normpath(posixpath.join(dn, u)) if os.access(srcfile, os.F_OK): return self._load(srcfile, uri) else: self.instances[uri] = None if exc_if_not_found: raise exceptions.TopLevelLookupException("Failed to locate template for uri '%s'" % uri) return None
def _load(self, filename, uri, text=None): try: if filename is not None: filename = posixpath.normpath(filename) encoding_errors = "replace" if len(uri) > 4 and (uri[-5:].lower() == ".html" or uri[-4:].lower() == ".xml"): encoding_errors = "htmlentityreplace" cache_type = config.get("smisk.mvc.template.cache_type", "memory") self.instances[uri] = Template( uri=uri, filename=filename, text=text, lookup=self, module_filename=None, format_exceptions=config.get("smisk.mvc.template.format_exceptions", True), input_encoding=config.get("smisk.mvc.template.input_encoding", "utf-8"), output_encoding=smisk.mvc.Response.charset, encoding_errors=encoding_errors, cache_type=cache_type, default_filters=config.get("smisk.mvc.template.default_filters", ["unicode"]), imports=self.imports, ) if log.level <= logging.DEBUG and cache_type != "file": code = self.instances[uri].code log.debug("Compiled %s into %d bytes of python code:\n%s", uri, len(code), code) return self.instances[uri] except: self.instances.pop(uri, None) raise
def _update_config_files_list(self): config_files = set() if config.get('smisk.autoreload.config', config.get('smisk.autoreload')): for path,conf in config.sources: if path[0] != '<': config_files.add(path) self.config_files = config_files
def reset_cache(self): limit = config.get("smisk.mvc.template.cache_limit", -1) if limit == -1: self.instances = {} self._uri_cache = {} else: self.instances = LRUCache(limit) self._uri_cache = LRUCache(limit)
def run(self): '''Reload the process if registered files have been modified.''' sysfiles = set() if config.get('smisk.autoreload.modules', config.get('smisk.autoreload')): for k, m in sys.modules.items(): if self.match is None or self.match.match(k): if hasattr(m, '__loader__'): if hasattr(m.__loader__, 'archive'): k = m.__loader__.archive k = getattr(m, '__file__', None) sysfiles.add(k) for path in sysfiles | self.config_files: if path: if path.endswith('.pyc') or path.endswith('.pyo'): path = path[:-1] oldtime = self.mtimes.get(path, 0) if oldtime is None: # Module with no .py file. Skip it. continue #self.log.info('Checking %r' % sysfiles) try: mtime = os.stat(path).st_mtime except OSError: # Either a module with no .py file, or it's been deleted. mtime = None if path not in self.mtimes: # If a module has no .py file, this will be None. self.mtimes[path] = mtime else: #self.log.info("checking %s", path) if mtime is None or mtime > oldtime: if path.endswith(config.filename_ext) and path in [k for k,d in config.sources]: self.on_config_modified(path) self.mtimes[path] = mtime else: self.on_module_modified(path) return
def run(self, bind=None, application=None, forks=None, handle_errors=False): '''Helper for running an application. ''' # Write PID if self.pidfile: flags = os.O_WRONLY | os.O_APPEND if hasattr(os, 'O_EXLOCK'): flags = flags | os.O_EXLOCK fd = os.open(self.pidfile, flags) try: os.write(fd, '%d\n' % os.getpid()) finally: os.close(fd) # Make sure we have an application application = absapp(application) # Bind if bind is not None: os.environ['SMISK_BIND'] = bind if 'SMISK_BIND' in os.environ: smisk.core.bind(os.environ['SMISK_BIND']) log.info('Listening on %s', smisk.core.listening()) # Enable auto-reloading if any of these are True: if _config.get('smisk.autoreload.modules') \ or _config.get('smisk.autoreload.config', _config.get('smisk.autoreload')): from smisk.autoreload import Autoreloader ar = Autoreloader() ar.start() # Forks if isinstance(forks, int): application.forks = forks # Call app.run() if handle_errors: return handle_errors_wrapper(application.run) else: return application.run()
def send_response(self, rsp): '''Send the response to the current client, finalizing the current HTTP transaction. ''' # Empty rsp if rsp is None: # The leaf might have sent content using low-level functions, # so we need to confirm the response has not yet started and # a custom content length header has not been set. if not self.response.has_begun: self.response.adjust_status(False) return # Add headers if the response has not yet begun if not self.response.has_begun: # Add Content-Length header if self.response.find_header('Content-Length:') == -1: self.response.headers.append('Content-Length: %d' % len(rsp)) # Add Content-Type header self.response.serializer.add_content_type_header(self.response, self.response.charset) # Has content or not? if len(rsp) > 0: # Make sure appropriate status is set, if needed self.response.adjust_status(True) # Add ETag if enabled etag = config.get('smisk.mvc.etag') if etag is not None and self.response.find_header('ETag:') == -1: h = etag(''.join(self.response.headers)) h.update(rsp) self.response.headers.append('ETag: "%s"' % h.hexdigest()) else: # Make sure appropriate status is set, if needed self.response.adjust_status(False) # Debug print if log.level <= logging.DEBUG: self._log_debug_sending_rsp(rsp) # Send headers self.response.begin() # Head does not contain a payload, but all the headers should be exactly # like they would with a GET. (Including Content-Length) if self.request.method != 'HEAD': # Send body if __debug__: assert isinstance(rsp, str), 'type(rsp) == %s' % type(rsp) self.response.write(rsp)
def render_error(self, status, params={}, format="html"): # Compile body from template errors = config.get("smisk.mvc.template.errors", {}) if status.code in errors: template = self.template_for_uri("%s.%s" % (errors[status.code], format), False) elif status in errors: template = self.template_for_uri("%s.%s" % (errors[status], format), False) elif 0 in errors: template = self.template_for_uri("%s.%s" % (errors[0], format), False) else: template = None # We can't render this error because we did not find a suiting template. if template is None: return None # Render template return template.render(**params)
def configure(self, config_key='smisk.mvc.routes'): filters = config.get(config_key, []) if not isinstance(filters, (list, tuple)): raise TypeError('configuration parameter %r must be a list' % config_key) for filter in filters: try: # Convert a list or tuple mapping if isinstance(filter, (tuple, list)): if len(filter) > 2: filter = {'methods':filter[0], 'pattern': filter[1], 'destination': filter[2]} else: filter = {'pattern': filter[0], 'destination': filter[1]} # Create a filter from the mapping dest = URL(filter['destination']) self.filter(filter['pattern'], dest, match_on_full_url=dest.scheme, methods=filter.get('methods', None)) except TypeError, e: e.args = ('configuration parameter %r must contain dictionaries or lists' % config_key,) raise except IndexError, e: e.args = ('%r in configuration parameter %r' % (e.message, config_key),) raise
def setup(self): '''Setup application state ''' # Setup ETag etag = config.get('smisk.mvc.etag') if etag is not None and isinstance(etag, basestring): import hashlib config.set_default('smisk.mvc.etag', getattr(hashlib, etag)) # Check templates config if self.templates: if not self.templates.directories: path = os.path.join(os.environ['SMISK_APP_DIR'], 'templates') if os.path.isdir(path): self.templates.directories = [path] log.debug('using template directories: %s', ', '.join(self.templates.directories)) else: log.info('template directory not found -- disabling templates.') self.templates.directories = [] self.templates = None # Set fallback serializer if isinstance(Response.fallback_serializer, basestring): Response.fallback_serializer = serializers.find(Response.fallback_serializer) if Response.fallback_serializer not in serializers: # Might have been unregistered and need to be reconfigured Response.fallback_serializer = None if Response.fallback_serializer is None: try: Response.fallback_serializer = serializers.extensions['html'] except KeyError: try: Response.fallback_serializer = serializers[0] except IndexError: Response.fallback_serializer = None # Create tables if needed and setup any models if model.metadata.bind: model.setup_all(True)
def apply_leaf_restrictions(self): '''Applies any restrictions set by the current leaf/destination. :rtype: None ''' # Method restrictions try: log.debug('applying method restrictions for leaf %r', self.destination.leaf) leaf_methods = self.destination.leaf.methods method = self.request.method log.debug('leaf allows %r, request is %r', leaf_methods, method) if leaf_methods is not None: method_not_allowed = method not in leaf_methods is_opts_and_refl = (method == 'OPTIONS' and control.enable_reflection) if method_not_allowed and method == 'HEAD' and 'GET' in leaf_methods: # HEAD is always allowed as long as GET is allowed. # We perform the check here in order to give the user the possibility # to explicitly @expose a leaf with OPTIONS included in the methods # argument. (Same reason with OPTIONS further down here) method_not_allowed = False elif method_not_allowed or method == 'OPTIONS': # HTTP 1.1 requires us to specify allowed methods in a 405 response # and we should also include Allow for OPTIONS requests. if is_opts_and_refl and method_not_allowed: # OPTIONS was not in leaf_methods, so add it through copy (not appending) leaf_methods = leaf_methods + ['OPTIONS'] if 'HEAD' not in leaf_methods and 'GET' in leaf_methods: # HEAD was not in leaf_methods, but GET is, so add it through copy (not appending) leaf_methods = leaf_methods + ['HEAD'] self.response.headers.append('Allow: ' + ', '.join(leaf_methods)) if method_not_allowed: # If OPTIONS request and control.enable_reflection is True, respond # with leaf relfection. Placing the check here, inside method_not_allowed, # allows the application designer to explicitly @expose a leaf with # OPTIONS included in the methods argument, in order for her to handle # a OPTIONS request, rather than Smisk taking over. if is_opts_and_refl: class LeafReflectionDestination(Destination): def _call_leaf(self, *args, **params): return control.leaf_reflection(self.leaf) self.destination = LeafReflectionDestination(self.destination.leaf) else: # Method not allowed raise http.MethodNotAllowed("The requested method %s is not allowed for the URI %s." %\ (method, self.request.url.uri)) except AttributeError: # self.destination.leaf does not have any method restrictions pass # Format restrictions try: leaf_formats = self.destination.leaf.formats for ext in self.response.serializer.extensions: if ext not in leaf_formats: self.response.serializer = None break if self.response.serializer is None: log.warn('client requested a response type which is not available for the current leaf') if self.response.format is not None: raise http.NotFound('Resource not available as %r' % self.response.format) elif config.get('smisk.mvc.strict_tcn', True) or len(leaf_formats) == 0: raise http.NotAcceptable() else: try: self.response.serializer = serializers.extensions[leaf_formats[0]] except KeyError: raise http.NotAcceptable() except AttributeError: # self.destination.leaf.formats does not exist -- no restrictions apply pass
def parse_request(self): ''' Parses the request, involving appropriate serializer if needed. :returns: (list arguments, dict parameters) :rtype: tuple ''' args = [] params = {} log.debug('parsing request') # Look at Accept-Charset header and set self.response.charset accordingly accept_charset = self.request.env.get('HTTP_ACCEPT_CHARSET', False) if accept_charset: self.response.charsets, highqs, partials, accept_any = parse_qvalue_header(accept_charset.lower()) if accept_any: self.response.charsets = [] else: alt_cs = None for cq in self.response.charsets: c = cq[0] try: char_codecs.lookup(c) alt_cs = c break except LookupError: pass if alt_cs is not None: self.response.charset = alt_cs else: # If an Accept-Charset header is present, and if the server cannot send a response # which is acceptable according to the Accept-Charset header, then the server # SHOULD send an error response with the 406 (not acceptable) status code, though # the sending of an unacceptable response is also allowed. [RFC 2616] log.info('client demanded charset(s) we can not respond using. "Accept-Charset: %s"', accept_charset) if config.get('smisk.mvc.strict_tcn', True): raise http.NotAcceptable() if log.level <= logging.DEBUG: log.debug('using alternate response character encoding: %r (requested by client)', self.response.charset) # Handle params try: if self.charset: # trigger build-up and thus decoding of text data self.request.post self.request.cookies params.update(self.request.get) else: for k,v in self.request.get.items(): if isinstance(v, str): v = v.decode('latin_1', self.unicode_errors) params[k] = v except UnicodeDecodeError: # We do not speak about latin-1 in this message since in the case of URL-escaped # bytes we can never fail to decode bytes as latin-1. raise http.BadRequest('Unable to decode text data. '\ 'Please encode text using the %s character set.' % self.charset) # Parse body if POST request if self.request.method in ('POST', 'PUT'): path_ext_serializer = self._serializer_for_request_path_ext() content_type = self.request.env.get('CONTENT_TYPE', '').lower() content_charset = None p = content_type.find(';') if p != -1: for k,v in [kv.split('=') for kv in content_type[p+1:].strip().split(';')]: if k == 'charset': content_charset = v content_type = content_type[:p] if path_ext_serializer is not None and not content_type: content_type = path_ext_serializer.media_types[0] if content_type == 'application/x-www-form-urlencoded' or len(content_type) == 0: # Standard urlencoded content params.update(self.request.post) elif not content_type.startswith('multipart/'): # Multiparts are parsed by smisk.core, so let's try to # decode the body only if it's of another type. try: if content_type: self.request.serializer = serializers.media_types[content_type] elif path_ext_serializer is not None: self.request.serializer = path_ext_serializer if not self.request.serializer or not self.request.serializer.can_unserialize: # If we can not decode the payload, raise a KeyError in order to # generate a UnsupportedMediaType response (see further down...) raise KeyError() log.debug('decoding request payload using %s', self.request.serializer) content_length = int(self.request.env.get('CONTENT_LENGTH', -1)) (eargs, eparams) = self.request.serializer.unserialize(self.request.input, content_length, content_charset) if eargs is not None: args.extend(eargs) if eparams is not None: params.update(eparams) except KeyError: log.error('unable to parse request -- no serializer able to decode %r', content_type) raise http.UnsupportedMediaType() return (args, params)
def response_serializer(self, no_http_exc=False): ''' Return the most appropriate serializer for handling response encoding. :param no_http_exc: If true, HTTP statuses are never rised when no acceptable serializer is found. Instead a fallback serializer will be returned: First we try to return a serializer for format html, if that fails we return the first registered serializer. If that also fails there is nothing more left to do but return None. Primarily used by `error()`. :type no_http_exc: bool :return: The most appropriate serializer :rtype: Serializer ''' # Overridden by explicit response.format? if self.response.format is not None: # Should fail if not exists return serializers.extensions[self.response.format] # Overridden internally by explicit Content-Type header? p = self.response.find_header('Content-Type:') if p != -1: content_type = self.response.headers[p][13:].strip("\t ").lower() p = content_type.find(';') if p != -1: content_type = content_type[:p].rstrip("\t ") try: return serializers.media_types[content_type] except KeyError: if no_http_exc: return Response.fallback_serializer else: raise http.InternalServerError('Content-Type response header is set to type %r '\ 'which does not have any valid serializer associated with it.' % content_type) # Try filename extension fallback = None if no_http_exc: fallback = Response.fallback_serializer serializer = self._serializer_for_request_path_ext(fallback=fallback) if serializer is not None: return serializer # Try media type accept_types = self.request.env.get('HTTP_ACCEPT', None) if accept_types is not None and len(accept_types): if log.level <= logging.DEBUG: log.debug('client accepts: %r', accept_types) # Parse the qvalue header tqs, highqs, partials, accept_any = parse_qvalue_header(accept_types) # If the default serializer exists in the highest quality accept types, return it if Response.serializer is not None: for t in Response.serializer.media_types: if t in highqs: if '*' not in t and self.response.find_header('Content-Type:') == -1: self.response.headers.append('Content-Type: '+t) return Response.serializer # Find a serializer matching any accept type, ordered by qvalue available_types = serializers.media_types.keys() for tq in tqs: t = tq[0] if t in available_types: if '*' not in t and self.response.find_header('Content-Type:') == -1: self.response.headers.append('Content-Type: '+t) return serializers.media_types[t] # Accepts */* which is far more common than accepting partials, so we test this here # and simply return Response.serializer if the client accepts anything. if accept_any: if Response.serializer is not None: return Response.serializer else: return Response.fallback_serializer # If the default serializer matches any partial, return it (the likeliness of # this happening is so small we wait until now) if Response.serializer is not None: for t in Response.serializer.media_types: if t[:t.find('/', 0)] in partials: return Response.serializer # Test the rest of the partials for t, serializer in serializers.media_types.items(): if t[:t.find('/', 0)] in partials: return serializer # If an Accept header field is present, and if the server cannot send a response which # is acceptable according to the combined Accept field value, then the server SHOULD # send a 406 (not acceptable) response. [RFC 2616] log.info('client demanded content type(s) we can not respond in. "Accept: %s"', accept_types) if config.get('smisk.mvc.strict_tcn', True): raise http.NotAcceptable() # The client did not ask for any type in particular # Strict TCN if Response.serializer is None: if no_http_exc or len(serializers) < 2: return Response.fallback_serializer else: raise http.MultipleChoices(self.request.cn_url) # Return the default serializer return Response.serializer
def handle_errors_wrapper(fnc, error_cb=sys.exit, abort_cb=None, *args, **kwargs): '''Call `fnc` catching any errors and writing information to ``error.log``. ``error.log`` will be written to, or appended to if it aldready exists, ``ENV["SMISK_LOG_DIR"]/error.log``. If ``SMISK_LOG_DIR`` is not set, the file will be written to ``ENV["SMISK_APP_DIR"]/error.log``. * ``KeyboardInterrupt`` is discarded/passed, causing a call to `abort_cb`, if set, without any arguments. * ``SystemExit`` is passed on to Python and in normal cases causes a program termination, thus this function will not return. * Any other exception causes ``error.log`` to be written to and finally a call to `error_cb` with a single argument; exit status code. :param error_cb: Called after an exception was caught and info has been written to ``error.log``. Receives a single argument: Status code as an integer. Defaults to ``sys.exit`` causing normal program termination. The returned value of this callable will be returned by `handle_errors_wrapper` itself. :type error_cb: callable :param abort_cb: Like `error_cb` but instead called when ``KeyboardInterrupt`` was raised. :type abort_cb: callable :rtype: object ''' try: # Run the wrapped callable return fnc(*args, **kwargs) except KeyboardInterrupt: if abort_cb: return abort_cb() except SystemExit: raise except: # Write to error.log try: logfile = os.environ.get('SMISK_LOG_DIR', os.environ.get(os.environ['SMISK_APP_DIR'], '.')) logfile = os.path.join(logfile, 'error.log') logfile = os.path.abspath(_config.get('smisk.emergency_logfile', logfile)) f = open(logfile, 'a') try: from traceback import print_exc from datetime import datetime f.write(datetime.now().isoformat()) f.write(" [%d] " % os.getpid()) print_exc(1000, f) finally: f.close() try: print_exc(1000, sys.stderr) except: pass sys.stderr.write('Wrote emergency log to %s\n' % logfile) except Exception, e: try: sys.stderr.write('Failed to write emergency log to %s: %s\n' % (logfile, e)) except: pass # Call error callback if error_cb: return error_cb(1)
def __init__(self): # If persistent evaluates to True, the contents of the shared # dict will be flushed to disk on shutdown and read from disk # on startup, thus providing a persistent set of data. self.entries = shared_dict(persistent=config.get('persistent'))