localstack-py-avro-schema 3.9.2__tar.gz → 3.9.3__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 (39) hide show
  1. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/PKG-INFO +1 -1
  2. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/src/localstack_py_avro_schema.egg-info/PKG-INFO +1 -1
  3. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/src/py_avro_schema/_schemas.py +58 -21
  4. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/tests/test_plain_class.py +13 -2
  5. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/tests/test_primitives.py +11 -0
  6. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/.editorconfig +0 -0
  7. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/.flake8 +0 -0
  8. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/.github/workflows/release-package.yml +0 -0
  9. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/.github/workflows/test-package.yml +0 -0
  10. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/.gitignore +0 -0
  11. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/.pre-commit-config.yaml +0 -0
  12. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/.readthedocs.yaml +0 -0
  13. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/LICENSE +0 -0
  14. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/LICENSE_HEADER.txt +0 -0
  15. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/README.md +0 -0
  16. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/docs/conf.py +0 -0
  17. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/docs/index.rst +0 -0
  18. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/docs/modules.rst +0 -0
  19. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/docs/py_avro_schema.rst +0 -0
  20. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/docs/tutorial.rst +0 -0
  21. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/docs/types.rst +0 -0
  22. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/pyproject.toml +0 -0
  23. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/setup.cfg +0 -0
  24. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/src/localstack_py_avro_schema.egg-info/SOURCES.txt +0 -0
  25. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/src/localstack_py_avro_schema.egg-info/dependency_links.txt +0 -0
  26. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/src/localstack_py_avro_schema.egg-info/requires.txt +0 -0
  27. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/src/localstack_py_avro_schema.egg-info/top_level.txt +0 -0
  28. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/src/py_avro_schema/__init__.py +0 -0
  29. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/src/py_avro_schema/_alias.py +0 -0
  30. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/src/py_avro_schema/_testing.py +0 -0
  31. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/src/py_avro_schema/_typing.py +0 -0
  32. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/src/py_avro_schema/py.typed +0 -0
  33. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/tests/test_avro_schema.py +0 -0
  34. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/tests/test_dataclass.py +0 -0
  35. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/tests/test_logicals.py +0 -0
  36. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/tests/test_pydantic.py +0 -0
  37. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/tests/test_typed_dict.py +0 -0
  38. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/tests/test_typing.py +0 -0
  39. {localstack_py_avro_schema-3.9.2 → localstack_py_avro_schema-3.9.3}/tox.ini +0 -0
@@ -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.3
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,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.3
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
@@ -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
@@ -843,19 +845,33 @@ class EnumSchema(NamedSchema):
843
845
  if symbol_types != {str}:
844
846
  raise TypeError(f"Avro enum schema members must be strings. {py_type} uses {symbol_types} values.")
845
847
 
848
+ def _is_valid_enum(self) -> bool:
849
+ """Checks if all the symbols of the enum are valid Avro names."""
850
+ try:
851
+ for _symbol in self.symbols:
852
+ avro.name.validate_basename(_symbol)
853
+ except InvalidName:
854
+ return False
855
+ return True
856
+
846
857
  def data_before_deduplication(self, names: NamesType) -> JSONObj:
847
858
  """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
- }
859
+ if not self._is_valid_enum():
860
+ # Special case for StrEnum might contain invalid avro names. We just use a StrSubclassSchema schema instead.
861
+ enum_schema = {"type": "string", "namedString": self.name}
862
+ else:
863
+ enum_schema = {
864
+ "type": "enum",
865
+ "name": self.name,
866
+ "symbols": self.symbols,
867
+ # This is the default for the enum, not the default value for a record field using the enum type!
868
+ # See Avro schema specification for use. For now, we force the default value to be the first symbol.
869
+ # This means that if the writer schema has an additional member that the reader schema does NOT have,
870
+ # the reader will simply and silently assume the default specified here.
871
+ # Now that may not always be what we want, but standard lib Python enums don't really have a way
872
+ # to specify this.
873
+ "default": self.symbols[0],
874
+ }
859
875
  if self.namespace is not None:
860
876
  enum_schema["namespace"] = self.namespace
861
877
  fqn = f"{self.namespace}.{self.name}"
@@ -1064,7 +1080,15 @@ class PydanticSchema(RecordSchema):
1064
1080
 
1065
1081
  @register_schema
1066
1082
  class PlainClassSchema(RecordSchema):
1067
- """An Avro record schema for a plain Python class with typed constructor method arguments"""
1083
+ """
1084
+ An Avro record schema for a plain Python class with type annotations, e.g.,
1085
+ ::
1086
+ class MyClass:
1087
+ var: str
1088
+
1089
+ def __init__(self, var: str = "foo"):
1090
+ self.var = var
1091
+ """
1068
1092
 
1069
1093
  @classmethod
1070
1094
  def handles_type(cls, py_type: Type) -> bool:
@@ -1077,13 +1101,13 @@ class PlainClassSchema(RecordSchema):
1077
1101
  and not hasattr(py_type, "__pydantic_private__")
1078
1102
  # If we are subclassing a string, used the "named string" approach
1079
1103
  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__))
1104
+ # and any other class with typed annotations
1105
+ and bool(get_type_hints(py_type))
1082
1106
  )
1083
1107
 
1084
1108
  def __init__(self, py_type: Type, namespace: Optional[str] = None, options: Option = Option(0)):
1085
1109
  """
