{"id":21524732,"url":"https://github.com/teomewhy/lago-mago","last_synced_at":"2025-08-21T16:25:48.732Z","repository":{"id":247231253,"uuid":"823101946","full_name":"TeoMeWhy/lago-mago","owner":"TeoMeWhy","description":"Projeto de construção de datalake do zero","archived":false,"fork":false,"pushed_at":"2024-08-30T12:21:38.000Z","size":75,"stargazers_count":95,"open_issues_count":4,"forks_count":12,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-08-18T15:58:24.842Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/TeoMeWhy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"TeoMeWhy"}},"created_at":"2024-07-02T12:30:07.000Z","updated_at":"2025-08-16T22:05:49.000Z","dependencies_parsed_at":"2024-07-07T14:42:49.616Z","dependency_job_id":"c21c80a6-d5d7-4660-a085-55da63c5a005","html_url":"https://github.com/TeoMeWhy/lago-mago","commit_stats":null,"previous_names":["teomewhy/lago-mago"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/TeoMeWhy/lago-mago","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TeoMeWhy%2Flago-mago","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TeoMeWhy%2Flago-mago/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TeoMeWhy%2Flago-mago/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TeoMeWhy%2Flago-mago/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/TeoMeWhy","download_url":"https://codeload.github.com/TeoMeWhy/lago-mago/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TeoMeWhy%2Flago-mago/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":271507560,"owners_count":24771825,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-21T02:00:08.990Z","response_time":74,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-24T01:28:49.856Z","updated_at":"2025-08-21T16:25:48.704Z","avatar_url":"https://github.com/TeoMeWhy.png","language":"Python","funding_links":["https://github.com/sponsors/TeoMeWhy"],"categories":[],"sub_categories":[],"readme":"# O Lago do Mago\n\nConstrução de um Lakehouse completamente do zero!\n\nSeja membro ou sub de nossos canais e assista todos os vídeos deste projeto.\n\n- [YouTube](https://www.youtube.com/playlist?list=PLvlkVRRKOYFTcLehYZ2Bd5hGIcLH0dJHE)\n- [Twitch](https://www.twitch.tv/collections/2e8D0Vgd3hf04g)\n\n\u003cimg src=\"https://i.ibb.co/vvzVMnn/Datalake-upsell.png\" alt=\"Datalake-upsell\" border=\"0\"\u003e\n\n## Sobre\n\nA partir dos dados do nosso sistema de pontos, vamos construir ingestões de dados no Databricks.\n\nDB -\u003e Raw -\u003e Bronze -\u003e Silver -\u003e Silver FS -\u003e Modelo I.A.\n\n### Envio dos dados para o bucket S3\n\nCriamos um script em Python que verifica cada novo registro (ou atualização) que ocorre no banco de dados em produto. Este mesmo script envia os dados de cada tabela para o S3 em formato `.parquet`, simulando um `Change Data Capture` (CDC).\n\nFoi realizda uma carga `full-load` dia 13/06/2024, para o mesmo bucket, em um diretório específico.\n\nA criação deste script foi realizado em algumas lives aleatórias do dia a dia (estamos online todos os dias 9AM na [Twitch](https://twitch.tv/teomewhy).\n\n### Setup Databricks\n\nNo primeiro dia de projeto, mostramos como realizar o setup do ambiente do Databricks. Isto é:\n- Criação do Workspace + Unity Catalog\n- Setup do External Location (S3 em Raw)\n- Adição do Volume dos dados em Raw\n\n### Consumo dos dados para Bronze\n\nSeguimos no projeto para realizar as primeiras ingestões de dados.\n\nCriamos nosso primeiro notebook e fizemos a leitura dos dados `full-load` em Raw e salvamos em Bronze.\n\nAlgo similar à este script:\n\n```python\ndf_full = (spark.read\n                .format(\"parquet\")\n                .load(f\"/Volumes/raw/upsell/full_load/{tablename}/\"))\n\n(df_full.coalesce(1)\n        .write\n        .format(\"delta\")\n        .mode(\"overwrite\")\n        .saveAsTable(f\"{catalog}.{schema}.{tablename}\"))\n```\n\nAinda neste mesmo dia, realizamos a ingestão de todos os dados em CDC com Upsert em Delta.\n\nOu seja, identificamos a última versão válida do dado com base na `primary key` e no campo `modified date` que vem do CDC.\n\n```python\n(spark.read\n      .format(\"parquet\")\n      .load(f\"/Volumes/raw/upsell/cdc/{tablename}/\")\n      .createOrReplaceTempView(f\"view_{tablename}\"))\n\nquery = f'''\n    SELECT *\n    FROM \"view_{tablename}\"\n    QUALIFY ROW_NUMBER() OVER (PARTITION BY {primary_key} ORDER BY modified_date DESC) = 1\n'''\n\ndf_cdc_unique = spark.sql(query)\n\nbronze = delta.DeltaTable.forName(spark, f\"{catalog}.{schema}.{tablename}\")\n\n(bronze.alias(\"b\")\n       .merge(df_cdc_unique.alias(\"d\"), f\"b.{primary_key} = d.{primary_key}\") \n       .whenMatchedDelete(condition = \"d.OP = 'D'\")\n       .whenMatchedUpdateAll(condition = \"d.OP = 'U'\")\n       .whenNotMatchedInsertAll(condition = \"d.OP = 'I' OR d.OP = 'U'\")\n       .execute())\n```\n\nApesar deste código ser funcional, não é muito bacana. Pois a cada nova carga em CDC, todos os arquivos são lidos e processados. No dia seguinte mostramos uma solução interessante  para esta questão, utilizando Spark Streaming (`CloudFiles`).\n\n### Consumo por Streaming (CloudFiles)\n\nEmbora a solução anterior em `batch` para `CDC` tenha funcionado, essa não é uma solução muito performática. Uma vez que a cada nova carga, todos os dados na pasta `CDC` serão lidos e processados. existem algumas alternativas para solucionar essa perda de performance, como:\n\n- Particionamento dos dados em pastas de data (yyyy-mm-dd)\n- Movimentação dos arquivos lidos para outro diretório/bucket\n- Leitura via Streaming\n\nAdotaremos a última opção, realizando a leitura dos dados utilizando Streaming com Apache Spark:\n\n```python\ndf_stream = (spark.readStream\n                  .format(\"cloudFiles\")\n                  .option(\"cloudFiles.format\", \"parquet\")\n                  .schema(schema)\n                  .load(f\"/Volumes/raw/upsell/cdc/{tablename}/\"))\n```\n\nA opção de formato `cloudFiles` é algo específico do Databricks. Você pode utilizar apenas `parquet` caso esteja trabalhando com o Apache Spark Vanilla. Vale ressaltar que para o streaming funcionar, é necessário passar o `schema` dos arquivos a serem lidos.\n\nO próximo passo é realizar a escrita dos dados a partir do Dataframe criado com Streaming:\n\n```python\nstream = (df_stream.writeStream\n                   .option(\"checkpointLocation\", f\"/Volumes/raw/upsell/cdc/{tablename}_checkpoint/\")\n                   .foreachBatch(lambda df, batchID: upsert(df, bronze))\n                   .trigger(availableNow=True))\n```\n\nPontos de destaque:\n- `checkpointLocation`:trata-se de uma diretório especial onde o Spark conseguirá identificar a partir de qual arquivo ele deve ler na próxima iteração.\n- `.foreachBatch`: definição de como cada batch do streaming será processado, i.e. como lidaremos com os dados.\n- `.trigger(availableNow=True))`: garante que após o processamento de todos os arvuiso disponíveis, a stream é encerrada.\n\nAgora, precisamos definir a função `upsert`. Ela seguirá o mesmo racional apresentado na etapa de CDC anteriormente, isto é, consolidação dos dados novos para realizar o merge na tabela já existente em bronze.\n\n```python\ndef upsert(df, deltatable):\n    df.createOrReplaceGlobalTempView(f\"view_{tablename}\")\n\n    query = f'''\n        SELECT *\n        FROM global_temp.view_{tablename}\n        QUALIFY ROW_NUMBER() OVER (PARTITION BY {id_field} ORDER BY {timestamp_field} DESC) = 1\n    '''\n\n    df_cdc = spark.sql(query)\n\n    (deltatable.alias(\"b\")\n               .merge(df_cdc.alias(\"d\"), f\"b.{id_field} = d.{id_field}\") \n               .whenMatchedDelete(condition = \"d.OP = 'D'\")\n               .whenMatchedUpdateAll(condition = \"d.OP = 'U'\")\n               .whenNotMatchedInsertAll(condition = \"d.OP = 'I' OR d.OP = 'U'\")\n               .execute())\n```\n\nOu seja, desta forma, para cada batch lido na stream, realizamos o upsert dos dos em bronze.\n\n### Classes de ingestão\n\nBuscando melhorar ainda mais o nosso código, decidimos construir algumas classes para ingestão desses dados. Isso nos ajudará aplicar esses mesmos métodos e estratégias de ingestão em outros contextos ou necessidades.\n\n#### Classe para carga Full-load\n\n```python\nclass Ingestor:\n\n    def __init__(self, spark, catalog, schemaname, tablename, data_format):\n        self.spark = spark\n        self.catalog = catalog\n        self.schemaname = schemaname\n        self.tablename = tablename\n        self.format = data_format\n        self.set_schema()\n\n    def set_schema(self):\n        self.data_schema = utils.import_schema(self.tablename)\n    \n    def load(self, path):\n        df = (self.spark\n                  .read\n                  .format(self.format)\n                  .schema(self.data_schema)\n                  .load(path))\n        return df\n        \n    def save(self, df):\n        (df.write\n           .format(\"delta\")\n           .mode(\"overwrite\")\n           .saveAsTable(f\"{self.catalog}.{self.schemaname}.{self.tablename}\"))\n        return True\n        \n    def execute(self, path):\n        df = self.load(path)\n        return self.save(df)\n```\n\n#### Classe para carga CDC\n\n```python\nclass IngestorCDC(Ingestor):\n\n    def __init__(self, spark, catalog, schemaname, tablename, data_format, id_field, timestamp_field):\n        super().__init__(spark, catalog, schemaname, tablename, data_format)\n        self.id_field = id_field\n        self.timestamp_field = timestamp_field\n        self.set_deltatable()\n\n    def set_deltatable(self):\n        tablename = f\"{self.catalog}.{self.schemaname}.{self.tablename}\"\n        self.deltatable = delta.DeltaTable.forName(self.spark, tablename)\n\n    def upsert(self, df):\n        df.createOrReplaceGlobalTempView(f\"view_{self.tablename}\")\n        query = f'''\n            SELECT *\n            FROM global_temp.view_{self.tablename}\n            QUALIFY ROW_NUMBER() OVER (PARTITION BY {self.id_field} ORDER BY {self.timestamp_field} DESC) = 1\n        '''\n\n        df_cdc = self.spark.sql(query)\n\n        (self.deltatable\n             .alias(\"b\")\n             .merge(df_cdc.alias(\"d\"), f\"b.{self.id_field} = d.{self.id_field}\") \n             .whenMatchedDelete(condition = \"d.OP = 'D'\")\n             .whenMatchedUpdateAll(condition = \"d.OP = 'U'\")\n             .whenNotMatchedInsertAll(condition = \"d.OP = 'I' OR d.OP = 'U'\")\n             .execute())\n\n    def load(self, path):\n        df = (self.spark\n                  .readStream\n                  .format(\"cloudFiles\")\n                  .option(\"cloudFiles.format\", self.format)\n                  .schema(self.data_schema)\n                  .load(path))\n        return df\n    \n    def save(self, df):\n        stream = (df.writeStream\n                   .option(\"checkpointLocation\", f\"/Volumes/raw/{self.schemaname}/cdc/{self.tablename}_checkpoint/\")\n                   .foreachBatch(lambda df, batchID: self.upsert(df))\n                   .trigger(availableNow=True))\n        return stream.start()\n```\n\n#### Execução\n\nCom isso em mente, podemos apenas invorcar as classes e realizar sua execução:\n\n```python\nif not utils.table_exists(spark, catalog, schemaname, tablename):\n\n    print(\"Tabela não existente, criando...\")\n\n    dbutils.fs.rm(checkpoint_location, True)\n\n    ingest_full_load = ingestors.Ingestor(spark=spark,\n                                          catalog=catalog,\n                                          schemaname=schemaname,\n                                          tablename=tablename,\n                                          data_format=\"parquet\")\n    \n    ingest_full_load.execute(full_load_path)\n    print(\"Tabela criada com sucesso!\")\n    \nelse:\n    print(\"Tabela já existente, ignorando full-load\")\n\nprint(\"Executando carga cdc...\")\ningest_cdc = ingestors.IngestorCDC(spark=spark,\n                                   catalog=catalog,\n                                   schemaname=schemaname,\n                                   tablename=tablename,\n                                   data_format=\"parquet\",\n                                   id_field=id_field,\n                                   timestamp_field=timestamp_field)\n\nstream = ingest_cdc.execute(cdc_path)\nprint(\"ok\")\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fteomewhy%2Flago-mago","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fteomewhy%2Flago-mago","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fteomewhy%2Flago-mago/lists"}