Skip to content

Routing

Loaders

Routing is a big part of any web library, and there are many ways to do it. View does it's best to support as many methods as possible to give you a well-rounded approach to routing. In view, your choice of routing is called the loader/loader strategy, and there are four of them:

  • manual
  • simple
  • filesystem
  • patterns

Manually Routing

If you're used to Python libraries like Flask or FastAPI, then you're probably already familiar with manual routing. Manual routing is considered to be letting the user do all of the loading themself, and not do any automatic import or load mechanics. There are two ways to do manual routing, directly calling on your App being the most robust. Here's an example:

from view import new_app

app = new_app()

@app.get("/")
def index():
    return "Hello, view.py!"

app.run()

This type of function is called a direct router, and is what's recommended for small view.py projects. However, if you're more accustomed to JavaScript libraries, using the standard routers may be a good fit. When using manual routing, a standard router must be registered via a call to App.load.

view.app.App.load(routes: list[Route] | None = None) -> None

Load the app. This is automatically called most of the time and should only be called manually during manual loading.

Parameters:

Name Type Description Default
routes list[Route] | None

Routes to load into the app.

None
Source code in src/view/app.py
def load(self, routes: list[Route] | None = None) -> None:
    """Load the app. This is automatically called most of the time and should only be called manually during manual loading.

    Args:
        routes: Routes to load into the app.
    """
    if self.loaded:
        if routes:
            finalize(routes, self)
        Internal.warning("load called twice")
        return

    if routes and (self.config.app.loader != "manual"):
        warnings.warn(_ROUTES_WARN_MSG)

    if self.config.app.loader == "filesystem":
        load_fs(self, self.config.app.loader_path)
    elif self.config.app.loader == "simple":
        load_simple(self, self.config.app.loader_path)
    elif self.config.app.loader == "patterns":
        load_patterns(self, self.config.app.loader_path)
    else:
        finalize([*(routes or ()), *self._manual_routes], self)

    self.loaded = True

    from .routing import RouteInput

    for r in self.loaded_routes:
        if not r.path:
            continue

        body = {}
        query = {}

        for i in r.inputs:
            if not isinstance(i, RouteInput):
                continue

            target = body if i.is_body else query
            target[i.name] = InputDoc(
                i.doc or "No description provided.", i.tp, i.default
            )

        if r.method:
            self._docs[(r.method.name, r.path)] = RouteDoc(
                r.doc or "No description provided.", body, query
            )
        else:
            self._docs[
                (
                    tuple([i.name for i in r.method_list])
                    if r.method_list
                    else (
                        "GET",
                        "POST",
                        "PUT",
                        "PATCH",
                        "DELETE",
                        "OPTIONS",
                    ),
                    r.path,
                )
            ] = RouteDoc(r.doc or "No description provided.", body, query)

Standard and Direct Routers

Standard routers and direct routers have the exact same API (i.e. they are called the same way). The only difference is that direct routers automatically register a route onto the app, while standard routes do not. Direct routers tend to be used in small projects under manual loading, but standard routers are used in larger applications with one of the other loaders.

Here are all the routers (standard on left, direct on right):

  • view.get and App.get
  • view.post and App.post
  • view.put and App.put
  • view.patch and App.patch
  • view.delete and App.delete
  • view.options and App.options
from view import new_app, get

app = new_app()

@get("/")
def index():
    return "Hello, view.py!"

app.load([get])
app.run()

This method may be a bit more versatile if you plan on writing a larger project using manual routing, as you can import your routes from other files, but if that's the case it's recommended that you use one of the other loaders.

Tip

Use the direct variation if the App is already available, and use the standard version otherwise.

view.routing.get(path_or_route: str | None | RouteOrCallable = None, doc: str | None = None, *, cache_rate: int = -1) -> Path

Add a GET route.

Parameters:

Name Type Description Default
path_or_route str | None | RouteOrCallable

The path to this route, or the route itself.

None
doc str | None

The description of the route to be used in documentation.

None
cache_rate int

Reload the cache for this route every x number of requests. -1 means to never cache.

-1
Example
from view import get

@get("/")
async def index():
    return "Hello, view.py!"
