Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Voila Username API
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Username Fetch\n",
"\n",
"This is a widget example designed to show username fetching inside a Jupyter notebook or Voila dashboard running under [ContainDS and JupyterHub](https://cdsdashboards.readthedocs.io/).\n",
"\n",
"First create the Javascript view. Nothing is really run at this stage (i.e. no API call to ContainDS Dashboards)."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%%javascript\n",
"\n",
"require.undef('user_widget');\n",
"\n",
"define('user_widget', [\"@jupyter-widgets/base\"], function (widgets) {\n",
"\n",
" \n",
" var UserView = widgets.DOMWidgetView.extend({\n",
" initialize: function(attributes, options) {\n",
"\n",
" this.response = fetch(\n",
" '/hub/dashboards-api/hub-info/user',\n",
" { \n",
" mode: 'no-cors', \n",
" credentials: 'same-origin',\n",
" headers: new Headers({'Access-Control-Allow-Origin':'*'}) \n",
" });\n",
" this.response.then( response => {\n",
" this.result = response.json();\n",
" this.result.then( json => {\n",
" this.model.set('value', json);\n",
" this.model.set('name', json.name);\n",
" this.model.save_changes();\n",
" });\n",
"\n",
" });\n",
" \n",
" },\n",
" \n",
" render: async function () {\n",
" await this.response;\n",
" await this.result;\n",
" var json = this.model.get('value');\n",
" var text = 'No user';\n",
" if (json.hasOwnProperty('name')) {\n",
" text = 'Rendered by Javascript: '+json['name'];\n",
" }\n",
" this.el.appendChild(document.createTextNode(text));\n",
"\n",
" },\n",
" \n",
" });\n",
"\n",
" return {\n",
" UserView: UserView\n",
" };\n",
"});"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Define Python view"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from traitlets import Unicode, Dict, Unicode\n",
"from ipywidgets import DOMWidget, register\n",
"\n",
"@register\n",
"class User(DOMWidget):\n",
" _view_name = Unicode('UserView').tag(sync=True)\n",
" _view_module = Unicode('user_widget').tag(sync=True)\n",
" _view_module_version = Unicode('0.1.0').tag(sync=True)\n",
"\n",
" value = Dict({}, help=\"User info\").tag(sync=True)\n",
" name = Unicode('').tag(sync=True)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Create the Python side of the widget. Nothing happens yet since there is no Javascript client-side widget instantiated."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"user = User()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Try to access username. But there still won't be any API call because we are only inspecting the Python attributes here - which are still empty due to defaults."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"user.value"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now we display the widget, so a Javascript client-side widget is created and the API call is made in initialize. The widget itself should display 'Rendered by Javascript: dan' or whatever the username is. That should happen on Voilà or Jupyter notebook."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"user"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"However, the Python attribute will be available (probably unless you're really quick) on Jupyter notebook but NOT in Voilà."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"user.value"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In Voilà this is not actually a timing issue. If you put `time.sleep(5)` in the cell above `user.value` then you would still not see the results. This is because the Voilà execution model runs the Python code from top to bottom _before_ any client-side rendering. It's what's happening during the 'Executing' part when Voilà starts up.\n",
"\n",
"So the question remains: in Voilà how can we take the Python attribute of the username and use it within some Python code. The answer, as always in Voilà, is to use callbacks.\n",
"\n",
"Let's say the simple Python function we want to call just duplicates the username - to show 'dan dan'."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from ipywidgets.widgets import Text, VBox\n",
"from ipywidgets import link\n",
"from IPython.display import display\n",
"\n",
"def dup_username(change):\n",
" out.value = user.name + ' '+ user.name\n",
"\n",
"txt = Text('')\n",
"out = Text('')\n",
"\n",
"link((txt, 'value'), (user, 'name'))\n",
"\n",
"user.observe(dup_username, 'value')\n",
"\n",
"display(VBox([txt, out]))\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The above code works in Voilà because we essentially have two independent event handlers - an implicit one through 'link' which updates the value in the first text box, and then a completely independent 'observe' handler to detect the change when the username API call comes back."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"You're still never going to get the Python attributes to show up in Voilà - stop trying! :)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"user.value"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"But why didn't the 'observe' or 'link' callbacks work in Jupyter then?! That's only really because the user widget was displayed first (and the API call returned fairly quickly) so the change happened before these callbacks were registered. To fix you could display the user widget after setting up the callback instead of before (which should also be fine for Voilà)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"At first glance, being forced to use this callback system feels like it's missing the point of Voilà being based on Jupyter notebooks in the first place. I think that's right in some ways, but ultimately this is the same way any other input widget works. If you just asked the user to type their name into a Text box, you could only process that text through callbacks."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Further reading: [Asynchronous Widgets](https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20Asynchronous.html)\n",
"\n",
"But I don't think that changes anything for Voilà."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.4"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment