ETL процСсс получСния Π΄Π°Π½Π½Ρ‹Ρ… ΠΈΠ· элСктронной ΠΏΠΎΡ‡Ρ‚Ρ‹ Π² Apache Airflow

ETL процСсс получСния Π΄Π°Π½Π½Ρ‹Ρ… ΠΈΠ· элСктронной ΠΏΠΎΡ‡Ρ‚Ρ‹ Π² Apache Airflow

Как Π±Ρ‹ сильно Π½Π΅ Ρ€Π°Π·Π²ΠΈΠ²Π°Π»ΠΈΡΡŒ Ρ‚Π΅Ρ…Π½ΠΎΠ»ΠΎΠ³ΠΈΠΈ, Π·Π° Ρ€Π°Π·Π²ΠΈΡ‚ΠΈΠ΅ΠΌ всСгда тянСтся Π²Π΅Ρ€Π΅Π½ΠΈΡ†Π° ΡƒΡΡ‚Π°Ρ€Π΅Π²ΡˆΠΈΡ… ΠΏΠΎΠ΄Ρ…ΠΎΠ΄ΠΎΠ². Π­Ρ‚ΠΎ ΠΌΠΎΠΆΠ΅Ρ‚ Π±Ρ‹Ρ‚ΡŒ обусловлСно ΠΏΠ»Π°Π²Π½Ρ‹ΠΌ ΠΏΠ΅Ρ€Π΅Ρ…ΠΎΠ΄ΠΎΠΌ, чСловСчСским Ρ„Π°ΠΊΡ‚ΠΎΡ€ΠΎΠΌ, тСхнологичСскими нСобходимостями ΠΈΠ»ΠΈ Ρ‡Π΅ΠΌ-Ρ‚ΠΎ Π΄Ρ€ΡƒΠ³ΠΈΠΌ. Π’ области ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ Π΄Π°Π½Π½Ρ‹Ρ… Π½Π°ΠΈΠ±ΠΎΠ»Π΅Π΅ ΠΏΠΎΠΊΠ°Π·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹ΠΌΠΈ Π² этой части ΡΠ²Π»ΡΡŽΡ‚ΡΡ источники Π΄Π°Π½Π½Ρ‹Ρ…. Как Π±Ρ‹ ΠΌΡ‹ Π½Π΅ ΠΌΠ΅Ρ‡Ρ‚Π°Π»ΠΈ ΠΎΡ‚ этого ΠΈΠ·Π±Π°Π²ΠΈΡ‚ΡŒΡΡ, Π½ΠΎ ΠΏΠΎΠΊΠ° Ρ‡Π°ΡΡ‚ΡŒ Π΄Π°Π½Π½Ρ‹Ρ… пСрСсылаСтся Π² мСссСндТСрах ΠΈ элСктронных ΠΏΠΈΡΡŒΠΌΠ°Ρ…, Π½Π΅ говоря ΠΈ ΠΏΡ€ΠΎ Π±ΠΎΠ»Π΅Π΅ Π°Ρ€Ρ…Π°ΠΈΡ‡Π½Ρ‹Π΅ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚Ρ‹. ΠŸΡ€ΠΈΠ³Π»Π°ΡˆΠ°ΡŽ ΠΏΠΎΠ΄ ΠΊΠ°Ρ‚ Ρ€Π°Π·ΠΎΠ±Ρ€Π°Ρ‚ΡŒ ΠΎΠ΄ΠΈΠ½ ΠΈΠ· Π²Π°Ρ€ΠΈΠ°Π½Ρ‚ΠΎΠ² для Apache Airflow, ΠΈΠ»Π»ΡŽΡΡ‚Ρ€ΠΈΡ€ΡƒΡŽΡ‰ΠΈΠΉ, ΠΊΠ°ΠΊ ΠΌΠΎΠΆΠ½ΠΎ Π·Π°Π±ΠΈΡ€Π°Ρ‚ΡŒ Π΄Π°Π½Π½Ρ‹Π΅ ΠΈΠ· элСктронных писСм.

ΠŸΡ€Π΅Π΄Ρ‹ΡΡ‚ΠΎΡ€ΠΈΡ

