├── .editorconfig ├── .github ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE │ ├── 01_bug-report.yml │ ├── 02_feature-request.yml │ ├── 03_documentation.yml │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md ├── SUPPORT.md ├── release.yml └── workflows │ ├── ci.yml │ ├── release.yml │ └── require-labels.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── .pages ├── README.md ├── images │ └── banner.png ├── streamlit_pydantic.settings.md └── streamlit_pydantic.ui_renderer.md ├── examples ├── complex_defaults.py ├── complex_disabled_showcase.py ├── complex_instance_model.py ├── complex_nested_model.py ├── complex_showcase.py ├── custom_form.py ├── data_validation.py ├── dataclass_form.py ├── multiple_forms.py ├── optional_fields.py ├── overwrite_streamlit_args.py ├── render_input.py ├── render_output.py ├── simple_form.py ├── streamlit_settings.py ├── union_field.py └── union_field_discriminator.py ├── playground ├── .streamlit │ └── secrets.toml └── playground_app.py ├── pyproject.toml ├── requirements-dev.lock ├── requirements.lock ├── requirements.txt ├── src └── streamlit_pydantic │ ├── __init__.py │ ├── _about.py │ ├── py.typed │ ├── schema_utils.py │ ├── settings.py │ └── ui_renderer.py └── tests ├── __init__.py ├── conftest.py └── test_renderer.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_style = space 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | max_line_length = off 14 | trim_trailing_whitespace = false 15 | 16 | [*.json] 17 | indent_size = 2 18 | 19 | [*.{yaml,yml}] 20 | indent_size = 2 21 | 22 | [*.py] 23 | indent_size = 4 24 | 25 | [*.js] 26 | indent_size = 2 27 | 28 | [*.jsx] 29 | indent_size = 2 30 | 31 | [*.java] 32 | indent_size = 2 33 | 34 | [{Makefile,**.mk}] 35 | # Use tabs for indentation (Makefiles require tabs) 36 | indent_style = tab 37 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This file provides an overview of code owners in this repository. 2 | 3 | # Each line is a file pattern followed by one or more owners. 4 | # The last matching pattern has the most precedence. 5 | # For more details, read the following article on GitHub: https://help.github.com/articles/about-codeowners/. 6 | 7 | # These are the default owners for the whole content of this repository. The default owners are automatically added as reviewers when you open a pull request, unless different owners are specified in the file. 8 | pyproject.toml @lukasmasuch 9 | .github @lukasmasuch 10 | LICENSE @lukasmasuch 11 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at team@mltooling.org. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/01_bug-report.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug report 2 | description: Submit a bug report to help us improve streamlit-pydantic. 3 | labels: ["type:bug", "status:needs-triage"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to report this problem! 9 | - type: checkboxes 10 | attributes: 11 | label: Checklist 12 | description: Please confirm and check all the following options. 13 | options: 14 | - label: I have searched the [existing issues](https://github.com/lukasmasuch/streamlit-pydantic/issues) for similar issues. 15 | required: true 16 | - label: I added a very descriptive title to this issue. 17 | required: true 18 | - label: I have provided sufficient information below to help reproduce this issue. 19 | required: true 20 | - type: textarea 21 | attributes: 22 | label: Summary 23 | description: Type here a clear and concise description of the problem. Aim for 2-3 sentences. 24 | validations: 25 | required: true 26 | - type: textarea 27 | attributes: 28 | label: Reproducible Code Example 29 | render: Python 30 | description: | 31 | If applicable, please provide a [self-contained minimal code example](https://stackoverflow.com/help/minimal-reproducible-example) that reproduces the problem you ran into. 32 | If we can copy it, run it, and see it right away, there's a much higher chance we will be able to help you. 33 | placeholder: | 34 | import streamlit_pydantic as sp 35 | 36 | data = sp.pydantic_form(key="my_form", model=ExampleModel) 37 | validations: 38 | required: false 39 | - type: textarea 40 | attributes: 41 | label: Steps To Reproduce 42 | description: Please provide the steps we should take to reproduce the bug. 43 | placeholder: | 44 | 1. Click on... 45 | 2. See error 46 | validations: 47 | required: false 48 | - type: textarea 49 | attributes: 50 | label: Expected Behavior 51 | description: Explain what you expect to happen when you go through the steps above, assuming there were no bugs. 52 | validations: 53 | required: false 54 | - type: textarea 55 | attributes: 56 | label: Current Behavior 57 | placeholder: | 58 | Error message: 59 | ``` 60 | Exception ... 61 | ``` 62 | description: | 63 | Explain the buggy behavior you experience when you go through the steps above. 64 | If you have error messages or stack traces please provide them here as well. 65 | If applicable, add screenshots to help explain your problem. 66 | 67 | _Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in._ 68 | validations: 69 | required: false 70 | - type: checkboxes 71 | attributes: 72 | label: Is this a regression? 73 | description: Did this use to work the way you expected in the past? 74 | options: 75 | - label: Yes, this used to work in a previous version. 76 | required: false 77 | - type: textarea 78 | attributes: 79 | label: Debug info 80 | description: | 81 | Please share some system information related to the environment your app is running in. 82 | 83 | Example: 84 | - **streamlit-pydantic version**: 0.6.0 85 | - **Python version**: 3.9 _(get it with `$ python --version`)_ 86 | value: | 87 | - streamlit-pydantic version: 88 | - Python version: 89 | validations: 90 | required: false 91 | - type: textarea 92 | attributes: 93 | label: Additional Information 94 | description: | 95 | Links? References? Anything that will give us more context about the issue you are encountering! 96 | 97 | _Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in._ 98 | validations: 99 | required: false 100 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/02_feature-request.yml: -------------------------------------------------------------------------------- 1 | name: ✨ Feature request 2 | description: Suggest a feature or enhancement. 3 | labels: [type:enhancement, "status:needs-triage"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to suggest a feature or enhancement ❤️ 9 | - type: checkboxes 10 | attributes: 11 | label: Checklist 12 | description: Please confirm and check all the following options. 13 | options: 14 | - label: I have searched the [existing issues](https://github.com/lukasmasuch/streamlit-pydantic/issues) for similar feature requests. 15 | required: true 16 | - label: I added a descriptive title and summary to this issue. 17 | required: true 18 | - type: textarea 19 | attributes: 20 | label: Summary 21 | description: Type here a clear and concise description of the feature or enhancement request. Aim for 2-3 sentences. 22 | validations: 23 | required: true 24 | - type: textarea 25 | attributes: 26 | label: Why? 27 | description: Please outline the problem, motivation, or use case related to this feature request. 28 | placeholder: | 29 | I'm always frustrated when ... 30 | validations: 31 | required: false 32 | - type: textarea 33 | attributes: 34 | label: How? 35 | description: | 36 | Please describe the solution or implementation you'd like to see. 37 | Don't worry if you don't have a clear solution in mind; any input helps! 38 | placeholder: | 39 | Introduce a new command called `sp.pydantic_foo` with the following set of parameters... 40 | validations: 41 | required: false 42 | - type: textarea 43 | attributes: 44 | label: Additional Context 45 | description: | 46 | Links? References? Anything that will give us more context about the feature request here! 47 | 48 | _Tip: You can attach images by clicking this area to highlight it and then dragging files in._ 49 | validations: 50 | required: false 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/03_documentation.yml: -------------------------------------------------------------------------------- 1 | name: 📝 Documentation 2 | description: Is there a mistake in the docs, is anything unclear or do you have a suggestion? 3 | labels: [type:docs, "status:needs-triage"] 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: Describe your request 8 | description: Describe the problem or suggestion here. If you've found a mistake and you know the answer, feel free to submit a pull request straight away. 9 | validations: 10 | required: true 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: 🧑‍💻 Usage Problem 4 | url: https://github.com/lukasmasuch/streamlit-pydantic/discussions/new?category=q-a 5 | about: Do you have problems with usage and none of the suggestions in the docs helped? 6 | - name: ❓ Anything else? 7 | url: https://github.com/lukasmasuch/streamlit-pydantic/discussions 8 | about: Do you have any other question? 9 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Describe your changes 2 | 3 | ## Related GitHub Issue 4 | 5 | --- 6 | 7 | **Contribution License Agreement** 8 | 9 | By submitting this pull request you agree that all contributions to this project are made under the MIT license. 10 | -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | Please refer to our [support](../../../#support--feedback) and [contribution](../../../#contribution) sections on our main README for more information. 4 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | categories: 3 | - title: 💥 Breaking Changes 4 | labels: 5 | - change:breaking 6 | - title: ✨ Features & Enhancements 7 | labels: 8 | - change:feature 9 | - title: 🐛 Bug Fixes 10 | labels: 11 | - change:fix 12 | - title: Others 13 | labels: 14 | - change:internal 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | types: [opened, synchronize, reopened] 8 | workflow_call: 9 | jobs: 10 | lint: 11 | name: lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Install Rye 16 | uses: eifinger/setup-rye@v4 17 | - name: Install dependencies 18 | run: | 19 | rye sync 20 | if [[ -n $(git diff --stat requirements.lock) ]]; then 21 | echo "requirements.lock has changed. Exiting the workflow." 22 | echo "Please run 'rye sync' locally and push the updated lockfile." 23 | exit 1 24 | fi 25 | - name: Run linters 26 | run: rye lint 27 | 28 | test: 29 | name: test 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | - name: Install Rye 34 | uses: eifinger/setup-rye@v4 35 | - name: Install dependencies 36 | run: | 37 | rye sync 38 | if [[ -n $(git diff --stat requirements.lock) ]]; then 39 | echo "requirements.lock has changed. Exiting the workflow." 40 | echo "Please run 'rye sync' locally and push the updated lockfile." 41 | exit 1 42 | fi 43 | - name: Run tests 44 | run: rye test 45 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release on PyPI 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | run-tests: 7 | uses: ./.github/workflows/ci.yml 8 | 9 | publish: 10 | needs: 11 | - run-tests 12 | 13 | environment: 14 | name: release # needed for PyPI OIDC 15 | url: https://pypi.org/p/streamlit-pydantic 16 | runs-on: ubuntu-latest 17 | permissions: 18 | id-token: write 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Install Rye 22 | uses: eifinger/setup-rye@v4 23 | - name: Build package 24 | run: rye build 25 | - name: Publish on PyPI 26 | uses: pypa/gh-action-pypi-publish@release/v1 27 | -------------------------------------------------------------------------------- /.github/workflows/require-labels.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Labels 2 | 3 | on: 4 | pull_request: 5 | types: [opened, labeled, unlabeled, synchronize] 6 | 7 | jobs: 8 | do-not-merge-guardrail: 9 | runs-on: ubuntu-latest 10 | 11 | permissions: 12 | pull-requests: read 13 | 14 | steps: 15 | - uses: mheap/github-action-required-labels@5847eef68201219cf0a4643ea7be61e77837bbce 16 | with: 17 | mode: exactly 18 | count: 0 19 | labels: "do-not-merge" 20 | change-description: 21 | runs-on: ubuntu-latest 22 | 23 | permissions: 24 | pull-requests: read 25 | 26 | steps: 27 | - uses: mheap/github-action-required-labels@5847eef68201219cf0a4643ea7be61e77837bbce 28 | with: 29 | mode: exactly 30 | count: 1 31 | labels: "change:feature,change:fix,change:internal" 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ 2 | target/ 3 | .idea/ 4 | research/ 5 | *.iml 6 | 7 | # Sublime 8 | *.sublime-workspace 9 | 10 | # VS Code 11 | .project 12 | .classpath 13 | # Shared VS Code Settings 14 | .vscode/* 15 | # Ignore all local history of files 16 | **/.history 17 | 18 | # Python 19 | *.pyc 20 | *.egg-info 21 | __pycache__/ 22 | .ipynb_checkpoints 23 | .Python 24 | dist/ 25 | .python-version 26 | .installed.cfg 27 | *.egg 28 | reqlib-metadata 29 | .mypy_cache/ 30 | .venv 31 | venv/ 32 | build/ 33 | wheels/ 34 | *.py[cod] 35 | *$py.class 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .nox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Runtime data 51 | pids 52 | *.pid 53 | *.seed 54 | *.pid.lock 55 | 56 | # Logs 57 | logs 58 | *.log 59 | npm-debug.log* 60 | yarn-debug.log* 61 | yarn-error.log* 62 | lerna-debug.log* 63 | 64 | # vim temporary files 65 | *~ 66 | .*.sw? 67 | 68 | # Other Artifacts 69 | hs_err_pid* 70 | *.log 71 | *.swp 72 | *.swo 73 | temp/* 74 | .DS_Store 75 | 76 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | # Contribute to Streamlit Pydantic 3 | 4 | Thanks for your interest in contributing to our project. This page will give you a quick overview of how things are organized and, most importantly, how to get involved. 5 | 6 | Everyone is welcome to contribute, and we value everybody's contribution. Code is thus not the only way to help the community. Answering questions, helping others, reaching out and improving the documentations are immensely valuable to the projects as well. 7 | 8 | ## Table of contents 9 | 10 | 1. [Issues and bug reports](#issues-and-bug-reports) 11 | 2. [Contributing to the code base](#contributing-to-the-code-base) 12 | - [Development instructions](#development-instructions) 13 | - [Commit messages guidelines](#commit-messages-guidelines) 14 | - [Opening a pull request](#opening-a-pull-request) 15 | - [Review & merging of a pull request](#review--merging-of-a-pull-request) 16 | - [Git workflow & versioning](#git-workflow--versioning) 17 | 3. [Code conventions](#code-conventions) 18 | - [Python conventions](#python-conventions) 19 | 4. [Code of conduct](#code-of-conduct) 20 | 21 | ## Issues and bug reports 22 | 23 | - We use GitHub issues to track bugs and enhancement requests. Submit issues for any [feature request and enhancement](https://github.com/lukasmasuch/streamlit-pydantic/issues/new?assignees=&labels=feature&template=02_feature-request.md&title=), [bugs](https://github.com/lukasmasuch/streamlit-pydantic/issues/new?assignees=&labels=bug&template=01_bug-report.md&title=), or [documentation](https://github.com/lukasmasuch/streamlit-pydantic/issues/new?assignees=&labels=documentation&template=03_documentation.md&title=) problems. 24 | - First, do a quick search on the Github issue tracker or the known issues section in the readme to see if the issue has already been reported. If so, it's often better to just leave a comment on an existing issue rather than creating a new one. Old - and sometimes closed - issues also often include helpful tips and solutions to common problems. 25 | - When creating an issue, try using one of our [issue templates](https://github.com/lukasmasuch/streamlit-pydantic/issues/new/choose) which already contain some guidelines on which content is expected to process the issue most efficiently. If no template applies, you can of course also create an issue from scratch. 26 | - Please provide as much context as possible when you open an issue. The information you provide must be comprehensive enough to reproduce that issue for the assignee. Therefore, contributors should use but aren't restricted to the issue template provided by the project maintainers. 27 | - Please apply one or more applicable [labels](https://github.com/lukasmasuch/streamlit-pydantic/labels) to your issue so that all community members are able to cluster the issues better. 28 | - If you have questions about one of the existing issues, please comment on them, and one of the maintainers will clarify. 29 | 30 | ## Contributing to the code base 31 | 32 | You are welcome to contribute code in order to fix a bug, to implement a new feature, to propose new documentation, or just to fix a typo. Check out [good first issue](https://github.com/lukasmasuch/streamlit-pydantic/labels/good%20first%20issue) and [help wanted](https://github.com/lukasmasuch/streamlit-pydantic/labels/help%20wanted) issues if you want to find open issues to implement. 33 | 34 | - Before writing code, we strongly advise you to search through the existing PRs or issues to make sure that nobody is already working on the same thing. If you find your issue already exists, make relevant comments and add your reaction (👍 - upvote, 👎 - downvote). If you are unsure, it is always a good idea to open an issue to get some feedback. 35 | - Should you wish to work on an existing issue that has not yet been claimed, please claim it first by commenting on the GitHub issue that you want to work on and begin work (the maintainers will assign it to your GitHub user as soon as they can). This is to prevent duplicated efforts from other contributors on the same issue. 36 | - To contribute changes, always branch from the `main` branch and after implementing the changes create a pull request as described [below](#opening-a-pull-request). 37 | - Commits should be as small as possible while ensuring that each commit is correct independently (i.e., each commit should compile and pass tests). Also, make sure to follow the commit message guidelines. 38 | - Test your changes as thoroughly as possible before you commit them. Preferably, automate your test by unit/integration tests. 39 | 40 | ### Commit messages guidelines 41 | 42 | Commit messages should be as standardized as possible within the repository. A few best practices: 43 | 44 | 1. Always use simple present (imperative mood) to describe what the commit does. Explain what & why, not how! 45 | 2. Start with a capital letter. 46 | 3. Don’t end the subject line with a period. 47 | 4. Descriptive but short subject line (< 50 chars). 48 | 5. Link to issues by mentioning them in commit messages. 49 | 6. Examples: `Add image to documentation section 3`, `Fix memory leak. Closes #3`, `Split method X into two methods`. Refer to [this blog](https://chris.beams.io/posts/git-commit/) for more information about good commit messages. 50 | 51 | ### Opening a pull request 52 | 53 | 1. **Set title**. The title should follow our [commit message guidelines](#commit-messages-guidelines) (example: `Fix memory leak in picture loader`). If the pull request closes a specific issue, the title can be used to mention the issue (example: `Fix memory leak in picture loader. Closes #3`). Prefix the title with `[WIP]` *(Work In Progress)* to indicate that you are not done but need clarification or an explicit review before you can continue your work item. 54 | 2. **Add appropriate labels** (e.g. bug, enhancement, documentation). 55 | 3. **Set description:** Describe what the pull request is about and add some bullet points describing what’s changed and why (make use of the provided template). Link the pull request to all relevant issues in the pull request description (e.g. `Closes #10`). Find more information on linking pull requests to issues [here](https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue). Add `BREAKING CHANGE` into the description in case the PR introduces breaking changes. 56 | 4. Open the pull request and make sure existing tests and checks pass. The PR will only be merged into `main` if it is consistent with style and linting guidelines. 57 | 58 | ### Review & merging of a pull request 59 | 60 | 1. Every pull request will be reviewed by at least 1 reviewer and will also trigger CI pipelines to automatically build and test the changes. If your PR is not getting reviewed for a longer time, you can @-reply a reviewer in the pull request or comment. 61 | 2. Every comment on PR should be accepted as a change request and should be discussed. When something is optional, it should be noted in the comment. If a review requires you to make additional changes, please test the changes again. Create a comment on the PR to notify the reviewers that your amendments are ready for another round of review. 62 | 3. Once the pull request is approved by at least 1 reviewer, the pull request can be merged. `Squash & merge` is the preferred merging strategy. 63 | 4. In case a new (feature) branch was created in the main repository, please delete this branch after a successful merge. 64 | 65 | ### Git workflow & versioning 66 | 67 | - We use [GitLab Flow](https://docs.gitlab.com/ee/university/training/gitlab_flow.html#release-branch) as our branching workflow. 68 | - Feature branching: Use (short-lived) feature branches. Naming schema: `feature/`, if related to one specific issue use suffix: `feature//`. In addition to the `feature/` prefix, we recommend using more descriptive feature branch prefixes such as `bugfix/` for bug-fixes, `maintenance/` for maintenance or refactoring changes, and `documentation/` for changes related to the documentation. 69 | - For versioning, we have adapted the [Semantic Versioning](https://semver.org/) standard. All version tags on Git must be prefixed with `v` (e.g.`v..`). The version can be suffixed with a tag that indicates a pre-release or unfinished version. All our local development builds will use the following dev suffix: `-dev.`. 70 | 71 | ## Code conventions 72 | 73 | - Code Style: [PEP8](https://www.python.org/dev/peps/pep-0008/) 74 | - Documentation Style: [Google Style](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html) 75 | - Naming Conventions: [naming-convention-guides](https://github.com/naming-convention/naming-convention-guides/tree/master/python#python-naming-convention) 76 | - Code Formatter: [ruff](https://docs.astral.sh/ruff/) 77 | - Import Sorting: [ruff](https://docs.astral.sh/ruff/) 78 | - Linting: [ruff](https://docs.astral.sh/ruff/) 79 | - Type Checking: [mypy](https://github.com/python/mypy) 80 | - Testing: [pytest](http://doc.pytest.org/) 81 | - Package Manager: [rye](https://rye.astral.sh/) 82 | - Use type hints wherever possible: [Cheatsheet](https://mypy.readthedocs.io/en/latest/cheat_sheet_py3.html) 83 | - Minimum compatibility: Python 3.8 84 | 85 | ### Code formatting 86 | 87 | We use [ruff](https://docs.astral.sh/ruff/) for code formatting and import sorting. The following commands runs the formatting on all Python files: 88 | 89 | ```bash 90 | rye run format 91 | ``` 92 | 93 | ### Code linting 94 | 95 | We use [ruff](https://docs.astral.sh/ruff/) for linting and [mypy](https://github.com/python/mypy) for type checking. You can find our ruff configuration inside the `pyproject.toml`. The following commands runs all linting and type checks on all python files: 96 | 97 | ```bash 98 | rye run checks 99 | ``` 100 | 101 | ### Adding & running tests 102 | 103 | We use the [pytest](http://doc.pytest.org/) framework for testing. For more info on this, see the [pytest documentation](http://docs.pytest.org/en/latest/contents.html). Tests for modules and classes live in their own directories of the same name inside the `tests` folder. To be discovered, all test files and test functions need to be prefixed with `test_`. To run the test suite, execute: 104 | 105 | ```bash 106 | rye test 107 | ``` 108 | 109 | When adding tests, make sure to use descriptive names, keep the code short and concise and only test for one behavior at a time. Try to avoid unnecessary imports and use `parametrize` test cases wherever possible. Parametrizing tests allows to test multiple inputs to a function and verify that they return the expected output. Use [fixtures](https://docs.pytest.org/en/stable/fixture.html) to share test setups with - optional - setup and tear-down routines. Fixtures can also be parameterized. 110 | 111 | ## Code of Conduct 112 | 113 | All members of the project community must abide by the [Contributor Covenant, version 2.0](./.github/CODE_OF_CONDUCT.md). Only by respecting each other we can develop a productive, collaborative community. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting a project maintainer. 114 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Machine Learning Tooling 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | Streamlit Pydantic 4 |

