Commit 32844c48 authored by Yakubov, Sergey's avatar Yakubov, Sergey
Browse files

refactor setup section

parent 26f15cf1
Loading
Loading
Loading
Loading
Loading
+76 −70
Original line number Diff line number Diff line
@@ -30,28 +30,6 @@ In this section, we will explore Pydantic, a powerful Python library for data va

The complete code for this episode is available in the `code/episode_6` directory.

## Setup

Let\'s start by setting up a couple of new applications from the template. 

First, create a CLI project answering the `copier` questions, use `Advanced Pydantic` for the project name and use defaults for all other questions.

```bash
copier copy https://code.ornl.gov/ndip/project-templates/nova-application-template.git advanced_pydantic
poetry install -C advanced_pydantic
```

Next, create a simple Trame GUI, use `Trame With Pydantic` for the project name, 
set up a Trame-based GUI application based on MVVM and use defaults for all other questions.

```bash
copier copy https://code.ornl.gov/ndip/project-templates/nova-application-template.git pydantic_mvvm
poetry install -C pydantic_mvvm
```

We will be using `Advanced Pydantic` project for all examples except for the last section
where we switch to the `Trame With Pydantic` project.

## Why Data Validation Matters

Data validation is the process of ensuring that data meets certain criteria before it\'s processed by your application. It\'s a crucial step in building robust and reliable software. Without proper data validation, your application could be vulnerable to:
@@ -80,11 +58,23 @@ Key Features of Pydantic:
*   **Improved Code Readability:** Makes code easier to understand and maintain by explicitly defining data models. Type hints make it clear what type of data is expected for each field.


## Setup I

Let\'s start by setting up a new application from the template. 

Create a CLI project answering the `copier` questions, use `Advanced Pydantic` for the project name and use defaults for all other questions.

```bash
copier copy https://code.ornl.gov/ndip/project-templates/nova-application-template.git advanced_pydantic
cd advanced_pydantic
poetry install
```

## How Pydantic Works

Pydantic uses Python type hints to define data models. When you create an instance of a Pydantic model, Pydantic automatically validates the input data against the defined types and constraints.

Here\'s a simple example (add this code to `advanced_pydantic/src/advanced_pydantic/main.py`):
Here\'s a simple example (add this code to `src/advanced_pydantic/main.py`):

```python
from pydantic import BaseModel, Field
@@ -98,11 +88,12 @@ In this example, we define a `User` model with two fields: `id` and `name`. We u

When you create an instance of the `User` model, Pydantic automatically validates the input data.

Add the code to the main function in `advanced_pydantic/src/advanced_pydantic/main.py`
Modify the main function in `src/advanced_pydantic/main.py`

```python
from pydantic import ValidationError

def main() -> None:  
    try:
        user = User(id=0, name="")
        print(user)
@@ -123,7 +114,7 @@ If the input data is invalid, Pydantic raises a `ValidationError` exception with

When working with structured data, it\'s common to have nested objects. For example, a User model from the above example might have multiple Address entries. In Pydantic, we can achieve this by creating nested models.

1. Creating the Address Model (add code to `advanced_pydantic/src/advanced_pydantic/main.py`).
1. Creating the Address Model (add code to `src/advanced_pydantic/main.py`).

The Address model represents a simple address with three fields:

@@ -143,9 +134,9 @@ class Address(BaseModel):
    type: Literal["home", "work"] = Field()
```

2. Using the Address Model as a Nested Field (add code to `advanced_pydantic/src/advanced_pydantic/main.py`).
2. Using the Address Model as a Nested Field (modify User model in `src/advanced_pydantic/main.py`).

The User model contains:
The User model now contains:

- id: An integer that must be greater than 0 (default is 1).
- name: A required string with at least 1 character (default is "someName").
@@ -160,9 +151,11 @@ class User(BaseModel):
    addresses: List[Address] = Field(min_length=1)
```

now you can try to test the model. Add the code to the main function in `advanced_pydantic/src/advanced_pydantic/main.py`
now you can try to test the model. Modify the main function in `src/advanced_pydantic/main.py`

```python    
def main() -> None:
   
    # Example input
    user_data = {
        "id": 1,
@@ -193,7 +186,7 @@ For easier integration with the NOVA framework, where model field information is

Sometimes, simple validation like checking the minimum length is not enough. In such cases, you can write a custom validation function for a specific field.

For example, let\'s say we have a User model where only even IDs are allowed. We can enforce this constraint using the `@field_validator decorator` (`advanced_pydantic/src/advanced_pydantic/main.py`):
For example, let\'s say we have a User model where only even IDs are allowed. We can enforce this constraint using the `@field_validator decorator` (modify `src/advanced_pydantic/main.py`):

```python
from pydantic import BaseModel, Field, field_validator
@@ -209,6 +202,7 @@ class User(BaseModel):
            raise ValueError(f"{value} is not an even number")
        return value
    
def main() -> None:      
    # Example input
    user_data = {
        "id": 1,    
@@ -217,7 +211,6 @@ user_data = {
    
    user = User.model_validate(user_data)
    print(user)

```

This code will raise a ValueError because the provided id (1) is not an even number.
@@ -232,7 +225,7 @@ Note that we used the mode="**after**" option for the validator. This ensures th

In some cases, you may need to validate the entire model, not just individual fields. This can be done by writing a custom validation function for the whole model using the `@model_validator` decorator.

For example, let\'s say we have a User model where the name and id must meet specific conditions together. For instance, we only allow users with even IDs to have names that start with a capital letter. We can enforce this logic using a @model_validator (`advanced_pydantic/src/advanced_pydantic/main.py`):
For example, let\'s say we have a User model where the name and id must meet specific conditions together. For instance, we only allow users with even IDs to have names that start with a capital letter. We can enforce this logic using a @model_validator (modify `src/advanced_pydantic/main.py`):

```python
from pydantic import BaseModel, Field, model_validator
@@ -249,6 +242,7 @@ class User(BaseModel):
        
        return self

def main() -> None:   
    # Example input
    user_data = {
        "id": 2,    
@@ -261,11 +255,23 @@ print(user)

This code will raise a ValueError because the name ("alice") does not start with a capital letter, while the id is even.

## Setup II

Now, let\'s create a simple Trame GUI, use `Trame With Pydantic` for the project name, 
set up a Trame-based GUI application based on MVVM and use defaults for all other questions.

```bash
cd .. # if you are in advanced_pydantic folder
copier copy https://code.ornl.gov/ndip/project-templates/nova-application-template.git pydantic_mvvm
cd pydantic_mvvm
poetry install
```

## Using Pydantic models in NOVA framework

One of the great features of the NOVA Framework is that it allows to leverage Pydantic models to automatically validation UI elements. Let\'s walk through what that looks like in code.

First, let\'s add the following model (`pydantic_mvvm/src/trame_with_pydantic/app/models/settings.py`):
First, let\'s add the following model (create `src/trame_with_pydantic/app/models/settings.py`):

```python
from pydantic import BaseModel, Field
@@ -274,7 +280,7 @@ class SettingsModel(BaseModel):
    port: int = Field(default=8080, gt=0, lt=65536, title="Port Number", description="The port to listen on.", examples=["12345"])
```

Then in your viewmodel, you create binding for this model (`pydantic_mvvm/src/trame_with_pydantic/app/view_models/main.py`, clean up code created by tempalte engine, we don't need it for this example):
Then in your viewmodel, you create binding for this model (modify `src/trame_with_pydantic/app/view_models/main.py`, clean up code created by tempalte engine, we don't need it for this example):

```python
from typing import Any, Dict
@@ -291,7 +297,7 @@ class MainViewModel:
        self.settings_bind.update_view(self.settings)
```

And in your view, remove all other fields and add the following InputField (`pydantic_mvvm/src/trame_with_pydantic/app/views/main.py`):
And in your view, remove all other fields and add the following InputField (modify `src/trame_with_pydantic/app/views/main.py`):

```python
...
@@ -308,7 +314,7 @@ In that fashion, the `InputField` seamlessly pulls information from your code\'s

### Using callbacks in ViewModel to react to validation errors

Sometimes, you may want to respond to UI validation errors beyond just marking a field as invalid (which happens automatically). In such cases, you can add a callback to the `new_bind` function (`pydantic_mvvm/src/trame_with_pydantic/app/view_models/main.py`):
Sometimes, you may want to respond to UI validation errors beyond just marking a field as invalid (which happens automatically). In such cases, you can add a callback to the `new_bind` function (modify `src/trame_with_pydantic/app/view_models/main.py`):

```python
class MainViewModel: