localstack-py-avro-schema 3.9.2__py3-none-any.whl → 3.9.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: localstack-py-avro-schema
3
- Version: 3.9.2
3
+ Version: 3.9.4
4
4
  Summary: Generate Apache Avro schemas for Python types including standard library data-classes and Pydantic data models.
5
5
  Author-email: LocalStack Contributors <info@localstack.cloud>, "J.P. Morgan Chase & Co." <open_source@jpmorgan.com>
6
6
  License: Apache License
@@ -1,12 +1,12 @@
1
- localstack_py_avro_schema-3.9.2.dist-info/licenses/LICENSE,sha256=zE1N4JILDTkSIDtdmqdnKKxKEQh_VdqeoAV2230eNOI,10141
2
- localstack_py_avro_schema-3.9.2.dist-info/licenses/LICENSE_HEADER.txt,sha256=Nx3RGmYbJKjIKM8Y2yMrCPJgCw4oBt_H62PxDKlAsto,564
1
+ localstack_py_avro_schema-3.9.4.dist-info/licenses/LICENSE,sha256=zE1N4JILDTkSIDtdmqdnKKxKEQh_VdqeoAV2230eNOI,10141
2
+ localstack_py_avro_schema-3.9.4.dist-info/licenses/LICENSE_HEADER.txt,sha256=Nx3RGmYbJKjIKM8Y2yMrCPJgCw4oBt_H62PxDKlAsto,564
3
3
  py_avro_schema/__init__.py,sha256=bK6hTw4XJy5bAP4lgIvJR5LHtEiyUWq4GNJ5w-uSEpc,2414
4
4
  py_avro_schema/_alias.py,sha256=nCQqtn7IbpjV0ibpJf9aMX9FvwVx2EDC1iKzwjQ7CqI,3412
5
- py_avro_schema/_schemas.py,sha256=I5otHWPh8tcws3ey4iwsx7c0Zz1VNLmMTabTh43E-7s,46993
5
+ py_avro_schema/_schemas.py,sha256=L9_c-I1ihS9BJpXe1-foDMUppxY7PyTT544hMbrS1TM,48589
6
6
  py_avro_schema/_testing.py,sha256=3aSfMbNDDeatl3H7GEUcynxK81HFiF-WCLOZ3FCCLiw,2261
7
7
  py_avro_schema/_typing.py,sha256=9kBlPA7C33zZP3nRhCvSJA_0m54uvUo0AQ4wJJmSxMk,3291
8
8
  py_avro_schema/py.typed,sha256=HnEDkaznpgfRW07Qfogy4tFLu_4dcQ5YcOsI7pmU5rQ,52
9
- localstack_py_avro_schema-3.9.2.dist-info/METADATA,sha256=6K-dy4M5Pj2-uoHcRF15vZVRz7pfiibgQcAnysW4YKY,15164
10
- localstack_py_avro_schema-3.9.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
- localstack_py_avro_schema-3.9.2.dist-info/top_level.txt,sha256=0t4oO_5rsdfZGSk8iSzK4TjdyfVz-RE1IZ2TIz6YvI0,15
12
- localstack_py_avro_schema-3.9.2.dist-info/RECORD,,
9
+ localstack_py_avro_schema-3.9.4.dist-info/METADATA,sha256=2VBFpx5j-KsRbwOmtIICNMNLSgB34kwkCra0Jh_WO5U,15164
10
+ localstack_py_avro_schema-3.9.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
+ localstack_py_avro_schema-3.9.4.dist-info/top_level.txt,sha256=0t4oO_5rsdfZGSk8iSzK4TjdyfVz-RE1IZ2TIz6YvI0,15
12
+ localstack_py_avro_schema-3.9.4.dist-info/RECORD,,
@@ -45,9 +45,11 @@ from typing import (
45
45
  get_type_hints,
46
46
  )
47
47
 
48
+ import avro.name
48
49
  import more_itertools
49
50
  import orjson
50
51
  import typeguard
52
+ from avro.errors import InvalidName
51
53
 
