avrotize 2.21.0__py3-none-any.whl → 2.22.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.
avrotize/avrotorust.py CHANGED
@@ -144,12 +144,15 @@ class AvroToRust:
144
144
  field_name = self.safe_identifier(snake(original_field_name))
145
145
  field_type = self.convert_avro_type_to_rust(field_name, field['type'], parent_namespace)
146
146
  serde_rename = field_name != original_field_name
147
+ # Check if this is a generated type (enum, union, or record) where random values may match default
148
+ is_generated_type = field_type in self.generated_types_rust_package or '::' in field_type
147
149
  fields.append({
148
150
  'original_name': original_field_name,
149
151
  'name': field_name,
150
152
  'type': field_type,
151
153
  'serde_rename': serde_rename,
152
- 'random_value': self.generate_random_value(field_type)
154
+ 'random_value': self.generate_random_value(field_type),
155
+ 'is_generated_type': is_generated_type
153
156
  })
154
157
 
155
158
  struct_name = self.safe_identifier(pascal(avro_schema['name']))
@@ -187,28 +190,51 @@ class AvroToRust:
187
190
  def get_is_json_match_clause(self, field_name: str, field_type: str, for_union=False) -> str:
188
191
  """Generates the is_json_match clause for a field"""
189
192
  ref = f'node[\"{field_name}\"]' if not for_union else 'node'
190
- if field_type == 'String' or field_type == 'Option<String>':
191
- return f"{ref}.is_string()"
192
- elif field_type == 'bool' or field_type == 'Option<bool>':
193
- return f"{ref}.is_boolean()"
194
- elif field_type == 'i32' or field_type == 'Option<i32>':
195
- return f"{ref}.is_i64()"
196
- elif field_type == 'i64' or field_type == 'Option<i64>':
197
- return f"{ref}.is_i64()"
198
- elif field_type == 'f32' or field_type == 'Option<f32>':
199
- return f"{ref}.is_f64()"
200
- elif field_type == 'f64' or field_type == 'Option<f64>':
201
- return f"{ref}.is_f64()"
202
- elif field_type == 'Vec<u8>' or field_type == 'Option<Vec<u8>>':
203
- return f"{ref}.is_array()"
204
- elif field_type == 'serde_json::Value' or field_type == 'std::collections::HashMap<String, String>':
205
- return f"{ref}.is_object()"
206
- elif field_type.startswith('std::collections::HashMap<String, '):
207
- return f"{ref}.is_object()"
208
- elif field_type.startswith('Vec<'):
209
- return f"{ref}.is_array()"
193
+
194
+ # Check if type is optional - if so, we need to allow null values
195
+ is_optional = field_type.startswith('Option<')
196
+ base_type = field_type[7:-1] if is_optional else field_type
197
+ null_check = f" || {ref}.is_null()" if is_optional else ""
198
+
199
+ # serde_json::Value can be any JSON type, so always return true
200
+ if base_type == 'serde_json::Value':
201
+ return "true"
202
+
203
+ if base_type == 'String':
204
+ return f"({ref}.is_string(){null_check})"
205
+ elif base_type == 'bool':
206
+ return f"({ref}.is_boolean(){null_check})"
207
+ elif base_type == 'i32':
208
+ return f"({ref}.is_i64(){null_check})"
209
+ elif base_type == 'i64':
210
+ return f"({ref}.is_i64(){null_check})"
211
+ elif base_type == 'f32':
212
+ return f"({ref}.is_f64(){null_check})"
213
+ elif base_type == 'f64':
214
+ return f"({ref}.is_f64(){null_check})"
215
+ elif base_type == 'Vec<u8>':
216
+ return f"({ref}.is_array(){null_check})"
217
+ elif base_type == 'std::collections::HashMap<String, String>':
218
+ return f"({ref}.is_object(){null_check})"
219
+ elif base_type.startswith('std::collections::HashMap<String, '):
220
+ return f"({ref}.is_object(){null_check})"
221
+ elif base_type.startswith('Vec<'):
222
+ return f"({ref}.is_array(){null_check})"
223
+ # chrono types - check for string (ISO 8601 format) or number (timestamp)
224
+ elif 'chrono::NaiveDateTime' in base_type or 'NaiveDateTime' in base_type:
225
+ return f"({ref}.is_string() || {ref}.is_i64(){null_check})"
226
+ elif 'chrono::NaiveDate' in base_type or 'NaiveDate' in base_type:
227
+ return f"({ref}.is_string() || {ref}.is_i64(){null_check})"
228
+ elif 'chrono::NaiveTime' in base_type or 'NaiveTime' in base_type:
229
+ return f"({ref}.is_string() || {ref}.is_i64(){null_check})"
230
+ # uuid type - check for string
231
+ elif 'uuid::Uuid' in base_type or 'Uuid' in base_type:
232
+ return f"({ref}.is_string(){null_check})"
210
233
  else:
