def cli(ctx, config, verbose): """Decentralised, minimalist microblogging service for hackers.""" init_logging(debug=verbose) if ctx.invoked_subcommand == "quickstart": return # Skip initializing config file try: if config: conf = Config.from_file(config) else: conf = Config.discover() except ValueError as e: if "Error in config file." in str(e): click.echo( "✗ Please correct the errors mentioned above an run twtxt again." ) else: click.echo( "✗ Config file not found or not readable. You may want to run twtxt quickstart." ) sys.exit() ctx.default_map = conf.build_default_map() ctx.obj = {'conf': conf}
def quickstart(ctx): """Quickstart wizard for setting up twtxt.""" width = click.get_terminal_size()[0] width = width if width <= 79 else 79 click.secho("twtxt - quickstart", fg="cyan") click.secho("==================", fg="cyan") click.echo() help = "This wizard will generate a basic configuration file for twtxt with all mandatory options set. " \ "Have a look at the README.rst to get information about the other available options and their meaning." click.echo(textwrap.fill(help, width)) click.echo() nick = click.prompt("➤ Please enter your desired nick", default=os.environ.get("USER", "")) twtfile = click.prompt("➤ Please enter the desired location for your twtxt file", "~/twtxt.txt", type=click.Path()) click.echo() add_news = click.confirm("➤ Do you want to follow the twtxt news feed?", default=True) conf = Config(None) conf.create_config(nick, twtfile, add_news) open(os.path.expanduser(twtfile), 'a').close() click.echo() click.echo("✓ Created config file at '{}'.".format(click.format_filename(conf.config_file)))
def quickstart(ctx): """Quickstart wizard for setting up twtxt.""" width = click.get_terminal_size()[0] width = width if width <= 79 else 79 click.secho("twtxt - quickstart", fg="cyan") click.secho("==================", fg="cyan") click.echo() help = "This wizard will generate a basic configuration file for twtxt with all mandatory options set. " \ "Have a look at the README.rst to get information about the other available options and their meaning." click.echo(textwrap.fill(help, width)) click.echo() nick = click.prompt("➤ Please enter your desired nick", default=os.environ.get("USER", "")) twtfile = click.prompt( "➤ Please enter the desired location for your twtxt file", "~/twtxt.txt", type=click.Path()) click.echo() add_news = click.confirm("➤ Do you want to follow the twtxt news feed?", default=True) conf = Config(None) conf.create_config(nick, twtfile, add_news) open(os.path.expanduser(twtfile), 'a').close() click.echo() click.echo("✓ Created config file at '{}'.".format( click.format_filename(conf.config_file)))
def test_build_default_map(): empty_cfg = configparser.ConfigParser() empty_conf = Config("foobar", empty_cfg) default_map = { "following": { "check": empty_conf.check_following, "timeout": empty_conf.timeout, "porcelain": empty_conf.porcelain, }, "tweet": { "twtfile": empty_conf.twtfile, }, "timeline": { "pager": empty_conf.use_pager, "cache": empty_conf.use_cache, "limit": empty_conf.limit_timeline, "timeout": empty_conf.timeout, "sorting": empty_conf.sorting, "porcelain": empty_conf.porcelain, "twtfile": empty_conf.twtfile, }, "view": { "pager": empty_conf.use_pager, "cache": empty_conf.use_cache, "limit": empty_conf.limit_timeline, "timeout": empty_conf.timeout, "sorting": empty_conf.sorting, "porcelain": empty_conf.porcelain, } } assert empty_conf.build_default_map() == default_map
def test_from_file(config_dir): with pytest.raises(ValueError) as e: Config.from_file("invalid") assert "Config file not found." in str(e.value) with open(str(config_dir.join("empty")), "a") as fh: fh.write("XXX") with pytest.raises(ValueError) as e: Config.from_file(str(config_dir.join("empty"))) assert "Config file is invalid." in str(e.value) conf = Config.from_file(str(config_dir.join(Config.config_name))) check_cfg(conf)
def test_check_config_file_sanity(capsys, config_dir): with pytest.raises(ValueError) as e: Config.from_file(str(config_dir.join("config_sanity"))) assert "Error in config file." in str(e.value) out, err = capsys.readouterr() for line in ["✗ Config error on limit_timeline - invalid literal for int() with base 10: '2t0'", "✗ Config error on check_following - Not a boolean: TTrue", "✗ Config error on porcelain - Not a boolean: Faltse", "✗ Config error on disclose_identity - Not a boolean: Ftalse", "✗ Config error on timeout - could not convert string to float: '5t.0'", "✗ Config error on use_pager - Not a boolean: Falste", "✗ Config error on use_cache - Not a boolean: Trute"]: assert line in out
def test_check_config_file_sanity(capsys, config_dir): with pytest.raises(ValueError) as e: Config.from_file(str(config_dir.join("config_sanity"))) assert "Error in config file." in str(e.value) out, err = capsys.readouterr() for line in [ "✗ Config error on limit_timeline - invalid literal for int() with base 10: '2t0'", "✗ Config error on check_following - Not a boolean: TTrue", "✗ Config error on porcelain - Not a boolean: Faltse", "✗ Config error on disclose_identity - Not a boolean: Ftalse", "✗ Config error on timeout - could not convert string to float: '5t.0'", "✗ Config error on use_pager - Not a boolean: Falste", "✗ Config error on use_cache - Not a boolean: Trute" ]: assert line in out
def check_if_following(follower): config = Config.discover() if config.get_source_by_nick(follower): return True return False
def test_create_config(config_dir): config_dir_old = Config.config_dir Config.config_dir = str(config_dir.join("new")) conf_w = Config.create_config("bar", "batz.txt", True) conf_r = Config.discover() assert conf_r.nick == "bar" assert conf_r.twtfile == "batz.txt" assert conf_r.following[0].nick == "twtxt" assert conf_r.following[0].url == "https://buckket.org/twtxt_news.txt" assert set(conf_r.options.keys()) == {"nick", "twtfile"} conf_r.cfg.remove_section("twtxt") assert conf_r.options == {} conf_r.cfg.remove_section("following") assert conf_r.following == [] Config.config_dir = config_dir_old
def cli(ctx, config, verbose): """Decentralised, minimalist microblogging service for hackers.""" init_logging(debug=verbose) try: if config: conf = Config.from_file(config) else: conf = Config.discover() except ValueError: click.echo("Error loading config file.") if not config: if click.confirm("Do you want to run the twtxt quickstart wizard?", abort=True): pass ctx.default_map = conf.build_default_map() ctx.obj = {'conf': conf}
def cli(ctx, config, verbose): """Decentralised, minimalist microblogging service for hackers.""" init_logging(debug=verbose) if ctx.invoked_subcommand == "quickstart": return try: if config: conf = Config.from_file(config) else: conf = Config.discover() except ValueError: click.echo("✗ Config file not found or not readable. You may want to run twtxt quickstart.") sys.exit() ctx.default_map = conf.build_default_map() ctx.obj = {'conf': conf}
def test_create_config(config_dir): config_dir_old = Config.config_dir Config.config_dir = str(config_dir.join("new")) conf_w = Config.create_config("bar", "batz.txt", False, True) conf_r = Config.discover() assert conf_r.nick == "bar" assert conf_r.twtfile == "batz.txt" assert conf_r.character_limit == 140 assert conf_r.following[0].nick == "twtxt" assert conf_r.following[0].url == "https://buckket.org/twtxt_news.txt" assert set(conf_r.options.keys()) == {"nick", "twtfile", "disclose_identity", "character_limit"} conf_r.cfg.remove_section("twtxt") assert conf_r.options == {} conf_r.cfg.remove_section("following") assert conf_r.following == [] Config.config_dir = config_dir_old
def cli(ctx, config, verbose): """Decentralised, minimalist microblogging service for hackers.""" init_logging(debug=verbose) if ctx.invoked_subcommand == "quickstart": return try: if config: conf = Config.from_file(config) else: conf = Config.discover() except ValueError: click.echo( "✗ Config file not found or not readable. You may want to run twtxt quickstart." ) sys.exit() ctx.default_map = conf.build_default_map() ctx.obj = {'conf': conf}
def cli(ctx, config, verbose): """Decentralised, minimalist microblogging service for hackers.""" init_logging(debug=verbose) if ctx.invoked_subcommand == "quickstart": return # Skip initializing config file try: if config: conf = Config.from_file(config) else: conf = Config.discover() except ValueError as e: if "Error in config file." in str(e): click.echo("✗ Please correct the errors mentioned above an run twtxt again.") else: click.echo("✗ Config file not found or not readable. You may want to run twtxt quickstart.") sys.exit() ctx.default_map = conf.build_default_map() ctx.obj = {'conf': conf}
def quickstart(): """Quickstart wizard for setting up twtxt.""" width = click.get_terminal_size()[0] width = width if width <= 79 else 79 click.secho("twtxt - quickstart", fg="cyan") click.secho("==================", fg="cyan") click.echo() help_text = "This wizard will generate a basic configuration file for twtxt with all mandatory options set. " \ "You can change all of these later with either twtxt itself or by editing the config file manually. " \ "Have a look at the docs to get information about the other available options and their meaning." click.echo(textwrap.fill(help_text, width)) click.echo() nick = click.prompt("➤ Please enter your desired nick", default=os.environ.get("USER", "")) def overwrite_check(path): if os.path.isfile(path): click.confirm("➤ '{0}' already exists. Overwrite?".format(path), abort=True) cfgfile = click.prompt("➤ Please enter the desired location for your config file", os.path.join(Config.config_dir, Config.config_name), type=click.Path(readable=True, writable=True, file_okay=True)) cfgfile = os.path.expanduser(cfgfile) overwrite_check(cfgfile) twtfile = click.prompt("➤ Please enter the desired location for your twtxt file", os.path.expanduser("~/twtxt.txt"), type=click.Path(readable=True, writable=True, file_okay=True)) twtfile = os.path.expanduser(twtfile) overwrite_check(twtfile) twturl = click.prompt("➤ Please enter the URL your twtxt file will be accessible from", default="https://example.org/twtxt.txt") disclose_identity = click.confirm("➤ Do you want to disclose your identity? Your nick and URL will be shared when " "making HTTP requests", default=False) click.echo() add_news = click.confirm("➤ Do you want to follow the twtxt news feed?", default=True) conf = Config.create_config(cfgfile, nick, twtfile, twturl, disclose_identity, add_news) twtfile_dir = os.path.dirname(twtfile) if not os.path.exists(twtfile_dir): os.makedirs(twtfile_dir) open(twtfile, "a").close() click.echo() click.echo("✓ Created config file at '{0}'.".format(click.format_filename(conf.config_file))) click.echo("✓ Created twtxt file at '{0}'.".format(click.format_filename(twtfile)))
def quickstart(): """Quickstart wizard for setting up twtxt.""" width = click.get_terminal_size()[0] width = width if width <= 79 else 79 click.secho("twtxt - quickstart", fg="cyan") click.secho("==================", fg="cyan") click.echo() help_text = "This wizard will generate a basic configuration file for twtxt with all mandatory options set. " \ "You can change all of these later with either twtxt itself or by editing the config file manually. " \ "Have a look at the docs to get information about the other available options and their meaning." click.echo(textwrap.fill(help_text, width)) click.echo() nick = click.prompt("➤ Please enter your desired nick", default=os.environ.get("USER", "")) def overwrite_check(path): if os.path.isfile(path): click.confirm("➤ '{0}' already exists. Overwrite?".format(path), abort=True) cfgfile = click.prompt("➤ Please enter the desired location for your config file", os.path.join(Config.config_dir, Config.config_name), type=click.Path(readable=True, writable=True, file_okay=True)) cfgfile = os.path.expanduser(cfgfile) overwrite_check(cfgfile) twtfile = click.prompt("➤ Please enter the desired location for your twtxt file", os.path.expanduser("~/twtxt.txt"), type=click.Path(readable=True, writable=True, file_okay=True)) twtfile = os.path.expanduser(twtfile) overwrite_check(twtfile) twturl = click.prompt("➤ Please enter the URL your twtxt file will be accessible from", default="https://example.org/twtxt.txt") disclose_identity = click.confirm("➤ Do you want to disclose your identity? Otherwise your nick and URL will be shared when " "making HTTP requests", default=False) click.echo() add_news = click.confirm("➤ Do you want to follow the twtxt news feed?", default=True) conf = Config.create_config(cfgfile, nick, twtfile, twturl, disclose_identity, add_news) twtfile_dir = os.path.dirname(twtfile) if not os.path.exists(twtfile_dir): os.makedirs(twtfile_dir) open(twtfile, "a").close() click.echo() click.echo("✓ Created config file at '{0}'.".format(click.format_filename(conf.config_file))) click.echo("✓ Created twtxt file at '{0}'.".format(click.format_filename(twtfile)))
def test_defaults(): empty_cfg = configparser.ConfigParser() empty_conf = Config("foobar", empty_cfg) assert empty_conf.nick == os.environ.get("USER", "") assert empty_conf.twtfile == "twtxt.txt" assert empty_conf.twturl is None assert empty_conf.check_following is True assert empty_conf.use_pager is False assert empty_conf.porcelain is False assert empty_conf.limit_timeline == 20 assert empty_conf.timeout == 5.0 assert empty_conf.sorting == "descending" assert empty_conf.post_tweet_hook is None
def test_create_config(config_dir): config_dir_old = Config.config_dir Config.config_dir = str(config_dir.join("new")) conf_w = Config.create_config( os.path.join(Config.config_dir, Config.config_name), "bar", "batz.txt", False, True) conf_r = Config.discover() assert conf_r.nick == "bar" assert conf_r.twtfile == "batz.txt" assert conf_r.character_limit == 140 assert conf_r.character_warning == 140 assert conf_r.following[0].nick == "twtxt" assert conf_r.following[0].url == "https://buckket.org/twtxt_news.txt" assert set(conf_r.options.keys()) == { "nick", "twtfile", "disclose_identity", "character_limit", "character_warning" } conf_r.cfg.remove_section("twtxt") assert conf_r.options == {} conf_r.cfg.remove_section("following") assert conf_r.following == [] Config.config_dir = config_dir_old
def test_add_get_remove_source(): conf = Config.discover() conf.cfg.remove_section("following") assert conf.remove_source_by_nick("foo") is False assert conf.get_source_by_nick("baz") is None conf.add_source(Source("foo", "bar")) source = conf.get_source_by_nick("foo") assert source.nick == "foo" assert source.url == "bar" assert conf.remove_source_by_nick("baz") is False assert conf.remove_source_by_nick("foo") is True assert conf.following == []
def test_defaults(): empty_cfg = configparser.ConfigParser() empty_conf = Config("foobar", empty_cfg) assert empty_conf.nick == os.environ.get("USER", "") assert empty_conf.twtfile == "twtxt.txt" assert empty_conf.twturl is None assert empty_conf.check_following is True assert empty_conf.use_pager is False assert empty_conf.use_cache is True assert empty_conf.porcelain is False assert empty_conf.character_limit is None assert empty_conf.character_warning is None assert empty_conf.disclose_identity is False assert empty_conf.limit_timeline == 20 assert empty_conf.timeline_update_interval == 10 assert empty_conf.timeout == 5.0 assert empty_conf.sorting == "descending" assert empty_conf.post_tweet_hook is None assert empty_conf.pre_tweet_hook is None
def test_discover(): conf = Config.discover() check_cfg(conf)
def retrieve_file(client, source, limit, cache): is_cached = cache.is_cached(source.url) if cache else None headers = {"If-Modified-Since": cache.last_modified(source.url)} if is_cached else {} try: response = yield from client.request("get",source.url, headers=headers,allow_redirects=False) content = yield from response.text() except Exception as e: if is_cached: logger.debug("{}: {} - using cached content".format(source.url, e)) return cache.get_tweets(source.url, limit) #comp490 elif e==ssl.CertificateError: click.echo("Warning the source: "+source.nick+" is unsafe: Hostname does not match name on SSL certificate") return [] elif e==aiohttp.errors.ClientOSError: if "[[SSL: CERTIFICATE_VERIFY_FAILED" in str(e): click.echo("Warning the source: "+source.nick+" is unsafe: The ssl certificate has expired") return [] elif "[SSL: EXCESSIVE_MESSAGE_SIZE]" in str(e): click.echo("Warning the source: "+source.nick+" is unsafe: source has sent an invalid response") #COMP490 else: logger.debug(e) return [] if response.status == 200: tweets = parse_tweets(content.splitlines(), source) if cache: last_modified_header = response.headers.get("Last-Modified") if last_modified_header: logger.debug("{} returned 200 and Last-Modified header - adding content to cache".format(source.url)) cache.add_tweets(source.url, last_modified_header, tweets) else: logger.debug("{} returned 200 but no Last-Modified header - can’t cache content".format(source.url)) else: logger.debug("{} returned 200".format(source.url)) return sorted(tweets, reverse=True)[:limit] #comp490 elif response.status==301: cache = Cache.discover() conf=Config.discover() tweets=cache.get_tweets(source.url) conf.remove_source_by_nick(source.nick) url=response.headers["Location"] conf.add_source(Source(source.nick,url)) for tweet in tweets: cache.add_tweet(url,0,tweet) #comp490 elif response.status == 410 and is_cached: # 410 Gone: # The resource requested is no longer available, # and will not be available again. logger.debug("{} returned 410 - deleting cached content".format(source.url)) cache.remove_tweets(source.url) return [] elif is_cached: logger.debug("{} returned {} - using cached content".format(source.url, response.status)) return cache.get_tweets(source.url, limit) else: logger.debug("{} returned {}".format(source.url, response.status)) return []