1086
- An Avro record schema for a plain Python class with typed constructor method arguments
1110
+ An Avro record schema for a plain Python class with type hints
1087
1111
 
1088
1112
  :param py_type: The Python class to generate a schema for.
1089
1113
  :param namespace: The Avro namespace to add to schemas.
@@ -1091,17 +1115,30 @@ class PlainClassSchema(RecordSchema):
1091
1115
  """
1092
1116
  super().__init__(py_type, namespace=namespace, options=options)
1093
1117
  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:]
1118
+
1119
+ self.py_fields: list[tuple[str, type]] = []
1120
+ for k, v in py_type.__annotations__.items():
1121
+ self.py_fields.append((k, v))
1122
+ # We store __init__ parameters with default values. They can be used as defaults for the record.
1123
+ self.signature_fields = {
1124
+ param.name: (param.annotation, param.default)
1125
+ for param in list(inspect.signature(py_type.__init__).parameters.values())[1:]
1126
+ if param.default is not inspect._empty
1127
+ }
1096
1128
  self.record_fields = [self._record_field(field) for field in self.py_fields]
1097
1129
 
1098
- def _record_field(self, py_field: inspect.Parameter) -> RecordField:
1130
+ def _record_field(self, py_field: tuple[str, Type]) -> RecordField:
1099
1131
  """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)
1132
+ aliases, actual_type = get_field_aliases_and_actual_type(py_field[1])
1133
+ name = py_field[0]
1134
+ default = dataclasses.MISSING
1135
+ if field := self.signature_fields.get(name):
1136
+ _annotation, _default = field
1137
+ if actual_type is _annotation:
1138
+ default = _default
1102
1139
  field_obj = RecordField(
1103
1140
  py_type=actual_type,
1104
- name=py_field.name,
1141
+ name=name,
1105
1142
  namespace=self.namespace_override,
1106
1143
  default=default,
1107
1144
  aliases=aliases,
@@ -24,6 +24,11 @@ def test_plain_class_with_type_hints():
24
24
  class PyType:
25
25
  """A port, not using dataclass"""
26
26
 
27
+ name: str
28
+ country: str
29
+ latitude: float
30
+ longitude: float
31
+
27
32
  def __init__(
28
33
  self, name: str, *, country: str = "NLD", latitude: float, longitude: float
29
34
  ): # note the non-default kwargs near the end!
@@ -65,6 +70,8 @@ def test_plain_class_annotated():
65
70
  class PyType:
66
71
  """A port, not using dataclass"""
67
72
 
73
+ name: str
74
+
68
75
  def __init__(self, name: str):
69
76
  self.name = name
70
77
 
@@ -84,7 +91,9 @@ def test_plain_class_annotated():
84
91
 
85
92
  def test_field_alias():
86
93
  class PyType:
87
- def __init__(self, name: Annotated[str, Alias("old_name")]):
94
+ name: Annotated[str, Alias("old_name")]
95
+
96
+ def __init__(self, name):
88
97
  self.name = name
89
98
 
90
99
  expected = {
@@ -105,7 +114,9 @@ def test_plain_class_no_type_hints():
105
114
  class PyType:
106
115
  """A port, not using dataclass"""
107
116
 
108
- def __init__(self, name, *, country="NLD", latitude, longitude): # note the non-default kwargs near the end!
117
+ def __init__(
118
+ self, name: str, *, country: str = "NLD", latitude: float, longitude: float
119
+ ): # note the non-default kwargs near the end!
109
120
  self.name = name
110
121
  self.country = country.upper()
111
122
  self.latitude = latitude
@@ -508,3 +508,14 @@ def test_enum_docs():
508
508
  "default": "RED",
509
509
  }
510
510
  assert_schema(PyType, expected, do_doc=True)
511
+
512
+
513
+ def test_str_enum_invalid_name():
514
+ class OriginProtocolPolicy(str, enum.Enum):
515
+ http_only = "http-only"
516
+ match_viewer = "match-viewer"
517
+ https_only = "https-only"
518
+
519
+ expected = {"namedString": "OriginProtocolPolicy", "type": "string"}
520
+
521
+ assert_schema(OriginProtocolPolicy, expected)