pydantic-forms 2.5.0__tar.gz → 2.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pydantic_forms-2.7.0/PKG-INFO +132 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/README.md +19 -44
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/__init__.py +3 -1
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/core/asynchronous.py +7 -3
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/core/shared.py +64 -1
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/core/sync.py +13 -4
- pydantic_forms-2.7.0/pydantic_forms/exception_handlers/fastapi.py +84 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/exceptions.py +33 -10
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/types.py +24 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/utils/json.py +1 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/validators/__init__.py +3 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/validators/components/accept.py +7 -0
- pydantic_forms-2.7.0/pydantic_forms/validators/components/buttons.py +83 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/validators/components/callout.py +6 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/validators/components/choice.py +5 -2
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/validators/components/choice_list.py +3 -2
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/validators/components/contact_person.py +1 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/validators/components/contact_person_list.py +5 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/validators/components/display_subscription.py +4 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/validators/components/divider.py +1 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/validators/components/hidden.py +1 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/validators/components/label.py +1 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/validators/components/list_of_one.py +1 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/validators/components/list_of_two.py +1 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/validators/components/long_text.py +1 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/validators/components/markdown.py +5 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/validators/components/migration_summary.py +2 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/validators/components/organisation_id.py +4 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/validators/components/timestamp.py +7 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/validators/components/unique_constrained_list.py +2 -0
- pydantic_forms-2.7.0/pyproject.toml +247 -0
- pydantic_forms-2.7.0/pyproject.toml.orig +245 -0
- pydantic_forms-2.7.0/tests/paths.py +21 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/tests/unit_tests/exception_handlers/test_fastapi.py +15 -1
- pydantic_forms-2.7.0/tests/unit_tests/test_buttons.py +72 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/tests/unit_tests/test_core.py +137 -4
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/tests/unit_tests/test_core_async.py +12 -2
- pydantic_forms-2.7.0/tests/unit_tests/test_default_factory.py +57 -0
- pydantic_forms-2.7.0/tests/unit_tests/test_documentation.py +203 -0
- pydantic_forms-2.7.0/tests/unit_tests/test_form_meta.py +37 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/tests/unit_tests/test_read_only_field.py +9 -0
- pydantic_forms-2.5.0/.bumpversion.cfg +0 -12
- pydantic_forms-2.5.0/.claude/.gitignore +0 -1
- pydantic_forms-2.5.0/.coveragerc +0 -3
- pydantic_forms-2.5.0/.github/dependabot.yml +0 -11
- pydantic_forms-2.5.0/.github/workflows/publish-release.yaml +0 -41
- pydantic_forms-2.5.0/.github/workflows/run-linting-tests.yml +0 -37
- pydantic_forms-2.5.0/.github/workflows/run-unit-tests.yml +0 -73
- pydantic_forms-2.5.0/.gitignore +0 -130
- pydantic_forms-2.5.0/.pre-commit-config.yaml +0 -53
- pydantic_forms-2.5.0/.stignore +0 -117
- pydantic_forms-2.5.0/PKG-INFO +0 -188
- pydantic_forms-2.5.0/codecov.yml +0 -2
- pydantic_forms-2.5.0/pydantic_forms/exception_handlers/fastapi.py +0 -73
- pydantic_forms-2.5.0/pyproject.toml +0 -171
- pydantic_forms-2.5.0/setup.cfg +0 -61
- pydantic_forms-2.5.0/setup.py +0 -20
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/LICENSE +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/core/__init__.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/core/translations.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/exception_handlers/__init__.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/py.typed +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/utils/__init__.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/utils/schema.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/validators/components/__init__.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/validators/components/read_only.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/validators/constants.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/validators/helpers.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/tests/__init__.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/tests/unit_tests/__init__.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/tests/unit_tests/exception_handlers/__init__.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/tests/unit_tests/helpers.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/tests/unit_tests/test_accept.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/tests/unit_tests/test_callout.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/tests/unit_tests/test_choice.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/tests/unit_tests/test_choice_list.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/tests/unit_tests/test_constrained_list.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/tests/unit_tests/test_contact_person.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/tests/unit_tests/test_contact_person_list.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/tests/unit_tests/test_display_subscription.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/tests/unit_tests/test_divider.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/tests/unit_tests/test_hidden.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/tests/unit_tests/test_label.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/tests/unit_tests/test_list_of_one.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/tests/unit_tests/test_list_of_two.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/tests/unit_tests/test_long_text.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/tests/unit_tests/test_markdown.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/tests/unit_tests/test_migration_summary.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/tests/unit_tests/test_organisation_id.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/tests/unit_tests/test_shared.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/tests/unit_tests/test_timestamp.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/tests/unit_tests/test_utils_dict_without_none.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/tests/unit_tests/test_utils_from_serializable.py +0 -0
- {pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/tests/unit_tests/test_utils_to_serializable.py +0 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pydantic-forms
|
|
3
|
+
Version: 2.7.0
|
|
4
|
+
Summary: Pydantic-forms engine.
|
|
5
|
+
Author: SURF
|
|
6
|
+
Author-email: SURF <automation-beheer@surf.nl>
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Web Environment
|
|
11
|
+
Classifier: Framework :: AsyncIO
|
|
12
|
+
Classifier: Framework :: FastAPI
|
|
13
|
+
Classifier: Framework :: Flask
|
|
14
|
+
Classifier: Framework :: Pydantic :: 2
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: Information Technology
|
|
17
|
+
Classifier: Intended Audience :: System Administrators
|
|
18
|
+
Classifier: Intended Audience :: Telecommunications Industry
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Programming Language :: Python :: 3
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
27
|
+
Classifier: Programming Language :: Python
|
|
28
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
29
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
30
|
+
Classifier: Topic :: Internet
|
|
31
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
32
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
33
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
34
|
+
Classifier: Topic :: Software Development
|
|
35
|
+
Classifier: Typing :: Typed
|
|
36
|
+
Requires-Dist: more-itertools>=10.0.0
|
|
37
|
+
Requires-Dist: pydantic[email]>=2.9.0
|
|
38
|
+
Requires-Dist: pydantic-i18n==0.4.5
|
|
39
|
+
Requires-Dist: fastapi>=0.103.2 ; extra == 'fastapi'
|
|
40
|
+
Requires-Dist: orjson>=3.10.7,<4.0.0 ; extra == 'orjson'
|
|
41
|
+
Requires-Python: >=3.10, <3.15
|
|
42
|
+
Project-URL: Documentation, https://workfloworchestrator.org/pydantic-forms/
|
|
43
|
+
Project-URL: Homepage, https://github.com/workfloworchestrator/pydantic-forms
|
|
44
|
+
Project-URL: Source, https://github.com/workfloworchestrator/pydantic-forms
|
|
45
|
+
Provides-Extra: fastapi
|
|
46
|
+
Provides-Extra: orjson
|
|
47
|
+
Description-Content-Type: text/markdown
|
|
48
|
+
|
|
49
|
+
# Pydantic forms
|
|
50
|
+
|
|
51
|
+
[](https://pypi.org/project/pydantic-forms)
|
|
52
|
+
[](https://pypi.org/project/pydantic-forms)
|
|
53
|
+
[](https://pepy.tech/project/pydantic-forms)
|
|
54
|
+
[](https://codecov.io/gh/workfloworchestrator/pydantic-forms)
|
|
55
|
+
|
|
56
|
+
A Python package that lets you add smart forms to [FastAPI](https://fastapi.tiangolo.com/)
|
|
57
|
+
and [Flask](https://palletsprojects.com/p/flask/). Forms will respond with a JSON scheme that
|
|
58
|
+
contains all info needed in a React frontend with uniforms to render the forms and handle all validation tasks.
|
|
59
|
+
|
|
60
|
+
Forms can also consist out of a wizard, so you can create complex form flows consisting out of multiple
|
|
61
|
+
consecutive forms. The forms and the validation logic are defined by
|
|
62
|
+
using [Pydantic](https://pydantic-docs.helpmanual.io/) models.
|
|
63
|
+
|
|
64
|
+
Documentation regarding the usage of Forms can be found at
|
|
65
|
+
[workfloworchestrator.org/pydantic-forms](https://workfloworchestrator.org/pydantic-forms/).
|
|
66
|
+
|
|
67
|
+
### Installation (Development standalone)
|
|
68
|
+
|
|
69
|
+
This project uses [uv](https://docs.astral.sh/uv/). Install the project and its dependencies to develop on the code:
|
|
70
|
+
|
|
71
|
+
```shell
|
|
72
|
+
uv sync --all-extras
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
This creates a `.venv`, installs `pydantic-forms` in editable mode, and installs the `dev` dependency group
|
|
76
|
+
(which chains in `test` and `doc`) plus the `fastapi` and `orjson` extras. There is no need to create or
|
|
77
|
+
activate a virtualenv yourself -- prefix commands with `uv run` and uv will use the right interpreter.
|
|
78
|
+
|
|
79
|
+
### Running tests
|
|
80
|
+
Run the unit-test suite to verify a correct setup.
|
|
81
|
+
|
|
82
|
+
```shell
|
|
83
|
+
uv run pytest tests/unit_tests
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
or with xdist:
|
|
87
|
+
|
|
88
|
+
```shell
|
|
89
|
+
uv run pytest -n auto tests/unit_tests
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
If you do not encounter any failures in the test, you should be able to develop features in the pydantic-forms.
|
|
93
|
+
|
|
94
|
+
### Installation (Development version used by a project that depends on pydantic-forms)
|
|
95
|
+
|
|
96
|
+
If you are working on a project that already uses `pydantic-forms` and you want to test your new form features
|
|
97
|
+
against it, point that project at your local checkout. Run this **from the other project**:
|
|
98
|
+
|
|
99
|
+
```shell
|
|
100
|
+
uv add --editable /path/to/pydantic-forms
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
This replaces the PyPI dependency with an editable install of your working copy and records it under
|
|
104
|
+
`[tool.uv.sources]` in that project's `pyproject.toml`. Undo it with:
|
|
105
|
+
|
|
106
|
+
```shell
|
|
107
|
+
uv remove pydantic-forms && uv add pydantic-forms
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
# Increasing the version number for a (pre) release.
|
|
111
|
+
|
|
112
|
+
When your PR is accepted you will get a version number.
|
|
113
|
+
|
|
114
|
+
You can do the necessary change with a clean, e.g. every change committed, branch:
|
|
115
|
+
|
|
116
|
+
```shell
|
|
117
|
+
uv version 0.0.1
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Note: specifying the version explicitly, instead of relying on `uv version --bump patch` to increase it, allows
|
|
121
|
+
you to set a "RC1" version if needed -- e.g. `uv version 0.0.1rc1`.
|
|
122
|
+
|
|
123
|
+
# Debugging Form behaviour
|
|
124
|
+
|
|
125
|
+
If you want/need the traceback of pydantic in a Form response you can add an env variable:
|
|
126
|
+
|
|
127
|
+
`
|
|
128
|
+
LOG_LEVEL_PYDANTIC_FORMS=DEBUG
|
|
129
|
+
`
|
|
130
|
+
|
|
131
|
+
This will add the traceback to the `JSONResponse`. If the loglevel is set to DEBUG the library will also add the
|
|
132
|
+
traceback to the logger.
|
|
@@ -13,75 +13,50 @@ Forms can also consist out of a wizard, so you can create complex form flows con
|
|
|
13
13
|
consecutive forms. The forms and the validation logic are defined by
|
|
14
14
|
using [Pydantic](https://pydantic-docs.helpmanual.io/) models.
|
|
15
15
|
|
|
16
|
-
Documentation regarding the usage of Forms can be found
|
|
17
|
-
[
|
|
16
|
+
Documentation regarding the usage of Forms can be found at
|
|
17
|
+
[workfloworchestrator.org/pydantic-forms](https://workfloworchestrator.org/pydantic-forms/).
|
|
18
18
|
|
|
19
19
|
### Installation (Development standalone)
|
|
20
|
-
Install the project and its dependencies to develop on the code.
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
This project uses [uv](https://docs.astral.sh/uv/). Install the project and its dependencies to develop on the code:
|
|
23
22
|
|
|
24
23
|
```shell
|
|
25
|
-
|
|
26
|
-
source venv/bin/activate
|
|
27
|
-
pip install flit
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
#### Step 2 - install the development code:
|
|
31
|
-
```shell
|
|
32
|
-
flit install --deps develop --symlink --python venv/bin/python
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
!!! danger
|
|
36
|
-
Make sure to use the flit binary that is installed in your environment. You can check the correct
|
|
37
|
-
path by running
|
|
38
|
-
```shell
|
|
39
|
-
which flit
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
To be sure that the packages will be installed against the correct venv you can also prepend the python interpreter
|
|
43
|
-
that you want to use:
|
|
44
|
-
|
|
45
|
-
```shell
|
|
46
|
-
flit install --deps develop --symlink --python venv/bin/python
|
|
24
|
+
uv sync --all-extras
|
|
47
25
|
```
|
|
48
26
|
|
|
27
|
+
This creates a `.venv`, installs `pydantic-forms` in editable mode, and installs the `dev` dependency group
|
|
28
|
+
(which chains in `test` and `doc`) plus the `fastapi` and `orjson` extras. There is no need to create or
|
|
29
|
+
activate a virtualenv yourself -- prefix commands with `uv run` and uv will use the right interpreter.
|
|
49
30
|
|
|
50
31
|
### Running tests
|
|
51
32
|
Run the unit-test suite to verify a correct setup.
|
|
52
33
|
|
|
53
|
-
#### Step 2 - Run tests
|
|
54
34
|
```shell
|
|
55
|
-
pytest tests/unit_tests
|
|
35
|
+
uv run pytest tests/unit_tests
|
|
56
36
|
```
|
|
57
37
|
|
|
58
38
|
or with xdist:
|
|
59
39
|
|
|
60
40
|
```shell
|
|
61
|
-
pytest -n auto tests/unit_tests
|
|
41
|
+
uv run pytest -n auto tests/unit_tests
|
|
62
42
|
```
|
|
63
43
|
|
|
64
44
|
If you do not encounter any failures in the test, you should be able to develop features in the pydantic-forms.
|
|
65
45
|
|
|
66
|
-
### Installation (Development
|
|
67
|
-
|
|
68
|
-
If you are working on a project that already uses the `pydantic-forms` and you want to test your new form features
|
|
69
|
-
against it, you can use some `flit` magic to symlink the dev version of the forms to your project. It will
|
|
70
|
-
automatically replace the pypi dep with a symlink to the development version
|
|
71
|
-
of the core and update/downgrade all required packages in your own project.
|
|
46
|
+
### Installation (Development version used by a project that depends on pydantic-forms)
|
|
72
47
|
|
|
73
|
-
|
|
48
|
+
If you are working on a project that already uses `pydantic-forms` and you want to test your new form features
|
|
49
|
+
against it, point that project at your local checkout. Run this **from the other project**:
|
|
74
50
|
|
|
75
51
|
```shell
|
|
76
|
-
|
|
77
|
-
source venv/bin/activate
|
|
78
|
-
pip install flit
|
|
52
|
+
uv add --editable /path/to/pydantic-forms
|
|
79
53
|
```
|
|
80
54
|
|
|
81
|
-
|
|
55
|
+
This replaces the PyPI dependency with an editable install of your working copy and records it under
|
|
56
|
+
`[tool.uv.sources]` in that project's `pyproject.toml`. Undo it with:
|
|
82
57
|
|
|
83
58
|
```shell
|
|
84
|
-
|
|
59
|
+
uv remove pydantic-forms && uv add pydantic-forms
|
|
85
60
|
```
|
|
86
61
|
|
|
87
62
|
# Increasing the version number for a (pre) release.
|
|
@@ -91,11 +66,11 @@ When your PR is accepted you will get a version number.
|
|
|
91
66
|
You can do the necessary change with a clean, e.g. every change committed, branch:
|
|
92
67
|
|
|
93
68
|
```shell
|
|
94
|
-
|
|
69
|
+
uv version 0.0.1
|
|
95
70
|
```
|
|
96
71
|
|
|
97
|
-
Note: specifying
|
|
98
|
-
set a "RC1" version if needed.
|
|
72
|
+
Note: specifying the version explicitly, instead of relying on `uv version --bump patch` to increase it, allows
|
|
73
|
+
you to set a "RC1" version if needed -- e.g. `uv version 0.0.1rc1`.
|
|
99
74
|
|
|
100
75
|
# Debugging Form behaviour
|
|
101
76
|
|
|
@@ -18,11 +18,12 @@ import structlog
|
|
|
18
18
|
from pydantic import ValidationError
|
|
19
19
|
from pydantic_i18n import PydanticI18n
|
|
20
20
|
|
|
21
|
-
from pydantic_forms.core.shared import FORMS
|
|
21
|
+
from pydantic_forms.core.shared import FORMS, GenerateFormJsonSchema, get_form_meta
|
|
22
22
|
from pydantic_forms.core.translations import translations
|
|
23
23
|
from pydantic_forms.exceptions import (
|
|
24
24
|
FormException,
|
|
25
25
|
FormNotCompleteError,
|
|
26
|
+
FormNotFoundError,
|
|
26
27
|
FormOverflowError,
|
|
27
28
|
FormValidationError,
|
|
28
29
|
)
|
|
@@ -102,12 +103,15 @@ async def post_form(
|
|
|
102
103
|
return generated_form
|
|
103
104
|
|
|
104
105
|
# Form is not completely filled raise next form
|
|
105
|
-
raise FormNotCompleteError(
|
|
106
|
+
raise FormNotCompleteError(
|
|
107
|
+
generated_form.model_json_schema(schema_generator=GenerateFormJsonSchema),
|
|
108
|
+
meta=get_form_meta(generated_form),
|
|
109
|
+
)
|
|
106
110
|
|
|
107
111
|
|
|
108
112
|
def _get_form(key: str) -> StateInputFormGeneratorAsync:
|
|
109
113
|
if not (func := FORMS.get(key)):
|
|
110
|
-
raise
|
|
114
|
+
raise FormNotFoundError(f"Form {key} does not exist.")
|
|
111
115
|
|
|
112
116
|
if not isasyncgenfunction(func):
|
|
113
117
|
raise FormException(f"Form {key} is not an asyncgenerator function")
|
|
@@ -11,10 +11,15 @@
|
|
|
11
11
|
# See the License for the specific language governing permissions and
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
from inspect import isasyncgenfunction, isgeneratorfunction
|
|
14
|
-
from typing import Any, Callable
|
|
14
|
+
from typing import Any, Callable, ClassVar, cast
|
|
15
15
|
|
|
16
16
|
import structlog
|
|
17
17
|
from pydantic import BaseModel, ConfigDict, PydanticUndefinedAnnotation, version
|
|
18
|
+
from pydantic.json_schema import GenerateJsonSchema, JsonSchemaValue
|
|
19
|
+
from pydantic_core import core_schema
|
|
20
|
+
|
|
21
|
+
from pydantic_forms.types import FormMeta
|
|
22
|
+
from pydantic_forms.validators.components.buttons import merge_buttons_into_meta
|
|
18
23
|
|
|
19
24
|
logger = structlog.get_logger(__name__)
|
|
20
25
|
|
|
@@ -22,6 +27,35 @@ logger = structlog.get_logger(__name__)
|
|
|
22
27
|
PYDANTIC_VERSION = version.version_short()
|
|
23
28
|
|
|
24
29
|
|
|
30
|
+
class GenerateFormJsonSchema(GenerateJsonSchema):
|
|
31
|
+
"""JSON schema generator that also emits ``default`` values produced by ``default_factory``.
|
|
32
|
+
|
|
33
|
+
Pydantic intentionally leaves ``default_factory`` results out of the generated JSON schema. For
|
|
34
|
+
forms this is a problem: ``default_factory`` is the recommended way to declare mutable defaults
|
|
35
|
+
(``Field(default_factory=list)`` and friends), and frontends pre-fill fields from the schema
|
|
36
|
+
``default``. Without it such fields render uninitialised, and dynamic array/object widgets reject
|
|
37
|
+
submission until the user performs a dummy add/remove interaction to materialise an empty value.
|
|
38
|
+
|
|
39
|
+
This generator evaluates argument-less factories and adds their result as the schema ``default``.
|
|
40
|
+
Factories that need the already-validated data (``default_factory_takes_data``) cannot be
|
|
41
|
+
evaluated at schema-generation time and are skipped, as is any factory that raises.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def default_schema(self, schema: core_schema.WithDefaultSchema) -> JsonSchemaValue:
|
|
45
|
+
json_schema = super().default_schema(schema)
|
|
46
|
+
if (
|
|
47
|
+
"default" not in json_schema
|
|
48
|
+
and "default_factory" in schema
|
|
49
|
+
and not schema.get("default_factory_takes_data", False)
|
|
50
|
+
):
|
|
51
|
+
factory = cast(Callable[[], Any], schema["default_factory"])
|
|
52
|
+
try:
|
|
53
|
+
json_schema["default"] = self.encode_default(factory())
|
|
54
|
+
except Exception: # never let a factory break schema generation
|
|
55
|
+
logger.debug("Could not evaluate default_factory for form schema", exc_info=True)
|
|
56
|
+
return json_schema
|
|
57
|
+
|
|
58
|
+
|
|
25
59
|
class DisplayOnlyFieldType:
|
|
26
60
|
pass
|
|
27
61
|
|
|
@@ -34,6 +68,18 @@ class FormPage(BaseModel):
|
|
|
34
68
|
validate_default=True,
|
|
35
69
|
)
|
|
36
70
|
|
|
71
|
+
meta__: ClassVar[FormMeta | None] = None
|
|
72
|
+
"""Data about the page itself, passed to the frontend alongside its JSON schema.
|
|
73
|
+
|
|
74
|
+
Set it on a subclass to tell the frontend something the schema cannot express, such as whether
|
|
75
|
+
another page follows (`hasNext`). It travels out as the `meta` key of the `FormNotCompleteError`
|
|
76
|
+
response, see `form_meta()`. Being a `ClassVar` it is not a form field, so it stays out of the
|
|
77
|
+
schema and the validated result.
|
|
78
|
+
|
|
79
|
+
Relabel or restyle the previous/next buttons with a `ButtonsConfig` field, or else with
|
|
80
|
+
`customButtons` here. The two can't be combined.
|
|
81
|
+
"""
|
|
82
|
+
|
|
37
83
|
def __init__(self, **data: Any):
|
|
38
84
|
frozen_fields = {k: v for k, v in self.__class__.model_fields.items() if v.frozen}
|
|
39
85
|
|
|
@@ -45,6 +91,11 @@ class FormPage(BaseModel):
|
|
|
45
91
|
mutable_data = {k: get_value(k, v) for k, v in data.items()}
|
|
46
92
|
super().__init__(**mutable_data)
|
|
47
93
|
|
|
94
|
+
@classmethod
|
|
95
|
+
def form_meta(cls) -> FormMeta | None:
|
|
96
|
+
"""Return `meta__` with the page's `ButtonsConfig` field added."""
|
|
97
|
+
return merge_buttons_into_meta(cls.__name__, cls.meta__, cls.model_fields)
|
|
98
|
+
|
|
48
99
|
if PYDANTIC_VERSION in ("2.9", "2.10", "2.11"):
|
|
49
100
|
|
|
50
101
|
@classmethod
|
|
@@ -52,6 +103,7 @@ class FormPage(BaseModel):
|
|
|
52
103
|
# The default and requiredness of a field is not a property of a field
|
|
53
104
|
# In the case of DisplayOnlyFieldTypes, we do kind of want that.
|
|
54
105
|
# Using this method we set the right properties after the form is created
|
|
106
|
+
cls.form_meta() # run now so a conflicting declaration fails on import, not when the page is first sent
|
|
55
107
|
needs_rebuild = False
|
|
56
108
|
|
|
57
109
|
for field in cls.model_fields.values():
|
|
@@ -84,6 +136,7 @@ class FormPage(BaseModel):
|
|
|
84
136
|
# The default and requiredness of a field is not a property of a field
|
|
85
137
|
# In the case of DisplayOnlyFieldTypes, we do kind of want that.
|
|
86
138
|
# Using this method we set the right properties after the form is created
|
|
139
|
+
cls.form_meta()
|
|
87
140
|
needs_rebuild = False
|
|
88
141
|
|
|
89
142
|
for field in cls.model_fields.values():
|
|
@@ -111,3 +164,13 @@ def register_form(key: str, form: Callable) -> None:
|
|
|
111
164
|
|
|
112
165
|
def list_forms() -> list[str]:
|
|
113
166
|
return list(FORMS.keys())
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def get_form_meta(form: type[BaseModel]) -> Any:
|
|
170
|
+
"""Return the meta to send to the frontend with `form`.
|
|
171
|
+
|
|
172
|
+
A generator may yield any pydantic model, not only a `FormPage`; such a model can still carry a `meta__`.
|
|
173
|
+
"""
|
|
174
|
+
if issubclass(form, FormPage):
|
|
175
|
+
return form.form_meta()
|
|
176
|
+
return getattr(form, "meta__", None)
|
|
@@ -18,9 +18,15 @@ import structlog
|
|
|
18
18
|
from pydantic import ValidationError
|
|
19
19
|
from pydantic_i18n import PydanticI18n
|
|
20
20
|
|
|
21
|
-
from pydantic_forms.core.shared import FORMS
|
|
21
|
+
from pydantic_forms.core.shared import FORMS, GenerateFormJsonSchema, get_form_meta
|
|
22
22
|
from pydantic_forms.core.translations import translations
|
|
23
|
-
from pydantic_forms.exceptions import
|
|
23
|
+
from pydantic_forms.exceptions import (
|
|
24
|
+
FormException,
|
|
25
|
+
FormNotCompleteError,
|
|
26
|
+
FormNotFoundError,
|
|
27
|
+
FormOverflowError,
|
|
28
|
+
FormValidationError,
|
|
29
|
+
)
|
|
24
30
|
from pydantic_forms.types import InputForm, State, StateInputFormGenerator
|
|
25
31
|
|
|
26
32
|
logger = structlog.get_logger(__name__)
|
|
@@ -88,7 +94,10 @@ def post_form(
|
|
|
88
94
|
generated_form = generator.send(form_validated_data)
|
|
89
95
|
|
|
90
96
|
# Form is not completely filled; raise next form
|
|
91
|
-
raise FormNotCompleteError(
|
|
97
|
+
raise FormNotCompleteError(
|
|
98
|
+
generated_form.model_json_schema(schema_generator=GenerateFormJsonSchema),
|
|
99
|
+
meta=get_form_meta(generated_form),
|
|
100
|
+
)
|
|
92
101
|
except StopIteration as e:
|
|
93
102
|
if user_inputs:
|
|
94
103
|
raise FormOverflowError(f"Did not process all user_inputs ({len(user_inputs)} remaining)")
|
|
@@ -99,7 +108,7 @@ def post_form(
|
|
|
99
108
|
|
|
100
109
|
def _get_form(key: str) -> StateInputFormGenerator:
|
|
101
110
|
if not (func := FORMS.get(key)):
|
|
102
|
-
raise
|
|
111
|
+
raise FormNotFoundError(f"Form {key} does not exist.")
|
|
103
112
|
|
|
104
113
|
if not isgeneratorfunction(func):
|
|
105
114
|
raise FormException(f"Form {key} is not a generator function")
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Copyright 2019-2023 SURF.
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
# you may not use this file except in compliance with the License.
|
|
4
|
+
# You may obtain a copy of the License at
|
|
5
|
+
#
|
|
6
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
#
|
|
8
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
# See the License for the specific language governing permissions and
|
|
12
|
+
# limitations under the License.
|
|
13
|
+
import os
|
|
14
|
+
|
|
15
|
+
# TODO Decide how to expose this so pydantic-forms can be framework agnostic
|
|
16
|
+
from http import HTTPStatus
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
import structlog
|
|
20
|
+
from fastapi.requests import Request
|
|
21
|
+
from fastapi.responses import JSONResponse
|
|
22
|
+
|
|
23
|
+
from pydantic_forms.exceptions import (
|
|
24
|
+
FormException,
|
|
25
|
+
FormNotCompleteError,
|
|
26
|
+
FormNotFoundError,
|
|
27
|
+
FormValidationError,
|
|
28
|
+
show_ex,
|
|
29
|
+
)
|
|
30
|
+
from pydantic_forms.utils.json import json_dumps, json_loads
|
|
31
|
+
|
|
32
|
+
logger = structlog.get_logger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def form_error_handler(request: Request, exc: FormException) -> JSONResponse:
|
|
36
|
+
"""FastAPI exception handler that turns a FormException into a HTTP 4xx/5xx response with JSON body."""
|
|
37
|
+
match exc:
|
|
38
|
+
case FormValidationError():
|
|
39
|
+
status = HTTPStatus.BAD_REQUEST
|
|
40
|
+
base_content = _create_content(exc, status, "Form not valid")
|
|
41
|
+
detail_content = base_content | {
|
|
42
|
+
"validation_errors": json_loads(json_dumps(exc.errors)),
|
|
43
|
+
}
|
|
44
|
+
debug_content = _add_traceback(exc, detail_content)
|
|
45
|
+
return JSONResponse(debug_content, status_code=status)
|
|
46
|
+
|
|
47
|
+
case FormNotCompleteError():
|
|
48
|
+
status = HTTPStatus.NOT_EXTENDED
|
|
49
|
+
base_content = _create_content(exc, status, "Form not complete")
|
|
50
|
+
detail_content = base_content | {
|
|
51
|
+
"form": json_loads(json_dumps(exc.form)),
|
|
52
|
+
"meta": getattr(exc, "meta", None),
|
|
53
|
+
}
|
|
54
|
+
debug_content = _add_traceback(exc, detail_content)
|
|
55
|
+
return JSONResponse(debug_content, status_code=status)
|
|
56
|
+
|
|
57
|
+
case FormNotFoundError():
|
|
58
|
+
status = HTTPStatus.NOT_FOUND
|
|
59
|
+
base_content = _create_content(exc, status, "Form not found")
|
|
60
|
+
return JSONResponse(base_content, status_code=status)
|
|
61
|
+
|
|
62
|
+
case _:
|
|
63
|
+
status = HTTPStatus.INTERNAL_SERVER_ERROR
|
|
64
|
+
base_content = _create_content(exc, status, "Internal Server Error")
|
|
65
|
+
return JSONResponse(base_content, status_code=status)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _create_content(exc: FormException, status: HTTPStatus, title: str) -> dict[str, str | HTTPStatus]:
|
|
69
|
+
return {
|
|
70
|
+
"type": type(exc).__name__,
|
|
71
|
+
"detail": str(exc),
|
|
72
|
+
"title": title,
|
|
73
|
+
"status": status,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _add_traceback(exc: FormException, content: dict[str, Any]) -> dict[str, Any]:
|
|
78
|
+
LOG_LEVEL_PYDANTIC_FORMS = "DEBUG" if os.getenv("LOG_LEVEL_PYDANTIC_FORMS", "INFO").upper() == "DEBUG" else "INFO"
|
|
79
|
+
if LOG_LEVEL_PYDANTIC_FORMS == "DEBUG":
|
|
80
|
+
content_with_traceback = content | {"traceback": show_ex(exc)}
|
|
81
|
+
logger.debug("Form validation Response", result=content_with_traceback)
|
|
82
|
+
return content_with_traceback
|
|
83
|
+
|
|
84
|
+
return content
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import traceback
|
|
3
|
+
from collections.abc import Mapping
|
|
3
4
|
from typing import Any, Iterable, Optional, TypedDict, Union, cast
|
|
4
5
|
|
|
5
6
|
import structlog
|
|
@@ -36,9 +37,26 @@ class FormOverflowError(FormException):
|
|
|
36
37
|
"""Raised when more inputs are provided than the form can process."""
|
|
37
38
|
|
|
38
39
|
|
|
40
|
+
class FormNotFoundError(FormException):
|
|
41
|
+
"""Raised when the requested form key is not registered."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
Loc = tuple[Union[int, str], ...]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _error_is_nested_object_validator(error: ErrorDetails) -> bool:
|
|
48
|
+
"""Return True for custom errors raised for an object payload instead of a leaf field."""
|
|
49
|
+
exc = error.get("ctx", {}).get("error")
|
|
50
|
+
return (
|
|
51
|
+
bool(error["loc"]) and isinstance(exc, (ValueError, AssertionError)) and isinstance(error.get("input"), Mapping)
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
39
55
|
# def convert_errors(validation_error: ValidationError) -> Iterable[ErrorDetails]:
|
|
40
56
|
def convert_errors(
|
|
41
|
-
validation_error: ValidationError,
|
|
57
|
+
validation_error: ValidationError,
|
|
58
|
+
tr: PydanticI18n,
|
|
59
|
+
locale: str = "en_US",
|
|
42
60
|
) -> Iterable[ErrorDetails]:
|
|
43
61
|
"""Convert Pydantic's error messages to our needs.
|
|
44
62
|
|
|
@@ -55,6 +73,11 @@ def convert_errors(
|
|
|
55
73
|
# Reinstate the "__root__" location for validation errors raised by Pydantic v1's root_validator.
|
|
56
74
|
# With Pydantic v2's model_validator the location is an empty string, which breaks frontend clients.
|
|
57
75
|
error["loc"] = ("__root__",)
|
|
76
|
+
elif _error_is_nested_object_validator(error):
|
|
77
|
+
# Errors raised by a model_validator on a nested model point at the nested model itself, which
|
|
78
|
+
# frontend clients cannot map to an input field. Mark them as model-level errors so that clients
|
|
79
|
+
# display the custom message, like they do for the form's own "__root__" errors.
|
|
80
|
+
error["loc"] = (*error["loc"], "__root__")
|
|
58
81
|
return
|
|
59
82
|
|
|
60
83
|
if LOG_LEVEL_PYDANTIC_FORMS == "DEBUG":
|
|
@@ -90,9 +113,6 @@ class FormValidationError(FormException):
|
|
|
90
113
|
)
|
|
91
114
|
|
|
92
115
|
|
|
93
|
-
Loc = tuple[Union[int, str], ...]
|
|
94
|
-
|
|
95
|
-
|
|
96
116
|
class _ErrorDictRequired(TypedDict):
|
|
97
117
|
loc: Loc
|
|
98
118
|
msg: str
|
|
@@ -122,12 +142,15 @@ def _display_error_type_and_ctx(error: "ErrorDict") -> str:
|
|
|
122
142
|
def show_ex(ex: Exception, stacklimit: Union[int, None] = None) -> str:
|
|
123
143
|
"""Show an exception, including its class name, message and (limited) stacktrace.
|
|
124
144
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
145
|
+
Examples:
|
|
146
|
+
--------
|
|
147
|
+
>>> try:
|
|
148
|
+
... raise Exception("Something went wrong")
|
|
149
|
+
... except Exception as e:
|
|
150
|
+
... print(show_ex(e))
|
|
151
|
+
Exception: Something went wrong
|
|
152
|
+
...
|
|
153
|
+
|
|
131
154
|
"""
|
|
132
155
|
tbfmt = "".join(traceback.format_tb(ex.__traceback__, stacklimit))
|
|
133
156
|
return "{}: {}\n{}".format(type(ex).__name__, ex, tbfmt)
|
|
@@ -41,6 +41,30 @@ class SummaryData(TypedDict, total=False):
|
|
|
41
41
|
columns: list[list[Union[str, int, bool, float]]]
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
class ButtonConfig(TypedDict, total=False):
|
|
45
|
+
text: str
|
|
46
|
+
color: str
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CustomButtons(TypedDict, total=False):
|
|
50
|
+
previous: ButtonConfig
|
|
51
|
+
next: ButtonConfig
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class FormMeta(TypedDict, total=False):
|
|
55
|
+
"""Documented keys for ``FormPage.meta__``, not enforced at runtime; extra keys are allowed.
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
hasNext: Whether another page follows, so the frontend can label the submit button
|
|
59
|
+
accordingly.
|
|
60
|
+
customButtons: Per-button overrides (label, color) for "previous" and "next". Prefer a `ButtonsConfig`
|
|
61
|
+
field on the page, which sets this key; the two cannot be combined.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
hasNext: bool
|
|
65
|
+
customButtons: CustomButtons
|
|
66
|
+
|
|
67
|
+
|
|
44
68
|
InputForm = Type[BaseModel]
|
|
45
69
|
AcceptData = list[Union[tuple[str, AcceptItemType], tuple[str, AcceptItemType, dict]]]
|
|
46
70
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from pydantic_forms.validators.components.accept import Accept, AcceptValues
|
|
2
|
+
from pydantic_forms.validators.components.buttons import Button, ButtonsConfig
|
|
2
3
|
from pydantic_forms.validators.components.callout import Callout, callout
|
|
3
4
|
from pydantic_forms.validators.components.choice import Choice
|
|
4
5
|
from pydantic_forms.validators.components.choice_list import choice_list
|
|
@@ -21,6 +22,8 @@ from pydantic_forms.validators.components.unique_constrained_list import unique_
|
|
|
21
22
|
__all__ = (
|
|
22
23
|
"Accept",
|
|
23
24
|
"AcceptValues",
|
|
25
|
+
"Button",
|
|
26
|
+
"ButtonsConfig",
|
|
24
27
|
"Choice",
|
|
25
28
|
"choice_list",
|
|
26
29
|
"contact_person_list",
|
{pydantic_forms-2.5.0 → pydantic_forms-2.7.0}/pydantic_forms/validators/components/accept.py
RENAMED
|
@@ -19,11 +19,18 @@ from pydantic_forms.types import AcceptData, strEnum
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class AcceptValues(strEnum):
|
|
22
|
+
|
|
22
23
|
ACCEPTED = "ACCEPTED"
|
|
23
24
|
INCOMPLETE = "INCOMPLETE"
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
class Accept(str):
|
|
28
|
+
"""A checklist the user works through before the form can be submitted.
|
|
29
|
+
|
|
30
|
+
Validation fails while the value is still `"INCOMPLETE"`. Subclass it and set `data` to
|
|
31
|
+
describe the items to display.
|
|
32
|
+
"""
|
|
33
|
+
|
|
27
34
|
data: ClassVar[Optional[AcceptData]] = None
|
|
28
35
|
|
|
29
36
|
_enum_adapter = TypeAdapter(AcceptValues)
|