From e72a2c3172c09a5ef84509a82de1bb6876c40a1c Mon Sep 17 00:00:00 2001 From: kunzimariano Date: Mon, 2 May 2016 17:15:01 -0300 Subject: [PATCH 1/7] First unit tests for client.py --- setup.py | 1 + v1pysdk/tests/__init__.py | 2 +- v1pysdk/tests/client_tests.py | 51 +++++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 v1pysdk/tests/client_tests.py diff --git a/setup.py b/setup.py index 7b5d540..15774cd 100644 --- a/setup.py +++ b/setup.py @@ -21,6 +21,7 @@ 'testtools', 'iso8601', 'python-ntlm', + 'mock' ], test_suite = "v1pysdk.tests", diff --git a/v1pysdk/tests/__init__.py b/v1pysdk/tests/__init__.py index e494681..c1fce79 100644 --- a/v1pysdk/tests/__init__.py +++ b/v1pysdk/tests/__init__.py @@ -1,3 +1,3 @@ import connect_tests -import meta_tests +import string_utils_tests diff --git a/v1pysdk/tests/client_tests.py b/v1pysdk/tests/client_tests.py new file mode 100644 index 0000000..3dacbc1 --- /dev/null +++ b/v1pysdk/tests/client_tests.py @@ -0,0 +1,51 @@ +from v1pysdk.client import V1Server +from mock import * +import unittest + +class TestClient(unittest.TestCase): + + @patch('v1pysdk.client.urllib2.HTTPPasswordMgrWithDefaultRealm') + @patch('v1pysdk.client.urllib2.build_opener') + def setUp(self, build_opener, HTTPPasswordMgr): + self.manager = Mock() + self.opener = Mock() + + build_opener.return_value = self.opener + HTTPPasswordMgr.return_value = self.manager + + self.server = V1Server() + + def tearDown(self): + print "nothing to do here" + + def test_constructor(self): + assert self.opener.add_handler.call_count is 1 + assert self.manager.add_password.call_count is 1 + + @patch('v1pysdk.client.Request') + def test_http_get(self, request): + r = Mock() + request.return_value = r + + self.server.http_get("http://get.com") + + request.assert_called_with("http://get.com") + r.add_header.assert_called_with("Content-Type", "text/xml;charset=UTF-8") + self.opener.open.assert_called_with(r) + + @patch('v1pysdk.client.Request') + def test_http_post(self, request): + r = Mock() + request.return_value = r + data = Mock() + + self.server.http_post("http://post.com", data) + + request.assert_called_with("http://post.com", data) + r.add_header.assert_called_with("Content-Type", "text/xml;charset=UTF-8") + self.opener.open.assert_called_with(r) + + if __name__ == "__main__": + unittest.main() + + From 999c91b134b558d575b250c8611d1c336a436302 Mon Sep 17 00:00:00 2001 From: kunzimariano Date: Mon, 2 May 2016 17:45:42 -0300 Subject: [PATCH 2/7] Added test_build_url. --- v1pysdk/tests/client_tests.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/v1pysdk/tests/client_tests.py b/v1pysdk/tests/client_tests.py index 3dacbc1..1455306 100644 --- a/v1pysdk/tests/client_tests.py +++ b/v1pysdk/tests/client_tests.py @@ -3,6 +3,11 @@ import unittest class TestClient(unittest.TestCase): + address = 'local' + instance = 'V1' + username = 'usr' + password = 'pass' + scheme = 'https' @patch('v1pysdk.client.urllib2.HTTPPasswordMgrWithDefaultRealm') @patch('v1pysdk.client.urllib2.build_opener') @@ -13,10 +18,10 @@ def setUp(self, build_opener, HTTPPasswordMgr): build_opener.return_value = self.opener HTTPPasswordMgr.return_value = self.manager - self.server = V1Server() + self.server = V1Server(self.address, self.instance, self.username, self.password, self.scheme) def tearDown(self): - print "nothing to do here" + print 'nothing to do here' def test_constructor(self): assert self.opener.add_handler.call_count is 1 @@ -27,10 +32,10 @@ def test_http_get(self, request): r = Mock() request.return_value = r - self.server.http_get("http://get.com") + self.server.http_get('http://get.com') - request.assert_called_with("http://get.com") - r.add_header.assert_called_with("Content-Type", "text/xml;charset=UTF-8") + request.assert_called_with('http://get.com') + r.add_header.assert_called_with('Content-Type', 'text/xml;charset=UTF-8') self.opener.open.assert_called_with(r) @patch('v1pysdk.client.Request') @@ -39,13 +44,17 @@ def test_http_post(self, request): request.return_value = r data = Mock() - self.server.http_post("http://post.com", data) + self.server.http_post('http://post.com', data) - request.assert_called_with("http://post.com", data) - r.add_header.assert_called_with("Content-Type", "text/xml;charset=UTF-8") + request.assert_called_with('http://post.com', data) + r.add_header.assert_called_with('Content-Type', 'text/xml;charset=UTF-8') self.opener.open.assert_called_with(r) - if __name__ == "__main__": + def test_build_url(self): + url = self.server.build_url('mypath', {'a': 'b'}, 'myfragment') + self.assertEqual('https://local/V1/mypath?a=b#myfragment', url) + + if __name__ == '__main__': unittest.main() From 6feeb5c553f0c1a9fc147f8a838ab6c48f57859c Mon Sep 17 00:00:00 2001 From: kunzimariano Date: Wed, 4 May 2016 13:55:22 -0300 Subject: [PATCH 3/7] Refactored V1Server by extracting some logic into the new HttpClient class. --- v1pysdk/client.py | 349 ++++++++++++++--------------- v1pysdk/query.py | 30 ++- v1pysdk/tests/connect_tests.py | 6 +- v1pysdk/tests/http_client_tests.py | 47 ++++ 4 files changed, 230 insertions(+), 202 deletions(-) create mode 100644 v1pysdk/tests/http_client_tests.py diff --git a/v1pysdk/client.py b/v1pysdk/client.py index f579d2f..01b2efd 100644 --- a/v1pysdk/client.py +++ b/v1pysdk/client.py @@ -1,7 +1,6 @@ - -import logging, time, base64 +import logging import urllib2 -from urllib2 import Request, urlopen, HTTPError, HTTPBasicAuthHandler, HTTPCookieProcessor +from urllib2 import Request, HTTPError, HTTPBasicAuthHandler, HTTPCookieProcessor from urllib import urlencode from urlparse import urlunparse, urlparse @@ -27,197 +26,181 @@ class CustomHTTPNtlmAuthHandler(HTTPNtlmAuthHandler): call chain, most importantly `HTTPErrorProcessor.http_response`, which normally raises an error for 'bad' http status codes.. """ + def http_error_401(self, req, fp, code, msg, hdrs): response = HTTPNtlmAuthHandler.http_error_401(self, req, fp, code, msg, hdrs) if not (200 <= response.code < 300): response = self.parent.error( - 'http', req, response, response.code, response.msg, - response.info) + 'http', req, response, response.code, response.msg, + response.info) return response - AUTH_HANDLERS.append(CustomHTTPNtlmAuthHandler) - + AUTH_HANDLERS.append(CustomHTTPNtlmAuthHandler) class V1Error(Exception): pass + class V1AssetNotFoundError(V1Error): pass -class V1Server(object): - "Accesses a V1 HTTP server as a client of the XML API protocol" - - def __init__(self, address="localhost", instance="VersionOne.Web", username='', password='', scheme="http", instance_url=None, logparent=None, loglevel=logging.ERROR): - if instance_url: - self.instance_url = instance_url - parsed = urlparse(instance_url) - self.address = parsed.netloc - self.instance = parsed.path.strip('/') - self.scheme = parsed.scheme - else: - self.address = address - self.instance = instance.strip('/') - self.scheme = scheme - self.instance_url = self.build_url('') - - modulelogname='v1pysdk.client' - logname = "%s.%s" % (logparent, modulelogname) if logparent else None - self.logger = logging.getLogger(logname) - self.logger.setLevel(loglevel) - self.username = username - self.password = password - self._install_opener() - - def _install_opener(self): - base_url = self.build_url('') - password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() - password_manager.add_password(None, base_url, self.username, self.password) - handlers = [HandlerClass(password_manager) for HandlerClass in AUTH_HANDLERS] - self.opener = urllib2.build_opener(*handlers) - self.opener.add_handler(HTTPCookieProcessor()) - - def http_get(self, url): - request = Request(url) - request.add_header("Content-Type", "text/xml;charset=UTF-8") - response = self.opener.open(request) - return response - - def http_post(self, url, data=''): - request = Request(url, data) - request.add_header("Content-Type", "text/xml;charset=UTF-8") - response = self.opener.open(request) - return response - - def build_url(self, path, query='', fragment='', params=''): - "So we dont have to interpolate urls ad-hoc" - path = self.instance + '/' + path.strip('/') - if isinstance(query, dict): - query = urlencode(query) - url = urlunparse( (self.scheme, self.address, path, params, query, fragment) ) - return url - - def _debug_headers(self, headers): - self.logger.debug("Headers:") - for hdr in str(headers).split('\n'): - self.logger.debug(" %s" % hdr) - - def _debug_body(self, body, headers): - try: - ctype = headers['content-type'] - except AttributeError: - ctype = None - if ctype is not None and ctype[:5] == 'text/': - self.logger.debug("Body:") - for line in str(body).split('\n'): - self.logger.debug(" %s" % line) - else: - self.logger.debug("Body: non-textual content (Content-Type: %s). Not logged." % ctype) - - def fetch(self, path, query='', postdata=None): - "Perform an HTTP GET or POST depending on whether postdata is present" - url = self.build_url(path, query=query) - self.logger.debug("URL: %s" % url) - try: - if postdata is not None: - if isinstance(postdata, dict): - postdata = urlencode(postdata) - self.logger.debug("postdata: %s" % postdata) - response = self.http_post(url, postdata) - else: - response = self.http_get(url) - body = response.read() - self._debug_headers(response.headers) - self._debug_body(body, response.headers) - return (None, body) - except HTTPError, e: - if e.code == 401: - raise - body = e.fp.read() - self._debug_headers(e.headers) - self._debug_body(body, e.headers) - return (e, body) - - def handle_non_xml_response(self, body, exception, msg, postdata): - if exception.code >= 500: - # 5XX error codes mean we won't have an XML response to parse - self.logger.error("{0} during {1}".format(exception, msg)) - if postdata is not None: - self.logger.error(postdata) - raise exception - - def get_xml(self, path, query='', postdata=None): - verb = "HTTP POST to " if postdata else "HTTP GET from " - msg = verb + path - self.logger.info(msg) - exception, body = self.fetch(path, query=query, postdata=postdata) - if exception: - self.handle_non_xml_response(body, exception, msg, postdata) - - self.logger.warn("{0} during {1}".format(exception, msg)) - if postdata is not None: - self.logger.warn(postdata) - document = ElementTree.fromstring(body) - if exception: - exception.xmldoc = document - if exception.code == 404: - raise V1AssetNotFoundError(exception) - elif exception.code == 400: - raise V1Error('\n'+body) - else: - raise V1Error(exception) - return document - - def get_asset_xml(self, asset_type_name, oid, moment=None): - path = '/rest-1.v1/Data/{0}/{1}/{2}'.format(asset_type_name, oid, moment) if moment else '/rest-1.v1/Data/{0}/{1}'.format(asset_type_name, oid) - return self.get_xml(path) - - def get_query_xml(self, asset_type_name, where=None, sel=None): - path = '/rest-1.v1/Data/{0}'.format(asset_type_name) - query = {} - if where is not None: - query['Where'] = where - if sel is not None: - query['sel'] = sel - return self.get_xml(path, query=query) - - def get_meta_xml(self, asset_type_name): - path = '/meta.v1/{0}'.format(asset_type_name) - return self.get_xml(path) - - def execute_operation(self, asset_type_name, oid, opname): - path = '/rest-1.v1/Data/{0}/{1}'.format(asset_type_name, oid) - query = {'op': opname} - return self.get_xml(path, query=query, postdata={}) - - def get_attr(self, asset_type_name, oid, attrname, moment=None): - path = '/rest-1.v1/Data/{0}/{1}/{3}/{2}'.format(asset_type_name, oid, attrname, moment) if moment else '/rest-1.v1/Data/{0}/{1}/{2}'.format(asset_type_name, oid, attrname) - return self.get_xml(path) - - def create_asset(self, asset_type_name, xmldata, context_oid=''): - body = ElementTree.tostring(xmldata, encoding="utf-8") - query = {} - if context_oid: - query = {'ctx': context_oid} - path = '/rest-1.v1/Data/{0}'.format(asset_type_name) - return self.get_xml(path, query=query, postdata=body) - - def update_asset(self, asset_type_name, oid, update_doc): - newdata = ElementTree.tostring(update_doc, encoding='utf-8') - path = '/rest-1.v1/Data/{0}/{1}'.format(asset_type_name, oid) - return self.get_xml(path, postdata=newdata) - - - def get_attachment_blob(self, attachment_id, blobdata=None): - path = '/attachment.v1/{0}'.format(attachment_id) - exception, body = self.fetch(path, postdata=blobdata) - if exception: - raise exception - return body - - set_attachment_blob = get_attachment_blob - - - - - +class V1Server(object): + "Accesses a V1 HTTP server as a client of the XML API protocol" + def __init__(self, address="localhost", instance="VersionOne.Web", username='', password='', scheme="http", + instance_url=None, logparent=None, loglevel=logging.ERROR): + if instance_url: + self.instance_url = instance_url + parsed = urlparse(instance_url) + self.address = parsed.netloc + self.instance = parsed.path.strip('/') + self.scheme = parsed.scheme + else: + self.address = address + self.instance = instance.strip('/') + self.scheme = scheme + self.instance_url = self.build_url('') + + modulelogname = 'v1pysdk.client' + logname = "%s.%s" % (logparent, modulelogname) if logparent else None + self.logger = logging.getLogger(logname) + self.logger.setLevel(loglevel) + base_url = self.build_url('') + self.http_client = HttpClient(base_url, username, password) + + def build_url(self, path, query='', fragment='', params=''): + "So we dont have to interpolate urls ad-hoc" + path = self.instance + '/' + path.strip('/') + if isinstance(query, dict): + query = urlencode(query) + url = urlunparse((self.scheme, self.address, path, params, query, fragment)) + return url + + def __handle_non_xml_response(self, body, exception, msg, postdata): + if exception.code >= 500: + # 5XX error codes mean we won't have an XML response to parse + self.logger.error("{0} during {1}".format(exception, msg)) + if postdata is not None: + self.logger.error(postdata) + raise exception + + def __handle_http_exception(self, exception, document, body): + if exception: + exception.xmldoc = document + if exception.code == 404: + raise V1AssetNotFoundError(exception) + elif exception.code == 400: + raise V1Error('\n' + body) + else: + raise V1Error(exception) + + def __parse_xml(self, body, exception): + if exception: + self.__handle_non_xml_response(body, exception, '', postdata=None) + document = ElementTree.fromstring(body) + self.__handle_http_exception(exception, document, body) + return document + + def get_asset_xml(self, asset_type_name, oid, moment=None): + path = '/rest-1.v1/Data/{0}/{1}/{2}'.format(asset_type_name, oid, + moment) if moment else '/rest-1.v1/Data/{0}/{1}'.format( + asset_type_name, oid) + url = self.build_url(path) + exception, body = self.http_client.get(url) + return self.__parse_xml(body, exception) + + def get_query_xml(self, asset_type_name, where=None, sel=None): + path = '/rest-1.v1/Data/{0}'.format(asset_type_name) + query = {} + if where is not None: + query['Where'] = where + if sel is not None: + query['sel'] = sel + url = self.build_url(path, query) + exception, body = self.http_client.get(url) + return self.__parse_xml(body, exception) + + def get_meta_xml(self, asset_type_name): + path = '/meta.v1/{0}'.format(asset_type_name) + url = self.build_url(path) + exception, body = self.http_client.get(url) + return self.__parse_xml(body, exception) + + def execute_operation(self, asset_type_name, oid, opname): + path = '/rest-1.v1/Data/{0}/{1}'.format(asset_type_name, oid) + query = {'op': opname} + url = self.build_url(path, query) + exception, body = self.http_client.post(url, postdata={}) + return self.__parse_xml(body, exception) + + def get_attr(self, asset_type_name, oid, attrname, moment=None): + path = '/rest-1.v1/Data/{0}/{1}/{3}/{2}'.format(asset_type_name, oid, attrname, + moment) if moment else '/rest-1.v1/Data/{0}/{1}/{2}'.format( + asset_type_name, oid, attrname) + url = self.build_url(path) + exception, body = self.http_client.get(url) + return self.__parse_xml(body, exception) + + def create_asset(self, asset_type_name, xmldata, context_oid=''): + body = ElementTree.tostring(xmldata, encoding="utf-8") + query = {} + if context_oid: + query = {'ctx': context_oid} + path = '/rest-1.v1/Data/{0}'.format(asset_type_name) + url = self.build_url(path, query) + exception, body = self.http_client.post(url, body) + return self.__parse_xml(body, exception) + + def update_asset(self, asset_type_name, oid, update_doc): + newdata = ElementTree.tostring(update_doc, encoding='utf-8') + path = '/rest-1.v1/Data/{0}/{1}'.format(asset_type_name, oid) + url = self.build_url(path) + exception, body = self.http_client.post(url, newdata) + return self.__parse_xml(body, exception) + + def get_attachment_blob(self, attachment_id, blobdata=None): + path = '/attachment.v1/{0}'.format(attachment_id) + #TODO: post or get? + exception, body = self.__http_get(path) + if exception: + raise exception + return body + + set_attachment_blob = get_attachment_blob + + +class HttpClient(object): + def __init__(self, base_url, username, password): + password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() + password_manager.add_password(None, base_url, username, password) + handlers = [HandlerClass(password_manager) for HandlerClass in AUTH_HANDLERS] + self.opener = urllib2.build_opener(*handlers) + self.opener.add_handler(HTTPCookieProcessor()) + + def get(self, url): + try: + request = Request(url) + request.add_header("Content-Type", "text/xml;charset=UTF-8") + response = self.opener.open(request) + body = response.read() + return (None, body) + except HTTPError, e: + if e.code == 401: + raise + body = e.fp.read() + return (e, body) + + def post(self, url, postdata): + try: + if isinstance(postdata, dict): + postdata = urlencode(postdata) + request = Request(url, postdata) + request.add_header("Content-Type", "text/xml;charset=UTF-8") + response = self.opener.open(request) + body = response.read() + return (None, body) + except HTTPError, e: + if e.code == 401: + raise + body = e.fp.read() + return (e, body) diff --git a/v1pysdk/query.py b/v1pysdk/query.py index 9471626..1f95178 100644 --- a/v1pysdk/query.py +++ b/v1pysdk/query.py @@ -5,7 +5,7 @@ class V1Query(object): """A fluent query object. Use .select() and .where() to add items to the select list and the query criteria, then iterate over the object to execute and use the query results.""" - + def __init__(self, asset_class, sel_string=None, filterexpr=None): "Takes the asset class we will be querying" self.asset_class = asset_class @@ -17,7 +17,7 @@ def __init__(self, asset_class, sel_string=None, filterexpr=None): self.sel_string = sel_string self.empty_sel = sel_string is None self.where_string = filterexpr - + def __iter__(self): "Iterate over the results." if not self.query_has_run: @@ -25,7 +25,7 @@ def __iter__(self): for (result, asof) in self.query_results: for found_asset in result.findall('Asset'): yield self.asset_class.from_query_select(found_asset, asof) - + def get_sel_string(self): if self.sel_string: return self.sel_string @@ -36,14 +36,12 @@ def get_where_string(self): if self.where_string: terms.append(self.where_string) return ';'.join(terms) - + def run_single_query(self, url_params={}, api="Data"): - urlquery = urlencode(url_params) - urlpath = '/rest-1.v1/{1}/{0}'.format(self.asset_class._v1_asset_type_name, api) # warning: tight coupling ahead - xml = self.asset_class._v1_v1meta.server.get_xml(urlpath, query=urlquery) + xml = self.asset_class._v1_v1meta.server.get_query_xml(self.asset_class._v1_asset_type_name) return xml - + def run_query(self): "Actually hit the server to perform the query" url_params = {} @@ -65,12 +63,12 @@ def run_query(self): xml = self.run_single_query(url_params) self.query_results.append((xml, None)) self.query_has_run = True - + def select(self, *args, **kw): """Add attribute names to the select list for this query. The attributes in the select list will be returned in the query results, and can be used without further network traffic""" - + for sel in args: parts = split_attribute(sel) for i in range(1, len(parts)): @@ -79,18 +77,18 @@ def select(self, *args, **kw): self.sel_list.append(pname) self.sel_list.append(sel) return self - + def where(self, terms={}, **kw): """Add where terms to the criteria for this query. Right now this method only allows Equals comparisons.""" self.where_terms.update(terms) self.where_terms.update(kw) return self - + def filter(self, filterexpr): self.where_string = filterexpr return self - + def asof(self, *asofs): for asof_list in asofs: if isinstance(asof_list, str): @@ -98,14 +96,14 @@ def asof(self, *asofs): for asof in asof_list: self.asof_list.append(asof) return self - + def first(self): return list(self)[0] - + def set(self, **updatelist): for found_asset in self: found_asset.pending(updatelist) - + def __getattr__(self, attrname): """ Return a sequence of the attribute from all matched results diff --git a/v1pysdk/tests/connect_tests.py b/v1pysdk/tests/connect_tests.py index a5eb7e9..b76bc71 100644 --- a/v1pysdk/tests/connect_tests.py +++ b/v1pysdk/tests/connect_tests.py @@ -1,14 +1,14 @@ from testtools import TestCase from testtools.matchers import Equals -from elementtree.ElementTree import parse, fromstring, ElementTree +from elementtree.ElementTree import fromstring from v1pysdk.client import * class TestV1Connection(TestCase): def test_connect(self, username='admin', password='admin'): - server = V1Server(address='www14.v1host.com', username=username, password=password,instance='v1sdktesting') - code, body = server.fetch('/rest-1.v1/Data/Story?sel=Name') + client = HttpClient('http://www14.v1host.com/v1sdktesting', username=username, password=password) + code, body = client.get('http://www14.v1host.com/v1sdktesting/rest-1.v1/Data/Story?sel=Name') print "\n\nCode: ", code print "Body: ", body elem = fromstring(body) diff --git a/v1pysdk/tests/http_client_tests.py b/v1pysdk/tests/http_client_tests.py new file mode 100644 index 0000000..04d9d33 --- /dev/null +++ b/v1pysdk/tests/http_client_tests.py @@ -0,0 +1,47 @@ +from v1pysdk.client import HttpClient +from mock import * +import unittest + +class TestHttpClient(unittest.TestCase): + base_url = 'https://localhost/VersionOne/' + username = 'usr' + password = 'pass' + + @patch('v1pysdk.client.urllib2.HTTPPasswordMgrWithDefaultRealm') + @patch('v1pysdk.client.urllib2.build_opener') + def setUp(self, build_opener, HTTPPasswordMgr): + self.manager = Mock() + self.opener = Mock() + build_opener.return_value = self.opener + HTTPPasswordMgr.return_value = self.manager + self.http_client = HttpClient(self.base_url, self.username, self.password) + + def test_constructor(self): + self.assertEqual(1, self.opener.add_handler.call_count) + self.assertEqual(1, self.manager.add_password.call_count) + + @patch('v1pysdk.client.Request') + def test_get(self, request): + r = Mock() + request.return_value = r + + self.http_client.get('http://get.com') + + request.assert_called_with('http://get.com') + r.add_header.assert_called_with('Content-Type', 'text/xml;charset=UTF-8') + self.opener.open.assert_called_with(r) + + @patch('v1pysdk.client.Request') + def test_post(self, request): + r = Mock() + request.return_value = r + data = Mock() + + self.http_client.post('http://post.com', data) + + request.assert_called_with('http://post.com', data) + r.add_header.assert_called_with('Content-Type', 'text/xml;charset=UTF-8') + self.opener.open.assert_called_with(r) + + if __name__ == '__main__': + unittest.main() From 88cdc190c47d8ceca5705c558e90004058350200 Mon Sep 17 00:00:00 2001 From: kunzimariano Date: Thu, 5 May 2016 17:09:28 -0300 Subject: [PATCH 4/7] Fixed query since it was broken after the refactor. --- v1pysdk/client.py | 4 ++-- v1pysdk/query.py | 5 +++-- v1pysdk/v1meta.py | 7 ++++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/v1pysdk/client.py b/v1pysdk/client.py index 01b2efd..6863597 100644 --- a/v1pysdk/client.py +++ b/v1pysdk/client.py @@ -109,8 +109,8 @@ def get_asset_xml(self, asset_type_name, oid, moment=None): exception, body = self.http_client.get(url) return self.__parse_xml(body, exception) - def get_query_xml(self, asset_type_name, where=None, sel=None): - path = '/rest-1.v1/Data/{0}'.format(asset_type_name) + def get_query_xml(self, api, asset_type_name, where=None, sel=None): + path = '/rest-1.v1/{0}/{1}'.format(api, asset_type_name) query = {} if where is not None: query['Where'] = where diff --git a/v1pysdk/query.py b/v1pysdk/query.py index 1f95178..3487f1d 100644 --- a/v1pysdk/query.py +++ b/v1pysdk/query.py @@ -38,8 +38,9 @@ def get_where_string(self): return ';'.join(terms) def run_single_query(self, url_params={}, api="Data"): - # warning: tight coupling ahead - xml = self.asset_class._v1_v1meta.server.get_query_xml(self.asset_class._v1_asset_type_name) + where = url_params['where'] + sel = url_params['sel'] + xml = self.asset_class._v1_v1meta.server.get_query_xml(api, self.asset_class._v1_asset_type_name, where, sel) return xml def run_query(self): diff --git a/v1pysdk/v1meta.py b/v1pysdk/v1meta.py index b75db82..dae8e45 100644 --- a/v1pysdk/v1meta.py +++ b/v1pysdk/v1meta.py @@ -10,7 +10,8 @@ from none_deref import NoneDeref from string_utils import split_attribute -class V1Meta(object): + +class V1Meta(object): def __init__(self, *args, **kw): self.server = V1Server(*args, **kw) self.global_cache = {} @@ -142,8 +143,8 @@ def get_attr(self, asset_type_name, oid, attrname, moment=None): dummy_asset.append(xml) return self.unpack_asset(dummy_asset)[attrname] - def query(self, asset_type_name, wherestring, selstring): - return self.server.get_query_xml(asset_type_name, wherestring, selstring) + def query(self, asset_type_name, where, sel): + return self.server.get_query_xml('Data', asset_type_name, where, sel) def read_asset(self, asset_type_name, asset_oid, moment=None): xml = self.server.get_asset_xml(asset_type_name, asset_oid, moment) From 054e630081236ec50a5f4abf5e6fd605b0ce5439 Mon Sep 17 00:00:00 2001 From: kunzimariano Date: Fri, 6 May 2016 15:37:05 -0300 Subject: [PATCH 5/7] Fixed a bug in query.py --- v1pysdk/query.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/v1pysdk/query.py b/v1pysdk/query.py index 3487f1d..2fdb9a5 100644 --- a/v1pysdk/query.py +++ b/v1pysdk/query.py @@ -38,8 +38,12 @@ def get_where_string(self): return ';'.join(terms) def run_single_query(self, url_params={}, api="Data"): - where = url_params['where'] - sel = url_params['sel'] + where = None + sel = None + if 'where' in url_params: + where = url_params['where'] + if 'sel' in url_params: + sel = url_params['sel'] xml = self.asset_class._v1_v1meta.server.get_query_xml(api, self.asset_class._v1_asset_type_name, where, sel) return xml From 3c3e44dacce9a4f48d9da705b364578c06278132 Mon Sep 17 00:00:00 2001 From: kunzimariano Date: Fri, 6 May 2016 15:39:12 -0300 Subject: [PATCH 6/7] Added TestV1Server. Also grouped V1Server and HttpClient tests under the same file (client_tests.py). --- v1pysdk/tests/client_tests.py | 84 +++++++++++++++++++++++------- v1pysdk/tests/http_client_tests.py | 47 ----------------- 2 files changed, 64 insertions(+), 67 deletions(-) delete mode 100644 v1pysdk/tests/http_client_tests.py diff --git a/v1pysdk/tests/client_tests.py b/v1pysdk/tests/client_tests.py index 1455306..5e613be 100644 --- a/v1pysdk/tests/client_tests.py +++ b/v1pysdk/tests/client_tests.py @@ -1,60 +1,104 @@ -from v1pysdk.client import V1Server +from v1pysdk.client import V1Server, HttpClient from mock import * import unittest -class TestClient(unittest.TestCase): + +class TestV1Server(unittest.TestCase): address = 'local' instance = 'V1' username = 'usr' password = 'pass' scheme = 'https' + @patch('v1pysdk.client.HttpClient') + def setUp(self, http_client): + self.http_client = http_client + http_client.return_value = self.client = Mock() + self.client.get.return_value = (None, 'response') + self.client.post.return_value = (None, 'response') + + self.server = V1Server(self.address, self.instance, self.username, self.password, self.scheme) + + def test_constructor(self): + self.assertEqual(1, self.http_client.call_count) + + def test_build_url(self): + # {'sel': 'Name', 'Where': "AssetState='64','Name='No importa'"} + url = self.server.build_url('mypath', {'a': 'b'}, 'myfragment') + self.assertEqual('https://local/V1/mypath?a=b#myfragment', url) + + def test_get_asset_xml(self): + xml = self.server.get_asset_xml('Story', '123') + + self.client.get.assert_called_once_with('https://local/V1/rest-1.v1/Data/Story/123') + self.assertEqual('root', xml.tag) + + def test_get_query_xml(self): + xml = self.server.get_query_xml('Data', 'Story', "Name='My Story';AssetState='64'", 'Name,Number') + + self.client.get \ + .assert_called_once_with( + 'https://local/V1/rest-1.v1/Data/Story?sel=Name%2CNumber&Where=Name%3D%27My+Story%27%3BAssetState%3D%2764%27') + self.assertEqual('root', xml.tag) + + def test_get_meta_xml(self): + xml = self.server.get_meta_xml('Story') + + self.client.get \ + .assert_called_once_with('https://local/V1/meta.v1/Story') + self.assertEqual('root', xml.tag) + + def test_execute_operation(self): + xml = self.server.execute_operation('Story', '1234', 'Inactivate') + + self.client.post.assert_called_once_with('https://local/V1/rest-1.v1/Data/Story/1234?op=Inactivate', + postdata={}) + self.assertEqual('root', xml.tag) + + if __name__ == '__main__': + unittest.main() + + +class TestHttpClient(unittest.TestCase): + base_url = 'https://localhost/VersionOne/' + username = 'usr' + password = 'pass' + @patch('v1pysdk.client.urllib2.HTTPPasswordMgrWithDefaultRealm') @patch('v1pysdk.client.urllib2.build_opener') def setUp(self, build_opener, HTTPPasswordMgr): self.manager = Mock() self.opener = Mock() - build_opener.return_value = self.opener HTTPPasswordMgr.return_value = self.manager - - self.server = V1Server(self.address, self.instance, self.username, self.password, self.scheme) - - def tearDown(self): - print 'nothing to do here' + self.http_client = HttpClient(self.base_url, self.username, self.password) def test_constructor(self): - assert self.opener.add_handler.call_count is 1 - assert self.manager.add_password.call_count is 1 + self.assertEqual(1, self.opener.add_handler.call_count) + self.assertEqual(1, self.manager.add_password.call_count) @patch('v1pysdk.client.Request') - def test_http_get(self, request): + def test_get(self, request): r = Mock() request.return_value = r - self.server.http_get('http://get.com') + self.http_client.get('http://get.com') request.assert_called_with('http://get.com') r.add_header.assert_called_with('Content-Type', 'text/xml;charset=UTF-8') self.opener.open.assert_called_with(r) @patch('v1pysdk.client.Request') - def test_http_post(self, request): + def test_post(self, request): r = Mock() request.return_value = r data = Mock() - self.server.http_post('http://post.com', data) + self.http_client.post('http://post.com', data) request.assert_called_with('http://post.com', data) r.add_header.assert_called_with('Content-Type', 'text/xml;charset=UTF-8') self.opener.open.assert_called_with(r) - def test_build_url(self): - url = self.server.build_url('mypath', {'a': 'b'}, 'myfragment') - self.assertEqual('https://local/V1/mypath?a=b#myfragment', url) - if __name__ == '__main__': unittest.main() - - diff --git a/v1pysdk/tests/http_client_tests.py b/v1pysdk/tests/http_client_tests.py deleted file mode 100644 index 04d9d33..0000000 --- a/v1pysdk/tests/http_client_tests.py +++ /dev/null @@ -1,47 +0,0 @@ -from v1pysdk.client import HttpClient -from mock import * -import unittest - -class TestHttpClient(unittest.TestCase): - base_url = 'https://localhost/VersionOne/' - username = 'usr' - password = 'pass' - - @patch('v1pysdk.client.urllib2.HTTPPasswordMgrWithDefaultRealm') - @patch('v1pysdk.client.urllib2.build_opener') - def setUp(self, build_opener, HTTPPasswordMgr): - self.manager = Mock() - self.opener = Mock() - build_opener.return_value = self.opener - HTTPPasswordMgr.return_value = self.manager - self.http_client = HttpClient(self.base_url, self.username, self.password) - - def test_constructor(self): - self.assertEqual(1, self.opener.add_handler.call_count) - self.assertEqual(1, self.manager.add_password.call_count) - - @patch('v1pysdk.client.Request') - def test_get(self, request): - r = Mock() - request.return_value = r - - self.http_client.get('http://get.com') - - request.assert_called_with('http://get.com') - r.add_header.assert_called_with('Content-Type', 'text/xml;charset=UTF-8') - self.opener.open.assert_called_with(r) - - @patch('v1pysdk.client.Request') - def test_post(self, request): - r = Mock() - request.return_value = r - data = Mock() - - self.http_client.post('http://post.com', data) - - request.assert_called_with('http://post.com', data) - r.add_header.assert_called_with('Content-Type', 'text/xml;charset=UTF-8') - self.opener.open.assert_called_with(r) - - if __name__ == '__main__': - unittest.main() From 870f929758cfa51db2fdb9ccada23e0202948137 Mon Sep 17 00:00:00 2001 From: kunzimariano Date: Fri, 6 May 2016 17:29:28 -0300 Subject: [PATCH 7/7] Added tests for V1Server.get_attr --- v1pysdk/tests/client_tests.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/v1pysdk/tests/client_tests.py b/v1pysdk/tests/client_tests.py index 5e613be..1d7b81e 100644 --- a/v1pysdk/tests/client_tests.py +++ b/v1pysdk/tests/client_tests.py @@ -55,8 +55,20 @@ def test_execute_operation(self): postdata={}) self.assertEqual('root', xml.tag) - if __name__ == '__main__': - unittest.main() + def test_get_attr_without_moment(self): + xml = self.server.get_attr(asset_type_name='Story', oid='1234', attrname='AssetState') + + self.client.get.assert_called_once_with('https://local/V1/rest-1.v1/Data/Story/1234/AssetState') + self.assertEqual('root', xml.tag) + + def test_get_attr_with_moment(self): + xml = self.server.get_attr(asset_type_name='Story', oid='1234', attrname='AssetState', moment='1016') + + self.client.get.assert_called_once_with('https://local/V1/rest-1.v1/Data/Story/1234/1016/AssetState') + self.assertEqual('root', xml.tag) + + if __name__ == '__main__': + unittest.main() class TestHttpClient(unittest.TestCase):