forked from jpmorganchase/py-avro-schema
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path_alias.py
More file actions
129 lines (95 loc) · 3.73 KB
/
_alias.py
File metadata and controls
129 lines (95 loc) · 3.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
"""
Module to register aliases for Python types
This module maintains global state via the _ALIASES registry.
Decorators will modify this state when applied to classes.
"""
import dataclasses
from collections import defaultdict
from typing import Annotated, Type, get_args, get_origin
FQN = str
"""Fully qualified name for a Python type"""
_ALIASES: dict[FQN, set[FQN]] = defaultdict(set)
"""Maps the FQN of a Python type to a set of aliases"""
@dataclasses.dataclass
class Alias:
"""Alias for a record field"""
alias: str
@dataclasses.dataclass
class Aliases:
"""Aliases for a record field"""
aliases: list[str]
class Opaque:
"""
This is a marker for complex Avro fields (e.g., maps) that are serialized to a simple string.
"""
pass
def get_fully_qualified_name(py_type: type) -> str:
"""Returns the fully qualified name for a Python type"""
module = getattr(py_type, "__module__", None)
qualname = getattr(py_type, "__qualname__", py_type.__name__)
# py-avro-schema does not consider <locals> in the namespace.
# we skip it here as well for consistency
if module and "<locals>" in qualname:
return f"{module}.{py_type.__name__}"
if module and module not in ("builtins", "__main__"):
return f"{module}.{qualname}"
return qualname
def register_type_aliases(aliases: list[FQN]):
"""
Decorator to register aliases for a given type.
It allows for compatible schemas following a change type (e.g., a rename), if the type fields do not
change in an incompatible way.
Example::
@register_type_aliases(aliases=["py_avro_schema.OldAddress"])
class Address(TypedDict):
street: str
number: int
"""
def _wrapper(cls):
"""Wrapper function that updates the aliases dictionary"""
fqn = get_fully_qualified_name(cls)
_ALIASES[fqn].update(aliases)
return cls
return _wrapper
def register_type_alias(alias: FQN):
"""
Decorator to register a single alias for a given type.
It allows for compatible schemas following a change type (e.g., a rename), if the type fields do not
change in an incompatible way.
Example::
@register_type_alias(alias="py_avro_schema.OldAddress")
class Address(TypedDict):
street: str
number: int
"""
def _wrapper(cls):
"""Wrapper function that updates the aliases dictionary"""
fqn = get_fully_qualified_name(cls)
_ALIASES[fqn].add(alias)
return cls
return _wrapper
def get_aliases(fqn: str) -> list[str]:
"""Returns the list of aliases for a given type"""
if aliases := _ALIASES.get(fqn):
return sorted(aliases)
return []
def get_field_aliases_and_actual_type(py_type: Type) -> tuple[list[str] | None, Type]:
"""
Check if a type contains an alias metadata via `Alias` or `Aliases` as metadata.
It returns the eventual aliases and the type.
"""
# py_type is not annotated. It can't have aliases
if get_origin(py_type) is not Annotated:
return [], py_type
args = get_args(py_type)
actual_type, annotation = args[0], args[1]
# When a field is annotated with the Opaque class, we return bytes as type.
# The object serializer is responsible for dumping the entire attribute as a JSON string
if isinstance(annotation, type) and issubclass(annotation, Opaque):
return [], str
# Annotated type but not an alias. We do nothing.
if type(annotation) not in (Alias, Aliases):
return [], py_type
# If the annotated type is an alias, we extract the aliases and return the actual type
aliases = annotation.aliases if type(annotation) is Aliases else [annotation.alias]
return aliases, actual_type