modular reimplementation from scratch
This commit is contained in:
parent
cee043b6d2
commit
844209e7b6
1 changed files with 149 additions and 299 deletions
|
|
@ -7,327 +7,177 @@ import argparse
|
||||||
import datetime
|
import datetime
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
|
|
||||||
#import tweepy
|
|
||||||
|
|
||||||
|
|
||||||
class AppKeyError(Exception):
|
|
||||||
def __init__(self, msg):
|
|
||||||
self.msg = msg
|
|
||||||
|
|
||||||
|
|
||||||
def write(data, stream = sys.stdout):
|
|
||||||
"""
|
|
||||||
Write "data" on the given stream, then flush, silently handling broken pipes.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
stream.write(data.encode(locale.getpreferredencoding(True) or "utf-8"))
|
|
||||||
stream.flush()
|
|
||||||
|
|
||||||
# Silently handle broken pipes
|
|
||||||
except IOError:
|
|
||||||
try:
|
|
||||||
stream.close()
|
|
||||||
except IOError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def readline( stream_in ):
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
line = stream_in.readline().decode(stream_in.encoding or locale.getpreferredencoding(True)).strip()
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
continue
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
break
|
|
||||||
if not line:
|
|
||||||
break
|
|
||||||
yield line
|
|
||||||
|
|
||||||
raise StopIteration
|
|
||||||
|
|
||||||
|
|
||||||
def setup_counter( data, asked ):
|
|
||||||
assert(asked.counter)
|
|
||||||
|
|
||||||
# unroll everything
|
|
||||||
data = list(data)
|
|
||||||
nb = len(data)
|
|
||||||
# " int/int"
|
|
||||||
counter_size = len(str(nb).encode("utf-8").decode("utf-8") )*2 + 1 + 1
|
|
||||||
|
|
||||||
for i,line in enumerate(data):
|
|
||||||
counter = u" %i/%i" % (i+1,nb)
|
|
||||||
curmax = asked.max_len - len(counter)
|
|
||||||
if len(line) > curmax:
|
|
||||||
if asked.ignore:
|
|
||||||
data[i] = line
|
|
||||||
elif asked.trim:
|
|
||||||
data[i] = line[:curmax]
|
|
||||||
data[i] = data[i] + counter
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def setup_hem(data, asked):
|
|
||||||
for line in data:
|
|
||||||
if len(line) > asked.max_len:
|
|
||||||
if asked.ignore:
|
|
||||||
pass
|
|
||||||
elif asked.trim:
|
|
||||||
line = line[:asked.max_len]
|
|
||||||
|
|
||||||
yield line
|
|
||||||
|
|
||||||
|
|
||||||
def setup( data, asked ):
|
|
||||||
if asked.counter:
|
|
||||||
f = setup_counter
|
|
||||||
else:
|
|
||||||
f = setup_hem
|
|
||||||
|
|
||||||
for line in f(data, asked):
|
|
||||||
yield line
|
|
||||||
|
|
||||||
|
|
||||||
def operate( func, *args ):
|
|
||||||
for line in func( readline(sys.stdin), *args ):
|
|
||||||
write(line)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# STDOUT API
|
# LIBRARY
|
||||||
#
|
#
|
||||||
|
|
||||||
def on_stdout( data, asked, endline="\n" ):
|
|
||||||
lines = setup(data, asked)
|
|
||||||
# You can do something on the whole set of lines if you want.
|
|
||||||
|
|
||||||
for i,line in enumerate(lines):
|
logger = logging.getLogger("forthlift")
|
||||||
if endline:
|
|
||||||
if line[-1] != endline:
|
|
||||||
line += endline
|
|
||||||
|
|
||||||
if asked.independent:
|
|
||||||
yield line
|
|
||||||
else:
|
|
||||||
l = u"%i %s" % (i,line)
|
|
||||||
yield l
|
|
||||||
|
|
||||||
|
|
||||||
#
|
class stream:
|
||||||
# TWITTER API
|
class Stream:
|
||||||
#
|
def __call__(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def on_twitter( data, api, asked, endline="\n" ):
|
class stdin(Stream):
|
||||||
lines = setup(data, asked)
|
def __call__(self):
|
||||||
|
return sys.stdin
|
||||||
|
|
||||||
prev_status_id = None
|
class consume:
|
||||||
|
class Consume:
|
||||||
|
def __call__(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
images = asked.twitter_images
|
class lines(Consume):
|
||||||
if images:
|
def __call__(self, stream):
|
||||||
images.reverse()
|
return stream.readlines()
|
||||||
|
|
||||||
for line in lines:
|
class format:
|
||||||
if images:
|
class Format:
|
||||||
img = images.pop()
|
def __call__(self, lines):
|
||||||
else:
|
raise NotImplementedError
|
||||||
img = None
|
|
||||||
|
|
||||||
if img:
|
class as_is(Format):
|
||||||
# API.update_with_media(filename[, status][, in_reply_to_status_id][, lat][, long][, source][, place_id][, file])
|
def __call__(self, lines):
|
||||||
status = api.update_with_media(img, line, prev_status_id)
|
return lines
|
||||||
else:
|
|
||||||
# API.update_status(status[, in_reply_to_status_id][, lat][, long][, source][, place_id])
|
|
||||||
status = api.update_status(line, prev_status_id)
|
|
||||||
|
|
||||||
if asked.independent:
|
class lift:
|
||||||
prev_status_id = None
|
class Lift:
|
||||||
else:
|
def __call__(self, items):
|
||||||
prev_status_id = status.id
|
raise NotImplementedError
|
||||||
|
|
||||||
yield status.text + endline
|
class stdout(Lift):
|
||||||
|
def __call__(self, items):
|
||||||
|
for item in items:
|
||||||
|
print(item, end="")
|
||||||
|
|
||||||
|
|
||||||
def setup_twitter(configfile="twitter.conf"):
|
class Forthlifter:
|
||||||
|
def __init__(self,
|
||||||
|
consumer = consume.lines(),
|
||||||
|
streamers = [stream.stdin()],
|
||||||
|
formatters = [format.as_is()],
|
||||||
|
lifters = [lift.stdout()],
|
||||||
|
):
|
||||||
|
self.consumer = consumer
|
||||||
|
logger.debug(f"├ consumer: {type(consumer).__name__}")
|
||||||
|
|
||||||
config = ConfigParser.RawConfigParser()
|
if not isinstance(streamers, list):
|
||||||
|
streamers = [streamers]
|
||||||
|
self.streamers = streamers
|
||||||
|
logger.debug(f"├ streamers: {', '.join(type(i).__name__ for i in self.streamers)}")
|
||||||
|
|
||||||
# Authenticate the application.
|
if not isinstance(formatters, list):
|
||||||
config.read(configfile)
|
formatters = [formatters]
|
||||||
|
self.formatters = formatters
|
||||||
|
logger.debug(f"├ formatters: {', '.join(type(i).__name__ for i in self.formatters)}")
|
||||||
|
|
||||||
if not config.has_section("App"):
|
if not isinstance(lifters, list):
|
||||||
raise AppKeyError("ERROR: did not found application keys, ask your distribution maintainer or get keys from Twitter first.")
|
lifters = [lifters]
|
||||||
|
self.lifters = lifters
|
||||||
|
logger.debug(f"└lifters: {', '.join(type(i).__name__ for i in self.lifters)}")
|
||||||
|
|
||||||
app_key = config.get("App","app_key")
|
def consume(self):
|
||||||
app_secret = config.get("App","app_key_secret")
|
logger.debug("│ ├ consume")
|
||||||
|
lines = []
|
||||||
|
for streamer in self.streamers:
|
||||||
|
logger.debug(f"│ │ ├ {type(self.consumer).__name__}({type(streamer).__name__})")
|
||||||
|
# Concatenate
|
||||||
|
lines += self.consumer(streamer())
|
||||||
|
logger.debug(f"│ │ ├ {len(lines)} lines")
|
||||||
|
logger.debug("│ │ └OK")
|
||||||
|
return lines
|
||||||
|
|
||||||
# auth = tweepy.OAuthHandler(app_key, app_secret)
|
def format(self, lines):
|
||||||
|
logger.debug(f"│ ├ format {len(lines)} lines")
|
||||||
|
items = lines
|
||||||
|
for formatter in self.formatters:
|
||||||
|
logger.debug(f"│ │ ├ {type(formatter).__name__}")
|
||||||
|
# Replace
|
||||||
|
items = formatter(items)
|
||||||
|
logger.debug(f"│ │ ├ {len(items)} items")
|
||||||
|
logger.debug(f"│ │ └OK {len(items)} items")
|
||||||
|
return items
|
||||||
|
|
||||||
|
def lift(self, items):
|
||||||
|
logger.debug(f"│ ├ lift {len(items)} items")
|
||||||
|
for lifter in self.lifters:
|
||||||
|
logger.debug(f"│ │ ├ {type(lifter).__name__}")
|
||||||
|
# Call
|
||||||
|
lifter(items)
|
||||||
|
logger.debug("│ │ └OK")
|
||||||
|
|
||||||
# Authenticate the user.
|
def __call__(self):
|
||||||
# auth_url = auth.get_authorization_url()
|
logger.debug("├ call")
|
||||||
# print("Copy and paste this URL in your browser while your are logged in the twitter account where you want to post.")
|
self.lift(self.format(self.consume()))
|
||||||
# print("Authorization URL: " + auth_url)
|
logger.debug("│ └OK")
|
||||||
|
|
||||||
# print("Then paste the Personal Identification Number given by Twitter:")
|
|
||||||
# verifier = raw_input('PIN: ').strip()
|
|
||||||
# token = auth.get_access_token(verifier)
|
|
||||||
|
|
||||||
# Authenticate and get the user name.
|
|
||||||
# token_key, token_secret = token
|
|
||||||
# auth.set_access_token(token_key, token_secret)
|
|
||||||
# api = tweepy.API(auth)
|
|
||||||
# username = api.me().name
|
|
||||||
# print("Authentication successful, ready to post to account: " + username)
|
|
||||||
|
|
||||||
|
|
||||||
# Save authentication tokens.
|
|
||||||
if not config.has_section("Info"):
|
|
||||||
config.add_section('Info')
|
|
||||||
config.set('Info','account', username)
|
|
||||||
config.set('Info','auth_date', datetime.datetime.utcnow().isoformat())
|
|
||||||
|
|
||||||
if not config.has_section("Auth"):
|
|
||||||
config.add_section('Auth')
|
|
||||||
config.set('Auth', 'local_token', token_key)
|
|
||||||
config.set('Auth', 'local_token_secret', token_secret)
|
|
||||||
|
|
||||||
# Writing our configuration file to 'example.cfg'
|
|
||||||
with open(configfile, 'wb') as fd:
|
|
||||||
config.write(fd)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# CLI
|
# CLI
|
||||||
#
|
#
|
||||||
|
|
||||||
|
def classes_of(namespace):
|
||||||
|
name = namespace.__name__[0].upper()+namespace.__name__[1:]
|
||||||
|
itf = getattr(namespace, name)
|
||||||
|
return [cls.__name__ for cls in itf.__subclasses__()]
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
errors = {"NO_ERROR":0, "UNKNOWN_ERROR":1, "NO_SETUP_NEEDED":2, "NO_APP_KEY":10}
|
errors = {"NO_ERROR":0, "UNKNOWN_ERROR":1, "NO_SETUP_NEEDED":2, "NO_APP_KEY":10}
|
||||||
|
|
||||||
usage = "Post text read on the standard input to a website or an API."
|
usage = "Post text read on the standard input to a website or an API."
|
||||||
apis =["stdout", "twitter"] # TODO "http_post", "http_get",
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser( description=usage,
|
parser = argparse.ArgumentParser( description=usage,
|
||||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||||
|
streamers = classes_of(stream)
|
||||||
|
parser.add_argument("-s", "--stream",
|
||||||
|
choices = streamers,
|
||||||
|
metavar = "STREAMER(S)",
|
||||||
|
default = ["stdin"],
|
||||||
|
action="append",
|
||||||
|
help="Where to get items.")
|
||||||
|
|
||||||
parser.add_argument("-a", "--api", choices=apis, default="stdout",
|
consumers = classes_of(consume)
|
||||||
help="Name of the API to use.")
|
parser.add_argument("-c", "--consume",
|
||||||
|
choices = consumers,
|
||||||
|
metavar = "CONSUMER",
|
||||||
|
default = "lines",
|
||||||
|
help="How to get the content form items.")
|
||||||
|
|
||||||
# Generic options
|
formaters = classes_of(format)
|
||||||
|
parser.add_argument("-f", "--format",
|
||||||
|
choices = formaters,
|
||||||
|
metavar = "FORMATER(S)",
|
||||||
|
default = "as_is",
|
||||||
|
action="append",
|
||||||
|
help="How to format items.")
|
||||||
|
|
||||||
parser.add_argument("-m", "--max-len", metavar="MAXLEN", type=int, default=140,
|
lifters = classes_of(lift)
|
||||||
help="Maximum number of characters in the lines.")
|
parser.add_argument("-l", "--lift",
|
||||||
|
choices = lifters,
|
||||||
|
metavar = "LIFTER(S)",
|
||||||
|
default = ["stdout"],
|
||||||
|
action="append",
|
||||||
|
help="How to lift items.")
|
||||||
|
|
||||||
parser.add_argument("-i", "--ignore", action="store_true",
|
|
||||||
help="Ignore lines that are longer than MAXLEN")
|
|
||||||
|
|
||||||
parser.add_argument("-t", "--trim", action="store_true",
|
|
||||||
help="Trim down lines that are longer than MAXLEN.")
|
|
||||||
|
|
||||||
parser.add_argument("-c", "--counter", action="store_true",
|
|
||||||
help="Add a counter of the form \" x/N\" at the end of the lines, \
|
|
||||||
with N being the number of lines read and x the current index of the line. \
|
|
||||||
NOTE: this necessitate to read all the input before processing it.")
|
|
||||||
|
|
||||||
parser.add_argument("-q", "--quiet", action="store_true",
|
|
||||||
help="Do not print errors and warnings on the standard error output.")
|
|
||||||
|
|
||||||
# TODO option: rate at which to post lines
|
|
||||||
|
|
||||||
# API-dependent options
|
|
||||||
|
|
||||||
parser.add_argument("-s", "--setup", action="store_true",
|
|
||||||
help="Setup the selected API (e.g. authenticate and get authorization to post).")
|
|
||||||
|
|
||||||
parser.add_argument("-d", "--independent", action="store_true",
|
|
||||||
help="Post each item independently from the previous one. \
|
|
||||||
The behaviour of this option depends on the selected API. \
|
|
||||||
For example on Twitter: do not post a line as an answer to the previous one but as a new tweet.")
|
|
||||||
|
|
||||||
# Twitter
|
|
||||||
parser.add_argument("--twitter-images", metavar="FILENAME(S)", nargs="+", type=str,
|
|
||||||
help="Upload each given image files along with the corresponding tweets in the sequence. If there are more images than tweets, they are silently ignored.")
|
|
||||||
|
|
||||||
asked = parser.parse_args()
|
asked = parser.parse_args()
|
||||||
|
|
||||||
# Setup
|
logging.basicConfig()
|
||||||
if asked.setup:
|
logger.setLevel("DEBUG")
|
||||||
configfile = asked.api+".conf"
|
|
||||||
if asked.api == "twitter":
|
|
||||||
try:
|
|
||||||
setup_twitter(configfile)
|
|
||||||
except AppKeyError as e:
|
|
||||||
if not asked.quiet:
|
|
||||||
sys.stderr.write(e.msg)
|
|
||||||
sys.exit(errors["NO_APP_KEY"])
|
|
||||||
except:
|
|
||||||
logging.error(f"Unexpected error:\n{sys.exc_info()[0]}")
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
sys.exit(errors["NO_ERROR"])
|
|
||||||
|
|
||||||
else: # other API
|
logger.debug("Available operators:")
|
||||||
if not asked.quiet:
|
logger.debug(f"├ streamers: {', '.join(streamers)}")
|
||||||
sys.stderr.write("This API does not need setup.")
|
logger.debug(f"├ consumers: {', '.join(consumers)}")
|
||||||
sys.exit(errors["NO_SETUP_NEEDED"])
|
logger.debug(f"├ formaters: {', '.join(formaters)}")
|
||||||
|
logger.debug(f"└ lifters: {', '.join(lifters)}")
|
||||||
|
|
||||||
|
logger.debug("instantiate")
|
||||||
# Consistency checks
|
forthlift = Forthlifter()
|
||||||
|
logger.debug("run")
|
||||||
if asked.ignore and asked.trim:
|
forthlift()
|
||||||
if not asked.quiet:
|
logger.debug("└OK")
|
||||||
sys.stderr.write("WARNING: asking to trim AND to ignore is not logical, I will ignore.")
|
|
||||||
assert( not (asked.ignore and asked.trim) )
|
|
||||||
|
|
||||||
if asked.twitter_images:
|
|
||||||
if not asked.api == "twitter":
|
|
||||||
if not asked.quiet:
|
|
||||||
sys.stderr.write("WARNING: asking to upload images on twitter while not using the twitter API is not logical, I will ignore.")
|
|
||||||
assert( not (asked.twitter_images and not asked.api=="twitter") )
|
|
||||||
|
|
||||||
else: # Test readable images
|
|
||||||
cannot=[]
|
|
||||||
for img in asked.twitter_images:
|
|
||||||
try:
|
|
||||||
with open(img) as f:
|
|
||||||
f.read()
|
|
||||||
except OSError:
|
|
||||||
cannot.append(img)
|
|
||||||
if cannot:
|
|
||||||
logger.error("Cannot open the following image files, I will not continue: ")
|
|
||||||
for img in cannot:
|
|
||||||
logger.error(img)
|
|
||||||
sys.exit(5) # I/O Error
|
|
||||||
|
|
||||||
# APIs
|
|
||||||
|
|
||||||
if asked.api == "stdout":
|
|
||||||
|
|
||||||
operate( on_stdout, asked )
|
|
||||||
|
|
||||||
|
|
||||||
elif asked.api == "twitter":
|
|
||||||
|
|
||||||
# Authenticate
|
|
||||||
config = ConfigParser.RawConfigParser()
|
|
||||||
config.read('twitter.conf')
|
|
||||||
|
|
||||||
app_key = config.get("App","app_key")
|
|
||||||
app_secret = config.get("App","app_key_secret")
|
|
||||||
|
|
||||||
try:
|
|
||||||
verifier_code = config.get("Auth","code")
|
|
||||||
except:
|
|
||||||
access_token = config.get("Auth","local_token")
|
|
||||||
access_token_secret = config.get("Auth","local_token_secret")
|
|
||||||
|
|
||||||
# auth = tweepy.OAuthHandler(app_key, app_secret, "https://api.twitter.com/1.1/")
|
|
||||||
# auth.set_access_token(access_token, access_token_secret)
|
|
||||||
|
|
||||||
# api = tweepy.API(auth)
|
|
||||||
|
|
||||||
# # Post
|
|
||||||
# operate( on_twitter, api, asked )
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue