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.
Files changed (102) hide show
  1. instructure_pysqlsync-0.8.5.dev0.dist-info/METADATA +244 -0
  2. instructure_pysqlsync-0.8.5.dev0.dist-info/RECORD +102 -0
  3. instructure_pysqlsync-0.8.5.dev0.dist-info/WHEEL +5 -0
  4. instructure_pysqlsync-0.8.5.dev0.dist-info/licenses/LICENSE +21 -0
  5. instructure_pysqlsync-0.8.5.dev0.dist-info/top_level.txt +1 -0
  6. instructure_pysqlsync-0.8.5.dev0.dist-info/zip-safe +1 -0
  7. pysqlsync/__init__.py +15 -0
  8. pysqlsync/base.py +1386 -0
  9. pysqlsync/connection.py +101 -0
  10. pysqlsync/data/__init__.py +0 -0
  11. pysqlsync/data/exchange.py +184 -0
  12. pysqlsync/data/generator.py +305 -0
  13. pysqlsync/dialect/README.md +35 -0
  14. pysqlsync/dialect/__init__.py +0 -0
  15. pysqlsync/dialect/delta/__init__.py +0 -0
  16. pysqlsync/dialect/delta/data_types.py +51 -0
  17. pysqlsync/dialect/delta/dependency.py +7 -0
  18. pysqlsync/dialect/delta/engine.py +18 -0
  19. pysqlsync/dialect/delta/generator.py +74 -0
  20. pysqlsync/dialect/delta/object_types.py +87 -0
  21. pysqlsync/dialect/mssql/README.md +23 -0
  22. pysqlsync/dialect/mssql/__init__.py +0 -0
  23. pysqlsync/dialect/mssql/connection.py +146 -0
  24. pysqlsync/dialect/mssql/data_types.py +113 -0
  25. pysqlsync/dialect/mssql/dependency.py +9 -0
  26. pysqlsync/dialect/mssql/discovery.py +99 -0
  27. pysqlsync/dialect/mssql/engine.py +20 -0
  28. pysqlsync/dialect/mssql/generator.py +88 -0
  29. pysqlsync/dialect/mssql/mutation.py +46 -0
  30. pysqlsync/dialect/mssql/object_types.py +135 -0
  31. pysqlsync/dialect/mysql/__init__.py +0 -0
  32. pysqlsync/dialect/mysql/connection.py +126 -0
  33. pysqlsync/dialect/mysql/data_types.py +40 -0
  34. pysqlsync/dialect/mysql/dependency.py +10 -0
  35. pysqlsync/dialect/mysql/discovery.py +145 -0
  36. pysqlsync/dialect/mysql/engine.py +20 -0
  37. pysqlsync/dialect/mysql/generator.py +140 -0
  38. pysqlsync/dialect/mysql/mutation.py +65 -0
  39. pysqlsync/dialect/mysql/object_types.py +75 -0
  40. pysqlsync/dialect/oracle/README.md +22 -0
  41. pysqlsync/dialect/oracle/__init__.py +0 -0
  42. pysqlsync/dialect/oracle/connection.py +141 -0
  43. pysqlsync/dialect/oracle/data_types.py +104 -0
  44. pysqlsync/dialect/oracle/dependency.py +9 -0
  45. pysqlsync/dialect/oracle/discovery.py +231 -0
  46. pysqlsync/dialect/oracle/engine.py +20 -0
  47. pysqlsync/dialect/oracle/generator.py +93 -0
  48. pysqlsync/dialect/oracle/mutation.py +37 -0
  49. pysqlsync/dialect/oracle/object_types.py +59 -0
  50. pysqlsync/dialect/postgresql/__init__.py +0 -0
  51. pysqlsync/dialect/postgresql/connection.py +133 -0
  52. pysqlsync/dialect/postgresql/data_types.py +6 -0
  53. pysqlsync/dialect/postgresql/dependency.py +9 -0
  54. pysqlsync/dialect/postgresql/discovery.py +392 -0
  55. pysqlsync/dialect/postgresql/engine.py +20 -0
  56. pysqlsync/dialect/postgresql/generator.py +99 -0
  57. pysqlsync/dialect/postgresql/mutation.py +82 -0
  58. pysqlsync/dialect/postgresql/object_types.py +99 -0
  59. pysqlsync/dialect/redshift/__init__.py +0 -0
  60. pysqlsync/dialect/redshift/connection.py +156 -0
  61. pysqlsync/dialect/redshift/data_types.py +10 -0
  62. pysqlsync/dialect/redshift/dependency.py +9 -0
  63. pysqlsync/dialect/redshift/engine.py +20 -0
  64. pysqlsync/dialect/redshift/generator.py +82 -0
  65. pysqlsync/dialect/snowflake/__init__.py +0 -0
  66. pysqlsync/dialect/snowflake/data_types.py +18 -0
  67. pysqlsync/dialect/snowflake/dependency.py +9 -0
  68. pysqlsync/dialect/snowflake/engine.py +18 -0
  69. pysqlsync/dialect/snowflake/generator.py +64 -0
  70. pysqlsync/dialect/snowflake/object_types.py +84 -0
  71. pysqlsync/dialect/test/__init__.py +0 -0
  72. pysqlsync/dialect/test/dependency.py +10 -0
  73. pysqlsync/dialect/trino/__init__.py +0 -0
  74. pysqlsync/dialect/trino/connection.py +86 -0
  75. pysqlsync/dialect/trino/dependency.py +9 -0
  76. pysqlsync/dialect/trino/discovery.py +21 -0
  77. pysqlsync/dialect/trino/engine.py +20 -0
  78. pysqlsync/dialect/trino/generator.py +31 -0
  79. pysqlsync/factory.py +169 -0
  80. pysqlsync/formation/__init__.py +0 -0
  81. pysqlsync/formation/constraints.py +78 -0
  82. pysqlsync/formation/data_types.py +192 -0
  83. pysqlsync/formation/discovery.py +390 -0
  84. pysqlsync/formation/inspection.py +179 -0
  85. pysqlsync/formation/mutation.py +320 -0
  86. pysqlsync/formation/object_dict.py +79 -0
  87. pysqlsync/formation/object_types.py +872 -0
  88. pysqlsync/formation/py_to_sql.py +1134 -0
  89. pysqlsync/formation/sql_to_py.py +194 -0
  90. pysqlsync/model/__init__.py +0 -0
  91. pysqlsync/model/data_types.py +397 -0
  92. pysqlsync/model/entity_types.py +68 -0
  93. pysqlsync/model/id_types.py +193 -0
  94. pysqlsync/model/key_types.py +38 -0
  95. pysqlsync/model/properties.py +234 -0
  96. pysqlsync/py.typed +0 -0
  97. pysqlsync/python_types.py +192 -0
  98. pysqlsync/resultset.py +106 -0
  99. pysqlsync/util/__init__.py +0 -0
  100. pysqlsync/util/dataclasses.py +80 -0
  101. pysqlsync/util/dispatch.py +28 -0
  102. pysqlsync/util/typing.py +13 -0