МногиС Π΄Π°Π½Π½Ρ‹Π΅ Π΄ΠΎ сих ΠΏΠΎΡ€ ΠΏΠ΅Ρ€Π΅Π΄Π°ΡŽΡ‚ΡΡ Ρ‡Π΅Ρ€Π΅Π· ΡΠ»Π΅ΠΊΡ‚Ρ€ΠΎΠ½Π½ΡƒΡŽ ΠΏΠΎΡ‡Ρ‚Ρƒ, начиная с мСТличностных ΠΊΠΎΠΌΠΌΡƒΠ½ΠΈΠΊΠ°Ρ†ΠΈΠΉ ΠΈ заканчивая стандартами взаимодСйствия ΠΌΠ΅ΠΆΠ΄Ρƒ компаниями. Π₯ΠΎΡ€ΠΎΡˆΠΎ, Ссли удаСтся для получСния Π΄Π°Π½Π½Ρ‹Ρ… Π½Π°ΠΏΠΈΡΠ°Ρ‚ΡŒ интСрфСйс ΠΈΠ»ΠΈ ΠΏΠΎΡΠ°Π΄ΠΈΡ‚ΡŒ людСй Π² офисС, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ эту ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡŽ Π±ΡƒΠ΄ΡƒΡ‚ Π²Π½ΠΎΡΠΈΡ‚ΡŒ Π² Π±ΠΎΠ»Π΅Π΅ ΡƒΠ΄ΠΎΠ±Π½Ρ‹Π΅ источники, Π½ΠΎ Π·Π°Ρ‡Π°ΡΡ‚ΡƒΡŽ Ρ‚Π°ΠΊΠΎΠΉ возмоТности ΠΌΠΎΠΆΠ΅Ρ‚ просто Π½Π΅ Π±Ρ‹Ρ‚ΡŒ. ΠšΠΎΠ½ΠΊΡ€Π΅Ρ‚Π½Π°Ρ Π·Π°Π΄Π°Ρ‡Π°, с ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠΉ столкнулся я, β€” это ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ нСбСзызвСстной CRM систСмы ΠΊ Ρ…Ρ€Π°Π½ΠΈΠ»ΠΈΡ‰Ρƒ Π΄Π°Π½Π½Ρ‹Ρ…, Π° Π΄Π°Π»Π΅Π΅ β€” ΠΊ систСмС OLAP. Π’Π°ΠΊ историчСски слоТилось, Ρ‡Ρ‚ΠΎ для нашСй ΠΊΠΎΠΌΠΏΠ°Π½ΠΈΠΈ использованиС этой систСмы Π±Ρ‹Π»ΠΎ ΡƒΠ΄ΠΎΠ±Π½ΠΎ Π² ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½ΠΎ взятой области бизнСса. ΠŸΠΎΡΡ‚ΠΎΠΌΡƒ всСм ΠΎΡ‡Π΅Π½ΡŒ Ρ…ΠΎΡ‚Π΅Π»ΠΎΡΡŒ ΠΈΠΌΠ΅Ρ‚ΡŒ Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡ‚ΡŒ ΠΎΠΏΠ΅Ρ€ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ Π΄Π°Π½Π½Ρ‹ΠΌΠΈ ΠΈ ΠΈΠ· этой стороннСй систСмы Π² Ρ‚ΠΎΠΌ числС. Π’ ΠΏΠ΅Ρ€Π²ΡƒΡŽ ΠΎΡ‡Π΅Ρ€Π΅Π΄ΡŒ, ΠΊΠΎΠ½Π΅Ρ‡Π½ΠΎ, Π±Ρ‹Π»Π° ΠΈΠ·ΡƒΡ‡Π΅Π½Π° Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡ‚ΡŒ получСния Π΄Π°Π½Π½Ρ‹Ρ… ΠΈΠ· ΠΎΡ‚ΠΊΡ€Ρ‹Ρ‚ΠΎΠ³ΠΎ API. К соТалСнию, API Π½Π΅ ΠΏΠΎΠΊΡ€Ρ‹Π²Π°Π»ΠΎ ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ всСх Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΡ‹Ρ… Π΄Π°Π½Π½Ρ‹Ρ…, Π΄Π° ΠΈ, Π²Ρ‹Ρ€Π°ΠΆΠ°ΡΡΡŒ простым языком, Π±Ρ‹Π»ΠΎ Π²ΠΎ ΠΌΠ½ΠΎΠ³ΠΎΠΌ ΠΊΡ€ΠΈΠ²ΠΎΠ²Π°Ρ‚ΠΎ, Π° тСхничСская ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠ° Π½Π΅ Π·Π°Ρ…ΠΎΡ‚Π΅Π»Π° ΠΈΠ»ΠΈ Π½Π΅ смогла ΠΏΠΎΠΉΡ‚ΠΈ навстрСчу для прСдоставлСния Π±ΠΎΠ»Π΅Π΅ ΠΈΡΡ‡Π΅Ρ€ΠΏΡ‹Π²Π°ΡŽΡ‰Π΅Π³ΠΎ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π»Π°. Π—Π°Ρ‚ΠΎ данная систСма прСдоставляла Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡ‚ΡŒ пСриодичСского получСния Π½Π΅Π΄ΠΎΡΡ‚Π°ΡŽΡ‰ΠΈΡ… Π΄Π°Π½Π½Ρ‹Ρ… Π½Π° ΠΏΠΎΡ‡Ρ‚Ρƒ Π² Π²ΠΈΠ΄Π΅ ссылки для Π²Ρ‹Π³Ρ€ΡƒΠ·ΠΊΠΈ Π°Ρ€Ρ…ΠΈΠ²Π°.

НуТно ΠΎΡ‚ΠΌΠ΅Ρ‚ΠΈΡ‚ΡŒ, Ρ‡Ρ‚ΠΎ это Π±Ρ‹Π» Π½Π΅ СдинствСнный кСйс, ΠΏΠΎ ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠΌΡƒ бизнСс Ρ…ΠΎΡ‚Π΅Π» ΡΠΎΠ±ΠΈΡ€Π°Ρ‚ΡŒ Π΄Π°Π½Π½Ρ‹Π΅ ΠΈΠ· ΠΏΠΎΡ‡Ρ‚ΠΎΠ²Ρ‹Ρ… писСм ΠΈΠ»ΠΈ мСссСндТСров. Однако, Π² Π΄Π°Π½Π½ΠΎΠΌ случаС ΠΌΡ‹ Π½Π΅ ΠΌΠΎΠ³Π»ΠΈ ΠΏΠΎΠ²Π»ΠΈΡΡ‚ΡŒ Π½Π° ΡΡ‚ΠΎΡ€ΠΎΠ½Π½ΡŽΡŽ компанию, которая прСдоставляСт Ρ‡Π°ΡΡ‚ΡŒ Π΄Π°Π½Π½Ρ‹Ρ… Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Ρ‚Π°ΠΊΠΈΠΌ способом.

Apache Airflow

Для построСния ETL процСссов ΠΌΡ‹ Ρ‡Π°Ρ‰Π΅ всСго ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ Apache Airflow. Для Ρ‚ΠΎΠ³ΠΎ Ρ‡Ρ‚ΠΎΠ±Ρ‹ Ρ‡ΠΈΡ‚Π°Ρ‚Π΅Π»ΡŒ, Π½Π΅Π·Π½Π°ΠΊΠΎΠΌΡ‹ΠΉ с этой Ρ‚Π΅Ρ…Π½ΠΎΠ»ΠΎΠ³ΠΈΠ΅ΠΉ, Π»ΡƒΡ‡ΡˆΠ΅ понял, ΠΊΠ°ΠΊ это выглядит Π² контСкстС ΠΈ Π² Ρ†Π΅Π»ΠΎΠΌ, ΠΎΠΏΠΈΡˆΡƒ ΠΏΠ°Ρ€Ρƒ Π²Π²ΠΎΠ΄Π½Ρ‹Ρ….

Apache Airflow β€” свободная ΠΏΠ»Π°Ρ‚Ρ„ΠΎΡ€ΠΌΠ°, которая ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ для построСния, выполнСния ΠΈ ΠΌΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³Π° ETL (Extract-Transform-Loading) процСссов Π½Π° языкС Python. ΠžΡΠ½ΠΎΠ²Π½Ρ‹ΠΌ понятиСм Π² Airflow являСтся ΠΎΡ€ΠΈΠ΅Π½Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹ΠΉ Π°Ρ†ΠΈΠΊΠ»ΠΈΡ‡Π½Ρ‹ΠΉ Π³Ρ€Π°Ρ„, Π³Π΄Π΅ Π²Π΅Ρ€ΡˆΠΈΠ½Ρ‹ Π³Ρ€Π°Ρ„Π° β€” ΠΊΠΎΠ½ΠΊΡ€Π΅Ρ‚Π½Ρ‹Π΅ процСссы, Π° Ρ€Π΅Π±Ρ€Π° Π³Ρ€Π°Ρ„Π° β€” ΠΏΠΎΡ‚ΠΎΠΊ управлСния ΠΈΠ»ΠΈ ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΠΈ. ΠŸΡ€ΠΎΡ†Π΅ΡΡ ΠΌΠΎΠΆΠ΅Ρ‚ просто Π²Ρ‹Π·Ρ‹Π²Π°Ρ‚ΡŒ Π»ΡŽΠ±ΡƒΡŽ Python Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ, Π° ΠΌΠΎΠΆΠ΅Ρ‚ ΠΈΠΌΠ΅Ρ‚ΡŒ Π±ΠΎΠ»Π΅Π΅ ΡΠ»ΠΎΠΆΠ½ΡƒΡŽ Π»ΠΎΠ³ΠΈΠΊΡƒ ΠΈΠ· ΠΏΠΎΡΠ»Π΅Π΄ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΠ½ΠΎΠ³ΠΎ Π²Ρ‹Π·ΠΎΠ²Π° Π½Π΅ΡΠΊΠΎΠ»ΡŒΠΊΠΈΡ… Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΉ Π² контСкстС класса. Для Π½Π°ΠΈΠ±ΠΎΠ»Π΅Π΅ частых ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΉ ΡƒΠΆΠ΅ Π΅ΡΡ‚ΡŒ мноТСство Π³ΠΎΡ‚ΠΎΠ²Ρ‹Ρ… Π½Π°Ρ€Π°Π±ΠΎΡ‚ΠΎΠΊ, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ΠΌΠΎΠΆΠ½ΠΎ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ Π² качСствС процСссов. К Ρ‚Π°ΠΊΠΈΠΌ Π½Π°Ρ€Π°Π±ΠΎΡ‚ΠΊΠ°ΠΌ относятся:

  • ΠΎΠΏΠ΅Ρ€Π°Ρ‚ΠΎΡ€Ρ‹ β€” для ΠΏΠ΅Ρ€Π΅Π³ΠΎΠ½Π° Π΄Π°Π½Π½Ρ‹Ρ… ΠΈΠ· ΠΎΠ΄Π½ΠΎΠ³ΠΎ мСста Π² Π΄Ρ€ΡƒΠ³ΠΎΠ΅, Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€ ΠΈΠ· Ρ‚Π°Π±Π»ΠΈΡ†Ρ‹ Π‘Π” Π² Ρ…Ρ€Π°Π½ΠΈΠ»ΠΈΡ‰Π΅ Π΄Π°Π½Π½Ρ‹Ρ…;
  • сСнсоры β€” для оТидания наступлСния ΠΎΠΏΡ€Π΅Π΄Π΅Π»Π΅Π½Π½ΠΎΠ³ΠΎ события ΠΈ направлСния ΠΏΠΎΡ‚ΠΎΠΊΠ° управлСния Π² ΠΏΠΎΡΠ»Π΅Π΄ΡƒΡŽΡ‰ΠΈΠ΅ Π²Π΅Ρ€ΡˆΠΈΠ½Ρ‹ Π³Ρ€Π°Ρ„Π°;
  • Ρ…ΡƒΠΊΠΈ β€” для Π±ΠΎΠ»Π΅Π΅ Π½ΠΈΠ·ΠΊΠΎΡƒΡ€ΠΎΠ²Π½Π΅Π²Ρ‹Ρ… ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΉ, Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€, для получСния Π΄Π°Π½Π½Ρ‹Ρ… ΠΈΠ· Ρ‚Π°Π±Π»ΠΈΡ†Ρ‹ Π‘Π” (ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΡŽΡ‚ΡΡ Π² ΠΎΠΏΠ΅Ρ€Π°Ρ‚ΠΎΡ€Π°Ρ…);
  • ΠΈ Ρ‚.Π΄.

ΠžΠΏΠΈΡΡ‹Π²Π°Ρ‚ΡŒ Apache Airflow ΠΏΠΎΠ΄Ρ€ΠΎΠ±Π½ΠΎ Π² этой ΡΡ‚Π°Ρ‚ΡŒΠ΅ Π±ΡƒΠ΄Π΅Ρ‚ нСцСлСсообразно. ΠšΡ€Π°Ρ‚ΠΊΠΈΠ΅ ввСдСния ΠΌΠΎΠΆΠ½ΠΎ ΠΏΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ здСсь ΠΈΠ»ΠΈ здСсь.