5 | 6 |

7 | Auto-generate Streamlit UI elements from Pydantic models. 8 |

9 | 10 | 11 |

12 | 13 | 14 | 15 | 16 | 17 |

18 | 19 |

20 | Getting Started • 21 | Documentation • 22 | Support • 23 | Report a Bug • 24 | Contribution • 25 | Changelog 26 |

27 | 28 | Streamlit-pydantic makes it easy to auto-generate UI elements from [Pydantic](https://github.com/samuelcolvin/pydantic/) models or [dataclasses](https://docs.python.org/3/library/dataclasses.html). Just define your data model and turn it into a full-fledged UI form. It supports data validation, nested models, and field limitations. Streamlit-pydantic can be easily integrated into any Streamlit app. 29 | 30 | 31 | 32 | --- 33 | 34 |

35 | Try out and explore various examples in our playground here. 36 |

37 | 38 | --- 39 | 40 | ## Highlights 41 | 42 | - 🪄  Auto-generated UI elements from Pydantic models & Dataclasses. 43 | - 📇  Out-of-the-box data validation. 44 | - 📑  Supports nested Pydantic models. 45 | - 📏  Supports field limits and customizations. 46 | - 🎈  Easy to integrate into any Streamlit app. 47 | 48 | ## Getting Started 49 | 50 | ### Installation 51 | 52 | ```bash 53 | pip install streamlit-pydantic 54 | ``` 55 | 56 | ### Usage 57 | 58 | 1. Create a script (`my_script.py`) with a Pydantic model and render it via `pydantic_form`: 59 | 60 | ```python 61 | import streamlit as st 62 | import streamlit_pydantic as sp 63 | from pydantic import BaseModel 64 | 65 | 66 | class ExampleModel(BaseModel): 67 | some_text: str 68 | some_number: int 69 | some_boolean: bool 70 | 71 | data = sp.pydantic_form(key="my_sample_form", model=ExampleModel) 72 | if data: 73 | st.json(data.model_dump()) 74 | ``` 75 | 76 | 2. Run the Streamlit server on the Python script: `streamlit run my_script.py` 77 | 78 | 3. You can find additional examples in the [examples](#examples) section below. 79 | 80 | ## Examples 81 | 82 | --- 83 | 84 |

85 | 👉  Try out and explore these examples in our playground here 86 |

87 | 88 | --- 89 | 90 | The following collection of examples demonstrates how Streamlit Pydantic can be applied in more advanced scenarios. You can find additional - even more advanced - examples in the [examples folder](./examples) or on the [playground](https://st-pydantic.streamlit.app/). 91 | 92 | ### Simple Form 93 | 94 | ```python 95 | import streamlit as st 96 | import streamlit_pydantic as sp 97 | from pydantic import BaseModel 98 | 99 | 100 | class ExampleModel(BaseModel): 101 | some_text: str 102 | some_number: int 103 | some_boolean: bool 104 | 105 | data = sp.pydantic_form(key="my_sample_form", model=ExampleModel) 106 | if data: 107 | st.json(data.model_dump()) 108 | ``` 109 | 110 | ### Date Validation 111 | 112 | ```python 113 | import streamlit as st 114 | import streamlit_pydantic as sp 115 | from pydantic import BaseModel, Field, HttpUrl 116 | from pydantic_extra_types.color import Color 117 | 118 | class ExampleModel(BaseModel): 119 | url: HttpUrl 120 | color: Color = Field("blue", format="text") 121 | email: str = Field(..., max_length=100, regex=r"^\S+@\S+$") 122 | 123 | data = sp.pydantic_form(key="my_form", model=ExampleModel) 124 | if data: 125 | st.json(data.model_dump_json()) 126 | ``` 127 | 128 | ### Dataclasses Support 129 | 130 | ```python 131 | import dataclasses 132 | import json 133 | 134 | import streamlit as st 135 | from pydantic.json import pydantic_encoder 136 | 137 | import streamlit_pydantic as sp 138 | 139 | 140 | @dataclasses.dataclass 141 | class ExampleModel: 142 | some_number: int 143 | some_boolean: bool 144 | some_text: str = "default input" 145 | 146 | 147 | data = sp.pydantic_form(key="my_dataclass_form", model=ExampleModel) 148 | if data: 149 | st.json(dataclasses.asdict(data)) 150 | ``` 151 | 152 | ### Complex Nested Model 153 | 154 | ```python 155 | from enum import Enum 156 | from typing import Set 157 | 158 | import streamlit as st 159 | from pydantic import BaseModel, Field 160 | 161 | import streamlit_pydantic as sp 162 | 163 | 164 | class OtherData(BaseModel): 165 | text: str 166 | integer: int 167 | 168 | 169 | class SelectionValue(str, Enum): 170 | FOO = "foo" 171 | BAR = "bar" 172 | 173 | 174 | class ExampleModel(BaseModel): 175 | long_text: str = Field( 176 | ..., format="multi-line", description="Unlimited text property" 177 | ) 178 | integer_in_range: int = Field( 179 | 20, 180 | ge=10, 181 | le=30, 182 | multiple_of=2, 183 | description="Number property with a limited range.", 184 | ) 185 | single_selection: SelectionValue = Field( 186 | ..., description="Only select a single item from a set." 187 | ) 188 | multi_selection: Set[SelectionValue] = Field( 189 | ..., description="Allows multiple items from a set." 190 | ) 191 | read_only_text: str = Field( 192 | "Lorem ipsum dolor sit amet", 193 | description="This is a ready only text.", 194 | readOnly=True, 195 | ) 196 | single_object: OtherData = Field( 197 | ..., 198 | description="Another object embedded into this model.", 199 | ) 200 | 201 | 202 | data = sp.pydantic_form(key="my_form", model=ExampleModel) 203 | if data: 204 | st.json(data.model_dump_json()) 205 | ``` 206 | 207 | ### Render Input 208 | 209 | ```python 210 | from pydantic import BaseModel 211 | 212 | import streamlit_pydantic as sp 213 | 214 | 215 | class ExampleModel(BaseModel): 216 | some_text: str 217 | some_number: int = 10 # Optional 218 | some_boolean: bool = True # Option 219 | 220 | 221 | input_data = sp.pydantic_input( 222 | "model_input", model=ExampleModel, group_optional_fields="sidebar" 223 | ) 224 | ``` 225 | 226 | ### Render Output 227 | 228 | ```python 229 | import datetime 230 | 231 | from pydantic import BaseModel, Field 232 | 233 | import streamlit_pydantic as sp 234 | 235 | 236 | class ExampleModel(BaseModel): 237 | text: str = Field(..., description="A text property") 238 | integer: int = Field(..., description="An integer property.") 239 | date: datetime.date = Field(..., description="A date.") 240 | 241 | 242 | instance = ExampleModel(text="Some text", integer=40, date=datetime.date.today()) 243 | sp.pydantic_output(instance) 244 | ``` 245 | 246 | ### Custom Form 247 | 248 | ```python 249 | import streamlit as st 250 | from pydantic import BaseModel 251 | 252 | import streamlit_pydantic as sp 253 | 254 | 255 | class ExampleModel(BaseModel): 256 | some_text: str 257 | some_number: int = 10 258 | some_boolean: bool = True 259 | 260 | 261 | with st.form(key="pydantic_form"): 262 | data = sp.pydantic_input(key="my_custom_form_model", model=ExampleModel) 263 | submit_button = st.form_submit_button(label="Submit") 264 | obj = ExampleModel(data) 265 | 266 | if data: 267 | st.json(obj.model_dump()) 268 | ``` 269 | 270 | ## Support & Feedback 271 | 272 | | Type | Channel | 273 | | ------------------------ | ------------------------------------------------------ | 274 | | 🐛  **Bug Reports** | | 275 | | ✨  **Feature Requests** | | 276 | | 👩‍💻  **Usage Questions** | | 277 | | 📢  **Announcements** | | 278 | 279 | ## Documentation 280 | 281 | The API documentation can be found [here](./docs). To generate UI elements, you can use the high-level [`pydantic_form`](./docs/streamlit_pydantic.ui_renderer.md#function-pydantic_form) method. Or the more flexible lower-level [`pydantic_input`](./docs/streamlit_pydantic.ui_renderer.md#function-pydantic_input) and [`pydantic_output`](./docs/streamlit_pydantic.ui_renderer.md#function-pydantic_output) methods. See the [examples](#examples) section on how to use those methods. 282 | 283 | ## Contribution 284 | 285 | - Pull requests are encouraged and always welcome. Read our [contribution guidelines](https://github.com/lukasmasuch/streamlit-pydantic/tree/main/CONTRIBUTING.md) and check out [help-wanted](https://github.com/lukasmasuch/streamlit-pydantic/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+label%3A"help+wanted"+sort%3Areactions-%2B1-desc+) issues. 286 | - Submit Github issues for any [feature request and enhancement](https://github.com/lukasmasuch/streamlit-pydantic/issues/new?assignees=&labels=type%3Aenhancement%2Cstatus%3Aneeds-triage&projects=&template=02_feature-request.yml), [bugs](https://github.com/lukasmasuch/streamlit-pydantic/issues/new?assignees=&labels=type%3Abug%2Cstatus%3Aneeds-triage&projects=&template=01_bug-report.yml), or [documentation](https://github.com/lukasmasuch/streamlit-pydantic/issues/new?assignees=&labels=type%3Adocs%2Cstatus%3Aneeds-triage&projects=&template=03_documentation.yml) problems. 287 | - By participating in this project, you agree to abide by its [Code of Conduct](https://github.com/lukasmasuch/streamlit-pydantic/blob/main/.github/CODE_OF_CONDUCT.md). 288 | - The [development section](#development) below contains information on how to build and test the project after you have implemented some changes. 289 | 290 | ## Development 291 | 292 |

293 | Linting & testing 294 | Ruff 295 | Rye 296 | mypy 297 |

298 | 299 | 300 | 301 | 302 | This repo uses [Rye](https://rye.astral.sh/) for development. To get started, [install Rye](https://rye.astral.sh/) and sync the project: 303 | 304 | ```bash 305 | rye sync 306 | ``` 307 | 308 | Run the playground app: 309 | 310 | ```bash 311 | rye run playground 312 | ``` 313 | 314 | Run linting and type checks: 315 | 316 | ```bash 317 | rye run checks 318 | ``` 319 | 320 | > [!TIP] 321 | > The linting and formatting is using [ruff](https://github.com/astral-sh/ruff) and 322 | > type-checking is done with [mypy](https://github.com/python/mypy). You can use 323 | > the ruff and mypy extensions of your IDE to automatically run these checks 324 | > during development. 325 | 326 | Format the code: 327 | 328 | ```bash 329 | rye run format 330 | ``` 331 | 332 | Run tests: 333 | 334 | ```bash 335 | rye test 336 | ``` 337 | 338 | --- 339 | 340 | Licensed **MIT**. 341 | -------------------------------------------------------------------------------- /docs/.pages: -------------------------------------------------------------------------------- 1 | title: API Reference 2 | nav: 3 | - Overview: README.md 4 | - ... 5 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # API Overview 4 | 5 | ## Modules 6 | 7 | - [`streamlit_pydantic.settings`](./streamlit_pydantic.settings.md#module-streamlit_pydanticsettings) 8 | - [`streamlit_pydantic.ui_renderer`](./streamlit_pydantic.ui_renderer.md#module-streamlit_pydanticui_renderer) 9 | 10 | ## Classes 11 | 12 | - [`settings.StreamlitSettings`](./streamlit_pydantic.settings.md#class-streamlitsettings) 13 | - [`ui_renderer.GroupOptionalFieldsStrategy`](./streamlit_pydantic.ui_renderer.md#class-groupoptionalfieldsstrategy): An enumeration. 14 | 15 | ## Functions 16 | 17 | - [`ui_renderer.pydantic_form`](./streamlit_pydantic.ui_renderer.md#function-pydantic_form): Auto-generates a Streamlit form based on the given (Pydantic-based) input class. 18 | - [`ui_renderer.pydantic_input`](./streamlit_pydantic.ui_renderer.md#function-pydantic_input): Auto-generates input UI elements for a selected Pydantic class. 19 | - [`ui_renderer.pydantic_output`](./streamlit_pydantic.ui_renderer.md#function-pydantic_output): Auto-generates output UI elements for all properties of a (Pydantic-based) model instance. 20 | 21 | 22 | --- 23 | 24 | _This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ 25 | -------------------------------------------------------------------------------- /docs/images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasmasuch/streamlit-pydantic/9f84145b6b6e74cdff3a7815ab75b0464c4d4f24/docs/images/banner.png -------------------------------------------------------------------------------- /docs/streamlit_pydantic.settings.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # module `streamlit_pydantic.settings` 6 | 7 | 8 | 9 | 10 | 11 | 12 | --- 13 | 14 | 15 | 16 | ## class `StreamlitSettings` 17 | 18 | 19 | 20 | 21 | 22 | --- 23 | 24 | #### property model_extra 25 | 26 | Get extra fields set during validation. 27 | 28 | 29 | 30 | **Returns:** 31 | A dictionary of extra fields, or `None` if `config.extra` is not set to `"allow"`. 32 | 33 | --- 34 | 35 | #### property model_fields_set 36 | 37 | Returns the set of fields that have been explicitly set on this model instance. 38 | 39 | 40 | 41 | **Returns:** 42 | A set of strings representing the fields that have been set, i.e. that were not filled from defaults. 43 | 44 | 45 | 46 | 47 | 48 | 49 | --- 50 | 51 | _This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ 52 | -------------------------------------------------------------------------------- /docs/streamlit_pydantic.ui_renderer.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # module `streamlit_pydantic.ui_renderer` 6 | 7 | 8 | 9 | 10 | 11 | --- 12 | 13 | 14 | 15 | ## function `pydantic_input` 16 | 17 | ```python 18 | pydantic_input( 19 | key: str, 20 | model: Type[BaseModel], 21 | group_optional_fields: GroupOptionalFieldsStrategy = 'no', 22 | lowercase_labels: bool = False, 23 | ignore_empty_values: bool = False 24 | ) → Dict 25 | ``` 26 | 27 | Auto-generates input UI elements for a selected Pydantic class. 28 | 29 | 30 | 31 | **Args:** 32 | 33 | - `key` (str): A string that identifies the form. Each form must have its own key. 34 | - `model` (Type[BaseModel]): The input model. Either a class or instance based on Pydantic `BaseModel` or Python `dataclass`. 35 | - `group_optional_fields` (str, optional): If `sidebar`, optional input elements will be rendered on the sidebar. If `expander`, optional input elements will be rendered inside an expander element. Defaults to `no`. 36 | - `lowercase_labels` (bool): If `True`, all input element labels will be lowercased. Defaults to `False`. 37 | - `ignore_empty_values` (bool): If `True`, empty values for strings and numbers will not be stored in the session state. Defaults to `False`. 38 | 39 | 40 | 41 | **Returns:** 42 | 43 | - `Dict`: A dictionary with the current state of the input data. 44 | 45 | 46 | --- 47 | 48 | 49 | 50 | ## function `pydantic_output` 51 | 52 | ```python 53 | pydantic_output(output_data: Any) → None 54 | ``` 55 | 56 | Auto-generates output UI elements for all properties of a (Pydantic-based) model instance. 57 | 58 | 59 | 60 | **Args:** 61 | 62 | - `output_data` (Any): The output data. 63 | 64 | 65 | --- 66 | 67 | 68 | 69 | ## function `pydantic_form` 70 | 71 | ```python 72 | pydantic_form( 73 | key: str, 74 | model: Type[~T], 75 | submit_label: str = 'Submit', 76 | clear_on_submit: bool = False, 77 | group_optional_fields: GroupOptionalFieldsStrategy = 'no', 78 | lowercase_labels: bool = False, 79 | ignore_empty_values: bool = False 80 | ) → Union[~T, NoneType] 81 | ``` 82 | 83 | Auto-generates a Streamlit form based on the given (Pydantic-based) input class. 84 | 85 | 86 | 87 | **Args:** 88 | 89 | - `key` (str): A string that identifies the form. Each form must have its own key. 90 | - `model` (Type[BaseModel]): The input model. Either a class or instance based on Pydantic `BaseModel` or Python `dataclass`. 91 | - `submit_label` (str): A short label explaining to the user what this button is for. Defaults to “Submit”. 92 | - `clear_on_submit` (bool): If True, all widgets inside the form will be reset to their default values after the user presses the Submit button. Defaults to False. 93 | - `group_optional_fields` (str, optional): If `sidebar`, optional input elements will be rendered on the sidebar. If `expander`, optional input elements will be rendered inside an expander element. Defaults to `no`. 94 | - `lowercase_labels` (bool): If `True`, all input element labels will be lowercased. Defaults to `False`. 95 | - `ignore_empty_values` (bool): If `True`, empty values for strings and numbers will not be stored in the session state. Defaults to `False`. 96 | 97 | 98 | 99 | **Returns:** 100 | 101 | - `Optional[BaseModel]`: An instance of the given input class, if the submit button is used and the input data passes the Pydantic validation. 102 | 103 | 104 | --- 105 | 106 | 107 | 108 | ## class `GroupOptionalFieldsStrategy` 109 | An enumeration. 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | --- 118 | 119 | _This file was automatically generated via [lazydocs](https://github.com/ml-tooling/lazydocs)._ 120 | -------------------------------------------------------------------------------- /examples/complex_defaults.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Dict, List, Set 3 | 4 | import streamlit as st 5 | from pydantic import BaseModel, Field 6 | from pydantic.color import Color 7 | 8 | import streamlit_pydantic as sp 9 | 10 | 11 | class OtherData(BaseModel): 12 | text: str = "default_text" 13 | integer: int = 99 14 | 15 | 16 | class SelectionValue(str, Enum): 17 | FOO = "foo" 18 | BAR = "bar" 19 | 20 | 21 | class ExampleModel(BaseModel): 22 | """A model to showcase & test different types of pydantic fields with default values.""" 23 | 24 | long_text: str = Field( 25 | "default string", format="multi-line", description="Unlimited text property" 26 | ) 27 | integer_in_range: int = Field( 28 | 22, 29 | ge=10, 30 | le=30, 31 | multiple_of=2, 32 | description="Number property with a limited range", 33 | ) 34 | single_selection: SelectionValue = Field( 35 | "bar", description="Only select a single item from a set." 36 | ) 37 | multi_selection: Set[SelectionValue] = Field( 38 | "bar", description="Allows multiple items from a set." 39 | ) 40 | read_only_text: str = Field( 41 | "Lorem ipsum dolor sit amet", 42 | description="This is ready only text.", 43 | readOnly=True, 44 | ) 45 | default_color: Color = Field("yellow", description="A defaulted color") 46 | default_object: OtherData = Field( 47 | OtherData(), 48 | description="An object embedded into the model with a default", 49 | ) 50 | overriden_default_object: OtherData = Field( 51 | OtherData(text="overridden object text", integer="12"), 52 | description="Default object overrides the embedded object defaults", 53 | ) 54 | default_dict: Dict[str, str] = {"foo": "bar"} 55 | default_list: List[str] = ["foo", "bar"] 56 | default_object_list: List[OtherData] = Field( 57 | [OtherData()], 58 | description="A list of objects with a default object in the list", 59 | ) 60 | 61 | 62 | data = sp.pydantic_input(key="my_default_input", model=ExampleModel) 63 | if data: 64 | st.json(data) 65 | -------------------------------------------------------------------------------- /examples/complex_disabled_showcase.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from enum import Enum 3 | from typing import Dict, List, Literal, Set 4 | 5 | import streamlit as st 6 | from pydantic import Base64UrlBytes, BaseModel, Field, SecretStr 7 | from pydantic_extra_types.color import Color 8 | 9 | import streamlit_pydantic as sp 10 | 11 | 12 | class SelectionValue(str, Enum): 13 | FOO = "foo" 14 | BAR = "bar" 15 | 16 | 17 | class OtherData(BaseModel): 18 | text: str 19 | integer: int 20 | 21 | 22 | class DisabledModel(BaseModel): 23 | short_text: str = Field( 24 | ..., readOnly=True, max_length=60, description="Short text property" 25 | ) 26 | password: SecretStr = Field( 27 | ..., readOnly=True, description="Password text property" 28 | ) 29 | long_text: str = Field( 30 | ..., format="multi-line", readOnly=True, description="Unlimited text property" 31 | ) 32 | integer_in_range: int = Field( 33 | 20, 34 | ge=10, 35 | le=30, 36 | multiple_of=2, 37 | readOnly=True, 38 | description="Number property with a limited range. Optional because of default value.", 39 | ) 40 | positive_integer: int = Field( 41 | ..., 42 | ge=0, 43 | multiple_of=10, 44 | readOnly=True, 45 | description="Positive integer with step count of 10.", 46 | ) 47 | float_number: float = Field(0.001, readOnly=True) 48 | date: datetime.date = Field( 49 | datetime.date.today(), 50 | readOnly=True, 51 | description="Date property. Optional because of default value.", 52 | ) 53 | time: datetime.time = Field( 54 | datetime.datetime.now().time(), 55 | readOnly=True, 56 | description="Time property. Optional because of default value.", 57 | ) 58 | dt: datetime.datetime = Field( 59 | datetime.datetime.now(), 60 | readOnly=True, 61 | description="Datetime property. Optional because of default value.", 62 | ) 63 | boolean: bool = Field( 64 | False, 65 | readOnly=True, 66 | description="Boolean property. Optional because of default value.", 67 | ) 68 | colour: Color = Field( 69 | Color("Blue"), 70 | readOnly=True, 71 | description="Color property. Optional because of default value.", 72 | ) 73 | read_only_text: str = Field( 74 | "Lorem ipsum dolor sit amet", 75 | description="This is a read only text.", 76 | readOnly=True, 77 | ) 78 | file_list: List[Base64UrlBytes] = Field( 79 | [], 80 | readOnly=True, 81 | description="A list of files. Optional property.", 82 | ) 83 | single_file: Base64UrlBytes = Field( 84 | None, 85 | readOnly=True, 86 | description="A single file. Optional property.", 87 | ) 88 | single_selection: SelectionValue = Field( 89 | ..., readOnly=True, description="Only select a single item from a set." 90 | ) 91 | single_selection_with_literal: Literal["foo", "bar"] = Field( 92 | "foo", readOnly=True, description="Only select a single item from a set." 93 | ) 94 | multi_selection: Set[SelectionValue] = Field( 95 | ..., readOnly=True, description="Allows multiple items from a set." 96 | ) 97 | multi_selection_with_literal: Set[Literal["foo", "bar"]] = Field( 98 | ["foo", "bar"], readOnly=True, description="Allows multiple items from a set." 99 | ) 100 | single_object: OtherData = Field( 101 | ..., 102 | readOnly=True, 103 | description="Another object embedded into this model.", 104 | ) 105 | string_list: List[str] = Field( 106 | ..., max_items=20, readOnly=True, description="List of string values" 107 | ) 108 | int_list: List[int] = Field(..., readOnly=True, description="List of int values") 109 | string_dict: Dict[str, str] = Field( 110 | ..., readOnly=True, description="Dict property with string values" 111 | ) 112 | float_dict: Dict[str, float] = Field( 113 | ..., readOnly=True, description="Dict property with float values" 114 | ) 115 | object_list: List[OtherData] = Field( 116 | ..., 117 | readOnly=True, 118 | description="A list of objects embedded into this model.", 119 | ) 120 | 121 | 122 | instance = DisabledModel( 123 | short_text="Some INSTANCE text", 124 | password="$uper_$ecret!", 125 | long_text="This is some really long text from the INSTANCE", 126 | integer_in_range=28, 127 | positive_integer=20, 128 | float_number=0.00444, 129 | date=datetime.date(1999, 9, 9), 130 | time=datetime.time(9, 9, 16), 131 | dt=datetime.datetime(1999, 9, 9), 132 | boolean=True, 133 | colour=Color("Yellow"), 134 | read_only_text="INSTANCE read only text", 135 | file_list=[], 136 | single_file=b"", 137 | single_selection=SelectionValue.FOO, 138 | single_selection_with_literal="bar", 139 | multi_selection=[SelectionValue.FOO, SelectionValue.BAR], 140 | multi_selection_with_literal=["foo", "bar"], 141 | single_object=OtherData(text="nested data INSTANCE text", integer=66), 142 | string_list=["a", "ab", "abc"], 143 | int_list=[9, 99, 999], 144 | string_dict={"key 1": "A", "key 2": "B", "key 3": "C"}, 145 | float_dict={"key A": 9.99, "key B": 66.0, "key C": -55.8}, 146 | object_list=[ 147 | OtherData(text="object list INSTANCE item 1", integer=6), 148 | OtherData(text="object list INSTANCE item 2", integer=99), 149 | ], 150 | ) 151 | 152 | 153 | from_model_tab, from_instance_tab = st.tabs( 154 | ["Form inputs from model", "Form inputs from instance"] 155 | ) 156 | 157 | with from_model_tab: 158 | data = sp.pydantic_input(key="my_disabled_model", model=DisabledModel) 159 | with st.expander("Current Input State", expanded=False): 160 | st.json(data) 161 | 162 | with from_instance_tab: 163 | data = sp.pydantic_input(key="my_disabled_instance", model=instance) 164 | with st.expander("Current Input State", expanded=False): 165 | st.json(data) 166 | -------------------------------------------------------------------------------- /examples/complex_instance_model.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from enum import Enum 3 | from typing import Dict, List, Set 4 | 5 | import streamlit as st 6 | from annotated_types import Gt 7 | from pydantic import BaseModel, Field 8 | from pydantic_extra_types.color import Color 9 | from typing_extensions import Annotated 10 | 11 | import streamlit_pydantic as sp 12 | 13 | 14 | class OtherData(BaseModel): 15 | text: str 16 | integer: int 17 | 18 | 19 | class SelectionValue(str, Enum): 20 | FOO = "foo" 21 | BAR = "bar" 22 | 23 | 24 | class ExampleModel(BaseModel): 25 | some_number: float = 10.0 # Optional 26 | some_text: str = Field(..., description="A text property") 27 | some_text_with_an_alias: str = Field( 28 | ..., description="A text property with an alias", alias="some_alias" 29 | ) 30 | some_integer: int = Field(20, description="An integer property.") 31 | some_date: datetime.date = Field(..., description="A date.") 32 | some_time: datetime.time = Field(..., description="A time.") 33 | some_datetime: datetime.datetime = Field(..., description="A datetime.") 34 | some_boolean: bool = False # Option 35 | long_text: str = Field( 36 | ..., format="multi-line", description="Unlimited text property" 37 | ) 38 | integer_in_range: int = Field( 39 | 20, 40 | ge=10, 41 | le=30, 42 | multiple_of=2, 43 | description="Number property with a limited range.", 44 | ) 45 | some_colour: Color 46 | single_selection: SelectionValue = Field( 47 | ..., description="Only select a single item from a set." 48 | ) 49 | multi_selection: Set[SelectionValue] = Field( 50 | ..., description="Allows multiple items from a set." 51 | ) 52 | disabled_selection: SelectionValue = Field( 53 | ..., readOnly=True, description="A read only field that is shown as disabled" 54 | ) 55 | read_only_text: str = Field( 56 | "Lorem ipsum dolor sit amet", 57 | description="This is a ready only text.", 58 | readOnly=True, 59 | ) 60 | nested_object: OtherData = Field( 61 | ..., 62 | description="Another object embedded into this model.", 63 | ) 64 | int_dict: Dict[str, Annotated[int, Gt(-4)]] = Field( 65 | ..., 66 | description="Dict property with int values", 67 | ) 68 | date_dict: Dict[str, datetime.datetime] = Field( 69 | ..., 70 | description="Dict property with date values", 71 | ) 72 | bool_dict: Dict[str, bool] = Field( 73 | ..., 74 | description="Dict property with bool values", 75 | ) 76 | color_dict: Dict[str, Color] = Field( 77 | ..., 78 | description="A dict of colors embedded into this model.", 79 | ) 80 | int_list: List[Annotated[int, Gt(2)]] = Field( 81 | ..., 82 | description="List of int values", 83 | max_items=4, 84 | min_items=2, 85 | ) 86 | color_list: List[Color] = Field( 87 | ..., 88 | description="List of color values", 89 | min_items=2, 90 | ) 91 | object_list: List[OtherData] = Field( 92 | ..., 93 | max_items=5, 94 | description="A list of objects embedded into this model.", 95 | ) 96 | object_dict: Dict[str, OtherData] = Field( 97 | ..., 98 | description="Dict property with complex values", 99 | ) 100 | 101 | 102 | instance = ExampleModel( 103 | some_number=999.99, 104 | some_text="Some INSTANCE text", 105 | some_alias="Some INSTANCE alias text", 106 | some_integer=0, 107 | some_date=datetime.date(1999, 9, 9), 108 | some_time=datetime.time(9, 9, 16), 109 | some_datetime=datetime.datetime(1999, 9, 9), 110 | integer_in_range=28, 111 | some_boolean=True, 112 | long_text="This is some really long text from the INSTANCE", 113 | some_colour=Color("green"), 114 | single_selection=SelectionValue.FOO, 115 | disabled_selection=SelectionValue.BAR, 116 | multi_selection=[SelectionValue.FOO, SelectionValue.BAR], 117 | read_only_text="INSTANCE read only text", 118 | nested_object=OtherData(text="nested data INSTANCE text", integer=66), 119 | int_dict={"key 1": 33, "key 2": 33, "key 3": 333}, 120 | date_dict={"date_key 1": datetime.datetime(1999, 9, 9)}, 121 | bool_dict={"bool_key 1": True}, 122 | color_dict={"Colour A": Color("#F3F3F3"), "Colour B": Color("#4E4E4E")}, 123 | int_list=[9, 99, 999], 124 | color_list=[Color("#F300F3"), Color("#00F3F3")], 125 | object_list=[ 126 | OtherData(text="object list INSTANCE item 1", integer=6), 127 | OtherData(text="object list INSTANCE item 2", integer=99), 128 | ], 129 | object_dict={ 130 | "obj 1": OtherData(text="object list dict item 1", integer=6), 131 | }, 132 | ) 133 | 134 | 135 | from_model_tab, from_instance_tab = st.tabs( 136 | ["Form inputs from model", "Form inputs from instance"] 137 | ) 138 | 139 | with from_model_tab: 140 | data = sp.pydantic_input(key="my_complex_input_model", model=ExampleModel) 141 | if data: 142 | with st.expander("Current Input State", expanded=False): 143 | st.json(data) 144 | 145 | 146 | with from_instance_tab: 147 | instance_input_data = sp.pydantic_input( 148 | key="my_complex_input_instance", model=instance 149 | ) 150 | if instance_input_data: 151 | with st.expander("Current Input State", expanded=False): 152 | st.json(instance_input_data) 153 | 154 | st.markdown("---") 155 | 156 | with st.expander("Session State", expanded=False): 157 | st.write(st.session_state) 158 | -------------------------------------------------------------------------------- /examples/complex_nested_model.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Set 3 | 4 | import streamlit as st 5 | from pydantic import BaseModel, Field 6 | 7 | import streamlit_pydantic as sp 8 | 9 | 10 | class OtherData(BaseModel): 11 | text: str 12 | integer: int 13 | 14 | 15 | class SelectionValue(str, Enum): 16 | FOO = "foo" 17 | BAR = "bar" 18 | 19 | 20 | class ExampleModel(BaseModel): 21 | long_text: str = Field( 22 | ..., format="multi-line", description="Unlimited text property" 23 | ) 24 | integer_in_range: int = Field( 25 | 20, 26 | ge=10, 27 | le=30, 28 | multiple_of=2, 29 | description="Number property with a limited range.", 30 | ) 31 | single_selection: SelectionValue = Field( 32 | ..., description="Only select a single item from a set." 33 | ) 34 | multi_selection: Set[SelectionValue] = Field( 35 | ..., description="Allows multiple items from a set." 36 | ) 37 | read_only_text: str = Field( 38 | "Lorem ipsum dolor sit amet", 39 | description="This is a ready only text.", 40 | readOnly=True, 41 | ) 42 | single_object: OtherData = Field( 43 | ..., 44 | description="Another object embedded into this model.", 45 | ) 46 | 47 | 48 | data = sp.pydantic_form(key="my_form", model=ExampleModel) 49 | if data: 50 | st.json(data.model_dump_json()) 51 | -------------------------------------------------------------------------------- /examples/complex_showcase.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from enum import Enum 3 | from typing import Dict, List, Literal, Set 4 | 5 | import streamlit as st 6 | from pydantic import Base64UrlBytes, BaseModel, Field, SecretStr 7 | 8 | import streamlit_pydantic as sp 9 | 10 | 11 | class SelectionValue(str, Enum): 12 | FOO = "foo" 13 | BAR = "bar" 14 | 15 | 16 | class OtherData(BaseModel): 17 | text: str 18 | integer: int 19 | 20 | 21 | class ShowcaseModel(BaseModel): 22 | short_text: str = Field(..., max_length=60, description="Short text property") 23 | password: SecretStr = Field(..., description="Password text property") 24 | long_text: str = Field( 25 | ..., format="multi-line", description="Unlimited text property" 26 | ) 27 | integer_in_range: int = Field( 28 | 20, 29 | ge=10, 30 | le=30, 31 | multiple_of=2, 32 | description="Number property with a limited range. Optional because of default value.", 33 | ) 34 | positive_integer: int = Field( 35 | ..., ge=0, multiple_of=10, description="Positive integer with step count of 10." 36 | ) 37 | float_number: float = Field(0.001) 38 | date: datetime.date = Field( 39 | datetime.date.today(), 40 | description="Date property. Optional because of default value.", 41 | ) 42 | time: datetime.time = Field( 43 | datetime.datetime.now().time(), 44 | description="Time property. Optional because of default value.", 45 | ) 46 | boolean: bool = Field( 47 | False, 48 | description="Boolean property. Optional because of default value.", 49 | ) 50 | read_only_text: str = Field( 51 | "Lorem ipsum dolor sit amet", 52 | description="This is a ready only text.", 53 | readOnly=True, 54 | ) 55 | file_list: List[Base64UrlBytes] = Field( 56 | [], 57 | description="A list of files. Optional property.", 58 | ) 59 | single_file: Base64UrlBytes = Field( 60 | b"", 61 | description="A single file. Optional property.", 62 | ) 63 | single_selection: SelectionValue = Field( 64 | ..., description="Only select a single item from a set." 65 | ) 66 | single_selection_with_literal: Literal["foo", "bar"] = Field( 67 | "foo", description="Only select a single item from a set." 68 | ) 69 | multi_selection: Set[SelectionValue] = Field( 70 | ..., description="Allows multiple items from a set." 71 | ) 72 | multi_selection_with_literal: Set[Literal["foo", "bar"]] = Field( 73 | ["foo", "bar"], description="Allows multiple items from a set." 74 | ) 75 | single_object: OtherData = Field( 76 | ..., 77 | description="Another object embedded into this model.", 78 | ) 79 | string_list: List[str] = Field( 80 | ..., max_items=20, description="List of string values" 81 | ) 82 | int_list: List[int] = Field(..., description="List of int values") 83 | string_dict: Dict[str, str] = Field( 84 | ..., description="Dict property with string values" 85 | ) 86 | float_dict: Dict[str, float] = Field( 87 | ..., description="Dict property with float values" 88 | ) 89 | object_list: List[OtherData] = Field( 90 | ..., 91 | description="A list of objects embedded into this model.", 92 | ) 93 | 94 | 95 | data = sp.pydantic_input( 96 | key="my_showcase_input", model=ShowcaseModel, group_optional_fields="sidebar" 97 | ) 98 | 99 | if data: 100 | with st.expander("Current Input State", expanded=False): 101 | st.json(data) 102 | -------------------------------------------------------------------------------- /examples/custom_form.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | from pydantic import BaseModel 3 | 4 | import streamlit_pydantic as sp 5 | 6 | 7 | class ExampleModel(BaseModel): 8 | some_text: str 9 | some_number: int = 10 10 | some_boolean: bool = True 11 | 12 | 13 | with st.form(key="pydantic_form"): 14 | data = sp.pydantic_input(key="my_custom_form_model", model=ExampleModel) 15 | submit_button = st.form_submit_button(label="Submit") 16 | obj = ExampleModel.model_validate(data) 17 | 18 | if data: 19 | st.json(obj.model_dump()) 20 | -------------------------------------------------------------------------------- /examples/data_validation.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | from pydantic import BaseModel, EmailStr, Field, HttpUrl, ValidationError 3 | from pydantic_extra_types.color import Color 4 | 5 | import streamlit_pydantic as sp 6 | 7 | 8 | class ExampleModel(BaseModel): 9 | url: HttpUrl 10 | color: Color = Field("blue", format="text") 11 | email: EmailStr 12 | 13 | 14 | data = sp.pydantic_form(key="my_form", model=ExampleModel) 15 | if data: 16 | st.json(data.model_dump()) 17 | -------------------------------------------------------------------------------- /examples/dataclass_form.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | import json 3 | 4 | import streamlit as st 5 | 6 | import streamlit_pydantic as sp 7 | 8 | 9 | @dataclasses.dataclass 10 | class ExampleModel: 11 | some_number: int 12 | some_boolean: bool 13 | some_text: str = "default input" 14 | 15 | 16 | from_model_tab, from_instance_tab = st.tabs( 17 | ["Form inputs from model", "Form inputs from instance"] 18 | ) 19 | 20 | with from_model_tab: 21 | data = sp.pydantic_form(key="my_dataclass_form", model=ExampleModel) 22 | if data: 23 | st.json(dataclasses.asdict(data)) 24 | 25 | with from_instance_tab: 26 | instance = ExampleModel( 27 | some_number=999, some_boolean=True, some_text="instance text" 28 | ) 29 | 30 | instance_input_data = sp.pydantic_form( 31 | key="my_dataclass_form_instance", model=instance 32 | ) 33 | if instance_input_data: 34 | st.json(dataclasses.asdict(instance_input_data)) 35 | -------------------------------------------------------------------------------- /examples/multiple_forms.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import streamlit as st 4 | from pydantic import BaseModel 5 | 6 | import streamlit_pydantic as sp 7 | 8 | 9 | class ExampleModel(BaseModel): 10 | some_text: str 11 | some_number: int = 10 12 | some_boolean: bool = True 13 | 14 | 15 | col1, col2 = st.columns(2) 16 | 17 | with col1: 18 | data = sp.pydantic_form(key="form_1", model=ExampleModel) 19 | if data: 20 | st.json(data.model_dump_json()) 21 | 22 | with col2: 23 | data = sp.pydantic_form(key="form_2", model=ExampleModel) 24 | if data: 25 | st.json(data.model_dump_json()) 26 | -------------------------------------------------------------------------------- /examples/optional_fields.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import streamlit as st 4 | from pydantic import BaseModel, Field 5 | 6 | import streamlit_pydantic as sp 7 | 8 | 9 | class ExampleModel(BaseModel): 10 | some_text: str 11 | some_number: int 12 | some_boolean: bool 13 | optional_boolean: bool = False 14 | optional_text: str = None 15 | optional_number: int = Field(20) 16 | 17 | 18 | data = sp.pydantic_form( 19 | key="my_form", model=ExampleModel, group_optional_fields="expander" 20 | ) 21 | 22 | if data: 23 | st.json(data.model_dump()) 24 | -------------------------------------------------------------------------------- /examples/overwrite_streamlit_args.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import streamlit as st 4 | from pydantic import Base64UrlBytes, BaseModel, Field 5 | 6 | import streamlit_pydantic as sp 7 | 8 | 9 | class ExampleModel(BaseModel): 10 | text: str = Field(..., max_length=100, st_kwargs_max_chars=500) 11 | number: int = Field( 12 | 10, st_kwargs_min_value=10, st_kwargs_max_value=100, st_kwargs_step=5 13 | ) 14 | single_file: Base64UrlBytes = Field( 15 | b"", 16 | st_kwargs_type=["png", "jpg"], 17 | ) 18 | 19 | 20 | data = sp.pydantic_form(key="my_form", model=ExampleModel) 21 | if data: 22 | st.json(data.model_dump_json()) 23 | 24 | st.subheader("Pydantic Output") 25 | sp.pydantic_output(data) 26 | -------------------------------------------------------------------------------- /examples/render_input.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | import streamlit_pydantic as sp 4 | 5 | 6 | class ExampleModel(BaseModel): 7 | some_text: str 8 | some_number: int = 10 # Optional 9 | some_boolean: bool = True # Option 10 | 11 | 12 | input_data = sp.pydantic_input( 13 | "model_input", model=ExampleModel, group_optional_fields="sidebar" 14 | ) 15 | -------------------------------------------------------------------------------- /examples/render_output.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from pydantic import BaseModel, Field 4 | 5 | import streamlit_pydantic as sp 6 | 7 | 8 | class ExampleModel(BaseModel): 9 | text: str = Field(..., description="A text property") 10 | integer: int = Field(..., description="An integer property.") 11 | date: datetime.date = Field(..., description="A date.") 12 | 13 | 14 | instance = ExampleModel(text="Some text", integer=40, date=datetime.date.today()) 15 | sp.pydantic_output(instance) 16 | -------------------------------------------------------------------------------- /examples/simple_form.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | from pydantic import BaseModel 3 | 4 | import streamlit_pydantic as sp 5 | 6 | 7 | class ExampleModel(BaseModel): 8 | some_text: str 9 | some_number: int 10 | some_boolean: bool 11 | 12 | 13 | from_model_tab, from_instance_tab = st.tabs( 14 | ["Form inputs from model", "Form inputs from instance"] 15 | ) 16 | 17 | with from_model_tab: 18 | data = sp.pydantic_form(key="my_sample_form", model=ExampleModel) 19 | if data: 20 | st.json(data.model_dump()) 21 | 22 | with from_instance_tab: 23 | instance = ExampleModel( 24 | some_number=999, some_boolean=True, some_text="instance text" 25 | ) 26 | 27 | instance_input_data = sp.pydantic_form( 28 | key="my_sample_form_instance", model=instance 29 | ) 30 | if instance_input_data: 31 | st.json(instance_input_data.model_dump()) 32 | -------------------------------------------------------------------------------- /examples/streamlit_settings.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import streamlit as st 4 | from pydantic import BaseModel, Field, SecretStr 5 | 6 | import streamlit_pydantic as sp 7 | 8 | 9 | class SubModel(BaseModel): 10 | things_i_like: List[str] 11 | 12 | 13 | class MySettings(sp.StreamlitSettings): 14 | username: str = Field(..., description="The username for the database.") 15 | password: SecretStr 16 | my_cool_secrets: SubModel 17 | 18 | 19 | st.json(MySettings().model_dump()) 20 | -------------------------------------------------------------------------------- /examples/union_field.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | import streamlit as st 4 | from pydantic import BaseModel 5 | 6 | import streamlit_pydantic as sp 7 | 8 | 9 | class PostalAddress(BaseModel): 10 | street: str 11 | city: str 12 | house: int 13 | 14 | 15 | class EmailAddress(BaseModel): 16 | email: str 17 | send_news: bool 18 | 19 | 20 | class ContactMethod(BaseModel): 21 | contact: Union[PostalAddress, EmailAddress] 22 | text: str 23 | 24 | 25 | from_model_tab, from_instance_tab = st.tabs( 26 | ["Form inputs from model", "Form inputs from instance"] 27 | ) 28 | 29 | with from_model_tab: 30 | input_data = sp.pydantic_input(key="union_input", model=ContactMethod) 31 | if input_data: 32 | st.json(input_data) 33 | 34 | 35 | with from_instance_tab: 36 | instance = ContactMethod( 37 | contact=EmailAddress(email="instance@example.com", send_news=True), 38 | text="instance text", 39 | ) 40 | 41 | instance_input_data = sp.pydantic_input(key="union_input_instance", model=instance) 42 | 43 | if instance_input_data: 44 | st.json(instance_input_data) 45 | 46 | 47 | st.markdown("---") 48 | 49 | with st.expander("Session State", expanded=False): 50 | st.write(st.session_state) 51 | -------------------------------------------------------------------------------- /examples/union_field_discriminator.py: -------------------------------------------------------------------------------- 1 | from typing import Literal, Optional, Union 2 | 3 | import streamlit as st 4 | from pydantic import BaseModel, Field 5 | 6 | import streamlit_pydantic as sp 7 | 8 | 9 | class PostalAddress(BaseModel): 10 | contact_type: Literal["postal"] 11 | street: str 12 | city: str 13 | house: int 14 | 15 | 16 | class EmailAddress(BaseModel): 17 | contact_type: Literal["email"] 18 | email: str 19 | send_news: bool 20 | 21 | 22 | class ContactMethod(BaseModel): 23 | contact: Optional[Union[PostalAddress, EmailAddress]] = Field( 24 | ..., discriminator="contact_type" 25 | ) 26 | text: str 27 | 28 | 29 | from_model_tab, from_instance_tab = st.tabs( 30 | ["Form inputs from model", "Form inputs from instance"] 31 | ) 32 | 33 | with from_model_tab: 34 | input_data = sp.pydantic_input(key="discriminator_union_input", model=ContactMethod) 35 | if input_data: 36 | st.json(input_data) 37 | 38 | 39 | with from_instance_tab: 40 | instance = ContactMethod( 41 | contact=EmailAddress( 42 | contact_type="email", email="instance@example.com", send_news=True 43 | ), 44 | text="instance text", 45 | ) 46 | 47 | instance_input_data = sp.pydantic_input( 48 | key="discriminator_union_input_instance", model=instance 49 | ) 50 | 51 | if instance_input_data: 52 | st.json(instance_input_data) 53 | 54 | st.markdown("---") 55 | 56 | with st.expander("Session State", expanded=False): 57 | st.write(st.session_state) 58 | -------------------------------------------------------------------------------- /playground/.streamlit/secrets.toml: -------------------------------------------------------------------------------- 1 | # Everything in this section will be available as an environment variable 2 | username = "Jane" 3 | password = "12345qwerty" 4 | 5 | # You can also add other sections if you like. 6 | # The contents of sections as shown below will not become environment variables, 7 | # but they'll be easily accessible from within Streamlit anyway as we show 8 | # later in this doc. 9 | [my_cool_secrets] 10 | things_i_like = ["Streamlit", "Python"] 11 | -------------------------------------------------------------------------------- /playground/playground_app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pathlib 3 | 4 | import streamlit as st 5 | 6 | from streamlit_pydantic.ui_renderer import _name_to_title 7 | 8 | st.set_page_config(page_title="Streamlit Pydantic Playground", page_icon=":magic_wand:") 9 | st.title("Streamlit Pydantic - Playground") 10 | 11 | BADGES = """ 12 | 13 | """ 14 | st.markdown(BADGES, unsafe_allow_html=True) 15 | 16 | st.markdown( 17 | "Streamlit-pydantic makes it easy to auto-generate UI elements from Pydantic models. Just define your data model and turn it into a full-fledged UI form. " 18 | + " You can explore some examples below 👇" 19 | ) 20 | 21 | DEFAULT_DEMO = "simple_form.py" 22 | 23 | path_of_script = pathlib.Path(__file__).parent.resolve() 24 | path_to_examples = pathlib.Path(path_of_script).parent.joinpath("examples").resolve() 25 | 26 | demos = [] 27 | for example_file in os.listdir(path_to_examples): 28 | file_path = path_to_examples.joinpath(example_file).resolve() 29 | 30 | if not file_path.is_file(): 31 | continue 32 | 33 | demos.append(example_file) 34 | 35 | title_to_demo = {} 36 | 37 | demo_titles = [] 38 | default_index = 0 39 | for i, demo in enumerate(demos): 40 | if demo == DEFAULT_DEMO: 41 | # Use hello world as default 42 | default_index = i 43 | demo_title = _name_to_title(demo.replace(".py", "")) 44 | title_to_demo[demo_title] = demo 45 | demo_titles.append(demo_title) 46 | 47 | selected_demo_title = st.selectbox( 48 | "Select Demo", options=demo_titles, index=default_index 49 | ) 50 | selected_demo = title_to_demo[selected_demo_title] 51 | 52 | with st.expander("Source Code", expanded=False): 53 | with open(path_to_examples.joinpath(selected_demo), encoding="UTF-8") as f: 54 | st.code(f.read(), language="python") 55 | 56 | exec(open(path_to_examples.joinpath(selected_demo)).read()) 57 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling", "hatch-vcs"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "streamlit-pydantic" 7 | dynamic = ["version"] 8 | description = "Auto-generate Streamlit UI from Pydantic Models & Dataclasses." 9 | readme = "README.md" 10 | authors = [{ name = "Lukas Masuch", email = "lukas.masuch@gmail.com" }] 11 | requires-python = ">= 3.8" 12 | dependencies = [ 13 | "streamlit>=1.30.0", 14 | "pydantic[email]>=2.0", 15 | "pydantic-settings>=2.0", 16 | "pydantic-extra-types>=2.6.0", 17 | ] 18 | license = "MIT" 19 | classifiers = [ 20 | "Development Status :: 4 - Beta", 21 | "Framework :: Pydantic", 22 | "Framework :: Pydantic :: 2", 23 | "Intended Audience :: Developers", 24 | "Intended Audience :: Information Technology", 25 | "Intended Audience :: Science/Research", 26 | "License :: OSI Approved :: MIT License", 27 | "Natural Language :: English", 28 | "Operating System :: MacOS", 29 | "Operating System :: Microsoft :: Windows", 30 | "Operating System :: OS Independent", 31 | "Operating System :: POSIX", 32 | "Operating System :: POSIX :: Linux", 33 | "Programming Language :: Python", 34 | "Programming Language :: Python :: 3", 35 | "Programming Language :: Python :: 3 :: Only", 36 | "Programming Language :: Python :: 3.10", 37 | "Programming Language :: Python :: 3.11", 38 | "Programming Language :: Python :: 3.12", 39 | "Programming Language :: Python :: 3.8", 40 | "Programming Language :: Python :: 3.9", 41 | "Programming Language :: Python :: Implementation :: CPython", 42 | "Programming Language :: Python :: Implementation :: PyPy", 43 | "Topic :: Scientific/Engineering", 44 | "Topic :: Scientific/Engineering :: Visualization", 45 | "Topic :: Software Development", 46 | "Topic :: Software Development :: Libraries", 47 | "Topic :: Software Development :: Libraries :: Application Frameworks", 48 | "Topic :: Software Development :: Libraries :: Python Modules", 49 | "Topic :: Software Development :: User Interfaces", 50 | "Topic :: Utilities" 51 | ] 52 | keywords = ["streamlit", "pydantic"] 53 | 54 | [project.urls] 55 | Homepage = "https://github.com/lukasmasuch/streamlit-pydantic" 56 | Documentation = "https://github.com/lukasmasuch/streamlit-pydantic#documentation" 57 | Repository = "https://github.com/lukasmasuch/streamlit-pydantic" 58 | Issues = "https://github.com/lukasmasuch/streamlit-pydantic/issues" 59 | Changelog = "https://github.com/lukasmasuch/streamlit-pydantic/releases" 60 | 61 | [tool.rye] 62 | managed = true 63 | universal = true # # Generate lockfile valid on all platforms 64 | dev-dependencies = ["pytest", "mypy", "ruff", "types-dataclasses", "lazydocs"] 65 | 66 | [tool.hatch.metadata] 67 | allow-direct-references = true 68 | 69 | [tool.hatch.version] 70 | source = "vcs" 71 | 72 | [tool.hatch.build.targets.wheel] 73 | packages = ["src/streamlit_pydantic"] 74 | 75 | [tool.rye.scripts] 76 | all = { chain = ["format", "check", "test:pytest"] } 77 | format = { chain = [ 78 | "format:ruff", 79 | "isort:ruff", 80 | ]} 81 | checks = { chain = [ 82 | "formatcheck:ruff", 83 | "lint:ruff", 84 | "typecheck:mypy", 85 | "check:importable", 86 | # TODO: add doccs check here 87 | ]} 88 | "format:ruff" = "rye fmt" 89 | "isort:ruff" = "rye lint --fix -- --select I" 90 | "formatcheck:ruff" = "ruff format --check" 91 | "lint:ruff" = "rye lint" 92 | "test:pytest" = "rye test" 93 | "check:importable" = "python -c 'import streamlit_pydantic'" 94 | "typecheck:mypy" = "mypy src/streamlit_pydantic" 95 | playground = "streamlit run ./playground/playground_app.py" 96 | docs = "lazydocs --overview-file=README.md --src-base-url=https://github.com/lukasmasuch/streamlit-pydantic/blob/main streamlit_pydantic" 97 | 98 | [tool.ruff] 99 | target-version = 'py38' 100 | line-length = 88 101 | 102 | [tool.ruff.lint] 103 | # https://docs.astral.sh/ruff/rules/ 104 | select = [ 105 | "E", # pycodestyle errors 106 | "W", # pycodestyle warnings 107 | "I", # import sorting 108 | "D", # pydocstyle 109 | # "PL", # pylint 110 | # "F", # pyflakes 111 | # "B", # flake8-bugbear 112 | # "C4", # flake8-comprehensions 113 | # "UP", # pyupgrade 114 | ] 115 | ignore = [ 116 | "D100", # Missing docstring in public module 117 | "D101", # Missing docstring in public class 118 | "D102", # Missing docstring in public method 119 | "D103", # Missing docstring in public function 120 | "D104", # Missing docstring in public package 121 | "D105", # Missing docstring in magic method 122 | "D107", # Missing docstring in __init__ 123 | "D202", # No blank lines allowed after function docstring 124 | "E203", # space before : (needed for how black formats slicing) 125 | "E501", # line too long 126 | "E731", # do not assign a lambda expression, use a def 127 | "E721", # do not compare types, use 'isinstance()' 128 | "E402", # module level import not at top of file 129 | ] 130 | 131 | [tool.ruff.lint.pydocstyle] 132 | convention = "google" 133 | 134 | [tool.ruff.lint.isort] 135 | # so it knows to group first-party stuff last 136 | known-first-party = ["streamlit_pydantic", "tests"] 137 | 138 | [tool.mypy] 139 | # https://mypy.readthedocs.io/en/stable/config_file.html 140 | python_version = "3.8" 141 | ignore_missing_imports = true 142 | disallow_untyped_defs = true 143 | follow_imports = "skip" 144 | 145 | [tool.pytest.ini_options] 146 | addopts = "-s" 147 | -------------------------------------------------------------------------------- /requirements-dev.lock: -------------------------------------------------------------------------------- 1 | # generated by rye 2 | # use `rye lock` or `rye sync` to update this lockfile 3 | # 4 | # last locked with the following flags: 5 | # pre: false 6 | # features: [] 7 | # all-features: false 8 | # with-sources: false 9 | # generate-hashes: false 10 | # universal: true 11 | 12 | -e file:. 13 | altair==5.4.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 14 | # via streamlit 15 | annotated-types==0.7.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 16 | # via pydantic 17 | attrs==24.2.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 18 | # via jsonschema 19 | # via referencing 20 | blinker==1.8.2 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 21 | # via streamlit 22 | cachetools==5.4.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 23 | # via streamlit 24 | certifi==2024.7.4 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 25 | # via requests 26 | charset-normalizer==3.3.2 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 27 | # via requests 28 | click==8.1.7 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 29 | # via streamlit 30 | # via typer 31 | colorama==0.4.6 ; (platform_system == 'Windows' and (python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10'))) or (sys_platform == 'win32' and (python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10'))) 32 | # via click 33 | # via pytest 34 | dnspython==2.6.1 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 35 | # via email-validator 36 | email-validator==2.2.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 37 | # via pydantic 38 | exceptiongroup==1.2.2 ; python_version < '3.11' and (python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10')) 39 | # via pytest 40 | gitdb==4.0.11 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 41 | # via gitpython 42 | gitpython==3.1.43 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 43 | # via streamlit 44 | idna==3.7 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 45 | # via email-validator 46 | # via requests 47 | importlib-resources==6.4.3 ; python_version < '3.9' and (python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10')) 48 | # via jsonschema 49 | # via jsonschema-specifications 50 | iniconfig==2.0.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 51 | # via pytest 52 | jinja2==3.1.4 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 53 | # via altair 54 | # via pydeck 55 | jsonschema==4.23.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 56 | # via altair 57 | jsonschema-specifications==2023.12.1 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 58 | # via jsonschema 59 | lazydocs==0.4.8 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 60 | markdown-it-py==3.0.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 61 | # via rich 62 | markupsafe==2.1.5 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 63 | # via jinja2 64 | mdurl==0.1.2 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 65 | # via markdown-it-py 66 | mypy==1.11.1 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 67 | mypy-extensions==1.0.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 68 | # via mypy 69 | narwhals==1.4.2 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 70 | # via altair 71 | numpy==1.24.4 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 72 | # via pandas 73 | # via pyarrow 74 | # via pydeck 75 | # via streamlit 76 | packaging==24.1 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 77 | # via altair 78 | # via pytest 79 | # via streamlit 80 | pandas==2.0.3 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 81 | # via streamlit 82 | pillow==10.4.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 83 | # via streamlit 84 | pkgutil-resolve-name==1.3.10 ; python_version < '3.9' and (python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10')) 85 | # via jsonschema 86 | pluggy==1.5.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 87 | # via pytest 88 | protobuf==5.27.3 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 89 | # via streamlit 90 | pyarrow==17.0.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 91 | # via streamlit 92 | pydantic==2.8.2 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 93 | # via pydantic-extra-types 94 | # via pydantic-settings 95 | # via streamlit-pydantic 96 | pydantic-core==2.20.1 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 97 | # via pydantic 98 | pydantic-extra-types==2.9.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 99 | # via streamlit-pydantic 100 | pydantic-settings==2.4.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 101 | # via streamlit-pydantic 102 | pydeck==0.9.1 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 103 | # via streamlit 104 | pygments==2.18.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 105 | # via rich 106 | pytest==8.3.2 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 107 | python-dateutil==2.9.0.post0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 108 | # via pandas 109 | python-dotenv==1.0.1 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 110 | # via pydantic-settings 111 | pytz==2024.1 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 112 | # via pandas 113 | referencing==0.35.1 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 114 | # via jsonschema 115 | # via jsonschema-specifications 116 | requests==2.32.3 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 117 | # via streamlit 118 | rich==13.7.1 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 119 | # via streamlit 120 | # via typer 121 | rpds-py==0.20.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 122 | # via jsonschema 123 | # via referencing 124 | ruff==0.6.1 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 125 | shellingham==1.5.4 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 126 | # via typer 127 | six==1.16.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 128 | # via python-dateutil 129 | smmap==5.0.1 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 130 | # via gitdb 131 | streamlit==1.37.1 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 132 | # via streamlit-pydantic 133 | tenacity==8.5.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 134 | # via streamlit 135 | toml==0.10.2 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 136 | # via streamlit 137 | tomli==2.0.1 ; python_version < '3.11' and (python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10')) 138 | # via mypy 139 | # via pytest 140 | tornado==6.4.1 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 141 | # via streamlit 142 | typer==0.12.4 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 143 | # via lazydocs 144 | types-dataclasses==0.6.6 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 145 | typing-extensions==4.12.2 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.9' and python_version < '3.13' and python_version >= '3.10') or (python_version < '3.13' and python_version >= '3.10') 146 | # via altair 147 | # via annotated-types 148 | # via mypy 149 | # via pydantic 150 | # via pydantic-core 151 | # via rich 152 | # via streamlit 153 | # via typer 154 | tzdata==2024.1 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 155 | # via pandas 156 | urllib3==2.2.2 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 157 | # via requests 158 | watchdog==4.0.2 ; platform_system != 'Darwin' and (python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10')) 159 | # via streamlit 160 | zipp==3.20.0 ; python_version < '3.9' 161 | # via importlib-resources 162 | -------------------------------------------------------------------------------- /requirements.lock: -------------------------------------------------------------------------------- 1 | # generated by rye 2 | # use `rye lock` or `rye sync` to update this lockfile 3 | # 4 | # last locked with the following flags: 5 | # pre: false 6 | # features: [] 7 | # all-features: false 8 | # with-sources: false 9 | # generate-hashes: false 10 | # universal: true 11 | 12 | -e file:. 13 | altair==5.4.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 14 | # via streamlit 15 | annotated-types==0.7.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 16 | # via pydantic 17 | attrs==24.2.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 18 | # via jsonschema 19 | # via referencing 20 | blinker==1.8.2 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 21 | # via streamlit 22 | cachetools==5.4.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 23 | # via streamlit 24 | certifi==2024.7.4 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 25 | # via requests 26 | charset-normalizer==3.3.2 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 27 | # via requests 28 | click==8.1.7 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 29 | # via streamlit 30 | colorama==0.4.6 ; platform_system == 'Windows' and (python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10')) 31 | # via click 32 | dnspython==2.6.1 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 33 | # via email-validator 34 | email-validator==2.2.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 35 | # via pydantic 36 | gitdb==4.0.11 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 37 | # via gitpython 38 | gitpython==3.1.43 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 39 | # via streamlit 40 | idna==3.7 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 41 | # via email-validator 42 | # via requests 43 | importlib-resources==6.4.3 ; python_version < '3.9' and (python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10')) 44 | # via jsonschema 45 | # via jsonschema-specifications 46 | jinja2==3.1.4 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 47 | # via altair 48 | # via pydeck 49 | jsonschema==4.23.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 50 | # via altair 51 | jsonschema-specifications==2023.12.1 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 52 | # via jsonschema 53 | markdown-it-py==3.0.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 54 | # via rich 55 | markupsafe==2.1.5 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 56 | # via jinja2 57 | mdurl==0.1.2 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 58 | # via markdown-it-py 59 | narwhals==1.4.2 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 60 | # via altair 61 | numpy==1.24.4 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 62 | # via pandas 63 | # via pyarrow 64 | # via pydeck 65 | # via streamlit 66 | packaging==24.1 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 67 | # via altair 68 | # via streamlit 69 | pandas==2.0.3 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 70 | # via streamlit 71 | pillow==10.4.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 72 | # via streamlit 73 | pkgutil-resolve-name==1.3.10 ; python_version < '3.9' and (python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10')) 74 | # via jsonschema 75 | protobuf==5.27.3 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 76 | # via streamlit 77 | pyarrow==17.0.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 78 | # via streamlit 79 | pydantic==2.8.2 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 80 | # via pydantic-extra-types 81 | # via pydantic-settings 82 | # via streamlit-pydantic 83 | pydantic-core==2.20.1 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 84 | # via pydantic 85 | pydantic-extra-types==2.9.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 86 | # via streamlit-pydantic 87 | pydantic-settings==2.4.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 88 | # via streamlit-pydantic 89 | pydeck==0.9.1 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 90 | # via streamlit 91 | pygments==2.18.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 92 | # via rich 93 | python-dateutil==2.9.0.post0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 94 | # via pandas 95 | python-dotenv==1.0.1 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 96 | # via pydantic-settings 97 | pytz==2024.1 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 98 | # via pandas 99 | referencing==0.35.1 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 100 | # via jsonschema 101 | # via jsonschema-specifications 102 | requests==2.32.3 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 103 | # via streamlit 104 | rich==13.7.1 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 105 | # via streamlit 106 | rpds-py==0.20.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 107 | # via jsonschema 108 | # via referencing 109 | six==1.16.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 110 | # via python-dateutil 111 | smmap==5.0.1 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 112 | # via gitdb 113 | streamlit==1.37.1 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 114 | # via streamlit-pydantic 115 | tenacity==8.5.0 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 116 | # via streamlit 117 | toml==0.10.2 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 118 | # via streamlit 119 | tornado==6.4.1 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 120 | # via streamlit 121 | typing-extensions==4.12.2 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.9' and python_version < '3.13' and python_version >= '3.10') or (python_version < '3.13' and python_version >= '3.10') 122 | # via altair 123 | # via annotated-types 124 | # via pydantic 125 | # via pydantic-core 126 | # via rich 127 | # via streamlit 128 | tzdata==2024.1 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 129 | # via pandas 130 | urllib3==2.2.2 ; python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10') 131 | # via requests 132 | watchdog==4.0.2 ; platform_system != 'Darwin' and (python_version < '3.10' or python_version >= '3.13' or (python_version < '3.13' and python_version >= '3.10')) 133 | # via streamlit 134 | zipp==3.20.0 ; python_version < '3.9' 135 | # via importlib-resources 136 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | -------------------------------------------------------------------------------- /src/streamlit_pydantic/__init__.py: -------------------------------------------------------------------------------- 1 | from . import _about 2 | 3 | # define the version before the other imports since these need it 4 | __version__ = _about.__version__ 5 | 6 | # Do other imports here 7 | import streamlit as st 8 | 9 | from .settings import StreamlitSettings 10 | from .ui_renderer import pydantic_form as _pydantic_form 11 | 12 | pydantic_form = st._gather_metrics("pydantic_form", _pydantic_form) 13 | from .ui_renderer import pydantic_input as _pydantic_input 14 | 15 | pydantic_input = st._gather_metrics("pydantic_input", _pydantic_input) 16 | from .ui_renderer import pydantic_output as _pydantic_output 17 | 18 | pydantic_output = st._gather_metrics("pydantic_output", _pydantic_output) 19 | -------------------------------------------------------------------------------- /src/streamlit_pydantic/_about.py: -------------------------------------------------------------------------------- 1 | """Information about this library.""" 2 | 3 | from importlib.metadata import version 4 | 5 | __version__ = version("streamlit-pydantic") 6 | -------------------------------------------------------------------------------- /src/streamlit_pydantic/py.typed: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/streamlit_pydantic/schema_utils.py: -------------------------------------------------------------------------------- 1 | """Utilities to help with JSON Schema. 2 | 3 | lazydocs: ignore 4 | """ 5 | 6 | from typing import Dict, List 7 | 8 | 9 | def resolve_reference(reference: str, references: Dict) -> Dict: 10 | return references[reference.split("/")[-1]] 11 | 12 | 13 | def get_single_reference_item(property: Dict, references: Dict) -> Dict: 14 | # Ref can either be directly in the properties or the first element of allOf 15 | reference = property.get("$ref") 16 | if reference is None: 17 | reference = property["allOf"][0]["$ref"] 18 | return resolve_reference(reference, references) 19 | 20 | 21 | def get_union_references(property: Dict, references: Dict) -> List[Dict]: 22 | # Ref can either be directly in the properties or the first element of allOf 23 | # anyOf is used for union property prior to pydantic < 1.10 24 | union_references = property.get("oneOf", property.get("anyOf")) 25 | resolved_references: List[Dict] = [] 26 | for reference in union_references: # type: ignore 27 | if reference.get("oneOf") is not None: 28 | for disc_ref in reference["oneOf"]: 29 | resolved_references.append( 30 | resolve_reference(disc_ref["$ref"], references) 31 | ) 32 | elif reference.get("$ref") is not None: 33 | resolved_references.append(resolve_reference(reference["$ref"], references)) 34 | return resolved_references 35 | 36 | 37 | def is_single_string_property(property: Dict) -> bool: 38 | return property.get("type") == "string" 39 | 40 | 41 | def is_single_color_property(property: Dict) -> bool: 42 | if property.get("type") != "string": 43 | return False 44 | return property.get("format") in ["color"] 45 | 46 | 47 | def is_single_datetime_property(property: Dict) -> bool: 48 | if property.get("type") != "string": 49 | return False 50 | return property.get("format") in ["date-time", "time", "date"] 51 | 52 | 53 | def is_single_boolean_property(property: Dict) -> bool: 54 | return property.get("type") == "boolean" 55 | 56 | 57 | def is_single_number_property(property: Dict) -> bool: 58 | return property.get("type") in ["integer", "number"] 59 | 60 | 61 | def is_single_file_property(property: Dict) -> bool: 62 | if property.get("type") != "string": 63 | return False 64 | return property.get("format") in ["base64", "base64url"] 65 | 66 | 67 | def is_multi_enum_property(property: Dict, references: Dict) -> bool: 68 | if property.get("type") != "array": 69 | return False 70 | 71 | if property.get("uniqueItems") is not True: 72 | # Only relevant if it is a set or other datastructures with unique items 73 | return False 74 | 75 | try: 76 | # Uses literal 77 | _ = property["items"]["enum"] 78 | return True 79 | except Exception: 80 | pass 81 | 82 | try: 83 | # Uses enum 84 | _ = resolve_reference(property["items"]["$ref"], references)["enum"] 85 | return True 86 | except Exception: 87 | return False 88 | 89 | 90 | def is_single_enum_property(property: Dict, references: Dict) -> bool: 91 | if property.get("enum"): 92 | return True 93 | 94 | try: 95 | _ = get_single_reference_item(property, references)["enum"] 96 | return True 97 | except Exception: 98 | return False 99 | 100 | 101 | def is_single_dict_property(property: Dict) -> bool: 102 | if property.get("type") != "object": 103 | return False 104 | return "additionalProperties" in property 105 | 106 | 107 | def is_single_reference(property: Dict) -> bool: 108 | if property.get("type") is not None: 109 | return False 110 | 111 | return bool(property.get("$ref")) 112 | 113 | 114 | def is_multi_file_property(property: Dict) -> bool: 115 | if property.get("type") != "array": 116 | return False 117 | 118 | if property.get("items") is None: 119 | return False 120 | 121 | try: 122 | return property["items"]["format"] in ["base64", "base64url"] 123 | except Exception: 124 | return False 125 | 126 | 127 | def is_single_object(property: Dict, references: Dict) -> bool: 128 | try: 129 | object_reference = get_single_reference_item(property, references) 130 | if object_reference["type"] != "object": 131 | return False 132 | return "properties" in object_reference 133 | except Exception: 134 | return False 135 | 136 | 137 | def is_union_property(property: Dict) -> bool: 138 | # anyOf is used for union property prior to pydantic < 1.10 139 | union_prop = property.get("anyOf") 140 | 141 | if union_prop is None: 142 | return False 143 | 144 | if len(union_prop) == 0: # type: ignore 145 | return False 146 | 147 | discriminated = False 148 | 149 | for reference in union_prop: # type: ignore 150 | if ( 151 | reference.get("oneOf") is not None 152 | or reference.get("discriminated") is not None 153 | ): 154 | discriminated = True 155 | for discriminated_reference in reference.get("oneOf"): # type: ignore 156 | if not is_single_reference(discriminated_reference): 157 | return False 158 | 159 | if not discriminated and not is_single_reference(reference): 160 | return False 161 | 162 | return True 163 | 164 | 165 | def is_property_list(property: Dict) -> bool: 166 | if property.get("type") != "array": 167 | return False 168 | 169 | if property.get("items") is None: 170 | return False 171 | 172 | try: 173 | return property["items"]["type"] in ["string", "number", "integer"] 174 | except Exception: 175 | return False 176 | 177 | 178 | def is_object_list_property(property: Dict, references: Dict) -> bool: 179 | if property.get("type") != "array": 180 | return False 181 | 182 | try: 183 | object_reference = resolve_reference(property["items"]["$ref"], references) 184 | if object_reference["type"] != "object": 185 | return False 186 | return "properties" in object_reference 187 | except Exception: 188 | return False 189 | -------------------------------------------------------------------------------- /src/streamlit_pydantic/settings.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Tuple, Type 2 | 3 | import streamlit as st 4 | from pydantic.fields import FieldInfo 5 | from pydantic_settings import ( 6 | BaseSettings, 7 | PydanticBaseSettingsSource, 8 | ) 9 | 10 | 11 | class _StreamlitSettingsSource(PydanticBaseSettingsSource): 12 | def get_field_value( 13 | self, field: FieldInfo, field_name: str 14 | ) -> Tuple[Any, str, bool]: 15 | field_value = st.secrets.get(field_name) 16 | return field_value, field_name, self.field_is_complex(field) 17 | 18 | def prepare_field_value( 19 | self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool 20 | ) -> Any: 21 | return value 22 | 23 | def __call__(self) -> Dict[str, Any]: 24 | d: Dict[str, Any] = {} 25 | for field_name, field in self.settings_cls.model_fields.items(): 26 | field_value, field_key, value_is_complex = self.get_field_value( 27 | field, field_name 28 | ) 29 | field_value = self.prepare_field_value( 30 | field_name, field, field_value, value_is_complex 31 | ) 32 | if field_value is not None: 33 | d[field_key] = field_value 34 | 35 | return d 36 | 37 | 38 | class StreamlitSettings(BaseSettings): 39 | class Config: 40 | """Default for streamlit settings class.""" 41 | 42 | extra = "ignore" 43 | 44 | @classmethod 45 | def settings_customise_sources( 46 | cls, 47 | settings_cls: Type[BaseSettings], 48 | init_settings: PydanticBaseSettingsSource, 49 | env_settings: PydanticBaseSettingsSource, 50 | dotenv_settings: PydanticBaseSettingsSource, 51 | file_secret_settings: PydanticBaseSettingsSource, 52 | ) -> Tuple[PydanticBaseSettingsSource, ...]: 53 | """lazydocs: ignore.""" 54 | return ( 55 | init_settings, 56 | env_settings, 57 | dotenv_settings, 58 | file_secret_settings, 59 | _StreamlitSettingsSource(settings_cls), 60 | ) 61 | -------------------------------------------------------------------------------- /src/streamlit_pydantic/ui_renderer.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import contextlib 3 | import dataclasses 4 | import datetime 5 | import inspect 6 | import json 7 | import mimetypes 8 | import re 9 | from enum import Enum 10 | from typing import Any, Callable, Dict, List, Optional, Tuple, Type, TypeVar 11 | 12 | import pandas as pd 13 | import streamlit as st 14 | from pydantic import BaseModel, TypeAdapter, ValidationError 15 | from pydantic import dataclasses as pydantic_dataclasses 16 | from pydantic_extra_types.color import Color 17 | 18 | from streamlit_pydantic import schema_utils 19 | 20 | _OVERWRITE_STREAMLIT_KWARGS_PREFIX = "st_kwargs_" 21 | 22 | 23 | def _pydantic_encoder(obj: Any) -> Any: 24 | """Simplified version of pydantic v1's deprecated json encoder.""" 25 | if isinstance(obj, BaseModel): 26 | return obj.model_dump(mode="json") 27 | elif dataclasses.is_dataclass(obj) and not isinstance(obj, type): 28 | # Object is a dataclass instance 29 | return dataclasses.asdict(obj) 30 | 31 | raise TypeError( 32 | f"Object of type '{obj.__class__.__name__}' is not JSON serializable" 33 | ) 34 | 35 | 36 | def _name_to_title(name: str) -> str: 37 | """Converts a camelCase or snake_case name to title case.""" 38 | # If camelCase -> convert to snake case 39 | name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name) 40 | name = re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).lower() 41 | # Convert to title case 42 | return name.replace("_", " ").strip().title() 43 | 44 | 45 | def _function_has_named_arg(func: Callable, parameter: str) -> bool: 46 | try: 47 | sig = inspect.signature(func) 48 | for param in sig.parameters.values(): 49 | if param.name == "input": 50 | return True 51 | except Exception: 52 | return False 53 | return False 54 | 55 | 56 | def _has_output_ui_renderer(data_item: BaseModel) -> bool: 57 | return hasattr(data_item, "render_output_ui") 58 | 59 | 60 | def _has_input_ui_renderer(input_class: Type[BaseModel]) -> bool: 61 | return hasattr(input_class, "render_input_ui") 62 | 63 | 64 | def _is_compatible_audio(mime_type: str) -> bool: 65 | return mime_type in ["audio/mpeg", "audio/ogg", "audio/wav"] 66 | 67 | 68 | def _is_compatible_image(mime_type: str) -> bool: 69 | return mime_type in ["image/png", "image/jpeg"] 70 | 71 | 72 | def _is_compatible_video(mime_type: str) -> bool: 73 | return mime_type in ["video/mp4"] 74 | 75 | 76 | class GroupOptionalFieldsStrategy(str, Enum): 77 | NO = "no" 78 | EXPANDER = "expander" 79 | SIDEBAR = "sidebar" 80 | 81 | 82 | class InputUI: 83 | """Input UI renderer. 84 | 85 | lazydocs: ignore 86 | """ 87 | 88 | def __init__( 89 | self, 90 | key: str, 91 | model: Type[BaseModel], 92 | streamlit_container: Any = st, 93 | group_optional_fields: GroupOptionalFieldsStrategy = "no", # type: ignore 94 | lowercase_labels: bool = False, 95 | ignore_empty_values: bool = False, 96 | return_model: bool = False, 97 | ): 98 | self._key = key 99 | self._return_model = return_model 100 | 101 | self._session_state = st.session_state 102 | 103 | # Initialize Sessions State 104 | if "run_id" not in st.session_state: 105 | self._session_state.run_id = 0 106 | 107 | self._session_input_key = self._key + "-data" 108 | if self._session_input_key not in st.session_state: 109 | self._session_state[self._session_input_key] = {} 110 | 111 | self._lowercase_labels = lowercase_labels 112 | self._group_optional_fields = group_optional_fields 113 | self._streamlit_container = streamlit_container 114 | self._ignore_empty_values = ignore_empty_values 115 | 116 | if dataclasses.is_dataclass(model): 117 | self._input_class = model 118 | if isinstance(model, type): 119 | self._type_adapter = TypeAdapter(pydantic_dataclasses.dataclass(model)) 120 | else: 121 | self._type_adapter = TypeAdapter( 122 | pydantic_dataclasses.dataclass(model.__class__) 123 | ) 124 | self._input_schema = self._type_adapter.json_schema() 125 | else: 126 | self._type_adapter = None 127 | self._input_schema = model.model_json_schema(by_alias=True) 128 | self._input_class = model 129 | 130 | self._schema_properties = self._input_schema.get("properties", {}) 131 | self._schema_references = self._input_schema.get("$defs", {}) 132 | self._schema_required = self._input_schema.get("required", {}) 133 | 134 | def render_ui(self) -> Dict: 135 | if _has_input_ui_renderer(self._input_class): 136 | # The input model has a rendering function 137 | # The rendering also returns the current state of input data 138 | self._session_state[self._session_input_key] = ( 139 | self._input_class.render_input_ui( # type: ignore 140 | self._streamlit_container, 141 | self._session_state[self._session_input_key], 142 | ).model_dump() 143 | ) 144 | return self._session_state[self._session_input_key] 145 | 146 | properties_in_expander = [] 147 | 148 | # check if the input_class is an instance and build value dicts 149 | if isinstance(self._input_class, BaseModel): 150 | instance_dict = self._input_class.model_dump() 151 | instance_dict_by_alias = self._input_class.model_dump(by_alias=True) 152 | elif isinstance(self._input_class.__class__, type): # for dataclasses 153 | instance_dict = dict(self._input_class.__dict__) 154 | instance_dict_by_alias = None 155 | else: 156 | instance_dict = None 157 | instance_dict_by_alias = None 158 | 159 | for property_key in self._schema_properties.keys(): 160 | streamlit_app = self._streamlit_container 161 | if property_key not in self._schema_required: 162 | if self._group_optional_fields == "sidebar": 163 | streamlit_app = self._streamlit_container.sidebar 164 | elif self._group_optional_fields == "expander": 165 | properties_in_expander.append(property_key) 166 | # Render properties later in expander (see below) 167 | continue 168 | 169 | property = self._schema_properties[property_key] 170 | 171 | if not property.get("title"): 172 | # Set property key as fallback title 173 | property["title"] = _name_to_title(property_key) 174 | 175 | # if there are instance values, add them to the property dict 176 | if instance_dict is not None: 177 | instance_value = instance_dict.get(property_key) 178 | if instance_value in [None, ""] and instance_dict_by_alias: 179 | instance_value = instance_dict_by_alias.get(property_key) 180 | if instance_value not in [None, ""]: 181 | property["init_value"] = instance_value 182 | # keep a reference of the original class to help with non-discriminated unions 183 | # TODO: This will not succeed for attributes that have an alias 184 | attr = getattr(self._input_class, property_key, None) 185 | if attr is not None: 186 | property["instance_class"] = str(type(attr)) 187 | 188 | try: 189 | value = self._render_property(streamlit_app, property_key, property) 190 | if not self._is_value_ignored(property_key, value): 191 | self._store_value(property_key, value) 192 | except Exception: 193 | pass 194 | 195 | if properties_in_expander: 196 | # Render optional properties in expander 197 | with self._streamlit_container.expander( 198 | "Optional Parameters", expanded=False 199 | ): 200 | for property_key in properties_in_expander: 201 | property = self._schema_properties[property_key] 202 | 203 | if not property.get("title"): 204 | # Set property key as fallback title 205 | property["title"] = _name_to_title(property_key) 206 | 207 | try: 208 | value = self._render_property( 209 | self._streamlit_container, property_key, property 210 | ) 211 | 212 | if not self._is_value_ignored(property_key, value): 213 | self._store_value(property_key, value) 214 | 215 | except Exception: 216 | pass 217 | 218 | input_state = self._session_state[self._session_input_key] 219 | 220 | if self._return_model: 221 | # Validate and return a BaseModel or DataClass instance 222 | try: 223 | if self._type_adapter is not None: 224 | self._type_adapter.validate_python(input_state) 225 | if isinstance(self._input_class, type): 226 | # DataClass model 227 | return self._input_class(**input_state) # type: ignore 228 | else: 229 | # DataClass instance 230 | return self._input_class.__class__(**input_state) 231 | else: 232 | # BaseModel 233 | return self._input_class.model_validate(input_state) # type: ignore 234 | except ValidationError as ex: 235 | error_text = "**Input failed validation:**" 236 | for error in ex.errors(): 237 | if "loc" in error and "msg" in error: 238 | location = ".".join(error["loc"]).replace("__root__.", "") # type: ignore 239 | error_msg = f"**{location}:** " + error["msg"] 240 | error_text += "\n\n" + error_msg 241 | else: 242 | # Fallback 243 | error_text += "\n\n" + str(error) 244 | st.warning(error_text) 245 | return None # type: ignore 246 | else: 247 | return input_state 248 | 249 | def _get_overwrite_streamlit_kwargs(self, key: str, property: Dict) -> Dict: 250 | streamlit_kwargs: Dict = {} 251 | 252 | for kwarg in property: 253 | if kwarg.startswith(_OVERWRITE_STREAMLIT_KWARGS_PREFIX): 254 | streamlit_kwargs[ 255 | kwarg.replace(_OVERWRITE_STREAMLIT_KWARGS_PREFIX, "") 256 | ] = property[kwarg] 257 | return streamlit_kwargs 258 | 259 | def _get_default_streamlit_input_kwargs(self, key: str, property: Dict) -> Dict: 260 | label = property.get("title") 261 | if label and self._lowercase_labels: 262 | label = label.lower() 263 | 264 | disabled = False 265 | if property.get("readOnly"): 266 | # Read only property -> only show value 267 | disabled = True 268 | 269 | streamlit_kwargs = { 270 | "label": label, 271 | "key": str(self._session_state.run_id) + "-" + str(self._key) + "-" + key, 272 | "disabled": disabled, 273 | # "on_change": detect_change, -> not supported for inside forms 274 | # "args": (key,), 275 | } 276 | 277 | if property.get("description"): 278 | streamlit_kwargs["help"] = property.get("description") 279 | elif property.get("help"): 280 | # Fallback to help. Used more frequently with dataclasses 281 | streamlit_kwargs["help"] = property.get("help") 282 | 283 | return streamlit_kwargs 284 | 285 | def _is_value_ignored(self, property_key: str, value: Any) -> bool: 286 | """Returns `True` if the value should be ignored for storing in session. 287 | 288 | This is the case if `ignore_empty_values` is activated and the value is empty and not already set/changed before. 289 | """ 290 | return ( 291 | self._ignore_empty_values 292 | and ( 293 | type(value) == int or type(value) == float or isinstance(value, str) 294 | ) # only for int, float or str 295 | and not value 296 | and self._get_value(property_key) is None 297 | ) 298 | 299 | def _store_value_in_state(self, state: dict, key: str, value: Any) -> None: 300 | key_elements = key.split(".") 301 | for i, key_element in enumerate(key_elements): 302 | if i == len(key_elements) - 1: 303 | # add value to this element 304 | state[key_element] = value 305 | return 306 | if key_element not in state: 307 | state[key_element] = {} 308 | state = state[key_element] 309 | 310 | def _get_value_from_state(self, state: dict, key: str) -> Any: 311 | key_elements = key.split(".") 312 | for i, key_element in enumerate(key_elements): 313 | if i == len(key_elements) - 1: 314 | # add value to this element 315 | if key_element not in state: 316 | return None 317 | return state[key_element] 318 | if key_element not in state: 319 | state[key_element] = {} 320 | state = state[key_element] 321 | return None 322 | 323 | def _store_value(self, key: str, value: Any) -> None: 324 | return self._store_value_in_state( 325 | self._session_state[self._session_input_key], key, value 326 | ) 327 | 328 | def _get_value(self, key: str) -> Any: 329 | return self._get_value_from_state( 330 | self._session_state[self._session_input_key], key 331 | ) 332 | 333 | def _render_single_datetime_input( 334 | self, streamlit_app: Any, key: str, property: Dict 335 | ) -> Any: 336 | streamlit_kwargs = self._get_default_streamlit_input_kwargs(key, property) 337 | overwrite_kwargs = self._get_overwrite_streamlit_kwargs(key, property) 338 | 339 | if property.get("format") == "time": 340 | if property.get("init_value"): 341 | streamlit_kwargs["value"] = property.get("init_value") 342 | elif property.get("default"): 343 | with contextlib.suppress(Exception): 344 | streamlit_kwargs["value"] = datetime.time.fromisoformat( # type: ignore 345 | property["default"] 346 | ) 347 | return streamlit_app.time_input(**{**streamlit_kwargs, **overwrite_kwargs}) 348 | elif property.get("format") == "date": 349 | if property.get("init_value"): 350 | streamlit_kwargs["value"] = property.get("init_value") 351 | elif property.get("default"): 352 | with contextlib.suppress(Exception): 353 | streamlit_kwargs["value"] = datetime.date.fromisoformat( # type: ignore 354 | property["default"] 355 | ) 356 | return streamlit_app.date_input(**{**streamlit_kwargs, **overwrite_kwargs}) 357 | elif property.get("format") == "date-time": 358 | if property.get("init_value"): 359 | streamlit_kwargs["value"] = property.get("init_value") 360 | elif property.get("default"): 361 | with contextlib.suppress(Exception): 362 | streamlit_kwargs["value"] = datetime.datetime.fromisoformat( # type: ignore 363 | property["default"] 364 | ) 365 | with self._streamlit_container.container(): 366 | if not property.get("is_item"): 367 | self._streamlit_container.subheader(streamlit_kwargs.get("label")) 368 | if streamlit_kwargs.get("description"): 369 | self._streamlit_container.text(streamlit_kwargs.get("description")) 370 | selected_date = None 371 | selected_time = None 372 | 373 | # columns can not be used within a collection 374 | if property.get("is_item"): 375 | date_col = self._streamlit_container.container() 376 | time_col = self._streamlit_container.container() 377 | else: 378 | date_col, time_col = self._streamlit_container.columns(2) 379 | with date_col: 380 | date_kwargs = {**{**streamlit_kwargs, **overwrite_kwargs}} 381 | date_kwargs["label"] = "Date" 382 | date_kwargs["key"] = (f"{streamlit_kwargs.get('key')}-date-input",) 383 | 384 | value = streamlit_kwargs.get("value") 385 | if value: 386 | with contextlib.suppress(Exception): 387 | date_kwargs["value"] = value.date() 388 | selected_date = self._streamlit_container.date_input(**date_kwargs) 389 | 390 | with time_col: 391 | time_kwargs = {**{**streamlit_kwargs, **overwrite_kwargs}} 392 | time_kwargs["label"] = "Time" 393 | time_kwargs["key"] = f"{streamlit_kwargs.get('key')}-time-input" 394 | 395 | value = streamlit_kwargs.get("value") 396 | if value: 397 | with contextlib.suppress(Exception): 398 | time_kwargs["value"] = value.time() 399 | selected_time = self._streamlit_container.time_input(**time_kwargs) 400 | 401 | return datetime.datetime.combine(selected_date, selected_time) 402 | else: 403 | streamlit_app.warning( 404 | "Date format is not supported: " + str(property.get("format")) 405 | ) 406 | 407 | def _render_single_file_input( 408 | self, streamlit_app: Any, key: str, property: Dict 409 | ) -> Any: 410 | streamlit_kwargs = self._get_default_streamlit_input_kwargs(key, property) 411 | overwrite_kwargs = self._get_overwrite_streamlit_kwargs(key, property) 412 | 413 | file_extension = None 414 | if "mime_type" in property: 415 | file_extension = mimetypes.guess_extension(property["mime_type"]) 416 | 417 | uploaded_file = streamlit_app.file_uploader( 418 | **{ 419 | **streamlit_kwargs, 420 | "accept_multiple_files": False, 421 | "type": file_extension, 422 | **overwrite_kwargs, 423 | } 424 | ) 425 | if uploaded_file is None: 426 | return b"" 427 | 428 | file_bytes = uploaded_file.getvalue() 429 | if getattr(uploaded_file, "type"): 430 | if _is_compatible_audio(uploaded_file.type): 431 | # Show audio 432 | streamlit_app.audio(file_bytes, format=uploaded_file.type) 433 | if _is_compatible_image(uploaded_file.type): 434 | # Show image 435 | streamlit_app.image(file_bytes) 436 | if _is_compatible_video(uploaded_file.type): 437 | # Show video 438 | streamlit_app.video(file_bytes, format=uploaded_file.type) 439 | return base64.urlsafe_b64encode(file_bytes) 440 | 441 | def _render_single_string_input( 442 | self, streamlit_app: Any, key: str, property: Dict 443 | ) -> Any: 444 | streamlit_kwargs = self._get_default_streamlit_input_kwargs(key, property) 445 | overwrite_kwargs = self._get_overwrite_streamlit_kwargs(key, property) 446 | if property.get("init_value"): 447 | streamlit_kwargs["value"] = property.get("init_value") 448 | elif property.get("default"): 449 | streamlit_kwargs["value"] = property.get("default") 450 | elif property.get("example"): 451 | # TODO: also use example for other property types 452 | # Use example as value if it is provided 453 | streamlit_kwargs["value"] = property.get("example") 454 | 455 | if property.get("maxLength") is not None: 456 | streamlit_kwargs["max_chars"] = property.get("maxLength") 457 | 458 | if property.get("readOnly"): 459 | # Read only property -> only show value 460 | streamlit_kwargs["disabled"] = property.get("readOnly", False) 461 | 462 | if property.get("format") == "multi-line" and not property.get("writeOnly"): 463 | # Use text area if format is multi-line (custom definition) 464 | return streamlit_app.text_area(**{**streamlit_kwargs, **overwrite_kwargs}) 465 | else: 466 | # Use text input for most situations 467 | if property.get("writeOnly"): 468 | streamlit_kwargs["type"] = "password" 469 | return streamlit_app.text_input(**{**streamlit_kwargs, **overwrite_kwargs}) 470 | 471 | def _render_single_color_input( 472 | self, streamlit_app: Any, key: str, property: Dict 473 | ) -> Any: 474 | streamlit_kwargs = self._get_default_streamlit_input_kwargs(key, property) 475 | overwrite_kwargs = self._get_overwrite_streamlit_kwargs(key, property) 476 | if property.get("init_value") is not None: 477 | streamlit_kwargs["value"] = property["init_value"] 478 | elif property.get("default") is not None: 479 | streamlit_kwargs["value"] = property["default"] 480 | elif property.get("example") is not None: 481 | streamlit_kwargs["value"] = property["example"] 482 | 483 | if isinstance(streamlit_kwargs.get("value"), Color): 484 | streamlit_kwargs["value"] = streamlit_kwargs["value"].as_hex() 485 | elif isinstance(streamlit_kwargs.get("value"), str): 486 | streamlit_kwargs["value"] = Color(streamlit_kwargs["value"]).as_hex() 487 | 488 | if property.get("format") == "text": 489 | # Use text input if specified format is text 490 | return streamlit_app.text_input(**{**streamlit_kwargs, **overwrite_kwargs}) 491 | else: 492 | # Use color picker input for most situations 493 | return streamlit_app.color_picker( 494 | **{**streamlit_kwargs, **overwrite_kwargs} 495 | ) 496 | 497 | def _render_multi_enum_input( 498 | self, streamlit_app: Any, key: str, property: Dict 499 | ) -> Any: 500 | streamlit_kwargs = self._get_default_streamlit_input_kwargs(key, property) 501 | overwrite_kwargs = self._get_overwrite_streamlit_kwargs(key, property) 502 | 503 | select_options: List[str] = [] 504 | if property.get("items").get("enum"): # type: ignore 505 | # Using Literal 506 | select_options = property.get("items").get("enum") # type: ignore 507 | else: 508 | # Using Enum 509 | reference_item = schema_utils.resolve_reference( 510 | property["items"]["$ref"], self._schema_references 511 | ) 512 | select_options = reference_item["enum"] 513 | 514 | if property.get("init_value"): 515 | streamlit_kwargs["default"] = property.get("init_value") 516 | elif property.get("default"): 517 | try: 518 | streamlit_kwargs["default"] = property.get("default") 519 | except Exception: 520 | pass 521 | 522 | return streamlit_app.multiselect( 523 | **{**streamlit_kwargs, "options": select_options, **overwrite_kwargs} 524 | ) 525 | 526 | def _render_single_enum_input( 527 | self, streamlit_app: Any, key: str, property: Dict 528 | ) -> Any: 529 | streamlit_kwargs = self._get_default_streamlit_input_kwargs(key, property) 530 | overwrite_kwargs = self._get_overwrite_streamlit_kwargs(key, property) 531 | 532 | select_options: List[str] = [] 533 | if property.get("enum"): 534 | select_options = property.get("enum") # type: ignore 535 | else: 536 | reference_item = schema_utils.get_single_reference_item( 537 | property, self._schema_references 538 | ) 539 | select_options = reference_item["enum"] 540 | 541 | if property.get("init_value"): 542 | streamlit_kwargs["index"] = select_options.index( 543 | property.get("init_value") # type: ignore 544 | ) 545 | elif property.get("default") is not None: 546 | try: 547 | streamlit_kwargs["index"] = select_options.index( 548 | property.get("default") # type: ignore 549 | ) 550 | except Exception: 551 | # Use default selection 552 | pass 553 | 554 | # if there is only one option then there is no choice for the user to be make 555 | # so simply return the value (This is relevant for discriminator properties) 556 | if len(select_options) == 1: 557 | return select_options[0] 558 | else: 559 | return streamlit_app.selectbox( 560 | **{**streamlit_kwargs, "options": select_options, **overwrite_kwargs} 561 | ) 562 | 563 | def _render_single_dict_input( 564 | self, streamlit_app: Any, key: str, property: Dict 565 | ) -> Any: 566 | # Add title and subheader 567 | streamlit_app.subheader(property.get("title")) 568 | if property.get("description"): 569 | streamlit_app.markdown(property.get("description")) 570 | 571 | if self._get_value(key) is not None or self._get_value(key) == {}: 572 | data_dict = self._get_value(key) 573 | elif property.get("init_value"): 574 | data_dict = property.get("init_value") 575 | elif property.get("default"): 576 | data_dict = property.get("default") 577 | else: 578 | data_dict = {} 579 | 580 | is_object = True if property["additionalProperties"].get("$ref") else False 581 | 582 | add_col, clear_col, _ = streamlit_app.columns(3) 583 | 584 | add_col = add_col.empty() 585 | 586 | if self._clear_button_allowed(property): 587 | data_dict = self._render_dict_add_button(key, add_col, data_dict) 588 | 589 | if self._clear_button_allowed(property): 590 | data_dict = self._render_dict_clear_button(key, clear_col, data_dict) 591 | 592 | new_dict = {} 593 | 594 | for index, input_item in enumerate(data_dict.items()): 595 | updated_key, updated_value = self._render_dict_item( 596 | streamlit_app, 597 | key, 598 | input_item, 599 | index, 600 | property, 601 | ) 602 | 603 | if updated_key is not None and updated_value is not None: 604 | new_dict[updated_key] = updated_value 605 | 606 | if is_object: 607 | streamlit_app.markdown("---") 608 | 609 | if not is_object: 610 | streamlit_app.markdown("---") 611 | 612 | return new_dict 613 | 614 | def _render_single_reference( 615 | self, streamlit_app: Any, key: str, property: Dict 616 | ) -> Any: 617 | reference_item = schema_utils.get_single_reference_item( 618 | property, self._schema_references 619 | ) 620 | return self._render_property(streamlit_app, key, reference_item) 621 | 622 | def _render_union_property( 623 | self, streamlit_app: Any, key: str, property: Dict 624 | ) -> Any: 625 | streamlit_kwargs = self._get_default_streamlit_input_kwargs(key, property) 626 | 627 | reference_items = schema_utils.get_union_references( 628 | property, self._schema_references 629 | ) 630 | 631 | # special handling when there are instance values and a discriminator property 632 | # to differentiate between object types 633 | if property.get("init_value") and property.get("discriminator"): 634 | disc_prop = property["discriminator"]["propertyName"] 635 | # find the index where the discriminator is equal to the init_value 636 | ref_index = next( 637 | i 638 | for i, x in enumerate(reference_items) 639 | if x["properties"][disc_prop]["enum"] 640 | == [property["init_value"][disc_prop]] 641 | ) 642 | 643 | # add any init_value properties to the corresponding reference item 644 | reference_items[ref_index]["init_value"] = property["init_value"] 645 | streamlit_kwargs["index"] = ref_index 646 | elif property.get("init_value") and property.get("instance_class"): 647 | ref_index = next( 648 | i 649 | for i, x in enumerate(reference_items) 650 | if x["title"] in property["instance_class"] 651 | ) 652 | reference_items[ref_index]["init_value"] = property["init_value"] 653 | streamlit_kwargs["index"] = ref_index 654 | 655 | name_reference_mapping: Dict[str, Dict] = {} 656 | 657 | for reference in reference_items: 658 | reference_title = _name_to_title(reference["title"]) 659 | name_reference_mapping[reference_title] = reference 660 | 661 | streamlit_app.subheader(streamlit_kwargs["label"]) # type: ignore 662 | if "help" in streamlit_kwargs: 663 | streamlit_app.markdown(streamlit_kwargs["help"]) 664 | 665 | selected_reference = streamlit_app.selectbox( 666 | **{ 667 | **streamlit_kwargs, 668 | "label": streamlit_kwargs["label"] + " - Options", 669 | "options": name_reference_mapping.keys(), 670 | } 671 | ) 672 | 673 | input_data = self._render_object_input( 674 | streamlit_app, key, name_reference_mapping[selected_reference] 675 | ) 676 | 677 | streamlit_app.markdown("---") 678 | return input_data 679 | 680 | def _render_multi_file_input( 681 | self, streamlit_app: Any, key: str, property: Dict 682 | ) -> Any: 683 | streamlit_kwargs = self._get_default_streamlit_input_kwargs(key, property) 684 | overwrite_kwargs = self._get_overwrite_streamlit_kwargs(key, property) 685 | 686 | file_extension = None 687 | if "mime_type" in property: 688 | file_extension = mimetypes.guess_extension(property["mime_type"]) 689 | 690 | uploaded_files = streamlit_app.file_uploader( 691 | **{ 692 | **streamlit_kwargs, 693 | "accept_multiple_files": True, 694 | "type": file_extension, 695 | **overwrite_kwargs, 696 | } 697 | ) 698 | uploaded_files_bytes = [] 699 | if uploaded_files: 700 | for uploaded_file in uploaded_files: 701 | uploaded_files_bytes.append(uploaded_file.read()) 702 | return uploaded_files_bytes 703 | 704 | def _render_single_boolean_input( 705 | self, streamlit_app: Any, key: str, property: Dict 706 | ) -> Any: 707 | streamlit_kwargs = self._get_default_streamlit_input_kwargs(key, property) 708 | overwrite_kwargs = self._get_overwrite_streamlit_kwargs(key, property) 709 | 710 | if "init_value" in property: 711 | streamlit_kwargs["value"] = property.get("init_value") 712 | elif "default" in property: 713 | streamlit_kwargs["value"] = property.get("default") 714 | 715 | # special formatting when rendering within a list/dict 716 | if property.get("is_item"): 717 | streamlit_app.markdown("##") 718 | 719 | return streamlit_app.checkbox(**{**streamlit_kwargs, **overwrite_kwargs}) 720 | 721 | def _render_single_number_input( 722 | self, streamlit_app: Any, key: str, property: Dict 723 | ) -> Any: 724 | streamlit_kwargs = self._get_default_streamlit_input_kwargs(key, property) 725 | overwrite_kwargs = self._get_overwrite_streamlit_kwargs(key, property) 726 | 727 | number_transform = int 728 | if property.get("type") == "number": 729 | number_transform = float # type: ignore 730 | streamlit_kwargs["format"] = "%f" 731 | 732 | if "multipleOf" in property: 733 | # Set stepcount based on multiple of parameter 734 | streamlit_kwargs["step"] = number_transform(property["multipleOf"]) 735 | elif number_transform == int: 736 | # Set step size to 1 as default 737 | streamlit_kwargs["step"] = 1 738 | elif number_transform == float: 739 | # Set step size to 0.01 as default 740 | # TODO: adapt to default value 741 | streamlit_kwargs["step"] = 0.01 742 | 743 | if "minimum" in property: 744 | streamlit_kwargs["min_value"] = number_transform(property["minimum"]) 745 | if "exclusiveMinimum" in property: 746 | streamlit_kwargs["min_value"] = number_transform( 747 | property["exclusiveMinimum"] + streamlit_kwargs["step"] 748 | ) 749 | if "maximum" in property: 750 | streamlit_kwargs["max_value"] = number_transform(property["maximum"]) 751 | 752 | if "exclusiveMaximum" in property: 753 | streamlit_kwargs["max_value"] = number_transform( 754 | property["exclusiveMaximum"] - streamlit_kwargs["step"] 755 | ) 756 | 757 | if self._session_state.get(streamlit_kwargs["key"]) is None: 758 | if property.get("init_value") is not None: 759 | streamlit_kwargs["value"] = number_transform(property["init_value"]) 760 | elif property.get("default") is not None: 761 | streamlit_kwargs["value"] = number_transform(property["default"]) # type: ignore 762 | else: 763 | if "min_value" in streamlit_kwargs: 764 | streamlit_kwargs["value"] = streamlit_kwargs["min_value"] 765 | elif number_transform == int: 766 | streamlit_kwargs["value"] = 0 767 | else: 768 | # Set default value to step 769 | streamlit_kwargs["value"] = number_transform( 770 | streamlit_kwargs["step"] 771 | ) 772 | else: 773 | streamlit_kwargs["value"] = number_transform( 774 | self._session_state[streamlit_kwargs["key"]] 775 | ) 776 | 777 | if "min_value" in streamlit_kwargs and "max_value" in streamlit_kwargs: 778 | # TODO: Only if less than X steps 779 | return streamlit_app.slider(**{**streamlit_kwargs, **overwrite_kwargs}) 780 | else: 781 | return streamlit_app.number_input( 782 | **{**streamlit_kwargs, **overwrite_kwargs} 783 | ) 784 | 785 | def _render_object_input(self, streamlit_app: Any, key: str, property: Dict) -> Any: 786 | properties = property["properties"] 787 | object_inputs = {} 788 | for property_key in properties: 789 | new_property = properties[property_key] 790 | if not new_property.get("title"): 791 | # Set property key as fallback title 792 | new_property["title"] = _name_to_title(property_key) 793 | # construct full key based on key parts -> required later to get the value 794 | full_key = key + "." + property_key 795 | 796 | if property.get("init_value"): 797 | new_property["init_value"] = property["init_value"].get(property_key) 798 | if property.get("default"): 799 | new_property["default"] = property["default"].get(property_key) 800 | 801 | new_property["readOnly"] = property.get("readOnly", False) 802 | 803 | value = self._render_property(streamlit_app, full_key, new_property) 804 | if not self._is_value_ignored(property_key, value): 805 | object_inputs[property_key] = value 806 | 807 | return object_inputs 808 | 809 | def _render_single_object_input( 810 | self, streamlit_app: Any, key: str, property: Dict 811 | ) -> Any: 812 | # Add title and subheader 813 | title = property.get("title") 814 | if property.get("is_item"): 815 | streamlit_app.caption(title) 816 | else: 817 | streamlit_app.subheader(title) 818 | if property.get("description"): 819 | streamlit_app.markdown(property.get("description")) 820 | 821 | object_reference = schema_utils.get_single_reference_item( 822 | property, self._schema_references 823 | ) 824 | 825 | object_reference["init_value"] = property.get("init_value", None) 826 | 827 | object_reference["default"] = property.get("default", None) 828 | 829 | object_reference["readOnly"] = property.get("readOnly", None) 830 | 831 | return self._render_object_input(streamlit_app, key, object_reference) 832 | 833 | def _render_list_item( 834 | self, 835 | streamlit_app: Any, 836 | parent_key: str, 837 | value: Any, 838 | index: int, 839 | property: Dict[str, Any], 840 | ) -> Any: 841 | label = "Item #" + str(index + 1) 842 | new_key = self._key + "-" + parent_key + "." + str(index) 843 | item_placeholder = streamlit_app.empty() 844 | 845 | with item_placeholder: 846 | input_col, button_col = streamlit_app.columns([8, 3]) 847 | 848 | button_col.markdown("##") 849 | 850 | if self._remove_button_allowed(index, property): 851 | remove = False 852 | else: 853 | remove = button_col.button("Remove", key=new_key + "-remove") 854 | 855 | # insert an input field when the remove button has not been clicked 856 | if not remove: 857 | with input_col: 858 | new_property = { 859 | "title": label, 860 | "init_value": value if value else None, 861 | "is_item": True, 862 | "readOnly": property.get("readOnly"), 863 | **property["items"], 864 | } 865 | return self._render_property(streamlit_app, new_key, new_property) 866 | 867 | else: 868 | # when the remove button is clicked clear the placeholder and return None 869 | item_placeholder.empty() 870 | return None 871 | 872 | def _render_dict_item( 873 | self, 874 | streamlit_app: Any, 875 | parent_key: str, 876 | in_value: Tuple[str, Any], 877 | index: int, 878 | property: Dict[str, Any], 879 | ) -> Any: 880 | new_key = self._key + "-" + parent_key + "." + str(index) 881 | item_placeholder = streamlit_app.empty() 882 | 883 | with item_placeholder.container(): 884 | key_col, value_col, button_col = streamlit_app.columns([4, 4, 3]) 885 | 886 | dict_key = in_value[0] 887 | dict_value = in_value[1] 888 | 889 | dict_key_key = new_key + "-key" 890 | dict_value_key = new_key + "-value" 891 | 892 | button_col.markdown("##") 893 | 894 | if self._remove_button_allowed(index, property): 895 | remove = False 896 | else: 897 | remove = button_col.button("Remove", key=new_key + "-remove") 898 | 899 | if not remove: 900 | with key_col: 901 | updated_key = streamlit_app.text_input( 902 | "Key", 903 | value=dict_key, 904 | key=dict_key_key, 905 | disabled=property.get("readOnly", False), 906 | ) 907 | 908 | with value_col: 909 | new_property = { 910 | "title": "Value", 911 | "init_value": dict_value, 912 | "is_item": True, 913 | "readOnly": property.get("readOnly"), 914 | **property["additionalProperties"], 915 | } 916 | with value_col: 917 | updated_value = self._render_property( 918 | streamlit_app, dict_value_key, new_property 919 | ) 920 | 921 | return updated_key, updated_value 922 | 923 | else: 924 | # when the remove button is clicked clear the placeholder and return None 925 | item_placeholder.empty() 926 | return None, None 927 | 928 | def _add_button_allowed( 929 | self, 930 | index: int, 931 | property: Dict[str, Any], 932 | ) -> bool: 933 | add_allowed = not ( 934 | (property.get("readOnly", False) is True) 935 | or ((index) >= property.get("maxItems", 1000)) 936 | ) 937 | 938 | return add_allowed 939 | 940 | def _remove_button_allowed( 941 | self, 942 | index: int, 943 | property: Dict[str, Any], 944 | ) -> bool: 945 | remove_allowed = (property.get("readOnly") is True) or ( 946 | (index + 1) <= property.get("minItems", 0) 947 | ) 948 | 949 | return remove_allowed 950 | 951 | def _clear_button_allowed( 952 | self, 953 | property: Dict[str, Any], 954 | ) -> bool: 955 | clear_allowed = not ( 956 | (property.get("readOnly", False) is True) 957 | or (property.get("minItems", 0) > 0) 958 | ) 959 | 960 | return clear_allowed 961 | 962 | def _render_list_add_button( 963 | self, 964 | key: str, 965 | streamlit_app: Any, 966 | data_list: List[Any], 967 | ) -> List[Any]: 968 | if streamlit_app.button( 969 | "Add Item", 970 | key=self._key + "-" + key + "list-add-item", 971 | ): 972 | data_list.append(None) 973 | 974 | return data_list 975 | 976 | def _render_list_clear_button( 977 | self, 978 | key: str, 979 | streamlit_app: Any, 980 | data_list: List[Any], 981 | ) -> List[Any]: 982 | if streamlit_app.button( 983 | "Clear All", 984 | key=self._key + "_" + key + "-list_clear-all", 985 | ): 986 | data_list = [] 987 | 988 | return data_list 989 | 990 | def _render_dict_add_button( 991 | self, key: str, streamlit_app: Any, data_dict: Dict[str, Any] 992 | ) -> Dict[str, Any]: 993 | if streamlit_app.button( 994 | "Add Item", 995 | key=self._key + "-" + key + "-add-item", 996 | ): 997 | data_dict[str(len(data_dict) + 1)] = None 998 | 999 | return data_dict 1000 | 1001 | def _render_dict_clear_button( 1002 | self, 1003 | key: str, 1004 | streamlit_app: Any, 1005 | data_dict: Dict[str, Any], 1006 | ) -> Dict[str, Any]: 1007 | if streamlit_app.button( 1008 | "Clear All", 1009 | key=self._key + "-" + key + "-clear-all", 1010 | ): 1011 | data_dict = {} 1012 | 1013 | return data_dict 1014 | 1015 | def _render_list_input(self, streamlit_app: Any, key: str, property: Dict) -> Any: 1016 | # Add title and subheader 1017 | streamlit_app.subheader(property.get("title")) 1018 | if property.get("description"): 1019 | streamlit_app.markdown(property.get("description")) 1020 | 1021 | is_object = True if property["items"].get("$ref") else False 1022 | 1023 | object_list = [] 1024 | 1025 | # Treat empty list as a session data "hit" 1026 | if self._get_value(key) is not None or self._get_value(key) == []: 1027 | data_list = self._get_value(key) 1028 | elif property.get("init_value"): 1029 | data_list = property.get("init_value") 1030 | elif property.get("default"): 1031 | data_list = property.get("default") 1032 | else: 1033 | data_list = [] 1034 | 1035 | add_col, clear_col, _ = streamlit_app.columns(3) 1036 | 1037 | add_col = add_col.empty() 1038 | 1039 | self._render_list_add_button(key, add_col, data_list) 1040 | 1041 | if self._clear_button_allowed(property): 1042 | data_list = self._render_list_clear_button(key, clear_col, data_list) 1043 | 1044 | if len(data_list) > 0: 1045 | for index, item in enumerate(data_list): 1046 | output = self._render_list_item( 1047 | streamlit_app, 1048 | key, 1049 | item, 1050 | index, 1051 | property, 1052 | ) 1053 | if output is not None: 1054 | object_list.append(output) 1055 | 1056 | if is_object: 1057 | streamlit_app.markdown("---") 1058 | 1059 | if not self._add_button_allowed(len(object_list), property): 1060 | add_col = add_col.empty() 1061 | 1062 | if not is_object: 1063 | streamlit_app.markdown("---") 1064 | 1065 | return object_list 1066 | 1067 | def _render_property(self, streamlit_app: Any, key: str, property: Dict) -> Any: 1068 | if schema_utils.is_single_enum_property(property, self._schema_references): 1069 | return self._render_single_enum_input(streamlit_app, key, property) 1070 | 1071 | if schema_utils.is_multi_enum_property(property, self._schema_references): 1072 | return self._render_multi_enum_input(streamlit_app, key, property) 1073 | 1074 | if schema_utils.is_single_file_property(property): 1075 | return self._render_single_file_input(streamlit_app, key, property) 1076 | 1077 | if schema_utils.is_multi_file_property(property): 1078 | return self._render_multi_file_input(streamlit_app, key, property) 1079 | 1080 | if schema_utils.is_single_datetime_property(property): 1081 | return self._render_single_datetime_input(streamlit_app, key, property) 1082 | 1083 | if schema_utils.is_single_color_property(property): 1084 | return self._render_single_color_input(streamlit_app, key, property) 1085 | 1086 | if schema_utils.is_single_boolean_property(property): 1087 | return self._render_single_boolean_input(streamlit_app, key, property) 1088 | 1089 | if schema_utils.is_single_dict_property(property): 1090 | return self._render_single_dict_input(streamlit_app, key, property) 1091 | 1092 | if schema_utils.is_single_number_property(property): 1093 | return self._render_single_number_input(streamlit_app, key, property) 1094 | 1095 | if schema_utils.is_single_string_property(property): 1096 | return self._render_single_string_input(streamlit_app, key, property) 1097 | 1098 | if schema_utils.is_single_object(property, self._schema_references): 1099 | return self._render_single_object_input(streamlit_app, key, property) 1100 | 1101 | if schema_utils.is_object_list_property(property, self._schema_references): 1102 | return self._render_list_input(streamlit_app, key, property) 1103 | 1104 | if schema_utils.is_property_list(property): 1105 | return self._render_list_input(streamlit_app, key, property) 1106 | 1107 | if schema_utils.is_single_reference(property): 1108 | return self._render_single_reference(streamlit_app, key, property) 1109 | 1110 | if schema_utils.is_union_property(property): 1111 | return self._render_union_property(streamlit_app, key, property) 1112 | 1113 | streamlit_app.warning( 1114 | "The type of the following property is currently not supported: " 1115 | + str(property.get("title")) 1116 | ) 1117 | raise Exception("Unsupported property") 1118 | 1119 | 1120 | class OutputUI: 1121 | """Output UI renderer. 1122 | 1123 | lazydocs: ignore 1124 | """ 1125 | 1126 | def __init__(self, output_data: Any, input_data: Optional[Any] = None): 1127 | self._output_data = output_data 1128 | self._input_data = input_data 1129 | 1130 | def render_ui(self) -> None: 1131 | try: 1132 | if isinstance(self._output_data, BaseModel): 1133 | self._render_single_output(st, self._output_data) 1134 | return 1135 | if type(self._output_data) == list: 1136 | self._render_list_output(st, self._output_data) 1137 | return 1138 | except Exception as ex: 1139 | st.exception(ex) 1140 | # TODO: Fallback to 1141 | # st.json(jsonable_encoder(self._output_data)) 1142 | 1143 | def _render_single_text_property( 1144 | self, streamlit: Any, property_schema: Dict, value: Any 1145 | ) -> None: 1146 | # Add title and subheader 1147 | streamlit.subheader(property_schema.get("title")) 1148 | if property_schema.get("description"): 1149 | streamlit.markdown(property_schema.get("description")) 1150 | if value is None or value == "": 1151 | streamlit.info("No value returned!") 1152 | else: 1153 | streamlit.code(str(value), language="plain") 1154 | 1155 | def _render_single_file_property( 1156 | self, streamlit: Any, property_schema: Dict, value: Any 1157 | ) -> None: 1158 | # Add title and subheader 1159 | streamlit.subheader(property_schema.get("title")) 1160 | if property_schema.get("description"): 1161 | streamlit.markdown(property_schema.get("description")) 1162 | if value is None or len(value) == 0: 1163 | streamlit.info("No value returned!") 1164 | else: 1165 | # TODO: detect if it is base64 1166 | file_extension = "" 1167 | if "mime_type" in property_schema: 1168 | mime_type = property_schema["mime_type"] 1169 | file_extension = mimetypes.guess_extension(mime_type) or "" 1170 | 1171 | if _is_compatible_audio(mime_type): 1172 | streamlit.audio(value, format=mime_type) 1173 | return 1174 | 1175 | if _is_compatible_image(mime_type): 1176 | streamlit.image(value) 1177 | return 1178 | 1179 | if _is_compatible_video(mime_type): 1180 | streamlit.video(value, format=mime_type) 1181 | return 1182 | 1183 | filename = ( 1184 | (property_schema["title"] + file_extension) 1185 | .lower() 1186 | .strip() 1187 | .replace(" ", "-") 1188 | ) 1189 | st.download_button("Download File", value, file_name=filename) 1190 | 1191 | def _render_single_complex_property( 1192 | self, streamlit: Any, property_schema: Dict, value: Any 1193 | ) -> None: 1194 | # Add title and subheader 1195 | streamlit.subheader(property_schema.get("title")) 1196 | if property_schema.get("description"): 1197 | streamlit.markdown(property_schema.get("description")) 1198 | 1199 | streamlit.json(json.dumps(value, default=_pydantic_encoder)) 1200 | 1201 | def _render_single_output(self, streamlit: Any, output_data: BaseModel) -> None: 1202 | try: 1203 | if _has_output_ui_renderer(output_data): 1204 | if _function_has_named_arg(output_data.render_output_ui, "input"): # type: ignore 1205 | # render method also requests the input data 1206 | output_data.render_output_ui(streamlit, input=self._input_data) # type: ignore 1207 | else: 1208 | output_data.render_output_ui(streamlit) # type: ignore 1209 | return 1210 | except Exception: 1211 | # TODO 1212 | pass 1213 | # Use default auto-generation methods if the custom rendering throws an exception 1214 | # logger.exception( 1215 | # "Failed to execute custom render_output_ui function. Using auto-generation instead" 1216 | # ) 1217 | 1218 | model_schema = output_data.model_json_schema(by_alias=False) 1219 | model_properties = model_schema.get("properties") 1220 | definitions = model_schema.get("$defs") 1221 | 1222 | if model_properties: 1223 | for property_key in output_data.__dict__: 1224 | property_schema = model_properties.get(property_key) 1225 | if not property_schema.get("title"): 1226 | # Set property key as fallback title 1227 | property_schema["title"] = property_key 1228 | 1229 | output_property_value = output_data.__dict__[property_key] 1230 | 1231 | if _has_output_ui_renderer(output_property_value): 1232 | output_property_value.render_output_ui(streamlit) # type: ignore 1233 | continue 1234 | 1235 | if isinstance(output_property_value, BaseModel): 1236 | # Render output recursivly 1237 | streamlit.subheader(property_schema.get("title")) 1238 | if property_schema.get("description"): 1239 | streamlit.markdown(property_schema.get("description")) 1240 | self._render_single_output(streamlit, output_property_value) 1241 | continue 1242 | 1243 | if property_schema: 1244 | if schema_utils.is_multi_file_property(property_schema): 1245 | for file in output_property_value: 1246 | self._render_single_file_property( 1247 | streamlit, property_schema, file 1248 | ) 1249 | continue 1250 | 1251 | if schema_utils.is_single_file_property(property_schema): 1252 | self._render_single_file_property( 1253 | streamlit, property_schema, output_property_value 1254 | ) 1255 | continue 1256 | 1257 | if ( 1258 | schema_utils.is_single_string_property(property_schema) 1259 | or schema_utils.is_single_number_property(property_schema) 1260 | or schema_utils.is_single_datetime_property(property_schema) 1261 | or schema_utils.is_single_boolean_property(property_schema) 1262 | ): 1263 | self._render_single_text_property( 1264 | streamlit, property_schema, output_property_value 1265 | ) 1266 | continue 1267 | if definitions and schema_utils.is_single_enum_property( 1268 | property_schema, definitions 1269 | ): 1270 | self._render_single_text_property( 1271 | streamlit, property_schema, output_property_value.value 1272 | ) 1273 | continue 1274 | 1275 | if isinstance(output_property_value, (set, dict, tuple)): 1276 | self._render_single_text_property( 1277 | streamlit, property_schema, output_property_value 1278 | ) 1279 | continue 1280 | 1281 | # TODO: render dict as table 1282 | 1283 | self._render_single_complex_property( 1284 | streamlit, property_schema, output_property_value 1285 | ) 1286 | return 1287 | 1288 | # Display single field in code block: 1289 | # if len(output_data.__dict__) == 1: 1290 | # value = next(iter(output_data.__dict__.values())) 1291 | 1292 | # if type(value) in (int, float, str): 1293 | # # Should not be a complex object (with __dict__) -> should be a primitive 1294 | # # hasattr(output_data.__dict__[0], '__dict__') 1295 | # streamlit.subheader("This is a test:") 1296 | # streamlit.code(value, language="plain") 1297 | # return 1298 | 1299 | st.error("Cannot render output") 1300 | # TODO: Fallback to json output 1301 | # streamlit.json(jsonable_encoder(output_data)) 1302 | 1303 | def _render_list_output(self, streamlit: Any, output_data: List) -> None: 1304 | try: 1305 | data_items: List = [] 1306 | for data_item in output_data: 1307 | if _has_output_ui_renderer(data_item): 1308 | # Render using the render function 1309 | data_item.render_output_ui(streamlit) # type: ignore 1310 | continue 1311 | data_items.append(data_item.model_dump()) 1312 | # Try to show as dataframe 1313 | streamlit.table(pd.DataFrame(data_items)) 1314 | except Exception: 1315 | st.error("Cannot render output list") 1316 | # TODO Fallback to 1317 | # streamlit.json(jsonable_encoder(output_data)) 1318 | 1319 | 1320 | def pydantic_input( 1321 | key: str, 1322 | model: Type[BaseModel], 1323 | group_optional_fields: GroupOptionalFieldsStrategy = "no", # type: ignore 1324 | lowercase_labels: bool = False, 1325 | ignore_empty_values: bool = False, 1326 | ) -> Dict: 1327 | """Auto-generates input UI elements for a selected Pydantic class. 1328 | 1329 | Args: 1330 | key (str): A string that identifies the form. Each form must have its own key. 1331 | model (Type[BaseModel]): The input model. Either a class or instance based on Pydantic `BaseModel` or Python `dataclass`. 1332 | group_optional_fields (str, optional): If `sidebar`, optional input elements will be rendered on the sidebar. 1333 | If `expander`, optional input elements will be rendered inside an expander element. Defaults to `no`. 1334 | lowercase_labels (bool): If `True`, all input element labels will be lowercased. Defaults to `False`. 1335 | ignore_empty_values (bool): If `True`, empty values for strings and numbers will not be stored in the session state. Defaults to `False`. 1336 | 1337 | Returns: 1338 | Dict: A dictionary with the current state of the input data. 1339 | """ 1340 | return InputUI( 1341 | key, 1342 | model, 1343 | group_optional_fields=group_optional_fields, 1344 | lowercase_labels=lowercase_labels, 1345 | ignore_empty_values=ignore_empty_values, 1346 | return_model=False, 1347 | ).render_ui() 1348 | 1349 | 1350 | def pydantic_output(output_data: Any) -> None: 1351 | """Auto-generates output UI elements for all properties of a (Pydantic-based) model instance. 1352 | 1353 | Args: 1354 | output_data (Any): The output data. 1355 | """ 1356 | 1357 | OutputUI(output_data).render_ui() 1358 | 1359 | 1360 | # Define generic type to allow autocompletion for the model fields 1361 | T = TypeVar("T", bound=BaseModel) 1362 | 1363 | 1364 | def pydantic_form( 1365 | key: str, 1366 | model: Type[T], 1367 | submit_label: str = "Submit", 1368 | clear_on_submit: bool = False, 1369 | group_optional_fields: GroupOptionalFieldsStrategy = "no", # type: ignore 1370 | lowercase_labels: bool = False, 1371 | ignore_empty_values: bool = False, 1372 | ) -> Optional[T]: 1373 | """Auto-generates a Streamlit form based on the given (Pydantic-based) input class. 1374 | 1375 | Args: 1376 | key (str): A string that identifies the form. Each form must have its own key. 1377 | model (Type[BaseModel]): The input model. Either a class or instance based on Pydantic `BaseModel` or Python `dataclass`. 1378 | submit_label (str): A short label explaining to the user what this button is for. Defaults to “Submit”. 1379 | clear_on_submit (bool): If True, all widgets inside the form will be reset to their default values after the user presses the Submit button. Defaults to False. 1380 | group_optional_fields (str, optional): If `sidebar`, optional input elements will be rendered on the sidebar. 1381 | If `expander`, optional input elements will be rendered inside an expander element. Defaults to `no`. 1382 | lowercase_labels (bool): If `True`, all input element labels will be lowercased. Defaults to `False`. 1383 | ignore_empty_values (bool): If `True`, empty values for strings and numbers will not be stored in the session state. Defaults to `False`. 1384 | 1385 | Returns: 1386 | Optional[BaseModel]: An instance of the given input class, 1387 | if the submit button is used and the input data passes the Pydantic validation. 1388 | """ 1389 | 1390 | with st.form(key=key, clear_on_submit=clear_on_submit): 1391 | input_state = InputUI( 1392 | key, 1393 | model, 1394 | group_optional_fields=group_optional_fields, 1395 | lowercase_labels=lowercase_labels, 1396 | ignore_empty_values=ignore_empty_values, 1397 | return_model=True, 1398 | ).render_ui() 1399 | 1400 | if st.form_submit_button(label=submit_label): 1401 | return input_state # type: ignore 1402 | return None 1403 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasmasuch/streamlit-pydantic/9f84145b6b6e74cdff3a7815ab75b0464c4d4f24/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasmasuch/streamlit-pydantic/9f84145b6b6e74cdff3a7815ab75b0464c4d4f24/tests/conftest.py -------------------------------------------------------------------------------- /tests/test_renderer.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | import streamlit_pydantic as sp 4 | 5 | 6 | def test_renderer() -> None: 7 | class TestModel(BaseModel): 8 | name: str 9 | 10 | sp.pydantic_form("my_key", TestModel) 11 | --------------------------------------------------------------------------------