Skip to content

Commit

Permalink
owtwitter: adapt to changes in twitter api
Browse files Browse the repository at this point in the history
  • Loading branch information
PrimozGodec committed Mar 25, 2022
1 parent 4a288f2 commit 5b37c41
Show file tree
Hide file tree
Showing 2 changed files with 250 additions and 205 deletions.
247 changes: 88 additions & 159 deletions orangecontrib/text/widgets/owtwitter.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
from typing import List, Optional

from AnyQt.QtCore import Qt
from AnyQt.QtWidgets import QGridLayout, QLabel, QFormLayout
from AnyQt.QtWidgets import QGridLayout, QLabel
from PyQt5.QtWidgets import QPlainTextEdit

from orangewidget.utils.widgetpreview import WidgetPreview
from Orange.widgets import gui
from Orange.widgets.credentials import CredentialManager
from Orange.widgets.settings import Setting
from Orange.widgets.widget import OWWidget, Msg, Output
from Orange.widgets.utils.concurrent import TaskState, ConcurrentWidgetMixin
from tweepy import TooManyRequests

from orangecontrib.text import twitter
from orangecontrib.text.corpus import Corpus
from orangecontrib.text.language_codes import lang2code
from orangecontrib.text.twitter import TwitterAPI
from orangecontrib.text.widgets.utils import (
ComboBox,
ListEdit,
CheckListLayout,
gui_require,
)
from orangecontrib.text.language_codes import code2lang
from orangecontrib.text.twitter import TwitterAPI, SUPPORTED_LANGUAGES, NoAuthorError
from orangecontrib.text.widgets.utils import ComboBox, ListEdit, gui_require


