spark-sql-migrations 0.0.6__tar.gz → 0.0.7__tar.gz

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 (22) hide show
  1. {spark_sql_migrations-0.0.6 → spark_sql_migrations-0.0.7}/PKG-INFO +1 -1
  2. {spark_sql_migrations-0.0.6 → spark_sql_migrations-0.0.7}/setup.py +1 -1
  3. {spark_sql_migrations-0.0.6 → spark_sql_migrations-0.0.7}/spark_sql_migrations/spark_sql/spark_sql.py +39 -31
  4. {spark_sql_migrations-0.0.6 → spark_sql_migrations-0.0.7}/spark_sql_migrations/spark_utils.py +3 -3
  5. {spark_sql_migrations-0.0.6 → spark_sql_migrations-0.0.7}/spark_sql_migrations.egg-info/PKG-INFO +1 -1
  6. {spark_sql_migrations-0.0.6 → spark_sql_migrations-0.0.7}/LICENSE +0 -0
  7. {spark_sql_migrations-0.0.6 → spark_sql_migrations-0.0.7}/MANIFEST.in +0 -0
  8. {spark_sql_migrations-0.0.6 → spark_sql_migrations-0.0.7}/backend_shim.py +0 -0
  9. {spark_sql_migrations-0.0.6 → spark_sql_migrations-0.0.7}/setup.cfg +0 -0
  10. {spark_sql_migrations-0.0.6 → spark_sql_migrations-0.0.7}/spark_sql_migrations/__init__.py +0 -0
  11. {spark_sql_migrations-0.0.6 → spark_sql_migrations-0.0.7}/spark_sql_migrations/migration_templates/default_migration_template.sql +0 -0
  12. {spark_sql_migrations-0.0.6 → spark_sql_migrations-0.0.7}/spark_sql_migrations/migrations_initial/first_01_intial_create_cat.sql +0 -0
  13. {spark_sql_migrations-0.0.6 → spark_sql_migrations-0.0.7}/spark_sql_migrations/migrations_initial/first_02_intial_create_schema.sql +0 -0
  14. {spark_sql_migrations-0.0.6 → spark_sql_migrations-0.0.7}/spark_sql_migrations/migrations_initial/first_03_intial_create_state_table.sql +0 -0
  15. {spark_sql_migrations-0.0.6 → spark_sql_migrations-0.0.7}/spark_sql_migrations/py.typed +0 -0
  16. {spark_sql_migrations-0.0.6 → spark_sql_migrations-0.0.7}/spark_sql_migrations/spark_sql/__init__.py +0 -0
  17. {spark_sql_migrations-0.0.6 → spark_sql_migrations-0.0.7}/spark_sql_migrations.egg-info/SOURCES.txt +0 -0
  18. {spark_sql_migrations-0.0.6 → spark_sql_migrations-0.0.7}/spark_sql_migrations.egg-info/dependency_links.txt +0 -0
  19. {spark_sql_migrations-0.0.6 → spark_sql_migrations-0.0.7}/spark_sql_migrations.egg-info/entry_points.txt +0 -0
  20. {spark_sql_migrations-0.0.6 → spark_sql_migrations-0.0.7}/spark_sql_migrations.egg-info/namespace_packages.txt +0 -0
  21. {spark_sql_migrations-0.0.6 → spark_sql_migrations-0.0.7}/spark_sql_migrations.egg-info/requires.txt +0 -0
  22. {spark_sql_migrations-0.0.6 → spark_sql_migrations-0.0.7}/spark_sql_migrations.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spark_sql_migrations
3
- Version: 0.0.6
3
+ Version: 0.0.7
4
4
  Summary: Chained, idempotent SQL migrations for Spark and Databricks.
5
5
  Home-page: https://github.com/mcjug2015/spark_sql_migrations
6
6
  Author: Victor Semenov
@@ -299,5 +299,5 @@ Apache License 2.0 — see [LICENSE](LICENSE).
299
299
  },
300
300
  'python_requires': '>=3.10',
301
301
  'url': 'https://github.com/mcjug2015/spark_sql_migrations',
302
- 'version': '0.0.6',
302
+ 'version': '0.0.7',
303
303
  })
@@ -27,8 +27,11 @@ import os
27
27
  import re
28
28
  import uuid
29
29
  from dataclasses import dataclass
30
+ from typing import Any, Dict, List, Optional
30
31
 
31
32
  from jinja2 import Environment, FileSystemLoader, PackageLoader, select_autoescape
33
+ from pyspark.sql.session import SparkSession
34
+ from pyspark.sql.types import Row
32
35
 
33
36
  from spark_sql_migrations.spark_utils import get_spark, is_dbr
34
37
 
@@ -55,13 +58,13 @@ class Migration:
55
58
  template_name: str
56
59
 
57
60
 
