Skip to main content

Core routing component for Sanic

Project description

Sanic Routing

Background

Beginning in v21.3, Sanic makes use of this new AST-style router in two use cases:

  1. Routing paths; and
  2. Routing signals.

Therefore, this package comes with a BaseRouter that needs to be subclassed in order to be used for its specific needs.

Most Sanic users should never need to concern themselves with the details here.

Basic Example

A simple implementation:

import logging

from sanic_routing import BaseRouter

logging.basicConfig(level=logging.DEBUG)


class Router(BaseRouter):
    def get(self, path, *args, **kwargs):
        return self.resolve(path, *args, **kwargs)


router = Router()

router.add("/<foo>", lambda: ...)
router.finalize()
router.tree.display()
logging.info(router.find_route_src)

route, handler, params = router.get("/matchme", method="BASE", extra=None)

The above snippet uses router.tree.display() to show how the router has decided to arrange the routes into a tree. In this simple example:

<Node: level=0>
    <Node: part=__dynamic__:str, level=1, groups=[<RouteGroup: path=<foo:str> len=1>], dynamic=True>

We can can see the code that the router has generated for us. It is available as a string at router.find_route_src.

def find_route(path, method, router, basket, extra):
    parts = tuple(path[1:].split(router.delimiter))
    num = len(parts)

    # node=1 // part=__dynamic__:str
    if num == 1:  # CHECK 1
        try:
            basket['__matches__'][0] = str(parts[0])
        except ValueError:
            pass
        else:
            # Return 1
            return router.dynamic_routes[('<__dynamic__:str>',)][0], basket
    raise NotFound

FYI: If you are on Python 3.9, you can see a representation of the source after compilation at router.find_route_src_compiled

What's it doing?

Therefore, in general implementation requires you to:

  1. Define a router with a get method;
  2. Add one or more routes;
  3. Finalize the router (router.finalize()); and
  4. Call the router's get method.

NOTE: You can call router.finalize(False) if you do not want to compile the source code into executable form. This is useful if you only intend to review the generated output.

Every time you call router.add you create one (1) new Route instance. Even if that one route is created with multiple methods, it generates a single instance. If you add() another Route that has a similar path structure (but, perhaps has differen methods) they will be grouped together into a RouteGroup. It is worth also noting that a RouteGroup is created the first time you call add(), but subsequent similar routes will reuse the existing grouping instance.

When you call finalize(), it is taking the defined route groups and arranging them into "nodes" in a hierarchical tree. A single node is a path segment. A Node instance can have one or more RouteGroup on it where the Node is the termination point for that path.

Perhaps an example is easier:

router.add("/path/to/<foo>", lambda: ...)
router.add("/path/to/<foo:int>", lambda: ...)
router.add("/path/to/different/<foo>", lambda: ...)
router.add("/path/to/different/<foo>", lambda: ..., methods=["one", "two"])

The generated RouteGroup instances (3):

<RouteGroup: path=path/to/<foo:str> len=1>
<RouteGroup: path=path/to/<foo:int> len=1>
<RouteGroup: path=path/to/different/<foo:str> len=2>

The generated Route instances (4):

<Route: path=path/to/<foo:str>>
<Route: path=path/to/<foo:int>>
<Route: path=path/to/different/<foo:str>>
<Route: path=path/to/different/<foo:str>>

The Node Tree:

<Node: level=0>
    <Node: part=path, level=1>
        <Node: part=to, level=2>
            <Node: part=different, level=3>
                <Node: part=__dynamic__:str, level=4, groups=[<RouteGroup: path=path/to/different/<foo:str> len=2>], dynamic=True>
            <Node: part=__dynamic__:int, level=3, groups=[<RouteGroup: path=path/to/<foo:int> len=1>], dynamic=True>
            <Node: part=__dynamic__:str, level=3, groups=[<RouteGroup: path=path/to/<foo:str> len=1>], dynamic=True>

And, the generated source code:

