From 1fdb82cc726c6fedf5d061bf55c49947c9574a19 Mon Sep 17 00:00:00 2001 From: ravi-databricks Date: Wed, 25 Sep 2024 12:09:34 -0700 Subject: [PATCH 01/59] Added uc volume for fanout demo --- .gitignore | 6 ++ demo/conf/onboarding_cars.template | 4 +- demo/conf/onboarding_fanout_cars.template | 6 +- demo/launch_silver_fanout_demo.py | 21 +++-- integration_tests/run_integration_tests.py | 98 ++++++++++++++++++---- src/cli.py | 33 +++++--- 6 files changed, 127 insertions(+), 41 deletions(-) diff --git a/.gitignore b/.gitignore index e842f6b..f110e55 100644 --- a/.gitignore +++ b/.gitignore @@ -155,3 +155,9 @@ deployment-merged.yaml .databricks-login.json demo/conf/onboarding.json integration_tests/conf/onboarding.json +<<<<<<< Updated upstream +databricks.yaml +======= + +.databricks +>>>>>>> Stashed changes diff --git a/demo/conf/onboarding_cars.template b/demo/conf/onboarding_cars.template index b21c514..eacdd26 100644 --- a/demo/conf/onboarding_cars.template +++ b/demo/conf/onboarding_cars.template @@ -5,7 +5,7 @@ "source_system": "mysql", "source_format": "cloudFiles", "source_details": { - "source_path_demo": "{dbfs_path}/demo/resources/data/cars" + "source_path_demo": "{uc_volume_path}/demo/resources/data/cars" }, "bronze_database_demo": "{uc_catalog_name}.{bronze_schema}", "bronze_table": "cars", @@ -16,6 +16,6 @@ }, "silver_database_demo": "{uc_catalog_name}.{silver_schema}", "silver_table": "cars_usa", - "silver_transformation_json_demo": "{dbfs_path}/demo/conf/silver_transformations_cars.json" + "silver_transformation_json_demo": "{uc_volume_path}/demo/conf/silver_transformations_cars.json" } ] \ No newline at end of file diff --git a/demo/conf/onboarding_fanout_cars.template b/demo/conf/onboarding_fanout_cars.template index 53cc45a..3506427 100644 --- a/demo/conf/onboarding_fanout_cars.template +++ b/demo/conf/onboarding_fanout_cars.template @@ -6,7 +6,7 @@ "bronze_table": "cars", "silver_database_demo": "{uc_catalog_name}.{silver_schema}", "silver_table": "cars_germany", - "silver_transformation_json_demo": "{dbfs_path}/demo/conf/silver_transformations_cars.json" + "silver_transformation_json_demo": "{uc_volume_path}/demo/conf/silver_transformations_cars.json" }, { "data_flow_id": "102", @@ -15,7 +15,7 @@ "bronze_table": "cars", "silver_database_demo": "{uc_catalog_name}.{silver_schema}", "silver_table": "cars_uk", - "silver_transformation_json_demo": "{dbfs_path}/demo/conf/silver_transformations_cars.json" + "silver_transformation_json_demo": "{uc_volume_path}/demo/conf/silver_transformations_cars.json" }, { "data_flow_id": "103", @@ -24,6 +24,6 @@ "bronze_table": "cars", "silver_database_demo": "{uc_catalog_name}.{silver_schema}", "silver_table": "cars_japan", - "silver_transformation_json_demo": "{dbfs_path}/demo/conf/silver_transformations_cars.json" + "silver_transformation_json_demo": "{uc_volume_path}/demo/conf/silver_transformations_cars.json" } ] \ No newline at end of file diff --git a/demo/launch_silver_fanout_demo.py b/demo/launch_silver_fanout_demo.py index 0c538a2..aa7e376 100644 --- a/demo/launch_silver_fanout_demo.py +++ b/demo/launch_silver_fanout_demo.py @@ -66,7 +66,8 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: runner_conf = DLTMetaRunnerConf( run_id=run_id, username=self.wsi._my_username, - dbfs_tmp_path=f"{self.args.__dict__['dbfs_path']}/{run_id}", + # dbfs_tmp_path=f"{self.args.__dict__['dbfs_path']}/{run_id}", + uc_volume_path=f"{self.args.__dict__['uc_volume_path']}/{run_id}", int_tests_dir="file:./demo", dlt_meta_schema=f"dlt_meta_dataflowspecs_demo_{run_id}", bronze_schema=f"dlt_meta_bronze_demo_{run_id}", @@ -121,7 +122,7 @@ def create_sfo_workflow_spec(self, runner_conf: DLTMetaRunnerConf): "onboard_layer": "bronze_silver", "database": database, "onboarding_file_path": - f"{runner_conf.dbfs_tmp_path}/{runner_conf.onboarding_file_path}", + f"{runner_conf.uc_volume_path}/{runner_conf.onboarding_file_path}", "silver_dataflowspec_table": "silver_dataflowspec_cdc", "bronze_dataflowspec_table": "bronze_dataflowspec_cdc", "import_author": "Ravi", @@ -146,7 +147,7 @@ def create_sfo_workflow_spec(self, runner_conf: DLTMetaRunnerConf): "onboard_layer": "silver", "database": database, "onboarding_file_path": - f"{runner_conf.dbfs_tmp_path}/{runner_conf.onboarding_fanout_file_path}", + f"{runner_conf.uc_volume_path}/{runner_conf.onboarding_fanout_file_path}", "silver_dataflowspec_table": "silver_dataflowspec_cdc", "import_author": "Ravi", "version": "v1", @@ -180,11 +181,21 @@ def create_sfo_workflow_spec(self, runner_conf: DLTMetaRunnerConf): "--uc_catalog_name": "provide databricks uc_catalog name, this is required to create volume, schema, table", "--cloud_provider_name": "provide cloud provider name. Supported values are aws , azure , gcp", "--dbr_version": "Provide databricks runtime spark version e.g 15.3.x-scala2.12", +<<<<<<< Updated upstream + "--uc_volume_path": "provide databricks uc_volume name, where you want to push integration test \ + data and configurations" +} + +sfo_mandatory_args = ["uc_catalog_name", "cloud_provider_name", "dbr_version", "uc_volume_path"] +======= "--dbfs_path": "Provide databricks workspace dbfs path where you want run integration tests \ - e.g --dbfs_path=dbfs:/tmp/DLT-META/" + e.g --dbfs_path=dbfs:/tmp/DLT-META/", + "uc_volume_path": "provide databricks uc_volume name, where you want to push integration test \ + data and configurations" } -sfo_mandatory_args = ["uc_catalog_name", "cloud_provider_name", "dbr_version", "dbfs_path"] +sfo_mandatory_args = ["uc_catalog_name", "cloud_provider_name", "dbr_version"] +>>>>>>> Stashed changes def main(): diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index aef3d68..4a23819 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -9,7 +9,7 @@ from databricks.sdk.service.pipelines import PipelineLibrary, NotebookLibrary from databricks.sdk.service import jobs, pipelines, compute from databricks.sdk.service.workspace import ImportFormat -from databricks.sdk.service.catalog import SchemasAPI, VolumeInfo +from databricks.sdk.service.catalog import SchemasAPI, VolumeInfo, VolumeType from src.install import WorkspaceInstaller import json @@ -751,6 +751,8 @@ def create_cloudfiles_onboarding(self, runner_conf: DLTMetaRunnerConf): self.__populate_source_details(runner_conf, val, k, v) if 'dbfs_path' in value: data_flow[key] = value.format(dbfs_path=runner_conf.dbfs_tmp_path) + if 'uc_volume_path' in value: + data_flow[key] = value.format(uc_volume_path=runner_conf.uc_volume_path) if key == 'silver_append_flows': counter = 0 for flows in value: @@ -799,6 +801,8 @@ def create_cloudfiles_onboarding(self, runner_conf: DLTMetaRunnerConf): self.__populate_source_details(runner_conf, data_flow, key, value) if 'dbfs_path' in value: data_flow[key] = value.format(dbfs_path=runner_conf.dbfs_tmp_path) + if 'uc_volume_path' in value: + data_flow[key] = value.format(uc_volume_path=runner_conf.uc_volume_path) if 'uc_catalog_name' in value and 'bronze_schema' in value: if runner_conf.uc_catalog_name: data_flow[key] = value.format( @@ -818,6 +822,8 @@ def create_cloudfiles_onboarding(self, runner_conf: DLTMetaRunnerConf): self.__populate_source_details(runner_conf, data_flow, key, value) if 'dbfs_path' in value: data_flow[key] = value.format(dbfs_path=runner_conf.dbfs_tmp_path) + if 'uc_volume_path' in value: + data_flow[key] = value.format(uc_volume_path=runner_conf.uc_volume_path) if 'uc_catalog_name' in value and 'bronze_schema' in value: if runner_conf.uc_catalog_name: data_flow[key] = value.format( @@ -839,29 +845,85 @@ def __populate_source_details(self, runner_conf, data_flow, key, value): for source_key, source_value in value.items(): if 'dbfs_path' in source_value: data_flow[key][source_key] = source_value.format(dbfs_path=runner_conf.dbfs_tmp_path) + elif 'uc_volume_path' in source_value: + data_flow[key][source_key] = source_value.format(uc_volume_path=runner_conf.uc_volume_path) + + def copy(self, runner_conf: DLTMetaRunnerConf): + if runner_conf.uc_catalog_name: +<<<<<<< Updated upstream + # runner_conf.volume_info = self.ws.api_client.volumes.create(catalog_name=runner_conf.uc_catalog_name, + # schema_name=runner_conf.dlt_meta_schema, + # name=runner_conf.uc_volume_name, + # volume_type=VolumeType.MANAGED) + print(f"uploading to {runner_conf.uc_volume_path}/{self.base_dir}/ started") + src = runner_conf.int_tests_dir + dst = runner_conf.uc_volume_path +======= + runner_conf.volume_info = self.ws.api_client.volumes.create(catalog_name=runner_conf.uc_catalog_name, + schema_name=runner_conf.dlt_meta_schema, + name=runner_conf.uc_volume_name, + volume_type=VolumeType.MANAGED) + print(f"uploading to {runner_conf.dbfs_tmp_path}/{self.base_dir}/ started") + src = runner_conf.int_tests_dir + dst = runner_conf.dbfs_tmp_path +>>>>>>> Stashed changes + main_dir = src.replace('file:', '') + base_dir_name = None + if main_dir.endswith('/'): + base_dir_name = main_dir[:-1] + if base_dir_name is None: + base_dir_name = main_dir[main_dir.rfind('/') + 1:] + else: + base_dir_name = base_dir_name[base_dir_name.rfind('/') + 1:-1] + for root, dirs, files in os.walk(main_dir): + for filename in files: + target_dir = root[root.index(main_dir) + len(main_dir):len(root)] +<<<<<<< Updated upstream + uc_volume_path = f"{dst}/{base_dir_name}/{target_dir}/{filename}".replace("//", "/") + contents = open(os.path.join(root, filename), "rb") + # print(f"local_path={os.path.join(root, filename)}", + # f"dbfs_path={dst}/{base_dir_name}/{target_dir}/{filename}") + self.ws.files.upload(file_path=uc_volume_path, contents=contents, overwrite=True) +======= + dbfs_path = f"{dst}/{base_dir_name}/{target_dir}/{filename}" + contents = open(os.path.join(root, filename), "rb") + # print(f"local_path={os.path.join(root, filename)}", + # f"dbfs_path={dst}/{base_dir_name}/{target_dir}/{filename}") + self.ws.api_client.files.upload(file_path=dbfs_path, contents=contents, overwrite=True) +>>>>>>> Stashed changes - def copy(self, src, dst): - main_dir = src.replace('file:', '') - base_dir_name = None - if main_dir.endswith('/'): - base_dir_name = main_dir[:-1] - if base_dir_name is None: - base_dir_name = main_dir[main_dir.rfind('/') + 1:] else: - base_dir_name = base_dir_name[base_dir_name.rfind('/') + 1:-1] - for root, dirs, files in os.walk(main_dir): - for filename in files: - target_dir = root[root.index(main_dir) + len(main_dir):len(root)] - dbfs_path = f"{dst}/{base_dir_name}/{target_dir}/{filename}" - contents = open(os.path.join(root, filename), "rb") - # print(f"local_path={os.path.join(root, filename)}", - # f"dbfs_path={dst}/{base_dir_name}/{target_dir}/{filename}") - self.ws.dbfs.upload(dbfs_path, contents, overwrite=True) + src = runner_conf.int_tests_dir + dst = runner_conf.dbfs_tmp_path + main_dir = src.replace('file:', '') + base_dir_name = None + if main_dir.endswith('/'): + base_dir_name = main_dir[:-1] + if base_dir_name is None: + base_dir_name = main_dir[main_dir.rfind('/') + 1:] + else: + base_dir_name = base_dir_name[base_dir_name.rfind('/') + 1:-1] + for root, dirs, files in os.walk(main_dir): + for filename in files: + target_dir = root[root.index(main_dir) + len(main_dir):len(root)] +<<<<<<< Updated upstream + uc_volume_path = f"{dst}/{base_dir_name}/{target_dir}/{filename}" + contents = open(os.path.join(root, filename), "rb") + # print(f"local_path={os.path.join(root, filename)}", + # f"dbfs_path={dst}/{base_dir_name}/{target_dir}/{filename}") + self.ws.dbfs.upload(uc_volume_path, contents, overwrite=True) +======= + dbfs_path = f"{dst}/{base_dir_name}/{target_dir}/{filename}" + contents = open(os.path.join(root, filename), "rb") + # print(f"local_path={os.path.join(root, filename)}", + # f"dbfs_path={dst}/{base_dir_name}/{target_dir}/{filename}") + self.ws.dbfs.upload(dbfs_path, contents, overwrite=True) +>>>>>>> Stashed changes def init_dltmeta_runner_conf(self, runner_conf: DLTMetaRunnerConf): self.generate_onboarding_file(runner_conf) print("int_tests_dir: ", runner_conf.int_tests_dir) - self.copy(runner_conf.int_tests_dir, runner_conf.dbfs_tmp_path) + self.copy(runner_conf) print(f"uploading to {runner_conf.dbfs_tmp_path}/{self.base_dir}/ complete!!!") fp = open(runner_conf.runners_full_local_path, "rb") print(f"uploading to {runner_conf.runners_nb_path} started") diff --git a/src/cli.py b/src/cli.py index 9ea977a..401ba37 100644 --- a/src/cli.py +++ b/src/cli.py @@ -45,6 +45,7 @@ class OnboardCommand: version: str cloud: str dlt_meta_schema: str + serverless: bool = True bronze_schema: str = None silver_schema: str = None uc_enabled: bool = False @@ -207,27 +208,30 @@ def onboard(self, cmd: OnboardCommand): def create_onnboarding_job(self, cmd: OnboardCommand): """Create the onboarding job.""" - cluster_spec = compute.ClusterSpec( - spark_version=cmd.dbr_version, - num_workers=1, - driver_node_type_id=cloud_node_type_id_dict[cmd.cloud], - node_type_id=cloud_node_type_id_dict[cmd.cloud], - data_security_mode=compute.DataSecurityMode.SINGLE_USER - if cmd.uc_enabled else compute.DataSecurityMode.LEGACY_SINGLE_USER, - spark_conf={}, - spark_env_vars={ - "PYSPARK_PYTHON": "/databricks/python3/bin/python3" - } - ) + if not cmd.serverless: + cluster_spec = compute.ClusterSpec( + spark_version=cmd.dbr_version, + num_workers=1, + driver_node_type_id=cloud_node_type_id_dict[cmd.cloud], + node_type_id=cloud_node_type_id_dict[cmd.cloud], + data_security_mode=compute.DataSecurityMode.SINGLE_USER + if cmd.uc_enabled else compute.DataSecurityMode.LEGACY_SINGLE_USER, + spark_conf={}, + spark_env_vars={ + "PYSPARK_PYTHON": "/databricks/python3/bin/python3" + } + ) onboarding_filename = os.path.basename(cmd.onboarding_file_path) named_parameters = self._get_onboarding_named_parameters(cmd, onboarding_filename) return self._ws.jobs.create( name="dlt_meta_onboarding_job", + environments=None if not cmd.serverless else [jobs.JobEnvironment(key="dlt_meta_onboarding_env")], tasks=[ jobs.Task( task_key="dlt_meta_onbarding_task", description="test", - new_cluster=cluster_spec, + new_cluster=cluster_spec if cmd.serverless else None, + environment_key="dlt_meta_onboarding_env" if cmd.serverless else None, timeout_seconds=0, python_wheel_task=jobs.PythonWheelTask( package_name="dlt_meta", @@ -348,6 +352,9 @@ def _load_onboard_config(self) -> OnboardCommand: onboard_cmd_dict["onboarding_file_path"] = self._wsi._question( "Provide onboarding file path", default='demo/conf/onboarding.template') cwd = os.getcwd() + onboard_cmd_dict["serverless"] = self._wsi._choice( + "Run onboarding with serverless?", ['True', 'False']) + onboard_cmd_dict["serverless"] = True if onboard_cmd_dict["serverless"] == 'True' else False onboarding_files_dir_path = self._wsi._question( "Provide onboarding files local directory", default=f'{cwd}/demo/') onboard_cmd_dict["onboarding_files_dir_path"] = f"file:/{onboarding_files_dir_path}" From 9ca140c579b34d6ad640591e3d2813579129c243 Mon Sep 17 00:00:00 2001 From: ravi-databricks Date: Wed, 25 Sep 2024 12:21:19 -0700 Subject: [PATCH 02/59] Added uc volumes for fanout demo --- .gitignore | 4 --- demo/launch_silver_fanout_demo.py | 10 -------- integration_tests/run_integration_tests.py | 30 +++++----------------- 3 files changed, 6 insertions(+), 38 deletions(-) diff --git a/.gitignore b/.gitignore index f110e55..31759d8 100644 --- a/.gitignore +++ b/.gitignore @@ -155,9 +155,5 @@ deployment-merged.yaml .databricks-login.json demo/conf/onboarding.json integration_tests/conf/onboarding.json -<<<<<<< Updated upstream databricks.yaml -======= -.databricks ->>>>>>> Stashed changes diff --git a/demo/launch_silver_fanout_demo.py b/demo/launch_silver_fanout_demo.py index aa7e376..910046d 100644 --- a/demo/launch_silver_fanout_demo.py +++ b/demo/launch_silver_fanout_demo.py @@ -181,21 +181,11 @@ def create_sfo_workflow_spec(self, runner_conf: DLTMetaRunnerConf): "--uc_catalog_name": "provide databricks uc_catalog name, this is required to create volume, schema, table", "--cloud_provider_name": "provide cloud provider name. Supported values are aws , azure , gcp", "--dbr_version": "Provide databricks runtime spark version e.g 15.3.x-scala2.12", -<<<<<<< Updated upstream "--uc_volume_path": "provide databricks uc_volume name, where you want to push integration test \ data and configurations" } sfo_mandatory_args = ["uc_catalog_name", "cloud_provider_name", "dbr_version", "uc_volume_path"] -======= - "--dbfs_path": "Provide databricks workspace dbfs path where you want run integration tests \ - e.g --dbfs_path=dbfs:/tmp/DLT-META/", - "uc_volume_path": "provide databricks uc_volume name, where you want to push integration test \ - data and configurations" -} - -sfo_mandatory_args = ["uc_catalog_name", "cloud_provider_name", "dbr_version"] ->>>>>>> Stashed changes def main(): diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index 4a23819..0452f7c 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -753,6 +753,8 @@ def create_cloudfiles_onboarding(self, runner_conf: DLTMetaRunnerConf): data_flow[key] = value.format(dbfs_path=runner_conf.dbfs_tmp_path) if 'uc_volume_path' in value: data_flow[key] = value.format(uc_volume_path=runner_conf.uc_volume_path) + if 'uc_volume_path' in value: + data_flow[key] = value.format(uc_volume_path=runner_conf.uc_volume_path) if key == 'silver_append_flows': counter = 0 for flows in value: @@ -803,6 +805,8 @@ def create_cloudfiles_onboarding(self, runner_conf: DLTMetaRunnerConf): data_flow[key] = value.format(dbfs_path=runner_conf.dbfs_tmp_path) if 'uc_volume_path' in value: data_flow[key] = value.format(uc_volume_path=runner_conf.uc_volume_path) + if 'uc_volume_path' in value: + data_flow[key] = value.format(uc_volume_path=runner_conf.uc_volume_path) if 'uc_catalog_name' in value and 'bronze_schema' in value: if runner_conf.uc_catalog_name: data_flow[key] = value.format( @@ -824,6 +828,8 @@ def create_cloudfiles_onboarding(self, runner_conf: DLTMetaRunnerConf): data_flow[key] = value.format(dbfs_path=runner_conf.dbfs_tmp_path) if 'uc_volume_path' in value: data_flow[key] = value.format(uc_volume_path=runner_conf.uc_volume_path) + if 'uc_volume_path' in value: + data_flow[key] = value.format(uc_volume_path=runner_conf.uc_volume_path) if 'uc_catalog_name' in value and 'bronze_schema' in value: if runner_conf.uc_catalog_name: data_flow[key] = value.format( @@ -850,15 +856,9 @@ def __populate_source_details(self, runner_conf, data_flow, key, value): def copy(self, runner_conf: DLTMetaRunnerConf): if runner_conf.uc_catalog_name: -<<<<<<< Updated upstream - # runner_conf.volume_info = self.ws.api_client.volumes.create(catalog_name=runner_conf.uc_catalog_name, - # schema_name=runner_conf.dlt_meta_schema, - # name=runner_conf.uc_volume_name, - # volume_type=VolumeType.MANAGED) print(f"uploading to {runner_conf.uc_volume_path}/{self.base_dir}/ started") src = runner_conf.int_tests_dir dst = runner_conf.uc_volume_path -======= runner_conf.volume_info = self.ws.api_client.volumes.create(catalog_name=runner_conf.uc_catalog_name, schema_name=runner_conf.dlt_meta_schema, name=runner_conf.uc_volume_name, @@ -866,7 +866,6 @@ def copy(self, runner_conf: DLTMetaRunnerConf): print(f"uploading to {runner_conf.dbfs_tmp_path}/{self.base_dir}/ started") src = runner_conf.int_tests_dir dst = runner_conf.dbfs_tmp_path ->>>>>>> Stashed changes main_dir = src.replace('file:', '') base_dir_name = None if main_dir.endswith('/'): @@ -878,20 +877,11 @@ def copy(self, runner_conf: DLTMetaRunnerConf): for root, dirs, files in os.walk(main_dir): for filename in files: target_dir = root[root.index(main_dir) + len(main_dir):len(root)] -<<<<<<< Updated upstream uc_volume_path = f"{dst}/{base_dir_name}/{target_dir}/{filename}".replace("//", "/") contents = open(os.path.join(root, filename), "rb") # print(f"local_path={os.path.join(root, filename)}", # f"dbfs_path={dst}/{base_dir_name}/{target_dir}/{filename}") self.ws.files.upload(file_path=uc_volume_path, contents=contents, overwrite=True) -======= - dbfs_path = f"{dst}/{base_dir_name}/{target_dir}/{filename}" - contents = open(os.path.join(root, filename), "rb") - # print(f"local_path={os.path.join(root, filename)}", - # f"dbfs_path={dst}/{base_dir_name}/{target_dir}/{filename}") - self.ws.api_client.files.upload(file_path=dbfs_path, contents=contents, overwrite=True) ->>>>>>> Stashed changes - else: src = runner_conf.int_tests_dir dst = runner_conf.dbfs_tmp_path @@ -906,19 +896,11 @@ def copy(self, runner_conf: DLTMetaRunnerConf): for root, dirs, files in os.walk(main_dir): for filename in files: target_dir = root[root.index(main_dir) + len(main_dir):len(root)] -<<<<<<< Updated upstream - uc_volume_path = f"{dst}/{base_dir_name}/{target_dir}/{filename}" - contents = open(os.path.join(root, filename), "rb") - # print(f"local_path={os.path.join(root, filename)}", - # f"dbfs_path={dst}/{base_dir_name}/{target_dir}/{filename}") - self.ws.dbfs.upload(uc_volume_path, contents, overwrite=True) -======= dbfs_path = f"{dst}/{base_dir_name}/{target_dir}/{filename}" contents = open(os.path.join(root, filename), "rb") # print(f"local_path={os.path.join(root, filename)}", # f"dbfs_path={dst}/{base_dir_name}/{target_dir}/{filename}") self.ws.dbfs.upload(dbfs_path, contents, overwrite=True) ->>>>>>> Stashed changes def init_dltmeta_runner_conf(self, runner_conf: DLTMetaRunnerConf): self.generate_onboarding_file(runner_conf) From 0db98c842e7f487946443327135a119bba73db5b Mon Sep 17 00:00:00 2001 From: ravi-databricks Date: Wed, 25 Sep 2024 14:13:42 -0700 Subject: [PATCH 03/59] Added: 1.use uc volume paths to push config and resources 2.serverless for onboarding job and dlt --- demo/launch_silver_fanout_demo.py | 39 ++++++++-------- integration_tests/run_integration_tests.py | 54 ++++++++++++---------- 2 files changed, 50 insertions(+), 43 deletions(-) diff --git a/demo/launch_silver_fanout_demo.py b/demo/launch_silver_fanout_demo.py index 910046d..e54476c 100644 --- a/demo/launch_silver_fanout_demo.py +++ b/demo/launch_silver_fanout_demo.py @@ -1,7 +1,7 @@ import uuid import webbrowser -from databricks.sdk.service import jobs +from databricks.sdk.service import jobs, compute from src.install import WorkspaceInstaller from integration_tests.run_integration_tests import ( DLTMETARunner, @@ -48,7 +48,6 @@ def run(self, runner_conf: DLTMetaRunnerConf): try: self.init_dltmeta_runner_conf(runner_conf) self.create_bronze_silver_dlt(runner_conf) - self.create_cluster(runner_conf) self.launch_workflow(runner_conf) except Exception as e: print(e) @@ -66,8 +65,6 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: runner_conf = DLTMetaRunnerConf( run_id=run_id, username=self.wsi._my_username, - # dbfs_tmp_path=f"{self.args.__dict__['dbfs_path']}/{run_id}", - uc_volume_path=f"{self.args.__dict__['uc_volume_path']}/{run_id}", int_tests_dir="file:./demo", dlt_meta_schema=f"dlt_meta_dataflowspecs_demo_{run_id}", bronze_schema=f"dlt_meta_bronze_demo_{run_id}", @@ -84,17 +81,16 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: env="demo" ) runner_conf.uc_catalog_name = self.args.__dict__['uc_catalog_name'] + runner_conf.uc_volume_name = f"{runner_conf.uc_catalog_name}/dlt_meta_fout_demo/{run_id}" return runner_conf def launch_workflow(self, runner_conf: DLTMetaRunnerConf): created_job = self.create_sfo_workflow_spec(runner_conf) runner_conf.job_id = created_job.job_id - print(f"Job created successfully. job_id={created_job.job_id}, started run...") - webbrowser.open(f"{self.ws.config.host}/jobs/{created_job.job_id}?o={self.ws.get_workspace_id()}") - print(f"Waiting for job to complete. job_id={created_job.job_id}") - run_by_id = self.ws.jobs.run_now(job_id=created_job.job_id).result() - print(f"Job run finished. run_id={run_by_id}") - return created_job + url = f"{self.ws.config.host}/jobs/{created_job.job_id}?o={self.ws.get_workspace_id()}" + self.ws.jobs.run_now(job_id=created_job.job_id) + webbrowser.open(url) + print(f"Demo launched successfully. job_id={created_job.job_id}, url={url}") def create_sfo_workflow_spec(self, runner_conf: DLTMetaRunnerConf): """ @@ -107,13 +103,21 @@ def create_sfo_workflow_spec(self, runner_conf: DLTMetaRunnerConf): - created_job: The created job object. """ database, dlt_lib = self.init_db_dltlib(runner_conf) + dltmeta_environments = [ + jobs.JobEnvironment( + environment_key="dlt_meta_env", + spec=compute.Environment(client="1", dependencies=["dlt-meta==0.0.8"]) + ) + ] return self.ws.jobs.create( name=f"dlt-silver-fanout-demo-{runner_conf.run_id}", + environments=dltmeta_environments, tasks=[ jobs.Task( task_key="onboarding_job", description="Sets up metadata tables for DLT-META", - existing_cluster_id=runner_conf.cluster_id, + # existing_cluster_id=runner_conf.cluster_id, + environment_key="dlt_meta_env", timeout_seconds=0, python_wheel_task=jobs.PythonWheelTask( package_name="dlt_meta", @@ -132,13 +136,14 @@ def create_sfo_workflow_spec(self, runner_conf: DLTMetaRunnerConf): "uc_enabled": "True" }, ), - libraries=dlt_lib + # libraries=dlt_lib ), jobs.Task( task_key="onboard_silverfanout_job", description="Sets up metadata tables for DLT-META", depends_on=[jobs.TaskDependency(task_key="onboarding_job")], - existing_cluster_id=runner_conf.cluster_id, + # existing_cluster_id=runner_conf.cluster_id, + environment_key="dlt_meta_env", timeout_seconds=0, python_wheel_task=jobs.PythonWheelTask( package_name="dlt_meta", @@ -156,7 +161,7 @@ def create_sfo_workflow_spec(self, runner_conf: DLTMetaRunnerConf): "uc_enabled": "True" }, ), - libraries=dlt_lib + # libraries=dlt_lib ), jobs.Task( task_key="bronze_dlt", @@ -180,12 +185,10 @@ def create_sfo_workflow_spec(self, runner_conf: DLTMetaRunnerConf): "--profile": "provide databricks cli profile name, if not provide databricks_host and token", "--uc_catalog_name": "provide databricks uc_catalog name, this is required to create volume, schema, table", "--cloud_provider_name": "provide cloud provider name. Supported values are aws , azure , gcp", - "--dbr_version": "Provide databricks runtime spark version e.g 15.3.x-scala2.12", - "--uc_volume_path": "provide databricks uc_volume name, where you want to push integration test \ - data and configurations" + "--dbr_version": "Provide databricks runtime spark version e.g 15.3.x-scala2.12" } -sfo_mandatory_args = ["uc_catalog_name", "cloud_provider_name", "dbr_version", "uc_volume_path"] +sfo_mandatory_args = ["uc_catalog_name", "cloud_provider_name", "dbr_version"] def main(): diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index 0452f7c..b7cc2ae 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -184,7 +184,9 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: ) if self.args.__dict__['uc_catalog_name']: runner_conf.uc_catalog_name = self.args.__dict__['uc_catalog_name'] - + runner_conf.uc_volume_name = f"{self.args.__dict__['uc_catalog_name']}_volume_{run_id}", + runner_conf.uc_volume_path = (f"/Volumes/{runner_conf.uc_catalog_name}/" + f"{runner_conf.volume_info.schema_name}/{runner_conf.volume_info.name}/") runners_full_local_path = None if runner_conf.source.lower() == "cloudfiles": @@ -266,6 +268,7 @@ def create_dlt_meta_pipeline(self, created = self.ws.pipelines.create( catalog=runner_conf.uc_catalog_name, name=pipeline_name, + serverless=True, configuration=configuration, libraries=[ PipelineLibrary( @@ -274,8 +277,7 @@ def create_dlt_meta_pipeline(self, ) ) ], - target=target_schema, - clusters=[pipelines.PipelineCluster(label="default", num_workers=2)] + target=target_schema ) else: configuration[f"{layer}.dataflowspecTable"] = ( @@ -283,7 +285,7 @@ def create_dlt_meta_pipeline(self, ) created = self.ws.pipelines.create( name=pipeline_name, - # serverless=True, + serverless=True, channel="PREVIEW", configuration=configuration, libraries=[ @@ -293,9 +295,7 @@ def create_dlt_meta_pipeline(self, ) ) ], - target=target_schema, - clusters=[pipelines.PipelineCluster(label="default", num_workers=2)] - + target=target_schema ) if created is None: raise Exception("Pipeline creation failed") @@ -859,13 +859,6 @@ def copy(self, runner_conf: DLTMetaRunnerConf): print(f"uploading to {runner_conf.uc_volume_path}/{self.base_dir}/ started") src = runner_conf.int_tests_dir dst = runner_conf.uc_volume_path - runner_conf.volume_info = self.ws.api_client.volumes.create(catalog_name=runner_conf.uc_catalog_name, - schema_name=runner_conf.dlt_meta_schema, - name=runner_conf.uc_volume_name, - volume_type=VolumeType.MANAGED) - print(f"uploading to {runner_conf.dbfs_tmp_path}/{self.base_dir}/ started") - src = runner_conf.int_tests_dir - dst = runner_conf.dbfs_tmp_path main_dir = src.replace('file:', '') base_dir_name = None if main_dir.endswith('/'): @@ -903,6 +896,8 @@ def copy(self, runner_conf: DLTMetaRunnerConf): self.ws.dbfs.upload(dbfs_path, contents, overwrite=True) def init_dltmeta_runner_conf(self, runner_conf: DLTMetaRunnerConf): + if runner_conf.uc_catalog_name: + self.initialize_uc_resources(runner_conf) self.generate_onboarding_file(runner_conf) print("int_tests_dir: ", runner_conf.int_tests_dir) self.copy(runner_conf) @@ -914,18 +909,28 @@ def init_dltmeta_runner_conf(self, runner_conf: DLTMetaRunnerConf): format=ImportFormat.DBC, content=fp.read()) print(f"uploading to {runner_conf.runners_nb_path} complete!!!") if runner_conf.uc_catalog_name: - SchemasAPI(self.ws.api_client).create(catalog_name=runner_conf.uc_catalog_name, - name=runner_conf.dlt_meta_schema, - comment="dlt_meta framework schema") - SchemasAPI(self.ws.api_client).create(catalog_name=runner_conf.uc_catalog_name, - name=runner_conf.bronze_schema, - comment="bronze_schema") - if runner_conf.source and runner_conf.source.lower() == "cloudfiles": - SchemasAPI(self.ws.api_client).create(catalog_name=runner_conf.uc_catalog_name, - name=runner_conf.silver_schema, - comment="silver_schema") self.build_and_upload_package(runner_conf) + def initialize_uc_resources(self, runner_conf): + SchemasAPI(self.ws.api_client).create(catalog_name=runner_conf.uc_catalog_name, + name=runner_conf.dlt_meta_schema, + comment="dlt_meta framework schema") + SchemasAPI(self.ws.api_client).create(catalog_name=runner_conf.uc_catalog_name, + name=runner_conf.bronze_schema, + comment="bronze_schema") + if runner_conf.source and runner_conf.source.lower() == "cloudfiles": + SchemasAPI(self.ws.api_client).create(catalog_name=runner_conf.uc_catalog_name, + name=runner_conf.silver_schema, + comment="silver_schema") + volume_info = self.ws.volumes.create(catalog_name=runner_conf.uc_catalog_name, + schema_name=runner_conf.dlt_meta_schema, + name=runner_conf.dlt_meta_schema, + volume_type=VolumeType.MANAGED) + runner_conf.volume_info = volume_info + runner_conf.uc_volume_path = (f"/Volumes/{runner_conf.volume_info.catalog_name}/" + f"{runner_conf.volume_info.schema_name}/{runner_conf.volume_info.name}/" + ) + def create_cluster(self, runner_conf: DLTMetaRunnerConf): print("Cluster creation started...") if runner_conf.uc_catalog_name: @@ -959,7 +964,6 @@ def run(self, runner_conf: DLTMetaRunnerConf): try: self.init_dltmeta_runner_conf(runner_conf) self.create_bronze_silver_dlt(runner_conf) - self.create_cluster(runner_conf) self.launch_workflow(runner_conf) self.download_test_results(runner_conf) except Exception as e: From cbf8b2981fc2b3352a601d6621f67d95f8c31209 Mon Sep 17 00:00:00 2001 From: ravi-databricks Date: Wed, 25 Sep 2024 14:16:12 -0700 Subject: [PATCH 04/59] Removed linting errors --- integration_tests/run_integration_tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index b7cc2ae..14e0159 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from databricks.sdk import WorkspaceClient from databricks.sdk.service.pipelines import PipelineLibrary, NotebookLibrary -from databricks.sdk.service import jobs, pipelines, compute +from databricks.sdk.service import jobs, compute from databricks.sdk.service.workspace import ImportFormat from databricks.sdk.service.catalog import SchemasAPI, VolumeInfo, VolumeType from src.install import WorkspaceInstaller @@ -897,7 +897,7 @@ def copy(self, runner_conf: DLTMetaRunnerConf): def init_dltmeta_runner_conf(self, runner_conf: DLTMetaRunnerConf): if runner_conf.uc_catalog_name: - self.initialize_uc_resources(runner_conf) + self.initialize_uc_resources(runner_conf) self.generate_onboarding_file(runner_conf) print("int_tests_dir: ", runner_conf.int_tests_dir) self.copy(runner_conf) From 3bd552a50253e5c076cb38f6ea15b44f7191734a Mon Sep 17 00:00:00 2001 From: ravi-databricks Date: Wed, 25 Sep 2024 22:23:59 -0700 Subject: [PATCH 05/59] Added: 1.Updated all demo's to include unity catalog volumes path instead of dbfs 2.Updated all demo's to use serverless 3.Updated docs for demo options 4.Added demo workflow png files for DAIS and Techsummit demo --- demo/conf/cloudfiles-onboarding.template | 34 ++++----- demo/conf/cloudfiles-onboarding_A2.template | 8 +-- demo/conf/onboarding.template | 74 ++++++++++---------- demo/dbc/tech_summit_dlt_meta_runners.dbc | Bin 5393 -> 6929 bytes demo/launch_af_cloudfiles_demo.py | 21 ++---- demo/launch_dais_demo.py | 42 ++++++----- demo/launch_silver_fanout_demo.py | 12 ++-- demo/launch_techsummit_demo.py | 47 ++++++++----- docs/content/demo/Append_FLOW_CF.md | 2 +- docs/content/demo/Append_FLOW_EH.md | 4 +- docs/content/demo/DAIS.md | 23 ++---- docs/content/demo/Silver_Fanout.md | 9 +-- docs/content/demo/Techsummit.md | 25 ++----- docs/static/images/dais_demo.png | Bin 0 -> 233005 bytes docs/static/images/tech_summit_demo.png | Bin 0 -> 214257 bytes integration_tests/run_integration_tests.py | 51 +++++++------- 16 files changed, 163 insertions(+), 189 deletions(-) create mode 100644 docs/static/images/dais_demo.png create mode 100644 docs/static/images/tech_summit_demo.png diff --git a/demo/conf/cloudfiles-onboarding.template b/demo/conf/cloudfiles-onboarding.template index cd8ba86..3dcb04d 100644 --- a/demo/conf/cloudfiles-onboarding.template +++ b/demo/conf/cloudfiles-onboarding.template @@ -7,7 +7,7 @@ "source_details": { "source_database": "APP", "source_table": "CUSTOMERS", - "source_path_demo": "{dbfs_path}/demo/resources/data/afam/data/customers", + "source_path_demo": "{uc_volume_path}/demo/resources/data/afam/data/customers", "source_metadata": { "include_autoloader_metadata_column": "True", "autoloader_metadata_col_name": "source_metadata", @@ -16,7 +16,7 @@ "input_file_path": "_metadata.file_path" } }, - "source_schema_path": "{dbfs_path}/demo/resources/data/afam/ddl/customers.ddl" + "source_schema_path": "{uc_volume_path}/demo/resources/data/afam/ddl/customers.ddl" }, "bronze_database_demo": "{uc_catalog_name}.{bronze_schema}", "bronze_table": "customers", @@ -25,15 +25,13 @@ "cloudFiles.inferColumnTypes": "true", "cloudFiles.rescuedDataColumn": "_rescued_data" }, - "bronze_table_path_demo": "{dbfs_path}/data/bronze/customers", "bronze_table_properties": { "pipelines.autoOptimize.managed": "true", "pipelines.autoOptimize.zOrderCols": "id, email" }, - "bronze_data_quality_expectations_json_demo": "{dbfs_path}/demo/conf/dqe/customers/bronze_data_quality_expectations.json", + "bronze_data_quality_expectations_json_demo": "{uc_volume_path}/demo/conf/dqe/customers/bronze_data_quality_expectations.json", "bronze_database_quarantine_demo": "{uc_catalog_name}.{bronze_schema}", "bronze_quarantine_table": "customers_quarantine", - "bronze_quarantine_table_path_demo": "{dbfs_path}/data/bronze/customers_quarantine", "bronze_quarantine_table_properties": { "pipelines.reset.allowed": "false", "pipelines.autoOptimize.zOrderCols": "id, email" @@ -44,8 +42,8 @@ "create_streaming_table": false, "source_format": "cloudFiles", "source_details": { - "source_path_demo": "{dbfs_path}/demo/resources/data/afam/data/customers_af", - "source_schema_path": "{dbfs_path}/demo/resources/data/afam/ddl/customers.ddl" + "source_path_demo": "{uc_volume_path}/demo/resources/data/afam/data/customers_af", + "source_schema_path": "{uc_volume_path}/demo/resources/data/afam/ddl/customers.ddl" }, "reader_options": { "cloudFiles.format": "json", @@ -57,13 +55,12 @@ ], "silver_database_demo": "{uc_catalog_name}.{silver_schema}", "silver_table": "customers", - "silver_table_path_demo": "{dbfs_path}/data/silver/customers", - "silver_transformation_json_demo": "{dbfs_path}/demo/conf/afam_silver_transformations.json", + "silver_transformation_json_demo": "{uc_volume_path}/demo/conf/afam_silver_transformations.json", "silver_table_properties": { "pipelines.reset.allowed": "false", "pipelines.autoOptimize.zOrderCols": "id, email" }, - "silver_data_quality_expectations_json_demo": "{dbfs_path}/demo/conf/dqe/customers/silver_data_quality_expectations.json", + "silver_data_quality_expectations_json_demo": "{uc_volume_path}/demo/conf/dqe/customers/silver_data_quality_expectations.json", "silver_append_flows": [ { "name": "customers_silver_flow", @@ -85,7 +82,7 @@ "source_details": { "source_database": "APP", "source_table": "TRANSACTIONS", - "source_path_demo": "{dbfs_path}/demo/resources/data/afam/data/transactions", + "source_path_demo": "{uc_volume_path}/demo/resources/data/afam/data/transactions", "source_metadata": { "include_autoloader_metadata_column": "True", "select_metadata_cols": { @@ -93,7 +90,7 @@ "input_file_path": "_metadata.file_path" } }, - "source_schema_path": "{dbfs_path}/demo/resources/data/afam/ddl/transactions.ddl" + "source_schema_path": "{uc_volume_path}/demo/resources/data/afam/ddl/transactions.ddl" }, "bronze_database_demo": "{uc_catalog_name}.{bronze_schema}", "bronze_table": "transactions", @@ -102,15 +99,13 @@ "cloudFiles.inferColumnTypes": "true", "cloudFiles.rescuedDataColumn": "_rescued_data" }, - "bronze_table_path_demo": "{dbfs_path}/data/bronze/transactions", "bronze_table_properties": { "pipelines.reset.allowed": "true", "pipelines.autoOptimize.zOrderCols": "id, customer_id" }, - "bronze_data_quality_expectations_json_demo": "{dbfs_path}/demo/conf/dqe/transactions/bronze_data_quality_expectations.json", + "bronze_data_quality_expectations_json_demo": "{uc_volume_path}/demo/conf/dqe/transactions/bronze_data_quality_expectations.json", "bronze_database_quarantine_demo": "{uc_catalog_name}.{bronze_schema}", "bronze_quarantine_table": "transactions_quarantine", - "bronze_quarantine_table_path_demo": "{dbfs_path}/data/bronze/transactions_quarantine", "bronze_quarantine_table_properties": { "pipelines.reset.allowed": "true", "pipelines.autoOptimize.managed": "false", @@ -122,8 +117,8 @@ "create_streaming_table": false, "source_format": "cloudFiles", "source_details": { - "source_path_demo": "{dbfs_path}/demo/resources/data/afam/data/transactions_af", - "source_schema_path": "{dbfs_path}/demo/resources/data/afam/ddl/transactions.ddl" + "source_path_demo": "{uc_volume_path}/demo/resources/data/afam/data/transactions_af", + "source_schema_path": "{uc_volume_path}/demo/resources/data/afam/ddl/transactions.ddl" }, "reader_options": { "cloudFiles.format": "json", @@ -149,9 +144,8 @@ ], "flow_name":"silver_transactions_cdc_applychanges_flow" }, - "silver_table_path_demo": "{dbfs_path}/data/silver/transactions", - "silver_transformation_json_demo": "{dbfs_path}/demo/conf/afam_silver_transformations.json", - "silver_data_quality_expectations_json_demo": "{dbfs_path}/demo/conf/dqe/transactions/silver_data_quality_expectations.json", + "silver_transformation_json_demo": "{uc_volume_path}/demo/conf/afam_silver_transformations.json", + "silver_data_quality_expectations_json_demo": "{uc_volume_path}/demo/conf/dqe/transactions/silver_data_quality_expectations.json", "silver_table_properties": { "pipelines.reset.allowed": "false", "pipelines.autoOptimize.zOrderCols": "id, customer_id" diff --git a/demo/conf/cloudfiles-onboarding_A2.template b/demo/conf/cloudfiles-onboarding_A2.template index e7915c9..2694f70 100644 --- a/demo/conf/cloudfiles-onboarding_A2.template +++ b/demo/conf/cloudfiles-onboarding_A2.template @@ -7,14 +7,14 @@ "source_details": { "source_database": "APP", "source_table": "CUSTOMERS", - "source_path_demo": "{dbfs_path}/demo/resources/data/afam/data/customers_delta", + "source_path_demo": "{uc_volume_path}/demo/resources/data/afam/data/customers_delta", "source_metadata": { "select_metadata_cols": { "input_file_name": "_metadata.file_name", "input_file_path": "_metadata.file_path" } }, - "source_schema_path": "{dbfs_path}/demo/resources/data/afam/ddl/customers.ddl" + "source_schema_path": "{uc_volume_path}/demo/resources/data/afam/ddl/customers.ddl" }, "bronze_database_demo": "{uc_catalog_name}.{bronze_schema}", "bronze_table": "customers_delta", @@ -23,15 +23,13 @@ "cloudFiles.inferColumnTypes": "true", "cloudFiles.rescuedDataColumn": "_rescued_data" }, - "bronze_table_path_demo": "{dbfs_path}/data/bronze/customers_delta", "bronze_table_properties": { "pipelines.autoOptimize.managed": "true", "pipelines.autoOptimize.zOrderCols": "id, email" }, - "bronze_data_quality_expectations_json_demo": "{dbfs_path}/demo/conf/dqe/customers/bronze_data_quality_expectations.json", + "bronze_data_quality_expectations_json_demo": "{uc_volume_path}/demo/conf/dqe/customers/bronze_data_quality_expectations.json", "bronze_database_quarantine_demo": "{uc_catalog_name}.{bronze_schema}", "bronze_quarantine_table": "customers_delta_quarantine", - "bronze_quarantine_table_path_demo": "{dbfs_path}/data/bronze/customers_quarantine_delta", "bronze_quarantine_table_properties": { "pipelines.reset.allowed": "false", "pipelines.autoOptimize.zOrderCols": "id, email" diff --git a/demo/conf/onboarding.template b/demo/conf/onboarding.template index 39fd45c..42d25fa 100644 --- a/demo/conf/onboarding.template +++ b/demo/conf/onboarding.template @@ -7,24 +7,24 @@ "source_details": { "source_database": "customers", "source_table": "customers", - "source_path_prod": "{dbfs_path}/demo/resources/data/customers", - "source_schema_path": "{dbfs_path}/demo/resources/ddl/customers.ddl" + "source_path_prod": "{uc_volume_path}/demo/resources/data/customers", + "source_schema_path": "{uc_volume_path}/demo/resources/ddl/customers.ddl" }, "bronze_database_prod": "{uc_catalog_name}.{bronze_schema}", "bronze_table": "customers", - "bronze_table_path_prod": "{dbfs_path}/data/bronze/customers", + "bronze_table_path_prod": "{uc_volume_path}/data/bronze/customers", "bronze_reader_options": { "cloudFiles.format": "csv", "cloudFiles.rescuedDataColumn": "_rescued_data", "header": "true" }, - "bronze_data_quality_expectations_json_prod": "{dbfs_path}/demo/conf/dqe/customers.json", + "bronze_data_quality_expectations_json_prod": "{uc_volume_path}/demo/conf/dqe/customers.json", "bronze_database_quarantine_prod": "{uc_catalog_name}.{bronze_schema}", "bronze_quarantine_table": "customers_quarantine", - "bronze_quarantine_table_path_prod": "{dbfs_path}/data/bronze/customers_quarantine", + "bronze_quarantine_table_path_prod": "{uc_volume_path}/data/bronze/customers_quarantine", "silver_database_prod": "{uc_catalog_name}.{silver_schema}", "silver_table": "customers", - "silver_table_path_prod": "{dbfs_path}/data/silver/customers", + "silver_table_path_prod": "{uc_volume_path}/data/silver/customers", "silver_cdc_apply_changes": { "keys": [ "customer_id" @@ -38,8 +38,8 @@ "_rescued_data" ] }, - "silver_transformation_json_prod": "{dbfs_path}/demo/conf/silver_transformations.json", - "silver_data_quality_expectations_json_prod": "{dbfs_path}/demo/conf/dqe/customers_silver_dqe.json" + "silver_transformation_json_prod": "{uc_volume_path}/demo/conf/silver_transformations.json", + "silver_data_quality_expectations_json_prod": "{uc_volume_path}/demo/conf/dqe/customers_silver_dqe.json" }, { @@ -50,24 +50,24 @@ "source_details": { "source_database": "transactions", "source_table": "transactions", - "source_path_prod": "{dbfs_path}/demo/resources/data/transactions", - "source_schema_path": "{dbfs_path}/demo/resources/ddl/transactions.ddl" + "source_path_prod": "{uc_volume_path}/demo/resources/data/transactions", + "source_schema_path": "{uc_volume_path}/demo/resources/ddl/transactions.ddl" }, "bronze_database_prod": "{uc_catalog_name}.{bronze_schema}", "bronze_table": "transactions", - "bronze_table_path_prod": "{dbfs_path}/data/bronze/transactions", + "bronze_table_path_prod": "{uc_volume_path}/data/bronze/transactions", "bronze_reader_options": { "cloudFiles.format": "csv", "cloudFiles.rescuedDataColumn": "_rescued_data", "header": "true" }, - "bronze_data_quality_expectations_json_prod": "{dbfs_path}/demo/conf/dqe/transactions.json", + "bronze_data_quality_expectations_json_prod": "{uc_volume_path}/demo/conf/dqe/transactions.json", "bronze_database_quarantine_prod": "{uc_catalog_name}.{bronze_schema}", "bronze_quarantine_table": "transactions_quarantine", - "bronze_quarantine_table_path_prod": "{dbfs_path}/demo/resources/data/bronze/transactions_quarantine", + "bronze_quarantine_table_path_prod": "{uc_volume_path}/demo/resources/data/bronze/transactions_quarantine", "silver_database_prod": "{uc_catalog_name}.{silver_schema}", "silver_table": "transactions", - "silver_table_path_prod": "{dbfs_path}/data/silver/transactions", + "silver_table_path_prod": "{uc_volume_path}/data/silver/transactions", "silver_cdc_apply_changes": { "keys": [ "transaction_id" @@ -81,9 +81,9 @@ "_rescued_data" ] }, - "silver_table_path_prod": "{dbfs_path}/demo/resources/data/silver/transactions", - "silver_transformation_json_prod": "{dbfs_path}/demo/conf/silver_transformations.json", - "silver_data_quality_expectations_json_prod": "{dbfs_path}/demo/conf/dqe/transactions_silver_dqe.json" + "silver_table_path_prod": "{uc_volume_path}/demo/resources/data/silver/transactions", + "silver_transformation_json_prod": "{uc_volume_path}/demo/conf/silver_transformations.json", + "silver_data_quality_expectations_json_prod": "{uc_volume_path}/demo/conf/dqe/transactions_silver_dqe.json" }, { "data_flow_id": "103", @@ -93,25 +93,25 @@ "source_details": { "source_database": "products", "source_table": "products", - "source_path_prod": "{dbfs_path}/demo/resources/data/products", - "source_schema_path": "{dbfs_path}/demo/resources/ddl/products.ddl" + "source_path_prod": "{uc_volume_path}/demo/resources/data/products", + "source_schema_path": "{uc_volume_path}/demo/resources/ddl/products.ddl" }, "bronze_database_prod": "{uc_catalog_name}.{bronze_schema}", "bronze_table": "products", - "bronze_table_path_prod": "{dbfs_path}/data/bronze/products", + "bronze_table_path_prod": "{uc_volume_path}/data/bronze/products", "bronze_reader_options": { "cloudFiles.format": "csv", "cloudFiles.rescuedDataColumn": "_rescued_data", "header": "true" }, - "bronze_table_path_prod": "{dbfs_path}/demo/resources/data/bronze/products", - "bronze_data_quality_expectations_json_prod": "{dbfs_path}/demo/conf/dqe/products.json", + "bronze_table_path_prod": "{uc_volume_path}/demo/resources/data/bronze/products", + "bronze_data_quality_expectations_json_prod": "{uc_volume_path}/demo/conf/dqe/products.json", "bronze_database_quarantine_prod": "{uc_catalog_name}.{bronze_schema}", "bronze_quarantine_table": "products_quarantine", - "bronze_quarantine_table_path_prod": "{dbfs_path}/demo/resources/data/bronze/products_quarantine", + "bronze_quarantine_table_path_prod": "{uc_volume_path}/demo/resources/data/bronze/products_quarantine", "silver_database_prod": "{uc_catalog_name}.{silver_schema}", "silver_table": "products", - "silver_table_path_prod": "{dbfs_path}/data/silver/products", + "silver_table_path_prod": "{uc_volume_path}/data/silver/products", "silver_cdc_apply_changes": { "keys": [ "product_id" @@ -125,9 +125,9 @@ "_rescued_data" ] }, - "silver_table_path_prod": "{dbfs_path}/demo/resources/data/silver/products", - "silver_transformation_json_prod": "{dbfs_path}/demo/conf/silver_transformations.json", - "silver_data_quality_expectations_json_prod": "{dbfs_path}/demo/conf/dqe/products_silver_dqe.json" + "silver_table_path_prod": "{uc_volume_path}/demo/resources/data/silver/products", + "silver_transformation_json_prod": "{uc_volume_path}/demo/conf/silver_transformations.json", + "silver_data_quality_expectations_json_prod": "{uc_volume_path}/demo/conf/dqe/products_silver_dqe.json" }, { "data_flow_id": "104", @@ -137,25 +137,25 @@ "source_details": { "source_database": "stores", "source_table": "stores", - "source_path_prod": "{dbfs_path}/demo/resources/data/stores", - "source_schema_path": "{dbfs_path}/demo/resources/ddl/stores.ddl" + "source_path_prod": "{uc_volume_path}/demo/resources/data/stores", + "source_schema_path": "{uc_volume_path}/demo/resources/ddl/stores.ddl" }, "bronze_database_prod": "{uc_catalog_name}.{bronze_schema}", "bronze_table": "stores", - "bronze_table_path_prod": "{dbfs_path}/data/bronze/stores", + "bronze_table_path_prod": "{uc_volume_path}/data/bronze/stores", "bronze_reader_options": { "cloudFiles.format": "csv", "cloudFiles.rescuedDataColumn": "_rescued_data", "header": "true" }, - "bronze_table_path_prod": "{dbfs_path}/demo/resources/data/bronze/stores", - "bronze_data_quality_expectations_json_prod": "{dbfs_path}/demo/conf/dqe/stores.json", + "bronze_table_path_prod": "{uc_volume_path}/demo/resources/data/bronze/stores", + "bronze_data_quality_expectations_json_prod": "{uc_volume_path}/demo/conf/dqe/stores.json", "bronze_database_quarantine_prod": "{uc_catalog_name}.{bronze_schema}", "bronze_quarantine_table": "stores_quarantine", - "bronze_quarantine_table_path_prod": "{dbfs_path}/demo/resources/data/bronze/stores_quarantine", + "bronze_quarantine_table_path_prod": "{uc_volume_path}/demo/resources/data/bronze/stores_quarantine", "silver_database_prod": "{uc_catalog_name}.{silver_schema}", "silver_table": "stores", - "silver_table_path_prod": "{dbfs_path}/data/silver/stores", + "silver_table_path_prod": "{uc_volume_path}/data/silver/stores", "silver_cdc_apply_changes": { "keys": [ "store_id" @@ -169,8 +169,8 @@ "_rescued_data" ] }, - "silver_table_path_prod": "{dbfs_path}/demo/resources/data/silver/stores", - "silver_transformation_json_prod": "{dbfs_path}/demo/conf/silver_transformations.json", - "silver_data_quality_expectations_json_prod": "{dbfs_path}/demo/conf/dqe/stores_silver_dqe.json" + "silver_table_path_prod": "{uc_volume_path}/demo/resources/data/silver/stores", + "silver_transformation_json_prod": "{uc_volume_path}/demo/conf/silver_transformations.json", + "silver_data_quality_expectations_json_prod": "{uc_volume_path}/demo/conf/dqe/stores_silver_dqe.json" } ] \ No newline at end of file diff --git a/demo/dbc/tech_summit_dlt_meta_runners.dbc b/demo/dbc/tech_summit_dlt_meta_runners.dbc index e0bf7f55bcae1c3f4d1ce0a017a9ec26f680a836..eb31a12518ae133d09e69b5595123521171e8451 100644 GIT binary patch delta 6673 zcmZXZRa6{Gv$k;vPLSXpba0noA;7@k?(Si59bkaq?oMzG790i-8r*{fmkjO@^z8lL zeBU{L^{Tb1tJhoIH`N#QB(Wt4;;JbjqYxorU|=8^Qi|#);nG3dae}z95xiZgiZk(h zKTGxDOZ)E9OK&^k!%{+SJ-qSO%q6YZx%;-G2V8OsXpOho;l8E8BXkhz?|ikN1khkPOGXF?2-g4j|GxX5#J`qQ!-FTl z$1fzn!!ID%z|YrMKc95MN?yQOcBS>tvED!azjZ0}sGLw^EcaF6lA8Q^ODaEJPE#3dCP-uq75u8aV6upkv)GxsjY&pdL)2stIdk?sqbSpp#@;#R!vUx4DIe?w8Ar>yHlA;Qu5nh_wLC3ZoK zUt7@S9(b|!w0%7w-ostTrK&0>K4VW5HX~0tJRcZ~%H*WWo+;u7Xf~D~=?AgaHeYX{ zFpu20+3&Lqm51EYm|ss^^wSJTwrZ8Lw0lKC1RT^iI3z^Dn?v4z<=L;rn__uLC#6kd z&7p6F>VKOdc79#NY#fs`f&`~D4SMaAMo?=yGuoY+*Z1Kqshnb-t(kkfW)S7m#D8?& zLvJ5QNSji+lK{y&$;W&OeHZDQ-CSqkPl@)Of?1f>vY%x#tX0SL?L&SS_unQ)OO33k zo#6Wi&i)Y_ImMjqI`HvZVqRP&BI&C-&T44HNC(7fpyf#TKsvUVk&|4dK7ue;5YXuo z-&x0^XnRdty}gTtO(&PAvx~E#n2xE*90L}j)9{#V?42kU?8bspW<)02p(d}l3#zOp ze1uI0Irfw!W!<%1L`*+~pImKTBmSlND}8F)u_!5HmXyV}_4IV5$&3m0ccDu)`wg`_ z4^i+1xvUg94xy|pGT!$NN$7J3W08{5?CiRSo*6&wAekMAir=2NKQb8bVv@^(7O#4T z@RpCM?=uB-_d*I(PSPJm8f~Qdw-0gQKW6Gs*}>85xr~gx@at+JGg523$LzRs1*&C2 z9Bzl`g;7fTDgn>3Y=OSTwNvlKpHMJ<#R0yZDnS9I%%MLo<+q`M`gu6f(V<6tF3>ce z0m2LAL`lb={#y4oC}JJ21q^u;sIW^GhL^%==N^>|)0fZk+~fjI&*h=wfGX&e*^N-& z3>VCI_;TwX#EIWeDp%XxhV;f#v@Ja3kgb?lB)bS%9^pPlT52EJ+cY$Qpjc(s{b9gM z_TiGnxp6D%u)f!=4$8D!8?^67(xKoVLC=+KrlP4;v0Gf~2#66KOJ;2pj18G#)1x9e z(Xf_HEpKk-xt9K2WcIIM{!kWrHe4_zJ7#FIW%h)ryXojm+z#8;G_jV9=1vWfAA8t~ zRjFD_LQ=)vPOHWrVx__)C=cb7K&&>7DsvT26F;#ztS;WkRy+FWwsV zsGsDnR_zn+}^bE$LHZcuitv_&C>#@g=Fu6G+z{ zWn6zbH}5$DgHSAU^lnNd<;Pf0OI`lnWWUt8$1fm}C`7-l+-9OeS@R<4@=V_sd018D zq17b9${BZ2h3XAO2tn*rlWzBSxg5{D48w<7u!-M}di|j>`DnA2{&=pxMhr)0PqW=7aWTv%sOdw>AS=M`8iSUcR!Zr_CW1<8~ zk=rR2w}9N?%0Ax2NkHH`ODe*`I36u*GkDs8FOSjNx$7rlP21aPcb#mkX#@ z46tMq_-Jo`9{9()GA0wXYK|N!BeIWh5p&gwa5UOga1yjyS%rkF{~lyVXz=D8kp;v_ zucVUfaQcz4{3tExmQHuFD6Nb^(l2dAvDlUPj&D5Ik4 zX8l5|YjeyBI-I76&L$ z%Ifv0zCVVpYo&eR4or7gqV}#U@IYF^R#~MSvc7oBTzVx~(NT=55`982pOIN>BGsUO zAV_l^Qk-^3B<7qU9Ncywr*-`%U5qjp90?uHF>4rIanf9x#8ICF4RrK2gDOxuj$+jz$!i zbjLfUm9@mWETdJBno`r?s_pyL3HRsVs8&z(s|LnS^)mpSZUYqa*hP<9`xmc3qlur1 z%!`)b8A_<$sAG5{Yq1OPc0qt%jx>}wwen=K^okw6Jr;q7qpE0|2Z>bAVX}zy^wWS! z1*hVM`YWL+$-iSxcI;X5VYI39jN{;v=n(;Hg{(ql+3|!}lSVJ$8j7W3l2p+sI~0rK z^YbrZccTo)Hqg5aJc`sc*R^2t@1@VaoB@@Rf#EC7HGawvB$f~{$d{xSFZ;8T+4cP6 zS!&jRk|&K=kM!Reez2~)=+-+AoZ>!GibK6Q=Bh1h0b-mHp;~}AO&WYaB`OSNDarvb-c=~Q3|L*6n2W|$*LE9j9{|7Vk zjv%+H2ec|5b)|7>JK*g{gbED9`bEYV|CEJ~Jj#u`m91Li#gDHk9hx+K#=yivs>bYs5 z=?(jUSJtzDa{IuWu>{Wt&!}!u{i`2kHM^RM2C5_e*VO!yrL<*1%H~Fg?IcG@5z>-B z{GroJFH-jJ8e=$(PaPhge6<^UA$1&@bD_jPyuSUq>uw6=i;E4Of-JG{p-T23NB1wB zVIuKf!gMLnCmJ%z6cjQ==xxLi%ReCxrb}y3;Oq;a5x0_dCPk?fJ!V)0xOp9+d3BE8 zs5z*C&!$+RF?vqmFY%m0T?n>vuj|5^&rroX8f+deVq1s@0WrZf_>Z_-jq`=XBSAsH zR9?9=dRKP~-Xm5uXW`}-(X45;jI%G6z-+AgwBuoT25v?IIz#cud2@F0HO?SX)T{wA zB%WUSXhT=nPm7U=W4pz`!pga-HjQUuH|Gj$caiKWv;amBrm>uJ4rGhNH zP_}5WLUZ~-|IiK2EYiN3}(q97r7whXsz?wyTZ#Q2f2f_4N>Dx=!-fBP3MA| z`}D@bpPiR1;e`dIu+KgOgRX7<;xRI&D!^~R;NwF6m-%2O6u!p;yisf$5?w0}5#jF* z4B>LS#F?4Mofg>Wc<)WZpoEGO(G}_=L3n2;XnUE%f&lXYisDYhx0Cmb4n{p7tc!-v zt&J|o-Hsth62;{C>ASMd2a(D0MrDMFDCV=EIW@jTK0FNZ^;Gpw`ttkOYzDPJJ5IVo zbr5GiNz56sdvBx|(<&1>Kc|iaj+BB^2GXAZt>Gpf7|L=wft+Xtl;GET;;JE0MviHx zCBE_mqOfj(g5jyP!FxZ0CAw_yXep}*dAvBLG@my*L#sr3QKg3d2d7ystDRDL$bn@iM9E*dW9ifr5E~M~XUG-Qneht+4`n7ZR z@vjqI@&)vh*5=Q;185-<4JDU!q@V(+O1UTGvaSO{GrDIV^#ssfp75q^lkesl25?Lr zsS@?F7_3$eT(_Gm$&kmr(Z9K{ur!cK+m*|&qQ>YgGGKe%Jp>5g!gw>&zClA zHuw9kEO}ws22c85&)&9-nD-S%sR}Rx!KRGA{t8{U&TLDS<;|98E^O4>;xXsao(DkStAn7Y&?kW zF)Am3P%9l*mJD&8`w2@eiKGf~*{>v?!b``dh9^p3)GW9Aodqvxg zxA}rVT&c}=$BhsxneGsBz3K~G2%(PKHUiQ!?kXS|n%Nab-;^3RMMBqw_BlB@{b?Mn zeMGKSLH~G(#Zd+%@EzYcc;o2C;nVmmlTMRc8O4?%zTuO>3q|#Gr$<^VPS6IB42%=q zU}YpWxqSL{BbXlxEJ0&zl9#Rf3ULj%R=v8uyqw#s@wg#n{1aRl8T$6k3odtbN!Pkb zf_M}R+9A;)@gWhL%E1+A(>g(9Pc}3wRlKn#CeCOzyR`+dgMv5MpA}=4xw@TAPL^{} zPckT>k0(NEPde}dyYEH1u;mF0tJPnezs(ZrY1ZAHu4RN^g&-Ghr>)ZuKQf^;v=N(s z+=)2h#wxilrk+2`P+^u2knHKJd|N}e=0_MbBL`vKv{cuvvMKMH>G*quh1+9GZGhKS znhE{HnQMwOd+MqGFt80EP@?}p>bst%#C*IPXG>s!@0>Dp^Kb&ge~sD*i!4?B>!f%q z@Tc`mwsYH?euz+<%&wy_e#S^M?!~Hv-qxXFvbP?M&{ksl#MM?=n!md^@F!AmT)rr@ zLUR5l*kOkt{35(Yr8=n=7yUt&25wh`SFQ5lGx=vz(=h3W9=!Vz-!Sc`fzQ&H?+dyS zTEhlhcKbW-e158&AM-Q5NCyV5n)=o5F)`bQesmc5*(x&h>?TCcepg%zNe3?R(3QB3ioX;Dh7wlvgA zk7aDddH)&^|M_4R>}`JM3RjBkcK0|vXf)|!o(^Y+8~d?a2eaMy(D;2+wKoA|d6O}3 zF~;}bTjVkUv#~z_*`%yF+kf+#w-W0$)cE3rDD}pKj1f}*MuMGYYm zZT?I-J&=x|3}w7KG&@ItO)wHNws5!Sz_Y=ryE<`Vv6aWp(WLW09&7LTD+?i_hT^)~ z+qpiDiOeG^uh1FS9Xmh9l0TJH&p$|#{W%ls=GgDrHYcTp;?#huNxSAAo@sfd>?>}g zh@nBzd$=b{D#dKX%Zb%=(?R3q`8Tf->2m@%*g@JWKps#C?N^@UvuzRgtUxhIz4Zvp zVx7^tB)vHO-M+8-GcfeofQTG())V%F0-5hUju1%+thh-8Eni@$>QIj5vDnM|A=7wA z-B;6rwwhf7FAD;0Q`#~sWFp^krsSW|Z$C;Uo4P9HFQ^egbjLROKRZ>Y6JRHnGVl zLd9U^NXb#MPi5wsl3}!W3?X*0J?5#tk6S&(P4J}IZSIL;#)*I-&cxXe+N<(i`imX$ zazIo+KxybTZg!}6Gk4ZyK?<)H4GsOuHT33suj%gJFh(+O3#XOh-kyh)L)^$d#iZ-;9G%`7^Q5fH+$ z{_Su?BqD_W5S9POJ5pW=`E~x~EB{R~F#aJK|FNupLN`w^*xJpV>;KmH?;nKfKazhU t2E-I@R2snl75w*u-v2MbZ{idQL}n5-B~-M3^C~_0tX=)QkHlQ}{J$rdhZwj6eX`8V+6t;ezKaf3uJK)y`jSR5?8k{t zhKkuK8z0p5IhDOk2;@|BNPo~vL~W;4SH{AgFsi{Y0ssKkf8^h#{}}#r;;VV_1%>K( z`FMF7c=-{$&l|?$57{U~W<$rd|2XLXk$(p!?vV3LhI=#LhC&K9;Om@;82hC zilRW7!7Y{*3K7lP_p;fUC!7(eV`q$T>QH|u44n-nJKfAizGXhytiQ^F)D;X6a%VWr z*qDKrh@_!1;%958FcWHv3c))}n6jFV$IdX8#|Iq3Y7&bykW+N9M3Q1>VZ4aU@rc&- z{%}qOMSevsVBy|(YWB?bA@9*nJ2y?L4`$2)5LJrhHxmyCP110djn9nQSvE9>n3Ikp}b;51Q{UY7AfGd{|8D{Q?ToJ0DGfI14pRsD01;>);pIYZm((R}2wz zSk@rhZKGJ2V86bdE$yoV`AkfY5@cwXfAsJA__KtnZLU(oSR1AV5#69~du(vNzL>`2 z7q*Zp&q=rOtNrgEYqh|ZURvh5>0m;eQHZ|M8w=|ukD~ya_(+voJZq(GFn(Ep0rJMYuKT2FjiKxU}A@N6c&lDinL1P#E09)(zPmvdXp&&i&rSvF4a4D z;JvUR==mPK#tD<=K@p<7wVaI&63!Hok(v?H~B}Ay!Ddt9;UBCHs^EuX<`<`@t zM2(Oa*GJ}!&oQ4kTG{CxGZSLCY^`FgJuAw-g69`@*27eCMhr$@S{}T~lpAmUKqOvd z@mabhFYrO-X}dx^4H9FbI@MAA&W~=6wF^)Dr{h!)W>Iow%_UH`e zn8QSSNXK?6pWlenbJPi~G*a|DVu0euM5nl*k#9}}8>vqbsXtLy$rgHcE=qPTHY}cZ zm=3>G12d@ce^_Vj{H7?C8dUrB(}5Qrq%SrZVepZ9c)z<*mz&!)@-8nfEX`ta(x0|x zr2}vtR@7N2(z7$nk^^U$>M^+eRkRiEU&qef^$}%(iJYuUS~AF-NE~+WiqoExgEh`zm%sXLZ{8o_(WTZR=3v`(kh-WN!X zqGW+G;8$HVmbMrD5hg2V#pXfl{A^U*t^rzE0-VA9a$R~Pwh&CDuHtWuFakzwqtj3q z=C+v9L>CfqX3dB%)}oSWC`GO)&r}8H*jvS`$BRzLLQHPBM;FEzo3w)*9#yjIw@(t) z;?HQ>rd&@0X5e~vbdUIv)OtuGE5=`^m49X=yqsa7ZC8Y9SX~FyLyLtSG_03`5VmV@ zvrU0{Ug0YW@?7M3EcxU4$-O|p9H+?ZHmxgMCvuDnM(`d#>_qydFJNdmW6wSCwQVs+ ziipq~1?ak0sCM^VhZQNG3WQ&_(P=78fdvoKc&J@Gyk>F3Ao zr`~&4@wt_g!qQQ>wbVW#8mjE>sHf*SA~8#J19#Y3Y1%+Qv0p%VEK^TLmk>mgfaRf3K@FdHU0V?p;7K=vW zu5O%Jx<4pvMx-0&366G0dZN@IJNA5{M#JE(FFej)j7?N+J!-}AODlL^(xUcz5$6W? z6*teaXbpEgLRSR{ODUrwS+%3ZSk^nQ+%Kn_#b~94KOky@6D_~(qrZ7X;h(jBg-3 zEEHXTG)+ajS+-jI4NW)vxEqXVN6#IEX@=G``_!M1UmEAjuy|Hsn;!*y_m+>EK+UW0Vh3ok()lwwxEVjhH{&Z{`W;=s zYLe)(L>1N|(;_d>$0vC?pEw`D0@`nVIDUDl1vL)lq$5HSgG|2Wo67`~@mJC@cm3$t zVo1+~i42UHwp54LB+s%Q+}sPkkCYOXdagK6mP3oowu5wkevi*Gid(G#LWjYL6IBw* zQhv?hu-;Z`yKtvycN)!$yr1QZL1Y^8bT`VLVwWzqcEpx0EN|6SG|kxcX z*r_M*45v#)Le=g7b=gmy(&FI_BfW1AG8ixdi4gr}$*&4sQ4boxRBz=4<)ZK*SS&YE z@d4+9>76>}A<8N~PsFRcux0bR{MXM$j_t`VXgpYhEsSUe^M3kzf14?Hw_k6=x{X=7 zwSVqlbl12})n!F%)h~Wa_31^5T&DfmL!_?YVm^2gzjpas2igqCdOis5chs}%gn1w_ zA$>z#Va)Q3cDR(PVa4K8cYBqUe_y8!SJeTtx2m?X{{!M}cCtM2q|{_iTU|<|9rZTn zXaCA%E#rNFoTKaf2xdZ1q)y=e?+@{$%*M%tPZ%U#J{=>;DIj8e^Z1)k>4c035t8&B zr=Zdo=f>jORPv@BC1KeR{Hc;w)$s;+xB!#amz_}&A-1`Oqexy)S+_mf{tzL3FJIi@+*_*u*2gLaMQvDzoXy(Pec0ghh? z_1M3gdD%vpXeQOKg+&`#9cWjRfxN9H-)I>cB$24D|3ETmwan?j#~cSJPEJ5t`?YPE zciF7xF-x>*ceF&UpkAj{tFfMnbw3Z27YSm$v=&t{6bnJ}IvjEnd^Y$`o$zsJq#$df z0?ULXB4h|p?MeNu`|fT$P!h!Mgt5vHGmWmqWZI;A{W7mSiovb2Dd^jQpUS~yCy>n{ z+o^f_mDFrmM|mV%nFq*2Q)u*&CaO;QVWG;Yq{^l4{ihCUGc@I)(UD(s$`6ZSh2p^-=0& zaQ=hx)RO_#@;l>-S;^p(Aycp1{Js^N?bv)w4oy~lCO;vX) zx}A%o-^Ua|@fuR>?mk!n8MU_`xr3(jpKLI3_u$)A3;2C{)6E5Fc@0sar7OO|c~o*% z0&NnNk-rdbXkv4`$WH|ZZ%nrjh5R^+z)_l!z1;B~7y3j&D6A+oDkE9x2+b`_2@=jU z6||~}m7u7AyPMhttND!)p9DFz`&as-arf~YvJ-;0a#?KaKe)OHir#L`AIPMc;?4>| z%ulO#4)%55*-H=FuHJ3#+PjMp$1L2&;*y#|bg1%muz z_{Btez!fJ4?RZHzPl!saqj5`L6t_K%bL3to4PJJI-|6PAO(%m{2;RP}o$W0#n>8@$ zY^2+0oYy%^PSA^8W~++W=XK}q#e4Bsu<`oGT~rEuVo@oI6_)p^;Hn5y{@X;rvgmFi zRX=^_>o^X<_U6^@rLxt-a(<<_Huqux14OaZ4sK+NN6t zZ2jRSe4pdFBi4@pNyuTF=7lqCKP{SpIrqmVidQ(y1=tWu|Cri!!A#7L>U49FXV&Ih z^=OIotM&3RB3Lec`z+2u1H+zUQ$+xwGSZ$(R(9z)&#y%~;F%s$fk~i{cgmP}wzE%zjvk~pCT3WZMx?atU%A-P zfa~-sP??)h4wT8}JWFQ&v{PjzIwvS!SJ2FOKdlzK+cQDQrrHSd_0fJtc{f$~LVLNMYf!VcML$@XNL@B%N63dx7{CG;6Kj=m1vci3NOSFd=;{Lf~a=D5ydKi_t0%>Iau=d$D+ zcn8#&b*0<2(2JL=5~4I(?$?z+EiXL$9MQ_?8v@m%P!4loOQ+^Ab zDr;S*mwt=S75W30{n2mTh0eLV3XYF@=+{d2RqN&PA==S{#XF%9nx^9g{5W6nIFBc? zUW9_oQ;mR)adJ&*s(}Q^WpACOd7l^QNjfL5k}m(xjlIkckmEKW*iv5eoKS*d8<<|6 zuQ=GxPdFoBBrxFo*;hK;(;+j8r(wVv3}G4yD`C{3WaCSL1z6%lKT%8QgSQG@LS?D; zC{dc6RG_Jy1PZSZbH(U0Q{z$CqF%{IMt2fO?prjHyYG3TmjrXrEt&h7Uddo?c7#5M zi;rHw;m@&m236>i=$HP^2#lQE4~g1|=CAxdH6t8`TCH~LxOObDU-hfkd*{f4lKOH350YHhs|UDYj{N>XPp+{$y+lutMD@}4F_ z+;)T0+OAbpb2xq@#*=9$0BQ#5M>fPmrNVZjt)}b@2Zhn!HQ!uy<>KDl&Z7Z|Y_P=j zjFFb`pm3RD-g}}+SFHHw47CG+#Ynf)fNU<_!Ggow4X5i1@G*^eM>7+3oxW^PCqrh2 z4STWkP94fbNRPZqEcBO@RCKd~KYA6*GNHKz(BN<*@a`m3*#{S3xU~HK=KV{;+(*2> zNR}~*%{>YP05~c9MY1TUB!K^=(f-47B|cK{>;5Ix{z+Y7|2N6?KRWHt>+b37Z0+vB z^M7;v6Dc$QD~bUn@rINo@iQfm?r-{kHbV0+M-q%gaTHoIb!809zjpLLmk DLTMetaRunnerConf: runner_conf = DLTMetaRunnerConf( run_id=run_id, username=self.wsi._my_username, - dbfs_tmp_path=f"{self.args.__dict__['dbfs_path']}/{run_id}", int_tests_dir="file:./demo", dlt_meta_schema=f"dlt_meta_dataflowspecs_demo_{run_id}", bronze_schema=f"dlt_meta_bronze_demo_{run_id}", @@ -55,7 +53,6 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: runners_nb_path=f"/Users/{self.wsi._my_username}/dlt_meta_demo/{run_id}", source="cloudfiles", node_type_id=cloud_node_type_id_dict[self.args.__dict__['cloud_provider_name']], - dbr_version=self.args.__dict__['dbr_version'], cloudfiles_template="demo/conf/cloudfiles-onboarding.template", cloudfiles_A2_template="demo/conf/cloudfiles-onboarding_A2.template", onboarding_file_path="demo/conf/onboarding.json", @@ -69,27 +66,21 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: def launch_workflow(self, runner_conf: DLTMetaRunnerConf): created_job = self.create_cloudfiles_workflow_spec(runner_conf) runner_conf.job_id = created_job.job_id - print(f"Job created successfully. job_id={created_job.job_id}, started run...") - webbrowser.open(f"{self.ws.config.host}/jobs/{created_job.job_id}?o={self.ws.get_workspace_id()}") - print(f"Waiting for job to complete. job_id={created_job.job_id}") - run_by_id = self.ws.jobs.run_now(job_id=created_job.job_id).result() - print(f"Job run finished. run_id={run_by_id}") + self.ws.jobs.run_now(job_id=created_job.job_id) + url = f"{self.ws.config.host}/jobs/{created_job.job_id}?o={self.ws.get_workspace_id()}" + webbrowser.open(url) + print(f"Job created successfully. job_id={created_job.job_id}, url={url}") return created_job afam_args_map = { "--profile": "provide databricks cli profile name, if not provide databricks_host and token", "--uc_catalog_name": "provide databricks uc_catalog name, this is required to create volume, schema, table", - "--cloud_provider_name": "provide cloud provider name. Supported values are aws , azure , gcp", - "--dbr_version": "Provide databricks runtime spark version e.g 15.3.x-scala2.12", - "--dbfs_path": "Provide databricks workspace dbfs path where you want run integration tests \ - e.g --dbfs_path=dbfs:/tmp/DLT-META/" + "--cloud_provider_name": "provide cloud provider name. Supported values are aws , azure , gcp" } afam_mandatory_args = [ - "uc_catalog_name", "cloud_provider_name", - "dbr_version", "dbfs_path" -] + "uc_catalog_name", "cloud_provider_name"] def main(): diff --git a/demo/launch_dais_demo.py b/demo/launch_dais_demo.py index 4d5f673..2eda318 100644 --- a/demo/launch_dais_demo.py +++ b/demo/launch_dais_demo.py @@ -1,7 +1,8 @@ import uuid import webbrowser -from databricks.sdk.service import jobs +from databricks.sdk.service import jobs, compute from src.install import WorkspaceInstaller +from src.__about__ import __version__ from integration_tests.run_integration_tests import ( DLTMETARunner, DLTMetaRunnerConf, @@ -37,7 +38,6 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: runner_conf = DLTMetaRunnerConf( run_id=run_id, username=self._my_username(self.ws), - dbfs_tmp_path=f"{self.args.__dict__['dbfs_path']}/{run_id}", int_tests_dir="file:./demo", dlt_meta_schema=f"dlt_meta_dataflowspecs_demo_{run_id}", bronze_schema=f"dlt_meta_bronze_dais_demo_{run_id}", @@ -47,13 +47,13 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: dbr_version=self.args.__dict__['dbr_version'], cloudfiles_template="demo/conf/onboarding.template", env="prod", - source=self.args.__dict__['source'], + source="cloudFiles", runners_full_local_path='./demo/dbc/dais_dlt_meta_runners.dbc', onboarding_file_path='demo/conf/onboarding.json' ) if self.args.__dict__['uc_catalog_name']: runner_conf.uc_catalog_name = self.args.__dict__['uc_catalog_name'] - runner_conf.uc_volume_name = f"{self.args.__dict__['uc_catalog_name']}_volume_{run_id}" + runner_conf.uc_volume_name = f"{runner_conf.uc_catalog_name}_dais_demo_{run_id}" return runner_conf @@ -67,7 +67,6 @@ def run(self, runner_conf: DLTMetaRunnerConf): try: self.init_dltmeta_runner_conf(runner_conf) self.create_bronze_silver_dlt(runner_conf) - self.create_cluster(runner_conf) self.launch_workflow(runner_conf) except Exception as e: print(e) @@ -99,13 +98,23 @@ def create_daisdemo_workflow(self, runner_conf: DLTMetaRunnerConf): - created_job: created job object """ database, dlt_lib = self.init_db_dltlib(runner_conf) + dltmeta_environments = [ + jobs.JobEnvironment( + environment_key="dlt_meta_dais_demo_env", + spec=compute.Environment(client=f"dlt_meta_int_test_{__version__}", + # dependencies=[f"dlt_meta=={__version__}"], + dependencies=["dlt_meta==0.0.8"], + ) + ) + ] return self.ws.jobs.create( name=f"dltmeta_dais_demo-{runner_conf.run_id}", + environments=dltmeta_environments, tasks=[ jobs.Task( task_key="setup_dlt_meta_pipeline_spec", description="test", - existing_cluster_id=runner_conf.cluster_id, + environment_key="dlt_meta_dais_demo_env", timeout_seconds=0, python_wheel_task=jobs.PythonWheelTask( package_name="dlt_meta", @@ -113,23 +122,22 @@ def create_daisdemo_workflow(self, runner_conf: DLTMetaRunnerConf): named_parameters={ "onboard_layer": "bronze_silver", "database": database, - "onboarding_file_path": f"{runner_conf.dbfs_tmp_path}/demo/conf/onboarding.json", + "onboarding_file_path": f"{runner_conf.uc_volume_path}/demo/conf/onboarding.json", "silver_dataflowspec_table": "silver_dataflowspec_cdc", "silver_dataflowspec_path": ( - f"{runner_conf.dbfs_tmp_path}/demo/resources/data/dlt_spec/silver" + f"{runner_conf.uc_volume_path}/demo/resources/data/dlt_spec/silver" ), "bronze_dataflowspec_table": "bronze_dataflowspec_cdc", "import_author": "Ravi", "version": "v1", "bronze_dataflowspec_path": ( - f"{runner_conf.dbfs_tmp_path}/demo/resources/data/dlt_spec/bronze" + f"{runner_conf.uc_volume_path}/demo/resources/data/dlt_spec/bronze" ), "overwrite": "True", "env": runner_conf.env, "uc_enabled": "True" if runner_conf.uc_catalog_name else "False" } - ), - libraries=dlt_lib + ) ), jobs.Task( task_key="bronze_initial_run", @@ -149,11 +157,10 @@ def create_daisdemo_workflow(self, runner_conf: DLTMetaRunnerConf): task_key="load_incremental_data", description="Load Incremental Data", depends_on=[jobs.TaskDependency(task_key="silver_initial_run")], - existing_cluster_id=runner_conf.cluster_id, notebook_task=jobs.NotebookTask( notebook_path=f"{runner_conf.runners_nb_path}/runners/load_incremental_data", base_parameters={ - "dbfs_tmp_path": runner_conf.dbfs_tmp_path + "dbfs_tmp_path": runner_conf.uc_volume_path } ) ), @@ -177,16 +184,13 @@ def create_daisdemo_workflow(self, runner_conf: DLTMetaRunnerConf): dais_args_map = {"--profile": "provide databricks cli profile name, if not provide databricks_host and token", - "--source": "provide source. Supported values are cloudfiles, eventhub, kafka", "--uc_catalog_name": "provide databricks uc_catalog name, \ this is required to create volume, schema, table", "--cloud_provider_name": "provide cloud provider name. Supported values are aws , azure , gcp", - "--dbr_version": "Provide databricks runtime spark version e.g 15.3.x-scala2.12", - "--dbfs_path": "Provide databricks workspace dbfs path where you want run integration tests \ - e.g --dbfs_path=dbfs:/tmp/DLT-META/"} + "--dbr_version": "Provide databricks runtime spark version e.g 15.3.x-scala2.12" + } -dais_mandatory_args = ["source", "cloud_provider_name", - "dbr_version", "dbfs_path"] +dais_mandatory_args = ["uc_catalog_name", "cloud_provider_name", "dbr_version"] def main(): diff --git a/demo/launch_silver_fanout_demo.py b/demo/launch_silver_fanout_demo.py index e54476c..ffa433f 100644 --- a/demo/launch_silver_fanout_demo.py +++ b/demo/launch_silver_fanout_demo.py @@ -3,6 +3,7 @@ import webbrowser from databricks.sdk.service import jobs, compute from src.install import WorkspaceInstaller +from src.__about__ import __version__ from integration_tests.run_integration_tests import ( DLTMETARunner, DLTMetaRunnerConf, @@ -81,7 +82,7 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: env="demo" ) runner_conf.uc_catalog_name = self.args.__dict__['uc_catalog_name'] - runner_conf.uc_volume_name = f"{runner_conf.uc_catalog_name}/dlt_meta_fout_demo/{run_id}" + runner_conf.uc_volume_name = f"{runner_conf.uc_catalog_name}_dlt_meta_fout_demo_{run_id}" return runner_conf def launch_workflow(self, runner_conf: DLTMetaRunnerConf): @@ -105,8 +106,11 @@ def create_sfo_workflow_spec(self, runner_conf: DLTMetaRunnerConf): database, dlt_lib = self.init_db_dltlib(runner_conf) dltmeta_environments = [ jobs.JobEnvironment( - environment_key="dlt_meta_env", - spec=compute.Environment(client="1", dependencies=["dlt-meta==0.0.8"]) + environment_key="dl_meta_sfo_demo_env", + spec=compute.Environment(client=f"dlt_meta_int_test_{__version__}", + # dependencies=[f"dlt_meta=={__version__}"], + dependencies=["dlt_meta==0.0.8"] + ) ) ] return self.ws.jobs.create( @@ -117,7 +121,7 @@ def create_sfo_workflow_spec(self, runner_conf: DLTMetaRunnerConf): task_key="onboarding_job", description="Sets up metadata tables for DLT-META", # existing_cluster_id=runner_conf.cluster_id, - environment_key="dlt_meta_env", + environment_key="dl_meta_sfo_demo_env", timeout_seconds=0, python_wheel_task=jobs.PythonWheelTask( package_name="dlt_meta", diff --git a/demo/launch_techsummit_demo.py b/demo/launch_techsummit_demo.py index 768f391..0260fd4 100644 --- a/demo/launch_techsummit_demo.py +++ b/demo/launch_techsummit_demo.py @@ -24,11 +24,12 @@ import uuid import webbrowser -from databricks.sdk.service import jobs +from databricks.sdk.service import jobs, compute from databricks.sdk.service.catalog import VolumeType, SchemasAPI from databricks.sdk.service.workspace import ImportFormat from dataclasses import dataclass from src.install import WorkspaceInstaller +from src.__about__ import __version__ from integration_tests.run_integration_tests import ( DLTMETARunner, DLTMetaRunnerConf, @@ -81,14 +82,12 @@ def init_runner_conf(self) -> TechsummitRunnerConf: runner_conf = TechsummitRunnerConf( run_id=run_id, username=self._my_username(self.ws), - dbfs_tmp_path=f"{self.args.__dict__['dbfs_path']}/{run_id}", dlt_meta_schema=f"dlt_meta_dataflowspecs_demo_{run_id}", bronze_schema=f"dlt_meta_bronze_demo_{run_id}", silver_schema=f"dlt_meta_silver_demo_{run_id}", runners_full_local_path='./demo/dbc/tech_summit_dlt_meta_runners.dbc', runners_nb_path=f"/Users/{self._my_username(self.ws)}/dlt_meta_techsummit_demo/{run_id}", node_type_id=cloud_node_type_id_dict[self.args.__dict__['cloud_provider_name']], - dbr_version=self.args.__dict__['dbr_version'], env="prod", table_count=self.args.__dict__['table_count'] if self.args.__dict__['table_count'] else "100", table_column_count=(self.args.__dict__['table_column_count'] if self.args.__dict__['table_column_count'] @@ -96,7 +95,7 @@ def init_runner_conf(self) -> TechsummitRunnerConf: table_data_rows_count=(self.args.__dict__['table_data_rows_count'] if self.args.__dict__['table_data_rows_count'] else "10"), worker_nodes=self.args.__dict__['worker_nodes'] if self.args.__dict__['worker_nodes'] else "4", - source=self.args.__dict__['source'], + source="cloudFiles", onboarding_file_path='demo/conf/onboarding.json' ) if self.args.__dict__['uc_catalog_name']: @@ -131,6 +130,9 @@ def init_dltmeta_runner_conf(self, runner_conf: DLTMetaRunnerConf): SchemasAPI(self.ws.api_client).create(catalog_name=runner_conf.uc_catalog_name, name=runner_conf.silver_schema, comment="silver_schema") + runner_conf.uc_volume_path = (f"/Volumes/{runner_conf.volume_info.catalog_name}/" + f"{runner_conf.volume_info.schema_name}/{runner_conf.volume_info.name}/" + ) self.build_and_upload_package(runner_conf) # comment this line before merging to master @@ -144,7 +146,6 @@ def run(self, runner_conf: DLTMetaRunnerConf): try: self.init_dltmeta_runner_conf(runner_conf) self.create_bronze_silver_dlt(runner_conf) - self.create_cluster(runner_conf) self.launch_workflow(runner_conf) except Exception as e: print(e) @@ -177,22 +178,34 @@ def create_techsummit_demo_workflow(self, runner_conf: TechsummitRunnerConf): - created_job: The created job object. """ database, dlt_lib = self.init_db_dltlib(runner_conf) + environments = [ + jobs.JobEnvironment( + environment_key="dlt_meta_techsummit_demo_env", + spec=compute.Environment( + client=f"dlt_meta_int_test_{__version__}", + dependencies=["dlt_meta==0.0.8"] + ) + ) + ] return self.ws.jobs.create( name=f"dlt-meta-techsummit-demo-{runner_conf.run_id}", + environments=environments, tasks=[ jobs.Task( task_key="generate_data", description="Generate Test Data and Onboarding Files", - existing_cluster_id=runner_conf.cluster_id, timeout_seconds=0, notebook_task=jobs.NotebookTask( notebook_path=f"{runner_conf.runners_nb_path}/runners/data_generator", base_parameters={ - "base_input_path": runner_conf.dbfs_tmp_path, + "base_input_path": runner_conf.uc_volume_path, "table_column_count": runner_conf.table_column_count, "table_count": runner_conf.table_count, "table_data_rows_count": runner_conf.table_data_rows_count, + "uc_catalog_name": runner_conf.uc_catalog_name, "dlt_meta_schema": runner_conf.dlt_meta_schema, + "bronze_schema": runner_conf.bronze_schema, + "silver_schema": runner_conf.silver_schema, } ) @@ -201,7 +214,7 @@ def create_techsummit_demo_workflow(self, runner_conf: TechsummitRunnerConf): task_key="onboarding_job", description="Sets up metadata tables for DLT-META", depends_on=[jobs.TaskDependency(task_key="generate_data")], - existing_cluster_id=runner_conf.cluster_id, + environment_key="dlt_meta_techsummit_demo_env", timeout_seconds=0, python_wheel_task=jobs.PythonWheelTask( package_name="dlt_meta", @@ -210,26 +223,25 @@ def create_techsummit_demo_workflow(self, runner_conf: TechsummitRunnerConf): "onboard_layer": "bronze_silver", "database": database, "onboarding_file_path": - f"{runner_conf.dbfs_tmp_path}/conf/onboarding.json", + f"{runner_conf.uc_volume_path}/conf/onboarding.json", "silver_dataflowspec_table": "silver_dataflowspec_cdc", - "silver_dataflowspec_path": f"{runner_conf.dbfs_tmp_path}/data/dlt_spec/silver", + "silver_dataflowspec_path": f"{runner_conf.uc_volume_path}/data/dlt_spec/silver", "bronze_dataflowspec_table": "bronze_dataflowspec_cdc", "import_author": "Ravi", "version": "v1", - "bronze_dataflowspec_path": f"{runner_conf.dbfs_tmp_path}/data/dlt_spec/bronze", + "bronze_dataflowspec_path": f"{runner_conf.uc_volume_path}/data/dlt_spec/bronze", "overwrite": "True", "env": runner_conf.env, "uc_enabled": "True" if runner_conf.uc_catalog_name else "False" - }, - ), - libraries=dlt_lib + } + ) ), jobs.Task( task_key="bronze_dlt", depends_on=[jobs.TaskDependency(task_key="onboarding_job")], pipeline_task=jobs.PipelineTask( pipeline_id=runner_conf.bronze_pipeline_id - ), + ) ), jobs.Task( task_key="silver_dlt", @@ -243,19 +255,16 @@ def create_techsummit_demo_workflow(self, runner_conf: TechsummitRunnerConf): techsummit_args_map = {"--profile": "provide databricks cli profile name, if not provide databricks_host and token", - "--source": "provide --source=cloudfiles", "--uc_catalog_name": "provide databricks uc_catalog name, \ this is required to create volume, schema, table", "--cloud_provider_name": "cloud_provider_name", - "--dbr_version": "dbr_version", - "--dbfs_path": "dbfs_path", "--worker_nodes": "worker_nodes", "--table_count": "table_count", "--table_column_count": "table_column_count", "--table_data_rows_count": "table_data_rows_count" } -techsummit_mandatory_args = ["source", "cloud_provider_name", "dbr_version", "dbfs_path"] +techsummit_mandatory_args = ["uc_catalog_name", "cloud_provider_name"] def main(): diff --git a/docs/content/demo/Append_FLOW_CF.md b/docs/content/demo/Append_FLOW_CF.md index 7649c5b..875f0b0 100644 --- a/docs/content/demo/Append_FLOW_CF.md +++ b/docs/content/demo/Append_FLOW_CF.md @@ -12,7 +12,7 @@ This demo will perform following tasks: - Add file_name and file_path to target bronze table for autoloader source using [File metadata column](https://docs.databricks.com/en/ingestion/file-metadata-column.html) ## Append flow with autoloader -1. Launch Terminal/Command prompt +1. Launch Command Prompt 2. Install [Databricks CLI](https://docs.databricks.com/dev-tools/cli/index.html) - Once you install Databricks CLI, authenticate your current machine to a Databricks Workspace: diff --git a/docs/content/demo/Append_FLOW_EH.md b/docs/content/demo/Append_FLOW_EH.md index b5a4f6e..ada03b2 100644 --- a/docs/content/demo/Append_FLOW_EH.md +++ b/docs/content/demo/Append_FLOW_EH.md @@ -9,7 +9,7 @@ draft: false - Read from different eventhub topics and write to same target tables using [dlt.append_flow](https://docs.databricks.com/en/delta-live-tables/flows.html#append-flows) API ### Steps: -1. Launch Terminal/Command prompt +1. Launch Command Prompt 2. Install [Databricks CLI](https://docs.databricks.com/dev-tools/cli/index.html) - Once you install Databricks CLI, authenticate your current machine to a Databricks Workspace: @@ -62,7 +62,7 @@ draft: false - eventhub_port: Eventhub port 7. ```commandline - python3 demo/launch_af_eventhub_demo.py --cloud_provider_name=aws --dbr_version=15.3.x-scala2.12 --dbfs_path=dbfs:/tmp/DLT-META/demo/ --uc_catalog_name=ravi_dlt_meta_uc --eventhub_name=dltmeta_demo --eventhub_name_append_flow=dltmeta_demo_af --eventhub_secrets_scope_name=dltmeta_eventhub_creds --eventhub_namespace=dltmeta --eventhub_port=9093 --eventhub_producer_accesskey_name=RootManageSharedAccessKey --eventhub_consumer_accesskey_name=RootManageSharedAccessKey --eventhub_accesskey_secret_name=RootManageSharedAccessKey --uc_catalog_name=ravi_dlt_meta_uc + python demo/launch_af_eventhub_demo.py --cloud_provider_name=aws --uc_catalog_name=ravi_dlt_meta_uc --eventhub_name=dltmeta_demo --eventhub_name_append_flow=dltmeta_demo_af --eventhub_secrets_scope_name=dltmeta_eventhub_creds --eventhub_namespace=dltmeta --eventhub_port=9093 --eventhub_producer_accesskey_name=RootManageSharedAccessKey --eventhub_consumer_accesskey_name=RootManageSharedAccessKey --eventhub_accesskey_secret_name=RootManageSharedAccessKey --uc_catalog_name=ravi_dlt_meta_uc ``` ![af_eh_demo.png](/images/af_eh_demo.png) diff --git a/docs/content/demo/DAIS.md b/docs/content/demo/DAIS.md index f277ef3..4fd468d 100644 --- a/docs/content/demo/DAIS.md +++ b/docs/content/demo/DAIS.md @@ -14,7 +14,7 @@ This demo showcases DLT-META's capabilities of creating Bronze and Silver DLT pi - Runs Bronze and Silver DLT for incremental load for CDC events #### Steps to launch DAIS demo in your Databricks workspace: -1. Launch Terminal/Command promt +1. Launch Command Prompt 2. Install [Databricks CLI](https://docs.databricks.com/dev-tools/cli/index.html) - Once you install Databricks CLI, authenticate your current machine to a Databricks Workspace: @@ -39,25 +39,14 @@ This demo showcases DLT-META's capabilities of creating Bronze and Silver DLT pi export PYTHONPATH=$dlt_meta_home ``` -6. Run the command ```python demo/launch_dais_demo.py --username=<> --source=cloudfiles --uc_catalog_name=<> --cloud_provider_name=aws --dbr_version=13.3.x-scala2.12 --dbfs_path=dbfs:/dais-dlt-meta-demo-automated_new``` +6. ```commandline + python demo/launch_dais_demo.py --uc_catalog_name=<> --cloud_provider_name=<<>> + ``` + - uc_catalog_name : unit catalog name - cloud_provider_name : aws or azure or gcp - - db_version : Databricks Runtime Version - - dbfs_path : Path on your Databricks workspace where demo will be copied for launching DLT-META Pipelines - you can provide `--profile=databricks_profile name` in case you already have databricks cli otherwise command prompt will ask host and token. - - - 6a. Databricks Workspace URL: - - - Enter your workspace URL, with the format https://.cloud.databricks.com. To get your workspace URL, see Workspace instance names, URLs, and IDs. - - - - 6b. Token: - - In your Databricks workspace, click your Databricks username in the top bar, and then select User Settings from the drop down. - - - On the Access tokens tab, click Generate new token. - - - (Optional) Enter a comment that helps you to identify this token in the future, and change the token’s default lifetime of 90 days. To create a token with no lifetime (not recommended), leave the Lifetime (days) box empty (blank). - - - Click Generate. - - Copy the displayed token + ![dais_demo.png](/images/dais_demo.png) - - Paste to command prompt diff --git a/docs/content/demo/Silver_Fanout.md b/docs/content/demo/Silver_Fanout.md index 5671b85..8b57b9c 100644 --- a/docs/content/demo/Silver_Fanout.md +++ b/docs/content/demo/Silver_Fanout.md @@ -14,7 +14,7 @@ draft: false - Run onboarding for the silver tables, fanning out from the bronze cars tables to country-specific tables such as cars_usa, cars_uk, cars_germany, and cars_japan. ### Steps: -1. Launch Terminal/Command prompt +1. Launch Command Prompt 2. Install [Databricks CLI](https://docs.databricks.com/dev-tools/cli/index.html) - Once you install Databricks CLI, authenticate your current machine to a Databricks Workspace: @@ -37,10 +37,11 @@ draft: false ```commandline export PYTHONPATH=$dlt_meta_home -6. Run the command ```python demo/launch_silver_fanout_demo.py --source=cloudfiles --uc_catalog_name=<> --cloud_provider_name=aws --dbr_version=15.3.x-scala2.12 --dbfs_path=dbfs:/dais-dlt-meta-silver-fanout``` +6. ```commandline + python demo/launch_silver_fanout_demo.py --uc_catalog_name=<> --cloud_provider_name=aws + ``` + - uc_catalog_name : aws or azure - cloud_provider_name : aws or azure - - db_version : Databricks Runtime Version - - dbfs_path : Path on your Databricks workspace where demo will be copied for launching DLT-META Pipelines - you can provide `--profile=databricks_profile name` in case you already have databricks cli otherwise command prompt will ask host and token. - - 6a. Databricks Workspace URL: diff --git a/docs/content/demo/Techsummit.md b/docs/content/demo/Techsummit.md index 7a2e07e..d53b71d 100644 --- a/docs/content/demo/Techsummit.md +++ b/docs/content/demo/Techsummit.md @@ -8,7 +8,7 @@ draft: false ### Databricks Tech Summit FY2024 DEMO: This demo will launch auto generated tables(100s) inside single bronze and silver DLT pipeline using dlt-meta. -1. Launch Terminal/Command prompt +1. Launch Command Prompt 2. Install [Databricks CLI](https://docs.databricks.com/dev-tools/cli/index.html) - Once you install Databricks CLI, authenticate your current machine to a Databricks Workspace: @@ -33,24 +33,9 @@ This demo will launch auto generated tables(100s) inside single bronze and silve export PYTHONPATH=$dlt_meta_home ``` -6. Run the command ```python demo/launch_techsummit_demo.py --username=ravi.gawai@databricks.com --source=cloudfiles --cloud_provider_name=aws --dbr_version=13.3.x-scala2.12 --dbfs_path=dbfs:/techsummit-dlt-meta-demo-automated ``` - - cloud_provider_name : aws or azure or gcp - - db_version : Databricks Runtime Version - - dbfs_path : Path on your Databricks workspace where demo will be copied for launching DLT-META Pipelines +6. Run the command ```python demo/launch_techsummit_demo.py --uc_catalog_name=<> --cloud_provider_name=aws ``` + - uc_catalog_name : Unity Catalog name + - cloud_provider_name : aws or azure - you can provide `--profile=databricks_profile name` in case you already have databricks cli otherwise command prompt will ask host and token - - - 6a. Databricks Workspace URL: - - Enter your workspace URL, with the format https://.cloud.databricks.com. To get your workspace URL, see Workspace instance names, URLs, and IDs. - - - - 6b. Token: - - In your Databricks workspace, click your Databricks username in the top bar, and then select User Settings from the drop down. - - - On the Access tokens tab, click Generate new token. - - - (Optional) Enter a comment that helps you to identify this token in the future, and change the token’s default lifetime of 90 days. To create a token with no lifetime (not recommended), leave the Lifetime (days) box empty (blank). - - - Click Generate. - - - Copy the displayed token - - - Paste to command prompt \ No newline at end of file + ![tech_summit_demo.png](/images/tech_summit_demo.png) diff --git a/docs/static/images/dais_demo.png b/docs/static/images/dais_demo.png new file mode 100644 index 0000000000000000000000000000000000000000..d105e70e143ce7fedaec2a88fbf7fcba7fada111 GIT binary patch literal 233005 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

_+>aT(N|6~5w( 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

ojU$ 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 aLUfiZ Date: Thu, 26 Sep 2024 10:20:27 -0700 Subject: [PATCH 06/59] Added uc_volume_path for eventhub demo --- demo/README.md | 72 ++++++---------------- demo/conf/eventhub-onboarding.template | 8 +-- demo/launch_af_eventhub_demo.py | 16 +---- demo/launch_dais_demo.py | 3 + integration_tests/run_integration_tests.py | 42 ++++++++----- 5 files changed, 54 insertions(+), 87 deletions(-) diff --git a/demo/README.md b/demo/README.md index 9b5de36..ad0db13 100644 --- a/demo/README.md +++ b/demo/README.md @@ -15,7 +15,7 @@ This Demo launches Bronze and Silver DLT pipelines with following activities: - Runs Bronze and Silver DLT for incremental load for CDC events ### Steps: -1. Launch Terminal/Command prompt +1. Launch Command Prompt 2. Install [Databricks CLI](https://docs.databricks.com/dev-tools/cli/index.html) @@ -36,32 +36,19 @@ This Demo launches Bronze and Silver DLT pipelines with following activities: export PYTHONPATH=$dlt_meta_home ``` -6. Run the command ```python demo/launch_dais_demo.py --source=cloudfiles --uc_catalog_name=<> --cloud_provider_name=aws --dbr_version=15.3.x-scala2.12 --dbfs_path=dbfs:/dais-dlt-meta-demo-automated``` +6. ```commandline + python demo/launch_dais_demo.py --uc_catalog_name=<> --cloud_provider_name=aws + ``` + - uc_catalog_name : Unity catalog name - cloud_provider_name : aws or azure or gcp - - db_version : Databricks Runtime Version - - dbfs_path : Path on your Databricks workspace where demo will be copied for launching DLT-META Pipelines - you can provide `--profile=databricks_profile name` in case you already have databricks cli otherwise command prompt will ask host and token. - - - 6a. Databricks Workspace URL: - - - Enter your workspace URL, with the format https://.cloud.databricks.com. To get your workspace URL, see Workspace instance names, URLs, and IDs. - - - - 6b. Token: - - In your Databricks workspace, click your Databricks username in the top bar, and then select User Settings from the drop down. - - - On the Access tokens tab, click Generate new token. - - - (Optional) Enter a comment that helps you to identify this token in the future, and change the token’s default lifetime of 90 days. To create a token with no lifetime (not recommended), leave the Lifetime (days) box empty (blank). - - - Click Generate. - - - Copy the displayed token - - - Paste to command prompt + ![dais_demo.png](../docs/static/images/dais_demo.png) # Databricks Tech Summit FY2024 DEMO: This demo will launch auto generated tables(100s) inside single bronze and silver DLT pipeline using dlt-meta. -1. Launch Terminal/Command promt +1. Launch Command Prompt 2. Install [Databricks CLI](https://docs.databricks.com/dev-tools/cli/index.html) @@ -82,30 +69,14 @@ This demo will launch auto generated tables(100s) inside single bronze and silve export PYTHONPATH=$dlt_meta_home ``` -6. Run the command - ```commandline - python demo/launch_techsummit_demo.py --source=cloudfiles --cloud_provider_name=aws --dbr_version=15.3.x-scala2.12 --dbfs_path=dbfs:/techsummit-dlt-meta-demo-automated +6. ```commandline + python demo/launch_techsummit_demo.py --uc_catalog_name=<<>> --cloud_provider_name=<<>> ``` + - uc_catalog_name : Unity catalog name - cloud_provider_name : aws or azure - - db_version : Databricks Runtime Version - - dbfs_path : Path on your Databricks workspace where demo will be copied for launching DLT-META Pipelines - you can provide `--profile=databricks_profile name` in case you already have databricks cli otherwise command prompt will ask host and token - - - 6a. Databricks Workspace URL: - - Enter your workspace URL, with the format https://.cloud.databricks.com. To get your workspace URL, see Workspace instance names, URLs, and IDs. - - - - 6b. Token: - - In your Databricks workspace, click your Databricks username in the top bar, and then select User Settings from the drop down. - - - On the Access tokens tab, click Generate new token. - - - (Optional) Enter a comment that helps you to identify this token in the future, and change the token’s default lifetime of 90 days. To create a token with no lifetime (not recommended), leave the Lifetime (days) box empty (blank). - - - Click Generate. - - - Copy the displayed token - - - Paste to command prompt + ![tech_summit_demo.png](../docs/static/images/tech_summit_demo.png) # Append Flow Autoloader file metadata demo: @@ -114,7 +85,7 @@ This demo will perform following tasks: - Read from different delta tables and write to same silver table using append_flow API - Add file_name and file_path to target bronze table for autoloader source using [File metadata column](https://docs.databricks.com/en/ingestion/file-metadata-column.html) -1. Launch Terminal/Command prompt +1. Launch Command Prompt 2. Install [Databricks CLI](https://docs.databricks.com/dev-tools/cli/index.html) @@ -136,14 +107,11 @@ This demo will perform following tasks: ``` 6. ```commandline - python demo/launch_af_cloudfiles_demo.py --cloud_provider_name=aws --dbr_version=15.3.x-scala2.12 --dbfs_path=dbfs:/tmp/DLT-META/demo/ --uc_catalog_name=ravi_dlt_meta_uc + python demo/launch_af_cloudfiles_demo.py --uc_catalog_name=<<>> --cloud_provider_name=<<>> ``` - -- cloud_provider_name : aws or azure or gcp -- db_version : Databricks Runtime Version -- dbfs_path : Path on your Databricks workspace where demo will be copied for launching DLT-META Pipelines -- uc_catalog_name: Unity catalog name -- you can provide `--profile=databricks_profile name` in case you already have databricks cli otherwise command prompt will ask host and token + - uc_catalog_name : Unity Catalog name + - cloud_provider_name : aws or azure + - you can provide `--profile=databricks_profile name` in case you already have databricks cli otherwise command prompt will ask host and token ![af_am_demo.png](../docs/static/images/af_am_demo.png) @@ -151,7 +119,7 @@ This demo will perform following tasks: - Read from different eventhub topics and write to same target tables using append_flow API ### Steps: -1. Launch Terminal/Command prompt +1. Launch Command Prompt 2. Install [Databricks CLI](https://docs.databricks.com/dev-tools/cli/index.html) @@ -187,9 +155,7 @@ This demo will perform following tasks: - Following are the mandatory arguments for running EventHubs demo - cloud_provider_name: Cloud provider name e.g. aws or azure - - dbr_version: Databricks Runtime Version e.g. 15.3.x-scala2.12 - uc_catalog_name : unity catalog name e.g. ravi_dlt_meta_uc - - dbfs_path: Path on your Databricks workspace where demo will be copied for launching DLT-META Pipelines e.g. dbfs:/tmp/DLT-META/demo/ - eventhub_namespace: Eventhub namespace e.g. dltmeta - eventhub_name : Primary Eventhubname e.g. dltmeta_demo - eventhub_name_append_flow: Secondary eventhub name for appendflow feed e.g. dltmeta_demo_af @@ -199,7 +165,7 @@ This demo will perform following tasks: - eventhub_port: Eventhub port 7. ```commandline - python3 demo/launch_af_eventhub_demo.py --cloud_provider_name=aws --dbr_version=15.3.x-scala2.12 --dbfs_path=dbfs:/tmp/DLT-META/demo/ --uc_catalog_name=ravi_dlt_meta_uc --eventhub_name=dltmeta_demo --eventhub_name_append_flow=dltmeta_demo_af --eventhub_secrets_scope_name=dltmeta_eventhub_creds --eventhub_namespace=dltmeta --eventhub_port=9093 --eventhub_producer_accesskey_name=RootManageSharedAccessKey --eventhub_consumer_accesskey_name=RootManageSharedAccessKey --eventhub_accesskey_secret_name=RootManageSharedAccessKey --uc_catalog_name=ravi_dlt_meta_uc + python3 demo/launch_af_eventhub_demo.py --uc_catalog_name=<<>> --cloud_provider_name=<<>> --eventhub_name=dltmeta_demo --eventhub_name_append_flow=dltmeta_demo_af --eventhub_secrets_scope_name=dltmeta_eventhub_creds --eventhub_namespace=dltmeta --eventhub_port=9093 --eventhub_producer_accesskey_name=RootManageSharedAccessKey --eventhub_consumer_accesskey_name=RootManageSharedAccessKey --eventhub_accesskey_secret_name=RootManageSharedAccessKey ``` ![af_eh_demo.png](../docs/static/images/af_eh_demo.png) @@ -213,7 +179,7 @@ This demo will perform following tasks: - Run Silver DLT pipeline, fanning out from the bronze cars table to country-specific tables such as cars_usa, cars_uk, cars_germany, and cars_japan. ### Steps: -1. Launch Terminal/Command prompt +1. Launch Command Prompt 2. Install [Databricks CLI](https://docs.databricks.com/dev-tools/cli/index.html) diff --git a/demo/conf/eventhub-onboarding.template b/demo/conf/eventhub-onboarding.template index da57cc0..56c88d0 100644 --- a/demo/conf/eventhub-onboarding.template +++ b/demo/conf/eventhub-onboarding.template @@ -5,7 +5,7 @@ "source_system": "Sensor Device", "source_format": "eventhub", "source_details": { - "source_schema_path": "{dbfs_path}/demo/resources/data/afam/ddl/eventhub_iot_schema.ddl", + "source_schema_path": "{uc_volume_path}/demo/resources/data/afam/ddl/eventhub_iot_schema.ddl", "eventhub.accessKeyName": "{eventhub_accesskey_name}", "eventhub.name": "{eventhub_name}", "eventhub.accessKeySecretName": "{eventhub_accesskey_secret_name}", @@ -25,17 +25,17 @@ "bronze_database_demo": "{uc_catalog_name}.{bronze_schema}", "bronze_table": "bronze_{run_id}_iot", "bronze_partition_columns": "date", - "bronze_data_quality_expectations_json_demo": "{dbfs_path}/demo/conf/dqe/iot/bronze_data_quality_expectations.json", + "bronze_data_quality_expectations_json_demo": "{uc_volume_path}/demo/conf/dqe/iot/bronze_data_quality_expectations.json", "bronze_database_quarantine_demo": "{uc_catalog_name}.{bronze_schema}", "bronze_quarantine_table": "bronze_{run_id}_iot_quarantine", - "bronze_quarantine_table_path_demo": "{dbfs_path}/data/bronze/iot_quarantine", + "bronze_quarantine_table_path_demo": "{uc_volume_path}/data/bronze/iot_quarantine", "bronze_append_flows": [ { "name": "io_bronze_eventhub_append_flow", "create_streaming_table": false, "source_format": "eventhub", "source_details": { - "source_schema_path": "{dbfs_path}/demo/resources/data/afam/ddl/eventhub_iot_schema.ddl", + "source_schema_path": "{uc_volume_path}/demo/resources/data/afam/ddl/eventhub_iot_schema.ddl", "eventhub.accessKeyName": "{eventhub_accesskey_name}", "eventhub.name": "{eventhub_name_append_flow}", "eventhub.accessKeySecretName": "{eventhub_accesskey_secret_name}", diff --git a/demo/launch_af_eventhub_demo.py b/demo/launch_af_eventhub_demo.py index 7ac5f8a..c3d01dd 100644 --- a/demo/launch_af_eventhub_demo.py +++ b/demo/launch_af_eventhub_demo.py @@ -29,7 +29,6 @@ def run(self, runner_conf: DLTMetaRunnerConf): try: self.init_dltmeta_runner_conf(runner_conf) self.create_bronze_silver_dlt(runner_conf) - self.create_cluster(runner_conf) self.launch_workflow(runner_conf) except Exception as e: print(e) @@ -47,14 +46,11 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: runner_conf = DLTMetaRunnerConf( run_id=run_id, username=self.wsi._my_username, - dbfs_tmp_path=f"{self.args.__dict__['dbfs_path']}/{run_id}", int_tests_dir="file:./demo", dlt_meta_schema=f"dlt_meta_dataflowspecs_demo_{run_id}", bronze_schema=f"dlt_meta_bronze_demo_{run_id}", runners_nb_path=f"/Users/{self.wsi._my_username}/dlt_meta_demo/{run_id}", source="eventhub", - node_type_id=cloud_node_type_id_dict[self.args.__dict__['cloud_provider_name']], - dbr_version=self.args.__dict__['dbr_version'], eventhub_template="demo/conf/eventhub-onboarding.template", onboarding_file_path="demo/conf/onboarding.json", env="demo" @@ -65,12 +61,7 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: def launch_workflow(self, runner_conf: DLTMetaRunnerConf): created_job = self.create_eventhub_workflow_spec(runner_conf) - runner_conf.job_id = created_job.job_id - print(f"Job created successfully. job_id={created_job.job_id}, started run...") - webbrowser.open(f"{self.ws.config.host}/jobs/{created_job.job_id}?o={self.ws.get_workspace_id()}") - print(f"Waiting for job to complete. job_id={created_job.job_id}") - run_by_id = self.ws.jobs.run_now(job_id=created_job.job_id).result() - print(f"Job run finished. run_id={run_by_id}") + self.open return created_job @@ -78,9 +69,6 @@ def launch_workflow(self, runner_conf: DLTMetaRunnerConf): "--profile": "provide databricks cli profile name, if not provide databricks_host and token", "--uc_catalog_name": "provide databricks uc_catalog name, this is required to create volume, schema, table", "--cloud_provider_name": "provide cloud provider name. Supported values are aws , azure , gcp", - "--dbr_version": "Provide databricks runtime spark version e.g 15.3.x-scala2.12", - "--dbfs_path": "Provide databricks workspace dbfs path where you want run integration tests \ - e.g --dbfs_path=dbfs:/tmp/DLT-META/", "--eventhub_name": "Provide eventhub_name e.g --eventhub_name=iot", "--eventhub_name_append_flow": "Provide eventhub_name_append_flow e.g --eventhub_name_append_flow=iot_af", "--eventhub_producer_accesskey_name": "Provide access key that has write permission on the eventhub", @@ -93,7 +81,7 @@ def launch_workflow(self, runner_conf: DLTMetaRunnerConf): "--eventhub_port": "Provide eventhub_port e.g --eventhub_port=9093", } -afeh_mandatory_args = ["uc_catalog_name", "cloud_provider_name", "dbr_version", "dbfs_path", "eventhub_name", +afeh_mandatory_args = ["uc_catalog_name", "cloud_provider_name", "eventhub_name", "eventhub_name_append_flow", "eventhub_producer_accesskey_name", "eventhub_consumer_accesskey_name", "eventhub_secrets_scope_name", "eventhub_namespace", "eventhub_port"] diff --git a/demo/launch_dais_demo.py b/demo/launch_dais_demo.py index 2eda318..8e1f90a 100644 --- a/demo/launch_dais_demo.py +++ b/demo/launch_dais_demo.py @@ -81,6 +81,9 @@ def launch_workflow(self, runner_conf: DLTMetaRunnerConf): - runner_conf: DLTMetaRunnerConf object """ created_job = self.create_daisdemo_workflow(runner_conf) + self.launch_wf_browser(runner_conf, created_job) + + def launch_wf_browser(self, runner_conf, created_job): runner_conf.job_id = created_job.job_id url = f"{self.ws.config.host}/jobs/{created_job.job_id}?o={self.ws.get_workspace_id()}" self.ws.jobs.run_now(job_id=created_job.job_id) diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index 5450be8..f1395cb 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -4,6 +4,7 @@ import uuid import argparse import os +import webbrowser from dataclasses import dataclass from databricks.sdk import WorkspaceClient from databricks.sdk.service.pipelines import PipelineLibrary, NotebookLibrary @@ -437,14 +438,22 @@ def init_db_dltlib(self, runner_conf: DLTMetaRunnerConf): def create_eventhub_workflow_spec(self, runner_conf: DLTMetaRunnerConf): """Create Job specification.""" database, dlt_lib = self.init_db_dltlib(runner_conf) - dbfs_path = runner_conf.dbfs_tmp_path.replace("dbfs:/", "/dbfs/") + dltmeta_environments = [ + jobs.JobEnvironment( + environment_key="dl_meta_int_env", + spec=compute.Environment(client=f"dlt_meta_int_test_{__version__}", + dependencies=[runner_conf.remote_whl_path] + ) + ) + ] return self.ws.jobs.create( name=f"dlt-meta-{runner_conf.run_id}", + environments=dltmeta_environments, tasks=[ jobs.Task( task_key="setup_dlt_meta_pipeline_spec", - description="test", - existing_cluster_id=runner_conf.cluster_id, + description="setup_dlt_meta_pipeline_spec", + environment_key="dl_meta_int_env", timeout_seconds=0, python_wheel_task=jobs.PythonWheelTask( package_name="dlt_meta", @@ -453,19 +462,16 @@ def create_eventhub_workflow_spec(self, runner_conf: DLTMetaRunnerConf): "onboard_layer": "bronze", "database": database, "onboarding_file_path": - f"{runner_conf.dbfs_tmp_path}/{self.base_dir}/conf/onboarding.json", + f"{runner_conf.uc_volume_path}/{self.base_dir}/conf/onboarding.json", "silver_dataflowspec_table": "silver_dataflowspec_cdc", - "silver_dataflowspec_path": f"{runner_conf.dbfs_tmp_path}/dltmeta/data/dlt_spec/silver", "bronze_dataflowspec_table": "bronze_dataflowspec_cdc", "import_author": "Ravi", "version": "v1", - "bronze_dataflowspec_path": f"{runner_conf.dbfs_tmp_path}/dltmeta/data/dlt_spec/bronze", "overwrite": "True", "env": runner_conf.env, - "uc_enabled": "True" if runner_conf.uc_catalog_name else "False" - }, - ), - libraries=dlt_lib + "uc_enabled": "True" + } + ) ), jobs.Task( task_key="publish_events", @@ -481,9 +487,9 @@ def create_eventhub_workflow_spec(self, runner_conf: DLTMetaRunnerConf): "eventhub_secrets_scope_name": self.args.__getattribute__("eventhub_secrets_scope_name"), "eventhub_accesskey_name": self.args.__getattribute__("eventhub_producer_accesskey_name"), "eventhub_input_data": - f"/{dbfs_path}/{self.base_dir}/resources/data/iot/iot.json", + f"/{runner_conf.uc_volume_path}/{self.base_dir}/resources/data/iot/iot.json", "eventhub_append_flow_input_data": - f"/{dbfs_path}/{self.base_dir}/resources/data/iot_eventhub_af/iot.json", + f"/{runner_conf.uc_volume_path}/{self.base_dir}/resources/data/iot_eventhub_af/iot.json", } ) ), @@ -498,7 +504,6 @@ def create_eventhub_workflow_spec(self, runner_conf: DLTMetaRunnerConf): task_key="validate_results", description="test", depends_on=[jobs.TaskDependency(task_key="bronze_dlt_pipeline")], - existing_cluster_id=runner_conf.cluster_id, notebook_task=jobs.NotebookTask( notebook_path=f"{runner_conf.runners_nb_path}/runners/validate", base_parameters={ @@ -509,7 +514,7 @@ def create_eventhub_workflow_spec(self, runner_conf: DLTMetaRunnerConf): "output_file_path": f"/Workspace{runner_conf.test_output_file_path}" } ) - ), + ) ] ) @@ -956,7 +961,6 @@ def create_cluster(self, runner_conf: DLTMetaRunnerConf): print(f"Cluster creation finished. cluster_id={clstr.cluster_id}") print(f"host: {self.ws.config.host}, workspace_id: {self.ws.get_workspace_id()}") runner_conf.cluster_id = clstr.cluster_id - import webbrowser webbrowser.open(f"{self.ws.config.host}/compute/clusters/{clstr.cluster_id}?o={self.ws.get_workspace_id()}") def run(self, runner_conf: DLTMetaRunnerConf): @@ -1008,13 +1012,19 @@ def launch_workflow(self, runner_conf: DLTMetaRunnerConf): created_job = self.create_kafka_workflow_spec(runner_conf) runner_conf.job_id = created_job.job_id print(f"Job created successfully. job_id={created_job.job_id}, started run...") - import webbrowser webbrowser.open(f"{self.ws.config.host}/jobs/{created_job.job_id}?o={self.ws.get_workspace_id()}") print(f"Waiting for job to complete. job_id={created_job.job_id}") run_by_id = self.ws.jobs.run_now(job_id=created_job.job_id).result() print(f"Job run finished. run_id={run_by_id}") return created_job + def launch_wf_browser(self, runner_conf, created_job): + runner_conf.job_id = created_job.job_id + url = f"{self.ws.config.host}/jobs/{created_job.job_id}?o={self.ws.get_workspace_id()}" + self.ws.jobs.run_now(job_id=created_job.job_id) + webbrowser.open(url) + print(f"Job created successfully. job_id={created_job.job_id}, url={url}") + def clean_up(self, runner_conf: DLTMetaRunnerConf): print("Cleaning up...") if runner_conf.job_id: From 8529333889b3878e97febba42c65512de9adedb2 Mon Sep 17 00:00:00 2001 From: ravi-databricks Date: Thu, 26 Sep 2024 10:50:59 -0700 Subject: [PATCH 07/59] 1.Fixed uc volume path for eventhub 2.Added open_job_url function --- demo/launch_af_cloudfiles_demo.py | 8 +------- demo/launch_af_eventhub_demo.py | 4 +--- demo/launch_dais_demo.py | 10 +--------- demo/launch_silver_fanout_demo.py | 7 +------ demo/launch_techsummit_demo.py | 8 +------- integration_tests/run_integration_tests.py | 11 +++++++++-- 6 files changed, 14 insertions(+), 34 deletions(-) diff --git a/demo/launch_af_cloudfiles_demo.py b/demo/launch_af_cloudfiles_demo.py index f2b1e0a..b6cfc59 100644 --- a/demo/launch_af_cloudfiles_demo.py +++ b/demo/launch_af_cloudfiles_demo.py @@ -1,6 +1,5 @@ import uuid -import webbrowser from src.install import WorkspaceInstaller from integration_tests.run_integration_tests import ( DLTMETARunner, @@ -65,12 +64,7 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: def launch_workflow(self, runner_conf: DLTMetaRunnerConf): created_job = self.create_cloudfiles_workflow_spec(runner_conf) - runner_conf.job_id = created_job.job_id - self.ws.jobs.run_now(job_id=created_job.job_id) - url = f"{self.ws.config.host}/jobs/{created_job.job_id}?o={self.ws.get_workspace_id()}" - webbrowser.open(url) - print(f"Job created successfully. job_id={created_job.job_id}, url={url}") - return created_job + self.open_job_url(runner_conf, created_job) afam_args_map = { diff --git a/demo/launch_af_eventhub_demo.py b/demo/launch_af_eventhub_demo.py index c3d01dd..16c2009 100644 --- a/demo/launch_af_eventhub_demo.py +++ b/demo/launch_af_eventhub_demo.py @@ -1,11 +1,9 @@ import uuid -import webbrowser from src.install import WorkspaceInstaller from integration_tests.run_integration_tests import ( DLTMETARunner, DLTMetaRunnerConf, - cloud_node_type_id_dict, get_workspace_api_client, process_arguments ) @@ -61,7 +59,7 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: def launch_workflow(self, runner_conf: DLTMetaRunnerConf): created_job = self.create_eventhub_workflow_spec(runner_conf) - self.open + self.open_job_url(runner_conf, created_job) return created_job diff --git a/demo/launch_dais_demo.py b/demo/launch_dais_demo.py index 8e1f90a..c27e291 100644 --- a/demo/launch_dais_demo.py +++ b/demo/launch_dais_demo.py @@ -1,5 +1,4 @@ import uuid -import webbrowser from databricks.sdk.service import jobs, compute from src.install import WorkspaceInstaller from src.__about__ import __version__ @@ -81,14 +80,7 @@ def launch_workflow(self, runner_conf: DLTMetaRunnerConf): - runner_conf: DLTMetaRunnerConf object """ created_job = self.create_daisdemo_workflow(runner_conf) - self.launch_wf_browser(runner_conf, created_job) - - def launch_wf_browser(self, runner_conf, created_job): - runner_conf.job_id = created_job.job_id - url = f"{self.ws.config.host}/jobs/{created_job.job_id}?o={self.ws.get_workspace_id()}" - self.ws.jobs.run_now(job_id=created_job.job_id) - webbrowser.open(url) - print(f"Job created successfully. job_id={created_job.job_id}, url={url}") + self.open_job_url(runner_conf, created_job) def create_daisdemo_workflow(self, runner_conf: DLTMetaRunnerConf): """ diff --git a/demo/launch_silver_fanout_demo.py b/demo/launch_silver_fanout_demo.py index ffa433f..2239e2d 100644 --- a/demo/launch_silver_fanout_demo.py +++ b/demo/launch_silver_fanout_demo.py @@ -1,6 +1,5 @@ import uuid -import webbrowser from databricks.sdk.service import jobs, compute from src.install import WorkspaceInstaller from src.__about__ import __version__ @@ -87,11 +86,7 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: def launch_workflow(self, runner_conf: DLTMetaRunnerConf): created_job = self.create_sfo_workflow_spec(runner_conf) - runner_conf.job_id = created_job.job_id - url = f"{self.ws.config.host}/jobs/{created_job.job_id}?o={self.ws.get_workspace_id()}" - self.ws.jobs.run_now(job_id=created_job.job_id) - webbrowser.open(url) - print(f"Demo launched successfully. job_id={created_job.job_id}, url={url}") + self.open_job_url(runner_conf, created_job) def create_sfo_workflow_spec(self, runner_conf: DLTMetaRunnerConf): """ diff --git a/demo/launch_techsummit_demo.py b/demo/launch_techsummit_demo.py index 0260fd4..5929224 100644 --- a/demo/launch_techsummit_demo.py +++ b/demo/launch_techsummit_demo.py @@ -23,7 +23,6 @@ """ import uuid -import webbrowser from databricks.sdk.service import jobs, compute from databricks.sdk.service.catalog import VolumeType, SchemasAPI from databricks.sdk.service.workspace import ImportFormat @@ -160,12 +159,7 @@ def launch_workflow(self, runner_conf: DLTMetaRunnerConf): - runner_conf: The DLTMetaRunnerConf object containing the runner configuration parameters. """ created_job = self.create_techsummit_demo_workflow(runner_conf) - print(created_job) - runner_conf.job_id = created_job.job_id - self.ws.jobs.run_now(job_id=created_job.job_id) - url = f"{self.ws.config.host}/jobs/{created_job.job_id}?o={self.ws.get_workspace_id()}" - webbrowser.open(url) - print(f"Job created successfully. job_id={created_job.job_id}, url={url}") + self.open_job_url(runner_conf, created_job) def create_techsummit_demo_workflow(self, runner_conf: TechsummitRunnerConf): """ diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index f1395cb..eb192de 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -653,6 +653,8 @@ def create_eventhub_onboarding(self, runner_conf: DLTMetaRunnerConf): for source_key, source_value in value.items(): if 'dbfs_path' in source_value: data_flow[key][source_key] = source_value.format(dbfs_path=runner_conf.dbfs_tmp_path) + if 'uc_volume_path' in source_value: + data_flow[key][source_key] = source_value.format(uc_volume_path=runner_conf.uc_volume_path) if 'eventhub_name' in source_value: data_flow[key][source_key] = source_value.format(eventhub_name=eventhub_name) if 'eventhub_accesskey_name' in source_value: @@ -677,6 +679,9 @@ def create_eventhub_onboarding(self, runner_conf: DLTMetaRunnerConf): if 'dbfs_path' in source_value: data_flow[key][counter][flow_key][source_key] = source_value.format( dbfs_path=runner_conf.dbfs_tmp_path) + if 'uc_volume_path' in source_value: + data_flow[key][counter][flow_key][source_key] = source_value.format( + uc_volume_path=runner_conf.uc_volume_path) if 'eventhub_name_append_flow' in source_value: data_flow[key][counter][flow_key][source_key] = source_value.format( eventhub_name_append_flow=eventhub_name_append_flow) @@ -698,6 +703,8 @@ def create_eventhub_onboarding(self, runner_conf: DLTMetaRunnerConf): counter += 1 if 'dbfs_path' in value: data_flow[key] = value.format(dbfs_path=runner_conf.dbfs_tmp_path) + elif 'uc_volume_path' in value: + data_flow[key] = value.format(uc_volume_path=runner_conf.uc_volume_path) elif 'run_id' in value: data_flow[key] = value.format(run_id=runner_conf.run_id) elif 'uc_catalog_name' in value and 'bronze_schema' in value: @@ -905,7 +912,7 @@ def init_dltmeta_runner_conf(self, runner_conf: DLTMetaRunnerConf): self.generate_onboarding_file(runner_conf) print("int_tests_dir: ", runner_conf.int_tests_dir) self.copy(runner_conf) - print(f"uploading to {runner_conf.dbfs_tmp_path}/{self.base_dir}/ complete!!!") + print(f"uploading to {runner_conf.runners_nb_path}/{self.base_dir}/ complete!!!") fp = open(runner_conf.runners_full_local_path, "rb") print(f"uploading to {runner_conf.runners_nb_path} started") self.ws.workspace.mkdirs(runner_conf.runners_nb_path) @@ -1018,7 +1025,7 @@ def launch_workflow(self, runner_conf: DLTMetaRunnerConf): print(f"Job run finished. run_id={run_by_id}") return created_job - def launch_wf_browser(self, runner_conf, created_job): + def open_job_url(self, runner_conf, created_job): runner_conf.job_id = created_job.job_id url = f"{self.ws.config.host}/jobs/{created_job.job_id}?o={self.ws.get_workspace_id()}" self.ws.jobs.run_now(job_id=created_job.job_id) From d1aa9615e4323111b0d816e2b3d7797afba1dd98 Mon Sep 17 00:00:00 2001 From: ravi-databricks Date: Thu, 26 Sep 2024 13:46:41 -0700 Subject: [PATCH 08/59] Fixed int tests with uc volume paths and use serverless --- integration_tests/README.md | 95 +++++++++++++++++++ .../conf/cloudfiles-onboarding.template | 40 ++++---- .../conf/cloudfiles-onboarding_A2.template | 10 +- .../conf/eventhub-onboarding.template | 8 +- .../conf/kafka-onboarding.template | 8 +- integration_tests/run_integration_tests.py | 26 ++--- 6 files changed, 142 insertions(+), 45 deletions(-) create mode 100644 integration_tests/README.md diff --git a/integration_tests/README.md b/integration_tests/README.md new file mode 100644 index 0000000..73a7d09 --- /dev/null +++ b/integration_tests/README.md @@ -0,0 +1,95 @@ +#### Run Integration Tests +1. Install [Databricks CLI](https://docs.databricks.com/dev-tools/cli/index.html) + - Once you install Databricks CLI, authenticate your current machine to a Databricks Workspace: + + ```commandline + databricks auth login --host WORKSPACE_HOST + ``` + +2. ```commandline + git clone https://github.com/databrickslabs/dlt-meta.git + ``` + +3. ```commandline + cd dlt-meta + ``` + +4. ```commandline + python -m venv .venv + ``` + +5. ```commandline + source .venv/bin/activate + ``` + +6. ```commandline + pip install databricks-sdk + ``` + +7. ```commandline + dlt_meta_home=$(pwd) + ``` + +8. ```commandline + export PYTHONPATH=$dlt_meta_home + ``` + +9. Run integration test against cloudfile or eventhub or kafka using below options: If databricks profile configured using CLI then pass ```--profile ``` to below command otherwise provide workspace url and token in command line + - 9a. Run the command for cloudfiles + ```commandline + python integration-tests/run_integration_tests.py --uc_catalog_name= --cloud_provider_name=<<>> --source=cloudfiles + ``` + + - 9b. Run the command for eventhub + ```commandline + python integration-tests/run_integration_tests.py --uc_catalog_name= --cloud_provider_name=<<>> --source=eventhub --eventhub_name=iot --eventhub_secrets_scope_name=eventhubs_creds --eventhub_namespace=int_test-standard --eventhub_port=9093 --eventhub_producer_accesskey_name=producer --eventhub_consumer_accesskey_name=consumer + ``` + + - - For eventhub integration tests, the following are the prerequisites: + 1. Needs eventhub instance running + 2. Use Databricks CLI, Create databricks secrets scope for eventhub keys (```databricks secrets create-scope eventhubs_creds```) + 3. Use Databricks CLI, Create databricks secrets to store producer and consumer keys using the scope created in step + + - - Following are the mandatory arguments for running EventHubs integration test + 1. Provide your eventhub topic : --eventhub_name + 2. Provide eventhub namespace : --eventhub_namespace + 3. Provide eventhub port : --eventhub_port + 4. Provide databricks secret scope name : --eventhub_secrets_scope_name + 5. Provide eventhub producer access key name : --eventhub_producer_accesskey_name + 6. Provide eventhub access key name : --eventhub_consumer_accesskey_name + + + - 9c. Run the command for kafka + ```commandline + python3 integration-tests/run_integration_tests.py --uc_catalog_name= --cloud_provider_name=<<>> --source=kafka --kafka_topic_name=dlt-meta-integration-test --kafka_broker=host:9092 + ``` + + - - For kafka integration tests, the following are the prerequisites: + 1. Needs kafka instance running + + - - Following are the mandatory arguments for running EventHubs integration test + 1. Provide your kafka topic name : --kafka_topic_name + 2. Provide kafka_broker : --kafka_broker + +10. Once finished integration output file will be copied locally to +```integration-test-output_.txt``` + +11. Output of a successful run should have the following in the file +``` +,0 +0,Completed Bronze DLT Pipeline. +1,Completed Silver DLT Pipeline. +2,Validating DLT Bronze and Silver Table Counts... +3,Validating Counts for Table bronze_7d1d3ccc9e144a85b07c23110ea50133.transactions. +4,Expected: 10002 Actual: 10002. Passed! +5,Validating Counts for Table bronze_7d1d3ccc9e144a85b07c23110ea50133.transactions_quarantine. +6,Expected: 7 Actual: 7. Passed! +7,Validating Counts for Table bronze_7d1d3ccc9e144a85b07c23110ea50133.customers. +8,Expected: 98928 Actual: 98928. Passed! +9,Validating Counts for Table bronze_7d1d3ccc9e144a85b07c23110ea50133.customers_quarantine. +10,Expected: 1077 Actual: 1077. Passed! +11,Validating Counts for Table silver_7d1d3ccc9e144a85b07c23110ea50133.transactions. +12,Expected: 8759 Actual: 8759. Passed! +13,Validating Counts for Table silver_7d1d3ccc9e144a85b07c23110ea50133.customers. +14,Expected: 87256 Actual: 87256. Passed! +``` \ No newline at end of file diff --git a/integration_tests/conf/cloudfiles-onboarding.template b/integration_tests/conf/cloudfiles-onboarding.template index 170e093..9d0dbf4 100644 --- a/integration_tests/conf/cloudfiles-onboarding.template +++ b/integration_tests/conf/cloudfiles-onboarding.template @@ -7,7 +7,7 @@ "source_details": { "source_database": "APP", "source_table": "CUSTOMERS", - "source_path_it": "{dbfs_path}/integration_tests/resources/data/customers", + "source_path_it": "{uc_volume_path}/integration_tests/resources/data/customers", "source_metadata": { "include_autoloader_metadata_column": "True", "autoloader_metadata_col_name": "source_metadata", @@ -16,7 +16,7 @@ "input_file_path": "_metadata.file_path" } }, - "source_schema_path": "{dbfs_path}/integration_tests/resources/customers.ddl" + "source_schema_path": "{uc_volume_path}/integration_tests/resources/customers.ddl" }, "bronze_database_it": "{uc_catalog_name}.{bronze_schema}", "bronze_table": "customers", @@ -25,15 +25,15 @@ "cloudFiles.inferColumnTypes": "true", "cloudFiles.rescuedDataColumn": "_rescued_data" }, - "bronze_table_path_it": "{dbfs_path}/data/bronze/customers", + "bronze_table_path_it": "{uc_volume_path}/data/bronze/customers", "bronze_table_properties": { "pipelines.autoOptimize.managed": "true", "pipelines.autoOptimize.zOrderCols": "id, email" }, - "bronze_data_quality_expectations_json_it": "{dbfs_path}/integration_tests/conf/dqe/customers/bronze_data_quality_expectations.json", + "bronze_data_quality_expectations_json_it": "{uc_volume_path}/integration_tests/conf/dqe/customers/bronze_data_quality_expectations.json", "bronze_database_quarantine_it": "{uc_catalog_name}.{bronze_schema}", "bronze_quarantine_table": "customers_quarantine", - "bronze_quarantine_table_path_it": "{dbfs_path}/data/bronze/customers_quarantine", + "bronze_quarantine_table_path_it": "{uc_volume_path}/data/bronze/customers_quarantine", "bronze_quarantine_table_properties": { "pipelines.reset.allowed": "false", "pipelines.autoOptimize.zOrderCols": "id, email" @@ -44,8 +44,8 @@ "create_streaming_table": false, "source_format": "cloudFiles", "source_details": { - "source_path_it": "{dbfs_path}/integration_tests/resources/data/customers_af", - "source_schema_path": "{dbfs_path}/integration_tests/resources/customers.ddl" + "source_path_it": "{uc_volume_path}/integration_tests/resources/data/customers_af", + "source_schema_path": "{uc_volume_path}/integration_tests/resources/customers.ddl" }, "reader_options": { "cloudFiles.format": "json", @@ -57,13 +57,13 @@ ], "silver_database_it": "{uc_catalog_name}.{silver_schema}", "silver_table": "customers", - "silver_table_path_it": "{dbfs_path}/data/silver/customers", - "silver_transformation_json_it": "{dbfs_path}/integration_tests/conf/silver_transformations.json", + "silver_table_path_it": "{uc_volume_path}/data/silver/customers", + "silver_transformation_json_it": "{uc_volume_path}/integration_tests/conf/silver_transformations.json", "silver_table_properties": { "pipelines.reset.allowed": "false", "pipelines.autoOptimize.zOrderCols": "id, email" }, - "silver_data_quality_expectations_json_it": "{dbfs_path}/integration_tests/conf/dqe/customers/silver_data_quality_expectations.json", + "silver_data_quality_expectations_json_it": "{uc_volume_path}/integration_tests/conf/dqe/customers/silver_data_quality_expectations.json", "silver_append_flows": [ { "name": "customers_silver_flow", @@ -85,7 +85,7 @@ "source_details": { "source_database": "APP", "source_table": "TRANSACTIONS", - "source_path_it": "{dbfs_path}/integration_tests/resources/data/transactions", + "source_path_it": "{uc_volume_path}/integration_tests/resources/data/transactions", "source_metadata": { "include_autoloader_metadata_column": "True", "select_metadata_cols": { @@ -93,7 +93,7 @@ "input_file_path": "_metadata.file_path" } }, - "source_schema_path": "{dbfs_path}/integration_tests/resources/transactions.ddl" + "source_schema_path": "{uc_volume_path}/integration_tests/resources/transactions.ddl" }, "bronze_database_it": "{uc_catalog_name}.{bronze_schema}", "bronze_table": "transactions", @@ -102,15 +102,15 @@ "cloudFiles.inferColumnTypes": "true", "cloudFiles.rescuedDataColumn": "_rescued_data" }, - "bronze_table_path_it": "{dbfs_path}/data/bronze/transactions", + "bronze_table_path_it": "{uc_volume_path}/data/bronze/transactions", "bronze_table_properties": { "pipelines.reset.allowed": "true", "pipelines.autoOptimize.zOrderCols": "id, customer_id" }, - "bronze_data_quality_expectations_json_it": "{dbfs_path}/integration_tests/conf/dqe/transactions/bronze_data_quality_expectations.json", + "bronze_data_quality_expectations_json_it": "{uc_volume_path}/integration_tests/conf/dqe/transactions/bronze_data_quality_expectations.json", "bronze_database_quarantine_it": "{uc_catalog_name}.{bronze_schema}", "bronze_quarantine_table": "transactions_quarantine", - "bronze_quarantine_table_path_it": "{dbfs_path}/data/bronze/transactions_quarantine", + "bronze_quarantine_table_path_it": "{uc_volume_path}/data/bronze/transactions_quarantine", "bronze_quarantine_table_properties": { "pipelines.reset.allowed": "true", "pipelines.autoOptimize.managed": "false", @@ -122,8 +122,8 @@ "create_streaming_table": false, "source_format": "cloudFiles", "source_details": { - "source_path_it": "{dbfs_path}/integration_tests/resources/data/transactions_af", - "source_schema_path": "{dbfs_path}/integration_tests/resources/transactions.ddl" + "source_path_it": "{uc_volume_path}/integration_tests/resources/data/transactions_af", + "source_schema_path": "{uc_volume_path}/integration_tests/resources/transactions.ddl" }, "reader_options": { "cloudFiles.format": "json", @@ -149,9 +149,9 @@ ], "flow_name":"silver_transactions_cdc_applychanges_flow" }, - "silver_table_path_it": "{dbfs_path}/data/silver/transactions", - "silver_transformation_json_it": "{dbfs_path}/integration_tests/conf/silver_transformations.json", - "silver_data_quality_expectations_json_it": "{dbfs_path}/integration_tests/conf/dqe/transactions/silver_data_quality_expectations.json", + "silver_table_path_it": "{uc_volume_path}/data/silver/transactions", + "silver_transformation_json_it": "{uc_volume_path}/integration_tests/conf/silver_transformations.json", + "silver_data_quality_expectations_json_it": "{uc_volume_path}/integration_tests/conf/dqe/transactions/silver_data_quality_expectations.json", "silver_table_properties": { "pipelines.reset.allowed": "false", "pipelines.autoOptimize.zOrderCols": "id, customer_id" diff --git a/integration_tests/conf/cloudfiles-onboarding_A2.template b/integration_tests/conf/cloudfiles-onboarding_A2.template index ab578c7..be9c8c9 100644 --- a/integration_tests/conf/cloudfiles-onboarding_A2.template +++ b/integration_tests/conf/cloudfiles-onboarding_A2.template @@ -7,14 +7,14 @@ "source_details": { "source_database": "APP", "source_table": "CUSTOMERS", - "source_path_it": "{dbfs_path}/integration_tests/resources/data/customers_delta", + "source_path_it": "{uc_volume_path}/integration_tests/resources/data/customers_delta", "source_metadata": { "select_metadata_cols": { "input_file_name": "_metadata.file_name", "input_file_path": "_metadata.file_path" } }, - "source_schema_path": "{dbfs_path}/integration_tests/resources/customers.ddl" + "source_schema_path": "{uc_volume_path}/integration_tests/resources/customers.ddl" }, "bronze_database_it": "{uc_catalog_name}.{bronze_schema}", "bronze_table": "customers_delta", @@ -23,15 +23,15 @@ "cloudFiles.inferColumnTypes": "true", "cloudFiles.rescuedDataColumn": "_rescued_data" }, - "bronze_table_path_it": "{dbfs_path}/data/bronze/customers_delta", + "bronze_table_path_it": "{uc_volume_path}/data/bronze/customers_delta", "bronze_table_properties": { "pipelines.autoOptimize.managed": "true", "pipelines.autoOptimize.zOrderCols": "id, email" }, - "bronze_data_quality_expectations_json_it": "{dbfs_path}/integration_tests/conf/dqe/customers/bronze_data_quality_expectations.json", + "bronze_data_quality_expectations_json_it": "{uc_volume_path}/integration_tests/conf/dqe/customers/bronze_data_quality_expectations.json", "bronze_database_quarantine_it": "{uc_catalog_name}.{bronze_schema}", "bronze_quarantine_table": "customers_delta_quarantine", - "bronze_quarantine_table_path_it": "{dbfs_path}/data/bronze/customers_quarantine_delta", + "bronze_quarantine_table_path_it": "{uc_volume_path}/data/bronze/customers_quarantine_delta", "bronze_quarantine_table_properties": { "pipelines.reset.allowed": "false", "pipelines.autoOptimize.zOrderCols": "id, email" diff --git a/integration_tests/conf/eventhub-onboarding.template b/integration_tests/conf/eventhub-onboarding.template index 8a62a7d..48f4b28 100644 --- a/integration_tests/conf/eventhub-onboarding.template +++ b/integration_tests/conf/eventhub-onboarding.template @@ -5,7 +5,7 @@ "source_system": "Sensor Device", "source_format": "eventhub", "source_details": { - "source_schema_path": "{dbfs_path}/integration_tests/resources/eventhub_iot_schema.ddl", + "source_schema_path": "{uc_volume_path}/integration_tests/resources/eventhub_iot_schema.ddl", "eventhub.accessKeyName": "{eventhub_accesskey_name}", "eventhub.name": "{eventhub_name}", "eventhub.accessKeySecretName": "{eventhub_accesskey_secret_name}", @@ -25,17 +25,17 @@ "bronze_database_it": "{uc_catalog_name}.{bronze_schema}", "bronze_table": "bronze_{run_id}_iot", "bronze_partition_columns": "date", - "bronze_data_quality_expectations_json_it": "{dbfs_path}/integration_tests/conf/dqe/iot/bronze_data_quality_expectations.json", + "bronze_data_quality_expectations_json_it": "{uc_volume_path}/integration_tests/conf/dqe/iot/bronze_data_quality_expectations.json", "bronze_database_quarantine_it": "{uc_catalog_name}.{bronze_schema}", "bronze_quarantine_table": "bronze_{run_id}_iot_quarantine", - "bronze_quarantine_table_path_it": "{dbfs_path}/data/bronze/iot_quarantine", + "bronze_quarantine_table_path_it": "{uc_volume_path}/data/bronze/iot_quarantine", "bronze_append_flows": [ { "name": "io_bronze_eventhub_append_flow", "create_streaming_table": false, "source_format": "eventhub", "source_details": { - "source_schema_path": "{dbfs_path}/integration_tests/resources/eventhub_iot_schema.ddl", + "source_schema_path": "{uc_volume_path}/integration_tests/resources/eventhub_iot_schema.ddl", "eventhub.accessKeyName": "{eventhub_accesskey_name}", "eventhub.name": "{eventhub_name_append_flow}", "eventhub.accessKeySecretName": "{eventhub_accesskey_secret_name}", diff --git a/integration_tests/conf/kafka-onboarding.template b/integration_tests/conf/kafka-onboarding.template index ce3cc40..6eb2249 100644 --- a/integration_tests/conf/kafka-onboarding.template +++ b/integration_tests/conf/kafka-onboarding.template @@ -5,7 +5,7 @@ "source_system": "Sensor Device", "source_format": "kafka", "source_details": { - "source_schema_path": "{dbfs_path}/integration_tests/resources/eventhub_iot_schema.ddl", + "source_schema_path": "{uc_volume_path}/integration_tests/resources/eventhub_iot_schema.ddl", "subscribe": "{kafka_topic}", "kafka.bootstrap.servers": "{kafka_bootstrap_servers}" }, @@ -17,10 +17,10 @@ "bronze_database_it": "{uc_catalog_name}.{bronze_schema}", "bronze_table": "bronze_{run_id}_iot", "bronze_partition_columns": "date", - "bronze_table_path_it": "{dbfs_path}/data/bronze/iot", - "bronze_data_quality_expectations_json_it": "{dbfs_path}/integration_tests/conf/dqe/iot/bronze_data_quality_expectations.json", + "bronze_table_path_it": "{uc_volume_path}/data/bronze/iot", + "bronze_data_quality_expectations_json_it": "{uc_volume_path}/integration_tests/conf/dqe/iot/bronze_data_quality_expectations.json", "bronze_database_quarantine_it": "{uc_catalog_name}.{bronze_schema}", "bronze_quarantine_table": "bronze_{run_id}_iot_quarantine", - "bronze_quarantine_table_path_it": "{dbfs_path}/data/bronze/iot_quarantine" + "bronze_quarantine_table_path_it": "{uc_volume_path}/data/bronze/iot_quarantine" } ] \ No newline at end of file diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index eb192de..1fa6349 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -187,8 +187,7 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: if self.args.__dict__['uc_catalog_name']: runner_conf.uc_catalog_name = self.args.__dict__['uc_catalog_name'] runner_conf.uc_volume_name = f"{self.args.__dict__['uc_catalog_name']}_volume_{run_id}", - runner_conf.uc_volume_path = (f"/Volumes/{runner_conf.uc_catalog_name}/" - f"{runner_conf.volume_info.schema_name}/{runner_conf.volume_info.name}/") + runners_full_local_path = None if runner_conf.source.lower() == "cloudfiles": @@ -521,13 +520,23 @@ def create_eventhub_workflow_spec(self, runner_conf: DLTMetaRunnerConf): def create_kafka_workflow_spec(self, runner_conf: DLTMetaRunnerConf): """Create Job specification.""" database, dlt_lib = self.init_db_dltlib(runner_conf) + dltmeta_environments = [ + jobs.JobEnvironment( + environment_key="dl_meta_int_env", + spec=compute.Environment(client=f"dlt_meta_int_test_{__version__}", + dependencies=[runner_conf.remote_whl_path] + ) + ) + ] dbfs_path = runner_conf.dbfs_tmp_path.replace("dbfs:/", "/dbfs/") return self.ws.jobs.create( name=f"dlt-meta-{runner_conf.run_id}", + environemnts=dltmeta_environments, tasks=[ jobs.Task( task_key="setup_dlt_meta_pipeline_spec", description="test", + environment_key="dl_meta_int_env", existing_cluster_id=runner_conf.cluster_id, timeout_seconds=0, python_wheel_task=jobs.PythonWheelTask( @@ -547,15 +556,13 @@ def create_kafka_workflow_spec(self, runner_conf: DLTMetaRunnerConf): "overwrite": "True", "env": runner_conf.env, "uc_enabled": "True" if runner_conf.uc_catalog_name else "False" - }, - ), - libraries=dlt_lib + } + ) ), jobs.Task( task_key="publish_events", description="test", depends_on=[jobs.TaskDependency(task_key="setup_dlt_meta_pipeline_spec")], - existing_cluster_id=runner_conf.cluster_id, notebook_task=jobs.NotebookTask( notebook_path=f"{runner_conf.runners_nb_path}/runners/publish_events", base_parameters={ @@ -576,7 +583,6 @@ def create_kafka_workflow_spec(self, runner_conf: DLTMetaRunnerConf): task_key="validate_results", description="test", depends_on=[jobs.TaskDependency(task_key="bronze_dlt_pipeline")], - existing_cluster_id=runner_conf.cluster_id, notebook_task=jobs.NotebookTask( notebook_path=f"{runner_conf.runners_nb_path}/runners/validate", base_parameters={ @@ -1080,9 +1086,6 @@ def get_workspace_api_client(profile=None) -> WorkspaceClient: args_map = {"--profile": "provide databricks cli profile name, if not provide databricks_host and token", "--uc_catalog_name": "provide databricks uc_catalog name, this is required to create volume, schema, table", "--cloud_provider_name": "provide cloud provider name. Supported values are aws , azure , gcp", - "--dbr_version": "Provide databricks runtime spark version e.g 15.3.x-scala2.12", - "--dbfs_path": "Provide databricks workspace dbfs path where you want run integration tests \ - e.g --dbfs_path=dbfs:/tmp/DLT-META/", "--source": "Provide source type e.g --source=cloudfiles", "--eventhub_name": "Provide eventhub_name e.g --eventhub_name=iot", "--eventhub_name_append_flow": "Provide eventhub_name_append_flow e.g --eventhub_name_append_flow=iot_af", @@ -1099,8 +1102,7 @@ def get_workspace_api_client(profile=None) -> WorkspaceClient: } mandatory_args = [ - "uc_catalog_name", "cloud_provider_name", - "dbr_version", "source", "dbfs_path" + "uc_catalog_name", "cloud_provider_name", "source" ] From 514a974d672e0504581beed772dbb47656f368a7 Mon Sep 17 00:00:00 2001 From: ravi-databricks Date: Fri, 27 Sep 2024 10:48:59 -0700 Subject: [PATCH 09/59] Added: 1.CLI to include serverless for onboarding job 2.CLI to include uc volumes instead of dbfs for uc --- integration_tests/run_integration_tests.py | 3 +- src/cli.py | 179 ++++++++++++++------- 2 files changed, 118 insertions(+), 64 deletions(-) diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index 1fa6349..763ae62 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -536,8 +536,7 @@ def create_kafka_workflow_spec(self, runner_conf: DLTMetaRunnerConf): jobs.Task( task_key="setup_dlt_meta_pipeline_spec", description="test", - environment_key="dl_meta_int_env", - existing_cluster_id=runner_conf.cluster_id, + environment_key="dl_meta_int_env", timeout_seconds=0, python_wheel_task=jobs.PythonWheelTask( package_name="dlt_meta", diff --git a/src/cli.py b/src/cli.py index 401ba37..6e9fe51 100644 --- a/src/cli.py +++ b/src/cli.py @@ -11,8 +11,8 @@ from databricks.sdk.service import jobs, pipelines, compute from databricks.sdk.service.pipelines import PipelineLibrary, NotebookLibrary from databricks.sdk.core import DatabricksError -from databricks.sdk.service.catalog import SchemasAPI -from src import __about__ +from databricks.sdk.service.catalog import SchemasAPI, VolumeType +from src import __about__, __version__ from src.install import WorkspaceInstaller logger = logging.getLogger('databricks.labs.dltmeta') @@ -35,21 +35,22 @@ @dataclass class OnboardCommand: """Class representing the onboarding command.""" - dbr_version: str - dbfs_path: str onboarding_file_path: str onboarding_files_dir_path: str onboard_layer: str env: str import_author: str version: str - cloud: str dlt_meta_schema: str + dbfs_path: None + cloud: None + dbr_version: None serverless: bool = True bronze_schema: str = None silver_schema: str = None uc_enabled: bool = False uc_catalog_name: str = None + uc_volume_path: str = None overwrite: bool = True bronze_dataflowspec_table: str = "bronze_dataflowspec" silver_dataflowspec_table: str = "silver_dataflowspec" @@ -68,6 +69,13 @@ def __post_init__(self): raise ValueError("onboard_layer must be one of bronze, silver, bronze_silver") if self.uc_enabled == "": raise ValueError("uc_enabled is required, please set to True or False") + if not self.uc_enabled and not self.dbfs_path: + raise ValueError("dbfs_path is required") + if not self.serverless: + if not self.cloud: + raise ValueError("cloud is required") + if not self.dbr_version: + raise ValueError("dbr_version is required") if self.onboard_layer and self.onboard_layer.lower() == "bronze_silver": if not self.uc_enabled: if not self.bronze_dataflowspec_path or self.silver_dataflowspec_path == "": @@ -86,12 +94,6 @@ def __post_init__(self): raise ValueError("silver_dataflowspec_path is required") if not self.dlt_meta_schema: raise ValueError("dlt_meta_schema is required") - if not self.cloud: - raise ValueError("cloud is required") - if not self.dbfs_path: - raise ValueError("dbfs_path is required") - if not self.dbr_version: - raise ValueError("dbr_version is required") if not self.overwrite: raise ValueError("overwrite is required") if not self.import_author: @@ -150,7 +152,23 @@ def _my_username(self): _me = self._ws.current_user.me() return _me.user_name - def copy(self, src, dst): + def copy_to_uc_volume(self, src, dst): + main_dir = src.replace('file:', '') + base_dir_name = None + if main_dir.endswith('/'): + base_dir_name = main_dir[:-1] + if base_dir_name is None: + base_dir_name = main_dir[main_dir.rfind('/') + 1:] + else: + base_dir_name = base_dir_name[base_dir_name.rfind('/') + 1:-1] + for root, dirs, files in os.walk(main_dir): + for filename in files: + target_dir = root[root.index(main_dir) + len(main_dir):len(root)] + uc_volume_path = f"{dst}/{base_dir_name}/{target_dir}/{filename}".replace("//", "/") + contents = open(os.path.join(root, filename), "rb") + self._ws.files.upload(file_path=uc_volume_path, contents=contents, overwrite=True) + + def copy_to_dbfs(self, src, dst): dst = dst.replace('//', '/') main_dir = src.replace('file:', '') main_dir = main_dir.replace('//', '/') @@ -172,30 +190,30 @@ def copy(self, src, dst): ) self._ws.dbfs.upload(dbfs_path, contents, overwrite=True) + def create_uc_volume(self, uc_catalog_name, dlt_meta_schema): + volume_info = self._ws.volumes.create(catalog_name=uc_catalog_name, + schema_name=dlt_meta_schema, + name=dlt_meta_schema, + volume_type=VolumeType.MANAGED) + return (f"/Volumes/{volume_info.catalog_name}/" + f"{volume_info.schema_name}/{volume_info.name}/" + ) + def onboard(self, cmd: OnboardCommand): """Perform the onboarding process.""" self.update_ws_onboarding_paths(cmd) - if not self._ws.dbfs.exists(cmd.dbfs_path + "/dltmeta_conf/"): - self._ws.dbfs.mkdirs(f"{cmd.dbfs_path}/dltmeta_conf/") ob_file = open(cmd.onboarding_file_path, "rb") onboarding_filename = os.path.basename(cmd.onboarding_file_path) - self._ws.dbfs.upload(f"{cmd.dbfs_path}/dltmeta_conf/{onboarding_filename}", ob_file, overwrite=True) - self.copy(cmd.onboarding_files_dir_path, cmd.dbfs_path + "/dltmeta_conf/") - logger.info(f"uploading to {cmd.dbfs_path}/dltmeta_conf complete!!!") if cmd.uc_enabled: - try: - SchemasAPI(self._ws.api_client).get(full_name=f"{cmd.uc_catalog_name}.{cmd.dlt_meta_schema}") - except Exception: - msg = ( - "Schema {catalog}.{schema} not found. " - "Creating schema={schema}" - ).format(catalog=cmd.uc_catalog_name, schema=cmd.dlt_meta_schema) - logger.info(msg) - SchemasAPI(self._ws.api_client).create( - catalog_name=cmd.uc_catalog_name, - name=cmd.dlt_meta_schema, - comment="dlt_meta framework schema" - ) + self.create_uc_schema(cmd.uc_catalog_name, cmd.dlt_meta_schema) + cmd.uc_volume_path = self.create_uc_volume(cmd.uc_catalog_name, cmd.dlt_meta_schema) + self.copy_to_uc_volume(cmd.onboarding_files_dir_path, cmd.uc_volume_path + "/dltmeta_conf/") + logger.info(f"uploading to {cmd.uc_volume_path}/dltmeta_conf complete!!!") + else: + self._ws.dbfs.mkdirs(f"{cmd.dbfs_path}/dltmeta_conf/") + self._ws.dbfs.upload(f"{cmd.dbfs_path}/dltmeta_conf/{onboarding_filename}", ob_file, overwrite=True) + self.copy_to_dbfs(cmd.onboarding_files_dir_path, cmd.dbfs_path + "/dltmeta_conf/") + logger.info(f"uploading to {cmd.dbfs_path}/dltmeta_conf complete!!!") created_job = self.create_onnboarding_job(cmd) logger.info(f"Waiting for job to complete. job_id={created_job.job_id}") run = self._ws.jobs.run_now(job_id=created_job.job_id) @@ -206,9 +224,26 @@ def onboard(self, cmd: OnboardCommand): logger.info(msg) webbrowser.open(f"{self._ws.config.host}/jobs/{created_job.job_id}?o={self._ws.get_workspace_id()}") + def create_uc_schema(self, uc_catalog_name, dlt_meta_schema): + try: + SchemasAPI(self._ws.api_client).get(full_name=f"{uc_catalog_name}.{dlt_meta_schema}") + except Exception: + msg = ( + "Schema {catalog}.{schema} not found. " + "Creating schema={schema}" + ).format(catalog=uc_catalog_name, schema=dlt_meta_schema) + logger.info(msg) + SchemasAPI(self._ws.api_client).create( + catalog_name=uc_catalog_name, + name=dlt_meta_schema, + comment="dlt_meta framework schema" + ) + def create_onnboarding_job(self, cmd: OnboardCommand): """Create the onboarding job.""" - if not cmd.serverless: + if cmd.serverless: + cluster_spec = None + else: cluster_spec = compute.ClusterSpec( spark_version=cmd.dbr_version, num_workers=1, @@ -223,26 +258,29 @@ def create_onnboarding_job(self, cmd: OnboardCommand): ) onboarding_filename = os.path.basename(cmd.onboarding_file_path) named_parameters = self._get_onboarding_named_parameters(cmd, onboarding_filename) + dltmeta_environments = [ + jobs.JobEnvironment( + environment_key="dl_meta_cli_env", + spec=compute.Environment(client=f"dlt_meta_cli_{__version__}", + dependencies=[f"dlt-meta=={self.version}"] + ) + ) + ] return self._ws.jobs.create( name="dlt_meta_onboarding_job", - environments=None if not cmd.serverless else [jobs.JobEnvironment(key="dlt_meta_onboarding_env")], + environments=None if not cmd.serverless else dltmeta_environments, tasks=[ jobs.Task( task_key="dlt_meta_onbarding_task", description="test", - new_cluster=cluster_spec if cmd.serverless else None, - environment_key="dlt_meta_onboarding_env" if cmd.serverless else None, + new_cluster=cluster_spec if not cmd.serverless else None, + environment_key="dl_meta_cli_env" if cmd.serverless else None, timeout_seconds=0, python_wheel_task=jobs.PythonWheelTask( package_name="dlt_meta", entry_point="run", named_parameters=named_parameters, - ), - libraries=[ - jobs.compute.Library( - pypi=compute.PythonPyPiLibrary(package=f"dlt-meta=={self.version}") - ) - ], + ) ), ] ) @@ -253,14 +291,16 @@ def _get_onboarding_named_parameters(self, cmd: OnboardCommand, onboarding_filen "database": f"{cmd.uc_catalog_name}.{cmd.dlt_meta_schema}" if cmd.uc_enabled else cmd.dlt_meta_schema, - "onboarding_file_path": - f"{cmd.dbfs_path}/dltmeta_conf/{onboarding_filename}", "import_author": cmd.import_author, "version": cmd.version, "overwrite": "True" if cmd.overwrite else "False", "env": cmd.env, "uc_enabled": "True" if cmd.uc_enabled else "False" } + if cmd.uc_enabled: + named_parameters["onboarding_file_path"] = f"{cmd.uc_volume_path}/dltmeta_conf/{onboarding_filename}" + else: + named_parameters["onboarding_file_path"] = f"{cmd.dbfs_path}/dltmeta_conf/{onboarding_filename}" if cmd.onboard_layer == "bronze_silver": named_parameters["bronze_dataflowspec_table"] = cmd.bronze_dataflowspec_table named_parameters["silver_dataflowspec_table"] = cmd.silver_dataflowspec_table @@ -349,25 +389,33 @@ def deploy(self, cmd: DeployCommand): def _load_onboard_config(self) -> OnboardCommand: onboard_cmd_dict = {} - onboard_cmd_dict["onboarding_file_path"] = self._wsi._question( - "Provide onboarding file path", default='demo/conf/onboarding.template') - cwd = os.getcwd() + onboard_cmd_dict["uc_enabled"] = self._wsi._choice( + "Run onboarding with unity catalog enabled?", ['True', 'False']) + onboard_cmd_dict["uc_enabled"] = True if onboard_cmd_dict["uc_enabled"] == "True" else False + if onboard_cmd_dict["uc_enabled"]: + onboard_cmd_dict["dbfs_path"] = None + onboard_cmd_dict["uc_catalog_name"] = self._wsi._question( + "Provide unity catalog name") + else: + onboard_cmd_dict["dbfs_path"] = self._wsi._question( + "Provide dbfs path", default=f"dbfs:/dlt-meta_cli_demo_{uuid.uuid4().hex}") onboard_cmd_dict["serverless"] = self._wsi._choice( "Run onboarding with serverless?", ['True', 'False']) onboard_cmd_dict["serverless"] = True if onboard_cmd_dict["serverless"] == 'True' else False + if onboard_cmd_dict["serverless"]: + onboard_cmd_dict["cloud"] = None + onboard_cmd_dict["dbr_version"] = None + else: + onboard_cmd_dict["cloud"] = self._wsi._choice( + "Provide cloud provider name", ['aws', 'azure', 'gcp']) + onboard_cmd_dict["dbr_version"] = self._wsi._question( + "Provide databricks runtime version", default=self._ws.clusters.select_spark_version(latest=True)) + onboard_cmd_dict["onboarding_file_path"] = self._wsi._question( + "Provide onboarding file path", default='demo/conf/onboarding.template') + cwd = os.getcwd() onboarding_files_dir_path = self._wsi._question( "Provide onboarding files local directory", default=f'{cwd}/demo/') onboard_cmd_dict["onboarding_files_dir_path"] = f"file:/{onboarding_files_dir_path}" - onboard_cmd_dict["dbfs_path"] = self._wsi._question( - "Provide dbfs path", default=f"dbfs:/dlt-meta_cli_demo_{uuid.uuid4().hex}") - onboard_cmd_dict["dbr_version"] = self._wsi._question( - "Provide databricks runtime version", default=self._ws.clusters.select_spark_version(latest=True)) - onboard_cmd_dict["uc_enabled"] = self._wsi._choice( - "Run onboarding with unity catalog enabled?", ['True', 'False']) - onboard_cmd_dict["uc_enabled"] = True if onboard_cmd_dict["uc_enabled"] == 'True' else False - if onboard_cmd_dict["uc_enabled"]: - onboard_cmd_dict["uc_catalog_name"] = self._wsi._question( - "Provide unity catalog name") onboard_cmd_dict["dlt_meta_schema"] = self._wsi._question( "Provide dlt meta schema name", default=f'dlt_meta_dataflowspecs_{uuid.uuid4().hex}') onboard_cmd_dict["bronze_schema"] = self._wsi._question( @@ -406,11 +454,10 @@ def _load_onboard_config(self) -> OnboardCommand: "Provide environment name", default='prod') onboard_cmd_dict["import_author"] = self._wsi._question( "Provide import author name", default=self._wsi._short_name) - onboard_cmd_dict["cloud"] = self._wsi._choice( - "Provide cloud provider name", ['aws', 'azure', 'gcp']) onboard_cmd_dict["update_paths"] = self._wsi._choice( - "Update workspace/dbfs paths, unity catalog name, bronze/silver schema names in onboarding file?", + "Update workspace/dbfs uc volume paths, unity catalog name, bronze/silver schema names in onboarding file?", ['True', 'False']) + print(onboard_cmd_dict) cmd = OnboardCommand(**onboard_cmd_dict) return cmd @@ -469,10 +516,18 @@ def update_ws_onboarding_paths(self, cmd: OnboardCommand): for key, value in data_flow.items(): if key == "source_details": for source_key, source_value in value.items(): - if 'dbfs_path' in source_value: - data_flow[key][source_key] = source_value.format(dbfs_path=f"{cmd.dbfs_path}/dltmeta_conf/") - if 'dbfs_path' in value: - data_flow[key] = value.format(dbfs_path=f"{cmd.dbfs_path}/dltmeta_conf/") + if cmd.uc_enabled: + if 'uc_volume_path' in source_value: + data_flow[key][source_key] = source_value.format( + uc_volume_path=f"{cmd.uc_volume_path}/dltmeta_conf/") + else: + data_flow[key][source_key] = source_value.format( + uc_volume_path=f"{cmd.dbfs_path}/dltmeta_conf/") + if 'uc_volume_path' in value: + if cmd.uc_enabled: + data_flow[key] = value.format(uc_volume_path=f"{cmd.uc_volume_path}/dltmeta_conf/") + else: + data_flow[key] = value.format(uc_volume_path=f"{cmd.dbfs_path}/dltmeta_conf/") elif 'uc_catalog_name' in value and 'bronze_schema' in value: if cmd.uc_catalog_name: data_flow[key] = value.format( From 6cd66ad38446fabf8ad97ee6cedc2c076318c4a6 Mon Sep 17 00:00:00 2001 From: ravi-databricks Date: Fri, 27 Sep 2024 11:52:42 -0700 Subject: [PATCH 10/59] Added basic implementation for apply_changes_from_snapshot --- ...dlt_meta_pipeline_snapshot_ingestion.ipynb | 127 ++++++++++++++++++ src/dataflow_pipeline.py | 33 ++++- src/onboard_dataflowspec.py | 2 +- 3 files changed, 154 insertions(+), 8 deletions(-) create mode 100644 examples/dlt_meta_pipeline_snapshot_ingestion.ipynb diff --git a/examples/dlt_meta_pipeline_snapshot_ingestion.ipynb b/examples/dlt_meta_pipeline_snapshot_ingestion.ipynb new file mode 100644 index 0000000..4c17f9a --- /dev/null +++ b/examples/dlt_meta_pipeline_snapshot_ingestion.ipynb @@ -0,0 +1,127 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "18a5c12b-aa41-4465-b189-353db4370f83", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "%pip install dlt-meta" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Databricks notebook source\n", + "# DBTITLE 1,DLT Snapshot Processing Logic\n", + "import dlt\n", + "from datetime import timedelta\n", + "from datetime import datetime\n", + "\n", + "\n", + "def exist(path):\n", + " try:\n", + " if dbutils.fs.ls(path) is None:\n", + " return False\n", + " else:\n", + " return True\n", + " except:\n", + " return False\n", + "\n", + "\n", + "snapshot_root_path = \"path\" # read from dataflowspec source_details.path\n", + "\n", + "# List all objects in the bucket using dbutils.fs\n", + "object_paths = dbutils.fs.ls(snapshot_root_path)\n", + "\n", + "datetimes = []\n", + "for path in object_paths:\n", + " # Parse the datetime string to a datetime object\n", + " datetime_obj = datetime.strptime(path.name.strip('/\"'), '%Y-%m-%d %H')\n", + " datetimes.append(datetime_obj)\n", + "\n", + "# Find the earliest datetime\n", + "earliest_datetime = min(datetimes)\n", + "\n", + "# Convert the earliest datetime back to a string if needed\n", + "earliest_datetime_str = earliest_datetime.strftime('%Y-%m-%d %H')\n", + "\n", + "print(f\"The earliest datetime in the bucket is: {earliest_datetime_str}\")\n", + "\n", + "\n", + "def next_snapshot_and_version(latest_snapshot_datetime):\n", + " latest_datetime_str = latest_snapshot_datetime or earliest_datetime_str\n", + " if latest_snapshot_datetime is None:\n", + " snapshot_path = f\"{snapshot_root_path}/{earliest_datetime_str}\"\n", + " print(f\"Reading earliest snapshot from {snapshot_path}\")\n", + " earliest_snapshot = spark.read.format(\"parquet\").load(snapshot_path)\n", + " return earliest_snapshot, earliest_datetime_str\n", + " else:\n", + " latest_datetime = datetime.strptime(latest_datetime_str, '%Y-%m-%d %H')\n", + " # Calculate the next datetime\n", + " increment = timedelta(hours=1) # Increment by 1 hour because we are provided hourly snapshots\n", + " next_datetime = latest_datetime + increment\n", + " print(f\"The next snapshot version is : {next_datetime}\")\n", + "\n", + " # Convert the next_datetime to a string with the desired format\n", + " next_snapshot_datetime_str = next_datetime.strftime('%Y-%m-%d %H')\n", + " snapshot_path = f\"{snapshot_root_path}/{next_snapshot_datetime_str}\"\n", + " print(\"Attempting to read next snapshot from \" + snapshot_path)\n", + "\n", + " if (exist(snapshot_path)):\n", + " snapshot = spark.read.format(\"parquet\").load(snapshot_path)\n", + " return snapshot, next_snapshot_datetime_str\n", + " else:\n", + " print(f\"Couldn't find snapshot data at {snapshot_path}\")\n", + " return None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "de72e08f-5432-4e56-b17d-cadee25b4714", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "layer = spark.conf.get(\"layer\", None)\n", + "from src.dataflow_pipeline import DataflowPipeline\n", + "DataflowPipeline.invoke_dlt_pipeline(spark, layer, snapshot_reader_func=next_snapshot_and_version)" + ] + } + ], + "metadata": { + "application/vnd.databricks.v1+notebook": { + "dashboards": [], + "language": "python", + "notebookMetadata": { + "pythonIndentUnit": 2 + }, + "notebookName": "dlt_meta_pipeline", + "notebookOrigID": 4156927443107021, + "widgets": {} + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/src/dataflow_pipeline.py b/src/dataflow_pipeline.py index dfd9fb1..3e952b6 100644 --- a/src/dataflow_pipeline.py +++ b/src/dataflow_pipeline.py @@ -63,7 +63,8 @@ class DataflowPipeline: [type]: [description] """ - def __init__(self, spark, dataflow_spec, view_name, view_name_quarantine=None, custom_transform_func=None): + def __init__(self, spark, dataflow_spec, view_name, view_name_quarantine=None, + custom_transform_func=None, snapshot_reader_func=None): """Initialize Constructor.""" logger.info( f"""dataflowSpec={dataflow_spec} , @@ -72,13 +73,13 @@ def __init__(self, spark, dataflow_spec, view_name, view_name_quarantine=None, c ) if isinstance(dataflow_spec, BronzeDataflowSpec) or isinstance(dataflow_spec, SilverDataflowSpec): self.__initialize_dataflow_pipeline( - spark, dataflow_spec, view_name, view_name_quarantine, custom_transform_func + spark, dataflow_spec, view_name, view_name_quarantine, custom_transform_func, snapshot_reader_func ) else: raise Exception("Dataflow not supported!") def __initialize_dataflow_pipeline( - self, spark, dataflow_spec, view_name, view_name_quarantine, custom_transform_func + self, spark, dataflow_spec, view_name, view_name_quarantine, custom_transform_func, snapshot_reader_func ): """Initialize dataflow pipeline state.""" self.spark = spark @@ -90,6 +91,7 @@ def __initialize_dataflow_pipeline( if view_name_quarantine: self.view_name_quarantine = view_name_quarantine self.custom_transform_func = custom_transform_func + self.snapshot_reader_func = snapshot_reader_func if dataflow_spec.cdcApplyChanges: self.cdcApplyChanges = DataflowSpecUtils.get_cdc_apply_changes(self.dataflowSpec.cdcApplyChanges) else: @@ -117,13 +119,13 @@ def table_has_expectations(self): def read(self): """Read DLT.""" logger.info("In read function") - if isinstance(self.dataflowSpec, BronzeDataflowSpec): + if isinstance(self.dataflowSpec, BronzeDataflowSpec) and not self.snapshot_reader_func: dlt.view( self.read_bronze, name=self.view_name, comment=f"input dataset view for {self.view_name}", ) - elif isinstance(self.dataflowSpec, SilverDataflowSpec): + elif isinstance(self.dataflowSpec, SilverDataflowSpec) and not self.snapshot_reader_func: dlt.view( self.read_silver, name=self.view_name, @@ -178,6 +180,11 @@ def write(self): def write_bronze(self): """Write Bronze tables.""" bronze_dataflow_spec: BronzeDataflowSpec = self.dataflowSpec + if bronze_dataflow_spec.sourceFormat == "snapshot": + if self.snapshot_reader_func: + self.apply_changes_from_snapshot() + else: + raise Exception("Snapshot reader function not provided!") if bronze_dataflow_spec.dataQualityExpectations: self.write_bronze_with_dqe() elif bronze_dataflow_spec.cdcApplyChanges: @@ -298,6 +305,17 @@ def write_to_delta(self): """Write to Delta.""" return dlt.read_stream(self.view_name) + def apply_changes_from_snapshot(self): + target_path = None if self.uc_enabled else self.dataflowSpec.targetDetails["path"] + self.create_streaming_table(None, target_path) + dlt.apply_changes_from_snapshot( + target=f"{self.dataflowSpec.targetDetails['table']}", + snapshot_and_version=self.snapshot_reader_func, + keys=self.cdcApplyChanges.keys, + stored_as_scd_type=self.cdcApplyChanges.scd_type, + track_history_column_list=self.cdcApplyChanges.track_history_column_list, + ) + def write_bronze_with_dqe(self): """Write Bronze table with data quality expectations.""" bronzeDataflowSpec: BronzeDataflowSpec = self.dataflowSpec @@ -513,7 +531,7 @@ def run_dlt(self): self.write() @staticmethod - def invoke_dlt_pipeline(spark, layer, custom_transform_func=None): + def invoke_dlt_pipeline(spark, layer, custom_transform_func=None, snapshot_reader_func=None): """Invoke dlt pipeline will launch dlt with given dataflowspec. Args: @@ -544,7 +562,8 @@ def invoke_dlt_pipeline(spark, layer, custom_transform_func=None): dataflowSpec, f"{dataflowSpec.targetDetails['table']}_{layer}_inputView", quarantine_input_view_name, - custom_transform_func + custom_transform_func, + snapshot_reader_func ) dlt_data_flow.run_dlt() diff --git a/src/onboard_dataflowspec.py b/src/onboard_dataflowspec.py index 221d805..e2406f7 100644 --- a/src/onboard_dataflowspec.py +++ b/src/onboard_dataflowspec.py @@ -456,7 +456,7 @@ def __get_bronze_dataflow_spec_dataframe(self, onboarding_df, env): raise Exception(f"Source format not provided for row={onboarding_row}") source_format = onboarding_row["source_format"] - if source_format.lower() not in ["cloudfiles", "eventhub", "kafka", "delta"]: + if source_format.lower() not in ["cloudfiles", "eventhub", "kafka", "delta", "snapshot"]: raise Exception(f"Source format {source_format} not supported in DLT-META! row={onboarding_row}") source_details, bronze_reader_config_options, schema = self.get_bronze_source_details_reader_options_schema( onboarding_row, env) From 81a8b1b6c6a4938b263b4411468eb962e87ee3b8 Mon Sep 17 00:00:00 2001 From: ravi-databricks Date: Fri, 27 Sep 2024 15:15:38 -0700 Subject: [PATCH 11/59] Added: 1.Incorporated applyChangesFromSnapshot attribute into onboarding 2.Dataflowspec modified to incorporate applyChangesFromSnapshot 3.Unit tests --- src/dataflow_pipeline.py | 14 +++-- src/dataflow_spec.py | 50 ++++++++++++++++- src/onboard_dataflowspec.py | 19 +++++-- ...onboarding_applychanges_from_snapshot.json | 42 +++++++++++++++ tests/test_dataflow_pipeline.py | 1 + tests/test_dataflow_spec.py | 54 ++++++++++++++++++- tests/test_onboard_dataflowspec.py | 13 +++++ tests/test_pipeline_readers.py | 4 ++ tests/utils.py | 3 ++ 9 files changed, 190 insertions(+), 10 deletions(-) create mode 100644 tests/resources/onboarding_applychanges_from_snapshot.json diff --git a/src/dataflow_pipeline.py b/src/dataflow_pipeline.py index 3e952b6..831eb4b 100644 --- a/src/dataflow_pipeline.py +++ b/src/dataflow_pipeline.py @@ -91,7 +91,6 @@ def __initialize_dataflow_pipeline( if view_name_quarantine: self.view_name_quarantine = view_name_quarantine self.custom_transform_func = custom_transform_func - self.snapshot_reader_func = snapshot_reader_func if dataflow_spec.cdcApplyChanges: self.cdcApplyChanges = DataflowSpecUtils.get_cdc_apply_changes(self.dataflowSpec.cdcApplyChanges) else: @@ -101,12 +100,19 @@ def __initialize_dataflow_pipeline( else: self.appendFlows = None if isinstance(dataflow_spec, BronzeDataflowSpec): + self.snapshot_reader_func = snapshot_reader_func + if self.snapshot_reader_func: + self.appy_changes_from_snapshot = DataflowSpecUtils.get_apply_changes_from_snapshot( + self.dataflowSpec.applyChangesFromSnapshot + ) if dataflow_spec.schema is not None: self.schema_json = json.loads(dataflow_spec.schema) else: self.schema_json = None else: self.schema_json = None + self.snapshot_reader_func = None + self.appy_changes_from_snapshot = None if isinstance(dataflow_spec, SilverDataflowSpec): self.silver_schema = self.get_silver_schema() else: @@ -311,9 +317,9 @@ def apply_changes_from_snapshot(self): dlt.apply_changes_from_snapshot( target=f"{self.dataflowSpec.targetDetails['table']}", snapshot_and_version=self.snapshot_reader_func, - keys=self.cdcApplyChanges.keys, - stored_as_scd_type=self.cdcApplyChanges.scd_type, - track_history_column_list=self.cdcApplyChanges.track_history_column_list, + keys=self.self.appy_changes_from_snapshot.keys, + stored_as_scd_type=self.self.appy_changes_from_snapshot.scd_type, + track_history_column_list=self.self.appy_changes_from_snapshot.track_history_column_list, ) def write_bronze_with_dqe(self): diff --git a/src/dataflow_spec.py b/src/dataflow_spec.py index fc90088..32f7099 100644 --- a/src/dataflow_spec.py +++ b/src/dataflow_spec.py @@ -29,6 +29,7 @@ class BronzeDataflowSpec: schema: str partitionColumns: list cdcApplyChanges: str + applyChangesFromSnapshot: str dataQualityExpectations: str quarantineTargetDetails: map quarantineTableProperties: map @@ -88,6 +89,15 @@ class CDCApplyChanges: ignore_null_updates_except_column_list: list +@dataclass +class ApplyChangesFromSnapshot: + """CDC ApplyChangesFromSnapshot structure.""" + keys: list + scd_type: str + track_history_column_list: list + track_history_except_column_list: list + + @dataclass class AppendFlow: """Append Flow structure.""" @@ -138,8 +148,8 @@ class DataflowSpecUtils: "ignore_null_updates_except_column_list": None } + apply_changes_from_snapshot_api_mandatory_attributes = ["keys", "scd_type"] append_flow_mandatory_attributes = ["name", "source_format", "create_streaming_table", "source_details"] - append_flow_api_attributes_defaults = { "comment": None, "create_streaming_table": False, @@ -151,6 +161,7 @@ class DataflowSpecUtils: additional_bronze_df_columns = ["appendFlows", "appendFlowsSchemas"] additional_silver_df_columns = ["dataQualityExpectations", "appendFlows", "appendFlowsSchemas"] additional_cdc_apply_changes_columns = ["flow_name", "once"] + additional_apply_changes_from_snapshot_columns = ["track_history_column_list", "track_history_except_column_list"] @staticmethod def _get_dataflow_spec( @@ -268,6 +279,43 @@ def get_partition_cols(partition_columns): partition_cols = list(filter(None, partition_columns)) return partition_cols + @staticmethod + def get_apply_changes_from_snapshot(apply_changes_from_snapshot) -> ApplyChangesFromSnapshot: + """Get Apply changes from snapshot metadata.""" + logger.info(apply_changes_from_snapshot) + json_apply_changes_from_snapshot = json.loads(apply_changes_from_snapshot) + logger.info(f"actual mergeInfo={json_apply_changes_from_snapshot}") + payload_keys = json_apply_changes_from_snapshot.keys() + missing_apply_changes_from_snapshot_payload_keys = set( + DataflowSpecUtils.apply_changes_from_snapshot_api_mandatory_attributes).difference(payload_keys) + logger.info( + f"missing apply changes from snapshot payload keys:" + f"{missing_apply_changes_from_snapshot_payload_keys}" + ) + if set(DataflowSpecUtils.apply_changes_from_snapshot_api_mandatory_attributes) - set(payload_keys): + missing_mandatory_attr = set(DataflowSpecUtils.apply_changes_from_snapshot_api_mandatory_attributes) - set( + payload_keys) + logger.info(f"mandatory missing keys= {missing_mandatory_attr}") + raise Exception(f"mandatory missing keys= {missing_mandatory_attr} for merge info") + else: + logger.info( + f"""all mandatory keys + {DataflowSpecUtils.apply_changes_from_snapshot_api_mandatory_attributes} exists""" + ) + + for missing_apply_changes_from_snapshot_payload_key in missing_apply_changes_from_snapshot_payload_keys: + json_apply_changes_from_snapshot[ + missing_apply_changes_from_snapshot_payload_key + ] = DataflowSpecUtils.cdc_applychanges_api_attributes_defaults[ + missing_apply_changes_from_snapshot_payload_key] + + logger.info(f"final mergeInfo={json_apply_changes_from_snapshot}") + json_apply_changes_from_snapshot = DataflowSpecUtils.populate_additional_df_cols( + json_apply_changes_from_snapshot, + DataflowSpecUtils.additional_apply_changes_from_snapshot_columns + ) + return ApplyChangesFromSnapshot(**json_apply_changes_from_snapshot) + @staticmethod def get_cdc_apply_changes(cdc_apply_changes) -> CDCApplyChanges: """Get CDC Apply changes metadata.""" diff --git a/src/onboard_dataflowspec.py b/src/onboard_dataflowspec.py index e2406f7..d9fbd95 100644 --- a/src/onboard_dataflowspec.py +++ b/src/onboard_dataflowspec.py @@ -410,6 +410,7 @@ def __get_bronze_dataflow_spec_dataframe(self, onboarding_df, env): "schema", "partitionColumns", "cdcApplyChanges", + "applyChangesFromSnapshot", "dataQualityExpectations", "quarantineTargetDetails", "quarantineTableProperties", @@ -433,6 +434,7 @@ def __get_bronze_dataflow_spec_dataframe(self, onboarding_df, env): StructField("schema", StringType(), True), StructField("partitionColumns", ArrayType(StringType(), True), True), StructField("cdcApplyChanges", StringType(), True), + StructField("applyChangesFromSnapshot", StringType(), True), StructField("dataQualityExpectations", StringType(), True), StructField("quarantineTargetDetails", MapType(StringType(), StringType(), True), True), StructField("quarantineTableProperties", MapType(StringType(), StringType(), True), True), @@ -443,7 +445,7 @@ def __get_bronze_dataflow_spec_dataframe(self, onboarding_df, env): data = [] onboarding_rows = onboarding_df.collect() mandatory_fields = ["data_flow_id", "data_flow_group", "source_details", f"bronze_database_{env}", - "bronze_table", "bronze_reader_options"] # , f"bronze_table_path_{env}" + "bronze_table"] # , f"bronze_table_path_{env}" for onboarding_row in onboarding_rows: try: self.__validate_mandatory_fields(onboarding_row, mandatory_fields) @@ -479,6 +481,11 @@ def __get_bronze_dataflow_spec_dataframe(self, onboarding_df, env): if "bronze_cdc_apply_changes" in onboarding_row and onboarding_row["bronze_cdc_apply_changes"]: self.__validate_apply_changes(onboarding_row, "bronze") cdc_apply_changes = json.dumps(self.__delete_none(onboarding_row["bronze_cdc_apply_changes"].asDict())) + apply_changes_from_snapshot = None + if ("bronze_apply_changes_from_snapshot" in onboarding_row + and onboarding_row["bronze_apply_changes_from_snapshot"]): + apply_changes_from_snapshot = onboarding_row["bronze_apply_changes_from_snapshot"] + apply_changes_from_snapshot = json.dumps(self.__delete_none(apply_changes_from_snapshot.asDict())) data_quality_expectations = None quarantine_target_details = {} quarantine_table_properties = {} @@ -521,6 +528,7 @@ def __get_bronze_dataflow_spec_dataframe(self, onboarding_df, env): schema, partition_columns, cdc_apply_changes, + apply_changes_from_snapshot, data_quality_expectations, quarantine_target_details, quarantine_table_properties, @@ -606,9 +614,12 @@ def get_bronze_source_details_reader_options_schema(self, onboarding_row, env): bronze_reader_config_options = {} schema = None source_format = onboarding_row["source_format"] - bronze_reader_options_json = onboarding_row["bronze_reader_options"] - if bronze_reader_options_json: - bronze_reader_config_options = self.__delete_none(bronze_reader_options_json.asDict()) + if source_format.lower() == "snapshot": + bronze_reader_config_options = {} + else: + bronze_reader_options_json = onboarding_row["bronze_reader_options"] + if bronze_reader_options_json: + bronze_reader_config_options = self.__delete_none(bronze_reader_options_json.asDict()) source_details_json = onboarding_row["source_details"] if source_details_json: source_details_file = self.__delete_none(source_details_json.asDict()) diff --git a/tests/resources/onboarding_applychanges_from_snapshot.json b/tests/resources/onboarding_applychanges_from_snapshot.json new file mode 100644 index 0000000..53cef5c --- /dev/null +++ b/tests/resources/onboarding_applychanges_from_snapshot.json @@ -0,0 +1,42 @@ +[ + { + "data_flow_id": "201", + "data_flow_group": "A1", + "source_system": "delta", + "source_format": "snapshot", + "source_details": { + "source_database": "products", + "source_table": "products", + "source_path_dev": "tests/resources/data/products" + }, + "bronze_database_dev": "uc_catalog_name.bronze", + "bronze_table": "products", + "bronze_table_path_dev": "tests/resources/delta/bronze/products", + "bronze_apply_changes_from_snapshot": { + "keys": [ + "product_id" + ], + "scd_type": "2" + } + }, + { + "data_flow_id": "202", + "data_flow_group": "A1", + "source_system": "delta", + "source_format": "snapshot", + "source_details": { + "source_database": "stores", + "source_table": "stores", + "source_path_dev": "tests/resources/data/stores" + }, + "bronze_database_dev": "uc_catalog_name.bronze", + "bronze_table": "stores", + "bronze_table_path_dev": "tests/resources/delta/bronze/stores", + "bronze_apply_changes_from_snapshot": { + "keys": [ + "store_id" + ], + "scd_type": "2" + } + } + ] \ No newline at end of file diff --git a/tests/test_dataflow_pipeline.py b/tests/test_dataflow_pipeline.py index 835edc1..f1047b4 100644 --- a/tests/test_dataflow_pipeline.py +++ b/tests/test_dataflow_pipeline.py @@ -37,6 +37,7 @@ class DataflowPipelineTests(DLTFrameworkTestCase): "schema": None, "partitionColumns": [""], "cdcApplyChanges": None, + "applyChangesFromSnapshot": None, "dataQualityExpectations": """{ "expect_or_drop": { "no_rescued_data": "_rescued_data IS NULL", diff --git a/tests/test_dataflow_spec.py b/tests/test_dataflow_spec.py index 4a8e3fe..34a7ac0 100644 --- a/tests/test_dataflow_spec.py +++ b/tests/test_dataflow_spec.py @@ -1,13 +1,15 @@ """Test DataflowSpec script.""" +import copy +import json from tests.utils import DLTFrameworkTestCase from src.dataflow_spec import ( DataflowSpecUtils, CDCApplyChanges, + ApplyChangesFromSnapshot, BronzeDataflowSpec, SilverDataflowSpec, ) from src.onboard_dataflowspec import OnboardDataflowspec -import copy class DataFlowSpecTests(DLTFrameworkTestCase): @@ -418,3 +420,53 @@ def test_populate_additional_df_cols(self): } result = DataflowSpecUtils.populate_additional_df_cols(row_dict, additional_columns) self.assertEqual(result, expected_result) + + def test_get_apply_changes_from_snapshot_positive(self): + """Test get_apply_changes_from_snapshot with positive values.""" + apply_changes_from_snapshot = """{ + "keys": ["id"], + "scd_type": "1", + "track_history_column_list": ["col1"], + "track_history_except_column_list": ["col2"] + }""" + result = DataflowSpecUtils.get_apply_changes_from_snapshot(apply_changes_from_snapshot) + self.assertEqual(type(result), ApplyChangesFromSnapshot) + self.assertEqual(result.keys, ["id"]) + self.assertEqual(result.scd_type, "1") + self.assertEqual(result.track_history_column_list, ["col1"]) + self.assertEqual(result.track_history_except_column_list, ["col2"]) + + def test_get_apply_changes_from_snapshot_missing_mandatory_keys(self): + """Test get_apply_changes_from_snapshot with missing mandatory keys.""" + apply_changes_from_snapshot = """{ + "scd_type": "1", + "track_history_column_list": ["col1"], + "track_history_except_column_list": ["col2"] + }""" + with self.assertRaises(Exception): + DataflowSpecUtils.get_apply_changes_from_snapshot(apply_changes_from_snapshot) + + def test_get_apply_changes_from_snapshot_missing_optional_keys(self): + """Test get_apply_changes_from_snapshot with missing optional keys.""" + apply_changes_from_snapshot = """{ + "keys": ["id"], + "scd_type": "1" + }""" + result = DataflowSpecUtils.get_apply_changes_from_snapshot(apply_changes_from_snapshot) + self.assertEqual(type(result), ApplyChangesFromSnapshot) + self.assertEqual(result.keys, ["id"]) + self.assertEqual(result.scd_type, "1") + self.assertEqual(result.track_history_column_list, None) + self.assertEqual(result.track_history_except_column_list, None) + + def test_get_apply_changes_from_snapshot_invalid_json(self): + """Test get_apply_changes_from_snapshot with invalid JSON.""" + apply_changes_from_snapshot = """{ + "keys": ["id"], + "scd_type": "1", + "track_history_column_list": ["col1", + "track_history_except_column_list": ["col2"] + }""" # Missing closing bracket for track_history_column_list + with self.assertRaises(json.JSONDecodeError): + DataflowSpecUtils.get_apply_changes_from_snapshot(apply_changes_from_snapshot) + diff --git a/tests/test_onboard_dataflowspec.py b/tests/test_onboard_dataflowspec.py index ff7fc54..bc0b605 100644 --- a/tests/test_onboard_dataflowspec.py +++ b/tests/test_onboard_dataflowspec.py @@ -343,3 +343,16 @@ def test_onboard_bronze_silver_with_v7(self): silver_dataflowSpec_df.show(truncate=False) self.assertEqual(bronze_dataflowSpec_df.count(), 3) self.assertEqual(silver_dataflowSpec_df.count(), 3) + + def test_onboard_apply_changes_from_snapshot_positive(self): + """Test for onboardDataflowspec.""" + onboarding_params_map = copy.deepcopy(self.onboarding_bronze_silver_params_map) + del onboarding_params_map["silver_dataflowspec_table"] + del onboarding_params_map["silver_dataflowspec_path"] + onboarding_params_map["onboarding_file_path"] = self.onboarding_apply_changes_from_snapshot_json_file + onboardDataFlowSpecs = OnboardDataflowspec(self.spark, onboarding_params_map) + onboardDataFlowSpecs.onboard_bronze_dataflow_spec() + bronze_dataflowSpec_df = self.read_dataflowspec( + self.onboarding_bronze_silver_params_map['database'], + self.onboarding_bronze_silver_params_map['bronze_dataflowspec_table']) + self.assertEqual(bronze_dataflowSpec_df.count(), 2) diff --git a/tests/test_pipeline_readers.py b/tests/test_pipeline_readers.py index 231c086..08572cb 100644 --- a/tests/test_pipeline_readers.py +++ b/tests/test_pipeline_readers.py @@ -41,6 +41,7 @@ class PipelineReadersTests(DLTFrameworkTestCase): "schema": None, "partitionColumns": [""], "cdcApplyChanges": None, + "applyChangesFromSnapshot": None, "dataQualityExpectations": None, "quarantineTargetDetails": None, "quarantineTableProperties": None, @@ -81,6 +82,7 @@ class PipelineReadersTests(DLTFrameworkTestCase): "schema": None, "partitionColumns": [""], "cdcApplyChanges": None, + "applyChangesFromSnapshot": None, "dataQualityExpectations": None, "quarantineTargetDetails": None, "quarantineTableProperties": None, @@ -120,6 +122,7 @@ class PipelineReadersTests(DLTFrameworkTestCase): "schema": None, "partitionColumns": [""], "cdcApplyChanges": None, + "applyChangesFromSnapshot": None, "dataQualityExpectations": None, "quarantineTargetDetails": None, "quarantineTableProperties": None, @@ -151,6 +154,7 @@ class PipelineReadersTests(DLTFrameworkTestCase): "schema": None, "partitionColumns": [""], "cdcApplyChanges": None, + "applyChangesFromSnapshot": None, "dataQualityExpectations": None, "quarantineTargetDetails": None, "quarantineTableProperties": None, diff --git a/tests/utils.py b/tests/utils.py index f3462cb..3249de8 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -41,6 +41,9 @@ def setUp(self): self.onboarding_bronze_type2_json_file = "tests/resources/onboarding_ac_bronze_type2.json" self.onboarding_append_flow_json_file = "tests/resources/onboarding_append_flow.json" self.onboarding_silver_fanout_json_file = "tests/resources/onboarding_silverfanout.json" + self.onboarding_apply_changes_from_snapshot_json_file = ( + "tests/resources/onboarding_applychanges_from_snapshot.json" + ) self.deltaPipelinesMetaStoreOps.drop_database("ravi_dlt_demo") self.deltaPipelinesMetaStoreOps.create_database("ravi_dlt_demo", "Unittest") self.onboarding_bronze_silver_params_map = { From 9b7b406a3b2701aed1605d300485856d3bd3daa4 Mon Sep 17 00:00:00 2001 From: ravi-databricks Date: Mon, 30 Sep 2024 13:03:15 -0700 Subject: [PATCH 12/59] Updated snapshot initializing exception handling --- src/dataflow_pipeline.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/dataflow_pipeline.py b/src/dataflow_pipeline.py index 831eb4b..c8dc69a 100644 --- a/src/dataflow_pipeline.py +++ b/src/dataflow_pipeline.py @@ -105,6 +105,9 @@ def __initialize_dataflow_pipeline( self.appy_changes_from_snapshot = DataflowSpecUtils.get_apply_changes_from_snapshot( self.dataflowSpec.applyChangesFromSnapshot ) + else: + if dataflow_spec.sourceFormat == "snapshot": + raise Exception(f"Snapshot reader function not provided for dataflowspec={dataflow_spec}!") if dataflow_spec.schema is not None: self.schema_json = json.loads(dataflow_spec.schema) else: @@ -186,7 +189,7 @@ def write(self): def write_bronze(self): """Write Bronze tables.""" bronze_dataflow_spec: BronzeDataflowSpec = self.dataflowSpec - if bronze_dataflow_spec.sourceFormat == "snapshot": + if bronze_dataflow_spec.sourceFormat and bronze_dataflow_spec.sourceFormat.lower() == "snapshot": if self.snapshot_reader_func: self.apply_changes_from_snapshot() else: From 437ed5682f1b464fbbb9fa3f1a9af2f5ffd6a6fc Mon Sep 17 00:00:00 2001 From: ravi-databricks Date: Mon, 30 Sep 2024 22:10:33 -0700 Subject: [PATCH 13/59] Corrected: 1.Removed cloud_provider variable from demo 2.Removed dbfs path from demos 3.Modified cloud_provider inside integration tests 4.Modified dbfs path inside inside integration tests --- demo/README.md | 15 +++++---------- demo/launch_af_cloudfiles_demo.py | 7 ++----- demo/launch_af_eventhub_demo.py | 3 +-- demo/launch_dais_demo.py | 8 +++----- demo/launch_silver_fanout_demo.py | 13 +++++-------- demo/launch_techsummit_demo.py | 6 ++---- docs/content/additionals/integration_tests.md | 4 ++-- integration_tests/README.md | 6 +++--- integration_tests/run_integration_tests.py | 10 +++++----- 9 files changed, 28 insertions(+), 44 deletions(-) diff --git a/demo/README.md b/demo/README.md index ad0db13..6e5b9e9 100644 --- a/demo/README.md +++ b/demo/README.md @@ -37,10 +37,9 @@ This Demo launches Bronze and Silver DLT pipelines with following activities: ``` 6. ```commandline - python demo/launch_dais_demo.py --uc_catalog_name=<> --cloud_provider_name=aws + python demo/launch_dais_demo.py --uc_catalog_name=<> ``` - uc_catalog_name : Unity catalog name - - cloud_provider_name : aws or azure or gcp - you can provide `--profile=databricks_profile name` in case you already have databricks cli otherwise command prompt will ask host and token. ![dais_demo.png](../docs/static/images/dais_demo.png) @@ -70,10 +69,9 @@ This demo will launch auto generated tables(100s) inside single bronze and silve ``` 6. ```commandline - python demo/launch_techsummit_demo.py --uc_catalog_name=<<>> --cloud_provider_name=<<>> + python demo/launch_techsummit_demo.py --uc_catalog_name=<<>> ``` - uc_catalog_name : Unity catalog name - - cloud_provider_name : aws or azure - you can provide `--profile=databricks_profile name` in case you already have databricks cli otherwise command prompt will ask host and token ![tech_summit_demo.png](../docs/static/images/tech_summit_demo.png) @@ -107,10 +105,9 @@ This demo will perform following tasks: ``` 6. ```commandline - python demo/launch_af_cloudfiles_demo.py --uc_catalog_name=<<>> --cloud_provider_name=<<>> + python demo/launch_af_cloudfiles_demo.py --uc_catalog_name=<<>> ``` - uc_catalog_name : Unity Catalog name - - cloud_provider_name : aws or azure - you can provide `--profile=databricks_profile name` in case you already have databricks cli otherwise command prompt will ask host and token ![af_am_demo.png](../docs/static/images/af_am_demo.png) @@ -154,7 +151,6 @@ This demo will perform following tasks: - Create databricks secrets to store producer and consumer keys using the scope created in step 2 - Following are the mandatory arguments for running EventHubs demo - - cloud_provider_name: Cloud provider name e.g. aws or azure - uc_catalog_name : unity catalog name e.g. ravi_dlt_meta_uc - eventhub_namespace: Eventhub namespace e.g. dltmeta - eventhub_name : Primary Eventhubname e.g. dltmeta_demo @@ -165,7 +161,7 @@ This demo will perform following tasks: - eventhub_port: Eventhub port 7. ```commandline - python3 demo/launch_af_eventhub_demo.py --uc_catalog_name=<<>> --cloud_provider_name=<<>> --eventhub_name=dltmeta_demo --eventhub_name_append_flow=dltmeta_demo_af --eventhub_secrets_scope_name=dltmeta_eventhub_creds --eventhub_namespace=dltmeta --eventhub_port=9093 --eventhub_producer_accesskey_name=RootManageSharedAccessKey --eventhub_consumer_accesskey_name=RootManageSharedAccessKey --eventhub_accesskey_secret_name=RootManageSharedAccessKey + python3 demo/launch_af_eventhub_demo.py --uc_catalog_name=<<>> --eventhub_name=dltmeta_demo --eventhub_name_append_flow=dltmeta_demo_af --eventhub_secrets_scope_name=dltmeta_eventhub_creds --eventhub_namespace=dltmeta --eventhub_port=9093 --eventhub_producer_accesskey_name=RootManageSharedAccessKey --eventhub_consumer_accesskey_name=RootManageSharedAccessKey --eventhub_accesskey_secret_name=RootManageSharedAccessKey ``` ![af_eh_demo.png](../docs/static/images/af_eh_demo.png) @@ -197,8 +193,7 @@ This demo will perform following tasks: ```commandline export PYTHONPATH=$dlt_meta_home -6. Run the command ```python demo/launch_silver_fanout_demo.py --source=cloudfiles --uc_catalog_name=<> --cloud_provider_name=aws --dbr_version=15.3.x-scala2.12 --dbfs_path=dbfs:/dais-dlt-meta-silver-fanout``` - - cloud_provider_name : aws or azure +6. Run the command ```python demo/launch_silver_fanout_demo.py --source=cloudfiles --uc_catalog_name=<> --dbr_version=15.3.x-scala2.12 --dbfs_path=dbfs:/dais-dlt-meta-silver-fanout``` - db_version : Databricks Runtime Version - dbfs_path : Path on your Databricks workspace where demo will be copied for launching DLT-META Pipelines - you can provide `--profile=databricks_profile name` in case you already have databricks cli otherwise command prompt will ask host and token. diff --git a/demo/launch_af_cloudfiles_demo.py b/demo/launch_af_cloudfiles_demo.py index b6cfc59..db01b60 100644 --- a/demo/launch_af_cloudfiles_demo.py +++ b/demo/launch_af_cloudfiles_demo.py @@ -4,7 +4,6 @@ from integration_tests.run_integration_tests import ( DLTMETARunner, DLTMetaRunnerConf, - cloud_node_type_id_dict, get_workspace_api_client, process_arguments ) @@ -51,7 +50,6 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: silver_schema=f"dlt_meta_silver_demo_{run_id}", runners_nb_path=f"/Users/{self.wsi._my_username}/dlt_meta_demo/{run_id}", source="cloudfiles", - node_type_id=cloud_node_type_id_dict[self.args.__dict__['cloud_provider_name']], cloudfiles_template="demo/conf/cloudfiles-onboarding.template", cloudfiles_A2_template="demo/conf/cloudfiles-onboarding_A2.template", onboarding_file_path="demo/conf/onboarding.json", @@ -69,12 +67,11 @@ def launch_workflow(self, runner_conf: DLTMetaRunnerConf): afam_args_map = { "--profile": "provide databricks cli profile name, if not provide databricks_host and token", - "--uc_catalog_name": "provide databricks uc_catalog name, this is required to create volume, schema, table", - "--cloud_provider_name": "provide cloud provider name. Supported values are aws , azure , gcp" + "--uc_catalog_name": "provide databricks uc_catalog name, this is required to create volume, schema, table" } afam_mandatory_args = [ - "uc_catalog_name", "cloud_provider_name"] + "uc_catalog_name"] def main(): diff --git a/demo/launch_af_eventhub_demo.py b/demo/launch_af_eventhub_demo.py index 16c2009..57d04b6 100644 --- a/demo/launch_af_eventhub_demo.py +++ b/demo/launch_af_eventhub_demo.py @@ -66,7 +66,6 @@ def launch_workflow(self, runner_conf: DLTMetaRunnerConf): afam_args_map = { "--profile": "provide databricks cli profile name, if not provide databricks_host and token", "--uc_catalog_name": "provide databricks uc_catalog name, this is required to create volume, schema, table", - "--cloud_provider_name": "provide cloud provider name. Supported values are aws , azure , gcp", "--eventhub_name": "Provide eventhub_name e.g --eventhub_name=iot", "--eventhub_name_append_flow": "Provide eventhub_name_append_flow e.g --eventhub_name_append_flow=iot_af", "--eventhub_producer_accesskey_name": "Provide access key that has write permission on the eventhub", @@ -79,7 +78,7 @@ def launch_workflow(self, runner_conf: DLTMetaRunnerConf): "--eventhub_port": "Provide eventhub_port e.g --eventhub_port=9093", } -afeh_mandatory_args = ["uc_catalog_name", "cloud_provider_name", "eventhub_name", +afeh_mandatory_args = ["uc_catalog_name", "eventhub_name", "eventhub_name_append_flow", "eventhub_producer_accesskey_name", "eventhub_consumer_accesskey_name", "eventhub_secrets_scope_name", "eventhub_namespace", "eventhub_port"] diff --git a/demo/launch_dais_demo.py b/demo/launch_dais_demo.py index c27e291..06251c5 100644 --- a/demo/launch_dais_demo.py +++ b/demo/launch_dais_demo.py @@ -5,7 +5,6 @@ from integration_tests.run_integration_tests import ( DLTMETARunner, DLTMetaRunnerConf, - cloud_node_type_id_dict, get_workspace_api_client, process_arguments ) @@ -43,7 +42,7 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: silver_schema=f"dlt_meta_silver_dais_demo_{run_id}", runners_nb_path=f"/Users/{self._my_username(self.ws)}/dlt_meta_dais_demo/{run_id}", node_type_id=cloud_node_type_id_dict[self.args.__dict__['cloud_provider_name']], - dbr_version=self.args.__dict__['dbr_version'], + # dbr_version=self.args.__dict__['dbr_version'], cloudfiles_template="demo/conf/onboarding.template", env="prod", source="cloudFiles", @@ -181,11 +180,10 @@ def create_daisdemo_workflow(self, runner_conf: DLTMetaRunnerConf): dais_args_map = {"--profile": "provide databricks cli profile name, if not provide databricks_host and token", "--uc_catalog_name": "provide databricks uc_catalog name, \ this is required to create volume, schema, table", - "--cloud_provider_name": "provide cloud provider name. Supported values are aws , azure , gcp", - "--dbr_version": "Provide databricks runtime spark version e.g 15.3.x-scala2.12" + "--cloud_provider_name": "provide cloud provider name. Supported values are aws , azure , gcp" } -dais_mandatory_args = ["uc_catalog_name", "cloud_provider_name", "dbr_version"] +dais_mandatory_args = ["uc_catalog_name", "cloud_provider_name"] def main(): diff --git a/demo/launch_silver_fanout_demo.py b/demo/launch_silver_fanout_demo.py index 2239e2d..f47eb04 100644 --- a/demo/launch_silver_fanout_demo.py +++ b/demo/launch_silver_fanout_demo.py @@ -6,7 +6,6 @@ from integration_tests.run_integration_tests import ( DLTMETARunner, DLTMetaRunnerConf, - cloud_node_type_id_dict, get_workspace_api_client, process_arguments ) @@ -72,8 +71,8 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: runners_nb_path=f"/Users/{self.wsi._my_username}/dlt_meta_fout_demo/{run_id}", runners_full_local_path='./demo/dbc/silver_fout_runners.dbc', source="cloudFiles", - node_type_id=cloud_node_type_id_dict[self.args.__dict__['cloud_provider_name']], - dbr_version=self.args.__dict__['dbr_version'], + # node_type_id=cloud_node_type_id_dict[self.args.__dict__['cloud_provider_name']], + # dbr_version=self.args.__dict__['dbr_version'], cloudfiles_template="demo/conf/onboarding_cars.template", onboarding_fanout_templates="demo/conf/onboarding_fanout_cars.template", onboarding_file_path="demo/conf/onboarding_cars.json", @@ -142,7 +141,7 @@ def create_sfo_workflow_spec(self, runner_conf: DLTMetaRunnerConf): description="Sets up metadata tables for DLT-META", depends_on=[jobs.TaskDependency(task_key="onboarding_job")], # existing_cluster_id=runner_conf.cluster_id, - environment_key="dlt_meta_env", + environment_key="dl_meta_sfo_demo_env", timeout_seconds=0, python_wheel_task=jobs.PythonWheelTask( package_name="dlt_meta", @@ -182,12 +181,10 @@ def create_sfo_workflow_spec(self, runner_conf: DLTMetaRunnerConf): sfo_args_map = { "--profile": "provide databricks cli profile name, if not provide databricks_host and token", - "--uc_catalog_name": "provide databricks uc_catalog name, this is required to create volume, schema, table", - "--cloud_provider_name": "provide cloud provider name. Supported values are aws , azure , gcp", - "--dbr_version": "Provide databricks runtime spark version e.g 15.3.x-scala2.12" + "--uc_catalog_name": "provide databricks uc_catalog name, this is required to create volume, schema, table" } -sfo_mandatory_args = ["uc_catalog_name", "cloud_provider_name", "dbr_version"] +sfo_mandatory_args = ["uc_catalog_name"] def main(): diff --git a/demo/launch_techsummit_demo.py b/demo/launch_techsummit_demo.py index 5929224..02be07b 100644 --- a/demo/launch_techsummit_demo.py +++ b/demo/launch_techsummit_demo.py @@ -32,7 +32,6 @@ from integration_tests.run_integration_tests import ( DLTMETARunner, DLTMetaRunnerConf, - cloud_node_type_id_dict, get_workspace_api_client, process_arguments ) @@ -86,7 +85,7 @@ def init_runner_conf(self) -> TechsummitRunnerConf: silver_schema=f"dlt_meta_silver_demo_{run_id}", runners_full_local_path='./demo/dbc/tech_summit_dlt_meta_runners.dbc', runners_nb_path=f"/Users/{self._my_username(self.ws)}/dlt_meta_techsummit_demo/{run_id}", - node_type_id=cloud_node_type_id_dict[self.args.__dict__['cloud_provider_name']], + # node_type_id=cloud_node_type_id_dict[self.args.__dict__['cloud_provider_name']], env="prod", table_count=self.args.__dict__['table_count'] if self.args.__dict__['table_count'] else "100", table_column_count=(self.args.__dict__['table_column_count'] if self.args.__dict__['table_column_count'] @@ -251,14 +250,13 @@ def create_techsummit_demo_workflow(self, runner_conf: TechsummitRunnerConf): techsummit_args_map = {"--profile": "provide databricks cli profile name, if not provide databricks_host and token", "--uc_catalog_name": "provide databricks uc_catalog name, \ this is required to create volume, schema, table", - "--cloud_provider_name": "cloud_provider_name", "--worker_nodes": "worker_nodes", "--table_count": "table_count", "--table_column_count": "table_column_count", "--table_data_rows_count": "table_data_rows_count" } -techsummit_mandatory_args = ["uc_catalog_name", "cloud_provider_name"] +techsummit_mandatory_args = ["uc_catalog_name"] def main(): diff --git a/docs/content/additionals/integration_tests.md b/docs/content/additionals/integration_tests.md index 1a16ed4..03b06ce 100644 --- a/docs/content/additionals/integration_tests.md +++ b/docs/content/additionals/integration_tests.md @@ -36,7 +36,7 @@ draft: false ``` 2. Run integration test against cloudfile or eventhub or kafka using below options: If databricks profile configured using CLI then pass ```--profile ``` to below command otherwise provide workspace url and token in command line - - 2a. Run the command for cloudfiles ```python integration-tests/run_integration_tests.py --cloud_provider_name=aws --dbr_version=15.3.x-scala2.12 --source=cloudfiles --dbfs_path=dbfs:/tmp/DLT-META/``` + - 2a. Run the command for cloudfiles ```python integration_tests/run_integration_tests.py --source=cloudfiles uc_catalog_name=<<>>``` - 2b. Run the command for eventhub ```python integration-tests/run_integration_tests.py --cloud_provider_name=azure --dbr_version=15.3.x-scala2.12 --source=eventhub --dbfs_path=dbfs:/tmp/DLT-META/ --eventhub_name=iot --eventhub_secrets_scope_name=eventhubs_creds --eventhub_namespace=int_test-standard --eventhub_port=9093 --eventhub_producer_accesskey_name=producer --eventhub_consumer_accesskey_name=consumer``` @@ -54,7 +54,7 @@ draft: false 6. Provide eventhub access key name : --eventhub_consumer_accesskey_name - - 2c. Run the command for kafka ```python3 integration-tests/run_integration_tests.py --cloud_provider_name=aws --dbr_version=15.3.x-scala2.12 --source=kafka --dbfs_path=dbfs:/tmp/DLT-META/ --kafka_topic_name=dlt-meta-integration-test --kafka_broker=host:9092``` + - 2c. Run the command for kafka ```python3 integration_tests/run_integration_tests.py --cloud_provider_name=aws --dbr_version=15.3.x-scala2.12 --source=kafka --dbfs_path=dbfs:/tmp/DLT-META/ --kafka_topic_name=dlt-meta-integration-test --kafka_broker=host:9092``` - - For kafka integration tests, the following are the prerequisites: 1. Needs kafka instance running diff --git a/integration_tests/README.md b/integration_tests/README.md index 73a7d09..2724732 100644 --- a/integration_tests/README.md +++ b/integration_tests/README.md @@ -37,12 +37,12 @@ 9. Run integration test against cloudfile or eventhub or kafka using below options: If databricks profile configured using CLI then pass ```--profile ``` to below command otherwise provide workspace url and token in command line - 9a. Run the command for cloudfiles ```commandline - python integration-tests/run_integration_tests.py --uc_catalog_name= --cloud_provider_name=<<>> --source=cloudfiles + python integration_tests/run_integration_tests.py --uc_catalog_name= --source=cloudfiles ``` - 9b. Run the command for eventhub ```commandline - python integration-tests/run_integration_tests.py --uc_catalog_name= --cloud_provider_name=<<>> --source=eventhub --eventhub_name=iot --eventhub_secrets_scope_name=eventhubs_creds --eventhub_namespace=int_test-standard --eventhub_port=9093 --eventhub_producer_accesskey_name=producer --eventhub_consumer_accesskey_name=consumer + python integration_tests/run_integration_tests.py --uc_catalog_name=<<>> --source=eventhub --eventhub_name=iot --eventhub_secrets_scope_name=eventhubs_creds --eventhub_namespace=int_test-standard --eventhub_port=9093 --eventhub_producer_accesskey_name=producer --eventhub_consumer_accesskey_name=consumer ``` - - For eventhub integration tests, the following are the prerequisites: @@ -61,7 +61,7 @@ - 9c. Run the command for kafka ```commandline - python3 integration-tests/run_integration_tests.py --uc_catalog_name= --cloud_provider_name=<<>> --source=kafka --kafka_topic_name=dlt-meta-integration-test --kafka_broker=host:9092 + python3 integration_tests/run_integration_tests.py --uc_catalog_name=<<>> --source=kafka --kafka_topic_name=dlt-meta-integration-test --kafka_broker=host:9092 ``` - - For kafka integration tests, the following are the prerequisites: diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index 763ae62..9ad6a87 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -162,15 +162,15 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: runner_conf = DLTMetaRunnerConf( run_id=run_id, username=self.wsi._my_username, - dbfs_tmp_path=f"{self.args.__dict__['dbfs_path']}/{run_id}", + dbfs_tmp_path=f"{self.args.__dict__.get('dbfs_path', None)}/{run_id}", int_tests_dir="file:./integration_tests", dlt_meta_schema=f"dlt_meta_dataflowspecs_it_{run_id}", bronze_schema=f"dlt_meta_bronze_it_{run_id}", silver_schema=f"dlt_meta_silver_it_{run_id}", runners_nb_path=f"/Users/{self.wsi._my_username}/dlt_meta_int_tests/{run_id}", source=self.args.__dict__['source'], - node_type_id=cloud_node_type_id_dict[self.args.__dict__['cloud_provider_name']], - dbr_version=self.args.__dict__['dbr_version'], + node_type_id=cloud_node_type_id_dict[self.args.__dict__.get('cloud_provider_name', None)], + dbr_version=self.args.__dict__.get('dbr_version', None), cloudfiles_template="integration_tests/conf/cloudfiles-onboarding.template", cloudfiles_A2_template="integration_tests/conf/cloudfiles-onboarding_A2.template", eventhub_template="integration_tests/conf/eventhub-onboarding.template", @@ -1125,8 +1125,8 @@ def process_arguments(args_map, mandatory_args): check_mandatory_arg(args, mandatory_args) supported_cloud_providers = ["aws", "azure", "gcp"] - cloud_provider_name = args.__getattribute__("cloud_provider_name") - if cloud_provider_name.lower() not in supported_cloud_providers: + cloud_provider_name = args.__getattribute__("cloud_provider_name") if args.__contains__("cloud_provider_name") else None + if cloud_provider_name and cloud_provider_name.lower() not in supported_cloud_providers: raise Exception("Invalid value for --cloud_provider_name! Supported values are aws, azure, gcp") return args From 7a6b28f76deb68ed08f3b51073012bce6d8bbb5a Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Tue, 1 Oct 2024 14:08:29 -0400 Subject: [PATCH 14/59] added integration test outputs to gitinore --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 31759d8..b66d8f1 100644 --- a/.gitignore +++ b/.gitignore @@ -154,6 +154,6 @@ deployment-merged.yaml .databricks .databricks-login.json demo/conf/onboarding.json -integration_tests/conf/onboarding.json +integration_tests/conf/onboarding*.json databricks.yaml - +integration_test_output*.csv From a75ccba243cb4a46e1e08d78dfd8ebd651a18e1a Mon Sep 17 00:00:00 2001 From: ravi-databricks Date: Tue, 1 Oct 2024 13:27:21 -0700 Subject: [PATCH 15/59] Corrected dataflowpipeline apply_from_snapshot api --- src/dataflow_pipeline.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/dataflow_pipeline.py b/src/dataflow_pipeline.py index c8dc69a..11df12d 100644 --- a/src/dataflow_pipeline.py +++ b/src/dataflow_pipeline.py @@ -141,7 +141,8 @@ def read(self): comment=f"input dataset view for {self.view_name}", ) else: - raise Exception("Dataflow read not supported for {}".format(type(self.dataflowSpec))) + if not self.snapshot_reader_func: + raise Exception("Dataflow read not supported for {}".format(type(self.dataflowSpec))) if self.appendFlows: self.read_append_flows() @@ -320,9 +321,9 @@ def apply_changes_from_snapshot(self): dlt.apply_changes_from_snapshot( target=f"{self.dataflowSpec.targetDetails['table']}", snapshot_and_version=self.snapshot_reader_func, - keys=self.self.appy_changes_from_snapshot.keys, - stored_as_scd_type=self.self.appy_changes_from_snapshot.scd_type, - track_history_column_list=self.self.appy_changes_from_snapshot.track_history_column_list, + keys=self.appy_changes_from_snapshot.keys, + stored_as_scd_type=self.appy_changes_from_snapshot.scd_type, + track_history_column_list=self.appy_changes_from_snapshot.track_history_column_list, ) def write_bronze_with_dqe(self): From 8fc401db59ba01a0ef1a483651e5a80bf003ab34 Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Tue, 1 Oct 2024 17:02:17 -0400 Subject: [PATCH 16/59] uc catalog name consistency in sample code --- demo/README.md | 6 +++--- integration_tests/README.md | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/demo/README.md b/demo/README.md index 6e5b9e9..5504d5b 100644 --- a/demo/README.md +++ b/demo/README.md @@ -69,7 +69,7 @@ This demo will launch auto generated tables(100s) inside single bronze and silve ``` 6. ```commandline - python demo/launch_techsummit_demo.py --uc_catalog_name=<<>> + python demo/launch_techsummit_demo.py --uc_catalog_name=<> ``` - uc_catalog_name : Unity catalog name - you can provide `--profile=databricks_profile name` in case you already have databricks cli otherwise command prompt will ask host and token @@ -105,7 +105,7 @@ This demo will perform following tasks: ``` 6. ```commandline - python demo/launch_af_cloudfiles_demo.py --uc_catalog_name=<<>> + python demo/launch_af_cloudfiles_demo.py --uc_catalog_name=<> ``` - uc_catalog_name : Unity Catalog name - you can provide `--profile=databricks_profile name` in case you already have databricks cli otherwise command prompt will ask host and token @@ -161,7 +161,7 @@ This demo will perform following tasks: - eventhub_port: Eventhub port 7. ```commandline - python3 demo/launch_af_eventhub_demo.py --uc_catalog_name=<<>> --eventhub_name=dltmeta_demo --eventhub_name_append_flow=dltmeta_demo_af --eventhub_secrets_scope_name=dltmeta_eventhub_creds --eventhub_namespace=dltmeta --eventhub_port=9093 --eventhub_producer_accesskey_name=RootManageSharedAccessKey --eventhub_consumer_accesskey_name=RootManageSharedAccessKey --eventhub_accesskey_secret_name=RootManageSharedAccessKey + python3 demo/launch_af_eventhub_demo.py --uc_catalog_name=<> --eventhub_name=dltmeta_demo --eventhub_name_append_flow=dltmeta_demo_af --eventhub_secrets_scope_name=dltmeta_eventhub_creds --eventhub_namespace=dltmeta --eventhub_port=9093 --eventhub_producer_accesskey_name=RootManageSharedAccessKey --eventhub_consumer_accesskey_name=RootManageSharedAccessKey --eventhub_accesskey_secret_name=RootManageSharedAccessKey ``` ![af_eh_demo.png](../docs/static/images/af_eh_demo.png) diff --git a/integration_tests/README.md b/integration_tests/README.md index 2724732..ed4c4ae 100644 --- a/integration_tests/README.md +++ b/integration_tests/README.md @@ -34,15 +34,16 @@ export PYTHONPATH=$dlt_meta_home ``` -9. Run integration test against cloudfile or eventhub or kafka using below options: If databricks profile configured using CLI then pass ```--profile ``` to below command otherwise provide workspace url and token in command line +9. Run integration test against cloudfile or eventhub or kafka using below options. To use the Databricks profile configured using CLI then pass ```--profile ``` to below command otherwise provide workspace url and token in command line. You will also need to provide a Unity Catalog catalog for which the schemas, tables, and files will be created in. + - 9a. Run the command for cloudfiles ```commandline - python integration_tests/run_integration_tests.py --uc_catalog_name= --source=cloudfiles + python integration_tests/run_integration_tests.py --uc_catalog_name=<> --source=cloudfiles ``` - 9b. Run the command for eventhub ```commandline - python integration_tests/run_integration_tests.py --uc_catalog_name=<<>> --source=eventhub --eventhub_name=iot --eventhub_secrets_scope_name=eventhubs_creds --eventhub_namespace=int_test-standard --eventhub_port=9093 --eventhub_producer_accesskey_name=producer --eventhub_consumer_accesskey_name=consumer + python integration_tests/run_integration_tests.py --uc_catalog_name=<> --source=eventhub --eventhub_name=iot --eventhub_secrets_scope_name=eventhubs_creds --eventhub_namespace=int_test-standard --eventhub_port=9093 --eventhub_producer_accesskey_name=producer --eventhub_consumer_accesskey_name=consumer ``` - - For eventhub integration tests, the following are the prerequisites: From 04adc91cf111c02470c5d0b2383c97b4c91e19c2 Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Wed, 2 Oct 2024 13:55:49 -0400 Subject: [PATCH 17/59] updated runner notebook upload to .py files from .dbc so updates can be made directly in editor and don't need to be modified in Databricks and downloaded as .dbc files --- integration_tests/README.md | 26 +++--- integration_tests/dbc/cloud_files_runners.dbc | Bin 3061 -> 0 bytes integration_tests/dbc/eventhub_runners.dbc | Bin 5450 -> 0 bytes integration_tests/dbc/kafka_runners.dbc | Bin 4824 -> 0 bytes .../init_dlt_meta_pipeline.py | 10 +++ .../notebooks/cloudfile_runners/validate.py | 48 +++++++++++ .../init_dlt_meta_pipeline.py | 10 +++ .../eventhub_runners/publish_events.py | 76 ++++++++++++++++++ .../notebooks/eventhub_runners/validate.py | 38 +++++++++ .../kafka_runners/init_dlt_meta_pipeline.py | 10 +++ .../notebooks/kafka_runners/publish_events.py | 32 ++++++++ .../notebooks/kafka_runners/validate.py | 32 ++++++++ integration_tests/run_integration_tests.py | 56 ++++++++----- 13 files changed, 306 insertions(+), 32 deletions(-) delete mode 100644 integration_tests/dbc/cloud_files_runners.dbc delete mode 100644 integration_tests/dbc/eventhub_runners.dbc delete mode 100644 integration_tests/dbc/kafka_runners.dbc create mode 100644 integration_tests/notebooks/cloudfile_runners/init_dlt_meta_pipeline.py create mode 100644 integration_tests/notebooks/cloudfile_runners/validate.py create mode 100644 integration_tests/notebooks/eventhub_runners/init_dlt_meta_pipeline.py create mode 100644 integration_tests/notebooks/eventhub_runners/publish_events.py create mode 100644 integration_tests/notebooks/eventhub_runners/validate.py create mode 100644 integration_tests/notebooks/kafka_runners/init_dlt_meta_pipeline.py create mode 100644 integration_tests/notebooks/kafka_runners/publish_events.py create mode 100644 integration_tests/notebooks/kafka_runners/validate.py diff --git a/integration_tests/README.md b/integration_tests/README.md index ed4c4ae..ebc56d7 100644 --- a/integration_tests/README.md +++ b/integration_tests/README.md @@ -1,7 +1,7 @@ #### Run Integration Tests 1. Install [Databricks CLI](https://docs.databricks.com/dev-tools/cli/index.html) - Once you install Databricks CLI, authenticate your current machine to a Databricks Workspace: - + ```commandline databricks auth login --host WORKSPACE_HOST ``` @@ -29,27 +29,27 @@ 7. ```commandline dlt_meta_home=$(pwd) ``` - + 8. ```commandline export PYTHONPATH=$dlt_meta_home ``` 9. Run integration test against cloudfile or eventhub or kafka using below options. To use the Databricks profile configured using CLI then pass ```--profile ``` to below command otherwise provide workspace url and token in command line. You will also need to provide a Unity Catalog catalog for which the schemas, tables, and files will be created in. - - 9a. Run the command for cloudfiles - ```commandline - python integration_tests/run_integration_tests.py --uc_catalog_name=<> --source=cloudfiles + - 9a. Run the command for cloudfiles + ```commandline + python integration_tests/run_integration_tests.py --uc_catalog_name=<> --source=cloudfiles --cloud_provider_name=aws --profile=<> ``` - - 9b. Run the command for eventhub - ```commandline - python integration_tests/run_integration_tests.py --uc_catalog_name=<> --source=eventhub --eventhub_name=iot --eventhub_secrets_scope_name=eventhubs_creds --eventhub_namespace=int_test-standard --eventhub_port=9093 --eventhub_producer_accesskey_name=producer --eventhub_consumer_accesskey_name=consumer + - 9b. Run the command for eventhub + ```commandline + python integration_tests/run_integration_tests.py --uc_catalog_name=<> --source=eventhub --eventhub_name=iot --eventhub_secrets_scope_name=eventhubs_creds --eventhub_namespace=int_test-standard --eventhub_port=9093 --eventhub_producer_accesskey_name=producer --eventhub_consumer_accesskey_name=consumer --eventhub_name_append_flow=TODO? --eventhub_accesskey_secret_name=TODO? ``` - - For eventhub integration tests, the following are the prerequisites: 1. Needs eventhub instance running 2. Use Databricks CLI, Create databricks secrets scope for eventhub keys (```databricks secrets create-scope eventhubs_creds```) - 3. Use Databricks CLI, Create databricks secrets to store producer and consumer keys using the scope created in step + 3. Use Databricks CLI, Create databricks secrets to store producer and consumer keys using the scope created in step - - Following are the mandatory arguments for running EventHubs integration test 1. Provide your eventhub topic : --eventhub_name @@ -60,9 +60,9 @@ 6. Provide eventhub access key name : --eventhub_consumer_accesskey_name - - 9c. Run the command for kafka + - 9c. Run the command for kafka ```commandline - python3 integration_tests/run_integration_tests.py --uc_catalog_name=<<>> --source=kafka --kafka_topic_name=dlt-meta-integration-test --kafka_broker=host:9092 + python integration_tests/run_integration_tests.py --uc_catalog_name=<> --source=kafka --kafka_topic_name=dlt-meta-integration-test --kafka_broker=host:9092 --cloud_provider_name=aws --profile=DEFAULT ``` - - For kafka integration tests, the following are the prerequisites: @@ -72,10 +72,10 @@ 1. Provide your kafka topic name : --kafka_topic_name 2. Provide kafka_broker : --kafka_broker -10. Once finished integration output file will be copied locally to +10. Once finished integration output file will be copied locally to ```integration-test-output_.txt``` -11. Output of a successful run should have the following in the file +11. Output of a successful run should have the following in the file ``` ,0 0,Completed Bronze DLT Pipeline. diff --git a/integration_tests/dbc/cloud_files_runners.dbc b/integration_tests/dbc/cloud_files_runners.dbc deleted file mode 100644 index 09019cc173507b75db6d62ada65f38aa5958c68f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3061 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 Q0Em9AfM4&|MF0ErU!tbA4FCWD diff --git a/integration_tests/dbc/kafka_runners.dbc b/integration_tests/dbc/kafka_runners.dbc deleted file mode 100644 index 6746a1e3b571cd5793a922983c7ad1f83b65215d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4824 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 W1gH2ZY5)Mm&kOT&S|*IYM*jf?0*jUa diff --git a/integration_tests/notebooks/cloudfile_runners/init_dlt_meta_pipeline.py b/integration_tests/notebooks/cloudfile_runners/init_dlt_meta_pipeline.py new file mode 100644 index 0000000..f69fdd1 --- /dev/null +++ b/integration_tests/notebooks/cloudfile_runners/init_dlt_meta_pipeline.py @@ -0,0 +1,10 @@ +# Databricks notebook source +dlt_meta_whl = spark.conf.get("dlt_meta_whl") +%pip install $dlt_meta_whl + +# COMMAND ---------- + +layer = spark.conf.get("layer", None) + +from src.dataflow_pipeline import DataflowPipeline +DataflowPipeline.invoke_dlt_pipeline(spark, layer) diff --git a/integration_tests/notebooks/cloudfile_runners/validate.py b/integration_tests/notebooks/cloudfile_runners/validate.py new file mode 100644 index 0000000..93f6075 --- /dev/null +++ b/integration_tests/notebooks/cloudfile_runners/validate.py @@ -0,0 +1,48 @@ +# Databricks notebook source +import pandas as pd + +run_id = dbutils.widgets.get("run_id") +uc_enabled = eval(dbutils.widgets.get("uc_enabled")) +uc_catalog_name = dbutils.widgets.get("uc_catalog_name") +bronze_schema = dbutils.widgets.get("bronze_schema") +silver_schema = dbutils.widgets.get("silver_schema") +output_file_path = dbutils.widgets.get("output_file_path") +log_list = [] + +# Assumption is that to get to this notebook Bronze and Silver completed successfully +log_list.append("Completed Bronze DLT Pipeline.") +log_list.append("Completed Silver DLT Pipeline.") + +UC_TABLES = { + f"{uc_catalog_name}.{bronze_schema}.transactions": 10002, + f"{uc_catalog_name}.{bronze_schema}.transactions_quarantine": 6, + f"{uc_catalog_name}.{bronze_schema}.customers": 51453, + f"{uc_catalog_name}.{bronze_schema}.customers_quarantine": 256, + f"{uc_catalog_name}.{silver_schema}.transactions": 8759, + f"{uc_catalog_name}.{silver_schema}.customers": 73212, +} + +NON_UC_TABLES = { + f"{bronze_schema}.transactions": 10002, + f"{bronze_schema}.transactions_quarantine": 6, + f"{bronze_schema}.customers": 51453, + f"{bronze_schema}.customers_quarantine": 256, + f"{silver_schema}.transactions": 8759, + f"{silver_schema}.customers": 73212, +} + +log_list.append("Validating DLT Bronze and Silver Table Counts...") +tables = UC_TABLES if uc_enabled else NON_UC_TABLES +for table, counts in tables.items(): + query = spark.sql(f"SELECT count(*) as cnt FROM {table}") + cnt = query.collect()[0].cnt + + log_list.append(f"Validating Counts for Table {table}.") + try: + assert int(cnt) == counts + log_list.append(f"Expected: {counts} Actual: {cnt}. Passed!") + except AssertionError: + log_list.append(f"Expected: {counts} Actual: {cnt}. Failed!") + +pd_df = pd.DataFrame(log_list) +pd_df.to_csv(output_file_path) diff --git a/integration_tests/notebooks/eventhub_runners/init_dlt_meta_pipeline.py b/integration_tests/notebooks/eventhub_runners/init_dlt_meta_pipeline.py new file mode 100644 index 0000000..e1e579b --- /dev/null +++ b/integration_tests/notebooks/eventhub_runners/init_dlt_meta_pipeline.py @@ -0,0 +1,10 @@ +# Databricks notebook source +dlt_meta_whl = spark.conf.get("dlt_meta_whl") +%pip install $dlt_meta_whl + +# COMMAND ---------- + +layer = spark.conf.get("layer", None) + +from src.dataflow_pipeline import DataflowPipeline +DataflowPipeline.invoke_dlt_pipeline(spark, layer) \ No newline at end of file diff --git a/integration_tests/notebooks/eventhub_runners/publish_events.py b/integration_tests/notebooks/eventhub_runners/publish_events.py new file mode 100644 index 0000000..6f3aa2b --- /dev/null +++ b/integration_tests/notebooks/eventhub_runners/publish_events.py @@ -0,0 +1,76 @@ +# Databricks notebook source +# MAGIC %md +# MAGIC ## Install azure-eventhub + +# COMMAND ---------- + +# MAGIC %sh pip install azure-eventhub + +# COMMAND ---------- + +dbutils.library.restartPython() + +# COMMAND ---------- + +dbutils.widgets.text("eventhub_name","eventhub_name","") +dbutils.widgets.text("eventhub_name_append_flow","eventhub_name_append_flow","") +dbutils.widgets.text("eventhub_namespace","eventhub_namespace","") +dbutils.widgets.text("eventhub_secrets_scope_name","eventhub_secrets_scope_name","") +dbutils.widgets.text("eventhub_accesskey_name","eventhub_accesskey_name","") +dbutils.widgets.text("eventhub_input_data","eventhub_input_data","") +dbutils.widgets.text("eventhub_append_flow_input_data","eventhub_append_flow_input_data","") + +# COMMAND ---------- + +eventhub_name = dbutils.widgets.get("eventhub_name") +eventhub_name_append_flow = dbutils.widgets.get("eventhub_name_append_flow") +eventhub_namespace = dbutils.widgets.get("eventhub_namespace") +eventhub_secrets_scope_name = dbutils.widgets.get("eventhub_secrets_scope_name") +eventhub_accesskey_name = dbutils.widgets.get("eventhub_accesskey_name") +eventhub_input_data = dbutils.widgets.get("eventhub_input_data") +eventhub_append_flow_input_data = dbutils.widgets.get("eventhub_append_flow_input_data") + +# COMMAND ---------- + +print(f"eventhub_name={eventhub_name}, eventhub_name_append_flow={eventhub_name_append_flow}, eventhub_namespace={eventhub_namespace}, eventhub_secrets_scope_name={eventhub_secrets_scope_name}, eventhub_accesskey_name={eventhub_accesskey_name}, eventhub_input_data={eventhub_input_data}, eventhub_append_flow_input_data={eventhub_append_flow_input_data}") + +# COMMAND ---------- + +import json +from azure.eventhub import EventHubProducerClient, EventData + +eventhub_shared_access_value = dbutils.secrets.get(scope = eventhub_secrets_scope_name, key = eventhub_accesskey_name) +eventhub_conn = f"Endpoint=sb://{eventhub_namespace}.servicebus.windows.net/;SharedAccessKeyName={eventhub_accesskey_name};SharedAccessKey={eventhub_shared_access_value}" + +client = EventHubProducerClient.from_connection_string(eventhub_conn, eventhub_name=eventhub_name) + + + +# COMMAND ---------- + +# MAGIC %md +# MAGIC ## Publish iot data to eventhub + +# COMMAND ---------- + +with open(f"{eventhub_input_data}") as f: + data = json.load(f) + +for event in data: + event_data_batch = client.create_batch() + event_data_batch.add(EventData(json.dumps(event))) + with client: + client.send_batch(event_data_batch) + +# COMMAND ---------- + +append_flow_client = EventHubProducerClient.from_connection_string(eventhub_conn, eventhub_name=eventhub_name_append_flow) + +with open(f"{eventhub_append_flow_input_data}") as f: + af_data = json.load(f) + +for event in af_data: + event_data_batch = client.create_batch() + event_data_batch.add(EventData(json.dumps(event))) + with client: + append_flow_client.send_batch(event_data_batch) \ No newline at end of file diff --git a/integration_tests/notebooks/eventhub_runners/validate.py b/integration_tests/notebooks/eventhub_runners/validate.py new file mode 100644 index 0000000..4e803f8 --- /dev/null +++ b/integration_tests/notebooks/eventhub_runners/validate.py @@ -0,0 +1,38 @@ +# Databricks notebook source +import pandas as pd + +run_id = dbutils.widgets.get("run_id") +uc_enabled = eval(dbutils.widgets.get("uc_enabled")) +uc_catalog_name = dbutils.widgets.get("uc_catalog_name") +output_file_path = dbutils.widgets.get("output_file_path") +bronze_schema = dbutils.widgets.get("bronze_schema") +log_list = [] + +# Assumption is that to get to this notebook Bronze and Silver completed successfully +log_list.append("Completed Bronze Eventhub DLT Pipeline.") + +UC_TABLES = { + f"{uc_catalog_name}.{bronze_schema}.bronze_{run_id}_iot": 20, + f"{uc_catalog_name}.{bronze_schema}.bronze_{run_id}_iot_quarantine": 2 +} + +NON_UC_TABLES = { + f"{uc_catalog_name}.{bronze_schema}.bronze_{run_id}_iot": 20, + f"{uc_catalog_name}.{bronze_schema}.bronze_{run_id}_iot_quarantine": 2 +} + +log_list.append("Validating DLT EVenthub Bronze Table Counts...") +tables = UC_TABLES if uc_enabled else NON_UC_TABLES +for table, counts in tables.items(): + query = spark.sql(f"SELECT count(*) as cnt FROM {table}") + cnt = query.collect()[0].cnt + + log_list.append(f"Validating Counts for Table {table}.") + try: + assert int(cnt) >= counts + log_list.append(f"Expected >= {counts} Actual: {cnt}. Passed!") + except AssertionError: + log_list.append(f"Expected > {counts} Actual: {cnt}. Failed!") + +pd_df = pd.DataFrame(log_list) +pd_df.to_csv(output_file_path) \ No newline at end of file diff --git a/integration_tests/notebooks/kafka_runners/init_dlt_meta_pipeline.py b/integration_tests/notebooks/kafka_runners/init_dlt_meta_pipeline.py new file mode 100644 index 0000000..e1e579b --- /dev/null +++ b/integration_tests/notebooks/kafka_runners/init_dlt_meta_pipeline.py @@ -0,0 +1,10 @@ +# Databricks notebook source +dlt_meta_whl = spark.conf.get("dlt_meta_whl") +%pip install $dlt_meta_whl + +# COMMAND ---------- + +layer = spark.conf.get("layer", None) + +from src.dataflow_pipeline import DataflowPipeline +DataflowPipeline.invoke_dlt_pipeline(spark, layer) \ No newline at end of file diff --git a/integration_tests/notebooks/kafka_runners/publish_events.py b/integration_tests/notebooks/kafka_runners/publish_events.py new file mode 100644 index 0000000..23fbc4b --- /dev/null +++ b/integration_tests/notebooks/kafka_runners/publish_events.py @@ -0,0 +1,32 @@ +# Databricks notebook source +# DBTITLE 1,Install kafka python lib +# MAGIC %pip install kafka-python + +# COMMAND ---------- + +# DBTITLE 1,Extract input from notebook params +dbutils.widgets.text("kafka_topic","kafka_topic","") +dbutils.widgets.text("kafka_broker","kafka_broker","") +dbutils.widgets.text("kafka_input_data","kafka_input_data","") +kafka_topic = dbutils.widgets.get("kafka_topic") +kafka_broker = dbutils.widgets.get("kafka_broker") +kafka_input_data = dbutils.widgets.get("kafka_input_data") +print(f"kafka_topic={kafka_topic}, kafka_broker={kafka_broker}, kafka_input_data={kafka_input_data}") + +# COMMAND ---------- + +# DBTITLE 1,Initialize kafka producer +from kafka import KafkaProducer +import json +producer = KafkaProducer(bootstrap_servers=kafka_broker, value_serializer=lambda v: json.dumps(v).encode('utf-8')) + +# COMMAND ---------- + +# DBTITLE 1,Send Messages +with open(f"{kafka_input_data}") as f: + data = json.load(f) + +for event in data: + producer.send(kafka_topic,event) + +producer.close() \ No newline at end of file diff --git a/integration_tests/notebooks/kafka_runners/validate.py b/integration_tests/notebooks/kafka_runners/validate.py new file mode 100644 index 0000000..58bb43e --- /dev/null +++ b/integration_tests/notebooks/kafka_runners/validate.py @@ -0,0 +1,32 @@ +# Databricks notebook source +import pandas as pd + +run_id = dbutils.widgets.get("run_id") +uc_catalog_name = dbutils.widgets.get("uc_catalog_name") +uc_volume_path = dbutils.widgets.get("uc_volume_path") +bronze_schema = dbutils.widgets.get("bronze_schema") +log_list = [] + +# Assumption is that to get to this notebook Bronze and Silver completed successfully +log_list.append("Completed Bronze Eventhub DLT Pipeline.") + +TABLES = { + f"{uc_catalog_name}.{bronze_schema}.bronze_it_{run_id}_iot": 20, + f"{uc_catalog_name}.{bronze_schema}.bronze_it_{run_id}_iot_quarantine": 2 +} + +log_list.append("Validating DLT EVenthub Bronze Table Counts...") +for table, counts in TABLES.items(): + query = spark.sql(f"SELECT count(*) as cnt FROM {table}") + cnt = query.collect()[0].cnt + + log_list.append(f"Validating Counts for Table {table}.") + try: + assert int(cnt) >= counts + log_list.append(f"Expected >= {counts} Actual: {cnt}. Passed!") + except AssertionError: + log_list.append(f"Expected {counts} Actual: {cnt}. Failed!") + +pd_df = pd.DataFrame(log_list) +log_file =f"{uc_volume_path}/integration-test-output.csv" +pd_df.to_csv(log_file) diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index 9ad6a87..4bc12cc 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -10,7 +10,7 @@ from databricks.sdk.service.pipelines import PipelineLibrary, NotebookLibrary from databricks.sdk.service import jobs, compute from src.__about__ import __version__ -from databricks.sdk.service.workspace import ImportFormat +from databricks.sdk.service.workspace import ImportFormat, Language from databricks.sdk.service.catalog import SchemasAPI, VolumeInfo, VolumeType from src.install import WorkspaceInstaller @@ -188,17 +188,19 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: runner_conf.uc_catalog_name = self.args.__dict__['uc_catalog_name'] runner_conf.uc_volume_name = f"{self.args.__dict__['uc_catalog_name']}_volume_{run_id}", - runners_full_local_path = None - if runner_conf.source.lower() == "cloudfiles": - runners_full_local_path = './integration_tests/dbc/cloud_files_runners.dbc' - elif runner_conf.source.lower() == "eventhub": - runners_full_local_path = './integration_tests/dbc/eventhub_runners.dbc' - elif runner_conf.source.lower() == "kafka": - runners_full_local_path = './integration_tests/dbc/kafka_runners.dbc' - else: - raise Exception("Supported source not found in argument") - runner_conf.runners_full_local_path = runners_full_local_path + # Set the proper directory location for the notebooks that need to be uploaded to run and + # validate the integration tests + source_paths = { + "cloudfiles": "./integration_tests/notebooks/cloudfile_runners/", + "eventhub": "./integration_tests/notebooks/eventhub_runners/", + "kafka": "./integration_tests/notebooks/kafka_runners/", + } + try: + runner_conf.runners_full_local_path = source_paths[runner_conf.source.lower()] + except KeyError: + raise Exception("Given source is not support. Support source are: cloudfiles, eventhub, or kafka") + return runner_conf def _install_folder(self): @@ -912,22 +914,38 @@ def copy(self, runner_conf: DLTMetaRunnerConf): self.ws.dbfs.upload(dbfs_path, contents, overwrite=True) def init_dltmeta_runner_conf(self, runner_conf: DLTMetaRunnerConf): + """Create testing metadata including schemas, volumes, and uploading necessary notebooks""" if runner_conf.uc_catalog_name: self.initialize_uc_resources(runner_conf) self.generate_onboarding_file(runner_conf) + + print("int_tests_dir: ", runner_conf.int_tests_dir) self.copy(runner_conf) - print(f"uploading to {runner_conf.runners_nb_path}/{self.base_dir}/ complete!!!") - fp = open(runner_conf.runners_full_local_path, "rb") + print( + f"uploading to {runner_conf.runners_nb_path}/{self.base_dir}/ complete!!!" + ) + + # Upload required notebooks for the given source print(f"uploading to {runner_conf.runners_nb_path} started") - self.ws.workspace.mkdirs(runner_conf.runners_nb_path) - self.ws.workspace.upload(path=f"{runner_conf.runners_nb_path}/runners", - format=ImportFormat.DBC, content=fp.read()) + self.ws.workspace.mkdirs(f"{runner_conf.runners_nb_path}/runners") + for notebook in os.listdir(runner_conf.runners_full_local_path): + local_path = os.path.join(runner_conf.runners_full_local_path, notebook) + with open(local_path, "rb") as nb_file: + self.ws.workspace.upload( + path=f"{runner_conf.runners_nb_path}/runners/{notebook}", + format=ImportFormat.SOURCE, + language=Language.PYTHON, + content=nb_file.read(), + ) print(f"uploading to {runner_conf.runners_nb_path} complete!!!") + + if runner_conf.uc_catalog_name: self.build_and_upload_package(runner_conf) def initialize_uc_resources(self, runner_conf): + '''Create UC schemas and volumes needed to run the integration tests''' SchemasAPI(self.ws.api_client).create(catalog_name=runner_conf.uc_catalog_name, name=runner_conf.dlt_meta_schema, comment="dlt_meta framework schema") @@ -978,9 +996,9 @@ def create_cluster(self, runner_conf: DLTMetaRunnerConf): def run(self, runner_conf: DLTMetaRunnerConf): try: self.init_dltmeta_runner_conf(runner_conf) - self.create_bronze_silver_dlt(runner_conf) - self.launch_workflow(runner_conf) - self.download_test_results(runner_conf) + # self.create_bronze_silver_dlt(runner_conf) + # self.launch_workflow(runner_conf) + # self.download_test_results(runner_conf) except Exception as e: print(e) # finally: From 4b9bffc6ca48c5e140f2cfa5afc03388f7af2650 Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Wed, 2 Oct 2024 14:44:45 -0400 Subject: [PATCH 18/59] lower the source during the arg parse instead of at each time it is used --- integration_tests/run_integration_tests.py | 29 ++++++++++++---------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index 4bc12cc..d23b4a8 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -168,7 +168,7 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: bronze_schema=f"dlt_meta_bronze_it_{run_id}", silver_schema=f"dlt_meta_silver_it_{run_id}", runners_nb_path=f"/Users/{self.wsi._my_username}/dlt_meta_int_tests/{run_id}", - source=self.args.__dict__['source'], + source=self.args.__dict__['source'].lower(), node_type_id=cloud_node_type_id_dict[self.args.__dict__.get('cloud_provider_name', None)], dbr_version=self.args.__dict__.get('dbr_version', None), cloudfiles_template="integration_tests/conf/cloudfiles-onboarding.template", @@ -197,7 +197,7 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: "kafka": "./integration_tests/notebooks/kafka_runners/", } try: - runner_conf.runners_full_local_path = source_paths[runner_conf.source.lower()] + runner_conf.runners_full_local_path = source_paths[runner_conf.source] except KeyError: raise Exception("Given source is not support. Support source are: cloudfiles, eventhub, or kafka") @@ -600,7 +600,7 @@ def create_kafka_workflow_spec(self, runner_conf: DLTMetaRunnerConf): def generate_onboarding_file(self, runner_conf: DLTMetaRunnerConf): """Generate onboarding file from template.""" - source = runner_conf.source.lower() + source = runner_conf.source if source == "cloudfiles": self.create_cloudfiles_onboarding(runner_conf) elif source == "eventhub": @@ -952,7 +952,7 @@ def initialize_uc_resources(self, runner_conf): SchemasAPI(self.ws.api_client).create(catalog_name=runner_conf.uc_catalog_name, name=runner_conf.bronze_schema, comment="bronze_schema") - if runner_conf.source and runner_conf.source.lower() == "cloudfiles": + if runner_conf.source and runner_conf.source == "cloudfiles": SchemasAPI(self.ws.api_client).create(catalog_name=runner_conf.uc_catalog_name, name=runner_conf.silver_schema, comment="silver_schema") @@ -1018,7 +1018,7 @@ def create_bronze_silver_dlt(self, runner_conf: DLTMetaRunnerConf): runner_conf.bronze_schema, runner_conf) - if runner_conf.source and runner_conf.source.lower() == "cloudfiles": + if runner_conf.source and runner_conf.source == "cloudfiles": runner_conf.bronze_pipeline_A2_id = self.create_dlt_meta_pipeline( f"dlt-meta-bronze-A2-{runner_conf.run_id}", "bronze", @@ -1034,11 +1034,11 @@ def create_bronze_silver_dlt(self, runner_conf: DLTMetaRunnerConf): runner_conf) def launch_workflow(self, runner_conf: DLTMetaRunnerConf): - if runner_conf.source.lower() == "cloudfiles": + if runner_conf.source == "cloudfiles": created_job = self.create_cloudfiles_workflow_spec(runner_conf) - elif runner_conf.source.lower() == "eventhub": + elif runner_conf.source == "eventhub": created_job = self.create_eventhub_workflow_spec(runner_conf) - elif runner_conf.source.lower() == "kafka": + elif runner_conf.source == "kafka": created_job = self.create_kafka_workflow_spec(runner_conf) runner_conf.job_id = created_job.job_id print(f"Job created successfully. job_id={created_job.job_id}, started run...") @@ -1137,7 +1137,10 @@ def process_arguments(args_map, mandatory_args): """Process command line arguments.""" parser = argparse.ArgumentParser() for key, value in args_map.items(): - parser.add_argument(key, help=value) + if key == '--source': # Only expecting lowercase source options + parser.add_argument(key, help=value, type=str.lower) + else: + parser.add_argument(key, help=value) args = parser.parse_args() check_mandatory_arg(args, mandatory_args) @@ -1153,14 +1156,14 @@ def post_arg_processing(args): """Post processing of arguments.""" supported_sources = ["cloudfiles", "eventhub", "kafka"] source = args.__getattribute__("source") - if source.lower() not in supported_sources: - raise Exception("Invalid value for --source! Supported values: --source=cloudfiles") - if source.lower() == "eventhub": + if source not in supported_sources: + raise Exception(f"Invalid value for --source! Supported values: {supported_sources}") + if source == "eventhub": eventhub_madatory_args = ["eventhub_name", "eventhub_name_append_flow", "eventhub_producer_accesskey_name", "eventhub_consumer_accesskey_name", "eventhub_secrets_scope_name", "eventhub_namespace", "eventhub_port"] check_mandatory_arg(args, eventhub_madatory_args) - if source.lower() == "kafka": + if source == "kafka": kafka_madatory_args = ["kafka_topic_name", "kafka_broker"] check_mandatory_arg(args, kafka_madatory_args) print(f"Parsing argument complete. args={args}") From 5a38c8570aadbfcce1b6b7b59c085293616e0935 Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Wed, 2 Oct 2024 15:57:54 -0400 Subject: [PATCH 19/59] Complete rewrite of the arg parser for the intgegration tests --- integration_tests/README.md | 2 +- integration_tests/run_integration_tests.py | 222 +++++++++++++++------ 2 files changed, 159 insertions(+), 65 deletions(-) diff --git a/integration_tests/README.md b/integration_tests/README.md index ebc56d7..97142d8 100644 --- a/integration_tests/README.md +++ b/integration_tests/README.md @@ -43,7 +43,7 @@ - 9b. Run the command for eventhub ```commandline - python integration_tests/run_integration_tests.py --uc_catalog_name=<> --source=eventhub --eventhub_name=iot --eventhub_secrets_scope_name=eventhubs_creds --eventhub_namespace=int_test-standard --eventhub_port=9093 --eventhub_producer_accesskey_name=producer --eventhub_consumer_accesskey_name=consumer --eventhub_name_append_flow=TODO? --eventhub_accesskey_secret_name=TODO? + python integration_tests/run_integration_tests.py --uc_catalog_name=<> --source=eventhub --cloud_provider_name=aws --eventhub_name=iot --eventhub_secrets_scope_name=eventhubs_creds --eventhub_namespace=int_test-standard --eventhub_port=9093 --eventhub_producer_accesskey_name=producer --eventhub_consumer_accesskey_name=consumer --eventhub_name_append_flow=TODO? --eventhub_accesskey_secret_name=TODO? ``` - - For eventhub integration tests, the following are the prerequisites: diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index d23b4a8..d104941 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -168,7 +168,7 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: bronze_schema=f"dlt_meta_bronze_it_{run_id}", silver_schema=f"dlt_meta_silver_it_{run_id}", runners_nb_path=f"/Users/{self.wsi._my_username}/dlt_meta_int_tests/{run_id}", - source=self.args.__dict__['source'].lower(), + source=self.args.__dict__['source'], node_type_id=cloud_node_type_id_dict[self.args.__dict__.get('cloud_provider_name', None)], dbr_version=self.args.__dict__.get('dbr_version', None), cloudfiles_template="integration_tests/conf/cloudfiles-onboarding.template", @@ -1100,81 +1100,175 @@ def get_workspace_api_client(profile=None) -> WorkspaceClient: return workspace_client -args_map = {"--profile": "provide databricks cli profile name, if not provide databricks_host and token", - "--uc_catalog_name": "provide databricks uc_catalog name, this is required to create volume, schema, table", - "--cloud_provider_name": "provide cloud provider name. Supported values are aws , azure , gcp", - "--source": "Provide source type e.g --source=cloudfiles", - "--eventhub_name": "Provide eventhub_name e.g --eventhub_name=iot", - "--eventhub_name_append_flow": "Provide eventhub_name_append_flow e.g --eventhub_name_append_flow=iot_af", - "--eventhub_producer_accesskey_name": "Provide access key that has write permission on the eventhub", - "--eventhub_consumer_accesskey_name": "Provide access key that has read permission on the eventhub", - "--eventhub_secrets_scope_name": "Provide eventhub_secrets_scope_name e.g \ - --eventhub_secrets_scope_name=eventhubs_creds", - "--eventhub_accesskey_secret_name": "Provide eventhub_accesskey_secret_name e.g \ - -eventhub_accesskey_secret_name=RootManageSharedAccessKey", - "--eventhub_namespace": "Provide eventhub_namespace e.g --eventhub_namespace=topic-standard", - "--eventhub_port": "Provide eventhub_port e.g --eventhub_port=9093", - "--kafka_topic_name": "Provide kafka topic name e.g --kafka_topic_name=iot", - "--kafka_broker": "Provide kafka broker e.g --127.0.0.1:9092" - } - -mandatory_args = [ - "uc_catalog_name", "cloud_provider_name", "source" -] - - def main(): """Entry method to run integration tests.""" - args = process_arguments(args_map, mandatory_args) - post_arg_processing(args) + + args_map = { + "--profile": "provide databricks cli profile name, if not provide databricks_host and token", + "--uc_catalog_name": "provide databricks uc_catalog name, this is required to create volume, schema, table", + "--cloud_provider_name": "provide cloud provider name. Supported values are aws , azure , gcp", + "--source": "Provide source type e.g --source=cloudfiles", + "--eventhub_name": "Provide eventhub_name e.g --eventhub_name=iot", + "--eventhub_name_append_flow": "Provide eventhub_name_append_flow e.g --eventhub_name_append_flow=iot_af", + "--eventhub_producer_accesskey_name": "Provide access key that has write permission on the eventhub", + "--eventhub_consumer_accesskey_name": "Provide access key that has read permission on the eventhub", + "--eventhub_secrets_scope_name": "Provide eventhub_secrets_scope_name e.g --eventhub_secrets_scope_name=eventhubs_creds", + "--eventhub_accesskey_secret_name": "Provide eventhub_accesskey_secret_name e.g -eventhub_accesskey_secret_name=RootManageSharedAccessKey", + "--eventhub_namespace": "Provide eventhub_namespace e.g --eventhub_namespace=topic-standard", + "--eventhub_port": "Provide eventhub_port e.g --eventhub_port=9093", + "--kafka_topic_name": "Provide kafka topic name e.g --kafka_topic_name=iot", + "--kafka_broker": "Provide kafka broker e.g --127.0.0.1:9092", + } + mandatory_args = ["uc_catalog_name", "cloud_provider_name", "source"] + + args = process_arguments() + exit() workspace_client = get_workspace_api_client(args.profile) integration_test_runner = DLTMETARunner(args, workspace_client, "integration_tests") runner_conf = integration_test_runner.init_runner_conf() integration_test_runner.run(runner_conf) +def process_arguments() -> dict[str: str]: + """ + Get, process, and validate the command line arguements + + Returns: + A dictionary where the argument names are the keys and the values aredictionary values + """ -def process_arguments(args_map, mandatory_args): - """Process command line arguments.""" + print("Processing comand line arguments...") + + # Possible input arguments, organized as elements in a list like: + # [argument, help message, type, required, choices (if applicable)] + input_args = [ + # Generic arguments + [ + "profile", + "Provide databricks cli profile name, if not provide databricks_host and token", + str, + False, + [], + ], + [ + "uc_catalog_name", + "Provide databricks uc_catalog name, this is required to create volume, schema, table", + str, + True, + [], + ], + [ + "cloud_provider_name", + "Provide cloud provider name. Supported values are aws , azure , gcp", + str.lower, + True, + ["aws", "azure", "gcp"], + ], + [ + "source", + "Provide source type: cloudfiles, eventhub, kafka", + str.lower, + True, + ["cloudfiles", "eventhub", "kafka"], + ], + # Eventhub arguments + ["eventhub_name", "Provide eventhub_name e.g: iot", str, False, []], + [ + "eventhub_name_append_flow", + "Provide eventhub_name_append_flow e.g: iot_af", + str, + False, + [], + ], + [ + "eventhub_producer_accesskey_name", + "Provide access key that has write permission on the eventhub", + str, + False, + [], + ], + [ + "eventhub_consumer_accesskey_name", + "Provide access key that has read permission on the eventhub", + str, + False, + [], + ], + [ + "eventhub_secrets_scope_name", + "Provide eventhub_secrets_scope_name e.g: eventhubs_creds", + str, + False, + [], + ], + [ + "eventhub_accesskey_secret_name", + "Provide eventhub_accesskey_secret_name e.g: RootManageSharedAccessKey", + str, + False, + [], + ], + [ + "eventhub_namespace", + "Provide eventhub_namespace e.g: topic-standar", + str, + False, + [], + ], + [ + "eventhub_port", + "Provide eventhub_port e.g: 9093", + str, + False, + [], + ], + # Kafka arguments + [ + "kafka_topic_name", + "Provide kafka topic name e.g: iot", + str, + False, + [], + ], + ["kafka_broker", "Provide kafka broker e.g 127.0.0.1:9092", str, False, []], + ] + + # Build cli parser parser = argparse.ArgumentParser() - for key, value in args_map.items(): - if key == '--source': # Only expecting lowercase source options - parser.add_argument(key, help=value, type=str.lower) + for arg in input_args: + if arg[4]: + parser.add_argument(f"--{arg[0]}", help=arg[1], type=arg[2], required=arg[3], choices=arg[4]) else: - parser.add_argument(key, help=value) - - args = parser.parse_args() - check_mandatory_arg(args, mandatory_args) - supported_cloud_providers = ["aws", "azure", "gcp"] + parser.add_argument(f"--{arg[0]}", help=arg[1], type=arg[2], required=arg[3]) + args = vars(parser.parse_args()) + + def check_mandatory_arg(args, mandatory_args): + """Post argument parsing check for conditionally required arguments""" + for mand_arg in mandatory_args: + if args[mand_arg] is None: + raise Exception(f"Please provide '--{mand_arg}'") + + # Check for arguments that are required depending on the selected source + if args["source"] == "eventhub": + check_mandatory_arg( + args, + [ + "eventhub_name", + "eventhub_name_append_flow", + "eventhub_producer_accesskey_name", + "eventhub_consumer_accesskey_name", + "eventhub_secrets_scope_name", + "eventhub_namespace", + "eventhub_port", + ], + ) + elif args["source"] == "kafka": + check_mandatory_arg( + args, + ["kafka_topic_name", "kafka_broker"], + ) - cloud_provider_name = args.__getattribute__("cloud_provider_name") if args.__contains__("cloud_provider_name") else None - if cloud_provider_name and cloud_provider_name.lower() not in supported_cloud_providers: - raise Exception("Invalid value for --cloud_provider_name! Supported values are aws, azure, gcp") + print(f"Processing comand line arguments Complete: {args}") return args - -def post_arg_processing(args): - """Post processing of arguments.""" - supported_sources = ["cloudfiles", "eventhub", "kafka"] - source = args.__getattribute__("source") - if source not in supported_sources: - raise Exception(f"Invalid value for --source! Supported values: {supported_sources}") - if source == "eventhub": - eventhub_madatory_args = ["eventhub_name", "eventhub_name_append_flow", "eventhub_producer_accesskey_name", - "eventhub_consumer_accesskey_name", "eventhub_secrets_scope_name", - "eventhub_namespace", "eventhub_port"] - check_mandatory_arg(args, eventhub_madatory_args) - if source == "kafka": - kafka_madatory_args = ["kafka_topic_name", "kafka_broker"] - check_mandatory_arg(args, kafka_madatory_args) - print(f"Parsing argument complete. args={args}") - - -def check_mandatory_arg(args, mandatory_args): - """Check mandatory argument present.""" - for mand_arg in mandatory_args: - if args.__dict__[f'{mand_arg}'] is None: - raise Exception(f"Please provide '--{mand_arg}'") - - if __name__ == "__main__": main() From 72fe9dc623a46c9b0709a84de4b2a0a3f34fce07 Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Wed, 2 Oct 2024 16:21:15 -0400 Subject: [PATCH 20/59] Update args parsing --- integration_tests/run_integration_tests.py | 70 ++++++++-------------- 1 file changed, 25 insertions(+), 45 deletions(-) diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index d104941..91fb7a6 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -143,7 +143,7 @@ class DLTMETARunner: - workspace_client: Databricks workspace client - runner_conf: test information """ - def __init__(self, args, ws, base_dir): + def __init__(self, args: dict[str: str], ws, base_dir): self.args = args self.ws = ws self.wsi = WorkspaceInstaller(ws) @@ -162,15 +162,15 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: runner_conf = DLTMetaRunnerConf( run_id=run_id, username=self.wsi._my_username, - dbfs_tmp_path=f"{self.args.__dict__.get('dbfs_path', None)}/{run_id}", + dbfs_tmp_path=f"{self.args.get('dbfs_path')}/{run_id}", int_tests_dir="file:./integration_tests", dlt_meta_schema=f"dlt_meta_dataflowspecs_it_{run_id}", bronze_schema=f"dlt_meta_bronze_it_{run_id}", silver_schema=f"dlt_meta_silver_it_{run_id}", runners_nb_path=f"/Users/{self.wsi._my_username}/dlt_meta_int_tests/{run_id}", - source=self.args.__dict__['source'], - node_type_id=cloud_node_type_id_dict[self.args.__dict__.get('cloud_provider_name', None)], - dbr_version=self.args.__dict__.get('dbr_version', None), + source=self.args['source'], + node_type_id=cloud_node_type_id_dict[self.args['cloud_provider_name']], + dbr_version=self.args.get('dbr_version'), cloudfiles_template="integration_tests/conf/cloudfiles-onboarding.template", cloudfiles_A2_template="integration_tests/conf/cloudfiles-onboarding_A2.template", eventhub_template="integration_tests/conf/eventhub-onboarding.template", @@ -184,9 +184,8 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: ) ) - if self.args.__dict__['uc_catalog_name']: - runner_conf.uc_catalog_name = self.args.__dict__['uc_catalog_name'] - runner_conf.uc_volume_name = f"{self.args.__dict__['uc_catalog_name']}_volume_{run_id}", + runner_conf.uc_catalog_name = self.args['uc_catalog_name'] + runner_conf.uc_volume_name = f"{self.args['uc_catalog_name']}_volume_{run_id}", # Set the proper directory location for the notebooks that need to be uploaded to run and @@ -482,11 +481,11 @@ def create_eventhub_workflow_spec(self, runner_conf: DLTMetaRunnerConf): notebook_task=jobs.NotebookTask( notebook_path=f"{runner_conf.runners_nb_path}/runners/publish_events", base_parameters={ - "eventhub_name": self.args.__getattribute__("eventhub_name"), - "eventhub_name_append_flow": self.args.__getattribute__("eventhub_name_append_flow"), - "eventhub_namespace": self.args.__getattribute__("eventhub_namespace"), - "eventhub_secrets_scope_name": self.args.__getattribute__("eventhub_secrets_scope_name"), - "eventhub_accesskey_name": self.args.__getattribute__("eventhub_producer_accesskey_name"), + "eventhub_name": self.args["eventhub_name"], + "eventhub_name_append_flow": self.args["eventhub_name_append_flow"], + "eventhub_namespace": self.args["eventhub_namespace"], + "eventhub_secrets_scope_name": self.args["eventhub_secrets_scope_name"], + "eventhub_accesskey_name": self.args["eventhub_producer_accesskey_name"], "eventhub_input_data": f"/{runner_conf.uc_volume_path}/{self.base_dir}/resources/data/iot/iot.json", "eventhub_append_flow_input_data": @@ -567,8 +566,8 @@ def create_kafka_workflow_spec(self, runner_conf: DLTMetaRunnerConf): notebook_task=jobs.NotebookTask( notebook_path=f"{runner_conf.runners_nb_path}/runners/publish_events", base_parameters={ - "kafka_topic": self.args.__getattribute__("kafka_topic_name"), - "kafka_broker": self.args.__getattribute__("kafka_broker"), + "kafka_topic": self.args["kafka_topic_name"], + "kafka_broker": self.args["kafka_broker"], "kafka_input_data": f"/{dbfs_path}/{self.base_dir}/resources/data/iot/iot.json" } ) @@ -612,8 +611,8 @@ def create_kafka_onboarding(self, runner_conf: DLTMetaRunnerConf): """Create kafka onboarding file.""" with open(f"{runner_conf.kafka_template}") as f: onboard_obj = json.load(f) - kafka_topic = self.args.__getattribute__("kafka_topic_name").lower() - kafka_bootstrap_servers = self.args.__getattribute__("kafka_broker").lower() + kafka_topic = self.args["kafka_topic_name"].lower() + kafka_bootstrap_servers = self.args["kafka_broker"].lower() for data_flow in onboard_obj: for key, value in data_flow.items(): if key == "source_details": @@ -647,13 +646,13 @@ def create_eventhub_onboarding(self, runner_conf: DLTMetaRunnerConf): """Create eventhub onboarding file.""" with open(f"{runner_conf.eventhub_template}") as f: onboard_obj = json.load(f) - eventhub_name = self.args.__getattribute__("eventhub_name").lower() - eventhub_name_append_flow = self.args.__getattribute__("eventhub_name_append_flow").lower() - eventhub_accesskey_name = self.args.__getattribute__("eventhub_consumer_accesskey_name").lower() - eventhub_accesskey_secret_name = self.args.__getattribute__("eventhub_accesskey_secret_name").lower() - eventhub_secrets_scope_name = self.args.__getattribute__("eventhub_secrets_scope_name").lower() - eventhub_namespace = self.args.__getattribute__("eventhub_namespace").lower() - eventhub_port = self.args.__getattribute__("eventhub_port").lower() + eventhub_name = self.args["eventhub_name"].lower() + eventhub_name_append_flow = self.args["eventhub_name_append_flow"].lower() + eventhub_accesskey_name = self.args["eventhub_consumer_accesskey_name"].lower() + eventhub_accesskey_secret_name = self.args["eventhub_accesskey_secret_name"].lower() + eventhub_secrets_scope_name = self.args["eventhub_secrets_scope_name"].lower() + eventhub_namespace = self.args["eventhub_namespace"].lower() + eventhub_port = self.args["eventhub_port"].lower() for data_flow in onboard_obj: for key, value in data_flow.items(): if key == "source_details": @@ -1102,29 +1101,10 @@ def get_workspace_api_client(profile=None) -> WorkspaceClient: def main(): """Entry method to run integration tests.""" - - args_map = { - "--profile": "provide databricks cli profile name, if not provide databricks_host and token", - "--uc_catalog_name": "provide databricks uc_catalog name, this is required to create volume, schema, table", - "--cloud_provider_name": "provide cloud provider name. Supported values are aws , azure , gcp", - "--source": "Provide source type e.g --source=cloudfiles", - "--eventhub_name": "Provide eventhub_name e.g --eventhub_name=iot", - "--eventhub_name_append_flow": "Provide eventhub_name_append_flow e.g --eventhub_name_append_flow=iot_af", - "--eventhub_producer_accesskey_name": "Provide access key that has write permission on the eventhub", - "--eventhub_consumer_accesskey_name": "Provide access key that has read permission on the eventhub", - "--eventhub_secrets_scope_name": "Provide eventhub_secrets_scope_name e.g --eventhub_secrets_scope_name=eventhubs_creds", - "--eventhub_accesskey_secret_name": "Provide eventhub_accesskey_secret_name e.g -eventhub_accesskey_secret_name=RootManageSharedAccessKey", - "--eventhub_namespace": "Provide eventhub_namespace e.g --eventhub_namespace=topic-standard", - "--eventhub_port": "Provide eventhub_port e.g --eventhub_port=9093", - "--kafka_topic_name": "Provide kafka topic name e.g --kafka_topic_name=iot", - "--kafka_broker": "Provide kafka broker e.g --127.0.0.1:9092", - } - mandatory_args = ["uc_catalog_name", "cloud_provider_name", "source"] - args = process_arguments() - exit() - workspace_client = get_workspace_api_client(args.profile) + workspace_client = get_workspace_api_client(args["profile"]) integration_test_runner = DLTMETARunner(args, workspace_client, "integration_tests") + exit() runner_conf = integration_test_runner.init_runner_conf() integration_test_runner.run(runner_conf) From c337f59cd7fbb019090f38a998034101a518706b Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Wed, 2 Oct 2024 17:00:42 -0400 Subject: [PATCH 21/59] begin movign to uc volumes only and getting rid of dbfs as well as simplifying configs --- integration_tests/run_integration_tests.py | 60 ++++++++-------------- 1 file changed, 21 insertions(+), 39 deletions(-) diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index 91fb7a6..018fb65 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -162,15 +162,15 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: runner_conf = DLTMetaRunnerConf( run_id=run_id, username=self.wsi._my_username, - dbfs_tmp_path=f"{self.args.get('dbfs_path')}/{run_id}", + uc_catalog_name=self.args["uc_catalog_name"], + uc_volume_name=f"{self.args['uc_catalog_name']}_volume_{run_id}", int_tests_dir="file:./integration_tests", dlt_meta_schema=f"dlt_meta_dataflowspecs_it_{run_id}", bronze_schema=f"dlt_meta_bronze_it_{run_id}", silver_schema=f"dlt_meta_silver_it_{run_id}", runners_nb_path=f"/Users/{self.wsi._my_username}/dlt_meta_int_tests/{run_id}", - source=self.args['source'], - node_type_id=cloud_node_type_id_dict[self.args['cloud_provider_name']], - dbr_version=self.args.get('dbr_version'), + source=self.args["source"], + node_type_id=cloud_node_type_id_dict[self.args["cloud_provider_name"]], cloudfiles_template="integration_tests/conf/cloudfiles-onboarding.template", cloudfiles_A2_template="integration_tests/conf/cloudfiles-onboarding_A2.template", eventhub_template="integration_tests/conf/eventhub-onboarding.template", @@ -181,12 +181,8 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: test_output_file_path=( f"/Users/{self.wsi._my_username}/dlt_meta_int_tests/" f"{run_id}/integration-test-output.csv" - ) - + ), ) - runner_conf.uc_catalog_name = self.args['uc_catalog_name'] - runner_conf.uc_volume_name = f"{self.args['uc_catalog_name']}_volume_{run_id}", - # Set the proper directory location for the notebooks that need to be uploaded to run and # validate the integration tests @@ -303,6 +299,7 @@ def create_dlt_meta_pipeline(self, raise Exception("Pipeline creation failed") return created.pipeline_id + def create_cloudfiles_workflow_spec(self, runner_conf: DLTMetaRunnerConf): """ Create the CloudFiles workflow specification. @@ -322,7 +319,6 @@ def create_cloudfiles_workflow_spec(self, runner_conf: DLTMetaRunnerConf): Exception If the job creation fails. """ - database, dlt_lib = self.init_db_dltlib(runner_conf) dltmeta_environments = [ jobs.JobEnvironment( environment_key="dl_meta_int_env", @@ -345,7 +341,7 @@ def create_cloudfiles_workflow_spec(self, runner_conf: DLTMetaRunnerConf): entry_point="run", named_parameters={ "onboard_layer": "bronze_silver", - "database": database, + "database": f"{runner_conf.uc_catalog_name}.{runner_conf.dlt_meta_schema}", "onboarding_file_path": f"{runner_conf.uc_volume_path}/{self.base_dir}/conf/onboarding.json", "silver_dataflowspec_table": "silver_dataflowspec_cdc", @@ -356,7 +352,7 @@ def create_cloudfiles_workflow_spec(self, runner_conf: DLTMetaRunnerConf): "bronze_dataflowspec_path": f"{runner_conf.uc_volume_path}/data/dlt_spec/bronze", "overwrite": "True", "env": runner_conf.env, - "uc_enabled": "True" if runner_conf.uc_catalog_name else "False" + "uc_enabled": "True" }, ) ), @@ -378,7 +374,7 @@ def create_cloudfiles_workflow_spec(self, runner_conf: DLTMetaRunnerConf): entry_point="run", named_parameters={ "onboard_layer": "bronze", - "database": database, + "database": f"{runner_conf.uc_catalog_name}.{runner_conf.dlt_meta_schema}", "onboarding_file_path": f"{runner_conf.uc_volume_path}/{self.base_dir}/conf/onboarding_A2.json", "bronze_dataflowspec_table": "bronze_dataflowspec_cdc", @@ -386,7 +382,7 @@ def create_cloudfiles_workflow_spec(self, runner_conf: DLTMetaRunnerConf): "version": "v1", "overwrite": "False", "env": runner_conf.env, - "uc_enabled": "True" if runner_conf.uc_catalog_name else "False" + "uc_enabled": "True" }, ) ), @@ -411,7 +407,7 @@ def create_cloudfiles_workflow_spec(self, runner_conf: DLTMetaRunnerConf): notebook_task=jobs.NotebookTask( notebook_path=f"{runner_conf.runners_nb_path}/runners/validate", base_parameters={ - "uc_enabled": "True" if runner_conf.uc_catalog_name else "False", + "uc_enabled": "True", "uc_catalog_name": f"{runner_conf.uc_catalog_name}", "bronze_schema": f"{runner_conf.bronze_schema}", "silver_schema": f"{runner_conf.silver_schema}", @@ -424,20 +420,8 @@ def create_cloudfiles_workflow_spec(self, runner_conf: DLTMetaRunnerConf): ] ) - def init_db_dltlib(self, runner_conf: DLTMetaRunnerConf): - database = None - dlt_lib = [] - if runner_conf.uc_catalog_name: - database = f"{runner_conf.uc_catalog_name}.{runner_conf.dlt_meta_schema}" - dlt_lib.append(jobs.compute.Library(whl=runner_conf.remote_whl_path)) - else: - database = runner_conf.dlt_meta_schema - dlt_lib.append(jobs.compute.Library(whl=runner_conf.remote_whl_path.replace("/Workspace", "dbfs:"))) - return database, dlt_lib - def create_eventhub_workflow_spec(self, runner_conf: DLTMetaRunnerConf): """Create Job specification.""" - database, dlt_lib = self.init_db_dltlib(runner_conf) dltmeta_environments = [ jobs.JobEnvironment( environment_key="dl_meta_int_env", @@ -460,7 +444,7 @@ def create_eventhub_workflow_spec(self, runner_conf: DLTMetaRunnerConf): entry_point="run", named_parameters={ "onboard_layer": "bronze", - "database": database, + "database": f"{runner_conf.uc_catalog_name}.{runner_conf.dlt_meta_schema}", "onboarding_file_path": f"{runner_conf.uc_volume_path}/{self.base_dir}/conf/onboarding.json", "silver_dataflowspec_table": "silver_dataflowspec_cdc", @@ -508,7 +492,7 @@ def create_eventhub_workflow_spec(self, runner_conf: DLTMetaRunnerConf): notebook_path=f"{runner_conf.runners_nb_path}/runners/validate", base_parameters={ "run_id": runner_conf.run_id, - "uc_enabled": "True" if runner_conf.uc_catalog_name else "False", + "uc_enabled": "True", "uc_catalog_name": runner_conf.uc_catalog_name, "bronze_schema": runner_conf.bronze_schema, "output_file_path": f"/Workspace{runner_conf.test_output_file_path}" @@ -520,7 +504,6 @@ def create_eventhub_workflow_spec(self, runner_conf: DLTMetaRunnerConf): def create_kafka_workflow_spec(self, runner_conf: DLTMetaRunnerConf): """Create Job specification.""" - database, dlt_lib = self.init_db_dltlib(runner_conf) dltmeta_environments = [ jobs.JobEnvironment( environment_key="dl_meta_int_env", @@ -544,7 +527,7 @@ def create_kafka_workflow_spec(self, runner_conf: DLTMetaRunnerConf): entry_point="run", named_parameters={ "onboard_layer": "bronze", - "database": database, + "database": f"{runner_conf.uc_catalog_name}.{runner_conf.dlt_meta_schema}", "onboarding_file_path": f"{runner_conf.dbfs_tmp_path}/{self.base_dir}/conf/onboarding.json", "silver_dataflowspec_table": "silver_dataflowspec_cdc", @@ -555,7 +538,7 @@ def create_kafka_workflow_spec(self, runner_conf: DLTMetaRunnerConf): "bronze_dataflowspec_path": f"{self._install_folder()}/dltmeta/data/dlt_spec/bronze", "overwrite": "True", "env": runner_conf.env, - "uc_enabled": "True" if runner_conf.uc_catalog_name else "False" + "uc_enabled": "True" } ) ), @@ -587,7 +570,7 @@ def create_kafka_workflow_spec(self, runner_conf: DLTMetaRunnerConf): notebook_path=f"{runner_conf.runners_nb_path}/runners/validate", base_parameters={ "run_id": runner_conf.run_id, - "uc_enabled": "True" if runner_conf.uc_catalog_name else "False", + "uc_enabled": "True" , "uc_catalog_name": runner_conf.uc_catalog_name, "bronze_schema": runner_conf.bronze_schema, "output_file_path": f"/Workspace{runner_conf.test_output_file_path}" @@ -918,7 +901,6 @@ def init_dltmeta_runner_conf(self, runner_conf: DLTMetaRunnerConf): self.initialize_uc_resources(runner_conf) self.generate_onboarding_file(runner_conf) - print("int_tests_dir: ", runner_conf.int_tests_dir) self.copy(runner_conf) print( @@ -939,7 +921,6 @@ def init_dltmeta_runner_conf(self, runner_conf: DLTMetaRunnerConf): ) print(f"uploading to {runner_conf.runners_nb_path} complete!!!") - if runner_conf.uc_catalog_name: self.build_and_upload_package(runner_conf) @@ -995,9 +976,10 @@ def create_cluster(self, runner_conf: DLTMetaRunnerConf): def run(self, runner_conf: DLTMetaRunnerConf): try: self.init_dltmeta_runner_conf(runner_conf) - # self.create_bronze_silver_dlt(runner_conf) - # self.launch_workflow(runner_conf) - # self.download_test_results(runner_conf) + exit() + self.create_bronze_silver_dlt(runner_conf) + self.launch_workflow(runner_conf) + self.download_test_results(runner_conf) except Exception as e: print(e) # finally: @@ -1104,8 +1086,8 @@ def main(): args = process_arguments() workspace_client = get_workspace_api_client(args["profile"]) integration_test_runner = DLTMETARunner(args, workspace_client, "integration_tests") - exit() runner_conf = integration_test_runner.init_runner_conf() + exit() integration_test_runner.run(runner_conf) def process_arguments() -> dict[str: str]: From 3e1e4868ff7a5c6d10681f7db042ef1f50badda5 Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Wed, 2 Oct 2024 17:20:09 -0400 Subject: [PATCH 22/59] uc resource generation update --- integration_tests/run_integration_tests.py | 127 ++++++++++----------- 1 file changed, 61 insertions(+), 66 deletions(-) diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index 018fb65..761afed 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -47,8 +47,6 @@ class DLTMetaRunnerConf: The path to the onboarding file to use for the test run. dbfs_tmp_path : str, optional The temporary DBFS path to use for the test run. - uc_volume_name : str, optional - The name of the unified volume to use for the test run. int_tests_dir : str, optional The directory containing the integration tests. dlt_meta_schema : str, optional @@ -98,26 +96,25 @@ class DLTMetaRunnerConf: username: str = None run_name: str = None uc_catalog_name: str = None + uc_volume_name: str = "dlt_meta_files" onboarding_file_path: str = None - onboarding_A2_file_path: str = None - onboarding_fanout_file_path: str = None - dbfs_tmp_path: str = None - uc_volume_name: str = None - int_tests_dir: str = None + onboarding_A2_file_path: str = "integration_tests/conf/onboarding_A2.json" + onboarding_fanout_file_path: str = "integration_tests/conf/onboarding.json" + int_tests_dir: str = "file:./integration_tests" dlt_meta_schema: str = None bronze_schema: str = None silver_schema: str = None runners_nb_path: str = None runners_full_local_path: str = None source: str = None - cloudfiles_template: str = None - cloudfiles_A2_template: str = None + cloudfiles_template: str = "integration_tests/conf/cloudfiles-onboarding.template" + cloudfiles_A2_template: str = "integration_tests/conf/cloudfiles-onboarding_A2.template" onboarding_fanout_templates: str = None - eventhub_template: str = None + eventhub_template: str = "integration_tests/conf/eventhub-onboarding.template", eventhub_input_data: str = None eventhub_append_flow_input_data: str = None - kafka_template: str = None - env: str = None + kafka_template: str = "integration_tests/conf/kafka-onboarding.template" + env: str = "it" whl_path: str = None volume_info: VolumeInfo = None uc_volume_path: str = None @@ -163,21 +160,12 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: run_id=run_id, username=self.wsi._my_username, uc_catalog_name=self.args["uc_catalog_name"], - uc_volume_name=f"{self.args['uc_catalog_name']}_volume_{run_id}", - int_tests_dir="file:./integration_tests", dlt_meta_schema=f"dlt_meta_dataflowspecs_it_{run_id}", bronze_schema=f"dlt_meta_bronze_it_{run_id}", silver_schema=f"dlt_meta_silver_it_{run_id}", runners_nb_path=f"/Users/{self.wsi._my_username}/dlt_meta_int_tests/{run_id}", source=self.args["source"], node_type_id=cloud_node_type_id_dict[self.args["cloud_provider_name"]], - cloudfiles_template="integration_tests/conf/cloudfiles-onboarding.template", - cloudfiles_A2_template="integration_tests/conf/cloudfiles-onboarding_A2.template", - eventhub_template="integration_tests/conf/eventhub-onboarding.template", - kafka_template="integration_tests/conf/kafka-onboarding.template", - onboarding_file_path="integration_tests/conf/onboarding.json", - onboarding_A2_file_path="integration_tests/conf/onboarding_A2.json", - env="it", test_output_file_path=( f"/Users/{self.wsi._my_username}/dlt_meta_int_tests/" f"{run_id}/integration-test-output.csv" @@ -198,6 +186,9 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: return runner_conf + + + def _install_folder(self): return f"/Users/{self.wsi._my_username}/dlt-meta" @@ -895,11 +886,35 @@ def copy(self, runner_conf: DLTMetaRunnerConf): # f"dbfs_path={dst}/{base_dir_name}/{target_dir}/{filename}") self.ws.dbfs.upload(dbfs_path, contents, overwrite=True) + def initialize_uc_resources(self, runner_conf): + '''Create UC schemas and volumes needed to run the integration tests''' + SchemasAPI(self.ws.api_client).create(catalog_name=runner_conf.uc_catalog_name, + name=runner_conf.dlt_meta_schema, + comment="dlt_meta framework schema") + SchemasAPI(self.ws.api_client).create(catalog_name=runner_conf.uc_catalog_name, + name=runner_conf.bronze_schema, + comment="bronze_schema") + if runner_conf.source == "cloudfiles": + SchemasAPI(self.ws.api_client).create(catalog_name=runner_conf.uc_catalog_name, + name=runner_conf.silver_schema, + comment="silver_schema") + volume_info = self.ws.volumes.create(catalog_name=runner_conf.uc_catalog_name, + schema_name=runner_conf.dlt_meta_schema, + name=runner_conf.uc_volume_name, + volume_type=VolumeType.MANAGED) + runner_conf.volume_info = volume_info + runner_conf.uc_volume_path = (f"/Volumes/{runner_conf.volume_info.catalog_name}/" + f"{runner_conf.volume_info.schema_name}/{runner_conf.volume_info.name}/" + ) + + def init_dltmeta_runner_conf(self, runner_conf: DLTMetaRunnerConf): """Create testing metadata including schemas, volumes, and uploading necessary notebooks""" - if runner_conf.uc_catalog_name: - self.initialize_uc_resources(runner_conf) + + # Generate uc schemas, volumes and upload onboarding files + self.initialize_uc_resources(runner_conf) self.generate_onboarding_file(runner_conf) + exit() print("int_tests_dir: ", runner_conf.int_tests_dir) self.copy(runner_conf) @@ -924,26 +939,7 @@ def init_dltmeta_runner_conf(self, runner_conf: DLTMetaRunnerConf): if runner_conf.uc_catalog_name: self.build_and_upload_package(runner_conf) - def initialize_uc_resources(self, runner_conf): - '''Create UC schemas and volumes needed to run the integration tests''' - SchemasAPI(self.ws.api_client).create(catalog_name=runner_conf.uc_catalog_name, - name=runner_conf.dlt_meta_schema, - comment="dlt_meta framework schema") - SchemasAPI(self.ws.api_client).create(catalog_name=runner_conf.uc_catalog_name, - name=runner_conf.bronze_schema, - comment="bronze_schema") - if runner_conf.source and runner_conf.source == "cloudfiles": - SchemasAPI(self.ws.api_client).create(catalog_name=runner_conf.uc_catalog_name, - name=runner_conf.silver_schema, - comment="silver_schema") - volume_info = self.ws.volumes.create(catalog_name=runner_conf.uc_catalog_name, - schema_name=runner_conf.dlt_meta_schema, - name=runner_conf.dlt_meta_schema, - volume_type=VolumeType.MANAGED) - runner_conf.volume_info = volume_info - runner_conf.uc_volume_path = (f"/Volumes/{runner_conf.volume_info.catalog_name}/" - f"{runner_conf.volume_info.schema_name}/{runner_conf.volume_info.name}/" - ) + def create_cluster(self, runner_conf: DLTMetaRunnerConf): print("Cluster creation started...") @@ -973,19 +969,6 @@ def create_cluster(self, runner_conf: DLTMetaRunnerConf): runner_conf.cluster_id = clstr.cluster_id webbrowser.open(f"{self.ws.config.host}/compute/clusters/{clstr.cluster_id}?o={self.ws.get_workspace_id()}") - def run(self, runner_conf: DLTMetaRunnerConf): - try: - self.init_dltmeta_runner_conf(runner_conf) - exit() - self.create_bronze_silver_dlt(runner_conf) - self.launch_workflow(runner_conf) - self.download_test_results(runner_conf) - except Exception as e: - print(e) - # finally: - # print("Cleaning up...") - # self.clean_up(runner_conf) - def download_test_results(self, runner_conf: DLTMetaRunnerConf): ws_output_file = self.ws.workspace.download(runner_conf.test_output_file_path) with open(f"integration_test_output_{runner_conf.run_id}.csv", "wb") as output_file: @@ -1071,6 +1054,19 @@ def clean_up(self, runner_conf: DLTMetaRunnerConf): self.ws.schemas.delete(schema.full_name) print("Cleaning up complete!!!") + def run(self, runner_conf: DLTMetaRunnerConf): + try: + self.init_dltmeta_runner_conf(runner_conf) + exit() + self.create_bronze_silver_dlt(runner_conf) + self.launch_workflow(runner_conf) + self.download_test_results(runner_conf) + except Exception as e: + print(e) + # finally: + # print("Cleaning up...") + # self.clean_up(runner_conf) + def get_workspace_api_client(profile=None) -> WorkspaceClient: """Get api client with config.""" @@ -1080,16 +1076,6 @@ def get_workspace_api_client(profile=None) -> WorkspaceClient: workspace_client = WorkspaceClient(host=input('Databricks Workspace URL: '), token=input('Token: ')) return workspace_client - -def main(): - """Entry method to run integration tests.""" - args = process_arguments() - workspace_client = get_workspace_api_client(args["profile"]) - integration_test_runner = DLTMETARunner(args, workspace_client, "integration_tests") - runner_conf = integration_test_runner.init_runner_conf() - exit() - integration_test_runner.run(runner_conf) - def process_arguments() -> dict[str: str]: """ Get, process, and validate the command line arguements @@ -1232,5 +1218,14 @@ def check_mandatory_arg(args, mandatory_args): print(f"Processing comand line arguments Complete: {args}") return args + +def main(): + """Entry method to run integration tests.""" + args = process_arguments() + workspace_client = get_workspace_api_client(args["profile"]) + integration_test_runner = DLTMETARunner(args, workspace_client, "integration_tests") + runner_conf = integration_test_runner.init_runner_conf() + integration_test_runner.run(runner_conf) + if __name__ == "__main__": main() From 6a8eb47b9ede8405bcc207afaa71fcb14502088a Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Thu, 3 Oct 2024 17:11:28 -0400 Subject: [PATCH 23/59] continued unused file, and dbfs code removal along with code clean-up and simplification --- .../conf/silver_transformations.json | 27 -- integration_tests/run_integration_tests.py | 272 ++++++------------ 2 files changed, 88 insertions(+), 211 deletions(-) delete mode 100644 integration_tests/conf/silver_transformations.json diff --git a/integration_tests/conf/silver_transformations.json b/integration_tests/conf/silver_transformations.json deleted file mode 100644 index abe4cbe..0000000 --- a/integration_tests/conf/silver_transformations.json +++ /dev/null @@ -1,27 +0,0 @@ -[ - { - "target_table": "customers", - "select_exp": [ - "address", - "email", - "firstname", - "id", - "lastname", - "operation_date", - "operation", - "_rescued_data" - ] - }, - { - "target_table": "transactions", - "select_exp": [ - "id", - "customer_id", - "amount", - "item_count", - "operation_date", - "operation", - "_rescued_data" - ] - } -] \ No newline at end of file diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index 761afed..f94f7f3 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -97,10 +97,10 @@ class DLTMetaRunnerConf: run_name: str = None uc_catalog_name: str = None uc_volume_name: str = "dlt_meta_files" - onboarding_file_path: str = None + onboarding_file_path: str = "integration_tests/conf/onboarding.json" onboarding_A2_file_path: str = "integration_tests/conf/onboarding_A2.json" onboarding_fanout_file_path: str = "integration_tests/conf/onboarding.json" - int_tests_dir: str = "file:./integration_tests" + int_tests_dir: str = "./integration_tests" dlt_meta_schema: str = None bronze_schema: str = None silver_schema: str = None @@ -109,7 +109,7 @@ class DLTMetaRunnerConf: source: str = None cloudfiles_template: str = "integration_tests/conf/cloudfiles-onboarding.template" cloudfiles_A2_template: str = "integration_tests/conf/cloudfiles-onboarding_A2.template" - onboarding_fanout_templates: str = None + #onboarding_fanout_templates: str = None eventhub_template: str = "integration_tests/conf/eventhub-onboarding.template", eventhub_input_data: str = None eventhub_append_flow_input_data: str = None @@ -186,9 +186,6 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: return runner_conf - - - def _install_folder(self): return f"/Users/{self.wsi._my_username}/dlt-meta" @@ -290,7 +287,6 @@ def create_dlt_meta_pipeline(self, raise Exception("Pipeline creation failed") return created.pipeline_id - def create_cloudfiles_workflow_spec(self, runner_conf: DLTMetaRunnerConf): """ Create the CloudFiles workflow specification. @@ -571,16 +567,6 @@ def create_kafka_workflow_spec(self, runner_conf: DLTMetaRunnerConf): ] ) - def generate_onboarding_file(self, runner_conf: DLTMetaRunnerConf): - """Generate onboarding file from template.""" - source = runner_conf.source - if source == "cloudfiles": - self.create_cloudfiles_onboarding(runner_conf) - elif source == "eventhub": - self.create_eventhub_onboarding(runner_conf) - elif source == "kafka": - self.create_kafka_onboarding(runner_conf) - def create_kafka_onboarding(self, runner_conf: DLTMetaRunnerConf): """Create kafka onboarding file.""" with open(f"{runner_conf.kafka_template}") as f: @@ -735,116 +721,64 @@ def replace_eventhub_source_details_values(self, if 'eventhub_port' in source_value: data_flow[key][source_key] = source_value.format(eventhub_port=eventhub_port) + def initialize_uc_resources(self, runner_conf): + '''Create UC schemas and volumes needed to run the integration tests''' + SchemasAPI(self.ws.api_client).create(catalog_name=runner_conf.uc_catalog_name, + name=runner_conf.dlt_meta_schema, + comment="dlt_meta framework schema") + SchemasAPI(self.ws.api_client).create(catalog_name=runner_conf.uc_catalog_name, + name=runner_conf.bronze_schema, + comment="bronze_schema") + if runner_conf.source == "cloudfiles": + SchemasAPI(self.ws.api_client).create(catalog_name=runner_conf.uc_catalog_name, + name=runner_conf.silver_schema, + comment="silver_schema") + volume_info = self.ws.volumes.create(catalog_name=runner_conf.uc_catalog_name, + schema_name=runner_conf.dlt_meta_schema, + name=runner_conf.uc_volume_name, + volume_type=VolumeType.MANAGED) + runner_conf.volume_info = volume_info + runner_conf.uc_volume_path = (f"/Volumes/{runner_conf.volume_info.catalog_name}/" + f"{runner_conf.volume_info.schema_name}/{runner_conf.volume_info.name}/" + ) + + def generate_onboarding_file(self, runner_conf: DLTMetaRunnerConf): + """Generate onboarding file from template.""" + match runner_conf.source: + case "cloudfiles": + self.create_cloudfiles_onboarding(runner_conf) + case "eventhub": + self.create_eventhub_onboarding(runner_conf) + case "kafka": + self.create_kafka_onboarding(runner_conf) + def create_cloudfiles_onboarding(self, runner_conf: DLTMetaRunnerConf): - """Create onboarding file for cloudfiles as source.""" - with open(f"{runner_conf.cloudfiles_template}") as f: - onboard_obj = json.load(f) + """Create onboarding file when the source is cloudfiles by filling out the templates.""" + + string_subs = { + "{uc_volume_path}": runner_conf.uc_volume_path, + "{uc_catalog_name}": runner_conf.uc_catalog_name, + "{bronze_schema}": runner_conf.bronze_schema, + "{silver_schema}": runner_conf.silver_schema, + # "{run_id}": runner_conf.run_id, + } - for data_flow in onboard_obj: - for key, value in data_flow.items(): - self.__populate_source_details(runner_conf, data_flow, key, value) - if isinstance(value, list): - for val in value: - for k, v in val.items(): - self.__populate_source_details(runner_conf, val, k, v) - if 'dbfs_path' in value: - data_flow[key] = value.format(dbfs_path=runner_conf.dbfs_tmp_path) - if 'uc_volume_path' in value: - data_flow[key] = value.format(uc_volume_path=runner_conf.uc_volume_path) - if key == 'silver_append_flows': - counter = 0 - for flows in value: - for flow_key, flow_value in flows.items(): - if flow_key == "source_details": - for source_key, source_value in flows[flow_key].items(): - if '{uc_catalog_name}.{bronze_schema}' in source_value: - data_flow[key][counter][flow_key][source_key] = source_value.format( - uc_catalog_name=runner_conf.uc_catalog_name, - bronze_schema=runner_conf.bronze_schema) - elif 'run_id' in value: - data_flow[key] = value.format(run_id=runner_conf.run_id) - elif 'uc_catalog_name' in value and 'bronze_schema' in value: - if runner_conf.uc_catalog_name: - data_flow[key] = value.format( - uc_catalog_name=runner_conf.uc_catalog_name, - bronze_schema=runner_conf.bronze_schema - ) - else: - data_flow[key] = value.format( - uc_catalog_name=f"bronze_{runner_conf.run_id}", - bronze_schema="" - ).replace(".", "") + # Open the onboarding templates and sub in the proper table locations, paths, etc. + with open(f"{runner_conf.cloudfiles_template}", "r") as f: + onboard_json = f.read() - elif 'uc_catalog_name' in value and 'silver_schema' in value: - if runner_conf.uc_catalog_name: - data_flow[key] = value.format( - uc_catalog_name=runner_conf.uc_catalog_name, - silver_schema=runner_conf.silver_schema - ) - else: - data_flow[key] = value.format( - uc_catalog_name=f"silver_{runner_conf.run_id}", - silver_schema="" - ).replace(".", "") + with open(f"{runner_conf.cloudfiles_A2_template}") as f: + onboard_json_a2 = f.read() + + for key, val in string_subs.items(): + onboard_json = onboard_json.replace(key, val) + onboard_json_a2 = onboard_json_a2.replace(key, val) with open(runner_conf.onboarding_file_path, "w") as onboarding_file: - json.dump(onboard_obj, onboarding_file) + json.dump(json.loads(onboard_json), onboarding_file, indent=4) - if runner_conf.cloudfiles_A2_template: - with open(f"{runner_conf.cloudfiles_A2_template}") as f: - onboard_obj = json.load(f) - - for data_flow in onboard_obj: - for key, value in data_flow.items(): - self.__populate_source_details(runner_conf, data_flow, key, value) - if 'dbfs_path' in value: - data_flow[key] = value.format(dbfs_path=runner_conf.dbfs_tmp_path) - if 'uc_volume_path' in value: - data_flow[key] = value.format(uc_volume_path=runner_conf.uc_volume_path) - if 'uc_catalog_name' in value and 'bronze_schema' in value: - if runner_conf.uc_catalog_name: - data_flow[key] = value.format( - uc_catalog_name=runner_conf.uc_catalog_name, - bronze_schema=runner_conf.bronze_schema - ) - - with open(runner_conf.onboarding_A2_file_path, "w") as onboarding_file: - json.dump(onboard_obj, onboarding_file) - - if runner_conf.onboarding_fanout_templates: - with open(f"{runner_conf.onboarding_fanout_templates}") as f: - onboard_obj = json.load(f) - - for data_flow in onboard_obj: - for key, value in data_flow.items(): - self.__populate_source_details(runner_conf, data_flow, key, value) - if 'dbfs_path' in value: - data_flow[key] = value.format(dbfs_path=runner_conf.dbfs_tmp_path) - if 'uc_volume_path' in value: - data_flow[key] = value.format(uc_volume_path=runner_conf.uc_volume_path) - if 'uc_catalog_name' in value and 'bronze_schema' in value: - if runner_conf.uc_catalog_name: - data_flow[key] = value.format( - uc_catalog_name=runner_conf.uc_catalog_name, - bronze_schema=runner_conf.bronze_schema - ) - if 'uc_catalog_name' in value and 'silver_schema' in value: - if runner_conf.uc_catalog_name: - data_flow[key] = value.format( - uc_catalog_name=runner_conf.uc_catalog_name, - silver_schema=runner_conf.silver_schema - ) - - with open(runner_conf.onboarding_fanout_file_path, "w") as onboarding_file: - json.dump(onboard_obj, onboarding_file) - - def __populate_source_details(self, runner_conf, data_flow, key, value): - if key == "source_details": - for source_key, source_value in value.items(): - if 'dbfs_path' in source_value: - data_flow[key][source_key] = source_value.format(dbfs_path=runner_conf.dbfs_tmp_path) - elif 'uc_volume_path' in source_value: - data_flow[key][source_key] = source_value.format(uc_volume_path=runner_conf.uc_volume_path) + with open(runner_conf.onboarding_A2_file_path, "w") as onboarding_file_a2: + json.dump(json.loads(onboard_json_a2), onboarding_file_a2, indent=4) def copy(self, runner_conf: DLTMetaRunnerConf): if runner_conf.uc_catalog_name: @@ -866,65 +800,30 @@ def copy(self, runner_conf: DLTMetaRunnerConf): uc_volume_path = f"{dst}/{base_dir_name}/{target_dir}/{filename}".replace("//", "/") contents = open(os.path.join(root, filename), "rb") self.ws.files.upload(file_path=uc_volume_path, contents=contents, overwrite=True) - else: - src = runner_conf.int_tests_dir - dst = runner_conf.dbfs_tmp_path - main_dir = src.replace('file:', '') - base_dir_name = None - if main_dir.endswith('/'): - base_dir_name = main_dir[:-1] - if base_dir_name is None: - base_dir_name = main_dir[main_dir.rfind('/') + 1:] - else: - base_dir_name = base_dir_name[base_dir_name.rfind('/') + 1:-1] - for root, dirs, files in os.walk(main_dir): - for filename in files: - target_dir = root[root.index(main_dir) + len(main_dir):len(root)] - dbfs_path = f"{dst}/{base_dir_name}/{target_dir}/{filename}" - contents = open(os.path.join(root, filename), "rb") - # print(f"local_path={os.path.join(root, filename)}", - # f"dbfs_path={dst}/{base_dir_name}/{target_dir}/{filename}") - self.ws.dbfs.upload(dbfs_path, contents, overwrite=True) - def initialize_uc_resources(self, runner_conf): - '''Create UC schemas and volumes needed to run the integration tests''' - SchemasAPI(self.ws.api_client).create(catalog_name=runner_conf.uc_catalog_name, - name=runner_conf.dlt_meta_schema, - comment="dlt_meta framework schema") - SchemasAPI(self.ws.api_client).create(catalog_name=runner_conf.uc_catalog_name, - name=runner_conf.bronze_schema, - comment="bronze_schema") - if runner_conf.source == "cloudfiles": - SchemasAPI(self.ws.api_client).create(catalog_name=runner_conf.uc_catalog_name, - name=runner_conf.silver_schema, - comment="silver_schema") - volume_info = self.ws.volumes.create(catalog_name=runner_conf.uc_catalog_name, - schema_name=runner_conf.dlt_meta_schema, - name=runner_conf.uc_volume_name, - volume_type=VolumeType.MANAGED) - runner_conf.volume_info = volume_info - runner_conf.uc_volume_path = (f"/Volumes/{runner_conf.volume_info.catalog_name}/" - f"{runner_conf.volume_info.schema_name}/{runner_conf.volume_info.name}/" - ) + def upload_files_to_databricks(self, runner_conf: DLTMetaRunnerConf): + uc_vol_full_path = f"{runner_conf.uc_volume_path}/{runner_conf.int_tests_dir}" + print(f"Integration test file upload to {uc_vol_full_path} starting...") + # Upload the entire resources directory containing ddl and test data + for root, dirs, files in os.walk(f"{runner_conf.int_tests_dir}/resources"): + print(root, '|', dirs, '|', files) + print(f"Integration test file upload to {uc_vol_full_path} complete!!!") def init_dltmeta_runner_conf(self, runner_conf: DLTMetaRunnerConf): """Create testing metadata including schemas, volumes, and uploading necessary notebooks""" # Generate uc schemas, volumes and upload onboarding files - self.initialize_uc_resources(runner_conf) - self.generate_onboarding_file(runner_conf) - exit() + # self.initialize_uc_resources(runner_conf) + # self.generate_onboarding_file(runner_conf) + self.upload_files_to_databricks(runner_conf) - print("int_tests_dir: ", runner_conf.int_tests_dir) - self.copy(runner_conf) - print( - f"uploading to {runner_conf.runners_nb_path}/{self.base_dir}/ complete!!!" - ) + exit() # Upload required notebooks for the given source - print(f"uploading to {runner_conf.runners_nb_path} started") + print(f"Notebooks upload to {runner_conf.runners_nb_path} started...") self.ws.workspace.mkdirs(f"{runner_conf.runners_nb_path}/runners") + for notebook in os.listdir(runner_conf.runners_full_local_path): local_path = os.path.join(runner_conf.runners_full_local_path, notebook) with open(local_path, "rb") as nb_file: @@ -933,14 +832,12 @@ def init_dltmeta_runner_conf(self, runner_conf: DLTMetaRunnerConf): format=ImportFormat.SOURCE, language=Language.PYTHON, content=nb_file.read(), - ) - print(f"uploading to {runner_conf.runners_nb_path} complete!!!") + ) + print(f"Notebooks upload to {runner_conf.runners_nb_path} complete!!!") if runner_conf.uc_catalog_name: self.build_and_upload_package(runner_conf) - - def create_cluster(self, runner_conf: DLTMetaRunnerConf): print("Cluster creation started...") if runner_conf.uc_catalog_name: @@ -1055,14 +952,21 @@ def clean_up(self, runner_conf: DLTMetaRunnerConf): print("Cleaning up complete!!!") def run(self, runner_conf: DLTMetaRunnerConf): - try: - self.init_dltmeta_runner_conf(runner_conf) - exit() - self.create_bronze_silver_dlt(runner_conf) - self.launch_workflow(runner_conf) - self.download_test_results(runner_conf) - except Exception as e: - print(e) + + self.init_dltmeta_runner_conf(runner_conf) + exit() + self.create_bronze_silver_dlt(runner_conf) + self.launch_workflow(runner_conf) + self.download_test_results(runner_conf) + + # try: + # self.init_dltmeta_runner_conf(runner_conf) + # exit() + # self.create_bronze_silver_dlt(runner_conf) + # self.launch_workflow(runner_conf) + # self.download_test_results(runner_conf) + # except Exception as e: + # print(e) # finally: # print("Cleaning up...") # self.clean_up(runner_conf) @@ -1189,7 +1093,7 @@ def process_arguments() -> dict[str: str]: parser.add_argument(f"--{arg[0]}", help=arg[1], type=arg[2], required=arg[3]) args = vars(parser.parse_args()) - def check_mandatory_arg(args, mandatory_args): + def check_cond_mandatory_arg(args, mandatory_args): """Post argument parsing check for conditionally required arguments""" for mand_arg in mandatory_args: if args[mand_arg] is None: @@ -1197,7 +1101,7 @@ def check_mandatory_arg(args, mandatory_args): # Check for arguments that are required depending on the selected source if args["source"] == "eventhub": - check_mandatory_arg( + check_cond_mandatory_arg( args, [ "eventhub_name", @@ -1210,7 +1114,7 @@ def check_mandatory_arg(args, mandatory_args): ], ) elif args["source"] == "kafka": - check_mandatory_arg( + check_cond_mandatory_arg( args, ["kafka_topic_name", "kafka_broker"], ) From 6e1d8313cce3382a657563dccd663988b4739d51 Mon Sep 17 00:00:00 2001 From: ravi-databricks Date: Thu, 3 Oct 2024 15:26:42 -0700 Subject: [PATCH 24/59] Added: 1.Integration test workflow for apply_changes_from_snapshot 2.Fixed lambda function for next_snapshot_version --- demo/launch_dais_demo.py | 2 +- examples/dlt_meta_pipeline_snapshot.ipynb | 95 +++++++++++++ ...dlt_meta_pipeline_snapshot_ingestion.ipynb | 127 ------------------ integration_tests/README.md | 14 +- .../conf/snapshot-onboarding.template | 44 ++++++ integration_tests/dbc/snapshot_runners.dbc | Bin 0 -> 3532 bytes .../incremental_snapshots/products/LOAD_2.csv | 21 +++ .../incremental_snapshots/products/LOAD_3.csv | 21 +++ .../incremental_snapshots/stores/LOAD_2.csv | 5 + .../incremental_snapshots/stores/LOAD_3.csv | 5 + .../data/snapshots/products/LOAD_1.csv | 21 +++ .../data/snapshots/stores/LOAD_1.csv | 5 + integration_tests/run_integration_tests.py | 125 ++++++++++++++++- src/dataflow_pipeline.py | 33 +++-- src/onboard_dataflowspec.py | 29 ++-- ...onboarding_applychanges_from_snapshot.json | 84 ++++++------ tests/test_cli.py | 4 +- tests/test_onboard_dataflowspec.py | 3 +- 18 files changed, 435 insertions(+), 203 deletions(-) create mode 100644 examples/dlt_meta_pipeline_snapshot.ipynb delete mode 100644 examples/dlt_meta_pipeline_snapshot_ingestion.ipynb create mode 100644 integration_tests/conf/snapshot-onboarding.template create mode 100644 integration_tests/dbc/snapshot_runners.dbc create mode 100644 integration_tests/resources/data/snapshots/incremental_snapshots/products/LOAD_2.csv create mode 100644 integration_tests/resources/data/snapshots/incremental_snapshots/products/LOAD_3.csv create mode 100644 integration_tests/resources/data/snapshots/incremental_snapshots/stores/LOAD_2.csv create mode 100644 integration_tests/resources/data/snapshots/incremental_snapshots/stores/LOAD_3.csv create mode 100644 integration_tests/resources/data/snapshots/products/LOAD_1.csv create mode 100644 integration_tests/resources/data/snapshots/stores/LOAD_1.csv diff --git a/demo/launch_dais_demo.py b/demo/launch_dais_demo.py index 06251c5..d8b801d 100644 --- a/demo/launch_dais_demo.py +++ b/demo/launch_dais_demo.py @@ -41,7 +41,7 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: bronze_schema=f"dlt_meta_bronze_dais_demo_{run_id}", silver_schema=f"dlt_meta_silver_dais_demo_{run_id}", runners_nb_path=f"/Users/{self._my_username(self.ws)}/dlt_meta_dais_demo/{run_id}", - node_type_id=cloud_node_type_id_dict[self.args.__dict__['cloud_provider_name']], + # node_type_id=cloud_node_type_id_dict[self.args.__dict__['cloud_provider_name']], # dbr_version=self.args.__dict__['dbr_version'], cloudfiles_template="demo/conf/onboarding.template", env="prod", diff --git a/examples/dlt_meta_pipeline_snapshot.ipynb b/examples/dlt_meta_pipeline_snapshot.ipynb new file mode 100644 index 0000000..25016c1 --- /dev/null +++ b/examples/dlt_meta_pipeline_snapshot.ipynb @@ -0,0 +1,95 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "18a5c12b-aa41-4465-b189-353db4370f83", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "%pip install dlt-meta" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Databricks notebook source\n", + "# DBTITLE 1,DLT Snapshot Processing Logic\n", + "import dlt\n", + "from src.dataflow_spec import BronzeDataflowSpec\n", + "\n", + "def exist(path):\n", + " try:\n", + " if dbutils.fs.ls(path) is None:\n", + " return False\n", + " else:\n", + " return True\n", + " except:\n", + " return False\n", + "\n", + "\n", + "def next_snapshot_and_version(latest_snapshot_version, dataflow_spec):\n", + " latest_snapshot_version = latest_snapshot_version or 0\n", + " next_version = latest_snapshot_version + 1 \n", + " bronze_dataflow_spec: BronzeDataflowSpec = dataflow_spec\n", + " options = bronze_dataflow_spec.readerConfigOptions\n", + " snapshot_format = bronze_dataflow_spec.sourceDetails[\"snapshot_format\"]\n", + " snapshot_root_path = bronze_dataflow_spec.sourceDetails['path'] \n", + " snapshot_path = f\"{snapshot_root_path}{next_version}.csv\"\n", + " if (exist(snapshot_path)):\n", + " snapshot = spark.read.format(snapshot_format).options(**options).load(snapshot_path)\n", + " return (snapshot, next_version)\n", + " else:\n", + " # No snapshot available\n", + " return None \n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "application/vnd.databricks.v1+cell": { + "cellMetadata": {}, + "inputWidgets": {}, + "nuid": "de72e08f-5432-4e56-b17d-cadee25b4714", + "showTitle": false, + "title": "" + } + }, + "outputs": [], + "source": [ + "layer = spark.conf.get(\"layer\", None)\n", + "\n", + "from src.dataflow_pipeline import DataflowPipeline\n", + "DataflowPipeline.invoke_dlt_pipeline(spark, layer)" + ] + } + ], + "metadata": { + "application/vnd.databricks.v1+notebook": { + "dashboards": [], + "language": "python", + "notebookMetadata": { + "pythonIndentUnit": 2 + }, + "notebookName": "dlt_meta_pipeline", + "notebookOrigID": 4156927443107021, + "widgets": {} + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/examples/dlt_meta_pipeline_snapshot_ingestion.ipynb b/examples/dlt_meta_pipeline_snapshot_ingestion.ipynb deleted file mode 100644 index 4c17f9a..0000000 --- a/examples/dlt_meta_pipeline_snapshot_ingestion.ipynb +++ /dev/null @@ -1,127 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "cellMetadata": {}, - "inputWidgets": {}, - "nuid": "18a5c12b-aa41-4465-b189-353db4370f83", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "%pip install dlt-meta" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Databricks notebook source\n", - "# DBTITLE 1,DLT Snapshot Processing Logic\n", - "import dlt\n", - "from datetime import timedelta\n", - "from datetime import datetime\n", - "\n", - "\n", - "def exist(path):\n", - " try:\n", - " if dbutils.fs.ls(path) is None:\n", - " return False\n", - " else:\n", - " return True\n", - " except:\n", - " return False\n", - "\n", - "\n", - "snapshot_root_path = \"path\" # read from dataflowspec source_details.path\n", - "\n", - "# List all objects in the bucket using dbutils.fs\n", - "object_paths = dbutils.fs.ls(snapshot_root_path)\n", - "\n", - "datetimes = []\n", - "for path in object_paths:\n", - " # Parse the datetime string to a datetime object\n", - " datetime_obj = datetime.strptime(path.name.strip('/\"'), '%Y-%m-%d %H')\n", - " datetimes.append(datetime_obj)\n", - "\n", - "# Find the earliest datetime\n", - "earliest_datetime = min(datetimes)\n", - "\n", - "# Convert the earliest datetime back to a string if needed\n", - "earliest_datetime_str = earliest_datetime.strftime('%Y-%m-%d %H')\n", - "\n", - "print(f\"The earliest datetime in the bucket is: {earliest_datetime_str}\")\n", - "\n", - "\n", - "def next_snapshot_and_version(latest_snapshot_datetime):\n", - " latest_datetime_str = latest_snapshot_datetime or earliest_datetime_str\n", - " if latest_snapshot_datetime is None:\n", - " snapshot_path = f\"{snapshot_root_path}/{earliest_datetime_str}\"\n", - " print(f\"Reading earliest snapshot from {snapshot_path}\")\n", - " earliest_snapshot = spark.read.format(\"parquet\").load(snapshot_path)\n", - " return earliest_snapshot, earliest_datetime_str\n", - " else:\n", - " latest_datetime = datetime.strptime(latest_datetime_str, '%Y-%m-%d %H')\n", - " # Calculate the next datetime\n", - " increment = timedelta(hours=1) # Increment by 1 hour because we are provided hourly snapshots\n", - " next_datetime = latest_datetime + increment\n", - " print(f\"The next snapshot version is : {next_datetime}\")\n", - "\n", - " # Convert the next_datetime to a string with the desired format\n", - " next_snapshot_datetime_str = next_datetime.strftime('%Y-%m-%d %H')\n", - " snapshot_path = f\"{snapshot_root_path}/{next_snapshot_datetime_str}\"\n", - " print(\"Attempting to read next snapshot from \" + snapshot_path)\n", - "\n", - " if (exist(snapshot_path)):\n", - " snapshot = spark.read.format(\"parquet\").load(snapshot_path)\n", - " return snapshot, next_snapshot_datetime_str\n", - " else:\n", - " print(f\"Couldn't find snapshot data at {snapshot_path}\")\n", - " return None" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "application/vnd.databricks.v1+cell": { - "cellMetadata": {}, - "inputWidgets": {}, - "nuid": "de72e08f-5432-4e56-b17d-cadee25b4714", - "showTitle": false, - "title": "" - } - }, - "outputs": [], - "source": [ - "layer = spark.conf.get(\"layer\", None)\n", - "from src.dataflow_pipeline import DataflowPipeline\n", - "DataflowPipeline.invoke_dlt_pipeline(spark, layer, snapshot_reader_func=next_snapshot_and_version)" - ] - } - ], - "metadata": { - "application/vnd.databricks.v1+notebook": { - "dashboards": [], - "language": "python", - "notebookMetadata": { - "pythonIndentUnit": 2 - }, - "notebookName": "dlt_meta_pipeline", - "notebookOrigID": 4156927443107021, - "widgets": {} - }, - "language_info": { - "name": "python" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/integration_tests/README.md b/integration_tests/README.md index 2724732..a2292bb 100644 --- a/integration_tests/README.md +++ b/integration_tests/README.md @@ -35,12 +35,12 @@ ``` 9. Run integration test against cloudfile or eventhub or kafka using below options: If databricks profile configured using CLI then pass ```--profile ``` to below command otherwise provide workspace url and token in command line - - 9a. Run the command for cloudfiles + - 9a. Run the command for **cloudfiles** ```commandline python integration_tests/run_integration_tests.py --uc_catalog_name= --source=cloudfiles ``` - - 9b. Run the command for eventhub + - 9b. Run the command for **eventhub** ```commandline python integration_tests/run_integration_tests.py --uc_catalog_name=<<>> --source=eventhub --eventhub_name=iot --eventhub_secrets_scope_name=eventhubs_creds --eventhub_namespace=int_test-standard --eventhub_port=9093 --eventhub_producer_accesskey_name=producer --eventhub_consumer_accesskey_name=consumer ``` @@ -59,9 +59,9 @@ 6. Provide eventhub access key name : --eventhub_consumer_accesskey_name - - 9c. Run the command for kafka + - 9c. Run the command for **kafka** ```commandline - python3 integration_tests/run_integration_tests.py --uc_catalog_name=<<>> --source=kafka --kafka_topic_name=dlt-meta-integration-test --kafka_broker=host:9092 + python integration_tests/run_integration_tests.py --uc_catalog_name=<<>> --source=kafka --kafka_topic_name=dlt-meta-integration-test --kafka_broker=host:9092 ``` - - For kafka integration tests, the following are the prerequisites: @@ -70,6 +70,12 @@ - - Following are the mandatory arguments for running EventHubs integration test 1. Provide your kafka topic name : --kafka_topic_name 2. Provide kafka_broker : --kafka_broker + + - 9d. Run the command for **snapshot** + ```commandline + python integration_tests/run_integration_tests.py --source=snapshot --uc_catalog_name=<<>> + ``` + 10. Once finished integration output file will be copied locally to ```integration-test-output_.txt``` diff --git a/integration_tests/conf/snapshot-onboarding.template b/integration_tests/conf/snapshot-onboarding.template new file mode 100644 index 0000000..3b62403 --- /dev/null +++ b/integration_tests/conf/snapshot-onboarding.template @@ -0,0 +1,44 @@ +[ + { + "data_flow_id": "201", + "data_flow_group": "A1", + "source_system": "delta", + "source_format": "snapshot", + "source_details": { + "source_path_it": "{uc_volume_path}/integration_tests/resources/data/snapshots/products/LOAD_", + "snapshot_format": "csv" + }, + "bronze_reader_options": { + "header": "true" + }, + "bronze_database_it": "{uc_catalog_name}.{bronze_schema}", + "bronze_table": "products", + "bronze_apply_changes_from_snapshot": { + "keys": [ + "product_id" + ], + "scd_type": "2" + } + }, + { + "data_flow_id": "202", + "data_flow_group": "A1", + "source_system": "delta", + "source_format": "snapshot", + "source_details": { + "source_path_it": "{uc_volume_path}/integration_tests/resources/data/snapshots/stores/LOAD_", + "snapshot_format": "csv" + }, + "bronze_reader_options": { + "header": "true" + }, + "bronze_database_it": "{uc_catalog_name}.{bronze_schema}", + "bronze_table": "stores", + "bronze_apply_changes_from_snapshot": { + "keys": [ + "store_id" + ], + "scd_type": "2" + } + } + ] \ No newline at end of file diff --git a/integration_tests/dbc/snapshot_runners.dbc b/integration_tests/dbc/snapshot_runners.dbc new file mode 100644 index 0000000000000000000000000000000000000000..3a4e2d11b4a40f7de781e1b560b70a600cc9ee9c GIT binary patch literal 3532 zcmaJ^S5On$77Zmp009w@ju!$_gGx2@Dn(iVL8_515K0oI2qHxcf|Q_ii1Z*JD7^*g zAXTdLBGQXU69_zcGjFcfnRnNjGjnFntUYW0?6sGnE*Uu!fQpI=U|OVh7w{Ws0OSC7 zIKsuz0f~aRJDzp{0Ai7HbU{~WmA5{BVUKFX$#`xv48fWJ;8T``DF@F0-z^2tW8!oE zX4lbe4h_v-aBM=VW;upGAsDrLB02&GFUqT%1_k(cs zC=69+5xFJ26*4gML$RBN-kT0{FX*~Pw7dvIjBF}4{LPA=`>V1M)~Wr^)K?zal_QjP z+QO)+cRqwh;9CUm5ma|0rL0D_D9=^#;_HvjRoVWlDxp7AxgcCnw)So)TXzQ(+}6{@ z)4|OJ;Q;aUMLByQHq6~UCZrioMS^SIcsa3E*tp$PRw~52@Y8ya^OB%N!wu&<1$eQf z+BZ8%&knfwyF9$4%f=cscxZo2|J>e04Pk3Z@rkpWcz=FLu>*N2nBi?RuU+U=6zz^oN{#K;UjJLn>eFc0#}B=nyPc?LCA6jSsl?_q+{z_x zsn?)(y=ZH~aZvGDK1TYf{qr{#K^VheVPw>ye6{pZsu1xDYy!fce8p#G91{D(8&nr( zy$}b(J-HU~=59o2j`>D^WSo&1u3i4p>-)TgQHn*QQD;BE#xkr{7}d?g)%%a7pczr` z9I<^_v9%(Saj*_u!vd(2%{Si(pjxevSLKdZv~(;ujQyBvb$XsN3ts)e8AKrmRV7^B zfqZJMpCX$V+zU-Yt>2*}*iSfI*mO*akiQzHT4Ep(C!%~c(ztDjMXK!uY->&{N!WYG z8LNo}%X|sw%eQgEI+M6H^iS+HCv|+{pHV;YDS?dXEZddYE+Iik*j0NyapqJ|@iqGG zDx+IkzPMBR@P)qRWJ-naT~OYvI>s#^OHnmR>#4IlbkOm9;Sd4le= z(lx(cMQF;Ex#*+|%}vP8=P!uQUtmR2(?>13FCqf&tXK%Jg!Cjyb$B9MWN3n;)(1!W zUxsk>kAqzo>%Q&Oi0$8J%u(P;`3vN4!@AEWzS62D{+u*9^dl~%^=U2QiPHoZVjL0` zUJp+cu!%?UiLbZK@~h!&`m^9H^zT(P=i0}M2{R98WS6K+dGGq=aUy0kUNYv~hIhpt z!*8TpMW)&upWL6H^ep@q*56ysBqYsC-d@dse64@el123a)(Mck3kJB7eB9z)R@wq6>q;7l=xh(51Uh(c(J(KfydP=Bw1JvnTB@T zJ%r{0gInm5ORL4IEIck53w(`p9#TT!^c9Y&kDzG!-Q3MeGJn`?w03BM>Uibh$nyqb zMVOsk$9PBX<{)xmEzdfCG|i+a7vT>0u#??8sbE|*qE8`}+GB6$xjJMCy*YOOt=)A+ z--F&$SG2dv@>8PI&+56#gPkU7Zu?V?!X$Td`>@%>wuA0`yr}wW$8F}8cWT3HhI>$x zdOFjy0mDVgK+e#B?aXwxf||hD3k!ks{MVyGI`V{QYi1NofKxq&!_f#UI(oMDXc+5Tv_w*8Z&E-jn<$3o_qzM$7BDh}zYHBD)Y6GRNat&bP3~?kmQD zS}WHY7#|r1FguqS2=}???TiNc!hFwo^Y;8Ofg1VvDCrz{YMx2ewx&;<;lQek=PtmDT5wCaVG4squJF2lxdk7Z?t+;S@{N88w=-01* z$^mA?zlbEKHLKNglNRx>Jz^tDL1)NL>*SR31_Mp~s*VSU$7+)bR0SqlEBBb8;ep-eWLg^*zWSF06j?JSzqDx`#HG4LHd4Rev&aDzzDX}dYDT<8;UU@d<=X@LFR z<)|EMVN_6I`t5gXALp`_@3kukbBK-ai1IBGoWEN*nKwM#qX<4nl{K7VxwD*dROGMrj51B zX~MZ4fB~oK7-e2t4>`y{W}<|)x&Z``n+=XYH5Pnj6W)ql))#&H1{MHh<_a~+I%CU3 zKK5Md2jGQkSDy;|Ca2=k++8EIE3eHRYD|`e28MNsq$@GXJAH66v8I2uobZs;>OceD z&Blb`KOAHR!bn^>4@nSi%qt9iAjZc1?`~^{EgiBC zYi8y|m7;sMOY5SoH=NCMj|JA|hnDsj6mnfWD>ZFc1E2^=+1ZzZIiM+S#GNEIOCW~t zDyWppuCFpgUUa?1z^axiSg^P*v_bBT)P08~1M$er1sc8NSX2e#h?ndZizjW%}LG zB>Wn=++dPmw%DhXr3p>hS~obV@suHajm6$}p-@uY+Zm(B=QziCjgrUspk5BdjW{8Eze-?sh(TANF) literal 0 HcmV?d00001 diff --git a/integration_tests/resources/data/snapshots/incremental_snapshots/products/LOAD_2.csv b/integration_tests/resources/data/snapshots/incremental_snapshots/products/LOAD_2.csv new file mode 100644 index 0000000..64568e9 --- /dev/null +++ b/integration_tests/resources/data/snapshots/incremental_snapshots/products/LOAD_2.csv @@ -0,0 +1,21 @@ +dmsTimestamp,product_id,name,price +2022-06-24 18:53:24.619896,1,shorts_v2,793.50 +2022-06-24 18:53:24.619917,2,hat_v2,598.91 +2022-06-24 18:53:24.619920,3,hat_v2,914.34 +2022-06-24 18:53:24.619923,4,accessories,717.76 +2022-06-24 18:53:24.619925,5,sneakers,975.06 +2022-06-24 18:53:24.619928,6,shorts_v2,875.98 +2022-06-24 18:53:24.619931,7,coat,170.43 +2022-06-24 18:53:24.619933,8,hat_v2,931.89 +2022-06-24 18:53:24.619936,9,shorts_v2,627.72 +2022-06-24 18:53:24.619938,10,shirt,214.82 +2022-06-24 18:53:24.619941,11,sweater,534.26 +2022-06-24 18:53:24.619943,12,boots,933.89 +2022-06-24 18:53:24.619946,13,cap,600.41 +2022-06-24 18:53:24.619948,14,cap,608.73 +2022-06-24 18:53:24.619951,15,hat_v2,747.17 +2022-06-24 18:53:24.619953,16,accessories,487.74 +2022-06-24 18:53:24.619956,17,coat,236.99 +2022-06-24 18:53:24.619958,18,accessories,960.39 +2022-06-24 18:53:24.619961,19,sweatshirt,600.43 +2022-06-24 18:53:24.619964,20,shirt,492.41 diff --git a/integration_tests/resources/data/snapshots/incremental_snapshots/products/LOAD_3.csv b/integration_tests/resources/data/snapshots/incremental_snapshots/products/LOAD_3.csv new file mode 100644 index 0000000..fe24dce --- /dev/null +++ b/integration_tests/resources/data/snapshots/incremental_snapshots/products/LOAD_3.csv @@ -0,0 +1,21 @@ +dmsTimestamp,product_id,name,price +2022-06-24 18:53:24.619896,1,shorts_v3,793.50 +2022-06-24 18:53:24.619917,2,hat_v3,598.91 +2022-06-24 18:53:24.619920,3,hat_v3,914.34 +2022-06-24 18:53:24.619923,4,accessories,717.76 +2022-06-24 18:53:24.619925,5,sneakers,975.06 +2022-06-24 18:53:24.619928,6,shorts_v3,875.98 +2022-06-24 18:53:24.619931,7,coat,170.43 +2022-06-24 18:53:24.619933,8,hat_v3,931.89 +2022-06-24 18:53:24.619936,9,shorts_v3,627.72 +2022-06-24 18:53:24.619938,10,shirt,214.82 +2022-06-24 18:53:24.619941,11,sweater,534.26 +2022-06-24 18:53:24.619943,12,boots,933.89 +2022-06-24 18:53:24.619946,13,cap,600.41 +2022-06-24 18:53:24.619948,14,cap,608.73 +2022-06-24 18:53:24.619951,15,hat_v3,747.17 +2022-06-24 18:53:24.619953,16,accessories,487.74 +2022-06-24 18:53:24.619956,17,coat,236.99 +2022-06-24 18:53:24.619958,18,accessories,960.39 +2022-06-24 18:53:24.619961,19,sweatshirt,600.43 +2022-06-24 18:53:24.619964,20,shirt,492.41 diff --git a/integration_tests/resources/data/snapshots/incremental_snapshots/stores/LOAD_2.csv b/integration_tests/resources/data/snapshots/incremental_snapshots/stores/LOAD_2.csv new file mode 100644 index 0000000..e0ccf60 --- /dev/null +++ b/integration_tests/resources/data/snapshots/incremental_snapshots/stores/LOAD_2.csv @@ -0,0 +1,5 @@ +dmsTimestamp,store_id,address +2022-06-24 18:53:25.141463,1,"V2 6761 Brian Falls Navarrobury, VA 17977" +2022-06-24 18:53:25.141482,2,"V2 4215 Bruce Shoals Apt. 920 Port Travis, SC 71335" +2022-06-24 18:53:25.141487,3,"V2 96924 Gregory Mill Pricefurt, GA 68691" +2022-06-24 18:53:25.141491,4,"V2 070 Cynthia Cliff Paulport, FL 21469" diff --git a/integration_tests/resources/data/snapshots/incremental_snapshots/stores/LOAD_3.csv b/integration_tests/resources/data/snapshots/incremental_snapshots/stores/LOAD_3.csv new file mode 100644 index 0000000..7c4f1ef --- /dev/null +++ b/integration_tests/resources/data/snapshots/incremental_snapshots/stores/LOAD_3.csv @@ -0,0 +1,5 @@ +dmsTimestamp,store_id,address +2022-06-24 18:53:25.141463,1,"v3_6761 Brian Falls Navarrobury, VA 17977" +2022-06-24 18:53:25.141482,2,"v3_4215 Bruce Shoals Apt. 920 Port Travis, SC 71335" +2022-06-24 18:53:25.141487,3,"v3_96924 Gregory Mill Pricefurt, GA 68691" +2022-06-24 18:53:25.141491,4,"v3_070 Cynthia Cliff Paulport, FL 21469" diff --git a/integration_tests/resources/data/snapshots/products/LOAD_1.csv b/integration_tests/resources/data/snapshots/products/LOAD_1.csv new file mode 100644 index 0000000..c2907f6 --- /dev/null +++ b/integration_tests/resources/data/snapshots/products/LOAD_1.csv @@ -0,0 +1,21 @@ +dmsTimestamp,product_id,name,price +2022-06-24 18:53:24.619896,1,shorts,793.50 +2022-06-24 18:53:24.619917,2,hat,598.91 +2022-06-24 18:53:24.619920,3,hat,914.34 +2022-06-24 18:53:24.619923,4,accessories,717.76 +2022-06-24 18:53:24.619925,5,sneakers,975.06 +2022-06-24 18:53:24.619928,6,shorts,875.98 +2022-06-24 18:53:24.619931,7,coat,170.43 +2022-06-24 18:53:24.619933,8,hat,931.89 +2022-06-24 18:53:24.619936,9,shorts,627.72 +2022-06-24 18:53:24.619938,10,shirt,214.82 +2022-06-24 18:53:24.619941,11,sweater,534.26 +2022-06-24 18:53:24.619943,12,boots,933.89 +2022-06-24 18:53:24.619946,13,cap,600.41 +2022-06-24 18:53:24.619948,14,cap,608.73 +2022-06-24 18:53:24.619951,15,hat,747.17 +2022-06-24 18:53:24.619953,16,accessories,487.74 +2022-06-24 18:53:24.619956,17,coat,236.99 +2022-06-24 18:53:24.619958,18,accessories,960.39 +2022-06-24 18:53:24.619961,19,sweatshirt,600.43 +2022-06-24 18:53:24.619964,20,shirt,492.41 diff --git a/integration_tests/resources/data/snapshots/stores/LOAD_1.csv b/integration_tests/resources/data/snapshots/stores/LOAD_1.csv new file mode 100644 index 0000000..1ce6216 --- /dev/null +++ b/integration_tests/resources/data/snapshots/stores/LOAD_1.csv @@ -0,0 +1,5 @@ +dmsTimestamp,store_id,address +2022-06-24 18:53:25.141463,1,"6761 Brian Falls Navarrobury, VA 17977" +2022-06-24 18:53:25.141482,2,"4215 Bruce Shoals Apt. 920 Port Travis, SC 71335" +2022-06-24 18:53:25.141487,3,"96924 Gregory Mill Pricefurt, GA 68691" +2022-06-24 18:53:25.141491,4,"070 Cynthia Cliff Paulport, FL 21469" diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index 9ad6a87..2add1d2 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -117,6 +117,7 @@ class DLTMetaRunnerConf: eventhub_input_data: str = None eventhub_append_flow_input_data: str = None kafka_template: str = None + snapshot_template: str = None env: str = None whl_path: str = None volume_info: VolumeInfo = None @@ -169,12 +170,14 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: silver_schema=f"dlt_meta_silver_it_{run_id}", runners_nb_path=f"/Users/{self.wsi._my_username}/dlt_meta_int_tests/{run_id}", source=self.args.__dict__['source'], - node_type_id=cloud_node_type_id_dict[self.args.__dict__.get('cloud_provider_name', None)], + node_type_id=None if not self.args.__dict__.get('cloud_provider_name', None) else cloud_node_type_id_dict[ + self.args.__dict__.get('cloud_provider_name')], dbr_version=self.args.__dict__.get('dbr_version', None), cloudfiles_template="integration_tests/conf/cloudfiles-onboarding.template", cloudfiles_A2_template="integration_tests/conf/cloudfiles-onboarding_A2.template", eventhub_template="integration_tests/conf/eventhub-onboarding.template", kafka_template="integration_tests/conf/kafka-onboarding.template", + snapshot_template="integration_tests/conf/snapshot-onboarding.template", onboarding_file_path="integration_tests/conf/onboarding.json", onboarding_A2_file_path="integration_tests/conf/onboarding_A2.json", env="it", @@ -196,6 +199,8 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: runners_full_local_path = './integration_tests/dbc/eventhub_runners.dbc' elif runner_conf.source.lower() == "kafka": runners_full_local_path = './integration_tests/dbc/kafka_runners.dbc' + elif runner_conf.source.lower() == 'snapshot': + runners_full_local_path = './integration_tests/dbc/snapshot_runners.dbc' else: raise Exception("Supported source not found in argument") runner_conf.runners_full_local_path = runners_full_local_path @@ -517,6 +522,90 @@ def create_eventhub_workflow_spec(self, runner_conf: DLTMetaRunnerConf): ] ) + def create_snapshot_workflow_spec(self, runner_conf: DLTMetaRunnerConf): + database, dlt_lib = self.init_db_dltlib(runner_conf) + dltmeta_environments = [ + jobs.JobEnvironment( + environment_key="dl_meta_int_env", + spec=compute.Environment(client="2", + dependencies=[runner_conf.remote_whl_path] + ) + ) + ] + return self.ws.jobs.create( + name=f"dlt-meta-{runner_conf.run_id}", + environments=dltmeta_environments, + tasks=[ + jobs.Task( + task_key="setup_dlt_meta_pipeline_spec", + environment_key="dl_meta_int_env", + description="test", + timeout_seconds=0, + python_wheel_task=jobs.PythonWheelTask( + package_name="dlt_meta", + entry_point="run", + named_parameters={ + "onboard_layer": "bronze", + "database": database, + "onboarding_file_path": + f"{runner_conf.uc_volume_path}/{self.base_dir}/conf/onboarding.json", + "bronze_dataflowspec_table": "bronze_dataflowspec_cdc", + "import_author": "Ravi", + "version": "v1", + "overwrite": "True", + "env": runner_conf.env, + "uc_enabled": "True" + }, + ) + ), + jobs.Task( + task_key="bronze_snapshot_v1_dlt_pipeline", + depends_on=[jobs.TaskDependency(task_key="setup_dlt_meta_pipeline_spec")], + pipeline_task=jobs.PipelineTask( + pipeline_id=runner_conf.bronze_pipeline_id + ) + ), + jobs.Task( + task_key="upload_v2_snapshots", + description="upload_v2_snapshots", + depends_on=[jobs.TaskDependency(task_key="bronze_snapshot_v1_dlt_pipeline")], + notebook_task=jobs.NotebookTask( + notebook_path=f"{runner_conf.runners_nb_path}/runners/upload_snapshots", + base_parameters={ + "base_path": f"{runner_conf.uc_volume_path}/{self.base_dir}/resources/data/snapshots/", + "version": "2" + } + ) + ), + jobs.Task( + task_key="bronze_snapshot_v2_dlt_pipeline", + depends_on=[jobs.TaskDependency(task_key="upload_v2_snapshots")], + pipeline_task=jobs.PipelineTask( + pipeline_id=runner_conf.bronze_pipeline_id + ) + ), + jobs.Task( + task_key="upload_v3_snapshots", + description="upload_v3_snapshots", + depends_on=[jobs.TaskDependency(task_key="bronze_snapshot_v2_dlt_pipeline")], + notebook_task=jobs.NotebookTask( + notebook_path=f"{runner_conf.runners_nb_path}/runners/upload_snapshots", + base_parameters={ + "base_path": f"{runner_conf.uc_volume_path}/{self.base_dir}/resources/data/snapshots/", + "version": "3" + } + ) + ), + jobs.Task( + task_key="bronze_snapshot_v3_dlt_pipeline", + depends_on=[jobs.TaskDependency(task_key="upload_v3_snapshots")], + pipeline_task=jobs.PipelineTask( + pipeline_id=runner_conf.bronze_pipeline_id + ) + ), + ] + ) + def create_kafka_workflow_spec(self, runner_conf: DLTMetaRunnerConf): """Create Job specification.""" database, dlt_lib = self.init_db_dltlib(runner_conf) @@ -605,6 +694,27 @@ def generate_onboarding_file(self, runner_conf: DLTMetaRunnerConf): self.create_eventhub_onboarding(runner_conf) elif source == "kafka": self.create_kafka_onboarding(runner_conf) + elif source == "snapshot": + self.create_snapshot_onboarding(runner_conf) + + def create_snapshot_onboarding(self, runner_conf: DLTMetaRunnerConf): + with open(f"{runner_conf.snapshot_template}") as f: + onboard_obj = json.load(f) + + for data_flow in onboard_obj: + for key, value in data_flow.items(): + if key == "source_details": + for source_key, source_value in value.items(): + if 'uc_volume_path' in source_value: + data_flow[key][source_key] = source_value.format(uc_volume_path=runner_conf.uc_volume_path) + elif 'uc_catalog_name' in value and 'bronze_schema' in value: + if runner_conf.uc_catalog_name: + data_flow[key] = value.format( + uc_catalog_name=runner_conf.uc_catalog_name, + bronze_schema=runner_conf.bronze_schema + ) + with open(runner_conf.onboarding_file_path, "w") as onboarding_file: + json.dump(onboard_obj, onboarding_file) def create_kafka_onboarding(self, runner_conf: DLTMetaRunnerConf): """Create kafka onboarding file.""" @@ -1022,6 +1132,8 @@ def launch_workflow(self, runner_conf: DLTMetaRunnerConf): created_job = self.create_eventhub_workflow_spec(runner_conf) elif runner_conf.source.lower() == "kafka": created_job = self.create_kafka_workflow_spec(runner_conf) + elif runner_conf.source.lower() == "snapshot": + created_job = self.create_snapshot_workflow_spec(runner_conf) runner_conf.job_id = created_job.job_id print(f"Job created successfully. job_id={created_job.job_id}, started run...") webbrowser.open(f"{self.ws.config.host}/jobs/{created_job.job_id}?o={self.ws.get_workspace_id()}") @@ -1084,7 +1196,6 @@ def get_workspace_api_client(profile=None) -> WorkspaceClient: args_map = {"--profile": "provide databricks cli profile name, if not provide databricks_host and token", "--uc_catalog_name": "provide databricks uc_catalog name, this is required to create volume, schema, table", - "--cloud_provider_name": "provide cloud provider name. Supported values are aws , azure , gcp", "--source": "Provide source type e.g --source=cloudfiles", "--eventhub_name": "Provide eventhub_name e.g --eventhub_name=iot", "--eventhub_name_append_flow": "Provide eventhub_name_append_flow e.g --eventhub_name_append_flow=iot_af", @@ -1101,7 +1212,7 @@ def get_workspace_api_client(profile=None) -> WorkspaceClient: } mandatory_args = [ - "uc_catalog_name", "cloud_provider_name", "source" + "uc_catalog_name", "source" ] @@ -1125,7 +1236,11 @@ def process_arguments(args_map, mandatory_args): check_mandatory_arg(args, mandatory_args) supported_cloud_providers = ["aws", "azure", "gcp"] - cloud_provider_name = args.__getattribute__("cloud_provider_name") if args.__contains__("cloud_provider_name") else None + cloud_provider_name = ( + args.__getattribute__("cloud_provider_name") + if args.__contains__("cloud_provider_name") + else None + ) if cloud_provider_name and cloud_provider_name.lower() not in supported_cloud_providers: raise Exception("Invalid value for --cloud_provider_name! Supported values are aws, azure, gcp") return args @@ -1133,7 +1248,7 @@ def process_arguments(args_map, mandatory_args): def post_arg_processing(args): """Post processing of arguments.""" - supported_sources = ["cloudfiles", "eventhub", "kafka"] + supported_sources = ["cloudfiles", "eventhub", "kafka", "snapshot"] source = args.__getattribute__("source") if source.lower() not in supported_sources: raise Exception("Invalid value for --source! Supported values: --source=cloudfiles") diff --git a/src/dataflow_pipeline.py b/src/dataflow_pipeline.py index 11df12d..0933722 100644 --- a/src/dataflow_pipeline.py +++ b/src/dataflow_pipeline.py @@ -2,6 +2,7 @@ import json import logging import dlt +from typing import Callable from pyspark.sql import DataFrame from pyspark.sql.functions import expr from pyspark.sql.types import StructType, StructField @@ -64,7 +65,7 @@ class DataflowPipeline: """ def __init__(self, spark, dataflow_spec, view_name, view_name_quarantine=None, - custom_transform_func=None, snapshot_reader_func=None): + custom_transform_func=None, next_snapshot_and_version: Callable = None): """Initialize Constructor.""" logger.info( f"""dataflowSpec={dataflow_spec} , @@ -73,13 +74,14 @@ def __init__(self, spark, dataflow_spec, view_name, view_name_quarantine=None, ) if isinstance(dataflow_spec, BronzeDataflowSpec) or isinstance(dataflow_spec, SilverDataflowSpec): self.__initialize_dataflow_pipeline( - spark, dataflow_spec, view_name, view_name_quarantine, custom_transform_func, snapshot_reader_func + spark, dataflow_spec, view_name, view_name_quarantine, custom_transform_func, next_snapshot_and_version ) else: raise Exception("Dataflow not supported!") def __initialize_dataflow_pipeline( - self, spark, dataflow_spec, view_name, view_name_quarantine, custom_transform_func, snapshot_reader_func + self, spark, dataflow_spec, view_name, view_name_quarantine, custom_transform_func, + next_snapshot_and_version: Callable ): """Initialize dataflow pipeline state.""" self.spark = spark @@ -100,8 +102,8 @@ def __initialize_dataflow_pipeline( else: self.appendFlows = None if isinstance(dataflow_spec, BronzeDataflowSpec): - self.snapshot_reader_func = snapshot_reader_func - if self.snapshot_reader_func: + self.next_snapshot_and_version = next_snapshot_and_version + if self.next_snapshot_and_version: self.appy_changes_from_snapshot = DataflowSpecUtils.get_apply_changes_from_snapshot( self.dataflowSpec.applyChangesFromSnapshot ) @@ -114,7 +116,7 @@ def __initialize_dataflow_pipeline( self.schema_json = None else: self.schema_json = None - self.snapshot_reader_func = None + self.next_snapshot_and_version = None self.appy_changes_from_snapshot = None if isinstance(dataflow_spec, SilverDataflowSpec): self.silver_schema = self.get_silver_schema() @@ -128,20 +130,20 @@ def table_has_expectations(self): def read(self): """Read DLT.""" logger.info("In read function") - if isinstance(self.dataflowSpec, BronzeDataflowSpec) and not self.snapshot_reader_func: + if isinstance(self.dataflowSpec, BronzeDataflowSpec) and not self.next_snapshot_and_version: dlt.view( self.read_bronze, name=self.view_name, comment=f"input dataset view for {self.view_name}", ) - elif isinstance(self.dataflowSpec, SilverDataflowSpec) and not self.snapshot_reader_func: + elif isinstance(self.dataflowSpec, SilverDataflowSpec) and not self.next_snapshot_and_version: dlt.view( self.read_silver, name=self.view_name, comment=f"input dataset view for {self.view_name}", ) else: - if not self.snapshot_reader_func: + if not self.next_snapshot_and_version: raise Exception("Dataflow read not supported for {}".format(type(self.dataflowSpec))) if self.appendFlows: self.read_append_flows() @@ -191,11 +193,11 @@ def write_bronze(self): """Write Bronze tables.""" bronze_dataflow_spec: BronzeDataflowSpec = self.dataflowSpec if bronze_dataflow_spec.sourceFormat and bronze_dataflow_spec.sourceFormat.lower() == "snapshot": - if self.snapshot_reader_func: + if self.next_snapshot_and_version: self.apply_changes_from_snapshot() else: raise Exception("Snapshot reader function not provided!") - if bronze_dataflow_spec.dataQualityExpectations: + elif bronze_dataflow_spec.dataQualityExpectations: self.write_bronze_with_dqe() elif bronze_dataflow_spec.cdcApplyChanges: self.cdc_apply_changes() @@ -320,7 +322,10 @@ def apply_changes_from_snapshot(self): self.create_streaming_table(None, target_path) dlt.apply_changes_from_snapshot( target=f"{self.dataflowSpec.targetDetails['table']}", - snapshot_and_version=self.snapshot_reader_func, + source=lambda latest_snapshot_version: + self.next_snapshot_and_version(latest_snapshot_version, + self.dataflowSpec + ), keys=self.appy_changes_from_snapshot.keys, stored_as_scd_type=self.appy_changes_from_snapshot.scd_type, track_history_column_list=self.appy_changes_from_snapshot.track_history_column_list, @@ -541,7 +546,7 @@ def run_dlt(self): self.write() @staticmethod - def invoke_dlt_pipeline(spark, layer, custom_transform_func=None, snapshot_reader_func=None): + def invoke_dlt_pipeline(spark, layer, custom_transform_func=None, next_snapshot_and_version: Callable = None): """Invoke dlt pipeline will launch dlt with given dataflowspec. Args: @@ -573,7 +578,7 @@ def invoke_dlt_pipeline(spark, layer, custom_transform_func=None, snapshot_reade f"{dataflowSpec.targetDetails['table']}_{layer}_inputView", quarantine_input_view_name, custom_transform_func, - snapshot_reader_func + next_snapshot_and_version ) dlt_data_flow.run_dlt() diff --git a/src/onboard_dataflowspec.py b/src/onboard_dataflowspec.py index d9fbd95..a61283e 100644 --- a/src/onboard_dataflowspec.py +++ b/src/onboard_dataflowspec.py @@ -468,7 +468,10 @@ def __get_bronze_dataflow_spec_dataframe(self, onboarding_df, env): "table": onboarding_row["bronze_table"] } if not self.uc_enabled: - bronze_target_details["path"] = onboarding_row[f"bronze_table_path_{env}"] + if f"bronze_table_path_{env}" in onboarding_row: + bronze_target_details["path"] = onboarding_row[f"bronze_table_path_{env}"] + else: + raise Exception(f"bronze_table_path_{env} not provided in onboarding_row={onboarding_row}") bronze_table_properties = {} if "bronze_table_properties" in onboarding_row and onboarding_row["bronze_table_properties"]: bronze_table_properties = self.__delete_none(onboarding_row["bronze_table_properties"].asDict()) @@ -614,16 +617,19 @@ def get_bronze_source_details_reader_options_schema(self, onboarding_row, env): bronze_reader_config_options = {} schema = None source_format = onboarding_row["source_format"] - if source_format.lower() == "snapshot": - bronze_reader_config_options = {} - else: - bronze_reader_options_json = onboarding_row["bronze_reader_options"] - if bronze_reader_options_json: - bronze_reader_config_options = self.__delete_none(bronze_reader_options_json.asDict()) + bronze_reader_options_json = ( + onboarding_row["bronze_reader_options"] + if "bronze_reader_options" in onboarding_row + else {} + ) + if bronze_reader_options_json: + bronze_reader_config_options = self.__delete_none(bronze_reader_options_json.asDict()) source_details_json = onboarding_row["source_details"] if source_details_json: source_details_file = self.__delete_none(source_details_json.asDict()) - if source_format.lower() == "cloudfiles" or source_format.lower() == "delta": + if (source_format.lower() == "cloudfiles" + or source_format.lower() == "delta" + or source_format.lower() == "snapshot"): if f"source_path_{env}" in source_details_file: source_details["path"] = source_details_file[f"source_path_{env}"] if "source_database" in source_details_file: @@ -638,6 +644,13 @@ def get_bronze_source_details_reader_options_schema(self, onboarding_row, env): ) source_metadata_dict["select_metadata_cols"] = select_metadata_cols source_details["source_metadata"] = json.dumps(self.__delete_none(source_metadata_dict)) + if source_format.lower() == "snapshot": + snapshot_format = source_details_file.get("snapshot_format", None) + if snapshot_format is None: + raise Exception("snapshot_format is missing in the source_details") + source_details["snapshot_format"] = snapshot_format + if f"source_path_{env}" in source_details_file: + source_details["path"] = source_details_file[f"source_path_{env}"] elif source_format.lower() == "eventhub" or source_format.lower() == "kafka": source_details = source_details_file if "source_schema_path" in source_details_file: diff --git a/tests/resources/onboarding_applychanges_from_snapshot.json b/tests/resources/onboarding_applychanges_from_snapshot.json index 53cef5c..4f5c36e 100644 --- a/tests/resources/onboarding_applychanges_from_snapshot.json +++ b/tests/resources/onboarding_applychanges_from_snapshot.json @@ -1,42 +1,44 @@ [ - { - "data_flow_id": "201", - "data_flow_group": "A1", - "source_system": "delta", - "source_format": "snapshot", - "source_details": { - "source_database": "products", - "source_table": "products", - "source_path_dev": "tests/resources/data/products" - }, - "bronze_database_dev": "uc_catalog_name.bronze", - "bronze_table": "products", - "bronze_table_path_dev": "tests/resources/delta/bronze/products", - "bronze_apply_changes_from_snapshot": { - "keys": [ - "product_id" - ], - "scd_type": "2" - } - }, - { - "data_flow_id": "202", - "data_flow_group": "A1", - "source_system": "delta", - "source_format": "snapshot", - "source_details": { - "source_database": "stores", - "source_table": "stores", - "source_path_dev": "tests/resources/data/stores" - }, - "bronze_database_dev": "uc_catalog_name.bronze", - "bronze_table": "stores", - "bronze_table_path_dev": "tests/resources/delta/bronze/stores", - "bronze_apply_changes_from_snapshot": { - "keys": [ - "store_id" - ], - "scd_type": "2" - } - } - ] \ No newline at end of file + { + "data_flow_id": "201", + "data_flow_group": "A1", + "source_system": "delta", + "source_format": "snapshot", + "source_details": { + "source_path_it": "/Volumes/ravi_dlt_meta_uc/dlt_meta_dataflowspecs_it_23de6188b0b0442a9f0bdbaed368b1f7/dlt_meta_dataflowspecs_it_23de6188b0b0442a9f0bdbaed368b1f7//integration_tests/resources/data/snapshots/products/LOAD_", + "snapshot_format": "csv" + }, + "bronze_reader_options": { + "header": "true" + }, + "bronze_database_it": "ravi_dlt_meta_uc.dlt_meta_bronze_it_23de6188b0b0442a9f0bdbaed368b1f7", + "bronze_table": "products", + "bronze_apply_changes_from_snapshot": { + "keys": [ + "product_id" + ], + "scd_type": "2" + } + }, + { + "data_flow_id": "202", + "data_flow_group": "A1", + "source_system": "delta", + "source_format": "snapshot", + "source_details": { + "source_path_it": "/Volumes/ravi_dlt_meta_uc/dlt_meta_dataflowspecs_it_23de6188b0b0442a9f0bdbaed368b1f7/dlt_meta_dataflowspecs_it_23de6188b0b0442a9f0bdbaed368b1f7//integration_tests/resources/data/snapshots/stores/LOAD_", + "snapshot_format": "csv" + }, + "bronze_reader_options": { + "header": "true" + }, + "bronze_database_it": "ravi_dlt_meta_uc.dlt_meta_bronze_it_23de6188b0b0442a9f0bdbaed368b1f7", + "bronze_table": "stores", + "bronze_apply_changes_from_snapshot": { + "keys": [ + "store_id" + ], + "scd_type": "2" + } + } +] \ No newline at end of file diff --git a/tests/test_cli.py b/tests/test_cli.py index 6d756ae..deffb21 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -41,7 +41,7 @@ class CliTests(unittest.TestCase): dbfs_path="/dbfs", ) - def test_copy(self): + def test_copy_to_uc_volume(self): mock_ws = MagicMock() dltmeta = DLTMeta(mock_ws) with patch('os.walk') as mock_walk: @@ -53,7 +53,7 @@ def test_copy(self): mock_open.return_value = MagicMock() mock_dbfs_upload = MagicMock() mock_ws.dbfs.upload = mock_dbfs_upload - dltmeta.copy("file:/path/to/src", "/dbfs/path/to/dst") + dltmeta.test_copy_to_uc_volume("file:/path/to/src", "/dbfs/path/to/dst") self.assertEqual(mock_dbfs_upload.call_count, 3) @patch('src.cli.WorkspaceClient') diff --git a/tests/test_onboard_dataflowspec.py b/tests/test_onboard_dataflowspec.py index bc0b605..27b6e49 100644 --- a/tests/test_onboard_dataflowspec.py +++ b/tests/test_onboard_dataflowspec.py @@ -347,10 +347,11 @@ def test_onboard_bronze_silver_with_v7(self): def test_onboard_apply_changes_from_snapshot_positive(self): """Test for onboardDataflowspec.""" onboarding_params_map = copy.deepcopy(self.onboarding_bronze_silver_params_map) + onboarding_params_map['env'] = 'it' del onboarding_params_map["silver_dataflowspec_table"] del onboarding_params_map["silver_dataflowspec_path"] onboarding_params_map["onboarding_file_path"] = self.onboarding_apply_changes_from_snapshot_json_file - onboardDataFlowSpecs = OnboardDataflowspec(self.spark, onboarding_params_map) + onboardDataFlowSpecs = OnboardDataflowspec(self.spark, onboarding_params_map, uc_enabled=True) onboardDataFlowSpecs.onboard_bronze_dataflow_spec() bronze_dataflowSpec_df = self.read_dataflowspec( self.onboarding_bronze_silver_params_map['database'], From f360160c8fe277e287e01ff028df9a9004954372 Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Fri, 4 Oct 2024 12:06:39 -0400 Subject: [PATCH 25/59] update upload to databricks --- integration_tests/run_integration_tests.py | 45 ++++++++++++++++------ 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index f94f7f3..8c81152 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -100,7 +100,7 @@ class DLTMetaRunnerConf: onboarding_file_path: str = "integration_tests/conf/onboarding.json" onboarding_A2_file_path: str = "integration_tests/conf/onboarding_A2.json" onboarding_fanout_file_path: str = "integration_tests/conf/onboarding.json" - int_tests_dir: str = "./integration_tests" + int_tests_dir: str = "integration_tests" dlt_meta_schema: str = None bronze_schema: str = None silver_schema: str = None @@ -802,24 +802,35 @@ def copy(self, runner_conf: DLTMetaRunnerConf): self.ws.files.upload(file_path=uc_volume_path, contents=contents, overwrite=True) def upload_files_to_databricks(self, runner_conf: DLTMetaRunnerConf): + """ + Upload all necessary data, configuration files, wheels, and notebooks to run the + integration tests + """ uc_vol_full_path = f"{runner_conf.uc_volume_path}/{runner_conf.int_tests_dir}" print(f"Integration test file upload to {uc_vol_full_path} starting...") # Upload the entire resources directory containing ddl and test data for root, dirs, files in os.walk(f"{runner_conf.int_tests_dir}/resources"): - print(root, '|', dirs, '|', files) + for file in files: + with open(os.path.join(root, file), "rb") as content: + self.ws.files.upload( + file_path=f"{runner_conf.uc_volume_path}/{root}/{file}", + contents=content, + overwrite=True, + ) + # Upload all the JSONs in the conf directory, that is the generated onboarding JSONs and + # the DQE JSONS + for root, dirs, files in os.walk(f"{runner_conf.int_tests_dir}/conf"): + if file.endswith("json"): + for file in files: + with open(os.path.join(root, file), "rb") as content: + self.ws.files.upload( + file_path=f"{runner_conf.uc_volume_path}/{root}/{file}", + contents=content, + overwrite=True, + ) print(f"Integration test file upload to {uc_vol_full_path} complete!!!") - def init_dltmeta_runner_conf(self, runner_conf: DLTMetaRunnerConf): - """Create testing metadata including schemas, volumes, and uploading necessary notebooks""" - - # Generate uc schemas, volumes and upload onboarding files - # self.initialize_uc_resources(runner_conf) - # self.generate_onboarding_file(runner_conf) - self.upload_files_to_databricks(runner_conf) - - exit() - # Upload required notebooks for the given source print(f"Notebooks upload to {runner_conf.runners_nb_path} started...") self.ws.workspace.mkdirs(f"{runner_conf.runners_nb_path}/runners") @@ -835,6 +846,16 @@ def init_dltmeta_runner_conf(self, runner_conf: DLTMetaRunnerConf): ) print(f"Notebooks upload to {runner_conf.runners_nb_path} complete!!!") + def init_dltmeta_runner_conf(self, runner_conf: DLTMetaRunnerConf): + """Create testing metadata including schemas, volumes, and uploading necessary notebooks""" + + # Generate uc schemas, volumes and upload onboarding files + self.initialize_uc_resources(runner_conf) + self.generate_onboarding_file(runner_conf) + self.upload_files_to_databricks(runner_conf) + + exit() + if runner_conf.uc_catalog_name: self.build_and_upload_package(runner_conf) From c678ac8dca4cb34a7ce5f2876279cfcde6f8561e Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Fri, 4 Oct 2024 13:30:58 -0400 Subject: [PATCH 26/59] only upload wheel to workspace location --- src/install.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/install.py b/src/install.py index 4bdf703..e4fbb16 100644 --- a/src/install.py +++ b/src/install.py @@ -104,10 +104,6 @@ def _upload_wheel(self) -> str: local_wheel = self._build_wheel(tmp_dir) remote_wheel = f"{self._install_folder}/wheels/{local_wheel.name}" remote_dirname = os.path.dirname(remote_wheel) - with local_wheel.open("rb") as f: - self._ws.dbfs.mkdirs(remote_dirname) - logger.info(f"Uploading wheel to dbfs:{remote_wheel}") - self._ws.dbfs.upload(remote_wheel, f, overwrite=True) with local_wheel.open("rb") as f: self._ws.workspace.mkdirs(remote_dirname) logger.info(f"Uploading wheel to /Workspace{remote_wheel}") From d6020b55c8aa5db193f95bcd6e09cc911b043cdd Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Fri, 4 Oct 2024 13:44:07 -0400 Subject: [PATCH 27/59] added uc volume upload of the wheel if provided --- src/install.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/install.py b/src/install.py index e4fbb16..81c7afe 100644 --- a/src/install.py +++ b/src/install.py @@ -99,7 +99,11 @@ def _app(self): def _version(self): return __version__ - def _upload_wheel(self) -> str: + def _upload_wheel(self, uc_volume_path: str = None) -> str: + """ + Upload the wheel to user's workspace folder and to the uc_volume for the run if provided. + The path to the UC volume wheel will be provided if possible, else the workspace location. + """ with tempfile.TemporaryDirectory() as tmp_dir: local_wheel = self._build_wheel(tmp_dir) remote_wheel = f"{self._install_folder}/wheels/{local_wheel.name}" @@ -108,7 +112,12 @@ def _upload_wheel(self) -> str: self._ws.workspace.mkdirs(remote_dirname) logger.info(f"Uploading wheel to /Workspace{remote_wheel}") self._ws.workspace.upload(remote_wheel, f, overwrite=True, format=ImportFormat.AUTO) - return remote_wheel + if uc_volume_path: + uc_wheel_path = f"{uc_volume_path}/wheels/{local_wheel.name}" + logger.info(f"Uploading wheel to {uc_wheel_path}") + self._ws.workspace.upload(uc_wheel_path, f, overwrite=True, format=ImportFormat.AUTO) + return uc_wheel_path + return f"/Workspace{remote_wheel}" def _build_wheel(self, tmp_dir: str, *, verbose: bool = False): """Helper to build the wheel package""" From acee79b3a6d48227763e32a7519614c7a4e8a2a5 Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Fri, 4 Oct 2024 13:48:42 -0400 Subject: [PATCH 28/59] wheel upload added, all data uploads reworked --- integration_tests/run_integration_tests.py | 46 +++------------------- 1 file changed, 5 insertions(+), 41 deletions(-) diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index 8c81152..f06ec4f 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -194,22 +194,6 @@ def _my_username(self, ws): _me = ws.current_user.me() return _me.user_name - def build_and_upload_package(self, runner_conf: DLTMetaRunnerConf): - """ - Build and upload the Python package. - - Parameters: - ---------- - runner_conf : DLTMetaRunnerConf - The runner configuration. - - Raises: - ------ - Exception - If the build process fails. - """ - runner_conf.remote_whl_path = f"/Workspace{self.wsi._upload_wheel()}" - def create_dlt_meta_pipeline(self, pipeline_name: str, layer: str, @@ -780,27 +764,6 @@ def create_cloudfiles_onboarding(self, runner_conf: DLTMetaRunnerConf): with open(runner_conf.onboarding_A2_file_path, "w") as onboarding_file_a2: json.dump(json.loads(onboard_json_a2), onboarding_file_a2, indent=4) - def copy(self, runner_conf: DLTMetaRunnerConf): - if runner_conf.uc_catalog_name: - print(f"uploading to {runner_conf.uc_volume_path}/{self.base_dir}/ started") - src = runner_conf.int_tests_dir - dst = runner_conf.uc_volume_path - main_dir = src.replace('file:', '') - base_dir_name = None - if main_dir.endswith('/'): - base_dir_name = main_dir[:-1] - if base_dir_name is None: - base_dir_name = main_dir[main_dir.rfind('/') + 1:] - else: - base_dir_name = base_dir_name[base_dir_name.rfind('/') + 1:-1] - for root, dirs, files in os.walk(main_dir): - for filename in files: - if not filename.endswith(".py") and not filename.endswith(".md"): - target_dir = root[root.index(main_dir) + len(main_dir):len(root)] - uc_volume_path = f"{dst}/{base_dir_name}/{target_dir}/{filename}".replace("//", "/") - contents = open(os.path.join(root, filename), "rb") - self.ws.files.upload(file_path=uc_volume_path, contents=contents, overwrite=True) - def upload_files_to_databricks(self, runner_conf: DLTMetaRunnerConf): """ Upload all necessary data, configuration files, wheels, and notebooks to run the @@ -846,6 +809,11 @@ def upload_files_to_databricks(self, runner_conf: DLTMetaRunnerConf): ) print(f"Notebooks upload to {runner_conf.runners_nb_path} complete!!!") + print("Python wheel upload starting...") + # Upload the wheel to both the workspace and the uc volume + runner_conf.remote_whl_path = f"{self.wsi._upload_wheel(uc_volume_path=runner_conf.uc_volume_path)}" + print(f"Python wheel upload to {runner_conf.remote_whl_path} completed!!!") + def init_dltmeta_runner_conf(self, runner_conf: DLTMetaRunnerConf): """Create testing metadata including schemas, volumes, and uploading necessary notebooks""" @@ -854,10 +822,6 @@ def init_dltmeta_runner_conf(self, runner_conf: DLTMetaRunnerConf): self.generate_onboarding_file(runner_conf) self.upload_files_to_databricks(runner_conf) - exit() - - if runner_conf.uc_catalog_name: - self.build_and_upload_package(runner_conf) def create_cluster(self, runner_conf: DLTMetaRunnerConf): print("Cluster creation started...") From d316554018e54896087e55471d1671cab4d5ccf8 Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Fri, 4 Oct 2024 14:08:15 -0400 Subject: [PATCH 29/59] initial upload and setup for integration testing for cloud files is done --- integration_tests/run_integration_tests.py | 48 ++++++++++++---------- src/install.py | 14 +++++-- 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index f06ec4f..a69870a 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -769,14 +769,14 @@ def upload_files_to_databricks(self, runner_conf: DLTMetaRunnerConf): Upload all necessary data, configuration files, wheels, and notebooks to run the integration tests """ - uc_vol_full_path = f"{runner_conf.uc_volume_path}/{runner_conf.int_tests_dir}" + uc_vol_full_path = f"{runner_conf.uc_volume_path}{runner_conf.int_tests_dir}" print(f"Integration test file upload to {uc_vol_full_path} starting...") # Upload the entire resources directory containing ddl and test data for root, dirs, files in os.walk(f"{runner_conf.int_tests_dir}/resources"): for file in files: with open(os.path.join(root, file), "rb") as content: self.ws.files.upload( - file_path=f"{runner_conf.uc_volume_path}/{root}/{file}", + file_path=f"{runner_conf.uc_volume_path}{root}/{file}", contents=content, overwrite=True, ) @@ -784,14 +784,14 @@ def upload_files_to_databricks(self, runner_conf: DLTMetaRunnerConf): # Upload all the JSONs in the conf directory, that is the generated onboarding JSONs and # the DQE JSONS for root, dirs, files in os.walk(f"{runner_conf.int_tests_dir}/conf"): - if file.endswith("json"): for file in files: - with open(os.path.join(root, file), "rb") as content: - self.ws.files.upload( - file_path=f"{runner_conf.uc_volume_path}/{root}/{file}", - contents=content, - overwrite=True, - ) + if file.endswith(".json"): + with open(os.path.join(root, file), "rb") as content: + self.ws.files.upload( + file_path=f"{runner_conf.uc_volume_path}{root}/{file}", + contents=content, + overwrite=True, + ) print(f"Integration test file upload to {uc_vol_full_path} complete!!!") # Upload required notebooks for the given source @@ -822,7 +822,6 @@ def init_dltmeta_runner_conf(self, runner_conf: DLTMetaRunnerConf): self.generate_onboarding_file(runner_conf) self.upload_files_to_databricks(runner_conf) - def create_cluster(self, runner_conf: DLTMetaRunnerConf): print("Cluster creation started...") if runner_conf.uc_catalog_name: @@ -957,15 +956,7 @@ def run(self, runner_conf: DLTMetaRunnerConf): # self.clean_up(runner_conf) -def get_workspace_api_client(profile=None) -> WorkspaceClient: - """Get api client with config.""" - if profile: - workspace_client = WorkspaceClient(profile=profile) - else: - workspace_client = WorkspaceClient(host=input('Databricks Workspace URL: '), token=input('Token: ')) - return workspace_client - -def process_arguments() -> dict[str: str]: +def process_arguments() -> dict[str:str]: """ Get, process, and validate the command line arguements @@ -1073,9 +1064,13 @@ def process_arguments() -> dict[str: str]: parser = argparse.ArgumentParser() for arg in input_args: if arg[4]: - parser.add_argument(f"--{arg[0]}", help=arg[1], type=arg[2], required=arg[3], choices=arg[4]) + parser.add_argument( + f"--{arg[0]}", help=arg[1], type=arg[2], required=arg[3], choices=arg[4] + ) else: - parser.add_argument(f"--{arg[0]}", help=arg[1], type=arg[2], required=arg[3]) + parser.add_argument( + f"--{arg[0]}", help=arg[1], type=arg[2], required=arg[3] + ) args = vars(parser.parse_args()) def check_cond_mandatory_arg(args, mandatory_args): @@ -1108,6 +1103,17 @@ def check_cond_mandatory_arg(args, mandatory_args): return args +def get_workspace_api_client(profile=None) -> WorkspaceClient: + """Get api client with config.""" + if profile: + workspace_client = WorkspaceClient(profile=profile) + else: + workspace_client = WorkspaceClient( + host=input("Databricks Workspace URL: "), token=input("Token: ") + ) + return workspace_client + + def main(): """Entry method to run integration tests.""" args = process_arguments() diff --git a/src/install.py b/src/install.py index 81c7afe..a1c698e 100644 --- a/src/install.py +++ b/src/install.py @@ -111,11 +111,17 @@ def _upload_wheel(self, uc_volume_path: str = None) -> str: with local_wheel.open("rb") as f: self._ws.workspace.mkdirs(remote_dirname) logger.info(f"Uploading wheel to /Workspace{remote_wheel}") - self._ws.workspace.upload(remote_wheel, f, overwrite=True, format=ImportFormat.AUTO) - if uc_volume_path: - uc_wheel_path = f"{uc_volume_path}/wheels/{local_wheel.name}" + self._ws.workspace.upload( + remote_wheel, f, overwrite=True, format=ImportFormat.AUTO + ) + if uc_volume_path: + # Reopen to the wheel file since how it uploads it, if you try to upload twice + # under the same open statement, the second upload the file is empty, it probably + # treats the open output as some sort of iterator + with local_wheel.open("rb") as f: + uc_wheel_path = f"{uc_volume_path}wheels/{local_wheel.name}" logger.info(f"Uploading wheel to {uc_wheel_path}") - self._ws.workspace.upload(uc_wheel_path, f, overwrite=True, format=ImportFormat.AUTO) + self._ws.files.upload(uc_wheel_path, f, overwrite=True) return uc_wheel_path return f"/Workspace{remote_wheel}" From 5ed96323959072a4e2136381640c9b18c31f4ee7 Mon Sep 17 00:00:00 2001 From: ravi-databricks Date: Fri, 4 Oct 2024 12:16:23 -0700 Subject: [PATCH 30/59] Added: 1.Demo for apply_changes_from_snapshot 2.integration tests with scd_typ1 3.Documentations --- demo/README.md | 34 +++++++- demo/conf/snapshot-onboarding.template | 44 ++++++++++ demo/dbc/snapshot_runners.dbc | Bin 0 -> 3532 bytes demo/launch_acfs_demo.py | 75 ++++++++++++++++++ demo/launch_af_cloudfiles_demo.py | 2 +- .../incremental_snapshots/products/LOAD_2.csv | 21 +++++ .../incremental_snapshots/products/LOAD_3.csv | 21 +++++ .../incremental_snapshots/stores/LOAD_2.csv | 4 + .../incremental_snapshots/stores/LOAD_3.csv | 3 + .../data/snapshots/products/LOAD_1.csv | 21 +++++ .../data/snapshots/stores/LOAD_1.csv | 5 ++ .../demo/Apply_Changes_From_Snapshot.md | 50 ++++++++++++ docs/content/demo/_index.md | 3 +- docs/static/images/acfs.png | Bin 0 -> 238988 bytes examples/dlt_meta_pipeline_snapshot.ipynb | 2 +- .../conf/snapshot-onboarding.template | 2 +- .../incremental_snapshots/stores/LOAD_2.csv | 2 - .../incremental_snapshots/stores/LOAD_3.csv | 4 +- 18 files changed, 283 insertions(+), 10 deletions(-) create mode 100644 demo/conf/snapshot-onboarding.template create mode 100644 demo/dbc/snapshot_runners.dbc create mode 100644 demo/launch_acfs_demo.py create mode 100644 demo/resources/data/snapshots/incremental_snapshots/products/LOAD_2.csv create mode 100644 demo/resources/data/snapshots/incremental_snapshots/products/LOAD_3.csv create mode 100644 demo/resources/data/snapshots/incremental_snapshots/stores/LOAD_2.csv create mode 100644 demo/resources/data/snapshots/incremental_snapshots/stores/LOAD_3.csv create mode 100644 demo/resources/data/snapshots/products/LOAD_1.csv create mode 100644 demo/resources/data/snapshots/stores/LOAD_1.csv create mode 100644 docs/content/demo/Apply_Changes_From_Snapshot.md create mode 100644 docs/static/images/acfs.png diff --git a/demo/README.md b/demo/README.md index 6e5b9e9..4755f23 100644 --- a/demo/README.md +++ b/demo/README.md @@ -4,6 +4,7 @@ 3. [Append FLOW Autoloader Demo](#append-flow-autoloader-file-metadata-demo): Write to same target from multiple sources using [dlt.append_flow](https://docs.databricks.com/en/delta-live-tables/flows.html#append-flows) and adding [File metadata column](https://docs.databricks.com/en/ingestion/file-metadata-column.html) 4. [Append FLOW Eventhub Demo](#append-flow-eventhub-demo): Write to same target from multiple sources using [dlt.append_flow](https://docs.databricks.com/en/delta-live-tables/flows.html#append-flows) and adding [File metadata column](https://docs.databricks.com/en/ingestion/file-metadata-column.html) 5. [Silver Fanout Demo](#silver-fanout-demo): This demo showcases the implementation of fanout architecture in the silver layer. + 6. [Apply Changes From Snapshot Demo](#Apply-changes-from-snapshot-demo): This demo showcases the implementation of ingesting from snapshots in bronze layer @@ -216,4 +217,35 @@ This demo will perform following tasks: ![silver_fanout_workflow.png](../docs/static/images/silver_fanout_workflow.png) - ![silver_fanout_dlt.png](../docs/static/images/silver_fanout_dlt.png) \ No newline at end of file + ![silver_fanout_dlt.png](../docs/static/images/silver_fanout_dlt.png) + + +# Apply Changes From Snapshot Demo +This demo will showcase how to load bronze tables from snapshot files. There are two sources product and stores in which product is **scd_type=2** and stores is **scd_type=1**. +Day1 there is LOAD_1.csv file and for which next_snapshot_and_version function will be provided inside[dlt_meta_pipeline_snapshot.ipynb](https://github.com/databrickslabs/dlt-meta/blob/main/examples/dlt_meta_pipeline_snapshot.ipynb) +Day2 there will be LOAD_2.csv file loaded which will have updated values for products and stores with v2_ +Day3 there will be LOAD_3.csv file loaded which will have updated values for products and stores with v3_ +As part of above scenarios for scd_type1 stores if records are missing in snapshot those records will be deleted. for scd_typ2 matching keys will expire old records and insert new record with version_number +### Steps: +1. Launch Command Prompt + +2. Install [Databricks CLI](https://docs.databricks.com/dev-tools/cli/index.html) + +3. ```commandline + git clone https://github.com/databrickslabs/dlt-meta.git + ``` + +4. ```commandline + cd dlt-meta + ``` +5. Set python environment variable into terminal + ```commandline + dlt_meta_home=$(pwd) + ``` + ```commandline + export PYTHONPATH=$dlt_meta_home + +6. Run the command + ```commandline + python demo/launch_acfs_demo.py --uc_catalog_name=<<>> + ``` \ No newline at end of file diff --git a/demo/conf/snapshot-onboarding.template b/demo/conf/snapshot-onboarding.template new file mode 100644 index 0000000..ceb6734 --- /dev/null +++ b/demo/conf/snapshot-onboarding.template @@ -0,0 +1,44 @@ +[ + { + "data_flow_id": "201", + "data_flow_group": "A1", + "source_system": "delta", + "source_format": "snapshot", + "source_details": { + "source_path_demo": "{uc_volume_path}/demo/resources/data/snapshots/products/LOAD_", + "snapshot_format": "csv" + }, + "bronze_reader_options": { + "header": "true" + }, + "bronze_database_demo": "{uc_catalog_name}.{bronze_schema}", + "bronze_table": "products", + "bronze_apply_changes_from_snapshot": { + "keys": [ + "product_id" + ], + "scd_type": "2" + } + }, + { + "data_flow_id": "202", + "data_flow_group": "A1", + "source_system": "delta", + "source_format": "snapshot", + "source_details": { + "source_path_demo": "{uc_volume_path}/demo/resources/data/snapshots/stores/LOAD_", + "snapshot_format": "csv" + }, + "bronze_reader_options": { + "header": "true" + }, + "bronze_database_demo": "{uc_catalog_name}.{bronze_schema}", + "bronze_table": "stores", + "bronze_apply_changes_from_snapshot": { + "keys": [ + "store_id" + ], + "scd_type": "1" + } + } + ] \ No newline at end of file diff --git a/demo/dbc/snapshot_runners.dbc b/demo/dbc/snapshot_runners.dbc new file mode 100644 index 0000000000000000000000000000000000000000..3a4e2d11b4a40f7de781e1b560b70a600cc9ee9c GIT binary patch literal 3532 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 literal 0 HcmV?d00001 diff --git a/demo/launch_acfs_demo.py b/demo/launch_acfs_demo.py new file mode 100644 index 0000000..5a1fedb --- /dev/null +++ b/demo/launch_acfs_demo.py @@ -0,0 +1,75 @@ + +import uuid +from src.install import WorkspaceInstaller +from integration_tests.run_integration_tests import ( + DLTMETARunner, + DLTMetaRunnerConf, + get_workspace_api_client, + process_arguments +) + + +class ApplyChangesFromSnapshotDemo(DLTMETARunner): + def __init__(self, args, ws, base_dir): + self.args = args + self.ws = ws + self.wsi = WorkspaceInstaller(ws) + self.base_dir = base_dir + + def run(self, runner_conf: DLTMetaRunnerConf): + """ + Runs the DLT-META Apply Changes from Snapshot Demo by calling the necessary methods in the correct order. + + Parameters: + - runner_conf: The DLTMetaRunnerConf object containing the runner configuration parameters. + """ + try: + self.init_dltmeta_runner_conf(runner_conf) + self.create_bronze_silver_dlt(runner_conf) + self.launch_workflow(runner_conf) + except Exception as e: + print(e) + + def init_runner_conf(self) -> DLTMetaRunnerConf: + run_id = uuid.uuid4().hex + runner_conf = DLTMetaRunnerConf( + run_id=run_id, + username=self.wsi._my_username, + int_tests_dir="file:./demo", + dlt_meta_schema=f"dlt_meta_dataflowspecs_demo_{run_id}", + bronze_schema=f"dlt_meta_bronze_demo_{run_id}", + runners_nb_path=f"/Users/{self.wsi._my_username}/dlt_meta_demo/{run_id}", + source="snapshot", + snapshot_template="demo/conf/snapshot-onboarding.template", + onboarding_file_path="demo/conf/onboarding.json", + env="demo" + ) + runner_conf.uc_catalog_name = self.args.__dict__['uc_catalog_name'] + runner_conf.runners_full_local_path = './demo/dbc/snapshot_runners.dbc' + return runner_conf + + def launch_workflow(self, runner_conf: DLTMetaRunnerConf): + created_job = self.create_snapshot_workflow_spec(runner_conf) + self.open_job_url(runner_conf, created_job) + + +acfs_args_map = { + "--profile": "provide databricks cli profile name, if not provide databricks_host and token", + "--uc_catalog_name": "provide databricks uc_catalog name, this is required to create volume, schema, table" +} + +afam_mandatory_args = [ + "uc_catalog_name"] + + +def main(): + args = process_arguments(acfs_args_map, afam_mandatory_args) + workspace_client = get_workspace_api_client(args.profile) + dltmeta_afam_demo_runner = ApplyChangesFromSnapshotDemo(args, workspace_client, "demo") + print("initializing complete") + runner_conf = dltmeta_afam_demo_runner.init_runner_conf() + dltmeta_afam_demo_runner.run(runner_conf) + + +if __name__ == "__main__": + main() diff --git a/demo/launch_af_cloudfiles_demo.py b/demo/launch_af_cloudfiles_demo.py index db01b60..11db0bc 100644 --- a/demo/launch_af_cloudfiles_demo.py +++ b/demo/launch_af_cloudfiles_demo.py @@ -61,7 +61,7 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: return runner_conf def launch_workflow(self, runner_conf: DLTMetaRunnerConf): - created_job = self.create_cloudfiles_workflow_spec(runner_conf) + created_job = self.create_snapshot_workflow_spec(runner_conf) self.open_job_url(runner_conf, created_job) diff --git a/demo/resources/data/snapshots/incremental_snapshots/products/LOAD_2.csv b/demo/resources/data/snapshots/incremental_snapshots/products/LOAD_2.csv new file mode 100644 index 0000000..64568e9 --- /dev/null +++ b/demo/resources/data/snapshots/incremental_snapshots/products/LOAD_2.csv @@ -0,0 +1,21 @@ +dmsTimestamp,product_id,name,price +2022-06-24 18:53:24.619896,1,shorts_v2,793.50 +2022-06-24 18:53:24.619917,2,hat_v2,598.91 +2022-06-24 18:53:24.619920,3,hat_v2,914.34 +2022-06-24 18:53:24.619923,4,accessories,717.76 +2022-06-24 18:53:24.619925,5,sneakers,975.06 +2022-06-24 18:53:24.619928,6,shorts_v2,875.98 +2022-06-24 18:53:24.619931,7,coat,170.43 +2022-06-24 18:53:24.619933,8,hat_v2,931.89 +2022-06-24 18:53:24.619936,9,shorts_v2,627.72 +2022-06-24 18:53:24.619938,10,shirt,214.82 +2022-06-24 18:53:24.619941,11,sweater,534.26 +2022-06-24 18:53:24.619943,12,boots,933.89 +2022-06-24 18:53:24.619946,13,cap,600.41 +2022-06-24 18:53:24.619948,14,cap,608.73 +2022-06-24 18:53:24.619951,15,hat_v2,747.17 +2022-06-24 18:53:24.619953,16,accessories,487.74 +2022-06-24 18:53:24.619956,17,coat,236.99 +2022-06-24 18:53:24.619958,18,accessories,960.39 +2022-06-24 18:53:24.619961,19,sweatshirt,600.43 +2022-06-24 18:53:24.619964,20,shirt,492.41 diff --git a/demo/resources/data/snapshots/incremental_snapshots/products/LOAD_3.csv b/demo/resources/data/snapshots/incremental_snapshots/products/LOAD_3.csv new file mode 100644 index 0000000..fe24dce --- /dev/null +++ b/demo/resources/data/snapshots/incremental_snapshots/products/LOAD_3.csv @@ -0,0 +1,21 @@ +dmsTimestamp,product_id,name,price +2022-06-24 18:53:24.619896,1,shorts_v3,793.50 +2022-06-24 18:53:24.619917,2,hat_v3,598.91 +2022-06-24 18:53:24.619920,3,hat_v3,914.34 +2022-06-24 18:53:24.619923,4,accessories,717.76 +2022-06-24 18:53:24.619925,5,sneakers,975.06 +2022-06-24 18:53:24.619928,6,shorts_v3,875.98 +2022-06-24 18:53:24.619931,7,coat,170.43 +2022-06-24 18:53:24.619933,8,hat_v3,931.89 +2022-06-24 18:53:24.619936,9,shorts_v3,627.72 +2022-06-24 18:53:24.619938,10,shirt,214.82 +2022-06-24 18:53:24.619941,11,sweater,534.26 +2022-06-24 18:53:24.619943,12,boots,933.89 +2022-06-24 18:53:24.619946,13,cap,600.41 +2022-06-24 18:53:24.619948,14,cap,608.73 +2022-06-24 18:53:24.619951,15,hat_v3,747.17 +2022-06-24 18:53:24.619953,16,accessories,487.74 +2022-06-24 18:53:24.619956,17,coat,236.99 +2022-06-24 18:53:24.619958,18,accessories,960.39 +2022-06-24 18:53:24.619961,19,sweatshirt,600.43 +2022-06-24 18:53:24.619964,20,shirt,492.41 diff --git a/demo/resources/data/snapshots/incremental_snapshots/stores/LOAD_2.csv b/demo/resources/data/snapshots/incremental_snapshots/stores/LOAD_2.csv new file mode 100644 index 0000000..181f39c --- /dev/null +++ b/demo/resources/data/snapshots/incremental_snapshots/stores/LOAD_2.csv @@ -0,0 +1,4 @@ +dmsTimestamp,store_id,address +2022-06-24 18:53:25.141463,1,"V2 6761 Brian Falls Navarrobury, VA 17977" +2022-06-24 18:53:25.141487,3,"V2 96924 Gregory Mill Pricefurt, GA 68691" +2022-06-24 18:53:25.141491,4,"V2 070 Cynthia Cliff Paulport, FL 21469" diff --git a/demo/resources/data/snapshots/incremental_snapshots/stores/LOAD_3.csv b/demo/resources/data/snapshots/incremental_snapshots/stores/LOAD_3.csv new file mode 100644 index 0000000..d49d41e --- /dev/null +++ b/demo/resources/data/snapshots/incremental_snapshots/stores/LOAD_3.csv @@ -0,0 +1,3 @@ +dmsTimestamp,store_id,address +2022-06-24 18:53:25.141463,1,"v3_6761 Brian Falls Navarrobury, VA 17977" +2022-06-24 18:53:25.141482,5,"v3_4215 Bruce Shoals Apt. 920 Port Travis, SC 71335" diff --git a/demo/resources/data/snapshots/products/LOAD_1.csv b/demo/resources/data/snapshots/products/LOAD_1.csv new file mode 100644 index 0000000..c2907f6 --- /dev/null +++ b/demo/resources/data/snapshots/products/LOAD_1.csv @@ -0,0 +1,21 @@ +dmsTimestamp,product_id,name,price +2022-06-24 18:53:24.619896,1,shorts,793.50 +2022-06-24 18:53:24.619917,2,hat,598.91 +2022-06-24 18:53:24.619920,3,hat,914.34 +2022-06-24 18:53:24.619923,4,accessories,717.76 +2022-06-24 18:53:24.619925,5,sneakers,975.06 +2022-06-24 18:53:24.619928,6,shorts,875.98 +2022-06-24 18:53:24.619931,7,coat,170.43 +2022-06-24 18:53:24.619933,8,hat,931.89 +2022-06-24 18:53:24.619936,9,shorts,627.72 +2022-06-24 18:53:24.619938,10,shirt,214.82 +2022-06-24 18:53:24.619941,11,sweater,534.26 +2022-06-24 18:53:24.619943,12,boots,933.89 +2022-06-24 18:53:24.619946,13,cap,600.41 +2022-06-24 18:53:24.619948,14,cap,608.73 +2022-06-24 18:53:24.619951,15,hat,747.17 +2022-06-24 18:53:24.619953,16,accessories,487.74 +2022-06-24 18:53:24.619956,17,coat,236.99 +2022-06-24 18:53:24.619958,18,accessories,960.39 +2022-06-24 18:53:24.619961,19,sweatshirt,600.43 +2022-06-24 18:53:24.619964,20,shirt,492.41 diff --git a/demo/resources/data/snapshots/stores/LOAD_1.csv b/demo/resources/data/snapshots/stores/LOAD_1.csv new file mode 100644 index 0000000..1ce6216 --- /dev/null +++ b/demo/resources/data/snapshots/stores/LOAD_1.csv @@ -0,0 +1,5 @@ +dmsTimestamp,store_id,address +2022-06-24 18:53:25.141463,1,"6761 Brian Falls Navarrobury, VA 17977" +2022-06-24 18:53:25.141482,2,"4215 Bruce Shoals Apt. 920 Port Travis, SC 71335" +2022-06-24 18:53:25.141487,3,"96924 Gregory Mill Pricefurt, GA 68691" +2022-06-24 18:53:25.141491,4,"070 Cynthia Cliff Paulport, FL 21469" diff --git a/docs/content/demo/Apply_Changes_From_Snapshot.md b/docs/content/demo/Apply_Changes_From_Snapshot.md new file mode 100644 index 0000000..8006ec7 --- /dev/null +++ b/docs/content/demo/Apply_Changes_From_Snapshot.md @@ -0,0 +1,50 @@ +--- +title: "Silver Fanout Demo" +date: 2024-10-04T14:25:26-04:00 +weight: 26 +draft: false +--- + +### Apply Changes From Snapshot Demo + - This demo will perform following steps + - Showcase onboarding process for apply changes from snapshot pattern + - Run onboarding for the bronze stores and products tables, which contains data snapshot data in csv files. + - Run Bronze DLT to load initial snapshot (LOAD_1.csv) + - Upload incremental snapshot LOAD_2.csv version=2 for stores and product + - Run Bronze DLT to load incremental snapshot (LOAD_2.csv). Stores is scd_type=2 so updated records will expired and added new records with version_number. Products is scd_type=1 so in case records missing for scd_type=1 will be deleted. + - Upload incremental snapshot LOAD_3.csv version=3 for stores and product + - Run Bronze DLT to load incremental snapshot (LOAD_3.csv). Stores is scd_type=2 so updated records will expired and added new records with version_number. Products is scd_type=1 so in case records missing for scd_type=1 will be deleted. + + +### Steps: +1. Launch Command Prompt + +2. Install [Databricks CLI](https://docs.databricks.com/dev-tools/cli/index.html) + - Once you install Databricks CLI, authenticate your current machine to a Databricks Workspace: + + ```commandline + databricks auth login --host WORKSPACE_HOST + ``` + +3. ```commandline + git clone https://github.com/databrickslabs/dlt-meta.git + ``` + +4. ```commandline + cd dlt-meta + ``` +5. Set python environment variable into terminal + ```commandline + dlt_meta_home=$(pwd) + ``` + ```commandline + export PYTHONPATH=$dlt_meta_home + +6. ```commandline + python demo/launch_acfs_demo.py --uc_catalog_name=<> + ``` + - uc_catalog_name : Unity catalog name + - you can provide `--profile=databricks_profile name` in case you already have databricks cli otherwise command prompt will ask host and token. + + + ![acfs.png](/images/acfs.png) \ No newline at end of file diff --git a/docs/content/demo/_index.md b/docs/content/demo/_index.md index 0d490cb..13e6a38 100644 --- a/docs/content/demo/_index.md +++ b/docs/content/demo/_index.md @@ -9,4 +9,5 @@ draft: false 2. **Databricks Techsummit Demo**: 100s of data sources ingestion in bronze and silver DLT pipelines automatically. 3. **Append FLOW Autoloader Demo**: Write to same target from multiple sources using append_flow and adding file metadata using [File metadata column](https://docs.databricks.com/en/ingestion/file-metadata-column.html) 4. **Append FLOW Eventhub Demo**: Write to same target from multiple sources using append_flow and adding using [File metadata column](https://docs.databricks.com/en/ingestion/file-metadata-column.html) - 5. **Silver Fanout Demo**: This demo will showcase fanout architecture can be implemented in silver layer \ No newline at end of file + 5. **Silver Fanout Demo**: This demo will showcase fanout architecture can be implemented in silver layer + 6. **Apply Changes From Snapshot Demo**: This demo will showcase [apply_changes_from_snapshot](https://docs.databricks.com/en/delta-live-tables/python-ref.html#change-data-capture-from-database-snapshots-with-python-in-delta-live-tables) can be implemented inside bronze layer \ No newline at end of file diff --git a/docs/static/images/acfs.png b/docs/static/images/acfs.png new file mode 100644 index 0000000000000000000000000000000000000000..c283bafadcc28fb3bf57d1a6f8b564df69a7067a GIT binary patch literal 238988 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%0DTloj~U$i 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 AO#lD@ literal 0 HcmV?d00001 diff --git a/examples/dlt_meta_pipeline_snapshot.ipynb b/examples/dlt_meta_pipeline_snapshot.ipynb index 25016c1..fbf6490 100644 --- a/examples/dlt_meta_pipeline_snapshot.ipynb +++ b/examples/dlt_meta_pipeline_snapshot.ipynb @@ -71,7 +71,7 @@ "layer = spark.conf.get(\"layer\", None)\n", "\n", "from src.dataflow_pipeline import DataflowPipeline\n", - "DataflowPipeline.invoke_dlt_pipeline(spark, layer)" + "DataflowPipeline.invoke_dlt_pipeline(spark, layer, next_snapshot_and_version=next_snapshot_and_version)" ] } ], diff --git a/integration_tests/conf/snapshot-onboarding.template b/integration_tests/conf/snapshot-onboarding.template index 3b62403..8ff1cc9 100644 --- a/integration_tests/conf/snapshot-onboarding.template +++ b/integration_tests/conf/snapshot-onboarding.template @@ -38,7 +38,7 @@ "keys": [ "store_id" ], - "scd_type": "2" + "scd_type": "1" } } ] \ No newline at end of file diff --git a/integration_tests/resources/data/snapshots/incremental_snapshots/stores/LOAD_2.csv b/integration_tests/resources/data/snapshots/incremental_snapshots/stores/LOAD_2.csv index e0ccf60..3d83954 100644 --- a/integration_tests/resources/data/snapshots/incremental_snapshots/stores/LOAD_2.csv +++ b/integration_tests/resources/data/snapshots/incremental_snapshots/stores/LOAD_2.csv @@ -1,5 +1,3 @@ dmsTimestamp,store_id,address 2022-06-24 18:53:25.141463,1,"V2 6761 Brian Falls Navarrobury, VA 17977" 2022-06-24 18:53:25.141482,2,"V2 4215 Bruce Shoals Apt. 920 Port Travis, SC 71335" -2022-06-24 18:53:25.141487,3,"V2 96924 Gregory Mill Pricefurt, GA 68691" -2022-06-24 18:53:25.141491,4,"V2 070 Cynthia Cliff Paulport, FL 21469" diff --git a/integration_tests/resources/data/snapshots/incremental_snapshots/stores/LOAD_3.csv b/integration_tests/resources/data/snapshots/incremental_snapshots/stores/LOAD_3.csv index 7c4f1ef..d49d41e 100644 --- a/integration_tests/resources/data/snapshots/incremental_snapshots/stores/LOAD_3.csv +++ b/integration_tests/resources/data/snapshots/incremental_snapshots/stores/LOAD_3.csv @@ -1,5 +1,3 @@ dmsTimestamp,store_id,address 2022-06-24 18:53:25.141463,1,"v3_6761 Brian Falls Navarrobury, VA 17977" -2022-06-24 18:53:25.141482,2,"v3_4215 Bruce Shoals Apt. 920 Port Travis, SC 71335" -2022-06-24 18:53:25.141487,3,"v3_96924 Gregory Mill Pricefurt, GA 68691" -2022-06-24 18:53:25.141491,4,"v3_070 Cynthia Cliff Paulport, FL 21469" +2022-06-24 18:53:25.141482,5,"v3_4215 Bruce Shoals Apt. 920 Port Travis, SC 71335" From 9287e656f71a1bbe4e2819770a1f16a4d3dd5078 Mon Sep 17 00:00:00 2001 From: ravi-databricks Date: Fri, 4 Oct 2024 12:22:04 -0700 Subject: [PATCH 31/59] Added image inside demo readme --- demo/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/demo/README.md b/demo/README.md index 4755f23..c395e29 100644 --- a/demo/README.md +++ b/demo/README.md @@ -248,4 +248,5 @@ As part of above scenarios for scd_type1 stores if records are missing in snapsh 6. Run the command ```commandline python demo/launch_acfs_demo.py --uc_catalog_name=<<>> - ``` \ No newline at end of file + ``` + ![acfs.png](../docs/static/images/acfs.png) \ No newline at end of file From 9710e579f951e291d27b64e40efa39df772b21ce Mon Sep 17 00:00:00 2001 From: ravi-databricks Date: Fri, 4 Oct 2024 12:29:26 -0700 Subject: [PATCH 32/59] Updated demo readme with description matching docs site --- demo/README.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/demo/README.md b/demo/README.md index c395e29..7e21586 100644 --- a/demo/README.md +++ b/demo/README.md @@ -221,11 +221,14 @@ This demo will perform following tasks: # Apply Changes From Snapshot Demo -This demo will showcase how to load bronze tables from snapshot files. There are two sources product and stores in which product is **scd_type=2** and stores is **scd_type=1**. -Day1 there is LOAD_1.csv file and for which next_snapshot_and_version function will be provided inside[dlt_meta_pipeline_snapshot.ipynb](https://github.com/databrickslabs/dlt-meta/blob/main/examples/dlt_meta_pipeline_snapshot.ipynb) -Day2 there will be LOAD_2.csv file loaded which will have updated values for products and stores with v2_ -Day3 there will be LOAD_3.csv file loaded which will have updated values for products and stores with v3_ -As part of above scenarios for scd_type1 stores if records are missing in snapshot those records will be deleted. for scd_typ2 matching keys will expire old records and insert new record with version_number + - This demo will perform following steps + - Showcase onboarding process for apply changes from snapshot pattern + - Run onboarding for the bronze stores and products tables, which contains data snapshot data in csv files. + - Run Bronze DLT to load initial snapshot (LOAD_1.csv) + - Upload incremental snapshot LOAD_2.csv version=2 for stores and product + - Run Bronze DLT to load incremental snapshot (LOAD_2.csv). Stores is scd_type=2 so updated records will expired and added new records with version_number. Products is scd_type=1 so in case records missing for scd_type=1 will be deleted. + - Upload incremental snapshot LOAD_3.csv version=3 for stores and product + - Run Bronze DLT to load incremental snapshot (LOAD_3.csv). Stores is scd_type=2 so updated records will expired and added new records with version_number. Products is scd_type=1 so in case records missing for scd_type=1 will be deleted. ### Steps: 1. Launch Command Prompt From 2db4a0d5f711f31ec9cc5f9f84b6e1333e63453e Mon Sep 17 00:00:00 2001 From: ravi-databricks Date: Sat, 5 Oct 2024 10:11:32 -0700 Subject: [PATCH 33/59] Fixed CLI Unit tests --- src/cli.py | 10 +++++----- tests/test_cli.py | 31 +++++++++++++++---------------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/cli.py b/src/cli.py index 6e9fe51..fb9dfd6 100644 --- a/src/cli.py +++ b/src/cli.py @@ -42,9 +42,9 @@ class OnboardCommand: import_author: str version: str dlt_meta_schema: str - dbfs_path: None - cloud: None - dbr_version: None + dbfs_path: str = None + cloud: str = None + dbr_version: str = None serverless: bool = True bronze_schema: str = None silver_schema: str = None @@ -520,9 +520,9 @@ def update_ws_onboarding_paths(self, cmd: OnboardCommand): if 'uc_volume_path' in source_value: data_flow[key][source_key] = source_value.format( uc_volume_path=f"{cmd.uc_volume_path}/dltmeta_conf/") - else: + elif 'dbfs_path' in source_value: data_flow[key][source_key] = source_value.format( - uc_volume_path=f"{cmd.dbfs_path}/dltmeta_conf/") + dbfs_path=f"{cmd.dbfs_path}/dltmeta_conf/") if 'uc_volume_path' in value: if cmd.uc_enabled: data_flow[key] = value.format(uc_volume_path=f"{cmd.uc_volume_path}/dltmeta_conf/") diff --git a/tests/test_cli.py b/tests/test_cli.py index deffb21..98aa4d9 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -49,12 +49,18 @@ def test_copy_to_uc_volume(self): ("/path/to/src", [], ["file1.txt", "file2.txt"]), ("/path/to/src/subdir", [], ["file3.txt"]) ] - with patch('builtins.open') as mock_open: + with patch('builtins.open', new_callable=MagicMock) as mock_open: mock_open.return_value = MagicMock() - mock_dbfs_upload = MagicMock() - mock_ws.dbfs.upload = mock_dbfs_upload - dltmeta.test_copy_to_uc_volume("file:/path/to/src", "/dbfs/path/to/dst") - self.assertEqual(mock_dbfs_upload.call_count, 3) + mock_files_upload = MagicMock() + mock_ws.files.upload = mock_files_upload + dltmeta.copy_to_uc_volume("file:/path/to/src", "/dbfs/path/to/dst") + self.assertEqual(mock_files_upload.call_count, 3) + mock_files_upload.assert_any_call(file_path="/dbfs/path/to/dst/src/file1.txt", + contents=mock_open.return_value, overwrite=True) + mock_files_upload.assert_any_call(file_path="/dbfs/path/to/dst/src/file2.txt", + contents=mock_open.return_value, overwrite=True) + mock_files_upload.assert_any_call(file_path="/dbfs/path/to/dst/src/subdir/file3.txt", + contents=mock_open.return_value, overwrite=True) @patch('src.cli.WorkspaceClient') def test_onboard(self, mock_workspace_client): @@ -62,9 +68,6 @@ def test_onboard(self, mock_workspace_client): mock_jobs = MagicMock() mock_workspace_client.dbfs = mock_dbfs mock_workspace_client.jobs = mock_jobs - mock_workspace_client.dbfs.exists.return_value = False - mock_workspace_client.dbfs.mkdirs.return_value = None - mock_workspace_client.dbfs.upload.return_value = None mock_workspace_client.jobs.create.return_value = MagicMock(job_id="job_id") mock_workspace_client.jobs.run_now.return_value = MagicMock(run_id="run_id") @@ -73,8 +76,6 @@ def test_onboard(self, mock_workspace_client): with patch.object(dltmeta._wsi, "_upload_wheel", return_value="/path/to/wheel"): dltmeta.onboard(self.onboard_cmd) - mock_workspace_client.dbfs.exists.assert_called_once_with('/dbfs/dltmeta_conf/') - mock_workspace_client.dbfs.mkdirs.assert_called_once_with("/dbfs/dltmeta_conf/") mock_workspace_client.jobs.create.assert_called_once() mock_workspace_client.jobs.run_now.assert_called_once_with(job_id="job_id") @@ -120,20 +121,18 @@ def test_create_dlt_meta_pipeline(self, mock_workspace_client): def test_get_onboarding_named_parameters(self): cmd = OnboardCommand( - dbr_version="7.3", - dbfs_path="/dbfs", onboarding_file_path="tests/resources/onboarding.json", onboarding_files_dir_path="tests/resources/", onboard_layer="bronze", env="dev", - import_author="John Doe", + import_author="Ravi Gawai", version="1.0", - cloud="aws", dlt_meta_schema="dlt_meta", bronze_dataflowspec_path="tests/resources/bronze_dataflowspec", silver_dataflowspec_path="tests/resources/silver_dataflowspec", uc_enabled=True, uc_catalog_name="uc_catalog", + uc_volume_path="/dbfs/mnt/uc_volume", overwrite=True, bronze_dataflowspec_table="bronze_dataflowspec", silver_dataflowspec_table="silver_dataflowspec", @@ -144,8 +143,8 @@ def test_get_onboarding_named_parameters(self): expected_named_parameters = { "onboard_layer": "bronze", "database": "uc_catalog.dlt_meta" if cmd.uc_enabled else "dlt_meta", - "onboarding_file_path": "/dbfs/dltmeta_conf/onboarding.json", - "import_author": "John Doe", + "onboarding_file_path": "/dbfs/mnt/uc_volume/dltmeta_conf/onboarding.json", + "import_author": "Ravi Gawai", "version": "1.0", "overwrite": "True", "env": "dev", From f4bd2377b84db449d6d5d907a40cf1e5ede879a3 Mon Sep 17 00:00:00 2001 From: ravi-databricks Date: Sun, 6 Oct 2024 15:48:55 -0700 Subject: [PATCH 34/59] Added unit test coverage --- src/dataflow_pipeline.py | 1 + src/dataflow_spec.py | 14 +- src/onboard_dataflowspec.py | 82 +++++--- tests/resources/onboarding.json | 169 +++++++++++++++- ...ding_applychanges_from_snapshot_error.json | 41 ++++ tests/resources/onboarding_v0.0.8.json | 181 ++++++++++++++++++ tests/test_dataflow_pipeline.py | 21 ++ tests/test_dataflow_spec.py | 1 - tests/test_onboard_dataflowspec.py | 84 ++++++++ tests/utils.py | 4 + 10 files changed, 571 insertions(+), 27 deletions(-) create mode 100644 tests/resources/onboarding_applychanges_from_snapshot_error.json create mode 100644 tests/resources/onboarding_v0.0.8.json diff --git a/src/dataflow_pipeline.py b/src/dataflow_pipeline.py index 0933722..488b51d 100644 --- a/src/dataflow_pipeline.py +++ b/src/dataflow_pipeline.py @@ -329,6 +329,7 @@ def apply_changes_from_snapshot(self): keys=self.appy_changes_from_snapshot.keys, stored_as_scd_type=self.appy_changes_from_snapshot.scd_type, track_history_column_list=self.appy_changes_from_snapshot.track_history_column_list, + track_history_except_column_list=self.appy_changes_from_snapshot.track_history_except_column_list, ) def write_bronze_with_dqe(self): diff --git a/src/dataflow_spec.py b/src/dataflow_spec.py index 32f7099..91627b1 100644 --- a/src/dataflow_spec.py +++ b/src/dataflow_spec.py @@ -148,7 +148,6 @@ class DataflowSpecUtils: "ignore_null_updates_except_column_list": None } - apply_changes_from_snapshot_api_mandatory_attributes = ["keys", "scd_type"] append_flow_mandatory_attributes = ["name", "source_format", "create_streaming_table", "source_details"] append_flow_api_attributes_defaults = { "comment": None, @@ -158,10 +157,21 @@ class DataflowSpecUtils: "once": False } - additional_bronze_df_columns = ["appendFlows", "appendFlowsSchemas"] + additional_bronze_df_columns = ["appendFlows", "appendFlowsSchemas", "applyChangesFromSnapshot"] additional_silver_df_columns = ["dataQualityExpectations", "appendFlows", "appendFlowsSchemas"] additional_cdc_apply_changes_columns = ["flow_name", "once"] + apply_changes_from_snapshot_api_attributes = [ + "keys", + "scd_type", + "track_history_column_list", + "track_history_except_column_list" + ] + apply_changes_from_snapshot_api_mandatory_attributes = ["keys", "scd_type"] additional_apply_changes_from_snapshot_columns = ["track_history_column_list", "track_history_except_column_list"] + apply_changes_from_snapshot_api_attributes_defaults = { + "track_history_column_list": None, + "track_history_except_column_list": None + } @staticmethod def _get_dataflow_spec( diff --git a/src/onboard_dataflowspec.py b/src/onboard_dataflowspec.py index a61283e..f18f9c5 100644 --- a/src/onboard_dataflowspec.py +++ b/src/onboard_dataflowspec.py @@ -487,8 +487,10 @@ def __get_bronze_dataflow_spec_dataframe(self, onboarding_df, env): apply_changes_from_snapshot = None if ("bronze_apply_changes_from_snapshot" in onboarding_row and onboarding_row["bronze_apply_changes_from_snapshot"]): - apply_changes_from_snapshot = onboarding_row["bronze_apply_changes_from_snapshot"] - apply_changes_from_snapshot = json.dumps(self.__delete_none(apply_changes_from_snapshot.asDict())) + self.__validate_apply_changes_from_snapshot(onboarding_row, "bronze") + apply_changes_from_snapshot = json.dumps( + self.__delete_none(onboarding_row["bronze_apply_changes_from_snapshot"].asDict()) + ) data_quality_expectations = None quarantine_target_details = {} quarantine_table_properties = {} @@ -498,26 +500,9 @@ def __get_bronze_dataflow_spec_dataframe(self, onboarding_df, env): data_quality_expectations = ( self.__get_data_quality_expecations(bronze_data_quality_expectations_json)) if onboarding_row["bronze_quarantine_table"]: - quarantine_table_partition_columns = "" - if ( - "bronze_quarantine_table_partitions" in onboarding_row - and onboarding_row["bronze_quarantine_table_partitions"] - ): - quarantine_table_partition_columns = onboarding_row["bronze_quarantine_table_partitions"] - quarantine_target_details = { - "database": onboarding_row[f"bronze_database_quarantine_{env}"], - "table": onboarding_row["bronze_quarantine_table"], - "partition_columns": quarantine_table_partition_columns, - } - if not self.uc_enabled: - quarantine_target_details["path"] = onboarding_row[ - f"bronze_quarantine_table_path_{env}"] - if ( - "bronze_quarantine_table_properties" in onboarding_row - and onboarding_row["bronze_quarantine_table_properties"] - ): - quarantine_table_properties = self.__delete_none( - onboarding_row["bronze_quarantine_table_properties"].asDict()) + quarantine_target_details, quarantine_table_properties = self.__get_quarantine_details( + env, onboarding_row + ) append_flows, append_flows_schemas = self.get_append_flows_json(onboarding_row, "bronze", env) bronze_row = ( bronze_data_flow_spec_id, @@ -545,6 +530,29 @@ def __get_bronze_dataflow_spec_dataframe(self, onboarding_df, env): return data_flow_spec_rows_df + def __get_quarantine_details(self, env, onboarding_row): + quarantine_table_partition_columns = "" + quarantine_target_details = {} + quarantine_table_properties = {} + if ( + "bronze_quarantine_table_partitions" in onboarding_row + and onboarding_row["bronze_quarantine_table_partitions"] + ): + quarantine_table_partition_columns = onboarding_row["bronze_quarantine_table_partitions"] + quarantine_target_details = {"database": onboarding_row[f"bronze_database_quarantine_{env}"], + "table": onboarding_row["bronze_quarantine_table"], + "partition_columns": quarantine_table_partition_columns + } + if not self.uc_enabled: + quarantine_target_details["path"] = onboarding_row[f"bronze_quarantine_table_path_{env}"] + if ( + "bronze_quarantine_table_properties" in onboarding_row + and onboarding_row["bronze_quarantine_table_properties"] + ): + quarantine_table_properties = self.__delete_none( + onboarding_row["bronze_quarantine_table_properties"].asDict()) + return quarantine_target_details, quarantine_table_properties + def get_append_flows_json(self, onboarding_row, layer, env): append_flows = None append_flows_schema = {} @@ -579,7 +587,7 @@ def get_append_flows_json(self, onboarding_row, layer, env): def __validate_apply_changes(self, onboarding_row, layer): cdc_apply_changes = onboarding_row[f"{layer}_cdc_apply_changes"] - json_cdc_apply_changes = cdc_apply_changes.asDict() + json_cdc_apply_changes = self.__delete_none(cdc_apply_changes.asDict()) logger.info(f"actual mergeInfo={json_cdc_apply_changes}") payload_keys = json_cdc_apply_changes.keys() missing_cdc_payload_keys = set(DataflowSpecUtils.cdc_applychanges_api_attributes).difference(payload_keys) @@ -603,6 +611,34 @@ def __validate_apply_changes(self, onboarding_row, layer): {DataflowSpecUtils.cdc_applychanges_api_mandatory_attributes} exists""" ) + def __validate_apply_changes_from_snapshot(self, onboarding_row, layer): + apply_changes_from_snapshot = onboarding_row[f"{layer}_apply_changes_from_snapshot"] + json_apply_changes_from_snapshot = self.__delete_none(apply_changes_from_snapshot.asDict()) + logger.info(f"actual applyChangesFromSnapshot={json_apply_changes_from_snapshot}") + payload_keys = json_apply_changes_from_snapshot.keys() + missing_apply_changes_from_snapshot_payload_keys = ( + set(DataflowSpecUtils.apply_changes_from_snapshot_api_attributes).difference(payload_keys) + ) + logger.info( + f"""missing applyChangesFromSnapshot payload keys:{missing_apply_changes_from_snapshot_payload_keys} + for onboarding row = {onboarding_row}""" + ) + if set(DataflowSpecUtils.apply_changes_from_snapshot_api_mandatory_attributes) - set(payload_keys): + missing_mandatory_attr = set(DataflowSpecUtils.apply_changes_from_snapshot_api_mandatory_attributes) - set( + payload_keys + ) + logger.info(f"mandatory missing keys= {missing_mandatory_attr}") + raise Exception( + f"""mandatory missing atrributes for {layer}_apply_changes_from_snapshot = { + missing_mandatory_attr} + for onboarding row = {onboarding_row}""" + ) + else: + logger.info( + f"""all mandatory {layer}_apply_changes_from_snapshot atrributes + {DataflowSpecUtils.apply_changes_from_snapshot_api_mandatory_attributes} exists""" + ) + def get_bronze_source_details_reader_options_schema(self, onboarding_row, env): """Get bronze source reader options. diff --git a/tests/resources/onboarding.json b/tests/resources/onboarding.json index 791e7b6..9ccab56 100644 --- a/tests/resources/onboarding.json +++ b/tests/resources/onboarding.json @@ -1 +1,168 @@ -[{"data_flow_id": "100", "data_flow_group": "A1", "source_system": "MYSQL", "source_format": "cloudFiles", "source_details": {"source_database": "APP", "source_table": "CUSTOMERS", "source_path_dev": "tests/resources/data/customers", "source_schema_path": "tests/resources/schema/customer_schema.ddl", "source_metadata": {"include_autoloader_metadata_column": "True", "autoloader_metadata_col_name": "source_metadata", "select_metadata_cols": {"input_file_name": "_metadata.file_name", "input_file_path": "_metadata.file_path"}}}, "bronze_database_dev": "bronze", "bronze_database_staging": "bronze", "bronze_database_prd": "bronze", "bronze_table": "customers_cdc", "bronze_reader_options": {"cloudFiles.format": "json", "cloudFiles.inferColumnTypes": "true", "cloudFiles.rescuedDataColumn": "_rescued_data"}, "bronze_table_path_dev": "tests/resources/delta/customers", "bronze_table_properties": {"pipelines.autoOptimize.managed": "false", "pipelines.reset.allowed": "false"}, "bronze_data_quality_expectations_json_dev": "tests/resources/dqe/customers/bronze_data_quality_expectations.json", "silver_database_dev": "silver", "silver_database_staging": "silver", "silver_database_prd": "silver", "silver_table": "customers", "silver_cdc_apply_changes": {"keys": ["id"], "sequence_by": "operation_date", "scd_type": "1", "apply_as_deletes": "operation = 'DELETE'", "except_column_list": ["operation", "operation_date", "_rescued_data"]}, "silver_table_path_dev": "tests/resources/data/silver/customers", "silver_table_properties": {"pipelines.autoOptimize.managed": "false", "pipelines.reset.allowed": "false", "pipelines.autoOptimize.zOrderCols": "id,email"}, "silver_transformation_json_dev": "tests/resources/silver_transformations.json", "silver_data_quality_expectations_json_dev": "tests/resources/dqe/customers/silver_data_quality_expectations.json"}, {"data_flow_id": "101", "data_flow_group": "A1", "source_system": "MYSQL", "source_format": "cloudFiles", "source_details": {"source_database": "APP", "source_table": "TRANSACTIONS", "source_path_prd": "tests/resources/data/transactions", "source_path_dev": "tests/resources/data/transactions", "source_metadata": {"include_autoloader_metadata_column": "True"}}, "bronze_database_dev": "bronze", "bronze_database_staging": "bronze", "bronze_database_prd": "bronze", "bronze_table": "transactions_cdc", "bronze_reader_options": {"cloudFiles.format": "json", "cloudFiles.inferColumnTypes": "true", "cloudFiles.rescuedDataColumn": "_rescued_data"}, "bronze_table_path_dev": "tests/resources/delta/transactions", "bronze_table_path_staging": "s3://db-dlt-meta-staging/demo/data/bronze/transactions", "bronze_table_path_prd": "s3://db-dlt-meta-prod/demo/data/bronze/transactions", "bronze_table_properties": {"pipelines.reset.allowed": "false"}, "bronze_data_quality_expectations_json_dev": "tests/resources/dqe/transactions/bronze_data_quality_expectations.json", "bronze_database_quarantine_dev": "bronze", "bronze_database_quarantine_staging": "bronze", "bronze_database_quarantine_prd": "bronze", "bronze_quarantine_table": "transactions_cdc_quarantine", "bronze_quarantine_table_path_dev": "tests/resources/data/bronze/transactions_quarantine", "silver_database_dev": "silver", "silver_database_preprd": "silver", "silver_database_prd": "silver", "silver_table": "transactions", "silver_cdc_apply_changes": {"keys": ["id"], "sequence_by": "operation_date", "scd_type": "1", "apply_as_deletes": "operation = 'DELETE'", "except_column_list": ["operation", "operation_date", "_rescued_data"]}, "silver_partition_columns": "transaction_date", "silver_table_path_dev": "tests/resources/data/silver/transactions", "silver_transformation_json_dev": "tests/resources/silver_transformations.json", "silver_table_properties": {"pipelines.reset.allowed": "false", "pipelines.autoOptimize.zOrderCols": "id, customer_id"}, "silver_data_quality_expectations_json_dev": "tests/resources/dqe/transactions/silver_data_quality_expectations.json"}, {"data_flow_id": "103", "data_flow_group": "A2", "source_system": "MYSQL", "source_format": "eventhub", "source_details": {"source_schema_path": "tests/resources/schema/eventhub_iot_schema.ddl", "eventhub.accessKeyName": "iotIngestionAccessKey", "eventhub.name": "iot", "eventhub.accessKeySecretName": "iotIngestionAccessKey", "eventhub.secretsScopeName": "eventhubs_creds", "kafka.sasl.mechanism": "PLAIN", "kafka.security.protocol": "SASL_SSL", "kafka.bootstrap.servers": "standard.servicebus.windows.net:9093"}, "bronze_database_dev": "bronze", "bronze_database_staging": "bronze", "bronze_database_prd": "bronze", "bronze_table": "iot_cdc", "bronze_reader_options": {"maxOffsetsPerTrigger": "50000", "startingOffsets": "latest", "failOnDataLoss": "false", "kafka.request.timeout.ms": "60000", "kafka.session.timeout.ms": "60000"}, "bronze_table_path_dev": "tests/resources/delta/iot_cdc", "bronze_table_path_staging": "s3://db-dlt-meta-staging/demo/data/bronze/iot_cdc", "bronze_table_path_prd": "s3://db-dlt-meta-prod/demo/data/bronze/iot_cdc", "bronze_data_quality_expectations_json_dev": "tests/resources/dqe/iot_cdc/bronze_data_quality_expectations.json", "silver_database_dev": "silver", "silver_table": "iot_cdc", "silver_cdc_apply_changes": {"keys": ["device_id"], "sequence_by": "timestamp", "scd_type": "1", "apply_as_deletes": "operation = 'DELETE'", "except_column_list": []}, "silver_table_path_dev": "tests/resources/data/silver/iot_cdc", "silver_transformation_json_dev": "tests/resources/silver_transformations.json", "silver_data_quality_expectations_json_dev": "tests/resources/dqe/iot_cdc/silver_data_quality_expectations.json"}] \ No newline at end of file +[ + { + "data_flow_id": "100", + "data_flow_group": "A1", + "source_system": "MYSQL", + "source_format": "cloudFiles", + "source_details": { + "source_database": "APP", + "source_table": "CUSTOMERS", + "source_path_dev": "tests/resources/data/customers", + "source_schema_path": "tests/resources/schema/customer_schema.ddl", + "source_metadata": { + "include_autoloader_metadata_column": "True", + "autoloader_metadata_col_name": "source_metadata", + "select_metadata_cols": { + "input_file_name": "_metadata.file_name", + "input_file_path": "_metadata.file_path" + } + } + }, + "bronze_database_dev": "bronze", + "bronze_database_staging": "bronze", + "bronze_database_prd": "bronze", + "bronze_table": "customers_cdc", + "bronze_reader_options": { + "cloudFiles.format": "json", + "cloudFiles.inferColumnTypes": "true", + "cloudFiles.rescuedDataColumn": "_rescued_data" + }, + "bronze_table_path_dev": "tests/resources/delta/customers", + "bronze_table_properties": { + "pipelines.autoOptimize.managed": "false", + "pipelines.reset.allowed": "false" + }, + "bronze_data_quality_expectations_json_dev": "tests/resources/dqe/customers/bronze_data_quality_expectations.json", + "silver_database_dev": "silver", + "silver_database_staging": "silver", + "silver_database_prd": "silver", + "silver_table": "customers", + "silver_cdc_apply_changes": { + "keys": [ + "id" + ], + "sequence_by": "operation_date", + "scd_type": "1", + "apply_as_deletes": "operation = 'DELETE'", + "except_column_list": [ + "operation", + "operation_date", + "_rescued_data" + ] + }, + "silver_table_path_dev": "tests/resources/data/silver/customers", + "silver_table_properties": { + "pipelines.autoOptimize.managed": "false", + "pipelines.reset.allowed": "false", + "pipelines.autoOptimize.zOrderCols": "id,email" + }, + "silver_transformation_json_dev": "tests/resources/silver_transformations.json", + "silver_data_quality_expectations_json_dev": "tests/resources/dqe/customers/silver_data_quality_expectations.json" + }, + { + "data_flow_id": "101", + "data_flow_group": "A1", + "source_system": "MYSQL", + "source_format": "cloudFiles", + "source_details": { + "source_database": "APP", + "source_table": "TRANSACTIONS", + "source_path_prd": "tests/resources/data/transactions", + "source_path_dev": "tests/resources/data/transactions", + "source_metadata": { + "include_autoloader_metadata_column": "True" + } + }, + "bronze_database_dev": "bronze", + "bronze_database_staging": "bronze", + "bronze_database_prd": "bronze", + "bronze_table": "transactions_cdc", + "bronze_reader_options": { + "cloudFiles.format": "json", + "cloudFiles.inferColumnTypes": "true", + "cloudFiles.rescuedDataColumn": "_rescued_data" + }, + "bronze_table_path_dev": "tests/resources/delta/transactions", + "bronze_table_path_staging": "s3://db-dlt-meta-staging/demo/data/bronze/transactions", + "bronze_table_path_prd": "s3://db-dlt-meta-prod/demo/data/bronze/transactions", + "bronze_table_properties": { + "pipelines.reset.allowed": "false" + }, + "bronze_data_quality_expectations_json_dev": "tests/resources/dqe/transactions/bronze_data_quality_expectations.json", + "bronze_database_quarantine_dev": "bronze", + "bronze_database_quarantine_staging": "bronze", + "bronze_database_quarantine_prd": "bronze", + "bronze_quarantine_table": "transactions_cdc_quarantine", + "bronze_quarantine_table_path_dev": "tests/resources/data/bronze/transactions_quarantine", + "silver_database_dev": "silver", + "silver_database_preprd": "silver", + "silver_database_prd": "silver", + "silver_table": "transactions", + "silver_cdc_apply_changes": { + "keys": [ + "id" + ], + "sequence_by": "operation_date", + "scd_type": "1", + "apply_as_deletes": "operation = 'DELETE'", + "except_column_list": [ + "operation", + "operation_date", + "_rescued_data" + ] + }, + "silver_partition_columns": "transaction_date", + "silver_table_path_dev": "tests/resources/data/silver/transactions", + "silver_transformation_json_dev": "tests/resources/silver_transformations.json", + "silver_table_properties": { + "pipelines.reset.allowed": "false", + "pipelines.autoOptimize.zOrderCols": "id, customer_id" + }, + "silver_data_quality_expectations_json_dev": "tests/resources/dqe/transactions/silver_data_quality_expectations.json" + }, + { + "data_flow_id": "103", + "data_flow_group": "A2", + "source_system": "MYSQL", + "source_format": "eventhub", + "source_details": { + "source_schema_path": "tests/resources/schema/eventhub_iot_schema.ddl", + "eventhub.accessKeyName": "iotIngestionAccessKey", + "eventhub.name": "iot", + "eventhub.accessKeySecretName": "iotIngestionAccessKey", + "eventhub.secretsScopeName": "eventhubs_creds", + "kafka.sasl.mechanism": "PLAIN", + "kafka.security.protocol": "SASL_SSL", + "kafka.bootstrap.servers": "standard.servicebus.windows.net:9093" + }, + "bronze_database_dev": "bronze", + "bronze_database_staging": "bronze", + "bronze_database_prd": "bronze", + "bronze_table": "iot_cdc", + "bronze_reader_options": { + "maxOffsetsPerTrigger": "50000", + "startingOffsets": "latest", + "failOnDataLoss": "false", + "kafka.request.timeout.ms": "60000", + "kafka.session.timeout.ms": "60000" + }, + "bronze_table_path_dev": "tests/resources/delta/iot_cdc", + "bronze_table_path_staging": "s3://db-dlt-meta-staging/demo/data/bronze/iot_cdc", + "bronze_table_path_prd": "s3://db-dlt-meta-prod/demo/data/bronze/iot_cdc", + "bronze_data_quality_expectations_json_dev": "tests/resources/dqe/iot_cdc/bronze_data_quality_expectations.json", + "silver_database_dev": "silver", + "silver_table": "iot_cdc", + "silver_cdc_apply_changes": { + "keys": [ + "device_id" + ], + "sequence_by": "timestamp", + "scd_type": "1", + "apply_as_deletes": "operation = 'DELETE'", + "except_column_list": [] + }, + "silver_table_path_dev": "tests/resources/data/silver/iot_cdc", + "silver_transformation_json_dev": "tests/resources/silver_transformations.json", + "silver_data_quality_expectations_json_dev": "tests/resources/dqe/iot_cdc/silver_data_quality_expectations.json" + } +] \ No newline at end of file diff --git a/tests/resources/onboarding_applychanges_from_snapshot_error.json b/tests/resources/onboarding_applychanges_from_snapshot_error.json new file mode 100644 index 0000000..bca0b30 --- /dev/null +++ b/tests/resources/onboarding_applychanges_from_snapshot_error.json @@ -0,0 +1,41 @@ +[ + + { + "data_flow_id": "203", + "data_flow_group": "A2", + "source_system": "delta", + "source_format": "snapshot", + "source_details": { + "source_path_it": "/Volumes/ravi_dlt_meta_uc/dlt_meta_dataflowspecs_it_23de6188b0b0442a9f0bdbaed368b1f7/dlt_meta_dataflowspecs_it_23de6188b0b0442a9f0bdbaed368b1f7//integration_tests/resources/data/snapshots/stores/LOAD_", + "snapshot_format": "csv" + }, + "bronze_reader_options": { + "header": "true" + }, + "bronze_database_it": "ravi_dlt_meta_uc.dlt_meta_bronze_it_23de6188b0b0442a9f0bdbaed368b1f7", + "bronze_table": "stores", + "bronze_apply_changes_from_snapshot": { + "scd_type": "2" + } +}, +{ + "data_flow_id": "204", + "data_flow_group": "A2", + "source_system": "delta", + "source_format": "snapshot", + "source_details": { + "source_path_it": "/Volumes/ravi_dlt_meta_uc/dlt_meta_dataflowspecs_it_23de6188b0b0442a9f0bdbaed368b1f7/dlt_meta_dataflowspecs_it_23de6188b0b0442a9f0bdbaed368b1f7//integration_tests/resources/data/snapshots/stores/LOAD_", + "snapshot_format": "csv" + }, + "bronze_reader_options": { + "header": "true" + }, + "bronze_database_it": "ravi_dlt_meta_uc.dlt_meta_bronze_it_23de6188b0b0442a9f0bdbaed368b1f7", + "bronze_table": "stores", + "bronze_apply_changes_from_snapshot": { + "keys": [ + "store_id" + ] + } +} +] \ No newline at end of file diff --git a/tests/resources/onboarding_v0.0.8.json b/tests/resources/onboarding_v0.0.8.json new file mode 100644 index 0000000..e1828e2 --- /dev/null +++ b/tests/resources/onboarding_v0.0.8.json @@ -0,0 +1,181 @@ +[ + { + "data_flow_id": "100", + "data_flow_group": "A1", + "source_system": "MYSQL", + "source_format": "cloudFiles", + "source_details": { + "source_database": "APP", + "source_table": "CUSTOMERS", + "source_path_dev": "tests/resources/data/customers", + "source_schema_path": "tests/resources/schema/customer_schema.ddl", + "source_metadata": { + "include_autoloader_metadata_column": "True", + "autoloader_metadata_col_name": "source_metadata", + "select_metadata_cols": { + "input_file_name": "_metadata.file_name", + "input_file_path": "_metadata.file_path" + } + } + }, + "bronze_database_dev": "bronze", + "bronze_database_staging": "bronze", + "bronze_database_prd": "bronze", + "bronze_table": "customers_cdc", + "bronze_reader_options": { + "cloudFiles.format": "json", + "cloudFiles.inferColumnTypes": "true", + "cloudFiles.rescuedDataColumn": "_rescued_data" + }, + "bronze_table_path_dev": "tests/resources/delta/customers", + "bronze_cdc_apply_changes": { + "keys": [ + "id" + ], + "sequence_by": "operation_date", + "scd_type": "1", + "apply_as_deletes": "operation = 'DELETE'", + "except_column_list": [ + "operation", + "operation_date", + "_rescued_data" + ] + }, + "bronze_table_properties": { + "pipelines.autoOptimize.managed": "false", + "pipelines.reset.allowed": "false" + }, + "bronze_data_quality_expectations_json_dev": "tests/resources/dqe/customers/bronze_data_quality_expectations.json", + "silver_database_dev": "silver", + "silver_database_staging": "silver", + "silver_database_prd": "silver", + "silver_table": "customers", + "silver_cdc_apply_changes": { + "keys": [ + "id" + ], + "sequence_by": "operation_date", + "scd_type": "1", + "apply_as_deletes": "operation = 'DELETE'", + "except_column_list": [ + "operation", + "operation_date", + "_rescued_data" + ] + }, + "silver_table_path_dev": "tests/resources/data/silver/customers", + "silver_table_properties": { + "pipelines.autoOptimize.managed": "false", + "pipelines.reset.allowed": "false", + "pipelines.autoOptimize.zOrderCols": "id,email" + }, + "silver_transformation_json_dev": "tests/resources/silver_transformations.json", + "silver_data_quality_expectations_json_dev": "tests/resources/dqe/customers/silver_data_quality_expectations.json" + }, + { + "data_flow_id": "101", + "data_flow_group": "A1", + "source_system": "MYSQL", + "source_format": "cloudFiles", + "source_details": { + "source_database": "APP", + "source_table": "TRANSACTIONS", + "source_path_prd": "tests/resources/data/transactions", + "source_path_dev": "tests/resources/data/transactions", + "source_metadata": { + "include_autoloader_metadata_column": "True" + } + }, + "bronze_database_dev": "bronze", + "bronze_database_staging": "bronze", + "bronze_database_prd": "bronze", + "bronze_table": "transactions_cdc", + "bronze_reader_options": { + "cloudFiles.format": "json", + "cloudFiles.inferColumnTypes": "true", + "cloudFiles.rescuedDataColumn": "_rescued_data" + }, + "bronze_table_path_dev": "tests/resources/delta/transactions", + "bronze_table_path_staging": "s3://db-dlt-meta-staging/demo/data/bronze/transactions", + "bronze_table_path_prd": "s3://db-dlt-meta-prod/demo/data/bronze/transactions", + "bronze_table_properties": { + "pipelines.reset.allowed": "false" + }, + "bronze_data_quality_expectations_json_dev": "tests/resources/dqe/transactions/bronze_data_quality_expectations.json", + "bronze_database_quarantine_dev": "bronze", + "bronze_database_quarantine_staging": "bronze", + "bronze_database_quarantine_prd": "bronze", + "bronze_quarantine_table": "transactions_cdc_quarantine", + "bronze_quarantine_table_path_dev": "tests/resources/data/bronze/transactions_quarantine", + "silver_database_dev": "silver", + "silver_database_preprd": "silver", + "silver_database_prd": "silver", + "silver_table": "transactions", + "silver_cdc_apply_changes": { + "keys": [ + "id" + ], + "sequence_by": "operation_date", + "scd_type": "1", + "apply_as_deletes": "operation = 'DELETE'", + "except_column_list": [ + "operation", + "operation_date", + "_rescued_data" + ] + }, + "silver_partition_columns": "transaction_date", + "silver_table_path_dev": "tests/resources/data/silver/transactions", + "silver_transformation_json_dev": "tests/resources/silver_transformations.json", + "silver_table_properties": { + "pipelines.reset.allowed": "false", + "pipelines.autoOptimize.zOrderCols": "id, customer_id" + }, + "silver_data_quality_expectations_json_dev": "tests/resources/dqe/transactions/silver_data_quality_expectations.json" + }, + { + "data_flow_id": "103", + "data_flow_group": "A2", + "source_system": "MYSQL", + "source_format": "eventhub", + "source_details": { + "source_schema_path": "tests/resources/schema/eventhub_iot_schema.ddl", + "eventhub.accessKeyName": "iotIngestionAccessKey", + "eventhub.name": "iot", + "eventhub.accessKeySecretName": "iotIngestionAccessKey", + "eventhub.secretsScopeName": "eventhubs_creds", + "kafka.sasl.mechanism": "PLAIN", + "kafka.security.protocol": "SASL_SSL", + "kafka.bootstrap.servers": "standard.servicebus.windows.net:9093" + }, + "bronze_database_dev": "bronze", + "bronze_database_staging": "bronze", + "bronze_database_prd": "bronze", + "bronze_table": "iot_cdc", + "bronze_reader_options": { + "maxOffsetsPerTrigger": "50000", + "startingOffsets": "latest", + "failOnDataLoss": "false", + "kafka.request.timeout.ms": "60000", + "kafka.session.timeout.ms": "60000" + }, + "bronze_table_path_dev": "tests/resources/delta/iot_cdc", + "bronze_table_path_staging": "s3://db-dlt-meta-staging/demo/data/bronze/iot_cdc", + "bronze_table_path_prd": "s3://db-dlt-meta-prod/demo/data/bronze/iot_cdc", + "bronze_data_quality_expectations_json_dev": "tests/resources/dqe/iot_cdc/bronze_data_quality_expectations.json", + "silver_database_dev": "silver", + "silver_table": "iot_cdc", + "silver_cdc_apply_changes": { + "keys": [ + "device_id" + ], + "sequence_by": "timestamp", + "scd_type": "1", + "apply_as_deletes": "operation = 'DELETE'", + "except_column_list": [] + }, + "silver_table_path_dev": "tests/resources/data/silver/iot_cdc", + "silver_transformation_json_dev": "tests/resources/silver_transformations.json", + "silver_data_quality_expectations_json_dev": "tests/resources/dqe/iot_cdc/silver_data_quality_expectations.json" + } +] \ No newline at end of file diff --git a/tests/test_dataflow_pipeline.py b/tests/test_dataflow_pipeline.py index f1047b4..604fdec 100644 --- a/tests/test_dataflow_pipeline.py +++ b/tests/test_dataflow_pipeline.py @@ -1004,3 +1004,24 @@ def test_read_append_flows(self, mock_view): bronze_dataflowSpec_df.appendFlows = None with self.assertRaises(Exception): pipeline = DataflowPipeline(self.spark, bronze_dataflowSpec_df, view_name, None) + + def test_get_dq_expectations_with_expect_all(self): + onboarding_params_map = copy.deepcopy(self.onboarding_bronze_silver_params_map) + onboarding_params_map['onboarding_file_path'] = self.onboarding_type2_json_file + o_dfs = OnboardDataflowspec(self.spark, onboarding_params_map) + o_dfs.onboard_bronze_dataflow_spec() + bronze_dataflowSpec_df = self.spark.read.format("delta").load( + self.onboarding_bronze_silver_params_map['bronze_dataflowspec_path'] + ) + bronze_df_row = bronze_dataflowSpec_df.filter(bronze_dataflowSpec_df.dataFlowId == "201").collect()[0] + bronze_row_dict = DataflowSpecUtils.populate_additional_df_cols( + bronze_df_row.asDict(), + DataflowSpecUtils.additional_bronze_df_columns + ) + bronze_dataflow_spec = BronzeDataflowSpec(**bronze_row_dict) + view_name = f"{bronze_dataflow_spec.targetDetails['table']}_inputView" + pipeline = DataflowPipeline(self.spark, bronze_dataflow_spec, view_name, None) + expect_all_dict, expect_all_or_drop_dict, expect_all_or_fail_dict = pipeline.get_dq_expectations() + self.assertIsNotNone(expect_all_dict) + self.assertIsNotNone(expect_all_or_drop_dict) + self.assertIsNotNone(expect_all_or_fail_dict) diff --git a/tests/test_dataflow_spec.py b/tests/test_dataflow_spec.py index 34a7ac0..fda71d4 100644 --- a/tests/test_dataflow_spec.py +++ b/tests/test_dataflow_spec.py @@ -469,4 +469,3 @@ def test_get_apply_changes_from_snapshot_invalid_json(self): }""" # Missing closing bracket for track_history_column_list with self.assertRaises(json.JSONDecodeError): DataflowSpecUtils.get_apply_changes_from_snapshot(apply_changes_from_snapshot) - diff --git a/tests/test_onboard_dataflowspec.py b/tests/test_onboard_dataflowspec.py index 27b6e49..5edf47d 100644 --- a/tests/test_onboard_dataflowspec.py +++ b/tests/test_onboard_dataflowspec.py @@ -344,6 +344,22 @@ def test_onboard_bronze_silver_with_v7(self): self.assertEqual(bronze_dataflowSpec_df.count(), 3) self.assertEqual(silver_dataflowSpec_df.count(), 3) + def test_onboard_bronze_silver_with_v8(self): + local_params = copy.deepcopy(self.onboarding_bronze_silver_params_map) + local_params["onboarding_file_path"] = self.onboarding_json_v8_file + onboardDataFlowSpecs = OnboardDataflowspec(self.spark, local_params) + onboardDataFlowSpecs.onboard_dataflow_specs() + bronze_dataflowSpec_df = self.read_dataflowspec( + self.onboarding_bronze_silver_params_map['database'], + self.onboarding_bronze_silver_params_map['bronze_dataflowspec_table']) + bronze_dataflowSpec_df.show(truncate=False) + silver_dataflowSpec_df = self.read_dataflowspec( + self.onboarding_bronze_silver_params_map['database'], + self.onboarding_bronze_silver_params_map['silver_dataflowspec_table']) + silver_dataflowSpec_df.show(truncate=False) + self.assertEqual(bronze_dataflowSpec_df.count(), 3) + self.assertEqual(silver_dataflowSpec_df.count(), 3) + def test_onboard_apply_changes_from_snapshot_positive(self): """Test for onboardDataflowspec.""" onboarding_params_map = copy.deepcopy(self.onboarding_bronze_silver_params_map) @@ -357,3 +373,71 @@ def test_onboard_apply_changes_from_snapshot_positive(self): self.onboarding_bronze_silver_params_map['database'], self.onboarding_bronze_silver_params_map['bronze_dataflowspec_table']) self.assertEqual(bronze_dataflowSpec_df.count(), 2) + + def test_onboard_apply_changes_from_snapshot_negative(self): + """Test for onboardDataflowspec.""" + onboarding_params_map = copy.deepcopy(self.onboarding_bronze_silver_params_map) + onboarding_params_map['env'] = 'it' + del onboarding_params_map["silver_dataflowspec_table"] + del onboarding_params_map["silver_dataflowspec_path"] + onboarding_params_map["onboarding_file_path"] = self.onboarding_apply_changes_from_snapshot_json__error_file + onboardDataFlowSpecs = OnboardDataflowspec(self.spark, onboarding_params_map, uc_enabled=True) + with self.assertRaises(Exception): + onboardDataFlowSpecs.onboard_bronze_dataflow_spec() + + def test_get_quarantine_details_with_partitions_and_properties(self): + """Test get_quarantine_details with partitions and properties.""" + onboarding_row = { + "bronze_quarantine_table_partitions": "partition_col", + "bronze_database_quarantine_it": "quarantine_db", + "bronze_quarantine_table": "quarantine_table", + "bronze_quarantine_table_path_it": "quarantine_path", + "bronze_quarantine_table_properties": MagicMock( + asDict=MagicMock(return_value={"property_key": "property_value"}) + ) + } + onboardDataFlowSpecs = OnboardDataflowspec(self.spark, self.onboarding_bronze_silver_params_map) + quarantine_target_details, quarantine_table_properties = onboardDataFlowSpecs.__get_quarantine_details( + "it", onboarding_row + ) + self.assertEqual(quarantine_target_details["database"], "quarantine_db") + self.assertEqual(quarantine_target_details["table"], "quarantine_table") + self.assertEqual(quarantine_target_details["partition_columns"], "partition_col") + self.assertEqual(quarantine_target_details["path"], "quarantine_path") + self.assertEqual(quarantine_table_properties, {"property_key": "property_value"}) + + def test_get_quarantine_details_without_partitions_and_properties(self): + """Test get_quarantine_details without partitions and properties.""" + onboarding_row = { + "bronze_database_quarantine_it": "quarantine_db", + "bronze_quarantine_table": "quarantine_table", + "bronze_quarantine_table_path_it": "quarantine_path" + } + onboardDataFlowSpecs = OnboardDataflowspec(self.spark, self.onboarding_bronze_silver_params_map) + quarantine_target_details, quarantine_table_properties = onboardDataFlowSpecs.__get_quarantine_details( + "it", onboarding_row) + self.assertEqual(quarantine_target_details["database"], "quarantine_db") + self.assertEqual(quarantine_target_details["table"], "quarantine_table") + self.assertEqual(quarantine_target_details["partition_columns"], "") + self.assertEqual(quarantine_target_details["path"], "quarantine_path") + self.assertEqual(quarantine_table_properties, {}) + + def test_get_quarantine_details_with_uc_enabled(self): + """Test get_quarantine_details with UC enabled.""" + onboarding_row = { + "bronze_database_quarantine_it": "quarantine_db", + "bronze_quarantine_table": "quarantine_table", + "bronze_quarantine_table_properties": MagicMock( + asDict=MagicMock(return_value={"property_key": "property_value"}) + ) + } + onboardDataFlowSpecs = OnboardDataflowspec( + self.spark, self.onboarding_bronze_silver_params_map, uc_enabled=True + ) + quarantine_target_details, quarantine_table_properties = onboardDataFlowSpecs.__get_quarantine_details( + "it", onboarding_row + ) + self.assertEqual(quarantine_target_details["database"], "quarantine_db") + self.assertEqual(quarantine_target_details["table"], "quarantine_table") + self.assertNotIn("path", quarantine_target_details) + self.assertEqual(quarantine_table_properties, {"property_key": "property_value"}) diff --git a/tests/utils.py b/tests/utils.py index 3249de8..b0adf32 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -31,6 +31,7 @@ def setUp(self): self.temp_delta_tables_path = tempfile.mkdtemp() self.onboarding_json_file = "tests/resources/onboarding.json" self.onboarding_json_v7_file = "tests/resources/onboarding_v0.0.7.json" + self.onboarding_json_v8_file = "tests/resources/onboarding_v0.0.8.json" self.onboarding_unsupported_file = "tests/resources/schema.ddl" self.onboarding_v2_json_file = "tests/resources/onboarding_v2.json" self.onboarding_without_ids_json_file = "tests/resources/onboarding_without_ids.json" @@ -44,6 +45,9 @@ def setUp(self): self.onboarding_apply_changes_from_snapshot_json_file = ( "tests/resources/onboarding_applychanges_from_snapshot.json" ) + self.onboarding_apply_changes_from_snapshot_json__error_file = ( + "tests/resources/onboarding_applychanges_from_snapshot_error.json" + ) self.deltaPipelinesMetaStoreOps.drop_database("ravi_dlt_demo") self.deltaPipelinesMetaStoreOps.create_database("ravi_dlt_demo", "Unittest") self.onboarding_bronze_silver_params_map = { From 7b71fc9edf2e0380a79c0933fedfc29b58a21771 Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Mon, 7 Oct 2024 11:32:23 -0400 Subject: [PATCH 35/59] job workflow clean-up --- integration_tests/run_integration_tests.py | 213 ++++++++------------- 1 file changed, 84 insertions(+), 129 deletions(-) diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index a69870a..afb90eb 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -194,36 +194,31 @@ def _my_username(self, ws): _me = ws.current_user.me() return _me.user_name - def create_dlt_meta_pipeline(self, - pipeline_name: str, - layer: str, - group: str, - target_schema: str, - runner_conf: DLTMetaRunnerConf - ): + def create_dlt_meta_pipeline( + self, + pipeline_name: str, + layer: str, + group: str, + target_schema: str, + runner_conf: DLTMetaRunnerConf, + ) -> str: """ Create a DLT pipeline. Parameters: ---------- - pipeline_name : str - The name of the pipeline. - layer : str - The layer of the pipeline. - target_schema : str - The target schema of the pipeline. - runner_conf : DLTMetaRunnerConf - The runner configuration. + pipeline_name : str = The name of the pipeline. + layer : str = The layer of the pipeline. + target_schema : str = The target schema of the pipeline. + runner_conf : DLTMetaRunnerConf = The runner configuration. Returns: ------- - str - The ID of the created pipeline. + str - The ID of the created pipeline. Raises: ------ - Exception - If the pipeline creation fails. + Exception - If the pipeline creation fails. """ configuration = { "layer": layer, @@ -231,42 +226,25 @@ def create_dlt_meta_pipeline(self, "dlt_meta_whl": runner_conf.remote_whl_path, } created = None - if runner_conf.uc_catalog_name: - configuration[f"{layer}.dataflowspecTable"] = ( - f"{runner_conf.uc_catalog_name}.{runner_conf.dlt_meta_schema}.{layer}_dataflowspec_cdc" - ) - created = self.ws.pipelines.create( - catalog=runner_conf.uc_catalog_name, - name=pipeline_name, - serverless=True, - configuration=configuration, - libraries=[ - PipelineLibrary( - notebook=NotebookLibrary( - path=f"{runner_conf.runners_nb_path}/runners/init_dlt_meta_pipeline" - ) - ) - ], - target=target_schema - ) - else: - configuration[f"{layer}.dataflowspecTable"] = ( - f"{runner_conf.dlt_meta_schema}.{layer}_dataflowspec_cdc" - ) - created = self.ws.pipelines.create( - name=pipeline_name, - serverless=True, - channel="PREVIEW", - configuration=configuration, - libraries=[ - PipelineLibrary( - notebook=NotebookLibrary( - path=f"{runner_conf.runners_nb_path}/runners/init_dlt_meta_pipeline" - ) + + configuration[f"{layer}.dataflowspecTable"] = ( + f"{runner_conf.uc_catalog_name}.{runner_conf.dlt_meta_schema}.{layer}_dataflowspec_cdc" + ) + created = self.ws.pipelines.create( + catalog=runner_conf.uc_catalog_name, + name=pipeline_name, + serverless=True, + configuration=configuration, + libraries=[ + PipelineLibrary( + notebook=NotebookLibrary( + path=f"{runner_conf.runners_nb_path}/runners/init_dlt_meta_pipeline" ) - ], - target=target_schema - ) + ) + ], + target=target_schema, + ) + if created is None: raise Exception("Pipeline creation failed") return created.pipeline_id @@ -277,25 +255,23 @@ def create_cloudfiles_workflow_spec(self, runner_conf: DLTMetaRunnerConf): Parameters: ---------- - runner_conf : DLTMetaRunnerConf - The runner configuration. + runner_conf : DLTMetaRunnerConf = The runner configuration. Returns: ------- - Job - The created job. + Job - The created job. Raises: ------ - Exception - If the job creation fails. + Exception - If the job creation fails. """ dltmeta_environments = [ jobs.JobEnvironment( environment_key="dl_meta_int_env", - spec=compute.Environment(client=f"dlt_meta_int_test_{__version__}", - dependencies=[runner_conf.remote_whl_path] - ) + spec=compute.Environment( + client=f"dlt_meta_int_test_{__version__}", + dependencies=[runner_conf.remote_whl_path], + ), ) ] return self.ws.jobs.create( @@ -313,8 +289,7 @@ def create_cloudfiles_workflow_spec(self, runner_conf: DLTMetaRunnerConf): named_parameters={ "onboard_layer": "bronze_silver", "database": f"{runner_conf.uc_catalog_name}.{runner_conf.dlt_meta_schema}", - "onboarding_file_path": - f"{runner_conf.uc_volume_path}/{self.base_dir}/conf/onboarding.json", + "onboarding_file_path": f"{runner_conf.uc_volume_path}/{self.base_dir}/conf/onboarding.json", "silver_dataflowspec_table": "silver_dataflowspec_cdc", "silver_dataflowspec_path": f"{runner_conf.uc_volume_path}/data/dlt_spec/silver", "bronze_dataflowspec_table": "bronze_dataflowspec_cdc", @@ -323,13 +298,15 @@ def create_cloudfiles_workflow_spec(self, runner_conf: DLTMetaRunnerConf): "bronze_dataflowspec_path": f"{runner_conf.uc_volume_path}/data/dlt_spec/bronze", "overwrite": "True", "env": runner_conf.env, - "uc_enabled": "True" + "uc_enabled": "True", }, - ) + ), ), jobs.Task( task_key="bronze_dlt_pipeline", - depends_on=[jobs.TaskDependency(task_key="setup_dlt_meta_pipeline_spec")], + depends_on=[ + jobs.TaskDependency(task_key="setup_dlt_meta_pipeline_spec") + ], pipeline_task=jobs.PipelineTask( pipeline_id=runner_conf.bronze_pipeline_id ), @@ -346,16 +323,15 @@ def create_cloudfiles_workflow_spec(self, runner_conf: DLTMetaRunnerConf): named_parameters={ "onboard_layer": "bronze", "database": f"{runner_conf.uc_catalog_name}.{runner_conf.dlt_meta_schema}", - "onboarding_file_path": - f"{runner_conf.uc_volume_path}/{self.base_dir}/conf/onboarding_A2.json", + "onboarding_file_path": f"{runner_conf.uc_volume_path}/{self.base_dir}/conf/onboarding_A2.json", "bronze_dataflowspec_table": "bronze_dataflowspec_cdc", "import_author": "Ravi", "version": "v1", "overwrite": "False", "env": runner_conf.env, - "uc_enabled": "True" + "uc_enabled": "True", }, - ) + ), ), jobs.Task( task_key="bronze_A2_dlt_pipeline", @@ -369,7 +345,7 @@ def create_cloudfiles_workflow_spec(self, runner_conf: DLTMetaRunnerConf): depends_on=[jobs.TaskDependency(task_key="bronze_A2_dlt_pipeline")], pipeline_task=jobs.PipelineTask( pipeline_id=runner_conf.silver_pipeline_id - ) + ), ), jobs.Task( task_key="validate_results", @@ -383,12 +359,11 @@ def create_cloudfiles_workflow_spec(self, runner_conf: DLTMetaRunnerConf): "bronze_schema": f"{runner_conf.bronze_schema}", "silver_schema": f"{runner_conf.silver_schema}", "output_file_path": f"/Workspace{runner_conf.test_output_file_path}", - "run_id": runner_conf.run_id - } - ) - + "run_id": runner_conf.run_id, + }, + ), ), - ] + ], ) def create_eventhub_workflow_spec(self, runner_conf: DLTMetaRunnerConf): @@ -784,10 +759,10 @@ def upload_files_to_databricks(self, runner_conf: DLTMetaRunnerConf): # Upload all the JSONs in the conf directory, that is the generated onboarding JSONs and # the DQE JSONS for root, dirs, files in os.walk(f"{runner_conf.int_tests_dir}/conf"): - for file in files: - if file.endswith(".json"): - with open(os.path.join(root, file), "rb") as content: - self.ws.files.upload( + for file in files: + if file.endswith(".json"): + with open(os.path.join(root, file), "rb") as content: + self.ws.files.upload( file_path=f"{runner_conf.uc_volume_path}{root}/{file}", contents=content, overwrite=True, @@ -822,77 +797,59 @@ def init_dltmeta_runner_conf(self, runner_conf: DLTMetaRunnerConf): self.generate_onboarding_file(runner_conf) self.upload_files_to_databricks(runner_conf) - def create_cluster(self, runner_conf: DLTMetaRunnerConf): - print("Cluster creation started...") - if runner_conf.uc_catalog_name: - mode = compute.DataSecurityMode.SINGLE_USER - spark_confs = {} - else: - mode = compute.DataSecurityMode.NONE - spark_confs = {} - clstr = self.ws.clusters.create( - cluster_name=f"dlt-meta-onboarding-cluster-{runner_conf.run_id}", - spark_version=runner_conf.dbr_version, - node_type_id=runner_conf.node_type_id, - driver_node_type_id=runner_conf.node_type_id, - num_workers=2, - spark_conf=spark_confs, - autotermination_minutes=30, - spark_env_vars={ - "PYSPARK_PYTHON": "/databricks/python3/bin/python3", - "WSFS_ENABLE": "false" - }, - data_security_mode=mode - ).result() - print(f"Cluster creation finished. clusters={clstr}") - print(f"Cluster creation finished. cluster_id={clstr.cluster_id}") - print(f"host: {self.ws.config.host}, workspace_id: {self.ws.get_workspace_id()}") - runner_conf.cluster_id = clstr.cluster_id - webbrowser.open(f"{self.ws.config.host}/compute/clusters/{clstr.cluster_id}?o={self.ws.get_workspace_id()}") - - def download_test_results(self, runner_conf: DLTMetaRunnerConf): - ws_output_file = self.ws.workspace.download(runner_conf.test_output_file_path) - with open(f"integration_test_output_{runner_conf.run_id}.csv", "wb") as output_file: - output_file.write(ws_output_file.read()) - def create_bronze_silver_dlt(self, runner_conf: DLTMetaRunnerConf): runner_conf.bronze_pipeline_id = self.create_dlt_meta_pipeline( f"dlt-meta-bronze-{runner_conf.run_id}", "bronze", "A1", runner_conf.bronze_schema, - runner_conf) + runner_conf, + ) - if runner_conf.source and runner_conf.source == "cloudfiles": + if runner_conf.source == "cloudfiles": runner_conf.bronze_pipeline_A2_id = self.create_dlt_meta_pipeline( f"dlt-meta-bronze-A2-{runner_conf.run_id}", "bronze", "A2", runner_conf.bronze_schema, - runner_conf) + runner_conf, + ) runner_conf.silver_pipeline_id = self.create_dlt_meta_pipeline( f"dlt-meta-silver-{runner_conf.run_id}", "silver", "A1", runner_conf.silver_schema, - runner_conf) + runner_conf, + ) def launch_workflow(self, runner_conf: DLTMetaRunnerConf): - if runner_conf.source == "cloudfiles": - created_job = self.create_cloudfiles_workflow_spec(runner_conf) - elif runner_conf.source == "eventhub": - created_job = self.create_eventhub_workflow_spec(runner_conf) - elif runner_conf.source == "kafka": - created_job = self.create_kafka_workflow_spec(runner_conf) + + match runner_conf.source: + case "cloudfiles": + created_job = self.create_cloudfiles_workflow_spec(runner_conf) + case "eventhub": + created_job = self.create_eventhub_workflow_spec(runner_conf) + case "kafka": + created_job = self.create_kafka_workflow_spec(runner_conf) + runner_conf.job_id = created_job.job_id print(f"Job created successfully. job_id={created_job.job_id}, started run...") - webbrowser.open(f"{self.ws.config.host}/jobs/{created_job.job_id}?o={self.ws.get_workspace_id()}") + webbrowser.open( + f"{self.ws.config.host}/jobs/{created_job.job_id}?o={self.ws.get_workspace_id()}" + ) print(f"Waiting for job to complete. job_id={created_job.job_id}") run_by_id = self.ws.jobs.run_now(job_id=created_job.job_id).result() print(f"Job run finished. run_id={run_by_id}") return created_job + def download_test_results(self, runner_conf: DLTMetaRunnerConf): + ws_output_file = self.ws.workspace.download(runner_conf.test_output_file_path) + with open( + f"integration_test_output_{runner_conf.run_id}.csv", "wb" + ) as output_file: + output_file.write(ws_output_file.read()) + def open_job_url(self, runner_conf, created_job): runner_conf.job_id = created_job.job_id url = f"{self.ws.config.host}/jobs/{created_job.job_id}?o={self.ws.get_workspace_id()}" @@ -910,8 +867,6 @@ def clean_up(self, runner_conf: DLTMetaRunnerConf): self.ws.pipelines.delete(runner_conf.silver_pipeline_id) if runner_conf.cluster_id: self.ws.clusters.delete(runner_conf.cluster_id) - if runner_conf.dbfs_tmp_path: - self.ws.dbfs.delete(runner_conf.dbfs_tmp_path, recursive=True) if runner_conf.uc_catalog_name: test_schema_list = [runner_conf.dlt_meta_schema, runner_conf.bronze_schema, runner_conf.silver_schema] schema_list = self.ws.schemas.list(catalog_name=runner_conf.uc_catalog_name) @@ -938,9 +893,9 @@ def clean_up(self, runner_conf: DLTMetaRunnerConf): def run(self, runner_conf: DLTMetaRunnerConf): self.init_dltmeta_runner_conf(runner_conf) - exit() self.create_bronze_silver_dlt(runner_conf) self.launch_workflow(runner_conf) + exit() self.download_test_results(runner_conf) # try: From 5ee1843132117aaa3131408640083a7a127dbb2b Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Mon, 7 Oct 2024 14:17:46 -0400 Subject: [PATCH 36/59] cloud files testing, added back accidently removed json --- .../conf/silver_transformations.json | 27 +++++++++++++++++++ integration_tests/run_integration_tests.py | 1 - 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 integration_tests/conf/silver_transformations.json diff --git a/integration_tests/conf/silver_transformations.json b/integration_tests/conf/silver_transformations.json new file mode 100644 index 0000000..ad9da77 --- /dev/null +++ b/integration_tests/conf/silver_transformations.json @@ -0,0 +1,27 @@ +[ + { + "target_table": "customers", + "select_exp": [ + "address", + "email", + "firstname", + "id", + "lastname", + "operation_date", + "operation", + "_rescued_data" + ] + }, + { + "target_table": "transactions", + "select_exp": [ + "id", + "customer_id", + "amount", + "item_count", + "operation_date", + "operation", + "_rescued_data" + ] + } + ] \ No newline at end of file diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index afb90eb..68c7db9 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -895,7 +895,6 @@ def run(self, runner_conf: DLTMetaRunnerConf): self.init_dltmeta_runner_conf(runner_conf) self.create_bronze_silver_dlt(runner_conf) self.launch_workflow(runner_conf) - exit() self.download_test_results(runner_conf) # try: From e70f85de97f3fc9b6f3374b363105825d21c9fe8 Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Mon, 7 Oct 2024 14:43:12 -0400 Subject: [PATCH 37/59] cloud files integration test works --- integration_tests/run_integration_tests.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index 68c7db9..f34032e 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -238,7 +238,7 @@ def create_dlt_meta_pipeline( libraries=[ PipelineLibrary( notebook=NotebookLibrary( - path=f"{runner_conf.runners_nb_path}/runners/init_dlt_meta_pipeline" + path=f"{runner_conf.runners_nb_path}/runners/init_dlt_meta_pipeline.py" ) ) ], @@ -352,7 +352,7 @@ def create_cloudfiles_workflow_spec(self, runner_conf: DLTMetaRunnerConf): description="test", depends_on=[jobs.TaskDependency(task_key="silver_dlt_pipeline")], notebook_task=jobs.NotebookTask( - notebook_path=f"{runner_conf.runners_nb_path}/runners/validate", + notebook_path=f"{runner_conf.runners_nb_path}/runners/validate.py", base_parameters={ "uc_enabled": "True", "uc_catalog_name": f"{runner_conf.uc_catalog_name}", @@ -409,7 +409,7 @@ def create_eventhub_workflow_spec(self, runner_conf: DLTMetaRunnerConf): depends_on=[jobs.TaskDependency(task_key="setup_dlt_meta_pipeline_spec")], existing_cluster_id=runner_conf.cluster_id, notebook_task=jobs.NotebookTask( - notebook_path=f"{runner_conf.runners_nb_path}/runners/publish_events", + notebook_path=f"{runner_conf.runners_nb_path}/runners/publish_events.py", base_parameters={ "eventhub_name": self.args["eventhub_name"], "eventhub_name_append_flow": self.args["eventhub_name_append_flow"], @@ -435,7 +435,7 @@ def create_eventhub_workflow_spec(self, runner_conf: DLTMetaRunnerConf): description="test", depends_on=[jobs.TaskDependency(task_key="bronze_dlt_pipeline")], notebook_task=jobs.NotebookTask( - notebook_path=f"{runner_conf.runners_nb_path}/runners/validate", + notebook_path=f"{runner_conf.runners_nb_path}/runners/validate.py", base_parameters={ "run_id": runner_conf.run_id, "uc_enabled": "True", @@ -493,7 +493,7 @@ def create_kafka_workflow_spec(self, runner_conf: DLTMetaRunnerConf): description="test", depends_on=[jobs.TaskDependency(task_key="setup_dlt_meta_pipeline_spec")], notebook_task=jobs.NotebookTask( - notebook_path=f"{runner_conf.runners_nb_path}/runners/publish_events", + notebook_path=f"{runner_conf.runners_nb_path}/runners/publish_events.py", base_parameters={ "kafka_topic": self.args["kafka_topic_name"], "kafka_broker": self.args["kafka_broker"], @@ -513,7 +513,7 @@ def create_kafka_workflow_spec(self, runner_conf: DLTMetaRunnerConf): description="test", depends_on=[jobs.TaskDependency(task_key="bronze_dlt_pipeline")], notebook_task=jobs.NotebookTask( - notebook_path=f"{runner_conf.runners_nb_path}/runners/validate", + notebook_path=f"{runner_conf.runners_nb_path}/runners/validate.py", base_parameters={ "run_id": runner_conf.run_id, "uc_enabled": "True" , From 9e77b1fc51b60feca2b7180fe24aff46161c49d4 Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Mon, 7 Oct 2024 17:38:38 -0400 Subject: [PATCH 38/59] formatting --- integration_tests/run_integration_tests.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index f34032e..20e4141 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -371,9 +371,10 @@ def create_eventhub_workflow_spec(self, runner_conf: DLTMetaRunnerConf): dltmeta_environments = [ jobs.JobEnvironment( environment_key="dl_meta_int_env", - spec=compute.Environment(client=f"dlt_meta_int_test_{__version__}", - dependencies=[runner_conf.remote_whl_path] - ) + spec=compute.Environment( + client=f"dlt_meta_int_test_{__version__}", + dependencies=[runner_conf.remote_whl_path], + ), ) ] return self.ws.jobs.create( From e78c751ce8f8127fc8c6fe331b41bddf87f63e67 Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Tue, 8 Oct 2024 16:07:09 -0400 Subject: [PATCH 39/59] continuing code simplication between 3 integration run types --- integration_tests/README.md | 4 +- .../conf/eventhub-onboarding.template | 8 +- .../conf/kafka-onboarding.template | 2 +- integration_tests/run_integration_tests.py | 768 ++++++------------ 4 files changed, 268 insertions(+), 514 deletions(-) diff --git a/integration_tests/README.md b/integration_tests/README.md index 97142d8..4d7afc7 100644 --- a/integration_tests/README.md +++ b/integration_tests/README.md @@ -62,14 +62,14 @@ - 9c. Run the command for kafka ```commandline - python integration_tests/run_integration_tests.py --uc_catalog_name=<> --source=kafka --kafka_topic_name=dlt-meta-integration-test --kafka_broker=host:9092 --cloud_provider_name=aws --profile=DEFAULT + python integration_tests/run_integration_tests.py --uc_catalog_name=<> --source=kafka --kafka_topic=dlt-meta-integration-test --kafka_broker=host:9092 --cloud_provider_name=aws --profile=DEFAULT ``` - - For kafka integration tests, the following are the prerequisites: 1. Needs kafka instance running - - Following are the mandatory arguments for running EventHubs integration test - 1. Provide your kafka topic name : --kafka_topic_name + 1. Provide your kafka topic name : --kafka_topic 2. Provide kafka_broker : --kafka_broker 10. Once finished integration output file will be copied locally to diff --git a/integration_tests/conf/eventhub-onboarding.template b/integration_tests/conf/eventhub-onboarding.template index 48f4b28..113c13f 100644 --- a/integration_tests/conf/eventhub-onboarding.template +++ b/integration_tests/conf/eventhub-onboarding.template @@ -12,7 +12,7 @@ "eventhub.secretsScopeName": "{eventhub_secrets_scope_name}", "kafka.sasl.mechanism": "PLAIN", "kafka.security.protocol": "SASL_SSL", - "eventhub.namespace": "{eventhub_nmspace}", + "eventhub.namespace": "{eventhub_namespace}", "eventhub.port": "{eventhub_port}" }, "bronze_reader_options": { @@ -42,7 +42,7 @@ "eventhub.secretsScopeName": "{eventhub_secrets_scope_name}", "kafka.sasl.mechanism": "PLAIN", "kafka.security.protocol": "SASL_SSL", - "eventhub.namespace": "{eventhub_nmspace}", + "eventhub.namespace": "{eventhub_namespace}", "eventhub.port": "{eventhub_port}" }, "reader_options": { @@ -53,7 +53,7 @@ "kafka.session.timeout.ms": "60000" }, "once": false - } - ] + } + ] } ] \ No newline at end of file diff --git a/integration_tests/conf/kafka-onboarding.template b/integration_tests/conf/kafka-onboarding.template index 6eb2249..8dc2bba 100644 --- a/integration_tests/conf/kafka-onboarding.template +++ b/integration_tests/conf/kafka-onboarding.template @@ -7,7 +7,7 @@ "source_details": { "source_schema_path": "{uc_volume_path}/integration_tests/resources/eventhub_iot_schema.ddl", "subscribe": "{kafka_topic}", - "kafka.bootstrap.servers": "{kafka_bootstrap_servers}" + "kafka.bootstrap.servers": "{kafka_broker}" }, "bronze_reader_options": { "startingOffsets": "earliest", diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index 20e4141..ee69fd3 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -5,6 +5,7 @@ import argparse import os import webbrowser +import traceback from dataclasses import dataclass from databricks.sdk import WorkspaceClient from databricks.sdk.service.pipelines import PipelineLibrary, NotebookLibrary @@ -13,23 +14,11 @@ from databricks.sdk.service.workspace import ImportFormat, Language from databricks.sdk.service.catalog import SchemasAPI, VolumeInfo, VolumeType from src.install import WorkspaceInstaller - import json # Dictionary mapping cloud providers to node types cloud_node_type_id_dict = {"aws": "i3.xlarge", "azure": "Standard_D3_v2", "gcp": "n1-highmem-4"} -DLT_META_RUNNER_NOTEBOOK = """ -# Databricks notebook source -# MAGIC %pip install {remote_wheel} -# dbutils.library.restartPython() -# COMMAND ---------- -layer = spark.conf.get("layer", None) -from src.dataflow_pipeline import DataflowPipeline -DataflowPipeline.invoke_dlt_pipeline(spark, layer) -""" - - @dataclass class DLTMetaRunnerConf: """ @@ -45,8 +34,6 @@ class DLTMetaRunnerConf: The name of the unified catalog to use for the test run. onboarding_file_path : str, optional The path to the onboarding file to use for the test run. - dbfs_tmp_path : str, optional - The temporary DBFS path to use for the test run. int_tests_dir : str, optional The directory containing the integration tests. dlt_meta_schema : str, optional @@ -77,18 +64,12 @@ class DLTMetaRunnerConf: The path to the unified volume to use for the test run. uc_target_whl_path : str, optional The path to the unified catalog target whl file to use for the test run. - dbfs_whl_path : str, optional - The path to the DBFS whl file to use for the test run. node_type_id : str, optional The node type ID to use for the test run. - dbr_version : str, optional - The Databricks runtime version to use for the test run. bronze_pipeline_id : str, optional The ID of the bronze pipeline to use for the test run. silver_pipeline_id : str, optional The ID of the silver pipeline to use for the test run. - cluster_id : str, optional - The ID of the cluster to use for the test run. job_id : str, optional The ID of the job to use for the test run. """ @@ -99,7 +80,8 @@ class DLTMetaRunnerConf: uc_volume_name: str = "dlt_meta_files" onboarding_file_path: str = "integration_tests/conf/onboarding.json" onboarding_A2_file_path: str = "integration_tests/conf/onboarding_A2.json" - onboarding_fanout_file_path: str = "integration_tests/conf/onboarding.json" + #onboarding_fanout_file_path: str = "integration_tests/conf/onboarding.json" + #onboarding_fanout_templates: str = None int_tests_dir: str = "integration_tests" dlt_meta_schema: str = None bronze_schema: str = None @@ -107,29 +89,40 @@ class DLTMetaRunnerConf: runners_nb_path: str = None runners_full_local_path: str = None source: str = None - cloudfiles_template: str = "integration_tests/conf/cloudfiles-onboarding.template" - cloudfiles_A2_template: str = "integration_tests/conf/cloudfiles-onboarding_A2.template" - #onboarding_fanout_templates: str = None - eventhub_template: str = "integration_tests/conf/eventhub-onboarding.template", - eventhub_input_data: str = None - eventhub_append_flow_input_data: str = None - kafka_template: str = "integration_tests/conf/kafka-onboarding.template" env: str = "it" whl_path: str = None volume_info: VolumeInfo = None uc_volume_path: str = None uc_target_whl_path: str = None remote_whl_path: str = None - dbfs_whl_path: str = None node_type_id: str = None - dbr_version: str = None bronze_pipeline_id: str = None bronze_pipeline_A2_id: str = None silver_pipeline_id: str = None - cluster_id: str = None job_id: str = None test_output_file_path: str = None + # cloudfiles info + cloudfiles_template: str = "integration_tests/conf/cloudfiles-onboarding.template" + cloudfiles_A2_template: str = "integration_tests/conf/cloudfiles-onboarding_A2.template" + + # eventhub info + eventhub_template: str = "integration_tests/conf/eventhub-onboarding.template", + eventhub_input_data: str = None + eventhub_append_flow_input_data: str = None + eventhub_name: str = None + eventhub_name_append_flow: str = None + eventhub_accesskey_name: str = None + eventhub_accesskey_secret_name: str = None + eventhub_secrets_scope_name: str = None + eventhub_namespace: str = None + eventhub_port: str = None + + # kafka info + kafka_template: str = "integration_tests/conf/kafka-onboarding.template" + kafka_topic: str = None + kafka_broker: str = None + class DLTMETARunner: """ @@ -170,6 +163,18 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: f"/Users/{self.wsi._my_username}/dlt_meta_int_tests/" f"{run_id}/integration-test-output.csv" ), + # kafka provided args + kafka_topic = self.args["kafka_topic"], + kafka_broker = self.args["kafka_broker"], + # eventhub provided args + eventhub_name = self.args["eventhub_name"], + eventhub_name_append_flow = self.args["eventhub_name_append_flow"], + eventhub_accesskey_name = self.args["eventhub_accesskey_name"], + eventhub_accesskey_secret_name = self.args["eventhub_accesskey_secret_name"], + eventhub_secrets_scope_name = self.args["eventhub_secrets_scope_name"], + eventhub_namespace = self.args["eventhub_namespace"], + eventhub_port = self.args["eventhub_port"], + ) # Set the proper directory location for the notebooks that need to be uploaded to run and @@ -249,22 +254,9 @@ def create_dlt_meta_pipeline( raise Exception("Pipeline creation failed") return created.pipeline_id - def create_cloudfiles_workflow_spec(self, runner_conf: DLTMetaRunnerConf): - """ - Create the CloudFiles workflow specification. + def create_workflow_spec(self, runner_conf: DLTMetaRunnerConf): + """Create the Databricks Workflow Job given the DLT Meta configuration specs""" - Parameters: - ---------- - runner_conf : DLTMetaRunnerConf = The runner configuration. - - Returns: - ------- - Job - The created job. - - Raises: - ------ - Exception - If the job creation fails. - """ dltmeta_environments = [ jobs.JobEnvironment( environment_key="dl_meta_int_env", @@ -274,471 +266,248 @@ def create_cloudfiles_workflow_spec(self, runner_conf: DLTMetaRunnerConf): ), ) ] - return self.ws.jobs.create( - name=f"dlt-meta-{runner_conf.run_id}", - environments=dltmeta_environments, - tasks=[ - jobs.Task( - task_key="setup_dlt_meta_pipeline_spec", - environment_key="dl_meta_int_env", - description="test", - timeout_seconds=0, - python_wheel_task=jobs.PythonWheelTask( - package_name="dlt_meta", - entry_point="run", - named_parameters={ - "onboard_layer": "bronze_silver", - "database": f"{runner_conf.uc_catalog_name}.{runner_conf.dlt_meta_schema}", - "onboarding_file_path": f"{runner_conf.uc_volume_path}/{self.base_dir}/conf/onboarding.json", - "silver_dataflowspec_table": "silver_dataflowspec_cdc", - "silver_dataflowspec_path": f"{runner_conf.uc_volume_path}/data/dlt_spec/silver", - "bronze_dataflowspec_table": "bronze_dataflowspec_cdc", - "import_author": "Ravi", - "version": "v1", - "bronze_dataflowspec_path": f"{runner_conf.uc_volume_path}/data/dlt_spec/bronze", - "overwrite": "True", - "env": runner_conf.env, - "uc_enabled": "True", - }, - ), + + tasks = [ + jobs.Task( + task_key="setup_dlt_meta_pipeline_spec", + environment_key="dl_meta_int_env", + description="test", + timeout_seconds=0, + python_wheel_task=jobs.PythonWheelTask( + package_name="dlt_meta", + entry_point="run", + named_parameters={ + "onboard_layer": ( + "bronze_silver" + if runner_conf.source == "cloudfiles" + else "bronze" + ), + "database": f"{runner_conf.uc_catalog_name}.{runner_conf.dlt_meta_schema}", + "onboarding_file_path": f"{runner_conf.uc_volume_path}/{self.base_dir}/conf/onboarding.json", + "silver_dataflowspec_table": "silver_dataflowspec_cdc", + "silver_dataflowspec_path": f"{runner_conf.uc_volume_path}/data/dlt_spec/silver", + "bronze_dataflowspec_table": "bronze_dataflowspec_cdc", + "import_author": "Ravi", + "version": "v1", + "bronze_dataflowspec_path": f"{runner_conf.uc_volume_path}/data/dlt_spec/bronze", + "overwrite": "True", + "env": runner_conf.env, + "uc_enabled": "True", + }, ), - jobs.Task( - task_key="bronze_dlt_pipeline", - depends_on=[ - jobs.TaskDependency(task_key="setup_dlt_meta_pipeline_spec") - ], - pipeline_task=jobs.PipelineTask( - pipeline_id=runner_conf.bronze_pipeline_id - ), + ), + jobs.Task( + task_key="bronze_dlt_pipeline", + depends_on=[ + jobs.TaskDependency( + task_key=( + "setup_dlt_meta_pipeline_spec" + if runner_conf.source == "cloudfiles" + else "publish_events" + ) + ) + ], + pipeline_task=jobs.PipelineTask( + pipeline_id=runner_conf.bronze_pipeline_id ), - jobs.Task( - task_key="onboard_spec_A2", - depends_on=[jobs.TaskDependency(task_key="bronze_dlt_pipeline")], - description="test", - environment_key="dl_meta_int_env", - timeout_seconds=0, - python_wheel_task=jobs.PythonWheelTask( - package_name="dlt_meta", - entry_point="run", - named_parameters={ - "onboard_layer": "bronze", - "database": f"{runner_conf.uc_catalog_name}.{runner_conf.dlt_meta_schema}", - "onboarding_file_path": f"{runner_conf.uc_volume_path}/{self.base_dir}/conf/onboarding_A2.json", - "bronze_dataflowspec_table": "bronze_dataflowspec_cdc", - "import_author": "Ravi", - "version": "v1", - "overwrite": "False", - "env": runner_conf.env, - "uc_enabled": "True", - }, - ), + ), + jobs.Task( + task_key="validate_results", + description="test", + depends_on=[ + jobs.TaskDependency( + task_key=( + "silver_dlt_pipeline" + if runner_conf.source == "cloudfiles" + else "bronze_dlt_pipeline" + ) + ) + ], + notebook_task=jobs.NotebookTask( + notebook_path=f"{runner_conf.runners_nb_path}/runners/validate.py", + base_parameters={ + "uc_enabled": "True", + "uc_catalog_name": f"{runner_conf.uc_catalog_name}", + "bronze_schema": f"{runner_conf.bronze_schema}", + "silver_schema": ( + f"{runner_conf.silver_schema}" + if runner_conf.source == "cloudfiles" + else None + ), + "output_file_path": f"/Workspace{runner_conf.test_output_file_path}", + "run_id": runner_conf.run_id, + }, ), - jobs.Task( - task_key="bronze_A2_dlt_pipeline", - depends_on=[jobs.TaskDependency(task_key="onboard_spec_A2")], - pipeline_task=jobs.PipelineTask( - pipeline_id=runner_conf.bronze_pipeline_A2_id + ), + ] + + if runner_conf.source == "cloudfiles": + tasks.extend( + [ + jobs.Task( + task_key="onboard_spec_A2", + depends_on=[ + jobs.TaskDependency(task_key="bronze_dlt_pipeline") + ], + description="test", + environment_key="dl_meta_int_env", + timeout_seconds=0, + python_wheel_task=jobs.PythonWheelTask( + package_name="dlt_meta", + entry_point="run", + named_parameters={ + "onboard_layer": "bronze", + "database": f"{runner_conf.uc_catalog_name}.{runner_conf.dlt_meta_schema}", + "onboarding_file_path": f"{runner_conf.uc_volume_path}/{self.base_dir}/conf/onboarding_A2.json", + "bronze_dataflowspec_table": "bronze_dataflowspec_cdc", + "import_author": "Ravi", + "version": "v1", + "overwrite": "False", + "env": runner_conf.env, + "uc_enabled": "True", + }, + ), ), - ), - jobs.Task( - task_key="silver_dlt_pipeline", - depends_on=[jobs.TaskDependency(task_key="bronze_A2_dlt_pipeline")], - pipeline_task=jobs.PipelineTask( - pipeline_id=runner_conf.silver_pipeline_id + jobs.Task( + task_key="bronze_A2_dlt_pipeline", + depends_on=[jobs.TaskDependency(task_key="onboard_spec_A2")], + pipeline_task=jobs.PipelineTask( + pipeline_id=runner_conf.bronze_pipeline_A2_id + ), ), - ), - jobs.Task( - task_key="validate_results", - description="test", - depends_on=[jobs.TaskDependency(task_key="silver_dlt_pipeline")], - notebook_task=jobs.NotebookTask( - notebook_path=f"{runner_conf.runners_nb_path}/runners/validate.py", - base_parameters={ - "uc_enabled": "True", - "uc_catalog_name": f"{runner_conf.uc_catalog_name}", - "bronze_schema": f"{runner_conf.bronze_schema}", - "silver_schema": f"{runner_conf.silver_schema}", - "output_file_path": f"/Workspace{runner_conf.test_output_file_path}", - "run_id": runner_conf.run_id, - }, + jobs.Task( + task_key="silver_dlt_pipeline", + depends_on=[ + jobs.TaskDependency(task_key="bronze_A2_dlt_pipeline") + ], + pipeline_task=jobs.PipelineTask( + pipeline_id=runner_conf.silver_pipeline_id + ), ), - ), - ], - ) - - def create_eventhub_workflow_spec(self, runner_conf: DLTMetaRunnerConf): - """Create Job specification.""" - dltmeta_environments = [ - jobs.JobEnvironment( - environment_key="dl_meta_int_env", - spec=compute.Environment( - client=f"dlt_meta_int_test_{__version__}", - dependencies=[runner_conf.remote_whl_path], - ), + ] ) - ] - return self.ws.jobs.create( - name=f"dlt-meta-{runner_conf.run_id}", - environments=dltmeta_environments, - tasks=[ - jobs.Task( - task_key="setup_dlt_meta_pipeline_spec", - description="setup_dlt_meta_pipeline_spec", - environment_key="dl_meta_int_env", - timeout_seconds=0, - python_wheel_task=jobs.PythonWheelTask( - package_name="dlt_meta", - entry_point="run", - named_parameters={ - "onboard_layer": "bronze", - "database": f"{runner_conf.uc_catalog_name}.{runner_conf.dlt_meta_schema}", - "onboarding_file_path": - f"{runner_conf.uc_volume_path}/{self.base_dir}/conf/onboarding.json", - "silver_dataflowspec_table": "silver_dataflowspec_cdc", - "bronze_dataflowspec_table": "bronze_dataflowspec_cdc", - "import_author": "Ravi", - "version": "v1", - "overwrite": "True", - "env": runner_conf.env, - "uc_enabled": "True" - } - ) - ), + else: + match runner_conf.source: + case "eventhub": + base_parameters = { + "eventhub_name": runner_conf.eventhub_name, + "eventhub_name_append_flow": runner_conf.eventhub_name_append_flow, + "eventhub_namespace": runner_conf.eventhub_namespace, + "eventhub_secrets_scope_name": runner_conf.eventhub_secrets_scope_name, + "eventhub_accesskey_name": runner_conf.eventhub_accesskey_name, + "eventhub_input_data": f"/{runner_conf.uc_volume_path}/{self.base_dir}/resources/data/iot/iot.json", + "eventhub_append_flow_input_data": f"/{runner_conf.uc_volume_path}/{self.base_dir}/resources/data/iot_eventhub_af/iot.json", + } + case "kafka": + base_parameters = { + "kafka_topic": runner_conf.kafka_topic, + "kafka_broker": runner_conf.kafka_broker, + "kafka_input_data": f"/{runner_conf.uc_volume_path}/{self.base_dir}/resources/data/iot/iot.json", + } + + tasks.append( jobs.Task( task_key="publish_events", description="test", - depends_on=[jobs.TaskDependency(task_key="setup_dlt_meta_pipeline_spec")], - existing_cluster_id=runner_conf.cluster_id, + depends_on=[ + jobs.TaskDependency(task_key="setup_dlt_meta_pipeline_spec") + ], notebook_task=jobs.NotebookTask( notebook_path=f"{runner_conf.runners_nb_path}/runners/publish_events.py", - base_parameters={ - "eventhub_name": self.args["eventhub_name"], - "eventhub_name_append_flow": self.args["eventhub_name_append_flow"], - "eventhub_namespace": self.args["eventhub_namespace"], - "eventhub_secrets_scope_name": self.args["eventhub_secrets_scope_name"], - "eventhub_accesskey_name": self.args["eventhub_producer_accesskey_name"], - "eventhub_input_data": - f"/{runner_conf.uc_volume_path}/{self.base_dir}/resources/data/iot/iot.json", - "eventhub_append_flow_input_data": - f"/{runner_conf.uc_volume_path}/{self.base_dir}/resources/data/iot_eventhub_af/iot.json", - } - ) - ), - jobs.Task( - task_key="bronze_dlt_pipeline", - depends_on=[jobs.TaskDependency(task_key="publish_events")], - pipeline_task=jobs.PipelineTask( - pipeline_id=runner_conf.bronze_pipeline_id + base_parameters=base_parameters, ), ), - jobs.Task( - task_key="validate_results", - description="test", - depends_on=[jobs.TaskDependency(task_key="bronze_dlt_pipeline")], - notebook_task=jobs.NotebookTask( - notebook_path=f"{runner_conf.runners_nb_path}/runners/validate.py", - base_parameters={ - "run_id": runner_conf.run_id, - "uc_enabled": "True", - "uc_catalog_name": runner_conf.uc_catalog_name, - "bronze_schema": runner_conf.bronze_schema, - "output_file_path": f"/Workspace{runner_conf.test_output_file_path}" - } - ) - ) - ] - ) - - def create_kafka_workflow_spec(self, runner_conf: DLTMetaRunnerConf): - """Create Job specification.""" - dltmeta_environments = [ - jobs.JobEnvironment( - environment_key="dl_meta_int_env", - spec=compute.Environment(client=f"dlt_meta_int_test_{__version__}", - dependencies=[runner_conf.remote_whl_path] - ) ) - ] - dbfs_path = runner_conf.dbfs_tmp_path.replace("dbfs:/", "/dbfs/") + return self.ws.jobs.create( name=f"dlt-meta-{runner_conf.run_id}", - environemnts=dltmeta_environments, - tasks=[ - jobs.Task( - task_key="setup_dlt_meta_pipeline_spec", - description="test", - environment_key="dl_meta_int_env", - timeout_seconds=0, - python_wheel_task=jobs.PythonWheelTask( - package_name="dlt_meta", - entry_point="run", - named_parameters={ - "onboard_layer": "bronze", - "database": f"{runner_conf.uc_catalog_name}.{runner_conf.dlt_meta_schema}", - "onboarding_file_path": - f"{runner_conf.dbfs_tmp_path}/{self.base_dir}/conf/onboarding.json", - "silver_dataflowspec_table": "silver_dataflowspec_cdc", - "silver_dataflowspec_path": f"{self._install_folder()}/dltmeta/data/dlt_spec/silver", - "bronze_dataflowspec_table": "bronze_dataflowspec_cdc", - "import_author": "Ravi", - "version": "v1", - "bronze_dataflowspec_path": f"{self._install_folder()}/dltmeta/data/dlt_spec/bronze", - "overwrite": "True", - "env": runner_conf.env, - "uc_enabled": "True" - } - ) - ), - jobs.Task( - task_key="publish_events", - description="test", - depends_on=[jobs.TaskDependency(task_key="setup_dlt_meta_pipeline_spec")], - notebook_task=jobs.NotebookTask( - notebook_path=f"{runner_conf.runners_nb_path}/runners/publish_events.py", - base_parameters={ - "kafka_topic": self.args["kafka_topic_name"], - "kafka_broker": self.args["kafka_broker"], - "kafka_input_data": f"/{dbfs_path}/{self.base_dir}/resources/data/iot/iot.json" - } - ) - ), - jobs.Task( - task_key="bronze_dlt_pipeline", - depends_on=[jobs.TaskDependency(task_key="publish_events")], - pipeline_task=jobs.PipelineTask( - pipeline_id=runner_conf.bronze_pipeline_id - ), - ), - jobs.Task( - task_key="validate_results", - description="test", - depends_on=[jobs.TaskDependency(task_key="bronze_dlt_pipeline")], - notebook_task=jobs.NotebookTask( - notebook_path=f"{runner_conf.runners_nb_path}/runners/validate.py", - base_parameters={ - "run_id": runner_conf.run_id, - "uc_enabled": "True" , - "uc_catalog_name": runner_conf.uc_catalog_name, - "bronze_schema": runner_conf.bronze_schema, - "output_file_path": f"/Workspace{runner_conf.test_output_file_path}" - } - ) - ), - ] + environments=dltmeta_environments, + tasks= [] ) - def create_kafka_onboarding(self, runner_conf: DLTMetaRunnerConf): - """Create kafka onboarding file.""" - with open(f"{runner_conf.kafka_template}") as f: - onboard_obj = json.load(f) - kafka_topic = self.args["kafka_topic_name"].lower() - kafka_bootstrap_servers = self.args["kafka_broker"].lower() - for data_flow in onboard_obj: - for key, value in data_flow.items(): - if key == "source_details": - for source_key, source_value in value.items(): - if 'dbfs_path' in source_value: - data_flow[key][source_key] = source_value.format(dbfs_path=runner_conf.dbfs_tmp_path) - if 'kafka_topic' in source_value: - data_flow[key][source_key] = source_value.format(kafka_topic=kafka_topic) - if 'kafka_bootstrap_servers' in source_value: - data_flow[key][source_key] = source_value.format( - kafka_bootstrap_servers=kafka_bootstrap_servers) - if 'dbfs_path' in value: - data_flow[key] = value.format(dbfs_path=runner_conf.dbfs_tmp_path) - elif 'run_id' in value: - data_flow[key] = value.format(run_id=runner_conf.run_id) - elif 'uc_catalog_name' in value and 'bronze_schema' in value: - if runner_conf.uc_catalog_name: - data_flow[key] = value.format( - uc_catalog_name=runner_conf.uc_catalog_name, - bronze_schema=runner_conf.bronze_schema - ) - else: - data_flow[key] = value.format( - uc_catalog_name=f"bronze_{runner_conf.run_id}", - bronze_schema="" - ).replace(".", "") - with open(runner_conf.onboarding_file_path, "w") as onboarding_file: - json.dump(onboard_obj, onboarding_file) - - def create_eventhub_onboarding(self, runner_conf: DLTMetaRunnerConf): - """Create eventhub onboarding file.""" - with open(f"{runner_conf.eventhub_template}") as f: - onboard_obj = json.load(f) - eventhub_name = self.args["eventhub_name"].lower() - eventhub_name_append_flow = self.args["eventhub_name_append_flow"].lower() - eventhub_accesskey_name = self.args["eventhub_consumer_accesskey_name"].lower() - eventhub_accesskey_secret_name = self.args["eventhub_accesskey_secret_name"].lower() - eventhub_secrets_scope_name = self.args["eventhub_secrets_scope_name"].lower() - eventhub_namespace = self.args["eventhub_namespace"].lower() - eventhub_port = self.args["eventhub_port"].lower() - for data_flow in onboard_obj: - for key, value in data_flow.items(): - if key == "source_details": - for source_key, source_value in value.items(): - if 'dbfs_path' in source_value: - data_flow[key][source_key] = source_value.format(dbfs_path=runner_conf.dbfs_tmp_path) - if 'uc_volume_path' in source_value: - data_flow[key][source_key] = source_value.format(uc_volume_path=runner_conf.uc_volume_path) - if 'eventhub_name' in source_value: - data_flow[key][source_key] = source_value.format(eventhub_name=eventhub_name) - if 'eventhub_accesskey_name' in source_value: - data_flow[key][source_key] = source_value.format( - eventhub_accesskey_name=eventhub_accesskey_name) - if 'eventhub_secrets_scope_name' in source_value: - data_flow[key][source_key] = source_value.format( - eventhub_secrets_scope_name=eventhub_secrets_scope_name) - if 'eventhub_accesskey_secret_name' in source_value: - data_flow[key][source_key] = source_value.format( - eventhub_accesskey_secret_name=eventhub_accesskey_secret_name) - if 'eventhub_nmspace' in source_value: - data_flow[key][source_key] = source_value.format(eventhub_nmspace=eventhub_namespace) - if 'eventhub_port' in source_value: - data_flow[key][source_key] = source_value.format(eventhub_port=eventhub_port) - if key == 'bronze_append_flows': - counter = 0 - for flows in value: - for flow_key, flow_value in flows.items(): - if flow_key == "source_details": - for source_key, source_value in flows[flow_key].items(): - if 'dbfs_path' in source_value: - data_flow[key][counter][flow_key][source_key] = source_value.format( - dbfs_path=runner_conf.dbfs_tmp_path) - if 'uc_volume_path' in source_value: - data_flow[key][counter][flow_key][source_key] = source_value.format( - uc_volume_path=runner_conf.uc_volume_path) - if 'eventhub_name_append_flow' in source_value: - data_flow[key][counter][flow_key][source_key] = source_value.format( - eventhub_name_append_flow=eventhub_name_append_flow) - if 'eventhub_accesskey_name' in source_value: - data_flow[key][counter][flow_key][source_key] = source_value.format( - eventhub_accesskey_name=eventhub_accesskey_name) - if 'eventhub_secrets_scope_name' in source_value: - data_flow[key][counter][flow_key][source_key] = source_value.format( - eventhub_secrets_scope_name=eventhub_secrets_scope_name) - if 'eventhub_accesskey_secret_name' in source_value: - data_flow[key][counter][flow_key][source_key] = source_value.format( - eventhub_accesskey_secret_name=eventhub_accesskey_secret_name) - if 'eventhub_nmspace' in source_value: - data_flow[key][counter][flow_key][source_key] = source_value.format( - eventhub_nmspace=eventhub_namespace) - if 'eventhub_port' in source_value: - data_flow[key][counter][flow_key][source_key] = source_value.format( - eventhub_port=eventhub_port) - counter += 1 - if 'dbfs_path' in value: - data_flow[key] = value.format(dbfs_path=runner_conf.dbfs_tmp_path) - elif 'uc_volume_path' in value: - data_flow[key] = value.format(uc_volume_path=runner_conf.uc_volume_path) - elif 'run_id' in value: - data_flow[key] = value.format(run_id=runner_conf.run_id) - elif 'uc_catalog_name' in value and 'bronze_schema' in value: - if runner_conf.uc_catalog_name: - data_flow[key] = value.format( - uc_catalog_name=runner_conf.uc_catalog_name, - bronze_schema=runner_conf.bronze_schema - ) - else: - data_flow[key] = value.format( - uc_catalog_name=f"bronze_{runner_conf.run_id}", - bronze_schema="" - ).replace(".", "") - - with open(runner_conf.onboarding_file_path, "w") as onboarding_file: - json.dump(onboard_obj, onboarding_file) - - def replace_eventhub_source_details_values(self, - runner_conf, - eventhub_name, - eventhub_name_append_flow, - eventhub_accesskey_name, - eventhub_accesskey_secret_name, - eventhub_secrets_scope_name, - eventhub_namespace, - eventhub_port, - data_flow, - key, - source_key, - source_value): - if 'dbfs_path' in source_value: - data_flow[key][source_key] = source_value.format(dbfs_path=runner_conf.dbfs_tmp_path) - if 'eventhub_name' in source_value: - data_flow[key][source_key] = source_value.format(eventhub_name=eventhub_name) - if 'eventhub_name_append_flow' in source_value: - data_flow[key][source_key] = source_value.format(eventhub_name_append_flow=eventhub_name_append_flow) - if 'eventhub_accesskey_name' in source_value: - data_flow[key][source_key] = source_value.format( - eventhub_accesskey_name=eventhub_accesskey_name) - if 'eventhub_secrets_scope_name' in source_value: - data_flow[key][source_key] = source_value.format( - eventhub_secrets_scope_name=eventhub_secrets_scope_name) - if 'eventhub_accesskey_secret_name' in source_value: - data_flow[key][source_key] = source_value.format( - eventhub_accesskey_secret_name=eventhub_accesskey_secret_name) - if 'eventhub_nmspace' in source_value: - data_flow[key][source_key] = source_value.format(eventhub_nmspace=eventhub_namespace) - if 'eventhub_port' in source_value: - data_flow[key][source_key] = source_value.format(eventhub_port=eventhub_port) - def initialize_uc_resources(self, runner_conf): - '''Create UC schemas and volumes needed to run the integration tests''' - SchemasAPI(self.ws.api_client).create(catalog_name=runner_conf.uc_catalog_name, - name=runner_conf.dlt_meta_schema, - comment="dlt_meta framework schema") - SchemasAPI(self.ws.api_client).create(catalog_name=runner_conf.uc_catalog_name, - name=runner_conf.bronze_schema, - comment="bronze_schema") + """Create UC schemas and volumes needed to run the integration tests""" + SchemasAPI(self.ws.api_client).create( + catalog_name=runner_conf.uc_catalog_name, + name=runner_conf.dlt_meta_schema, + comment="dlt_meta framework schema", + ) + SchemasAPI(self.ws.api_client).create( + catalog_name=runner_conf.uc_catalog_name, + name=runner_conf.bronze_schema, + comment="bronze_schema", + ) if runner_conf.source == "cloudfiles": - SchemasAPI(self.ws.api_client).create(catalog_name=runner_conf.uc_catalog_name, - name=runner_conf.silver_schema, - comment="silver_schema") - volume_info = self.ws.volumes.create(catalog_name=runner_conf.uc_catalog_name, - schema_name=runner_conf.dlt_meta_schema, - name=runner_conf.uc_volume_name, - volume_type=VolumeType.MANAGED) + SchemasAPI(self.ws.api_client).create( + catalog_name=runner_conf.uc_catalog_name, + name=runner_conf.silver_schema, + comment="silver_schema", + ) + volume_info = self.ws.volumes.create( + catalog_name=runner_conf.uc_catalog_name, + schema_name=runner_conf.dlt_meta_schema, + name=runner_conf.uc_volume_name, + volume_type=VolumeType.MANAGED, + ) runner_conf.volume_info = volume_info - runner_conf.uc_volume_path = (f"/Volumes/{runner_conf.volume_info.catalog_name}/" - f"{runner_conf.volume_info.schema_name}/{runner_conf.volume_info.name}/" - ) + runner_conf.uc_volume_path = ( + f"/Volumes/{runner_conf.volume_info.catalog_name}/" + f"{runner_conf.volume_info.schema_name}/{runner_conf.volume_info.name}/" + ) def generate_onboarding_file(self, runner_conf: DLTMetaRunnerConf): - """Generate onboarding file from template.""" - match runner_conf.source: - case "cloudfiles": - self.create_cloudfiles_onboarding(runner_conf) - case "eventhub": - self.create_eventhub_onboarding(runner_conf) - case "kafka": - self.create_kafka_onboarding(runner_conf) - - def create_cloudfiles_onboarding(self, runner_conf: DLTMetaRunnerConf): - """Create onboarding file when the source is cloudfiles by filling out the templates.""" + """Generate onboarding file from templates.""" string_subs = { "{uc_volume_path}": runner_conf.uc_volume_path, "{uc_catalog_name}": runner_conf.uc_catalog_name, "{bronze_schema}": runner_conf.bronze_schema, - "{silver_schema}": runner_conf.silver_schema, - # "{run_id}": runner_conf.run_id, } + match runner_conf.source: + case "cloudfiles": + string_subs.update({"{silver_schema}": runner_conf.silver_schema}) + case "eventhub": + string_subs.update( + { + "{run_id}": runner_conf.run_id, + "{eventhub_name}": runner_conf.eventhub_name, + "{eventhub_name_append_flow}": runner_conf.eventhub_name_append_flow, + "{eventhub_accesskey_name}": runner_conf.eventhub_accesskey_name, + "{eventhub_accesskey_secret_name}": runner_conf.eventhub_accesskey_secret_name, + "{eventhub_secrets_scope_name}": runner_conf.eventhub_secrets_scope_name, + "{eventhub_namespace}": runner_conf.eventhub_namespace, + "{eventhub_port}": runner_conf.eventhub_port, + } + ) + case "kafka": + string_subs.update( + { + "{run_id}": runner_conf.run_id, + "{kafka_topic}": runner_conf.kafka_topic, + "{kafka_broker}": runner_conf.kafka_broker, + } + ) + # Open the onboarding templates and sub in the proper table locations, paths, etc. with open(f"{runner_conf.cloudfiles_template}", "r") as f: onboard_json = f.read() - with open(f"{runner_conf.cloudfiles_A2_template}") as f: - onboard_json_a2 = f.read() + if runner_conf.source == "cloudfiles": + with open(f"{runner_conf.cloudfiles_A2_template}") as f: + onboard_json_a2 = f.read() for key, val in string_subs.items(): onboard_json = onboard_json.replace(key, val) - onboard_json_a2 = onboard_json_a2.replace(key, val) + if runner_conf.source == "cloudfiles": + onboard_json_a2 = onboard_json_a2.replace(key, val) with open(runner_conf.onboarding_file_path, "w") as onboarding_file: json.dump(json.loads(onboard_json), onboarding_file, indent=4) - with open(runner_conf.onboarding_A2_file_path, "w") as onboarding_file_a2: - json.dump(json.loads(onboard_json_a2), onboarding_file_a2, indent=4) + if runner_conf.source == "cloudfiles": + with open(runner_conf.onboarding_A2_file_path, "w") as onboarding_file_a2: + json.dump(json.loads(onboard_json_a2), onboarding_file_a2, indent=4) def upload_files_to_databricks(self, runner_conf: DLTMetaRunnerConf): """ @@ -826,13 +595,7 @@ def create_bronze_silver_dlt(self, runner_conf: DLTMetaRunnerConf): def launch_workflow(self, runner_conf: DLTMetaRunnerConf): - match runner_conf.source: - case "cloudfiles": - created_job = self.create_cloudfiles_workflow_spec(runner_conf) - case "eventhub": - created_job = self.create_eventhub_workflow_spec(runner_conf) - case "kafka": - created_job = self.create_kafka_workflow_spec(runner_conf) + created_job = self.create_workflow_spec(runner_conf) runner_conf.job_id = created_job.job_id print(f"Job created successfully. job_id={created_job.job_id}, started run...") @@ -866,8 +629,6 @@ def clean_up(self, runner_conf: DLTMetaRunnerConf): self.ws.pipelines.delete(runner_conf.bronze_pipeline_id) if runner_conf.silver_pipeline_id: self.ws.pipelines.delete(runner_conf.silver_pipeline_id) - if runner_conf.cluster_id: - self.ws.clusters.delete(runner_conf.cluster_id) if runner_conf.uc_catalog_name: test_schema_list = [runner_conf.dlt_meta_schema, runner_conf.bronze_schema, runner_conf.silver_schema] schema_list = self.ws.schemas.list(catalog_name=runner_conf.uc_catalog_name) @@ -892,23 +653,16 @@ def clean_up(self, runner_conf: DLTMetaRunnerConf): print("Cleaning up complete!!!") def run(self, runner_conf: DLTMetaRunnerConf): - - self.init_dltmeta_runner_conf(runner_conf) - self.create_bronze_silver_dlt(runner_conf) - self.launch_workflow(runner_conf) - self.download_test_results(runner_conf) - - # try: - # self.init_dltmeta_runner_conf(runner_conf) - # exit() - # self.create_bronze_silver_dlt(runner_conf) - # self.launch_workflow(runner_conf) - # self.download_test_results(runner_conf) - # except Exception as e: - # print(e) - # finally: - # print("Cleaning up...") - # self.clean_up(runner_conf) + try: + self.init_dltmeta_runner_conf(runner_conf) + self.create_bronze_silver_dlt(runner_conf) + self.launch_workflow(runner_conf) + self.download_test_results(runner_conf) + except Exception as e: + traceback.print_exc() + finally: + print("Cleaning up...") + self.clean_up(runner_conf) def process_arguments() -> dict[str:str]: @@ -958,61 +712,61 @@ def process_arguments() -> dict[str:str]: [ "eventhub_name_append_flow", "Provide eventhub_name_append_flow e.g: iot_af", - str, + str.lower, False, [], ], [ "eventhub_producer_accesskey_name", "Provide access key that has write permission on the eventhub", - str, + str.lower, False, [], ], [ "eventhub_consumer_accesskey_name", "Provide access key that has read permission on the eventhub", - str, + str.lower, False, [], ], [ "eventhub_secrets_scope_name", "Provide eventhub_secrets_scope_name e.g: eventhubs_creds", - str, + str.lower, False, [], ], [ "eventhub_accesskey_secret_name", "Provide eventhub_accesskey_secret_name e.g: RootManageSharedAccessKey", - str, + str.lower, False, [], ], [ "eventhub_namespace", "Provide eventhub_namespace e.g: topic-standar", - str, + str.lower, False, [], ], [ "eventhub_port", "Provide eventhub_port e.g: 9093", - str, + str.lower, False, [], ], # Kafka arguments [ - "kafka_topic_name", + "kafka_topic", "Provide kafka topic name e.g: iot", - str, + str.lower, False, [], ], - ["kafka_broker", "Provide kafka broker e.g 127.0.0.1:9092", str, False, []], + ["kafka_broker", "Provide kafka broker e.g 127.0.0.1:9092", str.lower, False, []], ] # Build cli parser @@ -1051,7 +805,7 @@ def check_cond_mandatory_arg(args, mandatory_args): elif args["source"] == "kafka": check_cond_mandatory_arg( args, - ["kafka_topic_name", "kafka_broker"], + ["kafka_topic", "kafka_broker"], ) print(f"Processing comand line arguments Complete: {args}") @@ -1075,7 +829,7 @@ def main(): workspace_client = get_workspace_api_client(args["profile"]) integration_test_runner = DLTMETARunner(args, workspace_client, "integration_tests") runner_conf = integration_test_runner.init_runner_conf() - integration_test_runner.run(runner_conf) + #integration_test_runner.run(runner_conf) if __name__ == "__main__": main() From 1cdb50325249c8d002667b2095bf40179eb2f2aa Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Tue, 8 Oct 2024 16:07:48 -0400 Subject: [PATCH 40/59] redundant doc strings removal --- integration_tests/run_integration_tests.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index ee69fd3..0ebe382 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -140,14 +140,7 @@ def __init__(self, args: dict[str: str], ws, base_dir): self.base_dir = base_dir def init_runner_conf(self) -> DLTMetaRunnerConf: - """ - Initialize the runner configuration for running integration tests. - - Returns: - ------- - DLTMetaRunnerConf - The initialized runner configuration. - """ + """Initialize the runner configuration for running integration tests. """ run_id = uuid.uuid4().hex runner_conf = DLTMetaRunnerConf( run_id=run_id, From 37652f9b8f59067fea13e939c35e054794e5f2d0 Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Tue, 8 Oct 2024 16:08:17 -0400 Subject: [PATCH 41/59] remove early exit --- integration_tests/run_integration_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index 0ebe382..eccba58 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -822,7 +822,7 @@ def main(): workspace_client = get_workspace_api_client(args["profile"]) integration_test_runner = DLTMETARunner(args, workspace_client, "integration_tests") runner_conf = integration_test_runner.init_runner_conf() - #integration_test_runner.run(runner_conf) + integration_test_runner.run(runner_conf) if __name__ == "__main__": main() From a6249fe09876706d7603bf5096c44c7ea16c5b23 Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Tue, 8 Oct 2024 16:15:19 -0400 Subject: [PATCH 42/59] eventhub_accesskey_name updates --- .../conf/eventhub-onboarding.template | 4 ++-- integration_tests/run_integration_tests.py | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/integration_tests/conf/eventhub-onboarding.template b/integration_tests/conf/eventhub-onboarding.template index 113c13f..8ce3792 100644 --- a/integration_tests/conf/eventhub-onboarding.template +++ b/integration_tests/conf/eventhub-onboarding.template @@ -6,7 +6,7 @@ "source_format": "eventhub", "source_details": { "source_schema_path": "{uc_volume_path}/integration_tests/resources/eventhub_iot_schema.ddl", - "eventhub.accessKeyName": "{eventhub_accesskey_name}", + "eventhub.accessKeyName": "{eventhub_consumer_accesskey_name}", "eventhub.name": "{eventhub_name}", "eventhub.accessKeySecretName": "{eventhub_accesskey_secret_name}", "eventhub.secretsScopeName": "{eventhub_secrets_scope_name}", @@ -36,7 +36,7 @@ "source_format": "eventhub", "source_details": { "source_schema_path": "{uc_volume_path}/integration_tests/resources/eventhub_iot_schema.ddl", - "eventhub.accessKeyName": "{eventhub_accesskey_name}", + "eventhub.accessKeyName": "{eventhub_consumer_accesskey_name}", "eventhub.name": "{eventhub_name_append_flow}", "eventhub.accessKeySecretName": "{eventhub_accesskey_secret_name}", "eventhub.secretsScopeName": "{eventhub_secrets_scope_name}", diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index eccba58..6dab0bd 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -112,7 +112,8 @@ class DLTMetaRunnerConf: eventhub_append_flow_input_data: str = None eventhub_name: str = None eventhub_name_append_flow: str = None - eventhub_accesskey_name: str = None + eventhub_producer_accesskey_name: str = None + eventhub_consumer_accesskey_name: str = None eventhub_accesskey_secret_name: str = None eventhub_secrets_scope_name: str = None eventhub_namespace: str = None @@ -162,7 +163,8 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: # eventhub provided args eventhub_name = self.args["eventhub_name"], eventhub_name_append_flow = self.args["eventhub_name_append_flow"], - eventhub_accesskey_name = self.args["eventhub_accesskey_name"], + eventhub_producer_accesskey_name = self.args["eventhub_consumer_accesskey_name"], + eventhub_consumer_accesskey_name = self.args["eventhub_consumer_accesskey_name"], eventhub_accesskey_secret_name = self.args["eventhub_accesskey_secret_name"], eventhub_secrets_scope_name = self.args["eventhub_secrets_scope_name"], eventhub_namespace = self.args["eventhub_namespace"], @@ -387,7 +389,7 @@ def create_workflow_spec(self, runner_conf: DLTMetaRunnerConf): "eventhub_name_append_flow": runner_conf.eventhub_name_append_flow, "eventhub_namespace": runner_conf.eventhub_namespace, "eventhub_secrets_scope_name": runner_conf.eventhub_secrets_scope_name, - "eventhub_accesskey_name": runner_conf.eventhub_accesskey_name, + "eventhub_accesskey_name": runner_conf.eventhub_producer_accesskey_name, "eventhub_input_data": f"/{runner_conf.uc_volume_path}/{self.base_dir}/resources/data/iot/iot.json", "eventhub_append_flow_input_data": f"/{runner_conf.uc_volume_path}/{self.base_dir}/resources/data/iot_eventhub_af/iot.json", } @@ -466,7 +468,7 @@ def generate_onboarding_file(self, runner_conf: DLTMetaRunnerConf): "{run_id}": runner_conf.run_id, "{eventhub_name}": runner_conf.eventhub_name, "{eventhub_name_append_flow}": runner_conf.eventhub_name_append_flow, - "{eventhub_accesskey_name}": runner_conf.eventhub_accesskey_name, + "{eventhub_consumer_accesskey_name}": runner_conf.eventhub_consumer_accesskey_name, "{eventhub_accesskey_secret_name}": runner_conf.eventhub_accesskey_secret_name, "{eventhub_secrets_scope_name}": runner_conf.eventhub_secrets_scope_name, "{eventhub_namespace}": runner_conf.eventhub_namespace, @@ -657,7 +659,6 @@ def run(self, runner_conf: DLTMetaRunnerConf): print("Cleaning up...") self.clean_up(runner_conf) - def process_arguments() -> dict[str:str]: """ Get, process, and validate the command line arguements @@ -701,7 +702,7 @@ def process_arguments() -> dict[str:str]: ["cloudfiles", "eventhub", "kafka"], ], # Eventhub arguments - ["eventhub_name", "Provide eventhub_name e.g: iot", str, False, []], + ["eventhub_name", "Provide eventhub_name e.g: iot", str.lower, False, []], [ "eventhub_name_append_flow", "Provide eventhub_name_append_flow e.g: iot_af", @@ -804,7 +805,6 @@ def check_cond_mandatory_arg(args, mandatory_args): print(f"Processing comand line arguments Complete: {args}") return args - def get_workspace_api_client(profile=None) -> WorkspaceClient: """Get api client with config.""" if profile: From c740093c396246f39e459f20c481feae280d9b6c Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Tue, 8 Oct 2024 16:16:44 -0400 Subject: [PATCH 43/59] fixed job create --- integration_tests/run_integration_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index 6dab0bd..d85a7af 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -417,7 +417,7 @@ def create_workflow_spec(self, runner_conf: DLTMetaRunnerConf): return self.ws.jobs.create( name=f"dlt-meta-{runner_conf.run_id}", environments=dltmeta_environments, - tasks= [] + tasks= tasks ) def initialize_uc_resources(self, runner_conf): From b769973eeb79a892eea74bbef5ceae90a89e2e83 Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Tue, 8 Oct 2024 16:32:21 -0400 Subject: [PATCH 44/59] fixed clean up --- integration_tests/run_integration_tests.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index d85a7af..8c9bd46 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -622,6 +622,8 @@ def clean_up(self, runner_conf: DLTMetaRunnerConf): self.ws.jobs.delete(runner_conf.job_id) if runner_conf.bronze_pipeline_id: self.ws.pipelines.delete(runner_conf.bronze_pipeline_id) + if runner_conf.bronze_pipeline_A2_id: + self.ws.pipelines.delete(runner_conf.bronze_pipeline_A2_id) if runner_conf.silver_pipeline_id: self.ws.pipelines.delete(runner_conf.silver_pipeline_id) if runner_conf.uc_catalog_name: From f6bb5fe6bd50345c2dad830df1cd404aaf59d163 Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Tue, 8 Oct 2024 16:32:54 -0400 Subject: [PATCH 45/59] redundant message removed --- integration_tests/run_integration_tests.py | 1 - 1 file changed, 1 deletion(-) diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index 8c9bd46..d7f53ea 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -658,7 +658,6 @@ def run(self, runner_conf: DLTMetaRunnerConf): except Exception as e: traceback.print_exc() finally: - print("Cleaning up...") self.clean_up(runner_conf) def process_arguments() -> dict[str:str]: From 381531c60bad580da9378bb644319807a46432e2 Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Tue, 8 Oct 2024 16:40:56 -0400 Subject: [PATCH 46/59] formatting --- integration_tests/run_integration_tests.py | 87 ++++++++++++++-------- 1 file changed, 58 insertions(+), 29 deletions(-) diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index d7f53ea..c143726 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -17,7 +17,12 @@ import json # Dictionary mapping cloud providers to node types -cloud_node_type_id_dict = {"aws": "i3.xlarge", "azure": "Standard_D3_v2", "gcp": "n1-highmem-4"} +cloud_node_type_id_dict = { + "aws": "i3.xlarge", + "azure": "Standard_D3_v2", + "gcp": "n1-highmem-4", +} + @dataclass class DLTMetaRunnerConf: @@ -73,6 +78,7 @@ class DLTMetaRunnerConf: job_id : str, optional The ID of the job to use for the test run. """ + run_id: str username: str = None run_name: str = None @@ -80,8 +86,8 @@ class DLTMetaRunnerConf: uc_volume_name: str = "dlt_meta_files" onboarding_file_path: str = "integration_tests/conf/onboarding.json" onboarding_A2_file_path: str = "integration_tests/conf/onboarding_A2.json" - #onboarding_fanout_file_path: str = "integration_tests/conf/onboarding.json" - #onboarding_fanout_templates: str = None + # onboarding_fanout_file_path: str = "integration_tests/conf/onboarding.json" + # onboarding_fanout_templates: str = None int_tests_dir: str = "integration_tests" dlt_meta_schema: str = None bronze_schema: str = None @@ -104,10 +110,12 @@ class DLTMetaRunnerConf: # cloudfiles info cloudfiles_template: str = "integration_tests/conf/cloudfiles-onboarding.template" - cloudfiles_A2_template: str = "integration_tests/conf/cloudfiles-onboarding_A2.template" + cloudfiles_A2_template: str = ( + "integration_tests/conf/cloudfiles-onboarding_A2.template" + ) # eventhub info - eventhub_template: str = "integration_tests/conf/eventhub-onboarding.template", + eventhub_template: str = ("integration_tests/conf/eventhub-onboarding.template",) eventhub_input_data: str = None eventhub_append_flow_input_data: str = None eventhub_name: str = None @@ -134,14 +142,15 @@ class DLTMETARunner: - workspace_client: Databricks workspace client - runner_conf: test information """ - def __init__(self, args: dict[str: str], ws, base_dir): + + def __init__(self, args: dict[str:str], ws, base_dir): self.args = args self.ws = ws self.wsi = WorkspaceInstaller(ws) self.base_dir = base_dir def init_runner_conf(self) -> DLTMetaRunnerConf: - """Initialize the runner configuration for running integration tests. """ + """Initialize the runner configuration for running integration tests.""" run_id = uuid.uuid4().hex runner_conf = DLTMetaRunnerConf( run_id=run_id, @@ -158,18 +167,21 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: f"{run_id}/integration-test-output.csv" ), # kafka provided args - kafka_topic = self.args["kafka_topic"], - kafka_broker = self.args["kafka_broker"], + kafka_topic=self.args["kafka_topic"], + kafka_broker=self.args["kafka_broker"], # eventhub provided args - eventhub_name = self.args["eventhub_name"], - eventhub_name_append_flow = self.args["eventhub_name_append_flow"], - eventhub_producer_accesskey_name = self.args["eventhub_consumer_accesskey_name"], - eventhub_consumer_accesskey_name = self.args["eventhub_consumer_accesskey_name"], - eventhub_accesskey_secret_name = self.args["eventhub_accesskey_secret_name"], - eventhub_secrets_scope_name = self.args["eventhub_secrets_scope_name"], - eventhub_namespace = self.args["eventhub_namespace"], - eventhub_port = self.args["eventhub_port"], - + eventhub_name=self.args["eventhub_name"], + eventhub_name_append_flow=self.args["eventhub_name_append_flow"], + eventhub_producer_accesskey_name=self.args[ + "eventhub_consumer_accesskey_name" + ], + eventhub_consumer_accesskey_name=self.args[ + "eventhub_consumer_accesskey_name" + ], + eventhub_accesskey_secret_name=self.args["eventhub_accesskey_secret_name"], + eventhub_secrets_scope_name=self.args["eventhub_secrets_scope_name"], + eventhub_namespace=self.args["eventhub_namespace"], + eventhub_port=self.args["eventhub_port"], ) # Set the proper directory location for the notebooks that need to be uploaded to run and @@ -182,7 +194,9 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: try: runner_conf.runners_full_local_path = source_paths[runner_conf.source] except KeyError: - raise Exception("Given source is not support. Support source are: cloudfiles, eventhub, or kafka") + raise Exception( + "Given source is not support. Support source are: cloudfiles, eventhub, or kafka" + ) return runner_conf @@ -417,7 +431,7 @@ def create_workflow_spec(self, runner_conf: DLTMetaRunnerConf): return self.ws.jobs.create( name=f"dlt-meta-{runner_conf.run_id}", environments=dltmeta_environments, - tasks= tasks + tasks=tasks, ) def initialize_uc_resources(self, runner_conf): @@ -528,10 +542,10 @@ def upload_files_to_databricks(self, runner_conf: DLTMetaRunnerConf): if file.endswith(".json"): with open(os.path.join(root, file), "rb") as content: self.ws.files.upload( - file_path=f"{runner_conf.uc_volume_path}{root}/{file}", - contents=content, - overwrite=True, - ) + file_path=f"{runner_conf.uc_volume_path}{root}/{file}", + contents=content, + overwrite=True, + ) print(f"Integration test file upload to {uc_vol_full_path} complete!!!") # Upload required notebooks for the given source @@ -551,7 +565,9 @@ def upload_files_to_databricks(self, runner_conf: DLTMetaRunnerConf): print("Python wheel upload starting...") # Upload the wheel to both the workspace and the uc volume - runner_conf.remote_whl_path = f"{self.wsi._upload_wheel(uc_volume_path=runner_conf.uc_volume_path)}" + runner_conf.remote_whl_path = ( + f"{self.wsi._upload_wheel(uc_volume_path=runner_conf.uc_volume_path)}" + ) print(f"Python wheel upload to {runner_conf.remote_whl_path} completed!!!") def init_dltmeta_runner_conf(self, runner_conf: DLTMetaRunnerConf): @@ -627,21 +643,25 @@ def clean_up(self, runner_conf: DLTMetaRunnerConf): if runner_conf.silver_pipeline_id: self.ws.pipelines.delete(runner_conf.silver_pipeline_id) if runner_conf.uc_catalog_name: - test_schema_list = [runner_conf.dlt_meta_schema, runner_conf.bronze_schema, runner_conf.silver_schema] + test_schema_list = [ + runner_conf.dlt_meta_schema, + runner_conf.bronze_schema, + runner_conf.silver_schema, + ] schema_list = self.ws.schemas.list(catalog_name=runner_conf.uc_catalog_name) for schema in schema_list: if schema.name in test_schema_list: print(f"Deleting schema: {schema.name}") vol_list = self.ws.volumes.list( catalog_name=runner_conf.uc_catalog_name, - schema_name=schema.name + schema_name=schema.name, ) for vol in vol_list: print(f"Deleting volume:{vol.full_name}") self.ws.volumes.delete(vol.full_name) tables_list = self.ws.tables.list( catalog_name=runner_conf.uc_catalog_name, - schema_name=schema.name + schema_name=schema.name, ) for table in tables_list: print(f"Deleting table:{table.full_name}") @@ -660,6 +680,7 @@ def run(self, runner_conf: DLTMetaRunnerConf): finally: self.clean_up(runner_conf) + def process_arguments() -> dict[str:str]: """ Get, process, and validate the command line arguements @@ -761,7 +782,13 @@ def process_arguments() -> dict[str:str]: False, [], ], - ["kafka_broker", "Provide kafka broker e.g 127.0.0.1:9092", str.lower, False, []], + [ + "kafka_broker", + "Provide kafka broker e.g 127.0.0.1:9092", + str.lower, + False, + [], + ], ] # Build cli parser @@ -806,6 +833,7 @@ def check_cond_mandatory_arg(args, mandatory_args): print(f"Processing comand line arguments Complete: {args}") return args + def get_workspace_api_client(profile=None) -> WorkspaceClient: """Get api client with config.""" if profile: @@ -825,5 +853,6 @@ def main(): runner_conf = integration_test_runner.init_runner_conf() integration_test_runner.run(runner_conf) + if __name__ == "__main__": main() From eb44fa580f249a4dd8f25280bae5d1d00ba845a2 Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Tue, 8 Oct 2024 17:50:23 -0400 Subject: [PATCH 47/59] non cloud files testing, niether can be confirmed right now --- integration_tests/README.md | 2 +- .../notebooks/eventhub_runners/publish_events.py | 2 +- .../notebooks/kafka_runners/publish_events.py | 9 +++++++-- integration_tests/run_integration_tests.py | 9 +++++++++ 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/integration_tests/README.md b/integration_tests/README.md index 4d7afc7..5263ebf 100644 --- a/integration_tests/README.md +++ b/integration_tests/README.md @@ -43,7 +43,7 @@ - 9b. Run the command for eventhub ```commandline - python integration_tests/run_integration_tests.py --uc_catalog_name=<> --source=eventhub --cloud_provider_name=aws --eventhub_name=iot --eventhub_secrets_scope_name=eventhubs_creds --eventhub_namespace=int_test-standard --eventhub_port=9093 --eventhub_producer_accesskey_name=producer --eventhub_consumer_accesskey_name=consumer --eventhub_name_append_flow=TODO? --eventhub_accesskey_secret_name=TODO? + python integration_tests/run_integration_tests.py --uc_catalog_name=<> --source=eventhub --cloud_provider_name=aws --eventhub_name=iot --eventhub_secrets_scope_name=eventhubs_creds --eventhub_namespace=int_test-standard --eventhub_port=9093 --eventhub_producer_accesskey_name=producer --eventhub_consumer_accesskey_name=consumer --eventhub_name_append_flow=test_append_flow --eventhub_accesskey_secret_name=test_secret_name --profile=<> ``` - - For eventhub integration tests, the following are the prerequisites: diff --git a/integration_tests/notebooks/eventhub_runners/publish_events.py b/integration_tests/notebooks/eventhub_runners/publish_events.py index 6f3aa2b..e5ac556 100644 --- a/integration_tests/notebooks/eventhub_runners/publish_events.py +++ b/integration_tests/notebooks/eventhub_runners/publish_events.py @@ -4,7 +4,7 @@ # COMMAND ---------- -# MAGIC %sh pip install azure-eventhub +# MAGIC %pip install azure-eventhub # COMMAND ---------- diff --git a/integration_tests/notebooks/kafka_runners/publish_events.py b/integration_tests/notebooks/kafka_runners/publish_events.py index 23fbc4b..03bf6e6 100644 --- a/integration_tests/notebooks/kafka_runners/publish_events.py +++ b/integration_tests/notebooks/kafka_runners/publish_events.py @@ -2,6 +2,11 @@ # DBTITLE 1,Install kafka python lib # MAGIC %pip install kafka-python +# COMMAND ---------- + +dbutils.library.restartPython() + + # COMMAND ---------- # DBTITLE 1,Extract input from notebook params @@ -27,6 +32,6 @@ data = json.load(f) for event in data: - producer.send(kafka_topic,event) - + producer.send(kafka_topic,event) + producer.close() \ No newline at end of file diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index c143726..9a4fce0 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -855,4 +855,13 @@ def main(): if __name__ == "__main__": + """ + Cloud files tests passing + + Kafka is failling due to 'AttributeError: '_SixMetaPathImporter' object has no attribute 'find_spec'' + that occurs on from kafka import KafkaProducer when using a serverless notebook (it succeeds on + a classic cluster) + + No eventhub connection to be able to test that + """ main() From f6954fb671868bc4dcd03a946f3bf12d55e4cb91 Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Wed, 9 Oct 2024 11:36:10 -0400 Subject: [PATCH 48/59] af cloud demo update --- .gitignore | 1 + demo/README.md | 33 ++++++++-------- demo/launch_af_cloudfiles_demo.py | 30 +++++++------- .../init_dlt_meta_pipeline.py | 10 +++++ .../afam_cloudfiles_runners/validate.py | 39 +++++++++++++++++++ 5 files changed, 81 insertions(+), 32 deletions(-) create mode 100644 demo/notebooks/afam_cloudfiles_runners/init_dlt_meta_pipeline.py create mode 100644 demo/notebooks/afam_cloudfiles_runners/validate.py diff --git a/.gitignore b/.gitignore index b66d8f1..4adc1ea 100644 --- a/.gitignore +++ b/.gitignore @@ -155,5 +155,6 @@ deployment-merged.yaml .databricks-login.json demo/conf/onboarding.json integration_tests/conf/onboarding*.json +demo/conf/onboarding*.json databricks.yaml integration_test_output*.csv diff --git a/demo/README.md b/demo/README.md index 5504d5b..a150300 100644 --- a/demo/README.md +++ b/demo/README.md @@ -1,4 +1,4 @@ - # [DLT-META](https://github.com/databrickslabs/dlt-meta) DEMO's + # [DLT-META](https://github.com/databrickslabs/dlt-meta) DEMO's 1. [DAIS 2023 DEMO](#dais-2023-demo): Showcases DLT-META's capabilities of creating Bronze and Silver DLT pipelines with initial and incremental mode automatically. 2. [Databricks Techsummit Demo](#databricks-tech-summit-fy2024-demo): 100s of data sources ingestion in bronze and silver DLT pipelines automatically. 3. [Append FLOW Autoloader Demo](#append-flow-autoloader-file-metadata-demo): Write to same target from multiple sources using [dlt.append_flow](https://docs.databricks.com/en/delta-live-tables/flows.html#append-flows) and adding [File metadata column](https://docs.databricks.com/en/ingestion/file-metadata-column.html) @@ -7,7 +7,7 @@ -# DAIS 2023 DEMO +# DAIS 2023 DEMO ## [DAIS 2023 Session Recording](https://www.youtube.com/watch?v=WYv5haxLlfA) This Demo launches Bronze and Silver DLT pipelines with following activities: - Customer and Transactions feeds for initial load @@ -20,7 +20,7 @@ This Demo launches Bronze and Silver DLT pipelines with following activities: 2. Install [Databricks CLI](https://docs.databricks.com/dev-tools/cli/index.html) 3. ```commandline - git clone https://github.com/databrickslabs/dlt-meta.git + git clone https://github.com/databrickslabs/dlt-meta.git ``` 4. ```commandline @@ -52,10 +52,10 @@ This demo will launch auto generated tables(100s) inside single bronze and silve 2. Install [Databricks CLI](https://docs.databricks.com/dev-tools/cli/index.html) 3. ```commandline - git clone https://github.com/databrickslabs/dlt-meta.git + git clone https://github.com/databrickslabs/dlt-meta.git ``` -4. ```commandline +4. ```commandline cd dlt-meta ``` @@ -68,7 +68,7 @@ This demo will launch auto generated tables(100s) inside single bronze and silve export PYTHONPATH=$dlt_meta_home ``` -6. ```commandline +6. ```commandline python demo/launch_techsummit_demo.py --uc_catalog_name=<> ``` - uc_catalog_name : Unity catalog name @@ -88,7 +88,7 @@ This demo will perform following tasks: 2. Install [Databricks CLI](https://docs.databricks.com/dev-tools/cli/index.html) 3. ```commandline - git clone https://github.com/databrickslabs/dlt-meta.git + git clone https://github.com/databrickslabs/dlt-meta.git ``` 4. ```commandline @@ -105,9 +105,10 @@ This demo will perform following tasks: ``` 6. ```commandline - python demo/launch_af_cloudfiles_demo.py --uc_catalog_name=<> + python demo/launch_af_cloudfiles_demo.py --uc_catalog_name=<> --source=cloudfiles --cloud_provider_name=aws --profile=<> ``` - uc_catalog_name : Unity Catalog name + - cloud_provier_name : Which cloud you are using, either AWS, Azure, or GCP - you can provide `--profile=databricks_profile name` in case you already have databricks cli otherwise command prompt will ask host and token ![af_am_demo.png](../docs/static/images/af_am_demo.png) @@ -121,7 +122,7 @@ This demo will perform following tasks: 2. Install [Databricks CLI](https://docs.databricks.com/dev-tools/cli/index.html) 3. ```commandline - git clone https://github.com/databrickslabs/dlt-meta.git + git clone https://github.com/databrickslabs/dlt-meta.git ``` 4. ```commandline @@ -141,14 +142,14 @@ This demo will perform following tasks: - ``` commandline databricks secrets create-scope eventhubs_dltmeta_creds ``` - - ```commandline + - ```commandline databricks secrets put-secret --json '{ "scope": "eventhubs_dltmeta_creds", "key": "RootManageSharedAccessKey", "string_value": "<>" - }' + }' ``` -- Create databricks secrets to store producer and consumer keys using the scope created in step 2 +- Create databricks secrets to store producer and consumer keys using the scope created in step 2 - Following are the mandatory arguments for running EventHubs demo - uc_catalog_name : unity catalog name e.g. ravi_dlt_meta_uc @@ -160,7 +161,7 @@ This demo will perform following tasks: - eventhub_secrets_scope_name: Databricks secret scope name e.g. eventhubs_dltmeta_creds - eventhub_port: Eventhub port -7. ```commandline +7. ```commandline python3 demo/launch_af_eventhub_demo.py --uc_catalog_name=<> --eventhub_name=dltmeta_demo --eventhub_name_append_flow=dltmeta_demo_af --eventhub_secrets_scope_name=dltmeta_eventhub_creds --eventhub_namespace=dltmeta --eventhub_port=9093 --eventhub_producer_accesskey_name=RootManageSharedAccessKey --eventhub_consumer_accesskey_name=RootManageSharedAccessKey --eventhub_accesskey_secret_name=RootManageSharedAccessKey ``` @@ -172,7 +173,7 @@ This demo will perform following tasks: - Run the onboarding process for the bronze cars table, which contains data from various countries. - Run the onboarding process for the silver tables, which have a `where_clause` based on the country condition specified in [silver_transformations_cars.json](https://github.com/databrickslabs/dlt-meta/blob/main/demo/conf/silver_transformations_cars.json). - Run the Bronze DLT pipeline which will produce cars table. - - Run Silver DLT pipeline, fanning out from the bronze cars table to country-specific tables such as cars_usa, cars_uk, cars_germany, and cars_japan. + - Run Silver DLT pipeline, fanning out from the bronze cars table to country-specific tables such as cars_usa, cars_uk, cars_germany, and cars_japan. ### Steps: 1. Launch Command Prompt @@ -180,7 +181,7 @@ This demo will perform following tasks: 2. Install [Databricks CLI](https://docs.databricks.com/dev-tools/cli/index.html) 3. ```commandline - git clone https://github.com/databrickslabs/dlt-meta.git + git clone https://github.com/databrickslabs/dlt-meta.git ``` 4. ```commandline @@ -215,5 +216,5 @@ This demo will perform following tasks: - Paste to command prompt ![silver_fanout_workflow.png](../docs/static/images/silver_fanout_workflow.png) - + ![silver_fanout_dlt.png](../docs/static/images/silver_fanout_dlt.png) \ No newline at end of file diff --git a/demo/launch_af_cloudfiles_demo.py b/demo/launch_af_cloudfiles_demo.py index db01b60..4bc0649 100644 --- a/demo/launch_af_cloudfiles_demo.py +++ b/demo/launch_af_cloudfiles_demo.py @@ -7,6 +7,7 @@ get_workspace_api_client, process_arguments ) +import traceback class DLTMETAFCFDemo(DLTMETARunner): @@ -29,7 +30,7 @@ def run(self, runner_conf: DLTMetaRunnerConf): self.create_bronze_silver_dlt(runner_conf) self.launch_workflow(runner_conf) except Exception as e: - print(e) + traceback.print_exc() def init_runner_conf(self) -> DLTMetaRunnerConf: """ @@ -44,7 +45,8 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: runner_conf = DLTMetaRunnerConf( run_id=run_id, username=self.wsi._my_username, - int_tests_dir="file:./demo", + uc_catalog_name=self.args["uc_catalog_name"], + int_tests_dir="demo", dlt_meta_schema=f"dlt_meta_dataflowspecs_demo_{run_id}", bronze_schema=f"dlt_meta_bronze_demo_{run_id}", silver_schema=f"dlt_meta_silver_demo_{run_id}", @@ -54,29 +56,25 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: cloudfiles_A2_template="demo/conf/cloudfiles-onboarding_A2.template", onboarding_file_path="demo/conf/onboarding.json", onboarding_A2_file_path="demo/conf/onboarding_A2.json", - env="demo" + env="demo", + runners_full_local_path = './demo/notebooks/afam_cloudfiles_runners/', + test_output_file_path=( + f"/Users/{self.wsi._my_username}/dlt_meta_demo/" + f"{run_id}/demo-output.csv" + ), ) - runner_conf.uc_catalog_name = self.args.__dict__['uc_catalog_name'] - runner_conf.runners_full_local_path = './demo/dbc/afam_cloud_files_runners.dbc' + return runner_conf def launch_workflow(self, runner_conf: DLTMetaRunnerConf): - created_job = self.create_cloudfiles_workflow_spec(runner_conf) + created_job = self.create_workflow_spec(runner_conf) self.open_job_url(runner_conf, created_job) -afam_args_map = { - "--profile": "provide databricks cli profile name, if not provide databricks_host and token", - "--uc_catalog_name": "provide databricks uc_catalog name, this is required to create volume, schema, table" -} - -afam_mandatory_args = [ - "uc_catalog_name"] - def main(): - args = process_arguments(afam_args_map, afam_mandatory_args) - workspace_client = get_workspace_api_client(args.profile) + args = process_arguments() + workspace_client = get_workspace_api_client(args["profile"]) dltmeta_afam_demo_runner = DLTMETAFCFDemo(args, workspace_client, "demo") print("initializing complete") runner_conf = dltmeta_afam_demo_runner.init_runner_conf() diff --git a/demo/notebooks/afam_cloudfiles_runners/init_dlt_meta_pipeline.py b/demo/notebooks/afam_cloudfiles_runners/init_dlt_meta_pipeline.py new file mode 100644 index 0000000..f69fdd1 --- /dev/null +++ b/demo/notebooks/afam_cloudfiles_runners/init_dlt_meta_pipeline.py @@ -0,0 +1,10 @@ +# Databricks notebook source +dlt_meta_whl = spark.conf.get("dlt_meta_whl") +%pip install $dlt_meta_whl + +# COMMAND ---------- + +layer = spark.conf.get("layer", None) + +from src.dataflow_pipeline import DataflowPipeline +DataflowPipeline.invoke_dlt_pipeline(spark, layer) diff --git a/demo/notebooks/afam_cloudfiles_runners/validate.py b/demo/notebooks/afam_cloudfiles_runners/validate.py new file mode 100644 index 0000000..c70d139 --- /dev/null +++ b/demo/notebooks/afam_cloudfiles_runners/validate.py @@ -0,0 +1,39 @@ +# Databricks notebook source +import pandas as pd + +run_id = dbutils.widgets.get("run_id") +uc_enabled = eval(dbutils.widgets.get("uc_enabled")) +uc_catalog_name = dbutils.widgets.get("uc_catalog_name") +bronze_schema = dbutils.widgets.get("bronze_schema") +silver_schema = dbutils.widgets.get("silver_schema") +output_file_path = dbutils.widgets.get("output_file_path") +log_list = [] + +# Assumption is that to get to this notebook Bronze and Silver completed successfully +log_list.append("Completed Bronze DLT Pipeline.") +log_list.append("Completed Silver DLT Pipeline.") + +UC_TABLES = { + f"{uc_catalog_name}.{bronze_schema}.transactions": 10002, + f"{uc_catalog_name}.{bronze_schema}.transactions_quarantine": 6, + f"{uc_catalog_name}.{bronze_schema}.customers": 51453, + f"{uc_catalog_name}.{bronze_schema}.customers_quarantine": 256, + f"{uc_catalog_name}.{silver_schema}.transactions": 8759, + f"{uc_catalog_name}.{silver_schema}.customers": 73212, +} + + +log_list.append("Validating DLT Bronze and Silver Table Counts...") +for table, counts in UC_TABLES.items(): + query = spark.sql(f"SELECT count(*) as cnt FROM {table}") + cnt = query.collect()[0].cnt + + log_list.append(f"Validating Counts for Table {table}.") + try: + assert int(cnt) == counts + log_list.append(f"Expected: {counts} Actual: {cnt}. Passed!") + except AssertionError: + log_list.append(f"Expected: {counts} Actual: {cnt}. Failed!") + +pd_df = pd.DataFrame(log_list) +pd_df.to_csv(output_file_path) From ec9a31769683d0dd5948597eb18781da23a5e403 Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Wed, 9 Oct 2024 15:28:03 -0400 Subject: [PATCH 49/59] formatting --- src/cli.py | 14 +-- tests/resources/onboarding.json | 169 +++++++++++++++++++++++++++++++- 2 files changed, 175 insertions(+), 8 deletions(-) diff --git a/src/cli.py b/src/cli.py index 6e9fe51..f35a870 100644 --- a/src/cli.py +++ b/src/cli.py @@ -191,13 +191,13 @@ def copy_to_dbfs(self, src, dst): self._ws.dbfs.upload(dbfs_path, contents, overwrite=True) def create_uc_volume(self, uc_catalog_name, dlt_meta_schema): - volume_info = self._ws.volumes.create(catalog_name=uc_catalog_name, - schema_name=dlt_meta_schema, - name=dlt_meta_schema, - volume_type=VolumeType.MANAGED) - return (f"/Volumes/{volume_info.catalog_name}/" - f"{volume_info.schema_name}/{volume_info.name}/" - ) + volume_info = self._ws.volumes.create( + catalog_name=uc_catalog_name, + schema_name=dlt_meta_schema, + name=dlt_meta_schema, + volume_type=VolumeType.MANAGED, + ) + return f"/Volumes/{volume_info.catalog_name}/{volume_info.schema_name}/{volume_info.name}/" def onboard(self, cmd: OnboardCommand): """Perform the onboarding process.""" diff --git a/tests/resources/onboarding.json b/tests/resources/onboarding.json index 791e7b6..9ccab56 100644 --- a/tests/resources/onboarding.json +++ b/tests/resources/onboarding.json @@ -1 +1,168 @@ -[{"data_flow_id": "100", "data_flow_group": "A1", "source_system": "MYSQL", "source_format": "cloudFiles", "source_details": {"source_database": "APP", "source_table": "CUSTOMERS", "source_path_dev": "tests/resources/data/customers", "source_schema_path": "tests/resources/schema/customer_schema.ddl", "source_metadata": {"include_autoloader_metadata_column": "True", "autoloader_metadata_col_name": "source_metadata", "select_metadata_cols": {"input_file_name": "_metadata.file_name", "input_file_path": "_metadata.file_path"}}}, "bronze_database_dev": "bronze", "bronze_database_staging": "bronze", "bronze_database_prd": "bronze", "bronze_table": "customers_cdc", "bronze_reader_options": {"cloudFiles.format": "json", "cloudFiles.inferColumnTypes": "true", "cloudFiles.rescuedDataColumn": "_rescued_data"}, "bronze_table_path_dev": "tests/resources/delta/customers", "bronze_table_properties": {"pipelines.autoOptimize.managed": "false", "pipelines.reset.allowed": "false"}, "bronze_data_quality_expectations_json_dev": "tests/resources/dqe/customers/bronze_data_quality_expectations.json", "silver_database_dev": "silver", "silver_database_staging": "silver", "silver_database_prd": "silver", "silver_table": "customers", "silver_cdc_apply_changes": {"keys": ["id"], "sequence_by": "operation_date", "scd_type": "1", "apply_as_deletes": "operation = 'DELETE'", "except_column_list": ["operation", "operation_date", "_rescued_data"]}, "silver_table_path_dev": "tests/resources/data/silver/customers", "silver_table_properties": {"pipelines.autoOptimize.managed": "false", "pipelines.reset.allowed": "false", "pipelines.autoOptimize.zOrderCols": "id,email"}, "silver_transformation_json_dev": "tests/resources/silver_transformations.json", "silver_data_quality_expectations_json_dev": "tests/resources/dqe/customers/silver_data_quality_expectations.json"}, {"data_flow_id": "101", "data_flow_group": "A1", "source_system": "MYSQL", "source_format": "cloudFiles", "source_details": {"source_database": "APP", "source_table": "TRANSACTIONS", "source_path_prd": "tests/resources/data/transactions", "source_path_dev": "tests/resources/data/transactions", "source_metadata": {"include_autoloader_metadata_column": "True"}}, "bronze_database_dev": "bronze", "bronze_database_staging": "bronze", "bronze_database_prd": "bronze", "bronze_table": "transactions_cdc", "bronze_reader_options": {"cloudFiles.format": "json", "cloudFiles.inferColumnTypes": "true", "cloudFiles.rescuedDataColumn": "_rescued_data"}, "bronze_table_path_dev": "tests/resources/delta/transactions", "bronze_table_path_staging": "s3://db-dlt-meta-staging/demo/data/bronze/transactions", "bronze_table_path_prd": "s3://db-dlt-meta-prod/demo/data/bronze/transactions", "bronze_table_properties": {"pipelines.reset.allowed": "false"}, "bronze_data_quality_expectations_json_dev": "tests/resources/dqe/transactions/bronze_data_quality_expectations.json", "bronze_database_quarantine_dev": "bronze", "bronze_database_quarantine_staging": "bronze", "bronze_database_quarantine_prd": "bronze", "bronze_quarantine_table": "transactions_cdc_quarantine", "bronze_quarantine_table_path_dev": "tests/resources/data/bronze/transactions_quarantine", "silver_database_dev": "silver", "silver_database_preprd": "silver", "silver_database_prd": "silver", "silver_table": "transactions", "silver_cdc_apply_changes": {"keys": ["id"], "sequence_by": "operation_date", "scd_type": "1", "apply_as_deletes": "operation = 'DELETE'", "except_column_list": ["operation", "operation_date", "_rescued_data"]}, "silver_partition_columns": "transaction_date", "silver_table_path_dev": "tests/resources/data/silver/transactions", "silver_transformation_json_dev": "tests/resources/silver_transformations.json", "silver_table_properties": {"pipelines.reset.allowed": "false", "pipelines.autoOptimize.zOrderCols": "id, customer_id"}, "silver_data_quality_expectations_json_dev": "tests/resources/dqe/transactions/silver_data_quality_expectations.json"}, {"data_flow_id": "103", "data_flow_group": "A2", "source_system": "MYSQL", "source_format": "eventhub", "source_details": {"source_schema_path": "tests/resources/schema/eventhub_iot_schema.ddl", "eventhub.accessKeyName": "iotIngestionAccessKey", "eventhub.name": "iot", "eventhub.accessKeySecretName": "iotIngestionAccessKey", "eventhub.secretsScopeName": "eventhubs_creds", "kafka.sasl.mechanism": "PLAIN", "kafka.security.protocol": "SASL_SSL", "kafka.bootstrap.servers": "standard.servicebus.windows.net:9093"}, "bronze_database_dev": "bronze", "bronze_database_staging": "bronze", "bronze_database_prd": "bronze", "bronze_table": "iot_cdc", "bronze_reader_options": {"maxOffsetsPerTrigger": "50000", "startingOffsets": "latest", "failOnDataLoss": "false", "kafka.request.timeout.ms": "60000", "kafka.session.timeout.ms": "60000"}, "bronze_table_path_dev": "tests/resources/delta/iot_cdc", "bronze_table_path_staging": "s3://db-dlt-meta-staging/demo/data/bronze/iot_cdc", "bronze_table_path_prd": "s3://db-dlt-meta-prod/demo/data/bronze/iot_cdc", "bronze_data_quality_expectations_json_dev": "tests/resources/dqe/iot_cdc/bronze_data_quality_expectations.json", "silver_database_dev": "silver", "silver_table": "iot_cdc", "silver_cdc_apply_changes": {"keys": ["device_id"], "sequence_by": "timestamp", "scd_type": "1", "apply_as_deletes": "operation = 'DELETE'", "except_column_list": []}, "silver_table_path_dev": "tests/resources/data/silver/iot_cdc", "silver_transformation_json_dev": "tests/resources/silver_transformations.json", "silver_data_quality_expectations_json_dev": "tests/resources/dqe/iot_cdc/silver_data_quality_expectations.json"}] \ No newline at end of file +[ + { + "data_flow_id": "100", + "data_flow_group": "A1", + "source_system": "MYSQL", + "source_format": "cloudFiles", + "source_details": { + "source_database": "APP", + "source_table": "CUSTOMERS", + "source_path_dev": "tests/resources/data/customers", + "source_schema_path": "tests/resources/schema/customer_schema.ddl", + "source_metadata": { + "include_autoloader_metadata_column": "True", + "autoloader_metadata_col_name": "source_metadata", + "select_metadata_cols": { + "input_file_name": "_metadata.file_name", + "input_file_path": "_metadata.file_path" + } + } + }, + "bronze_database_dev": "bronze", + "bronze_database_staging": "bronze", + "bronze_database_prd": "bronze", + "bronze_table": "customers_cdc", + "bronze_reader_options": { + "cloudFiles.format": "json", + "cloudFiles.inferColumnTypes": "true", + "cloudFiles.rescuedDataColumn": "_rescued_data" + }, + "bronze_table_path_dev": "tests/resources/delta/customers", + "bronze_table_properties": { + "pipelines.autoOptimize.managed": "false", + "pipelines.reset.allowed": "false" + }, + "bronze_data_quality_expectations_json_dev": "tests/resources/dqe/customers/bronze_data_quality_expectations.json", + "silver_database_dev": "silver", + "silver_database_staging": "silver", + "silver_database_prd": "silver", + "silver_table": "customers", + "silver_cdc_apply_changes": { + "keys": [ + "id" + ], + "sequence_by": "operation_date", + "scd_type": "1", + "apply_as_deletes": "operation = 'DELETE'", + "except_column_list": [ + "operation", + "operation_date", + "_rescued_data" + ] + }, + "silver_table_path_dev": "tests/resources/data/silver/customers", + "silver_table_properties": { + "pipelines.autoOptimize.managed": "false", + "pipelines.reset.allowed": "false", + "pipelines.autoOptimize.zOrderCols": "id,email" + }, + "silver_transformation_json_dev": "tests/resources/silver_transformations.json", + "silver_data_quality_expectations_json_dev": "tests/resources/dqe/customers/silver_data_quality_expectations.json" + }, + { + "data_flow_id": "101", + "data_flow_group": "A1", + "source_system": "MYSQL", + "source_format": "cloudFiles", + "source_details": { + "source_database": "APP", + "source_table": "TRANSACTIONS", + "source_path_prd": "tests/resources/data/transactions", + "source_path_dev": "tests/resources/data/transactions", + "source_metadata": { + "include_autoloader_metadata_column": "True" + } + }, + "bronze_database_dev": "bronze", + "bronze_database_staging": "bronze", + "bronze_database_prd": "bronze", + "bronze_table": "transactions_cdc", + "bronze_reader_options": { + "cloudFiles.format": "json", + "cloudFiles.inferColumnTypes": "true", + "cloudFiles.rescuedDataColumn": "_rescued_data" + }, + "bronze_table_path_dev": "tests/resources/delta/transactions", + "bronze_table_path_staging": "s3://db-dlt-meta-staging/demo/data/bronze/transactions", + "bronze_table_path_prd": "s3://db-dlt-meta-prod/demo/data/bronze/transactions", + "bronze_table_properties": { + "pipelines.reset.allowed": "false" + }, + "bronze_data_quality_expectations_json_dev": "tests/resources/dqe/transactions/bronze_data_quality_expectations.json", + "bronze_database_quarantine_dev": "bronze", + "bronze_database_quarantine_staging": "bronze", + "bronze_database_quarantine_prd": "bronze", + "bronze_quarantine_table": "transactions_cdc_quarantine", + "bronze_quarantine_table_path_dev": "tests/resources/data/bronze/transactions_quarantine", + "silver_database_dev": "silver", + "silver_database_preprd": "silver", + "silver_database_prd": "silver", + "silver_table": "transactions", + "silver_cdc_apply_changes": { + "keys": [ + "id" + ], + "sequence_by": "operation_date", + "scd_type": "1", + "apply_as_deletes": "operation = 'DELETE'", + "except_column_list": [ + "operation", + "operation_date", + "_rescued_data" + ] + }, + "silver_partition_columns": "transaction_date", + "silver_table_path_dev": "tests/resources/data/silver/transactions", + "silver_transformation_json_dev": "tests/resources/silver_transformations.json", + "silver_table_properties": { + "pipelines.reset.allowed": "false", + "pipelines.autoOptimize.zOrderCols": "id, customer_id" + }, + "silver_data_quality_expectations_json_dev": "tests/resources/dqe/transactions/silver_data_quality_expectations.json" + }, + { + "data_flow_id": "103", + "data_flow_group": "A2", + "source_system": "MYSQL", + "source_format": "eventhub", + "source_details": { + "source_schema_path": "tests/resources/schema/eventhub_iot_schema.ddl", + "eventhub.accessKeyName": "iotIngestionAccessKey", + "eventhub.name": "iot", + "eventhub.accessKeySecretName": "iotIngestionAccessKey", + "eventhub.secretsScopeName": "eventhubs_creds", + "kafka.sasl.mechanism": "PLAIN", + "kafka.security.protocol": "SASL_SSL", + "kafka.bootstrap.servers": "standard.servicebus.windows.net:9093" + }, + "bronze_database_dev": "bronze", + "bronze_database_staging": "bronze", + "bronze_database_prd": "bronze", + "bronze_table": "iot_cdc", + "bronze_reader_options": { + "maxOffsetsPerTrigger": "50000", + "startingOffsets": "latest", + "failOnDataLoss": "false", + "kafka.request.timeout.ms": "60000", + "kafka.session.timeout.ms": "60000" + }, + "bronze_table_path_dev": "tests/resources/delta/iot_cdc", + "bronze_table_path_staging": "s3://db-dlt-meta-staging/demo/data/bronze/iot_cdc", + "bronze_table_path_prd": "s3://db-dlt-meta-prod/demo/data/bronze/iot_cdc", + "bronze_data_quality_expectations_json_dev": "tests/resources/dqe/iot_cdc/bronze_data_quality_expectations.json", + "silver_database_dev": "silver", + "silver_table": "iot_cdc", + "silver_cdc_apply_changes": { + "keys": [ + "device_id" + ], + "sequence_by": "timestamp", + "scd_type": "1", + "apply_as_deletes": "operation = 'DELETE'", + "except_column_list": [] + }, + "silver_table_path_dev": "tests/resources/data/silver/iot_cdc", + "silver_transformation_json_dev": "tests/resources/silver_transformations.json", + "silver_data_quality_expectations_json_dev": "tests/resources/dqe/iot_cdc/silver_data_quality_expectations.json" + } +] \ No newline at end of file From ae27548be2fd5495136503f16d2bd7d8eeb8b80a Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Wed, 9 Oct 2024 15:33:52 -0400 Subject: [PATCH 50/59] formatting --- src/__main__.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/__main__.py b/src/__main__.py index 2bb2d33..a3702a5 100644 --- a/src/__main__.py +++ b/src/__main__.py @@ -7,19 +7,20 @@ logger = logging.getLogger("dlt-meta") logger.setLevel(logging.INFO) -arguments = ["--onboard_layer", - "--onboarding_file_path", - "--database", - "--env", - "--bronze_dataflowspec_table", - "--bronze_dataflowspec_path", - "--silver_dataflowspec_table", - "--silver_dataflowspec_path", - "--import_author", - "--version", - "--overwrite", - "--uc_enabled", - ] +arguments = [ + "--onboard_layer", + "--onboarding_file_path", + "--database", + "--env", + "--bronze_dataflowspec_table", + "--bronze_dataflowspec_path", + "--silver_dataflowspec_table", + "--silver_dataflowspec_path", + "--import_author", + "--version", + "--overwrite", + "--uc_enabled", +] def parse_args(): From a60d0e224bb416751d91f5626733a59d20c71f47 Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Thu, 10 Oct 2024 15:13:36 -0400 Subject: [PATCH 51/59] linting fixes --- demo/launch_dais_demo.py | 3 +- .../init_dlt_meta_pipeline.py | 2 +- .../init_dlt_meta_pipeline.py | 2 +- .../eventhub_runners/publish_events.py | 49 +++++++++++-------- .../notebooks/eventhub_runners/validate.py | 6 +-- .../kafka_runners/init_dlt_meta_pipeline.py | 2 +- .../notebooks/kafka_runners/publish_events.py | 20 +++++--- .../notebooks/kafka_runners/validate.py | 4 +- tests/test_cli.py | 2 +- 9 files changed, 53 insertions(+), 37 deletions(-) diff --git a/demo/launch_dais_demo.py b/demo/launch_dais_demo.py index 06251c5..fbed696 100644 --- a/demo/launch_dais_demo.py +++ b/demo/launch_dais_demo.py @@ -6,7 +6,8 @@ DLTMETARunner, DLTMetaRunnerConf, get_workspace_api_client, - process_arguments + process_arguments, + cloud_node_type_id_dict ) diff --git a/integration_tests/notebooks/cloudfile_runners/init_dlt_meta_pipeline.py b/integration_tests/notebooks/cloudfile_runners/init_dlt_meta_pipeline.py index f69fdd1..5ed2c1f 100644 --- a/integration_tests/notebooks/cloudfile_runners/init_dlt_meta_pipeline.py +++ b/integration_tests/notebooks/cloudfile_runners/init_dlt_meta_pipeline.py @@ -1,6 +1,6 @@ # Databricks notebook source dlt_meta_whl = spark.conf.get("dlt_meta_whl") -%pip install $dlt_meta_whl +%pip install $dlt_meta_whl # noqa : E999 # COMMAND ---------- diff --git a/integration_tests/notebooks/eventhub_runners/init_dlt_meta_pipeline.py b/integration_tests/notebooks/eventhub_runners/init_dlt_meta_pipeline.py index e1e579b..e91d139 100644 --- a/integration_tests/notebooks/eventhub_runners/init_dlt_meta_pipeline.py +++ b/integration_tests/notebooks/eventhub_runners/init_dlt_meta_pipeline.py @@ -1,6 +1,6 @@ # Databricks notebook source dlt_meta_whl = spark.conf.get("dlt_meta_whl") -%pip install $dlt_meta_whl +%pip install $dlt_meta_whl # noqa : E999 # COMMAND ---------- diff --git a/integration_tests/notebooks/eventhub_runners/publish_events.py b/integration_tests/notebooks/eventhub_runners/publish_events.py index e5ac556..410b6f8 100644 --- a/integration_tests/notebooks/eventhub_runners/publish_events.py +++ b/integration_tests/notebooks/eventhub_runners/publish_events.py @@ -12,13 +12,15 @@ # COMMAND ---------- -dbutils.widgets.text("eventhub_name","eventhub_name","") -dbutils.widgets.text("eventhub_name_append_flow","eventhub_name_append_flow","") -dbutils.widgets.text("eventhub_namespace","eventhub_namespace","") -dbutils.widgets.text("eventhub_secrets_scope_name","eventhub_secrets_scope_name","") -dbutils.widgets.text("eventhub_accesskey_name","eventhub_accesskey_name","") -dbutils.widgets.text("eventhub_input_data","eventhub_input_data","") -dbutils.widgets.text("eventhub_append_flow_input_data","eventhub_append_flow_input_data","") +dbutils.widgets.text("eventhub_name", "eventhub_name", "") +dbutils.widgets.text("eventhub_name_append_flow", "eventhub_name_append_flow", "") +dbutils.widgets.text("eventhub_namespace", "eventhub_namespace", "") +dbutils.widgets.text("eventhub_secrets_scope_name", "eventhub_secrets_scope_name", "") +dbutils.widgets.text("eventhub_accesskey_name", "eventhub_accesskey_name", "") +dbutils.widgets.text("eventhub_input_data", "eventhub_input_data", "") +dbutils.widgets.text( + "eventhub_append_flow_input_data", "eventhub_append_flow_input_data", "" +) # COMMAND ---------- @@ -32,18 +34,23 @@ # COMMAND ---------- -print(f"eventhub_name={eventhub_name}, eventhub_name_append_flow={eventhub_name_append_flow}, eventhub_namespace={eventhub_namespace}, eventhub_secrets_scope_name={eventhub_secrets_scope_name}, eventhub_accesskey_name={eventhub_accesskey_name}, eventhub_input_data={eventhub_input_data}, eventhub_append_flow_input_data={eventhub_append_flow_input_data}") +print( + f"eventhub_name={eventhub_name}, eventhub_name_append_flow={eventhub_name_append_flow}, eventhub_namespace={eventhub_namespace}, eventhub_secrets_scope_name={eventhub_secrets_scope_name}, eventhub_accesskey_name={eventhub_accesskey_name}, eventhub_input_data={eventhub_input_data}, eventhub_append_flow_input_data={eventhub_append_flow_input_data}" +) # COMMAND ---------- import json from azure.eventhub import EventHubProducerClient, EventData -eventhub_shared_access_value = dbutils.secrets.get(scope = eventhub_secrets_scope_name, key = eventhub_accesskey_name) +eventhub_shared_access_value = dbutils.secrets.get( + scope=eventhub_secrets_scope_name, key=eventhub_accesskey_name +) eventhub_conn = f"Endpoint=sb://{eventhub_namespace}.servicebus.windows.net/;SharedAccessKeyName={eventhub_accesskey_name};SharedAccessKey={eventhub_shared_access_value}" -client = EventHubProducerClient.from_connection_string(eventhub_conn, eventhub_name=eventhub_name) - +client = EventHubProducerClient.from_connection_string( + eventhub_conn, eventhub_name=eventhub_name +) # COMMAND ---------- @@ -57,20 +64,22 @@ data = json.load(f) for event in data: - event_data_batch = client.create_batch() - event_data_batch.add(EventData(json.dumps(event))) - with client: - client.send_batch(event_data_batch) + event_data_batch = client.create_batch() + event_data_batch.add(EventData(json.dumps(event))) + with client: + client.send_batch(event_data_batch) # COMMAND ---------- -append_flow_client = EventHubProducerClient.from_connection_string(eventhub_conn, eventhub_name=eventhub_name_append_flow) +append_flow_client = EventHubProducerClient.from_connection_string( + eventhub_conn, eventhub_name=eventhub_name_append_flow +) with open(f"{eventhub_append_flow_input_data}") as f: af_data = json.load(f) for event in af_data: - event_data_batch = client.create_batch() - event_data_batch.add(EventData(json.dumps(event))) - with client: - append_flow_client.send_batch(event_data_batch) \ No newline at end of file + event_data_batch = client.create_batch() + event_data_batch.add(EventData(json.dumps(event))) + with client: + append_flow_client.send_batch(event_data_batch) diff --git a/integration_tests/notebooks/eventhub_runners/validate.py b/integration_tests/notebooks/eventhub_runners/validate.py index 4e803f8..3a8a718 100644 --- a/integration_tests/notebooks/eventhub_runners/validate.py +++ b/integration_tests/notebooks/eventhub_runners/validate.py @@ -13,12 +13,12 @@ UC_TABLES = { f"{uc_catalog_name}.{bronze_schema}.bronze_{run_id}_iot": 20, - f"{uc_catalog_name}.{bronze_schema}.bronze_{run_id}_iot_quarantine": 2 + f"{uc_catalog_name}.{bronze_schema}.bronze_{run_id}_iot_quarantine": 2, } NON_UC_TABLES = { f"{uc_catalog_name}.{bronze_schema}.bronze_{run_id}_iot": 20, - f"{uc_catalog_name}.{bronze_schema}.bronze_{run_id}_iot_quarantine": 2 + f"{uc_catalog_name}.{bronze_schema}.bronze_{run_id}_iot_quarantine": 2, } log_list.append("Validating DLT EVenthub Bronze Table Counts...") @@ -35,4 +35,4 @@ log_list.append(f"Expected > {counts} Actual: {cnt}. Failed!") pd_df = pd.DataFrame(log_list) -pd_df.to_csv(output_file_path) \ No newline at end of file +pd_df.to_csv(output_file_path) diff --git a/integration_tests/notebooks/kafka_runners/init_dlt_meta_pipeline.py b/integration_tests/notebooks/kafka_runners/init_dlt_meta_pipeline.py index e1e579b..e91d139 100644 --- a/integration_tests/notebooks/kafka_runners/init_dlt_meta_pipeline.py +++ b/integration_tests/notebooks/kafka_runners/init_dlt_meta_pipeline.py @@ -1,6 +1,6 @@ # Databricks notebook source dlt_meta_whl = spark.conf.get("dlt_meta_whl") -%pip install $dlt_meta_whl +%pip install $dlt_meta_whl # noqa : E999 # COMMAND ---------- diff --git a/integration_tests/notebooks/kafka_runners/publish_events.py b/integration_tests/notebooks/kafka_runners/publish_events.py index 03bf6e6..74ae478 100644 --- a/integration_tests/notebooks/kafka_runners/publish_events.py +++ b/integration_tests/notebooks/kafka_runners/publish_events.py @@ -10,20 +10,26 @@ # COMMAND ---------- # DBTITLE 1,Extract input from notebook params -dbutils.widgets.text("kafka_topic","kafka_topic","") -dbutils.widgets.text("kafka_broker","kafka_broker","") -dbutils.widgets.text("kafka_input_data","kafka_input_data","") +dbutils.widgets.text("kafka_topic", "kafka_topic", "") +dbutils.widgets.text("kafka_broker", "kafka_broker", "") +dbutils.widgets.text("kafka_input_data", "kafka_input_data", "") kafka_topic = dbutils.widgets.get("kafka_topic") kafka_broker = dbutils.widgets.get("kafka_broker") kafka_input_data = dbutils.widgets.get("kafka_input_data") -print(f"kafka_topic={kafka_topic}, kafka_broker={kafka_broker}, kafka_input_data={kafka_input_data}") +print( + f"kafka_topic={kafka_topic}, kafka_broker={kafka_broker}, kafka_input_data={kafka_input_data}" +) # COMMAND ---------- # DBTITLE 1,Initialize kafka producer from kafka import KafkaProducer import json -producer = KafkaProducer(bootstrap_servers=kafka_broker, value_serializer=lambda v: json.dumps(v).encode('utf-8')) + +producer = KafkaProducer( + bootstrap_servers=kafka_broker, + value_serializer=lambda v: json.dumps(v).encode("utf-8"), +) # COMMAND ---------- @@ -32,6 +38,6 @@ data = json.load(f) for event in data: - producer.send(kafka_topic,event) + producer.send(kafka_topic, event) -producer.close() \ No newline at end of file +producer.close() diff --git a/integration_tests/notebooks/kafka_runners/validate.py b/integration_tests/notebooks/kafka_runners/validate.py index 58bb43e..0785eac 100644 --- a/integration_tests/notebooks/kafka_runners/validate.py +++ b/integration_tests/notebooks/kafka_runners/validate.py @@ -12,7 +12,7 @@ TABLES = { f"{uc_catalog_name}.{bronze_schema}.bronze_it_{run_id}_iot": 20, - f"{uc_catalog_name}.{bronze_schema}.bronze_it_{run_id}_iot_quarantine": 2 + f"{uc_catalog_name}.{bronze_schema}.bronze_it_{run_id}_iot_quarantine": 2, } log_list.append("Validating DLT EVenthub Bronze Table Counts...") @@ -28,5 +28,5 @@ log_list.append(f"Expected {counts} Actual: {cnt}. Failed!") pd_df = pd.DataFrame(log_list) -log_file =f"{uc_volume_path}/integration-test-output.csv" +log_file = f"{uc_volume_path}/integration-test-output.csv" pd_df.to_csv(log_file) diff --git a/tests/test_cli.py b/tests/test_cli.py index 6d756ae..0b6454b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -53,7 +53,7 @@ def test_copy(self): mock_open.return_value = MagicMock() mock_dbfs_upload = MagicMock() mock_ws.dbfs.upload = mock_dbfs_upload - dltmeta.copy("file:/path/to/src", "/dbfs/path/to/dst") + dltmeta.copy_to_dbfs("file:/path/to/src", "/dbfs/path/to/dst") self.assertEqual(mock_dbfs_upload.call_count, 3) @patch('src.cli.WorkspaceClient') From 7b17edfc23fea3e8b4117e38b6da49093d2ce050 Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Mon, 14 Oct 2024 10:37:05 -0400 Subject: [PATCH 52/59] formatting and linting --- .flake8 | 2 +- demo/launch_af_cloudfiles_demo.py | 4 +- .../init_dlt_meta_pipeline.py | 2 +- .../eventhub_runners/publish_events.py | 10 +- integration_tests/run_integration_tests.py | 9 +- src/install.py | 67 ++- src/onboard_dataflowspec.py | 474 ++++++++++++------ 7 files changed, 401 insertions(+), 167 deletions(-) diff --git a/.flake8 b/.flake8 index c817b88..7421ff9 100644 --- a/.flake8 +++ b/.flake8 @@ -1,6 +1,6 @@ [flake8] ignore = BLK100,E402,W503 -exclude = .git,__pycache__,docs/source/conf.py,old,build,dist,dist,.eggs +exclude = .git,__pycache__,docs/source/conf.py,old,build,dist,dist,.eggs,integration_tests/notebooks/*/*.py,demo/notebooks/*/*.py,.venv builtins = dlt,dbutils,spark,display,log_integration_test,pyspark.dbutils max-line-length = 120 per-file-ignores = diff --git a/demo/launch_af_cloudfiles_demo.py b/demo/launch_af_cloudfiles_demo.py index 4bc0649..a22e6a7 100644 --- a/demo/launch_af_cloudfiles_demo.py +++ b/demo/launch_af_cloudfiles_demo.py @@ -30,6 +30,7 @@ def run(self, runner_conf: DLTMetaRunnerConf): self.create_bronze_silver_dlt(runner_conf) self.launch_workflow(runner_conf) except Exception as e: + print(e) traceback.print_exc() def init_runner_conf(self) -> DLTMetaRunnerConf: @@ -57,7 +58,7 @@ def init_runner_conf(self) -> DLTMetaRunnerConf: onboarding_file_path="demo/conf/onboarding.json", onboarding_A2_file_path="demo/conf/onboarding_A2.json", env="demo", - runners_full_local_path = './demo/notebooks/afam_cloudfiles_runners/', + runners_full_local_path='./demo/notebooks/afam_cloudfiles_runners/', test_output_file_path=( f"/Users/{self.wsi._my_username}/dlt_meta_demo/" f"{run_id}/demo-output.csv" @@ -71,7 +72,6 @@ def launch_workflow(self, runner_conf: DLTMetaRunnerConf): self.open_job_url(runner_conf, created_job) - def main(): args = process_arguments() workspace_client = get_workspace_api_client(args["profile"]) diff --git a/demo/notebooks/afam_cloudfiles_runners/init_dlt_meta_pipeline.py b/demo/notebooks/afam_cloudfiles_runners/init_dlt_meta_pipeline.py index f69fdd1..5ed2c1f 100644 --- a/demo/notebooks/afam_cloudfiles_runners/init_dlt_meta_pipeline.py +++ b/demo/notebooks/afam_cloudfiles_runners/init_dlt_meta_pipeline.py @@ -1,6 +1,6 @@ # Databricks notebook source dlt_meta_whl = spark.conf.get("dlt_meta_whl") -%pip install $dlt_meta_whl +%pip install $dlt_meta_whl # noqa : E999 # COMMAND ---------- diff --git a/integration_tests/notebooks/eventhub_runners/publish_events.py b/integration_tests/notebooks/eventhub_runners/publish_events.py index 410b6f8..e7deb0e 100644 --- a/integration_tests/notebooks/eventhub_runners/publish_events.py +++ b/integration_tests/notebooks/eventhub_runners/publish_events.py @@ -35,7 +35,13 @@ # COMMAND ---------- print( - f"eventhub_name={eventhub_name}, eventhub_name_append_flow={eventhub_name_append_flow}, eventhub_namespace={eventhub_namespace}, eventhub_secrets_scope_name={eventhub_secrets_scope_name}, eventhub_accesskey_name={eventhub_accesskey_name}, eventhub_input_data={eventhub_input_data}, eventhub_append_flow_input_data={eventhub_append_flow_input_data}" + f"""eventhub_name={eventhub_name}, + eventhub_name_append_flow={eventhub_name_append_flow}, + eventhub_namespace={eventhub_namespace}, + eventhub_secrets_scope_name={eventhub_secrets_scope_name}, + eventhub_accesskey_name={eventhub_accesskey_name}, + eventhub_input_data={eventhub_input_data}, + eventhub_append_flow_input_data={eventhub_append_flow_input_data}""" ) # COMMAND ---------- @@ -46,7 +52,7 @@ eventhub_shared_access_value = dbutils.secrets.get( scope=eventhub_secrets_scope_name, key=eventhub_accesskey_name ) -eventhub_conn = f"Endpoint=sb://{eventhub_namespace}.servicebus.windows.net/;SharedAccessKeyName={eventhub_accesskey_name};SharedAccessKey={eventhub_shared_access_value}" +eventhub_conn = f"Endpoint=sb://{eventhub_namespace}.servicebus.windows.net/;SharedAccessKeyName={eventhub_accesskey_name};SharedAccessKey={eventhub_shared_access_value}" # noqa : E501 client = EventHubProducerClient.from_connection_string( eventhub_conn, eventhub_name=eventhub_name diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index 9a4fce0..5a2b25e 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -367,7 +367,7 @@ def create_workflow_spec(self, runner_conf: DLTMetaRunnerConf): named_parameters={ "onboard_layer": "bronze", "database": f"{runner_conf.uc_catalog_name}.{runner_conf.dlt_meta_schema}", - "onboarding_file_path": f"{runner_conf.uc_volume_path}/{self.base_dir}/conf/onboarding_A2.json", + "onboarding_file_path": f"{runner_conf.uc_volume_path}/{self.base_dir}/conf/onboarding_A2.json", # noqa : E501 "bronze_dataflowspec_table": "bronze_dataflowspec_cdc", "import_author": "Ravi", "version": "v1", @@ -404,14 +404,14 @@ def create_workflow_spec(self, runner_conf: DLTMetaRunnerConf): "eventhub_namespace": runner_conf.eventhub_namespace, "eventhub_secrets_scope_name": runner_conf.eventhub_secrets_scope_name, "eventhub_accesskey_name": runner_conf.eventhub_producer_accesskey_name, - "eventhub_input_data": f"/{runner_conf.uc_volume_path}/{self.base_dir}/resources/data/iot/iot.json", - "eventhub_append_flow_input_data": f"/{runner_conf.uc_volume_path}/{self.base_dir}/resources/data/iot_eventhub_af/iot.json", + "eventhub_input_data": f"/{runner_conf.uc_volume_path}/{self.base_dir}/resources/data/iot/iot.json", # noqa : E501 + "eventhub_append_flow_input_data": f"/{runner_conf.uc_volume_path}/{self.base_dir}/resources/data/iot_eventhub_af/iot.json",# noqa : E501 } case "kafka": base_parameters = { "kafka_topic": runner_conf.kafka_topic, "kafka_broker": runner_conf.kafka_broker, - "kafka_input_data": f"/{runner_conf.uc_volume_path}/{self.base_dir}/resources/data/iot/iot.json", + "kafka_input_data": f"/{runner_conf.uc_volume_path}/{self.base_dir}/resources/data/iot/iot.json", # noqa : E501 } tasks.append( @@ -676,6 +676,7 @@ def run(self, runner_conf: DLTMetaRunnerConf): self.launch_workflow(runner_conf) self.download_test_results(runner_conf) except Exception as e: + print(e) traceback.print_exc() finally: self.clean_up(runner_conf) diff --git a/src/install.py b/src/install.py index a1c698e..43977f9 100644 --- a/src/install.py +++ b/src/install.py @@ -7,20 +7,23 @@ from dataclasses import replace from pathlib import Path from typing import Any + from databricks.sdk import WorkspaceClient from databricks.sdk.core import DatabricksError -from databricks.sdk.service.workspace import ImportFormat from databricks.sdk.service import compute from databricks.sdk.service.sql import EndpointInfoWarehouseType -from src.config import WorkspaceConfig -from src.__about__ import __version__ +from databricks.sdk.service.workspace import ImportFormat +from src.__about__ import __version__ +from src.config import WorkspaceConfig -logger = logging.getLogger('databricks.labs.dltmeta') +logger = logging.getLogger("databricks.labs.dltmeta") class WorkspaceInstaller: - def __init__(self, ws: WorkspaceClient, *, prefix: str = "dlt-meta", promtps: bool = True): + def __init__( + self, ws: WorkspaceClient, *, prefix: str = "dlt-meta", promtps: bool = True + ): if "DATABRICKS_RUNTIME_VERSION" in os.environ: msg = "WorkspaceInstaller is not supposed to be executed in Databricks Runtime" raise SystemExit(msg) @@ -39,7 +42,11 @@ def run(self): def _warehouse_id(self) -> str: if self._current_config.warehouse_id is not None: return self._current_config.warehouse_id - warehouses = [_ for _ in self._ws.warehouses.list() if _.warehouse_type == EndpointInfoWarehouseType.PRO] + warehouses = [ + _ + for _ in self._ws.warehouses.list() + if _.warehouse_type == EndpointInfoWarehouseType.PRO + ] warehouse_id = self._current_config.warehouse_id if not warehouse_id and not warehouses: msg = "need either configured warehouse_id or an existing PRO SQL warehouse" @@ -93,7 +100,7 @@ def _configure(self): @property def _app(self): - return 'dlt-meta' + return "dlt-meta" @property def _version(self): @@ -147,7 +154,16 @@ def _build_wheel(self, tmp_dir: str, *, verbose: bool = False): project_root = tmp_dir_path logger.debug(f"Building wheel for {project_root} in {tmp_dir}") subprocess.run( - [sys.executable, "-m", "pip", "wheel", "--no-deps", "--wheel-dir", tmp_dir, project_root], + [ + sys.executable, + "-m", + "pip", + "wheel", + "--no-deps", + "--wheel-dir", + tmp_dir, + project_root, + ], **streams, check=True, ) @@ -175,18 +191,35 @@ def _cluster_node_type(self, spec: compute.ClusterSpec) -> compute.ClusterSpec: cfg = self._current_config if cfg.instance_pool_id is not None: return replace(spec, instance_pool_id=cfg.instance_pool_id) - spec = replace(spec, node_type_id=self._ws.clusters.select_node_type(local_disk=True)) + spec = replace( + spec, node_type_id=self._ws.clusters.select_node_type(local_disk=True) + ) if self._ws.config.is_aws: - return replace(spec, aws_attributes=compute.AwsAttributes(availability=compute.AwsAvailability.ON_DEMAND)) + return replace( + spec, + aws_attributes=compute.AwsAttributes( + availability=compute.AwsAvailability.ON_DEMAND + ), + ) if self._ws.config.is_azure: return replace( - spec, azure_attributes=compute.AzureAttributes(availability=compute.AzureAvailability.ON_DEMAND_AZURE) + spec, + azure_attributes=compute.AzureAttributes( + availability=compute.AzureAvailability.ON_DEMAND_AZURE + ), ) - return replace(spec, gcp_attributes=compute.GcpAttributes(availability=compute.GcpAvailability.ON_DEMAND_GCP)) + return replace( + spec, + gcp_attributes=compute.GcpAttributes( + availability=compute.GcpAvailability.ON_DEMAND_GCP + ), + ) @staticmethod def _question(text: str, *, default: str = None) -> str: - default_help = "" if default is None else f"\033[36m (default: {default})\033[0m" + default_help = ( + "" if default is None else f"\033[36m (default: {default})\033[0m" + ) prompt = f"\033[1m{text}{default_help}: \033[0m" res = None while not res: @@ -199,8 +232,10 @@ def _choice(self, text: str, choices: list[Any], *, max_attempts: int = 10) -> s if not self._prompts: return "any" choices = sorted(choices, key=str.casefold) - numbered = "\n".join(f"\033[1m[{i}]\033[0m \033[36m{v}\033[0m" for i, v in enumerate(choices)) - prompt = f"\033[1m{text}\033[0m\n{numbered}\nEnter a number between 0 and {len(choices)-1}: " + numbered = "\n".join( + f"\033[1m[{i}]\033[0m \033[36m{v}\033[0m" for i, v in enumerate(choices) + ) + prompt = f"\033[1m{text}\033[0m\n{numbered}\nEnter a number between 0 and {len(choices) - 1}: " attempt = 0 while attempt < max_attempts: attempt += 1 @@ -229,7 +264,7 @@ def _choice_from_dict(self, text: str, choices: dict[str, Any]) -> Any: # 3. python -m src.install console_handler = logging.StreamHandler(sys.stderr) - console_handler.setLevel('DEBUG') + console_handler.setLevel("DEBUG") logging.root.addHandler(console_handler) ws = WorkspaceClient(product="dlt-meta", product_version=__version__) diff --git a/src/onboard_dataflowspec.py b/src/onboard_dataflowspec.py index 221d805..ff93e09 100644 --- a/src/onboard_dataflowspec.py +++ b/src/onboard_dataflowspec.py @@ -1,22 +1,18 @@ """OnboardDataflowSpec class provides bronze/silver onboarding features.""" + import copy import dataclasses import json import logging + import pyspark.sql.types as T from pyspark.sql import functions as f -from pyspark.sql.types import ( - StructField, - StructType, - StringType, - ArrayType, - MapType, -) - -from src.dataflow_spec import BronzeDataflowSpec, SilverDataflowSpec, DataflowSpecUtils -from src.metastore_ops import DeltaPipelinesMetaStoreOps, DeltaPipelinesInternalTableOps - -logger = logging.getLogger('databricks.labs.dltmeta') +from pyspark.sql.types import ArrayType, MapType, StringType, StructField, StructType + +from src.dataflow_spec import BronzeDataflowSpec, DataflowSpecUtils, SilverDataflowSpec +from src.metastore_ops import DeltaPipelinesInternalTableOps, DeltaPipelinesMetaStoreOps + +logger = logging.getLogger("databricks.labs.dltmeta") logger.setLevel(logging.INFO) @@ -67,8 +63,14 @@ def __validate_dict_attributes(attributes, dict_obj): attributes_keys = set(dict_obj.keys()) logger.info("In validate dict attributes") logger.info(set(attributes), attributes_keys) - logger.info("missing attributes : {}".format(set(attributes).difference(attributes_keys))) - raise ValueError(f"missing attributes : {set(attributes).difference(attributes_keys)}") + logger.info( + "missing attributes : {}".format( + set(attributes).difference(attributes_keys) + ) + ) + raise ValueError( + f"missing attributes : {set(attributes).difference(attributes_keys)}" + ) def onboard_dataflow_specs(self): """ @@ -118,29 +120,37 @@ def onboard_dataflow_specs(self): def register_bronze_dataflow_spec_tables(self): """Register bronze/silver dataflow specs tables.""" - self.deltaPipelinesMetaStoreOps.create_database(self.dict_obj["database"], "dlt-meta database") + self.deltaPipelinesMetaStoreOps.create_database( + self.dict_obj["database"], "dlt-meta database" + ) self.deltaPipelinesMetaStoreOps.register_table_in_metastore( self.dict_obj["database"], self.dict_obj["bronze_dataflowspec_table"], self.dict_obj["bronze_dataflowspec_path"], ) logger.info( - f"""onboarded bronze table={ self.dict_obj["database"]}.{self.dict_obj["bronze_dataflowspec_table"]}""" + f"""onboarded bronze table={self.dict_obj["database"]}.{self.dict_obj["bronze_dataflowspec_table"]}""" ) - self.spark.read.table(f"""{ self.dict_obj["database"]}.{self.dict_obj["bronze_dataflowspec_table"]}""").show() + self.spark.read.table( + f"""{self.dict_obj["database"]}.{self.dict_obj["bronze_dataflowspec_table"]}""" + ).show() def register_silver_dataflow_spec_tables(self): """Register bronze dataflow specs tables.""" - self.deltaPipelinesMetaStoreOps.create_database(self.dict_obj["database"], "dlt-meta database") + self.deltaPipelinesMetaStoreOps.create_database( + self.dict_obj["database"], "dlt-meta database" + ) self.deltaPipelinesMetaStoreOps.register_table_in_metastore( self.dict_obj["database"], self.dict_obj["silver_dataflowspec_table"], self.dict_obj["silver_dataflowspec_path"], ) logger.info( - f"""onboarded silver table={ self.dict_obj["database"]}.{self.dict_obj["silver_dataflowspec_table"]}""" + f"""onboarded silver table={self.dict_obj["database"]}.{self.dict_obj["silver_dataflowspec_table"]}""" ) - self.spark.read.table(f"""{ self.dict_obj["database"]}.{self.dict_obj["silver_dataflowspec_table"]}""").show() + self.spark.read.table( + f"""{self.dict_obj["database"]}.{self.dict_obj["silver_dataflowspec_table"]}""" + ).show() def onboard_silver_dataflow_spec(self): """ @@ -175,12 +185,18 @@ def onboard_silver_dataflow_spec(self): attributes.append("silver_dataflowspec_path") self.__validate_dict_attributes(attributes, dict_obj) - onboarding_df = self.__get_onboarding_file_dataframe(dict_obj["onboarding_file_path"]) - silver_data_flow_spec_df = self.__get_silver_dataflow_spec_dataframe(onboarding_df, dict_obj["env"]) + onboarding_df = self.__get_onboarding_file_dataframe( + dict_obj["onboarding_file_path"] + ) + silver_data_flow_spec_df = self.__get_silver_dataflow_spec_dataframe( + onboarding_df, dict_obj["env"] + ) columns = StructType( [ StructField("select_exp", ArrayType(StringType(), True), True), - StructField("target_partition_cols", ArrayType(StringType(), True), True), + StructField( + "target_partition_cols", ArrayType(StringType(), True), True + ), StructField("target_table", StringType(), True), StructField("where_clause", ArrayType(StringType(), True), True), ] @@ -188,22 +204,27 @@ def onboard_silver_dataflow_spec(self): emp_rdd = [] env = dict_obj["env"] - silver_transformation_json_df = self.spark.createDataFrame(data=emp_rdd, schema=columns) - silver_transformation_json_file = onboarding_df.select(f"silver_transformation_json_{env}").dropDuplicates() + silver_transformation_json_df = self.spark.createDataFrame( + data=emp_rdd, schema=columns + ) + silver_transformation_json_file = onboarding_df.select( + f"silver_transformation_json_{env}" + ).dropDuplicates() silver_transformation_json_files = silver_transformation_json_file.collect() for row in silver_transformation_json_files: silver_transformation_json_df = silver_transformation_json_df.union( self.spark.read.option("multiline", "true") - .schema(columns) - .json(row[f"silver_transformation_json_{env}"]) + .schema(columns) + .json(row[f"silver_transformation_json_{env}"]) ) logger.info(silver_transformation_json_file) silver_data_flow_spec_df = silver_transformation_json_df.join( silver_data_flow_spec_df, - silver_transformation_json_df.target_table == silver_data_flow_spec_df.targetDetails["table"], + silver_transformation_json_df.target_table + == silver_data_flow_spec_df.targetDetails["table"], ) silver_dataflow_spec_df = ( silver_data_flow_spec_df.drop("target_table") # .drop("path") @@ -214,7 +235,10 @@ def onboard_silver_dataflow_spec(self): silver_dataflow_spec_df = self.__add_audit_columns( silver_dataflow_spec_df, - {"import_author": dict_obj["import_author"], "version": dict_obj["version"]}, + { + "import_author": dict_obj["import_author"], + "version": dict_obj["version"], + }, ) silver_fields = [field.name for field in dataclasses.fields(SilverDataflowSpec)] @@ -224,21 +248,28 @@ def onboard_silver_dataflow_spec(self): if dict_obj["overwrite"] == "True": if self.uc_enabled: - (silver_dataflow_spec_df.write.format("delta").mode("overwrite").option("mergeSchema", "true") - .saveAsTable(f"{database}.{table}") - ) - else: - silver_dataflow_spec_df.write.mode("overwrite").format("delta").option("mergeSchema", "true").save( - dict_obj["silver_dataflowspec_path"] + ( + silver_dataflow_spec_df.write.format("delta") + .mode("overwrite") + .option("mergeSchema", "true") + .saveAsTable(f"{database}.{table}") ) + else: + silver_dataflow_spec_df.write.mode("overwrite").format("delta").option( + "mergeSchema", "true" + ).save(dict_obj["silver_dataflowspec_path"]) else: if self.uc_enabled: - original_dataflow_df = self.spark.read.format("delta").table(f"{database}.{table}") + original_dataflow_df = self.spark.read.format("delta").table( + f"{database}.{table}" + ) else: - self.deltaPipelinesMetaStoreOps.register_table_in_metastore(database, - table, - dict_obj["silver_dataflowspec_path"]) - original_dataflow_df = self.spark.read.format("delta").load(dict_obj["silver_dataflowspec_path"]) + self.deltaPipelinesMetaStoreOps.register_table_in_metastore( + database, table, dict_obj["silver_dataflowspec_path"] + ) + original_dataflow_df = self.spark.read.format("delta").load( + dict_obj["silver_dataflowspec_path"] + ) logger.info("In Merge block for Silver") self.deltaPipelinesInternalTableOps.merge( silver_dataflow_spec_df, @@ -289,13 +320,20 @@ def onboard_bronze_dataflow_spec(self): attributes.append("bronze_dataflowspec_path") self.__validate_dict_attributes(attributes, dict_obj) - onboarding_df = self.__get_onboarding_file_dataframe(dict_obj["onboarding_file_path"]) + onboarding_df = self.__get_onboarding_file_dataframe( + dict_obj["onboarding_file_path"] + ) - bronze_dataflow_spec_df = self.__get_bronze_dataflow_spec_dataframe(onboarding_df, dict_obj["env"]) + bronze_dataflow_spec_df = self.__get_bronze_dataflow_spec_dataframe( + onboarding_df, dict_obj["env"] + ) bronze_dataflow_spec_df = self.__add_audit_columns( bronze_dataflow_spec_df, - {"import_author": dict_obj["import_author"], "version": dict_obj["version"]}, + { + "import_author": dict_obj["import_author"], + "version": dict_obj["version"], + }, ) bronze_fields = [field.name for field in dataclasses.fields(BronzeDataflowSpec)] bronze_dataflow_spec_df = bronze_dataflow_spec_df.select(bronze_fields) @@ -303,21 +341,31 @@ def onboard_bronze_dataflow_spec(self): table = dict_obj["bronze_dataflowspec_table"] if dict_obj["overwrite"] == "True": if self.uc_enabled: - (bronze_dataflow_spec_df.write.format("delta").mode("overwrite").option("mergeSchema", "true") - .saveAsTable(f"{database}.{table}") - ) + ( + bronze_dataflow_spec_df.write.format("delta") + .mode("overwrite") + .option("mergeSchema", "true") + .saveAsTable(f"{database}.{table}") + ) else: - (bronze_dataflow_spec_df.write.mode("overwrite").format("delta").option("mergeSchema", "true") - .save(path=dict_obj["bronze_dataflowspec_path"]) - ) + ( + bronze_dataflow_spec_df.write.mode("overwrite") + .format("delta") + .option("mergeSchema", "true") + .save(path=dict_obj["bronze_dataflowspec_path"]) + ) else: if self.uc_enabled: - original_dataflow_df = self.spark.read.format("delta").table(f"{database}.{table}") + original_dataflow_df = self.spark.read.format("delta").table( + f"{database}.{table}" + ) else: - self.deltaPipelinesMetaStoreOps.register_table_in_metastore(database, - table, - dict_obj["bronze_dataflowspec_path"]) - original_dataflow_df = self.spark.read.format("delta").load(dict_obj["bronze_dataflowspec_path"]) + self.deltaPipelinesMetaStoreOps.register_table_in_metastore( + database, table, dict_obj["bronze_dataflowspec_path"] + ) + original_dataflow_df = self.spark.read.format("delta").load( + dict_obj["bronze_dataflowspec_path"] + ) logger.info("In Merge block for Bronze") self.deltaPipelinesInternalTableOps.merge( @@ -339,14 +387,20 @@ def __delete_none(self, _dict): def __get_onboarding_file_dataframe(self, onboarding_file_path): onboarding_df = None if onboarding_file_path.lower().endswith(".json"): - onboarding_df = self.spark.read.option("multiline", "true").json(onboarding_file_path) + onboarding_df = self.spark.read.option("multiline", "true").json( + onboarding_file_path + ) self.onboard_file_type = "json" - onboarding_df_dupes = onboarding_df.groupBy("data_flow_id").count().filter("count > 1") + onboarding_df_dupes = ( + onboarding_df.groupBy("data_flow_id").count().filter("count > 1") + ) if len(onboarding_df_dupes.head(1)) > 0: onboarding_df_dupes.show() raise Exception("onboarding file have duplicated data_flow_ids! ") else: - raise Exception("Onboarding file format not supported! Please provide json file format") + raise Exception( + "Onboarding file format not supported! Please provide json file format" + ) return onboarding_df def __add_audit_columns(self, df, dict_obj): @@ -377,7 +431,9 @@ def __get_bronze_schema(self, metadata_file): Args: metadata_file ([string]): metadata schema file path """ - ddlSchemaStr = self.spark.read.text(paths=metadata_file, wholetext=True).collect()[0]["value"] + ddlSchemaStr = self.spark.read.text( + paths=metadata_file, wholetext=True + ).collect()[0]["value"] spark_schema = T._parse_datatype_string(ddlSchemaStr) logger.info(spark_schema) schema = json.dumps(spark_schema.jsonValue()) @@ -421,29 +477,53 @@ def __get_bronze_dataflow_spec_dataframe(self, onboarding_df, env): StructField("dataFlowId", StringType(), True), StructField("dataFlowGroup", StringType(), True), StructField("sourceFormat", StringType(), True), - StructField("sourceDetails", MapType(StringType(), StringType(), True), True), + StructField( + "sourceDetails", MapType(StringType(), StringType(), True), True + ), StructField( "readerConfigOptions", MapType(StringType(), StringType(), True), True, ), StructField("targetFormat", StringType(), True), - StructField("targetDetails", MapType(StringType(), StringType(), True), True), - StructField("tableProperties", MapType(StringType(), StringType(), True), True), + StructField( + "targetDetails", MapType(StringType(), StringType(), True), True + ), + StructField( + "tableProperties", MapType(StringType(), StringType(), True), True + ), StructField("schema", StringType(), True), StructField("partitionColumns", ArrayType(StringType(), True), True), StructField("cdcApplyChanges", StringType(), True), StructField("dataQualityExpectations", StringType(), True), - StructField("quarantineTargetDetails", MapType(StringType(), StringType(), True), True), - StructField("quarantineTableProperties", MapType(StringType(), StringType(), True), True), + StructField( + "quarantineTargetDetails", + MapType(StringType(), StringType(), True), + True, + ), + StructField( + "quarantineTableProperties", + MapType(StringType(), StringType(), True), + True, + ), StructField("appendFlows", StringType(), True), - StructField("appendFlowsSchemas", MapType(StringType(), StringType(), True), True) + StructField( + "appendFlowsSchemas", + MapType(StringType(), StringType(), True), + True, + ), ] ) data = [] onboarding_rows = onboarding_df.collect() - mandatory_fields = ["data_flow_id", "data_flow_group", "source_details", f"bronze_database_{env}", - "bronze_table", "bronze_reader_options"] # , f"bronze_table_path_{env}" + mandatory_fields = [ + "data_flow_id", + "data_flow_group", + "source_details", + f"bronze_database_{env}", + "bronze_table", + "bronze_reader_options", + ] # , f"bronze_table_path_{env}" for onboarding_row in onboarding_rows: try: self.__validate_mandatory_fields(onboarding_row, mandatory_fields) @@ -456,59 +536,99 @@ def __get_bronze_dataflow_spec_dataframe(self, onboarding_df, env): raise Exception(f"Source format not provided for row={onboarding_row}") source_format = onboarding_row["source_format"] - if source_format.lower() not in ["cloudfiles", "eventhub", "kafka", "delta"]: - raise Exception(f"Source format {source_format} not supported in DLT-META! row={onboarding_row}") - source_details, bronze_reader_config_options, schema = self.get_bronze_source_details_reader_options_schema( - onboarding_row, env) + if source_format.lower() not in [ + "cloudfiles", + "eventhub", + "kafka", + "delta", + ]: + raise Exception( + f"Source format {source_format} not supported in DLT-META! row={onboarding_row}" + ) + source_details, bronze_reader_config_options, schema = ( + self.get_bronze_source_details_reader_options_schema( + onboarding_row, env + ) + ) bronze_target_format = "delta" bronze_target_details = { "database": onboarding_row["bronze_database_{}".format(env)], - "table": onboarding_row["bronze_table"] + "table": onboarding_row["bronze_table"], } if not self.uc_enabled: - bronze_target_details["path"] = onboarding_row[f"bronze_table_path_{env}"] + bronze_target_details["path"] = onboarding_row[ + f"bronze_table_path_{env}" + ] bronze_table_properties = {} - if "bronze_table_properties" in onboarding_row and onboarding_row["bronze_table_properties"]: - bronze_table_properties = self.__delete_none(onboarding_row["bronze_table_properties"].asDict()) + if ( + "bronze_table_properties" in onboarding_row + and onboarding_row["bronze_table_properties"] + ): + bronze_table_properties = self.__delete_none( + onboarding_row["bronze_table_properties"].asDict() + ) partition_columns = [""] - if "bronze_partition_columns" in onboarding_row and onboarding_row["bronze_partition_columns"]: + if ( + "bronze_partition_columns" in onboarding_row + and onboarding_row["bronze_partition_columns"] + ): partition_columns = [onboarding_row["bronze_partition_columns"]] cdc_apply_changes = None - if "bronze_cdc_apply_changes" in onboarding_row and onboarding_row["bronze_cdc_apply_changes"]: + if ( + "bronze_cdc_apply_changes" in onboarding_row + and onboarding_row["bronze_cdc_apply_changes"] + ): self.__validate_apply_changes(onboarding_row, "bronze") - cdc_apply_changes = json.dumps(self.__delete_none(onboarding_row["bronze_cdc_apply_changes"].asDict())) + cdc_apply_changes = json.dumps( + self.__delete_none( + onboarding_row["bronze_cdc_apply_changes"].asDict() + ) + ) data_quality_expectations = None quarantine_target_details = {} quarantine_table_properties = {} if f"bronze_data_quality_expectations_json_{env}" in onboarding_row: - bronze_data_quality_expectations_json = onboarding_row[f"bronze_data_quality_expectations_json_{env}"] + bronze_data_quality_expectations_json = onboarding_row[ + f"bronze_data_quality_expectations_json_{env}" + ] if bronze_data_quality_expectations_json: - data_quality_expectations = ( - self.__get_data_quality_expecations(bronze_data_quality_expectations_json)) + data_quality_expectations = self.__get_data_quality_expecations( + bronze_data_quality_expectations_json + ) if onboarding_row["bronze_quarantine_table"]: quarantine_table_partition_columns = "" if ( "bronze_quarantine_table_partitions" in onboarding_row and onboarding_row["bronze_quarantine_table_partitions"] ): - quarantine_table_partition_columns = onboarding_row["bronze_quarantine_table_partitions"] + quarantine_table_partition_columns = onboarding_row[ + "bronze_quarantine_table_partitions" + ] quarantine_target_details = { - "database": onboarding_row[f"bronze_database_quarantine_{env}"], + "database": onboarding_row[ + f"bronze_database_quarantine_{env}" + ], "table": onboarding_row["bronze_quarantine_table"], "partition_columns": quarantine_table_partition_columns, } if not self.uc_enabled: quarantine_target_details["path"] = onboarding_row[ - f"bronze_quarantine_table_path_{env}"] + f"bronze_quarantine_table_path_{env}" + ] if ( "bronze_quarantine_table_properties" in onboarding_row and onboarding_row["bronze_quarantine_table_properties"] ): quarantine_table_properties = self.__delete_none( - onboarding_row["bronze_quarantine_table_properties"].asDict()) - append_flows, append_flows_schemas = self.get_append_flows_json(onboarding_row, "bronze", env) + onboarding_row[ + "bronze_quarantine_table_properties" + ].asDict() + ) + append_flows, append_flows_schemas = self.get_append_flows_json( + onboarding_row, "bronze", env + ) bronze_row = ( bronze_data_flow_spec_id, bronze_data_flow_spec_group, @@ -525,22 +645,28 @@ def __get_bronze_dataflow_spec_dataframe(self, onboarding_df, env): quarantine_target_details, quarantine_table_properties, append_flows, - append_flows_schemas + append_flows_schemas, ) data.append(bronze_row) # logger.info(bronze_parition_columns) - data_flow_spec_rows_df = self.spark.createDataFrame(data, data_flow_spec_schema).toDF(*data_flow_spec_columns) + data_flow_spec_rows_df = self.spark.createDataFrame( + data, data_flow_spec_schema + ).toDF(*data_flow_spec_columns) return data_flow_spec_rows_df def get_append_flows_json(self, onboarding_row, layer, env): append_flows = None append_flows_schema = {} - if f"{layer}_append_flows" in onboarding_row and onboarding_row[f"{layer}_append_flows"]: + if ( + f"{layer}_append_flows" in onboarding_row + and onboarding_row[f"{layer}_append_flows"] + ): self.__validate_append_flow(onboarding_row, layer) json_append_flows = onboarding_row[f"{layer}_append_flows"] from pyspark.sql.types import Row + af_list = [] for json_append_flow in json_append_flows: json_append_flow = json_append_flow.asDict() @@ -551,14 +677,18 @@ def get_append_flows_json(self, onboarding_row, layer, env): mp = {} for ff in fs: if f"source_path_{env}" == ff: - mp['path'] = json_append_flow[key][f'{ff}'] + mp["path"] = json_append_flow[key][f"{ff}"] elif "source_schema_path" == ff: - source_schema_path = json_append_flow[key][f'{ff}'] + source_schema_path = json_append_flow[key][f"{ff}"] if source_schema_path: - schema = self.__get_bronze_schema(source_schema_path) - append_flows_schema[json_append_flow['name']] = schema + schema = self.__get_bronze_schema( + source_schema_path + ) + append_flows_schema[json_append_flow["name"]] = ( + schema + ) else: - mp[f'{ff}'] = json_append_flow[key][f'{ff}'] + mp[f"{ff}"] = json_append_flow[key][f"{ff}"] append_flow_map[key] = self.__delete_none(mp) else: append_flow_map[key] = json_append_flow[key] @@ -571,29 +701,31 @@ def __validate_apply_changes(self, onboarding_row, layer): json_cdc_apply_changes = cdc_apply_changes.asDict() logger.info(f"actual mergeInfo={json_cdc_apply_changes}") payload_keys = json_cdc_apply_changes.keys() - missing_cdc_payload_keys = set(DataflowSpecUtils.cdc_applychanges_api_attributes).difference(payload_keys) + missing_cdc_payload_keys = set( + DataflowSpecUtils.cdc_applychanges_api_attributes + ).difference(payload_keys) logger.info( f"""missing cdc payload keys:{missing_cdc_payload_keys} for onboarding row = {onboarding_row}""" ) - if set(DataflowSpecUtils.cdc_applychanges_api_mandatory_attributes) - set(payload_keys): - missing_mandatory_attr = set(DataflowSpecUtils.cdc_applychanges_api_mandatory_attributes) - set( - payload_keys - ) + if set(DataflowSpecUtils.cdc_applychanges_api_mandatory_attributes) - set( + payload_keys + ): + missing_mandatory_attr = set( + DataflowSpecUtils.cdc_applychanges_api_mandatory_attributes + ) - set(payload_keys) logger.info(f"mandatory missing keys= {missing_mandatory_attr}") raise Exception( - f"""mandatory missing atrributes for {layer}_cdc_apply_changes = { - missing_mandatory_attr} + f"""mandatory missing atrributes for {layer}_cdc_apply_changes = {missing_mandatory_attr} for onboarding row = {onboarding_row}""" ) else: logger.info( f"""all mandatory {layer}_cdc_apply_changes atrributes - {DataflowSpecUtils.cdc_applychanges_api_mandatory_attributes} exists""" + {DataflowSpecUtils.cdc_applychanges_api_mandatory_attributes} exists""" ) def get_bronze_source_details_reader_options_schema(self, onboarding_row, env): - """Get bronze source reader options. Args: @@ -608,32 +740,49 @@ def get_bronze_source_details_reader_options_schema(self, onboarding_row, env): source_format = onboarding_row["source_format"] bronze_reader_options_json = onboarding_row["bronze_reader_options"] if bronze_reader_options_json: - bronze_reader_config_options = self.__delete_none(bronze_reader_options_json.asDict()) + bronze_reader_config_options = self.__delete_none( + bronze_reader_options_json.asDict() + ) source_details_json = onboarding_row["source_details"] if source_details_json: source_details_file = self.__delete_none(source_details_json.asDict()) - if source_format.lower() == "cloudfiles" or source_format.lower() == "delta": + if ( + source_format.lower() == "cloudfiles" + or source_format.lower() == "delta" + ): if f"source_path_{env}" in source_details_file: source_details["path"] = source_details_file[f"source_path_{env}"] if "source_database" in source_details_file: - source_details["source_database"] = source_details_file["source_database"] + source_details["source_database"] = source_details_file[ + "source_database" + ] if "source_table" in source_details_file: source_details["source_table"] = source_details_file["source_table"] if "source_metadata" in source_details_file: - source_metadata_dict = self.__delete_none(source_details_file["source_metadata"].asDict()) + source_metadata_dict = self.__delete_none( + source_details_file["source_metadata"].asDict() + ) if "select_metadata_cols" in source_metadata_dict: select_metadata_cols = self.__delete_none( source_metadata_dict["select_metadata_cols"].asDict() ) - source_metadata_dict["select_metadata_cols"] = select_metadata_cols - source_details["source_metadata"] = json.dumps(self.__delete_none(source_metadata_dict)) - elif source_format.lower() == "eventhub" or source_format.lower() == "kafka": + source_metadata_dict["select_metadata_cols"] = ( + select_metadata_cols + ) + source_details["source_metadata"] = json.dumps( + self.__delete_none(source_metadata_dict) + ) + elif ( + source_format.lower() == "eventhub" or source_format.lower() == "kafka" + ): source_details = source_details_file if "source_schema_path" in source_details_file: source_schema_path = source_details_file["source_schema_path"] if source_schema_path: if self.bronze_schema_mapper is not None: - schema = self.bronze_schema_mapper(source_schema_path, self.spark) + schema = self.bronze_schema_mapper( + source_schema_path, self.spark + ) else: schema = self.__get_bronze_schema(source_schema_path) else: @@ -648,20 +797,22 @@ def __validate_append_flow(self, onboarding_row, layer): json_append_flow = append_flow.asDict() logger.info(f"actual appendFlow={json_append_flow}") payload_keys = json_append_flow.keys() - missing_append_flow_payload_keys = ( - set(DataflowSpecUtils.append_flow_api_attributes_defaults) - .difference(payload_keys) - ) + missing_append_flow_payload_keys = set( + DataflowSpecUtils.append_flow_api_attributes_defaults + ).difference(payload_keys) logger.info( f"""missing append flow payload keys:{missing_append_flow_payload_keys} for onboarding row = {onboarding_row}""" ) - if set(DataflowSpecUtils.append_flow_mandatory_attributes) - set(payload_keys): - missing_mandatory_attr = set(DataflowSpecUtils.append_flow_mandatory_attributes) - set(payload_keys) + if set(DataflowSpecUtils.append_flow_mandatory_attributes) - set( + payload_keys + ): + missing_mandatory_attr = set( + DataflowSpecUtils.append_flow_mandatory_attributes + ) - set(payload_keys) logger.info(f"mandatory missing keys= {missing_mandatory_attr}") raise Exception( - f"""mandatory missing atrributes for {layer}_append_flow = { - missing_mandatory_attr} + f"""mandatory missing atrributes for {layer}_append_flow = {missing_mandatory_attr} for onboarding row = {onboarding_row}""" ) else: @@ -707,34 +858,49 @@ def __get_silver_dataflow_spec_dataframe(self, onboarding_df, env): "cdcApplyChanges", "dataQualityExpectations", "appendFlows", - "appendFlowsSchemas" + "appendFlowsSchemas", ] data_flow_spec_schema = StructType( [ StructField("dataFlowId", StringType(), True), StructField("dataFlowGroup", StringType(), True), StructField("sourceFormat", StringType(), True), - StructField("sourceDetails", MapType(StringType(), StringType(), True), True), + StructField( + "sourceDetails", MapType(StringType(), StringType(), True), True + ), StructField( "readerConfigOptions", MapType(StringType(), StringType(), True), True, ), StructField("targetFormat", StringType(), True), - StructField("targetDetails", MapType(StringType(), StringType(), True), True), - StructField("tableProperties", MapType(StringType(), StringType(), True), True), + StructField( + "targetDetails", MapType(StringType(), StringType(), True), True + ), + StructField( + "tableProperties", MapType(StringType(), StringType(), True), True + ), StructField("partitionColumns", ArrayType(StringType(), True), True), StructField("cdcApplyChanges", StringType(), True), StructField("dataQualityExpectations", StringType(), True), StructField("appendFlows", StringType(), True), - StructField("appendFlowsSchemas", MapType(StringType(), StringType(), True), True) + StructField( + "appendFlowsSchemas", + MapType(StringType(), StringType(), True), + True, + ), ] ) data = [] onboarding_rows = onboarding_df.collect() - mandatory_fields = ["data_flow_id", "data_flow_group", f"silver_database_{env}", - "silver_table", f"silver_transformation_json_{env}"] # f"silver_table_path_{env}", + mandatory_fields = [ + "data_flow_id", + "data_flow_group", + f"silver_database_{env}", + "silver_table", + f"silver_transformation_json_{env}", + ] # f"silver_table_path_{env}", for onboarding_row in onboarding_rows: try: @@ -750,38 +916,62 @@ def __get_silver_dataflow_spec_dataframe(self, onboarding_df, env): bronze_target_details = { "database": onboarding_row["bronze_database_{}".format(env)], - "table": onboarding_row["bronze_table"] + "table": onboarding_row["bronze_table"], } silver_target_details = { "database": onboarding_row["silver_database_{}".format(env)], - "table": onboarding_row["silver_table"] + "table": onboarding_row["silver_table"], } if not self.uc_enabled: - bronze_target_details["path"] = onboarding_row[f"bronze_table_path_{env}"] - silver_target_details["path"] = onboarding_row[f"silver_table_path_{env}"] + bronze_target_details["path"] = onboarding_row[ + f"bronze_table_path_{env}" + ] + silver_target_details["path"] = onboarding_row[ + f"silver_table_path_{env}" + ] silver_table_properties = {} - if "silver_table_properties" in onboarding_row and onboarding_row["silver_table_properties"]: - silver_table_properties = self.__delete_none(onboarding_row["silver_table_properties"].asDict()) + if ( + "silver_table_properties" in onboarding_row + and onboarding_row["silver_table_properties"] + ): + silver_table_properties = self.__delete_none( + onboarding_row["silver_table_properties"].asDict() + ) silver_parition_columns = [""] - if "silver_partition_columns" in onboarding_row and onboarding_row["silver_partition_columns"]: + if ( + "silver_partition_columns" in onboarding_row + and onboarding_row["silver_partition_columns"] + ): silver_parition_columns = [onboarding_row["silver_partition_columns"]] silver_cdc_apply_changes = None - if "silver_cdc_apply_changes" in onboarding_row and onboarding_row["silver_cdc_apply_changes"]: + if ( + "silver_cdc_apply_changes" in onboarding_row + and onboarding_row["silver_cdc_apply_changes"] + ): self.__validate_apply_changes(onboarding_row, "silver") - silver_cdc_apply_changes_row = onboarding_row["silver_cdc_apply_changes"] + silver_cdc_apply_changes_row = onboarding_row[ + "silver_cdc_apply_changes" + ] if self.onboard_file_type == "json": - silver_cdc_apply_changes = json.dumps(self.__delete_none(silver_cdc_apply_changes_row.asDict())) + silver_cdc_apply_changes = json.dumps( + self.__delete_none(silver_cdc_apply_changes_row.asDict()) + ) data_quality_expectations = None if f"silver_data_quality_expectations_json_{env}" in onboarding_row: - silver_data_quality_expectations_json = onboarding_row[f"silver_data_quality_expectations_json_{env}"] + silver_data_quality_expectations_json = onboarding_row[ + f"silver_data_quality_expectations_json_{env}" + ] if silver_data_quality_expectations_json: - data_quality_expectations = ( - self.__get_data_quality_expecations(silver_data_quality_expectations_json)) - append_flows, append_flow_schemas = self.get_append_flows_json(onboarding_row, layer="silver", env=env) + data_quality_expectations = self.__get_data_quality_expecations( + silver_data_quality_expectations_json + ) + append_flows, append_flow_schemas = self.get_append_flows_json( + onboarding_row, layer="silver", env=env + ) silver_row = ( silver_data_flow_spec_id, silver_data_flow_spec_group, @@ -795,10 +985,12 @@ def __get_silver_dataflow_spec_dataframe(self, onboarding_df, env): silver_cdc_apply_changes, data_quality_expectations, append_flows, - append_flow_schemas + append_flow_schemas, ) data.append(silver_row) logger.info(f"silver_data ==== {data}") - data_flow_spec_rows_df = self.spark.createDataFrame(data, data_flow_spec_schema).toDF(*data_flow_spec_columns) + data_flow_spec_rows_df = self.spark.createDataFrame( + data, data_flow_spec_schema + ).toDF(*data_flow_spec_columns) return data_flow_spec_rows_df From 0d9975a98d840612315e9ef8b7e738a487a8c5c8 Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Mon, 14 Oct 2024 14:46:22 -0400 Subject: [PATCH 53/59] removed match syntax since testing on python 3.9 --- integration_tests/run_integration_tests.py | 51 +++++++++++----------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index 5a2b25e..591744a 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -1,20 +1,22 @@ """ A script to run integration tests for DLT-Meta.""" # Import necessary modules -import uuid import argparse +import json import os -import webbrowser import traceback +import uuid +import webbrowser from dataclasses import dataclass + from databricks.sdk import WorkspaceClient -from databricks.sdk.service.pipelines import PipelineLibrary, NotebookLibrary -from databricks.sdk.service import jobs, compute -from src.__about__ import __version__ -from databricks.sdk.service.workspace import ImportFormat, Language +from databricks.sdk.service import compute, jobs from databricks.sdk.service.catalog import SchemasAPI, VolumeInfo, VolumeType +from databricks.sdk.service.pipelines import NotebookLibrary, PipelineLibrary +from databricks.sdk.service.workspace import ImportFormat, Language + +from src.__about__ import __version__ from src.install import WorkspaceInstaller -import json # Dictionary mapping cloud providers to node types cloud_node_type_id_dict = { @@ -367,7 +369,7 @@ def create_workflow_spec(self, runner_conf: DLTMetaRunnerConf): named_parameters={ "onboard_layer": "bronze", "database": f"{runner_conf.uc_catalog_name}.{runner_conf.dlt_meta_schema}", - "onboarding_file_path": f"{runner_conf.uc_volume_path}/{self.base_dir}/conf/onboarding_A2.json", # noqa : E501 + "onboarding_file_path": f"{runner_conf.uc_volume_path}/{self.base_dir}/conf/onboarding_A2.json", # noqa : E501 "bronze_dataflowspec_table": "bronze_dataflowspec_cdc", "import_author": "Ravi", "version": "v1", @@ -396,23 +398,22 @@ def create_workflow_spec(self, runner_conf: DLTMetaRunnerConf): ] ) else: - match runner_conf.source: - case "eventhub": - base_parameters = { - "eventhub_name": runner_conf.eventhub_name, - "eventhub_name_append_flow": runner_conf.eventhub_name_append_flow, - "eventhub_namespace": runner_conf.eventhub_namespace, - "eventhub_secrets_scope_name": runner_conf.eventhub_secrets_scope_name, - "eventhub_accesskey_name": runner_conf.eventhub_producer_accesskey_name, - "eventhub_input_data": f"/{runner_conf.uc_volume_path}/{self.base_dir}/resources/data/iot/iot.json", # noqa : E501 - "eventhub_append_flow_input_data": f"/{runner_conf.uc_volume_path}/{self.base_dir}/resources/data/iot_eventhub_af/iot.json",# noqa : E501 - } - case "kafka": - base_parameters = { - "kafka_topic": runner_conf.kafka_topic, - "kafka_broker": runner_conf.kafka_broker, - "kafka_input_data": f"/{runner_conf.uc_volume_path}/{self.base_dir}/resources/data/iot/iot.json", # noqa : E501 - } + if runner_conf.source == "eventhub": + base_parameters = { + "eventhub_name": runner_conf.eventhub_name, + "eventhub_name_append_flow": runner_conf.eventhub_name_append_flow, + "eventhub_namespace": runner_conf.eventhub_namespace, + "eventhub_secrets_scope_name": runner_conf.eventhub_secrets_scope_name, + "eventhub_accesskey_name": runner_conf.eventhub_producer_accesskey_name, + "eventhub_input_data": f"/{runner_conf.uc_volume_path}/{self.base_dir}/resources/data/iot/iot.json", # noqa : E501 + "eventhub_append_flow_input_data": f"/{runner_conf.uc_volume_path}/{self.base_dir}/resources/data/iot_eventhub_af/iot.json", # noqa : E501 + } + elif runner_conf.source == "kafka": + base_parameters = { + "kafka_topic": runner_conf.kafka_topic, + "kafka_broker": runner_conf.kafka_broker, + "kafka_input_data": f"/{runner_conf.uc_volume_path}/{self.base_dir}/resources/data/iot/iot.json", # noqa : E501 + } tasks.append( jobs.Task( From 386db9d030f920bbaeba092324eda3927770702d Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Mon, 14 Oct 2024 14:54:31 -0400 Subject: [PATCH 54/59] removed other match statement --- integration_tests/run_integration_tests.py | 47 +++++++++++----------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/integration_tests/run_integration_tests.py b/integration_tests/run_integration_tests.py index 591744a..6925f63 100644 --- a/integration_tests/run_integration_tests.py +++ b/integration_tests/run_integration_tests.py @@ -474,30 +474,29 @@ def generate_onboarding_file(self, runner_conf: DLTMetaRunnerConf): "{bronze_schema}": runner_conf.bronze_schema, } - match runner_conf.source: - case "cloudfiles": - string_subs.update({"{silver_schema}": runner_conf.silver_schema}) - case "eventhub": - string_subs.update( - { - "{run_id}": runner_conf.run_id, - "{eventhub_name}": runner_conf.eventhub_name, - "{eventhub_name_append_flow}": runner_conf.eventhub_name_append_flow, - "{eventhub_consumer_accesskey_name}": runner_conf.eventhub_consumer_accesskey_name, - "{eventhub_accesskey_secret_name}": runner_conf.eventhub_accesskey_secret_name, - "{eventhub_secrets_scope_name}": runner_conf.eventhub_secrets_scope_name, - "{eventhub_namespace}": runner_conf.eventhub_namespace, - "{eventhub_port}": runner_conf.eventhub_port, - } - ) - case "kafka": - string_subs.update( - { - "{run_id}": runner_conf.run_id, - "{kafka_topic}": runner_conf.kafka_topic, - "{kafka_broker}": runner_conf.kafka_broker, - } - ) + if runner_conf.source == "cloudfiles": + string_subs.update({"{silver_schema}": runner_conf.silver_schema}) + elif runner_conf.source == "eventhub": + string_subs.update( + { + "{run_id}": runner_conf.run_id, + "{eventhub_name}": runner_conf.eventhub_name, + "{eventhub_name_append_flow}": runner_conf.eventhub_name_append_flow, + "{eventhub_consumer_accesskey_name}": runner_conf.eventhub_consumer_accesskey_name, + "{eventhub_accesskey_secret_name}": runner_conf.eventhub_accesskey_secret_name, + "{eventhub_secrets_scope_name}": runner_conf.eventhub_secrets_scope_name, + "{eventhub_namespace}": runner_conf.eventhub_namespace, + "{eventhub_port}": runner_conf.eventhub_port, + } + ) + elif runner_conf.source == "kafka": + string_subs.update( + { + "{run_id}": runner_conf.run_id, + "{kafka_topic}": runner_conf.kafka_topic, + "{kafka_broker}": runner_conf.kafka_broker, + } + ) # Open the onboarding templates and sub in the proper table locations, paths, etc. with open(f"{runner_conf.cloudfiles_template}", "r") as f: From 05e8b3a8d58f04aeddcf9d2ff57f4e0cec183408 Mon Sep 17 00:00:00 2001 From: Drew Vander Wood Date: Mon, 14 Oct 2024 15:32:46 -0400 Subject: [PATCH 55/59] uc volume path for onboarding file test --- tests/test_cli.py | 50 ++++++++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 0b6454b..e386f73 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,7 +1,8 @@ import unittest from unittest.mock import MagicMock, patch + from src.__about__ import __version__ -from src.cli import DLTMeta, OnboardCommand, DeployCommand, DLT_META_RUNNER_NOTEBOOK +from src.cli import DLT_META_RUNNER_NOTEBOOK, DeployCommand, DLTMeta, OnboardCommand class CliTests(unittest.TestCase): @@ -24,7 +25,7 @@ class CliTests(unittest.TestCase): overwrite=True, bronze_dataflowspec_table="bronze_dataflowspec", silver_dataflowspec_table="silver_dataflowspec", - update_paths=True + update_paths=True, ) deploy_cmd = DeployCommand( layer="bronze", @@ -44,19 +45,19 @@ class CliTests(unittest.TestCase): def test_copy(self): mock_ws = MagicMock() dltmeta = DLTMeta(mock_ws) - with patch('os.walk') as mock_walk: + with patch("os.walk") as mock_walk: mock_walk.return_value = [ ("/path/to/src", [], ["file1.txt", "file2.txt"]), - ("/path/to/src/subdir", [], ["file3.txt"]) + ("/path/to/src/subdir", [], ["file3.txt"]), ] - with patch('builtins.open') as mock_open: + with patch("builtins.open") as mock_open: mock_open.return_value = MagicMock() mock_dbfs_upload = MagicMock() mock_ws.dbfs.upload = mock_dbfs_upload dltmeta.copy_to_dbfs("file:/path/to/src", "/dbfs/path/to/dst") self.assertEqual(mock_dbfs_upload.call_count, 3) - @patch('src.cli.WorkspaceClient') + @patch("src.cli.WorkspaceClient") def test_onboard(self, mock_workspace_client): mock_dbfs = MagicMock() mock_jobs = MagicMock() @@ -73,12 +74,12 @@ def test_onboard(self, mock_workspace_client): with patch.object(dltmeta._wsi, "_upload_wheel", return_value="/path/to/wheel"): dltmeta.onboard(self.onboard_cmd) - mock_workspace_client.dbfs.exists.assert_called_once_with('/dbfs/dltmeta_conf/') + mock_workspace_client.dbfs.exists.assert_called_once_with("/dbfs/dltmeta_conf/") mock_workspace_client.dbfs.mkdirs.assert_called_once_with("/dbfs/dltmeta_conf/") mock_workspace_client.jobs.create.assert_called_once() mock_workspace_client.jobs.run_now.assert_called_once_with(job_id="job_id") - @patch('src.cli.WorkspaceClient') + @patch("src.cli.WorkspaceClient") def test_create_onnboarding_job(self, mock_workspace_client): mock_workspace_client.jobs.create.return_value = MagicMock(job_id="job_id") @@ -90,7 +91,7 @@ def test_create_onnboarding_job(self, mock_workspace_client): mock_workspace_client.jobs.create.assert_called_once() self.assertEqual(job.job_id, "job_id") - @patch('src.cli.WorkspaceClient') + @patch("src.cli.WorkspaceClient") def test_install_folder(self, mock_workspace_client): dltmeta = DLTMeta(mock_workspace_client) dltmeta._wsi = mock_workspace_client.return_value @@ -98,9 +99,11 @@ def test_install_folder(self, mock_workspace_client): folder = dltmeta._install_folder() self.assertEqual(folder, "/Users/name/dlt-meta") - @patch('src.cli.WorkspaceClient') + @patch("src.cli.WorkspaceClient") def test_create_dlt_meta_pipeline(self, mock_workspace_client): - mock_workspace_client.pipelines.create.return_value = MagicMock(pipeline_id="pipeline_id") + mock_workspace_client.pipelines.create.return_value = MagicMock( + pipeline_id="pipeline_id" + ) mock_workspace_client.workspace.mkdirs.return_value = None mock_workspace_client.workspace.upload.return_value = None dltmeta = DLTMeta(mock_workspace_client) @@ -108,12 +111,16 @@ def test_create_dlt_meta_pipeline(self, mock_workspace_client): dltmeta._wsi._upload_wheel.return_value = None dltmeta._my_username = MagicMock(return_value="name") dltmeta._create_dlt_meta_pipeline(self.deploy_cmd) - runner_notebook_py = DLT_META_RUNNER_NOTEBOOK.format(version=__version__).encode("utf8") + runner_notebook_py = DLT_META_RUNNER_NOTEBOOK.format( + version=__version__ + ).encode("utf8") runner_notebook_path = f"{dltmeta._install_folder()}/init_dlt_meta_pipeline.py" - mock_workspace_client.workspace.mkdirs.assert_called_once_with("/Users/name/dlt-meta") - mock_workspace_client.workspace.upload.assert_called_once_with(runner_notebook_path, - runner_notebook_py, - overwrite=True) + mock_workspace_client.workspace.mkdirs.assert_called_once_with( + "/Users/name/dlt-meta" + ) + mock_workspace_client.workspace.upload.assert_called_once_with( + runner_notebook_path, runner_notebook_py, overwrite=True + ) mock_workspace_client.pipelines.create.assert_called_once() mock_workspace_client.pipelines.create.assert_called_once() @@ -134,22 +141,25 @@ def test_get_onboarding_named_parameters(self): silver_dataflowspec_path="tests/resources/silver_dataflowspec", uc_enabled=True, uc_catalog_name="uc_catalog", + uc_volume_path="uc_catalog/dlt_meta/files", overwrite=True, bronze_dataflowspec_table="bronze_dataflowspec", silver_dataflowspec_table="silver_dataflowspec", - update_paths=True + update_paths=True, ) dltmeta = DLTMeta(None) - named_parameters = dltmeta._get_onboarding_named_parameters(cmd, "onboarding.json") + named_parameters = dltmeta._get_onboarding_named_parameters( + cmd, "onboarding.json" + ) expected_named_parameters = { "onboard_layer": "bronze", "database": "uc_catalog.dlt_meta" if cmd.uc_enabled else "dlt_meta", - "onboarding_file_path": "/dbfs/dltmeta_conf/onboarding.json", + "onboarding_file_path": "uc_catalog/dlt_meta/files/dltmeta_conf/onboarding.json", "import_author": "John Doe", "version": "1.0", "overwrite": "True", "env": "dev", "uc_enabled": "True", - "bronze_dataflowspec_table": "bronze_dataflowspec" + "bronze_dataflowspec_table": "bronze_dataflowspec", } self.assertEqual(named_parameters, expected_named_parameters) From 27dadc81e470fb00fa92815061419eda1bac549c Mon Sep 17 00:00:00 2001 From: ravi-databricks Date: Mon, 14 Oct 2024 18:54:23 -0700 Subject: [PATCH 56/59] fixed cli related tests and added new unit test for onboard and deploy --- .coveragerc | 2 - src/cli.py | 10 +- tests/test_cli.py | 318 ++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 312 insertions(+), 18 deletions(-) diff --git a/.coveragerc b/.coveragerc index 8e49e48..b23aa2f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -7,8 +7,6 @@ omit = tests/* src/install.py src/uninstall.py - src/config.py - src/cli.py [report] exclude_lines = diff --git a/src/cli.py b/src/cli.py index f35a870..176b27e 100644 --- a/src/cli.py +++ b/src/cli.py @@ -42,9 +42,9 @@ class OnboardCommand: import_author: str version: str dlt_meta_schema: str - dbfs_path: None - cloud: None - dbr_version: None + dbfs_path: str = None + cloud: str = None + dbr_version: str = None serverless: bool = True bronze_schema: str = None silver_schema: str = None @@ -520,9 +520,9 @@ def update_ws_onboarding_paths(self, cmd: OnboardCommand): if 'uc_volume_path' in source_value: data_flow[key][source_key] = source_value.format( uc_volume_path=f"{cmd.uc_volume_path}/dltmeta_conf/") - else: + elif 'dbfs_path' in source_value: data_flow[key][source_key] = source_value.format( - uc_volume_path=f"{cmd.dbfs_path}/dltmeta_conf/") + dbfs_path=f"{cmd.dbfs_path}/dltmeta_conf/") if 'uc_volume_path' in value: if cmd.uc_enabled: data_flow[key] = value.format(uc_volume_path=f"{cmd.uc_volume_path}/dltmeta_conf/") diff --git a/tests/test_cli.py b/tests/test_cli.py index e386f73..8ca46fa 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,15 +1,13 @@ import unittest from unittest.mock import MagicMock, patch - +from databricks.sdk.service.catalog import VolumeType from src.__about__ import __version__ -from src.cli import DLT_META_RUNNER_NOTEBOOK, DeployCommand, DLTMeta, OnboardCommand +from src.cli import DLT_META_RUNNER_NOTEBOOK, DeployCommand, DLTMeta, OnboardCommand, main class CliTests(unittest.TestCase): onboarding_file_path = "tests/resources/onboarding.json" - onboard_cmd = OnboardCommand( - dbr_version="15.3", - dbfs_path="/dbfs", + onboard_cmd_with_uc = OnboardCommand( onboarding_file_path=onboarding_file_path, onboarding_files_dir_path="tests/resources/", onboard_layer="bronze", @@ -22,11 +20,32 @@ class CliTests(unittest.TestCase): silver_dataflowspec_path="tests/resources/silver_dataflowspec", uc_enabled=True, uc_catalog_name="uc_catalog", + uc_volume_path="uc_catalog/dlt_meta/files", overwrite=True, bronze_dataflowspec_table="bronze_dataflowspec", silver_dataflowspec_table="silver_dataflowspec", update_paths=True, ) + + onboard_cmd_without_uc = OnboardCommand( + onboarding_file_path=onboarding_file_path, + onboarding_files_dir_path="tests/resources/", + onboard_layer="bronze", + env="dev", + import_author="John Doe", + version="1.0", + cloud="aws", + dlt_meta_schema="dlt_meta", + bronze_dataflowspec_path="tests/resources/bronze_dataflowspec", + silver_dataflowspec_path="tests/resources/silver_dataflowspec", + uc_enabled=False, + dbfs_path="/dbfs", + overwrite=True, + bronze_dataflowspec_table="bronze_dataflowspec", + silver_dataflowspec_table="silver_dataflowspec", + update_paths=True, + ) + deploy_cmd = DeployCommand( layer="bronze", onboard_group="A1", @@ -58,24 +77,58 @@ def test_copy(self): self.assertEqual(mock_dbfs_upload.call_count, 3) @patch("src.cli.WorkspaceClient") - def test_onboard(self, mock_workspace_client): + @patch("builtins.open", new_callable=MagicMock) + def test_onboard_with_uc(self, mock_open, mock_workspace_client): + mock_jobs = MagicMock() + mock_open.return_value = MagicMock() + mock_workspace_client.jobs = mock_jobs + mock_workspace_client.jobs.create.return_value = MagicMock(job_id="job_id") + mock_workspace_client.jobs.run_now.return_value = MagicMock(run_id="run_id") + dltmeta = DLTMeta(mock_workspace_client) + dltmeta._wsi = mock_workspace_client.return_value + dltmeta.update_ws_onboarding_paths = MagicMock() + dltmeta.create_uc_schema = MagicMock() + dltmeta.create_uc_volume = MagicMock() + dltmeta.copy_to_uc_volume = MagicMock() + with patch.object(dltmeta._wsi, "_upload_wheel", return_value="/path/to/wheel"): + dltmeta.onboard(self.onboard_cmd_with_uc) + dltmeta.create_uc_volume.assert_called_once_with( + self.onboard_cmd_with_uc.uc_catalog_name, + self.onboard_cmd_with_uc.dlt_meta_schema + ) + dltmeta.create_uc_schema.assert_called_once_with( + self.onboard_cmd_with_uc.uc_catalog_name, + self.onboard_cmd_with_uc.dlt_meta_schema + ) + mock_workspace_client.jobs.create.assert_called_once() + mock_workspace_client.jobs.run_now.assert_called_once_with(job_id="job_id") + + @patch("src.cli.WorkspaceClient") + @patch("builtins.open", new_callable=MagicMock) + def test_onboard_without_uc(self, mock_open, mock_workspace_client): mock_dbfs = MagicMock() mock_jobs = MagicMock() + mock_open.return_value = MagicMock() mock_workspace_client.dbfs = mock_dbfs mock_workspace_client.jobs = mock_jobs - mock_workspace_client.dbfs.exists.return_value = False mock_workspace_client.dbfs.mkdirs.return_value = None mock_workspace_client.dbfs.upload.return_value = None + mock_copy_to_dbfs = MagicMock() mock_workspace_client.jobs.create.return_value = MagicMock(job_id="job_id") mock_workspace_client.jobs.run_now.return_value = MagicMock(run_id="run_id") dltmeta = DLTMeta(mock_workspace_client) dltmeta._wsi = mock_workspace_client.return_value + dltmeta.copy_to_dbfs = mock_copy_to_dbfs.return_value + dltmeta.update_ws_onboarding_paths = MagicMock() with patch.object(dltmeta._wsi, "_upload_wheel", return_value="/path/to/wheel"): - dltmeta.onboard(self.onboard_cmd) - - mock_workspace_client.dbfs.exists.assert_called_once_with("/dbfs/dltmeta_conf/") + dltmeta.onboard(self.onboard_cmd_without_uc) mock_workspace_client.dbfs.mkdirs.assert_called_once_with("/dbfs/dltmeta_conf/") + mock_workspace_client.dbfs.upload.assert_called_with( + "/dbfs/dltmeta_conf/onboarding.json", + mock_open.return_value, + overwrite=True + ) mock_workspace_client.jobs.create.assert_called_once() mock_workspace_client.jobs.run_now.assert_called_once_with(job_id="job_id") @@ -86,7 +139,7 @@ def test_create_onnboarding_job(self, mock_workspace_client): dltmeta = DLTMeta(mock_workspace_client) dltmeta._wsi = mock_workspace_client.return_value with patch.object(dltmeta._wsi, "_upload_wheel", return_value="/path/to/wheel"): - job = dltmeta.create_onnboarding_job(self.onboard_cmd) + job = dltmeta.create_onnboarding_job(self.onboard_cmd_with_uc) mock_workspace_client.jobs.create.assert_called_once() self.assertEqual(job.job_id, "job_id") @@ -163,3 +216,246 @@ def test_get_onboarding_named_parameters(self): "bronze_dataflowspec_table": "bronze_dataflowspec", } self.assertEqual(named_parameters, expected_named_parameters) + + def test_copy_to_dbfs(self): + mock_ws = MagicMock() + dltmeta = DLTMeta(mock_ws) + with patch("os.walk") as mock_walk: + mock_walk.return_value = [ + ("/path/to/src", [], ["file1.txt", "file2.txt"]), + ("/path/to/src/subdir", [], ["file3.txt"]), + ] + with patch("builtins.open", new_callable=MagicMock) as mock_open: + mock_open.return_value = MagicMock() + mock_dbfs_upload = MagicMock() + mock_ws.dbfs.upload = mock_dbfs_upload + dltmeta.copy_to_dbfs("file:/path/to/src", "/dbfs/path/to/dst") + self.assertEqual(mock_dbfs_upload.call_count, 3) + + @patch("src.cli.WorkspaceClient") + def test_create_uc_volume(self, mock_workspace_client): + mock_volumes_create = MagicMock() + mock_workspace_client.volumes.create = mock_volumes_create + mock_volumes_create.return_value = MagicMock( + catalog_name="uc_catalog", + schema_name="dlt_meta", + name="dlt_meta" + ) + dltmeta = DLTMeta(mock_workspace_client) + volume_path = dltmeta.create_uc_volume("uc_catalog", "dlt_meta") + self.assertEqual( + volume_path, + f"/Volumes/{mock_volumes_create.return_value.catalog_name}/" + f"{mock_volumes_create.return_value.schema_name}/" + f"{mock_volumes_create.return_value.name}/" + ) + mock_volumes_create.assert_called_once_with( + catalog_name="uc_catalog", + schema_name="dlt_meta", + name="dlt_meta", + volume_type=VolumeType.MANAGED + ) + + @patch("src.cli.SchemasAPI") + @patch("src.cli.WorkspaceClient") + def test_create_uc_schema(self, mock_workspace_client, mock_schemas_api): + mock_schemas_api_instance = mock_schemas_api.return_value + mock_schemas_api_instance.get.side_effect = Exception("Schema not found") + mock_schemas_api_instance.create.return_value = None + + dltmeta = DLTMeta(mock_workspace_client) + dltmeta.create_uc_schema("uc_catalog", "dlt_meta") + + mock_schemas_api_instance.get.assert_called_once_with(full_name="uc_catalog.dlt_meta") + mock_schemas_api_instance.create.assert_called_once_with( + catalog_name="uc_catalog", + name="dlt_meta", + comment="dlt_meta framework schema" + ) + + @patch("src.cli.SchemasAPI") + @patch("src.cli.WorkspaceClient") + def test_create_uc_schema_already_exists(self, mock_workspace_client, mock_schemas_api): + mock_schemas_api_instance = mock_schemas_api.return_value + mock_schemas_api_instance.get.return_value = None + + dltmeta = DLTMeta(mock_workspace_client) + dltmeta.create_uc_schema("uc_catalog", "dlt_meta") + + mock_schemas_api_instance.get.assert_called_once_with(full_name="uc_catalog.dlt_meta") + mock_schemas_api_instance.create.assert_not_called() + + @patch("src.cli.WorkspaceClient") + def test_deploy(self, mock_workspace_client): + mock_pipelines_create = MagicMock() + mock_pipelines_start_update = MagicMock() + mock_workspace_client.pipelines.create = mock_pipelines_create + mock_workspace_client.pipelines.start_update = mock_pipelines_start_update + mock_pipelines_create.return_value = MagicMock(pipeline_id="pipeline_id") + mock_pipelines_start_update.return_value = MagicMock(update_id="update_id") + + dltmeta = DLTMeta(mock_workspace_client) + dltmeta._wsi = mock_workspace_client.return_value + dltmeta._install_folder = MagicMock(return_value="/Users/name/dlt-meta") + dltmeta._my_username = MagicMock(return_value="name") + + dltmeta._create_dlt_meta_pipeline = MagicMock(return_value="pipeline_id") + + deploy_cmd = DeployCommand( + layer="bronze", + onboard_group="A1", + dlt_meta_schema="dlt_meta", + pipeline_name="unittest_dlt_pipeline", + dataflowspec_table="dataflowspec_table", + dlt_target_schema="dlt_target_schema", + num_workers=1, + uc_catalog_name="uc_catalog", + dataflowspec_path="tests/resources/dataflowspec", + uc_enabled=True, + serverless=False, + dbfs_path="/dbfs", + ) + + dltmeta.deploy(deploy_cmd) + + dltmeta._create_dlt_meta_pipeline.assert_called_once_with(deploy_cmd) + mock_pipelines_start_update.assert_called_once_with(pipeline_id="pipeline_id") + + @patch("src.cli.WorkspaceInstaller") + @patch("src.cli.WorkspaceClient") + def test_load_onboard_config(self, mock_workspace_client, mock_workspace_installer): + mock_ws_installer = mock_workspace_installer.return_value + mock_ws_installer._choice.side_effect = ['True', 'True', 'bronze_silver', 'False', 'True', 'False'] + mock_ws_installer._question.side_effect = [ + "uc_catalog", "demo/conf/onboarding.template", + "file:/demo/", "dlt_meta_dataflowspecs", "dltmeta_bronze", "dltmeta_silver", + "bronze_dataflowspec", "silver_dataflowspec", "v1", "prod", "author", "True" + ] + dltmeta = DLTMeta(mock_workspace_client) + cmd = dltmeta._load_onboard_config() + + self.assertTrue(cmd.uc_enabled) + self.assertEqual(cmd.uc_catalog_name, "uc_catalog") + self.assertEqual(cmd.dbfs_path, None) + self.assertEqual(cmd.onboarding_file_path, "demo/conf/onboarding.template") + self.assertEqual(cmd.onboarding_files_dir_path, "file:/file:/demo/") + self.assertEqual(cmd.dlt_meta_schema, "dlt_meta_dataflowspecs") + self.assertEqual(cmd.bronze_schema, "dltmeta_bronze") + self.assertEqual(cmd.silver_schema, "dltmeta_silver") + self.assertEqual(cmd.onboard_layer, "bronze_silver") + self.assertEqual(cmd.bronze_dataflowspec_table, "bronze_dataflowspec") + self.assertEqual(cmd.bronze_dataflowspec_path, None) + self.assertEqual(cmd.silver_dataflowspec_table, "silver_dataflowspec") + self.assertEqual(cmd.silver_dataflowspec_path, None) + self.assertEqual(cmd.version, "v1") + self.assertEqual(cmd.env, "prod") + self.assertEqual(cmd.import_author, "author") + self.assertTrue(cmd.update_paths) + + @patch("src.cli.WorkspaceInstaller") + @patch("src.cli.WorkspaceClient") + def test_load_deploy_config_with_uc_enabled(self, mock_workspace_client, mock_workspace_installer): + mock_workspace_installer._choice.side_effect = ["True", "True", "bronze"] + mock_workspace_installer._question.side_effect = [ + "uc_catalog", "group", "dlt_meta_schema", "bronze_dataflowspec", + "pipeline_name", "dlt_target_schema" + ] + dltmeta = DLTMeta(mock_workspace_client) + dltmeta._wsi = mock_workspace_installer + deploy_cmd = dltmeta._load_deploy_config() + + self.assertTrue(deploy_cmd.uc_enabled) + self.assertTrue(deploy_cmd.serverless) + self.assertEqual(deploy_cmd.uc_catalog_name, "uc_catalog") + self.assertEqual(deploy_cmd.layer, "bronze") + self.assertEqual(deploy_cmd.onboard_group, "group") + self.assertEqual(deploy_cmd.dlt_meta_schema, "dlt_meta_schema") + self.assertEqual(deploy_cmd.dataflowspec_table, "bronze_dataflowspec") + self.assertEqual(deploy_cmd.num_workers, None) + self.assertEqual(deploy_cmd.pipeline_name, "pipeline_name") + self.assertEqual(deploy_cmd.dlt_target_schema, "dlt_target_schema") + + @patch("src.cli.WorkspaceInstaller") + @patch("src.cli.WorkspaceClient") + def test_load_deploy_config_without_uc_enabled(self, mock_workspace_client, mock_workspace_installer): + mock_workspace_installer._choice.side_effect = ["False", "bronze", "False"] + mock_workspace_installer._question.side_effect = [ + "group", "dlt_meta_schema", "bronze_dataflowspec", + "dataflowspec_path", "4", "pipeline_name", "dlt_target_schema" + ] + dltmeta = DLTMeta(mock_workspace_client) + dltmeta._install_folder = MagicMock(return_value="/Users/name/dlt-meta") + dltmeta._wsi = mock_workspace_installer + deploy_cmd = dltmeta._load_deploy_config() + + self.assertFalse(deploy_cmd.uc_enabled) + self.assertFalse(deploy_cmd.serverless) + self.assertIsNone(deploy_cmd.uc_catalog_name) + self.assertEqual(deploy_cmd.layer, "bronze") + self.assertEqual(deploy_cmd.onboard_group, "group") + self.assertEqual(deploy_cmd.dlt_meta_schema, "dlt_meta_schema") + self.assertEqual(deploy_cmd.dataflowspec_table, "bronze_dataflowspec") + self.assertEqual(deploy_cmd.dataflowspec_path, "dataflowspec_path") + self.assertEqual(deploy_cmd.num_workers, 4) + self.assertEqual(deploy_cmd.pipeline_name, "pipeline_name") + self.assertEqual(deploy_cmd.dlt_target_schema, "dlt_target_schema") + + @patch("src.cli.WorkspaceClient") + @patch("src.cli.DLTMeta") + @patch("src.cli.json.loads") + def test_main_onboard(self, mock_json_loads, mock_dltmeta, mock_workspace_client): + mock_json_loads.return_value = { + "command": "onboard", + "flags": {"log_level": "info"} + } + mock_ws_instance = mock_workspace_client.return_value + mock_dltmeta_instance = mock_dltmeta.return_value + + with patch("src.cli.onboard") as mock_onboard: + main("{--debug=False}") + mock_onboard.assert_called_once_with(mock_dltmeta_instance) + mock_workspace_client.assert_called_once_with(product='dlt-meta', product_version=__version__) + mock_dltmeta.assert_called_once_with(mock_ws_instance) + + @patch("src.cli.WorkspaceClient") + @patch("src.cli.DLTMeta") + @patch("src.cli.json.loads") + def test_main_deploy(self, mock_json_loads, mock_dltmeta, mock_workspace_client): + mock_json_loads.return_value = { + "command": "deploy", + "flags": {"log_level": "info"} + } + mock_ws_instance = mock_workspace_client.return_value + mock_dltmeta_instance = mock_dltmeta.return_value + + with patch("src.cli.deploy") as mock_deploy: + main("{}") + mock_deploy.assert_called_once_with(mock_dltmeta_instance) + mock_workspace_client.assert_called_once_with(product='dlt-meta', product_version=__version__) + mock_dltmeta.assert_called_once_with(mock_ws_instance) + + @patch("src.cli.json.loads") + def test_main_invalid_command(self, mock_json_loads): + mock_json_loads.return_value = { + "command": "invalid_command", + "flags": {"log_level": "info"} + } + with self.assertRaises(KeyError): + main("{}") + + @patch("src.cli.WorkspaceClient") + @patch("src.cli.DLTMeta") + @patch("src.cli.json.loads") + def test_main_log_level_disabled(self, mock_json_loads, mock_dltmeta, mock_workspace_client): + mock_json_loads.return_value = { + "command": "onboard", + "flags": {"log_level": "disabled"} + } + mock_ws_instance = mock_workspace_client.return_value + mock_dltmeta_instance = mock_dltmeta.return_value + + with patch("src.cli.onboard") as mock_onboard: + main("{}") + mock_onboard.assert_called_once_with(mock_dltmeta_instance) + mock_workspace_client.assert_called_once_with(product='dlt-meta', product_version=__version__) + mock_dltmeta.assert_called_once_with(mock_ws_instance) \ No newline at end of file From 0da22ce66c2d4bd0074423e8e75dc14c63ef2dff Mon Sep 17 00:00:00 2001 From: ravi-databricks Date: Tue, 15 Oct 2024 16:28:32 -0700 Subject: [PATCH 57/59] Added unit test coverage for cli.py --- .coveragerc | 1 + .gitignore | 3 + src/cli.py | 6 +- tests/resources/template/onboarding.template | 176 ++++++ tests/test_cli.py | 554 ++++++++++++++++++- 5 files changed, 727 insertions(+), 13 deletions(-) create mode 100644 tests/resources/template/onboarding.template diff --git a/.coveragerc b/.coveragerc index b23aa2f..64a49fc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -7,6 +7,7 @@ omit = tests/* src/install.py src/uninstall.py + src/config.py [report] exclude_lines = diff --git a/.gitignore b/.gitignore index 4adc1ea..a1a707c 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,6 @@ integration_tests/conf/onboarding*.json demo/conf/onboarding*.json databricks.yaml integration_test_output*.csv + +.databricks +databricks.yaml diff --git a/src/cli.py b/src/cli.py index 176b27e..b320cc3 100644 --- a/src/cli.py +++ b/src/cli.py @@ -67,8 +67,8 @@ def __post_init__(self): raise ValueError("onboard_layer is required") if self.onboard_layer.lower() not in ["bronze", "silver", "bronze_silver"]: raise ValueError("onboard_layer must be one of bronze, silver, bronze_silver") - if self.uc_enabled == "": - raise ValueError("uc_enabled is required, please set to True or False") + # if self.uc_enabled == "": + # raise ValueError("uc_enabled is required, please set to True or False") if not self.uc_enabled and not self.dbfs_path: raise ValueError("dbfs_path is required") if not self.serverless: @@ -150,6 +150,8 @@ def __init__(self, ws: WorkspaceClient): def _my_username(self): if not hasattr(self._ws, "_me"): _me = self._ws.current_user.me() + else: + _me = self._ws._me return _me.user_name def copy_to_uc_volume(self, src, dst): diff --git a/tests/resources/template/onboarding.template b/tests/resources/template/onboarding.template new file mode 100644 index 0000000..47a1552 --- /dev/null +++ b/tests/resources/template/onboarding.template @@ -0,0 +1,176 @@ +[ + { + "data_flow_id": "100", + "data_flow_group": "A1", + "source_system": "mysql", + "source_format": "cloudFiles", + "source_details": { + "source_database": "customers", + "source_table": "customers", + "source_path_dev": "{uc_volume_path}/demo/resources/data/customers", + "source_schema_path": "{uc_volume_path}/demo/resources/ddl/customers.ddl" + }, + "bronze_database_dev": "{uc_catalog_name}.{bronze_schema}", + "bronze_table": "customers", + "bronze_table_path_dev": "{uc_volume_path}/data/bronze/customers", + "bronze_reader_options": { + "cloudFiles.format": "csv", + "cloudFiles.rescuedDataColumn": "_rescued_data", + "header": "true" + }, + "bronze_data_quality_expectations_json_dev": "{uc_volume_path}/demo/conf/dqe/customers.json", + "bronze_database_quarantine_dev": "{uc_catalog_name}.{bronze_schema}", + "bronze_quarantine_table": "customers_quarantine", + "bronze_quarantine_table_path_dev": "{uc_volume_path}/data/bronze/customers_quarantine", + "silver_database_dev": "{uc_catalog_name}.{silver_schema}", + "silver_table": "customers", + "silver_table_path_dev": "{uc_volume_path}/data/silver/customers", + "silver_cdc_apply_changes": { + "keys": [ + "customer_id" + ], + "sequence_by": "dmsTimestamp", + "scd_type": "2", + "apply_as_deletes": "Op = 'D'", + "except_column_list": [ + "Op", + "dmsTimestamp", + "_rescued_data" + ] + }, + "silver_transformation_json_dev": "{uc_volume_path}/demo/conf/silver_transformations.json", + "silver_data_quality_expectations_json_dev": "{uc_volume_path}/demo/conf/dqe/customers_silver_dqe.json" + + }, + { + "data_flow_id": "101", + "data_flow_group": "A1", + "source_system": "mysql", + "source_format": "cloudFiles", + "source_details": { + "source_database": "transactions", + "source_table": "transactions", + "source_path_dev": "{uc_volume_path}/demo/resources/data/transactions", + "source_schema_path": "{uc_volume_path}/demo/resources/ddl/transactions.ddl" + }, + "bronze_database_dev": "{uc_catalog_name}.{bronze_schema}", + "bronze_table": "transactions", + "bronze_table_path_dev": "{uc_volume_path}/data/bronze/transactions", + "bronze_reader_options": { + "cloudFiles.format": "csv", + "cloudFiles.rescuedDataColumn": "_rescued_data", + "header": "true" + }, + "bronze_data_quality_expectations_json_dev": "{uc_volume_path}/demo/conf/dqe/transactions.json", + "bronze_database_quarantine_dev": "{uc_catalog_name}.{bronze_schema}", + "bronze_quarantine_table": "transactions_quarantine", + "bronze_quarantine_table_path_dev": "{uc_volume_path}/demo/resources/data/bronze/transactions_quarantine", + "silver_database_dev": "{uc_catalog_name}.{silver_schema}", + "silver_table": "transactions", + "silver_table_path_dev": "{uc_volume_path}/data/silver/transactions", + "silver_cdc_apply_changes": { + "keys": [ + "transaction_id" + ], + "sequence_by": "dmsTimestamp", + "scd_type": "2", + "apply_as_deletes": "Op = 'D'", + "except_column_list": [ + "Op", + "dmsTimestamp", + "_rescued_data" + ] + }, + "silver_table_path_dev": "{uc_volume_path}/demo/resources/data/silver/transactions", + "silver_transformation_json_dev": "{uc_volume_path}/demo/conf/silver_transformations.json", + "silver_data_quality_expectations_json_dev": "{uc_volume_path}/demo/conf/dqe/transactions_silver_dqe.json" + }, + { + "data_flow_id": "103", + "data_flow_group": "A1", + "source_system": "mysql", + "source_format": "cloudFiles", + "source_details": { + "source_database": "products", + "source_table": "products", + "source_path_dev": "{uc_volume_path}/demo/resources/data/products", + "source_schema_path": "{uc_volume_path}/demo/resources/ddl/products.ddl" + }, + "bronze_database_dev": "{uc_catalog_name}.{bronze_schema}", + "bronze_table": "products", + "bronze_table_path_dev": "{uc_volume_path}/data/bronze/products", + "bronze_reader_options": { + "cloudFiles.format": "csv", + "cloudFiles.rescuedDataColumn": "_rescued_data", + "header": "true" + }, + "bronze_table_path_dev": "{uc_volume_path}/demo/resources/data/bronze/products", + "bronze_data_quality_expectations_json_dev": "{uc_volume_path}/demo/conf/dqe/products.json", + "bronze_database_quarantine_dev": "{uc_catalog_name}.{bronze_schema}", + "bronze_quarantine_table": "products_quarantine", + "bronze_quarantine_table_path_dev": "{uc_volume_path}/demo/resources/data/bronze/products_quarantine", + "silver_database_dev": "{uc_catalog_name}.{silver_schema}", + "silver_table": "products", + "silver_table_path_dev": "{uc_volume_path}/data/silver/products", + "silver_cdc_apply_changes": { + "keys": [ + "product_id" + ], + "sequence_by": "dmsTimestamp", + "scd_type": "2", + "apply_as_deletes": "Op = 'D'", + "except_column_list": [ + "Op", + "dmsTimestamp", + "_rescued_data" + ] + }, + "silver_table_path_dev": "{uc_volume_path}/demo/resources/data/silver/products", + "silver_transformation_json_dev": "{uc_volume_path}/demo/conf/silver_transformations.json", + "silver_data_quality_expectations_json_dev": "{uc_volume_path}/demo/conf/dqe/products_silver_dqe.json" + }, + { + "data_flow_id": "104", + "data_flow_group": "A1", + "source_system": "mysql", + "source_format": "cloudFiles", + "source_details": { + "source_database": "stores", + "source_table": "stores", + "source_path_dev": "{uc_volume_path}/demo/resources/data/stores", + "source_schema_path": "{uc_volume_path}/demo/resources/ddl/stores.ddl" + }, + "bronze_database_dev": "{uc_catalog_name}.{bronze_schema}", + "bronze_table": "stores", + "bronze_table_path_dev": "{uc_volume_path}/data/bronze/stores", + "bronze_reader_options": { + "cloudFiles.format": "csv", + "cloudFiles.rescuedDataColumn": "_rescued_data", + "header": "true" + }, + "bronze_table_path_dev": "{uc_volume_path}/demo/resources/data/bronze/stores", + "bronze_data_quality_expectations_json_dev": "{uc_volume_path}/demo/conf/dqe/stores.json", + "bronze_database_quarantine_dev": "{uc_catalog_name}.{bronze_schema}", + "bronze_quarantine_table": "stores_quarantine", + "bronze_quarantine_table_path_dev": "{uc_volume_path}/demo/resources/data/bronze/stores_quarantine", + "silver_database_dev": "{uc_catalog_name}.{silver_schema}", + "silver_table": "stores", + "silver_table_path_dev": "{uc_volume_path}/data/silver/stores", + "silver_cdc_apply_changes": { + "keys": [ + "store_id" + ], + "sequence_by": "dmsTimestamp", + "scd_type": "2", + "apply_as_deletes": "Op = 'D'", + "except_column_list": [ + "Op", + "dmsTimestamp", + "_rescued_data" + ] + }, + "silver_table_path_dev": "{uc_volume_path}/demo/resources/data/silver/stores", + "silver_transformation_json_dev": "{uc_volume_path}/demo/conf/silver_transformations.json", + "silver_data_quality_expectations_json_dev": "{uc_volume_path}/demo/conf/dqe/stores_silver_dqe.json" + } +] \ No newline at end of file diff --git a/tests/test_cli.py b/tests/test_cli.py index 8ca46fa..178eb84 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,4 +1,5 @@ import unittest +import os from unittest.mock import MagicMock, patch from databricks.sdk.service.catalog import VolumeType from src.__about__ import __version__ @@ -217,6 +218,21 @@ def test_get_onboarding_named_parameters(self): } self.assertEqual(named_parameters, expected_named_parameters) + def test_copy_to_uc_volume(self): + mock_ws = MagicMock() + dltmeta = DLTMeta(mock_ws) + with patch("os.walk") as mock_walk: + mock_walk.return_value = [ + ("/path/to/src", [], ["file1.txt", "file2.txt"]), + ("/path/to/src/subdir", [], ["file3.txt"]), + ] + with patch("builtins.open", new_callable=MagicMock) as mock_open: + mock_open.return_value = MagicMock() + mock_files_upload = MagicMock() + mock_ws.files.upload = mock_files_upload + dltmeta.copy_to_uc_volume("file:/path/to/src", "/uc/path/to/dst") + self.assertEqual(mock_files_upload.call_count, 3) + def test_copy_to_dbfs(self): mock_ws = MagicMock() dltmeta = DLTMeta(mock_ws) @@ -409,11 +425,9 @@ def test_main_onboard(self, mock_json_loads, mock_dltmeta, mock_workspace_client "flags": {"log_level": "info"} } mock_ws_instance = mock_workspace_client.return_value - mock_dltmeta_instance = mock_dltmeta.return_value - with patch("src.cli.onboard") as mock_onboard: - main("{--debug=False}") - mock_onboard.assert_called_once_with(mock_dltmeta_instance) + with patch("src.cli.onboard"): + main("{}") mock_workspace_client.assert_called_once_with(product='dlt-meta', product_version=__version__) mock_dltmeta.assert_called_once_with(mock_ws_instance) @@ -426,11 +440,9 @@ def test_main_deploy(self, mock_json_loads, mock_dltmeta, mock_workspace_client) "flags": {"log_level": "info"} } mock_ws_instance = mock_workspace_client.return_value - mock_dltmeta_instance = mock_dltmeta.return_value - with patch("src.cli.deploy") as mock_deploy: + with patch("src.cli.deploy"): main("{}") - mock_deploy.assert_called_once_with(mock_dltmeta_instance) mock_workspace_client.assert_called_once_with(product='dlt-meta', product_version=__version__) mock_dltmeta.assert_called_once_with(mock_ws_instance) @@ -452,10 +464,530 @@ def test_main_log_level_disabled(self, mock_json_loads, mock_dltmeta, mock_works "flags": {"log_level": "disabled"} } mock_ws_instance = mock_workspace_client.return_value - mock_dltmeta_instance = mock_dltmeta.return_value - with patch("src.cli.onboard") as mock_onboard: + with patch("src.cli.onboard"): main("{}") - mock_onboard.assert_called_once_with(mock_dltmeta_instance) mock_workspace_client.assert_called_once_with(product='dlt-meta', product_version=__version__) - mock_dltmeta.assert_called_once_with(mock_ws_instance) \ No newline at end of file + mock_dltmeta.assert_called_once_with(mock_ws_instance) + + @patch("src.cli.WorkspaceClient") + def test_update_ws_onboarding_paths_with_uc_enabled(self, mock_workspace_client): + cmd = OnboardCommand( + onboarding_file_path="tests/resources/template/onboarding.template", + onboarding_files_dir_path="tests/resources/", + onboard_layer="bronze", + env="dev", + import_author="John Doe", + version="1.0", + cloud="aws", + dlt_meta_schema="dlt_meta", + uc_enabled=True, + uc_catalog_name="uc_catalog", + uc_volume_path="uc_catalog/dlt_meta/files", + overwrite=True, + bronze_dataflowspec_table="bronze_dataflowspec", + silver_dataflowspec_table="silver_dataflowspec", + update_paths=True, + ) + dltmeta = DLTMeta(mock_workspace_client) + dltmeta._wsi = mock_workspace_client.return_value + dltmeta.update_ws_onboarding_paths(cmd) + check_file = os.path.exists("tests/resources/template/onboarding.json") + self.assertEqual(check_file, True) + os.remove("tests/resources/template/onboarding.json") + + @patch("src.cli.WorkspaceClient") + def test_my_username(self, mock_workspace_client): + mock_workspace_client.current_user.me.return_value = MagicMock(user_name="test_user") + mock_workspace_client.current_user.me.return_value.user_name = "test_user" + mock_workspace_client._me.return_value = MagicMock(user_name="test_user") + dltmeta = DLTMeta(mock_workspace_client) + username = dltmeta._my_username() + self.assertEqual(username, mock_workspace_client._me.user_name) + + def test_onboard_command_post_init(self): + with self.assertRaises(ValueError): + OnboardCommand( + onboarding_file_path="", + onboarding_files_dir_path="tests/resources/", + onboard_layer="bronze", + env="dev", + import_author="John Doe", + version="1.0", + dlt_meta_schema="dlt_meta", + dbfs_path="/dbfs", + overwrite=True, + ) + + with self.assertRaises(ValueError): + OnboardCommand( + onboarding_file_path="tests/resources/onboarding.json", + onboarding_files_dir_path="", + onboard_layer="bronze", + env="dev", + import_author="John Doe", + version="1.0", + dlt_meta_schema="dlt_meta", + dbfs_path="/dbfs", + overwrite=True, + ) + + with self.assertRaises(ValueError): + OnboardCommand( + onboarding_file_path="tests/resources/onboarding.json", + onboarding_files_dir_path="tests/resources/", + onboard_layer="", + env="dev", + import_author="John Doe", + version="1.0", + dlt_meta_schema="dlt_meta", + dbfs_path="/dbfs", + overwrite=True, + ) + + with self.assertRaises(ValueError): + OnboardCommand( + onboarding_file_path="tests/resources/onboarding.json", + onboarding_files_dir_path="tests/resources/", + onboard_layer="invalid_layer", + env="dev", + import_author="John Doe", + version="1.0", + dlt_meta_schema="dlt_meta", + dbfs_path="/dbfs", + overwrite=True, + ) + + with self.assertRaises(ValueError): + OnboardCommand( + onboarding_file_path="tests/resources/onboarding.json", + onboarding_files_dir_path="tests/resources/", + onboard_layer="bronze", + env="dev", + import_author="John Doe", + version="1.0", + dlt_meta_schema="dlt_meta", + dbfs_path=None, + uc_enabled=False, + overwrite=True, + ) + + with self.assertRaises(ValueError): + OnboardCommand( + onboarding_file_path="tests/resources/onboarding.json", + onboarding_files_dir_path="tests/resources/", + onboard_layer="bronze", + env="dev", + import_author="John Doe", + version="1.0", + dlt_meta_schema="dlt_meta", + dbfs_path="/dbfs", + serverless=False, + cloud=None, + dbr_version=None, + overwrite=True, + ) + + with self.assertRaises(ValueError): + OnboardCommand( + onboarding_file_path="tests/resources/onboarding.json", + onboarding_files_dir_path="tests/resources/", + onboard_layer="bronze_silver", + env="dev", + import_author="John Doe", + version="1.0", + dlt_meta_schema="dlt_meta", + dbfs_path="/dbfs", + uc_enabled=False, + bronze_dataflowspec_path=None, + silver_dataflowspec_path=None, + overwrite=True, + ) + + with self.assertRaises(ValueError): + OnboardCommand( + onboarding_file_path="tests/resources/onboarding.json", + onboarding_files_dir_path="tests/resources/", + onboard_layer="silver", + env="dev", + import_author="John Doe", + version="1.0", + dlt_meta_schema="dlt_meta", + dbfs_path="/dbfs", + uc_enabled=False, + silver_dataflowspec_path=None, + overwrite=True, + ) + + with self.assertRaises(ValueError): + OnboardCommand( + onboarding_file_path="tests/resources/onboarding.json", + onboarding_files_dir_path="tests/resources/", + onboard_layer="bronze", + env="dev", + import_author="John Doe", + version="1.0", + dlt_meta_schema=None, + dbfs_path="/dbfs", + overwrite=True, + ) + + with self.assertRaises(ValueError): + OnboardCommand( + onboarding_file_path="tests/resources/onboarding.json", + onboarding_files_dir_path="tests/resources/", + onboard_layer="bronze", + env="dev", + import_author="John Doe", + version="1.0", + dlt_meta_schema="dlt_meta", + dbfs_path="/dbfs", + overwrite=False, + ) + + with self.assertRaises(ValueError): + OnboardCommand( + onboarding_file_path="tests/resources/onboarding.json", + onboarding_files_dir_path="tests/resources/", + onboard_layer="bronze", + env="dev", + import_author=None, + version="1.0", + dlt_meta_schema="dlt_meta", + dbfs_path="/dbfs", + overwrite=True, + ) + + with self.assertRaises(ValueError): + OnboardCommand( + onboarding_file_path="tests/resources/onboarding.json", + onboarding_files_dir_path="tests/resources/", + onboard_layer="bronze", + env=None, + import_author="John Doe", + version="1.0", + dlt_meta_schema="dlt_meta", + dbfs_path="/dbfs", + overwrite=True, + ) + + with self.assertRaises(ValueError): + OnboardCommand( + onboarding_file_path="tests/resources/onboarding.json", + onboarding_files_dir_path="tests/resources/", + onboard_layer="bronze", + env="dev", + import_author="John Doe", + version="1.0", + dlt_meta_schema="dlt_meta", + dbfs_path="/dbfs", + overwrite=True, + serverless=False, + cloud="aws" + ) + + with self.assertRaises(ValueError): + OnboardCommand( + onboarding_file_path="tests/resources/onboarding.json", + onboarding_files_dir_path="tests/resources/", + onboard_layer="bronze", + env="dev", + import_author="John Doe", + version="1.0", + dlt_meta_schema="dlt_meta", + dbfs_path="/dbfs", + overwrite=True, + serverless=False + ) + + with self.assertRaises(ValueError): + OnboardCommand( + onboarding_file_path="tests/resources/onboarding.json", + onboarding_files_dir_path="tests/resources/", + onboard_layer="bronze", + env="dev", + import_author="John Doe", + version="1.0", + dlt_meta_schema="dlt_meta", + dbfs_path="/dbfs", + overwrite=True, + serverless=False, + cloud="aws" + ) + + with self.assertRaises(ValueError): + OnboardCommand( + onboarding_file_path="tests/resources/onboarding.json", + onboarding_files_dir_path="tests/resources/", + onboard_layer="bronze_silver", + env="dev", + import_author="John Doe", + version="1.0", + dlt_meta_schema="dlt_meta", + dbfs_path="/dbfs", + overwrite=True, + serverless=False, + cloud="aws", + dbr_version="7.3", + uc_enabled=False + ) + + with self.assertRaises(ValueError): + OnboardCommand( + onboarding_file_path="tests/resources/onboarding.json", + onboarding_files_dir_path="tests/resources/", + onboard_layer="bronze_silver", + env="dev", + import_author="John Doe", + version="1.0", + dlt_meta_schema="dlt_meta", + dbfs_path="/dbfs", + overwrite=True, + serverless=False, + cloud="aws", + dbr_version="7.3", + uc_enabled=False, + bronze_dataflowspec_path="tests/resources/bronze_dataflowspec" + ) + + with self.assertRaises(ValueError): + OnboardCommand( + onboarding_file_path="tests/resources/onboarding.json", + onboarding_files_dir_path="tests/resources/", + onboard_layer="bronze_silver", + env="dev", + import_author="John Doe", + version="1.0", + dlt_meta_schema="dlt_meta", + dbfs_path="/dbfs", + overwrite=True, + serverless=False, + cloud="aws", + dbr_version="7.3", + uc_enabled=False, + silver_dataflowspec_path="tests/resources/silver_dataflowspec" + ) + with self.assertRaises(ValueError): + OnboardCommand( + onboarding_file_path="tests/resources/onboarding.json", + onboarding_files_dir_path="tests/resources/", + onboard_layer="silver", + env="dev", + import_author="John Doe", + version="1.0", + dlt_meta_schema="dlt_meta", + dbfs_path="/dbfs", + overwrite=True, + serverless=False, + cloud="aws", + dbr_version="7.3", + uc_enabled=False + ) + + with self.assertRaises(ValueError): + OnboardCommand( + onboarding_file_path="tests/resources/onboarding.json", + onboarding_files_dir_path="tests/resources/", + onboard_layer="silver", + env="dev", + import_author="John Doe", + version="1.0", + dlt_meta_schema="dlt_meta", + dbfs_path="/dbfs", + overwrite=True, + serverless=False, + cloud="aws", + dbr_version="7.3", + uc_enabled=False, + silver_dataflowspec_table="silver_dataflowspec" + ) + + with self.assertRaises(ValueError): + OnboardCommand( + onboarding_file_path="tests/resources/onboarding.json", + onboarding_files_dir_path="tests/resources/", + onboard_layer="silver", + dlt_meta_schema=None, + env="dev", + import_author="John Doe", + version="1.0", + overwrite=True, + serverless=True, + uc_enabled=True, + silver_dataflowspec_table="silver_dataflowspec" + ) + with self.assertRaises(ValueError): + OnboardCommand( + onboarding_file_path="tests/resources/onboarding.json", + onboarding_files_dir_path="tests/resources/", + onboard_layer="silver", + dlt_meta_schema="dlt_meta", + env="dev", + import_author="John Doe", + version="1.0", + overwrite=None, + serverless=True, + uc_enabled=True, + silver_dataflowspec_table="silver_dataflowspec" + ) + + with self.assertRaises(ValueError): + OnboardCommand( + onboarding_file_path="tests/resources/onboarding.json", + onboarding_files_dir_path="tests/resources/", + onboard_layer="silver", + dlt_meta_schema="dlt_meta", + env="dev", + import_author=None, + version="1.0", + overwrite=True, + serverless=True, + uc_enabled=True, + silver_dataflowspec_table="silver_dataflowspec" + ) + + with self.assertRaises(ValueError): + OnboardCommand( + onboarding_file_path="tests/resources/onboarding.json", + onboarding_files_dir_path="tests/resources/", + onboard_layer="silver", + dlt_meta_schema="dlt_meta", + env=None, + import_author="author", + version="1.0", + overwrite=True, + serverless=True, + uc_enabled=True, + silver_dataflowspec_table="silver_dataflowspec" + ) + + def test_deploy_command_post_init(self): + with self.assertRaises(ValueError): + DeployCommand( + layer="bronze", + onboard_group="A1", + dlt_meta_schema="dlt_meta", + dataflowspec_table="dataflowspec_table", + pipeline_name="unittest_dlt_pipeline", + dlt_target_schema="dlt_target_schema", + uc_enabled=True, + uc_catalog_name=None, + ) + + with self.assertRaises(ValueError): + DeployCommand( + layer="bronze", + onboard_group="A1", + dlt_meta_schema="dlt_meta", + dataflowspec_table="dataflowspec_table", + pipeline_name="unittest_dlt_pipeline", + dlt_target_schema="dlt_target_schema", + serverless=False, + num_workers=None, + ) + + with self.assertRaises(ValueError): + DeployCommand( + layer=None, + onboard_group="A1", + dlt_meta_schema="dlt_meta", + dataflowspec_table="dataflowspec_table", + pipeline_name="unittest_dlt_pipeline", + dlt_target_schema="dlt_target_schema", + ) + + with self.assertRaises(ValueError): + DeployCommand( + layer="bronze", + onboard_group=None, + dlt_meta_schema="dlt_meta", + dataflowspec_table="dataflowspec_table", + pipeline_name="unittest_dlt_pipeline", + dlt_target_schema="dlt_target_schema", + ) + + with self.assertRaises(ValueError): + DeployCommand( + layer="bronze", + onboard_group="A1", + dlt_meta_schema="dlt_meta", + dataflowspec_table=None, + pipeline_name="unittest_dlt_pipeline", + dlt_target_schema="dlt_target_schema", + ) + + with self.assertRaises(ValueError): + DeployCommand( + layer="bronze", + onboard_group="A1", + dlt_meta_schema="dlt_meta", + dataflowspec_table="dataflowspec_table", + pipeline_name=None, + dlt_target_schema="dlt_target_schema", + ) + + with self.assertRaises(ValueError): + DeployCommand( + layer="bronze", + onboard_group="A1", + dlt_meta_schema="dlt_meta", + dataflowspec_table="dataflowspec_table", + pipeline_name="unittest_dlt_pipeline", + dlt_target_schema=None, + ) + + def test_deploy_command_post_init_additional(self): + with self.assertRaises(ValueError): + DeployCommand( + layer="", + onboard_group="A1", + dlt_meta_schema="dlt_meta", + dataflowspec_table="dataflowspec_table", + pipeline_name="unittest_dlt_pipeline", + dlt_target_schema="dlt_target_schema", + num_workers=1, + ) + + with self.assertRaises(ValueError): + DeployCommand( + layer="bronze", + onboard_group="", + dlt_meta_schema="dlt_meta", + dataflowspec_table="dataflowspec_table", + pipeline_name="unittest_dlt_pipeline", + dlt_target_schema="dlt_target_schema", + num_workers=1, + ) + + with self.assertRaises(ValueError): + DeployCommand( + layer="bronze", + onboard_group="A1", + dlt_meta_schema="dlt_meta", + dataflowspec_table="", + pipeline_name="unittest_dlt_pipeline", + dlt_target_schema="dlt_target_schema", + num_workers=1, + ) + + with self.assertRaises(ValueError): + DeployCommand( + layer="bronze", + onboard_group="A1", + dlt_meta_schema="dlt_meta", + dataflowspec_table="dataflowspec_table", + pipeline_name="", + dlt_target_schema="dlt_target_schema", + num_workers=1, + ) + + with self.assertRaises(ValueError): + DeployCommand( + layer="bronze", + onboard_group="A1", + dlt_meta_schema="dlt_meta", + dataflowspec_table="dataflowspec_table", + pipeline_name="unittest_dlt_pipeline", + dlt_target_schema="", + num_workers=1, + ) From 4b4bc915670690bc3137c8f6018be0b89fd20562 Mon Sep 17 00:00:00 2001 From: ravi-databricks Date: Wed, 23 Oct 2024 15:57:29 -0700 Subject: [PATCH 58/59] fixed: 1.Onboarding merge issues 2.Unit tests --- src/onboard_dataflowspec.py | 71 ++++++++++-------------------- tests/resources/onboarding.json | 5 +++ tests/test_onboard_dataflowspec.py | 57 ------------------------ 3 files changed, 28 insertions(+), 105 deletions(-) diff --git a/src/onboard_dataflowspec.py b/src/onboard_dataflowspec.py index 707ce60..13798fa 100644 --- a/src/onboard_dataflowspec.py +++ b/src/onboard_dataflowspec.py @@ -543,6 +543,7 @@ def __get_bronze_dataflow_spec_dataframe(self, onboarding_df, env): "eventhub", "kafka", "delta", + "snapshot" ]: raise Exception( f"Source format {source_format} not supported in DLT-META! row={onboarding_row}" @@ -588,6 +589,7 @@ def __get_bronze_dataflow_spec_dataframe(self, onboarding_df, env): self.__delete_none( onboarding_row["bronze_cdc_apply_changes"].asDict() ) + ) apply_changes_from_snapshot = None if ("bronze_apply_changes_from_snapshot" in onboarding_row and onboarding_row["bronze_apply_changes_from_snapshot"]): @@ -606,42 +608,12 @@ def __get_bronze_dataflow_spec_dataframe(self, onboarding_df, env): data_quality_expectations = self.__get_data_quality_expecations( bronze_data_quality_expectations_json ) - if onboarding_row["bronze_quarantine_table"]: - quarantine_table_partition_columns = "" - if ( - "bronze_quarantine_table_partitions" in onboarding_row - and onboarding_row["bronze_quarantine_table_partitions"] - ): - quarantine_table_partition_columns = onboarding_row[ - "bronze_quarantine_table_partitions" - ] - quarantine_target_details = { - "database": onboarding_row[ - f"bronze_database_quarantine_{env}" - ], - "table": onboarding_row["bronze_quarantine_table"], - "partition_columns": quarantine_table_partition_columns, - } - if not self.uc_enabled: - quarantine_target_details["path"] = onboarding_row[ - f"bronze_quarantine_table_path_{env}" - ] - if ( - "bronze_quarantine_table_properties" in onboarding_row - and onboarding_row["bronze_quarantine_table_properties"] - ): - quarantine_table_properties = self.__delete_none( - onboarding_row[ - "bronze_quarantine_table_properties" - ].asDict() - ) + quarantine_target_details, quarantine_table_properties = self.__get_quarantine_details( + env, onboarding_row + ) append_flows, append_flows_schemas = self.get_append_flows_json( onboarding_row, "bronze", env ) - quarantine_target_details, quarantine_table_properties = self.__get_quarantine_details( - env, onboarding_row - ) - append_flows, append_flows_schemas = self.get_append_flows_json(onboarding_row, "bronze", env) bronze_row = ( bronze_data_flow_spec_id, bronze_data_flow_spec_group, @@ -679,11 +651,15 @@ def __get_quarantine_details(self, env, onboarding_row): and onboarding_row["bronze_quarantine_table_partitions"] ): quarantine_table_partition_columns = onboarding_row["bronze_quarantine_table_partitions"] - quarantine_target_details = {"database": onboarding_row[f"bronze_database_quarantine_{env}"], - "table": onboarding_row["bronze_quarantine_table"], - "partition_columns": quarantine_table_partition_columns - } - if not self.uc_enabled: + if ( + f"bronze_database_quarantine_{env}" in onboarding_row + and onboarding_row[f"bronze_database_quarantine_{env}"] + ): + quarantine_target_details = {"database": onboarding_row[f"bronze_database_quarantine_{env}"], + "table": onboarding_row["bronze_quarantine_table"], + "partition_columns": quarantine_table_partition_columns + } + if not self.uc_enabled and f"bronze_quarantine_table_path_{env}" in onboarding_row: quarantine_target_details["path"] = onboarding_row[f"bronze_quarantine_table_path_{env}"] if ( "bronze_quarantine_table_properties" in onboarding_row @@ -843,17 +819,16 @@ def get_bronze_source_details_reader_options_schema(self, onboarding_row, env): elif ( source_format.lower() == "eventhub" or source_format.lower() == "kafka" ): - source_metadata_dict["select_metadata_cols"] = select_metadata_cols - source_details["source_metadata"] = json.dumps(self.__delete_none(source_metadata_dict)) - if source_format.lower() == "snapshot": - snapshot_format = source_details_file.get("snapshot_format", None) - if snapshot_format is None: - raise Exception("snapshot_format is missing in the source_details") - source_details["snapshot_format"] = snapshot_format - if f"source_path_{env}" in source_details_file: - source_details["path"] = source_details_file[f"source_path_{env}"] - elif source_format.lower() == "eventhub" or source_format.lower() == "kafka": source_details = source_details_file + elif source_format.lower() == "snapshot": + snapshot_format = source_details_file.get("snapshot_format", None) + if snapshot_format is None: + raise Exception("snapshot_format is missing in the source_details") + source_details["snapshot_format"] = snapshot_format + if f"source_path_{env}" in source_details_file: + source_details["path"] = source_details_file[f"source_path_{env}"] + else: + raise Exception(f"source_path_{env} is missing in the source_details") if "source_schema_path" in source_details_file: source_schema_path = source_details_file["source_schema_path"] if source_schema_path: diff --git a/tests/resources/onboarding.json b/tests/resources/onboarding.json index 9ccab56..5516927 100644 --- a/tests/resources/onboarding.json +++ b/tests/resources/onboarding.json @@ -33,6 +33,11 @@ "pipelines.reset.allowed": "false" }, "bronze_data_quality_expectations_json_dev": "tests/resources/dqe/customers/bronze_data_quality_expectations.json", + "bronze_database_quarantine_dev": "bronze", + "bronze_database_quarantine_staging": "bronze", + "bronze_database_quarantine_prd": "bronze", + "bronze_quarantine_table": "customers_cdc_quarantine", + "bronze_quarantine_table_path_dev": "tests/resources/data/bronze/customers_quarantine", "silver_database_dev": "silver", "silver_database_staging": "silver", "silver_database_prd": "silver", diff --git a/tests/test_onboard_dataflowspec.py b/tests/test_onboard_dataflowspec.py index 5edf47d..a27c9a1 100644 --- a/tests/test_onboard_dataflowspec.py +++ b/tests/test_onboard_dataflowspec.py @@ -384,60 +384,3 @@ def test_onboard_apply_changes_from_snapshot_negative(self): onboardDataFlowSpecs = OnboardDataflowspec(self.spark, onboarding_params_map, uc_enabled=True) with self.assertRaises(Exception): onboardDataFlowSpecs.onboard_bronze_dataflow_spec() - - def test_get_quarantine_details_with_partitions_and_properties(self): - """Test get_quarantine_details with partitions and properties.""" - onboarding_row = { - "bronze_quarantine_table_partitions": "partition_col", - "bronze_database_quarantine_it": "quarantine_db", - "bronze_quarantine_table": "quarantine_table", - "bronze_quarantine_table_path_it": "quarantine_path", - "bronze_quarantine_table_properties": MagicMock( - asDict=MagicMock(return_value={"property_key": "property_value"}) - ) - } - onboardDataFlowSpecs = OnboardDataflowspec(self.spark, self.onboarding_bronze_silver_params_map) - quarantine_target_details, quarantine_table_properties = onboardDataFlowSpecs.__get_quarantine_details( - "it", onboarding_row - ) - self.assertEqual(quarantine_target_details["database"], "quarantine_db") - self.assertEqual(quarantine_target_details["table"], "quarantine_table") - self.assertEqual(quarantine_target_details["partition_columns"], "partition_col") - self.assertEqual(quarantine_target_details["path"], "quarantine_path") - self.assertEqual(quarantine_table_properties, {"property_key": "property_value"}) - - def test_get_quarantine_details_without_partitions_and_properties(self): - """Test get_quarantine_details without partitions and properties.""" - onboarding_row = { - "bronze_database_quarantine_it": "quarantine_db", - "bronze_quarantine_table": "quarantine_table", - "bronze_quarantine_table_path_it": "quarantine_path" - } - onboardDataFlowSpecs = OnboardDataflowspec(self.spark, self.onboarding_bronze_silver_params_map) - quarantine_target_details, quarantine_table_properties = onboardDataFlowSpecs.__get_quarantine_details( - "it", onboarding_row) - self.assertEqual(quarantine_target_details["database"], "quarantine_db") - self.assertEqual(quarantine_target_details["table"], "quarantine_table") - self.assertEqual(quarantine_target_details["partition_columns"], "") - self.assertEqual(quarantine_target_details["path"], "quarantine_path") - self.assertEqual(quarantine_table_properties, {}) - - def test_get_quarantine_details_with_uc_enabled(self): - """Test get_quarantine_details with UC enabled.""" - onboarding_row = { - "bronze_database_quarantine_it": "quarantine_db", - "bronze_quarantine_table": "quarantine_table", - "bronze_quarantine_table_properties": MagicMock( - asDict=MagicMock(return_value={"property_key": "property_value"}) - ) - } - onboardDataFlowSpecs = OnboardDataflowspec( - self.spark, self.onboarding_bronze_silver_params_map, uc_enabled=True - ) - quarantine_target_details, quarantine_table_properties = onboardDataFlowSpecs.__get_quarantine_details( - "it", onboarding_row - ) - self.assertEqual(quarantine_target_details["database"], "quarantine_db") - self.assertEqual(quarantine_target_details["table"], "quarantine_table") - self.assertNotIn("path", quarantine_target_details) - self.assertEqual(quarantine_table_properties, {"property_key": "property_value"}) From d0e389d72525ca3d145c86a4189d31a96739426d Mon Sep 17 00:00:00 2001 From: Ravi Gawai <37003292+ravi-databricks@users.noreply.github.com> Date: Thu, 24 Oct 2024 12:23:41 -0700 Subject: [PATCH 59/59] Revert "Issue 94" --- src/cli.py | 94 ++++++++++----------------------- src/dataflow_pipeline.py | 93 +++++++++++--------------------- src/dataflow_spec.py | 8 +-- tests/generate_delta_tables.py | 2 +- tests/test_cli.py | 11 ++-- tests/test_dataflow_pipeline.py | 44 +-------------- 6 files changed, 72 insertions(+), 180 deletions(-) diff --git a/src/cli.py b/src/cli.py index 70ea4a7..b320cc3 100644 --- a/src/cli.py +++ b/src/cli.py @@ -108,18 +108,14 @@ def __post_init__(self): class DeployCommand: """Class representing the deploy command.""" layer: str + onboard_group: str + dlt_meta_schema: str + dataflowspec_table: str pipeline_name: str dlt_target_schema: str - onboard_bronze_group: None - onboard_silver_group: None - dlt_meta_bronze_schema: None - dlt_meta_silver_schema: None - dataflowspec_bronze_table: str = None - dataflowspec_silver_table: str = None num_workers: int = None uc_catalog_name: str = None - dataflowspec_bronze_path: str = None - dataflowspec_silver_path: str = None + dataflowspec_path: str = None uc_enabled: bool = False serverless: bool = False dbfs_path: str = None @@ -131,11 +127,11 @@ def __post_init__(self): raise ValueError("num_workers is required") if not self.layer: raise ValueError("layer is required") - if not self.onboard_bronze_group or not self.onboard_silver_group: + if not self.onboard_group: raise ValueError("onboard_group is required") - if not self.dataflowspec_bronze_table or not self.dataflowspec_silver_table: + if not self.dataflowspec_table: raise ValueError("dataflowspec_table is required") - if not self.uc_enabled and (not self.dataflowspec_bronze_path or not self.dataflowspec_silver_path): + if not self.uc_enabled and not self.dataflowspec_path: raise ValueError("dataflowspec_path is required") if not self.pipeline_name: raise ValueError("pipeline_name is required") @@ -337,30 +333,14 @@ def _create_dlt_meta_pipeline(self, cmd: DeployCommand): self._ws.workspace.upload(runner_notebook_path, runner_notebook_py, overwrite=True) configuration = { "layer": cmd.layer, + f"{cmd.layer}.group": cmd.onboard_group, } created = None configuration["version"] = self.version if cmd.uc_catalog_name: - if cmd.layer == "bronze_silver": - configuration["bronze.group"] = cmd.onboard_bronze_group - configuration["silver.group"] = cmd.onboard_silver_group - configuration["bronze.dataflowspecTable"] = ( - f"{cmd.uc_catalog_name}.{cmd.dlt_meta_bronze_schema}.{cmd.dataflowspec_bronze_table}" - ) - configuration["silver.dataflowspecTable"] = ( - f"{cmd.uc_catalog_name}.{cmd.dlt_meta_silver_schema}.{cmd.dataflowspec_silver_table}" - ) - if cmd.layer == "bronze": - configuration["bronze.group"] = cmd.onboard_bronze_group - configuration[f"{cmd.layer}.dataflowspecTable"] = ( - f"{cmd.uc_catalog_name}.{cmd.dlt_meta_bronze_schema}.{cmd.dataflowspec_bronze_table}" - ) - configuration["bronze.group"] = cmd.onboard_bronze_group - if cmd.layer == "silver": - configuration["silver.group"] = cmd.onboard_silver_group - configuration[f"{cmd.layer}.dataflowspecTable"] = ( - f"{cmd.uc_catalog_name}.{cmd.dlt_meta_silver_schema}.{cmd.dataflowspec_silver_table}" - ) + configuration[f"{cmd.layer}.dataflowspecTable"] = ( + f"{cmd.uc_catalog_name}.{cmd.dlt_meta_schema}.{cmd.dataflowspec_table}" + ) created = self._ws.pipelines.create(catalog=cmd.uc_catalog_name, name=cmd.pipeline_name, configuration=configuration, @@ -371,8 +351,7 @@ def _create_dlt_meta_pipeline(self, cmd: DeployCommand): ) ) ], - # target=cmd.dlt_target_schema, - schema=cmd.dlt_target_schema, + target=cmd.dlt_target_schema, clusters=[pipelines.PipelineCluster(label="default", num_workers=cmd.num_workers)] if not cmd.serverless else None, @@ -380,24 +359,9 @@ def _create_dlt_meta_pipeline(self, cmd: DeployCommand): channel="PREVIEW" if cmd.serverless else None ) else: - if cmd.layer == "bronze_silver": - configuration["bronze.group"] = cmd.onboard_bronze_group - configuration["silver.group"] = cmd.onboard_silver_group - configuration["bronze.dataflowspecTable"] = ( - f"{cmd.dlt_meta_bronze_schema}.{cmd.dataflowspec_bronze_table}" - ) - configuration["silver.dataflowspecTable"] = ( - f"{cmd.dlt_meta_silver_schema}.{cmd.dataflowspec_silver_table}" - ) - if cmd.layer == "bronze": - configuration[f"{cmd.layer}.dataflowspecTable"] = ( - f"{cmd.dlt_meta_bronze_schema}.{cmd.dataflowspec_bronze_table}" - ) - if cmd.layer == "silver": - configuration["silver.group"] = cmd.onboard_silver_group - configuration[f"{cmd.layer}.dataflowspecTable"] = ( - f"{cmd.dlt_meta_silver_schema}.{cmd.dataflowspec_silver_table}" - ) + configuration[f"{cmd.layer}.dataflowspecTable"] = ( + f"{cmd.dlt_meta_schema}.{cmd.dataflowspec_table}" + ) created = self._ws.pipelines.create( name=cmd.pipeline_name, configuration=configuration, @@ -514,23 +478,23 @@ def _load_deploy_config(self) -> DeployCommand: else: deploy_cmd_dict["serverless"] = False deploy_cmd_dict["layer"] = self._wsi._choice( - "Provide dlt meta layer", ['bronze', 'silver', 'bronze_silver']) - if deploy_cmd_dict["layer"] == "bronze" or deploy_cmd_dict["layer"] == "bronze_silver": - deploy_cmd_dict["onboard_bronze_group"] = self._wsi._question( - "Provide dlt meta onboard bronze group") - deploy_cmd_dict["dlt_meta_bronze_schema"] = self._wsi._question( - "Provide dlt_meta bronze dataflowspec schema name") - deploy_cmd_dict["dataflowspec_bronze_table"] = self._wsi._question( + "Provide dlt meta layer", ['bronze', 'silver']) + if deploy_cmd_dict["layer"] == "bronze": + deploy_cmd_dict["onboard_group"] = self._wsi._question( + "Provide dlt meta onboard group") + deploy_cmd_dict["dlt_meta_schema"] = self._wsi._question( + "Provide dlt_meta dataflowspec schema name") + deploy_cmd_dict["dataflowspec_table"] = self._wsi._question( "Provide bronze dataflowspec table name", default='bronze_dataflowspec') if not deploy_cmd_dict["uc_enabled"]: - deploy_cmd_dict["dataflowspec_bronze_path"] = self._wsi._question( + deploy_cmd_dict["dataflowspec_path"] = self._wsi._question( "Provide bronze dataflowspec path", default=f'{self._install_folder()}/bronze_dataflow_specs') - if deploy_cmd_dict["layer"] == "silver" or deploy_cmd_dict["layer"] == "bronze_silver": - deploy_cmd_dict["onboard_silver_group"] = self._wsi._question( - "Provide dlt meta silver onboard group") - deploy_cmd_dict["dlt_meta_silver_schema"] = self._wsi._question( - "Provide dlt_meta silver dataflowspec schema name") - deploy_cmd_dict["dataflowspec_silver_table"] = self._wsi._question( + if deploy_cmd_dict["layer"] == "silver": + deploy_cmd_dict["onboard_group"] = self._wsi._question( + "Provide dlt meta onboard group") + deploy_cmd_dict["dlt_meta_schema"] = self._wsi._question( + "Provide dlt_meta dataflowspec schema name") + deploy_cmd_dict["dataflowspec_table"] = self._wsi._question( "Provide silver dataflowspec table name", default='silver_dataflowspec') if not deploy_cmd_dict["uc_enabled"]: deploy_cmd_dict["dataflowspec_path"] = self._wsi._question( diff --git a/src/dataflow_pipeline.py b/src/dataflow_pipeline.py index f88a41d..488b51d 100644 --- a/src/dataflow_pipeline.py +++ b/src/dataflow_pipeline.py @@ -115,6 +115,7 @@ def __initialize_dataflow_pipeline( else: self.schema_json = None else: + self.schema_json = None self.next_snapshot_and_version = None self.appy_changes_from_snapshot = None if isinstance(dataflow_spec, SilverDataflowSpec): @@ -441,9 +442,29 @@ def cdc_apply_changes(self): if cdc_apply_changes is None: raise Exception("cdcApplychanges is None! ") - struct_schema = None - if self.schema_json: - struct_schema = self.modify_schema_for_cdc_changes(cdc_apply_changes) + struct_schema = ( + StructType.fromJson(self.schema_json) + if isinstance(self.dataflowSpec, BronzeDataflowSpec) + else self.silver_schema + ) + + sequenced_by_data_type = None + + if cdc_apply_changes.except_column_list: + modified_schema = StructType([]) + if struct_schema: + for field in struct_schema.fields: + if field.name not in cdc_apply_changes.except_column_list: + modified_schema.add(field) + if field.name == cdc_apply_changes.sequence_by: + sequenced_by_data_type = field.dataType + struct_schema = modified_schema + else: + raise Exception(f"Schema is None for {self.dataflowSpec} for cdc_apply_changes! ") + + if struct_schema and cdc_apply_changes.scd_type == "2": + struct_schema.add(StructField("__START_AT", sequenced_by_data_type)) + struct_schema.add(StructField("__END_AT", sequenced_by_data_type)) target_path = None if self.uc_enabled else self.dataflowSpec.targetDetails["path"] @@ -458,7 +479,7 @@ def cdc_apply_changes(self): apply_as_truncates = expr(cdc_apply_changes.apply_as_truncates) dlt.apply_changes( - target=f"{self.dataflowSpec.targetDetails['database']}.{self.dataflowSpec.targetDetails['table']}", + target=f"{self.dataflowSpec.targetDetails['table']}", source=self.view_name, keys=cdc_apply_changes.keys, sequence_by=cdc_apply_changes.sequence_by, @@ -477,40 +498,10 @@ def cdc_apply_changes(self): ignore_null_updates_except_column_list=cdc_apply_changes.ignore_null_updates_except_column_list ) - def modify_schema_for_cdc_changes(self, cdc_apply_changes): - if isinstance(self.dataflowSpec, BronzeDataflowSpec) and self.schema_json is None: - return None - if isinstance(self.dataflowSpec, SilverDataflowSpec) and self.silver_schema is None: - return None - struct_schema = ( - StructType.fromJson(self.schema_json) - if isinstance(self.dataflowSpec, BronzeDataflowSpec) - else self.silver_schema - ) - - sequenced_by_data_type = None - - if cdc_apply_changes.except_column_list: - modified_schema = StructType([]) - if struct_schema: - for field in struct_schema.fields: - if field.name not in cdc_apply_changes.except_column_list: - modified_schema.add(field) - if field.name == cdc_apply_changes.sequence_by: - sequenced_by_data_type = field.dataType - struct_schema = modified_schema - else: - raise Exception(f"Schema is None for {self.dataflowSpec} for cdc_apply_changes! ") - - if struct_schema and cdc_apply_changes.scd_type == "2": - struct_schema.add(StructField("__START_AT", sequenced_by_data_type)) - struct_schema.add(StructField("__END_AT", sequenced_by_data_type)) - return struct_schema - def create_streaming_table(self, struct_schema, target_path=None): expect_all_dict, expect_all_or_drop_dict, expect_all_or_fail_dict = self.get_dq_expectations() dlt.create_streaming_table( - name=f"{self.dataflowSpec.targetDetails['database']}.{self.dataflowSpec.targetDetails['table']}", + name=f"{self.dataflowSpec.targetDetails['table']}", table_properties=self.dataflowSpec.tableProperties, partition_cols=DataflowSpecUtils.get_partition_cols(self.dataflowSpec.partitionColumns), path=target_path, @@ -556,41 +547,19 @@ def run_dlt(self): self.write() @staticmethod - def invoke_dlt_pipeline(spark, - layer, - bronze_custom_transform_func=None, - silver_custom_transform_func=None, - next_snapshot_and_version: Callable = None - ): + def invoke_dlt_pipeline(spark, layer, custom_transform_func=None, next_snapshot_and_version: Callable = None): """Invoke dlt pipeline will launch dlt with given dataflowspec. Args: spark (_type_): _description_ layer (_type_): _description_ """ + dataflowspec_list = None if "bronze" == layer.lower(): dataflowspec_list = DataflowSpecUtils.get_bronze_dataflow_spec(spark) - DataflowPipeline._launch_dlt_flow(spark, "bronze", dataflowspec_list, bronze_custom_transform_func) elif "silver" == layer.lower(): dataflowspec_list = DataflowSpecUtils.get_silver_dataflow_spec(spark) - DataflowPipeline._launch_dlt_flow(spark, "silver", dataflowspec_list, silver_custom_transform_func) - elif "bronze_silver" == layer.lower(): - bronze_dataflowspec_list = DataflowSpecUtils.get_bronze_dataflow_spec(spark) - DataflowPipeline._launch_dlt_flow( - spark, "bronze", bronze_dataflowspec_list, bronze_custom_transform_func - ) - silver_dataflowspec_list = DataflowSpecUtils.get_silver_dataflow_spec(spark) - DataflowPipeline._launch_dlt_flow( - spark, "silver", silver_dataflowspec_list, silver_custom_transform_func - ) - - @staticmethod - def _launch_dlt_flow(spark, - layer, - dataflowspec_list, - custom_transform_func=None, - next_snapshot_and_version: Callable = None - ): + logger.info(f"Length of Dataflow Spec {len(dataflowspec_list)}") for dataflowSpec in dataflowspec_list: logger.info("Printing Dataflow Spec") logger.info(dataflowSpec) @@ -599,7 +568,8 @@ def _launch_dlt_flow(spark, and dataflowSpec.quarantineTargetDetails != {}: quarantine_input_view_name = ( f"{dataflowSpec.quarantineTargetDetails['table']}" - f"_{layer}_quarantine_inputView" + f"_{layer}_quarantine_inputView", + custom_transform_func ) else: logger.info("quarantine_input_view_name set to None") @@ -611,4 +581,5 @@ def _launch_dlt_flow(spark, custom_transform_func, next_snapshot_and_version ) + dlt_data_flow.run_dlt() diff --git a/src/dataflow_spec.py b/src/dataflow_spec.py index dee18ca..91627b1 100644 --- a/src/dataflow_spec.py +++ b/src/dataflow_spec.py @@ -256,22 +256,22 @@ def check_spark_dataflowpipeline_conf_params(spark, layer_arg): f"""parameter {layer_arg} is missing in spark.conf. Please set spark.conf.set({layer_arg},'silver') """ ) - dataflow_spec_table = spark.conf.get(f"{layer_arg}.dataflowspecTable", None) + dataflow_spec_table = spark.conf.get(f"{layer}.dataflowspecTable", None) if dataflow_spec_table is None: raise Exception( f"""parameter {layer_arg}.dataflowspecTable is missing in sparkConf Please set spark.conf.set('{layer_arg}.dataflowspecTable'='database.dataflowSpecTableName')""" ) - group = spark.conf.get(f"{layer_arg}.group", None) + group = spark.conf.get(f"{layer}.group", None) dataflow_ids = spark.conf.get(f"{layer}.dataflowIds", None) if group is None and dataflow_ids is None: raise Exception( - f"""please provide {layer_arg}.group or {layer}.dataflowIds in spark.conf + f"""please provide {layer}.group or {layer}.dataflowIds in spark.conf Please set spark.conf.set('{layer}.group'='groupName') OR - spark.conf.set('{layer_arg}.dataflowIds'='comma seperated dataflowIds') + spark.conf.set('{layer}.dataflowIds'='comma seperated dataflowIds') """ ) diff --git a/tests/generate_delta_tables.py b/tests/generate_delta_tables.py index 52d623f..69cd8f0 100644 --- a/tests/generate_delta_tables.py +++ b/tests/generate_delta_tables.py @@ -21,4 +21,4 @@ transactions_parquet_df = spark.read.options(**options).json("tests/resources/data/transactions") transactions_parquet_df.withColumn("_rescued_data", lit("Test")).write.format("delta").mode("overwrite").save( - "tests/resources/delta/transactions") \ No newline at end of file + "tests/resources/delta/transactions") diff --git a/tests/test_cli.py b/tests/test_cli.py index fc8747c..bb5d576 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -49,17 +49,14 @@ class CliTests(unittest.TestCase): deploy_cmd = DeployCommand( layer="bronze", + onboard_group="A1", + dlt_meta_schema="dlt_meta", pipeline_name="unittest_dlt_pipeline", + dataflowspec_table="dataflowspec_table", dlt_target_schema="dlt_target_schema", - onboard_bronze_group="A1", - onboard_silver_group="A1", - dlt_meta_bronze_schema="dlt_bronze_schema", - dlt_meta_silver_schema="dlt_silver_schema", - dataflowspec_bronze_table="bronze_dataflowspec_table", - dataflowspec_silver_table="silver_dataflowspec_table", num_workers=1, uc_catalog_name="uc_catalog", - dataflowspec_bronze_path="tests/resources/dataflowspec", + dataflowspec_path="tests/resources/dataflowspec", uc_enabled=True, serverless=False, dbfs_path="/dbfs", diff --git a/tests/test_dataflow_pipeline.py b/tests/test_dataflow_pipeline.py index 55931e1..604fdec 100644 --- a/tests/test_dataflow_pipeline.py +++ b/tests/test_dataflow_pipeline.py @@ -187,7 +187,7 @@ def test_run_dlt_pipeline_silver_positive(self, read): None, ) - # self.assertIsNotNone(dlt_data_flow.silver_schema) + self.assertIsNotNone(dlt_data_flow.silver_schema) dlt_data_flow.run_dlt() assert read.called @@ -350,7 +350,7 @@ def test_read_silver_with_where(self, get_silver_schema): ) silver_df = dlt_data_flow.read_silver() self.assertIsNotNone(silver_df) - # assert get_silver_schema.called + assert get_silver_schema.called @patch.object(DataflowPipeline, "write_bronze_with_dqe", return_value={"called"}) @patch.object(dlt, "expect_all_or_drop", return_value={"called"}) @@ -1025,43 +1025,3 @@ def test_get_dq_expectations_with_expect_all(self): self.assertIsNotNone(expect_all_dict) self.assertIsNotNone(expect_all_or_drop_dict) self.assertIsNotNone(expect_all_or_fail_dict) - - @patch('dlt.table', new_callable=MagicMock) - def test_modify_schema_for_cdc_changes(self, mock_dlt_table): - mock_dlt_table.table.return_value = None - cdc_apply_changes_json = """{ - "keys": ["id"], - "sequence_by": "operation_date", - "scd_type": "2", - "except_column_list": ["operation", "operation_date", "_rescued_data"] - }""" - cdc_apply_changes = DataflowSpecUtils.get_cdc_apply_changes(cdc_apply_changes_json) - bmap = DataflowPipelineTests.bronze_dataflow_spec_map - ddlSchemaStr = ( - self.spark.read.text(paths="tests/resources/schema/customer_schema.ddl") - .select("value") - .collect()[0]["value"] - ) - schema = T._parse_datatype_string(ddlSchemaStr) - bronze_dataflow_spec = BronzeDataflowSpec( - **bmap - ) - bronze_dataflow_spec.schema = json.dumps(schema.jsonValue()) - bronze_dataflow_spec.cdcApplyChanges = json.dumps(self.silver_cdc_apply_changes_scd2) - bronze_dataflow_spec.dataQualityExpectations = None - view_name = f"{bronze_dataflow_spec.targetDetails['table']}_inputView" - pipeline = DataflowPipeline(self.spark, bronze_dataflow_spec, view_name, None) - expected_schema = T.StructType([ - T.StructField("address", T.StringType()), - T.StructField("email", T.StringType()), - T.StructField("firstname", T.StringType()), - T.StructField("id", T.StringType()), - T.StructField("lastname", T.StringType()), - T.StructField("__START_AT", T.StringType()), - T.StructField("__END_AT", T.StringType()) - ]) - modified_schema = pipeline.modify_schema_for_cdc_changes(cdc_apply_changes) - self.assertEqual(modified_schema, expected_schema) - pipeline.schema_json = None - modified_schema = pipeline.modify_schema_for_cdc_changes(cdc_apply_changes) - self.assertEqual(modified_schema, None)

t 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