[Python-modules-commits] [python-cs] 01/04: New upstream release.

Vincent Bernat bernat at moszumanska.debian.org
Fri Oct 6 13:20:01 UTC 2017


This is an automated email from the git hooks/post-receive script.

bernat pushed a commit to branch debian/master
in repository python-cs.

commit 4a124ad6923999c5f11c45ea01de427d9c388f60
Author: Vincent Bernat <bernat at debian.org>
Date:   Fri Oct 6 15:12:34 2017 +0200

    New upstream release.
---
 .gitignore            |   1 +
 README.rst            |  62 ++++++++++++++++++++++++++-
 cs/__init__.py        | 109 +++++++++++++++++++++++++++++++++++++++++++++++
 cs/__main__.py        |   5 +++
 cs/_async.py          | 113 +++++++++++++++++++++++++++++++++++++++++++++++++
 cs.py => cs/client.py | 114 +++++++-------------------------------------------
 setup.py              |  27 ++++++++----
 7 files changed, 322 insertions(+), 109 deletions(-)

diff --git a/.gitignore b/.gitignore
index 8dd6a02..a7a07c7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@ cs.egg-info
 dist
 .tox
 build
+*.pyc
diff --git a/README.rst b/README.rst
index 9974473..99c16d7 100644
--- a/README.rst
+++ b/README.rst
@@ -8,6 +8,7 @@ CS
 A simple, yet powerful CloudStack API client for python and the command-line.
 
 * Python 2.6+ and 3.3+ support.
+* Async support for Python 3.5+
 * All present and future CloudStack API calls and parameters are supported.
 * Syntax highlight in the command-line client if Pygments is installed.
 * BSD license.
@@ -22,7 +23,9 @@ Installation
 Usage
 -----
 
-In Python::
+In Python:
+
+.. code-block:: python
 
     from cs import CloudStack
 
@@ -82,7 +85,9 @@ Configuration is read from several locations, in the following order:
 * A ``cloudstack.ini`` file in the current working directory,
 * A ``.cloudstack.ini`` file in the home directory.
 
-To use that configuration scheme from your Python code::
+To use that configuration scheme from your Python code:
+
+.. code-block:: python
 
     from cs import CloudStack, read_config
 
@@ -133,6 +138,59 @@ Or in Python::
 
     cs.listVirtualMachines(fetch_list=True)
 
+Async client
+------------
+
+``cs`` provides the ``AIOCloudStack`` class for async/await calls in Python
+3.5+.
+
+.. code-block:: python
+
+    from cs import AIOCloudStack, read_config
+
+    cs = AIOCloudStack(**read_config())
+    vms = await cs.listVirtualMachines()
+
+By default, this client polls CloudStack's async jobs to return actual results
+for commands that result in an async job being created. You can customize this
+behavior with ``job_timeout`` (default: None -- wait indefinitely) and
+``poll_interval`` (default: 2s).
+
+.. code-block:: python
+
+    cs = AIOCloudStack(**read_config(), job_timeout=300, poll_interval=5)
+
+Async deployment of multiple vms
+________________________________
+
+.. code-block:: python
+
+    import asyncio
+    from cs import AIOCloudStack, read_config
+
+    cs = AIOCloudStack(**read_config())
+    tasks = [asyncio.ensure_future(cs.deployVirtualMachine(zoneid='',
+                                                           serviceofferingid='',
+                                                           templateid='')) for _ in range(5)]
+    results = []
+    done, pending = await asyncio.wait(tasks)
+    exceptions = 0
+    last_exception = None
+    for t in done:
+        if t.exception():
+            exceptions += 1
+            last_exception = t.exception()
+        elif t.result():
+            results.append(t.result())
+    if exceptions:
+        print(f"{exceptions} deployment(s) failed")
+        raise last_exception
+
+    # Destroy all of them, but skip waiting on the job results
+    tasks = [cs.destroyVirtualMachine(id=vm['id'], fetch_result=False)
+             for vm in results]
+    await asyncio.wait(tasks)
+
 Links
 -----
 
