diff --git a/.github/workflows/python-package.yaml b/.github/workflows/python-package.yaml new file mode 100644 index 00000000..ee8141fc --- /dev/null +++ b/.github/workflows/python-package.yaml @@ -0,0 +1,38 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Python package + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.9", "3.10", "3.11"] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + ./download_etcd.sh 3.4.0 + python -m pip install --upgrade pip + python -m pip install tox coveralls + - name: Test with tox + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + tox + coveralls + diff --git a/.gitignore b/.gitignore index 3f90b7fd..d782d163 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ build dist docs .coverage +.venv +.env diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2c3ba505..00000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -language: python -python: - - "2.7" - - "3.5" - -before_install: - - ./build_etcd.sh v2.2.0 - - pip install --upgrade setuptools - -# command to install dependencies -install: - - pip install coveralls - - pip install coverage - - python bootstrap.py - - bin/buildout - -# command to run tests -script: - PATH=$PATH:./etcd/bin coverage run --source=src/etcd --omit="src/etcd/tests/*" bin/test - -after_success: coveralls -# Add env var to detect it during build -env: TRAVIS=True diff --git a/AUTHORS b/AUTHORS index 03485651..716c8835 100644 --- a/AUTHORS +++ b/AUTHORS @@ -8,22 +8,29 @@ Contributors: ------------ Aleksandar Veselinovic Alexander Brand +Alexander Kukushkin Alex Chan Alex Ianchici +Ainlolcat Bartlomiej Biernacki Bradley Cicenas Christoph Heer +Gigi Sayfan Hogenmiller +Huangdong Jimmy Zelinskie Jim Rollenhagen John Kristensen Joshua Conner +Lars Bahner +Matthew Barnes Matthias Urlichs Michal Witkowski Mike Place Nick Bartos Mingqing Peter Wagner +Realityone Roberto Aguilar Roy Smith Ryan Fowler @@ -37,9 +44,11 @@ SkyLothar Spike Curtis Stephen Milner Taylor McKinnon +Tobe Tomas Kral Tom Denham Toshiya Kawasaki WillPlatnick Weizheng Xu WooParadog +Wei Tie diff --git a/MANIFEST.in b/MANIFEST.in index 1e7e5684..e34a526c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,4 @@ +include AUTHORS +include LICENSE.txt include README.rst include NEWS.txt diff --git a/NEWS.txt b/NEWS.txt index b410e80b..39f8db71 100644 --- a/NEWS.txt +++ b/NEWS.txt @@ -1,5 +1,36 @@ News ==== +0.5.0 +----- +*Release date: 31-Oct-2023 + +* Drop python 2.x compatibility (should still work) +* Move to use pytest +* Support urllib3 v2, including support of self-signed certs +* Fix version check to avoid crashes with non-official releases +* Correctly handle watch timeouts in lock +* Allow trying more than one domain when looking up SRV records +* Support auth API both <= 2.2.5 and >= 2.3.0 +* Use github actions instead than travis + +0.4.5 +----- +*Release date: 3-Mar-2017* + +* Remove dnspython2/3 requirement +* Change property name setter in lock +* Fixed acl tests +* Added version/cluster_version properties to client +* Fixes in lock when used as context manager +* Fixed improper usage of urllib3 exceptions +* Minor fixes for error classes +* In lock return modifiedIndex to watch changes +* In lock fix context manager exception handling +* Improvments to the documentation +* Remove _base_uri only after refresh from cluster +* Avoid double update of _machines_cache + + 0.4.4 ----- *Release date: 10-Jan-2017* diff --git a/README.rst b/README.rst index 9e7381eb..8d68c00a 100644 --- a/README.rst +++ b/README.rst @@ -28,6 +28,13 @@ From source $ python setup.py install +From Pypi +~~~~~~~~~ + +.. code:: bash + + $ python -m pip install python-etcd + Usage ----- @@ -43,6 +50,7 @@ Create a client object client = etcd.Client() # this will create a client against etcd server running on localhost on port 4001 client = etcd.Client(port=4002) client = etcd.Client(host='127.0.0.1', port=4003) + client = etcd.Client(host=(('127.0.0.1', 4001), ('127.0.0.1', 4002), ('127.0.0.1', 4003))) client = etcd.Client(host='127.0.0.1', port=4003, allow_redirect=False) # wont let you run sensitive commands on non-leader machines, default is true # If you have defined a SRV record for _etcd._tcp.example.com pointing to the clients client = etcd.Client(srv_domain='example.com', protocol="https") @@ -201,18 +209,17 @@ List contents of a directory Development setup ----------------- -To create a buildout, +To check your code, .. code:: bash - $ python bootstrap.py - $ bin/buildout + $ tox to test you should have etcd available in your system path: .. code:: bash - $ bin/test + $ command -v etcd to generate documentation, diff --git a/black.toml b/black.toml new file mode 100644 index 00000000..11d9138c --- /dev/null +++ b/black.toml @@ -0,0 +1,19 @@ +[tool.black] +line-length = 100 +target-version = ['py37'] +include = '\.pyi?$' +exclude = ''' +( + \.eggs # exclude a few common directories in the + | \.git # root of the project + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | venv + | _build + | buck-out + | build + | dist +) +''' diff --git a/bootstrap.py b/bootstrap.py deleted file mode 100644 index 1b28969a..00000000 --- a/bootstrap.py +++ /dev/null @@ -1,170 +0,0 @@ -############################################################################## -# -# Copyright (c) 2006 Zope Foundation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE. -# -############################################################################## -"""Bootstrap a buildout-based project - -Simply run this script in a directory containing a buildout.cfg. -The script accepts buildout command-line options, so you can -use the -c option to specify an alternate configuration file. -""" - -import os -import shutil -import sys -import tempfile - -from optparse import OptionParser - -tmpeggs = tempfile.mkdtemp() - -usage = '''\ -[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options] - -Bootstraps a buildout-based project. - -Simply run this script in a directory containing a buildout.cfg, using the -Python that you want bin/buildout to use. - -Note that by using --find-links to point to local resources, you can keep -this script from going over the network. -''' - -parser = OptionParser(usage=usage) -parser.add_option("-v", "--version", help="use a specific zc.buildout version") - -parser.add_option("-t", "--accept-buildout-test-releases", - dest='accept_buildout_test_releases', - action="store_true", default=False, - help=("Normally, if you do not specify a --version, the " - "bootstrap script and buildout gets the newest " - "*final* versions of zc.buildout and its recipes and " - "extensions for you. If you use this flag, " - "bootstrap and buildout will get the newest releases " - "even if they are alphas or betas.")) -parser.add_option("-c", "--config-file", - help=("Specify the path to the buildout configuration " - "file to be used.")) -parser.add_option("-f", "--find-links", - help=("Specify a URL to search for buildout releases")) - - -options, args = parser.parse_args() - -###################################################################### -# load/install setuptools - -to_reload = False -try: - import pkg_resources - import setuptools -except ImportError: - ez = {} - - try: - from urllib.request import urlopen - except ImportError: - from urllib2 import urlopen - - # XXX use a more permanent ez_setup.py URL when available. - exec(urlopen('https://bitbucket.org/pypa/setuptools/raw/0.7.2/ez_setup.py' - ).read(), ez) - setup_args = dict(to_dir=tmpeggs, download_delay=0) - ez['use_setuptools'](**setup_args) - - if to_reload: - reload(pkg_resources) - import pkg_resources - # This does not (always?) update the default working set. We will - # do it. - for path in sys.path: - if path not in pkg_resources.working_set.entries: - pkg_resources.working_set.add_entry(path) - -###################################################################### -# Install buildout - -ws = pkg_resources.working_set - -cmd = [sys.executable, '-c', - 'from setuptools.command.easy_install import main; main()', - '-mZqNxd', tmpeggs] - -find_links = os.environ.get( - 'bootstrap-testing-find-links', - options.find_links or - ('http://downloads.buildout.org/' - if options.accept_buildout_test_releases else None) - ) -if find_links: - cmd.extend(['-f', find_links]) - -setuptools_path = ws.find( - pkg_resources.Requirement.parse('setuptools')).location - -requirement = 'zc.buildout' -version = options.version -if version is None and not options.accept_buildout_test_releases: - # Figure out the most recent final version of zc.buildout. - import setuptools.package_index - _final_parts = '*final-', '*final' - - def _final_version(parsed_version): - for part in parsed_version: - if (part[:1] == '*') and (part not in _final_parts): - return False - return True - index = setuptools.package_index.PackageIndex( - search_path=[setuptools_path]) - if find_links: - index.add_find_links((find_links,)) - req = pkg_resources.Requirement.parse(requirement) - if index.obtain(req) is not None: - best = [] - bestv = None - for dist in index[req.project_name]: - distv = dist.parsed_version - if _final_version(distv): - if bestv is None or distv > bestv: - best = [dist] - bestv = distv - elif distv == bestv: - best.append(dist) - if best: - best.sort() - version = best[-1].version -if version: - requirement = '=='.join((requirement, version)) -cmd.append(requirement) - -import subprocess -if subprocess.call(cmd, env=dict(os.environ, PYTHONPATH=setuptools_path)) != 0: - raise Exception( - "Failed to execute command:\n%s", - repr(cmd)[1:-1]) - -###################################################################### -# Import and run buildout - -ws.add_entry(tmpeggs) -ws.require(requirement) -import zc.buildout.buildout - -if not [a for a in args if '=' not in a]: - args.append('bootstrap') - -# if -c was provided, we push it back into args for buildout' main function -if options.config_file is not None: - args[0:0] = ['-c', options.config_file] - -zc.buildout.buildout.main(args) -shutil.rmtree(tmpeggs) diff --git a/build_etcd.sh b/build_etcd.sh deleted file mode 100755 index fc319919..00000000 --- a/build_etcd.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/sh - -if [ $# -gt 0 ] - then - ETCD_VERSION="$1"; - else - ETCD_VERSION="master"; -fi - -echo "Using ETCD version $ETCD_VERSION" - -git clone https://github.com/coreos/etcd.git -cd etcd -git checkout $ETCD_VERSION -./build - - -${TRAVIS:?"This is not a Travis build. All Done"} -#Temporal solution to travis issue #155 -sudo rm -rf /dev/shm && sudo ln -s /run/shm /dev/shm -echo "All Done" diff --git a/buildout.cfg b/buildout.cfg deleted file mode 100644 index 4de90366..00000000 --- a/buildout.cfg +++ /dev/null @@ -1,41 +0,0 @@ -[buildout] -parts = python - sphinxbuilder - test - coverage -develop = . -eggs = - urllib3==1.7.1 - pyOpenSSL==0.13.1 - ${deps:extraeggs} - -[python] -recipe = zc.recipe.egg -interpreter = python -eggs = ${buildout:eggs} - -[test] -recipe = pbp.recipe.noserunner -eggs = ${python:eggs} - mock - -[coverage] -recipe = pbp.recipe.noserunner -eggs = ${test:eggs} - coverage -defaults = --with-coverage - --cover-package=etcd - -[sphinxbuilder] -recipe = collective.recipe.sphinxbuilder -source = ${buildout:directory}/docs-source -build = ${buildout:directory}/docs - - -[deps:python2] -extraeggs = - dnspython==1.12.0 - -[deps:python3] -extraeggs = - dnspython3==1.12.0 diff --git a/docs-source/conf.py b/docs-source/conf.py index 5148c23a..8b94a705 100644 --- a/docs-source/conf.py +++ b/docs-source/conf.py @@ -2,6 +2,7 @@ import sys, os + class Mock(object): def __init__(self, *args, **kwargs): pass @@ -11,8 +12,8 @@ def __call__(self, *args, **kwargs): @classmethod def __getattr__(cls, name): - if name in ('__file__', '__path__'): - return '/dev/null' + if name in ("__file__", "__path__"): + return "/dev/null" elif name[0] == name[0].upper(): mockType = type(name, (), {}) mockType.__module__ = __name__ @@ -20,216 +21,211 @@ def __getattr__(cls, name): else: return Mock() -MOCK_MODULES = ['urllib3'] + +MOCK_MODULES = ["urllib3"] for mod_name in MOCK_MODULES: sys.modules[mod_name] = Mock() # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('../src')) +sys.path.insert(0, os.path.abspath("../src")) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc'] +extensions = ["sphinx.ext.autodoc"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'python-etcd' -copyright = u'2013-2015 Jose Plana, Giuseppe Lavagetto' +project = "python-etcd" +copyright = "2013-2015 Jose Plana, Giuseppe Lavagetto" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '0.4' +version = "0.4" # The full version, including alpha/beta/rc tags. -release = '0.4.3' +release = "0.4.3" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'sphinxdoc' +html_theme = "sphinxdoc" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'python-etcddoc' +htmlhelp_basename = "python-etcddoc" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'python-etcd.tex', u'python-etcd Documentation', - u'Jose Plana', 'manual'), + ("index", "python-etcd.tex", "python-etcd Documentation", "Jose Plana", "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'python-etcd', u'python-etcd Documentation', - [u'Jose Plana'], 1) -] +man_pages = [("index", "python-etcd", "python-etcd Documentation", ["Jose Plana"], 1)] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ @@ -238,16 +234,22 @@ def __getattr__(cls, name): # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'python-etcd', u'python-etcd Documentation', - u'Jose Plana', 'python-etcd', 'One line description of project.', - 'Miscellaneous'), + ( + "index", + "python-etcd", + "python-etcd Documentation", + "Jose Plana", + "python-etcd", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' diff --git a/download_etcd.sh b/download_etcd.sh new file mode 100755 index 00000000..f9e97dd4 --- /dev/null +++ b/download_etcd.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -e +VERSION=${1:-2.3.7} +mkdir -p bin +URL="https://github.com/coreos/etcd/releases/download/v${VERSION}/etcd-v${VERSION}-linux-amd64.tar.gz" +curl -L $URL | tar -C ./bin --strip-components=1 -xzvf - "etcd-v${VERSION}-linux-amd64/etcd" +mv bin/etcd /usr/local/bin/ diff --git a/setup.py b/setup.py index 52f6994a..8bbc4d33 100644 --- a/setup.py +++ b/setup.py @@ -2,46 +2,38 @@ import sys, os here = os.path.abspath(os.path.dirname(__file__)) -README = open(os.path.join(here, 'README.rst')).read() -NEWS = open(os.path.join(here, 'NEWS.txt')).read() +README = open(os.path.join(here, "README.rst")).read() +NEWS = open(os.path.join(here, "NEWS.txt")).read() -version = '0.4.4' +version = "0.5.0" -install_requires = [ - 'urllib3>=1.7.1', - 'dnspython>=1.13.0' -] +install_requires = ["urllib3>=1.7.1", "dnspython>=1.13.0"] -test_requires = [ - 'mock', - 'nose', - 'pyOpenSSL>=0.14' -] +test_requires = ["mock", "pytest", "pyOpenSSL>=0.14"] setup( - name='python-etcd', + name="python-etcd", version=version, description="A python client for etcd", - long_description=README + '\n\n' + NEWS, + long_description=README + "\n\n" + NEWS, classifiers=[ "Topic :: System :: Distributed Computing", "Topic :: Software Development :: Libraries", "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", "Topic :: Database :: Front-Ends", ], - keywords='etcd raft distributed log api client', - author='Jose Plana', - author_email='jplana@gmail.com', - url='http://github.com/jplana/python-etcd', - license='MIT', - packages=find_packages('src'), - package_dir = {'': 'src'}, + keywords="etcd raft distributed log api client", + author="Jose Plana", + author_email="jplana@gmail.com", + url="http://github.com/jplana/python-etcd", + license="MIT", + packages=find_packages("src"), + package_dir={"": "src"}, include_package_data=True, zip_safe=False, install_requires=install_requires, tests_require=test_requires, - test_suite='nose.collector', + test_suite="nose.collector", ) diff --git a/src/etcd/__init__.py b/src/etcd/__init__.py index 33b1d679..d716e9be 100644 --- a/src/etcd/__init__.py +++ b/src/etcd/__init__.py @@ -13,19 +13,21 @@ class NullHandler(logging.Handler): def emit(self, record): pass + + _log.addHandler(NullHandler()) class EtcdResult(object): _node_props = { - 'key': None, - 'value': None, - 'expiration': None, - 'ttl': None, - 'modifiedIndex': None, - 'createdIndex': None, - 'newKey': False, - 'dir': False, + "key": None, + "value": None, + "expiration": None, + "ttl": None, + "modifiedIndex": None, + "createdIndex": None, + "newKey": False, + "dir": False, } def __init__(self, action=None, node=None, prevNode=None, **kwdargs): @@ -41,16 +43,16 @@ def __init__(self, action=None, node=None, prevNode=None, **kwdargs): """ self.action = action - for (key, default) in self._node_props.items(): + for key, default in self._node_props.items(): if key in node: setattr(self, key, node[key]) else: setattr(self, key, default) self._children = [] - if self.dir and 'nodes' in node: + if self.dir and "nodes" in node: # We keep the data in raw format, converting them only when needed - self._children = node['nodes'] + self._children = node["nodes"] if prevNode: self._prev_node = EtcdResult(None, node=prevNode) @@ -60,8 +62,8 @@ def __init__(self, action=None, node=None, prevNode=None, **kwdargs): def parse_headers(self, response): headers = response.getheaders() - self.etcd_index = int(headers.get('x-etcd-index', 1)) - self.raft_index = int(headers.get('x-raft-index', 1)) + self.etcd_index = int(headers.get("x-etcd-index", 1)) + self.raft_index = int(headers.get("x-raft-index", 1)) def get_subtree(self, leaves_only=False): """ @@ -73,7 +75,7 @@ def get_subtree(self, leaves_only=False): """ if not self._children: - #if the current result is a leaf, return itself + # if the current result is a leaf, return itself yield self return else: @@ -92,7 +94,7 @@ def leaves(self): @property def children(self): - """ Deprecated, use EtcdResult.leaves instead """ + """Deprecated, use EtcdResult.leaves instead""" return self.leaves def __eq__(self, other): @@ -120,6 +122,7 @@ class EtcdException(Exception): """ Generic Etcd Exception. """ + def __init__(self, message=None, payload=None): super(EtcdException, self).__init__(message) self.payload = payload @@ -129,6 +132,7 @@ class EtcdValueError(EtcdException, ValueError): """ Base class for Etcd value-related errors. """ + pass @@ -136,6 +140,7 @@ class EtcdCompareFailed(EtcdValueError): """ Compare-and-swap failure """ + pass @@ -145,6 +150,7 @@ class EtcdClusterIdChanged(EtcdException): with a backup. Raised to prevent waiting on an etcd_index that was only valid on the old cluster. """ + pass @@ -152,6 +158,7 @@ class EtcdKeyError(EtcdException): """ Etcd Generic KeyError Exception """ + pass @@ -159,6 +166,7 @@ class EtcdKeyNotFound(EtcdKeyError): """ Etcd key not found exception (100) """ + pass @@ -166,6 +174,7 @@ class EtcdNotFile(EtcdKeyError): """ Etcd not a file exception (102) """ + pass @@ -173,6 +182,7 @@ class EtcdNotDir(EtcdKeyError): """ Etcd not a directory exception (104) """ + pass @@ -180,6 +190,7 @@ class EtcdAlreadyExist(EtcdKeyError): """ Etcd already exist exception (105) """ + pass @@ -187,6 +198,7 @@ class EtcdEventIndexCleared(EtcdException): """ Etcd event index is outdated and cleared exception (401) """ + pass @@ -194,9 +206,9 @@ class EtcdConnectionFailed(EtcdException): """ Connection to etcd failed. """ + def __init__(self, message=None, payload=None, cause=None): - super(EtcdConnectionFailed, self).__init__(message=message, - payload=payload) + super(EtcdConnectionFailed, self).__init__(message=message, payload=payload) self.cause = cause @@ -204,6 +216,7 @@ class EtcdInsufficientPermissions(EtcdException): """ Request failed because of insufficient permissions. """ + pass @@ -211,6 +224,7 @@ class EtcdWatchTimedOut(EtcdConnectionFailed): """ A watch timed out without returning a result. """ + pass @@ -218,6 +232,7 @@ class EtcdWatcherCleared(EtcdException): """ Watcher is cleared due to etcd recovery. """ + pass @@ -225,6 +240,7 @@ class EtcdLeaderElectionInProgress(EtcdException): """ Request failed due to in-progress leader election. """ + pass @@ -232,6 +248,7 @@ class EtcdRootReadOnly(EtcdKeyError): """ Operation is not valid on the root, which is read only. """ + pass @@ -239,6 +256,7 @@ class EtcdDirNotEmpty(EtcdValueError): """ Directory not empty. """ + pass @@ -246,6 +264,7 @@ class EtcdLockExpired(EtcdException): """ Our lock apparently expired while we were trying to acquire it. """ + pass @@ -263,7 +282,6 @@ class EtcdError(object): 108: EtcdDirNotEmpty, # 109: Non-public: existing peer addr. 110: EtcdInsufficientPermissions, - 200: EtcdValueError, # Not part of v2 201: EtcdValueError, 202: EtcdValueError, @@ -275,10 +293,8 @@ class EtcdError(object): 208: EtcdValueError, 209: EtcdValueError, 210: EtcdValueError, - # 300: Non-public: Raft internal error. 301: EtcdLeaderElectionInProgress, - 400: EtcdWatcherCleared, 401: EtcdEventIndexCleared, } @@ -293,7 +309,7 @@ def handle(cls, payload): error_code = payload.get("errorCode") message = payload.get("message") cause = payload.get("cause") - msg = '{} : {}'.format(message, cause) + msg = "{} : {}".format(message, cause) status = payload.get("status") # Some general status handling, as # not all endpoints return coherent error messages @@ -312,6 +328,7 @@ def handle(cls, payload): # Blatantly copied from requests. try: from urllib3.contrib import pyopenssl + pyopenssl.inject_into_urllib3() except ImportError: pass diff --git a/src/etcd/auth.py b/src/etcd/auth.py index 796772d7..a1930681 100644 --- a/src/etcd/auth.py +++ b/src/etcd/auth.py @@ -7,20 +7,32 @@ class EtcdAuthBase(object): - entity = 'example' + entity = "example" def __init__(self, client, name): self.client = client self.name = name - self.uri = "{}/auth/{}s/{}".format(self.client.version_prefix, - self.entity, self.name) + self.uri = "{}/auth/{}s/{}".format(self.client.version_prefix, self.entity, self.name) + # This will be lazily evaluated if not manually set + self._legacy_api = None + + @property + def legacy_api(self): + if self._legacy_api is None: + # The auth API has changed between 2.2 and 2.3, true story! + major, minor = map(int, self.client.version[:3].split(".")) + self._legacy_api = major < 3 and minor < 3 + return self._legacy_api @property def names(self): key = "{}s".format(self.entity) uri = "{}/auth/{}".format(self.client.version_prefix, key) response = self.client.api_execute(uri, self.client._MGET) - return json.loads(response.data.decode('utf-8'))[key] + if self.legacy_api: + return json.loads(response.data.decode("utf-8"))[key] + else: + return [obj[self.entity] for obj in json.loads(response.data.decode("utf-8"))[key]] def read(self): try: @@ -32,11 +44,14 @@ def read(self): _log.info("%s '%s' not found", self.entity, self.name) raise except Exception as e: - _log.error("Failed to fetch %s in %s%s: %r", - self.entity, self.client._base_uri, - self.client.version_prefix, e) - raise etcd.EtcdException( - "Could not fetch {} '{}'".format(self.entity, self.name)) + _log.error( + "Failed to fetch %s in %s%s: %r", + self.entity, + self.client._base_uri, + self.client.version_prefix, + e, + ) + raise etcd.EtcdException("Could not fetch {} '{}'".format(self.entity, self.name)) self._from_net(response.data) @@ -48,9 +63,7 @@ def write(self): r = None try: for payload in self._to_net(r): - response = self.client.api_execute_json(self.uri, - self.client._MPUT, - params=payload) + response = self.client.api_execute_json(self.uri, self.client._MPUT, params=payload) # This will fail if the response is an error self._from_net(response.data) except etcd.EtcdInsufficientPermissions as e: @@ -60,8 +73,8 @@ def write(self): _log.error("Failed to write %s '%s'", self.entity, self.name) # TODO: fine-grained exception handling raise etcd.EtcdException( - "Could not write {} '{}': {}".format(self.entity, - self.name, e)) + "Could not write {} '{}': {}".format(self.entity, self.name, e) + ) def delete(self): try: @@ -73,10 +86,14 @@ def delete(self): _log.info("%s '%s' not found", self.entity, self.name) raise except Exception as e: - _log.error("Failed to delete %s in %s%s: %r", - self.entity, self._base_uri, self.version_prefix, e) - raise etcd.EtcdException( - "Could not delete {} '{}'".format(self.entity, self.name)) + _log.error( + "Failed to delete %s in %s%s: %r", + self.entity, + self._base_uri, + self.version_prefix, + e, + ) + raise etcd.EtcdException("Could not delete {} '{}'".format(self.entity, self.name)) def _from_net(self, data): raise NotImplementedError() @@ -93,7 +110,8 @@ def new(cls, client, data): class EtcdUser(EtcdAuthBase): """Class to manage in a orm-like way etcd users""" - entity = 'user' + + entity = "user" def __init__(self, client, name): super(EtcdUser, self).__init__(client, name) @@ -101,14 +119,28 @@ def __init__(self, client, name): self._password = None def _from_net(self, data): - d = json.loads(data.decode('utf-8')) - self.roles = d.get('roles', []) - self.name = d.get('user') + d = json.loads(data.decode("utf-8")) + roles = d.get("roles", []) + try: + self.roles = roles + except TypeError: + # with the change of API, PUT responses are different + # from GET reponses, which makes everything so funny. + # Specifically, PUT responses are the same as before... + if self.legacy_api: + raise + self.roles = [obj["role"] for obj in roles] + self.name = d.get("user") def _to_net(self, prevobj=None): if prevobj is None: - retval = [{"user": self.name, "password": self._password, - "roles": list(self.roles)}] + retval = [ + { + "user": self.name, + "password": self._password, + "roles": list(self.roles), + } + ] else: retval = [] if self._password: @@ -146,9 +178,8 @@ def __str__(self): return json.dumps(self._to_net()[0]) - class EtcdRole(EtcdAuthBase): - entity = 'role' + entity = "role" def __init__(self, client, name): super(EtcdRole, self).__init__(client, name) @@ -156,8 +187,8 @@ def __init__(self, client, name): self._write_paths = set() def _from_net(self, data): - d = json.loads(data.decode('utf-8')) - self.name = d.get('role') + d = json.loads(data.decode("utf-8")) + self.name = d.get("role") try: kv = d["permissions"]["kv"] @@ -166,50 +197,48 @@ def _from_net(self, data): self._write_paths = set() return - self._read_paths = set(kv.get('read', [])) - self._write_paths = set(kv.get('write', [])) + self._read_paths = set(kv.get("read", [])) + self._write_paths = set(kv.get("write", [])) def _to_net(self, prevobj=None): retval = [] if prevobj is None: - retval.append({ - "role": self.name, - "permissions": + retval.append( { - "kv": - { - "read": list(self._read_paths), - "write": list(self._write_paths) - } + "role": self.name, + "permissions": { + "kv": { + "read": list(self._read_paths), + "write": list(self._write_paths), + } + }, } - }) + ) else: to_grant = { - 'read': list(self._read_paths - prevobj._read_paths), - 'write': list(self._write_paths - prevobj._write_paths) + "read": list(self._read_paths - prevobj._read_paths), + "write": list(self._write_paths - prevobj._write_paths), } to_revoke = { - 'read': list(prevobj._read_paths - self._read_paths), - 'write': list(prevobj._write_paths - self._write_paths) + "read": list(prevobj._read_paths - self._read_paths), + "write": list(prevobj._write_paths - self._write_paths), } if [path for sublist in to_revoke.values() for path in sublist]: - retval.append({'role': self.name, 'revoke': {'kv': to_revoke}}) + retval.append({"role": self.name, "revoke": {"kv": to_revoke}}) if [path for sublist in to_grant.values() for path in sublist]: - retval.append({'role': self.name, 'grant': {'kv': to_grant}}) + retval.append({"role": self.name, "grant": {"kv": to_grant}}) return retval def grant(self, path, permission): - if permission.upper().find('R') >= 0: + if permission.upper().find("R") >= 0: self._read_paths.add(path) - if permission.upper().find('W') >= 0: + if permission.upper().find("W") >= 0: self._write_paths.add(path) def revoke(self, path, permission): - if permission.upper().find('R') >= 0 and \ - path in self._read_paths: + if permission.upper().find("R") >= 0 and path in self._read_paths: self._read_paths.remove(path) - if permission.upper().find('W') >= 0 and \ - path in self._write_paths: + if permission.upper().find("W") >= 0 and path in self._write_paths: self._write_paths.remove(path) @property @@ -217,12 +246,12 @@ def acls(self): perms = {} try: for path in self._read_paths: - perms[path] = 'R' + perms[path] = "R" for path in self._write_paths: if path in perms: - perms[path] += 'W' + perms[path] += "W" else: - perms[path] = 'W' + perms[path] = "W" except: pass return perms @@ -235,7 +264,7 @@ def acls(self, acls): self.grant(path, permission) def __str__(self): - return json.dumps({"role": self.name, 'acls': self.acls}) + return json.dumps({"role": self.name, "acls": self.acls}) class Auth(object): @@ -246,7 +275,7 @@ def __init__(self, client): @property def active(self): resp = self.client.api_execute(self.uri, self.client._MGET) - return json.loads(resp.data.decode('utf-8'))['enabled'] + return json.loads(resp.data.decode("utf-8"))["enabled"] @active.setter def active(self, value): diff --git a/src/etcd/client.py b/src/etcd/client.py index a71da5ae..a0117574 100644 --- a/src/etcd/client.py +++ b/src/etcd/client.py @@ -7,6 +7,7 @@ """ import logging + try: # Python 3 from http.client import HTTPException @@ -15,12 +16,15 @@ from httplib import HTTPException import socket import urllib3 -import urllib3.util +from urllib3.exceptions import HTTPError +from urllib3.exceptions import ReadTimeoutError import json import ssl import dns.resolver from functools import wraps import etcd +from dns.resolver import NXDOMAIN +import re try: from urlparse import urlparse @@ -37,34 +41,34 @@ class Client(object): Client for etcd, the distributed log service using raft. """ - _MGET = 'GET' - _MPUT = 'PUT' - _MPOST = 'POST' - _MDELETE = 'DELETE' - _comparison_conditions = set(('prevValue', 'prevIndex', 'prevExist', 'refresh')) - _read_options = set(('recursive', 'wait', 'waitIndex', 'sorted', 'quorum')) - _del_conditions = set(('prevValue', 'prevIndex')) + _MGET = "GET" + _MPUT = "PUT" + _MPOST = "POST" + _MDELETE = "DELETE" + _comparison_conditions = set(("prevValue", "prevIndex", "prevExist", "refresh")) + _read_options = set(("recursive", "wait", "waitIndex", "sorted", "quorum")) + _del_conditions = set(("prevValue", "prevIndex")) http = None def __init__( - self, - host='127.0.0.1', - port=4001, - srv_domain=None, - version_prefix='/v2', - read_timeout=60, - allow_redirect=True, - protocol='http', - cert=None, - ca_cert=None, - username=None, - password=None, - allow_reconnect=False, - use_proxies=False, - expected_cluster_id=None, - per_host_pool_size=10, - lock_prefix="/_locks" + self, + host="127.0.0.1", + port=4001, + srv_domain=None, + version_prefix="/v2", + read_timeout=60, + allow_redirect=True, + protocol="http", + cert=None, + ca_cert=None, + username=None, + password=None, + allow_reconnect=False, + use_proxies=False, + expected_cluster_id=None, + per_host_pool_size=10, + lock_prefix="/_locks", ): """ Initialize the client. @@ -89,7 +93,7 @@ def __init__( cert (mixed): If a string, the whole ssl client certificate; if a tuple, the cert and key file names. - ca_cert (str): The ca certificate. If pressent it will enable + ca_cert (str): The ca certificate. If present it will enable validation. username (str): username for etcd authentication. @@ -121,13 +125,12 @@ def __init__( try: host = self._discover(srv_domain) except Exception as e: - _log.error("Could not discover the etcd hosts from %s: %s", - srv_domain, e) + _log.error("Could not discover the etcd hosts from %s: %s", srv_domain, e) self._protocol = protocol def uri(protocol, host, port): - return '%s://%s:%d' % (protocol, host, port) + return "%s://%s:%d" % (protocol, host, port) if not isinstance(host, tuple): self._machines_cache = [] @@ -135,7 +138,9 @@ def uri(protocol, host, port): else: if not allow_reconnect: _log.error("List of hosts incompatible with allow_reconnect.") - raise etcd.EtcdException("A list of hosts to connect to was given, but reconnection not allowed?") + raise etcd.EtcdException( + "A list of hosts to connect to was given, but reconnection not allowed?" + ) self._machines_cache = [uri(self._protocol, *conn) for conn in host] self._base_uri = self._machines_cache.pop(0) @@ -150,25 +155,26 @@ def uri(protocol, host, port): # SSL Client certificate support - kw = { - 'maxsize': per_host_pool_size - } + kw = {"maxsize": per_host_pool_size} if self._read_timeout > 0: - kw['timeout'] = self._read_timeout + kw["timeout"] = self._read_timeout if cert: if isinstance(cert, tuple): # Key and cert are separate - kw['cert_file'] = cert[0] - kw['key_file'] = cert[1] + kw["cert_file"] = cert[0] + kw["key_file"] = cert[1] else: # combined certificate - kw['cert_file'] = cert + kw["cert_file"] = cert if ca_cert: - kw['ca_certs'] = ca_cert - kw['cert_reqs'] = ssl.CERT_REQUIRED + kw["ca_certs"] = ca_cert + kw["cert_reqs"] = ssl.CERT_REQUIRED + else: + kw["cert_reqs"] = ssl.CERT_NONE + urllib3.disable_warnings() self.username = None self.password = None @@ -176,9 +182,9 @@ def uri(protocol, host, port): self.username = username self.password = password elif username: - _log.warning('Username provided without password, both are required for authentication') + _log.warning("Username provided without password, both are required for authentication") elif password: - _log.warning('Password provided without username, both are required for authentication') + _log.warning("Password provided without username, both are required for authentication") self.http = urllib3.PoolManager(num_pools=10, **kw) @@ -198,23 +204,53 @@ def uri(protocol, host, port): # extend the list given to the client with what we get # from self.machines if not self._use_proxies: - self._machines_cache = list(set(self._machines_cache) | - set(self.machines)) + self._machines_cache = list(set(self._machines_cache) | set(self.machines)) if self._base_uri in self._machines_cache: self._machines_cache.remove(self._base_uri) - _log.debug("Machines cache initialised to %s", - self._machines_cache) + _log.debug("Machines cache initialised to %s", self._machines_cache) + + # Versions set to None. They will be set upon first usage. + self._version = self._cluster_version = None + + def _set_version_info(self): + """ + Sets the version information provided by the server. + """ + # Set the version + data = self.api_execute("/version", self._MGET).data + version_info = json.loads(data.decode("utf-8")) + self._version = version_info["etcdserver"] + self._cluster_version = version_info["etcdcluster"] def _discover(self, domain): - srv_name = "_etcd._tcp.{}".format(domain) - answers = dns.resolver.query(srv_name, 'SRV') + srv_names = [ + "_etcd-client-ssl._tcp.{}".format(domain), + "_etcd-client._tcp.{}".format(domain), + "_etcd-ssl._tcp.{}".format(domain), + "_etcd._tcp.{}".format(domain), + ] + found = False + for srv_name in srv_names: + try: + answers = dns.resolver.query(srv_name, "SRV") + if len(answers): + found = True + break + except NXDOMAIN: + continue + + if not found: + raise ValueError("Could not find SRV record for domain {}.".format(domain)) + + if re.search("-ssl", srv_name): + self._protocol = "https" + hosts = [] for answer in answers: - hosts.append( - (answer.target.to_text(omit_final_dot=True), answer.port)) + hosts.append((answer.target.to_text(omit_final_dot=True), answer.port)) _log.debug("Found %s", hosts) if not len(hosts): - raise ValueError("The SRV record is present but no host were found") + raise ValueError("The SRV record is present but no hosts were found") return tuple(hosts) def __del__(self): @@ -234,12 +270,12 @@ def base_uri(self): @property def host(self): """Node to connect etcd.""" - return urlparse(self._base_uri).netloc.split(':')[0] + return urlparse(self._base_uri).netloc.split(":")[0] @property def port(self): """Port to connect etcd.""" - return int(urlparse(self._base_uri).netloc.split(':')[1]) + return int(urlparse(self._base_uri).netloc.split(":")[1]) @property def protocol(self): @@ -274,36 +310,41 @@ def machines(self): """ # We can't use api_execute here, or it causes a logical loop try: - uri = self._base_uri + self.version_prefix + '/machines' + uri = self._base_uri + self.version_prefix + "/machines" response = self.http.request( self._MGET, uri, headers=self._get_headers(), timeout=self.read_timeout, - redirect=self.allow_redirect) + redirect=self.allow_redirect, + ) machines = [ - node.strip() for node in - self._handle_server_response(response).data.decode('utf-8').split(',') + node.strip() + for node in self._handle_server_response(response).data.decode("utf-8").split(",") ] _log.debug("Retrieved list of machines: %s", machines) return machines - except (urllib3.exceptions.HTTPError, - HTTPException, - socket.error) as e: + except (HTTPError, HTTPException, socket.error) as e: # We can't get the list of machines, if one server is in the # machines cache, try on it - _log.error("Failed to get list of machines from %s%s: %r", - self._base_uri, self.version_prefix, e) + _log.error( + "Failed to get list of machines from %s%s: %r", + self._base_uri, + self.version_prefix, + e, + ) if self._machines_cache: self._base_uri = self._machines_cache.pop(0) _log.info("Retrying on %s", self._base_uri) # Call myself return self.machines else: - raise etcd.EtcdException("Could not get the list of servers, " - "maybe you provided the wrong " - "host(s) to connect to?") + raise etcd.EtcdException( + "Could not get the list of servers, " + "maybe you provided the wrong " + "host(s) to connect to?" + ) @property def members(self): @@ -315,14 +356,17 @@ def members(self): # Empty the members list self._members = {} try: - data = self.api_execute(self.version_prefix + '/members', - self._MGET).data.decode('utf-8') + data = self.api_execute(self.version_prefix + "/members", self._MGET).data.decode( + "utf-8" + ) res = json.loads(data) - for member in res['members']: - self._members[member['id']] = member + for member in res["members"]: + self._members[member["id"]] = member return self._members except: - raise etcd.EtcdException("Could not get the members list, maybe the cluster has gone away?") + raise etcd.EtcdException( + "Could not get the members list, maybe the cluster has gone away?" + ) @property def leader(self): @@ -334,11 +378,12 @@ def leader(self): {"id":"ce2a822cea30bfca","name":"default","peerURLs":["http://localhost:2380","http://localhost:7001"],"clientURLs":["http://127.0.0.1:4001"]} """ try: - leader = json.loads( - self.api_execute(self.version_prefix + '/stats/self', - self._MGET).data.decode('utf-8')) - return self.members[leader['leaderInfo']['leader']] + self.api_execute(self.version_prefix + "/stats/self", self._MGET).data.decode( + "utf-8" + ) + ) + return self.members[leader["leaderInfo"]["leader"]] except Exception as e: raise etcd.EtcdException("Cannot get leader data: %s" % e) @@ -356,7 +401,7 @@ def leader_stats(self): Returns: dict. the stats of the leader """ - return self._stats('leader') + return self._stats("leader") @property def store_stats(self): @@ -364,23 +409,43 @@ def store_stats(self): Returns: dict. the stats of the kv store """ - return self._stats('store') + return self._stats("store") - def _stats(self, what='self'): - """ Internal method to access the stats endpoints""" - data = self.api_execute(self.version_prefix - + '/stats/' + what, self._MGET).data.decode('utf-8') + def _stats(self, what="self"): + """Internal method to access the stats endpoints""" + data = self.api_execute(self.version_prefix + "/stats/" + what, self._MGET).data.decode( + "utf-8" + ) try: return json.loads(data) - except (TypeError,ValueError): + except (TypeError, ValueError): raise etcd.EtcdException("Cannot parse json data in the response") + @property + def version(self): + """ + Version of etcd. + """ + if not self._version: + self._set_version_info() + return self._version + + @property + def cluster_version(self): + """ + Version of the etcd cluster. + """ + if not self._cluster_version: + self._set_version_info() + + return self._cluster_version + @property def key_endpoint(self): """ REST key endpoint. """ - return self.version_prefix + '/keys' + return self.version_prefix + "/keys" def __contains__(self, key): """ @@ -396,7 +461,7 @@ def __contains__(self, key): return False def _sanitize_key(self, key): - if not key.startswith('/'): + if not key.startswith("/"): key = "/{}".format(key) return key @@ -433,23 +498,21 @@ def write(self, key, value, ttl=None, dir=False, append=False, **kwdargs): 'newValue' """ - _log.debug("Writing %s to key %s ttl=%s dir=%s append=%s", - value, key, ttl, dir, append) + _log.debug("Writing %s to key %s ttl=%s dir=%s append=%s", value, key, ttl, dir, append) key = self._sanitize_key(key) params = {} if value is not None: - params['value'] = value + params["value"] = value if ttl is not None: - params['ttl'] = ttl + params["ttl"] = ttl if dir: if value: - raise etcd.EtcdException( - 'Cannot create a directory with a value') - params['dir'] = "true" + raise etcd.EtcdException("Cannot create a directory with a value") + params["dir"] = "true" - for (k, v) in kwdargs.items(): + for k, v in kwdargs.items(): if k in self._comparison_conditions: if type(v) == bool: params[k] = v and "true" or "false" @@ -457,8 +520,8 @@ def write(self, key, value, ttl=None, dir=False, append=False, **kwdargs): params[k] = v method = append and self._MPOST or self._MPUT - if '_endpoint' in kwdargs: - path = kwdargs['_endpoint'] + key + if "_endpoint" in kwdargs: + path = kwdargs["_endpoint"] + key else: path = self.key_endpoint + key @@ -484,7 +547,7 @@ def refresh(self, key, ttl, **kwdargs): Other parameters modifying the write method are accepted as `EtcdClient.write`. """ # overwrite kwdargs' prevExist - kwdargs['prevExist'] = True + kwdargs["prevExist"] = True return self.write(key=key, value=None, ttl=ttl, refresh=True, **kwdargs) def update(self, obj): @@ -501,15 +564,11 @@ def update(self, obj): """ _log.debug("Updating %s to %s.", obj.key, obj.value) - kwdargs = { - 'dir': obj.dir, - 'ttl': obj.ttl, - 'prevExist': True - } + kwdargs = {"dir": obj.dir, "ttl": obj.ttl, "prevExist": True} if not obj.dir: # prevIndex on a dir causes a 'not a file' error. d'oh! - kwdargs['prevIndex'] = obj.modifiedIndex + kwdargs["prevIndex"] = obj.modifiedIndex return self.write(obj.key, obj.value, **kwdargs) def read(self, key, **kwdargs): @@ -548,18 +607,18 @@ def read(self, key, **kwdargs): key = self._sanitize_key(key) params = {} - for (k, v) in kwdargs.items(): + for k, v in kwdargs.items(): if k in self._read_options: if type(v) == bool: params[k] = v and "true" or "false" elif v is not None: params[k] = v - timeout = kwdargs.get('timeout', None) + timeout = kwdargs.get("timeout", None) response = self.api_execute( - self.key_endpoint + key, self._MGET, params=params, - timeout=timeout) + self.key_endpoint + key, self._MGET, params=params, timeout=timeout + ) return self._result_from_response(response) def delete(self, key, recursive=None, dir=None, **kwdargs): @@ -591,23 +650,27 @@ def delete(self, key, recursive=None, dir=None, **kwdargs): '/key' """ - _log.debug("Deleting %s recursive=%s dir=%s extra args=%s", - key, recursive, dir, kwdargs) + _log.debug( + "Deleting %s recursive=%s dir=%s extra args=%s", + key, + recursive, + dir, + kwdargs, + ) key = self._sanitize_key(key) kwds = {} if recursive is not None: - kwds['recursive'] = recursive and "true" or "false" + kwds["recursive"] = recursive and "true" or "false" if dir is not None: - kwds['dir'] = dir and "true" or "false" + kwds["dir"] = dir and "true" or "false" for k in self._del_conditions: if k in kwdargs: kwds[k] = kwdargs[k] _log.debug("Calculated params = %s", kwds) - response = self.api_execute( - self.key_endpoint + key, self._MDELETE, params=kwds) + response = self.api_execute(self.key_endpoint + key, self._MDELETE, params=kwds) return self._result_from_response(response) def pop(self, key, recursive=None, dir=None, **kwdargs): @@ -719,9 +782,9 @@ def watch(self, key, index=None, timeout=None, recursive=None): client.EtcdResult Raises: - KeyValue: If the key doesn't exists. + KeyValue: If the key doesn't exist. - urllib3.exceptions.TimeoutError: If timeout is reached. + etcd.EtcdWatchTimedOut: If timeout is reached. >>> print client.watch('/key').value 'value' @@ -729,11 +792,9 @@ def watch(self, key, index=None, timeout=None, recursive=None): """ _log.debug("About to wait on key %s, index %s", key, index) if index: - return self.read(key, wait=True, waitIndex=index, timeout=timeout, - recursive=recursive) + return self.read(key, wait=True, waitIndex=index, timeout=timeout, recursive=recursive) else: - return self.read(key, wait=True, timeout=timeout, - recursive=recursive) + return self.read(key, wait=True, timeout=timeout, recursive=recursive) def eternal_watch(self, key, index=None, recursive=None): """ @@ -761,20 +822,19 @@ def eternal_watch(self, key, index=None, recursive=None): yield response def get_lock(self, *args, **kwargs): - raise NotImplementedError('Lock primitives were removed from etcd 2.0') + raise NotImplementedError("Lock primitives were removed from etcd 2.0") @property def election(self): - raise NotImplementedError('Election primitives were removed from etcd 2.0') + raise NotImplementedError("Election primitives were removed from etcd 2.0") def _result_from_response(self, response): - """ Creates an EtcdResult from json dictionary """ + """Creates an EtcdResult from json dictionary""" raw_response = response.data try: - res = json.loads(raw_response.decode('utf-8')) + res = json.loads(raw_response.decode("utf-8")) except (TypeError, ValueError, UnicodeError) as e: - raise etcd.EtcdException( - 'Server response was not valid JSON: %r' % e) + raise etcd.EtcdException("Server response was not valid JSON: %r" % e) try: r = etcd.EtcdResult(**res) if response.status == 201: @@ -782,19 +842,19 @@ def _result_from_response(self, response): r.parse_headers(response) return r except Exception as e: - raise etcd.EtcdException( - 'Unable to decode server response: %r' % e) + raise etcd.EtcdException("Unable to decode server response: %r" % e) def _next_server(self, cause=None): - """ Selects the next server in the list, refreshes the server list. """ - _log.debug("Selection next machine in cache. Available machines: %s", - self._machines_cache) + """Selects the next server in the list, refreshes the server list.""" + _log.debug( + "Selecting next machine in cache. Available machines: %s", + self._machines_cache, + ) try: mach = self._machines_cache.pop() except IndexError: _log.error("Machines cache is empty, no machines to try.") - raise etcd.EtcdConnectionFailed('No more machines in the cluster', - cause=cause) + raise etcd.EtcdConnectionFailed("No more machines in the cluster", cause=cause) else: _log.info("Selected new etcd server %s", mach) return mach @@ -802,7 +862,6 @@ def _next_server(self, cause=None): def _wrap_request(payload): @wraps(payload) def wrapper(self, path, method, params=None, timeout=None): - some_request_failed = False response = False if timeout is None: @@ -811,39 +870,34 @@ def wrapper(self, path, method, params=None, timeout=None): if timeout == 0: timeout = None - if not path.startswith('/'): - raise ValueError('Path does not start with /') + if not path.startswith("/"): + raise ValueError("Path does not start with /") while not response: + some_request_failed = False try: - response = payload(self, path, method, - params=params, timeout=timeout) + response = payload(self, path, method, params=params, timeout=timeout) # Check the cluster ID hasn't changed under us. We use # preload_content=False above so we can read the headers # before we wait for the content of a watch. - self._check_cluster_id(response) + self._check_cluster_id(response, path) # Now force the data to be preloaded in order to trigger any # IO-related errors in this method rather than when we try to # access it later. _ = response.data # urllib3 doesn't wrap all httplib exceptions and earlier versions # don't wrap socket errors either. - except (urllib3.exceptions.HTTPError, - HTTPException, socket.error) as e: - if (isinstance(params, dict) and - params.get("wait") == "true" and - isinstance(e, - urllib3.exceptions.ReadTimeoutError)): + except (HTTPError, HTTPException, socket.error) as e: + if ( + isinstance(params, dict) + and params.get("wait") == "true" + and isinstance(e, ReadTimeoutError) + ): _log.debug("Watch timed out.") - raise etcd.EtcdWatchTimedOut( - "Watch timed out: %r" % e, - cause=e - ) - _log.error("Request to server %s failed: %r", - self._base_uri, e) + raise etcd.EtcdWatchTimedOut("Watch timed out: %r" % e, cause=e) + _log.error("Request to server %s failed: %r", self._base_uri, e) if self._allow_reconnect: - _log.info("Reconnection allowed, looking for another " - "server.") + _log.info("Reconnection allowed, looking for another " "server.") # _next_server() raises EtcdException if there are no # machines left to try, breaking out of the loop. self._base_uri = self._next_server(cause=e) @@ -856,27 +910,27 @@ def wrapper(self, path, method, params=None, timeout=None): else: _log.debug("Reconnection disabled, giving up.") raise etcd.EtcdConnectionFailed( - "Connection to etcd failed due to %r" % e, - cause=e + "Connection to etcd failed due to %r" % e, cause=e ) except etcd.EtcdClusterIdChanged as e: _log.warning(e) raise except: - _log.exception("Unexpected request failure, re-raising.") + _log.debug("Unexpected request failure, re-raising.") raise if some_request_failed: if not self._use_proxies: # The cluster may have changed since last invocation self._machines_cache = self.machines - self._machines_cache.remove(self._base_uri) + self._machines_cache.remove(self._base_uri) return self._handle_server_response(response) + return wrapper @_wrap_request def api_execute(self, path, method, params=None, timeout=None): - """ Executes the query. """ + """Executes the query.""" url = self._base_uri + path if (method == self._MGET) or (method == self._MDELETE): @@ -887,7 +941,8 @@ def api_execute(self, path, method, params=None, timeout=None): fields=params, redirect=self.allow_redirect, headers=self._get_headers(), - preload_content=False) + preload_content=False, + ) elif (method == self._MPUT) or (method == self._MPOST): return self.http.request_encode_body( @@ -898,32 +953,34 @@ def api_execute(self, path, method, params=None, timeout=None): encode_multipart=False, redirect=self.allow_redirect, headers=self._get_headers(), - preload_content=False) + preload_content=False, + ) else: - raise etcd.EtcdException( - 'HTTP method {} not supported'.format(method)) + raise etcd.EtcdException("HTTP method {} not supported".format(method)) @_wrap_request def api_execute_json(self, path, method, params=None, timeout=None): url = self._base_uri + path json_payload = json.dumps(params) headers = self._get_headers() - headers['Content-Type'] = 'application/json' - return self.http.urlopen(method, - url, - body=json_payload, - timeout=timeout, - redirect=self.allow_redirect, - headers=headers, - preload_content=False) - - def _check_cluster_id(self, response): + headers["Content-Type"] = "application/json" + return self.http.urlopen( + method, + url, + body=json_payload, + timeout=timeout, + redirect=self.allow_redirect, + headers=headers, + preload_content=False, + ) + + def _check_cluster_id(self, response, path): cluster_id = response.getheader("x-etcd-cluster-id") if not cluster_id: - _log.warning("etcd response did not contain a cluster ID") + if self.version_prefix in path: + _log.warning("etcd response did not contain a cluster ID") return - id_changed = (self.expected_cluster_id and - cluster_id != self.expected_cluster_id) + id_changed = self.expected_cluster_id and cluster_id != self.expected_cluster_id # Update the ID so we only raise the exception once. old_expected_cluster_id = self.expected_cluster_id self.expected_cluster_id = cluster_id @@ -932,29 +989,29 @@ def _check_cluster_id(self, response): # time. self.http.clear() raise etcd.EtcdClusterIdChanged( - 'The UUID of the cluster changed from {} to ' - '{}.'.format(old_expected_cluster_id, cluster_id)) + "The UUID of the cluster changed from {} to " + "{}.".format(old_expected_cluster_id, cluster_id) + ) def _handle_server_response(self, response): - """ Handles the server response """ + """Handles the server response""" if response.status in [200, 201]: return response else: - resp = response.data.decode('utf-8') + resp = response.data.decode("utf-8") # throw the appropriate exception try: r = json.loads(resp) - r['status'] = response.status + r["status"] = response.status except (TypeError, ValueError): # Bad JSON, make a response locally. - r = {"message": "Bad response", - "cause": str(resp)} + r = {"message": "Bad response", "cause": str(resp)} etcd.EtcdError.handle(r) def _get_headers(self): if self.username and self.password: - credentials = ':'.join((self.username, self.password)) + credentials = ":".join((self.username, self.password)) return urllib3.make_headers(basic_auth=credentials) return {} diff --git a/src/etcd/lock.py b/src/etcd/lock.py index a21e70fc..2f790dd7 100644 --- a/src/etcd/lock.py +++ b/src/etcd/lock.py @@ -4,6 +4,7 @@ _log = logging.getLogger(__name__) + class Lock(object): """ Locking recipe for etcd, inspired by the kazoo recipe for zookeeper @@ -17,7 +18,7 @@ def __init__(self, client, lock_name): # prevent us from getting back the full path name. We prefix our # lock name with a uuid and can check for its presence on retry. self._uuid = uuid.uuid4().hex - self.path = "{}/{}".format(client.lock_prefix, lock_name) + self.path = "{}/{}".format(client.lock_prefix, lock_name) self.is_taken = False self._sequence = None _log.debug("Initiating lock for %s with uuid %s", self.path, self._uuid) @@ -30,11 +31,11 @@ def uuid(self): return self._uuid @uuid.setter - def set_uuid(self, value): + def uuid(self, value): old_uuid = self._uuid self._uuid = value if not self._find_lock(): - _log.warn("The hand-set uuid was not found, refusing") + _log.warning("The hand-set uuid was not found, refusing") self._uuid = old_uuid raise ValueError("Inexistent UUID") @@ -50,7 +51,7 @@ def is_acquired(self): self.client.read(self.lock_key) return True except etcd.EtcdKeyNotFound: - _log.warn("Lock was supposedly taken, but we cannot find it") + _log.warning("Lock was supposedly taken, but we cannot find it") self.is_taken = False return False @@ -61,6 +62,11 @@ def acquire(self, blocking=True, lock_ttl=3600, timeout=0): :param blocking Block until the lock is obtained, or timeout is reached :param lock_ttl The duration of the lock we acquired, set to None for eternal locks :param timeout The time to wait before giving up on getting a lock + + Raises: + etcd.EtcdLockExpired: If lock expired when try to acquire. + + etcd.EtcdWatchTimeOut: If timeout is reached. """ # First of all try to write, if our lock is not present. if not self._find_lock(): @@ -94,7 +100,7 @@ def __enter__(self): """ You can use the lock as a contextmanager """ - self.acquire(blocking=True, lock_ttl=0) + self.acquire(blocking=True, lock_ttl=None) return self def __exit__(self, type, value, traceback): @@ -125,7 +131,7 @@ def _acquired(self, blocking=True, timeout=0): except etcd.EtcdKeyNotFound: _log.debug("Key %s not present anymore, moving on", watch_key) return self._acquired(blocking=True, timeout=timeout) - except etcd.EtcdLockExpired as e: + except (etcd.EtcdLockExpired, etcd.EtcdWatchTimeOut) as e: raise e except etcd.EtcdException: _log.exception("Unexpected exception") @@ -134,10 +140,10 @@ def _acquired(self, blocking=True, timeout=0): def lock_key(self): if not self._sequence: raise ValueError("No sequence present.") - return self.path + '/' + str(self._sequence) + return self.path + "/" + str(self._sequence) def _set_sequence(self, key): - self._sequence = key.replace(self.path, '').lstrip('/') + self._sequence = key.replace(self.path, "").lstrip("/") def _find_lock(self): if self._sequence: @@ -158,8 +164,7 @@ def _find_lock(self): return False def _get_locker(self): - results = [res for res in - self.client.read(self.path, recursive=True).leaves] + results = [res for res in self.client.read(self.path, recursive=True).leaves] if not self._sequence: self._find_lock() l = sorted([r.key for r in results]) @@ -170,9 +175,9 @@ def _get_locker(self): _log.debug("No key before our one, we are the locker") return (l[0], None) else: - _log.debug("Locker: %s, key to watch: %s", l[0], l[i-1]) - return (l[0], next(x for x in results if x.key == l[i-1])) + _log.debug("Locker: %s, key to watch: %s", l[0], l[i - 1]) + return (l[0], next(x for x in results if x.key == l[i - 1])) except ValueError: # Something very wrong is going on, most probably # our lock has expired - raise etcd.EtcdLockExpired(u"Lock not found") + raise etcd.EtcdLockExpired("Lock not found") diff --git a/src/etcd/tests/integration/helpers.py b/src/etcd/tests/integration/helpers.py index 1f1d22bf..131e81d0 100644 --- a/src/etcd/tests/integration/helpers.py +++ b/src/etcd/tests/integration/helpers.py @@ -1,3 +1,4 @@ +import re import shutil import subprocess import tempfile @@ -10,39 +11,66 @@ class EtcdProcessHelper(object): - def __init__( - self, - base_directory, - proc_name='etcd', - port_range_start=4001, - internal_port_range_start=7001, - cluster=False, - tls=False + self, + base_directory, + proc_name="etcd", + port_range_start=4001, + internal_port_range_start=7001, + cluster=False, + tls=False, ): - self.base_directory = base_directory self.proc_name = proc_name self.port_range_start = port_range_start self.internal_port_range_start = internal_port_range_start self.processes = {} self.cluster = cluster - self.schema = 'http://' + self.schema = "http://" if tls: - self.schema = 'https://' + self.schema = "https://" + self.compat_args = self.check_compat_args() + + def check_compat_args(self): + version_re = re.compile(r"^etcd version:\s+(\d)\.(\d)", re.I) + version_data = subprocess.check_output([self.proc_name, "--version"]).decode("utf-8") + match = version_re.match(version_data) + if match is not None: + etcd_version = (int(match.group(1)), int(match.group(2))) + else: + etcd_version = (0, 0) + if etcd_version[0] < 3 or (etcd_version[0] == 3 and etcd_version[1] < 4): + return [] + else: + return ["--enable-v2=true"] def run(self, number=1, proc_args=[]): if number > 1: - initial_cluster = ",".join([ "test-node-{}={}127.0.0.1:{}".format(slot, 'http://', self.internal_port_range_start + slot) for slot in range(0, number)]) - proc_args.extend([ - '-initial-cluster', initial_cluster, - '-initial-cluster-state', 'new' - ]) + initial_cluster = ",".join( + [ + "test-node-{}={}127.0.0.1:{}".format( + slot, "http://", self.internal_port_range_start + slot + ) + for slot in range(0, number) + ] + ) + proc_args.extend( + [ + "-initial-cluster", + initial_cluster, + "-initial-cluster-state", + "new", + ] + ) else: - proc_args.extend([ - '-initial-cluster', 'test-node-0=http://127.0.0.1:{}'.format(self.internal_port_range_start), - '-initial-cluster-state', 'new' - ]) + proc_args.extend( + [ + "-initial-cluster", + "test-node-0=http://127.0.0.1:{}".format(self.internal_port_range_start), + "-initial-cluster-state", + "new", + ] + ) for i in range(0, number): self.add_one(i, proc_args) @@ -54,30 +82,36 @@ def stop(self): def add_one(self, slot, proc_args=None): log = logging.getLogger() - directory = tempfile.mkdtemp( - dir=self.base_directory, - prefix='python-etcd.%d-' % slot) - - log.debug('Created directory %s' % directory) - client = '%s127.0.0.1:%d' % (self.schema, self.port_range_start + slot) - peer = '%s127.0.0.1:%d' % ('http://', self.internal_port_range_start - + slot) + directory = tempfile.mkdtemp(dir=self.base_directory, prefix="python-etcd.%d-" % slot) + + log.debug("Created directory %s" % directory) + client = "%s127.0.0.1:%d" % (self.schema, self.port_range_start + slot) + peer = "%s127.0.0.1:%d" % ( + "http://", + self.internal_port_range_start + slot, + ) daemon_args = [ self.proc_name, - '-data-dir', directory, - '-name', 'test-node-%d' % slot, - '-initial-advertise-peer-urls', peer, - '-listen-peer-urls', peer, - '-advertise-client-urls', client, - '-listen-client-urls', client + "-data-dir", + directory, + "-name", + "test-node-%d" % slot, + "-initial-advertise-peer-urls", + peer, + "-listen-peer-urls", + peer, + "-advertise-client-urls", + client, + "-listen-client-urls", + client, ] - + daemon_args.extend(self.compat_args) if proc_args: daemon_args.extend(proc_args) daemon = subprocess.Popen(daemon_args) - log.debug('Started %d' % daemon.pid) - log.debug('Params: %s' % daemon_args) + log.debug("Started %d" % daemon.pid) + log.debug("Params: %s" % daemon_args) time.sleep(2) self.processes[slot] = (directory, daemon) @@ -86,13 +120,12 @@ def kill_one(self, slot): data_dir, process = self.processes.pop(slot) process.kill() time.sleep(2) - log.debug('Killed etcd pid:%d', process.pid) + log.debug("Killed etcd pid:%d", process.pid) shutil.rmtree(data_dir) - log.debug('Removed directory %s' % data_dir) + log.debug("Removed directory %s" % data_dir) class TestingCA(object): - @classmethod def create_test_ca_certificate(cls, cert_path, key_path, cn=None): k = crypto.PKey() @@ -103,7 +136,7 @@ def create_test_ca_certificate(cls, cert_path, key_path, cn=None): serial = uuid.uuid4().int else: md5_hash = hashlib.md5() - md5_hash.update(cn.encode('utf-8')) + md5_hash.update(cn.encode("utf-8")) serial = int(md5_hash.hexdigest(), 36) cert.get_subject().CN = cn @@ -117,31 +150,45 @@ def create_test_ca_certificate(cls, cert_path, key_path, cn=None): cert.gmtime_adj_notAfter(315360000) cert.set_issuer(cert.get_subject()) cert.set_pubkey(k) - cert.add_extensions([ - crypto.X509Extension("basicConstraints".encode('ascii'), False, - "CA:TRUE".encode('ascii')), - crypto.X509Extension("keyUsage".encode('ascii'), False, - "keyCertSign, cRLSign".encode('ascii')), - crypto.X509Extension("subjectKeyIdentifier".encode('ascii'), False, - "hash".encode('ascii'), - subject=cert), - ]) - - cert.add_extensions([ - crypto.X509Extension( - "authorityKeyIdentifier".encode('ascii'), False, - "keyid:always".encode('ascii'), issuer=cert) - ]) - - cert.sign(k, 'sha1') - - with open(cert_path, 'w') as f: - f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert) - .decode('utf-8')) - - with open(key_path, 'w') as f: - f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k) - .decode('utf-8')) + cert.add_extensions( + [ + crypto.X509Extension( + "basicConstraints".encode("ascii"), + False, + "CA:TRUE".encode("ascii"), + ), + crypto.X509Extension( + "keyUsage".encode("ascii"), + False, + "keyCertSign, cRLSign".encode("ascii"), + ), + crypto.X509Extension( + "subjectKeyIdentifier".encode("ascii"), + False, + "hash".encode("ascii"), + subject=cert, + ), + ] + ) + + cert.add_extensions( + [ + crypto.X509Extension( + "authorityKeyIdentifier".encode("ascii"), + False, + "keyid:always".encode("ascii"), + issuer=cert, + ) + ] + ) + + cert.sign(k, "sha1") + + with open(cert_path, "w") as f: + f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode("utf-8")) + + with open(key_path, "w") as f: + f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k).decode("utf-8")) return cert, k @@ -155,7 +202,7 @@ def create_test_certificate(cls, ca, ca_key, cert_path, key_path, cn=None): serial = uuid.uuid4().int else: md5_hash = hashlib.md5() - md5_hash.update(cn.encode('utf-8')) + md5_hash.update(cn.encode("utf-8")) serial = int(md5_hash.hexdigest(), 36) cert.get_subject().CN = cn @@ -165,20 +212,25 @@ def create_test_certificate(cls, ca, ca_key, cert_path, key_path, cn=None): cert.get_subject().O = "Organization" cert.get_subject().OU = "Organizational Unit" - cert.add_extensions([ - crypto.X509Extension( - "keyUsage".encode('ascii'), - False, - "nonRepudiation,digitalSignature,keyEncipherment".encode('ascii')), - crypto.X509Extension( - "extendedKeyUsage".encode('ascii'), - False, - "clientAuth,serverAuth".encode('ascii')), - crypto.X509Extension( - "subjectAltName".encode('ascii'), - False, - "IP: 127.0.0.1".encode('ascii')), - ]) + cert.add_extensions( + [ + crypto.X509Extension( + "keyUsage".encode("ascii"), + False, + "nonRepudiation,digitalSignature,keyEncipherment".encode("ascii"), + ), + crypto.X509Extension( + "extendedKeyUsage".encode("ascii"), + False, + "clientAuth,serverAuth".encode("ascii"), + ), + crypto.X509Extension( + "subjectAltName".encode("ascii"), + False, + "IP: 127.0.0.1".encode("ascii"), + ), + ] + ) cert.gmtime_adj_notBefore(0) cert.gmtime_adj_notAfter(315360000) @@ -186,12 +238,10 @@ def create_test_certificate(cls, ca, ca_key, cert_path, key_path, cn=None): cert.set_pubkey(k) cert.set_serial_number(serial) - cert.sign(ca_key, 'sha1') + cert.sign(ca_key, "sha1") - with open(cert_path, 'w') as f: - f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert) - .decode('utf-8')) + with open(cert_path, "w") as f: + f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode("utf-8")) - with open(key_path, 'w') as f: - f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k) - .decode('utf-8')) + with open(key_path, "w") as f: + f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k).decode("utf-8")) diff --git a/src/etcd/tests/integration/test_simple.py b/src/etcd/tests/integration/test_simple.py index 4baeadeb..631a14b6 100644 --- a/src/etcd/tests/integration/test_simple.py +++ b/src/etcd/tests/integration/test_simple.py @@ -11,9 +11,6 @@ import etcd from . import helpers -from nose.tools import nottest - - log = logging.getLogger() @@ -23,12 +20,13 @@ class EtcdIntegrationTest(unittest.TestCase): @classmethod def setUpClass(cls): program = cls._get_exe() - cls.directory = tempfile.mkdtemp(prefix='python-etcd') + cls.directory = tempfile.mkdtemp(prefix="python-etcd") cls.processHelper = helpers.EtcdProcessHelper( cls.directory, proc_name=program, port_range_start=6001, - internal_port_range_start=8001) + internal_port_range_start=8001, + ) cls.processHelper.run(number=cls.cl_size) cls.client = etcd.Client(port=6001) @@ -43,7 +41,7 @@ def _is_exe(cls, fpath): @classmethod def _get_exe(cls): - PROGRAM = 'etcd' + PROGRAM = "etcd" program_path = None @@ -55,196 +53,201 @@ def _get_exe(cls): break if not program_path: - raise Exception('etcd not in path!!') + raise Exception("etcd not in path!!") return program_path class TestSimple(EtcdIntegrationTest): - def test_machines(self): - """ INTEGRATION: retrieve machines """ - self.assertEquals(self.client.machines[0], 'http://127.0.0.1:6001') + """INTEGRATION: retrieve machines""" + self.assertEquals(self.client.machines[0], "http://127.0.0.1:6001") def test_leader(self): - """ INTEGRATION: retrieve leader """ - self.assertIn(self.client.leader['clientURLs'][0], - ['http://127.0.0.1:6001','http://127.0.0.1:6002','http://127.0.0.1:6003']) + """INTEGRATION: retrieve leader""" + self.assertIn( + self.client.leader["clientURLs"][0], + ["http://127.0.0.1:6001", "http://127.0.0.1:6002", "http://127.0.0.1:6003"], + ) def test_get_set_delete(self): - """ INTEGRATION: set a new value """ + """INTEGRATION: set a new value""" try: - get_result = self.client.get('/test_set') + get_result = self.client.get("/test_set") assert False except etcd.EtcdKeyNotFound as e: pass - self.assertFalse('/test_set' in self.client) + self.assertFalse("/test_set" in self.client) - set_result = self.client.set('/test_set', 'test-key') - self.assertEquals('set', set_result.action.lower()) - self.assertEquals('/test_set', set_result.key) - self.assertEquals('test-key', set_result.value) + set_result = self.client.set("/test_set", "test-key") + self.assertEquals("set", set_result.action.lower()) + self.assertEquals("/test_set", set_result.key) + self.assertEquals("test-key", set_result.value) - self.assertTrue('/test_set' in self.client) + self.assertTrue("/test_set" in self.client) - get_result = self.client.get('/test_set') - self.assertEquals('get', get_result.action.lower()) - self.assertEquals('/test_set', get_result.key) - self.assertEquals('test-key', get_result.value) + get_result = self.client.get("/test_set") + self.assertEquals("get", get_result.action.lower()) + self.assertEquals("/test_set", get_result.key) + self.assertEquals("test-key", get_result.value) - delete_result = self.client.delete('/test_set') - self.assertEquals('delete', delete_result.action.lower()) - self.assertEquals('/test_set', delete_result.key) + delete_result = self.client.delete("/test_set") + self.assertEquals("delete", delete_result.action.lower()) + self.assertEquals("/test_set", delete_result.key) - self.assertFalse('/test_set' in self.client) + self.assertFalse("/test_set" in self.client) try: - get_result = self.client.get('/test_set') + get_result = self.client.get("/test_set") assert False except etcd.EtcdKeyNotFound as e: pass def test_update(self): """INTEGRATION: update a value""" - self.client.set('/foo', 3) - c = self.client.get('/foo') + self.client.set("/foo", 3) + c = self.client.get("/foo") c.value = int(c.value) + 3 self.client.update(c) - newres = self.client.get('/foo') - self.assertEquals(newres.value, u'6') + newres = self.client.get("/foo") + self.assertEquals(newres.value, "6") self.assertRaises(ValueError, self.client.update, c) def test_retrieve_subkeys(self): - """ INTEGRATION: retrieve multiple subkeys """ - set_result = self.client.write('/subtree/test_set', 'test-key1') - set_result = self.client.write('/subtree/test_set1', 'test-key2') - set_result = self.client.write('/subtree/test_set2', 'test-key3') - get_result = self.client.read('/subtree', recursive=True) + """INTEGRATION: retrieve multiple subkeys""" + set_result = self.client.write("/subtree/test_set", "test-key1") + set_result = self.client.write("/subtree/test_set1", "test-key2") + set_result = self.client.write("/subtree/test_set2", "test-key3") + get_result = self.client.read("/subtree", recursive=True) result = [subkey.value for subkey in get_result.leaves] - self.assertEquals(['test-key1', 'test-key2', 'test-key3'].sort(), result.sort()) + self.assertEquals(["test-key1", "test-key2", "test-key3"].sort(), result.sort()) def test_directory_ttl_update(self): - """ INTEGRATION: should be able to update a dir TTL """ - self.client.write('/dir', None, dir=True, ttl=30) - res = self.client.write('/dir', None, dir=True, ttl=31, prevExist=True) + """INTEGRATION: should be able to update a dir TTL""" + self.client.write("/dir", None, dir=True, ttl=30) + res = self.client.write("/dir", None, dir=True, ttl=31, prevExist=True) self.assertEquals(res.ttl, 31) - res = self.client.get('/dir') + res = self.client.get("/dir") res.ttl = 120 new_res = self.client.update(res) self.assertEquals(new_res.ttl, 120) - class TestErrors(EtcdIntegrationTest): - def test_is_not_a_file(self): - """ INTEGRATION: try to write value to an existing directory """ + """INTEGRATION: try to write value to an existing directory""" - self.client.set('/directory/test-key', 'test-value') - self.assertRaises(etcd.EtcdNotFile, self.client.set, '/directory', 'test-value') + self.client.set("/directory/test-key", "test-value") + self.assertRaises(etcd.EtcdNotFile, self.client.set, "/directory", "test-value") def test_test_and_set(self): - """ INTEGRATION: try test_and_set operation """ + """INTEGRATION: try test_and_set operation""" - set_result = self.client.set('/test-key', 'old-test-value') + set_result = self.client.set("/test-key", "old-test-value") - set_result = self.client.test_and_set( - '/test-key', - 'test-value', - 'old-test-value') + set_result = self.client.test_and_set("/test-key", "test-value", "old-test-value") - self.assertRaises(ValueError, self.client.test_and_set, '/test-key', 'new-value', 'old-test-value') + self.assertRaises( + ValueError, + self.client.test_and_set, + "/test-key", + "new-value", + "old-test-value", + ) def test_creating_already_existing_directory(self): - """ INTEGRATION: creating an already existing directory without - `prevExist=True` should fail """ - self.client.write('/mydir', None, dir=True) + """INTEGRATION: creating an already existing directory without + `prevExist=True` should fail""" + self.client.write("/mydir", None, dir=True) - self.assertRaises(etcd.EtcdNotFile, self.client.write, '/mydir', None, dir=True) - self.assertRaises(etcd.EtcdAlreadyExist, self.client.write, '/mydir', None, dir=True, prevExist=False) + self.assertRaises(etcd.EtcdNotFile, self.client.write, "/mydir", None, dir=True) + self.assertRaises( + etcd.EtcdAlreadyExist, + self.client.write, + "/mydir", + None, + dir=True, + prevExist=False, + ) class TestClusterFunctions(EtcdIntegrationTest): - @classmethod def setUpClass(cls): program = cls._get_exe() - cls.directory = tempfile.mkdtemp(prefix='python-etcd') + cls.directory = tempfile.mkdtemp(prefix="python-etcd") cls.processHelper = helpers.EtcdProcessHelper( cls.directory, proc_name=program, port_range_start=6001, internal_port_range_start=8001, - cluster=True) + cluster=True, + ) def test_reconnect(self): - """ INTEGRATION: get key after the server we're connected fails. """ + """INTEGRATION: get key after the server we're connected fails.""" self.processHelper.stop() self.processHelper.run(number=3) self.client = etcd.Client(port=6001, allow_reconnect=True) - set_result = self.client.set('/test_set', 'test-key1') - get_result = self.client.get('/test_set') + set_result = self.client.set("/test_set", "test-key1") + get_result = self.client.get("/test_set") - self.assertEquals('test-key1', get_result.value) + self.assertEquals("test-key1", get_result.value) self.processHelper.kill_one(0) - get_result = self.client.get('/test_set') - self.assertEquals('test-key1', get_result.value) + get_result = self.client.get("/test_set") + self.assertEquals("test-key1", get_result.value) def test_reconnect_with_several_hosts_passed(self): - """ INTEGRATION: receive several hosts at connection setup. """ + """INTEGRATION: receive several hosts at connection setup.""" self.processHelper.stop() self.processHelper.run(number=3) self.client = etcd.Client( - host=( - ('127.0.0.1', 6004), - ('127.0.0.1', 6001)), - allow_reconnect=True) - set_result = self.client.set('/test_set', 'test-key1') - get_result = self.client.get('/test_set') + host=(("127.0.0.1", 6004), ("127.0.0.1", 6001)), allow_reconnect=True + ) + set_result = self.client.set("/test_set", "test-key1") + get_result = self.client.get("/test_set") - self.assertEquals('test-key1', get_result.value) + self.assertEquals("test-key1", get_result.value) self.processHelper.kill_one(0) - get_result = self.client.get('/test_set') - self.assertEquals('test-key1', get_result.value) + get_result = self.client.get("/test_set") + self.assertEquals("test-key1", get_result.value) def test_reconnect_not_allowed(self): - """ INTEGRATION: fail on server kill if not allow_reconnect """ + """INTEGRATION: fail on server kill if not allow_reconnect""" self.processHelper.stop() self.processHelper.run(number=3) self.client = etcd.Client(port=6001, allow_reconnect=False) self.processHelper.kill_one(0) - self.assertRaises(etcd.EtcdConnectionFailed, self.client.get, - '/test_set') + self.assertRaises(etcd.EtcdConnectionFailed, self.client.get, "/test_set") def test_reconnet_fails(self): - """ INTEGRATION: fails to reconnect if no available machines """ + """INTEGRATION: fails to reconnect if no available machines""" self.processHelper.stop() # Start with three instances (0, 1, 2) self.processHelper.run(number=3) # Connect to instance 0 self.client = etcd.Client(port=6001, allow_reconnect=True) - set_result = self.client.set('/test_set', 'test-key1') + set_result = self.client.set("/test_set", "test-key1") - get_result = self.client.get('/test_set') - self.assertEquals('test-key1', get_result.value) + get_result = self.client.get("/test_set") + self.assertEquals("test-key1", get_result.value) self.processHelper.kill_one(2) self.processHelper.kill_one(1) self.processHelper.kill_one(0) - self.assertRaises(etcd.EtcdException, self.client.get, '/test_set') + self.assertRaises(etcd.EtcdException, self.client.get, "/test_set") class TestWatch(EtcdIntegrationTest): - def test_watch(self): - """ INTEGRATION: Receive a watch event from other process """ + """INTEGRATION: Receive a watch event from other process""" - set_result = self.client.set('/test-key', 'test-value') + set_result = self.client.set("/test-key", "test-value") queue = multiprocessing.Queue() @@ -257,10 +260,14 @@ def watch_value(key, queue): queue.put(c.watch(key).value) changer = multiprocessing.Process( - target=change_value, args=('/test-key', 'new-test-value',)) + target=change_value, + args=( + "/test-key", + "new-test-value", + ), + ) - watcher = multiprocessing.Process( - target=watch_value, args=('/test-key', queue)) + watcher = multiprocessing.Process(target=watch_value, args=("/test-key", queue)) watcher.start() time.sleep(1) @@ -271,16 +278,16 @@ def watch_value(key, queue): watcher.join(timeout=5) changer.join(timeout=5) - assert value == 'new-test-value' + assert value == "new-test-value" def test_watch_indexed(self): - """ INTEGRATION: Receive a watch event from other process, indexed """ + """INTEGRATION: Receive a watch event from other process, indexed""" - set_result = self.client.set('/test-key', 'test-value') - set_result = self.client.set('/test-key', 'test-value0') + set_result = self.client.set("/test-key", "test-value") + set_result = self.client.set("/test-key", "test-value0") original_index = int(set_result.modifiedIndex) - set_result = self.client.set('/test-key', 'test-value1') - set_result = self.client.set('/test-key', 'test-value2') + set_result = self.client.set("/test-key", "test-value1") + set_result = self.client.set("/test-key", "test-value2") queue = multiprocessing.Queue() @@ -295,10 +302,16 @@ def watch_value(key, index, queue): queue.put(c.watch(key, index=index + i).value) proc = multiprocessing.Process( - target=change_value, args=('/test-key', 'test-value3',)) + target=change_value, + args=( + "/test-key", + "test-value3", + ), + ) watcher = multiprocessing.Process( - target=watch_value, args=('/test-key', original_index, queue)) + target=watch_value, args=("/test-key", original_index, queue) + ) watcher.start() time.sleep(0.5) @@ -308,15 +321,15 @@ def watch_value(key, index, queue): for i in range(0, 3): value = queue.get() log.debug("index: %d: %s" % (i, value)) - self.assertEquals('test-value%d' % i, value) + self.assertEquals("test-value%d" % i, value) watcher.join(timeout=5) proc.join(timeout=5) def test_watch_generator(self): - """ INTEGRATION: Receive a watch event from other process (gen) """ + """INTEGRATION: Receive a watch event from other process (gen)""" - set_result = self.client.set('/test-key', 'test-value') + set_result = self.client.set("/test-key", "test-value") queue = multiprocessing.Queue() @@ -324,7 +337,7 @@ def change_value(key): time.sleep(0.5) c = etcd.Client(port=6001) for i in range(0, 3): - c.set(key, 'test-value%d' % i) + c.set(key, "test-value%d" % i) c.get(key) def watch_value(key, queue): @@ -333,16 +346,14 @@ def watch_value(key, queue): event = next(c.eternal_watch(key)).value queue.put(event) - changer = multiprocessing.Process( - target=change_value, args=('/test-key',)) + changer = multiprocessing.Process(target=change_value, args=("/test-key",)) - watcher = multiprocessing.Process( - target=watch_value, args=('/test-key', queue)) + watcher = multiprocessing.Process(target=watch_value, args=("/test-key", queue)) watcher.start() changer.start() - values = ['test-value0', 'test-value1', 'test-value2'] + values = ["test-value0", "test-value1", "test-value2"] for i in range(0, 1): value = queue.get() log.debug("index: %d: %s" % (i, value)) @@ -352,13 +363,13 @@ def watch_value(key, queue): changer.join(timeout=5) def test_watch_indexed_generator(self): - """ INTEGRATION: Receive a watch event from other process, ixd, (2) """ + """INTEGRATION: Receive a watch event from other process, ixd, (2)""" - set_result = self.client.set('/test-key', 'test-value') - set_result = self.client.set('/test-key', 'test-value0') + set_result = self.client.set("/test-key", "test-value") + set_result = self.client.set("/test-key", "test-value0") original_index = int(set_result.modifiedIndex) - set_result = self.client.set('/test-key', 'test-value1') - set_result = self.client.set('/test-key', 'test-value2') + set_result = self.client.set("/test-key", "test-value1") + set_result = self.client.set("/test-key", "test-value2") queue = multiprocessing.Queue() @@ -373,10 +384,16 @@ def watch_value(key, index, queue): queue.put(next(iterevents).value) proc = multiprocessing.Process( - target=change_value, args=('/test-key', 'test-value3',)) + target=change_value, + args=( + "/test-key", + "test-value3", + ), + ) watcher = multiprocessing.Process( - target=watch_value, args=('/test-key', original_index, queue)) + target=watch_value, args=("/test-key", original_index, queue) + ) watcher.start() time.sleep(0.5) @@ -385,7 +402,7 @@ def watch_value(key, index, queue): for i in range(0, 3): value = queue.get() log.debug("index: %d: %s" % (i, value)) - self.assertEquals('test-value%d' % i, value) + self.assertEquals("test-value%d" % i, value) watcher.join(timeout=5) proc.join(timeout=5) diff --git a/src/etcd/tests/integration/test_ssl.py b/src/etcd/tests/integration/test_ssl.py index 6ba6a3ad..ce1b0c97 100644 --- a/src/etcd/tests/integration/test_ssl.py +++ b/src/etcd/tests/integration/test_ssl.py @@ -6,175 +6,169 @@ import multiprocessing import tempfile +import pytest import urllib3 import etcd from . import helpers from . import test_simple -from nose.tools import nottest log = logging.getLogger() -class TestEncryptedAccess(test_simple.EtcdIntegrationTest): +class TestEncryptedAccess(test_simple.EtcdIntegrationTest): @classmethod def setUpClass(cls): program = cls._get_exe() - cls.directory = tempfile.mkdtemp(prefix='python-etcd') + cls.directory = tempfile.mkdtemp(prefix="python-etcd") - cls.ca_cert_path = os.path.join(cls.directory, 'ca.crt') - ca_key_path = os.path.join(cls.directory, 'ca.key') + cls.ca_cert_path = os.path.join(cls.directory, "ca.crt") + ca_key_path = os.path.join(cls.directory, "ca.key") - cls.ca2_cert_path = os.path.join(cls.directory, 'ca2.crt') - ca2_key_path = os.path.join(cls.directory, 'ca2.key') + cls.ca2_cert_path = os.path.join(cls.directory, "ca2.crt") + ca2_key_path = os.path.join(cls.directory, "ca2.key") - server_cert_path = os.path.join(cls.directory, 'server.crt') - server_key_path = os.path.join(cls.directory, 'server.key') + server_cert_path = os.path.join(cls.directory, "server.crt") + server_key_path = os.path.join(cls.directory, "server.key") ca, ca_key = helpers.TestingCA.create_test_ca_certificate( - cls.ca_cert_path, ca_key_path, 'TESTCA') + cls.ca_cert_path, ca_key_path, "TESTCA" + ) ca2, ca2_key = helpers.TestingCA.create_test_ca_certificate( - cls.ca2_cert_path, ca2_key_path, 'TESTCA2') + cls.ca2_cert_path, ca2_key_path, "TESTCA2" + ) helpers.TestingCA.create_test_certificate( - ca, ca_key, server_cert_path, server_key_path, '127.0.0.1') + ca, ca_key, server_cert_path, server_key_path, "127.0.0.1" + ) cls.processHelper = helpers.EtcdProcessHelper( cls.directory, proc_name=program, port_range_start=6001, internal_port_range_start=8001, - tls=True + tls=True, ) - cls.processHelper.run(number=3, - proc_args=[ - '-cert-file=%s' % server_cert_path, - '-key-file=%s' % server_key_path - ]) + cls.processHelper.run( + number=3, + proc_args=[ + "-cert-file=%s" % server_cert_path, + "-key-file=%s" % server_key_path, + ], + ) def test_get_set_unauthenticated(self): - """ INTEGRATION: set/get a new value unauthenticated (http->https) """ + """INTEGRATION: set/get a new value unauthenticated (http->https)""" client = etcd.Client(port=6001) # Since python 3 raises a MaxRetryError here, this gets caught in # different code blocks in python 2 and python 3, thus messages are # different. Python 3 does the right thing(TM), for the record - self.assertRaises( - etcd.EtcdException, client.set, '/test_set', 'test-key') + self.assertRaises(etcd.EtcdException, client.set, "/test_set", "test-key") - self.assertRaises(etcd.EtcdException, client.get, '/test_set') + self.assertRaises(etcd.EtcdException, client.get, "/test_set") - @nottest def test_get_set_unauthenticated_missing_ca(self): - """ INTEGRATION: try unauthenticated w/out validation (https->https)""" + """INTEGRATION: try unauthenticated w/out validation (https->https)""" # This doesn't work for now and will need further inspection - client = etcd.Client(protocol='https', port=6001) - set_result = client.set('/test_set', 'test-key') - get_result = client.get('/test_set') - + client = etcd.Client(protocol="https", port=6001) + set_result = client.set("/test_set", "test-key") + get_result = client.get("/test_set") def test_get_set_unauthenticated_with_ca(self): - """ INTEGRATION: try unauthenticated with validation (https->https)""" - client = etcd.Client( - protocol='https', port=6001, ca_cert=self.ca2_cert_path) + """INTEGRATION: try unauthenticated with validation (https->https)""" + client = etcd.Client(protocol="https", port=6001, ca_cert=self.ca2_cert_path) - self.assertRaises(etcd.EtcdConnectionFailed, client.set, '/test-set', 'test-key') - self.assertRaises(etcd.EtcdConnectionFailed, client.get, '/test-set') + self.assertRaises(etcd.EtcdConnectionFailed, client.set, "/test-set", "test-key") + self.assertRaises(etcd.EtcdConnectionFailed, client.get, "/test-set") def test_get_set_authenticated(self): - """ INTEGRATION: set/get a new value authenticated """ + """INTEGRATION: set/get a new value authenticated""" - client = etcd.Client( - port=6001, protocol='https', ca_cert=self.ca_cert_path) + client = etcd.Client(port=6001, protocol="https") - set_result = client.set('/test_set', 'test-key') - get_result = client.get('/test_set') + set_result = client.set("/test_set", "test-key") + get_result = client.get("/test_set") class TestClientAuthenticatedAccess(test_simple.EtcdIntegrationTest): - @classmethod def setUpClass(cls): program = cls._get_exe() - cls.directory = tempfile.mkdtemp(prefix='python-etcd') + cls.directory = tempfile.mkdtemp(prefix="python-etcd") - cls.ca_cert_path = os.path.join(cls.directory, 'ca.crt') - ca_key_path = os.path.join(cls.directory, 'ca.key') + cls.ca_cert_path = os.path.join(cls.directory, "ca.crt") + ca_key_path = os.path.join(cls.directory, "ca.key") - server_cert_path = os.path.join(cls.directory, 'server.crt') - server_key_path = os.path.join(cls.directory, 'server.key') + server_cert_path = os.path.join(cls.directory, "server.crt") + server_key_path = os.path.join(cls.directory, "server.key") - cls.client_cert_path = os.path.join(cls.directory, 'client.crt') - cls.client_key_path = os.path.join(cls.directory, 'client.key') + cls.client_cert_path = os.path.join(cls.directory, "client.crt") + cls.client_key_path = os.path.join(cls.directory, "client.key") - cls.client_all_cert = os.path.join(cls.directory, 'client-all.crt') + cls.client_all_cert = os.path.join(cls.directory, "client-all.crt") - ca, ca_key = helpers.TestingCA.create_test_ca_certificate( - cls.ca_cert_path, ca_key_path) + ca, ca_key = helpers.TestingCA.create_test_ca_certificate(cls.ca_cert_path, ca_key_path) helpers.TestingCA.create_test_certificate( - ca, ca_key, server_cert_path, server_key_path, '127.0.0.1') + ca, ca_key, server_cert_path, server_key_path, "127.0.0.1" + ) helpers.TestingCA.create_test_certificate( - ca, - ca_key, - cls.client_cert_path, - cls.client_key_path) + ca, ca_key, cls.client_cert_path, cls.client_key_path + ) cls.processHelper = helpers.EtcdProcessHelper( cls.directory, proc_name=program, port_range_start=6001, internal_port_range_start=8001, - tls=True + tls=True, ) - with open(cls.client_all_cert, 'w') as f: - with open(cls.client_key_path, 'r') as g: + with open(cls.client_all_cert, "w") as f: + with open(cls.client_key_path, "r") as g: f.write(g.read()) - with open(cls.client_cert_path, 'r') as g: + with open(cls.client_cert_path, "r") as g: f.write(g.read()) - cls.processHelper.run(number=3, - proc_args=[ - '-cert-file=%s' % server_cert_path, - '-key-file=%s' % server_key_path, - '-ca-file=%s' % cls.ca_cert_path, - ]) - + cls.processHelper.run( + number=3, + proc_args=[ + "-cert-file=%s" % server_cert_path, + "-key-file=%s" % server_key_path, + "-trusted-ca-file", + cls.ca_cert_path, + ], + ) def test_get_set_unauthenticated(self): - """ INTEGRATION: set/get a new value unauthenticated (http->https) """ + """INTEGRATION: set/get a new value unauthenticated (http->https)""" client = etcd.Client(port=6001) # See above for the reason of this change - self.assertRaises( - etcd.EtcdException, client.set, '/test_set', 'test-key') - self.assertRaises(etcd.EtcdException, client.get, '/test_set') + self.assertRaises(etcd.EtcdException, client.set, "/test_set", "test-key") + self.assertRaises(etcd.EtcdException, client.get, "/test_set") - @nottest + @pytest.mark.skip(reason="We need non SHA1-signed certs and I won't implement it now.") def test_get_set_authenticated(self): - """ INTEGRATION: connecting to server with mutual auth """ - # This gives an unexplicable ssl error, as connecting to the same - # Etcd cluster where this fails with the exact same code this - # doesn't fail + """INTEGRATION: connecting to server with mutual auth""" client = etcd.Client( port=6001, - protocol='https', + protocol="https", cert=self.client_all_cert, - ca_cert=self.ca_cert_path ) - set_result = client.set('/test_set', 'test-key') - self.assertEquals(u'set', set_result.action.lower()) - self.assertEquals(u'/test_set', set_result.key) - self.assertEquals(u'test-key', set_result.value) - get_result = client.get('/test_set') - self.assertEquals('get', get_result.action.lower()) - self.assertEquals('/test_set', get_result.key) - self.assertEquals('test-key', get_result.value) + set_result = client.set("/test_set", "test-key") + self.assertEquals("set", set_result.action.lower()) + self.assertEquals("/test_set", set_result.key) + self.assertEquals("test-key", set_result.value) + get_result = client.get("/test_set") + self.assertEquals("get", get_result.action.lower()) + self.assertEquals("/test_set", get_result.key) + self.assertEquals("test-key", get_result.value) diff --git a/src/etcd/tests/test_auth.py b/src/etcd/tests/test_auth.py index fc6ce705..f122882f 100644 --- a/src/etcd/tests/test_auth.py +++ b/src/etcd/tests/test_auth.py @@ -8,18 +8,17 @@ class TestEtcdAuthBase(EtcdIntegrationTest): def setUp(self): # Sets up the root user, toggles auth - u = auth.EtcdUser(self.client, 'root') - u.password = 'testpass' + u = auth.EtcdUser(self.client, "root") + u.password = "testpass" u.write() - self.client = etcd.Client(port=6001, username='root', - password='testpass') + self.client = etcd.Client(port=6001, username="root", password="testpass") self.unauth_client = etcd.Client(port=6001) a = auth.Auth(self.client) a.active = True def tearDown(self): - u = auth.EtcdUser(self.client, 'test_user') - r = auth.EtcdRole(self.client, 'test_role') + u = auth.EtcdUser(self.client, "test_user") + r = auth.EtcdRole(self.client, "test_role") try: u.delete() except: @@ -34,11 +33,11 @@ def tearDown(self): class EtcdUserTest(TestEtcdAuthBase): def test_names(self): - u = auth.EtcdUser(self.client, 'test_user') - self.assertEquals(u.names, ['root']) + u = auth.EtcdUser(self.client, "test_user") + self.assertEquals(u.names, ["root"]) def test_read(self): - u = auth.EtcdUser(self.client, 'root') + u = auth.EtcdUser(self.client, "root") # Reading an existing user succeeds try: u.read() @@ -46,32 +45,31 @@ def test_read(self): self.fail("reading the root user raised an exception") # roles for said user are fetched - self.assertEquals(u.roles, set(['root'])) + self.assertEquals(u.roles, set(["root"])) # The user is correctly rendered out - self.assertEquals(u._to_net(), [{'user': 'root', 'password': None, - 'roles': ['root']}]) + self.assertEquals(u._to_net(), [{"user": "root", "password": None, "roles": ["root"]}]) # An inexistent user raises the appropriate exception - u = auth.EtcdUser(self.client, 'user.does.not.exist') + u = auth.EtcdUser(self.client, "user.does.not.exist") self.assertRaises(etcd.EtcdKeyNotFound, u.read) # Reading with an unauthenticated client raises an exception - u = auth.EtcdUser(self.unauth_client, 'root') + u = auth.EtcdUser(self.unauth_client, "root") self.assertRaises(etcd.EtcdInsufficientPermissions, u.read) # Generic errors are caught c = etcd.Client(port=9999) - u = auth.EtcdUser(c, 'root') + u = auth.EtcdUser(c, "root") self.assertRaises(etcd.EtcdException, u.read) def test_write_and_delete(self): # Create an user - u = auth.EtcdUser(self.client, 'test_user') - u.roles.add('guest') - u.roles.add('root') + u = auth.EtcdUser(self.client, "test_user") + u.roles.add("guest") + u.roles.add("root") # directly from my suitcase - u.password = '123456' + u.password = "123456" try: u.write() except: @@ -81,31 +79,34 @@ def test_write_and_delete(self): u.read() # Verify we can log in as this user and access the auth (it has the # root role) - cl = etcd.Client(port=6001, username='test_user', - password='123456') - ul = auth.EtcdUser(cl, 'root') + cl = etcd.Client(port=6001, username="test_user", password="123456") + ul = auth.EtcdUser(cl, "root") try: ul.read() except etcd.EtcdInsufficientPermissions: self.fail("Reading auth with the new user is not possible") self.assertEquals(u.name, "test_user") - self.assertEquals(u.roles, set(['guest', 'root'])) + self.assertEquals(u.roles, set(["guest", "root"])) # set roles as a list, it works! - u.roles = ['guest', 'test_group'] + u.roles = ["guest", "test_group"] + # We need this or the new API will return an internal error + r = auth.EtcdRole(self.client, "test_group") + r.acls = {"*": "R", "/test/*": "RW"} + r.write() try: u.write() except: self.fail("updating a user you previously created fails") u.read() - self.assertIn('test_group', u.roles) + self.assertIn("test_group", u.roles) # Unauthorized access is properly handled - ua = auth.EtcdUser(self.unauth_client, 'test_user') + ua = auth.EtcdUser(self.unauth_client, "test_user") self.assertRaises(etcd.EtcdInsufficientPermissions, ua.write) # now let's test deletion - du = auth.EtcdUser(self.client, 'user.does.not.exist') + du = auth.EtcdUser(self.client, "user.does.not.exist") self.assertRaises(etcd.EtcdKeyNotFound, du.delete) # Delete test_user @@ -117,39 +118,45 @@ def test_write_and_delete(self): class EtcdRoleTest(TestEtcdAuthBase): def test_names(self): - r = auth.EtcdRole(self.client, 'guest') - self.assertListEqual(r.names, [u'guest', u'root']) + r = auth.EtcdRole(self.client, "guest") + self.assertListEqual(r.names, ["guest", "root"]) def test_read(self): - r = auth.EtcdRole(self.client, 'guest') + r = auth.EtcdRole(self.client, "guest") try: r.read() except: - self.fail('Reading an existing role failed') - - self.assertEquals(r.acls, {'*': 'RW'}) + self.fail("Reading an existing role failed") + + # XXX The ACL path result changed from '*' to '/*' at some point + # between etcd-2.2.2 and 2.2.5. They're equivalent so allow + # for both. + if "/*" in r.acls: + self.assertEquals(r.acls, {"/*": "RW"}) + else: + self.assertEquals(r.acls, {"*": "RW"}) # We can actually skip most other read tests as they are common # with EtcdUser def test_write_and_delete(self): - r = auth.EtcdRole(self.client, 'test_role') - r.acls = {'*': 'R', '/test/*': 'RW'} + r = auth.EtcdRole(self.client, "test_role") + r.acls = {"*": "R", "/test/*": "RW"} try: r.write() except: self.fail("Writing a simple groups should not fail") - r1 = auth.EtcdRole(self.client, 'test_role') + r1 = auth.EtcdRole(self.client, "test_role") r1.read() self.assertEquals(r1.acls, r.acls) - r.revoke('/test/*', 'W') + r.revoke("/test/*", "W") r.write() r1.read() - self.assertEquals(r1.acls, {'*': 'R', '/test/*': 'R'}) - r.grant('/pub/*', 'RW') + self.assertEquals(r1.acls, {"*": "R", "/test/*": "R"}) + r.grant("/pub/*", "RW") r.write() r1.read() - self.assertEquals(r1.acls['/pub/*'], 'RW') + self.assertEquals(r1.acls["/pub/*"], "RW") # All other exceptions are tested by the user tests r1.name = None self.assertRaises(etcd.EtcdException, r1.write) diff --git a/src/etcd/tests/unit/__init__.py b/src/etcd/tests/unit/__init__.py index 9360b6bb..a1b95c44 100644 --- a/src/etcd/tests/unit/__init__.py +++ b/src/etcd/tests/unit/__init__.py @@ -2,6 +2,7 @@ import unittest import urllib3 import json + try: import mock except ImportError: @@ -9,15 +10,14 @@ class TestClientApiBase(unittest.TestCase): - def setUp(self): self.client = etcd.Client() def _prepare_response(self, s, d, cluster_id=None): if isinstance(d, dict): - data = json.dumps(d).encode('utf-8') + data = json.dumps(d).encode("utf-8") else: - data = d.encode('utf-8') + data = d.encode("utf-8") r = mock.create_autospec(urllib3.response.HTTPResponse)() r.status = s diff --git a/src/etcd/tests/unit/test_client.py b/src/etcd/tests/unit/test_client.py index bb05a66a..37cdee17 100644 --- a/src/etcd/tests/unit/test_client.py +++ b/src/etcd/tests/unit/test_client.py @@ -3,137 +3,164 @@ import dns.name import dns.rdtypes.IN.SRV import dns.resolver +from etcd.tests.unit import TestClientApiBase + try: import mock except ImportError: from unittest import mock -class TestClient(unittest.TestCase): - +class TestClient(TestClientApiBase): def test_instantiate(self): - """ client can be instantiated""" + """client can be instantiated""" client = etcd.Client() assert client is not None def test_default_host(self): - """ default host is 127.0.0.1""" + """default host is 127.0.0.1""" client = etcd.Client() assert client.host == "127.0.0.1" def test_default_port(self): - """ default port is 4001""" + """default port is 4001""" client = etcd.Client() assert client.port == 4001 def test_default_prefix(self): client = etcd.Client() - assert client.version_prefix == '/v2' + assert client.version_prefix == "/v2" def test_default_protocol(self): - """ default protocol is http""" + """default protocol is http""" client = etcd.Client() - assert client.protocol == 'http' + assert client.protocol == "http" def test_default_read_timeout(self): - """ default read_timeout is 60""" + """default read_timeout is 60""" client = etcd.Client() assert client.read_timeout == 60 def test_default_allow_redirect(self): - """ default allow_redirect is True""" + """default allow_redirect is True""" client = etcd.Client() assert client.allow_redirect def test_default_username(self): - """ default username is None""" + """default username is None""" client = etcd.Client() assert client.username is None def test_default_password(self): - """ default username is None""" + """default username is None""" client = etcd.Client() assert client.password is None def test_set_host(self): - """ can change host """ - client = etcd.Client(host='192.168.1.1') - assert client.host == '192.168.1.1' + """can change host""" + client = etcd.Client(host="192.168.1.1") + assert client.host == "192.168.1.1" def test_set_port(self): - """ can change port """ + """can change port""" client = etcd.Client(port=4002) assert client.port == 4002 def test_set_prefix(self): - client = etcd.Client(version_prefix='/etcd') - assert client.version_prefix == '/etcd' + client = etcd.Client(version_prefix="/etcd") + assert client.version_prefix == "/etcd" def test_set_protocol(self): - """ can change protocol """ - client = etcd.Client(protocol='https') - assert client.protocol == 'https' + """can change protocol""" + client = etcd.Client(protocol="https") + assert client.protocol == "https" def test_set_read_timeout(self): - """ can set read_timeout """ + """can set read_timeout""" client = etcd.Client(read_timeout=45) assert client.read_timeout == 45 def test_set_allow_redirect(self): - """ can change allow_redirect """ + """can change allow_redirect""" client = etcd.Client(allow_redirect=False) assert not client.allow_redirect def test_default_base_uri(self): - """ default uri is http://127.0.0.1:4001 """ + """default uri is http://127.0.0.1:4001""" client = etcd.Client() - assert client.base_uri == 'http://127.0.0.1:4001' + assert client.base_uri == "http://127.0.0.1:4001" def test_set_base_uri(self): - """ can change base uri """ - client = etcd.Client( - host='192.168.1.1', - port=4003, - protocol='https') - assert client.base_uri == 'https://192.168.1.1:4003' + """can change base uri""" + client = etcd.Client(host="192.168.1.1", port=4003, protocol="https") + assert client.base_uri == "https://192.168.1.1:4003" def test_set_use_proxies(self): - """ can set the use_proxies flag """ - client = etcd.Client(use_proxies = True) + """can set the use_proxies flag""" + client = etcd.Client(use_proxies=True) assert client._use_proxies def test_set_username_only(self): - client = etcd.Client(username='username') + client = etcd.Client(username="username") assert client.username is None def test_set_password_only(self): - client = etcd.Client(password='password') + client = etcd.Client(password="password") assert client.password is None def test_set_username_password(self): - client = etcd.Client(username='username', password='password') - assert client.username == 'username' - assert client.password == 'password' + client = etcd.Client(username="username", password="password") + assert client.username == "username" + assert client.password == "password" def test_get_headers_with_auth(self): - client = etcd.Client(username='username', password='password') - assert client._get_headers() == { - 'authorization': 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=' - } + client = etcd.Client(username="username", password="password") + assert client._get_headers() == {"authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ="} + + def test__set_version_info(self): + """Verify _set_version_info makes the proper call to the server""" + data = {"etcdserver": "2.2.3", "etcdcluster": "2.3.0"} + self._mock_api(200, data) + self.client.api_execute.return_value.getheader.return_value = None + # Create the client and make the call. + self.client._set_version_info() + + # Verify we call the proper endpoint + self.client.api_execute.assert_called_once_with("/version", self.client._MGET) + # Verify the properties while we are here + self.assertEqual("2.2.3", self.client.version) + self.assertEqual("2.3.0", self.client.cluster_version) + + def test_version_property(self): + """Ensure the version property is set on first access.""" + data = {"etcdserver": "2.2.3", "etcdcluster": "2.3.0"} + self._mock_api(200, data) + self.client.api_execute.return_value.getheader.return_value = None + + # Verify the version property is set + self.assertEqual("2.2.3", self.client.version) + + def test_cluster_version_property(self): + """Ensure the cluster version property is set on first access.""" + data = {"etcdserver": "2.2.3", "etcdcluster": "2.3.0"} + self._mock_api(200, data) + self.client.api_execute.return_value.getheader.return_value = None + # Verify the cluster_version property is set + self.assertEqual("2.3.0", self.client.cluster_version) def test_get_headers_without_auth(self): client = etcd.Client() assert client._get_headers() == {} def test_allow_reconnect(self): - """ Fails if allow_reconnect is false and a list of hosts is given""" + """Fails if allow_reconnect is false and a list of hosts is given""" with self.assertRaises(etcd.EtcdException): etcd.Client( - host=(('localhost', 4001), ('localhost', 4002)), + host=(("localhost", 4001), ("localhost", 4002)), ) # This doesn't raise an exception client = etcd.Client( - host=(('localhost', 4001), ('localhost', 4002)), + host=(("localhost", 4001), ("localhost", 4002)), allow_reconnect=True, use_proxies=True, ) @@ -141,21 +168,22 @@ def test_allow_reconnect(self): def test_discover(self): """Tests discovery.""" answers = [] - for i in range(1,3): + for i in range(1, 3): r = mock.create_autospec(dns.rdtypes.IN.SRV.SRV) r.port = 2379 try: method = dns.name.from_unicode except AttributeError: method = dns.name.from_text - r.target = method(u'etcd{}.example.com'.format(i)) + r.target = method("etcd{}.example.com".format(i)) answers.append(r) dns.resolver.query = mock.create_autospec(dns.resolver.query, return_value=answers) self.machines = etcd.Client.machines - etcd.Client.machines = mock.create_autospec(etcd.Client.machines, return_value=[u'https://etcd2.example.com:2379']) + etcd.Client.machines = mock.create_autospec( + etcd.Client.machines, return_value=["https://etcd2.example.com:2379"] + ) c = etcd.Client(srv_domain="example.com", allow_reconnect=True, protocol="https") etcd.Client.machines = self.machines - self.assertEquals(c.host, u'etcd1.example.com') - self.assertEquals(c.port, 2379) - self.assertEquals(c._machines_cache, - [u'https://etcd2.example.com:2379']) + self.assertEqual(c.host, "etcd1.example.com") + self.assertEqual(c.port, 2379) + self.assertEqual(c._machines_cache, ["https://etcd2.example.com:2379"]) diff --git a/src/etcd/tests/unit/test_lock.py b/src/etcd/tests/unit/test_lock.py index b114c1fc..5996872e 100644 --- a/src/etcd/tests/unit/test_lock.py +++ b/src/etcd/tests/unit/test_lock.py @@ -1,4 +1,5 @@ import etcd + try: import mock except ImportError: @@ -7,137 +8,150 @@ class TestClientLock(TestClientApiBase): - def recursive_read(self): nodes = [ - {"key": "/_locks/test_lock/1", "value": "2qwwwq", - "modifiedIndex":33,"createdIndex":33}, - {"key": "/_locks/test_lock/34", "value": self.locker.uuid, - "modifiedIndex":34,"createdIndex":34}, + { + "key": "/_locks/test_lock/1", + "value": "2qwwwq", + "modifiedIndex": 33, + "createdIndex": 33, + }, + { + "key": "/_locks/test_lock/34", + "value": self.locker.uuid, + "modifiedIndex": 34, + "createdIndex": 34, + }, ] d = { "action": "get", - "node": {"dir": True, - "nodes": [{"key":"/_locks/test_lock", "dir": True, - "nodes": nodes}]} + "node": { + "dir": True, + "nodes": [{"key": "/_locks/test_lock", "dir": True, "nodes": nodes}], + }, } self._mock_api(200, d) def setUp(self): super(TestClientLock, self).setUp() - self.locker = etcd.Lock(self.client, 'test_lock') + self.locker = etcd.Lock(self.client, "test_lock") def test_initialization(self): """ Verify the lock gets initialized correctly """ - self.assertEquals(self.locker.name, u'test_lock') - self.assertEquals(self.locker.path, u'/_locks/test_lock') - self.assertEquals(self.locker.is_taken, False) + self.assertEqual(self.locker.name, "test_lock") + self.assertEqual(self.locker.path, "/_locks/test_lock") + self.assertEqual(self.locker.is_taken, False) def test_acquire(self): """ Acquiring a precedingly inexistent lock works. """ - l = etcd.Lock(self.client, 'test_lock') + l = etcd.Lock(self.client, "test_lock") l._find_lock = mock.MagicMock(spec=l._find_lock, return_value=False) l._acquired = mock.MagicMock(spec=l._acquired, return_value=True) # Mock the write d = { - u'action': u'set', - u'node': { - u'modifiedIndex': 190, - u'key': u'/_locks/test_lock/1', - u'value': l.uuid - } + "action": "set", + "node": { + "modifiedIndex": 190, + "key": "/_locks/test_lock/1", + "value": l.uuid, + }, } self._mock_api(200, d) - self.assertEquals(l.acquire(), True) - self.assertEquals(l._sequence, '1') + self.assertEqual(l.acquire(), True) + self.assertEqual(l._sequence, "1") def test_is_acquired(self): """ Test is_acquired """ - self.locker._sequence = '1' + self.locker._sequence = "1" d = { - u'action': u'get', - u'node': { - u'modifiedIndex': 190, - u'key': u'/_locks/test_lock/1', - u'value': self.locker.uuid - } + "action": "get", + "node": { + "modifiedIndex": 190, + "key": "/_locks/test_lock/1", + "value": self.locker.uuid, + }, } self._mock_api(200, d) self.locker.is_taken = True - self.assertEquals(self.locker.is_acquired, True) + self.assertEqual(self.locker.is_acquired, True) def test_is_not_acquired(self): """ Test is_acquired failures """ - self.locker._sequence = '2' + self.locker._sequence = "2" self.locker.is_taken = False - self.assertEquals(self.locker.is_acquired, False) + self.assertEqual(self.locker.is_acquired, False) self.locker.is_taken = True self._mock_exception(etcd.EtcdKeyNotFound, self.locker.lock_key) - self.assertEquals(self.locker.is_acquired, False) - self.assertEquals(self.locker.is_taken, False) + self.assertEqual(self.locker.is_acquired, False) + self.assertEqual(self.locker.is_taken, False) def test_acquired(self): """ Test the acquiring primitives """ - self.locker._sequence = '4' - retval = ('/_locks/test_lock/4', None) - self.locker._get_locker = mock.MagicMock( - spec=self.locker._get_locker, return_value=retval) + self.locker._sequence = "4" + retval = ("/_locks/test_lock/4", None) + self.locker._get_locker = mock.MagicMock(return_value=retval) self.assertTrue(self.locker._acquired()) self.assertTrue(self.locker.is_taken) - retval = ('/_locks/test_lock/1', '/_locks/test_lock/4') + retval = ("/_locks/test_lock/1", "/_locks/test_lock/4") self.locker._get_locker = mock.MagicMock(return_value=retval) self.assertFalse(self.locker._acquired(blocking=False)) self.assertFalse(self.locker.is_taken) d = { - u'action': u'delete', - u'node': { - u'modifiedIndex': 190, - u'key': u'/_locks/test_lock/1', - u'value': self.locker.uuid - } + "action": "delete", + "node": { + "modifiedIndex": 190, + "key": "/_locks/test_lock/1", + "value": self.locker.uuid, + }, } self._mock_api(200, d) - returns = [('/_locks/test_lock/1', '/_locks/test_lock/4'), ('/_locks/test_lock/4', None)] + returns = [ + ("/_locks/test_lock/1", "/_locks/test_lock/4"), + ("/_locks/test_lock/4", None), + ] def side_effect(): return returns.pop() - self.locker._get_locker = mock.MagicMock( - spec=self.locker._get_locker, side_effect=side_effect) + self.locker._get_locker = mock.MagicMock(side_effect=side_effect) self.assertTrue(self.locker._acquired()) def test_acquired_no_timeout(self): self.locker._sequence = 4 returns = [ - ('/_locks/test_lock/4', None), - ('/_locks/test_lock/1', etcd.EtcdResult(node={"key": '/_locks/test_lock/4', "modifiedIndex": 1})) + ("/_locks/test_lock/4", None), + ( + "/_locks/test_lock/1", + etcd.EtcdResult(node={"key": "/_locks/test_lock/4", "modifiedIndex": 1}), + ), ] def side_effect(): return returns.pop() d = { - u'action': u'get', - u'node': { - u'modifiedIndex': 190, - u'key': u'/_locks/test_lock/4', - u'value': self.locker.uuid - } + "action": "get", + "node": { + "modifiedIndex": 190, + "key": "/_locks/test_lock/4", + "value": self.locker.uuid, + }, } self._mock_api(200, d) self.locker._get_locker = mock.create_autospec( - self.locker._get_locker, side_effect=side_effect) + self.locker._get_locker, side_effect=side_effect + ) self.assertTrue(self.locker._acquired()) def test_lock_key(self): @@ -146,24 +160,24 @@ def test_lock_key(self): """ with self.assertRaises(ValueError): self.locker.lock_key - self.locker._sequence = '5' - self.assertEquals(u'/_locks/test_lock/5',self.locker.lock_key) + self.locker._sequence = "5" + self.assertEqual("/_locks/test_lock/5", self.locker.lock_key) def test_set_sequence(self): - self.locker._set_sequence('/_locks/test_lock/10') - self.assertEquals('10', self.locker._sequence) + self.locker._set_sequence("/_locks/test_lock/10") + self.assertEqual("10", self.locker._sequence) def test_find_lock(self): d = { - u'action': u'get', - u'node': { - u'modifiedIndex': 190, - u'key': u'/_locks/test_lock/1', - u'value': self.locker.uuid - } + "action": "get", + "node": { + "modifiedIndex": 190, + "key": "/_locks/test_lock/1", + "value": self.locker.uuid, + }, } self._mock_api(200, d) - self.locker._sequence = '1' + self.locker._sequence = "1" self.assertTrue(self.locker._find_lock()) # Now let's pretend the lock is not there self._mock_exception(etcd.EtcdKeyNotFound, self.locker.lock_key) @@ -171,24 +185,42 @@ def test_find_lock(self): self.locker._sequence = None self.recursive_read() self.assertTrue(self.locker._find_lock()) - self.assertEquals(self.locker._sequence, '34') + self.assertEqual(self.locker._sequence, "34") def test_get_locker(self): self.recursive_read() - self.assertEquals((u'/_locks/test_lock/1', etcd.EtcdResult(node={'newKey': False, '_children': [], 'createdIndex': 33, 'modifiedIndex': 33, 'value': u'2qwwwq', 'expiration': None, 'key': u'/_locks/test_lock/1', 'ttl': None, 'action': None, 'dir': False})), - self.locker._get_locker()) + self.assertEqual( + ( + "/_locks/test_lock/1", + etcd.EtcdResult( + node={ + "newKey": False, + "_children": [], + "createdIndex": 33, + "modifiedIndex": 33, + "value": "2qwwwq", + "expiration": None, + "key": "/_locks/test_lock/1", + "ttl": None, + "action": None, + "dir": False, + } + ), + ), + self.locker._get_locker(), + ) with self.assertRaises(etcd.EtcdLockExpired): - self.locker._sequence = '35' + self.locker._sequence = "35" self.locker._get_locker() def test_release(self): d = { - u'action': u'delete', - u'node': { - u'modifiedIndex': 190, - u'key': u'/_locks/test_lock/1', - u'value': self.locker.uuid - } + "action": "delete", + "node": { + "modifiedIndex": 190, + "key": "/_locks/test_lock/1", + "value": self.locker.uuid, + }, } self._mock_api(200, d) self.locker._sequence = 1 diff --git a/src/etcd/tests/unit/test_old_request.py b/src/etcd/tests/unit/test_old_request.py index 0d437131..b660c24d 100644 --- a/src/etcd/tests/unit/test_old_request.py +++ b/src/etcd/tests/unit/test_old_request.py @@ -1,5 +1,6 @@ import etcd import unittest + try: import mock except ImportError: @@ -9,10 +10,9 @@ class FakeHTTPResponse(object): - - def __init__(self, status, data='', headers=None): + def __init__(self, status, data="", headers=None): self.status = status - self.data = data.encode('utf-8') + self.data = data.encode("utf-8") self.headers = headers or { "x-etcd-cluster-id": "abdef12345", } @@ -25,340 +25,385 @@ def getheader(self, header): class TestClientRequest(unittest.TestCase): - def test_set(self): - """ Can set a value """ + """Can set a value""" client = etcd.Client() client.api_execute = mock.Mock( - return_value=FakeHTTPResponse(201, - '{"action":"SET",' - '"node": {' - '"key":"/testkey",' - '"value":"test",' - '"newKey":true,' - '"expiration":"2013-09-14T00:56:59.316195568+02:00",' - '"ttl":19,"modifiedIndex":183}}') + return_value=FakeHTTPResponse( + 201, + '{"action":"SET",' + '"node": {' + '"key":"/testkey",' + '"value":"test",' + '"newKey":true,' + '"expiration":"2013-09-14T00:56:59.316195568+02:00",' + '"ttl":19,"modifiedIndex":183}}', + ) ) - result = client.set('/testkey', 'test', ttl=19) + result = client.set("/testkey", "test", ttl=19) - self.assertEquals( + self.assertEqual( etcd.EtcdResult( - **{u'action': u'SET', - 'node': { - u'expiration': u'2013-09-14T00:56:59.316195568+02:00', - u'modifiedIndex': 183, - u'key': u'/testkey', - u'newKey': True, - u'ttl': 19, - u'value': u'test'}}), result) + **{ + "action": "SET", + "node": { + "expiration": "2013-09-14T00:56:59.316195568+02:00", + "modifiedIndex": 183, + "key": "/testkey", + "newKey": True, + "ttl": 19, + "value": "test", + }, + } + ), + result, + ) def test_test_and_set(self): - """ Can test and set a value """ + """Can test and set a value""" client = etcd.Client() client.api_execute = mock.Mock( - return_value=FakeHTTPResponse(200, - '{"action":"SET",' - '"node": {' - '"key":"/testkey",' - '"prevValue":"test",' - '"value":"newvalue",' - '"expiration":"2013-09-14T02:09:44.24390976+02:00",' - '"ttl":49,"modifiedIndex":203}}') + return_value=FakeHTTPResponse( + 200, + '{"action":"SET",' + '"node": {' + '"key":"/testkey",' + '"prevValue":"test",' + '"value":"newvalue",' + '"expiration":"2013-09-14T02:09:44.24390976+02:00",' + '"ttl":49,"modifiedIndex":203}}', + ) ) - result = client.test_and_set('/testkey', 'newvalue', 'test', ttl=19) - self.assertEquals( + result = client.test_and_set("/testkey", "newvalue", "test", ttl=19) + self.assertEqual( etcd.EtcdResult( - **{u'action': u'SET', - u'node': { - u'expiration': u'2013-09-14T02:09:44.24390976+02:00', - u'modifiedIndex': 203, - u'key': u'/testkey', - u'prevValue': u'test', - u'ttl': 49, - u'value': u'newvalue'} - }), result) + **{ + "action": "SET", + "node": { + "expiration": "2013-09-14T02:09:44.24390976+02:00", + "modifiedIndex": 203, + "key": "/testkey", + "prevValue": "test", + "ttl": 49, + "value": "newvalue", + }, + } + ), + result, + ) def test_test_and_test_failure(self): - """ Exception will be raised if prevValue != value in test_set """ + """Exception will be raised if prevValue != value in test_set""" client = etcd.Client() client.api_execute = mock.Mock( side_effect=ValueError( - 'The given PrevValue is not equal' - ' to the value of the key : TestAndSet: 1!=3')) + "The given PrevValue is not equal" " to the value of the key : TestAndSet: 1!=3" + ) + ) try: - result = client.test_and_set( - '/testkey', - 'newvalue', - 'test', ttl=19) + result = client.test_and_set("/testkey", "newvalue", "test", ttl=19) except ValueError as e: - #from ipdb import set_trace; set_trace() - self.assertEquals( - 'The given PrevValue is not equal' - ' to the value of the key : TestAndSet: 1!=3', str(e)) + # from ipdb import set_trace; set_trace() + self.assertEqual( + "The given PrevValue is not equal" " to the value of the key : TestAndSet: 1!=3", + str(e), + ) def test_delete(self): - """ Can delete a value """ + """Can delete a value""" client = etcd.Client() client.api_execute = mock.Mock( - return_value=FakeHTTPResponse(200, - '{"action":"DELETE",' - '"node": {' - '"key":"/testkey",' - '"prevValue":"test",' - '"expiration":"2013-09-14T01:06:35.5242587+02:00",' - '"modifiedIndex":189}}') + return_value=FakeHTTPResponse( + 200, + '{"action":"DELETE",' + '"node": {' + '"key":"/testkey",' + '"prevValue":"test",' + '"expiration":"2013-09-14T01:06:35.5242587+02:00",' + '"modifiedIndex":189}}', + ) + ) + result = client.delete("/testkey") + self.assertEqual( + etcd.EtcdResult( + **{ + "action": "DELETE", + "node": { + "expiration": "2013-09-14T01:06:35.5242587+02:00", + "modifiedIndex": 189, + "key": "/testkey", + "prevValue": "test", + }, + } + ), + result, ) - result = client.delete('/testkey') - self.assertEquals(etcd.EtcdResult( - **{u'action': u'DELETE', - u'node': { - u'expiration': u'2013-09-14T01:06:35.5242587+02:00', - u'modifiedIndex': 189, - u'key': u'/testkey', - u'prevValue': u'test'} - }), result) def test_get(self): - """ Can get a value """ + """Can get a value""" client = etcd.Client() client.api_execute = mock.Mock( - return_value=FakeHTTPResponse(200, - '{"action":"GET",' - '"node": {' - '"key":"/testkey",' - '"value":"test",' - '"modifiedIndex":190}}') + return_value=FakeHTTPResponse( + 200, + '{"action":"GET",' + '"node": {' + '"key":"/testkey",' + '"value":"test",' + '"modifiedIndex":190}}', + ) ) - result = client.get('/testkey') - self.assertEquals(etcd.EtcdResult( - **{u'action': u'GET', - u'node': { - u'modifiedIndex': 190, - u'key': u'/testkey', - u'value': u'test'} - }), result) + result = client.get("/testkey") + self.assertEqual( + etcd.EtcdResult( + **{ + "action": "GET", + "node": {"modifiedIndex": 190, "key": "/testkey", "value": "test"}, + } + ), + result, + ) def test_get_multi(self): """Can get multiple values""" pass def test_get_subdirs(self): - """ Can understand dirs in results """ + """Can understand dirs in results""" pass def test_not_in(self): - """ Can check if key is not in client """ + """Can check if key is not in client""" client = etcd.Client() client.get = mock.Mock(side_effect=etcd.EtcdKeyNotFound()) - result = '/testkey' not in client - self.assertEquals(True, result) + result = "/testkey" not in client + self.assertEqual(True, result) def test_in(self): - """ Can check if key is in client """ + """Can check if key is in client""" client = etcd.Client() client.api_execute = mock.Mock( - return_value=FakeHTTPResponse(200, - '{"action":"GET",' - '"node": {' - '"key":"/testkey",' - '"value":"test",' - '"modifiedIndex":190}}') + return_value=FakeHTTPResponse( + 200, + '{"action":"GET",' + '"node": {' + '"key":"/testkey",' + '"value":"test",' + '"modifiedIndex":190}}', + ) ) - result = '/testkey' in client + result = "/testkey" in client - self.assertEquals(True, result) + self.assertEqual(True, result) def test_simple_watch(self): - """ Can watch values """ + """Can watch values""" client = etcd.Client() client.api_execute = mock.Mock( - return_value=FakeHTTPResponse(200, - '{"action":"SET",' - '"node": {' - '"key":"/testkey",' - '"value":"test",' - '"newKey":true,' - '"expiration":"2013-09-14T01:35:07.623681365+02:00",' - '"ttl":19,' - '"modifiedIndex":192}}') + return_value=FakeHTTPResponse( + 200, + '{"action":"SET",' + '"node": {' + '"key":"/testkey",' + '"value":"test",' + '"newKey":true,' + '"expiration":"2013-09-14T01:35:07.623681365+02:00",' + '"ttl":19,' + '"modifiedIndex":192}}', + ) ) - result = client.watch('/testkey') - self.assertEquals( + result = client.watch("/testkey") + self.assertEqual( etcd.EtcdResult( - **{u'action': u'SET', - u'node': { - u'expiration': u'2013-09-14T01:35:07.623681365+02:00', - u'modifiedIndex': 192, - u'key': u'/testkey', - u'newKey': True, - u'ttl': 19, - u'value': u'test'} - }), result) + **{ + "action": "SET", + "node": { + "expiration": "2013-09-14T01:35:07.623681365+02:00", + "modifiedIndex": 192, + "key": "/testkey", + "newKey": True, + "ttl": 19, + "value": "test", + }, + } + ), + result, + ) def test_index_watch(self): - """ Can watch values from index """ + """Can watch values from index""" client = etcd.Client() client.api_execute = mock.Mock( - return_value=FakeHTTPResponse(200, - '{"action":"SET",' - '"node": {' - '"key":"/testkey",' - '"value":"test",' - '"newKey":true,' - '"expiration":"2013-09-14T01:35:07.623681365+02:00",' - '"ttl":19,' - '"modifiedIndex":180}}') + return_value=FakeHTTPResponse( + 200, + '{"action":"SET",' + '"node": {' + '"key":"/testkey",' + '"value":"test",' + '"newKey":true,' + '"expiration":"2013-09-14T01:35:07.623681365+02:00",' + '"ttl":19,' + '"modifiedIndex":180}}', + ) ) - result = client.watch('/testkey', index=180) - self.assertEquals( + result = client.watch("/testkey", index=180) + self.assertEqual( etcd.EtcdResult( - **{u'action': u'SET', - u'node': { - u'expiration': u'2013-09-14T01:35:07.623681365+02:00', - u'modifiedIndex': 180, - u'key': u'/testkey', - u'newKey': True, - u'ttl': 19, - u'value': u'test'} - }), result) + **{ + "action": "SET", + "node": { + "expiration": "2013-09-14T01:35:07.623681365+02:00", + "modifiedIndex": 180, + "key": "/testkey", + "newKey": True, + "ttl": 19, + "value": "test", + }, + } + ), + result, + ) class TestEventGenerator(object): - def check_watch(self, result): - assert etcd.EtcdResult( - **{u'action': u'SET', - u'node': { - u'expiration': u'2013-09-14T01:35:07.623681365+02:00', - u'modifiedIndex': 180, - u'key': u'/testkey', - u'newKey': True, - u'ttl': 19, - u'value': u'test'} - }) == result + assert ( + etcd.EtcdResult( + **{ + "action": "SET", + "node": { + "expiration": "2013-09-14T01:35:07.623681365+02:00", + "modifiedIndex": 180, + "key": "/testkey", + "newKey": True, + "ttl": 19, + "value": "test", + }, + } + ) + == result + ) def test_eternal_watch(self): - """ Can watch values from generator """ + """Can watch values from generator""" client = etcd.Client() client.api_execute = mock.Mock( - return_value=FakeHTTPResponse(200, - '{"action":"SET",' - '"node": {' - '"key":"/testkey",' - '"value":"test",' - '"newKey":true,' - '"expiration":"2013-09-14T01:35:07.623681365+02:00",' - '"ttl":19,' - '"modifiedIndex":180}}') + return_value=FakeHTTPResponse( + 200, + '{"action":"SET",' + '"node": {' + '"key":"/testkey",' + '"value":"test",' + '"newKey":true,' + '"expiration":"2013-09-14T01:35:07.623681365+02:00",' + '"ttl":19,' + '"modifiedIndex":180}}', + ) ) for result in range(1, 5): - result = next(client.eternal_watch('/testkey', index=180)) - yield self.check_watch, result + result = next(client.eternal_watch("/testkey", index=180)) + self.check_watch(result) class TestClientApiExecutor(unittest.TestCase): - def test_get(self): - """ http get request """ + """http get request""" client = etcd.Client() - response = FakeHTTPResponse(status=200, data='arbitrary json data') + response = FakeHTTPResponse(status=200, data="arbitrary json data") client.http.request = mock.Mock(return_value=response) - result = client.api_execute('/v1/keys/testkey', client._MGET) - self.assertEquals('arbitrary json data'.encode('utf-8'), result.data) + result = client.api_execute("/v1/keys/testkey", client._MGET) + self.assertEqual("arbitrary json data".encode("utf-8"), result.data) def test_delete(self): - """ http delete request """ + """http delete request""" client = etcd.Client() - response = FakeHTTPResponse(status=200, data='arbitrary json data') + response = FakeHTTPResponse(status=200, data="arbitrary json data") client.http.request = mock.Mock(return_value=response) - result = client.api_execute('/v1/keys/testkey', client._MDELETE) - self.assertEquals('arbitrary json data'.encode('utf-8'), result.data) + result = client.api_execute("/v1/keys/testkey", client._MDELETE) + self.assertEqual("arbitrary json data".encode("utf-8"), result.data) def test_get_error(self): - """ http get error request 101""" + """http get error request 101""" client = etcd.Client() - response = FakeHTTPResponse(status=400, - data='{"message": "message",' - ' "cause": "cause",' - ' "errorCode": 100}') + response = FakeHTTPResponse( + status=400, + data='{"message": "message",' ' "cause": "cause",' ' "errorCode": 100}', + ) client.http.request = mock.Mock(return_value=response) try: - client.api_execute('/v2/keys/testkey', client._MGET) + client.api_execute("/v2/keys/testkey", client._MGET) assert False except etcd.EtcdKeyNotFound as e: - self.assertEquals(str(e), 'message : cause') + self.assertEqual(str(e), "message : cause") def test_put(self): - """ http put request """ + """http put request""" client = etcd.Client() - response = FakeHTTPResponse(status=200, data='arbitrary json data') + response = FakeHTTPResponse(status=200, data="arbitrary json data") client.http.request_encode_body = mock.Mock(return_value=response) - result = client.api_execute('/v2/keys/testkey', client._MPUT) - self.assertEquals('arbitrary json data'.encode('utf-8'), result.data) + result = client.api_execute("/v2/keys/testkey", client._MPUT) + self.assertEqual("arbitrary json data".encode("utf-8"), result.data) def test_test_and_set_error(self): - """ http post error request 101 """ + """http post error request 101""" client = etcd.Client() response = FakeHTTPResponse( status=400, - data='{"message": "message", "cause": "cause", "errorCode": 101}') + data='{"message": "message", "cause": "cause", "errorCode": 101}', + ) client.http.request_encode_body = mock.Mock(return_value=response) - payload = {'value': 'value', 'prevValue': 'oldValue', 'ttl': '60'} + payload = {"value": "value", "prevValue": "oldValue", "ttl": "60"} try: - client.api_execute('/v2/keys/testkey', client._MPUT, payload) + client.api_execute("/v2/keys/testkey", client._MPUT, payload) self.fail() except ValueError as e: - self.assertEquals('message : cause', str(e)) + self.assertEqual("message : cause", str(e)) def test_set_not_file_error(self): - """ http post error request 102 """ + """http post error request 102""" client = etcd.Client() response = FakeHTTPResponse( status=400, - data='{"message": "message", "cause": "cause", "errorCode": 102}') + data='{"message": "message", "cause": "cause", "errorCode": 102}', + ) client.http.request_encode_body = mock.Mock(return_value=response) - payload = {'value': 'value', 'prevValue': 'oldValue', 'ttl': '60'} + payload = {"value": "value", "prevValue": "oldValue", "ttl": "60"} try: - client.api_execute('/v2/keys/testkey', client._MPUT, payload) + client.api_execute("/v2/keys/testkey", client._MPUT, payload) self.fail() except etcd.EtcdNotFile as e: - self.assertEquals('message : cause', str(e)) + self.assertEqual("message : cause", str(e)) def test_get_error_unknown(self): - """ http get error request unknown """ + """http get error request unknown""" client = etcd.Client() - response = FakeHTTPResponse(status=400, - data='{"message": "message",' - ' "cause": "cause",' - ' "errorCode": 42}') + response = FakeHTTPResponse( + status=400, + data='{"message": "message",' ' "cause": "cause",' ' "errorCode": 42}', + ) client.http.request = mock.Mock(return_value=response) try: - client.api_execute('/v2/keys/testkey', client._MGET) + client.api_execute("/v2/keys/testkey", client._MGET) self.fail() except etcd.EtcdException as e: self.assertEqual(str(e), "message : cause") def test_get_error_request_invalid(self): - """ http get error request invalid """ + """http get error request invalid""" client = etcd.Client() - response = FakeHTTPResponse(status=400, - data='{)*garbage') + response = FakeHTTPResponse(status=400, data="{)*garbage") client.http.request = mock.Mock(return_value=response) try: - client.api_execute('/v2/keys/testkey', client._MGET) + client.api_execute("/v2/keys/testkey", client._MGET) self.fail() except etcd.EtcdException as e: - self.assertEqual(str(e), - "Bad response : {)*garbage") + self.assertEqual(str(e), "Bad response : {)*garbage") def test_get_error_invalid(self): - """ http get error request invalid """ + """http get error request invalid""" client = etcd.Client() - response = FakeHTTPResponse(status=400, - data='{){){)*garbage*') + response = FakeHTTPResponse(status=400, data="{){){)*garbage*") client.http.request = mock.Mock(return_value=response) - self.assertRaises(etcd.EtcdException, client.api_execute, - '/v2/keys/testkey', client._MGET) + self.assertRaises(etcd.EtcdException, client.api_execute, "/v2/keys/testkey", client._MGET) diff --git a/src/etcd/tests/unit/test_request.py b/src/etcd/tests/unit/test_request.py index b972a8c5..7685dcaf 100644 --- a/src/etcd/tests/unit/test_request.py +++ b/src/etcd/tests/unit/test_request.py @@ -11,66 +11,54 @@ class TestClientApiInternals(TestClientApiBase): - def test_read_default_timeout(self): - """ Read timeout set to the default """ + """Read timeout set to the default""" d = { - u'action': u'get', - u'node': { - u'modifiedIndex': 190, - u'key': u'/testkey', - u'value': u'test' - } + "action": "get", + "node": {"modifiedIndex": 190, "key": "/testkey", "value": "test"}, } self._mock_api(200, d) - res = self.client.read('/testkey') - self.assertEqual(self.client.api_execute.call_args[1]['timeout'], None) + res = self.client.read("/testkey") + self.assertEqual(self.client.api_execute.call_args[1]["timeout"], None) def test_read_custom_timeout(self): - """ Read timeout set to the supplied value """ + """Read timeout set to the supplied value""" d = { - u'action': u'get', - u'node': { - u'modifiedIndex': 190, - u'key': u'/testkey', - u'value': u'test' - } + "action": "get", + "node": {"modifiedIndex": 190, "key": "/testkey", "value": "test"}, } self._mock_api(200, d) - self.client.read('/testkey', timeout=15) - self.assertEqual(self.client.api_execute.call_args[1]['timeout'], 15) + self.client.read("/testkey", timeout=15) + self.assertEqual(self.client.api_execute.call_args[1]["timeout"], 15) def test_read_no_timeout(self): - """ Read timeout disabled """ + """Read timeout disabled""" d = { - u'action': u'get', - u'node': { - u'modifiedIndex': 190, - u'key': u'/testkey', - u'value': u'test' - } + "action": "get", + "node": {"modifiedIndex": 190, "key": "/testkey", "value": "test"}, } self._mock_api(200, d) - self.client.read('/testkey', timeout=0) - self.assertEqual(self.client.api_execute.call_args[1]['timeout'], 0) + self.client.read("/testkey", timeout=0) + self.assertEqual(self.client.api_execute.call_args[1]["timeout"], 0) def test_write_no_params(self): - """ Calling `write` without a value argument will omit the `value` from - the API call params """ + """Calling `write` without a value argument will omit the `value` from + the API call params""" d = { - u'action': u'set', - u'node': { - u'createdIndex': 17, - u'dir': True, - u'key': u'/newdir', - u'modifiedIndex': 17 - } + "action": "set", + "node": { + "createdIndex": 17, + "dir": True, + "key": "/newdir", + "modifiedIndex": 17, + }, } self._mock_api(200, d) - self.client.write('/newdir', None, dir=True) - self.assertEquals(self.client.api_execute.call_args, - (('/v2/keys/newdir', 'PUT'), - dict(params={'dir': 'true'}))) + self.client.write("/newdir", None, dir=True) + self.assertEqual( + self.client.api_execute.call_args, + (("/v2/keys/newdir", "PUT"), dict(params={"dir": "true"})), + ) class TestClientApiInterface(TestClientApiBase): @@ -79,65 +67,69 @@ class TestClientApiInterface(TestClientApiBase): If a test should be run only in this class, please override the method there. """ - @mock.patch('urllib3.request.RequestMethods.request') - def test_machines(self, mocker): - """ Can request machines """ - data = ['http://127.0.0.1:4001', - 'http://127.0.0.1:4002', 'http://127.0.0.1:4003'] - d = ','.join(data) - mocker.return_value = self._prepare_response(200, d) - self.assertEquals(data, self.client.machines) - - @mock.patch('etcd.Client.machines', new_callable=mock.PropertyMock) + + def test_machines(self): + """Can request machines""" + data = [ + "http://127.0.0.1:4001", + "http://127.0.0.1:4002", + "http://127.0.0.1:4003", + ] + d = ",".join(data) + self.client.http.request = mock.MagicMock(return_value=self._prepare_response(200, d)) + self.assertEqual(data, self.client.machines) + + @mock.patch("etcd.Client.machines", new_callable=mock.PropertyMock) def test_use_proxies(self, mocker): """Do not overwrite the machines cache when using proxies""" - mocker.return_value = ['https://10.0.0.2:4001', - 'https://10.0.0.3:4001', - 'https://10.0.0.4:4001'] + mocker.return_value = [ + "https://10.0.0.2:4001", + "https://10.0.0.3:4001", + "https://10.0.0.4:4001", + ] c = etcd.Client( - host=(('localhost', 4001), ('localproxy', 4001)), - protocol='https', + host=(("localhost", 4001), ("localproxy", 4001)), + protocol="https", allow_reconnect=True, - use_proxies=True + use_proxies=True, ) - self.assertEquals(c._machines_cache, ['https://localproxy:4001']) - self.assertEquals(c._base_uri, 'https://localhost:4001') + self.assertEqual(c._machines_cache, ["https://localproxy:4001"]) + self.assertEqual(c._base_uri, "https://localhost:4001") self.assertNotIn(c.base_uri, c._machines_cache) c = etcd.Client( - host=(('localhost', 4001), ('10.0.0.2', 4001)), - protocol='https', + host=(("localhost", 4001), ("10.0.0.2", 4001)), + protocol="https", allow_reconnect=True, - use_proxies=False + use_proxies=False, ) - self.assertIn('https://10.0.0.3:4001', c._machines_cache) + self.assertIn("https://10.0.0.3:4001", c._machines_cache) self.assertNotIn(c.base_uri, c._machines_cache) def test_members(self): - """ Can request machines """ + """Can request machines""" data = { - "members": - [ + "members": [ { "id": "ce2a822cea30bfca", "name": "default", "peerURLs": ["http://localhost:2380", "http://localhost:7001"], - "clientURLs": ["http://127.0.0.1:4001"] + "clientURLs": ["http://127.0.0.1:4001"], } ] } self._mock_api(200, data) - self.assertEquals(self.client.members["ce2a822cea30bfca"]["id"], "ce2a822cea30bfca") + self.assertEqual(self.client.members["ce2a822cea30bfca"]["id"], "ce2a822cea30bfca") def test_self_stats(self): - """ Request for stats """ + """Request for stats""" data = { "id": "eca0338f4ea31566", "leaderInfo": { "leader": "8a69d5f6b7814500", "startTime": "2014-10-24T13:15:51.186620747-07:00", - "uptime": "10m59.322358947s" + "uptime": "10m59.322358947s", }, "name": "node3", "recvAppendRequestCnt": 5944, @@ -145,357 +137,314 @@ def test_self_stats(self): "recvPkgRate": 9.00892789741075, "sendAppendRequestCnt": 0, "startTime": "2014-10-24T13:15:50.072007085-07:00", - "state": "StateFollower" + "state": "StateFollower", } - self._mock_api(200,data) - self.assertEquals(self.client.stats['name'], "node3") + self._mock_api(200, data) + self.assertEqual(self.client.stats["name"], "node3") def test_leader_stats(self): - """ Request for leader stats """ + """Request for leader stats""" data = {"leader": "924e2e83e93f2560", "followers": {}} - self._mock_api(200,data) - self.assertEquals(self.client.leader_stats['leader'], "924e2e83e93f2560") - + self._mock_api(200, data) + self.assertEqual(self.client.leader_stats["leader"], "924e2e83e93f2560") - @mock.patch('etcd.Client.members', new_callable=mock.PropertyMock) + @mock.patch("etcd.Client.members", new_callable=mock.PropertyMock) def test_leader(self, mocker): - """ Can request the leader """ + """Can request the leader""" members = {"ce2a822cea30bfca": {"id": "ce2a822cea30bfca", "name": "default"}} mocker.return_value = members - self._mock_api(200, {"leaderInfo":{"leader": "ce2a822cea30bfca", "followers": {}}}) - self.assertEquals(self.client.leader, members["ce2a822cea30bfca"]) + self._mock_api(200, {"leaderInfo": {"leader": "ce2a822cea30bfca", "followers": {}}}) + self.assertEqual(self.client.leader, members["ce2a822cea30bfca"]) def test_set_plain(self): - """ Can set a value """ - d = {u'action': u'set', - u'node': { - u'expiration': u'2013-09-14T00:56:59.316195568+02:00', - u'modifiedIndex': 183, - u'key': u'/testkey', - u'ttl': 19, - u'value': u'test' - } - } + """Can set a value""" + d = { + "action": "set", + "node": { + "expiration": "2013-09-14T00:56:59.316195568+02:00", + "modifiedIndex": 183, + "key": "/testkey", + "ttl": 19, + "value": "test", + }, + } self._mock_api(200, d) - res = self.client.write('/testkey', 'test') - self.assertEquals(res, etcd.EtcdResult(**d)) + res = self.client.write("/testkey", "test") + self.assertEqual(res, etcd.EtcdResult(**d)) def test_update(self): """Can update a result.""" - d = {u'action': u'set', - u'node': { - u'expiration': u'2013-09-14T00:56:59.316195568+02:00', - u'modifiedIndex': 6, - u'key': u'/testkey', - u'ttl': 19, - u'value': u'test' - } - } - self._mock_api(200,d) - res = self.client.get('/testkey') - res.value = 'ciao' - d['node']['value'] = 'ciao' - self._mock_api(200,d) + d = { + "action": "set", + "node": { + "expiration": "2013-09-14T00:56:59.316195568+02:00", + "modifiedIndex": 6, + "key": "/testkey", + "ttl": 19, + "value": "test", + }, + } + self._mock_api(200, d) + res = self.client.get("/testkey") + res.value = "ciao" + d["node"]["value"] = "ciao" + self._mock_api(200, d) newres = self.client.update(res) - self.assertEquals(newres.value, 'ciao') + self.assertEqual(newres.value, "ciao") def test_newkey(self): - """ Can set a new value """ + """Can set a new value""" d = { - u'action': u'set', - u'node': { - u'expiration': u'2013-09-14T00:56:59.316195568+02:00', - u'modifiedIndex': 183, - u'key': u'/testkey', - u'ttl': 19, - u'value': u'test' - } + "action": "set", + "node": { + "expiration": "2013-09-14T00:56:59.316195568+02:00", + "modifiedIndex": 183, + "key": "/testkey", + "ttl": 19, + "value": "test", + }, } self._mock_api(201, d) - res = self.client.write('/testkey', 'test') - d['node']['newKey'] = True - self.assertEquals(res, etcd.EtcdResult(**d)) + res = self.client.write("/testkey", "test") + d["node"]["newKey"] = True + self.assertEqual(res, etcd.EtcdResult(**d)) def test_refresh(self): - """ Can refresh a new value """ + """Can refresh a new value""" d = { - u'action': u'update', - u'node': { - u'expiration': u'2016-05-31T08:27:54.660337Z', - u'modifiedIndex': 183, - u'key': u'/testkey', - u'ttl': 600, - u'value': u'test' - } + "action": "update", + "node": { + "expiration": "2016-05-31T08:27:54.660337Z", + "modifiedIndex": 183, + "key": "/testkey", + "ttl": 600, + "value": "test", + }, } self._mock_api(200, d) - res = self.client.refresh('/testkey', ttl=600) - self.assertEquals(res, etcd.EtcdResult(**d)) + res = self.client.refresh("/testkey", ttl=600) + self.assertEqual(res, etcd.EtcdResult(**d)) def test_not_found_response(self): - """ Can handle server not found response """ - self._mock_api(404, 'Not found') - self.assertRaises(etcd.EtcdException, self.client.read, '/somebadkey') + """Can handle server not found response""" + self._mock_api(404, "Not found") + self.assertRaises(etcd.EtcdException, self.client.read, "/somebadkey") def test_compare_and_swap(self): - """ Can set compare-and-swap a value """ - d = {u'action': u'compareAndSwap', - u'node': { - u'expiration': u'2013-09-14T00:56:59.316195568+02:00', - u'modifiedIndex': 183, - u'key': u'/testkey', - u'ttl': 19, - u'value': u'test' - } - } + """Can set compare-and-swap a value""" + d = { + "action": "compareAndSwap", + "node": { + "expiration": "2013-09-14T00:56:59.316195568+02:00", + "modifiedIndex": 183, + "key": "/testkey", + "ttl": 19, + "value": "test", + }, + } self._mock_api(200, d) - res = self.client.write('/testkey', 'test', prevValue='test_old') - self.assertEquals(res, etcd.EtcdResult(**d)) + res = self.client.write("/testkey", "test", prevValue="test_old") + self.assertEqual(res, etcd.EtcdResult(**d)) def test_compare_and_swap_failure(self): - """ Exception will be raised if prevValue != value in test_set """ - self._mock_exception(ValueError, 'Test Failed : [ 1!=3 ]') - self.assertRaises( - ValueError, - self.client.write, - '/testKey', - 'test', - prevValue='oldbog' - ) + """Exception will be raised if prevValue != value in test_set""" + self._mock_exception(ValueError, "Test Failed : [ 1!=3 ]") + self.assertRaises(ValueError, self.client.write, "/testKey", "test", prevValue="oldbog") def test_set_append(self): - """ Can append a new key """ + """Can append a new key""" d = { - u'action': u'create', - u'node': { - u'createdIndex': 190, - u'modifiedIndex': 190, - u'key': u'/testdir/190', - u'value': u'test' - } + "action": "create", + "node": { + "createdIndex": 190, + "modifiedIndex": 190, + "key": "/testdir/190", + "value": "test", + }, } self._mock_api(201, d) - res = self.client.write('/testdir', 'test') - self.assertEquals(res.createdIndex, 190) + res = self.client.write("/testdir", "test") + self.assertEqual(res.createdIndex, 190) def test_set_dir_with_value(self): - """ Creating a directory with a value raises an error. """ - self.assertRaises(etcd.EtcdException, self.client.write, - '/bar', 'testvalye', dir=True) + """Creating a directory with a value raises an error.""" + self.assertRaises(etcd.EtcdException, self.client.write, "/bar", "testvalye", dir=True) def test_delete(self): - """ Can delete a value """ + """Can delete a value""" d = { - u'action': u'delete', - u'node': { - u'key': u'/testkey', - "modifiedIndex": 3, - "createdIndex": 2 - } + "action": "delete", + "node": {"key": "/testkey", "modifiedIndex": 3, "createdIndex": 2}, } self._mock_api(200, d) - res = self.client.delete('/testKey') - self.assertEquals(res, etcd.EtcdResult(**d)) + res = self.client.delete("/testKey") + self.assertEqual(res, etcd.EtcdResult(**d)) def test_pop(self): - """ Can pop a value """ + """Can pop a value""" d = { - u'action': u'delete', - u'node': { - u'key': u'/testkey', - u'modifiedIndex': 3, - u'createdIndex': 2 + "action": "delete", + "node": {"key": "/testkey", "modifiedIndex": 3, "createdIndex": 2}, + "prevNode": { + "newKey": False, + "createdIndex": None, + "modifiedIndex": 190, + "value": "test", + "expiration": None, + "key": "/testkey", + "ttl": None, + "dir": False, }, - u'prevNode': {u'newKey': False, u'createdIndex': None, - u'modifiedIndex': 190, u'value': u'test', u'expiration': None, - u'key': u'/testkey', u'ttl': None, u'dir': False} } self._mock_api(200, d) - res = self.client.pop(d['node']['key']) - self.assertEquals({attr: getattr(res, attr) for attr in dir(res) - if attr in etcd.EtcdResult._node_props}, d['prevNode']) - self.assertEqual(res.value, d['prevNode']['value']) + res = self.client.pop(d["node"]["key"]) + self.assertEqual( + {attr: getattr(res, attr) for attr in dir(res) if attr in etcd.EtcdResult._node_props}, + d["prevNode"], + ) + self.assertEqual(res.value, d["prevNode"]["value"]) def test_read(self): - """ Can get a value """ + """Can get a value""" d = { - u'action': u'get', - u'node': { - u'modifiedIndex': 190, - u'key': u'/testkey', - u'value': u'test' - } + "action": "get", + "node": {"modifiedIndex": 190, "key": "/testkey", "value": "test"}, } self._mock_api(200, d) - res = self.client.read('/testKey') - self.assertEquals(res, etcd.EtcdResult(**d)) + res = self.client.read("/testKey") + self.assertEqual(res, etcd.EtcdResult(**d)) def test_get_dir(self): """Can get values in dirs""" d = { - u'action': u'get', - u'node': { - u'modifiedIndex': 190, - u'key': u'/testkey', - u'dir': True, - u'nodes': [ + "action": "get", + "node": { + "modifiedIndex": 190, + "key": "/testkey", + "dir": True, + "nodes": [ + {"key": "/testDir/testKey", "modifiedIndex": 150, "value": "test"}, { - u'key': u'/testDir/testKey', - u'modifiedIndex': 150, - u'value': 'test' + "key": "/testDir/testKey2", + "modifiedIndex": 190, + "value": "test2", }, - { - u'key': u'/testDir/testKey2', - u'modifiedIndex': 190, - u'value': 'test2' - } - ] - } + ], + }, } self._mock_api(200, d) - res = self.client.read('/testDir', recursive=True) - self.assertEquals(res, etcd.EtcdResult(**d)) + res = self.client.read("/testDir", recursive=True) + self.assertEqual(res, etcd.EtcdResult(**d)) def test_not_in(self): - """ Can check if key is not in client """ - self._mock_exception(etcd.EtcdKeyNotFound, 'Key not Found : /testKey') - self.assertTrue('/testey' not in self.client) + """Can check if key is not in client""" + self._mock_exception(etcd.EtcdKeyNotFound, "Key not Found : /testKey") + self.assertTrue("/testey" not in self.client) def test_in(self): - """ Can check if key is not in client """ + """Can check if key is not in client""" d = { - u'action': u'get', - u'node': { - u'modifiedIndex': 190, - u'key': u'/testkey', - u'value': u'test' - } + "action": "get", + "node": {"modifiedIndex": 190, "key": "/testkey", "value": "test"}, } self._mock_api(200, d) - self.assertTrue('/testey' in self.client) + self.assertTrue("/testey" in self.client) def test_watch(self): - """ Can watch a key """ + """Can watch a key""" d = { - u'action': u'get', - u'node': { - u'modifiedIndex': 190, - u'key': u'/testkey', - u'value': u'test' - } + "action": "get", + "node": {"modifiedIndex": 190, "key": "/testkey", "value": "test"}, } self._mock_api(200, d) - res = self.client.read('/testkey', wait=True) - self.assertEquals(res, etcd.EtcdResult(**d)) + res = self.client.read("/testkey", wait=True) + self.assertEqual(res, etcd.EtcdResult(**d)) def test_watch_index(self): - """ Can watch a key starting from the given Index """ + """Can watch a key starting from the given Index""" d = { - u'action': u'get', - u'node': { - u'modifiedIndex': 170, - u'key': u'/testkey', - u'value': u'testold' - } + "action": "get", + "node": {"modifiedIndex": 170, "key": "/testkey", "value": "testold"}, } self._mock_api(200, d) - res = self.client.read('/testkey', wait=True, waitIndex=True) - self.assertEquals(res, etcd.EtcdResult(**d)) + res = self.client.read("/testkey", wait=True, waitIndex=True) + self.assertEqual(res, etcd.EtcdResult(**d)) class TestClientRequest(TestClientApiInterface): - def setUp(self): self.client = etcd.Client(expected_cluster_id="abcdef1234") def _mock_api(self, status, d, cluster_id=None): resp = self._prepare_response(status, d) resp.getheader.return_value = cluster_id or "abcdef1234" - self.client.http.request_encode_body = mock.MagicMock( - return_value=resp) + self.client.http.request_encode_body = mock.MagicMock(return_value=resp) self.client.http.request = mock.MagicMock(return_value=resp) - def _mock_error(self, error_code, msg, cause, method='PUT', fields=None, - cluster_id=None): + def _mock_error(self, error_code, msg, cause, method="PUT", fields=None, cluster_id=None): resp = self._prepare_response( - 500, - {'errorCode': error_code, 'message': msg, 'cause': cause} + 500, {"errorCode": error_code, "message": msg, "cause": cause} ) resp.getheader.return_value = cluster_id or "abcdef1234" self.client.http.request_encode_body = mock.create_autospec( self.client.http.request_encode_body, return_value=resp ) - self.client.http.request = mock.create_autospec( - self.client.http.request, return_value=resp - ) + self.client.http.request = mock.create_autospec(self.client.http.request, return_value=resp) def test_compare_and_swap_failure(self): - """ Exception will be raised if prevValue != value in test_set """ - self._mock_error(200, 'Test Failed', - '[ 1!=3 ]', fields={'prevValue': 'oldbog'}) - self.assertRaises( - ValueError, - self.client.write, - '/testKey', - 'test', - prevValue='oldbog' - ) + """Exception will be raised if prevValue != value in test_set""" + self._mock_error(200, "Test Failed", "[ 1!=3 ]", fields={"prevValue": "oldbog"}) + self.assertRaises(ValueError, self.client.write, "/testKey", "test", prevValue="oldbog") def test_watch_timeout(self): - """ Exception will be raised if prevValue != value in test_set """ + """Exception will be raised if prevValue != value in test_set""" self.client.http.request = mock.create_autospec( self.client.http.request, - side_effect=urllib3.exceptions.ReadTimeoutError(self.client.http, - "foo", - "Read timed out") + side_effect=urllib3.exceptions.ReadTimeoutError( + self.client.http, "foo", "Read timed out" + ), ) self.assertRaises( etcd.EtcdWatchTimedOut, self.client.watch, - '/testKey', + "/testKey", ) def test_path_without_trailing_slash(self): - """ Exception will be raised if a path without a trailing slash is used """ - self.assertRaises(ValueError, self.client.api_execute, - 'testpath/bar', self.client._MPUT) + """Exception will be raised if a path without a trailing slash is used""" + self.assertRaises(ValueError, self.client.api_execute, "testpath/bar", self.client._MPUT) def test_api_method_not_supported(self): - """ Exception will be raised if an unsupported HTTP method is used """ - self.assertRaises(etcd.EtcdException, - self.client.api_execute, '/testpath/bar', 'TRACE') + """Exception will be raised if an unsupported HTTP method is used""" + self.assertRaises(etcd.EtcdException, self.client.api_execute, "/testpath/bar", "TRACE") def test_read_cluster_id_changed(self): - """ Read timeout set to the default """ + """Read timeout set to the default""" d = { - u'action': u'set', - u'node': { - u'expiration': u'2013-09-14T00:56:59.316195568+02:00', - u'modifiedIndex': 6, - u'key': u'/testkey', - u'ttl': 19, - u'value': u'test', - } + "action": "set", + "node": { + "expiration": "2013-09-14T00:56:59.316195568+02:00", + "modifiedIndex": 6, + "key": "/testkey", + "ttl": 19, + "value": "test", + }, } self._mock_api(200, d, cluster_id="notabcd1234") - self.assertRaises(etcd.EtcdClusterIdChanged, - self.client.read, '/testkey') + self.assertRaises(etcd.EtcdClusterIdChanged, self.client.read, "/testkey") self.client.read("/testkey") def test_read_connection_error(self): self.client.http.request = mock.create_autospec( - self.client.http.request, - side_effect=socket.error() + self.client.http.request, side_effect=socket.error() ) - self.assertRaises(etcd.EtcdConnectionFailed, - self.client.read, '/something') + self.assertRaises(etcd.EtcdConnectionFailed, self.client.read, "/something") # Direct GET request - self.assertRaises(etcd.EtcdConnectionFailed, - self.client.api_execute, '/a', 'GET') + self.assertRaises(etcd.EtcdConnectionFailed, self.client.api_execute, "/a", "GET") def test_not_in(self): pass @@ -504,16 +453,16 @@ def test_in(self): pass def test_update_fails(self): - """ Non-atomic updates fail """ + """Non-atomic updates fail""" d = { - u'action': u'set', - u'node': { - u'expiration': u'2013-09-14T00:56:59.316195568+02:00', - u'modifiedIndex': 6, - u'key': u'/testkey', - u'ttl': 19, - u'value': u'test' - } + "action": "set", + "node": { + "expiration": "2013-09-14T00:56:59.316195568+02:00", + "modifiedIndex": 6, + "key": "/testkey", + "ttl": 19, + "value": "test", + }, } res = etcd.EtcdResult(**d) @@ -521,7 +470,8 @@ def test_update_fails(self): "errorCode": 101, "message": "Compare failed", "cause": "[ != bar] [7 != 6]", - "index": 6} + "index": 6, + } self._mock_api(412, error) - res.value = 'bar' + res.value = "bar" self.assertRaises(ValueError, self.client.update, res) diff --git a/src/etcd/tests/unit/test_result.py b/src/etcd/tests/unit/test_result.py index cb1414b1..372f10c3 100644 --- a/src/etcd/tests/unit/test_result.py +++ b/src/etcd/tests/unit/test_result.py @@ -8,22 +8,24 @@ except ImportError: from unittest import mock -class TestEtcdResult(unittest.TestCase): +class TestEtcdResult(unittest.TestCase): def test_get_subtree_1_level(self): """ Test get_subtree() for a read with tree 1 level deep. """ - response = {"node": { - 'key': "/test", - 'value': "hello", - 'expiration': None, - 'ttl': None, - 'modifiedIndex': 5, - 'createdIndex': 1, - 'newKey': False, - 'dir': False, - }} + response = { + "node": { + "key": "/test", + "value": "hello", + "expiration": None, + "ttl": None, + "modifiedIndex": 5, + "createdIndex": 1, + "newKey": False, + "dir": False, + } + } result = etcd.EtcdResult(**response) self.assertEqual(result.key, response["node"]["key"]) self.assertEqual(result.value, response["node"]["value"]) @@ -39,35 +41,37 @@ def test_get_subtree_2_level(self): Test get_subtree() for a read with tree 2 levels deep. """ leaf0 = { - 'key': "/test/leaf0", - 'value': "hello1", - 'expiration': None, - 'ttl': None, - 'modifiedIndex': 5, - 'createdIndex': 1, - 'newKey': False, - 'dir': False, + "key": "/test/leaf0", + "value": "hello1", + "expiration": None, + "ttl": None, + "modifiedIndex": 5, + "createdIndex": 1, + "newKey": False, + "dir": False, } leaf1 = { - 'key': "/test/leaf1", - 'value': "hello2", - 'expiration': None, - 'ttl': None, - 'modifiedIndex': 6, - 'createdIndex': 2, - 'newKey': False, - 'dir': False, + "key": "/test/leaf1", + "value": "hello2", + "expiration": None, + "ttl": None, + "modifiedIndex": 6, + "createdIndex": 2, + "newKey": False, + "dir": False, + } + testnode = { + "node": { + "key": "/test/", + "expiration": None, + "ttl": None, + "modifiedIndex": 6, + "createdIndex": 2, + "newKey": False, + "dir": True, + "nodes": [leaf0, leaf1], + } } - testnode = {"node": { - 'key': "/test/", - 'expiration': None, - 'ttl': None, - 'modifiedIndex': 6, - 'createdIndex': 2, - 'newKey': False, - 'dir': True, - 'nodes': [leaf0, leaf1] - }} result = etcd.EtcdResult(**testnode) self.assertEqual(result.key, "/test/") self.assertTrue(result.dir) @@ -90,36 +94,24 @@ def test_get_subtree_3_level(self): Test get_subtree() for a read with tree 3 levels deep. """ leaf0 = { - 'key': "/test/mid0/leaf0", - 'value': "hello1", + "key": "/test/mid0/leaf0", + "value": "hello1", } leaf1 = { - 'key': "/test/mid0/leaf1", - 'value': "hello2", + "key": "/test/mid0/leaf1", + "value": "hello2", } leaf2 = { - 'key': "/test/mid1/leaf2", - 'value': "hello1", + "key": "/test/mid1/leaf2", + "value": "hello1", } leaf3 = { - 'key': "/test/mid1/leaf3", - 'value': "hello2", - } - mid0 = { - 'key': "/test/mid0/", - 'dir': True, - 'nodes': [leaf0, leaf1] - } - mid1 = { - 'key': "/test/mid1/", - 'dir': True, - 'nodes': [leaf2, leaf3] + "key": "/test/mid1/leaf3", + "value": "hello2", } - testnode = {"node": { - 'key': "/test/", - 'dir': True, - 'nodes': [mid0, mid1] - }} + mid0 = {"key": "/test/mid0/", "dir": True, "nodes": [leaf0, leaf1]} + mid1 = {"key": "/test/mid1/", "dir": True, "nodes": [leaf2, leaf3]} + testnode = {"node": {"key": "/test/", "dir": True, "nodes": [mid0, mid1]}} result = etcd.EtcdResult(**testnode) self.assertEqual(result.key, "/test/") self.assertTrue(result.dir) diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..c8b44115 --- /dev/null +++ b/tox.ini @@ -0,0 +1,31 @@ +[tox] +minversion = 2.5.0 +envlist = py{3,37}-{style,unit} +skip_missing_interpreters = True + +[testenv] +usedevelop = True +basepython = + py3: python3 + py37: python3.7 +description = + style: Style consistency checker + unit: Run unit tests. + py3: (Python 3.x) + py37: (Python 3.7) + +commands = +; style: flake8 + style: black --config black.toml --check src + unit: pytest --cov=etcd src/etcd/tests/ --cov-report=term-missing + +deps = + style: flake8 + style: black + unit: pytest-cov + unit: pyOpenSSL>=0.14 + +[flake8] +max-line-length = 100 +statistics = True +exclude = .venv,.eggs,.tox,build,venv