211
- return f"{field_type}::is_json_match(&{ref})"
234
+ # Custom types - call their is_json_match method
235
+ if is_optional:
236
+ return f"({base_type}::is_json_match(&{ref}) || {ref}.is_null())"
237
+ return f"{base_type}::is_json_match(&{ref})"
212
238
 
213
239
 
214
240
  def generate_enum(self, avro_schema: Dict, parent_namespace: str) -> str:
@@ -250,17 +276,29 @@ class AvroToRust:
250
276
  ns = namespace.replace('.', '::').lower()
251
277
  union_enum_name = pascal(field_name) + 'Union'
252
278
  union_types = [self.convert_avro_type_to_rust(field_name + "Option" + str(i), t, namespace) for i, t in enumerate(avro_type) if t != 'null']
253
- union_fields = [
254
- {
279
+
280
+ # Track seen predicates to identify structurally identical variants
281
+ seen_predicates: set = set()
282
+ union_fields = []
283
+ for i, t in enumerate(union_types):
284
+ predicate = self.get_is_json_match_clause(field_name, t, for_union=True)
285
+ # Mark if this is the first variant with this predicate structure
286
+ # Subsequent variants with same predicate can't be distinguished during JSON deserialization
287
+ is_first_with_predicate = predicate not in seen_predicates
288
+ seen_predicates.add(predicate)
289
+ union_fields.append({
255
290
  'name': pascal(t.rsplit('::',1)[-1]),
256
291
  'type': t,
257
292
  'random_value': self.generate_random_value(t),
258
293
  'default_value': 'Default::default()',
259
- 'json_match_predicate': self.get_is_json_match_clause(field_name, t, for_union=True),
260
- } for i, t in enumerate(union_types)]
294
+ 'json_match_predicate': predicate,
295
+ 'is_first_with_predicate': is_first_with_predicate,
296
+ })
297
+
261
298
  qualified_union_enum_name = self.safe_package(self.concat_package(ns, union_enum_name))
262
299
  context = {
263
300
  'serde_annotation': self.serde_annotation,
301
+ 'avro_annotation': self.avro_annotation,
264
302
  'union_enum_name': union_enum_name,
265
303
  'union_fields': union_fields,
266
304
  'json_match_predicates': [self.get_is_json_match_clause(f['name'], f['type'], for_union=True) for f in union_fields]
@@ -143,6 +143,35 @@ class StructureToCSharp:
143
143
  ]
144
144
  return word in reserved_words
145
145
 
146
+ def safe_identifier(self, name: str, class_name: str = '', fallback_prefix: str = 'field') -> str:
147
+ """Converts a name to a safe C# identifier.
148
+
149
+ Handles:
150
+ - Reserved words (prepend @)
151
+ - Numeric prefixes (prepend _)
152
+ - Special characters (replace with _)
153
+ - All-special-char names (use fallback_prefix)
154
+ - Class name collision (append _)
155
+ """
156
+ import re
157
+ # Replace invalid characters with underscores
158
+ safe = re.sub(r'[^a-zA-Z0-9_]', '_', str(name))
159
+ # Remove leading/trailing underscores from sanitization
160
+ safe = safe.strip('_') if safe != name else safe
161
+ # If nothing left after removing special chars, use fallback
162
+ if not safe or not re.match(r'^[a-zA-Z_@]', safe):
163
+ if safe and re.match(r'^[0-9]', safe):
164
+ safe = '_' + safe # Numeric prefix
165
+ else:
166
+ safe = fallback_prefix + '_' + (safe if safe else 'unnamed')
167
+ # Handle reserved words with @ prefix
168
+ if self.is_csharp_reserved_word(safe):
169
+ safe = '@' + safe
170
+ # Handle class name collision
171
+ if class_name and safe == class_name:
172
+ safe = safe + '_'
173
+ return safe
174
+
146
175
  def is_csharp_primitive_type(self, csharp_type: str) -> bool:
