pymetadataeditor 0.1.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.
@@ -0,0 +1,617 @@
1
+ import copy
2
+
3
+ # import logging
4
+ import warnings
5
+ from enum import Enum
6
+ from typing import Dict, List, Optional, Tuple, Type, get_args
7
+
8
+ from annotated_types import Ge, Le, MaxLen, MinLen
9
+ from metadataschemas.utils.schema_base_model import SchemaBaseModel
10
+ from metadataschemas.utils.utils import (
11
+ get_subtype_of_optional_or_list,
12
+ is_list_annotation,
13
+ is_optional_annotation,
14
+ is_optional_list,
15
+ merge_dicts,
16
+ standardize_keys_in_dict,
17
+ )
18
+ from pydantic import BaseModel, Field, create_model
19
+ from pydantic._internal._fields import pydantic_general_metadata
20
+ from pydantic.fields import FieldInfo
21
+
22
+ from pymetadataeditor.utils import strip_model_rules
23
+
24
+ # # Configure logging
25
+ # def setup_logging(enable_logging: bool = True):
26
+ # if enable_logging:
27
+ # logging.basicConfig(
28
+ # level=logging.DEBUG, # Set log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
29
+ # format="%(filename)s:%(lineno)d - %(levelname)s - %(message)s",
30
+ # )
31
+ # logging.captureWarnings(True) # Redirect warnings to the logging system
32
+ # else:
33
+ # logging.disable(logging.CRITICAL) # Disable all logging
34
+
35
+
36
+ # # Enable or disable logging by setting this variable
37
+ # ENABLE_LOGGING = False
38
+
39
+ # # Call the setup function
40
+ # setup_logging(ENABLE_LOGGING)
41
+
42
+
43
+ __all__ = ["pydantic_from_template"]
44
+
45
+
46
+ def copy_field_set_required_status(field: FieldInfo, required: bool) -> FieldInfo:
47
+ """
48
+ Create a copy of an existing field with the 'required' status adjusted.
49
+
50
+ We want to take most of the field info from the original field, but the reqruied status from the template.
51
+
52
+ Args:
53
+ field (FieldInfo): The original field to copy.
54
+ required (bool): Whether the new field should be required.
55
+
56
+ Returns:
57
+ FieldInfo: A new field instance with the updated 'required' status.
58
+ """
59
+ return FieldInfo(
60
+ default=(... if required else None), # Adjust required status
61
+ alias=field.alias if hasattr(field, "alias") else None,
62
+ title=field.title if hasattr(field, "title") else None,
63
+ description=field.description if hasattr(field, "description") else None,
64
+ examples=field.examples if hasattr(field, "examples") else None,
65
+ json_schema_extra=field.json_schema_extra if hasattr(field, "json_schema_extra") else None,
66
+ metadata=field.metadata if hasattr(field, "metadata") else None,
67
+ )
68
+
69
+
70
+ def get_child_field_info_from_dot_annotated_name(name, parent_schema):
71
+ name_split = name.split(".")
72
+ sub_schema = parent_schema
73
+ for key in name_split[:-1]:
74
+ sub_schema = sub_schema.model_fields[key].annotation
75
+ if is_optional_annotation(sub_schema) or is_list_annotation(sub_schema):
76
+ sub_schema = get_subtype_of_optional_or_list(sub_schema)
77
+ if not isinstance(sub_schema, type(BaseModel)):
78
+ raise KeyError(name)
79
+ try:
80
+ child_field_info = standardize_keys_in_dict(sub_schema.model_fields)[name_split[-1]]
81
+ except KeyError as e:
82
+ raise KeyError(name_split[-1]) from e
83
+ # except:
84
+ # raise ValueError(f"name={name}, parent_schema={sub_schema}")
85
+ return copy.copy(child_field_info)
86
+
87
+
88
+ def define_simple_element(
89
+ item, parent_schema, element_type=str, apply_rules: bool = True
90
+ ) -> Dict[str : Tuple[Type[BaseModel]], Field]:
91
+ assert (
92
+ isinstance(item, dict)
93
+ and "type" in item
94
+ and item["type"] in ["string", "text", "integer", "number", "boolean", "date", "textarea"]
95
+ ), f"expected string, integer or boolean item, got {item}"
96
+ try:
97
+ child_field_info = get_child_field_info_from_dot_annotated_name(item["key"], parent_schema)
98
+ except KeyError as e:
99
+ if "prop_key" in item:
100
+ prop_key = item["prop_key"]
101
+ prop_key_split = prop_key.split(".")
102
+ # drop elements from prop_key_split if they begin "section-"
103
+ prop_key_split = [x for x in prop_key_split if not x.startswith("section-")]
104
+ prop_key = ".".join(prop_key_split)
105
+ try:
106
+ child_field_info = get_child_field_info_from_dot_annotated_name(prop_key, parent_schema)
107
+ except KeyError as e2:
108
+ warnings.warn(
109
+ (
110
+ f"KeyError: {e2}. Field likely doesn't exist in base schema. "
111
+ f"Proceeding since prop_key = '{prop_key}' is a {element_type} type. "
112
+ f"Full item defition = {item}"
113
+ ),
114
+ UserWarning,
115
+ )
116
+ child_field_info = Field(..., title=item["title"])
117
+ else:
118
+ warnings.warn(
119
+ (
120
+ f"KeyError: {e}. Field likely doesn't exist in base schema. "
121
+ f"Proceeding since key='{item['key']}' is a {element_type} type. "
122
+ f"Full item defition = {item}"
123
+ ),
124
+ UserWarning,
125
+ )
126
+ child_field_info = Field(..., title=item["title"])
127
+ field_type = update_field_info(item, child_field_info, element_type, apply_rules=apply_rules)
128
+ return {item["key"]: field_type}
129
+
130
+
131
+ def get_constraints_from_string(s: str) -> List:
132
+ """
133
+ Parses a string of constraints separated by '|' and returns a list of corresponding constraint objects.
134
+
135
+ Supported constraints:
136
+ - "required": This constraint is ignored in the current implementation as it is applied elsewhere
137
+ - "alpha_dash": Adds a pattern constraint that allows only alphabetic characters and dashes.
138
+ - "min:<value>": Adds a minimum length constraint.
139
+ - "max:<value>": Adds a maximum length constraint.
140
+
141
+ Args:
142
+ s (str): A string containing constraints separated by '|'.
143
+
144
+ Returns:
145
+ List: A list of constraint objects based on the input string.
146
+
147
+ Example:
148
+ >>> get_constraints_from_string("required|alpha_dash|min:5|max:80")
149
+ [pydantic_general_metadata(pattern='^[a-zA-Z\\-]+$'), MinLen(5), MaxLen(80)]
150
+ """
151
+ annotations = []
152
+ for annotation in s.split("|"):
153
+ annotation = annotation.strip()
154
+ if annotation == "":
155
+ continue
156
+ elif annotation in ["required", "idno", "numeric"]:
157
+ continue
158
+ elif annotation == "alpha_dash":
159
+ annotations.append(pydantic_general_metadata(pattern="^[a-zA-Z\\-]+$"))
160
+ elif annotation.startswith("min:"):
161
+ annotations.append(MinLen(int(annotation.split(":")[1])))
162
+ elif annotation.startswith("max:"):
163
+ annotations.append(MaxLen(int(annotation.split(":")[1])))
164
+ elif annotation.startswith("min_value"):
165
+ annotations.append(Ge(int(annotation.strip("min_value").strip(":"))))
166
+ elif annotation.startswith("max_value"):
167
+ annotations.append(Le(int(annotation.strip("max_value").strip(":"))))
168
+ else:
169
+ # logging.warning(f"Unknown constraint: '{annotation}' from string: '{s}'")
170
+ warnings.warn(f"UnknownConstraint: '{annotation}' from the list of constraints: '{s}'")
171
+ return annotations
172
+
173
+
174
+ def create_enum(enum_name, values: list | dict) -> Enum:
175
+ """
176
+ Create an enum from a list of values.
177
+
178
+ Args:
179
+ enum_name (str): The name of the enum.
180
+ values (list|dict): Can be a list of strings or a list of dicts with 'label' and 'code' keys.
181
+
182
+ Returns:
183
+ Enum: The created enum.
184
+ """
185
+ # Create the enum
186
+ e = {}
187
+ for value in values:
188
+ if isinstance(value, str):
189
+ e[value.strip().replace(" ", "_")] = value.strip()
190
+ elif isinstance(value, dict) and "label" in value and "code" in value:
191
+ e[value["label"].strip().replace(" ", "_")] = value["code"].strip()
192
+ else:
193
+ raise ValueError(
194
+ f"Invalid value for enum: {value} from {values},"
195
+ " must be a list of strings or dicts with 'label' and 'code' keys."
196
+ )
197
+ return Enum(enum_name, e)
198
+
199
+
200
+ def update_field_info(
201
+ template_info, child_field_info, overriding_element_type: Optional[str] = None, apply_rules: bool = True
202
+ ) -> Tuple[Type, FieldInfo]:
203
+ """
204
+ Updates the attributes of `child_field_info` based on the `template_info` dictionary.
205
+
206
+ Args:
207
+ template_info (dict): A dictionary containing template information with keys and values to update.
208
+ child_field_info (object): The object whose attributes are to be updated.
209
+ overriding_element_type (Optional[str], optional): An optional string to override the element type.
210
+
211
+ Returns:
212
+ tuple: A tuple containing the field type and the updated `child_field_info`.
213
+
214
+ Raises:
215
+ AttributeError: If an attribute in `template_info` cannot be set on `child_field_info`.
216
+
217
+ Logs:
218
+ Logs warnings if an attribute cannot be set on `child_field_info`.
219
+ Logs information if the field is required or if the key contains "abbreviation".
220
+ """
221
+ child_field_info.title = template_info.get("title") or template_info.get("name") or child_field_info.title
222
+ child_field_info.description = (
223
+ template_info.get("description") or template_info.get("help_text") or child_field_info.description
224
+ )
225
+ # child_field_info.metadata = get_constraints_from_string(template_info.get('constraints', ''))
226
+ rules = (
227
+ template_info.get("constraints")
228
+ or template_info.get("rules_")
229
+ or template_info.get("_rules")
230
+ or template_info.get("__rules", "")
231
+ )
232
+ if apply_rules and rules:
233
+ child_field_info.metadata = get_constraints_from_string(rules)
234
+ # if 'min' in rules and apply_rules:
235
+ # logging.info(f"MIN: template_info = {template_info}; child_field_info = {child_field_info}")
236
+
237
+ for k, v in template_info.items():
238
+ if k in [
239
+ "required",
240
+ "is_required",
241
+ "key",
242
+ "prop_key",
243
+ "description",
244
+ "help_text",
245
+ "type",
246
+ "display_type",
247
+ "title",
248
+ "name",
249
+ "rules",
250
+ "rules_",
251
+ "_rules",
252
+ "__rules",
253
+ "is_recommended",
254
+ "class",
255
+ "enum",
256
+ "props",
257
+ "_ddi_xpath",
258
+ "is_custom",
259
+ "items",
260
+ "type_options",
261
+ ]:
262
+ continue
263
+ try:
264
+ setattr(child_field_info, k, v)
265
+ except AttributeError:
266
+ # logging.warning(f"UnknownFieldInfoKey: {k}={v} not set on {child_field_info}")
267
+ warnings.warn(f"UnknownFieldInfoKey: {k}='{v}' in template info {template_info}")
268
+ continue
269
+
270
+ if overriding_element_type is None:
271
+ type_to_type = {
272
+ "string": str,
273
+ "text": str,
274
+ "integer": int,
275
+ "number": int,
276
+ "boolean": bool,
277
+ "date": str,
278
+ "textarea": str,
279
+ }
280
+
281
+ element_type = template_info.get("type") or template_info.get("display_type")
282
+ element_type = type_to_type.get(element_type, str)
283
+ if apply_rules and "rules" in template_info:
284
+ if isinstance(template_info["rules"], dict) and "is_uri" in template_info["rules"]:
285
+ element_type = str # AnyUrl - for required fields the default is example.com which is confusing
286
+ elif isinstance(template_info["rules"], list) and len(template_info["rules"]) == 0:
287
+ pass
288
+ else:
289
+ # logging.warning(f"Unknown rule: {template_info['rules']}")
290
+ warnings.warn(f"UnknownRule: {template_info['rules']} - expected dict")
291
+ if "enum" in template_info:
292
+ if isinstance(template_info["enum"], list) and len(template_info["enum"]) > 0:
293
+ enum_info = template_info["enum"]
294
+ name = child_field_info.title
295
+ try:
296
+ element_type = create_enum(name, enum_info)
297
+ except ValueError:
298
+ warnings.warn(f"UnknownEnum: '{template_info['enum']}' found in template_info: {template_info}")
299
+ element_type = str
300
+ else:
301
+ # logging.warning(f"Unknown enum: '{template_info['enum']}' found in template_info: {template_info}")
302
+ warnings.warn(f"UnknownEnum: '{template_info['enum']}' found in template_info: {template_info}")
303
+ else:
304
+ element_type = overriding_element_type
305
+
306
+ required = (
307
+ template_info.get("required")
308
+ or template_info.get("is_required")
309
+ or template_info.get("class", "") == "required"
310
+ or "required" in template_info.get("rules_", "")
311
+ or "required" in template_info.get("_rules", "")
312
+ or "required" in template_info.get("__rules", "")
313
+ )
314
+ if required:
315
+ # logging.info("REQUIRED, item: %s", template_info)
316
+ child_field_info = copy_field_set_required_status(child_field_info, True)
317
+ field_type = element_type, child_field_info
318
+ else:
319
+ child_field_info.default = None
320
+ child_field_info = copy_field_set_required_status(child_field_info, False)
321
+ field_type = Optional[element_type], child_field_info
322
+ return field_type
323
+
324
+
325
+ def fill_skipped_field_info(d_old, parent_schema, parent_name=""):
326
+ new_vals = {}
327
+ for k, v in d_old.items():
328
+ if isinstance(v, dict):
329
+ childname = ".".join([parent_name, k]).strip(".")
330
+ # v_new = fill_skipped_field_info(v, parent_schema, parent_name=childname)
331
+ try:
332
+ field_info = get_child_field_info_from_dot_annotated_name(childname, parent_schema)
333
+ except KeyError:
334
+ field_info = Field(..., title=k)
335
+ mdl = create_model_for_template(v, parent_schema, k, childname)
336
+ if ("required" in v and v["required"]) or ("is_required" in v and v["is_required"]):
337
+ field_info = copy_field_set_required_status(field_info, True)
338
+ else:
339
+ mdl = Optional[mdl]
340
+ field_info = copy_field_set_required_status(field_info, False)
341
+ new_vals[k] = (mdl, field_info)
342
+ d_new = d_old
343
+ for k, v in new_vals.items():
344
+ d_new[k] = v
345
+ return d_new
346
+
347
+
348
+ def create_model_for_template(
349
+ dict_of_elements: dict,
350
+ parent_schema: Type[SchemaBaseModel],
351
+ name: str,
352
+ parent_name: Optional[str] = None,
353
+ uid: Optional[str] = None,
354
+ ) -> Type[BaseModel]:
355
+ dict_of_elements = fill_skipped_field_info(
356
+ dict_of_elements, parent_schema, parent_name if parent_name is not None else ""
357
+ )
358
+ dict_of_elements = standardize_keys_in_dict(dict_of_elements, pascal_to_snake=True)
359
+
360
+ model_name = name.replace(" ", "_").rstrip("_").replace(".", "-")
361
+ model = create_model(
362
+ model_name,
363
+ __module__="template",
364
+ __base__=SchemaBaseModel,
365
+ **dict_of_elements,
366
+ )
367
+ model._metadata_type__ = (
368
+ parent_schema._metadata_type__
369
+ if isinstance(parent_schema._metadata_type__, str)
370
+ else parent_schema._metadata_type__.default
371
+ )
372
+ model._metadata_type_version__ = (
373
+ parent_schema._metadata_type_version__
374
+ if isinstance(parent_schema._metadata_type_version__, str)
375
+ else parent_schema._metadata_type_version__.default
376
+ )
377
+ model._template_name__ = name if uid is not None else None
378
+ model._template_uid__ = uid
379
+ return model
380
+
381
+
382
+ def get_children_of_props(
383
+ props: List[Dict[str, str]], parent_schema: Type[BaseModel], apply_rules: bool = True
384
+ ) -> Dict[str : Tuple[Type[BaseModel]], Field]:
385
+ children = {}
386
+ for prop in props:
387
+ if "prop_key" not in prop:
388
+ children.update(template_type_handler(prop, parent_schema, apply_rules=apply_rules))
389
+ else:
390
+ name = prop["prop_key"]
391
+ try:
392
+ child_field_info = get_child_field_info_from_dot_annotated_name(name, parent_schema)
393
+ # if "title" in prop:
394
+ # child_field_info.title = prop["title"]
395
+ # if "help_text" in prop:
396
+ # child_field_info.description = prop["help_text"]
397
+ # child_field = child_field_info.annotation, child_field_info
398
+ child_field = update_field_info(prop, child_field_info, apply_rules=apply_rules)
399
+ children[prop["key"]] = child_field
400
+ except KeyError:
401
+ children.update(template_type_handler(prop, parent_schema, apply_rules=apply_rules))
402
+ return children
403
+
404
+
405
+ def make_array_element_name(key: str) -> str:
406
+ """
407
+ If name ends in s then replace the s and capitalize, else append Item to the string and capitalize
408
+
409
+ >>>make_array_element_name("elements")
410
+ "Element"
411
+
412
+ >>>make_array_element_name("license")
413
+ "LicenseItem"
414
+ """
415
+ assert isinstance(key, str), f"make_array_element_name expected str, got type {type(key)} for input {key}"
416
+ key = key.split(".")[-1]
417
+ if key[-1] == "s":
418
+ return key[:-1].capitalize()
419
+ else:
420
+ return f"{key.capitalize()}Item"
421
+
422
+
423
+ def define_array_element(item, parent_schema, apply_rules: bool = True) -> Dict[str : Tuple[Type[BaseModel]], Field]:
424
+ assert "type" in item and (
425
+ item["type"] == "array" or item["type"] == "nested_array"
426
+ ), f"expected array item but got {item}"
427
+ assert "key" in item, f"expected key in item but got {item.keys()}"
428
+ field_info = Field(..., title=item["title"])
429
+ # if "help_text" in item:
430
+ # field_info.description = item["help_text"]
431
+ if "props" not in item:
432
+ warnings.warn(f"ArrayMissingType: '{item}' - assuming it's an array of str")
433
+ element_type = List[str]
434
+ else:
435
+ children = get_children_of_props(item["props"], parent_schema, apply_rules=apply_rules)
436
+ child_model = create_model_for_template(children, parent_schema, make_array_element_name(item["key"]))
437
+ element_type = List[child_model]
438
+
439
+ field_type = update_field_info(item, field_info, overriding_element_type=element_type, apply_rules=apply_rules)
440
+
441
+ return {item["key"]: field_type}
442
+
443
+
444
+ def define_simple_array_element(
445
+ item: dict, parent_schema: Type[BaseModel], apply_rules: bool = True
446
+ ) -> Dict[str : Tuple[Type[BaseModel]], Field]:
447
+ assert (
448
+ isinstance(item, dict) and "type" in item and item["type"] == "simple_array"
449
+ ), f"expected simple_array item, got {item}"
450
+ try:
451
+ child_field_info = get_child_field_info_from_dot_annotated_name(item["key"], parent_schema)
452
+ except KeyError as e:
453
+ warnings.warn(
454
+ (
455
+ f"KeyError: {e}. Field likely doesn't exist in base schema. "
456
+ f"Proceeding since key={item['key']} is a simple_array type. "
457
+ f"Full item defition = {item}"
458
+ ),
459
+ UserWarning,
460
+ )
461
+ child_field_info = Field(..., title=item["title"])
462
+ field_type = update_field_info(item, child_field_info, overriding_element_type=List[str], apply_rules=apply_rules)
463
+ return {item["key"]: field_type}
464
+
465
+
466
+ def dot_to_hierarchy(d):
467
+ """
468
+ Where dictionary keys have '.', separate the strings either side of the dot into a hierarchy of dictionaries.
469
+
470
+ Example:
471
+
472
+ >>> dot_to_hierarchy({"firstkey.secondkey.thirdkey": 1,
473
+ "firstkey.secondkey.fourthkey": 2,
474
+ "fifthkey": 3,
475
+ "firstkey.sixthkey": 4})
476
+ {'firstkey': {'secondkey': {'thirdkey': 1, 'fourthkey': 2}, 'sixthkey': 4}, 'fifthkey': 3}
477
+ """
478
+
479
+ def merge_dicts(target, source):
480
+ """Recursively merge source dictionary into target dictionary."""
481
+ for key, value in source.items():
482
+ if key in target and isinstance(target[key], dict) and isinstance(value, dict):
483
+ merge_dicts(target[key], value)
484
+ else:
485
+ target[key] = value
486
+
487
+ result = {}
488
+
489
+ for key, value in d.items():
490
+ if isinstance(value, dict):
491
+ value = dot_to_hierarchy(value) # Recursively handle nested dictionaries
492
+
493
+ parts = key.split(".")
494
+ current = result
495
+ for part in parts[:-1]:
496
+ if part not in current:
497
+ current[part] = {}
498
+ current = current[part]
499
+
500
+ # Merge if necessary
501
+ if isinstance(current.get(parts[-1]), dict) and isinstance(value, dict):
502
+ merge_dicts(current[parts[-1]], value)
503
+ else:
504
+ current[parts[-1]] = value
505
+
506
+ return result
507
+
508
+
509
+ def define_group_of_elements(
510
+ items: List[dict], parent_schema: Type[BaseModel], apply_rules: bool = True
511
+ ) -> Dict[str : Tuple[Type[BaseModel]], Field]:
512
+ elements = {}
513
+ for item in items:
514
+ if "is_custom" in item and item["is_custom"] is True:
515
+ warnings.warn(f"SkippingCustomElement: '{item}' - Future work required to support these.")
516
+ continue
517
+ # if "additional" not in elements:
518
+ # elements["additional"] = {}
519
+ # elif not isinstance(elements["additional"], dict):
520
+ # elements["additional"] = {
521
+ # k: (v.annotation, v) for k, v in elements["additional"][0].model_fields.items()
522
+ # }
523
+ # d = template_type_handler(item, parent_schema, apply_rules=apply_rules)
524
+ # m = create_model_for_template(d, parent_schema, item["key"])
525
+ # field_info = Field(..., title=item["title"])
526
+ # field_type = update_field_info(item, field_info, overriding_element_type = m, apply_rules=apply_rules)
527
+ # elements["additional"][item["key"]] = field_type
528
+ else:
529
+ new_dict = template_type_handler(item, parent_schema, apply_rules=apply_rules)
530
+ elements = merge_dicts(elements, new_dict)
531
+ elements = dot_to_hierarchy(elements)
532
+ elements = standardize_keys_in_dict(elements, pascal_to_snake=True)
533
+ if "additional" in elements and isinstance(elements["additional"], dict):
534
+ additional = elements.pop("additional")
535
+ additional = create_model_for_template(additional, parent_schema, "additional")
536
+ sub_field = Field(...)
537
+ sub_field.title = "additional"
538
+ elements["additional"] = additional, sub_field
539
+ return elements
540
+
541
+
542
+ def template_type_handler(item, parent_schema, apply_rules: bool = True):
543
+ if item["type"] in ["string", "text", "date", "textarea"]:
544
+ return define_simple_element(item, parent_schema, str, apply_rules=apply_rules)
545
+ elif item["type"] in ["integer", "number"]:
546
+ return define_simple_element(item, parent_schema, int, apply_rules=apply_rules)
547
+ elif item["type"] == "boolean":
548
+ return define_simple_element(item, parent_schema, bool, apply_rules=apply_rules)
549
+ elif item["type"] in ["array", "nested_array"]:
550
+ return define_array_element(item, parent_schema, apply_rules=apply_rules)
551
+ elif item["type"] == "simple_array":
552
+ return define_simple_array_element(item, parent_schema, apply_rules=apply_rules)
553
+ elif item["type"] in ["section", "section_container"]:
554
+ if "items" in item:
555
+ return define_group_of_elements(item["items"], parent_schema, apply_rules=apply_rules)
556
+ elif "props" in item:
557
+ return define_group_of_elements(item["props"], parent_schema, apply_rules=apply_rules)
558
+ else:
559
+ raise ValueError(f"{item['type']} does not contain items or props, found only {item}")
560
+ else:
561
+ raise NotImplementedError(f"type {item['type']}, {item}")
562
+
563
+
564
+ def append_variables_and_data_files(model_elements, parent_schema, apply_rules: bool = True):
565
+ # as a hack, when we need no rules for LLM inference, don't include the "data_files", "variables", "variable_groups"
566
+ # I found that the LLM is bad at these anyway and struggled with default *numerical* values.
567
+ # I will have to come back to dealing with default numerical values.
568
+ if not apply_rules:
569
+ return model_elements
570
+
571
+ if not (
572
+ parent_schema._metadata_type__ == "microdata"
573
+ or (
574
+ hasattr(parent_schema._metadata_type__, "default") and parent_schema._metadata_type__.default == "microdata"
575
+ )
576
+ ):
577
+ return model_elements
578
+
579
+ for v in ["data_files", "variables", "variable_groups"]:
580
+ if v in model_elements:
581
+ continue
582
+ field_info = get_child_field_info_from_dot_annotated_name(v, parent_schema)
583
+ annotation = field_info.annotation
584
+ if is_optional_annotation(annotation):
585
+ field_info = copy_field_set_required_status(field_info, False)
586
+ else:
587
+ field_info = copy_field_set_required_status(field_info, True)
588
+
589
+ if not apply_rules:
590
+ if is_optional_list(annotation):
591
+ print("is optional list")
592
+ annotation = Optional[List[strip_model_rules(get_args(get_args(annotation)[0])[0])]]
593
+ elif is_list_annotation(annotation):
594
+ print("is list")
595
+ annotation = List[strip_model_rules(get_args(annotation)[0])]
596
+ elif is_optional_annotation(annotation):
597
+ print("is optional")
598
+ annotation = Optional[strip_model_rules(get_args(annotation)[0])]
599
+ model_elements[v] = (annotation, field_info)
600
+ return model_elements
601
+
602
+
603
+ def pydantic_from_template(
604
+ template: Dict, parent_schema: Type[SchemaBaseModel], uid: str, name: Optional[str] = None, apply_rules: bool = True
605
+ ) -> Type[BaseModel]:
606
+ assert "items" in template, f"expected 'items' in template but got {list(template.keys())}"
607
+ if name is None:
608
+ if "title" in template:
609
+ name = template["title"]
610
+ else:
611
+ name = "new_model"
612
+ model_elements = define_group_of_elements(template["items"], parent_schema, apply_rules=apply_rules)
613
+ if parent_schema._metadata_type__ == "microdata" or (
614
+ hasattr(parent_schema._metadata_type__, "default") and parent_schema._metadata_type__.default == "microdata"
615
+ ):
616
+ model_elements = append_variables_and_data_files(model_elements, parent_schema, apply_rules=apply_rules)
617
+ return create_model_for_template(model_elements, parent_schema, name, uid=uid)