αžŠαŸ†αžŽαžΎαžšαž€αžΆαžš ETL αžŸαž˜αŸ’αžšαžΆαž”αŸ‹αž€αžΆαžšαž‘αž‘αž½αž›αž”αžΆαž“αž‘αž·αž“αŸ’αž“αž“αŸαž™αž–αžΈαž’αŸŠαžΈαž˜αŸ‚αž›αž“αŸ…αž€αŸ’αž“αž»αž„ Apache Airflow

αžŠαŸ†αžŽαžΎαžšαž€αžΆαžš ETL αžŸαž˜αŸ’αžšαžΆαž”αŸ‹αž€αžΆαžšαž‘αž‘αž½αž›αž”αžΆαž“αž‘αž·αž“αŸ’αž“αž“αŸαž™αž–αžΈαž’αŸŠαžΈαž˜αŸ‚αž›αž“αŸ…αž€αŸ’αž“αž»αž„ Apache Airflow

αž˜αž·αž“αžαžΆαž”αž…αŸ’αž…αŸαž€αžœαž·αž‘αŸ’αž™αžΆαžšαžΈαž€αž…αž˜αŸ’αžšαžΎαž“αž”αŸ‰αž»αžŽαŸ’αžŽαžΆαž“αŸ„αŸ‡αž‘αŸ αžœαž·αž’αžΈαžŸαžΆαžŸαŸ’αžšαŸ’αžαž αž½αžŸαžŸαž˜αŸαž™αžαŸ‚αž„αžαŸ‚αžŠαžΎαžšαžαžΆαž˜αž–αžΈαž€αŸ’αžšαŸ„αž™αž€αžΆαžšαž’αž—αž·αžœαžŒαŸ’αžαž“αŸαŸ” αž“αŸαŸ‡αž’αžΆαž…αž”αžŽαŸ’αžαžΆαž›αž˜αž€αž–αžΈαž€αžΆαžšαž•αŸ’αž›αžΆαžŸαŸ‹αž”αŸ’αžαžΌαžšαžšαž›αžΌαž“ αž€αžαŸ’αžαžΆαž˜αž“αž»αžŸαŸ’αžŸ αžαž˜αŸ’αžšαžΌαžœαž€αžΆαžšαž”αž…αŸ’αž…αŸαž€αžœαž·αž‘αŸ’αž™αžΆ αž¬αž’αŸ’αžœαžΈαž•αŸ’αžŸαŸαž„αž‘αŸ€αžαŸ” αž“αŸ…αž€αŸ’αž“αž»αž„αžœαž·αžŸαŸαž™αžŠαŸ†αžŽαžΎαžšαž€αžΆαžšαž‘αž·αž“αŸ’αž“αž“αŸαž™ αž”αŸ’αžšαž—αž–αž‘αž·αž“αŸ’αž“αž“αŸαž™αž‚αžΊαž”αž„αŸ’αž αžΆαž‰αž±αŸ’αž™αžƒαžΎαž‰αž…αŸ’αžšαžΎαž“αž”αŸ†αž•αž»αžαž“αŸ…αž€αŸ’αž“αž»αž„αž•αŸ’αž“αŸ‚αž€αž“αŸαŸ‡αŸ” αž˜αž·αž“αžαžΆαž™αžΎαž„αžŸαž»αž”αž·αž“αž…αž„αŸ‹αž€αž˜αŸ’αž…αžΆαžαŸ‹αžœαžΆαž”αŸ‰αž»αžŽαŸ’αžŽαžΆαž“αŸ„αŸ‡αž‘αŸ αž”αŸ‰αž»αž“αŸ’αžαŸ‚αžšαž αžΌαžαž˜αž€αžŠαž›αŸ‹αž–αŸαž›αž“αŸαŸ‡ αž‘αž·αž“αŸ’αž“αž“αŸαž™αž˜αž½αž™αž•αŸ’αž“αŸ‚αž€αžαŸ’αžšαžΌαžœαž”αžΆαž“αž•αŸ’αž‰αžΎαž‡αžΆαžŸαžΆαžšαž”αž“αŸ’αž‘αžΆαž“αŸ‹ αž“αž·αž„αž’αŸŠαžΈαž˜αŸ‚αž› αžŠαŸ„αž™αž˜αž·αž“αž“αž·αž™αžΆαž™αž’αŸ†αž–αžΈαž‘αž˜αŸ’αžšαž„αŸ‹αž…αžΆαžŸαŸ‹αž‡αžΆαž„αž“αŸαŸ‡αž‘αŸαŸ” αžαŸ’αž‰αž»αŸ†αžŸαžΌαž˜αž’αž‰αŸ’αž‡αžΎαž‰αž’αŸ’αž“αž€αž±αŸ’αž™αž•αŸ’αžαžΆαž…αŸ‹αž‡αž˜αŸ’αžšαžΎαžŸαž˜αž½αž™αžŸαž˜αŸ’αžšαžΆαž”αŸ‹ Apache Airflow αžŠαŸ„αž™αž”αž„αŸ’αž αžΆαž‰αž–αžΈαžšαž”αŸ€αž”αžŠαŸ‚αž›αž’αŸ’αž“αž€αž’αžΆαž…αž™αž€αž‘αž·αž“αŸ’αž“αž“αŸαž™αž–αžΈαž’αŸŠαžΈαž˜αŸ‚αž›αŸ”

