Develop and Deploy a server less python application that updates Twitter banner in real time

gillarohith

Rohith Gilla

Posted on June 12, 2021

Develop and Deploy a server less python application that updates Twitter banner in real time

Hey hope you are staying positive and testing negative in the pandemic 🙌🏻

Hello gif

Let's check step by step on how to make your python serverless application that updates your Twitter banner or you could do anything, this has a huge potential.

We will be using a service called Deta, as their tagline says, "Build your apps in hours, deploy them in seconds." we will be doing the same :D
Btw it is completely free now so feel free to use it 🔥

This all started with this tweet of mine

Then this

We will be covering the latest tweet application here, if you want the notion one too, let me know in the comments below

So let's break it down step by step and make the application.

Let's make a list of requirements

Twitter Developer Account

Apply for a Twitter developer account if you haven't already, they will take some time for verification but after then you will be good to go.

Step 1 - Create a new Twitter application

Head over to Projects and Apps Tab and create a new application

Step 1

Step 2 - Name your application

Step 2

Step 3 - Consumer keys and secret

This is an important one, you will be getting your keys and tokens so please note them.
Step 3

Step 4 - Permissions

Once you go to the project dashboard head over to the app permissions, click edit and make the app Read and Write.

Step 4 - 1
Step 4 - 2

Step 5 - Access token and access secret

The step y'all have been waiting for getting the tokens.
Step 5 1
Step 5 2

By end of this process, we will be having

The two keys from step 3

  • CONSUMER_KEY
  • CONSUMER_SECRET

The two keys from step 5

  • ACCESS_TOKEN
  • ACCESS_TOKEN_SECRET

Deta

Deta logo

Docs of the Deta are amazing, you can check that if you have any issues with the application.

Step 0 - Install Deta

Install Deta based on your OS.

TL;DR
For Mac and Linux

curl -fsSL https://get.deta.dev/cli.sh | sh
Enter fullscreen mode Exit fullscreen mode

For Windows

iwr https://get.deta.dev/cli.ps1 -useb | iex
Enter fullscreen mode Exit fullscreen mode

then please do a deta login in your preferred terminal.

There is a known issue with Safari and Brave browser, so please use chrome when performing this step.

Step 1 - Create new project

We will be creating a new Deta Micro

To create a new micro with python run the following command.

deta new --python banner_update

Here banner_update is the application name.

To constantly check for updates and deploy them to the cloud deta offers a command, it's called deta watch.

Open a new terminal and run deta watch and leave it aside until we finish the project :D

Step 2 - Install the dependencies

Create a requirements.txt file in the root directory and have the required packages listed in the file.

You can use the following packages for this application.

requests
fastapi
python-dotenv
tweepy
Pillow
Enter fullscreen mode Exit fullscreen mode

Step 3 - Environment variables

Create a .env. file
Your .env file should look something similar to this

CONSUMER_KEY="your key"
CONSUMER_SECRET="your secret"
ACCESS_TOKEN="your token"
ACCESS_TOKEN_SECRET="your secret"
Enter fullscreen mode Exit fullscreen mode

create a .detaignore file and add !.env to it.

Env file

Deta ignores .env file by default.

Step 4 Python Code

Let's code

You can head over to Headers me and create a base for your twitter banner.

I created my base and it looks like this

Twitter banner

Download font of your choice in .ttf format. We need the font to write on the image.

The entire code looks something like this, don't worry let's go over it step by step.

from deta import App
from fastapi import FastAPI
from dotenv import load_dotenv
from os.path import join, dirname
import os
import tweepy
import shutil
import tempfile
from PIL import Image, ImageDraw, ImageFont


# Connecting to twitter service
AUTH = tweepy.OAuthHandler(
    os.environ.get("CONSUMER_KEY"), os.environ.get("CONSUMER_SECRET")
)
AUTH.set_access_token(
    os.environ.get("ACCESS_TOKEN"), os.environ.get("ACCESS_TOKEN_SECRET")
)

api = tweepy.API(AUTH,parser=tweepy.parsers.JSONParser())
upload_api =  tweepy.API(AUTH)

consolas_font = ImageFont.truetype("fonts/Consolas.ttf", 48)


dotenv_path = join(dirname(__file__), ".env")
load_dotenv(dotenv_path)

app = App(FastAPI())



# Helpers
def text_wrap(text, font, max_width):

    lines = []
    # If the width of the text is smaller than image width
    # we don't need to split it, just add it to the lines array
    # and return
    if font.getsize(text)[0] <= max_width:
        lines.append(text) 
    else:
        # split the line by spaces to get words
        words = text.split(' ')  
        i = 0
        # append every word to a line while its width is shorter than image width
        while i < len(words):
            line = ''         
            while i < len(words) and font.getsize(line + words[i])[0] <= max_width:                
                line = line + words[i] + " "
                i += 1
            if not line:
                line = words[i]
                i += 1
            # when the line gets longer than the max width do not append the word, 
            # add the line to the lines array
            lines.append(line)    
    return lines


@app.get("/")
def http():
    return "Hello DEV Community"


@app.lib.run()
@app.lib.cron()
def driver(e=None):
    data = api.followers()
    followers = data['users'][:3]
    shutil.copy('base.png', '/tmp/')
    img = Image.open('/tmp/base.png')
    image_editable = ImageDraw.Draw(img)
    initial_x = 42
    initial_y = 64
    image_size = img.size 
    for follower in followers:
        screen_name =follower["screen_name"]
        description = follower["description"]
        message = f"{screen_name} : {description}"
        lines = text_wrap(message, consolas_font, image_size[0] * 0.48)
        for val in lines:
            image_editable.text(
            (initial_x, initial_y), val, font=consolas_font, fill=(255, 255, 255)
        )
            initial_y = initial_y + 48
        initial_y = initial_y + 48

    edited_temp = tempfile.NamedTemporaryFile(suffix=".png")
    img.save(edited_temp.name)
    upload_api.update_profile_banner(edited_temp.name)
    return followers
