Commit c54422af authored by Ayres, Andrew's avatar Ayres, Andrew
Browse files

Reorder 4 and fix remotefile in 5

parent 5cea110c
Loading
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -10,5 +10,5 @@ class SampleTab1:
        self.create_ui()

    def create_ui(self) -> None:
        RemoteFileInput(v_model="file", base_paths=["/HFIR", "/SNS"])
        RemoteFileInput(v_model="config.file", base_paths=["/HFIR", "/SNS"])
        InputField(v_model="config.username")
+126 −76
Original line number Diff line number Diff line
@@ -142,78 +142,77 @@ Finally, we connect a UI component to the connector object. The template applica
InputField(v_model="config.username")
```

## Implementing MVVM with `nova-mvvm` and Pydantic
## Project Structure

Let\'s see how to implement the MVVM pattern using `nova-mvvm` and incorporate Pydantic for data validation.
The template creates a well-organized project structure following best practices, including the Model-View-ViewModel (MVVM) design pattern.  This structure promotes code maintainability, testability, and separation of concerns. Here's a breakdown of the key directories and files:

**1. Adding Fractal to the ViewModel (`src/nova_tutorial/app/view_models/main.py`) (Modify):**
*   `nova_tutorial/`: The root directory of your project. This is the top-level directory containing all project files and subdirectories.

*   **Running our Model**:  We start by adding a method to bottom of our ViewModel which will run the Fractal tool.
*   `nova_tutorial/src/`:  This directory contains all the source code for your application.  The separation into `src` helps distinguish your application code from configuration files, tests, and other project-related files that reside in the root.

```python
    def run_fractal(self) -> None:
        self.model.fractal.run_fractal_tool()
        self.update_view()
```
*   `nova_tutorial/src/nova_tutorial/`: This is the main Python package for your application. Its name (`nova_tutorial` in this case) is used when importing modules within your project.  Inside this directory, you'll find the core application logic, organized according to the MVVM pattern:

**2. Updating our Fractal Class for pydantaic and MVVM (`src/nova_tutorial/app/models/fractal.py`) (Modify)**
    *   `nova_tutorial/src/nova_tutorial/app/`:  This directory contains the main application logic, further subdivided to reflect the MVVM structure.

*   **Adding new imports**: We need to add some imports for pydantic and working with base64 encodings to deal with the image. Modify your import block to match below.
        *   `nova_tutorial/src/nova_tutorial/app/models/`: **(Model)** This is where you define your data models and business logic. These classes represent the data your application works with and the rules for manipulating that data.

```python
import os
from base64 import b64encode
from typing import Literal
        *   `nova_tutorial/src/nova_tutorial/app/view_models/`: **(ViewModel)** This directory holds the ViewModels. These classes act as intermediaries between the Models and the Views.  They prepare data for display and handle user interactions from the View.

from pydantic import BaseModel, Field
from nova.galaxy import Connection, Parameters, Tool
```
        *   `nova_tutorial/src/nova_tutorial/app/views/`: **(View)** This directory contains the user interface (UI) components.  These are built using Trame and Vuetify (via `nova-trame`).  They are responsible for displaying data and capturing user input.

*   **Update class variables:** Now we will update fractal_type and other class variables to support pydantic. We will also add an image variable to store the image. Modify the variable declarations to the following:
        *   `nova_tutorial/src/nova_tutorial/app/main.py`: The entry point for your NOVA application.  This file initializes and starts the Trame server and the `MainApp` view.

```python
class Fractal(BaseModel):
    fractal_type: Literal["mandelbrot", "julia", "random", "markus"] = Field(default="mandelbrot")
    galaxy_url: str = Field(default_factory=lambda: os.getenv("GALAXY_URL"), description="NDIP Galaxy URL")
    galaxy_key: str = Field(default_factory=lambda: os.getenv("GALAXY_API_KEY"), description="NDIP Galaxy API Key")
    image_data: str = Field(default="", description="Base64 encoded PNG")
