Skip to content

Commit cba4e5d

Browse files
committed
v0.1.0
1 parent 6b6aca2 commit cba4e5d

File tree

14 files changed

+391
-107
lines changed

14 files changed

+391
-107
lines changed

README.md

+223-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,224 @@
11
# bokeh-django
2-
Support for running Bokeh on Django
2+
Support for running Bokeh apps with Django
3+
4+
## Introduction
5+
Both Bokeh and Django are web frameworks that can be used independently to build and host web applications. They each have their own strengths and the purpose of the ``bokeh_django`` package is to integrate these two frameworks so their strengths can be used together.
6+
7+
## Installation
8+
9+
```commandline
10+
pip install bokeh-django
11+
```
12+
13+
## Configuration
14+
15+
This documentation assumes that you have already started a [Django project](https://docs.djangoproject.com/en/4.2/intro/tutorial01/).
16+
17+
`bokeh-django` enables you to define routes (URLs) in your Django project that will map to Bokeh applications or embed Bokeh applications into a template rendered by Django. However, before defining the routes there are several configuration steps that need to be completed first.
18+
19+
1. Configure ``INSTALLED_APPS``:
20+
21+
In the ``settings.py`` file ensure that both ``channels`` and ``bokeh_django`` are added to the ``INSTALLED_APPS`` list:
22+
23+
```python
24+
INSTALLED_APPS = [
25+
...,
26+
'channels',
27+
'bokeh_django',
28+
]
29+
```
30+
31+
2. Set Up an ASGI Application:
32+
33+
By default, the Django project will be configured to use a WSGI application, but the ``startproject`` command should have also created an ``asgi.py`` file.
34+
35+
In ``settings.py`` change the ``WSGI_APPLICATION`` setting to ``ASGI_APPLICATION`` and modify the path accordingly. It should look something like this:
36+
37+
```python
38+
ASGI_APPLICATION = 'mysite.asgi.application'
39+
```
40+
41+
Next, modify the contents of the ``asgi.py`` file to get the URL patterns from the ``bokeh_django`` app config. Something similar to this will work:
42+
43+
```python
44+
from channels.auth import AuthMiddlewareStack
45+
from channels.routing import ProtocolTypeRouter, URLRouter
46+
from django.apps import apps
47+
48+
bokeh_app_config = apps.get_app_config('bokeh_django')
49+
50+
application = ProtocolTypeRouter({
51+
'websocket': AuthMiddlewareStack(URLRouter(bokeh_app_config.routes.get_websocket_urlpatterns())),
52+
'http': AuthMiddlewareStack(URLRouter(bokeh_app_config.routes.get_http_urlpatterns())),
53+
})
54+
```
55+
56+
3. Configure Static Files:
57+
58+
Both Bokeh and Django have several ways of configuring serving static resources. This documentation will describe several possible configuration approaches.
59+
60+
The Bokeh [``resources`` setting](https://docs.bokeh.org/en/latest/docs/reference/settings.html#resources) can be set to one of several values (e.g ``server``, ``inline``, ``cdn``), the default is ``cdn``. If this setting is set to ``inline``, or ``cdn`` then Bokeh resources will be served independently of Django resources. However, if the Bokeh ``resources`` setting is set to ``server``, then the Bokeh resources are served up by the Django server in the same way that the Django static resources are and so Django must be configured to be able to find the Bokeh resources.
61+
62+
To specify the Bokeh ``resources`` setting add the following to the Django ``settings.py`` file:
63+
64+
```python
65+
from bokeh.settings import settings as bokeh_settings
66+
67+
bokeh_settings.resources = 'server'
68+
```
69+
70+
If the Bokeh ``resources`` setting is set to ``server`` then we must add the location of the Bokeh resources to the ``STATICFILES_DIRS`` setting:
71+
72+
```python
73+
from bokeh.settings import settings as bokeh_settings, bokehjsdir
74+
75+
STATICFILES_DIRS = [
76+
...,
77+
bokehjsdir(),
78+
]
79+
```
80+
81+
Django can be configured to automatically find and collect static files using the [``staticfiles`` app](https://docs.djangoproject.com/en/4.2/ref/contrib/staticfiles/), or the static file URL patterns can be explicitly added to the list of ``urlpatterns`` in the ``urls.py`` file.
82+
83+
To explicitly add the static file ``urlpatterns`` add the following to the ``urls.py`` file:
84+
85+
```python
86+
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
87+
from bokeh_django import static_extensions
88+
89+
urlpatterns = [
90+
...,
91+
*static_extensions(),
92+
*staticfiles_urlpatterns(),
93+
]
94+
```
95+
96+
Be sure that the ``static_extensions`` are listed before the ``staticfiles_urlpatterns``.
97+
98+
Alternatively, you can configure the [``staticfiles`` app](https://docs.djangoproject.com/en/4.2/ref/contrib/staticfiles/) by adding ``'django.contrib.staticfiles',`` to ``INSTALLED_APPS``:
99+
100+
```python
101+
INSTALLED_APPS = [
102+
...,
103+
'django.contrib.staticfiles',
104+
'channels',
105+
'bokeh_django',
106+
]
107+
```
108+
109+
Next add ``bokeh_django.static.BokehExtensionFinder`` to the ``STATICFILES_FINDERS`` setting. The default value for ``STATICFILES_FINDERS`` has two items. If you override the default by adding the ``STATICFILES_FINDERS`` setting to your ``settings.py`` file, then be sure to also list the two default values in addition to the ``BokehExtensionFinder``:
110+
111+
```python
112+
STATICFILES_FINDERS = (
113+
"django.contrib.staticfiles.finders.FileSystemFinder",
114+
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
115+
'bokeh_django.static.BokehExtensionFinder',
116+
)
117+
```
118+
119+
## Define Routes
120+
121+
Bokeh applications are integrated into Django through routing or URLs.
122+
123+
In a Django app, the file specified by the ``ROOT_URLCONF`` setting (e.g. ``urls.py``) must define ``urlpatterns`` which is a sequence of ``django.url.path`` and/or ``django.url.re_path`` objects. When integrating a Django app with Bokeh, the ``urls.py`` file must also define ``bokeh_apps`` as a sequence of ``bokeh_django`` routing objects. This should be done using the ``bokeh_djagno.document`` and/or ``bokeh_django.autoload`` functions.
124+
125+
### Document
126+
127+
The first way to define a route is to use ``bokeh_django.document``, which defines a route to a Bokeh app (as either a file-path or a function).
128+
129+
```python
130+
from bokeh_django import document
131+
from .views import my_bokeh_app_function
132+
133+
bokeh_apps = [
134+
document('url-pattern/', '/path/to/bokeh/app.py'),
135+
document('another-url-pattern/', my_bokeh_app_function)
136+
]
137+
```
138+
When using the ``document`` route Django will route the URL directly to the Bokeh app and all the rendering will be handled by Bokeh.
139+
140+
### Directory
141+
142+
An alternative way to create ``document`` routes is to use ``bokeh_django.directory`` to automatically create a ``document`` route for all the bokeh apps found in a directory. In this case the file name will be used as the URL pattern.
143+
144+
```python
145+
from bokeh_django import directory
146+
147+
bokeh_apps = directory('/path/to/bokeh/apps/')
148+
```
149+
150+
### Autoload
151+
152+
To integrate more fully into a Django application routes can be created using ``autoload``. This allows the Bokeh application to be embedded in a template that is rendered by Django. This has the advantage of being able to leverage Django capabilities in the view and the template, but is slightly more involved to set up. There are five components that all need to be configured to work together: the [Bokeh handler](#bokeh-handler), the [Django view](#django-view), the [template](#template), the [Django URL path](#django-url-path), and the [Bokeh URL route](#bokeh-url-route).
153+
154+
#### Bokeh Handler
155+
156+
The handler is a function (or any callable) that accepts a ``bokeh.document.Document`` object and configures it with the Bokeh content that should be embedded. This is done by adding a Bokeh object as the document root:
157+
158+
```python
159+
from bokeh.document import Document
160+
from bokeh.layouts import column
161+
from bokeh.models import Slider
162+
163+
def bokeh_handler(doc: Document) -> None:
164+
slider = Slider(start=0, end=30, value=0, step=1, title="Example")
165+
doc.add_root(column(slider))
166+
```
167+
168+
The handler can also embed a Panel object. In this case the document is passed in to the ``server_doc`` method of the Panel object:
169+
170+
```python
171+
import panel as pn
172+
def panel_handler(doc: Document) -> None:
173+
pn.Row().server_doc(doc)
174+
```
175+
176+
#### Django View
177+
178+
The view is a Django function that accepts a ``request`` object and returns a ``response``. A view that embeds a Bokeh app must create a ``bokeh.embed.server_document`` and pass it in the context to the template when rendering the response.
179+
180+
```python
181+
from bokeh.embed import server_document
182+
from django.shortcuts import render
183+
184+
def view_function(request):
185+
script = server_document(request.build_absolute_uri())
186+
return render(request, "embed.html", dict(script=script))
187+
```
188+
189+
#### Template
190+
191+
The template document is a Django HTML template (e.g. ``"embed.html"``) that will be rendered by Django. It can be as complex as desired, but at the very least must render the ``script`` that was passed in from the context:
192+
193+
```html
194+
<!doctype html>
195+
<html lang="en">
196+
<body>
197+
{{ script|safe }}
198+
</body>
199+
</html>
200+
```
201+
202+
#### Django URL Path
203+
204+
The [Django URL Path](#django-url-path) is a ``django.url.path`` or ``django.url.re_path`` object that is included in the ``urlpatters`` sequence and that maps a URL pattern to the [Django View](#django-view) as would normally be done with Django.
205+
206+
```python
207+
urlpatterns = [
208+
path("embedded-bokeh-app/", views.view_function),
209+
]
210+
```
211+
212+
#### Bokeh URL Route
213+
214+
The [Bokeh URL Route](#bokeh-url-route) is a ``bokeh_django.autoload`` object that is included in the ``bokeh_apps`` sequence and that maps a URL pattern to the [Bokeh handler](#bokeh-handler).
215+
216+
```python
217+
from bokeh_django import autoload
218+
219+
bokeh_apps = [
220+
autoload("embedded-bokeh-app/", views.handler)
221+
]
222+
```
223+
224+
Note that the URL pattern should be the same URL pattern that was used in the corresponding [Django URL Path](#django-url-path). In reality the URL pattern must match the URL that the ``server_document`` script is configured with in the [Django View](#django-view). Normally, it is easiest to use the URL from the ``request`` object (e.g. ``script = server_document(request.build_absolute_uri())``), which is the URL of the corresponding [Django URL Path](#django-url-path).

bokeh_django/consumers.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ def get_argument(self, name: str, default: str | None = None) -> str | None:
9090
return self.arguments.get(name, default)
9191

9292
def resources(self, absolute_url: str | None = None) -> Resources:
93-
mode = settings.resources(default="server")
93+
mode = settings.resources()
9494
if mode == "server":
9595
root_url = urljoin(absolute_url, self._prefix) if absolute_url else self._prefix
9696
return Resources(mode="server", root_url=root_url, path_versioner=StaticHandler.append_version)

bokeh_django/routing.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
# General API
5757
# -----------------------------------------------------------------------------
5858

59+
5960
class DjangoApplicationContext(ApplicationContext):
6061
async def create_session_if_needed(self, session_id: ID, request: HTTPServerRequest | None = None,
6162
token: str | None = None) -> ServerSession:
@@ -135,6 +136,10 @@ def __init__(self, url: str, app: ApplicationLike, *, document: bool = False, au
135136
self.document = document
136137
self.autoload = autoload
137138

139+
def __repr__(self):
140+
doc = 'document' if self.document else ''
141+
return f'<{self.__module__}.{self.__class__.__name__} url="{self.url}" {doc}>'
142+
138143
def _normalize(self, obj: ApplicationLike) -> Application:
139144
if callable(obj):
140145
return Application(FunctionHandler(obj, trap_exceptions=True))
@@ -162,11 +167,12 @@ def directory(*apps_paths: Path) -> List[Routing]:
162167

163168
for apps_path in apps_paths:
164169
if apps_path.exists():
165-
paths += [ entry for entry in apps_path.glob("*") if is_bokeh_app(entry) ]
170+
paths += [entry for entry in apps_path.glob("*") if is_bokeh_app(entry)]
166171
else:
167-
log.warn(f"bokeh applications directory '{apps_path}' doesn't exist")
172+
log.warning(f"bokeh applications directory '{apps_path}' doesn't exist")
168173

169-
return [ document(url, app) for url, app in build_single_handler_applications(paths).items() ]
174+
paths = [str(p) for p in paths]
175+
return [document(url, app) for url, app in build_single_handler_applications(paths).items()]
170176

171177

172178
class RoutingConfiguration:
@@ -187,7 +193,7 @@ def _add_new_routing(self, routing: Routing) -> None:
187193
kwargs = dict(app_context=routing.app_context)
188194

189195
def join(*components):
190-
return "/".join([ component.strip("/") for component in components if component ])
196+
return "/".join([component.strip("/") for component in components if component])
191197

192198
def urlpattern(suffix=""):
193199
return r"^{}$".format(join(re.escape(routing.url)) + suffix)

conda.recipe/meta.yaml

+9-8
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
1-
{% set sdata = load_setup_py_data() %}
1+
{% set pyproject = load_file_data('../pyproject.toml', from_recipe_dir=True) %}
2+
{% set project_data = pyproject.get('project') %}
23

34
package:
45
name: bokeh-django
5-
version: {{ sdata['version'] }}
6+
version: {{ project_data['version'] }}
67

78
source:
89
path: ..
910

1011
build:
1112
noarch: python
1213
number: 0
13-
script: python setup.py install --single-version-externally-managed --record=record.txt
14+
script: pip install .
1415

1516

1617
requirements:
1718
build:
18-
- python=3.7*
19+
- python
1920
- setuptools
2021
run:
2122
- python
2223

2324
# dependencies are defined in setup.py
24-
{% for dep in sdata['install_requires'] %}
25+
{% for dep in project_data['dependencies'] %}
2526
- {{ dep.lower() }}
2627
{% endfor %}
2728

@@ -32,6 +33,6 @@ test:
3233
- bokeh_django.consumers
3334

3435
about:
35-
home: {{ sdata['url'] }}
36-
summary: {{ sdata['description'] }}
37-
license: {{ sdata['license'] }}
36+
home: {{ project_data['urls']['Homepage'] }}
37+
summary: {{ project_data['description'] }}
38+
license: BSD-3-Clause

examples/django_embed/README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
Install django, panel and channels (pip only) and run:
1+
Install panel and pandas and run:
22

33
```sh
4-
./manage.py runserver 0.0.0.0:8000
4+
python manage.py runserver
55
```
66

7-
then navigate to <http://127.0.0.1:8000/shape_viewer>, <http://127.0.0.1:8000/sea_surface>, or <http://127.0.0.1:8000/sea_surface_with_template>.
7+
then navigate to <http://127.0.0.1:8000>.

examples/django_embed/bokeh_apps/sea_surface.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@
1111
title="Sea Surface Temperature at 43.18, -70.43")
1212
plot.line("time", "temperature", source=source)
1313

14+
1415
def callback(attr, old, new):
1516
if new == 0:
1617
data = df
1718
else:
1819
data = df.rolling(f"{new}D").mean()
19-
source.data = ColumnDataSource(data=data).data
20+
source.data = dict(ColumnDataSource(data=data).data)
21+
2022

2123
slider = Slider(start=0, end=30, value=0, step=1, title="Smoothing by N Days")
2224
slider.on_change("value", callback)
+8-15
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,10 @@
1-
"""
2-
ASGI config for django_embed project.
1+
from channels.auth import AuthMiddlewareStack
2+
from channels.routing import ProtocolTypeRouter, URLRouter
3+
from django.apps import apps
34

4-
It exposes the ASGI callable as a module-level variable named ``application``.
5+
bokeh_app_config = apps.get_app_config('bokeh_django')
56

6-
For more information on this file, see
7-
https://channels.readthedocs.io/en/latest/deploying.html
8-
"""
9-
10-
import os
11-
12-
import django
13-
from channels.routing import get_default_application
14-
15-
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_embed.settings')
16-
django.setup()
17-
application = get_default_application()
7+
application = ProtocolTypeRouter({
8+
'websocket': AuthMiddlewareStack(URLRouter(bokeh_app_config.routes.get_websocket_urlpatterns())),
9+
'http': AuthMiddlewareStack(URLRouter(bokeh_app_config.routes.get_http_urlpatterns())),
10+
})

examples/django_embed/django_embed/routing.py

-10
This file was deleted.

0 commit comments

Comments
 (0)