58
- def get_default_template_path():
61
+ def get_default_template_path() -> str:
59
62
  """the template this package ships; callers may pass their own instead."""
60
63
  return os.path.join(os.path.dirname(__file__), "..", TEMPLATES_PACKAGE_PATH, DEFAULT_TEMPLATE_NAME)
61
64
 
62
65
 
63
- def _parse_migration(migrations_dir, template_name):
64
- headers = {}
66
+ def _parse_migration(migrations_dir: str, template_name: str) -> Migration:
67
+ headers: Dict[str, Any | str] = {}
65
68
  migration_file_path = os.path.join(migrations_dir, template_name)
66
69
  if os.path.getsize(migration_file_path) == 0:
67
70
  raise ValueError(f"{template_name} is empty;")
@@ -84,8 +87,8 @@ def _parse_migration(migrations_dir, template_name):
84
87
  )
85
88
 
86
89
 
87
- def get_ordered_migration_objs(migration_objs):
88
- by_revision = {}
90
+ def get_ordered_migration_objs(migration_objs: List[Migration]) -> List[Migration]:
91
+ by_revision: Dict[str, Migration] = {}
89
92
  for migration in migration_objs:
90
93
  clash = by_revision.get(migration.revision_id)
91
94
  if clash:
@@ -102,7 +105,7 @@ def get_ordered_migration_objs(migration_objs):
102
105
  f"found {[m.template_name for m in roots]};"
103
106
  )
104
107
 
105
- next_by_revision = {}
108
+ next_by_revision: Dict[str, Migration] = {}
106
109
  for migration in migration_objs:
107
110
  if not migration.prev_revision_id:
108
111
  continue
@@ -119,7 +122,7 @@ def get_ordered_migration_objs(migration_objs):
119
122
  next_by_revision[migration.prev_revision_id] = migration
120
123
 
121
124
  ordered = []
122
- current = roots[0]
125
+ current: Migration | None = roots[0]
123
126
  while current:
124
127
  ordered.append(current)
125
128
  current = next_by_revision.get(current.revision_id)
@@ -134,7 +137,7 @@ def get_ordered_migration_objs(migration_objs):
134
137
  return ordered
135
138
 
136
139
 
137
- def get_migrations_list(migrations_dir):
140
+ def get_migrations_list(migrations_dir: str) -> List[Migration]:
138
141
  """every migration in one directory, walked from its root to its head."""
139
142
  if not os.path.isdir(migrations_dir):
140
143
  raise ValueError(f"no migrations dir at {migrations_dir};")
@@ -150,7 +153,7 @@ def get_migrations_list(migrations_dir):
150
153
  return get_ordered_migration_objs(migrations)
151
154
 
152
155
 
153
- def get_table_version(spark, cat: str, schema: str):
156
+ def get_table_version(spark: SparkSession, cat: str, schema: str) -> Optional[Row]:
154
157
  """the revision VERSION_TABLE records, or None if it holds no row yet."""
155
158
  version_table = f"{cat}.{schema}.{VERSION_TABLE}"
156
159
  if not spark.catalog.tableExists(version_table):
@@ -158,7 +161,7 @@ def get_table_version(spark, cat: str, schema: str):
158
161
  return spark.read.table(version_table).select("version_num").head()
159
162
 
160
163
 
161
- def get_unapplied_migrations_list(spark, migrations_dir, cat: str, schema: str):
164
+ def get_unapplied_migrations_list(spark: SparkSession, migrations_dir: str, cat: str, schema: str) -> List[Migration]:
162
165
  ordered = get_migrations_list(migrations_dir)
163
166
  current = get_table_version(spark, cat, schema)
164
167
  if not current:
@@ -180,7 +183,7 @@ def get_unapplied_migrations_list(spark, migrations_dir, cat: str, schema: str):
180
183
  raise ValueError(f"{VERSION_TABLE} is at {current_revision_id}, which matches no migration in {migrations_dir};")
181
184
 
182
185
 