diff --git a/cs/__init__.py b/cs/__init__.py
new file mode 100644
index 0000000..fe8f8e7
--- /dev/null
+++ b/cs/__init__.py
@@ -0,0 +1,109 @@
+import argparse
+import json
+import os
+import sys
+import time
+from collections import defaultdict
+
+try:
+    from configparser import NoSectionError
+except ImportError:  # python 2
+    from ConfigParser import NoSectionError
+
+try:
+    import pygments
+    from pygments.lexers import JsonLexer
+    from pygments.formatters import TerminalFormatter
+except ImportError:
+    pygments = None
+
+from .client import read_config, CloudStack, CloudStackException  # noqa
+
+
+__all__ = ['read_config', 'CloudStack', 'CloudStackException']
+
+if sys.version_info >= (3, 5):
+    try:
+        import aiohttp  # noqa
+    except ImportError:
+        pass
+    else:
+        from ._async import AIOCloudStack  # noqa
+        __all__.append('AIOCloudStack')
+
+
+def main():
+    parser = argparse.ArgumentParser(description='Cloustack client.')
+    parser.add_argument('--region', metavar='REGION',
+                        help='Cloudstack region in ~/.cloudstack.ini',
+                        default=os.environ.get('CLOUDSTACK_REGION',
+                                               'cloudstack'))
+    parser.add_argument('--post', action='store_true', default=False,
+                        help='use POST instead of GET')
+    parser.add_argument('--async', action='store_true', default=False,
+                        help='do not wait for async result')
+    parser.add_argument('--quiet', '-q', action='store_true', default=False,
+                        help='do not display additional status messages')
+    parser.add_argument('command', metavar="COMMAND",
+                        help='Cloudstack API command to execute')
+
+    def parse_option(x):
+        if '=' not in x:
+            raise ValueError("{!r} is not a correctly formatted "
+                             "option".format(x))
+        return x.split('=', 1)
+
+    parser.add_argument('arguments', metavar="OPTION=VALUE",
+                        nargs='*', type=parse_option,
+                        help='Cloudstack API argument')
+
+    options = parser.parse_args()
+    command = options.command
+    kwargs = defaultdict(set)
+    for arg in options.arguments:
+        key, value = arg
+        kwargs[key].add(value.strip(" \"'"))
+
+    try:
+        config = read_config(ini_group=options.region)
+    except NoSectionError:
+        raise SystemExit("Error: region '%s' not in config" % options.region)
+
+    if options.post:
+        config['method'] = 'post'
+    cs = CloudStack(**config)
+    ok = True
+    try:
+        response = getattr(cs, command)(**kwargs)
+    except CloudStackException as e:
+        response = e.args[1]
+        if not options.quiet:
+            sys.stderr.write("Cloudstack error: HTTP response "
+                             "{0}\n".format(response.status_code))
+            sys.stderr.write(response.text)
+            sys.exit(1)
+
+    if 'Async' not in command and 'jobid' in response and not options.async:
+        if not options.quiet:
+            sys.stderr.write("Polling result... ^C to abort\n")
+        while True:
+            try:
+                res = cs.queryAsyncJobResult(**response)
+                if res['jobstatus'] != 0:
+                    response = res
+                    if res['jobresultcode'] != 0:
+                        ok = False
+                    break
+                time.sleep(3)
+            except KeyboardInterrupt:
+                if not options.quiet:
+                    sys.stderr.write("Result not ready yet.\n")
+                break
+
+    data = json.dumps(response, indent=2, sort_keys=True)
+
+    if pygments and sys.stdout.isatty():
+        data = pygments.highlight(data, JsonLexer(), TerminalFormatter())
+    sys.stdout.write(data)
+    sys.stdout.write('\n')
+    sys.exit(int(not ok))
diff --git a/cs/__main__.py b/cs/__main__.py
new file mode 100644
index 0000000..daf5509
--- /dev/null
+++ b/cs/__main__.py
@@ -0,0 +1,5 @@
+from . import main
+
+
+if __name__ == '__main__':
+    main()
diff --git a/cs/_async.py b/cs/_async.py
new file mode 100644
index 0000000..4a08988
--- /dev/null
+++ b/cs/_async.py
@@ -0,0 +1,113 @@
+import asyncio
+import ssl
+
+import aiohttp
+
+from . import CloudStack, CloudStackException
+from .client import transform
+
+
+class AIOCloudStack(CloudStack):
+    def __init__(self, job_timeout=None, poll_interval=2.0,
+                 *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.job_timeout = job_timeout
+        self.poll_interval = poll_interval
+
+    def __getattr__(self, command):
+        async def handler(**kwargs):
+            return (await self._request(command, **kwargs))
+        return handler
+
+    async def _request(self, command, json=True, opcode_name='command',
+                       fetch_list=False, fetch_result=True, **kwargs):
+        kwarg, kwargs = self._prepare_request(command, json, opcode_name,
+                                              fetch_list, **kwargs)
+
+        ssl_context = None
+        if self.cert:
+            ssl_context = ssl.create_default_context(cafile=self.cert)
+        connector = aiohttp.TCPConnector(verify_ssl=self.verify,
+                                         ssl_context=ssl_context)
+
+        async with aiohttp.ClientSession(read_timeout=self.timeout,
+                                         conn_timeout=self.timeout,
+                                         connector=connector) as session:
+            handler = getattr(session, self.method)
+
+            done = False
+            final_data = []
+            page = 1
+            while not done:
+                if fetch_list:
+                    kwargs['page'] = page
+
+                kwargs = transform(kwargs)
+                kwargs.pop('signature', None)
+                kwargs['signature'] = self._sign(kwargs)
+                response = await handler(self.endpoint, **{kwarg: kwargs})
+
+                ctype = response.headers['content-type'].split(';')[0]
+                try:
+                    data = await response.json(content_type=ctype)
+                except ValueError as e:
+                    msg = "Make sure endpoint URL {!r} is correct.".format(
+                        self.endpoint)
+                    raise CloudStackException(
+                        "HTTP {0} response from CloudStack".format(
+                            response.status),
+                        response,
+                        "{}. {}".format(e, msg))
+
+                [key] = data.keys()
+                data = data[key]
+                if response.status != 200:
+                    raise CloudStackException(
+                        "HTTP {0} response from CloudStack".format(
+                            response.status), response, data)
+                if fetch_list:
+                    try:
+                        [key] = [k for k in data.keys() if k != 'count']
+                    except ValueError:
+                        done = True
+                    else:
+                        final_data.extend(data[key])
+                        page += 1
+                if fetch_result and 'jobid' in data:
+                    try:
+                        final_data = await asyncio.wait_for(
+                            self._jobresult(data['jobid']),
+                            self.job_timeout)
+                    except asyncio.TimeoutError:
+                        raise CloudStackException(
+                            "Timeout waiting for async job result",
+                            data['jobid'])
+                    done = True
+                else:
+                    final_data = data
+                    done = True
+            return final_data
+
+    async def _jobresult(self, jobid):
+        failures = 0
+        while True:
+            try:
+                j = await self.queryAsyncJobResult(jobid=jobid,
+                                                   fetch_result=False)
+                failures = 0
+                if j['jobstatus'] != 0:
+                    if j['jobresultcode'] != 0 or j['jobstatus'] != 1:
+                        raise CloudStackException("Job failure", j)
+                    if 'jobresult' not in j:
+                        raise CloudStackException("Unkonwn job result", j)
+                    return j['jobresult']
+
+            except CloudStackException:
+                raise
+
+            except Exception:
+                failures += 1
+                if failures > 10:
+                    raise
+
+            await asyncio.sleep(self.poll_interval)
diff --git a/cs.py b/cs/client.py
similarity index 66%
rename from cs.py
rename to cs/client.py
index 101c048..ec04fce 100644
--- a/cs.py
+++ b/cs/client.py
@@ -1,36 +1,23 @@
 #! /usr/bin/env python
 
-import argparse
 import base64
 import hashlib
 import hmac
-import json
 import os
 import sys
-import time
-
-from collections import defaultdict
 
 try:
-    from configparser import ConfigParser, NoSectionError
+    from configparser import ConfigParser
 except ImportError:  # python 2
-    from ConfigParser import ConfigParser, NoSectionError
+    from ConfigParser import ConfigParser
 
 try:
     from urllib.parse import quote
 except ImportError:  # python 2
     from urllib import quote
 
-try:
-    import pygments
-    from pygments.lexers import JsonLexer
-    from pygments.formatters import TerminalFormatter
-except ImportError:
-    pygments = None
-
 import requests
 
-
 PY2 = sys.version_info < (3, 0)
 
 if PY2:
@@ -42,6 +29,12 @@ else:
     string_type = str
     integer_types = int
 
+if sys.version_info >= (3, 5):
+    try:
+        from cs.async import AIOCloudStack  # noqa
+    except ImportError:
+        pass
+
 
 def cs_encode(value):
     """
@@ -110,8 +103,8 @@ class CloudStack(object):
             return self._request(command, **kwargs)
         return handler
 
-    def _request(self, command, json=True, opcode_name='command',
-                 fetch_list=False, **kwargs):
+    def _prepare_request(self, command, json, opcode_name, fetch_list,
+                         **kwargs):
         kwargs.update({
             'apiKey': self.key,
             opcode_name: command,
@@ -122,6 +115,12 @@ class CloudStack(object):
             kwargs.setdefault('pagesize', 500)
 
         kwarg = 'params' if self.method == 'get' else 'data'
+        return kwarg, kwargs
+
+    def _request(self, command, json=True, opcode_name='command',
+                 fetch_list=False, **kwargs):
+        kwarg, kwargs = self._prepare_request(command, json, opcode_name,
+                                              fetch_list, **kwargs)
 
         done = False
         final_data = []
@@ -222,84 +221,3 @@ def read_config(ini_group=None):
         cs_conf = dict(conf.items(ini_group))
     cs_conf['name'] = ini_group
     return cs_conf
-
-
-def main():
-    parser = argparse.ArgumentParser(description='Cloustack client.')
-    parser.add_argument('--region', metavar='REGION',
-                        help='Cloudstack region in ~/.cloudstack.ini',
-                        default=os.environ.get('CLOUDSTACK_REGION',
-                                               'cloudstack'))
-    parser.add_argument('--post', action='store_true', default=False,
-                        help='use POST instead of GET')
-    parser.add_argument('--async', action='store_true', default=False,
-                        help='do not wait for async result')
-    parser.add_argument('--quiet', '-q', action='store_true', default=False,
-                        help='do not display additional status messages')
-    parser.add_argument('command', metavar="COMMAND",
-                        help='Cloudstack API command to execute')
-
-    def parse_option(x):
-        if '=' not in x:
-            raise ValueError("{!r} is not a correctly formatted "
-                             "option".format(x))
-        return x.split('=', 1)
-
-    parser.add_argument('arguments', metavar="OPTION=VALUE",
-                        nargs='*', type=parse_option,
-                        help='Cloudstack API argument')
-
-    options = parser.parse_args()
-    command = options.command
-    kwargs = defaultdict(set)
-    for arg in options.arguments:
-        key, value = arg
-        kwargs[key].add(value.strip(" \"'"))
-
-    try:
-        config = read_config(ini_group=options.region)
-    except NoSectionError:
-        raise SystemExit("Error: region '%s' not in config" % options.region)
-
-    if options.post:
-        config['method'] = 'post'
-    cs = CloudStack(**config)
-    ok = True
-    try:
-        response = getattr(cs, command)(**kwargs)
-    except CloudStackException as e:
-        response = e.args[1]
-        if not options.quiet:
-            sys.stderr.write("Cloudstack error: HTTP response "
-                             "{0}\n".format(response.status_code))
-            sys.stderr.write(response.text)
-            sys.exit(1)
-
-    if 'Async' not in command and 'jobid' in response and not options.async:
-        if not options.quiet:
-            sys.stderr.write("Polling result... ^C to abort\n")
-        while True:
-            try:
-                res = cs.queryAsyncJobResult(**response)
-                if res['jobstatus'] != 0:
-                    response = res
-                    if res['jobresultcode'] != 0:
-                        ok = False
-                    break
-                time.sleep(3)
-            except KeyboardInterrupt:
-                if not options.quiet:
-                    sys.stderr.write("Result not ready yet.\n")
-                break
-
-    data = json.dumps(response, indent=2, sort_keys=True)
-
-    if pygments and sys.stdout.isatty():
-        data = pygments.highlight(data, JsonLexer(), TerminalFormatter())
-    sys.stdout.write(data)
-    sys.stdout.write('\n')
-    sys.exit(int(not ok))
-
-
-if __name__ == '__main__':
-    main()
diff --git a/setup.py b/setup.py
index 779c222..0760c94 100644
--- a/setup.py
+++ b/setup.py
@@ -1,19 +1,32 @@
 # coding: utf-8
-from setuptools import setup
+import sys
+import setuptools
+from setuptools import find_packages, setup
 
 with open('README.rst', 'r') as f:
     long_description = f.read()
 
+install_requires = ['requests']
+extras_require = {
+    'highlight': ['pygments'],
+}
+
+if int(setuptools.__version__.split(".", 1)[0]) < 18:
+    if sys.version_info[0:2] >= (3, 5):
+        install_requires.append("aiohttp")
+else:
+    extras_require[":python_version>='3.5'"] = ["aiohttp"]
+
 setup(
     name='cs',
-    version='1.1.1',
+    version='2.0.0',
     url='https://github.com/exoscale/cs',
     license='BSD',
     author=u'Bruno Renié',
     description=('A simple yet powerful CloudStack API client for '
                  'Python and the command-line.'),
     long_description=long_description,
-    py_modules=('cs',),
+    packages=find_packages(exclude=['tests']),
     zip_safe=False,
     include_package_data=True,
     platforms='any',
@@ -26,12 +39,8 @@ setup(
         'Programming Language :: Python :: 2',
         'Programming Language :: Python :: 3',
     ),
-    install_requires=(
-        'requests',
-    ),
-    extras_require={
-        'highlight': ['pygments'],
-    },
+    install_requires=install_requires,
+    extras_require=extras_require,
     test_suite='tests',
     entry_points={
         'console_scripts': [

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/python-cs.git



More information about the Python-modules-commits mailing list