[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