183
- def gate_on_is_dbr(migration_content):
186
+ def gate_on_is_dbr(migration_content: str) -> str:
184
187
  """wrap a migration's body in the jinja conditional, below its headers.
185
188
 
186
189
  the revision headers stay outside it: they are read off the file as comments,
@@ -199,7 +202,7 @@ def gate_on_is_dbr(migration_content):
199
202
 
200
203
  def create_new_migration(
201
204
  message: str, template_path: str, output_path: str, truncate_slug_length: int = 40, add_is_dbr: bool = False
202
- ):
205
+ ) -> None:
203
206
  rev_id = uuid.uuid4().hex[-12:]
204
207
 
205
208
  date_prefix = datetime.datetime.today().strftime("%y%m%d")
@@ -228,32 +231,32 @@ def create_new_migration(
228
231
  migration_file.write(default_migration_content)
229
232
 
230
233
 
231
- def apply_template(output_dir, template, cat: str, schema: str):
234
+ def apply_template(output_dir: str, template, cat: str, schema: str) -> str:
232
235
  result_sql = template.render(cat=cat, schema=schema, is_dbr=is_dbr())
233
236
  with open(os.path.join(output_dir, template.name.replace(".sql", "_primed.sql")), "w") as file_handle:
234
237
  file_handle.write(result_sql)
235
238
  return result_sql
236
239
 
237
240
 
238
- def get_ascending_letters_within_minute():
241
+ def get_ascending_letters_within_minute() -> str:
239
242
  micros_since_minute = datetime.datetime.now() - datetime.datetime.now().replace(second=0, microsecond=0)
240
243
  result = str(micros_since_minute.microseconds).translate(str.maketrans("0123456789", "ABCDEFGHIJ"))
241
244
  return result
242
245
 
243
246
 
244
- def get_output_folder(output_parent_path):
247
+ def get_output_folder(output_parent_path: str) -> str:
245
248
  folder_name = f"{datetime.datetime.today().strftime('%Y%m%d_%H%M')}_{get_ascending_letters_within_minute()}"
246
249
  return os.path.join(output_parent_path, folder_name)
247
250
 
248
251
 
249
- def has_no_statements(rendered_sql):
252
+ def has_no_statements(rendered_sql: str) -> bool:
250
253
  for line in rendered_sql.splitlines():
251
254
  if not _COMMENT_OR_BLANK_RE.match(line):
252
255
  return False
253
256
  return True
254
257
 
255
258
 
256
- def render_and_apply(spark, output_folder, template, cat: str, schema: str):
259
+ def render_and_apply(spark: SparkSession, output_folder: str, template, cat: str, schema: str) -> None:
257
260
  """render one migration and run it, unless this engine was gated out of it."""
258
261
  result_sql = apply_template(output_folder, template, cat=cat, schema=schema)
259
262
  if has_no_statements(result_sql):
@@ -262,7 +265,7 @@ def render_and_apply(spark, output_folder, template, cat: str, schema: str):
262
265
  spark.sql(result_sql)
263
266
 
264
267
 
265
- def migrate_initial(spark, output_folder, cat: str, schema: str):
268
+ def migrate_initial(spark: SparkSession, output_folder: str, cat: str, schema: str) -> None:
266
269
  """apply the package's own bootstrap chain, in filename order rather than by revision."""
