codeforms 0.2.1__tar.gz → 0.2.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.
Files changed (32) hide show
  1. {codeforms-0.2.1 → codeforms-0.2.2}/.gitignore +1 -0
  2. {codeforms-0.2.1 → codeforms-0.2.2}/PKG-INFO +93 -1
  3. {codeforms-0.2.1 → codeforms-0.2.2}/README.md +92 -0
  4. {codeforms-0.2.1 → codeforms-0.2.2}/pyproject.toml +1 -1
  5. {codeforms-0.2.1 → codeforms-0.2.2}/src/codeforms/__init__.py +2 -0
  6. {codeforms-0.2.1 → codeforms-0.2.2}/src/codeforms/export.py +29 -0
  7. {codeforms-0.2.1 → codeforms-0.2.2}/src/codeforms/fields.py +28 -1
  8. {codeforms-0.2.1 → codeforms-0.2.2}/src/codeforms/forms.py +171 -266
  9. {codeforms-0.2.1 → codeforms-0.2.2}/src/codeforms/registry.py +2 -0
  10. codeforms-0.2.2/tests/test_object_list_field.py +166 -0
  11. codeforms-0.2.1/uv.lock +0 -416
  12. {codeforms-0.2.1 → codeforms-0.2.2}/.github/workflows/python-publish.yml +0 -0
  13. {codeforms-0.2.1 → codeforms-0.2.2}/.github/workflows/tests.yml +0 -0
  14. {codeforms-0.2.1 → codeforms-0.2.2}/.python-version +0 -0
  15. {codeforms-0.2.1 → codeforms-0.2.2}/LICENSE +0 -0
  16. {codeforms-0.2.1 → codeforms-0.2.2}/examples/basic_usage.py +0 -0
  17. {codeforms-0.2.1 → codeforms-0.2.2}/examples/conditional_visibility.py +0 -0
  18. {codeforms-0.2.1 → codeforms-0.2.2}/examples/custom_fields.py +0 -0
  19. {codeforms-0.2.1 → codeforms-0.2.2}/examples/dependent_options.py +0 -0
  20. {codeforms-0.2.1 → codeforms-0.2.2}/examples/i18n_usage.py +0 -0
  21. {codeforms-0.2.1 → codeforms-0.2.2}/examples/wizard_form.py +0 -0
  22. {codeforms-0.2.1 → codeforms-0.2.2}/src/codeforms/i18n.py +0 -0
  23. {codeforms-0.2.1 → codeforms-0.2.2}/tests/__init__.py +0 -0
  24. {codeforms-0.2.1 → codeforms-0.2.2}/tests/conftest.py +0 -0
  25. {codeforms-0.2.1 → codeforms-0.2.2}/tests/test_basic_usage.py +0 -0
  26. {codeforms-0.2.1 → codeforms-0.2.2}/tests/test_dependent_options.py +0 -0
  27. {codeforms-0.2.1 → codeforms-0.2.2}/tests/test_dynamic_visibility.py +0 -0
  28. {codeforms-0.2.1 → codeforms-0.2.2}/tests/test_i18n.py +0 -0
  29. {codeforms-0.2.1 → codeforms-0.2.2}/tests/test_json_schema_export.py +0 -0
  30. {codeforms-0.2.1 → codeforms-0.2.2}/tests/test_phase2_compat.py +0 -0
  31. {codeforms-0.2.1 → codeforms-0.2.2}/tests/test_registry.py +0 -0
  32. {codeforms-0.2.1 → codeforms-0.2.2}/tests/test_wizard_steps.py +0 -0
@@ -7,6 +7,7 @@ wheels/
7
7
 
8
8
  # Virtual environments
9
9
  .venv
10
+ uv.lock
10
11
 
11
12
  # Private
12
13
  todo
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeforms
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Python library for creating, validating, and rendering web forms using Pydantic
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -100,6 +100,69 @@ All fields inherit from `FormFieldBase` and share these common attributes:
100
100
  - `accept`: Accepted file types (e.g. `"image/*,.pdf"`).
101
101
  - `multiple`: Allow multiple file uploads.
102
102
  - **`HiddenField`** — Hidden field (`<input type="hidden">`).
