def test_with_variables(self): m = Map([ URITemplateRule('/', endpoint='root'), URITemplateRule('/browse/', endpoint='entry'), URITemplateRule('/browse/{id}/', endpoint='entry/id'), # this has to explicitly consume the final '/' path fragment URITemplateRule('/browse/{id}{/path*}/', endpoint='entry/id/dir'), # before the fully consumed version URITemplateRule('/browse/{id}{/path*}', endpoint='entry/id/path'), ]).bind('example.com') self.assertEqual(('root', {}), m.match('/')) # TODO allow this type of redirects to work? # self.assertEqual(('entry', {}), m.match('/browse')) self.assertEqual(('entry', {}), m.match('/browse/')) self.assertEqual(('entry/id', { 'id': '1', }), m.match('/browse/1/')) # ditto for /browse/1 # however, if we have this kind of distinction... self.assertEqual(('entry/id/path', { 'id': '1', 'path': ['some', 'nested', 'path'], }), m.match('/browse/1/some/nested/path')) # ... this forces endpoint implementation to be explicit about # how they handle the paths passed. self.assertEqual(('entry/id/dir', { 'id': '1', 'path': ['some', 'nested', 'path'], }), m.match('/browse/1/some/nested/path/'))
def test_precedence(self): m = Map([ URITemplateRule('/entry/{id}', endpoint='entry'), URITemplateRule('/entry/index', endpoint='entry_index'), ]).bind('example.com') self.assertEqual(('entry', {'id': '1'}), m.match('/entry/1')) self.assertEqual(('entry_index', {}), m.match('/entry/index'))
def test_basic_creation_from_str(self): m = Map([ URITemplateRule('/', endpoint='root'), URITemplateRule('/somewhere', endpoint='elsewhere'), ]).bind('example.com') self.assertEqual(('root', {}), m.match('/')) self.assertEqual(('elsewhere', {}), m.match('/somewhere')) with self.assertRaises(NotFound): m.match('/nowhere')
def test_pattern(path, pattern): pattern = '/' + pattern.strip('/') + '/<path:extra>' adapter = Map([Rule(pattern)]).bind('dummy.invalid') try: endpoint, values = adapter.match(path.strip('/')) except NotFound: return return values['year'], values['month'], values['day']
def test_pattern(path, pattern): pattern = "/" + pattern.strip("/") + "/<path:extra>" adapter = Map([Rule(pattern)]).bind("dummy.invalid") try: endpoint, values = adapter.match(path.strip("/")) except NotFound: return return values["year"], values["month"], values["day"]
def test_with_variables_without_trailing_slash(self): m = Map([ URITemplateRule('/', endpoint='root'), URITemplateRule('/browse/', endpoint='entry'), URITemplateRule('/browse/{id}/', endpoint='entry/id'), URITemplateRule('/browse/{id}{/path*}', endpoint='entry/id/path'), ]).bind('example.com') # unlike the previous where there is a "...{/path*}/" version # defined, this will simply be fed into the other view, with # path list containing a trailing empty string path fragment. self.assertEqual(('entry/id/path', { 'id': '1', 'path': ['some', 'nested', 'path', ''], }), m.match('/browse/1/some/nested/path/'))
def match_endpoint(path: str) -> tuple[models.Endpoint, Mapping[str, Any]]: """ Uses werkzeug routing to match the given path to an endpoint. Returns the matched endpoint as well as a mapping of URL parameters passed to the endpoint. If no endpoint was matched, raises a NotFound exception. """ log.debug(f"Matching path `{path}`") endpoints: list[models.Endpoint] = models.Endpoint.query.filter( models.Endpoint.method == request.method.upper()).all() rules = [ Rule(endpoint.path, endpoint=endpoint, methods=[request.method]) for endpoint in endpoints ] urls = Map(rules).bind("localhost") # we're disabling mypy here because you're supposed to get strings back from `match`, not full endpoint objects. return urls.match(path, method=request.method) # type: ignore
class Router(object): converters = { str: 'string', int: 'int', float: 'float', # path, any, uuid } not_found = None method_not_allowed = None def __init__(self, routes: List[Route]): required_type = wsgi.WSGIResponse initial_types = [wsgi.WSGIEnviron, URLPathArgs] rules = [] views = {} for (path, method, view) in routes: view_signature = inspect.signature(view) uritemplate = URITemplate(path) # Ensure view arguments include all URL arguments for arg in uritemplate.variable_names: assert arg in view_signature.parameters, ( 'URL argument "%s" in path "%s" must be included as a ' 'keyword argument in the view function "%s"' % (arg, path, view.__name__)) # Create a werkzeug path string werkzeug_path = path[:] for arg in uritemplate.variable_names: param = view_signature.parameters[arg] if param.annotation == inspect.Signature.empty: annotated_type = str else: annotated_type = param.annotation converter = self.converters[annotated_type] werkzeug_path = werkzeug_path.replace( '{%s}' % arg, '<%s:%s>' % (converter, arg)) # Create a werkzeug routing rule name = view.__name__ rule = Rule(werkzeug_path, methods=[method], endpoint=name) rules.append(rule) # Determine any inferred type annotations for the view extra_annotations = {} for param in view_signature.parameters.values(): if param.annotation == inspect.Signature.empty: annotated_type = str else: annotated_type = param.annotation if param.name in uritemplate.variable_names: class TypedURLPathArg(URLPathArg): schema = annotated_type extra_annotations[param.name] = TypedURLPathArg elif (annotated_type in primitive_types) or issubclass( annotated_type, schema_types): class TypedQueryParam(http.QueryParam): schema = annotated_type extra_annotations[param.name] = TypedQueryParam if 'return' not in view.__annotations__: extra_annotations['return'] = http.ResponseData # Determine the pipeline for the view. pipeline = pipelines.build_pipeline(view, initial_types, required_type, extra_annotations) views[name] = Endpoint(view, pipeline) # Add pipelines for 404 and 405 cases. pipeline = pipelines.build_pipeline(view_404, initial_types, required_type, {}) self.not_found = (None, pipeline, {}) pipeline = pipelines.build_pipeline(view_405, initial_types, required_type, {}) self.method_not_allowed = (None, pipeline, {}) self.routes = routes self.adapter = Map(rules).bind('example.com') self.views = views def lookup(self, path, method): try: (name, kwargs) = self.adapter.match(path, method) except werkzeug.exceptions.NotFound: return self.not_found except werkzeug.exceptions.MethodNotAllowed: return self.method_not_allowed (view, pipeline) = self.views[name] return (view, pipeline, kwargs)
class Router(BaseRouter): def __init__(self, routes): rules = [] name_lookups = {} for path, name, route in self.walk_routes(routes): path_params = [ item.strip('{}') for item in re.findall('{[^}]*}', path) ] args = inspect.signature(route.handler).parameters for path_param in path_params: if path_param.startswith('+'): path = path.replace( '{%s}' % path_param, "<path:%s>" % path_param.lstrip('+') ) elif path_param in args and args[path_param].annotation is int: path = path.replace( '{%s}' % path_param, "<int:%s>" % path_param ) elif path_param in args and args[path_param].annotation is float: path = path.replace( '{%s}' % path_param, "<float:%s>" % path_param ) else: path = path.replace( '{%s}' % path_param, "<string:%s>" % path_param ) rule = Rule(path, methods=[route.method], endpoint=name) rules.append(rule) name_lookups[name] = route self.adapter = Map(rules).bind('') self.name_lookups = name_lookups # Use an MRU cache for router lookups. self._lookup_cache = dict_type() self._lookup_cache_size = 10000 def walk_routes(self, routes, url_prefix='', name_prefix=''): walked = [] for item in routes: if isinstance(item, Route): result = (url_prefix + item.url, name_prefix + item.name, item) walked.append(result) elif isinstance(item, Include): result = self.walk_routes( item.routes, url_prefix + item.url, name_prefix + item.name + ':' ) walked.extend(result) return walked def lookup(self, path: str, method: str): lookup_key = method + ' ' + path try: return self._lookup_cache[lookup_key] except KeyError: pass try: name, path_params = self.adapter.match(path, method) except werkzeug.exceptions.NotFound: raise exceptions.NotFound() from None except werkzeug.exceptions.MethodNotAllowed: raise exceptions.MethodNotAllowed() from None except werkzeug.routing.RequestRedirect as exc: path = urlparse(exc.new_url).path raise exceptions.Found(path) from None route = self.name_lookups[name] self._lookup_cache[lookup_key] = (route, path_params) if len(self._lookup_cache) > self._lookup_cache_size: self._lookup_cache.pop(next(iter(self._lookup_cache))) return (route, path_params) def reverse_url(self, name: str, **params) -> str: try: return self.adapter.build(name, params) except werkzeug.routing.BuildError as exc: raise exceptions.NoReverseMatch(str(exc)) from None
class Router(object): def __init__(self, routes: RoutesConfig, initial_types: List[type]=None) -> None: required_type = wsgi.WSGIResponse initial_types = initial_types or [] initial_types += [wsgi.WSGIEnviron, URLPathArgs, Exception] rules = [] views = {} for (path, method, view) in walk(routes): view_signature = inspect.signature(view) uritemplate = URITemplate(path) # Ensure view arguments include all URL arguments for arg in uritemplate.variable_names: assert arg in view_signature.parameters, ( 'URL argument "%s" in path "%s" must be included as a ' 'keyword argument in the view function "%s"' % (arg, path, view.__name__) ) # Create a werkzeug path string werkzeug_path = path[:] for arg in uritemplate.variable_names: param = view_signature.parameters[arg] if param.annotation is inspect.Signature.empty: converter = 'string' elif issubclass(param.annotation, (schema.String, str)): if getattr(param.annotation, 'format', None) == 'path': converter = 'path' else: converter = 'string' elif issubclass(param.annotation, (schema.Number, float)): converter = 'float' elif issubclass(param.annotation, (schema.Integer, int)): converter = 'int' else: msg = 'Invalid type for path parameter, %s.' % param.annotation raise exceptions.ConfigurationError(msg) werkzeug_path = werkzeug_path.replace( '{%s}' % arg, '<%s:%s>' % (converter, arg) ) # Create a werkzeug routing rule name = view.__name__ rule = Rule(werkzeug_path, methods=[method], endpoint=name) rules.append(rule) # Determine any inferred type annotations for the view extra_annotations = {} # type: Dict[str, type] for param in view_signature.parameters.values(): if param.annotation is inspect.Signature.empty: annotated_type = str else: annotated_type = param.annotation if param.name in uritemplate.variable_names: class TypedURLPathArg(URLPathArg): schema = annotated_type extra_annotations[param.name] = TypedURLPathArg elif (annotated_type in primitive_types) or issubclass(annotated_type, schema_types): if method in ('POST', 'PUT', 'PATCH'): if issubclass(annotated_type, (schema.Object, schema.Array)): class TypedDataParam(http.RequestData): schema = annotated_type extra_annotations[param.name] = TypedDataParam else: class TypedFieldParam(http.RequestField): schema = annotated_type extra_annotations[param.name] = TypedFieldParam else: class TypedQueryParam(http.QueryParam): schema = annotated_type extra_annotations[param.name] = TypedQueryParam return_annotation = view_signature.return_annotation if return_annotation is inspect.Signature.empty: extra_annotations['return'] = http.ResponseData elif issubclass(return_annotation, (schema_types, primitive_types, typing_types)): # type: ignore extra_annotations['return'] = http.ResponseData # Determine the pipeline for the view. pipeline = core.build_pipeline(view, initial_types, required_type, extra_annotations) views[name] = Endpoint(view, pipeline) self.exception_pipeline = core.build_pipeline(exception_handler, initial_types, required_type, {}) self.routes = routes self.adapter = Map(rules).bind('') self.views = views def lookup(self, path: str, method: str) -> RouterLookup: try: (name, kwargs) = self.adapter.match(path, method) except werkzeug.exceptions.NotFound: raise exceptions.NotFound() except werkzeug.exceptions.MethodNotAllowed: raise exceptions.MethodNotAllowed() except werkzeug.routing.RequestRedirect as exc: path = urlparse(exc.new_url).path raise exceptions.Found(path) (view, pipeline) = self.views[name] return (view, pipeline, kwargs) def reverse_url(self, view_name: str, **url_params) -> str: endpoint = self.views.get(view_name) if not endpoint: raise exceptions.NoReverseMatch flattened_routes = walk(self.routes) matched_views = [ route for route in flattened_routes if route.view == endpoint.view ] return matched_views[0].path.format(**url_params)
class Router(object): def __init__(self, routes: RoutesConfig, initial_types: List[type] = None) -> None: required_type = wsgi.WSGIResponse initial_types = initial_types or [] initial_types += [wsgi.WSGIEnviron, URLPathArgs, Exception] rules = [] views = {} for (path, method, view) in walk(routes): view_signature = inspect.signature(view) uritemplate = URITemplate(path) # Ensure view arguments include all URL arguments for arg in uritemplate.variable_names: assert arg in view_signature.parameters, ( 'URL argument "%s" in path "%s" must be included as a ' 'keyword argument in the view function "%s"' % (arg, path, view.__name__)) # Create a werkzeug path string werkzeug_path = path[:] for arg in uritemplate.variable_names: param = view_signature.parameters[arg] if param.annotation is inspect.Signature.empty: converter = 'string' elif issubclass(param.annotation, (schema.String, str)): if getattr(param.annotation, 'format', None) == 'path': converter = 'path' else: converter = 'string' elif issubclass(param.annotation, (schema.Number, float)): converter = 'float' elif issubclass(param.annotation, (schema.Integer, int)): converter = 'int' else: msg = 'Invalid type for path parameter, %s.' % param.annotation raise exceptions.ConfigurationError(msg) werkzeug_path = werkzeug_path.replace( '{%s}' % arg, '<%s:%s>' % (converter, arg)) # Create a werkzeug routing rule name = view.__name__ rule = Rule(werkzeug_path, methods=[method], endpoint=name) rules.append(rule) # Determine any inferred type annotations for the view extra_annotations = {} # type: Dict[str, type] for param in view_signature.parameters.values(): if param.annotation is inspect.Signature.empty: annotated_type = str else: annotated_type = param.annotation if param.name in uritemplate.variable_names: class TypedURLPathArg(URLPathArg): schema = annotated_type extra_annotations[param.name] = TypedURLPathArg elif (annotated_type in primitive_types) or issubclass( annotated_type, schema_types): if method in ('POST', 'PUT', 'PATCH'): if issubclass(annotated_type, schema.Object): class TypedDataParam(http.RequestData): schema = annotated_type extra_annotations[param.name] = TypedDataParam else: class TypedFieldParam(http.RequestField): schema = annotated_type extra_annotations[param.name] = TypedFieldParam else: class TypedQueryParam(http.QueryParam): schema = annotated_type extra_annotations[param.name] = TypedQueryParam return_annotation = view_signature.return_annotation if return_annotation is inspect.Signature.empty: extra_annotations['return'] = http.ResponseData elif issubclass( return_annotation, (schema_types, primitive_types, typing_types)): # type: ignore extra_annotations['return'] = http.ResponseData # Determine the pipeline for the view. pipeline = pipelines.build_pipeline(view, initial_types, required_type, extra_annotations) views[name] = Endpoint(view, pipeline) self.exception_pipeline = pipelines.build_pipeline( exception_handler, initial_types, required_type, {}) self.routes = routes self.adapter = Map(rules).bind('example.com') self.views = views def lookup(self, path: str, method: str) -> RouterLookup: try: (name, kwargs) = self.adapter.match(path, method) except werkzeug.exceptions.NotFound: raise exceptions.NotFound() except werkzeug.exceptions.MethodNotAllowed: raise exceptions.MethodNotAllowed() except werkzeug.routing.RequestRedirect as exc: path = urlparse(exc.new_url).path raise exceptions.Found(path) (view, pipeline) = self.views[name] return (view, pipeline, kwargs)
class WerkzeugRouter(Router): def __init__(self, routes: RouteConfig) -> None: rules = [] # type: typing.List[Rule] views = {} # type: typing.Dict[str, typing.Callable] for path, method, view, name in flatten_routes(routes): if name in views: msg = ('Route wtih name "%s" exists more than once. Use an ' 'explicit name="..." on the Route to avoid a conflict.' ) % name raise exceptions.ConfigurationError(msg) template = uritemplate.URITemplate(path) werkzeug_path = str(path) parameters = inspect.signature(view).parameters for arg in template.variable_names: converter = self._get_converter(parameters, arg, view) template_format = '{%s}' % arg werkzeug_format = '<%s:%s>' % (converter, arg) werkzeug_path = werkzeug_path.replace(template_format, werkzeug_format) rule = Rule(werkzeug_path, methods=[method], endpoint=name) rules.append(rule) views[name] = view self._routes = routes self._adapter = Map(rules).bind('') self._views = views # Use an MRU cache for router lookups. self._lookup_cache = collections.OrderedDict( ) # type: collections.OrderedDict self._lookup_cache_size = 10000 def _get_converter(self, parameters: typing.Mapping[str, inspect.Parameter], arg: str, view: typing.Callable) -> str: if arg not in parameters: msg = 'URL Argument "%s" missing from view "%s".' % (arg, view) raise exceptions.ConfigurationError(msg) annotation = parameters[arg].annotation if annotation is inspect.Parameter.empty: return 'string' elif issubclass(annotation, PathWildcard): return 'path' elif issubclass(annotation, str): return 'string' elif issubclass(annotation, int): return 'int' elif issubclass(annotation, float): return 'float' msg = 'Invalid type for path parameter "%s" in view "%s".' % ( parameters[arg], view) raise exceptions.ConfigurationError(msg) def lookup(self, path: str, method: str) -> HandlerLookup: lookup_key = method + ' ' + path try: return self._lookup_cache[lookup_key] except KeyError: pass try: name, kwargs = self._adapter.match(path, method) except werkzeug.exceptions.NotFound: raise exceptions.NotFound() from None except werkzeug.exceptions.MethodNotAllowed: raise exceptions.MethodNotAllowed() from None except werkzeug.routing.RequestRedirect as exc: path = urlparse(exc.new_url).path raise exceptions.Found(path) from None view = self._views[name] self._lookup_cache[lookup_key] = (view, kwargs) if len(self._lookup_cache) > self._lookup_cache_size: self._lookup_cache.pop(next(iter( self._lookup_cache))) # pragma: nocover return (view, kwargs) def reverse_url(self, identifier: str, values: dict = None) -> str: try: return self._adapter.build(identifier, values) except werkzeug.routing.BuildError as exc: raise exceptions.NoReverseMatch(str(exc)) from None
class Arsa(object): """ Arsa is an object that stores the configuration needed for Arsa.io. """ def __init__(self): self.factory = RouteFactory() self.routes = None self.middlewares = [] self.exceptions = [] def route(self, rule, methods=None, content_type='application/json'): """ Convenience decorator for defining a route """ if methods is None: methods = ['GET'] def decorator(func): route = self.factory.register_endpoint(func) route.set_rule(rule, methods, mimetype=content_type) return func return decorator def required(self, **expected_kwargs): def decorator(func): route = self.factory.register_endpoint(func) route.add_validation(**expected_kwargs) return func return decorator def optional(self, **optional_kwargs): def decorator(func): route = self.factory.register_endpoint(func) route.add_validation(True, **optional_kwargs) return func return decorator def handler(self, event, context): app = self.create_app() builder = AWSEnvironBuilder(event, context) builder.close() environ = builder.get_environ() resp = run_wsgi_app(app, environ) #wrap response response = Response(*resp) # log response print('{host} - - "{method} {path} {protocol}" {status}\n'.format( host=environ.get('SERVER_NAME', 'localhost'), method=environ.get('REQUEST_METHOD', 'GET'), path=environ.get('PATH_INFO', '/'), protocol=environ.get('SERVER_PROTOCOL', '/'), status=response.status_code)) return { "statusCode": response.status_code, "headers": dict(response.headers), "body": response.get_data(as_text=True) } def add_middleware(self, middleware): if callable(middleware): self.middlewares.append(middleware) def add_exception(self, error_type): self.exceptions.append(error_type) def create_app(self): if not self.routes: self.routes = Map(rules=[self.factory]).bind('arsa.io') def app(environ, start_response): req = Request(environ) _request_ctx_stack.push(RequestContext(req)) try: # Call middlewares for middleware in self.middlewares: middleware() # Find url rule (rule, arguments) = self.routes.match(req.path, method=req.method, return_rule=True) arguments.update(dict(req.args)) if req.form: arguments.update(req.form) if req.data: try: data = json.loads(req.data) arguments.update(data) except ValueError: raise BadRequest("JSON body was malformed") rule.has_valid_arguments(arguments) decoded_args = rule.decode_arguments(arguments) body = rule.endpoint(**decoded_args) if rule.mimetype == 'application/json': body = json.dumps(body, default=to_serializable) resp = Response(body, mimetype=rule.mimetype) except Redirect as error: resp = error except tuple(self.exceptions) as error: code = error.code if hasattr(error, 'code') else 400 resp = Response(response=json.dumps(error, default=to_serializable), status=code, mimetype='application/json') except HTTPException as error: resp = Response(response=json.dumps(error, default=to_serializable), status=error.code, mimetype='application/json') _request_ctx_stack.pop() return resp(environ, start_response) return app