Source code in src/view/routing.py
def get(
    path_or_route: str | None | RouteOrCallable = None,
    doc: str | None = None,
    *,
    cache_rate: int = -1,
) -> Path:
    """Add a GET route.

    Args:
        path_or_route: The path to this route, or the route itself.
        doc: The description of the route to be used in documentation.
        cache_rate: Reload the cache for this route every x number of requests. `-1` means to never cache.

    Example:
        ```py
        from view import get

        @get("/")
        async def index():
            return "Hello, view.py!"
        ```
    """
    return _method_wrapper(path_or_route, doc, Method.GET, cache_rate)

view.routing.post(path_or_route: str | None | RouteOrCallable = None, doc: str | None = None, *, cache_rate: int = -1) -> Path

Add a POST route.

Parameters:

Name Type Description Default
path_or_route str | None | RouteOrCallable

The path to this route, or the route itself.

None
doc str | None

The description of the route to be used in documentation.

None
cache_rate int

Reload the cache for this route every x number of requests. -1 means to never cache.

-1
Example
from view import post

@post("/")
async def index():
    return "Hello, view.py!"
Source code in src/view/routing.py
def post(
    path_or_route: str | None | RouteOrCallable = None,
    doc: str | None = None,
    *,
    cache_rate: int = -1,
) -> Path:
    """Add a POST route.

    Args:
        path_or_route: The path to this route, or the route itself.
        doc: The description of the route to be used in documentation.
        cache_rate: Reload the cache for this route every x number of requests. `-1` means to never cache.

    Example:
        ```py
        from view import post

        @post("/")
        async def index():
            return "Hello, view.py!"
        ```
    """
    return _method_wrapper(path_or_route, doc, Method.POST, cache_rate)

view.routing.put(path_or_route: str | None | RouteOrCallable = None, doc: str | None = None, *, cache_rate: int = -1) -> Path

Add a PUT route.

Parameters:

Name Type Description Default
path_or_route str | None | RouteOrCallable

The path to this route, or the route itself.

None
doc str | None

The description of the route to be used in documentation.

None
cache_rate int

Reload the cache for this route every x number of requests. -1 means to never cache.

-1
Example
from view import put

@put("/")
async def index():
    return "Hello, view.py!"
Source code in src/view/routing.py
def put(
    path_or_route: str | None | RouteOrCallable = None,
    doc: str | None = None,
    *,
    cache_rate: int = -1,
) -> Path:
    """Add a PUT route.

    Args:
        path_or_route: The path to this route, or the route itself.
        doc: The description of the route to be used in documentation.
        cache_rate: Reload the cache for this route every x number of requests. `-1` means to never cache.

    Example:
        ```py
        from view import put

        @put("/")
        async def index():
            return "Hello, view.py!"
        ```
    """
    return _method_wrapper(path_or_route, doc, Method.PUT, cache_rate)

view.routing.patch(path_or_route: str | None | RouteOrCallable = None, doc: str | None = None, *, cache_rate: int = -1) -> Path

Add a PATCH route.

Parameters:

Name Type Description Default
path_or_route str | None | RouteOrCallable

The path to this route, or the route itself.

None
doc str | None

The description of the route to be used in documentation.

None
cache_rate int

Reload the cache for this route every x number of requests. -1 means to never cache.

-1
Example
from view import patch

@patch("/")
async def index():
    return "Hello, view.py!"
Source code in src/view/routing.py
def patch(
    path_or_route: str | None | RouteOrCallable = None,
    doc: str | None = None,
    *,
    cache_rate: int = -1,
) -> Path:
    """Add a PATCH route.

    Args:
        path_or_route: The path to this route, or the route itself.
        doc: The description of the route to be used in documentation.
        cache_rate: Reload the cache for this route every x number of requests. `-1` means to never cache.

    Example:
        ```py
        from view import patch

        @patch("/")
        async def index():
            return "Hello, view.py!"
        ```
    """
    return _method_wrapper(path_or_route, doc, Method.PATCH, cache_rate)

view.routing.delete(path_or_route: str | None | RouteOrCallable = None, doc: str | None = None, *, cache_rate: int = -1) -> Path

Add a DELETE route.

Parameters:

Name Type Description Default
path_or_route str | None | RouteOrCallable

The path to this route, or the route itself.