*   `nova_tutorial/tests/`:  Contains unit tests for your application.  A well-structured project should include tests to ensure code quality and prevent regressions.  The tests are typically organized to mirror the structure of your application code (e.g., tests for models, view models, and potentially UI components).

    def set_fractal_type(self, fractal_type: str):
        self.fractal_type = fractal_type
```
*   `nova_tutorial/README.md`:  A Markdown file providing a description of your project, instructions for setup and usage, and any other relevant information.

*   **Decode the image data:** Finally, we need to decode the image that we receive as the output from the tool execution. Modify the section where we execute the tool to the following:
*   `pyproject.toml`:  A configuration file for Poetry, the dependency management and packaging tool used by NOVA.  It specifies project dependencies, build settings, and other metadata.

```python
            output.get_dataset("output").download("tmp.png")
## Implementing MVVM with `nova-mvvm` and Pydantic

            with open("tmp.png", "rb") as image_file:
                self.image_data = f"data:image/png;base64,{b64encode(image_file.read()).decode()}"
```
Let's implement the MVVM pattern, starting with the UI and basic ViewModel connections, and then building up the Model functionality.

**1. Initial Setup and ViewModel Basics**

First, let's simplify our `main.py` and set up the basic structure of our UI and ViewModel interaction. We'll create a button in the UI that, when clicked, will eventually run our Fractal tool. For now, it will just trigger a placeholder method in the ViewModel.

**3. Updating our MainModel Class to add the new Fractal Class (`src/nova_tutorial/app/models/main_model.py`) (Modify):**
*   **`main.py` - Simplifying the Application Entry Point (`src/nova_tutorial/app/main.py`) (Modify):**

*   **Add Fractal to imports**: Add an import for the Fractal class into our MainModel.
We're removing the direct Fractal tool execution from `main()`. The application will now solely focus on launching the NOVA app.

 ```python
from .fractal import Fractal  # Import Fractal
import sys

def main() -> None:
    kwargs = {}
    from .views.main import MainApp

    app = MainApp()
    for arg in sys.argv[2:]:
        try:
            key, value = arg.split("=")
            kwargs[key] = int(value)
        except Exception:
            pass
    app.server.start(**kwargs)
```

*   **Add the Fractal Model to the MainModel**: Modify the end of the MainModel class so that it matches the code below.
*   **Adding a Placeholder Method to the ViewModel (`src/nova_tutorial/app/view_models/main.py`) (Modify):**

Add a `run_fractal` method to the `MainViewModel`.  For now, it just prints a message to the console. This confirms that the button click is connected to the ViewModel.

```python
    password: str = Field(default="test_password", title="User Password")
    fractal: Fractal = Field(default_factory=Fractal) #Add Fractal Model
    def run_fractal(self) -> None:
        print("run_fractal method called!")
```

**4. Creating a FractalTab (`src/nova_tutorial/app/views/fractal_tab.py`) (Create):**
*   **Creating a FractalTab (`src/nova_tutorial/app/views/fractal_tab.py`) (Create):**

*   **Create a fractal tab**: Create a new file and add the following code:
This is the UI for our Fractal interaction.  It includes a button that calls the `run_fractal` method in the ViewModel.  We don't have image display yet.

```python
from trame.widgets import vuetify3 as vuetify

from nova.trame.view.components import InputField
from nova_tutorial.app.view_models.main import MainViewModel

class FractalTab:
@@ -225,14 +224,13 @@ class FractalTab:
        InputField(v_model="config.fractal.fractal_type")
        vuetify.VBtn(
            "Run Fractal",
            click=self.view_model.run_fractal # calls the run_fractal_tool method
            click=self.view_model.run_fractal
    )
        vuetify.VImg(src=("config.fractal.image_data",), height="400", width="400")
