Skip to content

Commit

Permalink
Merge pull request #90 from jathanism/unittest-reboot
Browse files Browse the repository at this point in the history
Completely overhauled unit-testing and fixed some bugs as a result.
  • Loading branch information
dmar42 committed Mar 14, 2016
2 parents 97cd49e + e1ce2c3 commit c810872
Show file tree
Hide file tree
Showing 21 changed files with 666 additions and 1,244 deletions.
10 changes: 7 additions & 3 deletions pynsot/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
'devices': ['hostname'],
'networks': ['cidr'],
'attributes': ['name', 'resource_name'],
'interfaces': ['name', 'device'],
# 'interfaces': ['name', 'device'],
}

# Mapping of resource_names to what we want objects to look like when formatted
Expand Down Expand Up @@ -107,6 +107,10 @@ class App(object):
def __init__(self, ctx, client_args=None, verbose=False):
if client_args is None:
client_args = {}

# Force api_version 1.0 for CLI util.
client_args['extra_args'] = {'api_version': '1.0'}

self.client_args = client_args
self.ctx = ctx
self.verbose = verbose
Expand Down Expand Up @@ -558,7 +562,7 @@ def list(self, data, display_fields=None, resource=None,
if obj_id:
log.debug('Retrieving by obj_id=%r' % obj_id)
result = resource(obj_id).get()
obj = result['data'][self.singular]
obj = get_result(result)

# If we still don't have an object try param-based lookup.
if obj is None:
Expand Down Expand Up @@ -698,7 +702,7 @@ def update(self, data):
if obj_id:
log.debug('Retrieving by obj_id=%r' % obj_id)
result = self.resource(obj_id).get()
obj = result['data'][self.singular]
obj = get_result(result)
else:
obj = self.get_single_object(data)
except HTTP_ERRORS as err:
Expand Down
24 changes: 16 additions & 8 deletions pynsot/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ def _fetch_resources(self):
"""Fetch resources from API"""
headers = self._headers
auth = self._auth
r = slumber.requests.get(self._base_url, auth=auth, headers=headers)
api_root = self._base_url + '/'
r = slumber.requests.get(api_root, auth=auth, headers=headers)

if r.ok:
return r.json()
Expand Down Expand Up @@ -322,7 +323,8 @@ def get_auth_client_info(auth_method):
return AUTH_CLIENTS[auth_method]


def get_api_client(auth_method=None, url=None, extra_args=None):
def get_api_client(auth_method=None, url=None, extra_args=None,
use_dotfile=True):
"""
Safely create an API client so that users don't see tracebacks.
Expand All @@ -337,16 +339,22 @@ def get_api_client(auth_method=None, url=None, extra_args=None):
:param extra_args:
Dict of extra keyword args to be passed to the API client class
:param use_dotfile:
Whether to read the dotfile or not.
"""
if extra_args is None:
extra_args = {}

# Read the dotfile
try:
log.debug('Reading dotfile.')
client_args = dotfile.Dotfile().read()
except dotfile.DotfileError as err:
raise click.UsageError(err.message)
# Should we read the dotfile? If not, client_args will be an empty dict
if use_dotfile:
try:
log.debug('Reading dotfile.')
client_args = dotfile.Dotfile().read()
except dotfile.DotfileError as err:
raise click.UsageError(err.message)
else:
client_args = {}

# Merge the extra_args w/ the client_args from the config
client_args.update(extra_args)
Expand Down
14 changes: 9 additions & 5 deletions pynsot/commands/callbacks.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
# -*- coding: utf-8 -*-

"""
Callbacks used in handling command plugins.
"""

from __future__ import unicode_literals
import ast
import csv
from itertools import chain
Expand Down Expand Up @@ -130,7 +133,7 @@ def process_bulk_add(ctx, param, value):
objects = []

# Value is already an open file handle
reader = csv.DictReader(value, delimiter=':')
reader = csv.DictReader(value, delimiter=b':')
for r in reader:
lineno = reader.line_num

Expand All @@ -147,7 +150,7 @@ def process_bulk_add(ctx, param, value):
r['attributes'] = attributes

# Transform True, False into booleans
log.debug ('FILE ROW: %r', r)
log.debug('FILE ROW: %r', r)
for k, v in r.iteritems():
# Don't evaluate dicts
if isinstance(v, dict):
Expand Down Expand Up @@ -237,9 +240,9 @@ def list_subcommand(ctx, display_fields, my_name=None):
# endpoint resource used to call this endpoint.
parent_resource_name = app.parent_resource_name # e.g. 'networks'

if my_name is not None:
app.resource_name = my_name
else:
# If we've provided my_name, overload the App's resource_name to match
# it.
if my_name is None:
my_name = ctx.info_name # e.g. 'supernets'

# e.g. /api/sites/1/networks/
Expand All @@ -255,4 +258,5 @@ def list_subcommand(ctx, display_fields, my_name=None):
# e.g. /api/sites/1/networks/5/supernets/
my_resource = getattr(parent_resource(parent_resource_id), my_name)

app.resource_name = my_name
app.list(data, display_fields=display_fields, resource=my_resource)
13 changes: 7 additions & 6 deletions pynsot/commands/cmd_attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,18 @@
getattr(ctx.obj, ctx.info_name)(ctx.params)
"""

__author__ = 'Jathan McCollum'
__maintainer__ = 'Jathan McCollum'
__email__ = 'jathan@dropbox.com'
__copyright__ = 'Copyright (c) 2015 Dropbox, Inc.'

from __future__ import unicode_literals

from ..vendor import click

from . import callbacks


__author__ = 'Jathan McCollum'
__maintainer__ = 'Jathan McCollum'
__email__ = 'jathan@dropbox.com'
__copyright__ = 'Copyright (c) 2015-2016 Dropbox, Inc.'


# Ordered list of 2-tuples of (field, display_name) used to translate object
# field names oto their human-readable form when calling .print_list().
DISPLAY_FIELDS = (
Expand Down
11 changes: 6 additions & 5 deletions pynsot/commands/cmd_changes.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,18 @@
getattr(ctx.obj, ctx.info_name)(ctx.params)
"""

from __future__ import unicode_literals

from ..vendor import click
from . import callbacks


__author__ = 'Jathan McCollum'
__maintainer__ = 'Jathan McCollum'
__email__ = 'jathan@dropbox.com'
__copyright__ = 'Copyright (c) 2015 Dropbox, Inc.'


from ..vendor import click

from . import callbacks


# Ordered list of 2-tuples of (field, display_name) used to translate object
# field names oto their human-readable form when calling .print_list().
DISPLAY_FIELDS = (
Expand Down
31 changes: 21 additions & 10 deletions pynsot/commands/cmd_devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ def add(ctx, attributes, bulk_add, hostname, site_id):


# List
@cli.command()
# @cli.command()
@cli.group(invoke_without_command=True)
@click.option(
'-a',
'--attributes',
Expand Down Expand Up @@ -185,15 +186,25 @@ def list(ctx, attributes, delimited, grep, hostname, id, limit, offset, query,
data = ctx.params
data.pop('delimited') # We don't want this going to the server.

if query:
results = ctx.obj.api.sites(site_id).devices.query.get(
query=query, limit=limit, offset=offset)
objects = results['data']['devices']
devices = sorted(d['hostname'] for d in objects)
joiner = ',' if delimited else '\n'
click.echo(joiner.join(devices))
else:
ctx.obj.list(data, display_fields=DISPLAY_FIELDS)
if ctx.invoked_subcommand is None:
if query:
results = ctx.obj.api.sites(site_id).devices.query.get(
query=query, limit=limit, offset=offset)
devices = sorted(d['hostname'] for d in results)
joiner = ',' if delimited else '\n'
click.echo(joiner.join(devices))
else:
ctx.obj.list(data, display_fields=DISPLAY_FIELDS)


@list.command()
@click.pass_context
def interfaces(ctx, *args, **kwargs):
"""Get interfaces for a Device."""
from .cmd_interfaces import DISPLAY_FIELDS as INTERFACE_DISPLAY_FIELDS
callbacks.list_subcommand(
ctx, display_fields=INTERFACE_DISPLAY_FIELDS, my_name='interfaces'
)


# Remove
Expand Down
4 changes: 2 additions & 2 deletions pynsot/commands/cmd_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,8 +369,8 @@ def remove(ctx, id, site_id):
'attr_action',
flag_value='replace',
help=(
'Causes attributes to be replaced instead of updated. If combined with '
'--multi, the entire list will be replaced.'
'Causes attributes to be replaced instead of updated. If combined '
'with --multi, the entire list will be replaced.'
),
)
@click.option(
Expand Down
3 changes: 1 addition & 2 deletions pynsot/commands/cmd_networks.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,9 +256,8 @@ def list(ctx, attributes, cidr, delimited, grep, id, include_ips,
if ctx.invoked_subcommand is None:
if query:
results = ctx.obj.api.sites(site_id).networks.query.get(**data)
objects = results['data']['networks']
networks = sorted(
(d['network_address'], d['prefix_length']) for d in objects
(d['network_address'], d['prefix_length']) for d in results
)
networks = ['%s/%s' % obj for obj in networks]
joiner = ',' if delimited else '\n'
Expand Down
11 changes: 5 additions & 6 deletions pynsot/commands/cmd_sites.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,17 @@
getattr(ctx.obj, ctx.info_name)(ctx.params)
"""

from __future__ import unicode_literals

from ..vendor import click


__author__ = 'Jathan McCollum'
__maintainer__ = 'Jathan McCollum'
__email__ = 'jathan@dropbox.com'
__copyright__ = 'Copyright (c) 2015 Dropbox, Inc.'


from ..vendor import click


# Ordered list of 2-tuples of (field, display_name) used to translate object
# field names oto their human-readable form when calling .print_list().
DISPLAY_FIELDS = (
Expand All @@ -46,9 +48,6 @@ def cli(ctx):
resources. This could be beneficial for isolating corporeate vs. production
environments, or pulling in the IP space of an acquisition.
"""
# if ctx.obj.verbose:
# print 'I am:', ctx.info_name
# print 'my parent is:', ctx.parent.info_name


# Add
Expand Down
14 changes: 7 additions & 7 deletions pynsot/commands/cmd_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,18 @@
getattr(ctx.obj, ctx.info_name)(ctx.params)
"""

from __future__ import unicode_literals

from ..vendor import click
from . import callbacks


__author__ = 'Jathan McCollum'
__maintainer__ = 'Jathan McCollum'
__email__ = 'jathan@dropbox.com'
__copyright__ = 'Copyright (c) 2016 Dropbox, Inc.'


from ..vendor import click

from . import callbacks


# Ordered list of 2-tuples of (field, display_name) used to translate object
# field names oto their human-readable form when calling .print_list().
DISPLAY_FIELDS = (
Expand Down Expand Up @@ -98,6 +99,5 @@ def list(ctx, name, resource_name, site_id):
# Fetch the matching values directly from the API client, filter
# duplicates, and sort the output.
results = ctx.obj.api.sites(site_id).values.get(**data)
objects = results['data']['values']
values = {d['value'] for d in objects}
values = {d['value'] for d in results}
click.echo('\n'.join(sorted(values)))
19 changes: 4 additions & 15 deletions pynsot/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,20 @@
See the examples in the docstring for ``ApiModel``.
"""

from __future__ import unicode_literals
import collections


__author__ = 'Jathan McCollum'
__maintainer__ = 'Jathan McCollum'
__email__ = 'jathan@dropbox.com'
__copyright__ = 'Copyright (c) 2015 Dropbox, Inc.'


import collections


# Valid top-level types.
TYPES = ('network', 'attribute', 'user', 'site', 'device')


# What an error response looks like:
'''
{
"status": "error",
"error": {
"code": 404,
"message": "Resource not found."
}
}
'''


class ApiModel(collections.MutableMapping):
"""
Simple class to make an API response dict into an object.
Expand Down
13 changes: 7 additions & 6 deletions pynsot/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@
<Site(id=1, description=u'Foo site', name=u'Foo')>
"""

__author__ = 'Jathan McCollum'
__maintainer__ = 'Jathan McCollum'
__email__ = 'jathan@dropbox.com'
__copyright__ = 'Copyright (c) 2015 Dropbox, Inc.'
from __future__ import unicode_literals

from .vendor.slumber.serialize import JsonSerializer
from .import models

import models

from .vendor.slumber.serialize import JsonSerializer
__author__ = 'Jathan McCollum'
__maintainer__ = 'Jathan McCollum'
__email__ = 'jathan@dropbox.com'
__copyright__ = 'Copyright (c) 2015-2016 Dropbox, Inc.'


class ModelSerializer(JsonSerializer):
Expand Down
2 changes: 1 addition & 1 deletion pynsot/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '0.19.2'
__version__ = '0.19.3'
5 changes: 5 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[pytest]
DJANGO_SETTINGS_MODULE = tests.nsot_settings
django_find_project = false
python_paths = .
addopts = -vv
4 changes: 3 additions & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
-r requirements.txt
fake-factory==0.5.0
netaddr==0.7.15
nsot==0.15.6
py==1.4.26
pytest==2.7.0
pytest-django==2.9.1
pytest-pythonpath==0.6
requests-mock==0.6.0
Sphinx==1.3.6
sphinx-autobuild==0.6.0
Expand Down

0 comments on commit c810872

Please sign in to comment.