Π₯ΡƒΠΊ для получСния Π΄Π°Π½Π½Ρ‹Ρ…

Π’ ΠΏΠ΅Ρ€Π²ΡƒΡŽ ΠΎΡ‡Π΅Ρ€Π΅Π΄ΡŒ, для Ρ€Π΅ΡˆΠ΅Π½ΠΈΡ Π·Π°Π΄Π°Ρ‡ΠΈ Π½ΡƒΠΆΠ½ΠΎ Π½Π°ΠΏΠΈΡΠ°Ρ‚ΡŒ Ρ…ΡƒΠΊ, с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ³ΠΎ ΠΌΡ‹ ΠΌΠΎΠ³Π»ΠΈ Π±Ρ‹:

  • ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π°Ρ‚ΡŒΡΡ ΠΊ элСктронной ΠΏΠΎΡ‡Ρ‚Π΅;
  • Π½Π°Ρ…ΠΎΠ΄ΠΈΡ‚ΡŒ Π½ΡƒΠΆΠ½ΠΎΠ΅ письмо;
  • ΠΏΠΎΠ»ΡƒΡ‡Π°Ρ‚ΡŒ Π΄Π°Π½Π½Ρ‹Π΅ ΠΈΠ· письма.

from airflow.hooks.base_hook import BaseHook
import imaplib
import logging

class IMAPHook(BaseHook):
    def __init__(self, imap_conn_id):
        """
           IMAP hook для получСния Π΄Π°Π½Π½Ρ‹Ρ… с элСктронной ΠΏΠΎΡ‡Ρ‚Ρ‹

           :param imap_conn_id:       Π˜Π΄Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ‚ΠΎΡ€ ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ ΠΊ ΠΏΠΎΡ‡Ρ‚Π΅
           :type imap_conn_id:        string
        """
        self.connection = self.get_connection(imap_conn_id)
        self.mail = None

    def authenticate(self):
        """ 
            ΠŸΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π°Π΅ΠΌΡΡ ΠΊ ΠΏΠΎΡ‡Ρ‚Π΅
        """
        mail = imaplib.IMAP4_SSL(self.connection.host)
        response, detail = mail.login(user=self.connection.login, password=self.connection.password)
        if response != "OK":
            raise AirflowException("Sign in failed")
        else:
            self.mail = mail

    def get_last_mail(self, check_seen=True, box="INBOX", condition="(UNSEEN)"):
        """
            ΠœΠ΅Ρ‚ΠΎΠ΄ для получСния ΠΈΠ΄Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ‚ΠΎΡ€Π° послСднСго письма, 
            ΡƒΠ΄ΠΎΠ²Π»Π΅Ρ‚Π²ΠΎΡ€Π°ΡΡŽΡ‰Π΅Π³ΠΎ условиям поиска

            :param check_seen:      ΠžΡ‚ΠΌΠ΅Ρ‡Π°Ρ‚ΡŒ послСднСС письмо ΠΊΠ°ΠΊ ΠΏΡ€ΠΎΡ‡ΠΈΡ‚Π°Π½Π½ΠΎΠ΅
            :type check_seen:       bool
            :param box:             НаимСнования ящика
            :type box:              string
            :param condition:       Условия поиска писСм
            :type condition:        string
        """
        self.authenticate()
        self.mail.select(mailbox=box)
        response, data = self.mail.search(None, condition)
        mail_ids = data[0].split()
        logging.info("Π’ ящикС Π½Π°ΠΉΠ΄Π΅Π½Ρ‹ ΡΠ»Π΅Π΄ΡƒΡŽΡ‰ΠΈΠ΅ письма: " + str(mail_ids))

        if not mail_ids:
            logging.info("НС Π½Π°ΠΉΠ΄Π΅Π½ΠΎ Π½ΠΎΠ²Ρ‹Ρ… писСм")
            return None

        mail_id = mail_ids[0]

        # Ссли Ρ‚Π°ΠΊΠΈΡ… писСм нСсколько
        if len(mail_ids) > 1:
            # ΠΎΡ‚ΠΌΠ΅Ρ‡Π°Π΅ΠΌ ΠΎΡΡ‚Π°Π»ΡŒΠ½Ρ‹Π΅ ΠΏΡ€ΠΎΡ‡ΠΈΡ‚Π°Π½Π½Ρ‹ΠΌΠΈ
            for id in mail_ids:
                self.mail.store(id, "+FLAGS", "\Seen")

            # Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅ΠΌ послСднСС
            mail_id = mail_ids[-1]

        # Π½ΡƒΠΆΠ½ΠΎ Π»ΠΈ ΠΎΡ‚ΠΌΠ΅Ρ‚ΠΈΡ‚ΡŒ послСднСС ΠΏΡ€ΠΎΡ‡ΠΈΡ‚Π°Π½Π½Ρ‹ΠΌ
        if not check_seen:
            self.mail.store(mail_id, "-FLAGS", "\Seen")

        return mail_id

