diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5e9235c..b2aa857 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 @@ -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 diff --git a/Dockerfile b/Dockerfile index 86dac2f..1e52a32 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/README.md b/README.md index 2da0752..1c1a871 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. diff --git a/examples/complex_pydantic_rules/main.py b/examples/complex_pydantic_rules/main.py new file mode 100644 index 0000000..22fa977 --- /dev/null +++ b/examples/complex_pydantic_rules/main.py @@ -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() diff --git a/examples/complex_pydantic_rules/model.py b/examples/complex_pydantic_rules/model.py new file mode 100644 index 0000000..8a091ff --- /dev/null +++ b/examples/complex_pydantic_rules/model.py @@ -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() diff --git a/examples/complex_pydantic_rules/view.py b/examples/complex_pydantic_rules/view.py new file mode 100644 index 0000000..06197b5 --- /dev/null +++ b/examples/complex_pydantic_rules/view.py @@ -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) diff --git a/examples/complex_pydantic_rules/view_model.py b/examples/complex_pydantic_rules/view_model.py new file mode 100644 index 0000000..8e60ac1 --- /dev/null +++ b/examples/complex_pydantic_rules/view_model.py @@ -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) diff --git a/examples/conditional_disabling/main.py b/examples/conditional_disabling/main.py new file mode 100644 index 0000000..a52610f --- /dev/null +++ b/examples/conditional_disabling/main.py @@ -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() diff --git a/examples/conditional_disabling/model.py b/examples/conditional_disabling/model.py new file mode 100644 index 0000000..408f447 --- /dev/null +++ b/examples/conditional_disabling/model.py @@ -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() diff --git a/examples/conditional_disabling/view.py b/examples/conditional_disabling/view.py new file mode 100644 index 0000000..8b20f19 --- /dev/null +++ b/examples/conditional_disabling/view.py @@ -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) diff --git a/examples/conditional_disabling/view_model.py b/examples/conditional_disabling/view_model.py new file mode 100644 index 0000000..8feb178 --- /dev/null +++ b/examples/conditional_disabling/view_model.py @@ -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) diff --git a/examples/conditional_rendering/main.py b/examples/conditional_rendering/main.py new file mode 100644 index 0000000..a22f50f --- /dev/null +++ b/examples/conditional_rendering/main.py @@ -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() diff --git a/examples/conditional_rendering/model.py b/examples/conditional_rendering/model.py new file mode 100644 index 0000000..2ccbfc5 --- /dev/null +++ b/examples/conditional_rendering/model.py @@ -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 diff --git a/examples/conditional_rendering/view.py b/examples/conditional_rendering/view.py new file mode 100644 index 0000000..116d886 --- /dev/null +++ b/examples/conditional_rendering/view.py @@ -0,0 +1,58 @@ +"""View for conditional rendering 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, VBoxLayout +from trame.widgets import html +from trame.widgets import vuetify3 as vuetify + +from .model import Model +from .view_model import ViewModel + + +class App(ThemedApp): + """View for conditional rendering 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(classes="mb-2", gap="0.5em", valign="center"): + InputField(v_model="data.show_phone_field", type="checkbox") + + # Now, we can use show_phone_field to conditionally render the phone number field. + InputField(v_if="data.show_phone_field", v_model="data.phone_number") + # Following a v_if, we can use v_else_if and v_else. + html.Span("{{ data.full_name }}'s phone number is hidden.", v_else_if="data.full_name") + html.Span("Phone number is hidden.", v_else=True) + + with VBoxLayout(gap="0.5em"): + # We can also use v_show to conditionally render a field. Note that when using v_show, you won't + # be able to use v_else_if or v_else afterwards. There is a deeper discussion of the differences + # between v_if and v_show in the Vue.js documentation: https://vuejs.org/guide/essentials/conditional#v-if-vs-v-show. + vuetify.VBtn("Toggle Comments Field", click=self.view_model.toggle_comments) + InputField(v_show="data.show_comments_field", v_model="data.comments", type="textarea") + + def create_vm(self) -> None: + binding = TrameBinding(self.state) + + model = Model() + self.view_model = ViewModel(model, binding) diff --git a/examples/conditional_rendering/view_model.py b/examples/conditional_rendering/view_model.py new file mode 100644 index 0000000..695f998 --- /dev/null +++ b/examples/conditional_rendering/view_model.py @@ -0,0 +1,24 @@ +"""View model implementation for conditional rendering example.""" + +from nova.mvvm.interface import BindingInterface + +from .model import Model + + +class ViewModel: + """View model implementation for conditional rendering 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 toggle_comments(self) -> None: + self.model.toggle_comments() + # Since we are programmatically changing the model, we need to inform the view of the change. + self.update_form_data() + + 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) diff --git a/examples/data_selector/main.py b/examples/data_selector/main.py new file mode 100644 index 0000000..5d4c829 --- /dev/null +++ b/examples/data_selector/main.py @@ -0,0 +1,12 @@ +"""Runs the data selector example.""" + +from examples.data_selector.view import App + + +def main() -> None: + app = App() + app.server.start(open_browser=False) + + +if __name__ == "__main__": + main() diff --git a/examples/data_selector/model.py b/examples/data_selector/model.py new file mode 100644 index 0000000..e372970 --- /dev/null +++ b/examples/data_selector/model.py @@ -0,0 +1,21 @@ +"""Model implementation for data selector example.""" + +from typing import List + +from pydantic import BaseModel, Field + + +class FormData(BaseModel): + """Pydantic model for the form data.""" + + selected_files: List[str] = Field(default=[], title="Selected Files") + + +class Model: + """Model implementation for data selector example.""" + + def __init__(self) -> None: + self.form = FormData() + + def get_selected_files(self) -> List[str]: + return self.form.selected_files diff --git a/examples/data_selector/view.py b/examples/data_selector/view.py new file mode 100644 index 0000000..be46325 --- /dev/null +++ b/examples/data_selector/view.py @@ -0,0 +1,53 @@ +"""View for data selector example.""" + +import os + +from nova.mvvm.trame_binding import TrameBinding +from nova.trame import ThemedApp +from nova.trame.view.components import DataSelector +from nova.trame.view.layouts import VBoxLayout +from trame.widgets import html +from trame.widgets import vuetify3 as vuetify + +from .model import Model +from .view_model import ViewModel + + +class App(ThemedApp): + """View for data selector 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: + self.set_theme("CompactTheme") + + with super().create_ui() as layout: + with layout.content: + with vuetify.VCard(classes="mx-auto my-4 pa-1", max_width=600): + with VBoxLayout(height=400): + # Please note that this is a dangerous operation. You should ensure that you restrict this + # component to only expose files that are strictly necessary to making your application + # functional. + DataSelector( + v_model="data.selected_files", + directory=os.environ.get("HOME", "/"), + classes="mb-1", + ) + html.Span("You have selected {{ data.selected_files.length }} files.") + + def create_vm(self) -> None: + binding = TrameBinding(self.state) + + model = Model() + self.view_model = ViewModel(model, binding) diff --git a/examples/data_selector/view_model.py b/examples/data_selector/view_model.py new file mode 100644 index 0000000..ef17c92 --- /dev/null +++ b/examples/data_selector/view_model.py @@ -0,0 +1,26 @@ +"""View model implementation for data selector example.""" + +from typing import Any, Dict + +from nova.mvvm.interface import BindingInterface + +from .model import Model + + +class ViewModel: + """View model implementation for data selector 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.on_update) + + def on_update(self, results: Dict[str, Any]) -> None: + # This fires when the data selector is updated. You could run some process on the newly selected data or update + # other portions of the UI here as necessary. + print(f"Selected files updated: {self.model.get_selected_files()}") + + 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) diff --git a/examples/dynamic_pydantic_rules/main.py b/examples/dynamic_pydantic_rules/main.py new file mode 100644 index 0000000..b27e715 --- /dev/null +++ b/examples/dynamic_pydantic_rules/main.py @@ -0,0 +1,12 @@ +"""Runs the dynamic Pydantic rules example.""" + +from examples.dynamic_pydantic_rules.view import App + + +def main() -> None: + app = App() + app.server.start(open_browser=False) + + +if __name__ == "__main__": + main() diff --git a/examples/dynamic_pydantic_rules/model.py b/examples/dynamic_pydantic_rules/model.py new file mode 100644 index 0000000..1693b1c --- /dev/null +++ b/examples/dynamic_pydantic_rules/model.py @@ -0,0 +1,26 @@ +"""Model implementation for dynamic Pydantic rules example.""" + +from pydantic import BaseModel, Field, model_validator +from typing_extensions import Self + + +class FormData(BaseModel): + """Pydantic model for the form data.""" + + min: float = Field(default=0.0, title="Minimum") + max: float = Field(default=10.0, title="Maximum") + value: float = Field(default=5.0, title="Value") + + @model_validator(mode="after") + def validate_value_in_range(self) -> Self: + if self.value < self.min or self.value > self.max: + raise ValueError("Value must be between Minimum and Maximum") + + return self + + +class Model: + """Model implementation for dynamic Pydantic rules example.""" + + def __init__(self) -> None: + self.form = FormData() diff --git a/examples/dynamic_pydantic_rules/view.py b/examples/dynamic_pydantic_rules/view.py new file mode 100644 index 0000000..97b16b6 --- /dev/null +++ b/examples/dynamic_pydantic_rules/view.py @@ -0,0 +1,42 @@ +"""View for dynamic 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 GridLayout +from trame.widgets import vuetify3 as vuetify + +from .model import Model +from .view_model import ViewModel + + +class App(ThemedApp): + """View for dynamic 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 GridLayout(columns=3, gap="0.5em"): + InputField(v_model="data.min") + InputField(v_model="data.max") + InputField(v_model="data.value") + + def create_vm(self) -> None: + binding = TrameBinding(self.state) + + model = Model() + self.view_model = ViewModel(model, binding) diff --git a/examples/dynamic_pydantic_rules/view_model.py b/examples/dynamic_pydantic_rules/view_model.py new file mode 100644 index 0000000..ebf15c6 --- /dev/null +++ b/examples/dynamic_pydantic_rules/view_model.py @@ -0,0 +1,19 @@ +"""View model implementation for dynamic Pydantic rules example.""" + +from nova.mvvm.interface import BindingInterface + +from .model import Model + + +class ViewModel: + """View model implementation for dynamic 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) + + 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) diff --git a/examples/multitab/main.py b/examples/multitab/main.py new file mode 100644 index 0000000..40390db --- /dev/null +++ b/examples/multitab/main.py @@ -0,0 +1,12 @@ +"""Runs the events example.""" + +from examples.multitab.view.main_view import App + + +def main() -> None: + app = App() + app.server.start(open_browser=False) + + +if __name__ == "__main__": + main() diff --git a/examples/multitab/model/__init__.py b/examples/multitab/model/__init__.py new file mode 100644 index 0000000..8096f02 --- /dev/null +++ b/examples/multitab/model/__init__.py @@ -0,0 +1,4 @@ +from .input import InputModel +from .stats import StatsModel + +__all__ = ["InputModel", "StatsModel"] diff --git a/examples/multitab/model/input.py b/examples/multitab/model/input.py new file mode 100644 index 0000000..a1c1c49 --- /dev/null +++ b/examples/multitab/model/input.py @@ -0,0 +1,34 @@ +"""Model for input tab.""" + +from typing import List + +import numpy as np +from pydantic import BaseModel, Field, model_validator +from typing_extensions import Self + + +class InputFields(BaseModel): + """Pydantic class for input tab.""" + + count: int = Field(default=1, ge=1, title="Count") + min: int = Field(default=0, title="Minimum") + max: int = Field(default=10, title="Maximum") + + @model_validator(mode="after") + def validate_range(self) -> Self: + if self.max < self.min: + raise ValueError("The maximum must be greater than or equal to the minimum.") + + return self + + +class InputModel: + """Model for input tab.""" + + def __init__(self) -> None: + self.inputs = InputFields() + + def get_values(self) -> List[int]: + return np.random.randint( + low=int(self.inputs.min), high=int(self.inputs.max) + 1, size=int(self.inputs.count) + ).tolist() diff --git a/examples/multitab/model/stats.py b/examples/multitab/model/stats.py new file mode 100644 index 0000000..bdeb2a1 --- /dev/null +++ b/examples/multitab/model/stats.py @@ -0,0 +1,35 @@ +"""Model for stats tab.""" + +from typing import List, Optional + +import numpy as np +from pydantic import BaseModel, Field + + +class StatsFields(BaseModel): + """Pydantic class for stats tab.""" + + average: Optional[float] = Field(default=None, title="Average") + mean: Optional[float] = Field(default=None, title="Mean") + median: Optional[float] = Field(default=None, title="Median") + quantile: Optional[float] = Field(default=None, title="75th Percentile") + std: Optional[float] = Field(default=None, title="Standard Deviation") + variance: Optional[float] = Field(default=None, title="Variance") + values: List[int] = Field(default=[]) + + +class StatsModel: + """Model for stats tab.""" + + def __init__(self) -> None: + self.stats = StatsFields() + + def update(self, values: List[int]) -> None: + self.stats.values = values + + self.stats.average = np.average(self.stats.values) + self.stats.mean = np.mean(self.stats.values) + self.stats.median = np.median(self.stats.values) + self.stats.quantile = np.quantile(self.stats.values, 0.75) + self.stats.std = np.std(self.stats.values) + self.stats.variance = np.var(self.stats.values) diff --git a/examples/multitab/view/input.py b/examples/multitab/view/input.py new file mode 100644 index 0000000..469f8b6 --- /dev/null +++ b/examples/multitab/view/input.py @@ -0,0 +1,17 @@ +"""View for the input tab.""" + +from nova.trame.view.components import InputField +from nova.trame.view.layouts import GridLayout + + +class InputTab: + """View for the input tab.""" + + def __init__(self) -> None: + self.create_ui() + + def create_ui(self) -> None: + with GridLayout(columns=3, classes="mb-4", gap="0.5em"): + InputField("inputs.count") + InputField("inputs.min") + InputField("inputs.max") diff --git a/examples/multitab/view/main_view.py b/examples/multitab/view/main_view.py new file mode 100644 index 0000000..0c2e503 --- /dev/null +++ b/examples/multitab/view/main_view.py @@ -0,0 +1,52 @@ +"""Main view for multitab example.""" + +from nova.mvvm.trame_binding import TrameBinding +from nova.trame import ThemedApp +from trame.widgets import vuetify3 as vuetify + +from ..model import InputModel, StatsModel +from ..view_model import InputViewModel, MainViewModel, StatsViewModel +from .input import InputTab +from .stats import StatsTab + + +class App(ThemedApp): + """Main view for multitab example.""" + + def __init__(self) -> None: + super().__init__() + + self.create_vm() + self.main_vm.view_state_bind.connect("view_state") + self.input_vm.inputs_bind.connect("inputs") + self.stats_vm.stats_bind.connect("stats") + + self.input_vm.update_stats() + + self.create_ui() + + def create_ui(self) -> None: + with super().create_ui() as layout: + with layout.pre_content: + with vuetify.VTabs( + v_model="view_state.active_tab", classes="pl-8", update_modelValue="flushState('view_state')" + ): + vuetify.VTab("Input", value=0) + vuetify.VTab("Statistics", value=1) + + with layout.content: + with vuetify.VCard(classes="d-flex flex-column", style="height: calc(100vh - 150px);"): + with vuetify.VWindow(v_model="view_state.active_tab"): + with vuetify.VWindowItem(value=0): + InputTab() + with vuetify.VWindowItem(value=1): + StatsTab() + + def create_vm(self) -> None: + binding = TrameBinding(self.state) + + self.main_vm = MainViewModel(binding) + input_model = InputModel() + self.input_vm = InputViewModel(input_model, binding) + stats_model = StatsModel() + self.stats_vm = StatsViewModel(stats_model, binding) diff --git a/examples/multitab/view/stats.py b/examples/multitab/view/stats.py new file mode 100644 index 0000000..4a41e4b --- /dev/null +++ b/examples/multitab/view/stats.py @@ -0,0 +1,23 @@ +"""View for the stats tab.""" + +from nova.trame.view.components import InputField +from nova.trame.view.layouts import GridLayout +from trame.widgets import html + + +class StatsTab: + """View for the stats tab.""" + + def __init__(self) -> None: + self.create_ui() + + def create_ui(self) -> None: + html.P("Generated List: {{ stats.values }}", classes="mb-4") + + with GridLayout(columns=3, gap="0.5em"): + InputField("stats.average", readonly=True) + InputField("stats.mean", readonly=True) + InputField("stats.median", readonly=True) + InputField("stats.quantile", readonly=True) + InputField("stats.std", readonly=True) + InputField("stats.variance", readonly=True) diff --git a/examples/multitab/view_model/__init__.py b/examples/multitab/view_model/__init__.py new file mode 100644 index 0000000..aadc88a --- /dev/null +++ b/examples/multitab/view_model/__init__.py @@ -0,0 +1,5 @@ +from .input import InputViewModel +from .main import MainViewModel +from .stats import StatsViewModel + +__all__ = ["InputViewModel", "MainViewModel", "StatsViewModel"] diff --git a/examples/multitab/view_model/input.py b/examples/multitab/view_model/input.py new file mode 100644 index 0000000..cd3e292 --- /dev/null +++ b/examples/multitab/view_model/input.py @@ -0,0 +1,25 @@ +"""View model for the input tab.""" + +from typing import Any + +from nova.common.events import get_event +from nova.mvvm.interface import BindingInterface + +from ..model.input import InputModel + + +class InputViewModel: + """View model for the input tab.""" + + def __init__(self, model: InputModel, binding: BindingInterface) -> None: + self.model = model + + # self.on_update is called any time the view updates the binding. + self.inputs_bind = binding.new_bind(self.model.inputs, callback_after_update=self.update_stats) + + # To communicate changes in this view model to other view models, we can create an event. + self.update_event = get_event("inputs_updated") + + def update_stats(self, results: Any = None) -> None: + # Now, we can send an event to all listening view models. + self.update_event.send_sync(values=self.model.get_values()) diff --git a/examples/multitab/view_model/main.py b/examples/multitab/view_model/main.py new file mode 100644 index 0000000..ce2346f --- /dev/null +++ b/examples/multitab/view_model/main.py @@ -0,0 +1,18 @@ +"""Main view model.""" + +from nova.mvvm.interface import BindingInterface +from pydantic import BaseModel, Field + + +class ViewState(BaseModel): + """Pydantic class for view state.""" + + active_tab: int = Field(default=0) + + +class MainViewModel: + """Main view model.""" + + def __init__(self, binding: BindingInterface) -> None: + self.view_state = ViewState() + self.view_state_bind = binding.new_bind(self.view_state) diff --git a/examples/multitab/view_model/stats.py b/examples/multitab/view_model/stats.py new file mode 100644 index 0000000..2177f49 --- /dev/null +++ b/examples/multitab/view_model/stats.py @@ -0,0 +1,25 @@ +"""View model for the stats tab.""" + +from typing import Any, List, Optional + +from nova.common.events import get_event +from nova.mvvm.interface import BindingInterface + +from ..model.stats import StatsModel + + +class StatsViewModel: + """View model for the stats tab.""" + + def __init__(self, model: StatsModel, binding: BindingInterface) -> None: + self.model = model + + self.stats_bind = binding.new_bind(self.model.stats) + + # To listen to an event sent from other view models, we can call connect. + self.update_event = get_event("inputs_updated") + self.update_event.connect(self.on_inputs_update) + + def on_inputs_update(self, sender: Any = None, values: Optional[List[int]] = None, **kwargs: Any): + self.model.update(values) + self.stats_bind.update_in_view(self.model.stats) diff --git a/examples/ornl/neutron_data_selector/main.py b/examples/ornl/neutron_data_selector/main.py new file mode 100644 index 0000000..43258c7 --- /dev/null +++ b/examples/ornl/neutron_data_selector/main.py @@ -0,0 +1,12 @@ +"""Runs the neutron data selector example.""" + +from examples.ornl.neutron_data_selector.view import App + + +def main() -> None: + app = App() + app.server.start(open_browser=False) + + +if __name__ == "__main__": + main() diff --git a/examples/ornl/neutron_data_selector/model.py b/examples/ornl/neutron_data_selector/model.py new file mode 100644 index 0000000..d99ccd3 --- /dev/null +++ b/examples/ornl/neutron_data_selector/model.py @@ -0,0 +1,21 @@ +"""Model implementation for neutron data selector example.""" + +from typing import List + +from pydantic import BaseModel, Field + + +class FormData(BaseModel): + """Pydantic model for the form data.""" + + selected_files: List[str] = Field(default=[], title="Selected Files") + + +class Model: + """Model implementation for neutron data selector example.""" + + def __init__(self) -> None: + self.form = FormData() + + def get_selected_files(self) -> List[str]: + return self.form.selected_files diff --git a/examples/ornl/neutron_data_selector/view.py b/examples/ornl/neutron_data_selector/view.py new file mode 100644 index 0000000..7080fcb --- /dev/null +++ b/examples/ornl/neutron_data_selector/view.py @@ -0,0 +1,54 @@ +"""View for neutron data selector example.""" + +from nova.mvvm.trame_binding import TrameBinding +from nova.trame import ThemedApp +from nova.trame.view.components.ornl import NeutronDataSelector +from nova.trame.view.layouts import VBoxLayout +from trame.widgets import html +from trame.widgets import vuetify3 as vuetify + +from .model import Model +from .view_model import ViewModel + + +class App(ThemedApp): + """View for neutron data selector 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: + self.set_theme("CompactTheme") + + with super().create_ui() as layout: + with layout.content: + with vuetify.VCard(classes="mx-auto my-4 pa-1", max_width=600): + with VBoxLayout(height=400): + # Please note that this example will not work locally if /HFIR and /SNS don't exist on your + # development machine. You can either simulate or try to mount them depending on your + # circumstances. + NeutronDataSelector( + v_model="data.selected_files", + base_paths=["/HFIR", "/SNS"], + classes="mb-1", + # You can uncomment the below lines to restrict data selection to a specific instrument. + # facility="SNS", + # instrument="TOPAZ", + ) + html.Span("You have selected {{ data.selected_files.length }} files.") + + def create_vm(self) -> None: + binding = TrameBinding(self.state) + + model = Model() + self.view_model = ViewModel(model, binding) diff --git a/examples/ornl/neutron_data_selector/view_model.py b/examples/ornl/neutron_data_selector/view_model.py new file mode 100644 index 0000000..cb8790d --- /dev/null +++ b/examples/ornl/neutron_data_selector/view_model.py @@ -0,0 +1,26 @@ +"""View model implementation for neutron data selector example.""" + +from typing import Any, Dict + +from nova.mvvm.interface import BindingInterface + +from .model import Model + + +class ViewModel: + """View model implementation for neutron data selector 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.on_update) + + def on_update(self, results: Dict[str, Any]) -> None: + # This fires when the data selector is updated. You could run some process on the newly selected data or update + # other portions of the UI here as necessary. + print(f"Selected files updated: {self.model.get_selected_files()}") + + 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) diff --git a/examples/ornl/oncat/main.py b/examples/ornl/oncat/main.py new file mode 100644 index 0000000..e3d6899 --- /dev/null +++ b/examples/ornl/oncat/main.py @@ -0,0 +1,12 @@ +"""Runs the ONCat example.""" + +from examples.ornl.oncat.view import App + + +def main() -> None: + app = App() + app.server.start(open_browser=False) + + +if __name__ == "__main__": + main() diff --git a/examples/ornl/oncat/model.py b/examples/ornl/oncat/model.py new file mode 100644 index 0000000..966e167 --- /dev/null +++ b/examples/ornl/oncat/model.py @@ -0,0 +1,21 @@ +"""Model implementation for ONCat example.""" + +from typing import List + +from pydantic import BaseModel, Field + + +class FormData(BaseModel): + """Pydantic model for the form data.""" + + selected_files: List[str] = Field(default=[], title="Selected Files") + + +class Model: + """Model implementation for ONCat example.""" + + def __init__(self) -> None: + self.form = FormData() + + def get_selected_files(self) -> List[str]: + return self.form.selected_files diff --git a/examples/ornl/oncat/view.py b/examples/ornl/oncat/view.py new file mode 100644 index 0000000..f3efaba --- /dev/null +++ b/examples/ornl/oncat/view.py @@ -0,0 +1,62 @@ +"""View for ONCat example.""" + +from nova.mvvm.trame_binding import TrameBinding +from nova.trame import ThemedApp +from nova.trame.view.components.ornl import NeutronDataSelector +from nova.trame.view.layouts import VBoxLayout +from trame.widgets import html +from trame.widgets import vuetify3 as vuetify + +from .model import Model +from .view_model import ViewModel + + +class App(ThemedApp): + """View for ONCat 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: + self.set_theme("CompactTheme") + + with super().create_ui() as layout: + with layout.content: + with vuetify.VCard(classes="mx-auto my-4 pa-1", max_width=600): + with VBoxLayout(height=400): + # You must set the ONCAT_CLIENT_ID and ONCAT_CLIENT_SECRET environment variables for this to + # work locally. If you don't know how to set them, then please visit https://oncat.ornl.gov/ and + # review the documentation or reach out to ONCat support for assistance. When this runs in + # production, we will set the environment for you automatically. + NeutronDataSelector( + v_model="data.selected_files", + base_paths=["/HFIR", "/SNS"], + classes="mb-1", + # This triggers the NeutronDataSelector to pull data from ONCat. + data_source="oncat", + # ONCat by default will return a large amount of metadata for each file. This can be + # extremely slow, so we strongly recommend setting the projection attribute to restrict the + # metadata to only what you need. This parameter matches the ONCat projection parameter + # exactly. + projection=["indexed.run_number"], + # You can uncomment the below lines to restrict data selection to a specific instrument. + # facility="SNS", + # instrument="TOPAZ", + ) + html.Span("You have selected {{ data.selected_files.length }} files.") + + def create_vm(self) -> None: + binding = TrameBinding(self.state) + + model = Model() + self.view_model = ViewModel(model, binding) diff --git a/examples/ornl/oncat/view_model.py b/examples/ornl/oncat/view_model.py new file mode 100644 index 0000000..1c02b79 --- /dev/null +++ b/examples/ornl/oncat/view_model.py @@ -0,0 +1,26 @@ +"""View model implementation for ONCat example.""" + +from typing import Any, Dict + +from nova.mvvm.interface import BindingInterface + +from .model import Model + + +class ViewModel: + """View model implementation for ONCat 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.on_update) + + def on_update(self, results: Dict[str, Any]) -> None: + # This fires when the data selector is updated. You could run some process on the newly selected data or update + # other portions of the UI here as necessary. + print(f"Selected files updated: {self.model.get_selected_files()}") + + 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) diff --git a/examples/run_galaxy_tool/main.py b/examples/run_galaxy_tool/main.py new file mode 100644 index 0000000..8d4dae3 --- /dev/null +++ b/examples/run_galaxy_tool/main.py @@ -0,0 +1,12 @@ +"""Runs the Galaxy tool example.""" + +from examples.run_galaxy_tool.view import App + + +def main() -> None: + app = App() + app.server.start(open_browser=False) + + +if __name__ == "__main__": + main() diff --git a/examples/run_galaxy_tool/model.py b/examples/run_galaxy_tool/model.py new file mode 100644 index 0000000..34aa4bd --- /dev/null +++ b/examples/run_galaxy_tool/model.py @@ -0,0 +1,76 @@ +"""Model implementation for Galaxy tool example.""" + +from enum import Enum +from io import BytesIO +from typing import List, Tuple + +from nova.galaxy import Parameters, Tool +from nova.galaxy.interfaces import BasicTool +from PIL import Image +from PIL.ImageStat import Stat +from pydantic import BaseModel, Field + + +class FractalOptions(str, Enum): + """Defines options for the fractal_type field.""" + + mandelbrot = "Mandelbrot Set" + julia = "Julia Set Animation" + random = "Random Walk" + markus = "Markus-Lyapunov Fractal" + + +class FormData(BaseModel): + """Pydantic model for the form data.""" + + fractal_type: FractalOptions = Field(default=FractalOptions.mandelbrot, title="Type") + + +class ImageStatistics(BaseModel): + """Pydantic model for holding image statistics.""" + + count: List[int] = Field(default=[]) + extrema: List[Tuple[int, int]] = Field(default=[]) + mean: List[float] = Field(default=[]) + median: List[int] = Field(default=[]) + + +class Model: + """Model implementation for Galaxy tool example.""" + + def __init__(self) -> None: + self.form = FormData() + self.stats = ImageStatistics() + + +class FractalsTool(BasicTool): + """Class that prepares IPS Fastran tool.""" + + def __init__(self, model: Model) -> None: + super().__init__() + self.model = model + + def prepare_tool(self) -> Tuple[Tool, Parameters]: + # Pass the fractal type dropdown value to the tool. The "option" name comes from the tool XML file. + tool_params = Parameters() + tool_params.add_input(name="option", value=self.model.form.fractal_type) + + # Create the tool. + self.tool = Tool(id="neutrons_fractal") + + return self.tool, tool_params + + def compute_stats(self) -> None: + outputs = self.tool.get_results() + output = outputs.get_dataset("output").get_content() + + img = Image.open(BytesIO(output)) + stat = Stat(img) + + self.model.stats.count = stat.count + self.model.stats.extrema = stat.extrema + self.model.stats.mean = [round(mean, 3) for mean in stat.mean] + self.model.stats.median = stat.median + + def get_results(self, tool: Tool) -> None: + pass diff --git a/examples/run_galaxy_tool/view.py b/examples/run_galaxy_tool/view.py new file mode 100644 index 0000000..49bc7e4 --- /dev/null +++ b/examples/run_galaxy_tool/view.py @@ -0,0 +1,67 @@ +"""View for Galaxy tool example.""" + +from nova.mvvm.trame_binding import TrameBinding +from nova.trame import ThemedApp +from nova.trame.view.components import ExecutionButtons, InputField, ProgressBar, ToolOutputWindows +from nova.trame.view.layouts import VBoxLayout +from trame.widgets import html +from trame.widgets import vuetify3 as vuetify + +from .model import Model +from .view_model import ViewModel + + +class App(ThemedApp): + """View for Galaxy tool 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") + self.view_model.stats_bind.connect("stats") + # 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 pa-1", max_width=600): + with VBoxLayout(gap="0.5em"): + InputField(v_model="data.fractal_type", type="select") + + # These components add UI components for monitoring the job automatically. + # The "fractals" string needs to be consistent with the ToolRunner in the view model. + ProgressBar("fractals") + ToolOutputWindows("fractals") + + with html.Div(v_if="stats.count.length > 0"): + html.P( + "PIL computed {{ stats.count.length }} bands for the generated fractal image.", + classes="mb-2", + ) + html.P( + ( + "Band {{ index + 1 }} Stats: " + "Count={{ stats.count[index] }}, " + "Extrema={{ stats.extrema[index] }}, " + "Mean={{ stats.mean[index] }}, " + "Median={{ stats.median[index] }}" + ), + v_for="(_, index) in stats.count.length", + ) + + with layout.post_content: + # This adds UI components for running and stopping the tool. + ExecutionButtons("fractals") + + def create_vm(self) -> None: + binding = TrameBinding(self.state) + + model = Model() + self.view_model = ViewModel(model, binding) diff --git a/examples/run_galaxy_tool/view_model.py b/examples/run_galaxy_tool/view_model.py new file mode 100644 index 0000000..dda8e4a --- /dev/null +++ b/examples/run_galaxy_tool/view_model.py @@ -0,0 +1,53 @@ +"""View model implementation for Galaxy tool example.""" + +import os +from typing import Any, Dict + +from blinker import signal +from nova.common.job import WorkState +from nova.common.signals import Signal, get_signal_id +from nova.galaxy.tool_runner import ToolRunner +from nova.mvvm.interface import BindingInterface + +from .model import FractalsTool, Model + + +class ViewModel: + """View model implementation for Galaxy tool example.""" + + def __init__(self, model: Model, binding: BindingInterface) -> None: + # You must set these environment variables for this example to run. Please see the tutorial at + # https://nova.ornl.gov/tutorial for instructions on how to set these environment variables. Note that when a + # NOVA application is deployed to Galaxy, we will automatically set these environment variables for you. + galaxy_url = os.environ.get("GALAXY_URL", "https://calvera-test.ornl.gov") + galaxy_api_key = os.environ.get("GALAXY_API_KEY", None) + if not galaxy_url or not galaxy_api_key: + raise EnvironmentError("GALAXY_URL and GALAXY_API_KEY must be set to run this example.") + + self.model = model + + self.form_data_bind = binding.new_bind(self.model.form) + self.stats_bind = binding.new_bind(self.model.stats) + + # Using the ToolRunner will allow the tool's progress to be automatically communicated to the view. + self.tool = FractalsTool(model) + # The "fractals" string needs to be consistent with the view components. + self.tool_runner = ToolRunner("fractals", self.tool, self.store_factory, galaxy_url, galaxy_api_key) + + # Now we need to listen for when the tool finished running. + self.completion_signal = signal(get_signal_id("fractals", Signal.PROGRESS)) + self.completion_signal.connect(self.on_completion, weak=False) + + async def on_completion(self, _sender: Any, state: WorkState, details: Dict[str, Any]) -> None: + if state == WorkState.FINISHED: + self.tool.compute_stats() + self.stats_bind.update_in_view(self.model.stats) + + def store_factory(self) -> str: + # This will cause a history named "fractals" to be created on Galaxy to store the tool's outputs, errors, and + # results. + return "fractals" + + 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) diff --git a/examples/run_galaxy_workflow/main.py b/examples/run_galaxy_workflow/main.py new file mode 100644 index 0000000..fa521ba --- /dev/null +++ b/examples/run_galaxy_workflow/main.py @@ -0,0 +1,12 @@ +"""Runs the Galaxy workflow example.""" + +from examples.run_galaxy_workflow.view import App + + +def main() -> None: + app = App() + app.server.start(open_browser=False) + + +if __name__ == "__main__": + main() diff --git a/examples/run_galaxy_workflow/model.py b/examples/run_galaxy_workflow/model.py new file mode 100644 index 0000000..dadc181 --- /dev/null +++ b/examples/run_galaxy_workflow/model.py @@ -0,0 +1,60 @@ +"""Model implementation for Galaxy workflow example.""" + +from enum import Enum +from typing import Callable + +from nova.galaxy.connection import Connection +from nova.galaxy.workflow import Workflow, WorkflowParameters +from pydantic import BaseModel, Field, field_validator + + +class FractalOptions(str, Enum): + """Defines options for the fractal_type field.""" + + mandelbrot = "Mandelbrot Set" + julia = "Julia Set Animation" + random = "Random Walk" + markus = "Markus-Lyapunov Fractal" + + +class FormData(BaseModel): + """Pydantic model for the form data.""" + + input_path: str = Field(default="", title="Analysis Cluster File to Ingest") + + @field_validator("input_path", mode="after") + @classmethod + def validate_input_path(cls, input_path: str) -> str: + if not input_path.startswith("/SNS") and not input_path.startswith("/HFIR"): + raise ValueError("File path must start with /SNS or /HFIR.") + + return input_path + + +class Model: + """Model implementation for Galaxy workflow example.""" + + def __init__(self) -> None: + self.form = FormData() + + def run_workflow(self, galaxy_url: str, galaxy_api_key: str, progress: Callable) -> None: + # You can call the progress method to inform the view model as progress is made in this function if needed. + nova_instance = Connection(galaxy_url, galaxy_api_key) + with nova_instance.connect() as conn: + data_store = conn.get_data_store("workflow_test") + + # This allows us to retrieve a workflow by it's name for readability. + workflows = conn.galaxy_instance.workflows.get_workflows( + name="simple_test_workflow_with_dataset", published=True + ) + workflow_id = workflows[0]["id"] + workflow = Workflow(id=workflow_id) + + # We can add parameters similar to tools, but we must make sure that we label which step in the workflow the + # parameter is for. + params = WorkflowParameters() + params.add_workflow_input("0", True) + params.add_step_param("1", "params|ingest_mode", "file") + params.add_step_param("1", "params|filepath", self.form.input_path) + + workflow.run(data_store=data_store, params=params, wait=True) diff --git a/examples/run_galaxy_workflow/view.py b/examples/run_galaxy_workflow/view.py new file mode 100644 index 0000000..cf2d8d3 --- /dev/null +++ b/examples/run_galaxy_workflow/view.py @@ -0,0 +1,49 @@ +"""View for Galaxy workflow 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 HBoxLayout +from trame.widgets import html +from trame.widgets import vuetify3 as vuetify + +from .model import Model +from .view_model import ViewModel + + +class App(ThemedApp): + """View for Galaxy workflow 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") + self.view_model.view_state_bind.connect("state") + # 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 pa-1", max_width=600): + with HBoxLayout(classes="mb-2", gap="0.5em", valign="center"): + InputField(v_model="data.input_path") + with vuetify.VBtn( + disabled=("state.running || state.errors",), click=self.view_model.run_workflow + ): + vuetify.VProgressCircular(v_if="state.running", indeterminate=True, size=16) + html.Span("Run Workflow", v_else=True) + + html.Span("Workflow Completed!", v_if="data.complete") + + def create_vm(self) -> None: + binding = TrameBinding(self.state) + + model = Model() + self.view_model = ViewModel(model, binding) diff --git a/examples/run_galaxy_workflow/view_model.py b/examples/run_galaxy_workflow/view_model.py new file mode 100644 index 0000000..91498c8 --- /dev/null +++ b/examples/run_galaxy_workflow/view_model.py @@ -0,0 +1,74 @@ +"""View model implementation for Galaxy workflow example.""" + +import os +from functools import partial +from typing import Any, Dict + +from nova.mvvm.interface import BindingInterface +from pydantic import BaseModel, Field + +from .model import Model + + +class ViewState(BaseModel): + """Pydantic model for view state.""" + + complete: bool = Field(default=False) + errors: bool = Field(default=True) + running: bool = Field(default=False) + + +class ViewModel: + """View model implementation for Galaxy workflow example.""" + + def __init__(self, model: Model, binding: BindingInterface) -> None: + # You must set these environment variables for this example to run. Please see the tutorial at + # https://nova.ornl.gov/tutorial for instructions on how to set these environment variables. Note that when a + # NOVA application is deployed to Galaxy, we will automatically set these environment variables for you. + self.galaxy_url = os.environ.get("GALAXY_URL", "https://calvera-test.ornl.gov") + self.galaxy_api_key = os.environ.get("GALAXY_API_KEY", None) + if not self.galaxy_url or not self.galaxy_api_key: + raise EnvironmentError("GALAXY_URL and GALAXY_API_KEY must be set to run this example.") + + self.model = model + self.binding = binding + + self.form_data_bind = binding.new_bind(self.model.form, callback_after_update=self.on_form_update) + + self.view_state = ViewState() + self.view_state_bind = binding.new_bind(self.view_state) + + def on_completion(self) -> None: + # Here, we can fetch and process outputs similar to the Galaxy tool example. See the TOPAZ Reduction GUI for a + # complete example including results processing for workflows. + # https://code.ornl.gov/ndip/tool-sources/single-crystal-diffraction/topaz-data-reduction + self.view_state.complete = True + self.view_state.running = False + self.update_view() + + def on_form_update(self, results: Dict[str, Any]) -> None: + # This blocks running the workflow in the form inputs are invalid. + if results.get("errored", []): + self.view_state.errors = True + else: + self.view_state.errors = False + + self.update_view() + + def run_workflow(self) -> None: + self.view_state.complete = False + self.view_state.running = True + self.update_view() + + # This runs the workflow in a background thread to avoid blocking the Trame server. + worker = self.binding.new_worker(partial(self.model.run_workflow, self.galaxy_url, self.galaxy_api_key)) + worker.connect_finished(self.on_completion) + worker.connect_progress(lambda *args: None) + worker.start() + + 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) + + def update_view(self) -> None: + self.view_state_bind.update_in_view(self.view_state) diff --git a/examples/vtk/main.py b/examples/vtk/main.py new file mode 100644 index 0000000..140090e --- /dev/null +++ b/examples/vtk/main.py @@ -0,0 +1,12 @@ +"""Runs the VTK example.""" + +from examples.vtk.view import App + + +def main() -> None: + app = App() + app.server.start(open_browser=False) + + +if __name__ == "__main__": + main() diff --git a/examples/vtk/model.py b/examples/vtk/model.py new file mode 100644 index 0000000..f6899d5 --- /dev/null +++ b/examples/vtk/model.py @@ -0,0 +1,22 @@ +"""Model implementation for VTK example.""" + +from vtkmodules.vtkFiltersSources import vtkConeSource +from vtkmodules.vtkRenderingCore import vtkActor, vtkPolyDataMapper + + +class Model: + """Model implementation for Plotly example.""" + + def __init__(self) -> None: + pass + + def get_actor(self) -> vtkActor: + cone_source = vtkConeSource() + cone_source.SetResolution(10) + + mapper = vtkPolyDataMapper() + actor = vtkActor() + mapper.SetInputConnection(cone_source.GetOutputPort()) + actor.SetMapper(mapper) + + return actor diff --git a/examples/vtk/view.py b/examples/vtk/view.py new file mode 100644 index 0000000..fec329d --- /dev/null +++ b/examples/vtk/view.py @@ -0,0 +1,58 @@ +"""View for VTK example.""" + +import vtkmodules.vtkRenderingOpenGL2 # noqa +from nova.mvvm.trame_binding import TrameBinding +from nova.trame import ThemedApp +from nova.trame.view.layouts import VBoxLayout +from trame.widgets import vtk +from trame.widgets import vuetify3 as vuetify + +# VTK factory initialization +from vtkmodules.vtkInteractionStyle import vtkInteractorStyleSwitch # noqa +from vtkmodules.vtkRenderingCore import vtkRenderer, vtkRenderWindow, vtkRenderWindowInteractor + +from .model import Model +from .view_model import ViewModel + + +class App(ThemedApp): + """View for VTK example.""" + + def __init__(self) -> None: + super().__init__() + + self.create_vm() + + self.create_renderer() + self.create_ui() + + def create_renderer(self) -> None: + renderer = vtkRenderer() + self.render_window = vtkRenderWindow() + self.render_window.AddRenderer(renderer) + self.render_window.OffScreenRenderingOn() # Prevent popup window + + render_window_interactor = vtkRenderWindowInteractor() + render_window_interactor.SetRenderWindow(self.render_window) + render_window_interactor.GetInteractorStyle().SetCurrentStyleToTrackballCamera() + + renderer.AddActor(self.view_model.get_actor()) + renderer.ResetCamera() + self.render_window.Render() + + def create_ui(self) -> None: + with super().create_ui() as layout: + with layout.content: + with vuetify.VCard( + classes="d-flex flex-column mx-auto my-4", max_width=1200, style="height: calc(100vh - 120px);" + ): + with VBoxLayout(height="100%", gap="0.5em"): + # Choosing vtkRemoteView will enforce server-side rendering. We strongly recommend this for + # anything non-trivial in size. + vtk.VtkRemoteView(self.render_window) + + def create_vm(self) -> None: + binding = TrameBinding(self.state) + + model = Model() + self.view_model = ViewModel(model, binding) diff --git a/examples/vtk/view_model.py b/examples/vtk/view_model.py new file mode 100644 index 0000000..2f9267a --- /dev/null +++ b/examples/vtk/view_model.py @@ -0,0 +1,16 @@ +"""View model implementation for VTK example.""" + +from nova.mvvm.interface import BindingInterface +from vtkmodules.vtkRenderingCore import vtkActor + +from .model import Model + + +class ViewModel: + """View model implementation for VTK example.""" + + def __init__(self, model: Model, binding: BindingInterface) -> None: + self.model = model + + def get_actor(self) -> vtkActor: + return self.model.get_actor() diff --git a/poetry.lock b/poetry.lock index 4a0fabb..73f79c4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -207,6 +207,40 @@ docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphi tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +description = "Backport of asyncio.Runner, a context manager that controls event loop life cycle." +optional = false +python-versions = "<3.11,>=3.8" +groups = ["main"] +markers = "python_version == \"3.10\"" +files = [ + {file = "backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5"}, + {file = "backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162"}, +] + +[[package]] +name = "bioblend" +version = "1.6.0" +description = "Library for interacting with the Galaxy API" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "bioblend-1.6.0-py3-none-any.whl", hash = "sha256:7127e7b654d75595836d7910a197f690f11af2eed94dea05b440ee3b03cc5f86"}, + {file = "bioblend-1.6.0.tar.gz", hash = "sha256:b787f1ee30407645b0a292a1adbb96ac0880b54455b612af1bb6123ec9aa8739"}, +] + +[package.dependencies] +PyYAML = "*" +requests = ">=2.20.0" +requests-toolbelt = ">=0.5.1,<0.9.0 || >0.9.0" +tuspy = "*" + +[package.extras] +testing = ["pytest"] + [[package]] name = "blinker" version = "1.9.0" @@ -332,6 +366,21 @@ files = [ {file = "charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"}, ] +[[package]] +name = "click" +version = "8.2.1" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, + {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "colorama" version = "0.4.6" @@ -339,7 +388,7 @@ description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["main"] -markers = "sys_platform == \"win32\"" +markers = "sys_platform == \"win32\" or platform_system == \"Windows\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -462,6 +511,24 @@ optimize = ["orjson"] static = ["flake8 (>=7.1.0,<7.2.0)", "flake8-pyproject (>=1.2.3,<1.3.0)", "pydantic (>=2.10.0,<2.11.0)"] test = ["pytest (>=8.3.0,<8.4.0)", "pytest-benchmark (>=5.1.0,<5.2.0)", "pytest-cov (>=6.0.0,<6.1.0)", "python-dotenv (>=1.0.0,<1.1.0)"] +[[package]] +name = "deprecated" +version = "1.2.18" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +groups = ["main"] +files = [ + {file = "Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec"}, + {file = "deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d"}, +] + +[package.dependencies] +wrapt = ">=1.10,<2" + +[package.extras] +dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools ; python_version >= \"3.12\"", "tox"] + [[package]] name = "distlib" version = "0.4.0" @@ -760,6 +827,18 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "joblib" +version = "1.5.2" +description = "Lightweight pipelining with Python functions" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "joblib-1.5.2-py3-none-any.whl", hash = "sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241"}, + {file = "joblib-1.5.2.tar.gz", hash = "sha256:3faa5c39054b2f03ca547da9b2f52fde67c06240c31853f306aea97f13647b55"}, +] + [[package]] name = "jsonschema" version = "4.25.0" @@ -1420,6 +1499,32 @@ files = [ fast = ["fastnumbers (>=2.0.0)"] icu = ["PyICU (>=1.0.0)"] +[[package]] +name = "nltk" +version = "3.9.1" +description = "Natural Language Toolkit" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "nltk-3.9.1-py3-none-any.whl", hash = "sha256:4fa26829c5b00715afe3061398a8989dc643b92ce7dd93fb4585a70930d168a1"}, + {file = "nltk-3.9.1.tar.gz", hash = "sha256:87d127bd3de4bd89a4f81265e5fa59cb1b199b27440175370f7417d2bc7ae868"}, +] + +[package.dependencies] +click = "*" +joblib = "*" +regex = ">=2021.8.3" +tqdm = "*" + +[package.extras] +all = ["matplotlib", "numpy", "pyparsing", "python-crfsuite", "requests", "scikit-learn", "scipy", "twython"] +corenlp = ["requests"] +machine-learning = ["numpy", "python-crfsuite", "scikit-learn", "scipy"] +plot = ["matplotlib"] +tgrep = ["pyparsing"] +twitter = ["twython"] + [[package]] name = "nodeenv" version = "1.9.1" @@ -1447,6 +1552,25 @@ files = [ blinker = ">=1.9.0,<2.0.0" pydantic = ">=2.11.4,<3.0.0" +[[package]] +name = "nova-galaxy" +version = "0.11.2" +description = "Utilties for accessing the ORNL Galaxy instance" +optional = false +python-versions = "<4.0,>=3.10" +groups = ["main"] +files = [ + {file = "nova_galaxy-0.11.2-py3-none-any.whl", hash = "sha256:a2dfcd414deec0b22b75b9000071bbe7fcd87400c53963d9baddce5d72a281d1"}, +] + +[package.dependencies] +bioblend = ">=1.5.0" +blinker = ">=1.9.0" +deprecated = ">=1.2.18" +nova-common = ">=0.2.0" +pytest-asyncio = ">=0.26.0" +tomli = ">=2.0.2" + [[package]] name = "nova-mvvm" version = "0.12.0" @@ -1470,13 +1594,13 @@ pyqt6 = ["pyqt6"] [[package]] name = "nova-trame" -version = "0.26.1" +version = "0.27.0" description = "A Python Package for injecting curated themes and custom components into Trame applications" optional = false python-versions = "<4.0,>=3.10" groups = ["main"] files = [ - {file = "nova_trame-0.26.1-py3-none-any.whl", hash = "sha256:cc709f0ff7637ed7bca6ef0aadb71aa9e3896e4d25b940a2e00dba8097d6668b"}, + {file = "nova_trame-0.27.0-py3-none-any.whl", hash = "sha256:b8a3884141029504a81f5d6b3b2d5cd5c30ff1169bf166334c5dec7e96c8dd8b"}, ] [package.dependencies] @@ -2118,6 +2242,27 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-asyncio" +version = "1.2.0" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99"}, + {file = "pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57"}, +] + +[package.dependencies] +backports-asyncio-runner = {version = ">=1.1,<2", markers = "python_version < \"3.11\""} +pytest = ">=8.2,<9" +typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -2213,6 +2358,103 @@ attrs = ">=22.2.0" rpds-py = ">=0.7.0" typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} +[[package]] +name = "regex" +version = "2025.9.1" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "regex-2025.9.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c5aa2a6a73bf218515484b36a0d20c6ad9dc63f6339ff6224147b0e2c095ee55"}, + {file = "regex-2025.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c2ff5c01d5e47ad5fc9d31bcd61e78c2fa0068ed00cab86b7320214446da766"}, + {file = "regex-2025.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d49dc84e796b666181de8a9973284cad6616335f01b52bf099643253094920fc"}, + {file = "regex-2025.9.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9914fe1040874f83c15fcea86d94ea54091b0666eab330aaab69e30d106aabe"}, + {file = "regex-2025.9.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e71bceb3947362ec5eabd2ca0870bb78eae4edfc60c6c21495133c01b6cd2df4"}, + {file = "regex-2025.9.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67a74456f410fe5e869239ee7a5423510fe5121549af133809d9591a8075893f"}, + {file = "regex-2025.9.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c3b96ed0223b32dbdc53a83149b6de7ca3acd5acd9c8e64b42a166228abe29c"}, + {file = "regex-2025.9.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:113d5aa950f428faf46fd77d452df62ebb4cc6531cb619f6cc30a369d326bfbd"}, + {file = "regex-2025.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fcdeb38de4f7f3d69d798f4f371189061446792a84e7c92b50054c87aae9c07c"}, + {file = "regex-2025.9.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4bcdff370509164b67a6c8ec23c9fb40797b72a014766fdc159bb809bd74f7d8"}, + {file = "regex-2025.9.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:7383efdf6e8e8c61d85e00cfb2e2e18da1a621b8bfb4b0f1c2747db57b942b8f"}, + {file = "regex-2025.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1ec2bd3bdf0f73f7e9f48dca550ba7d973692d5e5e9a90ac42cc5f16c4432d8b"}, + {file = "regex-2025.9.1-cp310-cp310-win32.whl", hash = "sha256:9627e887116c4e9c0986d5c3b4f52bcfe3df09850b704f62ec3cbf177a0ae374"}, + {file = "regex-2025.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:94533e32dc0065eca43912ee6649c90ea0681d59f56d43c45b5bcda9a740b3dd"}, + {file = "regex-2025.9.1-cp310-cp310-win_arm64.whl", hash = "sha256:a874a61bb580d48642ffd338570ee24ab13fa023779190513fcacad104a6e251"}, + {file = "regex-2025.9.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e5bcf112b09bfd3646e4db6bf2e598534a17d502b0c01ea6550ba4eca780c5e6"}, + {file = "regex-2025.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:67a0295a3c31d675a9ee0238d20238ff10a9a2fdb7a1323c798fc7029578b15c"}, + {file = "regex-2025.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea8267fbadc7d4bd7c1301a50e85c2ff0de293ff9452a1a9f8d82c6cafe38179"}, + {file = "regex-2025.9.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aeff21de7214d15e928fb5ce757f9495214367ba62875100d4c18d293750cc1"}, + {file = "regex-2025.9.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d89f1bbbbbc0885e1c230f7770d5e98f4f00b0ee85688c871d10df8b184a6323"}, + {file = "regex-2025.9.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca3affe8ddea498ba9d294ab05f5f2d3b5ad5d515bc0d4a9016dd592a03afe52"}, + {file = "regex-2025.9.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91892a7a9f0a980e4c2c85dd19bc14de2b219a3a8867c4b5664b9f972dcc0c78"}, + {file = "regex-2025.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e1cb40406f4ae862710615f9f636c1e030fd6e6abe0e0f65f6a695a2721440c6"}, + {file = "regex-2025.9.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:94f6cff6f7e2149c7e6499a6ecd4695379eeda8ccbccb9726e8149f2fe382e92"}, + {file = "regex-2025.9.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6c0226fb322b82709e78c49cc33484206647f8a39954d7e9de1567f5399becd0"}, + {file = "regex-2025.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a12f59c7c380b4fcf7516e9cbb126f95b7a9518902bcf4a852423ff1dcd03e6a"}, + {file = "regex-2025.9.1-cp311-cp311-win32.whl", hash = "sha256:49865e78d147a7a4f143064488da5d549be6bfc3f2579e5044cac61f5c92edd4"}, + {file = "regex-2025.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:d34b901f6f2f02ef60f4ad3855d3a02378c65b094efc4b80388a3aeb700a5de7"}, + {file = "regex-2025.9.1-cp311-cp311-win_arm64.whl", hash = "sha256:47d7c2dab7e0b95b95fd580087b6ae196039d62306a592fa4e162e49004b6299"}, + {file = "regex-2025.9.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:84a25164bd8dcfa9f11c53f561ae9766e506e580b70279d05a7946510bdd6f6a"}, + {file = "regex-2025.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:645e88a73861c64c1af558dd12294fb4e67b5c1eae0096a60d7d8a2143a611c7"}, + {file = "regex-2025.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10a450cba5cd5409526ee1d4449f42aad38dd83ac6948cbd6d7f71ca7018f7db"}, + {file = "regex-2025.9.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9dc5991592933a4192c166eeb67b29d9234f9c86344481173d1bc52f73a7104"}, + {file = "regex-2025.9.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a32291add816961aab472f4fad344c92871a2ee33c6c219b6598e98c1f0108f2"}, + {file = "regex-2025.9.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:588c161a68a383478e27442a678e3b197b13c5ba51dbba40c1ccb8c4c7bee9e9"}, + {file = "regex-2025.9.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47829ffaf652f30d579534da9085fe30c171fa2a6744a93d52ef7195dc38218b"}, + {file = "regex-2025.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e978e5a35b293ea43f140c92a3269b6ab13fe0a2bf8a881f7ac740f5a6ade85"}, + {file = "regex-2025.9.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4cf09903e72411f4bf3ac1eddd624ecfd423f14b2e4bf1c8b547b72f248b7bf7"}, + {file = "regex-2025.9.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d016b0f77be63e49613c9e26aaf4a242f196cd3d7a4f15898f5f0ab55c9b24d2"}, + {file = "regex-2025.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:656563e620de6908cd1c9d4f7b9e0777e3341ca7db9d4383bcaa44709c90281e"}, + {file = "regex-2025.9.1-cp312-cp312-win32.whl", hash = "sha256:df33f4ef07b68f7ab637b1dbd70accbf42ef0021c201660656601e8a9835de45"}, + {file = "regex-2025.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:5aba22dfbc60cda7c0853516104724dc904caa2db55f2c3e6e984eb858d3edf3"}, + {file = "regex-2025.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:ec1efb4c25e1849c2685fa95da44bfde1b28c62d356f9c8d861d4dad89ed56e9"}, + {file = "regex-2025.9.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bc6834727d1b98d710a63e6c823edf6ffbf5792eba35d3fa119531349d4142ef"}, + {file = "regex-2025.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c3dc05b6d579875719bccc5f3037b4dc80433d64e94681a0061845bd8863c025"}, + {file = "regex-2025.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22213527df4c985ec4a729b055a8306272d41d2f45908d7bacb79be0fa7a75ad"}, + {file = "regex-2025.9.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e3f6e3c5a5a1adc3f7ea1b5aec89abfc2f4fbfba55dafb4343cd1d084f715b2"}, + {file = "regex-2025.9.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcb89c02a0d6c2bec9b0bb2d8c78782699afe8434493bfa6b4021cc51503f249"}, + {file = "regex-2025.9.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0e2f95413eb0c651cd1516a670036315b91b71767af83bc8525350d4375ccba"}, + {file = "regex-2025.9.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a41dc039e1c97d3c2ed3e26523f748e58c4de3ea7a31f95e1cf9ff973fff5a"}, + {file = "regex-2025.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f0b4258b161094f66857a26ee938d3fe7b8a5063861e44571215c44fbf0e5df"}, + {file = "regex-2025.9.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bf70e18ac390e6977ea7e56f921768002cb0fa359c4199606c7219854ae332e0"}, + {file = "regex-2025.9.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b84036511e1d2bb0a4ff1aec26951caa2dea8772b223c9e8a19ed8885b32dbac"}, + {file = "regex-2025.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c2e05dcdfe224047f2a59e70408274c325d019aad96227ab959403ba7d58d2d7"}, + {file = "regex-2025.9.1-cp313-cp313-win32.whl", hash = "sha256:3b9a62107a7441b81ca98261808fed30ae36ba06c8b7ee435308806bd53c1ed8"}, + {file = "regex-2025.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:b38afecc10c177eb34cfae68d669d5161880849ba70c05cbfbe409f08cc939d7"}, + {file = "regex-2025.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:ec329890ad5e7ed9fc292858554d28d58d56bf62cf964faf0aa57964b21155a0"}, + {file = "regex-2025.9.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:72fb7a016467d364546f22b5ae86c45680a4e0de6b2a6f67441d22172ff641f1"}, + {file = "regex-2025.9.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c9527fa74eba53f98ad86be2ba003b3ebe97e94b6eb2b916b31b5f055622ef03"}, + {file = "regex-2025.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c905d925d194c83a63f92422af7544ec188301451b292c8b487f0543726107ca"}, + {file = "regex-2025.9.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74df7c74a63adcad314426b1f4ea6054a5ab25d05b0244f0c07ff9ce640fa597"}, + {file = "regex-2025.9.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4f6e935e98ea48c7a2e8be44494de337b57a204470e7f9c9c42f912c414cd6f5"}, + {file = "regex-2025.9.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4a62d033cd9ebefc7c5e466731a508dfabee827d80b13f455de68a50d3c2543d"}, + {file = "regex-2025.9.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef971ebf2b93bdc88d8337238be4dfb851cc97ed6808eb04870ef67589415171"}, + {file = "regex-2025.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d936a1db208bdca0eca1f2bb2c1ba1d8370b226785c1e6db76e32a228ffd0ad5"}, + {file = "regex-2025.9.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:7e786d9e4469698fc63815b8de08a89165a0aa851720eb99f5e0ea9d51dd2b6a"}, + {file = "regex-2025.9.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6b81d7dbc5466ad2c57ce3a0ddb717858fe1a29535c8866f8514d785fdb9fc5b"}, + {file = "regex-2025.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cd4890e184a6feb0ef195338a6ce68906a8903a0f2eb7e0ab727dbc0a3156273"}, + {file = "regex-2025.9.1-cp314-cp314-win32.whl", hash = "sha256:34679a86230e46164c9e0396b56cab13c0505972343880b9e705083cc5b8ec86"}, + {file = "regex-2025.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:a1196e530a6bfa5f4bde029ac5b0295a6ecfaaffbfffede4bbaf4061d9455b70"}, + {file = "regex-2025.9.1-cp314-cp314-win_arm64.whl", hash = "sha256:f46d525934871ea772930e997d577d48c6983e50f206ff7b66d4ac5f8941e993"}, + {file = "regex-2025.9.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a13d20007dce3c4b00af5d84f6c191ed1c0f70928c6d9b6cd7b8d2f125df7f46"}, + {file = "regex-2025.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d6b046b0a01cb713fd53ef36cb59db4b0062b343db28e83b52ac6aa01ee5b368"}, + {file = "regex-2025.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0fa9a7477288717f42dbd02ff5d13057549e9a8cdb81f224c313154cc10bab52"}, + {file = "regex-2025.9.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2b3ad150c6bc01a8cd5030040675060e2adbe6cbc50aadc4da42c6d32ec266e"}, + {file = "regex-2025.9.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aa88d5a82dfe80deaf04e8c39c8b0ad166d5d527097eb9431cb932c44bf88715"}, + {file = "regex-2025.9.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6f1dae2cf6c2dbc6fd2526653692c144721b3cf3f769d2a3c3aa44d0f38b9a58"}, + {file = "regex-2025.9.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ff62a3022914fc19adaa76b65e03cf62bc67ea16326cbbeb170d280710a7d719"}, + {file = "regex-2025.9.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a34ef82216189d823bc82f614d1031cb0b919abef27cecfd7b07d1e9a8bdeeb4"}, + {file = "regex-2025.9.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6d40e6b49daae9ebbd7fa4e600697372cba85b826592408600068e83a3c47211"}, + {file = "regex-2025.9.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:0aeb0fe80331059c152a002142699a89bf3e44352aee28261315df0c9874759b"}, + {file = "regex-2025.9.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a90014d29cb3098403d82a879105d1418edbbdf948540297435ea6e377023ea7"}, + {file = "regex-2025.9.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6ff623271e0b0cc5a95b802666bbd70f17ddd641582d65b10fb260cc0c003529"}, + {file = "regex-2025.9.1-cp39-cp39-win32.whl", hash = "sha256:d161bfdeabe236290adfd8c7588da7f835d67e9e7bf2945f1e9e120622839ba6"}, + {file = "regex-2025.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:43ebc77a7dfe36661192afd8d7df5e8be81ec32d2ad0c65b536f66ebfec3dece"}, + {file = "regex-2025.9.1-cp39-cp39-win_arm64.whl", hash = "sha256:5d74b557cf5554001a869cda60b9a619be307df4d10155894aeaad3ee67c9899"}, + {file = "regex-2025.9.1.tar.gz", hash = "sha256:88ac07b38d20b54d79e704e38aa3bd2c0f8027432164226bdee201a1c0c9c9ff"}, +] + [[package]] name = "requests" version = "2.32.4" @@ -2254,6 +2496,21 @@ requests = ">=2.0.0" [package.extras] rsa = ["oauthlib[signedtoken] (>=3.0.0)"] +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +description = "A utility belt for advanced users of python-requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +files = [ + {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, + {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, +] + +[package.dependencies] +requests = ">=2.0.1,<3.0.0" + [[package]] name = "rpds-py" version = "0.27.0" @@ -2460,6 +2717,18 @@ files = [ {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] +[[package]] +name = "tinydb" +version = "4.8.2" +description = "TinyDB is a tiny, document oriented database optimized for your happiness :)" +optional = false +python-versions = "<4.0,>=3.8" +groups = ["main"] +files = [ + {file = "tinydb-4.8.2-py3-none-any.whl", hash = "sha256:f97030ee5cbc91eeadd1d7af07ab0e48ceb04aa63d4a983adbaca4cba16e86c3"}, + {file = "tinydb-4.8.2.tar.gz", hash = "sha256:f7dfc39b8d7fda7a1ca62a8dbb449ffd340a117c1206b68c50b1a481fb95181d"}, +] + [[package]] name = "tomli" version = "2.2.1" @@ -2524,6 +2793,28 @@ files = [ {file = "tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0"}, ] +[[package]] +name = "tqdm" +version = "4.67.1" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, + {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] +discord = ["requests"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + [[package]] name = "trame" version = "3.11.0" @@ -2679,6 +2970,24 @@ files = [ [package.dependencies] trame-client = "*" +[[package]] +name = "trame-vtk" +version = "2.9.1" +description = "VTK widgets for trame" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "trame_vtk-2.9.1-py3-none-any.whl", hash = "sha256:e01d3d9ef7781cbdadafc9d543c30221310c022c2091fd7973da67053d572a72"}, + {file = "trame_vtk-2.9.1.tar.gz", hash = "sha256:2423f0cb7c019bd919d313e904ab8de2fe1dbfc5e3ff77961fc13ea9d1b89594"}, +] + +[package.dependencies] +trame-client = ">=3.4,<4" + +[package.extras] +dev = ["coverage", "nox", "pre-commit", "pytest", "pytest-asyncio", "ruff"] + [[package]] name = "trame-vuetify" version = "3.0.2" @@ -2697,6 +3006,27 @@ trame-client = ">=3.7,<4" [package.extras] dev = ["pre-commit", "pytest", "ruff"] +[[package]] +name = "tuspy" +version = "1.1.0" +description = "A Python client for the tus resumable upload protocol -> http://tus.io" +optional = false +python-versions = ">=3.5.3" +groups = ["main"] +files = [ + {file = "tuspy-1.1.0-py3-none-any.whl", hash = "sha256:7fc5ac8fb25de37c96c90213f83a1ffdede7f48a471cb5a15a2f57846828a79a"}, + {file = "tuspy-1.1.0.tar.gz", hash = "sha256:156734eac5c61a046cfecd70f14119f05be92cce198eb5a1a99a664482bedb89"}, +] + +[package.dependencies] +aiohttp = ">=3.6.2" +requests = ">=2.18.4" +tinydb = ">=3.5.0" + +[package.extras] +dev = ["Sphinx (==1.7.1)", "sphinx-autobuild (==2021.3.14)", "tox (>=2.3.1)"] +test = ["aioresponses (>=0.6.2)", "coverage (>=4.2)", "parametrize (>=0.1.1)", "pytest (>=3.0.3)", "pytest-cov (>=2.3.1,<2.6)", "responses (>=0.5.1)"] + [[package]] name = "typing-extensions" version = "4.14.1" @@ -2764,6 +3094,143 @@ typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\"" docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] +[[package]] +name = "vtk" +version = "9.5.2" +description = "VTK is an open-source toolkit for 3D computer graphics, image processing, and visualization" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "vtk-9.5.2-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:9ca87122352cf3c8748fee73c48930efa46fe1a868149a1f760bc17e8fae27ba"}, + {file = "vtk-9.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6da02d69dcf2d42472ec8c227e6a8406cedea53d3928af97f8d4e776ff89c95f"}, + {file = "vtk-9.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c0ba9cc4b5cd463a1984dfac6d0a9eeef888b273208739f8ebc46d392ddabb93"}, + {file = "vtk-9.5.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c9254f864ebef3d69666a1feedf09cad129e4c91f85ca804c38cf8addedb2748"}, + {file = "vtk-9.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:7c56dbd02e5b4ec0422886bf9e26059ad2d4622857dbfb90d9ed254104fd9d6c"}, + {file = "vtk-9.5.2-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:afcbc6dc122ebba877793940fda8fd2cbe14e1dae590e6872ea74894abdab9be"}, + {file = "vtk-9.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:005877a568b96cf00ceb5bec268cf102db756bed509cb240fa40ada414a24bf0"}, + {file = "vtk-9.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2e2fe2535483adb1ba8cc83a0dc296faaffa2505808a3b04f697084f656e5f84"}, + {file = "vtk-9.5.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:0248aab2ee51a69fadcdcf74697a045e2d525009a35296100eed2211f0cca2bb"}, + {file = "vtk-9.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:f78674fd265022499ea6b7f03d7f11a861e89e1df043592a82e4f5235c537ef5"}, + {file = "vtk-9.5.2-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:cf5dbc93b6806b08799204430a4fc4bea74290c1c101fa64f1a4703144087fa3"}, + {file = "vtk-9.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cce212b911d13fb0ca36d339f658c9db1ff27a5a730cdddd5d0c6b2ec24c15b1"}, + {file = "vtk-9.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:454711c51038824ddc75f955e1064c4e214b452c2e67083f01a8b43fc0ed62cb"}, + {file = "vtk-9.5.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:9fce9688f0dede00dc6f3b046037c5fa8378479fa8303a353fd69afae4078d9a"}, + {file = "vtk-9.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:5357bccdf8629373195cab871e45c50383d052d316192aa48f45bd9f87bafccb"}, + {file = "vtk-9.5.2-cp313-cp313-macosx_10_10_x86_64.whl", hash = "sha256:1eae5016620a5fd78f4918256ea65dbe100a7c3ce68f763b64523f06aaaeafbc"}, + {file = "vtk-9.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:29ad766e308dcaa23b36261180cd9960215f48815b31c7ac2aa52edc88e21ef7"}, + {file = "vtk-9.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:11cf870c05539e9f82f4a5adf450384e0be4ee6cc80274f9502715a4139e2777"}, + {file = "vtk-9.5.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:3c4b658d61815cb87177f4e94281396c9be5a28798464a2c6fa0897b1bba282f"}, + {file = "vtk-9.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:974783b8865e2ddc2818d3090705b6bc6bf8ae40346d67f9a43485fabcfb3a99"}, + {file = "vtk-9.5.2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:227c5e6e9195aa9d92a64d6d07d09f000576b5df231522b5c156a3c4c4190d69"}, + {file = "vtk-9.5.2-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b5d0a89e893d9279ba9742d0bbd47d7dfac96fccd8fb9d024bb8aa098fde5637"}, + {file = "vtk-9.5.2-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:371d9068f5cb25861aa51c1d1792fffce5a44032dbece55412562429c5f257cc"}, + {file = "vtk-9.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:7cf2e2e12184c018388f06fbffcb93ea9e478ca4bf636c3f66bd7503e2230298"}, + {file = "vtk-9.5.2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:9b148e57837d1fd2a8a72f171a0fb40872837dea191f673f2b7ec397935c754e"}, + {file = "vtk-9.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6a3b27f22d7e15f6a2d60510e70d75dac4ed2a53600e31275b67fedc45afbcc0"}, + {file = "vtk-9.5.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b6b91968581132b0d96142a08d50028efa5aa7a876d4aff6de1664e99e006c89"}, + {file = "vtk-9.5.2-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:2b37670d56de32935eeadee58e1a9a0b5d3847294ca24ea9329101089be5de83"}, + {file = "vtk-9.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:1ac9ff528892e585f8f3286b26a90250bd6ea9107c38e6e194939f6f28269ad6"}, +] + +[package.dependencies] +matplotlib = ">=2.0.0" + +[package.extras] +numpy = ["numpy (>=1.9)"] +web = ["wslink (>=1.0.4)"] + +[[package]] +name = "wrapt" +version = "1.17.3" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04"}, + {file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2"}, + {file = "wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c"}, + {file = "wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775"}, + {file = "wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd"}, + {file = "wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05"}, + {file = "wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418"}, + {file = "wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390"}, + {file = "wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6"}, + {file = "wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18"}, + {file = "wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7"}, + {file = "wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85"}, + {file = "wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f"}, + {file = "wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311"}, + {file = "wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1"}, + {file = "wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5"}, + {file = "wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2"}, + {file = "wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89"}, + {file = "wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77"}, + {file = "wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a"}, + {file = "wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0"}, + {file = "wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba"}, + {file = "wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd"}, + {file = "wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828"}, + {file = "wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9"}, + {file = "wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396"}, + {file = "wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc"}, + {file = "wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe"}, + {file = "wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c"}, + {file = "wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6"}, + {file = "wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0"}, + {file = "wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77"}, + {file = "wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7"}, + {file = "wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277"}, + {file = "wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d"}, + {file = "wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa"}, + {file = "wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050"}, + {file = "wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8"}, + {file = "wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb"}, + {file = "wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16"}, + {file = "wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39"}, + {file = "wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235"}, + {file = "wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c"}, + {file = "wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b"}, + {file = "wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa"}, + {file = "wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7"}, + {file = "wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4"}, + {file = "wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10"}, + {file = "wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6"}, + {file = "wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58"}, + {file = "wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a"}, + {file = "wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067"}, + {file = "wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454"}, + {file = "wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e"}, + {file = "wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f"}, + {file = "wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056"}, + {file = "wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804"}, + {file = "wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977"}, + {file = "wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116"}, + {file = "wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6"}, + {file = "wrapt-1.17.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:70d86fa5197b8947a2fa70260b48e400bf2ccacdcab97bb7de47e3d1e6312225"}, + {file = "wrapt-1.17.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:df7d30371a2accfe4013e90445f6388c570f103d61019b6b7c57e0265250072a"}, + {file = "wrapt-1.17.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:caea3e9c79d5f0d2c6d9ab96111601797ea5da8e6d0723f77eabb0d4068d2b2f"}, + {file = "wrapt-1.17.3-cp38-cp38-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:758895b01d546812d1f42204bd443b8c433c44d090248bf22689df673ccafe00"}, + {file = "wrapt-1.17.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02b551d101f31694fc785e58e0720ef7d9a10c4e62c1c9358ce6f63f23e30a56"}, + {file = "wrapt-1.17.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:656873859b3b50eeebe6db8b1455e99d90c26ab058db8e427046dbc35c3140a5"}, + {file = "wrapt-1.17.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a9a2203361a6e6404f80b99234fe7fb37d1fc73487b5a78dc1aa5b97201e0f22"}, + {file = "wrapt-1.17.3-cp38-cp38-win32.whl", hash = "sha256:55cbbc356c2842f39bcc553cf695932e8b30e30e797f961860afb308e6b1bb7c"}, + {file = "wrapt-1.17.3-cp38-cp38-win_amd64.whl", hash = "sha256:ad85e269fe54d506b240d2d7b9f5f2057c2aa9a2ea5b32c66f8902f768117ed2"}, + {file = "wrapt-1.17.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:30ce38e66630599e1193798285706903110d4f057aab3168a34b7fdc85569afc"}, + {file = "wrapt-1.17.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:65d1d00fbfb3ea5f20add88bbc0f815150dbbde3b026e6c24759466c8b5a9ef9"}, + {file = "wrapt-1.17.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7c06742645f914f26c7f1fa47b8bc4c91d222f76ee20116c43d5ef0912bba2d"}, + {file = "wrapt-1.17.3-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e18f01b0c3e4a07fe6dfdb00e29049ba17eadbc5e7609a2a3a4af83ab7d710a"}, + {file = "wrapt-1.17.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f5f51a6466667a5a356e6381d362d259125b57f059103dd9fdc8c0cf1d14139"}, + {file = "wrapt-1.17.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:59923aa12d0157f6b82d686c3fd8e1166fa8cdfb3e17b42ce3b6147ff81528df"}, + {file = "wrapt-1.17.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:46acc57b331e0b3bcb3e1ca3b421d65637915cfcd65eb783cb2f78a511193f9b"}, + {file = "wrapt-1.17.3-cp39-cp39-win32.whl", hash = "sha256:3e62d15d3cfa26e3d0788094de7b64efa75f3a53875cdbccdf78547aed547a81"}, + {file = "wrapt-1.17.3-cp39-cp39-win_amd64.whl", hash = "sha256:1f23fa283f51c890eda8e34e4937079114c74b4c81d2b2f1f1d94948f5cc3d7f"}, + {file = "wrapt-1.17.3-cp39-cp39-win_arm64.whl", hash = "sha256:24c2ed34dc222ed754247a2702b1e1e89fdbaa4016f324b4b8f1a802d4ffe87f"}, + {file = "wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22"}, + {file = "wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0"}, +] + [[package]] name = "wslink" version = "2.3.4" @@ -2905,4 +3372,4 @@ propcache = ">=0.2.1" [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "f9a3bafd50781e235bb8d677dd86d9d532d19b6d8cc0f59b4510d9f4538d649e" +content-hash = "b3197c25e540739486a0b1418194b651c3d0337aa856c88cd7459d82eaa8c1aa" diff --git a/pyproject.toml b/pyproject.toml index 23bdc14..7876740 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ version = "0.1.0" description = "NOVA Examples" authors = [] license = "MIT" -keywords = ["NDIP", "NOVA", "python"] +keywords = ["Galaxy", "NOVA", "python"] packages = [ { include = "examples" } @@ -13,6 +13,7 @@ packages = [ [tool.poetry.dependencies] python = "^3.10" +nova-galaxy = "*" nova-mvvm = "*" nova-trame = "*" mypy = "*" @@ -21,6 +22,10 @@ pytest = "*" requests = "*" ruff = "*" trame-code = "*" +nltk = "*" +pillow = "^11.3.0" +trame-vtk = "^2.9.1" +vtk = "^9.5.2" [build-system] requires = ["poetry-core"] diff --git a/tests/test_examples.py b/tests/test_examples.py index 55c41e0..709e920 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -6,21 +6,37 @@ from multiprocessing import Process from pathlib import Path from time import sleep +from typing import List import pytest from requests import get -base_path = Path("examples") -examples_list = [] -try: - for example in sorted(os.listdir(base_path)): - if example.startswith(".") or example.startswith("_"): - continue - - if os.path.isdir(base_path / example): - examples_list.append(example) -except OSError: - pass +EXAMPLES_DIRECTORY = Path("examples") +ORNL_SUBDIRECTORY = Path("ornl") + + +def get_examples(directory: Path) -> List[str]: + found_examples = [] + + try: + for example in sorted(os.listdir(directory)): + if example.startswith(".") or example.startswith("_"): + continue + + if example.startswith(str(ORNL_SUBDIRECTORY)): + if not os.environ.get("INCLUDE_ORNL_TESTS", False): + continue + + found_examples.extend(get_examples(directory / ORNL_SUBDIRECTORY)) + elif os.path.isdir(directory / example): + found_examples.append(example) + except OSError: + pass + + return found_examples + + +examples_list = get_examples(EXAMPLES_DIRECTORY) def run_server(input_directory: str) -> None: