GithubHelp home page GithubHelp logo

dash-pydantic-form's Introduction

Dash pydantic form

This package allows users to quickly create forms with Plotly Dash based on pydantic models.

See the full docs at dash-pydantic-form docs.

Check out a full self-standing example app in usage.py.

Getting started

Install with pip

pip install dash-pydantic-form

Create a pydantic model you would like to display a form for.

Note: This package uses pydantic 2.

from datetime import date
from typing import Literal
from pydantic import BaseModel, Field

class Employee(BaseModel):
    first_name: str = Field(title="First name")
    last_name: str = Field(title="Last name")
    office: Literal["au", "uk", "us", "fr"] = Field(title="Office")
    joined: date = Field(title="Employment date")

Then you can get an auto-generated form with ModelForm, leveraging dash-mantine-components (version 0.14) for form inputs.

from dash_pydantic_form import ModelForm

# somewhere in your layout:
form = ModelForm(
    Employee,
    aio_id="employees",
    form_id="new_employee",
)

Simple form

You can also render a pre-filled form by passing an instance of the data model rather than the class

# NOTE: This could come from a database
bob = Employee(first_name="Bob", last_name="K", office="au", joined="2020-05-20")

form = ModelForm(
    bob,
    aio_id="employees",
    form_id="bob",
)

You can then retrieve the contents of the whole form at once in a callback as follows

from dash import Input, Output, callback

@callback(
    Output("some-output-id", "some-output-attribute"),
    Input(ModelForm.ids.main("employees", "new_employee"), "data"),
)
def use_form_data(form_data: dict):
    try:
        print(Employee(**form_data))
    except ValidationError as exc:
        print("Could not validate form data:")
        print(exc.errors())
    return # ...

Customising inputs

The ModelForm will automaticlly pick which input type to use based on the type annotation for the model field. However, you can customise how each field input is rendered, and or pass additional props to the DMC component.

from dash_pydantic_form import ModelfForm, fields

form = ModelForm(
    Employee,
    aio_id="employees",
    form_id="new_employee",
    fields_repr={
        # Change the default from a Select to Radio items
        # NOTE: `description` can be set on pydantic fields as well
        "office": fields.RadioItems(description="Wich country office?"),
        # Pass additional props to the default input field
        "joined": {"input_kwargs": {"maxDate": "2024-01-01"}},
    },
)

List of current field inputs:

Based on DMC:

  • Checkbox
  • Checklist
  • Color
  • Date
  • Json
  • MultiSelect
  • Number
  • Password
  • RadioItems
  • Range
  • SegmentedControl
  • Select
  • Slider
  • Switch
  • Textarea
  • Text
  • Time

Custom:

  • EditableTable
  • Model
  • List
  • Dict

Creating sections

There are 2 main avenues to create form sections:

1. Create a submodel in one of the model fields

class HRData(BaseModel):
    office: Literal["au", "uk", "us", "fr"] = Field(title="Office")
    joined: date = Field(title="Employment date")

class EmployeeNested(BaseModel):
    first_name: str = Field(title="First name")
    last_name: str = Field(title="Last name")
    hr_data: HRData = Field(title="HR data")

ModelForm will then recognise HRData as a pydantic model and use the fields.Model to render it, de facto creating a section.

Nested model

2. Pass sections information to ModelForm

from dash_pydantic_form import FormSection, ModelForm, Sections

form = ModelForm(
    Employee,
    aio_id="employees",
    form_id="new_employee",
    sections=Sections(
        sections=[
            FormSection(name="General", fields=["first_name", "last_name"], default_open=True),
            FormSection(name="HR data", fields=["office", "joined"], default_open=False),
        ],
        # 3 render values are available: accordion, tabs and steps
        render="tabs",
    ),
)

Form sections

List of nested models

Dash pydantic form also handles lists of nested models with the possibility to add/remove items from the list and edit each one.

Let's say we now want to record the employee's pets

1. List

This creates a list of sub-forms each of which can take similar arguments as a ModelForm (fields_repr, sections).

class Pet(BaseModel):
    name: str = Field(title="Name")
    species: Literal["cat", "dog"] = Field(title="Species")
    age: int = Field(title="Age")

class Employee(BaseModel):
    first_name: str = Field(title="First name")
    last_name: str = Field(title="Last name")
    pets: list[Pet] = Field(title="Pets", default_factory=list)

form = ModelForm(
    Employee,
    aio_id="employees",
    form_id="new_employee",
    fields_repr={
        "pets": fields.List(
            fields_repr={
                "species": {"options_labels": {"cat": "Cat", "dog": "Dog"}}
            },
            # 3 render_type options: accordion, list or modal
            render_type="accordion",
        )
    },
)

List

2. EditableTable

You can also represent the list of sub-models as an ag-grid table with fields.EditableTable.

form = ModelForm(
    Employee,
    aio_id="employees",
    form_id="new_employee",
    fields_repr={
        "pets": fields.EditableTable(
            fields_repr={
                "species": {"options_labels": {"cat": "Cat", "dog": "Dog"}}
            },
        )
    },
)

EditableTable

Make fields conditionnally visible

You can make field visibility depend on the value of other fields in the form. To do so, simply pass a visible argument to the field.

class Employee(BaseModel):
    first_name: str
    last_name: str
    only_bob: str | None = Field(
        title="Only for Bobs",
        description="What's your favourite thing about being a Bob?",
        default=None,
    )

form = ModelForm(
    Employee,
    aio_id="employees",
    form_id="new_employee",
    fields_repr={
        "only_bob": fields.Textarea(
            visible=("first_name", "==", "Bob"),
        )
    },
)

Conditionally visible field

visible accepts a boolean, a 3-tuple or list of 3-tuples with format: (field, operator, value). The available operators are:

  • "=="
  • "!="
  • "in"
  • "not in"
  • "array_contains"
  • "array_contains_any"

NOTE: The field in the 3-tuples is a ":" separated path relative to the current field's level of nesting. If you need to reference a field from a parent or the root use the special values _parent_ or _root_.

E.g., visible=("_root_:first_name", "==", "Bob")

Discriminated unions

Dash pydantic form supports Pydantic discriminated unions with str discriminator

class HomeOffice(BaseModel):
    """Home office model."""

    type: Literal["home_office"]
    has_workstation: bool = Field(title="Has workstation", description="Does the employee have a suitable workstation")


class WorkOffice(BaseModel):
    """Work office model."""

    type: Literal["work_office"]
    commute_time: int = Field(title="Commute time", description="Commute time in minutes", ge=0)

class Employee(BaseModel):
    name: str = Field(title="Name")
    work_location: HomeOffice | WorkOffice | None = Field("Work location", default=None, discriminator="type")

form = ModelForm(
    Employee,
    aio_id="employees",
    form_id="new_employee",
    fields_repr={
        "work_location": {
            "fields_repr": {
                "type": fields.RadioItems(
                    options_labels={"home_office": "Home", "work_office": "Work"}
                )
            },
        },
    }
)

Discriminated union

Creating custom fields

To be written

dash-pydantic-form's People

Contributors

renaudln avatar emilhe avatar

Stargazers

Rick_Lei2777 avatar Rajendra Agrawal avatar Feffery avatar mapix avatar Joseph Perkins avatar Maximilian Schulz avatar Fran Hrženjak avatar  avatar  avatar Kirill S. avatar Martim avatar  avatar Juan Miguel Serrano Rodríguez avatar David Harris avatar Pip Install Python avatar Snehil Vijay avatar Kohei Mito avatar Ann Marie Ward avatar  avatar

Watchers

 avatar

dash-pydantic-form's Issues

Missing license

Just wanted to ask if there were plans to add a LICENSE.txt :) ?

Constant parent id for new components in a list

Sorry it's me again.

Found an issue with this functionality

When i add a new department, and then a new sub-department for that new department, the text field created has the same parent id of colleague:department:0:sub_departments when it should be colleague:department:1:sub_departments. The parent id seems to always be colleague:department:0:sub_departments for any number of sub departments i want to add. Hence, the input changes for the sub departments of the first department in the list.

image
image

I tried digging into the code which is how I found out about the id being constant.

List[str] or List[int] not rendering

Thanks for putting up such a great module! Really excited to integrate it into my current dash application.

I have a question about BaseModels with fields of type List[str] and List[int].
I'm not sure how it works, why does a list of BaseModels render differently from a list of Python native types like int and str?

This is

from typing import List
from pydantic import BaseModel, Field, ValidationError

