From dbfed147fec7012cc9ba002868b57634944accab Mon Sep 17 00:00:00 2001 From: Marc Nijdam Date: Mon, 19 Sep 2016 17:13:09 -0700 Subject: [PATCH] Fix organization timeseries, remove leftover old commands --- _helium/__init__.py | 7 - _helium/__main__.py | 3 - _helium/_version.py | 5 - _helium/cli.py | 28 -- _helium/commands/__init__.py | 1 - _helium/commands/label.py | 37 -- _helium/commands/organization.py | 117 ------ _helium/commands/sensor-script.py | 124 ------ _helium/commands/sensor.py | 57 --- _helium/commands/timeseries.py | 234 ----------- _helium/commands/util.py | 231 ----------- _helium/commands/writer.py | 167 -------- _helium/service.py | 389 ------------------ _helium/version.py | 1 - helium_commander/commands/organization.py | 3 + helium_commander/commands/timeseries.py | 26 +- ...nds.test_organization.test_timeseries.json | 134 ++++++ tests/commands/test_organization.py | 5 + 18 files changed, 158 insertions(+), 1411 deletions(-) delete mode 100644 _helium/__init__.py delete mode 100644 _helium/__main__.py delete mode 100644 _helium/_version.py delete mode 100644 _helium/cli.py delete mode 100644 _helium/commands/__init__.py delete mode 100644 _helium/commands/label.py delete mode 100644 _helium/commands/organization.py delete mode 100644 _helium/commands/sensor-script.py delete mode 100644 _helium/commands/sensor.py delete mode 100644 _helium/commands/timeseries.py delete mode 100644 _helium/commands/util.py delete mode 100644 _helium/commands/writer.py delete mode 100644 _helium/service.py delete mode 100644 _helium/version.py create mode 100644 tests/cassettes/tests.commands.test_organization.test_timeseries.json diff --git a/_helium/__init__.py b/_helium/__init__.py deleted file mode 100644 index 2d5a061..0000000 --- a/_helium/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from helium.version import __version__ -try: - # ignore import errors for setup.py to be able to do it's work - # pre-dependency installations - from helium.service import Service -except: - pass diff --git a/_helium/__main__.py b/_helium/__main__.py deleted file mode 100644 index 5da29c4..0000000 --- a/_helium/__main__.py +++ /dev/null @@ -1,3 +0,0 @@ -if __name__ == '__main__': - from .cli import main - main() diff --git a/_helium/_version.py b/_helium/_version.py deleted file mode 100644 index 3d0eba7..0000000 --- a/_helium/_version.py +++ /dev/null @@ -1,5 +0,0 @@ - -# This file is automatically generated by setup.py. -__version__ = '0.9.3.post4' -__sha__ = 'g19deb3a' -__revision__ = 'g19deb3a' diff --git a/_helium/cli.py b/_helium/cli.py deleted file mode 100644 index d4847a7..0000000 --- a/_helium/cli.py +++ /dev/null @@ -1,28 +0,0 @@ -import helium -import click -from . import commands -from .version import __version__ - -_commands = [ - "label", - "sensor", - "element", - "sensor-script", - "cloud-script", -] - -@click.option('--api-key', - envvar='HELIUM_API_KEY', - help='your Helium API key. Can also be specified using the HELIUM_API_KEY environment variable') -@click.option('--host', - envvar='HELIUM_API_URL', - default=None, - help= 'The Helium base API URL. Can also be specified using the HELIUM_API_URL environment variable.' ) -@commands.cli(version=__version__, package='helium.commands', commands = _commands) -def cli(ctx, api_key, host, **kwargs): - ctx.obj = helium.Service(api_key, host) - -main = commands.main(cli) - -if __name__ == '__main__': - main() diff --git a/_helium/commands/__init__.py b/_helium/commands/__init__.py deleted file mode 100644 index 336916c..0000000 --- a/_helium/commands/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .util import cli, main diff --git a/_helium/commands/label.py b/_helium/commands/label.py deleted file mode 100644 index 8965ac1..0000000 --- a/_helium/commands/label.py +++ /dev/null @@ -1,37 +0,0 @@ -import helium -import click -import dpath.util as dpath -from .timeseries import options as timeseries_options -from .timeseries import dump as timeseries_dump -from .sensor import sort_option as sensor_sort_option -from .sensor import _tabulate as _tabulate_sensors -from .util import tabulate, lookup_resource_id, shorten_json_id -from .util import ResourceParamType, update_resource_relationship - - -pass_service = click.make_pass_decorator(helium.Service) - - -@click.group() -def cli(): - """Operations on labels of sensors. - """ - pass - - -@cli.command() -@click.argument('label') -@timeseries_options() -@pass_service -def dump(service, label, **kwargs): - """Dumps timeseries data to files. - - Dumps the timeseries data for all sensors in a given LABEL. - - One file is generated for each sensor with the sensor id as - filename and the file extension based on the requested dump format - - """ - label = lookup_resource_id(service.get_labels, label) - sensors = dpath.values(service.get_label_sensors(label), '/data/*/id') - timeseries_dump(service, sensors, **kwargs) diff --git a/_helium/commands/organization.py b/_helium/commands/organization.py deleted file mode 100644 index c591809..0000000 --- a/_helium/commands/organization.py +++ /dev/null @@ -1,117 +0,0 @@ -import click -import helium -import dpath.util as dpath -from .util import tabulate -from helium_commander import timeseries as ts - - -pass_service = click.make_pass_decorator(helium.Service) - - -def _tabulate(result): - def _map_user_count(json): - return len(dpath.get(json, 'relationships/user/data')) - tabulate(result, [ - ('id', 'id'), - ('name', 'attributes/name'), - ('users', _map_user_count) - ]) - - -def _tabulate_users(result): - tabulate(result, [ - ('id', 'id') - ]) - - -@click.group() -def cli(): - """Operations on the authorized organization - """ - pass - - -@pass_service -def _get_org_timeseries(service, **kwargs): - """List readings for the organization. - - Get readings for the authenticated organization. - """ - return service.get_org_timeseries(**kwargs).get('data') - - -@pass_service -def _post_org_timeseries(service, **kwargs): - """Post readings to the organization. - - Posts timeseries to the authenticated organization. - """ - return [service.post_org_timeseries(**kwargs).get('data')] - -@pass_service -def _live_org_timeseries(service, **kwargs): - """Live readings from the organization - - Reports readings from the authenticated organization as they come - in. - - """ - return service.live_org_timeseries(**kwargs) - - -cli.add_command(ts.cli(get=_get_org_timeseries, - post=_post_org_timeseries, - live=_live_org_timeseries)) - - -@cli.command() -@ts.options(page_size=5000) -@pass_service -def dump(service, **kwargs): - """Dumps timeseries data to files. - - Dumps the timeseries data for all sensors in the organization. - - One file is generated for each sensor with the sensor id as - filename and the file extension based on the requested dump format - - """ - sensors = dpath.values(service.get_sensors(), '/data/*/id') - ts.dump(service, sensors, **kwargs) - - -@cli.command() -@click.option('--name', - help="the new name for the organization") -@pass_service -def update(service, **kwargs): - """Updates the attributes of the organization. - - Updates the attributes of the currently authorized organization. - """ - org = [service.update_org(**kwargs).get('data')] - _tabulate(org) - - -@cli.command() -@pass_service -def list(service): - """Display basic information of the organization. - - Displays basic attributes of the authorized organization. - """ - org = [service.get_org().get('data')] - _tabulate(org) - - -@cli.command() -@pass_service -def user(service): - """Lists users for the organization. - - Lists the users that are part of the authorized organization. - """ - org = service.get_org().get('data') - # TODO: Once include=user is supported fix up to display 'name' - users = dpath.get(org, 'relationships/user/data') - _tabulate_users(users) diff --git a/_helium/commands/sensor-script.py b/_helium/commands/sensor-script.py deleted file mode 100644 index e12575b..0000000 --- a/_helium/commands/sensor-script.py +++ /dev/null @@ -1,124 +0,0 @@ -import click -import helium -import dpath.util as dpath -import requests -from . import util - - -pass_service = click.make_pass_decorator(helium.Service) - - -@click.group() -def cli(): - """Operations on sensor-scripts. - """ - pass - - -def _tabulate_scripts(result): - def _map_sensor_count(json): - targets = dpath.get(json, "relationships/status/data") - return len(targets) - - def _map_progress(json): - progress = dpath.values(json, "relationships/status/data/*/meta/progress") - return sum(progress)/len(progress) if len(progress) > 0 else 0 - - util.tabulate(result, [ - ('id', util.shorten_json_id), - ('created', 'meta/created'), - ('sensors', _map_sensor_count), - ('state', 'meta/state'), - ('progress', _map_progress), - ('files', util.map_script_filenames) - ]) - - -def _tabulate_status(result): - util.tabulate(result, [ - ('sensor', util.shorten_json_id), - ('mac', 'meta/mac'), - ('progress', 'meta/progress'), - ('files', util.map_script_filenames) - ]) - - -@cli.command(name="list") -@pass_service -# renamed function to avoid collision with use of the built-in list function -def _list(service): - """List all known sensor-scripts. - """ - _tabulate_scripts(service.get_sensor_scripts().get('data')) - - -@cli.command() -@click.argument('script') -@pass_service -def status(service, script): - """Get status for a script. - - Retrieves the current status for a sensor-script deploy request. - """ - script = util.lookup_resource_id(service.get_sensor_scripts, script) - script_info = service.get_sensor_script(script) - _tabulate_status(dpath.get(script_info, 'data/relationships/status/data')) - - -@cli.command() -@click.argument('file', nargs=-1) -@click.option('--main', type=click.Path(exists=True), - help="The main file for the script") -@click.option('-l', '--label', multiple=True, - help="the id of a label") -@click.option('-s', '--sensor', multiple=True, - help="the id of a sensor") -@click.option('-sf', '--sensor-file', type=click.File('rb'), - help="the name of a file with sensor ids") -@pass_service -def deploy(service, file, sensor, sensor_file, label, main): - """Deploy a sensor-script. - - Submit a deploy request of one or more FILEs. The targets for the - deploy can be a combination of sensors or labels. - - The label (-l) and sensor (-s) specifier can be given multiple - times to target multiple labels or sensors for a deploy. - - If the --main option is specified the given file is used as the - `user.lua`, i.e. the main user script for the deployment. The file - may be part of the list of files given to make it easier to - specify wildcards. - - Note: One of the given files _may_ be called user.lua if the - --main option is not given. This file will be considered the - primary script for the deploy. - - """ - sensor = list(sensor) - if sensor_file: - sensor.extend(sensor_file.read().splitlines()) - sensor = [util.lookup_resource_id(service.get_sensors, sensor_id) - for sensor_id in sensor] - deploy = service.deploy_sensor_script(file, - label=label, - sensor=sensor, - main=main).get('data') - _tabulate_scripts([deploy]) - - -@cli.command() -@click.argument('script') -@click.argument('file') -@pass_service -def show(service, script, file): - """Gets a script file from a given sensor-script. - - Fetches a FILE from a given sensor-SCRIPT. - """ - script = util.lookup_resource_id(service.get_sensor_scripts, script) - json = service.get_sensor_script(script).get('data') - file_urls = [f.encode('utf-8') for f in dpath.get(json, 'meta/scripts')] - names = dict(zip(util.extract_script_filenames(file_urls), file_urls)) - file_url = names[file] - click.echo(requests.get(file_url).text) diff --git a/_helium/commands/sensor.py b/_helium/commands/sensor.py deleted file mode 100644 index d86e40d..0000000 --- a/_helium/commands/sensor.py +++ /dev/null @@ -1,57 +0,0 @@ -import click -import helium -import dpath.util as dpath -from helium_commander import timeseries as ts -from .util import tabulate, lookup_resource_id, shorten_json_id -from .util import sort_option as _sort_option - - -pass_service = click.make_pass_decorator(helium.Service) - - -def version_option(f): - return click.option('--versions', type=click.Choice(['none', 'fw', 'all']), - default='none', - help="display version information")(f) - - -def sort_option(f): - return _sort_option(['seen', 'name'])(f) - - -def mac_option(f): - return click.option('--mac', is_flag=True, - help="Whether the given id is a mac address")(f) - - -def card_type(card_id, default): - return { - '2': 'blue', - '5': 'green', - }.get(card_id, default) - - -@click.group() -def cli(): - """Operations on physical or virtual sensors. - """ - pass - - -@cli.command() -@click.argument('sensor') -@ts.options(page_size=5000) -@mac_option -@pass_service -def dump(service, sensor, **kwargs): - """Dumps timeseries data to files. - - Dumps the timeseries data for one SENSOR to a file. - If no sensors or label is specified all sensors for the organization - are dumped. - - One file is generated with the sensor id as filename and the - file extension based on the requested dump format - """ - sensor = _find_sensor_id(service, sensor, **kwargs) - ts.dump(service, [sensor], **kwargs) diff --git a/_helium/commands/timeseries.py b/_helium/commands/timeseries.py deleted file mode 100644 index 0e95966..0000000 --- a/_helium/commands/timeseries.py +++ /dev/null @@ -1,234 +0,0 @@ -import helium -import click -import sys -from json import loads as load_json -from concurrent import futures -from functools import update_wrapper -from .util import shorten_json_id, tabulate, output_format -from .writer import for_format as writer_for_format -from contextlib import closing - - -def cli(get=None, post=None, live=None): - def tabulating_decorator(f): - def new_func(*args, **kwargs): - ctx = click.get_current_context() - data = ctx.invoke(f, *args, **kwargs) - _tabulate(data, **kwargs) - return data - return update_wrapper(new_func, f) - - def live_decorator(f): - def new_func(*args, **kwargs): - ctx = click.get_current_context() - with writer_for_format(output_format(default_format='json'), - click.utils.get_text_stream('stdout'), - mapping=_mapping_for(**kwargs)) as _writer: - with closing(ctx.invoke(f, *args, **kwargs)) as live: - for type, data in live.events(): - data = data.get('data') - _writer.write_entries([data]) - return update_wrapper(new_func, f) - - group = click.Group(name='timeseries', - short_help="Commands on timeseries readings.") - - # List - # Create options, wrapping the tabulating getter - list_params = getattr(get, '__click_params__', []) - get.__click_params__ = [] - options_get = options()(tabulating_decorator(get)) - # then construct the actual list command - list_command = click.command('list')(options_get) - list_command.params = list_params + list_command.params - group.add_command(list_command) - - # Post - # Pull of any parameters from the given poster since we want them - # at the head of the other post parameters - post_params = getattr(post, '__click_params__', []) - post.__click_params__ = [] - # Construct the post options wrapping the tabulating poster - options_post = post_options()(tabulating_decorator(post)) - post_command = click.command('post')(options_post) - # and prefix the poster's parameters - post_command.params = post_params + post_command.params - group.add_command(post_command) - - # Live - live_command = click.command('live')(live_decorator(live)) - group.add_command(live_command) - - return group - - -_options_docs = """ - Readings can be filtered by PORT and by START and END date and can - be aggregated given an aggregation type and aggregation window size. - - Dates are given in ISO-8601 and may be one of the following forms: - - \b - * YYYY-MM-DD - Example: 2016-05-05 - * YYYY-MM-DDTHH:MM:SSZ - Example: 2016-04-07T19:12:06Z - - Aggregations or bucketing of data can be done by specifying - the size of each aggregation bucket using agg-size - and one of the size specifiers. - - \b - Examples: 1m, 2m, 5m, 10m, 30m, 1h, 1d - - How data-points are aggregated is indicated by a list of - aggregation types using agg-type. - - \b - Examples: min, max, avg - - For example, to aggregate min, max for a specific port 't' and - aggregate on a daily basis use the following: - - \b - --agg-type min,max --agg-size 1d --port t -""" - - -def options(page_size=20): - """Standard options for retrieving timeseries readings. In addition it - appends the documentation for these options to the caller. - - The common usecase is to use this function as a decorator for a - command like: - - \b - ... - @timeseries.options() - def dump(): - ... - """ - options = [ - click.option('--page-size', default=page_size, - help="the number of readings to get per request"), - click.option('--port', - help="the port to filter readings on"), - click.option('--start', - help="the start date to filter readings on"), - click.option('--end', - help="the end date to filter readings on"), - click.option('--agg-size', - help="the time window of the aggregation"), - click.option('--agg-type', - help="the kinds of aggregations to perform"), - ] - - def wrapper(func): - func.__doc__ += _options_docs - for option in reversed(options): - func = option(func) - return func - return wrapper - - -_post_options_docs = """ - The given VALUE is inserted to the timeseries stream using the given PORT. - - The optional timestamp option allows fine grained control over the date of - the reading and can be given in ISO8601 form: - - \b - * YYYY-MM-DD - Example: 2016-05-05 - * YYYY-MM-DDTHH:MM:SSZ - Example: 2016-04-07T19:12:06Z -""" - - -def post_options(): - """Standard arguments and options for posting timeseries readings. - """ - options = [ - click.argument('port'), - click.argument('value', type=JSONParamType()), - click.option('--timestamp', metavar='DATE', - help='the time of the reading'), - ] - - def wrapper(func): - func.__doc__ += _post_options_docs - for option in reversed(options): - func = option(func) - return func - return wrapper - - -class JSONParamType(click.ParamType): - name = 'JSON' - - def convert(self, value, param, ctx): - try: - return load_json(value) - except ValueError: - self.fail('{} is not a valid json value'.format(value), param, ctx) - - -def _mapping_for(uuid=False, **kwargs): - agg_types = kwargs.pop('agg_type', None) - if agg_types: - agg_types = agg_types.split(',') - value_map = [(key, "attributes/value/" + key) for key in agg_types] - else: - value_map = [('value', 'attributes/value')] - map = [ - ('id', shorten_json_id if not uuid else 'id'), - ('timestamp', 'attributes/timestamp'), - ('port', 'attributes/port') - ] + value_map - return map - - -def _tabulate(result, **kwargs): - if not result: - click.echo('No data') - return - tabulate(result, _mapping_for(**kwargs)) - - -def dump(service, sensors, **kwargs): - label = str.format("Dumping {}", len(sensors)) - with click.progressbar(length=len(sensors), - label=label, - show_eta=False, - width=50) as bar: - with futures.ThreadPoolExecutor(max_workers=10) as executor: - all_futures = [] - format = output_format('csv') - for sensor_id in sensors: - future = executor.submit(_dump_one, service, sensor_id, format, - **kwargs) - future.add_done_callback(lambda f: bar.update(1)) - all_futures.append(future) - # Pass in timeout to wait to enable keyboard abort - # (Python 2.7 issue) - result_futures = futures.wait(all_futures, - return_when=futures.FIRST_EXCEPTION, - timeout=sys.maxint) - for future in result_futures.done: - future.result() # re-raises the exception - - -def _process_timeseries(writer, service, sensor_id, **kwargs): - def json_data(json): - return json['data'] if json else None - # Get the first page - res = service.get_sensor_timeseries(sensor_id, **kwargs) - writer.write_entries(json_data(res)) - while res is not None: - res = service.get_prev_page(res) - writer.write_entries(json_data(res)) - - -def _dump_one(service, sensor_id, format, **kwargs): - filename = (sensor_id+'.'+format).encode('ascii', 'replace') - with click.open_file(filename, "wb") as file: - csv_mapping = _mapping_for(shorten_json_id=False, **kwargs) - service = helium.Service(service.api_key, service.base_url) - with writer_for_format(format, file, mapping=csv_mapping) as output: - _process_timeseries(output, service, sensor_id, **kwargs) diff --git a/_helium/commands/util.py b/_helium/commands/util.py deleted file mode 100644 index a5b05f8..0000000 --- a/_helium/commands/util.py +++ /dev/null @@ -1,231 +0,0 @@ -import dpath.util as dpath -import sys -import os -import click -import uuid -from . import writer -from requests.compat import urlsplit -from functools import update_wrapper, reduce -from importlib import import_module - - -def is_uuid(str): - try: - uuid.UUID(str) - return True - except ValueError: - return False - - -def lookup_resource_id(list, id_rep, name_path=None, mac=False, **kwargs): - if hasattr(list, '__call__'): - list = list().get('data') - _is_uuid = not mac and is_uuid(id_rep) - id_rep_lower = id_rep.lower() - id_rep_len = len(id_rep) - name_path = name_path or "attributes/name" - matches = [] - for entry in list: - entry_id = entry.get('id') - if _is_uuid: - if entry_id == id_rep: - return entry_id - elif mac: - try: - entry_mac = dpath.get(entry, 'meta/mac') - if entry_mac[-id_rep_len:].lower() == id_rep_lower: - matches.append(entry_id.encode('utf8')) - except KeyError: - pass - - else: - short_id = shorten_id(entry_id) - if short_id == id_rep: - matches.append(entry_id.encode('utf8')) - else: - try: - entry_name = dpath.get(entry, name_path) - if entry_name[:id_rep_len].lower() == id_rep_lower: - matches.append(entry_id.encode('utf8')) - except KeyError: - pass - if len(matches) == 0: - raise KeyError('Id: ' + id_rep.encode('utf8') + ' does not exist') - elif len(matches) > 1: - short_matches = [shorten_id(id) for id in matches] - match_list = ' (' + ', '.join(short_matches) + ')' - raise KeyError('Ambiguous id: ' + id_rep.encode('utf8') + match_list) - - return matches[0] - - -def shorten_id(str): - return str.split('-')[0] - - -def shorten_json_id(json, **kwargs): - # Ugh, reaching for global state isn't great but very convenient here - _uuid = kwargs.get('uuid', False) - try: - root_context = click.get_current_context().find_root() - shorten = not root_context.params.get('uuid', False) - except RuntimeError: - shorten = not _uuid - json_id = json.get('id') - return shorten_id(json_id) if shorten else json_id - - -def tabulate(result, map, **kwargs): - if not map or not result: - return result - - file = kwargs.pop('file', click.utils.get_text_stream('stdout')) - with writer.for_format(output_format(**kwargs), - file, mapping=map, **kwargs) as _writer: - _writer.write_entries(result) - - -def map_script_filenames(json): - files = dpath.get(json, 'meta/scripts') - return ', '.join(extract_script_filenames(files)) - - -def extract_script_filenames(files): - return [urlsplit(url).path.split('/')[-1] for url in files] - - -def output_format(default_format='tabular', **kwargs): - override_format = kwargs.get('format') - try: - root_context = click.get_current_context().find_root() - click_format = root_context.params.get('format') - except RuntimeError: - click_format = None - return override_format or click_format or default_format - - -def sort_option(options): - options = [ - click.option('--reverse', is_flag=True, - help='Sort in reverse order'), - click.option('--sort', type=click.Choice(options), - help='How to sort the result') - ] - - def wrapper(func): - for option in reversed(options): - func = option(func) - return func - return wrapper - - -class ResourceParamType(click.ParamType): - name = 'resource' - - def __init__(self, nargs=-1, metavar='TEXT'): - self.nargs = nargs - self.metavar = metavar - - def get_metavar(self, param): - metavar = self.metavar - if self.nargs == -1: - return '{0}[,{0},...]* | @filename'.format(metavar) - else: - return metavar - - def convert(self, value, param, ctx): - def collect_resources(acc, resource_rep): - if resource_rep.startswith('@'): - for line in click.open_file(resource_rep[1:]): - acc.append(line.strip()) - else: - acc.append(resource_rep) - return acc - nargs = self.nargs - value = value.split(',') if isinstance(value, basestring) else value - resources = reduce(collect_resources, value, []) - if nargs > 0 and nargs != len(resources): - self.fail('Expected {} resources, but got {}'.format(nargs, len(resources))) - return resources - - def __repr__(self): - 'Resource(metavar={}, nargs={})'.fomat(self.metavar, self.nargs) - - -def update_resource_relationship(resources, find_item_id, **kwargs): - """Constructs an updated relationship list. - - Takes the items in the relationship and items to be added in - 'added' or removed in 'remove' and returns a new list with the new - list of ids or None if no difference was detected - """ - def _extract_list(items): - return items.split(',') if isinstance(items, basestring) else items - - item_ids = dpath.values(resources, "*/id") - remove_items = kwargs.pop('remove', None) - if remove_items: - remove_items = _extract_list(remove_items) - remove_items = [find_item_id(item, **kwargs) for item in remove_items] - item_ids = set.difference(set(item_ids), set(remove_items)) - - add_items = kwargs.pop('add', None) - if add_items: - add_items = _extract_list(add_items) - add_items = [find_item_id(item, **kwargs) for item in add_items] - item_ids = set.union(set(item_ids), set(add_items)) - - if add_items or remove_items: - if item_ids is None: - item_ids = [] - return item_ids - - return None - - -CONTEXT_SETTINGS = dict( - help_option_names=['-h', '--help'] -) - - -def cli(version=None, package=None, commands=None): - class Loader(click.MultiCommand): - def list_commands(self, ctx): - commands.sort() - return commands - - def get_command(self, ctx, name): - try: - command = import_module(package + "." + name) - return command.cli - except ImportError as e: - click.secho(str(e), fg='red') - return - - def decorator(f): - @click.option('--uuid', is_flag=True, - help="Whether to display long identifiers") - @click.option('--format', - type=click.Choice(['csv', 'json', 'tabular']), - default=None, - help="The output format (default 'tabular')") - @click.version_option(version=version) - @click.command(cls=Loader, context_settings=CONTEXT_SETTINGS) - @click.pass_context - def new_func(ctx, *args, **kwargs): - ctx.invoke(f, ctx, *args, **kwargs) - return update_wrapper(new_func, f) - return decorator - - -def main(cli): - def decorator(): - args = sys.argv[1:] - try: - cli.main(args=args, prog_name=None) - except Exception as e: - if os.environ.get("HELIUM_COMMANDER_DEBUG"): - raise - click.secho(str(e), fg='red') - sys.exit(1) - return decorator diff --git a/_helium/commands/writer.py b/_helium/commands/writer.py deleted file mode 100644 index 39fec7c..0000000 --- a/_helium/commands/writer.py +++ /dev/null @@ -1,167 +0,0 @@ -import unicodecsv as csv -import json -import abc -import terminaltables -import dpath.util as dpath -from textwrap import wrap -from operator import itemgetter -from collections import OrderedDict -from contextlib import contextmanager - - -@contextmanager -def for_format(format, file, **kwargs): - json_opts = kwargs.pop('json', None) - if format == 'json': - json_opts = json_opts or { - "indent": 4 - } - result = JSONWriter(file, json_opts=json_opts, **kwargs) - elif format == 'csv': - json_opts = json_opts or { - "indent": None - } - result = CSVWriter(file, json_opts=json_opts, **kwargs) - elif format == 'tabular': - json_opts = json_opts or { - "indent": 0 - } - result = TerminalWriter(file, json_opts=json_opts, **kwargs) - try: - result.start() - yield result - finally: - result.finish() - - -class BaseWriter: - __metaclass__ = abc.ABCMeta - - def __init__(self, file, **kwargs): - self.file = file - self.mapping = OrderedDict(kwargs.pop('mapping')) - self.json_opts = kwargs.pop('json', None) - self.sort = kwargs.pop('sort', None) - self.reverse = kwargs.pop('reverse', False) - - def start(self): - return - - @abc.abstractmethod - def write_entries(self, entries): - return - - def finish(self): - return - - def lookup(self, o, path, **kwargs): - try: - if hasattr(path, '__call__'): - result = path(o) - else: - result = dpath.get(o, path) - except KeyError: - return "" - json_opts = kwargs.pop('json', None) - if isinstance(result, dict) and json_opts: - result = json.dumps(result, **json_opts) - else: - result = result - return result - - -class CSVWriter(BaseWriter): - def __init__(self, file, **kwargs): - super(CSVWriter, self).__init__(file, **kwargs) - self.writer = csv.DictWriter(file, self.mapping.keys()) - - def start(self): - self.writer.writeheader() - - def write_entries(self, entries): - if entries is None: - return - for entry in entries: - row = {key: self.lookup(entry, path, json=self.json_opts) - for key, path in self.mapping.iteritems()} - self.writer.writerow(row) - - -class JSONWriter(BaseWriter): - def __init__(self, file, **kwargs): - super(JSONWriter, self).__init__(file, **kwargs) - - def start(self): - self.file.write('[\n') - self.is_first_entries = True - - def write_entries(self, entries): - if entries is None: - return - for entry in entries: - if self.is_first_entries: - self.is_first_entries = False - else: - self.file.write(',') - - if self.mapping: - entry = {key: self.lookup(entry, path) - for key, path in self.mapping.iteritems()} - - json.dump(entry, self.file, **self.json_opts) - - def finish(self): - self.file.write('\n]\n') - - -class TerminalWriter(BaseWriter): - def __init__(self, file, **kwargs): - super(TerminalWriter, self).__init__(file, **kwargs) - self.data = [[key.upper() for key in self.mapping.keys()]] - self.json_opts = kwargs.pop('json_opts', None) - self.max_width = kwargs.pop('max_width', None) - - def start(self): - pass - - def order_entries(self, entries): - if not self.mapping: - return entries - if self.sort: - sort_index = self.mapping.keys().index(self.sort) - entries = sorted(entries, - key=itemgetter(sort_index), - reverse=self.reverse) - elif self.reverse: - entries = reversed(entries) - return entries - - def write_entries(self, entries): - def safe_unicode(o, *args): - try: - return unicode(o, *args) - except UnicodeDecodeError: - return unicode(str(o).encode('string_escape')) - - def map_entry(o): - return [safe_unicode(self.lookup(o, path, json=self.json_opts)) - for _, path in self.mapping.items()] - - if entries is None: - return - if self.mapping: - entries = [map_entry(o) for o in entries] - - self.data.extend(self.order_entries(entries)) - - def finish(self): - table = terminaltables.AsciiTable(self.data) - # table.inner_column_border=False - # table.inner_heading_row_border=False - # table.outer_border=False - last_column = len(self.mapping) - 1 - max_width = self.max_width or table.column_max_width(last_column) - for entry in table.table_data: - entry[last_column] = '\n'.join(wrap(entry[last_column], max_width)) - self.file.write(table.table) - self.file.write('\n') diff --git a/_helium/service.py b/_helium/service.py deleted file mode 100644 index 358a9c6..0000000 --- a/_helium/service.py +++ /dev/null @@ -1,389 +0,0 @@ -import io -import os -from . import __version__ -from json import loads as load_json, dumps as dump_json -from requests import Session, HTTPError -from requests.compat import urljoin, urlsplit - - -class LiveService: - """Represents a live SSE endpoint. - - Some of the helium endpoints return a Server Sent Event connection - which returns events as they happen in the system. The most common - usecase for this are the "live" endpoints. - - Typically you work with this class by calling one of the Service - endpoints which returns an instance of this and then iterate over - the result of the `events` method. - - """ - _FIELD_SEPARATOR = ':' - - def __init__(self, source): - """Initializes a LiveSession. - - :param source: A requests.Response object - """ - self.source = source - - def _read(self): - data = "" - for line in self.source.iter_lines(decode_unicode=True): - if not line.strip(): - yield data - data = "" - data = data + "\n" + line - - def events(self): - for chunk in self._read(): - event_type = None - event_data = "" - for line in chunk.splitlines(): - # Ignore empty lines or comments - # Comments lines in SSE start with the field separator - if not line.strip() or line.startswith(self._FIELD_SEPARATOR): - continue - - data = line.split(self._FIELD_SEPARATOR, 1) - field = data[0] - data = data[1] - - if field == 'event': - event_type = data - elif field == 'data': - event_data += data - else: - event_data = data - - if not event_data: - # Don't report on events with no data - continue - - if event_data.endswith('\n'): - event_data = event_data[:-1] - - event_data = load_json(event_data) - yield (event_type, event_data) - - def close(self): - self.source.close() - - -class Service: - production_base_url = "https://api.helium.com/v1" - user_agent = "Helium/"+__version__ - - def __init__(self, api_key, base_url=None): - self.api_key = api_key - self.base_url = base_url if base_url else self.production_base_url - # ensure we can urljoin paths to the base url - if not self.base_url.endswith('/'): - self.base_url += '/' - self.session = Session() - - def mk_params(self, map, kwargs): - result = {} - for kw_key, result_key in map.iteritems(): - value = kwargs.get(kw_key, None) - if value: - result[result_key] = value - return result - - def mk_attributes_body(self, type, id, attributes): - if attributes is None or not type: - return None - result = { - "data": { - "attributes": attributes, - "type": type - } - } - if id is not None: - result['data']['id'] = id - return result - - def mk_relationships_body(self, type, ids): - if ids is None or not type: - return None - return { - "data": [{"id": id, "type": type} for id in ids] - } - - def mk_datapoint_body(self, **kwargs): - params = self.mk_params({ - 'value': 'value', - 'port': 'port', - 'timestamp': 'timestamp', - }, kwargs) - return self.mk_attributes_body("data-point", None, params) - - def mk_timeseries_params(self, **kwargs): - return self.mk_params({ - "page_id": "page[id]", - "page_size": "page[size]", - "port": "filter[port]", - "start": "filter[start]", - "end": "filter[end]", - "agg_size": "agg[size]", - "agg_type": "agg[type]" - }, kwargs) - - def mk_include_params(self, **kwargs): - return self.mk_params({ - "include": "include" - }, kwargs) - - def do(self, method, path, - params=None, json=None, - files=None, stream=False): - if path is None: - return None - url = path if urlsplit(path).scheme else urljoin(self.base_url, path) - params = params or {} - headers = { - 'Authorization': self.api_key, - 'User-agent': self.user_agent - } - res = self.session.request(method, url, - stream=stream, - params=params, - json=json, - files=files, - headers=headers, - allow_redirects=True) - if res.ok: - try: - return res.json() if not stream else res - except ValueError: - return res - else: - try: - # Pull out the first error info block (for now) - errors = res.json()['errors'] - err = errors[0] - errmsg = str.format("{} Error: {} for url {}", - err['status'], - err['detail'], - res.url) - # and use that as the error - raise HTTPError(errmsg, response=res) - except ValueError: - # otherwise raise as a base HTTPError - res.raise_for_status() - - def get(self, path, **kwargs): - return self.do('GET', path, **kwargs) - - def post(self, path, **kwargs): - return self.do('POST', path, **kwargs) - - def delete(self, path, **kwargs): - return self.do('DELETE', path, **kwargs) - - def patch(self, path, **kwargs): - return self.do('PATCH', path, **kwargs) - - def is_production(self): - return self.base_url == self.production_base_url - - def get_user(self): - return self.get('user') - - def auth_user(self, user, password): - body = { - "email": user, - "password": password - } - return self.post('user/auth', json=body) - - def create_sensor(self, **kwargs): - body = self.mk_attributes_body("sensor", None, kwargs) - return self.post('sensor', json=body) - - def update_sensor(self, sensor_id, **kwargs): - body = self.mk_attributes_body("sensor", sensor_id, kwargs) - return self.patch('sensor/{}'.format(sensor_id), json=body) - - def delete_sensor(self, sensor_id): - return self.delete('sensor/{}'.format(sensor_id)) - - def get_sensors(self): - return self.get("sensor") - - def get_sensor(self, sensor_id): - return self.get('sensor/{}'.format(sensor_id)) - - def get_sensor_timeseries(self, sensor_id, **kwargs): - params = self.mk_timeseries_params(**kwargs) - return self.get('sensor/{}/timeseries'.format(sensor_id), - params=params) - - def post_sensor_timeseries(self, sensor_id, **kwargs): - body = self.mk_datapoint_body(**kwargs) - return self.post('sensor/{}/timeseries'.format(sensor_id), - json=body) - - def live_sensor_timeseries(self, sensor_id, **kwargs): - params = self.mk_timeseries_params(**kwargs) - source = self.get('sensor/{}/timeseries/live'.format(sensor_id), - params=params, stream=True) - return LiveService(source) - - def get_org(self): - return self.get('organization') - - def update_org(self, **kwargs): - body = self.mk_attributes_body("organization", None, kwargs) - return self.patch('organization', json=body) - - def get_org_timeseries(self, **kwargs): - params = self.mk_timeseries_params(**kwargs) - return self.get('organization/timeseries', params=params) - - def live_org_timeseries(self, **kwargs): - params = self.mk_timeseries_params(**kwargs) - source = self.get('organization/timeseries/live', params=params) - return LiveService(source) - - def post_org_timeseries(self, **kwargs): - body = self.mk_datapoint_body(**kwargs) - return self.post('organization/timeseries', json=body) - - def _get_json_path(self, json, path): - try: - return reduce(dict.__getitem__, path, json) - except KeyError: - return None - - def get_prev_page(self, json): - prev_url = self._get_json_path(json, ["links", "prev"]) - return self.get(prev_url) - - def get_next_page(self, json): - next_url = self._get_json_path(json, ["links", "next"]) - return self.get(next_url) - - def create_label(self, name=None): - body = self.mk_attributes_body("label", None, { - "name": name - }) if name else None - return self.post('label', json=body) - - def update_label(self, label_id, **kwargs): - body = self.mk_attributes_body('label', label_id, kwargs) - return self.patch('label/{}'.format(label_id), json=body) - - def delete_label(self, label_id): - return self.delete('label/{}'.format(label_id)) - - def get_labels(self, **kwargs): - params = self.mk_include_params(**kwargs) - return self.get('label', params=params) - - def get_label(self, label_id, **kwargs): - params = self.mk_include_params(**kwargs) - return self.get('label/{}'.format(label_id), params=params) - - def get_label_sensors(self, label_id): - return self.get('label/{}/relationships/sensor'.format(label_id)) - - def update_label_sensors(self, label_id, sensor_ids): - body = self.mk_relationships_body("sensor", sensor_ids) - return self.patch('label/{}/relationships/sensor'.format(label_id), - json=body) - - def get_elements(self, **kwargs): - params = self.mk_include_params(**kwargs) - return self.get('element', params=params) - - def get_element(self, element_id, **kwargs): - params = self.mk_include_params(**kwargs) - return self.get('element/{}'.format(element_id), params=params) - - def update_element(self, element_id, **kwargs): - body = self.mk_attributes_body("element", element_id, kwargs) - return self.patch('element/{}'.format(element_id), json=body) - - def get_element_timeseries(self, element_id, **kwargs): - params = self.mk_timeseries_params(**kwargs) - return self.get('element/{}/timeseries'.format(element_id), - params=params) - - def post_element_timeseries(self, element_id, **kwargs): - body = self.mk_datapoint_body(**kwargs) - return self.post('element/{}/timeseries'.format(element_id), - json=body) - - def live_element_timeseries(self, element_id, **kwargs): - params = self.mk_timeseries_params(**kwargs) - source = self.get('element/{}/timeseries/live'.format(element_id), - params=params, stream=True) - return LiveService(source) - - def _lua_uploads_from_files(self, files, **kwargs): - def basename(f): - return os.path.basename(f) - - def lua_file(name): - return (basename(name), io.open(name, 'rb'), 'application/x-lua') - - main_file = kwargs.pop('main', None) - # construct a dictionary of files that are not the main file - files = {basename(name): lua_file(name) for name in files - if name != main_file} - # then add the main file if given - if main_file: - files["user.lua"] = lua_file(main_file) - return files - - def get_sensor_scripts(self): - return self.get('sensor-script') - - def get_sensor_script(self, script_id): - return self.get('sensor-script/{}'.format(script_id)) - - def deploy_sensor_script(self, files, **kwargs): - manifest = { - "target": { - "labels": kwargs.pop('label', []), - "sensors": kwargs.pop('sensor', []) - } - } - uploads = self._lua_uploads_from_files(files, **kwargs) - uploads['manifest'] = ('manifest.json', - dump_json(manifest), - 'application/json') - return self.post('sensor-script', files=uploads) - - def get_cloud_scripts(self): - return self.get('cloud-script') - - def get_cloud_script(self, script_id): - return self.get('cloud-script/{}'.format(script_id)) - - def get_cloud_script_timeseries(self, script_id, **kwargs): - params = self.mk_timeseries_params(**kwargs) - return self.get('cloud-script/{}/timeseries'.format(script_id), - params=params) - - def delete_cloud_script(self, script_id): - return self.delete('cloud-script/{}'.format(script_id)) - - def _mk_cloud_script_attributes(self, script_id, **kwargs): - return self.mk_attributes_body("cloud-script", script_id, { - "state": kwargs.pop("state", "running"), - "name": kwargs.pop("name", None) - }) - - def update_cloud_script(self, script_id, **kwargs): - body = self._mk_cloud_script_attributes(script_id, **kwargs) - return self.patch('cloud-script/{}'.format(script_id), json=body) - - def deploy_cloud_script(self, files, **kwargs): - uploads = self._lua_uploads_from_files(files, **kwargs) - attributes = self._mk_cloud_script_attributes(None, **kwargs) - uploads['attributes'] = ('attributes.json', - dump_json(attributes), - 'application/json') - return self.post('cloud-script', files=uploads) diff --git a/_helium/version.py b/_helium/version.py deleted file mode 100644 index 4b0f5cf..0000000 --- a/_helium/version.py +++ /dev/null @@ -1 +0,0 @@ -__version__="0.9.3" diff --git a/helium_commander/commands/organization.py b/helium_commander/commands/organization.py index 8c77adb..a8665e5 100644 --- a/helium_commander/commands/organization.py +++ b/helium_commander/commands/organization.py @@ -38,3 +38,6 @@ def update(client, name): org = org.update(name=name) org = Organization.singleton(client, include=[User, Element, Sensor]) Organization.display(client, [org]) + + +cli.add_command(timeseries.cli(Organization, singleton=True)) diff --git a/helium_commander/commands/timeseries.py b/helium_commander/commands/timeseries.py index 4a355a2..8d509cf 100644 --- a/helium_commander/commands/timeseries.py +++ b/helium_commander/commands/timeseries.py @@ -8,47 +8,53 @@ pass_client = click.make_pass_decorator(Client) -def cli(cls, lookup_options=None): +def cli(cls, singleton=False): group = click.Group(name='timeseries', short_help="Commands on timeseries readings.") resource_type = cls._resource_type() + id_required = not singleton + + def _fetch_resource(client, id, **kwargs): + if singleton: + resource = cls.singleton(client) + else: + mac = kwargs.pop('mac', False) + resource = cls.lookup(client, id, mac=mac) + return resource @group.command('list') - @click.argument('id', metavar=resource_type) + @click.argument('id', metavar=resource_type, required=id_required) @list_options() @click.option('--count', default=20, help="the number of readings to fetch. Use -1 for all") @pass_client def _list(client, id, **kwargs): """Get timeseries readings.""" - mac = kwargs.pop('mac', False) count = kwargs.pop('count', 20) - resource = cls.lookup(client, id, mac=mac) + resource = _fetch_resource(client, id, **kwargs) timeseries = resource.timeseries(**kwargs) if count >= 0: timeseries = islice(timeseries, count) DataPoint.display(client, timeseries, **kwargs) @group.command('post') - @click.argument('id', metavar=resource_type) + @click.argument('id', metavar=resource_type, required=id_required) @post_options() @pass_client def _post(client, id, **kwargs): """Post timeseries readings.""" - mac = kwargs.pop('mac', False) - resource = cls.lookup(client, id, mac=mac) + resource = _fetch_resource(client, id, **kwargs) timeseries = resource.timeseries() point = timeseries.post(**kwargs) DataPoint.display(client, [point], **kwargs) @group.command('live') - @click.argument('id', metavar=resource_type) + @click.argument('id', metavar=resource_type, required=id_required) @list_options() @pass_client def _live(client, id, **kwargs): """Get live timeseries readings""" - mac = kwargs.pop('mac', False) - resource = cls.lookup(client, id, mac=mac) + resource = _fetch_resource(client, id, **kwargs) timeseries = resource.timeseries(**kwargs) mapping = DataPoint.display_map(client) with cls.display_writer(client, mapping, **kwargs) as writer: diff --git a/tests/cassettes/tests.commands.test_organization.test_timeseries.json b/tests/cassettes/tests.commands.test_organization.test_timeseries.json new file mode 100644 index 0000000..71b7c20 --- /dev/null +++ b/tests/cassettes/tests.commands.test_organization.test_timeseries.json @@ -0,0 +1,134 @@ +{ + "http_interactions": [ + { + "recorded_at": "2016-09-20T00:01:57", + "request": { + "body": { + "encoding": "utf-8", + "string": "" + }, + "headers": { + "Accept": "application/json", + "Accept-Charset": "utf-8", + "Accept-Encoding": "gzip, deflate", + "Authorization": "", + "Connection": "keep-alive", + "Content-Type": "application/json", + "User-Agent": "helium-python/0.1.0" + }, + "method": "GET", + "uri": "https://api.helium.com/v1/organization" + }, + "response": { + "body": { + "base64_string": "H4sIAAAAAAAEA31W225cNwz8l30OA91F+SlAfyF9SREEpEg1BnwJ7HWBJvC/d47t2gtvdt981uJFo5khf+1M9rK7+LWT/f7uUh/2fr993ci17y52+Np/e/iBM/7t6acPu/3ltf+8vdn+++fnP3aPH3Z3fiX7y9ub+++XP56CH+79bkvynPqvX7tL23LdXn/67leXD9cf5+31Dpn+/bFleTr9+OHllPk/JHPePtzs79+Ovz/9FWXv/eb+9jd1AqtoZSULo1KZfdKIaVJcHFKxEW3Ut3wvWV7r17hybcVIlw4q2jspN6YlnFc0TyuP09GtiZfYArUiiQpKkaoH4hG1B+9Ro5+ODhaE+1rEZWYqPU/SXNCFrBZTmrGVciY6Ws01RkqJjUrNKOuyqBcrecauInw6eqaRVq2ZpCdFNCcaNQrpCKHGsEKyM/fWkrqoL/IVnEpJY8OcER3HEpegdqbzNdDx4kVjmlDJMeOv1Wmu5V6t9lDDmc67xmiBiWsuVJoXkogHDDJyaJPH0H46WlRyD+rEuQM1LZEURalx0xDaMO5nOi8j9TrQtCwNVKw1Ak/wbIiNlsJMfKbz6KDqrAjUtsCWuUD9MqlFtJwVRBz5dOdTDYxcleocQE1LpbFip76CsiEFh3g6elNDzUkItOpU2CpJcafFnnoeRfI8wxaN2StYRaEKMB8T7+250gwjLs8lQSyna2tW7TaVmEGUEkujkYJQFw8DpeMKZ+7dQUfPIPb0yVSC48XyypSNN4kC8/ab994cw6/82m/2x9YEuXXp3Gk04FlWB/k9BaoSdRbgKa29Xef/NK+ekTSAqStR6I6XSGABN9xHw+TcYs0aDzo6Co/Dysg6CVTaLAeuJROWk6ODRKPPXA4M6yjc3JIrg/WzbdXR94DNUHPJOfHy0g+UexTel1oIq8EpF5jY2wZnNuphziwCQR+K7ygcZlPdAugLzm/qQ3VD89Wzz8oxJztgwlF46z5t451I26iwCrHUCrPOBr+csawD+R2Fu07T3jpJDmCxJegv4NM8ppJduucDFh+FR4XMHVYHewJ0IhgV1hX4mXr0Id0O7PoofCZRaDxAclvzEgqNkgq5c41t5FVMz9Amx9RcYVzZFNYTJhizgpHx6o7eMTPONQ/JrNnRrbLi7mMAxFowPBZ3eNBoyc6xjod3eLrA+CCeklFYAAcZPHvaNMuHw+bo7uwYo6qZWm4TtMHUEomTOEiHaydw+pxkqkMTrTAAw6TG3SONgLsItGcjZczvs6RlbTJAljEzkG8dE2PARnqS2GaGKaV0BnmGomfHk0+pePclgj4UstPMqou5nVXcDKZ1PlEVqi0YrSQDOcAi/D7Zgh341yt0mwVdYUpeHRuQcRXnDt5bwN7gFrED4FlmHWWA4wIfeLvPc5JX+zGOmJ5pG90CMCBh0gYaMvMaa9aWx8EQeh8cvbqPTFAaSFgMk1Aww3v3AoBrnFVOVtZl7IyndyxKWBrgHcAVGWLseEDsTXbgXO8qY6GpeUGsaWJhKlEBYNVGPjM4lYCBHDDoXfBoDKA5Us+oVxQ9qFmjJOh5YJVxPh1cYvWWsdklBe9Km3CNDhZ3TgCjz1aTnb5zzbEapI6FCpUjjJcBPFiUsV1ajJMPDO9d2ya9RKyQQBuQF8PIHNvWomV4YZjB8gPNvgR/fQRxnlfo43X82p/X93nnWNK3PTuFWCkMiuFzChc1XET+GDnzaF9wqedl/uVcezkX+aKOi9Q/Viivf9mW+pfl/Pbub7m5/Pm03+8eH/8DnM2w3S8MAAA=", + "encoding": null, + "string": "" + }, + "headers": { + "Access-Control-Allow-Headers": "Origin, Content-Type, Accept, Authorization", + "Access-Control-Allow-Origin": "*", + "Airship-Quip": "sharkfed", + "Airship-Trace": "b13,b12,b11,b10,b09,b08,b07,b06,b05,b04,b03,c03,c04,d04,e05,e06,f06,f07,g07,g08,h10,i12,l13,m16,n16,o16,o17,o18", + "Content-Encoding": "gzip", + "Content-Type": "application/json", + "Date": "Tue, 20 Sep 2016 00:01:57 GMT", + "Server": "Warp/3.2.7", + "Transfer-Encoding": "chunked" + }, + "status": { + "code": 200, + "message": "OK" + }, + "url": "https://api.helium.com/v1/organization" + } + }, + { + "recorded_at": "2016-09-20T00:01:57", + "request": { + "body": { + "encoding": "utf-8", + "string": "" + }, + "headers": { + "Accept": "application/json", + "Accept-Charset": "utf-8", + "Accept-Encoding": "gzip, deflate", + "Authorization": "", + "Connection": "keep-alive", + "Content-Type": "application/json", + "User-Agent": "helium-python/0.1.0" + }, + "method": "GET", + "uri": "https://api.helium.com/v1/organization" + }, + "response": { + "body": { + "base64_string": "H4sIAAAAAAAEA31W225cNwz8l30OA91F+SlAfyF9SREEpEg1BnwJ7HWBJvC/d47t2gtvdt981uJFo5khf+1M9rK7+LWT/f7uUh/2fr993ci17y52+Np/e/iBM/7t6acPu/3ltf+8vdn+++fnP3aPH3Z3fiX7y9ub+++XP56CH+79bkvynPqvX7tL23LdXn/67leXD9cf5+31Dpn+/bFleTr9+OHllPk/JHPePtzs79+Ovz/9FWXv/eb+9jd1AqtoZSULo1KZfdKIaVJcHFKxEW3Ut3wvWV7r17hybcVIlw4q2jspN6YlnFc0TyuP09GtiZfYArUiiQpKkaoH4hG1B+9Ro5+ODhaE+1rEZWYqPU/SXNCFrBZTmrGVciY6Ws01RkqJjUrNKOuyqBcrecauInw6eqaRVq2ZpCdFNCcaNQrpCKHGsEKyM/fWkrqoL/IVnEpJY8OcER3HEpegdqbzNdDx4kVjmlDJMeOv1Wmu5V6t9lDDmc67xmiBiWsuVJoXkogHDDJyaJPH0H46WlRyD+rEuQM1LZEURalx0xDaMO5nOi8j9TrQtCwNVKw1Ak/wbIiNlsJMfKbz6KDqrAjUtsCWuUD9MqlFtJwVRBz5dOdTDYxcleocQE1LpbFip76CsiEFh3g6elNDzUkItOpU2CpJcafFnnoeRfI8wxaN2StYRaEKMB8T7+250gwjLs8lQSyna2tW7TaVmEGUEkujkYJQFw8DpeMKZ+7dQUfPIPb0yVSC48XyypSNN4kC8/ab994cw6/82m/2x9YEuXXp3Gk04FlWB/k9BaoSdRbgKa29Xef/NK+ekTSAqStR6I6XSGABN9xHw+TcYs0aDzo6Co/Dysg6CVTaLAeuJROWk6ODRKPPXA4M6yjc3JIrg/WzbdXR94DNUHPJOfHy0g+UexTel1oIq8EpF5jY2wZnNuphziwCQR+K7ygcZlPdAugLzm/qQ3VD89Wzz8oxJztgwlF46z5t451I26iwCrHUCrPOBr+csawD+R2Fu07T3jpJDmCxJegv4NM8ppJduucDFh+FR4XMHVYHewJ0IhgV1hX4mXr0Id0O7PoofCZRaDxAclvzEgqNkgq5c41t5FVMz9Amx9RcYVzZFNYTJhizgpHx6o7eMTPONQ/JrNnRrbLi7mMAxFowPBZ3eNBoyc6xjod3eLrA+CCeklFYAAcZPHvaNMuHw+bo7uwYo6qZWm4TtMHUEomTOEiHaydw+pxkqkMTrTAAw6TG3SONgLsItGcjZczvs6RlbTJAljEzkG8dE2PARnqS2GaGKaV0BnmGomfHk0+pePclgj4UstPMqou5nVXcDKZ1PlEVqi0YrSQDOcAi/D7Zgh341yt0mwVdYUpeHRuQcRXnDt5bwN7gFrED4FlmHWWA4wIfeLvPc5JX+zGOmJ5pG90CMCBh0gYaMvMaa9aWx8EQeh8cvbqPTFAaSFgMk1Aww3v3AoBrnFVOVtZl7IyndyxKWBrgHcAVGWLseEDsTXbgXO8qY6GpeUGsaWJhKlEBYNVGPjM4lYCBHDDoXfBoDKA5Us+oVxQ9qFmjJOh5YJVxPh1cYvWWsdklBe9Km3CNDhZ3TgCjz1aTnb5zzbEapI6FCpUjjJcBPFiUsV1ajJMPDO9d2ya9RKyQQBuQF8PIHNvWomV4YZjB8gPNvgR/fQRxnlfo43X82p/X93nnWNK3PTuFWCkMiuFzChc1XET+GDnzaF9wqedl/uVcezkX+aKOi9Q/Viivf9mW+pfl/Pbub7m5/Pm03+8eH/8DnM2w3S8MAAA=", + "encoding": null, + "string": "" + }, + "headers": { + "Access-Control-Allow-Headers": "Origin, Content-Type, Accept, Authorization", + "Access-Control-Allow-Origin": "*", + "Airship-Quip": "firm pat on the back", + "Airship-Trace": "b13,b12,b11,b10,b09,b08,b07,b06,b05,b04,b03,c03,c04,d04,e05,e06,f06,f07,g07,g08,h10,i12,l13,m16,n16,o16,o17,o18", + "Content-Encoding": "gzip", + "Content-Type": "application/json", + "Date": "Tue, 20 Sep 2016 00:01:57 GMT", + "Server": "Warp/3.2.7", + "Transfer-Encoding": "chunked" + }, + "status": { + "code": 200, + "message": "OK" + }, + "url": "https://api.helium.com/v1/organization" + } + }, + { + "recorded_at": "2016-09-20T00:01:57", + "request": { + "body": { + "encoding": "utf-8", + "string": "" + }, + "headers": { + "Accept": "application/json", + "Accept-Charset": "utf-8", + "Accept-Encoding": "gzip, deflate", + "Authorization": "", + "Connection": "keep-alive", + "Content-Type": "application/json", + "User-Agent": "helium-python/0.1.0" + }, + "method": "GET", + "uri": "https://api.helium.com/v1/organization/timeseries?page%5Bsize%5D=20" + }, + "response": { + "body": { + "base64_string": "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", + "encoding": null, + "string": "" + }, + "headers": { + "Access-Control-Allow-Headers": "Origin, Content-Type, Accept, Authorization", + "Access-Control-Allow-Origin": "*", + "Airship-Quip": "WARNING: ulimit -n is 1024", + "Airship-Trace": "b13,b12,b11,b10,b09,b08,b07,b06,b05,b04,b03,c03,c04,d04,e05,e06,f06,f07,g07,g08,h10,i12,l13,m16,n16,o16,o17,o18", + "Content-Encoding": "gzip", + "Content-Type": "application/json", + "Date": "Tue, 20 Sep 2016 00:01:57 GMT", + "Server": "Warp/3.2.7", + "Transfer-Encoding": "chunked" + }, + "status": { + "code": 200, + "message": "OK" + }, + "url": "https://api.helium.com/v1/organization/timeseries?page%5Bsize%5D=20" + } + } + ], + "recorded_with": "betamax/0.8.0" +} \ No newline at end of file diff --git a/tests/commands/test_organization.py b/tests/commands/test_organization.py index 9151801..92c2402 100644 --- a/tests/commands/test_organization.py +++ b/tests/commands/test_organization.py @@ -16,3 +16,8 @@ def test_update(client, authorized_organization): output = cli_run(client, ['organization', 'update', '--name', current_name]) assert current_name in output + + +def test_timeseries(client, authorized_organization): + output = cli_run(client, ['organization', 'timeseries', 'list']) + assert output is not None