al-data-utils 0.3.1__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 (60) hide show
  1. al_data_utils/__init__.py +10 -0
  2. al_data_utils/airflow_utils/__init__.py +13 -0
  3. al_data_utils/airflow_utils/airflow_utils.py +83 -0
  4. al_data_utils/azure_as_utils/__init__.py +31 -0
  5. al_data_utils/azure_as_utils/auth.py +82 -0
  6. al_data_utils/azure_as_utils/azure_as_utils.py +223 -0
  7. al_data_utils/azure_as_utils/client.py +158 -0
  8. al_data_utils/azure_as_utils/errors.py +78 -0
  9. al_data_utils/azure_as_utils/models.py +265 -0
  10. al_data_utils/azure_as_utils/poller.py +121 -0
  11. al_data_utils/backup_utils/__init__.py +9 -0
  12. al_data_utils/backup_utils/mssql/dacpac/__init__.py +1 -0
  13. al_data_utils/backup_utils/mssql/dacpac/sqlpackage.py +207 -0
  14. al_data_utils/backup_utils/mssql/schema_snapshot/__init__.py +8 -0
  15. al_data_utils/backup_utils/mssql/schema_snapshot/config.py +150 -0
  16. al_data_utils/backup_utils/mssql/schema_snapshot/emit/__init__.py +11 -0
  17. al_data_utils/backup_utils/mssql/schema_snapshot/emit/foreign_keys.py +38 -0
  18. al_data_utils/backup_utils/mssql/schema_snapshot/emit/indexes.py +37 -0
  19. al_data_utils/backup_utils/mssql/schema_snapshot/emit/sql_objects.py +17 -0
  20. al_data_utils/backup_utils/mssql/schema_snapshot/emit/tables.py +139 -0
  21. al_data_utils/backup_utils/mssql/schema_snapshot/introspect/__init__.py +19 -0
  22. al_data_utils/backup_utils/mssql/schema_snapshot/introspect/foreign_keys.py +173 -0
  23. al_data_utils/backup_utils/mssql/schema_snapshot/introspect/functions.py +133 -0
  24. al_data_utils/backup_utils/mssql/schema_snapshot/introspect/indexes.py +190 -0
  25. al_data_utils/backup_utils/mssql/schema_snapshot/introspect/sequences.py +180 -0
  26. al_data_utils/backup_utils/mssql/schema_snapshot/introspect/stored_procedures.py +313 -0
  27. al_data_utils/backup_utils/mssql/schema_snapshot/introspect/synonyms.py +135 -0
  28. al_data_utils/backup_utils/mssql/schema_snapshot/introspect/tables.py +251 -0
  29. al_data_utils/backup_utils/mssql/schema_snapshot/introspect/views.py +244 -0
  30. al_data_utils/backup_utils/mssql/schema_snapshot/io/__init__.py +10 -0
  31. al_data_utils/backup_utils/mssql/schema_snapshot/io/layout.py +28 -0
  32. al_data_utils/backup_utils/mssql/schema_snapshot/io/writer.py +40 -0
  33. al_data_utils/backup_utils/mssql/schema_snapshot/models/__init__.py +14 -0
  34. al_data_utils/backup_utils/mssql/schema_snapshot/models/foreign_key_def.py +19 -0
  35. al_data_utils/backup_utils/mssql/schema_snapshot/models/index_def.py +19 -0
  36. al_data_utils/backup_utils/mssql/schema_snapshot/models/sql_object_def.py +11 -0
  37. al_data_utils/backup_utils/mssql/schema_snapshot/models/table_column_def.py +27 -0
  38. al_data_utils/backup_utils/mssql/schema_snapshot/models/table_def.py +22 -0
  39. al_data_utils/backup_utils/mssql/schema_snapshot/snapshot_processor.py +754 -0
  40. al_data_utils/backup_utils/mssql/scripter/__init__.py +1 -0
  41. al_data_utils/config_utils/__init__.py +6 -0
  42. al_data_utils/config_utils/config_utils.py +31 -0
  43. al_data_utils/files_sync_utils/__init__.py +15 -0
  44. al_data_utils/files_sync_utils/files_sync_utils.py +567 -0
  45. al_data_utils/geo_utils/__init__.py +0 -0
  46. al_data_utils/git_utils/__init__.py +27 -0
  47. al_data_utils/git_utils/git_utils.py +142 -0
  48. al_data_utils/logging_utils/__init__.py +3 -0
  49. al_data_utils/logging_utils/logging_utils.py +39 -0
  50. al_data_utils/ones_api_utils/__init__.py +3 -0
  51. al_data_utils/ones_api_utils/ones_api_utils.py +61 -0
  52. al_data_utils/sql_utils/__init__.py +3 -0
  53. al_data_utils/sql_utils/sql_utils.py +158 -0
  54. al_data_utils/tg_utils/__init__.py +3 -0
  55. al_data_utils/tg_utils/tg_utils.py +45 -0
  56. al_data_utils-0.3.1.dist-info/METADATA +157 -0
  57. al_data_utils-0.3.1.dist-info/RECORD +60 -0
  58. al_data_utils-0.3.1.dist-info/WHEEL +5 -0
  59. al_data_utils-0.3.1.dist-info/licenses/LICENSE +21 -0
  60. al_data_utils-0.3.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,244 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+ import re