267
270
  env = Environment(
268
271
  loader=PackageLoader(package_name=PACKAGE_NAME, package_path=INITIAL_MIGRATIONS_PACKAGE_PATH),
@@ -276,14 +279,14 @@ def migrate_initial(spark, output_folder, cat: str, schema: str):
276
279
  logger.info(f"rendered {len(all_templates)} initial migrations;")
277
280
 
278
281
 
279
- def record_revision(spark, cat: str, schema: str, revision_id: str):
282
+ def record_revision(spark: SparkSession, cat: str, schema: str, revision_id: str) -> None:
280
283
  version_table = f"{cat}.{schema}.{VERSION_TABLE}"
281
284
  revision_literal = revision_id.replace("'", "''")
282
285
  spark.sql(f"delete from {version_table}")
283
286
  spark.sql(f"insert into {version_table} (version_num) values ('{revision_literal}')")
284
287
 
285
288
 
286
- def migrate_w_rev(spark, output_folder, migrations_dir, cat: str, schema: str):
289
+ def migrate_w_rev(spark: SparkSession, output_folder: str, migrations_dir: str, cat: str, schema: str) -> None:
287
290
  """
288
291
  TODO XXX permit applying up to a revision below the head revision
289
292
  TODO XXX add backwards migrations, permit migrating backwards
@@ -305,7 +308,7 @@ def migrate_w_rev(spark, output_folder, migrations_dir, cat: str, schema: str):
305
308
  logger.info(f"{migration.template_name} done; now at {migration.revision_id};")
306
309
 
307
310
 
308
- def run_migrations(spark, cat, schema, output_folder, migrations_dir):
311
+ def run_migrations(spark: SparkSession, cat: str, schema: str, output_folder: str, migrations_dir: str) -> None:
309
312
  initial_output_folder = os.path.join(output_folder, INITIAL_MIGRATIONS_PACKAGE_PATH)
310
313
  client_output_folder = os.path.join(output_folder, CLIENT_OUTPUT_DIRNAME)
311
314
  os.makedirs(initial_output_folder, exist_ok=True)
@@ -318,39 +321,44 @@ def run_migrations(spark, cat, schema, output_folder, migrations_dir):
318
321
  migrate_w_rev(spark, client_output_folder, migrations_dir, cat, schema)
319
322
 
320
323
 
321
- def main(cat, schema, migrations_dir, output_parent_path=None):
324
+ def main(cat: str, schema: str, migrations_dir: str, output_parent_path: str | None = None) -> None:
322
325
  output_folder = get_output_folder(output_parent_path or os.path.join(os.getcwd(), "migrations_out"))
323
326
  os.makedirs(output_folder)
324
327
  run_migrations(get_spark(), cat, schema, output_folder, migrations_dir)
325
328
 
326
329
 
327
- def _cli_main(cat, schema, migrations_dir): # pragma: no cover
330
+ def _cli_main(cat: str, schema: str, migrations_dir: str) -> None: # pragma: no cover
328
331
  main(cat, schema, migrations_dir)
329
332
 
330
333
 
331
- def _cli_create_new_migration(message, output_path, template_path, add_is_dbr): # pragma: no cover
334
+ def _cli_create_new_migration(
335
+ message: str, output_path: str, template_path: str, add_is_dbr: bool
336
+ ) -> None: # pragma: no cover
332
337
  create_new_migration(message, template_path or get_default_template_path(), output_path, add_is_dbr=add_is_dbr)
333
338
 
334
339
 
335
- def build_parser(): # pragma: no cover
340
+ def build_parser() -> argparse.ArgumentParser: # pragma: no cover
336
341
  parser = argparse.ArgumentParser(description="spark_sql_migrations migration utilities")
337
342
  subparsers = parser.add_subparsers(dest="command", required=True)
338
343
 
339
344
  p_run = subparsers.add_parser("run", help="apply all pending migrations")
340
- p_run.add_argument("--cat", default="spark_catalog")
341
- p_run.add_argument("--schema", default="default")
342
- p_run.add_argument("--migrations-dir", required=True, help="the directory holding the migration chain")
345
+ p_run.add_argument("--cat", type=str, default="spark_catalog")
346
+ p_run.add_argument("--schema", type=str, default="default")
347
+ p_run.add_argument("--migrations-dir", type=str, required=True, help="the directory holding the migration chain")
343
348
  p_run.set_defaults(func=_cli_main)
344
349
 
345
350
  p_create = subparsers.add_parser("create_new_migration", help="create a new migration from the template")
346
- p_create.add_argument("--message", required=True)
351
+ p_create.add_argument("--message", type=str, required=True)
347
352
  p_create.add_argument(
348
353
  "--output-path",
354
+ type=str,
349
355
  required=True,
350
356
  help="the chain directory to write the new migration into. if sql files are in there, the last sql's rev_id"
351
357
  "will be set as the new migrations prev_rev_id.",
352
358
  )
353
- p_create.add_argument("--template-path", default=None, help="defaults to the template shipped in this package")
359
+ p_create.add_argument(
360
+ "--template-path", type=str, default=None, help="defaults to the template shipped in this package"
361
+ )
354
362
  # the %% are argparse's: it interpolates help strings
355
363
  p_create.add_argument(
356
364
  "--add-is-dbr",
@@ -362,7 +370,7 @@ def build_parser(): # pragma: no cover
362
370
  return parser
363
371
 
364
372
 
365
- def cli(argv=None): # pragma: no cover
373
+ def cli(argv: list[str] | None = None) -> None: # pragma: no cover
366
374
  """the `spark-sql-migrations` console command.
367
375
 
368
376
  an application entry point, so unlike the rest of this package it configures
@@ -11,7 +11,7 @@ METASTORE_DIR_ENV_VAR = "SPARK_METASTORE_DIR"
11
11
  DEFAULT_WAREHOUSE_DIRNAME = "spark-warehouse"
12
12
 
13
13
 
14
- def is_dbr():
14
+ def is_dbr() -> bool:
15
15
  try:
16
16
  from pyspark.dbutils import DBUtils # type: ignore # noqa: F401
17
17
 
@@ -21,7 +21,7 @@ def is_dbr():
21
21
  return False
22
22
 
23
23
 
24
- def get_warehouse_dir():
24
+ def get_warehouse_dir() -> str:
25
25
  """where local (non-DBR) spark keeps its warehouse.
26
26
 
27
27
  resolved against the caller's cwd, never against __file__: installed from a
@@ -34,7 +34,7 @@ def get_warehouse_dir():
34
34
  )
35
35
 
36
36
 
37
- def get_spark(use_dbc=False):
37
+ def get_spark(use_dbc: bool = False) -> SparkSession:
38
38
  if use_dbc or is_dbr():
39
39
  from databricks.connect.session import DatabricksSession # type: ignore # pants: no-infer-dep
40
40
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spark_sql_migrations
3
- Version: 0.0.6
3
+ Version: 0.0.7
4
4
  Summary: Chained, idempotent SQL migrations for Spark and Databricks.
5
5
  Home-page: https://github.com/mcjug2015/spark_sql_migrations
6
6
  Author: Victor Semenov