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.
"social_env": { "packages": ["requests", "jstyleson", "xxhash", "tweepy"] }
$ nvp pyenv setup social_env
"tweet": { "custom_python_env": "social_env", "cmd": "${PYTHON} ${PROJECT_ROOT_DIR}/nvh/social/twitter_handler.py", "python_path": ["${PROJECT_ROOT_DIR}", "${NVP_ROOT_DIR}"] }
"""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()
$ nvp run tweet send "Hello manu!" 2022/05/12 14:35:05 [__main__] INFO: Should send a message here.
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): """Setup the Twitter API object""" # 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"] # Authenticate to Twitter 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')
$ 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
status = "This is my first post to Twitter using the API. I am still learning, please be kind :)" self.api.update_status(status=status)
"social_env": { "packages": ["requests", "jstyleson", "xxhash", "requests-oauthlib"] }
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))
$ 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." } }
media_category=amplify
[…and I have no idea what this means yet…])"""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()
run_app
method.""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)
"""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() # Add our component: comp = context.register_component("NervGate", NervGate(context)) context.define_subparsers("main", { 'run': None, }) psr = context.get_parser('main.run') # psr.add_argument("message", type=str, # help="Simple message that we should send") comp.run()
"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}"] }
$ nvp run nvgate
"""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)
"""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))
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))
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)
btn = gui.create_bitmap_button("mail-send", text=" Send") btn.pressed.connect(self.send_message)
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("")
"+components": { "twitter": "nvh.social.twitter_handler" },
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
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)
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("")
$ nvp run nvgate 2022/05/13 10:39:44 [nvp.nvp_context] INFO: Loading dynamic component twitter from module nvh.social.twitter_handler 2022/05/13 10:39:44 [nvh.social.twitter_handler] INFO: Response code: 201 2022/05/13 10:39:44 [nvh.social.twitter_handler] INFO: { "data": { "id": "1525048133083865088", "text": "Hello twitter world! What a wonderfull day it is today to try some twitter automation, isn't it ?" } }
⇒ Okay, this is a pretty large article/dev session already, so i think I should stop it here for the moment. We will get back to this when I get an elevated twitte access to implement support to send images, etc. Meanwhile, Have a nice day everyone! 🖐