103
+ - **`ListField`** — Array of primitive values.
104
+ - `item_type`: Primitive type for each item (`text`, `number`, `email`, `url`, `date`).
105
+ - `min_items`, `max_items`: List size limits.
106
+ - **`ObjectListField`** — Array of homogeneous objects validated against nested subfields.
107
+ - `fields`: List of subfields that define each object shape.
108
+ - `min_items`, `max_items`: List size limits.
109
+
110
+ ### `ObjectListField`
111
+
112
+ Use `ObjectListField` when a form needs a repeatable list of structured rows, such as parallel approvers, attendees with roles, or line items.
113
+
114
+ ```python
115
+ from codeforms import Form, ObjectListField, TextField, CheckboxField
116
+
117
+ form = Form(
118
+ name="parallel_approvers",
119
+ fields=[
120
+ ObjectListField(
121
+ name="parallel_approvals",
122
+ label="Aprobadores",
123
+ required=True,
124
+ min_items=1,
125
+ max_items=5,
126
+ fields=[
127
+ TextField(name="approver_email", label="Email", required=True),
128
+ TextField(name="label", label="Etiqueta", required=True),
129
+ CheckboxField(name="required", label="Obligatorio"),
130
+ ],
131
+ )
132
+ ],
133
+ )
134
+ ```
135
+
136
+ Expected submitted value:
137
+
138
+ ```json
139
+ {
140
+ "parallel_approvals": [
141
+ {
142
+ "approver_email": "ana@empresa.com",
143
+ "label": "Compras",
144
+ "required": true
145
+ },
146
+ {
147
+ "approver_email": "luis@empresa.com",
148
+ "label": "Finanzas"
149
+ }
150
+ ]
151
+ }
152
+ ```
153
+
154
+ Validation behavior:
155
+
156
+ - The top-level field must be a JSON array.
157
+ - Each item must be a JSON object.
158
+ - Unknown keys inside items are rejected.
159
+ - Required nested subfields are enforced.
160
+ - Validation errors include nested paths like `parallel_approvals[0].label`.
161
+
162
+ Current limitation:
163
+
164
+ - `ObjectListField` is fully supported in backend validation and JSON Schema export.
165
+ - Rich repeatable HTML UI generation is not implemented yet. If you need an interactive editor, prefer consuming the exported `json_schema` from your frontend.
103
166
 
104
167
  ## Data Validation
105
168
 
@@ -284,9 +347,38 @@ Output:
284
347
  | `UrlField` | `string` (`format: "uri"`) | `minLength`, `maxLength` |
285
348
  | `TextareaField` | `string` | `minLength`, `maxLength` |
286
349
  | `ListField` | `array` | `minItems`, `maxItems` |
350
+ | `ObjectListField` | `array` of `object` | nested `properties`, nested `required`, `minItems`, `maxItems` |
287
351
 
288
352
  Field annotations like `label`, `help_text`, `default_value`, and `readonly` map to the JSON Schema keywords `title`, `description`, `default`, and `readOnly` respectively.
289
353
 
354
+ Example `ObjectListField` schema:
355
+
356
+ ```json
357
+ {
358
+ "type": "array",
359
+ "minItems": 1,
360
+ "items": {
361
+ "type": "object",
362
+ "properties": {
363
+ "approver_email": {
364
+ "type": "string",
365
+ "title": "Email"
366
+ },
367
+ "label": {
368
+ "type": "string",
369
+ "title": "Etiqueta"
370
+ },
371
+ "required": {
372
+ "type": "boolean",
373
+ "title": "Obligatorio"
374
+ }
375
+ },
376
+ "required": ["approver_email", "label"],
377
+ "additionalProperties": false
378
+ }
379
+ }
380
+ ```
381
+
290
382
  Fields inside `FieldGroup` and `FormStep` containers are flattened into the top-level `properties` automatically.
291
383
 
292
384
  ## Internationalization (i18n)
@@ -87,6 +87,69 @@ All fields inherit from `FormFieldBase` and share these common attributes:
87
87
  - `accept`: Accepted file types (e.g. `"image/*,.pdf"`).
88
88
  - `multiple`: Allow multiple file uploads.
89
89
  - **`HiddenField`** — Hidden field (`<input type="hidden">`).