```

**5. Modify the tab panel (`src/nova_tutorial/app/views/tabs_panel.py`) (Modify):**
*   **Modify the tab panel (`src/nova_tutorial/app/views/tabs_panel.py`) (Modify):**

*   **Add Fractal Tab to the tab panel**: Modify the tab panel to add our new Fractal tab
Add the "Fractal" tab to the tab bar.

```python
        with vuetify.VTabs(v_model=("active_tab", 0), classes="pl-5"):
@@ -241,17 +239,14 @@ class FractalTab:
            vuetify.VTab("Sample Tab 2", value=3)
```

**6. Modify the tab panel content (`src/nova_tutorial/app/views/tab_content_panel.py`) (Modify):**
*   **Modify the tab panel content (`src/nova_tutorial/app/views/tab_content_panel.py`) (Modify):**

*   **Add FractalTab to imports**: Import the newly created FractalTab class into our tab_content_panel.
Display the `FractalTab` content when the "Fractal" tab is selected.

```python
from .fractal_tab import FractalTab  # Import the FractalTab
```

*   **Add the Fractal Tab to our existing tabs**: Add the Fractal Tab lines to the vuetify.VWindow section and modify the values.

```python
    # ... (rest of the file) ...
                    with vuetify.VWindow(v_model="active_tab"):
                        with vuetify.VWindowItem(value=1):
                            FractalTab(self.view_model)  # Add FractalTab
@@ -261,40 +256,95 @@ from .fractal_tab import FractalTab # Import the FractalTab
                            SampleTab2()
```

**7. `main.py` - Calling the Model (`src/nova_tutorial/app/main.py`) (Modify):**
**Demonstration (Initial UI and ViewModel Connection):**

Run the application: `poetry run app`

You should see a new "Fractal" tab in the application.  Click the "Run Fractal" button.  You should see "run_fractal method called!" printed in your terminal. This demonstrates that the button click in the View is successfully triggering the `run_fractal` method in the ViewModel, even though the method doesn't do anything substantial yet. This establishes the basic MVVM wiring.

**2. Fractal Model and Pydantic Integration**

We are now going to modify the existing `main.py` file. Change the main method to match the code below.
Now, let's build out the `Fractal` model using Pydantic and integrate it into our `MainModel`.

*   **Instantiate and Run**: In the `main()` function, we no longer need to setup the Fractal tool as it's managed via our MVVM application now.
*   **Updating our Fractal Class for pydantaic and MVVM (`src/nova_tutorial/app/models/fractal.py`) (Modify)**

*   **Adding new imports**: Add imports for Pydantic and base64 handling.

```python
import sys
from .models.fractal import Fractal
import os
from base64 import b64encode
from typing import Literal

from pydantic import BaseModel, Field
from nova.galaxy import Connection, Parameters, Tool
 ```

def main() -> None:
    kwargs = {}
    from .views.main import MainApp
*   **Update class variables:** Use Pydantic's `Field` for type hinting and validation.  Add the `image_data` field.

    app = MainApp()
    for arg in sys.argv[2:]:
        try:
            key, value = arg.split("=")
            kwargs[key] = int(value)
        except Exception:
            pass
    app.server.start(**kwargs)
```python
class Fractal(BaseModel):
    fractal_type: Literal["mandelbrot", "julia", "random", "markus"] = Field(default="mandelbrot")
    galaxy_url: str = Field(default_factory=lambda: os.getenv("GALAXY_URL"), description="NDIP Galaxy URL")
    galaxy_key: str = Field(default_factory=lambda: os.getenv("GALAXY_API_KEY"), description="NDIP Galaxy API Key")
    image_data: str = Field(default="", description="Base64 encoded PNG")

    def set_fractal_type(self, fractal_type: str):
        self.fractal_type = fractal_type
```

## Running the application
*  **Decode the data:** Update how the image is decoded.
```python
            output.get_dataset("output").download("tmp.png")

To run the code, use the following command in the top level of your `nova_tutorial` project:
            with open("tmp.png", "rb") as image_file:
                self.image_data = f"data:image/png;base64,{b64encode(image_file.read()).decode()}"