αžŸαž˜αŸαž™αž”αž»αžšαŸαž”αŸ’αžšαžœαžαŸ’αžαž·αžŸαžΆαžŸαŸ’αžαŸ’αžš

αž‘αž·αž“αŸ’αž“αž“αŸαž™αž‡αžΆαž…αŸ’αžšαžΎαž“αž“αŸ…αžαŸ‚αžαŸ’αžšαžΌαžœαž”αžΆαž“αž•αŸ’αž‘αŸαžšαžαžΆαž˜αžšαž™αŸˆαž’αŸŠαžΈαž˜αŸ‚αž› αž–αžΈαž‘αŸ†αž“αžΆαž€αŸ‹αž‘αŸ†αž“αž„αžšαžœαžΆαž„αž”αž»αž‚αŸ’αž‚αž›αž‘αŸ…αžŸαŸ’αžαž„αŸ‹αžŠαžΆαžšαž“αŸƒαž’αž“αŸ’αžαžšαž€αž˜αŸ’αž˜αžšαžœαžΆαž„αž€αŸ’αžšαž»αž˜αž αŸŠαž»αž“αŸ” αžœαžΆαž‡αžΆαž€αžΆαžšαž›αŸ’αž’αž”αŸ’αžšαžŸαž·αž“αž”αžΎαž’αžΆαž…αžŸαžšαžŸαŸαžšαž…αŸ†αžŽαž»αž…αž”αŸ’αžšαž‘αžΆαž€αŸ‹αžŠαžΎαž˜αŸ’αž”αžΈαž‘αž‘αž½αž›αž”αžΆαž“αž‘αž·αž“αŸ’αž“αž“αŸαž™ αž¬αžŠαžΆαž€αŸ‹αž˜αž“αž»αžŸαŸ’αžŸαž“αŸ…αž€αŸ’αž“αž»αž„αž€αžΆαžšαž·αž™αžΆαž›αŸαž™αžŠαŸ‚αž›αž“αžΉαž„αž”αž‰αŸ’αž…αžΌαž›αž–αŸαžαŸŒαž˜αžΆαž“αž“αŸαŸ‡αž‘αŸ…αž€αŸ’αž“αž»αž„αž”αŸ’αžšαž—αž–αžŠαŸ‚αž›αž„αžΆαž™αžŸαŸ’αžšαž½αž›αž‡αžΆαž„ αž”αŸ‰αž»αž“αŸ’αžαŸ‚αž‡αžΆαžšαžΏαž™αŸ—αžœαžΆαž”αŸ’αžšαž αŸ‚αž›αž‡αžΆαž˜αž·αž“αž’αžΆαž…αž‘αŸ…αžšαž½αž…αž“αŸ„αŸ‡αž‘αŸαŸ” αž—αžΆαžšαž€αž·αž…αŸ’αž…αž‡αžΆαž€αŸ‹αž›αžΆαž€αŸ‹αžŠαŸ‚αž›αžαŸ’αž‰αž»αŸ†αž”αžΆαž“αž”αŸ’αžšαžˆαž˜αž‚αžΊαž€αžΆαžšαž—αŸ’αž‡αžΆαž”αŸ‹αž”αŸ’αžšαž–αŸαž“αŸ’αž’ CRM αžŠαŸαž›αŸ’αž”αžΈαž›αŸ’αž”αžΆαž‰αž‘αŸ…αž“αžΉαž„αžƒαŸ’αž›αžΆαŸ†αž„αž‘αž·αž“αŸ’αž“αž“αŸαž™ αž αžΎαž™αž”αž“αŸ’αž‘αžΆαž”αŸ‹αž˜αž€αž‘αŸ…αž”αŸ’αžšαž–αŸαž“αŸ’αž’ OLAP αŸ” αžœαžΆαž”αžΆαž“αž€αžΎαžαž‘αžΎαž„αž‡αžΆαž”αŸ’αžšαžœαžαŸ’αžαž·αžŸαžΆαžŸαŸ’αžαŸ’αžšαžαžΆαžŸαž˜αŸ’αžšαžΆαž”αŸ‹αž€αŸ’αžšαž»αž˜αž αŸŠαž»αž“αžšαž”αžŸαŸ‹αž™αžΎαž„αž€αžΆαžšαž”αŸ’αžšαžΎαž”αŸ’αžšαžΆαžŸαŸ‹αž”αŸ’αžšαž–αŸαž“αŸ’αž’αž“αŸαŸ‡αž‚αžΊαž˜αžΆαž“αž—αžΆαž–αž„αžΆαž™αžŸαŸ’αžšαž½αž›αž“αŸ…αž€αŸ’αž“αž»αž„αžαŸ†αž”αž“αŸ‹αž‡αžΆαž€αŸ‹αž›αžΆαž€αŸ‹αž“αŸƒαž’αžΆαž‡αžΈαžœαž€αž˜αŸ’αž˜αŸ” αžŠαžΌαž…αŸ’αž“αŸαŸ‡αž αžΎαž™ αž’αŸ’αž“αž€αž‚αŸ’αžšαž”αŸ‹αž‚αŸ’αž“αžΆαž–αž·αžαž‡αžΆαž…αž„αŸ‹αž’αŸ’αžœαžΎαž”αŸ’αžšαžαž·αž”αžαŸ’αžαž·αž€αžΆαžšαž‡αžΆαž˜αž½αž™αž‘αž·αž“αŸ’αž“αž“αŸαž™αž–αžΈαž”αŸ’αžšαž–αŸαž“αŸ’αž’αž—αžΆαž‚αžΈαž‘αžΈαž”αžΈαž“αŸαŸ‡αž•αž„αžŠαŸ‚αžšαŸ” αž‡αžΆαžŠαŸ†αž”αžΌαž„ αž‡αžΆαž€αžΆαžšαž–αž·αž αž›αž‘αŸ’αž’αž—αžΆαž–αž“αŸƒαž€αžΆαžšαž‘αž‘αž½αž›αž”αžΆαž“αž‘αž·αž“αŸ’αž“αž“αŸαž™αž–αžΈ API αž”αžΎαž€αž…αŸ†αž αžαŸ’αžšαžΌαžœαž”αžΆαž“αžŸαž·αž€αŸ’αžŸαžΆαŸ” αž‡αžΆαž’αž€αž»αžŸαž› API αž˜αž·αž“αž”αžΆαž“αž‚αŸ’αžšαž”αžŠαžŽαŸ’αžαž”αŸ‹αž›αžΎαž€αžΆαžšαž‘αž‘αž½αž›αž”αžΆαž“αž‘αž·αž“αŸ’αž“αž“αŸαž™αž…αžΆαŸ†αž”αžΆαž…αŸ‹αž‘αžΆαŸ†αž„αž’αžŸαŸ‹αž“αŸ„αŸ‡αž‘αŸ αž αžΎαž™αž“αž·αž™αžΆαž™αž±αŸ’αž™αž…αŸ†αž‘αŸ… αžœαžΆαž‚αžΊαž“αŸ…αž€αŸ’αž“αž»αž„αžœαž·αž’αžΈαž‡αžΆαž…αŸ’αžšαžΎαž“αžŠαŸ‚αž›αž˜αžΆαž“αž›αž€αŸ’αžαžŽαŸˆαž…αŸ’αžšαž”αžΌαž€αž…αŸ’αžšαž”αž›αŸ‹ αž αžΎαž™αž‡αŸ†αž“αž½αž™αž•αŸ’αž“αŸ‚αž€αž”αž…αŸ’αž…αŸαž€αž‘αŸαžŸαž˜αž·αž“αž…αž„αŸ‹αž”αžΆαž“ αž¬αž˜αž·αž“αž’αžΆαž…αž”αŸ†αž–αŸαž‰αž”αžΆαž“αž–αžΆαž€αŸ‹αž€αžŽαŸ’αžαžΆαž›αžŠαžΎαž˜αŸ’αž”αžΈαž•αŸ’αžαž›αŸ‹αž˜αž»αžαž„αžΆαžšαž‘αžΌαž›αŸ†αž‘αžΌαž›αžΆαž™αž”αž“αŸ’αžαŸ‚αž˜αž‘αŸ€αžαŸ” αž”αŸ‰αž»αž“αŸ’αžαŸ‚αž”αŸ’αžšαž–αŸαž“αŸ’αž’αž“αŸαŸ‡αž”αžΆαž“αž•αŸ’αžαž›αŸ‹αž±αž€αžΆαžŸαž€αŸ’αž“αž»αž„αž€αžΆαžšαž‘αž‘αž½αž›αž‘αž·αž“αŸ’αž“αž“αŸαž™αžŠαŸ‚αž›αž”αžΆαžαŸ‹αžαžΆαž˜αž”αŸ’αžšαŸƒαžŸαžŽαžΈαž™αŸαžαžΆαž˜αž€αžΆαž›αž€αŸ†αžŽαžαŸ‹αž€αŸ’αž“αž»αž„αž‘αž˜αŸ’αžšαž„αŸ‹αž‡αžΆαžαŸ†αžŽαž—αŸ’αž‡αžΆαž”αŸ‹αžŸαž˜αŸ’αžšαžΆαž”αŸ‹αž€αžΆαžšαžŠαž€αž―αž€αžŸαžΆαžšαž‘αž»αž€αŸ”

