diff --git a/.gitignore b/.gitignore index c88edd9..9c541d8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ output.xml parts/ report.html src/robotframework_httplibrary.egg-info/ +/build/ diff --git a/.travis.yml b/.travis.yml index d1d5fd2..e2a33b3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,10 @@ language: python -install: python bootstrap.py --distribute; ./bin/buildout versions:robotframework=$ROBOTFRAMEWORK_VERSION +install: python bootstrap.py --setuptools-version=19.3; ./bin/buildout versions:robotframework=$ROBOTFRAMEWORK_VERSION python: - "2.6" - "2.7" env: - - ROBOTFRAMEWORK_VERSION=2.6.3 - - ROBOTFRAMEWORK_VERSION=2.7.7 - - ROBOTFRAMEWORK_VERSION=2.8.1 + - ROBOTFRAMEWORK_VERSION=2.9.2 script: - ./bin/robotframework --monitorwidth 65 tests/ diff --git a/README.rst b/README.rst index 42a10f3..f961b54 100644 --- a/README.rst +++ b/README.rst @@ -74,6 +74,24 @@ mostly a wrapper supposed to have a nice API)! Changelog --------- +**v0.4.7** +- Improvement: Added PATCH HTTP method + +**v0.4.6** +- Improvement: Added stringify parameter to json_value_should_equal and json_value_should_not_equal methods + +**v0.4.5** +- Fix: Keep original data type returned by 'Get Json Value' keyword + +**v0.4.4** +- Fix: 'Set Json Value' keyword. + https://github.com/vikulin/robotframework-httplibrary/pull/1 + +**v0.4.3** + +- Fix: Added explicit OPTIONS method + https://github.com/peritus/robotframework-httplibrary/issues/30 + **v0.4.2** - Don't enforce ASCII when converting to JSON (so chinese characters are diff --git a/bootstrap.py b/bootstrap.py index 63aebb9..1594d67 100644 --- a/bootstrap.py +++ b/bootstrap.py @@ -1,6 +1,6 @@ ############################################################################## # -# Copyright (c) 2006 Zope Corporation and Contributors. +# Copyright (c) 2006 Zope Foundation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, @@ -16,98 +16,195 @@ 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. - -$Id: bootstrap.py 102545 2009-08-06 14:49:47Z chrisw $ """ -import os, shutil, sys, tempfile, urllib2 -from optparse import OptionParser - -tmpeggs = tempfile.mkdtemp() +import os +import shutil +import sys +import tempfile -is_jython = sys.platform.startswith('java') +from optparse import OptionParser -# parsing arguments -parser = OptionParser() -parser.add_option("-v", "--version", dest="version", - help="use a specific zc.buildout version") -parser.add_option("-d", "--distribute", - action="store_true", dest="distribute", default=True, - help="Use Disribute rather than Setuptools.") +__version__ = '2015-07-01' +# See zc.buildout's changelog if this version is up to date. + +tmpeggs = tempfile.mkdtemp(prefix='bootstrap-') + +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("--version", + action="store_true", default=False, + help=("Return bootstrap.py 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")) +parser.add_option("--allow-site-packages", + action="store_true", default=False, + help=("Let bootstrap.py use existing site packages")) +parser.add_option("--buildout-version", + help="Use a specific zc.buildout version") +parser.add_option("--setuptools-version", + help="Use a specific setuptools version") +parser.add_option("--setuptools-to-dir", + help=("Allow for re-use of existing directory of " + "setuptools versions")) options, args = parser.parse_args() +if options.version: + print(("bootstrap.py version %s" % __version__)) + sys.exit(0) -if options.version is not None: - VERSION = '==%s' % options.version -else: - VERSION = '' -USE_DISTRIBUTE = options.distribute -args = args + ['bootstrap'] +###################################################################### +# load/install setuptools -to_reload = False try: - import pkg_resources - if not hasattr(pkg_resources, '_distribute'): - to_reload = True - raise ImportError + from urllib.request import urlopen except ImportError: - ez = {} - if USE_DISTRIBUTE: - exec urllib2.urlopen('http://python-distribute.org/distribute_setup.py' - ).read() in ez - ez['use_setuptools'](to_dir=tmpeggs, download_delay=0, no_fake=True) - else: - exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py' - ).read() in ez - ez['use_setuptools'](to_dir=tmpeggs, download_delay=0) - - if to_reload: - reload(pkg_resources) - else: - import pkg_resources - -if sys.platform == 'win32': - def quote(c): - if ' ' in c: - return '"%s"' % c # work around spawn lamosity on windows - else: - return c -else: - def quote (c): - return c + from urllib.request import urlopen -cmd = 'from setuptools.command.easy_install import main; main()' -ws = pkg_resources.working_set - -if USE_DISTRIBUTE: - requirement = 'distribute' +ez = {} +if os.path.exists('ez_setup.py'): + exec(open('ez_setup.py').read(), ez) else: - requirement = 'setuptools' + exec(urlopen('https://bootstrap.pypa.io/ez_setup.py').read(), ez) + +if not options.allow_site_packages: + # ez_setup imports site, which adds site packages + # this will remove them from the path to ensure that incompatible versions + # of setuptools are not in the path + import site + # inside a virtualenv, there is no 'getsitepackages'. + # We can't remove these reliably + if hasattr(site, 'getsitepackages'): + for sitepackage_path in site.getsitepackages(): + # Strip all site-packages directories from sys.path that + # are not sys.prefix; this is because on Windows + # sys.prefix is a site-package directory. + if sitepackage_path != sys.prefix: + sys.path[:] = [x for x in sys.path + if sitepackage_path not in x] + +setup_args = dict(to_dir=tmpeggs, download_delay=0) + +if options.setuptools_version is not None: + setup_args['version'] = options.setuptools_version +if options.setuptools_to_dir is not None: + setup_args['to_dir'] = options.setuptools_to_dir + +ez['use_setuptools'](**setup_args) +import setuptools +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 + +setuptools_path = ws.find( + pkg_resources.Requirement.parse('setuptools')).location + +# Fix sys.path here as easy_install.pth added before PYTHONPATH +cmd = [sys.executable, '-c', + 'import sys; sys.path[0:0] = [%r]; ' % setuptools_path + + '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]) + +requirement = 'zc.buildout' +version = options.buildout_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): + try: + return not parsed_version.is_prerelease + except AttributeError: + # Older setuptools + 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) != 0: + raise Exception( + "Failed to execute command:\n%s" % repr(cmd)[1:-1]) + +###################################################################### +# Import and run buildout -if is_jython: - import subprocess +ws.add_entry(tmpeggs) +ws.require(requirement) +import zc.buildout.buildout - assert subprocess.Popen([sys.executable] + ['-c', quote(cmd), '-mqNxd', - quote(tmpeggs), 'zc.buildout' + VERSION], - env=dict(os.environ, - PYTHONPATH= - ws.find(pkg_resources.Requirement.parse(requirement)).location - ), - ).wait() == 0 +if not [a for a in args if '=' not in a]: + args.append('bootstrap') -else: - assert os.spawnle( - os.P_WAIT, sys.executable, quote (sys.executable), - '-c', quote (cmd), '-mqNxd', quote (tmpeggs), 'zc.buildout' + VERSION, - dict(os.environ, - PYTHONPATH= - ws.find(pkg_resources.Requirement.parse(requirement)).location - ), - ) == 0 +# 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] -ws.add_entry(tmpeggs) -ws.require('zc.buildout' + VERSION) -import zc.buildout.buildout zc.buildout.buildout.main(args) shutil.rmtree(tmpeggs) diff --git a/buildout.cfg b/buildout.cfg index 180f1d0..9b37afb 100644 --- a/buildout.cfg +++ b/buildout.cfg @@ -12,7 +12,7 @@ recipe = zc.recipe.egg eggs = robotframework-httplibrary [versions] -robotframework = 2.7.5 +robotframework = 3.0.0 [robotframework] recipe = zc.recipe.egg diff --git a/setup.py b/setup.py index 42ef4c1..b688b97 100644 --- a/setup.py +++ b/setup.py @@ -12,19 +12,18 @@ setup( name='robotframework-httplibrary', - version="0.4.2", + version="0.4.7", description='Robot Framework keywords for HTTP requests', long_description=long_description, - author='Filip Noetzel', - author_email='filip+rfhttplibrary@j03.de', - url='https://github.com/peritus/robotframework-httplibrary', + author='Vadym Vikulin', + author_email='vadym.vikulin@gmail.com', + url='https://github.com/vikulin/robotframework-httplibrary', license='Beerware', - keywords='robotframework testing testautomation web http webtest', + keywords='robotframework testing test automation web http webtest', platforms='any', zip_safe=False, classifiers=CLASSIFIERS.splitlines(), package_dir={'': 'src'}, - install_requires=['robotframework', 'webtest>=2.0', 'jsonpatch', - 'jsonpointer'], + install_requires=['robotframework', 'webtest>=2.0', 'jsonpatch', 'jsonpointer'], packages=['HttpLibrary'] ) diff --git a/src/HttpLibrary/__init__.py b/src/HttpLibrary/__init__.py index ae11328..b70750a 100644 --- a/src/HttpLibrary/__init__.py +++ b/src/HttpLibrary/__init__.py @@ -2,9 +2,10 @@ from base64 import b64encode from functools import wraps -from urlparse import urlparse +from urllib.parse import urlparse +from webtest import utils -import livetest +from . import livetest import json import jsonpointer import jsonpatch @@ -13,7 +14,7 @@ def load_json(json_string): try: return json.loads(json_string) - except ValueError, e: + except ValueError as e: raise ValueError("Could not parse '%s' as JSON: %s" % (json_string, e)) @@ -35,7 +36,7 @@ class HTTP: Pointer, go to http://tools.ietf.org/html/draft-pbryan-zyp-json-pointer-00. """ - ROBOT_LIBRARY_VERSION = "0.4.2" + ROBOT_LIBRARY_VERSION = "0.4.7" class Context(object): def __init__(self, http, host=None, scheme='http'): @@ -64,9 +65,9 @@ def __init__(self, http, host=None, scheme='http'): self.post_process_request(None) def pre_process_request(self): - if len(self.request_headers.items()) > 0: + if len(list(self.request_headers.items())) > 0: logger.debug("Request headers:") - for name, value in self.request_headers.items(): + for name, value in list(self.request_headers.items()): logger.debug("%s: %s" % (name, value)) else: logger.debug("No request headers set") @@ -138,6 +139,9 @@ def response(self): def _path_from_url_or_path(self, url_or_path): + if url_or_path.startswith("\"") and url_or_path.endswith("\""): + url_or_path = url_or_path[1:-1] + if url_or_path.startswith("/"): return url_or_path @@ -197,7 +201,8 @@ def http_request(self, verb, url): logger.debug("Performing %s request on %s://%s%s" % (verb, self.context._scheme, self.app.host, path,)) self.context.post_process_request( - self.context.app.request(path, {}, self.context.request_headers, + self.context.app.request(path, {}, + headers=self.context.request_headers, method=verb.upper(),) ) @@ -267,6 +272,25 @@ def PUT(self, url): self.context.request_headers, **kwargs) ) + def PATCH(self, url): + """ + Issues an HTTP PATCH request. + + `url` is the URL relative to the server root, e.g. '/_utils/config.html' + """ + path = self._path_from_url_or_path(url) + kwargs = {} + if 'Content-Type' in self.context.request_headers: + kwargs[ + 'content_type'] = self.context.request_headers['Content-Type'] + self.context.pre_process_request() + logger.debug("Performing PATCH request on %s://%s%s" % ( + self.context._scheme, self.app.host, url)) + self.context.post_process_request( + self.app.patch(path, self.context.request_body or {}, + self.context.request_headers, **kwargs) + ) + def DELETE(self, url): """ Issues a HTTP DELETE request. @@ -278,7 +302,54 @@ def DELETE(self, url): logger.debug("Performing DELETE request on %s://%s%s" % ( self.context._scheme, self.app.host, url)) self.context.post_process_request( - self.app.delete(path, {}, self.context.request_headers) + self.app.delete(path, utils.NoDefault, self.context.request_headers) + ) + + def OPTIONS(self, url): + """ + Issues a HTTP OPTIONS request. + + `url` is the URL relative to the server root, e.g. '/_utils/config.html' + """ + path = self._path_from_url_or_path(url) + self.context.pre_process_request() + logger.debug("Performing OPTIONS request on %s://%s%s" % ( + self.context._scheme, self.app.host, path,)) + self.context.post_process_request( + self.app.options(path, self.context.request_headers) + ) + + def OPTIONS(self, url): + """ + Issues a HTTP OPTIONS request. + + `url` is the URL relative to the server root, e.g. '/_utils/config.html' + """ + path = self._path_from_url_or_path(url) + self.context.pre_process_request() + logger.debug("Performing OPTIONS request on %s://%s%s" % ( + self.context._scheme, self.app.host, path)) + self.context.post_process_request( + self.app.options(path, self.context.request_headers) + ) + + def PATCH(self, url): + """ + Issues a HTTP PATCH request. + + `url` is the URL relative to the server root, e.g. '/_utils/config.html' + """ + path = self._path_from_url_or_path(url) + kwargs = {} + if 'Content-Type' in self.context.request_headers: + kwargs[ + 'content_type'] = self.context.request_headers['Content-Type'] + self.context.pre_process_request() + logger.debug("Performing PATCH request on %s://%s%s" % ( + self.context._scheme, self.app.host, url)) + self.context.post_process_request( + self.app.patch(path, self.context.request_body or {}, + self.context.request_headers, **kwargs) ) def follow_response(self): @@ -417,7 +488,7 @@ def log_response_headers(self, log_level='INFO'): Specify `log_level` (default: "INFO") to set the log level. """ logger.write("Response headers:", log_level) - for name, value in self.response.headers.items(): + for name, value in list(self.response.headers.items()): logger.write("%s: %s" % (name, value), log_level) # request headers @@ -431,7 +502,7 @@ def set_request_header(self, header_name, header_value): """ logger.info( 'Set request header "%s" to "%s"' % (header_name, header_value)) - self.context.request_headers[header_name] = header_value + self.context.request_headers[str(header_name)] = header_value def set_basic_auth(self, username, password): """ @@ -471,7 +542,7 @@ def get_response_body(self): | ${body}= | Get Response Body | | | Should Start With | ${body} |