Routing API

Last Updated: December 2022

Routing is the way a request to a URL is connected (or routed) to a function or class that is responsible for handling that request. When a request is submitted to Tethys, it matches the URL of that request against a list of registered URLs and calls the function or class that is registered with that URL. This connection between the URL and the function or class that handles it is also called a URL mapping. A function or class that is mapped to a URL endpoint is known as either a controller or a consumer (depending on the protocol used in the URL).

Beginning in Tethys 4.0, registering a URL is done by decorating its function or class with one of the Tethys routing decorators (controller or consumer). The controller decorator is used to register a URL using the HTTP protocol, while the consumer decorator is used to register a URL using the websocket protocol.

Warning

The url_maps method in the app.py is deprecated in favor of the new controller decorator approach (described below) and WILL BE REMOVED in Tethys 4.1.0. The url_maps method is temporarily available in Tethys 4.0 to allow for easier migration of apps to using the new controller decorator method. If you still wish to declare UrlMaps in the app.py, use the new register_url_maps() method and then remove the url_maps() method (see: Register URL Maps Method). Use of the url_maps method in Tethys 4.0.0 causes long warning messages to be displayed in the terminal to encourage users to migrate as soon as possible. Don't wait until 4.1.0 to move to the ``controller`` decorator!

Controller Decorator

The controller decorator is used to decorate a function or class (see Class-Based Controllers) that serves as a controller for a URL endpoint. When Tethys registers a URL mapping, in the background, it uses a UrlMap object that needs at least three things: a controller, a name, and a url. When you decorate a function with the controller decorator that provides the first element. The next two elements can either be automatically derived from the decorated function, or you can supply them explicitly. For example, if you register a controller like this:

@controller
def my_demo_controller(request):
  ...

Tethys will create a UrlMap with the following arguments:

UrlMap(
  name='my_demo_controller',
  url='my-demo-controller/',
  controller='my_first_app.controllers.my_demo_controller',
)

Note

The full URL endpoint that gets registered will include the hostname of your Tethys Portal and the root_url that is defined in your app.py file. So in the case of the UrlMap above the final endpoint would be something like:

http://my-tethys-portal.com/apps/my-first-app/my-demo-controller/

In this case Tethys just uses the name of the function as the name of the UrlMap and a modified version of the name (just replacing _ with -) for the url.

If you want to customize either the name or the url then you can provide them as key-word arguments to the controller decorator:

@controller(
  name='demo',
  url='demo/url/',
)
def my_demo_controller(request):
  ...

which will result in the following UrlMap:

UrlMap(
  name='demo',
  url='demo/url/',
  controller='my_first_app.controllers.my_demo_controller',
)

Note

The index attribute of your app class (defined in app.py) specifies the name of the URL that should serve as the index route for your app. When the URL whose name attribute matches the index is registered, the url attribute of the UrlMap is overridden to be the root_url of your app.

For example, normally the full URL endpoint for the 'demo' URL above would be:

http://my-tethys-portal.com/apps/my-first-app/demo/url/

However, if the index attribute in app.py were set to 'demo' then the url would be overridden and the endpoint would be:

http://my-tethys-portal.com/apps/my-first-app/

The controller decorator also accepts many other arguments that modify the behavior of the controller. For example the permissions_required argument lets you specify permissions that a user is required to have to access the URL. Or the app_workspace argument will pass in a reference to the app's workspace directory as an argument to the function. The full list of arguments that the controller decorator accepts are documented below.

tethys_apps.base.controller.controller(function_or_class=None, /, *, name=None, url=None, protocol='http', regex=None, _handler=None, _handler_type=None, login_required=True, redirect_field_name='next', login_url=None, app_workspace=False, user_workspace=False, ensure_oauth2_provider=None, enforce_quotas=None, permissions_required=None, permissions_use_or=False, permissions_message=None, permissions_raise_exception=False, **kwargs)

Decorator to register a function or TethysController class as a controller (by automatically registering a UrlMap for it).

