From bb110da97299c42e5b585638d1b5454272055d8d Mon Sep 17 00:00:00 2001 From: Mike McLean Date: Fri, 11 May 2018 17:32:25 -0400 Subject: [PATCH 01/15] first stab at a new multicall approach --- koji/__init__.py | 54 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/koji/__init__.py b/koji/__init__.py index 4388e8b2..a7f8617a 100644 --- a/koji/__init__.py +++ b/koji/__init__.py @@ -2858,6 +2858,60 @@ class ClientSession(object): return base64.b64decode(result) +class MultiCallSession(object): + + """Manages a single multicall, acts like a session""" + + def __init__(self, session): + self._session = session + self._calls = [] + + def __getattr__(self, name): + #if name[:1] == '_': + # raise AttributeError("no attribute %r" % name) + return VirtualMethod(self._callMethod, name) + + def _callMethod(self, name, args, kwargs=None, retry=True): + """Make a call to the hub with retries and other niceties""" + + if kwargs is None: + kwargs = {} + args = encode_args(*args, **kwargs) + self._calls.append({'methodName': name, 'params': args}) + return MultiCallInProgress + + def callMethod(self, name, *args, **opts): + """compatibility wrapper for _callMethod""" + return self._callMethod(name, args, opts) + + def call_all(self, strict=False): + """Perform all calls in a single multicall + + Returns a the hub's multiCall result, which is a list of results for + each call. For successful calls, the entry will be a singleton list. + For calls that raised a fault, the entry will be a dictionary with + keys "faultCode", "faultString", and "traceback". + """ + + if len(self._calls) == 0: + return [] + + calls = self._calls + self._calls = [] + ret = self._callMethod('multiCall', (calls,), {}) + if strict: + #check for faults and raise first one + for entry in ret: + if isinstance(entry, dict): + fault = Fault(entry['faultCode'], entry['faultString']) + err = convertFault(fault) + raise err + return ret + + # alias for compatibility with ClientSession + multiCall = call_all + + class DBHandler(logging.Handler): """ A handler class which writes logging records, appropriately formatted, From 75982719c1151b05513a41585269a941eb7d9328 Mon Sep 17 00:00:00 2001 From: Mike McLean Date: Fri, 11 May 2018 19:00:37 -0400 Subject: [PATCH 02/15] Use VirtualCall helper --- koji/__init__.py | 46 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/koji/__init__.py b/koji/__init__.py index a7f8617a..438f7711 100644 --- a/koji/__init__.py +++ b/koji/__init__.py @@ -2858,6 +2858,36 @@ class ClientSession(object): return base64.b64decode(result) +class MultiCallNotReady(Exception): + """Raised when a multicall result is accessed before the multicall""" + pass + + +class VirtualCall(object): + """Represents a call within a multicall""" + + def __init__(self, method, params): + self.method = method + self.params = params + self._result = MultiCallInProgress() + + def format(self): + '''return the call in the format needed for multiCall''' + return {'methodName': self.method, 'params': self.params} + + @property + def result(self): + result = self._result + if isinstance(result, MultiCallInProgress): + raise MultiCallNotReady() + if isinstance(result, dict): + fault = Fault(result['faultCode'], result['faultString']) + err = convertFault(fault) + raise err + # otherwise should be a singleton + return result[0] + + class MultiCallSession(object): """Manages a single multicall, acts like a session""" @@ -2872,13 +2902,14 @@ class MultiCallSession(object): return VirtualMethod(self._callMethod, name) def _callMethod(self, name, args, kwargs=None, retry=True): - """Make a call to the hub with retries and other niceties""" + """Add a new call to the multicall""" if kwargs is None: kwargs = {} args = encode_args(*args, **kwargs) - self._calls.append({'methodName': name, 'params': args}) - return MultiCallInProgress + ret = VirtualCall(name, args) + self._calls.append(ret) + return ret def callMethod(self, name, *args, **opts): """compatibility wrapper for _callMethod""" @@ -2898,15 +2929,18 @@ class MultiCallSession(object): calls = self._calls self._calls = [] - ret = self._callMethod('multiCall', (calls,), {}) + args = ([c.format() for c in calls],) + results = self._session._callMethod('multiCall', args, {}) + for call, result in zip(calls, results): + call._result = result if strict: #check for faults and raise first one - for entry in ret: + for entry in results: if isinstance(entry, dict): fault = Fault(entry['faultCode'], entry['faultString']) err = convertFault(fault) raise err - return ret + return results # alias for compatibility with ClientSession multiCall = call_all From cecffa84327576a0a2a73d86215013c2711b7dab Mon Sep 17 00:00:00 2001 From: Mike McLean Date: Fri, 11 May 2018 19:22:58 -0400 Subject: [PATCH 03/15] add a context manager --- koji/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/koji/__init__.py b/koji/__init__.py index 438f7711..b1c0439a 100644 --- a/koji/__init__.py +++ b/koji/__init__.py @@ -2945,6 +2945,16 @@ class MultiCallSession(object): # alias for compatibility with ClientSession multiCall = call_all + # implement a context manager + def __enter__(self): + return self + + def __exit__(self, _type, value, traceback): + if _type is None: + self.call_all() + # don't eat exceptions + return False + class DBHandler(logging.Handler): """ From abcc993b00c50557d29cdafd1695578fd79d1c10 Mon Sep 17 00:00:00 2001 From: Mike McLean Date: Fri, 11 May 2018 19:52:51 -0400 Subject: [PATCH 04/15] get a MultiCallSession by calling session.multicall --- koji/__init__.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/koji/__init__.py b/koji/__init__.py index b1c0439a..9a435f00 100644 --- a/koji/__init__.py +++ b/koji/__init__.py @@ -2132,13 +2132,27 @@ class ClientSession(object): self.opts = opts self.authtype = None self.setSession(sinfo) - self.multicall = False + self._multicall = MultiCallHack(self) self._calls = [] self.logger = logging.getLogger('koji') self.rsession = None self.new_session() self.opts.setdefault('timeout', DEFAULT_REQUEST_TIMEOUT) + @property + def multicall(self): + """The multicall property acts as a settable boolean or a callable + + This setup allows preserving the original multicall interface + alongside the new one without adding yet another similar sounding + attribute to the session (we already have both multicall and + multiCall). + """ + return self._multicall + + @multicall.setter + def multicall(self, value): + self._multicall.value = value def new_session(self): self.logger.debug("Opening new requests session") @@ -2858,6 +2872,26 @@ class ClientSession(object): return base64.b64decode(result) +class MultiCallHack(object): + """Workaround of a terribly overloaded namespace + + This allows session.multicall to act as a boolean value or a callable + """ + + def __init__(self, session): + self.value = False + self.session = session + + def __nonzero__(self): + return self.value + + def __bool__(self): + return self.value + + def __call__(self, **kw): + return MultiCallSession(self.session, **kw) + + class MultiCallNotReady(Exception): """Raised when a multicall result is accessed before the multicall""" pass From 4e0fe71acbcfa4740c481077f1d3b10253da07c2 Mon Sep 17 00:00:00 2001 From: Mike McLean Date: Fri, 11 May 2018 21:12:14 -0400 Subject: [PATCH 05/15] strict option for MultiCallSession class --- koji/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/koji/__init__.py b/koji/__init__.py index 9a435f00..8cf8d6b7 100644 --- a/koji/__init__.py +++ b/koji/__init__.py @@ -2926,8 +2926,9 @@ class MultiCallSession(object): """Manages a single multicall, acts like a session""" - def __init__(self, session): + def __init__(self, session, strict=False): self._session = session + self.strict = strict self._calls = [] def __getattr__(self, name): @@ -2949,7 +2950,7 @@ class MultiCallSession(object): """compatibility wrapper for _callMethod""" return self._callMethod(name, args, opts) - def call_all(self, strict=False): + def call_all(self, strict=None): """Perform all calls in a single multicall Returns a the hub's multiCall result, which is a list of results for @@ -2958,6 +2959,9 @@ class MultiCallSession(object): keys "faultCode", "faultString", and "traceback". """ + if strict is None: + strict = self.strict + if len(self._calls) == 0: return [] From 80a9d05d65ec077ac5f295fe9e6ba46e4466a12c Mon Sep 17 00:00:00 2001 From: Mike McLean Date: Tue, 5 Jun 2018 14:00:42 -0400 Subject: [PATCH 06/15] store args/kwargs in VirtualCall instances --- koji/__init__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/koji/__init__.py b/koji/__init__.py index 8cf8d6b7..271a456a 100644 --- a/koji/__init__.py +++ b/koji/__init__.py @@ -2900,14 +2900,16 @@ class MultiCallNotReady(Exception): class VirtualCall(object): """Represents a call within a multicall""" - def __init__(self, method, params): + def __init__(self, method, args, kwargs): self.method = method - self.params = params + self.args = args + self.kwargs = kwargs self._result = MultiCallInProgress() def format(self): '''return the call in the format needed for multiCall''' - return {'methodName': self.method, 'params': self.params} + return {'methodName': self.method, + 'params': encode_args(*self.args, **self.kwargs)} @property def result(self): @@ -2941,8 +2943,7 @@ class MultiCallSession(object): if kwargs is None: kwargs = {} - args = encode_args(*args, **kwargs) - ret = VirtualCall(name, args) + ret = VirtualCall(name, args, kwargs) self._calls.append(ret) return ret From 32290e4a3ecce94a408e441b9b2b42c37757a98d Mon Sep 17 00:00:00 2001 From: Mike McLean Date: Tue, 5 Jun 2018 14:05:09 -0400 Subject: [PATCH 07/15] cleanup --- koji/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/koji/__init__.py b/koji/__init__.py index 271a456a..2b01cb83 100644 --- a/koji/__init__.py +++ b/koji/__init__.py @@ -2934,8 +2934,6 @@ class MultiCallSession(object): self._calls = [] def __getattr__(self, name): - #if name[:1] == '_': - # raise AttributeError("no attribute %r" % name) return VirtualMethod(self._callMethod, name) def _callMethod(self, name, args, kwargs=None, retry=True): @@ -2973,7 +2971,7 @@ class MultiCallSession(object): for call, result in zip(calls, results): call._result = result if strict: - #check for faults and raise first one + # check for faults and raise first one for entry in results: if isinstance(entry, dict): fault = Fault(entry['faultCode'], entry['faultString']) From c06c34c52993ee254f6028cd53dfe92e6331f915 Mon Sep 17 00:00:00 2001 From: Mike McLean Date: Thu, 16 Aug 2018 13:40:05 -0400 Subject: [PATCH 08/15] stub unit test --- tests/test_lib/test_multicall_session.py | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 tests/test_lib/test_multicall_session.py diff --git a/tests/test_lib/test_multicall_session.py b/tests/test_lib/test_multicall_session.py new file mode 100644 index 00000000..cd514e72 --- /dev/null +++ b/tests/test_lib/test_multicall_session.py @@ -0,0 +1,26 @@ +import mock +try: + import unittest2 as unittest +except ImportError: + import unittest + +import koji + + +class TestNewMultiCall(unittest.TestCase): + + def setUp(self): + self._callMethod = mock.patch('koji.ClientSession._callMethod').start() + self.session = koji.ClientSession('FAKE_URL') + + def tearDown(self): + mock.patch.stopall() + + def test_basic_multicall(self): + with self.session.multicall() as m: + ret = {} + for i in range(10): + ret[i] = m.echo(i) + self._callMethod.assert_called_once() + self.assertEqual(self._callMethod.call_args[0][0], 'multiCall') + print self._callMethod.call_args From 64249cf753574b06e4aafc1425272d50ac232391 Mon Sep 17 00:00:00 2001 From: Mike McLean Date: Fri, 11 Jan 2019 16:31:53 -0500 Subject: [PATCH 09/15] provide multicall attribute for backwards compat --- koji/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/koji/__init__.py b/koji/__init__.py index 2b01cb83..ed53aa56 100644 --- a/koji/__init__.py +++ b/koji/__init__.py @@ -2982,6 +2982,12 @@ class MultiCallSession(object): # alias for compatibility with ClientSession multiCall = call_all + # more backwards compat + # multicall returns True but cannot be set + @property + def multicall(): + return True + # implement a context manager def __enter__(self): return self From 78e3f48d8bba6455d7d5044b2a4e6037a8097a25 Mon Sep 17 00:00:00 2001 From: Mike McLean Date: Fri, 3 May 2019 20:49:19 -0400 Subject: [PATCH 10/15] expand unit test a little --- tests/test_lib/test_multicall_session.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/test_lib/test_multicall_session.py b/tests/test_lib/test_multicall_session.py index cd514e72..5d15d580 100644 --- a/tests/test_lib/test_multicall_session.py +++ b/tests/test_lib/test_multicall_session.py @@ -23,4 +23,11 @@ class TestNewMultiCall(unittest.TestCase): ret[i] = m.echo(i) self._callMethod.assert_called_once() self.assertEqual(self._callMethod.call_args[0][0], 'multiCall') - print self._callMethod.call_args + self.assertEqual(self._callMethod.call_args[0][2], {}) + _calls = self._callMethod.call_args[0][1] + if not isinstance(_calls, tuple) or len(_calls) != 1: + raise Exception('multiCall args not wrapped in singleton') + calls = _calls[0] + for i in range(10): + self.assertEqual(calls[i]['methodName'], "echo") + self.assertEqual(calls[i]['params'], (i,)) From 37e8afcf8cf0f37bef49a3b21a65c7b877dd8fc5 Mon Sep 17 00:00:00 2001 From: Mike McLean Date: Fri, 3 May 2019 21:11:48 -0400 Subject: [PATCH 11/15] support batch option in MultiCallSession --- koji/__init__.py | 26 ++++++++++++++++-------- tests/test_lib/test_multicall_session.py | 22 ++++++++++++++++++++ 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/koji/__init__.py b/koji/__init__.py index ed53aa56..f2d4c67e 100644 --- a/koji/__init__.py +++ b/koji/__init__.py @@ -2928,9 +2928,10 @@ class MultiCallSession(object): """Manages a single multicall, acts like a session""" - def __init__(self, session, strict=False): + def __init__(self, session, strict=False, batch=None): self._session = session - self.strict = strict + self._strict = strict + self._batch = batch self._calls = [] def __getattr__(self, name): @@ -2949,7 +2950,7 @@ class MultiCallSession(object): """compatibility wrapper for _callMethod""" return self._callMethod(name, args, opts) - def call_all(self, strict=None): + def call_all(self, strict=None, batch=None): """Perform all calls in a single multicall Returns a the hub's multiCall result, which is a list of results for @@ -2959,17 +2960,26 @@ class MultiCallSession(object): """ if strict is None: - strict = self.strict + strict = self._strict + if batch is None: + batch = self._batch if len(self._calls) == 0: return [] calls = self._calls self._calls = [] - args = ([c.format() for c in calls],) - results = self._session._callMethod('multiCall', args, {}) - for call, result in zip(calls, results): - call._result = result + if batch: + batches = [calls[i:i+batch] for i in range(0, len(calls), batch)] + else: + batches = [calls] + results = [] + for calls in batches: + args = ([c.format() for c in calls],) + _results = self._session._callMethod('multiCall', args, {}) + for call, result in zip(calls, _results): + call._result = result + results.extend(_results) if strict: # check for faults and raise first one for entry in results: diff --git a/tests/test_lib/test_multicall_session.py b/tests/test_lib/test_multicall_session.py index 5d15d580..15b9142d 100644 --- a/tests/test_lib/test_multicall_session.py +++ b/tests/test_lib/test_multicall_session.py @@ -31,3 +31,25 @@ class TestNewMultiCall(unittest.TestCase): for i in range(10): self.assertEqual(calls[i]['methodName'], "echo") self.assertEqual(calls[i]['params'], (i,)) + + def test_batch_multicall(self): + with self.session.multicall(batch=10) as m: + ret = {} + for i in range(42): + ret[i] = m.echo(i) + + # should be 5 batches + self.assertEqual(self._callMethod.call_count, 5) + i = 0 + for args, kwargs in self._callMethod.call_args_list: + self.assertEqual(kwargs, {}) + self.assertEqual(args[0], 'multiCall') + self.assertEqual(args[2], {}) + _calls = args[1] + if not isinstance(_calls, tuple) or len(_calls) != 1: + raise Exception('multiCall args not wrapped in singleton') + calls = _calls[0] + for call in calls: + self.assertEqual(call['methodName'], "echo") + self.assertEqual(call['params'], (i,)) + i += 1 From 29f62a090bf30f601554ec91f42c4ef965faecf2 Mon Sep 17 00:00:00 2001 From: Mike McLean Date: Sat, 4 May 2019 12:37:36 -0400 Subject: [PATCH 12/15] update docstring --- koji/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/koji/__init__.py b/koji/__init__.py index f2d4c67e..50e15b80 100644 --- a/koji/__init__.py +++ b/koji/__init__.py @@ -2951,12 +2951,12 @@ class MultiCallSession(object): return self._callMethod(name, args, opts) def call_all(self, strict=None, batch=None): - """Perform all calls in a single multicall + """Perform all calls in one or more multiCall batches - Returns a the hub's multiCall result, which is a list of results for - each call. For successful calls, the entry will be a singleton list. - For calls that raised a fault, the entry will be a dictionary with - keys "faultCode", "faultString", and "traceback". + Returns a list of results for each call. For successful calls, the + entry will be a singleton list. For calls that raised a fault, the + entry will be a dictionary with keys "faultCode", "faultString", + and "traceback". """ if strict is None: From 14b25b1f4b9226d6c698faab20725cd8dbc18a9f Mon Sep 17 00:00:00 2001 From: Mike McLean Date: Sat, 4 May 2019 15:56:32 -0400 Subject: [PATCH 13/15] use multicall in a few more places in the cli --- cli/koji_cli/commands.py | 18 +++++++++--------- tests/test_cli/test_set_pkg_arches.py | 5 ++++- tests/test_cli/test_set_pkg_owner.py | 5 ++++- tests/test_cli/test_unblock_pkg.py | 5 ++++- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/cli/koji_cli/commands.py b/cli/koji_cli/commands.py index d231d52f..eb8070f0 100644 --- a/cli/koji_cli/commands.py +++ b/cli/koji_cli/commands.py @@ -6294,9 +6294,9 @@ def handle_set_pkg_arches(goptions, session, args): activate_session(session, goptions) arches = koji.parse_arches(args[0]) tag = args[1] - for package in args[2:]: - #really should implement multicall... - session.packageListSetArches(tag,package,arches,force=options.force) + with session.multicall(strict=True) as m: + for package in args[2:]: + m.packageListSetArches(tag,package,arches,force=options.force) def handle_set_pkg_owner(goptions, session, args): @@ -6312,9 +6312,9 @@ def handle_set_pkg_owner(goptions, session, args): activate_session(session, goptions) owner = args[0] tag = args[1] - for package in args[2:]: - #really should implement multicall... - session.packageListSetOwner(tag,package,owner,force=options.force) + with session.multicall(strict=True) as m: + for package in args[2:]: + m.packageListSetOwner(tag,package,owner,force=options.force) def handle_set_pkg_owner_global(goptions, session, args): @@ -6638,9 +6638,9 @@ def handle_unblock_pkg(goptions, session, args): assert False # pragma: no cover activate_session(session, goptions) tag = args[0] - for package in args[1:]: - #really should implement multicall... - session.packageListUnblock(tag,package) + with session.multicall(strict=True) as m: + for package in args[1:]: + m.packageListUnblock(tag,package) def anon_handle_download_build(options, session, args): diff --git a/tests/test_cli/test_set_pkg_arches.py b/tests/test_cli/test_set_pkg_arches.py index 794040d5..0211e441 100644 --- a/tests/test_cli/test_set_pkg_arches.py +++ b/tests/test_cli/test_set_pkg_arches.py @@ -47,10 +47,13 @@ class TestSetPkgArches(utils.CliTestCase): activate_session_mock.assert_not_called() # Case 2. run set arch to x86_64 + multicall = mock.MagicMock() + multicall.__enter__.return_value = multicall + session.multicall.return_value = multicall calls = [mock.call('tag', pkg, 'x86_64', force=True) for pkg in arguments[3:]] handle_set_pkg_arches(options, session, arguments) activate_session_mock.assert_called_with(session, options) - session.packageListSetArches.assert_has_calls(calls) + multicall.packageListSetArches.assert_has_calls(calls) self.assert_console_message(stdout, '') def test_handle_set_pkg_arches_help(self): diff --git a/tests/test_cli/test_set_pkg_owner.py b/tests/test_cli/test_set_pkg_owner.py index 77bda431..968f6038 100644 --- a/tests/test_cli/test_set_pkg_owner.py +++ b/tests/test_cli/test_set_pkg_owner.py @@ -47,10 +47,13 @@ class TestSetPkgOwner(utils.CliTestCase): activate_session_mock.assert_not_called() # Case 2. run set owner + multicall = mock.MagicMock() + multicall.__enter__.return_value = multicall + session.multicall.return_value = multicall calls = [mock.call('tag', pkg, 'owner', force=True) for pkg in arguments[3:]] handle_set_pkg_owner(options, session, arguments) activate_session_mock.assert_called_with(session, options) - session.packageListSetOwner.assert_has_calls(calls) + multicall.packageListSetOwner.assert_has_calls(calls) self.assert_console_message(stdout, '') def test_handle_set_pkg_owner_help(self): diff --git a/tests/test_cli/test_unblock_pkg.py b/tests/test_cli/test_unblock_pkg.py index 6529c5fe..2b6555cf 100644 --- a/tests/test_cli/test_unblock_pkg.py +++ b/tests/test_cli/test_unblock_pkg.py @@ -48,10 +48,13 @@ class TestUnblockPkg(utils.CliTestCase): activate_session_mock.assert_not_called() # Case 2. run unlock + multicall = mock.MagicMock() + multicall.__enter__.return_value = multicall + session.multicall.return_value = multicall calls = [mock.call('tag', pkg) for pkg in arguments[1:]] handle_unblock_pkg(options, session, arguments) activate_session_mock.assert_called_with(session, options) - session.packageListUnblock.assert_has_calls(calls) + multicall.packageListUnblock.assert_has_calls(calls) self.assert_console_message(stdout, '') def test_handle_unblock_pkg_help(self): From 71c4146638a663038d8219376bf782796dac70bb Mon Sep 17 00:00:00 2001 From: Mike McLean Date: Fri, 14 Jun 2019 12:21:16 -0400 Subject: [PATCH 14/15] update multicall docs --- docs/source/writing_koji_code.rst | 67 +++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/docs/source/writing_koji_code.rst b/docs/source/writing_koji_code.rst index 6aa911b2..649be76a 100644 --- a/docs/source/writing_koji_code.rst +++ b/docs/source/writing_koji_code.rst @@ -185,6 +185,73 @@ The ``ClientSession`` class provides support for this and there are several examples in the existing client code. Some examples in the cli include: ``edit-host``, ``add-pkg``, ``disable-host``, and ``list-hosts``. +There are two ways to use multicall. +The original modal method works within the ``ClientSession`` object and +prevents making other normal calls until the multicall is completed. +The newer method uses a separate ``MultiCallSession`` object and is much +more flexible. + +**Using MultiCallSession** + +Note: this feature was added in Koji version 1.18. + +A ``MultiCallSession`` object is used to track an individual multicall attached +to a session. +To create one, you can simply call your session's ``multicall`` method. +Once created, the object can be used like a session, but calls are stored +rather can sent immediately. +The stored calls are executed by calling the ``call_all()`` method. + +:: + + m = session.multicall() + for task_id in mylist: + m.cancelTask(task_id) + m.call_all() + +This object can also be used as a context manager, so the following is +equivalent: + +:: + + with session.multicall() as m: + for task_id in mylist: + m.cancelTask(task_id) + +Method calls to a ``MultiCallSession`` object return a ``VirtualCall`` object +that stands in for the result. +Once the multicall is executed, the result of each call can be accessed via +the ``result`` property of the ``VirtualCall`` object. +Accessing the ``result`` property before the call is executed will result in +an error. + +:: + + with session.multicall() as m: + tags = [m.getTag(tag_id) for tag_id in mylist] + for tag in tags: + print(tag.result['name']) + +There are two parameters affecting the behavior of the multicall. +If the ``strict`` parameter is set to True, the multicall will raise the first +error it encounters, if any. +If the ``batch`` parameter is set to a number greater than zero, the multicall +will spread the calls across multiple multicall batches of at most that number. +These parameters may be passed when the ``MultiCallSession`` is initialized, +or they may be passed to the ``call_all`` method. + +:: + + with session.multicall(strict=True, batch=500): + builds = [m.getBuild(build_id) for build_id in mylist] + + +**Using ClientSession.multiCall** + +Note: this approach is still supported, but we highly recommend using +``MultiCallSession`` as described above, unless you need to support Koji +versions prior to 1.18. + To use the feature, you first set the ``multicall`` attribute of the session to ``True``. Once this is done, the session will not immediately process further calls but will instead store their parameters for later. To tell the From 19bb507b5ec85d4b79ebf71e238e068b5a065880 Mon Sep 17 00:00:00 2001 From: Mike McLean Date: Mon, 17 Jun 2019 09:36:14 -0400 Subject: [PATCH 15/15] fix typo --- docs/source/writing_koji_code.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/writing_koji_code.rst b/docs/source/writing_koji_code.rst index 649be76a..41d66925 100644 --- a/docs/source/writing_koji_code.rst +++ b/docs/source/writing_koji_code.rst @@ -174,7 +174,7 @@ can fix this behavior in ``koji/__init__.py`` in the \_taskLabel function. Here you can define the string(s) to display when Koji receives status on a task. That is the return value. -Using multiCall +Using multicall ~~~~~~~~~~~~~~~ Koji supports a multicall feature where many calls are passed to the @@ -199,7 +199,7 @@ A ``MultiCallSession`` object is used to track an individual multicall attached to a session. To create one, you can simply call your session's ``multicall`` method. Once created, the object can be used like a session, but calls are stored -rather can sent immediately. +rather than sent immediately. The stored calls are executed by calling the ``call_all()`` method. ::