90
+ - **`ListField`** — Array of primitive values.
91
+ - `item_type`: Primitive type for each item (`text`, `number`, `email`, `url`, `date`).
92
+ - `min_items`, `max_items`: List size limits.
93
+ - **`ObjectListField`** — Array of homogeneous objects validated against nested subfields.
94
+ - `fields`: List of subfields that define each object shape.
95
+ - `min_items`, `max_items`: List size limits.
96
+
97
+ ### `ObjectListField`
98
+
99
+ Use `ObjectListField` when a form needs a repeatable list of structured rows, such as parallel approvers, attendees with roles, or line items.
100
+
101
+ ```python
102
+ from codeforms import Form, ObjectListField, TextField, CheckboxField
103
+
104
+ form = Form(
105
+ name="parallel_approvers",
106
+ fields=[
107
+ ObjectListField(
108
+ name="parallel_approvals",
109
+ label="Aprobadores",
110
+ required=True,
111
+ min_items=1,
112
+ max_items=5,
113
+ fields=[
114
+ TextField(name="approver_email", label="Email", required=True),
115
+ TextField(name="label", label="Etiqueta", required=True),
116
+ CheckboxField(name="required", label="Obligatorio"),
117
+ ],
118
+ )
119
+ ],
120
+ )
121
+ ```
122
+
123
+ Expected submitted value:
124
+
125
+ ```json
126
+ {
127
+ "parallel_approvals": [
128
+ {
129
+ "approver_email": "ana@empresa.com",
130
+ "label": "Compras",
131
+ "required": true
132
+ },
133
+ {
134
+ "approver_email": "luis@empresa.com",
135
+ "label": "Finanzas"
136
+ }
137
+ ]
138
+ }
139
+ ```
140
+
141
+ Validation behavior:
142
+
143
+ - The top-level field must be a JSON array.
144
+ - Each item must be a JSON object.
145
+ - Unknown keys inside items are rejected.
146
+ - Required nested subfields are enforced.
147
+ - Validation errors include nested paths like `parallel_approvals[0].label`.
148
+
149
+ Current limitation:
150
+
151
+ - `ObjectListField` is fully supported in backend validation and JSON Schema export.
152
+ - Rich repeatable HTML UI generation is not implemented yet. If you need an interactive editor, prefer consuming the exported `json_schema` from your frontend.
90
153
 
91
154
  ## Data Validation
92
155
 
@@ -271,9 +334,38 @@ Output:
271
334
  | `UrlField` | `string` (`format: "uri"`) | `minLength`, `maxLength` |
272
335
  | `TextareaField` | `string` | `minLength`, `maxLength` |
273
336
  | `ListField` | `array` | `minItems`, `maxItems` |
337
+ | `ObjectListField` | `array` of `object` | nested `properties`, nested `required`, `minItems`, `maxItems` |
274
338
 
275
339
  Field annotations like `label`, `help_text`, `default_value`, and `readonly` map to the JSON Schema keywords `title`, `description`, `default`, and `readOnly` respectively.
276
340
 
341
+ Example `ObjectListField` schema:
342
+
343
+ ```json
344
+ {
345
+ "type": "array",
346
+ "minItems": 1,
347
+ "items": {
348
+ "type": "object",
349
+ "properties": {
350
+ "approver_email": {
351
+ "type": "string",
352
+ "title": "Email"
353
+ },
354
+ "label": {
355
+ "type": "string",
356
+ "title": "Etiqueta"
357
+ },
358
+ "required": {
359
+ "type": "boolean",
360
+ "title": "Obligatorio"
361
+ }
362
+ },
363
+ "required": ["approver_email", "label"],
364
+ "additionalProperties": false
365
+ }
366
+ }
367
+ ```
368
+
277
369
  Fields inside `FieldGroup` and `FormStep` containers are flattened into the top-level `properties` automatically.
278
370
 
279
371
  ## Internationalization (i18n)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codeforms"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "Python library for creating, validating, and rendering web forms using Pydantic"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
