xrtr

PyPI - Status PyPI Package latest release Supported versions Travis-CI Build Status Documentation Status Coverage Status

A generic string router based on a Radix Tree structure, (partially) Cython optimized for speed.

Inspiration

xrtr is highly inspired in Router, by shiyanhui.

License

xrtr is a free software distributed under the MIT license, the same license as Router’s license.

To Do

  • There is a LOT of room for improvement (specially when migrating the code to C and Cython and the fact this is my first project with Cython);
  • Fix test coverage (and why is it not covering method declarations, as an example);
  • There is a lot of fixes to be done regarding Cython, distribution, naming conventions and so on;
  • Add Windows builds using AppVeyor;

Installing

To install xrtr, use pip (or your favorite Python package manager), like:

pip install xrtr

And you’re ready to the next step. Hooray!

Warning

At this time, xrtr binary packages are only available for Linux. Other platforms will be added in the future. PRs are welcome!

Using xrtr

xrtr is a fast (and will be faster) path router for Python written in Cython, primarily for web frameworks but it’s flexible to be used with other conventions as well. It is based on a compressed dynamic trie (radix tree) structure for efficient matching, with support for variables, endpoints, layered middlewares (or any other objects) and very fast speeds provided by a Cython optimization of the tree.

The tree

To use the xrtr router, you simply need to instantiate the RadixTree object:

>>> from xrtr import RadixTree

>>> tree = RadixTree()

Now, let’s say you have an endpoint (object, function) to be run without variables:

>>> def myendpoint():
...     return "hello, world"

>>> tree.insert("/", myendpoint, ["GET"])

>>> tree.get("/", "GET")
(<function __main__.myendpoint()>, [], {})

If you want to grab variables, you can choose to get a variable between the separator (defaults to /) or glob everything that comes after.

To create a simple variable, use a colon : as identifier:

>>> tree.insert("/foo/:bar", myendpoint, ["GET"])

>>> tree.get("/foo/hello", "GET")
(<function __main__.myendpoint()>, [], {'bar': 'hello'})

If you want to glob everything in a variable, use an asterisk * as identifier:

>>> tree.insert("/static/*path", myendpoint, ["GET"])

>>> tree.get("/static/my/file/may/be/somewhere.py", "GET")
(<function __main__.myendpoint()>, [], {'path': 'my/file/may/be/somewhere.py'})

Note

You may have already noticed that the method RadixTree.get returns a tuple of three objects: the endpoint (or None), a list of middlewares (see the next chapter) and a dictionary of the variables and its values (if any matches). In case of no matches or an endpoint object / function is not found, the default return will be None, [], {}.

Middlewares

Sometimes, you want to add a function or, most commonly, a middleware to be executed before or after a request, to any or certain points. Luckily, to execute a middleware in certain points may be hard on most solutions, either leaving you without any alternatives to implement one or based on subclassing views, routes, handlers, you name it. xrtr aims to make this very, very simple.

Nothing better than a simple example:

>>> def mymiddleware():
...     return "hello, middle world"

>>> tree.insert("/foo", mymiddleware, ["GET"], no_conflict=True)

>>> tree.get("/foo/hello", "GET")
(<function __main__.myendpoint()>, [<function __main__.mymiddleware()>], {'bar': 'hello'})

Those middlewares can be stacked without replication and, again, can be a function, object, anything you find appropriate. Just don’t forget to add the keyword no_conflict to True when invoking the RadixTree.insert method.

Configuring identifiers

In case you’re wondering: “another path based router?”, don’t worry: the separator (defaults to /) and variable (defaults to :) can be configurable (as long as they’re punctuations). The glob identifier (defaults to *) is not configurable.

There are two ways of changing the identifiers: using the class constructor or changing them at runtime.

Warning

You can only change the identifiers prior to inserting routes, otherwise it raises ValueError.

Changing at runtime
>>> from xrtr import RadixTree

>>> tree = RadixTree()

>>> tree.SEPARATOR = "."
>>> tree.SEPARATOR
'.'

>>> tree.VARIABLE = "$"
>>> tree.VARIABLE
'$'

>>> tree.insert(".foo.$bar", object, ["FOO"])
>>> tree.get(".foo.hello", "FOO")
(object, [], {'bar': 'hello'})