```

```bash
poetry run app
*   **Updating our MainModel Class to add the new Fractal Class (`src/nova_tutorial/app/models/main_model.py`) (Modify):**

Import and include the `Fractal` model as a field in the `MainModel`.

```python
    from .fractal import Fractal  # Import Fractal

class MainModel(BaseModel):
    # ... (other fields) ...
    password: str = Field(default="test_password", title="User Password")
    fractal: Fractal = Field(default_factory=Fractal) #Add Fractal Model
```

The application should launch a tab in your web browser. The GUI will have a `FRACTAL` tab and a few sample tabs which were created by the template application. The run button on the `Fractal` tab can be used to launch the `Fractal` NDIP tool. The tool will take a few minutes to complete but when it does, the resulting `Fractal` image will be displayed.
*   **Connect the UI elements in FractalTab (`src/nova_tutorial/app/views/fractal_tab.py`) (Modify):**

Update the create UI section to use InputField and the image.
```python
from nova.trame.view.components import InputField

    # ...(rest of file)...
    def create_ui(self) -> None:
        InputField(v_model="config.fractal.fractal_type")
        vuetify.VBtn(
            "Run Fractal",
            click=self.view_model.run_fractal
        )
        vuetify.VImg(src=("config.fractal.image_data",), height="400", width="400")
```

* **Add Full Functionality to the View Model (`src/nova_tutorial/app/view_models/main.py`) (Modify)**
Update the code in the run_fractal method.

```python
    def run_fractal(self) -> None:
        self.model.fractal.run_fractal_tool()
        self.update_view()
```

**Final Demonstration (Full Application):**

Run the application: `poetry run app`

Now, when you click "Run Fractal," the Fractal tool will execute in Galaxy, and the resulting image will be displayed in the UI.  You can also change the `fractal_type` using the input field. This demonstrates the complete MVVM flow, with data binding, Pydantic validation, and the interaction between the View, ViewModel, and Model.

This revised structure breaks down the implementation into smaller, more manageable steps, with demonstrations after each stage to show the progress and confirm that each part is working as expected. This addresses the feedback about making too many code changes at once and improves the learning experience.

::::::::::::::::::::::::::::::::::::::::: callout
If you don't want Trame to launch a tab by default, you can instead run ```poetry run app --server```.
+25 −2
Original line number Diff line number Diff line
@@ -200,10 +200,33 @@ class SampleTab1:
        self.create_ui()

    def create_ui(self) -> None:
        RemoteFileInput(v_model="file", base_paths=["/HFIR", "/SNS"])
        RemoteFileInput(v_model="config.file", base_paths=["/HFIR", "/SNS"])
        InputField(v_model="config.username")
```

**6. `src/nova_tutorial/app/models/main_model.py` (Modify):**

Add a `file` field to the `MainModel` to store the selected file path.  We use `Optional[str]` because initially, no file will be selected.

```python
from pydantic import BaseModel, Field
from .fractal import Fractal


class MainModel(BaseModel):
    username: str = Field(
        default="test_name",
        min_length=1,
        title="User Name",
        description="Please provide the name of the user",
        examples=["user"],
    )
    password: str = Field(default="test_password", title="User Password")
    file: str = Field(default="", title="Select a File")
    fractal: Fractal = Field(default_factory=Fractal)
```


:::::::::::::::::::::::::::::::: callout

If you want to connect your application to the analysis cluster, then it will need to be run on a computer where the filesystem is mounted. If your application is deployed through our platform, then we can ensure that your application runs in the correct environment to support your needs.
@@ -268,7 +291,7 @@ By combining these layout components, you can create complex and responsive UI l

As an example, we can use the layout classes to center the "Run Fractal" button.

**6. `src/nova_tutorial/app/views/main.py` (Modify):**
**7. `src/nova_tutorial/app/views/main.py` (Modify):**

```python
from nova.trame.view import layouts