Parameters:
  • name (str) -- Name of the url map. Letters and underscores only (_). Must be unique within the app. The default is the name of the function being decorated.

  • url (Union[str, list, tuple, dict, None]) -- URL pattern to map the endpoint for the controller or consumer. If a list then a separate UrlMap is generated for each URL in the list. The first URL is given name and subsequent URLS are named name _1, name _2 ... name _n. Can also be passed as dict mapping names to URL patterns. In this case the name argument is ignored.

  • protocol (str) -- 'http' for controllers or 'websocket' for consumers. Default is http.

  • regex (Union[str, list, tuple]) -- Custom regex pattern(s) for url variables. If a string is provided, it will be applied to all variables. If a list or tuple is provided, they will be applied in variable order.

  • login_required (bool) -- If user is required to be logged in to access the controller. Default is True.

  • redirect_field_name (str) -- URL query string parameter for the redirect path. Default is "next".

  • login_url (str) -- URL to send users to in order to authenticate.

  • app_workspace (bool) -- Whether to pass the app workspace as an argument to the controller.

  • user_workspace (bool) -- Whether to pass the user workspace as an argument to the controller.

  • ensure_oauth2_provider (str) -- An OAuth2 provider name to ensure is authenticated to access the controller.

  • enforce_quotas (Union[str, list, tuple, None]) -- The name(s) of quotas to enforce on the controller.

  • permissions_required (Union[str, list, tuple]) -- The name(s) of permissions that a user is required to have to access the controller.

  • permissions_use_or (bool) -- When multiple permissions are provided and this is True, use OR comparison rather than AND comparison, which is default.

  • permissions_message (str) -- Override default message that is displayed to user when permission is denied. Default message is "We're sorry, but you are not allowed to perform this operation.".

  • permissions_raise_exception (bool) -- Raise 403 error if True. Defaults to False.

Return type:

Callable

NOTE: The Handler Decorator should be used in favor of using the following arguments directly.

Parameters:
  • _handler (Union[str, Callable]) -- Dot-notation path a handler function. A handler is associated to a specific controller and contains the main logic for creating and establishing a communication between the client and the server.

  • _handler_type (str) -- Tethys supported handler type. 'bokeh' is the only handler type currently supported.

Example:

from tethys_sdk.routing import controller

@controller
def my_app_controller(request):
    ...

------------

@controller
def my_app_controller(request, url_arg):
    ...

------------

@controller(
    name='custom_name',
    url='customized-url/{url_arg}/with/arg',
)
def my_app_controller(request, url_arg):
    ...

------------

@controller
def my_app_controller(request, url_arg1, url_arg2=None, url_arg3=None):
    ...

# Note: having arguments with default values in the controller function without specifying the ``url`` argument
# in the ``controller`` decorator will result in multiple ``UrlMap`` instances being created.
# In this case the following ``UrlMap`` instances would be generated:

[
    UrlMap(
        name='my_app_controller',
        url='my-app-controller/{url_arg1}/'
    ),
    UrlMap(
        name='my_app_controller_1',
        url='my-app-controller/{url_arg1}/{url_arg2}/'
    ),
    UrlMap(
        name='my_app_controller_2',
        url='my-app-controller/{url_arg1}/{url_arg2}/{url_arg3}/'
    )
]

------------

# Alternatively, you can explicitly define the names and urls generated by passing a dict as the ``url`` argument:

@controller(
    url={
        'custom_controller_name': 'custom-controller/{url_arg1}/{url_arg2}/',
        'another_custom_name': 'another-custom-controller/{url_arg1}/{url_arg3}/'
    }
)
def my_app_controller(request, url_arg1, url_arg2=None, url_arg3=None):
    ...

------------

@controller(
    app_workspace=True,
)
def my_app_controller(request, app_workspace):
    ...

------------

@controller(
    app_workspace=True,
    user_workspace=True,
)
def my_app_controller(request, app_workspace, user_workspace, url_arg):
    # Note that if both the ``app_workspace`` and ``user_workspace`` arguments are passed to the controller
    # decorator, then "app_workspace" should precede "user_workspace" in the function argument list,
    # and both should be directly after the "request" argument.
    ...

------------

@controller(
    login_required=False,
)
def my_app_controller(request):
    # Note that ``login_required`` is True by default (recommended). However, the login requirement is automatically dropped on all controllers when ``OPEN_PORTAL_MODE`` is enabled on the portal."
    ...

------------

@controller(
    ensure_oauth2_provider='Google',
)
def my_app_controller(request):
    ...

------------

@controller(
    permissions_required=['create_projects', 'delete_projects'],
    permissions_use_or=True,
)
def my_app_controller(request):
    ...

------------

@controller(
    enforce_quotas='my_quota',
)
def my_app_controller(request):
    ...

------------

@controller(
    enforce_quotas=['my_quota1', 'my_quota2'],
)
def my_app_controller(request):
    ...

------------

# The ``controller`` decorator can also be used to decorate ``TethysController`` subclasses.

from tethys_sdk.routing import TethysController

@controller
class MyControllerClass(TethysController):
    ...

------------