147
176
  """ Checks if a type is a C# primitive type """
148
177
  if csharp_type.endswith('?'):
@@ -416,16 +445,18 @@ class StructureToCSharp:
416
445
  """ Generates a property for a class """
417
446
  property_definition = ''
418
447
 
419
- # Resolve property name
420
- field_name = prop_name
421
- if self.is_csharp_reserved_word(field_name):
422
- field_name = f"@{field_name}"
448
+ # Resolve property name using safe_identifier for special chars, numeric prefixes, etc.
449
+ field_name = self.safe_identifier(prop_name, class_name)
423
450
  if self.pascal_properties:
424
- field_name_cs = pascal(field_name)
451
+ field_name_cs = pascal(field_name.lstrip('@'))
452
+ # Re-check for class name collision after pascal casing
453
+ if field_name_cs == class_name:
454
+ field_name_cs += "_"
425
455
  else:
426
456
  field_name_cs = field_name
427
- if field_name_cs == class_name:
428
- field_name_cs += "_"
457
+
458
+ # Track if field name differs from original for JSON annotation
459
+ needs_json_annotation = field_name_cs != prop_name
429
460
 
430
461
  # Check if this is a const field
431
462
  if 'const' in prop_schema:
@@ -442,9 +473,9 @@ class StructureToCSharp:
442
473
 
443
474
  # Add JSON property name annotation when property name differs from schema name
444
475
  # This is needed for proper JSON serialization/deserialization, especially with pascal_properties
445
- if field_name != field_name_cs:
476
+ if needs_json_annotation:
446
477
  property_definition += f'{INDENT}[System.Text.Json.Serialization.JsonPropertyName("{prop_name}")]\n'
447
- if self.newtonsoft_json_annotation and field_name != field_name_cs:
478
+ if self.newtonsoft_json_annotation and needs_json_annotation:
448
479
  property_definition += f'{INDENT}[Newtonsoft.Json.JsonProperty("{prop_name}")]\n'
449
480
 
450
481
  # Add XML element annotation if enabled
@@ -473,9 +504,9 @@ class StructureToCSharp:
473
504
 
474
505
  # Add JSON property name annotation when property name differs from schema name
475
506
  # This is needed for proper JSON serialization/deserialization, especially with pascal_properties
476
- if field_name != field_name_cs:
507
+ if needs_json_annotation:
477
508
  property_definition += f'{INDENT}[System.Text.Json.Serialization.JsonPropertyName("{prop_name}")]\n'
478
- if self.newtonsoft_json_annotation and field_name != field_name_cs:
509
+ if self.newtonsoft_json_annotation and needs_json_annotation:
479
510
  property_definition += f'{INDENT}[Newtonsoft.Json.JsonProperty("{prop_name}")]\n'
480
511
 
481
512
  # Add XML element annotation if enabled
avrotize/structuretodb.py CHANGED
@@ -443,6 +443,27 @@ def structure_type_to_sql_type(structure_type: Any, dialect: str) -> str:
443
443
  struct_type = structure_type.get("type", "string")
444
444
  if struct_type in ["array", "set", "map", "object", "choice", "tuple"]:
445
445
  return type_map[dialect][struct_type]
446
+
447
+ # Handle string type with maxLength annotation
448
+ if struct_type == "string" and "maxLength" in structure_type:
449
+ max_length = structure_type["maxLength"]
450
+ if dialect == "sqlserver" or dialect == "sqlanywhere":
451
+ return f"NVARCHAR({max_length})"
452
+ elif dialect in ["postgres", "redshift", "db2"]:
453
+ return f"VARCHAR({max_length})"
454
+ elif dialect in ["mysql", "mariadb"]:
455
+ return f"VARCHAR({max_length})"
456
+ elif dialect == "sqlite":
457
+ return f"VARCHAR({max_length})"
458
+ elif dialect == "oracle":
459
+ return f"VARCHAR2({max_length})"
460
+ elif dialect == "bigquery":
461
+ return f"STRING({max_length})"
462
+ elif dialect == "snowflake":
463
+ return f"VARCHAR({max_length})"
464
+ else:
465
+ return f"VARCHAR({max_length})"
466
+
446
467
  return structure_type_to_sql_type(struct_type, dialect)
447
468
 
448
469
  return type_map.get(dialect, type_map["postgres"])["string"]
avrotize/structuretogo.py CHANGED
@@ -16,8 +16,15 @@ INDENT = ' '
16
16
  class StructureToGo:
17
17
  """ Converts JSON Structure schema to Go structs """
18
18
 
19
+ # Go reserved keywords that cannot be used as package names
20
+ GO_RESERVED_WORDS = [
21
+ 'break', 'default', 'func', 'interface', 'select', 'case', 'defer', 'go', 'map', 'struct', 'chan',
22
+ 'else', 'goto', 'package', 'switch', 'const', 'fallthrough', 'if', 'range', 'type', 'continue', 'for',
23
+ 'import', 'return', 'var',
24
+ ]
25
+
19
26
  def __init__(self, base_package: str = '') -> None:
20
- self.base_package = base_package
27
+ self.base_package = self._safe_package_name(base_package) if base_package else base_package
21
28
  self.output_dir = os.getcwd()
22
29
  self.json_annotation = False
23
30
  self.avro_annotation = False
@@ -31,17 +38,37 @@ class StructureToGo:
31
38
  self.structs: List[Dict] = []
32
39
  self.enums: List[Dict] = []
33
40
 
34
- def safe_identifier(self, name: str) -> str:
35
- """Converts a name to a safe Go identifier"""
36
- reserved_words = [
37
- 'break', 'default', 'func', 'interface', 'select', 'case', 'defer', 'go', 'map', 'struct', 'chan',
38
- 'else', 'goto', 'package', 'switch', 'const', 'fallthrough', 'if', 'range', 'type', 'continue', 'for',
39
- 'import', 'return', 'var',
40
- ]
41
- if name in reserved_words:
41
+ def _safe_package_name(self, name: str) -> str:
42
+ """Converts a name to a safe Go package name"""
43
+ if name in self.GO_RESERVED_WORDS:
42
44
  return f"{name}_"
43
45
  return name
44
46
 
47
+ def safe_identifier(self, name: str, fallback_prefix: str = 'field') -> str:
48
+ """Converts a name to a safe Go identifier.
49
+
50
+ Handles:
51
+ - Reserved words (append _)
52
+ - Numeric prefixes (prepend _)
53
+ - Special characters (replace with _)
54
+ - All-special-char names (use fallback_prefix)
55
+ """
56
+ import re
57
+ # Replace invalid characters with underscores
58
+ safe = re.sub(r'[^a-zA-Z0-9_]', '_', str(name))
59
+ # Remove leading/trailing underscores from sanitization
60
+ safe = safe.strip('_') if safe != name else safe
61
+ # If nothing left after removing special chars, use fallback
62
+ if not safe or not re.match(r'^[a-zA-Z_]', safe):
63
+ if safe and re.match(r'^[0-9]', safe):
64
+ safe = '_' + safe # Numeric prefix
65
+ else:
66
+ safe = fallback_prefix + '_' + (safe if safe else 'unnamed')
67
+ # Handle reserved words
68
+ if safe in self.GO_RESERVED_WORDS:
69
+ safe = safe + '_'
70
+ return safe
71
+
45
72
  def go_type_name(self, name: str, namespace: str = '') -> str:
46
73
  """Returns a qualified name for a Go struct or enum"""
47
74
  if namespace:
@@ -675,7 +702,8 @@ class StructureToGo:
675
702
  def convert(self, structure_schema_path: str, output_dir: str):
676
703
  """Converts JSON Structure schema to Go"""
677
704
  if not self.base_package:
678
- self.base_package = os.path.splitext(os.path.basename(structure_schema_path))[0].replace('-', '_').lower()
705
+ pkg_name = os.path.splitext(os.path.basename(structure_schema_path))[0].replace('-', '_').lower()
706
+ self.base_package = self._safe_package_name(pkg_name)
679
707
 
680
708
  with open(structure_schema_path, 'r', encoding='utf-8') as file:
681
709
  schema = json.load(file)
@@ -22,5 +22,10 @@
22
22
  <artifactId>jackson-annotations</artifactId>
23
23
  <version>${jackson.version}</version>
24
24
  </dependency>