αž‚αž½αžšαž€αžαŸ‹αžŸαž˜αŸ’αž‚αžΆαž›αŸ‹αžαžΆαž“αŸαŸ‡αž˜αž·αž“αž˜αŸ‚αž“αž‡αžΆαž€αžšαžŽαžΈαžαŸ‚αž˜αž½αž™αž‚αžαŸ‹αžŠαŸ‚αž›αž’αžΆαž‡αžΈαžœαž€αž˜αŸ’αž˜αž…αž„αŸ‹αž”αŸ’αžšαž˜αžΌαž›αž‘αž·αž“αŸ’αž“αž“αŸαž™αž–αžΈαž’αŸŠαžΈαž˜αŸ‚αž› αž¬αž’αŸ’αž“αž€αž•αŸ’αž‰αžΎαžŸαžΆαžšαž”αž“αŸ’αž‘αžΆαž“αŸ‹αž“αŸ„αŸ‡αž‘αŸαŸ” αž‘αŸ„αŸ‡αž™αŸ‰αžΆαž„αžŽαžΆαž€αŸαžŠαŸ„αž™ αž€αŸ’αž“αž»αž„αž€αžšαžŽαžΈαž“αŸαŸ‡ αž™αžΎαž„αž˜αž·αž“αž’αžΆαž…αž˜αžΆαž“αž₯αž‘αŸ’αž’αž·αž–αž›αž›αžΎαž€αŸ’αžšαž»αž˜αž αŸŠαž»αž“αž—αžΆαž‚αžΈαž‘αžΈαž”αžΈαžŠαŸ‚αž›αž•αŸ’αžαž›αŸ‹αž•αŸ’αž“αŸ‚αž€αž“αŸƒαž‘αž·αž“αŸ’αž“αž“αŸαž™αžαŸ‚αžαžΆαž˜αžœαž·αž’αžΈαž“αŸαŸ‡αž‘αŸαŸ”

