Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ name: ci

on: [push]

env:
GALAXY_URL: "${{ secrets.GALAXY_URL }}"
GALAXY_API_KEY: "${{ secrets.GALAXY_API_KEY }}"

jobs:
test:
runs-on: ubuntu-latest
Expand All @@ -25,4 +29,4 @@ jobs:
- name: Run mypy
run: docker run --rm ${{ steps.build.outputs.imageid }} poetry run mypy .
- name: Run tests
run: docker run --rm ${{ steps.build.outputs.imageid }} poetry run pytest test_examples.py
run: docker run --rm -e GALAXY_URL=${{ env.GALAXY_URL }} -e GALAXY_API_KEY=${{ env.GALAXY_API_KEY }} ${{ steps.build.outputs.imageid }} poetry run pytest test_examples.py
5 changes: 4 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ FROM python:3.13-slim

RUN apt update && apt install -y \
curl \
g++
g++ \
libosmesa6-dev \
libx11-dev \
libxrender1

ENV POETRY_HOME=/poetry
ENV PATH=/poetry/bin:$PATH
Expand Down
38 changes: 31 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,22 @@ This repository contains examples of how to build application functionalities in

1. [Creating a form for a Pydantic model](examples/pydantic_form)
2. [Create a JSON editor for a Pydantic model](examples/pydantic_monaco)
3. [Working with Plotly](examples/plotly)
4. [Working with Matplotlib](examples/matplotlib)
5. [Synchronizing changes between tabs](examples/multitab)
3. [Conditionally rendering elements](examples/conditional_rendering)
4. [Conditionally disabling elements](examples/conditional_disabling)
5. [Changing Pydantic rules based on user input](examples/dynamic_pydantic_rules)
6. [Complex Pydantic rules](examples/complex_pydantic_rules)
7. [Selecting datafiles from the server](examples/data_selector)
8. [Running a Galaxy tool](examples/run_galaxy_tool)
9. [Running a Galaxy workflow](examples/run_galaxy_workflow)
10. [Working with Plotly](examples/plotly)
11. [Working with Matplotlib](examples/matplotlib)
12. [Working with VTK](examples/vtk)
13. [Synchronizing changes between tabs](examples/multitab)

We also provide examples that take advantage of ORNL resources:

1. [Selecting datafiles from the Analysis Cluster](examples/ornl/neutron_data_selector)
2. [Selecting datafiles from ONCat](examples/ornl/oncat)

## Running Examples