# Note that when the ``controller`` decorator is applied to a class it applies to all the HTTP methods that are defined on that class:

@controller(
    user_workspace=True,
)
class MyControllerClass(TethysController):
    def get(self, request, user_workspace, url_arg):
        ...

    def post(self, request, user_workspace, url_arg):
        ...

Websockets

Tethys Platform supports WebSocket connections using Django Channels. The Websocket protocol provides a persistent connection between the client and the server. In contrast to the traditional HTTP protocol, the websocket protocol allows for bidirectional communication between the client and the server (i.e. the server can trigger a response without the client sending a request). Django Channels uses Consumers to structure code and handle client/server communication in a similar way Controllers are used with the HTTP protocol.

Note

For more information about Django Channels and Consumers visit the Django Channels docummentation.

Note

For more information on establishing a WebSocket connection see the JavaScript WebSocket API. Alternatively, other existing JavaScript or Python WebSocket clients can be used.

Tip

To create a URL mapping using the WebSocket protocol see the Consumer Decorator.

Tip

For an example demonstrating all the necessary components to integrating websockets into your app see WebSockets Concepts.

Consumer Decorator

The consumer decorator functions largely the same way as the controller decorator except that it is used to decorate a consumer class, which must be a subclass of either channels.consumer.AsyncConsumer or channels.consumer.SyncConsumer (see the Channels Consumers Documentation). Also, when the consumer decorator is used it will register a URL mapping with the websocket protocol.

The consumer decorator is somewhat more simple than the controller decorator. It's usage is documented below.

tethys_apps.base.controller.consumer(function_or_class=None, /, *, name=None, url=None, regex=None, login_required=True, permissions_required=None, permissions_use_or=False)

Decorator to register a Consumer class as routed consumer endpoint (by automatically registering a UrlMap for it).

Parameters:
  • name (str) -- Name of the url map. Letters and underscores only (_). Must be unique within the app. The default is the name of the class being decorated.

  • url (str) -- URL pattern to map the endpoint for the consumer. If a list then a seperate UrlMap is generated for each URL in the list. The first URL is given name and subsequent URLS are named name`_1, `name`_2 ... `name`_n. Can also be passed as dict mapping names to URL patterns. In this case the `name argument is ignored.

  • regex (Union[str, list, tuple]) -- Custom regex pattern(s) for url variables. If a string is provided, it will be applied to all variables. If a list or tuple is provided, they will be applied in variable order.

Return type:

Callable

Example:

from tethys_sdk.routing import consumer

from channels.generic.websocket import AsyncWebsocketConsumer


@consumer
class MyConsumer(AsyncWebsocketConsumer):
    pass

------------

@consumer(
    name='custom_name',
    url='customized/url',
)
class MyConsumer(AsyncWebsocketConsumer):

    def connect():
        pass

------------

@consumer(
    name='custom_name',
    url='customized/url',
    permissions_required='permission',
    login_required=True
)
class MyConsumer(AsyncWebsocketConsumer):

    def authorized_connect():
        pass

Bokeh Integration

Bokeh Integration in Tethys takes advantage of Websockets and Django Channels to leverage Bokeh's flexible architecture. In particular, the ability to sync model objects to the client allows for a responsive user interface that can receive updates from the server using Python. This is referred to as Bokeh Server in the Bokeh Documentation.

Note

Interactive Bokeh visualization tools can be entirely created using only Python with the help of Bokeh Server. However, this usually requires the use of an additional server (Tornado). One of the alternatives to Tornado is using Django Channels, which is already supported with Tethys. Therefore, interactive Bokeh models along with the all the advantages of using Bokeh Server can be leveraged in Tethys without the need of an additional server.

Even though Bokeh uses Websockets, routing with Bokeh endpoints is handled differently from other Websockets that would normally be handled by a Consumer class and use the Consumer Decorator. In contrast, Bokeh endpoints use a handler function that contains the main logic needed for a Bokeh model to be displayed. It contains the model or group of models as well as the callback functions that will help link them to the client. A handler function should be registered with the Handler Decorator.

Handlers are added to the Bokeh Document, the smallest serialization unit in Bokeh Server. This same Document is retrieved and added to the template variables in a controller function that is linked to the Handler function using Bokeh's server_document function. The controller function is created and registered automatically with the Handler Decorator. However, you can manually create a controller function if custom logic is needed. In this case the controller function should not be decorated, but rather passed in as an argument to the Handler Decorator.