52
54
  import py_avro_schema._typing
53
55
  from py_avro_schema._alias import get_aliases, get_field_aliases_and_actual_type
@@ -135,9 +137,10 @@ JSON_OPTIONS = [opt for opt in Option if opt.name and opt.name.startswith("JSON_
135
137
  _SCHEMA_CLASSES = []
136
138
 
137
139
 
138
- def register_schema(cls):
140
+ def register_schema(cls: type | None = None, *, priority: int = 0):
139
141
  """
140
142
  Decorator to register a class as a known ``Schema``
143
+ It also accept a priority value to sort the list of schemas. Default schemas have priority 0.
141
144
 
142
145
  Schema classes are instantiated when calling ``schema``. Example use::
143
146
 
@@ -147,10 +150,15 @@ def register_schema(cls):
147
150
  def handles_type(cls, py_type: Type) -> bool:
148
151
  ...
149
152
  ...
150
-
151
153
  """
152
- _SCHEMA_CLASSES.append(cls)
153
- return cls
154
+
155
+ def _wrapper(_cls):
156
+ """Wrapper function to attach priority and sort the list of schemas."""
157
+ _cls.__py_avro_priority = priority
158
+ _SCHEMA_CLASSES.append(_cls)
159
+ return _cls
160
+
161
+ return _wrapper if not cls else _wrapper(cls)
154
162
 
155
163
 
156
164
  def schema(
@@ -186,7 +194,7 @@ def _schema_obj(py_type: Type, namespace: Optional[str] = None, options: Option
186
194
  :param options: Schema generation options.
187
195
  """
188
196
  # Find concrete Schema subclasses defined in the current module
189
- for schema_class in _SCHEMA_CLASSES:
197
+ for schema_class in sorted(_SCHEMA_CLASSES, key=lambda c: getattr(c, "__py_avro_priority", 0)):
190
198
  # Find the first schema class that handles py_type
191
199
  schema_obj = schema_class(py_type, namespace=namespace, options=options) # type: ignore
192
200
  if schema_obj:
@@ -843,19 +851,33 @@ class EnumSchema(NamedSchema):
843
851
  if symbol_types != {str}:
844
852
  raise TypeError(f"Avro enum schema members must be strings. {py_type} uses {symbol_types} values.")
845
853
 
854
+ def _is_valid_enum(self) -> bool:
855
+ """Checks if all the symbols of the enum are valid Avro names."""
856
+ try:
857
+ for _symbol in self.symbols:
858
+ avro.name.validate_basename(_symbol)
859
+ except InvalidName:
860
+ return False
861
+ return True
862
+
846
863
  def data_before_deduplication(self, names: NamesType) -> JSONObj:
847
864
  """Return the schema data"""
848
- enum_schema = {
849
- "type": "enum",
850
- "name": self.name,
851
- "symbols": self.symbols,
852
- # This is the default for the enum, not the default value for a record field using the enum type! See Avro
853
- # schema specification for use. For now, we force the default value to be the first symbol. This means that
854
- # if the writer schema has an additional member that the reader schema does NOT have, the reader will simply
855
- # and silently assume the default specified here. Now that may not always be what we want, but standard lib
856
- # Python enums don't really have a way to specify this.
857
- "default": self.symbols[0],
858
- }
865
+ if not self._is_valid_enum():
866
+ # Special case for StrEnum might contain invalid avro names. We just use a StrSubclassSchema schema instead.
867
+ enum_schema = {"type": "string", "namedString": self.name}
868
+ else:
869
+ enum_schema = {
870
+ "type": "enum",
871
+ "name": self.name,
872
+ "symbols": self.symbols,
873
+ # This is the default for the enum, not the default value for a record field using the enum type!
874
+ # See Avro schema specification for use. For now, we force the default value to be the first symbol.
875
+ # This means that if the writer schema has an additional member that the reader schema does NOT have,
876
+ # the reader will simply and silently assume the default specified here.
877
+ # Now that may not always be what we want, but standard lib Python enums don't really have a way
878
+ # to specify this.
879
+ "default": self.symbols[0],
880
+ }
859
881
  if self.namespace is not None:
860
882
  enum_schema["namespace"] = self.namespace
861
883
  fqn = f"{self.namespace}.{self.name}"
@@ -1064,7 +1086,15 @@ class PydanticSchema(RecordSchema):
1064
1086
 
1065
1087
  @register_schema
1066
1088
  class PlainClassSchema(RecordSchema):
1067
- """An Avro record schema for a plain Python class with typed constructor method arguments"""
1089
+ """
1090
+ An Avro record schema for a plain Python class with type annotations, e.g.,
1091
+ ::
1092
+ class MyClass:
1093
+ var: str
1094
+
1095
+ def __init__(self, var: str = "foo"):
1096
+ self.var = var
1097
+ """
1068
1098
 
1069
1099
  @classmethod
1070
1100
  def handles_type(cls, py_type: Type) -> bool:
@@ -1077,13 +1107,13 @@ class PlainClassSchema(RecordSchema):
1077
1107
  and not hasattr(py_type, "__pydantic_private__")
1078
1108
  # If we are subclassing a string, used the "named string" approach
1079
1109
  and (inspect.isclass(py_type) and not issubclass(py_type, str))
1080
- # Any other class with __init__ with typed args
1081
- and bool(get_type_hints(py_type.__init__))
1110
+ # and any other class with typed annotations
1111
+ and bool(get_type_hints(py_type))
1082
1112
  )
1083
1113
 
1084
1114
  def __init__(self, py_type: Type, namespace: Optional[str] = None, options: Option = Option(0)):
1085
1115
  """
1086
- An Avro record schema for a plain Python class with typed constructor method arguments
1116
+ An Avro record schema for a plain Python class with type hints
1087
1117
 
1088
1118
  :param py_type: The Python class to generate a schema for.
1089
1119
  :param namespace: The Avro namespace to add to schemas.
@@ -1091,17 +1121,30 @@ class PlainClassSchema(RecordSchema):
1091
1121
  """
1092
1122
  super().__init__(py_type, namespace=namespace, options=options)
1093
1123
  py_type = _type_from_annotated(py_type)
1094
- # Extracting arguments from __init__, dropping first argument `self`.
1095
- self.py_fields = list(inspect.signature(py_type.__init__).parameters.values())[1:]
1124
+
1125
+ self.py_fields: list[tuple[str, type]] = []
1126
+ for k, v in py_type.__annotations__.items():
1127
+ self.py_fields.append((k, v))
1128
+ # We store __init__ parameters with default values. They can be used as defaults for the record.
1129
+ self.signature_fields = {
1130
+ param.name: (param.annotation, param.default)
1131
+ for param in list(inspect.signature(py_type.__init__).parameters.values())[1:]
1132
+ if param.default is not inspect._empty
1133
+ }
1096
1134
  self.record_fields = [self._record_field(field) for field in self.py_fields]
1097
1135
 
1098
- def _record_field(self, py_field: inspect.Parameter) -> RecordField:
1136
+ def _record_field(self, py_field: tuple[str, Type]) -> RecordField:
1099
1137
  """Return an Avro record field object for a given Python instance attribute"""
1100
- default = py_field.default if py_field.default != inspect.Parameter.empty else dataclasses.MISSING
1101
- aliases, actual_type = get_field_aliases_and_actual_type(py_field.annotation)
1138
+ aliases, actual_type = get_field_aliases_and_actual_type(py_field[1])
1139
+ name = py_field[0]
1140
+ default = dataclasses.MISSING
1141
+ if field := self.signature_fields.get(name):
1142
+ _annotation, _default = field
1143
+ if actual_type is _annotation:
1144
+ default = _default
1102
1145
  field_obj = RecordField(
1103
1146
  py_type=actual_type,
1104
- name=py_field.name,
1147
+ name=name,
1105
1148
  namespace=self.namespace_override,
1106
1149
  default=default,
1107
1150
  aliases=aliases,