fh-pydantic-form 0.3.9__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.
@@ -0,0 +1,1168 @@
1
+ Metadata-Version: 2.4
2
+ Name: fh-pydantic-form
3
+ Version: 0.3.9
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.29
22
+ Requires-Dist: pydantic>=2.0
23
+ Requires-Dist: python-fasthtml>=0.12.29
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. This makes it ideal for annotation workflows for structured outputs: when the schema updates, so does your annotation app.
34
+
35
+ <img width="1072" alt="image" src="https://github.com/user-attachments/assets/9e189266-0118-4de2-a35c-1118f11b0581" />
36
+
37
+
38
+
39
+
40
+ <details >
41
+ <summary>show demo screen recording</summary>
42
+ <video src="https://private-user-images.githubusercontent.com/27999937/462832274-a00096d9-47ee-485b-af0a-e74838ec6c6d.mp4" controls="controls">
43
+ </video>
44
+ </details>
45
+
46
+ ## Table of Contents
47
+ 1. [Purpose](#purpose)
48
+ 2. [Installation](#installation)
49
+ 3. [Quick Start](#quick-start)
50
+ 4. [Key Features](#key-features)
51
+ 5. [Spacing & Styling](#spacing--styling)
52
+ 6. [Working with Lists](#working-with-lists)
53
+ 7. [Nested Models](#nested-models)
54
+ 8. [Literal & Enum Fields](#literal--enum-fields)
55
+ 9. [Initial Values & Enum Parsing](#initial-values--enum-parsing)
56
+ 10. [Disabling & Excluding Fields](#disabling--excluding-fields)
57
+ 11. [Refreshing & Resetting](#refreshing--resetting)
58
+ 12. [Label Colors](#label-colors)
59
+ 13. [Metrics & Highlighting](#metrics--highlighting)
60
+ 14. [ComparisonForm](#comparisonform)
61
+ 15. [Schema Drift Resilience](#schema-drift-resilience)
62
+ 16. [Custom Renderers](#custom-renderers)
63
+ 17. [API Reference](#api-reference)
64
+ 18. [Contributing](#contributing)
65
+
66
+ ## Purpose
67
+
68
+ - **Reduce Boilerplate:** Automatically render form inputs (text, number, checkbox, select, date, time, etc.) based on Pydantic field types and annotations.
69
+ - **Data Validation:** Leverage Pydantic's validation rules directly from form submissions.
70
+ - **Nested Structures:** Support for nested Pydantic models and lists of models/simple types with accordion UI.
71
+ - **Dynamic Lists:** Built-in HTMX endpoints and JavaScript for adding, deleting, and reordering items in lists within the form.
72
+ - **Customization:** Easily register custom renderers for specific Pydantic types or fields.
73
+ - **Robust Schema Handling:** Gracefully handles model changes and missing fields in initial data.
74
+
75
+ ## Installation
76
+
77
+ You can install `fh-pydantic-form` using either `pip` or `uv`.
78
+
79
+ **Using pip:**
80
+
81
+ ```bash
82
+ pip install fh-pydantic-form
83
+ ```
84
+
85
+ Using uv:
86
+ ```bash
87
+ uv add fh-pydantic-form
88
+ ```
89
+
90
+ This will also install necessary dependencies like `pydantic`, `python-fasthtml`, and `monsterui`.
91
+
92
+ ## Quick Start
93
+
94
+ ```python
95
+ # examples/simple_example.py
96
+ import fasthtml.common as fh
97
+ import monsterui.all as mui
98
+ from pydantic import BaseModel, ValidationError
99
+
100
+ # 1. Import the form renderer
101
+ from fh_pydantic_form import PydanticForm
102
+
103
+ app, rt = fh.fast_app(
104
+ hdrs=[
105
+ mui.Theme.blue.headers(),
106
+ # Add list_manipulation_js() if using list fields
107
+ # from fh_pydantic_form import list_manipulation_js
108
+ # list_manipulation_js(),
109
+ ],
110
+ pico=False, # Using MonsterUI, not PicoCSS
111
+ live=True, # Enable live reload for development
112
+ )
113
+
114
+ # 2. Define your Pydantic model
115
+ class SimpleModel(BaseModel):
116
+ """Model representing a simple form"""
117
+ name: str = "Default Name"
118
+ age: int
119
+ is_active: bool = True
120
+
121
+ # 3. Create a form renderer instance
122
+ # - 'my_form': Unique name for the form (used for prefixes and routes)
123
+ # - SimpleModel: The Pydantic model class
124
+ form_renderer = PydanticForm("my_form", SimpleModel)
125
+
126
+ # (Optional) Register list manipulation routes if your model has List fields
127
+ # form_renderer.register_routes(app)
128
+
129
+ # 4. Define routes
130
+ @rt("/")
131
+ def get():
132
+ """Display the form"""
133
+ return fh.Div(
134
+ mui.Container(
135
+ mui.Card(
136
+ mui.CardHeader("Simple Pydantic Form"),
137
+ mui.CardBody(
138
+ # Use MonsterUI Form component for structure
139
+ mui.Form(
140
+ # Render the inputs using the renderer
141
+ form_renderer.render_inputs(),
142
+ # Add standard form buttons
143
+ fh.Div(
144
+ mui.Button("Submit", type="submit", cls=mui.ButtonT.primary),
145
+ form_renderer.refresh_button("🔄"),
146
+ form_renderer.reset_button("↩️"),
147
+ cls="mt-4 flex items-center gap-2",
148
+ ),
149
+ # HTMX attributes for form submission
150
+ hx_post="/submit_form",
151
+ hx_target="#result", # Target div for response
152
+ hx_swap="innerHTML",
153
+ # Set a unique ID for the form itself for refresh/reset inclusion
154
+ id=f"{form_renderer.name}-form",
155
+ )
156
+ ),
157
+ ),
158
+ # Div to display validation results
159
+ fh.Div(id="result"),
160
+ ),
161
+ )
162
+
163
+ @rt("/submit_form")
164
+ async def post_submit_form(req):
165
+ """Handle form submission and validation"""
166
+ try:
167
+ # 5. Validate the request data against the model
168
+ validated_data: SimpleModel = await form_renderer.model_validate_request(req)
169
+
170
+ # Success: Display the validated data
171
+ return mui.Card(
172
+ mui.CardHeader(fh.H3("Validation Successful")),
173
+ mui.CardBody(
174
+ fh.Pre(
175
+ validated_data.model_dump_json(indent=2),
176
+ )
177
+ ),
178
+ cls="mt-4",
179
+ )
180
+ except ValidationError as e:
181
+ # Validation Error: Display the errors
182
+ return mui.Card(
183
+ mui.CardHeader(fh.H3("Validation Error", cls="text-red-500")),
184
+ mui.CardBody(
185
+ fh.Pre(
186
+ e.json(indent=2),
187
+ )
188
+ ),
189
+ cls="mt-4",
190
+ )
191
+
192
+ if __name__ == "__main__":
193
+ fh.serve()
194
+ ```
195
+
196
+ ## Key Features
197
+
198
+ - **Automatic Field Rendering:** Handles `str`, `int`, `float`, `bool`, `date`, `time`, `Optional`, `Literal`, nested `BaseModel`s, and `List`s out-of-the-box.
199
+ - **Sensible Defaults:** Uses appropriate HTML5 input types (`text`, `number`, `date`, `time`, `checkbox`, `select`).
200
+ - **Labels & Placeholders:** Generates labels from field names (converting snake_case to Title Case) and basic placeholders.
201
+ - **Descriptions as Tooltips:** Uses `Field(description=...)` from Pydantic to create tooltips (`uk-tooltip` via UIkit).
202
+ - **Required Fields:** Automatically adds the `required` attribute based on field definitions (considering `Optional` and defaults).
203
+ - **Disabled Fields:** Disable the whole form with `disabled=True` or disable specific fields with `disabled_fields`
204
+ - **Collapsible Nested Models:** Renders nested Pydantic models in accordion-style components for better form organization and space management.
205
+ - **List Manipulation:**
206
+ - Renders lists of simple types or models in accordion-style cards with an enhanced UI.
207
+ - Provides HTMX endpoints (registered via `register_routes`) for adding and deleting list items.
208
+ - Includes JavaScript (`list_manipulation_js()`) for client-side reordering (moving items up/down).
209
+ - Click list field labels to toggle all items open/closed.
210
+ - **Form Refresh & Reset:**
211
+ - Provides HTMX-powered "Refresh" and "Reset" buttons (`form_renderer.refresh_button()`, `form_renderer.reset_button()`).
212
+ - Refresh updates list item summaries or other dynamic parts without full page reload.
213
+ - Reset reverts the form to its initial values.
214
+ - **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.
215
+ - **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.
216
+
217
+ ## Spacing & Styling
218
+
219
+ `fh-pydantic-form` ships with two spacing presets to fit different UI requirements:
220
+
221
+ | Theme | Purpose | Usage |
222
+ |-------|---------|-------|
223
+ | **normal** (default) | Comfortable margins & borders – great for desktop forms | `PydanticForm(..., spacing="normal")` |
224
+ | **compact** | Ultra-dense UIs, mobile layouts, or forms with many fields | `PydanticForm(..., spacing="compact")` |
225
+
226
+ ```python
227
+ # Example: side-by-side normal vs compact forms
228
+ form_normal = PydanticForm("normal_form", MyModel, spacing="normal")
229
+ form_compact = PydanticForm("compact_form", MyModel, spacing="compact")
230
+ ```
231
+
232
+
233
+ **Important:** The compact CSS is now scoped with `.fhpf-compact` classes and only affects form inputs, not layout containers. This prevents conflicts with your application's layout system.
234
+
235
+ ## Working with Lists
236
+
237
+ When your Pydantic models contain `List[str]`, `List[int]`, or `List[BaseModel]` fields, `fh-pydantic-form` provides rich list manipulation capabilities:
238
+
239
+ ### Basic Setup
240
+
241
+ ```python
242
+ from fh_pydantic_form import PydanticForm, list_manipulation_js
243
+ from typing import List
244
+
245
+ app, rt = fh.fast_app(
246
+ hdrs=[
247
+ mui.Theme.blue.headers(),
248
+ list_manipulation_js(), # Required for list manipulation
249
+ ],
250
+ pico=False,
251
+ live=True,
252
+ )
253
+
254
+ class ListModel(BaseModel):
255
+ name: str = ""
256
+ tags: List[str] = Field(["tag1", "tag2"])
257
+ addresses: List[Address] = Field(default_factory=list)
258
+
259
+ form_renderer = PydanticForm("list_model", ListModel)
260
+ form_renderer.register_routes(app) # Register HTMX endpoints
261
+ ```
262
+
263
+ ### List Features
264
+
265
+ - **Add Items:** Each list has an "Add Item" button that creates new entries
266
+ - **Delete Items:** Each list item has a delete button with confirmation
267
+ - **Reorder Items:** Move items up/down with arrow buttons
268
+ - **Toggle All:** Click the list field label to expand/collapse all items at once
269
+ - **Refresh Display:** Use the 🔄 icon next to list labels to update item summaries
270
+ - **Smart Defaults:** New items are created with sensible default values
271
+
272
+ The list manipulation uses HTMX for seamless updates without page reloads, and includes JavaScript for client-side reordering.
273
+
274
+ ## Nested Models
275
+
276
+ Nested Pydantic models are automatically rendered in collapsible accordion components:
277
+
278
+ ```python
279
+ class Address(BaseModel):
280
+ street: str = "123 Main St"
281
+ city: str = "Anytown"
282
+ is_billing: bool = False
283
+
284
+ class User(BaseModel):
285
+ name: str
286
+ address: Address # Rendered as collapsible accordion
287
+ backup_addresses: List[Address] # List of accordions
288
+ ```
289
+
290
+ **Key behaviors:**
291
+ - Nested models inherit `disabled` and `spacing` settings from the parent form
292
+ - Field prefixes are automatically managed (e.g., `user_address_street`)
293
+ - Accordions are open by default for better user experience
294
+ - Schema drift is handled gracefully - missing fields use defaults, unknown fields are ignored
295
+
296
+ ## Literal & Enum Fields
297
+
298
+ `fh-pydantic-form` provides comprehensive support for choice-based fields through `Literal`, `Enum`, and `IntEnum` types, all automatically rendered as dropdown selects:
299
+
300
+ ### Literal Fields
301
+
302
+ ```python
303
+ from typing import Literal, Optional
304
+
305
+ class OrderModel(BaseModel):
306
+ # Required Literal field - only defined choices available
307
+ shipping_method: Literal["STANDARD", "EXPRESS", "OVERNIGHT"] = "STANDARD"
308
+
309
+ # Optional Literal field - includes "-- None --" option
310
+ category: Optional[Literal["ELECTRONICS", "CLOTHING", "BOOKS", "OTHER"]] = None
311
+ ```
312
+
313
+ ### Enum Fields
314
+
315
+ ```python
316
+ from enum import Enum, IntEnum
317
+
318
+ class OrderStatus(Enum):
319
+ """Order status enum with string values."""
320
+ PENDING = "pending"
321
+ CONFIRMED = "confirmed"
322
+ SHIPPED = "shipped"
323
+ DELIVERED = "delivered"
324
+ CANCELLED = "cancelled"
325
+
326
+ class Priority(IntEnum):
327
+ """Priority levels using IntEnum for numeric ordering."""
328
+ LOW = 1
329
+ MEDIUM = 2
330
+ HIGH = 3
331
+ URGENT = 4
332
+
333
+ class OrderModel(BaseModel):
334
+ # Required Enum field with default
335
+ status: OrderStatus = OrderStatus.PENDING
336
+
337
+ # Optional Enum field without default
338
+ payment_method: Optional[PaymentMethod] = None
339
+
340
+ # Required IntEnum field with default
341
+ priority: Priority = Priority.MEDIUM
342
+
343
+ # Optional IntEnum field without default
344
+ urgency_level: Optional[Priority] = Field(
345
+ None, description="Override priority for urgent orders"
346
+ )
347
+
348
+ # Enum field without default (required)
349
+ fulfillment_status: OrderStatus = Field(
350
+ ..., description="Current fulfillment status"
351
+ )
352
+ ```
353
+
354
+ ### Field Rendering Behavior
355
+
356
+ | Field Type | Required | Optional | Notes |
357
+ |------------|----------|----------|-------|
358
+ | **Literal** | Shows only defined choices | Includes "-- None --" option | String values displayed as-is |
359
+ | **Enum** | Shows enum member values | Includes "-- None --" option | Displays `enum.value` in dropdown |
360
+ | **IntEnum** | Shows integer values | Includes "-- None --" option | Maintains numeric ordering |
361
+
362
+ **Key features:**
363
+ - **Automatic dropdown generation** for all choice-based field types
364
+ - **Proper value handling** - enum values are correctly parsed during form submission
365
+ - **Optional field support** - includes None option when fields are Optional
366
+ - **Field descriptions** become tooltips on hover
367
+ - **Default value selection** - dropdowns pre-select the appropriate default value
368
+
369
+ ## Initial Values & Enum Parsing
370
+
371
+ `fh-pydantic-form` intelligently parses initial values from dictionaries, properly converting strings and integers to their corresponding enum types:
372
+
373
+ ### Setting Initial Values
374
+
375
+ ```python
376
+ # Example initial values from a dictionary
377
+ initial_values_dict = {
378
+ "shipping_method": "EXPRESS", # Literal value as string
379
+ "category": "ELECTRONICS", # Optional Literal value
380
+ "status": "shipped", # Enum value (parsed to OrderStatus.SHIPPED)
381
+ "payment_method": "paypal", # Optional Enum (parsed to PaymentMethod.PAYPAL)
382
+ "priority": 3, # IntEnum as integer (parsed to Priority.HIGH)
383
+ "urgency_level": 4, # Optional IntEnum as integer (parsed to Priority.URGENT)
384
+ "fulfillment_status": "confirmed" # Required Enum (parsed to OrderStatus.CONFIRMED)
385
+ }
386
+
387
+ # Create form with initial values
388
+ form_renderer = PydanticForm("order_form", OrderModel, initial_values=initial_values_dict)
389
+ ```
390
+
391
+ ### Parsing Behavior
392
+
393
+ The form automatically handles conversion between different value formats:
394
+
395
+ | Input Type | Target Type | Example | Result |
396
+ |------------|-------------|---------|--------|
397
+ | String | Enum | `"shipped"` | `OrderStatus.SHIPPED` |
398
+ | String | Optional[Enum] | `"paypal"` | `PaymentMethod.PAYPAL` |
399
+ | Integer | IntEnum | `3` | `Priority.HIGH` |
400
+ | Integer | Optional[IntEnum] | `4` | `Priority.URGENT` |
401
+ | String | Literal | `"EXPRESS"` | `"EXPRESS"` (unchanged) |
402
+
403
+ **Benefits:**
404
+ - **Flexible data sources** - works with database records, API responses, or any dictionary
405
+ - **Type safety** - ensures enum values are valid during parsing
406
+ - **Graceful handling** - invalid enum values are passed through for Pydantic validation
407
+ - **Consistent behavior** - same parsing logic for required and optional fields
408
+
409
+ ### Example Usage
410
+
411
+ ```python
412
+ @rt("/")
413
+ def get():
414
+ return mui.Form(
415
+ form_renderer.render_inputs(), # Pre-populated with parsed enum values
416
+ fh.Div(
417
+ mui.Button("Submit", type="submit", cls=mui.ButtonT.primary),
418
+ form_renderer.refresh_button("🔄"),
419
+ form_renderer.reset_button("↩️"), # Resets to initial parsed values
420
+ cls="mt-4 flex items-center gap-2",
421
+ ),
422
+ hx_post="/submit_order",
423
+ hx_target="#result",
424
+ id=f"{form_renderer.name}-form",
425
+ )
426
+
427
+ @rt("/submit_order")
428
+ async def post_submit_order(req):
429
+ try:
430
+ # Validates and converts form data back to proper enum types
431
+ validated_order: OrderModel = await form_renderer.model_validate_request(req)
432
+
433
+ # Access enum properties
434
+ print(f"Status: {validated_order.status.value} ({validated_order.status.name})")
435
+ print(f"Priority: {validated_order.priority.value} ({validated_order.priority.name})")
436
+
437
+ return success_response(validated_order)
438
+ except ValidationError as e:
439
+ return error_response(e)
440
+ ```
441
+
442
+ This makes it easy to work with enum-based forms when loading data from databases, APIs, or configuration files.
443
+
444
+ ## Disabling & Excluding Fields
445
+
446
+ ### Disabling Fields
447
+
448
+ You can disable the entire form or specific fields:
449
+
450
+ ```python
451
+ # Disable all fields
452
+ form_renderer = PydanticForm("my_form", FormModel, disabled=True)
453
+
454
+ # Disable specific fields only
455
+ form_renderer = PydanticForm(
456
+ "my_form",
457
+ FormModel,
458
+ disabled_fields=["field1", "field3"]
459
+ )
460
+ ```
461
+
462
+ ### Excluding Fields
463
+
464
+ Exclude specific fields from being rendered in the form:
465
+
466
+ ```python
467
+ form_renderer = PydanticForm(
468
+ "my_form",
469
+ FormModel,
470
+ exclude_fields=["internal_field", "computed_field"]
471
+ )
472
+ ```
473
+
474
+ **Important:** When fields are excluded from the UI, `fh-pydantic-form` automatically injects their default values during form parsing and validation. This ensures:
475
+
476
+ - **Hidden fields with defaults** are still included in the final validated data
477
+ - **Required fields without defaults** will still cause validation errors if not provided elsewhere
478
+ - **Default factories** are executed to provide computed default values
479
+ - **Nested BaseModel defaults** are converted to dictionaries for consistency
480
+
481
+ This automatic default injection means you can safely exclude fields that shouldn't be user-editable while maintaining data integrity.
482
+
483
+ ### SkipJsonSchema Fields
484
+
485
+ `fh-pydantic-form` provides advanced handling for `SkipJsonSchema` fields with selective visibility control. By default, fields marked with `SkipJsonSchema` are hidden from forms, but you can selectively show specific ones using the `keep_skip_json_fields` parameter.
486
+
487
+ ```python
488
+ from pydantic.json_schema import SkipJsonSchema
489
+
490
+ class DocumentModel(BaseModel):
491
+ title: str
492
+ content: str
493
+
494
+ # Hidden by default - system fields
495
+ document_id: SkipJsonSchema[str] = Field(
496
+ default_factory=lambda: f"doc_{uuid4().hex[:12]}",
497
+ description="Internal document ID"
498
+ )
499
+ created_at: SkipJsonSchema[datetime.datetime] = Field(
500
+ default_factory=datetime.datetime.now,
501
+ description="Creation timestamp"
502
+ )
503
+ version: SkipJsonSchema[int] = Field(
504
+ default=1,
505
+ description="Document version"
506
+ )
507
+
508
+ # Normal form - all SkipJsonSchema fields hidden
509
+ form_normal = PydanticForm("doc_form", DocumentModel)
510
+
511
+ # Admin form - selectively show some SkipJsonSchema fields
512
+ form_admin = PydanticForm(
513
+ "admin_form",
514
+ DocumentModel,
515
+ keep_skip_json_fields=[
516
+ "document_id", # Show document ID
517
+ "version", # Show version number
518
+ # created_at remains hidden
519
+ ]
520
+ )
521
+ ```
522
+
523
+ #### Nested SkipJsonSchema Fields
524
+
525
+ The feature supports dot notation for nested objects and list items:
526
+
527
+ ```python
528
+ class Address(BaseModel):
529
+ street: str
530
+ city: str
531
+ # Hidden system field
532
+ internal_id: SkipJsonSchema[str] = Field(default_factory=lambda: f"addr_{uuid4().hex[:8]}")
533
+
534
+ class UserModel(BaseModel):
535
+ name: str
536
+ main_address: Address
537
+ other_addresses: List[Address]
538
+
539
+ # Show specific nested SkipJsonSchema fields
540
+ form = PydanticForm(
541
+ "user_form",
542
+ UserModel,
543
+ keep_skip_json_fields=[
544
+ "main_address.internal_id", # Show main address ID
545
+ "other_addresses.internal_id", # Show all address IDs in the list
546
+ ]
547
+ )
548
+ ```
549
+
550
+ **Key Features:**
551
+ - **Hidden by default:** SkipJsonSchema fields are automatically excluded from forms
552
+ - **Selective visibility:** Use `keep_skip_json_fields` to show specific fields
553
+ - **Nested support:** Access nested fields with dot notation (`"main_address.internal_id"`)
554
+ - **List support:** Show fields in all list items (`"addresses.internal_id"`)
555
+ - **Smart defaults:** Non-kept fields use model defaults, kept fields retain initial values
556
+ - **Admin interfaces:** Perfect for admin panels or debugging where you need to see system fields
557
+
558
+ See `examples/complex_example.py` for a comprehensive demonstration of SkipJsonSchema field handling.
559
+
560
+ ## Refreshing & Resetting
561
+
562
+ Forms support dynamic refresh and reset functionality:
563
+
564
+ ```python
565
+ mui.Form(
566
+ form_renderer.render_inputs(),
567
+ fh.Div(
568
+ mui.Button("Submit", type="submit", cls=mui.ButtonT.primary),
569
+ form_renderer.refresh_button("🔄 Refresh"), # Update display
570
+ form_renderer.reset_button("↩️ Reset"), # Restore initial values
571
+ cls="mt-4 flex items-center gap-2",
572
+ ),
573
+ # ... rest of form setup
574
+ )
575
+ ```
576
+
577
+ - **Refresh button** updates the form display based on current values (useful for updating list item summaries)
578
+ - **Reset button** restores all fields to their initial values with confirmation
579
+ - Both use HTMX for seamless updates without page reloads
580
+
581
+
582
+ ## Label Colors
583
+
584
+ Customize the appearance of field labels with the `label_colors` parameter:
585
+
586
+ ```python
587
+ form_renderer = PydanticForm(
588
+ "my_form",
589
+ MyModel,
590
+ label_colors={
591
+ "name": "text-blue-600", # Tailwind CSS class
592
+ "score": "#E12D39", # Hex color value
593
+ "status": "text-green-500", # Another Tailwind class
594
+ },
595
+ )
596
+ ```
597
+
598
+ **Supported formats:**
599
+ - **Tailwind CSS classes:** `"text-blue-600"`, `"text-red-500"`, etc.
600
+ - **Hex color values:** `"#FF0000"`, `"#0066CC"`, etc.
601
+ - **CSS color names:** `"red"`, `"blue"`, `"darkgreen"`, etc.
602
+
603
+ 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.
604
+
605
+ ## Metrics & Highlighting
606
+
607
+ `fh-pydantic-form` provides a powerful metrics system for visual highlighting of form fields based on extraction quality scores and confidence assessments. This is particularly useful for evaluating LLM structured output extraction, comparing generated data against ground truth, and building quality assessment interfaces.
608
+
609
+ <img width="796" alt="image" src="https://github.com/user-attachments/assets/df2f8623-991d-45b1-80d8-5c7239187a74" />
610
+
611
+
612
+ ### Basic Metrics Usage
613
+
614
+ ```python
615
+ from fh_pydantic_form import PydanticForm
616
+
617
+ # Define metrics for your form fields
618
+ metrics_dict = {
619
+ "title": {
620
+ "metric": 0.95,
621
+ "comment": "Excellent title quality - clear and engaging"
622
+ },
623
+ "rating": {
624
+ "metric": 0.3,
625
+ "comment": "Low rating needs attention"
626
+ },
627
+ "status": {
628
+ "metric": 0.0,
629
+ "comment": "Critical status issue - requires immediate review"
630
+ }
631
+ }
632
+
633
+ # Create form with metrics
634
+ form_renderer = PydanticForm(
635
+ "my_form",
636
+ MyModel,
637
+ metrics_dict=metrics_dict
638
+ )
639
+ ```
640
+
641
+ ### Metrics Dictionary Structure
642
+
643
+ Each field can have the following metrics properties:
644
+
645
+ | Property | Type | Description |
646
+ |----------|------|-------------|
647
+ | `metric` | `float` or `str` | Numeric score (0.0-1.0) or string assessment |
648
+ | `color` | `str` | Custom color (overrides automatic color-coding) |
649
+ | `comment` | `str` | Tooltip text shown on hover |
650
+
651
+ ### Automatic Color Coding
652
+
653
+ Numeric metrics are automatically color-coded:
654
+
655
+ - **1.0**: Bright green (perfect score)
656
+ - **0.5-1.0**: Medium green (good range)
657
+ - **0.0-0.5**: Dark red (needs attention)
658
+ - **0.0**: Bright red (critical issue)
659
+
660
+ ```python
661
+ metrics_dict = {
662
+ "field1": {"metric": 1.0, "comment": "Perfect!"}, # Bright green
663
+ "field2": {"metric": 0.8, "comment": "Very good"}, # Medium green
664
+ "field3": {"metric": 0.3, "comment": "Needs work"}, # Dark red
665
+ "field4": {"metric": 0.0, "comment": "Critical"}, # Bright red
666
+ }
667
+ ```
668
+
669
+ ### Custom Colors
670
+
671
+ Override automatic colors with custom values:
672
+
673
+ ```python
674
+ metrics_dict = {
675
+ "status": {
676
+ "metric": 0.0,
677
+ "color": "purple", # Custom color overrides red
678
+ "comment": "Status requires special attention"
679
+ },
680
+ "priority": {
681
+ "metric": 1.0,
682
+ "color": "#FF6B35", # Custom hex color
683
+ "comment": "High priority with custom highlight"
684
+ }
685
+ }
686
+ ```
687
+
688
+ ### String Metrics
689
+
690
+ Use string values for qualitative assessments:
691
+
692
+ ```python
693
+ metrics_dict = {
694
+ "validation_status": {
695
+ "metric": "NEEDS_REVIEW",
696
+ "color": "#F59E0B", # Amber color
697
+ "comment": "Requires human review"
698
+ },
699
+ "data_quality": {
700
+ "metric": "EXCELLENT",
701
+ "color": "#10B981", # Green color
702
+ "comment": "Data quality exceeds standards"
703
+ }
704
+ }
705
+ ```
706
+
707
+ ### Nested Field Metrics
708
+
709
+ Support for nested objects and list items:
710
+
711
+ ```python
712
+ metrics_dict = {
713
+ # Nested object fields
714
+ "author.name": {
715
+ "metric": 0.95,
716
+ "comment": "Author name perfectly formatted"
717
+ },
718
+ "author.email": {
719
+ "metric": 0.9,
720
+ "comment": "Email format excellent"
721
+ },
722
+
723
+ # List item metrics
724
+ "tags[0]": {
725
+ "metric": 1.0,
726
+ "comment": "First tag is perfect"
727
+ },
728
+ "tags[1]": {
729
+ "metric": 0.8,
730
+ "comment": "Second tag very good"
731
+ },
732
+
733
+ # Complex nested paths
734
+ "author.addresses[0].street": {
735
+ "metric": 1.0,
736
+ "comment": "Street address perfectly formatted"
737
+ },
738
+ "author.addresses[1].city": {
739
+ "metric": 0.1,
740
+ "color": "teal",
741
+ "comment": "City has verification problems"
742
+ }
743
+ }
744
+ ```
745
+
746
+ ### Practical Use Cases
747
+
748
+ **LLM Structured Output Evaluation:**
749
+ ```python
750
+ # Evaluate LLM extraction quality against ground truth
751
+ extraction_metrics = {
752
+ "product.name": {
753
+ "metric": 0.9,
754
+ "comment": "Name extracted with minor formatting issue: missing space"
755
+ },
756
+ "product.category": {
757
+ "metric": 0.0,
758
+ "comment": "Critical error: LLM misclassified Electronics instead of Sports"
759
+ },
760
+ "key_features": {
761
+ "metric": 0.6,
762
+ "comment": "LLM missed 2 of 5 key features from source text"
763
+ },
764
+ "extraction_confidence": {
765
+ "metric": 1.0,
766
+ "comment": "LLM confidence score accurately reflects actual performance"
767
+ }
768
+ }
769
+ ```
770
+
771
+ **Document Processing Quality:**
772
+ ```python
773
+ # Highlight extraction quality from documents
774
+ doc_extraction_metrics = {
775
+ "invoice_number": {
776
+ "metric": 1.0,
777
+ "comment": "Invoice number perfectly extracted from PDF"
778
+ },
779
+ "line_items": {
780
+ "metric": 0.75,
781
+ "comment": "3/4 line items extracted correctly"
782
+ },
783
+ "total_amount": {
784
+ "metric": 0.0,
785
+ "comment": "Amount extraction failed - currency symbol confusion"
786
+ }
787
+ }
788
+ ```
789
+
790
+ See `examples/metrics_example.py` for a comprehensive demonstration of all metrics features.
791
+
792
+ ## ComparisonForm
793
+
794
+ The `ComparisonForm` component provides side-by-side comparison of two related forms, perfect for evaluating LLM structured output against ground truth, annotation correction workflows, and comparing extraction results.
795
+
796
+
797
+ <img width="1177" alt="image" src="https://github.com/user-attachments/assets/75020059-0d4d-4519-9c71-70a082d3242e" />
798
+
799
+ ### Basic Usage
800
+
801
+ ```python
802
+ from fh_pydantic_form import PydanticForm, ComparisonForm
803
+
804
+ # Create two forms to compare
805
+ left_form = PydanticForm(
806
+ "ground_truth",
807
+ ProductModel,
808
+ initial_values=annotated_ground_truth,
809
+ disabled=False # Editable for annotation correction
810
+ )
811
+
812
+ right_form = PydanticForm(
813
+ "llm_output",
814
+ ProductModel,
815
+ initial_values=llm_extracted_data,
816
+ disabled=True, # Read-only LLM output
817
+ metrics_dict=extraction_quality_metrics
818
+ )
819
+
820
+ # Create comparison form
821
+ comparison_form = ComparisonForm(
822
+ name="extraction_evaluation",
823
+ left_form=left_form,
824
+ right_form=right_form,
825
+ left_label="📝 Ground Truth (Editable)",
826
+ right_label="🤖 LLM Output (with Quality Scores)"
827
+ )
828
+ ```
829
+
830
+ ### Required JavaScript
831
+
832
+ Include the comparison form JavaScript in your app headers:
833
+
834
+ ```python
835
+ from fh_pydantic_form import comparison_form_js
836
+
837
+ app, rt = fh.fast_app(
838
+ hdrs=[
839
+ mui.Theme.blue.headers(),
840
+ comparison_form_js(), # Required for comparison forms
841
+ ],
842
+ pico=False,
843
+ live=True,
844
+ )
845
+ ```
846
+
847
+ ### Complete Example
848
+
849
+ ```python
850
+ @rt("/")
851
+ def get():
852
+ return fh.Div(
853
+ mui.Container(
854
+ mui.Card(
855
+ mui.CardHeader(
856
+ fh.H1("LLM Extraction Evaluation")
857
+ ),
858
+ mui.CardBody(
859
+ # Render the comparison form
860
+ comparison_form.form_wrapper(
861
+ fh.Div(
862
+ comparison_form.render_inputs(),
863
+
864
+ # Action buttons
865
+ fh.Div(
866
+ mui.Button(
867
+ "Update Ground Truth",
868
+ type="submit",
869
+ hx_post="/update_ground_truth",
870
+ hx_target="#result"
871
+ ),
872
+ comparison_form.left_reset_button("Reset Left"),
873
+ comparison_form.left_refresh_button("Refresh Left"),
874
+ cls="mt-4 flex gap-2"
875
+ ),
876
+
877
+ fh.Div(id="result", cls="mt-4")
878
+ )
879
+ )
880
+ )
881
+ )
882
+ )
883
+ )
884
+
885
+ @rt("/update_ground_truth")
886
+ async def post_update_ground_truth(req):
887
+ # Validate left form (ground truth side)
888
+ validated = await comparison_form.left_form.model_validate_request(req)
889
+
890
+ # Process the ground truth update
891
+ return success_response(validated)
892
+
893
+ # Register routes for both forms
894
+ comparison_form.register_routes(app)
895
+ ```
896
+
897
+ ### Key Features
898
+ - **Aligned fields** input fields are horizontally aligned for easy comparison.
899
+ - **Synchronized Accordions**: Expanding/collapsing sections syncs between both forms
900
+ - **Independent Controls**: Separate refresh and reset buttons for each side
901
+ - **Metrics Integration**: Right side typically shows LLM output quality scores
902
+ - **Flexible Layout**: Responsive design works on desktop and mobile
903
+ - **Form Validation**: Standard validation works with either form
904
+ - **Intelligent List Copying**: Copy lists between forms with automatic length adjustment
905
+
906
+ ### Copying Between Forms
907
+
908
+ The ComparisonForm provides granular copy functionality at multiple levels. When you enable copy buttons (via `copy_left=True` or `copy_right=True`), each field, nested model, and list item gets its own copy button for maximum flexibility:
909
+
910
+ ```python
911
+ # Enable copy buttons (copy FROM right TO left)
912
+ comparison_form = ComparisonForm(
913
+ name="extraction_evaluation",
914
+ left_form=left_form,
915
+ right_form=right_form,
916
+ left_label="Ground Truth",
917
+ right_label="LLM Output",
918
+ copy_left=True, # Show copy buttons on right form to copy TO left
919
+ )
920
+ ```
921
+
922
+ **Copy Granularity Levels:**
923
+
924
+ The copy feature works at five different levels of granularity:
925
+
926
+ 1. **Individual Fields** - Copy a single field value (e.g., `name`, `price`, `status`)
927
+
928
+ 2. **Nested BaseModel (Entire Object)** - Copy all fields within a nested model at once
929
+
930
+ 3. **Individual Fields in Nested Models** - Copy a specific field within a nested object
931
+
932
+ 4. **Full List Fields** - Copy entire lists with automatic length adjustment
933
+
934
+ 5. **Individual List Items** - Add a single item from one list to another
935
+
936
+
937
+ ### Common Patterns
938
+
939
+ **LLM Output Evaluation:**
940
+ ```python
941
+ # Left: Editable ground truth
942
+ # Right: Read-only LLM output with extraction quality metrics
943
+ truth_form = PydanticForm(..., disabled=False, metrics_dict={})
944
+ llm_form = PydanticForm(..., disabled=True, metrics_dict=extraction_metrics)
945
+ ```
946
+
947
+ **Document Extraction Comparison:**
948
+ ```python
949
+ # Left: Manual annotation
950
+ # Right: Automated LLM extraction
951
+ manual_form = PydanticForm(..., initial_values=manual_annotation)
952
+ auto_form = PydanticForm(..., initial_values=llm_extraction, metrics_dict=quality_scores)
953
+ ```
954
+
955
+ **Annotation Correction Workflow:**
956
+ ```python
957
+ # Left: Correctable ground truth
958
+ # Right: LLM output with confidence scores
959
+ ground_truth_form = PydanticForm(..., disabled=False)
960
+ llm_output_form = PydanticForm(..., disabled=True, metrics_dict=confidence_scores)
961
+ ```
962
+
963
+ See `examples/comparison_example.py` for a complete LLM extraction evaluation interface demonstration.
964
+
965
+ ## Setting Initial Values
966
+
967
+ You can set initial form values of the form by passing a model instance or dictionary:
968
+
969
+ ```python
970
+ initial_data = MyModel(name="John", tags=["happy", "joy"])
971
+ form_renderer = PydanticForm("my_form", MyModel, initial_values=initial_data)
972
+
973
+
974
+ initial_data_dict = {"name": "John"}
975
+ form_renderer = PydanticForm("my_form", MyModel, initial_values=initial_values_dict)
976
+ ```
977
+
978
+ 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.
979
+
980
+ ### Reusing Form Configuration with Different Values
981
+
982
+ The `with_initial_values()` method allows you to create a new form instance with the same configuration but different initial values:
983
+
984
+ ```python
985
+ # Create a base form configuration
986
+ base_form = PydanticForm(
987
+ "product_form",
988
+ ProductModel,
989
+ disabled_fields=["id"],
990
+ label_colors={"name": "text-blue-600", "price": "text-green-600"},
991
+ spacing="compact"
992
+ )
993
+
994
+ # Create forms with different initial values using the same configuration
995
+ form_for_product_a = base_form.with_initial_values({"name": "Product A", "price": 29.99})
996
+ form_for_product_b = base_form.with_initial_values({"name": "Product B", "price": 45.50})
997
+
998
+ # Or with model instances
999
+ existing_product = ProductModel(name="Existing Product", price=19.99)
1000
+ form_for_existing = base_form.with_initial_values(existing_product)
1001
+ ```
1002
+
1003
+ This is particularly useful for:
1004
+ - **Editing workflows** where you need the same form configuration for different records
1005
+ - **Template forms** where you want to reuse styling and field configurations
1006
+ - **Bulk operations** where you process multiple items with the same form structure
1007
+
1008
+
1009
+
1010
+ ### Schema Drift Resilience
1011
+
1012
+ `fh-pydantic-form` gracefully handles model evolution and schema changes:
1013
+
1014
+ Initial values can come from **older or newer** versions of your model – unknown fields are ignored gracefully and missing fields use defaults.
1015
+
1016
+ ```python
1017
+ # Your model evolves over time
1018
+ class UserModel(BaseModel):
1019
+ name: str
1020
+ email: str # Added in v2
1021
+ phone: Optional[str] # Added in v3
1022
+
1023
+ # Old data still works
1024
+ old_data = {"name": "John"} # Missing newer fields
1025
+ form = PydanticForm("user", UserModel, initial_values=old_data)
1026
+
1027
+ # Newer data works too
1028
+ new_data = {"name": "Jane", "email": "jane@example.com", "phone": "555-1234", "removed_field": "ignored"}
1029
+ form = PydanticForm("user", UserModel, initial_values=new_data)
1030
+ ```
1031
+
1032
+ **Benefits:**
1033
+ - **Backward compatibility:** Old data structures continue to work
1034
+ - **Forward compatibility:** Unknown fields are silently ignored
1035
+ - **Graceful degradation:** Missing fields fall back to model defaults
1036
+ - **Production stability:** No crashes during rolling deployments
1037
+
1038
+ ## Custom Renderers
1039
+
1040
+ The library is extensible through custom field renderers for specialized input types:
1041
+
1042
+ ```python
1043
+ from fh_pydantic_form.field_renderers import BaseFieldRenderer
1044
+ from fh_pydantic_form import FieldRendererRegistry
1045
+
1046
+ class CustomDetail(BaseModel):
1047
+ value: str = "Default value"
1048
+ confidence: Literal["HIGH", "MEDIUM", "LOW"] = "MEDIUM"
1049
+
1050
+ def __str__(self) -> str:
1051
+ return f"{self.value} ({self.confidence})"
1052
+
1053
+ class CustomDetailFieldRenderer(BaseFieldRenderer):
1054
+ """Display value input and dropdown side by side"""
1055
+
1056
+ def render_input(self):
1057
+ value_input = fh.Div(
1058
+ mui.Input(
1059
+ value=self.value.get("value", ""),
1060
+ id=f"{self.field_name}_value",
1061
+ name=f"{self.field_name}_value",
1062
+ placeholder=f"Enter {self.original_field_name.replace('_', ' ')} value",
1063
+ cls="uk-input w-full",
1064
+ ),
1065
+ cls="flex-grow",
1066
+ )
1067
+
1068
+ confidence_options = [
1069
+ fh.Option(
1070
+ opt, value=opt, selected=(opt == self.value.get("confidence", "MEDIUM"))
1071
+ )
1072
+ for opt in ["HIGH", "MEDIUM", "LOW"]
1073
+ ]
1074
+
1075
+ confidence_select = mui.Select(
1076
+ *confidence_options,
1077
+ id=f"{self.field_name}_confidence",
1078
+ name=f"{self.field_name}_confidence",
1079
+ cls_wrapper="w-[110px] min-w-[110px] flex-shrink-0",
1080
+ )
1081
+
1082
+ return fh.Div(
1083
+ value_input,
1084
+ confidence_select,
1085
+ cls="flex items-start gap-2 w-full",
1086
+ )
1087
+
1088
+ # Register the custom renderer (multiple ways)
1089
+ FieldRendererRegistry.register_type_renderer(CustomDetail, CustomDetailFieldRenderer)
1090
+
1091
+ # Or pass directly to PydanticForm
1092
+ form_renderer = PydanticForm(
1093
+ "my_form",
1094
+ MyModel,
1095
+ custom_renderers=[(CustomDetail, CustomDetailFieldRenderer)],
1096
+ )
1097
+ ```
1098
+
1099
+ ### Registration Methods
1100
+
1101
+ - **Type-based:** `register_type_renderer(CustomDetail, CustomDetailFieldRenderer)`
1102
+ - **Type name:** `register_type_name_renderer("CustomDetail", CustomDetailFieldRenderer)`
1103
+ - **Predicate:** `register_type_renderer_with_predicate(lambda field: isinstance(field.annotation, CustomDetail), CustomDetailFieldRenderer)`
1104
+
1105
+ ## API Reference
1106
+
1107
+ ### PydanticForm Constructor
1108
+
1109
+ | Parameter | Type | Default | Description |
1110
+ |-----------|------|---------|-------------|
1111
+ | `form_name` | `str` | Required | Unique identifier for the form (used for HTMX routes and prefixes) |
1112
+ | `model_class` | `Type[BaseModel]` | Required | The Pydantic model class to render |
1113
+ | `initial_values` | `Optional[Union[BaseModel, Dict]]` | `None` | Initial form values as model instance or dictionary |
1114
+ | `custom_renderers` | `Optional[List[Tuple[Type, Type[BaseFieldRenderer]]]]` | `None` | List of (type, renderer_class) pairs for custom rendering |
1115
+ | `disabled` | `bool` | `False` | Whether to disable all form inputs |
1116
+ | `disabled_fields` | `Optional[List[str]]` | `None` | List of specific field names to disable |
1117
+ | `label_colors` | `Optional[Dict[str, str]]` | `None` | Mapping of field names to CSS colors or Tailwind classes |
1118
+ | `exclude_fields` | `Optional[List[str]]` | `None` | List of field names to exclude from rendering (auto-injected on submission) |
1119
+ | `keep_skip_json_fields` | `Optional[List[str]]` | `None` | List of SkipJsonSchema field paths to selectively show (supports dot notation for nested fields) |
1120
+ | `spacing` | `SpacingValue` | `"normal"` | Spacing theme: `"normal"`, `"compact"`, or `SpacingTheme` enum |
1121
+ | `metrics_dict` | `Optional[Dict[str, Dict]]` | `None` | Field metrics for highlighting and tooltips |
1122
+
1123
+ ### ComparisonForm Constructor
1124
+
1125
+ | Parameter | Type | Default | Description |
1126
+ |-----------|------|---------|-------------|
1127
+ | `name` | `str` | Required | Unique identifier for the comparison form |
1128
+ | `left_form` | `PydanticForm` | Required | Form to display on the left side |
1129
+ | `right_form` | `PydanticForm` | Required | Form to display on the right side |
1130
+ | `left_label` | `str` | `"Left"` | Label for the left form |
1131
+ | `right_label` | `str` | `"Right"` | Label for the right form |
1132
+
1133
+ ### PydanticForm Methods
1134
+
1135
+ | Method | Purpose |
1136
+ |--------|---------|
1137
+ | `render_inputs()` | Generate the HTML form inputs (without `<form>` wrapper) |
1138
+ | `with_initial_values(initial_values, metrics_dict=None)` | Create a new form instance with same configuration but different initial values |
1139
+ | `refresh_button(text=None, **kwargs)` | Create a refresh button component |
1140
+ | `reset_button(text=None, **kwargs)` | Create a reset button component |
1141
+ | `register_routes(app)` | Register HTMX endpoints for list manipulation |
1142
+ | `parse(form_dict)` | Parse raw form data into model-compatible dictionary |
1143
+ | `model_validate_request(req)` | Extract, parse, and validate form data from request |
1144
+
1145
+ ### ComparisonForm Methods
1146
+
1147
+ | Method | Purpose |
1148
+ |--------|---------|
1149
+ | `render_inputs()` | Generate side-by-side form inputs |
1150
+ | `form_wrapper(content)` | Wrap content with comparison form structure |
1151
+ | `left_reset_button(text=None, **kwargs)` | Reset button for left form |
1152
+ | `right_reset_button(text=None, **kwargs)` | Reset button for right form |
1153
+ | `left_refresh_button(text=None, **kwargs)` | Refresh button for left form |
1154
+ | `right_refresh_button(text=None, **kwargs)` | Refresh button for right form |
1155
+ | `register_routes(app)` | Register HTMX endpoints for both forms |
1156
+
1157
+ ### Utility Functions
1158
+
1159
+ | Function | Purpose |
1160
+ |----------|---------|
1161
+ | `list_manipulation_js()` | JavaScript for list reordering and toggle functionality |
1162
+ | `comparison_form_js()` | JavaScript for comparison form accordion synchronization |
1163
+ | `default_dict_for_model(model_class)` | Generate default values for all fields in a model |
1164
+ | `default_for_annotation(annotation)` | Get sensible default for a type annotation |
1165
+
1166
+ ## Contributing
1167
+
1168
+ Contributions are welcome! Please feel free to open an issue or submit a pull request.