25
+ <dependency>
26
+ <groupId>com.fasterxml.jackson.datatype</groupId>
27
+ <artifactId>jackson-datatype-jsr310</artifactId>
28
+ <version>${jackson.version}</version>
29
+ </dependency>
25
30
  </dependencies>
26
31
  </project>
@@ -71,13 +71,34 @@ class StructureToJava:
71
71
  self.is_class = is_class
72
72
  self.is_enum = is_enum
73
73
 
74
- def safe_identifier(self, name: str, class_name: str = '') -> str:
75
- """Converts a name to a safe Java identifier"""
76
- if is_java_reserved_word(name):
77
- return f"_{name}"
78
- if class_name and name == class_name:
79
- return f"{name}_"
80
- return name
74
+ def safe_identifier(self, name: str, class_name: str = '', fallback_prefix: str = 'field') -> str:
75
+ """Converts a name to a safe Java identifier.
76
+
77
+ Handles:
78
+ - Reserved words (prepend _)
79
+ - Numeric prefixes (prepend _)
80
+ - Special characters (replace with _)
81
+ - All-special-char names (use fallback_prefix)
82
+ - Class name collision (append _)
83
+ """
84
+ import re
85
+ # Replace invalid characters with underscores
86
+ safe = re.sub(r'[^a-zA-Z0-9_]', '_', str(name))
87
+ # Remove leading/trailing underscores from sanitization
88
+ safe = safe.strip('_') if safe != name else safe
89
+ # If nothing left after removing special chars, use fallback
90
+ if not safe or not re.match(r'^[a-zA-Z_]', safe):
91
+ if safe and re.match(r'^[0-9]', safe):
92
+ safe = '_' + safe # Numeric prefix
93
+ else:
94
+ safe = fallback_prefix + '_' + (safe if safe else 'unnamed')
95
+ # Handle reserved words
96
+ if is_java_reserved_word(safe):
97
+ safe = '_' + safe
98
+ # Handle class name collision
99
+ if class_name and safe == class_name:
100
+ safe = safe + '_'
101
+ return safe
81
102
 
82
103
  def safe_package(self, packageName: str) -> str:
83
104
  """Converts a name to a safe Java identifier by checking each path segment"""
@@ -384,7 +405,12 @@ class StructureToJava:
384
405
  field_count=len(field_names)
385
406
  )
386
407
 
387
- class_definition = class_definition.rstrip() + equals_hashcode + "\n}\n"
408
+ # Generate createTestInstance() method for testing (only for non-abstract classes)
409
+ create_test_instance = ''
410
+ if not is_abstract:
411
+ create_test_instance = self.generate_create_test_instance_method(class_name, fields, schema_namespace)
412
+
413
+ class_definition = class_definition.rstrip() + create_test_instance + equals_hashcode + "\n}\n"
388
414
 
389
415
  if write_file:
390
416
  self.write_to_file(package, class_name, class_definition)
@@ -455,6 +481,86 @@ class StructureToJava:
455
481
  return str(value)
456
482
  return f"/* unsupported const value */"
457
483
 
