├── utils ├── __init__.py ├── mail.py └── client.py ├── Procfile ├── Pipfile ├── README.md ├── Dockerfile ├── LICENSE ├── .gitignore ├── bot.py └── Pipfile.lock /utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | bot: python3 bot.py -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | pytz = "*" 8 | python-telegram-bot = "*" 9 | pyzmail36 = "*" 10 | 11 | [dev-packages] 12 | pylint = "*" 13 | 14 | [requires] 15 | python_version = "3.9" 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # telegram-mail-bot 2 | 3 | A Telegram bot that retrives the newest email periodically and sends them to you as chat messages. 4 | 5 | 6 | ![Python Version](https://img.shields.io/badge/python-3.6-blue.svg) 7 | [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | from python:3.9 2 | 3 | RUN pip install --no-cache-dir pytz python-telegram-bot pyzmail36 4 | 5 | COPY utils /opt/workdir/telegram-mail-bot/utils 6 | COPY bot.py /opt/workdir/telegram-mail-bot/ 7 | 8 | WORKDIR /opt/workdir/telegram-mail-bot 9 | 10 | ENV TELEGRAM_TOKEN= 11 | 12 | CMD ["/bin/sh", "-c", "/usr/local/bin/python bot.py" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 hyfc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /utils/mail.py: -------------------------------------------------------------------------------- 1 | from pyzmail import PyzMessage, decode_text 2 | 3 | class Email(object): 4 | def __init__(self, raw_mail_lines): 5 | msg_content = b'\r\n'.join(raw_mail_lines) 6 | msg = PyzMessage.factory(msg_content) 7 | 8 | self.subject = msg.get_subject() 9 | self.sender = msg.get_address('from') 10 | self.date = msg.get_decoded_header('date', '') 11 | self.id = msg.get_decoded_header('message-id', '') 12 | 13 | for mailpart in msg.mailparts: 14 | if mailpart.is_body=='text/plain': 15 | payload, used_charset=decode_text(mailpart.get_payload(), mailpart.charset, None) 16 | self.charset = used_charset 17 | self.text = payload 18 | return 19 | else: 20 | self.text = None 21 | 22 | def __repr__(self): 23 | mail_str = "Subject: %s\n" % self.subject 24 | mail_str += "From: %s %s\n" % self.sender 25 | mail_str += "Date: %s\n" % self.date 26 | mail_str += "ID: %s\n" % self.id 27 | if self.text: 28 | mail_str += "Text: %s\n" % self.text 29 | return mail_str 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | #IDEA files 107 | .idea/ 108 | 109 | #vscode files 110 | .vscode/ -------------------------------------------------------------------------------- /utils/client.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import poplib 3 | from utils.mail import Email 4 | 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | 10 | class EmailClient(object): 11 | def __init__(self, email_account, passwd): 12 | self.email_account = email_account 13 | self.password = passwd 14 | self.server = self.connect(self) 15 | 16 | @staticmethod 17 | def connect(self): 18 | # parse the server's hostname from email account 19 | pop3_server = 'pop.'+self.email_account.split('@')[-1] 20 | server = poplib.POP3_SSL(pop3_server) 21 | # display the welcome info received from server, 22 | # indicating the connection is set up properly 23 | logger.info(server.getwelcome().decode('utf8')) 24 | # authenticating 25 | server.user(self.email_account) 26 | server.pass_(self.password) 27 | return server 28 | 29 | def get_mails_list(self): 30 | _, mails, _ = self.server.list() 31 | return mails 32 | 33 | def get_mails_count(self): 34 | mails = self.get_mails_list() 35 | return len(mails) 36 | 37 | def get_mail_by_index(self, index): 38 | resp_status, mail_lines, mail_octets = self.server.retr(index) 39 | return Email(mail_lines) 40 | 41 | def __enter__(self): 42 | return self 43 | 44 | def __exit__(self, exc_type, exc_val, exc_tb): 45 | if exc_type is None: 46 | logger.info('exited normally\n') 47 | self.server.quit() 48 | else: 49 | logger.error('raise an exception! ' + str(exc_type)) 50 | self.server.close() 51 | return False # Propagate 52 | 53 | 54 | 55 | if __name__ == '__main__': 56 | useraccount = "XXXXX" 57 | password = "XXXXXX" 58 | 59 | client = EmailClient(useraccount, password) 60 | num = client.get_mails_count() 61 | print(num) 62 | for i in range(1, num): 63 | print(client.get_mail_by_index(i)) -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | from telegram import ParseMode, Update 5 | from telegram.constants import MAX_MESSAGE_LENGTH 6 | from telegram.ext import (Updater, CommandHandler, CallbackContext) 7 | from utils.client import EmailClient 8 | 9 | 10 | logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s:%(lineno)d - %(message)s', 11 | stream=sys.stdout, 12 | level=logging.INFO) 13 | logger = logging.getLogger(__name__) 14 | 15 | bot_token = os.environ['TELEGRAM_TOKEN'] 16 | 17 | owner_chat_id = int(os.environ['OWNER_CHAT_ID']) 18 | 19 | def is_owner(update: Update) -> bool: 20 | return update.message.chat_id == owner_chat_id 21 | 22 | def handle_large_text(text): 23 | while text: 24 | if len(text) < MAX_MESSAGE_LENGTH: 25 | yield text 26 | text = None 27 | else: 28 | out = text[:MAX_MESSAGE_LENGTH] 29 | yield out 30 | text = text.lstrip(out) 31 | 32 | def error(update: Update, context: CallbackContext) -> None: 33 | """Log Errors caused by Updates.""" 34 | logger.warning('Update "%s" caused error "%s"', update, context.error) 35 | 36 | def start_callback(update: Update, context: CallbackContext) -> None: 37 | if not is_owner(update): 38 | return 39 | msg = "Use /help to get help" 40 | # print(update) 41 | update.message.reply_text(msg) 42 | 43 | def _help(update: Update, context: CallbackContext) -> None: 44 | if not is_owner(update): 45 | return 46 | """Send a message when the command /help is issued.""" 47 | help_str = """邮箱设置: 48 | /setting john.doe@example.com password 49 | /inbox 50 | /get mail_index 51 | /help get help""" 52 | # help_str = "*Mailbox Setting*:\n" \ 53 | # "/setting john.doe@example.com password\n" \ 54 | # "/inbox\n" \ 55 | # "/get mail_index" 56 | context.bot.send_message(update.message.chat_id, 57 | # parse_mode=ParseMode.MARKDOWN, 58 | text=help_str) 59 | 60 | def setting_email(update: Update, context: CallbackContext) -> None: 61 | if not is_owner(update): 62 | return 63 | global email_addr, email_passwd, inbox_num 64 | email_addr = context.args[0] 65 | email_passwd = context.args[1] 66 | logger.info("received setting_email command.") 67 | update.message.reply_text("Configure email success!") 68 | with EmailClient(email_addr, email_passwd) as client: 69 | inbox_num = client.get_mails_count() 70 | context.job_queue.run_repeating(periodic_task, interval=60, context=update.message.chat_id) 71 | # chat_data['job'] = job 72 | logger.info("periodic task scheduled.") 73 | 74 | 75 | def periodic_task(context: CallbackContext) -> None: 76 | global inbox_num 77 | logger.info("entering periodic task.") 78 | with EmailClient(email_addr, email_passwd) as client: 79 | new_inbox_num = client.get_mails_count() 80 | if new_inbox_num > inbox_num: 81 | mail = client.get_mail_by_index(new_inbox_num) 82 | content = mail.__repr__() 83 | for text in handle_large_text(content): 84 | context.bot.send_message(context.job.context, 85 | text=text) 86 | inbox_num = new_inbox_num 87 | 88 | def inbox(update: Update, context: CallbackContext) -> None: 89 | if not is_owner(update): 90 | return 91 | logger.info("received inbox command.") 92 | with EmailClient(email_addr, email_passwd) as client: 93 | global inbox_num 94 | new_num = client.get_mails_count() 95 | reply_text = "The index of newest mail is *%d*," \ 96 | " received *%d* new mails since last" \ 97 | " time you checked." % \ 98 | (new_num, new_num - inbox_num) 99 | inbox_num = new_num 100 | context.bot.send_message(update.message.chat_id, 101 | parse_mode=ParseMode.MARKDOWN, 102 | text=reply_text) 103 | 104 | def get_email(update: Update, context: CallbackContext) -> None: 105 | if not is_owner(update): 106 | return 107 | index = context.args[0] 108 | logger.info("received get command.") 109 | with EmailClient(email_addr, email_passwd) as client: 110 | mail = client.get_mail_by_index(index) 111 | content = mail.__repr__() 112 | for text in handle_large_text(content): 113 | context.bot.send_message(update.message.chat_id, 114 | text=text) 115 | 116 | def main(): 117 | # Create the EventHandler and pass it your bot's token. 118 | updater = Updater(token=bot_token, use_context=True) 119 | print(bot_token) 120 | 121 | # Get the dispatcher to register handlers 122 | dp = updater.dispatcher 123 | 124 | # simple start function 125 | dp.add_handler(CommandHandler("start", start_callback)) 126 | 127 | dp.add_handler(CommandHandler("help", _help)) 128 | # 129 | # Add command handler to set email address and account. 130 | dp.add_handler(CommandHandler("setting", setting_email)) 131 | 132 | dp.add_handler(CommandHandler("inbox", inbox)) 133 | 134 | dp.add_handler(CommandHandler("get", get_email)) 135 | 136 | 137 | dp.add_error_handler(error) 138 | 139 | # Start the Bot 140 | updater.start_polling() 141 | 142 | # Run the bot until you press Ctrl-C or the process receives SIGINT, 143 | # SIGTERM or SIGABRT. This should be used most of the time, since 144 | # start_polling() is non-blocking and will stop the bot gracefully. 145 | updater.idle() 146 | 147 | 148 | if __name__ == '__main__': 149 | main() -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "65ffd10327170095842dff53978ba96e55245c9e61073a9fca7cc117a9bdf85b" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "certifi": { 20 | "hashes": [ 21 | "sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7", 22 | "sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0" 23 | ], 24 | "version": "==2018.4.16" 25 | }, 26 | "future": { 27 | "hashes": [ 28 | "sha256:e39ced1ab767b5936646cedba8bcce582398233d6a627067d4c6a454c90cfedb" 29 | ], 30 | "version": "==0.16.0" 31 | }, 32 | "python-telegram-bot": { 33 | "hashes": [ 34 | "sha256:725a28f77a04d056559015963a21eacf5d2d1f1722192237284828b7cc437465", 35 | "sha256:ca2f8a44ddef7271477e16f4986647fa90ef4df5b55a7953e53b9c9d2672f639" 36 | ], 37 | "index": "pypi", 38 | "version": "==10.1.0" 39 | }, 40 | "pytz": { 41 | "hashes": [ 42 | "sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053", 43 | "sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277" 44 | ], 45 | "index": "pypi", 46 | "version": "==2018.5" 47 | }, 48 | "pyzmail36": { 49 | "hashes": [ 50 | "sha256:4ac22b663a2525422b15de7f1ca5e60983e1adb647debd14ae955963d4d6b098" 51 | ], 52 | "index": "pypi", 53 | "version": "==1.0.3" 54 | } 55 | }, 56 | "develop": { 57 | "astroid": { 58 | "hashes": [ 59 | "sha256:0a0c484279a5f08c9bcedd6fa9b42e378866a7dcc695206b92d59dc9f2d9760d", 60 | "sha256:218e36cf8d98a42f16214e8670819ce307fa707d1dcf7f9af84c7aede1febc7f" 61 | ], 62 | "version": "==2.0.1" 63 | }, 64 | "colorama": { 65 | "hashes": [ 66 | "sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda", 67 | "sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1" 68 | ], 69 | "markers": "sys_platform == 'win32'", 70 | "version": "==0.3.9" 71 | }, 72 | "isort": { 73 | "hashes": [ 74 | "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", 75 | "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", 76 | "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" 77 | ], 78 | "version": "==4.3.4" 79 | }, 80 | "lazy-object-proxy": { 81 | "hashes": [ 82 | "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", 83 | "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", 84 | "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", 85 | "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", 86 | "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", 87 | "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", 88 | "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", 89 | "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", 90 | "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", 91 | "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", 92 | "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", 93 | "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", 94 | "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", 95 | "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", 96 | "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", 97 | "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", 98 | "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", 99 | "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", 100 | "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", 101 | "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", 102 | "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", 103 | "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", 104 | "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", 105 | "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", 106 | "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", 107 | "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", 108 | "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", 109 | "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", 110 | "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" 111 | ], 112 | "version": "==1.3.1" 113 | }, 114 | "mccabe": { 115 | "hashes": [ 116 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 117 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 118 | ], 119 | "version": "==0.6.1" 120 | }, 121 | "pylint": { 122 | "hashes": [ 123 | "sha256:248a7b19138b22e6390cba71adc0cb03ac6dd75a25d3544f03ea1728fa20e8f4", 124 | "sha256:9cd70527ef3b099543eeabeb5c80ff325d86b477aa2b3d49e264e12d12153bc8" 125 | ], 126 | "index": "pypi", 127 | "version": "==2.0.0" 128 | }, 129 | "six": { 130 | "hashes": [ 131 | "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", 132 | "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" 133 | ], 134 | "version": "==1.11.0" 135 | }, 136 | "typed-ast": { 137 | "hashes": [ 138 | "sha256:0948004fa228ae071054f5208840a1e88747a357ec1101c17217bfe99b299d58", 139 | "sha256:10703d3cec8dcd9eef5a630a04056bbc898abc19bac5691612acba7d1325b66d", 140 | "sha256:1f6c4bd0bdc0f14246fd41262df7dfc018d65bb05f6e16390b7ea26ca454a291", 141 | "sha256:25d8feefe27eb0303b73545416b13d108c6067b846b543738a25ff304824ed9a", 142 | "sha256:29464a177d56e4e055b5f7b629935af7f49c196be47528cc94e0a7bf83fbc2b9", 143 | "sha256:2e214b72168ea0275efd6c884b114ab42e316de3ffa125b267e732ed2abda892", 144 | "sha256:3e0d5e48e3a23e9a4d1a9f698e32a542a4a288c871d33ed8df1b092a40f3a0f9", 145 | "sha256:519425deca5c2b2bdac49f77b2c5625781abbaf9a809d727d3a5596b30bb4ded", 146 | "sha256:57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa", 147 | "sha256:668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe", 148 | "sha256:68ba70684990f59497680ff90d18e756a47bf4863c604098f10de9716b2c0bdd", 149 | "sha256:6de012d2b166fe7a4cdf505eee3aaa12192f7ba365beeefaca4ec10e31241a85", 150 | "sha256:79b91ebe5a28d349b6d0d323023350133e927b4de5b651a8aa2db69c761420c6", 151 | "sha256:8550177fa5d4c1f09b5e5f524411c44633c80ec69b24e0e98906dd761941ca46", 152 | "sha256:898f818399cafcdb93cbbe15fc83a33d05f18e29fb498ddc09b0214cdfc7cd51", 153 | "sha256:94b091dc0f19291adcb279a108f5d38de2430411068b219f41b343c03b28fb1f", 154 | "sha256:a26863198902cda15ab4503991e8cf1ca874219e0118cbf07c126bce7c4db129", 155 | "sha256:a8034021801bc0440f2e027c354b4eafd95891b573e12ff0418dec385c76785c", 156 | "sha256:bc978ac17468fe868ee589c795d06777f75496b1ed576d308002c8a5756fb9ea", 157 | "sha256:c05b41bc1deade9f90ddc5d988fe506208019ebba9f2578c622516fd201f5863", 158 | "sha256:c9b060bd1e5a26ab6e8267fd46fc9e02b54eb15fffb16d112d4c7b1c12987559", 159 | "sha256:edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87", 160 | "sha256:f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6" 161 | ], 162 | "markers": "python_version < '3.7' and implementation_name == 'cpython'", 163 | "version": "==1.1.0" 164 | }, 165 | "wrapt": { 166 | "hashes": [ 167 | "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6" 168 | ], 169 | "version": "==1.10.11" 170 | } 171 | } 172 | } 173 | --------------------------------------------------------------------------------