structurize 3.5.11__tar.gz → 3.6.0__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 (93) hide show
  1. {structurize-3.5.11/structurize.egg-info → structurize-3.6.0}/PKG-INFO +1 -1
  2. {structurize-3.5.11 → structurize-3.6.0}/avrotize/_version.py +3 -3
  3. {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotojsons.py +16 -4
  4. {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotojstruct.py +10 -0
  5. {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotokusto.py +32 -26
  6. {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotopython.py +38 -3
  7. {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotots.py +21 -5
  8. {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrovalidator.py +27 -0
  9. {structurize-3.5.11 → structurize-3.6.0}/avrotize/common.py +46 -0
  10. {structurize-3.5.11 → structurize-3.6.0}/avrotize/dependencies/typescript/node22/package.json +1 -1
  11. {structurize-3.5.11 → structurize-3.6.0}/avrotize/jsonstoavro.py +16 -6
  12. {structurize-3.5.11 → structurize-3.6.0}/avrotize/jstructtoavro.py +111 -30
  13. {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretocpp.py +14 -7
  14. {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretocsharp.py +13 -11
  15. {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretogo.py +6 -5
  16. {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretojava.py +6 -5
  17. {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretokusto.py +33 -27
  18. {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretopython.py +7 -6
  19. {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretorust.py +4 -5
  20. {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretots.py +4 -3
  21. {structurize-3.5.11 → structurize-3.6.0/structurize.egg-info}/PKG-INFO +1 -1
  22. {structurize-3.5.11 → structurize-3.6.0}/.gitignore +0 -0
  23. {structurize-3.5.11 → structurize-3.6.0}/LICENSE +0 -0
  24. {structurize-3.5.11 → structurize-3.6.0}/MANIFEST.in +0 -0
  25. {structurize-3.5.11 → structurize-3.6.0}/README.md +0 -0
  26. {structurize-3.5.11 → structurize-3.6.0}/avrotize/__init__.py +0 -0
  27. {structurize-3.5.11 → structurize-3.6.0}/avrotize/__main__.py +0 -0
  28. {structurize-3.5.11 → structurize-3.6.0}/avrotize/asn1toavro.py +0 -0
  29. {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotize.py +0 -0
  30. {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotocpp.py +0 -0
  31. {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotocsharp.py +0 -0
  32. {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotocsv.py +0 -0
  33. {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotodatapackage.py +0 -0
  34. {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotodb.py +0 -0
  35. {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotogo.py +0 -0
  36. {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotographql.py +0 -0
  37. {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotoiceberg.py +0 -0
  38. {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotojava.py +0 -0
  39. {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotojs.py +0 -0
  40. {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotomd.py +0 -0
  41. {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotools.py +0 -0
  42. {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotoparquet.py +0 -0
  43. {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotoproto.py +0 -0
  44. {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotorust.py +0 -0
  45. {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrototsml.py +0 -0
  46. {structurize-3.5.11 → structurize-3.6.0}/avrotize/avrotoxsd.py +0 -0
  47. {structurize-3.5.11 → structurize-3.6.0}/avrotize/cddltostructure.py +0 -0
  48. {structurize-3.5.11 → structurize-3.6.0}/avrotize/choice_inference.py +0 -0
  49. {structurize-3.5.11 → structurize-3.6.0}/avrotize/commands.json +0 -0
  50. {structurize-3.5.11 → structurize-3.6.0}/avrotize/constants.py +0 -0
  51. {structurize-3.5.11 → structurize-3.6.0}/avrotize/csvtoavro.py +0 -0
  52. {structurize-3.5.11 → structurize-3.6.0}/avrotize/datapackagetoavro.py +0 -0
  53. {structurize-3.5.11 → structurize-3.6.0}/avrotize/dependencies/cpp/vcpkg/vcpkg.json +0 -0
  54. {structurize-3.5.11 → structurize-3.6.0}/avrotize/dependency_resolver.py +0 -0
  55. {structurize-3.5.11 → structurize-3.6.0}/avrotize/dependency_version.py +0 -0
  56. {structurize-3.5.11 → structurize-3.6.0}/avrotize/jsonstostructure.py +0 -0
  57. {structurize-3.5.11 → structurize-3.6.0}/avrotize/jsontoschema.py +0 -0
  58. {structurize-3.5.11 → structurize-3.6.0}/avrotize/kstructtoavro.py +0 -0
  59. {structurize-3.5.11 → structurize-3.6.0}/avrotize/kustotoavro.py +0 -0
  60. {structurize-3.5.11 → structurize-3.6.0}/avrotize/kustotojstruct.py +0 -0
  61. {structurize-3.5.11 → structurize-3.6.0}/avrotize/mcp_server.py +0 -0
  62. {structurize-3.5.11 → structurize-3.6.0}/avrotize/openapitostructure.py +0 -0
  63. {structurize-3.5.11 → structurize-3.6.0}/avrotize/parquettoavro.py +0 -0
  64. {structurize-3.5.11 → structurize-3.6.0}/avrotize/proto2parser.py +0 -0
  65. {structurize-3.5.11 → structurize-3.6.0}/avrotize/proto3parser.py +0 -0
  66. {structurize-3.5.11 → structurize-3.6.0}/avrotize/prototoavro.py +0 -0
  67. {structurize-3.5.11 → structurize-3.6.0}/avrotize/schema_inference.py +0 -0
  68. {structurize-3.5.11 → structurize-3.6.0}/avrotize/sqltoavro.py +0 -0
  69. {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretocddl.py +0 -0
  70. {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretocsv.py +0 -0
  71. {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretodatapackage.py +0 -0
  72. {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretodb.py +0 -0
  73. {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretographql.py +0 -0
  74. {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretoiceberg.py +0 -0
  75. {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretojs.py +0 -0
  76. {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretojsons.py +0 -0
  77. {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretomd.py +0 -0
  78. {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretoproto.py +0 -0
  79. {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretotsml.py +0 -0
  80. {structurize-3.5.11 → structurize-3.6.0}/avrotize/structuretoxsd.py +0 -0
  81. {structurize-3.5.11 → structurize-3.6.0}/avrotize/tmslvalidate.py +0 -0
  82. {structurize-3.5.11 → structurize-3.6.0}/avrotize/validate.py +0 -0
  83. {structurize-3.5.11 → structurize-3.6.0}/avrotize/xmltoschema.py +0 -0
  84. {structurize-3.5.11 → structurize-3.6.0}/avrotize/xsdtoavro.py +0 -0
  85. {structurize-3.5.11 → structurize-3.6.0}/build.ps1 +0 -0
  86. {structurize-3.5.11 → structurize-3.6.0}/build.sh +0 -0
  87. {structurize-3.5.11 → structurize-3.6.0}/pyproject.toml +0 -0
  88. {structurize-3.5.11 → structurize-3.6.0}/setup.cfg +0 -0
  89. {structurize-3.5.11 → structurize-3.6.0}/structurize.egg-info/SOURCES.txt +0 -0
  90. {structurize-3.5.11 → structurize-3.6.0}/structurize.egg-info/dependency_links.txt +0 -0
  91. {structurize-3.5.11 → structurize-3.6.0}/structurize.egg-info/entry_points.txt +0 -0
  92. {structurize-3.5.11 → structurize-3.6.0}/structurize.egg-info/requires.txt +0 -0
  93. {structurize-3.5.11 → structurize-3.6.0}/structurize.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: structurize
3
- Version: 3.5.11
3
+ Version: 3.6.0
4
4
  Summary: Tools to convert from and to JSON Structure from various other schema languages.
5
5
  Author-email: Clemens Vasters <clemensv@microsoft.com>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '3.5.11'
22
- __version_tuple__ = version_tuple = (3, 5, 11)
21
+ __version__ = version = '3.6.0'
22
+ __version_tuple__ = version_tuple = (3, 6, 0)
23
23
 
24
- __commit_id__ = commit_id = 'g9e1128f35'
24
+ __commit_id__ = commit_id = 'g1329b8c45'
@@ -98,22 +98,34 @@ class AvroToJsonSchemaConverter:
98
98
  json_type['type'] = 'integer'
99
99
  json_type['format'] = 'int64'
100
100
  return json_type
101
- # Avrotize schema extensions: string-based logical types
102
- elif base_type == 'string' and logical_type == 'date':
101
+ # Avrotize schema extensions: string-based logical types.
102
+ # The rfc3339-* family is the current emission; the bare reserved
103
+ # names on a string base are still recognized for backward compat.
104
+ elif base_type == 'string' and logical_type in ['date', 'rfc3339-date']:
103
105
  # Avrotize extension: string with date logicalType
104
106
  json_type['type'] = 'string'
105
107
  json_type['format'] = 'date'
106
108
  return json_type
107
- elif base_type == 'string' and logical_type in ['timestamp-millis', 'timestamp-micros', 'datetime']:
109
+ elif base_type == 'string' and logical_type in [
110
+ 'timestamp-millis', 'timestamp-micros', 'datetime',
111
+ 'rfc3339-timestamp-millis', 'rfc3339-timestamp-micros',
112
+ 'rfc3339-local-timestamp-millis', 'rfc3339-local-timestamp-micros']:
108
113
  # Avrotize extension: string with datetime logicalType
109
114
  json_type['type'] = 'string'
110
115
  json_type['format'] = 'date-time'
111
116
  return json_type
112
- elif base_type == 'string' and logical_type in ['time-millis', 'time-micros', 'time']:
117
+ elif base_type == 'string' and logical_type in [
118
+ 'time-millis', 'time-micros', 'time',
119
+ 'rfc3339-time-millis', 'rfc3339-time-micros']:
113
120
  # Avrotize extension: string with time logicalType
114
121
  json_type['type'] = 'string'
115
122
  json_type['format'] = 'time'
116
123
  return json_type
124
+ elif base_type == 'string' and logical_type in ['duration', 'rfc3339-duration']:
125
+ # Avrotize extension: string with duration logicalType (RFC 3339)
126
+ json_type['type'] = 'string'
127
+ json_type['format'] = 'duration'
128
+ return json_type
117
129
  elif logical_type == 'decimal':
118
130
  json_type['type'] = 'number'
119
131
  return json_type
@@ -305,6 +305,16 @@ class AvroToJsonStructure:
305
305
  "timestamp-millis": {"type": "int64", "logicalType": "timestampMillis"},
306
306
  "date": {"type": "int32", "logicalType": "date"},
307
307
  "uuid": {"type": "string", "format": "uuid"},
308
+ # Avrotize Schema rfc3339-* string temporal family -> JSON Structure native
309
+ # temporal types (round-trips with jstructtoavro). See issue #335.
310
+ "rfc3339-date": {"type": "date"},
311
+ "rfc3339-time-millis": {"type": "time"},
312
+ "rfc3339-time-micros": {"type": "time"},
313
+ "rfc3339-timestamp-millis": {"type": "datetime"},
314
+ "rfc3339-timestamp-micros": {"type": "datetime"},
315
+ "rfc3339-local-timestamp-millis": {"type": "datetime"},
316
+ "rfc3339-local-timestamp-micros": {"type": "datetime"},
317
+ "rfc3339-duration": {"type": "duration"},
308
318
  }
309
319
  return mapping.get(logical_type, {"type": "string"})
310
320
 
@@ -7,6 +7,16 @@ from avrotize.common import build_flat_type_dict, inline_avro_references, strip_
7
7
  from azure.kusto.data import KustoClient, KustoConnectionStringBuilder, ClientRequestProperties
8
8
 
9
9
 
10
+ def kusto_string_literal(value: str) -> str:
11
+ """Return a KQL verbatim string literal for a raw Python string."""
12
+ return '@"' + value.replace('"', '""') + '"'
13
+
14
+
15
+ def kusto_json_literal(value: Any) -> str:
16
+ """Return a KQL string literal containing one JSON encoding of value."""
17
+ return kusto_string_literal(json.dumps(value))
18
+
19
+
10
20
  class AvroToKusto:
11
21
  """Converts an Avro schema to a Kusto table schema."""
12
22
 
@@ -60,9 +70,9 @@ class AvroToKusto:
60
70
  if "doc" in recordschema:
61
71
  doc_data = recordschema["doc"]
62
72
  doc_data = (doc_data[:997] + "...") if len(doc_data) > 1000 else doc_data
63
- doc_string = json.dumps(json.dumps({
73
+ doc_string = kusto_json_literal({
64
74
  "description": doc_data
65
- }))
75
+ })
66
76
  kusto.append(
67
77
  f".alter table {table_ref} docstring {doc_string};")
68
78
  kusto.append("")
@@ -89,7 +99,7 @@ class AvroToKusto:
89
99
  doc_content["schema"] = inline_schema
90
100
  else:
91
101
  doc_content["schema"] = inline_schema
92
- doc = json.dumps(json.dumps(doc_content))
102
+ doc = kusto_json_literal(doc_content)
93
103
  doc_string_statement.append(f" [{column_name}]: {doc}")
94
104
  if doc_string_statement and emit_cloudevents_columns:
95
105
  doc_string_statement.extend([
@@ -109,15 +119,13 @@ class AvroToKusto:
109
119
  # .create-or-alter table dfl_data_events ingestion json mapping
110
120
  kusto.append(
111
121
  f".create-or-alter table {table_ref} ingestion json mapping \"{mapping_base}_json_flat\"")
112
- kusto.append("```\n[")
122
+ mapping_entries = []
113
123
  if emit_cloudevents_columns:
114
- kusto.append(" {\"column\": \"___type\", \"path\": \"$.type\"},")
115
- kusto.append(
116
- " {\"column\": \"___source\", \"path\": \"$.source\"},")
117
- kusto.append(" {\"column\": \"___id\", \"path\": \"$.id\"},")
118
- kusto.append(" {\"column\": \"___time\", \"path\": \"$.time\"},")
119
- kusto.append(
120
- " {\"column\": \"___subject\", \"path\": \"$.subject\"},")
124
+ mapping_entries.append(" {\"column\": \"___type\", \"path\": \"$.type\"}")
125
+ mapping_entries.append(" {\"column\": \"___source\", \"path\": \"$.source\"}")
126
+ mapping_entries.append(" {\"column\": \"___id\", \"path\": \"$.id\"}")
127
+ mapping_entries.append(" {\"column\": \"___time\", \"path\": \"$.time\"}")
128
+ mapping_entries.append(" {\"column\": \"___subject\", \"path\": \"$.subject\"}")
121
129
  for field in fields:
122
130
  json_name = column_name = field["name"]
123
131
  if 'altnames' in field:
@@ -125,21 +133,19 @@ class AvroToKusto:
125
133
  column_name = field['altnames']['kql']
126
134
  if 'json' in field['altnames']:
127
135
  json_name = field['altnames']['json']
128
- kusto.append(
129
- f" {{\"column\": \"{column_name}\", \"path\": \"$.{json_name}\"}},")
130
- kusto.append("]\n```\n\n")
136
+ mapping_entries.append(
137
+ f" {{\"column\": \"{column_name}\", \"path\": \"$.{json_name}\"}}")
138
+ kusto.append("```\n[\n" + ",\n".join(mapping_entries) + "\n]\n```\n\n")
131
139
 
132
140
  if emit_cloudevents_columns:
133
141
  kusto.append(
134
142
  f".create-or-alter table {table_ref} ingestion json mapping \"{mapping_base}_json_ce_structured\"")
135
- kusto.append("```\n[")
136
- kusto.append(" {\"column\": \"___type\", \"path\": \"$.type\"},")
137
- kusto.append(
138
- " {\"column\": \"___source\", \"path\": \"$.source\"},")
139
- kusto.append(" {\"column\": \"___id\", \"path\": \"$.id\"},")
140
- kusto.append(" {\"column\": \"___time\", \"path\": \"$.time\"},")
141
- kusto.append(
142
- " {\"column\": \"___subject\", \"path\": \"$.subject\"},")
143
+ ce_entries = []
144
+ ce_entries.append(" {\"column\": \"___type\", \"path\": \"$.type\"}")
145
+ ce_entries.append(" {\"column\": \"___source\", \"path\": \"$.source\"}")
146
+ ce_entries.append(" {\"column\": \"___id\", \"path\": \"$.id\"}")
147
+ ce_entries.append(" {\"column\": \"___time\", \"path\": \"$.time\"}")
148
+ ce_entries.append(" {\"column\": \"___subject\", \"path\": \"$.subject\"}")
143
149
  for field in fields:
144
150
  json_name = column_name = field["name"]
145
151
  if 'altnames' in field:
@@ -147,9 +153,9 @@ class AvroToKusto:
147
153
  column_name = field['altnames']['kql']
148
154
  if 'json' in field['altnames']:
149
155
  json_name = field['altnames']['json']
150
- kusto.append(
151
- f" {{\"column\": \"{column_name}\", \"path\": \"$.data.{json_name}\"}},")
152
- kusto.append("]\n```\n\n")
156
+ ce_entries.append(
157
+ f" {{\"column\": \"{column_name}\", \"path\": \"$.data.{json_name}\"}}")
158
+ kusto.append("```\n[\n" + ",\n".join(ce_entries) + "\n]\n```\n\n")
153
159
 
154
160
  if emit_cloudevents_columns:
155
161
  kusto.append(
@@ -186,7 +192,7 @@ class AvroToKusto:
186
192
  kusto.append(
187
193
  f" \"Query\": \"{query}\",")
188
194
  kusto.append(" \"IsTransactional\": false,")
189
- kusto.append(" \"PropagateIngestionProperties\": true,")
195
+ kusto.append(" \"PropagateIngestionProperties\": true")
190
196
  kusto.append("}]")
191
197
  kusto.append("```\n")
192
198
 
@@ -90,6 +90,38 @@ class AvroToPython:
90
90
  return name + "_"
91
91
  return name
92
92
 
93
+ def safe_enum_symbols(self, symbols: List[str]) -> List[Dict[str, str]]:
94
+ """Converts Avro enum symbols to collision-safe Python enum members."""
95
+ enum_symbols = []
96
+ used_names = set()
97
+
98
+ for symbol in symbols:
99
+ if symbol.isidentifier() and not is_python_reserved_word(symbol):
100
+ member_name = symbol
101
+ else:
102
+ member_name = re.sub(r'[^0-9A-Za-z_]', '_', symbol).strip('_').upper()
103
+ if not member_name:
104
+ member_name = "VALUE"
105
+ if member_name[0].isdigit():
106
+ member_name = f"VALUE_{member_name}"
107
+ if is_python_reserved_word(symbol) or is_python_reserved_word(member_name.lower()) or is_python_reserved_word(member_name):
108
+ member_name = f"{member_name}_"
109
+
110
+ base_member_name = member_name
111
+ suffix = 2
112
+ while member_name in used_names or not member_name.isidentifier() or is_python_reserved_word(member_name):
113
+ member_name = f"{base_member_name}_{suffix}"
114
+ suffix += 1
115
+
116
+ used_names.add(member_name)
117
+ enum_symbols.append({
118
+ 'name': member_name,
119
+ 'value': symbol,
120
+ 'value_literal': repr(symbol)
121
+ })
122
+
123
+ return enum_symbols
124
+
93
125
  def pascal_type_name(self, ref: str) -> str:
94
126
  """Converts a reference to a type name"""
95
127
  return '_'.join([pascal(part) for part in ref.split('.')[-1].split('_')])
@@ -350,9 +382,12 @@ class AvroToPython:
350
382
  if python_qualified_name in self.generated_types:
351
383
  return python_qualified_name
352
384
 
353
- symbols = [symbol if not is_python_reserved_word(
354
- symbol) else symbol + "_" for symbol in avro_schema.get('symbols', [])]
355
- ordinals = avro_schema.get('ordinals', {})
385
+ symbols = self.safe_enum_symbols(avro_schema.get('symbols', []))
386
+ symbol_names_by_value = {symbol['value']: symbol['name'] for symbol in symbols}
387
+ ordinals = {
388
+ symbol_names_by_value[symbol]: ordinal
389
+ for symbol, ordinal in avro_schema.get('ordinals', {}).items()
390
+ }
356
391
 
357
392
  enum_definition = process_template(
358
393
  "avrotopython/enum_core.jinja",
@@ -16,7 +16,6 @@ def is_typescript_reserved_word(word: str) -> bool:
16
16
  'for', 'function', 'if', 'import', 'in', 'instanceof', 'new', 'return',
17
17
  'super', 'switch', 'this', 'throw', 'try', 'typeof', 'var', 'void',
18
18
  'while', 'with', 'yield', 'enum', 'string', 'number', 'boolean', 'symbol',
19
- 'type', 'namespace', 'module', 'declare', 'abstract', 'readonly',
20
19
  ]
21
20
  return word in reserved_words
22
21
 
@@ -32,6 +31,7 @@ class AvroToTypeScript:
32
31
  self.src_dir = os.path.join(self.output_dir, "src")
33
32
  self.generated_types: Dict[str, str] = {}
34
33
  self.generated_type_fields: Dict[str, List[Dict]] = {} # Store fields for test generation
34
+ self.generated_type_imports: Dict[str, List[str]] = {} # Store import type FQNs for test generation
35
35
  self.main_schema = None
36
36
  self.type_dict = None
37
37
  self.INDENT = ' ' * 4
@@ -207,6 +207,10 @@ class AvroToTypeScript:
207
207
  self.write_to_file(namespace, class_name, class_definition)
208
208
  self.generated_types[ts_qualified_name] = 'class'
209
209
  self.generated_type_fields[ts_qualified_name] = fields # Store fields for test generation
210
+ # Store the fully-qualified import names so generated test files can resolve
211
+ # sub-namespaced types (e.g. enums placed in a "<Record>_types" namespace)
212
+ # to their real source path instead of a flattened, non-existent one.
213
+ self.generated_type_imports[ts_qualified_name] = sorted(import_types)
210
214
  return ts_qualified_name
211
215
 
212
216
  def generate_enum(self, avro_schema: Dict, parent_namespace: str, write_file: bool = True) -> str:
@@ -670,17 +674,29 @@ class AvroToTypeScript:
670
674
 
671
675
  fields = self.generated_type_fields.get(qualified_name, [])
672
676
 
673
- # Build imports for nested types
677
+ # Build imports for nested types. Reuse the fully-qualified import names
678
+ # captured during class generation so that types living in sub-namespaces
679
+ # (e.g. enums placed in a "<Record>_types" namespace) resolve to their real
680
+ # source path instead of a flattened, non-existent one (issue #338).
674
681
  imports_with_paths: Dict[str, str] = {}
682
+ import_fqns = self.generated_type_imports.get(qualified_name, [])
683
+ path_by_leaf: Dict[str, str] = {}
684
+ for fqn in import_fqns:
685
+ fqn_parts = fqn.split('.')
686
+ leaf = pascal(fqn_parts[-1])
687
+ path_by_leaf[leaf] = '../src/' + '/'.join(fqn_parts) + '.js'
675
688
  for field in fields:
676
689
  field_type = field.get('type_no_null', '')
677
690
  if not self.is_typescript_primitive(field_type.replace('[]', '')):
678
691
  # It's a reference type, need to import it
679
692
  type_name = field_type.replace('[]', '').replace('?', '')
680
693
  if type_name and type_name not in ['null', 'any', 'Date']:
681
- # Build relative path from test dir to src dir
682
- src_path = '/'.join(parts)
683
- imports_with_paths[type_name] = f'../src/{src_path.rsplit("/", 1)[0]}/{type_name}.js' if '/' in src_path else f'../src/{parts[0]}/{type_name}.js'
694
+ if type_name in path_by_leaf:
695
+ imports_with_paths[type_name] = path_by_leaf[type_name]
696
+ else:
697
+ # Fallback: best-effort path relative to the class namespace
698
+ src_path = '/'.join(parts)
699
+ imports_with_paths[type_name] = f'../src/{src_path.rsplit("/", 1)[0]}/{type_name}.js' if '/' in src_path else f'../src/{parts[0]}/{type_name}.js'
684
700
 
685
701
  # Calculate relative path from test directory to class file
686
702
  class_path_parts = namespace.split('.') if namespace else []
@@ -414,6 +414,33 @@ class AvroValidator:
414
414
  """
415
415
  base_type = schema.get('type')
416
416
 
417
+ # Avrotize Schema rfc3339-* string temporal family (issue #335). These are
418
+ # valid standard Avro because rfc3339-* is not a reserved Avro logical type;
419
+ # the value is an RFC 3339 textual string on a string base.
420
+ if isinstance(logical_type, str) and logical_type.startswith('rfc3339-'):
421
+ if base_type != 'string':
422
+ raise AvroValidationError(
423
+ f"{logical_type} logical type requires string base type", path)
424
+ if not isinstance(instance, str):
425
+ raise AvroValidationError(
426
+ f"Expected string for {logical_type}, got {type(instance).__name__}", path)
427
+ core = logical_type[len('rfc3339-'):]
428
+ if core == 'date':
429
+ pattern = self.DATE_PATTERN
430
+ elif core in ('time-millis', 'time-micros'):
431
+ pattern = self.TIME_PATTERN
432
+ elif core in ('timestamp-millis', 'timestamp-micros',
433
+ 'local-timestamp-millis', 'local-timestamp-micros'):
434
+ pattern = self.DATETIME_PATTERN
435
+ elif core == 'duration':
436
+ pattern = self.DURATION_PATTERN
437
+ else:
438
+ pattern = None
439
+ if pattern is not None and not pattern.match(instance):
440
+ raise AvroValidationError(
441
+ f"Invalid {logical_type} format: {instance}", path)
442
+ return
443
+
417
444
  if logical_type == 'decimal':
418
445
  self._validate_decimal(instance, schema, path)
419
446
 
@@ -830,6 +830,52 @@ def altname(schema_obj: dict, purpose: str):
830
830
  return schema_obj["name"]
831
831
 
832
832
 
833
+ def json_wire_name(prop_name: str, prop_schema: Any) -> str:
834
+ """
835
+ Resolve the JSON wire key for a property, honoring JSON Structure ``altnames.json``.
836
+
837
+ JSON Structure requires property keys to be identifiers. A non-identifier wire key
838
+ (e.g. ``dr-type``) is modeled as an identifier property name (``dr_type``) carrying
839
+ ``altnames: {"json": "dr-type"}``. The ``json`` purpose is the canonical wire name.
840
+
841
+ Args:
842
+ prop_name (str): The property name (identifier) used as the schema key.
843
+ prop_schema (Any): The property schema; may carry an ``altnames`` map.
844
+
845
+ Returns:
846
+ str: ``altnames.json`` when present, otherwise ``prop_name``.
847
+ """
848
+ if isinstance(prop_schema, dict):
849
+ altnames = prop_schema.get("altnames")
850
+ if isinstance(altnames, dict) and "json" in altnames:
851
+ return altnames["json"]
852
+ return prop_name
853
+
854
+
855
+ def json_enum_wire_value(value: Any, enum_schema: Any) -> str:
856
+ """
857
+ Resolve the JSON wire string for an enum value, honoring JSON Structure ``altenums.json``.
858
+
859
+ ``altenums.json`` is a map keyed by the original enum value; the language member name
860
+ stays derived from the original value, while the wire value uses the mapping when present.
861
+ Unmapped values serialize verbatim (identity fallback).
862
+
863
+ Args:
864
+ value: The original enum value (schema key).
865
+ enum_schema (Any): The enum schema; may carry an ``altenums`` map.
866
+
867
+ Returns:
868
+ str: ``altenums.json[value]`` when present, otherwise ``str(value)``.
869
+ """
870
+ if isinstance(enum_schema, dict):
871
+ altenums = enum_schema.get("altenums")
872
+ if isinstance(altenums, dict):
873
+ j = altenums.get("json")
874
+ if isinstance(j, dict) and str(value) in j:
875
+ return j[str(value)]
876
+ return str(value)
877
+
878
+
833
879
  def process_template(file_path: str, **kvargs) -> str:
834
880
  """
835
881
  Process a file as a Jinja2 template with the given object as input.
@@ -8,7 +8,7 @@
8
8
  "kafkajs": "^2.2.4"
9
9
  },
10
10
  "devDependencies": {
11
- "@types/node": "^25.0.3",
11
+ "@types/node": "^26.0.0",
12
12
  "typescript": "^6.0.2",
13
13
  "jest": "^30.2.0",
14
14
  "@types/jest": "^30.0.0"
@@ -591,14 +591,16 @@ class JsonToAvroConverter:
591
591
 
592
592
  # if you've got { 'type': 'string', 'format': ['date-time', 'duration'] }, I'm sorry
593
593
  if format and isinstance(format, str):
594
- if format in ('date-time', 'date'):
594
+ if format == 'date-time':
595
+ avro_primitive = {'type': 'long', 'logicalType': 'timestamp-millis'}
596
+ elif format == 'date':
595
597
  avro_primitive = {'type': 'int', 'logicalType': 'date'}
596
- elif format in ('time'):
598
+ elif format == 'time':
597
599
  avro_primitive = {'type': 'int', 'logicalType': 'time-millis'}
598
- elif format in ('duration'):
600
+ elif format == 'duration':
599
601
  avro_primitive = {'type': 'fixed',
600
602
  'size': 12, 'logicalType': 'duration'}
601
- elif format in ('uuid'):
603
+ elif format == 'uuid':
602
604
  avro_primitive = {'type': 'string', 'logicalType': 'uuid'}
603
605
 
604
606
  return avro_primitive
@@ -1357,7 +1359,14 @@ class JsonToAvroConverter:
1357
1359
  if isinstance(avro_type, dict) and 'type' in avro_type and (isinstance(avro_type, list) or not avro_type['type'] in ['array', 'map', 'record', 'enum', 'fixed']):
1358
1360
  if 'dependencies' in avro_type:
1359
1361
  dependencies.extend(avro_type['dependencies'])
1360
- avro_type = avro_type['type']
1362
+ # Preserve logical-type annotations (e.g. timestamp-millis, date,
1363
+ # time-millis, uuid). Collapsing these to the bare base type would
1364
+ # silently discard the logical meaning (and, for date-time, the
1365
+ # required 64-bit width). See issue #337.
1366
+ if 'logicalType' in avro_type:
1367
+ avro_type = {k: v for k, v in avro_type.items() if k != 'dependencies'}
1368
+ else:
1369
+ avro_type = avro_type['type']
1361
1370
  return avro_type
1362
1371
 
1363
1372
  def register_type(self, avro_schema, avro_type) -> bool:
@@ -1733,8 +1742,9 @@ class JsonToAvroConverter:
1733
1742
  # None type is a problem
1734
1743
  raise ValueError(
1735
1744
  f"avro_field_type is None for field {field_name}")
1736
- if isinstance(avro_field_type, dict) and 'type' in avro_field_type and not self.is_avro_complex_type(avro_field_type):
1745
+ if isinstance(avro_field_type, dict) and 'type' in avro_field_type and 'logicalType' not in avro_field_type and not self.is_avro_complex_type(avro_field_type):
1737
1746
  # if the field type is a basic type, inline it
1747
+ # (logical types are kept intact, see issue #337)
1738
1748
  avro_field_type = avro_field_type['type']
1739
1749
  field_type_list.append(avro_field_type)
1740
1750
  field_ref_type_list.append(avro_field_ref_type)