# Quick project: twitter api [python]

In this new post, I want to build myself a new component to be able to access/use my twitter accounts from python. I like twitter, it's handy to just quickly share a few words and then forget about it. But, I feel it's a bit of a pain for me to have to open my browser to send something there! 😝 So I would like to be able to use twitter easily directly from a command line! And given my recent journey in my NervProj framework, there is no better place to try to implement that from my perspective.

• Actually I'm not quite sure what I can do exactly with the twitter API, but let's just do something simple for a start: Just trying to send a message from your account. Should be pretty standard, right ? 😊
• ⇒ So let me try to follow those notes…
• Next question I'm wondering is: how would I set the permissions to read or post anything ?
• ⇒ Okay, so I can create the access token for my twitter user account on the “keys & token” tab of my app:

• ⇒ This is not clearly explaining how to setup the permissions 😅, but anyway, I found that first I have to setup the authentication methods for the app: that's were we decide if the app needs read only/read write/read write with direct message permission,
• And only after that we generate the access token for our twitter user ⇒ should reflect the correct permissions then.
• Note: I also activated OAuth 2.0 support in my app, so I got a client_id and a client_secret, but not sure yet what I will do with these, we'll see…
• Another thing I find a bit puzzling for the moment are those callback URLS (you must provide one when setting up the app authentication): I simply used my wiki website address for now: https://wiki.nervtech.org/ but I have no idea yet why this is needed.
• Okay, so, in theory I should now be able to use tweepy to send a post ? [😆]
• ⇒ Let's try to prepare a minimal component for that:
• Defining a new social_env:
    "social_env": {
"packages": ["requests", "jstyleson", "xxhash", "tweepy"]
}
• And installing that env:
$nvp pyenv setup social_env • Also defining a new script for the new twitter component:  "tweet": { "custom_python_env": "social_env", "cmd": "${PYTHON} ${PROJECT_ROOT_DIR}/nvh/social/twitter_handler.py", "python_path": ["${PROJECT_ROOT_DIR}", "${NVP_ROOT_DIR}"] } • And here is the initial skeleton component: """Module for TwitterHandler class definition""" import logging from nvp.nvp_context import NVPContext from nvp.nvp_component import NVPComponent logger = logging.getLogger(__name__) class TwitterHandler(NVPComponent): """TwitterHandler component class""" def __init__(self, ctx: NVPContext): """class constructor""" NVPComponent.__init__(self, ctx) def process_command(self, cmd): """Check if this component can process the given command""" if cmd == 'send': logger.info("Should send a message here.") return True return False if __name__ == "__main__": # Create the context: context = NVPContext() # Add our component: comp = context.register_component("TwitterHandler", TwitterHandler(context)) context.define_subparsers("main", { 'send': None, }) psr = context.get_parser('main.send') psr.add_argument("message", type=str, help="Simple message that we should send") comp.run()  • Running this component works as desired (ie. doing nothing for now): $ nvp run tweet send "Hello manu!"
2022/05/12 14:35:05 [__main__] INFO: Should send a message here.
• Now I have just added a function called setup_api() where we check if we can properly establish a connection to the twitter API:
    def process_command(self, cmd):
"""Check if this component can process the given command"""

if cmd == 'send':
logger.info("Should send a message here.")
self.setup_api()
return True

return False

def setup_api(self):

# We read the key/secret from our config:
api_key = self.config["api_key"]
api_secrets = self.config["api_key_secret"]
access_token = self.config["access_token"]
access_secret = self.config["access_token_secret"]

auth = tweepy.OAuthHandler(api_key, api_secrets)
auth.set_access_token(access_token, access_secret)

self.api = tweepy.API(auth)