484
+ def get_test_value(self, java_type: str) -> str:
485
+ """ Get a test value for a Java type """
486
+ # Handle arrays/lists
487
+ if java_type.startswith("List<") or java_type.startswith("ArrayList<"):
488
+ return "new java.util.ArrayList<>()"
489
+ if java_type.startswith("Map<"):
490
+ return "new java.util.HashMap<>()"
491
+ if java_type.endswith("[]"):
492
+ return f"new {java_type[:-2]}[0]"
493
+
494
+ # Primitive test values
495
+ test_values = {
496
+ 'String': '"test-string"',
497
+ 'string': '"test-string"',
498
+ 'int': '42',
499
+ 'Integer': '42',
500
+ 'long': '42L',
501
+ 'Long': '42L',
502
+ 'double': '3.14',
503
+ 'Double': '3.14',
504
+ 'float': '3.14f',
505
+ 'Float': '3.14f',
506
+ 'boolean': 'true',
507
+ 'Boolean': 'true',
508
+ 'byte': '(byte)0',
509
+ 'Byte': '(byte)0',
510
+ 'short': '(short)0',
511
+ 'Short': '(short)0',
512
+ 'BigInteger': 'java.math.BigInteger.ZERO',
513
+ 'BigDecimal': 'java.math.BigDecimal.ZERO',
514
+ 'byte[]': 'new byte[0]',
515
+ 'LocalDate': 'java.time.LocalDate.now()',
516
+ 'LocalTime': 'java.time.LocalTime.now()',
517
+ 'Instant': 'java.time.Instant.now()',
518
+ 'Duration': 'java.time.Duration.ZERO',
519
+ 'UUID': 'java.util.UUID.randomUUID()',
520
+ 'URI': 'java.net.URI.create("http://example.com")',
521
+ 'Object': 'new Object()',
522
+ 'void': 'null',
523
+ 'Void': 'null',
524
+ }
525
+
526
+ if java_type in test_values:
527
+ return test_values[java_type]
528
+
529
+ # Check if it's a generated type (enum or class)
530
+ if java_type in self.generated_types_java_package:
531
+ type_kind = self.generated_types_java_package[java_type]
532
+ if type_kind == "enum":
533
+ return f'{java_type}.values()[0]'
534
+ elif type_kind == "class":
535
+ return f'{java_type}.createTestInstance()'
536
+
537
+ # Default: try to instantiate
538
+ return f'new {java_type}()'
539
+
540
+ def generate_create_test_instance_method(self, class_name: str, fields: List[Dict], parent_package: str) -> str:
541
+ """ Generates a static createTestInstance method that creates a fully initialized instance """
542
+ method = f"\n{INDENT}/**\n{INDENT} * Creates a test instance with all required fields populated\n{INDENT} * @return a fully initialized test instance\n{INDENT} */\n"
543
+ method += f"{INDENT}public static {class_name} createTestInstance() {{\n"
544
+ method += f"{INDENT*2}{class_name} instance = new {class_name}();\n"
545
+
546
+ for field in fields:
547
+ # Skip const fields
548
+ if field.get('is_const', False):
549
+ continue
550
+
551
+ field_name = field['name']
552
+ field_type = field['type']
553
+
554
+ # Get a test value for this field
555
+ test_value = self.get_test_value(field_type)
556
+
557
+ # Setter name: set{Pascal(field_name)}
558
+ method += f"{INDENT*2}instance.set{pascal(field_name)}({test_value});\n"
559
+
560
+ method += f"{INDENT*2}return instance;\n"
561
+ method += f"{INDENT}}}\n"
562
+ return method
563
+
458
564
  def generate_enum(self, structure_schema: Dict, field_name: str, parent_package: str, write_file: bool) -> JavaType:
459
565
  """ Generates a Java enum from JSON Structure enum schema """
460
566
 
@@ -1,6 +1,7 @@
1
1
  """ {{ class_name }} dataclass. """
2
2
 
3
3
  # pylint: disable=too-many-lines, too-many-locals, too-many-branches, too-many-statements, too-many-arguments, line-too-long, wildcard-import
4
+ from __future__ import annotations
4
5
 
5
6
  {%- if avro_annotation or dataclasses_json_annotation %}
6
7
  import io
@@ -69,7 +70,7 @@ class {{ class_name }}{% if base_class or is_abstract %}({% if base_class %}{{ b
69
70
  {% endif %}
70
71
  {% for field in fields %}
71
72
  {%- set isdate = field.type == "datetime.datetime" or field.type == "typing.Optional[datetime.datetime]" %}
72
- {{ field.name }}: {{ field.type }}=dataclasses.field(kw_only=True{% if dataclasses_json_annotation %}, metadata=dataclasses_json.config(field_name="{{ field.original_name }}"{%- if isdate -%}, encoder=lambda d: datetime.datetime.isoformat(d) if d else None, decoder=lambda d:datetime.datetime.fromisoformat(d) if d else None, mm_field=fields.DateTime(format='iso'){%- endif -%}){%- endif %})
73
+ {{ field.name }}: {{ field.type }}=dataclasses.field(kw_only=True{% if dataclasses_json_annotation %}, metadata=dataclasses_json.config(field_name="{{ field.original_name }}"{%- if isdate -%}, encoder=lambda d: d.isoformat() if isinstance(d, datetime.datetime) else d if d else None, decoder=lambda d: datetime.datetime.fromisoformat(d) if isinstance(d, str) else d if d else None, mm_field=fields.DateTime(format='iso'){%- endif -%}){%- endif %})
73
74
  {%- endfor %}
74
75
 
75
76
  @classmethod