None
doc str | None

The description of the route to be used in documentation.

None
cache_rate int

Reload the cache for this route every x number of requests. -1 means to never cache.

-1
Example
from view import delete

@delete("/")
async def index():
    return "Hello, view.py!"
Source code in src/view/routing.py
def delete(
    path_or_route: str | None | RouteOrCallable = None,
    doc: str | None = None,
    *,
    cache_rate: int = -1,
) -> Path:
    """Add a DELETE route.

    Args:
        path_or_route: The path to this route, or the route itself.
        doc: The description of the route to be used in documentation.
        cache_rate: Reload the cache for this route every x number of requests. `-1` means to never cache.

    Example:
        ```py
        from view import delete

        @delete("/")
        async def index():
            return "Hello, view.py!"
        ```
    """
    return _method_wrapper(path_or_route, doc, Method.DELETE, cache_rate)

view.routing.options(path_or_route: str | None | RouteOrCallable = None, doc: str | None = None, *, cache_rate: int = -1) -> Path

Add an OPTIONS route.

Parameters:

Name Type Description Default
path_or_route str | None | RouteOrCallable

The path to this route, or the route itself.

None
doc str | None

The description of the route to be used in documentation.

None
cache_rate int

Reload the cache for this route every x number of requests. -1 means to never cache.

-1
Example
from view import options

@options("/")
async def index():
    return "Hello, view.py!"
Source code in src/view/routing.py
def options(
    path_or_route: str | None | RouteOrCallable = None,
    doc: str | None = None,
    *,
    cache_rate: int = -1,
) -> Path:
    """Add an OPTIONS route.

    Args:
        path_or_route: The path to this route, or the route itself.
        doc: The description of the route to be used in documentation.
        cache_rate: Reload the cache for this route every x number of requests. `-1` means to never cache.

    Example:
        ```py
        from view import options

        @options("/")
        async def index():
            return "Hello, view.py!"
        ```
    """
    return _method_wrapper(path_or_route, doc, Method.OPTIONS, cache_rate)

Methodless Routing

So far, only routers that allow a single method are allowed. If you're familiar with the Flask framework, you've likely tried the route method that lets any a route be accessed with any method. View supports the same thing, via the route router function, and the App.route direct variation.

For example:

from view import new_app, route

app = new_app()

@route("/")
async def index():
    return "this can be accessed with any method!"

app.load([index])
app.run()

You can specify certain methods via the methods parameter:

from view import new_app

app = new_app()

@app.route("/", methods=("GET", "POST"))  # using the direct variation
async def index():
    return "this can be accessed with only GET and POST"

app.run()

view.routing.route(path_or_route: str | None | RouteOrCallable = None, doc: str | None = None, *, cache_rate: int = -1, methods: Iterable[StrMethod] | None = None) -> Path

Add a route that can be called with any method (or only specific methods).

Parameters:

Name Type Description Default
path_or_route str | None | RouteOrCallable

The path to this route, or the route itself.

None
doc str | None

The description of the route to be used in documentation.

None
cache_rate int

Reload the cache for this route every x number of requests. -1 means to never cache.

-1
methods Iterable[StrMethod] | None

Methods that can be used to access this route. If this is None, then all methods are allowed.

None
Example
from view import route

@route("/", methods=("GET", "POST"))
async def index():
    return "Hello, view.py!"
Source code in src/view/routing.py
def route(
    path_or_route: str | None | RouteOrCallable = None,
    doc: str | None = None,
    *,
    cache_rate: int = -1,
    methods: Iterable[StrMethod] | None = None
) -> Path:
    """Add a route that can be called with any method (or only specific methods).

    Args:
        path_or_route: The path to this route, or the route itself.
        doc: The description of the route to be used in documentation.
        cache_rate: Reload the cache for this route every x number of requests. `-1` means to never cache.
        methods: Methods that can be used to access this route. If this is `None`, then all methods are allowed.

    Example:
        ```py
        from view import route

        @route("/", methods=("GET", "POST"))
        async def index():
            return "Hello, view.py!"
        ```
    """
    return _method_wrapper(
        path_or_route,
        doc,
        None,
        cache_rate,
        method_list=[_STR_METHOD_MAPPING[i] for i in methods] if methods else None
    )