Π›ΠΎΠ³ΠΈΠΊΠ° такая: ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π°Π΅ΠΌΡΡ, Π½Π°Ρ…ΠΎΠ΄ΠΈΠΌ послСднСС самоС Π°ΠΊΡ‚ΡƒΠ°Π»ΡŒΠ½ΠΎΠ΅ письмо, Ссли Π΅ΡΡ‚ΡŒ Π΄Ρ€ΡƒΠ³ΠΈΠ΅ β€” ΠΈΠ³Π½ΠΎΡ€ΠΈΡ€ΡƒΠ΅ΠΌ ΠΈΡ…. Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ ΠΈΠΌΠ΅Π½Π½ΠΎ такая функция, ΠΏΠΎΡ‚ΠΎΠΌΡƒ Ρ‡Ρ‚ΠΎ Π±ΠΎΠ»Π΅Π΅ ΠΏΠΎΠ·Π΄Π½ΠΈΠ΅ письма содСрТат всС Π΄Π°Π½Π½Ρ‹Π΅ Ρ€Π°Π½Π½ΠΈΡ…. Если это Π½Π΅ Ρ‚Π°ΠΊ, Ρ‚ΠΎ ΠΌΠΎΠΆΠ½ΠΎ Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Ρ‚ΡŒ массив всСх писСм ΠΈΠ»ΠΈ ΠΎΠ±Ρ€Π°Π±Π°Ρ‚Ρ‹Π²Π°Ρ‚ΡŒ ΠΏΠ΅Ρ€Π²ΠΎΠ΅, Π° ΠΎΡΡ‚Π°Π»ΡŒΠ½Ρ‹Π΅ β€” ΠΏΡ€ΠΈ ΡΠ»Π΅Π΄ΡƒΡŽΡ‰Π΅ΠΌ ΠΏΡ€ΠΎΡ…ΠΎΠ΄Π΅. Π’ ΠΎΠ±Ρ‰Π΅ΠΌ, всС ΠΊΠ°ΠΊ всСгда зависит ΠΎΡ‚ Π·Π°Π΄Π°Ρ‡ΠΈ.

ДобавляСм ΠΊ Ρ…ΡƒΠΊΡƒ Π΄Π²Π΅ Π²ΡΠΏΠΎΠΌΠΎΠ³Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ: для скачивания Ρ„Π°ΠΉΠ»Π° ΠΈ для скачивания Ρ„Π°ΠΉΠ»Π° ΠΏΠΎ ссылкС ΠΈΠ· письма. К слову, ΠΈΡ… ΠΌΠΎΠΆΠ½ΠΎ вынСсти Π² ΠΎΠΏΠ΅Ρ€Π°Ρ‚ΠΎΡ€, это зависит ΠΎΡ‚ частоты использования Π΄Π°Π½Π½ΠΎΠ³ΠΎ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π»Π°. Π§Ρ‚ΠΎ Π΅Ρ‰Π΅ Π΄ΠΎΠΏΠΈΡΡ‹Π²Π°Ρ‚ΡŒ Π² Ρ…ΡƒΠΊ, ΠΎΠΏΡΡ‚ΡŒ ΠΆΠ΅, зависит ΠΎΡ‚ Π·Π°Π΄Π°Ρ‡ΠΈ: Ссли Π² письмС приходят сразу Ρ„Π°ΠΉΠ»Ρ‹, Ρ‚ΠΎ ΠΌΠΎΠΆΠ½ΠΎ ΡΠΊΠ°Ρ‡ΠΈΠ²Π°Ρ‚ΡŒ прилоТСния ΠΊ ΠΏΠΈΡΡŒΠΌΡƒ, Ссли Π΄Π°Π½Π½Ρ‹Π΅ приходят Π² письмС, Ρ‚ΠΎ Π½ΡƒΠΆΠ½ΠΎ ΠΏΠ°Ρ€ΡΠΈΡ‚ΡŒ письмо ΠΈ Ρ‚.Π΄. Π’ ΠΌΠΎΠ΅ΠΌ случаС, письмо ΠΏΡ€ΠΈΡ…ΠΎΠ΄ΠΈΡ‚ с ΠΎΠ΄Π½ΠΎΠΉ ссылкой Π½Π° Π°Ρ€Ρ…ΠΈΠ², ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΌΠ½Π΅ Π½ΡƒΠΆΠ½ΠΎ ΠΏΠΎΠ»ΠΎΠΆΠΈΡ‚ΡŒ Π² ΠΎΠΏΡ€Π΅Π΄Π΅Π»Π΅Π½Π½ΠΎΠ΅ мСсто ΠΈ Π·Π°ΠΏΡƒΡΡ‚ΠΈΡ‚ΡŒ дальнСйший процСсс ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ.

    def download_from_url(self, url, path, chunk_size=128):
        """
            ΠœΠ΅Ρ‚ΠΎΠ΄ для скачивания Ρ„Π°ΠΉΠ»Π°

            :param url:              АдрСс Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ
            :type url:               string
            :param path:             ΠšΡƒΠ΄Π° ΠΏΠΎΠ»ΠΎΠΆΠΈΡ‚ΡŒ Ρ„Π°ΠΉΠ»
            :type path:              string
            :param chunk_size:       По сколько Π±Π°ΠΉΡ‚ΠΎΠ² ΠΏΠΈΡΠ°Ρ‚ΡŒ
            :type chunk_size:        int
        """
        r = requests.get(url, stream=True)
        with open(path, "wb") as fd:
            for chunk in r.iter_content(chunk_size=chunk_size):
                fd.write(chunk)

    def download_mail_href_attachment(self, mail_id, path):
        """
            ΠœΠ΅Ρ‚ΠΎΠ΄ для скачивания Ρ„Π°ΠΉΠ»Π° ΠΏΠΎ ссылкС ΠΈΠ· письма

            :param mail_id:         Π˜Π΄Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ‚ΠΎΡ€ письма
            :type mail_id:          string
            :param path:            ΠšΡƒΠ΄Π° ΠΏΠΎΠ»ΠΎΠΆΠΈΡ‚ΡŒ Ρ„Π°ΠΉΠ»
            :type path:             string
        """
        response, data = self.mail.fetch(mail_id, "(RFC822)")
        raw_email = data[0][1]
        raw_soup = raw_email.decode().replace("r", "").replace("n", "")
        parse_soup = BeautifulSoup(raw_soup, "html.parser")
        link_text = ""

        for a in parse_soup.find_all("a", href=True, text=True):
            link_text = a["href"]

        self.download_from_url(link_text, path)