5
+
6
+ from al_data_utils.sql_utils.sql_utils import UnifiedSQLClient
7
+ from ..models import SqlObjectDef
8
+
9
+
10
+ _SQL_VIEWS = r"""
11
+ select
12
+ s.name as schema_name,
13
+ v.name as object_name,
14
+ m.definition as definition
15
+ from sys.views v
16
+ inner join sys.schemas s
17
+ on s.schema_id = v.schema_id
18
+ inner join sys.sql_modules m
19
+ on m.object_id = v.object_id
20
+ where v.is_ms_shipped = 0
21
+ order by s.name, v.name;
22
+ """
23
+
24
+
25
+ _SQL_VIEW_PERMISSIONS = r"""
26
+ select
27
+ s.name as schema_name,
28
+ v.name as object_name,
29
+ dp.permission_name,
30
+ dp.state,
31
+ grantee.name as grantee_name,
32
+ c.name as column_name,
33
+ cast(case when dp.minor_id > 0 then 1 else 0 end as bit) as is_column_level
34
+ from sys.database_permissions dp
35
+ inner join sys.views v
36
+ on v.object_id = dp.major_id
37
+ inner join sys.schemas s
38
+ on s.schema_id = v.schema_id
39
+ inner join sys.database_principals grantee
40
+ on grantee.principal_id = dp.grantee_principal_id
41
+ left outer join sys.columns c
42
+ on c.object_id = dp.major_id
43
+ and c.column_id = dp.minor_id
44
+ where dp.class = 1
45
+ and v.is_ms_shipped = 0
46
+ and dp.state in ('G', 'W', 'D')
47
+ order by s.name, v.name, dp.permission_name, dp.state, grantee.name, c.column_id;
48
+ """
49
+
50
+
51
+ def _normalized_set(values: set[str] | None) -> set[str]:
52
+ if not values:
53
+ return set()
54
+ return {str(v).strip().lower() for v in values if str(v).strip()}
55
+
56
+
57
+ def _split_qualified(values: set[str]) -> list[tuple[str, str]]:
58
+ pairs: list[tuple[str, str]] = []
59
+ for v in sorted(values):
60
+ if '.' not in v:
61
+ continue
62
+ schema, name = v.split('.', 1)
63
+ schema = schema.strip().lower()
64
+ name = name.strip().lower()
65
+ if schema and name:
66
+ pairs.append((schema, name))
67
+ return pairs
68
+
69
+
70
+ def _build_view_filter_sql_and_params(
71
+ *,
72
+ include_schemas: set[str] | None = None,
73
+ exclude_schemas: set[str] | None = None,
74
+ include_views: set[str] | None = None,
75
+ exclude_views: set[str] | None = None,
76
+ ) -> tuple[str, dict[str, str]]:
77
+ inc_schemas = sorted(_normalized_set(include_schemas))
78
+ exc_schemas = sorted(_normalized_set(exclude_schemas))
79
+ inc_views = _split_qualified(_normalized_set(include_views))
80
+ exc_views = _split_qualified(_normalized_set(exclude_views))
81
+
82
+ params: dict[str, str] = {}
83
+ clauses: list[str] = []
84
+
85
+ if inc_schemas:
86
+ names: list[str] = []
87
+ for i, schema in enumerate(inc_schemas):
88
+ key = f'inc_schema_{i}'
89
+ params[key] = schema
90
+ names.append(f':{key}')
91
+ clauses.append(f'lower(s.name) in ({", ".join(names)})')
92
+
93
+ if exc_schemas:
94
+ names = []
95
+ for i, schema in enumerate(exc_schemas):
96
+ key = f'exc_schema_{i}'
97
+ params[key] = schema
98
+ names.append(f':{key}')
99
+ clauses.append(f'lower(s.name) not in ({", ".join(names)})')
100
+
101
+ if inc_views:
102
+ parts: list[str] = []
103
+ for i, (schema, name) in enumerate(inc_views):
104
+ sk = f'inc_view_schema_{i}'
105
+ nk = f'inc_view_name_{i}'
106
+ params[sk] = schema
107
+ params[nk] = name
108
+ parts.append(f'(lower(s.name) = :{sk} and lower(v.name) = :{nk})')
109
+ clauses.append(f'({" or ".join(parts)})')
110
+
111
+ if exc_views:
112
+ parts = []
113
+ for i, (schema, name) in enumerate(exc_views):
114
+ sk = f'exc_view_schema_{i}'
115
+ nk = f'exc_view_name_{i}'
116
+ params[sk] = schema
117
+ params[nk] = name
118
+ parts.append(f'(lower(s.name) = :{sk} and lower(v.name) = :{nk})')
119
+ clauses.append(f'not ({" or ".join(parts)})')
120
+
121
+ if not clauses:
122
+ return '', {}
123
+
124
+ return '\n and ' + '\n and '.join(clauses), params
125
+
126
+
127
+ def _inject_view_filters(sql: str, filters_sql: str) -> str:
128
+ if not filters_sql:
129
+ return sql
130
+ return sql.replace(
131
+ 'where v.is_ms_shipped = 0', f'where v.is_ms_shipped = 0{filters_sql}'
132
+ )
133
+
134
+
135
+ def _q(name: str) -> str:
136
+ n = name.strip()
137
+ if re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', n):
138
+ return n
139
+ return '[' + n.replace(']', ']]') + ']'
140
+
141
+
142
+ def _build_permission_statements(
143
+ sql_client: UnifiedSQLClient,
144
+ *,
145
+ filters_sql: str = '',
146
+ params: dict[str, str] | None = None,
147
+ ) -> dict[tuple[str, str], list[str]]:
148
+ rows = sql_client.execute_command_with_result_to_dicts(
149
+ _inject_view_filters(_SQL_VIEW_PERMISSIONS, filters_sql), params or {}
150
+ )
151
+
152
+ object_perms: dict[tuple[str, str], list[str]] = defaultdict(list)
153
+ column_perms: dict[tuple[str, str], list[str]] = defaultdict(list)
154
+
155
+ # key: (schema, object, state, perm, grantee)
156
+ col_group: dict[tuple[str, str, str, str, str], list[str]] = defaultdict(list)
157
+
158
+ for r in rows:
159
+ schema_name = str(r['schema_name'])
160
+ object_name = str(r['object_name'])
161
+ permission_name = str(r['permission_name']).lower()
162
+ state = str(r['state']).upper()
163
+ grantee = str(r['grantee_name'])
164
+ is_column_level = bool(r['is_column_level'])
165
+ column_name = (
166
+ None if r.get('column_name') is None else str(r.get('column_name'))
167
+ )
168
+
169
+ target = f'{_q(schema_name)}.{_q(object_name)}'
170
+ grantee_sql = _q(grantee)
171
+
172
+ verb = 'grant' if state in {'G', 'W'} else 'deny'
173
+ with_grant = ' with grant option' if state == 'W' else ''
174
+
175
+ if is_column_level and column_name:
176
+ group_key = (schema_name, object_name, state, permission_name, grantee)
177
+ col_group[group_key].append(column_name)
178
+ else:
179
+ object_perms[(schema_name, object_name)].append(
180
+ f'{verb} {permission_name} on {target} to {grantee_sql}{with_grant}'
181
+ )
182
+
183
+ for (schema_name, object_name, state, permission_name, grantee), cols in sorted(
184
+ col_group.items()
185
+ ):
186
+ target = f'{_q(schema_name)}.{_q(object_name)}'
187
+ grantee_sql = _q(grantee)
188
+ verb = 'grant' if state in {'G', 'W'} else 'deny'
189
+ with_grant = ' with grant option' if state == 'W' else ''
190
+ seen_cols: set[str] = set()
191
+ ordered_cols: list[str] = []
192
+ for c in cols:
193
+ lc = c.lower()
194
+ if lc in seen_cols:
195
+ continue
196
+ seen_cols.add(lc)
197
+ ordered_cols.append(c)
198
+
199
+ col_list = ', '.join(_q(c) for c in ordered_cols)
200
+ column_perms[(schema_name, object_name)].append(
201
+ f'{verb} {permission_name} ({col_list}) on {target} to {grantee_sql}{with_grant}'
202
+ )
203
+
204
+ merged: dict[tuple[str, str], list[str]] = defaultdict(list)
205
+ keys = set(object_perms.keys()) | set(column_perms.keys())
206
+ for k in keys:
207
+ merged[k].extend(object_perms.get(k, []))
208
+ merged[k].extend(column_perms.get(k, []))
209
+
210
+ return merged
211
+
212
+
213
+ def get_views(
214
+ sql_client: UnifiedSQLClient,
215
+ *,
216
+ include_schemas: set[str] | None = None,
217
+ exclude_schemas: set[str] | None = None,
218
+ include_views: set[str] | None = None,
219
+ exclude_views: set[str] | None = None,
220
+ ) -> list[SqlObjectDef]:
221
+ filters_sql, params = _build_view_filter_sql_and_params(
222
+ include_schemas=include_schemas,
223
+ exclude_schemas=exclude_schemas,
224
+ include_views=include_views,
225
+ exclude_views=exclude_views,
226
+ )
227
+
228
+ rows = sql_client.execute_command_with_result_to_dicts(
229
+ _inject_view_filters(_SQL_VIEWS, filters_sql), params
230
+ )
231
+ perms_by_view = _build_permission_statements(
232
+ sql_client, filters_sql=filters_sql, params=params
233
+ )
234
+ return [
235
+ SqlObjectDef(
236
+ schema_name=str(r['schema_name']),
237
+ object_name=str(r['object_name']),
238
+ definition=str(r['definition'] or ''),
239
+ permissions_sql=perms_by_view.get(
240
+ (str(r['schema_name']), str(r['object_name'])), []
241
+ ),
242
+ )
243
+ for r in rows
244
+ ]
@@ -0,0 +1,10 @@
1
+ from .layout import table_file_path, object_file_path
2
+ from .writer import write_text_atomic, set_mtime_like, read_text_if_exists
3
+
4
+ __all__ = [
5
+ 'table_file_path',
6
+ 'object_file_path',
7
+ 'write_text_atomic',
8
+ 'set_mtime_like',
9
+ 'read_text_if_exists',
10
+ ]
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ def object_file_path(
7
+ *,
8
+ root: Path,
9
+ root_folder_name: str,
10
+ schema: str,
11
+ object_folder: str,
12
+ object_name: str,
13
+ ) -> Path:
14
+ return (
15
+ root / root_folder_name / schema / object_folder / f'{schema}.{object_name}.sql'
16
+ )
17
+
18
+
19
+ def table_file_path(
20
+ *, root: Path, root_folder_name: str, schema: str, table: str
21
+ ) -> Path:
22
+ return object_file_path(
23
+ root=root,
24
+ root_folder_name=root_folder_name,
25
+ schema=schema,
26
+ object_folder='tables',
27
+ object_name=table,
28
+ )
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+
7
+ def _normalize_text(text: str, newline: str) -> str:
8
+ t = text.replace("\r\n", "\n").replace("\r", "\n")
9
+ if newline != "\n":
10
+ t = t.replace("\n", newline)
11
+ if not t.endswith(newline):
12
+ t += newline
13
+ return t
14
+
15
+
16
+ def write_text_atomic(path: Path, text: str, *, encoding: str, newline: str) -> None:
17
+ path.parent.mkdir(parents=True, exist_ok=True)
18
+ tmp = path.with_suffix(path.suffix + ".tmp")
19
+ data = _normalize_text(text, newline)
20
+
21
+ with open(tmp, "w", encoding=encoding, newline="") as f:
22
+ f.write(data)
23
+
24
+ os.replace(tmp, path)
25
+
26
+
27
+ def set_mtime_like(path: Path, like_path: Path) -> None:
28
+ try:
29
+ st = like_path.stat()
30
+ m = int(st.st_mtime)
31
+ os.utime(path, (m, m))
32
+ except FileNotFoundError:
33
+ return
34
+
35
+
36
+ def read_text_if_exists(path: Path, *, encoding: str) -> str | None:
37
+ try:
38
+ return path.read_text(encoding=encoding)
39
+ except FileNotFoundError:
40
+ return None
@@ -0,0 +1,14 @@
1
+ from .index_def import IndexDef
2
+ from .foreign_key_def import ForeignKeyDef
3
+ from .table_column_def import TableColumnDef
4
+ from .table_def import TableDef, PrimaryKeyDef
5
+ from .sql_object_def import SqlObjectDef
6
+
7
+ __all__ = [
8
+ 'IndexDef',
9
+ 'ForeignKeyDef',
10
+ 'TableColumnDef',
11
+ 'TableDef',
12
+ 'PrimaryKeyDef',
13
+ 'SqlObjectDef',
14
+ ]
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class ForeignKeyDef:
8
+ parent_schema: str
9
+ parent_table: str
10
+ fk_name: str
11
+ referenced_schema: str
12
+ referenced_table: str
13
+ on_delete: str
14
+ on_update: str
15
+ is_not_for_replication: bool
16
+ is_disabled: bool
17
+ is_not_trusted: bool
18
+ parent_columns: str # formatted: [col1], [col2]
19
+ referenced_columns: str # formatted: [col1], [col2]
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class IndexDef:
9
+ schema_name: str
10
+ table_name: str
11
+ index_name: str
12
+ is_unique: bool
13
+ type_desc: str # CLUSTERED / NONCLUSTERED
14
+ has_filter: bool
15
+ filter_definition: Optional[str]
16
+ is_disabled: bool
17
+ is_hypothetical: bool
18
+ key_columns: str # formatted: [col] asc, [col2] desc
19
+ include_columns: str # formatted: [col], [col2]
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class SqlObjectDef:
8
+ schema_name: str
9
+ object_name: str
10
+ definition: str
11
+ permissions_sql: list[str] = field(default_factory=list)
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class TableColumnDef:
8
+ schema_name: str
9
+ table_name: str
10
+ column_name: str
11
+ ordinal_position: int
12
+
13
+ data_type: str
14
+ max_length: int | None
15
+ precision: int | None
16
+ scale: int | None
17
+
18
+ is_nullable: bool
19
+ is_identity: bool
20
+ identity_seed: int | None
21
+ identity_increment: int | None
22
+
23
+ is_computed: bool
24
+ computed_definition: str | None
25
+ collation_name: str | None
26
+ default_constraint_name: str | None
27
+ default_definition: str | None
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from .table_column_def import TableColumnDef
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class PrimaryKeyDef:
10
+ schema_name: str
11
+ table_name: str
12
+ constraint_name: str
13
+ key_columns: str
14
+ type_desc: str
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class TableDef:
19
+ schema_name: str
20
+ table_name: str
21
+ columns: list[TableColumnDef]
22
+ primary_key: PrimaryKeyDef | None = None