def search(
Expand All @@ -47,7 +42,7 @@ def advance(progress):
collecting=collecting,
callback=advance,
)
elif mode == "authors":
else: # mode == "authors":
return api.search_authors(
max_tweets=max_tweets,
authors=word_list,
Expand All @@ -57,72 +52,10 @@ def advance(progress):


class OWTwitter(OWWidget, ConcurrentWidgetMixin):
class APICredentialsDialog(OWWidget):
name = "Twitter API Credentials"
want_main_area = False
resizing_enabled = False

cm_key = CredentialManager("Twitter API Key")
cm_secret = CredentialManager("Twitter API Secret")

key_input = ""
secret_input = ""

class Error(OWWidget.Error):
invalid_credentials = Msg("This credentials are invalid.")

def __init__(self, parent):
super().__init__()
self.parent = parent
self.credentials = None

form = QFormLayout()
form.setContentsMargins(5, 5, 5, 5)
self.key_edit = gui.lineEdit(
self, self, "key_input", controlWidth=400
)
form.addRow("Key:", self.key_edit)
self.secret_edit = gui.lineEdit(
self, self, "secret_input", controlWidth=400
)
form.addRow("Secret:", self.secret_edit)
self.controlArea.layout().addLayout(form)

self.submit_button = gui.button(
self.controlArea, self, "OK", self.accept
)
self.load_credentials()

def load_credentials(self):
self.key_edit.setText(self.cm_key.key)
self.secret_edit.setText(self.cm_secret.key)

def save_credentials(self):
self.cm_key.key = self.key_input
self.cm_secret.key = self.secret_input

def check_credentials(self):
c = twitter.Credentials(self.key_input, self.secret_input)
if self.credentials != c:
if c.valid:
self.save_credentials()
else:
c = None
self.credentials = c

def accept(self, silent=False):
if not silent:
self.Error.invalid_credentials.clear()
self.check_credentials()
if self.credentials and self.credentials.valid:
self.parent.update_api(self.credentials)
super().accept()
elif not silent:
self.Error.invalid_credentials()

name = "Twitter"
description = "Load tweets from the Twitter API."
icon = "icons/Twitter.svg"
keywords = ["twitter", "tweet"]
priority = 150

class Outputs:
Expand All @@ -131,36 +64,64 @@ class Outputs:
want_main_area = False
resizing_enabled = False

class Warning(OWWidget.Warning):
no_text_fields = Msg("Text features are inferred when none selected.")
class Info(OWWidget.Information):
nut_enough_tweets = Msg(
"Downloaded fewer tweets than requested, since not enough tweets or rate limit reached"
)

class Error(OWWidget.Error):
api_error = Msg("Api error ({})")
rate_limit = Msg("Rate limit exceeded. Please try again later.")
empty_authors = Msg("Please provide some authors.")
wrong_authors = Msg("Query does not match Twitter user handle.")
key_missing = Msg("Please provide a valid API key to get the data.")
api_error = Msg("Api error: {}")
empty_query = Msg("Please provide {}.")
key_missing = Msg("Please provide a valid API token.")
wrong_author = Msg("Author '{}' does not exist.")

CONTENT, AUTHOR = 0, 1
MODES = ["Content", "Author"]
word_list = Setting([])
mode = Setting(0)
limited_search = Setting(True)
max_tweets = Setting(100)
language = Setting(None)
allow_retweets = Setting(False)
collecting = Setting(False)
word_list: List = Setting([])
mode: int = Setting(0)
limited_search: bool = Setting(True)
max_tweets: int = Setting(100)
language: Optional[str] = Setting(None)
allow_retweets: bool = Setting(False)
collecting: bool = Setting(False)

attributes = [f.name for f in twitter.TwitterAPI.string_attributes]
text_includes = Setting([f.name for f in twitter.TwitterAPI.text_features])
class APICredentialsDialog(OWWidget):
name = "Twitter API Credentials"
want_main_area = False
resizing_enabled = False

cm_key = CredentialManager("Twitter Bearer Token")

def __init__(self, parent):
super().__init__()
self.parent = parent

box = gui.vBox(self.controlArea, "Bearer Token")
self.key_edit = QPlainTextEdit()
box.layout().addWidget(self.key_edit)

self.submit_button = gui.button(self.buttonsArea, self, "OK", self.accept)
self.load_credentials()

def load_credentials(self):
self.key_edit.setPlainText(self.cm_key.key)

def save_credentials(self):
self.cm_key.key = self.key_edit.toPlainText()

def accept(self):
token = self.key_edit.toPlainText()
if token:
self.save_credentials()
self.parent.update_api(token)
super().accept()

def __init__(self):
OWWidget.__init__(self)
ConcurrentWidgetMixin.__init__(self)
self.api = None
self.corpus = None
self.api_dlg = self.APICredentialsDialog(self)
self.api_dlg.accept(silent=True)
self.api_dlg.accept()

# Set API key button
gui.button(
Expand Down Expand Up @@ -212,11 +173,8 @@ def add_row(label, items):
)

# Language
self.language_combo = ComboBox(
self,
"language",
items=(("Any", None),) + tuple(sorted(lang2code.items())),
)
langs = (("Any", None),) + tuple((code2lang[l], l) for l in SUPPORTED_LANGUAGES)
self.language_combo = ComboBox(self, "language", items=langs)
add_row("Language:", self.language_combo)

# Max tweets
Expand All @@ -243,21 +201,9 @@ def add_row(label, items):

query_box.layout().addLayout(layout)

self.controlArea.layout().addWidget(
CheckListLayout(
"Text includes",
self,
"text_includes",
self.attributes,
cols=2,
callback=self.set_text_features,
)
)

# Buttons
self.button_box = gui.hBox(self.controlArea)
self.search_button = gui.button(
self.button_box,
self.buttonsArea,
self,
"Search",
self.start_stop,
Expand Down Expand Up @@ -287,39 +233,27 @@ def start_stop(self):
@gui_require("api", "key_missing")
def run_search(self):
self.Error.clear()
self.Info.nut_enough_tweets.clear()
self.search()

def search(self):
max_tweets = self.max_tweets if self.limited_search else 0

if self.mode == self.CONTENT:
self.start(
search,
self.api,
max_tweets,
self.word_list,
self.collecting,
self.language,
self.allow_retweets,
"content",
)
else:
if not self.word_list:
self.Error.empty_authors()
return None
if not any(a.startswith("@") for a in self.word_list):
self.Error.wrong_authors()
return None
self.start(
search,
self.api,
max_tweets,
self.word_list,
self.collecting,
None,
None,
"authors",
)
max_tweets = self.max_tweets if self.limited_search else None
content = self.mode == self.CONTENT
if not self.word_list:
self.Error.empty_query("keywords" if content else "authors")
self.Outputs.corpus.send(None)
return

self.start(
search,
self.api,
max_tweets,
self.word_list,
self.collecting,
self.language if content else None,
self.allow_retweets if content else None,
"content" if content else "authors",
)
self.search_button.setText("Stop")

def update_api(self, key):
Expand All @@ -329,35 +263,30 @@ def update_api(self, key):
else:
self.api = None

def on_done(self, result):
def on_done(self, result_corpus):
self.search_button.setText("Search")
self.corpus = result
self.set_text_features()
if (
result_corpus is None # probably because of rate error at beginning
# or fewer tweets than expected
or self.mode == self.CONTENT
and len(result_corpus) < self.max_tweets
or self.mode == self.AUTHOR
# for authors, we expect self.max_tweets for each author
and len(result_corpus) < self.max_tweets * len(self.word_list)
):
self.Info.nut_enough_tweets()
self.Outputs.corpus.send(result_corpus)

def on_exception(self, ex):
self.search_button.setText("Search")
if isinstance(ex, TooManyRequests):
self.Error.rate_limit()
if isinstance(ex, NoAuthorError):
self.Error.wrong_author(str(ex))
else:
self.Error.api_error(str(ex))

def on_partial_result(self, _):
pass

def set_text_features(self):
self.Warning.no_text_fields.clear()
if not self.text_includes:
self.Warning.no_text_fields()

if self.corpus is not None:
vars_ = [
var
for var in self.corpus.domain.metas
if var.name in self.text_includes
]
self.corpus.set_text_features(vars_ or None)
self.Outputs.corpus.send(self.corpus)

@gui_require("api", "key_missing")
def send_report(self):
for task in self.api.search_history:
Expand Down
Loading

0 comments on commit 5b37c41

Please sign in to comment.