def find_route(path, method, router, basket, extra):
    parts = tuple(path[1:].split(router.delimiter))
    num = len(parts)

    # node=1 // part=path
    if num > 1:  # CHECK 1
        if parts[0] == "path":  # CHECK 4

            # node=1.1 // part=to
            if num > 2:  # CHECK 1
                if parts[1] == "to":  # CHECK 4

                    # node=1.1.1 // part=different
                    if num > 3:  # CHECK 1
                        if parts[2] == "different":  # CHECK 4

                            # node=1.1.1.1 // part=__dynamic__:str
                            if num == 4:  # CHECK 1
                                try:
                                    basket['__matches__'][3] = str(parts[3])
                                except ValueError:
                                    pass
                                else:
                                    if method in frozenset({'one', 'two'}):
                                        route_idx = 0
                                    elif method in frozenset({'BASE'}):
                                        route_idx = 1
                                    else:
                                        raise NoMethod
                                    # Return 1.1.1.1
                                    return router.dynamic_routes[('path', 'to', 'different', '<__dynamic__:str>')][route_idx], basket

                    # node=1.1.2 // part=__dynamic__:int
                    if num >= 3:  # CHECK 1
                        try:
                            basket['__matches__'][2] = int(parts[2])
                        except ValueError:
                            pass
                        else:
                            if num == 3:  # CHECK 5
                                # Return 1.1.2
                                return router.dynamic_routes[('path', 'to', '<__dynamic__:int>')][0], basket

                    # node=1.1.3 // part=__dynamic__:str
                    if num >= 3:  # CHECK 1
                        try:
                            basket['__matches__'][2] = str(parts[2])
                        except ValueError:
                            pass
                        else:
                            if num == 3:  # CHECK 5
                                # Return 1.1.3
                                return router.dynamic_routes[('path', 'to', '<__dynamic__:str>')][0], basket
    raise NotFound

Special cases

The above example only shows routes that have a dynamic path segment in them (example: <foo>). But, there are other use cases that are covered differently:

  1. fully static paths - These are paths with no parameters (example: /user/login). These are basically matched against a key/value store.
  2. regex paths - If a route as a single regular expression match, then the whole route will be matched via regex. In general, this happens inline not too dissimilar than what we see in the above example.
  3. special regex paths - The router comes with a special path type (example: <foo:path>) that can match on an expanded delimiter. This is also true for any regex that uses the path delimiter in it. These cannot be matched in the normal course since they are of unknown length.

What's next?

The current plan is for this code to live outside of the main project, and be merged into sanic-org/sanic for the Sanic 21.9 release.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sanic-routing-0.7.1.tar.gz (22.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sanic_routing-0.7.1-py3-none-any.whl (23.3 kB view details)

Uploaded Python 3

File details

Details for the file sanic-routing-0.7.1.tar.gz.

File metadata

  • Download URL: sanic-routing-0.7.1.tar.gz
  • Upload date:
  • Size: 22.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.4.1 importlib_metadata/4.6.0 pkginfo/1.7.0 requests/2.25.1 requests-toolbelt/0.9.1 tqdm/4.61.1 CPython/3.9.5

File hashes

Hashes for sanic-routing-0.7.1.tar.gz
Algorithm Hash digest
SHA256 35b09a12aceecc4e295bddb0ed39cff90363dd1504f9831c6b0b01f6f026635d
MD5 2318e40e7ede22b02f89ccd65cc75f45
BLAKE2b-256 d066eeb70d6289c728b3240265e7f0bb1315b0796c0aa99c14327affe2415284

See more details on using hashes here.

File details

Details for the file sanic_routing-0.7.1-py3-none-any.whl.

File metadata

  • Download URL: sanic_routing-0.7.1-py3-none-any.whl
  • Upload date:
  • Size: 23.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.4.1 importlib_metadata/4.6.0 pkginfo/1.7.0 requests/2.25.1 requests-toolbelt/0.9.1 tqdm/4.61.1 CPython/3.9.5

File hashes

Hashes for sanic_routing-0.7.1-py3-none-any.whl
Algorithm Hash digest
SHA256 0c08f5f65a55fdca259d9149d89c39fd2377a651b408065fd643bdabdb6ba293
MD5 055da36045641aeda4254df4d1422988
BLAKE2b-256 3efbff33c33d20b3b3c1a3b955acef5cc5ce6779e4c0c1f5836b297b0a6e8601

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page