From de0eaa6f6aba317df20546ae789f5edd919a5f49 Mon Sep 17 00:00:00 2001 From: Merret Buurman Date: Fri, 4 Sep 2020 20:18:35 +0200 Subject: [PATCH 01/12] Removed logging-handler, which should not be present in a library, and which made all log statements be printed double to stdout. --- pyhandle/client/batchhandleclient.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyhandle/client/batchhandleclient.py b/pyhandle/client/batchhandleclient.py index 97f4fc1..d8d0812 100644 --- a/pyhandle/client/batchhandleclient.py +++ b/pyhandle/client/batchhandleclient.py @@ -16,8 +16,6 @@ from .. import util -logging.basicConfig(level=logging.INFO) - LOGGER = logging.getLogger(__name__) LOGGER.addHandler(util.NullHandler()) From 3fb6a68793e81d7cb9d210b5c54fbfcc7cfc4f7c Mon Sep 17 00:00:00 2001 From: Merret Buurman Date: Fri, 4 Sep 2020 20:53:16 +0200 Subject: [PATCH 02/12] Convenience: Import the relevant submodules in __init__.py. --- pyhandle/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pyhandle/__init__.py b/pyhandle/__init__.py index 5e10c74..401bea8 100644 --- a/pyhandle/__init__.py +++ b/pyhandle/__init__.py @@ -8,3 +8,11 @@ # (The first comment - empty or not - would be appended to # the package name, the next non-empty comment would be # printed as description). + +from . import clientcredentials +from . import handleclient + +# Make sure that a single "import pyhandle" allows the use of +# relevant submodules! +# See +# https://github.com/psf/requests/blob/master/requests/__init__.py#L120 From 40a66571563219a0790dc1881c24b48241442b32 Mon Sep 17 00:00:00 2001 From: Merret Buurman Date: Mon, 1 Feb 2021 18:34:45 +0100 Subject: [PATCH 03/12] Add a NotImplemented exception for an argument that is not treated. --- pyhandle/client/resthandleclient.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyhandle/client/resthandleclient.py b/pyhandle/client/resthandleclient.py index 3d3b065..6a8f2be 100644 --- a/pyhandle/client/resthandleclient.py +++ b/pyhandle/client/resthandleclient.py @@ -603,7 +603,8 @@ def register_handle(self, handle, location, checksum=None, additional_URLs=None, :param checksum: Optional. The checksum string. :param extratypes: Optional. Additional key value pairs such as: additional_URLs for 10320/loc :param additional_URLs: Optional. A list of URLs (as strings) to be - added to the handle record as 10320/LOC entry. + added to the handle record as 10320/LOC entry. Note: This is currently + not implemented. :param overwrite: Optional. If set to True, an existing handle record will be overwritten. Defaults to False. :raises: :exc:`~pyhandle.handleexceptions.HandleAlreadyExistsException` Only if overwrite is not set or @@ -614,6 +615,10 @@ def register_handle(self, handle, location, checksum=None, additional_URLs=None, ''' LOGGER.debug('register_handle...') + # additional_URLs is not used, was this deprecated? + if additional_URLs is not None: + raise NotImplementedError('No support for argument "additional_URLs"!') + # If already exists and can't be overwritten: if overwrite == False: handlerecord_json = self.retrieve_handle_record_json(handle) From b89c6e14a9bf1ac7003ec92be3aaeb3bf4b81170 Mon Sep 17 00:00:00 2001 From: Merret Buurman Date: Mon, 1 Feb 2021 18:53:32 +0100 Subject: [PATCH 04/12] Added new method register_handle_kv which is much more convenient than register_handle. --- pyhandle/client/resthandleclient.py | 61 ++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/pyhandle/client/resthandleclient.py b/pyhandle/client/resthandleclient.py index 6a8f2be..9f5d187 100644 --- a/pyhandle/client/resthandleclient.py +++ b/pyhandle/client/resthandleclient.py @@ -596,6 +596,9 @@ def register_handle(self, handle, location, checksum=None, additional_URLs=None, Registers a new Handle with given name. If the handle already exists and overwrite is not set to True, the method will throw an exception. + Note: This is just a wrapper for register_handle_kv. It was made for + legacy reasons, as this library was created to replace an earlier + library that had a method with specifically this signature. :param handle: The full name of the handle to be registered (prefix and suffix) @@ -613,12 +616,46 @@ def register_handle(self, handle, location, checksum=None, additional_URLs=None, :raises: :exc:`~pyhandle.handleexceptions.HandleSyntaxError` :return: The handle name. ''' - LOGGER.debug('register_handle...') - # additional_URLs is not used, was this deprecated? + if extratypes is None: + extratypes = {} + + if not location is None: + extratypes["URL"] = location + + if not checksum is None: + extratypes["CHECKSUM"] = checksum + if additional_URLs is not None: raise NotImplementedError('No support for argument "additional_URLs"!') + return self.register_handle_kv( + handle, + overwrite, + **extratypes + ) + + + def register_handle_kv(self, handle, overwrite=False, **kv_pairs): + ''' + Registers a new Handle with given name. If the handle already exists + and overwrite is not set to True, the method will throw an + exception. + + :param handle: The full name of the handle to be registered (prefix + and suffix) + :param extratypes: Optional, but highly recommended. The key value pairs + to be included in the record, e.g. URL, CHECKSUM, ... + :param overwrite: Optional. If set to True, an existing handle record + will be overwritten. Defaults to False. + :raises: :exc:`~pyhandle.handleexceptions.HandleAlreadyExistsException` Only if overwrite is not set or + set to False. + :raises: :exc:`~pyhandle.handleexceptions.HandleAuthenticationError` + :raises: :exc:`~pyhandle.handleexceptions.HandleSyntaxError` + :return: The handle name. + ''' + LOGGER.debug('register_handle_kv...') + # If already exists and can't be overwritten: if overwrite == False: handlerecord_json = self.retrieve_handle_record_json(handle) @@ -638,25 +675,13 @@ def register_handle(self, handle, location, checksum=None, additional_URLs=None, list_of_entries.append(adminentry) # Create other entries - entry_URL = self.__create_entry( - 'URL', - location, - self.__make_another_index(list_of_entries, url=True) - ) - list_of_entries.append(entry_URL) - if checksum is not None: - entryChecksum = self.__create_entry( - 'CHECKSUM', - checksum, - self.__make_another_index(list_of_entries) - ) - list_of_entries.append(entryChecksum) - if extratypes is not None: - for key, value in extratypes.items(): + if kv_pairs is not None: + for key, value in kv_pairs.items(): + is_url = True if key == 'URL' else False entry = self.__create_entry( key, value, - self.__make_another_index(list_of_entries) + self.__make_another_index(list_of_entries, is_url) ) list_of_entries.append(entry) From 8b8c9d21b47252f2dc5961c7768cbbb8b3ed289b Mon Sep 17 00:00:00 2001 From: Merret Buurman Date: Mon, 1 Feb 2021 18:57:03 +0100 Subject: [PATCH 05/12] The method generate_and_register_handle now also uses the new method, to remove one level of wrapping. --- pyhandle/client/resthandleclient.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pyhandle/client/resthandleclient.py b/pyhandle/client/resthandleclient.py index 9f5d187..828842b 100644 --- a/pyhandle/client/resthandleclient.py +++ b/pyhandle/client/resthandleclient.py @@ -371,10 +371,15 @@ def generate_and_register_handle(self, prefix, location, checksum=None, **extrat LOGGER.debug('generate_and_register_handle...') handle = self.generate_PID_name(prefix) - handle = self.register_handle( + + if not location is None: + extratypes["URL"] = location + + if not checksum is None: + extratypes["CHECKSUM"] = checksum + + handle = self.register_handle_kv( handle, - location, - checksum, overwrite=True, **extratypes ) @@ -637,7 +642,7 @@ def register_handle(self, handle, location, checksum=None, additional_URLs=None, def register_handle_kv(self, handle, overwrite=False, **kv_pairs): - ''' + ''' Registers a new Handle with given name. If the handle already exists and overwrite is not set to True, the method will throw an exception. From e98917d26417fb0b6f94be0dac469af2157e3a92 Mon Sep 17 00:00:00 2001 From: Merret Buurman Date: Tue, 2 Feb 2021 20:41:43 +0100 Subject: [PATCH 06/12] Found and documented massive bug in unit tests: Comparison of results returns false--positive. --- pyhandle/tests/utilities.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/pyhandle/tests/utilities.py b/pyhandle/tests/utilities.py index 2c84170..e1609ee 100644 --- a/pyhandle/tests/utilities.py +++ b/pyhandle/tests/utilities.py @@ -61,16 +61,30 @@ def log_end_test_code(): REQUESTLOGGER.info('---<') def sort_lists(jsonobject): + # TODO: The whole function does not sort anything, as "sorted" returns + # a new list instead of modifying the existing one. So the sorted version + # vanishes in neverland... nowhereland. And the function returns None. + # Which, when compared, is... None. Yay. All tests pass. + msg = 'This sort function returns false positive when comparing sorted test results.' + raise ValueError(msg) # Sort: if type(jsonobject) == type([]): sorted(jsonobject, key=lambda x:sorted(x.keys())) - + # Python 2.6.6: + # sorted(iterable, cmp=None, key=None, reverse=False) --> new sorted list + # Python 3.7.1: + # Return a new list containing all items from the iterable in ascending order. + # A custom key function can be supplied to customize the sort order, and the + # reverse flag can be set to request the result in descending order. + + # Recursion: if type(jsonobject) == type({'b':2}): for item in jsonobject.items(): sort_lists(item) elif type(jsonobject) == type([2, 2]) or type(jsonobject) == type((2, 2)): + # TODO Isn't the first comparison bullshit, as lists are caught above? for item in jsonobject: sort_lists(item) From 2e7af6310ba25ed009a3bb6c1b49476f4eaf8ca3 Mon Sep 17 00:00:00 2001 From: Merret Buurman Date: Wed, 3 Feb 2021 00:06:06 +0100 Subject: [PATCH 07/12] Replaced the function that made the unit tests pass although they should fail. --- .../handleclient_write_patched_unit_test.py | 33 +++-- pyhandle/tests/utilities.py | 125 ++++++++++++++++++ 2 files changed, 149 insertions(+), 9 deletions(-) diff --git a/pyhandle/tests/testcases/handleclient_write_patched_unit_test.py b/pyhandle/tests/testcases/handleclient_write_patched_unit_test.py index 8e74c47..7becb55 100644 --- a/pyhandle/tests/testcases/handleclient_write_patched_unit_test.py +++ b/pyhandle/tests/testcases/handleclient_write_patched_unit_test.py @@ -13,7 +13,7 @@ from pyhandle.clientcredentials import PIDClientCredentials from pyhandle.handleexceptions import * from pyhandle.tests.mockresponses import MockResponse, MockSearchResponse -from pyhandle.tests.utilities import failure_message, replace_timestamps, sort_lists +from pyhandle.tests.utilities import failure_message, replace_timestamps, flattensort from pyhandle.utilhandle import check_handle_syntax class RESTHandleClientWriteaccessPatchedTestCase(unittest.TestCase): @@ -95,7 +95,9 @@ def test_register_handle(self, getpatch, putpatch): # Compare with expected payload: expected_payload = {"values": [{"index": 100, "type": "HS_ADMIN", "data": {"value": {"index": "200", "handle": "0.NA/my", "permissions": "011111110011"}, "format": "admin"}}, {"index": 1, "type": "URL", "data": "http://foo.bar"}, {"index": 2, "type": "CHECKSUM", "data": "123456"}, {"index": 3, "type": "FOO", "data": "foo"}, {"index": 4, "type": "BAR", "data": "bar"}, {"index": 5, "type": "10320/LOC", "data": ""}]} replace_timestamps(expected_payload) - self.assertEqual(sort_lists(passed_payload), sort_lists(expected_payload), + self.assertIsNotNone(flattensort(passed_payload)) + self.assertIsNotNone(flattensort(expected_payload)) + self.assertEqual(flattensort(passed_payload), flattensort(expected_payload), failure_message(expected=expected_payload, passed=passed_payload, methodname='register_handle')) @mock.patch('pyhandle.handlesystemconnector.HandleSystemConnector.check_if_username_exists') @@ -149,7 +151,9 @@ def test_register_handle_different_owner(self, getpatch, putpatch, username_chec # Compare with expected payload: expected_payload = {"values": [{"index": 100, "type": "HS_ADMIN", "data": {"value": {"index": 300, "handle": "handle/owner", "permissions": "011111110011"}, "format": "admin"}}, {"index": 1, "type": "URL", "data": "http://foo.bar"}, {"index": 2, "type": "CHECKSUM", "data": "123456"}, {"index": 3, "type": "FOO", "data": "foo"}, {"index": 4, "type": "BAR", "data": "bar"}, {"index": 5, "type": "10320/LOC", "data": ""}]} replace_timestamps(expected_payload) - self.assertEqual(sort_lists(passed_payload), sort_lists(expected_payload), + self.assertIsNotNone(flattensort(passed_payload)) + self.assertIsNotNone(flattensort(expected_payload)) + self.assertEqual(flattensort(passed_payload), flattensort(expected_payload), failure_message(expected=expected_payload, passed=passed_payload, methodname='register_handle')) @mock.patch('pyhandle.handlesystemconnector.requests.Session.put') @@ -209,9 +213,18 @@ def test_register_handle_already_exists_overwrite(self, getpatch, putpatch): # Compare with expected payload: expected_payload = {"values": [{"index": 100, "type": "HS_ADMIN", "data": {"value": {"index": "200", "handle": "0.NA/my", "permissions": "011111110011"}, "format": "admin"}}, {"index": 1, "type": "URL", "data": "http://foo.bar"}, {"index": 2, "type": "CHECKSUM", "data": "123456"}, {"index": 3, "type": "FOO", "data": "foo"}, {"index": 4, "type": "BAR", "data": "bar"}, {"index": 5, "type": "10320/LOC", "data": ""}]} replace_timestamps(expected_payload) - self.assertEqual(sort_lists(passed_payload), sort_lists(expected_payload), - failure_message(expected=expected_payload, passed=passed_payload, methodname='register_handle')) + # Visual comparison + #print('PASSED : %s' % passed_payload) + #print('EXPECTED : %s' % expected_payload) + #print('PASSED SORTED: %s' % flattensort(passed_payload)) + #print('EXPECTED SORTED: %s' % flattensort(expected_payload)) + #self.assertIsNotNone(None) # fail just to print the above + self.assertEqual(flattensort(passed_payload), flattensort(expected_payload), + failure_message(expected=expected_payload, passed=passed_payload, methodname='register_handle')) + self.assertIsNotNone(flattensort(passed_payload)) + self.assertIsNotNone(flattensort(expected_payload)) + # Check if requests.put received an authorization header: self.assertIn('Authorization', passed_headers, 'Authorization header not passed: ' + str(passed_headers)) @@ -248,7 +261,9 @@ def test_generate_and_register_handle(self, getpatch, putpatch): # Compare with expected payload: expected_payload = {"values": [{"index": 100, "type": "HS_ADMIN", "data": {"value": {"index": "200", "handle": "0.NA/my", "permissions": "011111110011"}, "format": "admin"}}, {"index": 1, "type": "URL", "data": "http://foo.bar"}, {"index": 2, "type": "CHECKSUM", "data": "123456"}]} replace_timestamps(expected_payload) - self.assertEqual(sort_lists(passed_payload), sort_lists(expected_payload), + self.assertIsNotNone(flattensort(passed_payload)) + self.assertIsNotNone(flattensort(expected_payload)) + self.assertEqual(flattensort(passed_payload), flattensort(expected_payload), failure_message(expected=expected_payload, passed=passed_payload, methodname='generate_and_register_handle')) # modify_handle_value @@ -382,7 +397,7 @@ def test_modify_handle_value_several(self, getpatch, putpatch): }] } replace_timestamps(expected_payload) - self.assertEqual(sort_lists(passed_payload), sort_lists(expected_payload), + self.assertEqual(flattensort(passed_payload), flattensort(expected_payload), failure_message(expected=expected_payload, passed=passed_payload, methodname='modify_handle_value')) @@ -469,7 +484,7 @@ def test_modify_handle_value_several_inexistent(self, getpatch, putpatch): expected_payload = {"values": [{"index": 2, "type": "TEST100", "data": "new100"}, {"index": 2222, "ttl": 86400, "type": "TEST2", "data": "new2"}, {"index": 4, "ttl": 86400, "type": "TEST4", "data": "new4"}]} expected_payload.get('values', {}) replace_timestamps(expected_payload) - self.assertEqual(sort_lists(passed_payload), sort_lists(expected_payload), + self.assertEqual(flattensort(passed_payload), flattensort(expected_payload), failure_message(expected=expected_payload, passed=passed_payload, methodname='modify_handle_value')) @@ -510,7 +525,7 @@ def test_modify_handle_value_several_inexistent_2(self, getpatch, putpatch): expected_payload = {'values': [{'index': 2, 'type': 'TEST100', 'data': 'new100'}, {'index': 2222, 'ttl': 86400, 'type': 'TEST2', 'data': 'new2'}, {'index': 4, 'ttl': 86400, 'type': 'TEST4', 'data': 'new4'}, {'index': 3, 'type': 'TEST101', 'data': 'new101'}]} expected_payload.get('values', {}) replace_timestamps(expected_payload) - self.assertEqual(sort_lists(passed_payload), sort_lists(expected_payload), + self.assertEqual(flattensort(passed_payload), flattensort(expected_payload), failure_message(expected=expected_payload, passed=passed_payload, methodname='modify_handle_value')) diff --git a/pyhandle/tests/utilities.py b/pyhandle/tests/utilities.py index e1609ee..63f009b 100644 --- a/pyhandle/tests/utilities.py +++ b/pyhandle/tests/utilities.py @@ -2,6 +2,7 @@ import os import collections import operator +import copy from functools import cmp_to_key from future.utils import viewitems class NullHandler(logging.Handler): @@ -61,11 +62,17 @@ def log_end_test_code(): REQUESTLOGGER.info('---<') def sort_lists(jsonobject): + ''' + Deprecated! + ''' + # DEPRECATED!!! + # # TODO: The whole function does not sort anything, as "sorted" returns # a new list instead of modifying the existing one. So the sorted version # vanishes in neverland... nowhereland. And the function returns None. # Which, when compared, is... None. Yay. All tests pass. msg = 'This sort function returns false positive when comparing sorted test results.' + msg += 'Use flattensort() instead.' raise ValueError(msg) # Sort: @@ -88,3 +95,121 @@ def sort_lists(jsonobject): # TODO Isn't the first comparison bullshit, as lists are caught above? for item in jsonobject: sort_lists(item) + +def flattensort(jsonobject): + ''' + Take a complex object (JSON object: nested dicts and lists of + any depth) and flatten and sort it. + + This is done recursively: The leaves are being sorted flattened + first. Then we go up and sort and flatten each level. + + Purpose: Compare JSON objects in unit tests, where the order of + the objects does not matter, and they can be quite deeply nested. + + Test using these: + +# These are the same, just differently sorted: +x = {"values": [{"index": 100, "type": "HS_ADMIN", "data": {"value": {"permissions": "011111110011", "index": "200", "handle": "0.NA/my"}, "format": "admin"}}, {"index": 1, "type": "URL", "data": "http://foo.bar"}, {"index": 2, "type": "CHECKSUM", "data": "123456"}, {"index": 4, "type": "BAR", "data": "bar"}, {"index": 3, "type": "FOO", "data": "foo"}, {"index": 5, "type": "10320/LOC", "data": ""}]} +y = {"values": [{"type": "HS_ADMIN", "index": 100, "data": {"value": {"index": "200", "handle": "0.NA/my", "permissions": "011111110011"}, "format": "admin"}}, {"index": 1, "type": "URL", "data": "http://foo.bar"}, {"index": 2, "type": "CHECKSUM", "data": "123456"}, {"index": 3, "type": "FOO", "data": "foo"}, {"index": 4, "type": "BAR", "data": "bar"}, {"index": 5, "type": "10320/LOC", "data": ""}]} + +# These are actually different: +x = {'values': [{'index': 100, 'type': 'HS_ADMIN', 'data': {'value': {'index': '200', 'handle': '0.NA/my', 'permissions': '011111110011'}, 'format': 'admin'}}, {'index': 2, 'type': 'FOO', 'data': 'foo'}, {'index': 3, 'type': 'BAR', 'data': 'bar'}, {'index': 1, 'type': 'URL', 'data': 'http://foo.bar'}, {'index': 4, 'type': 'CHECKSUM', 'data': '123456'}]} +y = {'values': [{'index': 100, 'type': 'HS_ADMIN', 'data': {'value': {'index': '200', 'handle': '0.NA/my', 'permissions': '011111110011'}, 'format': 'admin'}}, {'index': 1, 'type': 'URL', 'data': 'http://foo.bar'}, {'index': 2, 'type': 'CHECKSUM', 'data': '123456'}, {'index': 3, 'type': 'FOO', 'data': 'foo'}, {'index': 4, 'type': 'BAR', 'data': 'bar'}, {'index': 5, 'type': '10320/LOC', 'data': ''}]} + +print('x==x: %s' % (x==x)) +print('y==y: %s' % (y==y)) +print('x==y: %s' % (x==y)) +print('rx==ry: %s' % (flattensort(x)==flattensort(y))) + + ''' + + # Sorting a leave (end of recursion): + # If it is a shallow, simple list, we can use sort(). + + if type(jsonobject) == type([]): + + + res = copy.deepcopy(jsonobject) + # Why? The sort() can alter the list before failing, so + # we must operate on a deep-copy. + + try: + res.sort() + # We make it a string so that the deep lists become flat lists and + # can be sorted eventually. Otherwise we'd sort all the leaves, but + # still could not sort the higher levels. + res = ','.join(res) + res = '['+res+']' + return res + + except TypeError as e: + # Not a shallow list, or items have uncomparable types. + pass + + # Shallow list of various types: Recursion + res = [] + shallow = True + for item in jsonobject: + + if isinstance(item, list) or isinstance(item, tuple) or isinstance(item, dict): + shallow = False + res = None + break + else: + res.append(str(item)) + + if shallow: + res.sort() + res = ','.join(res) + res = '['+res+']' + return res + + + # Deep list: Recursion + res = [] + for item in jsonobject: + item = flattensort(item) + res.append(item) + # The deep list's entries were now all flattened, so we + # can and must sort them (then flatten): + res.sort() + res = ','.join(res) + res = '['+res+']' + return res + + # Dictionary: Recursion + if type(jsonobject) == type({'b':2}): + res = [] + for item in jsonobject.items(): + item = flattensort(item) + res.append(item) + # The dictionary's entries were now all flattened, so we + # can and must sort them (then flatten): + res.sort() + res = ','.join(res) + res = '{'+res+'}' + return res + + # Tuples: Recursion + elif type(jsonobject) == type((2, 2)): + + if not len(jsonobject) == 2: + raise ValueError('Tuple of length %s, expected 2!') + + # Here, tuples are dictionary entries, kv pairs, so we don't + # need to sort them but just flatten them to string: + tup1 = flattensort(jsonobject[0]) + tup2 = flattensort(jsonobject[1]) + res = tup1+':'+tup2 + return res + + # Simple types: Just return + try: + if isinstance(jsonobject, basestring): + return '"'+jsonobject+'"' + except NameError: + if isinstance(jsonobject, str): + return '"'+jsonobject+'"' + + return str(jsonobject) \ No newline at end of file From a055924bdfc9f32c4168eb6ae01c06285dad0692 Mon Sep 17 00:00:00 2001 From: Merret Buurman Date: Wed, 3 Feb 2021 00:36:19 +0100 Subject: [PATCH 08/12] Removed arguments that are no longer supported from unit tests, and added a test to check whether exception is raised. --- .../handleclient_write_patched_unit_test.py | 43 ++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/pyhandle/tests/testcases/handleclient_write_patched_unit_test.py b/pyhandle/tests/testcases/handleclient_write_patched_unit_test.py index 7becb55..c2e7096 100644 --- a/pyhandle/tests/testcases/handleclient_write_patched_unit_test.py +++ b/pyhandle/tests/testcases/handleclient_write_patched_unit_test.py @@ -76,7 +76,7 @@ def test_register_handle(self, getpatch, putpatch): testhandle = 'my/testhandle' testlocation = 'http://foo.bar' testchecksum = '123456' - additional_URLs = ['http://bar.bar', 'http://foo.foo'] + additional_URLs = None handle_returned = self.inst.register_handle(testhandle, location=testlocation, checksum=testchecksum, @@ -91,14 +91,45 @@ def test_register_handle(self, getpatch, putpatch): # Get the payload+headers passed to "requests.put" passed_payload, _ = self.get_payload_headers_from_mockresponse(putpatch) - + # Compare with expected payload: - expected_payload = {"values": [{"index": 100, "type": "HS_ADMIN", "data": {"value": {"index": "200", "handle": "0.NA/my", "permissions": "011111110011"}, "format": "admin"}}, {"index": 1, "type": "URL", "data": "http://foo.bar"}, {"index": 2, "type": "CHECKSUM", "data": "123456"}, {"index": 3, "type": "FOO", "data": "foo"}, {"index": 4, "type": "BAR", "data": "bar"}, {"index": 5, "type": "10320/LOC", "data": ""}]} + #expected_payload = {"values": [{"index": 100, "type": "HS_ADMIN", "data": {"value": {"index": "200", "handle": "0.NA/my", "permissions": "011111110011"}, "format": "admin"}}, {"index": 1, "type": "URL", "data": "http://foo.bar"}, {"index": 2, "type": "CHECKSUM", "data": "123456"}, {"index": 3, "type": "FOO", "data": "foo"}, {"index": 4, "type": "BAR", "data": "bar"}, {"index": 5, "type": "10320/LOC", "data": ""}]} + #expected_payload = {"values": [{"index": 100, "type": "HS_ADMIN", "data": {"value": {"index": "200", "handle": "0.NA/my", "permissions": "011111110011"}, "format": "admin"}}, {"index": 1, "type": "URL", "data": "http://foo.bar"}, {"index": 2, "type": "CHECKSUM", "data": "123456"}, {"index": 3, "type": "FOO", "data": "foo"}, {"index": 4, "type": "BAR", "data": "bar"}]} + expected_payload = {"values": [{"index": 100, "type": "HS_ADMIN", "data": {"value": {"index": "200", "handle": "0.NA/my", "permissions": "011111110011"}, "format": "admin"}}, {"index": 1, "type": "URL", "data": "http://foo.bar"}, {"index": 4, "type": "CHECKSUM", "data": "123456"}, {"index": 2, "type": "FOO", "data": "foo"}, {"index": 3, "type": "BAR", "data": "bar"}]} replace_timestamps(expected_payload) self.assertIsNotNone(flattensort(passed_payload)) self.assertIsNotNone(flattensort(expected_payload)) self.assertEqual(flattensort(passed_payload), flattensort(expected_payload), failure_message(expected=expected_payload, passed=passed_payload, methodname='register_handle')) + + @mock.patch('pyhandle.handlesystemconnector.requests.Session.put') + @mock.patch('pyhandle.handlesystemconnector.requests.Session.get') + def test_register_handle_additional_urls(self, getpatch, putpatch): + """Test registering a new handle with additional URLs, which is + not supported anymore.""" + + # Define the replacement for the patched GET method: + # The handle does not exist yet, so a response with 404 + mock_response_get = MockResponse(notfound=True) + getpatch.return_value = mock_response_get + + # Define the replacement for the patched requests.put method: + mock_response_put = MockResponse(wascreated=True) + putpatch.return_value = mock_response_put + + # Run the code to be tested: + testhandle = 'my/testhandle' + testlocation = 'http://foo.bar' + testchecksum = '123456' + additional_URLs = ['http://bar.bar', 'http://foo.foo'] + # Run code to be tested + check exception: + with self.assertRaises(NotImplementedError): + handle_returned = self.inst.register_handle(testhandle, + location=testlocation, + checksum=testchecksum, + additional_URLs=additional_URLs, + FOO='foo', + BAR='bar') @mock.patch('pyhandle.handlesystemconnector.HandleSystemConnector.check_if_username_exists') @mock.patch('pyhandle.handlesystemconnector.requests.Session.put') @@ -132,7 +163,8 @@ def test_register_handle_different_owner(self, getpatch, putpatch, username_chec testhandle = 'my/testhandle' testlocation = 'http://foo.bar' testchecksum = '123456' - additional_URLs = ['http://bar.bar', 'http://foo.foo'] + #additional_URLs = ['http://bar.bar', 'http://foo.foo'] + additional_URLs = None handle_returned = newInst.register_handle(testhandle, location=testlocation, checksum=testchecksum, @@ -194,7 +226,8 @@ def test_register_handle_already_exists_overwrite(self, getpatch, putpatch): testlocation = 'http://foo.bar' testchecksum = '123456' overwrite = True - additional_URLs = ['http://bar.bar', 'http://foo.foo'] + #additional_URLs = ['http://bar.bar', 'http://foo.foo'] + additional_URLs = None handle_returned = self.inst.register_handle(testhandle, location=testlocation, checksum=testchecksum, From 2b5707487e280a755542c0bc84c6f996c978bb57 Mon Sep 17 00:00:00 2001 From: Merret Buurman Date: Wed, 3 Feb 2021 00:36:52 +0100 Subject: [PATCH 09/12] Adapted some expected unit test results to recent changes. --- .../handleclient_write_patched_unit_test.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pyhandle/tests/testcases/handleclient_write_patched_unit_test.py b/pyhandle/tests/testcases/handleclient_write_patched_unit_test.py index c2e7096..9a6944f 100644 --- a/pyhandle/tests/testcases/handleclient_write_patched_unit_test.py +++ b/pyhandle/tests/testcases/handleclient_write_patched_unit_test.py @@ -181,7 +181,11 @@ def test_register_handle_different_owner(self, getpatch, putpatch, username_chec passed_payload, _ = self.get_payload_headers_from_mockresponse(putpatch) # Compare with expected payload: - expected_payload = {"values": [{"index": 100, "type": "HS_ADMIN", "data": {"value": {"index": 300, "handle": "handle/owner", "permissions": "011111110011"}, "format": "admin"}}, {"index": 1, "type": "URL", "data": "http://foo.bar"}, {"index": 2, "type": "CHECKSUM", "data": "123456"}, {"index": 3, "type": "FOO", "data": "foo"}, {"index": 4, "type": "BAR", "data": "bar"}, {"index": 5, "type": "10320/LOC", "data": ""}]} + # Previously contained 10320LOC field: + #expected_payload = {"values": [{"index": 100, "type": "HS_ADMIN", "data": {"value": {"index": 300, "handle": "handle/owner", "permissions": "011111110011"}, "format": "admin"}}, {"index": 1, "type": "URL", "data": "http://foo.bar"}, {"index": 2, "type": "CHECKSUM", "data": "123456"}, {"index": 3, "type": "FOO", "data": "foo"}, {"index": 4, "type": "BAR", "data": "bar"}, {"index": 5, "type": "10320/LOC", "data": ""}]} + # Changed order/index: + #expected_payload = {"values": [{"index": 100, "type": "HS_ADMIN", "data": {"value": {"index": 300, "handle": "handle/owner", "permissions": "011111110011"}, "format": "admin"}}, {"index": 1, "type": "URL", "data": "http://foo.bar"}, {"index": 2, "type": "CHECKSUM", "data": "123456"}, {"index": 3, "type": "FOO", "data": "foo"}, {"index": 4, "type": "BAR", "data": "bar"}]} + expected_payload = {"values": [{"index": 100, "type": "HS_ADMIN", "data": {"value": {"index": 300, "handle": "handle/owner", "permissions": "011111110011"}, "format": "admin"}}, {"index": 1, "type": "URL", "data": "http://foo.bar"}, {"index": 4, "type": "CHECKSUM", "data": "123456"}, {"index": 2, "type": "FOO", "data": "foo"}, {"index": 3, "type": "BAR", "data": "bar"}]} replace_timestamps(expected_payload) self.assertIsNotNone(flattensort(passed_payload)) self.assertIsNotNone(flattensort(expected_payload)) @@ -244,7 +248,11 @@ def test_register_handle_already_exists_overwrite(self, getpatch, putpatch): passed_payload, passed_headers = self.get_payload_headers_from_mockresponse(putpatch) # Compare with expected payload: - expected_payload = {"values": [{"index": 100, "type": "HS_ADMIN", "data": {"value": {"index": "200", "handle": "0.NA/my", "permissions": "011111110011"}, "format": "admin"}}, {"index": 1, "type": "URL", "data": "http://foo.bar"}, {"index": 2, "type": "CHECKSUM", "data": "123456"}, {"index": 3, "type": "FOO", "data": "foo"}, {"index": 4, "type": "BAR", "data": "bar"}, {"index": 5, "type": "10320/LOC", "data": ""}]} + # Previously contained 10320LOC: + #expected_payload = {"values": [{"index": 100, "type": "HS_ADMIN", "data": {"value": {"index": "200", "handle": "0.NA/my", "permissions": "011111110011"}, "format": "admin"}}, {"index": 1, "type": "URL", "data": "http://foo.bar"}, {"index": 2, "type": "CHECKSUM", "data": "123456"}, {"index": 3, "type": "FOO", "data": "foo"}, {"index": 4, "type": "BAR", "data": "bar"}, {"index": 5, "type": "10320/LOC", "data": ""}]} + #expected_payload = {"values": [{"index": 100, "type": "HS_ADMIN", "data": {"value": {"index": "200", "handle": "0.NA/my", "permissions": "011111110011"}, "format": "admin"}}, {"index": 1, "type": "URL", "data": "http://foo.bar"}, {"index": 2, "type": "CHECKSUM", "data": "123456"}, {"index": 3, "type": "FOO", "data": "foo"}, {"index": 4, "type": "BAR", "data": "bar"}]} + # Different indizes: + expected_payload = {"values": [{"index": 100, "type": "HS_ADMIN", "data": {"value": {"index": "200", "handle": "0.NA/my", "permissions": "011111110011"}, "format": "admin"}}, {"index": 1, "type": "URL", "data": "http://foo.bar"}, {"index": 4, "type": "CHECKSUM", "data": "123456"}, {"index": 2, "type": "FOO", "data": "foo"}, {"index": 3, "type": "BAR", "data": "bar"}]} replace_timestamps(expected_payload) # Visual comparison @@ -786,7 +794,8 @@ def test_GenericHandleError(self, getpatch, putpatch): """ # Define the replacement for the patched GET method: - cont = {"responseCode":1, "handle":"not/me", "values":[{"index":1, "type":"URL", "data":{"format":"string", "value":"www.url.foo"}, "ttl":86400, "timestamp":"2015-09-30T15:54:30Z"}, {"index":2, "type":"10320/LOC", "data":{"format":"string", "value":" "}, "ttl":86400, "timestamp":"2015-09-30T15:54:30Z"}]} + #cont = {"responseCode":1, "handle":"not/me", "values":[{"index":1, "type":"URL", "data":{"format":"string", "value":"www.url.foo"}, "ttl":86400, "timestamp":"2015-09-30T15:54:30Z"}, {"index":2, "type":"10320/LOC", "data":{"format":"string", "value":" "}, "ttl":86400, "timestamp":"2015-09-30T15:54:30Z"}]} + cont = {"responseCode":1, "handle":"not/me", "values":[{"index":1, "type":"URL", "data":{"format":"string", "value":"www.url.foo"}, "ttl":86400, "timestamp":"2015-09-30T15:54:30Z"}]} mock_response_get = MockResponse(status_code=200, content=json.dumps(cont)) getpatch.return_value = mock_response_get From 82f577d0b80035207d1465c7e075aa1c096ee759 Mon Sep 17 00:00:00 2001 From: Merret Buurman Date: Wed, 3 Feb 2021 01:14:05 +0100 Subject: [PATCH 10/12] Added a JSON convenience method and unit tests --- pyhandle/client/resthandleclient.py | 47 +++++- .../handleclient_write_patched_unit_test.py | 150 ++++++++++++++++++ pyhandle/utilhandle.py | 2 +- 3 files changed, 194 insertions(+), 5 deletions(-) diff --git a/pyhandle/client/resthandleclient.py b/pyhandle/client/resthandleclient.py index 828842b..d4145ca 100644 --- a/pyhandle/client/resthandleclient.py +++ b/pyhandle/client/resthandleclient.py @@ -5,6 +5,7 @@ import uuid import logging import datetime +import copy import requests # This import is needed for mocking in unit tests. from past.builtins import xrange @@ -595,7 +596,41 @@ def delete_handle(self, handle, *other): else: raise GenericHandleError(op=op, handle=handle, response=resp) - + + def register_handle_json(self, handle, list_of_entries, overwrite=False): + ''' + entry = {'index':index, 'type':entrytype, 'data':data} + # Optional 'ttl' + ''' + + # If already exists and can't be overwritten: + if overwrite == False: + handlerecord_json = self.retrieve_handle_record_json(handle) + if handlerecord_json is not None: + msg = 'Could not register handle' + LOGGER.error(msg + ', as it already exists.') + raise HandleAlreadyExistsException(handle=handle, msg=msg) + + # So we don't modify the caller's list: + list_of_entries = copy.deepcopy(list_of_entries) + + # Create admin entry + keys = [] + for entry in list_of_entries: + keys.append(entry['type']) + + if not 'HS_ADMIN' in keys: + adminentry = self.__create_admin_entry( + self.__handleowner, + self.__HS_ADMIN_permissions, + self.__make_another_index(list_of_entries, hs_admin=True), + handle + ) + list_of_entries.append(adminentry) + + # Create record itself and put to server: + return self.__handle_registering(handle, list_of_entries, overwrite) + def register_handle(self, handle, location, checksum=None, additional_URLs=None, overwrite=False, **extratypes): ''' Registers a new Handle with given name. If the handle already exists @@ -640,7 +675,6 @@ def register_handle(self, handle, location, checksum=None, additional_URLs=None, **extratypes ) - def register_handle_kv(self, handle, overwrite=False, **kv_pairs): ''' Registers a new Handle with given name. If the handle already exists @@ -690,7 +724,10 @@ def register_handle_kv(self, handle, overwrite=False, **kv_pairs): ) list_of_entries.append(entry) - # Create record itself and put to server + # Create record itself and put to server: + return self.__handle_registering(handle, list_of_entries, overwrite) + + def __handle_registering(self, handle, list_of_entries, overwrite): op = 'registering handle' resp, put_payload = self.__send_handle_put_request( handle, @@ -982,9 +1019,11 @@ def __create_admin_entry(self, handleowner, permissions, index, handle, ttl=None # If the handle owner is specified, use it. Otherwise, use 200:0.NA/prefix # With the prefix taken from the handle that is being created, not from anywhere else. if handleowner is None: - adminindex = '200' + adminindex = '200' # TODO Why string, not integer? prefix = handle.split('/')[0] adminhandle = '0.NA/' + prefix + # TODO: Why is adminindex string, not integer? When I retrieve from + # HandleSystem API, the JSON has an int there. else: adminindex, adminhandle = utilhandle.remove_index_from_handle(handleowner) diff --git a/pyhandle/tests/testcases/handleclient_write_patched_unit_test.py b/pyhandle/tests/testcases/handleclient_write_patched_unit_test.py index 9a6944f..c57d4ed 100644 --- a/pyhandle/tests/testcases/handleclient_write_patched_unit_test.py +++ b/pyhandle/tests/testcases/handleclient_write_patched_unit_test.py @@ -102,6 +102,156 @@ def test_register_handle(self, getpatch, putpatch): self.assertEqual(flattensort(passed_payload), flattensort(expected_payload), failure_message(expected=expected_payload, passed=passed_payload, methodname='register_handle')) + @mock.patch('pyhandle.handlesystemconnector.requests.Session.put') + @mock.patch('pyhandle.handlesystemconnector.requests.Session.get') + def test_register_handle_kv(self, getpatch, putpatch): + """Test registering a new handle with various types of values.""" + + # Define the replacement for the patched GET method: + # The handle does not exist yet, so a response with 404 + mock_response_get = MockResponse(notfound=True) + getpatch.return_value = mock_response_get + + # Define the replacement for the patched requests.put method: + mock_response_put = MockResponse(wascreated=True) + putpatch.return_value = mock_response_put + + # Run the code to be tested: + testhandle = 'my/testhandle' + handle_returned = self.inst.register_handle_kv(testhandle, + URL='http://foo.bar', + CHECKSUM='123456', + FOO='foo', + BAR='bar') + + + # Check if the PUT request was sent exactly once: + self.assertEqual(putpatch.call_count, 1, + 'The method "requests.put" was not called once, but ' + str(putpatch.call_count) + ' times.') + + # Get the payload+headers passed to "requests.put" + passed_payload, _ = self.get_payload_headers_from_mockresponse(putpatch) + + # Compare with expected payload: + expected_payload = {"values": [{"index": 100, "type": "HS_ADMIN", "data": {"value": {"index": "200", "handle": "0.NA/my", "permissions": "011111110011"}, "format": "admin"}}, {"index": 1, "type": "URL", "data": "http://foo.bar"}, {"index": 2, "type": "CHECKSUM", "data": "123456"}, {"index": 3, "type": "FOO", "data": "foo"}, {"index": 4, "type": "BAR", "data": "bar"}]} + #expected_payload = {"values": [{"index": 100, "type": "HS_ADMIN", "data": {"value": {"index": "200", "handle": "0.NA/my", "permissions": "011111110011"}, "format": "admin"}}, {"index": 1, "type": "URL", "data": "http://foo.bar"}, {"index": 4, "type": "CHECKSUM", "data": "123456"}, {"index": 2, "type": "FOO", "data": "foo"}, {"index": 3, "type": "BAR", "data": "bar"}]} + replace_timestamps(expected_payload) + self.assertIsNotNone(flattensort(passed_payload)) + self.assertIsNotNone(flattensort(expected_payload)) + self.assertEqual(flattensort(passed_payload), flattensort(expected_payload), + failure_message(expected=expected_payload, passed=passed_payload, methodname='register_handle')) + + @mock.patch('pyhandle.handlesystemconnector.requests.Session.put') + @mock.patch('pyhandle.handlesystemconnector.requests.Session.get') + def test_register_handle_json(self, getpatch, putpatch): + """Test registering a new handle using JSON entries, where HS_ADMIN is not specified.""" + + # Define the replacement for the patched GET method: + # The handle does not exist yet, so a response with 404 + mock_response_get = MockResponse(notfound=True) + getpatch.return_value = mock_response_get + + # Define the replacement for the patched requests.put method: + mock_response_put = MockResponse(wascreated=True) + putpatch.return_value = mock_response_put + + # Run the code to be tested: + testhandle = 'my/testhandle' + entries = [ + {'index':1, 'type':'URL', 'data':'http://foo.bar'}, + {'index':2, 'type':'CHECKSUM', 'data':'123456'}, + {'index':3, 'type':'FOO', 'data':'foo'}, + {'index':4, 'type':'BAR', 'data':'bar'} + ] + + handle_returned = self.inst.register_handle_json(testhandle, + entries) + + + # Check if the PUT request was sent exactly once: + self.assertEqual(putpatch.call_count, 1, + 'The method "requests.put" was not called once, but ' + str(putpatch.call_count) + ' times.') + + # Get the payload+headers passed to "requests.put" + passed_payload, _ = self.get_payload_headers_from_mockresponse(putpatch) + + # Compare with expected payload: + expected_payload = {"values": entries} + expected_payload["values"].append({ + 'index':100, + 'type':'HS_ADMIN', + 'data': { + 'value':{ + 'index':'200', # TODO Why string and not int? + 'handle':'0.NA/my', + 'permissions':'011111110011' + }, + 'format':'admin' + } + }) + replace_timestamps(expected_payload) + self.assertIsNotNone(flattensort(passed_payload)) + self.assertIsNotNone(flattensort(expected_payload)) + self.assertEqual(flattensort(passed_payload), flattensort(expected_payload), + failure_message(expected=expected_payload, passed=passed_payload, methodname='register_handle')) + + @mock.patch('pyhandle.handlesystemconnector.requests.Session.put') + @mock.patch('pyhandle.handlesystemconnector.requests.Session.get') + def test_register_handle_json_admin_given(self, getpatch, putpatch): + """Test registering a new handle using JSON entries, where HS_ADMIN is specified.""" + + # Define the replacement for the patched GET method: + # The handle does not exist yet, so a response with 404 + mock_response_get = MockResponse(notfound=True) + getpatch.return_value = mock_response_get + + # Define the replacement for the patched requests.put method: + mock_response_put = MockResponse(wascreated=True) + putpatch.return_value = mock_response_put + + # Run the code to be tested: + testhandle = 'my/testhandle' + entries = [ + {'index':1, 'type':'URL', 'data':'http://foo.bar'}, + {'index':2, 'type':'CHECKSUM', 'data':'123456'}, + {'index':3, 'type':'FOO', 'data':'foo'}, + {'index':4, 'type':'BAR', 'data':'bar'}, + { + 'index':100, + 'type':'HS_ADMIN', + 'data': { + 'value':{ + 'index':222, + 'handle':'0.NA/myprefix', + 'permissions':'bluubb' + }, + 'format':'admin' + } + } + ] + + handle_returned = self.inst.register_handle_json(testhandle, + entries) + + + # Check if the PUT request was sent exactly once: + self.assertEqual(putpatch.call_count, 1, + 'The method "requests.put" was not called once, but ' + str(putpatch.call_count) + ' times.') + + # Get the payload+headers passed to "requests.put" + passed_payload, _ = self.get_payload_headers_from_mockresponse(putpatch) + + # Compare with expected payload: + #expected_payload = {"values": [{"index": 100, "type": "HS_ADMIN", "data": {"value": {"index": "200", "handle": "0.NA/my", "permissions": "011111110011"}, "format": "admin"}}, {"index": 1, "type": "URL", "data": "http://foo.bar"}, {"index": 2, "type": "CHECKSUM", "data": "123456"}, {"index": 3, "type": "FOO", "data": "foo"}, {"index": 4, "type": "BAR", "data": "bar"}]} + #expected_payload = {"values": [{"index": 100, "type": "HS_ADMIN", "data": {"value": {"index": "200", "handle": "0.NA/my", "permissions": "011111110011"}, "format": "admin"}}, {"index": 1, "type": "URL", "data": "http://foo.bar"}, {"index": 4, "type": "CHECKSUM", "data": "123456"}, {"index": 2, "type": "FOO", "data": "foo"}, {"index": 3, "type": "BAR", "data": "bar"}]} + #expected_payload = entries + expected_payload = {"values": entries} + replace_timestamps(expected_payload) + self.assertIsNotNone(flattensort(passed_payload)) + self.assertIsNotNone(flattensort(expected_payload)) + self.assertEqual(flattensort(passed_payload), flattensort(expected_payload), + failure_message(expected=expected_payload, passed=passed_payload, methodname='register_handle')) + @mock.patch('pyhandle.handlesystemconnector.requests.Session.put') @mock.patch('pyhandle.handlesystemconnector.requests.Session.get') def test_register_handle_additional_urls(self, getpatch, putpatch): diff --git a/pyhandle/utilhandle.py b/pyhandle/utilhandle.py index c5824d3..dd95cef 100644 --- a/pyhandle/utilhandle.py +++ b/pyhandle/utilhandle.py @@ -17,7 +17,7 @@ def remove_index_from_handle(handle_with_index): :handle_with_index: The handle string with an index (e.g. 500:prefix/suffix) - :return: index and handle as a tuple. + :return: index and handle as a tuple, where index is integer. ''' split = handle_with_index.split(':') From b42a9a3c341d1d891ba2c204c1abf6b05affadfa Mon Sep 17 00:00:00 2001 From: Merret Buurman Date: Wed, 3 Feb 2021 01:38:21 +0100 Subject: [PATCH 11/12] Improved README.md. --- README.md | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 396d159..21b9cab 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,18 @@ For more details about the library you can build the documention using [Sphinx]( python setup.py build_sphinx ``` + +# Link to documentation + + +Check out the documentation [here](https://eudat-b2safe.github.io/PYHANDLE/). + +(You can find the source here in this repository at [/docs/source/index.rst](./docs/source/index.rst)!) + + # License -Copyright 2015-2017, Deutsches Klimarechenzentrum GmbH, GRNET S.A., SURFsara +Copyright 2015-2021, Deutsches Klimarechenzentrum GmbH, GRNET S.A., SURFsara The PYHANDLE library is licensed under the Apache License, Version 2.0 (the "License"); you may not use this product except in @@ -50,7 +59,29 @@ Copyright 2015-2017, Deutsches Klimarechenzentrum GmbH, GRNET S.A., SURFsara limitations under the License. +# Some usage notes + +(to be migrated to documentation) + + +* `register_handle_kv(handle, **kv-pairs)` allows to pass (additionally to the handle name) key-value pairs. + +* `register_handle_json(handle, list_of_entries, ...)` allow to pass JSON snippets instead of key-value pairs, so you can specify the indices. An entry looks like this: `{'index':index, 'type':entrytype, 'data':data}`. This is the format in which the changes are communicated to the handle server via its REST interface. An entry of type `HS_ADMIN` will be added if you do not provide one. + +* `register_handle(...)` allows to pass (additionally to the handle name) a mandatory URL, and optionally a CHECKSUM, and more types as key-value pairs. Old method, made for legacy reasons, as this library was created to replace an earlier library that had a method with specifically this signature. + +* `generate_and_register_handle(prefix, ...)` is a similar legacy method. Instead, just use `generate_PID_name(prefix)` to create a handle name and use one of the above. + + +# How to run the unit tests + +The simplest way (tested with python 3.7.1): + +```bash +python setup.py test +``` +Also look at `pyhandle/tests/README.md`. From 5cfb3b1116c573f9c8de10e633ce78b536e9106a Mon Sep 17 00:00:00 2001 From: Merret Buurman Date: Thu, 4 Feb 2021 19:46:26 +0100 Subject: [PATCH 12/12] Bump version to 1.0.4. --- pyhandle/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyhandle/__init__.py b/pyhandle/__init__.py index 507a1b5..d8f7f02 100644 --- a/pyhandle/__init__.py +++ b/pyhandle/__init__.py @@ -1,4 +1,7 @@ __version__ = "1.0.4" +# Version 1.0.4 of 2021-02-04 including some pull requests from 2020. +# The version number had been made 1.0.4 already in 2020, but +# no release was done back then. # The version as used in setup.py and docs/source/conf.py.