class Employee(BaseModel):
      name: str = Field(title="Name", description="Name of the employee", min_length=2)
      age: int = Field(title="Age", description="Age of the employee, starting from their birth", ge=18)
      mini_bio: str | None = Field(title="Mini bio", description="Short bio of the employee", default=None)
      joined: date = Field(title="Joined", description="Date when the employee joined the company")
      office: Office = Field(title="Office", description="Office of the employee")
      metadata: Metadata | None = Field(title="Employee metadata", default=None)
      location: HomeOffice | WorkOffice | None = Field(title="Work location", default=None, discriminator="type")
      pets: list[Pet] = Field(title="Pets", description="Employee pets", default_factory=list)
      jobs: list[str] = Field(title="Jobs", description="Employee jobs", default_factory=list)
      cycleTracking: List[CycleSequence]

image
image

Another question is, why is there a need to include Field object in the Jobs field? Leaving it as jobs: list[str] throws some error.
image

Wildcard matching callbacks

Hi again, I have a use case where I want to access the form by matching callback. I have multiple different types of object, each with their own prefilled form.
However, I see that the only support for this is to use the pydantic form ids.form_dependent_id

Is there a way to not use this, as this would require me to change all other callbacks to use this id type.
image

Issue with: @mantine/core: MantineProvider was not found in component tree, make sure you have it in your app

Hey! As said on the plotly forums, really cool stuff! Love it!

Was just trying it out, and for me your basic example does not work due to some error with mantine:

@mantine/core: MantineProvider was not found in component tree, make sure you have it in your app

Have you ever encountered that?

I am using the following toy code:

from datetime import date
from typing import Literal
from pydantic import BaseModel, Field
from dash import Dash, html, dcc, callback, Output, Input
import plotly.express as px
import pandas as pd

from dash_pydantic_form import ModelForm


class Employee(BaseModel):
    first_name: str = Field(title="First name")
    last_name: str = Field(title="Last name")
    office: Literal["au", "uk", "us", "fr"] = Field(title="Office")
    joined: date = Field(title="Employment date")


# somewhere in your layout:
form = ModelForm(
    Employee,
    aio_id="employees",
    form_id="new_employee",
)



df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/gapminder_unfiltered.csv')

app = Dash()

app.layout = [
    html.H1(children='Title of Dash App', style={'textAlign':'center'}),
    dcc.Dropdown(df.country.unique(), 'Canada', id='dropdown-selection'),
    dcc.Graph(id='graph-content'),
    form
]

@callback(
    Output('graph-content', 'figure'),
    Input('dropdown-selection', 'value')
)
def update_graph(value):
    dff = df[df.country==value]
    return px.line(dff, x='year', y='pop')

if __name__ == '__main__':
    app.run(debug=True)

and I am on python 3.12 and have mantine 0.14.3

Accessing item parameter from Dash callbacks

Thank you for this amazing tool! I've been trying to integrate it into existing projects and I have a question about accessing the item parameter. Is it possible to retrieve the model class that the form is built upon, similar to how we retrieve the data that the form contains?

if value:
    return ModelForm(
        item=value,
        aio_id="form",
        form_id=id_map.get(value),
    )

@callback(
    Output(ModelForm.ids.errors("MATCH", "MATCH"), "data"),
    Input(ModelForm.ids.main("MATCH", "MATCH"), "data"),
    Input(ModelForm.ids.main("MATCH", "MATCH"), "item"),
    prevent_initial_call=True,
)

I would like to render different forms based on something else that the user chose, hence I need different models and I need to call the model_validate function

Creating a Model object throws error

Hi!

I was trying to create a Model object so i can pass it into the form a generate a pre-filled form. I would like to pre-fill the form and have the form flag out any missing required parameters or wrong types. However, I get blocked by a ValidationError which makes it unable to create the object.

Would it be possible to throw a warning instead of an error so it does not block the creation of the object? I'm not sure how feasible is that but it would be nice to have such a functionality, thank you!

(update: I realise that this is something to do with pydantic model object creation and not related to this form)

Add support for List[BaseModel]

Hi again, @RenaudLN

Could support for List[BaseModel] be added when creating a prefilled form? I have a model where one of the parameters takes in a list of another model, and this other model contains parameters of floats, str, int, List[str], List[float] and List[int]. Hence, I would like to have these values populate the pre-filled form.

Example model structure

class Worker(BaseModel):
    name: str
    associates: List[str]
    
class Track(BaseModel):
    workers: List[Worker]
    count: int

The prefilled form shows
workers:[{name:"Ben", 'associates': [None]}]

even though the json_obj has a list of strings

Thanks!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.