Код простой, поэтому вряд Π»ΠΈ нуТдаСтся Π² Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Ρ… пояснСниях. РасскаТу лишь ΠΏΡ€ΠΎ ΠΌΠ°Π³ΠΈΡ‡Π΅ΡΠΊΡƒΡŽ строчку imap_conn_id. Apache Airflow Ρ…Ρ€Π°Π½ΠΈΡ‚ ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹ ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠΉ (Π»ΠΎΠ³ΠΈΠ½, ΠΏΠ°Ρ€ΠΎΠ»ΡŒ, адрСс ΠΈ Π΄Ρ€ΡƒΠ³ΠΈΠ΅ ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹), ΠΊ ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΌ ΠΌΠΎΠΆΠ½ΠΎ ΠΎΠ±Ρ€Π°Ρ‰Π°Ρ‚ΡŒΡΡ ΠΏΠΎ строковому ΠΈΠ΄Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ‚ΠΎΡ€Ρƒ. Π’ΠΈΠ·ΡƒΠ°Π»ΡŒΠ½ΠΎ ΡƒΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡΠΌΠΈ выглядит Π²ΠΎΡ‚ Ρ‚Π°ΠΊ

ETL процСсс получСния Π΄Π°Π½Π½Ρ‹Ρ… ΠΈΠ· элСктронной ΠΏΠΎΡ‡Ρ‚Ρ‹ Π² Apache Airflow

БСнсор для оТидания Π΄Π°Π½Π½Ρ‹Ρ…

ΠŸΠΎΡΠΊΠΎΠ»ΡŒΠΊΡƒ ΠΌΡ‹ ΡƒΠΆΠ΅ ΡƒΠΌΠ΅Π΅ΠΌ ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π°Ρ‚ΡŒΡΡ ΠΈ ΠΏΠΎΠ»ΡƒΡ‡Π°Ρ‚ΡŒ Π΄Π°Π½Π½Ρ‹Π΅ ΠΈΠ· ΠΏΠΎΡ‡Ρ‚Ρ‹, Ρ‚Π΅ΠΏΠ΅Ρ€ΡŒ ΠΌΠΎΠΆΠ΅ΠΌ Π½Π°ΠΏΠΈΡΠ°Ρ‚ΡŒ сСнсор для ΠΈΡ… оТидания. ΠΠ°ΠΏΠΈΡΠ°Ρ‚ΡŒ сразу ΠΎΠΏΠ΅Ρ€Π°Ρ‚ΠΎΡ€, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ Π±ΡƒΠ΄Π΅Ρ‚ ΠΎΠ±Ρ€Π°Π±Π°Ρ‚Ρ‹Π²Π°Ρ‚ΡŒ Π΄Π°Π½Π½Ρ‹Π΅, Ссли ΠΎΠ½ΠΈ ΠΈΠΌΠ΅ΡŽΡ‚ΡΡ, Π² ΠΌΠΎΠ΅ΠΌ случаС Π½Π΅ ΠΏΠΎΠ»ΡƒΡ‡ΠΈΠ»ΠΎΡΡŒ, Ρ‚Π°ΠΊ ΠΊΠ°ΠΊ Π½Π° основании ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½Π½Ρ‹Ρ… Π΄Π°Π½Π½Ρ‹Ρ… ΠΈΠ· ΠΏΠΎΡ‡Ρ‚Ρ‹ Ρ€Π°Π±ΠΎΡ‚Π°ΡŽΡ‚ ΠΈ Π΄Ρ€ΡƒΠ³ΠΈΠ΅ процСссы, Π² Ρ‚ΠΎΠΌ числС, Π±Π΅Ρ€ΡƒΡ‰ΠΈΠ΅ связанныС Π΄Π°Π½Π½Ρ‹Π΅ ΠΈΠ· Π΄Ρ€ΡƒΠ³ΠΈΡ… источников (API, тСлСфония, Π²Π΅Π± ΠΌΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ ΠΈ Ρ‚.Π΄.). ΠŸΡ€ΠΈΠ²Π΅Π΄Ρƒ ΠΏΡ€ΠΈΠΌΠ΅Ρ€. Π’ CRM систСмС появился Π½ΠΎΠ²Ρ‹ΠΉ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ, ΠΈ ΠΌΡ‹ Π΅Ρ‰Π΅ Π½Π΅ Π·Π½Π°Π΅ΠΌ ΠΏΡ€ΠΎ Π΅Π³ΠΎ UUID. Π’ΠΎΠ³Π΄Π° ΠΏΡ€ΠΈ ΠΏΠΎΠΏΡ‹Ρ‚ΠΊΠ΅ ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ Π΄Π°Π½Π½Ρ‹Π΅ с SIP-Ρ‚Π΅Π»Π΅Ρ„ΠΎΠ½ΠΈΠΈ ΠΌΡ‹ ΠΏΠΎΠ»ΡƒΡ‡ΠΈΠΌ Π·Π²ΠΎΠ½ΠΊΠΈ, привязанныС ΠΊ Π΅Π³ΠΎ UUID, Π½ΠΎ ΠΊΠΎΡ€Ρ€Π΅ΠΊΡ‚Π½ΠΎ ΡΠΎΡ…Ρ€Π°Π½ΠΈΡ‚ΡŒ ΠΈ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ ΠΈΡ… Π½Π΅ смоТСм. Π’ Ρ‚Π°ΠΊΠΈΡ… вопросах Π²Π°ΠΆΠ½ΠΎ ΠΈΠΌΠ΅Ρ‚ΡŒ Π² Π²ΠΈΠ΄Ρƒ Π·Π°Π²ΠΈΡΠΈΠΌΠΎΡΡ‚ΡŒ Π΄Π°Π½Π½Ρ‹Ρ…, особСнно, Ссли ΠΎΠ½ΠΈ ΠΈΠ· Ρ€Π°Π·Π½Ρ‹Ρ… источников. Π­Ρ‚ΠΎ, ΠΊΠΎΠ½Π΅Ρ‡Π½ΠΎ, нСдостаточныС ΠΌΠ΅Ρ€Ρ‹ сохранСния цСлостности Π΄Π°Π½Π½Ρ‹Ρ…, Π½ΠΎ Π² Π½Π΅ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Ρ… случаях Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΡ‹Π΅. Π”Π° ΠΈ Π² Ρ…ΠΎΠ»ΠΎΡΡ‚ΡƒΡŽ Π·Π°Π½ΠΈΠΌΠ°Ρ‚ΡŒ рСсурсы Ρ‚ΠΎΠΆΠ΅ Π½Π΅Ρ€Π°Ρ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎ.