A Bokeh Document comes with a Bokeh Request. This request contains most of the common attributes of a normal HTTPRequest, and can be easily converted to an HTTPRequest using the with_request argument in the Handler Decorator. Similarly, the with_workspaces argument can be used to add user_workspace and app_workspace to the Bokeh Document. This latter argument will also convert the Bokeh Request of the Document to an HTTPRequest, meaning it will do the same thing as the with_request argument in addition to adding workspaces.

Important

To use the handler decorator you will need the bokeh and bokeh-django packages which may not be installed by default. They can be installed with:

conda install -c conda-forge -c erdc/label/dev bokeh bokeh-django

Tip

For more information regarding Bokeh Server and available models visit the Bokeh Server Documentation and the Bokeh model widgets reference guide.

Handler Decorator

tethys_apps.base.controller.handler(function=None, /, *, controller=None, template=None, app_package=None, handler_type='bokeh', with_request=False, with_workspaces=False, **kwargs)

Decorator to register a handler function and connect it with a controller function (by automatically registering a UrlMap for it).

Parameters:
  • controller (Union[str, Callable[[HttpRequest, ...], None]]) -- reference to controller function or string with dot-notation path to controller function. This is only required if custom logic is needed in the controller. Otherwise use template or app_package.

  • template (str) -- The namespaced template file file (e.g. my_app/my_template.html). Use as an alternative to controller to automatically create a controller function that renders provided template.

  • app_package (str) -- The app_package name from the app.py file. Used as an alternative to controller and template to automatically create a controller and template that extends the app's "base.html" template. If none of controller, template and app_package are provided then a default controller and template will be created.

  • handler_type (str) -- Tethys supported handler type. 'bokeh' is the only handler type currently supported.

  • with_request (bool) -- If True then the HTTPRequest object will be added to the Bokeh Document.

  • with_workspaces (bool) -- if True then the app_workspace and user_workspace will be added to the Bokeh Document.

Example:

from tethys_sdk.routing import handler

@handler
def my_app_handler(document):
    ...

------------

@handler(
    name='home',
    app_package='my_app',
)
def my_app_handler(document):
    ...

------------

@handler(
    name='home',
    template='my_app/home.html',
)
def my_app_handler(document):
    ...

------------

from bokeh.embed import server_document
from .app import App

def home_controller(request):
    # custom logic here
    custom_value = ...

    script = server_document(request.build_absolute_uri())
    context = {
        'script': script,
        'custom_key': custom_value,
    }
    return App.render(request, 'home.html', context)

@handler(
    name='home',
    controller=home_controller,
)
def my_app_handler(document):
    ...

------------

@handler(
    name='home',
    controller='my_app.controllers.my_app_controller',
)
def my_app_handler(document):
    ...

------------

@handler(
    name='home',
    controller='tethysext.my_extension.controllers.my_controller',
)
def my_app_handler(document):
    ...

------------

@handler(
    with_request=True
)
def my_app_handler(document):
    # attribute available when using "with_request" argument
    request = document.request
    ...

------------

@handler(
    with_workspaces=True
)
def my_app_handler(document):
    # attributes available when using "with_workspaces" argument
    request = document.request
    user_workspace = document.user_workspace
    app_workspace = document.app_workspace
    ...

------------

def job_view(request, job_id):
    # Do something with URL variable ``job_id``

@handler(
    name='job_view',
    url='job-view/{job_id}',
    login_required=True,
    ensure_oauth2_provider=app.PROVIDER_NAME
)
def job_view_handler(document):
    ...

------------

Tip

For a more in-depth example of how to use Bokeh with Tethys see the Bokeh Integration Concepts.

Search Path

In a Tethys app the controllers are usually defined in the controllers.py module. If your app includes consumers then they should be defined in a consumers.py module. Tethys will automatically search both the controllers.py and consumers.py modules to find any functions or classes that have been decorated with the either controller or the consumer decorators and register them. If you have many controllers or consumers and want to organize them in multiple modules then you can convert the controllers.py or the consumers.py modules into packages with the same name (a directory named either controllers or consumers with an __init__.py and other Python modules in it).

For existing apps with controllers located in modules with different names than these defaults, it is recommended to move the modules into a package named either controllers or consumers as described above. However, you may also use the controller_modules property of the app class to define addtiional controller search locations. For example:

class MyFirstApp(TethysAppBase):
    ...
    controller_modules = [
        'custom_controllers',  # For a module named custom_controller.py in the same directory as app.py
        'rest',  # For a package named "rest" in the same directory as app.py containing modules with controllers
    ]

Class-Based Controllers

class tethys_apps.base.controller.TethysController(**kwargs)
classmethod as_controller(**kwargs)

