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.

@@ -0,0 +1,685 @@
1
+ Metadata-Version: 2.4
2
+ Name: fh-pydantic-form
3
+ Version: 0.2.0
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
+ [![PyPI](https://img.shields.io/pypi/v/fh-pydantic-form)](https://pypi.org/project/fh-pydantic-form/)
29
+ [![GitHub](https://img.shields.io/github/stars/Marcura/fh-pydantic-form?style=social)](https://github.com/Marcura/fh-pydantic-form)
30
+
31
+ **Generate HTML forms from Pydantic models for your FastHTML applications.**
32
+
33
+ `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.
34
+
35
+ <img width="1405" alt="image" src="https://github.com/user-attachments/assets/d65d9d68-1635-4ea4-83f8-70c4b6b79796" />
36
+
37
+
38
+ <details >
39
+ <summary>show demo screen recording</summary>
40
+ <video src="https://private-user-images.githubusercontent.com/27999937/436237879-feabf388-22af-43e6-b054-f103b8a1b6e6.mp4" controls="controls" style="max-width: 730px;">
41
+ </video>
42
+ </details>
43
+
44
+ ## Table of Contents
45
+ 1. [Purpose](#purpose)
46
+ 2. [Installation](#installation)
47
+ 3. [Quick Start](#quick-start)
48
+ 4. [Key Features](#key-features)
49
+ 5. [Spacing & Styling](#spacing--styling)
50
+ 6. [Working with Lists](#working-with-lists)
51
+ 7. [Nested Models](#nested-models)
52
+ 8. [Literal & Enum Fields](#literal--enum-fields)
53
+ 9. [Initial Values & Enum Parsing](#initial-values--enum-parsing)
54
+ 10. [Disabling & Excluding Fields](#disabling--excluding-fields)
55
+ 11. [Refreshing & Resetting](#refreshing--resetting)
56
+ 12. [Label Colors](#label-colors)
57
+ 13. [Schema Drift Resilience](#schema-drift-resilience)
58
+ 14. [Custom Renderers](#custom-renderers)
59
+ 15. [API Reference](#api-reference)
60
+ 16. [Contributing](#contributing)
61
+
62
+ ## Purpose
63
+
64
+ - **Reduce Boilerplate:** Automatically render form inputs (text, number, checkbox, select, date, time, etc.) based on Pydantic field types and annotations.
65
+ - **Data Validation:** Leverage Pydantic's validation rules directly from form submissions.
66
+ - **Nested Structures:** Support for nested Pydantic models and lists of models/simple types with accordion UI.
67
+ - **Dynamic Lists:** Built-in HTMX endpoints and JavaScript for adding, deleting, and reordering items in lists within the form.
68
+ - **Customization:** Easily register custom renderers for specific Pydantic types or fields.
69
+ - **Robust Schema Handling:** Gracefully handles model changes and missing fields in initial data.
70
+
71
+ ## Installation
72
+
73
+ You can install `fh-pydantic-form` using either `pip` or `uv`.
74
+
75
+ **Using pip:**
76
+
77
+ ```bash
78
+ pip install fh-pydantic-form
79
+ ```
80
+
81
+ Using uv:
82
+ ```bash
83
+ uv add fh-pydantic-form
84
+ ```
85
+
86
+ This will also install necessary dependencies like `pydantic`, `python-fasthtml`, and `monsterui`.
87
+
88
+ ## Quick Start
89
+
90
+ ```python
91
+ # examples/simple_example.py
92
+ import fasthtml.common as fh
93
+ import monsterui.all as mui
94
+ from pydantic import BaseModel, ValidationError
95
+
96
+ # 1. Import the form renderer
97
+ from fh_pydantic_form import PydanticForm
98
+
99
+ app, rt = fh.fast_app(
100
+ hdrs=[
101
+ mui.Theme.blue.headers(),
102
+ # Add list_manipulation_js() if using list fields
103
+ # from fh_pydantic_form import list_manipulation_js
104
+ # list_manipulation_js(),
105
+ ],
106
+ pico=False, # Using MonsterUI, not PicoCSS
107
+ live=True, # Enable live reload for development
108
+ )
109
+
110
+ # 2. Define your Pydantic model
111
+ class SimpleModel(BaseModel):
112
+ """Model representing a simple form"""
113
+ name: str = "Default Name"
114
+ age: int
115
+ is_active: bool = True
116
+
117
+ # 3. Create a form renderer instance
118
+ # - 'my_form': Unique name for the form (used for prefixes and routes)
119
+ # - SimpleModel: The Pydantic model class
120
+ form_renderer = PydanticForm("my_form", SimpleModel)
121
+
122
+ # (Optional) Register list manipulation routes if your model has List fields
123
+ # form_renderer.register_routes(app)
124
+
125
+ # 4. Define routes
126
+ @rt("/")
127
+ def get():
128
+ """Display the form"""
129
+ return fh.Div(
130
+ mui.Container(
131
+ mui.Card(
132
+ mui.CardHeader("Simple Pydantic Form"),
133
+ mui.CardBody(
134
+ # Use MonsterUI Form component for structure
135
+ mui.Form(
136
+ # Render the inputs using the renderer
137
+ form_renderer.render_inputs(),
138
+ # Add standard form buttons
139
+ fh.Div(
140
+ mui.Button("Submit", type="submit", cls=mui.ButtonT.primary),
141
+ form_renderer.refresh_button("🔄"),
142
+ form_renderer.reset_button("↩️"),
143
+ cls="mt-4 flex items-center gap-2",
144
+ ),
145
+ # HTMX attributes for form submission
146
+ hx_post="/submit_form",
147
+ hx_target="#result", # Target div for response
148
+ hx_swap="innerHTML",
149
+ # Set a unique ID for the form itself for refresh/reset inclusion
150
+ id=f"{form_renderer.name}-form",
151
+ )
152
+ ),
153
+ ),
154
+ # Div to display validation results
155
+ fh.Div(id="result"),
156
+ ),
157
+ )
158
+
159
+ @rt("/submit_form")
160
+ async def post_submit_form(req):
161
+ """Handle form submission and validation"""
162
+ try:
163
+ # 5. Validate the request data against the model
164
+ validated_data: SimpleModel = await form_renderer.model_validate_request(req)
165
+
166
+ # Success: Display the validated data
167
+ return mui.Card(
168
+ mui.CardHeader(fh.H3("Validation Successful")),
169
+ mui.CardBody(
170
+ fh.Pre(
171
+ validated_data.model_dump_json(indent=2),
172
+ )
173
+ ),
174
+ cls="mt-4",
175
+ )
176
+ except ValidationError as e:
177
+ # Validation Error: Display the errors
178
+ return mui.Card(
179
+ mui.CardHeader(fh.H3("Validation Error", cls="text-red-500")),
180
+ mui.CardBody(
181
+ fh.Pre(
182
+ e.json(indent=2),
183
+ )
184
+ ),
185
+ cls="mt-4",
186
+ )
187
+
188
+ if __name__ == "__main__":
189
+ fh.serve()
190
+ ```
191
+
192
+ ## Key Features
193
+
194
+ - **Automatic Field Rendering:** Handles `str`, `int`, `float`, `bool`, `date`, `time`, `Optional`, `Literal`, nested `BaseModel`s, and `List`s out-of-the-box.
195
+ - **Sensible Defaults:** Uses appropriate HTML5 input types (`text`, `number`, `date`, `time`, `checkbox`, `select`).
196
+ - **Labels & Placeholders:** Generates labels from field names (converting snake_case to Title Case) and basic placeholders.
197
+ - **Descriptions as Tooltips:** Uses `Field(description=...)` from Pydantic to create tooltips (`uk-tooltip` via UIkit).
198
+ - **Required Fields:** Automatically adds the `required` attribute based on field definitions (considering `Optional` and defaults).
199
+ - **Disabled Fields:** Disable the whole form with `disabled=True` or disable specific fields with `disabled_fields`
200
+ - **Collapsible Nested Models:** Renders nested Pydantic models in accordion-style components for better form organization and space management.
201
+ - **List Manipulation:**
202
+ - Renders lists of simple types or models in accordion-style cards with an enhanced UI.
203
+ - Provides HTMX endpoints (registered via `register_routes`) for adding and deleting list items.
204
+ - Includes JavaScript (`list_manipulation_js()`) for client-side reordering (moving items up/down).
205
+ - Click list field labels to toggle all items open/closed.
206
+ - **Form Refresh & Reset:**
207
+ - Provides HTMX-powered "Refresh" and "Reset" buttons (`form_renderer.refresh_button()`, `form_renderer.reset_button()`).
208
+ - Refresh updates list item summaries or other dynamic parts without full page reload.
209
+ - Reset reverts the form to its initial values.
210
+ - **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.
211
+ - **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.
212
+
213
+ ## Spacing & Styling
214
+
215
+ `fh-pydantic-form` ships with two spacing presets to fit different UI requirements:
216
+
217
+ | Theme | Purpose | Usage |
218
+ |-------|---------|-------|
219
+ | **normal** (default) | Comfortable margins & borders – great for desktop forms | `PydanticForm(..., spacing="normal")` |
220
+ | **compact** | Ultra-dense UIs, mobile layouts, or forms with many fields | `PydanticForm(..., spacing="compact")` |
221
+
222
+ ```python
223
+ # Example: side-by-side normal vs compact forms
224
+ form_normal = PydanticForm("normal_form", MyModel, spacing="normal")
225
+ form_compact = PydanticForm("compact_form", MyModel, spacing="compact")
226
+ ```
227
+
228
+ **Compact mode** automatically injects additional CSS (`COMPACT_EXTRA_CSS`) to minimize margins, borders, and padding throughout the form. You can also import and use this CSS independently:
229
+
230
+ ```python
231
+ from fh_pydantic_form import COMPACT_EXTRA_CSS
232
+
233
+ app, rt = fh.fast_app(
234
+ hdrs=[
235
+ mui.Theme.blue.headers(),
236
+ COMPACT_EXTRA_CSS, # Apply compact styling globally
237
+ ],
238
+ # ...
239
+ )
240
+ ```
241
+
242
+ ## Working with Lists
243
+
244
+ When your Pydantic models contain `List[str]`, `List[int]`, or `List[BaseModel]` fields, `fh-pydantic-form` provides rich list manipulation capabilities:
245
+
246
+ ### Basic Setup
247
+
248
+ ```python
249
+ from fh_pydantic_form import PydanticForm, list_manipulation_js
250
+ from typing import List
251
+
252
+ app, rt = fh.fast_app(
253
+ hdrs=[
254
+ mui.Theme.blue.headers(),
255
+ list_manipulation_js(), # Required for list manipulation
256
+ ],
257
+ pico=False,
258
+ live=True,
259
+ )
260
+
261
+ class ListModel(BaseModel):
262
+ name: str = ""
263
+ tags: List[str] = Field(["tag1", "tag2"])
264
+ addresses: List[Address] = Field(default_factory=list)
265
+
266
+ form_renderer = PydanticForm("list_model", ListModel)
267
+ form_renderer.register_routes(app) # Register HTMX endpoints
268
+ ```
269
+
270
+ ### List Features
271
+
272
+ - **Add Items:** Each list has an "Add Item" button that creates new entries
273
+ - **Delete Items:** Each list item has a delete button with confirmation
274
+ - **Reorder Items:** Move items up/down with arrow buttons
275
+ - **Toggle All:** Click the list field label to expand/collapse all items at once
276
+ - **Refresh Display:** Use the 🔄 icon next to list labels to update item summaries
277
+ - **Smart Defaults:** New items are created with sensible default values
278
+
279
+ The list manipulation uses HTMX for seamless updates without page reloads, and includes JavaScript for client-side reordering.
280
+
281
+ ## Nested Models
282
+
283
+ Nested Pydantic models are automatically rendered in collapsible accordion components:
284
+
285
+ ```python
286
+ class Address(BaseModel):
287
+ street: str = "123 Main St"
288
+ city: str = "Anytown"
289
+ is_billing: bool = False
290
+
291
+ class User(BaseModel):
292
+ name: str
293
+ address: Address # Rendered as collapsible accordion
294
+ backup_addresses: List[Address] # List of accordions
295
+ ```
296
+
297
+ **Key behaviors:**
298
+ - Nested models inherit `disabled` and `spacing` settings from the parent form
299
+ - Field prefixes are automatically managed (e.g., `user_address_street`)
300
+ - Accordions are open by default for better user experience
301
+ - Schema drift is handled gracefully - missing fields use defaults, unknown fields are ignored
302
+
303
+ ## Literal & Enum Fields
304
+
305
+ `fh-pydantic-form` provides comprehensive support for choice-based fields through `Literal`, `Enum`, and `IntEnum` types, all automatically rendered as dropdown selects:
306
+
307
+ ### Literal Fields
308
+
309
+ ```python
310
+ from typing import Literal, Optional
311
+
312
+ class OrderModel(BaseModel):
313
+ # Required Literal field - only defined choices available
314
+ shipping_method: Literal["STANDARD", "EXPRESS", "OVERNIGHT"] = "STANDARD"
315
+
316
+ # Optional Literal field - includes "-- None --" option
317
+ category: Optional[Literal["ELECTRONICS", "CLOTHING", "BOOKS", "OTHER"]] = None
318
+ ```
319
+
320
+ ### Enum Fields
321
+
322
+ ```python
323
+ from enum import Enum, IntEnum
324
+
325
+ class OrderStatus(Enum):
326
+ """Order status enum with string values."""
327
+ PENDING = "pending"
328
+ CONFIRMED = "confirmed"
329
+ SHIPPED = "shipped"
330
+ DELIVERED = "delivered"
331
+ CANCELLED = "cancelled"
332
+
333
+ class Priority(IntEnum):
334
+ """Priority levels using IntEnum for numeric ordering."""
335
+ LOW = 1
336
+ MEDIUM = 2
337
+ HIGH = 3
338
+ URGENT = 4
339
+
340
+ class OrderModel(BaseModel):
341
+ # Required Enum field with default
342
+ status: OrderStatus = OrderStatus.PENDING
343
+
344
+ # Optional Enum field without default
345
+ payment_method: Optional[PaymentMethod] = None
346
+
347
+ # Required IntEnum field with default
348
+ priority: Priority = Priority.MEDIUM
349
+
350
+ # Optional IntEnum field without default
351
+ urgency_level: Optional[Priority] = Field(
352
+ None, description="Override priority for urgent orders"
353
+ )
354
+
355
+ # Enum field without default (required)
356
+ fulfillment_status: OrderStatus = Field(
357
+ ..., description="Current fulfillment status"
358
+ )
359
+ ```
360
+
361
+ ### Field Rendering Behavior
362
+
363
+ | Field Type | Required | Optional | Notes |
364
+ |------------|----------|----------|-------|
365
+ | **Literal** | Shows only defined choices | Includes "-- None --" option | String values displayed as-is |
366
+ | **Enum** | Shows enum member values | Includes "-- None --" option | Displays `enum.value` in dropdown |
367
+ | **IntEnum** | Shows integer values | Includes "-- None --" option | Maintains numeric ordering |
368
+
369
+ **Key features:**
370
+ - **Automatic dropdown generation** for all choice-based field types
371
+ - **Proper value handling** - enum values are correctly parsed during form submission
372
+ - **Optional field support** - includes None option when fields are Optional
373
+ - **Field descriptions** become tooltips on hover
374
+ - **Default value selection** - dropdowns pre-select the appropriate default value
375
+
376
+ ## Initial Values & Enum Parsing
377
+
378
+ `fh-pydantic-form` intelligently parses initial values from dictionaries, properly converting strings and integers to their corresponding enum types:
379
+
380
+ ### Setting Initial Values
381
+
382
+ ```python
383
+ # Example initial values from a dictionary
384
+ initial_values_dict = {
385
+ "shipping_method": "EXPRESS", # Literal value as string
386
+ "category": "ELECTRONICS", # Optional Literal value
387
+ "status": "shipped", # Enum value (parsed to OrderStatus.SHIPPED)
388
+ "payment_method": "paypal", # Optional Enum (parsed to PaymentMethod.PAYPAL)
389
+ "priority": 3, # IntEnum as integer (parsed to Priority.HIGH)
390
+ "urgency_level": 4, # Optional IntEnum as integer (parsed to Priority.URGENT)
391
+ "fulfillment_status": "confirmed" # Required Enum (parsed to OrderStatus.CONFIRMED)
392
+ }
393
+
394
+ # Create form with initial values
395
+ form_renderer = PydanticForm("order_form", OrderModel, initial_values=initial_values_dict)
396
+ ```
397
+
398
+ ### Parsing Behavior
399
+
400
+ The form automatically handles conversion between different value formats:
401
+
402
+ | Input Type | Target Type | Example | Result |
403
+ |------------|-------------|---------|--------|
404
+ | String | Enum | `"shipped"` | `OrderStatus.SHIPPED` |
405
+ | String | Optional[Enum] | `"paypal"` | `PaymentMethod.PAYPAL` |
406
+ | Integer | IntEnum | `3` | `Priority.HIGH` |
407
+ | Integer | Optional[IntEnum] | `4` | `Priority.URGENT` |
408
+ | String | Literal | `"EXPRESS"` | `"EXPRESS"` (unchanged) |
409
+
410
+ **Benefits:**
411
+ - **Flexible data sources** - works with database records, API responses, or any dictionary
412
+ - **Type safety** - ensures enum values are valid during parsing
413
+ - **Graceful handling** - invalid enum values are passed through for Pydantic validation
414
+ - **Consistent behavior** - same parsing logic for required and optional fields
415
+
416
+ ### Example Usage
417
+
418
+ ```python
419
+ @rt("/")
420
+ def get():
421
+ return mui.Form(
422
+ form_renderer.render_inputs(), # Pre-populated with parsed enum values
423
+ fh.Div(
424
+ mui.Button("Submit", type="submit", cls=mui.ButtonT.primary),
425
+ form_renderer.refresh_button("🔄"),
426
+ form_renderer.reset_button("↩️"), # Resets to initial parsed values
427
+ cls="mt-4 flex items-center gap-2",
428
+ ),
429
+ hx_post="/submit_order",
430
+ hx_target="#result",
431
+ id=f"{form_renderer.name}-form",
432
+ )
433
+
434
+ @rt("/submit_order")
435
+ async def post_submit_order(req):
436
+ try:
437
+ # Validates and converts form data back to proper enum types
438
+ validated_order: OrderModel = await form_renderer.model_validate_request(req)
439
+
440
+ # Access enum properties
441
+ print(f"Status: {validated_order.status.value} ({validated_order.status.name})")
442
+ print(f"Priority: {validated_order.priority.value} ({validated_order.priority.name})")
443
+
444
+ return success_response(validated_order)
445
+ except ValidationError as e:
446
+ return error_response(e)
447
+ ```
448
+
449
+ This makes it easy to work with enum-based forms when loading data from databases, APIs, or configuration files.
450
+
451
+ ## Disabling & Excluding Fields
452
+
453
+ ### Disabling Fields
454
+
455
+ You can disable the entire form or specific fields:
456
+
457
+ ```python
458
+ # Disable all fields
459
+ form_renderer = PydanticForm("my_form", FormModel, disabled=True)
460
+
461
+ # Disable specific fields only
462
+ form_renderer = PydanticForm(
463
+ "my_form",
464
+ FormModel,
465
+ disabled_fields=["field1", "field3"]
466
+ )
467
+ ```
468
+
469
+ ### Excluding Fields
470
+
471
+ Exclude specific fields from being rendered in the form:
472
+
473
+ ```python
474
+ form_renderer = PydanticForm(
475
+ "my_form",
476
+ FormModel,
477
+ exclude_fields=["internal_field", "computed_field"]
478
+ )
479
+ ```
480
+
481
+ **Important:** When fields are excluded from the UI, `fh-pydantic-form` automatically injects their default values during form parsing and validation. This ensures:
482
+
483
+ - **Hidden fields with defaults** are still included in the final validated data
484
+ - **Required fields without defaults** will still cause validation errors if not provided elsewhere
485
+ - **Default factories** are executed to provide computed default values
486
+ - **Nested BaseModel defaults** are converted to dictionaries for consistency
487
+
488
+ This automatic default injection means you can safely exclude fields that shouldn't be user-editable while maintaining data integrity.
489
+
490
+ ## Refreshing & Resetting
491
+
492
+ Forms support dynamic refresh and reset functionality:
493
+
494
+ ```python
495
+ mui.Form(
496
+ form_renderer.render_inputs(),
497
+ fh.Div(
498
+ mui.Button("Submit", type="submit", cls=mui.ButtonT.primary),
499
+ form_renderer.refresh_button("🔄 Refresh"), # Update display
500
+ form_renderer.reset_button("↩️ Reset"), # Restore initial values
501
+ cls="mt-4 flex items-center gap-2",
502
+ ),
503
+ # ... rest of form setup
504
+ )
505
+ ```
506
+
507
+ - **Refresh button** updates the form display based on current values (useful for updating list item summaries)
508
+ - **Reset button** restores all fields to their initial values with confirmation
509
+ - Both use HTMX for seamless updates without page reloads
510
+
511
+
512
+ ## Label Colors
513
+
514
+ Customize the appearance of field labels with the `label_colors` parameter:
515
+
516
+ ```python
517
+ form_renderer = PydanticForm(
518
+ "my_form",
519
+ MyModel,
520
+ label_colors={
521
+ "name": "text-blue-600", # Tailwind CSS class
522
+ "score": "#E12D39", # Hex color value
523
+ "status": "text-green-500", # Another Tailwind class
524
+ },
525
+ )
526
+ ```
527
+
528
+ **Supported formats:**
529
+ - **Tailwind CSS classes:** `"text-blue-600"`, `"text-red-500"`, etc.
530
+ - **Hex color values:** `"#FF0000"`, `"#0066CC"`, etc.
531
+ - **CSS color names:** `"red"`, `"blue"`, `"darkgreen"`, etc.
532
+
533
+ This can be useful for e.g. highlighting the values of different fields in a pdf with different highlighting colors matching the form input label color.
534
+
535
+
536
+ ## Setting Initial Values
537
+
538
+ You can set initial form values of the form by passing a model instance or dictionary:
539
+
540
+ ```python
541
+ initial_data = MyModel(name="John", tags=["happy", "joy"])
542
+ form_renderer = PydanticForm("my_form", MyModel, initial_values=initial_data)
543
+
544
+
545
+ initial_data_dict = {"name": "John"}
546
+ form_renderer = PydanticForm("my_form", MyModel, initial_values=initial_values_dict)
547
+ ```
548
+
549
+ The dictionary does not have to be complete, and we try to handle schema drift gracefully. If you exclude fields from the form, we fill those fields with the initial_values or the default values.
550
+
551
+
552
+
553
+ ### Schema Drift Resilience
554
+
555
+ `fh-pydantic-form` gracefully handles model evolution and schema changes:
556
+
557
+ Initial values can come from **older or newer** versions of your model – unknown fields are ignored gracefully and missing fields use defaults.
558
+
559
+ ```python
560
+ # Your model evolves over time
561
+ class UserModel(BaseModel):
562
+ name: str
563
+ email: str # Added in v2
564
+ phone: Optional[str] # Added in v3
565
+
566
+ # Old data still works
567
+ old_data = {"name": "John"} # Missing newer fields
568
+ form = PydanticForm("user", UserModel, initial_values=old_data)
569
+
570
+ # Newer data works too
571
+ new_data = {"name": "Jane", "email": "jane@example.com", "phone": "555-1234", "removed_field": "ignored"}
572
+ form = PydanticForm("user", UserModel, initial_values=new_data)
573
+ ```
574
+
575
+ **Benefits:**
576
+ - **Backward compatibility:** Old data structures continue to work
577
+ - **Forward compatibility:** Unknown fields are silently ignored
578
+ - **Graceful degradation:** Missing fields fall back to model defaults
579
+ - **Production stability:** No crashes during rolling deployments
580
+
581
+ ## Custom Renderers
582
+
583
+ The library is extensible through custom field renderers for specialized input types:
584
+
585
+ ```python
586
+ from fh_pydantic_form.field_renderers import BaseFieldRenderer
587
+ from fh_pydantic_form import FieldRendererRegistry
588
+
589
+ class CustomDetail(BaseModel):
590
+ value: str = "Default value"
591
+ confidence: Literal["HIGH", "MEDIUM", "LOW"] = "MEDIUM"
592
+
593
+ def __str__(self) -> str:
594
+ return f"{self.value} ({self.confidence})"
595
+
596
+ class CustomDetailFieldRenderer(BaseFieldRenderer):
597
+ """Display value input and dropdown side by side"""
598
+
599
+ def render_input(self):
600
+ value_input = fh.Div(
601
+ mui.Input(
602
+ value=self.value.get("value", ""),
603
+ id=f"{self.field_name}_value",
604
+ name=f"{self.field_name}_value",
605
+ placeholder=f"Enter {self.original_field_name.replace('_', ' ')} value",
606
+ cls="uk-input w-full",
607
+ ),
608
+ cls="flex-grow",
609
+ )
610
+
611
+ confidence_options = [
612
+ fh.Option(
613
+ opt, value=opt, selected=(opt == self.value.get("confidence", "MEDIUM"))
614
+ )
615
+ for opt in ["HIGH", "MEDIUM", "LOW"]
616
+ ]
617
+
618
+ confidence_select = mui.Select(
619
+ *confidence_options,
620
+ id=f"{self.field_name}_confidence",
621
+ name=f"{self.field_name}_confidence",
622
+ cls_wrapper="w-[110px] min-w-[110px] flex-shrink-0",
623
+ )
624
+
625
+ return fh.Div(
626
+ value_input,
627
+ confidence_select,
628
+ cls="flex items-start gap-2 w-full",
629
+ )
630
+
631
+ # Register the custom renderer (multiple ways)
632
+ FieldRendererRegistry.register_type_renderer(CustomDetail, CustomDetailFieldRenderer)
633
+
634
+ # Or pass directly to PydanticForm
635
+ form_renderer = PydanticForm(
636
+ "my_form",
637
+ MyModel,
638
+ custom_renderers=[(CustomDetail, CustomDetailFieldRenderer)],
639
+ )
640
+ ```
641
+
642
+ ### Registration Methods
643
+
644
+ - **Type-based:** `register_type_renderer(CustomDetail, CustomDetailFieldRenderer)`
645
+ - **Type name:** `register_type_name_renderer("CustomDetail", CustomDetailFieldRenderer)`
646
+ - **Predicate:** `register_type_renderer_with_predicate(lambda field: isinstance(field.annotation, CustomDetail), CustomDetailFieldRenderer)`
647
+
648
+ ## API Reference
649
+
650
+ ### PydanticForm Constructor
651
+
652
+ | Parameter | Type | Default | Description |
653
+ |-----------|------|---------|-------------|
654
+ | `form_name` | `str` | Required | Unique identifier for the form (used for HTMX routes and prefixes) |
655
+ | `model_class` | `Type[BaseModel]` | Required | The Pydantic model class to render |
656
+ | `initial_values` | `Optional[Union[BaseModel, Dict]]` | `None` | Initial form values as model instance or dictionary |
657
+ | `custom_renderers` | `Optional[List[Tuple[Type, Type[BaseFieldRenderer]]]]` | `None` | List of (type, renderer_class) pairs for custom rendering |
658
+ | `disabled` | `bool` | `False` | Whether to disable all form inputs |
659
+ | `disabled_fields` | `Optional[List[str]]` | `None` | List of specific field names to disable |
660
+ | `label_colors` | `Optional[Dict[str, str]]` | `None` | Mapping of field names to CSS colors or Tailwind classes |
661
+ | `exclude_fields` | `Optional[List[str]]` | `None` | List of field names to exclude from rendering (auto-injected on submission) |
662
+ | `spacing` | `SpacingValue` | `"normal"` | Spacing theme: `"normal"`, `"compact"`, or `SpacingTheme` enum |
663
+
664
+ ### Key Methods
665
+
666
+ | Method | Purpose |
667
+ |--------|---------|
668
+ | `render_inputs()` | Generate the HTML form inputs (without `<form>` wrapper) |
669
+ | `refresh_button(text=None, **kwargs)` | Create a refresh button component |
670
+ | `reset_button(text=None, **kwargs)` | Create a reset button component |
671
+ | `register_routes(app)` | Register HTMX endpoints for list manipulation |
672
+ | `parse(form_dict)` | Parse raw form data into model-compatible dictionary |
673
+ | `model_validate_request(req)` | Extract, parse, and validate form data from request |
674
+
675
+ ### Utility Functions
676
+
677
+ | Function | Purpose |
678
+ |----------|---------|
679
+ | `list_manipulation_js()` | JavaScript for list reordering and toggle functionality |
680
+ | `default_dict_for_model(model_class)` | Generate default values for all fields in a model |
681
+ | `default_for_annotation(annotation)` | Get sensible default for a type annotation |
682
+
683
+ ## Contributing
684
+
685
+ Contributions are welcome! Please feel free to open an issue or submit a pull request.
@@ -0,0 +1,13 @@
1
+ fh_pydantic_form/__init__.py,sha256=auqrMQyy6WsEeiMIdXVrjHpSuW_L7CpW2AZ1FOXb8QE,4058
2
+ fh_pydantic_form/defaults.py,sha256=IzBA_soBOdXP_XAUqfFAtniDQaW6N23hiXmWJD2xq0c,5168
3
+ fh_pydantic_form/field_renderers.py,sha256=rPzTvpDpXuy7H4rim6zlogTWNhFrh5mflxRcI4MbF_M,48508
4
+ fh_pydantic_form/form_parser.py,sha256=TcoN7IWilyC0JptigZzdd3ciyEO6ARiqHhiyQ3Ufr_o,23658
5
+ fh_pydantic_form/form_renderer.py,sha256=9ysQLLT6e8a5LbPHszEOUpyryJIYEev-3a3eZxBx6Zs,34892
6
+ fh_pydantic_form/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ fh_pydantic_form/registry.py,sha256=sufK-85ST3rc3Vu0XmjjjdTqTAqgHr_ZbMGU0xRgTK8,4996
8
+ fh_pydantic_form/type_helpers.py,sha256=bWHOxu52yh9_79d_x5L3cfMqnZo856OsbL4sTttDoa4,4367
9
+ fh_pydantic_form/ui_style.py,sha256=L_Z21nJ1YVKcDRMDphRcgHuQ33P4YHxa3oSjMwD55gw,3808
10
+ fh_pydantic_form-0.2.0.dist-info/METADATA,sha256=o7qPV16HL1dTYTgACd3Osr7FQ_L6XCuMq3wq7rZntl0,26566
11
+ fh_pydantic_form-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
12
+ fh_pydantic_form-0.2.0.dist-info/licenses/LICENSE,sha256=AOi2eNK3D2aDycRHfPRiuACZ7WPBsKHTV2tTYNl7cls,577
13
+ fh_pydantic_form-0.2.0.dist-info/RECORD,,