fh-pydantic-form 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of fh-pydantic-form might be problematic. Click here for more details.
- fh_pydantic_form/__init__.py +90 -0
- fh_pydantic_form/field_renderers.py +1033 -0
- fh_pydantic_form/form_parser.py +537 -0
- fh_pydantic_form/form_renderer.py +713 -0
- fh_pydantic_form/py.typed +0 -0
- fh_pydantic_form/registry.py +145 -0
- fh_pydantic_form/type_helpers.py +42 -0
- fh_pydantic_form-0.1.2.dist-info/METADATA +327 -0
- fh_pydantic_form-0.1.2.dist-info/RECORD +11 -0
- fh_pydantic_form-0.1.2.dist-info/WHEEL +4 -0
- fh_pydantic_form-0.1.2.dist-info/licenses/LICENSE +13 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import (
|
|
3
|
+
Any,
|
|
4
|
+
Dict,
|
|
5
|
+
List,
|
|
6
|
+
Optional,
|
|
7
|
+
Tuple,
|
|
8
|
+
Union,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from fh_pydantic_form.type_helpers import (
|
|
12
|
+
_get_underlying_type_if_optional,
|
|
13
|
+
_is_literal_type,
|
|
14
|
+
_is_optional_type,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _identify_list_fields(model_class) -> Dict[str, Dict[str, Any]]:
|
|
21
|
+
"""
|
|
22
|
+
Identifies list fields in a model and their item types.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
model_class: The Pydantic model class to analyze
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Dictionary mapping field names to their metadata
|
|
29
|
+
"""
|
|
30
|
+
list_fields = {}
|
|
31
|
+
for field_name, field_info in model_class.model_fields.items():
|
|
32
|
+
annotation = getattr(field_info, "annotation", None)
|
|
33
|
+
if (
|
|
34
|
+
annotation is not None
|
|
35
|
+
and hasattr(annotation, "__origin__")
|
|
36
|
+
and annotation.__origin__ is list
|
|
37
|
+
):
|
|
38
|
+
item_type = annotation.__args__[0]
|
|
39
|
+
list_fields[field_name] = {
|
|
40
|
+
"item_type": item_type,
|
|
41
|
+
"is_model_type": hasattr(item_type, "model_fields"),
|
|
42
|
+
"field_info": field_info, # Store for later use if needed
|
|
43
|
+
}
|
|
44
|
+
return list_fields
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _parse_non_list_fields(
|
|
48
|
+
form_data: Dict[str, Any],
|
|
49
|
+
model_class,
|
|
50
|
+
list_field_defs: Dict[str, Dict[str, Any]],
|
|
51
|
+
base_prefix: str = "",
|
|
52
|
+
) -> Dict[str, Any]:
|
|
53
|
+
"""
|
|
54
|
+
Parses non-list fields from form data based on the model definition.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
form_data: Dictionary containing form field data
|
|
58
|
+
model_class: The Pydantic model class defining the structure
|
|
59
|
+
list_field_defs: Dictionary of list field definitions (to skip)
|
|
60
|
+
base_prefix: Prefix to use when looking up field names in form_data
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Dictionary with parsed non-list fields
|
|
64
|
+
"""
|
|
65
|
+
result: Dict[str, Any] = {}
|
|
66
|
+
|
|
67
|
+
for field_name, field_info in model_class.model_fields.items():
|
|
68
|
+
if field_name in list_field_defs:
|
|
69
|
+
continue # Skip list fields, handled separately
|
|
70
|
+
|
|
71
|
+
# Create full key with prefix
|
|
72
|
+
full_key = f"{base_prefix}{field_name}"
|
|
73
|
+
|
|
74
|
+
annotation = getattr(field_info, "annotation", None)
|
|
75
|
+
|
|
76
|
+
# Handle boolean fields (including Optional[bool])
|
|
77
|
+
if annotation is bool or (
|
|
78
|
+
_is_optional_type(annotation)
|
|
79
|
+
and _get_underlying_type_if_optional(annotation) is bool
|
|
80
|
+
):
|
|
81
|
+
result[field_name] = _parse_boolean_field(full_key, form_data)
|
|
82
|
+
|
|
83
|
+
# Handle Literal fields (including Optional[Literal[...]])
|
|
84
|
+
elif _is_literal_type(annotation):
|
|
85
|
+
result[field_name] = _parse_literal_field(full_key, form_data, field_info)
|
|
86
|
+
|
|
87
|
+
# Handle nested model fields (including Optional[NestedModel])
|
|
88
|
+
elif (
|
|
89
|
+
isinstance(annotation, type)
|
|
90
|
+
and hasattr(annotation, "model_fields")
|
|
91
|
+
or (
|
|
92
|
+
_is_optional_type(annotation)
|
|
93
|
+
and isinstance(_get_underlying_type_if_optional(annotation), type)
|
|
94
|
+
and hasattr(
|
|
95
|
+
_get_underlying_type_if_optional(annotation), "model_fields"
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
):
|
|
99
|
+
# Get the nested model class (unwrap Optional if needed)
|
|
100
|
+
nested_model_class = _get_underlying_type_if_optional(annotation)
|
|
101
|
+
|
|
102
|
+
# Parse the nested model - pass the base_prefix
|
|
103
|
+
nested_value = _parse_nested_model_field(
|
|
104
|
+
field_name, form_data, nested_model_class, field_info, base_prefix
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Only assign if we got a non-None value or the field is not optional
|
|
108
|
+
if nested_value is not None:
|
|
109
|
+
result[field_name] = nested_value
|
|
110
|
+
elif _is_optional_type(annotation):
|
|
111
|
+
# Explicitly set None for optional nested models
|
|
112
|
+
result[field_name] = None
|
|
113
|
+
|
|
114
|
+
# Handle simple fields
|
|
115
|
+
else:
|
|
116
|
+
# Use updated _parse_simple_field that handles optionality
|
|
117
|
+
result[field_name] = _parse_simple_field(full_key, form_data, field_info)
|
|
118
|
+
|
|
119
|
+
return result
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _parse_boolean_field(field_name: str, form_data: Dict[str, Any]) -> bool:
|
|
123
|
+
"""
|
|
124
|
+
Parse a boolean field from form data.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
field_name: Name of the field to parse
|
|
128
|
+
form_data: Dictionary containing form field data
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Boolean value - True if field name exists in form_data, False otherwise
|
|
132
|
+
"""
|
|
133
|
+
return field_name in form_data
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _parse_literal_field(field_name: str, form_data: Dict[str, Any], field_info) -> Any:
|
|
137
|
+
"""
|
|
138
|
+
Parse a Literal field, converting empty string OR '-- None --' to None for optional fields.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
field_name: Name of the field to parse
|
|
142
|
+
form_data: Dictionary containing form field data
|
|
143
|
+
field_info: FieldInfo object to check for optionality
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
The parsed value or None for empty/None values with optional fields
|
|
147
|
+
"""
|
|
148
|
+
value = form_data.get(field_name)
|
|
149
|
+
|
|
150
|
+
# Check if the field is Optional[Literal[...]]
|
|
151
|
+
if _is_optional_type(field_info.annotation):
|
|
152
|
+
# If the submitted value is the empty string OR the display text for None, treat it as None
|
|
153
|
+
if value == "" or value == "-- None --":
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
# Return the actual submitted value (string) for Pydantic validation
|
|
157
|
+
return value
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _parse_simple_field(
|
|
161
|
+
field_name: str, form_data: Dict[str, Any], field_info=None
|
|
162
|
+
) -> Any:
|
|
163
|
+
"""
|
|
164
|
+
Parse a simple field (string, number, etc.) from form data.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
field_name: Name of the field to parse
|
|
168
|
+
form_data: Dictionary containing form field data
|
|
169
|
+
field_info: Optional FieldInfo object to check for optionality
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Value of the field or None if not found
|
|
173
|
+
"""
|
|
174
|
+
if field_name in form_data:
|
|
175
|
+
value = form_data[field_name]
|
|
176
|
+
|
|
177
|
+
# Handle empty strings for optional fields
|
|
178
|
+
if value == "" and field_info and _is_optional_type(field_info.annotation):
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
return value
|
|
182
|
+
|
|
183
|
+
# If field is optional and not in form_data, return None
|
|
184
|
+
if field_info and _is_optional_type(field_info.annotation):
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _parse_nested_model_field(
|
|
191
|
+
field_name: str,
|
|
192
|
+
form_data: Dict[str, Any],
|
|
193
|
+
nested_model_class,
|
|
194
|
+
field_info,
|
|
195
|
+
parent_prefix: str = "",
|
|
196
|
+
) -> Optional[Dict[str, Any]]:
|
|
197
|
+
"""
|
|
198
|
+
Parse a nested Pydantic model field from form data.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
field_name: Name of the field to parse
|
|
202
|
+
form_data: Dictionary containing form field data
|
|
203
|
+
nested_model_class: The nested model class
|
|
204
|
+
field_info: The field info from the parent model
|
|
205
|
+
parent_prefix: Prefix from parent form/model to use when constructing keys
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Dictionary with nested model structure or None/default if no data found
|
|
209
|
+
"""
|
|
210
|
+
# Construct the full prefix for this nested model's fields
|
|
211
|
+
current_prefix = f"{parent_prefix}{field_name}_"
|
|
212
|
+
nested_data: Dict[str, Optional[Any]] = {}
|
|
213
|
+
found_any_subfield = False
|
|
214
|
+
|
|
215
|
+
# Check if any keys match this prefix
|
|
216
|
+
for key in form_data:
|
|
217
|
+
if key.startswith(current_prefix):
|
|
218
|
+
found_any_subfield = True
|
|
219
|
+
break
|
|
220
|
+
|
|
221
|
+
if found_any_subfield:
|
|
222
|
+
# Process each field in the nested model
|
|
223
|
+
for sub_field_name, sub_field_info in nested_model_class.model_fields.items():
|
|
224
|
+
sub_key = f"{current_prefix}{sub_field_name}"
|
|
225
|
+
annotation = getattr(sub_field_info, "annotation", None)
|
|
226
|
+
|
|
227
|
+
# Handle based on field type, with Optional unwrapping
|
|
228
|
+
is_optional = _is_optional_type(annotation)
|
|
229
|
+
base_type = _get_underlying_type_if_optional(annotation)
|
|
230
|
+
|
|
231
|
+
# Handle boolean fields (including Optional[bool])
|
|
232
|
+
if annotation is bool or (is_optional and base_type is bool):
|
|
233
|
+
nested_data[sub_field_name] = _parse_boolean_field(sub_key, form_data)
|
|
234
|
+
|
|
235
|
+
# Handle nested model fields (including Optional[NestedModel])
|
|
236
|
+
elif isinstance(base_type, type) and hasattr(base_type, "model_fields"):
|
|
237
|
+
# Pass the current_prefix to the recursive call
|
|
238
|
+
sub_value = _parse_nested_model_field(
|
|
239
|
+
sub_field_name, form_data, base_type, sub_field_info, current_prefix
|
|
240
|
+
)
|
|
241
|
+
if sub_value is not None:
|
|
242
|
+
nested_data[sub_field_name] = sub_value
|
|
243
|
+
elif is_optional:
|
|
244
|
+
nested_data[sub_field_name] = None
|
|
245
|
+
|
|
246
|
+
# Handle simple fields, including empty string to None conversion for Optional fields
|
|
247
|
+
elif sub_key in form_data:
|
|
248
|
+
value = form_data[sub_key]
|
|
249
|
+
if value == "" and is_optional:
|
|
250
|
+
nested_data[sub_field_name] = None
|
|
251
|
+
else:
|
|
252
|
+
nested_data[sub_field_name] = value
|
|
253
|
+
|
|
254
|
+
# Handle missing optional fields
|
|
255
|
+
elif is_optional:
|
|
256
|
+
nested_data[sub_field_name] = None
|
|
257
|
+
|
|
258
|
+
return nested_data
|
|
259
|
+
|
|
260
|
+
# No data found for this nested model
|
|
261
|
+
logger.debug(
|
|
262
|
+
f"No form data found for nested model field: {field_name} with prefix: {current_prefix}"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
is_field_optional = _is_optional_type(field_info.annotation)
|
|
266
|
+
|
|
267
|
+
# If the field is optional, return None
|
|
268
|
+
if is_field_optional:
|
|
269
|
+
logger.debug(
|
|
270
|
+
f"Nested field {field_name} is optional and no data found, returning None."
|
|
271
|
+
)
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
# If not optional, try to use default or default_factory
|
|
275
|
+
default_value = None
|
|
276
|
+
default_applied = False
|
|
277
|
+
|
|
278
|
+
if hasattr(field_info, "default") and field_info.default is not None:
|
|
279
|
+
default_value = field_info.default
|
|
280
|
+
default_applied = True
|
|
281
|
+
logger.debug(f"Nested field {field_name} using default value.")
|
|
282
|
+
elif (
|
|
283
|
+
hasattr(field_info, "default_factory")
|
|
284
|
+
and field_info.default_factory is not None
|
|
285
|
+
):
|
|
286
|
+
try:
|
|
287
|
+
default_value = field_info.default_factory()
|
|
288
|
+
default_applied = True
|
|
289
|
+
logger.debug(f"Nested field {field_name} using default_factory.")
|
|
290
|
+
except Exception as e:
|
|
291
|
+
logger.warning(
|
|
292
|
+
f"Error creating default for {field_name} using default_factory: {e}"
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
if default_applied:
|
|
296
|
+
if default_value is not None and hasattr(default_value, "model_dump"):
|
|
297
|
+
return default_value.model_dump()
|
|
298
|
+
elif isinstance(default_value, dict):
|
|
299
|
+
return default_value
|
|
300
|
+
else:
|
|
301
|
+
# Handle cases where default might not be a model/dict (unlikely for nested model)
|
|
302
|
+
logger.warning(
|
|
303
|
+
f"Default value for nested field {field_name} is not a model or dict: {type(default_value)}"
|
|
304
|
+
)
|
|
305
|
+
# Don't return PydanticUndefined or other non-dict values directly
|
|
306
|
+
# Fall through to empty dict return instead
|
|
307
|
+
|
|
308
|
+
# If not optional, no data found, and no default applicable, always return an empty dict
|
|
309
|
+
# This ensures the test_parse_nested_model_field passes and allows Pydantic to validate
|
|
310
|
+
# if the nested model can be created from empty data
|
|
311
|
+
logger.debug(
|
|
312
|
+
f"Nested field {field_name} is required, no data/default found, returning empty dict {{}}."
|
|
313
|
+
)
|
|
314
|
+
return {}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _parse_list_fields(
|
|
318
|
+
form_data: Dict[str, Any],
|
|
319
|
+
list_field_defs: Dict[str, Dict[str, Any]],
|
|
320
|
+
base_prefix: str = "",
|
|
321
|
+
) -> Dict[str, List[Any]]:
|
|
322
|
+
"""
|
|
323
|
+
Parse list fields from form data by analyzing keys and reconstructing ordered lists.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
form_data: Dictionary containing form field data
|
|
327
|
+
list_field_defs: Dictionary of list field definitions
|
|
328
|
+
base_prefix: Prefix to use when looking up field names in form_data
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
Dictionary with parsed list fields
|
|
332
|
+
"""
|
|
333
|
+
# Skip if no list fields defined
|
|
334
|
+
if not list_field_defs:
|
|
335
|
+
return {}
|
|
336
|
+
|
|
337
|
+
# Temporary storage: { list_field_name: { idx_str: item_data } }
|
|
338
|
+
list_items_temp: Dict[str, Dict[str, Union[Dict[str, Any], Any]]] = {
|
|
339
|
+
field_name: {} for field_name in list_field_defs
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
# Order tracking: { list_field_name: [idx_str1, idx_str2, ...] }
|
|
343
|
+
list_item_indices_ordered: Dict[str, List[str]] = {
|
|
344
|
+
field_name: [] for field_name in list_field_defs
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
# Process all form keys that might belong to list fields
|
|
348
|
+
for key, value in form_data.items():
|
|
349
|
+
parse_result = _parse_list_item_key(key, list_field_defs, base_prefix)
|
|
350
|
+
if not parse_result:
|
|
351
|
+
continue # Key doesn't belong to a known list field
|
|
352
|
+
|
|
353
|
+
field_name, idx_str, subfield, is_simple_list = parse_result
|
|
354
|
+
|
|
355
|
+
# Track order if seeing this index for the first time for this field
|
|
356
|
+
if idx_str not in list_items_temp[field_name]:
|
|
357
|
+
list_item_indices_ordered[field_name].append(idx_str)
|
|
358
|
+
# Initialize storage for this item index
|
|
359
|
+
list_items_temp[field_name][idx_str] = {} if not is_simple_list else None
|
|
360
|
+
|
|
361
|
+
# Store the value
|
|
362
|
+
if is_simple_list:
|
|
363
|
+
list_items_temp[field_name][idx_str] = value
|
|
364
|
+
else:
|
|
365
|
+
# It's a model list item, store subfield value
|
|
366
|
+
if subfield: # Should always have a subfield for model list items
|
|
367
|
+
list_items_temp[field_name][idx_str][subfield] = value
|
|
368
|
+
|
|
369
|
+
# Build final lists based on tracked order
|
|
370
|
+
final_lists = {}
|
|
371
|
+
for field_name, ordered_indices in list_item_indices_ordered.items():
|
|
372
|
+
field_def = list_field_defs[field_name]
|
|
373
|
+
item_type = field_def["item_type"]
|
|
374
|
+
|
|
375
|
+
items = []
|
|
376
|
+
for idx_str in ordered_indices:
|
|
377
|
+
item_data = list_items_temp[field_name][idx_str]
|
|
378
|
+
|
|
379
|
+
# Handle empty strings for optional fields inside models
|
|
380
|
+
if isinstance(item_data, dict):
|
|
381
|
+
# Check each subfield for optional type and empty string
|
|
382
|
+
for subfield_name, subfield_value in list(item_data.items()):
|
|
383
|
+
# Get the corresponding field_info from the item_type
|
|
384
|
+
if (
|
|
385
|
+
hasattr(item_type, "model_fields")
|
|
386
|
+
and subfield_name in item_type.model_fields
|
|
387
|
+
):
|
|
388
|
+
subfield_info = item_type.model_fields[subfield_name]
|
|
389
|
+
# Convert empty strings to None for optional fields
|
|
390
|
+
if subfield_value == "" and _is_optional_type(
|
|
391
|
+
subfield_info.annotation
|
|
392
|
+
):
|
|
393
|
+
item_data[subfield_name] = None
|
|
394
|
+
# Convert 'on' to True for boolean fields
|
|
395
|
+
elif subfield_value == "on":
|
|
396
|
+
annotation = getattr(subfield_info, "annotation", None)
|
|
397
|
+
base_type = _get_underlying_type_if_optional(annotation)
|
|
398
|
+
if base_type is bool:
|
|
399
|
+
item_data[subfield_name] = True
|
|
400
|
+
|
|
401
|
+
# Handle missing boolean fields in model list items
|
|
402
|
+
if field_def["is_model_type"] and hasattr(item_type, "model_fields"):
|
|
403
|
+
# Iterate through all model fields to find missing boolean fields
|
|
404
|
+
for (
|
|
405
|
+
model_field_name,
|
|
406
|
+
model_field_info,
|
|
407
|
+
) in item_type.model_fields.items():
|
|
408
|
+
annotation = getattr(model_field_info, "annotation", None)
|
|
409
|
+
base_type = _get_underlying_type_if_optional(annotation)
|
|
410
|
+
is_bool_type = base_type is bool
|
|
411
|
+
|
|
412
|
+
# If it's a boolean field and not in the item_data, set it to False
|
|
413
|
+
if is_bool_type and model_field_name not in item_data:
|
|
414
|
+
logger.info(
|
|
415
|
+
f"Setting missing boolean '{model_field_name}' to False for item in list '{field_name}'"
|
|
416
|
+
)
|
|
417
|
+
item_data[model_field_name] = False
|
|
418
|
+
|
|
419
|
+
# For model types, keep as dict for now
|
|
420
|
+
items.append(item_data)
|
|
421
|
+
|
|
422
|
+
if items: # Only add if items were found
|
|
423
|
+
final_lists[field_name] = items
|
|
424
|
+
|
|
425
|
+
# For any list field that didn't have form data, use its default
|
|
426
|
+
for field_name, field_def in list_field_defs.items():
|
|
427
|
+
if field_name not in final_lists:
|
|
428
|
+
field_info = field_def["field_info"]
|
|
429
|
+
if hasattr(field_info, "default") and field_info.default is not None:
|
|
430
|
+
final_lists[field_name] = field_info.default
|
|
431
|
+
elif (
|
|
432
|
+
hasattr(field_info, "default_factory")
|
|
433
|
+
and field_info.default_factory is not None
|
|
434
|
+
):
|
|
435
|
+
try:
|
|
436
|
+
final_lists[field_name] = field_info.default_factory()
|
|
437
|
+
except Exception:
|
|
438
|
+
pass
|
|
439
|
+
|
|
440
|
+
return final_lists
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _parse_list_item_key(
|
|
444
|
+
key: str, list_field_defs: Dict[str, Dict[str, Any]], base_prefix: str = ""
|
|
445
|
+
) -> Optional[Tuple[str, str, Optional[str], bool]]:
|
|
446
|
+
"""
|
|
447
|
+
Parse a form key that might represent a list item.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
key: Form field key to parse
|
|
451
|
+
list_field_defs: Dictionary of list field definitions
|
|
452
|
+
base_prefix: Prefix to use when looking up field names in form_data
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
Tuple of (field_name, idx_str, subfield, is_simple_list) if key is for a list item,
|
|
456
|
+
None otherwise
|
|
457
|
+
"""
|
|
458
|
+
# Check if key starts with any of our list field names with underscore
|
|
459
|
+
for field_name, field_def in list_field_defs.items():
|
|
460
|
+
full_prefix = f"{base_prefix}{field_name}_"
|
|
461
|
+
if key.startswith(full_prefix):
|
|
462
|
+
remaining = key[len(full_prefix) :]
|
|
463
|
+
is_model_type = field_def["is_model_type"]
|
|
464
|
+
|
|
465
|
+
# Handle key format based on whether it's a model list or simple list
|
|
466
|
+
if is_model_type:
|
|
467
|
+
# Complex model field: field_name_idx_subfield
|
|
468
|
+
try:
|
|
469
|
+
if "_" not in remaining:
|
|
470
|
+
# Invalid format for model list item
|
|
471
|
+
continue
|
|
472
|
+
|
|
473
|
+
# Special handling for "new_" prefix
|
|
474
|
+
if remaining.startswith("new_"):
|
|
475
|
+
# Format is "new_timestamp_subfield"
|
|
476
|
+
parts = remaining.split("_")
|
|
477
|
+
if len(parts) >= 3: # "new", "timestamp", "subfield"
|
|
478
|
+
idx_str = f"{parts[0]}_{parts[1]}" # "new_timestamp"
|
|
479
|
+
subfield = "_".join(
|
|
480
|
+
parts[2:]
|
|
481
|
+
) # "subfield" (or "subfield_with_underscores")
|
|
482
|
+
|
|
483
|
+
# Validate timestamp part is numeric
|
|
484
|
+
timestamp_part = parts[1]
|
|
485
|
+
if not timestamp_part.isdigit():
|
|
486
|
+
continue
|
|
487
|
+
|
|
488
|
+
return (
|
|
489
|
+
field_name,
|
|
490
|
+
idx_str,
|
|
491
|
+
subfield,
|
|
492
|
+
False,
|
|
493
|
+
) # Not a simple list
|
|
494
|
+
else:
|
|
495
|
+
continue
|
|
496
|
+
else:
|
|
497
|
+
# Regular numeric index format: "123_subfield"
|
|
498
|
+
idx_part, subfield = remaining.split("_", 1)
|
|
499
|
+
|
|
500
|
+
# Validate index is numeric
|
|
501
|
+
if not idx_part.isdigit():
|
|
502
|
+
continue
|
|
503
|
+
|
|
504
|
+
return (
|
|
505
|
+
field_name,
|
|
506
|
+
idx_part,
|
|
507
|
+
subfield,
|
|
508
|
+
False,
|
|
509
|
+
) # Not a simple list
|
|
510
|
+
|
|
511
|
+
except Exception:
|
|
512
|
+
continue
|
|
513
|
+
else:
|
|
514
|
+
# Simple list: field_name_idx
|
|
515
|
+
try:
|
|
516
|
+
# For simple types, the entire remaining part is the index
|
|
517
|
+
idx_str = remaining
|
|
518
|
+
|
|
519
|
+
# Validate index format - either numeric or "new_timestamp"
|
|
520
|
+
if idx_str.isdigit():
|
|
521
|
+
# Regular numeric index
|
|
522
|
+
pass
|
|
523
|
+
elif idx_str.startswith("new_"):
|
|
524
|
+
# New item with timestamp - validate timestamp part is numeric
|
|
525
|
+
timestamp_part = idx_str[4:] # Skip "new_" prefix
|
|
526
|
+
if not timestamp_part.isdigit():
|
|
527
|
+
continue
|
|
528
|
+
else:
|
|
529
|
+
continue
|
|
530
|
+
|
|
531
|
+
return field_name, idx_str, None, True # Simple list
|
|
532
|
+
|
|
533
|
+
except Exception:
|
|
534
|
+
continue
|
|
535
|
+
|
|
536
|
+
# Not a list field key
|
|
537
|
+
return None
|