diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 23b70e6b..5c5c2391 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,7 +1,7 @@ [bumpversion] commit = True tag = True -current_version = 0.7.3-dev +current_version = 1.1.0-dev parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+))? serialize = {major}.{minor}.{patch}-{release} diff --git a/.gitignore b/.gitignore index 43534652..05175233 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ *.pyo _trial_temp __pycache__ +.pytest_cache # osx *.DS_Store* diff --git a/.travis.yml b/.travis.yml index aaf5340b..2ab11056 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ python: - 2.6 - 2.7 - pypy-5.3.1 - - 3.4 - 3.5 - 3.6 diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e8606171..55cbcdcb 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,10 +1,20 @@ Change log ========== -0.7.3-dev ---------- +1.1.0 +----- + +* add support for nodemeta to catalog and health end points (thanks + @ibrahimmenem) +* update Check.script to use args, as Consul 1.1 has dropped the script + parameter + +1.0.1 +----- -* TBD +* Support for Python 3.4 dropped (sorry) +* Add support for Consul 1.0.0 (thanks @matusvalo!) +* Expose all 400 errors and add tests for common callback handler (thanks @bagerard) 0.7.2 ----- diff --git a/README.md b/README.md new file mode 100644 index 00000000..a978dcdc --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +This repo was an early Python client for [Consul.io](http://www.consul.io/). + +It was maintained roughly from 2014 to 2018 until the original maintainers stopped using Consul in their day-to-day stack. + +As of April 2024, [criteo/py-consul](https://github.com/criteo/py-consul) appears to be the most well maintained fork. + +🙏💚 diff --git a/README.rst b/README.rst deleted file mode 100644 index 64474126..00000000 --- a/README.rst +++ /dev/null @@ -1,101 +0,0 @@ -Python client for `Consul.io `_ -====================================================== - -Documentation -------------- - -`Read the Docs`_ - -Status ------- - -|Build Status|\ |Coverage Status| - -Example -------- - -.. code:: python - - import consul - - c = consul.Consul() - - # poll a key for updates - index = None - while True: - index, data = c.kv.get('foo', index=index) - print data['Value'] - - # in another process - c.kv.put('foo', 'bar') - -Installation ------------- - -:: - - pip install python-consul - -.. |Build Status| - image:: https://img.shields.io/travis/cablehead/python-consul.svg?style=flat-square - :target: https://travis-ci.org/cablehead/python-consul -.. |Coverage Status| - image:: https://img.shields.io/coveralls/cablehead/python-consul.svg?style=flat-square - :target: https://coveralls.io/r/cablehead/python-consul?branch=master -.. _Read the Docs: https://python-consul.readthedocs.io/ - -Status ------- - -There's a few API endpoints still to go to expose all features available in -Consul v0.6.0. If you need an endpoint that's not in the documentation, just -open an issue and I'll try and add it straight away. - -Contributing ------------- - -python-consul is currently maintained by @abn and @cablehead. Please reach out -if you're interested in being a maintainer as well. Otherwise, open a PR or -Issue we'll try and respond as quickly as we're able. - -Issue Labels -~~~~~~~~~~~~ - -:today!: Some triaging is in progress and this issue should be taken care of in - a couple of hours! - -:priority: There's a clear need to address this issue and it's likely a core - contributor will take it on. Opening a PR for these is greatly - appreciated! - -:help wanted: This issue makes sense and would be useful. It's unlikely a core - contributor will get to this though, so if you'd like to see it - addressed please open a PR. - -:question: The need for the issue isn't clear or needs clarification, so please - follow up. Issues in this state for a few months, without - responses will likely will be closed. - -PRs -~~~ - -Pull requests are very much appreciated! When you create a PR please ensure: - -#. All current tests pass, including flake8 -#. To add tests for your new features, if reasonable -#. To add docstrings for new api features you add and if needed link to these - docstrings from the sphinx documentation - -Releases -~~~~~~~~ - -.. code:: bash - - # release the current version, eg: 0.6.1-dev -> 0.6.1 - bumpversion release - - # prepare the next patch (z-stream) version, eg: 0.6.1 -> 0.6.2-dev - bumpversion --no-tag patch - - # else, prepare the next minor (y-stream) version, eg: 0.6.1 -> 0.7.0-dev - bumpversion --no-tag minor diff --git a/consul/__init__.py b/consul/__init__.py index 006fc16f..ee462f57 100644 --- a/consul/__init__.py +++ b/consul/__init__.py @@ -1,4 +1,4 @@ -__version__ = '0.7.3-dev' +__version__ = '1.1.0-dev' from consul.std import Consul diff --git a/consul/base.py b/consul/base.py index 09203b6d..ee6ab254 100755 --- a/consul/base.py +++ b/consul/base.py @@ -1,5 +1,6 @@ import abc import collections +import warnings import logging import base64 import json @@ -36,6 +37,11 @@ class BadRequest(ConsulException): pass +class ClientError(ConsulException): + """Encapsulates 4xx Http error code""" + pass + + # # Convenience to define checks @@ -44,11 +50,17 @@ class Check(object): There are three different kinds of checks: script, http and ttl """ @classmethod - def script(klass, script, interval): + def script(klass, args, interval): """ - Run *script* every *interval* (e.g. "10s") to peform health check + Run the script *args* every *interval* (e.g. "10s") to peform health + check """ - return {'script': script, 'interval': interval} + if isinstance(args, six.string_types) \ + or isinstance(args, six.binary_type): + warnings.warn( + "Check.script should take a list of args", DeprecationWarning) + args = ["sh", "-c", args] + return {'args': args, 'interval': interval} @classmethod def http(klass, url, interval, timeout=None, deregister=None, header=None): @@ -158,24 +170,28 @@ def _compat( class CB(object): @classmethod - def __status(klass, response, allow_404=True): + def _status(klass, response, allow_404=True): # status checking - if response.code >= 500 and response.code < 600: + if 400 <= response.code < 500: + if response.code == 400: + raise BadRequest('%d %s' % (response.code, response.body)) + elif response.code == 401: + raise ACLDisabled(response.body) + elif response.code == 403: + raise ACLPermissionDenied(response.body) + elif response.code == 404: + if not allow_404: + raise NotFound(response.body) + else: + raise ClientError("%d %s" % (response.code, response.body)) + elif 500 <= response.code < 600: raise ConsulException("%d %s" % (response.code, response.body)) - if response.code == 400: - raise BadRequest('%d %s' % (response.code, response.body)) - if response.code == 401: - raise ACLDisabled(response.body) - if response.code == 403: - raise ACLPermissionDenied(response.body) - if response.code == 404 and not allow_404: - raise NotFound(response.body) @classmethod def bool(klass): # returns True on successful response def cb(response): - CB.__status(response) + CB._status(response) return response.code == 200 return cb @@ -204,7 +220,7 @@ def json( *is_id* only the 'ID' field of the json object will be returned. """ def cb(response): - CB.__status(response, allow_404=allow_404) + CB._status(response, allow_404=allow_404) if response.code == 404: return response.headers['X-Consul-Index'], None @@ -377,16 +393,16 @@ def fire( """ assert not name.startswith('/'), \ 'keys should not start with a forward slash' - params = {} + params = [] if node is not None: - params['node'] = node + params.append(('node', node)) if service is not None: - params['service'] = service + params.append(('service', service)) if tag is not None: - params['tag'] = tag + params.append(('tag', tag)) token = token or self.agent.token if token: - params['token'] = token + params.append(('token', token)) return self.agent.http.put( CB.json(), @@ -432,13 +448,13 @@ def list( }, } """ - params = {} + params = [] if name is not None: - params['name'] = name + params.append(('name', name)) if index: - params['index'] = index + params.append(('index', index)) if wait: - params['wait'] = wait + params.append(('wait', wait)) return self.agent.http.get( CB.json(index=True, decode='Payload'), '/v1/event/list', params=params) @@ -504,26 +520,26 @@ def get( """ assert not key.startswith('/'), \ 'keys should not start with a forward slash' - params = {} + params = [] if index: - params['index'] = index + params.append(('index', index)) if wait: - params['wait'] = wait + params.append(('wait', wait)) if recurse: - params['recurse'] = '1' + params.append(('recurse', '1')) token = token or self.agent.token if token: - params['token'] = token + params.append(('token', token)) dc = dc or self.agent.dc if dc: - params['dc'] = dc + params.append(('dc', dc)) if keys: - params['keys'] = True + params.append(('keys', True)) if separator: - params['separator'] = separator + params.append(('separator', separator)) consistency = consistency or self.agent.consistency if consistency in ('consistent', 'stale'): - params[consistency] = '1' + params.append((consistency, '1')) one = False decode = False @@ -586,21 +602,21 @@ def put( isinstance(value, (six.string_types, six.binary_type)), \ "value should be None or a string / binary data" - params = {} + params = [] if cas is not None: - params['cas'] = cas + params.append(('cas', cas)) if flags is not None: - params['flags'] = flags + params.append(('flags', flags)) if acquire: - params['acquire'] = acquire + params.append(('acquire', acquire)) if release: - params['release'] = release + params.append(('release', release)) token = token or self.agent.token if token: - params['token'] = token + params.append(('token', token)) dc = dc or self.agent.dc if dc: - params['dc'] = dc + params.append(('dc', dc)) return self.agent.http.put( CB.json(), '/v1/kv/%s' % key, params=params, data=value) @@ -626,17 +642,17 @@ def delete(self, key, recurse=None, cas=None, token=None, dc=None): assert not key.startswith('/'), \ 'keys should not start with a forward slash' - params = {} + params = [] if recurse: - params['recurse'] = '1' + params.append(('recurse', '1')) if cas is not None: - params['cas'] = cas + params.append(('cas', cas)) token = token or self.agent.token if token: - params['token'] = token + params.append(('token', token)) dc = dc or self.agent.dc if dc: - params['dc'] = dc + params.append(('dc', dc)) return self.agent.http.delete( CB.json(), '/v1/kv/%s' % key, params=params) @@ -728,9 +744,9 @@ def members(self, wan=False): the list of WAN members instead of the LAN members which is default. """ - params = {} + params = [] if wan: - params['wan'] = 1 + params.append(('wan', 1)) return self.agent.http.get( CB.json(), '/v1/agent/members', params=params) @@ -746,11 +762,11 @@ def maintenance(self, enable, reason=None): operators. """ - params = {} + params = [] - params['enable'] = enable + params.append(('enable', enable)) if reason: - params['reason'] = reason + params.append(('reason', reason)) return self.agent.http.put( CB.bool(), '/v1/agent/maintenance', params=params) @@ -767,12 +783,12 @@ def join(self, address, wan=False): pool. Default is 'false'. """ - params = {} + params = [] if wan: - params['wan'] = 1 + params.append(('wan', 1)) - return self.agent.http.get( + return self.agent.http.put( CB.bool(), '/v1/agent/join/%s' % address, params=params) def force_leave(self, node): @@ -787,7 +803,7 @@ def force_leave(self, node): *node* is the node to change state for. """ - return self.agent.http.get( + return self.agent.http.put( CB.bool(), '/v1/agent/force-leave/%s' % node) class Service(object): @@ -803,6 +819,7 @@ def register( tags=None, check=None, token=None, + meta=None, # *deprecated* use check parameter script=None, interval=None, @@ -832,6 +849,9 @@ def register( Note this call will return successful even if the token doesn't have permissions to register this service. + *meta* specifies arbitrary KV metadata linked to the service + formatted as {k1:v1, k2:v2}. + *script*, *interval*, *ttl*, *http*, and *timeout* arguments are deprecated. use *check* instead. @@ -857,7 +877,8 @@ def register( payload['port'] = port if tags: payload['tags'] = tags - + if meta: + payload['meta'] = meta if check: payload['check'] = check @@ -869,10 +890,10 @@ def register( http=http, timeout=timeout)) - params = {} + params = [] token = token or self.agent.token if token: - params['token'] = token + params.append(('token', token)) return self.agent.http.put( CB.bool(), @@ -886,7 +907,7 @@ def deregister(self, service_id): take care of deregistering the service with the Catalog. If there is an associated check, that is also deregistered. """ - return self.agent.http.get( + return self.agent.http.put( CB.bool(), '/v1/agent/service/deregister/%s' % service_id) def maintenance(self, service_id, enable, reason=None): @@ -904,11 +925,11 @@ def maintenance(self, service_id, enable, reason=None): operators. """ - params = {} + params = [] - params['enable'] = enable + params.append(('enable', enable)) if reason: - params['reason'] = reason + params.append(('reason', reason)) return self.agent.http.put( CB.bool(), @@ -984,10 +1005,10 @@ def register( if service_id: payload['serviceid'] = service_id - params = {} + params = [] token = token or self.agent.token if token: - params['token'] = token + params.append(('token', token)) return self.agent.http.put( CB.bool(), @@ -999,7 +1020,7 @@ def deregister(self, check_id): """ Remove a check from the local agent. """ - return self.agent.http.get( + return self.agent.http.put( CB.bool(), '/v1/agent/check/deregister/%s' % check_id) @@ -1008,11 +1029,11 @@ def ttl_pass(self, check_id, notes=None): Mark a ttl based check as passing. Optional notes can be attached to describe the status of the check. """ - params = {} + params = [] if notes: - params['note'] = notes + params.append(('note', notes)) - return self.agent.http.get( + return self.agent.http.put( CB.bool(), '/v1/agent/check/pass/%s' % check_id, params=params) @@ -1023,11 +1044,11 @@ def ttl_fail(self, check_id, notes=None): attached to describe why check is failing. The status of the check will be set to critical and the ttl clock will be reset. """ - params = {} + params = [] if notes: - params['note'] = notes + params.append(('note', notes)) - return self.agent.http.get( + return self.agent.http.put( CB.bool(), '/v1/agent/check/fail/%s' % check_id, params=params) @@ -1038,11 +1059,11 @@ def ttl_warn(self, check_id, notes=None): attached to describe the warning. The status of the check will be set to warn and the ttl clock will be reset. """ - params = {} + params = [] if notes: - params['note'] = notes + params.append(('note', notes)) - return self.agent.http.get( + return self.agent.http.put( CB.bool(), '/v1/agent/check/warn/%s' % check_id, params=params) @@ -1057,7 +1078,8 @@ def register(self, service=None, check=None, dc=None, - token=None): + token=None, + node_meta=None): """ A low level mechanism for directly registering or updating entries in the catalog. It is usually recommended to use @@ -1107,6 +1129,9 @@ def register(self, *token* is an optional `ACL token`_ to apply to this request. + *node_meta* is an optional meta data used for filtering, a + dictionary formatted as {k1:v1, k2:v2}. + This manipulates the health check entry, but does not setup a script or TTL to actually update the status. The full documentation is `here `_. @@ -1114,7 +1139,7 @@ def register(self, Returns *True* on success. """ data = {'node': node, 'address': address} - params = {} + params = [] dc = dc or self.agent.dc if dc: data['datacenter'] = dc @@ -1125,7 +1150,11 @@ def register(self, token = token or self.agent.token if token: data['WriteRequest'] = {'Token': token} - params['token'] = token + params.append(('token', token)) + if node_meta: + for nodemeta_name, nodemeta_value in node_meta.items(): + params.append(('node-meta', '{0}:{1}'. + format(nodemeta_name, nodemeta_value))) return self.agent.http.put( CB.bool(), '/v1/catalog/register', @@ -1182,7 +1211,8 @@ def nodes( consistency=None, dc=None, near=None, - token=None): + token=None, + node_meta=None): """ Returns a tuple of (*index*, *nodes*) of all nodes known about in the *dc* datacenter. *dc* defaults to the current @@ -1204,6 +1234,9 @@ def nodes( *token* is an optional `ACL token`_ to apply to this request. + *node_meta* is an optional meta data used for filtering, a + dictionary formatted as {k1:v1, k2:v2}. + The response looks like this:: (index, [ @@ -1217,22 +1250,26 @@ def nodes( } ]) """ - params = {} + params = [] dc = dc or self.agent.dc if dc: - params['dc'] = dc + params.append(('dc', dc)) if index: - params['index'] = index + params.append(('index', index)) if wait: - params['wait'] = wait + params.append(('wait', wait)) if near: - params['near'] = near + params.append(('near', near)) token = token or self.agent.token if token: - params['token'] = token + params.append(('token', token)) consistency = consistency or self.agent.consistency if consistency in ('consistent', 'stale'): - params[consistency] = '1' + params.append((consistency, '1')) + if node_meta: + for nodemeta_name, nodemeta_value in node_meta.items(): + params.append(('node-meta', '{0}:{1}'. + format(nodemeta_name, nodemeta_value))) return self.agent.http.get( CB.json(index=True), '/v1/catalog/nodes', params=params) @@ -1241,7 +1278,8 @@ def services(self, wait=None, consistency=None, dc=None, - token=None): + token=None, + node_meta=None): """ Returns a tuple of (*index*, *services*) of all services known about in the *dc* datacenter. *dc* defaults to the current @@ -1260,6 +1298,9 @@ def services(self, *token* is an optional `ACL token`_ to apply to this request. + *node_meta* is an optional meta data used for filtering, a + dictionary formatted as {k1:v1, k2:v2}. + The response looks like this:: (index, { @@ -1274,20 +1315,24 @@ def services(self, The main keys are the service names and the list provides all the known tags for a given service. """ - params = {} + params = [] dc = dc or self.agent.dc if dc: - params['dc'] = dc + params.append(('dc', dc)) if index: - params['index'] = index + params.append(('index', index)) if wait: - params['wait'] = wait + params.append(('wait', wait)) token = token or self.agent.token if token: - params['token'] = token + params.append(('token', token)) consistency = consistency or self.agent.consistency if consistency in ('consistent', 'stale'): - params[consistency] = '1' + params.append((consistency, '1')) + if node_meta: + for nodemeta_name, nodemeta_value in node_meta.items(): + params.append(('node-meta', '{0}:{1}'. + format(nodemeta_name, nodemeta_value))) return self.agent.http.get( CB.json(index=True), '/v1/catalog/services', params=params) @@ -1343,20 +1388,20 @@ def node(self, } }) """ - params = {} + params = [] dc = dc or self.agent.dc if dc: - params['dc'] = dc + params.append(('dc', dc)) if index: - params['index'] = index + params.append(('index', index)) if wait: - params['wait'] = wait + params.append(('wait', wait)) token = token or self.agent.token if token: - params['token'] = token + params.append(('token', token)) consistency = consistency or self.agent.consistency if consistency in ('consistent', 'stale'): - params[consistency] = '1' + params.append((consistency, '1')) return self.agent.http.get( CB.json(index=True), '/v1/catalog/node/%s' % node, @@ -1371,7 +1416,8 @@ def service( consistency=None, dc=None, near=None, - token=None): + token=None, + node_meta=None): """ Returns a tuple of (*index*, *nodes*) of the nodes providing *service* in the *dc* datacenter. *dc* defaults to the current @@ -1396,6 +1442,9 @@ def service( *token* is an optional `ACL token`_ to apply to this request. + *node_meta* is an optional meta data used for filtering, a + dictionary formatted as {k1:v1, k2:v2}. + The response looks like this:: (index, [ @@ -1409,24 +1458,28 @@ def service( } ]) """ - params = {} + params = [] dc = dc or self.agent.dc if dc: - params['dc'] = dc + params.append(('dc', dc)) if tag: - params['tag'] = tag + params.append(('tag', tag)) if index: - params['index'] = index + params.append(('index', index)) if wait: - params['wait'] = wait + params.append(('wait', wait)) if near: - params['near'] = near + params.append(('near', near)) token = token or self.agent.token if token: - params['token'] = token + params.append(('token', token)) consistency = consistency or self.agent.consistency if consistency in ('consistent', 'stale'): - params[consistency] = '1' + params.append((consistency, '1')) + if node_meta: + for nodemeta_name, nodemeta_value in node_meta.items(): + params.append(('node-meta', '{0}:{1}'. + format(nodemeta_name, nodemeta_value))) return self.agent.http.get( CB.json(index=True), '/v1/catalog/service/%s' % service, @@ -1445,7 +1498,8 @@ def service(self, tag=None, dc=None, near=None, - token=None): + token=None, + node_meta=None): """ Returns a tuple of (*index*, *nodes*) @@ -1470,25 +1524,31 @@ def service(self, order based on the estimated round trip time from that node *token* is an optional `ACL token`_ to apply to this request. + + *node_meta* is an optional meta data used for filtering, a + dictionary formatted as {k1:v1, k2:v2}. """ - params = {} + params = [] if index: - params['index'] = index + params.append(('index', index)) if wait: - params['wait'] = wait + params.append(('wait', wait)) if passing: - params['passing'] = '1' + params.append(('passing', '1')) if tag is not None: - params['tag'] = tag + params.append(('tag', tag)) dc = dc or self.agent.dc if dc: - params['dc'] = dc + params.append(('dc', dc)) if near: - params['near'] = near + params.append(('near', near)) token = token or self.agent.token if token: - params['token'] = token - + params.append(('token', token)) + if node_meta: + for nodemeta_name, nodemeta_value in node_meta.items(): + params.append(('node-meta', '{0}:{1}'. + format(nodemeta_name, nodemeta_value))) return self.agent.http.get( CB.json(index=True), '/v1/health/service/%s' % service, @@ -1501,7 +1561,8 @@ def checks( wait=None, dc=None, near=None, - token=None): + token=None, + node_meta=None): """ Returns a tuple of (*index*, *checks*) with *checks* being the checks associated with the service. @@ -1522,21 +1583,27 @@ def checks( order based on the estimated round trip time from that node *token* is an optional `ACL token`_ to apply to this request. + + *node_meta* is an optional meta data used for filtering, a + dictionary formatted as {k1:v1, k2:v2}. """ - params = {} + params = [] if index: - params['index'] = index + params.append(('index', index)) if wait: - params['wait'] = wait + params.append(('wait', wait)) dc = dc or self.agent.dc if dc: - params['dc'] = dc + params.append(('dc', dc)) if near: - params['near'] = near + params.append(('near', near)) token = token or self.agent.token if token: - params['token'] = token - + params.append(('token', token)) + if node_meta: + for nodemeta_name, nodemeta_value in node_meta.items(): + params.append(('node-meta', '{0}:{1}'. + format(nodemeta_name, nodemeta_value))) return self.agent.http.get( CB.json(index=True), '/v1/health/checks/%s' % service, @@ -1548,7 +1615,8 @@ def state(self, wait=None, dc=None, near=None, - token=None): + token=None, + node_meta=None): """ Returns a tuple of (*index*, *nodes*) @@ -1573,23 +1641,29 @@ def state(self, *token* is an optional `ACL token`_ to apply to this request. + *node_meta* is an optional meta data used for filtering, a + dictionary formatted as {k1:v1, k2:v2}. + *nodes* are the nodes providing the given service. """ assert name in ['any', 'unknown', 'passing', 'warning', 'critical'] - params = {} + params = [] if index: - params['index'] = index + params.append(('index', index)) if wait: - params['wait'] = wait + params.append(('wait', wait)) dc = dc or self.agent.dc if dc: - params['dc'] = dc + params.append(('dc', dc)) if near: - params['near'] = near + params.append(('near', near)) token = token or self.agent.token if token: - params['token'] = token - + params.append(('token', token)) + if node_meta: + for nodemeta_name, nodemeta_value in node_meta.items(): + params.append(('node-meta', '{0}:{1}'. + format(nodemeta_name, nodemeta_value))) return self.agent.http.get( CB.json(index=True), '/v1/health/state/%s' % name, @@ -1613,17 +1687,17 @@ def node(self, node, index=None, wait=None, dc=None, token=None): *nodes* are the nodes providing the given service. """ - params = {} + params = [] if index: - params['index'] = index + params.append(('index', index)) if wait: - params['wait'] = wait + params.append(('wait', wait)) dc = dc or self.agent.dc if dc: - params['dc'] = dc + params.append(('dc', dc)) token = token or self.agent.token if token: - params['token'] = token + params.append(('token', token)) return self.agent.http.get( CB.json(index=True), @@ -1675,10 +1749,10 @@ def create( Returns the string *session_id* for the session. """ - params = {} + params = [] dc = dc or self.agent.dc if dc: - params['dc'] = dc + params.append(('dc', dc)) data = {} if name: data['name'] = name @@ -1712,10 +1786,10 @@ def destroy(self, session_id, dc=None): Returns *True* on success. """ - params = {} + params = [] dc = dc or self.agent.dc if dc: - params['dc'] = dc + params.append(('dc', dc)) return self.agent.http.put( CB.bool(), '/v1/session/destroy/%s' % session_id, @@ -1753,17 +1827,17 @@ def list(self, index=None, wait=None, consistency=None, dc=None): ... ]) """ - params = {} + params = [] dc = dc or self.agent.dc if dc: - params['dc'] = dc + params.append(('dc', dc)) if index: - params['index'] = index + params.append(('index', index)) if wait: - params['wait'] = wait + params.append(('wait', wait)) consistency = consistency or self.agent.consistency if consistency in ('consistent', 'stale'): - params[consistency] = '1' + params.append((consistency, '1')) return self.agent.http.get( CB.json(index=True), '/v1/session/list', params=params) @@ -1783,17 +1857,17 @@ def node(self, node, index=None, wait=None, consistency=None, dc=None): not specified *consistency* will the consistency level this client was configured with. """ - params = {} + params = [] dc = dc or self.agent.dc if dc: - params['dc'] = dc + params.append(('dc', dc)) if index: - params['index'] = index + params.append(('index', index)) if wait: - params['wait'] = wait + params.append(('wait', wait)) consistency = consistency or self.agent.consistency if consistency in ('consistent', 'stale'): - params[consistency] = '1' + params.append((consistency, '1')) return self.agent.http.get( CB.json(index=True), '/v1/session/node/%s' % node, params=params) @@ -1820,17 +1894,17 @@ def info(self, not specified *consistency* will the consistency level this client was configured with. """ - params = {} + params = [] dc = dc or self.agent.dc if dc: - params['dc'] = dc + params.append(('dc', dc)) if index: - params['index'] = index + params.append(('index', index)) if wait: - params['wait'] = wait + params.append(('wait', wait)) consistency = consistency or self.agent.consistency if consistency in ('consistent', 'stale'): - params[consistency] = '1' + params.append((consistency, '1')) return self.agent.http.get( CB.json(index=True, one=True), '/v1/session/info/%s' % session_id, @@ -1846,10 +1920,10 @@ def renew(self, session_id, dc=None): Returns the session. """ - params = {} + params = [] dc = dc or self.agent.dc if dc: - params['dc'] = dc + params.append(('dc', dc)) return self.agent.http.put( CB.json(one=True, allow_404=False), '/v1/session/renew/%s' % session_id, @@ -1866,10 +1940,10 @@ def list(self, token=None): default token. An *ACLPermissionDenied* exception will be raised if a management token is not used. """ - params = {} + params = [] token = token or self.agent.token if token: - params['token'] = token + params.append(('token', token)) return self.agent.http.get( CB.json(), '/v1/acl/list', params=params) @@ -1877,10 +1951,10 @@ def info(self, acl_id, token=None): """ Returns the token information for *acl_id*. """ - params = {} + params = [] token = token or self.agent.token if token: - params['token'] = token + params.append(('token', token)) return self.agent.http.get( CB.json(one=True), '/v1/acl/info/%s' % acl_id, params=params) @@ -1922,10 +1996,10 @@ def create(self, Returns the string *acl_id* for the new token. """ - params = {} + params = [] token = token or self.agent.token if token: - params['token'] = token + params.append(('token', token)) payload = {} if name: @@ -1971,10 +2045,10 @@ def update(self, acl_id, name=None, type=None, rules=None, token=None): Returns the string *acl_id* of this token on success. """ - params = {} + params = [] token = token or self.agent.token if token: - params['token'] = token + params.append(('token', token)) payload = {'ID': acl_id} if name: @@ -2005,10 +2079,10 @@ def clone(self, acl_id, token=None): Returns the string of the newly created *acl_id*. """ - params = {} + params = [] token = token or self.agent.token if token: - params['token'] = token + params.append(('token', token)) return self.agent.http.put( CB.json(is_id=True), '/v1/acl/clone/%s' % acl_id, @@ -2023,10 +2097,10 @@ def destroy(self, acl_id, token=None): Returns *True* on success. """ - params = {} + params = [] token = token or self.agent.token if token: - params['token'] = token + params.append(('token', token)) return self.agent.http.put( CB.json(), '/v1/acl/destroy/%s' % acl_id, @@ -2069,12 +2143,12 @@ def list(self, dc=None, token=None): *token* is an optional `ACL token`_ to apply to this request. """ - params = {} + params = [] token = token or self.agent.token if token: - params['token'] = token + params.append(('token', token)) if dc: - params['dc'] = dc + params.append(('dc', dc)) return self.agent.http.get(CB.json(), '/v1/query', params=params) @@ -2173,7 +2247,7 @@ def create(self, service, https://www.consul.io/docs/agent/http/query.html """ path = '/v1/query' - params = None if dc is None else {'dc': dc} + params = None if dc is None else [('dc', dc)] data = self._query_data( service, name, session, token, nearestn, datacenters, onlypassing, tags, ttl, regexp @@ -2201,7 +2275,7 @@ def update(self, query_id, all the other setting remains the same as the query create method """ path = '/v1/query/%s' % query_id - params = None if dc is None else {'dc': dc} + params = None if dc is None else [('dc', dc)] data = self._query_data( service, name, session, token, nearestn, datacenters, onlypassing, tags, ttl, regexp @@ -2223,12 +2297,12 @@ def get(self, *dc* is the datacenter that this agent will communicate with. By default the datacenter of the host is used. """ - params = {} + params = [] token = token or self.agent.token if token: - params['token'] = token + params.append(('token', token)) if dc: - params['dc'] = dc + params.append(('dc', dc)) return self.agent.http.get( CB.json(), '/v1/query/%s' % query_id, params=params) @@ -2243,12 +2317,12 @@ def delete(self, query_id, token=None, dc=None): *dc* is the datacenter that this agent will communicate with. By default the datacenter of the host is used. """ - params = {} + params = [] token = token or self.agent.token if token: - params['token'] = token + params.append(('token', token)) if dc: - params['dc'] = dc + params.append(('dc', dc)) return self.agent.http.delete( CB.bool(), '/v1/query/%s' % query_id, params=params) @@ -2274,16 +2348,16 @@ def execute(self, *limit* is used to limit the size of the list to the given number of nodes. This is applied after any sorting or shuffling. """ - params = {} + params = [] token = token or self.agent.token if token: - params['token'] = token + params.append(('token', token)) if dc: - params['dc'] = dc + params.append(('dc', dc)) if near: - params['near'] = near + params.append(('near', near)) if limit: - params['limit'] = limit + params.append(('limit', limit)) return self.agent.http.get( CB.json(), '/v1/query/%s/execute' % query, params=params) @@ -2301,12 +2375,12 @@ def explain(self, *dc* is the datacenter that this agent will communicate with. By default the datacenter of the host is used. """ - params = {} + params = [] token = token or self.agent.token if token: - params['token'] = token + params.append(('token', token)) if dc: - params['dc'] = dc + params.append(('dc', dc)) return self.agent.http.get( CB.json(), '/v1/query/%s/explain' % query, params=params) @@ -2337,16 +2411,16 @@ def nodes(self, dc=None, index=None, wait=None, consistency=None): not specified *consistency* will the consistency level this client was configured with. """ - params = {} + params = [] if dc: - params['dc'] = dc + params.append(('dc', dc)) if index: - params['index'] = index + params.append(('index', index)) if wait: - params['wait'] = wait + params.append(('wait', wait)) consistency = consistency or self.agent.consistency if consistency in ('consistent', 'stale'): - params[consistency] = '1' + params.append((consistency, '1')) return self.agent.http.get( CB.json(index=True), '/v1/coordinate/nodes', params=params) diff --git a/setup.py b/setup.py index e8ed5133..0a0385cf 100644 --- a/setup.py +++ b/setup.py @@ -72,6 +72,7 @@ def run_tests(self): 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', ], ) diff --git a/sonar-project.properties b/sonar-project.properties index 04f093e8..e5ca3e32 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,7 +1,7 @@ # Required metadata sonar.projectKey=com.github:cablehead:python-consul sonar.projectName=Python Consul -sonar.projectVersion=0.7.3-dev +sonar.projectVersion=1.1.0-dev # Comma-separated paths to directories with sources (required) sonar.sources=consul diff --git a/tests/conftest.py b/tests/conftest.py index 66f70ca5..bb77449b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,7 +9,6 @@ import time import json import os - import requests import pytest import py @@ -26,8 +25,6 @@ collect_ignore.append(p) p = os.path.join(os.path.dirname(__file__), 'test_tornado.py') collect_ignore.append(p) -else: - pytest_plugins = "pytest_twisted" def get_free_ports(num, host=None): @@ -55,10 +52,11 @@ def start_consul_instance(acl_master_token=None): instance is listening on """ ports = dict(zip( - ['http', 'rpc', 'serf_lan', 'serf_wan', 'server', 'dns'], - get_free_ports(5) + [-1])) + ['http', 'serf_lan', 'serf_wan', 'server', 'dns'], + get_free_ports(4) + [-1])) - config = {'ports': ports, 'performance': {'raft_multiplier': 1}} + config = {'ports': ports, 'performance': {'raft_multiplier': 1}, + 'enable_script_checks': True} if acl_master_token: config['acl_datacenter'] = 'dc1' config['acl_master_token'] = acl_master_token @@ -73,14 +71,15 @@ def start_consul_instance(acl_master_token=None): else: postfix = 'linux64' bin = os.path.join(os.path.dirname(__file__), 'consul.'+postfix) - command = '{bin} agent -server -bootstrap' \ + command = '{bin} agent -dev' \ ' -bind=127.0.0.1' \ - ' -config-dir=. -data-dir=./data' + ' -config-dir=.' command = command.format(bin=bin).strip() command = shlex.split(command) - p = subprocess.Popen( - command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + with open('/dev/null', 'w') as devnull: + p = subprocess.Popen( + command, stdout=devnull, stderr=devnull) # wait for consul instance to bootstrap base_uri = 'http://127.0.0.1:%s/v1/' % ports['http'] @@ -91,6 +90,7 @@ def start_consul_instance(acl_master_token=None): response = requests.get(base_uri + 'status/leader') except requests.ConnectionError: continue + print(response.text) if response.text.strip() != '""': break @@ -102,28 +102,36 @@ def start_consul_instance(acl_master_token=None): break time.sleep(0.1) - requests.get(base_uri + 'agent/service/deregister/foo') + requests.put(base_uri + 'agent/service/deregister/foo') # phew time.sleep(2) return p, ports['http'] -@pytest.yield_fixture(scope="session") +def clean_consul(port): + # remove all data from the instance, to have a clean start + base_uri = 'http://127.0.0.1:%s/v1/' % port + requests.delete(base_uri + 'kv/', params={'recurse': 1}) + services = requests.get(base_uri + 'agent/services').json().keys() + for s in services: + requests.put(base_uri + 'agent/service/deregister/%s' % s) + + +@pytest.fixture(scope="module") def consul_instance(): p, port = start_consul_instance() yield port p.terminate() -@pytest.yield_fixture +@pytest.fixture def consul_port(consul_instance): - yield consul_instance - # remove all data from the instance, to have a clean start - base_uri = 'http://127.0.0.1:%s/v1/' % consul_instance - requests.delete(base_uri + 'kv/?recurse=1') + port = consul_instance + yield port + clean_consul(port) -@pytest.yield_fixture(scope="session") +@pytest.fixture(scope="module") def acl_consul_instance(): acl_master_token = uuid.uuid4().hex p, port = start_consul_instance(acl_master_token=acl_master_token) @@ -131,11 +139,9 @@ def acl_consul_instance(): p.terminate() -@pytest.yield_fixture +@pytest.fixture def acl_consul(acl_consul_instance): ACLConsul = collections.namedtuple('ACLConsul', ['port', 'token']) port, token = acl_consul_instance yield ACLConsul(port, token) - # remove all data from the instance, to have a clean start - base_uri = 'http://127.0.0.1:%s/v1/' % port - requests.delete(base_uri + 'kv/?recurse=1') + clean_consul(port) diff --git a/tests/consul.linux64 b/tests/consul.linux64 index c92f9421..bce8ac3c 100755 Binary files a/tests/consul.linux64 and b/tests/consul.linux64 differ diff --git a/tests/consul.osx b/tests/consul.osx index 0c79f438..60362686 100755 Binary files a/tests/consul.osx and b/tests/consul.osx differ diff --git a/tests/test_aio.py b/tests/test_aio.py index b6e0c943..b251231f 100644 --- a/tests/test_aio.py +++ b/tests/test_aio.py @@ -169,12 +169,10 @@ def test_agent_services(self, loop, consul_port): def main(): c = consul.aio.Consul(port=consul_port, loop=loop) services = yield from c.agent.services() - del services['consul'] assert services == {} response = yield from c.agent.service.register('foo') assert response is True services = yield from c.agent.services() - del services['consul'] assert services == { 'foo': { 'Port': 0, @@ -183,12 +181,12 @@ def main(): 'ModifyIndex': 0, 'EnableTagOverride': False, 'Service': 'foo', - 'Tags': None, + 'Tags': [], + 'Meta': {}, 'Address': ''}, } response = yield from c.agent.service.deregister('foo') assert response is True services = yield from c.agent.services() - del services['consul'] assert services == {} c.close() diff --git a/tests/test_base.py b/tests/test_base.py index 7ea79fbd..ef76a9fa 100755 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,10 +1,14 @@ import collections +import json import pytest import consul.base +CB = consul.base.CB +Response = consul.base.Response + Request = collections.namedtuple( 'Request', ['method', 'path', 'params', 'data']) @@ -45,6 +49,28 @@ def _should_support(c): ) +def _should_support_node_meta(c): + return ( + # catalog + c.catalog.nodes, + c.catalog.services, + lambda **kw: c.catalog.service('foo', **kw), + lambda **kw: c.catalog.register('foo', 'bar', **kw), + # health + lambda **kw: c.health.service('foo', **kw), + lambda **kw: c.health.checks('foo', **kw), + lambda **kw: c.health.state('unknown', **kw), + ) + + +def _should_support_meta(c): + return ( + # agent + lambda **kw: c.agent.service.register('foo', **kw), + lambda **kw: c.agent.service.register('foo', 'bar', **kw), + ) + + class TestIndex(object): """ Tests read requests that should support blocking on an index @@ -52,8 +78,8 @@ class TestIndex(object): def test_index(self): c = Consul() for r in _should_support(c): - assert r().params == {} - assert r(index='5').params == {'index': '5'} + assert r().params == [] + assert r(index='5').params == [('index', '5')] class TestConsistency(object): @@ -63,18 +89,85 @@ class TestConsistency(object): def test_explict(self): c = Consul() for r in _should_support(c): - assert r().params == {} - assert r(consistency='default').params == {} - assert r(consistency='consistent').params == {'consistent': '1'} - assert r(consistency='stale').params == {'stale': '1'} + assert r().params == [] + assert r(consistency='default').params == [] + assert r(consistency='consistent').params == [('consistent', '1')] + assert r(consistency='stale').params == [('stale', '1')] def test_implicit(self): c = Consul(consistency='consistent') for r in _should_support(c): - assert r().params == {'consistent': '1'} - assert r(consistency='default').params == {} - assert r(consistency='consistent').params == {'consistent': '1'} - assert r(consistency='stale').params == {'stale': '1'} + assert r().params == [('consistent', '1')] + assert r(consistency='default').params == [] + assert r(consistency='consistent').params == [('consistent', '1')] + assert r(consistency='stale').params == [('stale', '1')] + + +class TestNodemeta(object): + """ + Tests read requests that should support node_meta + """ + + def test_node_meta(self): + c = Consul() + for r in _should_support_node_meta(c): + assert r().params == [] + assert sorted(r(node_meta={'env': 'prod', 'net': 1}).params) == \ + sorted([('node-meta', 'net:1'), ('node-meta', 'env:prod')]) + + +class TestMeta(object): + """ + Tests read requests that should support meta + """ + + def test_meta(self): + c = Consul() + for r in _should_support_meta(c): + d = json.loads(r(meta={'env': 'prod', 'net': 1}).data) + assert sorted(d['meta']) == sorted({'env': 'prod', 'net': 1}) + + +class TestCB(object): + + def test_status_200_passes(self): + response = consul.base.Response(200, None, None) + CB._status(response) + + @pytest.mark.parametrize( + 'response, expected_exception', + [ + (Response(400, None, None), consul.base.BadRequest), + (Response(401, None, None), consul.base.ACLDisabled), + (Response(403, None, None), consul.base.ACLPermissionDenied), + ]) + def test_status_4xx_raises_error(self, response, expected_exception): + with pytest.raises(expected_exception): + CB._status(response) + + def test_status_404_allow_404(self): + response = Response(404, None, None) + CB._status(response, allow_404=True) + + def test_status_404_dont_allow_404(self): + response = Response(404, None, None) + with pytest.raises(consul.base.NotFound): + CB._status(response, allow_404=False) + + def test_status_405_raises_generic_ClientError(self): + response = Response(405, None, None) + with pytest.raises(consul.base.ClientError): + CB._status(response) + + @pytest.mark.parametrize( + 'response', + [ + Response(500, None, None), + Response(599, None, None), + ]) + def test_status_5xx_raises_error(self, response): + with pytest.raises(consul.base.ConsulException): + CB._status(response) class TestChecks(object): diff --git a/tests/test_std.py b/tests/test_std.py index ad6e45be..6aef1e72 100644 --- a/tests/test_std.py +++ b/tests/test_std.py @@ -289,12 +289,12 @@ def test_agent_checks_service_id(self, consul_port): time.sleep(40/1000.0) index, nodes = c.health.service('foo1') - assert [ + assert set([ check['ServiceID'] for node in nodes - for check in node['Checks']] == ['foo1', ''] - assert [ + for check in node['Checks']]) == set(['foo1', '']) + assert set([ check['CheckID'] for node in nodes - for check in node['Checks']] == ['foo', 'serfHealth'] + for check in node['Checks']]) == set(['foo', 'serfHealth']) # Clean up tasks assert c.agent.check.deregister('foo') is True @@ -395,16 +395,16 @@ def test_agent_members(self, consul_port): def test_agent_self(self, consul_port): c = consul.Consul(port=consul_port) - assert set(c.agent.self().keys()) == set(['Member', 'Coord', 'Config', - 'Stats']) + assert set(c.agent.self().keys()) == set(['Member', 'Stats', 'Config', + 'Coord', 'DebugConfig', + 'Meta']) def test_agent_services(self, consul_port): c = consul.Consul(port=consul_port) - assert set(c.agent.services().keys()) == set(['consul']) assert c.agent.service.register('foo') is True - assert set(c.agent.services().keys()) == set(['consul', 'foo']) + assert set(c.agent.services().keys()) == set(['foo']) assert c.agent.service.deregister('foo') is True - assert set(c.agent.services().keys()) == set(['consul']) + assert set(c.agent.services().keys()) == set() # test address param assert c.agent.service.register('foo', address='10.10.10.1') is True @@ -764,7 +764,9 @@ def test_acl_explict_token_use(self, acl_consul): consul.ACLPermissionDenied, c.kv.delete, 'foo', token=token) assert c.kv.get('private/foo')[1]['Value'] == six.b('bar') - assert c.kv.get('private/foo', token=token)[1] is None + pytest.raises( + consul.ACLPermissionDenied, + c.kv.get, 'private/foo', token=token) pytest.raises( consul.ACLPermissionDenied, c.kv.put, 'private/foo', 'bar2', token=token) @@ -773,7 +775,9 @@ def test_acl_explict_token_use(self, acl_consul): c.kv.delete, 'private/foo', token=token) # test token pass through for service registration - c.agent.service.register("bar-1", token=token) + pytest.raises( + consul.ACLPermissionDenied, + c.agent.service.register, "bar-1", token=token) c.agent.service.register("foo-1", token=token) index, data = c.health.service('foo-1', token=token) assert data[0]['Service']['ID'] == "foo-1" @@ -784,7 +788,6 @@ def test_acl_explict_token_use(self, acl_consul): # clean up assert c.agent.service.deregister('foo-1') is True - assert c.agent.service.deregister('bar-1') is True c.acl.destroy(token, token=master_token) acls = c.acl.list(token=master_token) assert set([x['ID'] for x in acls]) == \ @@ -835,7 +838,9 @@ def test_acl_implicit_token_use(self, acl_consul): consul.ACLPermissionDenied, c_limited.kv.delete, 'foo') assert c.kv.get('private/foo')[1]['Value'] == six.b('bar') - assert c_limited.kv.get('private/foo')[1] is None + pytest.raises( + consul.ACLPermissionDenied, + c_limited.kv.get, 'private/foo') pytest.raises( consul.ACLPermissionDenied, c_limited.kv.put, 'private/foo', 'bar2') @@ -844,7 +849,10 @@ def test_acl_implicit_token_use(self, acl_consul): c_limited.kv.delete, 'private/foo') # check we can override the client's default token - assert c.kv.get('private/foo', token=token)[1] is None + pytest.raises( + consul.ACLPermissionDenied, + c.kv.get, 'private/foo', token=token + ) pytest.raises( consul.ACLPermissionDenied, c.kv.put, 'private/foo', 'bar2', token=token) @@ -862,11 +870,8 @@ def test_status_leader(self, consul_port): c = consul.Consul(port=consul_port) agent_self = c.agent.self() - port = agent_self['Config']['Ports']['Server'] - addr = agent_self['Config']['AdvertiseAddr'] - - addr_port = "{0}:{1}".format(addr, port) leader = c.status.leader() + addr_port = agent_self['Stats']['consul']['leader_addr'] assert leader == addr_port, \ "Leader value was {0}, expected value " \ @@ -877,10 +882,8 @@ def test_status_peers(self, consul_port): c = consul.Consul(port=consul_port) agent_self = c.agent.self() - port = agent_self['Config']['Ports']['Server'] - addr = agent_self['Config']['AdvertiseAddr'] - addr_port = "{0}:{1}".format(addr, port) + addr_port = agent_self['Stats']['consul']['leader_addr'] peers = c.status.peers() assert addr_port in peers, \ @@ -922,7 +925,7 @@ def test_coordinate(self, consul_port): c.coordinate.nodes() c.coordinate.datacenters() assert set(c.coordinate.datacenters()[0].keys()) == \ - set(['Datacenter', 'Coordinates']) + set(['Datacenter', 'Coordinates', 'AreaID']) def test_operator(self, consul_port): c = consul.Consul(port=consul_port) diff --git a/tests/test_tornado.py b/tests/test_tornado.py index dbe859be..f69e5ead 100644 --- a/tests/test_tornado.py +++ b/tests/test_tornado.py @@ -186,12 +186,10 @@ def test_agent_services(self, loop, consul_port): def main(): c = consul.tornado.Consul(port=consul_port) services = yield c.agent.services() - del services['consul'] assert services == {} response = yield c.agent.service.register('foo') assert response is True services = yield c.agent.services() - del services['consul'] assert services == { 'foo': { 'Port': 0, @@ -200,12 +198,12 @@ def main(): 'ModifyIndex': 0, 'EnableTagOverride': False, 'Service': 'foo', - 'Tags': None, + 'Tags': [], + 'Meta': {}, 'Address': ''}, } response = yield c.agent.service.deregister('foo') assert response is True services = yield c.agent.services() - del services['consul'] assert services == {} loop.stop() loop.run_sync(main) diff --git a/tests/test_twisted.py b/tests/test_twisted.py index ce380005..097674c5 100644 --- a/tests/test_twisted.py +++ b/tests/test_twisted.py @@ -112,12 +112,10 @@ def test_transaction(self, consul_port): def test_agent_services(self, consul_port): c = consul.twisted.Consul(port=consul_port) services = yield c.agent.services() - del services['consul'] assert services == {} response = yield c.agent.service.register('foo') assert response is True services = yield c.agent.services() - del services['consul'] assert services == { 'foo': { 'Port': 0, @@ -126,13 +124,13 @@ def test_agent_services(self, consul_port): 'ModifyIndex': 0, 'EnableTagOverride': False, 'Service': 'foo', - 'Tags': None, + 'Tags': [], + 'Meta': {}, 'Address': ''} } response = yield c.agent.service.deregister('foo') assert response is True services = yield c.agent.services() - del services['consul'] assert services == {} @pytest.inlineCallbacks diff --git a/tox.ini b/tox.ini index 1a97df92..4df9a5fc 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = flake8, py26, py27, pypy, py34, py35, py36 +envlist = flake8, py26, py27, pypy, py35, py36 [flake8] ignore = F811,E226 @@ -24,21 +24,6 @@ deps = commands = py.test --reruns=3 {posargs:consul tests} -[testenv:py34] -deps = - pytest - pytest-rerunfailures - pytest-twisted - twisted - treq - pyOpenSSL - tornado - aiohttp - flake8 -commands = - py.test --reruns=3 {posargs:consul tests} - flake8 --exclude=".tox/*,xx/*,__*,docs/*" - [testenv:py35] deps = pytest @@ -72,12 +57,3 @@ commands = [testenv:flake8] deps = flake8 commands = flake8 - -[testenv:coverage] -deps = - pytest-cov - tornado - coveralls -commands = - py.test --cov {envsitepackagesdir}/consul tests - coveralls