From 7ada8a291ea7e9b7f607e4f8d4174d8e08a4eb08 Mon Sep 17 00:00:00 2001 From: K0nstantine Date: Wed, 21 Mar 2018 00:52:06 -0400 Subject: [PATCH 1/9] Initial watchlist management cli --- piker/cli.py | 119 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/piker/cli.py b/piker/cli.py index daefd1e1..5ce7b943 100644 --- a/piker/cli.py +++ b/piker/cli.py @@ -2,6 +2,11 @@ Console interface to broker client/daemons. """ from functools import partial +from importlib import import_module +from os import path, makedirs, stat +from collections import defaultdict +import json +import ast import click import trio @@ -13,6 +18,8 @@ from .brokers import core, get_brokermod log = get_logger('cli') DEFAULT_BROKER = 'robinhood' +_config_dir = click.get_app_dir('piker') +_watchlists_data_path = path.join(_config_dir, 'watchlists.json') def run(main, loglevel='info'): log = get_console_log(loglevel) @@ -135,3 +142,115 @@ def watch(loglevel, broker, rate, name): rate = broker_limit log.warn(f"Limiting {brokermod.__name__} query rate to {rate}/sec") trio.run(_async_main, name, watchlists[name], brokermod, rate) + + # broker_conf_path = os.path.join( + # click.get_app_dir('piker'), 'watchlists.json') + # from piker.testing import _quote_streamer as brokermod + trio.run(_async_main, name, watchlists[name], brokermod) + + +@cli.group() +@click.option('--loglevel', '-l', default='warning', help='Logging level') +@click.pass_context +def watchlists(ctx, loglevel): + """Watchlists cl commands and operations + """ + # import pdb; pdb.set_trace() + get_console_log(loglevel) # activate console logging + + ctx.obj = {} + + if not path.isdir(_config_dir): + log.debug(f"Creating config dir {_config_dir}") + makedirs(_config_dir) + + if path.isfile(_watchlists_data_path): + f = open(_watchlists_data_path, 'r') + if not stat(_watchlists_data_path).st_size == 0: + ctx.obj = json.load(f) + f.close() + else: + f = open(_watchlists_data_path, 'w') + f.close() + +@watchlists.command(help='show watchlist') +@click.argument('name', nargs=1, required=False) +@click.pass_context +def show(ctx, name): + watchlist = ctx.obj + click.echo(colorize_json( + watchlist if name is None else watchlist[name])) + + +@watchlists.command(help='add a new watchlist') +@click.argument('name', nargs=1, required=True) +@click.pass_context +def new(ctx, name): + watchlist = ctx.obj + f = open(_watchlists_data_path, 'w') + watchlist.setdefault(name, []) + json.dump(watchlist, f) + f.close() + + +@watchlists.command(help='add ticker to watchlist') +@click.argument('name', nargs=1, required=True) +@click.argument('ticker_name', nargs=1, required=True) +@click.pass_context +def add(ctx, name, ticker_name): + watchlist = ctx.obj + f = open(_watchlists_data_path, 'w') + if name in watchlist: + watchlist[name].append(str(ticker_name).upper()) + json.dump(watchlist, f) + f.close() + + +@watchlists.command(help='remove ticker from watchlist') +@click.argument('name', nargs=1, required=True) +@click.argument('ticker_name', nargs=1, required=True) +@click.pass_context +def remove(ctx, name, ticker_name): + watchlist = ctx.obj + f = open(_watchlists_data_path, 'w') + if name in watchlist: + watchlist[name].remove(str(ticker_name).upper()) + json.dump(watchlist, f) + f.close() + + +@watchlists.command(help='delete watchlist') +@click.argument('name', nargs=1, required=True) +@click.pass_context +def delete(ctx, name): + watchlist = ctx.obj + f = open(_watchlists_data_path, 'w') + if name in watchlist: + del watchlist[name] + json.dump(watchlist, f) + f.close() + + +@watchlists.command(help='merge a watchlist from another user') +@click.argument('watchlist_to_merge', nargs=1, required=True) +@click.pass_context +def merge(ctx, watchlist_to_merge): + watchlist = ctx.obj + f = open(_watchlists_data_path, 'w') + merged_watchlist = defaultdict(list) + watchlist_to_merge = ast.literal_eval(watchlist_to_merge) + for d in (watchlist, watchlist_to_merge): + for key, value in d.items(): + merged_watchlist[key].extend(value) + json.dump(merged_watchlist, f) + f.close() + print('merge these') #remember to convert to set + +@watchlists.command(help='dump a text respresentation of a watchlist to console') +@click.argument('name', nargs=1, required=False) +@click.pass_context +def dump(ctx, name): + watchlist = ctx.obj + f = open(_watchlists_data_path, 'r') + print(json.dumps(watchlist)) + f.close() From 43fbea57224302e59bdc308e85a25d530a79faf5 Mon Sep 17 00:00:00 2001 From: K0nstantine Date: Fri, 23 Mar 2018 16:47:09 -0400 Subject: [PATCH 2/9] Sorted values and keys in watchlist dictionary and added a purge function --- piker/cli.py | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/piker/cli.py b/piker/cli.py index 5ce7b943..e3f89b8d 100644 --- a/piker/cli.py +++ b/piker/cli.py @@ -3,7 +3,7 @@ Console interface to broker client/daemons. """ from functools import partial from importlib import import_module -from os import path, makedirs, stat +import os from collections import defaultdict import json import ast @@ -19,7 +19,7 @@ log = get_logger('cli') DEFAULT_BROKER = 'robinhood' _config_dir = click.get_app_dir('piker') -_watchlists_data_path = path.join(_config_dir, 'watchlists.json') +_watchlists_data_path = os.path.join(_config_dir, 'watchlists.json') def run(main, loglevel='info'): log = get_console_log(loglevel) @@ -149,6 +149,14 @@ def watch(loglevel, broker, rate, name): trio.run(_async_main, name, watchlists[name], brokermod) +def json_sorted_writer(watchlist, open_file): + for key in watchlist: + watchlist[key].sort() + s = set(watchlist[key]) + watchlist[key] = list(s) + json.dump(watchlist, open_file, sort_keys=True) + + @cli.group() @click.option('--loglevel', '-l', default='warning', help='Logging level') @click.pass_context @@ -160,19 +168,20 @@ def watchlists(ctx, loglevel): ctx.obj = {} - if not path.isdir(_config_dir): + if not os.path.isdir(_config_dir): log.debug(f"Creating config dir {_config_dir}") - makedirs(_config_dir) + os.makedirs(_config_dir) - if path.isfile(_watchlists_data_path): + if os.path.isfile(_watchlists_data_path): f = open(_watchlists_data_path, 'r') - if not stat(_watchlists_data_path).st_size == 0: + if not os.stat(_watchlists_data_path).st_size == 0: ctx.obj = json.load(f) f.close() else: f = open(_watchlists_data_path, 'w') f.close() + @watchlists.command(help='show watchlist') @click.argument('name', nargs=1, required=False) @click.pass_context @@ -189,7 +198,7 @@ def new(ctx, name): watchlist = ctx.obj f = open(_watchlists_data_path, 'w') watchlist.setdefault(name, []) - json.dump(watchlist, f) + json_sorted_writer(watchlist, f) f.close() @@ -202,7 +211,7 @@ def add(ctx, name, ticker_name): f = open(_watchlists_data_path, 'w') if name in watchlist: watchlist[name].append(str(ticker_name).upper()) - json.dump(watchlist, f) + json_sorted_writer(watchlist, f) f.close() @@ -215,7 +224,7 @@ def remove(ctx, name, ticker_name): f = open(_watchlists_data_path, 'w') if name in watchlist: watchlist[name].remove(str(ticker_name).upper()) - json.dump(watchlist, f) + json_sorted_writer(watchlist, f) f.close() @@ -227,7 +236,7 @@ def delete(ctx, name): f = open(_watchlists_data_path, 'w') if name in watchlist: del watchlist[name] - json.dump(watchlist, f) + json_sorted_writer(watchlist, f) f.close() @@ -242,9 +251,9 @@ def merge(ctx, watchlist_to_merge): for d in (watchlist, watchlist_to_merge): for key, value in d.items(): merged_watchlist[key].extend(value) - json.dump(merged_watchlist, f) + json_sorted_writer(merged_watchlist, f) f.close() - print('merge these') #remember to convert to set + @watchlists.command(help='dump a text respresentation of a watchlist to console') @click.argument('name', nargs=1, required=False) @@ -254,3 +263,9 @@ def dump(ctx, name): f = open(_watchlists_data_path, 'r') print(json.dumps(watchlist)) f.close() + + +@watchlists.command(help='purge watchlists and remove json file') +def purge(): + # import pdb; pdb.set_trace() + os.remove(_watchlists_data_path) From ce75bd8f6fa9484ddfca84c480b7b88e1c6cc3cd Mon Sep 17 00:00:00 2001 From: K0nstantine Date: Tue, 27 Mar 2018 14:06:26 -0400 Subject: [PATCH 3/9] Update CLI and create watchlists module Moved the watchlists management implementation to a seperate module wrapped in an api. Resolves: #5 --- piker/cli.py | 80 ++++++++++++--------------------------------- piker/watchlists.py | 63 +++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 60 deletions(-) create mode 100644 piker/watchlists.py diff --git a/piker/cli.py b/piker/cli.py index e3f89b8d..86c42643 100644 --- a/piker/cli.py +++ b/piker/cli.py @@ -6,14 +6,15 @@ from importlib import import_module import os from collections import defaultdict import json -import ast import click import trio import pandas as pd + from .log import get_console_log, colorize_json, get_logger -from .brokers import core, get_brokermod +from . import watchlists as wl +from .brokers import core log = get_logger('cli') DEFAULT_BROKER = 'robinhood' @@ -149,14 +150,6 @@ def watch(loglevel, broker, rate, name): trio.run(_async_main, name, watchlists[name], brokermod) -def json_sorted_writer(watchlist, open_file): - for key in watchlist: - watchlist[key].sort() - s = set(watchlist[key]) - watchlist[key] = list(s) - json.dump(watchlist, open_file, sort_keys=True) - - @cli.group() @click.option('--loglevel', '-l', default='warning', help='Logging level') @click.pass_context @@ -165,21 +158,8 @@ def watchlists(ctx, loglevel): """ # import pdb; pdb.set_trace() get_console_log(loglevel) # activate console logging - - ctx.obj = {} - - if not os.path.isdir(_config_dir): - log.debug(f"Creating config dir {_config_dir}") - os.makedirs(_config_dir) - - if os.path.isfile(_watchlists_data_path): - f = open(_watchlists_data_path, 'r') - if not os.stat(_watchlists_data_path).st_size == 0: - ctx.obj = json.load(f) - f.close() - else: - f = open(_watchlists_data_path, 'w') - f.close() + wl.make_config_dir(_config_dir) + ctx.obj = wl.ensure_watchlists(_watchlists_data_path) @watchlists.command(help='show watchlist') @@ -191,15 +171,22 @@ def show(ctx, name): watchlist if name is None else watchlist[name])) +@watchlists.command(help='load passed in watchlist') +@click.argument('data', nargs=1, required=True) +@click.pass_context +def load(ctx, data): + try: + wl.load_watchlists(data, _watchlists_data_path) + except (json.JSONDecodeError, IndexError): + click.echo('You must pass in a text respresentation of a json object. Try again.') + + @watchlists.command(help='add a new watchlist') @click.argument('name', nargs=1, required=True) @click.pass_context def new(ctx, name): watchlist = ctx.obj - f = open(_watchlists_data_path, 'w') - watchlist.setdefault(name, []) - json_sorted_writer(watchlist, f) - f.close() + wl.new_group(name, watchlist, _watchlists_data_path) @watchlists.command(help='add ticker to watchlist') @@ -208,11 +195,7 @@ def new(ctx, name): @click.pass_context def add(ctx, name, ticker_name): watchlist = ctx.obj - f = open(_watchlists_data_path, 'w') - if name in watchlist: - watchlist[name].append(str(ticker_name).upper()) - json_sorted_writer(watchlist, f) - f.close() + wl.add_ticker(name, ticker_name, watchlist, _watchlists_data_path) @watchlists.command(help='remove ticker from watchlist') @@ -221,11 +204,7 @@ def add(ctx, name, ticker_name): @click.pass_context def remove(ctx, name, ticker_name): watchlist = ctx.obj - f = open(_watchlists_data_path, 'w') - if name in watchlist: - watchlist[name].remove(str(ticker_name).upper()) - json_sorted_writer(watchlist, f) - f.close() + wl.remove_ticker(name, ticker_name, watchlist, _watchlists_data_path) @watchlists.command(help='delete watchlist') @@ -233,11 +212,7 @@ def remove(ctx, name, ticker_name): @click.pass_context def delete(ctx, name): watchlist = ctx.obj - f = open(_watchlists_data_path, 'w') - if name in watchlist: - del watchlist[name] - json_sorted_writer(watchlist, f) - f.close() + wl.delete_group(name, watchlist, _watchlists_data_path) @watchlists.command(help='merge a watchlist from another user') @@ -245,14 +220,7 @@ def delete(ctx, name): @click.pass_context def merge(ctx, watchlist_to_merge): watchlist = ctx.obj - f = open(_watchlists_data_path, 'w') - merged_watchlist = defaultdict(list) - watchlist_to_merge = ast.literal_eval(watchlist_to_merge) - for d in (watchlist, watchlist_to_merge): - for key, value in d.items(): - merged_watchlist[key].extend(value) - json_sorted_writer(merged_watchlist, f) - f.close() + wl.merge_watchlist(watchlist_to_merge, watchlist, _watchlists_data_path) @watchlists.command(help='dump a text respresentation of a watchlist to console') @@ -260,12 +228,4 @@ def merge(ctx, watchlist_to_merge): @click.pass_context def dump(ctx, name): watchlist = ctx.obj - f = open(_watchlists_data_path, 'r') print(json.dumps(watchlist)) - f.close() - - -@watchlists.command(help='purge watchlists and remove json file') -def purge(): - # import pdb; pdb.set_trace() - os.remove(_watchlists_data_path) diff --git a/piker/watchlists.py b/piker/watchlists.py new file mode 100644 index 00000000..6084a094 --- /dev/null +++ b/piker/watchlists.py @@ -0,0 +1,63 @@ +import os +import json +import ast +from collections import defaultdict + + +def write_sorted_json(watchlist, path): + for key in watchlist: + watchlist[key].sort() + s = set(watchlist[key]) + watchlist[key] = list(s) + with open(path, 'w') as f: + json.dump(watchlist, f, sort_keys=True) + + +def make_config_dir(dir_path): + if not os.path.isdir(dir_path): + log.debug(f"Creating config dir {dir_path}") + os.makedirs(dir_path) + + +def ensure_watchlists(file_path): + mode = 'r' if os.path.isfile(file_path) else 'w' + with open(file_path, mode) as f: + data = json.load(f) if not os.stat(file_path).st_size == 0 else {} + return data + + +def load_watchlists(watchlist, path): + watchlist = json.loads(watchlist) + write_sorted_json(watchlist, path) + + +def new_group(name, watchlist, path): + watchlist.setdefault(name, []) + write_sorted_json(watchlist, path) + + +def add_ticker(name, ticker_name, watchlist, path): + if name in watchlist: + watchlist[name].append(str(ticker_name).upper()) + write_sorted_json(watchlist, path) + + +def remove_ticker(name, ticker_name, watchlist, path): + if name in watchlist: + watchlist[name].remove(str(ticker_name).upper()) + write_sorted_json(watchlist, path) + + +def delete_group(name, watchlist, path): + if name in watchlist: + del watchlist[name] + write_sorted_json(watchlist, path) + + +def merge_watchlist(watchlist_to_merge, watchlist, path): + merged_watchlist = defaultdict(list) + watchlist_to_merge = ast.literal_eval(watchlist_to_merge) + for d in (watchlist, watchlist_to_merge): + for key, value in d.items(): + merged_watchlist[key].extend(value) + write_sorted_json(merged_watchlist, path) From d28a3dc4614d045139cf4fd6e25f7b4de7288973 Mon Sep 17 00:00:00 2001 From: K0nstantine Date: Wed, 28 Mar 2018 20:43:33 -0400 Subject: [PATCH 4/9] Add initial config dir test --- piker/cli.py | 2 +- piker/watchlists.py | 6 ++++-- tests/test_cli.py | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/piker/cli.py b/piker/cli.py index 86c42643..2a5b663a 100644 --- a/piker/cli.py +++ b/piker/cli.py @@ -14,7 +14,7 @@ import pandas as pd from .log import get_console_log, colorize_json, get_logger from . import watchlists as wl -from .brokers import core +from .brokers import core, get_brokermod log = get_logger('cli') DEFAULT_BROKER = 'robinhood' diff --git a/piker/watchlists.py b/piker/watchlists.py index 6084a094..a57e7a32 100644 --- a/piker/watchlists.py +++ b/piker/watchlists.py @@ -1,8 +1,10 @@ import os import json -import ast from collections import defaultdict +from .log import get_logger + +log = get_logger(__name__) def write_sorted_json(watchlist, path): for key in watchlist: @@ -56,7 +58,7 @@ def delete_group(name, watchlist, path): def merge_watchlist(watchlist_to_merge, watchlist, path): merged_watchlist = defaultdict(list) - watchlist_to_merge = ast.literal_eval(watchlist_to_merge) + watchlist_to_merge = json.loads(watchlist_to_merge) for d in (watchlist, watchlist_to_merge): for key, value in d.items(): merged_watchlist[key].extend(value) diff --git a/tests/test_cli.py b/tests/test_cli.py index a9c9077e..d45ca31f 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -4,7 +4,11 @@ CLI testing, dawg. import json import subprocess import pytest +import tempfile +import os.path +import logging +from piker.watchlists import make_config_dir def run(cmd): """Run cmd and check for zero return code. @@ -85,3 +89,18 @@ def test_api_method_not_found(nyse_tickers, capfd): out, err = capfd.readouterr() assert 'null' in out assert f'No api method `{bad_meth}` could be found?' in err + + +def test_watchlists_config_dir_created(caplog): + """Ensure that a config directory is created + """ + #Create temporary directory + config_dir = os.path.join(tempfile.gettempdir(), 'piker') + with caplog.at_level(logging.DEBUG): + make_config_dir(config_dir) + assert len(caplog.records) == 1 + record = caplog.records[0] + assert record.levelname == 'DEBUG' + assert record.message == f"Creating config dir {config_dir}" + assert os.path.isdir(config_dir) + os.rmdir(config_dir) From e859222df457291a3880ea7519eb50ff39f80ffe Mon Sep 17 00:00:00 2001 From: K0nstantine Date: Fri, 30 Mar 2018 23:05:45 -0400 Subject: [PATCH 5/9] Add initial API test, need relocation --- piker/watchlists.py | 2 +- tests/test_cli.py | 107 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 100 insertions(+), 9 deletions(-) diff --git a/piker/watchlists.py b/piker/watchlists.py index a57e7a32..b2c95295 100644 --- a/piker/watchlists.py +++ b/piker/watchlists.py @@ -8,9 +8,9 @@ log = get_logger(__name__) def write_sorted_json(watchlist, path): for key in watchlist: - watchlist[key].sort() s = set(watchlist[key]) watchlist[key] = list(s) + watchlist[key].sort() with open(path, 'w') as f: json.dump(watchlist, f, sort_keys=True) diff --git a/tests/test_cli.py b/tests/test_cli.py index d45ca31f..819aa021 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -8,7 +8,7 @@ import tempfile import os.path import logging -from piker.watchlists import make_config_dir +import piker.watchlists as wl def run(cmd): """Run cmd and check for zero return code. @@ -91,16 +91,107 @@ def test_api_method_not_found(nyse_tickers, capfd): assert f'No api method `{bad_meth}` could be found?' in err -def test_watchlists_config_dir_created(caplog): +@pytest.fixture +def temp_dir(): + #Create temporary directory + with tempfile.TemporaryDirectory() as tempdir: + config_dir = os.path.join(tempdir, 'piker') + yield config_dir + + +@pytest.fixture +def piker_dir(temp_dir): + wl.make_config_dir(temp_dir) + path = os.path.join(temp_dir, 'watchlists.json') + yield path + + +def test_watchlist_is_sorted_and_saved_to_file(piker_dir): + """Ensure that watchlist is sorted and saved to file + """ + wl_temp = {'test': ['TEST.CN', 'AAA'], 'AA': ['TEST.CN']} + wl_sort = {'AA': ['TEST.CN'], 'test': ['AAA', 'TEST.CN']} + wl.write_sorted_json(wl_temp, piker_dir) + temp_sorted = wl.ensure_watchlists(piker_dir) + assert temp_sorted == wl_sort + + +def test_watchlists_config_dir_created(caplog, temp_dir): """Ensure that a config directory is created """ - #Create temporary directory - config_dir = os.path.join(tempfile.gettempdir(), 'piker') with caplog.at_level(logging.DEBUG): - make_config_dir(config_dir) + wl.make_config_dir(temp_dir) assert len(caplog.records) == 1 record = caplog.records[0] assert record.levelname == 'DEBUG' - assert record.message == f"Creating config dir {config_dir}" - assert os.path.isdir(config_dir) - os.rmdir(config_dir) + assert record.message == f"Creating config dir {temp_dir}" + assert os.path.isdir(temp_dir) + #Test that there is no error and that a log message is not generatd + #when trying to create a directory that already exists + with caplog.at_level(logging.DEBUG): + wl.make_config_dir(temp_dir) + assert len(caplog.records) == 1 + + +def test_watchlist_is_read_from_file(piker_dir): + """Ensure json info is read from file or an empty dict is generated + """ + wl_temp = wl.ensure_watchlists(piker_dir) + assert wl_temp == {} + wl_temp2 = '{"AA": ["TEST.CN"]}' + wl.load_watchlists(wl_temp2, piker_dir) + assert json.loads(wl_temp2) == wl.ensure_watchlists(piker_dir) + + +def test_watchlist_loaded(piker_dir): + """Ensure that text respresentation of a watchlist is loaded to file + """ + wl_temp = '{"test": ["TEST.CN"]}' + wl.load_watchlists(wl_temp, piker_dir) + wl_temp2 = wl.ensure_watchlists(piker_dir) + assert wl_temp == json.dumps(wl_temp2) + + +def test_new_watchlist_group_added(piker_dir): + """Ensure that a new watchlist key is added to the watchlists dictionary + """ + wl_temp = {} + wl.new_group('test', wl_temp, piker_dir) + wl_temp = wl.ensure_watchlists(piker_dir) + assert len(wl_temp.keys()) == 1 + + +def test_new_ticker_added(piker_dir): + """Ensure that a new ticker is added to a watchlist + """ + wl_temp = {'test': []} + wl.add_ticker('test', 'TEST.CN', wl_temp, piker_dir) + wl_temp = wl.ensure_watchlists(piker_dir) + assert len(wl_temp['test']) == 1 + + +def test_ticker_is_removed(piker_dir): + """Verify that passed in ticker is removed + """ + wl_temp = {'test': ['TEST.CN']} + wl.remove_ticker('test', 'TEST.CN', wl_temp, piker_dir) + wl_temp = wl.ensure_watchlists(piker_dir) + assert wl_temp == {'test': []} + +def test_group_is_deleted(piker_dir): + """Check that watchlist group is removed + """ + wl_temp = {'test': ['TEST.CN']} + wl.delete_group('test', wl_temp, piker_dir) + wl_temp = wl.ensure_watchlists(piker_dir) + assert wl_temp.get('test') == None + + +def test_watchlist_is_merged(piker_dir): + """Ensure that watchlist is merged + """ + wl_temp = {'test': ['TEST.CN']} + wl_temp2 = '{"test2": ["TEST2.CN"]}' + wl.merge_watchlist(wl_temp2, wl_temp, piker_dir) + wl_temp3 = wl.ensure_watchlists(piker_dir) + assert wl_temp3 == {'test': ['TEST.CN'], 'test2': ['TEST2.CN']} From 11f25958baeb5fc57de9f38a977f79be8e126aee Mon Sep 17 00:00:00 2001 From: K0nstantine Date: Fri, 30 Mar 2018 23:48:43 -0400 Subject: [PATCH 6/9] Move watchlist api tests to seperate module --- piker/cli.py | 24 +++------ piker/watchlists.py | 23 +++------ tests/test_cli.py | 108 -------------------------------------- tests/test_watchlists.py | 109 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 139 deletions(-) create mode 100644 tests/test_watchlists.py diff --git a/piker/cli.py b/piker/cli.py index 2a5b663a..3259785e 100644 --- a/piker/cli.py +++ b/piker/cli.py @@ -154,9 +154,8 @@ def watch(loglevel, broker, rate, name): @click.option('--loglevel', '-l', default='warning', help='Logging level') @click.pass_context def watchlists(ctx, loglevel): - """Watchlists cl commands and operations + """Watchlists commands and operations """ - # import pdb; pdb.set_trace() get_console_log(loglevel) # activate console logging wl.make_config_dir(_config_dir) ctx.obj = wl.ensure_watchlists(_watchlists_data_path) @@ -168,7 +167,7 @@ def watchlists(ctx, loglevel): def show(ctx, name): watchlist = ctx.obj click.echo(colorize_json( - watchlist if name is None else watchlist[name])) + watchlist if name is None else watchlist[name])) @watchlists.command(help='load passed in watchlist') @@ -176,17 +175,10 @@ def show(ctx, name): @click.pass_context def load(ctx, data): try: - wl.load_watchlists(data, _watchlists_data_path) + wl.write_watchlists(data, _watchlists_data_path) except (json.JSONDecodeError, IndexError): - click.echo('You must pass in a text respresentation of a json object. Try again.') - - -@watchlists.command(help='add a new watchlist') -@click.argument('name', nargs=1, required=True) -@click.pass_context -def new(ctx, name): - watchlist = ctx.obj - wl.new_group(name, watchlist, _watchlists_data_path) + click.echo('You have passed an invalid text respresentation of a ' + 'JSON object. Try again.') @watchlists.command(help='add ticker to watchlist') @@ -207,7 +199,7 @@ def remove(ctx, name, ticker_name): wl.remove_ticker(name, ticker_name, watchlist, _watchlists_data_path) -@watchlists.command(help='delete watchlist') +@watchlists.command(help='delete watchlist group') @click.argument('name', nargs=1, required=True) @click.pass_context def delete(ctx, name): @@ -223,9 +215,9 @@ def merge(ctx, watchlist_to_merge): wl.merge_watchlist(watchlist_to_merge, watchlist, _watchlists_data_path) -@watchlists.command(help='dump a text respresentation of a watchlist to console') +@watchlists.command(help='dump text respresentation of a watchlist to console') @click.argument('name', nargs=1, required=False) @click.pass_context def dump(ctx, name): watchlist = ctx.obj - print(json.dumps(watchlist)) + click.echo(json.dumps(watchlist)) diff --git a/piker/watchlists.py b/piker/watchlists.py index b2c95295..ad95ac06 100644 --- a/piker/watchlists.py +++ b/piker/watchlists.py @@ -6,11 +6,10 @@ from .log import get_logger log = get_logger(__name__) + def write_sorted_json(watchlist, path): for key in watchlist: - s = set(watchlist[key]) - watchlist[key] = list(s) - watchlist[key].sort() + watchlist[key] = sorted(list(set(watchlist[key]))) with open(path, 'w') as f: json.dump(watchlist, f, sort_keys=True) @@ -24,29 +23,23 @@ def make_config_dir(dir_path): def ensure_watchlists(file_path): mode = 'r' if os.path.isfile(file_path) else 'w' with open(file_path, mode) as f: - data = json.load(f) if not os.stat(file_path).st_size == 0 else {} - return data + return json.load(f) if not os.stat(file_path).st_size == 0 else {} -def load_watchlists(watchlist, path): - watchlist = json.loads(watchlist) - write_sorted_json(watchlist, path) - - -def new_group(name, watchlist, path): - watchlist.setdefault(name, []) - write_sorted_json(watchlist, path) +def write_watchlists(watchlist, path): + write_sorted_json(json.loads(watchlist), path) def add_ticker(name, ticker_name, watchlist, path): - if name in watchlist: - watchlist[name].append(str(ticker_name).upper()) + watchlist.setdefault(name, []).append(str(ticker_name).upper()) write_sorted_json(watchlist, path) def remove_ticker(name, ticker_name, watchlist, path): if name in watchlist: watchlist[name].remove(str(ticker_name).upper()) + if watchlist[name] == []: + del watchlist[name] write_sorted_json(watchlist, path) diff --git a/tests/test_cli.py b/tests/test_cli.py index 819aa021..35be3a01 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -4,11 +4,9 @@ CLI testing, dawg. import json import subprocess import pytest -import tempfile import os.path import logging -import piker.watchlists as wl def run(cmd): """Run cmd and check for zero return code. @@ -89,109 +87,3 @@ def test_api_method_not_found(nyse_tickers, capfd): out, err = capfd.readouterr() assert 'null' in out assert f'No api method `{bad_meth}` could be found?' in err - - -@pytest.fixture -def temp_dir(): - #Create temporary directory - with tempfile.TemporaryDirectory() as tempdir: - config_dir = os.path.join(tempdir, 'piker') - yield config_dir - - -@pytest.fixture -def piker_dir(temp_dir): - wl.make_config_dir(temp_dir) - path = os.path.join(temp_dir, 'watchlists.json') - yield path - - -def test_watchlist_is_sorted_and_saved_to_file(piker_dir): - """Ensure that watchlist is sorted and saved to file - """ - wl_temp = {'test': ['TEST.CN', 'AAA'], 'AA': ['TEST.CN']} - wl_sort = {'AA': ['TEST.CN'], 'test': ['AAA', 'TEST.CN']} - wl.write_sorted_json(wl_temp, piker_dir) - temp_sorted = wl.ensure_watchlists(piker_dir) - assert temp_sorted == wl_sort - - -def test_watchlists_config_dir_created(caplog, temp_dir): - """Ensure that a config directory is created - """ - with caplog.at_level(logging.DEBUG): - wl.make_config_dir(temp_dir) - assert len(caplog.records) == 1 - record = caplog.records[0] - assert record.levelname == 'DEBUG' - assert record.message == f"Creating config dir {temp_dir}" - assert os.path.isdir(temp_dir) - #Test that there is no error and that a log message is not generatd - #when trying to create a directory that already exists - with caplog.at_level(logging.DEBUG): - wl.make_config_dir(temp_dir) - assert len(caplog.records) == 1 - - -def test_watchlist_is_read_from_file(piker_dir): - """Ensure json info is read from file or an empty dict is generated - """ - wl_temp = wl.ensure_watchlists(piker_dir) - assert wl_temp == {} - wl_temp2 = '{"AA": ["TEST.CN"]}' - wl.load_watchlists(wl_temp2, piker_dir) - assert json.loads(wl_temp2) == wl.ensure_watchlists(piker_dir) - - -def test_watchlist_loaded(piker_dir): - """Ensure that text respresentation of a watchlist is loaded to file - """ - wl_temp = '{"test": ["TEST.CN"]}' - wl.load_watchlists(wl_temp, piker_dir) - wl_temp2 = wl.ensure_watchlists(piker_dir) - assert wl_temp == json.dumps(wl_temp2) - - -def test_new_watchlist_group_added(piker_dir): - """Ensure that a new watchlist key is added to the watchlists dictionary - """ - wl_temp = {} - wl.new_group('test', wl_temp, piker_dir) - wl_temp = wl.ensure_watchlists(piker_dir) - assert len(wl_temp.keys()) == 1 - - -def test_new_ticker_added(piker_dir): - """Ensure that a new ticker is added to a watchlist - """ - wl_temp = {'test': []} - wl.add_ticker('test', 'TEST.CN', wl_temp, piker_dir) - wl_temp = wl.ensure_watchlists(piker_dir) - assert len(wl_temp['test']) == 1 - - -def test_ticker_is_removed(piker_dir): - """Verify that passed in ticker is removed - """ - wl_temp = {'test': ['TEST.CN']} - wl.remove_ticker('test', 'TEST.CN', wl_temp, piker_dir) - wl_temp = wl.ensure_watchlists(piker_dir) - assert wl_temp == {'test': []} - -def test_group_is_deleted(piker_dir): - """Check that watchlist group is removed - """ - wl_temp = {'test': ['TEST.CN']} - wl.delete_group('test', wl_temp, piker_dir) - wl_temp = wl.ensure_watchlists(piker_dir) - assert wl_temp.get('test') == None - - -def test_watchlist_is_merged(piker_dir): - """Ensure that watchlist is merged - """ - wl_temp = {'test': ['TEST.CN']} - wl_temp2 = '{"test2": ["TEST2.CN"]}' - wl.merge_watchlist(wl_temp2, wl_temp, piker_dir) - wl_temp3 = wl.ensure_watchlists(piker_dir) - assert wl_temp3 == {'test': ['TEST.CN'], 'test2': ['TEST2.CN']} diff --git a/tests/test_watchlists.py b/tests/test_watchlists.py new file mode 100644 index 00000000..5325b81e --- /dev/null +++ b/tests/test_watchlists.py @@ -0,0 +1,109 @@ +""" +Watchlists testing. +""" +import json +import pytest +import tempfile +import os.path +import logging + +import piker.watchlists as wl + + +@pytest.fixture +def temp_dir(): + """Creates a path to a pretend config dir in a temporary directory for + + testing. + """ + with tempfile.TemporaryDirectory() as tempdir: + config_dir = os.path.join(tempdir, 'piker') + yield config_dir + + +@pytest.fixture +def piker_dir(temp_dir): + wl.make_config_dir(temp_dir) + yield os.path.join(temp_dir, 'watchlists.json') + + +def test_watchlist_is_sorted_no_dups_and_saved_to_file(piker_dir): + wl_temp = {'test': ['TEST.CN', 'AAA'], 'AA': ['TEST.CN', 'TEST.CN'], + 'AA': ['TEST.CN']} + wl_sort = {'AA': ['TEST.CN'], 'test': ['AAA', 'TEST.CN']} + wl.write_sorted_json(wl_temp, piker_dir) + temp_sorted = wl.ensure_watchlists(piker_dir) + assert temp_sorted == wl_sort + + +def test_watchlists_config_dir_created(caplog, temp_dir): + """Ensure that a config directory is created. + """ + with caplog.at_level(logging.DEBUG): + wl.make_config_dir(temp_dir) + assert len(caplog.records) == 1 + record = caplog.records[0] + assert record.levelname == 'DEBUG' + assert record.message == f"Creating config dir {temp_dir}" + assert os.path.isdir(temp_dir) + # Test that there is no error and that a log message is not generatd + # when trying to create a directory that already exists + with caplog.at_level(logging.DEBUG): + wl.make_config_dir(temp_dir) + # There should be no additional log message. + assert len(caplog.records) == 1 + + +def test_watchlist_is_read_from_file(piker_dir): + """Ensure json info is read from file or an empty dict is generated + + and that text respresentation of a watchlist is saved to file. + """ + wl_temp = wl.ensure_watchlists(piker_dir) + assert wl_temp == {} + wl_temp2 = '{"AA": ["TEST.CN"]}' + wl.write_watchlists(wl_temp2, piker_dir) + assert json.loads(wl_temp2) == wl.ensure_watchlists(piker_dir) + + +def test_new_ticker_added(piker_dir): + """Ensure that a new ticker is added to a watchlist for both cases. + """ + wl.add_ticker('test', 'TEST.CN', {'test': ['TEST2.CN']}, piker_dir) + wl_temp = wl.ensure_watchlists(piker_dir) + assert len(wl_temp['test']) == 2 + wl.add_ticker('test2', 'TEST.CN', wl_temp, piker_dir) + wl_temp = wl.ensure_watchlists(piker_dir) + assert wl_temp['test2'] + + +def test_ticker_is_removed(piker_dir): + """Verify that passed in ticker is removed and that a group is removed + + if no tickers left. + """ + wl_temp = {'test': ['TEST.CN', 'TEST2.CN'], 'test2': ['TEST.CN']} + wl.remove_ticker('test', 'TEST.CN', wl_temp, piker_dir) + wl.remove_ticker('test2', 'TEST.CN', wl_temp, piker_dir) + wl_temp = wl.ensure_watchlists(piker_dir) + assert wl_temp == {'test': ['TEST2.CN']} + assert not wl_temp.get('test2') + + +def test_group_is_deleted(piker_dir): + """Check that watchlist group is removed. + """ + wl_temp = {'test': ['TEST.CN']} + wl.delete_group('test', wl_temp, piker_dir) + wl_temp = wl.ensure_watchlists(piker_dir) + assert not wl_temp.get('test') + + +def test_watchlist_is_merged(piker_dir): + """Ensure that watchlist is merged. + """ + wl_temp = {'test': ['TEST.CN']} + wl_temp2 = '{"test2": ["TEST2.CN"]}' + wl.merge_watchlist(wl_temp2, wl_temp, piker_dir) + wl_temp3 = wl.ensure_watchlists(piker_dir) + assert wl_temp3 == {'test': ['TEST.CN'], 'test2': ['TEST2.CN']} From cd69c301431fff2f55f05fa0672040d9cdd64766 Mon Sep 17 00:00:00 2001 From: K0nstantine Date: Mon, 2 Apr 2018 23:55:02 -0400 Subject: [PATCH 7/9] CLI tests for watchlist commands --- piker/cli.py | 30 ++++++---- tests/test_cli.py | 125 ++++++++++++++++++++++++++++++++++++++- tests/test_watchlists.py | 3 - 3 files changed, 140 insertions(+), 18 deletions(-) diff --git a/piker/cli.py b/piker/cli.py index 3259785e..37d99e86 100644 --- a/piker/cli.py +++ b/piker/cli.py @@ -22,6 +22,7 @@ DEFAULT_BROKER = 'robinhood' _config_dir = click.get_app_dir('piker') _watchlists_data_path = os.path.join(_config_dir, 'watchlists.json') + def run(main, loglevel='info'): log = get_console_log(loglevel) @@ -152,20 +153,23 @@ def watch(loglevel, broker, rate, name): @cli.group() @click.option('--loglevel', '-l', default='warning', help='Logging level') +@click.option('--config_dir', '-d', default=_watchlists_data_path, + help='Path to piker configuration directory') @click.pass_context -def watchlists(ctx, loglevel): +def watchlists(ctx, loglevel, config_dir): """Watchlists commands and operations """ get_console_log(loglevel) # activate console logging wl.make_config_dir(_config_dir) - ctx.obj = wl.ensure_watchlists(_watchlists_data_path) + ctx.obj = {'path': config_dir, + 'watchlist': wl.ensure_watchlists(config_dir)} @watchlists.command(help='show watchlist') @click.argument('name', nargs=1, required=False) @click.pass_context def show(ctx, name): - watchlist = ctx.obj + watchlist = ctx.obj['watchlist'] click.echo(colorize_json( watchlist if name is None else watchlist[name])) @@ -175,7 +179,7 @@ def show(ctx, name): @click.pass_context def load(ctx, data): try: - wl.write_watchlists(data, _watchlists_data_path) + wl.write_watchlists(data, ctx.obj['path']) except (json.JSONDecodeError, IndexError): click.echo('You have passed an invalid text respresentation of a ' 'JSON object. Try again.') @@ -186,8 +190,8 @@ def load(ctx, data): @click.argument('ticker_name', nargs=1, required=True) @click.pass_context def add(ctx, name, ticker_name): - watchlist = ctx.obj - wl.add_ticker(name, ticker_name, watchlist, _watchlists_data_path) + watchlist = ctx.obj['watchlist'] + wl.add_ticker(name, ticker_name, watchlist, ctx.obj['path']) @watchlists.command(help='remove ticker from watchlist') @@ -195,29 +199,29 @@ def add(ctx, name, ticker_name): @click.argument('ticker_name', nargs=1, required=True) @click.pass_context def remove(ctx, name, ticker_name): - watchlist = ctx.obj - wl.remove_ticker(name, ticker_name, watchlist, _watchlists_data_path) + watchlist = ctx.obj['watchlist'] + wl.remove_ticker(name, ticker_name, watchlist, ctx.obj['path']) @watchlists.command(help='delete watchlist group') @click.argument('name', nargs=1, required=True) @click.pass_context def delete(ctx, name): - watchlist = ctx.obj - wl.delete_group(name, watchlist, _watchlists_data_path) + watchlist = ctx.obj['watchlist'] + wl.delete_group(name, watchlist, ctx.obj['path']) @watchlists.command(help='merge a watchlist from another user') @click.argument('watchlist_to_merge', nargs=1, required=True) @click.pass_context def merge(ctx, watchlist_to_merge): - watchlist = ctx.obj - wl.merge_watchlist(watchlist_to_merge, watchlist, _watchlists_data_path) + watchlist = ctx.obj['watchlist'] + wl.merge_watchlist(watchlist_to_merge, watchlist, ctx.obj['path']) @watchlists.command(help='dump text respresentation of a watchlist to console') @click.argument('name', nargs=1, required=False) @click.pass_context def dump(ctx, name): - watchlist = ctx.obj + watchlist = ctx.obj['watchlist'] click.echo(json.dumps(watchlist)) diff --git a/tests/test_cli.py b/tests/test_cli.py index 35be3a01..39ba72e3 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -4,14 +4,19 @@ CLI testing, dawg. import json import subprocess import pytest +import tempfile import os.path import logging +import piker.watchlists as wl +import piker.cli as cli +from piker.log import colorize_json -def run(cmd): + +def run(cmd, *args): """Run cmd and check for zero return code. """ - cp = subprocess.run(cmd.split()) + cp = subprocess.run(cmd.split() + list(args)) cp.check_returncode() return cp @@ -87,3 +92,119 @@ def test_api_method_not_found(nyse_tickers, capfd): out, err = capfd.readouterr() assert 'null' in out assert f'No api method `{bad_meth}` could be found?' in err + + +@pytest.fixture +def temp_dir(): + """Creates a path to a pretend config dir in a temporary directory for + testing. + """ + with tempfile.TemporaryDirectory() as tempdir: + config_dir = os.path.join(tempdir, 'piker') + yield config_dir + + +@pytest.fixture +def piker_dir(temp_dir): + wl.make_config_dir(temp_dir) + json_file_path = os.path.join(temp_dir, 'watchlists.json') + watchlist = { + 'dad': ['GM', 'TSLA', 'DOL.TO', 'CIM', 'SPY', 'SHOP.TO'], + 'pharma': ['ATE.VN'], + 'indexes': ['SPY', 'DAX', 'QQQ', 'DIA'], + } + wl.write_sorted_json(watchlist, json_file_path) + yield json_file_path + + +def test_show_watchlists(capfd, piker_dir): + """Ensure a watchlist is printed. + """ + expected_out = json.dumps({ + 'dad': ['CIM', 'DOL.TO', 'GM', 'SHOP.TO', 'SPY', 'TSLA'], + 'indexes': ['DAX', 'DIA', 'QQQ', 'SPY'], + 'pharma': ['ATE.VN'], + }, indent=4) + run(f"piker watchlists -d {piker_dir} show") + out, err = capfd.readouterr() + assert out.strip() == expected_out + + +def test_dump_watchlists(capfd, piker_dir): + """Ensure watchlist is dumped. + """ + expected_out = json.dumps({ + 'dad': ['CIM', 'DOL.TO', 'GM', 'SHOP.TO', 'SPY', 'TSLA'], + 'indexes': ['DAX', 'DIA', 'QQQ', 'SPY'], + 'pharma': ['ATE.VN'], + }) + run(f"piker watchlists -d {piker_dir} dump") + out, err = capfd.readouterr() + assert out.strip() == expected_out + + +def test_ticker_added_to_watchlists(capfd, piker_dir): + expected_out = { + 'dad': ['CIM', 'DOL.TO', 'GM', 'SHOP.TO', 'SPY', 'TSLA'], + 'indexes': ['DAX', 'DIA', 'QQQ', 'SPY'], + 'pharma': ['ATE.VN', 'CRACK'], + } + run(f"piker watchlists -d {piker_dir} add pharma CRACK") + out = wl.ensure_watchlists(piker_dir) + assert out == expected_out + + +def test_ticker_removed_from_watchlists(capfd, piker_dir): + expected_out = { + 'dad': ['CIM', 'DOL.TO', 'GM', 'SHOP.TO', 'SPY', 'TSLA'], + 'indexes': ['DAX', 'DIA', 'SPY'], + 'pharma': ['ATE.VN'], + } + run(f"piker watchlists -d {piker_dir} remove indexes QQQ") + out = wl.ensure_watchlists(piker_dir) + assert out == expected_out + + +def test_group_deleted_from_watchlists(capfd, piker_dir): + expected_out = { + 'dad': ['CIM', 'DOL.TO', 'GM', 'SHOP.TO', 'SPY', 'TSLA'], + 'indexes': ['DAX', 'DIA', 'QQQ', 'SPY'], + } + run(f"piker watchlists -d {piker_dir} delete pharma") + out = wl.ensure_watchlists(piker_dir) + assert out == expected_out + + +def test_watchlists_loaded(capfd, piker_dir): + expected_out_text = json.dumps({ + 'dad': ['CIM', 'DOL.TO', 'GM', 'SHOP.TO', 'SPY', 'TSLA'], + 'pharma': ['ATE.VN'], + }) + expected_out = { + 'dad': ['CIM', 'DOL.TO', 'GM', 'SHOP.TO', 'SPY', 'TSLA'], + 'pharma': ['ATE.VN'], + } + run(f"piker watchlists -d {piker_dir} load", expected_out_text) + out = wl.ensure_watchlists(piker_dir) + assert out == expected_out + + +def test_watchlists_is_merge(capfd, piker_dir): + orig_watchlist = { + 'dad': ['CIM', 'DOL.TO', 'GM', 'SHOP.TO', 'SPY', 'TSLA'], + 'indexes': ['DAX', 'DIA', 'QQQ', 'SPY'], + 'pharma': ['ATE.VN'], + } + list_to_merge = json.dumps({ + 'drugs': ['CRACK'] + }) + expected_out = { + 'dad': ['CIM', 'DOL.TO', 'GM', 'SHOP.TO', 'SPY', 'TSLA'], + 'indexes': ['DAX', 'DIA', 'QQQ', 'SPY'], + 'pharma': ['ATE.VN'], + 'drugs': ['CRACK'] + } + wl.write_sorted_json(orig_watchlist, piker_dir) + run(f"piker watchlists -d {piker_dir} merge", list_to_merge) + out = wl.ensure_watchlists(piker_dir) + assert out == expected_out diff --git a/tests/test_watchlists.py b/tests/test_watchlists.py index 5325b81e..80f7a4d7 100644 --- a/tests/test_watchlists.py +++ b/tests/test_watchlists.py @@ -13,7 +13,6 @@ import piker.watchlists as wl @pytest.fixture def temp_dir(): """Creates a path to a pretend config dir in a temporary directory for - testing. """ with tempfile.TemporaryDirectory() as tempdir: @@ -56,7 +55,6 @@ def test_watchlists_config_dir_created(caplog, temp_dir): def test_watchlist_is_read_from_file(piker_dir): """Ensure json info is read from file or an empty dict is generated - and that text respresentation of a watchlist is saved to file. """ wl_temp = wl.ensure_watchlists(piker_dir) @@ -79,7 +77,6 @@ def test_new_ticker_added(piker_dir): def test_ticker_is_removed(piker_dir): """Verify that passed in ticker is removed and that a group is removed - if no tickers left. """ wl_temp = {'test': ['TEST.CN', 'TEST2.CN'], 'test2': ['TEST.CN']} From 49b760673ea27915b5244b95d728b65398782088 Mon Sep 17 00:00:00 2001 From: K0nstantine Date: Tue, 3 Apr 2018 17:53:44 -0400 Subject: [PATCH 8/9] Remove write to file from API and move to CLI --- piker/cli.py | 27 ++++++++++++++------------- piker/watchlists.py | 20 ++++++++------------ tests/test_cli.py | 3 +-- tests/test_watchlists.py | 31 +++++++++++++------------------ 4 files changed, 36 insertions(+), 45 deletions(-) diff --git a/piker/cli.py b/piker/cli.py index 37d99e86..ad254e57 100644 --- a/piker/cli.py +++ b/piker/cli.py @@ -121,7 +121,7 @@ def watch(loglevel, broker, rate, name): log = get_console_log(loglevel) # activate console logging brokermod = get_brokermod(broker) - watchlists = { + watchlists = json.dumps({ 'cannabis': [ 'EMH.VN', 'LEAF.TO', 'HVT.VN', 'HMMJ.TO', 'APH.TO', 'CBW.VN', 'TRST.CN', 'VFF.TO', 'ACB.TO', 'ABCN.VN', @@ -135,7 +135,7 @@ def watch(loglevel, broker, rate, name): 'dad': ['GM', 'TSLA', 'DOL.TO', 'CIM', 'SPY', 'SHOP.TO'], 'pharma': ['ATE.VN'], 'indexes': ['SPY', 'DAX', 'QQQ', 'DIA'], - } + }) # broker_conf_path = os.path.join( # click.get_app_dir('piker'), 'watchlists.json') # from piker.testing import _quote_streamer as brokermod @@ -179,7 +179,7 @@ def show(ctx, name): @click.pass_context def load(ctx, data): try: - wl.write_watchlists(data, ctx.obj['path']) + wl.write_sorted_json(json.loads(data), ctx.obj['path']) except (json.JSONDecodeError, IndexError): click.echo('You have passed an invalid text respresentation of a ' 'JSON object. Try again.') @@ -190,8 +190,9 @@ def load(ctx, data): @click.argument('ticker_name', nargs=1, required=True) @click.pass_context def add(ctx, name, ticker_name): - watchlist = ctx.obj['watchlist'] - wl.add_ticker(name, ticker_name, watchlist, ctx.obj['path']) + watchlist = wl.add_ticker(name, ticker_name, + ctx.obj['watchlist']) + wl.write_sorted_json(watchlist, ctx.obj['path']) @watchlists.command(help='remove ticker from watchlist') @@ -199,29 +200,29 @@ def add(ctx, name, ticker_name): @click.argument('ticker_name', nargs=1, required=True) @click.pass_context def remove(ctx, name, ticker_name): - watchlist = ctx.obj['watchlist'] - wl.remove_ticker(name, ticker_name, watchlist, ctx.obj['path']) + watchlist = wl.remove_ticker(name, ticker_name, ctx.obj['watchlist']) + wl.write_sorted_json(watchlist, ctx.obj['path']) @watchlists.command(help='delete watchlist group') @click.argument('name', nargs=1, required=True) @click.pass_context def delete(ctx, name): - watchlist = ctx.obj['watchlist'] - wl.delete_group(name, watchlist, ctx.obj['path']) + watchlist = wl.delete_group(name, ctx.obj['watchlist']) + wl.write_sorted_json(watchlist, ctx.obj['path']) @watchlists.command(help='merge a watchlist from another user') @click.argument('watchlist_to_merge', nargs=1, required=True) @click.pass_context def merge(ctx, watchlist_to_merge): - watchlist = ctx.obj['watchlist'] - wl.merge_watchlist(watchlist_to_merge, watchlist, ctx.obj['path']) + merged_watchlist = wl.merge_watchlist(watchlist_to_merge, + ctx.obj['watchlist']) + wl.write_sorted_json(merged_watchlist, ctx.obj['path']) @watchlists.command(help='dump text respresentation of a watchlist to console') @click.argument('name', nargs=1, required=False) @click.pass_context def dump(ctx, name): - watchlist = ctx.obj['watchlist'] - click.echo(json.dumps(watchlist)) + click.echo(json.dumps(ctx.obj['watchlist'])) diff --git a/piker/watchlists.py b/piker/watchlists.py index ad95ac06..b440b5f1 100644 --- a/piker/watchlists.py +++ b/piker/watchlists.py @@ -26,33 +26,29 @@ def ensure_watchlists(file_path): return json.load(f) if not os.stat(file_path).st_size == 0 else {} -def write_watchlists(watchlist, path): - write_sorted_json(json.loads(watchlist), path) - - -def add_ticker(name, ticker_name, watchlist, path): +def add_ticker(name, ticker_name, watchlist): watchlist.setdefault(name, []).append(str(ticker_name).upper()) - write_sorted_json(watchlist, path) + return watchlist -def remove_ticker(name, ticker_name, watchlist, path): +def remove_ticker(name, ticker_name, watchlist): if name in watchlist: watchlist[name].remove(str(ticker_name).upper()) if watchlist[name] == []: del watchlist[name] - write_sorted_json(watchlist, path) + return watchlist -def delete_group(name, watchlist, path): +def delete_group(name, watchlist): if name in watchlist: del watchlist[name] - write_sorted_json(watchlist, path) + return watchlist -def merge_watchlist(watchlist_to_merge, watchlist, path): +def merge_watchlist(watchlist_to_merge, watchlist): merged_watchlist = defaultdict(list) watchlist_to_merge = json.loads(watchlist_to_merge) for d in (watchlist, watchlist_to_merge): for key, value in d.items(): merged_watchlist[key].extend(value) - write_sorted_json(merged_watchlist, path) + return merged_watchlist diff --git a/tests/test_cli.py b/tests/test_cli.py index 39ba72e3..8453e794 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -100,8 +100,7 @@ def temp_dir(): testing. """ with tempfile.TemporaryDirectory() as tempdir: - config_dir = os.path.join(tempdir, 'piker') - yield config_dir + yield os.path.join(tempdir, 'piker') @pytest.fixture diff --git a/tests/test_watchlists.py b/tests/test_watchlists.py index 80f7a4d7..8e8e7f70 100644 --- a/tests/test_watchlists.py +++ b/tests/test_watchlists.py @@ -59,48 +59,43 @@ def test_watchlist_is_read_from_file(piker_dir): """ wl_temp = wl.ensure_watchlists(piker_dir) assert wl_temp == {} - wl_temp2 = '{"AA": ["TEST.CN"]}' - wl.write_watchlists(wl_temp2, piker_dir) - assert json.loads(wl_temp2) == wl.ensure_watchlists(piker_dir) + wl_temp2 = {"AA": ["TEST.CN"]} + wl.write_sorted_json(wl_temp2, piker_dir) + assert wl_temp2 == wl.ensure_watchlists(piker_dir) -def test_new_ticker_added(piker_dir): +def test_new_ticker_added(): """Ensure that a new ticker is added to a watchlist for both cases. """ - wl.add_ticker('test', 'TEST.CN', {'test': ['TEST2.CN']}, piker_dir) - wl_temp = wl.ensure_watchlists(piker_dir) + wl_temp = wl.add_ticker('test', 'TEST.CN', {'test': ['TEST2.CN']}) assert len(wl_temp['test']) == 2 - wl.add_ticker('test2', 'TEST.CN', wl_temp, piker_dir) - wl_temp = wl.ensure_watchlists(piker_dir) + wl_temp = wl.add_ticker('test2', 'TEST.CN', wl_temp) assert wl_temp['test2'] -def test_ticker_is_removed(piker_dir): +def test_ticker_is_removed(): """Verify that passed in ticker is removed and that a group is removed if no tickers left. """ wl_temp = {'test': ['TEST.CN', 'TEST2.CN'], 'test2': ['TEST.CN']} - wl.remove_ticker('test', 'TEST.CN', wl_temp, piker_dir) - wl.remove_ticker('test2', 'TEST.CN', wl_temp, piker_dir) - wl_temp = wl.ensure_watchlists(piker_dir) + wl_temp = wl.remove_ticker('test', 'TEST.CN', wl_temp) + wl_temp = wl.remove_ticker('test2', 'TEST.CN', wl_temp) assert wl_temp == {'test': ['TEST2.CN']} assert not wl_temp.get('test2') -def test_group_is_deleted(piker_dir): +def test_group_is_deleted(): """Check that watchlist group is removed. """ wl_temp = {'test': ['TEST.CN']} - wl.delete_group('test', wl_temp, piker_dir) - wl_temp = wl.ensure_watchlists(piker_dir) + wl_temp = wl.delete_group('test', wl_temp) assert not wl_temp.get('test') -def test_watchlist_is_merged(piker_dir): +def test_watchlist_is_merged(): """Ensure that watchlist is merged. """ wl_temp = {'test': ['TEST.CN']} wl_temp2 = '{"test2": ["TEST2.CN"]}' - wl.merge_watchlist(wl_temp2, wl_temp, piker_dir) - wl_temp3 = wl.ensure_watchlists(piker_dir) + wl_temp3 = wl.merge_watchlist(wl_temp2, wl_temp) assert wl_temp3 == {'test': ['TEST.CN'], 'test2': ['TEST2.CN']} From 918133f265e7de283c842da989a1f4f4a0c42618 Mon Sep 17 00:00:00 2001 From: K0nstantine Date: Wed, 4 Apr 2018 00:07:05 -0400 Subject: [PATCH 9/9] Finalize WL Management and fix merge func --- piker/cli.py | 10 +++++----- piker/watchlists.py | 4 +--- tests/test_cli.py | 12 +++++------- tests/test_watchlists.py | 4 ++-- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/piker/cli.py b/piker/cli.py index ad254e57..65377937 100644 --- a/piker/cli.py +++ b/piker/cli.py @@ -121,7 +121,7 @@ def watch(loglevel, broker, rate, name): log = get_console_log(loglevel) # activate console logging brokermod = get_brokermod(broker) - watchlists = json.dumps({ + watchlists_base = { 'cannabis': [ 'EMH.VN', 'LEAF.TO', 'HVT.VN', 'HMMJ.TO', 'APH.TO', 'CBW.VN', 'TRST.CN', 'VFF.TO', 'ACB.TO', 'ABCN.VN', @@ -135,7 +135,9 @@ def watch(loglevel, broker, rate, name): 'dad': ['GM', 'TSLA', 'DOL.TO', 'CIM', 'SPY', 'SHOP.TO'], 'pharma': ['ATE.VN'], 'indexes': ['SPY', 'DAX', 'QQQ', 'DIA'], - }) + } + watchlist_from_file = wl.ensure_watchlists(_watchlists_data_path) + watchlists = wl.merge_watchlist(watchlist_from_file, watchlists_base) # broker_conf_path = os.path.join( # click.get_app_dir('piker'), 'watchlists.json') # from piker.testing import _quote_streamer as brokermod @@ -144,11 +146,9 @@ def watch(loglevel, broker, rate, name): rate = broker_limit log.warn(f"Limiting {brokermod.__name__} query rate to {rate}/sec") trio.run(_async_main, name, watchlists[name], brokermod, rate) - # broker_conf_path = os.path.join( # click.get_app_dir('piker'), 'watchlists.json') # from piker.testing import _quote_streamer as brokermod - trio.run(_async_main, name, watchlists[name], brokermod) @cli.group() @@ -216,7 +216,7 @@ def delete(ctx, name): @click.argument('watchlist_to_merge', nargs=1, required=True) @click.pass_context def merge(ctx, watchlist_to_merge): - merged_watchlist = wl.merge_watchlist(watchlist_to_merge, + merged_watchlist = wl.merge_watchlist(json.loads(watchlist_to_merge), ctx.obj['watchlist']) wl.write_sorted_json(merged_watchlist, ctx.obj['path']) diff --git a/piker/watchlists.py b/piker/watchlists.py index b440b5f1..ae777904 100644 --- a/piker/watchlists.py +++ b/piker/watchlists.py @@ -40,14 +40,12 @@ def remove_ticker(name, ticker_name, watchlist): def delete_group(name, watchlist): - if name in watchlist: - del watchlist[name] + watchlist.pop(name, None) return watchlist def merge_watchlist(watchlist_to_merge, watchlist): merged_watchlist = defaultdict(list) - watchlist_to_merge = json.loads(watchlist_to_merge) for d in (watchlist, watchlist_to_merge): for key, value in d.items(): merged_watchlist[key].extend(value) diff --git a/tests/test_cli.py b/tests/test_cli.py index 8453e794..10e5668f 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -175,32 +175,30 @@ def test_group_deleted_from_watchlists(capfd, piker_dir): def test_watchlists_loaded(capfd, piker_dir): - expected_out_text = json.dumps({ - 'dad': ['CIM', 'DOL.TO', 'GM', 'SHOP.TO', 'SPY', 'TSLA'], - 'pharma': ['ATE.VN'], - }) expected_out = { 'dad': ['CIM', 'DOL.TO', 'GM', 'SHOP.TO', 'SPY', 'TSLA'], 'pharma': ['ATE.VN'], } + expected_out_text = json.dumps(expected_out) run(f"piker watchlists -d {piker_dir} load", expected_out_text) out = wl.ensure_watchlists(piker_dir) assert out == expected_out -def test_watchlists_is_merge(capfd, piker_dir): +def test_watchlists_are_merged(capfd, piker_dir): orig_watchlist = { 'dad': ['CIM', 'DOL.TO', 'GM', 'SHOP.TO', 'SPY', 'TSLA'], 'indexes': ['DAX', 'DIA', 'QQQ', 'SPY'], 'pharma': ['ATE.VN'], } list_to_merge = json.dumps({ - 'drugs': ['CRACK'] + 'drugs': ['CRACK'], + 'pharma': ['ATE.VN', 'MALI', 'PERCOCET'] }) expected_out = { 'dad': ['CIM', 'DOL.TO', 'GM', 'SHOP.TO', 'SPY', 'TSLA'], 'indexes': ['DAX', 'DIA', 'QQQ', 'SPY'], - 'pharma': ['ATE.VN'], + 'pharma': ['ATE.VN', 'MALI', 'PERCOCET'], 'drugs': ['CRACK'] } wl.write_sorted_json(orig_watchlist, piker_dir) diff --git a/tests/test_watchlists.py b/tests/test_watchlists.py index 8e8e7f70..f198860f 100644 --- a/tests/test_watchlists.py +++ b/tests/test_watchlists.py @@ -96,6 +96,6 @@ def test_watchlist_is_merged(): """Ensure that watchlist is merged. """ wl_temp = {'test': ['TEST.CN']} - wl_temp2 = '{"test2": ["TEST2.CN"]}' + wl_temp2 = {'test': ['TOAST'], "test2": ["TEST2.CN"]} wl_temp3 = wl.merge_watchlist(wl_temp2, wl_temp) - assert wl_temp3 == {'test': ['TEST.CN'], 'test2': ['TEST2.CN']} + assert wl_temp3 == {'test': ['TEST.CN', 'TOAST'], 'test2': ['TEST2.CN']}