Simple Routing

Simple routing is similar to manual routing, but you tend to not use direct routers and don't have any call to load(). In your routes directory (routes/ by default, loader_path setting), your routes will be held in any number of files. Simple loading is recursive, so you may also use folders. View will automatically extract any route objects created in these files.

# routes/foo.py
from view import get

@get("/foo")
def index():
    return "foo"

@get("/bar")
def bar():
    return "bar"

/foo and /bar will be loaded properly, no extra call to App.load is required. In fact, you don't even have to import these in your app file. This is the recommended loader for larger view.py projects.

URL Pattern Routing

If you have ever used Django, you already know how URL pattern routing works. Instead of defining your routes all over the place, all routes are defined and imported into one central file. Traditionally, this file is called urls.py, but you can play around with the name via the loader_path configuration option.

view.patterns.path(target: str, path_or_function: str | ViewRoute | Route, *inputs: RouteInput, method: Method | StrMethod | None = _Get) -> Route

Source code in src/view/patterns.py
def path(
    target: str,
    path_or_function: str | ViewRoute | Route,
    *inputs: RouteInput,
    method: Method | StrMethod | None = _Get,
) -> Route:
    if isinstance(path_or_function, str):
        mod = run_path(path_or_function)
        route: Route | ViewRoute | None = None

        for v in mod.values():
            if isinstance(v, Route) or callable(v):
                if route:
                    raise DuplicateRouteError(
                        f"multiple routes found in {path_or_function}"
                    )

                route = v

        if not route:
            raise InvalidRouteError(f"no route in {path_or_function}")
    else:
        route = path_or_function

    if not isinstance(route, Route):
        method_enum = _get_method_enum(method)
        func = _FUNC_MAPPINGS[method_enum]
        route_obj = func(target)(route)
    else:
        if not route.method:
            route_obj = route_impl(target)(route)
            route_obj.method_list = route.method_list
        else:
            method_enum = _get_method_enum(method or route.method)
            func = _FUNC_MAPPINGS[method_enum]
            route_obj = func(target)(route)

    for i in inputs:
        i(route_obj)

    return route_obj

Pattern loading looks like this in view.py:

# something.py

def my_route(hello: str):
    return f"{hello}, world!"
from view import path, query
from something import my_route

patterns = (
    path("/", my_route, query("hello")),  # this is a route input, you'll learn about this later
    path("/another/thing", "/this/can/be/a/path/to/file.py")
)

In the above example, we defined two routes via exporting a tuple of Route objects (generated by path). The name patterns was used as the variable name, but it may be any of the following:

  • PATTERNS
  • patterns
  • URLPATTERNS
  • URL_PATTERNS
  • urlpatterns
  • url_patterns

Tip

Traditionally, Python constants are denoted via using the SCREAMING_SNAKE_CASE naming convention. To follow Python convention, use PATTERNS or URL_PATTERNS when using the patterns loader.

Filesystem Routing

Finally, if you're familiar with JavaScript frameworks like NextJS, you're likely already familiar with filesystem routing. If that's the case, this may be the proper loader for you. The filesystem loader works by recursively searching your loader_path (again, routes/ by default) and assigning each found file to a route. You do not have to pass an argument for the path when using filesystem routing.

Filesystem routing comes with a few quirks.

  • There should only be one route per file.
  • The upper directory structure is ignored, so /home/user/app/routes/foo.py, the assigned route would be /foo.
  • If a file is named index.py, the route is not named index, but instead the parent (e.g. foo/hello/index.py would be assigned to foo/hello).
  • If a file is prefixed with _ (e.g. _hello.py), then it will be skipped entirely and not loaded. Files like this should be used for utilities and such.

Here's an example of this in action:

# routes/_util.py

def do_something():
    ...
# routes/index.py
from view import get
from _util import do_something

@get()
def index():
    do_something()
    return "Hello, view.py!"

Review

In view, a loader is defined as the method of routing used. There are three loaders in view.py: manual, simple, and filesystem.

  • manual is good for small projects that are similar to Python libraries like Flask or FastAPI.
  • simple routing is the recommended loader for full-scale view.py applications
  • filesystem routing is similar to how JavaScript frameworks like NextJS handle routing.