def load(lines): """ Load and create URL mapper from the given set of rendered lines. See render() for more details. """ mapper = UrlMapper() inpat_re = re.compile('([^:\s]+)\s*:\s*(.*)\s*$') for line in lines: if not line: continue # Split the id and urlpattern. try: mo = inpat_re.match(line.strip()) if not mo: raise ValueError resid, urlpattern = mo.groups() except ValueError: raise RanvierError( "Warning: Error parsing line '%s' on load." % line) # Note: we do not have defaults when loading from the rendered # representation. # Parse the loaded line. unparsed, isterminal = urlpattern_to_components(urlpattern) # Create and add the new mapping. mapping = Mapping(resid, unparsed, isterminal) mapper._add_mapping(mapping) return mapper
def __init__(self, resid, unparsed, isterminal, resobj=None, optparams=None): """ 'unparsed' is a tuple of :: (scheme, netloc, absolute -> bool, components, query, fragment). Otherwise it is None and means that this is a relative mapping. See urlpattern_to_components() for a description of the components list. """ # Unpack and store the prefix/suffix for later scheme, netloc, absolute, components, query, fragment = unparsed self.prefix = scheme, netloc self.suffix = fragment, # Whether the path is relative to the mapper's rootloc or absolute # (without or outside of this site). self.absolute = absolute # The list of components self.components = components # Resource-id and resource object (if specified) self.resid = resid self.resource = resobj # True if this resource does not have any further branches self.isterminal = isterminal # # Pre-calculate stuff for faster backmapping when rendering pages. # # Build a usable URL string template. self.urltmpl, self.urltmpl_untyped = self.create_path_templates() # Set the positional args to the list of components with variables. self.positional = filter(lambda x: isinstance(x, VarComponent), components) self.posmap = dict((x.varname, None) for x in self.positional) # Note: only exists for efficient copy in mapurl. # Check for collisions. We keep the set around for fast validity # checks afterwards. varset = set() for comp in chain(self.positional, optparams or ()): if comp.varname in varset: raise RanvierError( "Variable name collision in URI path: '%s'" % comp.varname) varset.add(comp.varname) self.varset = varset # Note: varset includes both the positional and optional parameters. # A dict of the optional parameters. self.optparams = dict((x.varname, x) for x in optparams or ())
def create_coverage_reporter(connection_str): """ Create one of the coverage reporters from a connection string. This is used by the command-line module to specify a database connection via a single string. """ mo = type_re.match(connection_str) if not mo: raise RanvierError("Error: Invalid connection string '%s'." % connection_str) method, conndesc = mo.groups() # Support dbm. if method == 'dbm': # connstr is expected to be the filename. reporter = DbmCoverageReporter(conndesc, True) # Support SQL databases. elif method in ('postgres', ): try: mo = conn_str_re.match(conndesc) if not mo: raise RanvierError( "Error: Invalid database description '%s'." % conndesc) user, passwd, host, dbname = map(lambda x: x or '', mo.groups()) if method == 'postgres': import psycopg2 as dbmodule conn = dbmodule.connect(database=dbname, host=host, user=user, password=passwd) def acquire(): return conn # Unique reference to connection. def release(conn): pass reporter = SqlCoverageReporter(dbmodule, acquire, release) except Exception, e: raise RanvierError("Error: Could not connect to database: '%s'." % e)
def remove_reporter(self, reporter): """ Remove the given reporter from the list. The reporter must have been previously added. """ try: self.reporters.remove(reporter) except IndexError: raise RanvierError("Trying to remove an unregistered reporter.")
def urlload(url): """ Load and create a URL mapper by fetching the specified url via the network. """ try: enumres_text = urllib.urlopen(url).read() except IOError: raise RanvierError( "Error: Fetching contents of mapper from URL '%s'." % url) return UrlMapper.load(enumres_text.splitlines())
def declare_target(self, varname=None, format=None): """ Declare that the given resource may serve the contents at this point in the path (this does not have to be a leaf, this could be used for a directory: e.g. if the request is at the leaf, we serve the directory contents). 'varname' can be used to declare that it consumes some path components as well (optional). """ if self.isleaf(): raise RanvierError("Error: Resource is declared twice as a leaf.") if format and not varname: raise RanvierError("Error: You cannot declare a format without " "a variable.") if varname is None: self.leaf = (self.resource, None) else: self.leaf = (self.resource, VarComponent(varname, format))
def _get_mapping(self, res): """ Get the mapping for a particular resource-id. """ resid = getresid(res) # Get the desired mapping. try: mapping = self.mappings[resid] except KeyError: raise RanvierError("Error: invalid resource-id '%s'." % resid) return mapping
def add_alias(self, new_resid, existing_resid): """ Add an alias to a mapping, that is, another resource-id which will point to the same mapping. The target mapping must already be existing. """ try: mapping = self.mappings[existing_resid] except KeyError: raise RanvierError( "Error: Target mapping '%s' must exist for alias '%s'." % (existing_resid, new_resid)) new_mapping = copy.copy(mapping) new_mapping.resid = new_resid self._add_mapping(new_mapping)
def getdefault(self): if isinstance(self._default, str): # The default is a string, the name of the child, this must be # the first time it is called, we replace it by the actual # resource object for the next calls. try: res = self[self._default] except KeyError: raise RanvierError( "Error: folder default child '%s' not found" % self._default) else: res = self._default assert isinstance(res, (types.NoneType, Resource)) return res
def _add_mapping(self, mapping): """ Add the given mapping, check for uniqueness. """ resid = mapping.resid # Check that the resource-id has not already been seen. if resid in self.mappings: lines = ("Error: Duplicate resource id '%s':" % resid, " Existing mapping: %s" % self.mappings[resid].render_pattern(self.rootloc), " New mapping : %s" % mapping.render_pattern(self.rootloc)) raise RanvierError(os.linesep.join(lines)) # Store the mapping. self.mappings[resid] = mapping
def consume_component(self, ctxt): if _verbosity >= 1: ctxt.response.log("resolver: %s" % ctxt.locator.path[ctxt.locator.index:]) # Get the rest of the components. loc = ctxt.locator comps = [] while not loc.isleaf(): comps.append(loc.current()) loc.next() # Store the component values in the context. if hasattr(ctxt, self.compname): raise RanvierError("Error: Context already has attribute '%s'." % self.compname) setattr(ctxt, self.compname, comps)
def handle_base(self, ctxt): if _verbosity >= 1: ctxt.response.log("resolver: %s" % ctxt.locator.path[ctxt.locator.index:]) if ctxt.locator.isleaf(): if _verbosity >= 1: ctxt.response.log("resolver: at leaf") if not ctxt.locator.trailing and self.redirect_leaf_as_dir: # If a folder resource is requested by default, redirect so that # relative paths will work in that directory. return ctxt.response.redirect(ctxt.locator.uri() + '/') return self.handle_default(ctxt) # else ... name = ctxt.locator.current() if _verbosity >= 1: ctxt.response.log("resolver: getting named child %s" % name) try: child = self[ name ] if not isinstance(child, Resource): msg = "resolver: child is not a resource: %s" % child ctxt.response.log(msg) raise RanvierError(msg) except KeyError: # Try fallback method. child = self.notfound(ctxt, name) if child is None: if _verbosity >= 1: ctxt.response.log("resolver: child %s not found" % name) return ctxt.response.errorNotFound() if _verbosity >= 1: ctxt.response.log("resolver: child %s found, calling it" % name) # Let the folder do some custom handling. if Resource.handle_base(self, ctxt): return True ctxt.locator.next() return self.delegate(child, ctxt)
def declare_optparam(self, varname, format=None): """ Declare an optional query parameter that this node consumes. Query parameters are the arguments specified after the `?' as in :: /users/<user>)/index?category=<category> They are used to allow back-mapping URLs with a single syntax. In the example, above, this would be:: mapurl('@@IndexForUser', <user>, category=<category>) """ if not isinstance(varname, str): raise RanvierError( "Error: optional parameter names must be strings.") self.optparams.append(OptParam(varname, format))
def consume_component(self, ctxt): if _verbosity >= 1: ctxt.response.log("resolver: %s" % ctxt.locator.path[ctxt.locator.index:]) # Make sure we're not at the leaf. if ctxt.locator.isleaf(): return ctxt.response.errorNotFound() # Get the name of the current component. comp = ctxt.locator.current() # Store the component value in the context. if hasattr(ctxt, self.compname): raise RanvierError("Error: Context already has attribute '%s'." % self.compname) setattr(ctxt, self.compname, comp) # Consume the component. ctxt.locator.next()
def urlpattern_to_components(urlpattern): """ Convert a URL pattern string to a list of components. Return a tuple of (scheme, netloc, absolute, list of components, query, fragment). The list of components consists of (name -> str, var -> bool, default -> str|None, format -> str|None) tuples. Default values can be specified via 'defaults' if given, until we can add enough to the URL pattern format to provide this. Note: We determine solely from the URL pattern whether it is a relative vs. absolute path, and we do this here, e.g.:: http://domain.com/gizmos : external link /gizmos : absolute link gizmos : relative link Relative URLs are always considered to be relative to the root of the mapper, and not relative to each other, folder, etc. Normally, the only relative mappings are those from the resource tree. When we render out the mappings, however, they are always absolute. The format of the variable components is a parenthesized name, e.g.:: /users/(username)/mygizmos You can specify a format for producing URLs for specific components:: /catalog/gizmos/(id%08d) """ scheme, netloc, path, query, fragment = urlparse.urlsplit(urlpattern) # Find out if the URL we're trying to map is absolute. absolute = scheme or netloc or path.startswith('/') # Remove the prepending slash for splitting the components. if path.startswith('/'): path = path[1:] # Find out if this is a terminal and remove the extra slash. isterminal = path.endswith('/') if isterminal: path = path[:-1] # Parse the URL pattern. components = [] # name, var, format for comp in path.split('/'): mo = compre.match(comp) if not mo: # Catch components with parentheses that are misformed. if ')' in comp or '(' in comp: raise RanvierError( "Error: Invalid component in static mapping '%s'." % urlpattern) # Add a fixed component components.append(FixedComponent(comp)) continue else: varname, varformat = mo.group(1, 2) components.append(VarComponent(varname, varformat)) return (scheme, netloc, absolute, components, query, fragment), isterminal
def handle_request(self, method, uri, args, response_proxy=None, ctxt_cls=None, **extra): """ Handle a request, via the resource tree. This is the pattern matching / forward mapping part. 'method': the request method, i.e. GET, POST, etc. 'uri': the requested URL, including the rootloc, if present. 'args': a dict of the arguments (POST or GET variables) 'response_proxy': an adapter for the resources that Ranvier provides. 'ctxt_cls': a class object that is instantiated instead of the default one. Must derive from HandlerContext. 'extra': the extra keyword args are added as attribute to the context object that the handlers receive """ if self.root_resource is None: raise RanvierError("Error: You need to initialize the mapper with " "a resource to perform forward mapping.") assert isinstance(uri, str) # assert isinstance(args, dict) # Note: Also allow dict-like interfaces. assert isinstance(response_proxy, (types.NoneType, respproxy.ResponseProxy)) redirect_data = None while True: # Start reporter. for rep in self.reporters: rep.begin() # Remove the root location if necessary. if self.rootloc is not None: if not uri.startswith(self.rootloc): raise RanvierBadRoot("Error: Incorrect root location '%s' " "for requested URI '%s'." % (self.rootloc, uri)) uri = uri[len(self.rootloc):] # Create a context for the handling. if ctxt_cls is None: ctxt_cls = HandlerContext else: assert issubclass(ctxt_cls, HandlerContext) ctxt = ctxt_cls(method, uri, args, self.rootloc) ctxt.mapper = self # Add the redirect data ctxt.redirect_data = redirect_data # Setup the reporters. ctxt.reporters = self.reporters # Standard stuff that we graft onto the context object. ctxt.response = response_proxy # Provide in the context a function to backmap URLs from resource # ids. We should not need more than this, so we try not to provide # access to the full mapper to resource handlers, at least not until # we really need it. ctxt.mapurl = self.mapurl # Add extra payload on the context object. for aname, avalue in extra.iteritems(): setattr(ctxt, aname, avalue) # Handle the request. try: try: Resource.delegate(self.root_resource, ctxt) break # Success, break out. except InternalRedirect, e: redirect_data = e uri, args = e.uri, e.args # Loop again for the internal redirect. finally: # Complete reporters. for rep in self.reporters: rep.end() return ctxt
def mapurl(self, resid, *args, **kwds): """ Map a resource-id to its URL, filling in the parameters and making sure that they are valid. The keyword arguments are expected to match the required arguments for the URL exactly. The resource id can be either 1. a string of the class 2. the class object of the resource (if there is only one of them instanced in the tree) 3. the instance of the resource Positional arguments can be used as well, and they are used to fill in the URL string with the missing components, in left-to-right order (root to leaf). Alternatively, if a **single** positional argument is provided and it is **an instance or a dict type**, we will fetch the attributes/values from this object to fill in the missing values. You can combine this with keyword arguments as well. """ mapping = self._get_mapping(resid) # Create a dict of the required positional arguments. posargs = mapping.posmap.copy() # Check for an instance or dict to fetch some positional args from. if len(args) == 1 and not isinstance(args[0], (str, unicode, int, long)): diccontainer = args[0] if not isinstance(diccontainer, dict): # Then we must have hold of a user defined class. diccontainer = diccontainer.__dict__ else: # No dict, instead we have positional arguments. diccontainer = {} # Check that we don't have extra positional arguments. nbpos = len(posargs) if len(args) > nbpos: raise RanvierError( "Error: Resource '%s' takes at most %d arguments " "(%d given)." % (mapping.resid, nbpos, len(args))) # Set the positional arguments. for comp, arg in zip(mapping.positional, args): posargs[comp.varname] = arg # Fill in the missing positional arguments. for name, value in posargs.iteritems(): if value is None: # Try to get the name from the keyword arguments. try: value = kwds.pop(name) except KeyError: # Try to get the name from the container object. try: value = diccontainer[name] except KeyError: raise RanvierError( "Error: Resource '%s' had no value supplied for " "positional argument '%s'" % (mapping.resid, name)) posargs[name] = value # Sanity check. if __debug__: for value in posargs.itervalues(): assert value is not None # Process the remaining keyword arguments. They should specify only # optional parameters at this point, we should have extracted positional # arguments specified as keywords arguments above. for name, value in kwds.iteritems(): if name not in mapping.optparams: if name in posargs: raise RanvierError("Error: Resource '%s' got multiple " "values for optional parameter '%s'" % (mapping.resid, name)) else: # Note: we may want to accept all optional parameters # regardless, i.e. remove this error. raise RanvierError("Error: Resource '%s' got an " "unexpected optional parameter '%s'" % (mapping.resid, name)) optargs = kwds # Register the target in the call graph, if enabled. for rep in self.reporters: rep.register_rendered(resid) # Perform the substitution. return mapping.render(posargs, optargs, self.rootloc)
raise RanvierError( "Error: Invalid database description '%s'." % conndesc) user, passwd, host, dbname = map(lambda x: x or '', mo.groups()) if method == 'postgres': import psycopg2 as dbmodule conn = dbmodule.connect(database=dbname, host=host, user=user, password=passwd) def acquire(): return conn # Unique reference to connection. def release(conn): pass reporter = SqlCoverageReporter(dbmodule, acquire, release) except Exception, e: raise RanvierError("Error: Could not connect to database: '%s'." % e) else: # Note: I'm sure you could easily add other supported types... it's # pretty much just a matter of establishing a connection. raise RanvierError( "Error: Method '%s' not supported, invalid method?" % method) return reporter