Skip to content

Commit d1a9299

Browse files
authored
Expose more arguments and updated docs (#31)
1 parent 905f292 commit d1a9299

File tree

3 files changed

+235
-42
lines changed

3 files changed

+235
-42
lines changed

README.md

Lines changed: 148 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,159 @@
11
# Jupyter Server Proxy for Panel
22

3-
When jupyter-panel-proxy is installed and you launch a Jupyter server (Notebook, JupyterLab or JupyterHub), a Panel server will be launched when you visit the `/panel` endpoint of the server. This will show an index of all applications being served, to launch a particular application visit the corresponding endpoint `/panel/<name_of_file>`.
3+
<table>
4+
<tbody>
5+
<tr>
6+
<td>Downloads</td>
7+
<td><a href="https://pypistats.org/packages/jupyter-panel-proxy"><img src="https://img.shields.io/pypi/dm/jupyter-panel-proxy?label=pypi" alt="PyPi Downloads" /></a></td>
8+
</tr>
9+
<tr>
10+
<td>Build Status</td>
11+
<td><a href="https://github.com/holoviz/jupyter-panel-proxy/actions/workflows/test.yaml?query=branch%3Amain"><img src="https://github.com/holoviz/jupyter-panel-proxy/workflows/tests/badge.svg?query=branch%3Amain" alt="Linux/MacOS Build Status"></a></td>
12+
</tr>
13+
<tr>
14+
<td>Latest dev release</td>
15+
<td><a href="https://github.com/holoviz/jupyter-panel-proxy/tags"><img src="https://img.shields.io/github/v/tag/holoviz/jupyter-panel-proxy.svg?label=tag&amp;colorB=11ccbb" alt="Github tag"></a></td>
16+
</tr>
17+
<tr>
18+
<td>Latest release</td>
19+
<td><a href="https://github.com/holoviz/jupyter-panel-proxy/releases"><img src="https://img.shields.io/github/release/holoviz/jupyter-panel-proxy.svg?label=tag&amp;colorB=11ccbb" alt="Github release"></a> <a href="https://pypi.python.org/pypi/jupyter-panel-proxy"><img src="https://img.shields.io/pypi/v/jupyter-panel-proxy.svg?colorB=cc77dd" alt="PyPI version"></a> <a href="https://anaconda.org/pyviz/jupyter-panel-proxy"><img src="https://img.shields.io/conda/v/pyviz/jupyter-panel-proxy.svg?colorB=4488ff&amp;style=flat" alt="panel version"></a> <a href="https://anaconda.org/conda-forge/jupyter-panel-proxy"><img src="https://img.shields.io/conda/v/conda-forge/jupyter-panel-proxy.svg?label=conda%7Cconda-forge&amp;colorB=4488ff" alt="conda-forge version"></a></td>
20+
</tr>
21+
<td>Support</td>
22+
<td><a href="https://discourse.holoviz.org/"><img src="https://img.shields.io/discourse/status?server=https%3A%2F%2Fdiscourse.holoviz.org" alt="Discourse"></a> <a href="https://discord.gg/rb6gPXbdAr"><img alt="Discord" src="https://img.shields.io/discord/1075331058024861767"></a>
23+
</td>
24+
</tr>
25+
</tbody>
26+
</table>
27+
28+
`jupyter-panel-proxy` integrates [HoloViz Panel](https://panel.holoviz.org) seamlessly with Jupyter environments (Notebook, JupyterLab, and JupyterHub).
29+
When installed, it launches a Panel server automatically at the `/panel` endpoint of your running Jupyter server.
30+
31+
Visiting `/panel` will display an index of all available applications, and each application can be accessed at `/panel/<name_of_file>`.
32+
33+
## When to use this project
34+
35+
Use `jupyter-panel-proxy` when you want to:
36+
37+
- *Serve Panel apps* alongside Jupyter notebooks or JupyterHub — without managing a separate web server.
38+
- *Reuse existing authentication* from JupyterHub and optionally integrate OAuth2 for finer-grained control.
39+
- *Automatically discover and serve multiple Panel apps* in a directory structure (no manual `panel serve` required).
40+
- *Deploy lightweight dashboards and interactive apps* close to your notebooks or lab environment.
41+
- *Run Panel behind a reverse proxy* with clean URL prefixing (`/panel`), and modern server features.
442

543
## Installation
644

7-
The `jupyter-panel-proxy` is available from `pip`:
45+
You can install `jupyter-panel-proxy` from PyPI:
46+
47+
```bash
48+
pip install jupyter-panel-proxy
49+
````
850

9-
pip install jupyter-panel-proxy
51+
or from conda:
1052

11-
and conda:
53+
```bash
54+
conda install conda-forge::jupyter-panel-proxy
55+
```
1256

13-
conda install -c pyviz jupyter-panel-proxy
57+
Once installed, a Panel server will be available at:
58+
59+
```
60+
https://<your-jupyter-server>/panel
61+
```
1462
1563
## Configuration
1664
17-
The jupyter-panel-proxy provides the ability to configure the proxy server by declaring a `jupyter-panel-proxy.yml` in the directory the Jupyter server is being launched from. The `yaml` file may declare the following keys:
18-
19-
- `apps` (`list`): A list of applications or glob patterns to serve
20-
- `launcher_entry` (`dict`): A [jupyter-server-proxy launcher entry specification](https://jupyter-server-proxy.readthedocs.io/en/latest/server-process.html#launcher-entry)
21-
- `file_types` (`list(str)`): A list of file types to serve if no explicit apps list is provided
22-
- `exclude_patterns` (`list(str)`): A list of glob/(fnmatch) patterns to exclude specific applications
23-
- `index` (`str`): The path to a Bokeh index template
24-
- `autoreload` (`bool`): Whether to automatically reload user sessions when the application or any of its imports change.
25-
- `admin` (`bool`): Whether to load panel's admin module.
26-
- `static_dirs` (`list`): A list of dicts mapping from server route to the static directory to be served
27-
- `warm` (`bool`): Whether to execute scripts on startup to warm up the server.
28-
- `num_procs` (`int`): Number of worker processes for an app. Using 0 will autodetect number of cores (defaults to 1)
29-
- `oauth_provider` (`str`): The OAuth2 provider to use.
30-
- `oauth-key` (`str`): The OAuth2 key to use
31-
- `oauth-secret` (`str`): The OAuth2 secret to use
32-
- `oauth-redirect-uri` (`str`): The OAuth2 redirect URI
33-
- `oauth_extra_params` (`dict`): Additional parameters to the OAuth provider.
34-
- `oauth_jwt_user` (`str`): The key in the ID JWT token to consider the user.
65+
You can configure the behavior of the proxy server by creating a `jupyter-panel-proxy.yml` file in the directory from which your Jupyter server is launched.
66+
67+
### Available configuration keys
68+
69+
| Key | Type | Description |
70+
| ---------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------- |
71+
| `apps` | `list` | List of apps or glob patterns to serve. If not set, apps are discovered automatically by file type. |
72+
| `file_types` | `list(str)` | File extensions to auto-discover apps (default: `ipynb`, `py`). |
73+
| `exclude_patterns` | `list(str)` | Glob/fnmatch patterns to exclude apps. |
74+
| `launcher_entry` | `dict` | A [jupyter-server-proxy launcher entry](https://jupyter-server-proxy.readthedocs.io/en/latest/server-process.html#launcher-entry). |
75+
| `index` | `str` | Path to a custom Bokeh index template. |
76+
| `autoreload` | `bool` | Automatically reload sessions when code changes. It is recommended to use `dev` instead. |
77+
| `dev ` | `bool` | Automatically reload sessions when code changes. |
78+
| `admin` | `bool` | Enable Panel's admin module. |
79+
| `warm` | `bool` | Execute apps on startup to warm up the server. |
80+
| `num_procs` | `int` | Number of worker processes (0 = auto). |
81+
| `num_threads` | `int` | Number of threads in the thread pool. |
82+
| `static_dirs` | `list` | Key=value routes for serving static files. |
83+
| `reuse_sessions` | `bool` | Reuse existing sessions (recommended for JupyterHub). |
84+
| `keep_alive` | `int` (ms) | Interval for keep-alive pings to clients. |
85+
| `check_unused_sessions` | `int` (ms) | How often to check for unused sessions. |
86+
| `unused_session_lifetime` | `int` (ms) | How long unused sessions last. |
87+
| `websocket_max_message_size` | `int` | Max message size for WebSocket in bytes. |
88+
| `root_path` | `str` | Root path can be used to handle cases where Panel is served behind a proxy. |
89+
| `cookie_path` | `str` | Path to apply cookies to. |
90+
| `log_level` | `str` | Log level (`info`, `debug`, etc.). |
91+
| `liveness` | `bool` | Enable a liveness endpoint. |
92+
| `liveness_endpoint` | `str` | Path of the liveness endpoint (default: `/liveness`). |
93+
| `profiler` | `str` | Profiler to use (e.g. `pyinstrument`). |
94+
| `global_loading_spinner` | `bool` | Add a global loading spinner to the UI. |
95+
| `oauth_provider` | `str` | OAuth2 provider name. |
96+
| `oauth_key` | `str` | OAuth2 key. |
97+
| `oauth_secret` | `str` | OAuth2 secret. |
98+
| `oauth_redirect_uri` | `str` | OAuth2 redirect URI. |
99+
| `oauth_extra_params` | `dict` | Additional parameters for the OAuth provider. |
100+
| `oauth_jwt_user` | `str` | JWT key to identify the user. |
101+
| `oauth_optional` | `bool` | Allow guest access to all endpoints. |
102+
| `oauth_guest_endpoints` | `list(str)` | List of endpoints accessible without authentication. |
103+
| `cookie_secret` | `str` | Secret key for secure cookies (can also be set via `PANEL_COOKIE_SECRET`). |
104+
| `oauth_encryption_key` | `str` | Encryption key for OAuth user info (can also be set via `OAUTH_ENCRYPTION_KEY`). |
105+
106+
## Launcher
107+
108+
When you install `jupyter-panel-proxy`, it automatically adds a Panel Launcher card to the JupyterLab and Notebook launcher interface:
109+
110+
![Panel launcher tile](https://raw.githubusercontent.com/holoviz/jupyter-panel-proxy/refs/heads/main/doc/jupyter_panel_proxy_tile.png)
111+
112+
Clicking this Panel tile opens a new browser tab at `/panel` where your Panel apps are served. This behavior is controlled by the `launcher_entry` field in the configuration.
113+
114+
## Application discovery
115+
116+
By default, `jupyter-panel-proxy` automatically discovers Panel applications in the current working directory (or in an `examples/` subdirectory if present).
117+
118+
The discovery logic works like this:
119+
120+
1. If `apps` is defined in `jupyter-panel-proxy.yml`:
121+
122+
* Each entry is interpreted as a file path or glob pattern.
123+
* All matching files are included.
124+
125+
2. If `apps` is not defined:
126+
127+
* The proxy scans the base directory (or `./examples` if it exists) recursively.
128+
* It includes files that match any extension listed in `file_types` (default: `ipynb`, `py`).
129+
* It excludes any paths that match `exclude_patterns` (by default, this includes common patterns like `*setup.py` or `*.ipynb_checkpoints*`).
130+
131+
3. The discovered list of applications is then passed to `panel serve`.
132+
133+
## Example YAML configuration
134+
135+
```yaml
136+
# jupyter-panel-proxy.yml
137+
138+
log_level: info
139+
liveness: true
140+
liveness_endpoint: /health
141+
global_loading_spinner: true
142+
```
143+
144+
## How it works
145+
146+
* When the Jupyter server starts, this proxy registers `/panel` as a route.
147+
* When a user navigates to `/panel`, the proxy launches `panel serve` internally with the configured options.
148+
* Apps are discovered automatically or defined explicitly.
149+
* The Panel server runs under the same authentication/session as Jupyter, and can optionally integrate OAuth for additional controls.
150+
151+
## Further reading
152+
153+
* [Panel Documentation](https://panel.holoviz.org)
154+
* [Jupyter Server Proxy](https://jupyter-server-proxy.readthedocs.io)
155+
* [HoloViz](https://holoviz.org)
156+
157+
## License
158+
159+
BSD-3-Clause

doc/jupyter_panel_proxy_tile.png

101 KB
Loading

panel_server/__init__.py

Lines changed: 87 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import fnmatch
22
import glob
3+
import os
34
import pathlib
45

56
import yaml
@@ -17,14 +18,14 @@
1718
}
1819

1920
DEFAULT_CONFIG = {
20-
'autoreload': True,
21-
'admin': True,
21+
'dev': True,
2222
'file_types': ['ipynb', 'py'],
2323
'launcher_entry': LAUNCHER_ENTRY
2424
}
2525

2626

2727
def _get_config():
28+
"""Load configuration from jupyter-panel-proxy.yml if present."""
2829
config_path = pathlib.Path('jupyter-panel-proxy.yml')
2930
config = dict(DEFAULT_CONFIG)
3031
if config_path.is_file():
@@ -34,19 +35,21 @@ def _get_config():
3435

3536

3637
def _search_apps(config):
38+
"""Search for apps based on file types configured."""
3739
base_dir = pathlib.Path('./')
3840
example_dir = base_dir / 'examples'
3941
if example_dir.is_dir():
4042
base_path = example_dir
4143
else:
4244
base_path = base_dir
4345
apps = []
44-
for ft in config.get('file_types'):
46+
for ft in config.get('file_types', []):
4547
apps += [str(app) for app in base_path.glob(f'**/*.{ft}')]
4648
return apps
4749

4850

4951
def _discover_apps():
52+
"""Discover apps according to configuration and exclusion patterns."""
5053
config = _get_config()
5154
if 'apps' in config:
5255
found_apps = []
@@ -63,40 +66,105 @@ def _discover_apps():
6366

6467

6568
def _launch_command(port):
69+
"""Build the `panel serve` launch command based on configuration."""
6670
config = _discover_apps()
67-
command = ["panel", "serve", *config.get('apps'), "--allow-websocket-origin=*", "--port", f"{port}", "--prefix", "{base_url}panel", "--disable-index-redirect"]
68-
if config.get('autoreload'):
69-
command.append('--autoreload')
70-
if config.get('warm'):
71-
command.append('--warm')
72-
if config.get('admin'):
73-
command.append('--admin')
74-
if 'num_procs' in config:
75-
command += ['--num-procs', str(config['num_procs'])]
76-
if 'static_dirs' in config:
77-
command += ['--static-dirs', *config['static_dirs']]
71+
72+
command = [
73+
"panel", "serve",
74+
*config.get('apps'),
75+
"--allow-websocket-origin", "*",
76+
"--port", f"{port}",
77+
"--prefix", "{base_url}panel",
78+
"--disable-index-redirect"
79+
]
80+
81+
# Boolean flags
82+
for flag in ['autoreload', 'warm', 'admin', 'dev', 'reuse_sessions', 'liveness']:
83+
if config.get(flag):
84+
command.append(f'--{flag.replace("_", "-")}')
85+
86+
# Numeric flags
87+
numeric_flags = {
88+
'num_procs': '--num-procs',
89+
'num_threads': '--num-threads',
90+
'keep_alive': '--keep-alive',
91+
'check_unused_sessions': '--check-unused-sessions',
92+
'unused_session_lifetime': '--unused-session-lifetime',
93+
'websocket_max_message_size': '--websocket-max-message-size',
94+
'stats_log_frequency': '--stats-log-frequency',
95+
'mem_log_frequency': '--mem-log-frequency',
96+
'session_token_expiration': '--session-token-expiration'
97+
}
98+
for key, arg in numeric_flags.items():
99+
if key in config:
100+
command += [arg, str(config[key])]
101+
102+
# String flags
103+
string_flags = {
104+
'root_path': '--root-path',
105+
'cookie_path': '--cookie-path',
106+
'log_level': '--log-level',
107+
'liveness_endpoint': '--liveness-endpoint',
108+
'profiler': '--profiler',
109+
'index': '--index'
110+
}
111+
for key, arg in string_flags.items():
112+
if key in config:
113+
command += [arg, str(config[key])]
114+
115+
# List flags
116+
list_flags = {
117+
'static_dirs': '--static-dirs',
118+
'oauth_guest_endpoints': '--oauth-guest-endpoints'
119+
}
120+
for key, arg in list_flags.items():
121+
if key in config:
122+
for val in config[key]:
123+
command += [arg, str(val)]
124+
125+
# OAuth configuration
78126
if 'oauth_provider' in config:
79127
from cryptography.fernet import Fernet
80128
from bokeh.util.token import generate_secret_key
81129
command += ['--oauth-provider', config['oauth_provider']]
82-
command += ['--oauth-encryption-key', Fernet.generate_key()]
83-
command += ['--cookie-secret', generate_secret_key()]
130+
131+
encryption_key = config.get('oauth_encryption_key') or os.environ.get('OAUTH_ENCRYPTION_KEY')
132+
if not encryption_key:
133+
encryption_key = Fernet.generate_key()
134+
command += ['--oauth-encryption-key', encryption_key]
135+
136+
cookie_secret = config.get('cookie_secret') or os.environ.get('PANEL_COOKIE_SECRET')
137+
if not cookie_secret:
138+
cookie_secret = generate_secret_key()
139+
command += ['--cookie-secret', cookie_secret]
140+
84141
if 'oauth_key' in config:
85142
command += ['--oauth-key', config['oauth_key']]
143+
86144
if 'oauth_secret' in config:
87145
command += ['--oauth-secret', config['oauth_secret']]
146+
88147
if 'oauth_redirect_uri' in config:
89148
command += ['--oauth-redirect-uri', config['oauth_redirect_uri']]
149+
90150
if 'oauth_jwt_user' in config:
91-
command += ['--oauth-jtw-user', config['oauth_jwt_user']]
151+
command += ['--oauth-jwt-user', config['oauth_jwt_user']]
152+
92153
if 'oauth_extra_params' in config:
93154
command += ['--oauth-extra-params', repr(config['oauth_extra_params'])]
94-
if 'index' in config:
95-
command += ['--index', str(pathlib.Path(config['index']).absolute())]
155+
156+
if config.get('oauth_optional'):
157+
command.append('--oauth-optional')
158+
159+
# Global loading spinner
160+
if config.get('global_loading_spinner'):
161+
command.append('--global-loading-spinner')
162+
96163
return command
97164

98165

99166
def setup_panel_server():
167+
"""Entry point for Jupyter Server Proxy."""
100168
config = _get_config()
101169
spec = {
102170
'command': _launch_command,

0 commit comments

Comments
 (0)