diff --git a/.github/workflows/build-newsbot.yaml b/.github/workflows/build-newsbot.yaml new file mode 100644 index 0000000..af6343b --- /dev/null +++ b/.github/workflows/build-newsbot.yaml @@ -0,0 +1,51 @@ +name: Lint and build Docker +on: [push, pull_request] + +jobs: + lint: + timeout-minutes: 10 + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v1 + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: "3.7" + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + cd source-code/chapter-7/exercise-2/newsbot-compose + pip install -r requirements.txt + + - name: Lint with flake8 + run: | + pip install flake8 + cd source-code/chapter-7/exercise-2/newsbot-compose + # run flake8 first to detect any python syntax errors + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # run again to exit treating all errors as warnings + flake8 . --count --exit-zero --max-complexity=10 --statistics + + docker-build: + timeout-minutes: 10 + runs-on: ubuntu-latest + needs: lint + + steps: + - name: Checkout + uses: actions/checkout@v1 + + - name: Build Docker Image + env: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + + run: | + cd source-code/chapter-7/exercise-2/newsbot-compose + docker login -u ${DOCKER_USERNAME} -p ${DOCKER_PASSWORD} + docker build -t ${DOCKER_USERNAME}/newsbot:${GITHUB_SHA} . + docker push ${DOCKER_USERNAME}/newsbot:${GITHUB_SHA} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 178dc1f..ab5e453 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ **/*.pyc -**/venv/* +**/*venv/* **/.idea/* diff --git a/README.md b/README.md index 40e1f5a..facb3d2 100644 --- a/README.md +++ b/README.md @@ -7,16 +7,18 @@ This repository accompanies [*Practical Docker with Python*](https://www.apress. Download the files as a zip using the green button, or clone the repository to your machine using Git. -The source code is available chapter wise and is available in [/source-code](/source-code) directory. The zip files corresponding to each filename referenced in the exercise. The structure is as below +The source code is available chapter wise and is available in [/source-code](/source-code) directory. The zip files referenced in each chapter is available under [releases](https://github.com/Apress/practical-docker-with-python/releases) corresponding to each filename referenced in the exercise. The structure is as below ``` +── chapter-3 +│   ├── python-app ── chapter-4 │   ├── exercise-1 │   │   ├── docker-hello-world │   ├── exercise-2 │   │   ├── docker-multi-stage │   └── exercise-3 -│   └── docker-subreddit-fetcher +│   └── newsbot ├── chapter-5 │   ├── exercise-1 │   │   ├── docker-volume-bind-mount diff --git a/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/LICENSE b/source-code/chapter-3/python-app/LICENSE similarity index 96% rename from source-code/chapter-7/exercise-2/subreddit-fetcher-compose/LICENSE rename to source-code/chapter-3/python-app/LICENSE index 210b49e..6734375 100644 --- a/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/LICENSE +++ b/source-code/chapter-3/python-app/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Sathya +Copyright (c) 2015 Sathyajith Bhat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/source-code/chapter-3/python-app/README.md b/source-code/chapter-3/python-app/README.md new file mode 100644 index 0000000..4498390 --- /dev/null +++ b/source-code/chapter-3/python-app/README.md @@ -0,0 +1,21 @@ +# themnewsbot + +Telegram bot which fetches Bot posts top submissions from a subreddit. NewsBot is used to demo a short, simple Python project in my book, [Practical Docker With Python](https://www.apress.com/gp/book/9781484237830). + +Refer to the [book repo](https://github.com/apress/practical-docker-with-python) for other exercises using this code. + +### Getting started + +- Clone the repo or download the code +- Install the requirements with pip + + pip3 install -r requirements.txt + +- Set the environment variable `NBT_ACCESS_TOKEN` where the value is the Bot token generated using Telegram BotFather. + - See instructions on how to generate the token in Chapter 3 or [refer to this guide](https://core.telegram.org/bots/api#authorizing-your-bot) + - See this guide on [how to set environment variables](https://core.telegram.org/bots/api#authorizing-your-bot) +- Start the bot + python newsbot.py + +where `` is the [Telegram Bot API](https://core.telegram.org/bots/api) token + diff --git a/source-code/chapter-4/exercise-3/docker-subreddit-fetcher/constants.py b/source-code/chapter-3/python-app/constants.py similarity index 58% rename from source-code/chapter-4/exercise-3/docker-subreddit-fetcher/constants.py rename to source-code/chapter-3/python-app/constants.py index 61174bd..092cde6 100644 --- a/source-code/chapter-4/exercise-3/docker-subreddit-fetcher/constants.py +++ b/source-code/chapter-3/python-app/constants.py @@ -1,13 +1,15 @@ __author__ = 'Sathyajith' from os import environ +from sys import exit ERR_NO_SOURCE = 'No sources defined! Set a source using /source list, of, sub, reddits' skip_list = [] sources_dict = {} +UPDATE_PERIOD = 1 +FALSE_RESPONSE = {"ok": False} BOT_KEY = environ.get('NBT_ACCESS_TOKEN') -REDDIT_CLIENT_ID = environ.get('REDDIT_CLIENT_ID') -REDDIT_CLIENT_SECRET = environ.get('REDDIT_CLIENT_SECRET') -API_BASE = 'https://api.telegram.org/bot' -UPDATE_PERIOD = 6 -FALSE_RESPONSE = {"ok": False} +if not BOT_KEY: + print("Telegram access token not set, exiting.") + exit(1) +API_BASE = f'https://api.telegram.org/bot{BOT_KEY}' \ No newline at end of file diff --git a/source-code/chapter-3/python-app/last_updated.txt b/source-code/chapter-3/python-app/last_updated.txt new file mode 100644 index 0000000..53ff476 --- /dev/null +++ b/source-code/chapter-3/python-app/last_updated.txt @@ -0,0 +1 @@ +232420721 \ No newline at end of file diff --git a/source-code/chapter-4/exercise-3/docker-subreddit-fetcher/newsbot.py b/source-code/chapter-3/python-app/newsbot.py similarity index 80% rename from source-code/chapter-4/exercise-3/docker-subreddit-fetcher/newsbot.py rename to source-code/chapter-3/python-app/newsbot.py index 55216e6..e960170 100644 --- a/source-code/chapter-4/exercise-3/docker-subreddit-fetcher/newsbot.py +++ b/source-code/chapter-3/python-app/newsbot.py @@ -12,15 +12,15 @@ def get_last_updated(): f.close() except FileNotFoundError: last_updated = 0 - log.debug('Last updated id: {0}'.format(last_updated)) + log.debug(f"Last updated id: {last_updated}") return last_updated if __name__ == '__main__': try: - log.debug('Starting up') + log.info("Starting up") States.last_updated = get_last_updated() while True: handle_incoming_messages(States.last_updated) except KeyboardInterrupt: - log.info('Received KeybInterrupt, exiting') \ No newline at end of file + log.info("Received KeybInterrupt, exiting") \ No newline at end of file diff --git a/source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/reddit.py b/source-code/chapter-3/python-app/reddit.py similarity index 54% rename from source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/reddit.py rename to source-code/chapter-3/python-app/reddit.py index 31f8dff..0533c49 100644 --- a/source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/reddit.py +++ b/source-code/chapter-3/python-app/reddit.py @@ -4,32 +4,26 @@ __author__ = 'Sathyajith' -def summarize(url): - log.info('Not yet implemented!') - return url - - def get_latest_news(sub_reddits): log.debug('Fetching news from reddit') - r = praw.Reddit(user_agent='SubReddit Newsfetcher Bot', - client_id='ralalsYuEJXKDg', - client_secret="16DD-6O7VVaYVMlkUPZWLhdluhU") - r.read_only = True - + r = praw.Reddit(user_agent='Practical Docker With Python tutorial') # Can change the subreddit or add more. sub_reddits = clean_up_subreddits(sub_reddits) - log.info('Fetching subreddits: {0}'.format(sub_reddits)) - submissions = r.subreddit(sub_reddits).hot(limit=5) + log.debug(f"Fetching subreddits: {sub_reddits}") + submissions = r.get_subreddit(sub_reddits).get_top(limit=5) submission_content = '' try: for post in submissions: - submission_content += summarize(post.title + ' - ' + post.url) + '\n\n' + submission_content += f"{post.title} - {post.url} \n\n" except praw.errors.Forbidden: - log.debug('subreddit {0} is private'.format(sub_reddits)) + log.info(f"subreddit {sub_reddits} is private".format()) submission_content = "Sorry couldn't fetch; subreddit is private" except praw.errors.InvalidSubreddit: - log.debug('Subreddit {} is invalid or doesn''t exist.'.format(sub_reddits)) + log.info(f"Subreddit {sub_reddits} is invalid or doesn''t exist.") submission_content = "Sorry couldn't fetch; subreddit doesn't seem to exist" + except praw.errors.NotFound : + log.info(f"Subreddit {sub_reddits} is invalid or doesn''t exist.") + submission_content = "Sorry couldn't fetch; something went wrong, please do send a report to @sathyabhat" return submission_content diff --git a/source-code/chapter-3/python-app/requirements.txt b/source-code/chapter-3/python-app/requirements.txt new file mode 100644 index 0000000..11deb7e --- /dev/null +++ b/source-code/chapter-3/python-app/requirements.txt @@ -0,0 +1,2 @@ +praw==3.6.0 +requests==2.20.0 diff --git a/source-code/chapter-4/exercise-3/docker-subreddit-fetcher/states.py b/source-code/chapter-3/python-app/states.py similarity index 100% rename from source-code/chapter-4/exercise-3/docker-subreddit-fetcher/states.py rename to source-code/chapter-3/python-app/states.py diff --git a/source-code/chapter-4/exercise-3/docker-subreddit-fetcher/telegram.py b/source-code/chapter-3/python-app/telegram.py similarity index 76% rename from source-code/chapter-4/exercise-3/docker-subreddit-fetcher/telegram.py rename to source-code/chapter-3/python-app/telegram.py index 2991d42..a6931fb 100644 --- a/source-code/chapter-4/exercise-3/docker-subreddit-fetcher/telegram.py +++ b/source-code/chapter-3/python-app/telegram.py @@ -10,9 +10,9 @@ def get_updates(last_updated): - log.debug('Checking for requests, last updated passed is: {}'.format(last_updated)) + log.info('Checking for requests, last updated passed is: {last_updated}') sleep(UPDATE_PERIOD) - response = requests.get(API_BASE + BOT_KEY + '/getUpdates', params={'offset': last_updated+1}) + response = requests.get(f"{API_BASE}/getUpdates", params={'offset': last_updated+1}) json_response = FALSE_RESPONSE if response.status_code != 200: # wait for a bit, try again @@ -23,14 +23,14 @@ def get_updates(last_updated): except ValueError: sleep(UPDATE_PERIOD*20) get_updates(last_updated) - log.info('received response: {}'.format(json_response)) + log.info(f"received response: {json_response}") return json_response def post_message(chat_id, text): - log.info('posting {} to {}'.format(text, chat_id)) + log.debug(f"posting {text} to {chat_id}") payload = {'chat_id': chat_id, 'text': text} - requests.post(API_BASE + BOT_KEY + '/sendMessage', data=payload) + requests.post(f"{API_BASE}/sendMessage", data=payload) def handle_incoming_messages(last_updated): @@ -38,7 +38,10 @@ def handle_incoming_messages(last_updated): split_chat_text = [] if r['ok']: for req in r['result']: - chat_sender_id = req['message']['chat']['id'] + if 'message' in req: + chat_sender_id = req['message']['chat']['id'] + else: + chat_sender_id = req['edited_message']['chat']['id'] try: chat_text = req['message']['text'] split_chat_text = chat_text.split() @@ -46,23 +49,23 @@ def handle_incoming_messages(last_updated): chat_text = '' split_chat_text.append(chat_text) log.debug('Looks like no chat text was detected... moving on') - try: + + if 'message' in req: person_id = req['message']['from']['id'] - except KeyError: - pass + else: + person_id = req['edited_message']['from']['id'] - log.info('Chat text received: {0}'.format(chat_text)) + log.info(f"Chat text received: {chat_text}") r = re.search('(source+)(.*)', chat_text) if (r is not None and r.group(1) == 'source'): if r.group(2): sources_dict[person_id] = r.group(2) - log.debug('Sources set for {0} to {1}'.format(sources_dict[person_id], r.group(2))) - post_message(person_id, 'Sources set as {0}!'.format(r.group(2))) + post_message(person_id, f"Sources set as {r.group(2)}!") else: post_message(person_id, 'We need a comma separated list of subreddits! No subreddit, no news :-(') if chat_text == '/stop': - log.debug('Added {0} to skip list'.format(chat_sender_id)) + log.debug(f"Added {chat_sender_id} to skip list") skip_list.append(chat_sender_id) post_message(chat_sender_id, "Ok, we won't send you any more messages.") @@ -89,5 +92,5 @@ def handle_incoming_messages(last_updated): f.write(str(last_updated)) States.last_updated = last_updated log.debug( - 'Updated last_updated to {0}'.format(last_updated)) + f'Updated last_updated to {last_updated}') f.close() diff --git a/source-code/chapter-4/exercise-1/README.md b/source-code/chapter-4/exercise-1/README.md index 95dac57..4957d1a 100644 --- a/source-code/chapter-4/exercise-1/README.md +++ b/source-code/chapter-4/exercise-1/README.md @@ -1,3 +1,3 @@ ### README -This exercise contains the source code for the first exercise of chapter 4. At the start of the chapter, we introduced a simple Dockerfile that did not build due to syntax errors. Here, you’ll fix the Dockerfile and add some of the instructions that you learned about in this chapter. +This directory contains the source code for the first exercise of chapter 4. At the start of the chapter, we introduced a simple Dockerfile that did not build due to syntax errors. Here, you’ll fix the Dockerfile and add some of the instructions that you learned about in this chapter. diff --git a/source-code/chapter-4/exercise-1/docker-hello-world/Dockerfile b/source-code/chapter-4/exercise-1/docker-hello-world/Dockerfile index 7c1d5ab..620cf40 100644 --- a/source-code/chapter-4/exercise-1/docker-hello-world/Dockerfile +++ b/source-code/chapter-4/exercise-1/docker-hello-world/Dockerfile @@ -1,9 +1,8 @@ FROM python:3-alpine -LABEL author="sathyabhat" LABEL description="Dockerfile for Python script which prints Hello, Name" COPY hello-world.py /app/ -ENV NAME=Sathya +ENV NAME=Readers CMD python3 /app/hello-world.py diff --git a/source-code/chapter-4/exercise-1/docker-hello-world/hello-world.py b/source-code/chapter-4/exercise-1/docker-hello-world/hello-world.py index 850eed0..49e450d 100644 --- a/source-code/chapter-4/exercise-1/docker-hello-world/hello-world.py +++ b/source-code/chapter-4/exercise-1/docker-hello-world/hello-world.py @@ -1,10 +1,8 @@ #!/usr/bin/env python3 - from os import getenv if getenv('NAME') is None: name = 'World' else: name = getenv('NAME') - -print("Hello, {}!".format(name)) +print(f"Hello, {name}!") diff --git a/source-code/chapter-4/exercise-2/README.md b/source-code/chapter-4/exercise-2/README.md index 719d4d5..05da1fc 100644 --- a/source-code/chapter-4/exercise-2/README.md +++ b/source-code/chapter-4/exercise-2/README.md @@ -1,3 +1,6 @@ ### README -This exercise contains the source code for the second exercise of chapter 4. In this exercise, you will build two Docker images, the first one using a standard build process using python:3 as the base image. +This directory contains the source code for the second exercise of chapter 4. In this exercise, you will build two Docker images + + - Using the standard build process using python:3 as the base image (present in [docker-multi-stage/standard-build](docker-multi-stage/standard-build) directory. + - Using Multi-Stage builds (present [docker-multi-stage/multistage-build](docker-multi-stage/multistage-build) directory. diff --git a/source-code/chapter-4/exercise-2/docker-multi-stage/multistage-build/requirements.txt b/source-code/chapter-4/exercise-2/docker-multi-stage/multistage-build/requirements.txt index 6c81bc0..ffc6560 100644 --- a/source-code/chapter-4/exercise-2/docker-multi-stage/multistage-build/requirements.txt +++ b/source-code/chapter-4/exercise-2/docker-multi-stage/multistage-build/requirements.txt @@ -1 +1 @@ -praw +praw==3.6.0 \ No newline at end of file diff --git a/source-code/chapter-4/exercise-2/docker-multi-stage/standard-build/Dockerfile b/source-code/chapter-4/exercise-2/docker-multi-stage/standard-build/Dockerfile index 602dc00..b084ae9 100644 --- a/source-code/chapter-4/exercise-2/docker-multi-stage/standard-build/Dockerfile +++ b/source-code/chapter-4/exercise-2/docker-multi-stage/standard-build/Dockerfile @@ -1,4 +1,3 @@ FROM python:3 COPY requirements.txt . RUN pip install -r requirements.txt - diff --git a/source-code/chapter-4/exercise-2/docker-multi-stage/standard-build/requirements.txt b/source-code/chapter-4/exercise-2/docker-multi-stage/standard-build/requirements.txt index 6c81bc0..ffc6560 100644 --- a/source-code/chapter-4/exercise-2/docker-multi-stage/standard-build/requirements.txt +++ b/source-code/chapter-4/exercise-2/docker-multi-stage/standard-build/requirements.txt @@ -1 +1 @@ -praw +praw==3.6.0 \ No newline at end of file diff --git a/source-code/chapter-4/exercise-3/README.md b/source-code/chapter-4/exercise-3/README.md index 6c776ec..295e28e 100644 --- a/source-code/chapter-4/exercise-3/README.md +++ b/source-code/chapter-4/exercise-3/README.md @@ -1,3 +1,20 @@ ### README -This exercise contains the source code for the third exercise of chapter 4. In this exercise, we’ll try writing the Dockerfile for the project. +This directory contains the source code for the third exercise of chapter 4. In this exercise, we compose a Dockerfile for Newsbot and then use the Dockerfile to build a Docker image and run the container. + + +### Building the Docker image + +Build the image using the below command + +``` +docker build -t sathyabhat/newsbot +``` + +Run the container using + +``` +docker run -e NBT_ACCESS_TOKEN= sathyabhat/newsbot +``` + +Replace `` with the Telegram API Token that was generated. \ No newline at end of file diff --git a/source-code/chapter-4/exercise-3/docker-subreddit-fetcher/main.py b/source-code/chapter-4/exercise-3/docker-subreddit-fetcher/main.py deleted file mode 100644 index bacc165..0000000 --- a/source-code/chapter-4/exercise-3/docker-subreddit-fetcher/main.py +++ /dev/null @@ -1,22 +0,0 @@ -from flask import Flask -from newsbot import * -from states import States - -bot = Flask(__name__) - - -@bot.route('/index') -def index(): - return 'Thou shalt not pass!' - - -@bot.route('/telegram-update', methods=['POST']) -def telegram_update(): - handle_incoming_messages(States.last_updated_id) - - -if __name__ == '__main__': - States.last_updated_id = get_last_updated() - bot.run() - - diff --git a/source-code/chapter-4/exercise-3/docker-subreddit-fetcher/reddit.py b/source-code/chapter-4/exercise-3/docker-subreddit-fetcher/reddit.py deleted file mode 100644 index 2ccb2c8..0000000 --- a/source-code/chapter-4/exercise-3/docker-subreddit-fetcher/reddit.py +++ /dev/null @@ -1,48 +0,0 @@ -import praw -from states import log -from constants import REDDIT_CLIENT_ID, REDDIT_CLIENT_SECRET - -__author__ = 'Sathyajith' - - -def summarize(url): - log.info('Not yet implemented!') - return url - - -def get_latest_news(sub_reddits): - log.debug('Fetching news from reddit') - no_tokens_message = ( - "Reddit client id is not set, please create a client id as mentioned in" - " https://github.com/reddit-archive/reddit/wiki/OAuth2-Quick-Start-Example#first-steps" - " and set the environment variable `{}` similar to how `NBT_ACCESS_TOKEN` was done " - ) - - if REDDIT_CLIENT_ID is None: - return no_tokens_message.format("REDDIT_CLIENT_ID") - if REDDIT_CLIENT_SECRET is None: - return no_tokens_message.format("REDDIT_CLIENT_SECRET") - r = praw.Reddit(user_agent='SubReddit Newsfetcher Bot', client_id=REDDIT_CLIENT_ID, client_secret=REDDIT_CLIENT_SECRET) - # Can change the subreddit or add more. - sub_reddits = clean_up_subreddits(sub_reddits) - log.debug('Fetching subreddits: {0}'.format(sub_reddits)) - submissions = r.subreddit(sub_reddits).hot(limit=5) - submission_content = '' - try: - for post in submissions: - submission_content += summarize(post.title + ' - ' + post.url) + '\n' - except praw.errors.Forbidden: - log.debug('subreddit {0} is private'.format(sub_reddits)) - submission_content = "Sorry couldn't fetch; subreddit is private" - except praw.errors.InvalidSubreddit: - log.debug('Subreddit {} is invalid or doesn''t exist.'.format(sub_reddits)) - submission_content = "Sorry couldn't fetch; subreddit doesn't seem to exist" - except praw.errors.NotFound : - log.debug('Subreddit {} is invalid or doesn''t exist.'.format(sub_reddits)) - submission_content = "Sorry couldn't fetch; something went wrong, please do send a report to @sathyabhat" - return submission_content - - -def clean_up_subreddits(sub_reddits): - log.debug('Got subreddits to clean: {0}'.format(sub_reddits)) - return sub_reddits.strip().replace(" ", "").replace(',', '+') diff --git a/source-code/chapter-4/exercise-3/docker-subreddit-fetcher/requirements.txt b/source-code/chapter-4/exercise-3/docker-subreddit-fetcher/requirements.txt deleted file mode 100644 index 6c81bc0..0000000 --- a/source-code/chapter-4/exercise-3/docker-subreddit-fetcher/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -praw diff --git a/source-code/chapter-4/exercise-3/docker-subreddit-fetcher/Dockerfile b/source-code/chapter-4/exercise-3/newsbot/Dockerfile similarity index 55% rename from source-code/chapter-4/exercise-3/docker-subreddit-fetcher/Dockerfile rename to source-code/chapter-4/exercise-3/newsbot/Dockerfile index f3c594c..b0f0486 100644 --- a/source-code/chapter-4/exercise-3/docker-subreddit-fetcher/Dockerfile +++ b/source-code/chapter-4/exercise-3/newsbot/Dockerfile @@ -1,9 +1,5 @@ FROM python:3-alpine - -COPY * /apps/subredditfetcher/ WORKDIR /apps/subredditfetcher/ +COPY . . RUN ["pip", "install", "-r", "requirements.txt"] - -ENV NBT_ACCESS_TOKEN="495637361:AAHIhiDTX1UeX17KJy0-FsMZEqEtCFYfcP8" - CMD ["python", "newsbot.py"] diff --git a/source-code/chapter-4/exercise-3/docker-subreddit-fetcher/LICENSE b/source-code/chapter-4/exercise-3/newsbot/LICENSE similarity index 100% rename from source-code/chapter-4/exercise-3/docker-subreddit-fetcher/LICENSE rename to source-code/chapter-4/exercise-3/newsbot/LICENSE diff --git a/source-code/chapter-4/exercise-3/newsbot/constants.py b/source-code/chapter-4/exercise-3/newsbot/constants.py new file mode 100644 index 0000000..092cde6 --- /dev/null +++ b/source-code/chapter-4/exercise-3/newsbot/constants.py @@ -0,0 +1,15 @@ +__author__ = 'Sathyajith' + +from os import environ +from sys import exit +ERR_NO_SOURCE = 'No sources defined! Set a source using /source list, of, sub, reddits' +skip_list = [] +sources_dict = {} +UPDATE_PERIOD = 1 +FALSE_RESPONSE = {"ok": False} + +BOT_KEY = environ.get('NBT_ACCESS_TOKEN') +if not BOT_KEY: + print("Telegram access token not set, exiting.") + exit(1) +API_BASE = f'https://api.telegram.org/bot{BOT_KEY}' \ No newline at end of file diff --git a/source-code/chapter-4/exercise-3/newsbot/newsbot.py b/source-code/chapter-4/exercise-3/newsbot/newsbot.py new file mode 100644 index 0000000..e960170 --- /dev/null +++ b/source-code/chapter-4/exercise-3/newsbot/newsbot.py @@ -0,0 +1,26 @@ +from states import States, log +from telegram import handle_incoming_messages + + +def get_last_updated(): + try: + with open('last_updated.txt', 'r') as f: + try: + last_updated = int(f.read()) + except ValueError: + last_updated = 0 + f.close() + except FileNotFoundError: + last_updated = 0 + log.debug(f"Last updated id: {last_updated}") + return last_updated + +if __name__ == '__main__': + + try: + log.info("Starting up") + States.last_updated = get_last_updated() + while True: + handle_incoming_messages(States.last_updated) + except KeyboardInterrupt: + log.info("Received KeybInterrupt, exiting") \ No newline at end of file diff --git a/source-code/chapter-4/exercise-3/newsbot/reddit.py b/source-code/chapter-4/exercise-3/newsbot/reddit.py new file mode 100644 index 0000000..0533c49 --- /dev/null +++ b/source-code/chapter-4/exercise-3/newsbot/reddit.py @@ -0,0 +1,32 @@ +import praw +from states import log + +__author__ = 'Sathyajith' + + +def get_latest_news(sub_reddits): + log.debug('Fetching news from reddit') + r = praw.Reddit(user_agent='Practical Docker With Python tutorial') + # Can change the subreddit or add more. + sub_reddits = clean_up_subreddits(sub_reddits) + log.debug(f"Fetching subreddits: {sub_reddits}") + submissions = r.get_subreddit(sub_reddits).get_top(limit=5) + submission_content = '' + try: + for post in submissions: + submission_content += f"{post.title} - {post.url} \n\n" + except praw.errors.Forbidden: + log.info(f"subreddit {sub_reddits} is private".format()) + submission_content = "Sorry couldn't fetch; subreddit is private" + except praw.errors.InvalidSubreddit: + log.info(f"Subreddit {sub_reddits} is invalid or doesn''t exist.") + submission_content = "Sorry couldn't fetch; subreddit doesn't seem to exist" + except praw.errors.NotFound : + log.info(f"Subreddit {sub_reddits} is invalid or doesn''t exist.") + submission_content = "Sorry couldn't fetch; something went wrong, please do send a report to @sathyabhat" + return submission_content + + +def clean_up_subreddits(sub_reddits): + log.debug('Got subreddits to clean: {0}'.format(sub_reddits)) + return sub_reddits.strip().replace(" ", "").replace(',', '+') diff --git a/source-code/chapter-4/exercise-3/newsbot/requirements.txt b/source-code/chapter-4/exercise-3/newsbot/requirements.txt new file mode 100644 index 0000000..11deb7e --- /dev/null +++ b/source-code/chapter-4/exercise-3/newsbot/requirements.txt @@ -0,0 +1,2 @@ +praw==3.6.0 +requests==2.20.0 diff --git a/source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/states.py b/source-code/chapter-4/exercise-3/newsbot/states.py similarity index 100% rename from source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/states.py rename to source-code/chapter-4/exercise-3/newsbot/states.py diff --git a/source-code/chapter-7/exercise-1/docker-compose-adminer/telegram.py b/source-code/chapter-4/exercise-3/newsbot/telegram.py similarity index 63% rename from source-code/chapter-7/exercise-1/docker-compose-adminer/telegram.py rename to source-code/chapter-4/exercise-3/newsbot/telegram.py index 5e150c7..f7fb8e5 100644 --- a/source-code/chapter-7/exercise-1/docker-compose-adminer/telegram.py +++ b/source-code/chapter-4/exercise-3/newsbot/telegram.py @@ -5,16 +5,14 @@ from states import States, log from constants import * from reddit import get_latest_news -from models import * __author__ = 'Sathyajith' -db = MySQLDatabase(host="mysql", port=3306, user="root", password="dontusethisinprod", database="newsbot") def get_updates(last_updated): - log.debug('Checking for requests, last updated passed is: {}'.format(last_updated)) + log.debug('Checking for requests, last updated passed is: {last_updated}') sleep(UPDATE_PERIOD) - response = requests.get(API_BASE + BOT_KEY + '/getUpdates', params={'offset': last_updated+1}) + response = requests.get(f"{API_BASE}/getUpdates", params={'offset': last_updated+1}) json_response = FALSE_RESPONSE if response.status_code != 200: # wait for a bit, try again @@ -25,14 +23,14 @@ def get_updates(last_updated): except ValueError: sleep(UPDATE_PERIOD*20) get_updates(last_updated) - log.info('received response: {}'.format(json_response)) + log.info(f"received response: {json_response}") return json_response def post_message(chat_id, text): - log.info('posting {} to {}'.format(text, chat_id)) + log.debug(f"posting {text} to {chat_id}") payload = {'chat_id': chat_id, 'text': text} - requests.post(API_BASE + BOT_KEY + '/sendMessage', data=payload) + requests.post(f"{API_BASE}/sendMessage", data=payload) def handle_incoming_messages(last_updated): @@ -40,7 +38,10 @@ def handle_incoming_messages(last_updated): split_chat_text = [] if r['ok']: for req in r['result']: - chat_sender_id = req['message']['chat']['id'] + if 'message' in req: + chat_sender_id = req['message']['chat']['id'] + else: + chat_sender_id = req['edited_message']['chat']['id'] try: chat_text = req['message']['text'] split_chat_text = chat_text.split() @@ -48,30 +49,23 @@ def handle_incoming_messages(last_updated): chat_text = '' split_chat_text.append(chat_text) log.debug('Looks like no chat text was detected... moving on') - try: + + if 'message' in req: person_id = req['message']['from']['id'] - except KeyError: - pass + else: + person_id = req['edited_message']['from']['id'] - log.info('Chat text received: {0}'.format(chat_text)) + log.info(f"Chat text received: {chat_text}") r = re.search('(source+)(.*)', chat_text) if (r is not None and r.group(1) == 'source'): if r.group(2): sources_dict[person_id] = r.group(2) - log.info('Sources set for {0} to {1}'.format(person_id, sources_dict[person_id])) - try: - sources = Source.create(person_id = person_id, fetch_from = sources_dict[person_id]) - except IntegrityError: - sources = Source.get(person_id = person_id) - sources.fetch_from = sources_dict[person_id] - sources.save() - log.info(sources.person_id) - post_message(person_id, 'Sources set as {0}!'.format(r.group(2))) + post_message(person_id, f"Sources set as {r.group(2)}!") else: post_message(person_id, 'We need a comma separated list of subreddits! No subreddit, no news :-(') if chat_text == '/stop': - log.debug('Added {0} to skip list'.format(chat_sender_id)) + log.debug(f"Added {chat_sender_id} to skip list") skip_list.append(chat_sender_id) post_message(chat_sender_id, "Ok, we won't send you any more messages.") @@ -86,18 +80,17 @@ def handle_incoming_messages(last_updated): if split_chat_text[0] == '/fetch' and (person_id not in skip_list): post_message(person_id, 'Hang on, fetching your news..') - try: - sub_reddits = Source.get(person_id = person_id).fetch_from.strip() - summarized_news = get_latest_news(sub_reddits) - post_message(person_id, summarized_news) - except: + sub_reddits = sources_dict[person_id] + except KeyError: post_message(person_id, ERR_NO_SOURCE) - + else: + summarized_news = get_latest_news(sources_dict[person_id]) + post_message(person_id, summarized_news) last_updated = req['update_id'] with open('last_updated.txt', 'w') as f: f.write(str(last_updated)) States.last_updated = last_updated log.debug( - 'Updated last_updated to {0}'.format(last_updated)) + f'Updated last_updated to {last_updated}') f.close() diff --git a/source-code/chapter-5/exercise-1/README.md b/source-code/chapter-5/exercise-1/README.md index ab610e7..9bc786e 100644 --- a/source-code/chapter-5/exercise-1/README.md +++ b/source-code/chapter-5/exercise-1/README.md @@ -1,3 +1,3 @@ ### README -This exercise contains the source code for the first exercise of chapter 5. In this exercise, we build an nginx Docker image with a Docker volume attached, which contains a custom nginx configuration. Toward the second part of the exercise, we will attach a bind mount and a volume containing a static web page and a custom nginx configuration. The intent of the exercise is help the readers understand how to leverage volumes and bind mounts to make local development easy. +This directory contains the source code for the first exercise of chapter 5. In this exercise, we build an nginx Docker image with a Docker volume attached, which contains a custom nginx configuration. Toward the second part of the exercise, we will attach a bind mount and a volume containing a static web page and a custom nginx configuration. The intent of the exercise is help the readers understand how to leverage volumes and bind mounts to make local development easy. diff --git a/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/Dockerfile b/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/Dockerfile deleted file mode 100644 index 719519f..0000000 --- a/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM python:3-alpine - -RUN apk add gcc musl-dev python3-dev libffi-dev openssl-dev - -COPY * /apps/subredditfetcher/ -WORKDIR /apps/subredditfetcher/ - -RUN ["mkdir", "/apps/subredditfetcher/data/"] -RUN ["pip", "install", "-r", "requirements.txt"] -RUN ["python", "one_time.py"] - -VOLUME [ "/apps/subredditfetcher/data" ] -ENV NBT_ACCESS_TOKEN="495637361:AAHIhiDTX1UeX17KJy0-FsMZEqEtCFYfcP8" - -CMD ["python", "newsbot.py"] diff --git a/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/README.md b/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/README.md deleted file mode 100644 index 0aea875..0000000 --- a/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/README.md +++ /dev/null @@ -1,9 +0,0 @@ -[![Stories in Ready](https://badge.waffle.io/SathyaBhat/themnewsbot.png?label=ready&title=Ready)](https://waffle.io/SathyaBhat/themnewsbot) -# themnewsbot -News bot for telegram : https://web.telegram.org/#/im?p=@ThemNewsBot
- -Bot posts top submissions from a subreddit - -ToDo : - -- Check our [waffle board](https://waffle.io/SathyaBhat/themnewsbot) diff --git a/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/constants.py b/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/constants.py deleted file mode 100644 index cb189fe..0000000 --- a/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/constants.py +++ /dev/null @@ -1,11 +0,0 @@ -__author__ = 'Sathyajith' - -import os -ERR_NO_SOURCE = 'No sources defined! Set a source using /source list, of, sub, reddits' -skip_list = [] -sources_dict = {} - -BOT_KEY = os.environ['NBT_ACCESS_TOKEN'] -API_BASE = 'https://api.telegram.org/bot' -UPDATE_PERIOD = 6 -FALSE_RESPONSE = {"ok": False} \ No newline at end of file diff --git a/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/main.py b/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/main.py deleted file mode 100644 index bacc165..0000000 --- a/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/main.py +++ /dev/null @@ -1,22 +0,0 @@ -from flask import Flask -from newsbot import * -from states import States - -bot = Flask(__name__) - - -@bot.route('/index') -def index(): - return 'Thou shalt not pass!' - - -@bot.route('/telegram-update', methods=['POST']) -def telegram_update(): - handle_incoming_messages(States.last_updated_id) - - -if __name__ == '__main__': - States.last_updated_id = get_last_updated() - bot.run() - - diff --git a/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/newsbot b/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/newsbot deleted file mode 100644 index e69de29..0000000 diff --git a/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/one_time.py b/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/one_time.py deleted file mode 100644 index f7074d9..0000000 --- a/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/one_time.py +++ /dev/null @@ -1,11 +0,0 @@ - -from models import * - - -def create_tables(): - db.connect() - db.create_tables([Source, Request, Message], True) - db.close() - -if __name__ == '__main__': - create_tables() diff --git a/source-code/chapter-5/exercise-2/newsbot/Dockerfile b/source-code/chapter-5/exercise-2/newsbot/Dockerfile new file mode 100644 index 0000000..7590f98 --- /dev/null +++ b/source-code/chapter-5/exercise-2/newsbot/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3-alpine + +RUN apk add gcc musl-dev python3-dev libffi-dev openssl-dev +WORKDIR /apps/subredditfetcher/ +COPY . . + +RUN pip install -r requirements.txt +VOLUME ["/data"] +CMD ["python", "newsbot.py"] diff --git a/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/LICENSE b/source-code/chapter-5/exercise-2/newsbot/LICENSE similarity index 100% rename from source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/LICENSE rename to source-code/chapter-5/exercise-2/newsbot/LICENSE diff --git a/source-code/chapter-5/exercise-2/newsbot/README.md b/source-code/chapter-5/exercise-2/newsbot/README.md new file mode 100644 index 0000000..4498390 --- /dev/null +++ b/source-code/chapter-5/exercise-2/newsbot/README.md @@ -0,0 +1,21 @@ +# themnewsbot + +Telegram bot which fetches Bot posts top submissions from a subreddit. NewsBot is used to demo a short, simple Python project in my book, [Practical Docker With Python](https://www.apress.com/gp/book/9781484237830). + +Refer to the [book repo](https://github.com/apress/practical-docker-with-python) for other exercises using this code. + +### Getting started + +- Clone the repo or download the code +- Install the requirements with pip + + pip3 install -r requirements.txt + +- Set the environment variable `NBT_ACCESS_TOKEN` where the value is the Bot token generated using Telegram BotFather. + - See instructions on how to generate the token in Chapter 3 or [refer to this guide](https://core.telegram.org/bots/api#authorizing-your-bot) + - See this guide on [how to set environment variables](https://core.telegram.org/bots/api#authorizing-your-bot) +- Start the bot + python newsbot.py + +where `` is the [Telegram Bot API](https://core.telegram.org/bots/api) token + diff --git a/source-code/chapter-5/exercise-2/newsbot/constants.py b/source-code/chapter-5/exercise-2/newsbot/constants.py new file mode 100644 index 0000000..092cde6 --- /dev/null +++ b/source-code/chapter-5/exercise-2/newsbot/constants.py @@ -0,0 +1,15 @@ +__author__ = 'Sathyajith' + +from os import environ +from sys import exit +ERR_NO_SOURCE = 'No sources defined! Set a source using /source list, of, sub, reddits' +skip_list = [] +sources_dict = {} +UPDATE_PERIOD = 1 +FALSE_RESPONSE = {"ok": False} + +BOT_KEY = environ.get('NBT_ACCESS_TOKEN') +if not BOT_KEY: + print("Telegram access token not set, exiting.") + exit(1) +API_BASE = f'https://api.telegram.org/bot{BOT_KEY}' \ No newline at end of file diff --git a/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/data/newsbot.db b/source-code/chapter-5/exercise-2/newsbot/data/newsbot.db similarity index 100% rename from source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/data/newsbot.db rename to source-code/chapter-5/exercise-2/newsbot/data/newsbot.db diff --git a/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/last_updated.txt b/source-code/chapter-5/exercise-2/newsbot/last_updated.txt similarity index 100% rename from source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/last_updated.txt rename to source-code/chapter-5/exercise-2/newsbot/last_updated.txt diff --git a/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/models.py b/source-code/chapter-5/exercise-2/newsbot/models.py similarity index 100% rename from source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/models.py rename to source-code/chapter-5/exercise-2/newsbot/models.py diff --git a/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/newsbot.py b/source-code/chapter-5/exercise-2/newsbot/newsbot.py similarity index 81% rename from source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/newsbot.py rename to source-code/chapter-5/exercise-2/newsbot/newsbot.py index 1314864..09ba5b0 100644 --- a/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/newsbot.py +++ b/source-code/chapter-5/exercise-2/newsbot/newsbot.py @@ -1,6 +1,6 @@ from states import States, log from telegram import handle_incoming_messages - +from one_time import create_tables def get_last_updated(): try: @@ -12,13 +12,14 @@ def get_last_updated(): f.close() except FileNotFoundError: last_updated = 0 - log.debug('Last updated id: {0}'.format(last_updated)) + log.debug(f'Last updated id: {last_updated}') return last_updated if __name__ == '__main__': try: - log.debug('Starting up') + log.info('Starting newsbot') + create_tables() States.last_updated = get_last_updated() while True: handle_incoming_messages(States.last_updated) diff --git a/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/one_time.py b/source-code/chapter-5/exercise-2/newsbot/one_time.py similarity index 72% rename from source-code/chapter-7/exercise-2/subreddit-fetcher-compose/one_time.py rename to source-code/chapter-5/exercise-2/newsbot/one_time.py index f7074d9..acc9a43 100644 --- a/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/one_time.py +++ b/source-code/chapter-5/exercise-2/newsbot/one_time.py @@ -1,11 +1,7 @@ from models import * - def create_tables(): db.connect() db.create_tables([Source, Request, Message], True) db.close() - -if __name__ == '__main__': - create_tables() diff --git a/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/reddit.py b/source-code/chapter-5/exercise-2/newsbot/reddit.py similarity index 63% rename from source-code/chapter-7/exercise-2/subreddit-fetcher-compose/reddit.py rename to source-code/chapter-5/exercise-2/newsbot/reddit.py index 31f8dff..7f065eb 100644 --- a/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/reddit.py +++ b/source-code/chapter-5/exercise-2/newsbot/reddit.py @@ -4,35 +4,30 @@ __author__ = 'Sathyajith' -def summarize(url): - log.info('Not yet implemented!') - return url - - def get_latest_news(sub_reddits): log.debug('Fetching news from reddit') - r = praw.Reddit(user_agent='SubReddit Newsfetcher Bot', + r = praw.Reddit(user_agent='NewsBot', client_id='ralalsYuEJXKDg', client_secret="16DD-6O7VVaYVMlkUPZWLhdluhU") r.read_only = True # Can change the subreddit or add more. sub_reddits = clean_up_subreddits(sub_reddits) - log.info('Fetching subreddits: {0}'.format(sub_reddits)) + log.info('Fetching subreddits: {sub_reddits}') submissions = r.subreddit(sub_reddits).hot(limit=5) submission_content = '' try: for post in submissions: - submission_content += summarize(post.title + ' - ' + post.url) + '\n\n' + submission_content += post.title + ' - ' + post.url + '\n\n' except praw.errors.Forbidden: - log.debug('subreddit {0} is private'.format(sub_reddits)) + log.debug(f'subreddit {sub_reddits} is private') submission_content = "Sorry couldn't fetch; subreddit is private" except praw.errors.InvalidSubreddit: - log.debug('Subreddit {} is invalid or doesn''t exist.'.format(sub_reddits)) + log.debug(f'Subreddit {sub_reddits} is invalid or doesn''t exist') submission_content = "Sorry couldn't fetch; subreddit doesn't seem to exist" return submission_content def clean_up_subreddits(sub_reddits): - log.debug('Got subreddits to clean: {0}'.format(sub_reddits)) + log.debug(f'Got subreddits to clean: {sub_reddits}') return sub_reddits.strip().replace(" ", "").replace(',', '+') diff --git a/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/requirements.txt b/source-code/chapter-5/exercise-2/newsbot/requirements.txt similarity index 57% rename from source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/requirements.txt rename to source-code/chapter-5/exercise-2/newsbot/requirements.txt index 7a49e7e..a78486a 100644 --- a/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/requirements.txt +++ b/source-code/chapter-5/exercise-2/newsbot/requirements.txt @@ -1,2 +1,2 @@ -praw peewee==2.10.2 +praw==7.4.0 \ No newline at end of file diff --git a/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/states.py b/source-code/chapter-5/exercise-2/newsbot/states.py similarity index 81% rename from source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/states.py rename to source-code/chapter-5/exercise-2/newsbot/states.py index ff7daaa..09f3904 100644 --- a/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/states.py +++ b/source-code/chapter-5/exercise-2/newsbot/states.py @@ -4,7 +4,6 @@ class States(object): last_updated_id = '' -logging.basicConfig(level=logging.DEBUG, +logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(asctime)s - %(funcName)s - %(message)s') - log = logging.getLogger('nbt') \ No newline at end of file diff --git a/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/telegram.py b/source-code/chapter-5/exercise-2/newsbot/telegram.py similarity index 74% rename from source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/telegram.py rename to source-code/chapter-5/exercise-2/newsbot/telegram.py index 766e9e3..7d212fd 100644 --- a/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/telegram.py +++ b/source-code/chapter-5/exercise-2/newsbot/telegram.py @@ -13,9 +13,9 @@ def get_updates(last_updated): - log.debug('Checking for requests, last updated passed is: {}'.format(last_updated)) + log.debug('Checking for requests, last updated passed is: {last_updated}') sleep(UPDATE_PERIOD) - response = requests.get(API_BASE + BOT_KEY + '/getUpdates', params={'offset': last_updated+1}) + response = requests.get(f"{API_BASE}/getUpdates", params={'offset': last_updated+1}) json_response = FALSE_RESPONSE if response.status_code != 200: # wait for a bit, try again @@ -26,14 +26,14 @@ def get_updates(last_updated): except ValueError: sleep(UPDATE_PERIOD*20) get_updates(last_updated) - log.info('received response: {}'.format(json_response)) + log.info(f"received response: {json_response}") return json_response def post_message(chat_id, text): - log.info('posting {} to {}'.format(text, chat_id)) + log.debug(f"posting {text} to {chat_id}") payload = {'chat_id': chat_id, 'text': text} - requests.post(API_BASE + BOT_KEY + '/sendMessage', data=payload) + requests.post(f"{API_BASE}/sendMessage", data=payload) def handle_incoming_messages(last_updated): @@ -41,7 +41,10 @@ def handle_incoming_messages(last_updated): split_chat_text = [] if r['ok']: for req in r['result']: - chat_sender_id = req['message']['chat']['id'] + if 'message' in req: + chat_sender_id = req['message']['chat']['id'] + else: + chat_sender_id = req['edited_message']['chat']['id'] try: chat_text = req['message']['text'] split_chat_text = chat_text.split() @@ -49,33 +52,34 @@ def handle_incoming_messages(last_updated): chat_text = '' split_chat_text.append(chat_text) log.debug('Looks like no chat text was detected... moving on') - try: + + if 'message' in req: person_id = req['message']['from']['id'] - except KeyError: - pass + else: + person_id = req['edited_message']['from']['id'] - log.info('Chat text received: {0}'.format(chat_text)) + log.info(f"Chat text received: {chat_text}") r = re.search('(source+)(.*)', chat_text) if (r is not None and r.group(1) == 'source'): if r.group(2): sources_dict[person_id] = r.group(2) - log.info('Sources set for {0} to {1}'.format(person_id, sources_dict[person_id])) + log.info(f'Sources set for {person_id} to {sources_dict[person_id]}') with db.atomic() as txn: try: sources = Source.create(person_id=person_id, fetch_from=sources_dict[person_id]) - log.info('Inserted row id: {0}'.format(sources.person_id)) + log.debug(f'Inserted row id: {sources.person_id}') except peewee.IntegrityError: sources = Source.update(fetch_from=sources_dict[person_id]).where(person_id == person_id) rows_updated = sources.execute() - log.info('Updated {0} rows, query obj {1}'.format(rows_updated, sources)) + log.info(f'Updated {rows_updated} rows') txn.commit() post_message(person_id, 'Sources set as {0}!'.format(r.group(2))) else: post_message(person_id, 'We need a comma separated list of subreddits! No subreddit, no news :-(') if chat_text == '/stop': - log.debug('Added {0} to skip list'.format(chat_sender_id)) + log.debug(f'Added {chat_sender_id} to skip list') skip_list.append(chat_sender_id) post_message(chat_sender_id, "Ok, we won't send you any more messages.") @@ -83,8 +87,7 @@ def handle_incoming_messages(last_updated): helptext = ''' Hi! This is a News Bot which fetches news from subreddits. Use "/source" to select a subreddit source. Example "/source programming,games" fetches news from r/programming, r/games. - Use "/fetch for the bot to go ahead and fetch the news. At the moment, bot will fetch total of 5 posts from all sub reddits - I will have this configurable soon. + Use "/fetch for the bot to go ahead and fetch the news. At the moment, bot will fetch total of 5 posts from the selected subreddit. ''' post_message(chat_sender_id, helptext) @@ -95,12 +98,12 @@ def handle_incoming_messages(last_updated): summarized_news = get_latest_news(sub_reddits) post_message(person_id, summarized_news) except peewee.DoesNotExist: - post_message(person_id, 'Could not find a saved subreddit, please try setting sources with /source') + post_message(person_id, ERR_NO_SOURCE) last_updated = req['update_id'] with open('last_updated.txt', 'w') as f: f.write(str(last_updated)) States.last_updated = last_updated log.debug( - 'Updated last_updated to {0}'.format(last_updated)) + f'Updated last_updated to {last_updated}') f.close() diff --git a/source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/Dockerfile b/source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/Dockerfile deleted file mode 100644 index b92eb0d..0000000 --- a/source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM python:3-alpine -RUN apk add gcc musl-dev python3-dev libffi-dev openssl-dev -COPY * /apps/subredditfetcher/ -WORKDIR /apps/subredditfetcher/ - -VOLUME [ "/apps/subredditfetcher" ] -RUN ["pip", "install", "-r", "requirements.txt"] - -ENV NBT_ACCESS_TOKEN="495637361:AAHIhiDTX1UeX17KJy0-FsMZEqEtCFYfcP8" - -CMD ["python", "newsbot.py"] diff --git a/source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/constants.py b/source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/constants.py deleted file mode 100644 index cb189fe..0000000 --- a/source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/constants.py +++ /dev/null @@ -1,11 +0,0 @@ -__author__ = 'Sathyajith' - -import os -ERR_NO_SOURCE = 'No sources defined! Set a source using /source list, of, sub, reddits' -skip_list = [] -sources_dict = {} - -BOT_KEY = os.environ['NBT_ACCESS_TOKEN'] -API_BASE = 'https://api.telegram.org/bot' -UPDATE_PERIOD = 6 -FALSE_RESPONSE = {"ok": False} \ No newline at end of file diff --git a/source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/docker-compose.yml b/source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/docker-compose.yml deleted file mode 100644 index 2c0a4e5..0000000 --- a/source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/docker-compose.yml +++ /dev/null @@ -1,23 +0,0 @@ -version: '3' -services: - app: - build: . - depends_on: - - mysql - restart: "on-failure" - volumes: - - "appdata:/apps/subredditfetcher" - mysql: - image: mysql - volumes: - - "dbdata:/var/lib/mysql" - environment: - - MYSQL_ROOT_PASSWORD=dontusethisinprod - adminer: - image: adminer - ports: - - "8080:8080" - -volumes: - dbdata: - appdata: diff --git a/source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/main.py b/source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/main.py deleted file mode 100644 index bacc165..0000000 --- a/source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/main.py +++ /dev/null @@ -1,22 +0,0 @@ -from flask import Flask -from newsbot import * -from states import States - -bot = Flask(__name__) - - -@bot.route('/index') -def index(): - return 'Thou shalt not pass!' - - -@bot.route('/telegram-update', methods=['POST']) -def telegram_update(): - handle_incoming_messages(States.last_updated_id) - - -if __name__ == '__main__': - States.last_updated_id = get_last_updated() - bot.run() - - diff --git a/source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/one_time.py b/source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/one_time.py deleted file mode 100644 index f7074d9..0000000 --- a/source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/one_time.py +++ /dev/null @@ -1,11 +0,0 @@ - -from models import * - - -def create_tables(): - db.connect() - db.create_tables([Source, Request, Message], True) - db.close() - -if __name__ == '__main__': - create_tables() diff --git a/source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/requirements.txt b/source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/requirements.txt deleted file mode 100644 index 8854d79..0000000 --- a/source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -praw -peewee==2.10.2 -PyMySQL diff --git a/source-code/chapter-6/exercise-1/newsbot/Dockerfile b/source-code/chapter-6/exercise-1/newsbot/Dockerfile new file mode 100644 index 0000000..7d69914 --- /dev/null +++ b/source-code/chapter-6/exercise-1/newsbot/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3-alpine + +RUN apk add gcc musl-dev python3-dev libffi-dev openssl-dev cargo +WORKDIR /apps/subredditfetcher/ +COPY . . + +RUN pip install --upgrade pip && pip install -r requirements.txt +VOLUME ["/data"] +CMD ["python", "newsbot.py"] diff --git a/source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/LICENSE b/source-code/chapter-6/exercise-1/newsbot/LICENSE similarity index 100% rename from source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/LICENSE rename to source-code/chapter-6/exercise-1/newsbot/LICENSE diff --git a/source-code/chapter-6/exercise-1/newsbot/constants.py b/source-code/chapter-6/exercise-1/newsbot/constants.py new file mode 100644 index 0000000..092cde6 --- /dev/null +++ b/source-code/chapter-6/exercise-1/newsbot/constants.py @@ -0,0 +1,15 @@ +__author__ = 'Sathyajith' + +from os import environ +from sys import exit +ERR_NO_SOURCE = 'No sources defined! Set a source using /source list, of, sub, reddits' +skip_list = [] +sources_dict = {} +UPDATE_PERIOD = 1 +FALSE_RESPONSE = {"ok": False} + +BOT_KEY = environ.get('NBT_ACCESS_TOKEN') +if not BOT_KEY: + print("Telegram access token not set, exiting.") + exit(1) +API_BASE = f'https://api.telegram.org/bot{BOT_KEY}' \ No newline at end of file diff --git a/source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/last_updated.txt b/source-code/chapter-6/exercise-1/newsbot/last_updated.txt similarity index 100% rename from source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/last_updated.txt rename to source-code/chapter-6/exercise-1/newsbot/last_updated.txt diff --git a/source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/models.py b/source-code/chapter-6/exercise-1/newsbot/models.py similarity index 85% rename from source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/models.py rename to source-code/chapter-6/exercise-1/newsbot/models.py index a7834ba..ec73558 100644 --- a/source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/models.py +++ b/source-code/chapter-6/exercise-1/newsbot/models.py @@ -1,6 +1,8 @@ -from peewee import * -# db = SqliteDatabase('newsbot.db') +from peewee import Model, PrimaryKeyField, IntegerField, CharField, DateTimeField, MySQLDatabase + + db = MySQLDatabase(host="mysql", port=3306, user="root", password="dontusethisinprod", database="newsbot") + class BaseModel(Model): class Meta: database = db diff --git a/source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/newsbot.db b/source-code/chapter-6/exercise-1/newsbot/newsbot.db similarity index 100% rename from source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/newsbot.db rename to source-code/chapter-6/exercise-1/newsbot/newsbot.db diff --git a/source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/newsbot.py b/source-code/chapter-6/exercise-1/newsbot/newsbot.py similarity index 69% rename from source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/newsbot.py rename to source-code/chapter-6/exercise-1/newsbot/newsbot.py index 659bc41..020aba5 100644 --- a/source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/newsbot.py +++ b/source-code/chapter-6/exercise-1/newsbot/newsbot.py @@ -2,8 +2,7 @@ from telegram import handle_incoming_messages from models import * from time import sleep - -import sys +from peewee import OperationalError import pymysql @@ -28,14 +27,12 @@ def get_last_updated(): log.info('Checking on dbs') try: db.connect() - except OperationalError as o: - print("Could not connect to db, please check db parameters") - sys.exit(-1) - except InternalError as e: - # 1049 is MySQL error code for db doesn't exist - so we create it. - db_connection = pymysql.connect(host='mysql', user= 'root', password='dontusethisinprod') - db_connection.cursor().execute('CREATE DATABASE newsbot') - db_connection.close() + except OperationalError as e: + error_code, message = e.args[0], e.args[1] + if error_code == 1049: + db_connection = pymysql.connect(host='mysql', user= 'root', password='dontusethisinprod') + db_connection.cursor().execute('CREATE DATABASE newsbot') + db_connection.close() db.create_tables([Source, Request, Message], True) try: diff --git a/source-code/chapter-7/exercise-1/docker-compose-adminer/reddit.py b/source-code/chapter-6/exercise-1/newsbot/reddit.py similarity index 63% rename from source-code/chapter-7/exercise-1/docker-compose-adminer/reddit.py rename to source-code/chapter-6/exercise-1/newsbot/reddit.py index 31f8dff..7f065eb 100644 --- a/source-code/chapter-7/exercise-1/docker-compose-adminer/reddit.py +++ b/source-code/chapter-6/exercise-1/newsbot/reddit.py @@ -4,35 +4,30 @@ __author__ = 'Sathyajith' -def summarize(url): - log.info('Not yet implemented!') - return url - - def get_latest_news(sub_reddits): log.debug('Fetching news from reddit') - r = praw.Reddit(user_agent='SubReddit Newsfetcher Bot', + r = praw.Reddit(user_agent='NewsBot', client_id='ralalsYuEJXKDg', client_secret="16DD-6O7VVaYVMlkUPZWLhdluhU") r.read_only = True # Can change the subreddit or add more. sub_reddits = clean_up_subreddits(sub_reddits) - log.info('Fetching subreddits: {0}'.format(sub_reddits)) + log.info('Fetching subreddits: {sub_reddits}') submissions = r.subreddit(sub_reddits).hot(limit=5) submission_content = '' try: for post in submissions: - submission_content += summarize(post.title + ' - ' + post.url) + '\n\n' + submission_content += post.title + ' - ' + post.url + '\n\n' except praw.errors.Forbidden: - log.debug('subreddit {0} is private'.format(sub_reddits)) + log.debug(f'subreddit {sub_reddits} is private') submission_content = "Sorry couldn't fetch; subreddit is private" except praw.errors.InvalidSubreddit: - log.debug('Subreddit {} is invalid or doesn''t exist.'.format(sub_reddits)) + log.debug(f'Subreddit {sub_reddits} is invalid or doesn''t exist') submission_content = "Sorry couldn't fetch; subreddit doesn't seem to exist" return submission_content def clean_up_subreddits(sub_reddits): - log.debug('Got subreddits to clean: {0}'.format(sub_reddits)) + log.debug(f'Got subreddits to clean: {sub_reddits}') return sub_reddits.strip().replace(" ", "").replace(',', '+') diff --git a/source-code/chapter-6/exercise-1/newsbot/requirements.txt b/source-code/chapter-6/exercise-1/newsbot/requirements.txt new file mode 100644 index 0000000..0d19ff9 --- /dev/null +++ b/source-code/chapter-6/exercise-1/newsbot/requirements.txt @@ -0,0 +1,4 @@ +peewee==2.10.2 +praw==7.4.0 +PyMySQL +cryptography \ No newline at end of file diff --git a/source-code/chapter-7/exercise-1/docker-compose-adminer/states.py b/source-code/chapter-6/exercise-1/newsbot/states.py similarity index 100% rename from source-code/chapter-7/exercise-1/docker-compose-adminer/states.py rename to source-code/chapter-6/exercise-1/newsbot/states.py diff --git a/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/telegram.py b/source-code/chapter-6/exercise-1/newsbot/telegram.py similarity index 66% rename from source-code/chapter-7/exercise-2/subreddit-fetcher-compose/telegram.py rename to source-code/chapter-6/exercise-1/newsbot/telegram.py index 5e150c7..d852d72 100644 --- a/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/telegram.py +++ b/source-code/chapter-6/exercise-1/newsbot/telegram.py @@ -6,15 +6,16 @@ from constants import * from reddit import get_latest_news from models import * +import peewee __author__ = 'Sathyajith' db = MySQLDatabase(host="mysql", port=3306, user="root", password="dontusethisinprod", database="newsbot") def get_updates(last_updated): - log.debug('Checking for requests, last updated passed is: {}'.format(last_updated)) + log.debug('Checking for requests, last updated passed is: {last_updated}') sleep(UPDATE_PERIOD) - response = requests.get(API_BASE + BOT_KEY + '/getUpdates', params={'offset': last_updated+1}) + response = requests.get(f"{API_BASE}/getUpdates", params={'offset': last_updated+1}) json_response = FALSE_RESPONSE if response.status_code != 200: # wait for a bit, try again @@ -25,14 +26,14 @@ def get_updates(last_updated): except ValueError: sleep(UPDATE_PERIOD*20) get_updates(last_updated) - log.info('received response: {}'.format(json_response)) + log.info(f"received response: {json_response}") return json_response def post_message(chat_id, text): - log.info('posting {} to {}'.format(text, chat_id)) + log.debug(f"posting {text} to {chat_id}") payload = {'chat_id': chat_id, 'text': text} - requests.post(API_BASE + BOT_KEY + '/sendMessage', data=payload) + requests.post(f"{API_BASE}/sendMessage", data=payload) def handle_incoming_messages(last_updated): @@ -40,7 +41,10 @@ def handle_incoming_messages(last_updated): split_chat_text = [] if r['ok']: for req in r['result']: - chat_sender_id = req['message']['chat']['id'] + if 'message' in req: + chat_sender_id = req['message']['chat']['id'] + else: + chat_sender_id = req['edited_message']['chat']['id'] try: chat_text = req['message']['text'] split_chat_text = chat_text.split() @@ -48,30 +52,33 @@ def handle_incoming_messages(last_updated): chat_text = '' split_chat_text.append(chat_text) log.debug('Looks like no chat text was detected... moving on') - try: + + if 'message' in req: person_id = req['message']['from']['id'] - except KeyError: - pass + else: + person_id = req['edited_message']['from']['id'] - log.info('Chat text received: {0}'.format(chat_text)) + log.info(f"Chat text received: {chat_text}") r = re.search('(source+)(.*)', chat_text) if (r is not None and r.group(1) == 'source'): if r.group(2): sources_dict[person_id] = r.group(2) - log.info('Sources set for {0} to {1}'.format(person_id, sources_dict[person_id])) - try: - sources = Source.create(person_id = person_id, fetch_from = sources_dict[person_id]) - except IntegrityError: - sources = Source.get(person_id = person_id) - sources.fetch_from = sources_dict[person_id] - sources.save() - log.info(sources.person_id) + log.info(f'Sources set for {person_id} to {sources_dict[person_id]}') + with db.atomic() as txn: + try: + sources = Source.create(person_id=person_id, fetch_from=sources_dict[person_id]) + log.debug(f'Inserted row id: {sources.person_id}') + except peewee.IntegrityError: + sources = Source.update(fetch_from=sources_dict[person_id]).where(person_id == person_id) + rows_updated = sources.execute() + log.info(f'Updated {rows_updated} rows') + txn.commit() post_message(person_id, 'Sources set as {0}!'.format(r.group(2))) else: post_message(person_id, 'We need a comma separated list of subreddits! No subreddit, no news :-(') if chat_text == '/stop': - log.debug('Added {0} to skip list'.format(chat_sender_id)) + log.debug(f'Added {chat_sender_id} to skip list') skip_list.append(chat_sender_id) post_message(chat_sender_id, "Ok, we won't send you any more messages.") @@ -79,19 +86,17 @@ def handle_incoming_messages(last_updated): helptext = ''' Hi! This is a News Bot which fetches news from subreddits. Use "/source" to select a subreddit source. Example "/source programming,games" fetches news from r/programming, r/games. - Use "/fetch for the bot to go ahead and fetch the news. At the moment, bot will fetch total of 5 posts from all sub reddits - I will have this configurable soon. + Use "/fetch for the bot to go ahead and fetch the news. At the moment, bot will fetch total of 5 posts from the selected subreddit. ''' post_message(chat_sender_id, helptext) if split_chat_text[0] == '/fetch' and (person_id not in skip_list): post_message(person_id, 'Hang on, fetching your news..') - try: sub_reddits = Source.get(person_id = person_id).fetch_from.strip() summarized_news = get_latest_news(sub_reddits) post_message(person_id, summarized_news) - except: + except peewee.DoesNotExist: post_message(person_id, ERR_NO_SOURCE) last_updated = req['update_id'] @@ -99,5 +104,5 @@ def handle_incoming_messages(last_updated): f.write(str(last_updated)) States.last_updated = last_updated log.debug( - 'Updated last_updated to {0}'.format(last_updated)) + f'Updated last_updated to {last_updated}') f.close() diff --git a/source-code/chapter-7/exercise-1/docker-compose-adminer/Dockerfile b/source-code/chapter-7/exercise-1/docker-compose-adminer/Dockerfile deleted file mode 100644 index 29069bc..0000000 --- a/source-code/chapter-7/exercise-1/docker-compose-adminer/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM python:3-alpine - -RUN apk add gcc musl-dev python3-dev libffi-dev openssl-dev -COPY * /apps/subredditfetcher/ -WORKDIR /apps/subredditfetcher/ - -VOLUME [ "/apps/subredditfetcher" ] -RUN ["pip", "install", "-r", "requirements.txt"] -# RUN ["python", "one_time.py"] - -ENV NBT_ACCESS_TOKEN="495637361:AAHIhiDTX1UeX17KJy0-FsMZEqEtCFYfcP8" - -CMD ["python", "newsbot.py"] diff --git a/source-code/chapter-7/exercise-1/docker-compose-adminer/constants.py b/source-code/chapter-7/exercise-1/docker-compose-adminer/constants.py deleted file mode 100644 index cb189fe..0000000 --- a/source-code/chapter-7/exercise-1/docker-compose-adminer/constants.py +++ /dev/null @@ -1,11 +0,0 @@ -__author__ = 'Sathyajith' - -import os -ERR_NO_SOURCE = 'No sources defined! Set a source using /source list, of, sub, reddits' -skip_list = [] -sources_dict = {} - -BOT_KEY = os.environ['NBT_ACCESS_TOKEN'] -API_BASE = 'https://api.telegram.org/bot' -UPDATE_PERIOD = 6 -FALSE_RESPONSE = {"ok": False} \ No newline at end of file diff --git a/source-code/chapter-7/exercise-1/docker-compose-adminer/docker-compose.yml b/source-code/chapter-7/exercise-1/docker-compose-adminer/docker-compose.yml index 2c0a4e5..4fa85f5 100644 --- a/source-code/chapter-7/exercise-1/docker-compose-adminer/docker-compose.yml +++ b/source-code/chapter-7/exercise-1/docker-compose-adminer/docker-compose.yml @@ -1,23 +1,16 @@ -version: '3' -services: - app: - build: . - depends_on: - - mysql - restart: "on-failure" - volumes: - - "appdata:/apps/subredditfetcher" - mysql: - image: mysql - volumes: - - "dbdata:/var/lib/mysql" - environment: - - MYSQL_ROOT_PASSWORD=dontusethisinprod - adminer: - image: adminer - ports: - - "8080:8080" - -volumes: - dbdata: - appdata: +services: + mysql: + image: mysql + environment: + MYSQL_ROOT_PASSWORD: dontusethisinprod + ports: + - 3306:3306 + volumes: + - dbdata:/var/lib/mysql + adminer: + image: adminer + ports: + - 8080:8080 + +volumes: + dbdata: \ No newline at end of file diff --git a/source-code/chapter-7/exercise-1/docker-compose-adminer/main.py b/source-code/chapter-7/exercise-1/docker-compose-adminer/main.py deleted file mode 100644 index bacc165..0000000 --- a/source-code/chapter-7/exercise-1/docker-compose-adminer/main.py +++ /dev/null @@ -1,22 +0,0 @@ -from flask import Flask -from newsbot import * -from states import States - -bot = Flask(__name__) - - -@bot.route('/index') -def index(): - return 'Thou shalt not pass!' - - -@bot.route('/telegram-update', methods=['POST']) -def telegram_update(): - handle_incoming_messages(States.last_updated_id) - - -if __name__ == '__main__': - States.last_updated_id = get_last_updated() - bot.run() - - diff --git a/source-code/chapter-7/exercise-1/docker-compose-adminer/newsbot.db b/source-code/chapter-7/exercise-1/docker-compose-adminer/newsbot.db deleted file mode 100644 index 2c5e1e7..0000000 Binary files a/source-code/chapter-7/exercise-1/docker-compose-adminer/newsbot.db and /dev/null differ diff --git a/source-code/chapter-7/exercise-1/docker-compose-adminer/one_time.py b/source-code/chapter-7/exercise-1/docker-compose-adminer/one_time.py deleted file mode 100644 index f7074d9..0000000 --- a/source-code/chapter-7/exercise-1/docker-compose-adminer/one_time.py +++ /dev/null @@ -1,11 +0,0 @@ - -from models import * - - -def create_tables(): - db.connect() - db.create_tables([Source, Request, Message], True) - db.close() - -if __name__ == '__main__': - create_tables() diff --git a/source-code/chapter-7/exercise-1/docker-compose-adminer/requirements.txt b/source-code/chapter-7/exercise-1/docker-compose-adminer/requirements.txt deleted file mode 100644 index 8854d79..0000000 --- a/source-code/chapter-7/exercise-1/docker-compose-adminer/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -praw -peewee==2.10.2 -PyMySQL diff --git a/source-code/chapter-7/exercise-2/newsbot-compose/Dockerfile b/source-code/chapter-7/exercise-2/newsbot-compose/Dockerfile new file mode 100644 index 0000000..7d69914 --- /dev/null +++ b/source-code/chapter-7/exercise-2/newsbot-compose/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3-alpine + +RUN apk add gcc musl-dev python3-dev libffi-dev openssl-dev cargo +WORKDIR /apps/subredditfetcher/ +COPY . . + +RUN pip install --upgrade pip && pip install -r requirements.txt +VOLUME ["/data"] +CMD ["python", "newsbot.py"] diff --git a/source-code/chapter-7/exercise-1/docker-compose-adminer/LICENSE b/source-code/chapter-7/exercise-2/newsbot-compose/LICENSE similarity index 100% rename from source-code/chapter-7/exercise-1/docker-compose-adminer/LICENSE rename to source-code/chapter-7/exercise-2/newsbot-compose/LICENSE diff --git a/source-code/chapter-7/exercise-2/newsbot-compose/constants.py b/source-code/chapter-7/exercise-2/newsbot-compose/constants.py new file mode 100644 index 0000000..092cde6 --- /dev/null +++ b/source-code/chapter-7/exercise-2/newsbot-compose/constants.py @@ -0,0 +1,15 @@ +__author__ = 'Sathyajith' + +from os import environ +from sys import exit +ERR_NO_SOURCE = 'No sources defined! Set a source using /source list, of, sub, reddits' +skip_list = [] +sources_dict = {} +UPDATE_PERIOD = 1 +FALSE_RESPONSE = {"ok": False} + +BOT_KEY = environ.get('NBT_ACCESS_TOKEN') +if not BOT_KEY: + print("Telegram access token not set, exiting.") + exit(1) +API_BASE = f'https://api.telegram.org/bot{BOT_KEY}' \ No newline at end of file diff --git a/source-code/chapter-7/exercise-2/newsbot-compose/docker-compose.adminer.yml b/source-code/chapter-7/exercise-2/newsbot-compose/docker-compose.adminer.yml new file mode 100644 index 0000000..8132d37 --- /dev/null +++ b/source-code/chapter-7/exercise-2/newsbot-compose/docker-compose.adminer.yml @@ -0,0 +1,34 @@ +services: + newsbot: + build: . + depends_on: + - mysql + restart: "on-failure" + environment: + NBT_ACCESS_TOKEN: ${NBT_ACCESS_TOKEN} + networks: + - newsbot + + mysql: + image: mysql + volumes: + - newsbot-db:/var/lib/mysql + environment: + MYSQL_ROOT_PASSWORD: dontusethisinprod + + networks: + - newsbot + + adminer: + image: adminer + ports: + - 8080:8080 + networks: + - newsbot + +volumes: + newsbot-db: + + +networks: + newsbot: \ No newline at end of file diff --git a/source-code/chapter-7/exercise-2/newsbot-compose/docker-compose.yml b/source-code/chapter-7/exercise-2/newsbot-compose/docker-compose.yml new file mode 100644 index 0000000..20561bc --- /dev/null +++ b/source-code/chapter-7/exercise-2/newsbot-compose/docker-compose.yml @@ -0,0 +1,25 @@ +services: + newsbot: + build: . + depends_on: + - mysql + restart: "on-failure" + environment: + NBT_ACCESS_TOKEN: ${NBT_ACCESS_TOKEN} + networks: + - newsbot + + mysql: + image: mysql + volumes: + - newsbot-db:/var/lib/mysql + environment: + MYSQL_ROOT_PASSWORD: dontusethisinprod + networks: + - newsbot + +volumes: + newsbot-db: + +networks: + newsbot: \ No newline at end of file diff --git a/source-code/chapter-7/exercise-1/docker-compose-adminer/last_updated.txt b/source-code/chapter-7/exercise-2/newsbot-compose/last_updated.txt similarity index 100% rename from source-code/chapter-7/exercise-1/docker-compose-adminer/last_updated.txt rename to source-code/chapter-7/exercise-2/newsbot-compose/last_updated.txt diff --git a/source-code/chapter-7/exercise-1/docker-compose-adminer/models.py b/source-code/chapter-7/exercise-2/newsbot-compose/models.py similarity index 85% rename from source-code/chapter-7/exercise-1/docker-compose-adminer/models.py rename to source-code/chapter-7/exercise-2/newsbot-compose/models.py index a7834ba..ec73558 100644 --- a/source-code/chapter-7/exercise-1/docker-compose-adminer/models.py +++ b/source-code/chapter-7/exercise-2/newsbot-compose/models.py @@ -1,6 +1,8 @@ -from peewee import * -# db = SqliteDatabase('newsbot.db') +from peewee import Model, PrimaryKeyField, IntegerField, CharField, DateTimeField, MySQLDatabase + + db = MySQLDatabase(host="mysql", port=3306, user="root", password="dontusethisinprod", database="newsbot") + class BaseModel(Model): class Meta: database = db diff --git a/source-code/chapter-7/exercise-1/docker-compose-adminer/newsbot.py b/source-code/chapter-7/exercise-2/newsbot-compose/newsbot.py similarity index 69% rename from source-code/chapter-7/exercise-1/docker-compose-adminer/newsbot.py rename to source-code/chapter-7/exercise-2/newsbot-compose/newsbot.py index 659bc41..020aba5 100644 --- a/source-code/chapter-7/exercise-1/docker-compose-adminer/newsbot.py +++ b/source-code/chapter-7/exercise-2/newsbot-compose/newsbot.py @@ -2,8 +2,7 @@ from telegram import handle_incoming_messages from models import * from time import sleep - -import sys +from peewee import OperationalError import pymysql @@ -28,14 +27,12 @@ def get_last_updated(): log.info('Checking on dbs') try: db.connect() - except OperationalError as o: - print("Could not connect to db, please check db parameters") - sys.exit(-1) - except InternalError as e: - # 1049 is MySQL error code for db doesn't exist - so we create it. - db_connection = pymysql.connect(host='mysql', user= 'root', password='dontusethisinprod') - db_connection.cursor().execute('CREATE DATABASE newsbot') - db_connection.close() + except OperationalError as e: + error_code, message = e.args[0], e.args[1] + if error_code == 1049: + db_connection = pymysql.connect(host='mysql', user= 'root', password='dontusethisinprod') + db_connection.cursor().execute('CREATE DATABASE newsbot') + db_connection.close() db.create_tables([Source, Request, Message], True) try: diff --git a/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/reddit.py b/source-code/chapter-7/exercise-2/newsbot-compose/reddit.py similarity index 63% rename from source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/reddit.py rename to source-code/chapter-7/exercise-2/newsbot-compose/reddit.py index 31f8dff..3f6d4b1 100644 --- a/source-code/chapter-5/exercise-2/docker-subreddit-fetcher-volume/reddit.py +++ b/source-code/chapter-7/exercise-2/newsbot-compose/reddit.py @@ -4,35 +4,30 @@ __author__ = 'Sathyajith' -def summarize(url): - log.info('Not yet implemented!') - return url - - def get_latest_news(sub_reddits): log.debug('Fetching news from reddit') - r = praw.Reddit(user_agent='SubReddit Newsfetcher Bot', + r = praw.Reddit(user_agent='NewsBot', client_id='ralalsYuEJXKDg', client_secret="16DD-6O7VVaYVMlkUPZWLhdluhU") r.read_only = True # Can change the subreddit or add more. sub_reddits = clean_up_subreddits(sub_reddits) - log.info('Fetching subreddits: {0}'.format(sub_reddits)) + log.info(f'Fetching subreddits: {sub_reddits}') submissions = r.subreddit(sub_reddits).hot(limit=5) submission_content = '' try: for post in submissions: - submission_content += summarize(post.title + ' - ' + post.url) + '\n\n' + submission_content += post.title + ' - ' + post.url + '\n\n' except praw.errors.Forbidden: - log.debug('subreddit {0} is private'.format(sub_reddits)) + log.debug(f'subreddit {sub_reddits} is private') submission_content = "Sorry couldn't fetch; subreddit is private" except praw.errors.InvalidSubreddit: - log.debug('Subreddit {} is invalid or doesn''t exist.'.format(sub_reddits)) + log.debug(f'Subreddit {sub_reddits} is invalid or doesn''t exist') submission_content = "Sorry couldn't fetch; subreddit doesn't seem to exist" return submission_content def clean_up_subreddits(sub_reddits): - log.debug('Got subreddits to clean: {0}'.format(sub_reddits)) + log.debug(f'Got subreddits to clean: {sub_reddits}') return sub_reddits.strip().replace(" ", "").replace(',', '+') diff --git a/source-code/chapter-7/exercise-2/newsbot-compose/requirements.txt b/source-code/chapter-7/exercise-2/newsbot-compose/requirements.txt new file mode 100644 index 0000000..0d19ff9 --- /dev/null +++ b/source-code/chapter-7/exercise-2/newsbot-compose/requirements.txt @@ -0,0 +1,4 @@ +peewee==2.10.2 +praw==7.4.0 +PyMySQL +cryptography \ No newline at end of file diff --git a/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/states.py b/source-code/chapter-7/exercise-2/newsbot-compose/states.py similarity index 100% rename from source-code/chapter-7/exercise-2/subreddit-fetcher-compose/states.py rename to source-code/chapter-7/exercise-2/newsbot-compose/states.py diff --git a/source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/telegram.py b/source-code/chapter-7/exercise-2/newsbot-compose/telegram.py similarity index 66% rename from source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/telegram.py rename to source-code/chapter-7/exercise-2/newsbot-compose/telegram.py index 5e150c7..d852d72 100644 --- a/source-code/chapter-6/exercise-1/docker-subreddit-fetcher-network/telegram.py +++ b/source-code/chapter-7/exercise-2/newsbot-compose/telegram.py @@ -6,15 +6,16 @@ from constants import * from reddit import get_latest_news from models import * +import peewee __author__ = 'Sathyajith' db = MySQLDatabase(host="mysql", port=3306, user="root", password="dontusethisinprod", database="newsbot") def get_updates(last_updated): - log.debug('Checking for requests, last updated passed is: {}'.format(last_updated)) + log.debug('Checking for requests, last updated passed is: {last_updated}') sleep(UPDATE_PERIOD) - response = requests.get(API_BASE + BOT_KEY + '/getUpdates', params={'offset': last_updated+1}) + response = requests.get(f"{API_BASE}/getUpdates", params={'offset': last_updated+1}) json_response = FALSE_RESPONSE if response.status_code != 200: # wait for a bit, try again @@ -25,14 +26,14 @@ def get_updates(last_updated): except ValueError: sleep(UPDATE_PERIOD*20) get_updates(last_updated) - log.info('received response: {}'.format(json_response)) + log.info(f"received response: {json_response}") return json_response def post_message(chat_id, text): - log.info('posting {} to {}'.format(text, chat_id)) + log.debug(f"posting {text} to {chat_id}") payload = {'chat_id': chat_id, 'text': text} - requests.post(API_BASE + BOT_KEY + '/sendMessage', data=payload) + requests.post(f"{API_BASE}/sendMessage", data=payload) def handle_incoming_messages(last_updated): @@ -40,7 +41,10 @@ def handle_incoming_messages(last_updated): split_chat_text = [] if r['ok']: for req in r['result']: - chat_sender_id = req['message']['chat']['id'] + if 'message' in req: + chat_sender_id = req['message']['chat']['id'] + else: + chat_sender_id = req['edited_message']['chat']['id'] try: chat_text = req['message']['text'] split_chat_text = chat_text.split() @@ -48,30 +52,33 @@ def handle_incoming_messages(last_updated): chat_text = '' split_chat_text.append(chat_text) log.debug('Looks like no chat text was detected... moving on') - try: + + if 'message' in req: person_id = req['message']['from']['id'] - except KeyError: - pass + else: + person_id = req['edited_message']['from']['id'] - log.info('Chat text received: {0}'.format(chat_text)) + log.info(f"Chat text received: {chat_text}") r = re.search('(source+)(.*)', chat_text) if (r is not None and r.group(1) == 'source'): if r.group(2): sources_dict[person_id] = r.group(2) - log.info('Sources set for {0} to {1}'.format(person_id, sources_dict[person_id])) - try: - sources = Source.create(person_id = person_id, fetch_from = sources_dict[person_id]) - except IntegrityError: - sources = Source.get(person_id = person_id) - sources.fetch_from = sources_dict[person_id] - sources.save() - log.info(sources.person_id) + log.info(f'Sources set for {person_id} to {sources_dict[person_id]}') + with db.atomic() as txn: + try: + sources = Source.create(person_id=person_id, fetch_from=sources_dict[person_id]) + log.debug(f'Inserted row id: {sources.person_id}') + except peewee.IntegrityError: + sources = Source.update(fetch_from=sources_dict[person_id]).where(person_id == person_id) + rows_updated = sources.execute() + log.info(f'Updated {rows_updated} rows') + txn.commit() post_message(person_id, 'Sources set as {0}!'.format(r.group(2))) else: post_message(person_id, 'We need a comma separated list of subreddits! No subreddit, no news :-(') if chat_text == '/stop': - log.debug('Added {0} to skip list'.format(chat_sender_id)) + log.debug(f'Added {chat_sender_id} to skip list') skip_list.append(chat_sender_id) post_message(chat_sender_id, "Ok, we won't send you any more messages.") @@ -79,19 +86,17 @@ def handle_incoming_messages(last_updated): helptext = ''' Hi! This is a News Bot which fetches news from subreddits. Use "/source" to select a subreddit source. Example "/source programming,games" fetches news from r/programming, r/games. - Use "/fetch for the bot to go ahead and fetch the news. At the moment, bot will fetch total of 5 posts from all sub reddits - I will have this configurable soon. + Use "/fetch for the bot to go ahead and fetch the news. At the moment, bot will fetch total of 5 posts from the selected subreddit. ''' post_message(chat_sender_id, helptext) if split_chat_text[0] == '/fetch' and (person_id not in skip_list): post_message(person_id, 'Hang on, fetching your news..') - try: sub_reddits = Source.get(person_id = person_id).fetch_from.strip() summarized_news = get_latest_news(sub_reddits) post_message(person_id, summarized_news) - except: + except peewee.DoesNotExist: post_message(person_id, ERR_NO_SOURCE) last_updated = req['update_id'] @@ -99,5 +104,5 @@ def handle_incoming_messages(last_updated): f.write(str(last_updated)) States.last_updated = last_updated log.debug( - 'Updated last_updated to {0}'.format(last_updated)) + f'Updated last_updated to {last_updated}') f.close() diff --git a/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/Dockerfile b/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/Dockerfile deleted file mode 100644 index 29069bc..0000000 --- a/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM python:3-alpine - -RUN apk add gcc musl-dev python3-dev libffi-dev openssl-dev -COPY * /apps/subredditfetcher/ -WORKDIR /apps/subredditfetcher/ - -VOLUME [ "/apps/subredditfetcher" ] -RUN ["pip", "install", "-r", "requirements.txt"] -# RUN ["python", "one_time.py"] - -ENV NBT_ACCESS_TOKEN="495637361:AAHIhiDTX1UeX17KJy0-FsMZEqEtCFYfcP8" - -CMD ["python", "newsbot.py"] diff --git a/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/constants.py b/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/constants.py deleted file mode 100644 index cb189fe..0000000 --- a/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/constants.py +++ /dev/null @@ -1,11 +0,0 @@ -__author__ = 'Sathyajith' - -import os -ERR_NO_SOURCE = 'No sources defined! Set a source using /source list, of, sub, reddits' -skip_list = [] -sources_dict = {} - -BOT_KEY = os.environ['NBT_ACCESS_TOKEN'] -API_BASE = 'https://api.telegram.org/bot' -UPDATE_PERIOD = 6 -FALSE_RESPONSE = {"ok": False} \ No newline at end of file diff --git a/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/docker-compose.yml b/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/docker-compose.yml deleted file mode 100644 index 2c0a4e5..0000000 --- a/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/docker-compose.yml +++ /dev/null @@ -1,23 +0,0 @@ -version: '3' -services: - app: - build: . - depends_on: - - mysql - restart: "on-failure" - volumes: - - "appdata:/apps/subredditfetcher" - mysql: - image: mysql - volumes: - - "dbdata:/var/lib/mysql" - environment: - - MYSQL_ROOT_PASSWORD=dontusethisinprod - adminer: - image: adminer - ports: - - "8080:8080" - -volumes: - dbdata: - appdata: diff --git a/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/last_updated.txt b/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/last_updated.txt deleted file mode 100644 index 97702fb..0000000 --- a/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/last_updated.txt +++ /dev/null @@ -1 +0,0 @@ -865610176 \ No newline at end of file diff --git a/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/main.py b/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/main.py deleted file mode 100644 index bacc165..0000000 --- a/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/main.py +++ /dev/null @@ -1,22 +0,0 @@ -from flask import Flask -from newsbot import * -from states import States - -bot = Flask(__name__) - - -@bot.route('/index') -def index(): - return 'Thou shalt not pass!' - - -@bot.route('/telegram-update', methods=['POST']) -def telegram_update(): - handle_incoming_messages(States.last_updated_id) - - -if __name__ == '__main__': - States.last_updated_id = get_last_updated() - bot.run() - - diff --git a/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/models.py b/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/models.py deleted file mode 100644 index a7834ba..0000000 --- a/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/models.py +++ /dev/null @@ -1,24 +0,0 @@ -from peewee import * -# db = SqliteDatabase('newsbot.db') -db = MySQLDatabase(host="mysql", port=3306, user="root", password="dontusethisinprod", database="newsbot") -class BaseModel(Model): - class Meta: - database = db - -class Source(BaseModel): - person_id = PrimaryKeyField() - fetch_from = CharField() - - -class Request(BaseModel): - id = PrimaryKeyField() - from_id = IntegerField(index=True) - request_text = CharField() - received_time = DateTimeField() - - -class Message(BaseModel): - id = PrimaryKeyField() - sent_to_id = IntegerField(index=True) - sent_message = CharField() - sent_time = DateTimeField() diff --git a/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/newsbot.db b/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/newsbot.db deleted file mode 100644 index 2c5e1e7..0000000 Binary files a/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/newsbot.db and /dev/null differ diff --git a/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/newsbot.py b/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/newsbot.py deleted file mode 100644 index 659bc41..0000000 --- a/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/newsbot.py +++ /dev/null @@ -1,46 +0,0 @@ -from states import States, log -from telegram import handle_incoming_messages -from models import * -from time import sleep - -import sys -import pymysql - - -def get_last_updated(): - try: - with open('last_updated.txt', 'r') as f: - try: - last_updated = int(f.read()) - except ValueError: - last_updated = 0 - f.close() - except FileNotFoundError: - last_updated = 0 - log.debug('Last updated id: {0}'.format(last_updated)) - return last_updated - -if __name__ == '__main__': - log.info('Starting up') - log.info('Waiting for 60 seconds for db to come up') - sleep(60) - - log.info('Checking on dbs') - try: - db.connect() - except OperationalError as o: - print("Could not connect to db, please check db parameters") - sys.exit(-1) - except InternalError as e: - # 1049 is MySQL error code for db doesn't exist - so we create it. - db_connection = pymysql.connect(host='mysql', user= 'root', password='dontusethisinprod') - db_connection.cursor().execute('CREATE DATABASE newsbot') - db_connection.close() - db.create_tables([Source, Request, Message], True) - - try: - States.last_updated = get_last_updated() - while True: - handle_incoming_messages(States.last_updated) - except KeyboardInterrupt: - log.info('Received KeybInterrupt, exiting') diff --git a/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/requirements.txt b/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/requirements.txt deleted file mode 100644 index 8854d79..0000000 --- a/source-code/chapter-7/exercise-2/subreddit-fetcher-compose/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -praw -peewee==2.10.2 -PyMySQL diff --git a/source-code/chapter-8/exercise-1/README.md b/source-code/chapter-8/exercise-1/README.md new file mode 100644 index 0000000..a583e31 --- /dev/null +++ b/source-code/chapter-8/exercise-1/README.md @@ -0,0 +1,2 @@ +[kind](https://kind.sigs.k8s.io/), short for Kubernetes in Docker is a tool for running local Kubernetes clusters using Docker containers acting as nodes. For this exercise, we will look at how we can spin up a multi-node Kubernetes cluster using Kind. +Kind makes it easy to create multi-node clusters to test out locally. The final Kind configuration to create a multi-node cluster comprising of 3 control-plane nodes and 3 workers is found in [kind-multi-node.yml](kind-multi-node.yml) \ No newline at end of file diff --git a/source-code/chapter-8/exercise-1/kind-multi-node.yml b/source-code/chapter-8/exercise-1/kind-multi-node.yml new file mode 100644 index 0000000..5201231 --- /dev/null +++ b/source-code/chapter-8/exercise-1/kind-multi-node.yml @@ -0,0 +1,9 @@ +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +nodes: +- role: control-plane +- role: control-plane +- role: control-plane +- role: worker +- role: worker +- role: worker \ No newline at end of file diff --git a/source-code/chapter-8/exercise-2/README.md b/source-code/chapter-8/exercise-2/README.md new file mode 100644 index 0000000..8e6174a --- /dev/null +++ b/source-code/chapter-8/exercise-2/README.md @@ -0,0 +1,4 @@ +In this exercise, we will set up a Continuous Integration workflow for Newsbot that will run flake8, build the Docker image, and push the resulting image to Docker Hub. The Continuous Integration workflow will be set up using GitHub Actions, but the same principle could be applied using any Continuous Integration tool. + +This exercise also assumes that we will be working with the Newsbot source code and the Dockerfile from Chapter 7, Exercise 2. You can find the GitHub Actions Workflow file in [.github/workflows/build-newsbot.yaml](../../../.github/workflows/build-newsbot.yaml) file. +