Enter fullscreen mode Exit fullscreen mode

This essentially runs a Fast API service in the hood, so this can also be used to deploy your APIs and forgot about scaling because it is server less, they will take care of everything :D

Code Breakdown 1

# Connecting to twitter service
AUTH = tweepy.OAuthHandler(
    os.environ.get("CONSUMER_KEY"), os.environ.get("CONSUMER_SECRET")
)
AUTH.set_access_token(
    os.environ.get("ACCESS_TOKEN"), os.environ.get("ACCESS_TOKEN_SECRET")
)

api = tweepy.API(AUTH,parser=tweepy.parsers.JSONParser())
upload_api =  tweepy.API(AUTH)

consolas_font = ImageFont.truetype("fonts/Consolas.ttf", 48)


dotenv_path = join(dirname(__file__), ".env")
load_dotenv(dotenv_path)

app = App(FastAPI())
Enter fullscreen mode Exit fullscreen mode

Here we are initialising twitter api, fast api, font and loading environment variables.

Code Breakdown 2

# Helpers
def text_wrap(text, font, max_width):

    lines = []
    # If the width of the text is smaller than image width
    # we don't need to split it, just add it to the lines array
    # and return
    if font.getsize(text)[0] <= max_width:
        lines.append(text) 
    else:
        # split the line by spaces to get words
        words = text.split(' ')  
        i = 0
        # append every word to a line while its width is shorter than image width
        while i < len(words):
            line = ''         
            while i < len(words) and font.getsize(line + words[i])[0] <= max_width:                
                line = line + words[i] + " "
                i += 1
            if not line:
                line = words[i]
                i += 1
            # when the line gets longer than the max width do not append the word, 
            # add the line to the lines array
            lines.append(line)    
    return lines
Enter fullscreen mode Exit fullscreen mode

This is a helper function that helps us break lines in the given space. I found it online, trying to find the source to give credit but didn't find it. Will update once I find it.

Code breakdown 3

@app.lib.run()
@app.lib.cron()
def driver(e=None):
    data = api.followers()
    followers = data['users'][:3]
    shutil.copy('base.png', '/tmp/')
    img = Image.open('/tmp/base.png')
    image_editable = ImageDraw.Draw(img)
    initial_x = 42
    initial_y = 64
    image_size = img.size 
    for follower in followers:
        screen_name =follower["screen_name"]
        description = follower["description"]
        message = f"{screen_name} : {description}"
        lines = text_wrap(message, consolas_font, image_size[0] * 0.48)
        for val in lines:
            image_editable.text(
            (initial_x, initial_y), val, font=consolas_font, fill=(255, 255, 255)
        )
            initial_y = initial_y + 48
        initial_y = initial_y + 48

    edited_temp = tempfile.NamedTemporaryFile(suffix=".png")
    img.save(edited_temp.name)
    upload_api.update_profile_banner(edited_temp.name)
    return followers
Enter fullscreen mode Exit fullscreen mode

@app.lib.run() and @app.lib.cron() are specific to Deta and you can look them up here Run Cron

TL;DR version of cron and run are
Run - is to run the particular part of the application
Cron - is to run the part for every given interval of time, for example, run the function once every 2 minutes.

data = api.followers()
returns a JSON object of your followers.

followers = data['users'][:3]
returns the last three followers and saves them in the followers list.

shutil.copy('base.png', '/tmp/')
img = Image.open('/tmp/base.png')
image_editable = ImageDraw.Draw(img)
Enter fullscreen mode Exit fullscreen mode

This will be a bit tricky to understand so please read carefully.

So you cannot read a file with write permissions in the root folder, but ImageDraw requires us to open the file in write mode because it needs to draw on the image => writing on the image.
But we always have the /tmp/ folder where we can have write permissions because it is cleared frequently.
So we copy our base image into /tmp folder with shutil library.
Then open the image with ImageDraw

    initial_x = 42
    initial_y = 64
    image_size = img.size 
    for follower in followers:
        screen_name =follower["screen_name"];
        description = follower["description"];
        message = f"{screen_name} : {description}"
        lines = text_wrap(message, consolas_font, image_size[0] * 0.48)
        for val in lines:
            image_editable.text(
            (initial_x, initial_y), val, font=consolas_font, fill=(255, 255, 255)
        )
            initial_y = initial_y + 48
        initial_y = initial_y + 48
Enter fullscreen mode Exit fullscreen mode

Here we are iterating through the 3 latest followers we have and save their username and bio, writing them on the base image we have using the image_editable.text command and after every line, we are updating the y-axis of the cursor.

edited_temp = tempfile.NamedTemporaryFile(suffix=".png")
img.save(edited_temp.name)
upload_api.update_profile_banner(edited_temp.name)
return followers
Enter fullscreen mode Exit fullscreen mode

Step 5 Python Code

Making the application run once every two minutes.
Open a new terminal and just type

deta cron set "2 minutes"

Yup, it is that simple to run a cron job in Deta.

Woohoo, congratulations on making it to the end of the post.

Feel free to tag me on Twitter once it's done I will try to be your first tester, you can find me @gillarohith

Also please comment down if you get stuck anywhere or you can reach me on Twitter, my DMs are open.

Thanks

Thanks,
Rohith Gilla

💖 💪 🙅 🚩
gillarohith
Rohith Gilla

Posted on June 12, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related