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, app_media=False, user_media=False, app_resources=False, app_public=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.app_media (
bool
) -- Whether to pass the app media directory as an argument to the controller.user_media (
bool
) -- Whether to pass the user media directory as an argument to the controller.app_resources (
bool
) -- Whether to pass the app resources directory as an argument to the controller.app_public (
bool
) -- Whether to pass the app public directory 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, app_media=True, user_media=True, app_resources=True, app_public=True, ) def my_app_controller(request, app_workspace, user_workspace, app_media, user_media, app_public, app_resources, url_arg): # Note that the path arguments are passed in as key-word arguments, so the order does not matter, # but the name of the argument must match what is shown here (i.e. the same as the argument name # that is passed to the controller decorator). ... ------------ @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(consumer_class=None, /, *, name=None, url=None, regex=None, with_paths=False, 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.with_paths (
bool
) -- If True then initialize the following path properties on the consumer class when it is called (app_workspace, user_workspace, app_media, user_media, app_public, app_resources). Default is Falselogin_required (
bool
) -- If user is required to be logged in to access the consumer endpoint. Default is True.permissions_required (
Union
[str
,list
,tuple
]) -- The name(s) of permissions that a user is required to have to access the consumer endpoint. Default is Nonepermissions_use_or (
bool
) -- When multiple permissions are provided and this is True, use OR comparison rather than AND comparison, which is default. Default is False
- 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): pass ------------ @consumer( name='custom_name', url='customized/url', permissions_required='permission', login_required=True ) class MyConsumer(AsyncWebsocketConsumer): async def authorized_connect(self): ... ------------ @consumer( with_paths=True, login_required=True, ) class MyConsumer(AsyncWebsocketConsumer): async def authorized_connect(self): self.app_workspace self.user_workspace self.app_media self.user_media self.app_public self.app_resources
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. Note that the Handler Decorator supports both synchronous and asynchronous functions.
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, with_paths=False, **kwargs)
Decorator to register a handler function and connect it with a controller function (by automatically registering a UrlMap for it). Handler function may be synchronous or asynchronous.
- 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 theHTTPRequest
object will be added to the Bokeh Document.with_workspaces (DEPRECATED) -- if True then the app_workspace and user_workspace will be added to the Bokeh Document.
with_paths (
bool
) -- if True then the app_media_path, user_media_path, and app_resources_path will be added to the Bokeh Document.
Example:
from tethys_sdk.routing import handler @handler async def my_app_handler(document): ... ------------ @handler def my_sync_app_handler(document): ... ------------ @handler( name='home', app_package='my_app', ) async 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_paths=True ) async def my_app_handler(document): # attributes available when using "with_paths" argument request = document.request user_workspace = document.user_workspace user_media = document.user_media app_workspace = document.app_workspace app_media = document.app_media app_public = document.app_public app_resources = document.app_resources ... ------------ 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 App(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, title=None, index=None)
Abstract URL base class for Tethys app controllers and consumers
- __init__(name, url, controller, protocol='http', regex=None, handler=None, handler_type=None, title=None, index=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.
title (str) -- The title to be used both in navigation and in the browser tab.
index (int) -- Used to determine the render order of nav items in navigation. Defaults to the unpredictable processing order of decorated functions. Set to -1 to remove from navigation.
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 aUrlMap
class that is bound to theroot_url
of your app. Use theurl_map_maker()
function to create the boundUrlMap
class. Starting in Tethys 3.0, theWebSocket
protocol is supported along with theHTTP
protocol. To create aWebSocket UrlMap
, follow the same pattern used for theHTTP
protocol. In addition, provide aConsumer
path in the controllers parameter as well as aWebSocket
string value for the new protocol parameter for theWebSocket UrlMap
. Alternatively, Bokeh Server can also be integrated into Tethys usingDjango Channels
andWebsockets
. Tethys will automatically set these up for you if ahandler
andhandler_type
parameters are provided as part of theUrlMap
.- 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 inmodule
.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 theroot_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, )