diff --git a/src/forthlift/forthlift.py b/src/forthlift/forthlift.py index f66a736..6b2ebbb 100755 --- a/src/forthlift/forthlift.py +++ b/src/forthlift/forthlift.py @@ -7,327 +7,177 @@ import argparse import datetime 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): - if endline: - if line[-1] != endline: - line += endline - - if asked.independent: - yield line - else: - l = u"%i %s" % (i,line) - yield l +logger = logging.getLogger("forthlift") -# -# TWITTER API -# +class stream: + class Stream: + def __call__(self): + raise NotImplementedError -def on_twitter( data, api, asked, endline="\n" ): - lines = setup(data, asked) + class stdin(Stream): + def __call__(self): + return sys.stdin - prev_status_id = None +class consume: + class Consume: + def __call__(self): + raise NotImplementedError - images = asked.twitter_images - if images: - images.reverse() + class lines(Consume): + def __call__(self, stream): + return stream.readlines() - for line in lines: - if images: - img = images.pop() - else: - img = None +class format: + class Format: + def __call__(self, lines): + raise NotImplementedError - if img: - # API.update_with_media(filename[, status][, in_reply_to_status_id][, lat][, long][, source][, place_id][, file]) - status = api.update_with_media(img, line, prev_status_id) - else: - # API.update_status(status[, in_reply_to_status_id][, lat][, long][, source][, place_id]) - status = api.update_status(line, prev_status_id) + class as_is(Format): + def __call__(self, lines): + return lines - if asked.independent: - prev_status_id = None - else: - prev_status_id = status.id +class lift: + class Lift: + def __call__(self, items): + 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. - config.read(configfile) + if not isinstance(formatters, list): + formatters = [formatters] + self.formatters = formatters + logger.debug(f"├ formatters: {', '.join(type(i).__name__ for i in self.formatters)}") - if not config.has_section("App"): - raise AppKeyError("ERROR: did not found application keys, ask your distribution maintainer or get keys from Twitter first.") + if not isinstance(lifters, list): + 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") - app_secret = config.get("App","app_key_secret") + def consume(self): + 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. - # auth_url = auth.get_authorization_url() - # print("Copy and paste this URL in your browser while your are logged in the twitter account where you want to post.") - # print("Authorization URL: " + auth_url) - - # 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) - + def __call__(self): + logger.debug("├ call") + self.lift(self.format(self.consume())) + logger.debug("│ └OK") # # 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(): 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." - apis =["stdout", "twitter"] # TODO "http_post", "http_get", parser = argparse.ArgumentParser( description=usage, 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", - help="Name of the API to use.") + consumers = classes_of(consume) + 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, - help="Maximum number of characters in the lines.") + lifters = classes_of(lift) + 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() - # Setup - if asked.setup: - 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"]) + logging.basicConfig() + logger.setLevel("DEBUG") - else: # other API - if not asked.quiet: - sys.stderr.write("This API does not need setup.") - sys.exit(errors["NO_SETUP_NEEDED"]) + logger.debug("Available operators:") + logger.debug(f"├ streamers: {', '.join(streamers)}") + logger.debug(f"├ consumers: {', '.join(consumers)}") + logger.debug(f"├ formaters: {', '.join(formaters)}") + logger.debug(f"└ lifters: {', '.join(lifters)}") - - # Consistency checks - - if asked.ignore and asked.trim: - if not asked.quiet: - 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 ) + logger.debug("instantiate") + forthlift = Forthlifter() + logger.debug("run") + forthlift() + logger.debug("└OK") if __name__ == "__main__":