Expand All @@ -19,11 +32,22 @@ poetry install
poetry run python examples/{example_folder}/main.py
```

## Running Tests

This repo includes an automated test to ensure that each example runs:

```bash
poetry install
poetry run pytest
```

You can set the environment variable `INCLUDE_ORNL_TESTS=1` if you want to test the examples that rely on specific ORNL resources. Note that you will need to run them from a location that has access to these resources.

## Example Structure

We use the MVVM framework for NOVA applications. With this in mind, each example is broken down into the following files:
We use the MVVM framework for NOVA applications. With this in mind, each example is broken down into the following sections:

1. view.py - Sets up a Trame GUI for the example.
2. model.py - Sets up a Pydantic model and any business logic needed by the application.
3. view_model.py - Sets up a view model that binds the model and view.
1. view - Sets up a Trame GUI for the example.
2. model - Sets up a Pydantic model and any business logic needed by the application.
3. view_model - Sets up a view model that binds the model and view.
4. main.py - Entrypoint for the Trame GUI.
12 changes: 12 additions & 0 deletions examples/complex_pydantic_rules/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""Runs the complex Pydantic rules example."""

from examples.complex_pydantic_rules.view import App


def main() -> None:
app = App()
app.server.start(open_browser=False)


if __name__ == "__main__":
main()
51 changes: 51 additions & 0 deletions examples/complex_pydantic_rules/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""
Model implementation for complex Pydantic rules example.

Here, we will create a Pydantic rule to use nltk to clean and tokenize text provided by the user.
"""

import re

import nltk
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
from pydantic import BaseModel, Field, model_validator
from typing_extensions import Self


class FormData(BaseModel):
"""Pydantic model for the form data."""

tokenized_text: str = Field(default="", title="Tokenized Text")
text: str = Field(default="", title="Input Text")

# Now, we will leverage Pydantic to clean and tokenize the text.
# One could also use a Pydantic computed_field here.
@model_validator(mode="after")
def tokenize_text(self) -> Self:
input_text = self.text.lower()

# If any field in the model is invalid, then you can raise a ValueError and display the error to the user.
if "error" in input_text:
raise ValueError("Text must not contain the word error.")

# Remove punctuation and other non-word text.
cleaned_text = re.sub(r"[\W]", " ", input_text)

# Tokenize the text and remove stopwords.
words = word_tokenize(cleaned_text)
stop_words = set(stopwords.words("english"))
self.tokenized_text = " ".join([word for word in words if word not in stop_words])

# This is required by Pydantic.
return self


class Model:
"""Model implementation for complex Pydantic rules example."""

def __init__(self) -> None:
nltk.download("stopwords")
nltk.download("punkt_tab")

self.form = FormData()
41 changes: 41 additions & 0 deletions examples/complex_pydantic_rules/view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""View for complex Pydantic rules example."""

from nova.mvvm.trame_binding import TrameBinding
from nova.trame import ThemedApp
from nova.trame.view.components import InputField
from nova.trame.view.layouts import VBoxLayout
from trame.widgets import vuetify3 as vuetify

from .model import Model
from .view_model import ViewModel


class App(ThemedApp):
"""View for complex Pydantic rules example."""

def __init__(self) -> None:
super().__init__()

self.create_vm()
# If you forget to call connect, then the application will crash when you attempt to update the view.
self.view_model.form_data_bind.connect("data")
# Generally, we want to initialize the view state before creating the UI for ease of use. If initialization
# is expensive, then you can defer it. In this case, you must handle the view state potentially being
# uninitialized in the UI via v_if statements.
self.view_model.update_form_data()

self.create_ui()

def create_ui(self) -> None:
with super().create_ui() as layout:
with layout.content:
with vuetify.VCard(classes="mx-auto my-4", max_width=600):
with VBoxLayout(columns=3, gap="0.5em"):
InputField(v_model="data.text", type="textarea")
InputField(v_model="data.tokenized_text", readonly=True, type="textarea")

def create_vm(self) -> None:
binding = TrameBinding(self.state)

model = Model()
self.view_model = ViewModel(model, binding)
25 changes: 25 additions & 0 deletions examples/complex_pydantic_rules/view_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""View model implementation for complex Pydantic rules example."""

from typing import Any, Dict

from nova.mvvm.interface import BindingInterface

from .model import Model


class ViewModel:
"""View model implementation for complex Pydantic rules example."""

def __init__(self, model: Model, binding: BindingInterface) -> None:
self.model = model

# self.on_update is called any time the view updates the binding.
self.form_data_bind = binding.new_bind(self.model.form, callback_after_update=self.send_cleaned_text_to_view)

def send_cleaned_text_to_view(self, results: Dict[str, Any]) -> None:
# This is necessary to send the cleaned text to the view since it's updated programmatically.
self.form_data_bind.update_in_view(self.model.form)

def update_form_data(self) -> None:
# This will fail if you haven't called connect on the binding!
self.form_data_bind.update_in_view(self.model.form)
12 changes: 12 additions & 0 deletions examples/conditional_disabling/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""Runs the conditional disabling example."""

from examples.conditional_disabling.view import App


def main() -> None:
app = App()
app.server.start(open_browser=False)


if __name__ == "__main__":
main()
19 changes: 19 additions & 0 deletions examples/conditional_disabling/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Model implementation for conditional disabling example."""

from pydantic import BaseModel, Field


class FormData(BaseModel):
"""Pydantic model for the form data."""

disable_phone_field: bool = Field(default=False, title="Disable Phone Number?")
email_address: str = Field(default="", title="Email Address")
full_name: str = Field(default="", title="Full Name")
phone_number: str = Field(default="", title="Phone Number")


class Model:
"""Model implementation for conditional disabling example."""

def __init__(self) -> None:
self.form = FormData()
49 changes: 49 additions & 0 deletions examples/conditional_disabling/view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""View for conditional disabling example."""

from nova.mvvm.trame_binding import TrameBinding
from nova.trame import ThemedApp
from nova.trame.view.components import InputField
from nova.trame.view.layouts import GridLayout, HBoxLayout
from trame.widgets import vuetify3 as vuetify

from .model import Model
from .view_model import ViewModel


class App(ThemedApp):
"""View for conditional disabling example."""

def __init__(self) -> None:
super().__init__()

self.create_vm()
# If you forget to call connect, then the application will crash when you attempt to update the view.
self.view_model.form_data_bind.connect("data")
# Generally, we want to initialize the view state before creating the UI for ease of use. If initialization
# is expensive, then you can defer it. In this case, you must handle the view state potentially being
# uninitialized in the UI via v_if statements.
self.view_model.update_form_data()

self.create_ui()

def create_ui(self) -> None:
with super().create_ui() as layout:
with layout.content:
with vuetify.VCard(classes="mx-auto my-4", max_width=600):
with GridLayout(columns=2, classes="mb-2", gap="0.5em"):
InputField(v_model="data.email_address")
InputField(v_model="data.full_name")

with HBoxLayout(gap="0.5em", valign="center"):
InputField(v_model="data.disable_phone_field", type="checkbox")

# Now, we can use disable_phone_field to conditionally disable the phone number field.
# Note that because we need to bind to a parameter that doesn't start with v_ (indicating a
# v-directive in Vue.js), we need to use the Trame tuple syntax to bind to disabled.
InputField(v_model="data.phone_number", disabled=("data.disable_phone_field",))

def create_vm(self) -> None:
binding = TrameBinding(self.state)

model = Model()
self.view_model = ViewModel(model, binding)
19 changes: 19 additions & 0 deletions examples/conditional_disabling/view_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""View model implementation for conditional disabling example."""

from nova.mvvm.interface import BindingInterface

from .model import Model


class ViewModel:
"""View model implementation for conditional disabling example."""

def __init__(self, model: Model, binding: BindingInterface) -> None:
self.model = model

# self.on_update is called any time the view updates the binding.
self.form_data_bind = binding.new_bind(self.model.form)

def update_form_data(self) -> None:
# This will fail if you haven't called connect on the binding!
self.form_data_bind.update_in_view(self.model.form)
12 changes: 12 additions & 0 deletions examples/conditional_rendering/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""Runs the conditional rendering example."""

from examples.conditional_rendering.view import App


def main() -> None:
app = App()
app.server.start(open_browser=False)


if __name__ == "__main__":
main()
24 changes: 24 additions & 0 deletions examples/conditional_rendering/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Model implementation for conditional rendering example."""

from pydantic import BaseModel, Field


class FormData(BaseModel):
"""Pydantic model for the form data."""

comments: str = Field(default="", title="Comments")
email_address: str = Field(default="", title="Email Address")
full_name: str = Field(default="", title="Full Name")
phone_number: str = Field(default="", title="Phone Number")
show_phone_field: bool = Field(default=False, title="Add Phone Number?")
show_comments_field: bool = Field(default=False)


class Model:
"""Model implementation for conditional rendering example."""

def __init__(self) -> None:
self.form = FormData()

def toggle_comments(self) -> None:
self.form.show_comments_field = not self.form.show_comments_field
Loading