Π’Π°ΠΊΠΈΠΌ ΠΎΠ±Ρ€Π°Π·ΠΎΠΌ, наш сСнсор Π±ΡƒΠ΄Π΅Ρ‚ Π·Π°ΠΏΡƒΡΠΊΠ°Ρ‚ΡŒ ΠΏΠΎΡΠ»Π΅Π΄ΡƒΡŽΡ‰ΠΈΠ΅ Π²Π΅Ρ€ΡˆΠΈΠ½Ρ‹ Π³Ρ€Π°Ρ„Π°, Ссли Π΅ΡΡ‚ΡŒ свСТая информация Π½Π° ΠΏΠΎΡ‡Ρ‚Π΅, Π° Ρ‚Π°ΠΊΠΆΠ΅ ΠΏΠΎΠΌΠ΅Ρ‡Π°Ρ‚ΡŒ Π½Π΅Π°ΠΊΡ‚ΡƒΠ°Π»ΡŒΠ½ΠΎΠΉ ΠΏΡ€Π΅Π΄Ρ‹Π΄ΡƒΡ‰ΡƒΡŽ ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡŽ.

from airflow.sensors.base_sensor_operator import BaseSensorOperator
from airflow.utils.decorators import apply_defaults
from my_plugin.hooks.imap_hook import IMAPHook