αž›αŸ†αž αžΌαžšαž˜αŸ‰αžΆαžŸαŸŠαžΈαž“αžαŸ’αžšαž‡αžΆαž€αŸ‹ Apache

αžŠαžΎαž˜αŸ’αž”αžΈαž”αž„αŸ’αž€αžΎαžαžŠαŸ†αžŽαžΎαžšαž€αžΆαžš ETL αž™αžΎαž„αž—αžΆαž‚αž…αŸ’αžšαžΎαž“αž”αŸ’αžšαžΎ Apache Airflow αŸ” αžŠαžΎαž˜αŸ’αž”αžΈαž±αŸ’αž™αž’αŸ’αž“αž€αž’αžΆαž“αžŠαŸ‚αž›αž˜αž·αž“αžŸαŸ’αž‚αžΆαž›αŸ‹αž”αž…αŸ’αž…αŸαž€αžœαž·αž‘αŸ’αž™αžΆαž“αŸαŸ‡αž™αž›αŸ‹αž€αžΆαž“αŸ‹αžαŸ‚αž…αŸ’αž”αžΆαžŸαŸ‹αž’αŸ†αž–αžΈαžšαž”αŸ€αž”αžŠαŸ‚αž›αžœαžΆαž˜αžΎαž›αž‘αŸ…αž€αŸ’αž“αž»αž„αž”αžšαž·αž”αž‘ αž αžΎαž™αž‡αžΆαž‘αžΌαž‘αŸ… αžαŸ’αž‰αž»αŸ†αž“αžΉαž„αžšαŸ€αž”αžšαžΆαž”αŸ‹αž’αŸ†αž–αžΈαž€αžΆαžšαžŽαŸ‚αž“αžΆαŸ†αž˜αž½αž™αž…αŸ†αž“αž½αž“αŸ”

