fh-pydantic-form 0.1.3__py3-none-any.whl → 0.2.0__py3-none-any.whl
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.
Potentially problematic release.
This version of fh-pydantic-form might be problematic. Click here for more details.
- fh_pydantic_form/__init__.py +38 -3
- fh_pydantic_form/defaults.py +160 -0
- fh_pydantic_form/field_renderers.py +339 -134
- fh_pydantic_form/form_parser.py +75 -0
- fh_pydantic_form/form_renderer.py +229 -82
- fh_pydantic_form/type_helpers.py +108 -1
- fh_pydantic_form/ui_style.py +123 -0
- fh_pydantic_form-0.2.0.dist-info/METADATA +685 -0
- fh_pydantic_form-0.2.0.dist-info/RECORD +13 -0
- fh_pydantic_form-0.1.3.dist-info/METADATA +0 -327
- fh_pydantic_form-0.1.3.dist-info/RECORD +0 -11
- {fh_pydantic_form-0.1.3.dist-info → fh_pydantic_form-0.2.0.dist-info}/WHEEL +0 -0
- {fh_pydantic_form-0.1.3.dist-info → fh_pydantic_form-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,327 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: fh-pydantic-form
|
|
3
|
-
Version: 0.1.3
|
|
4
|
-
Summary: a library to turn any pydantic BaseModel object into a fasthtml/monsterui input form
|
|
5
|
-
Project-URL: Homepage, https://github.com/Marcura/fh-pydantic-form
|
|
6
|
-
Project-URL: Repository, https://github.com/Marcura/fh-pydantic-form
|
|
7
|
-
Project-URL: Documentation, https://github.com/Marcura/fh-pydantic-form
|
|
8
|
-
Author-email: Oege Dijk <o.dijk@marcura.com>
|
|
9
|
-
Maintainer-email: Oege Dijk <o.dijk@marcura.com>
|
|
10
|
-
License-File: LICENSE
|
|
11
|
-
Keywords: fasthtml,forms,monsterui,pydantic,ui,web
|
|
12
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
-
Classifier: Operating System :: OS Independent
|
|
14
|
-
Classifier: Programming Language :: Python :: 3
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
-
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content :: Content Management System
|
|
19
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
-
Requires-Python: >=3.10
|
|
21
|
-
Requires-Dist: monsterui>=1.0.19
|
|
22
|
-
Requires-Dist: pydantic>=2.0
|
|
23
|
-
Requires-Dist: python-fasthtml>=0.12.12
|
|
24
|
-
Description-Content-Type: text/markdown
|
|
25
|
-
|
|
26
|
-
# fh-pydantic-form
|
|
27
|
-
|
|
28
|
-
**Generate HTML forms from Pydantic models for your FastHTML applications.**
|
|
29
|
-
|
|
30
|
-
`fh-pydantic-form` simplifies creating web forms for [FastHTML](https://github.com/AnswerDotAI/fasthtml) by automatically generating the necessary HTML input elements based on your Pydantic model definitions. It integrates seamlessly with and leverages [MonsterUI](https://github.com/AnswerDotAI/monsterui) components for styling.
|
|
31
|
-
|
|
32
|
-
<details >
|
|
33
|
-
<summary>show demo screen recording</summary>
|
|
34
|
-
<video src="https://private-user-images.githubusercontent.com/27999937/436237879-feabf388-22af-43e6-b054-f103b8a1b6e6.mp4" controls="controls" style="max-width: 730px;">
|
|
35
|
-
</video>
|
|
36
|
-
</details>
|
|
37
|
-
|
|
38
|
-
## Purpose
|
|
39
|
-
|
|
40
|
-
- **Reduce Boilerplate:** Automatically render form inputs (text, number, checkbox, select, date, time, etc.) based on Pydantic field types and annotations.
|
|
41
|
-
- **Data Validation:** Leverage Pydantic's validation rules directly from form submissions.
|
|
42
|
-
- **Nested Structures:** Support for nested Pydantic models and lists of models/simple types.
|
|
43
|
-
- **Dynamic Lists:** Built-in HTMX endpoints and JavaScript for adding, deleting, and reordering items in lists within the form.
|
|
44
|
-
- **Customization:** Easily register custom renderers for specific Pydantic types or fields.
|
|
45
|
-
|
|
46
|
-
## Installation
|
|
47
|
-
|
|
48
|
-
You can install `fh-pydantic-form` using either `pip` or `uv`.
|
|
49
|
-
|
|
50
|
-
**Using pip:**
|
|
51
|
-
|
|
52
|
-
```bash
|
|
53
|
-
pip install fh-pydantic-form
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
Using uv:
|
|
57
|
-
```bash
|
|
58
|
-
uv add fh-pydantic-form
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
This will also install necessary dependencies like `pydantic`, `python-fasthtml`, and `monsterui`.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
# Basic Usage
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
```python
|
|
68
|
-
|
|
69
|
-
# examples/simple_example.py
|
|
70
|
-
import fasthtml.common as fh
|
|
71
|
-
import monsterui.all as mui
|
|
72
|
-
from pydantic import BaseModel, ValidationError
|
|
73
|
-
|
|
74
|
-
# 1. Import the form renderer
|
|
75
|
-
from fh_pydantic_form import PydanticForm
|
|
76
|
-
|
|
77
|
-
app, rt = fh.fast_app(
|
|
78
|
-
hdrs=[
|
|
79
|
-
mui.Theme.blue.headers(),
|
|
80
|
-
# Add list_manipulation_js() if using list fields
|
|
81
|
-
# from fh_pydantic_form import list_manipulation_js
|
|
82
|
-
# list_manipulation_js(),
|
|
83
|
-
],
|
|
84
|
-
pico=False, # Using MonsterUI, not PicoCSS
|
|
85
|
-
live=True, # Enable live reload for development
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
# 2. Define your Pydantic model
|
|
89
|
-
class SimpleModel(BaseModel):
|
|
90
|
-
"""Model representing a simple form"""
|
|
91
|
-
name: str = "Default Name"
|
|
92
|
-
age: int
|
|
93
|
-
is_active: bool = True
|
|
94
|
-
|
|
95
|
-
# 3. Create a form renderer instance
|
|
96
|
-
# - 'my_form': Unique name for the form (used for prefixes and routes)
|
|
97
|
-
# - SimpleModel: The Pydantic model class
|
|
98
|
-
form_renderer = PydanticForm("my_form", SimpleModel)
|
|
99
|
-
|
|
100
|
-
# (Optional) Register list manipulation routes if your model has List fields
|
|
101
|
-
# form_renderer.register_routes(app)
|
|
102
|
-
|
|
103
|
-
# 4. Define routes
|
|
104
|
-
@rt("/")
|
|
105
|
-
def get():
|
|
106
|
-
"""Display the form"""
|
|
107
|
-
return fh.Div(
|
|
108
|
-
mui.Container(
|
|
109
|
-
mui.Card(
|
|
110
|
-
mui.CardHeader("Simple Pydantic Form"),
|
|
111
|
-
mui.CardBody(
|
|
112
|
-
# Use MonsterUI Form component for structure
|
|
113
|
-
mui.Form(
|
|
114
|
-
# Render the inputs using the renderer
|
|
115
|
-
form_renderer.render_inputs(),
|
|
116
|
-
# Add standard form buttons
|
|
117
|
-
mui.Button("Submit", type="submit", cls=mui.ButtonT.primary),
|
|
118
|
-
# HTMX attributes for form submission
|
|
119
|
-
hx_post="/submit_form",
|
|
120
|
-
hx_target="#result", # Target div for response
|
|
121
|
-
hx_swap="innerHTML",
|
|
122
|
-
# Set a unique ID for the form itself for refresh/reset inclusion
|
|
123
|
-
id=f"{form_renderer.name}-form",
|
|
124
|
-
)
|
|
125
|
-
),
|
|
126
|
-
),
|
|
127
|
-
# Div to display validation results
|
|
128
|
-
fh.Div(id="result"),
|
|
129
|
-
),
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
@rt("/submit_form")
|
|
133
|
-
async def post_submit_form(req):
|
|
134
|
-
"""Handle form submission and validation"""
|
|
135
|
-
try:
|
|
136
|
-
# 5. Validate the request data against the model
|
|
137
|
-
validated_data: SimpleModel = await form_renderer.model_validate_request(req)
|
|
138
|
-
|
|
139
|
-
# Success: Display the validated data
|
|
140
|
-
return mui.Card(
|
|
141
|
-
mui.CardHeader(fh.H3("Validation Successful")),
|
|
142
|
-
mui.CardBody(
|
|
143
|
-
fh.Pre(
|
|
144
|
-
validated_data.model_dump_json(indent=2),
|
|
145
|
-
)
|
|
146
|
-
),
|
|
147
|
-
cls="mt-4",
|
|
148
|
-
)
|
|
149
|
-
except ValidationError as e:
|
|
150
|
-
# Validation Error: Display the errors
|
|
151
|
-
return mui.Card(
|
|
152
|
-
mui.CardHeader(fh.H3("Validation Error", cls="text-red-500")),
|
|
153
|
-
mui.CardBody(
|
|
154
|
-
fh.Pre(
|
|
155
|
-
e.json(indent=2),
|
|
156
|
-
)
|
|
157
|
-
),
|
|
158
|
-
cls="mt-4",
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
if __name__ == "__main__":
|
|
162
|
-
fh.serve()
|
|
163
|
-
|
|
164
|
-
```
|
|
165
|
-
## Key Features
|
|
166
|
-
|
|
167
|
-
- **Automatic Field Rendering:** Handles `str`, `int`, `float`, `bool`, `date`, `time`, `Optional`, `Literal`, nested `BaseModel`s, and `List`s out-of-the-box.
|
|
168
|
-
- **Sensible Defaults:** Uses appropriate HTML5 input types (`text`, `number`, `date`, `time`, `checkbox`, `select`).
|
|
169
|
-
- **Labels & Placeholders:** Generates labels from field names (converting snake_case to Title Case) and basic placeholders.
|
|
170
|
-
- **Descriptions as Tooltips:** Uses `Field(description=...)` from Pydantic to create tooltips (`uk-tooltip` via UIkit).
|
|
171
|
-
- **Required Fields:** Automatically adds the `required` attribute based on field definitions (considering `Optional` and defaults).
|
|
172
|
-
- **Disabled Fields:** Disable the whole form with `disabled=True` or disable specific fields with `disabled_fields`
|
|
173
|
-
- **Collapsible Nested Models:** Renders nested Pydantic models in collapsible details/summary elements for better form organization and space management.
|
|
174
|
-
- **List Manipulation:**
|
|
175
|
-
- Renders lists of simple types or models in accordion-style cards with an enhanced UI.
|
|
176
|
-
- Provides HTMX endpoints (registered via `register_routes`) for adding and deleting list items.
|
|
177
|
-
- Includes JavaScript (`list_manipulation_js()`) for client-side reordering (moving items up/down).
|
|
178
|
-
- **Form Refresh & Reset:**
|
|
179
|
-
- Provides HTMX-powered "Refresh" and "Reset" buttons (`form_renderer.refresh_button()`, `form_renderer.reset_button()`).
|
|
180
|
-
- Refresh updates list item summaries or other dynamic parts without full page reload.
|
|
181
|
-
- Reset reverts the form to its initial values.
|
|
182
|
-
- **Custom Renderers:** Register your own `BaseFieldRenderer` subclasses for specific Pydantic types or complex field logic using `FieldRendererRegistry` or by passing `custom_renderers` during `PydanticForm` initialization.
|
|
183
|
-
- **Form Data Parsing:** Includes logic (`form_renderer.parse` and `form_renderer.model_validate_request`) to correctly parse submitted form data (handling prefixes, list indices, nested structures, boolean checkboxes, etc.) back into a dictionary suitable for Pydantic validation.
|
|
184
|
-
|
|
185
|
-
## disabled fields
|
|
186
|
-
|
|
187
|
-
You can disable the full form with `PydanticForm("my_form", FormModel, disabled=True)` or disable specific fields with `PydanticForm("my_form", FormModel, disabled_fields=["field1", "field3"])`.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
## Manipulating lists fields
|
|
191
|
-
|
|
192
|
-
When you have `BaseModels` with fields that are e.g. `List[str]` or even `List[BaseModel]` you want to be able to easily edit the list by adding, deleting and moving items. For this we need a little bit of javascript and register some additional routes:
|
|
193
|
-
|
|
194
|
-
```python
|
|
195
|
-
from fh_pydantic_form import PydanticForm, list_manipulation_js
|
|
196
|
-
|
|
197
|
-
app, rt = fh.fast_app(
|
|
198
|
-
hdrs=[
|
|
199
|
-
mui.Theme.blue.headers(),
|
|
200
|
-
list_manipulation_js(),
|
|
201
|
-
],
|
|
202
|
-
pico=False,
|
|
203
|
-
live=True,
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
class ListModel(BaseModel):
|
|
208
|
-
name: str = ""
|
|
209
|
-
tags: List[str] = Field(["tag1", "tag2"])
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
form_renderer = PydanticForm("list_model", ListModel)
|
|
213
|
-
form_renderer.register_routes(app)
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
## Refreshing and resetting the form
|
|
217
|
-
|
|
218
|
-
You can set the initial values of the form by passing an instantiated BaseModel:
|
|
219
|
-
|
|
220
|
-
```python
|
|
221
|
-
form_renderer = PydanticForm("my_form", ListModel, initial_values=ListModel(name="John", tags=["happy", "joy"]))
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
You can reset the form back to these initial values by adding a `form_render.reset_button()` to your UI:
|
|
225
|
-
|
|
226
|
-
```python
|
|
227
|
-
mui.Form(
|
|
228
|
-
form_renderer.render_inputs(),
|
|
229
|
-
fh.Div(
|
|
230
|
-
mui.Button("Validate and Show JSON",cls=mui.ButtonT.primary,),
|
|
231
|
-
form_renderer.refresh_button(),
|
|
232
|
-
form_renderer.reset_button(),
|
|
233
|
-
),
|
|
234
|
-
hx_post="/submit_form",
|
|
235
|
-
hx_target="#result",
|
|
236
|
-
hx_swap="innerHTML",
|
|
237
|
-
)
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
The refresh button 🔄 refreshes the list item labels. These are rendered initially to summarize the underlying item, but do not automatically update after editing unless refreshed. You can also use the 🔄 icon next to the list field label.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
## Custom renderers
|
|
244
|
-
|
|
245
|
-
The library is extensible by adding your own input renderers for your types. This can be used to override e.g. the default BaseModelFieldRenderer for nested BaseModels, but also to register types that are not (yet) supported (but submit a PR then as well!)
|
|
246
|
-
|
|
247
|
-
You can register a renderer based on type, type str, or a predicate function:
|
|
248
|
-
|
|
249
|
-
```python
|
|
250
|
-
from fh_pydantic_form import FieldRendererRegistry
|
|
251
|
-
|
|
252
|
-
from fh_pydantic_form.field_renderers import BaseFieldRenderer
|
|
253
|
-
|
|
254
|
-
class CustomDetail(BaseModel):
|
|
255
|
-
value: str = "Default value"
|
|
256
|
-
confidence: Literal["HIGH", "MEDIUM", "LOW"] = "MEDIUM"
|
|
257
|
-
|
|
258
|
-
def __str__(self) -> str:
|
|
259
|
-
return f"{self.value} ({self.confidence})"
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
class CustomDetailFieldRenderer(BaseFieldRenderer):
|
|
263
|
-
"""display value input and dropdown side by side"""
|
|
264
|
-
|
|
265
|
-
def render_input(self):
|
|
266
|
-
value_input = fh.Div(
|
|
267
|
-
mui.Input(
|
|
268
|
-
value=self.value.get("value", ""),
|
|
269
|
-
id=f"{self.field_name}_value",
|
|
270
|
-
name=f"{self.field_name}_value",
|
|
271
|
-
placeholder=f"Enter {self.original_field_name.replace('_', ' ')} value",
|
|
272
|
-
cls="uk-input w-full",
|
|
273
|
-
),
|
|
274
|
-
cls="flex-grow", # apply some custom css
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
confidence_options_ft = [
|
|
278
|
-
fh.Option(
|
|
279
|
-
opt, value=opt, selected=(opt == self.value.get("confidence", "MEDIUM"))
|
|
280
|
-
)
|
|
281
|
-
for opt in ["HIGH", "MEDIUM", "LOW"]
|
|
282
|
-
]
|
|
283
|
-
|
|
284
|
-
confidence_select = mui.Select(
|
|
285
|
-
*confidence_options_ft,
|
|
286
|
-
id=f"{self.field_name}_confidence",
|
|
287
|
-
name=f"{self.field_name}_confidence",
|
|
288
|
-
cls_wrapper="w-[110px] min-w-[110px] flex-shrink-0", # apply some custom css
|
|
289
|
-
)
|
|
290
|
-
|
|
291
|
-
return fh.Div(
|
|
292
|
-
value_input,
|
|
293
|
-
confidence_select,
|
|
294
|
-
cls="flex items-start gap-2 w-full", # apply some custom css
|
|
295
|
-
)
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
# these are all equivalent. You can either register the type directly
|
|
299
|
-
FieldRendererRegistry.register_type_renderer(CustomDetail, CustomDetailFieldRender)
|
|
300
|
-
# or just by the name of the type
|
|
301
|
-
FieldRendererRegistry.register_type_name_renderer("CustomDetail", CustomDetailFieldRender)
|
|
302
|
-
# or register I predicate function
|
|
303
|
-
FieldRendererRegistry.register_type_renderer_with_predicate(lambda: x: isinstance(x, CustomDetail), CustomDetailFieldRender)
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
You can also pass these directly to the `PydanticForm` with the custom_renderers argument:
|
|
307
|
-
|
|
308
|
-
```python
|
|
309
|
-
|
|
310
|
-
form_renderer = PydanticForm(
|
|
311
|
-
form_name="main_form",
|
|
312
|
-
model_class=ComplexSchema,
|
|
313
|
-
initial_values=initial_values,
|
|
314
|
-
custom_renderers=[
|
|
315
|
-
(CustomDetail, CustomDetailFieldRenderer)
|
|
316
|
-
], # Register Detail renderer
|
|
317
|
-
)
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
## Contributing
|
|
321
|
-
|
|
322
|
-
Contributions are welcome! Please feel free to open an issue or submit a pull request.
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
fh_pydantic_form/__init__.py,sha256=oifnGdBOS1XaO9Q8gcZ1SJ98Xzz9TfbZH9kvHg9_beo,3056
|
|
2
|
-
fh_pydantic_form/field_renderers.py,sha256=zgGEgR09Oop_-lyYvcHw52L7Uz4X26dWcxyFqUCBSN0,39461
|
|
3
|
-
fh_pydantic_form/form_parser.py,sha256=3EGy4YHbLskH76V0ieTG8zmQ0b02ty8bOZf9AEg89ZY,20629
|
|
4
|
-
fh_pydantic_form/form_renderer.py,sha256=gv172TwuihNBIgUD7VBCPo2tJ3FuAAlHZkzXzQQyT3w,28990
|
|
5
|
-
fh_pydantic_form/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
fh_pydantic_form/registry.py,sha256=sufK-85ST3rc3Vu0XmjjjdTqTAqgHr_ZbMGU0xRgTK8,4996
|
|
7
|
-
fh_pydantic_form/type_helpers.py,sha256=ZCU8m4xxFk_ofMsAwxb8CunZhsDBsrEFjpJdtncreT0,1322
|
|
8
|
-
fh_pydantic_form-0.1.3.dist-info/METADATA,sha256=7xzIYoU9WIg-29glyEx4f7fkO2kG365zU6Ow810abP8,12555
|
|
9
|
-
fh_pydantic_form-0.1.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
10
|
-
fh_pydantic_form-0.1.3.dist-info/licenses/LICENSE,sha256=AOi2eNK3D2aDycRHfPRiuACZ7WPBsKHTV2tTYNl7cls,577
|
|
11
|
-
fh_pydantic_form-0.1.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|