# try:
self.api.verify_credentials()
logger.info('Successful Authentication')
# except:
#     logger.info('Failed authentication')
• Let's see what we get:
$nvp run tweet send "hello world" 2022/05/12 14:38:19 [__main__] INFO: Should send a message here. Traceback (most recent call last): File "D:\Projects\NervHome\nvh\social\twitter_handler.py", line 68, in <module> comp.run() File "D:\Projects\NervProj\nvp\nvp_component.py", line 69, in run res = self.process_command(cmd) File "D:\Projects\NervHome\nvh\social\twitter_handler.py", line 26, in process_command self.setup_api() File "D:\Projects\NervHome\nvh\social\twitter_handler.py", line 47, in setup_api self.api.verify_credentials() File "D:\Projects\NervProj\.pyenvs\social_env\lib\site-packages\tweepy\api.py", line 46, in wrapper return method(*args, **kwargs) File "D:\Projects\NervProj\.pyenvs\social_env\lib\site-packages\tweepy\api.py", line 2649, in verify_credentials return self.request( File "D:\Projects\NervProj\.pyenvs\social_env\lib\site-packages\tweepy\api.py", line 259, in request raise Forbidden(resp) tweepy.errors.Forbidden: 403 Forbidden 453 - You currently have Essential access which includes access to Twitter API v2 endpoints only. If you need access to this endpoint, you’ll need to apply for Elevated access via the Developer Portal. You can learn more here: https://developer.twitter.com/en/docs/twi tter-api/getting-started/about-twitter-api#v2-access-leve • ⇒ Of course… 😂 • So, let's just say we bypass verifying credentials for now, could we at least send a simple post ? • Okay, so same error if I just try to call update_status as follow:  status = "This is my first post to Twitter using the API. I am still learning, please be kind :)" self.api.update_status(status=status) I tried to look for another package that will support the API v2 in python but could not find anything so far. • Waaaooohh… amazing! It works 🥳! • ⇒ I removed the tweepy and then added the requests-oauthlib package in my env instead:  "social_env": { "packages": ["requests", "jstyleson", "xxhash", "requests-oauthlib"] } • And updated the code with a minimal “send_message” function:  def process_command(self, cmd): """Check if this component can process the given command""" if cmd == 'send': logger.info("Should send a message here.") # self.setup_api() # status = "This is my first post to Twitter using the API. I am still learning, please be kind :)" # self.api.update_status(status=status) msg = self.get_param("message") self.send_message(msg) return True return False def send_message(self, msg): """Send a given message""" # logger.info("Should send message '%s'", msg) # We read the key/secret from our config: api_key = self.config["api_key"] api_secrets = self.config["api_key_secret"] access_token = self.config["access_token"] access_secret = self.config["access_token_secret"] # Make the request oauth = OAuth1Session( api_key, client_secret=api_secrets, resource_owner_key=access_token, resource_owner_secret=access_secret, ) # Be sure to add replace the text of the with the text you wish to Tweet. You can also add parameters # to post polls, quote Tweets, Tweet with reply settings, and Tweet to Super Followers # in addition to other features. payload = {"text": msg} # Making the request response = oauth.post( "https://api.twitter.com/2/tweets", json=payload, ) if response.status_code != 201: raise Exception(f"Request returned an error: {response.status_code} {response.text}") logger.info("Response code: %d", response.status_code) # Saving the response as JSON json_response = response.json() logger.info(json.dumps(json_response, indent=4, sort_keys=True)) • And I could send a message from the command line: $ nvp run tweet send "hello world from python."
2022/05/12 16:01:18 [__main__] INFO: Should send a message here.
2022/05/12 16:01:19 [__main__] INFO: Response code: 201
2022/05/12 16:01:19 [__main__] INFO: {
"data": {
"id": "1524766675698077698",
"text": "hello world from python."
}
}


• And this will appear on my twitter account:

• Yeepeeee! 🥳✌!
• ⇒ let's see how I can integrate that in my component…
• Hmmm.. first things first, it seems this example code is using the API v1.1 so not good for me (?) Let's see if we have something similar in API v2.
• Well, no… 😰 Apparently there is currently no support to import media files in the API version 2.
• ⇒ So, I decided I should not make my life any harder, and just made a request to get an elevated access to the twitter API. We will see how this goes.
• Until my elevated access request above is accepted I think I should not focus that much on the API usage itself.
• But what I could do instead would be to start building a minimal GUI to manage my twitter access.
• Thus, I started with creating a kind of “base skeleton” that I may then use for multiple application, which I called AppBase naturally:
"""Module for AppBase class definition"""

import logging
import sys

from PyQt5.QtWidgets import QApplication

from nvp.nvp_context import NVPContext
from nvp.nvp_component import NVPComponent

from nvh.gui.main_window import MainWindow

logger = logging.getLogger(__name__)

class AppBase(NVPComponent):
"""AppBase component class"""

def __init__(self, ctx: NVPContext, app_name: str):
"""class constructor, should provide an app name that will be used
to select the config element to load."""

NVPComponent.__init__(self, ctx)
self.app_name = app_name
self.config = self.config[app_name]
self.app = None
self.main_window = None

def get_app(self):
"""Retrieve the Qapplication"""
return self.app

def get_config(self):
"""Retrieve the config for this application"""
return self.config

def get_main_window(self):
"""Retrieve the main window of the application"""
return self.main_window

def build_app(self):
"""Build the application."""

# Simple base build of an empty application:
self.app = QApplication(sys.argv)

self.main_window = MainWindow(self)

def run_app(self):
"""Run the application."""

# First we build the app:
self.build_app()

# Display the main window:
self.main_window.show()

# Run the event loop:
self.app.exec()

• In this AppBase I provide the minimal support to setup the application level config, create a MainWindow, and also run the application with the run_app method.
• Then of course I also had to build that generic MainWindow class:
""Module for the main window of the FCITuner application"""

import logging
from PyQt5.QtCore import Qt
from PyQt5 import QtCore
import PyQt5.QtWidgets as qwd

from nvp.nvp_context import NVPContext

logger = logging.getLogger(__name__)

class MainWindow(qwd.QMainWindow):
"""Main Window class for FCITuner application"""

def __init__(self, app):
"""Constructor of the MainWindow class"""
super().__init__()

self.app = app

# Retrieve the app config:
self.config = app.get_config()

# keep ref on the context:
self.ctx = NVPContext.get()

title = self.config["application_title"]
self.setWindowTitle(title)
• As you can see, this main window will already read its application title from the application config provided by the AppBase.
• Then I started to build a specific application, which I called NervGate on top of the AppBase:
"""Module for NervGate class definition"""

import logging

from nvp.nvp_context import NVPContext

from nvh.gui.app_base import AppBase

logger = logging.getLogger(__name__)

class NervGate(AppBase):
"""NervGate component class"""

def __init__(self, ctx: NVPContext):
"""class constructor"""
AppBase.__init__(self, ctx, "nervgate")

def process_command(self, cmd):
"""Check if this component can process the given command"""

if cmd == 'run':
self.run_app()
return True

return False

def build_app(self):
"""Re-implementation of the build function"""
super().build_app()

win = self.get_main_window()
# win.setWindowTitle("NervGate")

win.setFixedSize(800, 600)

if __name__ == "__main__":
# Create the context:
context = NVPContext()

comp = context.register_component("NervGate", NervGate(context))

context.define_subparsers("main", {
'run': None,
})

psr = context.get_parser('main.run')
#                  help="Simple message that we should send")

comp.run()

• Also added a dedicated python env and a new script to start that application:
    "nvgate": {
"custom_python_env": "nvgate_env",
"cmd": "${PYTHON}${PROJECT_ROOT_DIR}/nvh/app/nerv_gate.py run",
"python_path": ["${PROJECT_ROOT_DIR}", "${NVP_ROOT_DIR}"]
}
• And now, as expected I can display an empty window with the desired title by runnign the command:
$nvp run nvgate • Naturally, next I should add a “twitter” panel/widget to send some tweets 😝 • I started with the definition of a DockWidget class to support flexible docking in my app: """Base DockWidget module""" # cf. https://stackoverflow.com/questions/63638969/pyqt5-move-qdockwidget-by-dragging-tab import logging import PyQt5.QtWidgets as qwd # from PyQt5 import QtCore logger = logging.getLogger(__name__) class DockWidget(qwd.QDockWidget): """Re-implementation of Dock widget.""" def __init__(self, title: str): """Constructor for dock widget.""" super().__init__(title) self.title = title self.setMinimumSize(200, 100) self.setTitleBarWidget(qwd.QWidget(self)) self.dockLocationChanged.connect(self.on_dock_location_changed) def on_dock_location_changed(self): """Handle the display/hiding of the tab titles""" main: qwd.QMainWindow = self.parent() all_dock_widgets = main.findChildren(qwd.QDockWidget) for dock_widget in all_dock_widgets: sibling_tabs = main.tabifiedDockWidgets(dock_widget) # If you pull a tab out of a group the other tabs still see it as a sibling while dragging... sibling_tabs = [s for s in sibling_tabs if not s.isFloating()] if len(sibling_tabs) != 0: # Hide title bar # logger.info("Showing title for %s", dock_widget.title) dock_widget.setTitleBarWidget(qwd.QWidget(self)) # dock_widget.titleBarWidget().setFixedHeight(30) else: # Re-enable title bar # logger.info("Hidding title for %s", dock_widget.title) # dock_widget.titleBarWidget().setFixedHeight(20) dock_widget.setTitleBarWidget(None) # def minimumSizeHint(self) -> QtCore.QSize: # """Return min size hint""" # return QtCore.QSize(100, 100)  • And then started to build a minimal TwitterPanel widget: """TwitterPanel widget module""" import logging from PyQt5 import QtCore from PyQt5.QtCore import Qt import PyQt5.QtWidgets as qwd logger = logging.getLogger(__name__) class TwitterPanel(qwd.QWidget): """TwitterPanel class""" def __init__(self): """Constructor of the TwitterPanel class""" super().__init__() lyt = qwd.QVBoxLayout() lyt.setContentsMargins(0, 0, 0, 0) # Add a simple label: label = qwd.QLabel(self) label.setText("This is the twitter panel") label.setAlignment(Qt.AlignCenter) lyt.addWidget(label) self.setLayout(lyt) setattr(self, "sizeHint", lambda: QtCore.QSize(360, 300))  • Then I simply added that new widget in a dock inside the main window on construction:  def build_app(self): """Re-implementation of the build function""" super().build_app() win = self.get_main_window() # win.setWindowTitle("NervGate") # win.setFixedSize(800, 600) dock1 = DockWidget("Twitter") dock1.setWidget(TwitterPanel()) dock1.setAllowedAreas(Qt.AllDockWidgetAreas) win.addDockWidget(Qt.RightDockWidgetArea, dock1) win.setDockOptions(win.GroupedDragging | win.AllowTabbedDocks | win.AllowNestedDocks) win.setTabPosition(Qt.AllDockWidgetAreas, qwd.QTabWidget.North) # Set a size hint on the window: setattr(win, "sizeHint", lambda: QtCore.QSize(600, 300)) • And this is the result I get so far: • ⇒ The resulting panel is detachable, draggable, re-attachable, etc… so, all good so far ✌! • Next, let just add a text area to type in a message and a send button to send the tweet message:  lyt = qwd.QVBoxLayout() lyt.setContentsMargins(5, 5, 5, 5) lyt.setSpacing(5) lyt.addStretch(1) # Add a simple label: label = qwd.QLabel(self) label.setText("Message text:") label.setAlignment(Qt.AlignLeft) lyt.addWidget(label) wid = qwd.QTextEdit() wid.setMaximumHeight(100) lyt.addWidget(wid, stretch=0) row = qwd.QHBoxLayout() row.setContentsMargins(0, 0, 0, 0) row.setSpacing(5) row.addStretch(1) btn = gui.create_bitmap_button("mail-send", text=" Send") row.addWidget(btn) lyt.addLayout(row, stretch=0) self.setLayout(lyt) While I was at it, I also added support to set icons on the “Send” button and for the application itself 😜 • So here is the view we get now: • Now we should handle the user click on the send button • So connecting the button to an action:  btn = gui.create_bitmap_button("mail-send", text=" Send") btn.pressed.connect(self.send_message) • And defining the action:  def send_message(self): """Send the message currently typed in the message edit text""" msg = self.message_edit.toPlainText() logger.info("Should send the message: '%s'", msg) self.message_edit.setText("") • ⇒ This workds correctly. • But to actually send the message, I need access to the twitter_handler I created previously. I could construct/load that component when building the application, but I think I have a better idea on this already: I could instead just prepare a list of “components” that the application may need, and then only lazily load those components when requested with the get_component() method, how about that 🤪?! Let's check if this will work as expected… • So in my NervProj config file I'm now adding a list of “dynamic components” like that:  "+components": { "twitter": "nvh.social.twitter_handler" }, The “+” in front of the “components” key here is just the mechanism I use to notify that the content of this dict, which is read from an additional “user config” file in my case, not just the NervProj “base config” file, should be appended/added to the existing dict instead of simply replacing it. • Next I need the NVPContext to discover those dynamic components when needed:  def get_component(self, cname, do_init=True): """Retrieve a component by name or create it if missing""" proj = self.get_current_project() if proj is not None and proj.has_component(cname): return proj.get_component(cname, do_init) if cname in self.components: comp = self.components[cname] if do_init: comp.initialize() return comp # If the requested component is not found, then it might be a dynamic component: # So we search for it in our config: dyn_comps = self.config.get("components", {}) if cname not in dyn_comps: logger.warning("Cannot find component %s", cname) return None # We have a dyn component module name, so we try to load it: mname = dyn_comps[cname] logger.info("Loading dynamic component %s from module %s", cname, mname) comp_module = import_module(mname) comp = comp_module.create_component(self) # And we register that component now: self.register_component(cname, comp) if do_init: comp.initialize() return comp • Which also means that I need a create_component() function in each component file that I should be able to load dynamically: def create_component(ctx: NVPContext): """Create an instance of the component""" return TwitterHandler(ctx)  • And with that in place, I updated the action for the send button on my gui:  def send_message(self): """Send the message currently typed in the message edit text""" msg = self.message_edit.toPlainText() # logger.info("Should send the message: '%s'", msg) twitter: TwitterHandler = NVPContext.get().get_component('twitter') twitter.send_message(msg) self.message_edit.setText("") • And finally, time to give it a try (crossing fingers 🤞): $ nvp run nvgate
2022/05/13 10:39:44 [nvh.social.twitter_handler] INFO: Response code: 201