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 🙌🏻
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
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
- Deta Account
- 10-15 minutes of your time 😌
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 2 - Name your application
Step 3 - Consumer keys and secret
This is an important one, you will be getting your keys and tokens so please note them.
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 5 - Access token and access secret
The step y'all have been waiting for getting the tokens.
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
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
For Windows
iwr https://get.deta.dev/cli.ps1 -useb | iex
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
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
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"
create a .detaignore
file and add !.env
to it.
Deta ignores .env
file by default.
Step 4 Python 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
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
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
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())
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
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
@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)
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
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
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,
Rohith Gilla