Apache Airflow αž‚αžΊαž‡αžΆαžœαŸαž‘αž·αž€αžΆαž₯αžαž‚αž·αžαžαŸ’αž›αŸƒαžŠαŸ‚αž›αžαŸ’αžšαžΌαžœαž”αžΆαž“αž”αŸ’αžšαžΎαžŠαžΎαž˜αŸ’αž”αžΈαž”αž„αŸ’αž€αžΎαž αž”αŸ’αžšαžαž·αž”αžαŸ’αžαž· αž“αž·αž„αžαŸ’αžšαž½αžαž–αž·αž“αž·αžαŸ’αž™αžŠαŸ†αžŽαžΎαžšαž€αžΆαžš ETL (Extract-Transform-Loading) αž“αŸ…αž€αŸ’αž“αž»αž„ Python αŸ” αž‚αŸ„αž›αž‚αŸ†αž“αž·αžαž…αž˜αŸ’αž”αž„αž“αŸ…αž€αŸ’αž“αž»αž„ Airflow αž‚αžΊαž‡αžΆαž€αŸ’αžšαžΆαž αŸ’αžœ acyclic αžŠαžΉαž€αž“αžΆαŸ† αžŠαŸ‚αž›αž…αŸ†αž“αž»αž…αž€αŸ†αž–αžΌαž›αž“αŸƒαž€αŸ’αžšαžΆαž αŸ’αžœαž‚αžΊαž‡αžΆαžŠαŸ†αžŽαžΎαžšαž€αžΆαžšαž‡αžΆαž€αŸ‹αž›αžΆαž€αŸ‹ αž αžΎαž™αž‚αŸ‚αž˜αž“αŸƒαž€αŸ’αžšαžΆαž αŸ’αžœαž‚αžΊαž‡αžΆαž›αŸ†αž αžΌαžšαž“αŸƒαž€αžΆαžšαž‚αŸ’αžšαž”αŸ‹αž‚αŸ’αžšαž„ αž¬αž–αŸαžαŸŒαž˜αžΆαž“αŸ” αžŠαŸ†αžŽαžΎαžšαž€αžΆαžšαž˜αž½αž™αž’αžΆαž…αž αŸ…αž˜αž»αžαž„αžΆαžš 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, telephony αžšαž„αŸ’αžœαžΆαžŸαŸ‹αž”αžŽαŸ’αžαžΆαž‰αŸ”αž›αŸ”)αŸ”αž›αŸ”)αŸ” αžαŸ’αž‰αž»αŸ†αž“αžΉαž„αž•αŸ’αžαž›αŸ‹αž±αŸ’αž™αž’αŸ’αž“αž€αž“αžΌαžœαž§αž‘αžΆαž αžšαžŽαŸαž˜αž½αž™αŸ” αž’αŸ’αž“αž€αž”αŸ’αžšαžΎαž”αŸ’αžšαžΆαžŸαŸ‹αžαŸ’αž˜αžΈαž”αžΆαž“αž”αž„αŸ’αž αžΆαž‰αžαŸ’αž›αž½αž“αž“αŸ…αž€αŸ’αž“αž»αž„αž”αŸ’αžšαž–αŸαž“αŸ’αž’ 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 αŸ”

αž”αŸ’αžšαž—αž–: www.habr.com

αž”αž“αŸ’αžαŸ‚αž˜αž˜αžαž·αž™αŸ„αž”αž›αŸ‹