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
pysqlsync/resultset.py ADDED
@@ -0,0 +1,106 @@
1
+ """
2
+ pysqlsync: Synchronize schema and large volumes of data.
3
+
4
+ This module helps convert between data-class, dictionary and tuple representation of result-sets.
5
+
6
+ :see: https://github.com/instructure-internal/pysqlsync
7
+ """
8
+
9
+ import typing
10
+ from collections.abc import Sequence
11
+ from typing import Any, Iterable, TypeVar
12
+
13
+ from strong_typing.inspection import DataclassInstance, is_dataclass_type
14
+
15
+ D = TypeVar("D", bound=DataclassInstance)
16
+ T = TypeVar("T")
17
+
18
+
19
+ def resultset_unwrap_dict(signature: type[D], records: Iterable[dict[str, Any]]) -> list[D]:
20
+ """
21
+ Converts a result-set into a list of data-class instances.
22
+
23
+ :param signature: A data-class type.
24
+ :param records: The result-set whose rows to convert.
25
+ """
26
+
27
+ if not is_dataclass_type(signature):
28
+ raise TypeError(f"expected: data-class type as result-set signature; got: {signature}")
29
+
30
+ return [signature(**{name: value for name, value in record.items()}) for record in records]
31
+
32
+
33
+ def resultset_unwrap_object(signature: type[D], records: Iterable[Any]) -> list[D]:
34
+ """
35
+ Converts a result-set into a list of data-class instances.
36
+
37
+ :param signature: A data-class type.
38
+ :param records: The result-set whose rows to convert.
39
+ """
40
+
41
+ if not is_dataclass_type(signature):
42
+ raise TypeError(f"expected: data-class type as result-set signature; got: {signature}")
43
+
44
+ names = [name for name in signature.__dataclass_fields__.keys()] # pyright: ignore[reportUnknownMemberType]
45
+ return [signature(**{name: record.__getattribute__(name) for name in names}) for record in records]
46
+
47
+
48
+ def resultset_unwrap_tuple(signature: type[T], records: Iterable[Sequence[Any]]) -> list[T]:
49
+ """
50
+ Converts a result-set into a list of tuples, or a list of simple types (as appropriate).
51
+
52
+ :param signature: A tuple type, or a simple type (e.g. `bool` or `str`).
53
+ :param records: The result-set whose rows to convert.
54
+ """
55
+
56
+ if signature in [bool, int, float, str]:
57
+ scalar_results: list[T] = []
58
+
59
+ # check result shape
60
+ it = iter(records)
61
+ try:
62
+ item = next(it)
63
+ except StopIteration:
64
+ return []
65
+ if len(item) != 1:
66
+ raise ValueError(f"invalid number of columns, expected: 1; got: {len(item)}")
67
+ scalar_results.append(item[0])
68
+ while True:
69
+ try:
70
+ item = next(it)
71
+ except StopIteration:
72
+ return scalar_results
73
+ scalar_results.append(item[0])
74
+
75
+ origin_type = typing.get_origin(signature)
76
+ if origin_type is tuple:
77
+ origin_args = typing.get_args(signature)
78
+ results: list[T] = []
79
+
80
+ # check result shape
81
+ it = iter(records)
82
+ try:
83
+ item = next(it)
84
+ except StopIteration:
85
+ return []
86
+ if len(item) != len(origin_args):
87
+ raise ValueError(f"invalid number of columns, expected: {len(origin_args)}; got: {len(item)}")
88
+
89
+ if isinstance(item, tuple):
90
+ results.append(item) # type: ignore
91
+ while True:
92
+ try:
93
+ item = next(it)
94
+ except StopIteration:
95
+ return results
96
+ results.append(item) # type: ignore
97
+ else:
98
+ results.append(tuple(item)) # type: ignore
99
+ while True:
100
+ try:
101
+ item = next(it)
102
+ except StopIteration:
103
+ return results
104
+ results.append(tuple(item)) # type: ignore
105
+
106
+ raise TypeError(f"expected: tuple or simple type as result-set signature; got: {signature}")
File without changes
@@ -0,0 +1,80 @@
1
+ import dataclasses
2
+ import sys
3
+ import types
4
+ from typing import Optional
5
+
6
+ from strong_typing.docstring import Docstring, DocstringParam, parse_type
7
+ from strong_typing.inspection import DataclassField, DataclassInstance, dataclass_fields
8
+
9
+ from ..formation.inspection import is_struct_type
10
+ from ..model.properties import get_field_properties
11
+
12
+
13
+ def make_dataclass(
14
+ name: str,
15
+ fields: list[DataclassField],
16
+ *,
17
+ module: Optional[types.ModuleType] = None,
18
+ docstring: Optional[Docstring] = None,
19
+ ) -> type[DataclassInstance]:
20
+ """
21
+ Dynamically creates a new data class type.
22
+
23
+ :param name: Name of the newly created type.
24
+ :param fields: Fields of the new data class type to create.
25
+ :param module: The module to assign the new type to.
26
+ :param docstring: The doc-string to set on the newly created type.
27
+ """
28
+
29
+ field_names = set(field.name for field in fields)
30
+
31
+ if docstring is not None:
32
+ docstring.params = {name: param for name, param in docstring.params.items() if name in field_names}
33
+
34
+ dataclass_fields = [(field.name, field.type, field.default) for field in fields]
35
+ module_name = module.__name__ if module is not None else None
36
+ if sys.version_info >= (3, 12):
37
+ data_type = dataclasses.make_dataclass(name, dataclass_fields, module=module_name)
38
+ else:
39
+ namespace = {"__module__": module_name} if module is not None else None
40
+ data_type = dataclasses.make_dataclass(name, dataclass_fields, namespace=namespace)
41
+ if module is not None:
42
+ setattr(module, data_type.__name__, data_type)
43
+
44
+ if docstring is not None:
45
+ data_type.__doc__ = str(docstring)
46
+
47
+ return data_type
48
+
49
+
50
+ def flatten_dataclass(
51
+ cls: type[DataclassInstance],
52
+ ) -> tuple[list[DataclassField], Docstring]:
53
+ """
54
+ Extracts nested structures into a flat list of data-class fields.
55
+ """
56
+
57
+ fields: list[DataclassField] = []
58
+ docstring = parse_type(cls)
59
+
60
+ for field in dataclass_fields(cls):
61
+ props = get_field_properties(field.type)
62
+ plain_type = props.plain_type
63
+ if is_struct_type(plain_type):
64
+ nested_docstring = parse_type(plain_type)
65
+ for subfield in dataclass_fields(plain_type):
66
+ name = f"{field.name}__{subfield.name}"
67
+ fields.append(
68
+ DataclassField(
69
+ name,
70
+ subfield.type,
71
+ subfield.default,
72
+ )
73
+ )
74
+ param = nested_docstring.params.get(subfield.name)
75
+ if param is not None:
76
+ docstring.params[name] = DocstringParam(name, param.description, param.param_type)
77
+ else:
78
+ fields.append(field)
79
+
80
+ return fields, docstring
@@ -0,0 +1,28 @@
1
+ import asyncio
2
+ import functools
3
+ import inspect
4
+ from concurrent.futures import ThreadPoolExecutor
5
+ from typing import Callable, Coroutine, TypeVar
6
+
7
+ from .typing import ParamSpec
8
+
9
+ R = TypeVar("R")
10
+ P = ParamSpec("P")
11
+
12
+
13
+ def thread_dispatch(fn: Callable[P, R]) -> Callable[P, Coroutine[None, None, R]]:
14
+ "A decorator to transform a synchronous function into an asynchronous function dispatched to a thread pool."
15
+
16
+ if not callable(fn):
17
+ raise TypeError("expected: a callable")
18
+
19
+ if inspect.iscoroutinefunction(fn):
20
+ raise TypeError("expected: a regular function; got: an async function")
21
+
22
+ @functools.wraps(fn)
23
+ async def invoke(*args: P.args, **kwargs: P.kwargs) -> R:
24
+ loop = asyncio.get_running_loop()
25
+ with ThreadPoolExecutor(max_workers=1) as pool:
26
+ return await loop.run_in_executor(pool, functools.partial(fn, *args, **kwargs))
27
+
28
+ return invoke
@@ -0,0 +1,13 @@
1
+ import sys
2
+
3
+ if sys.version_info >= (3, 10):
4
+ from typing import ParamSpec as ParamSpec # noqa: F401
5
+ from typing import TypeGuard as TypeGuard # noqa: F401
6
+ else:
7
+ from typing_extensions import ParamSpec as ParamSpec # noqa: F401
8
+ from typing_extensions import TypeGuard as TypeGuard # noqa: F401
9
+
10
+ if sys.version_info >= (3, 12):
11
+ from typing import override as override # noqa: F401
12
+ else:
13
+ from typing_extensions import override as override # noqa: F401