@@ -12,6 +12,7 @@ from codeforms.fields import (
12
12
  FormStep,
13
13
  HiddenField,
14
14
  ListField,
15
+ ObjectListField,
15
16
  NumberField,
16
17
  RadioField,
17
18
  SelectField,
@@ -63,6 +64,7 @@ __all__ = [
63
64
  "UrlField",
64
65
  "TextareaField",
65
66
  "ListField",
67
+ "ObjectListField",
66
68
  "FieldGroup",
67
69
  "FormStep",
68
70
  # Form
@@ -12,6 +12,7 @@ from codeforms.fields import (
12
12
  FormStep,
13
13
  HiddenField,
14
14
  ListField,
15
+ ObjectListField,
15
16
  NumberField,
16
17
  RadioField,
17
18
  SelectField,
@@ -485,6 +486,26 @@ _LIST_ITEM_TYPE_MAP: Dict[str, str] = {
485
486
  }
486
487
 
487
488
 
489
+ def _object_list_item_schema(field: ObjectListField) -> Dict[str, Any]:
490
+ properties: Dict[str, Any] = {}
491
+ required = []
492
+
493
+ for subfield in field.fields:
494
+ properties[subfield.name] = _field_to_json_schema_property(subfield)
495
+ if subfield.required:
496
+ required.append(subfield.name)
497
+
498
+ item_schema: Dict[str, Any] = {
499
+ "type": "object",
500
+ "properties": properties,
501
+ "additionalProperties": False,
502
+ }
503
+ if required:
504
+ item_schema["required"] = required
505
+
506
+ return item_schema
507
+
508
+
488
509
  def _field_to_json_schema_property(field: FormFieldBase) -> Dict[str, Any]:
489
510
  """Convert a single form field to a JSON Schema property definition."""
490
511
  prop: Dict[str, Any] = {}
@@ -570,6 +591,14 @@ def _field_to_json_schema_property(field: FormFieldBase) -> Dict[str, Any]:
570
591
  if field.max_items is not None:
571
592
  prop["maxItems"] = field.max_items
572
593
 
594
+ elif isinstance(field, ObjectListField):
595
+ prop["type"] = "array"
596
+ prop["items"] = _object_list_item_schema(field)
597
+ if field.min_items is not None:
598
+ prop["minItems"] = field.min_items
599
+ if field.max_items is not None:
600
+ prop["maxItems"] = field.max_items
601
+
573
602
  elif isinstance(field, TextField):
574
603
  prop["type"] = "string"
575
604
  if field.minlength is not None:
@@ -30,6 +30,7 @@ class FieldType(str, Enum):
30
30
  HIDDEN = "hidden"
31
31
  URL = "url"
32
32
  LIST = "list"
33
+ OBJECT_LIST = "object-list"
33
34
 
34
35
 
35
36
  class ValidationRule(BaseModel):
@@ -279,7 +280,7 @@ class TextareaField(FormFieldBase):
279
280
 
280
281
 
281
282
  class ListField(FormFieldBase):
282
- """Campo para listas de valores (ej: lista de participantes)"""
283
+ """Campo para listas de valores primitivos (ej: lista de participantes)."""
283
284
 
284
285
  field_type: FieldType = FieldType.LIST
285
286
  min_items: Optional[int] = None
@@ -287,6 +288,32 @@ class ListField(FormFieldBase):
287
288
  item_type: str = "text" # Tipo de cada item en la lista
288
289
 
289
290
 
291
+ class ObjectListField(FormFieldBase):
292
+ """Campo para listas de objetos homogéneos validados por subcampos."""
293
+
294
+ field_type: FieldType = FieldType.OBJECT_LIST
295
+ min_items: Optional[int] = None
296
+ max_items: Optional[int] = None
297
+ fields: List[Any] = Field(default_factory=list)
298
+
299
+ @model_validator(mode="before")
300
+ @classmethod
301
+ def resolve_object_fields(cls, data: Any) -> Any:
302
+ if isinstance(data, dict) and "fields" in data:
303
+ from codeforms.registry import resolve_content_item
304
+
305
+ data = data.copy()
306
+ data["fields"] = [resolve_content_item(item) for item in data["fields"]]
307
+ return data
308
+
309
+ @model_validator(mode="after")
310
+ def validate_object_fields(self) -> "ObjectListField":
311
+ names = [field.name for field in self.fields]
312
+ if len(names) != len(set(names)):
313
+ raise ValueError(t("form.unique_field_names_in_group", title=self.label or self.name))
314
+ return self
315
+
316
+
290
317
  class FieldGroup(BaseModel):
291
318
  """Representa un grupo de campos en un formulario para organización en secciones"""
292
319