Thin veneer around the as_view method to make interface more consistent with Tethys terminology.

URL Maps

Under the hood, Tethys creates a UrlMap object that maps the URL endpoint to the controller or consumer that will handle the request. When using the controller or consumer decorators the UrlMap objects are created automatically. However, if you have a need to manually modify the list of registered UrlMap objects for your app then you can do so by overriding the register_url_maps method in the app.py file.

Tethys usually manages url_maps from the app.py file of each individual app using a UrlMap constructor. This constructor normally accepts a name, a url, and a controller. However, there are other parameters such as protocol, regex, handler, and handler_type. This section provides information on how to use the url_maps API.

URL Maps Contructor

class tethys_apps.base.url_map.UrlMapBase(name, url, controller, protocol='http', regex=None, handler=None, handler_type=None)

Abstract URL base class for Tethys app controllers and consumers

__init__(name, url, controller, protocol='http', regex=None, handler=None, handler_type=None)

Constructor

Parameters:
  • name (str) -- Name of the url map. Letters and underscores only (_). Must be unique within the app.

  • url (str) -- Url pattern to map the endpoint for the controller or consumer.

  • controller (str) -- Dot-notation path to the controller function or consumer class.

  • protocol (str) -- 'http' for controllers or 'websocket' for consumers. Default is http.

  • regex (str or iterable, optional) -- Custom regex pattern(s) for url variables. If a string is provided, it will be applied to all variables. If a list or tuple is provided, they will be applied in variable order.

  • handler (str) -- Dot-notation path a handler function. A handler is associated to a specific controller and contains the main logic for creating and establishing a communication between the client and the server.

  • handler_type (str) -- Tethys supported handler type. 'bokeh' is the only handler type currently supported.

Register URL Maps Method

The register_url_maps method is tightly related to the App Base Class API.

TethysBase.register_url_maps(set_index=True)

Only override this method to manually define or extend the URL Maps for your app. Your UrlMap objects must be created from a UrlMap class that is bound to the root_url of your app. Use the url_map_maker() function to create the bound UrlMap class. Starting in Tethys 3.0, the WebSocket protocol is supported along with the HTTP protocol. To create a WebSocket UrlMap, follow the same pattern used for the HTTP protocol. In addition, provide a Consumer path in the controllers parameter as well as a WebSocket string value for the new protocol parameter for the WebSocket UrlMap. Alternatively, Bokeh Server can also be integrated into Tethys using Django Channels and Websockets. Tethys will automatically set these up for you if a handler and handler_type parameters are provided as part of the UrlMap.

Parameters:

set_index -- If False then the index controller will not be configured/validated automatically, and it is left to the user to ensure that a controller name that matches self.index is configured.

Returns:

A list or tuple of UrlMap objects.

Return type:

iterable

Example:

from tethys_sdk.routing import url_map_maker

class MyFirstApp(TethysAppBase):

    def register_url_maps(self):
        """
        Example register_url_maps method.
        """

        root_url = self.root_url
        UrlMap = url_map_maker(root_url)

        url_maps = super().register_url_maps(set_index=False)
        url_maps.extend((
            UrlMap(
                name='home',
                url=root_url,
                controller='my_first_app.controllers.home',
            ),
            UrlMap(
                name='home_ws',
                url='my-first-ws',
                controller='my_first_app.controllers.HomeConsumer',
                protocol='websocket'
            ),
            UrlMap(
                name='bokeh_handler',
                url='my-first-app/bokeh-example',
                controller='my_first_app.controllers.bokeh_example',
                handler='my_first_app.controllers.bokeh_example_handler',
                handler_type='bokeh'
            ),
        ))

        return url_maps

Register Controllers

tethys_apps.base.controller.register_controllers(root_url, modules, index=None, catch_all='')

Registers UrlMap entries for all controllers that have been decorated with the @controller decorator.

Parameters:
  • root_url (str) -- The root-url to be used for all registered controllers found in module.

  • modules (Union[str, list, tuple]) -- The dot-notation path(s) to the module to search for controllers to register.

  • index (str) -- The index url name. If passed then the URL with <url_name> will be overridden with the root_url.

  • catch_all (str) -- Add a catch-all endpoint for the app (e.g. /my-first-app/.*/) that maps to the named controller.

Return type:

list

Returns:

A list of UrlMap objects.

Example:

from tethys_sdk.routing import register_controllers

# app = TethysAppBase instance

register_controllers(
    root_url=app.root_url,
    module=[f'{app.package_namespace}.{app.package}.controllers'],
    index=app.index,
)