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,320 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from ..model.data_types import SqlEnumType, SqlIntegerType, SqlVariableCharacterType, constant, quote
|
|
5
|
+
from ..model.id_types import SupportsName
|
|
6
|
+
from .object_types import (
|
|
7
|
+
Catalog,
|
|
8
|
+
Column,
|
|
9
|
+
ColumnFormationError,
|
|
10
|
+
EnumType,
|
|
11
|
+
FormationError,
|
|
12
|
+
Namespace,
|
|
13
|
+
StatementList,
|
|
14
|
+
StructType,
|
|
15
|
+
Table,
|
|
16
|
+
TableFormationError,
|
|
17
|
+
join_or_none,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class MutatorOptions:
|
|
23
|
+
"""
|
|
24
|
+
Options for synchronizing an existing source state with a desired target state.
|
|
25
|
+
|
|
26
|
+
:param allow_drop_enum: Permit dropping enumeration types.
|
|
27
|
+
:param allow_drop_struct: Permit dropping (non-table) structure types.
|
|
28
|
+
:param allow_drop_table: Permit dropping tables.
|
|
29
|
+
:param allow_drop_namespace: Permit dropping namespaces (database schemas) and all objects within.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
allow_drop_enum: bool = True
|
|
33
|
+
allow_drop_struct: bool = True
|
|
34
|
+
allow_drop_table: bool = True
|
|
35
|
+
allow_drop_namespace: bool = True
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Mutator:
|
|
39
|
+
"Morphs an existing source state into a desired target state."
|
|
40
|
+
|
|
41
|
+
options: MutatorOptions
|
|
42
|
+
|
|
43
|
+
def __init__(self, options: Optional[MutatorOptions] = None) -> None:
|
|
44
|
+
self.options = options or MutatorOptions()
|
|
45
|
+
|
|
46
|
+
def check_identity(self, source: SupportsName, target: SupportsName) -> None:
|
|
47
|
+
if source.name != target.name:
|
|
48
|
+
raise FormationError(f"object mismatch: {source.name} != {target.name}")
|
|
49
|
+
|
|
50
|
+
def migrate_enum_stmt(self, enum_type: EnumType, table: Table) -> Optional[str]:
|
|
51
|
+
enum_values = ", ".join(f"({quote(v)})" for v in enum_type.values)
|
|
52
|
+
return f'INSERT INTO {table.name} ("value") VALUES {enum_values};'
|
|
53
|
+
|
|
54
|
+
def mutate_enum_stmt(self, source: EnumType, target: EnumType) -> Optional[str]:
|
|
55
|
+
self.check_identity(source, target)
|
|
56
|
+
|
|
57
|
+
removed_values = [value for value in source.values if value not in target.values]
|
|
58
|
+
if removed_values:
|
|
59
|
+
raise FormationError(f"operation not permitted; cannot drop values in an enumeration: {''.join(removed_values)}")
|
|
60
|
+
|
|
61
|
+
added_values = [value for value in target.values if value not in source.values]
|
|
62
|
+
if added_values:
|
|
63
|
+
return "\n".join(f"ALTER TYPE {source.name} ADD VALUE {constant(v)};" for v in added_values)
|
|
64
|
+
else:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
def mutate_struct_stmt(self, source: StructType, target: StructType) -> Optional[str]:
|
|
68
|
+
self.check_identity(source, target)
|
|
69
|
+
|
|
70
|
+
statements: list[str] = []
|
|
71
|
+
statements.extend(f"DROP ATTRIBUTE {member.name}" for member in source.members.difference(target.members))
|
|
72
|
+
for source_member, target_member in source.members.intersection(target.members):
|
|
73
|
+
if source_member != target_member:
|
|
74
|
+
statements.append(f"ALTER ATTRIBUTE {source_member.name} SET DATA TYPE {target_member.data_type}")
|
|
75
|
+
statements.extend(f"ADD ATTRIBUTE {member}" for member in target.members.difference(source.members))
|
|
76
|
+
if statements:
|
|
77
|
+
return f"ALTER TYPE {source.name}\n" + ",\n".join(statements) + ";\n"
|
|
78
|
+
else:
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
def migrate_column_stmt(self, source_table: Table, source: Column, target_table: Table, target: Column) -> Optional[str]:
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
def is_column_migrated(self, source: Column, target: Column) -> Optional[bool]:
|
|
85
|
+
"""
|
|
86
|
+
True if the column requires data migration, false if no migration is needed.
|
|
87
|
+
|
|
88
|
+
:param source: The source column to convert data from.
|
|
89
|
+
:param target: The target column to convert data to.
|
|
90
|
+
:returns: True/False, or None if subclass method is to decide.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
# no migration is needed if column data type is unchanged
|
|
94
|
+
if source == target or source.data_type == target.data_type:
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
# check if target data type is a primary key type
|
|
98
|
+
if isinstance(target.data_type, SqlIntegerType):
|
|
99
|
+
if isinstance(source.data_type, SqlEnumType):
|
|
100
|
+
return True
|
|
101
|
+
# some database engines represent enumerations as `varchar`
|
|
102
|
+
elif isinstance(source.data_type, SqlVariableCharacterType):
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
return None # undecided (converts to False in a Boolean expression)
|
|
106
|
+
|
|
107
|
+
def mutate_column_type(self, source: Column, target: Column) -> Optional[str]:
|
|
108
|
+
if source.data_type != target.data_type:
|
|
109
|
+
return f"SET DATA TYPE {target.data_type}"
|
|
110
|
+
else:
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
def mutate_column_stmt(self, source: Column, target: Column) -> Optional[str]:
|
|
114
|
+
if source == target:
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
statements: list[str] = []
|
|
118
|
+
|
|
119
|
+
type_stmt = self.mutate_column_type(source, target)
|
|
120
|
+
if type_stmt:
|
|
121
|
+
statements.append(type_stmt)
|
|
122
|
+
|
|
123
|
+
if source.nullable and not target.nullable:
|
|
124
|
+
statements.append("SET NOT NULL")
|
|
125
|
+
elif not source.nullable and target.nullable:
|
|
126
|
+
statements.append("DROP NOT NULL")
|
|
127
|
+
|
|
128
|
+
if source.default is not None and target.default is None:
|
|
129
|
+
statements.append("DROP DEFAULT")
|
|
130
|
+
elif source.default != target.default:
|
|
131
|
+
statements.append(f"SET DEFAULT {target.default}")
|
|
132
|
+
|
|
133
|
+
if source.identity and not target.identity:
|
|
134
|
+
statements.append("DROP IDENTITY")
|
|
135
|
+
elif not source.identity and target.identity:
|
|
136
|
+
statements.append("ADD GENERATED BY DEFAULT AS IDENTITY")
|
|
137
|
+
|
|
138
|
+
if statements:
|
|
139
|
+
return ",\n".join(f"ALTER COLUMN {source.name} {s}" for s in statements)
|
|
140
|
+
else:
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
def mutate_table_stmt(self, source: Table, target: Table) -> Optional[str]:
|
|
144
|
+
self.check_identity(source, target)
|
|
145
|
+
|
|
146
|
+
statements: list[Optional[str]] = []
|
|
147
|
+
|
|
148
|
+
create_columns = list(target.columns.difference(source.columns))
|
|
149
|
+
common_columns = list(source.columns.intersection(target.columns))
|
|
150
|
+
drop_columns = list(source.columns.difference(target.columns))
|
|
151
|
+
migrated_columns: list[tuple[Column, Column]] = []
|
|
152
|
+
|
|
153
|
+
# accumulate object creation statements
|
|
154
|
+
create_stmts: list[str] = []
|
|
155
|
+
|
|
156
|
+
# rename conflicting columns
|
|
157
|
+
try:
|
|
158
|
+
for pair in common_columns:
|
|
159
|
+
source_column, target_column = pair
|
|
160
|
+
if self.is_column_migrated(source_column, target_column):
|
|
161
|
+
statements.append(source.alter_table_stmt([source_column.soft_drop_stmt()]))
|
|
162
|
+
create_stmts.append(target_column.create_stmt())
|
|
163
|
+
migrated_columns.append(pair)
|
|
164
|
+
except ColumnFormationError as e:
|
|
165
|
+
raise TableFormationError("failed to migrate columns in table", target.name) from e
|
|
166
|
+
|
|
167
|
+
# create new columns
|
|
168
|
+
try:
|
|
169
|
+
create_stmts.extend(column.create_stmt() for column in create_columns)
|
|
170
|
+
except ColumnFormationError as e:
|
|
171
|
+
raise TableFormationError("failed to create columns in table", target.name) from e
|
|
172
|
+
|
|
173
|
+
if create_stmts:
|
|
174
|
+
statements.append(source.alter_table_stmt(create_stmts))
|
|
175
|
+
|
|
176
|
+
# accumulate object mutation statements
|
|
177
|
+
alter_stmts: list[Optional[str]] = []
|
|
178
|
+
|
|
179
|
+
# migrate data when data type changes
|
|
180
|
+
try:
|
|
181
|
+
for source_column, target_column in migrated_columns:
|
|
182
|
+
statements.append(self.migrate_column_stmt(source, source_column, target, target_column))
|
|
183
|
+
alter_stmts.append(source_column.hard_drop_stmt())
|
|
184
|
+
except ColumnFormationError as e:
|
|
185
|
+
raise TableFormationError("failed to migrate columns in table", target.name) from e
|
|
186
|
+
|
|
187
|
+
# mutate existing columns as necessary
|
|
188
|
+
try:
|
|
189
|
+
for source_column, target_column in common_columns:
|
|
190
|
+
alter_stmts.append(self.mutate_column_stmt(source_column, target_column))
|
|
191
|
+
except ColumnFormationError as e:
|
|
192
|
+
raise TableFormationError("failed to update columns in table", target.name) from e
|
|
193
|
+
|
|
194
|
+
# remove deleted columns
|
|
195
|
+
try:
|
|
196
|
+
alter_stmts.extend(column.drop_stmt() for column in drop_columns)
|
|
197
|
+
except ColumnFormationError as e:
|
|
198
|
+
raise TableFormationError("failed to drop columns in table", target.name) from e
|
|
199
|
+
|
|
200
|
+
alter_stmts.extend(
|
|
201
|
+
f"DROP CONSTRAINT {constraint.name}"
|
|
202
|
+
for constraint in source.constraints.difference(target.constraints)
|
|
203
|
+
if constraint.is_alter_table()
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# remove outdated constraints
|
|
207
|
+
common_constraints = source.constraints.intersection(target.constraints)
|
|
208
|
+
for source_constraint, target_constraint in common_constraints:
|
|
209
|
+
if source_constraint != target_constraint:
|
|
210
|
+
raise TableFormationError(
|
|
211
|
+
f"failed to mutate constraint `{source_constraint.name}`",
|
|
212
|
+
target.name,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# add new constraints
|
|
216
|
+
alter_stmts.extend(
|
|
217
|
+
f"ADD CONSTRAINT {constraint.spec}"
|
|
218
|
+
for constraint in target.constraints.difference(source.constraints)
|
|
219
|
+
if constraint.is_alter_table()
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
stmts = [s for s in alter_stmts if s is not None]
|
|
223
|
+
if stmts:
|
|
224
|
+
statements.append(source.alter_table_stmt(stmts))
|
|
225
|
+
|
|
226
|
+
return join_or_none(statements)
|
|
227
|
+
|
|
228
|
+
def mutate_namespace_stmt(self, source: Namespace, target: Namespace) -> Optional[str]:
|
|
229
|
+
self.check_identity(source, target)
|
|
230
|
+
|
|
231
|
+
statements: StatementList = StatementList()
|
|
232
|
+
|
|
233
|
+
# identify objects to create
|
|
234
|
+
enum_create = list(target.enums.difference(source.enums))
|
|
235
|
+
struct_create = list(target.structs.difference(source.structs))
|
|
236
|
+
table_create = list(target.tables.difference(source.tables))
|
|
237
|
+
|
|
238
|
+
# identify objects to drop
|
|
239
|
+
enum_drop = list(source.enums.difference(target.enums))
|
|
240
|
+
struct_drop = list(source.structs.difference(target.structs))
|
|
241
|
+
table_drop = list(source.tables.difference(target.tables))
|
|
242
|
+
enum_soft_deleted: list[EnumType] = []
|
|
243
|
+
|
|
244
|
+
# create new objects
|
|
245
|
+
statements.extend(enum.create_stmt() for enum in enum_create)
|
|
246
|
+
statements.extend(struct.create_stmt() for struct in struct_create)
|
|
247
|
+
|
|
248
|
+
for enum_type in enum_drop:
|
|
249
|
+
for table in table_create:
|
|
250
|
+
if enum_type.name != table.name:
|
|
251
|
+
continue
|
|
252
|
+
statements.append(enum_type.soft_drop_stmt())
|
|
253
|
+
enum_soft_deleted.append(enum_type)
|
|
254
|
+
|
|
255
|
+
statements.extend(table.create_stmt() for table in table_create)
|
|
256
|
+
statements.extend(table.add_constraints_stmt() for table in table_create)
|
|
257
|
+
|
|
258
|
+
# populate new objects with data
|
|
259
|
+
for enum_type in enum_drop:
|
|
260
|
+
for table in table_create:
|
|
261
|
+
if enum_type.name != table.name:
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
statements.append(self.migrate_enum_stmt(enum_type, table))
|
|
265
|
+
|
|
266
|
+
# mutate existing object
|
|
267
|
+
enum_mutate = list(source.enums.intersection(target.enums))
|
|
268
|
+
statements.extend(self.mutate_enum_stmt(source_enum, target_enum) for source_enum, target_enum in enum_mutate)
|
|
269
|
+
|
|
270
|
+
struct_mutate = list(source.structs.intersection(target.structs))
|
|
271
|
+
statements.extend(self.mutate_struct_stmt(source_struct, target_struct) for source_struct, target_struct in struct_mutate)
|
|
272
|
+
|
|
273
|
+
table_mutate = list(source.tables.intersection(target.tables))
|
|
274
|
+
statements.extend(self.mutate_table_stmt(source_table, target_table) for source_table, target_table in table_mutate)
|
|
275
|
+
|
|
276
|
+
# drop old objects
|
|
277
|
+
if self.options.allow_drop_table:
|
|
278
|
+
statements.extend(table.drop_stmt() for table in table_drop)
|
|
279
|
+
|
|
280
|
+
if self.options.allow_drop_struct:
|
|
281
|
+
statements.extend(struct.drop_stmt() for struct in struct_drop)
|
|
282
|
+
|
|
283
|
+
if self.options.allow_drop_enum:
|
|
284
|
+
for enum_type in enum_drop:
|
|
285
|
+
statements.append(enum_type.hard_drop_stmt() if enum_type in enum_soft_deleted else enum_type.drop_stmt())
|
|
286
|
+
|
|
287
|
+
return join_or_none(statements)
|
|
288
|
+
|
|
289
|
+
def mutate_catalog_stmt(self, source: Catalog, target: Catalog) -> Optional[str]:
|
|
290
|
+
statements: StatementList = StatementList()
|
|
291
|
+
|
|
292
|
+
source.sort()
|
|
293
|
+
target.sort()
|
|
294
|
+
|
|
295
|
+
ns_create = list(target.namespaces.difference(source.namespaces))
|
|
296
|
+
ns_drop = list(source.namespaces.difference(target.namespaces))
|
|
297
|
+
ns_mutate = list(source.namespaces.intersection(target.namespaces))
|
|
298
|
+
|
|
299
|
+
# create new namespaces
|
|
300
|
+
statements.extend(namespace.create_schema_stmt() for namespace in ns_create)
|
|
301
|
+
|
|
302
|
+
# create objects in each namespace added
|
|
303
|
+
statements.extend(namespace.create_objects_stmt() for namespace in ns_create)
|
|
304
|
+
|
|
305
|
+
# add new constraints
|
|
306
|
+
statements.extend(namespace.add_constraints_stmt() for namespace in ns_create)
|
|
307
|
+
|
|
308
|
+
# mutate existing namespaces
|
|
309
|
+
statements.extend(self.mutate_namespace_stmt(source_ns, target_ns) for source_ns, target_ns in ns_mutate)
|
|
310
|
+
|
|
311
|
+
# remove old constraints
|
|
312
|
+
statements.extend(namespace.drop_constraints_stmt() for namespace in ns_drop)
|
|
313
|
+
if self.options.allow_drop_namespace:
|
|
314
|
+
# drop objects in each namespace removed
|
|
315
|
+
statements.extend(namespace.drop_objects_stmt() for namespace in ns_drop)
|
|
316
|
+
|
|
317
|
+
# drop old namespaces
|
|
318
|
+
statements.extend(namespace.drop_schema_stmt() for namespace in ns_drop)
|
|
319
|
+
|
|
320
|
+
return join_or_none(statements)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from collections.abc import ItemsView, KeysView, Mapping, ValuesView
|
|
2
|
+
from typing import Generic, Iterable, Iterator, Optional, TypeVar, Union, overload
|
|
3
|
+
|
|
4
|
+
from ..model.id_types import SupportsName
|
|
5
|
+
|
|
6
|
+
T = TypeVar("T")
|
|
7
|
+
ObjectItem = TypeVar("ObjectItem", bound=SupportsName)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ObjectDict(Generic[ObjectItem], Mapping[str, ObjectItem]):
|
|
11
|
+
_items: dict[str, ObjectItem]
|
|
12
|
+
|
|
13
|
+
def __init__(self, items: Iterable[ObjectItem]) -> None:
|
|
14
|
+
self._items = {item.name.local_id: item for item in items}
|
|
15
|
+
|
|
16
|
+
def __contains__(self, key: object) -> bool:
|
|
17
|
+
return key in self._items
|
|
18
|
+
|
|
19
|
+
def __eq__(self, value: object) -> bool:
|
|
20
|
+
if not isinstance(value, ObjectDict):
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
return self._items == value._items # pyright: ignore[reportUnknownMemberType]
|
|
24
|
+
|
|
25
|
+
def __getitem__(self, key: str) -> ObjectItem:
|
|
26
|
+
return self._items[key]
|
|
27
|
+
|
|
28
|
+
def __iter__(self) -> Iterator[str]:
|
|
29
|
+
return iter(self._items)
|
|
30
|
+
|
|
31
|
+
def __len__(self) -> int:
|
|
32
|
+
return len(self._items)
|
|
33
|
+
|
|
34
|
+
def __repr__(self) -> str:
|
|
35
|
+
return repr(list(self._items.values()))
|
|
36
|
+
|
|
37
|
+
@overload
|
|
38
|
+
def get(self, key: str, /) -> Optional[ObjectItem]: ...
|
|
39
|
+
|
|
40
|
+
@overload
|
|
41
|
+
def get(self, key: str, /, default: Union[ObjectItem, T]) -> Union[ObjectItem, T]: ...
|
|
42
|
+
|
|
43
|
+
def get(self, key: str, /, default: Optional[T] = None) -> Union[None, ObjectItem, T]:
|
|
44
|
+
return self._items.get(key, default)
|
|
45
|
+
|
|
46
|
+
def add(self, item: ObjectItem) -> None:
|
|
47
|
+
if item.name.local_id in self._items:
|
|
48
|
+
raise ValueError(f"item already in collection: {item.name}")
|
|
49
|
+
|
|
50
|
+
self._items[item.name.local_id] = item
|
|
51
|
+
|
|
52
|
+
def remove(self, key: str) -> None:
|
|
53
|
+
self._items.pop(key)
|
|
54
|
+
|
|
55
|
+
def keys(self) -> KeysView[str]:
|
|
56
|
+
return self._items.keys()
|
|
57
|
+
|
|
58
|
+
def values(self) -> ValuesView[ObjectItem]:
|
|
59
|
+
return self._items.values()
|
|
60
|
+
|
|
61
|
+
def items(self) -> ItemsView[str, ObjectItem]:
|
|
62
|
+
return self._items.items()
|
|
63
|
+
|
|
64
|
+
def difference(self, op: "ObjectDict[ObjectItem]") -> Iterable["ObjectItem"]:
|
|
65
|
+
for key, item in self.items():
|
|
66
|
+
if key not in op:
|
|
67
|
+
yield item
|
|
68
|
+
|
|
69
|
+
def intersection(self, op: "ObjectDict[ObjectItem]") -> Iterable[tuple["ObjectItem", "ObjectItem"]]:
|
|
70
|
+
for key, item in self.items():
|
|
71
|
+
op_item = op.get(key)
|
|
72
|
+
if op_item is not None:
|
|
73
|
+
yield (item, op_item)
|
|
74
|
+
|
|
75
|
+
def sort(self) -> None:
|
|
76
|
+
self._items = dict(sorted(self._items.items()))
|
|
77
|
+
|
|
78
|
+
def reorder(self, order: list[str]) -> None:
|
|
79
|
+
self._items = {key: self._items[key] for key in order}
|