Как мы автоматизировали ежедневный твит проекта 100DaysOfCode

Разработка /
Разработка: Как мы автоматизировали ежедневный твит проекта 100DaysOfCode
В этом посте я покажу вам способ сделать автоматический твит о прогрессе в испытании #100DaysOfCode Challenge. После этого у вас останется больше времени на разработку. Супер, да?

Это день 007 нашего испытания 100 Days of Code. Весь код проекта хранится здесь.

Подготовка

Итак, нам понадобятся pytz, tweepy и requests. Вы можете установить их все разом командой:

pip install -r requirements.txt
если вы склонировали репозиторий. Также рекомендую использовать virtualenv для изоляции окружений.

Вам также понадобятся Consumer Key/Secret и Access Token (Secret) из Twitter. Я добавил их в .bashrc, который я загружаю через os.environ в config.py. Там же запускается обработчик логгирования, записывающий исходящие твиты и все возможные ошибки.

Главный скрипт

Согласно PEP8, вначале импортируем стандартную библиотеку, затем внешние модули, а ниже — модули проекта:

import datetime
import os
import re
import sys

import requests
import pytz

from config import logging, api

Мой сервер запущен в часовом поясе Mountain Time, а мне необходимо было работать с поясами в EMEA. В этом нам поможет pytz, с ним очень легко работать с любыми часовыми поясами:

tz = pytz.timezone('Europe/Amsterdam')
now = datetime.datetime.now(tz)
start = datetime.datetime(2017, 3, 29, tzinfo=tz)  # = PyBites 100 days :)

По PEP8 константы зададим символами в верхнем регистре с разделителем в виде подчёркивания. Очень удобный расчёт дат:

CURRENT_CHALLENGE_DAY = str((now - start).days).zfill(3)
LOG = 'https://raw.githubusercontent.com/pybites/100DaysOfCode/master/LOG.md'
LOG_ENTRY = re.compile(r'\[(?P<title>.*?)\]\((?P<day>\d+)\)')
REPO_URL = 'https://github.com/pybites/100DaysOfCode/tree/master/'
TWEET_LEN = 140
TWEET_LINK_LEN = 23

Ну и куда без requests? Так одной строкой я получаю файл LOG.md из репозитория:

def get_log():
    return requests.get(LOG).text.split('\n')

Получим название скрипта и строку с соответствующей датой из LOG.md (сегодня = '007'):

def get_day_progress(html):
    lines = [line.strip()
            for line in html
            if line.strip()]

    for line in lines:
        day_entry = line.strip('|').split('|')[0].strip()
        if day_entry == CURRENT_CHALLENGE_DAY:
            return LOG_ENTRY.search(line).groupdict()

Создаём твит. Я добавил немного кода для сокращения названия скрипт, если он превышает допустимый размер твита:

def create_tweet(m):
    ht1, ht2 = '#100DaysOfCode', '#Python'
    title = m['title']
    day = m['day']
    url = REPO_URL + day
    allowed_len = TWEET_LEN + len(url) - TWEET_LINK_LEN

    fmt = '{} - Day {}: {} {} {}'
    tweet = fmt.format(ht1, day, title, url, ht2)
    surplus = len(tweet) - allowed_len

    if surplus > 0:
        new_title = title[:-(surplus + 4)] + '...'
        tweet = tweet.replace(title, new_title)
    return tweet

Метод tweet_status() отправляет твит. Здесь мы используем импортированный объект api (из config.py) для отправки твита, а также запишем в лог информацию об успешной отправке или об ошибке:

def tweet_status(tweet):
    try:
        api.update_status(tweet)
        logging.info('Posted to Twitter')
    except Exception as exc:
        logging.error('Error posting to Twitter: {}'.format(exc))

Будем запускать наш скрипт из main. Также я добавил несколько переменных для проверок:

if __name__ == '__main__':
    import socket
    local = 'MacBook' in socket.gethostname()
    test = local or 'dry' in sys.argv[1:]

В режиме тестирования я использую локальный файл LOG:

if test:
        log = os.path.basename(LOG)
        with open(log) as f:
            html = f.readlines()
    else:
        html = get_log()

Если по какой-то причине я не смогу получить данные из get_day_progress(), скрипт прекратит работу и в лог запишется ошибка:

m = get_day_progress(html)
    if not m:
        logging.error('Error getting day progress from log')
        sys.exit(1)

Создаём твит. В режиме тестирования просто запишем его в лог, иначе — отправляем:

tweet = create_tweet(m)
    if test:
        logging.info('Test: tweet to send: {}'.format(tweet))
    else:
        tweet_status(tweet)

Деплой

Есть несколько вещей, которые необходимо сделать для работы нашей программы: source .bashrc для загрузки переменных среды, экспортировать PYTHONPATH, задать полный путь до python3. И как сказано здесь: «Cron ничего не знает о вашей оболочке; он запускается системой, поэтому у него минимум данных о среде.»

$ crontab -l
...
34 14 * * * source $HOME/.bashrc && export PYTHONPATH=$HOME/bin/python3/lib/python3.5/site-packages && cd $HOME/code/100days/007 && $HOME/bin/python3/bin/python3.5 100day_autotweet.py

Результат

Какое совпадение: твит о прогрессе за сегодня как раз ушёл :)

Разработка: Как мы автоматизировали ежедневный твит проекта 100DaysOfCode

Логгирование

Очень полезная фишка модуля логгирования — автоматическое получение лога всех внешних модулей. Посмотрите в лог, там намного больше, чем пишет моя программа:

$ vi 100day_autotweet.log
...
...
14:34:02 tweepy.binder INFO     PARAMS: {'status': b'#100DaysOfCode - Day 007: script to automatically tweet 100DayOfCode progress tweet https://github.com/pybites/100DaysOfCode/tree/master/007 #Python'}
...
many more log entries ...
...
14:34:02 requests.packages.urllib3.connectionpool DEBUG    https://api.twitter.com:443 "POST /1.1/statuses/update.json?status=%23100DaysOfCode+-+Day+007%3A+script+to+automatically+tweet+100DayOfCode+progress+tweet+https%3A%2F%2Fgithub.com%2Fpybites%2F100DaysOfCode%2Ftree%2Fmaster%2F007+%23Python HTTP/1.1" 200 2693
14:34:02 root         INFO     Posted to Twitter ==> my message

Конечно, это можно отключить, повысив уровень логгирования (INFO или ещё выше) в logging.basicConfig (в config.py). Ну и почитайте документацию про это.

По материалам «How we Automated our 100DaysOfCode Daily Tweet».
0 комментариев
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.