fh-pydantic-form 0.1.2__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.

Potentially problematic release.


This version of fh-pydantic-form might be problematic. Click here for more details.

@@ -0,0 +1,40 @@
1
+ # This workflow will install Python dependencies, run tests and lint with a single version of Python
2
+ # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
3
+
4
+ name: fh_pydantic_form ci runner
5
+
6
+ on:
7
+ push:
8
+ branches: [main]
9
+ pull_request:
10
+ branches: [main]
11
+ schedule:
12
+ - cron: "0 0 * * *"
13
+ workflow_dispatch:
14
+
15
+ jobs:
16
+ uv-example:
17
+ name: python
18
+ runs-on: ubuntu-latest
19
+
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+
23
+ - name: Install uv
24
+ uses: astral-sh/setup-uv@v5
25
+ with:
26
+ enable-cache: true
27
+
28
+ - name: "Set up Python"
29
+ uses: actions/setup-python@v5
30
+ with:
31
+ python-version: "3.12"
32
+
33
+ - name: Install the project
34
+ run: uv sync --all-extras --dev
35
+
36
+ - name: Run pre-commit hooks
37
+ run: uv run pre-commit run -a
38
+
39
+ - name: Run tests
40
+ run: uv run pytest tests
@@ -0,0 +1,64 @@
1
+ name: Publish Package Distribution
2
+
3
+ # Trigger only on release
4
+ on:
5
+ release:
6
+ types: [published]
7
+
8
+ jobs:
9
+ build:
10
+ name: Build distributions
11
+ runs-on: ubuntu-latest
12
+ # Only minimal permissions needed here
13
+ permissions:
14
+ contents: read
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - name: Install uv
19
+ uses: astral-sh/setup-uv@v5
20
+ with:
21
+ enable-cache: true
22
+
23
+ - name: "Set up Python"
24
+ uses: actions/setup-python@v5
25
+ with:
26
+ python-version: "3.12"
27
+
28
+ - name: Install the project
29
+ run: uv sync --all-extras --dev
30
+
31
+ - name: Run pre-commit hooks
32
+ run: uv run pre-commit run -a
33
+
34
+ - name: Run tests
35
+ run: uv run pytest tests
36
+
37
+ - name: Build distributions
38
+ run: uv build --no-sources
39
+
40
+ - name: Upload distributions
41
+ uses: actions/upload-artifact@v4
42
+ with:
43
+ name: python-package-distributions
44
+ path: dist/
45
+
46
+ publish:
47
+ name: Publish to PyPI
48
+ runs-on: ubuntu-latest
49
+ needs: build
50
+ permissions:
51
+ id-token: write # Enable OIDC token issuance [oai_citation_attribution:10‡GitHub Docs](https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-pypi?utm_source=chatgpt.com)
52
+ contents: read
53
+ packages: write
54
+
55
+ steps:
56
+ - name: Download distributions
57
+ uses: actions/download-artifact@v4
58
+ with:
59
+ name: python-package-distributions
60
+ path: dist/
61
+
62
+ - name: Publish to PyPI
63
+ uses: pypa/gh-action-pypi-publish@release/v1
64
+ # No username/password or API token needed: OIDC flows automatically [oai_citation_attribution:9‡GitHub](https://github.com/pypa/gh-action-pypi-publish?utm_source=chatgpt.com)
@@ -0,0 +1,133 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ .pytest_cache/
6
+ .coverage
7
+ htmlcov/
8
+
9
+ # C extensions
10
+ *.so
11
+
12
+ # Distribution / packaging
13
+ .Python
14
+ build/
15
+ develop-eggs/
16
+ dist/
17
+ downloads/
18
+ eggs/
19
+ .eggs/
20
+ lib/
21
+ lib64/
22
+ parts/
23
+ sdist/
24
+ var/
25
+ wheels/
26
+ *.egg-info/
27
+ .installed.cfg
28
+ *.egg
29
+ MANIFEST
30
+
31
+ # PyInstaller
32
+ # Usually these files are written by a python script from a template
33
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
34
+ *.manifest
35
+ *.spec
36
+
37
+ # Installer logs
38
+ pip-log.txt
39
+ pip-delete-this-directory.txt
40
+
41
+ # Unit test / coverage reports
42
+ htmlcov/
43
+ .tox/
44
+ .nox/
45
+ .coverage
46
+ .coverage.*
47
+ .cache
48
+ nosetests.xml
49
+ coverage.xml
50
+ *.cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+
63
+ # Flask stuff:
64
+ instance/
65
+ .webassets-cache
66
+
67
+ # Scrapy stuff:
68
+ .scrapy
69
+
70
+ # Sphinx documentation
71
+ docs/_build/
72
+
73
+ # PyBuilder
74
+ target/
75
+
76
+ # Jupyter Notebook
77
+ .ipynb_checkpoints
78
+
79
+ # IPython
80
+ profile_default/
81
+ ipython_config.py
82
+
83
+ # pyenv
84
+ .python-version
85
+
86
+ # celery beat schedule file
87
+ celerybeat-schedule
88
+
89
+ # SageMath parsed files
90
+ *.sage.py
91
+
92
+ # Environments
93
+ .env
94
+ .venv
95
+ env/
96
+ venv/
97
+ ENV/
98
+ env.bak/
99
+ venv.bak/
100
+
101
+ # Spyder project settings
102
+ .spyderproject
103
+ .spyproject
104
+
105
+ # Rope project settings
106
+ .ropeproject
107
+
108
+ # mkdocs documentation
109
+ /site
110
+
111
+ # mypy
112
+ .mypy_cache/
113
+ .dmypy.json
114
+ dmypy.json
115
+
116
+ # Pyre type checker
117
+ .pyre/
118
+
119
+ # VSCode
120
+ .vscode/
121
+
122
+ # PyCharm
123
+ .idea/
124
+
125
+ # macOS
126
+ .DS_Store
127
+
128
+
129
+ .ruff_cache/
130
+ uv.lock
131
+
132
+
133
+ .sesskey
@@ -0,0 +1,30 @@
1
+ fail_fast: true
2
+ repos:
3
+ - repo: https://github.com/pre-commit/pre-commit-hooks
4
+ rev: v5.0.0
5
+ hooks:
6
+ - id: check-yaml
7
+ exclude: ^(mkdocs\.yml|{{cookiecutter.repo_name}}/mkdocs\.yml)$
8
+ - id: check-case-conflict
9
+ - id: debug-statements
10
+ - id: detect-private-key
11
+ - id: check-merge-conflict
12
+ - id: check-added-large-files
13
+ args: [--maxkb=100000] # 100MB
14
+
15
+ - repo: https://github.com/astral-sh/ruff-pre-commit
16
+ # Ruff version.
17
+ rev: v0.11.6
18
+ hooks:
19
+ # Run the linter
20
+ - id: ruff
21
+ args:
22
+ - --fix
23
+ - id: ruff-format
24
+
25
+
26
+ - repo: https://github.com/pre-commit/mirrors-mypy
27
+ rev: v1.15.0
28
+ hooks:
29
+ - id: mypy
30
+
@@ -0,0 +1,13 @@
1
+ Copyright 2025 Marcura
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
@@ -0,0 +1,327 @@
1
+ Metadata-Version: 2.4
2
+ Name: fh-pydantic-form
3
+ Version: 0.1.2
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
+