diff -Nru jsonrpclib-pelix-0.3.1/debian/changelog jsonrpclib-pelix-0.4.1/debian/changelog --- jsonrpclib-pelix-0.3.1/debian/changelog 2017-12-06 19:50:05.000000000 +0000 +++ jsonrpclib-pelix-0.4.1/debian/changelog 2020-07-23 07:15:28.000000000 +0000 @@ -1,6 +1,28 @@ +jsonrpclib-pelix (0.4.1-1) unstable; urgency=medium + + [ Ondřej Nový ] + * d/control: Set Vcs-* to salsa.debian.org + * Convert git repository from git-dpm to gbp layout + * Use debhelper-compat instead of debian/compat. + + [ Debian Janitor ] + * Bump debhelper from deprecated 9 to 12. + * Set upstream metadata fields: Bug-Database, Bug-Submit, Repository, + Repository-Browse. + + [ Tristan Seligmann ] + * New upstream release. + * Disable tests; they are not shipped. + * Declare rootless build. + * Set Maintainer to DPMT (was uploader). + * Bump Standards-Version to 4.5.0 (no changes). + * Bump debhelper-compat to 13. + * Switch to dh-sequence-*. + + -- Tristan Seligmann Thu, 23 Jul 2020 09:15:28 +0200 + jsonrpclib-pelix (0.3.1-1) unstable; urgency=low * Initial release (closes: #884064) -- Tristan Seligmann Wed, 06 Dec 2017 19:50:05 +0000 - diff -Nru jsonrpclib-pelix-0.3.1/debian/compat jsonrpclib-pelix-0.4.1/debian/compat --- jsonrpclib-pelix-0.3.1/debian/compat 2017-12-06 19:50:05.000000000 +0000 +++ jsonrpclib-pelix-0.4.1/debian/compat 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -9 diff -Nru jsonrpclib-pelix-0.3.1/debian/control jsonrpclib-pelix-0.4.1/debian/control --- jsonrpclib-pelix-0.3.1/debian/control 2017-12-06 19:50:05.000000000 +0000 +++ jsonrpclib-pelix-0.4.1/debian/control 2020-07-23 07:15:28.000000000 +0000 @@ -1,14 +1,18 @@ Source: jsonrpclib-pelix Section: python Priority: optional -Maintainer: Tristan Seligmann -Uploaders: - Debian Python Modules Team , -Build-Depends: debhelper (>= 9), dh-python, python3-all, python3-setuptools -Standards-Version: 4.1.2 +Maintainer: Debian Python Modules Team +Uploaders: Tristan Seligmann +Build-Depends: + debhelper-compat (= 13), + dh-sequence-python3, + python3-all, + python3-setuptools, +Standards-Version: 4.5.0 Homepage: https://github.com/tcalmant/jsonrpclib/ -Vcs-Git: https://anonscm.debian.org/git/python-modules/packages/jsonrpclib-pelix.git -Vcs-Browser: https://anonscm.debian.org/git/python-modules/packages/jsonrpclib-pelix.git +Vcs-Git: https://salsa.debian.org/python-team/modules/jsonrpclib-pelix.git +Vcs-Browser: https://salsa.debian.org/python-team/modules/jsonrpclib-pelix +Rules-Requires-Root: no Package: python3-jsonrpclib-pelix Architecture: all diff -Nru jsonrpclib-pelix-0.3.1/debian/.git-dpm jsonrpclib-pelix-0.4.1/debian/.git-dpm --- jsonrpclib-pelix-0.3.1/debian/.git-dpm 2017-12-06 19:50:05.000000000 +0000 +++ jsonrpclib-pelix-0.4.1/debian/.git-dpm 1970-01-01 00:00:00.000000000 +0000 @@ -1,11 +0,0 @@ -# see git-dpm(1) from git-dpm package -7bf68e286ce643e66e07b6ee1563f3e214a215a0 -7bf68e286ce643e66e07b6ee1563f3e214a215a0 -7bf68e286ce643e66e07b6ee1563f3e214a215a0 -7bf68e286ce643e66e07b6ee1563f3e214a215a0 -jsonrpclib-pelix_0.3.1.orig.tar.gz -a2b924e23c2a0280783e44938e80b540f34f0bd6 -29997 -debianTag="debian/%e%v" -patchedTag="patched/%e%v" -upstreamTag="upstream/%e%u" diff -Nru jsonrpclib-pelix-0.3.1/debian/python3-jsonrpclib-pelix.docs jsonrpclib-pelix-0.4.1/debian/python3-jsonrpclib-pelix.docs --- jsonrpclib-pelix-0.3.1/debian/python3-jsonrpclib-pelix.docs 2017-12-06 19:50:05.000000000 +0000 +++ jsonrpclib-pelix-0.4.1/debian/python3-jsonrpclib-pelix.docs 2020-07-23 07:15:28.000000000 +0000 @@ -1 +1 @@ -README.rst +README.md diff -Nru jsonrpclib-pelix-0.3.1/debian/rules jsonrpclib-pelix-0.4.1/debian/rules --- jsonrpclib-pelix-0.3.1/debian/rules 2017-12-06 19:50:05.000000000 +0000 +++ jsonrpclib-pelix-0.4.1/debian/rules 2020-07-23 07:15:28.000000000 +0000 @@ -1,5 +1,9 @@ #! /usr/bin/make -f export PYBUILD_NAME=jsonrpclib-pelix + +# The test suite is not shipped +export PYBUILD_DISABLE=test + %: - dh $@ --with python3 --buildsystem=pybuild + dh $@ --buildsystem=pybuild diff -Nru jsonrpclib-pelix-0.3.1/debian/upstream/metadata jsonrpclib-pelix-0.4.1/debian/upstream/metadata --- jsonrpclib-pelix-0.3.1/debian/upstream/metadata 1970-01-01 00:00:00.000000000 +0000 +++ jsonrpclib-pelix-0.4.1/debian/upstream/metadata 2020-07-23 07:15:28.000000000 +0000 @@ -0,0 +1,5 @@ +--- +Bug-Database: https://github.com/tcalmant/jsonrpclib/issues +Bug-Submit: https://github.com/tcalmant/jsonrpclib/issues/new +Repository: https://github.com/tcalmant/jsonrpclib.git +Repository-Browse: https://github.com/tcalmant/jsonrpclib diff -Nru jsonrpclib-pelix-0.3.1/jsonrpclib/config.py jsonrpclib-pelix-0.4.1/jsonrpclib/config.py --- jsonrpclib-pelix-0.3.1/jsonrpclib/config.py 2017-06-27 08:56:32.000000000 +0000 +++ jsonrpclib-pelix-0.4.1/jsonrpclib/config.py 2020-04-12 14:34:55.000000000 +0000 @@ -1,139 +1,139 @@ -#!/usr/bin/python -# -- Content-Encoding: UTF-8 -- -""" -The configuration module. - -:copyright: Copyright 2017, Thomas Calmant -:license: Apache License 2.0 -:version: 0.3.1 - -.. - - Copyright 2017 Thomas Calmant - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -""" - -import sys - -# ------------------------------------------------------------------------------ - -# Module version -__version_info__ = (0, 3, 1) -__version__ = ".".join(str(x) for x in __version_info__) - -# Documentation strings format -__docformat__ = "restructuredtext en" - -# ------------------------------------------------------------------------------ - - -class LocalClasses(dict): - """ - Associates local classes with their names (used in the jsonclass module) - """ - def add(self, cls, name=None): - """ - Stores a local class - - :param cls: A class - :param name: Custom name used in the __jsonclass__ attribute - """ - self[name or cls.__name__] = cls - -# ------------------------------------------------------------------------------ - - -class Config(object): - """ - This is pretty much used exclusively for the 'jsonclass' - functionality... set use_jsonclass to False to turn it off. - You can change serialize_method and ignore_attribute, or use - the local_classes.add(class) to include "local" classes. - """ - def __init__(self, version=2.0, content_type="application/json-rpc", - user_agent=None, use_jsonclass=True, - serialize_method='_serialize', - ignore_attribute='_ignore', - serialize_handlers=None): - """ - Sets up a configuration of JSONRPClib - - :param version: JSON-RPC specification version - :param content_type: HTTP content type header value - :param user_agent: The HTTP request user agent - :param use_jsonclass: Allow bean marshalling - :param serialize_method: A string that references the method on a - custom class object which is responsible for - returning a tuple of the arguments and a dict - of attributes. - :param ignore_attribute: A string that references the attribute on a - custom class object which holds strings and/or - references of the attributes the class - translator should ignore. - :param serialize_handlers: A dictionary of dump handler functions by - type for additional type support and for - overriding dump of built-in types in utils - """ - # JSON-RPC specification - self.version = version - - # Change to False to keep __jsonclass__ entries raw. - self.use_jsonclass = use_jsonclass - - # it SHOULD be 'application/json-rpc' - # but MAY be 'application/json' or 'application/jsonrequest' - self.content_type = content_type - - # Default user agent - if user_agent is None: - user_agent = 'jsonrpclib/{0} (Python {1})'.format( - __version__, - '.'.join(str(ver) for ver in sys.version_info[0:3])) - self.user_agent = user_agent - - # The list of classes to use for jsonclass translation. - self.classes = LocalClasses() - - # The serialize_method should be a string that references the - # method on a custom class object which is responsible for - # returning a tuple of the constructor arguments and a dict of - # attributes. - self.serialize_method = serialize_method - - # The ignore attribute should be a string that references the - # attribute on a custom class object which holds strings and / or - # references of the attributes the class translator should ignore. - self.ignore_attribute = ignore_attribute - - # The list of serialize handler functions for jsonclass dump. - # Used for handling additional types and overriding built-in types. - # Functions are expected to have the same parameters as jsonclass dump - # (possibility to call standard jsonclass dump function within). - self.serialize_handlers = serialize_handlers or {} - - def copy(self): - """ - Returns a shallow copy of this configuration bean - - :return: A shallow copy of this configuration - """ - new_config = Config(self.version, self.content_type, self.user_agent, - self.use_jsonclass, self.serialize_method, - self.ignore_attribute, None) - new_config.classes = self.classes.copy() - new_config.serialize_handlers = self.serialize_handlers.copy() - return new_config - -# Default configuration -DEFAULT = Config() +#!/usr/bin/python +# -- Content-Encoding: UTF-8 -- +""" +The configuration module. + +:copyright: Copyright 2020, Thomas Calmant +:license: Apache License 2.0 +:version: 0.4.1 + +.. + + Copyright 2020 Thomas Calmant + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +import sys + +# ------------------------------------------------------------------------------ + +# Module version +__version_info__ = (0, 4, 1) +__version__ = ".".join(str(x) for x in __version_info__) + +# Documentation strings format +__docformat__ = "restructuredtext en" + +# ------------------------------------------------------------------------------ + + +class LocalClasses(dict): + """ + Associates local classes with their names (used in the jsonclass module) + """ + def add(self, cls, name=None): + """ + Stores a local class + + :param cls: A class + :param name: Custom name used in the __jsonclass__ attribute + """ + self[name or cls.__name__] = cls + +# ------------------------------------------------------------------------------ + + +class Config(object): + """ + This is pretty much used exclusively for the 'jsonclass' + functionality... set use_jsonclass to False to turn it off. + You can change serialize_method and ignore_attribute, or use + the local_classes.add(class) to include "local" classes. + """ + def __init__(self, version=2.0, content_type="application/json-rpc", + user_agent=None, use_jsonclass=True, + serialize_method='_serialize', + ignore_attribute='_ignore', + serialize_handlers=None): + """ + Sets up a configuration of JSONRPClib + + :param version: JSON-RPC specification version + :param content_type: HTTP content type header value + :param user_agent: The HTTP request user agent + :param use_jsonclass: Allow bean marshalling + :param serialize_method: A string that references the method on a + custom class object which is responsible for + returning a tuple of the arguments and a dict + of attributes. + :param ignore_attribute: A string that references the attribute on a + custom class object which holds strings and/or + references of the attributes the class + translator should ignore. + :param serialize_handlers: A dictionary of dump handler functions by + type for additional type support and for + overriding dump of built-in types in utils + """ + # JSON-RPC specification + self.version = version + + # Change to False to keep __jsonclass__ entries raw. + self.use_jsonclass = use_jsonclass + + # it SHOULD be 'application/json-rpc' + # but MAY be 'application/json' or 'application/jsonrequest' + self.content_type = content_type + + # Default user agent + if user_agent is None: + user_agent = 'jsonrpclib/{0} (Python {1})'.format( + __version__, + '.'.join(str(ver) for ver in sys.version_info[0:3])) + self.user_agent = user_agent + + # The list of classes to use for jsonclass translation. + self.classes = LocalClasses() + + # The serialize_method should be a string that references the + # method on a custom class object which is responsible for + # returning a tuple of the constructor arguments and a dict of + # attributes. + self.serialize_method = serialize_method + + # The ignore attribute should be a string that references the + # attribute on a custom class object which holds strings and / or + # references of the attributes the class translator should ignore. + self.ignore_attribute = ignore_attribute + + # The list of serialize handler functions for jsonclass dump. + # Used for handling additional types and overriding built-in types. + # Functions are expected to have the same parameters as jsonclass dump + # (possibility to call standard jsonclass dump function within). + self.serialize_handlers = serialize_handlers or {} + + def copy(self): + """ + Returns a shallow copy of this configuration bean + + :return: A shallow copy of this configuration + """ + new_config = Config(self.version, self.content_type, self.user_agent, + self.use_jsonclass, self.serialize_method, + self.ignore_attribute, None) + new_config.classes = self.classes.copy() + new_config.serialize_handlers = self.serialize_handlers.copy() + return new_config + +# Default configuration +DEFAULT = Config() diff -Nru jsonrpclib-pelix-0.3.1/jsonrpclib/history.py jsonrpclib-pelix-0.4.1/jsonrpclib/history.py --- jsonrpclib-pelix-0.3.1/jsonrpclib/history.py 2017-06-27 08:56:32.000000000 +0000 +++ jsonrpclib-pelix-0.4.1/jsonrpclib/history.py 2020-04-12 14:34:55.000000000 +0000 @@ -1,93 +1,93 @@ -#!/usr/bin/python -# -- Content-Encoding: UTF-8 -- -""" -The history module. - -:authors: Josh Marshall, Thomas Calmant -:copyright: Copyright 2017, Thomas Calmant -:license: Apache License 2.0 -:version: 0.3.1 - -.. - - Copyright 2017 Thomas Calmant - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -""" - -# Module version -__version_info__ = (0, 3, 1) -__version__ = ".".join(str(x) for x in __version_info__) - -# Documentation strings format -__docformat__ = "restructuredtext en" - -# ------------------------------------------------------------------------------ - - -class History(object): - """ - This holds all the response and request objects for a - session. A server using this should call "clear" after - each request cycle in order to keep it from clogging - memory. - """ - def __init__(self): - """ - Sets up members - """ - self.requests = [] - self.responses = [] - - def add_response(self, response_obj): - """ - Adds a response to the history - - :param response_obj: Response content - """ - self.responses.append(response_obj) - - def add_request(self, request_obj): - """ - Adds a request to the history - - :param request_obj: A request object - """ - self.requests.append(request_obj) - - @property - def request(self): - """ - Returns the latest stored request or None - """ - try: - return self.requests[-1] - except IndexError: - return None - - @property - def response(self): - """ - Returns the latest stored response or None - """ - try: - return self.responses[-1] - except IndexError: - return None - - def clear(self): - """ - Clears the history lists - """ - del self.requests[:] - del self.responses[:] +#!/usr/bin/python +# -- Content-Encoding: UTF-8 -- +""" +The history module. + +:authors: Josh Marshall, Thomas Calmant +:copyright: Copyright 2020, Thomas Calmant +:license: Apache License 2.0 +:version: 0.4.1 + +.. + + Copyright 2020 Thomas Calmant + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +# Module version +__version_info__ = (0, 4, 1) +__version__ = ".".join(str(x) for x in __version_info__) + +# Documentation strings format +__docformat__ = "restructuredtext en" + +# ------------------------------------------------------------------------------ + + +class History(object): + """ + This holds all the response and request objects for a + session. A server using this should call "clear" after + each request cycle in order to keep it from clogging + memory. + """ + def __init__(self): + """ + Sets up members + """ + self.requests = [] + self.responses = [] + + def add_response(self, response_obj): + """ + Adds a response to the history + + :param response_obj: Response content + """ + self.responses.append(response_obj) + + def add_request(self, request_obj): + """ + Adds a request to the history + + :param request_obj: A request object + """ + self.requests.append(request_obj) + + @property + def request(self): + """ + Returns the latest stored request or None + """ + try: + return self.requests[-1] + except IndexError: + return None + + @property + def response(self): + """ + Returns the latest stored response or None + """ + try: + return self.responses[-1] + except IndexError: + return None + + def clear(self): + """ + Clears the history lists + """ + del self.requests[:] + del self.responses[:] diff -Nru jsonrpclib-pelix-0.3.1/jsonrpclib/__init__.py jsonrpclib-pelix-0.4.1/jsonrpclib/__init__.py --- jsonrpclib-pelix-0.3.1/jsonrpclib/__init__.py 2017-06-27 08:56:32.000000000 +0000 +++ jsonrpclib-pelix-0.4.1/jsonrpclib/__init__.py 2020-04-12 14:34:55.000000000 +0000 @@ -1,34 +1,44 @@ -#!/usr/bin/python -# -- Content-Encoding: UTF-8 -- -""" -Aliases to ease access to jsonrpclib classes - -:authors: Josh Marshall, Thomas Calmant -:copyright: Copyright 2017, Thomas Calmant -:license: Apache License 2.0 -:version: 0.3.1 - -.. - - Copyright 2017 Thomas Calmant - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -""" - -# Easy access to utility methods and classes -from jsonrpclib.jsonrpc import Server, ServerProxy -from jsonrpclib.jsonrpc import MultiCall, Fault, ProtocolError, AppError -from jsonrpclib.jsonrpc import loads, dumps, load, dump -from jsonrpclib.jsonrpc import jloads, jdumps -import jsonrpclib.history as history -import jsonrpclib.utils as utils +#!/usr/bin/python +# -- Content-Encoding: UTF-8 -- +""" +Aliases to ease access to jsonrpclib classes + +:authors: Josh Marshall, Thomas Calmant +:copyright: Copyright 2020, Thomas Calmant +:license: Apache License 2.0 +:version: 0.4.1 + +.. + + Copyright 2020 Thomas Calmant + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +# Easy access to utility methods and classes +from jsonrpclib.jsonrpc import Server, ServerProxy +from jsonrpclib.jsonrpc import MultiCall, Fault, ProtocolError, AppError +from jsonrpclib.jsonrpc import loads, dumps, load, dump +from jsonrpclib.jsonrpc import jloads, jdumps +import jsonrpclib.history as history +import jsonrpclib.utils as utils + +# ------------------------------------------------------------------------------ + + +# Module version +__version_info__ = (0, 4, 1) +__version__ = ".".join(str(x) for x in __version_info__) + +# Documentation strings format +__docformat__ = "restructuredtext en" diff -Nru jsonrpclib-pelix-0.3.1/jsonrpclib/jsonclass.py jsonrpclib-pelix-0.4.1/jsonrpclib/jsonclass.py --- jsonrpclib-pelix-0.3.1/jsonrpclib/jsonclass.py 2017-06-27 08:56:32.000000000 +0000 +++ jsonrpclib-pelix-0.4.1/jsonrpclib/jsonclass.py 2020-04-12 14:34:55.000000000 +0000 @@ -1,292 +1,292 @@ -#!/usr/bin/python -# -- Content-Encoding: UTF-8 -- -""" -The serialization module - -:authors: Josh Marshall, Thomas Calmant -:copyright: Copyright 2017, Thomas Calmant -:license: Apache License 2.0 -:version: 0.3.1 - -.. - - Copyright 2017 Thomas Calmant - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -""" - -# Standard library -import inspect -import re - -# Local package -import jsonrpclib.config -import jsonrpclib.utils as utils - -# ------------------------------------------------------------------------------ - -# Module version -__version_info__ = (0, 3, 1) -__version__ = ".".join(str(x) for x in __version_info__) - -# Documentation strings format -__docformat__ = "restructuredtext en" - -# ------------------------------------------------------------------------------ - -# Supported transmitted code -SUPPORTED_TYPES = (utils.DictType,) + utils.ITERABLE_TYPES \ - + utils.PRIMITIVE_TYPES - -# Regex of invalid module characters -INVALID_MODULE_CHARS = r'[^a-zA-Z0-9\_\.]' - -# ------------------------------------------------------------------------------ - - -class TranslationError(Exception): - """ - Unmarshalling exception - """ - pass - - -def _slots_finder(clazz, fields_set): - """ - Recursively visits the class hierarchy to find all slots - - :param clazz: Class to analyze - :param fields_set: Set where to store __slots___ content - """ - # ... class level - try: - fields_set.update(clazz.__slots__) - except AttributeError: - pass - - # ... parent classes level - for base_class in clazz.__bases__: - _slots_finder(base_class, fields_set) - - -def _find_fields(obj): - """ - Returns the names of the fields of the given object - - :param obj: An object to analyze - :return: A set of field names - """ - # Find fields... - fields = set() - - # ... using __dict__ - try: - fields.update(obj.__dict__) - except AttributeError: - pass - - # ... using __slots__ - _slots_finder(obj.__class__, fields) - return fields - - -def dump(obj, serialize_method=None, ignore_attribute=None, ignore=None, - config=jsonrpclib.config.DEFAULT): - """ - Transforms the given object into a JSON-RPC compliant form. - Converts beans into dictionaries with a __jsonclass__ entry. - Doesn't change primitive types. - - :param obj: An object to convert - :param serialize_method: Custom serialization method - :param ignore_attribute: Name of the object attribute containing the names - of members to ignore - :param ignore: A list of members to ignore - :param config: A JSONRPClib Config instance - :return: A JSON-RPC compliant object - """ - # Normalize arguments - serialize_method = serialize_method or config.serialize_method - ignore_attribute = ignore_attribute or config.ignore_attribute - ignore = ignore or [] - - # Parse / return default "types"... - # Apply additional types, override built-in types - # (reminder: config.serialize_handlers is a dict) - try: - serializer = config.serialize_handlers[type(obj)] - except KeyError: - # Not a serializer - pass - else: - if serializer is not None: - return serializer(obj, serialize_method, ignore_attribute, - ignore, config) - - # Primitive - if isinstance(obj, utils.PRIMITIVE_TYPES): - return obj - - # Iterative - elif isinstance(obj, utils.ITERABLE_TYPES): - # List, set or tuple - return [dump(item, serialize_method, ignore_attribute, ignore, config) - for item in obj] - - elif isinstance(obj, utils.DictType): - # Dictionary - return {key: dump(value, serialize_method, ignore_attribute, - ignore, config) - for key, value in obj.items()} - - # It's not a standard type, so it needs __jsonclass__ - module_name = inspect.getmodule(type(obj)).__name__ - json_class = obj.__class__.__name__ - - if module_name not in ('', '__main__'): - json_class = '{0}.{1}'.format(module_name, json_class) - - # Keep the class name in the returned object - return_obj = {"__jsonclass__": [json_class]} - - # If a serialization method is defined.. - if hasattr(obj, serialize_method): - # Params can be a dict (keyword) or list (positional) - # Attrs MUST be a dict. - serialize = getattr(obj, serialize_method) - params, attrs = serialize() - return_obj['__jsonclass__'].append(params) - return_obj.update(attrs) - elif utils.is_enum(obj): - # Add parameters for enumerations - return_obj['__jsonclass__'].append([obj.value]) - else: - # Otherwise, try to figure it out - # Obviously, we can't assume to know anything about the - # parameters passed to __init__ - return_obj['__jsonclass__'].append([]) - - # Prepare filtering lists - known_types = SUPPORTED_TYPES + tuple(config.serialize_handlers) - ignore_list = getattr(obj, ignore_attribute, []) + ignore - - # Find fields and filter them by name - fields = _find_fields(obj) - fields.difference_update(ignore_list) - - # Dump field values - attrs = {} - for attr_name in fields: - attr_value = getattr(obj, attr_name) - if isinstance(attr_value, known_types) and \ - attr_value not in ignore_list: - attrs[attr_name] = dump(attr_value, serialize_method, - ignore_attribute, ignore, config) - return_obj.update(attrs) - - return return_obj - -# ------------------------------------------------------------------------------ - - -def load(obj, classes=None): - """ - If 'obj' is a dictionary containing a __jsonclass__ entry, converts the - dictionary item into a bean of this class. - - :param obj: An object from a JSON-RPC dictionary - :param classes: A custom {name: class} dictionary - :return: The loaded object - """ - # Primitive - if isinstance(obj, utils.PRIMITIVE_TYPES): - return obj - - # List, set or tuple - elif isinstance(obj, utils.ITERABLE_TYPES): - # This comes from a JSON parser, so it can only be a list... - return [load(entry) for entry in obj] - - # Otherwise, it's a dict type - elif '__jsonclass__' not in obj: - return {key: load(value) for key, value in obj.items()} - - # It's a dictionary, and it has a __jsonclass__ - orig_module_name = obj['__jsonclass__'][0] - params = obj['__jsonclass__'][1] - - # Validate the module name - if not orig_module_name: - raise TranslationError('Module name empty.') - - json_module_clean = re.sub(INVALID_MODULE_CHARS, '', orig_module_name) - if json_module_clean != orig_module_name: - raise TranslationError('Module name {0} has invalid characters.' - .format(orig_module_name)) - - # Load the class - json_module_parts = json_module_clean.split('.') - if classes and len(json_module_parts) == 1: - # Local class name -- probably means it won't work - try: - json_class = classes[json_module_parts[0]] - except KeyError: - raise TranslationError('Unknown class or module {0}.' - .format(json_module_parts[0])) - else: - # Module + class - json_class_name = json_module_parts.pop() - json_module_tree = '.'.join(json_module_parts) - try: - # Use fromlist to load the module itself, not the package - temp_module = __import__(json_module_tree, - fromlist=[json_class_name]) - except ImportError: - raise TranslationError('Could not import {0} from module {1}.' - .format(json_class_name, json_module_tree)) - - try: - json_class = getattr(temp_module, json_class_name) - except AttributeError: - raise TranslationError("Unknown class {0}.{1}." - .format(json_module_tree, json_class_name)) - - # Create the object - if isinstance(params, utils.ListType): - try: - new_obj = json_class(*params) - except TypeError as ex: - raise TranslationError("Error instantiating {0}: {1}" - .format(json_class.__name__, ex)) - elif isinstance(params, utils.DictType): - try: - new_obj = json_class(**params) - except TypeError as ex: - raise TranslationError("Error instantiating {0}: {1}" - .format(json_class.__name__, ex)) - else: - raise TranslationError("Constructor args must be a dict or a list, " - "not {0}".format(type(params).__name__)) - - # Remove the class information, as it must be ignored during the - # reconstruction of the object - raw_jsonclass = obj.pop('__jsonclass__') - - for key, value in obj.items(): - # Recursive loading - setattr(new_obj, key, load(value, classes)) - - # Restore the class information for further usage - obj['__jsonclass__'] = raw_jsonclass - - return new_obj +#!/usr/bin/python +# -- Content-Encoding: UTF-8 -- +""" +The serialization module + +:authors: Josh Marshall, Thomas Calmant +:copyright: Copyright 2020, Thomas Calmant +:license: Apache License 2.0 +:version: 0.4.1 + +.. + + Copyright 2020 Thomas Calmant + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +# Standard library +import inspect +import re + +# Local package +import jsonrpclib.config +import jsonrpclib.utils as utils + +# ------------------------------------------------------------------------------ + +# Module version +__version_info__ = (0, 4, 1) +__version__ = ".".join(str(x) for x in __version_info__) + +# Documentation strings format +__docformat__ = "restructuredtext en" + +# ------------------------------------------------------------------------------ + +# Supported transmitted code +SUPPORTED_TYPES = (utils.DictType,) + utils.ITERABLE_TYPES \ + + utils.PRIMITIVE_TYPES + +# Regex of invalid module characters +INVALID_MODULE_CHARS = r'[^a-zA-Z0-9\_\.]' + +# ------------------------------------------------------------------------------ + + +class TranslationError(Exception): + """ + Unmarshalling exception + """ + pass + + +def _slots_finder(clazz, fields_set): + """ + Recursively visits the class hierarchy to find all slots + + :param clazz: Class to analyze + :param fields_set: Set where to store __slots___ content + """ + # ... class level + try: + fields_set.update(clazz.__slots__) + except AttributeError: + pass + + # ... parent classes level + for base_class in clazz.__bases__: + _slots_finder(base_class, fields_set) + + +def _find_fields(obj): + """ + Returns the names of the fields of the given object + + :param obj: An object to analyze + :return: A set of field names + """ + # Find fields... + fields = set() + + # ... using __dict__ + try: + fields.update(obj.__dict__) + except AttributeError: + pass + + # ... using __slots__ + _slots_finder(obj.__class__, fields) + return fields + + +def dump(obj, serialize_method=None, ignore_attribute=None, ignore=None, + config=jsonrpclib.config.DEFAULT): + """ + Transforms the given object into a JSON-RPC compliant form. + Converts beans into dictionaries with a __jsonclass__ entry. + Doesn't change primitive types. + + :param obj: An object to convert + :param serialize_method: Custom serialization method + :param ignore_attribute: Name of the object attribute containing the names + of members to ignore + :param ignore: A list of members to ignore + :param config: A JSONRPClib Config instance + :return: A JSON-RPC compliant object + """ + # Normalize arguments + serialize_method = serialize_method or config.serialize_method + ignore_attribute = ignore_attribute or config.ignore_attribute + ignore = ignore or [] + + # Parse / return default "types"... + # Apply additional types, override built-in types + # (reminder: config.serialize_handlers is a dict) + try: + serializer = config.serialize_handlers[type(obj)] + except KeyError: + # Not a serializer + pass + else: + if serializer is not None: + return serializer(obj, serialize_method, ignore_attribute, + ignore, config) + + # Primitive + if isinstance(obj, utils.PRIMITIVE_TYPES): + return obj + + # Iterative + elif isinstance(obj, utils.ITERABLE_TYPES): + # List, set or tuple + return [dump(item, serialize_method, ignore_attribute, ignore, config) + for item in obj] + + elif isinstance(obj, utils.DictType): + # Dictionary + return {key: dump(value, serialize_method, ignore_attribute, + ignore, config) + for key, value in obj.items()} + + # It's not a standard type, so it needs __jsonclass__ + module_name = inspect.getmodule(type(obj)).__name__ + json_class = obj.__class__.__name__ + + if module_name not in ('', '__main__'): + json_class = '{0}.{1}'.format(module_name, json_class) + + # Keep the class name in the returned object + return_obj = {"__jsonclass__": [json_class]} + + # If a serialization method is defined.. + if hasattr(obj, serialize_method): + # Params can be a dict (keyword) or list (positional) + # Attrs MUST be a dict. + serialize = getattr(obj, serialize_method) + params, attrs = serialize() + return_obj['__jsonclass__'].append(params) + return_obj.update(attrs) + elif utils.is_enum(obj): + # Add parameters for enumerations + return_obj['__jsonclass__'].append([obj.value]) + else: + # Otherwise, try to figure it out + # Obviously, we can't assume to know anything about the + # parameters passed to __init__ + return_obj['__jsonclass__'].append([]) + + # Prepare filtering lists + known_types = SUPPORTED_TYPES + tuple(config.serialize_handlers) + ignore_list = getattr(obj, ignore_attribute, []) + ignore + + # Find fields and filter them by name + fields = _find_fields(obj) + fields.difference_update(ignore_list) + + # Dump field values + attrs = {} + for attr_name in fields: + attr_value = getattr(obj, attr_name) + if isinstance(attr_value, known_types) and \ + attr_value not in ignore_list: + attrs[attr_name] = dump(attr_value, serialize_method, + ignore_attribute, ignore, config) + return_obj.update(attrs) + + return return_obj + +# ------------------------------------------------------------------------------ + + +def load(obj, classes=None): + """ + If 'obj' is a dictionary containing a __jsonclass__ entry, converts the + dictionary item into a bean of this class. + + :param obj: An object from a JSON-RPC dictionary + :param classes: A custom {name: class} dictionary + :return: The loaded object + """ + # Primitive + if isinstance(obj, utils.PRIMITIVE_TYPES): + return obj + + # List, set or tuple + elif isinstance(obj, utils.ITERABLE_TYPES): + # This comes from a JSON parser, so it can only be a list... + return [load(entry) for entry in obj] + + # Otherwise, it's a dict type + elif '__jsonclass__' not in obj: + return {key: load(value) for key, value in obj.items()} + + # It's a dictionary, and it has a __jsonclass__ + orig_module_name = obj['__jsonclass__'][0] + params = obj['__jsonclass__'][1] + + # Validate the module name + if not orig_module_name: + raise TranslationError('Module name empty.') + + json_module_clean = re.sub(INVALID_MODULE_CHARS, '', orig_module_name) + if json_module_clean != orig_module_name: + raise TranslationError('Module name {0} has invalid characters.' + .format(orig_module_name)) + + # Load the class + json_module_parts = json_module_clean.split('.') + if classes and len(json_module_parts) == 1: + # Local class name -- probably means it won't work + try: + json_class = classes[json_module_parts[0]] + except KeyError: + raise TranslationError('Unknown class or module {0}.' + .format(json_module_parts[0])) + else: + # Module + class + json_class_name = json_module_parts.pop() + json_module_tree = '.'.join(json_module_parts) + try: + # Use fromlist to load the module itself, not the package + temp_module = __import__(json_module_tree, + fromlist=[json_class_name]) + except ImportError: + raise TranslationError('Could not import {0} from module {1}.' + .format(json_class_name, json_module_tree)) + + try: + json_class = getattr(temp_module, json_class_name) + except AttributeError: + raise TranslationError("Unknown class {0}.{1}." + .format(json_module_tree, json_class_name)) + + # Create the object + if isinstance(params, utils.ListType): + try: + new_obj = json_class(*params) + except TypeError as ex: + raise TranslationError("Error instantiating {0}: {1}" + .format(json_class.__name__, ex)) + elif isinstance(params, utils.DictType): + try: + new_obj = json_class(**params) + except TypeError as ex: + raise TranslationError("Error instantiating {0}: {1}" + .format(json_class.__name__, ex)) + else: + raise TranslationError("Constructor args must be a dict or a list, " + "not {0}".format(type(params).__name__)) + + # Remove the class information, as it must be ignored during the + # reconstruction of the object + raw_jsonclass = obj.pop('__jsonclass__') + + for key, value in obj.items(): + # Recursive loading + setattr(new_obj, key, load(value, classes)) + + # Restore the class information for further usage + obj['__jsonclass__'] = raw_jsonclass + + return new_obj diff -Nru jsonrpclib-pelix-0.3.1/jsonrpclib/jsonrpc.py jsonrpclib-pelix-0.4.1/jsonrpclib/jsonrpc.py --- jsonrpclib-pelix-0.3.1/jsonrpclib/jsonrpc.py 2017-06-27 08:56:32.000000000 +0000 +++ jsonrpclib-pelix-0.4.1/jsonrpclib/jsonrpc.py 2020-04-12 14:34:55.000000000 +0000 @@ -1,1286 +1,1387 @@ -#!/usr/bin/python -# -- Content-Encoding: UTF-8 -- -""" -============================ -JSONRPC Library (jsonrpclib) -============================ - -This library is a JSON-RPC v.2 (proposed) implementation which -follows the xmlrpclib API for portability between clients. It -uses the same Server / ServerProxy, loads, dumps, etc. syntax, -while providing features not present in XML-RPC like: - -* Keyword arguments -* Notifications -* Versioning -* Batches and batch notifications - -Eventually, I'll add a SimpleXMLRPCServer compatible library, -and other things to tie the thing off nicely. :) - -For a quick-start, just open a console and type the following, -replacing the server address, method, and parameters -appropriately. ->>> import jsonrpclib ->>> server = jsonrpclib.Server('http://localhost:8181') ->>> server.add(5, 6) -11 ->>> server._notify.add(5, 6) ->>> batch = jsonrpclib.MultiCall(server) ->>> batch.add(3, 50) ->>> batch.add(2, 3) ->>> batch._notify.add(3, 5) ->>> batch() -[53, 5] - -See https://github.com/tcalmant/jsonrpclib for more info. - -:authors: Josh Marshall, Thomas Calmant -:copyright: Copyright 2017, Thomas Calmant -:license: Apache License 2.0 -:version: 0.3.1 - -.. - - Copyright 2017 Thomas Calmant - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -""" - -# Standard library -import contextlib -import logging -import sys -import uuid - -try: - # Python 3 - # pylint: disable=F0401,E0611 - from urllib.parse import splittype, splithost - from xmlrpc.client import Transport as XMLTransport - from xmlrpc.client import SafeTransport as XMLSafeTransport - from xmlrpc.client import ServerProxy as XMLServerProxy - from xmlrpc.client import _Method as XML_Method -except ImportError: - # Python 2 - # pylint: disable=F0401,E0611 - from urllib import splittype, splithost - from xmlrpclib import Transport as XMLTransport - from xmlrpclib import SafeTransport as XMLSafeTransport - from xmlrpclib import ServerProxy as XMLServerProxy - from xmlrpclib import _Method as XML_Method - -try: - # Check GZip support - import gzip -except ImportError: - # Python can be built without zlib/gzip support - # pylint: disable=C0103 - gzip = None - -# Library includes -import jsonrpclib.config -import jsonrpclib.jsonclass as jsonclass -import jsonrpclib.utils as utils - -# ------------------------------------------------------------------------------ - -# Module version -__version_info__ = (0, 3, 1) -__version__ = ".".join(str(x) for x in __version_info__) - -# Documentation strings format -__docformat__ = "restructuredtext en" - -# Create the logger -_logger = logging.getLogger(__name__) - -# ------------------------------------------------------------------------------ -# JSON library import - -try: - # pylint: disable=F0401,E0611 - # Using cjson - import cjson - _logger.debug("Using cjson as JSON library") - - # Declare cjson methods - def jdumps(obj, encoding='utf-8'): - """ - Serializes ``obj`` to a JSON formatted string, using cjson. - """ - return cjson.encode(obj) - - def jloads(json_string): - """ - Deserializes ``json_string`` (a string containing a JSON document) - to a Python object, using cjson. - """ - return cjson.decode(json_string) -except ImportError: - # pylint: disable=F0401,E0611 - # Use json or simplejson - try: - import json - _logger.debug("Using json as JSON library") - except ImportError: - try: - import simplejson as json - _logger.debug("Using simplejson as JSON library") - except ImportError: - _logger.error("No supported JSON library found") - raise ImportError('You must have the cjson, json, or simplejson ' - 'module(s) available.') - - # Declare json methods - if sys.version_info[0] < 3: - def jdumps(obj, encoding='utf-8'): - """ - Serializes ``obj`` to a JSON formatted string. - """ - # Python 2 (explicit encoding) - return json.dumps(obj, encoding=encoding) - else: - # Python 3 - def jdumps(obj, encoding='utf-8'): - """ - Serializes ``obj`` to a JSON formatted string. - """ - # Python 3 (the encoding parameter has been removed) - return json.dumps(obj) - - def jloads(json_string): - """ - Deserializes ``json_string`` (a string containing a JSON document) - to a Python object. - """ - return json.loads(json_string) - -# ------------------------------------------------------------------------------ -# XMLRPClib re-implementations - - -class ProtocolError(Exception): - """ - JSON-RPC error - - ProtocolError.args[0] can be: - * an error message (string) - * a (code, message) tuple - """ - pass - - -class AppError(ProtocolError): - """ - Application error: the error code is not in the pre-defined ones - - AppError.args[0][0]: Error code - AppError.args[0][1]: Error message or trace - AppError.args[0][2]: Associated data - """ - def data(self): - """ - Retrieves the value found in the 'data' entry of the error, or None - - :return: The data associated to the error, or None - """ - return self.args[0][2] - - -class JSONParser(object): - """ - Default JSON parser - """ - def __init__(self, target): - """ - Associates the target loader to the parser - - :param target: a JSONTarget instance - """ - self.target = target - - def feed(self, data): - """ - Feeds the associated target with the given data - """ - self.target.feed(data) - - @staticmethod - def close(): - """ - Does nothing - """ - pass - - -class JSONTarget(object): - """ - Unmarshalls stream data to a string - """ - def __init__(self): - """ - Sets up the unmarshaller - """ - self.data = [] - - def feed(self, data): - """ - Stores the given raw data into a buffer - """ - # Store raw data as it might not contain whole wide-character - self.data.append(data) - - def close(self): - """ - Unmarshalls the buffered data - """ - if not self.data: - return '' - else: - # Use type to have a valid join (str vs. bytes) - data = type(self.data[0])().join(self.data) - try: - # Convert the whole final string - data = utils.from_bytes(data) - except: - # Try a pass-through - pass - - return data - - -class TransportMixIn(object): - """ Just extends the XML-RPC transport where necessary. """ - # for Python 2.7 support - _connection = None - - # List of non-overridable headers - # Use the configuration to change the content-type - readonly_headers = ('content-length', 'content-type') - - def __init__(self, config=jsonrpclib.config.DEFAULT, context=None): - """ - Sets up the transport - - :param config: A JSONRPClib Config instance - """ - # Store the configuration - self._config = config - - # Store the SSL context - self.context = context - - # Set up the user agent - self.user_agent = config.user_agent - - # Additional headers: list of dictionaries - self.additional_headers = [] - - # Avoid a pep-8 error - self.accept_gzip_encoding = True - self.verbose = False - - def push_headers(self, headers): - """ - Adds a dictionary of headers to the additional headers list - - :param headers: A dictionary - """ - self.additional_headers.append(headers) - - def pop_headers(self, headers): - """ - Removes the given dictionary from the additional headers list. - Also validates that given headers are on top of the stack - - :param headers: Headers to remove - :raise AssertionError: The given dictionary is not on the latest stored - in the additional headers list - """ - assert self.additional_headers[-1] == headers - self.additional_headers.pop() - - def emit_additional_headers(self, connection): - """ - Puts headers as is in the request, filtered read only headers - - :param connection: The request connection - :return: The dictionary of headers added to the connection - """ - additional_headers = {} - - # Setup extra headers - # (list of tuples, inherited from xmlrpclib.client.Transport) - # Authentication headers are stored there - try: - extra_headers = self._extra_headers or [] - except AttributeError: - # Not available this version of Python (should not happen) - pass - else: - for (key, value) in extra_headers: - additional_headers[key] = value - - # Prepare the merged dictionary - for headers in self.additional_headers: - additional_headers.update(headers) - - # Normalize keys and values - additional_headers = dict( - (str(key).lower(), str(value)) - for key, value in additional_headers.items()) - - # Remove forbidden keys - for forbidden in self.readonly_headers: - additional_headers.pop(forbidden, None) - - # Reversed order: in the case of multiple headers value definition, - # the latest pushed has priority - for key, value in additional_headers.items(): - connection.putheader(key, value) - - return additional_headers - - def single_request(self, host, handler, request_body, verbose=0): - """ - Send a complete request, and parse the response. - - From xmlrpclib in Python 2.7 - - :param host: Target host. - :param handler: Target RPC handler. - :param request_body: JSON-RPC request body. - :param verbose: Debugging flag. - :return: Parsed response. - """ - connection = self.make_connection(host) - try: - self.send_request(connection, handler, request_body, verbose) - self.send_content(connection, request_body) - - response = connection.getresponse() - if response.status == 200: - self.verbose = verbose - return self.parse_response(response) - except: - # All unexpected errors leave connection in - # a strange state, so we clear it. - self.close() - raise - - # Discard any response data and raise exception - if response.getheader("content-length", 0): - response.read() - raise ProtocolError(host + handler, - response.status, response.reason, - response.msg) - - def send_request(self, connection, handler, request_body, debug=0): - """ - Send HTTP request. - - From xmlrpc.client in Python 3.4 - - :param connection: Connection handle. - :param handler: Target RPC handler (a path relative to host) - :param request_body: The JSON-RPC request body - :param debug: Enable debugging if debug is true. - :return: An HTTPConnection. - """ - if debug: - connection.set_debuglevel(1) - if self.accept_gzip_encoding and gzip: - connection.putrequest("POST", handler, skip_accept_encoding=True) - connection.putheader("Accept-Encoding", "gzip") - else: - connection.putrequest("POST", handler) - - return connection - - def send_content(self, connection, request_body): - """ - Completes the request headers and sends the request body of a JSON-RPC - request over a HTTPConnection - - :param connection: An HTTPConnection object - :param request_body: JSON-RPC request body - """ - # Convert the body first - request_body = utils.to_bytes(request_body) - - # "static" headers - connection.putheader("Content-Type", self._config.content_type) - connection.putheader("Content-Length", str(len(request_body))) - - # Emit additional headers here in order not to override content-length - additional_headers = self.emit_additional_headers(connection) - - # Add the user agent, if not overridden - if "user-agent" not in additional_headers: - connection.putheader("User-Agent", self.user_agent) - - connection.endheaders() - if request_body: - connection.send(request_body) - - @staticmethod - def getparser(): - """ - Create an instance of the parser, and attach it to an unmarshalling - object. Return both objects. - - :return: The parser and unmarshaller instances - """ - target = JSONTarget() - return JSONParser(target), target - - -class Transport(TransportMixIn, XMLTransport): - """ - Mixed-in HTTP transport - """ - def __init__(self, config): - TransportMixIn.__init__(self, config) - XMLTransport.__init__(self) - - -class SafeTransport(TransportMixIn, XMLSafeTransport): - """ - Mixed-in HTTPS transport - """ - def __init__(self, config, context): - TransportMixIn.__init__(self, config, context) - XMLSafeTransport.__init__(self) - -# ------------------------------------------------------------------------------ - - -class ServerProxy(XMLServerProxy): - """ - Unfortunately, much more of this class has to be copied since - so much of it does the serialization. - """ - def __init__(self, uri, transport=None, encoding=None, - verbose=0, version=None, headers=None, history=None, - config=jsonrpclib.config.DEFAULT, context=None): - """ - Sets up the server proxy - - :param uri: Request URI - :param transport: Custom transport handler - :param encoding: Specified encoding - :param verbose: Log verbosity level - :param version: JSON-RPC specification version - :param headers: Custom additional headers for each request - :param history: History object (for tests) - :param config: A JSONRPClib Config instance - :param context: The optional SSLContext to use - """ - # Store the configuration - self._config = config - self.__version = version or config.version - - schema, uri = splittype(uri) - if schema not in ('http', 'https'): - _logger.error("jsonrpclib only support http(s) URIs, not %s", - schema) - raise IOError('Unsupported JSON-RPC protocol.') - - self.__host, self.__handler = splithost(uri) - if not self.__handler: - # Not sure if this is in the JSON spec? - self.__handler = '/' - - if transport is None: - if schema == 'https': - transport = SafeTransport(config=config, context=context) - else: - transport = Transport(config=config) - self.__transport = transport - - self.__encoding = encoding - self.__verbose = verbose - self.__history = history - - # Global custom headers are injected into Transport - self.__transport.push_headers(headers or {}) - - def _request(self, methodname, params, rpcid=None): - """ - Calls a method on the remote server - - :param methodname: Name of the method to call - :param params: Method parameters - :param rpcid: ID of the remote call - :return: The parsed result of the call - """ - request = dumps(params, methodname, encoding=self.__encoding, - rpcid=rpcid, version=self.__version, - config=self._config) - response = self._run_request(request) - check_for_errors(response) - return response['result'] - - def _request_notify(self, methodname, params, rpcid=None): - """ - Calls a method as a notification - - :param methodname: Name of the method to call - :param params: Method parameters - :param rpcid: ID of the remote call - """ - request = dumps(params, methodname, encoding=self.__encoding, - rpcid=rpcid, version=self.__version, notify=True, - config=self._config) - response = self._run_request(request, notify=True) - check_for_errors(response) - - def _run_request(self, request, notify=False): - """ - Sends the given request to the remote server - - :param request: The request to send - :param notify: Notification request flag (unused) - :return: The response as a parsed JSON object - """ - if self.__history is not None: - self.__history.add_request(request) - - response = self.__transport.request( - self.__host, - self.__handler, - request, - verbose=self.__verbose - ) - - # Here, the XMLRPC library translates a single list - # response to the single value -- should we do the - # same, and require a tuple / list to be passed to - # the response object, or expect the Server to be - # outputting the response appropriately? - - if self.__history is not None: - self.__history.add_response(response) - - if not response: - return None - else: - return_obj = loads(response, self._config) - return return_obj - - def __getattr__(self, name): - """ - Returns a callable object to call the remote service - """ - if name.startswith("__") and name.endswith("__"): - # Don't proxy special methods. - raise AttributeError("ServerProxy has no attribute '%s'" % name) - # Same as original, just with new _Method reference - return _Method(self._request, name) - - def __close(self): - """ - Closes the transport layer - """ - self.__transport.close() - - def __call__(self, attr): - """ - A workaround to get special attributes on the ServerProxy - without interfering with the magic __getattr__ - - (code from xmlrpclib in Python 2.7) - """ - if attr == "close": - return self.__close - elif attr == "transport": - return self.__transport - - raise AttributeError("Attribute {0} not found".format(attr)) - - @property - def _notify(self): - """ - Like __getattr__, but sending a notification request instead of a call - """ - return _Notify(self._request_notify) - - @contextlib.contextmanager - def _additional_headers(self, headers): - """ - Allows to specify additional headers, to be added inside the with - block. - Example of usage: - - >>> with client._additional_headers({'X-Test' : 'Test'}) as new_client: - ... new_client.method() - ... - >>> # Here old headers are restored - """ - self.__transport.push_headers(headers) - yield self - self.__transport.pop_headers(headers) - -# ------------------------------------------------------------------------------ - - -class _Method(XML_Method): - """ - Some magic to bind an JSON-RPC method to an RPC server. - """ - def __call__(self, *args, **kwargs): - """ - Sends an RPC request and returns the unmarshalled result - """ - if args and kwargs: - raise ProtocolError("Cannot use both positional and keyword " - "arguments (according to JSON-RPC spec.)") - if args: - return self.__send(self.__name, args) - else: - return self.__send(self.__name, kwargs) - - def __getattr__(self, name): - """ - Returns a Method object for nested calls - """ - if name == "__name__": - return self.__name - return _Method(self.__send, "{0}.{1}".format(self.__name, name)) - - def __repr__(self): - """ - Returns a string representation of the method - """ - # Must use __class__ here because the base class is old-style. - return "<{0} {1}>".format(self.__class__, self.__name) - - -class _Notify(object): - """ - Same as _Method, but to send notifications - """ - def __init__(self, request): - """ - Sets the method to call to send a request to the server - """ - self._request = request - - def __getattr__(self, name): - """ - Returns a Method object, to be called as a notification - """ - return _Method(self._request, name) - -# ------------------------------------------------------------------------------ -# Batch implementation - - -class MultiCallMethod(object): - """ - Stores calls made to a MultiCall object for batch execution - """ - def __init__(self, method, notify=False, config=jsonrpclib.config.DEFAULT): - """ - Sets up the store - - :param method: Name of the method to call - :param notify: Notification flag - :param config: Request configuration - """ - self.method = method - self.params = [] - self.notify = notify - self._config = config - - def __call__(self, *args, **kwargs): - """ - Normalizes call parameters - """ - if kwargs and args: - raise ProtocolError('JSON-RPC does not support both ' + - 'positional and keyword arguments.') - if kwargs: - self.params = kwargs - else: - self.params = args - - def request(self, encoding=None, rpcid=None): - """ - Returns the request object as JSON-formatted string - """ - return dumps(self.params, self.method, version=2.0, - encoding=encoding, rpcid=rpcid, notify=self.notify, - config=self._config) - - def __repr__(self): - """ - String representation - """ - return str(self.request()) - - def __getattr__(self, method): - """ - Updates the object for a nested call - """ - self.method = "{0}.{1}".format(self.method, method) - return self - - -class MultiCallNotify(object): - """ - Same as MultiCallMethod but for notifications - """ - def __init__(self, multicall, config=jsonrpclib.config.DEFAULT): - """ - Sets ip the store - - :param multicall: The parent MultiCall instance - :param config: Request configuration - """ - self.multicall = multicall - self._config = config - - def __getattr__(self, name): - """ - Returns the MultiCallMethod to use as a notification - """ - new_job = MultiCallMethod(name, notify=True, config=self._config) - self.multicall._job_list.append(new_job) - return new_job - - -class MultiCallIterator(object): - """ - Iterates over the results of a MultiCall. - Exceptions are raised in response to JSON-RPC faults - """ - def __init__(self, results): - """ - Sets up the results store - """ - self.results = results - - @staticmethod - def __get_result(item): - """ - Checks for error and returns the "real" result stored in a MultiCall - result. - """ - check_for_errors(item) - return item['result'] - - def __iter__(self): - """ - Iterates over all results - """ - for item in self.results: - yield self.__get_result(item) - raise StopIteration - - def __getitem__(self, i): - """ - Returns the i-th object of the results - """ - return self.__get_result(self.results[i]) - - def __len__(self): - """ - Returns the number of results stored - """ - return len(self.results) - - -class MultiCall(object): - """ - server -> a object used to boxcar method calls, where server should be a - ServerProxy object. - - Methods can be added to the MultiCall using normal - method call syntax e.g.: - - multicall = MultiCall(server_proxy) - multicall.add(2,3) - multicall.get_address("Guido") - - To execute the multicall, call the MultiCall object e.g.: - - add_result, address = multicall() - """ - def __init__(self, server, config=jsonrpclib.config.DEFAULT): - """ - Sets up the multicall - - :param server: A ServerProxy object - :param config: Request configuration - """ - self._server = server - self._job_list = [] - self._config = config - - def _request(self): - """ - Sends the request to the server and returns the responses - - :return: A MultiCallIterator object - """ - if len(self._job_list) < 1: - # Should we alert? This /is/ pretty obvious. - return - request_body = "[ {0} ]".format( - ','.join(job.request() for job in self._job_list)) - responses = self._server._run_request(request_body) - del self._job_list[:] - if not responses: - responses = [] - return MultiCallIterator(responses) - - @property - def _notify(self): - """ - Prepares a notification call - """ - return MultiCallNotify(self, self._config) - - def __getattr__(self, name): - """ - Registers a method call - """ - new_job = MultiCallMethod(name, config=self._config) - self._job_list.append(new_job) - return new_job - - __call__ = _request - -# These lines conform to xmlrpclib's "compatibility" line. -# Not really sure if we should include these, but oh well. -Server = ServerProxy - -# ------------------------------------------------------------------------------ - - -class Fault(object): - """ - JSON-RPC error class - """ - def __init__(self, code=-32000, message='Server error', rpcid=None, - config=jsonrpclib.config.DEFAULT, data=None): - """ - Sets up the error description - - :param code: Fault code - :param message: Associated message - :param rpcid: Request ID - :param config: A JSONRPClib Config instance - :param data: Extra information added to an error description - """ - self.faultCode = code - self.faultString = message - self.rpcid = rpcid - self.config = config - self.data = data - - def error(self): - """ - Returns the error as a dictionary - - :returns: A {'code', 'message'} dictionary - """ - return {'code': self.faultCode, 'message': self.faultString, - 'data': self.data} - - def response(self, rpcid=None, version=None): - """ - Returns the error as a JSON-RPC response string - - :param rpcid: Forced request ID - :param version: JSON-RPC version - :return: A JSON-RPC response string - """ - if not version: - version = self.config.version - - if rpcid: - self.rpcid = rpcid - - return dumps(self, methodresponse=True, rpcid=self.rpcid, - version=version, config=self.config) - - def dump(self, rpcid=None, version=None): - """ - Returns the error as a JSON-RPC response dictionary - - :param rpcid: Forced request ID - :param version: JSON-RPC version - :return: A JSON-RPC response dictionary - """ - if not version: - version = self.config.version - - if rpcid: - self.rpcid = rpcid - - return dump(self, is_response=True, rpcid=self.rpcid, - version=version, config=self.config) - - def __repr__(self): - """ - String representation - """ - return ''.format(self.faultCode, self.faultString) - - -class Payload(object): - """ - JSON-RPC content handler - """ - def __init__(self, rpcid=None, version=None, - config=jsonrpclib.config.DEFAULT): - """ - Sets up the JSON-RPC handler - - :param rpcid: Request ID - :param version: JSON-RPC version - :param config: A JSONRPClib Config instance - """ - if not version: - version = config.version - - self.id = rpcid - self.version = float(version) - - def request(self, method, params=None): - """ - Prepares a method call request - - :param method: Method name - :param params: Method parameters - :return: A JSON-RPC request dictionary - """ - if not isinstance(method, utils.STRING_TYPES): - raise ValueError('Method name must be a string.') - - if not self.id: - # Generate a request ID - self.id = str(uuid.uuid4()) - - request = {'id': self.id, 'method': method} - if params or self.version < 1.1: - request['params'] = params or [] - - if self.version >= 2: - request['jsonrpc'] = str(self.version) - - return request - - def notify(self, method, params=None): - """ - Prepares a notification request - - :param method: Notification name - :param params: Notification parameters - :return: A JSON-RPC notification dictionary - """ - # Prepare the request dictionary - request = self.request(method, params) - - # Remove the request ID, as it's a notification - if self.version >= 2: - del request['id'] - else: - request['id'] = None - - return request - - def response(self, result=None): - """ - Prepares a response dictionary - - :param result: The result of method call - :return: A JSON-RPC response dictionary - """ - response = {'result': result, 'id': self.id} - - if self.version >= 2: - response['jsonrpc'] = str(self.version) - else: - response['error'] = None - - return response - - def error(self, code=-32000, message='Server error.', data=None): - """ - Prepares an error dictionary - - :param code: Error code - :param message: Error message - :param data: Extra data to associate to the error - :return: A JSON-RPC error dictionary - """ - error = self.response() - if self.version >= 2: - del error['result'] - else: - error['result'] = None - error['error'] = {'code': code, 'message': message} - if data is not None: - error['error']['data'] = data - return error - -# ------------------------------------------------------------------------------ - - -def dump(params=None, methodname=None, rpcid=None, version=None, - is_response=None, is_notify=None, config=jsonrpclib.config.DEFAULT): - """ - Prepares a JSON-RPC dictionary (request, notification, response or error) - - :param params: Method parameters (if a method name is given) or a Fault - :param methodname: Method name - :param rpcid: Request ID - :param version: JSON-RPC version - :param is_response: If True, this is a response dictionary - :param is_notify: If True, this is a notification request - :param config: A JSONRPClib Config instance - :return: A JSON-RPC dictionary - """ - # Default version - if not version: - version = config.version - - if not is_response and params is None: - params = [] - - # Validate method name and parameters - valid_params = [utils.TupleType, utils.ListType, utils.DictType, Fault] - if is_response: - valid_params.append(type(None)) - - if isinstance(methodname, utils.STRING_TYPES) and \ - not isinstance(params, tuple(valid_params)): - """ - If a method, and params are not in a listish or a Fault, - error out. - """ - raise TypeError("Params must be a dict, list, tuple " - "or Fault instance.") - - # Prepares the JSON-RPC content - payload = Payload(rpcid=rpcid, version=version) - - if isinstance(params, Fault): - # Prepare an error dictionary - # pylint: disable=E1103 - return payload.error(params.faultCode, params.faultString, params.data) - - if not isinstance(methodname, utils.STRING_TYPES) and not is_response: - # Neither a request nor a response - raise ValueError('Method name must be a string, or is_response ' - 'must be set to True.') - - if config.use_jsonclass: - # Use jsonclass to convert the parameters - params = jsonclass.dump(params, config=config) - - if is_response: - # Prepare a response dictionary - if rpcid is None: - # A response must have a request ID - raise ValueError('A method response must have an rpcid.') - return payload.response(params) - - if is_notify: - # Prepare a notification dictionary - return payload.notify(methodname, params) - else: - # Prepare a method call dictionary - return payload.request(methodname, params) - - -def dumps(params=None, methodname=None, methodresponse=None, - encoding=None, rpcid=None, version=None, notify=None, - config=jsonrpclib.config.DEFAULT): - """ - Prepares a JSON-RPC request/response string - - :param params: Method parameters (if a method name is given) or a Fault - :param methodname: Method name - :param methodresponse: If True, this is a response dictionary - :param encoding: Result string encoding - :param rpcid: Request ID - :param version: JSON-RPC version - :param notify: If True, this is a notification request - :param config: A JSONRPClib Config instance - :return: A JSON-RPC dictionary - """ - # Prepare the dictionary - request = dump(params, methodname, rpcid, version, methodresponse, notify, - config) - - # Returns it as a JSON string - return jdumps(request, encoding=encoding or "UTF-8") - - -def load(data, config=jsonrpclib.config.DEFAULT): - """ - Loads a JSON-RPC request/response dictionary. Calls jsonclass to load beans - - :param data: A JSON-RPC dictionary - :param config: A JSONRPClib Config instance (or None for default values) - :return: A parsed dictionary or None - """ - if data is None: - # Notification - return None - - # if the above raises an error, the implementing server code - # should return something like the following: - # { 'jsonrpc':'2.0', 'error': fault.error(), id: None } - if config.use_jsonclass: - # Convert beans - data = jsonclass.load(data, config.classes) - - return data - - -def loads(data, config=jsonrpclib.config.DEFAULT): - """ - Loads a JSON-RPC request/response string. Calls jsonclass to load beans - - :param data: A JSON-RPC string - :param config: A JSONRPClib Config instance (or None for default values) - :return: A parsed dictionary or None - """ - if data == '': - # Notification - return None - - # Parse the JSON dictionary - result = jloads(data) - - # Load the beans - return load(result, config) - -# ------------------------------------------------------------------------------ - - -def check_for_errors(result): - """ - Checks if a result dictionary signals an error - - :param result: A result dictionary - :raise TypeError: Invalid parameter - :raise NotImplementedError: Unknown JSON-RPC version - :raise ValueError: Invalid dictionary content - :raise ProtocolError: An error occurred on the server side - :return: The result parameter - """ - if not result: - # Notification - return result - - if not isinstance(result, utils.DictType): - # Invalid argument - raise TypeError('Response is not a dict.') - - if 'jsonrpc' in result and float(result['jsonrpc']) > 2.0: - # Unknown JSON-RPC version - raise NotImplementedError('JSON-RPC version not yet supported.') - - if 'result' not in result and 'error' not in result: - # Invalid dictionary content - raise ValueError('Response does not have a result or error key.') - - if 'error' in result and result['error']: - # Server-side error - if 'code' in result['error']: - # Code + Message - code = result['error']['code'] - try: - # Get the message (jsonrpclib) - message = result['error']['message'] - except KeyError: - # Get the trace (jabsorb) - message = result['error'].get('trace', '') - - if -32700 <= code <= -32000: - # Pre-defined errors - # See http://www.jsonrpc.org/specification#error_object - raise ProtocolError((code, message)) - else: - # Application error - data = result['error'].get('data', None) - raise AppError((code, message, data)) - - elif isinstance(result['error'], dict) and len(result['error']) == 1: - # Error with a single entry ('reason', ...): use its content - error_key = result['error'].keys()[0] - raise ProtocolError(result['error'][error_key]) - - else: - # Use the raw error content - raise ProtocolError(result['error']) - - return result - - -def isbatch(request): - """ - Tests if the given request is a batch call, i.e. a list of multiple calls - :param request: a JSON-RPC request object - :return: True if the request is a batch call - """ - if not isinstance(request, (utils.ListType, utils.TupleType)): - # Not a list: not a batch call - return False - elif len(request) < 1: - # Only one request: not a batch call - return False - elif not isinstance(request[0], utils.DictType): - # One of the requests is not a dictionary, i.e. a JSON Object - # therefore it is not a valid JSON-RPC request - return False - elif 'jsonrpc' not in request[0].keys(): - # No "jsonrpc" version in the JSON object: not a request - return False - - try: - version = float(request[0]['jsonrpc']) - except ValueError: - # Bad version of JSON-RPC - raise ProtocolError('"jsonrpc" key must be a float(able) value.') - - if version < 2: - # Batch call were not supported before JSON-RPC 2.0 - return False - - return True - - -def isnotification(request): - """ - Tests if the given request is a notification - - :param request: A request dictionary - :return: True if the request is a notification - """ - if 'id' not in request: - # 2.0 notification - return True - - if request['id'] is None: - # 1.0 notification - return True - - return False +#!/usr/bin/python +# -- Content-Encoding: UTF-8 -- +""" +============================ +JSONRPC Library (jsonrpclib) +============================ + +This library is a JSON-RPC v.2 (proposed) implementation which +follows the xmlrpclib API for portability between clients. It +uses the same Server / ServerProxy, loads, dumps, etc. syntax, +while providing features not present in XML-RPC like: + +* Keyword arguments +* Notifications +* Versioning +* Batches and batch notifications + +Eventually, I'll add a SimpleXMLRPCServer compatible library, +and other things to tie the thing off nicely. :) + +For a quick-start, just open a console and type the following, +replacing the server address, method, and parameters +appropriately. +>>> import jsonrpclib +>>> server = jsonrpclib.Server('http://localhost:8181') +>>> server.add(5, 6) +11 +>>> server._notify.add(5, 6) +>>> batch = jsonrpclib.MultiCall(server) +>>> batch.add(3, 50) +>>> batch.add(2, 3) +>>> batch._notify.add(3, 5) +>>> batch() +[53, 5] + +See https://github.com/tcalmant/jsonrpclib for more info. + +:authors: Josh Marshall, Thomas Calmant +:copyright: Copyright 2020, Thomas Calmant +:license: Apache License 2.0 +:version: 0.4.1 + +.. + + Copyright 2020 Thomas Calmant + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +# Standard library +import contextlib +import logging +import os +import socket +import sys +import uuid + +try: + # Python 3 + # pylint: disable=F0401,E0611 + from http.client import HTTPConnection + from urllib.parse import splittype, splithost + from xmlrpc.client import Transport as XMLTransport + from xmlrpc.client import SafeTransport as XMLSafeTransport + from xmlrpc.client import ServerProxy as XMLServerProxy + from xmlrpc.client import _Method as XML_Method +except ImportError: + # Python 2 + # pylint: disable=F0401,E0611 + from httplib import HTTPConnection + from urllib import splittype, splithost + from xmlrpclib import Transport as XMLTransport + from xmlrpclib import SafeTransport as XMLSafeTransport + from xmlrpclib import ServerProxy as XMLServerProxy + from xmlrpclib import _Method as XML_Method + +try: + # Check GZip support + import gzip +except ImportError: + # Python can be built without zlib/gzip support + # pylint: disable=C0103 + gzip = None + +# Library includes +import jsonrpclib.config +import jsonrpclib.jsonclass as jsonclass +import jsonrpclib.utils as utils + +# ------------------------------------------------------------------------------ + +# Module version +__version_info__ = (0, 4, 1) +__version__ = ".".join(str(x) for x in __version_info__) + +# Documentation strings format +__docformat__ = "restructuredtext en" + +# Create the logger +_logger = logging.getLogger(__name__) + +# ------------------------------------------------------------------------------ +# JSON library import + +try: + # pylint: disable=F0401,E0611 + # Using cjson + import cjson + _logger.debug("Using cjson as JSON library") + + # Declare cjson methods + def jdumps(obj, encoding='utf-8'): + """ + Serializes ``obj`` to a JSON formatted string, using cjson. + """ + return cjson.encode(obj) + + def jloads(json_string): + """ + Deserializes ``json_string`` (a string containing a JSON document) + to a Python object, using cjson. + """ + return cjson.decode(json_string) +except ImportError: + # pylint: disable=F0401,E0611 + # Use json or simplejson + try: + import json + _logger.debug("Using json as JSON library") + except ImportError: + try: + import simplejson as json + _logger.debug("Using simplejson as JSON library") + except ImportError: + _logger.error("No supported JSON library found") + raise ImportError('You must have the cjson, json, or simplejson ' + 'module(s) available.') + + # Declare json methods + if sys.version_info[0] < 3: + def jdumps(obj, encoding='utf-8'): + """ + Serializes ``obj`` to a JSON formatted string. + """ + # Python 2 (explicit encoding) + return json.dumps(obj, encoding=encoding) + else: + # Python 3 + def jdumps(obj, encoding='utf-8'): + """ + Serializes ``obj`` to a JSON formatted string. + """ + # Python 3 (the encoding parameter has been removed) + return json.dumps(obj) + + def jloads(json_string): + """ + Deserializes ``json_string`` (a string containing a JSON document) + to a Python object. + """ + return json.loads(json_string) + +# ------------------------------------------------------------------------------ +# XMLRPClib re-implementations + + +class ProtocolError(Exception): + """ + JSON-RPC error + + ProtocolError.args[0] can be: + * an error message (string) + * a (code, message) tuple + """ + pass + + +class AppError(ProtocolError): + """ + Application error: the error code is not in the pre-defined ones + + AppError.args[0][0]: Error code + AppError.args[0][1]: Error message or trace + AppError.args[0][2]: Associated data + """ + def data(self): + """ + Retrieves the value found in the 'data' entry of the error, or None + + :return: The data associated to the error, or None + """ + return self.args[0][2] + + +class JSONParser(object): + """ + Default JSON parser + """ + def __init__(self, target): + """ + Associates the target loader to the parser + + :param target: a JSONTarget instance + """ + self.target = target + + def feed(self, data): + """ + Feeds the associated target with the given data + """ + self.target.feed(data) + + @staticmethod + def close(): + """ + Does nothing + """ + pass + + +class JSONTarget(object): + """ + Unmarshalls stream data to a string + """ + def __init__(self): + """ + Sets up the unmarshaller + """ + self.data = [] + + def feed(self, data): + """ + Stores the given raw data into a buffer + """ + # Store raw data as it might not contain whole wide-character + self.data.append(data) + + def close(self): + """ + Unmarshalls the buffered data + """ + if not self.data: + return '' + else: + # Use type to have a valid join (str vs. bytes) + data = type(self.data[0])().join(self.data) + try: + # Convert the whole final string + data = utils.from_bytes(data) + except: + # Try a pass-through + pass + + return data + + +class TransportMixIn(object): + """ Just extends the XML-RPC transport where necessary. """ + # for Python 2.7 support + _connection = None + + # List of non-overridable headers + # Use the configuration to change the content-type + readonly_headers = ('content-length', 'content-type') + + def __init__(self, config=jsonrpclib.config.DEFAULT, context=None): + """ + Sets up the transport + + :param config: A JSONRPClib Config instance + """ + # Store the configuration + self._config = config + + # Store the SSL context + self.context = context + + # Set up the user agent + self.user_agent = config.user_agent + + # Additional headers: list of dictionaries + self.additional_headers = [] + + # Avoid a pep-8 error + self.accept_gzip_encoding = True + self.verbose = False + + def push_headers(self, headers): + """ + Adds a dictionary of headers to the additional headers list + + :param headers: A dictionary + """ + self.additional_headers.append(headers) + + def pop_headers(self, headers): + """ + Removes the given dictionary from the additional headers list. + Also validates that given headers are on top of the stack + + :param headers: Headers to remove + :raise AssertionError: The given dictionary is not on the latest stored + in the additional headers list + """ + assert self.additional_headers[-1] == headers + self.additional_headers.pop() + + def emit_additional_headers(self, connection): + """ + Puts headers as is in the request, filtered read only headers + + :param connection: The request connection + :return: The dictionary of headers added to the connection + """ + additional_headers = {} + + # Setup extra headers + # (list of tuples, inherited from xmlrpclib.client.Transport) + # Authentication headers are stored there + try: + extra_headers = self._extra_headers or [] + except AttributeError: + # Not available this version of Python (should not happen) + pass + else: + for (key, value) in extra_headers: + additional_headers[key] = value + + # Prepare the merged dictionary + for headers in self.additional_headers: + additional_headers.update(headers) + + # Normalize keys and values + additional_headers = dict( + (str(key).lower(), str(value)) + for key, value in additional_headers.items()) + + # Remove forbidden keys + for forbidden in self.readonly_headers: + additional_headers.pop(forbidden, None) + + # Reversed order: in the case of multiple headers value definition, + # the latest pushed has priority + for key, value in additional_headers.items(): + connection.putheader(key, value) + + return additional_headers + + def single_request(self, host, handler, request_body, verbose=0): + """ + Send a complete request, and parse the response. + + From xmlrpclib in Python 2.7 + + :param host: Target host. + :param handler: Target RPC handler. + :param request_body: JSON-RPC request body. + :param verbose: Debugging flag. + :return: Parsed response. + """ + connection = self.make_connection(host) + try: + self.send_request(connection, handler, request_body, verbose) + self.send_content(connection, request_body) + + response = connection.getresponse() + if response.status == 200: + self.verbose = verbose + return self.parse_response(response) + except: + # All unexpected errors leave connection in + # a strange state, so we clear it. + self.close() + raise + + # Discard any response data and raise exception + if response.getheader("content-length", 0): + response.read() + raise ProtocolError(host + handler, + response.status, response.reason, + response.msg) + + def send_request(self, connection, handler, request_body, debug=0): + """ + Send HTTP request. + + From xmlrpc.client in Python 3.4 + + :param connection: Connection handle. + :param handler: Target RPC handler (a path relative to host) + :param request_body: The JSON-RPC request body + :param debug: Enable debugging if debug is true. + :return: An HTTPConnection. + """ + if debug: + connection.set_debuglevel(1) + if self.accept_gzip_encoding and gzip: + connection.putrequest("POST", handler, skip_accept_encoding=True) + connection.putheader("Accept-Encoding", "gzip") + else: + connection.putrequest("POST", handler) + + return connection + + def send_content(self, connection, request_body): + """ + Completes the request headers and sends the request body of a JSON-RPC + request over a HTTPConnection + + :param connection: An HTTPConnection object + :param request_body: JSON-RPC request body + """ + # Convert the body first + request_body = utils.to_bytes(request_body) + + # "static" headers + connection.putheader("Content-Type", self._config.content_type) + connection.putheader("Content-Length", str(len(request_body))) + + # Emit additional headers here in order not to override content-length + additional_headers = self.emit_additional_headers(connection) + + # Add the user agent, if not overridden + if "user-agent" not in additional_headers: + connection.putheader("User-Agent", self.user_agent) + + connection.endheaders() + if request_body: + connection.send(request_body) + + @staticmethod + def getparser(): + """ + Create an instance of the parser, and attach it to an unmarshalling + object. Return both objects. + + :return: The parser and unmarshaller instances + """ + target = JSONTarget() + return JSONParser(target), target + + +class Transport(TransportMixIn, XMLTransport): + """ + Mixed-in HTTP transport + """ + def __init__(self, config): + TransportMixIn.__init__(self, config) + XMLTransport.__init__(self) + + +class SafeTransport(TransportMixIn, XMLSafeTransport): + """ + Mixed-in HTTPS transport + """ + def __init__(self, config, context): + TransportMixIn.__init__(self, config, context) + try: + # Give the context to XMLSafeTransport, to avoid it setting the + # context to None. + # See https://github.com/tcalmant/jsonrpclib/issues/39 + XMLSafeTransport.__init__(self, context=context) + except TypeError: + # On old versions of Python (Pre-2014), the context argument + # wasn't available + XMLSafeTransport.__init__(self) + +# ------------------------------------------------------------------------------ + + +class UnixHTTPConnection(HTTPConnection): + """ + Replaces the connect() method of HTTPConnection to use a Unix socket + """ + def __init__(self, path, *args, **kwargs): + """ + Constructs the HTTP connection. + + Forwards all given arguments except ``path`` to the constructor of + HTTPConnection + + :param path: Path to the Unix socket + """ + HTTPConnection.__init__(self, path, *args, **kwargs) + self.path = path + + def connect(self): + """ + Connects to the described server + """ + self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.sock.connect(self.path) + + +class UnixTransport(TransportMixIn, XMLTransport): + """ + Mixed-in HTTP transport over a UNIX socket + """ + def __init__(self, config, path=None): + """ + :param config: The jsonrpclib configuration + :param path: Path to the Unix socket (overrides the host name later) + """ + TransportMixIn.__init__(self, config) + XMLTransport.__init__(self) + # Keep track of the given path, if any + self.__unix_path = os.path.abspath(path) if path else None + + def make_connection(self, host): + """ + Connect to server. + + Return an existing connection if possible. + This allows HTTP/1.1 keep-alive. + + Code copied from xmlrpc.client (Python 3) + + :param host: Target host (ignored if a path was given) + :return A UnixHTTPConnection object + """ + if self.__unix_path: + host = self.__unix_path + + if self._connection and host == self._connection[0]: + return self._connection[1] + + # create a HTTP connection object from a host descriptor + path, self._extra_headers, _ = self.get_host_info(host) + self._connection = host, UnixHTTPConnection(path) + return self._connection[1] + +# ------------------------------------------------------------------------------ + + +class ServerProxy(XMLServerProxy): + """ + Unfortunately, much more of this class has to be copied since + so much of it does the serialization. + """ + def __init__(self, uri, transport=None, encoding=None, + verbose=0, version=None, headers=None, history=None, + config=jsonrpclib.config.DEFAULT, context=None): + """ + Sets up the server proxy + + :param uri: Request URI + :param transport: Custom transport handler + :param encoding: Specified encoding + :param verbose: Log verbosity level + :param version: JSON-RPC specification version + :param headers: Custom additional headers for each request + :param history: History object (for tests) + :param config: A JSONRPClib Config instance + :param context: The optional SSLContext to use + """ + # Store the configuration + self._config = config + self.__version = version or config.version + + schema, uri = splittype(uri) + use_unix = False + if schema.startswith("unix+"): + schema = schema[len("unix+"):] + use_unix = True + + if schema not in ('http', 'https'): + _logger.error("jsonrpclib only support http(s) URIs, not %s", + schema) + raise IOError('Unsupported JSON-RPC protocol.') + + self.__host, self.__handler = splithost(uri) + if use_unix: + unix_path = self.__handler + self.__handler = '/' + elif not self.__handler: + # Not sure if this is in the JSON spec? + self.__handler = '/' + + if transport is None: + if use_unix: + if schema == "http": + # In Unix mode, we use the path part of the URL (handler) + # as the path to the socket file + transport = UnixTransport( + config=config, path=unix_path + ) + elif schema == 'https': + transport = SafeTransport(config=config, context=context) + else: + transport = Transport(config=config) + + if transport is None: + raise IOError( + "Unhandled combination: UNIX={}, protocol={}" + .format(use_unix, schema) + ) + + self.__transport = transport + + self.__encoding = encoding + self.__verbose = verbose + self.__history = history + + # Global custom headers are injected into Transport + self.__transport.push_headers(headers or {}) + + def _request(self, methodname, params, rpcid=None): + """ + Calls a method on the remote server + + :param methodname: Name of the method to call + :param params: Method parameters + :param rpcid: ID of the remote call + :return: The parsed result of the call + """ + request = dumps(params, methodname, encoding=self.__encoding, + rpcid=rpcid, version=self.__version, + config=self._config) + response = self._run_request(request) + check_for_errors(response) + return response['result'] + + def _request_notify(self, methodname, params, rpcid=None): + """ + Calls a method as a notification + + :param methodname: Name of the method to call + :param params: Method parameters + :param rpcid: ID of the remote call + """ + request = dumps(params, methodname, encoding=self.__encoding, + rpcid=rpcid, version=self.__version, notify=True, + config=self._config) + response = self._run_request(request, notify=True) + check_for_errors(response) + + def _run_request(self, request, notify=False): + """ + Sends the given request to the remote server + + :param request: The request to send + :param notify: Notification request flag (unused) + :return: The response as a parsed JSON object + """ + if self.__history is not None: + self.__history.add_request(request) + + response = self.__transport.request( + self.__host, + self.__handler, + request, + verbose=self.__verbose + ) + + # Here, the XMLRPC library translates a single list + # response to the single value -- should we do the + # same, and require a tuple / list to be passed to + # the response object, or expect the Server to be + # outputting the response appropriately? + + if self.__history is not None: + self.__history.add_response(response) + + if not response: + return None + else: + return_obj = loads(response, self._config) + return return_obj + + def __getattr__(self, name): + """ + Returns a callable object to call the remote service + """ + if name.startswith("__") and name.endswith("__"): + # Don't proxy special methods. + raise AttributeError("ServerProxy has no attribute '%s'" % name) + # Same as original, just with new _Method reference + return _Method(self._request, name) + + def __close(self): + """ + Closes the transport layer + """ + self.__transport.close() + + def __call__(self, attr): + """ + A workaround to get special attributes on the ServerProxy + without interfering with the magic __getattr__ + + (code from xmlrpclib in Python 2.7) + """ + if attr == "close": + return self.__close + elif attr == "transport": + return self.__transport + + raise AttributeError("Attribute {0} not found".format(attr)) + + @property + def _notify(self): + """ + Like __getattr__, but sending a notification request instead of a call + """ + return _Notify(self._request_notify) + + @contextlib.contextmanager + def _additional_headers(self, headers): + """ + Allows to specify additional headers, to be added inside the with + block. + Example of usage: + + >>> with client._additional_headers({'X-Test' : 'Test'}) as new_client: + ... new_client.method() + ... + >>> # Here old headers are restored + """ + self.__transport.push_headers(headers) + yield self + self.__transport.pop_headers(headers) + +# ------------------------------------------------------------------------------ + + +class _Method(XML_Method): + """ + Some magic to bind an JSON-RPC method to an RPC server. + """ + def __call__(self, *args, **kwargs): + """ + Sends an RPC request and returns the unmarshalled result + """ + if args and kwargs: + raise ProtocolError("Cannot use both positional and keyword " + "arguments (according to JSON-RPC spec.)") + if args: + return self.__send(self.__name, args) + else: + return self.__send(self.__name, kwargs) + + def __getattr__(self, name): + """ + Returns a Method object for nested calls + """ + if name == "__name__": + return self.__name + return _Method(self.__send, "{0}.{1}".format(self.__name, name)) + + def __repr__(self): + """ + Returns a string representation of the method + """ + # Must use __class__ here because the base class is old-style. + return "<{0} {1}>".format(self.__class__, self.__name) + + +class _Notify(object): + """ + Same as _Method, but to send notifications + """ + def __init__(self, request): + """ + Sets the method to call to send a request to the server + """ + self._request = request + + def __getattr__(self, name): + """ + Returns a Method object, to be called as a notification + """ + return _Method(self._request, name) + +# ------------------------------------------------------------------------------ +# Batch implementation + + +class MultiCallMethod(object): + """ + Stores calls made to a MultiCall object for batch execution + """ + def __init__(self, method, notify=False, config=jsonrpclib.config.DEFAULT): + """ + Sets up the store + + :param method: Name of the method to call + :param notify: Notification flag + :param config: Request configuration + """ + self.method = method + self.params = [] + self.notify = notify + self._config = config + + def __call__(self, *args, **kwargs): + """ + Normalizes call parameters + """ + if kwargs and args: + raise ProtocolError('JSON-RPC does not support both ' + + 'positional and keyword arguments.') + if kwargs: + self.params = kwargs + else: + self.params = args + + def request(self, encoding=None, rpcid=None): + """ + Returns the request object as JSON-formatted string + """ + return dumps(self.params, self.method, version=2.0, + encoding=encoding, rpcid=rpcid, notify=self.notify, + config=self._config) + + def __repr__(self): + """ + String representation + """ + return str(self.request()) + + def __getattr__(self, method): + """ + Updates the object for a nested call + """ + self.method = "{0}.{1}".format(self.method, method) + return self + + +class MultiCallNotify(object): + """ + Same as MultiCallMethod but for notifications + """ + def __init__(self, multicall, config=jsonrpclib.config.DEFAULT): + """ + Sets ip the store + + :param multicall: The parent MultiCall instance + :param config: Request configuration + """ + self.multicall = multicall + self._config = config + + def __getattr__(self, name): + """ + Returns the MultiCallMethod to use as a notification + """ + new_job = MultiCallMethod(name, notify=True, config=self._config) + self.multicall._job_list.append(new_job) + return new_job + + +class MultiCallIterator(object): + """ + Iterates over the results of a MultiCall. + Exceptions are raised in response to JSON-RPC faults + """ + def __init__(self, results): + """ + Sets up the results store + """ + self.results = results + + @staticmethod + def __get_result(item): + """ + Checks for error and returns the "real" result stored in a MultiCall + result. + """ + check_for_errors(item) + return item['result'] + + def __iter__(self): + """ + Iterates over all results + """ + for item in self.results: + yield self.__get_result(item) + + # Since Python 3.7, we must return instead of raising a StopIteration + # (see PEP-479) + return + + def __getitem__(self, i): + """ + Returns the i-th object of the results + """ + return self.__get_result(self.results[i]) + + def __len__(self): + """ + Returns the number of results stored + """ + return len(self.results) + + +class MultiCall(object): + """ + server -> a object used to boxcar method calls, where server should be a + ServerProxy object. + + Methods can be added to the MultiCall using normal + method call syntax e.g.: + + multicall = MultiCall(server_proxy) + multicall.add(2,3) + multicall.get_address("Guido") + + To execute the multicall, call the MultiCall object e.g.: + + add_result, address = multicall() + """ + def __init__(self, server, config=jsonrpclib.config.DEFAULT): + """ + Sets up the multicall + + :param server: A ServerProxy object + :param config: Request configuration + """ + self._server = server + self._job_list = [] + self._config = config + + def _request(self): + """ + Sends the request to the server and returns the responses + + :return: A MultiCallIterator object + """ + if len(self._job_list) < 1: + # Should we alert? This /is/ pretty obvious. + return + request_body = "[ {0} ]".format( + ','.join(job.request() for job in self._job_list)) + responses = self._server._run_request(request_body) + del self._job_list[:] + if not responses: + responses = [] + return MultiCallIterator(responses) + + @property + def _notify(self): + """ + Prepares a notification call + """ + return MultiCallNotify(self, self._config) + + def __getattr__(self, name): + """ + Registers a method call + """ + new_job = MultiCallMethod(name, config=self._config) + self._job_list.append(new_job) + return new_job + + __call__ = _request + +# These lines conform to xmlrpclib's "compatibility" line. +# Not really sure if we should include these, but oh well. +Server = ServerProxy + +# ------------------------------------------------------------------------------ + + +class Fault(object): + """ + JSON-RPC error class + """ + def __init__(self, code=-32000, message='Server error', rpcid=None, + config=jsonrpclib.config.DEFAULT, data=None): + """ + Sets up the error description + + :param code: Fault code + :param message: Associated message + :param rpcid: Request ID + :param config: A JSONRPClib Config instance + :param data: Extra information added to an error description + """ + self.faultCode = code + self.faultString = message + self.rpcid = rpcid + self.config = config + self.data = data + + def error(self): + """ + Returns the error as a dictionary + + :returns: A {'code', 'message'} dictionary + """ + return {'code': self.faultCode, 'message': self.faultString, + 'data': self.data} + + def response(self, rpcid=None, version=None): + """ + Returns the error as a JSON-RPC response string + + :param rpcid: Forced request ID + :param version: JSON-RPC version + :return: A JSON-RPC response string + """ + if not version: + version = self.config.version + + if rpcid: + self.rpcid = rpcid + + return dumps(self, methodresponse=True, rpcid=self.rpcid, + version=version, config=self.config) + + def dump(self, rpcid=None, version=None): + """ + Returns the error as a JSON-RPC response dictionary + + :param rpcid: Forced request ID + :param version: JSON-RPC version + :return: A JSON-RPC response dictionary + """ + if not version: + version = self.config.version + + if rpcid: + self.rpcid = rpcid + + return dump(self, is_response=True, rpcid=self.rpcid, + version=version, config=self.config) + + def __repr__(self): + """ + String representation + """ + return ''.format(self.faultCode, self.faultString) + + +class Payload(object): + """ + JSON-RPC content handler + """ + def __init__(self, rpcid=None, version=None, + config=jsonrpclib.config.DEFAULT): + """ + Sets up the JSON-RPC handler + + :param rpcid: Request ID + :param version: JSON-RPC version + :param config: A JSONRPClib Config instance + """ + if not version: + version = config.version + + self.id = rpcid + self.version = float(version) + + def request(self, method, params=None): + """ + Prepares a method call request + + :param method: Method name + :param params: Method parameters + :return: A JSON-RPC request dictionary + """ + if not isinstance(method, utils.STRING_TYPES): + raise ValueError('Method name must be a string.') + + if not self.id: + # Generate a request ID + self.id = str(uuid.uuid4()) + + request = {'id': self.id, 'method': method} + if params or self.version < 1.1: + request['params'] = params or [] + + if self.version >= 2: + request['jsonrpc'] = str(self.version) + + return request + + def notify(self, method, params=None): + """ + Prepares a notification request + + :param method: Notification name + :param params: Notification parameters + :return: A JSON-RPC notification dictionary + """ + # Prepare the request dictionary + request = self.request(method, params) + + # Remove the request ID, as it's a notification + if self.version >= 2: + del request['id'] + else: + request['id'] = None + + return request + + def response(self, result=None): + """ + Prepares a response dictionary + + :param result: The result of method call + :return: A JSON-RPC response dictionary + """ + response = {'result': result, 'id': self.id} + + if self.version >= 2: + response['jsonrpc'] = str(self.version) + else: + response['error'] = None + + return response + + def error(self, code=-32000, message='Server error.', data=None): + """ + Prepares an error dictionary + + :param code: Error code + :param message: Error message + :param data: Extra data to associate to the error + :return: A JSON-RPC error dictionary + """ + error = self.response() + if self.version >= 2: + del error['result'] + else: + error['result'] = None + error['error'] = {'code': code, 'message': message} + if data is not None: + error['error']['data'] = data + return error + +# ------------------------------------------------------------------------------ + + +def dump(params=None, methodname=None, rpcid=None, version=None, + is_response=None, is_notify=None, config=jsonrpclib.config.DEFAULT): + """ + Prepares a JSON-RPC dictionary (request, notification, response or error) + + :param params: Method parameters (if a method name is given) or a Fault + :param methodname: Method name + :param rpcid: Request ID + :param version: JSON-RPC version + :param is_response: If True, this is a response dictionary + :param is_notify: If True, this is a notification request + :param config: A JSONRPClib Config instance + :return: A JSON-RPC dictionary + """ + # Default version + if not version: + version = config.version + + if not is_response and params is None: + params = [] + + # Validate method name and parameters + valid_params = [utils.TupleType, utils.ListType, utils.DictType, Fault] + if is_response: + valid_params.append(type(None)) + + if isinstance(methodname, utils.STRING_TYPES) and \ + not isinstance(params, tuple(valid_params)): + """ + If a method, and params are not in a listish or a Fault, + error out. + """ + raise TypeError("Params must be a dict, list, tuple " + "or Fault instance.") + + # Prepares the JSON-RPC content + payload = Payload(rpcid=rpcid, version=version) + + if isinstance(params, Fault): + # Prepare an error dictionary + # pylint: disable=E1103 + return payload.error(params.faultCode, params.faultString, params.data) + + if not isinstance(methodname, utils.STRING_TYPES) and not is_response: + # Neither a request nor a response + raise ValueError('Method name must be a string, or is_response ' + 'must be set to True.') + + if config.use_jsonclass: + # Use jsonclass to convert the parameters + params = jsonclass.dump(params, config=config) + + if is_response: + # Prepare a response dictionary + if rpcid is None: + # A response must have a request ID + raise ValueError('A method response must have an rpcid.') + return payload.response(params) + + if is_notify: + # Prepare a notification dictionary + return payload.notify(methodname, params) + else: + # Prepare a method call dictionary + return payload.request(methodname, params) + + +def dumps(params=None, methodname=None, methodresponse=None, + encoding=None, rpcid=None, version=None, notify=None, + config=jsonrpclib.config.DEFAULT): + """ + Prepares a JSON-RPC request/response string + + :param params: Method parameters (if a method name is given) or a Fault + :param methodname: Method name + :param methodresponse: If True, this is a response dictionary + :param encoding: Result string encoding + :param rpcid: Request ID + :param version: JSON-RPC version + :param notify: If True, this is a notification request + :param config: A JSONRPClib Config instance + :return: A JSON-RPC dictionary + """ + # Prepare the dictionary + request = dump(params, methodname, rpcid, version, methodresponse, notify, + config) + + # Returns it as a JSON string + return jdumps(request, encoding=encoding or "UTF-8") + + +def load(data, config=jsonrpclib.config.DEFAULT): + """ + Loads a JSON-RPC request/response dictionary. Calls jsonclass to load beans + + :param data: A JSON-RPC dictionary + :param config: A JSONRPClib Config instance (or None for default values) + :return: A parsed dictionary or None + """ + if data is None: + # Notification + return None + + # if the above raises an error, the implementing server code + # should return something like the following: + # { 'jsonrpc':'2.0', 'error': fault.error(), id: None } + if config.use_jsonclass: + # Convert beans + data = jsonclass.load(data, config.classes) + + return data + + +def loads(data, config=jsonrpclib.config.DEFAULT): + """ + Loads a JSON-RPC request/response string. Calls jsonclass to load beans + + :param data: A JSON-RPC string + :param config: A JSONRPClib Config instance (or None for default values) + :return: A parsed dictionary or None + """ + if data == '': + # Notification + return None + + # Parse the JSON dictionary + result = jloads(data) + + # Load the beans + return load(result, config) + +# ------------------------------------------------------------------------------ + + +def check_for_errors(result): + """ + Checks if a result dictionary signals an error + + :param result: A result dictionary + :raise TypeError: Invalid parameter + :raise NotImplementedError: Unknown JSON-RPC version + :raise ValueError: Invalid dictionary content + :raise ProtocolError: An error occurred on the server side + :return: The result parameter + """ + if not result: + # Notification + return result + + if not isinstance(result, utils.DictType): + # Invalid argument + raise TypeError('Response is not a dict.') + + if 'jsonrpc' in result and float(result['jsonrpc']) > 2.0: + # Unknown JSON-RPC version + raise NotImplementedError('JSON-RPC version not yet supported.') + + if 'result' not in result and 'error' not in result: + # Invalid dictionary content + raise ValueError('Response does not have a result or error key.') + + if 'error' in result and result['error']: + # Server-side error + if 'code' in result['error']: + # Code + Message + code = result['error']['code'] + try: + # Get the message (jsonrpclib) + message = result['error']['message'] + except KeyError: + # Get the trace (jabsorb) + message = result['error'].get('trace', '') + + if -32700 <= code <= -32000: + # Pre-defined errors + # See http://www.jsonrpc.org/specification#error_object + raise ProtocolError((code, message)) + else: + # Application error + data = result['error'].get('data', None) + raise AppError((code, message, data)) + + elif isinstance(result['error'], dict) and len(result['error']) == 1: + # Error with a single entry ('reason', ...): use its content + error_key = result['error'].keys()[0] + raise ProtocolError(result['error'][error_key]) + + else: + # Use the raw error content + raise ProtocolError(result['error']) + + return result + + +def isbatch(request): + """ + Tests if the given request is a batch call, i.e. a list of multiple calls + :param request: a JSON-RPC request object + :return: True if the request is a batch call + """ + if not isinstance(request, (utils.ListType, utils.TupleType)): + # Not a list: not a batch call + return False + elif len(request) < 1: + # Only one request: not a batch call + return False + elif not isinstance(request[0], utils.DictType): + # One of the requests is not a dictionary, i.e. a JSON Object + # therefore it is not a valid JSON-RPC request + return False + elif 'jsonrpc' not in request[0].keys(): + # No "jsonrpc" version in the JSON object: not a request + return False + + try: + version = float(request[0]['jsonrpc']) + except ValueError: + # Bad version of JSON-RPC + raise ProtocolError('"jsonrpc" key must be a float(able) value.') + + if version < 2: + # Batch call were not supported before JSON-RPC 2.0 + return False + + return True + + +def isnotification(request): + """ + Tests if the given request is a notification + + :param request: A request dictionary + :return: True if the request is a notification + """ + if 'id' not in request: + # 2.0 notification + return True + + if request['id'] is None: + # 1.0 notification + return True + + return False diff -Nru jsonrpclib-pelix-0.3.1/jsonrpclib/SimpleJSONRPCServer.py jsonrpclib-pelix-0.4.1/jsonrpclib/SimpleJSONRPCServer.py --- jsonrpclib-pelix-0.3.1/jsonrpclib/SimpleJSONRPCServer.py 2017-06-27 08:56:32.000000000 +0000 +++ jsonrpclib-pelix-0.4.1/jsonrpclib/SimpleJSONRPCServer.py 2020-04-12 14:34:55.000000000 +0000 @@ -1,613 +1,651 @@ -#!/usr/bin/python -# -- Content-Encoding: UTF-8 -- -""" -Defines a request dispatcher, a HTTP request handler, a HTTP server and a -CGI request handler. - -:authors: Josh Marshall, Thomas Calmant -:copyright: Copyright 2017, Thomas Calmant -:license: Apache License 2.0 -:version: 0.3.1 - -.. - - Copyright 2017 Thomas Calmant - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -""" - -# Standard library -import logging -import socket -import sys -import traceback - -try: - # Python 3 - # pylint: disable=F0401,E0611 - import xmlrpc.server as xmlrpcserver - # Make sure the module is complete. - # The "future" package under python2.7 provides an incomplete - # variant of this package. - SimpleXMLRPCDispatcher = xmlrpcserver.SimpleXMLRPCDispatcher - SimpleXMLRPCRequestHandler = xmlrpcserver.SimpleXMLRPCRequestHandler - resolve_dotted_attribute = xmlrpcserver.resolve_dotted_attribute - import socketserver -except (ImportError, AttributeError): - # Python 2 or IronPython - # pylint: disable=F0401,E0611 - import SimpleXMLRPCServer as xmlrpcserver - SimpleXMLRPCDispatcher = xmlrpcserver.SimpleXMLRPCDispatcher - SimpleXMLRPCRequestHandler = xmlrpcserver.SimpleXMLRPCRequestHandler - resolve_dotted_attribute = xmlrpcserver.resolve_dotted_attribute - import SocketServer as socketserver - -try: - # Windows - import fcntl -except ImportError: - # Other systems - # pylint: disable=C0103 - fcntl = None - -# Local modules -from jsonrpclib import Fault -import jsonrpclib.config -import jsonrpclib.utils as utils -import jsonrpclib.threadpool - -# ------------------------------------------------------------------------------ - -# Module version -__version_info__ = (0, 3, 1) -__version__ = ".".join(str(x) for x in __version_info__) - -# Documentation strings format -__docformat__ = "restructuredtext en" - -# Prepare the logger -_logger = logging.getLogger(__name__) - -# ------------------------------------------------------------------------------ - - -def get_version(request): - """ - Computes the JSON-RPC version - - :param request: A request dictionary - :return: The JSON-RPC version or None - """ - if 'jsonrpc' in request: - return 2.0 - elif 'id' in request: - return 1.0 - - return None - - -def validate_request(request, json_config): - """ - Validates the format of a request dictionary - - :param request: A request dictionary - :param json_config: A JSONRPClib Config instance - :return: True if the dictionary is valid, else a Fault object - """ - if not isinstance(request, utils.DictType): - # Invalid request type - fault = Fault(-32600, 'Request must be a dict, not {0}' - .format(type(request).__name__), - config=json_config) - _logger.warning("Invalid request content: %s", fault) - return fault - - # Get the request ID - rpcid = request.get('id', None) - - # Check request version - version = get_version(request) - if not version: - fault = Fault(-32600, 'Request {0} invalid.'.format(request), - rpcid=rpcid, config=json_config) - _logger.warning("No version in request: %s", fault) - return fault - - # Default parameters: empty list - request.setdefault('params', []) - - # Check parameters - method = request.get('method', None) - params = request.get('params') - param_types = (utils.ListType, utils.DictType, utils.TupleType) - - if not method or not isinstance(method, utils.STRING_TYPES) or \ - not isinstance(params, param_types): - # Invalid type of method name or parameters - fault = Fault(-32600, 'Invalid request parameters or method.', - rpcid=rpcid, config=json_config) - _logger.warning("Invalid request content: %s", fault) - return fault - - # Valid request - return True - -# ------------------------------------------------------------------------------ - - -class NoMulticallResult(Exception): - """ - No result in multicall - """ - pass - - -class SimpleJSONRPCDispatcher(SimpleXMLRPCDispatcher, object): - """ - Mix-in class that dispatches JSON-RPC requests. - - This class is used to register JSON-RPC method handlers - and then to dispatch them. This class doesn't need to be - instanced directly when used by SimpleJSONRPCServer. - """ - def __init__(self, encoding=None, config=jsonrpclib.config.DEFAULT): - """ - Sets up the dispatcher with the given encoding. - None values are allowed. - """ - SimpleXMLRPCDispatcher.__init__( - self, allow_none=True, encoding=encoding or "UTF-8") - self.json_config = config - - # Notification thread pool - self.__notification_pool = None - - def set_notification_pool(self, thread_pool): - """ - Sets the thread pool to use to handle notifications - """ - self.__notification_pool = thread_pool - - def _unmarshaled_dispatch(self, request, dispatch_method=None): - """ - Loads the request dictionary (unmarshaled), calls the method(s) - accordingly and returns a JSON-RPC dictionary (not marshaled) - - :param request: JSON-RPC request dictionary (or list of) - :param dispatch_method: Custom dispatch method (for method resolution) - :return: A JSON-RPC dictionary (or an array of) or None if the request - was a notification - :raise NoMulticallResult: No result in batch - """ - if not request: - # Invalid request dictionary - fault = Fault(-32600, 'Request invalid -- no request data.', - config=self.json_config) - _logger.warning("Invalid request: %s", fault) - return fault.dump() - - if isinstance(request, utils.ListType): - # This SHOULD be a batch, by spec - responses = [] - for req_entry in request: - # Validate the request - result = validate_request(req_entry, self.json_config) - if isinstance(result, Fault): - responses.append(result.dump()) - continue - - # Call the method - resp_entry = self._marshaled_single_dispatch( - req_entry, dispatch_method) - - # Store its result - if isinstance(resp_entry, Fault): - # pylint: disable=E1103 - responses.append(resp_entry.dump()) - elif resp_entry is not None: - responses.append(resp_entry) - - if not responses: - # No non-None result - _logger.error("No result in Multicall") - raise NoMulticallResult("No result") - - return responses - - else: - # Single call - result = validate_request(request, self.json_config) - if isinstance(result, Fault): - return result.dump() - - # Call the method - response = self._marshaled_single_dispatch( - request, dispatch_method) - if isinstance(response, Fault): - # pylint: disable=E1103 - return response.dump() - - return response - - def _marshaled_dispatch(self, data, dispatch_method=None, path=None): - """ - Parses the request data (marshaled), calls method(s) and returns a - JSON string (marshaled) - - :param data: A JSON request string - :param dispatch_method: Custom dispatch method (for method resolution) - :param path: Unused parameter, to keep compatibility with xmlrpclib - :return: A JSON-RPC response string (marshaled) - """ - # Parse the request - try: - request = jsonrpclib.loads(data, self.json_config) - except Exception as ex: - # Parsing/loading error - fault = Fault(-32700, 'Request {0} invalid. ({1}:{2})' - .format(data, type(ex).__name__, ex), - config=self.json_config) - _logger.warning("Error parsing request: %s", fault) - return fault.response() - - # Get the response dictionary - try: - response = self._unmarshaled_dispatch(request, dispatch_method) - if response is not None: - # Compute the string representation of the dictionary/list - return jsonrpclib.jdumps(response, self.encoding) - else: - # No result (notification) - return '' - except NoMulticallResult: - # Return an empty string (jsonrpclib internal behaviour) - return '' - - def _marshaled_single_dispatch(self, request, dispatch_method=None): - """ - Dispatches a single method call - - :param request: A validated request dictionary - :param dispatch_method: Custom dispatch method (for method resolution) - :return: A JSON-RPC response dictionary, or None if it was a - notification request - """ - method = request.get('method') - params = request.get('params') - - # Prepare a request-specific configuration - if 'jsonrpc' not in request and self.json_config.version >= 2: - # JSON-RPC 1.0 request on a JSON-RPC 2.0 - # => compatibility needed - config = self.json_config.copy() - config.version = 1.0 - else: - # Keep server configuration as is - config = self.json_config - - # Test if this is a notification request - is_notification = 'id' not in request or request['id'] in (None, '') - if is_notification and self.__notification_pool is not None: - # Use the thread pool for notifications - if dispatch_method is not None: - self.__notification_pool.enqueue( - dispatch_method, method, params) - else: - self.__notification_pool.enqueue( - self._dispatch, method, params, config) - - # Return immediately - return None - else: - # Synchronous call - try: - # Call the method - if dispatch_method is not None: - response = dispatch_method(method, params) - else: - response = self._dispatch(method, params, config) - except Exception as ex: - # Return a fault - fault = Fault(-32603, '{0}:{1}'.format(type(ex).__name__, ex), - config=config) - _logger.error("Error calling method %s: %s", method, fault) - return fault.dump() - - if is_notification: - # It's a notification, no result needed - # Do not use 'not id' as it might be the integer 0 - return None - - # Prepare a JSON-RPC dictionary - try: - return jsonrpclib.dump(response, rpcid=request['id'], - is_response=True, config=config) - except Exception as ex: - # JSON conversion exception - fault = Fault(-32603, '{0}:{1}'.format(type(ex).__name__, ex), - config=config) - _logger.error("Error preparing JSON-RPC result: %s", fault) - return fault.dump() - - def _dispatch(self, method, params, config=None): - """ - Default method resolver and caller - - :param method: Name of the method to call - :param params: List of arguments to give to the method - :param config: Request-specific configuration - :return: The result of the method - """ - config = config or self.json_config - - func = None - try: - # Look into registered methods - func = self.funcs[method] - except KeyError: - if self.instance is not None: - # Try with the registered instance - try: - # Instance has a custom dispatcher - return getattr(self.instance, '_dispatch')(method, params) - except AttributeError: - # Resolve the method name in the instance - try: - func = resolve_dotted_attribute( - self.instance, method, True) - except AttributeError: - # Unknown method - pass - - if func is not None: - try: - # Call the method - if isinstance(params, utils.ListType): - return func(*params) - else: - return func(**params) - except TypeError as ex: - # Maybe the parameters are wrong - fault = Fault(-32602, 'Invalid parameters: {0}'.format(ex), - config=config) - _logger.warning("Invalid call parameters: %s", fault) - return fault - except: - # Method exception - err_lines = traceback.format_exception(*sys.exc_info()) - trace_string = '{0} | {1}'.format(err_lines[-2].splitlines()[0].strip(), err_lines[-1]) - fault = Fault(-32603, 'Server error: {0}'.format(trace_string), - config=config) - _logger.exception("Server-side exception: %s", fault) - return fault - else: - # Unknown method - fault = Fault(-32601, 'Method {0} not supported.'.format(method), - config=config) - _logger.warning("Unknown method: %s", fault) - return fault - -# ------------------------------------------------------------------------------ - - -class SimpleJSONRPCRequestHandler(SimpleXMLRPCRequestHandler): - """ - HTTP request handler. - - The server that receives the requests must have a json_config member, - containing a JSONRPClib Config instance - """ - def do_POST(self): - """ - Handles POST requests - """ - if not self.is_rpc_path_valid(): - self.report_404() - return - - # Retrieve the configuration - config = getattr(self.server, 'json_config', jsonrpclib.config.DEFAULT) - - try: - # Read the request body - max_chunk_size = 10 * 1024 * 1024 - size_remaining = int(self.headers["content-length"]) - chunks = [] - while size_remaining: - chunk_size = min(size_remaining, max_chunk_size) - raw_chunk = self.rfile.read(chunk_size) - if not raw_chunk: - break - chunks.append(utils.from_bytes(raw_chunk)) - size_remaining -= len(chunks[-1]) - data = ''.join(chunks) - - try: - # Decode content - data = self.decode_request_content(data) - if data is None: - # Unknown encoding, response has been sent - return - except AttributeError: - # Available since Python 2.7 - pass - - # Execute the method - response = self.server._marshaled_dispatch( - data, getattr(self, '_dispatch', None), self.path) - - # No exception: send a 200 OK - self.send_response(200) - except: - # Exception: send 500 Server Error - self.send_response(500) - err_lines = traceback.format_exception(*sys.exc_info()) - trace_string = '{0} | {1}'.format(err_lines[-2].splitlines()[0].strip(), err_lines[-1]) - fault = jsonrpclib.Fault(-32603, 'Server error: {0}' - .format(trace_string), config=config) - _logger.exception("Server-side error: %s", fault) - response = fault.response() - - if response is None: - # Avoid to send None - response = '' - - # Convert the response to the valid string format - response = utils.to_bytes(response) - - # Send it - self.send_header("Content-type", config.content_type) - self.send_header("Content-length", str(len(response))) - self.end_headers() - if response: - self.wfile.write(response) - -# ------------------------------------------------------------------------------ - - -class SimpleJSONRPCServer(socketserver.TCPServer, SimpleJSONRPCDispatcher): - """ - JSON-RPC server (and dispatcher) - """ - # This simplifies server restart after error - allow_reuse_address = True - - # pylint: disable=C0103 - def __init__(self, addr, requestHandler=SimpleJSONRPCRequestHandler, - logRequests=True, encoding=None, bind_and_activate=True, - address_family=socket.AF_INET, - config=jsonrpclib.config.DEFAULT): - """ - Sets up the server and the dispatcher - - :param addr: The server listening address - :param requestHandler: Custom request handler - :param logRequests: Flag to(de)activate requests logging - :param encoding: The dispatcher request encoding - :param bind_and_activate: If True, starts the server immediately - :param address_family: The server listening address family - :param config: A JSONRPClib Config instance - """ - # Set up the dispatcher fields - SimpleJSONRPCDispatcher.__init__(self, encoding, config) - - # Prepare the server configuration - # logRequests is used by SimpleXMLRPCRequestHandler - self.logRequests = logRequests - self.address_family = address_family - self.json_config = config - - # Work on the request handler - class RequestHandlerWrapper(requestHandler, object): - """ - Wraps the request handle to have access to the configuration - """ - def __init__(self, *args, **kwargs): - """ - Constructs the wrapper after having stored the configuration - """ - self.config = config - super(RequestHandlerWrapper, self).__init__(*args, **kwargs) - - # Set up the server - socketserver.TCPServer.__init__(self, addr, requestHandler, - bind_and_activate) - - # Windows-specific - if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'): - flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD) - flags |= fcntl.FD_CLOEXEC - fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags) - -# ------------------------------------------------------------------------------ - - -class PooledJSONRPCServer(SimpleJSONRPCServer, socketserver.ThreadingMixIn): - """ - JSON-RPC server based on a thread pool - """ - def __init__(self, addr, requestHandler=SimpleJSONRPCRequestHandler, - logRequests=True, encoding=None, bind_and_activate=True, - address_family=socket.AF_INET, - config=jsonrpclib.config.DEFAULT, thread_pool=None): - """ - Sets up the server and the dispatcher - - :param addr: The server listening address - :param requestHandler: Custom request handler - :param logRequests: Flag to(de)activate requests logging - :param encoding: The dispatcher request encoding - :param bind_and_activate: If True, starts the server immediately - :param address_family: The server listening address family - :param config: A JSONRPClib Config instance - :param thread_pool: A ThreadPool object. The pool must be started. - """ - # Normalize the thread pool - if thread_pool is None: - # Start a thread pool with 30 threads max, 0 thread min - thread_pool = jsonrpclib.threadpool.ThreadPool( - 30, 0, logname="PooledJSONRPCServer") - thread_pool.start() - - # Store the thread pool - self.__request_pool = thread_pool - - # Prepare the server - SimpleJSONRPCServer.__init__(self, addr, requestHandler, logRequests, - encoding, bind_and_activate, - address_family, config) - - def process_request(self, request, client_address): - """ - Handle a client request: queue it in the thread pool - """ - self.__request_pool.enqueue(self.process_request_thread, - request, client_address) - - def server_close(self): - """ - Clean up the server - """ - SimpleJSONRPCServer.shutdown(self) - SimpleJSONRPCServer.server_close(self) - self.__request_pool.stop() - -# ------------------------------------------------------------------------------ - - -class CGIJSONRPCRequestHandler(SimpleJSONRPCDispatcher): - """ - JSON-RPC CGI handler (and dispatcher) - """ - def __init__(self, encoding=None, config=jsonrpclib.config.DEFAULT): - """ - Sets up the dispatcher - - :param encoding: Dispatcher encoding - :param config: A JSONRPClib Config instance - """ - SimpleJSONRPCDispatcher.__init__(self, encoding, config) - - def handle_jsonrpc(self, request_text): - """ - Handle a JSON-RPC request - """ - response = self._marshaled_dispatch(request_text) - sys.stdout.write('Content-Type: {0}\r\n' - .format(self.json_config.content_type)) - sys.stdout.write('Content-Length: {0:d}\r\n'.format(len(response))) - sys.stdout.write('\r\n') - sys.stdout.write(response) - - # XML-RPC alias - handle_xmlrpc = handle_jsonrpc +#!/usr/bin/python +# -- Content-Encoding: UTF-8 -- +""" +Defines a request dispatcher, a HTTP request handler, a HTTP server and a +CGI request handler. + +:authors: Josh Marshall, Thomas Calmant +:copyright: Copyright 2020, Thomas Calmant +:license: Apache License 2.0 +:version: 0.4.1 + +.. + + Copyright 2020 Thomas Calmant + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +# We use print() in the CGI request handler +from __future__ import print_function + +# Standard library +import logging +import socket +import sys +import traceback + +try: + # Python 3 + # pylint: disable=F0401,E0611 + import xmlrpc.server as xmlrpcserver + # Make sure the module is complete. + # The "future" package under python2.7 provides an incomplete + # variant of this package. + SimpleXMLRPCDispatcher = xmlrpcserver.SimpleXMLRPCDispatcher + SimpleXMLRPCRequestHandler = xmlrpcserver.SimpleXMLRPCRequestHandler + CGIXMLRPCRequestHandler = xmlrpcserver.CGIXMLRPCRequestHandler + resolve_dotted_attribute = xmlrpcserver.resolve_dotted_attribute + import socketserver +except (ImportError, AttributeError): + # Python 2 or IronPython + # pylint: disable=F0401,E0611 + import SimpleXMLRPCServer as xmlrpcserver + SimpleXMLRPCDispatcher = xmlrpcserver.SimpleXMLRPCDispatcher + SimpleXMLRPCRequestHandler = xmlrpcserver.SimpleXMLRPCRequestHandler + CGIXMLRPCRequestHandler = xmlrpcserver.CGIXMLRPCRequestHandler + resolve_dotted_attribute = xmlrpcserver.resolve_dotted_attribute + import SocketServer as socketserver + +try: + # Windows + import fcntl +except ImportError: + # Other systems + # pylint: disable=C0103 + fcntl = None + +try: + # Python with support for Unix socket + _AF_UNIX = socket.AF_UNIX +except AttributeError: + # Unix sockets are not supported, use a dummy value + _AF_UNIX = -1 + +# Local modules +from jsonrpclib import Fault +import jsonrpclib.config +import jsonrpclib.utils as utils +import jsonrpclib.threadpool + +# ------------------------------------------------------------------------------ + +# Module version +__version_info__ = (0, 4, 1) +__version__ = ".".join(str(x) for x in __version_info__) + +# Documentation strings format +__docformat__ = "restructuredtext en" + +# Prepare the logger +_logger = logging.getLogger(__name__) + +# ------------------------------------------------------------------------------ + + +def get_version(request): + """ + Computes the JSON-RPC version + + :param request: A request dictionary + :return: The JSON-RPC version or None + """ + if 'jsonrpc' in request: + return 2.0 + elif 'id' in request: + return 1.0 + + return None + + +def validate_request(request, json_config): + """ + Validates the format of a request dictionary + + :param request: A request dictionary + :param json_config: A JSONRPClib Config instance + :return: True if the dictionary is valid, else a Fault object + """ + if not isinstance(request, utils.DictType): + # Invalid request type + fault = Fault(-32600, 'Request must be a dict, not {0}' + .format(type(request).__name__), + config=json_config) + _logger.warning("Invalid request content: %s", fault) + return fault + + # Get the request ID + rpcid = request.get('id', None) + + # Check request version + version = get_version(request) + if not version: + fault = Fault(-32600, 'Request {0} invalid.'.format(request), + rpcid=rpcid, config=json_config) + _logger.warning("No version in request: %s", fault) + return fault + + # Default parameters: empty list + request.setdefault('params', []) + + # Check parameters + method = request.get('method', None) + params = request.get('params') + param_types = (utils.ListType, utils.DictType, utils.TupleType) + + if not method or not isinstance(method, utils.STRING_TYPES) or \ + not isinstance(params, param_types): + # Invalid type of method name or parameters + fault = Fault(-32600, 'Invalid request parameters or method.', + rpcid=rpcid, config=json_config) + _logger.warning("Invalid request content: %s", fault) + return fault + + # Valid request + return True + +# ------------------------------------------------------------------------------ + + +class NoMulticallResult(Exception): + """ + No result in multicall + """ + pass + + +class SimpleJSONRPCDispatcher(SimpleXMLRPCDispatcher, object): + """ + Mix-in class that dispatches JSON-RPC requests. + + This class is used to register JSON-RPC method handlers + and then to dispatch them. This class doesn't need to be + instanced directly when used by SimpleJSONRPCServer. + """ + def __init__(self, encoding=None, config=jsonrpclib.config.DEFAULT): + """ + Sets up the dispatcher with the given encoding. + None values are allowed. + """ + SimpleXMLRPCDispatcher.__init__( + self, allow_none=True, encoding=encoding or "UTF-8") + self.json_config = config + + # Notification thread pool + self.__notification_pool = None + + def set_notification_pool(self, thread_pool): + """ + Sets the thread pool to use to handle notifications + """ + self.__notification_pool = thread_pool + + def _unmarshaled_dispatch(self, request, dispatch_method=None): + """ + Loads the request dictionary (unmarshaled), calls the method(s) + accordingly and returns a JSON-RPC dictionary (not marshaled) + + :param request: JSON-RPC request dictionary (or list of) + :param dispatch_method: Custom dispatch method (for method resolution) + :return: A JSON-RPC dictionary (or an array of) or None if the request + was a notification + :raise NoMulticallResult: No result in batch + """ + if not request: + # Invalid request dictionary + fault = Fault(-32600, 'Request invalid -- no request data.', + config=self.json_config) + _logger.warning("Invalid request: %s", fault) + return fault.dump() + + if isinstance(request, utils.ListType): + # This SHOULD be a batch, by spec + responses = [] + for req_entry in request: + # Validate the request + result = validate_request(req_entry, self.json_config) + if isinstance(result, Fault): + responses.append(result.dump()) + continue + + # Call the method + resp_entry = self._marshaled_single_dispatch( + req_entry, dispatch_method) + + # Store its result + if isinstance(resp_entry, Fault): + # pylint: disable=E1103 + responses.append(resp_entry.dump()) + elif resp_entry is not None: + responses.append(resp_entry) + + if not responses: + # No non-None result + _logger.error("No result in Multicall") + raise NoMulticallResult("No result") + + return responses + + else: + # Single call + result = validate_request(request, self.json_config) + if isinstance(result, Fault): + return result.dump() + + # Call the method + response = self._marshaled_single_dispatch( + request, dispatch_method) + if isinstance(response, Fault): + # pylint: disable=E1103 + return response.dump() + + return response + + def _marshaled_dispatch(self, data, dispatch_method=None, path=None): + """ + Parses the request data (marshaled), calls method(s) and returns a + JSON string (marshaled) + + :param data: A JSON request string + :param dispatch_method: Custom dispatch method (for method resolution) + :param path: Unused parameter, to keep compatibility with xmlrpclib + :return: A JSON-RPC response string (marshaled) + """ + # Parse the request + try: + request = jsonrpclib.loads(data, self.json_config) + except Exception as ex: + # Parsing/loading error + fault = Fault(-32700, 'Request {0} invalid. ({1}:{2})' + .format(data, type(ex).__name__, ex), + config=self.json_config) + _logger.warning("Error parsing request: %s", fault) + return fault.response() + + # Get the response dictionary + try: + response = self._unmarshaled_dispatch(request, dispatch_method) + if response is not None: + # Compute the string representation of the dictionary/list + return jsonrpclib.jdumps(response, self.encoding) + else: + # No result (notification) + return '' + except NoMulticallResult: + # Return an empty string (jsonrpclib internal behaviour) + return '' + + def _marshaled_single_dispatch(self, request, dispatch_method=None): + """ + Dispatches a single method call + + :param request: A validated request dictionary + :param dispatch_method: Custom dispatch method (for method resolution) + :return: A JSON-RPC response dictionary, or None if it was a + notification request + """ + method = request.get('method') + params = request.get('params') + + # Prepare a request-specific configuration + if 'jsonrpc' not in request and self.json_config.version >= 2: + # JSON-RPC 1.0 request on a JSON-RPC 2.0 + # => compatibility needed + config = self.json_config.copy() + config.version = 1.0 + else: + # Keep server configuration as is + config = self.json_config + + # Test if this is a notification request + is_notification = 'id' not in request or request['id'] in (None, '') + if is_notification and self.__notification_pool is not None: + # Use the thread pool for notifications + if dispatch_method is not None: + self.__notification_pool.enqueue( + dispatch_method, method, params) + else: + self.__notification_pool.enqueue( + self._dispatch, method, params, config) + + # Return immediately + return None + else: + # Synchronous call + try: + # Call the method + if dispatch_method is not None: + response = dispatch_method(method, params) + else: + response = self._dispatch(method, params, config) + except Exception as ex: + # Return a fault + fault = Fault(-32603, '{0}:{1}'.format(type(ex).__name__, ex), + config=config) + _logger.error("Error calling method %s: %s", method, fault) + return fault.dump() + + if is_notification: + # It's a notification, no result needed + # Do not use 'not id' as it might be the integer 0 + return None + + # Prepare a JSON-RPC dictionary + try: + return jsonrpclib.dump(response, rpcid=request['id'], + is_response=True, config=config) + except Exception as ex: + # JSON conversion exception + fault = Fault(-32603, '{0}:{1}'.format(type(ex).__name__, ex), + config=config) + _logger.error("Error preparing JSON-RPC result: %s", fault) + return fault.dump() + + def _dispatch(self, method, params, config=None): + """ + Default method resolver and caller + + :param method: Name of the method to call + :param params: List of arguments to give to the method + :param config: Request-specific configuration + :return: The result of the method + """ + config = config or self.json_config + + func = None + try: + # Look into registered methods + func = self.funcs[method] + except KeyError: + if self.instance is not None: + # Try with the registered instance + try: + # Instance has a custom dispatcher + return getattr(self.instance, '_dispatch')(method, params) + except AttributeError: + # Resolve the method name in the instance + try: + func = resolve_dotted_attribute( + self.instance, method, True) + except AttributeError: + # Unknown method + pass + + if func is not None: + try: + # Call the method + if isinstance(params, utils.ListType): + return func(*params) + else: + return func(**params) + except TypeError as ex: + # Maybe the parameters are wrong + fault = Fault(-32602, 'Invalid parameters: {0}'.format(ex), + config=config) + _logger.warning("Invalid call parameters: %s", fault) + return fault + except: + # Method exception + err_lines = traceback.format_exception(*sys.exc_info()) + trace_string = "{0} | {1}".format( + err_lines[-2].splitlines()[0].strip(), err_lines[-1]) + fault = Fault(-32603, "Server error: {0}".format(trace_string), + config=config) + _logger.exception("Server-side exception: %s", fault) + return fault + else: + # Unknown method + fault = Fault(-32601, 'Method {0} not supported.'.format(method), + config=config) + _logger.warning("Unknown method: %s", fault) + return fault + +# ------------------------------------------------------------------------------ + + +class SimpleJSONRPCRequestHandler(SimpleXMLRPCRequestHandler): + """ + HTTP request handler. + + The server that receives the requests must have a json_config member, + containing a JSONRPClib Config instance + """ + def do_POST(self): + """ + Handles POST requests + """ + if not self.is_rpc_path_valid(): + self.report_404() + return + + # Retrieve the configuration + config = getattr(self.server, 'json_config', jsonrpclib.config.DEFAULT) + + try: + # Read the request body + max_chunk_size = 10 * 1024 * 1024 + size_remaining = int(self.headers["content-length"]) + chunks = [] + while size_remaining: + chunk_size = min(size_remaining, max_chunk_size) + raw_chunk = self.rfile.read(chunk_size) + if not raw_chunk: + break + chunks.append(utils.from_bytes(raw_chunk)) + size_remaining -= len(raw_chunk) + data = ''.join(chunks) + + try: + # Decode content + data = self.decode_request_content(data) + if data is None: + # Unknown encoding, response has been sent + return + except AttributeError: + # Available since Python 2.7 + pass + + # Execute the method + response = self.server._marshaled_dispatch( + data, getattr(self, '_dispatch', None), self.path) + + # No exception: send a 200 OK + self.send_response(200) + except: + # Exception: send 500 Server Error + self.send_response(500) + err_lines = traceback.format_exception(*sys.exc_info()) + trace_string = "{0} | {1}".format( + err_lines[-2].splitlines()[0].strip(), err_lines[-1]) + fault = jsonrpclib.Fault(-32603, "Server error: {0}" + .format(trace_string), config=config) + _logger.exception("Server-side error: %s", fault) + response = fault.response() + + if response is None: + # Avoid to send None + response = '' + + # Convert the response to the valid string format + response = utils.to_bytes(response) + + # Send it + self.send_header("Content-type", config.content_type) + self.send_header("Content-length", str(len(response))) + self.end_headers() + if response: + self.wfile.write(response) + +# ------------------------------------------------------------------------------ + + +class SimpleJSONRPCServer(socketserver.TCPServer, SimpleJSONRPCDispatcher): + """ + JSON-RPC server (and dispatcher) + """ + # This simplifies server restart after error + allow_reuse_address = True + + # pylint: disable=C0103 + def __init__(self, addr, requestHandler=SimpleJSONRPCRequestHandler, + logRequests=True, encoding=None, bind_and_activate=True, + address_family=socket.AF_INET, + config=jsonrpclib.config.DEFAULT): + """ + Sets up the server and the dispatcher + + :param addr: The server listening address + :param requestHandler: Custom request handler + :param logRequests: Flag to(de)activate requests logging + :param encoding: The dispatcher request encoding + :param bind_and_activate: If True, starts the server immediately + :param address_family: The server listening address family + :param config: A JSONRPClib Config instance + """ + # Set up the dispatcher fields + SimpleJSONRPCDispatcher.__init__(self, encoding, config) + + # Flag to ease handling of Unix socket mode + unix_socket = address_family == _AF_UNIX + + # Disable the reuse address flag when in Unix socket mode, or an + # exception will raise when binding the socket + self.allow_reuse_address = self.allow_reuse_address and not unix_socket + + # Prepare the server configuration + self.address_family = address_family + self.json_config = config + + # logRequests is used by SimpleXMLRPCRequestHandler + # This must be disabled in Unix socket mode (or an exception will raise + # at each connection) + self.logRequests = logRequests and not unix_socket + + # Work on the request handler + class RequestHandlerWrapper(requestHandler, object): + """ + Wraps the request handle to have access to the configuration + """ + def __init__(self, *args, **kwargs): + """ + Constructs the wrapper after having stored the configuration + """ + self.config = config + + if unix_socket: + # Disable TCP features over Unix socket, or an + # "invalid argument" error will raise + self.disable_nagle_algorithm = False + + super(RequestHandlerWrapper, self).__init__(*args, **kwargs) + + # Set up the server + socketserver.TCPServer.__init__(self, addr, RequestHandlerWrapper, + bind_and_activate) + + # Windows-specific + if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'): + flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD) + flags |= fcntl.FD_CLOEXEC + fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags) + +# ------------------------------------------------------------------------------ + + +class PooledJSONRPCServer(SimpleJSONRPCServer, socketserver.ThreadingMixIn): + """ + JSON-RPC server based on a thread pool + """ + def __init__(self, addr, requestHandler=SimpleJSONRPCRequestHandler, + logRequests=True, encoding=None, bind_and_activate=True, + address_family=socket.AF_INET, + config=jsonrpclib.config.DEFAULT, thread_pool=None): + """ + Sets up the server and the dispatcher + + :param addr: The server listening address + :param requestHandler: Custom request handler + :param logRequests: Flag to(de)activate requests logging + :param encoding: The dispatcher request encoding + :param bind_and_activate: If True, starts the server immediately + :param address_family: The server listening address family + :param config: A JSONRPClib Config instance + :param thread_pool: A ThreadPool object. The pool must be started. + """ + # Normalize the thread pool + if thread_pool is None: + # Start a thread pool with 30 threads max, 0 thread min + thread_pool = jsonrpclib.threadpool.ThreadPool( + 30, 0, logname="PooledJSONRPCServer") + thread_pool.start() + + # Store the thread pool + self.__request_pool = thread_pool + + # Prepare the server + SimpleJSONRPCServer.__init__(self, addr, requestHandler, logRequests, + encoding, bind_and_activate, + address_family, config) + + def process_request(self, request, client_address): + """ + Handle a client request: queue it in the thread pool + """ + self.__request_pool.enqueue(self.process_request_thread, + request, client_address) + + def server_close(self): + """ + Clean up the server + """ + SimpleJSONRPCServer.shutdown(self) + SimpleJSONRPCServer.server_close(self) + self.__request_pool.stop() + +# ------------------------------------------------------------------------------ + + +class CGIJSONRPCRequestHandler(SimpleJSONRPCDispatcher, CGIXMLRPCRequestHandler): + """ + JSON-RPC CGI handler (and dispatcher) + """ + def __init__(self, encoding="UTF-8", config=jsonrpclib.config.DEFAULT): + """ + Sets up the dispatcher + + :param encoding: Dispatcher encoding + :param config: A JSONRPClib Config instance + """ + SimpleJSONRPCDispatcher.__init__(self, encoding, config) + CGIXMLRPCRequestHandler.__init__(self, encoding=encoding) + + def handle_jsonrpc(self, request_text): + """ + Handle a JSON-RPC request + """ + try: + writer = sys.stdout.buffer + except AttributeError: + writer = sys.stdout + + response = self._marshaled_dispatch(request_text) + response = response.encode(self.encoding) + print("Content-Type:", self.json_config.content_type) + print("Content-Length:", len(response)) + print() + sys.stdout.flush() + writer.write(response) + writer.flush() + + # XML-RPC alias + handle_xmlrpc = handle_jsonrpc diff -Nru jsonrpclib-pelix-0.3.1/jsonrpclib/threadpool.py jsonrpclib-pelix-0.4.1/jsonrpclib/threadpool.py --- jsonrpclib-pelix-0.3.1/jsonrpclib/threadpool.py 2017-06-27 08:56:32.000000000 +0000 +++ jsonrpclib-pelix-0.4.1/jsonrpclib/threadpool.py 2020-04-12 14:34:56.000000000 +0000 @@ -1,504 +1,515 @@ -#!/usr/bin/env python -# -- Content-Encoding: UTF-8 -- -""" -Cached thread pool, inspired from Pelix/iPOPO Thread Pool - -:author: Thomas Calmant -:copyright: Copyright 2017, Thomas Calmant -:license: Apache License 2.0 -:version: 0.3.1 - -.. - - Copyright 2017 Thomas Calmant - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -""" - -# Standard library -import logging -import threading - -try: - # Python 3 - # pylint: disable=F0401 - import queue -except ImportError: - # Python 2 - # pylint: disable=F0401 - import Queue as queue - -# ------------------------------------------------------------------------------ - -# Module version -__version_info__ = (0, 3, 1) -__version__ = ".".join(str(x) for x in __version_info__) - -# Documentation strings format -__docformat__ = "restructuredtext en" - -# ------------------------------------------------------------------------------ - - -class EventData(object): - """ - A threading event with some associated data - """ - def __init__(self): - """ - Sets up the event - """ - self.__event = threading.Event() - self.__data = None - self.__exception = None - - @property - def data(self): - """ - Returns the associated value - """ - return self.__data - - @property - def exception(self): - """ - Returns the exception used to stop the wait() method - """ - return self.__exception - - def clear(self): - """ - Clears the event - """ - self.__event.clear() - self.__data = None - self.__exception = None - - def is_set(self): - """ - Checks if the event is set - """ - return self.__event.is_set() - - def set(self, data=None): - """ - Sets the event - """ - self.__data = data - self.__exception = None - self.__event.set() - - def raise_exception(self, exception): - """ - Raises an exception in wait() - - :param exception: An Exception object - """ - self.__data = None - self.__exception = exception - self.__event.set() - - def wait(self, timeout=None): - """ - Waits for the event or for the timeout - - :param timeout: Wait timeout (in seconds) - :return: True if the event as been set, else False - """ - # The 'or' part is for Python 2.6 - result = self.__event.wait(timeout) - # pylint: disable=E0702 - # Pylint seems to miss the "is None" check below - if self.__exception is None: - return result - else: - raise self.__exception - - -class FutureResult(object): - """ - An object to wait for the result of a threaded execution - """ - def __init__(self, logger=None): - """ - Sets up the FutureResult object - - :param logger: The Logger to use in case of error (optional) - """ - self._logger = logger or logging.getLogger(__name__) - self._done_event = EventData() - self.__callback = None - self.__extra = None - - def __notify(self): - """ - Notify the given callback about the result of the execution - """ - if self.__callback is not None: - try: - self.__callback(self._done_event.data, - self._done_event.exception, - self.__extra) - except Exception as ex: - self._logger.exception("Error calling back method: %s", ex) - - def set_callback(self, method, extra=None): - """ - Sets a callback method, called once the result has been computed or in - case of exception. - - The callback method must have the following signature: - ``callback(result, exception, extra)``. - - :param method: The method to call back in the end of the execution - :param extra: Extra parameter to be given to the callback method - """ - self.__callback = method - self.__extra = extra - if self._done_event.is_set(): - # The execution has already finished - self.__notify() - - def execute(self, method, args, kwargs): - """ - Execute the given method and stores its result. - The result is considered "done" even if the method raises an exception - - :param method: The method to execute - :param args: Method positional arguments - :param kwargs: Method keyword arguments - :raise Exception: The exception raised by the method - """ - # Normalize arguments - if args is None: - args = [] - - if kwargs is None: - kwargs = {} - - try: - # Call the method - result = method(*args, **kwargs) - except Exception as ex: - # Something went wrong: propagate to the event and to the caller - self._done_event.raise_exception(ex) - raise - else: - # Store the result - self._done_event.set(result) - finally: - # In any case: notify the call back (if any) - self.__notify() - - def done(self): - """ - Returns True if the job has finished, else False - """ - return self._done_event.is_set() - - def result(self, timeout=None): - """ - Waits up to timeout for the result the threaded job. - Returns immediately the result if the job has already been done. - - :param timeout: The maximum time to wait for a result (in seconds) - :raise OSError: The timeout raised before the job finished - :raise Exception: The exception encountered during the call, if any - """ - if self._done_event.wait(timeout): - return self._done_event.data - else: - raise OSError("Timeout raised") - -# ------------------------------------------------------------------------------ - - -class ThreadPool(object): - """ - Executes the tasks stored in a FIFO in a thread pool - """ - def __init__(self, max_threads, min_threads=1, queue_size=0, timeout=60, - logname=None): - """ - Sets up the thread pool. - - Threads are kept alive 60 seconds (timeout argument). - - :param max_threads: Maximum size of the thread pool - :param min_threads: Minimum size of the thread pool - :param queue_size: Size of the task queue (0 for infinite) - :param timeout: Queue timeout (in seconds, 60s by default) - :param logname: Name of the logger - :raise ValueError: Invalid number of threads - """ - # Validate parameters - try: - max_threads = int(max_threads) - if max_threads < 1: - raise ValueError("Pool size must be greater than 0") - except (TypeError, ValueError) as ex: - raise ValueError("Invalid pool size: {0}".format(ex)) - - try: - min_threads = int(min_threads) - if min_threads < 0: - min_threads = 0 - elif min_threads > max_threads: - min_threads = max_threads - except (TypeError, ValueError) as ex: - raise ValueError("Invalid pool size: {0}".format(ex)) - - # The logger - self._logger = logging.getLogger(logname or __name__) - - # The loop control event - self._done_event = threading.Event() - self._done_event.set() - - # The task queue - try: - queue_size = int(queue_size) - except (TypeError, ValueError): - # Not a valid integer - queue_size = 0 - - self._queue = queue.Queue(queue_size) - self._timeout = timeout - self.__lock = threading.RLock() - - # The thread pool - self._min_threads = min_threads - self._max_threads = max_threads - self._threads = [] - - # Thread count - self._thread_id = 0 - - # Current number of threads, active and alive, - # and number of task waiting - self.__nb_threads = 0 - self.__nb_active_threads = 0 - self.__nb_pending_task = 0 - - def start(self): - """ - Starts the thread pool. Does nothing if the pool is already started. - """ - if not self._done_event.is_set(): - # Stop event not set: we're running - return - - # Clear the stop event - self._done_event.clear() - - # Compute the number of threads to start to handle pending tasks - nb_pending_tasks = self._queue.qsize() - if nb_pending_tasks > self._max_threads: - nb_threads = self._max_threads - nb_pending_tasks = self._max_threads - elif nb_pending_tasks < self._min_threads: - nb_threads = self._min_threads - else: - nb_threads = nb_pending_tasks - - # Create the threads - for _ in range(nb_pending_tasks): - self.__nb_pending_task += 1 - self.__start_thread() - for _ in range(nb_threads-nb_pending_tasks): - self.__start_thread() - - def __start_thread(self): - """ - Starts a new thread, if possible - """ - with self.__lock: - if self.__nb_threads >= self._max_threads: - # Can't create more threads - return False - - if self._done_event.is_set(): - # We're stopped: do nothing - return False - - # Prepare thread and start it - name = "{0}-{1}".format(self._logger.name, self._thread_id) - self._thread_id += 1 - - thread = threading.Thread(target=self.__run, name=name) - thread.daemon = True - try: - self.__nb_threads += 1 - thread.start() - self._threads.append(thread) - return True - except (RuntimeError, OSError): - self.__nb_threads -= 1 - return False - - def stop(self): - """ - Stops the thread pool. Does nothing if the pool is already stopped. - """ - if self._done_event.is_set(): - # Stop event set: we're stopped - return - - # Set the stop event - self._done_event.set() - - with self.__lock: - # Add something in the queue (to unlock the join()) - try: - for _ in self._threads: - self._queue.put(self._done_event, True, self._timeout) - except queue.Full: - # There is already something in the queue - pass - - # Copy the list of threads to wait for - threads = self._threads[:] - - # Join threads outside the lock - for thread in threads: - while thread.is_alive(): - # Wait 3 seconds - thread.join(3) - if thread.is_alive(): - # Thread is still alive: something might be wrong - self._logger.warning("Thread %s is still alive...", - thread.name) - - # Clear storage - del self._threads[:] - self.clear() - - def enqueue(self, method, *args, **kwargs): - """ - Queues a task in the pool - - :param method: Method to call - :return: A FutureResult object, to get the result of the task - :raise ValueError: Invalid method - :raise Full: The task queue is full - """ - if not hasattr(method, '__call__'): - raise ValueError("{0} has no __call__ member." - .format(method.__name__)) - - # Prepare the future result object - future = FutureResult(self._logger) - - # Use a lock, as we might be "resetting" the queue - with self.__lock: - # Add the task to the queue - self._queue.put((method, args, kwargs, future), True, - self._timeout) - self.__nb_pending_task += 1 - - if self.__nb_pending_task > self.__nb_threads: - # All threads are taken: start a new one - self.__start_thread() - - return future - - def clear(self): - """ - Empties the current queue content. - Returns once the queue have been emptied. - """ - with self.__lock: - # Empty the current queue - try: - while True: - self._queue.get_nowait() - self._queue.task_done() - except queue.Empty: - # Queue is now empty - pass - - # Wait for the tasks currently executed - self.join() - - def join(self, timeout=None): - """ - Waits for all the tasks to be executed - - :param timeout: Maximum time to wait (in seconds) - :return: True if the queue has been emptied, else False - """ - if self._queue.empty(): - # Nothing to wait for... - return True - elif timeout is None: - # Use the original join - self._queue.join() - return True - else: - # Wait for the condition - with self._queue.all_tasks_done: - self._queue.all_tasks_done.wait(timeout) - return not bool(self._queue.unfinished_tasks) - - def __run(self): - """ - The main loop - """ - while not self._done_event.is_set(): - try: - # Wait for an action (blocking) - task = self._queue.get(True, self._timeout) - if task is self._done_event: - # Stop event in the queue: get out - self._queue.task_done() - with self.__lock: - self.__nb_threads -= 1 - return - except queue.Empty: - # Nothing to do yet - pass - else: - with self.__lock: - self.__nb_active_threads += 1 - # Extract elements - method, args, kwargs, future = task - try: - # Call the method - future.execute(method, args, kwargs) - except Exception as ex: - self._logger.exception("Error executing %s: %s", - method.__name__, ex) - finally: - # Mark the action as executed - self._queue.task_done() - - # Thread is not active anymore - with self.__lock: - self.__nb_pending_task -= 1 - self.__nb_active_threads -= 1 - - # Clean up thread if necessary - with self.__lock: - extra_threads = self.__nb_threads - self.__nb_active_threads - if self.__nb_threads > self._min_threads \ - and extra_threads > self._queue.qsize(): - # No more work for this thread - # if there are more non active_thread than task - # and we're above the minimum number of threads: - # stop this one - self.__nb_threads -= 1 - return - - with self.__lock: - # Thread stops - self.__nb_threads -= 1 +#!/usr/bin/env python +# -- Content-Encoding: UTF-8 -- +""" +Cached thread pool, inspired from Pelix/iPOPO Thread Pool + +:author: Thomas Calmant +:copyright: Copyright 2020, Thomas Calmant +:license: Apache License 2.0 +:version: 0.4.1 + +.. + + Copyright 2020 Thomas Calmant + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +# Standard library +import logging +import threading + +try: + # Python 3 + # pylint: disable=F0401 + import queue +except ImportError: + # Python 2 + # pylint: disable=F0401 + import Queue as queue + +# ------------------------------------------------------------------------------ + +# Module version +__version_info__ = (0, 4, 1) +__version__ = ".".join(str(x) for x in __version_info__) + +# Documentation strings format +__docformat__ = "restructuredtext en" + +# ------------------------------------------------------------------------------ + + +class EventData(object): + """ + A threading event with some associated data + """ + def __init__(self): + """ + Sets up the event + """ + self.__event = threading.Event() + self.__data = None + self.__exception = None + + @property + def data(self): + """ + Returns the associated value + """ + return self.__data + + @property + def exception(self): + """ + Returns the exception used to stop the wait() method + """ + return self.__exception + + def clear(self): + """ + Clears the event + """ + self.__event.clear() + self.__data = None + self.__exception = None + + def is_set(self): + """ + Checks if the event is set + """ + return self.__event.is_set() + + def set(self, data=None): + """ + Sets the event + """ + self.__data = data + self.__exception = None + self.__event.set() + + def raise_exception(self, exception): + """ + Raises an exception in wait() + + :param exception: An Exception object + """ + self.__data = None + self.__exception = exception + self.__event.set() + + def wait(self, timeout=None): + """ + Waits for the event or for the timeout + + :param timeout: Wait timeout (in seconds) + :return: True if the event as been set, else False + """ + # The 'or' part is for Python 2.6 + result = self.__event.wait(timeout) + # pylint: disable=E0702 + # Pylint seems to miss the "is None" check below + if self.__exception is None: + return result + else: + raise self.__exception + + +class FutureResult(object): + """ + An object to wait for the result of a threaded execution + """ + def __init__(self, logger=None): + """ + Sets up the FutureResult object + + :param logger: The Logger to use in case of error (optional) + """ + self._logger = logger or logging.getLogger(__name__) + self._done_event = EventData() + self.__callback = None + self.__extra = None + + def __notify(self): + """ + Notify the given callback about the result of the execution + """ + if self.__callback is not None: + try: + self.__callback(self._done_event.data, + self._done_event.exception, + self.__extra) + except Exception as ex: + self._logger.exception("Error calling back method: %s", ex) + + def set_callback(self, method, extra=None): + """ + Sets a callback method, called once the result has been computed or in + case of exception. + + The callback method must have the following signature: + ``callback(result, exception, extra)``. + + :param method: The method to call back in the end of the execution + :param extra: Extra parameter to be given to the callback method + """ + self.__callback = method + self.__extra = extra + if self._done_event.is_set(): + # The execution has already finished + self.__notify() + + def execute(self, method, args, kwargs): + """ + Execute the given method and stores its result. + The result is considered "done" even if the method raises an exception + + :param method: The method to execute + :param args: Method positional arguments + :param kwargs: Method keyword arguments + :raise Exception: The exception raised by the method + """ + # Normalize arguments + if args is None: + args = [] + + if kwargs is None: + kwargs = {} + + try: + # Call the method + result = method(*args, **kwargs) + except Exception as ex: + # Something went wrong: propagate to the event and to the caller + self._done_event.raise_exception(ex) + raise + else: + # Store the result + self._done_event.set(result) + finally: + # In any case: notify the call back (if any) + self.__notify() + + def done(self): + """ + Returns True if the job has finished, else False + """ + return self._done_event.is_set() + + def result(self, timeout=None): + """ + Waits up to timeout for the result the threaded job. + Returns immediately the result if the job has already been done. + + :param timeout: The maximum time to wait for a result (in seconds) + :raise OSError: The timeout raised before the job finished + :raise Exception: The exception encountered during the call, if any + """ + if self._done_event.wait(timeout): + return self._done_event.data + else: + raise OSError("Timeout raised") + +# ------------------------------------------------------------------------------ + + +class ThreadPool(object): + """ + Executes the tasks stored in a FIFO in a thread pool + """ + def __init__(self, max_threads, min_threads=1, queue_size=0, timeout=60, + logname=None): + """ + Sets up the thread pool. + + Threads are kept alive 60 seconds (timeout argument). + + :param max_threads: Maximum size of the thread pool + :param min_threads: Minimum size of the thread pool + :param queue_size: Size of the task queue (0 for infinite) + :param timeout: Queue timeout (in seconds, 60s by default) + :param logname: Name of the logger + :raise ValueError: Invalid number of threads + """ + # Validate parameters + try: + max_threads = int(max_threads) + if max_threads < 1: + raise ValueError("Pool size must be greater than 0") + except (TypeError, ValueError) as ex: + raise ValueError("Invalid pool size: {0}".format(ex)) + + try: + min_threads = int(min_threads) + if min_threads < 0: + min_threads = 0 + elif min_threads > max_threads: + min_threads = max_threads + except (TypeError, ValueError) as ex: + raise ValueError("Invalid pool size: {0}".format(ex)) + + # The logger + self._logger = logging.getLogger(logname or __name__) + + # The loop control event + self._done_event = threading.Event() + self._done_event.set() + + # The task queue + try: + queue_size = int(queue_size) + except (TypeError, ValueError): + # Not a valid integer + queue_size = 0 + + self._queue = queue.Queue(queue_size) + self._timeout = timeout + self.__lock = threading.RLock() + + # The thread pool + self._min_threads = min_threads + self._max_threads = max_threads + self._threads = [] + + # Thread count + self._thread_id = 0 + + # Current number of threads, active and alive, + # and number of task waiting + self.__nb_threads = 0 + self.__nb_active_threads = 0 + self.__nb_pending_task = 0 + + def start(self): + """ + Starts the thread pool. Does nothing if the pool is already started. + """ + if not self._done_event.is_set(): + # Stop event not set: we're running + return + + # Clear the stop event + self._done_event.clear() + + # Compute the number of threads to start to handle pending tasks + nb_pending_tasks = self._queue.qsize() + if nb_pending_tasks > self._max_threads: + nb_threads = self._max_threads + nb_pending_tasks = self._max_threads + elif nb_pending_tasks < self._min_threads: + nb_threads = self._min_threads + else: + nb_threads = nb_pending_tasks + + # Create the threads + for _ in range(nb_pending_tasks): + self.__nb_pending_task += 1 + self.__start_thread() + for _ in range(nb_threads-nb_pending_tasks): + self.__start_thread() + + def __start_thread(self): + """ + Starts a new thread, if possible + """ + with self.__lock: + if self.__nb_threads >= self._max_threads: + # Can't create more threads + return False + + if self._done_event.is_set(): + # We're stopped: do nothing + return False + + # Prepare thread and start it + name = "{0}-{1}".format(self._logger.name, self._thread_id) + self._thread_id += 1 + + thread = threading.Thread(target=self.__run, name=name) + thread.daemon = True + try: + self.__nb_threads += 1 + thread.start() + self._threads.append(thread) + return True + except (RuntimeError, OSError): + self.__nb_threads -= 1 + return False + + def stop(self): + """ + Stops the thread pool. Does nothing if the pool is already stopped. + """ + if self._done_event.is_set(): + # Stop event set: we're stopped + return + + # Set the stop event + self._done_event.set() + + with self.__lock: + # Add something in the queue (to unlock the join()) + try: + for _ in self._threads: + self._queue.put(self._done_event, True, self._timeout) + except queue.Full: + # There is already something in the queue + pass + + # Copy the list of threads to wait for + threads = self._threads[:] + + # Join threads outside the lock + for thread in threads: + while thread.is_alive(): + # Wait 3 seconds + thread.join(3) + if thread.is_alive(): + # Thread is still alive: something might be wrong + self._logger.warning("Thread %s is still alive...", + thread.name) + + # Clear storage + del self._threads[:] + self.clear() + + def enqueue(self, method, *args, **kwargs): + """ + Queues a task in the pool + + :param method: Method to call + :return: A FutureResult object, to get the result of the task + :raise ValueError: Invalid method + :raise Full: The task queue is full + """ + if not hasattr(method, '__call__'): + raise ValueError("{0} has no __call__ member." + .format(method.__name__)) + + # Prepare the future result object + future = FutureResult(self._logger) + + # Use a lock, as we might be "resetting" the queue + with self.__lock: + # Add the task to the queue + self._queue.put((method, args, kwargs, future), True, + self._timeout) + self.__nb_pending_task += 1 + + if self.__nb_pending_task > self.__nb_threads: + # All threads are taken: start a new one + self.__start_thread() + + return future + + def clear(self): + """ + Empties the current queue content. + Returns once the queue have been emptied. + """ + with self.__lock: + # Empty the current queue + try: + while True: + self._queue.get_nowait() + self._queue.task_done() + except queue.Empty: + # Queue is now empty + pass + + # Wait for the tasks currently executed + self.join() + + def join(self, timeout=None): + """ + Waits for all the tasks to be executed + + :param timeout: Maximum time to wait (in seconds) + :return: True if the queue has been emptied, else False + """ + if self._queue.empty(): + # Nothing to wait for... + return True + elif timeout is None: + # Use the original join + self._queue.join() + return True + else: + # Wait for the condition + with self._queue.all_tasks_done: + self._queue.all_tasks_done.wait(timeout) + return not bool(self._queue.unfinished_tasks) + + def __run(self): + """ + The main loop + """ + already_cleaned = False + try: + while not self._done_event.is_set(): + try: + # Wait for an action (blocking) + task = self._queue.get(True, self._timeout) + if task is self._done_event: + # Stop event in the queue: get out + self._queue.task_done() + return + except queue.Empty: + # Nothing to do yet + pass + else: + with self.__lock: + self.__nb_active_threads += 1 + # Extract elements + method, args, kwargs, future = task + try: + # Call the method + future.execute(method, args, kwargs) + except Exception as ex: + self._logger.exception("Error executing %s: %s", + method.__name__, ex) + finally: + # Mark the action as executed + self._queue.task_done() + + # Thread is not active anymore + with self.__lock: + self.__nb_pending_task -= 1 + self.__nb_active_threads -= 1 + + # Clean up thread if necessary + with self.__lock: + extra_threads = self.__nb_threads - self.__nb_active_threads + if self.__nb_threads > self._min_threads \ + and extra_threads > self._queue.qsize(): + # No more work for this thread + # if there are more non active_thread than task + # and we're above the minimum number of threads: + # stop this one + self.__nb_threads -= 1 + + # To avoid a race condition: decrease the number of + # threads here and mark it as already accounted for + already_cleaned = True + return + finally: + # Always clean up + with self.__lock: + # Thread stops: clean up references + try: + self._threads.remove(threading.current_thread()) + except ValueError: + pass + + if not already_cleaned: + self.__nb_threads -= 1 diff -Nru jsonrpclib-pelix-0.3.1/jsonrpclib/utils.py jsonrpclib-pelix-0.4.1/jsonrpclib/utils.py --- jsonrpclib-pelix-0.3.1/jsonrpclib/utils.py 2017-06-27 08:56:32.000000000 +0000 +++ jsonrpclib-pelix-0.4.1/jsonrpclib/utils.py 2020-04-12 14:34:55.000000000 +0000 @@ -1,149 +1,149 @@ -#!/usr/bin/python -# -- Content-Encoding: UTF-8 -- -""" -Utility methods, for compatibility between Python version - -:author: Thomas Calmant -:copyright: Copyright 2017, Thomas Calmant -:license: Apache License 2.0 -:version: 0.3.1 - -.. - - Copyright 2017 Thomas Calmant - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -""" - -import sys - -# ------------------------------------------------------------------------------ - -# Module version -__version_info__ = (0, 3, 1) -__version__ = ".".join(str(x) for x in __version_info__) - -# Documentation strings format -__docformat__ = "restructuredtext en" - -# ------------------------------------------------------------------------------ - -if sys.version_info[0] < 3: - # Python 2 - # pylint: disable=E1101 - import types - try: - STRING_TYPES = ( - types.StringType, - types.UnicodeType - ) - except NameError: - # Python built without unicode support - STRING_TYPES = (types.StringType,) - - NUMERIC_TYPES = ( - types.IntType, - types.LongType, - types.FloatType - ) - - def to_bytes(string): - """ - Converts the given string into bytes - """ - # pylint: disable=E0602 - if type(string) is unicode: - return str(string) - return string - - def from_bytes(data): - """ - Converts the given bytes into a string - """ - if type(data) is str: - return data - return str(data) -else: - # Python 3 - # pylint: disable=E1101 - STRING_TYPES = ( - bytes, - str - ) - - NUMERIC_TYPES = ( - int, - float - ) - - def to_bytes(string): - """ - Converts the given string into bytes - """ - if type(string) is bytes: - return string - return bytes(string, "UTF-8") - - def from_bytes(data): - """ - Converts the given bytes into a string - """ - if type(data) is str: - return data - return str(data, "UTF-8") - -# ------------------------------------------------------------------------------ -# Enumerations - -try: - import enum - - def is_enum(obj): - """ - Checks if an object is from an enumeration class - - :param obj: Object to test - :return: True if the object is an enumeration item - """ - return isinstance(obj, enum.Enum) -except ImportError: - # Pre-Python 3.4 - def is_enum(_): - """ - Before Python 3.4, enumerations didn't exist. - - :param _: Object to test - :return: Always False - """ - return False - -# ------------------------------------------------------------------------------ -# Common - -DictType = dict - -ListType = list -TupleType = tuple - -ITERABLE_TYPES = ( - list, - set, frozenset, - tuple -) - -VALUE_TYPES = ( - bool, - type(None) -) - -PRIMITIVE_TYPES = STRING_TYPES + NUMERIC_TYPES + VALUE_TYPES +#!/usr/bin/python +# -- Content-Encoding: UTF-8 -- +""" +Utility methods, for compatibility between Python version + +:author: Thomas Calmant +:copyright: Copyright 2020, Thomas Calmant +:license: Apache License 2.0 +:version: 0.4.1 + +.. + + Copyright 2020 Thomas Calmant + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +import sys + +# ------------------------------------------------------------------------------ + +# Module version +__version_info__ = (0, 4, 1) +__version__ = ".".join(str(x) for x in __version_info__) + +# Documentation strings format +__docformat__ = "restructuredtext en" + +# ------------------------------------------------------------------------------ + +if sys.version_info[0] < 3: + # Python 2 + # pylint: disable=E1101 + import types + try: + STRING_TYPES = ( + types.StringType, + types.UnicodeType + ) + except NameError: + # Python built without unicode support + STRING_TYPES = (types.StringType,) + + NUMERIC_TYPES = ( + types.IntType, + types.LongType, + types.FloatType + ) + + def to_bytes(string): + """ + Converts the given string into bytes + """ + # pylint: disable=E0602 + if type(string) is unicode: + return str(string) + return string + + def from_bytes(data): + """ + Converts the given bytes into a string + """ + if type(data) is str: + return data + return str(data) +else: + # Python 3 + # pylint: disable=E1101 + STRING_TYPES = ( + bytes, + str + ) + + NUMERIC_TYPES = ( + int, + float + ) + + def to_bytes(string): + """ + Converts the given string into bytes + """ + if type(string) is bytes: + return string + return bytes(string, "UTF-8") + + def from_bytes(data): + """ + Converts the given bytes into a string + """ + if type(data) is str: + return data + return str(data, "UTF-8") + +# ------------------------------------------------------------------------------ +# Enumerations + +try: + import enum + + def is_enum(obj): + """ + Checks if an object is from an enumeration class + + :param obj: Object to test + :return: True if the object is an enumeration item + """ + return isinstance(obj, enum.Enum) +except ImportError: + # Pre-Python 3.4 + def is_enum(_): + """ + Before Python 3.4, enumerations didn't exist. + + :param _: Object to test + :return: Always False + """ + return False + +# ------------------------------------------------------------------------------ +# Common + +DictType = dict + +ListType = list +TupleType = tuple + +ITERABLE_TYPES = ( + list, + set, frozenset, + tuple +) + +VALUE_TYPES = ( + bool, + type(None) +) + +PRIMITIVE_TYPES = STRING_TYPES + NUMERIC_TYPES + VALUE_TYPES diff -Nru jsonrpclib-pelix-0.3.1/jsonrpclib_pelix.egg-info/PKG-INFO jsonrpclib-pelix-0.4.1/jsonrpclib_pelix.egg-info/PKG-INFO --- jsonrpclib-pelix-0.3.1/jsonrpclib_pelix.egg-info/PKG-INFO 2017-06-27 11:08:06.000000000 +0000 +++ jsonrpclib-pelix-0.4.1/jsonrpclib_pelix.egg-info/PKG-INFO 2020-04-12 14:48:26.000000000 +0000 @@ -1,452 +1,498 @@ -Metadata-Version: 1.1 +Metadata-Version: 2.1 Name: jsonrpclib-pelix -Version: 0.3.1 -Summary: This project is an implementation of the JSON-RPC v2.0 specification (backwards-compatible) as a client library, for Python 2.7 and Python 3.This version is a fork of jsonrpclib by Josh Marshall, usable with Pelix remote services. +Version: 0.4.1 +Summary: This project is an implementation of the JSON-RPC v2.0 specification (backwards-compatible) as a client library, for Python 2.7 and Python 3. This version is a fork of jsonrpclib by Josh Marshall, made to be also usable with Pelix/iPOPO remote services. Home-page: http://github.com/tcalmant/jsonrpclib/ Author: Thomas Calmant Author-email: thomas.calmant+github@gmail.com License: Apache License 2.0 -Description: JSONRPClib (patched for Pelix and Python 3) - ########################################### +Description: # JSONRPClib (patched for Pelix and Python 3) - .. image:: https://img.shields.io/pypi/v/jsonrpclib-pelix.svg - :target: https://pypi.python.org/pypi/jsonrpclib-pelix/ - :alt: Latest Version - - .. image:: https://img.shields.io/pypi/l/jsonrpclib-pelix.svg - :target: https://pypi.python.org/pypi/jsonrpclib-pelix/ - :alt: License - - .. image:: https://travis-ci.org/tcalmant/jsonrpclib.svg?branch=master - :target: https://travis-ci.org/tcalmant/jsonrpclib - :alt: Travis-CI status - - .. image:: https://coveralls.io/repos/tcalmant/jsonrpclib/badge.svg?branch=master - :target: https://coveralls.io/r/tcalmant/jsonrpclib?branch=master - :alt: Coveralls status + [![Latest Version](https://img.shields.io/pypi/v/jsonrpclib-pelix.svg)](https://pypi.python.org/pypi/jsonrpclib-pelix/) + [![License](https://img.shields.io/pypi/l/jsonrpclib-pelix.svg)](https://pypi.python.org/pypi/jsonrpclib-pelix/) + [![Travis-CI status](https://travis-ci.org/tcalmant/jsonrpclib.svg?branch=master)](https://travis-ci.org/tcalmant/jsonrpclib) + [![Coveralls status](https://coveralls.io/repos/tcalmant/jsonrpclib/badge.svg?branch=master)](https://coveralls.io/r/tcalmant/jsonrpclib?branch=master) This library is an implementation of the JSON-RPC specification. - It supports both the original 1.0 specification, as well as the - new (proposed) 2.0 specification, which includes batch submission, keyword + It supports both the original 1.0 specification, as well as the new + (proposed) 2.0 specification, which includes batch submission, keyword arguments, etc. - It is licensed under the Apache License, Version 2.0 - (http://www.apache.org/licenses/LICENSE-2.0.html). + This library is licensed under the terms of the + [Apache Software License 2.0](). - About this version - ****************** + ## About this version - This is a patched version of the original ``jsonrpclib`` project by - Josh Marshall, available at https://github.com/joshmarshall/jsonrpclib. + This is a patched version of the original `jsonrpclib` project by Josh Marshall, + available at + [joshmarshall/jsonrpclib](). - The suffix *-pelix* only indicates that this version works with Pelix Remote - Services, but it is **not** a Pelix specific implementation. + The suffix *-pelix* only indicates that this version works with Pelix + Remote Services, but it is **not** a Pelix specific implementation. * This version adds support for Python 3, staying compatible with Python 2.7. - * It is now possible to use the dispatch_method argument while extending - the SimpleJSONRPCDispatcher, to use a custom dispatcher. + The support for Python 2.6 has been dropped, as it was becoming to hard to + maintain. + * It is now possible to use the `dispatch_method` argument while extending the + `SimpleJSONRPCDispatcher`, to use a custom dispatcher. This allows to use this package by Pelix Remote Services. * It can use thread pools to control the number of threads spawned to handle notification requests and clients connections. * The modifications added in other forks of this project have been added: + * From [drdaeman/jsonrpclib](): + * Improved JSON-RPC 1.0 support + * Less strict error response handling + * From [tuomassalo/jsonrpclib](): + * In case of a non-predefined error, raise an AppError and give access + to *error.data* + * From [dejw/jsonrpclib](): + * Custom headers can be sent with request and associated tests + * Since version 0.4, this package added back the support of Unix sockets. + * This package cannot be installed with the original `jsonrpclib`, as it uses + the same name. - * From https://github.com/drdaeman/jsonrpclib: - - * Improved JSON-RPC 1.0 support - * Less strict error response handling - - * From https://github.com/tuomassalo/jsonrpclib: - - * In case of a non-pre-defined error, raise an AppError and give access to - *error.data* - - * From https://github.com/dejw/jsonrpclib: - - * Custom headers can be sent with request and associated tests - - * The support for Unix sockets has been removed, as it is not trivial to convert - to Python 3 (and I don't use them) - * This version cannot be installed with the original ``jsonrpclib``, as it uses - the same package name. - - - Summary - ******* + ## Summary This library implements the JSON-RPC 2.0 proposed specification in pure Python. - It is designed to be as compatible with the syntax of ``xmlrpclib`` as possible - (it extends where possible), so that projects using ``xmlrpclib`` could easily - be modified to use JSON and experiment with the differences. + It is designed to be as compatible with the syntax of `xmlrpclib` as possible + (it extends where possible), so that projects using `xmlrpclib` could easily be + modified to use JSON and experiment with the differences. It is backwards-compatible with the 1.0 specification, and supports all of the new proposed features of 2.0, including: - * Batch submission (via MultiCall) - * Keyword arguments - * Notifications (both in a batch and 'normal') - * Class translation using the ``__jsonclass__`` key. - - I've added a "SimpleJSONRPCServer", which is intended to emulate the - "SimpleXMLRPCServer" from the default Python distribution. - - - Requirements - ************ - - It supports ``cjson`` and ``simplejson``, and looks for the parsers in that - order (searching first for ``cjson``, then for the *built-in* ``json`` in 2.7, - and then the ``simplejson`` external library). + - Batch submission (via the `MultiCall` class) + - Keyword arguments + - Notifications (both in a batch and 'normal') + - Class translation using the `__jsonclass__` key. + + A `SimpleJSONRPCServer` class has been added. It is intended to emulate the + `SimpleXMLRPCServer` from the default Python distribution. + + ## Requirements + + This library supports `cjson` and `simplejson`, and looks for the parsers in + that order (searching first for `cjson`, then for the *built-in* `json` in 2.7+, + and then the `simplejson` external library). One of these must be installed to use this library, although if you have a - standard distribution of 2.7, you should already have one. - Keep in mind that ``cjson`` is supposed to be the quickest, I believe, so if - you are going for full-on optimization you may want to pick it up. - - - Installation - ************ - - You can install this from PyPI with one of the following commands (sudo - may be required): - - .. code-block:: console - - easy_install jsonrpclib-pelix - pip install jsonrpclib-pelix - - Alternatively, you can download the source from the GitHub repository - at http://github.com/tcalmant/jsonrpclib and manually install it - with the following commands: - - .. code-block:: console - - git clone git://github.com/tcalmant/jsonrpclib.git - cd jsonrpclib - python setup.py install - - - SimpleJSONRPCServer - ******************* - - This is identical in usage (or should be) to the SimpleXMLRPCServer in the - Python standard library. Some of the differences in features are that it - obviously supports notification, batch calls, class translation (if left on), - etc. - Note: The import line is slightly different from the regular SimpleXMLRPCServer, - since the SimpleJSONRPCServer is distributed within the ``jsonrpclib`` library. - - .. code-block:: python - - from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer - - server = SimpleJSONRPCServer(('localhost', 8080)) - server.register_function(pow) - server.register_function(lambda x,y: x+y, 'add') - server.register_function(lambda x: x, 'ping') - server.serve_forever() + standard distribution of 2.7+, you should already have one. + Keep in mind that `cjson` is supposed to be the quickest, I believe, so if you + are going for full-on optimization you may want to pick it up. + + ## Installation + + You can install this from PyPI with one of the following commands (`sudo` + might be required): + + ``` + # Global installation + pip install jsonrpclib-pelix + + # Local installation + pip install --user jsonrpclib-pelix + ``` + + Alternatively, you can download the source from the GitHub repository at + [tcalmant/jsonrpclib](http://github.com/tcalmant/jsonrpclib) and manually + install it with the following commands: + + ``` + git clone git://github.com/tcalmant/jsonrpclib.git + cd jsonrpclib + python setup.py install + ``` + + ## A note on logging + + `jsonrpclib-pelix` uses the `logging` module from the standard Python + library to trace warnings and errors, but doesn't set it up. + As a result, you have to configure the Python logging to print out traces. + + The easiest way to do it is to add those lines at the beginning of your code: + ```python + import logging + logging.basiConfig() + ``` + + More information can be found in the + [`logging` documentation page](https://docs.python.org/3/library/logging.html). + + ## `SimpleJSONRPCServer` + + This is identical in usage (or should be) to the `SimpleXMLRPCServer` in the + Python standard library. + Some of the differences in features are that it obviously supports notification, + batch calls, class translation (if left on), etc. + + **Note:** The import line is slightly different from the regular + `SimpleXMLRPCServer`, since the `SimpleJSONRPCServer` is provided by th + `jsonrpclib` library. + + ```python + from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer + + server = SimpleJSONRPCServer(('localhost', 8080)) + server.register_function(pow) + server.register_function(lambda x,y: x+y, 'add') + server.register_function(lambda x: x, 'ping') + server.serve_forever() + ``` To start protect the server with SSL, use the following snippet: - .. code-block:: python - - from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer - - # Setup the SSL socket - server = SimpleJSONRPCServer(('localhost', 8080), bind_and_activate=False) - server.socket = ssl.wrap_socket(server.socket, certfile='server.pem', - server_side=True) - server.server_bind() - server.server_activate() + ```python + from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer + import ssl + + # Setup the SSL socket + server = SimpleJSONRPCServer(('localhost', 8080), bind_and_activate=False) + server.socket = ssl.wrap_socket(server.socket, certfile='server.pem', + server_side=True) + server.server_bind() + server.server_activate() + + # ... register functions + # Start the server + server.serve_forever() + ``` - # ... register functions - # Start the server - server.serve_forever() - - - Notification Thread Pool - ======================== + ### Notification Thread Pool By default, notification calls are handled in the request handling thread. It is possible to use a thread pool to handle them, by giving it to the server - using the ``set_notification_pool()`` method: - - .. code-block:: python - - from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer - from jsonrpclib.threadpool import ThreadPool - - # Setup the thread pool: between 0 and 10 threads - pool = ThreadPool(max_threads=10, min_threads=0) - - # Don't forget to start it - pool.start() - - # Setup the server - server = SimpleJSONRPCServer(('localhost', 8080), config) - server.set_notification_pool(pool) + using the `set_notification_pool()` method: - # Register methods - server.register_function(pow) - server.register_function(lambda x,y: x+y, 'add') - server.register_function(lambda x: x, 'ping') + ```python + from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer + from jsonrpclib.threadpool import ThreadPool + + # Setup the thread pool: between 0 and 10 threads + pool = ThreadPool(max_threads=10, min_threads=0) + + # Don't forget to start it + pool.start() + + # Setup the server + server = SimpleJSONRPCServer(('localhost', 8080)) + server.set_notification_pool(pool) + + # Register methods + server.register_function(pow) + server.register_function(lambda x,y: x+y, 'add') + server.register_function(lambda x: x, 'ping') + + try: + server.serve_forever() + finally: + # Stop the thread pool (let threads finish their current task) + pool.stop() + server.set_notification_pool(None) + ``` - try: - server.serve_forever() - finally: - # Stop the thread pool (let threads finish their current task) - pool.stop() - server.set_notification_pool(None) - - - Threaded server - =============== + ### Threaded server It is also possible to use a thread pool to handle clients requests, using the - ``PooledJSONRPCServer`` class. - By default, this class uses pool of 0 to 30 threads. A custom pool can be given - with the ``thread_pool`` parameter of the class constructor. + `PooledJSONRPCServer` class. + By default, this class uses pool of 0 to 30 threads. + A custom pool can be given with the `thread_pool` parameter of the class + constructor. The notification pool and the request pool are different: by default, a server with a request pool doesn't have a notification pool. - .. code-block:: python + ```python + from jsonrpclib.SimpleJSONRPCServer import PooledJSONRPCServer + from jsonrpclib.threadpool import ThreadPool + + # Setup the notification and request pools + nofif_pool = ThreadPool(max_threads=10, min_threads=0) + request_pool = ThreadPool(max_threads=50, min_threads=10) + + # Don't forget to start them + nofif_pool.start() + request_pool.start() + + # Setup the server + server = PooledJSONRPCServer(('localhost', 8080), thread_pool=request_pool) + server.set_notification_pool(nofif_pool) + + # Register methods + server.register_function(pow) + server.register_function(lambda x,y: x+y, 'add') + server.register_function(lambda x: x, 'ping') + + try: + server.serve_forever() + finally: + # Stop the thread pools (let threads finish their current task) + request_pool.stop() + nofif_pool.stop() + server.set_notification_pool(None) + ``` + + ### Unix socket + + To start a server listening on a Unix socket, you will have to use the + following snippet: + + ```python + from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer + import os + import socket + + # Set the path to the socket file + socket_name = "/tmp/my_socket.socket" + + # Ensure that the file doesn't exist yet (or an error will be raised) + if os.path.exists(socket_name): + os.remove(socket_name) + + try: + # Start the server, indicating the socket family + # The server will force some flags when in Unix socket mode + # (no log request, no reuse address, ...) + srv = SimpleJSONRPCServer(socket_name, address_family=socket.AF_UNIX) + + # ... register methods to the server + # Run the server + srv.serve_forever() + except KeyboardInterrupt: + # Shutdown the server gracefully + srv.shutdown() + srv.server_close() + finally: + # You should clean up after the server stopped + os.remove(socket_name) + ``` - from jsonrpclib.SimpleJSONRPCServer import PooledJSONRPCServer - from jsonrpclib.threadpool import ThreadPool + This feature is tested on Linux during Travis-CI builds. It also has + been tested on Windows Subsystem for Linux (WSL) on Windows 10 1809. - # Setup the notification and request pools - nofif_pool = ThreadPool(max_threads=10, min_threads=0) - request_pool = ThreadPool(max_threads=50, min_threads=10) - - # Don't forget to start them - nofif_pool.start() - request_pool.start() - - # Setup the server - server = PooledJSONRPCServer(('localhost', 8080), config, - thread_pool=request_pool) - server.set_notification_pool(nofif_pool) - - # Register methods - server.register_function(pow) - server.register_function(lambda x,y: x+y, 'add') - server.register_function(lambda x: x, 'ping') - - try: - server.serve_forever() - finally: - # Stop the thread pools (let threads finish their current task) - request_pool.stop() - nofif_pool.stop() - server.set_notification_pool(None) + This feature is not available on "pure" Windows, as it doesn't provide + the `AF_UNIX` address family. - Client Usage - ************ + ## Client Usage This is (obviously) taken from a console session. - .. code-block:: python - - >>> import jsonrpclib - >>> server = jsonrpclib.ServerProxy('http://localhost:8080') - >>> server.add(5,6) - 11 - >>> server.add(x=5, y=10) - 15 - >>> server._notify.add(5,6) - # No result returned... - >>> batch = jsonrpclib.MultiCall(server) - >>> batch.add(5, 6) - >>> batch.ping({'key':'value'}) - >>> batch._notify.add(4, 30) - >>> results = batch() - >>> for result in results: - >>> ... print(result) - 11 - {'key': 'value'} - # Note that there are only two responses -- this is according to spec. - - # Clean up - >>> server('close')() - - # Using client history - >>> history = jsonrpclib.history.History() - >>> server = jsonrpclib.ServerProxy('http://localhost:8080', history=history) - >>> server.add(5,6) - 11 - >>> print(history.request) - {"id": "f682b956-c8e1-4506-9db4-29fe8bc9fcaa", "jsonrpc": "2.0", - "method": "add", "params": [5, 6]} - >>> print(history.response) - {"id": "f682b956-c8e1-4506-9db4-29fe8bc9fcaa", "jsonrpc": "2.0", - "result": 11} - - # Clean up - >>> server('close')() - - If you need 1.0 functionality, there are a bunch of places you can pass that in, - although the best is just to give a specific configuration to - ``jsonrpclib.ServerProxy``: - - .. code-block:: python - - >>> import jsonrpclib - >>> jsonrpclib.config.DEFAULT.version - 2.0 - >>> config = jsonrpclib.config.Config(version=1.0) - >>> history = jsonrpclib.history.History() - >>> server = jsonrpclib.ServerProxy('http://localhost:8080', config=config, - history=history) - >>> server.add(7, 10) - 17 - >>> print(history.request) - {"id": "827b2923-5b37-49a5-8b36-e73920a16d32", - "method": "add", "params": [7, 10]} - >>> print(history.response) - {"id": "827b2923-5b37-49a5-8b36-e73920a16d32", "error": null, "result": 17} - >>> server('close')() - - The equivalent ``loads`` and ``dumps`` functions also exist, although with minor - modifications. The ``dumps`` arguments are almost identical, but it adds three - arguments: ``rpcid`` for the 'id' key, ``version`` to specify the JSON-RPC - compatibility, and ``notify`` if it's a request that you want to be a - notification. - - Additionally, the ``loads`` method does not return the params and method like - ``xmlrpclib``, but instead a.) parses for errors, raising ProtocolErrors, and + ```python + >>> import jsonrpclib + >>> server = jsonrpclib.ServerProxy('http://localhost:8080') + >>> server.add(5,6) + 11 + >>> server.add(x=5, y=10) + 15 + >>> server._notify.add(5,6) + # No result returned... + >>> batch = jsonrpclib.MultiCall(server) + >>> batch.add(5, 6) + >>> batch.ping({'key':'value'}) + >>> batch._notify.add(4, 30) + >>> results = batch() + >>> for result in results: + >>> ... print(result) + 11 + {'key': 'value'} + # Note that there are only two responses -- this is according to spec. + + # Clean up + >>> server('close')() + + # Using client history + >>> history = jsonrpclib.history.History() + >>> server = jsonrpclib.ServerProxy('http://localhost:8080', history=history) + >>> server.add(5,6) + 11 + >>> print(history.request) + {"id": "f682b956-c8e1-4506-9db4-29fe8bc9fcaa", "jsonrpc": "2.0", + "method": "add", "params": [5, 6]} + >>> print(history.response) + {"id": "f682b956-c8e1-4506-9db4-29fe8bc9fcaa", "jsonrpc": "2.0", + "result": 11} + + # Clean up + >>> server('close')() + ``` + + If you need 1.0 functionality, there are a bunch of places you can pass + that in, although the best is just to give a specific configuration to + `jsonrpclib.ServerProxy`: + + ```python + >>> import jsonrpclib + >>> jsonrpclib.config.DEFAULT.version + 2.0 + >>> config = jsonrpclib.config.Config(version=1.0) + >>> history = jsonrpclib.history.History() + >>> server = jsonrpclib.ServerProxy('http://localhost:8080', config=config, + history=history) + >>> server.add(7, 10) + 17 + >>> print(history.request) + {"id": "827b2923-5b37-49a5-8b36-e73920a16d32", + "method": "add", "params": [7, 10]} + >>> print(history.response) + {"id": "827b2923-5b37-49a5-8b36-e73920a16d32", "error": null, "result": 17} + >>> server('close')() + ``` + + The equivalent `loads` and `dumps` functions also exist, although with + minor modifications. + The `dumps` arguments are almost identical, but it adds three arguments: + `rpcid` for the `id` key, `version` to specify the JSON-RPC compatibility, + and `notify` if it's a request that you want to be a notification. + + Additionally, the `loads` method does not return the params and method like + `xmlrpclib`, but instead + a.) parses for errors, raising ProtocolErrors, and b.) returns the entire structure of the request / response for manual parsing. + ### Unix sockets - Additional headers - ****************** - - If your remote service requires custom headers in request, you can pass them - as as a ``headers`` keyword argument, when creating the ``ServerProxy``: - - .. code-block:: python - - >>> import jsonrpclib - >>> server = jsonrpclib.ServerProxy("http://localhost:8080", - headers={'X-Test' : 'Test'}) - - You can also put additional request headers only for certain method invocation: - - .. code-block:: python - - >>> import jsonrpclib - >>> server = jsonrpclib.Server("http://localhost:8080") - >>> with server._additional_headers({'X-Test' : 'Test'}) as test_server: - ... test_server.ping(42) - ... - >>> # X-Test header will be no longer sent in requests + To connect a JSON-RPC server over a Unix socket, you have to use a specific + protocol: `unix+http`. - Of course ``_additional_headers`` contexts can be nested as well. + When connecting to a Unix socket in the current working directory, you can use + the following syntax: `unix+http://my.socket` + When you need to give an absolute path you must use the path part of the URL, + the host part will be ignored. For example, you can use this URL to indicate a + Unix socket in `/var/lib/daemon.socket`: `unix+http://./var/lib/daemon.socket` - Class Translation - ***************** + **Note:** Currently, only HTTP is supported over a Unix socket. + If you want HTTPS support to be implemented, please create an + [issue on GitHub](https://github.com/tcalmant/jsonrpclib/issues) - I've recently added "automatic" class translation support, although it is - turned off by default. This can be devastatingly slow if improperly used, so - the following is just a short list of things to keep in mind when using it. + ### Additional headers - * Keep It (the object) Simple Stupid. (for exceptions, keep reading.) - * Do not require init params (for exceptions, keep reading) - * Getter properties without setters could be dangerous (read: not tested) + If your remote service requires custom headers in request, you can pass them + using the `headers` keyword argument, when creating the `ServerProxy`: - If any of the above are issues, use the _serialize method. (see usage below) - The server and client must BOTH have use_jsonclass configuration item on and - they must both have access to the same libraries used by the objects for - this to work. + ```python + >>> import jsonrpclib + >>> server = jsonrpclib.ServerProxy("http://localhost:8080", + headers={'X-Test' : 'Test'}) + ``` + + You can also put additional request headers only for certain method + invocation: + + ```python + >>> import jsonrpclib + >>> server = jsonrpclib.Server("http://localhost:8080") + >>> with server._additional_headers({'X-Test' : 'Test'}) as test_server: + ... test_server.ping(42) + ... + >>> # X-Test header will be no longer sent in requests + ``` + + Of course `_additional_headers` contexts can be nested as well. + + ## Class Translation + + The library supports an *"automatic"* class translation process, although it + is turned off by default. + This can be devastatingly slow if improperly used, so the following is just a + short list of things to keep in mind when using it. + + - Keep It (the object) Simple Stupid. (for exceptions, keep reading) + - Do not require init params (for exceptions, keep reading) + - Getter properties without setters could be dangerous (read: not tested) + + If any of the above are issues, use the `_serialize` method (see usage below). + The server and client must **BOTH** have the `use_jsonclass` configuration + item on and they must both have access to the same libraries used by the + objects for this to work. If you have excessively nested arguments, it would be better to turn off the translation and manually invoke it on specific objects using - ``jsonrpclib.jsonclass.dump`` / ``jsonrpclib.jsonclass.load`` (since the default - behavior recursively goes through attributes and lists / dicts / tuples). - - Sample file: *test_obj.py* - - .. code-block:: python + `jsonrpclib.jsonclass.dump` / `jsonrpclib.jsonclass.load` (since the + default behavior recursively goes through attributes and lists/dicts/tuples). - # This object is /very/ simple, and the system will look through the - # attributes and serialize what it can. - class TestObj(object): - foo = 'bar' - - # This object requires __init__ params, so it uses the _serialize method - # and returns a tuple of init params and attribute values (the init params - # can be a dict or a list, but the attribute values must be a dict.) - class TestSerial(object): - foo = 'bar' - def __init__(self, *args): - self.args = args - def _serialize(self): - return (self.args, {'foo':self.foo,}) - - * Sample usage - - .. code-block:: python - - >>> import jsonrpclib - >>> import test_obj - - # History is used only to print the serialized form of beans - >>> history = jsonrpclib.history.History() - >>> testobj1 = test_obj.TestObj() - >>> testobj2 = test_obj.TestSerial() - >>> server = jsonrpclib.Server('http://localhost:8080', history=history) - - # The 'ping' just returns whatever is sent - >>> ping1 = server.ping(testobj1) - >>> ping2 = server.ping(testobj2) - - >>> print(history.request) - {"id": "7805f1f9-9abd-49c6-81dc-dbd47229fe13", "jsonrpc": "2.0", - "method": "ping", "params": [{"__jsonclass__": - ["test_obj.TestSerial", []], "foo": "bar"} - ]} - >>> print(history.response) - {"id": "7805f1f9-9abd-49c6-81dc-dbd47229fe13", "jsonrpc": "2.0", - "result": {"__jsonclass__": ["test_obj.TestSerial", []], "foo": "bar"}} + * Sample file: `test_obj.py` - This behavior is turned by default. To deactivate it, just set the - ``use_jsonclass`` member of a server ``Config`` to False. + ```python + # This object is /very/ simple, and the system will look through the + # attributes and serialize what it can. + class TestObj(object): + foo = 'bar' + + # This object requires __init__ params, so it uses the _serialize method + # and returns a tuple of init params and attribute values (the init params + # can be a dict or a list, but the attribute values must be a dict.) + class TestSerial(object): + foo = 'bar' + def __init__(self, *args): + self.args = args + def _serialize(self): + return (self.args, {'foo':self.foo,}) + ``` + + - Sample usage: + + ```python + >>> import jsonrpclib + >>> import test_obj + + # History is used only to print the serialized form of beans + >>> history = jsonrpclib.history.History() + >>> testobj1 = test_obj.TestObj() + >>> testobj2 = test_obj.TestSerial() + >>> server = jsonrpclib.Server('http://localhost:8080', history=history) + + # The 'ping' just returns whatever is sent + >>> ping1 = server.ping(testobj1) + >>> ping2 = server.ping(testobj2) + + >>> print(history.request) + {"id": "7805f1f9-9abd-49c6-81dc-dbd47229fe13", "jsonrpc": "2.0", + "method": "ping", "params": [{"__jsonclass__": + ["test_obj.TestSerial", []], "foo": "bar"} + ]} + >>> print(history.response) + {"id": "7805f1f9-9abd-49c6-81dc-dbd47229fe13", "jsonrpc": "2.0", + "result": {"__jsonclass__": ["test_obj.TestSerial", []], "foo": "bar"}} + ``` + + This behavior is turned on by default. + To deactivate it, just set the `use_jsonclass` member of a server `Config` to + `False`. If you want to use a per-class serialization method, set its name in the - ``serialize_method`` member of a server ``Config``. + `serialize_method` member of a server `Config`. Finally, if you are using classes that you have defined in the implementation - (as in, not a separate library), you'll need to add those (on BOTH the server - and the client) using the ``config.classes.add()`` method. + (as in, not a separate library), you'll need to add those + (on **BOTH** the server and the client) using the `config.classes.add()` method. Feedback on this "feature" is very, VERY much appreciated. - Why JSON-RPC? - ************* + ## Tests + + Tests are an almost-verbatim drop from the JSON-RPC specification 2.0 + page. They can be run using *unittest* or *nosetest*: + + ``` + python -m unittest discover tests + python3 -m unittest discover tests + nosetests tests + ``` + + ## Why JSON-RPC? In my opinion, there are several reasons to choose JSON over XML for RPC: * Much simpler to read (I suppose this is opinion, but I know I'm right. :) - * Size / Bandwidth - Main reason, a JSON object representation is just much smaller. + * Size / Bandwidth - Main reason, a JSON object representation is just much + smaller. * Parsing - JSON should be much quicker to parse than XML. - * Easy class passing with ``jsonclass`` (when enabled) + * Easy class passing with `jsonclass` (when enabled) - In the interest of being fair, there are also a few reasons to choose XML - over JSON: + In the interest of being fair, there are also a few reasons to choose XML over + JSON: * Your server doesn't do JSON (rather obvious) * Wider XML-RPC support across APIs (can we change this? :)) - * Libraries are more established, i.e. more stable (Let's change this too.) - - Tests - ***** - - Tests are an almost-verbatim drop from the JSON-RPC specification 2.0 page. - They can be run using *unittest* or *nosetest*: - - .. code-block:: console - - python -m unittest discover tests - python3 -m unittest discover tests - nosetests tests + * Libraries are more established, *i.e.* more stable (Let's change this too) Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable @@ -459,3 +505,5 @@ Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Description-Content-Type: text/markdown diff -Nru jsonrpclib-pelix-0.3.1/jsonrpclib_pelix.egg-info/SOURCES.txt jsonrpclib-pelix-0.4.1/jsonrpclib_pelix.egg-info/SOURCES.txt --- jsonrpclib-pelix-0.3.1/jsonrpclib_pelix.egg-info/SOURCES.txt 2017-06-27 11:08:06.000000000 +0000 +++ jsonrpclib-pelix-0.4.1/jsonrpclib_pelix.egg-info/SOURCES.txt 2020-04-12 14:48:26.000000000 +0000 @@ -1,6 +1,6 @@ -LICENSE.txt +LICENSE MANIFEST.in -README.rst +README.md setup.cfg setup.py jsonrpclib/SimpleJSONRPCServer.py diff -Nru jsonrpclib-pelix-0.3.1/LICENSE jsonrpclib-pelix-0.4.1/LICENSE --- jsonrpclib-pelix-0.3.1/LICENSE 1970-01-01 00:00:00.000000000 +0000 +++ jsonrpclib-pelix-0.4.1/LICENSE 2018-06-15 21:52:39.000000000 +0000 @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff -Nru jsonrpclib-pelix-0.3.1/LICENSE.txt jsonrpclib-pelix-0.4.1/LICENSE.txt --- jsonrpclib-pelix-0.3.1/LICENSE.txt 2016-07-07 08:59:51.000000000 +0000 +++ jsonrpclib-pelix-0.4.1/LICENSE.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,11 +0,0 @@ -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff -Nru jsonrpclib-pelix-0.3.1/MANIFEST.in jsonrpclib-pelix-0.4.1/MANIFEST.in --- jsonrpclib-pelix-0.3.1/MANIFEST.in 2016-07-07 08:59:51.000000000 +0000 +++ jsonrpclib-pelix-0.4.1/MANIFEST.in 2019-01-04 16:29:14.000000000 +0000 @@ -1,2 +1,2 @@ -include *.txt -include README.rst +include LICENSE +include README.md diff -Nru jsonrpclib-pelix-0.3.1/PKG-INFO jsonrpclib-pelix-0.4.1/PKG-INFO --- jsonrpclib-pelix-0.3.1/PKG-INFO 2017-06-27 11:08:06.000000000 +0000 +++ jsonrpclib-pelix-0.4.1/PKG-INFO 2020-04-12 14:48:27.000000000 +0000 @@ -1,452 +1,498 @@ -Metadata-Version: 1.1 +Metadata-Version: 2.1 Name: jsonrpclib-pelix -Version: 0.3.1 -Summary: This project is an implementation of the JSON-RPC v2.0 specification (backwards-compatible) as a client library, for Python 2.7 and Python 3.This version is a fork of jsonrpclib by Josh Marshall, usable with Pelix remote services. +Version: 0.4.1 +Summary: This project is an implementation of the JSON-RPC v2.0 specification (backwards-compatible) as a client library, for Python 2.7 and Python 3. This version is a fork of jsonrpclib by Josh Marshall, made to be also usable with Pelix/iPOPO remote services. Home-page: http://github.com/tcalmant/jsonrpclib/ Author: Thomas Calmant Author-email: thomas.calmant+github@gmail.com License: Apache License 2.0 -Description: JSONRPClib (patched for Pelix and Python 3) - ########################################### +Description: # JSONRPClib (patched for Pelix and Python 3) - .. image:: https://img.shields.io/pypi/v/jsonrpclib-pelix.svg - :target: https://pypi.python.org/pypi/jsonrpclib-pelix/ - :alt: Latest Version - - .. image:: https://img.shields.io/pypi/l/jsonrpclib-pelix.svg - :target: https://pypi.python.org/pypi/jsonrpclib-pelix/ - :alt: License - - .. image:: https://travis-ci.org/tcalmant/jsonrpclib.svg?branch=master - :target: https://travis-ci.org/tcalmant/jsonrpclib - :alt: Travis-CI status - - .. image:: https://coveralls.io/repos/tcalmant/jsonrpclib/badge.svg?branch=master - :target: https://coveralls.io/r/tcalmant/jsonrpclib?branch=master - :alt: Coveralls status + [![Latest Version](https://img.shields.io/pypi/v/jsonrpclib-pelix.svg)](https://pypi.python.org/pypi/jsonrpclib-pelix/) + [![License](https://img.shields.io/pypi/l/jsonrpclib-pelix.svg)](https://pypi.python.org/pypi/jsonrpclib-pelix/) + [![Travis-CI status](https://travis-ci.org/tcalmant/jsonrpclib.svg?branch=master)](https://travis-ci.org/tcalmant/jsonrpclib) + [![Coveralls status](https://coveralls.io/repos/tcalmant/jsonrpclib/badge.svg?branch=master)](https://coveralls.io/r/tcalmant/jsonrpclib?branch=master) This library is an implementation of the JSON-RPC specification. - It supports both the original 1.0 specification, as well as the - new (proposed) 2.0 specification, which includes batch submission, keyword + It supports both the original 1.0 specification, as well as the new + (proposed) 2.0 specification, which includes batch submission, keyword arguments, etc. - It is licensed under the Apache License, Version 2.0 - (http://www.apache.org/licenses/LICENSE-2.0.html). + This library is licensed under the terms of the + [Apache Software License 2.0](). - About this version - ****************** + ## About this version - This is a patched version of the original ``jsonrpclib`` project by - Josh Marshall, available at https://github.com/joshmarshall/jsonrpclib. + This is a patched version of the original `jsonrpclib` project by Josh Marshall, + available at + [joshmarshall/jsonrpclib](). - The suffix *-pelix* only indicates that this version works with Pelix Remote - Services, but it is **not** a Pelix specific implementation. + The suffix *-pelix* only indicates that this version works with Pelix + Remote Services, but it is **not** a Pelix specific implementation. * This version adds support for Python 3, staying compatible with Python 2.7. - * It is now possible to use the dispatch_method argument while extending - the SimpleJSONRPCDispatcher, to use a custom dispatcher. + The support for Python 2.6 has been dropped, as it was becoming to hard to + maintain. + * It is now possible to use the `dispatch_method` argument while extending the + `SimpleJSONRPCDispatcher`, to use a custom dispatcher. This allows to use this package by Pelix Remote Services. * It can use thread pools to control the number of threads spawned to handle notification requests and clients connections. * The modifications added in other forks of this project have been added: + * From [drdaeman/jsonrpclib](): + * Improved JSON-RPC 1.0 support + * Less strict error response handling + * From [tuomassalo/jsonrpclib](): + * In case of a non-predefined error, raise an AppError and give access + to *error.data* + * From [dejw/jsonrpclib](): + * Custom headers can be sent with request and associated tests + * Since version 0.4, this package added back the support of Unix sockets. + * This package cannot be installed with the original `jsonrpclib`, as it uses + the same name. - * From https://github.com/drdaeman/jsonrpclib: - - * Improved JSON-RPC 1.0 support - * Less strict error response handling - - * From https://github.com/tuomassalo/jsonrpclib: - - * In case of a non-pre-defined error, raise an AppError and give access to - *error.data* - - * From https://github.com/dejw/jsonrpclib: - - * Custom headers can be sent with request and associated tests - - * The support for Unix sockets has been removed, as it is not trivial to convert - to Python 3 (and I don't use them) - * This version cannot be installed with the original ``jsonrpclib``, as it uses - the same package name. - - - Summary - ******* + ## Summary This library implements the JSON-RPC 2.0 proposed specification in pure Python. - It is designed to be as compatible with the syntax of ``xmlrpclib`` as possible - (it extends where possible), so that projects using ``xmlrpclib`` could easily - be modified to use JSON and experiment with the differences. + It is designed to be as compatible with the syntax of `xmlrpclib` as possible + (it extends where possible), so that projects using `xmlrpclib` could easily be + modified to use JSON and experiment with the differences. It is backwards-compatible with the 1.0 specification, and supports all of the new proposed features of 2.0, including: - * Batch submission (via MultiCall) - * Keyword arguments - * Notifications (both in a batch and 'normal') - * Class translation using the ``__jsonclass__`` key. - - I've added a "SimpleJSONRPCServer", which is intended to emulate the - "SimpleXMLRPCServer" from the default Python distribution. - - - Requirements - ************ - - It supports ``cjson`` and ``simplejson``, and looks for the parsers in that - order (searching first for ``cjson``, then for the *built-in* ``json`` in 2.7, - and then the ``simplejson`` external library). + - Batch submission (via the `MultiCall` class) + - Keyword arguments + - Notifications (both in a batch and 'normal') + - Class translation using the `__jsonclass__` key. + + A `SimpleJSONRPCServer` class has been added. It is intended to emulate the + `SimpleXMLRPCServer` from the default Python distribution. + + ## Requirements + + This library supports `cjson` and `simplejson`, and looks for the parsers in + that order (searching first for `cjson`, then for the *built-in* `json` in 2.7+, + and then the `simplejson` external library). One of these must be installed to use this library, although if you have a - standard distribution of 2.7, you should already have one. - Keep in mind that ``cjson`` is supposed to be the quickest, I believe, so if - you are going for full-on optimization you may want to pick it up. - - - Installation - ************ - - You can install this from PyPI with one of the following commands (sudo - may be required): - - .. code-block:: console - - easy_install jsonrpclib-pelix - pip install jsonrpclib-pelix - - Alternatively, you can download the source from the GitHub repository - at http://github.com/tcalmant/jsonrpclib and manually install it - with the following commands: - - .. code-block:: console - - git clone git://github.com/tcalmant/jsonrpclib.git - cd jsonrpclib - python setup.py install - - - SimpleJSONRPCServer - ******************* - - This is identical in usage (or should be) to the SimpleXMLRPCServer in the - Python standard library. Some of the differences in features are that it - obviously supports notification, batch calls, class translation (if left on), - etc. - Note: The import line is slightly different from the regular SimpleXMLRPCServer, - since the SimpleJSONRPCServer is distributed within the ``jsonrpclib`` library. - - .. code-block:: python - - from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer - - server = SimpleJSONRPCServer(('localhost', 8080)) - server.register_function(pow) - server.register_function(lambda x,y: x+y, 'add') - server.register_function(lambda x: x, 'ping') - server.serve_forever() + standard distribution of 2.7+, you should already have one. + Keep in mind that `cjson` is supposed to be the quickest, I believe, so if you + are going for full-on optimization you may want to pick it up. + + ## Installation + + You can install this from PyPI with one of the following commands (`sudo` + might be required): + + ``` + # Global installation + pip install jsonrpclib-pelix + + # Local installation + pip install --user jsonrpclib-pelix + ``` + + Alternatively, you can download the source from the GitHub repository at + [tcalmant/jsonrpclib](http://github.com/tcalmant/jsonrpclib) and manually + install it with the following commands: + + ``` + git clone git://github.com/tcalmant/jsonrpclib.git + cd jsonrpclib + python setup.py install + ``` + + ## A note on logging + + `jsonrpclib-pelix` uses the `logging` module from the standard Python + library to trace warnings and errors, but doesn't set it up. + As a result, you have to configure the Python logging to print out traces. + + The easiest way to do it is to add those lines at the beginning of your code: + ```python + import logging + logging.basiConfig() + ``` + + More information can be found in the + [`logging` documentation page](https://docs.python.org/3/library/logging.html). + + ## `SimpleJSONRPCServer` + + This is identical in usage (or should be) to the `SimpleXMLRPCServer` in the + Python standard library. + Some of the differences in features are that it obviously supports notification, + batch calls, class translation (if left on), etc. + + **Note:** The import line is slightly different from the regular + `SimpleXMLRPCServer`, since the `SimpleJSONRPCServer` is provided by th + `jsonrpclib` library. + + ```python + from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer + + server = SimpleJSONRPCServer(('localhost', 8080)) + server.register_function(pow) + server.register_function(lambda x,y: x+y, 'add') + server.register_function(lambda x: x, 'ping') + server.serve_forever() + ``` To start protect the server with SSL, use the following snippet: - .. code-block:: python - - from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer - - # Setup the SSL socket - server = SimpleJSONRPCServer(('localhost', 8080), bind_and_activate=False) - server.socket = ssl.wrap_socket(server.socket, certfile='server.pem', - server_side=True) - server.server_bind() - server.server_activate() + ```python + from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer + import ssl + + # Setup the SSL socket + server = SimpleJSONRPCServer(('localhost', 8080), bind_and_activate=False) + server.socket = ssl.wrap_socket(server.socket, certfile='server.pem', + server_side=True) + server.server_bind() + server.server_activate() + + # ... register functions + # Start the server + server.serve_forever() + ``` - # ... register functions - # Start the server - server.serve_forever() - - - Notification Thread Pool - ======================== + ### Notification Thread Pool By default, notification calls are handled in the request handling thread. It is possible to use a thread pool to handle them, by giving it to the server - using the ``set_notification_pool()`` method: - - .. code-block:: python - - from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer - from jsonrpclib.threadpool import ThreadPool - - # Setup the thread pool: between 0 and 10 threads - pool = ThreadPool(max_threads=10, min_threads=0) - - # Don't forget to start it - pool.start() - - # Setup the server - server = SimpleJSONRPCServer(('localhost', 8080), config) - server.set_notification_pool(pool) + using the `set_notification_pool()` method: - # Register methods - server.register_function(pow) - server.register_function(lambda x,y: x+y, 'add') - server.register_function(lambda x: x, 'ping') + ```python + from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer + from jsonrpclib.threadpool import ThreadPool + + # Setup the thread pool: between 0 and 10 threads + pool = ThreadPool(max_threads=10, min_threads=0) + + # Don't forget to start it + pool.start() + + # Setup the server + server = SimpleJSONRPCServer(('localhost', 8080)) + server.set_notification_pool(pool) + + # Register methods + server.register_function(pow) + server.register_function(lambda x,y: x+y, 'add') + server.register_function(lambda x: x, 'ping') + + try: + server.serve_forever() + finally: + # Stop the thread pool (let threads finish their current task) + pool.stop() + server.set_notification_pool(None) + ``` - try: - server.serve_forever() - finally: - # Stop the thread pool (let threads finish their current task) - pool.stop() - server.set_notification_pool(None) - - - Threaded server - =============== + ### Threaded server It is also possible to use a thread pool to handle clients requests, using the - ``PooledJSONRPCServer`` class. - By default, this class uses pool of 0 to 30 threads. A custom pool can be given - with the ``thread_pool`` parameter of the class constructor. + `PooledJSONRPCServer` class. + By default, this class uses pool of 0 to 30 threads. + A custom pool can be given with the `thread_pool` parameter of the class + constructor. The notification pool and the request pool are different: by default, a server with a request pool doesn't have a notification pool. - .. code-block:: python + ```python + from jsonrpclib.SimpleJSONRPCServer import PooledJSONRPCServer + from jsonrpclib.threadpool import ThreadPool + + # Setup the notification and request pools + nofif_pool = ThreadPool(max_threads=10, min_threads=0) + request_pool = ThreadPool(max_threads=50, min_threads=10) + + # Don't forget to start them + nofif_pool.start() + request_pool.start() + + # Setup the server + server = PooledJSONRPCServer(('localhost', 8080), thread_pool=request_pool) + server.set_notification_pool(nofif_pool) + + # Register methods + server.register_function(pow) + server.register_function(lambda x,y: x+y, 'add') + server.register_function(lambda x: x, 'ping') + + try: + server.serve_forever() + finally: + # Stop the thread pools (let threads finish their current task) + request_pool.stop() + nofif_pool.stop() + server.set_notification_pool(None) + ``` + + ### Unix socket + + To start a server listening on a Unix socket, you will have to use the + following snippet: + + ```python + from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer + import os + import socket + + # Set the path to the socket file + socket_name = "/tmp/my_socket.socket" + + # Ensure that the file doesn't exist yet (or an error will be raised) + if os.path.exists(socket_name): + os.remove(socket_name) + + try: + # Start the server, indicating the socket family + # The server will force some flags when in Unix socket mode + # (no log request, no reuse address, ...) + srv = SimpleJSONRPCServer(socket_name, address_family=socket.AF_UNIX) + + # ... register methods to the server + # Run the server + srv.serve_forever() + except KeyboardInterrupt: + # Shutdown the server gracefully + srv.shutdown() + srv.server_close() + finally: + # You should clean up after the server stopped + os.remove(socket_name) + ``` - from jsonrpclib.SimpleJSONRPCServer import PooledJSONRPCServer - from jsonrpclib.threadpool import ThreadPool + This feature is tested on Linux during Travis-CI builds. It also has + been tested on Windows Subsystem for Linux (WSL) on Windows 10 1809. - # Setup the notification and request pools - nofif_pool = ThreadPool(max_threads=10, min_threads=0) - request_pool = ThreadPool(max_threads=50, min_threads=10) - - # Don't forget to start them - nofif_pool.start() - request_pool.start() - - # Setup the server - server = PooledJSONRPCServer(('localhost', 8080), config, - thread_pool=request_pool) - server.set_notification_pool(nofif_pool) - - # Register methods - server.register_function(pow) - server.register_function(lambda x,y: x+y, 'add') - server.register_function(lambda x: x, 'ping') - - try: - server.serve_forever() - finally: - # Stop the thread pools (let threads finish their current task) - request_pool.stop() - nofif_pool.stop() - server.set_notification_pool(None) + This feature is not available on "pure" Windows, as it doesn't provide + the `AF_UNIX` address family. - Client Usage - ************ + ## Client Usage This is (obviously) taken from a console session. - .. code-block:: python - - >>> import jsonrpclib - >>> server = jsonrpclib.ServerProxy('http://localhost:8080') - >>> server.add(5,6) - 11 - >>> server.add(x=5, y=10) - 15 - >>> server._notify.add(5,6) - # No result returned... - >>> batch = jsonrpclib.MultiCall(server) - >>> batch.add(5, 6) - >>> batch.ping({'key':'value'}) - >>> batch._notify.add(4, 30) - >>> results = batch() - >>> for result in results: - >>> ... print(result) - 11 - {'key': 'value'} - # Note that there are only two responses -- this is according to spec. - - # Clean up - >>> server('close')() - - # Using client history - >>> history = jsonrpclib.history.History() - >>> server = jsonrpclib.ServerProxy('http://localhost:8080', history=history) - >>> server.add(5,6) - 11 - >>> print(history.request) - {"id": "f682b956-c8e1-4506-9db4-29fe8bc9fcaa", "jsonrpc": "2.0", - "method": "add", "params": [5, 6]} - >>> print(history.response) - {"id": "f682b956-c8e1-4506-9db4-29fe8bc9fcaa", "jsonrpc": "2.0", - "result": 11} - - # Clean up - >>> server('close')() - - If you need 1.0 functionality, there are a bunch of places you can pass that in, - although the best is just to give a specific configuration to - ``jsonrpclib.ServerProxy``: - - .. code-block:: python - - >>> import jsonrpclib - >>> jsonrpclib.config.DEFAULT.version - 2.0 - >>> config = jsonrpclib.config.Config(version=1.0) - >>> history = jsonrpclib.history.History() - >>> server = jsonrpclib.ServerProxy('http://localhost:8080', config=config, - history=history) - >>> server.add(7, 10) - 17 - >>> print(history.request) - {"id": "827b2923-5b37-49a5-8b36-e73920a16d32", - "method": "add", "params": [7, 10]} - >>> print(history.response) - {"id": "827b2923-5b37-49a5-8b36-e73920a16d32", "error": null, "result": 17} - >>> server('close')() - - The equivalent ``loads`` and ``dumps`` functions also exist, although with minor - modifications. The ``dumps`` arguments are almost identical, but it adds three - arguments: ``rpcid`` for the 'id' key, ``version`` to specify the JSON-RPC - compatibility, and ``notify`` if it's a request that you want to be a - notification. - - Additionally, the ``loads`` method does not return the params and method like - ``xmlrpclib``, but instead a.) parses for errors, raising ProtocolErrors, and + ```python + >>> import jsonrpclib + >>> server = jsonrpclib.ServerProxy('http://localhost:8080') + >>> server.add(5,6) + 11 + >>> server.add(x=5, y=10) + 15 + >>> server._notify.add(5,6) + # No result returned... + >>> batch = jsonrpclib.MultiCall(server) + >>> batch.add(5, 6) + >>> batch.ping({'key':'value'}) + >>> batch._notify.add(4, 30) + >>> results = batch() + >>> for result in results: + >>> ... print(result) + 11 + {'key': 'value'} + # Note that there are only two responses -- this is according to spec. + + # Clean up + >>> server('close')() + + # Using client history + >>> history = jsonrpclib.history.History() + >>> server = jsonrpclib.ServerProxy('http://localhost:8080', history=history) + >>> server.add(5,6) + 11 + >>> print(history.request) + {"id": "f682b956-c8e1-4506-9db4-29fe8bc9fcaa", "jsonrpc": "2.0", + "method": "add", "params": [5, 6]} + >>> print(history.response) + {"id": "f682b956-c8e1-4506-9db4-29fe8bc9fcaa", "jsonrpc": "2.0", + "result": 11} + + # Clean up + >>> server('close')() + ``` + + If you need 1.0 functionality, there are a bunch of places you can pass + that in, although the best is just to give a specific configuration to + `jsonrpclib.ServerProxy`: + + ```python + >>> import jsonrpclib + >>> jsonrpclib.config.DEFAULT.version + 2.0 + >>> config = jsonrpclib.config.Config(version=1.0) + >>> history = jsonrpclib.history.History() + >>> server = jsonrpclib.ServerProxy('http://localhost:8080', config=config, + history=history) + >>> server.add(7, 10) + 17 + >>> print(history.request) + {"id": "827b2923-5b37-49a5-8b36-e73920a16d32", + "method": "add", "params": [7, 10]} + >>> print(history.response) + {"id": "827b2923-5b37-49a5-8b36-e73920a16d32", "error": null, "result": 17} + >>> server('close')() + ``` + + The equivalent `loads` and `dumps` functions also exist, although with + minor modifications. + The `dumps` arguments are almost identical, but it adds three arguments: + `rpcid` for the `id` key, `version` to specify the JSON-RPC compatibility, + and `notify` if it's a request that you want to be a notification. + + Additionally, the `loads` method does not return the params and method like + `xmlrpclib`, but instead + a.) parses for errors, raising ProtocolErrors, and b.) returns the entire structure of the request / response for manual parsing. + ### Unix sockets - Additional headers - ****************** - - If your remote service requires custom headers in request, you can pass them - as as a ``headers`` keyword argument, when creating the ``ServerProxy``: - - .. code-block:: python - - >>> import jsonrpclib - >>> server = jsonrpclib.ServerProxy("http://localhost:8080", - headers={'X-Test' : 'Test'}) - - You can also put additional request headers only for certain method invocation: - - .. code-block:: python - - >>> import jsonrpclib - >>> server = jsonrpclib.Server("http://localhost:8080") - >>> with server._additional_headers({'X-Test' : 'Test'}) as test_server: - ... test_server.ping(42) - ... - >>> # X-Test header will be no longer sent in requests + To connect a JSON-RPC server over a Unix socket, you have to use a specific + protocol: `unix+http`. - Of course ``_additional_headers`` contexts can be nested as well. + When connecting to a Unix socket in the current working directory, you can use + the following syntax: `unix+http://my.socket` + When you need to give an absolute path you must use the path part of the URL, + the host part will be ignored. For example, you can use this URL to indicate a + Unix socket in `/var/lib/daemon.socket`: `unix+http://./var/lib/daemon.socket` - Class Translation - ***************** + **Note:** Currently, only HTTP is supported over a Unix socket. + If you want HTTPS support to be implemented, please create an + [issue on GitHub](https://github.com/tcalmant/jsonrpclib/issues) - I've recently added "automatic" class translation support, although it is - turned off by default. This can be devastatingly slow if improperly used, so - the following is just a short list of things to keep in mind when using it. + ### Additional headers - * Keep It (the object) Simple Stupid. (for exceptions, keep reading.) - * Do not require init params (for exceptions, keep reading) - * Getter properties without setters could be dangerous (read: not tested) + If your remote service requires custom headers in request, you can pass them + using the `headers` keyword argument, when creating the `ServerProxy`: - If any of the above are issues, use the _serialize method. (see usage below) - The server and client must BOTH have use_jsonclass configuration item on and - they must both have access to the same libraries used by the objects for - this to work. + ```python + >>> import jsonrpclib + >>> server = jsonrpclib.ServerProxy("http://localhost:8080", + headers={'X-Test' : 'Test'}) + ``` + + You can also put additional request headers only for certain method + invocation: + + ```python + >>> import jsonrpclib + >>> server = jsonrpclib.Server("http://localhost:8080") + >>> with server._additional_headers({'X-Test' : 'Test'}) as test_server: + ... test_server.ping(42) + ... + >>> # X-Test header will be no longer sent in requests + ``` + + Of course `_additional_headers` contexts can be nested as well. + + ## Class Translation + + The library supports an *"automatic"* class translation process, although it + is turned off by default. + This can be devastatingly slow if improperly used, so the following is just a + short list of things to keep in mind when using it. + + - Keep It (the object) Simple Stupid. (for exceptions, keep reading) + - Do not require init params (for exceptions, keep reading) + - Getter properties without setters could be dangerous (read: not tested) + + If any of the above are issues, use the `_serialize` method (see usage below). + The server and client must **BOTH** have the `use_jsonclass` configuration + item on and they must both have access to the same libraries used by the + objects for this to work. If you have excessively nested arguments, it would be better to turn off the translation and manually invoke it on specific objects using - ``jsonrpclib.jsonclass.dump`` / ``jsonrpclib.jsonclass.load`` (since the default - behavior recursively goes through attributes and lists / dicts / tuples). - - Sample file: *test_obj.py* - - .. code-block:: python + `jsonrpclib.jsonclass.dump` / `jsonrpclib.jsonclass.load` (since the + default behavior recursively goes through attributes and lists/dicts/tuples). - # This object is /very/ simple, and the system will look through the - # attributes and serialize what it can. - class TestObj(object): - foo = 'bar' - - # This object requires __init__ params, so it uses the _serialize method - # and returns a tuple of init params and attribute values (the init params - # can be a dict or a list, but the attribute values must be a dict.) - class TestSerial(object): - foo = 'bar' - def __init__(self, *args): - self.args = args - def _serialize(self): - return (self.args, {'foo':self.foo,}) - - * Sample usage - - .. code-block:: python - - >>> import jsonrpclib - >>> import test_obj - - # History is used only to print the serialized form of beans - >>> history = jsonrpclib.history.History() - >>> testobj1 = test_obj.TestObj() - >>> testobj2 = test_obj.TestSerial() - >>> server = jsonrpclib.Server('http://localhost:8080', history=history) - - # The 'ping' just returns whatever is sent - >>> ping1 = server.ping(testobj1) - >>> ping2 = server.ping(testobj2) - - >>> print(history.request) - {"id": "7805f1f9-9abd-49c6-81dc-dbd47229fe13", "jsonrpc": "2.0", - "method": "ping", "params": [{"__jsonclass__": - ["test_obj.TestSerial", []], "foo": "bar"} - ]} - >>> print(history.response) - {"id": "7805f1f9-9abd-49c6-81dc-dbd47229fe13", "jsonrpc": "2.0", - "result": {"__jsonclass__": ["test_obj.TestSerial", []], "foo": "bar"}} + * Sample file: `test_obj.py` - This behavior is turned by default. To deactivate it, just set the - ``use_jsonclass`` member of a server ``Config`` to False. + ```python + # This object is /very/ simple, and the system will look through the + # attributes and serialize what it can. + class TestObj(object): + foo = 'bar' + + # This object requires __init__ params, so it uses the _serialize method + # and returns a tuple of init params and attribute values (the init params + # can be a dict or a list, but the attribute values must be a dict.) + class TestSerial(object): + foo = 'bar' + def __init__(self, *args): + self.args = args + def _serialize(self): + return (self.args, {'foo':self.foo,}) + ``` + + - Sample usage: + + ```python + >>> import jsonrpclib + >>> import test_obj + + # History is used only to print the serialized form of beans + >>> history = jsonrpclib.history.History() + >>> testobj1 = test_obj.TestObj() + >>> testobj2 = test_obj.TestSerial() + >>> server = jsonrpclib.Server('http://localhost:8080', history=history) + + # The 'ping' just returns whatever is sent + >>> ping1 = server.ping(testobj1) + >>> ping2 = server.ping(testobj2) + + >>> print(history.request) + {"id": "7805f1f9-9abd-49c6-81dc-dbd47229fe13", "jsonrpc": "2.0", + "method": "ping", "params": [{"__jsonclass__": + ["test_obj.TestSerial", []], "foo": "bar"} + ]} + >>> print(history.response) + {"id": "7805f1f9-9abd-49c6-81dc-dbd47229fe13", "jsonrpc": "2.0", + "result": {"__jsonclass__": ["test_obj.TestSerial", []], "foo": "bar"}} + ``` + + This behavior is turned on by default. + To deactivate it, just set the `use_jsonclass` member of a server `Config` to + `False`. If you want to use a per-class serialization method, set its name in the - ``serialize_method`` member of a server ``Config``. + `serialize_method` member of a server `Config`. Finally, if you are using classes that you have defined in the implementation - (as in, not a separate library), you'll need to add those (on BOTH the server - and the client) using the ``config.classes.add()`` method. + (as in, not a separate library), you'll need to add those + (on **BOTH** the server and the client) using the `config.classes.add()` method. Feedback on this "feature" is very, VERY much appreciated. - Why JSON-RPC? - ************* + ## Tests + + Tests are an almost-verbatim drop from the JSON-RPC specification 2.0 + page. They can be run using *unittest* or *nosetest*: + + ``` + python -m unittest discover tests + python3 -m unittest discover tests + nosetests tests + ``` + + ## Why JSON-RPC? In my opinion, there are several reasons to choose JSON over XML for RPC: * Much simpler to read (I suppose this is opinion, but I know I'm right. :) - * Size / Bandwidth - Main reason, a JSON object representation is just much smaller. + * Size / Bandwidth - Main reason, a JSON object representation is just much + smaller. * Parsing - JSON should be much quicker to parse than XML. - * Easy class passing with ``jsonclass`` (when enabled) + * Easy class passing with `jsonclass` (when enabled) - In the interest of being fair, there are also a few reasons to choose XML - over JSON: + In the interest of being fair, there are also a few reasons to choose XML over + JSON: * Your server doesn't do JSON (rather obvious) * Wider XML-RPC support across APIs (can we change this? :)) - * Libraries are more established, i.e. more stable (Let's change this too.) - - Tests - ***** - - Tests are an almost-verbatim drop from the JSON-RPC specification 2.0 page. - They can be run using *unittest* or *nosetest*: - - .. code-block:: console - - python -m unittest discover tests - python3 -m unittest discover tests - nosetests tests + * Libraries are more established, *i.e.* more stable (Let's change this too) Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable @@ -459,3 +505,5 @@ Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Description-Content-Type: text/markdown diff -Nru jsonrpclib-pelix-0.3.1/README.md jsonrpclib-pelix-0.4.1/README.md --- jsonrpclib-pelix-0.3.1/README.md 1970-01-01 00:00:00.000000000 +0000 +++ jsonrpclib-pelix-0.4.1/README.md 2019-10-05 16:20:43.000000000 +0000 @@ -0,0 +1,487 @@ +# JSONRPClib (patched for Pelix and Python 3) + +[![Latest Version](https://img.shields.io/pypi/v/jsonrpclib-pelix.svg)](https://pypi.python.org/pypi/jsonrpclib-pelix/) +[![License](https://img.shields.io/pypi/l/jsonrpclib-pelix.svg)](https://pypi.python.org/pypi/jsonrpclib-pelix/) +[![Travis-CI status](https://travis-ci.org/tcalmant/jsonrpclib.svg?branch=master)](https://travis-ci.org/tcalmant/jsonrpclib) +[![Coveralls status](https://coveralls.io/repos/tcalmant/jsonrpclib/badge.svg?branch=master)](https://coveralls.io/r/tcalmant/jsonrpclib?branch=master) + +This library is an implementation of the JSON-RPC specification. +It supports both the original 1.0 specification, as well as the new +(proposed) 2.0 specification, which includes batch submission, keyword +arguments, etc. + +This library is licensed under the terms of the +[Apache Software License 2.0](). + + +## About this version + +This is a patched version of the original `jsonrpclib` project by Josh Marshall, +available at +[joshmarshall/jsonrpclib](). + +The suffix *-pelix* only indicates that this version works with Pelix +Remote Services, but it is **not** a Pelix specific implementation. + +* This version adds support for Python 3, staying compatible with Python 2.7. + The support for Python 2.6 has been dropped, as it was becoming to hard to + maintain. +* It is now possible to use the `dispatch_method` argument while extending the + `SimpleJSONRPCDispatcher`, to use a custom dispatcher. + This allows to use this package by Pelix Remote Services. +* It can use thread pools to control the number of threads spawned to handle + notification requests and clients connections. +* The modifications added in other forks of this project have been added: + * From [drdaeman/jsonrpclib](): + * Improved JSON-RPC 1.0 support + * Less strict error response handling + * From [tuomassalo/jsonrpclib](): + * In case of a non-predefined error, raise an AppError and give access + to *error.data* + * From [dejw/jsonrpclib](): + * Custom headers can be sent with request and associated tests +* Since version 0.4, this package added back the support of Unix sockets. +* This package cannot be installed with the original `jsonrpclib`, as it uses + the same name. + +## Summary + +This library implements the JSON-RPC 2.0 proposed specification in pure Python. +It is designed to be as compatible with the syntax of `xmlrpclib` as possible +(it extends where possible), so that projects using `xmlrpclib` could easily be +modified to use JSON and experiment with the differences. + +It is backwards-compatible with the 1.0 specification, and supports all of the +new proposed features of 2.0, including: + +- Batch submission (via the `MultiCall` class) +- Keyword arguments +- Notifications (both in a batch and 'normal') +- Class translation using the `__jsonclass__` key. + +A `SimpleJSONRPCServer` class has been added. It is intended to emulate the +`SimpleXMLRPCServer` from the default Python distribution. + +## Requirements + +This library supports `cjson` and `simplejson`, and looks for the parsers in +that order (searching first for `cjson`, then for the *built-in* `json` in 2.7+, +and then the `simplejson` external library). +One of these must be installed to use this library, although if you have a +standard distribution of 2.7+, you should already have one. +Keep in mind that `cjson` is supposed to be the quickest, I believe, so if you +are going for full-on optimization you may want to pick it up. + +## Installation + +You can install this from PyPI with one of the following commands (`sudo` +might be required): + +``` +# Global installation +pip install jsonrpclib-pelix + +# Local installation +pip install --user jsonrpclib-pelix +``` + +Alternatively, you can download the source from the GitHub repository at +[tcalmant/jsonrpclib](http://github.com/tcalmant/jsonrpclib) and manually +install it with the following commands: + +``` +git clone git://github.com/tcalmant/jsonrpclib.git +cd jsonrpclib +python setup.py install +``` + +## A note on logging + +`jsonrpclib-pelix` uses the `logging` module from the standard Python +library to trace warnings and errors, but doesn't set it up. +As a result, you have to configure the Python logging to print out traces. + +The easiest way to do it is to add those lines at the beginning of your code: +```python +import logging +logging.basiConfig() +``` + +More information can be found in the +[`logging` documentation page](https://docs.python.org/3/library/logging.html). + +## `SimpleJSONRPCServer` + +This is identical in usage (or should be) to the `SimpleXMLRPCServer` in the +Python standard library. +Some of the differences in features are that it obviously supports notification, +batch calls, class translation (if left on), etc. + +**Note:** The import line is slightly different from the regular +`SimpleXMLRPCServer`, since the `SimpleJSONRPCServer` is provided by th +`jsonrpclib` library. + +```python +from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer + +server = SimpleJSONRPCServer(('localhost', 8080)) +server.register_function(pow) +server.register_function(lambda x,y: x+y, 'add') +server.register_function(lambda x: x, 'ping') +server.serve_forever() +``` + +To start protect the server with SSL, use the following snippet: + +```python +from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer +import ssl + +# Setup the SSL socket +server = SimpleJSONRPCServer(('localhost', 8080), bind_and_activate=False) +server.socket = ssl.wrap_socket(server.socket, certfile='server.pem', + server_side=True) +server.server_bind() +server.server_activate() + +# ... register functions +# Start the server +server.serve_forever() +``` + +### Notification Thread Pool + +By default, notification calls are handled in the request handling thread. +It is possible to use a thread pool to handle them, by giving it to the server +using the `set_notification_pool()` method: + +```python +from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer +from jsonrpclib.threadpool import ThreadPool + +# Setup the thread pool: between 0 and 10 threads +pool = ThreadPool(max_threads=10, min_threads=0) + +# Don't forget to start it +pool.start() + +# Setup the server +server = SimpleJSONRPCServer(('localhost', 8080)) +server.set_notification_pool(pool) + +# Register methods +server.register_function(pow) +server.register_function(lambda x,y: x+y, 'add') +server.register_function(lambda x: x, 'ping') + +try: + server.serve_forever() +finally: + # Stop the thread pool (let threads finish their current task) + pool.stop() + server.set_notification_pool(None) +``` + +### Threaded server + +It is also possible to use a thread pool to handle clients requests, using the +`PooledJSONRPCServer` class. +By default, this class uses pool of 0 to 30 threads. +A custom pool can be given with the `thread_pool` parameter of the class +constructor. + +The notification pool and the request pool are different: by default, a server +with a request pool doesn't have a notification pool. + +```python +from jsonrpclib.SimpleJSONRPCServer import PooledJSONRPCServer +from jsonrpclib.threadpool import ThreadPool + +# Setup the notification and request pools +nofif_pool = ThreadPool(max_threads=10, min_threads=0) +request_pool = ThreadPool(max_threads=50, min_threads=10) + +# Don't forget to start them +nofif_pool.start() +request_pool.start() + +# Setup the server +server = PooledJSONRPCServer(('localhost', 8080), thread_pool=request_pool) +server.set_notification_pool(nofif_pool) + +# Register methods +server.register_function(pow) +server.register_function(lambda x,y: x+y, 'add') +server.register_function(lambda x: x, 'ping') + +try: + server.serve_forever() +finally: + # Stop the thread pools (let threads finish their current task) + request_pool.stop() + nofif_pool.stop() + server.set_notification_pool(None) +``` + +### Unix socket + +To start a server listening on a Unix socket, you will have to use the +following snippet: + +```python +from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer +import os +import socket + +# Set the path to the socket file +socket_name = "/tmp/my_socket.socket" + +# Ensure that the file doesn't exist yet (or an error will be raised) +if os.path.exists(socket_name): + os.remove(socket_name) + +try: + # Start the server, indicating the socket family + # The server will force some flags when in Unix socket mode + # (no log request, no reuse address, ...) + srv = SimpleJSONRPCServer(socket_name, address_family=socket.AF_UNIX) + + # ... register methods to the server + # Run the server + srv.serve_forever() +except KeyboardInterrupt: + # Shutdown the server gracefully + srv.shutdown() + srv.server_close() +finally: + # You should clean up after the server stopped + os.remove(socket_name) +``` + +This feature is tested on Linux during Travis-CI builds. It also has +been tested on Windows Subsystem for Linux (WSL) on Windows 10 1809. + +This feature is not available on "pure" Windows, as it doesn't provide +the `AF_UNIX` address family. + +## Client Usage + +This is (obviously) taken from a console session. + +```python +>>> import jsonrpclib +>>> server = jsonrpclib.ServerProxy('http://localhost:8080') +>>> server.add(5,6) +11 +>>> server.add(x=5, y=10) +15 +>>> server._notify.add(5,6) +# No result returned... +>>> batch = jsonrpclib.MultiCall(server) +>>> batch.add(5, 6) +>>> batch.ping({'key':'value'}) +>>> batch._notify.add(4, 30) +>>> results = batch() +>>> for result in results: +>>> ... print(result) +11 +{'key': 'value'} +# Note that there are only two responses -- this is according to spec. + +# Clean up +>>> server('close')() + +# Using client history +>>> history = jsonrpclib.history.History() +>>> server = jsonrpclib.ServerProxy('http://localhost:8080', history=history) +>>> server.add(5,6) +11 +>>> print(history.request) +{"id": "f682b956-c8e1-4506-9db4-29fe8bc9fcaa", "jsonrpc": "2.0", + "method": "add", "params": [5, 6]} +>>> print(history.response) +{"id": "f682b956-c8e1-4506-9db4-29fe8bc9fcaa", "jsonrpc": "2.0", + "result": 11} + +# Clean up +>>> server('close')() +``` + +If you need 1.0 functionality, there are a bunch of places you can pass +that in, although the best is just to give a specific configuration to +`jsonrpclib.ServerProxy`: + +```python +>>> import jsonrpclib +>>> jsonrpclib.config.DEFAULT.version +2.0 +>>> config = jsonrpclib.config.Config(version=1.0) +>>> history = jsonrpclib.history.History() +>>> server = jsonrpclib.ServerProxy('http://localhost:8080', config=config, + history=history) +>>> server.add(7, 10) +17 +>>> print(history.request) +{"id": "827b2923-5b37-49a5-8b36-e73920a16d32", + "method": "add", "params": [7, 10]} +>>> print(history.response) +{"id": "827b2923-5b37-49a5-8b36-e73920a16d32", "error": null, "result": 17} +>>> server('close')() +``` + +The equivalent `loads` and `dumps` functions also exist, although with +minor modifications. +The `dumps` arguments are almost identical, but it adds three arguments: +`rpcid` for the `id` key, `version` to specify the JSON-RPC compatibility, +and `notify` if it's a request that you want to be a notification. + +Additionally, the `loads` method does not return the params and method like +`xmlrpclib`, but instead +a.) parses for errors, raising ProtocolErrors, and +b.) returns the entire structure of the request / response for manual parsing. + +### Unix sockets + +To connect a JSON-RPC server over a Unix socket, you have to use a specific +protocol: `unix+http`. + +When connecting to a Unix socket in the current working directory, you can use +the following syntax: `unix+http://my.socket` + +When you need to give an absolute path you must use the path part of the URL, +the host part will be ignored. For example, you can use this URL to indicate a +Unix socket in `/var/lib/daemon.socket`: `unix+http://./var/lib/daemon.socket` + +**Note:** Currently, only HTTP is supported over a Unix socket. +If you want HTTPS support to be implemented, please create an +[issue on GitHub](https://github.com/tcalmant/jsonrpclib/issues) + +### Additional headers + +If your remote service requires custom headers in request, you can pass them +using the `headers` keyword argument, when creating the `ServerProxy`: + +```python +>>> import jsonrpclib +>>> server = jsonrpclib.ServerProxy("http://localhost:8080", + headers={'X-Test' : 'Test'}) +``` + +You can also put additional request headers only for certain method +invocation: + +```python +>>> import jsonrpclib +>>> server = jsonrpclib.Server("http://localhost:8080") +>>> with server._additional_headers({'X-Test' : 'Test'}) as test_server: +... test_server.ping(42) +... +>>> # X-Test header will be no longer sent in requests +``` + +Of course `_additional_headers` contexts can be nested as well. + +## Class Translation + +The library supports an *"automatic"* class translation process, although it +is turned off by default. +This can be devastatingly slow if improperly used, so the following is just a +short list of things to keep in mind when using it. + +- Keep It (the object) Simple Stupid. (for exceptions, keep reading) +- Do not require init params (for exceptions, keep reading) +- Getter properties without setters could be dangerous (read: not tested) + +If any of the above are issues, use the `_serialize` method (see usage below). +The server and client must **BOTH** have the `use_jsonclass` configuration +item on and they must both have access to the same libraries used by the +objects for this to work. + +If you have excessively nested arguments, it would be better to turn off the +translation and manually invoke it on specific objects using +`jsonrpclib.jsonclass.dump` / `jsonrpclib.jsonclass.load` (since the +default behavior recursively goes through attributes and lists/dicts/tuples). + +* Sample file: `test_obj.py` + +```python +# This object is /very/ simple, and the system will look through the +# attributes and serialize what it can. +class TestObj(object): + foo = 'bar' + +# This object requires __init__ params, so it uses the _serialize method +# and returns a tuple of init params and attribute values (the init params +# can be a dict or a list, but the attribute values must be a dict.) +class TestSerial(object): + foo = 'bar' + def __init__(self, *args): + self.args = args + def _serialize(self): + return (self.args, {'foo':self.foo,}) +``` + +- Sample usage: + +```python +>>> import jsonrpclib +>>> import test_obj + +# History is used only to print the serialized form of beans +>>> history = jsonrpclib.history.History() +>>> testobj1 = test_obj.TestObj() +>>> testobj2 = test_obj.TestSerial() +>>> server = jsonrpclib.Server('http://localhost:8080', history=history) + +# The 'ping' just returns whatever is sent +>>> ping1 = server.ping(testobj1) +>>> ping2 = server.ping(testobj2) + +>>> print(history.request) +{"id": "7805f1f9-9abd-49c6-81dc-dbd47229fe13", "jsonrpc": "2.0", + "method": "ping", "params": [{"__jsonclass__": + ["test_obj.TestSerial", []], "foo": "bar"} + ]} +>>> print(history.response) +{"id": "7805f1f9-9abd-49c6-81dc-dbd47229fe13", "jsonrpc": "2.0", + "result": {"__jsonclass__": ["test_obj.TestSerial", []], "foo": "bar"}} +``` + +This behavior is turned on by default. +To deactivate it, just set the `use_jsonclass` member of a server `Config` to +`False`. +If you want to use a per-class serialization method, set its name in the +`serialize_method` member of a server `Config`. +Finally, if you are using classes that you have defined in the implementation +(as in, not a separate library), you'll need to add those +(on **BOTH** the server and the client) using the `config.classes.add()` method. + +Feedback on this "feature" is very, VERY much appreciated. + +## Tests + +Tests are an almost-verbatim drop from the JSON-RPC specification 2.0 +page. They can be run using *unittest* or *nosetest*: + +``` +python -m unittest discover tests +python3 -m unittest discover tests +nosetests tests +``` + +## Why JSON-RPC? + +In my opinion, there are several reasons to choose JSON over XML for RPC: + +* Much simpler to read (I suppose this is opinion, but I know I'm right. :) +* Size / Bandwidth - Main reason, a JSON object representation is just much + smaller. +* Parsing - JSON should be much quicker to parse than XML. +* Easy class passing with `jsonclass` (when enabled) + +In the interest of being fair, there are also a few reasons to choose XML over +JSON: + +* Your server doesn't do JSON (rather obvious) +* Wider XML-RPC support across APIs (can we change this? :)) +* Libraries are more established, *i.e.* more stable (Let's change this too) diff -Nru jsonrpclib-pelix-0.3.1/README.rst jsonrpclib-pelix-0.4.1/README.rst --- jsonrpclib-pelix-0.3.1/README.rst 2017-04-27 08:30:53.000000000 +0000 +++ jsonrpclib-pelix-0.4.1/README.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,441 +0,0 @@ -JSONRPClib (patched for Pelix and Python 3) -########################################### - -.. image:: https://img.shields.io/pypi/v/jsonrpclib-pelix.svg - :target: https://pypi.python.org/pypi/jsonrpclib-pelix/ - :alt: Latest Version - -.. image:: https://img.shields.io/pypi/l/jsonrpclib-pelix.svg - :target: https://pypi.python.org/pypi/jsonrpclib-pelix/ - :alt: License - -.. image:: https://travis-ci.org/tcalmant/jsonrpclib.svg?branch=master - :target: https://travis-ci.org/tcalmant/jsonrpclib - :alt: Travis-CI status - -.. image:: https://coveralls.io/repos/tcalmant/jsonrpclib/badge.svg?branch=master - :target: https://coveralls.io/r/tcalmant/jsonrpclib?branch=master - :alt: Coveralls status - -This library is an implementation of the JSON-RPC specification. -It supports both the original 1.0 specification, as well as the -new (proposed) 2.0 specification, which includes batch submission, keyword -arguments, etc. - -It is licensed under the Apache License, Version 2.0 -(http://www.apache.org/licenses/LICENSE-2.0.html). - - -About this version -****************** - -This is a patched version of the original ``jsonrpclib`` project by -Josh Marshall, available at https://github.com/joshmarshall/jsonrpclib. - -The suffix *-pelix* only indicates that this version works with Pelix Remote -Services, but it is **not** a Pelix specific implementation. - -* This version adds support for Python 3, staying compatible with Python 2.7. -* It is now possible to use the dispatch_method argument while extending - the SimpleJSONRPCDispatcher, to use a custom dispatcher. - This allows to use this package by Pelix Remote Services. -* It can use thread pools to control the number of threads spawned to handle - notification requests and clients connections. -* The modifications added in other forks of this project have been added: - - * From https://github.com/drdaeman/jsonrpclib: - - * Improved JSON-RPC 1.0 support - * Less strict error response handling - - * From https://github.com/tuomassalo/jsonrpclib: - - * In case of a non-pre-defined error, raise an AppError and give access to - *error.data* - - * From https://github.com/dejw/jsonrpclib: - - * Custom headers can be sent with request and associated tests - -* The support for Unix sockets has been removed, as it is not trivial to convert - to Python 3 (and I don't use them) -* This version cannot be installed with the original ``jsonrpclib``, as it uses - the same package name. - - -Summary -******* - -This library implements the JSON-RPC 2.0 proposed specification in pure Python. -It is designed to be as compatible with the syntax of ``xmlrpclib`` as possible -(it extends where possible), so that projects using ``xmlrpclib`` could easily -be modified to use JSON and experiment with the differences. - -It is backwards-compatible with the 1.0 specification, and supports all of the -new proposed features of 2.0, including: - -* Batch submission (via MultiCall) -* Keyword arguments -* Notifications (both in a batch and 'normal') -* Class translation using the ``__jsonclass__`` key. - -I've added a "SimpleJSONRPCServer", which is intended to emulate the -"SimpleXMLRPCServer" from the default Python distribution. - - -Requirements -************ - -It supports ``cjson`` and ``simplejson``, and looks for the parsers in that -order (searching first for ``cjson``, then for the *built-in* ``json`` in 2.7, -and then the ``simplejson`` external library). -One of these must be installed to use this library, although if you have a -standard distribution of 2.7, you should already have one. -Keep in mind that ``cjson`` is supposed to be the quickest, I believe, so if -you are going for full-on optimization you may want to pick it up. - - -Installation -************ - -You can install this from PyPI with one of the following commands (sudo -may be required): - -.. code-block:: console - - easy_install jsonrpclib-pelix - pip install jsonrpclib-pelix - -Alternatively, you can download the source from the GitHub repository -at http://github.com/tcalmant/jsonrpclib and manually install it -with the following commands: - -.. code-block:: console - - git clone git://github.com/tcalmant/jsonrpclib.git - cd jsonrpclib - python setup.py install - - -SimpleJSONRPCServer -******************* - -This is identical in usage (or should be) to the SimpleXMLRPCServer in the -Python standard library. Some of the differences in features are that it -obviously supports notification, batch calls, class translation (if left on), -etc. -Note: The import line is slightly different from the regular SimpleXMLRPCServer, -since the SimpleJSONRPCServer is distributed within the ``jsonrpclib`` library. - -.. code-block:: python - - from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer - - server = SimpleJSONRPCServer(('localhost', 8080)) - server.register_function(pow) - server.register_function(lambda x,y: x+y, 'add') - server.register_function(lambda x: x, 'ping') - server.serve_forever() - -To start protect the server with SSL, use the following snippet: - -.. code-block:: python - - from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer - - # Setup the SSL socket - server = SimpleJSONRPCServer(('localhost', 8080), bind_and_activate=False) - server.socket = ssl.wrap_socket(server.socket, certfile='server.pem', - server_side=True) - server.server_bind() - server.server_activate() - - # ... register functions - # Start the server - server.serve_forever() - - -Notification Thread Pool -======================== - -By default, notification calls are handled in the request handling thread. -It is possible to use a thread pool to handle them, by giving it to the server -using the ``set_notification_pool()`` method: - -.. code-block:: python - - from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer - from jsonrpclib.threadpool import ThreadPool - - # Setup the thread pool: between 0 and 10 threads - pool = ThreadPool(max_threads=10, min_threads=0) - - # Don't forget to start it - pool.start() - - # Setup the server - server = SimpleJSONRPCServer(('localhost', 8080), config) - server.set_notification_pool(pool) - - # Register methods - server.register_function(pow) - server.register_function(lambda x,y: x+y, 'add') - server.register_function(lambda x: x, 'ping') - - try: - server.serve_forever() - finally: - # Stop the thread pool (let threads finish their current task) - pool.stop() - server.set_notification_pool(None) - - -Threaded server -=============== - -It is also possible to use a thread pool to handle clients requests, using the -``PooledJSONRPCServer`` class. -By default, this class uses pool of 0 to 30 threads. A custom pool can be given -with the ``thread_pool`` parameter of the class constructor. - -The notification pool and the request pool are different: by default, a server -with a request pool doesn't have a notification pool. - -.. code-block:: python - - from jsonrpclib.SimpleJSONRPCServer import PooledJSONRPCServer - from jsonrpclib.threadpool import ThreadPool - - # Setup the notification and request pools - nofif_pool = ThreadPool(max_threads=10, min_threads=0) - request_pool = ThreadPool(max_threads=50, min_threads=10) - - # Don't forget to start them - nofif_pool.start() - request_pool.start() - - # Setup the server - server = PooledJSONRPCServer(('localhost', 8080), config, - thread_pool=request_pool) - server.set_notification_pool(nofif_pool) - - # Register methods - server.register_function(pow) - server.register_function(lambda x,y: x+y, 'add') - server.register_function(lambda x: x, 'ping') - - try: - server.serve_forever() - finally: - # Stop the thread pools (let threads finish their current task) - request_pool.stop() - nofif_pool.stop() - server.set_notification_pool(None) - -Client Usage -************ - -This is (obviously) taken from a console session. - -.. code-block:: python - - >>> import jsonrpclib - >>> server = jsonrpclib.ServerProxy('http://localhost:8080') - >>> server.add(5,6) - 11 - >>> server.add(x=5, y=10) - 15 - >>> server._notify.add(5,6) - # No result returned... - >>> batch = jsonrpclib.MultiCall(server) - >>> batch.add(5, 6) - >>> batch.ping({'key':'value'}) - >>> batch._notify.add(4, 30) - >>> results = batch() - >>> for result in results: - >>> ... print(result) - 11 - {'key': 'value'} - # Note that there are only two responses -- this is according to spec. - - # Clean up - >>> server('close')() - - # Using client history - >>> history = jsonrpclib.history.History() - >>> server = jsonrpclib.ServerProxy('http://localhost:8080', history=history) - >>> server.add(5,6) - 11 - >>> print(history.request) - {"id": "f682b956-c8e1-4506-9db4-29fe8bc9fcaa", "jsonrpc": "2.0", - "method": "add", "params": [5, 6]} - >>> print(history.response) - {"id": "f682b956-c8e1-4506-9db4-29fe8bc9fcaa", "jsonrpc": "2.0", - "result": 11} - - # Clean up - >>> server('close')() - -If you need 1.0 functionality, there are a bunch of places you can pass that in, -although the best is just to give a specific configuration to -``jsonrpclib.ServerProxy``: - -.. code-block:: python - - >>> import jsonrpclib - >>> jsonrpclib.config.DEFAULT.version - 2.0 - >>> config = jsonrpclib.config.Config(version=1.0) - >>> history = jsonrpclib.history.History() - >>> server = jsonrpclib.ServerProxy('http://localhost:8080', config=config, - history=history) - >>> server.add(7, 10) - 17 - >>> print(history.request) - {"id": "827b2923-5b37-49a5-8b36-e73920a16d32", - "method": "add", "params": [7, 10]} - >>> print(history.response) - {"id": "827b2923-5b37-49a5-8b36-e73920a16d32", "error": null, "result": 17} - >>> server('close')() - -The equivalent ``loads`` and ``dumps`` functions also exist, although with minor -modifications. The ``dumps`` arguments are almost identical, but it adds three -arguments: ``rpcid`` for the 'id' key, ``version`` to specify the JSON-RPC -compatibility, and ``notify`` if it's a request that you want to be a -notification. - -Additionally, the ``loads`` method does not return the params and method like -``xmlrpclib``, but instead a.) parses for errors, raising ProtocolErrors, and -b.) returns the entire structure of the request / response for manual parsing. - - -Additional headers -****************** - -If your remote service requires custom headers in request, you can pass them -as as a ``headers`` keyword argument, when creating the ``ServerProxy``: - -.. code-block:: python - - >>> import jsonrpclib - >>> server = jsonrpclib.ServerProxy("http://localhost:8080", - headers={'X-Test' : 'Test'}) - -You can also put additional request headers only for certain method invocation: - -.. code-block:: python - - >>> import jsonrpclib - >>> server = jsonrpclib.Server("http://localhost:8080") - >>> with server._additional_headers({'X-Test' : 'Test'}) as test_server: - ... test_server.ping(42) - ... - >>> # X-Test header will be no longer sent in requests - -Of course ``_additional_headers`` contexts can be nested as well. - - -Class Translation -***************** - -I've recently added "automatic" class translation support, although it is -turned off by default. This can be devastatingly slow if improperly used, so -the following is just a short list of things to keep in mind when using it. - -* Keep It (the object) Simple Stupid. (for exceptions, keep reading.) -* Do not require init params (for exceptions, keep reading) -* Getter properties without setters could be dangerous (read: not tested) - -If any of the above are issues, use the _serialize method. (see usage below) -The server and client must BOTH have use_jsonclass configuration item on and -they must both have access to the same libraries used by the objects for -this to work. - -If you have excessively nested arguments, it would be better to turn off the -translation and manually invoke it on specific objects using -``jsonrpclib.jsonclass.dump`` / ``jsonrpclib.jsonclass.load`` (since the default -behavior recursively goes through attributes and lists / dicts / tuples). - - Sample file: *test_obj.py* - -.. code-block:: python - - # This object is /very/ simple, and the system will look through the - # attributes and serialize what it can. - class TestObj(object): - foo = 'bar' - - # This object requires __init__ params, so it uses the _serialize method - # and returns a tuple of init params and attribute values (the init params - # can be a dict or a list, but the attribute values must be a dict.) - class TestSerial(object): - foo = 'bar' - def __init__(self, *args): - self.args = args - def _serialize(self): - return (self.args, {'foo':self.foo,}) - -* Sample usage - -.. code-block:: python - - >>> import jsonrpclib - >>> import test_obj - - # History is used only to print the serialized form of beans - >>> history = jsonrpclib.history.History() - >>> testobj1 = test_obj.TestObj() - >>> testobj2 = test_obj.TestSerial() - >>> server = jsonrpclib.Server('http://localhost:8080', history=history) - - # The 'ping' just returns whatever is sent - >>> ping1 = server.ping(testobj1) - >>> ping2 = server.ping(testobj2) - - >>> print(history.request) - {"id": "7805f1f9-9abd-49c6-81dc-dbd47229fe13", "jsonrpc": "2.0", - "method": "ping", "params": [{"__jsonclass__": - ["test_obj.TestSerial", []], "foo": "bar"} - ]} - >>> print(history.response) - {"id": "7805f1f9-9abd-49c6-81dc-dbd47229fe13", "jsonrpc": "2.0", - "result": {"__jsonclass__": ["test_obj.TestSerial", []], "foo": "bar"}} - -This behavior is turned by default. To deactivate it, just set the -``use_jsonclass`` member of a server ``Config`` to False. -If you want to use a per-class serialization method, set its name in the -``serialize_method`` member of a server ``Config``. -Finally, if you are using classes that you have defined in the implementation -(as in, not a separate library), you'll need to add those (on BOTH the server -and the client) using the ``config.classes.add()`` method. - -Feedback on this "feature" is very, VERY much appreciated. - -Why JSON-RPC? -************* - -In my opinion, there are several reasons to choose JSON over XML for RPC: - -* Much simpler to read (I suppose this is opinion, but I know I'm right. :) -* Size / Bandwidth - Main reason, a JSON object representation is just much smaller. -* Parsing - JSON should be much quicker to parse than XML. -* Easy class passing with ``jsonclass`` (when enabled) - -In the interest of being fair, there are also a few reasons to choose XML -over JSON: - -* Your server doesn't do JSON (rather obvious) -* Wider XML-RPC support across APIs (can we change this? :)) -* Libraries are more established, i.e. more stable (Let's change this too.) - -Tests -***** - -Tests are an almost-verbatim drop from the JSON-RPC specification 2.0 page. -They can be run using *unittest* or *nosetest*: - -.. code-block:: console - - python -m unittest discover tests - python3 -m unittest discover tests - nosetests tests diff -Nru jsonrpclib-pelix-0.3.1/setup.cfg jsonrpclib-pelix-0.4.1/setup.cfg --- jsonrpclib-pelix-0.3.1/setup.cfg 2017-06-27 11:08:06.000000000 +0000 +++ jsonrpclib-pelix-0.4.1/setup.cfg 2020-04-12 14:48:27.000000000 +0000 @@ -4,5 +4,4 @@ [egg_info] tag_build = tag_date = 0 -tag_svn_revision = 0 diff -Nru jsonrpclib-pelix-0.3.1/setup.py jsonrpclib-pelix-0.4.1/setup.py --- jsonrpclib-pelix-0.3.1/setup.py 2017-06-27 08:56:32.000000000 +0000 +++ jsonrpclib-pelix-0.4.1/setup.py 2020-04-12 14:34:55.000000000 +0000 @@ -1,69 +1,76 @@ -#!/usr/bin/env python -# -- Content-Encoding: UTF-8 -- -""" -Installation script - -:authors: Josh Marshall, Thomas Calmant -:copyright: Copyright 2017, Thomas Calmant -:license: Apache License 2.0 -:version: 0.3.1 - -.. - - Copyright 2017 Thomas Calmant - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -""" - -# Module version -__version_info__ = (0, 3, 1) -__version__ = ".".join(str(x) for x in __version_info__) - -# Documentation strings format -__docformat__ = "restructuredtext en" - -# ------------------------------------------------------------------------------ - -try: - from setuptools import setup -except ImportError: - from distutils.core import setup - -# ------------------------------------------------------------------------------ - -setup( - name="jsonrpclib-pelix", - version=__version__, - license="Apache License 2.0", - author="Thomas Calmant", - author_email="thomas.calmant+github@gmail.com", - url="http://github.com/tcalmant/jsonrpclib/", - description= - "This project is an implementation of the JSON-RPC v2.0 specification " - "(backwards-compatible) as a client library, for Python 2.7 and Python 3." - "This version is a fork of jsonrpclib by Josh Marshall, " - "usable with Pelix remote services.", - long_description=open("README.rst").read(), - packages=["jsonrpclib"], - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6'] -) +#!/usr/bin/env python +# -- Content-Encoding: UTF-8 -- +""" +Installation script + +:authors: Josh Marshall, Thomas Calmant +:copyright: Copyright 2020, Thomas Calmant +:license: Apache License 2.0 +:version: 0.4.1 + +.. + + Copyright 2020 Thomas Calmant + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +# Module version +__version_info__ = (0, 4, 1) +__version__ = ".".join(str(x) for x in __version_info__) + +# Documentation strings format +__docformat__ = "restructuredtext en" + +# ------------------------------------------------------------------------------ + +try: + from setuptools import setup +except ImportError: + from distutils.core import setup + +# ------------------------------------------------------------------------------ + +# Read the description file +with open("README.md", "r") as fh: + long_description = fh.read() + +setup( + name="jsonrpclib-pelix", + version=__version__, + license="Apache License 2.0", + author="Thomas Calmant", + author_email="thomas.calmant+github@gmail.com", + url="http://github.com/tcalmant/jsonrpclib/", + description="This project is an implementation of the JSON-RPC v2.0 " + "specification (backwards-compatible) as a client library, for Python 2.7 " + "and Python 3. This version is a fork of jsonrpclib by Josh Marshall, " + "made to be also usable with Pelix/iPOPO remote services.", + long_description=long_description, + long_description_content_type="text/markdown", + packages=["jsonrpclib"], + test_suite="tests", + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + ], +)