>>> tree.config
{'variable': '$', 'separator': '.'}
Using the constructor
>>> from xrtr import RadixTree

>>> tree = RadixTree(separator=".", variable="$")

>>> tree.SEPARATOR
'.'

>>> tree.VARIABLE
'$'

>>> tree.insert(".foo.$bar", object, ["FOO"])
>>> tree.get(".foo.hello", "FOO")
(object, [], {'bar': 'hello'})

>>> tree.config
{'variable': '$', 'separator': '.'}

Method utilities

Starting with xrtr 0.2.0, some changes were required to quickly identify if the given route does in fact exists, but the requested method is not available. Enter in scene: the sentinel object.

Sentinel object

Everytime you search for a route and its specific method, sometimes the route even exists (let’s say, /foo), but the requested method doesn’t (GET exists, OPTIONS don’t). This can be quickly checked against the sentinel object (or property, in xrtr case):

>>> from xrtr import RadixTree

>>> tree = RadixTree()

>>> tree.insert("/foo", some_endpoint, ["GET"])

>>> handler, middlewares, params = tree.get("/foo", "OPTIONS")

>>> handler is tree.sentinel
True

This way, it is simple to deal with more fine grained errors, such as HTTP 405.

Available methods

In case you need just to get the available methods of one simple endpoint (for informational purposes), you can perform that by using the methods_for method:

>>> from xrtr import RadixTree
>>> tree = RadixTree()
>>> tree.insert("/foo", some_endpoint, ["GET"])
>>> tree.methods_for("/foo")
{'GET'}
Code coverage

For now, xrtr may have a low coverage (than intended), but that’s due to a characteristic of Cython projects where coverage won’t catch function signatures. See more regarding this here. When a less hacky, integrated solution becomes available, it shall be used.

Reference

xrtr

Contributing

Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given.

Bug reports

When reporting a bug please include:

  • Your operating system name and version.
  • Any details about your local setup that might be helpful in troubleshooting.
  • Detailed steps to reproduce the bug.

Documentation improvements

xrtr could always use more documentation, whether as part of the official xrtr docs, in docstrings, or even on the web in blog posts, articles, and such.

Feature requests and feedback

The best way to send feedback is to file an issue at https://github.com/vltr/xrtr/issues.

If you are proposing a feature:

  • Explain in detail how it would work.
  • Keep the scope as narrow as possible, to make it easier to implement.
  • Remember that this is a volunteer-driven project, and that code contributions are welcome :)

Development

To set up xrtr for local development:

  1. Fork xrtr (look for the “Fork” button).

  2. Clone your fork locally:

    git clone git@github.com:your_name_here/xrtr.git
    
  3. Create a branch for local development:

    git checkout -b name-of-your-bugfix-or-feature
    

    Now you can make your changes locally.

  4. When you’re done making changes, run all the checks, doc builder and spell checker with tox one command:

    tox
    
  5. Commit your changes and push your branch to GitHub:

    git add .
    git commit -m "Your detailed description of your changes."
    git push origin name-of-your-bugfix-or-feature
    
  6. Submit a pull request through the GitHub website.

Pull Request Guidelines

If you need some code review or feedback while you’re developing the code just make the pull request.

For merging, you should:

  1. Include passing tests (run tox) [1].
  2. Update documentation when there’s new API, functionality etc.
  3. Add a note to CHANGELOG.rst about the changes.
  4. Add yourself to AUTHORS.rst.
[1]

If you don’t have all the necessary python versions available locally you can rely on Travis - it will run the tests for each change you add in the pull request.

It will be slower though …

Tips

To run a subset of tests:

tox -e envname -- pytest -k test_myfeature

To run all the test environments in parallel (you need to pip install detox):

detox

Changelog

v0.2.1 on 2018-10-09

  • Fixed bug where the no_conflict flag where not being propagated to the add_method if a “non-conflicting method” was the first node being created in that tree.

v0.2.0 on 2018-10-03

  • Add method_for function.

v0.1.4 on 2018-10-03

  • Add sentinel object.

v0.1.3 on 2018-09-21

  • Add testing for repeated variables.

v0.1.2 on 2018-09-14

  • Minor tweaks and improvements (search optimizations).

v0.1.1 on 2018-09-11

  • Minor tweaks and improvements.

v0.1.0 on 2018-08-22

  • First release on PyPI.

Authors