class MailSensor(BaseSensorOperator):
    @apply_defaults
    def __init__(self, conn_id, check_seen=True, box="Inbox", condition="(UNSEEN)", *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.conn_id = conn_id
        self.check_seen = check_seen
        self.box = box
        self.condition = condition

    def poke(self, context):
        conn = IMAPHook(self.conn_id)
        mail_id = conn.get_last_mail(check_seen=self.check_seen, box=self.box, condition=self.condition)

        if mail_id is None:
            return False
        else:
            return True

ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ ΠΈ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ Π΄Π°Π½Π½Ρ‹Π΅

Для получСния ΠΈ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ Π΄Π°Π½Π½Ρ‹Ρ… ΠΌΠΎΠΆΠ½ΠΎ Π½Π°ΠΏΠΈΡΠ°Ρ‚ΡŒ ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½Ρ‹ΠΉ ΠΎΠΏΠ΅Ρ€Π°Ρ‚ΠΎΡ€, ΠΌΠΎΠΆΠ½ΠΎ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ Π³ΠΎΡ‚ΠΎΠ²Ρ‹Π΅. ΠŸΠΎΡΠΊΠΎΠ»ΡŒΠΊΡƒ ΠΏΠΎΠΊΠ° Π»ΠΎΠ³ΠΈΠΊΠ° Ρ‚Ρ€ΠΈΠ²ΠΈΠ°Π»ΡŒΠ½Π°Ρ β€” Π·Π°Π±Ρ€Π°Ρ‚ΡŒ Π΄Π°Π½Π½Ρ‹Π΅ ΠΈΠ· письма, Ρ‚ΠΎ для ΠΏΡ€ΠΈΠΌΠ΅Ρ€Π° ΠΏΡ€Π΅Π΄Π»Π°Π³Π°ΡŽ стандартный PythonOperator

from airflow.models import DAG

from airflow.operators.python_operator import PythonOperator
from airflow.sensors.my_plugin import MailSensor
from my_plugin.hooks.imap_hook import IMAPHook

start_date = datetime(2020, 4, 4)

# Π‘Ρ‚Π°Π½Π΄Π°Ρ€Ρ‚Π½ΠΎΠ΅ ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ Π³Ρ€Π°Ρ„Π°
args = {
    "owner": "example",
    "start_date": start_date,
    "email": ["[email protected]"],
    "email_on_failure": False,
    "email_on_retry": False,
    "retry_delay": timedelta(minutes=15),
    "provide_context": False,
}

dag = DAG(
    dag_id="test_etl",
    default_args=args,
    schedule_interval="@hourly",
)

# ΠžΠΏΡ€Π΅Π΄Π΅Π»ΡΠ΅ΠΌ сСнсор
mail_check_sensor = MailSensor(
    task_id="check_new_emails",
    poke_interval=10,
    conn_id="mail_conn_id",
    timeout=10,
    soft_fail=True,
    box="my_box",
    dag=dag,
    mode="poke",
)

# Ѐункция для получСния Π΄Π°Π½Π½Ρ‹Ρ… ΠΈΠ· письма
def prepare_mail():
    imap_hook = IMAPHook("mail_conn_id")
    mail_id = imap_hook.get_last_mail(check_seen=True, box="my_box")
    if mail_id is None:
        raise AirflowException("Empty mailbox")

    conn.download_mail_href_attachment(mail_id, "./path.zip")

prepare_mail_data = PythonOperator(task_id="prepare_mail_data", default_args=args, dag=dag, python_callable= prepare_mail)

# ОписаниС ΠΎΡΡ‚Π°Π»ΡŒΠ½Ρ‹Ρ… Π²Π΅Ρ€ΡˆΠΈΠ½ Π³Ρ€Π°Ρ„Π°
...

# Π—Π°Π΄Π°Π΅ΠΌ связь Π½Π° Π³Ρ€Π°Ρ„Π΅
mail_check_sensor >> prepare_mail_data
prepare_data >> ...
# ОписаниС ΠΎΡΡ‚Π°Π»ΡŒΠ½Ρ‹Ρ… ΠΏΠΎΡ‚ΠΎΠΊΠΎΠ² управлСния

ΠšΡΡ‚Π°Ρ‚ΠΈ, Ссли ваша корпоративная ΠΏΠΎΡ‡Ρ‚Π° Ρ‚ΠΎΠΆΠ΅ Π½Π° mail.ru, Ρ‚ΠΎ Π²Π°ΠΌ Π±ΡƒΠ΄Π΅Ρ‚ нСдоступСн поиск писСм ΠΏΠΎ Ρ‚Π΅ΠΌΠ΅, ΠΎΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚Π΅Π»ΡŽ ΠΈ Ρ‚.Π΄. Они Π΅Ρ‰Π΅ Π² Π΄Π°Π»Π΅ΠΊΠΎΠΌ 2016 ΠΎΠ±Π΅Ρ‰Π°Π»ΠΈ ввСсти, Π½ΠΎ, Π²ΠΈΠ΄ΠΈΠΌΠΎ, ΠΏΠ΅Ρ€Π΅Π΄ΡƒΠΌΠ°Π»ΠΈ. Π― Ρ€Π΅ΡˆΠΈΠ» эту ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΡƒ, создав ΠΏΠΎΠ΄ Π½ΡƒΠΆΠ½Ρ‹Π΅ письма ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½ΡƒΡŽ ΠΏΠ°ΠΏΠΊΡƒ ΠΈ настроив Π² Π²Π΅Π±-интСрфСйсС ΠΏΠΎΡ‡Ρ‚Ρ‹ Ρ„ΠΈΠ»ΡŒΡ‚Ρ€ Π½Π° Π½ΡƒΠΆΠ½Ρ‹Π΅ письма. Π’Π°ΠΊΠΈΠΌ ΠΎΠ±Ρ€Π°Π·ΠΎΠΌ, Π² эту ΠΏΠ°ΠΏΠΊΡƒ ΠΏΠΎΠΏΠ°Π΄Π°ΡŽΡ‚ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π½ΡƒΠΆΠ½Ρ‹Π΅ письма ΠΈ условия для поиска Π² ΠΌΠΎΠ΅ΠΌ случаС просто (UNSEEN).

Π Π΅Π·ΡŽΠΌΠΈΡ€ΡƒΡ, ΠΌΡ‹ ΠΈΠΌΠ΅Π΅ΠΌ ΡΠ»Π΅Π΄ΡƒΡŽΡ‰ΡƒΡŽ ΠΏΠΎΡΠ»Π΅Π΄ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ: провСряСм, Π΅ΡΡ‚ΡŒ Π»ΠΈ Π½ΠΎΠ²Ρ‹Π΅ письма, ΡΠΎΠΎΡ‚Π²Π΅Ρ‚ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠ΅ условиям, Ссли Π΅ΡΡ‚ΡŒ, Ρ‚ΠΎ скачиваСм Π°Ρ€Ρ…ΠΈΠ² ΠΏΠΎ ссылкС ΠΈΠ· послСднСго письма.
Под послСдними многоточиями ΠΎΠΏΡƒΡ‰Π΅Π½ΠΎ, Ρ‡Ρ‚ΠΎ этот Π°Ρ€Ρ…ΠΈΠ² Π±ΡƒΠ΄Π΅Ρ‚ распакован, Π΄Π°Π½Π½Ρ‹Π΅ ΠΈΠ· Π°Ρ€Ρ…ΠΈΠ²Π° ΠΎΡ‡ΠΈΡ‰Π΅Π½Ρ‹ ΠΈ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Π°Π½Ρ‹, ΠΈ Π² ΠΈΡ‚ΠΎΠ³Π΅ всС это Π΄Π΅Π»ΠΎ ΡƒΠΉΠ΄Π΅Ρ‚ Π΄Π°Π»Π΅Π΅ Π½Π° ΠΊΠΎΠ½Π²Π΅ΠΉΠ΅Ρ€ ETL процСсса, Π½ΠΎ это ΡƒΠΆΠ΅ Π²Ρ‹Ρ…ΠΎΠ΄ΠΈΡ‚ Π·Π° Ρ€Π°ΠΌΠΊΠΈ Ρ‚Π΅ΠΌΡ‹ ΡΡ‚Π°Ρ‚ΡŒΠΈ. Если ΠΏΠΎΠ»ΡƒΡ‡ΠΈΠ»ΠΎΡΡŒ интСрСсно ΠΈ ΠΏΠΎΠ»Π΅Π·Π½ΠΎ, Ρ‚ΠΎ с Ρ€Π°Π΄ΠΎΡΡ‚ΡŒΡŽ ΠΏΡ€ΠΎΠ΄ΠΎΠ»ΠΆΡƒ ΠΎΠΏΠΈΡΡ‹Π²Π°Ρ‚ΡŒ ETL Ρ€Π΅ΡˆΠ΅Π½ΠΈΡ ΠΈ ΠΈΡ… части для Apache Airflow.

Π˜ΡΡ‚ΠΎΡ‡Π½ΠΈΠΊ: habr.com