@@ -0,0 +1,93 @@
1
+ import datetime
2
+ import ipaddress
3
+ import uuid
4
+ from typing import Any, Callable, Optional
5
+
6
+ from strong_typing.auxiliary import int8, int16, int32, int64
7
+ from strong_typing.core import JsonType
8
+
9
+ from pysqlsync.base import BaseGenerator, GeneratorOptions
10
+ from pysqlsync.dialect.oracle.mutation import OracleMutator
11
+ from pysqlsync.formation.inspection import is_ip_address_type
12
+ from pysqlsync.formation.object_types import Column
13
+ from pysqlsync.formation.py_to_sql import ArrayMode, DataclassConverter, DataclassConverterOptions, EnumMode, NamespaceMapping, StructMode
14
+ from pysqlsync.util.typing import override
15
+
16
+ from .data_types import OracleIntegerType, OracleTimestampType, OracleTimeType, OracleVariableBinaryType, OracleVariableCharacterType
17
+ from .object_types import OracleObjectFactory
18
+
19
+ MIN_DATETIME = datetime.datetime.min.replace(tzinfo=datetime.timezone.utc)
20
+ MIN_DATE = datetime.date.min
21
+
22
+
23
+ class OracleGenerator(BaseGenerator):
24
+ "Generator for Oracle."
25
+
26
+ converter: DataclassConverter
27
+
28
+ def __init__(self, options: GeneratorOptions) -> None:
29
+ super().__init__(options, OracleObjectFactory(), OracleMutator(options.synchronization))
30
+
31
+ self.check_enum_mode(exclude=[EnumMode.TYPE, EnumMode.INLINE])
32
+ self.check_struct_mode(matches=StructMode.JSON)
33
+ self.check_array_mode(include=[ArrayMode.JSON, ArrayMode.RELATION])
34
+
35
+ self.converter = DataclassConverter(
36
+ options=DataclassConverterOptions(
37
+ enum_mode=options.enum_mode or EnumMode.RELATION,
38
+ struct_mode=options.struct_mode or StructMode.JSON,
39
+ array_mode=options.array_mode or ArrayMode.JSON,
40
+ qualified_names=False,
41
+ namespaces=NamespaceMapping(options.namespaces),
42
+ foreign_constraints=options.foreign_constraints,
43
+ initialize_tables=options.initialize_tables,
44
+ substitutions={
45
+ bytes: OracleVariableBinaryType(),
46
+ datetime.time: OracleTimeType(),
47
+ datetime.datetime: OracleTimestampType(),
48
+ int: OracleIntegerType(),
49
+ int8: OracleIntegerType(),
50
+ int16: OracleIntegerType(),
51
+ int32: OracleIntegerType(),
52
+ int64: OracleIntegerType(),
53
+ str: OracleVariableCharacterType(),
54
+ uuid.UUID: OracleVariableBinaryType(16),
55
+ JsonType: OracleVariableCharacterType(),
56
+ ipaddress.IPv4Address: OracleVariableBinaryType(4),
57
+ ipaddress.IPv6Address: OracleVariableBinaryType(16),
58
+ },
59
+ factory=self.factory,
60
+ skip_annotations=options.skip_annotations,
61
+ auto_default=options.auto_default,
62
+ )
63
+ )
64
+
65
+ @override
66
+ def get_current_schema_stmt(self) -> str:
67
+ return "SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA')"
68
+
69
+ @override
70
+ def placeholder(self, index: int) -> str:
71
+ return f":{index}"
72
+
73
+ @override
74
+ def get_field_extractor(self, column: Column, field_name: str, field_type: type) -> Callable[[Any], Any]:
75
+ if field_type is uuid.UUID:
76
+ return lambda obj: getattr(obj, field_name).bytes
77
+ elif is_ip_address_type(field_type):
78
+ return lambda obj: getattr(obj, field_name).packed
79
+ elif field_type is datetime.time:
80
+ return lambda obj: datetime.datetime.combine(MIN_DATE, getattr(obj, field_name)) - MIN_DATETIME
81
+
82
+ return super().get_field_extractor(column, field_name, field_type)
83
+
84
+ @override
85
+ def get_value_transformer(self, column: Column, field_type: type) -> Optional[Callable[[Any], Any]]:
86
+ if field_type is uuid.UUID:
87
+ return lambda field: field.bytes
88
+ elif is_ip_address_type(field_type):
89
+ return lambda field: field.packed
90
+ elif field_type is datetime.time:
91
+ return lambda field: datetime.datetime.combine(datetime.date.min, field) - datetime.datetime.min
92
+
93
+ return super().get_value_transformer(column, field_type)
@@ -0,0 +1,37 @@
1
+ import typing
2
+ from typing import Optional
3
+
4
+ from pysqlsync.formation.mutation import Mutator
5
+ from pysqlsync.formation.object_types import Column
6
+
7
+ from .object_types import OracleColumn
8
+
9
+
10
+ class OracleMutator(Mutator):
11
+ def mutate_column_stmt(self, source: Column, target: Column) -> Optional[str]:
12
+ source_column = typing.cast(OracleColumn, source)
13
+ target_column = typing.cast(OracleColumn, target)
14
+
15
+ changes: list[str] = []
16
+ if source_column.data_type != target_column.data_type:
17
+ changes.append(str(target_column.data_type))
18
+ if source_column.default != target_column.default:
19
+ if target_column.default is not None:
20
+ changes.append(f"DEFAULT {target_column.default_expr}")
21
+ else:
22
+ changes.append("DEFAULT NULL")
23
+ if source_column.nullable != target_column.nullable:
24
+ if not target_column.nullable:
25
+ changes.append("NOT NULL")
26
+ else:
27
+ changes.append("NULL")
28
+ if source_column.identity != target_column.identity:
29
+ if target_column.identity:
30
+ changes.append("GENERATED BY DEFAULT AS IDENTITY")
31
+ else:
32
+ changes.append("DROP IDENTITY")
33
+
34
+ if changes:
35
+ return f"MODIFY {source_column.name} {' '.join(changes)}"
36
+ else:
37
+ return None
@@ -0,0 +1,59 @@
1
+ import re
2
+
3
+ from pysqlsync.formation.object_types import Column, EnumTable, ObjectFactory, Table
4
+ from pysqlsync.model.data_types import SqlTimestampType
5
+
6
+
7
+ class OracleColumn(Column):
8
+ @property
9
+ def default_expr(self) -> str:
10
+ if self.default is None:
11
+ raise ValueError("default value is NULL")
12
+
13
+ if isinstance(self.data_type, SqlTimestampType):
14
+ m = re.match(
15
+ r"^'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2}) (?P<hour>\d{2}):(?P<minute>\d{2}):(?P<second>\d{2})'$",
16
+ self.default,
17
+ )
18
+ if m:
19
+ return f"TIMESTAMP {self.default}"
20
+
21
+ return self.default
22
+
23
+ @property
24
+ def data_spec(self) -> str:
25
+ default = f" DEFAULT {self.default_expr}" if self.default is not None else ""
26
+ nullable = " NOT NULL" if not self.nullable and not self.identity else ""
27
+ identity = " GENERATED BY DEFAULT AS IDENTITY" if self.identity else ""
28
+ return f"{self.data_type}{default}{nullable}{identity}"
29
+
30
+
31
+ class OracleTable(Table):
32
+ def alter_table_stmt(self, statements: list[str]) -> str:
33
+ return "\n".join(f"ALTER TABLE {self.name} {statement};" for statement in statements)
34
+
35
+ def drop_if_exists_stmt(self) -> str:
36
+ return (
37
+ "BEGIN\n"
38
+ f" EXECUTE IMMEDIATE 'DROP TABLE {self.name} CASCADE CONSTRAINTS PURGE';\n"
39
+ "EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF;\n"
40
+ "END;"
41
+ )
42
+
43
+
44
+ class OracleEnumTable(EnumTable, OracleTable):
45
+ pass
46
+
47
+
48
+ class OracleObjectFactory(ObjectFactory):
49
+ @property
50
+ def column_class(self) -> type[Column]:
51
+ return OracleColumn
52
+
53
+ @property
54
+ def table_class(self) -> type[Table]:
55
+ return OracleTable
56
+
57
+ @property
58
+ def enum_table_class(self) -> type[EnumTable]:
59
+ return OracleEnumTable
File without changes
@@ -0,0 +1,133 @@
1
+ import dataclasses
2
+ import logging
3
+ import ssl
4
+ import typing
5
+ from typing import Iterable, Optional, TypeVar
6
+
7
+ import asyncpg
8
+ from strong_typing.inspection import DataclassInstance, is_dataclass_type
9
+
10
+ from pysqlsync.base import BaseConnection, BaseContext, ClassRef, DataSource
11
+ from pysqlsync.connection import ConnectionSSLMode, create_context
12
+ from pysqlsync.formation.object_types import Table
13
+ from pysqlsync.model.properties import is_identity_type
14
+ from pysqlsync.resultset import resultset_unwrap_dict, resultset_unwrap_tuple
15
+ from pysqlsync.util.typing import override
16
+
17
+ D = TypeVar("D", bound=DataclassInstance)
18
+ T = TypeVar("T")
19
+
20
+ LOGGER = logging.getLogger("pysqlsync.postgres")
21
+
22
+
23
+ class PostgreSQLConnection(BaseConnection):
24
+ native: asyncpg.Connection
25
+
26
+ @override
27
+ async def open(self) -> BaseContext:
28
+ LOGGER.info("connecting to %s", self.params)
29
+
30
+ ssl_mode = self.params.ssl
31
+ if ssl_mode is None or ssl_mode is ConnectionSSLMode.disable:
32
+ return await self._open()
33
+ elif ssl_mode is ConnectionSSLMode.prefer:
34
+ try:
35
+ return await self._open(create_context(ssl_mode))
36
+ except ConnectionError:
37
+ return await self._open()
38
+ elif ssl_mode is ConnectionSSLMode.allow:
39
+ try:
40
+ return await self._open()
41
+ except ConnectionError:
42
+ return await self._open(create_context(ssl_mode))
43
+ elif ssl_mode is ConnectionSSLMode.require or ssl_mode is ConnectionSSLMode.verify_ca or ssl_mode is ConnectionSSLMode.verify_full:
44
+ return await self._open(create_context(ssl_mode))
45
+ else:
46
+ raise ValueError(f"unsupported SSL mode: {ssl_mode}")
47
+
48
+ async def _open(self, ctx: Optional[ssl.SSLContext] = None) -> BaseContext:
49
+ conn = await asyncpg.connect(
50
+ host=self.params.host,
51
+ port=self.params.port,
52
+ user=self.params.username,
53
+ password=self.params.password,
54
+ database=self.params.database,
55
+ ssl=ctx,
56
+ )
57
+
58
+ ver = conn.get_server_version()
59
+ LOGGER.info(
60
+ "PostgreSQL version %d.%d.%d %s",
61
+ ver.major,
62
+ ver.minor,
63
+ ver.micro,
64
+ ver.releaselevel,
65
+ )
66
+
67
+ self.native = conn
68
+ return PostgreSQLContext(self)
69
+
70
+ @override
71
+ async def close(self) -> None:
72
+ await self.native.close()
73
+
74
+
75
+ class PostgreSQLContext(BaseContext):
76
+ def __init__(self, connection: PostgreSQLConnection) -> None:
77
+ super().__init__(connection)
78
+
79
+ @property
80
+ def native_connection(self) -> asyncpg.Connection:
81
+ return typing.cast(PostgreSQLConnection, self.connection).native
82
+
83
+ @override
84
+ async def _execute(self, statement: str) -> None:
85
+ await self.native_connection.execute(statement)
86
+
87
+ @override
88
+ async def _execute_all(self, statement: str, source: DataSource) -> None:
89
+ async for batch in source.batches():
90
+ await self.native_connection.executemany(statement, batch)
91
+
92
+ @override
93
+ async def _query_all(self, signature: type[T], statement: str) -> list[T]:
94
+ records: list[asyncpg.Record] = await self.native_connection.fetch(statement)
95
+ if is_dataclass_type(signature):
96
+ return resultset_unwrap_dict(signature, records) # type: ignore
97
+ else:
98
+ return resultset_unwrap_tuple(signature, records) # type: ignore
99
+
100
+ @override
101
+ async def insert_data(self, table: type[D], data: Iterable[D]) -> None:
102
+ if not is_dataclass_type(table):
103
+ raise TypeError(f"expected dataclass type, got: {table}")
104
+ generator = self.connection.generator
105
+ table_name = generator.get_qualified_id(ClassRef(table))
106
+ records = generator.get_dataclasses_as_records(table, data, skip_identity=True)
107
+ result = await self.native_connection.copy_records_to_table(
108
+ schema_name=table_name.scope_id,
109
+ table_name=table_name.local_id,
110
+ columns=tuple(field.name for field in dataclasses.fields(table) if not is_identity_type(field.type)),
111
+ records=records,
112
+ )
113
+ LOGGER.debug(result)
114
+
115
+ @override
116
+ async def _insert_rows(
117
+ self,
118
+ table: Table,
119
+ source: DataSource,
120
+ *,
121
+ field_types: tuple[type, ...],
122
+ field_names: Optional[tuple[str, ...]] = None,
123
+ ) -> None:
124
+ record_generator = await self._generate_records(table, source, field_types=field_types, field_names=field_names)
125
+ order = tuple(name for name in field_names if name) if field_names else None
126
+ async for batch in record_generator.batches():
127
+ result = await self.native_connection.copy_records_to_table(
128
+ schema_name=table.name.scope_id,
129
+ table_name=table.name.local_id,
130
+ columns=[str(col.name.local_id) for col in table.get_columns(order)],
131
+ records=batch,
132
+ )
133
+ LOGGER.debug(result)
@@ -0,0 +1,6 @@
1
+ from pysqlsync.model.data_types import SqlJsonType
2
+
3
+
4
+ class PostgreSQLJsonType(SqlJsonType):
5
+ def __str__(self) -> str:
6
+ return "jsonb"
@@ -0,0 +1,9 @@
1
+ """
2
+ pysqlsync: Synchronize schema and large volumes of data.
3
+
4
+ This module defines dependencies required for PostgreSQL.
5
+
6
+ :see: https://github.com/instructure-internal/pysqlsync
7
+ """
8
+
9
+ import asyncpg # pyright: ignore[reportUnusedImport] # noqa: F401