instructure-pysqlsync 0.8.5.dev0__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.
- instructure_pysqlsync-0.8.5.dev0.dist-info/METADATA +244 -0
- instructure_pysqlsync-0.8.5.dev0.dist-info/RECORD +102 -0
- instructure_pysqlsync-0.8.5.dev0.dist-info/WHEEL +5 -0
- instructure_pysqlsync-0.8.5.dev0.dist-info/licenses/LICENSE +21 -0
- instructure_pysqlsync-0.8.5.dev0.dist-info/top_level.txt +1 -0
- instructure_pysqlsync-0.8.5.dev0.dist-info/zip-safe +1 -0
- pysqlsync/__init__.py +15 -0
- pysqlsync/base.py +1386 -0
- pysqlsync/connection.py +101 -0
- pysqlsync/data/__init__.py +0 -0
- pysqlsync/data/exchange.py +184 -0
- pysqlsync/data/generator.py +305 -0
- pysqlsync/dialect/README.md +35 -0
- pysqlsync/dialect/__init__.py +0 -0
- pysqlsync/dialect/delta/__init__.py +0 -0
- pysqlsync/dialect/delta/data_types.py +51 -0
- pysqlsync/dialect/delta/dependency.py +7 -0
- pysqlsync/dialect/delta/engine.py +18 -0
- pysqlsync/dialect/delta/generator.py +74 -0
- pysqlsync/dialect/delta/object_types.py +87 -0
- pysqlsync/dialect/mssql/README.md +23 -0
- pysqlsync/dialect/mssql/__init__.py +0 -0
- pysqlsync/dialect/mssql/connection.py +146 -0
- pysqlsync/dialect/mssql/data_types.py +113 -0
- pysqlsync/dialect/mssql/dependency.py +9 -0
- pysqlsync/dialect/mssql/discovery.py +99 -0
- pysqlsync/dialect/mssql/engine.py +20 -0
- pysqlsync/dialect/mssql/generator.py +88 -0
- pysqlsync/dialect/mssql/mutation.py +46 -0
- pysqlsync/dialect/mssql/object_types.py +135 -0
- pysqlsync/dialect/mysql/__init__.py +0 -0
- pysqlsync/dialect/mysql/connection.py +126 -0
- pysqlsync/dialect/mysql/data_types.py +40 -0
- pysqlsync/dialect/mysql/dependency.py +10 -0
- pysqlsync/dialect/mysql/discovery.py +145 -0
- pysqlsync/dialect/mysql/engine.py +20 -0
- pysqlsync/dialect/mysql/generator.py +140 -0
- pysqlsync/dialect/mysql/mutation.py +65 -0
- pysqlsync/dialect/mysql/object_types.py +75 -0
- pysqlsync/dialect/oracle/README.md +22 -0
- pysqlsync/dialect/oracle/__init__.py +0 -0
- pysqlsync/dialect/oracle/connection.py +141 -0
- pysqlsync/dialect/oracle/data_types.py +104 -0
- pysqlsync/dialect/oracle/dependency.py +9 -0
- pysqlsync/dialect/oracle/discovery.py +231 -0
- pysqlsync/dialect/oracle/engine.py +20 -0
- pysqlsync/dialect/oracle/generator.py +93 -0
- pysqlsync/dialect/oracle/mutation.py +37 -0
- pysqlsync/dialect/oracle/object_types.py +59 -0
- pysqlsync/dialect/postgresql/__init__.py +0 -0
- pysqlsync/dialect/postgresql/connection.py +133 -0
- pysqlsync/dialect/postgresql/data_types.py +6 -0
- pysqlsync/dialect/postgresql/dependency.py +9 -0
- pysqlsync/dialect/postgresql/discovery.py +392 -0
- pysqlsync/dialect/postgresql/engine.py +20 -0
- pysqlsync/dialect/postgresql/generator.py +99 -0
- pysqlsync/dialect/postgresql/mutation.py +82 -0
- pysqlsync/dialect/postgresql/object_types.py +99 -0
- pysqlsync/dialect/redshift/__init__.py +0 -0
- pysqlsync/dialect/redshift/connection.py +156 -0
- pysqlsync/dialect/redshift/data_types.py +10 -0
- pysqlsync/dialect/redshift/dependency.py +9 -0
- pysqlsync/dialect/redshift/engine.py +20 -0
- pysqlsync/dialect/redshift/generator.py +82 -0
- pysqlsync/dialect/snowflake/__init__.py +0 -0
- pysqlsync/dialect/snowflake/data_types.py +18 -0
- pysqlsync/dialect/snowflake/dependency.py +9 -0
- pysqlsync/dialect/snowflake/engine.py +18 -0
- pysqlsync/dialect/snowflake/generator.py +64 -0
- pysqlsync/dialect/snowflake/object_types.py +84 -0
- pysqlsync/dialect/test/__init__.py +0 -0
- pysqlsync/dialect/test/dependency.py +10 -0
- pysqlsync/dialect/trino/__init__.py +0 -0
- pysqlsync/dialect/trino/connection.py +86 -0
- pysqlsync/dialect/trino/dependency.py +9 -0
- pysqlsync/dialect/trino/discovery.py +21 -0
- pysqlsync/dialect/trino/engine.py +20 -0
- pysqlsync/dialect/trino/generator.py +31 -0
- pysqlsync/factory.py +169 -0
- pysqlsync/formation/__init__.py +0 -0
- pysqlsync/formation/constraints.py +78 -0
- pysqlsync/formation/data_types.py +192 -0
- pysqlsync/formation/discovery.py +390 -0
- pysqlsync/formation/inspection.py +179 -0
- pysqlsync/formation/mutation.py +320 -0
- pysqlsync/formation/object_dict.py +79 -0
- pysqlsync/formation/object_types.py +872 -0
- pysqlsync/formation/py_to_sql.py +1134 -0
- pysqlsync/formation/sql_to_py.py +194 -0
- pysqlsync/model/__init__.py +0 -0
- pysqlsync/model/data_types.py +397 -0
- pysqlsync/model/entity_types.py +68 -0
- pysqlsync/model/id_types.py +193 -0
- pysqlsync/model/key_types.py +38 -0
- pysqlsync/model/properties.py +234 -0
- pysqlsync/py.typed +0 -0
- pysqlsync/python_types.py +192 -0
- pysqlsync/resultset.py +106 -0
- pysqlsync/util/__init__.py +0 -0
- pysqlsync/util/dataclasses.py +80 -0
- pysqlsync/util/dispatch.py +28 -0
- pysqlsync/util/typing.py +13 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Optional, Protocol, runtime_checkable
|
|
4
|
+
|
|
5
|
+
ID_QUOTE_CHAR = '"'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def quote_id(name: str) -> str:
|
|
9
|
+
return name.replace(ID_QUOTE_CHAR, 2 * ID_QUOTE_CHAR)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@runtime_checkable
|
|
13
|
+
class SupportsQuotedId(Protocol):
|
|
14
|
+
__slots__ = ()
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
@abc.abstractmethod
|
|
18
|
+
def quoted_id(self) -> str:
|
|
19
|
+
"A fully-quoted identifier."
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@runtime_checkable
|
|
24
|
+
class SupportsLocalId(Protocol):
|
|
25
|
+
__slots__ = ()
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
@abc.abstractmethod
|
|
29
|
+
def local_id(self) -> str:
|
|
30
|
+
"Unquoted component of an identifier to be used in a local context, e.g. columns of a table."
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
@abc.abstractmethod
|
|
35
|
+
def quoted_id(self) -> str:
|
|
36
|
+
"A fully-quoted identifier."
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@runtime_checkable
|
|
41
|
+
class SupportsQualifiedId(Protocol):
|
|
42
|
+
__slots__ = ()
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
@abc.abstractmethod
|
|
46
|
+
def scope_id(self) -> Optional[str]:
|
|
47
|
+
"Unquoted scope identifier."
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
@abc.abstractmethod
|
|
52
|
+
def local_id(self) -> str:
|
|
53
|
+
"Unquoted component of an identifier to be used in a local context, e.g. columns of a table."
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
@abc.abstractmethod
|
|
58
|
+
def compact_id(self) -> str:
|
|
59
|
+
"An unquoted composite identifier."
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
@abc.abstractmethod
|
|
64
|
+
def quoted_id(self) -> str:
|
|
65
|
+
"A fully-quoted identifier."
|
|
66
|
+
...
|
|
67
|
+
|
|
68
|
+
def rename(self, id: str) -> "SupportsQualifiedId": ...
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@runtime_checkable
|
|
72
|
+
class SupportsName(Protocol):
|
|
73
|
+
__slots__ = ()
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
@abc.abstractmethod
|
|
77
|
+
def name(self) -> SupportsLocalId: ...
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class LocalId:
|
|
82
|
+
id: str
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def local_id(self) -> str:
|
|
86
|
+
"Unquoted identifier."
|
|
87
|
+
|
|
88
|
+
return self.id
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def quoted_id(self) -> str:
|
|
92
|
+
return ID_QUOTE_CHAR + quote_id(self.id) + ID_QUOTE_CHAR
|
|
93
|
+
|
|
94
|
+
def __str__(self) -> str:
|
|
95
|
+
"Quotes an identifier to be embedded in a SQL statement."
|
|
96
|
+
|
|
97
|
+
return self.quoted_id
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True)
|
|
101
|
+
class PrefixedId:
|
|
102
|
+
namespace: Optional[str]
|
|
103
|
+
id: str
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def scope_id(self) -> Optional[str]:
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def local_id(self) -> str:
|
|
111
|
+
return f"{self.namespace}__{self.id}" if self.namespace is not None else self.id
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def compact_id(self) -> str:
|
|
115
|
+
return self.local_id
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def quoted_id(self) -> str:
|
|
119
|
+
if self.namespace is not None:
|
|
120
|
+
return ID_QUOTE_CHAR + quote_id(self.namespace) + "__" + quote_id(self.id) + ID_QUOTE_CHAR
|
|
121
|
+
else:
|
|
122
|
+
return ID_QUOTE_CHAR + quote_id(self.id) + ID_QUOTE_CHAR
|
|
123
|
+
|
|
124
|
+
def rename(self, id: str) -> "SupportsQualifiedId":
|
|
125
|
+
return PrefixedId(self.namespace, id)
|
|
126
|
+
|
|
127
|
+
def __str__(self) -> str:
|
|
128
|
+
"Quotes a qualified identifier to be embedded in a SQL statement."
|
|
129
|
+
|
|
130
|
+
return self.quoted_id
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass(frozen=True)
|
|
134
|
+
class QualifiedId:
|
|
135
|
+
namespace: Optional[str]
|
|
136
|
+
id: str
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def scope_id(self) -> Optional[str]:
|
|
140
|
+
return self.namespace
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def local_id(self) -> str:
|
|
144
|
+
return self.id
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def compact_id(self) -> str:
|
|
148
|
+
if self.namespace is not None:
|
|
149
|
+
return f"{self.namespace}.{self.id}"
|
|
150
|
+
else:
|
|
151
|
+
return self.id
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def quoted_id(self) -> str:
|
|
155
|
+
if self.namespace is not None:
|
|
156
|
+
return ID_QUOTE_CHAR + quote_id(self.namespace) + ID_QUOTE_CHAR + "." + ID_QUOTE_CHAR + quote_id(self.id) + ID_QUOTE_CHAR
|
|
157
|
+
else:
|
|
158
|
+
return ID_QUOTE_CHAR + quote_id(self.id) + ID_QUOTE_CHAR
|
|
159
|
+
|
|
160
|
+
def rename(self, id: str) -> "SupportsQualifiedId":
|
|
161
|
+
return QualifiedId(self.namespace, id)
|
|
162
|
+
|
|
163
|
+
def __str__(self) -> str:
|
|
164
|
+
"Quotes a qualified identifier to be embedded in a SQL statement."
|
|
165
|
+
|
|
166
|
+
return self.quoted_id
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@dataclass(frozen=True)
|
|
170
|
+
class GlobalId:
|
|
171
|
+
id: str
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def scope_id(self) -> Optional[str]:
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def local_id(self) -> str:
|
|
179
|
+
return self.id
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def compact_id(self) -> str:
|
|
183
|
+
return self.id
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def quoted_id(self) -> str:
|
|
187
|
+
return self.id
|
|
188
|
+
|
|
189
|
+
def rename(self, id: str) -> "SupportsQualifiedId":
|
|
190
|
+
return GlobalId(id)
|
|
191
|
+
|
|
192
|
+
def __str__(self) -> str:
|
|
193
|
+
return self.id
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import Annotated, TypeVar, Union
|
|
2
|
+
|
|
3
|
+
T = TypeVar("T")
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PrimaryKeyTag:
|
|
7
|
+
"Marks a field as the primary key of a table."
|
|
8
|
+
|
|
9
|
+
def __repr__(self) -> str:
|
|
10
|
+
return "PrimaryKey"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class IdentityTag:
|
|
14
|
+
"Marks a field as an identity column in a table."
|
|
15
|
+
|
|
16
|
+
def __repr__(self) -> str:
|
|
17
|
+
return "Identity"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class UniqueTag:
|
|
21
|
+
"Marks a field as a column with a unique constraint."
|
|
22
|
+
|
|
23
|
+
def __repr__(self) -> str:
|
|
24
|
+
return "Unique"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DefaultTag:
|
|
28
|
+
"The placeholder value DEFAULT."
|
|
29
|
+
|
|
30
|
+
def __repr__(self) -> str:
|
|
31
|
+
return "DEFAULT"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
DEFAULT = DefaultTag()
|
|
35
|
+
|
|
36
|
+
PrimaryKey = Annotated[T, PrimaryKeyTag()]
|
|
37
|
+
Identity = Annotated[Union[T, DefaultTag], IdentityTag()]
|
|
38
|
+
Unique = Annotated[T, UniqueTag()]
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import datetime
|
|
3
|
+
import decimal
|
|
4
|
+
import ipaddress
|
|
5
|
+
import typing
|
|
6
|
+
import uuid
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Annotated, Any, Union
|
|
9
|
+
|
|
10
|
+
from strong_typing.inspection import (
|
|
11
|
+
DataclassInstance,
|
|
12
|
+
TypeLike,
|
|
13
|
+
dataclass_fields,
|
|
14
|
+
get_annotation,
|
|
15
|
+
is_dataclass_type,
|
|
16
|
+
is_type_enum,
|
|
17
|
+
is_type_literal,
|
|
18
|
+
is_type_optional,
|
|
19
|
+
is_type_union,
|
|
20
|
+
unwrap_annotated_type,
|
|
21
|
+
unwrap_literal_types,
|
|
22
|
+
unwrap_optional_type,
|
|
23
|
+
unwrap_union_types,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from .key_types import DefaultTag, IdentityTag, PrimaryKeyTag, UniqueTag
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_primary_key_type(field_type: TypeLike) -> bool:
|
|
30
|
+
"Checks if the field type is marked as the primary key of a table."
|
|
31
|
+
|
|
32
|
+
return get_annotation(field_type, PrimaryKeyTag) is not None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def is_identity_type(field_type: TypeLike) -> bool:
|
|
36
|
+
"Checks if the field type is marked as an identity column in a table."
|
|
37
|
+
|
|
38
|
+
return get_annotation(field_type, IdentityTag) is not None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def is_unique_type(field_type: TypeLike) -> bool:
|
|
42
|
+
"Checks if the field type is marked as a unique column in a table."
|
|
43
|
+
|
|
44
|
+
return get_annotation(field_type, UniqueTag) is not None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_primary_key_name(class_type: type[DataclassInstance]) -> str:
|
|
48
|
+
"Fetches the primary key of the table."
|
|
49
|
+
|
|
50
|
+
for field in dataclass_fields(class_type):
|
|
51
|
+
if is_primary_key_type(field.type):
|
|
52
|
+
return field.name
|
|
53
|
+
|
|
54
|
+
raise TypeError(f"table type has no primary key: {class_type.__name__}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_primary_key_name_type(class_type: type[DataclassInstance]) -> tuple[str, type]:
|
|
58
|
+
"Returns the primary key of the table and its plain representation type."
|
|
59
|
+
|
|
60
|
+
for field in dataclass_fields(class_type):
|
|
61
|
+
props = get_field_properties(field.type)
|
|
62
|
+
if props.is_primary:
|
|
63
|
+
return field.name, props.tsv_type
|
|
64
|
+
|
|
65
|
+
raise TypeError(f"table type has no primary key: {class_type.__name__}")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def is_constraint(item: Any) -> bool:
|
|
69
|
+
return isinstance(item, (PrimaryKeyTag, IdentityTag, UniqueTag))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def tsv_type(plain_type: TypeLike) -> type:
|
|
73
|
+
"Python type that represents the TSV storage type for this data."
|
|
74
|
+
|
|
75
|
+
plain_type = unwrap_annotated_type(plain_type)
|
|
76
|
+
|
|
77
|
+
if is_type_enum(plain_type):
|
|
78
|
+
return str
|
|
79
|
+
elif is_type_literal(plain_type):
|
|
80
|
+
literal_types = set(tsv_type(t) for t in unwrap_literal_types(plain_type))
|
|
81
|
+
if len(literal_types) != 1:
|
|
82
|
+
raise TypeError(f"inconsistent literal value types: {literal_types}")
|
|
83
|
+
return literal_types.pop()
|
|
84
|
+
elif is_type_union(plain_type):
|
|
85
|
+
union_types = set(tsv_type(t) for t in unwrap_union_types(plain_type))
|
|
86
|
+
if len(union_types) != 1:
|
|
87
|
+
if len(union_types) == 2 and ipaddress.IPv4Address in union_types and ipaddress.IPv6Address in union_types:
|
|
88
|
+
return typing.cast(type, plain_type)
|
|
89
|
+
raise TypeError(f"inconsistent union types: {union_types}")
|
|
90
|
+
return union_types.pop()
|
|
91
|
+
elif is_dataclass_type(plain_type):
|
|
92
|
+
return dict
|
|
93
|
+
elif plain_type is bool:
|
|
94
|
+
return bool
|
|
95
|
+
elif plain_type is bytes:
|
|
96
|
+
return bytes
|
|
97
|
+
elif plain_type is int:
|
|
98
|
+
return int
|
|
99
|
+
elif plain_type is float:
|
|
100
|
+
return float
|
|
101
|
+
elif plain_type is str:
|
|
102
|
+
return str
|
|
103
|
+
elif plain_type is uuid.UUID:
|
|
104
|
+
return uuid.UUID
|
|
105
|
+
elif plain_type is datetime.datetime:
|
|
106
|
+
return datetime.datetime
|
|
107
|
+
elif plain_type is datetime.date:
|
|
108
|
+
return datetime.date
|
|
109
|
+
elif plain_type is datetime.time:
|
|
110
|
+
return datetime.time
|
|
111
|
+
elif plain_type is decimal.Decimal:
|
|
112
|
+
return decimal.Decimal
|
|
113
|
+
elif plain_type is ipaddress.IPv4Address:
|
|
114
|
+
return ipaddress.IPv4Address
|
|
115
|
+
elif plain_type is ipaddress.IPv6Address:
|
|
116
|
+
return ipaddress.IPv6Address
|
|
117
|
+
|
|
118
|
+
origin_type = typing.get_origin(plain_type)
|
|
119
|
+
if origin_type is list: # JSON
|
|
120
|
+
return list
|
|
121
|
+
elif origin_type is set: # JSON
|
|
122
|
+
return set
|
|
123
|
+
elif origin_type is dict: # JSON
|
|
124
|
+
return dict
|
|
125
|
+
|
|
126
|
+
raise TypeError(f"unsupported TSV value type: {plain_type}")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass
|
|
130
|
+
class FieldProperties:
|
|
131
|
+
"""
|
|
132
|
+
Captures type information associated with a field type.
|
|
133
|
+
|
|
134
|
+
:param plain_type: Unadorned type without any metadata.
|
|
135
|
+
:param nullable: True if the field is optional.
|
|
136
|
+
:param metadata: Any metadata that is not a constraint such as identity, primary key or unique.
|
|
137
|
+
:param is_primary: True if the field is a primary key.
|
|
138
|
+
:param is_identity: True if the field is generated as identity.
|
|
139
|
+
:param is_unique: True if values of this type must be unique.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
plain_type: TypeLike
|
|
143
|
+
nullable: bool
|
|
144
|
+
metadata: tuple[Any, ...]
|
|
145
|
+
is_primary: bool
|
|
146
|
+
is_identity: bool
|
|
147
|
+
is_unique: bool
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def field_type(self) -> TypeLike:
|
|
151
|
+
"Type without constraint annotations such as identity, primary key, or unique."
|
|
152
|
+
|
|
153
|
+
if self.metadata:
|
|
154
|
+
# type becomes Annotated[T, ...]
|
|
155
|
+
return Annotated[(self.plain_type, *self.metadata)]
|
|
156
|
+
else:
|
|
157
|
+
# type becomes a regular type
|
|
158
|
+
return self.plain_type
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def tsv_type(self) -> type:
|
|
162
|
+
"Python type that represents the TSV storage type for this data."
|
|
163
|
+
|
|
164
|
+
return tsv_type(self.plain_type)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _get_field_metadata(field_type: TypeLike) -> list[Any]:
|
|
168
|
+
# field has a type of Annotated[T, ...]
|
|
169
|
+
return list(getattr(field_type, "__metadata__", ()))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def get_field_properties(field_type: TypeLike) -> FieldProperties:
|
|
173
|
+
"Extracts column properties such as primary key, identity or unique constraints."
|
|
174
|
+
|
|
175
|
+
metadata = _get_field_metadata(field_type)
|
|
176
|
+
plain_type = unwrap_annotated_type(field_type)
|
|
177
|
+
|
|
178
|
+
# check if field has DEFAULT as a union type member
|
|
179
|
+
if is_type_union(plain_type):
|
|
180
|
+
union_types = list(unwrap_union_types(plain_type))
|
|
181
|
+
if DefaultTag in union_types:
|
|
182
|
+
union_types.remove(DefaultTag)
|
|
183
|
+
plain_type = Union[tuple(union_types)] # may cease to become a union type
|
|
184
|
+
|
|
185
|
+
# check if field is nullable
|
|
186
|
+
if is_type_optional(plain_type):
|
|
187
|
+
nullable = True
|
|
188
|
+
plain_type = unwrap_optional_type(plain_type)
|
|
189
|
+
else:
|
|
190
|
+
nullable = False
|
|
191
|
+
|
|
192
|
+
metadata.extend(_get_field_metadata(plain_type))
|
|
193
|
+
plain_type = unwrap_annotated_type(plain_type)
|
|
194
|
+
|
|
195
|
+
if not metadata:
|
|
196
|
+
# field has a type without annotations or constraints
|
|
197
|
+
return FieldProperties(
|
|
198
|
+
plain_type=plain_type,
|
|
199
|
+
nullable=nullable,
|
|
200
|
+
metadata=(),
|
|
201
|
+
is_primary=False,
|
|
202
|
+
is_identity=False,
|
|
203
|
+
is_unique=False,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# check for constraints
|
|
207
|
+
is_primary = any(isinstance(m, PrimaryKeyTag) for m in metadata)
|
|
208
|
+
is_identity = any(isinstance(m, IdentityTag) for m in metadata)
|
|
209
|
+
is_unique = any(isinstance(m, UniqueTag) for m in metadata)
|
|
210
|
+
|
|
211
|
+
# filter annotations that represent constraints
|
|
212
|
+
metadata = [item for item in metadata if not is_constraint(item)]
|
|
213
|
+
|
|
214
|
+
return FieldProperties(
|
|
215
|
+
plain_type=plain_type,
|
|
216
|
+
nullable=nullable,
|
|
217
|
+
metadata=tuple(metadata),
|
|
218
|
+
is_primary=is_primary,
|
|
219
|
+
is_identity=is_identity,
|
|
220
|
+
is_unique=is_unique,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@dataclass
|
|
225
|
+
class ClassProperties:
|
|
226
|
+
fields: tuple[FieldProperties, ...]
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def tsv_types(self) -> tuple[type, ...]:
|
|
230
|
+
return tuple(prop.tsv_type for prop in self.fields)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def get_class_properties(class_type: type[DataclassInstance]) -> ClassProperties:
|
|
234
|
+
return ClassProperties(tuple(get_field_properties(field.type) for field in dataclasses.fields(class_type)))
|
pysqlsync/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import enum
|
|
3
|
+
import inspect
|
|
4
|
+
import io
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
import textwrap
|
|
8
|
+
import typing
|
|
9
|
+
from types import ModuleType
|
|
10
|
+
from typing import TextIO
|
|
11
|
+
|
|
12
|
+
from strong_typing.docstring import has_docstring, parse_type
|
|
13
|
+
from strong_typing.inspection import (
|
|
14
|
+
DataclassInstance,
|
|
15
|
+
get_module_classes,
|
|
16
|
+
is_dataclass_type,
|
|
17
|
+
is_type_enum,
|
|
18
|
+
)
|
|
19
|
+
from strong_typing.name import TypeFormatter
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def module_to_stream(module: ModuleType, target: TextIO) -> None:
|
|
23
|
+
"Generates Python code of all declared types in a module."
|
|
24
|
+
|
|
25
|
+
print("from __future__ import annotations", file=target)
|
|
26
|
+
print("import dataclasses", file=target)
|
|
27
|
+
print("import enum", file=target)
|
|
28
|
+
print("from datetime import datetime, date, time", file=target)
|
|
29
|
+
print("from decimal import Decimal", file=target)
|
|
30
|
+
print("from uuid import UUID", file=target)
|
|
31
|
+
print(
|
|
32
|
+
"from typing import Annotated, Dict, List, Literal, Optional, Union",
|
|
33
|
+
file=target,
|
|
34
|
+
)
|
|
35
|
+
print(
|
|
36
|
+
"from strong_typing.auxiliary import int16, int32, int64, float32, float64, MaxLength, Precision, TimePrecision",
|
|
37
|
+
file=target,
|
|
38
|
+
)
|
|
39
|
+
print(file=target)
|
|
40
|
+
|
|
41
|
+
for cls in get_module_classes(module):
|
|
42
|
+
if is_type_enum(cls):
|
|
43
|
+
enum_class_to_stream(cls, target)
|
|
44
|
+
elif is_dataclass_type(cls):
|
|
45
|
+
dataclass_to_stream(cls, target)
|
|
46
|
+
else:
|
|
47
|
+
raise NotImplementedError("classes in module must be of enumeration or data-class type")
|
|
48
|
+
print(file=target)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def module_to_code(module: ModuleType) -> str:
|
|
52
|
+
"Generates Python code of all declared types in a module."
|
|
53
|
+
|
|
54
|
+
with io.StringIO() as out:
|
|
55
|
+
module_to_stream(module, out)
|
|
56
|
+
return out.getvalue()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def dataclass_to_stream(typ: type[DataclassInstance], target: TextIO) -> None:
|
|
60
|
+
"Generates Python code corresponding to a dataclass type."
|
|
61
|
+
|
|
62
|
+
print("@dataclasses.dataclass", file=target)
|
|
63
|
+
print(f"class {typ.__name__}:", file=target)
|
|
64
|
+
|
|
65
|
+
# check if class has a doc-string other than the auto-generated string assigned by @dataclass
|
|
66
|
+
if has_docstring(typ):
|
|
67
|
+
if "\n" in typing.cast(str, typ.__doc__):
|
|
68
|
+
ds = parse_type(typ)
|
|
69
|
+
print(' """', file=target)
|
|
70
|
+
|
|
71
|
+
if ds.short_description:
|
|
72
|
+
_wrap_print(ds.short_description, file=target)
|
|
73
|
+
if ds.long_description:
|
|
74
|
+
print(file=target)
|
|
75
|
+
_wrap_print(ds.long_description, file=target)
|
|
76
|
+
|
|
77
|
+
if ds.short_description and (ds.params or ds.returns):
|
|
78
|
+
print(file=target)
|
|
79
|
+
|
|
80
|
+
for name, param in ds.params.items():
|
|
81
|
+
_wrap_print(f":param {name}: {param.description}", file=target)
|
|
82
|
+
if ds.returns:
|
|
83
|
+
_wrap_print(f":returns: {ds.returns.description}", file=target)
|
|
84
|
+
|
|
85
|
+
print(' """', file=target)
|
|
86
|
+
else:
|
|
87
|
+
print(f" {repr(typ.__doc__)}", file=target)
|
|
88
|
+
print(file=target)
|
|
89
|
+
|
|
90
|
+
# class variables (e.g. "primary_key")
|
|
91
|
+
field_names = [field.name for field in dataclasses.fields(typ)]
|
|
92
|
+
variables = {
|
|
93
|
+
name: value
|
|
94
|
+
for name, value in inspect.getmembers(typ, lambda m: not inspect.isroutine(m))
|
|
95
|
+
if not re.match(r"^__.+__$", name) and name not in field_names
|
|
96
|
+
}
|
|
97
|
+
if variables:
|
|
98
|
+
for name, value in variables.items():
|
|
99
|
+
print(f" {name} = {repr(value)}", file=target)
|
|
100
|
+
print(file=target)
|
|
101
|
+
|
|
102
|
+
# member variables
|
|
103
|
+
fmt = TypeFormatter(context=sys.modules[typ.__module__])
|
|
104
|
+
for field in dataclasses.fields(typ):
|
|
105
|
+
type_name = fmt.python_type_to_str(field.type)
|
|
106
|
+
metadata = dict(field.metadata)
|
|
107
|
+
|
|
108
|
+
field_initializer: dict[str, str] = {}
|
|
109
|
+
if field.default is not dataclasses.MISSING:
|
|
110
|
+
field_initializer["default"] = repr(field.default)
|
|
111
|
+
if field.default_factory is not dataclasses.MISSING:
|
|
112
|
+
field_initializer["default_factory"] = fmt.python_type_to_str(field.default_factory)
|
|
113
|
+
if metadata:
|
|
114
|
+
field_initializer["metadata"] = repr(metadata)
|
|
115
|
+
|
|
116
|
+
if not field_initializer:
|
|
117
|
+
initializer = ""
|
|
118
|
+
elif field.default is not dataclasses.MISSING and len(field_initializer) == 1:
|
|
119
|
+
initializer = f" = {repr(field.default)}"
|
|
120
|
+
else:
|
|
121
|
+
initializer_list = ", ".join(f"{key} = {value}" for key, value in field_initializer.items())
|
|
122
|
+
initializer = f" = field({initializer_list})"
|
|
123
|
+
|
|
124
|
+
print(f" {field.name}: {type_name}{initializer}", file=target)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def dataclasses_to_stream(types: list[type[DataclassInstance]], target: TextIO) -> None:
|
|
128
|
+
"Generates Python code corresponding to a set of dataclass types."
|
|
129
|
+
|
|
130
|
+
for typ in types:
|
|
131
|
+
dataclass_to_stream(typ, target)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def dataclass_to_code(data_type: type[DataclassInstance]) -> str:
|
|
135
|
+
"Generates Python code corresponding to a dataclass type."
|
|
136
|
+
|
|
137
|
+
return dataclasses_to_code([data_type])
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def dataclasses_to_code(types: list[type[DataclassInstance]]) -> str:
|
|
141
|
+
"Generates Python code corresponding to a set of dataclass types."
|
|
142
|
+
|
|
143
|
+
f = io.StringIO()
|
|
144
|
+
dataclasses_to_stream(types, f)
|
|
145
|
+
return f.getvalue()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def enum_class_to_stream(enum_class: type[enum.Enum], target: TextIO) -> None:
|
|
149
|
+
"Writes an enumeration class as a class definition."
|
|
150
|
+
|
|
151
|
+
print("@enum.unique", file=target)
|
|
152
|
+
print(f"class {enum_class.__name__}(enum.Enum):", file=target)
|
|
153
|
+
if enum_class.__doc__:
|
|
154
|
+
print(f" {repr(enum_class.__doc__)}", file=target)
|
|
155
|
+
print(file=target)
|
|
156
|
+
|
|
157
|
+
for e in enum_class:
|
|
158
|
+
value = repr(e.value)
|
|
159
|
+
print(f" {e.name} = {value}", file=target)
|
|
160
|
+
|
|
161
|
+
for e in enum_class:
|
|
162
|
+
if not e.__doc__ or e.__doc__ == enum_class.__doc__:
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
print(
|
|
166
|
+
f"{enum_class.__name__}.{e.name}.__doc__ = {repr(e.__doc__)}",
|
|
167
|
+
file=target,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def enum_class_to_code(enum_class: type[enum.Enum]) -> str:
|
|
172
|
+
"Returns an enumeration class as a class definition string."
|
|
173
|
+
|
|
174
|
+
with io.StringIO() as out:
|
|
175
|
+
enum_class_to_stream(enum_class, out)
|
|
176
|
+
return out.getvalue()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _wrap_print(text: str, file: TextIO) -> None:
|
|
180
|
+
if not text:
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
# wrap long lines
|
|
184
|
+
for line in textwrap.wrap(
|
|
185
|
+
text,
|
|
186
|
+
width=139,
|
|
187
|
+
initial_indent=" ",
|
|
188
|
+
subsequent_indent=" ",
|
|
189
|
+
break_long_words=False,
|
|
190
|
+
break_on_hyphens=False,
|
|
191
|
+
):
|
|
192
|
+
print(line, file=file)
|