pax_global_header00006660000000000000000000000064145140411640014512gustar00rootroot0000000000000052 comment=bc9cc9ab0719f26d96ac3a230404f05b1176e0b1 Pyro5-5.15/000077500000000000000000000000001451404116400124625ustar00rootroot00000000000000Pyro5-5.15/.gitattributes000066400000000000000000000001401451404116400153500ustar00rootroot00000000000000* text=auto *.py text=auto *.rst text=auto *.txt text=auto *.sh text=lf *.png -text *.jpg -text Pyro5-5.15/.github/000077500000000000000000000000001451404116400140225ustar00rootroot00000000000000Pyro5-5.15/.github/workflows/000077500000000000000000000000001451404116400160575ustar00rootroot00000000000000Pyro5-5.15/.github/workflows/main-ci.yml000066400000000000000000000020341451404116400201160ustar00rootroot00000000000000# This workflow will install Python dependencies, run tests and lint with a single version of Python # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: Python application on: push: branches: [ master ] pull_request: branches: [ master ] # allow manual trigger workflow_dispatch: jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] steps: - name: Checkout source uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install pytest msgpack pip install -r requirements.txt - name: build and install run: pip install . - name: Test with pytest run: | pytest -v tests Pyro5-5.15/.gitignore000066400000000000000000000001721451404116400144520ustar00rootroot00000000000000*.py[cod] *.class *.egg *.egg-info /MANIFEST /.idea/ /.tox/ /build/ /dist/ /.cache/ /.eggs/ /.mypy_cache/ /.pytest_cache/ Pyro5-5.15/LICENSE000066400000000000000000000020511451404116400134650ustar00rootroot00000000000000MIT License Copyright (c) Irmen de Jong Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Pyro5-5.15/MANIFEST.in000066400000000000000000000005351451404116400142230ustar00rootroot00000000000000include Readme.rst include LICENSE recursive-include certs * recursive-include tests * recursive-include examples * global-exclude */.svn/* global-exclude */.idea/* global-exclude *.class global-exclude *.pyc global-exclude *.pyo global-exclude *.coverage global-exclude .git global-exclude .gitignore global-exclude .tox global-exclude __pycache__ Pyro5-5.15/Makefile000066400000000000000000000022331451404116400141220ustar00rootroot00000000000000.PHONY: all dist docs install upload clean test PYTHON=python3 all: @echo "targets: dist, docs, upload, install, clean, test" docs: sphinx-build docs/source build/docs dist: $(PYTHON) setup.py sdist bdist_wheel upload: dist @echo "Uploading to Pypi using twine...." twine upload dist/* install: $(PYTHON) setup.py install test: PYTHONPATH=. python3 -m pytest tests clean: @echo "Removing tox dirs, logfiles, temp files, .pyo/.pyc files..." rm -rf .tox .eggs .cache .pytest_cache *.egg-info find . -name __pycache__ -print0 | xargs -0 rm -rf find . -name \*_log -print0 | xargs -0 rm -f find . -name \*.log -print0 | xargs -0 rm -f find . -name \*_URI -print0 | xargs -0 rm -f find . -name \*.pyo -print0 | xargs -0 rm -f find . -name \*.pyc -print0 | xargs -0 rm -f find . -name \*.class -print0 | xargs -0 rm -f find . -name \*.DS_Store -print0 | xargs -0 rm -f find . -name \.coverage -print0 | xargs -0 rm -f find . -name \coverage.xml -print0 | xargs -0 rm -f rm -f MANIFEST rm -rf build rm -rf dist rm -rf tests/test-reports find . -name '.#*' -print0 | xargs -0 rm -f find . -name '#*#' -print0 | xargs -0 rm -f @echo "clean!" Pyro5-5.15/Pyro5/000077500000000000000000000000001451404116400135005ustar00rootroot00000000000000Pyro5-5.15/Pyro5/__init__.py000066400000000000000000000032731451404116400156160ustar00rootroot00000000000000""" Pyro package. Some generic init stuff to set up logging etc. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ __version__ = "5.15" __author__ = "Irmen de Jong" def __configure_logging(): """Do some basic config of the logging module at package import time. The configuring is done only if the PYRO_LOGLEVEL env var is set. If you want to use your own logging config, make sure you do that before any Pyro imports. Then Pyro will skip the autoconfig. Set the env var PYRO_LOGFILE to change the name of the autoconfigured log file (default is pyro5.log in the current dir). Use '{stderr}' to make the log go to the standard error output.""" import os import logging level = os.environ.get("PYRO_LOGLEVEL") logfilename = os.environ.get("PYRO_LOGFILE", "pyro5.log") if level: levelvalue = getattr(logging, level) if len(logging.root.handlers) == 0: logging.basicConfig( level=levelvalue, filename=None if logfilename == "{stderr}" else logfilename, datefmt="%Y-%m-%d %H:%M:%S", format="[%(asctime)s.%(msecs)03d,%(name)s,%(levelname)s] %(message)s" ) log = logging.getLogger("Pyro5") log.info("Pyro log configured using built-in defaults, level=%s", level) else: # PYRO_LOGLEVEL is not set, disable Pyro logging. No message is printed about this fact. log = logging.getLogger("Pyro5") log.setLevel(9999) return logfilename, None return logfilename, level or None _pyro_logfile, _pyro_loglevel = __configure_logging() from .configure import global_config as config Pyro5-5.15/Pyro5/api.py000066400000000000000000000027201451404116400146240ustar00rootroot00000000000000""" Single module that centralizes the main symbols from the Pyro5 API. It imports most of the other packages that it needs and provides shortcuts to the most frequently used objects and functions from those packages. This means you can mostly just ``import Pyro5.api`` in your code to have access to most of the Pyro5 objects and functions. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ from . import __version__ from .configure import global_config as config from .core import URI, locate_ns, resolve, type_meta from .client import Proxy, BatchProxy, SerializedBlob from .server import Daemon, DaemonObject, callback, expose, behavior, oneway, serve from .nameserver import start_ns, start_ns_loop from .serializers import SerializerBase from .callcontext import current_context register_dict_to_class = SerializerBase.register_dict_to_class register_class_to_dict = SerializerBase.register_class_to_dict unregister_dict_to_class = SerializerBase.unregister_dict_to_class unregister_class_to_dict = SerializerBase.unregister_class_to_dict __all__ = ["config", "URI", "locate_ns", "resolve", "type_meta", "current_context", "Proxy", "BatchProxy", "SerializedBlob", "SerializerBase", "Daemon", "DaemonObject", "callback", "expose", "behavior", "oneway", "start_ns", "start_ns_loop", "serve", "register_dict_to_class", "register_class_to_dict", "unregister_dict_to_class", "unregister_class_to_dict"] Pyro5-5.15/Pyro5/callcontext.py000066400000000000000000000033311451404116400163720ustar00rootroot00000000000000""" Deals with the context variables of a Pyro call. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import threading from . import errors class _CallContext(threading.local): """call context thread local""" def __init__(self): # per-thread initialization self.client = None self.client_sock_addr = None self.seq = 0 self.msg_flags = 0 self.serializer_id = 0 self.annotations = {} self.response_annotations = {} self.correlation_id = None def to_global(self): return dict(self.__dict__) def from_global(self, values): self.client = values["client"] self.seq = values["seq"] self.msg_flags = values["msg_flags"] self.serializer_id = values["serializer_id"] self.annotations = values["annotations"] self.response_annotations = values["response_annotations"] self.correlation_id = values["correlation_id"] self.client_sock_addr = values["client_sock_addr"] def track_resource(self, resource): """keep a weak reference to the resource to be tracked for this connection""" if self.client: self.client.tracked_resources.add(resource) else: raise errors.PyroError("cannot track resource on a connectionless call") def untrack_resource(self, resource): """no longer track the resource for this connection""" if self.client: self.client.tracked_resources.discard(resource) else: raise errors.PyroError("cannot untrack resource on a connectionless call") current_context = _CallContext() """the thread-local context object for the current Pyro call""" Pyro5-5.15/Pyro5/client.py000066400000000000000000000756401451404116400153440ustar00rootroot00000000000000""" Client related classes (Proxy, mostly) Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import sys import time import logging import serpent import contextlib from . import config, core, serializers, protocol, errors, socketutil from .callcontext import current_context try: from greenlet import getcurrent as get_ident except ImportError: from threading import get_ident log = logging.getLogger("Pyro5.client") __all__ = ["Proxy", "BatchProxy", "SerializedBlob"] class Proxy(object): """ Pyro proxy for a remote object. Intercepts method calls and dispatches them to the remote object. .. automethod:: _pyroBind .. automethod:: _pyroRelease .. automethod:: _pyroReconnect .. automethod:: _pyroValidateHandshake .. autoattribute:: _pyroTimeout .. attribute:: _pyroMaxRetries Number of retries to perform on communication calls by this proxy, allows you to override the default setting. .. attribute:: _pyroSerializer Name of the serializer to use by this proxy, allows you to override the default setting. .. attribute:: _pyroHandshake The data object that should be sent in the initial connection handshake message. Can be any serializable object. .. attribute:: _pyroLocalSocket The socket that is used locally to connect to the remote daemon. The format depends on the address family used for the connection, but usually for IPV4 connections it is the familiar (hostname, port) tuple. Consult the Python documentation on `socket families `_ for more details """ __pyroAttributes = frozenset( ["__getnewargs__", "__getnewargs_ex__", "__getinitargs__", "_pyroConnection", "_pyroUri", "_pyroOneway", "_pyroMethods", "_pyroAttrs", "_pyroTimeout", "_pyroSeq", "_pyroLocalSocket", "_pyroRawWireResponse", "_pyroHandshake", "_pyroMaxRetries", "_pyroSerializer", "_Proxy__pyroTimeout", "_Proxy__pyroOwnerThread"]) def __init__(self, uri, connected_socket=None): if connected_socket: uri = core.URI("PYRO:" + uri + "@<>:0") if isinstance(uri, str): uri = core.URI(uri) elif not isinstance(uri, core.URI): raise TypeError("expected Pyro URI") self._pyroUri = uri self._pyroConnection = None self._pyroSerializer = None # can be set to the name of a serializer to override the global one per-proxy self._pyroMethods = set() # all methods of the remote object, gotten from meta-data self._pyroAttrs = set() # attributes of the remote object, gotten from meta-data self._pyroOneway = set() # oneway-methods of the remote object, gotten from meta-data self._pyroSeq = 0 # message sequence number self._pyroRawWireResponse = False # internal switch to enable wire level responses self._pyroHandshake = "hello" # the data object that should be sent in the initial connection handshake message self._pyroMaxRetries = config.MAX_RETRIES self.__pyroTimeout = config.COMMTIMEOUT self.__pyroOwnerThread = get_ident() # the thread that owns this proxy if config.SERIALIZER not in serializers.serializers: raise ValueError("unknown serializer configured") # note: we're not clearing the client annotations dict here. # that is because otherwise it will be wiped if a new proxy is needed to connect PYRONAME uris. # clearing the response annotations is okay. current_context.response_annotations = {} if connected_socket: self.__pyroCreateConnection(False, connected_socket) def __del__(self): if hasattr(self, "_pyroConnection"): try: self._pyroRelease() except Exception: pass def __getattr__(self, name): if name in Proxy.__pyroAttributes: # allows it to be safely pickled raise AttributeError(name) # get metadata if it's not there yet if not self._pyroMethods and not self._pyroAttrs: self._pyroGetMetadata() if name in self._pyroAttrs: return self._pyroInvoke("__getattr__", (name,), None) if name not in self._pyroMethods: # client side check if the requested attr actually exists raise AttributeError("remote object '%s' has no exposed attribute or method '%s'" % (self._pyroUri, name)) return _RemoteMethod(self._pyroInvoke, name, self._pyroMaxRetries) def __setattr__(self, name, value): if name in Proxy.__pyroAttributes: return super(Proxy, self).__setattr__(name, value) # one of the special pyro attributes # get metadata if it's not there yet if not self._pyroMethods and not self._pyroAttrs: self._pyroGetMetadata() if name in self._pyroAttrs: return self._pyroInvoke("__setattr__", (name, value), None) # remote attribute # client side validation if the requested attr actually exists raise AttributeError("remote object '%s' has no exposed attribute '%s'" % (self._pyroUri, name)) def __repr__(self): if self._pyroConnection: connected = "connected " + self._pyroConnection.family() else: connected = "not connected" return "<%s.%s at 0x%x; %s; for %s; owner %s>" % (self.__class__.__module__, self.__class__.__name__, id(self), connected, self._pyroUri, self.__pyroOwnerThread) def __getstate__(self): # make sure a tuple of just primitive types are used to allow for proper serialization return str(self._pyroUri), tuple(self._pyroOneway), tuple(self._pyroMethods), \ tuple(self._pyroAttrs), self._pyroHandshake, self._pyroSerializer def __setstate__(self, state): self._pyroUri = core.URI(state[0]) self._pyroOneway = set(state[1]) self._pyroMethods = set(state[2]) self._pyroAttrs = set(state[3]) self._pyroHandshake = state[4] self._pyroSerializer = state[5] self.__pyroTimeout = config.COMMTIMEOUT self._pyroMaxRetries = config.MAX_RETRIES self._pyroConnection = None self._pyroLocalSocket = None self._pyroSeq = 0 self._pyroRawWireResponse = False self.__pyroOwnerThread = get_ident() def __copy__(self): p = object.__new__(type(self)) p.__setstate__(self.__getstate__()) p._pyroTimeout = self._pyroTimeout p._pyroRawWireResponse = self._pyroRawWireResponse p._pyroMaxRetries = self._pyroMaxRetries return p def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self._pyroRelease() def __eq__(self, other): if other is self: return True return isinstance(other, Proxy) and other._pyroUri == self._pyroUri def __ne__(self, other): if other and isinstance(other, Proxy): return other._pyroUri != self._pyroUri return True def __hash__(self): return hash(self._pyroUri) def __dir__(self): result = dir(self.__class__) + list(self.__dict__.keys()) return sorted(set(result) | self._pyroMethods | self._pyroAttrs) # When special methods are invoked via special syntax (e.g. obj[index] calls # obj.__getitem__(index)), the special methods are not looked up via __getattr__ # for efficiency reasons; instead, their presence is checked directly. # Thus we need to define them here to force (remote) lookup through __getitem__. def __bool__(self): return True def __len__(self): return self.__getattr__('__len__')() def __getitem__(self, index): return self.__getattr__('__getitem__')(index) def __setitem__(self, index, val): return self.__getattr__('__setitem__')(index, val) def __delitem__(self, index): return self.__getattr__('__delitem__')(index) def __iter__(self): try: # use remote iterator if it exists yield from self.__getattr__('__iter__')() except AttributeError: # fallback to indexed based iteration try: yield from (self[index] for index in range(sys.maxsize)) except (StopIteration, IndexError): return def _pyroRelease(self): """release the connection to the pyro daemon""" self.__check_owner() if self._pyroConnection is not None: self._pyroConnection.close() self._pyroConnection = None self._pyroLocalSocket = None def _pyroBind(self): """ Bind this proxy to the exact object from the uri. That means that the proxy's uri will be updated with a direct PYRO uri, if it isn't one yet. If the proxy is already bound, it will not bind again. """ return self.__pyroCreateConnection(True) def __pyroGetTimeout(self): return self.__pyroTimeout def __pyroSetTimeout(self, timeout): self.__pyroTimeout = timeout if self._pyroConnection is not None: self._pyroConnection.timeout = timeout _pyroTimeout = property(__pyroGetTimeout, __pyroSetTimeout, doc=""" The timeout in seconds for calls on this proxy. Defaults to ``None``. If the timeout expires before the remote method call returns, Pyro will raise a :exc:`Pyro5.errors.TimeoutError`""") def _pyroInvoke(self, methodname, vargs, kwargs, flags=0, objectId=None): """perform the remote method call communication""" self.__check_owner() current_context.response_annotations = {} if self._pyroConnection is None: self.__pyroCreateConnection() serializer = serializers.serializers[self._pyroSerializer or config.SERIALIZER] objectId = objectId or self._pyroConnection.objectId annotations = current_context.annotations if vargs and isinstance(vargs[0], SerializedBlob): # special serialization of a 'blob' that stays serialized data, flags = self.__serializeBlobArgs(vargs, kwargs, annotations, flags, objectId, methodname, serializer) else: # normal serialization of the remote call data = serializer.dumpsCall(objectId, methodname, vargs, kwargs) if methodname in self._pyroOneway: flags |= protocol.FLAGS_ONEWAY self._pyroSeq = (self._pyroSeq + 1) & 0xffff msg = protocol.SendingMessage(protocol.MSG_INVOKE, flags, self._pyroSeq, serializer.serializer_id, data, annotations=annotations) if config.LOGWIRE: protocol.log_wiredata(log, "proxy wiredata sending", msg) try: self._pyroConnection.send(msg.data) del msg # invite GC to collect the object, don't wait for out-of-scope if flags & protocol.FLAGS_ONEWAY: return None # oneway call, no response data else: msg = protocol.recv_stub(self._pyroConnection, [protocol.MSG_RESULT]) if config.LOGWIRE: protocol.log_wiredata(log, "proxy wiredata received", msg) self.__pyroCheckSequence(msg.seq) if msg.serializer_id != serializer.serializer_id: error = "invalid serializer in response: %d" % msg.serializer_id log.error(error) raise errors.SerializeError(error) if msg.annotations: current_context.response_annotations = msg.annotations if self._pyroRawWireResponse: return msg data = serializer.loads(msg.data) if msg.flags & protocol.FLAGS_ITEMSTREAMRESULT: streamId = bytes(msg.annotations.get("STRM", b"")).decode() if not streamId: raise errors.ProtocolError("result of call is an iterator, but the server is not configured to allow streaming") return _StreamResultIterator(streamId, self) if msg.flags & protocol.FLAGS_EXCEPTION: raise data # if you see this in your traceback, you should probably inspect the remote traceback as well else: return data except (errors.CommunicationError, KeyboardInterrupt): # Communication error during read. To avoid corrupt transfers, we close the connection. # Otherwise we might receive the previous reply as a result of a new method call! # Special case for keyboardinterrupt: people pressing ^C to abort the client # may be catching the keyboardinterrupt in their code. We should probably be on the # safe side and release the proxy connection in this case too, because they might # be reusing the proxy object after catching the exception... self._pyroRelease() raise def __pyroCheckSequence(self, seq): if seq != self._pyroSeq: err = "invoke: reply sequence out of sync, got %d expected %d" % (seq, self._pyroSeq) log.error(err) raise errors.ProtocolError(err) def __pyroCreateConnection(self, replaceUri=False, connected_socket=None): """ Connects this proxy to the remote Pyro daemon. Does connection handshake. Returns true if a new connection was made, false if an existing one was already present. """ def connect_and_handshake(conn): try: if self._pyroConnection is not None: return False # already connected if config.SSL: sslContext = socketutil.get_ssl_context(clientcert=config.SSL_CLIENTCERT, clientkey=config.SSL_CLIENTKEY, keypassword=config.SSL_CLIENTKEYPASSWD, cacerts=config.SSL_CACERTS) else: sslContext = None sock = socketutil.create_socket(connect=connect_location, reuseaddr=config.SOCK_REUSE, timeout=self.__pyroTimeout, nodelay=config.SOCK_NODELAY, sslContext=sslContext) conn = socketutil.SocketConnection(sock, uri.object) # Do handshake. serializer = serializers.serializers[self._pyroSerializer or config.SERIALIZER] data = {"handshake": self._pyroHandshake, "object": uri.object} data = serializer.dumps(data) msg = protocol.SendingMessage(protocol.MSG_CONNECT, 0, self._pyroSeq, serializer.serializer_id, data, annotations=current_context.annotations) if config.LOGWIRE: protocol.log_wiredata(log, "proxy connect sending", msg) conn.send(msg.data) msg = protocol.recv_stub(conn, [protocol.MSG_CONNECTOK, protocol.MSG_CONNECTFAIL]) if config.LOGWIRE: protocol.log_wiredata(log, "proxy connect response received", msg) except Exception as x: if conn: conn.close() err = "cannot connect to %s: %s" % (connect_location, x) log.error(err) if isinstance(x, errors.CommunicationError): raise else: raise errors.CommunicationError(err) from x else: handshake_response = "?" if msg.data: serializer = serializers.serializers_by_id[msg.serializer_id] handshake_response = serializer.loads(msg.data) if msg.type == protocol.MSG_CONNECTFAIL: error = "connection to %s rejected: %s" % (connect_location, handshake_response) conn.close() log.error(error) raise errors.CommunicationError(error) elif msg.type == protocol.MSG_CONNECTOK: self.__processMetadata(handshake_response["meta"]) handshake_response = handshake_response["handshake"] self._pyroConnection = conn self._pyroLocalSocket = conn.sock.getsockname() if replaceUri: self._pyroUri = uri self._pyroValidateHandshake(handshake_response) log.debug("connected to %s - %s - %s", self._pyroUri, conn.family(), "SSL" if sslContext else "unencrypted") if msg.annotations: current_context.response_annotations = msg.annotations else: conn.close() err = "cannot connect to %s: invalid msg type %d received" % (connect_location, msg.type) log.error(err) raise errors.ProtocolError(err) self.__check_owner() if self._pyroConnection is not None: return False # already connected uri = core.resolve(self._pyroUri) # socket connection (normal or Unix domain socket) conn = None log.debug("connecting to %s", uri) connect_location = uri.sockname or (uri.host, uri.port) if connected_socket: self._pyroConnection = socketutil.SocketConnection(connected_socket, uri.object, True) self._pyroLocalSocket = connected_socket.getsockname() else: connect_and_handshake(conn) # obtain metadata if this feature is enabled, and the metadata is not known yet if not self._pyroMethods and not self._pyroAttrs: self._pyroGetMetadata(uri.object) return True def _pyroGetMetadata(self, objectId=None, known_metadata=None): """ Get metadata from server (methods, attrs, oneway, ...) and remember them in some attributes of the proxy. Usually this will already be known due to the default behavior of the connect handshake, where the connect response also includes the metadata. """ objectId = objectId or self._pyroUri.object log.debug("getting metadata for object %s", objectId) if self._pyroConnection is None and not known_metadata: try: self.__pyroCreateConnection() except errors.PyroError: log.error("problem getting metadata: cannot connect") raise if self._pyroMethods or self._pyroAttrs: return # metadata has already been retrieved as part of creating the connection try: # invoke the get_metadata method on the daemon result = known_metadata or self._pyroInvoke("get_metadata", [objectId], {}, objectId=core.DAEMON_NAME) self.__processMetadata(result) except errors.PyroError: log.exception("problem getting metadata") raise def __processMetadata(self, metadata): if not metadata: return self._pyroOneway = set(metadata["oneway"]) self._pyroMethods = set(metadata["methods"]) self._pyroAttrs = set(metadata["attrs"]) if log.isEnabledFor(logging.DEBUG): log.debug("from meta: methods=%s, oneway methods=%s, attributes=%s", sorted(self._pyroMethods), sorted(self._pyroOneway), sorted(self._pyroAttrs)) if not self._pyroMethods and not self._pyroAttrs: raise errors.PyroError("remote object doesn't expose any methods or attributes. Did you forget setting @expose on them?") def _pyroReconnect(self, tries=100000000): """ (Re)connect the proxy to the daemon containing the pyro object which the proxy is for. In contrast to the _pyroBind method, this one first releases the connection (if the proxy is still connected) and retries making a new connection until it succeeds or the given amount of tries ran out. """ self._pyroRelease() while tries: try: self.__pyroCreateConnection() return except errors.CommunicationError: tries -= 1 if tries: time.sleep(2) msg = "failed to reconnect" log.error(msg) raise errors.ConnectionClosedError(msg) def _pyroInvokeBatch(self, calls, oneway=False): flags = protocol.FLAGS_BATCH if oneway: flags |= protocol.FLAGS_ONEWAY return self._pyroInvoke("", calls, None, flags) def _pyroValidateHandshake(self, response): """ Process and validate the initial connection handshake response data received from the daemon. Simply return without error if everything is ok. Raise an exception if something is wrong and the connection should not be made. """ return def _pyroClaimOwnership(self): """ The current thread claims the ownership of this proxy from another thread. Any existing connection will remain active! """ if get_ident() != self.__pyroOwnerThread: # if self._pyroConnection is not None: # self._pyroConnection.close() # self._pyroConnection = None self.__pyroOwnerThread = get_ident() def __serializeBlobArgs(self, vargs, kwargs, annotations, flags, objectId, methodname, serializer): """ Special handling of a "blob" argument that has to stay serialized until explicitly deserialized in client code. This makes efficient, transparent gateways or dispatchers and such possible: they don't have to de/reserialize the message and are independent from the serialized class definitions. Annotations are passed in because some blob metadata is added. They're not part of the blob itself. """ if len(vargs) > 1 or kwargs: raise errors.SerializeError("if SerializedBlob is used, it must be the only argument") blob = vargs[0] flags |= protocol.FLAGS_KEEPSERIALIZED # Pass the objectId and methodname separately in an annotation because currently, # they are embedded inside the serialized message data. And we're not deserializing that, # so we have to have another means of knowing the object and method it is meant for... # A better solution is perhaps to split the actual remote method arguments from the # control data (object + methodname) but that requires a major protocol change. # The code below is not as nice but it works without any protocol change and doesn't # require a hack either - so it's actually not bad like this. import marshal annotations["BLBI"] = marshal.dumps((blob.info, objectId, methodname)) if blob._contains_blob: # directly pass through the already serialized msg data from within the blob protocol_msg = blob._data return protocol_msg.data, flags else: # replaces SerializedBlob argument with the data to be serialized return serializer.dumpsCall(objectId, methodname, blob._data, kwargs), flags def __check_owner(self): if get_ident() != self.__pyroOwnerThread: raise errors.PyroError("the calling thread is not the owner of this proxy, " "create a new proxy in this thread or transfer ownership.") class _RemoteMethod(object): """method call abstraction""" def __init__(self, send, name, max_retries): self.__send = send self.__name = name self.__max_retries = max_retries def __getattr__(self, name): return _RemoteMethod(self.__send, "%s.%s" % (self.__name, name), self.__max_retries) def __call__(self, *args, **kwargs): for attempt in range(self.__max_retries + 1): try: return self.__send(self.__name, args, kwargs) except (errors.ConnectionClosedError, errors.TimeoutError): # only retry for recoverable network errors if attempt >= self.__max_retries: # last attempt, raise the exception raise class _StreamResultIterator(object): """ Pyro returns this as a result of a remote call which returns an iterator or generator. It is a normal iterable and produces elements on demand from the remote iterator. You can simply use it in for loops, list comprehensions etc. """ def __init__(self, streamId, proxy): self.streamId = streamId self.proxy = proxy self.pyroseq = proxy._pyroSeq def __iter__(self): return self def __next__(self): if self.proxy is None: raise StopIteration if self.proxy._pyroConnection is None: raise errors.ConnectionClosedError("the proxy for this stream result has been closed") self.pyroseq += 1 try: return self.proxy._pyroInvoke("get_next_stream_item", [self.streamId], {}, objectId=core.DAEMON_NAME) except (StopIteration, GeneratorExit): # when the iterator is exhausted, the proxy is removed to avoid unneeded close_stream calls later # (the server has closed its part of the stream by itself already) self.proxy = None raise def __del__(self): try: self.close() except Exception: pass def close(self): if self.proxy and self.proxy._pyroConnection is not None: if self.pyroseq == self.proxy._pyroSeq: # we're still in sync, it's okay to use the same proxy to close this stream self.proxy._pyroInvoke("close_stream", [self.streamId], {}, flags=protocol.FLAGS_ONEWAY, objectId=core.DAEMON_NAME) else: # The proxy's sequence number has diverged. # One of the reasons this can happen is because this call is being done from python's GC where # it decides to gc old iterator objects *during a new call on the proxy*. # If we use the same proxy and do a call in between, the other call on the proxy will get an out of sync seq and crash! # We create a temporary second proxy to call close_stream on. This is inefficient, but avoids the problem. with contextlib.suppress(errors.CommunicationError): with self.proxy.__copy__() as closingProxy: closingProxy._pyroInvoke("close_stream", [self.streamId], {}, flags=protocol.FLAGS_ONEWAY, objectId=core.DAEMON_NAME) self.proxy = None class _BatchedRemoteMethod(object): """method call abstraction that is used with batched calls""" def __init__(self, calls, name): self.__calls = calls self.__name = name def __getattr__(self, name): return _BatchedRemoteMethod(self.__calls, "%s.%s" % (self.__name, name)) def __call__(self, *args, **kwargs): self.__calls.append((self.__name, args, kwargs)) class BatchProxy(object): """Proxy that lets you batch multiple method calls into one. It is constructed with a reference to the normal proxy that will carry out the batched calls. Call methods on this object that you want to batch, and finally call the batch proxy itself. That call will return a generator for the results of every method call in the batch (in sequence).""" def __init__(self, proxy): self.__proxy = proxy self.__calls = [] def __getattr__(self, name): return _BatchedRemoteMethod(self.__calls, name) def __enter__(self): return self def __exit__(self, *args): pass def __copy__(self): copy = type(self)(self.__proxy) copy.__calls = list(self.__calls) return copy def __resultsgenerator(self, results): for result in results: if isinstance(result, core._ExceptionWrapper): result.raiseIt() # re-raise the remote exception locally. else: yield result # it is a regular result object, yield that and continue. def __call__(self, oneway=False): self.__proxy._pyroClaimOwnership() results = self.__proxy._pyroInvokeBatch(self.__calls, oneway) self.__calls = [] # clear for re-use if not oneway: return self.__resultsgenerator(results) def _pyroInvoke(self, name, args, kwargs): # ignore all parameters, we just need to execute the batch results = self.__proxy._pyroInvokeBatch(self.__calls) self.__calls = [] # clear for re-use return self.__resultsgenerator(results) class SerializedBlob(object): """ Used to wrap some data to make Pyro pass this object transparently (it keeps the serialized payload as-is) Only when you need to access the actual client data you can deserialize on demand. This makes efficient, transparent gateways or dispatchers and such possible: they don't have to de/reserialize the message and are independent from the serialized class definitions. You have to pass this as the only parameter to a remote method call for Pyro to understand it. Init arguments: ``info`` = some (small) descriptive data about the blob. Can be a simple id or name or guid. Must be marshallable. ``data`` = the actual client data payload that you want to transfer in the blob. Can be anything that you would otherwise have used as regular remote call arguments. """ def __init__(self, info, data, is_blob=False): self.info = info self._data = data self._contains_blob = is_blob if is_blob and not isinstance(data, (protocol.SendingMessage, protocol.ReceivingMessage)): raise TypeError("data should be a protocol message object if is_blob is true") def deserialized(self): """Retrieves the client data stored in this blob. Deserializes the data automatically if required.""" if self._contains_blob: protocol_msg = self._data serializer = serializers.serializers_by_id[protocol_msg.serializer_id] if isinstance(protocol_msg, protocol.ReceivingMessage): _, _, data, _ = serializer.loads(protocol_msg.data) else: # strip off header bytes from SendingMessage payload_data = memoryview(protocol_msg.data)[protocol._header_size:] _, _, data, _ = serializer.loads(payload_data) return data else: return self._data # register the special serializers for the pyro objects serpent.register_class(Proxy, serializers.pyro_class_serpent_serializer) serializers.SerializerBase.register_class_to_dict(Proxy, serializers.serialize_pyro_object_to_dict, serpent_too=False) Pyro5-5.15/Pyro5/compatibility/000077500000000000000000000000001451404116400163515ustar00rootroot00000000000000Pyro5-5.15/Pyro5/compatibility/Pyro4.py000066400000000000000000000136501451404116400177450ustar00rootroot00000000000000""" An effort to provide a backward-compatible Pyro4 API layer, to make porting existing code from Pyro4 to Pyro5 easier. This only works for code that imported Pyro4 symbols from the Pyro4 module directly, instead of from one of Pyro4's sub modules. So, for instance: from Pyro4 import Proxy instead of: from Pyro4.core import Proxy *some* submodules are more or less emulated such as Pyro4.errors, Pyro4.socketutil. So, you may first have to convert your old code to use the importing scheme to only import the Pyro4 module and not from its submodules, and then you should insert this at the top to enable the compatibility layer: from Pyro5.compatibility import Pyro4 Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import sys import ipaddress from .. import api from .. import errors from .. import serializers from .. import socketutil as socketutil_pyro5 from ..configure import Configuration __all__ = ["config", "URI", "Proxy", "Daemon", "callback", "batch", "asyncproxy", "oneway", "expose", "behavior", "current_context", "locateNS", "resolve", "Future", "errors"] # symbols that are no longer available in Pyro5 and that we don't emulate: def asyncproxy(*args, **kwargs): raise NotImplementedError("async proxy is no longer available in Pyro5") class Future(object): def __init__(self, *args): raise NotImplementedError("Pyro5 no longer provides its own Future class, " "you should use Python's concurrent.futures module instead for that") class NamespaceInterceptor: def __init__(self, namespace): self.namespace = namespace def __getattr__(self, item): raise NotImplementedError("The Pyro4 compatibility layer doesn't provide the Pyro4.{0} module, " "first make sure the code only uses symbols from the Pyro4 package directly" .format(self.namespace)) naming = NamespaceInterceptor("naming") core = NamespaceInterceptor("core") message = NamespaceInterceptor("message") # compatibility wrappers for the other symbols: __version__ = api.__version__ callback = api.callback oneway = api.oneway expose = api.expose behavior = api.behavior current_context = api.current_context class CompatConfiguration(Configuration): def asDict(self): return self.as_dict() config = CompatConfiguration() class URI(api.URI): pass class Proxy(api.Proxy): def _pyroAsync(self, asynchronous=True): raise NotImplementedError("async proxy is no longer available in Pyro5") @property def _pyroHmacKey(self): raise NotImplementedError("pyro5 doesn't have hmacs anymore") def __setattr__(self, name, value): if name == "_pyroHmacKey": raise NotImplementedError("pyro5 doesn't have hmacs anymore") return super().__setattr__(name, value) class Daemon(api.Daemon): pass def locateNS(host=None, port=None, broadcast=True, hmac_key=None): if hmac_key: raise NotImplementedError("hmac_key is no longer available in Pyro5, consider using 2-way SSL instead") return api.locate_ns(host, port, broadcast) def resolve(uri, hmac_key=None): if hmac_key: raise NotImplementedError("hmac_key is no longer available in Pyro5, consider using 2-way SSL instead") return api.resolve(uri) class BatchProxy(api.BatchProxy): def __call__(self, oneway=False, asynchronous=False): if asynchronous: raise NotImplementedError("async proxy is no longer available in Pyro5") return super().__call__(oneway) def batch(proxy): return BatchProxy(proxy) class UtilModule: @staticmethod def getPyroTraceback(ex_type=None, ex_value=None, ex_tb=None): return errors.get_pyro_traceback(ex_type, ex_value, ex_tb) @staticmethod def formatTraceback(ex_type=None, ex_value=None, ex_tb=None, detailed=False): return errors.format_traceback(ex_type, ex_value, ex_tb, detailed) SerializerBase = serializers.SerializerBase def excepthook(self, *args, **kwargs): return errors.excepthook(*args, **kwargs) util = UtilModule() class SocketUtilModule: @staticmethod def getIpVersion(hostnameOrAddress): return ipaddress.ip_address(hostnameOrAddress).version @staticmethod def getIpAddress(hostname, workaround127=False, ipVersion=None): return str(socketutil_pyro5.get_ip_address(hostname, workaround127, ipVersion)) @staticmethod def getInterfaceAddress(ip_address): return str(socketutil_pyro5.get_interface(ip_address).ip) @staticmethod def createSocket(bind=None, connect=None, reuseaddr=False, keepalive=True, timeout=-1, noinherit=False, ipv6=False, nodelay=True, sslContext=None): return socketutil_pyro5.create_socket(bind, connect, reuseaddr, keepalive, timeout, noinherit, ipv6, nodelay, sslContext) @staticmethod def createBroadcastSocket(bind=None, reuseaddr=False, timeout=-1, ipv6=False): return socketutil_pyro5.create_bc_socket(bind, reuseaddr, timeout, ipv6) @staticmethod def receiveData(sock, size): return socketutil_pyro5.receive_data(sock, size) @staticmethod def sendData(sock, data): return socketutil_pyro5.send_data(sock, data) socketutil = SocketUtilModule() class ConstantsModule: from .. import __version__ as VERSION from ..core import DAEMON_NAME, NAMESERVER_NAME from ..protocol import PROTOCOL_VERSION constants = ConstantsModule() # make sure that subsequent from Pyro4 import ... will work: sys.modules["Pyro4"] = sys.modules[__name__] sys.modules["Pyro4.errors"] = errors sys.modules["Pyro4.core"] = core sys.modules["Pyro4.naming"] = naming sys.modules["Pyro4.util"] = util sys.modules["Pyro4.socketutil"] = socketutil sys.modules["Pyro4.constants"] = constants sys.modules["Pyro4.message"] = message Pyro5-5.15/Pyro5/compatibility/__init__.py000066400000000000000000000000001451404116400204500ustar00rootroot00000000000000Pyro5-5.15/Pyro5/configure.py000066400000000000000000000127061451404116400160410ustar00rootroot00000000000000""" Configuration settings. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import os import platform from . import __version__, _pyro_logfile, _pyro_loglevel # noinspection PyAttributeOutsideInit class Configuration: # Declare available config items. # DO NOT EDIT THESE HERE IN THIS MODULE! They are the global defaults. # Instead, specify them later in your own code or via environment variables. __slots__ = [ "HOST", "NS_HOST", "NS_PORT", "NS_BCPORT", "NS_BCHOST", "NS_AUTOCLEAN", "NS_LOOKUP_DELAY", "NATHOST", "NATPORT", "COMPRESSION", "SERVERTYPE", "COMMTIMEOUT", "POLLTIMEOUT", "MAX_RETRIES", "SOCK_REUSE", "SOCK_NODELAY", "DETAILED_TRACEBACK", "THREADPOOL_SIZE", "THREADPOOL_SIZE_MIN", "MAX_MESSAGE_SIZE", "BROADCAST_ADDRS", "PREFER_IP_VERSION", "SERIALIZER", "SERPENT_BYTES_REPR", "ITER_STREAMING", "ITER_STREAM_LIFETIME", "ITER_STREAM_LINGER", "LOGFILE", "LOGLEVEL", "LOGWIRE", "SSL", "SSL_SERVERCERT", "SSL_SERVERKEY", "SSL_SERVERKEYPASSWD", "SSL_REQUIRECLIENTCERT", "SSL_CLIENTCERT", "SSL_CLIENTKEY", "SSL_CLIENTKEYPASSWD", "SSL_CACERTS" ] def __init__(self): self.reset() def reset(self, use_environment=True): """ Reset to default config items. If use_environment is False, won't read environment variables settings (useful if you can't trust your env). """ self.HOST = "localhost" self.NS_HOST = "localhost" self.NS_PORT = 9090 self.NS_BCPORT = 9091 self.NS_BCHOST = None self.NS_AUTOCLEAN = 0.0 self.NS_LOOKUP_DELAY = 0.0 self.NATHOST = None self.NATPORT = 0 self.COMPRESSION = False self.SERVERTYPE = "thread" self.COMMTIMEOUT = 0.0 self.POLLTIMEOUT = 2.0 self.MAX_RETRIES = 0 self.SOCK_REUSE = True # so_reuseaddr on server sockets? self.SOCK_NODELAY = False # tcp_nodelay on socket? self.DETAILED_TRACEBACK = False self.THREADPOOL_SIZE = 80 self.THREADPOOL_SIZE_MIN = 4 self.MAX_MESSAGE_SIZE = 1024 * 1024 * 1024 # 1 gigabyte self.BROADCAST_ADDRS = ["", "0.0.0.0"] self.PREFER_IP_VERSION = 0 # 4, 6 or 0 (0=let OS choose according to RFC 3484) self.SERIALIZER = "serpent" self.SERPENT_BYTES_REPR = False self.LOGWIRE = False self.ITER_STREAMING = True self.ITER_STREAM_LIFETIME = 0.0 self.ITER_STREAM_LINGER = 30.0 self.LOGFILE = _pyro_logfile self.LOGLEVEL = _pyro_loglevel self.SSL = False self.SSL_SERVERCERT = "" self.SSL_SERVERKEY = "" self.SSL_SERVERKEYPASSWD = "" self.SSL_REQUIRECLIENTCERT = False self.SSL_CLIENTCERT = "" self.SSL_CLIENTKEY = "" self.SSL_CLIENTKEYPASSWD = "" self.SSL_CACERTS = "" if use_environment: # environment variables overwrite config items prefix = "PYRO_" for item, envvalue in (e for e in os.environ.items() if e[0].startswith(prefix)): item = item[len(prefix):] if item not in self.__slots__: raise ValueError("invalid Pyro environment config variable: %s%s" % (prefix, item)) value = getattr(self, item) valuetype = type(value) if valuetype is set: envvalue = {v.strip() for v in envvalue.split(",")} elif valuetype is list: envvalue = [v.strip() for v in envvalue.split(",")] elif valuetype is bool: envvalue = envvalue.lower() if envvalue in ("0", "off", "no", "false"): envvalue = False elif envvalue in ("1", "yes", "on", "true"): envvalue = True else: raise ValueError("invalid boolean value: %s%s=%s" % (prefix, item, envvalue)) else: try: envvalue = valuetype(envvalue) except ValueError: raise ValueError("invalid Pyro environment config value: %s%s=%s" % (prefix, item, envvalue)) from None setattr(self, item, envvalue) def copy(self): """returns a copy of this config""" other = object.__new__(Configuration) for item in self.__slots__: setattr(other, item, getattr(self, item)) return other def as_dict(self): """returns this config as a regular dictionary""" return {item: getattr(self, item) for item in self.__slots__} def dump(self): """Easy config diagnostics""" from .protocol import PROTOCOL_VERSION result = ["Pyro version: %s" % __version__, "Loaded from: %s" % os.path.dirname(__file__), "Python version: %s %s (%s, %s)" % (platform.python_implementation(), platform.python_version(), platform.system(), os.name), "Protocol version: %d" % PROTOCOL_VERSION, "Currently active global configuration settings:"] for item, value in sorted(self.as_dict().items()): result.append("{:s} = {:s}".format(item, str(value))) return "\n".join(result) global_config = Configuration() def dump(): print(global_config.dump()) if __name__ == "__main__": dump() Pyro5-5.15/Pyro5/core.py000066400000000000000000000307151451404116400150100ustar00rootroot00000000000000""" Multi purpose stuff used by both clients and servers (URI etc) Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import re import logging import contextlib import ipaddress import socket import random import serpent from typing import Union, Optional from . import config, errors, socketutil, serializers __all__ = ["URI", "DAEMON_NAME", "NAMESERVER_NAME", "resolve", "locate_ns", "type_meta"] log = logging.getLogger("Pyro5.core") # standard object name for the Daemon object DAEMON_NAME = "Pyro.Daemon" # standard name for the Name server itself NAMESERVER_NAME = "Pyro.NameServer" class URI(object): """ Pyro object URI (universal resource identifier). The uri format is like this: ``PYRO:objectid@location`` where location is one of: - ``hostname:port`` (tcp/ip socket on given port) - ``./u:sockname`` (Unix domain socket on localhost) There is also a 'Magic format' for simple name resolution using Name server: ``PYRONAME:objectname[@location]`` (optional name server location, can also omit location port) And one that looks up things in the name server by metadata: ``PYROMETA:meta1,meta2,...[@location]`` (optional name server location, can also omit location port) You can write the protocol in lowercase if you like (``pyro:...``) but it will automatically be converted to uppercase internally. """ uriRegEx = re.compile(r"(?P[Pp][Yy][Rr][Oo][a-zA-Z]*):(?P\S+?)(@(?P.+))?$") def __init__(self, uri): if isinstance(uri, URI): state = uri.__getstate__() self.__setstate__(state) return if not isinstance(uri, str): raise TypeError("uri parameter object is of wrong type") self.sockname = self.host = self.port = None match = self.uriRegEx.match(uri) if not match: raise errors.PyroError("invalid uri") self.protocol = match.group("protocol").upper() self.object = match.group("object") location = match.group("location") if self.protocol == "PYRONAME": self._parseLocation(location, config.NS_PORT) elif self.protocol == "PYRO": if not location: raise errors.PyroError("invalid uri") self._parseLocation(location, None) elif self.protocol == "PYROMETA": self.object = set(m.strip() for m in self.object.split(",")) self._parseLocation(location, config.NS_PORT) else: raise errors.PyroError("invalid uri (protocol)") def _parseLocation(self, location, defaultPort): if not location: return if location.startswith("./u:"): self.sockname = location[4:] if (not self.sockname) or ':' in self.sockname: raise errors.PyroError("invalid uri (location)") else: if location.startswith("["): # ipv6 if location.startswith("[["): # possible mistake: double-bracketing raise errors.PyroError("invalid ipv6 address: enclosed in too many brackets") ipv6locationmatch = re.match(r"\[([0-9a-fA-F:%]+)](:(\d+))?", location) if not ipv6locationmatch: raise errors.PyroError("invalid ipv6 address: the part between brackets must be a numeric ipv6 address") self.host, _, self.port = ipv6locationmatch.groups() else: self.host, _, self.port = location.partition(":") if not self.port: self.port = defaultPort try: self.port = int(self.port) except (ValueError, TypeError): raise errors.PyroError("invalid port in uri, port=" + str(self.port)) @staticmethod def isUnixsockLocation(location): """determine if a location string is for a Unix domain socket""" return location.startswith("./u:") @property def location(self): """property containing the location string, for instance ``"servername.you.com:5555"``""" if self.host: if ":" in self.host: # ipv6 return "[%s]:%d" % (self.host, self.port) else: return "%s:%d" % (self.host, self.port) elif self.sockname: return "./u:" + self.sockname else: return None def __str__(self): if self.protocol == "PYROMETA": result = "PYROMETA:" + ",".join(self.object) else: result = self.protocol + ":" + self.object if self.location: return result + "@" + self.location return result def __repr__(self): return "<%s.%s at 0x%x; %s>" % (self.__class__.__module__, self.__class__.__name__, id(self), str(self)) def __eq__(self, other): if not isinstance(other, URI): return False return self.__getstate__() == other.__getstate__() def __ne__(self, other): return not self.__eq__(other) def __hash__(self): return hash(self.__getstate__()) def __getstate__(self): return self.protocol, self.object, self.sockname, self.host, self.port def __setstate__(self, state): self.protocol, self.object, self.sockname, self.host, self.port = state class _ExceptionWrapper(object): """Class that wraps a remote exception. If this is returned, Pyro will re-throw the exception on the receiving side. Usually this is taken care of by a special response message flag, but in the case of batched calls this flag is useless and another mechanism was needed.""" def __init__(self, exception): self.exception = exception def raiseIt(self): raise self.exception def __serialized_dict__(self): """serialized form as a dictionary""" return { "__class__": "Pyro5.core._ExceptionWrapper", "exception": serializers.SerializerBase.class_to_dict(self.exception) } # register the special serializers for the pyro objects with Serpent serpent.register_class(URI, serializers.pyro_class_serpent_serializer) serpent.register_class(_ExceptionWrapper, serializers.pyro_class_serpent_serializer) serializers.SerializerBase.register_class_to_dict(URI, serializers.serialize_pyro_object_to_dict, serpent_too=False) serializers.SerializerBase.register_class_to_dict(_ExceptionWrapper, _ExceptionWrapper.__serialized_dict__, serpent_too=False) def resolve(uri: Union[str, URI], delay_time: float = 0.0) -> URI: """ Resolve a 'magic' uri (PYRONAME, PYROMETA) into the direct PYRO uri. It finds a name server, and use that to resolve a PYRONAME uri into the direct PYRO uri pointing to the named object. If uri is already a PYRO uri, it is returned unmodified. You can consider this a shortcut function so that you don't have to locate and use a name server proxy yourself. Note: if you need to resolve more than a few names, consider using the name server directly instead of repeatedly calling this function, to avoid the name server lookup overhead from each call. You can set delay_time to the maximum number of seconds you are prepared to wait until a name registration becomes available in the nameserver. """ if isinstance(uri, str): uri = URI(uri) elif not isinstance(uri, URI): raise TypeError("can only resolve Pyro URIs") if uri.protocol == "PYRO": return uri log.debug("resolving %s", uri) from . import nameserver # doing it here to avoid circular import issues if uri.protocol == "PYRONAME": with locate_ns(uri.host, uri.port) as ns: return nameserver.lookup(ns, uri.object, delay_time) elif uri.protocol == "PYROMETA": with locate_ns(uri.host, uri.port) as ns: candidates = nameserver.yplookup(ns, uri.object, None, False, delay_time) if candidates: candidate = random.choice(list(candidates.values())) log.debug("resolved to candidate %s", candidate) return URI(candidate) raise errors.NamingError("no registrations available with desired metadata properties %s" % uri.object) else: raise errors.PyroError("invalid uri protocol") def locate_ns(host: Union[str, ipaddress.IPv4Address, ipaddress.IPv6Address] = "", port: Optional[int] = None, broadcast: bool = True) -> "client.Proxy": """Get a proxy for a name server somewhere in the network.""" from . import client if not host: # first try localhost if we have a good chance of finding it there if config.NS_HOST in ("localhost", "::1") or config.NS_HOST.startswith("127."): if ":" in config.NS_HOST: # ipv6 hosts = ["[%s]" % config.NS_HOST] else: # Some systems have 127.0.1.1 in the hosts file assigned to the hostname, # so try this too (only if it's actually used as a valid ip address) try: socket.gethostbyaddr("127.0.1.1") hosts = [config.NS_HOST] if config.NS_HOST == "127.0.1.1" else [config.NS_HOST, "127.0.1.1"] except socket.error: hosts = [config.NS_HOST] for host in hosts: uristring = "PYRO:%s@%s:%d" % (NAMESERVER_NAME, host, port or config.NS_PORT) log.debug("locating the NS: %s", uristring) proxy = client.Proxy(uristring) with contextlib.suppress(errors.PyroError): proxy._pyroBind() log.debug("located NS") return proxy if config.PREFER_IP_VERSION == 6: broadcast = False # ipv6 doesn't have broadcast. We should probably use multicast.... if broadcast: # broadcast lookup if not port: port = config.NS_BCPORT log.debug("broadcast locate") sock = socketutil.create_bc_socket(reuseaddr=config.SOCK_REUSE, timeout=0.7) for _ in range(3): try: for bcaddr in config.BROADCAST_ADDRS: try: sock.sendto(b"GET_NSURI", 0, (bcaddr, port)) except socket.error as x: err = getattr(x, "errno", x.args[0]) # handle some errno's that some platforms like to throw: if err not in socketutil.ERRNO_EADDRNOTAVAIL and err not in socketutil.ERRNO_EADDRINUSE: raise data, _ = sock.recvfrom(100) sock.close() text = data.decode("iso-8859-1") log.debug("located NS: %s", text) proxy = client.Proxy(text) return proxy except socket.timeout: continue with contextlib.suppress(OSError, socket.error): sock.shutdown(socket.SHUT_RDWR) sock.close() log.debug("broadcast locate failed, try direct connection on NS_HOST") else: log.debug("skipping broadcast lookup") # broadcast failed or skipped, try PYRO directly on specific host host = config.NS_HOST port = config.NS_PORT elif not isinstance(host, str): host = str(host) # take care of the occasion where host is an ipaddress.IpAddress # pyro direct lookup port = config.NS_PORT if not port else port if URI.isUnixsockLocation(host): uristring = "PYRO:%s@%s" % (NAMESERVER_NAME, host) else: # if not a unix socket, check for ipv6 if host and ":" in str(host): host = "[%s]" % host uristring = "PYRO:%s@%s:%d" % (NAMESERVER_NAME, host, port) uri = URI(uristring) log.debug("locating the NS: %s", uri) proxy = client.Proxy(uri) try: proxy._pyroBind() log.debug("located NS") return proxy except errors.PyroError as x: raise errors.NamingError("Failed to locate the nameserver") from x def type_meta(class_or_object, prefix="class:"): """extracts type metadata from the given class or object, can be used as Name server metadata.""" if hasattr(class_or_object, "__mro__"): return {prefix + c.__module__ + "." + c.__name__ for c in class_or_object.__mro__ if c.__module__ not in ("builtins", "__builtin__")} if hasattr(class_or_object, "__class__"): return type_meta(class_or_object.__class__) return frozenset() Pyro5-5.15/Pyro5/errors.py000066400000000000000000000150401451404116400153660ustar00rootroot00000000000000""" Definition of the various exceptions that are used in Pyro. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import sys import linecache import traceback from . import config class PyroError(Exception): """Generic base of all Pyro-specific errors.""" pass class CommunicationError(PyroError): """Base class for the errors related to network communication problems.""" pass class ConnectionClosedError(CommunicationError): """The connection was unexpectedly closed.""" pass class TimeoutError(CommunicationError): """ A call could not be completed within the set timeout period, or the network caused a timeout. """ pass class ProtocolError(CommunicationError): """Pyro received a message that didn't match the active Pyro network protocol, or there was a protocol related error.""" pass class MessageTooLargeError(ProtocolError): """Pyro received a message or was trying to send a message that exceeds the maximum message size as configured.""" pass class NamingError(PyroError): """There was a problem related to the name server or object names.""" pass class DaemonError(PyroError): """The Daemon encountered a problem.""" pass class SecurityError(PyroError): """A security related error occurred.""" pass class SerializeError(ProtocolError): """Something went wrong while (de)serializing data.""" pass def get_pyro_traceback(ex_type=None, ex_value=None, ex_tb=None): """Returns a list of strings that form the traceback information of a Pyro exception. Any remote Pyro exception information is included. Traceback information is automatically obtained via ``sys.exc_info()`` if you do not supply the objects yourself.""" def formatRemoteTraceback(remote_tb_lines): result = [" +--- This exception occured remotely (Pyro) - Remote traceback:"] for line in remote_tb_lines: if line.endswith("\n"): line = line[:-1] lines = line.split("\n") for line2 in lines: result.append("\n | ") result.append(line2) result.append("\n +--- End of remote traceback\n") return result try: if ex_type is not None and ex_value is None and ex_tb is None: if type(ex_type) is not type: raise TypeError("invalid argument: ex_type should be an exception type, or just supply no arguments at all") if ex_type is None and ex_tb is None: ex_type, ex_value, ex_tb = sys.exc_info() remote_tb = getattr(ex_value, "_pyroTraceback", None) local_tb = format_traceback(ex_type, ex_value, ex_tb, config.DETAILED_TRACEBACK) if remote_tb: remote_tb = formatRemoteTraceback(remote_tb) return local_tb + remote_tb else: # hmm. no remote tb info, return just the local tb. return local_tb finally: # clean up cycle to traceback, to allow proper GC del ex_type, ex_value, ex_tb def format_traceback(ex_type=None, ex_value=None, ex_tb=None, detailed=False): """Formats an exception traceback. If you ask for detailed formatting, the result will contain info on the variables in each stack frame. You don't have to provide the exception info objects, if you omit them, this function will obtain them itself using ``sys.exc_info()``.""" if ex_type is None and ex_tb is None: ex_type, ex_value, ex_tb = sys.exc_info() if detailed: def makeStrValue(value): try: return repr(value) except Exception: try: return str(value) except Exception: return "" try: result = ["-" * 52 + "\n", " EXCEPTION %s: %s\n" % (ex_type, ex_value), " Extended stacktrace follows (most recent call last)\n"] skipLocals = True # don't print the locals of the very first stack frame while ex_tb: frame = ex_tb.tb_frame sourceFileName = frame.f_code.co_filename if "self" in frame.f_locals: location = "%s.%s" % (frame.f_locals["self"].__class__.__name__, frame.f_code.co_name) else: location = frame.f_code.co_name result.append("-" * 52 + "\n") result.append("File \"%s\", line %d, in %s\n" % (sourceFileName, ex_tb.tb_lineno, location)) result.append("Source code:\n") result.append(" " + linecache.getline(sourceFileName, ex_tb.tb_lineno).strip() + "\n") if not skipLocals: names = set() names.update(getattr(frame.f_code, "co_varnames", ())) names.update(getattr(frame.f_code, "co_names", ())) names.update(getattr(frame.f_code, "co_cellvars", ())) names.update(getattr(frame.f_code, "co_freevars", ())) result.append("Local values:\n") for name2 in sorted(names): if name2 in frame.f_locals: value = frame.f_locals[name2] result.append(" %s = %s\n" % (name2, makeStrValue(value))) if name2 == "self": # print the local variables of the class instance for name3, value in vars(value).items(): result.append(" self.%s = %s\n" % (name3, makeStrValue(value))) skipLocals = False ex_tb = ex_tb.tb_next result.append("-" * 52 + "\n") result.append(" EXCEPTION %s: %s\n" % (ex_type, ex_value)) result.append("-" * 52 + "\n") return result except Exception: return ["-" * 52 + "\nError building extended traceback!!! :\n", "".join(traceback.format_exception(*sys.exc_info())) + '-' * 52 + '\n', "Original Exception follows:\n", "".join(traceback.format_exception(ex_type, ex_value, ex_tb))] else: # default traceback format. return traceback.format_exception(ex_type, ex_value, ex_tb) def excepthook(ex_type, ex_value, ex_tb): """An exception hook you can use for ``sys.excepthook``, to automatically print remote Pyro tracebacks""" tb = "".join(get_pyro_traceback(ex_type, ex_value, ex_tb)) sys.stderr.write(tb) Pyro5-5.15/Pyro5/nameserver.py000066400000000000000000001040241451404116400162220ustar00rootroot00000000000000""" Name Server and helper functions. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import warnings import re import sys import logging import socket import time import contextlib import threading from collections.abc import MutableMapping try: import sqlite3 except ImportError: pass from . import config, core, socketutil, server, errors from .errors import NamingError, PyroError, ProtocolError __all__ = ["start_ns_loop", "start_ns"] log = logging.getLogger("Pyro5.naming") class MemoryStorage(dict): """ Storage implementation that is just an in-memory dict. (because it inherits from dict it is automatically a collections.MutableMapping) Stopping the nameserver will make the server instantly forget about everything. """ def __init__(self, **kwargs): super(MemoryStorage, self).__init__(**kwargs) def __setitem__(self, key, value): uri, metadata = value super(MemoryStorage, self).__setitem__(key, (uri, metadata or frozenset())) def optimized_prefix_list(self, prefix, return_metadata=False): return None def optimized_regex_list(self, regex, return_metadata=False): return None def optimized_metadata_search(self, metadata_all=None, metadata_any=None, return_metadata=False): return None def everything(self, return_metadata=False): if return_metadata: return self.copy() return {name: uri for name, (uri, metadata) in self.items()} def remove_items(self, items): for item in items: if item in self: del self[item] def close(self): pass class SqlStorage(MutableMapping): """ Sqlite-based storage. It is just a single (name,uri) table for the names and another table for the metadata. Sqlite db connection objects aren't thread-safe, so a new connection is created in every method. """ def __init__(self, dbfile): if dbfile == ":memory:": raise ValueError("We don't support the sqlite :memory: database type. Just use the default volatile in-memory store.") self.dbfile = dbfile with sqlite3.connect(dbfile) as db: db.execute("PRAGMA foreign_keys=ON") try: db.execute("SELECT COUNT(*) FROM pyro_names").fetchone() except sqlite3.OperationalError: # the table does not yet exist self._create_schema(db) else: # check if we need to update the existing schema try: db.execute("SELECT COUNT(*) FROM pyro_metadata").fetchone() except sqlite3.OperationalError: # metadata schema needs to be created and existing data migrated db.execute("ALTER TABLE pyro_names RENAME TO pyro_names_old") self._create_schema(db) db.execute("INSERT INTO pyro_names(name, uri) SELECT name, uri FROM pyro_names_old") db.execute("DROP TABLE pyro_names_old") db.commit() def _create_schema(self, db): db.execute("""CREATE TABLE pyro_names ( id integer PRIMARY KEY, name nvarchar NOT NULL UNIQUE, uri nvarchar NOT NULL );""") db.execute("""CREATE TABLE pyro_metadata ( object integer NOT NULL, metadata nvarchar NOT NULL, FOREIGN KEY(object) REFERENCES pyro_names(id) );""") def __getattr__(self, item): raise NotImplementedError("SqlStorage doesn't implement method/attribute '" + item + "'") def __getitem__(self, item): try: with sqlite3.connect(self.dbfile) as db: result = db.execute("SELECT id, uri FROM pyro_names WHERE name=?", (item,)).fetchone() if result: dbid, uri = result metadata = {m[0] for m in db.execute("SELECT metadata FROM pyro_metadata WHERE object=?", (dbid,)).fetchall()} return uri, metadata else: raise KeyError(item) except sqlite3.DatabaseError as e: raise NamingError("sqlite error in getitem: " + str(e)) def __setitem__(self, key, value): uri, metadata = value try: with sqlite3.connect(self.dbfile) as db: cursor = db.cursor() cursor.execute("PRAGMA foreign_keys=ON") dbid = cursor.execute("SELECT id FROM pyro_names WHERE name=?", (key,)).fetchone() if dbid: dbid = dbid[0] cursor.execute("DELETE FROM pyro_metadata WHERE object=?", (dbid,)) cursor.execute("DELETE FROM pyro_names WHERE id=?", (dbid,)) cursor.execute("INSERT INTO pyro_names(name, uri) VALUES(?,?)", (key, uri)) if metadata: object_id = cursor.lastrowid for m in metadata: cursor.execute("INSERT INTO pyro_metadata(object, metadata) VALUES (?,?)", (object_id, m)) cursor.close() db.commit() except sqlite3.DatabaseError as e: raise NamingError("sqlite error in setitem: " + str(e)) def __len__(self): try: with sqlite3.connect(self.dbfile) as db: return db.execute("SELECT count(*) FROM pyro_names").fetchone()[0] except sqlite3.DatabaseError as e: raise NamingError("sqlite error in len: " + str(e)) def __contains__(self, item): try: with sqlite3.connect(self.dbfile) as db: return db.execute("SELECT EXISTS(SELECT 1 FROM pyro_names WHERE name=? LIMIT 1)", (item,)).fetchone()[0] except sqlite3.DatabaseError as e: raise NamingError("sqlite error in contains: " + str(e)) def __delitem__(self, key): try: with sqlite3.connect(self.dbfile) as db: db.execute("PRAGMA foreign_keys=ON") dbid = db.execute("SELECT id FROM pyro_names WHERE name=?", (key,)).fetchone() if dbid: dbid = dbid[0] db.execute("DELETE FROM pyro_metadata WHERE object=?", (dbid,)) db.execute("DELETE FROM pyro_names WHERE id=?", (dbid,)) db.commit() except sqlite3.DatabaseError as e: raise NamingError("sqlite error in delitem: " + str(e)) def __iter__(self): try: with sqlite3.connect(self.dbfile) as db: result = db.execute("SELECT name FROM pyro_names") return iter([n[0] for n in result.fetchall()]) except sqlite3.DatabaseError as e: raise NamingError("sqlite error in iter: " + str(e)) def clear(self): try: with sqlite3.connect(self.dbfile) as db: db.execute("PRAGMA foreign_keys=ON") db.execute("DELETE FROM pyro_metadata") db.execute("DELETE FROM pyro_names") db.commit() with sqlite3.connect(self.dbfile, isolation_level=None) as db: db.execute("VACUUM") # this cannot run inside a transaction. except sqlite3.DatabaseError as e: raise NamingError("sqlite error in clear: " + str(e)) def optimized_prefix_list(self, prefix, return_metadata=False): try: with sqlite3.connect(self.dbfile) as db: names = {} if return_metadata: for dbid, name, uri in db.execute("SELECT id, name, uri FROM pyro_names WHERE name LIKE ?", (prefix + '%',)).fetchall(): metadata = {m[0] for m in db.execute("SELECT metadata FROM pyro_metadata WHERE object=?", (dbid,)).fetchall()} names[name] = uri, metadata else: for name, uri in db.execute("SELECT name, uri FROM pyro_names WHERE name LIKE ?", (prefix + '%',)).fetchall(): names[name] = uri return names except sqlite3.DatabaseError as e: raise NamingError("sqlite error in optimized_prefix_list: " + str(e)) def optimized_regex_list(self, regex, return_metadata=False): # defining a regex function isn't much better than simply regexing ourselves over the full table. return None def optimized_metadata_search(self, metadata_all=None, metadata_any=None, return_metadata=False): try: with sqlite3.connect(self.dbfile) as db: if metadata_any: # any of the given metadata params = list(metadata_any) sql = "SELECT id, name, uri FROM pyro_names WHERE id IN (SELECT object FROM pyro_metadata WHERE metadata IN ({seq}))" \ .format(seq=",".join(['?'] * len(metadata_any))) else: # all of the given metadata params = list(metadata_all) params.append(len(metadata_all)) sql = "SELECT id, name, uri FROM pyro_names WHERE id IN (SELECT object FROM pyro_metadata WHERE metadata IN ({seq}) " \ "GROUP BY object HAVING COUNT(metadata)=?)".format(seq=",".join(['?'] * len(metadata_all))) result = db.execute(sql, params).fetchall() if return_metadata: names = {} for dbid, name, uri in result: metadata = {m[0] for m in db.execute("SELECT metadata FROM pyro_metadata WHERE object=?", (dbid,)).fetchall()} names[name] = uri, metadata else: names = {name: uri for (dbid, name, uri) in result} return names except sqlite3.DatabaseError as e: raise NamingError("sqlite error in optimized_metadata_search: " + str(e)) def remove_items(self, items): try: with sqlite3.connect(self.dbfile) as db: db.execute("PRAGMA foreign_keys=ON") for item in items: dbid = db.execute("SELECT id FROM pyro_names WHERE name=?", (item,)).fetchone() if dbid: dbid = dbid[0] db.execute("DELETE FROM pyro_metadata WHERE object=?", (dbid,)) db.execute("DELETE FROM pyro_names WHERE id=?", (dbid,)) db.commit() except sqlite3.DatabaseError as e: raise NamingError("sqlite error in remove_items: " + str(e)) def everything(self, return_metadata=False): try: with sqlite3.connect(self.dbfile) as db: names = {} if return_metadata: for dbid, name, uri in db.execute("SELECT id, name, uri FROM pyro_names").fetchall(): metadata = {m[0] for m in db.execute("SELECT metadata FROM pyro_metadata WHERE object=?", (dbid,)).fetchall()} names[name] = uri, metadata else: for name, uri in db.execute("SELECT name, uri FROM pyro_names").fetchall(): names[name] = uri return names except sqlite3.DatabaseError as e: raise NamingError("sqlite error in everything: " + str(e)) def close(self): pass @server.expose class NameServer(object): """ Pyro name server. Provides a simple flat name space to map logical object names to Pyro URIs. Default storage is done in an in-memory dictionary. You can provide custom storage types. """ def __init__(self, storageProvider=None): self.storage = storageProvider if storageProvider is None: self.storage = MemoryStorage() log.debug("using volatile in-memory dict storage") self.lock = threading.RLock() def count(self): """Returns the number of name registrations.""" return len(self.storage) def lookup(self, name, return_metadata=False): """ Lookup the given name, returns an URI if found. Returns tuple (uri, metadata) if return_metadata is True. """ try: uri, metadata = self.storage[name] uri = core.URI(uri) if return_metadata: return uri, set(metadata or []) return uri except KeyError: raise NamingError("unknown name: " + name) def register(self, name, uri, safe=False, metadata=None): """Register a name with an URI. If safe is true, name cannot be registered twice. The uri can be a string or an URI object. Metadata must be None, or a collection of strings.""" if isinstance(uri, core.URI): uri = str(uri) elif not isinstance(uri, str): raise TypeError("only URIs or strings can be registered") else: core.URI(uri) # check if uri is valid if not isinstance(name, str): raise TypeError("name must be a str") if isinstance(metadata, str): raise TypeError("metadata should not be a str, but another iterable (set, list, etc)") metadata and iter(metadata) # validate that metadata is iterable with self.lock: if safe and name in self.storage: raise NamingError("name already registered: " + name) self.storage[name] = uri, set(metadata) if metadata else None def set_metadata(self, name, metadata): """update the metadata for an existing registration""" if not isinstance(name, str): raise TypeError("name must be a str") if isinstance(metadata, str): raise TypeError("metadata should not be a str, but another iterable (set, list, etc)") metadata and iter(metadata) # validate that metadata is iterable with self.lock: try: uri, old_meta = self.storage[name] self.storage[name] = uri, set(metadata) if metadata else None except KeyError: raise NamingError("unknown name: " + name) def remove(self, name=None, prefix=None, regex=None): """Remove a registration. returns the number of items removed.""" if name and name in self.storage and name != core.NAMESERVER_NAME: with self.lock: del self.storage[name] return 1 if prefix: items = list(self.list(prefix=prefix).keys()) if core.NAMESERVER_NAME in items: items.remove(core.NAMESERVER_NAME) self.storage.remove_items(items) return len(items) if regex: items = list(self.list(regex=regex).keys()) if core.NAMESERVER_NAME in items: items.remove(core.NAMESERVER_NAME) self.storage.remove_items(items) return len(items) return 0 # noinspection PyNoneFunctionAssignment def list(self, prefix=None, regex=None, return_metadata=False): """ Retrieve the registered items as a dictionary name-to-URI. The URIs in the resulting dict are strings, not URI objects. You can filter by prefix or by regex. """ if prefix and regex: raise ValueError("you can only filter on one thing at a time") with self.lock: if prefix: result = self.storage.optimized_prefix_list(prefix, return_metadata) if result is not None: return result result = {} for name in self.storage: if name.startswith(prefix): result[name] = self.storage[name] if return_metadata else self.storage[name][0] return result elif regex: result = self.storage.optimized_regex_list(regex, return_metadata) if result is not None: return result result = {} try: regex = re.compile(regex) except re.error as x: raise errors.NamingError("invalid regex: " + str(x)) else: for name in self.storage: if regex.match(name): result[name] = self.storage[name] if return_metadata else self.storage[name][0] return result else: # just return (a copy of) everything return self.storage.everything(return_metadata) # noinspection PyNoneFunctionAssignment def yplookup(self, meta_all=None, meta_any=None, return_metadata=True): """ Do a yellow-pages lookup for registrations that have all or any of the given metadata tags. By default returns the actual metadata in the result as well. """ if meta_all and meta_any: raise ValueError("you can't use meta_all or meta_any at the same time") with self.lock: if meta_all: # return the entries which have all of the given metadata as (a subset of) their metadata if isinstance(meta_all, str): raise TypeError("metadata_all should not be a str, but another iterable (set, list, etc)") meta_all and iter(meta_all) # validate that metadata is iterable result = self.storage.optimized_metadata_search(metadata_all=meta_all, return_metadata=return_metadata) if result is not None: return result meta_all = frozenset(meta_all) result = {} for name, (uri, meta) in self.storage.everything(return_metadata=True).items(): if meta_all.issubset(meta): result[name] = (uri, meta) if return_metadata else uri return result elif meta_any: # return the entries which have any of the given metadata as part of their metadata if isinstance(meta_any, str): raise TypeError("metadata_any should not be a str, but another iterable (set, list, etc)") meta_any and iter(meta_any) # validate that metadata is iterable result = self.storage.optimized_metadata_search(metadata_any=meta_any, return_metadata=return_metadata) if result is not None: return result meta_any = frozenset(meta_any) result = {} for name, (uri, meta) in self.storage.everything(return_metadata=True).items(): if meta_any & meta: result[name] = (uri, meta) if return_metadata else uri return result else: return {} def ping(self): """A simple test method to check if the name server is running correctly.""" pass class NameServerDaemon(server.Daemon): """Daemon that contains the Name Server.""" def __init__(self, host=None, port=None, unixsocket=None, nathost=None, natport=None, storage=None): if host is None: host = config.HOST elif not isinstance(host, str): host = str(host) # take care of the occasion where host is an ipaddress.IpAddress if port is None: port = config.NS_PORT if nathost is None: nathost = config.NATHOST elif not isinstance(nathost, str): nathost = str(nathost) # take care of the occasion where host is an ipaddress.IpAddress if natport is None: natport = config.NATPORT or None storage = storage or "memory" if storage == "memory": log.debug("using volatile in-memory dict storage") self.nameserver = NameServer(MemoryStorage()) elif storage.startswith("sql:") and len(storage) > 4: sqlfile = storage[4:] log.debug("using persistent sql storage in file %s", sqlfile) self.nameserver = NameServer(SqlStorage(sqlfile)) else: raise ValueError("invalid storage type '%s'" % storage) existing_count = self.nameserver.count() if existing_count > 0: log.debug("number of existing entries in storage: %d", existing_count) super(NameServerDaemon, self).__init__(host, port, unixsocket, nathost=nathost, natport=natport) self.register(self.nameserver, core.NAMESERVER_NAME) metadata = {"class:Pyro5.nameserver.NameServer"} self.nameserver.register(core.NAMESERVER_NAME, self.uriFor(self.nameserver), metadata=metadata) if config.NS_AUTOCLEAN > 0: if not AutoCleaner.override_autoclean_min and config.NS_AUTOCLEAN < AutoCleaner.min_autoclean_value: raise ValueError("NS_AUTOCLEAN cannot be smaller than " + str(AutoCleaner.min_autoclean_value)) log.debug("autoclean enabled") self.cleaner_thread = AutoCleaner(self.nameserver) self.cleaner_thread.start() else: log.debug("autoclean not enabled") self.cleaner_thread = None log.info("nameserver daemon created") def close(self): super(NameServerDaemon, self).close() if self.nameserver is not None: self.nameserver.storage.close() self.nameserver = None if self.cleaner_thread: self.cleaner_thread.stop = True self.cleaner_thread.join() self.cleaner_thread = None def __enter__(self): if not self.nameserver: raise PyroError("cannot reuse this object") return self def __exit__(self, exc_type, exc_value, traceback): if self.nameserver is not None: self.nameserver.storage.close() self.nameserver = None if self.cleaner_thread: self.cleaner_thread.stop = True self.cleaner_thread.join() self.cleaner_thread = None return super(NameServerDaemon, self).__exit__(exc_type, exc_value, traceback) def handleRequest(self, conn): try: return super(NameServerDaemon, self).handleRequest(conn) except ProtocolError as x: # Notify the user that a protocol error occurred. # This is useful for instance when a wrong serializer is used, it helps # a lot to immediately see what is going wrong. warnings.warn("Pyro protocol error occurred: " + str(x)) raise class AutoCleaner(threading.Thread): """ Takes care of checking every registration in the name server. If it cannot be contacted anymore, it will be removed after ~20 seconds. """ min_autoclean_value = 3 max_unreachable_time = 20.0 loop_delay = 2.0 override_autoclean_min = False # only for unit test purposes def __init__(self, nameserver): assert config.NS_AUTOCLEAN > 0 if not self.override_autoclean_min and config.NS_AUTOCLEAN < self.min_autoclean_value: raise ValueError("NS_AUTOCLEAN cannot be smaller than " + str(self.min_autoclean_value)) super(AutoCleaner, self).__init__() self.nameserver = nameserver self.stop = False self.daemon = True self.last_cleaned = time.time() self.unreachable = {} # name->since when def run(self): while not self.stop: time.sleep(self.loop_delay) time_since_last_autoclean = time.time() - self.last_cleaned if time_since_last_autoclean < config.NS_AUTOCLEAN: continue for name, uri in self.nameserver.list().items(): if name in (core.DAEMON_NAME, core.NAMESERVER_NAME): continue try: uri_obj = core.URI(uri) timeout = config.COMMTIMEOUT or 5 sock = socketutil.create_socket(connect=(uri_obj.host, uri_obj.port), timeout=timeout) sock.close() # if we get here, the listed server is still answering on its port if name in self.unreachable: del self.unreachable[name] except socket.error: if name not in self.unreachable: self.unreachable[name] = time.time() if time.time() - self.unreachable[name] >= self.max_unreachable_time: log.info("autoclean: unregistering %s; cannot connect uri %s for %d sec", name, uri, self.max_unreachable_time) self.nameserver.remove(name) del self.unreachable[name] continue self.last_cleaned = time.time() if self.unreachable: log.debug("autoclean: %d/%d names currently unreachable", len(self.unreachable), self.nameserver.count()) class BroadcastServer(object): class TransportServerAdapter(object): # this adapter is used to be able to pass the BroadcastServer to Daemon.combine() to integrate the event loops. def __init__(self, bcserver): self.sockets = [bcserver] def events(self, eventobjects): for bc in eventobjects: bc.processRequest() def __init__(self, nsUri, bchost=None, bcport=None, ipv6=False): self.transportServer = self.TransportServerAdapter(self) self.nsUri = nsUri if bcport is None: bcport = config.NS_BCPORT if bchost is None: bchost = config.NS_BCHOST elif not isinstance(bchost, str): bchost = str(bchost) # take care of the occasion where host is an ipaddress.IpAddress if ":" in nsUri.host or ipv6: # match nameserver's ip version bchost = bchost or "::" self.sock = socketutil.create_bc_socket((bchost, bcport, 0, 0), reuseaddr=config.SOCK_REUSE, timeout=2.0) else: self.sock = socketutil.create_bc_socket((bchost, bcport), reuseaddr=config.SOCK_REUSE, timeout=2.0) self._sockaddr = self.sock.getsockname() bchost = bchost or self._sockaddr[0] bcport = bcport or self._sockaddr[1] if ":" in bchost: # ipv6 self.locationStr = "[%s]:%d" % (bchost, bcport) else: self.locationStr = "%s:%d" % (bchost, bcport) log.info("ns broadcast server created on %s - %s", self.locationStr, socketutil.family_str(self.sock)) self.running = True def close(self): log.debug("ns broadcast server closing") self.running = False with contextlib.suppress(OSError, socket.error): self.sock.shutdown(socket.SHUT_RDWR) self.sock.close() def getPort(self): return self.sock.getsockname()[1] def fileno(self): return self.sock.fileno() def runInThread(self): """Run the broadcast server loop in its own thread.""" thread = threading.Thread(target=self.__requestLoop) thread.daemon = True thread.start() log.debug("broadcast server loop running in own thread") return thread def __requestLoop(self): while self.running: self.processRequest() log.debug("broadcast server loop terminating") def processRequest(self): with contextlib.suppress(socket.error): data, addr = self.sock.recvfrom(100) if data == b"GET_NSURI": responsedata = core.URI(self.nsUri) if responsedata.host == "0.0.0.0": # replace INADDR_ANY address by the interface IP address that connects to the requesting client with contextlib.suppress(socket.error): interface_ip = socketutil.get_interface(addr[0]).ip responsedata.host = str(interface_ip) log.debug("responding to broadcast request from %s: interface %s", addr[0], responsedata.host) responsedata = str(responsedata).encode("iso-8859-1") self.sock.sendto(responsedata, 0, addr) def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.close() def start_ns_loop(host=None, port=None, enableBroadcast=True, bchost=None, bcport=None, unixsocket=None, nathost=None, natport=None, storage=None): """utility function that starts a new Name server and enters its requestloop.""" daemon = NameServerDaemon(host, port, unixsocket, nathost=nathost, natport=natport, storage=storage) nsUri = daemon.uriFor(daemon.nameserver) internalUri = daemon.uriFor(daemon.nameserver, nat=False) bcserver = None if unixsocket: hostip = "Unix domain socket" else: hostip = daemon.sock.getsockname()[0] if daemon.sock.family == socket.AF_INET6: # ipv6 doesn't have broadcast. We should probably use multicast instead... print("Not starting broadcast server for IPv6.") log.info("Not starting NS broadcast server because NS is using IPv6") enableBroadcast = False elif hostip.startswith("127.") or hostip in ("localhost", "::1"): print("Not starting broadcast server for localhost.") log.info("Not starting NS broadcast server because NS is bound to localhost") enableBroadcast = False if enableBroadcast: # Make sure to pass the internal uri to the broadcast responder. # It is almost always useless to let it return the external uri, # because external systems won't be able to talk to this thing anyway. bcserver = BroadcastServer(internalUri, bchost, bcport, ipv6=daemon.sock.family == socket.AF_INET6) print("Broadcast server running on %s" % bcserver.locationStr) bcserver.runInThread() existing = daemon.nameserver.count() if existing > 1: # don't count our own nameserver registration print("Persistent store contains %d existing registrations." % existing) print("NS running on %s (%s)" % (daemon.locationStr, hostip)) if daemon.natLocationStr: print("internal URI = %s" % internalUri) print("external URI = %s" % nsUri) else: print("URI = %s" % nsUri) sys.stdout.flush() try: daemon.requestLoop() finally: daemon.close() if bcserver is not None: bcserver.close() print("NS shut down.") def start_ns(host=None, port=None, enableBroadcast=True, bchost=None, bcport=None, unixsocket=None, nathost=None, natport=None, storage=None): """utility fuction to quickly get a Name server daemon to be used in your own event loops. Returns (nameserverUri, nameserverDaemon, broadcastServer).""" daemon = NameServerDaemon(host, port, unixsocket, nathost=nathost, natport=natport, storage=storage) bcserver = None nsUri = daemon.uriFor(daemon.nameserver) if not unixsocket: hostip = daemon.sock.getsockname()[0] if hostip.startswith("127.") or hostip in ("localhost", "::1"): # not starting broadcast server for localhost. enableBroadcast = False if enableBroadcast: internalUri = daemon.uriFor(daemon.nameserver, nat=False) bcserver = BroadcastServer(internalUri, bchost, bcport, ipv6=daemon.sock.family == socket.AF_INET6) return nsUri, daemon, bcserver def lookup(nameserver, name, return_metadata=False, delay_time=0): """ Utility function to call nameserver.lookup, with the possibility of a retry loop until the asked name becomes available. You have to set the delay_time (or the corresponding config item) to the maximum number of seconds you are willing to wait. """ delay_time = delay_time or config.NS_LOOKUP_DELAY start = time.time() while time.time()-start <= delay_time: try: return nameserver.lookup(name, return_metadata) except (errors.NamingError, errors.TimeoutError) as x: pass time.sleep(max(0.2, delay_time / 5)) return nameserver.lookup(name, return_metadata) def yplookup(nameserver, meta_all=None, meta_any=None, return_metadata=True, delay_time=0): """ Utility function to call nameserver.yplookup, with the possibility of a retry loop until the asked name becomes available. You have to set the delay_time (or the corresponding config item) to the maximum number of seconds you are willing to wait. """ delay_time = delay_time or config.NS_LOOKUP_DELAY start = time.time() while time.time()-start <= delay_time: try: result = nameserver.yplookup(meta_all, meta_any, return_metadata) if result: return result except (errors.NamingError, errors.TimeoutError) as x: pass time.sleep(max(0.2, delay_time / 5)) return nameserver.yplookup(meta_all, meta_any, return_metadata) def main(args=None): from argparse import ArgumentParser parser = ArgumentParser(description="Pyro name server command line launcher.") parser.add_argument("-n", "--host", dest="host", help="hostname to bind server on") parser.add_argument("-p", "--port", dest="port", type=int, help="port to bind server on (0=random)") parser.add_argument("-u", "--unixsocket", help="Unix domain socket name to bind server on") parser.add_argument("-s", "--storage", help="Storage system to use (memory, sql:file)", default="memory") parser.add_argument("--bchost", dest="bchost", help="hostname to bind broadcast server on (default is \"\")") parser.add_argument("--bcport", dest="bcport", type=int, help="port to bind broadcast server on (0=random)") parser.add_argument("--nathost", dest="nathost", help="external hostname in case of NAT") parser.add_argument("--natport", dest="natport", type=int, help="external port in case of NAT") parser.add_argument("-x", "--nobc", dest="enablebc", action="store_false", default=True, help="don't start a broadcast server") options = parser.parse_args(args) start_ns_loop(options.host, options.port, enableBroadcast=options.enablebc, bchost=options.bchost, bcport=options.bcport, unixsocket=options.unixsocket, nathost=options.nathost, natport=options.natport, storage=options.storage) if __name__ == "__main__": main() Pyro5-5.15/Pyro5/nsc.py000066400000000000000000000112351451404116400146370ustar00rootroot00000000000000""" Name server control tool. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ from . import errors, core def handle_command(namesrv, cmd, args): def print_list_result(resultdict, title=""): print("--------START LIST %s" % title) for name, (uri, metadata) in sorted(resultdict.items()): print("%s --> %s" % (name, uri)) if metadata: print(" metadata:", metadata) print("--------END LIST %s" % title) def cmd_ping(): namesrv.ping() print("Name server ping ok.") def cmd_listprefix(): if len(args) == 0: print_list_result(namesrv.list(return_metadata=True)) else: print_list_result(namesrv.list(prefix=args[0], return_metadata=True), "- prefix '%s'" % args[0]) def cmd_listregex(): if len(args) != 1: raise SystemExit("requires one argument: pattern") print_list_result(namesrv.list(regex=args[0], return_metadata=True), "- regex '%s'" % args[0]) def cmd_lookup(): if len(args) != 1: raise SystemExit("requires one argument: name") uri, metadata = namesrv.lookup(args[0], return_metadata=True) print(uri) if metadata: print("metadata:", metadata) def cmd_register(): if len(args) != 2: raise SystemExit("requires two arguments: name uri") namesrv.register(args[0], args[1], safe=True) print("Registered %s" % args[0]) def cmd_remove(): if len(args) != 1: raise SystemExit("requires one argument: name") count = namesrv.remove(args[0]) if count > 0: print("Removed %s" % args[0]) else: print("Nothing removed") def cmd_removeregex(): if len(args) != 1: raise SystemExit("requires one argument: pattern") sure = input("Potentially removing lots of items from the Name server. Are you sure (y/n)?").strip() if sure in ('y', 'Y'): count = namesrv.remove(regex=args[0]) print("%d items removed." % count) def cmd_setmeta(): if len(args) < 1: raise SystemExit("requires arguments: uri and zero or more meta tags") metadata = set(args[1:]) namesrv.set_metadata(args[0], metadata) if metadata: print("Metadata updated") else: print("Metadata cleared") def cmd_yplookup_all(): if len(args) < 1: raise SystemExit("requires at least one metadata tag argument") print_list_result(namesrv.yplookup(meta_all=args, return_metadata=True), " - searched by metadata") def cmd_yplookup_any(): if len(args) < 1: raise SystemExit("requires at least one metadata tag argument") print_list_result(namesrv.yplookup(meta_any=args, return_metadata=True), " - searched by metadata") commands = { "ping": cmd_ping, "list": cmd_listprefix, "listmatching": cmd_listregex, "yplookup_all": cmd_yplookup_all, "yplookup_any": cmd_yplookup_any, "lookup": cmd_lookup, "register": cmd_register, "remove": cmd_remove, "removematching": cmd_removeregex, "setmeta": cmd_setmeta } try: commands[cmd]() except Exception as x: print("Error: %s - %s" % (type(x).__name__, x)) raise def main(args=None): from argparse import ArgumentParser parser = ArgumentParser(description="Pyro name server control utility.") parser.add_argument("-n", "--host", dest="host", help="hostname of the NS", default="") parser.add_argument("-p", "--port", dest="port", type=int, help="port of the NS (or bc-port if host isn't specified)") parser.add_argument("-u", "--unixsocket", help="Unix domain socket name of the NS") parser.add_argument("-v", "--verbose", action="store_true", dest="verbose", help="verbose output") parser.add_argument("command", choices=("list", "lookup", "register", "remove", "removematching", "listmatching", "yplookup_all", "yplookup_any", "setmeta", "ping")) options, unknown_args = parser.parse_known_args(args) if options.verbose: print("Locating name server...") if options.unixsocket: options.host = "./u:" + options.unixsocket try: namesrv = core.locate_ns(options.host, options.port) except errors.PyroError as x: print("Error:", x) return if options.verbose: print("Name server found:", namesrv._pyroUri) handle_command(namesrv, options.command, unknown_args) if options.verbose: print("Done.") if __name__ == "__main__": main() Pyro5-5.15/Pyro5/protocol.py000066400000000000000000000212251451404116400157150ustar00rootroot00000000000000""" The pyro wire protocol structures. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). Wire messages contains of a fixed size header, an optional set of annotation chunks, and then the payload data. This class doesn't deal with the payload data: (de)serialization and handling of that data is done elsewhere. Annotation chunks are only parsed. The header format is:: 0x00 4s 4 'PYRO' (message identifier) 0x04 H 2 protocol version 0x06 B 1 message type 0x07 B 1 serializer id 0x08 H 2 message flags 0x0a H 2 sequence number (to identify proper request-reply sequencing) 0x0c I 4 data length (max 4 Gb) 0x10 I 4 annotations length (max 4 Gb, total of all chunks, 0 if no annotation chunks present) 0x14 16s 16 correlation uuid 0x24 H 2 (reserved) 0x26 H 2 magic number 0x4dc5 total size: 0x28 (40 bytes) After the header, zero or more annotation chunks may follow, of the format:: 4s 4 annotation id (4 ASCII letters) I 4 chunk length (max 4 Gb) B x annotation chunk databytes After that, the actual payload data bytes follow. """ import struct import logging import zlib import uuid from . import config, errors from .callcontext import current_context log = logging.getLogger("Pyro5.protocol") MSG_CONNECT = 1 MSG_CONNECTOK = 2 MSG_CONNECTFAIL = 3 MSG_INVOKE = 4 MSG_RESULT = 5 MSG_PING = 6 FLAGS_EXCEPTION = 1 << 0 FLAGS_COMPRESSED = 1 << 1 # compress the data, but not the annotations (if you need that, do it yourself) FLAGS_ONEWAY = 1 << 2 FLAGS_BATCH = 1 << 3 FLAGS_ITEMSTREAMRESULT = 1 << 4 FLAGS_KEEPSERIALIZED = 1 << 5 FLAGS_CORR_ID = 1 << 6 # wire protocol version. Note that if this gets updated, Pyrolite might need an update too. PROTOCOL_VERSION = 502 _magic_number = 0x4dc5 _header_format = '!4sHBBHHII16sHH' _header_size = struct.calcsize(_header_format) _magic_number_bytes = _magic_number.to_bytes(2, "big") _protocol_version_bytes = PROTOCOL_VERSION.to_bytes(2, "big") _empty_correlation_id = b"\0" * 16 class SendingMessage: """Wire protocol message that will be sent.""" def __init__(self, msgtype, flags, seq, serializer_id, payload, annotations=None): self.type = msgtype self.seq = seq self.serializer_id = serializer_id annotations = annotations or {} annotations_size = sum([8 + len(v) for v in annotations.values()]) flags &= ~FLAGS_COMPRESSED if config.COMPRESSION and len(payload) > 100: payload = zlib.compress(payload, 4) flags |= FLAGS_COMPRESSED self.flags = flags total_size = len(payload) + annotations_size if total_size > config.MAX_MESSAGE_SIZE: raise errors.ProtocolError("message too large ({:d}, max={:d})".format(total_size, config.MAX_MESSAGE_SIZE)) if current_context.correlation_id: flags |= FLAGS_CORR_ID self.corr_id = current_context.correlation_id.bytes else: self.corr_id = _empty_correlation_id header_data = struct.pack(_header_format, b"PYRO", PROTOCOL_VERSION, msgtype, serializer_id, flags, seq, len(payload), annotations_size, self.corr_id, 0, _magic_number) annotation_data = [] for k, v in annotations.items(): if len(k) != 4: raise errors.ProtocolError("annotation identifier must be 4 ascii characters") annotation_data.append(struct.pack("!4sI", k.encode("ascii"), len(v))) if not isinstance(v, (bytes, bytearray, memoryview)): raise errors.ProtocolError("annotation data must be bytes, bytearray, or memoryview", type(v)) annotation_data.append(v) # note: annotations are not compressed by Pyro self.data = header_data + b"".join(annotation_data) + payload def __repr__(self): return "<{:s}.{:s} at 0x{:x}; type={:d} flags={:d} seq={:d} size={:d}>" \ .format(self.__module__, self.__class__.__name__, id(self), self.type, self.flags, self.seq, len(self.data)) @staticmethod def ping(pyroConnection): """Convenience method to send a 'ping' message and wait for the 'pong' response""" ping = SendingMessage(MSG_PING, 0, 0, 42, b"ping") pyroConnection.send(ping.data) recv_stub(pyroConnection, [MSG_PING]) class ReceivingMessage: """Wire protocol message that was received.""" def __init__(self, header, payload=None): """Parses a message from the given header.""" tag, ver, self.type, self.serializer_id, self.flags, self.seq, self.data_size, \ self.annotations_size, self.corr_id, _, magic = struct.unpack(_header_format, header) if tag != b"PYRO" or ver != PROTOCOL_VERSION or magic != _magic_number: raise errors.ProtocolError("invalid message or protocol version") if self.data_size+self.annotations_size > config.MAX_MESSAGE_SIZE: raise errors.ProtocolError("message too large ({:d}, max={:d})" .format(self.data_size+self.annotations_size, config.MAX_MESSAGE_SIZE)) self.data = None self.annotations = {} if payload is not None: self.add_payload(payload) def __repr__(self): return "<{:s}.{:s} at 0x{:x}; type={:d} flags={:d} seq={:d} size={:d}>" \ .format(self.__module__, self.__class__.__name__, id(self), self.type, self.flags, self.seq, len(self.data or "")) @staticmethod def validate(data): """Checks if the message data looks like a valid Pyro message, if not, raise an error.""" ld = len(data) if ld < 4: raise ValueError("data must be at least 4 bytes to be able to identify") if not data.startswith(b"PYRO"): raise errors.ProtocolError("invalid data") if ld >= 6 and data[4:6] != _protocol_version_bytes: raise errors.ProtocolError("invalid protocol version: {:d}".format(int.from_bytes(data[4:6], "big"))) if ld >= _header_size and data[38:40] != _magic_number_bytes: raise errors.ProtocolError("invalid magic number") def add_payload(self, payload): """Parses (annotations processing) and adds payload data to a received message.""" assert not self.data if len(payload) != self.data_size + self.annotations_size: raise errors.ProtocolError("payload length doesn't match message header") if self.annotations_size: payload = memoryview(payload) # avoid copying self.annotations = {} i = 0 while i < self.annotations_size: annotation_id = bytes(payload[i:i+4]).decode("ascii") length = int.from_bytes(payload[i+4:i+8], "big") self.annotations[annotation_id] = payload[i+8:i+8+length] # note: it stores a memoryview! i += 8 + length assert i == self.annotations_size self.data = payload[self.annotations_size:] else: self.data = payload if self.flags & FLAGS_COMPRESSED: self.data = zlib.decompress(self.data) self.flags &= ~FLAGS_COMPRESSED self.data_size = len(self.data) def log_wiredata(logger, text, msg): """logs all the given properties of the wire message in the given logger""" num_anns = len(msg.annotations) if hasattr(msg, "annotations") else 0 corr_bytes = bytes(msg.corr_id) if hasattr(msg, "corr_id") else _empty_correlation_id corr_id = uuid.UUID(bytes=corr_bytes) logger.debug("%s: msgtype=%d flags=0x%x ser=%d seq=%d num_annotations=%s corr_id=%s\ndata=%r" % (text, msg.type, msg.flags, msg.serializer_id, msg.seq, num_anns, corr_id, bytes(msg.data))) def recv_stub(connection, accepted_msgtypes=None): """ Receives a pyro message from a given connection. Accepts the given message types (None=any, or pass a sequence). Also reads annotation chunks and the actual payload data. """ # TODO decouple i/o from actual protocol logic, so that the protocol can be easily unit tested header = connection.recv(6) # 'PYRO' + 2 bytes protocol version ReceivingMessage.validate(header) header += connection.recv(_header_size - 6) msg = ReceivingMessage(header) if accepted_msgtypes and msg.type not in accepted_msgtypes: err = "invalid msg type {:d} received (expected: {:s})".format(msg.type, ",".join(str(t) for t in accepted_msgtypes)) log.error(err) exc = errors.ProtocolError(err) exc.pyroMsg = msg raise exc payload = connection.recv(msg.annotations_size + msg.data_size) msg.add_payload(payload) return msg Pyro5-5.15/Pyro5/serializers.py000066400000000000000000000475411451404116400164210ustar00rootroot00000000000000""" The various serializers. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import array import builtins import uuid import logging import struct import datetime import decimal import numbers import inspect import marshal import json import serpent import contextlib try: import msgpack except ImportError: msgpack = None from . import errors, config __all__ = ["SerializerBase", "SerpentSerializer", "JsonSerializer", "MarshalSerializer", "MsgpackSerializer", "serializers", "serializers_by_id"] log = logging.getLogger("Pyro5.serializers") all_exceptions = {} for name, t in vars(builtins).items(): if type(t) is type and issubclass(t, BaseException): all_exceptions[name] = t for name, t in vars(errors).items(): if type(t) is type and issubclass(t, errors.PyroError): all_exceptions[name] = t def pyro_class_serpent_serializer(obj, serializer, stream, level): # Override the default way that a Pyro URI/proxy/daemon is serialized. # Because it defines a __getstate__ it would otherwise just become a tuple, # and not be deserialized as a class. d = SerializerBase.class_to_dict(obj) serializer.ser_builtins_dict(d, stream, level) def serialize_pyro_object_to_dict(obj): return { "__class__": "{:s}.{:s}".format(obj.__module__, obj.__class__.__name__), "state": obj.__getstate__() } class SerializerBase(object): """Base class for (de)serializer implementations (which must be thread safe)""" serializer_id = 0 # define uniquely in subclass __custom_class_to_dict_registry = {} __custom_dict_to_class_registry = {} def loads(self, data): raise NotImplementedError("implement in subclass") def loadsCall(self, data): raise NotImplementedError("implement in subclass") def dumps(self, data): raise NotImplementedError("implement in subclass") def dumpsCall(self, obj, method, vargs, kwargs): raise NotImplementedError("implement in subclass") @classmethod def register_type_replacement(cls, object_type, replacement_function): raise NotImplementedError("implement in subclass") def _convertToBytes(self, data): if type(data) is bytearray: return bytes(data) if type(data) is memoryview: return data.tobytes() return data @classmethod def register_class_to_dict(cls, clazz, converter, serpent_too=True): """Registers a custom function that returns a dict representation of objects of the given class. The function is called with a single parameter; the object to be converted to a dict.""" cls.__custom_class_to_dict_registry[clazz] = converter if serpent_too: with contextlib.suppress(errors.ProtocolError): def serpent_converter(obj, serializer, stream, level): d = converter(obj) serializer.ser_builtins_dict(d, stream, level) serpent.register_class(clazz, serpent_converter) @classmethod def unregister_class_to_dict(cls, clazz): """Removes the to-dict conversion function registered for the given class. Objects of the class will be serialized by the default mechanism again.""" if clazz in cls.__custom_class_to_dict_registry: del cls.__custom_class_to_dict_registry[clazz] with contextlib.suppress(errors.ProtocolError): serpent.unregister_class(clazz) @classmethod def register_dict_to_class(cls, classname, converter): """ Registers a custom converter function that creates objects from a dict with the given classname tag in it. The function is called with two parameters: the classname and the dictionary to convert to an instance of the class. """ cls.__custom_dict_to_class_registry[classname] = converter @classmethod def unregister_dict_to_class(cls, classname): """ Removes the converter registered for the given classname. Dicts with that classname tag will be deserialized by the default mechanism again. """ if classname in cls.__custom_dict_to_class_registry: del cls.__custom_dict_to_class_registry[classname] @classmethod def class_to_dict(cls, obj): """ Convert a non-serializable object to a dict. Partly borrowed from serpent. """ for clazz in cls.__custom_class_to_dict_registry: if isinstance(obj, clazz): return cls.__custom_class_to_dict_registry[clazz](obj) if type(obj) in (set, dict, tuple, list): # we use a ValueError to mirror the exception type returned by serpent and other serializers raise ValueError("can't serialize type " + str(obj.__class__) + " into a dict") if hasattr(obj, "_pyroDaemon"): obj._pyroDaemon = None if isinstance(obj, BaseException): # special case for exceptions return { "__class__": obj.__class__.__module__ + "." + obj.__class__.__name__, "__exception__": True, "args": obj.args, "attributes": vars(obj) # add custom exception attributes } # note: python 3.11+ object itself now has __getstate__ has_own_getstate = ( hasattr(type(obj), '__getstate__') and type(obj).__getstate__ is not getattr(object, '__getstate__', None) ) if has_own_getstate: value = obj.__getstate__() if isinstance(value, dict): return value try: value = dict(vars(obj)) # make sure we can serialize anything that resembles a dict value["__class__"] = obj.__class__.__module__ + "." + obj.__class__.__name__ return value except TypeError: if hasattr(obj, "__slots__"): # use the __slots__ instead of the vars dict value = {} for slot in obj.__slots__: value[slot] = getattr(obj, slot) value["__class__"] = obj.__class__.__module__ + "." + obj.__class__.__name__ return value else: raise errors.SerializeError("don't know how to serialize class " + str(obj.__class__) + " using serializer " + str(cls.__name__) + ". Give it vars() or an appropriate __getstate__") @classmethod def dict_to_class(cls, data): """ Recreate an object out of a dict containing the class name and the attributes. Only a fixed set of classes are recognized. """ from . import core, client, server # circular imports... classname = data.get("__class__", "") if isinstance(classname, bytes): classname = classname.decode("utf-8") if classname in cls.__custom_dict_to_class_registry: converter = cls.__custom_dict_to_class_registry[classname] return converter(classname, data) if "__" in classname: raise errors.SecurityError("refused to deserialize types with double underscores in their name: " + classname) # for performance reasons, the constructors below are hardcoded here # instead of added on a per-class basis to the dict-to-class registry if classname == "Pyro5.core.URI": uri = core.URI.__new__(core.URI) uri.__setstate__(data["state"]) return uri elif classname == "Pyro5.client.Proxy": proxy = client.Proxy.__new__(client.Proxy) proxy.__setstate__(data["state"]) return proxy elif classname == "Pyro5.server.Daemon": daemon = server.Daemon.__new__(server.Daemon) daemon.__setstate__(data["state"]) return daemon elif classname.startswith("Pyro5.util."): if classname == "Pyro5.util.SerpentSerializer": return SerpentSerializer() elif classname == "Pyro5.util.MarshalSerializer": return MarshalSerializer() elif classname == "Pyro5.util.JsonSerializer": return JsonSerializer() elif classname == "Pyro5.util.MsgpackSerializer": return MsgpackSerializer() elif classname.startswith("Pyro5.errors."): errortype = getattr(errors, classname.split('.', 2)[2]) if issubclass(errortype, errors.PyroError): return SerializerBase.make_exception(errortype, data) elif classname == "struct.error": return SerializerBase.make_exception(struct.error, data) elif classname == "Pyro5.core._ExceptionWrapper": ex = data["exception"] if isinstance(ex, dict) and "__class__" in ex: ex = SerializerBase.dict_to_class(ex) return core._ExceptionWrapper(ex) elif data.get("__exception__", False): if classname in all_exceptions: return SerializerBase.make_exception(all_exceptions[classname], data) # translate to the appropriate namespace... namespace, short_classname = classname.split('.', 1) if namespace in ("builtins", "exceptions"): exceptiontype = getattr(builtins, short_classname) if issubclass(exceptiontype, BaseException): return SerializerBase.make_exception(exceptiontype, data) elif namespace == "sqlite3" and short_classname.endswith("Error"): import sqlite3 exceptiontype = getattr(sqlite3, short_classname) if issubclass(exceptiontype, BaseException): return SerializerBase.make_exception(exceptiontype, data) log.warning("unsupported serialized class: " + classname) raise errors.SerializeError("unsupported serialized class: " + classname) @staticmethod def make_exception(exceptiontype, data): ex = exceptiontype(*data["args"]) if "attributes" in data: # restore custom attributes on the exception object for attr, value in data["attributes"].items(): setattr(ex, attr, value) return ex def recreate_classes(self, literal): t = type(literal) if t is set: return {self.recreate_classes(x) for x in literal} if t is list: return [self.recreate_classes(x) for x in literal] if t is tuple: return tuple(self.recreate_classes(x) for x in literal) if t is dict: if "__class__" in literal: return self.dict_to_class(literal) result = {} for key, value in literal.items(): result[key] = self.recreate_classes(value) return result return literal def __eq__(self, other): """this equality method is only to support the unit tests of this class""" return isinstance(other, SerializerBase) and vars(self) == vars(other) def __ne__(self, other): return not self.__eq__(other) __hash__ = object.__hash__ class SerpentSerializer(SerializerBase): """(de)serializer that wraps the serpent serialization protocol.""" serializer_id = 1 # never change this def dumpsCall(self, obj, method, vargs, kwargs): return serpent.dumps((obj, method, vargs, kwargs), module_in_classname=True, bytes_repr=config.SERPENT_BYTES_REPR) def dumps(self, data): return serpent.dumps(data, module_in_classname=True, bytes_repr=config.SERPENT_BYTES_REPR) def loadsCall(self, data): obj, method, vargs, kwargs = serpent.loads(data) vargs = self.recreate_classes(vargs) kwargs = self.recreate_classes(kwargs) return obj, method, vargs, kwargs def loads(self, data): return self.recreate_classes(serpent.loads(data)) @classmethod def register_type_replacement(cls, object_type, replacement_function): def custom_serializer(obj, serpent_serializer, outputstream, indentlevel): replaced = replacement_function(obj) if replaced is obj: serpent_serializer.ser_default_class(replaced, outputstream, indentlevel) else: serpent_serializer._serialize(replaced, outputstream, indentlevel) if object_type is type or not inspect.isclass(object_type): raise ValueError("refusing to register replacement for a non-type or the type 'type' itself") serpent.register_class(object_type, custom_serializer) @classmethod def dict_to_class(cls, data): if data.get("__class__") == "float": return float(data["value"]) # serpent encodes a float nan as a special class dict like this return super(SerpentSerializer, cls).dict_to_class(data) class MarshalSerializer(SerializerBase): """(de)serializer that wraps the marshal serialization protocol.""" serializer_id = 2 # never change this def dumpsCall(self, obj, method, vargs, kwargs): vargs = [self.convert_obj_into_marshallable(value) for value in vargs] kwargs = {key: self.convert_obj_into_marshallable(value) for key, value in kwargs.items()} return marshal.dumps((obj, method, vargs, kwargs)) def dumps(self, data): return marshal.dumps(self.convert_obj_into_marshallable(data)) def loadsCall(self, data): data = self._convertToBytes(data) obj, method, vargs, kwargs = marshal.loads(data) vargs = self.recreate_classes(vargs) kwargs = self.recreate_classes(kwargs) return obj, method, vargs, kwargs def loads(self, data): data = self._convertToBytes(data) return self.recreate_classes(marshal.loads(data)) def convert_obj_into_marshallable(self, obj): marshalable_types = (str, int, float, type(None), bool, complex, bytes, bytearray, tuple, set, frozenset, list, dict) if isinstance(obj, array.array): if obj.typecode == 'c': return obj.tostring() if obj.typecode == 'u': return obj.tounicode() return obj.tolist() if isinstance(obj, marshalable_types): return obj return self.class_to_dict(obj) @classmethod def class_to_dict(cls, obj): if isinstance(obj, uuid.UUID): return str(obj) return super(MarshalSerializer, cls).class_to_dict(obj) @classmethod def register_type_replacement(cls, object_type, replacement_function): pass # marshal serializer doesn't support per-type hooks class JsonSerializer(SerializerBase): """(de)serializer that wraps the json serialization protocol.""" serializer_id = 3 # never change this __type_replacements = {} def dumpsCall(self, obj, method, vargs, kwargs): data = {"object": obj, "method": method, "params": vargs, "kwargs": kwargs} data = json.dumps(data, ensure_ascii=False, default=self.default) return data.encode("utf-8") def dumps(self, data): data = json.dumps(data, ensure_ascii=False, default=self.default) return data.encode("utf-8") def loadsCall(self, data): data = self._convertToBytes(data).decode("utf-8") data = json.loads(data) vargs = self.recreate_classes(data["params"]) kwargs = self.recreate_classes(data["kwargs"]) return data["object"], data["method"], vargs, kwargs def loads(self, data): data = self._convertToBytes(data).decode("utf-8") return self.recreate_classes(json.loads(data)) def default(self, obj): replacer = self.__type_replacements.get(type(obj), None) if replacer: obj = replacer(obj) if isinstance(obj, set): return tuple(obj) # json module can't deal with sets so we make a tuple out of it if isinstance(obj, uuid.UUID): return str(obj) if isinstance(obj, (datetime.datetime, datetime.date)): return obj.isoformat() if isinstance(obj, decimal.Decimal): return str(obj) if isinstance(obj, array.array): if obj.typecode == 'c': return obj.tostring() if obj.typecode == 'u': return obj.tounicode() return obj.tolist() return self.class_to_dict(obj) @classmethod def register_type_replacement(cls, object_type, replacement_function): if object_type is type or not inspect.isclass(object_type): raise ValueError("refusing to register replacement for a non-type or the type 'type' itself") cls.__type_replacements[object_type] = replacement_function class MsgpackSerializer(SerializerBase): """(de)serializer that wraps the msgpack serialization protocol.""" serializer_id = 4 # never change this __type_replacements = {} def dumpsCall(self, obj, method, vargs, kwargs): return msgpack.packb((obj, method, vargs, kwargs), use_bin_type=True, default=self.default) def dumps(self, data): return msgpack.packb(data, use_bin_type=True, default=self.default) def loadsCall(self, data): return msgpack.unpackb(self._convertToBytes(data), raw=False, object_hook=self.object_hook) def loads(self, data): return msgpack.unpackb(self._convertToBytes(data), raw=False, object_hook=self.object_hook, ext_hook=self.ext_hook) def default(self, obj): replacer = self.__type_replacements.get(type(obj), None) if replacer: obj = replacer(obj) if isinstance(obj, set): return tuple(obj) # msgpack module can't deal with sets so we make a tuple out of it if isinstance(obj, uuid.UUID): return str(obj) if isinstance(obj, complex): return msgpack.ExtType(0x30, struct.pack("dd", obj.real, obj.imag)) if isinstance(obj, datetime.datetime): if obj.tzinfo: raise errors.SerializeError("msgpack cannot serialize datetime with timezone info") return msgpack.ExtType(0x32, struct.pack("d", obj.timestamp())) if isinstance(obj, datetime.date): return msgpack.ExtType(0x33, struct.pack("l", obj.toordinal())) if isinstance(obj, decimal.Decimal): return str(obj) if isinstance(obj, numbers.Number): return msgpack.ExtType(0x31, str(obj).encode("ascii")) # long if isinstance(obj, array.array): if obj.typecode == 'c': return obj.tostring() if obj.typecode == 'u': return obj.tounicode() return obj.tolist() return self.class_to_dict(obj) def object_hook(self, obj): if "__class__" in obj: return self.dict_to_class(obj) return obj def ext_hook(self, code, data): if code == 0x30: real, imag = struct.unpack("dd", data) return complex(real, imag) if code == 0x31: return int(data) if code == 0x32: return datetime.datetime.fromtimestamp(struct.unpack("d", data)[0]) if code == 0x33: return datetime.date.fromordinal(struct.unpack("l", data)[0]) raise errors.SerializeError("invalid ext code for msgpack: " + str(code)) @classmethod def register_type_replacement(cls, object_type, replacement_function): if object_type is type or not inspect.isclass(object_type): raise ValueError("refusing to register replacement for a non-type or the type 'type' itself") cls.__type_replacements[object_type] = replacement_function """The various serializers that are supported""" serializers = { "serpent": SerpentSerializer(), "marshal": MarshalSerializer(), "json": JsonSerializer() } if msgpack: serializers["msgpack"] = MsgpackSerializer() """The available serializers by their internal id""" serializers_by_id = {ser.serializer_id: ser for ser in serializers.values()} Pyro5-5.15/Pyro5/server.py000066400000000000000000001377121451404116400153730ustar00rootroot00000000000000""" Server related classes (Daemon etc) Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import os import sys import uuid import time import socket import collections import threading import logging import inspect import warnings import weakref import serpent import ipaddress from typing import TypeVar, Tuple, Union, Optional, Dict, Any, Sequence, Set from . import config, core, errors, serializers, socketutil, protocol, client from .callcontext import current_context from collections.abc import Callable __all__ = ["Daemon", "DaemonObject", "callback", "expose", "behavior", "oneway", "serve"] log = logging.getLogger("Pyro5.server") _private_dunder_methods = frozenset([ "__init__", "__init_subclass__", "__class__", "__module__", "__weakref__", "__call__", "__new__", "__del__", "__repr__", "__str__", "__format__", "__nonzero__", "__bool__", "__coerce__", "__cmp__", "__eq__", "__ne__", "__hash__", "__ge__", "__gt__", "__le__", "__lt__", "__dir__", "__enter__", "__exit__", "__copy__", "__deepcopy__", "__sizeof__", "__getattr__", "__setattr__", "__hasattr__", "__getattribute__", "__delattr__", "__instancecheck__", "__subclasscheck__", "__getinitargs__", "__getnewargs__", "__getstate__", "__setstate__", "__reduce__", "__reduce_ex__", "__subclasshook__" ]) def is_private_attribute(attr_name: str) -> bool: """returns if the attribute name is to be considered private or not.""" if attr_name in _private_dunder_methods: return True if not attr_name.startswith('_'): return False if len(attr_name) > 4 and attr_name.startswith("__") and attr_name.endswith("__"): return False return True # decorators def callback(method: Callable) -> Callable: """ decorator to mark a method to be a 'callback'. This will make Pyro raise any errors also on the callback side, and not only on the side that does the callback call. """ method._pyroCallback = True # type: ignore return method def oneway(method: Callable) -> Callable: """ decorator to mark a method to be oneway (client won't wait for a response) """ method._pyroOneway = True # type: ignore return method _T = TypeVar("_T", bound=Union[Callable, type]) def expose(method_or_class: _T) -> _T: """ Decorator to mark a method or class to be exposed for remote calls. You can apply it to a method or a class as a whole. If you need to change the default instance mode or instance creator, also use a @behavior decorator. """ if inspect.isdatadescriptor(method_or_class): func = method_or_class.fget or method_or_class.fset or method_or_class.fdel # type: ignore if is_private_attribute(func.__name__): raise AttributeError("exposing private names (starting with _) is not allowed") func._pyroExposed = True return method_or_class attrname = getattr(method_or_class, "__name__", None) if not attrname or isinstance(method_or_class, (classmethod, staticmethod)): # we could be dealing with a descriptor (classmethod/staticmethod), this means the order of the decorators is wrong if inspect.ismethoddescriptor(method_or_class): attrname = method_or_class.__get__(None, dict).__name__ # type: ignore raise AttributeError("using @expose on a classmethod/staticmethod must be done " "after @classmethod/@staticmethod. Method: " + attrname) else: raise AttributeError("@expose cannot determine what this is: "+repr(method_or_class)) if is_private_attribute(attrname): raise AttributeError("exposing private names (starting with _) is not allowed") if inspect.isclass(method_or_class): clazz = method_or_class log.debug("exposing all members of %r", clazz) for name in clazz.__dict__: if is_private_attribute(name): continue thing = getattr(clazz, name) if inspect.isfunction(thing) or inspect.ismethoddescriptor(thing): thing._pyroExposed = True elif inspect.ismethod(thing): thing.__func__._pyroExposed = True elif inspect.isdatadescriptor(thing): if getattr(thing, "fset", None): thing.fset._pyroExposed = True if getattr(thing, "fget", None): thing.fget._pyroExposed = True if getattr(thing, "fdel", None): thing.fdel._pyroExposed = True clazz._pyroExposed = True # type: ignore return clazz method_or_class._pyroExposed = True # type: ignore return method_or_class def behavior(instance_mode: str = "session", instance_creator: Optional[Callable] = None) -> Callable: """ Decorator to specify the server behavior of your Pyro class. """ def _behavior(clazz): if not inspect.isclass(clazz): raise TypeError("behavior decorator can only be used on a class") if instance_mode not in ("single", "session", "percall"): raise ValueError("invalid instance mode: " + instance_mode) if instance_creator and not callable(instance_creator): raise TypeError("instance_creator must be a callable") clazz._pyroInstancing = (instance_mode, instance_creator) return clazz if not isinstance(instance_mode, str): raise SyntaxError("behavior decorator is missing argument(s)") return _behavior @expose class DaemonObject(object): """The part of the daemon that is exposed as a Pyro object.""" def __init__(self, daemon): self.daemon = daemon def registered(self): """returns a list of all object names registered in this daemon""" return list(self.daemon.objectsById.keys()) def ping(self): """a simple do-nothing method for testing purposes""" pass def info(self): """return some descriptive information about the daemon""" return "%s bound on %s, NAT %s, %d objects registered. Servertype: %s" % ( core.DAEMON_NAME, self.daemon.locationStr, self.daemon.natLocationStr, len(self.daemon.objectsById), self.daemon.transportServer) def get_metadata(self, objectId): """ Get metadata for the given object (exposed methods, oneways, attributes). """ obj = _unpack_weakref(self.daemon.objectsById.get(objectId)) if obj is not None: metadata = _get_exposed_members(obj) if not metadata["methods"] and not metadata["attrs"]: # Something seems wrong: nothing is remotely exposed. warnings.warn("Class %r doesn't expose any methods or attributes. Did you forget setting @expose on them?" % type(obj)) return metadata else: log.debug("unknown object requested: %s", objectId) raise errors.DaemonError("unknown object") def get_next_stream_item(self, streamId): if streamId not in self.daemon.streaming_responses: raise errors.PyroError("item stream terminated") client, timestamp, linger_timestamp, stream = self.daemon.streaming_responses[streamId] if client is None: # reset client connection association (can be None if proxy disconnected) self.daemon.streaming_responses[streamId] = (current_context.client, timestamp, 0, stream) try: return next(stream) except Exception: # in case of error (or StopIteration!) the stream is removed del self.daemon.streaming_responses[streamId] raise def close_stream(self, streamId): if streamId in self.daemon.streaming_responses: del self.daemon.streaming_responses[streamId] class Daemon(object): """ Pyro daemon. Contains server side logic and dispatches incoming remote method calls to the appropriate objects. """ def __init__(self, host=None, port=0, unixsocket=None, nathost=None, natport=None, interface=DaemonObject, connected_socket=None): if connected_socket: nathost = natport = None else: if host is None: host = config.HOST elif not isinstance(host, str): host = str(host) # take care of the occasion where host is an ipaddress.IpAddress if nathost is None: nathost = config.NATHOST elif not isinstance(nathost, str): nathost = str(nathost) # take care of the occasion where host is an ipaddress.IpAddress if natport is None and nathost is not None: natport = config.NATPORT if nathost and unixsocket: raise ValueError("cannot use nathost together with unixsocket") if (nathost is None) ^ (natport is None): raise ValueError("must provide natport with nathost") self.__mustshutdown = threading.Event() self.__mustshutdown.set() self.__loopstopped = threading.Event() self.__loopstopped.set() if connected_socket: from .svr_existingconn import SocketServer_ExistingConnection self.transportServer = SocketServer_ExistingConnection() self.transportServer.init(self, connected_socket) else: if config.SERVERTYPE == "thread": from .svr_threads import SocketServer_Threadpool self.transportServer = SocketServer_Threadpool() elif config.SERVERTYPE == "multiplex": from .svr_multiplex import SocketServer_Multiplex self.transportServer = SocketServer_Multiplex() else: raise errors.PyroError("invalid server type '%s'" % config.SERVERTYPE) self.transportServer.init(self, host, port, unixsocket) #: The location (str of the form ``host:portnumber``) on which the Daemon is listening self.locationStr = self.transportServer.locationStr log.debug("daemon created on %s - %s (pid %d)", self.locationStr, socketutil.family_str(self.transportServer.sock), os.getpid()) natport_for_loc = natport if natport == 0: # expose internal port number as NAT port as well. (don't use port because it could be 0 and will be chosen by the OS) natport_for_loc = int(self.locationStr.split(":")[1]) # The NAT-location (str of the form ``nathost:natportnumber``) on which the Daemon is exposed for use with NAT-routing self.natLocationStr = "%s:%d" % (nathost, natport_for_loc) if nathost else None if self.natLocationStr: log.debug("NAT address is %s", self.natLocationStr) pyroObject = interface(self) pyroObject._pyroId = core.DAEMON_NAME # Dictionary from Pyro object id to the actual Pyro object registered by this id self.objectsById = {pyroObject._pyroId: pyroObject} log.debug("pyro protocol version: %d" % protocol.PROTOCOL_VERSION) self._pyroInstances = {} # pyro objects for instance_mode=single (singletons, just one per daemon) self.streaming_responses = {} # stream_id -> (client, creation_timestamp, linger_timestamp, stream) self.housekeeper_lock = threading.Lock() self.create_single_instance_lock = threading.Lock() self.__mustshutdown.clear() self.methodcall_error_handler = _default_methodcall_error_handler @property def sock(self): """the server socket used by the daemon""" return self.transportServer.sock @property def sockets(self): """list of all sockets used by the daemon (server socket and all active client sockets)""" return self.transportServer.sockets @property def selector(self): """the multiplexing selector used, if using the multiplex server type""" return self.transportServer.selector @staticmethod def serveSimple(objects, host=None, port=0, daemon=None, ns=True, verbose=True) -> None: """ Backwards compatibility method to fire up a daemon and start serving requests. New code should just use the global ``serve`` function instead. """ serve(objects, host, port, daemon, ns, verbose) def requestLoop(self, loopCondition=lambda: True) -> None: """ Goes in a loop to service incoming requests, until someone breaks this or calls shutdown from another thread. """ self.__mustshutdown.clear() log.info("daemon %s entering requestloop", self.locationStr) try: self.__loopstopped.clear() self.transportServer.loop(loopCondition=lambda: not self.__mustshutdown.is_set() and loopCondition()) finally: self.__loopstopped.set() log.debug("daemon exits requestloop") def events(self, eventsockets): """for use in an external event loop: handle any requests that are pending for this daemon""" return self.transportServer.events(eventsockets) def shutdown(self): """Cleanly terminate a daemon that is running in the requestloop.""" log.debug("daemon shutting down") self.streaming_responses = {} time.sleep(0.02) self.__mustshutdown.set() if self.transportServer: self.transportServer.shutdown() time.sleep(0.02) self.close() self.__loopstopped.wait(timeout=5) # use timeout to avoid deadlock situations @property def _shutting_down(self): return self.__mustshutdown.is_set() def _handshake(self, conn, denied_reason=None): """ Perform connection handshake with new clients. Client sends a MSG_CONNECT message with a serialized data payload. If all is well, return with a CONNECT_OK message. The reason we're not doing this with a MSG_INVOKE method call on the daemon (like when retrieving the metadata) is because we need to force the clients to get past an initial connect handshake before letting them invoke any method. Return True for successful handshake, False if something was wrong. If a denied_reason is given, the handshake will fail with the given reason. """ serializer_id = serializers.MarshalSerializer.serializer_id msg_seq = 0 try: msg = protocol.recv_stub(conn, [protocol.MSG_CONNECT]) msg_seq = msg.seq if denied_reason: raise Exception(denied_reason) if config.LOGWIRE: protocol.log_wiredata(log, "daemon handshake received", msg) if msg.flags & protocol.FLAGS_CORR_ID: current_context.correlation_id = uuid.UUID(bytes=msg.corr_id) else: current_context.correlation_id = uuid.uuid4() serializer_id = msg.serializer_id serializer = serializers.serializers_by_id[serializer_id] data = serializer.loads(msg.data) handshake_response = self.validateHandshake(conn, data["handshake"]) handshake_response = { "handshake": handshake_response, "meta": self.objectsById[core.DAEMON_NAME].get_metadata(data["object"]) } data = serializer.dumps(handshake_response) msgtype = protocol.MSG_CONNECTOK except errors.ConnectionClosedError: log.debug("handshake failed, connection closed early") return False except Exception as x: log.debug("handshake failed, reason:", exc_info=True) serializer = serializers.serializers_by_id[serializer_id] data = serializer.dumps(str(x)) msgtype = protocol.MSG_CONNECTFAIL # We need a minimal amount of response data or the socket will remain blocked # on some systems... (messages smaller than 40 bytes) msg = protocol.SendingMessage(msgtype, 0, msg_seq, serializer_id, data, annotations=self.__annotations()) if config.LOGWIRE: protocol.log_wiredata(log, "daemon handshake response", msg) conn.send(msg.data) return msg.type == protocol.MSG_CONNECTOK def validateHandshake(self, conn, data): """ Override this to create a connection validator for new client connections. It should return a response data object normally if the connection is okay, or should raise an exception if the connection should be denied. """ return "hello" def clientDisconnect(self, conn): """ Override this to handle a client disconnect. Conn is the SocketConnection object that was disconnected. """ pass def handleRequest(self, conn): """ Handle incoming Pyro request. Catches any exception that may occur and wraps it in a reply to the calling side, as to not make this server side loop terminate due to exceptions caused by remote invocations. """ request_flags = 0 request_seq = 0 request_serializer_id = serializers.MarshalSerializer.serializer_id wasBatched = False isCallback = False try: msg = protocol.recv_stub(conn, [protocol.MSG_INVOKE, protocol.MSG_PING]) except errors.CommunicationError as x: # we couldn't even get data from the client, this is an immediate error # log.info("error receiving data from client %s: %s", conn.sock.getpeername(), x) raise x try: request_flags = msg.flags request_seq = msg.seq request_serializer_id = msg.serializer_id if msg.flags & protocol.FLAGS_CORR_ID: current_context.correlation_id = uuid.UUID(bytes=msg.corr_id) else: current_context.correlation_id = uuid.uuid4() if config.LOGWIRE: protocol.log_wiredata(log, "daemon wiredata received", msg) if msg.type == protocol.MSG_PING: # return same seq, but ignore any data (it's a ping, not an echo). Nothing is deserialized. msg = protocol.SendingMessage(protocol.MSG_PING, 0, msg.seq, msg.serializer_id, b"pong", annotations=self.__annotations()) if config.LOGWIRE: protocol.log_wiredata(log, "daemon wiredata sending", msg) conn.send(msg.data) return serializer = serializers.serializers_by_id[msg.serializer_id] if request_flags & protocol.FLAGS_KEEPSERIALIZED: # pass on the wire protocol message blob unchanged objId, method, vargs, kwargs = self.__deserializeBlobArgs(msg) else: # normal deserialization of remote call arguments objId, method, vargs, kwargs = serializer.loadsCall(msg.data) current_context.client = conn try: # store, because on oneway calls, socket will be disconnected: current_context.client_sock_addr = conn.sock.getpeername() except socket.error: current_context.client_sock_addr = None # sometimes getpeername() doesn't work... current_context.seq = msg.seq current_context.annotations = msg.annotations current_context.msg_flags = msg.flags current_context.serializer_id = msg.serializer_id del msg # invite GC to collect the object, don't wait for out-of-scope obj = _unpack_weakref(self.objectsById.get(objId)) if obj is not None: if inspect.isclass(obj): obj = self._getInstance(obj, conn) if request_flags & protocol.FLAGS_BATCH: # batched method calls, loop over them all and collect all results data = [] for method, vargs, kwargs in vargs: method = _get_attribute(obj, method) try: result = method(*vargs, **kwargs) # this is the actual method call to the Pyro object except Exception as xv: self.methodcall_error_handler(self, current_context.client_sock_addr, method, vargs, kwargs, xv) xv._pyroTraceback = errors.format_traceback(detailed=config.DETAILED_TRACEBACK) data.append(core._ExceptionWrapper(xv)) break # stop processing the rest of the batch else: data.append(result) # note that we don't support streaming results in batch mode wasBatched = True else: # normal single method call if method == "__getattr__": # special case for direct attribute access (only exposed @properties are accessible) data = _get_exposed_property_value(obj, vargs[0]) elif method == "__setattr__": # special case for direct attribute access (only exposed @properties are accessible) data = _set_exposed_property_value(obj, vargs[0], vargs[1]) else: method = _get_attribute(obj, method) if request_flags & protocol.FLAGS_ONEWAY: # oneway call to be run inside its own thread, otherwise client blocking can still occur # on the next call on the same proxy _OnewayCallThread(method, vargs, kwargs, self, current_context.client_sock_addr).start() else: isCallback = getattr(method, "_pyroCallback", False) try: data = method(*vargs, **kwargs) # this is the actual method call to the Pyro object except Exception as xv: self.methodcall_error_handler(self, current_context.client_sock_addr, method, vargs, kwargs, xv) raise if not request_flags & protocol.FLAGS_ONEWAY: isStream, data = self._streamResponse(data, conn) if isStream: # throw an exception as well as setting message flags # this way, it is backwards compatible with older pyro versions. exc = errors.ProtocolError("result of call is an iterator") ann = {"STRM": data.encode()} if data else {} self._sendExceptionResponse(conn, request_seq, serializer.serializer_id, exc, None, annotations=ann, flags=protocol.FLAGS_ITEMSTREAMRESULT) return else: log.debug("unknown object requested: %s", objId) raise errors.DaemonError("unknown object") if request_flags & protocol.FLAGS_ONEWAY: return # oneway call, don't send a response else: data = serializer.dumps(data) response_flags = 0 if wasBatched: response_flags |= protocol.FLAGS_BATCH msg = protocol.SendingMessage(protocol.MSG_RESULT, response_flags, request_seq, serializer.serializer_id, data, annotations=self.__annotations()) current_context.response_annotations = {} if config.LOGWIRE: protocol.log_wiredata(log, "daemon wiredata sending", msg) conn.send(msg.data) except Exception as xv: msg = getattr(xv, "pyroMsg", None) if msg: request_seq = msg.seq request_serializer_id = msg.serializer_id if not isinstance(xv, errors.ConnectionClosedError): if not request_flags & protocol.FLAGS_ONEWAY: if isinstance(xv, errors.SerializeError) or not isinstance(xv, errors.CommunicationError): # only return the error to the client if it wasn't a oneway call, and not a communication error # (in these cases, it makes no sense to try to report the error back to the client...) tblines = errors.format_traceback(detailed=config.DETAILED_TRACEBACK) self._sendExceptionResponse(conn, request_seq, request_serializer_id, xv, tblines) if isCallback or isinstance(xv, (errors.CommunicationError, errors.SecurityError)): raise # re-raise if flagged as callback, communication or security error. def _clientDisconnect(self, conn): if config.ITER_STREAM_LINGER > 0: # client goes away, keep streams around for a bit longer (allow reconnect) for streamId in list(self.streaming_responses): info = self.streaming_responses.get(streamId, None) if info and info[0] is conn: _, timestamp, _, stream = info self.streaming_responses[streamId] = (None, timestamp, time.time(), stream) else: # client goes away, close any streams it had open as well for streamId in list(self.streaming_responses): info = self.streaming_responses.get(streamId, None) if info and info[0] is conn: del self.streaming_responses[streamId] self.clientDisconnect(conn) # user overridable hook def _housekeeping(self): """ Perform periodical housekeeping actions (cleanups etc) """ if self._shutting_down: return with self.housekeeper_lock: if self.streaming_responses: if config.ITER_STREAM_LIFETIME > 0: # cleanup iter streams that are past their lifetime for streamId in list(self.streaming_responses.keys()): info = self.streaming_responses.get(streamId, None) if info: last_use_period = time.time() - info[1] if 0 < config.ITER_STREAM_LIFETIME < last_use_period: del self.streaming_responses[streamId] if config.ITER_STREAM_LINGER > 0: # cleanup iter streams that are past their linger time for streamId in list(self.streaming_responses.keys()): info = self.streaming_responses.get(streamId, None) if info and info[2]: linger_period = time.time() - info[2] if linger_period > config.ITER_STREAM_LINGER: del self.streaming_responses[streamId] self.housekeeping() def housekeeping(self): """ Override this to add custom periodic housekeeping (cleanup) logic. This will be called every few seconds by the running daemon's request loop. """ pass def _getInstance(self, clazz, conn): """ Find or create a new instance of the class """ def createInstance(clazz, creator): try: if creator: obj = creator(clazz) if isinstance(obj, clazz): return obj raise TypeError("instance creator returned object of different type") return clazz() except Exception: log.exception("could not create pyro object instance") raise instance_mode, instance_creator = clazz._pyroInstancing if instance_mode == "single": # create and use one singleton instance of this class (not a global singleton, just exactly one per daemon) with self.create_single_instance_lock: instance = self._pyroInstances.get(clazz) if not instance: log.debug("instancemode %s: creating new pyro object for %s", instance_mode, clazz) instance = createInstance(clazz, instance_creator) self._pyroInstances[clazz] = instance return instance elif instance_mode == "session": # Create and use one instance for this proxy connection # the instances are kept on the connection object. # (this is the default instance mode when using new style @expose) instance = conn.pyroInstances.get(clazz) if not instance: log.debug("instancemode %s: creating new pyro object for %s", instance_mode, clazz) instance = createInstance(clazz, instance_creator) conn.pyroInstances[clazz] = instance return instance elif instance_mode == "percall": # create and use a new instance just for this call log.debug("instancemode %s: creating new pyro object for %s", instance_mode, clazz) return createInstance(clazz, instance_creator) else: raise errors.DaemonError("invalid instancemode in registered class") def _sendExceptionResponse(self, connection, seq, serializer_id, exc_value, tbinfo, flags=0, annotations=None): """send an exception back including the local traceback info""" exc_value._pyroTraceback = tbinfo serializer = serializers.serializers_by_id[serializer_id] try: data = serializer.dumps(exc_value) except Exception: # the exception object couldn't be serialized, use a generic PyroError instead xt, xv, tb = sys.exc_info() msg = "Error serializing exception: %s. Original exception: %s: %s" % (str(xv), type(exc_value), str(exc_value)) exc_value = errors.PyroError(msg) exc_value._pyroTraceback = tbinfo data = serializer.dumps(exc_value) flags |= protocol.FLAGS_EXCEPTION annotations = dict(annotations or {}) annotations.update(self.annotations()) msg = protocol.SendingMessage(protocol.MSG_RESULT, flags, seq, serializer.serializer_id, data, annotations=annotations) if config.LOGWIRE: protocol.log_wiredata(log, "daemon wiredata sending (error response)", msg) connection.send(msg.data) def register(self, obj_or_class, objectId=None, force=False, weak=False): """ Register a Pyro object under the given id. Note that this object is now only known inside this daemon, it is not automatically available in a name server. This method returns a URI for the registered object. Pyro checks if an object is already registered, unless you set force=True. You can register a class or an object (instance) directly. For a class, Pyro will create instances of it to handle the remote calls according to the instance_mode (set via @expose on the class). The default there is one object per session (=proxy connection). If you register an object directly, Pyro will use that single object for *all* remote calls. With *weak=True*, only weak reference to the object will be stored, and the object will get unregistered from the daemon automatically when garbage-collected. """ if objectId: if not isinstance(objectId, str): raise TypeError("objectId must be a string or None") else: objectId = "obj_" + uuid.uuid4().hex # generate a new objectId if inspect.isclass(obj_or_class): if weak: raise TypeError("Classes cannot be registered with weak=True.") if not hasattr(obj_or_class, "_pyroInstancing"): obj_or_class._pyroInstancing = ("session", None) if not force: if hasattr(obj_or_class, "_pyroId") and obj_or_class._pyroId != "": # check for empty string is needed for Cython raise errors.DaemonError("object or class already has a Pyro id") if objectId in self.objectsById: raise errors.DaemonError("an object or class is already registered with that id") # set some pyro attributes obj_or_class._pyroId = objectId obj_or_class._pyroDaemon = self # register a custom serializer for the type to automatically return proxies # we need to do this for all known serializers for ser in serializers.serializers.values(): if inspect.isclass(obj_or_class): ser.register_type_replacement(obj_or_class, _pyro_obj_to_auto_proxy) else: ser.register_type_replacement(type(obj_or_class), _pyro_obj_to_auto_proxy) # register the object/class in the mapping self.objectsById[obj_or_class._pyroId] = (obj_or_class if not weak else weakref.ref(obj_or_class)) if weak: weakref.finalize(obj_or_class,self.unregister,objectId) return self.uriFor(objectId) def unregister(self, objectOrId): """ Remove a class or object from the known objects inside this daemon. You can unregister the class/object directly, or with its id. """ if objectOrId is None: raise ValueError("object or objectid argument expected") if not isinstance(objectOrId, str): objectId = getattr(objectOrId, "_pyroId", None) if objectId is None: raise errors.DaemonError("object isn't registered") else: objectId = objectOrId objectOrId = None if objectId == core.DAEMON_NAME: return if objectId in self.objectsById: del self.objectsById[objectId] if objectOrId is not None: del objectOrId._pyroId del objectOrId._pyroDaemon # Don't remove the custom type serializer because there may be # other registered objects of the same type still depending on it. def uriFor(self, objectOrId, nat=True): """ Get a URI for the given object (or object id) from this daemon. Only a daemon can hand out proper uris because the access location is contained in them. Note that unregistered objects cannot be given an uri, but unregistered object names can (it's just a string we're creating in that case). If nat is set to False, the configured NAT address (if any) is ignored and it will return an URI for the internal address. """ if not isinstance(objectOrId, str): objectOrId = getattr(objectOrId, "_pyroId", None) if objectOrId is None or objectOrId not in self.objectsById: raise errors.DaemonError("object isn't registered in this daemon") if nat: loc = self.natLocationStr or self.locationStr else: loc = self.locationStr return core.URI("PYRO:%s@%s" % (objectOrId, loc)) def resetMetadataCache(self, objectOrId, nat=True): """Reset cache of metadata when a Daemon has available methods/attributes dynamically updated. Clients will have to get a new proxy to see changes""" uri = self.uriFor(objectOrId, nat) # can only be cached if registered, else no-op if uri.object in self.objectsById: registered_object = _unpack_weakref(self.objectsById[uri.object]) # Clear cache regardless of how it is accessed _reset_exposed_members(registered_object) def proxyFor(self, objectOrId, nat=True): """ Get a fully initialized Pyro Proxy for the given object (or object id) for this daemon. If nat is False, the configured NAT address (if any) is ignored. The object or id must be registered in this daemon, or you'll get an exception. (you can't get a proxy for an unknown object) """ uri = self.uriFor(objectOrId, nat) proxy = client.Proxy(uri) try: registered_object = _unpack_weakref(self.objectsById[uri.object]) except KeyError: raise errors.DaemonError("object isn't registered in this daemon") meta = _get_exposed_members(registered_object) proxy._pyroGetMetadata(known_metadata=meta) return proxy def close(self): """Close down the server and release resources""" self.__mustshutdown.set() self.streaming_responses = {} if self.transportServer: log.debug("daemon closing") self.transportServer.close() self.transportServer = None def annotations(self): """Override to return a dict with custom user annotations to be sent with each response message.""" return {} def combine(self, daemon): """ Combines the event loop of the other daemon in the current daemon's loop. You can then simply run the current daemon's requestLoop to serve both daemons. This works fine on the multiplex server type, but doesn't work with the threaded server type. """ log.debug("combining event loop with other daemon") self.transportServer.combine_loop(daemon.transportServer) def __annotations(self): annotations = current_context.response_annotations annotations.update(self.annotations()) return annotations def __repr__(self): if hasattr(self, "locationStr"): family = socketutil.family_str(self.sock) return "<%s.%s at 0x%x; %s - %s; %d objects>" % (self.__class__.__module__, self.__class__.__name__, id(self), self.locationStr, family, len(self.objectsById)) else: # daemon objects may come back from serialized form without being properly initialized (by design) return "<%s.%s at 0x%x; unusable>" % (self.__class__.__module__, self.__class__.__name__, id(self)) def __enter__(self): if not self.transportServer: raise errors.PyroError("cannot reuse this object") return self def __exit__(self, exc_type, exc_value, traceback): self.close() def __getstate__(self): # A little hack to make it possible to serialize Pyro objects, because they can reference a daemon, # but it is not meant to be able to properly serialize/deserialize Daemon objects. return tuple() def __setstate__(self, state): assert len(state) == 0 def _streamResponse(self, data, client): if isinstance(data, collections.abc.Iterator) or inspect.isgenerator(data): if config.ITER_STREAMING: if type(data) in (type({}.keys()), type({}.values()), type({}.items())): raise errors.PyroError("won't serialize or stream lazy dict iterators, convert to list yourself") stream_id = str(uuid.uuid4()) self.streaming_responses[stream_id] = (client, time.time(), 0, data) return True, stream_id return True, None return False, data def __deserializeBlobArgs(self, protocolmsg): import marshal blobinfo = protocolmsg.annotations["BLBI"] blobinfo, objId, method = marshal.loads(blobinfo) blob = client.SerializedBlob(blobinfo, protocolmsg, is_blob=True) return objId, method, (blob,), {} # object, method, vargs, kwargs def serve(objects: Dict[Any, str], host: Optional[Union[str, ipaddress.IPv4Address, ipaddress.IPv6Address]] = None, port: int = 0, daemon: Optional[Daemon] = None, use_ns: bool = True, verbose: bool = True) -> None: """ Basic method to fire up a daemon (or supply one yourself). objects is a dict containing objects to register as keys, and their names (or None) as values. If ns is true they will be registered in the naming server as well, otherwise they just stay local. If you need to publish on a unix domain socket, or require finer control of the daemon's behavior, you can't use this shortcut method. Create a Daemon yourself and use its appropriate methods. See the documentation on 'publishing objects' (in chapter: Servers) for more details. """ if daemon is None: daemon = Daemon(host, port) with daemon: ns = core.locate_ns() if use_ns else None for obj, name in objects.items(): if ns: localname = None # name is used for the name server else: localname = name # no name server, use name in daemon uri = daemon.register(obj, localname) if verbose: print("Object {0}:\n uri = {1}".format(repr(obj), uri)) if name and ns: ns.register(name, uri) if verbose: print(" name = {0}".format(name)) if verbose: print("Pyro daemon running.") daemon.requestLoop() def _default_methodcall_error_handler(daemon: Daemon, client_sock: socketutil.SocketConnection, method: Callable, vargs: Sequence[Any], kwargs: Dict[str, Any], exception: Exception) -> None: """The default routine called to process a exception raised in the user code of a method call""" log.debug("exception occurred in method call user code: client={} method={} exception={}" .format(client_sock, method.__qualname__, repr(exception))) # register the special serializers for the pyro objects serpent.register_class(Daemon, serializers.pyro_class_serpent_serializer) serializers.SerializerBase.register_class_to_dict(Daemon, serializers.serialize_pyro_object_to_dict, serpent_too=False) def _pyro_obj_to_auto_proxy(obj: Any) -> Any: """reduce function that automatically replaces Pyro objects by a Proxy""" daemon = getattr(obj, "_pyroDaemon", None) if daemon: # only return a proxy if the object is a registered pyro object return daemon.proxyFor(obj) return obj def _get_attribute(obj: Any, attr: str) -> Any: """ Resolves an attribute name to an object. Raises an AttributeError if any attribute in the chain starts with a '``_``'. Doesn't resolve a dotted name, because that is a security vulnerability. It treats it as a single attribute name (and the lookup will likely fail). """ if is_private_attribute(attr): raise AttributeError("attempt to access private attribute '%s'" % attr) else: obj = getattr(obj, attr) if getattr(obj, "_pyroExposed", False): return obj raise AttributeError("attempt to access unexposed attribute '%s'" % attr) __exposed_member_cache = {} # type: Dict[Tuple[type, bool], Dict[str, Set[str]]] def _reset_exposed_members(obj: Any, only_exposed: bool = True) -> None: """Delete any cached exposed members forcing recalculation on next request""" if not inspect.isclass(obj): obj = obj.__class__ cache_key = (obj, only_exposed) __exposed_member_cache.pop(cache_key, None) def _get_exposed_members(obj: Any, only_exposed: bool = True) -> Dict[str, Set[str]]: """ Return public and exposed members of the given object's class. You can also provide a class directly. Private members are ignored no matter what (names starting with underscore). If only_exposed is True, only members tagged with the @expose decorator are returned. If it is False, all public members are returned. The return value consists of the exposed methods, exposed attributes, and methods tagged as @oneway. (All this is used as meta data that Pyro sends to the proxy if it asks for it) """ if not inspect.isclass(obj): obj = obj.__class__ cache_key = (obj, only_exposed) if cache_key in __exposed_member_cache: return __exposed_member_cache[cache_key] methods = set() # all methods oneway = set() # oneway methods attrs = set() # attributes for m in dir(obj): # also lists names inherited from super classes if is_private_attribute(m): continue v = getattr(obj, m) if inspect.ismethod(v) or inspect.isfunction(v) or inspect.ismethoddescriptor(v): if getattr(v, "_pyroExposed", not only_exposed): methods.add(m) # check if the method is marked with the 'oneway' decorator: if getattr(v, "_pyroOneway", False): oneway.add(m) elif inspect.isdatadescriptor(v): func = getattr(v, "fget", None) or getattr(v, "fset", None) or getattr(v, "fdel", None) if func is not None and getattr(func, "_pyroExposed", not only_exposed): attrs.add(m) # Note that we don't expose plain class attributes no matter what. # it is a syntax error to add a decorator on them, and it is not possible # to give them a _pyroExposed tag either. # The way to expose attributes is by using properties for them. # This automatically solves the protection/security issue: you have to # explicitly decide to make an attribute into a @property (and to @expose it) # before it becomes remotely accessible. result = { "methods": methods, "oneway": oneway, "attrs": attrs } __exposed_member_cache[cache_key] = result return result def _unpack_weakref(obj: Any): """ Unpack weak reference, or return the object itself, if not a weak reference. If the weak reference is dead (calling it returns None), raises an exception. Even though register(...,weak=True) creates finalizer which will delete the weakref from the mapping, it is possible that the object is garbage-collected asynchronously between obtaining weakref from the mapping and reference unpacking, making the weakref invalid; this is handled by the exception here. """ if not isinstance(obj,weakref.ref): return obj ret=obj() # ret will hold strong reference to obj, until it gets deleted itself if ret is None: raise errors.DaemonError("Weakly registered deleted meanwhile (or finalizer failed?).") return ret def _get_exposed_property_value(obj: Any, propname: str, only_exposed: bool = True) -> Any: """ Return the value of an @exposed @property. If the requested property is not a @property or not exposed, an AttributeError is raised instead. """ v = getattr(obj.__class__, propname) if inspect.isdatadescriptor(v): if v.fget and getattr(v.fget, "_pyroExposed", not only_exposed): return v.fget(obj) raise AttributeError("attempt to access unexposed or unknown remote attribute '%s'" % propname) def _set_exposed_property_value(obj: Any, propname: str, value: Any, only_exposed: bool = True) -> Any: """ Sets the value of an @exposed @property. If the requested property is not a @property or not exposed, an AttributeError is raised instead. """ v = getattr(obj.__class__, propname) if inspect.isdatadescriptor(v): pfunc = v.fget or v.fset or v.fdel if v.fset and getattr(pfunc, "_pyroExposed", not only_exposed): return v.fset(obj, value) raise AttributeError("attempt to access unexposed or unknown remote attribute '%s'" % propname) class _OnewayCallThread(threading.Thread): def __init__(self, pyro_method, vargs, kwargs, pyro_daemon, pyro_client_sock): super(_OnewayCallThread, self).__init__(target=self._methodcall, name="oneway-call") self.daemon = True self.parent_context = current_context.to_global() self.pyro_daemon = pyro_daemon self.pyro_client_sock = pyro_client_sock self.pyro_method = pyro_method self.pyro_vargs = vargs self.pyro_kwars = kwargs def run(self): current_context.from_global(self.parent_context) super(_OnewayCallThread, self).run() def _methodcall(self): try: self.pyro_method(*self.pyro_vargs, **self.pyro_kwars) except Exception as xv: self.pyro_daemon.methodcall_error_handler(self.pyro_daemon, self.pyro_client_sock, self.pyro_method, self.pyro_vargs, self.pyro_kwars, xv) Pyro5-5.15/Pyro5/socketutil.py000066400000000000000000000553161451404116400162520ustar00rootroot00000000000000""" Low level socket utilities. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import os import platform import socket import errno import time import select import ipaddress import weakref import contextlib from typing import Union, Optional, Tuple, Dict, Type, Any try: import ssl except ImportError: pass from . import config from .errors import CommunicationError, TimeoutError, ConnectionClosedError # Note: other interesting errnos are EPERM, ENOBUFS, EMFILE # but it seems to me that all these signify an unrecoverable situation. # So I didn't include them in the list of retryable errors. ERRNO_RETRIES = [errno.EINTR, errno.EAGAIN, errno.EWOULDBLOCK, errno.EINPROGRESS] if hasattr(errno, "WSAEINTR"): ERRNO_RETRIES.append(errno.WSAEINTR) if hasattr(errno, "WSAEWOULDBLOCK"): ERRNO_RETRIES.append(errno.WSAEWOULDBLOCK) if hasattr(errno, "WSAEINPROGRESS"): ERRNO_RETRIES.append(errno.WSAEINPROGRESS) ERRNO_BADF = [errno.EBADF] if hasattr(errno, "WSAEBADF"): ERRNO_BADF.append(errno.WSAEBADF) ERRNO_ENOTSOCK = [errno.ENOTSOCK] if hasattr(errno, "WSAENOTSOCK"): ERRNO_ENOTSOCK.append(errno.WSAENOTSOCK) if not hasattr(socket, "SOL_TCP"): socket.SOL_TCP = socket.IPPROTO_TCP ERRNO_EADDRNOTAVAIL = [errno.EADDRNOTAVAIL] if hasattr(errno, "WSAEADDRNOTAVAIL"): ERRNO_EADDRNOTAVAIL.append(errno.WSAEADDRNOTAVAIL) ERRNO_EADDRINUSE = [errno.EADDRINUSE] if hasattr(errno, "WSAEADDRINUSE"): ERRNO_EADDRINUSE.append(errno.WSAEADDRINUSE) # msg_waitall has proven to be unreliable on windows USE_MSG_WAITALL = hasattr(socket, "MSG_WAITALL") and platform.system() != "Windows" def get_ip_address(hostname: str, workaround127: bool = False, version: int = None) \ -> Union[ipaddress.IPv4Address, ipaddress.IPv6Address]: """ Returns the IP address for the given host. If you enable the workaround, it will use a little hack if the ip address is found to be the loopback address. The hack tries to discover an externally visible ip address instead (this only works for ipv4 addresses). Set ipVersion=6 to return ipv6 addresses, 4 to return ipv4, 0 to let OS choose the best one or None to use config.PREFER_IP_VERSION. """ if not workaround127: with contextlib.suppress(ValueError): addr = ipaddress.ip_address(hostname) return addr def getaddr(ip_version): if ip_version == 6: family = socket.AF_INET6 elif ip_version == 4: family = socket.AF_INET elif ip_version == 0: family = socket.AF_UNSPEC else: raise ValueError("unknown value for argument ipVersion.") ip = socket.getaddrinfo(hostname or socket.gethostname(), 80, family, socket.SOCK_STREAM, socket.SOL_TCP)[0][4][0] if workaround127 and (ip.startswith("127.") or ip == "0.0.0.0"): return get_interface("4.2.2.2").ip return ipaddress.ip_address(ip) try: if hostname and ':' in hostname and version is None: version = 0 return getaddr(config.PREFER_IP_VERSION) if version is None else getaddr(version) except socket.gaierror: if version == 6 or (version is None and config.PREFER_IP_VERSION == 6): raise socket.error("unable to determine IPV6 address") return getaddr(0) def get_interface(ip_address: Union[str, ipaddress.IPv4Address, ipaddress.IPv6Address]) \ -> Union[ipaddress.IPv4Interface, ipaddress.IPv6Interface]: """tries to find the network interface that connects to the given host's address""" if isinstance(ip_address, str): ip_address = get_ip_address(ip_address) family = socket.AF_INET if ip_address.version == 4 else socket.AF_INET6 with socket.socket(family, socket.SOCK_DGRAM) as sock: sock.connect((str(ip_address), 53)) # 53=dns return ipaddress.ip_interface(sock.getsockname()[0]) def __retrydelays(): # first try a few very short delays, # if that doesn't work, increase by 0.1 sec every time yield 0.0001 yield 0.001 yield 0.01 d = 0.1 while True: yield d d += 0.1 def receive_data(sock: socket.socket, size: int) -> bytes: """Retrieve a given number of bytes from a socket. It is expected the socket is able to supply that number of bytes. If it isn't, an exception is raised (you will not get a zero length result or a result that is smaller than what you asked for). The partial data that has been received however is stored in the 'partialData' attribute of the exception object.""" try: delays = __retrydelays() msglen = 0 data = bytearray() if USE_MSG_WAITALL and not hasattr(sock, "getpeercert"): # ssl doesn't support recv flags while True: try: chunk = sock.recv(size, socket.MSG_WAITALL) if len(chunk) == size: return chunk # less data than asked, drop down into normal receive loop to finish msglen = len(chunk) data.extend(chunk) break except socket.timeout: raise TimeoutError("receiving: timeout") except socket.error as x: err = getattr(x, "errno", x.args[0]) if err not in ERRNO_RETRIES: raise ConnectionClosedError("receiving: connection lost: " + str(x)) time.sleep(next(delays)) # a slight delay to wait before retrying # old fashioned recv loop, we gather chunks until the message is complete while True: try: while msglen < size: # 60k buffer limit avoids problems on certain OSes like VMS, Windows chunk = sock.recv(min(60000, size - msglen)) if not chunk: break data.extend(chunk) msglen += len(chunk) if len(data) != size: err = ConnectionClosedError("receiving: not enough data") err.partialData = data # store the message that was received until now raise err return data # yay, complete except socket.timeout: raise TimeoutError("receiving: timeout") except socket.error as x: err = getattr(x, "errno", x.args[0]) if err not in ERRNO_RETRIES: raise ConnectionClosedError("receiving: connection lost: " + str(x)) time.sleep(next(delays)) # a slight delay to wait before retrying except socket.timeout: raise TimeoutError("receiving: timeout") def send_data(sock: socket.socket, data: bytes) -> None: """ Send some data over a socket. Some systems have problems with ``sendall()`` when the socket is in non-blocking mode. For instance, Mac OS X seems to be happy to throw EAGAIN errors too often. This function falls back to using a regular send loop if needed. """ if sock.gettimeout() is None: # socket is in blocking mode, we can use sendall normally. try: sock.sendall(data) return except socket.timeout: raise TimeoutError("sending: timeout") except socket.error as x: raise ConnectionClosedError("sending: connection lost: " + str(x)) else: # Socket is in non-blocking mode, use regular send loop. delays = __retrydelays() while data: try: sent = sock.send(data) data = data[sent:] except socket.timeout: raise TimeoutError("sending: timeout") except socket.error as x: err = getattr(x, "errno", x.args[0]) if err not in ERRNO_RETRIES: raise ConnectionClosedError("sending: connection lost: " + str(x)) time.sleep(next(delays)) # a slight delay to wait before retrying def create_socket(bind: Union[Tuple, str] = None, connect: Union[Tuple, str] = None, reuseaddr: bool = False, keepalive: bool = True, timeout: Optional[float] = -1, noinherit: bool = False, ipv6: bool = False, nodelay: bool = True, sslContext: ssl.SSLContext = None) -> socket.socket: """ Create a socket. Default socket options are keepalive and IPv4 family, and nodelay (nagle disabled). If 'bind' or 'connect' is a string, it is assumed a Unix domain socket is requested. Otherwise, a normal tcp/ip socket tuple (addr, port, ...) is used. Set ipv6=True to create an IPv6 socket rather than IPv4. Set ipv6=None to use the PREFER_IP_VERSION config setting. """ if bind and connect: raise ValueError("bind and connect cannot both be specified at the same time") forceIPv6 = ipv6 or (ipv6 is None and config.PREFER_IP_VERSION == 6) if isinstance(bind, str) or isinstance(connect, str): family = socket.AF_UNIX elif not bind and not connect: family = socket.AF_INET6 if forceIPv6 else socket.AF_INET elif isinstance(bind, tuple): if not bind[0]: family = socket.AF_INET6 if forceIPv6 else socket.AF_INET else: addr = get_ip_address(bind[0]) if addr.version == 4: if forceIPv6: raise ValueError("IPv4 address is used bind argument with forceIPv6 argument:" + bind[0] + ".") family = socket.AF_INET elif addr.version == 6: family = socket.AF_INET6 # replace bind addresses by their ipv6 counterparts (4-tuple) bind = (bind[0], bind[1], 0, 0) else: raise ValueError("unknown bind format.") elif isinstance(connect, tuple): if not connect[0]: family = socket.AF_INET6 if forceIPv6 else socket.AF_INET else: addr = get_ip_address(connect[0]) if addr.version == 4: if forceIPv6: raise ValueError("IPv4 address is used in connect argument with forceIPv6 argument:" + connect[0] + ".") family = socket.AF_INET elif addr.version == 6: family = socket.AF_INET6 # replace connect addresses by their ipv6 counterparts (4-tuple) connect = (connect[0], connect[1], 0, 0) else: raise ValueError("unknown connect format.") else: raise ValueError("unknown bind or connect format.") sock = socket.socket(family, socket.SOCK_STREAM) if sslContext: if bind: sock = sslContext.wrap_socket(sock, server_side=True) elif connect: sock = sslContext.wrap_socket(sock, server_side=False, server_hostname=connect[0]) else: sock = sslContext.wrap_socket(sock, server_side=False) if nodelay: set_nodelay(sock) if reuseaddr: set_reuseaddr(sock) if noinherit: set_noinherit(sock) if timeout is not None: if timeout == 0: timeout = None elif timeout >= 0: sock.settimeout(timeout) if bind: if type(bind) is tuple and bind[1] == 0: bind_unused_port(sock, bind[0]) else: sock.bind(bind) with contextlib.suppress(OSError, IOError): sock.listen(100) if connect: try: sock.connect(connect) except socket.error as xv: # This can happen when the socket is in non-blocking mode (or has a timeout configured). # We check if it is a retryable errno (usually EINPROGRESS). # If so, we use select() to wait until the socket is in writable state, # essentially rebuilding a blocking connect() call. errno = getattr(xv, "errno", 0) if errno in ERRNO_RETRIES: if timeout: if timeout < 0: timeout = None elif timeout < 0.1: timeout = 0.1 while True: try: sr, sw, se = select.select([], [sock], [sock], timeout) except InterruptedError: continue if sock in sw: break # yay, writable now, connect() completed elif sock in se: sock.close() # close the socket that refused to connect raise socket.error("connect failed") else: sock.close() # close the socket that refused to connect raise if keepalive: set_keepalive(sock) return sock def create_bc_socket(bind: Union[Tuple, str] = None, reuseaddr: bool = False, timeout: Optional[float] = -1, ipv6: bool = False) -> socket.socket: """ Create a udp broadcast socket. Set ipv6=True to create an IPv6 socket rather than IPv4. Set ipv6=None to use the PREFER_IP_VERSION config setting. """ forceIPv6 = ipv6 or (ipv6 is None and config.PREFER_IP_VERSION == 6) if not bind: family = socket.AF_INET6 if forceIPv6 else socket.AF_INET elif isinstance(bind, tuple): if not bind[0]: family = socket.AF_INET6 if forceIPv6 else socket.AF_INET else: addr = get_ip_address(bind[0]) if addr.version == 4: if forceIPv6: raise ValueError("IPv4 address is used with forceIPv6 option:" + bind[0] + ".") family = socket.AF_INET elif addr.version == 6: family = socket.AF_INET6 bind = (bind[0], bind[1], 0, 0) else: raise ValueError("unknown bind format: %r" % (bind,)) else: raise ValueError("unknown bind format: %r" % (bind,)) sock = socket.socket(family, socket.SOCK_DGRAM) if family == socket.AF_INET: sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) if reuseaddr: set_reuseaddr(sock) if timeout is None: sock.settimeout(None) else: if timeout >= 0: sock.settimeout(timeout) if bind: host = bind[0] or "" port = bind[1] if port == 0: bind_unused_port(sock, host) else: if len(bind) == 2: sock.bind((host, port)) # ipv4 elif len(bind) == 4: sock.bind((host, port, 0, 0)) # ipv6 else: raise ValueError("bind must be None, 2-tuple or 4-tuple") return sock def set_reuseaddr(sock: socket.socket) -> None: """sets the SO_REUSEADDR option on the socket, if possible.""" with contextlib.suppress(Exception): sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) def set_nodelay(sock: socket.socket) -> None: """sets the TCP_NODELAY option on the socket (to disable Nagle's algorithm), if possible.""" with contextlib.suppress(Exception): sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) def set_keepalive(sock: socket.socket) -> None: """sets the SO_KEEPALIVE option on the socket, if possible.""" with contextlib.suppress(Exception): sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) try: import fcntl def set_noinherit(sock: socket.socket) -> None: """Mark the given socket fd as non-inheritable to child processes""" fd = sock.fileno() flags = fcntl.fcntl(fd, fcntl.F_GETFD) fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC) except ImportError: # no fcntl available, try the windows version try: from ctypes import windll, WinError, wintypes # help ctypes to set the proper args for this kernel32 call on 64-bit pythons _SetHandleInformation = windll.kernel32.SetHandleInformation _SetHandleInformation.argtypes = [wintypes.HANDLE, wintypes.DWORD, wintypes.DWORD] _SetHandleInformation.restype = wintypes.BOOL # don't need this, but might as well def set_noinherit(sock: socket.socket) -> None: """Mark the given socket fd as non-inheritable to child processes""" if not _SetHandleInformation(sock.fileno(), 1, 0): raise WinError() except (ImportError, NotImplementedError): # nothing available, define a dummy function def set_noinherit(sock: socket.socket) -> None: """Mark the given socket fd as non-inheritable to child processes (dummy)""" pass class SocketConnection(object): """A wrapper class for plain sockets, containing various methods such as :meth:`send` and :meth:`recv`""" def __init__(self, sock: socket.socket, objectId: str = None, keep_open: bool = False) -> None: self.sock = sock self.objectId = objectId self.pyroInstances = {} # type: Dict[Type, Any] # pyro objects for instance_mode=session self.tracked_resources = weakref.WeakSet() # type: weakref.WeakSet[Any] # weakrefs to resources for this connection self.keep_open = keep_open def __del__(self): self.close() def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() def send(self, data: bytes) -> None: send_data(self.sock, data) def recv(self, size: int) -> bytes: return receive_data(self.sock, size) def close(self) -> None: if self.keep_open: return with contextlib.suppress(Exception): self.sock.shutdown(socket.SHUT_RDWR) with contextlib.suppress(Exception): self.sock.close() self.pyroInstances = {} # release the session instances for rsc in self.tracked_resources: with contextlib.suppress(Exception): rsc.close() # it is assumed a 'resource' has a close method. self.tracked_resources.clear() def fileno(self) -> int: return self.sock.fileno() def family(self) -> str: return family_str(self.sock) def settimeout(self, timeout: Optional[float]) -> None: self.sock.settimeout(timeout) def gettimeout(self) -> Optional[float]: return self.sock.gettimeout() def getpeercert(self) -> Optional[dict]: try: return self.sock.getpeercert() # type: ignore except AttributeError: return None timeout = property(gettimeout, settimeout) def family_str(sock) -> str: f = sock.family if f == socket.AF_INET: return "IPv4" if f == socket.AF_INET6: return "IPv6" if hasattr(socket, "AF_UNIX") and f == socket.AF_UNIX: return "Unix" return "???" def find_probably_unused_port(family: int = socket.AF_INET, socktype: int = socket.SOCK_STREAM) -> int: """Returns an unused port that should be suitable for binding (likely, but not guaranteed). This code is copied from the stdlib's test.test_support module.""" with socket.socket(family, socktype) as sock: return bind_unused_port(sock) def bind_unused_port(sock: socket.socket, host: Union[str, ipaddress.IPv4Address, ipaddress.IPv6Address] = 'localhost') -> int: """Bind the socket to a free port and return the port number. This code is based on the code in the stdlib's test.test_support module.""" if sock.family in (socket.AF_INET, socket.AF_INET6) and sock.type == socket.SOCK_STREAM: if hasattr(socket, "SO_EXCLUSIVEADDRUSE"): with contextlib.suppress(socket.error): sock.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1) if not isinstance(host, str): host = str(host) if sock.family == socket.AF_INET: if host == 'localhost': sock.bind(('127.0.0.1', 0)) else: sock.bind((host, 0)) elif sock.family == socket.AF_INET6: if host == 'localhost': sock.bind(('::1', 0, 0, 0)) else: sock.bind((host, 0, 0, 0)) else: raise CommunicationError("unsupported socket family: " + str(sock.family)) return sock.getsockname()[1] def interrupt_socket(address: Tuple[str, int]) -> None: """bit of a hack to trigger a blocking server to get out of the loop, useful at clean shutdowns""" with contextlib.suppress(socket.error): sock = create_socket(connect=address, keepalive=False, timeout=-1) with contextlib.suppress(socket.error, AttributeError): sock.sendall(b"!" * 16) with contextlib.suppress(OSError, socket.error): sock.shutdown(socket.SHUT_RDWR) sock.close() __ssl_server_context = None __ssl_client_context = None def get_ssl_context(servercert: str = "", serverkey: str = "", clientcert: str = "", clientkey: str = "", cacerts: str = "", keypassword: str = "") -> ssl.SSLContext: """creates an SSL context and caches it, so you have to set the parameters correctly before doing anything""" global __ssl_client_context, __ssl_server_context if servercert: if clientcert: raise ValueError("can't have both server cert and client cert") # server context if __ssl_server_context: return __ssl_server_context if not os.path.isfile(servercert): raise IOError("server cert file not found") if serverkey and not os.path.isfile(serverkey): raise IOError("server key file not found") __ssl_server_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) __ssl_server_context.load_cert_chain(servercert, serverkey or None, keypassword or None) # type: ignore if cacerts: if os.path.isdir(cacerts): __ssl_server_context.load_verify_locations(capath=cacerts) else: __ssl_server_context.load_verify_locations(cafile=cacerts) if config.SSL_REQUIRECLIENTCERT: __ssl_server_context.verify_mode = ssl.CERT_REQUIRED # 2-way ssl, server+client certs else: __ssl_server_context.verify_mode = ssl.CERT_NONE # 1-way ssl, server cert only return __ssl_server_context else: # client context if __ssl_client_context: return __ssl_client_context __ssl_client_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) if clientcert: if not os.path.isfile(clientcert): raise IOError("client cert file not found") __ssl_client_context.load_cert_chain(clientcert, clientkey or None, keypassword or None) # type: ignore if cacerts: if os.path.isdir(cacerts): __ssl_client_context.load_verify_locations(capath=cacerts) else: __ssl_client_context.load_verify_locations(cafile=cacerts) return __ssl_client_context Pyro5-5.15/Pyro5/svr_existingconn.py000066400000000000000000000077271451404116400174710ustar00rootroot00000000000000""" Socket server for a the special case of a single, already existing, connection. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import socket import sys import logging import ssl from . import config, socketutil, errors log = logging.getLogger("Pyro5.existingconnectionserver") class SocketServer_ExistingConnection(object): def __init__(self): self.sock = self.daemon = self.locationStr = self.conn = None self.shutting_down = False def init(self, daemon, connected_socket): connected_socket.getpeername() # check that it is connected if config.SSL and not isinstance(connected_socket, ssl.SSLSocket): raise socket.error("SSL configured for Pyro but existing socket is not a SSL socket") self.daemon = daemon self.sock = connected_socket log.info("starting server on user-supplied connected socket " + str(connected_socket)) sn = connected_socket.getsockname() if hasattr(socket, "AF_UNIX") and connected_socket.family == socket.AF_UNIX: self.locationStr = "./u:" + (sn or "<>") else: host, port = sn[:2] if ":" in host: # ipv6 self.locationStr = "[%s]:%d" % (host, port) else: self.locationStr = "%s:%d" % (host, port) self.conn = socketutil.SocketConnection(connected_socket) def __repr__(self): return "<%s on %s>" % (self.__class__.__name__, self.locationStr) def __del__(self): if self.sock is not None: self.sock = None self.conn = None @property def selector(self): raise TypeError("single-connection server doesn't have multiplexing selector") @property def sockets(self): return [self.sock] def combine_loop(self, server): raise errors.PyroError("cannot combine servers when using user-supplied connected socket") def events(self, eventsockets): raise errors.PyroError("cannot combine events when using user-supplied connected socket") def shutdown(self): self.shutting_down = True self.close() self.sock = None self.conn = None def close(self): # don't close the socket itself, that's the user's responsibility self.sock = None self.conn = None def handleRequest(self): """Handles a single connection request event and returns if the connection is still active""" try: self.daemon.handleRequest(self.conn) return True except (socket.error, errors.ConnectionClosedError, errors.SecurityError): # client went away or caused a security error. # close the connection silently. try: peername = self.conn.sock.getpeername() log.debug("disconnected %s", peername) except socket.error: log.debug("disconnected a client") self.shutdown() return False except errors.TimeoutError as x: # for timeout errors we're not really interested in detailed traceback info log.warning("error during handleRequest: %s" % x) return False except Exception: # other error occurred, close the connection, but also log a warning ex_t, ex_v, ex_tb = sys.exc_info() tb = errors.format_traceback(ex_t, ex_v, ex_tb) msg = "error during handleRequest: %s; %s" % (ex_v, "".join(tb)) log.warning(msg) return False def loop(self, loopCondition=lambda: True): log.debug("entering requestloop") while loopCondition() and self.sock: try: self.handleRequest() self.daemon._housekeeping() except socket.timeout: pass # just continue the loop on a timeout except KeyboardInterrupt: log.debug("stopping on break signal") break Pyro5-5.15/Pyro5/svr_multiplex.py000066400000000000000000000216121451404116400167710ustar00rootroot00000000000000""" Socket server based on socket multiplexing. Doesn't use threads. Uses the best available selector (kqueue, poll, select). Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import socket import time import sys import logging import os import selectors import contextlib from collections import defaultdict from . import config, socketutil, errors log = logging.getLogger("Pyro5.multiplexserver") class SocketServer_Multiplex(object): """Multiplexed transport server for socket connections (uses select, poll, kqueue, ...)""" def __init__(self): self.sock = self.daemon = self.locationStr = None self.selector = selectors.DefaultSelector() self.shutting_down = False def init(self, daemon, host, port, unixsocket=None): log.info("starting multiplexed socketserver") log.debug("selector implementation: %s.%s", self.selector.__class__.__module__, self.selector.__class__.__name__) self.sock = None bind_location = unixsocket if unixsocket else (host, port) if config.SSL: sslContext = socketutil.get_ssl_context(servercert=config.SSL_SERVERCERT, serverkey=config.SSL_SERVERKEY, keypassword=config.SSL_SERVERKEYPASSWD, cacerts=config.SSL_CACERTS) log.info("using SSL, cert=%s key=%s cacerts=%s", config.SSL_SERVERCERT, config.SSL_SERVERKEY, config.SSL_CACERTS) else: sslContext = None log.info("not using SSL") self.sock = socketutil.create_socket(bind=bind_location, reuseaddr=config.SOCK_REUSE, timeout=config.COMMTIMEOUT, noinherit=True, nodelay=config.SOCK_NODELAY, sslContext=sslContext) self.daemon = daemon self._socketaddr = sockaddr = self.sock.getsockname() if not unixsocket and sockaddr[0].startswith("127."): if host is None or host.lower() != "localhost" and not host.startswith("127."): log.warning("weird DNS setup: %s resolves to localhost (127.x.x.x)", host) if unixsocket: self.locationStr = "./u:" + unixsocket else: host = host or sockaddr[0] port = port or sockaddr[1] if ":" in host: # ipv6 self.locationStr = "[%s]:%d" % (host, port) else: self.locationStr = "%s:%d" % (host, port) self.selector.register(self.sock, selectors.EVENT_READ, self) def __repr__(self): return "<%s on %s; %d connections>" % (self.__class__.__name__, self.locationStr, len(self.selector.get_map()) - 1) def __del__(self): if self.sock is not None: self.selector.close() self.sock.close() self.sock = None def events(self, eventsockets): """handle events that occur on one of the sockets of this server""" for s in eventsockets: if self.shutting_down: return if s is self.sock: # server socket, means new connection conn = self._handleConnection(self.sock) if conn: self.selector.register(conn, selectors.EVENT_READ, self) else: # must be client socket, means remote call active = self.handleRequest(s) if not active: try: self.daemon._clientDisconnect(s) except Exception as x: log.warning("Error in clientDisconnect: " + str(x)) self.selector.unregister(s) s.close() self.daemon._housekeeping() def _handleConnection(self, sock): try: if sock is None: return csock, caddr = sock.accept() if hasattr(csock, "getpeercert"): log.debug("connected %s - SSL", caddr) else: log.debug("connected %s - unencrypted", caddr) if config.COMMTIMEOUT: csock.settimeout(config.COMMTIMEOUT) except (socket.error, OSError) as x: err = getattr(x, "errno", x.args[0]) if err in socketutil.ERRNO_BADF or err in socketutil.ERRNO_ENOTSOCK: # our server socket got destroyed raise errors.ConnectionClosedError("server socket closed") # socket errors may not lead to a server abort, so we log it and continue err = getattr(x, "errno", x.args[0]) log.warning("accept() failed '%s' with errno=%d, shouldn't happen", x, err) return None try: conn = socketutil.SocketConnection(csock) if self.daemon._handshake(conn): return conn conn.close() except Exception: # catch all errors, otherwise the event loop could terminate ex_t, ex_v, ex_tb = sys.exc_info() tb = errors.format_traceback(ex_t, ex_v, ex_tb) log.warning("error during connect/handshake: %s; %s", ex_v, "\n".join(tb)) with contextlib.suppress(OSError, socket.error): csock.shutdown(socket.SHUT_RDWR) csock.close() return None def shutdown(self): self.shutting_down = True self.wakeup() time.sleep(0.05) self.close() self.sock = None def close(self): self.selector.close() if self.sock: sockname = None with contextlib.suppress(OSError, socket.error): sockname = self.sock.getsockname() self.sock.close() if type(sockname) is str: # it was a Unix domain socket, remove it from the filesystem if os.path.exists(sockname): os.remove(sockname) self.sock = None @property def sockets(self): registrations = self.selector.get_map() if registrations: return [sk.fileobj for sk in registrations.values()] else: return [] def wakeup(self): """bit of a hack to trigger a blocking server to get out of the loop, useful at clean shutdowns""" socketutil.interrupt_socket(self._socketaddr) def handleRequest(self, conn): """Handles a single connection request event and returns if the connection is still active""" try: self.daemon.handleRequest(conn) return True except (socket.error, errors.ConnectionClosedError, errors.SecurityError): # client went away or caused a security error. # close the connection silently. try: peername = conn.sock.getpeername() log.debug("disconnected %s", peername) except socket.error: log.debug("disconnected a client") return False except errors.TimeoutError as x: # for timeout errors we're not really interested in detailed traceback info log.warning("error during handleRequest: %s" % x) return False except Exception: # other error occurred, close the connection, but also log a warning ex_t, ex_v, ex_tb = sys.exc_info() tb = errors.format_traceback(ex_t, ex_v, ex_tb) msg = "error during handleRequest: %s; %s" % (ex_v, "".join(tb)) log.warning(msg) return False def loop(self, loopCondition=lambda: True): log.debug("entering multiplexed requestloop") while loopCondition(): try: try: events = self.selector.select(config.POLLTIMEOUT) except OSError: events = [] # get all the socket connection objects that have a READ event # (the WRITE events are ignored here, they're registered to let timeouts work etc) events_per_server = defaultdict(list) for key, mask in events: if mask & selectors.EVENT_READ: events_per_server[key.data].append(key.fileobj) for server, fileobjs in events_per_server.items(): server.events(fileobjs) if not events_per_server: self.daemon._housekeeping() except socket.timeout: pass # just continue the loop on a timeout except KeyboardInterrupt: log.debug("stopping on break signal") break def combine_loop(self, server): for sock in server.sockets: self.selector.register(sock, selectors.EVENT_READ, server) server.selector = self.selector Pyro5-5.15/Pyro5/svr_threads.py000066400000000000000000000325711451404116400164060ustar00rootroot00000000000000""" Socket server based on a worker thread pool. Doesn't use select. Uses a single worker thread per client connection. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import socket import logging import sys import time import threading import os import selectors import contextlib from . import config, socketutil, errors log = logging.getLogger("Pyro5.threadpoolserver") _client_disconnect_lock = threading.Lock() class ClientConnectionJob(object): """ Takes care of a single client connection and all requests that may arrive during its life span. """ def __init__(self, clientSocket, clientAddr, daemon): self.csock = socketutil.SocketConnection(clientSocket) self.caddr = clientAddr self.daemon = daemon def __call__(self): if self.handleConnection(): try: while True: try: self.daemon.handleRequest(self.csock) except (socket.error, errors.ConnectionClosedError): # client went away. log.debug("disconnected %s", self.caddr) break except errors.SecurityError: log.debug("security error on client %s", self.caddr) break except errors.TimeoutError as x: # for timeout errors we're not really interested in detailed traceback info log.warning("error during handleRequest: %s" % x) break except Exception: # other errors log a warning, break this loop and close the client connection ex_t, ex_v, ex_tb = sys.exc_info() tb = errors.format_traceback(ex_t, ex_v, ex_tb) msg = "error during handleRequest: %s; %s" % (ex_v, "".join(tb)) log.warning(msg) break finally: with _client_disconnect_lock: try: self.daemon._clientDisconnect(self.csock) except Exception as x: log.warning("Error in clientDisconnect: " + str(x)) self.csock.close() def handleConnection(self): # connection handshake try: if self.daemon._handshake(self.csock): return True self.csock.close() except Exception: ex_t, ex_v, ex_tb = sys.exc_info() tb = errors.format_traceback(ex_t, ex_v, ex_tb) log.warning("error during connect/handshake: %s; %s", ex_v, "\n".join(tb)) self.csock.close() return False def denyConnection(self, reason): log.warning("client connection was denied: " + reason) # return failed handshake self.daemon._handshake(self.csock, denied_reason=reason) self.csock.close() class Housekeeper(threading.Thread): def __init__(self, daemon): super(Housekeeper, self).__init__(name="housekeeper") self.pyroDaemon = daemon self.stop = threading.Event() self.daemon = True self.waittime = min(config.POLLTIMEOUT or 0, max(config.COMMTIMEOUT or 0, 5)) def run(self): while True: if self.stop.wait(self.waittime): break self.pyroDaemon._housekeeping() class SocketServer_Threadpool(object): """transport server for socket connections, worker thread pool version.""" def __init__(self): self.daemon = self.sock = self._socketaddr = self.locationStr = self.pool = None self.shutting_down = False self.housekeeper = None self._selector = selectors.DefaultSelector() def init(self, daemon, host, port, unixsocket=None): log.info("starting thread pool socketserver") self.daemon = daemon self.sock = None bind_location = unixsocket if unixsocket else (host, port) if config.SSL: sslContext = socketutil.get_ssl_context(servercert=config.SSL_SERVERCERT, serverkey=config.SSL_SERVERKEY, keypassword=config.SSL_SERVERKEYPASSWD, cacerts=config.SSL_CACERTS) log.info("using SSL, cert=%s key=%s cacerts=%s", config.SSL_SERVERCERT, config.SSL_SERVERKEY, config.SSL_CACERTS) else: sslContext = None log.info("not using SSL") self.sock = socketutil.create_socket(bind=bind_location, reuseaddr=config.SOCK_REUSE, timeout=config.COMMTIMEOUT, noinherit=True, nodelay=config.SOCK_NODELAY, sslContext=sslContext) self._socketaddr = self.sock.getsockname() if not unixsocket and self._socketaddr[0].startswith("127."): if host is None or host.lower() != "localhost" and not host.startswith("127."): log.warning("weird DNS setup: %s resolves to localhost (127.x.x.x)", host) if unixsocket: self.locationStr = "./u:" + unixsocket else: host = host or self._socketaddr[0] port = port or self._socketaddr[1] if ":" in host: # ipv6 self.locationStr = "[%s]:%d" % (host, port) else: self.locationStr = "%s:%d" % (host, port) self.pool = Pool() self.housekeeper = Housekeeper(daemon) self.housekeeper.start() self._selector.register(self.sock, selectors.EVENT_READ, self) def __del__(self): if self.sock is not None: self.sock.close() self.sock = None if self.pool is not None: self.pool.close() self.pool = None if self.housekeeper: self.housekeeper.stop.set() self.housekeeper.join() self.housekeeper = None def __repr__(self): return "<%s on %s; %d workers>" % (self.__class__.__name__, self.locationStr, self.pool.num_workers()) def loop(self, loopCondition=lambda: True): log.debug("threadpool server requestloop") while (self.sock is not None) and not self.shutting_down and loopCondition(): try: self.events([self.sock]) except (socket.error, OSError) as x: if not loopCondition(): # swallow the socket error if loop terminates anyway # this can occur if we are asked to shutdown, socket can be invalid then break # socket errors may not lead to a server abort, so we log it and continue err = getattr(x, "errno", x.args[0]) log.warning("socket error '%s' with errno=%d, shouldn't happen", x, err) continue except KeyboardInterrupt: log.debug("stopping on break signal") break def combine_loop(self, server): raise TypeError("You can't use the loop combiner on the threadpool server type") def events(self, eventsockets): """used for external event loops: handle events that occur on one of the sockets of this server""" # we only react on events on our own server socket. # all other (client) sockets are owned by their individual threads. assert self.sock in eventsockets with contextlib.suppress(socket.timeout): # just continue the loop on a timeout on accept events = self._selector.select(config.POLLTIMEOUT) if not events: return csock, caddr = self.sock.accept() if self.shutting_down: csock.close() return if hasattr(csock, "getpeercert"): log.debug("connected %s - SSL", caddr) else: log.debug("connected %s - unencrypted", caddr) if config.COMMTIMEOUT: csock.settimeout(config.COMMTIMEOUT) job = ClientConnectionJob(csock, caddr, self.daemon) try: self.pool.process(job) except NoFreeWorkersError: job.denyConnection("no free workers, increase server threadpool size") def shutdown(self): self.shutting_down = True self.wakeup() time.sleep(0.05) self.close() self.sock = None def close(self): if self.housekeeper: self.housekeeper.stop.set() self.housekeeper.join() self.housekeeper = None if self.sock: with contextlib.suppress(socket.error, OSError): sockname = self.sock.getsockname() with contextlib.suppress(Exception): self.sock.close() if type(sockname) is str: # it was a Unix domain socket, remove it from the filesystem if os.path.exists(sockname): os.remove(sockname) self.sock = None self.pool.close() @property def sockets(self): # the server socket is all we care about, all client sockets are running in their own threads return [self.sock] @property def selector(self): raise TypeError("threadpool server doesn't have multiplexing selector") def wakeup(self): socketutil.interrupt_socket(self._socketaddr) class PoolError(Exception): pass class NoFreeWorkersError(PoolError): pass class Worker(threading.Thread): def __init__(self, pool): super(Worker, self).__init__() self.daemon = True self.name = "Pyro-Worker-%d" % id(self) self.job_available = threading.Event() self.job = None self.pool = pool def process(self, job): self.job = job self.job_available.set() def run(self): while True: self.job_available.wait() self.job_available.clear() if self.job is None: break try: self.job() except Exception as x: log.exception("unhandled exception from job in worker thread %s: %s", self.name, x) self.job = None self.pool.notify_done(self) self.pool = None class Pool(object): """ A job processing pool that is using a pool of worker threads. The amount of worker threads in the pool is configurable and scales between min/max size. """ def __init__(self): if config.THREADPOOL_SIZE < 1 or config.THREADPOOL_SIZE_MIN < 1: raise ValueError("threadpool sizes must be greater than zero") if config.THREADPOOL_SIZE_MIN > config.THREADPOOL_SIZE: raise ValueError("minimum threadpool size must be less than or equal to max size") self.idle = set() self.busy = set() self.closed = False for _ in range(config.THREADPOOL_SIZE_MIN): worker = Worker(self) self.idle.add(worker) worker.start() log.debug("worker pool created with initial size %d", self.num_workers()) self.count_lock = threading.Lock() def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() def close(self): if not self.closed: log.debug("closing down") for w in list(self.busy): w.process(None) for w in list(self.idle): w.process(None) self.closed = True time.sleep(0.1) idle, self.idle = self.idle, set() busy, self.busy = self.busy, set() # check if the threads that are joined are not the current thread. current_thread = threading.current_thread() while idle: p = idle.pop() if p is not current_thread: p.join(timeout=0.1) while busy: p = busy.pop() if p is not current_thread: p.join(timeout=0.1) def __repr__(self): return "<%s.%s at 0x%x; %d busy workers; %d idle workers>" % \ (self.__class__.__module__, self.__class__.__name__, id(self), len(self.busy), len(self.idle)) def num_workers(self): return len(self.busy) + len(self.idle) def process(self, job): if self.closed: raise PoolError("job queue is closed") if self.idle: worker = self.idle.pop() elif self.num_workers() < config.THREADPOOL_SIZE: worker = Worker(self) worker.start() else: raise NoFreeWorkersError("no free workers available, increase thread pool size") self.busy.add(worker) worker.process(job) log.debug("worker counts: %d busy, %d idle", len(self.busy), len(self.idle)) def notify_done(self, worker): if worker in self.busy: self.busy.remove(worker) if self.closed: worker.process(None) return if len(self.idle) >= config.THREADPOOL_SIZE_MIN: worker.process(None) else: self.idle.add(worker) log.debug("worker counts: %d busy, %d idle", len(self.busy), len(self.idle)) Pyro5-5.15/Pyro5/utils/000077500000000000000000000000001451404116400146405ustar00rootroot00000000000000Pyro5-5.15/Pyro5/utils/__init__.py000066400000000000000000000000371451404116400167510ustar00rootroot00000000000000# just to make this a package. Pyro5-5.15/Pyro5/utils/echoserver.py000066400000000000000000000150601451404116400173610ustar00rootroot00000000000000""" Echo server for test purposes. This is usually invoked by starting this module as a script: :command:`python -m Pyro5.test.echoserver` or simply: :command:`pyro5-test-echoserver` It is also possible to use the :class:`EchoServer` in user code but that is not terribly useful. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import sys import time import threading from argparse import ArgumentParser from .. import config, core, server, nameserver __all__ = ["EchoServer"] @server.expose class EchoServer(object): """ The echo server object that is provided as a Pyro object by this module. If its :attr:`verbose` attribute is set to ``True``, it will print messages as it receives calls. """ _verbose = False _must_shutdown = False def echo(self, message): """return the message""" if self._verbose: message_str = repr(message).encode(sys.stdout.encoding, errors="replace").decode(sys.stdout.encoding) print("%s - echo: %s" % (time.asctime(), message_str)) return message def error(self): """generates a simple exception without text""" if self._verbose: print("%s - error: generating exception" % time.asctime()) raise ValueError("this is the generated error from echoserver echo() method") def error_with_text(self): """generates a simple exception with message""" if self._verbose: print("%s - error: generating exception" % time.asctime()) raise ValueError("the message of the error") @server.oneway def oneway_echo(self, message): """just like echo, but oneway; the client won't wait for response""" if self._verbose: message_str = repr(message).encode(sys.stdout.encoding, errors="replace").decode(sys.stdout.encoding) print("%s - oneway_echo: %s" % (time.asctime(), message_str)) return "bogus return value" def slow(self): """returns (and prints) a message after a certain delay""" if self._verbose: print("%s - slow: waiting a bit..." % time.asctime()) time.sleep(5) if self._verbose: print("%s - slow: returning result" % time.asctime()) return "Finally, an answer!" def generator(self): """a generator function that returns some elements on demand""" yield "one" yield "two" yield "three" def nan(self): return float("nan") def inf(self): return float("inf") @server.oneway def oneway_slow(self): """prints a message after a certain delay, and returns; but the client won't wait for it""" if self._verbose: print("%s - oneway_slow: waiting a bit..." % time.asctime()) time.sleep(5) if self._verbose: print("%s - oneway_slow: returning result" % time.asctime()) return "bogus return value" def _private(self): """a 'private' method that should not be accessible""" return "should not be allowed" def __private(self): """another 'private' method that should not be accessible""" return "should not be allowed" def __dunder__(self): """a double underscore method that should be accessible normally""" return "should be allowed (dunder)" def shutdown(self): """called to signal the echo server to shut down""" if self._verbose: print("%s - shutting down" % time.asctime()) self._must_shutdown = True @property def verbose(self): return self._verbose @verbose.setter def verbose(self, onoff): self._verbose = bool(onoff) class NameServer(threading.Thread): def __init__(self, hostname): super(NameServer, self).__init__() self.daemon = True self.hostname = hostname self.started = threading.Event() def run(self): self.uri, self.ns_daemon, self.bc_server = nameserver.start_ns(self.hostname) self.started.set() if self.bc_server: self.bc_server.runInThread() self.ns_daemon.requestLoop() def start_nameserver(host): ns = NameServer(host) ns.start() ns.started.wait() return ns def main(args=None, returnWithoutLooping=False): parser = ArgumentParser(description="Pyro test echo/nameserver command line launcher.") parser.add_argument("-H", "--host", default="localhost", help="hostname to bind server on (default=%(default)s)") parser.add_argument("-p", "--port", type=int, default=0, help="port to bind server on") parser.add_argument("-u", "--unixsocket", help="Unix domain socket name to bind server on") parser.add_argument("-n", "--naming", action="store_true", default=False, help="register with nameserver") parser.add_argument("-N", "--nameserver", action="store_true", default=False, help="also start a nameserver") parser.add_argument("-v", "--verbose", action="store_true", default=False, help="verbose output") parser.add_argument("-q", "--quiet", action="store_true", default=False, help="don't output anything") args = parser.parse_args(args) if args.verbose: args.quiet = False if not args.quiet: print("Starting Pyro's built-in test echo server.") config.SERVERTYPE = "multiplex" namesvr = None if args.nameserver: args.naming = True namesvr = start_nameserver(args.host) d = server.Daemon(host=args.host, port=args.port, unixsocket=args.unixsocket) echo = EchoServer() echo._verbose = args.verbose objectName = "test.echoserver" uri = d.register(echo, objectName) if args.naming: host, port = None, None if namesvr is not None: host, port = namesvr.uri.host, namesvr.uri.port ns = core.locate_ns(host, port) ns.register(objectName, uri) if args.verbose: print("using name server at %s" % ns._pyroUri) if namesvr is not None: if namesvr.bc_server: print("broadcast server running at %s" % namesvr.bc_server.locationStr) else: print("not using a broadcast server") else: if args.verbose: print("not using a name server.") if not args.quiet: print("object name: %s" % objectName) print("echo uri: %s" % uri) print("echoserver running.") if returnWithoutLooping: return d, echo, uri # for unit testing else: d.requestLoop(loopCondition=lambda: not echo._must_shutdown) d.close() if __name__ == "__main__": main() Pyro5-5.15/Pyro5/utils/httpgateway.py000066400000000000000000000412131451404116400175540ustar00rootroot00000000000000""" HTTP gateway: connects the web browser's world of javascript+http and Pyro. Creates a stateless HTTP server that essentially is a proxy for the Pyro objects behind it. It exposes the Pyro objects through a HTTP interface and uses the JSON serializer, so that you can immediately process the response data in the browser. You can start this module as a script from the command line, to easily get a http gateway server running: :command:`python -m Pyro5.utils.httpgateway` or simply: :command:`pyro5-httpgateway` It is also possible to import the 'pyro_app' function and stick that into a WSGI server of your choice, to have more control. The javascript code in the web page of the gateway server works with the same-origin browser policy because it is served by the gateway itself. If you want to access it from scripts in different sites, you have to work around this or embed the gateway app in your site. Non-browser clients that access the http api have no problems. See the `http` example for two of such clients (node.js and python). Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import sys import re import urllib.parse import uuid import json from wsgiref.simple_server import make_server from argparse import ArgumentParser import traceback from .. import __version__, config, errors, client, core, protocol, serializers, callcontext __all__ = ["pyro_app", "main"] _nameserver = None def get_nameserver(): global _nameserver if not _nameserver: _nameserver = core.locate_ns() try: _nameserver.ping() return _nameserver except errors.ConnectionClosedError: _nameserver = None print("Connection with nameserver lost, reconnecting...") return get_nameserver() def cors_response_header(header, cors): header.append(('Access-Control-Allow-Origin', cors)) header.append(('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')) header.append(('Access-Control-Allow-Headers', 'Content-Type')) return header def invalid_request(start_response): """Called if invalid http method.""" start_response('405 Method Not Allowed', cors_response_header([('Content-Type', 'text/plain')], pyro_app.cors)) return [b'Error 405: Method Not Allowed'] def option_request(start_response): """OPTION Call with CORS""" start_response('200 OK', cors_response_header([('Content-Type', 'text/plain')], pyro_app.cors)) return [b'200 OK'] def not_found(start_response): """Called if Url not found.""" start_response('404 Not Found', cors_response_header([('Content-Type', 'text/plain')], pyro_app.cors)) return [b'Error 404: Not Found'] def redirect(start_response, target): """Called to do a redirect""" start_response('302 Found', [('Location', target)]) return [] index_page_template = """ Pyro HTTP gateway

Pyro HTTP gateway

Use http+json to talk to Pyro objects. Docs.

Note: performance isn't maxed; it is stateless. Does a name lookup and uses a new Pyro proxy for each request.

Currently exposed contents of name server on {hostname}:

(Limited to 10 entries, exposed name pattern = '{ns_regex}')

{name_server_contents_list}

Name server examples: (these examples are working if you expose the Pyro.NameServer object)

Echoserver examples: (these examples are working if you expose the test.echoserver object)

Pyro response data (via Ajax):

Call:
   
Response:
   

Pyro version: {pyro_version} — © Irmen de Jong

""" def return_homepage(environ, start_response): try: nameserver = get_nameserver() except errors.NamingError as x: print("Name server error:", x) start_response('500 Internal Server Error', cors_response_header([('Content-Type', 'text/plain')], pyro_app.cors)) return [b"Cannot connect to the Pyro name server. Is it running? Refresh page to retry."] start_response('200 OK', cors_response_header([('Content-Type', 'text/html')], pyro_app.cors)) nslist = [""] names = sorted(list(nameserver.list(regex=pyro_app.ns_regex).keys())[:10]) with client.BatchProxy(nameserver) as nsbatch: for name in names: nsbatch.lookup(name) for name, uri in zip(names, nsbatch()): attributes = "-" try: with client.Proxy(uri) as proxy: proxy._pyroBind() methods = "   ".join(proxy._pyroMethods) or "-" attributes = [ "{attribute}" .format(name=name, attribute=attribute) for attribute in proxy._pyroAttrs ] attributes = "   ".join(attributes) or "-" except errors.PyroError as x: stderr = environ["wsgi.errors"] print("ERROR getting metadata for {0}:".format(uri), file=stderr) traceback.print_exc(file=stderr) methods = "??error:%s??" % str(x) nslist.append( "" .format(name=name, methods=methods, attributes=attributes)) nslist.append("
Namemethodsattributes (zero-param methods)
{name}{methods}{attributes}
") index_page = index_page_template.format(ns_regex=pyro_app.ns_regex, name_server_contents_list="".join(nslist), pyro_version=__version__, hostname=nameserver._pyroUri.location) return [index_page.encode("utf-8")] def process_pyro_request(environ, path, parameters, start_response): pyro_options = environ.get("HTTP_X_PYRO_OPTIONS", "").split(",") if not path: return return_homepage(environ, start_response) matches = re.match(r"(.+)/(.+)", path) if not matches: return not_found(start_response) object_name, method = matches.groups() if pyro_app.gateway_key: gateway_key = environ.get("HTTP_X_PYRO_GATEWAY_KEY", "") or parameters.get("$key", "") gateway_key = gateway_key.encode("utf-8") if gateway_key != pyro_app.gateway_key: start_response('403 Forbidden', cors_response_header([('Content-Type', 'text/plain')], pyro_app.cors)) return [b"403 Forbidden - incorrect gateway api key"] if "$key" in parameters: del parameters["$key"] if pyro_app.ns_regex and not re.match(pyro_app.ns_regex, object_name): start_response('403 Forbidden', cors_response_header([('Content-Type', 'text/plain')], pyro_app.cors)) return [b"403 Forbidden - access to the requested object has been denied"] try: nameserver = get_nameserver() uri = nameserver.lookup(object_name) with client.Proxy(uri) as proxy: header_corr_id = environ.get("HTTP_X_PYRO_CORRELATION_ID", "") if header_corr_id: callcontext.current_context.correlation_id = uuid.UUID(header_corr_id) # use the correlation id from the request header else: callcontext.current_context.correlation_id = uuid.uuid4() # set new correlation id proxy._pyroGetMetadata() if "oneway" in pyro_options: proxy._pyroOneway.add(method) if method == "$meta": result = {"methods": tuple(proxy._pyroMethods), "attributes": tuple(proxy._pyroAttrs)} reply = json.dumps(result).encode("utf-8") start_response('200 OK', cors_response_header([ ('Content-Type', 'application/json; charset=utf-8'), ('X-Pyro-Correlation-Id', str(callcontext.current_context.correlation_id)) ], pyro_app.cors)) return [reply] else: proxy._pyroRawWireResponse = True # we want to access the raw response json if method in proxy._pyroAttrs: # retrieve the attribute assert not parameters, "attribute lookup can't have query parameters" msg = getattr(proxy, method) else: # call the remote method msg = getattr(proxy, method)(**parameters) if msg is None or "oneway" in pyro_options: # was a oneway call, no response available start_response('200 OK', cors_response_header([ ('Content-Type', 'application/json; charset=utf-8'), ('X-Pyro-Correlation-Id', str(callcontext.current_context.correlation_id)) ], pyro_app.cors)) return [] elif msg.flags & protocol.FLAGS_EXCEPTION: # got an exception response so send a 500 status start_response('500 Internal Server Error', cors_response_header([ ('Content-Type', 'application/json; charset=utf-8') ], pyro_app.cors)) return [msg.data] else: # normal response start_response('200 OK', cors_response_header([ ('Content-Type', 'application/json; charset=utf-8'), ('X-Pyro-Correlation-Id', str(callcontext.current_context.correlation_id)) ], pyro_app.cors)) return [msg.data] except Exception as x: stderr = environ["wsgi.errors"] print("ERROR handling {0} with params {1}:".format(path, parameters), file=stderr) traceback.print_exc(file=stderr) start_response('500 Internal Server Error', cors_response_header([('Content-Type', 'application/json; charset=utf-8')], pyro_app.cors)) reply = json.dumps(serializers.SerializerBase.class_to_dict(x)).encode("utf-8") return [reply] def pyro_app(environ, start_response): """ The WSGI app function that is used to process the requests. You can stick this into a wsgi server of your choice, or use the main() method to use the default wsgiref server. """ config.SERIALIZER = "json" # we only talk json through the http proxy config.COMMTIMEOUT = pyro_app.comm_timeout method = environ.get("REQUEST_METHOD") path = environ.get('PATH_INFO', '').lstrip('/') if not path: return redirect(start_response, "/pyro/") if path.startswith("pyro/"): if method in ("GET", "POST", "OPTIONS"): if method in ("OPTIONS"): return option_request(start_response) else: """GET POST""" parameters = singlyfy_parameters(urllib.parse.parse_qs(environ["QUERY_STRING"])) return process_pyro_request(environ, path[5:], parameters, start_response) else: return invalid_request(start_response) return not_found(start_response) def singlyfy_parameters(parameters): """ Makes a parsed querystring parameter dictionary into a dict where the values that are just a list of a single value, are converted to just that single value. """ for key, value in parameters.items(): if isinstance(value, (list, tuple)) and len(value) == 1: parameters[key] = value[0] return parameters pyro_app.ns_regex = r"http\." pyro_app.cors = "" pyro_app.gateway_key = None pyro_app.comm_timeout = config.COMMTIMEOUT def main(args=None): parser = ArgumentParser(description="Pyro http gateway command line launcher.") parser.add_argument("-H", "--host", default="localhost", help="hostname to bind server on (default=%(default)s)") parser.add_argument("-c", "--cors", default="*", help="Allow cross origin domain/url") parser.add_argument("-p", "--port", type=int, default=8080, help="port to bind server on (default=%(default)d)") parser.add_argument("-e", "--expose", default=pyro_app.ns_regex, help="a regex of object names to expose (default=%(default)s)") parser.add_argument("-g", "--gatewaykey", help="the api key to use to connect to the gateway itself") parser.add_argument("-t", "--timeout", type=float, default=pyro_app.comm_timeout, help="Pyro timeout value to use (COMMTIMEOUT setting, default=%(default)f)") options = parser.parse_args(args) pyro_app.gateway_key = (options.gatewaykey or "").encode("utf-8") pyro_app.ns_regex = options.expose pyro_app.cors = options.cors pyro_app.comm_timeout = config.COMMTIMEOUT = options.timeout if pyro_app.ns_regex: print("Exposing objects with names matching: ", pyro_app.ns_regex) else: print("Warning: exposing all objects (no expose regex set)") try: ns = get_nameserver() except errors.PyroError: print("Not yet connected to a name server.") else: print("Connected to name server at: ", ns._pyroUri) server = make_server(options.host, options.port, pyro_app) print("Pyro HTTP gateway running on http://{0}:{1}/pyro/".format(*server.socket.getsockname())) server.serve_forever() server.server_close() return 0 if __name__ == "__main__": sys.exit(main()) Pyro5-5.15/Readme.rst000066400000000000000000000075461451404116400144250ustar00rootroot00000000000000Pyro5 ===== *Remote objects communication library* .. image:: https://img.shields.io/pypi/v/Pyro5.svg :target: https://pypi.python.org/pypi/Pyro5 .. image:: https://anaconda.org/conda-forge/pyro5/badges/version.svg :target: https://anaconda.org/conda-forge/pyro5 Info ---- Pyro enables you to build applications in which objects can talk to each other over the network, with minimal programming effort. You can just use normal Python method calls, and Pyro takes care of locating the right object on the right computer to execute the method. It is designed to be very easy to use, and to stay out of your way. But it also provides a set of powerful features that enables you to build distributed applications rapidly and effortlessly. Pyro is a pure Python library and runs on many different platforms and Python versions. Pyro is copyright © Irmen de Jong (irmen@razorvine.net | http://www.razorvine.net). Please read the file ``license``. Pyro can be found on Pypi as `Pyro5 `_. Source is on Github: https://github.com/irmen/Pyro5 Documentation is here: https://pyro5.readthedocs.io/ Pyro5 is the current version of Pyro. `Pyro4 `_ is the predecessor that only gets important bugfixes and security fixes, but is otherwise no longer being improved. New code should use Pyro5 if at all possible. Features -------- - written in 100% Python so extremely portable, supported on Python 3.8 and newer, and Pypy3 - works between different system architectures and operating systems. - able to communicate between different Python versions transparently. - defaults to a safe serializer (`serpent `_) that supports many Python data types. - supports different serializers (serpent, json, marshal, msgpack). - can use IPv4, IPv6 and Unix domain sockets. - optional secure connections via SSL/TLS (encryption, authentication and integrity), including certificate validation on both ends (2-way ssl). - lightweight client library available for .NET and Java native code ('Pyrolite', provided separately). - designed to be very easy to use and get out of your way as much as possible, but still provide a lot of flexibility when you do need it. - name server that keeps track of your object's actual locations so you can move them around transparently. - yellow-pages type lookups possible, based on metadata tags on registrations in the name server. - support for automatic reconnection to servers in case of interruptions. - automatic proxy-ing of Pyro objects which means you can return references to remote objects just as if it were normal objects. - one-way invocations for enhanced performance. - batched invocations for greatly enhanced performance of many calls on the same object. - remote iterator on-demand item streaming avoids having to create large collections upfront and transfer them as a whole. - you can define timeouts on network communications to prevent a call blocking forever if there's something wrong. - remote exceptions will be raised in the caller, as if they were local. You can extract detailed remote traceback information. - http gateway available for clients wanting to use http+json (such as browser scripts). - stable network communication code that has worked reliably on many platforms for over a decade. - can hook onto existing sockets created for instance with socketpair() to communicate efficiently between threads or sub-processes. - possibility to integrate Pyro's event loop into your own (or third party) event loop. - three different possible instance modes for your remote objects (singleton, one per session, one per call). - many simple examples included to show various features and techniques. - large amount of unit tests and high test coverage. - reliable and established: built upon more than 20 years of existing Pyro history, with ongoing support and development. Pyro5-5.15/certs/000077500000000000000000000000001451404116400136025ustar00rootroot00000000000000Pyro5-5.15/certs/client_cert.pem000066400000000000000000000026071451404116400166050ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIID5zCCAs+gAwIBAgIUH6vXMyAKUtztBwK3qFZ3PzeqwOMwDQYJKoZIhvcNAQEL BQAwgYIxCzAJBgNVBAYTAk5MMRMwEQYDVQQIDApTb21lLVN0YXRlMRYwFAYDVQQK DA1SYXpvcnZpbmUubmV0MQ4wDAYDVQQLDAVQeXJvNTESMBAGA1UEAwwJbG9jYWxo b3N0MSIwIAYJKoZIhvcNAQkBFhNpcm1lbkByYXpvcnZpbmUubmV0MB4XDTIzMTAx ODIwMDUxMFoXDTI2MDcxMzIwMDUxMFowgYIxCzAJBgNVBAYTAk5MMRMwEQYDVQQI DApTb21lLVN0YXRlMRYwFAYDVQQKDA1SYXpvcnZpbmUubmV0MQ4wDAYDVQQLDAVQ eXJvNTESMBAGA1UEAwwJbG9jYWxob3N0MSIwIAYJKoZIhvcNAQkBFhNpcm1lbkBy YXpvcnZpbmUubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4+xK kHgMHDKvhAh0XUiEBaMCAs5T1sMZ4LTWxPhWYbIHilmyuxubl2jCGRzQ5YsnBc4Q oyDefog53f36neYJjuI/04G50Ijif9qvrxs/1DTkre+ukvXeThU9lAtfX4F8XLE0 y1jeOSSkuPMeynStpPb3KQgrxNM1EOzTKwTZZOyeLQpZNytKwcNedPFUlJLXJbZt xWWZZUsCCnukLemq03Uc/qG/UvP7yXrLFyJTMo8JQytdsfC3mnlNeVqt0lHIU3tJ dzxl48qYFhDXDGpNDmDqv0BNTxe12kdFkBLjys/JYMNAgKVvZRN1YK4FMH3sWnvJ MAJLpSJeF1rqe5WhAwIDAQABo1MwUTAdBgNVHQ4EFgQUwsK62EoZPhoTBMNw5Fde 33m9XnMwHwYDVR0jBBgwFoAUwsK62EoZPhoTBMNw5Fde33m9XnMwDwYDVR0TAQH/ BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAPt4W0fZ6Upl7mZx/3YrpW7pCi4aD FvJ0EfZ+Jc74cMziegMrXe8C6UECybdWCT2UyR1wzwJ8rBaZ+CTXxzLQz1Z5c5DV Am7SgKUe9j29/hVIQbj2BfEmD0ls5hVB0waOS3b7i9T0mSjZv52xqBLfnpryI1Ln r/Fa1Z2SvWxEHyQ+scFyoQV514X8Rjiugb4PickYYCSgIwTyGS7+HfpDydhxHNgO m0Bcplw1jNt3rLPZhO/B3lhIEuetPirwKVsS4P5R60k5J+4rkst0SKqFdkyWWeFK 7pQPZMakBA7U2r+XFDlBGGrqiN0Q9mVVDKMvZY3Aq53SdVPgOfFBdoy/+w== -----END CERTIFICATE----- Pyro5-5.15/certs/client_key.pem000066400000000000000000000032501451404116400164330ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDj7EqQeAwcMq+E CHRdSIQFowICzlPWwxngtNbE+FZhsgeKWbK7G5uXaMIZHNDliycFzhCjIN5+iDnd /fqd5gmO4j/TgbnQiOJ/2q+vGz/UNOSt766S9d5OFT2UC19fgXxcsTTLWN45JKS4 8x7KdK2k9vcpCCvE0zUQ7NMrBNlk7J4tClk3K0rBw1508VSUktcltm3FZZllSwIK e6Qt6arTdRz+ob9S8/vJessXIlMyjwlDK12x8LeaeU15Wq3SUchTe0l3PGXjypgW ENcMak0OYOq/QE1PF7XaR0WQEuPKz8lgw0CApW9lE3VgrgUwfexae8kwAkulIl4X Wup7laEDAgMBAAECggEAPbBUtilnzbICQ1AufpkD8qqd/rhthLElreYEQyeb6bFP zShd8bqVMDPQZQ+hkp9JHo8Zfa2FyuWAFA+L53S9nYirEcoIyuJhu40rA8/yRLNU OaenrmsRkjy5f/pcA/N9/3CPA4K4EutSEiTrboyJ+x5E4zws7Ibl1ADlXr1fQasu ZdIwgs59iWefdPIerqCE9PvKwkz/le3Soum4Do3Mb+M5rKqV9H+dmyjqd+c/DR9S xw42VwbNkTMJaSky8pidZKmlUt/U7picXxi7pBHpA0aK822jRnAmIv5+LnfV6X4P OlgpJsmBIs6JCLsyQVRtot1+5JHKcUuTeegccP2BkQKBgQD/gPZmPEObKfPSEUJP O9D7LE+tyLLfIkiLLi9sOK0XjUBvBaxppxl/koxmPck3A6VBXKn/WGh8JA5h1AUG v2N9ymcPfl7UcZEZUDXTiIemapVT4XSwwCiuKdMpCi/pD5O82Gb9w4hoMrntWnsr 2yx7dQ+j698GGI1GbbmvEzHCzQKBgQDkXZ2SGJ7/+34FulddUK/LydJcX8PRKrbU t7E7CuMcEWrV+vmgYtUOoifW/yLNUeRE6E89T46wesR6fmqT8I82HBmnN5JSKzls a1H7n6F1hihivMQJ9DpUVkzlduB2dGa6sht+78Lil4ZcP0oZfdzt+gPyqmUnqJwR B/59Dw4TDwKBgQDcC7spjVlENrtP/aE4D/IJf74XkzPJzALiKyKYd69LC1GkzCQS 0eC56AKWwzuZ77/RLPcTfJZv47WnNywlBYuv+DMOOu181Vn7jQLubTU2c7Crjw4q czQV2tuLCsT8WXgJOe5pOo8t/hH2guh1essygDy6Fhf7bgWt1C4Iw+UlOQKBgQDb G3cz1au4r/QaSs/IGMKTJPFQ8BFRf0osjpLds3R0WcHHzSX1XN5PTAYtol4h4ZDD DKH6kXq2mRQq82AO0aCWqh9y8T7S1+YgwFfItUCVIkNdeQAfDNVqVeMxxv1Wqhhm yLzY7fJutjOUDqVqD/kJ2/gtvI+RnZUgQitKkkdOwQKBgBUZ4XhDau/XUbP+wdym rPbI1sZ2lmZx+UKjhurl8Ev8GKHqU3RJo+bfIkhxl/+jHQLY/etIeIsErp4YbxSf ppQ32t5cMzNG0fvXDpVcm++e1R8dtLaiZtc2KorV+MYP2mXkSMeoqTpGII6hg9qo 1IqrIzI5XWAVTc5uX0jC7RsM -----END PRIVATE KEY----- Pyro5-5.15/certs/readme.txt000066400000000000000000000011511451404116400155760ustar00rootroot00000000000000These SSL/TLS certificates are self-signed and have no CA trust chain. They contain some info to see that they're for Pyro5 (O=Razorvine.net, OU=Pyro5, CN=localhost) They're meant to be used for testing purposes. There is no key password. It's easy to make your own certs by the way, it's mentioned in the docs of the ssl module: https://docs.python.org/3/library/ssl.html#self-signed-certificates For instance: $ openssl req -new -x509 -days 365 -nodes -out cert.pem -keyout key.pem It's also possible to make your own CA certs and sign your client and server certs with them, but that is a lot more elaborate. Pyro5-5.15/certs/server_cert.pem000066400000000000000000000026071451404116400166350ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIID5zCCAs+gAwIBAgIUY7Wi63Hix4y+QgDH6rljTntmlAcwDQYJKoZIhvcNAQEL BQAwgYIxCzAJBgNVBAYTAk5MMRMwEQYDVQQIDApTb21lLVN0YXRlMRYwFAYDVQQK DA1SYXpvcnZpbmUubmV0MQ4wDAYDVQQLDAVQeXJvNTESMBAGA1UEAwwJbG9jYWxo b3N0MSIwIAYJKoZIhvcNAQkBFhNpcm1lbkByYXpvcnZpbmUubmV0MB4XDTIzMTAx ODIwMDY0NVoXDTI2MDcxMzIwMDY0NVowgYIxCzAJBgNVBAYTAk5MMRMwEQYDVQQI DApTb21lLVN0YXRlMRYwFAYDVQQKDA1SYXpvcnZpbmUubmV0MQ4wDAYDVQQLDAVQ eXJvNTESMBAGA1UEAwwJbG9jYWxob3N0MSIwIAYJKoZIhvcNAQkBFhNpcm1lbkBy YXpvcnZpbmUubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxNxk TbBAMb6FG6oip8SrSvKsXPNZNRg4qbSezQ2TJJTxDZ8z01km5yuBW+vnb8NymCet sKiUvWmegy8z5Rz8idp2tHJP3NjdiLdwueCZ8tA49LUC85UWobxdBKRiRocsu7Ns MVAMvIWF46+z5h5NzsQTAUXtMCU5g8CjR1kAKox1JTkU75rRREOdx0oVMzHf4rgi WJfyuIlcloGWx4nFEiRfpfuU+qiUvEIwaIBPwxOuco5R1/1CoUJfAbJejggmmJ+L TdtZvS21YSZUh2xDfDVTYbumNzncN/euZwC91dplz9beALDWaLdQku5Fq0975k1S /hWRfDYp+hqMBfVwzQIDAQABo1MwUTAdBgNVHQ4EFgQULMZobyPRpnoDA09X2slN Vv5dLBkwHwYDVR0jBBgwFoAULMZobyPRpnoDA09X2slNVv5dLBkwDwYDVR0TAQH/ BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEARWouYRd/6ySsoML9Ji5D2UnU8DSm eC3KFJ8ATW65/WZD1ZcCkbjVvk1m09yPGr+Ko5kZ329uopSzrlTyncmMgC+rEXgI 1bzBkqTUexQG797/oAY1VxqQTrczjLnO1UkzKTBbFQn3BxkMRdFbU/FmVDgDZ6ix Yv2W688MoGGc8uL3BH8UM4ShBSbXvJVRvBRbqD4HwpFrIWR9+YuMnhBiI0RHnpE7 d2VEOZRfrI+wb/LlpqWkRW2SsoQRMrk5aGK+hQOSS6wCGKQkM4tC/U/5owdkAW3q OZ/4pGyKK8PHTZoH5XYcZzdKFokZZjFT/O+1HoRbHF9QsO+r5xIdbZSxEQ== -----END CERTIFICATE----- Pyro5-5.15/certs/server_key.pem000066400000000000000000000032541451404116400164670ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDE3GRNsEAxvoUb qiKnxKtK8qxc81k1GDiptJ7NDZMklPENnzPTWSbnK4Fb6+dvw3KYJ62wqJS9aZ6D LzPlHPyJ2na0ck/c2N2It3C54Jny0Dj0tQLzlRahvF0EpGJGhyy7s2wxUAy8hYXj r7PmHk3OxBMBRe0wJTmDwKNHWQAqjHUlORTvmtFEQ53HShUzMd/iuCJYl/K4iVyW gZbHicUSJF+l+5T6qJS8QjBogE/DE65yjlHX/UKhQl8Bsl6OCCaYn4tN21m9LbVh JlSHbEN8NVNhu6Y3Odw3965nAL3V2mXP1t4AsNZot1CS7kWrT3vmTVL+FZF8Nin6 GowF9XDNAgMBAAECggEAD8upRqyGMheZ4ZLgrfpzThOzrc+e0EpNvZwvA7/7lvtW biPgiixEmVbdzczbaJXTm47PenXEXYBchiUi8lbFkqATVz421z8VY3NomZmCcL+x Wj0t6/KB+t88zXMNKaCN/8+RNlG4e+XwzMib1DKJRrZn2fnM4siR2Vb7Iu3qu/8e BkZQZwjpmZC4hr0JborBRqW+U4mLPuGtVkR7cXNQ3qm/MV9xy/urmGYmwfVVeH5e xUue9B69RdUs/eNRnjmOmR56AVLJt2JXSCLrLR+TB1NVGEXZ7VBUaIUSjxgCPh6R IoPahnnFJBMUQOVGERispLc0iJi+CEakr6NJnmOUxwKBgQDxZqyd+HbgDIXoKJhW g63NTDfHuW3Dg0f+nh3owvhESFD6fIwJriE3ox7QzBlLEUSANToEnUf6ZLGAAaMG Jw3Mt4ZaoRZ/Z4Tu0LrbyYkVBGE57Qg5HX9pdBMlAVlY6ek/d5hRZGEds566pt/9 Ca2Q78edIfeTdLMClJ2OP5XQIwKBgQDQxCe02VS9kOmwrjic8cO107groP8ovJ9W 0Qm0Alc2vhAonl2lFIU9/IAhqAiA6viSnbPnM8LrLOR3717xHnS5gjYgTYGYYo64 3GeDWkUG8Tn/PCI+VRW34M+4id7s3Oh/BFbwAWMwZv9mh8Ru7UxxaBt4xa9nA3aL /IEkJyVSTwKBgQCUkjulEfmf1TVI+Esh2/NJCiK+gopirVbPB2OjEPQZmmR0ddj+ UDRTeMqLeUIL6Hm/aoLluiNFoVl5TgiWzcx5dW50MvaUvRKcpMyMXtJGpCZur0rD VDtJnM33lYf26CfNDv8pAN2gmR8VA4WRx7YSIPE67V/hWg6ehPcfSFUc/wKBgQCW OuH78X1aoQKaAvV4cz4MBZx9wPB9JydeuTTLVffey+0i3buzxM2RarfmAF6GLxDL qTLCCOyWggqzCA2BZBJJQJukqUG+IAZmnyzaSEZuFX9P3b0ir+XeGahBOu2x89JX PQ82zTjMpwHZjY/c52TgIzPJuDBd6A8R85YXJxhjXwKBgQCM1AG9kgSn+0Yuc8XI blTUWZKU+v9yIJoI+T02qZj1EwlwSR6iVSlmI2+Y72jpT0BcgPCEwsAiDro+RmmM 2CrOkOuaZAa+lKrYAR/9zoFxFpQyP0U4F5CtBv/zn1k4DXWpk9XaFgVsyQgakLRa oYBrU6tJTMtbETSwyB2F+jDWmQ== -----END PRIVATE KEY----- Pyro5-5.15/docs/000077500000000000000000000000001451404116400134125ustar00rootroot00000000000000Pyro5-5.15/docs/Makefile000066400000000000000000000107641451404116400150620ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = ../build/sphinx # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Pyro.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Pyro.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Pyro" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Pyro" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." Pyro5-5.15/docs/make.bat000066400000000000000000000106511451404116400150220ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=../build/sphinx set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Pyro.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Pyro.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end Pyro5-5.15/docs/source/000077500000000000000000000000001451404116400147125ustar00rootroot00000000000000Pyro5-5.15/docs/source/_static/000077500000000000000000000000001451404116400163405ustar00rootroot00000000000000Pyro5-5.15/docs/source/_static/css/000077500000000000000000000000001451404116400171305ustar00rootroot00000000000000Pyro5-5.15/docs/source/_static/css/customize.css000066400000000000000000000004151451404116400216640ustar00rootroot00000000000000.wy-nav-content { max-width: 1000px; } /* override table width restrictions */ .wy-table-responsive table td, .wy-table-responsive table th { white-space: normal; } .wy-table-responsive { margin-bottom: 24px; max-width: 100%; overflow: visible; } Pyro5-5.15/docs/source/_static/flammable.png000066400000000000000000000176371451404116400210040ustar00rootroot00000000000000‰PNG  IHDR€¥=bsRGB®ÎégAMA± üa pHYsÃÃÇo¨dtEXtSoftwarePaint.NET v3.5.9@<°ËIDATx^í ¼MÕÇ׈dVQÉ,šS/ ¯()M’4z¡éñzM¤”ÈØä‰PŠ”ÐkÔ(O=!I¢+Z´¨'¯½öÚ}ûö…GŸ”÷TÈØ½{w‹MݨÏùƒ>ø“ßO9ÙÂ{@!à…^8è ƒâÀŸXÁ)<¥¶§ÂÀ–-[êÔ9.>õõ_'Mš”Z²…×{a`àÀ~¨Ï5U«Výí·ß£R {*4¬^½ºl™C}Àe÷Þ{o É^×…€N:‰Ô/_VZRøKÙ²eW¬X¡RÕSá€}o±bÅDF TýzÉ‘tïÞ½©¢\Hýþøã¦MeѳAµs•úe™ªv´€˜}òI¦‹¤…€ñãÇ‹¢g‘"êTîzëø×P•“#`ФI“ I3Tž5kÖYL»Vjïv­VÈŒhâÄñ!q‹”t“é<øàƒ"]K–T_¾¡¾žï¼,P£Æ±›7oN ñÂè4£XµjUéÒ¥EºÞÑ=õ`ßê’ e ~x`´JI™ :µk®¹F¤hå#ÔÆ¥NÀàëT‰C„;*T(“±"iæ0gÎ4k"O=*P_O‚ÛnrI3SKš¡ìÚµ«éÙMDZžt‚úc ¬]¨*Uî+^¼èܹsRÂD’ë4C˜8q¢H}DÏ÷¦ºR_¯ÆCåe[!’fà¾,øå—M5ªWhß:*zjrÇ;V©ºÇËŒè¥É“¯á߉ GéWêPµä#á¯ñ˜öœ @­Zµ2ÍpŸq¬^ý]I„|©ý£—/êÀžuªÅÙ2º?üaœD™‚JûöíEÊ]Y=ÝÑço©âÅ…žÊ—/Ïö" Š…|kfðч³ÝDÏg÷;ü5$ˆ¤]®’'A—ë3HKšAlß¾ý´ÓNivúIñDO·I°òsU®¬ÐN¸V„<’í.ƒ3æY‘úìÆ>x5Øð7ܧ< ÐogˆHš)üüóÏÕªV©uõ¥jï ðûJUí(ƒÉ“'':jü/SèׯŸH§2¥Õ·Ÿ&H}=ž)€–;DÒŒMY©R’È¢Ô=½“¢>ìZ£XBbþ¼ýû÷s0'ÔWú€·iÓF=«¨ß¾K0ø`š*&©õ0ܯ\¹2!º…vSúxóÍ™-"ðüˆ¨Î.»HfD7té”^-iš@ô<ñÄEÚ49Eí^ë€çFZú‰:¬”ð¶sçÎ m<ï(Í<ùäpxq,aŠSŸ¼îAýmß+7À¸3¸ýyœÓì¬={ö']8w¤€Ÿ~úéðér]o­çƒw)ìòn»°Øó¿.“MŒ€4ú’¦€ÛûüM¤~ùrê»y”]·H^Q]%@2z< êÔ©½m[z›ÒÀ’%KJ”(‘°ÖóŸ}"·‚„ÿIÀ¾¬A]Q$U¤G$MðÜV­Z‰Ô?¦ª‚¹Ç§é‹³D·ñÃ@·3&(1¾¦bÅŠk×® ‡¯é%=̘1C 3"öbÒho‚šáW¶ó¾ÞçîuªUK-i:DÒ4ðûï¿7nÜX¤A³3Õ^¢çöÿ©*GFï>¢’÷Œq̧ÿ¾­DOß"EŠ|þùçA†oצ€‘#eí ZÏÿ¼á=œ§ŽQv¹•IóöKÞw90èv< Z4oQƒtQЬ_¿ÞMô¼áj_t¼&ÆbÖ¡­¯íà½R®Œ€^À¯¼òJ&{mAгgOqìA޵ |Ñ1VŒ!F#,¤‘xäyÔ«w|Aú’(_~ù¥›èùP?_ÔÇÚ~¤´uôO_·Û'Áæåª¶ìv­{ì±d¶ïû ´ž]$«Äޝ®ÐíÔ1~çÞýÛ÷ªbyaäâ„ÚÙÿ†À˜ Ĩò ¬Y³Æ7 “º°à˜>}ªg pã±ÓŽa~KgguœÚž˜É:50Á "(H\çœ)3¢›o¾9)ºú¾¹€@ô¬_¿¾ø­-› ÷Of(ôq´†uÕ‹OY~>š¸k(ܳÄvWO'ˆüq¼Hu‡M—÷elS,Xà›Œ‰_X@ 6X¤ʈùï#tAóÈšýUýï¿Öe¿~«XrÅF„Œc°Ì"íxN‹+/‘;lÞ¼y˜ €M›6•)# }JÝØÑ•@ÓÆæ ûªP^½6ÞÚ¦ 6 ?í߸Iÿ™7(þDS sÊ”)‰mwÝ»wIV¶ŒúñKWmýÎ)ª³SCX ¼ºÿé㣽½ò/ëÌœÞ0E°<‹­~½”û’¦€ùóç—(!‡øzÊŽœë$ ìH¯ bà È0¶u´±Oø€x7ï•AƒùÊ ^•ZÐzžwÞy"±êÔTø‘ÇgÐnnUnàA¤;Ä”Žˆvíe¾à]ÅV±b¹uëR¨%M-/¿<¹ˆ”\†ì=E$¦æ±ÿ­Qý¹ß¹©B9¿û¶§4’Õ¥K—‡·ÛRÀ¶mÛj×®-~S«QÉ2Î$@@ ÔõÈìì»etŸŽG¬ùBžoMVb.¢bÅž?žb&rI üØÃ"ùd\ðž“Þk-¡Žýð_¢¿*[ZqKûüÛíØe ûõ v,ðȾ_ ?±E‹)ISÀºuëJ•’ü@”êq½ðñÿ~ÑÒÑ_ÃbMx–kê¬_ €’%TÇöΜÄMÚiÍN‚ùáò6ãf*˜1cZ"#Üëž”À`éÚUNlˆ8o—ÖÍ—3úL˜õM#qAáj{~>†?”' u]œûÈ2³‚¦BKšæÍ›ç–]fØ÷—-° ”((’oõjEŸøõÇÏ”»ou} –ü-Ä6tHøZÒð »Lóær€VýÚ2ó5 &nÚÙg¨7''Ok7§ûÇÖ·nðú8‹? ’‹HZ>t-iøâ+:»¡*€ q>›ÃØRQ ¹Š ù´~ùF‘NåÒÖÑ[ÿrZ¼7ªuäçôèÑÝ‹«û{ÈlÙ²¹fÍcÅw‡Å¥¦ —É^ÒAižïzvÎ+þ£ê›oqFŸøêsù,ϦÇâÅ‹|ñÅÁh÷êxàDjrˆZ#zÆ’ào7£5;/ÏFp}Û œ:g6"žZÒæ‘ûÆ£)D_Ò0 Ä·¬‹ÖÊšF÷ÀªÿÝjö+NÿgÎûoð4¤ÉsÏòGôÊ®¼ÀECtá*Zô é¯…f¸ DÏŽ;ŠÄ@- ¶KœãGDì*5««ÉOGaxø)SÌd˜ht;¡ž¥~ýùDðÔò¶lGˆÅÖ°aðò’†ÀÇP\ŒŒV²9î=&Í…Í#öOð›#øTû%öìbÊæ¹j••xþ—(xŽg3ÉæìŒè§%jñùvã<‹‹Ø† ÊJd—iÖL=aÓnû~{ 5ðÅ€¹³,¢bÑS¥&ŸÖ<„4a”Z5_N#j'"ivì ÐUÜx•r[Û}w8]óÜ4²ø’âã”<á0aÂQôäSß|Ñ•Ûb®²‡n!®¸ù‰hò\Tèáv®Vgœ¬¾ÉûÍid’€i  "°2~ƒï‘`îÖÔqÇFg!·ÜÙ]JغBá/,¶=zdäÔ&W³øŠ—·‰g½0öúoPúj¢/𭶬ˆüþï[Š,NHúœ÷i±Þ($÷Ð=Äã†ÉïHxžNIb ˆ“]† _Ú{Ò%bBübXÿF›Õ×ô §þx<^1m\|˜Ë¦–¹G#‰Æ„Û…^˜d^Òd óˆ›ÖóÎÞ¢ùY3õÇcŽwè«5Ì$£@ýfN”«@571ÆAÍ»¼^•\ â{"’ΘñZ2“ )=¯¾:oŒi‹˜,ÏͶC#ÈýàgØZ _yôÉv†]…¨Úªõøý‘Ëöólp< °þ‹iÚéèÐGÓÀ®];Ï<ó ñóNmìmp7߬…H·6s¢Eä¥S[SaÞ¿=ˆõô`ËÇDs$f@c<éoíýÀ5½»Ê½•+WnÆ ‰aø §¶èÙÊöŠaèó“¸l@ßx4½GwµéË#è©Ç×Ò¿c¶·ƒr,ßÿes\‰ˆMqȃ‚„޶ƒáÌçÓƒ^Öñ2yqÄž†{ï0räb÷øÑ{&6 ú%öë‘a0ö^w…3:+l$Ö0€•ø¯Í Ùº¶îŠl’y™ø÷âúèf*¸í¶ÛâOòÊ:­N‚©±û~X !o½diùñù9ë4Ëm–‡¸‰£¢‰`&¦#Éê‘£(?ƒF–01¬cž‘ò‰½Uì]ødˆíÐC]´hQ â€ÚçÖ[{ˆý£ƒBXßP¨ûÁ\áèzÉÅñ´¤ñX¸pa©R²¦ÏÎTЫ×Í꤆ùŽ“Q &'¬£]«œT¼¶îÓÍT@ÄýÌ™3Ý&+x[¸e—©~LÔ''Üïi#Ç»*s‚þ¸/lïÍü©rfÕ¸Q}œE \˜9óõ¢E‘ÑÓ¿5è×jøX”Û¢C5eذa@ô ”]&(¡Ý®'K Q’©;f噸Sz´9_ž–GQ*!±È3à‰'dÑ÷ãÏßLí¤”:ÐùWªCäZªwïÞ¾(\E‘ðH“=âS Öqò$(]ºÔÒ¥_90f@¯^²ètÅË^K¶mÛ:"î`óªVM.¤™%hò8ì°ÃØÛÚ'€Õ«W»9Û&ÿølP૯òq!'7n¬X!ŒðÜ,±% R@x3€¿upñ:É’4y ´lÙÂa)a*;e—äiÛCåÊ•-ryËû0 ¬,•_SñZŸØ²jԨѵkW±Œ®«*i‰´Õ²-y Ä cò6ÈxÕ²$C,ÉP/„{³„@ÄdºØ·=wç¹ÛÇd)°sVî¾ß=uA{s7w(­áúˆ_/ÎÍý#îFlÏÊÜ %Pê¤ØTGÕƒr÷,ÀŠÜ ‡dH!vçóÛYödH±Å) @ G·& ¸6¬—ãvy¤ïßÔ{S-gÛ4­Šð5¡@²6φÿqKž±íB€Fˆ`6Š‘pTŽ3«B€‘ŽCIi9ÈC|¨Ã«¹TI+3XƒBØÍßFéKŠ âÙÛñSXؤCM²š†/;3vBoYneÝ£Ä)8ÈZÏAÞšî­sö( Âo»/rà Ï:ÙF‰žduaÚQŠ<Šbª1\ÆûÝ–7#k¨fM¬à'’"’FÁô¶4ÿ¤Ä ›¨Øþ·|Üqj'‡cˆŽ Y%˜cæ‹ÈÑÕ£‹u=©}¤€l öäÏäN2ïGìu­®«8¡Ü«/&•’iÿè•Cd@lMÔ;3¾ôŒUÌÁÑ ë°‡¢Î–x·›÷AZ#)…G’.a¦R«˜F¤ß”1ÑRO|Qf@àêG_—Ѫ߈»*¶Z`¸1QOmÏ:z÷þ²xvŽ9Ú5ê‘€Hóñd¬qKgO6c`ì“]Í-ž’ BÍÔÛ¸örUÚV£"ã€"þ é×pŽiÔÔsÞó1Õ+Hú¿çç à.M¾ø´°Ç&ƒÌ¹¦»?é c™0yÓ` _¼£`wŸ½é ™7¼ýr4Ò‘{‘›É D<•NL»£»0tØ3’d#dô›§§ÀXd}öñè”oP'ò Ý:E6q'7ÊÙ°$:r »=z "| u‘+û `µ‡Žf¤³Ô›ICN}=¡2Æ?™øV{'ýkË®i êF7òƒåKk£‰©©ÆÄâ`AÜBP†Î%àH¦Æ”ø®D˜èŒžº‰À¬ìë3“ÆFÄŽ¦õH[á <8ϲWÌÓÀMt8ùs×-Ìw ¾?: tÝ-; "BxVðd+¹ €=i! ¢íä ¿$Œ»ç êÂV’GPµu9­u\d†î¸%J²ØzÈ]¦i¨Go^ͬÃöþ–ìçà¨Ê>`ø3W á‹l/&7'©')fa2wëïgïÆkrÀˆ´læ Àí¶<„ö9H3m¬S ò_+NWc²Ïƒ‘^sª€Ý¯ ÞDƘþ|d±5uj¡;鿨@1v ,•†G' €½üËã8 d*ìñh=H"mñ9£¹:&×s7s u–½r§N¦E|¯äÕ³ ë˜o |êQGÆ[üÌ{ÝŸ¿œžo†€DîK ²ˆï¯jJQÆ!k&@ >åÄïúXüzæqKv¶7¦þªw§F™@÷ëóÅ,im˜„gÓÖ¬ӎͰΠÁήóù^IÏ6¦ Ù),™ukå°Ò4¨›£“f"ù>Kúæ0­S ëÆÖ”´G:ó3»MûŒIhôDÿ|»*U’ÊPĆfOxaÄPbÍ. ©b¡2š *“²ý„@…^]KÐú€Ø'º Òº9ÔŒbͲìû?,ˆ[ØÙ‘"¶¡œèÓMPE β§ÓH¹ü?…€:Ç[ Úö•Î•Š­&ñºŽÆ¤–€)´†ü÷}^n°`^«ž¦(F2)z¦x$üÛ«Ïɺ Tlh™D{ïù¶z¬™2%)Êèv0»Y â$ÛƒáÀQ5¨£X*©i·er Æ1}¢e„”¬ÒæŒIclæyõ_Ÿ|TxCû·ï-á UΨ·1JaöÅ6—¿ïvuþ9–’ƒy‰òjüpg6$º2ï3#O® |„"†~j/šÐ-?/±Ô‘ã–ÎÎÌXÆJÁj¯S2¦áøs€®éV7TäšÖšmf‹ªQuœ“Ø*ÀþÜ@kR Ûó¸±Î£2¢@ªuXðÝê Ä„øÓ_g‰ž±*~ÄPld~Š*¦‰ȇ`âM EôQ(?®j§¨F­Ϭs)$}ˆº ”¿e(Ü63;9@f@æ‘,iÆ& @€tl2g=:Þ›š3û•mòÖ;šw¦ä 3à7›I~Ç©BD-7.ˆÍ:L1SÎûLùÜB*bÌññá® 5jÂyzl¾R>Ÿó²Â{ÆR³qÇ¡ŒV©‚¥Ê×^'ø¦¹}$´.W9s`)ÅžzÔ{*P}*2z¤W§Ù‹s‡E\?ýŒd½óˆ‡œïLBe¤ðÓ‰uMˆ;þ9?ûÉ {ÀµôÛO³ä± û @¡È‡L{œÇ´róµ–¿&Ψ‚9Ov9M¾ë¯Ê¡Öæ@Œ®:\@Ï€ÑX›#®$ÜCoPŸq»Fžh˜n( fŸØˆA“Ê)¡»}wŠÕùÄÉêÛ¸Aþn\Æ[ñ›ªžº^ƨG¬kpÆT´ßüÅ-θdýýÖH–_–YžŠ¤âåFôØÚTÏ2äò&<ôú«¬Oæ¼} 6ÀʧNÏ|²áÛù†f3+.þšx~qèB„{,DCrm¨¬p¤Ñ {ÁÚÅ¡«"å½e„a7§ÝŸWË…¬çœ¥Ì¶yÝïÒ¡u%†ªûîÈ1 î‡ô˜B)Á<°ŸeúGÕÃâ„RšF툋*#<÷ä¼Üã.·z&5 §ÕËÚXlWâ7^ÚU3ziþK£p!~¸¸dQ͇- ÏjßZ®ucƒº–ÍÙ€:m+ÚŽ«ËÛZæUÙÈœ< {)¯Ë¡­tæ¾aDÌÛ£…×`8í$ëË9(¯ÅÚk€‰ñI ê(ª‰1`iïNÍÇLµ×3ƒ @HÍȉ£­!FoX~hÐH@yÄžNüfåÄ2Ã4â·@gÝ×nKºì.¼”†Ýë)f†g[ªlÆ8©{ù|ä ÕA¹·I>–óÌ9Ãç0@ÿÊÌxÀÀ´éäÌ À"ŒÙ}ýWÖwF,“F[/gê‚_ÕÎZÿ-?íòŠJæ€9ylsÔ#Ö{¢¦æ7f/ZÇöÖï÷§YoUû8« „¶©0Ñh‚âÇGÃäg7Ìêëy‡¶¯ ÑA‰_ ‚KÒô àå`Ê>àbœ‚à]z@Ý}«s¼ˆàO—ÃŸŽ™Glˆ.w ^Èéj¾­ZæÀ$ÍÆ¢G›:& isi”ª§Â9„ò"©€õSWX„÷ÁµÏ¬Ÿ¼¢vñdžÅF2‰àÀBÃF±({ ø^{X%ð1PEÈ>ףׯ+ê«Ã<Ù˜€{+ ׿3ÅR»2,°E U­ƒ`qÿ²Ñ>Œ¾jG[SïW~ëTÒLÖ¸9l´w× š÷†uA·ë"û„ d< à§œ7NýD˜ÐˆcappLâa¸R»âjÈo<ÅaÓ,3p30ÑÇÎcC¦C~ Øp1 ñ»ó•Ö£! Eó"LP„"Î3u8ÿÌ`ë5X9ø ç7² û>ÓÁ€ÿbT8±¡ÅâÇðôæ "†Æ›uå@ àœGæá7œ.ª_àx¼¼sH`€_85:K@Às„ß â  ‰¦ vbûŠS˜Q€ Èi‰…óŒ}lÜN{‘°®Æ¾n¤‡<¯q6eÜ¥ãÑÀ/=þtHœge6Ýrÿå$â7oÅrª«»™‹ùm¿ž¹¨8ÏÇ"3üù.ÇyV¦m+#K?¬X¦|ŸÏ¦ˆ&}²`,þÀ½¬fÐ `£W¡$¯fâM›¨Ö--g?FhìpŽí ðçFÆKb…@ý¼^f]“ ôð$v¥÷Í–¿ AŸ†'FBËØ¡Ñ⃙E¬T¨ðRÀŸŸpa‘ Íce÷Ò¸ù‚öíÈÝØ(ͯÖXËÀ~6ÖËÝ·Í#uqîÞµ¹[oÏÝÜ){„L­}r÷¬òÌ—L Àì½)M[˜dáÞðR•ù Ï.%IEND®B`‚Pyro5-5.15/docs/source/_static/pyro-large.png000066400000000000000000001145131451404116400211340ustar00rootroot00000000000000‰PNG  IHDRw‚½èˆàzTXtRaw profile type exifxÚ­ši’¹v…ÿc^f\,c„wàåû; Õj©õág«¤"++‰ág@ÊÿúÏëþƒ?¹÷ìriV{­ž?¹çoÌþ|^ƒÏïûûÓÚ÷]øõº³øýPäRâ5}~¬çó×ËßÊßëó×ë®­ï8öèû‹&ͬÉöw‘ßRü\ߟ]ÿ®hÔ¿mçûïþØbû¼üþsncÆKÑÅ“BòŸïŸ™«H= ^ëûÎî¹’xŸùíà}ügüÜwŽ?°ØŸãç×÷Žô3Ÿ~l«þ§ïõPþ¿¥¿¯(Äï-ñç/Þ8=üÈé?ãw·Ý{>»¹:ÂU¿›ú±Å÷Ž'áLïc•¯Æ¿Âûö¾:_æ‡_dm³Õéüäf&Ö7ä°Ã7œ÷ºÂb‰9žØxqÅô®Yj±Çõ’’õnlŽüìddc‘¹¤¼üµ–ðæíšÉŒ™wàÎŒÌýúå~¿ðï~ý2н*óÌb/V¬+*à,C™Ówî"!á~cZ^|ƒû¼øßÿ(±‰ –fcƒÃÏϳ„Ÿµ•^ž“/Ž[³ÿôKhû;!bîÂbB"¾†TB ¾ÅØB ŽF~+)ÇIBq%nVsJ•äÐ ÌÍgZx÷Æ?—QhšFjh ’•sÉ•~3Jh¸’J.¥ÔÒŠ•^FM5×RkmU85Zj¹•V[kÖz–,[±jÍ̺{ÆŠëµ·n½÷1˜täÁXƒûfœiæYfmÚìs,ÊgåUV]mÙêkì¸ÓÜ®»mÛ}¥tò)§žvìô3.µvÓÍ·ÜzÛµÛïø+k߬þšµß3÷?g-|³_¢t_û™5.·öcˆ 8)Ê‹9ñ¦ PÐQ9órŽÊœræ{L.¥YeQrvPÆÈ`>!–þÊÝÏÌý˼9¢û¿Í[üSæœR÷ÿ‘9§Ôý-sÿÌÛ²¶ÇƒÛô¤.$¦ d¢ý¸iDã/tòコ?þâä¶ZKg4‚TXKm¯a±4Ô4\¸;6 <FÈò4Ÿv#l§/ŠÞ/ dŒ|NßkKlpŸBPd"XDcÄŤþ ÂŽ]„[ë ™LNƬ±(ƒ!¶Ökú:©B•¤M2öd6MT,ãvíd«µÀË«ÅE‰½j x=TXY¨kÛto¼}@X±¼p°KÇq¸0¸˜ ý»\b KòE€ÁœÆd•â©v{(ð0Ž*·±°b;g 3 H#³°àu*ƒÏsœ€40`Txn/e”@Xß {X …C­ˆàÙ»®ð""Ç.J½%°ð€°C¨Œ ®›ÑmkÑ»H0 žÖZðC9 VÑpð³q¯ÜÂl:CܱA'p'ßJ ãö(ZE¤^ü“c*6z5*‘3’YmDimAòèdcŸ _“—W 1‹ ô¨»Cà­P€mڃɱX4“ñ»‚{Wë$WPgfåð²B1 ¿¡˜Œ¸S÷#Gá ì 0šÖ®WE ˜Mdßní«¨Å,‘¡ÑÙüf{S2À1¯|¸f–Ø!TÚuåxŠæžÄòî­×:ÕÔ@r ï &5¥éº«ni$€h¿[q%‡7BÛW£ãO±óæzÊ·©d;S)_¤W[›HI?”]*aòª65ìÚ¤&ÇV~Íf:YU½Q¦µ'¯Ñl¶@]Ôd²ý¹zA‰ÑŒ†'>}ñ GÌNÇ,ß1R°üÔž$·6ŠÜ:åçBbm‰~¿¸78 ÎA0´²mÕöÐTnb ¯uá•®E‰$¨cs JÙLM:),‰aKà„Šá9Ô3B„”‹ a”:£[Q;/–æÒ0 52P<•VE6€Z$d"Ï™%Q“‡´35ØÆ4f.‹Ñ?ºhf¬0ç&ב¬~¿€E;{ôŸCØ !˜ `jWR÷Œ‚f(­ÓÄÔM§E&´(i¶Å®Ä$NWŸJ+nÊ)ˆ:<åMÀú¡+QÈ› g@07;à”*"92FŒKƒŠfÁ¦”´AD"ÝKü&ø1Qëf‡»¦ð¨ì& Ð – •BôјFq!̦0¯'úOÚHSJ(7 Œ£1Ðoq¯›° "„°Dìó9Ô :nLzÐ×´>ñCGóò†äI!ªáfï.Ö%>>øD±R( §ÈžBÖáîøXGìUUÍ?ˆâÝ3ñ;ƒ Ñøh3¶Š¼XHÿ¸HqJx¨°cf²„à8"TÅìÜC©ÊlÉq¤6ì¤è– F^Åëõ¼6Ê8™äN*ؼ#Š ’Õr0#øÄS;© Ê4·‹ò…TºÁCzt”Wc'°û;G'…zƒ ×ò o,cHŸ³´-w »GmôUä¥%œ®¬r—XÍs€0ó,ŠÕæL7`.…jH:)Ž1t<>ÀE|» À£¡s©Â„€N@Ó!tùC¤è±Äþ|Â=¤Ç¾p(¬£]!A°@Ð@éÑ#`AÆÿâg ‰ O#‹raçr ³ˆÀ }„žY¸Y¶’¨( “]WÒB¡ ’ÀòÇs wÏ€2&#^4ÏÚ,m¡ü°ÿ7&×P9Ôü“VØ~ •¢åQAæt&`/i{u‚z±†ò}סzjï:ð¶ àñâe¹>¤&vB ÑÀ ”jÄ-æL[w ¢Iœð!{ƒNàÑÁ®¡ÖŠ(Øp”*^z5<5Ÿƒ…OjEÈ’ DEÃÇ›„:"{¸¤ß u¿¥‡Y!í¨”ÓW=Ðû¡¿fP´ ·ÂiÔéÏWo0=¹Ü]¡Þ(Ys±tG¬¢ÕUÙӈءÔ'’É/}i!*vöÓ™F:'2@Œ’ûÅÎÆíP«&ÄÌO, œu¼V¦»Ñ{¹:4UOâók`ÐQ=)CD—z.ÿž{èüŽ·h)‘!Yx¦ö ˜ß…v»ÌáãY‘Ç^(°H‘Ã(AÏè´Ë2p£èæüP/lKíÏÇÿž½ëÒ¤ú®heJ4™Ã mâ\§âöÑT…jþ(hŸ—bÇH,Ç$~À \¥)ä1A†$Œr•ËÉmÊ”£À2Ib°p”ˆNÂÿ15x=@5"e8c™ˆT’XÒßîÌø{?1Û $˜·Q„ˆœ€ØLd§÷â¤p0eíÝödõB(ez0êDŽ‘ÓY'‘O´2µUȬÉT¦:XQõ®eô:Ï:X A¿#àœ,¸xrKð¤A¼ ×× û„L(:N?°N‘¨Z£õ¨"ZÇ{T=î£I?ᨠaöì{Ýåà+E—Ú­ø5‚íÃ¥ÀÐ Òq¡Iü”ž€!x¢S†O(䓊ƒ’êÒ%hÎþxÝÑÄØÝÿenàse4ÏÒá¶ ;GX—Ñ*†ôÑaÏ%G&ßÒ£nz³8†.o! ìȆ­µþ„ži&5ÆÑð OD ¥(.ŠaGá9tïΔ ¤Ú Ô¦cü4C’ MX>´Nñ ºX룗»¼&æ‚ÅTÓnJåW­ƒÖYPâ`ÍÖ18¬ôº®xœ){žXÐ_š4ƒe+Æ3pïÈ‚u£Ãd-×$—ë $öÊô5ö” ¿·èÚfãšléÔÒ#ó‹£+ñWÔ”š·M_AÚ@$²C£•-˜@€°íLO½$Ó:ˆ ‘¶¼`ziä “Æ ©P£•ÂB¯F‘TQYt™@²;Ìh²âaçN’@ä©{îa9_}¿äÚäÊè=Ä|,ï$dë8‚$îx%–ލÞ] g96à‘ˆÆ9™›}1cÜÈŽrS.THŽA±í].ø–ÚÙƒ@²C›õˆQÞ )ŠdÄgÀ÷¡ÏQÙ¸–5ñº~‘§ó¼]•vCÏÑËX¤JÑí«Ó0bogbÚYÁU\˜d¶©- !K¢p0ÜtŒ‹Ÿ†ÖÈ 3:6Óá;âaözÃM »MnÒez¨,‘a‚å68ËÅ„¥“ìî‡9ÂÕ1nCøÃe×0uÄyC%¸¬u*ŠŒÞÀеHÔ^ä+ö‰©Ÿƒb#×øî¾–Žìßgéà™´×Ú$õQ5r.0´Ê   †(¶Á66Ü»Þ9<`¸ÐrªhЭ]æ…Ê!%ÎÌWD#”EIM’=\#<Û¨Ú>}!.HmÎhjvéWCñ#{ZØ×"…ÒY.ú‰Z©ŠŠÏøw)ù±ËB°¥j¶¨’duu¡:ž,Љ¦SàCð¨P1hââ{.èajÀèýø)ÈûÂéQW'[|V,©£×ª"ÃEÊÉÁ0 Rß“¥åæ|+‰Ѐú³4÷PÖx:/ÃÓ€;-c#- @£ZrÕtèPAFÞöÜ¢NUº4ÄÞ†¼ š îì„!ƒÓ~u^Š»š 5]À8€¤`îPÅRìô’ñ¡®ã~EzÄq^ R(´¹Ý0‹l*®à;ÉèØBZ÷¼Á\: ŸŒ­ó¢DŸ^uØ÷‘7,vÇ=FŒ Ãa€ LϰJøËÁa*»°õC…Ý'$cÒ'â¦ÿ]Ñ–üXY`R>ÐkzÌ A¢?cÄ3‘Ä­BA…_!&Hb¡ |a+¶ b?hOTg[õI‡ŽyÓsý6@įC¶4ÞyEè˜`“¶ Üg„€cÑ’H&%²ò@H‹”¼þsÃr ^Ëþ¨µé!§|¸]® 'ˆP„ T¡f'áFoxà”ÝëÈéê t¤†‘ðv4Í™ CÁ1S§Y€“ç38¸µÜÏjRv€Å!wŒÜAª!(¤·Îñô_%PwRè öêT¬¬@ ¡tì×è«ÀÑM%Î¥†ˆëó´ÊL¬^J±)nø×QKðºÐ%, D*£]uð«(§ÿ¢Ç|Zà¯5¡¹}Ôš8ñÏjÆ`ùðØZŠÐ²’~°#ñX œøÅ¢blîÔÿ‡™ÊN,SÙakðîA±£§üÃVY\;QÜGAGø@”ß>¨WYÍS‘Hg{Ò…©§"´×8|éyµ•`RháHÍÁ´ cZVU˜pl~ÀÊ”ti‚#í%!1Û¬ÝÐè}™é1‰ö“ÇC^ÀþÃ]äš ñ„3IžzEÏ69DZcÓs4@5é ºâ–==¢”éžê$2¤ŸpxÚCsDDÿ B E€5ÝÛ¢žÀ³Û6'˜Š¾L ¹CßèÉ-0¦.Dž“Íl‰$êW„ÎÎËK%#ÍôÜ`²OóHÌ|ézÀžÝÁQw=%a|Iø"$-I_P1JüPHú"Àú[î’,bò)›ŠPM‘:Džb9"­j:ùÔéae= Tz‰”·ž¢bÎuhÿ©„õªm¼³Iþºoþ¯¯ „§€Õ£aÔ‰4‘]Á@± G:ëÖÃ\¹´Pñ)„åÞÁçª1ÆÑÁ86ÍQ¤ ßÓBÜ¢­5ùÃèq÷ßÖ®y)Aw_…iCCPICC profilexœ}‘=HÃ@Å_[µE+vé¡v² *â¨U(B…P+´ê`ré‡Ð¤!Iqq\ ~,V\œuupÁW'E)ñI¡EŒÇýxwïq÷ð7*L5»ÆU³ŒL*)äò+Bð=¡QÄ%fê³¢˜†çøº‡¯w žå}îÏѯLøâ¦ñ:ñÔ¦¥sÞ'ް²¤ŸtAâG®Ë.¿q.9ìç™#›™#Ž ¥–;˜• •x’8¦¨åûs.+œ·8«•kÝ“¿0\Ж—¸N3аdÔ° ,$hÕH1‘¡ý¤‡Øñ‹ä’ɵFŽyT¡Brüàð»[³81î&…“@÷‹mŒÁ] Y·íïcÛnžgàJkû« `ú“ôz[‹ÛÀÅu[“÷€Ë`èI— É‘4ýÅ"ð~Fß”oÞU··Ö>N€,u•¾x‰²×<Þêìíß3­þ~JËr—¥Ó¨TbKGDWF)EÕÁº pHYs × ×B(›xtIMEã * |xq! IDATxÚì½ydÙuÞ÷»ËÛr­ÌZ»«×™Á`! $@R\DŠ‹B²õ‡LÒ–ìöÃa3”é%:¶`[²IS¢¨}%Èà&Hì A` Á6ûL÷ôL÷ôôVÕµfVîo¹×Ü—[-=Õ³6¥</*+3ëÕ[îûî¹ßùÎ9ÂZk™ÙÌf6³;˜µ!™1ýξ×3»7LÎ.ÁÌf6³—vH’ŒÍf…%M3²4õ–™8÷™ÍlfŠÌäÀÝì&üÜï<Ë_úÇòþ‡¯AÓî Ä YfF ?³{ÇôìÌlf3»“×®¤àé{|ê™-J¾â—>ý^óß1Ç^ߣX|H9£hfžûÌf6³?èI’r_Mó]«%2 ò¿òÐmþø©5ú{ {úƒY–Í(š¸Ïlf3ûÓbq’QÔðs?0G% ™üêW›q±C{w›Ýf‹~@’¤3zæM´™rf3›ÙÁ]J‰—ƒ{m®Ì ?  øsñ:]u[¥ S>y]s¾ÒàÝÊÃÓ¥$RH„'fòȸÏlf3{#ÌZ‹± … 3)r}ºØð ´$ð}Ê¥É|Fœddi¦üa3$±±|ðÁJqáùøžF)…§Ÿü Üg6³™½®À ¤ÀfZ LjH (å¼ì¡§í^K¤´xž&ŠBªÆ`LFše˜$æ’ þ¸³J¨,/õ|>öB—¿VØÂó}<­RŽö3óàgà>³™Íìuv‹%I2þõ#ë|åJ‹sõ€ÿö=u–Ë>§‘J!¥Àa±d¥$¾¯)bM…45diÊ»ãëܺ¹Ã³Éò'[!÷_íð£á¾ï¡==øYë Üg6³™½–ƒ’‚ù'ëü½‡nQrµÍ3·Züݯ³P‰Hü€(ôÑZjÈÖZ¤”øžO±`I3C’¤$qÌÆk¬¯G4M„–^÷9SÙE!AࡵBI9óÞß@Sï}ï{ß;» 3›Ù tŒµôû ÿèáuZ±¡à)J¾äÆ^Æ•Í.אּ8ZFI‰È½÷T”¹g/°€2 ~¿Ás½Z š©¢ÙKy{y€öçÁkå8x1 °¾6“BÎlfÿ!yî™EÚ”ÓMœXc)‚olÂ?ÿÆÛ·×ÙmìÑéõIÓcÆzõ!0+¥ƒ€r¹D½^enq‘w.û|Oa›¾‘´å‰fÈ×®ui7¶óòÒt¦Ÿyî3›ÙÌ^kß4ËHâ˜9Ñç+7bú™@A à…¶"évx 0Àj_;¾|è¥Þ5ïù†…$ƒJ²Ëõޤi„€­žåÁ¨C! ð‚ ÍŒž™ûÌf6³×Ž–1ÆÐî%i‡ ¿Ëc ÏuþO žkyDI‹3…,—4NÓ)“´Ê(è*À&Eõö¸Ð-àIÁv¢ ²>çŠ)* Çå+%gôÌŒ–™ÙÌföZØH•„U¼½<à‡ƒ[ÄV"‹kÑJò‡kEi—½ímš­ýA<ª3IѸ'Ÿb¡@­^anao_öy{Ô¢o$²<²qåö{» Zíƒ8žÕž™ûÌf6³×àµR„.TxW±É[“‰¥‡ÄbH,}<>|-àú­ »M:Ýq<]lèyk- Ÿr©@­Va~qZP Bš™Ï—×{;;4›z½>I’Lqù3›Ñ23›ÙÌ^=Aƒ1†,Ë$†¨q.´‚ʬ-`+õ‰{]îúH?$ð4j*)i¸ ³[Ý 5– ëÓl÷¸Ò”åvßcU·Y(*ü Ä\‰‚á¾f6÷™Ílf¯Ês‡aR’eÈÃ[ÂÝkܰUb]@Z×pCK¸Þ¨§»¬2ð <Î~’êq¼½+_`,$©¡˜ìñ\CÒ³šÄJZÃ[‹=‚(" ¼<¸:¬Ù Üg6³™½Jf’Nqv‰™ °Û\SËãïYK&7»Šój—Ràþ”'÷7œ8N9“‹6ân›‹íˆ@Z¶b%Ùf¥(ðC\õFÙ«³ÌÕ¸Ïlf3;H´X09m,c™¬ÿ5 ìŒeŒÒý4Bìµ:ÜÖ‹h V4–¦ñéö<vÐaDà{è}ɱòÅÉ$­µÄ©¥˜îq¥iÙÍ<,‚8My rÞ{†ø¾—{ï3zfî3›Ù̦Ì‹¸2$)!M3²Ìpf:€—9@KðH¢ö:7UFb°´€õØg1Ûe© ð‚qƩӫï÷ºKf d1ýN‡ ­OZ‰æ¤j±PÐQH陸ÏÀ}f3›Ù4°Kh÷>ôô.øÖ¿Øf«ÕçdÁ¢€4³£¤£ýÔŒ”bì5 ‰°)ª¹ÆU[ÃH X$–Dh}Ã}ªAX(ž7íuOX‡Ÿ$†0ípaÚ™&EaÓ˜û¢>AT Šò‰"/ <ø¸Ïlf3`7°lµb~þ·øÍ'š\ÙM¸°5àsWº\ÚðöJ‚/-™u|ø$-3¤Q†ôŒ!ñã{Í·äjä½[vLH7XR¼¨à8ó}ê™I ÈqïeböÚ].îù ©â¬nQ+z„…ˆ ò‰bæ½ÿ{î.!bÌŽ$ò÷ìÄÒÚ×?àbkÆÇ”¯0sêòß¿A7TK¸ŸãkýFëÔÿ?d3ÃqpÛÐc4ùY¼VcæNÇ9Åu¸²[oȳc¬E Ã/=´Á§/wY*j´øJPô—v3®n÷yg©‹Í›e¨œ†™øa!0!ÂBfÁooqµÐSN=+$;±æ¬Ý¦\ s¯{Rñ2øüãÊ ûI›§¶}+è[gœ/Ä…"Qäû™e­þ)÷¡äÊ J05ãˉm¼´³d™+-jIxx­`߀ǒ¦Ž;|½ÿ÷éíë&1WÊu¼a-Æ0Y‚×ò4MÞýÇWS\b'þ¿o¸M ^îç!û6a.ÇCfìÔ>Îù˜ÈMî|kÝØ`xlfü:ŸUŒ±df8ÆÁ2é!¼öãÅZnîöùÇ_oŒöïþ·ÀXKQ ^êHºí6o :dr\zw S4ŽƒG¸3“YÌ`o‡+Y é0h,-BÜá\ÐÇ/ Cçuï§fƸ%É :°Ùìñ|ÛÇW°—JîóZ”‹!Q!rIU3Ýûkjúõô̸C)‰Rbt˃˜Íݽƒ$c§¤™A+‰ï)|­¨”*…ÈwºZ‰kÙ¼2÷ÚâU­zÜIJÙgv|ú"¿|BZKA[>·UbQnòcfcÂrÏÎP¯îê´kŠE×'µ»²Ìwïµ¹´¶Í5ñDбàIË3ñ<ï¼}ƒrm—r±@àûè}Í=†•#ß§P)”+¼g¥Í×v2b«hf!Ï6§š Úµ9*¥A ´r“é àïMp7y"­:ôk;\¸²Á£nòÄ¥uÖ·Úlïõh¶ûÄiFœÒÌâiåÀÝ×TK!óÕˆ•ù"ß~ÿï|`™ÏÔ9½\Å÷5qj0´r`4¤w}¼™A)És×¶ù©¿ýqOS5¥ÈçüJ‰ïºwÝ·Àw¿er! Ÿæ55ôÁåè½è©[kQJb³Œo^ÞåÑ—¤Œ†…®Fïï×[ ÿð‡}¾çd: ÌF¸ñ1öàÇ“ž;ðAœr³•r­•ry7e½“ÑJ-íâÌ’G[#p÷¤¥è JæC8_–¬%+EE5T ¡Ò=¯Ö1Ò™ƒ$£¢S tS”¬È‰©ñT'|t³Ê’¾Í·ëq]u)ƒ©cq`ìQ*¨×ªôN,ñý­Û¬µæ°JB¾ZhËO4}NolRªT(œ¤QŽœFÁZ­Q•ŠÜ·Pà•._߉P^èxw£E¥Õ¦[-S($N9#gUQî9p7&u­J±¾Ùä‹ß¸ÌÇ¿x‘o>sƒµí6q’¹%¡’xZæœÀךÀ/-{ƒ”v·Åµõ=Œ…=|ß׬Ìy׃Ëüä{Îòg¾}•3'æÈRÉ ÏÓhÅÔró¸^XL–xŠ$³4» ;í˜+·[|ö±[žæÝo©óŸþà9~ì»NRŠB’D¸Âáär/¼[ È,Ÿzß|ä\Ù£—¸n|]ã ;š…µØ¼{’ÆÒŸÜ(±®£ý":\¥90Ö„A@¥\¤3?Ï;<ÓÝâi{_$`A Ë¥´ÎµkTæ›TÊ%¢0Èi•éŠR®îL¡Q®–y×Boí”ÜNCníí²¸×¢Ó™£\.ZÏ<÷{Ü­u¼¸ódW®mòo?òM>ôÙ§xéV+ ô= G±à»ï/—{#f²r2'e… ›ø2ŸøÊN/—ùÿÌ}ü̾…Ï,›ŒD¨<È#'ø\ñ²^Í2q‚µ!Ý2Z+I(˜_~›¯]ÚæÞú¿ð—߯[N׉Ÿb`s­î½ðC:L+ÉíÝÿÇG.ñÙg¶°BRòAAçŒqî 燻¶¶É ¯Kua%‡¯ráË ‡<º–Ðï'|þù&|r—'Özt‹¯$¾'©FŽGX†Löx Ló.CvdL5ä¬N^?œáÏÑK÷»Eà‰Œ›7nQLJÌ/[<¥Ðž£h OA–f³Î§¿ökÛ{œ[*0Wò‰“ld§½Îƒp•îLÊ•›Û|ìkצÿ÷è¾&ð4—owøÒ³œ(INÎ$ÆN< ¼É4ÍØ¿¸ÃÏ~à)yqrèák9 &®·pjæ¥Á J ð‚€B†þ‘Åld´ä©æJ áSÏìð¿æ¿õØ·öR|%)ø -‚b¬29Ä?Ì;c¤ˆ)jfôÝ}^¼UÐfþöÓxRE…(@û–xÒòÔzŸ÷=Ùæ#—û¬w ¡–”|I sO=çÉu¾ _+5þ}¸Ié<ü¡×*EÎÅ+A¨Ü*e;‘<Ù”\ÜÍqºŠ¹×?JÛ¿‹à«µ`²ŒÞ !é´ñ·¸0(“IéÜ÷©ŽÈi®õØçT¶A%òñÃß;¨v)¦,$™Å‹Û\k¦lØ"žp#Ç I7œ2TÊ% …ˆ ôGÁÀªµ$i†L\ßés¹¥ÐR’Ã}~›b±@±¹í¾ÀÌÞpOÓÌ%¿ù‘oðßýíßç_¼BP.º ¨±SÚ‹¡$eÈGÞù¤Ë&>¸‚)ÀUJQ4ý$ãk7xè±›(›qßr!ifQû>< eÉ2W!ïÊm>ù͇Ûä*#ô{=×.ns¶*8QõIí0˜&ïšzm©´Ï¼´Ëóþ'Yo&T#ÌÚ}×v¢îÚX!¨ì^¡"cŠå •r¨M¬‚¦Ô1·nð”äÒZ‹÷~æ:ÿò‘M6»ÅÀ¥m.cµbz2™è©÷F€}ˆßgŸü€¶¯Rºþ…J…J¹H¡Td®ÐŒ-¿}¡Ëo]è±Ñ3}IÑ9M:oïÀ>|­rPüè§£e~üR€/ž‚fªx²©¸ÑJ©˜e•¯Rp¤S2;vçiŒ%MSºqŠhïÐßÙàš^f&Ùw­$–Žé÷úœ×MüÈC=O弘Z¡ ‘;oiFÒnðl·4Ú—ÂÐåx‡•(£P*¹„$ÏŸZ‰ŒU<†ÌLšÒïvøÖ†»V]£8¥šÌ—< Å‚›$|4áÌì wGÃdxZ³¶Ùà¿ïùå_ÿ½AB¥Œ(ö³¸“Ç%ƃz:p61!ˆý„[)QèÑì$|îÑ[\¹µËƒ'‹T ±1Hqp°“:š,åòÍm>ýÍ[‡ÛtÏZðµ Éàñ—ZVHì„øåœ7¿ærÏÌ¥ýÆM6;);á"ÊfÓ!‰Ü½R²mB·™$~ä‚¡“+´iïÝqûÞ Åå†aÛDèaŒJhÒ$á ;.°Z<\Ò8”5[k$)*éòÄFÊ^,HÑm3…˜¨X¦XvkšU‹|Ã9wWÚâyš‡¹ÄÏýßæ¹·¨W Xë‚x£¾[EÕŠC_¾üçâÐ?²Ö5þõ´$ðŸ}tçnìñ7~æÛù‘w!M2Š…ÐyZîãóržÖZ¬±ÓûßçáOþßÌBà 6Ú†_ÿâ-~!’d6H‰Þ(åûâÙ]Ã/âyž½Õa¾ä‘f‡H §¼äqh°?Ðï[’$=Ðuç@|E Zݘÿçs7øðS»„ž¢j·B°bj0é‘î¿…‡>á@°ô÷õpïT)T: ¹v¬ Á„ÉøÌºæË $­‡cÐ*^tÄjèyçÔŠÈccÊíàÑ ¯ÐdÒ•±C]¿ “ [Ef.ŃŠgÍ'w*\ï´øó+›ÌתdÅ"…0@{wNèÊ}ߣTŠ˜¯U謮òžç¯±/Ñ÷JH“ŽÉ÷<P`‰UÈ7ºsœ]¿M¡\¡…9=#Q¹ìWæ1—0 (–Š,ÌÏñ=Û¼¸] Ö †M9ÇõÆË»»têUÊ¥BîyO¯ö¤TxZã! ÕßVëóR|e¹—h´¶™ïtè÷Ä¥” 0.q™µð?÷\m¶î©ãúw?ÿVÞó®ï;ð¾¼;`ÏlOñ;û?ó×ÿ-WoìR¯œ÷kÇx÷&ŽÖ`ö‘=8 ªJÁãÖnŸ_|ÿ£üî?K¯½G³Õ¦×ïŠ)èc‡Jƒ;HAöýÃÌ@Ñ<³‘òù§ÖiloÓh¶èçÿçì2“e­áO.nñ©§·˜+x#ýè‰qú½,3d©Á˜I`8ž¬7zü¼Ìï=±C9p¿ÌuOÔFÏQtÌ¡÷Èà‡é•ÇÉÖ_$(D()ørr’/4ŠJPôÝJoè™{Jài§‚ñ•ÀW®èð3_»Ï}Ͻöó÷‚|¾¾ïkÜ÷óLQO‰ñþ4£ÿ«¤kH]ôªüîuŸk·¶h5›´»=â8%˲C'Ø1` –{Áv›pévh²º²úê|‘FkÀñoÅdÆqæúˆ¼½ƒ£xøj±O’/¼ÐãíËëè $šªt—gþ½Î^»Å0è§¼ÿáë$™!ô.XL1]G_f‘g¯ZŽz¤’ÌàiÁåõ6?ÿW¹´5 ^Ð#otÊ㞊ŸL.æÄ1½öƒ‹Œ÷bßO­$’Œö·>Ǽ¯ ¢"—ßÍ®\¤$R'ý”Î[Ò+:ÿÝÑ+Œør÷Z ‡^òÐsg‰; ¸œGk¬ÅHçhHãT7©u×W0.â%ÓaU=ôÌ7)ÙâìrJfª”l„ï“{ÓÓÙ®#U™Rø¾O©X¤^Oèž\å»[Ïs½s‹›Ñ)<çê";R kHUÀcýEî_ߤ4W§TŒF¹jJû>…R‰“óe\ë³Þrͯ­u’Æ›¢ÎÖÎu–öÚt{}Jq2Z L³”Ò• "î«,…=Öz‚Thn÷ÝN‡n·O¿? XŒð­™7¼Ùvsü{ Ú#uƒ“ ˇOþÇçØ ž§ù'xˆÿù—?ê¸5%É2{8ྌ'ŽALj;îÌâ•NLFÖ¢¤À÷ÿâ³WùÀ§Ÿ¥ÝÜe·Ñ¢ßä^Š=4–#˜CoRç+Xë¸ æîÖ6ͽ6½Aœ{o×nÑ.¯·xôZ‹È×^å ZïÓõ‰ýú #î¿‚«m~öƒW¸´Õ§*—Qºï>^ŽJ‡,(ö{íâ -#ÄÑtŒ5¥2{_û æÚ* ‹ì}ÇŸ§1wŠ‚HFšz-§=soħ“ÿ.s/\Žñ©”K,,Ô8¹ºÂw«5Tc‡“¦½MÆš^àònBs{›V«C0ö˜­µÈøÐótF勉¯nŒÝ¿wñrü¿û« Å¥Û67·ÙÛkÓïH’k_ß.ïã†G.ï°×KÑò×éX—ÛNä8ºý+aYÛéò?}øE®5bÊ¡Êÿ¯8z&?,tqëÅ‘\ûËÒ1ÃÏLFX™£{éqv>ýTçªèúi²o!È(¥ò@)SARoè}µ ÝgCªe Øõp“¯ÇTŒïåû™šDÜDâÉáûLp•””¡©‹|²Q㥛[4w9E“§Sªˆ€ QÈ\µL}i‰w,zœO׈ÑN€ºÿ:ZC¢C.ÆU¶66œCÒëÆì5£aà‹ç7Ã.\yéž÷w=xö•T-iê<öÏ~ù~îï|˜À×H‘w}9,ºtlþä.‚ªwNâ`°UJð<Åû>“r¨ùÉ÷œÕŽÿàwˆ¨à’B°Ölní0¿Üb¾î;‚ÀGÊ×7°j­%‰Sž¹Ù•vZÙ˜Ǽk™qŽ~Ê/~ä*6ú.pjî°:û—?s÷äÅñî󨤬¡ ÛLù‹ö6–Éü‰!Õ1­‰–—ð}b!¢V«Ð_YæÝ­u®%Ë®.»5“yM£.K×½e®ïl°°Û`n®L¡MW}ß•ç]˜+ñŽr‹·@È!-)ØTsì4^¤Õr“Dœ¤„ÆìS͈œFòÐ~ÈýuHÈ,´ñÙé V»zýqœ{íž~üž÷ûO¯¾2pÏ2ƒ§%W®mð?þŸ"Ë,¾/ó%¢à®‘â8Xξ8êö‘²ŽKÍüún°TÑ|çÛ$J*ûƒë IDAT*¸H¾5†}‰}w5! !覂­&{{-z]7À³Ì•cx=kéöcv; J +‹ìrq·³äPcÐ~éó7ø“—ÚÔ šô°Èó¤'n™VLqôW¿+>†“¬B”Ò4ù;Ÿü7”eÆòÿe ïúQìj̱+yPÖèMþ.°dŽCÎ]8Î\Lðïû¯©(Qíê* Œ«úàT9„V•+‰F÷)Ï첬„¢ÍXóxx·ÃÉ5„’²@oªÞÏaôL¹T¤;?Ï[ww¹ïÖ&ÏrŠã4v|¡¥ÍèyU.·JœßÚ¢¾0O¥\$ }´Qãr̽G¥2ï¨5yh;%E!Hkh«2›ŒVsn·GœÄdiˆÖŸ­Ê÷9U ˜¬÷ |¶~¯G¯7÷,oFòæ—#ØiÀÅÝæ=ƹw8uâ€û°ti·?à¯ÿ_¿ÏK7w™«FyW—Wê­ïç³÷m?„Zy­‚)&צïv ¿þùüÍRÒiià @ ô\Fû܆«ƒ84Z=Úí½¾‹ú“aíëZ|k ½8a·› „ÀûöÜYGf–HÇÛà·Ýb.Ò8‡ñíwôè'=oç}["/è…pU+® (Ö•“y%C¤Bx>Ödô¯=GëK&½üõr‘…·}åùi¤‰G‰GCN[‹œɽô!=ãíóÚµêÖÉ%e^æÈ3Q] Ü=#ù‘"¯å"Ȥ+, HK>)XäˆÄ²£FÒÃgÀæÁØá  €á’·Juëy¾×Û@+5!³‡&ø8àÔ¢€J¥ÄÂÒßÙØâù^ê¸÷\ 9Î-¶)¹!çÙØ¾Ê|³E½V¦Xˆ¦¢£¢bžÆ/Dœ™ XðSnÆ ‰SÍĺÈfÛ£ÕlÒéôˆû1i1Ã3Ó] „h)Ñžf®ès¶×ÛNa´i"ú}ç¹1i–å×÷Í—DÞ¼}ïSá*'WîÜ]’’ïiþɾÀ§¾t‘…zÉiؽG=áCEŠ¡A*WgÄäà*ˆzB{EM†MX“åÇb¿€ l–bMŠé¶è_}–Á•§È®>E(-K§W©WK?úS¨¨€Îú.ÃÑdQ/jƒW9ç­EþùžùçÂM<ž„aD(´†,…¸? A <0$qF¿ï‰JJŒtRÙÌXÒ\“Lª±Ät³ Xå.‘ÓË»ZMO$§X¸õ^¸LÒ‘’å`bÙ°P—ïû !å¹:oŸopòf“kÌ㓌‡F^£GÙ”†?ÏZëV»´Û5Êåa˜å“‰rsW.» ‚€z¥ÀÙb‡—ú.Pl[oË*í½]ÚÝýAL’¦#jfÌáK%G“Ðù9ÅÃkBÀ¶‰èôôû}ƒ„4Is‰ï›ï¹¿tSÿ£k(?¸;pwTñÜå[üƒûåbèäŽwI¥üŽ騴‡ííBoe”M6Ch¡}¬ 1QÖ@»®°ÙT±©;Ñ1‡S>c ô%Ÿ®Í·­®ñ½žïNÁ'˲»ŒêŽR7Èqœk†Ó‰€íëy‹ônêþˆ#Úr²Ž~`Œ±`2Þ÷Èk{ ÕHKL:íÂÂéáØ !2*aâ>ƒÛ/Ñ¿ú,ÙÖ-lÜ…A²dÔ¸yêpã¶ßE$=TÒ§ú”O.S©ÍQ/xˆ·}?í•Pé¥Õ¸d€ø¸V xRŽÞ÷öñî¬år™tpû¥«<ûõoqýòz­ífTës*eNœ;Ã;ÞýÝœ8w¿Ñi»À¤Ì ß ›Œë½Œ—°6÷¤MÞàÃàŽÃXo3ºa™Çuª·ÖðÂ<Ù(Ï‚ž¤g¦ u “ÊççøÎ&×zµ1­5¹Ò²†Ä/pc¯Ä[¶wiŸè:)b!Ê3Mí¨=ßPvY,¸¿Òáá3ª$€=¯J£½F§íö‘&é„"m2¡ÉSžÏu@e`-²CÜë3ˆcGkÞ#AÕ /^¿çÀýÝo=wÇÏõ‘<«ü¿ïûcnm´FIJãšÇÁ¿}eó„éaz;ØÆ¼Á.¾H\1.ßÃó}´§Q"Œéö7ô}zŽnñ4™_=„>9Šò9Ú”€N"øä»œYˆ°R£(bóNñ‡Ç FNë;Ó4#‰“ÑÀ¶¼èëâµç[–™<uÒ#¿C0UìfæJှ4|ý¥Ÿ¾Ô¢È<€*ŽX1½LâZî˰ˆM4ù$'†[x&võûƒÀ•{õ<çÝÙ‰d¨P¥"ž_#ˆB …ˆb!b®\ ¨ÖyþþïCÙl_Y& {‘S0Œµîû°–Œ†r¹„ÍR¾ð¡òÅ?ü7^¸Â ÓÅÓŠ ð ­ë—ÍHî÷G¿õ{œ<–÷üÄŸãûÿÂOP¨–híuÝu¤‘ŸnÁç~¸,Zk]`V 0Ò¢­ÀXI`3nOqiw“âÆ&a^ìK«éôüéJŒÂ5Þˆ\òÑÛ«-¾ØíÓ!Š,T&uó’ÛÞÛË´öÚôzNéøþHC0Ô©ûžÂ CîŸS¤%±ÒuŲ¿ÆnKÐi¹} ¦‚¢ùä–—XÐZ¡<•ŠGQ÷i'–ðè$0è犙­Éë¶ò=®}á™Çî½`ê™Õ»÷,Ï@ýÆãWøÈ瞢Z ò|WÝÞ9&¤Æ¦1vãYtç&…@Rª—)—ËKŠ…hÔ®Kçjc qÓïõéµÛìõ/±Ý›g/8…UòPfùxèn,„Zpq3ã±6ù3aˆ®”©¯%½Ä¾ê¸c,ÁÕÚH¬$ËœÇaŒ™(ið:T±Ä©¡Ûœ–á.Ô¦Ã@¥A¦ ÂZCš¤|ôb‡Ø8>ÔÚãÒ`cÏTŒ½,}­ÍX÷ðmwú‰A¨CJ*ôÜW  LJ.á«k–^E=Ô‰}är*N6?ö>Ò'¿ÀòÉ,..°´XcyyÅ…õúÕj™b1" ÆeöWÖÒZ)Ç9+EAY¾p[±“iæGyH9®§>ì¤4ôà‡`>RÆLÈ$ …ßþ{ÿ„ÿ›pbu™å•Eçóã\¬S¯U©VËyšþ¸ëÐ0É/Žc:Ý>{{mNZasc‡gú"ƒV‹ûkpVkE^¦ÀÑ™µ(+ÐŒr o,¨¡|׺R½"ˆ¸á-±º~“êü¼;ßU`<@ŽJHBß'(•x Ò¤ÚˆéàçõާLjQ>»ªÂ^£A»Óc0äÀl°VNQ3žÒDQÈr¡3AÈ91DWFt:Ût{ƒÑq°¹ü8°Z5 E…±nâiŸ$Ùc'y»qÆì›EË\½ywOîgNÝ[à>äm•¯øì—/²±Ýf®Rp”Ìñ‰ö©À©´ð6§VñYZYaõÄ"««Ë¬ž\fyiùù9*•"Q”— õÔ¨‹{hRƒÄyDí6;;Mn/ԨͭQº¾ÅW·41êåüã#’$–¾Q\\ï²Zß¡X®('I;´ôßõs†<ª’¶ž#MœR MÍ( t‡ü§Wɹ;ÏýÂm×ñ¨ªÅÁÌaqDDuxžBB:@ƒ–‚¾å¹–—v”f¾_°tp—â@@W![Ÿz?Ù“ŸgåÜyV–æ9µºÌêê2'–YX¬S›«P*óÉ]k=%õ¿£JˆBºn?6MxöJšWvn‰HS^ûä¥-%JºI¹:WâþÙ¿æ£ï{?÷½å<ËË ¬ž\buu…+‹,-Ö©Õ*‹9-ãû…ÕJI’Ò ètº4š-æk,,Öx|åÃÈŸý«ÿ¦ÓǪÜP9¨K‹ÍuñÃs0TNѸŸm3Ú¥Ön¾ÀâÎÕjiÔ»ÔùÊOD'«‹^Bs:Òô³ MoŽf{n§K¿ïDÃ,Ñq-Gi?àDY¡¥›°†ÁÙ¾.Òé§ô{C︊µ£cÒlJI”öX.*”ˆè⧆$‰ÇànÍ› ’^¼qߎ9}òä½îK§ÓãS_|•?@¯(b)$˜½õ$µ²Ï‰“'9}z…ógW9}ú'O,±0_£RÍ;¸ä5Ãõ¾ì»Ì˜‘WÔëU©ÍU™«:O¿XŒ0/lóðºÆÊW®ƒ•Rðâžd}³ÁŠTDú¨‰âeÎÛ:-s[WèÆ·‰qÎ9š‘çñzxÖâ8ã±ë]ԚܩZƒˆ´4‰[Ít=v'Ì8v-î\,HL/‘Q‰æ×>Möäç9qþ>VO.qöÌIΞ9É©U7¹×jUŠ¥‚ {ÑJ9ª3#I¹âIÚ” ·3^l B-Fr½‘6}ø»Èß“EÁ†¨ÖR©•ùúg>ÏGß÷o8ÿ–ó¬®.sæô Ξ]åôê ËË ÔkUJåâ(˜9\YL®ž²,#MSúý˜ÚåT*yîÙçyöËóöþaL»‹•e*Ÿ<3i~˜ô4‘ü$%#P&#.Ìq›2;[Ì/.¸„£ pÍÏí>jFŒÓý=ߣ\Š8õ¹4˜˜}‡¼;aSzÑ<{› vg" 9]Çf¨šQžÇɒ—ƭ.òìÛ_¦ÛÉF´L–¦S5œÆ!PÒ­ÀVÊ.ë]bèáÑMÉÈs7oz‚‡î*3õ'OÜ{஥äÂÕÛµÂâbJ¥ìêHûÞTƒ‹Ã”;YP(D£-Š‚ÀÇÓ’fÒä±­_“6Ú×UI K3õ¹±µG­¸Ç|b¬¼s¦ê!NBב%vâ½ýA<ÊR}=Ææ°`Ø…Í.Ϭ÷´8P¦W í¢¹á<ÁõAˆ9lõrTéåIÊÇ: ¡í9zÿ>K'O°zr‰ûÎâü}§9{ú$++‹ÔëUŠÅ¨•߸óÎኬɱ‘¤eR.ìºÔý Ïž|‡o*÷ò%–0 غqƒßüå_¥>_cõä2çάrß}§9{v•+‹Ì×ç°yUVgÝŽšÃGQ:§ÅBD†h%¹üo°pú4õSgèö£€“ˆ;~}X•23Ããµãs”Ö4£¶¶/±ÒlÓ›«P*¦ãíã´'½wE÷D=_ê!w VŽ3·ÇqÈtÈ^"èt\"Qš Ëë}ûtû+hªÞ€­SÌkÉtD'S rÏ=I§ËÿNóî.޲\r]»Œµô dL¬~Sìèï_jóå‚©Olí;˜ú§$a±t£d\iÇ/Üd§Ù£TòšÐw‰ìÒÃöÓ-N,±zr‘sçNqþü)Ξ9ÉÒÒù1ÿ>|-0DŸ?øí2hµxàoçôê2çÏŸâü¹Ó¬®.±°PwÍ'‚À5¿Ct¥rIk<íá{^î¸h´§I㘛?ÆüéSNeƒñ¡2fÿ9ˆÑk!\Ƹ´ÀöÆ“4›{t»5â¤L¸:Ù¯œÑZ¡ÃEAQeÄV3•Ç,¥b”GËúôÚm>Ä1R*B{T"Í|Ðg½žÈ1©éŠ€Aß)nÒ4%ËåŒxw!@*J¦ìÃîR¡'JyîÃôë@m¾œ­mÜ]0õß~÷ŠMyîI’ðèÓ×IS3%c»Ûu¾×¹N­±¼¼ÈéÓ'8sú$§O­°”/Å …Ø¥šêmz¸ªE`…“’9ï^"s^>³²„÷l^ã³Ï¥N3,Ž_‹ Æa;öi´ö(•"ÐÏäñÒÄ%²V …a-«ðâî.çöš´; T« YæËç׆šÉŒ!Ððè >õÔE_aì1oÑ$6K‰éµÐ­-‚ž Ø#DböåAÃkzí—Ÿ@ݺÄòùÓ¬žXäÌ™“œ=ãè¸ù0Ã0ÈA]ÞU¯Lk-¶: 7Ú¸¸:'“ê1AËL‚&–¨rý¹øæý1§Îžbyyž3gNpæÌIVO:`¯”K®ÎÊ+ËÃÆê0Õ^)‰Tc~–f\¼x™«W™;wi·ïßü£Ï“ úœ<ùN®,²ººÂÊòõù9*e<öò<îýùd"„$ð}(ŽÅ ­æëר=yU‚Ìœç.¦¼ø}Þ|.Y^HW§%‹£[‡H•§ûW 5mØŒ9Xì;/Ô÷JôzÛNkž$£ú.ûÙ%4i žtr†G¹\yìBÜ}' Ie‰ï{‹’$åÄÉe77H{´Im6òÊ…Í'cG% Øw‹ô|*¤½·C·; Z-ž»RŠ(ðX û\ì&HkIT@¯ŸºòqJ–š©úî#¥‹”H­(û“Ã΂P¤Ò#‰câÔqöãúJûÚ#äcÀ÷T/rcÎ%=›jûøFË!zên‚©Š¿ò÷Ÿž>ò¸°ÓÚù;ëÀó¼­Öç¿þ oå¿çm¬,-¿êc—ूµ=­~®Z9&§1I¢YK`ZT«eæk,-ÖY˜w:æ(ö!µòJm,ñrA¢r¥ÄòRï8[qÅÇ^É>­%>ýâ4ãl1¦¨3ÌÝ>ÖÃÞ@¨à™]>Ó¦µ³E³Ù¢ÝéJ™¾O$3NZé{‚÷?t•|õ•(oJ}|ßr">¢0½6òÆÓKîõUá®W+#ÏÏ Èn_%ʺÌ/γ¸Xcq¡N½î€=|À> ðë3,Æ8ò'k MyóLP€xlÞ¼ÉÖõë,­,Q¯Ï´ì¥\¢G«,ñŠÇé°¼mà»æµù•È'Ùk"µMDà ðH!4Á¹3A[ áIP¢ÓíÓ-_f<9Eá{šŠ/îØG'•ƒØ%%iŠ1‡D‡1¥(ûò@t'•q’‘$ɨ/«=ŒtÏg¶@K-F=˜S$ÆÒÌLôf~ã­±nn¿q«„Ý¿ù;/ñƒã“üo¿öܸuãÕûð¹ÄZv›]zýdœÂ~7T“QЕj™Z­J½V¥RÉ厾w,ÞòØ®$ž§)D!¥r‰ó'ªø4ÞÇ?~#=cI3K=‚sŘĈWîa[—¹÷ùkðþ¯n³³µE·Ý¦Õéå57ŽÏ%ZkI23ªò?q‰¿ó±ˆå_Ýæú‰Æææúý€»ÈÁd¯;`«û~ ßµˆ¢B:h”q¬'™µò‰Žmã8ÍJ„ëXùÖî¾ •Ú> #äÖ%¼íÛT *å"…B-œC‹¤ÇXq͹ì†!Q! P†ÒÊ(kצîÐ÷Q&re IDATQâ ç ‡XàjÐÝÙ1™l!¢P ƒq{¹“ºWG³wÇqLí倸8p¬£3bÿd°í2Ä9u’eׇe·Y²#Seß6æ#Gnè¤Cœ$#£ÿG‹ßeÚïDS@Î2ãŸ#ëh"DKb¬|Æe4{èÅÔwoò8Äg.øï~âãlooÞÓëì|EÕÚlñ”—}>6±!°Px®ï{~:hä8iÖΉ‚{Þî&%–e.‡Þg÷H¥!p‡bÑç£3ÞÛQèt‚Qöbø¹çWùÂ…->r®Ìï{ºÎ³g«”"ŸÈw<³í½Xóö­-¾|a¯\\ã›;xŽ5±‹û\ÂŒæ‹ôIÞxŽR)¢V«P«•)•‹Ð³î|ñï(Œ©pt’^8Ñi§âV'itª¸yÈ4ês;%FÊ (…ç:¾Gà»8é0ÝIgíÃ{•¡húw²í½Ë,žN¥¥ã8N©Q £ûé¡lr*Ýpç›5!‰q–u+ Œ4G[MÕ!ǘeÝ£Ô£æˆÛ*Å…QSw…™îÊÃÌë>À?÷ú7y\â+×ëü­Ÿù8?ñ—ÿÂ>Þîz\~àAOž0FÔ®ëâ9¶j‹S«rgÕöÎú±~ ,[èÄp“®ƒøà¬Å÷mìðµE‰çˆº¹TªÕ]ð,–vb~íå~ã•5j›jäR ]J¡Cg ØØ‹Yß°º3 3Ðø®¤˜Œ:Q8É¡Â/Ð}ñ³øïQÿÀûiÔ*ÔkUJÅkåÁZ±,Û˜ç:faSx|ðëi¹ØBß“*sNa¥òQ¹„ø8Žm§À~´ÑÑ÷©LuÌm,Wܹ4¢Þè~oLW)}çij•E.7|§ûCé¡èÒúPj2xÍÁÉA¡‡ œS[xÄpñé'#´™NRqµ¾]óP©™Ímxþö2O|âÕ ûÒoñ‡øÜE¦)­O —ÔðàN:•7ªîwZÀK öz1ƒéú¹ß1e¡Dêô^ˆ}~ð‰W6û,õ\Kß_'ÉÈ3›ñç¾c¡´`«›°¾ÛEÓ%Ñ:Õ>1.;®-ñ]³‰VéŽê’_­®Ï`ñ úÍ/Q›nÑjÔhµëÔëeŠ…Ðôtë»|ˆ;ƒLõLlÛN•S]ö{©#{w„Ê·ñûQëá÷õ!˜©µ"(q\לoË Æf¥§u¿ Ó‹™­ÑÇOºÞ'§½|$?fïöìfŸ!Øu 9ò»¿¯Ò‡´Ì¦­“ZMAô]ÎK_ ÆÙ)dÈ®2ç~ó±ôLüÍõ"?ð‘ßK¡X>ç>’±Ÿ@âžO¯ÉÔa¿ºßI‡Æ¨æ­nõØëÇyØýxçTH§‹Ô )-Ç& }Êå³Óu>vF#Iöùè<Ÿ¤§-ž#]C¹DžïÈTzØüÜá”§¾Ã9ú×JÑ}áS”‹V»E»]§ÝªS«–‰¢!äý+xèT*Δ¹ºãɦ7¢I>ºé̾Î3õýØ—Råf˶ž\ýÐPm9©O꾌œñïÖ ¨^7Ð{¾>H¥Œ›Jrì#Âaû3ïñ6E=²@è±ÅöN ¡´&Î3wÊx?ÚxïSŠ¥î_yñë÷CË<ø)B¤®7rl¢íT€=ëÀÔš—.­'f$¹×Ï!$2ÞÃÒ1–e“ß'Š|¤%yö\‡+«|u)À·Æ3Í05ævk¬?#?¥A‘½óŸÆÛ¸Jó}O1Ý®3=Õ¤Ù¬S*‘Z#eÿéù¾?ÎñJ!ûÐI}Ž1–APq’¼6¤÷íŒ k‰5ùžÒb 8ãAL}v†R³A¯×Ë9üÑŽ¥Ó13W$Ž—ª13ßÑÇO–ò$=.)‰·ÖŒe_^üåîæ4éûmö9dOC^”½½´Eq_ÛãVwÙºëñ÷ÏŽv¼èÎòu±‚ÊR'ãˆ6nJ/:оõîâQ{~ôI‡~`޹v‰v£D³VÊ;­<×FJ›^?f¯£X\ÚæÒ{›üâçßâü­•9¶ó;/ðc?ôcÇ÷ªQªDŒñr§fó½µÛ㥋Ë8¶¼¿!I›–cã8¾ïE®ë2 øØ“»lvvxc§„o±IÜÜ÷'³‹9ôS`ï¼öúµ/Ð<3ÇL»ÎÌlÛè½Ô* q/›KW÷ýËÆ 6òŒýd éIÍÕiׇÒû2ùþ§ă˜b©HëÌYv—n ‹“§hf®µF'Šé 54ÅN×õì˜ÑäÿOk†ÂY`Š[kX™qI6}{Œ –(ÍFßtâè£.hwÊiª;-ØêíÛüŒ{CPGûëf;+Óò;\¬tW2~ï<\xîõoä`þGßïòÃߎ÷msf®EáªoøP¯Öù®÷×ùñy‚o¾9àï|üs¼|ûÁ@þ·Þln®S.WïÜõƒ>€<”1áX)ÿ._zå…L9¡î1òvÀ¸?`IÙôµYŒ­I´ÛUìtÅÈ÷”ËaïÆ¬A? ó9Û²GÄÓĺfiW±Þ3Ê©.²èAÑÙÂq3}Ї¡ªºý„ÍžéVÓ™`J°âÒN»å²Vè#÷|šn¬Øíë|ŠØÖq~³ÚGåŸzá¹ð?ÿùßCüïÑévOÜ¿ˆ…mKÞ½¾Âßýù¯÷û}¥…èïà«=‚0̇¯²V>Ç1CYårv³ÆìÜ4O=1ÏMíð„¾N¢L&)wš¼×- XÂñØyéótûç¨=fæ8;?͹sóœ9cܪi!Èqlcow`GpŒcÒÜ- ÛqXðú¥r<@ÃŒc¨*U½nŸêü*O?Ãí[‹ììì±×é÷ŸvXêkVµ Z+³ƒHr@ׇî2²vÇ,£WöÞ»€kKÂ( ðM"m™{#µs(nï)6ûfÞBÀv ƒ.rgÇóM;s6£°/ëÎÚ7;Åv_å lÚg¬¸‹eÛiMÀJ©uZhv{ŠõNbÔ&u‚GœÒ2£5œopX˜…?÷ágïãäÛÜãØÜ½^¿ü~‹K××ñ]ûÎ…T}4—¡……½s›ÀV„QDúx#^™ÙTaT«¦§šÌ/ÌpîÉsüÞ阪 ȸK"Ýt<ä°¾R}ŸÙøqFgë^ˆŽlþÎ/¢¾ñÚíógæ8wf†'žXàì™fR%ÄBæŸqŒ ÒC' ÷·ÂéSv!¹µp<ü˜ úZ¦ ¸/Û£cÆ3ä$• b¦?üýÄ… ·oÜbg·C§Û3‡¾o`7[Ö{1ï%J‘{6}i¨ ¥Ì¤ôþ?ÃÏ£¥EwsƒÝ˯Q¬” ?UZup,ûŽ3æÖP\ÛJè«#h!¡»‹Ý߯MÍÊtVå0PFCg Xïê±ÅB$ÕÇIéMÛ–yWÏaçH¢YÝ‹éņ–ñéã ²…á„å §ø¡ï¿p÷¼»wáÛŸÝcÚ«=–f'8–¤Ûëó—ÿáçøâKïQ)ùĉù¿¯B˜àìÜ$j‘(2Ù»ãÚéÍen,ÏsÓ6-¸aô:,û ѵ‹¼Ýi³í·ÒBªøîáÝ:kîXXÝ—Fip=½Ø;ÿiüÝÛ4Ï.0=Õ`a~š³gæ8{f–ÙÙ6fR1ÊɳÏsW K<Ú럲ÂfÑuª¡Í¼Üæ5U R˜?ɨׯˆ:Q™G© âXaI‹ú³ßÏÖß`éÖbnÖ86÷å•ûF·ÏåžM_ ’D+M¬ VÚdïš±L>IÒÅ'ûž•$7dë—`s™Ò™Rˆ¢Ð×n:â²h­Iâ„×WÆ:òV"ÒEo-ã¢òäÆñ¶}®òÀNÌF×Xšš€…èí`éÛ¾þ0ÿ†Ñ‚ñâöÀ¼§xIG¤àž&W÷rî¿•¢QyjçÁýx@ôÙuM:­ql‹Û+[ü÷ÿà3ü/\¡Zð‰“c Š8Â…Èv`g™0Ù¢X>G©hÞ8ðØcYƒ”rlµÌ†ulׯõ®S¼zw6o²èÎÓ Û†æÐñÈ£$îïdæ?®G†¨²Ú‚DØØƒåëì½ö;È[oR.„´ž|‚©vÃpì ³ÌÏM3=ݢѨP* ¥m¥4€rçCÚì:óÅÖ#ÙÝF‹™"öçÝ%Þê5ˆ-9R4M4X i¾–J€¸kbaL2Ä Ás} ßó}l¼w÷æb®¥N*U ò®Üo®¡’R1˽„Ë=‹N"Œ>¹J—D“$f1JÔ*JÔðo5’Ý+q¢X}ùËDG©\Ê¥´ÝTqõ(‰­ /îÆ\ÚPÆgø01iÁæ®ÔxApÀ|?(Kï®è':)õ%-œÝUbÇM3wûÈì;_$¶bbeÆå=ú8BcI{dq‘ßv¼;€ëÜÿkK…Âý€»x`”×£4÷TœÊø¼ìu# jIÅž¿Äÿñ³_ä›——©ƒ{ÐY9¬!ß´Î9«—(•r‰J¹D±áû~Þ…0w@XxéP²‘ °ŒÒŸëâ‡!Åkשß|“ë+×X/œ# j Ó §“±ó{ô™ÔGÿLšêËÛ Â ¶–é½õU’w_&t4ÕÙišÓS fgMWÌÜL›v»n¨˜BDûQ Ž&“€Ë°?œ®™ÜâͶp‚…P3½»Ê-{[$$J`ÉŒÇ9ØK•‚©À€ú¨W)ÑqûÜX_¹‰XZC ¨JɜӺ„•vj½N³QaºÝ`z¦Åìt‹©©&Íf͘f„£Ì©ðÓû)Å]Öõì›â”ÙöýànŽ2=ö¬,s+iH +åÒ)Ò,Ø€¸•S3¤¦ÔÈk„J³Â8Á±v{Žõ^‡Agƒîêå( <“%§™hvËg2‰RôÍÚ@ss ØIŒº¦¡b 3Pz„–1`çßÓyM’S6)Íd n=÷BGR«U©VJ”Ê¢0À¹ƒ NF •ðâí>½DãÊ}xZ#„1I·ÖoÎV Ÿï{Gdîæ?;½˜«›1¶•7ABcï®âz¦)!£vËܳvʽ^ÌònŒ•& ]¤”#úTÖ·-¸¯oÝï+©Wj÷îú^³öqr¸+öº¶v:¬oîâxÛt{1¶cÝõ)¥ÙÚí±±Ýem«ËKoßâK/¿Çëï®°¹Û§¹!IRŽýÞvúý:¥Ð¥^«Ò¨W¨”KÆ\ÄsólèpOJ+ïÁµm ×5ž®QR)¨Ö*Ôo/³¼x›••wX»ö6[¢@oúCèò4àäGéÃèƒõ%v^ùzõn¼K”Uªõ*õJ™F³f´bÚM¦ÛušÍÕjföì§þµÖ½?(wËà&éž¼1¿p J%ÎEËÌlÜä–s[Å$R˜L]h’tוhâ2I³v5>l'²Ï#@÷clÛg»@æ~ ±ÐŠX¸¼²^çͯnáœË~-5iH§Ür“ËÑ Æ¡ ¦ÖÐë+ºý˜½^Loã¤6•‚oºî¦ëzÞÇöÑ«ïvnQ›9K³Q¥Q¯Q­‰¢÷ˆ.Qp1ÿß1Y¤edŽƒ0 XŒ¨VJ4ê–[ VVÖXYYgíÖ Þ¬±%fÓ}ÉÝ(®QªC#l‡þâeâ _£>;Oµ¾@­Z¡^+Ó¨—©7j4›5ZF•jjq…™ÙóøgG™ihö9nÏCw)noÒ®Ý`¹W'ö#¬D‘I"±ÔH± Ø‹$­^Œuúˆ\{PKÒ ¶%bËe[€Ô ++jÜ( Am T?ÀpèCÐβõDÁ@+ðI–Á«áÏh³$é÷µôö:¼û›¿@¹ÐhÖh6ªfð¬P0uëèn’Dil¯/õxgSá[£*Ž#j0BÀÒ»Ž *ˆ¢ M,ií+¦*,4ï®õXëªaϼ´ÛË8ªÔÓ–âQ.¹ï1T4Ë;}nï˜EÂ&Á×}¤eçÝ6Y¯ü·[ÇÌÎ.üóÎß—¤ð~ïñ îJËÙÔqèC.è'’^¬¡Û˵<ÙÃ4ôEbŸGš†j‚žkÎPSC%#­z£¼ïè1±òŒÊJ ÝßÅ^|…jµD³Y£Ù2€XÊzœÃ·ºã?Æ¡^J󳙎yxà«%š*kku–V7XkUØ\.°'ûúŒ‡ãTkãùÔ§¦˜žf*ýj·ê45êõ2µj…J¥H1{@}ß ¹ØÙ¤á¶s1¾Ù« à´ÄŒšq‡0ð)V*,TV8»t‘K%•fí‰&F¤¶v‚ÈŒN”¹ŸôÁÅT[%J ,­ ,!L×M&d%F×´áÄi2Ò³ž´J…±•ñîÑûŒ‡O”6…Ö èc…K\ùÍ“,^¡ýÝ Ù¨Òj5¨ÕÊD… µ¯”cYõè×Jkdóܵ½¼t3V’¤×oS(•(D!Q¤ž·ÖH!ëÛ©^Yì±7Д=Ó¤- {w—~àû^š¹g“®ã÷r¢Àš K]ö‰Y¨“mBúH»ˆ—·bZC ƒo£ø¯'÷©ßãCÏ|ä$ÀýÞůzX¹OEžHEùïîbdÈR“ÄzÜ9ø°Îco5Òy8!ÑW¾NÙÐl/0Õn0ÕnR«•)”o?ÞÍ”ýŒã^ж,\Ç! ÿ^©”¨×«Ô[¬Õ ¼5è Vâ0eI Z&*¬óóaÛÕj‰™é&Oœc~~Šv»i¶é墱‹ }|ÏK3 1¢§~ìdÔA‹Ct=ŽX嵆þ{d<‡Ãø’ZøžK±Riµyfõk+ØšúRõL¶.B™6»0âT’ÑãpuÒZ£¤éÚH„ÆNËFBŽ~h}§Ç>ýRS ^g§ÿ΋©#\û Ñ#¼{ì12(pýü—¹þù_ea~–V£š×OÊå"aä-C{¼j£¸ºÞãw¯ð¬CÔ•i¡M®½³u›âÜS !…(Äó=ÛγæáMªØíż|«;Òyc4ÜõëÆÝ* ×=ÚbSiÓ÷ya¹Kœ€ëŠzW$X©¾“cÛ¹‹Û·S¬mÀÿöK¿r_¯ýPã:ï?÷ôý‚û Læe pàý4ûÜt.­žôÕŸäúË;7iœ;ÃôTƒ™éíVjµDJæ^ 8ÙôœeéÜøÁqlãvúDQDXˆ(GÅ÷n¡–Âñ±zÝÓ…Iƒm[TÊ%Z­³sFF ÝjP.—ˆ"ÏMÇÑÓ‚é°ð'îéêyþ\—Ñc659è!-ïXš"ð&{¢€jµÌÔì4ïë"/¯Wèצª‡T–)ž¦EÓXèCÒ9ïa§ h:nÒb¬2ÂOb,‘¾Mut"vtPjÜ3ºfŒP0zX`$ ¸7¯óæ/üµJ‘©é3Ó-¦§š4êUSôOó¨Bj’h\­øì;{ÜØQ”]s<ã³,iæ~å5ÊŽ4-–Å(¯;Ù¶=Ö´ XBs}½ËåµAZœ5ï!ú{¸›7 *!QàÞç~Èdª†ínÌ…¥nZ”…ªÞ2f9i“‚ß˧›¹ÿÒo\à{Þ—ðÝÏ|àԽׇŸüçÙì îëõÿÅüXÖý‚û}¡ùñ;(ïW ÷8ïqHa@§õÜ€äöüå×iÌL1Ýn2›JÞ6UŠÅžçÝ·‡æ˜·djì8NÚaaáKM௡uŸ» ©FcII! ¨TJ4jõõz•b!Â󜼳àä Rô¸éþâ¯Ö`Ù°µ‚ìï`;õ]ÓÏγmK|Ï£\Œh4ë,lm±ùî×¹dÿ q©Šˆ,£Έ2¸ÐyãR.,ÄP‚W›á')0ôNê„5Õj“Éҡ cV$ç߇À>È ­š~œ€å²·µÅ7ÿÉOà']fæŸbfºÉììSíFš„ø9—}Ôí/Q,m÷øÌ;=“µX· E™tv7ߢ\-S.Fiÿ¼Ÿre­Ÿ:íâ±Q¼¶Øe­£ˆ‘kÞÈ­ëxƒmüh0ôSÎÞM9{y@¾@¢XÙîqu}€-%–N(é=¤-q=ßsL€´N’ùå/^ä¯ýëþü÷¿ÄŸûc¿—§ŸxòT~O¿ÿ׿xýòýRëÞU~ôßÿãÇþyûDû~©Cû(šeôe#4Æ(¯¾ÿNr’•+8ן§Þª13Ób~nŠùÙ)ÚíÕJ)±Sñ@ 3y”'>"îc;ÎØ4è¡õ`sûðY”æÆÏ:s¢tû›L‡>¥âäWë£zFÁ}ù*NÇ÷ÓåÓïtÈä!¢( V)±7=Íîö{—‡Oÿ¢XBÄ}C ="U?túÐÚ€—–F1Ò  ”4bVBfTÎÐ÷sÿÉɵmÈT 3í˜Ñâêx5öAœ -—n·ÇùŸþ;$·¯°ðþ÷1;Õda~šÙ“„ Qš„X‡Ö„4¦ËÆŠÏ^Þã„¢fý”Bx!ƒËßÀÛ[¥|î*å"å|8*ëÂïoôc^¼ÑSü×B`¯]ÓE¡¡?m…”ì0M”Âpq©Ãf7Æ’îÑEØ.¾—c3sõÓSIÙÚ†óK«x~þ|ÂÏŸÿ]þ̳_åOÿ؇ùОƶù=×oÁßþÙ/ò¹KWïû=þêŸú0Q±t‚à~Try·tZ7M?þÄæøOãuZ!µzëêW¨×J©äí4 3ÌÌ´ÍSŒðóâÔÉe–Y¹eÉ´852¤÷gè‡VQÇ3faÀÌvL½íXX¶<r=Î uíóŸQ n¾xiœ;ÂÙÊSËÞ3€÷Xûöcçøè³Orvn6T¾·¸½¿õ•+ü½_ÿ2{Çû:*~`a‘?ú±ÿäž^cs/°zbÔ‰8¢±e<›fЇ½™8"õic°=âÅ·±oœ§Q¯03;Í™…iΜe~~švËHÞéâétyŒha+Á·ç»‘CO¹ßœ¦bžÈê&û.jvQF:E„ã3¸y wñÑûÏ ‡`ÜaAì´²÷¬-ÕqlÂÀ§R-1èè÷ÏÑ\ ùÆo°þ]?͈;¤1ì)„ZZ` •vǘŒ=K‹´ª‘Ê|n)¯MäU†ŸqîÉ(E£ôEÓĈ ÀÖíÛ<ÿS›øÆ;Ì?õ$ó3-Ξ™ãÌ™£»_¯P(„9]rT'W¬4®PüÊ;¼»™˜nµOr@)„ëÓ»ò:öò»Ôž~‚J¥HµR2òé³éRQZã øòÕnî$3y‹tÛËx{+Ó Š#á‘-Öá%ÓßÞéżrsGšVÒªÚFJ£áä§Ê¬Žm=ÝzBqíÖÑOÏký×á×iùþäG¦øà“SÌOW©UŠ” AÚŽj¡µ¤ÛÐíŬ¬÷¹te™óo¬ð/Ÿ¿pOçý¿ú8®÷`à~÷¦{ÑFcœ±†câ»Ó3ûö›f .%ɵ—q–ߠѬ133Ź3³š¼ëºD¡&©'ĉ"IbTr‰‹/’g> ›j¾Ë4“¥d†·‚DÅÃÔü)o‡THÆ­ý† ž©;fS§y5Iˆ•FFeß|•—ÿé?@n/³ðÔ“Ì϶9wnžsçf™›¢Ù¬Q*ð½Ã‹¨Ù¿¥p„æÂb‡O¼µGèHÉïwâNïÂÁë¿K-t©ÕªÔk3Åez5r,k×J±ÓíóùK»ã¬§eá,]Äg@¡X Xm¥´û®½Ò ‰æêj‡7nwqm‰Ô ½´,<ßÊn;û»uN>.^];ÖÏ-u~æ¹UxnõÁÈŽ{Ž?ûß|ˆ'Ͻïž_i?™>Ü÷+Œu×ßwÄŽAìãìSÚÛCw¶IÞ{ž ·D#í48»0ÃÙ³óœ;;ËìL‹z=ã/žF}´q؇Öç÷êC˜¡Ñ­Vvh*A„%:/~géµgÞG­R2ÛûB˜‚‘<ÕŽ™q€7­‘º¥¢[©«’|—˯~е•¸úA\!!î£-Ö2my4»Ê„Æ„¡g’”È ªÙ+ÄÁuwh2JÍè$Å5`$#Éß¾ˆ¼ý eGјŸczªÁ™ùΜåìÂ,33­4ŠðSÉÛ‡ì#½ÕÇ)e<²…åˆó:îȨÄÞ…—ˆŸÿ$Só34UšÍµj™¨á¹ÞHŸ²8up—’TsÆkQ”,ÇåÝ·_æöÊ5âg?F0}­¨8FYKÏDÆ,izÝeª?#Å0}âðõpðzl¨)£c’8A[6",±ví ¯ÿ›ÉÊùçhNµ˜ši37ÝâìÙYΜ™eanšv»A¥R$ lÛ9t”Õub¥p¤âWÞØâóWzÜ´“}`ÒJÓ}áÓT}‹z}8Ñ\,fj¡Vηg]2–JøüÅ-vúš’—uɸȕ+x»+ÎÌQ*( ¡áÛ£´Ötû¾üΖéDBÐÒ¸Ä8®Y\‚À32©;Øi=›.|öÝ÷ðK`Oø;ªÊúãÿÑ}¿ƒý@Tû=´$޽è^taŽúÅ*}j¥Bl-¡n½†ß[¦R-Óh5–yZ<Ÿ›Î¹ËR±v˜X¹ÛË££;îP¨Ð G@õ#8¾Ã:w”F ‰Ø{û<ÝßùZõ*í´»ÝnP­–‰¢0í>zxÓ…Ù"mÛÆy~ú©éŠçâ]¸Ìµ/ü< Ï>ûû *ub5@Å –4…ïDéÔä9}lpIÍœ‚»N‹©†kW`9ˆ(¤³±Á¥Oý*ïü»O`÷;,Ž;^ÜÜOÇ(m¬ó.-uø™·ñì¡ Ü8מ ½Î[/ o¼Iýé'iÖ+45ªsÍÆ§^E.7p}½ËßÙÅ·w pn¾‰ïZ‹E*å"ÅLÇÈ9xÌJi,4—V:¼¶˜R2(Zj!¥é ƒŠf9ü ¿ÃŒü¡-|â.¶¯ÿEïïÎÐèt“JY¦õN+’­[°zgw‘BèR]˜Ë%oçfÛÌÍM3;Ó6ƒJµ ÅÂp@ã¡ðìÇe^ôpÀk|RuDäeïZ °^ˆRŠ­¯}ŠÁùOÑH­çgÛÌÍš®ŽJÅtu<Šsl²[…3R ϦY]×Á÷}¢«×yïo°úÎ7Ù>÷,…ïþ(QsÊô¾'’8N ©™ÅœÎ}@G«Çb=ìÉè,\¤ÅÎÒW¿òoyï‹ÿŽþòujÍ&Í©ÚÍ*³3-æf§™›bzºI£Q£TŠÌP]Z’Ræ€}@_]iâ$æ§_ØàöžÊ‹¨²v)Iú]ºç?C£\Lå ê4êUÊåBJýŒO½&‰)Ð~êÍ ®nÆ”AèÐ,?-]I+•1h@wµs¹u·¿Nè;”gZÔêUZu3¦=3Óbf¦ÅT»A£aF¶3ÉÛ;™œnf9ª!®‡ÝwÙeƒ96yÂG8l÷Ë[æ,„í m Õï³wéeº¯|{ù2SÓm¦gÛœY˜áÌ™¬ø—NPzÎý)PžÀ¹Aà›âhÖQø‘o /n\»ÁK_aõͯ³6óáSßCqá)üJÕ<õZ¡’TI26¡:ºðš |‰°Ò¤ÃˆÿÓÙÚdõõ׸õÒWYzåyk·)W«Ì?ó4Z…V«ÆÌt‹™™6ÓÓMZÍLs?Lu¬»Š½)­±…â¥[{Ýš]ûñãTØBsyy—×níᥔLSo€ù`^˜YZö©?§ß¸øÆcê1áŠü™?üCœ™[8Ñw¶ï Ä"àµÙòéÁƒõ«©obš雌´dƒcU*£*ˆV T¡úXª-\Û ]üFÃc#*å"µj™z£J³Q£Ù¬R¯U©VNF˜Vî÷;+=Ôl] -ø¾3v67Ø\[cie­­º½qœú²ŽÕ‡ÒÈ–eáú>ew@Ð*à¥7ò?‹Hûý~ôŒÍY±Äú­›,Þ¸ÁúÍkì-ßÀô)ø¥'ÏR­” õ5ÝdnvŠÙÙ6ÓSͼ{Øôh› à3ý}ðF·$ŒBÊ¥"µj‰F£ÊÔÌ4«+k¬,/³¶xí‹/³šh”"¼«PBúÒv‘žâ^—¤ß#ÞÛ¡·±Î`o›Áö z¸ŽMT(0õäÅb‘R1¤R6])Í´ðÜhÔ ¨—K£Œkv޽³Ì,ïâ$Á'–çõ†[ÓÊøÛ¿DÉöT‹é¶©Ô땼k,ënÉÞÛš[~õõM“µ§&ì8!ö•—ð;k[g©VKT+¥‘‚ì8§SýzW+¾paå˜È³)è.•d ‘ê0 !Aä¦í§íú7þâGøs—Ïð«7ø×_½ÄµÎC»7Ïûüç?z–ýž073{*¿Ã>fÕï.ôŒi‹ÒƒÖÆE‚áï<Ë5S”yaäÊgþ˜ŽwgNG¶SÀv¼t$Ùðr>ÅBr¥@¥R¢Z)S«–©VJ”3ÉÛ0È †7Ê£áØ¥%}²BKlñú›Ë\Ü^áæ`‰Ímº½>*Qc ”wz¤âXÅBH{ºEµvƨöyF³æAÛ ³é?!L–ûœ ˜KàÍí¾¿G¡âÒ+œÁ¶mÂЧ\,P«•iµê´[f—Ôhš‚\!mÕ$yÔ1œb•Hiî'5úˆ¢Àdïµ ëÍMÖÖê¬o¶ÙØÜaks›íím¶·¶ÙÛÙ£³v~¿O ’Äx ¤T¢”’жqC¯>›ZÕ™®BP*¨”KT«åôÃR©˜ ¹¹¡ô½î,5~“…„ºk=ð„i¿4UÝY¨°õܯáܾHëé÷Á¼™­V݈΅µjâ”kÿäkë\Yϲvmê]½Ü÷^&ˆ T«æüUª¥´ˆîÐg2Z2°¼ÕåÓ¯oàZ‚ÉŒ^Å¥o(™TNcèuúlx¹ýÞýÞÿíŸý0WolóîµÞ~w•ç^[ä…[k'øÛ4¿^ð‡>²À÷<3Ë3OžÁqN·œ{³Ž;8}p1ÛÃb­J¥V§Z)R(D¹î‰Ç ¹ÈPÖáàØ™•)†…A@¡R,šÌ½T,ä_“¡¹ï¨¢Ü£,œŠÔ6̲]ܨH­Õbn Šev÷:ôûƒ}’­ÃP e[„A@¥ZbzÚÈGaº–F*ùþ?žù]RNZza¹BkvíøÔ·wPI‚ª/–KEªÕµj™Z*5\*r‰WDZ)¾?z€ÏLÛf,ƒ?Õß/Ójî²µ½ÃÖö.ÛÛ;lïì±»Û¡ÓéÒíõé÷zô Iç…Srü¶”ØŽ‘|ö}À÷£b! X,¤=àQªµæâZ^êYç–¼ç«7Pš’£ùã­m>þ^žía+c9%\Λϓœÿ4Ó33Ì´›Ìδ™šjÒ¨Wr…ÉQ@VZcKÍåÛ»üâ77ðDzvûêËx5Ê ó4êS-S=™ƒEt3\¥øâÅu..w ]‰¥̪%´´Œ&1$Œ‚5ʇKéÙ6®Æ Å%åPï'/²FåJ‘z§ÇÞžô½½.N‡N·G¯7 ßë3ˆcâ86ÃQyæ.ÒDÄÆu³hø.~à&! R0÷}/•f¶ólÙ²lWi AŒÅ³åÿ¡s¯¬‡lФ³KïWI^ÿ"­F™Ù¹iææÚy÷˜Ž2Ò£Y»JûÚÿÉ×–YÜŽ‡\»ÐÛýú2adÄF½–z!Dx~6× Ç¸v­5»½Ÿ|e!41ó¬%{Hϧ… Qnøµ(?ê(¥b ¨ñ­'¦-£´&]êÕ2³³mÎcªÝ \.à{Þ¡>ŠG|îR¶,+5Ÿ02 ®k¤t]×1Žõ¶5²¥•¾Åñ¨ùß0 ŒF‡ãP.évºF 0ÛâÁ›Xé.Æó\¢(¤X )¢tÑ<™ GJ™ª+†€ <êõqç½ãpeÚÞ•ð8ìî5“B -™¶À÷D1ƒA‘~`h˜þ€Á fФÀ>¤eTVˆN‹ÛYGŽë8؎ߣžëà¸Nê j§÷´;Žª+X–ùa‘§KK¨[¯rù½E–n¯ÐßÝÁŸiÓhÖX˜Ÿbaa†é™Ökß—µ›a(ÍçÞZç3ooSÈè˜ÔÁ~÷<^g•Ò|šµ7*”Ë%“Äåö”£¸®Ô|íÊ&/]ß%°-4Šyµ„Fç;§¬éÁI[?§ß·j¿ zÇvHT}ŠNŸJmÊ e¤}Ï•J ßsFÄÝ.˜/$3-“ÝX–¹yì´`; *¢Xz/E½Ì5HJﺋ‘Š ØïP†B"$X–ƒ…ë:wT¼°Ë†,Ë ”ĉÉP…H‡{l;•ò•ùµx\Ïû±¯ ÓÏäØ6®rQ*!I‰JˆcE’$$) 'I‚N}Rǯñð~ÍÎm›E9[œs­}!Æ ?ìÞùTn¥Qk·™í%Û¥V-Ó$øžC­VfvvŠ…9ó|–JÅT l¼¯]hÍúN—üÕå\M2“tÛËx—¿FX,Q¯—i5Ÿ@¹\È ©åqóë¯,Ó(„çÐf“ŠÚÛÍkQÖoY´Ëê;ÜÌà5ZXxÉÅPP.—Ó¶Ä*F5wïø’Œ›O~?pKa:p²"àã@½÷A̶á®ã$~Úí ÎÚÇj ÕH1'¬Ù"”³\Ç5­¨súyû¥àD²ÎÇåºäT˜ÐH t*ÿ«G’T:,¤µJ§Qõˆb³»Íb¼OÁSÂiô³Åó\Šå"Ó©Go1 ØÚÚ!Ž\Ï¡\*ÒjÖòöà(oYO¬4®Ôüüù^¿ÝQ1…Tçí/á©•Z‹V£F³YO½±6ãQzÇš7omóÜ¥-BW’h8ÃBŸaD±”êÑDAê.eO€ýTÀ}lnIß%ÓÎ^€N(Ä·)J”K…´ÈfF‘ éPý $ˆ±IÕýÆÔß `~€>]â8ŒhtéãŸs¶9ÑVÈì¥8ÄÉKœøï|¬^:-þõþGax^ÌTë°Ý÷tÏ—¡Ô\Š‘t3´_‰N§K’¨\¹X2ÆŒÃÍiL!0|ýò&?÷Ò‘+ÇŠ¨ÖÍ7ñn½A¡Ñ Ù(§s$U#66–µÏS¢ŽVüòKˬíÅD¾CEwhÆk(éPL ôF² ‘«˜ó)fî#z&wè˜Ak´åâtnPò”**ãæRˆBÓ·ëØ÷©•>ª…ûíãƒ0Ç?'¦†'N÷OÃÃö[ôzíÿ:ËØÅav·c'íáééÚO†öóRÚo0äf1Žcê%žç¦-Ã6ÚDÞ¥û·nÑhüÌB/+¢¾õE|ߣ^¯Ðn5iµêT«å´öãŒHdòÃGÂKïlðÉWW‰\I¬gõVÒÃL—[¹dº‡|ß}h"sßÁà®è˜IsùQ;;ËAô6(nPn·¨×L{\©T$Ó¶¨ûî ßÀq¯çd’Õ<âkwÄÂ'ñýdÚ;ÍߎmÚ1•RhŒ¢¥iN0#ýÖe”ÑNRÇüänqa¹GÙ“Ä#ETçÂs¸»K”gæòÉÖf£F©\òöÖø”VŠDÅü³¯Þb·—à»5±Ë\²H"lÊéŒA¹lÀÝMͰ'”Ìi{Þ^}Ä QžÄw‰v/P­4›uZÍ:F*BäßYz“˜ÄÉgðRZhKbk+Ÿ`­ìßA&‰Â±4ÿê¿ùÖ&%ß"V ´B;>öíK¸ï|¨\¥Ù¨05¢üYH¹vÛ¶Æh«X)>ÿê*¿ýö&‘kÑ×ð ·°â–oúþ+•’™…ñÜÅØIœ6-sÔ÷¥ ýmüÝ ÔJ6í©63Ó-¦§[4ë5Š¥B® =¹X“˜ÄÃÝŽfÐwÚ-*e†ºž¿¼Éÿóååqç&i#;›¸¯|ß±©7LÆ>3Ý̹vß÷°¬ƒ2Bkvv{ü¿_¾…Ò¦¯}FnÑ,1å(0Bc¥ThlŸôÁ$N&Ž®rêÑ/2…@-,Äî-­×h-¦f¦™Ÿ›bn~Š™iÓ?[Œ¢TªTNVãILâ‚ýQÏ_ü΀Ÿúò»}…•šsd‹î+ÿ·»N¥QgªUcf¦M;×™ÒΪqùá8ÑØ~ã›Ëœo‡Ð•4ïç&*ŽñþµE\ zÊâ ¹Bu°F_ÚT‹!µj™J¹d¦¬GÞg'îÙVN"Ó‚cg]1v² ƒmd¼…/ºŠ•Ú ­f™™ ÓœY˜ev¶M½^¥Xq=g8°1©|Ob_Vô %'¡Z¼¹: `Câ†xË—/¿R¥Õ¨233ÅìˆÐXàûêi¹ò#Šú»7¹¼Ü¥à[ó>uƒAœDÆÐ£Z-§ÒA.ì7÷SÊÜ¥”tz1Ï<Õæïþ?ÌÒâmÞ»ú·o³µ¹Apœ€0jR.—hÔ+´Û fgZC¢F•R)J]Ï­|hc“˜ÄãƒDá[ðŸ}@ò3çlÅ’âúe¢‹¿…Œ|ê óŒÏÏMIçZ¦5®^ièÓ'ÿ…×VøÅ–(x’ž²ønk‘°»El»”KµjÅc‹Y!u’µŸ¸gzã hÖŠ|ôC ¼í'HÕÁw;µ2ð=—b!LÅ‚ª4[5ZM3ÈP©–(¦2¿þl“x¼CçóÐ5OWÿõÙM^½xÕµ«ì\Š©V…ùfgZ4µ1íQ:Fiã7»´¾ÇO~îSDmÊ]žŠ¯ÓWP,øT*fz½T*úÞ˜ÇÂ$N)s©¬¬ã:x~@£QgkJ¥ý~!ŒT¡P0ÅJ‰J¥D95Ä5jnnªu"'gu“x¬)3A+0Ê«Âv)Fóµ;™¢Úë…ívƒù¹)Z­†¡Q|ÿ°g}òŽHø‡_x‹KJ¾E¢5ß+®B¿‹ãúTJêõŠ1õ(„xž7áÚ¸ƒmÙGµZ4Qè³Ûé’ÄI®]†QEa¤zÔn®G2ÉØ'1‰o°,‰ëÚ õz…$I I’…õz•f£jü¢`¬.‹8Q8üæËK|âåʾ¤£$¶oQî®ÐÁ¢^ ©Õ+ÔkŠ¥‚Qœd탖!SÕB]2âQÅb!_ÎŒ\7_v~·ó-(÷:‰ILb8Õê:.…(B54®ëP¯WÑZãûnneiô¡ÜÏy’(, ×–wøŸ¿†# §-f¬žˆ¯Ñ@Tð©UË4ÕÔÏ8LÍÓ'‰àCËÜj£ƒFµ0 #ÐQb”‡Hí~+˽Nbp¹¨˜”ß÷‰ã؃mvëÆÕì ¯«RÊ´Nª˜ŸüìU®¯÷(xB'|/WôºØžG¥bñüM~úK7)y’=eqÖÚ`ap½Ãã×*4›u#&X¶IOvûÜ'à=‰I|çü~ ûÃð “plÁ×.¬ò·>sß ´¤auø°~‡n·çûT+%#$X¯R©s™ °?BpŸÄ$&ñ ô‡ÅŠwnmñ¿üú%úmK|~Ÿxµ»ƒ°?p«Y£Ù¬¦®Oá¡6“8ݘ4¥Ob“¸kh­±$¬owø+¿v‰›}<ÛÔä~Ÿ}§³Ê@V˜HËíIDATX”ŠF•V«N½^¥TŒrý˜IëãÜ'1‰Iä.sfpÝ>DO­VaªÝ YðE‡gObÄ$“Ì•°¶Ó£k6’÷ûÛ|ˆëìô|ߥZ+35ÕdjªA­VI‡•Ü °?˜T'1‰IÜ1ÐéÆüø‹ÜZ)°±±Å3,³¹¦ð}Ÿr¥ÀT»ÉT»A½Q¥T*æc“"êÜ'1‰I<¦Y»iLp%üùïöyïÚ:ïÞ° žëШW™žnÒlÖ¨”K„©:ìÔ'à>‰ILâqÍÚG͵¥¤‡ƒðB3˜T*†~:¬T£Z-†þˆÇ„õ€û$&1‰Çàeª [.Eh•M”JJå"Q*ý=1à˜€û$&1‰opwl ßsQÅÛ¶è— xžCø¾ŸwÆLâ1¹nú°™ãILb“H#3äH’„Á fǨ$aÌ>ÇN=UådPiÄ$¾•B)…Ö µB)êЕXè2çæ'1÷ILbßBÙûðo#;@j×gþ=öÇ-þÝÏqLJ£ÓIEND®B`‚Pyro5-5.15/docs/source/_static/pyro.png000066400000000000000000000567711451404116400200570ustar00rootroot00000000000000‰PNG  IHDRÈh´D¿pzTXtRaw profile type exifxÚµšir%9²ÿcZ“ËÁh¦hùúNÕÕ]]Ofý$eV%É˸€gpÜpÿ×ÿ|áð§z,¡Tïm´ùSFiòM?~¾Z,߿ߟýÇïì__í_$^Ê|Í??¶û{ýäõúç¼ü¾¾þõõàû÷>ý÷F¿¿øã†YON|s~ù{£œ~^·ßŸÃH?ßÌöOÛùýÿ®Ø~ßôýùëÏÅ Æ©Ü/§n¶þý¹(³Š<òäkûþíI¯d¾ÏÙùײÿ{üÂ?B÷7ìõïã÷ïùÏpüÜèmµ¿Äé÷u«¿/Jÿ¼"K¿—¤?ñýøì5ý[üÞ;ý½û³»YZ \íwSlñûŽ áÌßÛÿ+ßû÷wð·Ç7Y;lu…¸øaX"ÖÏŠ›öì~_·m–XÒMÎ×”vÊßk={iI)úk/y ?'wrµÉ\æåôµØ÷Ü¡çñ°Î“qe2nf¼ã_þ†¿¾ðßýû/7zOen¦`öúÅŠu%•ËPæô/W‘{¿1­_|-ü|‰ý£Äf2X¿0w68ãú¹Åªögmå/Ï9ÖÀ¥%þô‹ùù½!âÙ•ÅX&±Y®Ö,zJnF;ù™¬<å’°j:¬2•œÉ¡x6ïqû®M5ý¼ ¼ˆJÓ8©¡HV)µ4ú­SB3Ô\K­µU¯½Ž:[n¥ÕÖš7áÔôìÅ«7wï>|öÜK¯½uï½>G«a´á£1æä¡³Lî5¹~òÂJ+¯²êjËW_cÍMùì²ënÛwßcÏ“N>@@8íøégœyíRJ·ÜzÛõÛï¸óQk/¿òêkÏ_ãÍdí7«ÿšµ¿fîÿœ5ûÍZú¥ëüϬñ²û·0ÁIUÎÈX*FÆ]  “r»•’”9å,Ž”CÎ5±ÊªäSÆÈ`¹–ê³äîÏÌý—y D÷?Í[ú»Ì¥îÿEæ‚R÷O™û÷¼ýMÖÎüà6 RS2Ó~\4Sç?èä¿÷5üo›vLmõÝ}{aã·r9¢õàœSÚ{âÔ“ŒmåõÒ¢vÁ¯f4ïºñ€ 7Íš 7O™'"?:+„EÛi“z„G,Ó–AÎN'”$˜àD~MBC›µ«Ê:§ÏÛ…V›ŸD3¶ÚÉuÝ4hGqÓרÇy` µz 4Ùä. Œ¥5(\sš©;·ʳÕ*ðá‹™¦œ&¢ô¶ò@b:z\¥g{[²ôÔ ä!eýù$(S`¶Òë®%>/™; {Z%´Èp¸•õòÒÅbZ%!åU^ó9Dªy(´=LqÑò–Ñf€ËWÏdví0´d{[ßÜŸh'ñŽ«K_¢GçîtW^t$¨B"ôIëZNY¡ºÁìÙꄇcƒ"qb¨«—›B[P¿§ÐóužÚ³Ó+N‘Ä€X_4My‚aìMª¨sè5(Ž_nJ/Jà än¥‘Ç[tâ1&Wu´ìØåIÃý5£Š  ²Œ\–áÀGÌZ¦0Ä) °0@¤´ í%‘:/*ʼ£@‰àfS½‡/þ …["êẠmƽAÈXÎ:kØ ¼‘1@ 4 ,8<~5‘ïÛÇPô6TŸ£iW“ôÁ׋iòÚxÑ! ”AU—ƒS±i~§ s‚§XÈbÐ'>cNN }”‚µ^9@må‰+Ü "ðŒ<j ŠÄD€Ló‚l@÷Cƒp5»/9;÷àD¥ ªa"µ-ê¿‚‰…x ““DzÊqMp€>æ Å‘ (ŽAÙÔ¿,æ‡þ»ð[B¿eì€ñ9öwøyÆpˆÃï²Ô†Pì€W<#´ú eÔΑúÄ€ê¯írpT°fbYù6 W# ZJúÁ‚31eì—$o ±‚FËl¡ˆŽ )T0ŠŽÒ¾ïRh—‚®.xç‹`šÔO…nèÇŽµjTó×’§VZ'›_g*SéðÆáQ´CO-$šÓ;Ä>C”„%}Òœw÷°â­Ü){ø@í‹hñ‰à‰2OMò©qîØ fƒÅf^ju‚•;Ÿ €!%}³#)“8ðRXÌš\O‹„ˆª¥¥ïÚuÔ1l·ŽœE#í÷ʦê½nÏ8Îûð¸­pØø£Gq +MŠé‚·€Ç{…ÈCx¡‚ÛÍÍöN—B¿9Z„zãõ7â¦Z¦T  ¦ÝâEI] À#¯Å.©žK{õ¾_Ø Þ-¨Bªý ±Òü ÑÞJb¯¸£ :XYÀúÄ›Qe9ÂËi!·,Hq€H–{q”-2?pæ ´!² G$Ã&8ˆÂ®pF2vô?M7ÄyQ»ŽÞ±K âáeÄ:[CÝ ú ù`HÔAG^<3 "ḘQ^%ÊOl꨷ÝR$^¤ŒH­H(É)< æÍøÀäàÙ Fz”rñáIIÖ…í7J+#"ÛŽ„„ ‰o2ä+é ¯oŒ‘€¢¶«Y­L,ÑSi ðî ’m[³Ði•‰€¦€‘I‚O‘2ýÑBH*žüzº×Lg½EÄ„áJn6S¡"&~Íñ{ ÍÓ#Í*pœÅCrG‰’`v\åÁo¶-XYï&FªWò' åzϹåäÒä9‚3ö¨anK¼Öðvá<dzW„¨-ZuîP„býF¬R•:Æá¢¥ö:ä <¹C $„Þ“l!D›Þ8ÃSs4Xè|i‘.ìâ:„½OønÝ€ÔM3ÍY Aÿò‰&Ò$–éê ¶?hüˆÊUÇš†8PÊÞ`ÍÉOïJƒsq‡O(YEÌKú!yX3*ù$ŠCÓ¬4 ŠõÑ ©êZ4ÏÓh8†RØL…@mAr]ªpüPÖ‘ýLH–õ¤ëÙ+)5P¯´ÊØ’¦EŠ=X’eÕ@¿ ˆbIâ–¬G ÔáFÛâçlY à3eqÐwïÐe^¥ èÈäÀç#àáe‚áDÀÜháBܯ¦`#0Ds^´•Zfj5°MO—ÌÔ¿°a ÈvY9‘Ô3¢Õ𥨠Z7®O£S‰lDö¬*ß`=Œ=¬ŽªÈõKróAü,Äÿð^a_óWÉW¸•LñÓçû©—š‡ëW°ç³D¨ä…©ë5eŒ²yô\]•¢¤ó[ja¬¤¥Álçq¬‹4Dð€`>Ú<`Ù4ùs ŸÐ_“@]qï#n8þ¬q« h6±Ñ?àôìqaÏAC±ªA$P ‹æP Q‚<³ðÞAua`19ŽR^ð(©n“ïÁ]W°®Ïï€Ø 4—PµíkSX³æ[X.,%ÄÇOs¨h…OG¬¸)Dê\Igv`#šYƒúϵQe©Ü'ácðvÄ#¸f T8§6Äm[·£µoAOÀls63»bƒEœN(Š5d¸4®#†ñL€øÜ3È3ê3Q)o\4ašÆt,‰øz80”×)‘ØÑ¤H Óðª^ÚjoZˆ˜  ~‰8‰Kâ-]¯êÁÅ{ØÜý5â›e55‘'Rt«M¾ ™ >JWR|”é²ÚåE0›¬½NjìŽU›àœ:ÍúM°;½Æ†j‡c³Ó4òÊÈI¶GéBô¤Á!jT!.ñÐcoâ"JAðz ÜU-¢ è_©z`®áï/ÂçŠéAB°^ŽE§õ0ËÍÈÐo,ã¾ëÍ9tAY¼ý4¨ÆqQd‘ZÇÕ"|HP“rfã¥@|âŠ]¯¥š!§N…Â|\V–ÄŒ(æv\EkN|ÉèÊ/£þz%V¼¤9!5ůqµ\Ê;éõð~‚hm*BÌñˬto¨ÌpÒ`ƒ†®EÇ ¨ª¨3\÷‘z™ ®Ï‡cëõ‚`oÒgë½6x?$Bìm¸0ˆµÓ¡äˆoØ÷·æ’(&˜:„ƒ§„¤UŽR±ƒ äÂbólªüʾ㕨þ»ô÷;n®gìÔIÕD‹…d Ç•²ªðÄz¢fà¿sỞi×öy.MËÀ~ Ý(2øt~@ëÜm„i2óuad¯ÎÞìÁÇ8ò§Ví³Î,)Y)@1ÿPܮإíÿkÁ£>Ô„"cÜ— ²d‚Ò“˜C[ÐpÝeôÊ[:/dçMÜ~é`†E<|úè çÅfQK¢} VŸxyçç´õAw5¡šg”Ñ“œc ° ”^²¼vµÉ@PÈ PtÔûưD‡¦ êFeèts ©’”ÏcšYU@vô*þ5–&Ã4mö¸Á*„/îKuOÛ#¥‹~ùl®¨æìŒÿÛÖd`v õÙa |kòŒ j:½(Ô,:§q“ß 9B¦ðªòß?GAã5šæNZä˜á=sº>c!ÍÏbàM¼ÙµsyXàÊÍ@^ÅóÑÉšŽT ¹ß`üèãÝÄ™)Bl+²Í"¾€»V%ÊEï—ïêß+§„md9Üÿj( ΀›§Ü*öi`+t¾£%“ Q©Ú[–9©¡„Zùžoà ³ü^~fšî碮O𠓆걇W©0€œbXÏ·wùÀ²ÌýÁeÓÓ‰]^:ÜL{ÇFc’©’’+-_Q¯4ÖÐiR•d+Šªl޲\ù?iÒÁ$T`MX£²Û+•hó}Ö䜈iT2‹mömKž]®iâ]¿qÖP;è“7:ñƒ °å\RW‘ëÜ- \¿Ôí3=oyéç$ŸîÇbØ7j8ºLà Ï84ô¯Ò¨›LB\§ =ÕŸ¨ö©éEÊíÀ°ðŸ}nA¶"ë¿éJÆ¢NóÔ&KQv5Êéh>Ó@ý ¨u¨¿é£Y E}âÁ¨Æ,CP)®_[ÑüHŸJÙí;ÌYU¯ò@TB*´=\0ÉÜG'œ˜¯*ä"W‹¸ƒ.A÷¸7q“âDP7”8“PúàU¥£‘z°ß´Â<Ó‰ŠH,ЦIýÌ šüqë,‚‹ô¡16¡†uÅÁSJUa×qŸ’¦YÕ¢æ4„±Blw ÷®²ø–úµ’Ÿ|ôù**SÞÀŒ¶µ/WIŸô{Åd6ÐúÙP©Ó´ùÔCi`Nu„r.ñKÑŒÿþ¦jG¶4[Ññ2ªÞv„–tøÙ 9|_AT'$¤øâ ôa}8ájºÕÎwÉÀÞn§PdáÚÔAn T³ÎUS‹ ‚ ˜m]à"4<&?øBA!¦öã¥_ƒ:‚bÚß GW}õZÿ£×„z@UUlÚ&v‚B«m:AÂ?õ¯*(n,äcgüo}@@Ø­J>¦zTe÷ò ä|•g«úÄ mŽÞ¦©‰£>tƒŸ•s©h•±÷¶ª’¬ª›!dKþ”èƒqP•ÇÖ1ÊÑP[—@à:Uo}Þn¦†ÒläI–G\KV°Þù`Ô0Cp?‚_™4›¦% )HE.Œ º!7¼ˆäp\½ÌõP-àÎ…(M l4å\–õ Öá-BrnÖø[ÇÆÛWD˜.²:Øâ5;ô"Og $  PC÷'—ÎXžŽ:x¦j Œ’èõ'1Äßoþo¿þÿ¿e€@üo?ýúÝev?…iCCPICC profilexœ}‘=HÃ@Å_[µE+vé¡v² *â¨U(B…P+´ê`ré‡Ð¤!Iqq\ ~,V\œuupÁW'E)ñI¡EŒÇýxwïq÷ð7*L5»ÆU³ŒL*)äò+Bð=¡QÄ%fê³¢˜†çøº‡¯w žå}îÏѯLøâ¦ñ:ñÔ¦¥sÞ'ް²¤ŸtAâG®Ë.¿q.9ìç™#›™#Ž ¥–;˜• •x’8¦¨åûs.+œ·8«•kÝ“¿0\Ж—¸N3аdÔ° ,$hÕH1‘¡ý¤‡Øñ‹ä’ɵFŽyT¡Brüàð»[³81î&…“@÷‹mŒÁ] Y·íïcÛnžgàJkû« `ú“ôz[‹ÛÀÅu[“÷€Ë`èI— É‘4ýÅ"ð~Fß”oÞU··Ö>N€,u•¾x‰²×<Þêìíß3­þ~JËr—¥Ó¨TbKGDWF)EÕÁº pHYs Ö ÖoyœtIMEã %˜æ7 IDATxÚì½WdÙyç÷;çÚôYY™UYÞuµ™é Ü’ârAJ»K)âJ¡•´zØ…ô¢ÐƒÌ‹"(­¤­ìnPA‰vi@pA€ €@`àf03˜éééiïªËûJŸ×œ£‡{Ó•é®6à¨>™•Uy͹çóÿï„ÖZób¼?ÁCkRFBˆîϳùbz_ŒŸtáðÅ?{íòÃU|Ï£íù(¥xºß|1Å/ÆOòPZ³¶×à·°J¨õZ¿{.G>Ÿ%™p1Mã™,É òbüd H¨ràÏ¥± Éï¾µÃ7?XeooŸF£E*žÅ¼ã'~øAÈ?X0øÄpÈ~[óko×yóê ››Û4[-Â0xjwë…‹õbüD!¦it-~~¤É½=“Ðå÷¯¶HØ›$’ ¤$ò©\­äÅø‰R \ǦPÈ33Qäß,íS4[ܯI~ë½KWÙÝ;À‹÷òbüZG¹ÖQ:·_@,Ë$›M3V.q~²ÀÏf÷hnW ¾~}ŸÍÍA>q<òÂÅz1>â‚ Åí­&~¨˜²1 ‰aH¤ŒÜ&)%©dJÃxž”°vuwC|eI0äl’N%RIGß=­»õB@^Œ¼€¼¿Rã¿üóûø¡æ?þdŽ¿»˜!•JbÛV7k’dÒ¥T,àû!ÿÚþ-Ö4xÐNñåû>/—7q\×±ÂÆ0N' ƯüʯüÊ‹Çðb|T…#B>y‹×ïÖPJóÎrѪ2žÔ†išHY)%¦i`ÙЬ¿Ë‡ûÛ¾E«Õb*N§°,ëÔVäE òb|¤G†\–X†@ih)øì Ÿ·n®³½³K»íŵQ<’I§(—Kœ,ñél[*~´-ù`i—ÝÝš­VM9E<òB@^Œ¶€(Ílþ­Ñ¦Ð-eð›jÞº±ÆÎÎ^¼à#!‘Râ8…BžÉ©qþÎl’y·E%4ùÊX]Yco¿‚çyh­^È‹ñ“=¤HÓäÕ\›E¹‰Ðlù&_¸²²²ÎÁA ß»VDJIÂu(rLO”øÛ£ͽ†Í{+ìlíÒlµ ÃÇ È‹äÅøH! aà“¨®ñpß§n¤AÀ¶o¡šÆÜ€t:‰ešÝØBJi %I|–wjlx;M˜vriׯ2ÍGÆ"/,È‹ña$.££EÎÌOóSÉRªŽˆï÷v¼{o—ÝÝ}Z­v×ÕB`™&¹lšâH‰Ÿƒœ²Ö¶xgµÅîÎ.õz³¿¼° /ÆGvh š¨Þ¡´¦=w4{';åØ6¦òЛÜUC  ­ VkšsN•d2A2á"¥1P#‘†ÄZT+UnÖöÛš É©T’t*ÙÍ„½ã#(š T|ï~•Ë« L’4Z „è5?I)±,¤Äñk¬ìÖÙiªÊ"éUKA&›Æ¶¬®€tŽ„!–Wã­ ¨%Q£œ6É峨¶…a½) ˆž/ùœÿ¼ù\¯¯{çÏ÷˜§û°ßOxy¦s>ïûQJsÐôù_¿³Å¯¾±Ãwî7øúíÚk1 AHL£g ¢º‡iYÈýu–šMa#­–`DíQʧqË2¬LnUYmZ4Å´Ó¤0”%á:XÖñ±Ès(¯¬û¦RÑ‘8èc>½ðE¯sÞΡĿB©Q ë»ýîGOq]:ž·îí Ýù4>°Tï³Ïuß3ˆ1L:º¸“.EǬ| Zuß+¹C<ãsŒ.Sóƃ*¿ñîA4iÚ¡æêvÀ”Q!kiÇ€•RbH‚6ý=î9š–6ñ[ ÎfÉtŠ„ëvöÎå…ZÓ®ðᮦeÉáÈÍr]çØ6]ó™´Y<éa²Wi²_m²µ[cs·F¥ááù¦aàÚ&¥B’b.I©"tHØ6²/ãpšIÖÚ~Àwß_¦R÷H% fFÒ2 ²©ÎD§>ÞóôŸƒP±ßðYßo²qÐæ 0Sp¸4‘Á0 È5Ùé,¯Öšv ØkúT›UŸýf@Ó×x"W'ÅÓg&D¿žïi þi›|ÊÆ²¬nåùðùüPQóBêmÅn3¤ê)Ú"Ô`J°ä]AÚ’ä\‰cX¦ìV±Ÿ¬\£µbe¿E úäMÚ ~ûºæ?j®ñii0\È“LºH)±m‹\.ÃÔô8?Ûhrõz…52|ÐâÛ¤³2é†!1ÍèºÇ&›Ípa<˹µÞÞ±xo×âÓ›Û åsd2©î=<³€(iìfËçËøÁ{÷ùáÕe–×+ìÕZxAH¨4JÓ ’Û$éÚLŽfY˜âç?1ÃÇÏ29š!£E$]þ×ZÑhyüÆ—?àÍ›X¦d8ë2YLóoÿô4çâÅ| Ó²°bŒÎSP”Ö­Yßoò'o­ñÖýîï´høŠ@Á¿{1ÃT²Œ›J‘J&y=kÖìÖ=¾w¯Ê{« ®m¶Øi„4TÇ^tG¤ˆ%#Ö”bðw!J Î!³#r¹ ¶m#Dl)´¢Ö¹²éqs×ãA5¤ÒÖ´•&ìÅËÈè˜B`KMÑŒ%‡%SƒlÂAšfõ¸yzÉCÊN@„FKx|Â5ßæ/–šLåÖ±L˲°mÑ+嘜ão­Þç/’ø¥âÉd˲ÖÊc¤“›^ZÝãŸþ¿ßäøµ¯³¼¾Bö|êÒ'}ó°€t¿'M/äÍ뛼qu•é’K&Ù‘úžÆìX¯ýJ¯¾õõýVoô SÓS¼{¿ÂBr ×uº>òóJK¢ŸýÁCþ¯o> PZ =oì¯0n5)Q,á8Ö€€(¥ùáýþû¯-óÕžÔºâʦcú¼«TºÏ@b,_#»s‡ÑòAz˜?½¯øîZ,¦Œ¶!p,¿·c± Š˜’H@:¯BÐP’«û‚Õƒ6EÑÄ”`˜&RpÌü „ШP¡ÂpýÃ4¾°ºBÐÐ&~½Ê\Ò'‘J ßRH4à ®lúT•…‚Z(8#¶Éå³d3©n£3­ÒÔ+>Ø i„’ ³Jy8K&ì®—S ˆÖ*ÅÒÊ.ÿé¯ü ñ­Aˆˆž«kâû$v/dŸ%9I@„%zݪ´yëú&®¡˜M#„ì£m¨Ø‚|퇬ï·ãó0`µ„´ÍÖAƒ…!A6›Â¶íg¦éÍý&ÿÕŸ\§Öï¿sßñ«UYc6019ÆèÈ0®kGZOEAý7oîñß~e™¥ýØ=;ÖúU@§–xY* |÷ëL¤Vy–o·Ël´%®)±Mÿ؆ì ‰eFV¤#ƒ†"ÆJÅ·.âØhÓ7yXñÉø52v”ž5 9àôé€è³VêÞâ`:Yö‹Ùp¡laO"DœÇSŠ­­=îµ@ÓÆfÄÛ`$ë2\Èqµ¥ð5.¯{ìy’¼n0?l“ÉfH%PxùHË¡BÞx÷>ÿø¿þÞ¹ú°«ÉgÉ»™¡O•“?I¥lì·øß>•?üÚV×7©Õ›ø~+E*õpš1Žw¤|¸áñú•e66¶©×„axŠ¢Áã…Ã÷þèe¶k^O¸c…1p]B Tdõú®Jkò;o¬ñO¾ºLµ­BDï Dÿ±•ÌášÒáè[X¦a<¼†Ü¼Csô,ï¹giaàZ2 ¿—$,kIVô¾÷ÚyÿnöýÍ–¸ñ÷KâX×”¸¦`-tùÜšÃû÷7ÙÞÙ¥Ùlwaº¯Rî8Cù,3³SülF‚ÝC‚®iI~°m±¼¼A­Þ ‚n…ÝurCy>VvÈ™Aô=ip³‘`}}›j­>°~ C’plÊ… ç†$m-yزØßÛ§Z«ƒ (òQ1ǽåþóÿñó\½½Þ—>.H‰ã«Tâ¸ïžp¨@i~ãkwùü·®³³ÝÁ̄ݚÀ@N žX-6/\~Pe}uƒJ yŠžý#s²[kñƒ;{X†|„èO¶öjJE @_¾²Íg¾¿AÃWƒ.ôÅqš»þû´&þ{œ ‘^“ÚwþŒôøõÅŸAÛNcе½Ù]Üî€t„@Äï£ÿKtÿ?úŽc \Sô¬’!hI‡¯l'¸º´ÃÞþív»KœÐ¿`]סTbvv’O8;˜:èS)‰âN˜ãÞÚ.{{Z-/ΤFýé¤ËÌH†ÙT@¨£ÿ_×9Vw*Tj´Û^÷¼½ž‘$g‹JÃNè²P£VkàùAo?B@ÂPq÷áÿÙ?ù÷–w1:îØ!ŽùýÈS=µ%鉆ò[ß\âµ7o±±¹E£ÙŠ­ÀéÊÌBÀJ––רÛ; Ý>þÿqÁùêNƒå½v”½)ñ¨z‰V|õê6ÿû·V»nê£ÍmŸ0¼c”DŸEU»lá×ȆHÿÂ?Ä´l£³xå€pô ÷Úÿ“´I{ð3×î}ϵĀ8¦À6%Ž U‘àk»nÝ_g{gÿXvÓ4Èf¢&§OM$˜‘h!{Ö¨È4ìÁææÕz½«0;¬&ÅB–O{Ë &“,íìîîÓh6@Œ2ÁÙ‘$ *Úa³êS­Ôb!O¥¢Š÷ÿüÿ|7/?èúyGŸ¾8M©;þ7}œ’ísÍ8d¢¯ø¡æwÿj‰]]bo÷ ‹Ö<­× ë›{ìïWžšö¥ß½ÒJ±[kÑÔ³Æcç%Ž•Ý&ÿüõ5*í°÷}ñ$Õg%ú\0BÓB+EëÎöþ寒Ø_fäïý#œ\a –èÄNŸ¶ï K°»†À1èZ§ï÷DŸ+å²DàÞ1¶Œb˜š™ä;Û&KËTª—gP mÛ"›M3=^â“éz_!4ºG- î…C¬¯mR9¨áù~·AÊ0MRéçK)¢„Òb­m±·½K½Þ zÏ^Êè|cy—RBãc²Ñ’ÔjuZ-oák~’Aò/¿öò•˘†|̓譥@DÁ0ÒÃÔŒƒ~ÒQõÕ}ßûö¦FÒŒ—ñýðøPâ˜k µ`ï FµVÇóý-òTB¢4µ¦G¨4q¥îD¥ßJkª ŸÿóM¶jA7 ?bq»CНZ…h¿ö[èÀëK] P!Úkl¯âß~ó`Éá<™Ÿþ÷pF'±¤Â2å@vÊî¸C†À2%ŽA7ce¿U§Þhx>‰tÛ±q“i´¡&P`H0Âȱ•"Né¡b„„H´V,É?\{H6µ”‚|Ψ9tšœ†‹>>~À7«u¶t**hê(¾Ýµ ÜßxÈÄÎ.££Ã8¶…2.º”‡3L&÷¸U‹‚ìm™cgg7J¿[éºY©syÉJÖ=›J­N³ÙÂ÷\×9* a¨y¸ºË¯þÞë2J¡‰ã4œè‹ûPsÙØÀ 븦Æ4†i0ðÍ4ž•§acQŽqÃbMyu­Í·ß]â_7Ml7q:ÉØ-l6Û´[í8ÑÏ`Aâ»Ó…ÖÕÞ'£»,­øî¾ÿ †!û½ëȌۤDµ4ï]Á»{QÙÄÒ¦ÐQ¦¥“ß× C…$ ÈfÓ&Ïš^dïܧ°P˜†ŒS³+«£å;˜Ä2ñ›u>xó-®|ïl>\ÆoµhÜd‚d&ÍÂ¥—9ûñWX|õcø!H¡¢tK4 MÔ?®tO*C`˜&7‚æ–îã¸.‰„‹”½”jҞɤæâÃm¾UOÌdhÚÜ«ZœÛÚ¥Ñh‘J&1ÍHáÙ–E:âLnÛµØÍ²rlí/S­Ôñ<ŸDÂÅ0z}&–ã0?dñƪÇ~èP«×h4[øA‡†è Hð÷Çñ×noÄ—$Q ýÆþ-R¢A±!Ÿ/RÊá&\lÓDiM³Õ¢^k°Q{À¶¦f ž«£´ñkW÷˜,,3=9Šeè“.¥ï¢@ /P‘õxFã耊z;$’=à]𴃶ðgwª´Å.òqîa´º÷?¤ùý/à¶ΓË’Ï¥q]7J]J‰ˆk¦e⺹\†\:Á[É—N"zÈÆ `ô×6l“è½ÔÜ~÷G|á3¿ÉÞú:Ã…C¹,¹±aÜ0„!Û7®óðòen]üŸþÅ¿Ga|"¶ñ2Ö‘ÅU´”ˆ°\¡¦é¤¸¼g“]^'—Ë`ŽcX×±Éåó\Þå­f@ChhD³&ò¬oïQ­5Èå28ŽA`L'‘`qÈä¯Ö4ž‚ÀpÙhhö+´ÚmÒar€&ȲmƲ6¶Ñ¦šTÍF ÏóãxUö ˆÒTj þàKï*…ìÂãV5÷°+·)ç-&&f™››btd˜R©W&M´Ò4š-*Õk«›¬¬msy»ÂŽÊ ƒ„8n¹³RQ¼ss‹LÒÆŒÍ®âW¦#Q‚À UøLûEt@}{õ¥OB°öY ˆ ŵ-Ÿ» CžoˆC HHªo…ðý¿br8ËÄÙsÌÌŒS*(çI¸nDm HÄOkb[ ×fÇ7ÙÞ(àÈÁ¢že€¿Ú2)pm“ï|îÏxíþˆlÊåÒųÌÎLP*Î㺑€„AH­Ñä`¿Êƒ¥¾óŸåçþÃHarª ü c‹(M¨A–ÔøR`)Åš]âîÚMÆ'ª$n—‚§?ÔL&˜)&)oúÜi™½r‚ÖTŒ ›{ËT*5ÚÅ!RɦAI,ÛbŸÅql È€ižG½Þbk{‡ÕÕMŠ×îòæƒ÷ëÉãå°Ï¢hÀW‚÷WZ\(o"•|DV­S %UR´=ßâ ýé:5:@Âk›MB GyÇ­r­A›@§øÑŽDݧ cŽëÑšú{߸ò æf&¸tq‘ùÙ)ææ&)òdû0f²¯ö!¥@* y÷®"ÂU™]!˜"¶ R`škï¼öu¾òÛ¿ÇÔd™‹Ï2?7ÅÂüTt¾LËŽz%´Ò´Úmjµ Ó,/¯ñÁW¿Ê¥_üûä&¦¢˜C „’P*| ¡˜˜JÐpÒÜß•,®o’Í$ã@1Ð=è:¥¡ é}n¶F_J¤m&ÙØWTª´Z-”Ê u=±-‹BÆ%ïzì{‘’¬h—J5Š-¢G|Y‘|Ò&c ¶<ƒz­Vßï%tºÒjy|õ;bHÙ·ˆô‰Y”‡S½ÍÌD‘óçøô§.±0?ÅÔä™L*ö÷zÉ0Tø¾O©T ùñ—™™g´\"•Là¸võ,D?l#ÊlUܬØFäz !0 " •A·n’¥kò•ßù}¦¹p~žO~â"³3Œ•K¤RIÇîU­5a€çù‹C”G‹¤®ÝæÁ?à•_.cA(%˜†ÆÔÓÐZ`)Aš&[Ö0ë«kŒŽÊgPk˜N%XÌðå­Þ$k­Ñ†Å^`rpP‰‹a·ÙÉ4 ²I›¢+¸[‰žÃHQ­oÑŠãÐîš`Ù¤EΕ¬×$ÕÀ Ýjãy=ejvR»Û»U._[Á4NY³±ÁhNòÒKgxõcxåÒ9ÆÆJäsÆAèºÖ×±q]‡t*r½’ —µê5¾vàÕ9­§Ml7(æ†ÌúqLVL³K–åƒÕJf«M:|â~‘¨-4ä+l³S°Lùø„^cÛ¯q ]Ê—Û2IÚ‘ËîiIÛóð|¿WˆŒ{»X]ߣÞò£Å£0Âåá$óóS,,L311 ‡Õ5Ç'/òhÓT*I©Tàì¯Îå ËN ªêGR^r|Nl¸ü"+-—oÞ¬²º¾EµÖ üSì* ©·|þ—¿¼Ãýíæ`ðĺ ÀØ]"“thÈä¡´®8^ ÃV®355Îâ™æf'-’Š¥#dìZ-Dwe $”=Ðsç3¡57Þz‡±r‰3 ³ÌÏMQ.I§' GÿRb™&ÙLšÑÑ"ãÅ<ºVÁ4dW€¤`FGaÒ²¨û{4»®Ïà1 ƒL¡`«A×Bƒo&¨Ö›´šm —|‘1ÕOÖ‰«ðh”4iû­8íµÇ}/¦AÊ6¢>- |Ÿ ŽWµÖÈNð¹¼¾Uõ£d#Â9ºÁèh‘©É1ÆÊ%ò13„”§sî»B’L02Ràã‹¥(ëôØœ™¦­-Z^È|ÆÃ6Ô£‹˜¢Wcxý¡æ/ÞÝ`ks›JµÑ´? ¬©Ø­µù?¾r‹ïÝÙ;®ÕþX3§ª;$*+äò9ÚFâtd¯…ÛØarjœ©©2¥RtìVÊԠ€í¦êUUúr#Ào·Ø¸w©éq&'Ë”JÃdÒéÓŸ/ÛŽ¸pGG ˜~ iÈÈb‰^¢ôµVG¿7¥M¥Z£ÕjÇÚzð؆aàØ;B÷?Ó@Z4ZmZí6AØ—Œaõ×èª!% Úxq ªKø˜†$iK”ƒ  Â^ÍÌì4ÏWjÍèˇûšyIKS* Ç.K×qž¸ß"* ™¤Ó)¦Æ XfT\:¿]Uˆ$P0•”늇uyªJ;„/]o¡üòß ¹0_Ž(ôãmq¨òíù!·ëüÓ/ÝäÍ»ûBm‡=&­Ð÷ߥMRB&„§˜ MÆ1)R*“I§°-ëÔÌ- CD£t[oû[g4¡ïA0::ÎH©@6“¶­'Fô÷‰ïùݽÊ;‚9,ˆ±ÏøÚ V€„Ar˜Gĵ´uôz´åÐnûxž? é0˜>ôí+ IDATdl£Ø ‰¯‰‹á/jÌ“$b+@¢Â0®™õ ˆÒšzÓ;% CãÚù|–\.ƒ›pžº×BJcÛóÑ¥íàñnOÂÏ$ø˜,Ý9eÒVG®Ï¯Vxgåÿà5>µXäìÄ ׯ¶LB¥ix!÷¶ê|ãÊ&_ý`3n¥•§;‰Ö¨ÚÎö]Æ^š¢TF7N! Dè“r-òùLw±Jyú^: e~¨Á† @‡¦€¡|6J;öm,3èI®KÂnÒ8 °> Ó I«ÝŽÑ³‡Ø uÔScÄý(GîUxžÓƒVAHaÊS†Š  CÅa0…’ Ä@+E†(–Í£… OYD`[É„ÛåzlðøÈÉ5ЈðM§Ô•R@2•âÕ!“ë;5îTN] Gk>Ÿùö ¿÷æ—¡”C&iQk)6«›U†¢c·àÔ•)Q×^§˜LMŒ1::Œ| O…“\Ç&™LDõ#Óà‰Œ²Ž’ ò”=/Ò0IeR¸ Û±»­ÉOûM3‚n<öôŒ–çáÆ ­&$e'x:„a´ãïöâÙ°n¢&êkR·Iî èy.‚ˆh¤ÿ˜Ý:ÈiƒVû‡¶mcö‘s=íê­Ïˆôc8:j0J¥ åø…Å€í+m*þ7âù«·C®¯ÖÑ4âÀV"%N7ìGü¨#«\ÿð:™ýû̼ú3³‘ë"—‘e;4¯–e`[V$â‰`¾ÝÃÚâh¾üp‹›F`Z6ÉL&ÎlÉgìÛ[£M+¦eð‡A–.åµÐ¦î`µª1#Ë‘E¡#Ê NxЙÿNˆÐÏ›Öûÿ˜Þô˜ótÚrLTwÄÎwå ÙØé]£ˆÀ«× ÿô'Íõ‡ûJÉ>ÅzŒ°V —áá<Ï”ù¹ùD×î)!¢ôg'ûÒïÆZchö6àúëÌÌN±¸0ÃÔäù|î ØE7—ÿ¸Là£FÆŒ:;ÊRéÞ‚U}ËqžœÀk{Q×ã(É“²h¾iLJ(í:>^¤AX;ˆûÊA…¤¡³üv¯,ÒtèüYyØ'ç5´ƒ(n2û ¿i¹=^}VnÏFÿ©´¦ÑòùþÕ5LCQ$ú˜ ‘6±¬ˆã(—Ë055ÆÏ½4«CM$ªÇ¥6pÍcŽÌ³ U»IðöŸ3šÐœ?;ÇââlTeN%Ÿh‚:Z\ðäJ'jQ5(Xa7#¨ú´xG8:([„ÁøÙóÔën ü´¢µÆ ¡)­¨Ó¡ÃÀØw>_ƒÒDnUuÛ21Ìžì—Q+¼c¦¢U²d1íðs¬¶£ó A¨©ƒ±ÝáxVk¼ V”:Œº1û.F>nÝ<*)žÅrĬïÞ\ç­ë›1Äåñn‘TI8®ëL&( ÌÎMñ ò¼œ­£µ8•Ïÿt`Å#¾ ªY£ýöu.œ[àÂ…fg&ºX4qdzõcçõi®WÄMS#ŽÂ@¡U4¿JC¨‰Ñµ˜0ÔqAqn™Îr°_‰„$PO,$ZkÂ0d_ ZZF牑¼y î’ª8QâÕ*ˆúA„ö¶í#„kÖ{í£T«a™Ó2Ò¨ö+¥Ùo÷Z¤ßÂÔj€¾tPIC+P±Q]œŽe3Îòèê/Ïf=ڞϯá2ûµÖÉY¿Õ }R4H§#["áJ&Bðòù9tÐ~‡»í<á#9ñÄ7Oœt¿ÇÝzÄ ªïã½ý%†©ðò«/óÊ+ç8»U¿“‰Aàóc‘Ö“\4C2âBJ´1571ÒV B¥ Be”^¾ÄÎÊ=ªÕÉ„óNþ•R4Ú>›ÊÄWÄ(^ÝÄ0fƒT:ú]iAcm G†¤3©. õ˜If»Y!úÈÐ5ÈÚ.vÚíÒ¨Š¾Ç*ÅNCu­€Ú2*FYºÁ§†Š½fˆ`é3v¬Ú¿’m •R4[Ÿùü;|ÿƒÕã­G_¢ãÏÉæ.YW’Ífº$_ŽG03=5HµÚän¯s¹9‚/»ŸQÐû®Å_½Epõ[Œ§aqq‘ìçÏÏ3>6J6“ŠÈÍb‰Gúý¯O?:;*å&ãF“›ÚÅÒÅ*ºš=PSEX%ßW¤Ç§hkk[$bÞOp÷Øçè<ðP(E 4AÈ!KÒù="­¨ÝºÂ\>G6“ŽÉ©#ºQ+Ír% ‚-ûWt€Qß!1 Wö­Ã/¼Ýˆ¼F`­¨ض0 óˆ¥j!»KhÚCm‡þ‘Þ•xL,òän•Öàù!ÿüÞä·¾ô~Ü~-¾Ã´5ƒþMˆS[eh!O¡#•JDC"¥Íðp!a’JÞŹò€‡k ç˜\Žxl¬Õíªíb¤ ÂêÁƒ°V.3SÌqéÒY.œ[àÒ¥³LN–Éç3ضÃ'Ä ? >ŠÇ0 ÒI—sî>×jyBY‹°³h¥&Ôøa„2µ$9{†ýÕû¬mlã8étÛ²!†y—¡¢åÜi(¶<(¼0â”Æ­TÿÄHÍý¼å›_™g(Ÿ!qLM~òÁvЋâD’öÛØí*ÉT)jä2n™AkM½²V 1„F ‰ÙÚǶÌhóÃçÑšZ+ä b HÓÊ`™V÷˜æ£‹^H½”Þ£3 ÿ •âÎò.¿ýÅ÷øÃ×®ÆÌ'ù܇XË[2ꀑ‘YŠÃMd?Q²›py´Ö$.¦i1rg‰«K×Ye˜†;‚6­'³ }=)ªQ!X¿…¸÷9[3wn†ù¹i>öÊ9æç¦˜ž'—McÛv<¹j°Cp ‡=þý9z]=‚f—¹¬ ¿_¡aäBddE|¦Ò!˜2ª'B!8ã3ìÔö±7wñ3d³é¾f¦ž t¸¾öÛ+mͶ'ðƒ(H÷ƒ¨P金@úÁ 5a¨QÂb÷ÊÛ$U;êÊáÆÊÃÖ£Ö ¸¶`ÊþøO@}'l‘N§H$=•Öì5|¶›aÜ/°»¸#v¼á ó¿Ži_kžÂÒ!6 ˲°¬Þ^‡æS¸º(¥ðü€VÓ£ÙlwÎqë«Õ¨·|n=Üåk«üákWÙØ­G,¸=âÈ¢ì+bïߣ”w+Q,H&ly¦a ¥b×up›‘Ò0ÃCw¸ÿ`…»Ëì† jÉqT¦Â8j¡ItÐ^¹Úyˆµ¿LÖTLÌŒ11QæüÙ9æf'X˜Ÿ¦Xê62=²hªˆçd@" ×a¸ãüÃuÞ S„†I ¢?#Ô|Ñ)0®#d‡YW>õF‹bX'í˜$ SH´Œâ‡Z¨Ù÷aËxJà‘µðÂÈ‚x¡ŽTŸ5@C³ZáàG¯s~¦ÄX¹Da(‡} LIiÍýý6*á€ó)¤Dï,“¶ ›I“L¸Ý5Щ‰¬xT½¸%; qÛR©q\ÇÀ™uê/[5–¯pñq¤Âqì>C¿ÑÇ{‡×«Ö‚åFŠÏ¾¾Î—ß«‘H¾‡Ñ1]ýt™q9´Þ ¨5}¶´¼ˆ’Åǰ¥ˆG¤gj;äÃ&'Î19>Êp!‡ãXG„RJ‰ëÚñ¾Ùl¦› ž¸ûÕµ-înÜcIå ÅIIÝE ù{ï|‘ÒH‘ɹ2ãc%fç&™œ(37;·ÀÅ=âæðáʱxd¡ðùˆH‡>'“Íp.»ÊƒÝ-öÌ1ŒPãKRc*!R÷xç´ˆ«ÈQCR`YÔÐXžÆ¢ <-:ñ„Ò]·Ê tW8|¥ñc Y…ï‡,û/ÉYŠé©qÆÊ¥ØJGppB…¼µÚ¦âé>ŸN£¥±}Ÿ\6E.—Ž›òbA#´âÖN;JÛ"í* á‘J¥H&]ÌÃ(e­XÞkÓ §=lœxwªNfÍ|¤ssBca3°XÚj£·Û}ôû‡¸ae”E–†è²qw8ýŘŽ+×σ¤{'Ö~ gë&gKÌÍO1>1J.ŸÅ:¬ë¼ïP݆AÂuI§“Œ•˜œ(³¾±Cþæ ë+&­Gf¬" dH˜áì…E.¾´ÈääSSe† Q”L$:îžîrÄ‹UBëgò¶zÝxI&ËÃ\Ú}À·Û9|™Ä #²h£ "Ô}×!€•V„*jÅõ%q_OGçit·ÎÄÇJ´ƒH(ÚA,,±»å…QGáæ‡ïR½ü]>ùÊ"ss”?JFžtq€Z)šmŸo/µÑŠ8ÍiSÕ¨Ü_fxî¹\†„ëv·hSaH¼»ÚFÅõ ³¶EÒ6ÈdÓ¸î!wLEõáF_ rºŽc.Ï›Oÿ0téËã™ÝsÅj¥n6q·aó*#‰3 3,ÌM122 ?âÒÁ†Ä² R©¹lšÒÈ0méðÚæ.-_ˆÃôª¦a0>>Âùs |üÕ Œ2<œ'á:QåT›ÅèC’p\ÖJ"¼6²›‡Ïæf96ÃÃC,ŽW¸óKÆËøÒF(‰vúB¢eÔ!ÔJj‰)5êlm »PõÎ,éN]%Ôºkt]­ cEˆ¬‹ÒTwvYúòg™,d83?ÍÌÌCù,®k š•Rüp¹ÁýƒSèpa¸z›¬#( Qÿ‘k#úà›Õ6·w½¦5öÞC2™4ÙLª› è?W£p{»…òº†m›$bƘN¬bžª òhe{B UslÝ@–(®?JéFëI£7oPb—3 ‹\8·ÀÜì$CùÜ@‹æãªÒ–eašQÃq]ÊÛ ãá+‡›˜¯”’áBžñ±&c†ödû÷“c—úŽß´ ‰Ø_Ã6D—¡ãiqQ~©\6ÃÄD™K›;´Ößgkò“1¹[o~t¬k•ÒhCj…e‚n'b‡°^÷U·éÖT:Ù±ž+Õ±$š¶¯ð”¦²µÅµÏþ:EWpþüBÔ}:Q&޶%Œm¡ÒðùÂÍFœ¦íÃ^>bé}FGKqKp'NˆDÅJŕՕ¶Â‘€×Ä­o›#ŸËÄÔE}éd­Ø8h±z`£È‰&N"Úê-j3xb rœ¿u\ÚòPŽôiK[7É5°øò/¿t†3 ÓŒ–‹q`&ŸÈíˆEà8Ö!­õr ­lÇ"™ŒŠ’ýVã‰gZœH$‰V˜[wIÎæº»= Æ-ŠÅ¢`}anŠfão>¼Lkâe¤°ã”ˆBi ¨ˆ‰D+LÕK ñwIÙÛÖ sa½O§®tbŽ@êÕ×>ûëØë\øä%^º°ÀÌô8…xK‚%§òÆÃ:ïozȸÊÝIa©e•5Ê?F©X ót<ò¦çóƒ ŒX°ŒÆ>IÝf¨ÐƒówÜüNFõÖfƒ¦¯)-2$™Ht©ˆŸæ}‚•Ý­Dk1Jôçù#y9œ8æÊGoÞ¢à/³°0Í«»ÀÅ‹g™š'ŸË ´žÐG+€õ+w}@ø|ªàÝE\‘^'ÕÜfhh†t*ï!(žçÖe=O'™˜%TšÚÛïsíæ÷i,þ Úµ£EN´¹eD‹ÑTQJ8Bøj$"Þò¥’Ñʸê¹XaŸñBE %ÛK÷¹ñùß!YßâÂK‹¼ú± \8¿@¹\"N ¸;Bˆ¸"ð/®TiøÑÞÝÉ2LÂ?¤”v¡TŠ÷ñˆI†,í¶x{µ‰)¢~{÷>Ù„Ép!O6›éîÞ9¤ç¼½1]æUljæ+™tZŽÍ£"q²«ÔƒdJÍžVžô# •B ½&rý=†ŒÎ/páÂ.½|–¹¹)††rñ4ϸZQz¢aÔÇ[Äg;!xˆv‚°º‹þðÛŒN•-öõö?›0J)0ͨÓoRiEäµÛ¼ÿÁ×iÍ} †G»Û9‡*^ð-Ö„ŽûÉõ ͰîÀFâzˆîe³ü€8XWl\¿ÂÍ?ýM²fØeJ¹p~ÉÉȵ²LóHÙB¾t½ÂíÝ k:sîmânÝfüâBÄœ“ÏvëMZkŠ7ÔØiÑ÷BŸÄî}†&ò yÒ©A ¥ÛÕ6—Wê8R3`¹K‹ëºÂk>ÙŠ>$}CG*Ãâ”.›ˆXRôö]ìÊ&†ÌÍå㯾Äùsó,ž ƒ çùmÈ©OP±Ðs*NècŠ-Z4Ð~›ö÷ÿŒ1Qcnî%¦&ÇÈås8¶ý\îÓ0¢ f …g¦qqùCn|ð5¶G.`Ï\@e2˜aDj„Óˆ™Hb7Kò 5} Š:²¡Žã*ÛÛÜúËϱÿÁ[ÌŒ—8sf†O~üeÏÌ2;3ï°{HøcëqyµÎ¿¸R ƒ}Ö# ðÞ}©Bšé˜¯+—ÍtÓÃJiZmŸ¯Þ¨(ŒÝ‡duƒÒȆ yR©dï¥c˜ó‡k5Vö=’´¢N2U ›IwËÇZ“á{úÑ1ÅIürc†Wú`£ºLÞlSžæü¹yÎ,ÌpéâY&'Ê”Š…|÷s)f3YZ?÷sé8ûwïG_a$ÜeñÂÎ,Ì0>>o1öüî5›áB>êÞD“ɤyÿꇬ-]¥5ÿq’SgpRI ¥¢:I—àAw÷ íÞƒî¹XJGzó`Ÿßý[—ßÀ œ[˜êÒ½tá år‰\>‹Ó'ýéÖÀù­wØn¨®~ê,í.ÎÖm¦?qž©É±(‹˜p»î’RŠ·T¹½ëE ŒJaoÜdh(ÏèH|>j ïF(¥@‡¼q÷€†¯™ÕU2$—M“É$±­Áyb±ê$ëq;æÐ*D„:h ÞY+ 8:Äìì"3Ócœ?¿ÀôÔ““cd³i1/ìóY0‘G}ñÖÊ'\o´s§)ìé6í´svòòømƒMü‡×1–Þc,ërñcç¹øÒ"çÎÎQ&[Êç†ñín)Óä²Qßû½ûË\»þ-v.›pdg|ŽÄèN2ƒé8]ôk'Ó§Ñ„~@è{Ô¶·¨®¯°yù-jnâ¨ÓåQ"^ßóçæ™˜,36Úah´ŽÍ‚óäsqAl=öëm>ûÞ²ÒÕ¶HVW½8 f.o“Ы Wß¿WE AYí’p-òù,™L:.øÊ\,}š¸A£ª«HD@±.V§·ãmo{ÐaÔ&«<¤jcãá˜I»ärEÆÊ#”Ë%æç¦˜˜ez²ÌÐP>®²šÏQ8¢Å’qM~zÚáæÝ¶w÷£íÝ”îðÌuk:¦e‘¶É‘dÔØ#åe•D¼™éOjî>Xcùî*«Ûd,Áøü8Ó3\ºx–3óӌɦŸ«õ8“8ŽÕõßÛf¸X`¸X`åá–°ýð*kž&t’ÈT™L#-a^›°Ý¤µ»ƒWÙ#8ØÁP™”ÃØd‘‰‰2cc%様e||”\.C*™Ä²Œ„#âòj´Ú±ÏÒÝ"´¦uå;äý}æf#:Ô(ÍÞ#hÍ[KU®l4£}D„½r•œ-(»[;wæ´Sgyw逵ŠOJ† éÉT´Mw[œƒŸeê¹J‘ ³êËÒ‚\:M2ébHãä­ ã*ºe%°í,©T’T*ÅðpŽB!ÏX¹D©X \Ž(„ïJû<‹”PÌ%ø>Uä{Þ7›ÛlT·ñ¼` xR.¥B±ñ‹d³é¸ÿþt ‚ÝÔ²iòKg\ñ®ì´8ì…a†òY¦&ǘœevv’R)ú̶Íç¶]õI×ÔIUÛ¶E~(K©8ÄÎÜkk›líì³±±ÅÞÞû»›4öÐjz„aTå·„$e[¸Y—Ìä<ét’âpžbaˆ±ñŠÅ!FG¢DC*•ذæ¤(Ũ2— ¸ß°º{Á{÷¯a\{3/ÍsþÜ<Ó3ãv+¶JkZžÏ￳CË—¢¹Gzû£ “LN–)‡¶uÖª ?{o)ãz‡„©Éå²äóÙØu´Þæiui‘( CÎÏ—™˜ˆ*Ë–e#¶Üó¶m[¤’.©T22iÙC¹,étŠT*ÑÅÁ<Ï”jÿéìb44<Ĺ gÈ å¨Tk1;_/¹ÔÅ4eRÑ6¥¡¨(%NoE¢m¾l†† ÌŸ™ÅM&ñ<×qÈæÒŒ”†Ée) åI&Çú±XŽ“\®d2ª»ŽÍÐP–‘‘aªÕ{{ªÕZ—½Cú¬bÅa›&Žc“N%I¥“ å3d2òù,©T‚T2m[§Vp‚ˆLû— »üžÇrÓÀß^ÃxïkŒä8·8Ãüü¥b´F7ö_½¶ÇÕÍVD ®ÖÃ+äSãeÊ£E²ÙL /‘]ëq}­Æû+5’2 ìoãd†rdâæ­ÃÙÃ'®ƒ˜øŒ3\¼x– çç‰Ïñ­®I2 3Úä$FKºŽƒíX8vTéþqXŒ#‹VH’É££EÇfnv²·YÊ¡D“aXV´o^6ž<ñ©W)%ŽcGf>á21>‚RÛŽš¼’ÉŽmw­ÆËr<ʚضÄ0£ø$“NãôZys»ÕÆóýˆ2 »{¢˜fDø=G×µãû°ºû¡Ÿö9v”–ã8,“üüÚ~tã{;{¤çF™àâųÌÍNvIÑ…q‘¯Æo¿µ¥„5ûë$7o0~n’™™qFF†I§Ý8J)ZñÅ÷·Øm„L‹r4È硇Nä#>Eš·ßœ’²N©ezj,ŽʸÒ~]f¿(à•1Š4Œ.ë[0úã‚Nͱí˜bR›±ê<<Ó2±úøÉ4µìZÆ\.#™Ew!ýu ƱY®øy†ÄÑÉ„ƒRŠ T¨P¡TD…ÓÙPKÄýÚ=Ph¯Šxr–‚(àP.—ð|Ëv¨Öê$“ ÆË¥¨`x(nÍ•ÝÍTÿðmV*~÷ÑÙ÷ÈPB2=Yfbl$*犵½¯ßÞ“j Ç2(ò䇲$\÷Øäȸ»îr».J„ò(ºUFG&£Ø¡T`x(‹eY§Ò®Ç¡oÿ:GgÑÛ1ñò“jܧ|t¶š‹Âèß+Àî ¤œÏ1J8Œ”¢>ž‘Ò0í¶×uq‡âÅÛ ÌÃPñõë{|éZ¥‹H6×®“­®0y~ùù)ÆÆFâJ»Ù ÎÛ^À½½Án#dÄh2¬Èd†f(ŽmŽ+ΚGÒ¸ƒÕ¾]œCíUF&r”Ë¥¸í5¯†üÈ=ðÇ é_×õþ¤ÌËI×ûã¾þþNH˲H§“(¥0bîfÛ±ºœJknlÔùÌ›Hu±ÊfçÁÛŒŒ–˜™`b|4Nxô¼RŠ›ëU¾úá®3jÓ4)rÝöí“<ó¸"ù@8&šrZËŒe}æç§™›¢X"g~ÒÁ‹ñÑJÓ4âÕêö†ô+3¥µ¦Çÿôìy1ÀX`ßx¼n°0¿Àâ™Ê£¥)lt¿§TÈo•ÕŠÏŒ†;dóù®¢O¸î‰®®ùèØ\@èa·Ö(çæçgX\œezjŒÜ!ïÅxòpé?ù}¼ |æcý7¿dñÿýô-$'Y¬œý‹ìòþZ3‚£ 1W>$}ð€ñÙ)æç§˜œ(wëgÝ:‹Ò\yXáÛ·æUÔVP* 1R,N¥¹ŠydonB‡Õ *dä#¥4çÏŸç¥ ó\|y‘©©±îö[/äéÇÆ64ƒçQ7Ÿ›ÿ;Ohy>ï¯ÔbHÀÜ]"y绌O”9~žÅ33ŒŽ–öÆ •b·Úâ3¯¯à+ÍŒØe(¬-䙥4Òƒ14̨ÕRóS¯LÒ¬îsÿî=vwvÚu9“òè “e^~é ÓLMÅÕXó™§ÿËëð|@%åòß\ @…üì¸äÍ;a«Njé ²Y—3óÓ,.ÌP.—â}{ F´æ‹—7yg©JJ̄똎M©T 42L&“~ìîf”|êÒ 'äÝlmçÑ@6“bb|”±±ÿ_{g÷ÛTÆñOÛÓmm×÷Ó—­]۬̽Át1š`„paˆ˜`¸Ðâ Ä?€Ä¨\pE4†^ÀÄ‚øL@ÅÁ€ò²ØÆ^À±nc¥]ÛsŽm— «ÛXalý}.O{~OûÍyžó¼EÂA<'.§cJGŸÈ|èê(ÒI}ødßÒ}‚äfú6¸ôl ÜåÚµÆE“r ƒÁ€ßç!ªÂë™}Õ´¤Óå“5eH’„ÙdBCË.“Ñë‘$ýd‚MP<îÞƒ±tú‘׈ëêúÙóáa ‡ÈæJÊ‘õNìv+Æ\ÉK>›ŸuÅTÎuÅ8òÇ]êt˜2ã8½.^›Jëì{n¤¼Bó¥ùWd¿~9@˜¾ÎgMkƒ0Î4äÝzƒ¡b²¼D7 ÁIDAT¥Ö/+s7c||¬‹TF%¢Q• Ül& LŒ0UÌ~é¬$İ0tÞNMsU¡qYTç\­BÿQUÓŸ`÷ñnÇRxuqê2=è% Ÿ×M0àC–]sÇ#<=þj¿:ÍÕ$mG9râ47úct ű—CÈe¤9b¥9*ÓÚÁ+{„§ŠCÕK²óÛN® ƱÒ´*Ô$UÕUÙ&®€‡Ý:ëÕ“¢Ô´b7_ f ÐxáݯH¤Óq·ÂöÕ&6¿µ§ÃUò¶Ô´ìjÎ='nr ­ƒ–áE]7öä?ØVš›êˆFÃÔý¹z¯¹=Dä½úÜ,qøüçopÞ¾!@Í(ÄI̼bDVb8\vjkCD£!ü>ù¿­ºB Ïx€>8ßöæ}[v}I||¬Ä¢‘NgX_oaû ˆ9…Ín%ª&àñd§0>nîNdèêM'Ð rìäñ’ÜÑé°šË{mDš—ÑØ%\S=Ù8õ¸i ¤/D€žË Gm)6®ÓR"XåÎNU,+#Ñ34<ÁùKÃì<|ŠØDawìÀOØ´~J÷§”$ÖJ3~ŸŒ5×:ëtÚ¨´X(3Îo†ÒŸ2ŠŸú†µ+ëiX1›Û7ïìüšÞûã¾1À¹O·àrÉ%kSUUÉdÒé Š¢d»F§ôÈÏë %òìsò ¼÷ÅÁŸÆ9½ë ‚p ‡!Ú䂨l³Uv‚N7ÿÜžp±QP(TŸ ¼¬¼äã'•äAú¢ð±§L|„Q*Í•ÂHO!EÀðHáG}Ô–Âd2 # ”.gÿŒlMÛ¸jEn—³@dsá œnkCUç6 ¡§vû®À§^nY.Œ+²øiïΰyo;;vâÊõëhª:ã=ݰ}÷±ìÂiØÐcy½Èÿ„ ž—;opô’ž£—ÎòZ´7_m¤!Z=¹AK¯3O¨ô Žsê÷~>ùñ<Ц Îß߸A¸WO‘y ¨*¬Üv˜¡D²H'&Ù¿­‘Õ+W ã kñs/EG‚}[ë„8„‹µtè¿ Æ"œ³*ô€¶®¥6R+Œ*\¬¥C:W®ñïíìÿí*MÓýëê46½ÞÂKÏ7QVâYs!%N" ·†éì¹Ã­þ®÷ Ó?’àöèŽ ˆÛ„l3R´Ðõ º úý³Ûý&D.B ˆ@°TùN/ÝÙ  ”=IEND®B`‚Pyro5-5.15/docs/source/_static/tf_pyrotaunt.png000066400000000000000000001055761451404116400216220ustar00rootroot00000000000000‰PNG  IHDRÈÈ­X®ž cHRMz%€ƒùÿ€éu0ê`:˜o’_ÅF pHYs  ÒÝ~ütEXtSoftwarePaint.NET v3.5.87;€]ŠßIDATx^í}”Tç²õˆ‚Ë ãîîÚãîîîƒ»;ÁÝ Á‚‡ ÁI ÎLý»ÎÀ}÷½wß½&X³Ö·ºéé>}Î×µOÙ®ªÿ÷ÿÄÿÄ; Þñˆw@¼âï€xÄ;Ð1;ðàÁÃ~ûí·÷:æè⣊wà ß=_~i°nýú¾oøeˆO_¼³³fÏöihhÐ똣¿GýñÇ;]¿~½ë¯¿þ*Ö¤oÆOöòÎòé³gï=yò佟þùƒ›7o~páÂÅ._¾ÜùÅ7Ô××aåݽ{·ÓËûÖ×÷Hßÿý{[¶lé‘––¦åíåémoo_¯«§»VIIi¯···×ë{æÿþÌN:Õ¹¸¸XÃÝÍ=%44ÔøÀ¾©×ò·œ÷Õ«W{oÚ´É8=#3+.!¾2$,¬Ù7 °ÙÙÕ}d`pXñœ9s-Ïœ9ÓsÛ¶m½£¢c‡/]¶\åo9±Wô%ׯ_ë´yóf‰ð°ð(]ÝYrr²§ p§GîO»vëJ]>êÒ: ÿ€#‹-’xE§ø‡¾öÁƒÂMîÖ­[âwVwqqÉ2dÈî.]º<èÓ¯ï9 +ëÌá#Zzý¡ƒ¾Ío††øèðáê+W®L=zôœððð‹æ•”HNIdHVA±MNA¥UFN©MEEí±……å ?ÿ€Ò„Ää{GçéÆOèÿ¶ìöƒ5ÅÇÑ1Ñ:žX##£5’’’×?éù õìуºwÿ˜ºvíJ}ô}øáôþïS§ÎHMUe˜1£?y÷áÞ½{kkkõ‚‚‚ªõôõ>366®Ø7öéÓçV¯^½.ôèÑãçÎï¿Oݺu#ÙÛžžî¯ãuü­çÔÚÚÚé—_~‘9qâd-âàäÉ“ï´9;;?±´´¼`bbºÒØÔdAPHÈ”€ à1îžÞ# ×ÊÊÈß•“‘kSTPz¨  ô¢¢òM//¯¢Wijݸq£ó‰'z8q¼ï… çßÿ3É~Ä¡C‡úfffN–––>Õ¯¿_ <Ôýãî‚ðtëÊ« ´Æ‡ÏÁÑ™:¿ßIX=?éù¯¯óŸùîŽúÌýû÷;]»v­çÈ‘#µ4´Ž÷ïÛïa·n]Úºwëú¤oŸ>Ç r}}ýLƒ‚’•UTŽJJJ<ÐOÆÄĘuÔ9½öÇ}úôiƒÎO?ÿ4>ŵ¯¾úŠ =–——ˆŠŠ[ZZ*ÚºukŸu!pH;AË¨ÂæNÕÖÒþLVFœ),žžž---=:zê÷Î;÷ÑÜ9³ÅÅF«{{{f™™šn—––º4Drðwšêª\]œçædgiœ:uò>Ó¿:¯#GŽ|TŸ¯ìíååomc3_N^þ"Ÿ|Ò“>0z@[tƒõÁšâC<öèÞ½õ£?¤>øë}êüAgáoü ­_ýu׎ރwüï¾û®Ó§Ÿ~:0R/²}ª¡¢vAz Ä=É>½IAbI êOݺ|Hý`N¥¤¦ˆ^ëС¯zŒla¸téRIþ_å5¼’ïnkk{ïÙ³gƒ>|X •ûõÝ{÷ž;v¬ šãdsSseqQ±"lÒßõãâŽÝ©¾~¨´‹‹k.î´?*$ª*ª×s‘ù·Bùg/þ›o¾éŠ^93#=ÎNd3FCMe«¢¼ÌII‰A¿ ÐzõîE=zv§ÞXýûöiÕÒTß–—›múâûøœñÃÀ‡‹/t¯««u¶±¶jVTP8$!1øG£­gÏžßãca}üq»ÆøÂßùýÎ §ººº'ºwïþ¾‡`f @ù¨}}Üýã•••ŽöÿÊçøºŽ?Þ'%%%^OGoä€Áw÷éÓ*;°?Éý{õ ¾½z>…õó0 ;wîüÌÑÑq9n˜Jãþ•s}­>˦T­$L *ãÜO?ýDG}2aâÄ=EEÅ9Ÿ}öÙ¿Ô¿÷"b¢c¬t´uÖ)*(>““•’7oÞ¼n¿÷óÿê}tZ¸pA¿ÔÔM¿H//Ï1ššG¥†HÜп/ è߇âq`?^ý¨?ûöíÿ ;ü„nÔ`á¿«(+\ñòtO±µµ*†–Y óáKk+Ëêjj'¢§½{õ¢^ŸÀ·èÉfÔÇG•$%$b} Mñ¬'4H¼ÞµKW(êÔ©S›™¹ùÞþ>ì ò÷±¹Õ @8„÷a/6}úé¢!åúÿÈg·mÝÖ=+3KßÃÍc˜ºªÚéýzµIêG ’ýIzP_þcb@°Ÿ@?TõüñÇ­ø>ÿûóæÏ7ü#ß÷V½—µሻç„jBðÑ–[ÃGŒW]S£´{÷žþê}ôëNe¥e²pì÷)È+’²’ò™°Ð0“?zÜ_~¹÷ÞáÇzli± ð/022\«§«}JYIñž¼œÌ3É!4pѿ߭ýñ^ƒÉAï)Üõû|ÒÀéCƒ i)‰ß èó¸LŒü¾¾½!ô]aB}L½ð~GOÖݺÈ×Ï·räÈ5)©!—øõOX«0ž>üˆàØ>îݧw[¯Þ½ Z‡>„¹òQ7¼Þ»¾ûc’’’º••é÷G¯ÿ¾áÙfL›ahaj1YAFî´ÔàOd$¯¸ö.] á`² (:u¢÷;¿ßÆÚ¯Ï ñðôªüá‡þ²üÑó¥ï‡Öx`Ѐ=<üôéÓ¿BõÒªU«îT×T/8i¢6bú/Ý ÊÍɵÖÑÑ=#5Dú©µÍø#Gôï6aêÔ©ò‘N~~~Йîîn+ ôÏ+ÈÉ>U•"Y’•–"9i’Áãé!4/hx>p ~¾@“ôþ¤—  >ÁÝ|P¿^$;©¼¬$Iî ÀôÂ{ÂëÓ«'uïú!̱ÂóÞ½ Eð™÷!4þë&&& ¹ºñ ´Ë'ÐFl¾áu€« À÷ö0 ‚ˆÕåcÛ‡°ðû¡‘Úà£Í‚9ûÒ“‡0;O›6MùŠ0˜QÛöð«$öBZr pCø×Ó„.]°øæáGö‡0Û£n Ò½‡¨»Ñ{¸^ø]qLÅW*°ç—?zôèý‹/Ú´Iü_•×îó|w:ÿíy%ð„–­^µúîÄ [srsÎdfezŒ5òã¿ó„=~ô»[ „zåZ5Ô5 mð3Æ¿8Ÿ~ú±KmMÅHSS}Ü•åóˆmh¶ÿùŽÛ鉻›äÁÐ }‚‰Õî{ÈàæÅBAf—Œ€#O긛kª+‘ަ*èªCøµðš"Ž3 ÚE[UŽDVúäâhNNö¦0­d^e òw¤,]5y2ÑQ##mÕ;ùYéž“ÆQ Xgf¨G¶¶¶¤¥£CÆ&Æ{ììí&)++]ùÖ*/¯ÓJ†@{ãn€G€¡¦ÕCÖ€ Fnä!ý$bÿ4@à§ ´³ÕËÈJßU‡6³´²$SS€¢›O½ag a10äääH79yh8,y<—#œ³ö…AÁ ¿­oŸ¾œÛùÅÜÂ<âï”—ý.¦\¸p¾Æ—•0©.#yF CîÅÅņ ס_þo¾vÍÚÁH4Ú$%%-Ö×ÓoUWW'-m­“[¶n4Ùñc_ŒÜafj¡†yŸïvƒ8 °cݳ{Üýá °ÂÎ94‰±9dhdáW¤~òA¸Á°À3”±'ʸYðß•ñ\U•Tp“RÁo ª¦*üM¦ë€AƒX­Ð<¿A£Ü…x›úvN:ì|òäI˜O˶nÙzá‚…”““sª\ø¯Q™Ü‘š­JfÜ­ãÜ,YPŽ>4("<èsKsCÜí5!f“íê!ÂÞNE˜–M¯þ¿G]E€Rƒ¡AúÚšd £MúZªdn¤M6Æä`kNÎöVäî""/Oòpu$}˜Fjð54(ÄGD Q¾äì`3 &œñÜŒ0^ŸE^ö8Ž!| µ68ý!4÷ûõت¢®Mª:‡d;  êŠÜÁøþû­ +þŒÂ>øZSáðºáfõر\¹rå߆¶ÿ` ¥©yXNJ²oLŒüð„gqb3ÉÈÔç5˜ú#C®T¦—6öDGf-¡¬¡NêÐ|Zzú¤‰GeDZe @Ö & |¼SîîîIÁÁÁ~ f›^o<µɾ® Ò9îýrïB?º±¾™ë’¥™aßÁŒlq ; }òt‘¿·+ùyPx¨EERlTÀ`M††dblH1!ÎT†÷‰ÈÊ\ŸBý]¨¾<…«Óñš)+âÎ+‡»2¾›#?Ÿp¬wïgHþŠÜÇ5Øö_ÃÙ‚Ðêxø9ùùùö«V¯’G±ÔŸ6Ÿþç^rè{âĉʡaáJ*j唄&Ó\8JʼnJæ…ñ ÄÂÚZBIH\¢…LavYX˜‘©‰éÁ4ÔÔÕ! ]€EÚEþ‡Ô3E…µt´?wrqºpÑBu°*^zì÷ÊÇKßÙ3g•A'¨_·nÝw6lx_ãQiIén ]çÎû·9â¿÷ÂF­áééŠ8DIÉ[{÷îhû÷íäêâô¹…©1?ª±>êjñ††€ƒ-ÅN$ò’µê먓¹±eCY›¶k +rsASØ‘'–»EzP\d%ÇSFR8e¦ÅQvz…x¨%dmaNÉÑ^TUOaA.älgIán”žLQ¡ždb¤ÁͤGϧ°ç”<©¯¯¿ÜÚÆz˜Hd?Ä9ðÕ¢¨ ˆ€9*õ?÷ù éÔÔÔ$=}Ã/ÕÔµ“–•@ÊÑ)Ö^:æÄè`ÎÇ tmbnNºúFˆFõÂàÎÎää$‚bBZ‡"4®,ÂâC?ÓÖÕ½êãë3%,,,æ·ÏoG1Tu—7­\µªjÖ¬Ù×gÏšÝ6f̘`ÜDõW0´ÈkŒ?<ÓÞMÍÃzÁdó~È. ‹:D¹F¡áš.Ì$Ks#²ƒ&Y(&údf¤‹|…"svÈ¥=Ò"WGò|ÜÈßÇBïE‘Á¾âG‘¡æ)¢´X?ÊÏŠ§’ÜDª,J£Ú²j¨*Âÿ“È^dENö”D#ê3)&Ü ³ÂÁZª0·”BÃ,·²²ŠÅÍÆ Õ!@‡‡Äûíþ{»wíê‘•’ ñvkÑRQ¸MÛª¤¢‰ž¬™êü ûbœ0UBôNQNªMYAîGEyÙû"{;²‰`b ¦Á¸™8àšÌ QU`J©¨©=Ò1Ð?m-²]XSWç ÓoÐï½±½ïã,8.Ê YéáÓ§Ï87eÊÔ‡ ­……W«*«ªGŒ¡Ž¼ÂkIEÞ½{·ÌðáÃ]]]}ÿ1ÌŽ–05êgC$$Ÿ) ª^œkmr†»;Ù!ºd aA&°—a2pîÃÄP›ýœ)š :ÄŸâ#¡%B)=!’2R¢)-)–C<© %˜j˳Š|1´”Æ4UÒ¸54¢¡DÐ4n.”™NÃê³)6š˄“k­ÈŒ?­emrR¢zGÕ“3ÕÕ{¼¨àcD裆ú: £© C|7¨O'C iUT5àpK y Qs8[ ‘'eYø_*·ílVD„‡…ED|êíçKöÎNˆžÉú†ºÐ(º÷]ÜÝ>‹ŒŽ)..+Óݺm[¯7BàïIÂ9û‰6_pñWÁ½ºŠ¶ÚÚº99¹[JËÊ"7¡äó÷ëU¼Z£»ŠŠÊØËmLé}DˆR) Zp°õáKèji ¾…¢<’ujJp¬­ÉßË…}]É⻃9hkàîÎæ•6%ÄøPJRe§Å@K$SI^*U¥Smi•æPaj Ue…Ó¸a•4iT=MÛD³&Œ ¹SGÑŒ‰-äO>í&X^zņûâ{ܾ.-)qGý‹ÖíÛ·ÿT”é_í/³öîÛÛ79pËœA#/´±±YnffvÈÊÚjêØ­òÎÒÑÑÞ‰Pö“ž Fö†"-»¾–­çì½"VÈé´i¨*ýbd »ËËÓ­jé’%Š×®µsç6oÙª“ÂÉÕù‘º–Ö ä/v¥¤¥-\´ÈLí·“p»J)Ÿþù÷sæÌiÍÊÊ~TPP¸µ¤´Ô}Üøñ’¯«ÆøgA@>RTT<*H‰BŠ,ä°‡á4rÂMdeNvVVdcn†ð,;çŠädgEÁ~îÓ'*܃"‚œñw22ЃS®C9©aTVLÕ%™TW‘O͵EÐ%4ZbX]Õ¦Scn$Íß(€bጱ´xÖDZ0m MÕH!AäïéF~ž.Ð<áâçþ,;-aâ­ïÎõ¸}íâK±Å¡º,X°@;)1)tüYMï…¸ .Ô#ÖžÝ`b‚âñù‰Ëð'~ÃÍ£s>ƒÀã¬X;+)Ê’*rCªÊòìEÖëƒ|“ id¿>zô¿'qêô颂¢ÂÜÄ”dç9sçÊܹsç¥ ^ÅÍõß~'õüé§Ÿ?=þü£Õ«W·!BræT$h#/•èöw\¸H$𥤠߯¸ƒ­%¹Ãyôót…SìMqAµ†ÁÌ !M„_Yœà0‡ÁÉÎH £ÜôpÊI #_øÖ–æÈz›Q9À!˜Mêh|K-MÓ@Ó'6Ñœ)ÃhæÄFj®Î§ÆœpZ:£…–Ìž hÑUTQ˜ã%’«“#‡:d ÿÆšÊÒH»­2Áéç1ynßÖ&;›Z—V±vþÄß­™áŸ|¸léÒ>>>ÆpÞáÀ[ÏTWS9úI÷n9oc@°ÏÅœ¨ÝáG ™ÇLa®7áäÜ8Ùœ¬c‡›ùcJ†:’ºšª÷M õNyº¹43Êì­ø?"ˆ¨Õø+x@!€ã8~PýoäÁÛǧ@Q^îQ¯E‡RRL¥'ÅQfb å¦%B೨?ަqÍUT“JS†•Ss ;æ©øžxD¨")>&ŒÌíÁÔæj !“ëE ɶï©O• ®¿Œ©Hi9{âú™Ó'Ãù_¾îØáïçlog×ièzôìñK×.¶uíòêHº aXŽ8 ™í~íTü!ÏlÎëÈL›Q…‰©€ÄžºŠéh¨<¿ëptdxviq¡.JþRåÈÞñ^hHä&x>ÏŽ]¨ì{£9ùH:YÈËÉ]Ž £üô*ÎNEʹ JOÔT”Z›ªK‘(¤¨°àG:ZšODVFpÂ]ð¾(jªÎ ±Ãó…Š'O7'òòÀë9‰´hæhZ6w2­˜?…Ö,™Fk—N¤ F0 0¹J))ؙҢ})9&.òõp"kDÈPŽ+„Œ™ºÞù;€rnKm[8в );ʃ¦UEШl×Öœ0Ñ}O'›êÊ }÷îÙ5`ذaª¨SqC2­ Ûsè6´Ã3®¥à܃@kGV»/hæ‘x“`€¬v;ëW|&V"ãsÐÑR@AðÃÔ»@Hû©¹±Á9'{Ñœ1£FŠ`B½q–Âß.cŠšŽÁÿhƒcw<»¿å‹;ðKfÏž=´ô->^p¨3©º8§-7#e¿µõP©KEÙi”š@) ±gý|}Ž™!Ïáå.ÂkÁÔ\— ó©¾C…z!‚åM¥y)´rþDZ³h§Ðüé#hÂè ª¯Ê¦ÒÂdÓ)Èa`7[ÐHL!€ ÈÂ÷Gb­+2Þï æWÐj(Єštš14™&VÄP¨½&ÆºÓøOhe„d^äd©wVKSm;„üÌ¡[ð#žþ7³ ä@!/ì¶“\§¢P¨!ÚÄ>JY !«­ ÿK_GQ…O±ªj«­•ùI¢†ççæ­_·öµ`=t 8üùC`!{J¨Óø-11ÉÎÞkºý#W Û¹³§‡g– œìô¤ÊJ‰£–¦úå#F ·TT?biÆt#262¸ÞÒJsS£§¶Ð"áÁ®TQœ@³'UÕðS|ÿ°§äøp˜Xõ0Ÿ ©(;QˆFù»’Ÿ·;ò"!ËÎyôªjƒ3üLÖ‹Hœ}†Ìòdòu¾ã3@ÔePcŠ#•G[R€­:™é¨Pjˆb}­ÉT Ù~_qRŽßÏ•w/üˆLY.¥e³i0ò’åšU0ŒM È TS=ubö¯>Ó;4A–TS{¬­¡vË@Oû0’¢«ý}«Flñ¾pþüÛzý#ÂñGÞ Í!‹µUTUUµ™y<äó¯ó{§N™ª‹Â¦_E6æpÔ­˜³û¤Ác}{÷~ ®ªrËÊÒâËÀÀ€ñàeDF„¯°03zâdoFIq4~D!Í_Š„  r"ˆ~ÁT ð²æ6æÆ¤ÍTvä¸éhÅó 4ëH>.sppÈKNN6]¸p¡ÄŠËÁË“ÝÉÜ%¦uKôïIy¡Vî¢Kªr`³rE"ê@¸k6›¸Ohø€$Ý –,S_$?‚ÙÃ\›¢De ZBàäD¦ƒ™>™€©«ªØ¦£©yýĶ{yz”yº»ºæç)¯Z¹¢ïÍ7ÞHŸò•ÉH9è{{Ù²ewЗ(à•H|1‡¤ÑJgê=~AÞã[}]Ïœœœ†–—•ù Œ-RÜ?„×?(1!~¦¡î3s=„|‘·°÷JùY¡v| ¹G÷n\cñõ·áWœ‚–X A¬=ÃaÇŽƒÐ¶æô&ûÌ9ÈÓÃckA pfÑ$¡Eº{f̰€à£>\v+ìXÐäåQ°¥ _…] ÀPG0Ÿ4548yÒ¤F›¾¦êgkËa)‰þ[¶l–øþû¿VÿÑ?É›uH6¯Px¿‘+edgEe…ô›uÿùlóóòôÑÜ9*:2Òpò¤IƒAûþ— ,îã´uË鈈ðåjjª™eË€à¶>]Ú› <ÙÄ Ö#ñ6N³˜®Ú¨éƒë?€Æ *@쮚yaaa •V€ÆýÝG\¸$t@D×<ç‚,nÕ#P9 ®ÝÄ'ø’(.â: ¡T” M%QW.,®ŽdÿÌgZZš· Ïh¨«>BO®Ç>ÞžÓ=òRr*ÿygßw Ýïܾs> I¿téR‡ûk;£¹5HkÆèž¨‹Ö’ÑÑÑŠyyy2‹/æŠÄ~X½±z¢™\7Žù3C˜…ø_gŽ¿‡sïrð«¯>Ù´ió 3fÊ8)ñg®õÝ!ø!0•Öã.~ïðì; ô8gŠ8ÿ¶Gˆú}Sj`zzºyddd"¨-Ÿ¢^û,B¯¹Ë!/Æ -ÁµØ\mÇ5\ÏÕŠêpèUP7¡‚ÈWêieœm‰\Ïò#Ž{í‡V µ•• Ù³gOß’’b—óçšïÙ½K<åeâ–5L‚£H R|BbÌË<öÿu,w7UÞB›šk’Èè‚aû Ö)˜+‡KHì“SPü=v7#«»”îù¸cN¶°´hÁ» -ñÓ@‘†0º€Ì§Ì‰fZhk™èîæ6ÞØÈh1î¸Ûåe¤¢ºoifzºÎŸ¹&hÔN0— ¬µú…ï>ÊúfÓ LTI|·““³Ó0yyùÍ ê ÙiTïµqÓAKÀ—x¡!¸›M&&@âýà;Á€yä*2¥¬p'rF•¾n»Ù¤©Iª\d„÷0h ôõ¾ñpw÷‹ŠŠÔY¶li|·Øø3?ìŸù @²þöí;”‘™]‡†nºñ<¦ÀÎÞþ[É!R¹MlÑÁ’\ÉÇõÉhg3]9PÌ( ›\µÆµÎœ æ¾IlÇ Ó |¨¸kmeµ&Æm.‹EËOú¤ëûÔó£ÿG½º¼G>îÔ¦&;èZbx`á¨áÍF'Žïõi ÿkßÐxîøß±YÊÆè¶QŒÌuµµµõz¦Î£îûY{‘®‚†xŽ4 ì¶<¸.iR@Gzv¨YðõôôÚ‰ÌM¹ìÖžæOMË¢)ÊI‡L´U„†ª†.khuðÄôõtZ²x‰æŸù}ÅŸù‹;€át”ÍRqIé ô­êPz5L-G'çû–6ˆÙ;’ȞФ¹ÕÊʺÕÌÂÅ6fd‚»¦éé⎊2S®)WSU røî+‡²W.=å¾OœkB½ø€¢ùÚGvnoâÜwqô‡2ÒPÚ__YZY_]Y0{úTýß»]uw#X¶þWÏAÚ ÐÞHŸÍ¡á`¿ˆ8q›QN„c-b¤ê"ø\Y °F004h/$2· s3sÂÐê iDM1­ž;Ž2,ÈÕH5ë¨kG¹ªL,#C}8äj¤®¡¨ +ITùÅ ÄþÅïý_Öû@_¨@žænY[úe÷‡l675%Ø;:‘­£+©ki·2ÂÚÚêr\dÈm_WkòFª— ¹X›³5ù¸Ý X΀ÛÜÜÜ`]ÙÙµŠì쉗…¥Xm¡ö™#?œ©þýh?B'¿>ê&”†ª©¨¬Ÿ3e\ìŠ3jŠ ¢GÖV,˜>¥×ÿ<¿nÝz¯¦¶f°‡‡‡F.4ánÇü•›@¿ÿa{'uÖ`mâJ:®ø4kä"XK°PëàŽo¦ &0•L3Ý„H.s¶MC]ý)¢NmȽPv¸,£Doò2S$=¥!\00¹R 7 è„:Y˜™¯«©ª!ÅÿþÆ@‘$¼ßÔØtÅýF/Až¥kVvÖx;Däèr?<"|¶HdsY`̆èÕ&1°©Ë&[yŠp·¡ºŒ –±aÒ^+W®0rÔ9Lrpss½iŽ*@##ctÖ°AãcÔtÈ æMW”„A ƒZqª€s¤í¢,-u¸¬¸ÐpæÈúÌ•ógê4å%Žª*ËùŸ­8áh+XÛXÍFËœ;pÌŸr—ôεPMskö#¤`"r”‰µûz¹Z"7"²4!G0­QtÅ¥¹ øDOtut®!¼¼*(0°}~¯:;;R´—9…¹"Q¨@²ƒH ZµçÑàªF.¢BÇuä[4µÒÑÖü-6&:ço ñWñ€·¯zûi8¥¿Í;¯òú­vÄΠ_ü¨/ùÎ3ëê®Û$ФX²´´è’„ Ï…^·#Ï0°/2R—¢Ø@×e«×¬„z‰Añ ñÖnîn3äd¥Ÿé‚K„GD|Ô„ö2lÿ÷DØT™l;ÔwØË!G^òR¤#1èW3Ó”Õ3ÇÉÍU—¿mÕbÙªô¤Ä‰#‡{p_Ù¸Ø8'D…†Ã ¿Š¤]+òßrýû¶g­¹œô 0rxø0—¬¡Å¸”ÖÅOè`b ©ò«‰žæ93ã•îUh~m½?“‘Ýÿâ·ÙÚX·êÂÇIp4QhRîµ¥ŠR_6±X‹°Y¥ŽBÌãb€0hD¶ÖÇ¿:xði¿Žø½ÄÇ|¾ȃtGuÄÎ;©¥eäõ%K–ZtD£c4©6E8÷’H„ª=»GC³Bf:sbuDxØ#uüøZè%¥ƒ%#ÐÁ•À%ꃮçýúõ¹‰Þ³;Ñ=c'ú-]Féç.ùT‡#Ë}r¹ï·»aÓG}ª¬ñ7d·°\À˜u•B–RƒÛ̵57ܸ~ý£mKfiNZ–°hÎLéoÏ JŠ ûS¸ À7…æÖ5qâ¦f||E4PPWV‡ÓÜ KøJÖ–Ö„ä `:éëéµêhª?ã]y´uv´¡‡Â¢>˜!ò¿‚ qn®.?q—.à’äïÁâö?*Ì - Qû'€p2{ô¤±¡ÞS,¼ó  óô³ZWRRò4 ðë1cÆZ¼ìS@ˆ´þÄc[¡δÚTj)¥]ò°F̪š‡™ÀI7€aúÐÆÚ⺆ºÒSy.âQ’ƒ]ÞÎFÅh.¡ñ3'ðúÂü1Iϵݎ2’ä ÍáŠç®èNâ†Qt:ÔÐï‘‘¡ÁSS“ÍzêʧÌô¾25485¸¿gœ‹à \{ÎîŸÕNôC‘w0114ÓÕMMLá<=P®áÿŸ£c(>ùyù¶Ö–;¼Ü]Ïš9ýßF›PÌÕ­ª¢"ßÐ@ï!jº‘)ëçÏä•Ñ‘Q@k× 6¯Xƒ í(¢®‹ÎNv«—.Y<àeÿ>âãý‡@” *ÑAž¢+ú¶›6É@»¼”®÷…þkä0Èþ®(ì–FkEXQC’=%y’6: ªÁÄP‚`£ýΩ„øØàÜœ´I(õ¼gd¤‡p©\{¿YøÂl ÜõÑj_0©žƒÃàpS”!˜l¼Ü°Ì zó8LeêüÞÿ£¾K÷îJRû ”pä„È£Mfp¬Ù¹673´"jmf¦fß»ººl Î th1Bê³Ï¶ °Îœ=Û T‰C_ü„éý'a赞!Ák!ümò0ݸƒ¼"LFE\7k"Å ü@Q‡Obb¤w-+#ýµš=øŸ®÷­ø;sˆfÍšeA8ª¨¨òX]Kïoßµ`º6•••:}þùÎ(žúÃþ 'ÝfΜŽ$W+¦´*ýp'6!=P¯½í¨:Ù&—‡S]‚ˆp‡F„‡ûÛª)+/Ú¸qc¯ºÚÚñ––¹Õ?úx’k×»,B¼VCµk ˜Sî0o<ásxâ³ ˜+N°é=Q¤äÊ­.{÷èö!uGf»;"S<LÃ]¸)›Ml.AË Ìl܆<ÅC´é¹3j‡·§W=êÆ=öìÞ=ÍØþ£ðÿ^a€ÖI´´0»¯ŒŽ‹Ìõânñ\êªÆ~ˆÖheúO>ˆ¢ZêÊmŽövË8(ùþÞÍ~YïC4«f깸zŒ–WTýEOI-#c  ±¾îïç»¶¼¬4÷«¯þ¡&eGé—žžº E?`Ó:ÂFw$3PÍÙæÖÑT¡ü8/Z1¹ŠFå òÏÆS9åêä4Çö펙ñáõÕ„Œ4‡q»¡£  |[ä> wËCA𼆄Ì˃¡ª@¾ÐJ>x® ^U?4'Á¤#E€‹Í*NàqÒŽµÚˆ>6ÒÓºìëí¹0:*2;1>ÞtÜØ1’gÏœéæ¹¹96Ö–×ÙdäfuÊè°Â=‚ÙÄü8ë õrd‹“0ÿÔTî•ýá©Y/KNÄÇÁ@@,ÌÌŸp¢®½+ î°$;[ÑŒ4Ûœ““€A8ºgÏžý>Ì{MM:Q:'ç‚’‹¢++4}O(ÉØÐðŽ‡» ‰¬Ém85ÑD¡½ ®Ù˜hѬE´hd›’ì L&êÞ•ä¥$ž…y;>Mó3£HIôÃÔ¥nCÐû’ƒÔ`r8¼Xc`yÀ¬ò|àÔz\ÂH|°|00]Ì1CÃeT°ôp uõKȈo±³³yhfnzÇÕÍ¥nÃúu›m?lX“2ÂÜG¹–ƒÛép‡y 7ÁVU‘@òÂao{MøF614XÑX_ßay+1þ@»ŸÁ ÀuOOM]›ü™Š²ÊSÖ$ vRùñy4çWÌÛ8‘3ȫŚM˜¬ô£žb>ÄSD€.H‘üÝÀ¯ Nº3ápt aïFIèûÞC¥]g8æ(3Ì™FæP>"?°uù 8Jf4­¥š=Q k£N’0‘4à˜»¢“º7á¯$KAÐ~0§¼aVùA¨|`Vù>X~ø{fnãï¬U¬*MPÖu±ŒõoÍŒ‹-€‡äædg¦¦¥ú£Kä_úGjÕª•ý\œ×kpƒ5€£ x@T Bå‡y¹\¶ îЂJCh_µB@Bû£ß+~ÿŸÜäC:—W”% ßë'gÇþk‘‘»Çgàgeiµ‘›oŽˆä´1PLŒM‘!ÖÆ€1K•ýÎŽóدÏ‡Ý ƒh²íƒ®}{õ…³ z74Ah‰>øÛ ¼W³3ìõÉ\ãÒÐÎRaÖ¹SÇÒ„¦b ©‘†4Ybê#¦T0 ?&5…a2S8/59 Åc0º¬Gàoá*²Š?'H[<s˜dú’ƒž¸鯯ÏÉŽüù§Ÿ„‘ ·øá£S'OJ._ºXeæ´É“ÆQ_úéBŽ»¿¸zÅ2©3§Ouc ù'·õÿüصk×ÞÏHO-C4 攊`b ë9H^˜ZjÏs"í¹„û'ú:Ú_Í=GœyÙ?ο:ðN‘Q‘èÖ}ü¡ëBÚ`^AÆ»xúŒéލoPFÃ04$4¤½%0»nâ=­¬M´µuÊf| ±}„ûÚrÖ-e¡QÆsEUqWÃè1µ½±ú‘&B»Úà/é K‚táØ¢WÕ˜Æ Š÷³"se²R$?˜RAС@ƒ‹Á‰é‘Êxσ”Û_@È8Ú%ó6ñè„ä¡ šPÛkªž>nìËçÏwâqÔ·nÝTž7{j}}UÉö”¸¨c!¾ž§¼\ìNFûJóßéf³+7)fÜê•Ë^Êø²ÿ¹ïsç̶™õ”‰‰˜%„†Ê¸Ή¨B‹¨ Ùõ¡^Ö ÏA° ýç“@ÿ°7¾úß!à/ã;9üÉ”©S kª«ë­--ñf–f-¬,¾pqs9¼"6>6+;7[#Ócvsu;†ŠÎ¯Ðä©6î„Z g«Â–椙.lf]˜¦ cØÀ‘(vXœ³°Ã²çÄ"8ΰÁÐ~Æ‘³àX.h©¬1¸Mòd¦&CöÐaJRÐÒÍÇ;RX¬Eä 9T L“P¬0„Y˰ón ía4xÀ³HGa€ÀÑyÁ¬)ÑÙña=-´Éׯˆâƒ½Éͨm,(m‚T$úB¯÷ÉB¡/E¹[Ý™;s’Ã;?üá(Þ¿û]@>쎀ÀIKdâ5X‹ ¢Å¹eh<Áaf/;ìúe3«=£®‰¤*'VÑñ;D(+)S{¿¿ø¿c8÷1yâõìŒ4;TØ}¸ÿÀþ®-£ZdÂ" ' M0\ÓÀàÅñ4Ô›»~ù²^¬=–ÏŸQà`€Hƃe‚eŽñ¾Ž$/ÌÑÆŒìLt‘kéBw"ÉߣHg³ëc«í_öO™2) ..æGs3Sa0`f ¦´4 ƒ¦=7ÂûóhœtM„Ç·© Çãdiòõ鯼¶ö_öž½Ç>l¸FVf–Ó¿šñ@ ŠŒŒø>''›&%!I0ᨸ¨ˆ&(ÇŽ}MȰG\Ѹsçš0a¼°„ì2Ú\;zèZylô,_h~?€ƒ#VþÁ‚àø?_>¸ÛºCÙB !JE†XFíb­áÀ¾O+ÓS}n\»öÎåÃ…³&´F3c Í1ÁLˆ4ˆ·›=áÀ¼C'´µ6PkI÷NîaküˆF§—ùã<}ø°Ûî; ËJŠO¸ C:šJ¾wR¢oí¦û" ’Ѭvî„Í,[]¥'“škí(þ÷ìfªûùùÜÏÌH' z!Ôq£.‹ŒVZ´h!ñï¾úŠ.^¼€u‘vïÞE3fL'$Ï@×S­ôá´[Y˜¯u㽫W.w_4u²ë„šªÄ²„¸šTw· IövË"ÍL¶†òÑÑ<#R’»j-/{ÇRVꆅœô7öj*ìÔU6x››L÷óÉ»zåŠ@ ÁitÚ·çóü_–ni¤O0«¼œcŒA0'7; ²mÝu)žh§ã,²h-ÌÉœ¾cÛ–—bbÝýö›÷5ݘ–üåüà€ïF¥¦ü _„ÐEû"/Òî "Qû‡i§Ãs݈¢tA6šçn¹xËêå½ßñŸ"L0c/O÷gÍM„>L„²^aÞN ÐСõ4jäHA[Œ;†**Ê(0Àk 9dHacY)©[ðkþy7Ñžè½{wï¾ëÆõ.GöïÿdÍÂ…ƒæ§P‘‘¦ãíi•èçžäãéãîª7µe„ÜÒ9³ûŸÿæ\×ÿGáóß~ã_QœKáèsàé,øvVŠ͆×bB¼)Ø ƒ61fÍf˜–²â/S'Mz)ÅJ7¾ø¼ß®âüŠy΢'™´ƒi7 &]^Xyº» ¾ס·„kNDr# ­ÙNC‰€£Qœ¯HRÝõ)ÁÍtç¢i¿¿ùµXJ_á¬[·ÖÅÛ˃fÍšA(È´û!ëׯ£øø8BÑ!IFö螎QFKÀv7@ÁÔï'Ê Š+ÊË-þê\ïÿk .]¼èRRTØæ¶¤ª©®"{;[¡{‡.ìm-4Q† ÍwJ¦|î? ÕÔØx!Æës‰nGœëñ¯(®Xº¸2,$¸Í Ã&õ ]œ‡ ¦LÙ[›£«búë: L-K˜Y:¸kË ø—òÛÅ œ«,©Üè"j› 0Œ@Æ`rÕ0}  MP¹ê•É#àfõò¬D}ôò5Ò¡R_…œÍÔ)ÄI’¼P²`LyÆThBþüh³}þ°aþ¢øßk¾àe%¸»»BƒÌ$t7L+6¥Ð™w ÎÍ‘6¸&Bù`4fìÀáǵÜ\pÄ*ÉB^îî.‚‰Åíú9–Ïq}ž“§‹"fÎý@™—“–Ù2aüø+š7gV|uyÉ=HÀ ‰Æ|>o ù ö'oWÔÍÛ™’£¥9˜c̲÷iÉÉÿ+S0t· ãÇn+ËLÿ~KfòÓqt/)’nFÒ¥@7: pð:à`Eˬ-h*¨,“1H3¢W5Ð • å—#|+€á9@Já‹0@8eX%jŠ”‡¤a6¿È“TÂT­HjÑ›kŒÕ×ç÷î‡x;@–;ägÏžàëë}Ã¥µaH€µç>\xš²¿íóÉÕÓÊM‘gP³W4÷Aè%-1äTb|B‡õ¡ýêà~™Ñ-ÃwåeaÔ³û­Rƒ·iÁÔrÙÂQw&âZ™¢Ü¬ŒÊÅ~(÷u·µ~lõÊÁ¼a\›>}úT«êªÊÕ‰ñ±­aèÎH‰~t*Ô‡îb”ô Þ¹ì%¢3èyÒÙŠöÛ[Ò+ÄÌ&#œ;áÝfLƒªe'``€”¡¤¶fU 4¤ ©Äÿ«ð¼ +FÔ/ ç –A®¢Â¯ ßLp°[¸®¶úwûGòƒ‹úÇvÅ.‘‘a§MŒ ÉÃà …â$D[pŠ9ÿ̰É‚£*¼,ú\I ^dð€×]œ;lF"¸_¿vKS}͑҂üƒ¨•Ÿokmõ“1´›ò3\l„‰Mʈ®ÉI¢)6‚zõ"WŸ¶”4œ9}ºËèÑ£|²³2NÇDG> 0B( ä "¦úyÐÍ 7ºéëH—1Fú´« @öÁÄZÂÙùÉȯŒ8ša^ÕÀIço¹e¬9Êðÿ è%UЕ* mª +õµ¾-µ¶ZUîæž0#1ÑðÜνî¾#óþ˜„½áïæph\\ìfS䬭-ÉÇC)Ph „UÛÍ,„z¡E^8êr#…RÜ~½û<@Ý{ÊÏW.¿~ÛÖOö4Ö)­OK²ü|äðøƒsg]=xð¥ð¡À@þ«ËæY3]S¢#¿·D;S `t4f†Ëmx8x f°,(ófx}¶¿×ÙÚò’õ˜±þkxpøùbÀ§aâyV¬m‚S~ Úã’‡-v³¡¯abís° ÅÈÔO7Õ¡)FÚÔ`4 çQ­ÍæU»RmQŽèƒ¤NzƒÃP÷äP‹)Ó⢢·MotóÛozþü»«¼á¢öæž~vNÖKK32ãÓJÐ ËÔA]K¨Úp<¹ÄôE¨—ý>Ÿô~f¡¥½z~H@ÍLGÑâÙ6‡f9Ú^YšžrKs㣣‹?múáôé—2•ü°Î›ê—¥ûz?Áü1€£®Šóa>÷ß•”À¤'žöM¢5ÒÚ´-*ЇüÞéíèëåI¾^¿}yR™— ]€ö¸€ç#Ð 0ûpB¼3Lth*gС5êá{Tᑵ…,‡°Ô”Új,LO-®(³8½oo·_þ©C˜o®„½ág^ZZ\ë`oûÔ\'k˜"‘™š!+¬µ6×T Ñ,˜YHöCäÑÛŠGôëÓKúQ=#HÏXÜygÆDÑÒÒbÚ;}êÃKKï²vÕ˜Ö-›ÒÚva@ç¿ýCáÍÇu‚öøNv¿íE‡Gy{»½ÐlZ• ‚ÈîKÃ俣TWÔyG”O_‰·Þ` øxºÃ©w#/øV>`øZóbùãxË<蜻-tÑag:hgF«Ì h–‰6Mÿj(4F-´û섳yÅѬ2¼V¤ªð¬Zd½tis“¸Rð ÇÁÿyú(˜JÝä·5…ùb ž“T}ðŒx =WÇ ò!²B;;êœ1@C· ³*€¤vûX_šA[G £S Ìß]0¯õÉ’O´­^q¦mͪ©OìÓj½w÷?–Änݺ¹ÿøqcR&Mœ0qô¨‘KæädÞÚ JI•Y¸C„!\K&„V•RiQad¡·-U”•ÓŒ’bº€| ]ñs¦«>tÕ׉V8 Ôt˜ýÈA'sÚÓj7²ë_ùZц8;*N‰%t}¡Ÿï¤ÒÒRŠ%4:bkeI³A§œs¬ÈvL±]a¦KS‘Ao€ß‘ƒÀD*¤Yðoò”dŸ ôOسn]‡ô äk¸_¹yЬ¦¬­Ã3½¨1É‘ê T5±"ªˆ²¦âpsÊ 6Š~ ‰žã¢M¡vj`¨@±ê¸» ¹hü– äÃF/Á]·Îíh8ú“P»,"„AwÖ®¡§Gëú5ôpî,ú ù¥0›îŇÐÏažtÇÏ‘n@+MЦ™£G À(/+£ªÊJjjh¤Ñé´?:”®¸Ñ÷þ®tÝÏ•nÀißãaOå¦út9£.‰%íw±¥]¾Ô@˜LE˜§B ÕÐgÛ·Sm~&ÍÌ¡ñö”âhHËàô€)¶þÇgö6´Þ\– Âz5®) àˆEƒ €äY‰™ñšïÏŸïòþŒâSê¨8â«Oæ/X6<Óó?©6NDÕ16TiIÅa( 1¡TÄ¥ ž!³A¢´)ÂT™Ñ÷*f$á•îîlfÕ€Þ‚î#ã’¹ž.ôX¹çÍ¡_W­ gK?¥ûS'ѽæ¡ôKQÝK£{ÞôS€3ÝFVûl¸?Í­­¤DT &ÅÅ+62œê:€:ô+Ç÷É-¬ëÁ^tÈÏæ aÃQ/;:æfEGÏØŸb“‡ÕF„Q"j)GÆO oΞ¦«ë'Ðñþô°Þö'›Ó:„u÷Ø™Ó'+ø4Ö´ÒT—ÀÿËì]ø‰kG`|u²’üwCý|ÄI:J_×ãÞ½s«Ë¢1•c†gúPc¢“*¤< É1¥\˜XéN(2S£8ÜUÁNF)fG6xGùÑ,0Yá¬W#Ö'x 4ÈT'­ˆ¡££GÒ­%ŸÒÃùsèÞø1t»¾†~DAÔ/ áôK¤/Ýíã''º Á_S˜‡,"Nˆ2yî.4æÚ¹@ºæ`@{\C·“ïá‹|ãeÍaM§‘ì;)Ä‚xYÐjøâ#©Q¬TŒthÌŒ£ë+GÝ>F´ £=éñPú:Æ šÃ„¾Àñ Ë@Pœ €Dr°˜»B"œì?h`kª‰iѾ-[>z]GñyuÐüòóN›>ê0©2ñtS’k[$¶ e¦Tá¤F“ ÁÉD?«€# í~$IòøÀÁ‹)ßÌzse´­M€ ³0*œöÔ×Òåóèþ§ èÞä‰t£ª‚n÷"?ºìN?C‹Üòw¦]Ùéíz‘2ûn¨xt#Ý }èI×ѨáÄÇ™¾G4ërÎÆÎ g½­érª#© §Æ¨PJ‰ŠÂ,g„y]isa=ãOtl9ÑCD ÒˆFºÑÝrÂMà3€c«5-@&‚ü8 þT>œó@Ì%ô—–93§±Q¢ƒ~ñaß„8¹jeÖ¦šŠ‡Ó#üi´»%E4g,JM‡¡Ð§ ´í"IG¢0ü§T¬4óÀô-)ùG„JSÁ_B¸w$@2 ´™A´-ƒNNŸF?/[BЩñz] ]ÍÏ¡[ð9îFùÓ¯¡žôk« EÎ$FR$¢WÎhLÇààlêÐO¢8ê‡`€È…n@{\ƒßq¦Õp©ÎG:Òç)Ž4ª …¾Üý@›VAíÚ[[Óˆ(˜eµDÓ"‰N­%ºø9ÑÂ4ú¥Ùç;ÓmYÓ§|Þo×ñ 50^ýû?J³µ-{~Cñ9và\Ù±=yÿÄ ÷—¢1ÚtÐß§¢´u ‹ èíT¢µM>VœV^©¨ a°ä!ÄZŒ,{%ê58ËÌ$>¦bÔâ.<]G %Ïx$êÖ–ÑÁ ãèÆŠet¦Öm˜\—s³èjr<ýD¿†yÓ¯Á¸£ÃǸH™á¡@œ0íŠóñ¶0ŸÀÀý!Ènƒ¢Î,Üë@ìé[PEDºQÀ“™™I+W­¤è‰ÆÝ-âíåMÐKÒ¼è1Aó¡=n¥GÖÒܲ*HŒB¤Î—æ#еiB/ÝJø)¸>/IÉ+YNÎâò”½7âÐWvîðÙ=zÔ¹È,OCýôD$ G‚óÔ€J¸*´Ù,E(UÐ"ð=2¡9’ÙIH²À‡Ê/ªwÜj¤ܬ´ç©Åc½à pÃeš—G;G4Ó%äÖò¥ô œöKÅt ÷kÈß ó£{!^ùzÓõ0_ªCô‹ÇÀ9sRÏÞ––Ýdàm71@~@¨÷ò W<íè óehÞP‘–šJÅÅÅôÕ¡¯h2øMYq4'Ý—F†ÙR8’ŸçyÒ£fwº¶¸‰®^ú–0N›²²Qß €ÀB-ò:…` Dø¢ªºxí¬YâNˆo„wàI^Ùµ«ÏWsf—-‹‹}8ÍÆ’Æ1@ÀyjY±™ô 6³à{äéF2(æ©`Ó¦ËÈR&Ì­Rh&õ1óu(¼ÕaÕ£uP#ÞfþÆÃé^[UNg/¢ïÍú @ù®¡–Î'DÑŘpº@?$7°®…úÒ¤rs@t‚yåÆñJ„b€r¹€\@. §]­h%ºŸ4 Î£<#ƒRRRiÁÜ9tí›#t{VáJ—‹à´‡ÙQ ̱ý…Þ˜”•„~` èÒ¥K„Q”Ÿ€Äd0¥`®z)ª³pBÝK‘¯oÂ÷.üá.)øS‰ýªv tÏ~öYÔææ¦=Óý}Œ11â:jjDýE•¢åùr²m™r2Ïò4Ôn•è¨w°ßâçSQab¼°V_ût£¶ê£fôjFÂp(k]”¬"ªÕ Ž×´˜H:8c}·n-ý êÉÓ&ÓÙ”x:B‘³¸†ïwatå´K|=Țæ•ÈÆ ÝM‡ÿFÝ d $Q¬H'Zƒðo ¢2@MiFƽ(-…O¤ÖcˈN¯!š%D¬ÎäYSj€;…‚‡‡„aZjmÞ¼=‡wƒùëE–&¨o×7¦,hË8D¯Bde¾ßµaƒØ9Uù:~ï/?þøÁ‘;V·´äÍÏÍYÞâíýe¹¹ù©B=½Óµöö»FÍS?5%ÙmuË…Ë'Otyðë¯ö.[úÉì”DÝ™a¥“Üv 9Ý 4¡on¨+#¶Ý –ïù5«é—íÛèÞò%ÚL ç A. Ï¥Pº€löq'?ø "h3šEÛ`nb†æœ‡¹'ývÌ«\7n --¡Ø˜X”Ó£-¨=ÍH‰ Ç#}ˆ–äÝDÄ곑Ô6•~©²£¥).èó ¼ÅÇÅSnN.:¤$…‘9™šQ4æ<æAK†Ø–na±ô‡ë×ÅÚãuÔ×åœPÑùÖ߸|¥û¯˜ø{Îë—;w>Ü>i‚Ù”`¿ŠáÖf;›Lõ¿kÔUÒd¤K³3Ré0ZþŒHÓ£Ûé˜Y§â£P#ÞÖEôÜý&Øöø{SÌ«±¶° hÌæ8%É“ÆÚQ2ëëÖ¯§M›7 ‚Î4’`_?*öB®¤1ŒhŒ7Ñ—S‰®$š—L­ÉÅ"5G{‚ÊŽˆ0áœÈÌÈ”l L(ÑÀ€ÆæŸ çt¸€‘„îj=0®¶„Ñ‚ ?ʇK ¥¹ÆèÐînbNžúFT Vð4TŽƒß” +JNöbe` xÆùŸúåÅúS;ðëO?}¸$'³lnjÒµo6m¤_Ž¡7¬¥c•et"2y?:‹õ-ò‡}©Ãj¬ÑÕÚ ª1ÎóßW"»®fta©II¦D­† FÇí§3ËÆÐãD©êèË4k:^Lm£>^RN7/Ÿ£¹˜mg<Í)Ì0*[ iDùîlhE$ÌÁ ¿)ˆx%Åí ‡5 ÝQÄÿÄ;ð·íÀOõXYS¹{ÖÌ;7¤_¿ÜCgÇŽ¦#hD}úiôÀ:ìKG1¿`eÁÚ17Ã#šbmJ»‚|hBX­@ô+Žvaf]ÿl6µ]Ú.hŒ¶„‚Ëm諺ÑJKËhˆŠ˜-Oy`³¿â DO[›*1¿c>ˆ‰‹Ð z.£cá3e!„¤¢4ñÈöíÖïoÛpñ½™;phÃÏs»v¸‡qo×@‡ÿ*+#Ax|«³žt2À‹–¢†œÍ+Ks ²4Cc DÕÊSùÊÝžV¥'Ñ&OM©qCi2äáD‡æA–|nµ6;Ó ‘¬Í ¤œäDª­­%Œ¡£¥K— !]OoÛšrŽžÓj>ò5óà#MF@!äÄj7×ü‡4ÚáÍüÅÄgý·îÀµ >üîÔ)¯[‡Ýû>Ä‘ê :ˆQÇÓ $žDF}3º’¸¡‹‰%œs T<š¡,8IÌ} 'BíÈ"ø.Ÿ¢3É¡¢Pjå `$]†98‡hœ/=mr¢ãyöT—FéðOf̘A“&LDÃ9W²6·"søñ::4¥ÆóP¿2 ™ @6B¼µžîE>G°þV©ÙÿÚ«‡½½÷Ë'ggL¥½iI´/È›Ž ü\hr!áÈX¢%‘šÛ™¡¡r*ÛPóñ ÚõìolJ\4Íò@°6˜ZG$«Êˆ®î¢‡›GÒWõ É'†P,:)FED‚é'ÃÊԂ̹ÒÓÒ!/ŒTÍ4ÇÔPÉdämòàƒT»»Õ<øí71@Ä2ûjwàêñãæW>ßùí…¥‹iE íB4ëK˜X»ÀØ] €d£,Ö}ºÌ¸ë â ^…Zôóà`} ïüÄXªE½ùÐPÐOÁÖHO?Kߟ>HG G;ÕxtŒ C]»'†ì ŸbhŒ)Uð?ÐÁ^]ƒîí4 ‰ÌEh4,¥&ÃP¿éâÉ“¿+¬ýjwPüíoõ<@ß«+”[¶¤mÿ°&ÚF[ƒ|i£¯;-CÓ·°ymp‡7…a€XD9ÎʈNxÚÓ^Mv¶§Ü„XrÁ‡á1Þt§)€–W$Ò† èСCÂHF‚$b°•Å ·è3:šèû¥AVH2¬ãOáß|ŠHV-¸eiúº£Îûú÷zóÅ÷fìÀOß}§pzåŠk_ fd="SChüehù3G\MM€°13Ð¥"òg[ZÊH<Óá(¦–3šÞÍ΢¢ÔDª©©¡ýû÷Óš5k(%9u%®#mM‘à— º bêji’RibLSÐôn!úz-Óˆ"°< ³‰—Ξ—×¾"ôöŸå¥ÏwV|9vôÃÕîUÑá´T“Õh´ÒÉŽÂÑq…#X줛Á¡À€Ÿ ¡¤a:–ÈÖFÈ•øûùQ,¹nLX½)É)T_WOÛ‡A» ŸbfI&fTD ü=€Ct~¬,„'¡Ÿ¦ÚÂÌÒ§ñ \Í;½ÿêùöÿJâ+|e;ðÓ¹³†6müf º ¬‚_±&ÖŒCÛŠ®#™ˆd™³£ŽeØ¢«{È‹L`Ù‚¯…e ÄÆÆ7*½NOM§ðÐpr°µ8ÐR«L}=šÆZHÀà ¾ ’8CäW¬iF]ϱ0DËm*××Ýpxóæ~¯lCÄ_,ÞÞ§|òó‰ã L›Bëø[k#ê?>C5aµ­íóŒ:²ê¦p²uѳK_Œƒ€¸ èñˆTyÃBCÉËý¯LÍÉÝØ”J‚y L.EB0ÙÔ” yÄ5²è¼¼á×ÌÄç§a²Ô,ÐêgÁÔf sxrjŠ’øWïÀk±m­­^»–~ Ѭ-(¤Z ²ÎÕž69Ù£aƒ9œrDãjˆùs‡"ž'd±qur¢0'W‡nŒ+1ê W¥Ã(#Cš1ºF®Á*Ãk¦0Ùôà‡ðH7G]mÂfBƒÌ²@à°ÒÓúi¤ŸwÂk±9â“ïï@ë“'Êßñù½Õå´ýx—¡aÃZýltBôE£i{L–2ÿGD þžÛÀwp@ì œšÇ~êÙ·¢aõ:€gʇ—Á,[Ê ¹ŽUxO Ö ¯è#‹ÎË ¦Öo.uÈl¾ýõÑo>GV}fy,AG’Eð'F£>Ý ¼,6£Ìq2…Fਖ9„Ý ‚m ØÃy¯@Ӆͨ-ÿÌÝ JЄ¯¯ƒƒ¿ Q°È hœµx>qD‰>´/#¬±øÛ ^³®¥Úónè®+º½$>‰w{ˆ¨óõ£G¾ØRQA³áC´ *•~º!Ðì_Ø€ú.hÄ ‘' »°àCTëÐCk |–Íxß^ÈÂaaD[ͱX[M h!@å ÀagD»˜ „Ì™ p0@f¢ÙÄdp³ªµÔo6{{û¾Û¿Œøê_›8sðàšÙÍMT‘“…®Š±Âø4OtdüÐÞÙ¬‚G ˜OæpÈ£º]‰*›Qq¸~Ä07BKl@”ëstJÜyçë yÖ #¿}¯šS(:¦À÷Ð@xÌC 2]é Ô;× ­Šà‡LGv½AS­µÂÒrÒk³Aâywwƒ;•—,^|«”ôÐ PÒ½0ÇÃYhÿÃué–¸Ãs.Ä”i'0±Œ•J5d|†-¸ëo²4LªuÆZ€g @´ÙX›¶ ÛüZ<_€¬@æã ðk¸‹½6uóŒç3püy g4Ï t†„³N•æŸÿpõ{1/ëÝÍ×ãÊïܹ£?oÞÜG™`ÞÀ—ð€ßÁ=±Ø9·A.„Í)6­úÈ…øÀï˜îî« 96 a:mx5ö•ðQ6àp¯@–bqkÑÙˆde ËÔДuaÔ\¸.†vB³ÌfpàX3iÐ £a¾eª¨|srï¾î¯Ç.‰ÏâÝDäçì¬,Á´rwu!;‘àœ[=ϦãNoa6Řêáð76 )ÝzŒÛheBë‘ÃX kL±5ìœC“¬Ï±šc4Ê ülPÚ§ '’àYÂìÒBçHVêâ©€o@2 æ¯)ð]FCKhk>½ÿ€8«þÎJækrá?ÿôSÿU«VÞ,ÈÏ@ü€Ú9 ž€e pèéÂ42 k]]ƒ~Y›@V\ ²¦Ñ˜Z«–ÕXkÁZ ­³ YÍ2aÝÑÌT€`¬¡åá˜xÔPç9ŒêdT€Â2ÓµÆÁ›„dáx8é ÚêÏ=ÜǼ&[$>wy0·½Û¦M›¾­7Ðß“ Ð8Ñ&oø!Ж¸ó#áÇG-–fhŠ èÿ»ÎÖ’Ö£Qöjd—ãyû²¢åøÛR„~ç(-LÞ? Ú¤Z(ZÈ œ,UôÀâ¥úh¯š¢ŽpÜ›‘eÍ1Íó2ñ7‘¦Æ‰ çΞë|—ôU_û“'O:oÛºuËÐÚò÷ô¤`˜A%É4kA¦Üsø sh’:€f º²¯qq ¨ñ±–cæ 'C»ÌHæqvÑ©G3¢[뢡5tÑAQާö¢]AFšÜ EýJ†¶I•% óÜÑjU }²t4µ¦nX¿þãW½Gâï‡wàñ½{ƒ/[zvIU%MCkè"ËÑln½½%M€ ;A¸M Ax™ ¥Hî-…æX €lòõ¤ hô°ÔÇ›"ò5šc ´Æd8Û“ÐowüFøÕ0£RXðQ󡆯Ûjè=¬¯¯OV¨}·aÑ4þ¿6ؾ:¨ï<“]E^þëРñ<ôwX>_ù¥?¾{·ÿ¡™3¾YŸžF«¡ >E#íå`ó®µ1¡±ð9ƒþ 0ƒò¡QÀ¿˜ƒî‹Ë0¡v)&M̈́昧~5þÇ|®Ñ«¡ð=j¡òÖ Dºœv[k²Æ2G×F#´Ò…_£…úm8ìü¨“KVêŠò¤(#}ÇÚÜRœ0|åRòŽŸÀþ%‹#—§$ÞZ Ÿ‰ÖbkZna@£]rfÍ?„Ab €¤ÀÜšÆü+t<™¿O5ÓÇT+¼×„šñz/€£À(Õ½>G F¸áó¶pþíaÂY¡ÊPíFõtÀîÅbp¼XZˆ”i¢q·w+ÊHµzº¹~Çñå¿ê8wäHÿ¥Cë—Lƒƒ=¾À˜G ˜aw¢'PÝ!ðñÐ(£ðÚ(h’QÈm´ Ž£¯Õ":U •Â/…IUg;ž¿#ÂÎS ;;;²±±ÂÚƒò_ &Ñ‚ ¦©¢ KŽ"BCg¼êýÿ;¾{¾üRudsÓ×UàUUAðDZêwà õfÅš!Ôe`HåàSUbI2ÞhÙ“0d›ŽB¨T$0(B ¬L˜sbª¨@ÚxÍV¶XìsèŸ᥇ÈE_Où;„“ IþІРi «}j`Àìwüç_þ«Þ½{÷jaô󙘠@Š›7¬ÛR8×Åèÿ"ÀÓ‘‚0ÄÃ9#!Üih¼¿"]S’a>…ÂoðÇ£+@`á¶RU#c<×Åsm5:L&5UU²†iÅåºì{è!äkcmv°4ˆŠ© A¡w@¶^ŸÔ =ÔeåÈyˆ%éêœ=º{wÿW½Gâï‡wàØ×_«777Œ#ÐÖíííÁò!;4¯öE!TRTV x¹"Ô«Gæð,a2qNÃôh=hý<êÀ¿@ :H‰ÜÑ] óNXè-à·0@L¸p ZÇ–©,x®‹†Ö M MR‡If¥¬Â}z)ƒg† Ü[\xòСÞáŸH|é¯r>¤ÚÐ0ôx8L»‚/Å®1ê?LÈÏËárÍ„AÀ¥³FðGŒÙ/a…x^Ð8üº!ƒB¯ƯŽp½:"SÆðUDˆ`qe!³y¹h'šÐ6*Jʤ -£Ðy"Ü›픩ªD±÷Fëéí›Öبø*÷HüÝïðìÞµK¹¦ºúhxhÆ@ HÜ™#Dk6‚Ð{úž”f¯¹ -pàu!ºÅ]O Æs“玼!œt&6¶D“´ LåeMa ó‹®tM´b_Cr˜iÚ‡6Þc‡ÏfAû¤Á<‹€£î!-ó¨08¸úþ‰Ä—þ*w`÷î]²•{ÃCžÙo.’b€0¬ÐÈ:( Pø?k‡ÿ ¡Ús%Ïb€p݇w6±˜¹ËÑP¬QTe‰<ІŽjk¨€´¨‚Gø* Óà$æH þ4ø5‘˜òë+5„BôôN?øUßW¹Oâï~GwàÖ­[,œ;7/->޼=<„Ñÿ ï æÓsp±‰…p®!ÓÞžmgmÃ2„®?EÀ0„ÿa†ì¸ L%{„{EÐB6H( ú¥#€ƒ—²ðœið:‰!>‰cd(±ð]·ÁTè{|ß>±/òŽÊé+½ì“_|î8½0¿µCrÁèuF5¡Q'+h3ÀxÆ€}çKL/ Õ"Lë‰A9ÁX±Èq$CƒdB𳡠¢a29" ÆTzcÔ|èâ5]€C[Ca¨ aó,Z( LÂÿcó’¦-=JJä_éF‰¿üÝÜ3;w®®(y6üªzpª*Áè-O*æU’fdŒ1F”À ¡Ùð¨ð÷$[ÊC”J4zhÆÌó Ìðò¤&´ âŒz ´J%ÀSŒ¤a œn7ø*L71C€ÁÁKGS KY¨S×eß晋¾6¥B+¥@[ÅÁ™÷•W wiÙ‡ùåïæ/$¾êWº§÷ìr_ß<´mzô6¡ÖcH‡LmoBk(è%CA&äU-R­ÒÊÈpŒ€°p ªGy¸Ñ´ …<ÊÐUšÀÉ ÔA T ¥ˆf¥#Jå Ó‰C½Vàkéi©ž&?bé aš!äë ’q¾;¯"éè%'OÑúú§ömÝ&΋¼RiyǾüæ¥K]wÌ›3v㘑439ŽAgºz „¼<«áÔáÔaÈ®7óBÕƒ`¬·'åA Ç›—™"?Ÿmë·Ÿ©Gƒ‡Z{„>æV$¹ ‹'é Ê%Úà ½²ð>€0>M ¨’ ’ð…ÁLóƒ?â« HñN—-Óàß19}e—{lÛVéãÆìZÕXOÓ’âh¨³õ(— Bb žZ@5ÎàÀã0€Àcèñã±& ž}´ÈHdÞ‡#J5 i@ñ™:€¤«~Gü7c9aYèi“=Ư€º‹b©Thœt€"ŸMÆJÂJDH8¦V4Þˆ°°§¢"9«¨[“‘eóÊ6LüÅïÖ|6}ªÛʦ¡·—ÓøØ(ªuq ¡È0LbpŒäb ç®7áywL¥èÅfÕX/ -2 ¹’p84¼·5éÕ\4A/Ã1ò‘cÉA"2ÿÏÆÊÀ¨éL/ fW&Ì·€2ì`.¤ŠÃc,| 4{ 有Íé¶ «mû¾øB¬EÞ-Q}5W»y⸚OËŠŸ,,È¡Ñ1‘Tšó:Ô›7Ê>IF(£ñ8¦Vž70 êD¤n„P7Ãi†çÍíÝPx_ÀQÏTµE ü™J?÷é-Á*Äó\#ŸÉ†ÖÉBÍz²ø)ø2Àç±xŒÆç¢ñùph˜ZöðEÌ$¥–¦¥ ¦^ÜèՈͻñ­7/\蹪ièñ9˜x;#=™†G„R5 ¦jѱd¨9À`“ŠÁN%ÃÑT¡TøFv=¼ þ¯j.Œ`êØ)¾êñȯÕ` ƒÐ×Càká×T`•b¡åhøX…høocFÙÐ<éø*ò$)ø[‚qøL 4I4€X"\®pöM¥dÈJMãPEnžö»ñK‰¯ò•ìÀám[l–­}<à›=ÈŸjá¤×¡ÓaZÖC‹4$ÍK3æx4Átj`aÇbPmQs‰W-ÀR Q‡Çz.šÂßë¤zhz¢B_nŒ•ˆ’•¡l·ØÖœŠðXˆ•¦YxLÇkixLÁ{–hÖ"Q 3&™7¢ZÖàné ‘yàì:ü•lœøKßþ¸xêT÷•'LYPSEcãid|,5øyS=„´©Amy5Vª ,Cªƒ)TË B[ Óª€©Åž5ÄPh–ÿZîþCŒzhˆ:|/¡ÒΊÊí,¨ÄÎ’Šs)ÀÊYbx¨eâoiˆ¥`´[Þmƒï‹‚O°ƒaìÐ9 ©#-OZÒò÷Êó -nݼ)î~òö‹ìß{…{Ö¯3_:nì™)%ÅÔNã"¨ ‰Â¡0wj-á3À©~¾ªˆ¼°`úÔþÓ€€ÕðOkès­Q­QÓ©Q¯*•½U¢cJªK°Šð¼+¯g#–‰e*B¤4‰Ð$ñk4>‰ —?²ûÞ‰3œvCÔHJ“¾ÑüÑMÃÅ“©þ^ñy»¿íäÁ7Ì»oÕÔ)mã ò¨.8ý©ÁÝQH 4H¥µ)UBÀ+qg¿¡¨‡2Hêð·<ò žU´ ¦ åêðÙhjhj}¥ƒU8XS™ƒ-•b8Ojà Ðô!ÿÏÁÄÜ,Ð[ i ƒ$+ÀŠFmJ$²ïÁÈÜûay(.Ð"ꚬAHCJöž½SÆÛý‹‰¯îoÛoOœè¾mé’ºµ³g=[:aÊÊ *ªÃ: ð¬±0и¡Â]Ž;w…<HÊ$x^ÅQ«: î›ÅŽ8û5h*W Ó©’Á QŽž¾¥ÐÅX%9‰Ñ?’B­LÔ¬g Tœ†•(QH’\Í,¦Ž'¼ó–ÊîßrYÇì×ݼdÉwkæÌ¦ÙÛilq1U£Ô¶YñtGd!. ¤ (…){F »€(„OQ€ÈT‹¹*Æ*Áë¬I(ü÷|ŽfÁ/)`rÔLÌ^O`ØIÐâá‹Ä@{° …I ˆóðCxÙ LaS$5I¶YijoØ´fM×·ï×_Q‡îÀ£‡;ïX·nêºE‹-œ8¦Ô×Ѱ¬LªŽ‰¦o/¦„¦jî"AÈÍa/…æ(cížG»/@ åóòc °ŜüÃû ë(Àg EÈq` à`@È‹ðÿ"Îðßþ‘±BÔz$ÀÈe% ¥¿i` 'ÃÄJÄßâÑó7Ú# z8€Aƒ ÞÐ"öHê‘H$Ä{ܸv]öíP‰zË~p÷nËEÓg<˜7y2MZOÃ0Qª26†ò|) RyŽ0up§.zŽ)†À—Àl*0(ÂX[”ÅH! 0'&ïÍþG?%&[„»…0¿ ð÷B€ˆW>€” ä˜¹Ð¹8\ôÎótÅy¡´H2’rc<Ž‹‰sa€B‹1@üÁùò™Ñ Pt%¥ÈXQy_QZ†¸§ï[&Ãv9ß_¾üñêE ÇLii¡QuuÔTTH•)É”‹Aið=RQð”Íö?„´¦S?xA˜ YðsÖ(Æ‹U Rpä1Ÿ É…ÉÁ£°ø9ò'¹È¥äá1/,€+ŸÍÅcŽ—Ã`±sð=9œ,„É•ëêH^n”ƒ>Á@ŽD˜Vñ™ Eàƒ„Á Âw Ã€ì½ØÂŽ`û²©¥-1促SU‡m¨øÀo×|¾i£Õ©S.YU“—K…ññ”…‰¶É(xJ„)“S&æU.À‘ !l*åcå±ÉÄ‹ïüX,ìy,äH6„TX ¦gƒvòÏ+tù\¬ü¿/ÿOÃJÅó4|>‚ž dâ;3¡I²’,œK6ü¡,8삳þBƒ<H4̬|&Ÿg€øƒ‚ÂZÄl_Ô½ëIH‘öé;#ê‡ê¾]¿¤øj:dÖ/Y<|Zˈ'Íe¥T––Jhï“äëCq ©ÇäIB85B˜€äÁ„a`åCóùG¾ãçâ5^9Ð,ø™LOg&.¸WÂÂ< Þl°}…ÿ3`𘅕גñ·^`ú&p­>›‚c¤áØéøIL¼t¬ h³t˜|œyXh‘¬(ÁÌ2¥|V0³$ Gz¡ÞÄuïÒ²¤6`0:»Öÿxû¶¸ÉC‡HÕ[rЋçÎÉ.œ<éúhÚœlʇSžŒY„qð;â¹N„¦Â΄P2›6OF;8òpwgs(‡©èÝŒÒñ·4¼ÎwÖéA&€‘%ÔvÏW3P;’f M‰ðb°Â±Â Ćº ÄB°ãÀN27¦TÖ&8v¾?ç‘ °$ã1‰Í+€7qG4´H$ŽsÆg þ›?@è‹â,g4y0™¥>P‚4¥©Ð Ì ”ÅfÂÈäÊ@}-ôÔÕ¤8”ÑFk©Rˆ¦*pÿ^ô¾ @C¸`€$ 5"ì(3ÇKBD*BÏ aº;$‰@08^$ G’PõâÜþöCp.0³ŒÐ×Wmà`ò°M@>|Ùû*>Þ[²GPY>gÎÁñuµ44;‹ ##(å± éÆÂÎO€“Ì& 1wål&k‰öÕ ‚ï` @¼(…5¦Dܱ!ä ö84{‹A!S €®¦LAh3¨¢@¾* m˜*Õæ 'C.ˆãŽö=^\[0 Á7„?†} |gKa%ÁÜJ–9œs߃ßמ ù†ÂQ˜‚.Ö ¾ª×s Â&–j cÄyK„¹#.c禡ó'Oú™µG5‚y $f Ïnzì&"/‘„°k 0 wk6m² ”ì0 wç €ƒ‘'8ëuâ¸['„ŠÅøƒp€Âuâr²ä‰Ù‚îrÒµ&ûÀ_Oçbª—çòô€€ÙQvûÔ»`aþ$ñ(âQ¢›ŽóÉÂØ·4_oJBH7ç`Æãû™æó‰Ç¹Åâ߉ÂH*€a0@qž¾ˆâ €8ACéËÈ‘ê r·²%HGHÖ[rÌC_~éµxÖÌ;Í ³—'&P+¤Â÷Hzž[H†@²ÏIƒ0²£œ áL&!\› ߀A‘ÌÝEP5(t8`JEÁ”ñBÇC ‰Vë!C~pUQ9裣3)ÙË+yÁ”)º§Žëõb¹wïƒï¯\é¿kÇv‹å $MhþÅðšª{#P‡ÒRSM5¹ÙTß( ‰Ã¹Åàœâ ü18—œS ;åkœ+¯CÛÄ>ð…¼áÏxB£9á¼ d1ã>ˆ«•h8"vÒßy~é—qñ›oô?[¿þ›QÂT fÀ¼Jƒ£VÉð=ؾOÊ[±`À¼8pWŽ@âxŽ6 À€3Íšƒl4x8ZUTÎZXΈvröªËÎQܾn}×[×ÿ}ûñãǾüüsy%yê˜Ñ§Ç77?Bune¢»|ò2Ñ 'r(—‚¯p€¦}±ö0‡öhóú 1%o,Oh'ŒUЃ¢ âj-j䥋ÕÛqÀË—/4uâÄŠy3¦ß3i"ÕBSÎ{ÄÃ9g§—àda2ÛýX‰ \ /®âㆠ0±âØß@h6ÑP šCõhËš ¼N;þɃû÷ÿ4¥ãøÑ£?3'òÈ‘WFÚZ…sÌ‹…&ÁyÆ€° †s LT pì àœýQ÷Áy2H¼dWøDÆJJ¤ò¢³•mÃm±y;úe^ÅÕ«W??ztXS]ÝófΤ‰-#¨¥¶–2C‚)$‰eê„#$@È d±ÏÁ ÍÁÑ¥H¼ €ÄB‹$@{„jª>M°mš3~üïÜùÓÀøçk½qíÚGk—/wŸ7mÚÆ‰#F´N9’F`$u~LÅ!“ m'8å›U!‚ïíóöC$Ë—‚Á<ÞXž(¢rECmK¥-b­o<î³-[Åô÷—)\oñfL›;´¦úö°†¡4QÍÕ°óA/)DÝyúæFAè8z !‹Â]8@ˆAØ–µEV$lú¬0¬p€#àÑÑ~šêí;yé칃;b ‰º}µwoùg7^߸j%Í¡² £©9¡s0fVˆ`Z±cnº{;@|žÄ ļ,œ« û!ŠJ¤!-wÇÞÌ2çÈáÃïwÄ9‹ù†îÀ²%KR'Œ{oxS#ÿª¾¢œò‘Éù’èãMQHF ‘0ÖG„+¦Ô‹Žça¼ pÁºOÒ}}W­]¶|PGnÉõkß÷8väpÂŽÍ›~Þºv-Íš8 I¥”ᶃ¦¢aíÚàf€x!dì óÊ çë _É~ˆ1ÌéÈ£)¶¢òôÄ$§›7ntîÈsû Úk×®uß±}{Ùª•+-^´ˆ AR¢")14˜bQVá舻² ¨¸+C¸B \!¬PDBa¢„!+ÎíuÂð<Ä@ïqADØè-kÖ ø»¶àôñî{vîØ·aåʶqy÷`˜ƒA hÖ¾¬=ÌÌɦ•;"X W\‡®C„s¨44Â0Q}4½6ÕÖºâû‡^ŠYøwíƒø{:p._ºÔk˖͛׬ZEÇ¥XäÂÑl:†R×PTðqe^ìö „pƒ†`Œ{F&<M¤y>G ŽÖãXûO·¬Yý·Öz£[âû§×ÿlƽ“F¶¯ÉMILŠBmŒxŽÈëôã¾Nç²yÓ&ÿŠòòó¶i “ʶ¼0ÞBf ©ù«¯»kÞÊ¥K^;öÀþýJãÇŒ9†É¹Î(’²ÅžbŽŠvÆ&$Âù³Ie°ÏaÉàXÌ0Á˜iïˆ^©rbknþåÜÙ³å^§ßD|.¯Ñœ:uJ¢º¦zk&Â¥þ nA¨ôy𬖿c{»‰KúÚƒ·INÕ)“&OKL$W´µC½‡#2ûŽÉæ€ü=@b'€¤Ý¬²PÌ‘$4Á(i#M2ëXOM•¬LŒ· kVz~ñ©¼.;ðäÉ“÷JKKsrssïEEG“!îÀZê꤂"'5EÅ{îN.a¯Ë¹þÏ󨩬Ð,+.>]QZJá$Ì+}8æðE€C‹‘5üK€Þ3ÕÍ‘Ï1Å*cÐM$¨`t¶µ»ß>q·Å×õ‡~•çµqãFuÿoÃÃÃÉÀЈ445HI^¤Ñ`MbÀàß\]^åùý»ïž0v¬L~vÖÚôää¶ø¨(rÕ]Äþá^;l A–(ãý@×1Fµ#ƒÄ@S­-3%ÙÿƨSy]÷@|^´pÎß«««ssuu=fŒ;­®ž>i¢‡­š² ÉËÈ’Œ”IKH=ôóòIë Sx)‡=sêTŠ’’’¸ˆÈS^žmȤ‹ E¶XÖ ƒƒ—9käu̸&D¨&zÚ¿íݳ[ñ¥œŒø oÇÞÞäˆÖE"ÙQ·fŠ;ç@ÅâÈ•r êÊ doks„)7oêõŠÏû%ìÀÙ³g{¥§§O011y¢œ›V‚Ïph¢BCE™”ѼMCQþ×ï2½ÑÓ2bÄÌà  6K8ì¦0±,@oG½‡àwè#¢ü‡–:Ø9ØÚ|€ˆ)î/AÎÞØCLŸ>ÝÙÃÓã{#˜¦&¦d€Œ²6Z¬E0XFY½jå¥È×Ù~Á’ þÖâ§ŽØÔÉ“&%%&þ$¯Ì‰C}8åúÚ¤ÓJ‘+dÑuàh€¡ì ²Ý/HGü oÈ1=zø^MuÕlK+Ëg h ìœk¢B-pÔ@ÞSyØPUnw÷çŸßø»éÒ¥K5óòòN:;;â†`D†¨‚ü/€$H2@4ÁÅ@öŠò†sGœæg›×kDyß³EÏ([ÔzÂç°®¶¶6L Ò‚sngnðÔ×ÑvùwW®¼Î*LÊõuu3}ÀJ¶@,C4³3„Ñg-‚Ю¢Í“î(í¤#$ï 9æ²O绦Dú·æ¤ÆSDˆ?YYãnªKºðAL‘pÆÌó7‡ýåyÙ&oÈ%ýÇÓD!Ø{³gÍJÅ TH"´køÂÌú@`^¢ 50_ˆò·ôí}ÃùoÎu­,Ì1æäSÕ%"Üžyصú¹Ú> õrü)ØÃq_My‰æÛ¶ÇŽ“OIIùÁ ´}cä@ØÌ4üv ÒgGûmoÛµ‹¯çîÀçÛ·öX4w¦ö°úªä‚̤†Â¬Ä y‰ue¹iõ%*H¾ñ~ÇÿÜ’;wî¼_[SSáããõ½1"YЖ ÝÒ pÖ *øƒÛ)~»xÞŽ¸}ûv§eË–©η07½ùBƒh£XJ[KãIB|\èÞ½_Šè¼?·ø*þìlÞ¼y@ffú4Ö$zÈikj“šªÚ£1cÆèÿÙcŠ?'Þ·fΟ?? ²¢rcô‘Q×Cu¡&r@öööS¦M›öÊ»³¼5-¾7s~uÐ1++ë*rAðE È åÄÖhH¡¯§÷]||¼Ï›yUâ³ïÀKØ"zÿðáÃ+rrsÚ˜I ‡²[[[[`ÈÍ)LÌL¾ËËÏ3:úõÑ7š^ó¶J|ˆwq@Ô”Ú°aýÍdtŽ4F•!óÐLQŽ›Šyð#ÑrÕ5øÐ,Pr,®Iä]¿fdÕ½ëëëÛPV,˜Vlb±bgg'dîܹäëë×êää´ïN½ëó.]kkk§-[¶”ÅÅÅ‘ƒƒƒC4p`ª>JŒÃÐpnÖ¬YÂòòò~ÐÌ©¨¨Py—öH|­ïðüøã[ZFŒqtr"3Ì aÄÔwÖ º:ºd„&ܬ¹š>cYÛØ´¡>ÿBTT”ÿøñã_«&yïðÏ(¾ôŽÚ3§OöÏÉJßhŽY!¬9B˜Ž:û"¬I8¥e¥0¡f†LÐ ^__ÿª‡»{Óõê¨sW¼¯|.]¼Ð»¢8oæƒÅü $ mêa98ØSYY™à“ø¡K#ÿ ¥Èm666û M"Ð@^¬M^ù¯)>—¾÷ïßï´xá\‡·;–mÖ–ÆÐ N^…‹ª<ò­¬¬¤ÔÔTA«U—:Zmxßm''çqMMMvß}÷x"îKÿ•Ä|å;°ní*åâ¼Ì„äèÀé¹ÉYînN_˜š=Ö ^@a"£!š9¸¹»RRry£v{¯0ÐRPy©©…º}míÇ"‘Ýa?ÿa---ânð¯üWŸÀKÝ{÷îu:þõá¿=w¦óØÑôcÈlÍZMÑõÄ€9`™À srt  2ǬÁ¡nޱc¯‡Èü–§U'NœÏy©¿ø`¯Õܼq½KUINMx û5'4wà²\î× Œ‰–èÏÔtž¤Ì¬LBóFruq4‹…¥Åqô3Ö~­.H|2âxÙ;°vÕÒ-5ÑAî·¼ìMÈÜͬCDº–˜òëëçK£F¢Ï¶&˜_ Cƒû±11å/û|ÄÇïÀk¹£†ÕY¦Fxovµhsµ1Æ\!ãΡaÖ$5µ5´cÇ*+/#3´2àL¼•ùËÎU}-/H|Râx™;pùâùÎF6êfFûíçøÌÏ ÓªlÍÑSËXÐ$5554þ|JJJ`ÌÉYd:~5W'<Åøˆ—y.âc‰wàµÝMk— ÌKKŽ Øàþkˆ·#…x»PQNiN…Ù©èNþ.Oc‚<În¬uzm/F|bâ舸pþ|ç ÕÊ™Éq©éñ›#ü¿m®)y8sò¸gÅ9ÉÏbC|~ÎI‰YU”myùâ…÷;âÄÇïÀ±˜üÞé“'zÎ5]súäñ–Ó§M´Ü»g—ÒƒÅfÕñ ŠOR¼âï€xÄ; Þñ¼v;ðÿ«/ýÖŜ֬IEND®B`‚Pyro5-5.15/docs/source/api.rst000066400000000000000000000011011451404116400162060ustar00rootroot00000000000000***************** Pyro5 library API ***************** This chapter describes Pyro's library API. All Pyro classes and functions are defined in sub packages such as :mod:`Pyro5.core`, but for ease of use, the most important ones are also placed in the :mod:`Pyro5.api` package. .. toctree:: api/api.rst api/config.rst api/client.rst api/core.rst api/server.rst api/errors.rst api/nameserver.rst api/callcontext.rst api/protocol.rst api/socketutil.rst api/compatibility.rst api/echoserver.rst api/httpgateway.rst api/socketserver.rst Pyro5-5.15/docs/source/api/000077500000000000000000000000001451404116400154635ustar00rootroot00000000000000Pyro5-5.15/docs/source/api/api.rst000066400000000000000000000001661451404116400167710ustar00rootroot00000000000000:mod:`Pyro5.api` --- Main API package ===================================== .. automodule:: Pyro5.api :members: Pyro5-5.15/docs/source/api/callcontext.rst000066400000000000000000000002271451404116400205360ustar00rootroot00000000000000:mod:`Pyro5.callcontext` --- Call context handling ================================================== .. automodule:: Pyro5.callcontext :members: Pyro5-5.15/docs/source/api/client.rst000066400000000000000000000002001451404116400174630ustar00rootroot00000000000000:mod:`Pyro5.client` --- Client code logic ========================================= .. automodule:: Pyro5.client :members: Pyro5-5.15/docs/source/api/compatibility.rst000066400000000000000000000003111451404116400210610ustar00rootroot00000000000000:mod:`Pyro5.compatibility.Pyro4` --- Pyro4 backward compatibility layer ======================================================================= .. automodule:: Pyro5.compatibility.Pyro4 :members: Pyro5-5.15/docs/source/api/config.rst000066400000000000000000000011411451404116400174570ustar00rootroot00000000000000:mod:`Pyro5.config` --- Configuration items =========================================== Pyro's configuration is available in the ``Pyro5.config`` object. Detailed information about the API of this object is available in the :doc:`/config` chapter. .. note:: creation of the ``Pyro5.config`` object This object is constructed when you import Pyro5. It is an instance of the :class:`Pyro5.configure.Configuration` class. The package initializer code creates it and the initial configuration is determined (from defaults and environment variable settings). It is then assigned to ``Pyro5.config``. Pyro5-5.15/docs/source/api/core.rst000066400000000000000000000001661451404116400171500ustar00rootroot00000000000000:mod:`Pyro5.core` --- core Pyro logic ===================================== .. automodule:: Pyro5.core :members: Pyro5-5.15/docs/source/api/echoserver.rst000066400000000000000000000003151451404116400203610ustar00rootroot00000000000000:mod:`Pyro5.utils.echoserver` --- Built-in echo server for testing purposes =========================================================================== .. automodule:: Pyro5.utils.echoserver :members: Pyro5-5.15/docs/source/api/errors.rst000066400000000000000000000011361451404116400175320ustar00rootroot00000000000000:mod:`Pyro5.errors` --- Exception classes ========================================= The exception hierarchy is as follows:: Exception | +-- PyroError | +-- NamingError +-- DaemonError +-- SecurityError +-- CommunicationError | +-- ConnectionClosedError +-- TimeoutError +-- ProtocolError | +-- MessageTooLargeError +-- SerializeError .. automodule:: Pyro5.errors :members: Pyro5-5.15/docs/source/api/httpgateway.rst000066400000000000000000000002461451404116400205600ustar00rootroot00000000000000:mod:`Pyro5.utils.httpgateway` --- HTTP to Pyro gateway ======================================================= .. automodule:: Pyro5.utils.httpgateway :members: Pyro5-5.15/docs/source/api/nameserver.rst000066400000000000000000000003031451404116400203600ustar00rootroot00000000000000:mod:`Pyro5.nameserver` --- Pyro name server ============================================ .. automodule:: Pyro5.nameserver :members: .. autoclass:: Pyro5.nameserver.NameServer :members: Pyro5-5.15/docs/source/api/protocol.rst000077500000000000000000000005221451404116400200600ustar00rootroot00000000000000:mod:`Pyro5.protocol` --- Pyro wire protocol ============================================ .. automodule:: Pyro5.protocol :members: .. attribute:: MSG_* (*int*) The various message type identifiers .. attribute:: FLAGS_* (*int*) Various bitflags that specify the characteristics of the message, can be bitwise or-ed together Pyro5-5.15/docs/source/api/server.rst000066400000000000000000000002111451404116400175150ustar00rootroot00000000000000:mod:`Pyro5.server` --- Server (daemon) logic ============================================= .. automodule:: Pyro5.server :members: Pyro5-5.15/docs/source/api/socketserver.rst000066400000000000000000000043741451404116400207440ustar00rootroot00000000000000Socket server API contract ************************** For now, this is an internal API, used by the Pyro Daemon. The various servers in Pyro5.socketserver implement this. .. py:class:: SocketServer_API **Methods:** .. py:method:: init(daemon, host, port, unixsocket=None) Must bind the server on the given host and port (can be None). daemon is the object that will receive Pyro invocation calls (see below). When host or port is None, the server can select something appropriate itself. If possible, use ``Pyro4.config.COMMTIMEOUT`` on the sockets (see :doc:`config`). Set ``self.sock`` to the daemon server socket. If unixsocket is given the name of a Unix domain socket, that type of socket will be created instead of a regular tcp/ip socket. .. py:method:: loop(loopCondition) Start an endless loop that serves Pyro requests. loopCondition is an optional function that is called every iteration, if it returns False, the loop is terminated and this method returns. .. py:method:: events(eventsockets) Called from external event loops: let the server handle events that occur on one of the sockets of this server. eventsockets is a sequence of all the sockets for which an event occurred. .. py:method:: shutdown() Initiate shutdown of a running socket server, and close it. .. py:method:: close() Release resources and close a stopped server. It can no longer be used after calling this, until you call initServer again. .. py:method:: wakeup() This is called to wake up the :meth:`requestLoop` if it is in a blocking state. **Properties:** .. py:attribute:: sockets must be the list of all sockets used by this server (server socket + all connected client sockets) .. py:attribute:: sock must be the server socket itself. .. py:attribute:: locationStr must be a string of the form ``"serverhostname:serverport"`` can be different from the host:port arguments passed to initServer. because either of those can be None and the server will choose something appropriate. If the socket is a Unix domain socket, it should be of the form ``"./u:socketname"``. Pyro5-5.15/docs/source/api/socketutil.rst000066400000000000000000000002311451404116400203770ustar00rootroot00000000000000:mod:`Pyro5.socketutil` --- Socket related utilities ==================================================== .. automodule:: Pyro5.socketutil :members: Pyro5-5.15/docs/source/changelog.rst000066400000000000000000000131261451404116400173760ustar00rootroot00000000000000********** Change Log ********** **Pyro 5.15** - removed Python 3.7 from the support list (it is EOL). Now supported on Python 3.8 or newer. - fixed cgi.parse deprecation problem in http gateway - removed jquery dependency in http gateway - some small tweaks to setup, tests, examples, and docs. - updated the self-signed example certificates and serial numbers in the ssl example. **Pyro 5.14** - http gateway now also has OPTION call with CORS - fixed deprecation warning about setting threads in daemon mode - fixed more threading module deprecation warnings - proxy now correctly exposes remote __len__, __iter__ and __getitem__ etc - improved type hint for expose() - added Proxy._pyroLocalSocket property that is the local socket address used in the proxy. - serve() no longer defaults host parameter to empty string, but None. To be more similar to what a creation of Daemon() normally does. - fixed a Python 3.11 serialization issue **Pyro 5.13.1** - fixed @expose issue on static method/classmethod due to API change in Python 3.10 **Pyro 5.13** - removed Python 3.6 from the support list (it is EOL). Now supported on Python 3.7 or newer - corrected documentation about autoproxy: this feature is not configurable, it is always active. - introduced SERPENT_BYTES_REPR config item (and updated serpent library version requirement for this) - flush nameserver output to console before entering request loop - added optional boolean "weak" parameter to Daemon.register(), to register a weak reference to the server object that will be unregistered automatically when the server object gets deleted. - switched from travis to using github actions for CI builds and tests **Pyro 5.12** - fixed error when import Pyro5.server (workaround was to import Pyro5.core before it) - documented SSL_CACERTS config item - removed Python 3.5 from the support list (it is EOL). Now requires Python 3.6 or newer **Pyro 5.11** - reworked the timezones example. (it didn't work as intended) - httpgateway message data bytearray type fix - fixed ipv6 error in filetransfer example - added methodcall_error_handler in documentation **Pyro 5.10** - finally ported over the unit test suite from Pyro4 - finally updated the documentation from Pyro4 to Pyro5 (there's likely still some errors or omissions though) - fixed regex lookup index error in nameserver - the 4 custom class (un)register methods on the SerializerBase class are now also directly available in the api module **Pyro 5.9.2** - fixed a silent error in the server when doing error handling (avoid calling getpeername() which may fail) this issue could cause a method call to not being executed in a certain specific scenario. (oneway call on MacOS when using unix domain sockets). Still, it's probably wise to upgrade as this was a regression since version 5.8. **Pyro 5.9.1** - fixed some circular import conflicts - fixed empty nameserver host lookup issue **Pyro 5.9** - added privilege-separation example - added methodcall_error_handler to Daemon that allows you to provide a custom error handler, which is called when an exception occurs in the method call's user code - introduced ``api.serve`` / ``server.serve`` as a replacement for the static class method ``Daemon.serveSimple`` - fix possible race condition when creating instances with instancemode "single" - introduced some more type hintings **Pyro 5.8** - cython compatibility fix - removed explicit version checks of dependencies such as serpent. This fixes crash error when dealing with prerelease versions that didn't match the pattern. **Pyro 5.7** - fixed possible attribute error in proxy del method at interpreter shutdown - gave the serialization example a clearer name 'custom-serialization' - added NS_LOOKUP_DELAY config item and parameter to resolve() to have an optional wait delay until a name becomes available in the nameserver - added lookup() and yplookup() utility functions that implement this retry mechanism **Pyro 5.6** - improved and cleaned up exception handling throughout the code base - URIs now accept spaces in the location part. This is useful for unix domain sockets. **Pyro 5.5** - made msgpack serializer optional - Anaconda 'pyro5' package created **Pyro 5.4** - made the decision that Pyro5 will require Python 3.5 or newer, and won't support Python 2.7 (which will be EOL in january 2020) - begun making Pyro5 specific documentation instead of referring to Pyro4 - tox tests now include Python 3.8 as well (because 3.8 beta was released recently) - dropped support for Python 3.4 (which has reached end-of-life status). Supported Python versions are now 2.7, and 3.5 or newer. (the life cycle status of the Python versions can be seen here https://devguide.python.org/#status-of-python-branches) - code cleanups, removing some old compatibility stuff etc. **Pyro 5.3** various things ported over from recent Pyro4 changes: - added a few more methods to the 'private' list - fix thread server worker thread name - on windows, the threaded server can now also be stopped with ctrl-c (sigint) - NATPORT behavior fix when 0 - source dist archive is more complete now - small fix for cython **Pyro 5.2** - travis CI python3.7 improvements - serialization improvements/fixes - reintroduced config object to make a possibility for a non-static (non-global) pyro configuration **Pyro 5.1** - python 3.5 or newer is now required - socketutil module tweaks and cleanups - added a bunch of tests, taken from pyro4 mostly, for the socketutil module - moved to declarative setup.cfg rather than in setup.py - made sure the license is included in the distribution **Pyro 5.0** - first public release Pyro5-5.15/docs/source/clientcode.rst000066400000000000000000000610661451404116400175660ustar00rootroot00000000000000.. index:: client code, calling remote objects ******************************* Clients: Calling remote objects ******************************* This chapter explains how you write code that calls remote objects. Often, a program that calls methods on a Pyro object is called a *client* program. (The program that provides the object and actually runs the methods, is the *server*. Both roles can be mixed in a single program.) Make sure you are familiar with Pyro's :ref:`keyconcepts` before reading on. .. index:: object discovery, location, object name .. _object-discovery: Object discovery ================ To be able to call methods on a Pyro object, you have to tell Pyro where it can find the actual object. This is done by creating an appropriate URI, which contains amongst others the object name and the location where it can be found. You can create it in a number of ways. .. index:: PYRO protocol type * directly use the object name and location. This is the easiest way and you write an URI directly like this: ``PYRO:someobjectid@servername:9999`` It requires that you already know the object id, servername, and port number. You could choose to use fixed object names and fixed port numbers to connect Pyro daemons on. For instance, you could decide that your music server object is always called "musicserver", and is accessible on port 9999 on your server musicbox.my.lan. You could then simply use:: uri_string = "PYRO:musicserver@musicbox.my.lan:9999" # or use Pyro5.api.URI("...") for an URI object instead of a string Most examples that come with Pyro simply ask the user to type this in on the command line, based on what the server printed. This is not very useful for real programs, but it is a simple way to make it work. You could write the information to a file and read that from a file share (only slightly more useful, but it's just an idea). * use a logical name and look it up in the name server. A more flexible way of locating your objects is using logical names for them and storing those in the Pyro name server. Remember that the name server is like a phone book, you look up a name and it gives you the exact location. To continue on the previous bullet, this means your clients would only have to know the logical name "musicserver". They can then use the name server to obtain the proper URI:: import Pyro5.api nameserver = Pyro5.api.locate_ns() uri = nameserver.lookup("musicserver") # ... uri now contains the URI with actual location of the musicserver object You might wonder how Pyro finds the Name server. This is explained in the separate chapter :doc:`nameserver`. * use a logical name and let Pyro look it up in the name server for you. Very similar to the option above, but even more convenient, is using the *meta*-protocol identifier ``PYRONAME`` in your URI string. It lets Pyro know that it should lookup the name following it, in the name server. Pyro should then use the resulting URI from the name server to contact the actual object. See :ref:`nameserver-pyroname`. This means you can write:: uri_string = "PYRONAME:musicserver" # or Pyro5.api.URI("PYRONAME:musicserver") for an URI object You can use this URI everywhere you would normally use a normal uri (using ``PYRO``). Everytime Pyro encounters the ``PYRONAME`` uri it will use the name server automatically to look up the object for you. [#pyroname]_ * use object metadata tagging to look it up (yellow-pages style lookup). You can do this directly via the name server for maximum control, or use the ``PYROMETA`` protocol type. See :ref:`nameserver-pyrometa`. This means you can write:: uri_string = "PYROMETA:metatag1,metatag2" # or Pyro5.api.URI("PYROMETA:metatag1,metatag2") for an URI object You can use this URI everywhere you would normally use a normal uri. Everytime Pyro encounters the ``PYROMETA`` uri it will use the name server automatically to find a random object for you with the given metadata tags. [#pyroname]_ .. [#pyroname] this is not very efficient if it occurs often. Have a look at the :doc:`tipstricks` chapter for some hints about this. .. index:: double: Proxy; calling methods Calling methods =============== Once you have the location of the Pyro object you want to talk to, you create a Proxy for it. Normally you would perhaps create an instance of a class, and invoke methods on that object. But with Pyro, your remote method calls on Pyro objects go through a proxy. The proxy can be treated as if it was the actual object, so you write normal python code to call the remote methods and deal with the return values, or even exceptions:: # Continuing our imaginary music server example. # Assume that uri contains the uri for the music server object. musicserver = Pyro5.api.Proxy(uri) try: musicserver.load_playlist("90s rock") musicserver.play() print("Currently playing:", musicserver.current_song()) except MediaServerException: print("Couldn't select playlist or start playing") For normal usage, there's not a single line of Pyro specific code once you have a proxy! .. index:: single: object serialization double: serialization; serpent double: serialization; marshal double: serialization; json double: serialization; msgpack .. index:: double: Proxy; remote attributes Accessing remote attributes =========================== You can access exposed attributes of your remote objects directly via the proxy. If you try to access an undefined or unexposed attribute, the proxy will raise an AttributeError stating the problem. Note that direct remote attribute access only works if the metadata feature is enabled (``METADATA`` config item, enabled by default). :: import Pyro5.api p = Pyro5.api.Proxy("...") velo = p.velocity # attribute access, no method call print("velocity = ", velo) See the `attributes example `_ for more information. .. _object-serialization: Serialization ============= Pyro will serialize the objects that you pass to the remote methods, so they can be sent across a network connection. Depending on the serializer that is being used, there will be some limitations on what objects you can use. * **serpent**: the default serializer. Serializes into Python literal expressions. Accepts quite a lot of different types. Many will be serialized as dicts. You might need to explicitly translate literals back to specific types on the receiving end if so desired, because most custom classes aren't dealt with automatically. Requires third party library module, but it will be installed automatically as a dependency of Pyro. * **json**: more restricted as serpent, less types supported. Part of the standard library. * **marshal**: a very limited but very fast serializer. Can deal with a small range of builtin types only, no custom classes can be serialized. Part of the standard library. * **msgpack**: See https://pypi.python.org/pypi/msgpack Reasonably fast serializer (and a lot faster if you're using the C module extension). Can deal with many builtin types, but not all. Not enabled by default because it's optional, but it's safe to add to the accepted serializers config item if you have it installed. .. index:: SERIALIZER You select the serializer to be used by setting the ``SERIALIZER`` config item. (See the :doc:`/config` chapter). The valid choices are the names of the serializer from the list mentioned above. It is possible to override the serializer on a particular proxy. This allows you to connect to one server using the default serpent serializer and use another proxy to connect to a different server using the json serializer, for instance. Set the desired serializer name in ``proxy._pyroSerializer`` to override. .. index:: deserialization, serializing custom classes, deserializing custom classes .. _customizing-serialization: Customizing serialization ------------------------- By default, custom classes are serialized into a dict. They are not deserialized back into instances of your custom class. This avoids possible security issues. An exception to this however are certain classes in the Pyro5 package itself (such as the URI and Proxy classes). They *are* deserialized back into objects of that certain class, because they are critical for Pyro to function correctly. There are a few hooks however that allow you to extend this default behaviour and register certain custom converter functions. These allow you to change the way your custom classes are treated, and allow you to actually get instances of your custom class back from the deserialization if you so desire. The hooks are provided via several methods: :py:meth:`Pyro5.api.register_class_to_dict` and :py:meth:`Pyro5.api.register_dict_to_class` and their unregister-counterparts: :py:meth:`Pyro5.api.unregister_class_to_dict` and :py:meth:`Pyro5.api.unregister_dict_to_class` Click on the method link to see its apidoc, or have a look at the `custom-serialization example `_ and the `test_serialize unit tests `_ for more information. It is recommended to avoid using these hooks if possible, there's a security risk to create arbitrary objects from serialized data that is received from untrusted sources. .. index:: release proxy connection .. index:: double: Proxy; cleaning up .. _client_cleanup: Proxies, connections, threads and cleaning up ============================================= Here are some rules: * Every single Proxy object will have its own socket connection to the daemon. * You cannot share Proxy objects among threads. One single thread 'owns' a proxy. It is possible to explicitly transfer ownership to another thread. * Usually every connection in the daemon has its own processing thread there, but for more details see the :doc:`servercode` chapter. * Consider cleaning up a proxy object explicitly if you know you won't be using it again in a while. That will free up resources and socket connections. You can do this in two ways: 1. calling ``_pyroRelease()`` on the proxy. 2. using the proxy as a context manager in a ``with`` statement. *This is the preferred way of creating and using Pyro proxies.* This ensures that when you're done with it, or an error occurs (inside the with-block), the connection is released:: with Pyro5.api.Proxy(".....") as obj: obj.method() *Note:* you can still use the proxy object when it is disconnected: Pyro will reconnect it for you as soon as it's needed again. * At proxy creation, no actual connection is made. The proxy is only actually connected at first use, or when you manually connect it using the ``_pyroReconnect()`` or ``_pyroBind()`` methods. .. index:: double: oneway; client method call .. _oneway-calls-client: Oneway calls ============ Normal method calls always block until the response is returned. This can be any normal return value, ``None``, or an error in the form of a raised exception. The client code execution is suspended until the method call has finished and produced its result. Some methods never return any response or you are simply not interested in it (including errors and exceptions!), or you don't want to wait until the result is available but rather continue immediately. You can tell Pyro that calls to these methods should be done as *one-way calls*. For calls to such methods, Pyro will not wait for a response from the remote object. The return value of these calls is always ``None``, which is returned *immediately* after submitting the method invocation to the server. The server will process the call while your client continues execution. The client can't tell if the method call was successful, because no return value, no errors and no exceptions will be returned! If you want to find out later what - if anything - happened, you have to call another (non-oneway) method that does return a value. .. index:: double: @Pyro5.api.oneway; client handling **How to make methods one-way:** You mark the methods of your class *in the server* as one-way by using a special *decorator*. See :ref:`decorating-pyro-class` for details on how to do this. See the `oneway example `_ for some code that demonstrates the use of oneway methods. .. index:: batch calls .. _batched-calls: Batched calls ============= Doing many small remote method calls in sequence has a fair amount of latency and overhead. Pyro provides a means to gather all these small calls and submit it as a single 'batched call'. When the server processed them all, you get back all results at once. Depending on the size of the arguments, the network speed, and the amount of calls, doing a batched call can be *much* faster than invoking every call by itself. Note that this feature is only available for calls on the same proxy object. How it works: #. You create a batch proxy object for the proxy object. #. Call all the methods you would normally call on the regular proxy, but use the batch proxy object instead. #. Call the batch proxy object itself to obtain the generator with the results. You create a batch proxy using this: ``batch = Pyro5.api.BatchProxy(proxy)``. The signature of the batch proxy call is as follows: .. py:method:: batchproxy.__call__([oneway=False]) Invoke the batch and when done, returns a generator that produces the results of every call, in order. If ``oneway==True``, perform the whole batch as one-way calls, and return ``None`` immediately. If ``asynchronous==True``, perform the batch asynchronously, and return an asynchronous call result object immediately. **Simple example**:: batch = Pyro5.api.BatchProxy(proxy) batch.method1() batch.method2() # more calls ... batch.methodN() results = batch() # execute the batch for result in results: print(result) # process result in order of calls... **Oneway batch**:: results = batch(oneway=True) # results==None See the `batchedcalls example `_ for more details. .. index:: remote iterators/generators Remote iterators/generators =========================== You can iterate over a remote iterator or generator function as if it was a perfectly normal Python iterable. Pyro will fetch the items one by one from the server that is running the remote iterator until all elements have been consumed or the client disconnects. .. sidebar:: *Filter on the server* If you plan to filter the items that are returned from the iterator, it is strongly suggested to do that on the server and not in your client. Because otherwise it is possible that you first have to serialize and transfer all possible items from the server only to select a few out of them, which is very inefficient. *Beware of many small items* Pyro has to do a remote call to get every next item from the iterable. If your iterator produces lots of small individual items, this can be quite inefficient (many small network calls). Either chunk them up a bit or use larger individual items. So you can write in your client:: proxy = Pyro5.api.Proxy("...") for item in proxy.things(): print(item) The implementation of the ``things`` method can return a normal list but can also return an iterator or even be a generator function itself. This has the usual benefits of "lazy" generators: no need to create the full collection upfront which can take a lot of memory, possibility of infinite sequences, and spreading computation load more evenly. By default the remote item streaming is enabled in the server and there is no time limit set for how long iterators and generators can be 'alive' in the server. You can configure this however if you want to restrict resource usage or disable this feature altogether, via the ``ITER_STREAMING`` and ``ITER_STREAM_LIFETIME`` config items. Lingering when disconnected: the ``ITER_STREAM_LINGER`` config item controls the number of seconds a remote generator is kept alive when a disconnect happens. It defaults to 30 seconds. This allows you to reconnect the proxy and continue using the remote generator as if nothing happened (see :py:meth:`Pyro5.client.Proxy._pyroReconnect` or even :ref:`reconnecting`). If you reconnect the proxy and continue iterating again *after* the lingering timeout period expired, an exception is thrown because the remote generator has been discarded in the meantime. Lingering can be disabled completely by setting the value to 0, then all remote generators from a proxy will immediately be discarded in the server if the proxy gets disconnected or closed. There are several examples that use the remote iterator feature. Have a look at the `streaming `_ , `stockquotes `_ or the `filetransfer `_ examples. .. index:: callback Pyro Callbacks ============== Usually there is a nice separation between a server and a client. But with some Pyro programs it is not that simple. It isn't weird for a Pyro object in a server somewhere to invoke a method call on another Pyro object, that could even be running in the client program doing the initial call. In this case the client program is a server itself as well. These kinds of 'reverse' calls are labeled *callbacks*. You have to do a bit of work to make them possible, because normally, a client program is not running the required code to also act as a Pyro server to accept incoming callback calls. In fact, you have to start a Pyro daemon and register the callback Pyro objects in it, just as if you were writing a server program. Keep in mind though that you probably have to run the daemon's request loop in its own background thread. Or make heavy use of oneway method calls. If you don't, your client program won't be able to process the callback requests because it is by itself still waiting for results from the server. .. index:: single: exception in callback single: @Pyro5.api.callback double: decorator; callback **Exceptions in callback objects:** If your callback object raises an exception, Pyro will return that to the server doing the callback. Depending on what the server does with it, you might never see the actual exception, let alone the stack trace. This is why Pyro provides a decorator that you can use on the methods in your callback object in the client program: ``@Pyro5.api.callback``. This way, an exception in that method is not only returned to the caller, but also logged locally in your client program, so you can see it happen including the stack trace (if you have logging enabled):: import Pyro5.api class Callback(object): @Pyro5.api.expose @Pyro5.api.callback def call(self): print("callback received from server!") return 1//0 # crash! Also notice that the callback method (or the whole class) has to be decorated with ``@Pyro5.api.expose`` as well to allow it to be called remotely at all. See the `callback example `_ for more details and code. .. index:: misc features Miscellaneous features ====================== Pyro provides a few miscellaneous features when dealing with remote method calls. They are described in this section. .. index:: error handling Error handling -------------- You can just do exception handling as you would do when writing normal Python code. However, Pyro provides a few extra features when dealing with errors that occurred in remote objects. This subject is explained in detail its own chapter: :doc:`errors`. See the `exceptions example `_ for more details. .. index:: timeouts Timeouts -------- Because calls on Pyro objects go over the network, you might encounter network related problems that you don't have when using normal objects. One possible problems is some sort of network hiccup that makes your call unresponsive because the data never arrived at the server or the response never arrived back to the caller. By default, Pyro waits an indefinite amount of time for the call to return. You can choose to configure a *timeout* however. This can be done globally (for all Pyro network related operations) by setting the timeout config item:: Pyro5.config.COMMTIMEOUT = 1.5 # 1.5 seconds You can also do this on a per-proxy basis by setting the timeout property on the proxy:: proxy._pyroTimeout = 1.5 # 1.5 seconds See the `timeout example `_ for more details. Also, there is a automatic retry mechanism for timeout or connection closed (by server side), in order to use this automatically retry:: Pyro5.config.MAX_RETRIES = 3 # attempt to retry 3 times before raise the exception You can also do this on a pre-proxy basis by setting the max retries property on the proxy:: proxy._pyroMaxRetries = 3 # attempt to retry 3 times before raise the exception Be careful to use when remote functions have a side effect (e.g.: calling twice results in error)! See the `autoretry example `_ for more details. .. index:: double: reconnecting; automatic .. _reconnecting: Automatic reconnecting ---------------------- If your client program becomes disconnected to the server (because the server crashed for instance), Pyro will raise a :py:exc:`Pyro5.errors.ConnectionClosedError`. You can use the automatic retry mechanism to handle this exception, see the `autoretry example `_ for more details. Alternatively, it is also possible to catch this and tell Pyro to attempt to reconnect to the server by calling ``_pyroReconnect()`` on the proxy (it takes an optional argument: the number of attempts to reconnect to the daemon. By default this is almost infinite). Once successful, you can resume operations on the proxy:: try: proxy.method() except Pyro5.errors.ConnectionClosedError: # connection lost, try reconnecting obj._pyroReconnect() This will only work if you take a few precautions in the server. Most importantly, if it crashed and comes up again, it needs to publish its Pyro objects with the exact same URI as before (object id, hostname, daemon port number). See the `autoreconnect example `_ for more details and some suggestions on how to do this. The ``_pyroReconnect()`` method can also be used to force a newly created proxy to connect immediately, rather than on first use. .. index:: proxy sharing Proxy sharing between threads ----------------------------- A proxy is 'owned' by a thread. You cannot use it from another thread. Pyro does not allow you to share the same proxy across different threads, because concurrent access to the same network connection will likely corrupt the data sequence. You can explicitly transfer ownership of a proxy to another thread via the proxy's ``_pyroClaimOwnership()`` method. The current thread then claims the ownership of this proxy from another thread. Any existing connection will remain active. See the `threadproxysharing example `_ for more details. .. index:: double: Daemon; Metadata .. _metadata: Metadata from the daemon ------------------------ A proxy contains some meta-data about the object it connects to. It obtains the data via the (public) :py:meth:`Pyro5.server.DaemonObject.get_metadata` method on the daemon that it connects to. This method returns the following information about the object (or rather, its class): what methods and attributes are defined, and which of the methods are to be called as one-way. This information is used to properly execute one-way calls, and to do client-side validation of calls on the proxy (for instance to see if a method or attribute is actually available, without having to do a round-trip to the server). Also this enables a properly working ``hasattr`` on the proxy, and efficient and specific error messages if you try to access a method or attribute that is not defined or not exposed on the Pyro object. Lastly the direct access to attributes on the remote object is also made possible, because the proxy knows about what attributes are available. Pyro5-5.15/docs/source/commandline.rst000066400000000000000000000043411451404116400177340ustar00rootroot00000000000000.. index:: command line tools .. _command-line: ****************** Command line tools ****************** Pyro has several command line tools that you will be using sooner or later. They are generated and installed when you install Pyro. - :command:`pyro5-ns` (name server) - :command:`pyro5-nsc` (name server client tool) - :command:`pyro5-echoserver` (test echo server) - :command:`pyro5-check-config` (prints configuration) - :command:`pyro5-httpgateway` (http gateway server) If you prefer, you can also invoke the various "executable modules" inside Pyro directly, by using Python's "-m" command line argument. Some of these tools are described in detail in their respective sections of the manual: Name server tools: See :ref:`nameserver-nameserver` and :ref:`nameserver-nsc` for detailed information. HTTP gateway server: See :ref:`http-gateway` for detailed information. .. index:: double: echo server; command line .. _command-line-echoserver: Test echo server ================ :command:`python -m Pyro5.utils.echoserver [options]` (or simply: :command:`pyro5-echoserver [options]`) This is a simple built-in server that can be used for testing purposes. It launches a Pyro object that has several methods suitable for various tests (see below). Optionally it can also directly launch a name server. This way you can get a simple Pyro server plus name server up with just a few keystrokes. A short explanation of the available options can be printed with the help option: .. program:: Pyro5.utils.echoserver .. option:: -h, --help Print a short help message and exit. The echo server object is available by the name ``test.echoserver``. It exposes the following methods: .. method:: echo(argument) Simply returns the given argument object again. .. method:: error() Generates a run time exception. .. method:: shutdown() Terminates the echo server. .. index:: double: configuration check; command line Configuration check =================== :command:`python -m Pyro5.configure` (or simply: :command:`pyro5-check-config`) This is the equivalent of:: >>> import Pyro5 >>> print(Pyro5.config.dump()) It prints the Pyro version, the location it is imported from, and a dump of the active configuration items. Pyro5-5.15/docs/source/conf.py000066400000000000000000000171711451404116400162200ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Pyro documentation build configuration file, created by # sphinx-quickstart on Thu Jun 16 22:20:40 2011. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os if sys.version_info < (3, 0): raise RuntimeError("Use python 3.x to format the docs. It contains Python 3.x code blocks that won't be syntaxhighlighted otherwise.") # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('../../')) import Pyro5 # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. needs_sphinx = '1.5.3' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. source_encoding = 'utf-8' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Pyro' copyright = u'Irmen de Jong' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = Pyro5.__version__ # The full version, including alpha/beta/rc tags. release = Pyro5.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. #import guzzle_sphinx_theme #html_theme_path = guzzle_sphinx_theme.html_theme_path() #html_theme = 'guzzle_sphinx_theme' html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. html_theme_options = { # "rightsidebar": True, # "bodyfont": "Tahoma,Helvetica,\"Helvetica Neue\",Arial,sans-serif", # "linkcolor": "#3070a0", # "visitedlinkcolor": "#3070a0", } # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. html_logo = "_static/pyro.png" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # (deprecated, use docutils.conf) # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. html_domain_indices = False # If false, no index is generated. html_use_index = True # If true, the index is split into individual pages for each letter. html_split_index = False # If true, links to the reST sources are added to the pages. html_show_sourcelink = False # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'Pyrodoc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'Pyro.tex', u'Pyro Documentation', u'Irmen de Jong', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'pyro', u'Pyro Documentation', [u'Irmen de Jong'], 1) ] def setup(app): # add custom css app.add_css_file("css/customize.css") from sphinx.ext.autodoc import cut_lines # skip the copyright line in every module docstring (last line of docstring) app.connect('autodoc-process-docstring', cut_lines(pre=0, post=1, what=['module'])) Pyro5-5.15/docs/source/config.rst000066400000000000000000000232421451404116400167140ustar00rootroot00000000000000.. index:: configuration **************** Configuring Pyro **************** Pyro can be configured using several *configuration items*. The current configuration is accessible from the ``Pyro5.config`` object, it contains all config items as attributes. You can read them and update them to change Pyro's configuration. (usually you need to do this at the start of your program). For instance, to enable message compression and change the server type, you add something like this to the start of your code:: Pyro5.config.COMPRESSION = True Pyro5.config.SERVERTYPE = "multiplex" .. index:: double: configuration; environment variables You can also set them outside of your program, using environment variables from the shell. **To avoid conflicts, the environment variables have a ``PYRO_`` prefix.** This means that if you want to change the same two settings as above, but by using environment variables, you would do something like:: $ export PYRO_COMPRESSION=true $ export PYRO_SERVERTYPE=multiplex (or on windows:) C:\> set PYRO_COMPRESSION=true C:\> set PYRO_SERVERTYPE=multiplex This environment defined configuration is simply used as initial values for Pyro's configuration object. Your code can still overwrite them by setting the items to other values, or by resetting the config as a whole. .. index:: reset config to default Resetting the config to default values -------------------------------------- .. method:: Pyro5.config.reset([use_environment=True]) Resets the configuration items to their builtin default values. If `use_environment` is True, it will overwrite builtin config items with any values set by environment variables. If you don't trust your environment, it may be a good idea to reset the config items to just the builtin defaults (ignoring any environment variables) by calling this method with `use_environment` set to False. Do this before using any other part of the Pyro library. .. index:: current config, pyro5-check-config Inspecting current config ------------------------- To inspect the current configuration you have several options: 1. Access individual config items: ``print(Pyro5.config.COMPRESSION)`` 2. Dump the config in a console window: :command:`python -m Pyro5.configure` (or simply :command:`pyro5-check-config`) This will print something like:: Pyro version: 5.10 Loaded from: /home/irmen/Projects/pyro5/Pyro5 Python version: CPython 3.8.2 (Linux, posix) Protocol version: 502 Currently active global configuration settings: BROADCAST_ADDRS = ['', '0.0.0.0'] COMMTIMEOUT = 0.0 COMPRESSION = False ... 3. Access the config as a dictionary: ``Pyro5.config.as_dict()`` 4. Access the config string dump (used in #2): ``Pyro5.config.dump()`` .. index:: configuration items .. _config-items: Overview of Config Items ------------------------ ========================= ======= ======================= ======= config item type default meaning ========================= ======= ======================= ======= COMMTIMEOUT float 0.0 Network communication timeout in seconds. 0.0=no timeout (infinite wait) COMPRESSION bool False Enable to make Pyro compress the data that travels over the network DETAILED_TRACEBACK bool False Enable to get detailed exception tracebacks (including the value of local variables per stack frame) HOST str localhost Hostname where Pyro daemons will bind on MAX_MESSAGE_SIZE int 1073741824 (1 Gb) Maximum size in bytes of the messages sent or received on the wire. If a message exceeds this size, a ProtocolError is raised. NS_HOST str *equal to HOST* Hostname for the name server. Used for locating in clients only (use the normal HOST config item in the name server itself) NS_PORT int 9090 TCP port of the name server. Used by the server and for locating in clients. NS_BCPORT int 9091 UDP port of the broadcast responder from the name server. Used by the server and for locating in clients. NS_BCHOST str None Hostname for the broadcast responder of the name server. Used by the server only. NS_AUTOCLEAN float 0.0 Specify a recurring period in seconds where the Name server checks its registrations and removes the ones that are not available anymore. (0=disabled, otherwise should be >=3) NS_LOOKUP_DELAY float 0.0 The max. number of seconds a name lookup will wait until the name becomes available in the nameserver (client-side retry) NATHOST str None External hostname in case of NAT (used by the server) NATPORT int 0 External port in case of NAT (used by the server) 0=replicate internal port number as NAT port BROADCAST_ADDRS str , 0.0.0.0 List of comma separated addresses that Pyro should send broadcasts to (for NS locating in clients) ONEWAY_THREADED bool True Enable to make oneway calls be processed in their own separate thread POLLTIMEOUT float 2.0 For the multiplexing server only: the timeout of the select or poll calls SERVERTYPE str thread Select the Pyro server type. thread=thread pool based, multiplex=select/poll/kqueue based SOCK_REUSE bool True Should SO_REUSEADDR be used on sockets that Pyro creates. SOCK_NODELAY bool False Use tcp_nodelay on sockets PREFER_IP_VERSION int 0 The IP address type that is preferred (4=ipv4, 6=ipv6, 0=let OS decide). SERPENT_BYTES_REPR bool False If True, use Python's repr format to serialize bytes types, rather than the base-64 encoding format. THREADPOOL_SIZE int 80 For the thread pool server: maximum number of threads running THREADPOOL_SIZE_MIN int 4 For the thread pool server: minimum number of threads running SERIALIZER str serpent The wire protocol serializer to use for clients/proxies (one of: serpent, json, marshal, msgpack) LOGWIRE bool False If wire-level message data should be written to the logfile (you may want to disable COMPRESSION) MAX_RETRIES int 0 Automatically retry network operations for some exceptions (timeout / connection closed), be careful to use when remote functions have a side effect (e.g.: calling twice results in error) ITER_STREAMING bool True Should iterator item streaming support be enabled in the server (default=True) ITER_STREAM_LIFETIME float 0.0 Maximum lifetime in seconds for item streams (default=0, no limit - iterator only stops when exhausted or client disconnects) ITER_STREAM_LINGER float 30.0 Linger time in seconds to keep an item stream alive after proxy disconnects (allows to reconnect to stream) SSL bool False Should SSL/TSL communication security be used? Enabling it also requires some other SSL config items to be set. SSL_SERVERCERT str *empty str* Location of the server's certificate file SSL_SERVERKEY str *empty str* Location of the server's private key file SSL_SERVERKEYPASSWD str *empty str* Password for the server's private key SSL_REQUIRECLIENTCERT bool False Should the server require clients to connect with their own certificate (2-way-ssl) SSL_CLIENTCERT str *empty str* Location of the client's certificate file SSL_CLIENTKEY str *empty str* Location of the client's private key file SSL_CLIENTKEYPASSWD str *empty str* Password for the client's private key SSL_CACERTS str *empty str* Location of a 'CA' signing certificate (or a directory containing these in PEM format, `"following an OpenSSL specific layout" `_.) ========================= ======= ======================= ======= .. index:: double: configuration items; logging There are two special config items that control Pyro's logging, and that are only available as environment variable settings. This is because they are used at the moment the Pyro5 package is being imported (which means that modifying them as regular config items after importing Pyro5 is too late and won't work). It is up to you to set the environment variable you want to the desired value. You can do this from your OS or shell, or perhaps by modifying ``os.environ`` in your Python code *before* importing Pyro5. ======================= ======= ============== ======= environment variable type default meaning ======================= ======= ============== ======= PYRO_LOGLEVEL string *not set* The log level to use for Pyro's logger (DEBUG, WARN, ...) See Python's standard :py:mod:`logging` module for the allowed values. If it is not set, no logging is being configured. PYRO_LOGFILE string pyro.log The name of the log file. Use {stderr} to make the log go to the standard error output. ======================= ======= ============== ======= Pyro5-5.15/docs/source/docutils.conf000066400000000000000000000000541451404116400174060ustar00rootroot00000000000000[restructuredtext parser] smart_quotes=true Pyro5-5.15/docs/source/errors.rst000066400000000000000000000145421451404116400167660ustar00rootroot00000000000000.. index:: exceptions, remote traceback ******************************** Exceptions and remote tracebacks ******************************** There is an example that shows various ways to deal with exceptions when writing Pyro code. Have a look at the `exceptions example `_ . Pyro exceptions --------------- Pyro's exception classes can be found in :mod:`Pyro5.errors`. They are used by Pyro itself if something went wrong inside Pyro itself or related to something Pyro was doing. All errors are of type ``PyroError`` or a subclass thereof. .. index:: remote errors Remote exceptions ----------------- More interesting are how Pyro treats exeptions that occur in *your own* objects (the remote Pyro objects): it is making the remote objects appear as normal, local, Python objects. That also means that if they raise an error, Pyro will make it appear in the caller (client progam), as if the error occurred locally at the point of the call. Assume you have a remote object that can divide arbitrary numbers. It will raise a ``ZeroDivisionError`` when using 0 as the divisor. This can be dealt with by just catching the exception as if you were writing regular code:: import Pyro5.api divider=Pyro5.api.Proxy( ... ) try: result = divider.div(999,0) except ZeroDivisionError: print("yup, it crashed") Since the error occurred in a *remote* object, and Pyro itself raises it again on the client side, some information is initially lost: the actual traceback of the crash itself in the server code. Pyro stores the traceback information on a special attribute on the exception object (``_pyroTraceback``), as a list of strings (each is a line from the traceback text, including newlines). You can use this data on the client to print or process the traceback text from the exception as it occurred in the Pyro object on the server. There is a utility function in :mod:`Pyro5.errors` to make it easy to deal with this: :func:`Pyro5.errors.get_pyro_traceback` You use it like this:: import Pyro5.errors try: result = proxy.method() except Exception: print("Pyro traceback:") print("".join(Pyro5.errors.get_pyro_traceback())) .. index:: exception hook Also, there is another function that you can install in ``sys.excepthook``, if you want Python to automatically print the complete Pyro traceback including the remote traceback, if any: :func:`Pyro5.errors.excepthook` A full Pyro exception traceback, including the remote traceback on the server, looks something like this:: Traceback (most recent call last): File "client.py", line 54, in print(test.complexerror()) # due to the excepthook, the exception will show the pyro error File "/home/irmen/Projects/pyro5/Pyro5/client.py", line 476, in __call__ return self.__send(self.__name, args, kwargs) File "/home/irmen/Projects/pyro5/Pyro5/client.py", line 243, in _pyroInvoke raise data # if you see this in your traceback, you should probably inspect the remote traceback as well TypeError: unsupported operand type(s) for //: 'str' and 'int' +--- This exception occured remotely (Pyro) - Remote traceback: | Traceback (most recent call last): | File "/home/irmen/Projects/pyro5/Pyro5/server.py", line 466, in handleRequest | data = method(*vargs, **kwargs) # this is the actual method call to the Pyro object | File "/home/irmen/Projects/pyro5/examples/exceptions/excep.py", line 24, in complexerror | x.crash() | File "/home/irmen/Projects/pyro5/examples/exceptions/excep.py", line 32, in crash | self.crash2('going down...') | File "/home/irmen/Projects/pyro5/examples/exceptions/excep.py", line 36, in crash2 | x = arg // 2 | TypeError: unsupported operand type(s) for //: 'str' and 'int' +--- End of remote traceback As you can see, the first part is only the exception as it occurs locally on the client (raised by Pyro). The indented part marked with 'Remote traceback' is the exception as it occurred in the remote Pyro object. .. index:: traceback information Detailed traceback information ------------------------------ There is another utility that Pyro has to make it easier to debug remote object exceptions. If you enable the ``DETAILED_TRACEBACK`` config item on the server (see :ref:`config-items`), the remote traceback is extended with details of the values of the local variables in every frame:: +--- This exception occured remotely (Pyro) - Remote traceback: | ---------------------------------------------------- | EXCEPTION : unsupported operand type(s) for //: 'str' and 'int' | Extended stacktrace follows (most recent call last) | ---------------------------------------------------- | File "/home/irmen/Projects/pyro5/Pyro5/server.py", line 466, in Daemon.handleRequest | Source code: | data = method(*vargs, **kwargs) # this is the actual method call to the Pyro object | ---------------------------------------------------- | File "/home/irmen/Projects/pyro5/examples/exceptions/excep.py", line 24, in TestClass.complexerror | Source code: | x.crash() | Local values: | self = | x = | ---------------------------------------------------- | File "/home/irmen/Projects/pyro5/examples/exceptions/excep.py", line 32, in Foo.crash | Source code: | self.crash2('going down...') | Local values: | self = | ---------------------------------------------------- | File "/home/irmen/Projects/pyro5/examples/exceptions/excep.py", line 36, in Foo.crash2 | Source code: | x = arg // 2 | Local values: | arg = 'going down...' | self = | ---------------------------------------------------- | EXCEPTION : unsupported operand type(s) for //: 'str' and 'int' | ---------------------------------------------------- +--- End of remote traceback You can immediately see why the call produced a ``TypeError`` without the need to have a debugger running (the ``arg`` variable is a string and dividing that string by 2 is the cause of the error). Pyro5-5.15/docs/source/index.rst000066400000000000000000000030371451404116400165560ustar00rootroot00000000000000**************************************** Pyro - Python Remote Objects - |version| **************************************** .. image:: _static/pyro-large.png :align: center :alt: PYRO logo Manual :ref:`genindex` .. index:: what is Pyro What is Pyro? ------------- A library that enables you to build applications in which objects can talk to each other over the network, with minimal programming effort. You can just use normal Python method calls to call objects running on other machines. Pyro is a pure Python library and runs on many different platforms and Python versions. Pyro is copyright © Irmen de Jong (irmen@razorvine.net | http://www.razorvine.net). Please read :doc:`license`. Pyro can be found on Pypi as `Pyro5 `_. Source is on Github: https://github.com/irmen/Pyro5 Pyro5 is the current version of Pyro. `Pyro4 `_ is the predecessor that only gets important bugfixes and security fixes, but is otherwise no longer being improved. New code should use Pyro5 if at all possible. .. toctree:: :maxdepth: 2 :caption: Contents of this manual: intro.rst install.rst tutorials.rst commandline.rst clientcode.rst servercode.rst nameserver.rst security.rst errors.rst tipstricks.rst config.rst api.rst pyrolite.rst changelog.rst license.rst Index ===== * :ref:`genindex` * :ref:`search` .. figure:: _static/tf_pyrotaunt.png :target: http://wiki.teamfortress.com/wiki/Pyro :alt: PYYYRRRROOOO :align: center Pyro5-5.15/docs/source/install.rst000066400000000000000000000046001451404116400171120ustar00rootroot00000000000000.. index:: installing Pyro *************** Installing Pyro *************** This chapter will show how to obtain and install Pyro. .. index:: double: installing Pyro; requirements for Pyro Compatibility ------------- Pyro is written in 100% Python. It works on any recent operating system where a suitable supported Python implementation is available (3.7 or newer). .. index:: double: installing Pyro; obtaining Pyro Obtaining and installing Pyro ----------------------------- **Linux** Some Linux distributions may offer Pyro5 through their package manager. Make sure you install the correct one for the python version that you are using. It may be more convenient to just pip install it instead in a virtualenv. **Anaconda** Anaconda users can install the Pyro5 package from conda-forge using ``conda install -c conda-forge pyro5`` **Pip install** ``pip install Pyro5`` should do the trick. Pyro is available `here on pypi `_ . **Manual installation from source** Download the source distribution archive (Pyro5-X.YZ.tar.gz) from Pypi or from a `Github release `_, extract it and ``python setup.py install``. The `serpent `_ serialization library must also be installed. **Github** Source is on Github: https://github.com/irmen/Pyro5 The required serpent serializer library is there as well: https://github.com/irmen/Serpent Third party libraries that Pyro5 uses ------------------------------------- `serpent `_ - required, 1.27 or newer Should be installed automatically when you install Pyro. `msgpack `_ - optional, 0.5.2 or newer Install this to use the msgpack serializer. Interesting stuff that is extra in the source distribution archive and not with packaged versions ------------------------------------------------------------------------------------------------- If you decide to download the distribution (.tar.gz) you have a bunch of extras over simply installing the Pyro library directly: examples/ dozens of examples that demonstrate various Pyro features (highly recommended to examine these, many paragraphs in this manual refer to relevant examples here) tests/ the unittest suite that checks for correctness and regressions Pyro5-5.15/docs/source/intro.rst000066400000000000000000000417321451404116400166060ustar00rootroot00000000000000***************** Intro and Example ***************** .. image:: _static/pyro-large.png :align: center This chapter contains a little overview of Pyro's features and a simple example to show how it looks like. .. index:: features Features ======== Pyro enables you to build applications in which objects can talk to each other over the network, with minimal programming effort. You can just use normal Python method calls, and Pyro takes care of locating the right object on the right computer to execute the method. It is designed to be very easy to use, and to stay out of your way. But it also provides a set of powerful features that enables you to build distributed applications rapidly and effortlessly. Pyro is a pure Python library and runs on many different platforms and Python versions. Here's a quick overview of Pyro's features: - written in 100% Python so extremely portable, runs on Python 3.x and also Pypy3 - works between different system architectures and operating systems. - able to communicate between different Python versions transparently. - defaults to a safe serializer (`serpent `_) that supports many Python data types. - supports different serializers (serpent, json, marshal, msgpack). - can use IPv4, IPv6 and Unix domain sockets. - optional secure connections via SSL/TLS (encryption, authentication and integrity), including certificate validation on both ends (2-way ssl). - lightweight client library available for .NET and Java native code ('Pyrolite', provided separately). - designed to be very easy to use and get out of your way as much as possible, but still provide a lot of flexibility when you do need it. - name server that keeps track of your object's actual locations so you can move them around transparently. - yellow-pages type lookups possible, based on metadata tags on registrations in the name server. - support for automatic reconnection to servers in case of interruptions. - automatic proxy-ing of Pyro objects which means you can return references to remote objects just as if it were normal objects. - one-way invocations for enhanced performance. - batched invocations for greatly enhanced performance of many calls on the same object. - remote iterator on-demand item streaming avoids having to create large collections upfront and transfer them as a whole. - you can define timeouts on network communications to prevent a call blocking forever if there's something wrong. - remote exceptions will be raised in the caller, as if they were local. You can extract detailed remote traceback information. - http gateway available for clients wanting to use http+json (such as browser scripts). - stable network communication code that has worked reliably on many platforms for over a decade. - can hook onto existing sockets created for instance with socketpair() to communicate efficiently between threads or sub-processes. - possibility to integrate Pyro's event loop into your own (or third party) event loop. - three different possible instance modes for your remote objects (singleton, one per session, one per call). - many simple examples included to show various features and techniques. - large amount of unit tests and high test coverage. - reliable and established: built upon more than 20 years of existing Pyro history, with ongoing support and development. .. index:: usage What can you use Pyro for? ========================== Essentially, Pyro can be used to distribute and integrate various kinds of resources or responsibilities: computational (hardware) resources (cpu, storage, printers), informational resources (data, privileged information) and business logic (departments, domains). An example would be a high performance compute cluster with a large storage system attached to it. Usually this is not accessible directly, rather, smaller systems connect to it and feed it with jobs that need to run on the big cluster. Later, they collect the results. Pyro could be used to expose the available resources on the cluster to other computers. Their client software connects to the cluster and calls the Python program there to perform its heavy duty work, and collect the results (either directly from a method call return value, or perhaps via asynchronous callbacks). Remote controlling resources or other programs is a nice application as well. For instance, you could write a simple remote controller for your media server that is running on a machine somewhere in a closet. A simple remote control client program could be used to instruct the media server to play music, switch playlists, etc. Another example is the use of Pyro to implement a form of `privilege separation `_. There is a small component running with higher privileges, but just able to execute the few tasks (and nothing else) that require those higher privileges. That component could expose one or more Pyro objects that represent the privileged information or logic. Other programs running with normal privileges can talk to those Pyro objects to perform those specific tasks with higher privileges in a controlled manner. Finally, Pyro can be a communication glue library to easily integrate various pars of a heterogeneous system, consisting of many different parts and pieces. As long as you have a working (and supported) Python version running on it, you should be able to talk to it using Pyro from any other part of the system. Have a look at the `examples directory `_ in the source, perhaps one of the many example programs in there gives even more inspiration of possibilities. .. index:: upgrading from Pyro4 Upgrading from Pyro4 ==================== Pyro5 is the current version. It is based on most of the concepts of Pyro4, but includes some major improvements. Using it should be very familiar to current Pyro4 users, however Pyro5 is not compatible with Pyro4 and vice versa. To allow graceful upgrading, both versions can co-exist due to the new package name (the same happened years ago when Pyro 3 was upgraded to Pyro4). Pyro5 provides a basic backward-compatibility module so much of existing Pyro4 code doesn't have to change (apart from adding a single import statement). This only works for code that imported Pyro4 symbols from the Pyro4 module directly, instead of from one of Pyro4's sub modules. So, for instance: ``from Pyro4 import Proxy`` instead of: ``from Pyro4.core import Proxy``. *some* submodules are more or less emulated such as ``Pyro4.errors``, ``Pyro4.socketutil``. So you may first have to convert your old code to use the importing scheme to only import the Pyro4 module and not from its submodules, and then you should insert this at the top to enable the compatibility layer:: from Pyro5.compatibility import Pyro4 What has been changed since Pyro4 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If you're familiar with Pyro4, most of the things are the same in Pyro5. These are the changes though: - Supported on Python 3.8 or newer. - the Pyro5 API is redesigned and this library is not compatible with Pyro4 code (although everything should be familiar): - Pyro5 is the new package name - restructured the submodules, renamed some submodules (naming -> nameserver, message -> protocol, util -> serializers) - most classes and method names are the same or at least similar but may have been shuffled around to other modules - all toplevel functions are renamed to pep8 code style (but class method names are unchanged from Pyro4 for now) - instead of the global package namespace you should now ``import Pyro5.api`` if you want to have one place to access the most important things - *compatibility layer:* to make upgrading easier there's a (limited) Pyro4 compatibility layer, enable this by ``from Pyro5.compatibility import Pyro4`` at the top of your modules. Read the docstring of this module for more details. - Proxy moved from core to new client module - Daemon moved from core to new server module - no support for unsafe serializers AT ALL (pickle, dill, cloudpickle) - only safe serializers (serpent, marshal, json, msgpack) - for now, requires ``msgpack`` to be installed as well as ``serpent``. - no need anymore for the ability to configure the accepted serializers in a daemon, because of the previous change - removed some other obscure config items - removed all from future imports and all sys.version_info checks because we're Python 3 only - removed Flame (utils/flameserver.py, utils/flame.py) (although maybe the remote module access may come back in some form) - moved test.echoserver to utils.echoserver (next to httpgateway) - threadpool module moved into the same module as threadpool-server - moved the multiplex and thread socketservers modules into main package - no custom futures module anymore (you should use Python's own concurrent.futures instead) - async proxy removed (may come back but probably not directly integrated into the Proxy class) - batch calls now via client.BatchProxy, no convenience functions anymore ('batch') - nameserver storage option 'dbm' removed (only memory and sql possible now) - naming_storage module merged into nameserver module - no Hmac key anymore, use SSL and 2-way certs if you want true security - metadata in proxy can no longer be switched off - having to use the @expose decorator to expose classes or methods can no longer be switched off - @expose and other decorators moved from core to new server module - now prefers ipv6 over ipv4 if your os agrees - autoproxy always enabled for now (but this feature may be removed completely though) - values from constants module scattered to various other more relevant modules - util traceback and excepthook functions moved to errors module - util methods regarding object/class inspection moved to new server module - rest of util module renamed to serializers module - replaced deprecated usages of optparse with argparse - moved metadata search in the name server to a separate yplookup method (instead of using list as well) - proxy doesn't have a thread lock anymore and no can longer be shared across different threads. A single thread is the sole "owner" of a proxy. Another thread can use proxy._pyroClaimOwnership to take over. - simplified serializers by moving the task of compressing data to the protocol module instead (where it belonged) - optimized wire messages (less code, sometimes less data copying by using memoryviews, no more checksumming) - much larger annotations possible (4Gb instead of 64Kb) so it can be (ab)used for things like efficient binary data transfer - annotations on the protocol message are now stored as no-copy memoryviews. A memoryview doesn't support all methods you might expect so sometimes it may be required now to convert it to bytes or bytearray in your own code first, before further processing. Note that this will create a copy again, so it's best avoided. .. index:: example Simple Example ============== This example will show you in a nutshell what it's like to use Pyro in your programs. A much more extensive introduction is found in the :doc:`tutorials`. Here, we're making a simple greeting service that will return a personalized greeting message to its callers. First let's see the server code:: # saved as greeting-server.py import Pyro5.api @Pyro5.api.expose class GreetingMaker(object): def get_fortune(self, name): return "Hello, {0}. Here is your fortune message:\n" \ "Behold the warranty -- the bold print giveth and the fine print taketh away.".format(name) daemon = Pyro5.api.Daemon() # make a Pyro daemon uri = daemon.register(GreetingMaker) # register the greeting maker as a Pyro object print("Ready. Object uri =", uri) # print the uri so we can use it in the client later daemon.requestLoop() # start the event loop of the server to wait for calls Open a console window and start the greeting server:: $ python greeting-server.py Ready. Object uri = PYRO:obj_fbfd1d6f83e44728b4bf89b9466965d5@localhost:35845 Great, our server is running. Let's see the client code that invokes the server:: # saved as greeting-client.py import Pyro5.api uri = input("What is the Pyro uri of the greeting object? ").strip() name = input("What is your name? ").strip() greeting_maker = Pyro5.api.Proxy(uri) # get a Pyro proxy to the greeting object print(greeting_maker.get_fortune(name)) # call method normally Start this client program (from a different console window):: $ python greeting-client.py What is the Pyro uri of the greeting object? <> What is your name? <> Hello, Irmen. Here is your fortune message: Behold the warranty -- the bold print giveth and the fine print taketh away. As you can see the client code called the greeting maker that was running in the server elsewhere, and printed the resulting greeting string. With a name server ^^^^^^^^^^^^^^^^^^ While the example above works, it could become tiresome to work with object uris like that. There's already a big issue, *how is the client supposed to get the uri, if we're not copy-pasting it?* Thankfully Pyro provides a *name server* that works like an automatic phone book. You can name your objects using logical names and use the name server to search for the corresponding uri. We'll have to modify a few lines in :file:`greeting-server.py` to make it register the object in the name server:: # saved as greeting-server.py import Pyro5.api @Pyro5.api.expose class GreetingMaker(object): def get_fortune(self, name): return "Hello, {0}. Here is your fortune message:\n" \ "Tomorrow's lucky number is 12345678.".format(name) daemon = Pyro5.server.Daemon() # make a Pyro daemon ns = Pyro5.api.locate_ns() # find the name server uri = daemon.register(GreetingMaker) # register the greeting maker as a Pyro object ns.register("example.greeting", uri) # register the object with a name in the name server print("Ready.") daemon.requestLoop() # start the event loop of the server to wait for calls The :file:`greeting-client.py` is actually simpler now because we can use the name server to find the object:: # saved as greeting-client.py import Pyro5.api name = input("What is your name? ").strip() greeting_maker = Pyro5.api.Proxy("PYRONAME:example.greeting") # use name server object lookup uri shortcut print(greeting_maker.get_fortune(name)) The program now needs a Pyro name server that is running. You can start one by typing the following command: :command:`python -m Pyro5.nameserver` (or simply: :command:`pyro5-ns`) in a separate console window (usually there is just *one* name server running in your network). After that, start the server and client as before. There's no need to copy-paste the object uri in the client any longer, it will 'discover' the server automatically, based on the object name (:kbd:`example.greeting`). If you want you can check that this name is indeed known in the name server, by typing the command :command:`python -m Pyro5.nsc list` (or simply: :command:`pyro5-nsc list`), which will produce:: $ pyro5-nsc list --------START LIST Pyro.NameServer --> PYRO:Pyro.NameServer@localhost:9090 metadata: {'class:Pyro5.nameserver.NameServer'} example.greeting --> PYRO:obj_198af10aa51f4fa8ab54062e65fad96a@localhost:44687 --------END LIST (Once again the uri for our object will be random) This concludes this simple Pyro example. .. note:: In the source code there is an `examples directory `_ that contains a truckload of example programs that show the various features of Pyro. If you're interested in them (it is highly recommended to be so!) you will have to download the Pyro distribution archive. Installing Pyro only provides the library modules. For more information, see :doc:`config`. Other means of creating connections ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The example above showed two of the basic ways to set up connections between your client and server code. There are various other options, have a look at the client code details: :ref:`object-discovery` and the server code details: :ref:`publish-objects`. The use of the name server is optional, see :ref:`name-server` for details. .. index:: performance, benchmark Performance =========== Pyro is pretty fast, but speed depends largely on many external factors: - network connection speed - machine and operating system - I/O or CPU bound workload - contents and size of the pyro call request and response messages - the serializer being used Experiment with the `benchmark `_ , `batchedcalls `_ and `hugetransfer `_ examples to see what results you get on your own setup. Pyro5-5.15/docs/source/license.rst000066400000000000000000000027471451404116400171000ustar00rootroot00000000000000 .. index:: software license, license, disclaimer ******************************* Software License and Disclaimer ******************************* Pyro - Python Remote Objects - version 5.x - Copyright (c) by Irmen de Jong (irmen@razorvine.net). Pyro is licensed under the `MIT Software License `_: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. .. figure:: _static/tf_pyrotaunt.png :target: http://wiki.teamfortress.com/wiki/Pyro :alt: PYYYRRRROOOO :align: center Pyro5-5.15/docs/source/nameserver.rst000066400000000000000000000725151451404116400176250ustar00rootroot00000000000000.. index:: Name Server .. _name-server: *********** Name Server *********** The Pyro Name Server is a tool to help keeping track of your objects in your network. It is also a means to give your Pyro objects logical names instead of the need to always know the exact object name (or id) and its location. Pyro will name its objects like this:: PYRO:obj_dcf713ac20ce4fb2a6e72acaeba57dfd@localhost:51850 PYRO:custom_name@localhost:51851 It's either a generated unique object id on a certain host, or a name you chose yourself. But to connect to these objects you'll always need to know the exact object name or id and the exact hostname and port number of the Pyro daemon where the object is running. This can get tedious, and if you move servers around (or Pyro objects) your client programs can no longer connect to them until you update all URIs. Enter the *name server*. This is a simple phone-book like registry that maps logical object names to their corresponding URIs. No need to remember the exact URI anymore. Instead, you can ask the name server to look it up for you. You only need to give it the logical object name. .. note:: Usually you only need to run *one single instance* of the name server in your network. You can start multiple name servers but they are unconnected; you'll end up with a partitioned name space. **Example scenario:** Assume you've got a document archive server that publishes a Pyro object with several archival related methods in it. This archive server can register this object with the name server, using a logical name such as "Department.ArchiveServer". Any client can now connect to it using only the name "Department.ArchiveServer". They don't need to know the exact Pyro id and don't even need to know the location. This means you can move the archive server to another machine and as long as it updates its record in the name server, all clients won't notice anything and can keep on running without modification. .. index:: starting the name server double: name server; command line .. _nameserver-nameserver: Starting the Name Server ======================== The easiest way to start a name server is by using the command line tool. synopsys: :command:`python -m Pyro5.nameserver [options]` (or simply: :command:`pyro5-ns [options]`) Starts the Pyro Name Server. It can run without any arguments but there are several that you can use, for instance to control the hostname and port that the server is listening on. A short explanation of the available options can be printed with the help option. When it starts, it prints a message similar to this ('neptune' is the hostname of the machine it is running on):: $ pyro5-ns -n neptune Broadcast server running on 0.0.0.0:9091 NS running on neptune:9090 (192.168.178.20) URI = PYRO:Pyro.NameServer@neptune:9090 As you can see it prints that it started a broadcast server (and its location), a name server (and its location), and it also printed the URI that clients can use to access it directly. The nameserver uses a fast but volatile in-memory database by default. With a command line argument you can select a persistent storage mechanism (see below). If you're using that, your registrations will not be lost when the nameserver stops/restarts. The server will print the number of existing registrations at startup time if it discovers any. .. note:: Pyro by default binds its servers on localhost which means you cannot reach them from another machine on the network. This behavior also applies to the name server. If you want to be able to talk to the name server from other machines, you have to explicitly provide a hostname or non-loopback interface to bind on. There are several command line options for this tool: .. program:: Pyro5.nameserver .. option:: -h, --help Print a short help message and exit. .. option:: -n HOST, --host=HOST Specify hostname or ip address to bind the server on. The default is localhost, note that your name server will then not be visible from the network If the server binds on localhost, *no broadcast responder* is started either. Make sure to provide a hostname or ip address to make the name server reachable from other machines, if you want that. .. option:: -p PORT, --port=PORT Specify port to bind server on (0=random). .. option:: -u UNIXSOCKET, --unixsocket=UNIXSOCKET Specify a Unix domain socket name to bind server on, rather than a normal TCP/IP socket. .. option:: --bchost=BCHOST Specify the hostname or ip address to bind the broadcast responder on. Note: if the hostname where the name server binds on is localhost (or 127.0.x.x), no broadcast responder is started. .. option:: --bcport=BCPORT Specify the port to bind the broadcast responder on (0=random). .. option:: --nathost=NATHOST Specify the external host name to use in case of NAT .. option:: --natport=NATPORT Specify the external port use in case of NAT .. option:: -s STORAGE, --storage=STORAGE Specify the storage mechanism to use. You have several options: - ``memory`` - fast, volatile in-memory database. This is the default. - ``dbm:dbfile`` - dbm-style persistent database table. Provide the filename to use. This storage type does not support metadata. - ``sql:sqlfile`` - sqlite persistent database. Provide the filename to use. .. option:: -x, --nobc Don't start a broadcast responder. Clients will not be able to use the UDP-broadcast lookup to discover this name server. (The broadcast responder listens to UDP broadcast packets on the local network subnet, to signal its location to clients that want to talk to the name server) Starting the Name Server from within your own code ================================================== Another way to start up a name server is by doing it from within your own code. This is more complex than simply launching it via the command line tool, because you have to integrate the name server into the rest of your program (perhaps you need to merge event loops?). For your convenience, two helper functions are available to create a name server yourself: :py:func:`Pyro5.nameserver.start_ns` and :py:func:`Pyro5.nameserver.start_ns_loop`. Look at the `eventloop example `_ to see how you can use this. **Custom storage mechanism:** The utility functions allow you to specify a custom storage mechanism (via the ``storage`` parameter). By default the in memory storage :py:class:`Pyro5.nameserver.MemoryStorage` is used. In the :py:mod:`Pyro5.nameserver` module you can find the other implementation (sqlite). You could also build your own, as long as it has the same interface. .. index:: double: name server; configuration items Configuration items =================== There are a couple of config items related to the nameserver. They are used both by the name server itself (to configure the values it will use to start the server with), and the client code that locates the name server (to give it optional hints where the name server is located). Often these can be overridden with a command line option or with a method parameter in your code. ================== =========== Configuration item description ================== =========== HOST hostname that the name server will bind on (being a regular Pyro daemon). NS_HOST the hostname or ip address of the name server. Used for locating in clients only. NS_PORT the port number of the name server. Used by the server and for locating in clients. NS_BCHOST the hostname or ip address of the name server's broadcast responder. Used only by the server. NS_BCPORT the port number of the name server's broadcast responder. Used by the server and for locating in clients. NATHOST the external hostname in case of NAT. Used only by the server. NATPORT the external port in case of NAT. Used only by the server. NS_AUTOCLEAN a recurring period in seconds where the Name server checks its registrations, and removes the ones that are no longer available. Defaults to 0.0 (off). ================== =========== .. index:: double: name server; name server control .. _nameserver-nsc: Name server control tool ======================== The name server control tool (or 'nsc') is used to talk to a running name server and perform diagnostic or maintenance actions such as querying the registered objects, adding or removing a name registration manually, etc. synopsis: :command:`python -m Pyro5.nsc [options] command [arguments]` (or simply: :command:`pyro5-nsc [options] command [arguments]`) .. program:: Pyro5.nsc .. option:: -h, --help Print a short help message and exit. .. option:: -n HOST, --host=HOST Provide the hostname or ip address of the name server. The default is to do a broadcast lookup to search for a name server. .. option:: -p PORT, --port=PORT Provide the port of the name server, or its broadcast port if you're doing a broadcast lookup. .. option:: -u UNIXSOCKET, --unixsocket=UNIXSOCKET Provide the Unix domain socket name of the name server, rather than a normal TCP/IP socket. .. option:: -v, --verbose Print more output that could be useful. The available commands for this tool are: list : list [prefix] List all objects with their metadata registered in the name server. If you supply a prefix, the list will be filtered to show only the objects whose name starts with the prefix. listmatching : listmatching pattern List only the objects with a name matching the given regular expression pattern. lookup : lookup name Looks up a single name registration and prints the uri. yplookup_all : yplookup_all metadata [metadata...] List the objects having *all* of the given metadata tags yplookup_any : yplookup_any metadata [metadata...] List the objects having *any one* (or multiple) of the given metadata tags register : register name uri Registers a name to the given Pyro object :abbr:`URI (universal resource identifier)`. remove : remove name Removes the entry with the exact given name from the name server. removematching : removematching pattern Removes all entries matching the given regular expression pattern. setmeta : setmeta name [metadata...] Sets the new list of metadata tags for the given Pyro object. If you don't specify any metadata tags, the metadata of the object is cleared. ping Does nothing besides checking if the name server is running and reachable. Example:: $ pyro5-nsc ping Name server ping ok. $ pyro5-nsc list Pyro --------START LIST - prefix 'Pyro' Pyro.NameServer --> PYRO:Pyro.NameServer@localhost:9090 metadata: {'class:Pyro5.nameserver.NameServer'} --------END LIST - prefix 'Pyro' .. index:: double: name server; locating the name server Locating the Name Server and using it in your code ================================================== The name server is a Pyro object itself, and you access it through a normal Pyro proxy. The object exposed is :class:`Pyro5.nameserver.NameServer`. Getting a proxy for the name server is done using the following function: :func:`Pyro5.core.locate_ns` (also available as :func:`Pyro5.api.locate_ns`). .. index:: double: name server; broadcast lookup By far the easiest way to locate the Pyro name server is by using the broadcast lookup mechanism. This goes like this: you simply ask Pyro to look up the name server and return a proxy for it. It automatically figures out where in your subnet it is running by doing a broadcast and returning the first Pyro name server that responds. The broadcast is a simple UDP-network broadcast, so this means it usually won't travel outside your network subnet (or through routers) and your firewall needs to allow UDP network traffic. There is a config item ``BROADCAST_ADDRS`` that contains a comma separated list of the broadcast addresses Pyro should use when doing a broadcast lookup. Depending on your network configuration, you may have to change this list to make the lookup work. It could be that you have to add the network broadcast address for the specific network that the name server is located on. .. note:: You can only talk to a name server on a different machine if it didn't bind on localhost (that means you have to start it with an explicit host to bind on). The broadcast lookup mechanism only works in this case as well -- it doesn't work with a name server that binds on localhost. For instance, the name server started as an example in :ref:`nameserver-nameserver` was told to bind on the host name 'neptune' and it started a broadcast responder as well. If you use the default host (localhost) a broadcast responder will not be created. Normally, all name server lookups are done this way. In code, it is simply calling the locator function without any arguments. If you want to circumvent the broadcast lookup (because you know the location of the server already, somehow) you can specify the hostname. As soon as you provide a specific hostname to the name server locator (by using a host argument to the ``locate_ns`` call, or by setting the ``NS_HOST`` config item, etc) it will no longer use a broadcast too try to find the name server. .. function:: locate_ns([host=None, port=None, broadcast=True]) Get a proxy for a name server somewhere in the network. If you're not providing host or port arguments, the configured defaults are used. Unless you specify a host, a broadcast lookup is done to search for a name server. (api reference: :py:func:`Pyro5.core.locate_ns`) :param host: the hostname or ip address where the name server is running. Default is ``None`` which means it uses a network broadcast lookup. If you specify a host, no broadcast lookup is performed. :param port: the port number on which the name server is running. Default is ``None`` which means use the configured default. The exact meaning depends on whether the host parameter is given: * host parameter given: the port now means the actual name server port. * host parameter not given: the port now means the broadcast port. :param broadcast: should a broadcast be used to locate the name server, if no location is specified? Default is True. .. index:: PYRONAME protocol type .. _nameserver-pyroname: The PYRONAME protocol type ========================== To create a proxy and connect to a Pyro object, Pyro needs an URI so it can find the object. Because it is so convenient, the name server logic has been integrated into Pyro's URI mechanism by means of the special ``PYRONAME`` protocol type (rather than the normal ``PYRO`` protocol type). This protocol type tells Pyro to treat the URI as a logical object name instead, and Pyro will do a name server lookup automatically to get the actual object's URI. The form of a PYRONAME uri is very simple:: PYRONAME:some_logical_object_name PYRONAME:some_logical_object_name@nshostname # with optional host name PYRONAME:some_logical_object_name@nshostname:nsport # with optional host name + port where "some_logical_object_name" is the name of a registered Pyro object in the name server. When you also provide the ``nshostname`` and perhaps even ``nsport`` parts in the uri, you tell Pyro to look for the name server on that specific location (instead of relying on a broadcast lookup mechanism). (You can achieve more or less the same by setting the ``NS_HOST`` and ``NS_PORT`` config items) All this means that instead of manually resolving objects like this:: nameserver=Pyro5.core.locate_ns() uri=nameserver.lookup("Department.BackupServer") proxy=Pyro5.client.Proxy(uri) proxy.backup() you can write this instead:: proxy=Pyro5.client.Proxy("PYRONAME:Department.BackupServer") proxy.backup() An additional benefit of using a PYRONAME uri in a proxy is that the proxy isn't strictly tied to a specific object on a specific location. This is useful in scenarios where the server objects might move to another location, for instance when a disconnect/reconnect occurs. See the `autoreconnect example `_ for more details about this. .. note:: Pyro has to do a lookup every time it needs to connect one of these PYRONAME uris. If you connect/disconnect many times or with many different objects, consider using PYRO uris (you can type them directly or create them by resolving as explained in the following paragraph) or call :meth:`Pyro5.core.Proxy._pyroBind()` on the proxy to bind it to a fixed PYRO uri instead. .. index:: PYROMETA protocol type .. _nameserver-pyrometa: The PYROMETA protocol type ========================== Next to the ``PYRONAME`` protocol type there is another 'magic' protocol ``PYROMETA``. This protocol type tells Pyro to treat the URI as metadata tags, and Pyro will ask the name server for any (randomly chosen) object that has the given metadata tags. The form of a PYROMETA uri is:: PYROMETA:metatag PYROMETA:metatag1,metatag2,metatag3 PYROMETA:metatag@nshostname # with optional host name PYROMETA:metatag@nshostname:nsport # with optional host name + port So you can write this to connect to any random printer (given that all Pyro objects representing a printer have been registered in the name server with the ``resource.printer`` metadata tag):: proxy=Pyro5.client.Proxy("PYROMETA:resource.printer") proxy.printstuff() You have to explicitly add metadata tags when registering objects with the name server, see :ref:`nameserver-yellowpages`. Objects without metadata tags cannot be found via ``PYROMETA`` obviously. Note that the name server supports more advanced metadata features than what ``PYROMETA`` provides: in a PYROMETA uri you cannot use white spaces, and you cannot ask for an object that has one or more of the given tags -- multiple tags means that the object must have all of them. Metadata tags can be listed if you query the name server for registrations. .. index:: resolving object names, PYRONAME protocol type Resolving object names ====================== 'Resolving an object name' means to look it up in the name server's registry and getting the actual URI that belongs to it (with the actual object name or id and the location of the daemon in which it is running). This is not normally needed in user code (Pyro takes care of it automatically for you), but it can still be useful in certain situations. So, resolving a logical name can be done in several ways: #. The easiest way: let Pyro do it for you! Simply pass a ``PYRONAME`` URI to the proxy constructor, and forget all about the resolving happening under the hood:: obj = Pyro5.client.Proxy("PYRONAME:objectname") obj.method() #. obtain a name server proxy and use its ``lookup`` method (:meth:`Pyro5.nameserver.NameServer.lookup`). You could then use this resolved uri to get an actual proxy, or do other things with it:: ns = Pyro5.core.locate_ns() uri = ns.lookup("objectname") # uri now is the resolved 'objectname' obj = Pyro5.client.Proxy(uri) obj.method() #. use a ``PYRONAME`` URI and resolve it using the ``resolve`` utility function :func:`Pyro5.core.resolve` (also available as :func:`Pyro5.api.resolve`):: uri = Pyro5.core.resolve("PYRONAME:objectname") # uri now is the resolved 'objectname' obj = Pyro5.client.Proxy(uri) obj.method() #. use a ``PYROMETA`` URI and resolve it using the ``resolve`` utility function :func:`Pyro5.core.resolve` (also available as :func:`Pyro5.api.resolve`):: uri = Pyro5.core.resolve("PYROMETA:metatag1,metatag2") # uri is now randomly chosen from all objects having the given meta tags obj = Pyro5.client.Proxy(uri) .. index:: double: name server; registering objects double: name server; unregistering objects .. _nameserver-registering: Registering object names ======================== 'Registering an object' means that you associate the URI with a logical name, so that clients can refer to your Pyro object by using that name. Your server has to register its Pyro objects with the name server. It first registers an object with the Daemon, gets an URI back, and then registers that URI in the name server using the following method on the name server proxy: .. py:method:: register(name, uri, safe=False) Registers an object (uri) under a logical name in the name server. :param name: logical name that the object will be known as :type name: string :param uri: the URI of the object (you get it from the daemon) :type uri: string or :class:`Pyro5.core.URI` :param safe: normally registering the same name twice silently overwrites the old registration. If you set safe=True, the same name cannot be registered twice. :type safe: bool You can unregister objects as well using the :py:meth:`unregister` method. The name server also supports automatically checking for registrations that are no longer available, for instance because the server process crashed or a network problem occurs. It will then automatically remove those registrations after a certain timeout period. This feature is disabled by default (it potentially requires the NS to periodically create a lot of network connections to check for each of the registrations if it is still available). You can enable it by setting the ``NS_AUTOCLEAN`` config item to a non zero value; it then specifies the recurring period in seconds for the nameserver to check all its registrations. Choose an appropriately large value, the minimum allowed is 3. .. index:: scaling Name Server connections Free connections to the NS quickly ================================== By default the Name server uses a Pyro socket server based on whatever configuration is the default. Usually that will be a threadpool based server with a limited pool size. If more clients connect to the name server than the pool size allows, they will get a connection error. It is suggested you apply the following pattern when using the name server in your code: #. obtain a proxy for the NS #. look up the stuff you need, store it #. free the NS proxy (See :ref:`client_cleanup`) #. use the uri's/proxies you've just looked up This makes sure your client code doesn't consume resources in the name server for an excessive amount of time, and more importantly, frees up the limited connection pool to let other clients get their turn. If you have a proxy to the name server and you let it live for too long, it may eventually deny other clients access to the name server because its connection pool is exhausted. So if you don't need the proxy anymore, make sure to free it up. There are a number of things you can do to improve the matter on the side of the Name Server itself. You can control its behavior by setting certain Pyro config items before starting the server: - You can set ``SERVERTYPE=multiplex`` to create a server that doesn't use a limited connection (thread) pool, but multiplexes as many connections as the system allows. However, the actual calls to the server must now wait on eachother to complete before the next call is processed. This may impact performance in other ways. - You can set ``THREADPOOL_SIZE`` to an even larger number than the default. - You can set ``COMMTIMEOUT`` to a certain value, which frees up unused connections after the given time. But the client code may now crash with a TimeoutError or ConnectionClosedError when it tries to use a proxy it obtained earlier. (You can use Pyro's autoreconnect feature to work around this but it makes the code more complex) .. index:: double: name server; Yellow-pages double: name server; Metadata .. _nameserver-yellowpages: Yellow-pages ability of the Name Server (metadata tags) ======================================================= You can tag object registrations in the name server with one or more Metadata tags. These are simple strings but you're free to put anything you want in it. One way of using it, is to provide a form of Yellow-pages object lookup: instead of directly asking for the registered object by its unique name (telephone book), you're asking for any registration from a certain *category*. You get back a list of registered objects from the queried category, from which you can then choose the one you want. .. note:: Metadata tags are case-sensitive. As an example, imagine the following objects registered in the name server (with the metadata as shown): =================== ======================= ======== Name Uri Metadata =================== ======================= ======== printer.secondfloor PYRO:printer1@host:1234 printer printer.hallway PYRO:printer2@host:1234 printer storage.diskcluster PYRO:disks1@host:1234 storage storage.ssdcluster PYRO:disks2@host:1234 storage =================== ======================= ======== Instead of having to know the exact name of a required object you can query the name server for all objects having a certain set of metadata. So in the above case, your client code doesn't have to 'know' that it needs to lookup the ``printer.hallway`` object to get the uri of a printer (in this case the one down in the hallway). Instead it can just ask for a list of all objects having the ``printer`` metadata tag. It will get a list containing both ``printer.secondfloor`` and ``printer.hallway`` so you will still have to choose the object you want to use - or perhaps even use both. The objects tagged with ``storage`` won't be returned. Arguably the most useful way to deal with the metadata is to use it for Yellow-pages style lookups. You can ask for all objects having some set of metadata tags, where you can choose if they should have *all* of the given tags or only *any one* (or more) of the given tags. Additional or other filtering must be done in the client code itself. So in the above example, querying with ``meta_any={'printer', 'storage'}`` will return all four objects, while querying with ``meta_all={'printer', 'storage'}`` will return an empty list (because there are no objects that are both a printer and storage). **Setting metadata in the name server** Object registrations in the name server by default have an empty set of metadata tags associated with them. However the ``register`` method (:meth:`Pyro5.nameserver.NameServer.register`) has an optional ``metadata`` argument, you can set that to a set of strings that will be the metadata tags associated with the object registration. For instance:: ns.register("printer.secondfloor", "PYRO:printer1@host:1234", metadata={"printer"}) **Getting metadata back from the name server** The ``lookup`` (:meth:`Pyro5.nameserver.NameServer.lookup`) and ``list`` (:meth:`Pyro5.nameserver.NameServer.list`) methods of the name server have an optional ``return_metadata`` argument. By default it is False, and you just get back the registered URI (lookup) or a dictionary with the registered names and their URI as values (list). If you set it to True however, you'll get back tuples instead: (uri, set-of-metadata-tags):: ns.lookup("printer.secondfloor", return_metadata=True) # returns: (, {'printer'}) ns.list(return_metadata=True) # returns something like: # {'printer.secondfloor': ('PYRO:printer1@host:1234', {'printer'}), # 'Pyro.NameServer': ('PYRO:Pyro.NameServer@localhost:9090', {'class:Pyro5.nameserver.NameServer'})} # (as you can see the name server itself has also been registered with a metadata tag) **Querying on metadata (Yellow-page lookup)** You can ask the name server to list all objects having some set of metadata tags. The ``yplookup`` (:meth:`Pyro5.nameserver.NameServer.yplookup`) method of the name server has two arguments to allow you do do this: ``meta_all`` and ``meta_any``. #. ``meta_all``: give all objects having *all* of the given metadata tags:: ns.yplookup(meta_all={"printer"}) # returns: {'printer.secondfloor': 'PYRO:printer1@host:1234'} ns.yplookup(meta_all={"printer", "communication"}) # returns: {} (there is no object that's both a printer and a communication device) #. ``meta_any``: give all objects having *one* (or more) of the given metadata tags:: ns.yplookup(meta_any={"storage", "printer", "communication"}) # returns: {'printer.secondfloor': 'PYRO:printer1@host:1234'} **Querying on metadata via ``PYROMETA`` uri (Yellow-page lookup in uri)** As a convenience, similar to the ``PYRONAME`` uri protocol, you can use the ``PYROMETA`` uri protocol to let Pyro do the lookup for you. It only supports ``meta_all`` lookup, but it allows you to conveniently get a proxy like this:: Pyro5.client.Proxy("PYROMETA:resource.printer,performance.fast") this will connect to a (randomly chosen) object with both the ``resource.printer`` and ``performance.fast`` metadata tags. Also see :ref:`nameserver-pyrometa`. You can find some code that uses the metadata API in the `ns-metadata example `_ . Note that the ``nsc`` tool (:ref:`nameserver-nsc`) also allows you to manipulate the metadata in the name server from the command line. .. index:: Name Server API Other methods in the Name Server API ==================================== The name server has a few other methods that might be useful at times. For instance, you can ask it for a list of all registered objects. Because the name server itself is a regular Pyro object, you can access its methods through a regular Pyro proxy, and refer to the description of the exposed class to see what methods are available: :class:`Pyro5.nameserver.NameServer`. Pyro5-5.15/docs/source/pyrolite.rst000066400000000000000000000006551451404116400173210ustar00rootroot00000000000000.. index:: Pyrolite, Java, .NET, C# ******************************************* Pyrolite - client library for Java and .NET ******************************************* This library allows your Java or .NET program to interface very easily with the Python world. It uses the Pyro protocol to call methods on remote objects. https://github.com/irmen/Pyrolite The 5.x version works with Pyro5. (Use the 4.x version for Pyro4). Pyro5-5.15/docs/source/security.rst000066400000000000000000000144441451404116400173220ustar00rootroot00000000000000.. index:: security .. _security: ******** Security ******** .. warning:: Do not publish any Pyro objects to remote machines unless you've read and understood everything that is discussed in this chapter. This is also true when publishing Pyro objects with different credentials to other processes on the same machine. Why? In short: using Pyro has several security risks. Pyro has a few countermeasures to deal with them. Understanding the risks, the countermeasures, and their limits, is very important to avoid creating systems that are very easy to compromise by malicious entities. .. index:: double: security; network interfaces Network interface binding ========================= By default Pyro binds every server on localhost, to avoid exposing things on a public network or over the internet by mistake. If you want to expose your Pyro objects to anything other than localhost, you have to explicitly tell Pyro the network interface address it should use. This means it is a conscious effort to expose Pyro objects to other machines. It is possible to tell Pyro the interface address via an environment variable or global config item (``HOST``). In some situations - or if you're paranoid - it is advisable to override this setting in your server program by setting the config item from within your own code, instead of depending on an externally configured setting. .. index:: double: security; different user id Running Pyro servers with different credentials/user id ======================================================= The following is not a Pyro specific problem, but is important nonetheless: If you want to run your Pyro server as a different user id or with different credentials as regular users, *be very careful* what kind of Pyro objects you expose like this! Treat this situation as if you're exposing your server on the internet (even when it's only running on localhost). Keep in mind that it is still possible that a random user on the same machine connects to the local server. You may need additional security measures to prevent random users from calling your Pyro objects. .. index:: SSL, TLS double: security; encryption Secure communication via SSL/TLS ================================ Pyro itself doesn't encrypt the data it sends over the network. This means if you use the default configuration, you must never transfer sensitive data on untrusted networks (especially user data, passwords, and such) because eavesdropping is possible. You can run Pyro over a secure network (VPN, ssl/ssh tunnel) where the encryption is taken care of externally. It is also possible however to enable SSL/TLS in Pyro itself, so that all communication is secured via this industry standard that provides encryption, authentication, and anti-tampering (message integrity). **Using SSL/TLS** Enable it by setting the ``SSL`` config item to True, and configure the other SSL config items as required. You'll need to specify the cert files to use, private keys, and passwords if any. By default, the SSL mode only has a cert on the server (which is similar to visiting a https url in your browser). This means your *clients* can be sure that they are connecting to the expected server, but the *server* has no way to know what clients are connecting. You can solve this using SSL and custom certificate verification. You can do this in your client (checks the server's cert) but you can also tell your clients to use certs as well and check these in your server. This makes it 2-way-SSL or mutual authentication. For more details see here :ref:`cert_verification`. The SSL config items are in :ref:`config-items`. For example code on how to set up a 2-way-SSL Pyro client and server, with cert verification, see the `ssl example `_ . .. index:: double: security; object traversal double: security; dotted names Dotted names (object traversal) =============================== Using "dotted names" to traverse attributes on Pyro proxies (like ``proxy.aaa.bbb.ccc()``) is not possible. because that is a security vulnerability (for similar reasons as described here https://legacy.python.org/news/security/PSF-2005-001/ ). If you require access to a nested attribute, you'll have to explicitly add a method or attribute on the proxy itself to access it directly. .. index:: double: security; environment variables Environment variables overriding config items ============================================= Almost all config items can be overwritten by an environment variable. If you can't trust the environment in which your script is running, it may be a good idea to reset the config items to their default builtin values, without using any environment variables. See :doc:`config` for the proper way to do this. Preventing arbitrary connections ================================ .. index:: certificate verification, 2-way-SSL .. _cert_verification: ...by using 2-way-SSL and certificate verificiation --------------------------------------------------- When using SSL, you should also do some custom certificate verification, such as checking the serial number and commonName. This way your code is not only certain that the communication is encrypted, but also that it is talking to the intended party and nobody else (middleman). The server hostname and cert expiration dates *are* checked automatically, but other attributes you have to verify yourself. This is fairly easy to do: you can use :ref:`conn_handshake` for this. You can then get the peer certificate using :py:meth:`Pyro5.socketutil.SocketConnection.getpeercert`. If you configure a client cert as well as a server cert, you can/should also do verification of client certificates in your server. This is a good way to be absolutely certain that you only allow clients that you know and trust, because you can check the required unique certificate attributes. Having certs on both client and server is called 2-way-SSL or mutual authentication. It's a bit too involved to fully describe here but it not much harder than the basic SSL configuration described earlier. You just have to make sure you supply a client certificate and that the server requires a client certificate (and verifies some properties of it). The `ssl example `_ shows how to do all this. Pyro5-5.15/docs/source/servercode.rst000066400000000000000000001173201451404116400176110ustar00rootroot00000000000000.. index:: server code ***************************** Servers: hosting Pyro objects ***************************** This chapter explains how you write code that publishes objects to be remotely accessible. These objects are then called *Pyro objects* and the program that provides them, is often called a *server* program. (The program that calls the objects is usually called the *client*. Both roles can be mixed in a single program.) Make sure you are familiar with Pyro's :ref:`keyconcepts` before reading on. .. seealso:: :doc:`config` for several config items that you can use to tweak various server side aspects. .. index:: single: decorators single: @Pyro5.server.expose single: @Pyro5.server.oneway double: decorator; expose double: decorator; oneway .. _decorating-pyro-class: Creating a Pyro class and exposing its methods and properties ============================================================= Exposing classes, methods and properties is done using the ``@Pyro5.server.expose`` decorator. It lets you mark the following items to be available for remote access: - methods (including classmethod and staticmethod). You cannot expose a 'private' method, i.e. name starting with underscore. You *can* expose a 'dunder' method with double underscore for example ``__len__``. There is a short list of dunder methods that will never be remoted though (because they are essential to let the Pyro proxy function correctly). Make sure you put the ``@expose`` decorator after other decorators on the method, if any. - properties (these will be available as remote attributes on the proxy) It's not possible to expose a 'private' property (name starting with underscore). You can't expose attributes directly. It is required to provide a @property for them and decorate that with ``@expose``, if you want to provide a remotely accessible attribute. - classes as a whole (exposing a class has the effect of exposing every nonprivate method and property of the class automatically) Anything that isn't decorated with ``@expose`` is not remotely accessible. .. important:: **Private methods and attributes**: In the spirit of being secure by default, Pyro doesn't allow remote access to anything of your class unless explicitly told to do so. It will never allow remote access to 'private' methods and attributes (where 'private' means that their name starts with a single or double underscore). There's a special exception for the regular 'dunder' names with double underscores such as ``__len__`` though. Here's a piece of example code that shows how a partially exposed Pyro class may look like:: import Pyro5.server class PyroService(object): value = 42 # not exposed def __dunder__(self): # exposed pass def _private(self): # not exposed pass def __private(self): # not exposed pass @Pyro5.server.expose def get_value(self): # exposed return self.value @Pyro5.server.expose @property def attr(self): # exposed as 'proxy.attr' remote attribute return self.value @Pyro5.server.expose @attr.setter def attr(self, value): # exposed as 'proxy.attr' writable self.value = value .. index:: oneway decorator **Specifying one-way methods using the @Pyro5.server.oneway decorator:** You decide on the class of your Pyro object on the server, what methods are to be called as one-way. You use the ``@Pyro5.server.oneway`` decorator on these methods to mark them for Pyro. When the client proxy connects to the server it gets told automatically what methods are one-way, you don't have to do anything on the client yourself. Any calls your client code makes on the proxy object to methods that are marked with ``@Pyro5.server.oneway`` on the server, will happen as one-way calls:: import Pyro5 @Pyro5.server.expose class PyroService(object): def normal_method(self, args): result = do_long_calculation(args) return result @Pyro5.server.oneway def oneway_method(self, args): result = do_long_calculation(args) # no return value, cannot return anything to the client See :ref:`oneway-calls-client` for the documentation about how client code handles this. See the `oneway example `_ for some code that demonstrates the use of oneway methods. Exposing classes and methods without changing existing source code ================================================================== In the case where you cannot or don't want to change existing source code, it's not possible to use the ``@expose`` decorator to tell Pyro what methods should be exposed. This can happen if you're dealing with third-party library classes or perhaps a generic module that you don't want to 'taint' with a Pyro dependency because it's used elsewhere too. There are a few possibilities to deal with this: **Use adapter classes** The preferred solution is to not use the classes from the third party library directly, but create an adapter class yourself with the appropriate ``@expose`` set on it or on its methods. Register this adapter class instead. Then use the class from the library from within your own adapter class. This way you have full control over what exactly is exposed, and what parameter and return value types travel over the wire. **Create exposed classes by using ``@expose`` as a function** Creating adapter classes is good but if you're looking for the most convenient solution we can do better. You can still use ``@expose`` to make a class a proper Pyro class with exposed methods, *without having to change the source code* due to adding @expose decorators, and without having to create extra classes yourself. Remember that Python decorators are just functions that return another function (or class)? This means you can also call them as a regular function yourself, which allows you to use classes from third party libraries like this:: from awesome_thirdparty_library import SomeClassFromLibrary import Pyro5.server # expose the class from the library using @expose as wrapper function: ExposedClass = Pyro5.server.expose(SomeClassFromLibrary) daemon.register(ExposedClass) # register the exposed class rather than the library class itself There are a few caveats when using this: #. You can only expose the class and all its methods as a whole, you can't cherrypick methods that should be exposed #. You have no control over what data is returned from the methods. It may still be required to deal with serialization issues for instance when a method of the class returns an object whose type is again a class from the library. See the `thirdpartylib example `_ for a little server that deals with such a third party library. .. index:: publishing objects .. _publish-objects: Pyro Daemon: publishing Pyro objects ==================================== To publish a regular Python object and turn it into a Pyro object, you have to tell Pyro about it. After that, your code has to tell Pyro to start listening for incoming requests and to process them. Both are handled by the *Pyro daemon*. In its most basic form, you create one or more classes that you want to publish as Pyro objects, you create a daemon, register the class(es) with the daemon, and then enter the daemon's request loop:: import Pyro5.server @Pyro5.server.expose class MyPyroThing(object): # ... methods that can be called go here... pass daemon = Pyro5.server.Daemon() uri = daemon.register(MyPyroThing) print(uri) daemon.requestLoop() Once a client connects, Pyro will create an instance of the class and use that single object to handle the remote method calls during one client proxy session. The object is removed once the client disconnects. Another client will cause another instance to be created for its session. You can control more precisely when, how, and for how long Pyro will create an instance of your Pyro class. See :ref:`server-instancemode` below for more details. Anyway, when you run the code printed above, the uri will be printed and the server sits waiting for requests. The uri that is being printed looks a bit like this: ``PYRO:obj_dcf713ac20ce4fb2a6e72acaeba57dfd@localhost:51850`` Client programs use these uris to access the specific Pyro objects. .. note:: From the address in the uri that was printed you can see that Pyro by default binds its daemons on localhost. This means you cannot reach them from another machine on the network (a security measure). If you want to be able to talk to the daemon from other machines, you have to explicitly provide a hostname to bind on. This is done by giving a ``host`` argument to the daemon, see the paragraphs below for more details on this. .. index:: private methods .. note:: **Private methods:** Pyro considers any method or attribute whose name starts with at least one underscore ('_'), private. These cannot be accessed remotely. An exception is made for the 'dunder' methods with double underscores, such as ``__len__``. Pyro follows Python itself here and allows you to access these as normal methods, rather than treating them as private. .. note:: You can publish any regular Python object as a Pyro object. However since Pyro adds a few Pyro-specific attributes to the object, you can't use: * types that don't allow custom attributes, such as the builtin types (``str`` and ``int`` for instance) * types with ``__slots__`` (a possible way around this is to add Pyro's custom attributes to your ``__slots__``, but that isn't very nice) .. note:: Most of the the time a Daemon will keep running. However it's still possible to nicely free its resources when the request loop terminates by simply using it as a context manager in a ``with`` statement, like so:: with Pyro5.server.Daemon() as daemon: daemon.register(...) daemon.requestLoop() .. index:: publishing objects oneliner, serve .. _server-servesimple: Oneliner Pyro object publishing: Pyro5.server.serve() ----------------------------------------------------- Ok not really a one-liner, but one statement: use :py:meth:`serve` to publish a dict of objects/classes and start Pyro's request loop. The code above could also be written as:: import Pyro5.server @Pyro5.server.expose class MyPyroThing(object): pass obj = MyPyroThing() Pyro5.server.serve( { MyPyroThing: None, # register the class obj: None # register one specific instance }, ns=False) You can perform some limited customization: .. py:method:: serve(objects [host=None, port=0, daemon=None, use_ns=True, verbose=True]) Very basic method to fire up a daemon that hosts a bunch of objects. The objects will be registered automatically in the name server if you specify this. API reference: :py:func:`Pyro5.server.serve` :param objects: mapping of objects/classes to names, these are the Pyro objects that will be hosted by the daemon, using the names you provide as values in the mapping. Normally you'll provide a name yourself but in certain situations it may be useful to set it to ``None``. Read below for the exact behavior there. :type objects: dict :param host: optional hostname where the daemon should be reached on. Details below at :ref:`create_deamon` :type host: str or None :param port: optional port number where the daemon should be accessible on :type port: int :param daemon: optional existing daemon to use, that you created yourself. If you don't specify this, the method will create a new daemon object by itself. :type daemon: Pyro5.server.Daemon :param use_ns: optional, if True (the default), the objects will also be registered in the name server (located using :py:meth:`Pyro5.core.locate_ns`) for you. If this parameters is False, your objects will only be hosted in the daemon and are not published in a name server. Read below about the exact behavior of the object names you provide in the ``objects`` dictionary. :type ns: bool :param verbose: optional, if True (the default), print out a bit of info on the objects that are registered :type verbose: bool :returns: nothing, it starts the daemon request loop and doesn't return until that stops. If you set ``use_ns=True`` (the default) your objects will appear in the name server as well. Usually this means you provide a logical name for every object in the ``objects`` dictionary. If you don't (= set it to ``None``), the object will still be available in the daemon (by a generated name) but will *not* be registered in the name server (this is a bit strange, but hey, maybe you don't want all the objects to be visible in the name server). When not using a name server at all (``use_ns=False``), the names you provide are used as the object names in the daemon itself. If you set the name to ``None`` in this case, your object will get an automatically generated internal name, otherwise your own name will be used. .. important:: - The names you provide for each object have to be unique (or ``None``). For obvious reasons you can't register multiple objects with the same names. - if you use ``None`` for the name, you have to use the ``verbose`` setting as well, otherwise you won't know the name that Pyro generated for you. That would make your object more or less unreachable. The uri that is used to register your objects in the name server with, is of course generated by the daemon. So if you need to influence that, for instance because of NAT/firewall issues, it is the daemon's configuration you should be looking at. If you don't provide a daemon yourself, :py:meth:`serve` will create a new one for you using the default configuration or with a few custom parameters you can provide in the call, as described above. If you don't specify the ``host`` and ``port`` parameters, it will simple create a Daemon using the default settings. If you *do* specify ``host`` and/or ``port``, it will use these as parameters for creating the Daemon (see next paragraph). If you need to further tweak the behavior of the daemon, you have to create one yourself first, with the desired configuration. Then provide it to this function using the ``daemon`` parameter. Your daemon will then be used instead of a new one:: custom_daemon = Pyro5.server.Daemon(host="example", nathost="example") # some additional custom configuration Pyro5.server.serve( { MyPyroThing: None }, daemon = custom_daemon) .. index:: double: Pyro daemon; creating a daemon .. _create_deamon: Creating a Daemon ----------------- Pyro's daemon is ``Pyro5.server.Daemon``. It has a few optional arguments when you create it: .. function:: Daemon([host=None, port=0, unixsocket=None, nathost=None, natport=None, interface=DaemonObject, connected_socket=None]) Create a new Pyro daemon. :param host: the hostname or IP address to bind the server on. Default is ``None`` which means it uses the configured default (which is localhost). It is necessary to set this argument to a visible hostname or ip address, if you want to access the daemon from other machines. When binding to a hostname be careful of your OS's policies as it might still bind to localhost as well. Depending on your DNS setup you may have to use "", "0.0.0.0" or an explicit externally visible IP addres to make the server accessible over the network. :type host: str or None :param port: port to bind the server on. Defaults to 0, which means to pick a random port. :type port: int :param unixsocket: the name of a Unix domain socket to use instead of a TCP/IP socket. Default is ``None`` (don't use). :type unixsocket: str or None :param nathost: hostname to use in published addresses (useful when running behind a NAT firewall/router). Default is ``None`` which means to just use the normal host. For more details about NAT, see :ref:`nat-router`. :type host: str or None :param natport: port to use in published addresses (useful when running behind a NAT firewall/router). If you use 0 here, Pyro will replace the NAT-port by the internal port number to facilitate one-to-one NAT port mappings. :type port: int :param interface: optional alternative daemon object implementation (that provides the Pyro API of the daemon itself) :type interface: Pyro5.server.DaemonObject :param connected_socket: optional existing socket connection to use instead of creating a new server socket :type interface: socket .. index:: double: Pyro daemon; registering objects/classes Registering objects/classes --------------------------- Every object you want to publish as a Pyro object needs to be registered with the daemon. You can let Pyro choose a unique object id for you, or provide a more readable one yourself. .. method:: Daemon.register(obj_or_class [, objectId=None, force=False, weak=False]) Registers an object with the daemon to turn it into a Pyro object. :param obj_or_class: the singleton instance or class to register (class is the preferred way) :param objectId: optional custom object id (must be unique). Default is to let Pyro create one for you. :type objectId: str or None :param force: optional flag to force registration, normally Pyro checks if an object had already been registered. If you set this to True, the previous registration (if present) will be silently overwritten. :param weak: only store weak reference to the object, automatically unregistering it when it is garbage-collected. Without this, the daemon will keep the object alive by having it stored in its mapping, preventing garbage-collection until manual unregistration. :type force: bool :returns: an uri for the object :rtype: :class:`Pyro5.core.URI` It is important to do something with the uri that is returned: it is the key to access the Pyro object. You can save it somewhere, or perhaps print it to the screen. The point is, your client programs need it to be able to access your object (they need to create a proxy with it). Maybe the easiest thing is to store it in the Pyro name server. That way it is almost trivial for clients to obtain the proper uri and connect to your object. See :doc:`nameserver` for more information (:ref:`nameserver-registering`), but it boils down to getting a name server proxy and using its ``register`` method:: uri = daemon.register(some_object) ns = Pyro5.core.locate_ns() ns.register("example.objectname", uri) .. note:: If you ever need to create a new uri for an object, you can use :py:meth:`Pyro5.server.Daemon.uriFor`. The reason this method exists on the daemon is because an uri contains location information and the daemon is the one that knows about this. Intermission: Example 1: server and client not using name server ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ A little code example that shows the very basics of creating a daemon and publishing a Pyro object with it. Server code:: import Pyro5.server @Pyro5.server.expose class Thing(object): def method(self, arg): return arg*2 # ------ normal code ------ daemon = Pyro5.server.Daemon() uri = daemon.register(Thing) print("uri=",uri) daemon.requestLoop() # ------ alternatively, using serve ----- Pyro5.server.serve( { Thing: None }, ns=False, verbose=True) Client code example to connect to this object:: import Pyro5.client # use the URI that the server printed: uri = "PYRO:obj_b2459c80671b4d76ac78839ea2b0fb1f@localhost:49383" thing = Pyro5.client.Proxy(uri) print(thing.method(42)) # prints 84 With correct additional parameters --described elsewhere in this chapter-- you can control on which port the daemon is listening, on what network interface (ip address/hostname), what the object id is, etc. Intermission: Example 2: server and client, with name server ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ A little code example that shows the very basics of creating a daemon and publishing a Pyro object with it, this time using the name server for easier object lookup. Server code:: import Pyro5.server import Pyro5.core @Pyro5.server.expose class Thing(object): def method(self, arg): return arg*2 # ------ normal code ------ daemon = Pyro5.server.Daemon(host="yourhostname") ns = Pyro5.core.locate_ns() uri = daemon.register(Thing) ns.register("mythingy", uri) daemon.requestLoop() # ------ alternatively, using serve ----- Pyro5.server.serve( { Thing: "mythingy" }, ns=True, verbose=True, host="yourhostname") Client code example to connect to this object:: import Pyro5.client thing = Pyro5.client.Proxy("PYRONAME:mythingy") print(thing.method(42)) # prints 84 .. index:: double: Pyro daemon; unregistering objects Unregistering objects --------------------- When you no longer want to publish an object, you need to unregister it from the daemon (unless it was registered with ``weak=True`` when it will be unregistered automatically when garbage-collected): .. method:: Daemon.unregister(objectOrId) :param objectOrId: the object to unregister :type objectOrId: object itself or its id string .. index:: request loop Running the request loop ------------------------ Once you've registered your Pyro object you'll need to run the daemon's request loop to make Pyro wait for incoming requests. .. method:: Daemon.requestLoop([loopCondition]) :param loopCondition: optional callable returning a boolean, if it returns False the request loop will be aborted and the call returns This is Pyro's event loop and it will take over your program until it returns (it might never.) If this is not what you want, you can control it a tiny bit with the ``loopCondition``, or read the next paragraph. .. index:: double: event loop; integrate Pyro's requestLoop Integrating Pyro in your own event loop --------------------------------------- If you want to use a Pyro daemon in your own program that already has an event loop (aka main loop), you can't simply call ``requestLoop`` because that will block your program. A daemon provides a few tools to let you integrate it into your own event loop: * :py:attr:`Pyro5.server.Daemon.sockets` - list of all socket objects used by the daemon, to inject in your own event loop * :py:meth:`Pyro5.server.Daemon.events` - method to call from your own event loop when Pyro needs to process requests. Argument is a list of sockets that triggered. For more details and example code, see the `eventloop `_ and `gui_eventloop `_ examples. They show how to use Pyro including a name server, in your own event loop, and also possible ways to use Pyro from within a GUI program with its own event loop. .. index:: Combining Daemons Combining Daemon request loops ------------------------------ In certain situations you will be dealing with more than one daemon at the same time. For instance, when you want to run your own Daemon together with an 'embedded' Name Server Daemon, or perhaps just another daemon with different settings. Usually you run the daemon's :meth:`Pyro5.server.Daemon.requestLoop` method to handle incoming requests. But when you have more than one daemon to deal with, you have to run the loops of all of them in parallel somehow. There are a few ways to do this: 1. multithreading: run each daemon inside its own thread 2. multiplexing event loop: write a multiplexing event loop and call back into the appropriate daemon when one of its connections send a request. You can do this using :mod:`selectors` or :mod:`select` and you can even integrate other (non-Pyro) file-like selectables into such a loop. Also see the paragraph above. 3. use :meth:`Pyro5.server.Daemon.combine` to combine several daemons into one, so that you only have to call the requestLoop of that "master daemon". Basically Pyro will run an integrated multiplexed event loop for you. You can combine normal Daemon objects, the NameServerDaemon and also the name server's BroadcastServer. Again, have a look at the `eventloop example `_ to see how this can be done. (Note: this will only work with the ``multiplex`` server type, not with the ``thread`` type) .. index:: double: Pyro daemon; shutdown double: Pyro daemon; cleaning up Cleaning up ----------- To clean up the daemon itself (release its resources) either use the daemon object as a context manager in a ``with`` statement, or manually call :py:meth:`Pyro5.server.Daemon.close`. Of course, once the daemon is running, you first need a clean way to stop the request loop before you can even begin to clean things up. You can use force and hit ctrl-C or ctrl-\ or ctrl-Break to abort the request loop, but this usually doesn't allow your program to clean up neatly as well. It is therefore also possible to leave the loop cleanly from within your code (without using :py:meth:`sys.exit` or similar). You'll have to provide a ``loopCondition`` that you set to ``False`` in your code when you want the daemon to stop the loop. You could use some form of semi-global variable for this. (But if you're using the threaded server type, you have to also set ``COMMTIMEOUT`` because otherwise the daemon simply keeps blocking inside one of the worker threads). Another possibility is calling :py:meth:`Pyro5.server.Daemon.shutdown` on the running daemon object. This will also break out of the request loop and allows your code to neatly clean up after itself, and will also work on the threaded server type without any other requirements. If you are using your own event loop mechanism you have to use something else, depending on your own loop. .. index:: single: @Pyro5.server.behavior instance modes; instance_mode instance modes; instance_creator .. _server-instancemode: Controlling Instance modes and Instance creation ================================================ While it is possible to register a single singleton *object* with the daemon, it is actually preferred that you register a *class* instead. When doing that, it is Pyro itself that creates an instance (object) when it needs it. This allows for more control over when and for how long Pyro creates objects. Controlling the instance mode and creation is done by decorating your class with ``Pyro5.server.behavior`` and setting its ``instance_mode`` or/and ``instance_creator`` parameters. It can only be used on a class definition, because these behavioral settings only make sense at that level. By default, Pyro will create an instance of your class per *session* (=proxy connection) Here is an example of registering a class that will have one new instance for *every single method call* instead:: import Pyro5.server @Pyro5.server.behavior(instance_mode="percall") class MyPyroThing(object): @Pyro5.server.expose def method(self): return "something" daemon = Pyro5.server.Daemon() uri = daemon.register(MyPyroThing) print(uri) daemon.requestLoop() There are three possible choices for the ``instance_mode`` parameter: - ``session``: (the default) a new instance is created for every new proxy connection, and is reused for all the calls during that particular proxy session. Other proxy sessions will deal with a different instance. - ``single``: a single instance will be created and used for all method calls (for this daemon), regardless what proxy connection we're dealing with. This is the same as creating and registering a single object yourself (the old style of registering code with the deaemon). Be aware that the methods on this object can be called from separate threads concurrently. - ``percall``: a new instance is created for every single method call, and discarded afterwards. **Instance creation** .. sidebar:: Instance creation is lazy When you register a class in this way, be aware that Pyro only creates an actual instance of it when it is first needed. If nobody connects to the deamon requesting the services of this class, no instance is ever created. Normally Pyro will simply use a default parameterless constructor call to create the instance. If you need special initialization or the class's init method requires parameters, you have to specify an ``instance_creator`` callable as well. Pyro will then use that to create an instance of your class. It will call it with the class to create an instance of as the single parameter. See the `instancemode example `_ to learn about various ways to use this. See the `usersession example `_ to learn how you could use it to build user-bound resource access without concurrency problems. .. index:: automatic proxying Autoproxying ============ Pyro will automatically take care of any Pyro objects that you pass around through remote method calls. It will replace them by a proxy automatically, so the receiving side can call methods on it and be sure to talk to the remote object instead of a local copy. There is no need to create a proxy object manually. All you have to do is to register the new object with the appropriate daemon:: def some_pyro_method(self): thing=SomethingNew() self._pyroDaemon.register(thing) return thing # just return it, no need to return a proxy There is a `autoproxy example `_ that shows the use of this feature, and several other examples also make use of it. Note that when using the marshal serializer, this feature doesn't work. You have to use one of the other serializers to use autoproxying. .. index:: concurrency model, server types, SERVERTYPE .. _object_concurrency: Server types and Concurrency model ================================== Pyro supports multiple server types (the way the Daemon listens for requests). Select the desired type by setting the ``SERVERTYPE`` config item. It depends very much on what you are doing in your Pyro objects what server type is most suitable. For instance, if your Pyro object does a lot of I/O, it may benefit from the parallelism provided by the thread pool server. However if it is doing a lot of CPU intensive calculations, the multiplexed server may be more appropriate. If in doubt, go with the default setting. .. index:: double: server type; threaded 1. threaded server (servertype ``"thread"``, this is the default) This server uses a dynamically adjusted thread pool to handle incoming proxy connections. If the max size of the thread pool is too small for the number of proxy connections, new proxy connections will fail with an exception. The size of the pool is configurable via some config items: - ``THREADPOOL_SIZE`` this is the maximum number of threads that Pyro will use - ``THREADPOOL_SIZE_MIN`` this is the minimum number of threads that must remain standby Every proxy on a client that connects to the daemon will be assigned to a thread to handle the remote method calls. This way multiple calls can potentially be processed concurrently. *This means your Pyro object may have to be made thread-safe*! If you registered the pyro object's class with instance mode ``single``, that single instance will be called concurrently from different threads. If you used instance mode ``session`` or ``percall``, the instance will not be called from different threads because a new one is made per connection or even per call. But in every case, if you access a shared resource from your Pyro object, you may need to take thread locking measures such as using Queues. .. index:: double: server type; multiplex 2. multiplexed server (servertype ``"multiplex"``) This server uses a connection multiplexer to process all remote method calls sequentially. No threads are used in this server. It uses the best supported selector available on your platform (kqueue, poll, select). It means only one method call is running at a time, so if it takes a while to complete, all other calls are waiting for their turn (even when they are from different proxies). The instance mode used for registering your class, won't change the way the concurrent access to the instance is done: in all cases, there is only one call active at all times. Your objects will never be called concurrently from different threads, because there are no threads. It does still affect when and how often Pyro creates an instance of your class. .. note:: If the ``ONEWAY_THREADED`` config item is enabled (it is by default), *oneway* method calls will be executed in a separate worker thread, regardless of the server type you're using. .. index:: double: server type; what to choose? *When to choose which server type?* With the threadpool server at least you have a chance to achieve concurrency, and you don't have to worry much about blocking I/O in your remote calls. The usual trouble with using threads in Python still applies though: Python threads don't run concurrently unless they release the :abbr:`GIL (Global Interpreter Lock)`. If they don't, you will still hang your server process. For instance if a particular piece of your code doesn't release the :abbr:`GIL (Global Interpreter Lock)` during a longer computation, the other threads will remain asleep waiting to acquire the :abbr:`GIL (Global Interpreter Lock)`. One of these threads may be the Pyro server loop and then your whole Pyro server will become unresponsive. Doing I/O usually means the :abbr:`GIL (Global Interpreter Lock)` is released. Some C extension modules also release it when doing their work. So, depending on your situation, not all hope is lost. With the multiplexed server you don't have threading problems: everything runs in a single main thread. This means your requests are processed sequentially, but it's easier to make the Pyro server unresponsive. Any operation that uses blocking I/O or a long-running computation will block all remote calls until it has completed. .. index:: double: server; serialization Serialization ============= Pyro will serialize the objects that you pass to the remote methods, so they can be sent across a network connection. Depending on the serializer that is being used for your Pyro server, there will be some limitations on what objects you can use, and what serialization format is required of the clients that connect to your server. If your server also uses Pyro client code/proxies, you might also need to select the serializer for these by setting the ``SERIALIZER`` config item. See the :doc:`/config` chapter for details about the config items. See :ref:`object-serialization` for more details about serialization and the new config items. Other features ============== .. index:: attributes added to Pyro objects Attributes added to Pyro objects -------------------------------- The following attributes will be added to your object if you register it as a Pyro object: * ``_pyroId`` - the unique id of this object (a ``str``) * ``_pyroDaemon`` - a reference to the :py:class:`Pyro5.server.Daemon` object that contains this object Even though they start with an underscore (and are private, in a way), you can use them as you so desire. As long as you don't modify them! The daemon reference for instance is useful to register newly created objects with, to avoid the need of storing a global daemon object somewhere. These attributes will be removed again once you unregister the object. .. index:: network adapter binding, IP address, localhost, 127.0.0.1 Network adapter binding and localhost ------------------------------------- All Pyro daemons bind on localhost by default. This is because of security reasons. This means only processes on the same machine have access to your Pyro objects. If you want to make them available for remote machines, you'll have to tell Pyro on what network interface address it must bind the daemon. This also extends to the built in servers such as the name server. .. warning:: Read chapter :doc:`security` before exposing Pyro objects to remote machines! There are a few ways to tell Pyro what network address it needs to use. You can set a global config item ``HOST``, or pass a ``host`` parameter to the constructor of a Daemon, or use a command line argument if you're dealing with the name server. For more details, refer to the chapters in this manual about the relevant Pyro components. Pyro provides a couple of utility functions to help you with finding the appropriate IP address to bind your servers on if you want to make them publicly accessible: * :py:func:`Pyro5.socketutil.get_ip_address` * :py:func:`Pyro5.socketutil.get_interface` Cleaning up / disconnecting stale client connections ---------------------------------------------------- A client proxy will keep a connection open even if it is rarely used. It's good practice for the clients to take this in consideration and release the proxy. But the server can't enforce this, some clients may keep a connection open for a long time. Unfortunately it's hard to tell when a client connection has become stale (unused). Pyro's default behavior is to accept this fact and not kill the connection. This does mean however that many stale client connections will eventually block the server's resources, for instance all workers threads in the threadpool server. There's a simple possible solution to this, which is to specify a communication timeout on your server. For more information about this, read :ref:`tipstricks_release_proxy`. .. index:: Daemon API Daemon Pyro interface --------------------- A rather interesting aspect of Pyro's Daemon is that it (partly) is a Pyro object itself. This means it exposes a couple of remote methods that you can also invoke yourself if you want. The object exposed is :class:`Pyro5.server.DaemonObject` (as you can see it is a bit limited still). You access this object by creating a proxy for the ``"Pyro.Daemon"`` object. That is a reserved object name. You can use it directly but it is preferable to use the constant ``Pyro5.constants.DAEMON_NAME``. An example follows that accesses the daemon object from a running name server:: >>> import Pyro5.client >>> daemon=Pyro5.client.Proxy("PYRO:"+Pyro5.constants.DAEMON_NAME+"@localhost:9090") >>> daemon.ping() >>> daemon.registered() ['Pyro.NameServer', 'Pyro.Daemon'] Intercepting errors in user code executed in a method call ---------------------------------------------------------- When a method call is executed in a Pyro server/daemon, it eventually will execute some user written code that implements the remote method. This user code may raise an exception (intentionally or not). Normally, Pyro will only report the exception to the calling client. It may be useful however to also process the error on the *server*, for instance, to log the error somewhere for later reference. For this purpose, you can set the ``methodcall_error_handler`` attribute on the daemon object to a custom error handler function. See the `exceptions example `_ . This function's signature is:: def custom_error_handler(daemon: Daemon, client_sock: socketutil.SocketConnection, method: Callable, vargs: Sequence[Any], kwargs: Dict[str, Any], exception: Exception) -> None Pyro5-5.15/docs/source/tipstricks.rst000066400000000000000000001117111451404116400176450ustar00rootroot00000000000000.. index:: Tips & trics .. _tipstricks: ************* Tips & Tricks ************* .. index:: Best practices Best practices ============== Make as little as possible remotely accessible. ----------------------------------------------- Try to avoid simply sticking an ``@expose`` on the whole class. Instead only mark the methods that you really want to be remotely accessible. Alternatively, make sure the exposed class only consists of methods that are okay to be accessed remotely. Avoid circular communication topologies. ---------------------------------------- When you can have a circular communication pattern in your system (A-->B-->C-->A) this has the potential to deadlock. You should try to avoid circularity. Possible ways to break a cycle are to use a oneway call somewhere in the chain or set an ``COMMTIMEOUT`` so that after a certain period in a locking situation the caller aborts with a TimeoutError, effectively breaking the deadlock. .. index:: releasing a proxy .. _tipstricks_release_proxy: Release proxies when no longer used. Avoids 'After X simultaneous proxy connections, Pyro seems to freeze!' ----------------------------------------------------------------------------------------------------------- A connected proxy that is unused takes up resources on the server. In the case of the threadpool server type, it locks to a single thread. If you have too many connected proxies at the same time, the server runs out of threads and can't accept new connections. You can use the ``THREADPOOL_SIZE`` config item to increase the maximum number of threads that Pyro will use. Or use the multiplex server instead, which doesn't have this limitation. To free resources in a timely manner, close (release) proxies that your program no longer needs. Pyro wil auto-reconnect a proxy when it is used again later. The easiest way is to use a proxy as a context manager. You can also use an explicit ``_pyroRelease`` call on the proxy. Releasing and then reconnecting a proxy is very costly so make sure you're not doing this too often. .. index:: binary blob seealso: binary blob; binary data transfer Avoid large binary blobs over the wire. --------------------------------------- Pyro is not designed to efficiently transfer large amounts of binary data over the network. Try to find another protocol that better suits this requirement if you do this regularly. There are a few tricks to speed up transfer of large blocks of data using Pyro, read :ref:`binarytransfer` for details about that. .. index:: object graphs Minimize object structures that travel over the wire. ----------------------------------------------------- Pyro serializes the whole object structure you're passing, even when only a fraction of it is used on the receiving end. It may be necessary to define special lightweight objects for your Pyro interfaces that hold just the data you need, rather than passing a huge object structure. It's good design practice anyway to have an "external API" that is different from your internal code, and tuned for minimal communication overhead or complexity. This also ties in with just exposing the methods of your server object that should be remotely accessible, and using primitive types in the interfaces as much as possible to avoid serialization problems. Consider using basic data types instead of custom classes. ---------------------------------------------------------- Because Pyro serializes the objects you're passing, it needs to know how to serialize custom types. While you can teach Pyro about these (see :ref:`customizing-serialization`) it may sometimes be easier to just use a builtin datatype instead. For instance if you have a custom class whose state essentially is a set of numbers, consider then that it may be easier to just transfer a ``set`` or a ``list`` of those numbers rather than an instance of your custom class. It depends on your class and data of course, and whether the receiving code expects just the list of numbers or really needs an instance of your custom class. .. index:: Logging .. _logging: Logging ======= If you configure it (see :ref:`config-items`) Pyro will write a bit of debug information, errors, and notifications to a log file. It uses Python's standard :py:mod:`logging` module for this. Once enabled, your own program code could use Pyro's logging setup as well. But if you want to configure your own logging, you have to do this before importing Pyro. A little example to enable logging by setting the required environment variables from the shell:: $ export PYRO_LOGFILE=pyro.log $ export PYRO_LOGLEVEL=DEBUG $ python my_pyro_program.py Another way is by modifiying ``os.environ`` from within your code itself, *before* any import of Pyro is done:: import os os.environ["PYRO_LOGFILE"] = "pyro.log" os.environ["PYRO_LOGLEVEL"] = "DEBUG" import Pyro5.api # do stuff... Finally, it is possible to initialize the logging by means of the standard Python ``logging`` module only, but then you still have to tell Pyro what log level it should use (or it won't log anything):: import logging logging.basicConfig() # or your own sophisticated setup logging.getLogger("Pyro5").setLevel(logging.DEBUG) logging.getLogger("Pyro5.core").setLevel(logging.DEBUG) # ... set level of other logger names as desired ... import Pyro5.api # do stuff... The various logger names are similar to the module that uses the logger, so for instance logging done by code in ``Pyro5.core`` will use a logger category name of ``Pyro5.core``. Look at the top of the source code of the various modules from Pyro to see what the exact names are. .. index:: multiple NICs, network interfaces Multiple network interfaces =========================== This is a difficult subject but here are a few short notes about it. *At this time, Pyro doesn't support running on multiple network interfaces at the same time*. You can bind a deamon on INADDR_ANY (0.0.0.0) though, including the name server. But weird things happen with the URIs of objects published through these servers, because they will point to 0.0.0.0 and your clients won't be able to connect to the actual objects. The name server however contains a little trick. The broadcast responder can also be bound on 0.0.0.0 and it will in fact try to determine the correct ip address of the interface that a client needs to use to contact the name server on. So while you cannot run Pyro daemons on 0.0.0.0 (to respond to requests from all possible interfaces), sometimes it is possible to run only the name server on 0.0.0.0. Success of this depends on your particular network setup. .. index:: wire protocol version .. _wireprotocol: Wire protocol version ===================== Here is a little tip to find out what wire protocol version a given Pyro server is using. This could be useful if you are getting ``ProtocolError`` about invliad protocol version. **Server** This is a way to figure out the protocol version number a given Pyro server is using: by reading the first 6 bytes from the server socket connection. The Pyro daemon will respond with a 4-byte string "``PYRO``" followed by a 2-byte number that is the protocol version used:: $ nc `_ , using python 3.8, over a 1 Gbit LAN connection: ========== ========== ============= ================ ==================== serializer str mb/sec bytes mb/sec bytearray mb/sec bytearray w/iterator ========== ========== ============= ================ ==================== marshal 95.7 97.1 98.4 55.4 serpent 41.0 23.2 24.3 22.3 json 48.1 not supported not supported not supported ========== ========== ============= ================ ==================== The json serializer only works with strings, it can't serialize binary data at all. The serpent serializer can, but read the note above about why it's quite inefficent there. Marshal is very efficient and is almost saturating the 1 Gbit connection speed limit. **Alternative: avoid most of the serialization overhead by using annotations** Pyro allows you to add custom annotation chunks to the request and response messages (see :ref:`msg_annotations`). Because these are binary chunks they will not be passed through the serializer at all. Depending on what the configured maximum message size is you may have to split up larger files. The `filetransfer example `_ contains fully working example code to see this in action. It combines this with the remote iterator capability of Pyro to easily get all chunks of the file. It has to split up the file in small chunks but is still quite a bit faster than transmitting bytes through regular response values as bytes or arrays. Also it is using only regular Pyro high level logic and no low level network or socket code. **Alternative: integrating raw socket transfer in a Pyro server** It is possible to get data transfer speeds that are close to the limit of your network adapter by doing the actual data transfer via low-level socket code and everything else via Pyro. This keeps the amount of low-level code to a minimum. Have a look at the `filetransfer example `_ again, to see a possible way of doing this. It creates a special Daemon subclass that uses Pyro for everything as usual, but for actual file transfer it sets up a dedicated temporary socket connection over which the file data is transmitted. .. index:: IPv6 IPV6 support ============ Pyro supports IPv6. You can use IPv6 addresses (enclosed in brackets) in the same places where you would normally have used IPv4 addresses. There's one exception: the address notation in a Pyro URI. For example: ``PYRO:objectname@[::1]:3456`` this points at a Pyro object located on the IPv6 "::1" address (localhost). When Pyro displays a numeric IPv6 location from an URI it will also use the bracket notation. This bracket notation is only used in Pyro URIs, everywhere else you just type the IPv6 address without brackets. To tell Pyro to prefer using IPv6 you can use the ``PREFER_IP_VERSION`` config item. It is set to 0 by default, which means that your operating system is selecting the preferred protocol. Often this is ipv6 if it is available, but not always, so you can force it by setting this config item to 6 (or 4, if you want ipv4) .. index:: Numpy, numpy.ndarray .. _numpy: Pyro and Numpy ============== Pyro doesn't support Numpy out of the box. You'll see certain errors occur when trying to use numpy objects (ndarrays, etcetera) with Pyro:: TypeError: array([1, 2, 3]) is not JSON serializable or TypeError: don't know how to serialize class or TypeError: don't know how to serialize class or similar. These errors are caused by Numpy datatypes not being recognised by Pyro's serializer. Why is this: #. numpy is a third party library and there are many, many others. It is not Pyro's responsibility to understand all of them. #. numpy is often used in scenarios with large amounts of data. Sending these large arrays over the wire through Pyro is often not the best solution. It is not useful to provide transparent support for numpy types when you'll be running into trouble often such as slow calls and large network overhead. #. Pyrolite (:doc:`pyrolite`) would have to get numpy support as well and that is a lot of work (because every numpy type would require a mapping to the appropriate Java or .NET type) If you still want to use numpy with Pyro, you'll have to convert the data to standard Python datatypes before using them in Pyro. So instead of just ``na = numpy.array(...); return na;``, use this instead: ``return na.tolist()``. Or perhaps even ``return array.array('i', na)`` (serpent understands ``array.array`` just fine). Note that the elements of a numpy array usually are of a special numpy datatype as well (such as ``numpy.int32``). If you don't convert these individually as well, you will still get serialization errors. That is why something like ``list(na)`` doesn't work: it seems to return a regular python list but the elements are still numpy datatypes. You have to use the full conversions as mentioned earlier. Note that you'll have to do a bit more work to deal with multi-dimensional arrays: you have to convert the shape of the array separately. .. index:: double: HTTP gateway server; command line .. _http-gateway: Pyro via HTTP and JSON ====================== .. sidebar:: advanced topic This is an advanced/low-level Pyro topic. Pyro provides a HTTP gateway server that translates HTTP requests into Pyro calls. It responds with JSON messages. This allows clients (including web browsers) to use a simple http interface to call Pyro objects. Pyro's JSON serialization format is used so the gateway simply passes the JSON response messages back to the caller. It also provides a simple web page that shows how stuff works. *Starting the gateway:* You can launch the HTTP gateway server conveniently via the command line tool. Because the gateway is written as a wsgi app, you can also stick it into a wsgi server of your own choice. Import ``pyro_app`` from ``Pyro5.utils.httpgateway`` to do that (that's the app you need to use). :command:`python -m Pyro5.utils.httpgateway [options]` (or simply: :command:`pyro5-httpgateway [options]`) A short explanation of the available options can be printed with the help option: .. program:: Pyro5.utils.httpgateway .. option:: -h, --help Print a short help message and exit. Most other options should be self explanatory; you can set the listening host and portname etc. An important option is the exposed names regex option: this controls what objects are accessible from the http gateway interface. It defaults to something that won't just expose every internal object in your system. If you want to toy a bit with the examples provided in the gateway's web page, you'll have to change the option to something like: ``r'Pyro\.|test\.'`` so that those objects are exposed. This regex is the same as used when listing objects from the name server, so you can use the ``nsc`` tool to check it (with the listmatching command). *Using the gateway:* You request the url ``http://localhost:8080/pyro/<>/<>`` to invoke a method on the object with the given name (yes, every call goes through a naming server lookup). Parameters are passed via a regular query string parameter list (in case of a GET request) or via form post parameters (in case of a POST request). The response is a JSON document. In case of an exception, a JSON encoded exception object is returned. You can easily call this from your web page scripts using javascript's ``fetch()``. Have a look at the page source of the gateway's web page to see how this could be done. Note that you have to comply with the browser's same-origin policy: if you want to allow your own scripts to access the gateway, you'll have to make sure they are loaded from the same website. The http gateway server is *stateless* at the moment. This means every call you do will end be processed by a new Pyro proxy in the gateway server. This is not impacting your client code though, because every call that it does is also just a stateless http call. It only impacts performance: doing large amounts of calls through the http gateway will perform much slower as the same calls processed by a native Pyro proxy (which you can instruct to operate in batch mode as well). However because Pyro is quite efficient, a call through the gateway is still processed in just a few milliseconds, naming lookup and json serialization all included. Special http request headers: - ``X-Pyro-Options``: add this header to the request to set certain pyro options for the call. Possible values (comma-separated): - ``oneway``: force the Pyro call to be a oneway call and return immediately. The gateway server still returns a 200 OK http response as usual, but the response data is empty. This option is to override the semantics for non-oneway method calls if you so desire. - ``X-Pyro-Gateway-Key``: add this header to the request to set the http gateway key. You can also set it on the request with a ``$key=....`` querystring parameter. Special Http response headers: - ``X-Pyro-Correlation-Id``: contains the correlation id Guid that was used for this request/response. Http response status codes: - 200 OK: all went well, response is the Pyro response message in JSON serialized format - 403 Forbidden: you're trying to access an object that is not exposed by configuration - 404 Not Found: you're requesting a non existing object - 500 Internal server error: something went wrong during request processing, response is serialized exception object (if available) Look at the `http example `_ for working code how you could set this up. .. index:: current_context, correlation_id .. _current_context: Client information on the current_context, correlation id ========================================================= .. sidebar:: advanced topic This is an advanced/low-level Pyro topic. Pyro provides a *thread-local* object with some information about the current Pyro method call, such as the client that's performing the call. It is available as :py:data:`Pyro5.current_context` (shortcut to :py:data:`Pyro5.core.current_context`). When accessed in a Pyro server it contains various attributes: .. py:attribute:: Pyro5.current_context.client (:py:class:`Pyro5.socketutil.SocketConnection`) this is the socket connection with the client that's doing the request. You can check the source to see what this is all about, but perhaps the single most useful attribute exposed here is ``sock``, which is the socket connection. So the client's IP address can for instance be obtained via :code:`Pyro5.current_context.client.sock.getpeername()[0]` . However, since for oneway calls the socket connection will likely be closed already, this is not 100% reliable. Therefore Pyro stores the result of the ``getpeername`` call in a separate attribute on the context: ``client_sock_addr`` (see below) .. py:attribute:: Pyro5.current_context.client_sock_addr (*tuple*) the socket address of the client doing the call. It is a tuple of the client host address and the port. .. py:attribute:: Pyro5.current_context.seq (*int*) request sequence number .. py:attribute:: Pyro5.current_context.msg_flags (*int*) message flags, see :py:class:`Pyro5.message.Message` .. py:attribute:: Pyro5.current_context.serializer_id (*int*) numerical id of the serializer used for this communication, see :py:class:`Pyro5.message.Message` . .. py:attribute:: Pyro5.current_context.annotations (*dict*) message annotations, key is a 4-letter string and the value is a byte sequence. Used to send and receive annotations with Pyro requests. See :ref:`msg_annotations` for more information about that. .. py:attribute:: Pyro5.current_context.response_annotations (*dict*) message annotations, key is a 4-letter string and the value is a byte sequence. Used in client code, the annotations returned by a Pyro server are available here. See :ref:`msg_annotations` for more information about that. .. py:attribute:: Pyro5.current_context.correlation_id (:py:class:`uuid.UUID`, optional) correlation id of the current request / response. If you set this (in your client code) before calling a method on a Pyro proxy, Pyro will transfer the correlation id to the server context. If the server on their behalf invokes another Pyro method, the same correlation id will be passed along. This way it is possible to relate all remote method calls that originate from a single call. To make this work you'll have to set this to a new :py:class:`uuid.UUID` in your client code right before you call a Pyro method. Note that it is required that the correlation id is of type :py:class:`uuid.UUID`. Note that the HTTP gateway (see :ref:`http-gateway`) also creates a correlation id for every request, and will return it via the ``X-Pyro-Correlation-Id`` HTTP-header in the response. It will also accept this header optionally on a request in which case it will use the value from the header rather than generating a new id. For an example of how this information can be retrieved, and how to set the ``correlation_id``, see the `callcontext example `_ . See the `usersession example `_ to learn how you could use it to build user-bound resource access without concurrency problems. .. index:: resource-tracking .. _resource_tracking: Automatically freeing resources when client connection gets closed ================================================================== .. sidebar:: advanced topic This is an advanced/low-level Pyro topic. A client can call remote methods that allocate stuff in the server. Normally the client is responsible to call other methods once the resources should be freed. However if the client forgets this or the connection to the server is forcefully closed before the client can free the resources, the resources in the server will usually not be freed anymore. You may be able to solve this in your server code yourself (perhaps using some form of keepalive/timeout mechanism) but Pyro 4.63 and newer provides a built-in mechanism that can help: resource tracking on the client connection. Your server will register the resources when they are allocated, thereby making them tracked resources on the client connection. These tracked resources will be automatically freed by Pyro if the client connection is closed. For this to work, the resource object should have a ``close`` method (Pyro will call this). If needed, you can also override :py:meth:`Pyro5.core.Daemon.clientDisconnect` and do the cleanup yourself with the ``tracked_resources`` on the connection object. Resource tracking and untracking is done in your server class on the ``Pyro5.current_context`` object: .. py:method:: Pyro5.current_context.track_resource(resource) Let Pyro track the resource on the current client connection. .. py:method:: Pyro5.current_context.untrack_resource(resource) Untrack a previously tracked resource, useful if you have freed it normally. See the `resourcetracking example `_ for working code utilizing this. .. note:: The order in which the resources are freed is arbitrary. Also, if the resource can be garbage collected normally by Python, it is removed from the tracked resources. So the ``close`` method should not be the only way to properly free such resources (maybe you need a ``__del__`` as well). .. index:: annotations .. _msg_annotations: Message annotations =================== .. sidebar:: advanced topic This is an advanced/low-level Pyro topic. Pyro's wire protocol allows for a very flexible messaging format by means of *annotations*. Annotations are extra information chunks that are added to the pyro messages traveling over the network. An annotation is a low level datastructure (to optimize the generation of network messages): a chunk identifier string of exactly 4 characters (such as "CODE"), and its value, a byte sequence. If you want to put specific data structures into an annotation chunk value, you have to encode them to a byte sequence yourself (possibly by using one of Pyro's serializers, or any other). When processing a custom annotation, you have to decode it yourself too. Communicating annotations with Pyro is done via a normal dictionary of chunk id -> data bytes. Pyro will take care of encoding this dictionary into the wire message and extracting it out of a response message. *Adding annotations to messages:* In client code, you can set the ``annotations`` property of the :py:data:`Pyro5.current_context` object right before the proxy method call. Pyro will then add that annotations dict to the request message. In server code, you do this by setting the ``response_annotations`` property of the :py:data:`Pyro5.current_context` in your Pyro object, right before returning the regular response value. Pyro will add the annotations dict to the response message. *Using annotations:* In your client code, you can do that as well, but you should look at the ``response_annotations`` of this context object instead. If you're using large annotation chunks, it is advised to clear these fields after use. See :ref:`current_context`. In your server code, in the Daemon, you can use the :py:data:`Pyro5.current_context` to access the ``annotations`` of the last message that was received. To see how you can work with custom message annotations, see the `callcontext `_ or `filetransfer `_ examples. .. index:: handshake .. _conn_handshake: Connection handshake ==================== .. sidebar:: advanced topic This is an advanced/low-level Pyro topic. When a proxy is first connecting to a Pyro daemon, it exchanges a few messages to set up and validate the connection. This is called the connection *handshake*. Part of it is the daemon returning the object's metadata (see :ref:`metadata`). You can hook into this mechanism and influence the data that is initially exchanged during the connection setup, and you can act on this data. You can disallow the connection based on this, for example. You can set your own data on the proxy attribute :py:attr:`Pyro5.client.Proxy._pyroHandshake`. You can set any serializable object. Pyro will send this as the handshake message to the daemon when the proxy tries to connect. In the daemon, override the method :py:meth:`Pyro5.server.Daemon.validateHandshake` to customize/validate the connection setup. This method receives the data from the proxy and you can either raise an exception if you don't want to allow the connection, or return a result value if you are okay with the new connection. The result value again can be any serializable object. This result value will be received back in the Proxy where you can act on it if you subclass the proxy and override :py:meth:`Pyro5.client.Proxy._pyroValidateHandshake`. For an example of how you can work with connections handshake validation, see the `handshake example `_ . It implements a (bad!) security mechanism that requires the client to supply a "secret" password to be able to connect to the daemon. .. index:: dispatcher, gateway Efficient dispatchers or gateways that don't de/reserialize messages ==================================================================== .. sidebar:: advanced topic This is an advanced/low-level Pyro topic. Imagine you're designing a setup where a Pyro call is essentially dispatched or forwarded to another server. The dispatcher (sometimes also called gateway) does nothing else than deciding who the message is for, and then forwarding the Pyro call to the actual object that performs the operation. This can be built easily with Pyro by 'intercepting' the call in a dispatcher object, and performing the remote method call *again* on the actual server object. There's nothing wrong with this except for perhaps two things: #. Pyro will deserialize and reserialize the remote method call parameters on every hop, this can be quite inefficient if you're dealing with many calls or large argument data structures. #. The dispatcher object is now dependent on the method call argument data types, because Pyro has to be able to de/reserialize them. This often means the dispatcher also needs to have access to the same source code files that define the argument data types, that the client and server use. As long as the dispatcher itself *doesn't have to know what is even in the actual message*, Pyro provides a way to avoid both issues mentioned above: use the :py:class:`Pyro5.client.SerializedBlob`. If you use that as the (single) argument to a remote method call, Pyro will not deserialize the message payload *until you ask for it* by calling the ``deserialized()`` method on it. Which is something you only do in the actual server object, and *not* in the dispatcher. Because the message is then never de/reserialized in the dispatcher code, you avoid the serializer overhead, and also don't have to include the source code for the serialized types in the dispatcher. It just deals with a blob of serialized bytes. An example that shows how this mechanism can be used, is `blob-dispatch `_ . .. index:: socketpair, user provided sockets Hooking onto existing connected sockets such as from socketpair() ================================================================= For communication between threads or sub-processes, there is ``socket.socketpair()``. It creates spair of connected sockets that you can share between the threads or processes. Pyro can use a user-created socket like that, instead of creating new sockets itself, which means you can use Pyro to talk between threads or sub-processes over an efficient and isolated channel. You do this by creating a socket (or a pair) and providing it as the ``connected_socket`` parameter to the ``Daemon`` and ``Proxy`` classes. For the Daemon, don't pass any other arguments because they won't be used anyway. For the Proxy, set only the first parameter (``uri``) to just the *name* of the object in the daemon you want to connect to. So don't use a PYRO or PYRONAME prefix for the uri in this case. Closing the proxy or the daemon will *not* close the underlying user-supplied socket so you can use it again for another proxy (to access a different object). You created the socket(s) yourself, and you also have to close the socket(s) yourself. See the `socketpair example `_ for two example programs (one using threads, the other using fork to create a child process). Pyro5-5.15/docs/source/tutorials.rst000066400000000000000000000225441451404116400175010ustar00rootroot00000000000000.. include:: .. index:: tutorial ******** Tutorial ******** This tutorial will explain a couple of basic Pyro concepts. Warm-up ======= Before proceeding, you should install Pyro if you haven't done so. For instructions about that, see :doc:`install`. In this tutorial, you will use Pyro's default configuration settings, so once Pyro is installed, you're all set! All you need is a text editor and a couple of console windows. During the tutorial, you are supposed to run everything on a single machine. This avoids initial networking complexity. .. note:: For security reasons, Pyro runs stuff on localhost by default. If you want to access things from different machines, you'll have to tell Pyro to do that explicitly. .. index:: double: tutorial; concepts and tools Pyro concepts and tools ======================= Pyro enables code to call methods on objects even if that object is running on a remote machine:: +----------+ +----------+ | server A | | server B | | | < network > | | | Python | | Python | | OBJECT ----------foo.invoke()--------> OBJECT | | | | foo | +----------+ +----------+ Pyro is mainly used as a library in your code but it also has several supporting command line tools. We won't explain every one of them here as you will only need the "name server" for this tutorial. .. _keyconcepts: Key concepts ^^^^^^^^^^^^ Here are a couple of key concepts you encounter when using Pyro: Proxy A proxy is a substitute object for "the real thing". It intercepts the method calls you would normally do on an object as if it was the actual object. Pyro then performs some magic to transfer the call to the computer that contains the *real* object, where the actual method call is done, and the results are returned to the caller. This means the calling code doesn't have to know if it's dealing with a normal or a remote object, because the code is identical. The class implementing Pyro proxies is ``Pyro5.client.Proxy`` :abbr:`URI (Unique resource identifier)` This is what Pyro uses to identify every object. (similar to what a web page URL is to point to the different documents on the web). Its string form is like this: "PYRO:" + object name + "@" + server name + port number. There are a few other forms it can take as well. You can write the protocol in lowercase too if you want ("pyro:") but it will automatically be converted to uppercase internally. The class implementing Pyro uris is ``Pyro5.core.URI`` Pyro object This is a normal Python object but it is registered with Pyro so that you can access it remotely. Pyro objects are written just as any other object but the fact that Pyro knows something about them makes them special, in the way that you can call methods on them from other programs. A class can also be a Pyro object, but then you will also have to tell Pyro about how it should create actual objects from that class when handling remote calls. Pyro daemon (server) This is the part of Pyro that listens for remote method calls, dispatches them to the appropriate actual objects, and returns the results to the caller. All Pyro objects are registered in one or more daemons. Pyro name server The name server is a utility that provides a phone book for Pyro applications: you use it to look up a "number" by a "name". The name in Pyro's case is the logical name of a remote object. The number is the exact location where Pyro can contact the object. Serialization This is the process of transforming objects into streams of bytes that can be transported over the network. The receiver deserializes them back into actual objects. Pyro needs to do this with all the data that is passed as arguments to remote method calls, and their response data. Not all objects can be serialized, so it is possible that passing a certain object to Pyro won't work even though a normal method call would accept it just fine. Configuration Pyro can be configured in a lot of ways. Using environment variables (they're prefixed with ``PYRO_``) or by setting config items in your code. See the configuration chapter for more details. The default configuration should be ok for most situations though, so you many never have to touch any of these options at all! Starting a name server ^^^^^^^^^^^^^^^^^^^^^^ While the use of the Pyro name server is optional, we will use it in this tutorial. It also shows a few basic Pyro concepts, so let us begin by explaining a little about it. Open a console window and execute the following command to start a name server: :command:`python -m Pyro5.nameserver` (or simply: :command:`pyro5-ns`) The name server will start and it prints something like:: Not starting broadcast server for IPv6. NS running on localhost:9090 (::1) URI = PYRO:Pyro.NameServer@localhost:9090 .. sidebar:: Localhost By default, Pyro uses *localhost* to run stuff on, so you can't by mistake expose your system to the outside world. You'll need to tell Pyro explicitly to use something else than *localhost*. But it is fine for the tutorial, so we leave it as it is. The name server has started and is listening on *localhost port 9090*. (If your operating system supports it, it will likely use Ipv6 as well rather than the older Ipv4 addressing). It also printed an :abbr:`URI (unique resource identifier)`. Remember, this is what Pyro uses to identify every object. The nameserver itself is also just a Pyro object! The name server can be stopped with a :kbd:`control-c`, or on Windows, with :kbd:`ctrl-break`. But let it run in the background for the rest of this tutorial. Interacting with the name server ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ There's another command line tool that let you interact with the name server: "nsc" (name server control tool). You can use it, amongst other things, to see what all known registered objects in the naming server are. Let's do that right now. Type: :command:`python -m Pyro5.nsc list` (or simply: :command:`pyro5-nsc list`) and it will print something like this:: --------START LIST Pyro.NameServer --> PYRO:Pyro.NameServer@localhost:9090 metadata: {'class:Pyro5.nameserver.NameServer'} --------END LIST The only object that is currently registered, is the name server itself! (Yes, the name server is a Pyro object itself. Pyro and the "nsc" tool are using Pyro to talk to it). .. note:: As you can see, the name ``Pyro.NameServer`` is registered to point to the URI that we saw earlier. This is mainly for completeness sake, and is not often used, because there are different ways to get to talk to the name server (see below). .. sidebar:: The NameServer object The name server itself is a normal Pyro object which means the 'nsc' tool, and any other code that talks to it, is just using normal Pyro methods. What makes it a bit different from other Pyro servers is that is includes a broadcast responder (for discovery). There's a little detail left unexplained: *How did the nsc tool know where the name server was?* Pyro has a couple of ways to locate a name server. The nsc tool uses those too: there is a network broadcast discovery to see if there's a name server available somewhere (the name server contains a broadcast responder that will respond "Yeah hi I'm here"). So in many cases you won't have to configure anything to be able to discover the name server. If nobody answers though, Pyro tries the configured default or custom location. If still nobody answers it prints a sad message and exits. However if it found the name server, it is then possible to talk to it and get the location of any other registered object. This means that you won't have to hard code any object locations in your code, and that the code is capable of dynamically discovering everything at runtime. Not using the Name server ========================= In both tutorials above we used the Name Server for easy object lookup. The use of the name server is optional, see :ref:`name-server` for details. There are various other options for connecting your client code to your Pyro objects, have a look at the client code details: :ref:`object-discovery` and the server code details: :ref:`publish-objects`. .. index:: tutorial Tutorial examples ================= Pyro5 includes dozens of examples. You can find them in the `source distribution `_, or `online on github `_. Historically, two of them (warehouse and stockmarket) were used in this manual to walk you through creating a complete Pyro program. You can still read these tutorials in the Pyro4 manual, they're still almost unchanged in Pyro5 (follow along with the pyro5 example code to spot the few differences): * Pyro4 tutorial `building a warehouse `_ * Pyro4 tutorial `stockmarket simulator `_ They're useful starting points (especially since the examples are created in multiple phases), but there are many more concepts to explore in the other examples so don't hesitate to browse through them. Pyro5-5.15/examples/000077500000000000000000000000001451404116400143005ustar00rootroot00000000000000Pyro5-5.15/examples/attributes/000077500000000000000000000000001451404116400164665ustar00rootroot00000000000000Pyro5-5.15/examples/attributes/Readme.txt000066400000000000000000000000771451404116400204300ustar00rootroot00000000000000This is an example that shows direct remote attribute access. Pyro5-5.15/examples/attributes/client.py000066400000000000000000000060331451404116400203200ustar00rootroot00000000000000import Pyro5.api uri = input("enter attribute server object uri: ").strip() with Pyro5.api.Proxy(uri) as p: # Direct remote attribute access. print("\nDirect attribute access:") print("p.prop_value=", p.prop_value) print("adding 500 to p.prop_value") p.prop_value += 500 print("p.prop_value=", p.prop_value) print("actual remote value: ", p.getValue(), " (via p.getValue() remote method call)") if p.prop_value != p.getValue(): # they differ!? (should not happen) print("Remote value is different! The p.prop_value attribute must be a local one (not remote), this should not happen! (because metadata is enabled here)") print() # dunder names print("calling p.__dunder__()....: ", p.__dunder__()) with Pyro5.api.Proxy(uri) as p: # unexposed attributes print("\nAccessing unexposed attributes:") try: print("accessing p.sub...") _ = p.sub raise RuntimeError("this should not be possible!") # because p.sub is not an exposed property except AttributeError as x: print("ok, got expected error:", x) try: print("accessing p.value...") _ = p.value raise RuntimeError("this should not be possible!") # because p.value is not an exposed property except AttributeError as x: print("ok, got expected error:", x) try: print("accessing p._value...") _ = p._value raise RuntimeError("this should not be possible!") # because p._value is private except AttributeError as x: print("ok, got expected error:", x) try: print("accessing p.__value...") _ = p.__value raise RuntimeError("this should not be possible!") # because p.__value is private except AttributeError as x: print("ok, got expected error:", x) with Pyro5.api.Proxy(uri) as p: # Dotted name traversal is not supported by Pyro because that is a security vulnerability. # What will happen instead is that the first part of the name will be evaluated and returned, # and that the rest of the expression will be evaluated on the local object instead of # directly on the remote one. print("\nTrying dotted name traversal:") value = p.prop_sub print("value gotten from p.prop_sub=", value) print("\nTrying to update the dictionary directly on the remote object...") p.prop_sub.update({"test": "nope"}) # this will only update the local copy! new_value = p.prop_sub print("value gotten from p.prop_sub=", new_value, " (should be unchanged!)") assert new_value == value, "update should not have been done remotely" try: print("\nTrying longer dotted name: p.prop_sub.foobar.attribute") _ = p.prop_sub.foobar.attribute raise RuntimeError("this should not be possible!") except Exception as x: remote_tb = getattr(x, "_pyroTraceback", None) if remote_tb: raise RuntimeError("We got a remote traceback but this should have been a local one only") print("ok, got expected error (local only):", x) Pyro5-5.15/examples/attributes/server.py000066400000000000000000000012101451404116400203400ustar00rootroot00000000000000from Pyro5.api import expose, serve something = "Something" @expose class Thingy(object): def __init__(self): self.sub = {"name": "value"} self.value = 42 self._value = 123 self.__value = 999 def __dunder__(self): return "yep" def __len__(self): return 200 def getValue(self): return self.value @property def prop_value(self): return self.value @prop_value.setter def prop_value(self, value): self.value = value @property def prop_sub(self): return self.sub serve({ Thingy: "example.attributes" }, use_ns=False) Pyro5-5.15/examples/autoproxy/000077500000000000000000000000001451404116400163525ustar00rootroot00000000000000Pyro5-5.15/examples/autoproxy/Readme.txt000066400000000000000000000006111451404116400203060ustar00rootroot00000000000000This is an example that shows the autoproxy feature. Pyro will automatically return a Proxy instead of the object itself, if you are passing a Pyro object over a remote call. This means you can easily create new objects in a server and return them from remote calls, without the need to manually wrap them in a proxy. Note that when using the marshal serializer, this feature will not work. Pyro5-5.15/examples/autoproxy/client.py000066400000000000000000000007221451404116400202030ustar00rootroot00000000000000import Pyro5.api uri = input("enter factory server object uri: ").strip() factory = Pyro5.api.Proxy(uri) # create several things. print("Creating things.") thing1 = factory.createSomething(1) thing2 = factory.createSomething(2) thing3 = factory.createSomething(3) print(repr(thing1)) # interact with them on the server. print("Speaking stuff (see output on server).") thing1.speak("I am the first") thing2.speak("I am second") thing3.speak("I am last then...") Pyro5-5.15/examples/autoproxy/server.py000066400000000000000000000011631451404116400202330ustar00rootroot00000000000000from Pyro5.api import expose, serve from thingy import Thingy @expose class Factory(object): def createSomething(self, number): # create a new item thing = Thingy(number) # connect it to the Pyro daemon to make it a Pyro object self._pyroDaemon.register(thing) # Return it. Pyro's autoproxy feature turns it into a proxy automatically. # If that feature is disabled, the object itself (a copy) is returned, # and the client won't be able to interact with the actual Pyro object here. return thing serve({ Factory: "example.autoproxy" }, use_ns=False) Pyro5-5.15/examples/autoproxy/thingy.py000066400000000000000000000003341451404116400202260ustar00rootroot00000000000000from Pyro5.api import expose @expose class Thingy(object): def __init__(self, number): self.number = number def speak(self, message): print("Thingy {0} says: {1}".format(self.number, message)) Pyro5-5.15/examples/autoreconnect/000077500000000000000000000000001451404116400171515ustar00rootroot00000000000000Pyro5-5.15/examples/autoreconnect/Readme.txt000066400000000000000000000036021451404116400211100ustar00rootroot00000000000000This is an example that shows the auto reconnect feature, from a client's perspective. Start the server and the client. You can stop the server while it's running. The client will report that the connection is lost, and that it is trying to rebind. Start the server again. You'll see that the client continues. There are 2 examples: - reconnect using NS (clientNS/serverNS) - reconnect using PYRO (client/server) NOTES: 1- Your server has to be prepared for this feature. It must not rely on any transient internal state to function correctly, because that state is lost when your server is restarted. You could make the state persistent on disk and read it back in at restart. 2- By default Pyro starts its daemons on a random port. If you want to support autoreconnection, you will need to restart your daemon on the port it used before. Easiest is to pick a fixed port. 3- If you rely on PYRO-uri's: then your server MUST register the objects with their old objectId to the daemon. Otherwise the client will try to access an unknown object Id. (this is not needed when you are ONLY using PYRONAME-uris, where a new lookup to the name server is performed instead) 4- If the NS loses its registrations, you're out of luck. Clients that rely on name server based reconnects will fail. 5- The client is reponsible for detecting a network problem itself. It must also explicitly call the reconnect method on the object. 6- Why isn't this automagic? Because you need to have control about it when a network problem occurs. Furthermore, only you can decide if your system needs this feature, and if it can support it (see points above). 7- Read the source files for info on what is going on. 8- Also see the 'disconnects' example for another swing at dealing with client timeouts/disconnects, and how a special proxy class can make it easier to deal with for the clients. Pyro5-5.15/examples/autoreconnect/client.py000066400000000000000000000011741451404116400210040ustar00rootroot00000000000000import time import Pyro5.api import Pyro5.errors print("Autoreconnect using PYRO uri.") # We create a proxy with a PYRO uri. # Reconnect logic depends on the server now. # (it needs to restart the object with the same id) uri = input("Enter the uri that the server printed:").strip() obj = Pyro5.api.Proxy(uri) while True: print("call...") try: obj.method(42) print("Sleeping 1 second") time.sleep(1) except Pyro5.errors.ConnectionClosedError: # or possibly CommunicationError print("Connection lost. REBINDING...") print("(restart the server now)") obj._pyroReconnect() Pyro5-5.15/examples/autoreconnect/clientNS.py000066400000000000000000000011361451404116400212430ustar00rootroot00000000000000import time import Pyro5.api import Pyro5.errors print("Autoreconnect using Name Server.") # We create a proxy with a PYRONAME uri. # That allows Pyro to look up the object again in the NS when # it needs to reconnect later. obj = Pyro5.api.Proxy("PYRONAME:example.autoreconnect") while True: print("call...") try: obj.method(42) print("Sleeping 1 second") time.sleep(1) except Pyro5.errors.ConnectionClosedError: # or possibly CommunicationError print("Connection lost. REBINDING...") print("(restart the server now)") obj._pyroReconnect() Pyro5-5.15/examples/autoreconnect/server.py000066400000000000000000000020221451404116400210250ustar00rootroot00000000000000import time from Pyro5.api import expose, Daemon print("Autoreconnect using PYRO uri.") @expose class TestClass(object): def method(self, arg): print("Method called with %s" % arg) print("You can now try to stop this server with ctrl-C/ctrl-Break") time.sleep(1) # We are responsible to (re)connect objects with the same object Id, # so that the client can reuse its PYRO-uri directly to reconnect. # There are a few options, such as depending on the Name server to # maintain a name registration for our object (see the serverNS for this). # Or we could store our objects in our own persistent database. # But for this example we will just use a pre-generated id (fixed name). # The other thing is that your Daemon must re-bind on the same port. # By default Pyro will select a random port so we specify a fixed port. with Daemon(port=7777) as daemon: uri = daemon.register(TestClass, objectId="example.autoreconnect_fixed_objectid") print("Server started, uri: %s" % uri) daemon.requestLoop() Pyro5-5.15/examples/autoreconnect/serverNS.py000066400000000000000000000036161451404116400213000ustar00rootroot00000000000000import time from Pyro5.api import expose, Daemon, locate_ns import Pyro5.errors print("Autoreconnect using Name Server.") @expose class TestClass(object): def method(self, arg): print("Method called with %s" % arg) print("You can now try to stop this server with ctrl-C/ctrl-Break") time.sleep(1) # If we reconnect the object, it has to have the same objectId as before. # for this example, we rely on the Name Server registration to get our old id back. # If we KNOW 100% that PYRONAME-uris are the only thing used to access our # object, we could skip all this and just register as usual. # That works because the proxy, when reconnecting, will do a new nameserver lookup # and receive the new object uri back. This REQUIRES: # - clients will never connect using a PYRO-uri # - client proxy._pyroBind() is never called # BUT for sake of example, and because we really cannot guarantee the above, # here we go for the safe route and reuse our previous object id. ns = locate_ns() try: existing = ns.lookup("example.autoreconnect") print("Object still exists in Name Server with id: %s" % existing.object) print("Previous daemon socket port: %d" % existing.port) # start the daemon on the previous port daemon = Daemon(port=existing.port) # register the object in the daemon with the old objectId daemon.register(TestClass, objectId=existing.object) except Pyro5.errors.NamingError: print("There was no previous registration in the name server.") # just start a new daemon on a random port daemon = Daemon() # register the object in the daemon and let it get a new objectId # also need to register in name server because it's not there yet. uri = daemon.register(TestClass) ns.register("example.autoreconnect", uri) print("Server started.") daemon.requestLoop() # note: we are not removing the name server registration when terminating! Pyro5-5.15/examples/autoretry/000077500000000000000000000000001451404116400163365ustar00rootroot00000000000000Pyro5-5.15/examples/autoretry/Readme.txt000066400000000000000000000010201451404116400202650ustar00rootroot00000000000000This is an example that shows the auto retry feature (in the client). server.py -- the server you need to run for this example client.py -- client that uses auto retry settings The client disables and enables auto retries to show what happens. It shows an exception when auto retries disabled and server side closed the connection, and then use auto retries to avoid the exception raising to user code. Suggest to run the server with timeout warning output: PYRO_LOGLEVEL=WARNING PYRO_LOGFILE={stderr} python server.py Pyro5-5.15/examples/autoretry/client.py000066400000000000000000000017001451404116400201640ustar00rootroot00000000000000from time import sleep import Pyro5.api # client side will not have timeout Pyro5.config.COMMTIMEOUT = 0 # Not using auto-retry feature Pyro5.config.MAX_RETRIES = 0 obj = Pyro5.api.Proxy("PYRONAME:example.autoretry") print("Calling remote function 1st time (create connection)") obj.add(1, 1) print("Calling remote function 2nd time (not timed out yet)") obj.add(2, 2) print("Sleeping 1 second...") sleep(1) print("Calling remote function 3rd time (will raise an exception)") try: obj.add(3, 3) except Exception as e: print("Got exception %r as expected." % repr(e)) print("\nNow, let's enable the auto retry on the proxy") obj._pyroRelease() obj._pyroMaxRetries = 2 print("Calling remote function 1st time (create connection)") obj.add(1, 1) print("Calling remote function 2nd time (not timed out yet)") obj.add(2, 2) print("Sleeping 1 second...") sleep(1) print("Calling remote function 3rd time (will not raise any exceptions)") obj.add(3, 3) Pyro5-5.15/examples/autoretry/server.py000066400000000000000000000004621451404116400202200ustar00rootroot00000000000000from Pyro5.api import expose, serve, config @expose class CalcServer(object): def add(self, num1, num2): print("calling add: %d, %d" % (num1, num2)) return num1 + num2 config.COMMTIMEOUT = 0.5 # the server should time out easily now serve({ CalcServer: "example.autoretry" }) Pyro5-5.15/examples/banks/000077500000000000000000000000001451404116400153765ustar00rootroot00000000000000Pyro5-5.15/examples/banks/Readme.txt000066400000000000000000000006421451404116400173360ustar00rootroot00000000000000This is a simple electronic banking example. There are two banks:- Rabobank and ABN (don't ask - I'm from Holland) Their services are started with BankServer.py. The client runs some transactions on both banks (if found), like:- -creating accounts -deleting accounts -deposit money -withdraw money -inquire balance The ABN bank will not allow the client to overdraw and have a negative balance, the Rabobank will. Pyro5-5.15/examples/banks/banks.py000066400000000000000000000043201451404116400170450ustar00rootroot00000000000000from Pyro5.api import expose, behavior # Unrestricted account. class Account(object): def __init__(self): self._balance = 0.0 def withdraw(self, amount): self._balance -= amount def deposit(self, amount): self._balance += amount def balance(self): return self._balance # Restricted withdrawal account. class RestrictedAccount(Account): def withdraw(self, amount): if amount <= self._balance: self._balance -= amount else: raise ValueError('insufficent balance') # Abstract bank. @expose @behavior(instance_mode="single") class Bank(object): def __init__(self): self.accounts = {} def name(self): pass # must override this! def createAccount(self, name): pass # must override this! def deleteAccount(self, name): try: del self.accounts[name] except KeyError: raise KeyError('unknown account') def deposit(self, name, amount): try: return self.accounts[name].deposit(amount) except KeyError: raise KeyError('unknown account') def withdraw(self, name, amount): try: return self.accounts[name].withdraw(amount) except KeyError: raise KeyError('unknown account') def balance(self, name): try: return self.accounts[name].balance() except KeyError: raise KeyError('unknown account') def allAccounts(self): accs = {} for name in self.accounts.keys(): accs[name] = self.accounts[name].balance() return accs # Special bank: Rabobank. It has unrestricted accounts. @expose class Rabobank(Bank): def name(self): return 'Rabobank' def createAccount(self, name): if name in self.accounts: raise ValueError('Account already exists') self.accounts[name] = Account() # Special bank: ABN. It has restricted accounts. @expose class ABN(Bank): def name(self): return 'ABN bank' def createAccount(self, name): if name in self.accounts: raise ValueError('Account already exists') self.accounts[name] = RestrictedAccount() Pyro5-5.15/examples/banks/client.py000066400000000000000000000046751451404116400172420ustar00rootroot00000000000000# # Bank client. # # The client searches the two banks and performs a set of operations. # (the banks are searched simply by listing a namespace prefix path) # from Pyro5.api import locate_ns, Proxy # A bank client. class client(object): def __init__(self, name): self.name = name def doBusiness(self, bank): print("\n*** %s is doing business with %s:" % (self.name, bank.name())) print("Creating account") try: bank.createAccount(self.name) except ValueError as x: print("Failed: %s" % x) print("Removing account and trying again") bank.deleteAccount(self.name) bank.createAccount(self.name) print("Deposit money") bank.deposit(self.name, 200.00) print("Deposit money") bank.deposit(self.name, 500.75) print("Balance=%.2f" % bank.balance(self.name)) print("Withdraw money") bank.withdraw(self.name, 400.00) print("Withdraw money (overdraw)") try: bank.withdraw(self.name, 400.00) except ValueError as x: print("Failed: %s" % x) print("End balance=%.2f" % bank.balance(self.name)) print("Withdraw money from non-existing account") try: bank.withdraw('GOD', 2222.22) print("!!! Succeeded?!? That is an error") except KeyError as x: print("Failed as expected: %s" % x) print("Deleting non-existing account") try: bank.deleteAccount('GOD') print("!!! Succeeded?!? That is an error") except KeyError as x: print("Failed as expected: %s" % x) ns = locate_ns() # list the available banks by looking in the NS for the given prefix path banknames = [name for name in ns.list(prefix="example.banks.")] if not banknames: raise RuntimeError('There are no banks to do business with!') banks = [] # list of banks (proxies) print() for name in banknames: print("Contacting bank: %s" % name) uri = ns.lookup(name) banks.append(Proxy(uri)) # Different clients that do business with all banks irmen = client('Irmen') suzy = client('Suzy') for bank in banks: irmen.doBusiness(bank) suzy.doBusiness(bank) # List all accounts print() for bank in banks: print("The accounts in the %s:" % bank.name()) accounts = bank.allAccounts() for name in accounts.keys(): print(" %s : %.2f" % (name, accounts[name])) Pyro5-5.15/examples/banks/server.py000066400000000000000000000010011451404116400172460ustar00rootroot00000000000000# # The banks server # from Pyro5.api import Daemon, locate_ns import banks with Daemon() as daemon: with locate_ns() as ns: uri = daemon.register(banks.Rabobank) ns.register("example.banks.rabobank", uri) uri = daemon.register(banks.ABN) ns.register("example.banks.abn", uri) print("available banks:") print(list(ns.list(prefix="example.banks.").keys())) # enter the service loop. print("Banks are ready for customers.") daemon.requestLoop() Pyro5-5.15/examples/batchedcalls/000077500000000000000000000000001451404116400167115ustar00rootroot00000000000000Pyro5-5.15/examples/batchedcalls/Readme.txt000066400000000000000000000007471451404116400206570ustar00rootroot00000000000000This is an example that shows the batched calls feature. The example does a lot of method calls on the same proxy object. It shows the time it takes to do them individually. Afterwards, it does them again but this time using the batched calls feature. It prints the time taken and this should be much faster. It also shows what happens when one of the calls in the batch generates an error. (the batch is aborted and the error is raised locally once the result generator gets to it). Pyro5-5.15/examples/batchedcalls/client.py000066400000000000000000000074301451404116400205450ustar00rootroot00000000000000import time from Pyro5.errors import get_pyro_traceback import Pyro5.api NUMBER_OF_LOOPS = 20000 uri = input("enter server object uri: ").strip() p = Pyro5.api.Proxy(uri) # First, we do a loop of N normal remote calls on the proxy # We time the loop and validate the computation result print("Normal remote calls...") begin = time.time() total = 0 p.printmessage("beginning normal calls") for i in range(NUMBER_OF_LOOPS): total += p.multiply(7, 6) total += p.add(10, 20) p.printmessage("end of normal calls") assert total == (NUMBER_OF_LOOPS * (7 * 6 + 10 + 20)) # check duration = time.time() - begin print("that took {0:.2f} seconds ({1:.0f} calls/sec)".format(duration, NUMBER_OF_LOOPS * 2.0 / duration)) duration_normal = duration # Now we do the same loop of N remote calls but this time we use # the batched calls proxy. It collects all calls and processes them # in a single batch. For many subsequent calls on the same proxy this # is much faster than doing all calls individually. # (but it has a few limitations and requires changes to your code) print("\nBatched remote calls...") begin = time.time() batch = Pyro5.api.BatchProxy(p) # get a batched call proxy for 'p' batch.printmessage("beginning batch #1") for i in range(NUMBER_OF_LOOPS): batch.multiply(7, 6) # queue a call, note that it returns 'None' immediately batch.add(10, 20) # queue a call, note that it returns 'None' immediately batch.printmessage("end of batch #1") print("processing the results...") total = 0 result = batch() # execute the batch of remote calls, it returns a generator that produces all results in sequence for r in result: total += r duration = time.time() - begin assert total == (NUMBER_OF_LOOPS * (7 * 6 + 10 + 20)) # check print("total time taken {0:.2f} seconds ({1:.0f} calls/sec)".format(duration, NUMBER_OF_LOOPS * 2.0 / duration // 100 * 100)) print("batched calls were {0:.1f} times faster than normal remote calls".format(duration_normal / duration)) # Now we do another loop of batched calls, but this time oneway (no results). print("\nOneway batched remote calls...") begin = time.time() batch = Pyro5.api.BatchProxy(p) # get a batched call proxy for 'p' batch.printmessage("beginning batch #2") for i in range(NUMBER_OF_LOOPS): batch.multiply(7, 6) # queue a call, note that it returns 'None' immediately batch.add(10, 20) # queue a call, note that it returns 'None' immediately batch.delay(2) # queue a delay of 2 seconds (but we won't notice) batch.printmessage("end of batch #2") print("executing batch, there will be no result values. Check server to see printed messages...") result = batch(oneway=True) # execute the batch of remote calls, oneway, will return None assert result is None duration = time.time() - begin print("total time taken {0:.2f} seconds ({1:.0f} calls/sec)".format(duration, NUMBER_OF_LOOPS * 2.0 / duration // 100 * 100)) print("oneway batched calls were {0:.1f} times faster than normal remote calls".format(duration_normal / duration)) # Show what happens when one of the methods in a batch generates an error. # (the batch is aborted and the error is raised locally again). # Btw, you can re-use a batch proxy once you've called it and processed the results. print("\nBatch with an error. Dividing a number by decreasing divisors...") for d in range(3, -3, -1): # divide by 3,2,1,0,-1,-2,-3... but 0 will be a problem ;-) batch.divide(100, d) print("getting results...") divisor = 3 try: for result in batch(): print("100//%d = %d" % (divisor, result)) divisor -= 1 # this will raise the proper zerodivision exception once we're about # to process the batch result from the divide by 0 call. except ZeroDivisionError: print("A divide by zero error occurred during the batch! (expected)") print("".join(get_pyro_traceback())) Pyro5-5.15/examples/batchedcalls/server.py000066400000000000000000000007521451404116400205750ustar00rootroot00000000000000import time from Pyro5.api import expose, serve @expose class Thingy(object): def multiply(self, a, b): return a * b def add(self, a, b): return a + b def divide(self, a, b): return a // b def error(self): return 1 // 0 def delay(self, seconds): time.sleep(seconds) return seconds def printmessage(self, message): print(message) return 0 serve({ Thingy: "example.batched" }, use_ns=False) Pyro5-5.15/examples/benchmark/000077500000000000000000000000001451404116400162325ustar00rootroot00000000000000Pyro5-5.15/examples/benchmark/Readme.txt000066400000000000000000000015411451404116400201710ustar00rootroot00000000000000This test is to find out the average time it takes for a remote PYRO method call. Also it is a kind of stress test because lots of calls are made in a very short time. The oneway method call test is very fast if you run the client and server on different machines. If they're running on the same machine, the speedup is less noticable. There is also the 'connections' benchmark which tests the speed at which Pyro can make new proxy connections. It tests the raw connect speed (by releasing and rebinding existing proxies) and also the speed at which new proxies can be created that perform a single remote method call. Different serializers --------------------- Note that Pyro's performance is very much affected by two things: 1) the network latency and bandwith 2) the characteristics of your data (small messages or large) 2) the serializer that is used. Pyro5-5.15/examples/benchmark/bench.py000066400000000000000000000014041451404116400176620ustar00rootroot00000000000000from Pyro5.api import expose, oneway @expose class bench(object): def length(self, string): return len(string) def timestwo(self, value): return value * 2 def bigreply(self): return 'BIG REPLY' * 500 def bigarg(self, arg): return len(arg) def manyargs(self, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15): return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10 + a11 + a12 + a13 + a14 + a15 def noreply(self, arg): pass def varargs(self, *args): return len(args) def keywords(self, **args): return args def echo(self, *args): return args @oneway def oneway(self, *args): # oneway doesn't return anything pass Pyro5-5.15/examples/benchmark/client.py000066400000000000000000000027451451404116400200720ustar00rootroot00000000000000import time from Pyro5.api import Proxy, config config.SERIALIZER = "marshal" uri = input("Uri of benchmark server? ").strip() obj = Proxy(uri) obj._pyroBind() assert "oneway" in obj._pyroOneway # make sure this method is indeed marked as @oneway funcs = [ lambda: obj.length('Irmen de Jong'), lambda: obj.timestwo(21), lambda: obj.bigreply(), lambda: obj.manyargs(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15), lambda: obj.noreply([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]), lambda: obj.noreply(None), lambda: obj.varargs('een', 2, (3,), [4]), lambda: obj.keywords(arg1='zork'), lambda: obj.echo('een', 2, (3,), [4]), lambda: obj.echo({"aap": 42, "noot": 99, "mies": 987654}), lambda: obj.bigarg('Argument' * 50), lambda: obj.oneway('stringetje', 432423434, 9.8765432) ] print('-------- BENCHMARK REMOTE OBJECT ---------') begin = time.time() iters = 1000 print("warmup...") for _ in range(iters): funcs[0]() for i, f in enumerate(funcs, start=1): print("call #%d, %d times... " % (i, iters), end="") before = time.time() for _ in range(iters): f() print("%.3f" % (time.time() - before)) duration = time.time() - begin print('total time %.3f seconds' % duration) amount = len(funcs) * iters print('total method calls: %d' % amount) avg_pyro_msec = 1000.0 * duration / amount print('avg. time per method call: %.3f msec (%d/sec) (serializer: %s)' % (avg_pyro_msec, amount / duration, config.SERIALIZER)) Pyro5-5.15/examples/benchmark/connections.py000066400000000000000000000022601451404116400211260ustar00rootroot00000000000000import time from Pyro5.api import Proxy, config uri = input("Uri of benchmark server? ").strip() print("Timing raw connect speed (no method call)...") p = Proxy(uri) p.oneway() ITERATIONS = 2000 begin = time.time() for loop in range(ITERATIONS): if loop % 500 == 0: print(loop) p._pyroRelease() p._pyroBind() duration = time.time() - begin print("%d connections in %.3f sec = %.0f conn/sec" % (ITERATIONS, duration, ITERATIONS / duration)) del p print("Timing proxy creation+connect+methodcall speed...") ITERATIONS = 2000 begin = time.time() for loop in range(ITERATIONS): if loop % 500 == 0: print(loop) with Proxy(uri) as p: p.oneway() duration = time.time() - begin print("%d new proxy calls in %.3f sec = %.0f calls/sec" % (ITERATIONS, duration, ITERATIONS / duration)) print("Timing oneway proxy methodcall speed...") p = Proxy(uri) p.oneway() ITERATIONS = 10000 begin = time.time() for loop in range(ITERATIONS): if loop % 1000 == 0: print(loop) p.oneway() duration = time.time() - begin print("%d calls in %.3f sec = %.0f calls/sec" % (ITERATIONS, duration, ITERATIONS / duration)) print("Serializer used:", config.SERIALIZER) Pyro5-5.15/examples/benchmark/server.py000066400000000000000000000003311451404116400201070ustar00rootroot00000000000000import Pyro5.api import Pyro5.socketutil import bench Pyro5.api.serve({ bench.bench: "example.benchmark" }, daemon=Pyro5.api.Daemon(host=Pyro5.socketutil.get_ip_address("", True)), use_ns=False) Pyro5-5.15/examples/blob-dispatch/000077500000000000000000000000001451404116400170135ustar00rootroot00000000000000Pyro5-5.15/examples/blob-dispatch/Readme.txt000066400000000000000000000012611451404116400207510ustar00rootroot00000000000000This shows how you can pass through serialized arguments unchanged via the SerializedBlob class. The idea is that you tell Pyro to NOT serialize/deserialize particular message contents, because you'll be doing that yourself once it reaches the destination. This avoids a lot of serializer overhead (which is quite expensive). This way it is possible to make efficient dispatchers/proxies/routing services for Pyro, where only the actual receiving server at the end, deserializes the package once. Run this example by: - make sure a Pyro name server is running. - start a dispatcher from the dispatcher directory - start one or more listeners from listeners/main.py - start the client. Pyro5-5.15/examples/blob-dispatch/client/000077500000000000000000000000001451404116400202715ustar00rootroot00000000000000Pyro5-5.15/examples/blob-dispatch/client/client.py000066400000000000000000000013251451404116400221220ustar00rootroot00000000000000import sys import datetime import Pyro5.api import Pyro5.errors from customdata import CustomData sys.excepthook = Pyro5.errors.excepthook # teach Serpent how to serialize our data class Pyro5.api.SerializerBase.register_class_to_dict(CustomData, CustomData.to_dict) with Pyro5.api.Proxy("PYRONAME:example.blobdispatcher") as dispatcher: while True: topic = input("Enter topic to send data on (just enter to quit) ").strip() if not topic: break # create our custom data object and send it through the dispatcher data = CustomData(42, "hello world", datetime.datetime.now()) dispatcher.process_blob(Pyro5.api.SerializedBlob(topic, data)) print("processed") Pyro5-5.15/examples/blob-dispatch/client/customdata.py000066400000000000000000000012701451404116400230070ustar00rootroot00000000000000# note: this module is shared with the listeners so they can understand the data passed in class CustomData(object): serialized_classname = "blobdispatch.CustomData" def __init__(self, a, b, c): self.a = a self.b = b self.c = c def to_dict(self): """for (serpent) serialization""" return { "__class__": self.serialized_classname, "a": self.a, "b": self.b, "c": self.c } @classmethod def from_dict(cls, classname, d): """for (serpent) deserialization""" assert classname == cls.serialized_classname obj = cls(d["a"], d["b"], d["c"]) return obj Pyro5-5.15/examples/blob-dispatch/dispatcher/000077500000000000000000000000001451404116400211415ustar00rootroot00000000000000Pyro5-5.15/examples/blob-dispatch/dispatcher/dispatcher.py000066400000000000000000000016351451404116400236460ustar00rootroot00000000000000from collections import defaultdict from Pyro5.api import behavior, expose, serve # note: the dispatcher doesn't know anything about the CustomData class from the customdata module! @behavior(instance_mode="single") class Dispatcher(object): def __init__(self): self.listeners = defaultdict(list) @expose def register(self, topic, listener): self.listeners[topic].append(listener) print("New listener for topic {} registered: {}".format(topic, listener._pyroUri)) @expose def process_blob(self, blob): print("Dispatching blob with name:", blob.info) listeners = self.listeners.get(blob.info, []) for listener in listeners: listener._pyroClaimOwnership() # because this process_blob call may run in a different thread every time it is invoked listener.process_blob(blob) serve({ Dispatcher: "example.blobdispatcher" }) Pyro5-5.15/examples/blob-dispatch/listeners/000077500000000000000000000000001451404116400210235ustar00rootroot00000000000000Pyro5-5.15/examples/blob-dispatch/listeners/customdata.py000066400000000000000000000012661451404116400235460ustar00rootroot00000000000000# note: this module is shared with the client so we can understand the data they pass in class CustomData(object): serialized_classname = "blobdispatch.CustomData" def __init__(self, a, b, c): self.a = a self.b = b self.c = c def to_dict(self): """for (serpent) serialization""" return { "__class__": self.serialized_classname, "a": self.a, "b": self.b, "c": self.c } @classmethod def from_dict(cls, classname, d): """for (serpent) deserialization""" assert classname == cls.serialized_classname obj = cls(d["a"], d["b"], d["c"]) return obj Pyro5-5.15/examples/blob-dispatch/listeners/listener.py000066400000000000000000000014011451404116400232160ustar00rootroot00000000000000from Pyro5.api import expose, Proxy, register_dict_to_class from customdata import CustomData # teach the serializer how to deserialize our custom data class register_dict_to_class(CustomData.serialized_classname, CustomData.from_dict) class Listener(object): def __init__(self, topic): self.topic = topic def register_with_dispatcher(self): with Proxy("PYRONAME:example.blobdispatcher") as dispatcher: dispatcher.register(self.topic, self) @expose def process_blob(self, blob): assert blob.info == self.topic customdata = blob.deserialized() print("Received custom data (type={}):".format(type(customdata))) print(" a={}, b={}, c={}".format(customdata.a, customdata.b, customdata.c)) Pyro5-5.15/examples/blob-dispatch/listeners/main.py000066400000000000000000000007051451404116400223230ustar00rootroot00000000000000import sys from Pyro5.api import Daemon from listener import Listener if len(sys.argv) != 2: print("Give topic as argument.") else: topic = sys.argv[1].strip() if not topic: raise ValueError("Must give topic name.") listener = Listener(topic) daemon = Daemon() daemon.register(listener) listener.register_with_dispatcher() print("Listener for topic {} waiting for data.".format(topic)) daemon.requestLoop() Pyro5-5.15/examples/callback/000077500000000000000000000000001451404116400160345ustar00rootroot00000000000000Pyro5-5.15/examples/callback/Readme.txt000066400000000000000000000050211451404116400177700ustar00rootroot00000000000000These examples shows how you can let a server call back to the client. There are 2 examples. 1) first example: server.py + client.py The client creates some worker objects on the server. It provides them with a callback object that lives in the client. When a worker is done with its task, it will invoke a method on the callback object. That means that this time, the client gets a call from the server that notifies it that a worker has completed its job. (Note: the client uses oneway calls to start up the workers, this ensures that they are running in the background) For all of this to work, the client needs to create a daemon as well: it needs to be able to receive (callback) calls after all. So it creates a daemon, a callback receiver, and starts it all up just like a server would do. The server has to use _pyroClaimOwnership() on the callback proxy, because it may be called from a different thread every time the server is invoked. The client counts the number of 'work completed' callbacks it receives. To remain in the daemon loop, the client provides a special loop condition that is true while the counter is less than the number of workers. Notice that the client sets PYRO_COMMTIMEOUT. That is needed because otherwise it will block in the default requestloop, and it will never evaluate the loopcondition. By setting a timeout we force it to periodically break from the blocking wait and check the loop condition. We could also have used the 'select' servertype instead of setting a PYRO_COMMTIMEOUT, because that one already breaks periodically. (PYRO_POLLTIMEOUT). 2) second example: server2.py + client2.py This example shows how to use the callback decorator to flag a method to be a callback method. This makes Pyro log any exceptions that occur in this method also on the side where the method is running. Otherwise it would just silently pass the exception back to the side that was calling the callback method, and there is no way to see it occur on the callback side itself. It only logs a warning with the error and the traceback though. It doesn't actually print it to the screen, or raise the exception again. So you have to enable logging to see it appear. Also note that this example makes use of Pyro's AutoProxy feature. Sending pyro objects 'over the wire' will automatically convert them into proxies so that the other side will talk to the actual object, instead of a local copy. So the client just sends a callback object to the server, and the server can just return a worker object, as if it was a normal method call. Pyro5-5.15/examples/callback/client.py000066400000000000000000000020771451404116400176720ustar00rootroot00000000000000import random from Pyro5.api import expose, Daemon, Proxy, config # We need to set either a socket communication timeout, # or use the select based server. Otherwise the daemon requestLoop # will block indefinitely and is never able to evaluate the loopCondition. config.COMMTIMEOUT = 0.5 NUM_WORKERS = 5 class CallbackHandler(object): workdone = 0 @expose def done(self, number): print("callback: worker %d reports work is done!" % number) CallbackHandler.workdone += 1 with Daemon() as daemon: # register our callback handler callback = CallbackHandler() daemon.register(callback) # contact the server and put it to work print("creating a bunch of workers") with Proxy("PYRONAME:example.callback") as server: for _ in range(NUM_WORKERS): worker = server.addworker(callback) # provide our callback handler! worker.work(random.randint(1, 5)) print("waiting for all work complete...") daemon.requestLoop(loopCondition=lambda: CallbackHandler.workdone < NUM_WORKERS) print("done!") Pyro5-5.15/examples/callback/client2.py000066400000000000000000000022771451404116400177560ustar00rootroot00000000000000import logging import sys from Pyro5.api import expose, callback, Daemon, Proxy # initialize the logger so you can see what is happening with the callback exception message: logging.basicConfig(stream=sys.stderr, format="[%(asctime)s,%(name)s,%(levelname)s] %(message)s") log = logging.getLogger("Pyro5") log.setLevel(logging.WARNING) class CallbackHandler(object): def crash(self): a = 1 b = 0 return a // b @expose def call1(self): print("\n\ncallback 1 received from server!") print("going to crash - you won't see the exception here, only on the server") return self.crash() @expose @callback def call2(self): print("\n\ncallback 2 received from server!") print("going to crash - but you will see the exception printed here too:") return self.crash() daemon = Daemon() callback_handler = CallbackHandler() daemon.register(callback_handler) with Proxy("PYRONAME:example.callback2") as server: server.doCallback(callback_handler) # this is a oneway call, so we can continue right away print("waiting for callbacks to arrive...") print("(ctrl-c/break the program once it's done)") daemon.requestLoop() Pyro5-5.15/examples/callback/server.py000066400000000000000000000020541451404116400177150ustar00rootroot00000000000000import time from Pyro5.api import expose, oneway, serve class Worker(object): def __init__(self, number, callback): self.number = number self.callback = callback print("Worker %d created" % self.number) @expose @oneway def work(self, amount): print("Worker %d busy..." % self.number) time.sleep(amount) print("Worker %d done. Informing callback client." % self.number) self._pyroDaemon.unregister(self) self.callback._pyroClaimOwnership() # because this method may run in a different thread every time it's called self.callback.done(self.number) # invoke the callback object class CallbackServer(object): def __init__(self): self.number = 0 @expose def addworker(self, callback): self.number += 1 print("server: adding worker %d" % self.number) worker = Worker(self.number, callback) self._pyroDaemon.register(worker) # make it a Pyro object return worker serve({ CallbackServer: "example.callback" }) Pyro5-5.15/examples/callback/server2.py000066400000000000000000000014111451404116400177730ustar00rootroot00000000000000from Pyro5.api import expose, oneway, serve import Pyro5.errors class CallbackServer(object): @expose @oneway def doCallback(self, callback): print("\n\nserver: doing callback 1 to client") callback._pyroClaimOwnership() try: callback.call1() except Exception: print("got an exception from the callback:") print("".join(Pyro5.errors.get_pyro_traceback())) print("\n\nserver: doing callback 2 to client") try: callback.call2() except Exception: print("got an exception from the callback:") print("".join(Pyro5.errors.get_pyro_traceback())) print("server: callbacks done.\n") serve({ CallbackServer: "example.callback2" }) Pyro5-5.15/examples/callcontext/000077500000000000000000000000001451404116400166205ustar00rootroot00000000000000Pyro5-5.15/examples/callcontext/Readme.txt000066400000000000000000000007211451404116400205560ustar00rootroot00000000000000This example shows the use of several advanced Pyro constructs: - overriding proxy and daemon to customize their behavior - using the call context in the server to obtain information about the client - setting and printing correlation ids - using custom message annotations in the client Notice that for performance reasons, the annotation data is returned as a memoryview object. The code converts it into bytes first to be able to print it in a meaningful way. Pyro5-5.15/examples/callcontext/client.py000066400000000000000000000023721451404116400204540ustar00rootroot00000000000000import Pyro5.api import uuid # example: set a single correlation id on the context that should be passed along Pyro5.api.current_context.correlation_id = uuid.uuid4() print("correlation id set to:", Pyro5.api.current_context.correlation_id) uri = input("Enter the URI of the server object: ") print("\n------- get annotations via normal proxy and the call context... -----\n") with Pyro5.api.Proxy(uri) as proxy: print("normal call") Pyro5.api.current_context.annotations = {"XYZZ": b"custom annotation from client (1)"} result = proxy.echo("hi there - new method of annotation access in client") print("Annotations in response were: ") for key, value in Pyro5.api.current_context.response_annotations.items(): print(" ", key, "->", bytes(value)) print("\noneway call") Pyro5.api.current_context.annotations = {"XYZZ": b"custom annotation from client (2)"} proxy.oneway("hi there ONEWAY - new method of annotation access in client") print("Annotations in response were: ") for key, value in Pyro5.api.current_context.response_annotations.items(): print(" ", key, "->", bytes(value)) print("(should be an empty result because oneway!)") print("\nSee the console output on the server for more results.") Pyro5-5.15/examples/callcontext/server.py000066400000000000000000000024151451404116400205020ustar00rootroot00000000000000import Pyro5.api import threading @Pyro5.api.expose class EchoServer(object): def echo(self, message): ctx = Pyro5.api.current_context print("\nGot Message:", message) print(" thread: ", threading.current_thread().ident) print(" obj.pyroid: ", self._pyroId) print(" obj.daemon: ", self._pyroDaemon) print(" context.client: ", ctx.client) print(" context.client_sock_addr: ", ctx.client_sock_addr) print(" context.seq: ", ctx.seq) print(" context.msg_flags: ", ctx.msg_flags) print(" context.serializer_id: ", ctx.serializer_id) print(" context.correlation_id:", ctx.correlation_id) if "XYZZ" in ctx.annotations: print(" custom annotation 'XYZZ':", bytes(ctx.annotations["XYZZ"])) return message @Pyro5.api.oneway def oneway(self, message): return self.echo(message) class CustomDaemon(Pyro5.api.Daemon): def annotations(self): return {"DDAA": b"custom response annotation set by the daemon"} with CustomDaemon() as daemon: uri = daemon.register(EchoServer, "example.context") # provide a logical name ourselves print("Server is ready. You can use the following URI to connect:") print(uri) daemon.requestLoop() Pyro5-5.15/examples/chatbox/000077500000000000000000000000001451404116400157305ustar00rootroot00000000000000Pyro5-5.15/examples/chatbox/Readme.txt000066400000000000000000000016561451404116400176760ustar00rootroot00000000000000Chat box example. This chat box example is constructed as follows: A Chat Server (Pyro object) handles the login/logoff process and keeps track of all chat channels and clients that are subscribed to each channel. It implements the chatting and distributing of chat messages to all subscribers. It uses a oneway call for that to improve performance with a large number of subscribers, and to avoid blocking. The chat client runs the user input processing in the main thread. It runs another thread with the Pyro daemon that is listening for server chat messages, so that they can be printed while the main thread is still waiting for user input. Also note that this example makes use of Pyro's AutoProxy feature. Sending pyro objects 'over the wire' will automatically convert them into proxies so that the other side will talk to the actual object, instead of a local copy. So the client just sends the callback object to the server. Pyro5-5.15/examples/chatbox/client.py000066400000000000000000000043261451404116400175650ustar00rootroot00000000000000import threading import contextlib from Pyro5.api import expose, oneway, Proxy, Daemon # The daemon is running in its own thread, to be able to deal with server # callback messages while the main thread is processing user input. class Chatter(object): def __init__(self): self.chatbox = Proxy('PYRONAME:example.chatbox.server') self.abort = 0 @expose @oneway def message(self, nick, msg): if nick != self.nick: print('[{0}] {1}'.format(nick, msg)) def start(self): nicks = self.chatbox.getNicks() if nicks: print('The following people are on the server: %s' % (', '.join(nicks))) channels = sorted(self.chatbox.getChannels()) if channels: print('The following channels already exist: %s' % (', '.join(channels))) self.channel = input('Choose a channel or create a new one: ').strip() else: print('The server has no active channels.') self.channel = input('Name for new channel: ').strip() self.nick = input('Choose a nickname: ').strip() people = self.chatbox.join(self.channel, self.nick, self) print('Joined channel %s as %s' % (self.channel, self.nick)) print('People on this channel: %s' % (', '.join(people))) print('Ready for input! Type /quit to quit') try: with contextlib.suppress(EOFError): while not self.abort: line = input('> ').strip() if line == '/quit': break if line: self.chatbox.publish(self.channel, self.nick, line) finally: self.chatbox.leave(self.channel, self.nick) self.abort = 1 self._pyroDaemon.shutdown() class DaemonThread(threading.Thread): def __init__(self, chatter): threading.Thread.__init__(self) self.chatter = chatter self.daemon = True def run(self): with Daemon() as daemon: daemon.register(self.chatter) daemon.requestLoop(lambda: not self.chatter.abort) chatter = Chatter() daemonthread = DaemonThread(chatter) daemonthread.start() chatter.start() print('Exit.') Pyro5-5.15/examples/chatbox/server.py000066400000000000000000000050261451404116400176130ustar00rootroot00000000000000from Pyro5.api import expose, behavior, serve import Pyro5.errors # Chat box administration server. # Handles logins, logouts, channels and nicknames, and the chatting. @expose @behavior(instance_mode="single") class ChatBox(object): def __init__(self): self.channels = {} # registered channels { channel --> (nick, client callback) list } self.nicks = [] # all registered nicks on this server def getChannels(self): return list(self.channels.keys()) def getNicks(self): return self.nicks def join(self, channel, nick, callback): if not channel or not nick: raise ValueError("invalid channel or nick name") if nick in self.nicks: raise ValueError('this nick is already in use') if channel not in self.channels: print('CREATING NEW CHANNEL %s' % channel) self.channels[channel] = [] self.channels[channel].append((nick, callback)) self.nicks.append(nick) print("%s JOINED %s" % (nick, channel)) self.publish(channel, 'SERVER', '** ' + nick + ' joined **') return [nick for (nick, c) in self.channels[channel]] # return all nicks in this channel def leave(self, channel, nick): if channel not in self.channels: print('IGNORED UNKNOWN CHANNEL %s' % channel) return for (n, c) in self.channels[channel]: if n == nick: self.channels[channel].remove((n, c)) break self.publish(channel, 'SERVER', '** ' + nick + ' left **') if len(self.channels[channel]) < 1: del self.channels[channel] print('REMOVED CHANNEL %s' % channel) self.nicks.remove(nick) print("%s LEFT %s" % (nick, channel)) def publish(self, channel, nick, msg): if channel not in self.channels: print('IGNORED UNKNOWN CHANNEL %s' % channel) return for (n, c) in self.channels[channel][:]: # use a copy of the list c._pyroClaimOwnership() try: c.message(nick, msg) # oneway call except Pyro5.errors.ConnectionClosedError: # connection dropped, remove the listener if it's still there # check for existence because other thread may have killed it already if (n, c) in self.channels[channel]: self.channels[channel].remove((n, c)) print('Removed dead listener %s %s' % (n, c)) serve({ ChatBox: "example.chatbox.server" }) Pyro5-5.15/examples/circular/000077500000000000000000000000001451404116400161045ustar00rootroot00000000000000Pyro5-5.15/examples/circular/Readme.txt000066400000000000000000000011221451404116400200360ustar00rootroot00000000000000Create a chain of objects calling each other: client --> A --> B ^ | | v | `----- C I.e. C calls A again. A detects that the message went full circle and returns the result (a 'trace' of the route) to the client. (the detection checks if the name of the current server is already in the current trace of the route, i.e., if it arrives for a second time on the same server, it concludes that we're done). First start the three servers (servA,B,C) and then run the client. You need to have a nameserver running. Pyro5-5.15/examples/circular/chain.py000066400000000000000000000015171451404116400175440ustar00rootroot00000000000000from Pyro5.api import expose, Proxy # a Chain member. Passes messages to the next link, # until the message went full-circle: then it exits. class Chain(object): def __init__(self, name, next_node): self.name = name self.nextName = next_node self.next = None @expose def process(self, message): if self.next is None: self.next = Proxy("PYRONAME:example.chain." + self.nextName) if self.name in message: print("Back at %s; we completed the circle!" % self.name) return ["complete at " + self.name] else: print("I'm %s, passing to %s" % (self.name, self.nextName)) message.append(self.name) result = self.next.process(message) result.insert(0, "passed on from " + self.name) return result Pyro5-5.15/examples/circular/client.py000066400000000000000000000001621451404116400177330ustar00rootroot00000000000000from Pyro5.api import Proxy obj = Proxy("PYRONAME:example.chain.A") print("Result=%s" % obj.process(["hello"])) Pyro5-5.15/examples/circular/servA.py000066400000000000000000000006101451404116400175330ustar00rootroot00000000000000import Pyro5.api import chain this_node = "A" next_node = "B" servername = "example.chain." + this_node with Pyro5.api.Daemon() as daemon: obj = chain.Chain(this_node, next_node) uri = daemon.register(obj) with Pyro5.api.locate_ns() as ns: ns.register(servername, uri) # enter the service loop. print("Server started %s" % this_node) daemon.requestLoop() Pyro5-5.15/examples/circular/servB.py000066400000000000000000000006101451404116400175340ustar00rootroot00000000000000import Pyro5.api import chain this_node = "B" next_node = "C" servername = "example.chain." + this_node with Pyro5.api.Daemon() as daemon: obj = chain.Chain(this_node, next_node) uri = daemon.register(obj) with Pyro5.api.locate_ns() as ns: ns.register(servername, uri) # enter the service loop. print("Server started %s" % this_node) daemon.requestLoop() Pyro5-5.15/examples/circular/servC.py000066400000000000000000000006101451404116400175350ustar00rootroot00000000000000import Pyro5.api import chain this_node = "C" next_node = "A" servername = "example.chain." + this_node with Pyro5.api.Daemon() as daemon: obj = chain.Chain(this_node, next_node) uri = daemon.register(obj) with Pyro5.api.locate_ns() as ns: ns.register(servername, uri) # enter the service loop. print("Server started %s" % this_node) daemon.requestLoop() Pyro5-5.15/examples/custom-serialization/000077500000000000000000000000001451404116400204655ustar00rootroot00000000000000Pyro5-5.15/examples/custom-serialization/Readme.txt000077500000000000000000000007071451404116400224320ustar00rootroot00000000000000Shows the use of the serializer hooks to be able to transfer custom classes via Pyro. If you don't use the serializer hooks, the code will crash with a SerializeError: unsupported serialized class, but now, it will happily transfer your object using the custom serialization hooks. It is recommended to avoid using these hooks if possible, there's a security risk to create arbitrary objects from serialized data that is received from untrusted sources. Pyro5-5.15/examples/custom-serialization/client.py000077500000000000000000000030551451404116400223230ustar00rootroot00000000000000from Pyro5.api import Proxy, config, register_dict_to_class, register_class_to_dict import mycustomclasses # use serpent config.SERIALIZER = "serpent" # register the special serialization hooks def thingy_class_to_dict(obj): print("{serializer hook, converting to dict: %s}" % obj) return { "__class__": "waheeee-custom-thingy", "number-attribute": obj.number } def thingy_dict_to_class(classname, d): print("{deserializer hook, converting to class: %s}" % d) return mycustomclasses.Thingy(d["number-attribute"]) def otherthingy_dict_to_class(classname, d): print("{deserializer hook, converting to class: %s}" % d) return mycustomclasses.OtherThingy(d["number"]) # for 'Thingy' we register both serialization and deserialization hooks register_class_to_dict(mycustomclasses.Thingy, thingy_class_to_dict) register_dict_to_class("waheeee-custom-thingy", thingy_dict_to_class) # for 'OtherThingy' we only register a deserialization hook (and for serialization depend on serpent's default behavior) register_dict_to_class("mycustomclasses.OtherThingy", otherthingy_dict_to_class) # regular pyro stuff uri = input("Enter the URI of the server object: ") serv = Proxy(uri) print("\nTransferring thingy...") o = mycustomclasses.Thingy(42) response = serv.method(o) print("type of response object:", type(response)) print("response:", response) print("\nTransferring otherthingy...") o = mycustomclasses.OtherThingy(42) response = serv.othermethod(o) print("type of response object:", type(response)) print("response:", response) Pyro5-5.15/examples/custom-serialization/mycustomclasses.py000077500000000000000000000006251451404116400243030ustar00rootroot00000000000000# defines custom classes class Thingy(object): def __init__(self, num): self.number = num def __str__(self): return "" class OtherThingy(object): def __init__(self, num): self.number = num def __str__(self): return "" Pyro5-5.15/examples/custom-serialization/server.py000077500000000000000000000030551451404116400223530ustar00rootroot00000000000000from Pyro5.api import expose, serve, config, register_class_to_dict, register_dict_to_class import mycustomclasses # use serpent config.SERIALIZER = "serpent" # register the special serialization hooks def thingy_class_to_dict(obj): print("{serializer hook, converting to dict: %s}" % obj) return { "__class__": "waheeee-custom-thingy", "number-attribute": obj.number } def thingy_dict_to_class(classname, d): print("{deserializer hook, converting to class: %s}" % d) return mycustomclasses.Thingy(d["number-attribute"]) def otherthingy_dict_to_class(classname, d): print("{deserializer hook, converting to class: %s}" % d) return mycustomclasses.OtherThingy(d["number"]) # for 'Thingy' we register both serialization and deserialization hooks register_dict_to_class("waheeee-custom-thingy", thingy_dict_to_class) register_class_to_dict(mycustomclasses.Thingy, thingy_class_to_dict) # for 'OtherThingy' we only register a deserialization hook (and for serialization depend on serpent's default behavior) register_dict_to_class("mycustomclasses.OtherThingy", otherthingy_dict_to_class) # regular Pyro server stuff @expose class Server(object): def method(self, arg): print("\nmethod called, arg=", arg) response = mycustomclasses.Thingy(999) return response def othermethod(self, arg): print("\nothermethod called, arg=", arg) response = mycustomclasses.OtherThingy(999) return response serve( { Server: "example.customclasses" }, use_ns=False) Pyro5-5.15/examples/diffie-hellman/000077500000000000000000000000001451404116400171445ustar00rootroot00000000000000Pyro5-5.15/examples/diffie-hellman/Readme.txt000066400000000000000000000020731451404116400211040ustar00rootroot00000000000000Diffie-Hellman key exchange. Sometimes the server and client have to use the same symmetric private key, for instance to do symmetric data encryption. It can be problematic to distribute such a shared private key among your client and server code, as you may not want to hardcode it (especially in the client!) There's are secure algorithms to tackle the "key exchange" problem, and this example shows one of them: the Diffie-Hellman key exchange. It's based on calculating stuff with large prime exponenents and modulos, but in the end, both the client and server agree on a shared secret key that: a) has never publicly been sent over the wire, b) is not hardcoded anywhere. IMPORTANT NOTE: In this particular example there is NO ENCRYPTION done whatsoever. Encryption is a different topic! If you want, you can enable SSL/TLS in Pyro as well to provide this. However, if you use 2-way-ssl, this makes the use of a shared private key somewhat obsolete, because mutual verification of the SSL certificates essentially does the same thing. See the SSL example for more details. Pyro5-5.15/examples/diffie-hellman/client.py000066400000000000000000000007341451404116400210000ustar00rootroot00000000000000from Pyro5.api import Proxy from diffiehellman import DiffieHellman dh = DiffieHellman(group=14) with Proxy("PYRONAME:example.dh.keyexchange") as keyex: print("exchange public keys...") other_key = keyex.exchange_key(dh.public_key) print("got server public key, creating shared secret key...") dh.make_shared_secret_and_key(other_key) print("shared secret key = ", dh.key.hex()) print("(check the server output to see the same shared private key)") Pyro5-5.15/examples/diffie-hellman/diffiehellman.py000066400000000000000000000215651451404116400223160ustar00rootroot00000000000000import os import hashlib class DiffieHellman(object): def __init__(self, num_bits=544, group=17): if num_bits % 8 != 0: raise ValueError("private key size should be multiple of 8 bits") self.num_bits = num_bits self.prime = self.get_prime(group) self.generator = 2 self.private_key = self.make_private_key() self.public_key = self.make_public_key() self.shared_secret = None self.__key = None def __str__(self): return """""".format(self.num_bits, self.public_key, self.private_key, self.shared_secret) def get_prime(self, group): """ Return a huge prime number. Groups according to http://www.rfc-base.org/txt/rfc-3526.txt Group 17 is secure enough for an AES256 key if used with a private key (exponent) of >=540 bits """ if group == 5: return 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA237327FFFFFFFFFFFFFFFF elif group == 14: return 0x0FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF elif group == 15: return 0x0FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF elif group == 16: return 0x0FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199FFFFFFFFFFFFFFFF elif group == 17: return 0xFFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C93402849236C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B332051512BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97FBEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AACC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58BB7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E6DCC4024FFFFFFFFFFFFFFFF elif group == 18: return 0x0FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C93402849236C3FAB4D27C7026C1D4DCB2602646DEC9751E763DBA37BDF8FF9406AD9E530EE5DB382F413001AEB06A53ED9027D831179727B0865A8918DA3EDBEBCF9B14ED44CE6CBACED4BB1BDB7F1447E6CC254B332051512BD7AF426FB8F401378CD2BF5983CA01C64B92ECF032EA15D1721D03F482D7CE6E74FEF6D55E702F46980C82B5A84031900B1C9E59E7C97FBEC7E8F323A97A7E36CC88BE0F1D45B7FF585AC54BD407B22B4154AACC8F6D7EBF48E1D814CC5ED20F8037E0A79715EEF29BE32806A1D58BB7C5DA76F550AA3D8A1FBFF0EB19CCB1A313D55CDA56C9EC2EF29632387FE8D76E3C0468043E8F663F4860EE12BF2D5B0B7474D6E694F91E6DBE115974A3926F12FEE5E438777CB6A932DF8CD8BEC4D073B931BA3BC832B68D9DD300741FA7BF8AFC47ED2576F6936BA424663AAB639C5AE4F5683423B4742BF1C978238F16CBE39D652DE3FDB8BEFC848AD922222E04A4037C0713EB57A81A23F0C73473FC646CEA306B4BCBC8862F8385DDFA9D4B7FA2C087E879683303ED5BDD3A062B3CF5B3A278A66D2A13F83F44F82DDF310EE074AB6A364597E899A0255DC164F31CC50846851DF9AB48195DED7EA1B1D510BD7EE74D73FAF36BC31ECFA268359046F4EB879F924009438B481C6CD7889A002ED5EE382BC9190DA6FC026E479558E4475677E9AA9E3050E2765694DFC81F56E880B96E7160C980DD98EDD3DFFFFFFFFFFFFFFFFF raise ValueError("invalidprimenumber group, use one of 5, 14, 15, 16, 17, 18.") def make_private_key(self): """make a private key from data returned by the secure random number generator.""" random = os.urandom(self.num_bits//8) return int.from_bytes(random, "big") def make_public_key(self): """Generate a public key X with g^x mod p.""" return pow(self.generator, self.private_key, self.prime) def check_public_key(self, public_key): if 2 < public_key < self.prime - 1: if pow(public_key, (self.prime - 1)//2, self.prime) == 1: return True return False def make_shared_secret_and_key(self, other_key): """Compute shared secret (g^x mod p)^y mod p, which is actually: other_key ^ y mod p""" if not self.check_public_key(other_key): raise ValueError("other key is not a valid public key") self.shared_secret = pow(other_key, self.private_key, self.prime) self.__key = hashlib.sha256(self.shared_secret.to_bytes(self.shared_secret.bit_length()//8+1, 'big')).digest() @property def key(self): if self.__key is not None: return self.__key else: raise ValueError("shared key has not been calculated!") if __name__ == "__main__": a = DiffieHellman(group=5) b = DiffieHellman(group=5) a.make_shared_secret_and_key(b.public_key) b.make_shared_secret_and_key(a.public_key) print(a) print(b) print("Same key?", a.key == b.key) Pyro5-5.15/examples/diffie-hellman/server.py000066400000000000000000000012711451404116400210250ustar00rootroot00000000000000from Pyro5.api import behavior, expose, locate_ns, serve, config from diffiehellman import DiffieHellman config.SERVERTYPE = "multiplex" ns = locate_ns() @behavior(instance_mode="session") class KeyExchange(object): def __init__(self): print("New KeyExchange, initializing Diffie-Hellman") self.dh = DiffieHellman(group=14) @expose def exchange_key(self, other_public_key): print("received a public key, calculating shared secret...") self.dh.make_shared_secret_and_key(other_public_key) print("shared secret key = ", self.dh.key.hex()) return self.dh.public_key serve({ KeyExchange: "example.dh.keyexchange" }, use_ns=True) Pyro5-5.15/examples/disconnects/000077500000000000000000000000001451404116400166145ustar00rootroot00000000000000Pyro5-5.15/examples/disconnects/Readme.txt000066400000000000000000000021171451404116400205530ustar00rootroot00000000000000Example code that shows a possible way to deal with client disconnects in the server. It sets the COMMTIMEOUT config item on the server side. This will make the connections timeout after the given time if no more data is received. That connection will then be terminated. The problem with this is, that a client that is still connected but simply takes too long between remote method calls, will encounter a ConnectionClosedError. But you can use Pyro's auto-reconnect feature to deal with this. The client.py code creates a special Proxy class that you use instead of Pyro's default, which will automatically do this for you on every method call. Alternatively you can do it explicitly in your own code like the 'autoreconnect' client example does. A drawback of the code shown is that it is not very efficient; it now requires two remote messages for every method invocation on the proxy. Note that the custom proxy class shown in the client uses some advanced features of the Pyro API: - overrides internal method that handles method calls - creates and receives custom wire protocol messages Pyro5-5.15/examples/disconnects/client.py000066400000000000000000000045041451404116400204470ustar00rootroot00000000000000import warnings import Pyro5.api import Pyro5.protocol import Pyro5.errors warnings.filterwarnings("ignore") print("You can run this client on a different computer so you can disable the network connection (by yanking out the lan cable or whatever).") print("Alternatively, wait for a timeout on the server, which will then close its connection.") uri = input("Uri of server? ").strip() class AutoReconnectingProxy(Pyro5.api.Proxy): """ A Pyro proxy that automatically recovers from a server disconnect. It does this by intercepting every method call and then it first 'pings' the server to see if it still has a working connection. If not, it reconnects the proxy and retries the method call. Drawback is that every method call now uses two remote messages (a ping, and the actual method call). This uses some advanced features of the Pyro API. """ def _pyroInvoke(self, *args, **kwargs): # We override the method that does the actual remote calls: _pyroInvoke. # If there's still a connection, try a ping to see if it is still alive. # If it isn't alive, reconnect it. If there's no connection, simply call # the original method (it will reconnect automatically). if self._pyroConnection: try: print(" ") Pyro5.protocol.SendingMessage.ping(self._pyroConnection) # utility method on the Message class print(" ") except Pyro5.errors.ConnectionClosedError: print(" ") self._pyroReconnect() print(" ") return super(AutoReconnectingProxy, self)._pyroInvoke(*args, **kwargs) with AutoReconnectingProxy(uri) as obj: result = obj.echo("12345") print("result =", result) print("\nClient proxy connection is still open. Disable the network now (or wait until the connection timeout on the server expires) and see what the server does.") print("Once you see on the server that it got a timeout or a disconnect, enable the network again.") input("Press enter to continue:") print("\nDoing a new call on the same proxy:") result = obj.echo("12345") print("result =", result) Pyro5-5.15/examples/disconnects/server.py000066400000000000000000000006521451404116400204770ustar00rootroot00000000000000import logging from Pyro5.api import expose, serve, config logging.basicConfig(level=logging.DEBUG) logging.getLogger("Pyro5").setLevel(logging.DEBUG) config.COMMTIMEOUT = 5.0 config.POLLTIMEOUT = 5.0 # only used for multiplexing server class TestDisconnect(object): @expose def echo(self, arg): print("echo: ", arg) return arg serve({ TestDisconnect: "example.disconnect" }, use_ns=False) Pyro5-5.15/examples/distributed-computing/000077500000000000000000000000001451404116400206255ustar00rootroot00000000000000Pyro5-5.15/examples/distributed-computing/Readme.txt000066400000000000000000000023641451404116400225700ustar00rootroot00000000000000A simple distributed computing example with "pull" model. There is a single central work dispatcher/gatherer that is contacted by every worker you create. The worker asks the dispatcher for a chunk of work data and returns the results when it is done. The worker in this example finds the prime factorials for the numbers that it gets as 'work' from the dispatcher, and returns the list of factorials as 'result' to the dispatcher. The client program generates a list of random numbers and sends each number as a single work item to the dispatcher. It collects the results and prints them to the screen once everything is complete. *** Starting up *** - We're using a Name Server, so start one. - start the dispatcher (dispatcher.py) - start one or more workers (worker.py). For best results, start one of these on every machine/CPU in your network :-) - finally, give the system a task to solve: start the client.py program. Note: The dispatcher is pretty braindead. It only has a single work and result queue. Running multiple clients will probably break the system. Improvements are left as an exercise. Note: because the workitem is a custom class that is passed over the network, we use a custom class deserializer to be able to get it back from Pyro. Pyro5-5.15/examples/distributed-computing/client.py000066400000000000000000000035231451404116400224600ustar00rootroot00000000000000import random from Pyro5.api import Proxy, register_dict_to_class from workitem import Workitem # For 'workitem.Workitem' we register a deserialization hook to be able to get these back from Pyro register_dict_to_class("workitem.Workitem", Workitem.from_dict) NUMBER_OF_ITEMS = 40 def main(): print("\nThis program will calculate Prime Factorials of a bunch of random numbers.") print("The more workers you will start (on different cpus/cores/machines),") print("the faster you will get the complete list of results!\n") with Proxy("PYRONAME:example.distributed.dispatcher") as dispatcher: placework(dispatcher) numbers = collectresults(dispatcher) printresults(numbers) def placework(dispatcher): print("placing work items into dispatcher queue.") for i in range(NUMBER_OF_ITEMS): number = random.randint(3211, 4999999) * random.randint(3211, 999999) item = Workitem(i + 1, number) dispatcher.putWork(item) def collectresults(dispatcher): print("getting results from dispatcher queue.") numbers = {} while len(numbers) < NUMBER_OF_ITEMS: try: item = dispatcher.getResult() except ValueError: print("Not all results available yet (got %d out of %d). Work queue size: %d" % (len(numbers), NUMBER_OF_ITEMS, dispatcher.workQueueSize())) else: print("Got result: %s (from %s)" % (item, item.processedBy)) numbers[item.data] = item.result if dispatcher.resultQueueSize() > 0: print("there's still stuff in the dispatcher result queue, that is odd...") return numbers def printresults(numbers): print("\nComputed Prime Factorials follow:") for (number, factorials) in numbers.items(): print("%d --> %s" % (number, factorials)) if __name__ == "__main__": main() Pyro5-5.15/examples/distributed-computing/dispatcher.py000066400000000000000000000022631451404116400233300ustar00rootroot00000000000000import queue from Pyro5.api import expose, behavior, serve, register_dict_to_class from workitem import Workitem # For 'workitem.Workitem' we register a deserialization hook to be able to get these back from Pyro register_dict_to_class("workitem.Workitem", Workitem.from_dict) @expose @behavior(instance_mode="single") class DispatcherQueue(object): def __init__(self): self.workqueue = queue.Queue() self.resultqueue = queue.Queue() def putWork(self, item): self.workqueue.put(item) def getWork(self, timeout=5): try: return self.workqueue.get(block=True, timeout=timeout) except queue.Empty: raise ValueError("no items in queue") def putResult(self, item): self.resultqueue.put(item) def getResult(self, timeout=5): try: return self.resultqueue.get(block=True, timeout=timeout) except queue.Empty: raise ValueError("no result available") def workQueueSize(self): return self.workqueue.qsize() def resultQueueSize(self): return self.resultqueue.qsize() # main program serve({ DispatcherQueue: "example.distributed.dispatcher" }) Pyro5-5.15/examples/distributed-computing/worker.py000066400000000000000000000026711451404116400225160ustar00rootroot00000000000000import os import socket import sys from math import sqrt from Pyro5.api import Proxy, register_dict_to_class from workitem import Workitem # For 'workitem.Workitem' we register a deserialization hook to be able to get these back from Pyro register_dict_to_class("workitem.Workitem", Workitem.from_dict) WORKERNAME = "Worker_%d@%s" % (os.getpid(), socket.gethostname()) def factorize(n): """simple algorithm to find the prime factorials of the given number n""" def isPrime(n): return not any(x for x in range(2, int(sqrt(n)) + 1) if n % x == 0) primes = [] candidates = range(2, n + 1) candidate = 2 while not primes and candidate in candidates: if n % candidate == 0 and isPrime(candidate): primes = primes + [candidate] + factorize(n // candidate) candidate += 1 return primes def process(item): print("factorizing %s -->" % item.data) sys.stdout.flush() item.result = factorize(int(item.data)) print(item.result) item.processedBy = WORKERNAME def main(): dispatcher = Proxy("PYRONAME:example.distributed.dispatcher") print("This is worker %s" % WORKERNAME) print("getting work from dispatcher.") while True: try: item = dispatcher.getWork() except ValueError: print("no work available yet.") else: process(item) dispatcher.putResult(item) if __name__ == "__main__": main() Pyro5-5.15/examples/distributed-computing/workitem.py000066400000000000000000000011321451404116400230350ustar00rootroot00000000000000class Workitem(object): def __init__(self, itemId, data): print("Created workitem %s" % itemId) self.itemId = itemId self.data = data self.result = None self.processedBy = None def __str__(self): return "" % str(self.itemId) @staticmethod def from_dict(classname, d): """this method is used to deserialize a workitem from Pyro""" assert classname == "workitem.Workitem" w = Workitem(d["itemId"], d["data"]) w.result = d["result"] w.processedBy = d["processedBy"] return w Pyro5-5.15/examples/distributed-computing2/000077500000000000000000000000001451404116400207075ustar00rootroot00000000000000Pyro5-5.15/examples/distributed-computing2/Readme.txt000066400000000000000000000024671451404116400226560ustar00rootroot00000000000000A simple distributed computing example with "push" model. There are a handful of word-counter instances. Each is able to count the word frequencies in the lines of text given to it. The counters are registered in the name server using a common name prefix, which is used by the dispatcher to get a list of all available counters. The client reads the text (Alice in Wonderland by Lewis Carroll, downloaded from Project Gutenberg) and hands it off to the counters to determine the word frequencies. To demonstrate the massive speedup you can potentially get, it first uses just a single counter to process the full text. Then it does it again but now uses the dispatcher counter instead - which distributes chunks of the text across all available counters in parallel. Make sure a name server is running before starting this example. NOTE: ----- This particular example is not a "real" distributed calculation because it uses *threads* to process multiple Pyro calls concurrently. Because of Python's GIL, threads will NOT run in parallel unless they wait for a signal or are doing I/O. This is why this example has an artificial timer delay to make the compute calls not cpu-bound thereby enabling actual parallel execution. For "true" distributed parallel calculations, have a look at the other distributed-computing examples. Pyro5-5.15/examples/distributed-computing2/alice.zip000066400000000000000000001604711451404116400225210ustar00rootroot00000000000000PKý¸ÖFc~L¡à^Ž alice.txt„YÝn35½¯ÔwpÅE¥¿´¨´¥Em> .œ¬“]âµÛÛ^ ‰WàÌñºÛP$ôék“]{¨TÕG£üFiwðÎȯ}m>$å¼Zû˜ª}“êÓm[ùG¦Ð¬Sã]TûZ§èÍ“ 3¥~ô½jõ[»ƒjÒTm›'ƒJïñÔ‡Ó“`ÎûÈG½(L-’ m„üòÊdu {ìqkÛW¦:=…T-‚5ÞÙÆQóý~?Û–Í3¶0_Ð$k.þ×{²ôªOµ¯øи­ºÑ ¢¾îqàOÕï½÷‰úyNUÞzÿý_NOŒ5:šaá·:¬ë©zÿÓO?:=ùùVGßUxU]¨³6-4…„ÝrЭvÛ^o±wî¶¶‰u¶áÝwßUË«‡¥ºÿB-¿Z<ªï_/Õ—o–ó»Ïç_ªùç÷÷ߨ«ÛÅõ|ò¨®n¾Ÿß-ß<ÌÕâNýpw3¸½º»QE™¯þýßNYó]_ÍÕ·‹ÛÛùÝÝâÍ·ê‹7·×ø=¿Y,÷wêÃÙ{ã×_]}·œ?¨ÅLÝø½cÐôjÕ¤ó¯¼5²„AR{ÕÊlçÄçÉ«­I 8;¨ÔS `b“ ÀÅטðË‹PZi·›Á\Zë'Yé|ªq•¿ÀR$ɰ—cm°®R1h\òƒ$ íxU FW„tëðÌHŽ®YV"uíôš™q§'’hÄ´A&e¡’c¦<óý¶N*»a"h÷8£Hf‰þl"^{ôbuÃÛØÑbi㨹¸ºmpòÛx¿7Ö*¹~í{[MI ´µöIUÈÖVW†;7ÆØì÷há˜=Sß5Õ;SbR=$r'¨ï„ø öïäx aM<œ¯k Mör¢Š!Õ9ûƒïW–ØšξSLEXLXF)‰<ЩØW•qº¨ê&‘ªà(·Sæ`#íà*ë£ 2#Zñ!û© !zõýüáGÄ´Õa§EF@¤Ó%ÖU5Õ Y MØrzÂ=m¿®âSL˜.y¦Ãˆm¬çcìŒÆn‚ûZUXrvzR>©â¡­÷X°ÃÙD½…”‚ Hòˆ…Òàp¯C§x‡¯×}¬àѺs#·ñ©~‚r$:,Ó‰Jôò‹œÑ´F ªD‰hL‹u¿õâ]§¤( h<˜é0åñÀà }¶iŒ…>Œ_Ù/¦¤Þ!æö@\þÚÇ2"bt4,™ïT%‰¬°E°B&ÎÄù¢ Â[C¨/° §S|k\Êû÷Æz¡‹ÈˆGüQû=@!gÖÚª°L¡d/Ú"»Kr©—Zñ¨˜‚n€C¡fÛÀZÁ^g,ƒ}Ë´1pzR5PpÉsª=ÅÊñ ãKê…ÅÈäs¢ÂÏ+Q/&ßu4ÆÉ¿g H@<÷üb caq3i¯ëžgÒÂyS<*®×.‹W…5ýވׅž`ˆ1.\¼/‚LEý júÆÌú!'Ÿžl½èˇËÂø{š©/šS`’l*)²Ú‚cˆŒ¥£ÍòJaBz jÌ`d«Hæ¼Ì)¥pŒé,®ü°©+~’è2Lˆ³“ëx$+¬5¥Ó\÷ÝÊ µÉÅs`ŸL¼O˜“!€QïaL—W—"‰:Ö³’qÙÆ™zWŒP¿jIð-0iŠžÃ1GvH§c4Õeñ‚Õ+C='÷Ww_¢ç¹zøöêöêf>¡· ?mƒÑI0„' (‹ÓvépAÍ«†aJ°# ¾£ÔÍã¿ÑÅt×–L••¯L…V;½Í¡í*v,/í=I‹ ,a–,'š'?àÉÙ¿›ŽlÈPª2ED©sš i¹z”’uzÂt+õ”Ç÷íjÌ$8y¦¾ò{µ ¨DŒýDDáÞ,i7˜x†ª~éìÜ$—Ϻœä,P–SÍ+i˜ßd»“GÂ>€]‘µ,(ÙJ§ „ÞÌÞOÜbªçŸ3õƒœN!´únŽ‚/ Bÿ€Ö«ÎÔdQr’LÙBK -&Oy§qh?è1fûg“·¿¶¾¯f"¤Î_™Ò!\8(Ïc®ôkà(8·i|ùJ©%"ÂCNoâ7•ΔWJBÔ±t˜è‰ì Wd¦†táóPÇSèž36#GÎq^‰M ±ù‚å¢L×bÓøŒ—N•…NR¦Ï¹¥æHÐW¬ xtYÝvQ’aËœâÄÆÖÌûÍx›W8Ô‡û€c©5 ÎcãÄVnw :ŠWc¶‚ú\TîócïòTË-}Ç—ÄÒ%CÁd‚58z3§$me³nj›ª²f*Ä:Þ± VÐ×w+C~ç¢Ò¥V£úsnÍ–S¡^á1ƒW¹ÌTo› ‚-ÐŽÉýUeõN‚o~_›Nð пZœ©v&“zù "Ñ„8v¥¹Jj& ‹œÈÌ:H!ùRBs Þî%ó¡ÓYacü`|"Þ÷]°Æó¥hó|-[‰0ŰÃÎP!&#J¶ã«|t;“†V< ‘ÃÈbÖ^ÊŒ#|¦¯Ýì÷ìÍ´Ð  ôr©1PÌÆc6‰¦%FôÀ0alšM¤5n]`n»olrŸ:죀ÕÏþáIG—ª2–ÞoD|J¦:Wœ@¹Y nf~>GΖ¿zèìÖ‘G3ÂÈHÙ’ïPó=_V|çŒMúѯ-£A³xH6ã  i«L«SÔ{F‰‚É.4µd…ß V¢6ižYžs?LñpJ2»UDÞ*·þ¤”c•W¬½·tˆ8’42 ŒxÝ@ j“Gõ¢áΉº¸äŸxÊÝ„p2—ˆdÙóâ/=6æÌÿû•,; ÄÀ_ \”áà@±öýJÇNœò$<Â×ÓÝšflBÁ Š=öx¬‘Z­–B9ß%<7LP±÷°Uä~Ö ÊÚŽ·^~Ì›W®´kGaªe×¥zO¸äJË—VQ Àžã…VœyÉÎNø”õ§Ïâú°¹Ž÷¿N¤ @‘æ×KØFˆÎ%Û†búT/»i/¼P‡ÓıÅ)³âÚ:À[IîWhÃÌGGuŒ)ç»C(w+A¨W°èJöØ` rJC'œjC¶/áM¸-ÇÁ–ÌBMB“¿ñ'·óx€ã6ϲÆQ©7×È>—r*é¦êEÁÐTž6|`Mþág^YFîlZ‚OÎì#’ HKiMÙÁ®*LJ‚ܵxqÁ¢WÏ€µ¯÷äG=+y®‘ÔÆýfõâéâùƒ%ì]ÝæÈÐ~Dd«wr¤û}K¢'Q(šN["zHXvT‰‰=;gÓŸ}pñ ¡{ó-­}ã@¡ZÜy'~ÔìÉ0‹¤¾á[ùª*Ò"M±Çq3èZ€D­ËPn¡i]ÆÁÒ¢Ë[S_Ý_+cb TãøÜ:Ró¥ëA÷wxÈÀ[©©a|—,ŠõàU{eè³A¨ÃËŽ;uVY•GX(­ÃðM}LQ‹¢CÿþÙ}½KRÆË7g fƒÝíÖ¤‡Ú¢Òöf–x³Fpo×w85ˆ‘»ëµs¡@·Ñ%9%霾¢‰Út$e G”ì§fI ·ª8ݦ­4’¢—ˆ`k5]—FØVÓc*?g-×QŽK¬³;û@—q ÕR®™<Ñâà¡Q±G Œå¢•„Öô£`, sÅê4h^¼|wö´¢Q䉓µ`GÌ<N PQ³>P…ðޱ•Ë›ºáµR]CaøÞ®Ùüü&Óâ@"à÷×Ù*­`töLq+Ы½$& CT8ˆÍPŸ¶¹äÑ)#à­öÇ몫´»¶##p:­7ä{±¸¶Xüí7^ôï+þ½Œ:òÊ诓梌)èvkª¤mKúCHiCoº#!Hš“ ©ŒyñEäÃÜ¥ÉfQ”ÖŒ|Q \£:Ô 0çvù^q",§î‡´4áØ\½›ÑE±%­#ºtÊ\éW—ï‹ýQ%S^»Ž©”íðã[w#yãíüL_ˆ¸›‘ÞƒãA…õXЦ”Ì|ÞTêv$0±Qµþ0| Wæ* ¬Œo ^N•²(uŸJý¼ yjÞï”`ZÔÛMò-Ý – p7ÁyQùàS¯¢ï˜' œc, ïá"–½D¨CîÐâ5Ë\¡C]çéÊkKi„Iç€lºµR©ÝäÙT¹¦ºFŸw$£ÉK.ä݆(4µ2¹˜ÀkÁÕâ€gõ_‘$cœÑ¦<–1]nºÌÝNL¡ÍBàÍr…â;Ù÷Aj¦q¡â5¶âÃV(2m&½cÓwÛOÄiÝÙ·j\•mžu·Sωƒñ„‚Ô¯iÃļ™}WÞªüUÅEeôR†Gÿ®ž”B ÷ ×{èMTßË4¡ª–³Kׇlóà‘¶Øú¥‘Dzõúy’Äþ!D8¢´‚4#@|>·ã`ÒŸq•—Ò«S¦µ”ÃJÀuëa‹cíÊW±›™üª¡×“¥!¸±KöF¦ X%x“D¤¡-¥|9¶–r¤aµ°·î¾{¨Mzvä Dl£Ú"Î4§Ï9ö›q€Ö[.¦E%²¤ÃÄÆ\RÆs¾‘+ýã̓…û±=õ¼3œÔÅ ~¯òdyWnVZ°øçÂï0x±³•PÅ«#n¯ÁÁSRjw\Ft€>¾‘ÿk°ã勜ÓÝ·MD%\läR 2›gsGß¼ˆH9Àda!”d9šÈóEŽ HLd"â;ÒÏ„’Ð`W[T±”J&zâŽBBEÞ¬š•âj<6¥Pçj^!óúVªÅ<ËäùÊŽÃY2…:>Ÿ©&†r{‰ò?V“ätº ’!i¸[Bölĸ¶b U È›Äß=“ (<]‚ïóQ7ù› ,Ðêd^ôz~H¼®Ä°öK³Ig˜¤?#ö0zT Ûø¨î‰£šæ©©Ó)è.M‡ž·ú¦‰–Ç‹Q½Ü×_‡"ÛŠ¹{¨rÒa['B3÷©Év»y§ºtfæc8HÄÈI/ÈLë‹€t„Â2“_3ëÁ_þ0BšÜK ì¤Ü1Y&±yitÞYŒ®³|BêïßtYàºdDøõå7«Gß-¾|‰yëo_ßàÇúñøÁ7òyÿ¨ÂùÇ üç‚_äxø€ßõ˜~´ÀðÜêÝã_ ?{ùáÁU”g׫¨²vØP‹ŸrÂ<…)í ì¤:­¤›„ž4j/÷ô¦Í¡›>hM«~*–¢ÿ€ÓŠð)e9/ueóžÇýy=lO¼ÜÓæöü’«•Ùõ’¯&•ÅË$l›Mh ¥/=á¡´ )¶Ä€¬k™ß™*ŠÈx§¨èÜìð x¬Þœ’{Â[’ äÚQAÍ g°¬˜´—üŠ–¼Ò(@Øêý½rzëÅŽN#GµØ¥1 hY—ˆ›¢®â&'G—5ÝN1–/-Þ’{„ÐI@s_\ #Ÿ€4Ï"“e¡Êr-Fóÿ—üª”¨@ÌЧ/­MOÊoûÒÖˆ »pDW‰;Ø UI™OìÏP@VµWΊzëôêÐOtú½ÆFb„Ñ£ \ÁÄîêV¨mÄtDsË5Ye2û†]ÐS 8܉[vëEÔÒ¾'®¬µj›˜|-µ/x/‹Á¤¨*Å÷“è€âˆøeÛQGñîIC ÷E‘'”ã¢Å§JËÃÏá‘ê¤)T»m¤“ä4µpIKß?òR5ý0+Ä#}à›0xó•_Ù«âÝÅTƒü6+Wià™‚I°%3“â¤àŸ<燹ï}þ&$ƒ„¾úý{.T¶1Œ+ÿ)ƒ••ç.*:DŠ7Dè¬Bš–|OÏíF 6²eåEóW[pG7ôš×¬4¾Þ|~Je¬fçÒûk@X —WÔÝ%ê(mõÜ…ô$NAI/‹GøirR œØùKý É8 Ž:‚MÎNœYˆcÙyá&z=Ê1÷˜³Ó¨Ìéü\>X{—8¡·®O‘p¦ui3zMËÔIÍƧ¹”½$&:š/5»Èöe þÝ6“Pò¦^o2Á&|`¥bCê 2P-S@Ý›ëCë–§©6ZËí~òum»1ôWœ¼@ú°ØÞt»E³Å¢Š¥ØêÊ–!ÙMݯï9‡¤¨‰Ó(êØ#Í ‡Ãë!‹€û€% kgkÛÓCIÙÆOªà˜i Á ÷¼UƒôØÃ»ÅûÝJy¯å#r1}<ÿól=D“Gm‘†Kã|2=åùûwû›Ä™‹¼sÕÇk -£ÉÎV»ª¤µœ¥w!6ê´ÿ÷uUfæð˜û«-£ã”NYízn9EcP†·@]Œ‚˜ÀhŽú5ÿ>æÕQa,c†‹Êoj¡ŠZs¼Ò,T,™p1ÂØÝðƒ†ŽZ8“ô0¾i¨ôñÐãO4—ôXÌÿ ww½[N²ëtd˜gª¾kÇ™µŒy‘Ĺ2’Ñ…oÏæyëרйܛ^³Ü3lcÝ%hé¿l-óŸÇöo*Ä£¸fš0(Ã8­: ¥äÌNéñ~5Užô Ì½)²e ö¹Øb§s‚[’G y@Çàä–£öþcÓá~Ü]ÖÂÅ׺njÛS%õ×jhýˆôñ!¿õûP¹üxwwè§«#±6€¯„©|àû/o25¼4ñMžR 3·Ø/vtš¥q0Ýír2NU Ak*[ð™WQTGÜ1¿Rº‚Ž÷ŒúŠUiÚ ÍÑC`JÞéÒ<xç}…ûênãÀÕë~‹$ùSî¹È¿ˆÁ¼Xúînò0–×{Ô¾6}GxûãЃv*ÙµR lç®…äÓ‘\¦Õ€YöËì5ÍÆ hã¦Â%+×k¡,h¸3,ˆ•'B ÄlT1TÞ*‰8*¯ènW½Œóᎈáœ#î ñ¯ùL3jë¥ûV2þO¼ãfYÈ6¡Ž½=¯&ÙÝÄÎø¯Î%?\¿¬ˆó0¹Œø¢RéåÌwº$S.KâÝÙýuYö7l–dÂÁn*òû.½¼^JÏêi#IûýÒUQs-Mã(uD rvÕP©J@š˜ÑÕZÀ—ýÅêt£˜ëkWˆÚ†Gxi]O÷‹ß>"ëtDÃѽ¯kLæé[ä]4±èèvÇíâ /ú(ÑÛ™)”—šWºÖ’Œ‹O†©ÕQ÷‰ŒÆÈCÂ#Çç¡wßÐÆqÍ’/ÞÜ&Ñ?]'ZD½ŸŠnVÁ#.„˸[Ø÷↉Š î£Ô½Ã0BÊ]Q d枣(^Ù/½I›"kÉÞoìL,”2NÚ[FÒÅÒ$î¨7–Ñã±ÁÎçUÛÿŠ<^ZâËÑ™õÚØ™±¹`)3€nÉ*®Ô!M”1’0…µí%¹é)Måd }¸©Fwp$:i\`¦ÝÛ|òw1=¯ÕY¾(ewåHŵdÑØõáîEštëv»åبæJ$`/ ìXѽ‘Ö³J ©‡®¯õ˜,—ò†‚À¼·Pæÿú«„ᇔr¡?—È©X‹âx¡ÔC¯CJ4²U¯EóðKµ7C%j=@ý”,–vƒ–:|=b7.Kì ¢Gh“›ÍÚ¶$õ˜&+:KÆæ~Ó\ÝJ‡„ùÊ+Û guNÚµ Ò±?Uø‘"=Ö\Bv‘žO"k]©õ’r52ö¢R?XW—HÜ·¬¸¸Ú?”jn¹ÖÒRÖ”×)­H`:¾}´¹Þ¡}HqBc_¤¥çf _û²0uVÉ´UR»Ÿ¿ð"ÿöØ‘3. 奣~óØÅÙü˜z‚îñÁÖaˆöÚki‚Úý>ùÆ–ïiÚ÷ ‘^Çžž‘ œKt³qœÑ}‰–ùV í\S‰WÈ %Ÿ$O>âƒ6ø•2æÚmv5Ò<†Ôbð)îjâ7Å¡™Læ@!p¾™R{¡0t店¯2@yí‰1\þjéSþÑaT˜l˜òç›Z¶Æµ…«#eÎ ÿ04H†Dy°4“†ZØgö!©¬ïÛ{j$ÉpÀ*Ñ]Ç¢g¶â¨xì¼è‚ .ÚÐEÄ_ÔºÒÊ%TÔpÑñ£¨'Ö—] ÄÆ(R^ŠŒÙUµ ãfmÈŸæ ÃÏYÁ7ì>ë‹6Ö³ãD®š¬L¬õèw"ïÏGí!Ö$[®8-A¸xËì«ì\zsiX«Â»èf¥œ%˜<¬9LÞB.'DôH=T@*8‘W5K°©DÚª$fßM«8,·!K!Upᜰk‹4e…F|ü=ß÷6KÛÙñdj5³ŠL?O¼È4úDýÔ¡ïÐàTë> ‡-%_„ Öž8½ªxRëÍ Úr-V ”@6AXb¹jãUFs«Ö‹LGŒ´€•z·ï•ÿM 7<écO;e`F‰Ã4K[¢«þÁ首•%Fñꆨòú™ÛˆD5,Œ¸/ãl0å—Àv„Š b)ñ@¨CRÅ;s 2䆱8Œ9–ÒÀÆfõ †R3&[¾¼ÿ~Xž;<|<»t$—Y³G¢3ÈÀqsâb‘8Hçe©FZ ^Æô}ȧ‚ž0Ä’ËZ`fÐý˜—µKgrâ:8Ô‹ÞºÄåH” Mö6šÌ|LÌÕvk¨rTt1²åôÇžØèôfññÌSÚ‹h „¥¿G£M²ä=<ã‘­ä”\üëÜÁ5uoÍ=t¡õ—-—]JDîI=bg¥…›9[A!täJëtb•ØYÙs’»7ÃØ!b©6ù/H³E†¤hÆ VçúÅ•!’ðLD,„­^3kúÑ[Ùš d æk¤´B oèÏyÝç]sá¡6Óáä/¡¨\y•3ÍDˆ/­se¢”…tôÎsƒ;à'áäÖh£åîˆw꣺öÞ»CÀØûáàLžI'ÚSþÞ»;OTeV‘R%’ivUÛ(µ¦+.‰6ìñR[·æn¦r £ù죰Ȉ—¯ô·uãTåÜ ÃÒøÀv]¸xþ”Z‘âÍ‹DV\˜R–3¨ƒiè-hgÏEƒãÚrÁ² ˜¥`˜¬Kð»t£3³Íç#³­ýzûQׂ•A3ɸòÀÓ©á“bÀDlRƒ1ö.éˆóωK-IÛåå“q¤èÀ<œóš]Éi—~Эp¤pÖ­õ¡–uª Àç]ã*†›¦ŽkÙùÁ,œR*ÅÛ Na)á4Z²5@"v!ž;€4Éí®Ì±îÂ<ÿÊöff@@µ{¼ Çóèé?ï ›1q o²gÁn/n j ¿ä€4Uq•.D’ƒŽõnœÓD»õ2+HåÒGÀ‘ç®{GË‚`&lÀŒõ”S‘Ôz—A·¡ÆÉ,‚D,Q9e›‰Šð”/öذ/ÚÍ?cSþªÅ÷“ÏÜb‰ÊßÈbH\5*\G'¨øŸ7¤Ê˜h8]tÊ™×owðB´f>£ðî7ž¼–¤®dȶZåOŽQ e¡…;ú.¤—&4Z€ExêÃÆÉïìΕä3rÖCµÝrÔs#jÑ¡ «ÆDõ)Í2r'RãJ±~JB@¡¬aç ɨŒ©~§àÒ±]j•åßfØ'dBuP&=”a0]`8èE']I³ç΂RYÜfSúý/KéÚÓ› r¹“¸Leà92¦éÛjˆÚWí;³¾\‘i¤ƒ~5"`ì—Îk#C¼¹Ë4lÏBT'ƒ“)Ípañ`5NÂÛý]3kW˜äà3­.¦Ø4TF—…žTн®B´òmgËŠ5BѬÀ¶­Nú.úb”#]¡cn2´ÞsäŸ%B$ÎØŽ¢è]"T¤…Ã$± ÙÛ-Í·Àí©9EsÔóí³‘Ý×Ú5ËÇÖ𕪾æC]²÷à¾!$;=}iÌÆ¨‡¨Ö|çýÓ;±IÈͱ€ÝÑî7³$w!Pöqá!\³@ä™EÂÑ,à9;Óâ<”£Nds¶ ÃgÏsÑ¢4.OÔ.8šô½–Èž‹ô8fÝyS'kX¶tCQ[ü UÙˆ×r ‡äÁA-mGo=Á›Tœ‰ª½o=*!½ža øHvijХ›:|(„›ÑŽhëîÕ3…×Xc2´Ö¿º`^ê+}†ûûg•ˆ+Œ 6ݰ»wn;3ÓIñâÕ€Zã'öûú¥Õw ݰ©%Ô ÁZ?4æ­4àiˆçmE<‰èÿû6"ÖÓÊDðŽQ;§+÷ìêÐÀ´sAùL¡@ä‹LòïÚZ‚À¢sï2vððz»ð™Ã¬ˆÄêõÌp%hèièÀÝ^Ñ„›DY¼M“U¤¥p'Öô~1µay<µ[üáoÂI -{ö@dnvO8^+…ú–Ç;D¤æ!]ä‚^»lY9^Qª'B£Ap)®“Êêϲ«Š‡¿õNçý°W¬„ jÖ:^“tÓTÆ¥¯—‘L?ÄÛÄ^»Ç܉d¾ÁSÔ3²»q'¦Æm¡ûûK*yÛœ"*Éã–G¡Ó·…nÈÔëM³ËÓysÊt„áêk!„ÌÔ¡¡&¡7¹>=Xmrƒ „$ Y&fP" <‚3ä’ðã™Û¡Ùi!¸ï¡æž.Ly–X™•«\ÇÆ°Ùá*˜„—ˆŽe¸7:²JÕ`¤ƒuf‰Í?Èz]p§{µýCÆèJU«¼™Œzw3 š+JSÿÙcWiw·{ˆ»V¥‹u SLꙬ¶ÅcdQŽj@I[Û#ŠËÙBšÉŠVRÍ}¤HLˆñ [/‡Ø8É "ÿ:ÄDL«Ž.òIâ-O`Ø}/÷Æ .‹żÍØEŒ(‘Ü©œ_ÌJôÙÖ |ÙÄDðx/ê\³ÚÝSH–Ð)[/T—«‘øKçØg<%tQu•Á»­\1êèìÿ\…#0À'äËßÞ•=4}&_]çI«ÒÔ¹\au¨eåŠ«Š¶lñøñþ¿4ßý‘•'Ò"`÷>¾‚8…Öè"hëEëï ãÉ@ž¢ Ï5«€{“Â02nÌl”lNœR¶LõÀ“d‰°ÍŠ;h¨%º5NLQgwmõ¶±šhÍÎùÔY:ðW”4«ãÅ‚¤¿“ð7Ë/»»lÚaÓ±Ÿ»ÂNP%œÚ1Á²Ö˜tµ GÝ;a|ÖËÐsO´ØÐ²Éfl1qÙR¿ì¸‹¬ X¨#rÞàíb,“-š.ž‰ÎV…j²î•§“¥óWKcÖ$n‘œ%ûÁ²»»MOˆ`ÙK4À`£œ1̱XŒoÓŽÆM`‰*òŸÒ+Ä›R*F³Åô˜ŒE´¶‡Ü „œ›̘NŸb­‰>³-€7„u:דÉLÙ;ÃB§áT%œ mÜ9¿Vâ‹…³;ˆŠ•d‚Ñ»1­>H=MìCZRÚ¡4KpŒ¿Eí>¹]•žyÜÁÍá…¦”uÿ$ÿ? ÒŸû¯VͳcÔ-U›xdáƒóÜiË‹¯Ù!€@D  WF ¢}P]¶RÐW7?ŠCcÌ»eÆw½ ÏÓyBMb†þU®ÅÎr\ÚÐõúÙPä:š_p̬.M ç•Í’ 4#ä{phù«vq³ðÿjÇow—7±7!¬X W,z$S¿)MmÿÏÇ€‰ï+õƒöÏআdÛ™°L­ÙN<ó€¢&j ±Ë1-…Ž »NeX ÜݱmfΟŸLÔæáWö¦Ì¶…1}ínè óD“ÝÜFwøà€÷Bq/¤2¥4ß컓àF40Uâ²ÝÞK¶ˆi)Pd—3¦VšxêkÃ~²û+Ã-]ñ]D¾)z®Iüe¶OÞ½Ò⛪Ží\¦u9fš¹´táÑ6GÖ.&^´éš­ª²ýA‹¹bÍ˲Çã‚¡á„ ÜìV¹xá÷ýÞ•X‘¼7RÉv6Õüio{ïàÅØëèKœî¡¤³é-µÀÝj²ÄðñN>n¥i úéAáõ/®ØT>WR/¨Õè”¶Y°$Mã‡çÐzÍAΗÍb&¶ß3ïi Ãia‰ÀK›6±ÐJŽÙþÀLP¾BöOñ?ÛÉmz:Åd³[·Á©x÷Øï†ôô«ë13ÕµÎcÁòyCóÈC;GÀqzõ%£]Òµ&Ú†P‚#mç ×ûIb$HMµÌG$ƒAÏÅ®‰8IIP–5{¶AÞf;jè7ÿ[-ù;‡yVu-5e¼XJj¹ý™Qôl »}^výYBK{a9Ê 7ocJñ‘åÂí6Íà7ä»> k²h¹ÙàH‘_ * $åLÄ´ËQ¯,‹´qD˜4’Ùu6ZSÔ‘œÕk ÎHy“Y¼ ÖöžÔTUvæê¼Ì?OÛóbíõGñÏòÄi&n•¿té“M‘?ì£ ;¿Ûi¨ÿyk鬸ñ ÚõöÓpÔ Ý»åŸ¯ŽÍiÙ†Yƒ‘°‹²‚Òa‚žú☚mÊž¿e®ßòëÓ€‘÷L“úwŽ•쫨QU’U_ZL˜U°¾õÁSthF?Œ)(µ9ƒïoáæJ*[Ë|‹Æ<¶C~ESùœV³øÇ—_õ¾¨·I®ðbcä ÚYñ'®øtPsN¾¤Èœ.üÝ"~‰ZÅ*Ÿœè1ûfcŽÄ| äÿRveËAðWd^ „äð> ,±æ†åg-Ù «Ã¡‘0âë©ÌªTNKâ"€][£™žîêê:²² ~·þ%žÚÞ]¾d{sžeí¯î; ¡ÿá~_… »Z‡½Ù<ðT¦8-0¥-UŽÖ튳ìLõØŽeâÒ"^Õë+Ã/ð[.…—õ“g6´ x¦¢( Á„ùx öHµtk\ bÕ ˆà™U ±cèù%Ôþ®Å»ÄÆÕÿ«Î8ßE¸;e*5¥SŒ%*ŸùÆà›åãükÒ$<®Óö“¦ƒŸ?=×úmTàö€eÀ öÈTâ™LPä YûŠ.À‚e&8—\uxCOTm„ C?‚S`$WmŒ— CVH%i<ŒgM|6AÃ#Ö²SHx2‹©A¶p0ÌJ1¤– “ß6}BäËe›Æt$¶’@1ÅWfŒöaon¢ ä1‚ׯ†)¢ÍñÂÄp ïß1mbÆÙ@¿”ànt»ÃÇi¼g^”IÊЮ0x"Fù…z¿nM-òIôÊ´é6hX‚¶ÙKva¶bŽ"ív 4“ IÅ:“®`¬øŽhiü€<;1šß‡•´zBä5qKB0pE4œfÛ*+îöõ#‡8Õ´ n,å’"IÉ©ôÃ8€ÆiN_ïzvxèYyB4ÓÑ©¼£µÍÖÍVy19M¸/ÖGök:Õýý~ÙaoVózΕ(˜VŸD¥õø°_àƒ‹ùž@‘4»˜ó %ufÝîŠ\GaØœm…œèœS`âÓÍlªMÕ Üêì¬øÕÀa–n&\¬zXñgr¾ mªÕ¼·é3dÛEÂçE¯¨eç#ù®î¤;*Ůʯ“¿Òû)C51ª»Â“‘áUݘ’+“"%·|mHìWÝãS/ô÷óÒHêìTáOU"vEV„Osx»“µtÒoq‚¾“/L 2ëzl9%WöˆÝ&-™Y¹…‘0ì¶Š:^Ó¥ K©T`Ã`12£¨ÚÈ«›G¨&¨i}¯ÑÁsõð¿wD‘.4|ÈÁN0R5œ¶Q·g¸ƒeíîåR( Gúøtžãêb¡Ûi.*Á}àÛçntõ½Jh¦£š–†‡£¤[{"^'cš“ÇZÓÕ˜&ºVõù‚sƲz‰WîY6:ó.Pcµàˆ LSÚ€¿Í†§~‹ˆ%@8ÞttZî¼e‚öb,—¡ÀJ·[< i¡&Mc+ô`¤RqG Õ–û´£*?EQþV|ÂÜQîr‘‡1/äY| Jö£râ)zÖ\ú/ó…>¦$4ÞY׬©Ù¡M4RòŸ-,£2…ŒÊX›h9àæ5×ý®á›’J`Ô^ŒR“æ‡oÔ‘ðyþ¤‡©[heu"Ól+ddèñ1~”¢ xI ŠI9ƒÂïü*â×”ž›ÞiÕö×£t²ö»"¢G§G4mYS6ñ~KîJ½æÊ4Ï3‚Ö˜ƒçŽúw箺ýê¤y ¥ùJnõ5›¹ìЫ|[Éä)=j¢ynJƒ Œ°£ |jëžyaÛDžŽ¡+ÌȸW¨Å2F`ÉüÂ%Ø'0§F; ™Q`îðÌm–¤(;ËlÁW8^®×…—xùý«J3SÎU÷ÑäûýšNn\ä|X,ñ‰™ÚÌü•+Qïù¬T–RÇYÑ›vÕ•ò)=*c÷‘*4™ù$á'dFƒ+8U¦©jUm`,£ßq XjYl²Š©W(õ C-+$t.Ÿ*Ý !ÆQB:0³CäÞ{[t\tI›SI¯ݸ»ž4‘Qdžˆ8>á5›ÍrC 0ýªÐûAuÊ‘!>¥³(¢³Q/02ÉœP¦PÆ(”\6·µëªø þQFó÷nWy"l„üÜL¾ùñÇw?\‡5¶sra"I<ûu©¼§Uc"K~'›m3Ç Å6âp (*• r†& "KnU)(êZ·|MõQtÓ€½¼µvŽ)XÈÄ 2;T¥”ÀÖÔ(…ŒX-Ø*ŒéHœ¢[îÜ+P§9PrAU~ « ï2À^įYó…«å‰y¾Ü?[冷KúªñÂncy£6üH 4ðÅÊ»¥kyõ‰gz±#@Œ`‹­JhÝ'Sä“8%üÕùìæ½ ²H‚³T8æ¼þìZ·9\#ù^up8îŸ0/(WÍÍfg‘;T¥tO‚´õ?v3óF\ ¯.r»~~$B;pa©Í©~ÔüRu*©ƒÞïݘývçnðA÷¼‰q&J’Yf¶+<;øP8|‘áÐú¿mIuà,QT?qÔ®˜àj)ô¥ä$gS Ò¼ñø¹›¸×ù=ã¹v·`³-M.$íïÚnø‚þ}|››F||’æï*‰›É‰:·¢¾=)$!`9?¼F+ih|då)§eјÝ ›7ò†Å4Ðô¾ƒ³Èõ‘) mØ0èÊé)†ãÆI”ïŠ3—:K¦—k™ûǦºZÁÁ“`ˆ¥ztˆ¤H3eÂm9DG|—PÆaS=›4 ø. ,:ytj•°vû5»Žû§zŸ˜î˜0‹Q§‰êçOþ0вl—'þ¥ºs°E«žêåÞnÃ5XãT0yìNñÔj mIÆ00qœ5Åm¿åÔÓ¢Rá´ËšÍPÉÈ󒇥µq”:ôp›ù¦u)5E|œl!Àã,‰žÞÕ ¨mø†*lºðÍœQé5‡ ²\,EÃ\Ý@ÎÓÐÌPõ$¶µxÄ&W©êY³"²²*rDø\ÍšÊùáZiÏíÜxPb¨¤½„ â7ä)Î_ß]‚eB®â°+TGŒ÷ÆòP1ž«XH'¬DºZ«¿Ïɩ̖ғ;–2ŽM‰Hû[_-c;"+¹¯ŽJ¿¿,´ÈƒéîlÆÇ^ìïSfayÆŸ„#!†Sóm/†Ís\V==nÅë´3?mS)äY«å>i)3îŠ@¿EEk„¯-!ÈGŒ…úߦ çB<³"<•ÚE}ü˜y¥ðwÍ`Ôßå>Ê ¢Ä ^œK¦%ü"ñ…ÈõÞ!Lg& ©áÆôµm è¥<óðâåìñÊÒp–ÜÏf¼ÿF$ù¶·RîÜ@š\Ö0]AÓkÆê4€sLgQª¤a!¤YN· æõìr·\@јëËÇÉ>¶æÖ#•’‡hA’Ƥ©¦‰/bƯÔRÔ4éêæÑFàzU#¶GËÎ×3êZ0üR£û§Ì<òÛ <‚D~ ƒátĸZÀÁy-©ÊPïmo‚J#†Y;ªíàùÿþ•ñp„†^`ªa-¦ãk ì í¼ë¿8Stû?šððPÿˆÚ_ÓÓÃlÆ ×Œ1Á XX¾/þøk ²ç\‘Œù©¢[¥±äj­ø=>Ïzó ãešsò !éîOéKâÌÂWø„ˆ<è™ýºüñKÍ•t ™/ÙÉ_v»d9¹ñˆ:d¸÷DÑYuàfâÂìú\pý2b.`vñ=¹aÆ-óØ0àR¡â8:Öe4z O.u¿«`Ô¦ŠØRgɼ:ú…Î ™ùã3Å*V)Ï ¯@ˆÜæ`Æ× M·Uõ°IE­DÝ@èd“cCQôøŒ®rU ™L5uS£HµŸöhT’{ôgE„Ü jhó \¶¦’ aÐ& F(×+{I$Þ.ÄË_Wq|§±ÜÊpóD@×9¡ëºQ_<Ç*M“ö´CðRBùR¶e˜Ä Õ”k² 4 {3ÌÔ/÷[R„ Q|h"«ªñfW¤gï+l¥‰°‘¹Y$ߪ#Ó”d1ú{%K×#A¥!yéb¨1—-|QXnÿb ˜~Yû-ë:ÖÈ©é\èz0¿´e$Äã«â¼ Isï¸%/6sÄж­,ù qÆ}`ã›Ô®( šuü7ßS°'ýY¹: š÷÷dþ›…ü­²o¾!UÆ ¤L~½ø#eÐ1MîðÙlD¹Šroa’ó j ’°Tïi™–¯‹cÊÊ7ô/SvVEíûi’¦VeÃZøî&œ¥ŽëªÉø¢–×Äcg‘I7 •xÕB6]8Áo@Sö†¼*ZjV'ë8` f8ÅT¨Æ(W¨‰g¤åËwMš Nïv5~+<ô5`éøi}O¿u²Û}„!ó‰/õ%ÂŽ7͢ͱGþDD¶è&['nþhê(!ŽrPWí B…´Ó|˓ģŠ¨fYr–byd•_|i©d™×Ó1Ý­§+T̬gC¡Á"«ÀýXû±>Ñ2QžǬ®@:H_¸u½æ)60€ñër¤¹1ib/‹ø©ÿQCšieΡ`•ú%¸•›þ’N)kÍ"§ó’~ùAÑžÇDÌÍÏß´j¤¦‚‚9HN[PË´Ÿ§š HÉï«õ‹ÐMs…‰`ŠI^üë¶šÌîY£à=ÔèÜ»\%“M²é+ìö¬<ך°º,ƒ ªé@yXlÝÑ]©É\Ë’óÞÑ¢Jæ,ÝÖ*I·…ëZi@‘Ö!ŒõÅÒèœ]\“Ää4¯òÊÈv%ó+ƒŒ"št¸î B›ÒÖêÏ‘ï›Í°Uíw©)*7 Œ}&Xs‹ùë³~.Š€-•V›^Íf¯ñwÆ®+õ¢&‚<ð ø2hØ—ÇBhøi’RÛjYØþˆq-â„ï¡Çy$ˆ8ë_K,G€ ‹OÌŠÎЂ¬nƉéˆÏÝ,ú€b޾RKI˜{¨Dc þqb;yË_ƒÃœtHÏЬÐ*ûyž¶ÌfPÞYÇ?/Üv< ½sn É¬Íæz¨ÒcÜ{(‰o} à†¢eWð²º0Ö€°ã»âJŽÜ«\µæ†í¬ì(?€H‘¥I€<G. àèE€lÓˆIr~ÈðkÚ:OÞÝnÒ›rª$¦9±ËwW”f¹ÔÌåÖ Xe˜G†I»³ŠŒuùy?ß÷˜Å–¶-Ú "ýE%åÞEšóhOYÌ#úci2;lO_º¥Tçî.%’üúç›èj²¯Ù½Þ y+4"¬&Ybq>÷[a°püX*ÆÔ~QÒÝ࣠ìÞªoÀÅ0-ú˜”`Ò·%%4CÂ^97ã×âz*ÄÞ$±˜;!Ëášzò%€ËâÄÅÆ<\üë2ôƶaÙ|¬-}öIîñ>­1iDrþ‘'ÉÒò¨7U”ã²Ç²WåÍfÄ*B£aZ#-µ¯xøÏf¤Âð3é»+ž÷ø*’4ˆ`ð„tœ‰0Éì뀆°¸t¶}édÔ°3«¤"³I7]„Xï+ºŽ™r¨E­‰?8v/VŒáÿuU¡“ÀP¹x“R‰l\¢x¼x7ì\î¡1\ûRt|Ë¥@”°Ÿl÷ßoèÃf ê±ÿy`rfýšìÃQã‡åà4ßåŠ×UûÃlË*7ÙÑyŒgwl‚'t·`´28ö['SKé+çÒDtOƒNE7P«·»[ÖPÖP-5ŽÏŽøæs‹s«M'üáΨভMRSµ·£Þ8ÒDŠ¡¨™¦ QÙS¥bi3Qí.8ÈÐr›­J®U[ `ŸÕŠ­Ë­so¸¨œ^èªBˆ¬Z‰A–Î},$AšôÍ…1*’›œ±§›«ýŽ6ZÖ’ìßîÔ2]um…\êÈŘôŠ-ÞÆö¸©à"óÓá¹*Ó184<Å´w¤Vº4Y¯«WnÇp–·ÑùàФn »QÌN@18ÂJÁ¬P2&røió}xiÒU  ,|'8ß@XÅ‹= Bxªä«ÚáË>e/rucZvç³%2¼„µTœÕ~-Ð8óIØ•£u3•¡îÞ6«’Æ)àU‡~ŸÈ‹”,õð6k‹RÉfVPa‰ƒÒ·ê¹Ž+²­Š>PÒ€H¢›f?ÇǨ±ô€k¸5ÎK¼öYïq#=ÜD`hìˆÇ"·m»ê@ÛTð¤3HŒÛº˜Õ¸¸J€1ááRbœÞR<–H€¦/ô—ýŸ±©ÓD$MÕ û×4“=±Î<yÎ>Çè÷ኣpúq µK C |¢¤GÅuz“®ro7f&«pXT k7í®IÆ) Â%iÌÄÌyn‚¹6$ÌŸˆã­0øÐf"OŽ{EPÕµ·æ¬·¸Š'GcŽáþóAY ú!iè)Ù¹ñ_Ø%ŒŒ&”­xû%qø–à.rSP5^dnTÌTÀ¢ž©Ó·°å´8-MÛ:t§¹_  Ç+c”’u©S¶2ÃÏU“ V¦-Á–„±ÓgÏI¸aQP"ú˜F‡»èwQSBepBx‚ÊâLTU§vM`ý·>’Œá%¾ïÈ`<7¬$š†+Nšwˆbª¾ÿ|?© WA`MN\klÕíŠøP‡ënSÉ“!2=Ì-éñêØ¤ j,†Xü&ýn§<Æ¿'eÑ:AŸÁINٔхå1˜Ÿ€¸+âÆ®x—ÂúîaAvb ÎΦj“ Ålb ‘çØ#Š;5exŸp‰Š*S„²‹Q9c5éÔ¢GÅ>©Wâü¿¶éšd/®²/ BŸÈõqõHÖ?íè³(›{•æj¾ÈF7ð6ýË—U6æ¨KºU#Y•i fvȶ¾:)ødê´cû¨ôs  è¡¸H›9®®qX¸1jls¤`ÙåzÆ€>AiyrÆí€’†"o½bm:‘2Åïâ7µ×D´ÑD©g¿HÞyŸ)°dq©M¶X"ßɹÑw“Îh^“¬£SI¸£ÈT? ¼{GãÌýY¤´\`›ç¢ÀS¸ž±CÚéùï¶àÚóLܘvŒ'ÉVØRM¯Ôrºž”j* ’”´üAlþádŠÄ Ô©ÐC}mˆOam‚øWm6¬rQ‹uYÄPH‹¬)ª›Ô¿Ü¤* ɼ•býÎüÁí æâ²T¨ ò&°¸|PszBï©ï"~¤øQ±1\Çîb7*È2?WUOv!_¹’ÍÙêÀFàÏxœ†Œçšrq Pꆃ‡ r¼U™]_° _œ@zÊÉÆòžµJOS@í¨oè½Iç KN/7pZ.HSR‰O<¬¹gÒ•|D{ÀnÖˆÒÇUjă"¾+œBaz(¾zÇ÷·È%"‰ Îg3vºÄÖ\³øBìV6?ÄV0D–yâ êȦÀ¡êÅV‘A é¹TK`ÈÁlç:#úÚ{")ûÑJªïŒzi¶‘r6æ ª60Š3ºH³B¦ê³TÄ‚éZ.˜»ç¸S¿H*èóÀ“tô·õævãËÀFQ á ^Û9œ«zõ˜'½®h_ÚÉdò±íW)ŽÃMG(cªà´Ü'ž’{|ÇïøH¨Ô’/YW CN9LkE÷’”bèëµ{U¨äi„Ÿy¤Þ߇†wo!}e!tÉÊgð¾ÆåÆÉÁ„'-Ü£®²•¼/ g¸ºÚmR3À(Ä93‡w‰˜Ÿwæ¨íukÚHøt½6¿¤¹½µÔS¿ŒŸX ÛÖ0ÏæFíYƒµB¢j‰v6jÉv½ÅËS]Ä éR= sfÓ• ÂïÌ?H¡.±°ÿ^æüw| ‡?êDýÚ£_9âäæQ<€r£ä¼L˜EÃGVïöŽ®(ÏW¼|~¨”z1§$Š{:ZrËXÊÊ{©~ƒ”?².…nžëƒGpÚÆ:˜¶:œc«Ó÷“¶¡HSH뜠ÂÛÃ’¬ä³Y6öÛl<Šù*"«ÙL¹Š†L£BˆmF/7ó7籜µ¶ñ7L }˜Ì_Ó×é¯GäšâÌ!íÂÛdÑÿ›éš |»&@á˪âI¢œ¬@«àÖ*»úê—7¯¾ül:q# Îà µÑÛK°Õ_%fEj¯*~3ý‡mºväxª®€aëlàÊ¡v [ iÐøaÕu·|à}8¾62ùÂxV5–Ž<_~ßÅ:ÏÉ*HÃÏCVÙ4ºú¶€öŸ^°mL…IâÚc˜¢Æ‹¶a•x’ U¬;V‹mDòèª`p{º%šçÇ5 p3cÐÊQ[f­À]èÉXyÀ6h»ªa®?aÍÕ41V¸Ê„ݸ_ÿ?§c{l-Ëp€í u8›g@ë>µÓÉ1r‰Ô•2ÊX¸H¤¦¹º< ¬'¡i{n&â·†‡ „øâV®4Vw¦«µÑO­îQÜàm;­}¸á»n'_±×5fÂÜ+à§„§#Ù\6PÙJ™‚ÙO¢A¨¸\Õ"]©÷1­2ÍäÙdTì¨vÿ¤_ËV‘Û(®£E¦@.Wñ"7|‡Â7ŠÎôl½`=v'ç&¶» ï1M˜TÜ”5ê˜"Ä!“¡8;NÑðÅT`°mT7wŸ9*äpqt¿íÑ[N€‡ªþ®2fsû>öE‘©ü³öÄ$%̨ɿOäVØ*m]’²ÊDkyk ÿÓÓšÄÕ¹ÍÇ ³¼"ÅeÊ”ÛQ&¥¶?§ƒwˆ ÉÁzÆÙAJ8œöpX*éŽ)!vµÈBî˜JÎÎ2·´Þ²é̹é¿Z5Ê‹yƒ)QI9ƶ"¬¢NsðRX7¶§\·â¹zªÝÎúq©ñö' u ©Éu²2$ËŸGM¸nš¡pêÀÿ¤¹?qõþ]ecÀå4qö{¯¿ùyòé7?¼Xūɗw?üðå«ÉÇ?¾ùeòñ«WÓ÷íGÙ¤¤Ø«Óã¼_WÄÄÜyø}XþD¾…Ê÷o¾üt:ùìe<ø{fHï^~5}ï_e·–]Ô´¥Gá1 ÈÌ|ŽgÐRÉÿôÚäE°øÀ¢WC¡è!»ÿT¬Äcõè ^ªp¤§‡×ÝÅ€qzb¼K!òÞŸŽD¸B^Áv5¬KW“ÂÆW¸cí@süúä4éXÆTZk½A”¼0NèíØ¯+<- Ìû5_•Å$HåáŽýÀ$“{EGÚ0‘Ï ‘‡:>…e¤…{>õ^Íz‘É5¶‚‰d„ùÅW9©|h•½°úˆuZ+FBº]Žý—šxéa*àY(œØû¥°åJÍi¾Ýv‡b^ÈŽ2ãóÌïþnîý¨§vTÙ&Ñ à#Žã5 ÷%eñQо–õ9ìIÎùOÔË~0].ˆVtRѵÎ2tð+ë-^²qRv5Ô9­Ã¡·þè?/ƒý7¶e2©Àî¿N íÐŽ‹cÕë~Ñ.ØÂwA ²"ì9òõb’‰Œ—ƒ´øóžc©€“‚[ÿ¼DŒqOÀ0*Õ¿)–ÛgË»^‘xKiŽl…#æa´—wIMD"#ëÇxX¥GúÁU•üíxÿ×,ø`Š·œvË¥lÿyK`µi—ÿªŒw>„€A.ÕâPvXyÀ% ^ ír¼í ~+Q¸ظ)ï´Ž»kè”2m@ï/¬g <–@£áÞ^*—PU½lDdsw©áÌu>ãM»d/Ô·—Äëri‘´ºz¢+Ó%T-ò%²È$-ëaVÃGù”É­‹jb|ˆ€ã}h= ;Þ_t—œÖžQ®Õïßû=ò޾ûñî‡Wymãe7–üÿ>2Þ³ð)ý–øÈh„g=ѨD&q!û$» VïoƆ¾Fý<= ¤+ø¨@#DeTÜ\Š2µ]°ÕƒÃD‡4OÊ$Š‹•¢a'Óâ³üN…Ê—¾À†÷P @µé:Ì^`'ˆÙ…nG­<Úá?„Ø3Öä…óõõüÕ¿Ú¿Mô“Öñ è¨Þå¶›÷÷ÙŠ@ö yP'K×õ6ubåTÒ„^@@mqykÄ)bÀÿn¹µlU<&{M š¡w×PpÓ‰jÒБ–È­Û¯As5ê£ûìn¬mmij3?”.RyÍÅwPÍGí–Pì#7ѸÍÑäý~×pü7ÃÐüóûKÄTü{™Á´|L$¬ ]V#Ó=ðëÜ…³Á3‹5<^ñ ^ã"ó°0Ä/‹e%{éþÐbQØ™Áû.¶r¦9œ.á; ð+*S»ÇF# ‹±smîºÅäa¥\ø¿CÆ™"?tÏkæ¨ïëF*Ú+{)Ÿ@"äš¡‹†ááB€Øü祜ÂÌû)™³"û°ÉÇ´pPCª eHM Îã‚xƒQ/\]ÚŒÛ,¶¹|cá Ìò-.â#[’zLðázáËò¡vÓ4Õ-ÁZd=M2w…pµÆì² 'éŠüÿÂÆh¸yúw;N•Î’á_>ádá"´Z¤¯!g¢ ä ìmT3³¢` ¤jŒPr~Ð}\gÉiòO™áQ›Õ(ùÜô‚f‘%±Olk'§h.V¹‹ú‹¼L¢Y‡Cþ%üoñ¤ëhN—=sÌ/Îpk.•e¼,íéá]Lúhu-•c‚«3 3±8\µw¦ûf³úÃÄWtîÌmr*.7•„¨íjTè ƒ„3£Zj>ɇðc¿]¶‹+5ÖyÐE“U}Ì.2øgz‡²×Íy›J—xjBHK ÷'Ợ:g¯×Z‰´B„ÉÎ/(Æ1ßNA5GÂøÝ†*c`ÄT¥“o¾~•¤¦ Iñ@ï†â{?ÁzõœW/d!æ­†öPPá±R.o\F&L Àå ‘TÐÁâ3ÆmQ2²ß…ò Í …¢§Ï¨1Œ¸¼åÉî¶áP¬R™6“O÷`Z´€Ý`ý‚ »¼š„•×PòdóÁ$ø»­n»"­Wl6Lx–W»9£ƒ“ëØú7ÂmQ •nšÌ]۬Ƭ@ç«è f[¦Må2ćöŠ'ûVH`3\Fh`¦÷)oÑL¹DS!üùÜ7r£Rj«á³þ´À’0”¥“|.ÛÌÂÊr@¬_7 Å”Ç1 ŽÎf2 d’¿*·ÕÐñ³+{[ž•Vìú—ŠPN3ÐkþáÆƒ!}Lü«È”Ü+üœ ›+¤ã¶ñ Q/Á¡k| ™b¦®v…ÂÔ}/#ÒÞß±/Êptåqs2oy i•+þ-û¸-èÌ7¸ÉÖ9c²Æ%²¬“ÄÉW!çV…9ƒ ?Lˆ“ލŽ"ž (a×.4¥íÁuaðFa‘+*U‚‘N—ËIc~0]£(ŸJH§å »vîÖ~ׇ,Uù¨J6øi‡ïØ´ ¹Ø¬hóÞ;ùšÑ/öp‘\ÓÄ +ÄÔ¤€Ÿ;šŒ ½äl-ûðn+yP”Þ5±óHi8ª-Ò­MrZžÙpJ*Oáɺ΅Ö=‹9QÜSõá”ï7í,~]Ѹ1L!nï«%ÁÖË«¤‰Ý,„(=ÛMac79«Kw7й|ò·‹Ðò„ýrÑfað´ä‡wÓàÝIey­4Qv%âšPMËÜ1 ÌS„:ÔA%ožQœƒè3+®›õгRä)¦ðÍ'ÏsÞ÷¾-UöÜc¨è±É°Æ¡÷Yƒœòøì*)à7ëI]¨žäLYVF.Rd<ð2øPTy×wës/IOý•ô—¨€9hWžÏD=ÓrT8dq82‚± ±Ô†÷B>º!vŸ’Ï5g]Ÿ€mÑy“V50}»FŸÜ`Jlɤ+g²Á¦¡)Åx¿UO¥ŠZJyo-ƒ+“ÿŸÅÌÎ>;®á#ëŠÃ¢‘&N-B¾*g]’ #^g">¿`Ýœ÷Ø%š»ç;oFd¶rì³6ºúa«gvÄÎý˜¹Ë~ïw„es€üü»}œG|VšÂ‰\Q Ÿ'|ÍX1½æ1ÄÇ xÙ<µªî9àE¤ËnPšk޲ 7Ï£l_br6ʶW;ÅŠíÃ'K‘œPJ(<3¶e$t¨ÓzÙ½|1†Aou"Û<¦W p·ñžÇ€‰!´nC@®EÓ‘¥¢.%°Áb,T•î^øKZÜÔºy£âÕØñé±ÿÅܵl·Á_¡´áäR /|œX‰•ø‘Œ”L²„$HBL<9 óõé®îBás²Ë,ƶD÷Ù﮲|´‡ _sG‹Å7³ˆ¹àÄalh[ ß=u©sÆnTN(ãXLpÏì_ êÛ_Øê{{Yìô©.ð,NîB ñ\2«K$Ÿ6°„ ´†³Iâ_ù?×™€0. °ƒNrÝæÙÚÃCaVÀ!îj‰ý¸sÔ>I6 • a1n,LÖªÆðÀm“ÝŽÁH¶£(â¢àXî …YìtVøw–Kn_€©Ÿ¿û1b¹vº`¼jE  ÈJ¢d¾ÿn‰Ê´®VðbnûñH–±HØ·PWý;¸Q­™Š·C—ƒém6ŸËÛFÇ/Œ¨X«)î ­ÑXæmZw'k¡èn¡¡Y‘eƒÀõe¦ã¹"† eêÜö \%ïkŒ8µ½o Ž&!ž¾¼`cÌD_ªFE‹ê1/¯ Yáä\ÂòŒÑöWiÿã ÔÞÎUÒ =8’ï》A&YŸ²”áÖé&ªˆ ¥È6Q`–¡„±²Ðˆ'4êךú’Á„eØÿþùêw×ßþzG`i³ùÆaëØ¥¡)¸ÎV´U¿&–¸-Ks‚ Ùm“€w¥§ô'ÃIú1±š'/<ÜgІáÊA(&ÌYTÌoÜ &Iêñÿd¥±Éäl)$Ñs•ÐvøÊ5‚,¬z0 ï½IÊi’]AS'º`!Ö÷$mÜ?ã ø¤©êˆÝI²ñª(Û„?zxJ¸ÚãñæƒKÍng™7(Þþ0*ø´—× %âþ¸FæäÙÛÞ’kÓŸ)ö¬9¹ S’õ_u?¯­­#äK'Ç•WOÑÈ’ç^6G’ ³Éॎ/ ò)-UÖA s¹ð[¥uünÄGLIó¢.øßï>}ûù¯7øÙO×ô"xJJ!ýOF׬V}4Lÿâ)ÙWþOíXÊší“Ðv!ÐúZlU8Q>ÖzçSé{í|±êSÇbXAéÝ)t+T ÂèT?a£.ƒ¬âsþÀu½„2s'S¾Úë7÷ͼˆFÔÍ­nv½ýg{d[pMw‹ª?›ß/wP`eö‡Ý„Ñ€\,Ð?›jUü˜²ÉëÀ•O´Ý¢,ñ‡— Ð.éÑ3=b“Ìüî¢Ò+«%[.ÉhJ¼ ‹'ô«»¹°­Ö®WÂIüöQ†ÆðÛK;¾L­ÜWnþifÂoŽ2"ÜôËhm7Û<‰DUDˆ8wÌA¸tH=ÖðÝ0UÁº‹zéY{†ô4ÍŽE˜êônÛÎÈÑS¬"{]û¶ÕLíp²4p­<Û„àZ‘MŒAÍz«_bN‘YÁ JJ8– Ý!íín+Á¸‚ª€SˆE¦%ü @Û™à*é–vN!Ô%¢*%9¦ö“|kWP¬b\]BÝeà^t$ Ý‚p r{uÔ ÛçÝ?Hc ðŒ›³±ß¦ô[-Ùê7¹ò]iü±ühNª ýž_dŒhzª%èÀ½·9Ô_0©DÿïA,¬´ØƒK5½6ýr@”|÷ø1ÂÌö›÷·Ñ{ˆ0/WÎSÈ'ËBFs5ÚÀè~®°r0NŒ¹;ŠùÞ— uÁ&0cá󽻪<Ñ&åº@õ˜‹ëͱñO`¡šeN±&Û{ë³P$÷¿g4ÃÓ *DW@dÁL\v‘ À­ˆx^âmvqXãƒïlî=Fegò˜tª°û—Ó¡ºzžeebî—£«Z†ÉU_ˆe äÅnÆZ—É# C˜ÞBq‡|[Ñ€µÍUÕ2úx¹ö²NE‚ c&»’Gäeü-¿»ý³Hk–|Þïâ½WÒ]•ˆí€ŒÆö®‚H„ 8jBˆÈ7öùÖ…s’E•Ùr^ cx®?µOÚ öäš¡,Œªß ×èH³YàQÑ(üO).ñ‚ª3€Ù„"yA ´"¨Ÿ°Ó¸{T]Vfç´u&÷øa³çt$ýe}ј:iê+.&)¡ÑpJxÏà‹ù‡®©!ÉEpÉEÜÖMêp:ð0ã†äžÓ’@"/@ðY„}×›ÜÅ3!Ÿ ÑC[Ãäÿ|ðPö—í'Ê È©kYBê*/ Ô”¸V/VÕùÌ²Øæd¡TÁæå†ѾLz\FˆûÖsWsÔÐãx‚¤âå(°aº#Ÿ¢lÝqŒiþÔ"{¬ïKá=Ô;sÈËÿv½ÂÃ!5Ð)ãÅ8NÍ¢ðøúz=Í™ƒ 848ä@²˜ÍzÍ’íªOíìL¿8Ù?ãñÙÞˆ8b@o\þÈD$ßÕÉ q1– sµHU"gGr{k×at="Q”,›Û€³ßŠ …i¡>°ùšöø®nm/Q²¼k×ÛhüP¯ï’£Î÷+H*d³;¯KDálZYÁ\->aå/‰¯1yf[·K! ð2höMOűì£ÄTâkõÔãàZ6|ÃÏH@^,¾úÎàñkß&þvhß¶StÄô¼$¼À[ 'A`㡎v‡MæX¼Çÿ5ôïòó°(yäOÅÖb™po,‹UØþ"â>vü׿›j­‡å/F™@§•º®QaˆÚ\w¯¨¾p\ ™å(™C©Ô[,Ó«±N¿Uݲû>Wß²Ñ%‚CIÖg•ý•™A~³¬ü‡nꎴ 0­¬G–ú‹.hKüÛcðxë°ñQfJX•ðyßOöF¼"fQˆX¤õ¢.Aƒ%§*Ê%Z±)u…eıßÉìuÕ–Pµq5—zs’výÁÖ.À°T°– Ñѵd°*í`ù»±”x ¢‡³]Ð~@¾bÅÕDäú‡N0ÅE-ŒŸB³ÏÑTH„Ô7+.Ëg’Dç19¤z„ì¼ô>áSüæ>4rÓÛS‚%‡Ý+7(Ñ&DÙJë?D'ÖmŒáb¶ßH¾óB*BŽM$ùΪ4¹Ñî6·[­)U>^(Üb8%mç œ÷óV~†™@ƒŠùHîMꈰ̱í>¬Wœ¯¼Á¼f—ø^ `mé0>YmÖ‘¼©›Í,°$S×ôk›½pØXþî²\vˆD%‡ü¯àXRÿáøC°©øb}‰($²pKåU¢Sýî8’€]dK7j’X…É÷©:êøª=ѵÏ&]Háˆ,4‹mJ•ÔÞÂ$Ç ãŒ?VpÂ󞨹­Ùñ¬äâŽJycR2†V‹üÊøªªò¡•’³;#jˆð§:ª+r‘¬çPôº,rà>`¸¤}Ë!’²6UMipÄ6£FOtºåö"àÁYá§Æí«$êj2Ë‘üdÂ4©¦Ö¾KÔ_DØ[¢.ú+‚­ÙŸø‡ó¤å7i 5{êïBÊPºâÐÙ/­§:p=¯—/h™YOÛUöÌxOúì®­ÇöúÁ¼«õŸí¤Oiž÷•-œ=¡ölwª~áÞàvŽÍG@g?Ó˜p&Hƒgƒ[Àw€ALY«˜ÏÝ%6ÎÑcå:Ð eÓ ”M<û øpvmWQöœ2> 7ê5õQU@£¢4nB&ÿïßÀt¬m»E±&LÐpÌ3w¼Ýf(ö'Ç,¾0:±"U€¯(²PîÄò‚¡¤oº\¶;‘ò+º^º-ªŒ°¾¾D ðlÁ¿ÀÔv£’‰B1ú„Ø],mõòž¯·ÿ°Çò)ó@DŠ9ô>  drFžÌ[ߣù!ÉmÎ?Kæ«$§pX¬r©ÑYcSr%©j~žMoáªòÌ3xc¿«« E(H‹–ÃÁüJï˼9þAê¶rÜ e~'ìtt˜$µMÃcg8…íNäØTã?~BOT.*ÐÑÒ_--8­®Ì‘R!ï4±„ƒ¾ñŸ YBr;Äß w]ÚÆñ¥oA:¸ñ½©;Üz>XRÃÑ Õ%«Ÿ§´àùÂLñöT ¶G5ø`ö'²« Ñ¨ïŸå˜Ó{2U²…~!ŽçÎDµWH“¥¦¬àd¾ÛƒÂ{%—ÛÒY;¯8gÅúz6eÐp[vbÌQ\”²‹›×¡W ç¯ ìáÎÝ8&¼^;ðc¯­Þÿ;ÜB _¡Ž™ªÁŠ=Z«#2’`‚ùAÑX„a~ jðƒg’¢uöÞwtMt•„Ay:d6!›¡SSëÑG}ás‚4ghèuLR³Ù yÙ²…ž‰_E3Î$Π wÔ#wäoçX&†Q…)€ÐM?  RÃz½Žü«ºˆ qŠ®ª—‰z“³™»s‹ÌHd|~™R:¿˜ ƒÌþ|ÑœÁ¿vŽ_Æçð•ó°~ð†Õ2··\9;›|3%—à©þÁ»ŸHRŠ/ýo/—2F&EÈÏS¹7[Ü6hc 1ÙV™º‹_Ï }çùûü0¿ËÁ^ÛèoN§Ç èT² „Ž´GuЖj½­TŠåfØp…$èáHsR&ò:Þ)DÅ‘‹6dìò±ägÇPÂðüE!Â¦ÐøXލÆf®u‘ä~(.Hßf ¶ÞÞ¯³š«* ýM8vñ„` ὿'Âvôqí¼m9l¸2&”dHp¹"x§ÿpÚ‹áC”«ÄgHSèøB{PÐdxFlmï·žþdÁkë“O¿À#W^aðtDB&9ëär;R"4êlɃ"•s¥AàsžUä*³øíÕÍm¾a*í^ aO"-ubì°Ð÷¨õIò¨^ÞI<’³2lÚ—öŠ]µI„˜±åÄ0rV ïÛæ‘ʲ +ýó¨4"wûÉþõb q/q‡ú2¼(÷ônÚ­&ÈR[Ìx¸/*wHòÀâx62)Ø1»îv°×Ù*Ì:"x*QqedÈ®ÉÛü C*9ÆÊœ˜¦Šçù0˜ÉUÄ2ÐÉ„ äp»¼wöž`a:(órD¸½jõÔÎ#n]‡­˜[E,Áb9Ú½Š§¾_ñÝí÷$ó|˜~6°·ˆŸ¤eà¡ v¼ãHdã° Mn‘ˆ"}¥¢ª¾‰Ì:¾/6œ/×_¯fìÑ“ìÊÝy±\²UFJ‰µ26èÈÙ‚rÑÈ#=FIÙßweèMo˜BôIØ%$ZPjÊU¿l.— Êe‘óÝÐW‹ð¶Â²'Frä p—€VhmrüŽß ¶ƒ2n^.ÑÏ g5ioU¦µZè¬Xx“}ç›GëKÆz&®}¥2YW»NتqxÓ¢õe_ȱyå”aÙZÐ^ ÎTHžž`O'Æ•9û~ø¶ç¿.?_g<>󾥎&õŒ1W¹rÀ‰‘ÍJÈ“B¨)½Dnu?CÒÙêî1ÏæÔÀ­äÙÎi²w%*Í¢.åÖo¡z\‘Í;jË"öìF\Fá0ÄDo/"fúéúË\×]ißO}É#çΦ›Ï¢ù™á”\5^!‰£€2L—}‹i§ýsä(íXC½Ï‚Ifé0—8)aÒº¦oÜÛL+þªëåæžñõ糬,¾·n÷è;U(¡ןêÌm@€àùÑdŒª·q)ŠÛmT{Ø—_zBL½¬li–®G2@9ÀàAáÅ¿t=4Ë ÈBÁ1D¸1ŒZˆÞdžÆ•Ç2ˆþØ¡ÃÍ û°|À¹jèŸßÕ×ôŽîÄND†ÐX¦Ñ%¢«C{×@ë,?f¥ò*±Š ‚e5«qK"ÊŸ–×x/²')ÀçCW6‚,oÆ›8À‹ß°¼FwHª?¼¨:æ'‘‚t<¢“¡çùà‰l…9¯“„ñôÀÄðeèd…Âh¶¥­.9+n†3€—‰Ý•¨f-$mÄ{ÞqgI›†ÑM4­Òm1ñ‹&p ™WÈ $°:W]W= %ØØ‰õÚOÓ#Ÿ®àî{¸] ã5H?íL•D8šhjPtv¯ÛíÔæ¶Ru+5Š}Ïß2å§Y*®(RLÍ®É>yCɤN/TÎoq= ¿°¹É’Gñ!à7ä°èÀ´œû£þ‘sSw·zàsý5úç¤XO0‡º¾{3)<á§x]Ô–a¸ã¸Õéü¯;wGíÅjô‡9ww~ä3¢[¾ôë}Ò#á\©©çt‰Öëó%¹œ‹8AÁ»4ÂÙÀ’¦XŸÞþ2ž‹[¯7œ¾jYôª³ÜŒÇüÈMÝ9"œF›=0YKJZ6S‰FR®6Z¼fu&ãø,‰f}övÝ5*ºzÎ?yD?z"ÒºŽZ|{|„¤m“ç*s’¹k¯ÿµzòÆ'ãÊØTöÝá$¯#•!A–+â7™w8ðH\¥0 êB!ò¤ìîï› :BNÕâJØQL{Òv[2»3!QžŠ1ßYŠ»ðWUK«òI~2’9%K ÅYÃe£ñã@û²³†©öë'ª —#¡2s0̉!ÜCóŠÁMÈbRŸDÑž¾gŠÑ1•óU?z³èÛ»¤B2œþ¨^™rÈÃf¦|'¦(J³}1cHÌ…¾Ö£ze‚L5) 8ºÃÎÓ`Y Ëñk–í{ òü×v_ÈŽ7诃ƒ|°} ¤÷‹vk™–®ÞæqèQšÏB'ÇoèZ›OµQúTy^w)C$å ¹L4 0„Ÿ p˜AÇ"Ùª¾ÅƒU,1&¾+'Nóð ãcrUˆG¶º°—ÐÂd ON‚ÊàÒ¿-A¶¶K™];ØóÿHvó”ºÁ¯>›wax¡X’l«.ÃR¾°Á®>».¥bÛ Þ¾pqIX‰D-0‚ÉfgK˜ð¿¸Šê a–åò÷ëÓÆãbC™ï’¹° “ñL+Qôëtµé[áÏÕ}“&é­³{Ÿw•P_ÝÜÌê"eSM(ÁðåÛÏWˆÁr€ƒ7À(éˆE¶&2›v&àÝË>ÁD'©Þd¢Õ`â™~,̶²s©Ô(8(…!-n¡….s“Q‚®J#¨BÛ3l®[Åíoëˆ+ãÀÚR¾+.|ú×Èë0³k¢.a ²êp@Eú)€C¨œ½Uæ7¢Ï'Ê5ÐU°é³É߆ &Js4è âÈjRÇ‚ÓNÓ,4—,öäîPÊ¥¼ Ðuµ#â‡Zûµè|LˆÝz£ñÀ¾ªýgN~×8C°õšó(6B°=$¥¿Ùôˆ¢.,ûC˜vh¶A¦Rô‰½¯škC"A Öm šrN€=;ow…*)!>»à%èxAÑâóT ÉEú±ƒöq‘¹8áâ‚9êkØ·Ì?µ>ƵÃɤ“Œ¥GÀÇZ¼ÁôÓ“š}ƒÆ ”ÔŠ¬cx²CÑgzÀ~`37EË_rržÛ^´\‘è ÏRï±)m–”Rê°Ýã‚𶃼j3² “Þ –Ü–!™ž°:‰‘í'r3Eþ¶’Lhö¤k‹Bºß¨} ΢ãGÝ.D)ÏlgëWÑT´Cÿu½žnfVõD¨ž0x­Dm—‘|0.›Yeø0(V$Œžq´§EûYF)ou¹e¢mi¿ÑG»þÂãЖٖеÐ)u2K‰åʹ#o·¶<¾Ôº§zl €Ù³Ç Œò  F„³œh‘-®"Ù˜<….s¼Ä¸ç5SƒX‰ó Dˆ*± ¢mL9ËŽ0¼ØÏ˜ûk·Â™Eµ­‘K·mPÄ{Î(]~µ\QbM9˜µm(Ô"æl/ïŠBdÖȆ²aÿ½ò%xâê„mÑíÉRü³u¼w0Å£'ŒËo„UćÔN©j"ЦX÷‡ˆn i½º}ƒŽßmꥫŖyé·mhÛ³Ñp/W6×V ž•Wº¹¯üU!&±ëîWÖ%;FbÚ,­K朑]G× ^å @ºóÂV!È%¤Ön™¤Â ìHNŒ÷w‡>*åF£AäAý!$Œ8lý×´Úˆ`ÃDFpë3P»ÙDðž(½ãâ>ÀÂ×€‹[»&O—zYGæ Ïd ÄÒ¸±’¡Y$Ui®ÿÐòHââÀÀ0¥D@Mw'ÍKß,ŒK‹‡2 ¹£ÚV? þ?è$6c9’9è%ÁE8Xà…/&–uµ@abGŽèç« 4èa5U„q–W‡D¦"«nN~·Ñÿ(Ù·‚EdóŠý<à|Ñ5:³rª½?b¡Æwïj˜²lŒEºÌ’~ª>ì“ (‘ìq'x 1aÛ©ûÞfëŒ* 6§g7]b.t&m¤Xä©ZñœX8ª½#ûÚ´ÐÈÀ Ùt¾e®æÍ>P“‘ˆú¶+¹ ºÐ¬YâÚtv§.RŒ0e¸»BS ×ÙËä}Œ€ûj»t¸ëá ôêMTë/äÿtŽv· K<œnÌ)‚x˜TÒÇâWh·e65úûKr‚bHˆKìmLå 죕o`tÿA!¥ÿùиyñн„±«-þdÔJs5G0)rñR…p´ð!½ÞÊ>J{$fd'T¯ÏÇý¥î¢’?-UعøÓd6¦i‘ä> q´Ðš'K”PJdp'¶FxCKOô+ô–Û;þd/ÄÇNçZÀìJ¥ÏÏ}û´mX&û îòÏ•ÅH„(7j°©~_÷«]„B€BóÒÐ<= …ða ¸ÈD2.•1atAêcPÚB(¨ˆŠG3~ WÃÎPáZ4êw&ä[‹úÚ/²Mi·Pz§é£ú™|»h½§°ªnÝ.þtýõ‹_?.þò׫«¯‹o¿·J²?ßÞ”q˜S’z"êCø@ ²ÛpÓ&^4Æå´äô‘„CÀQìïÁ"³Ù&Š7ù3‚Ñ6HýhR¤…¸«QGP(Ê[B÷aVúš| ±%Ä%QÓ’Ü23‚K ¸û"÷Ø#Ž ÚJw'ð@^襄PK8,û/,²Ð(±a@½ïícc&v|ô{´[©'Ä–ËEÆiÊDêYXƒÝ7šÅš^x¯Ð^d<9žÐ%0a*à•¸‚{eîÕI™Ò4ÓTlƒ›ÔÑç!VÁzà Æ#0Ò]~¬ ÂRþrÄ}büvË4¾ò¥ú§‰“²µd21¼nNö+­Æ2 Ž1¾°î—"ªóè×ÂÙå-l+ lëšu¬QCÑz¶d9HT6t¸™7Wï§ ÇZNlÍô:Ù÷m¹"t|J¨?ÏM›wnË:‚¢€4Ö—&\ |îGl‡ Š…xœúVðwAàüø²¬Ê[[’RD#(´k7¨¯Š, %ùϘ:m~3©ÁÄWVW —éºщ gO†ªÐáBÃßôåšÙJš@LbS $ƒ·,yÚ š¯Bß#ͳ¡í¹‰¦v½Ã‹zÖR¥ŒyUPÝÈpêW¤>QX5 È’Myû ÈSIî§2Ämð°˜ÜÙ¥0¯º°hêgÓàï2g¹‚sû0a›ž{s6»tÌ„5Q›| ÃRÒ‘Ë[ûnM-ë™ø=¹& ýXÃvëq†:ÿP»:•N°Ù7ÿ ˸'£0víXÏX»*W—äÞEl…{£÷ÍYañmѶÔ&‘?;B7Ó”.[yVôDöT£b…°Eæ¨[ÂU#¢ß3+cŸ>ü äðÂ#2&¸ÔÛ²Õݶ\TU $ÿ$)÷i1´[àÉÖE"ÚÁ Çálž~ôxO÷žÊsPeŠ‘;ÕåKƒisÎpÓ QvÓ±)U0³ä+Ɉ7J)dñ²tî[{km.^€GÖT{ ºÔ¦§œ©Õ! ìÅûÀÃ;°œC±Ý|A¥2Úÿ2f4´‘f r×&d§â„°Js^œ× ]8â¨[EáÌŠ$~[8 Û"îų{‘o:UrÈ÷™wèêñøGQ¥lï-Ž6³Hü ´eÐP7oœC§jZ|*Še¬Q¶³¹w› «¶@CÖéqVC¬žöèbŸ¨uY×”‚¸þµy€ ,=D¦â^³©ÄÆÈC ¼k«n4ìß>HZÊsíòí®¡”Ë“õL&X¸¬ŸGÔ– åzÀ¹<TDJNê=W`ÃùºßøÿÏ —Â?r¨ÎŽŸ®Æ”£Â’»D?k²Èj¢´æ§CoÅTþ‰¾zˆ-­è BTÄ…vµ3isž-¡ÙK±9hToÀÚíÛz…Ûlćý3Ó:1G²»Êm[À%&ܸDéSÆö!k 1&Y-ÍjïÜ?‡¬ŠÝ•sû°ƒ=alˆx°\lf{ 犃!z4z_'TÏù0¨ó,jEíÞ¿ÆGv äDH5”Ð¥L'<¹T÷n(A_ƒ ØMj2®=Çë1ZÈØèÇ.øM|[cŸê ÌE®ô_+eÒ-¶àÏ?Ln–M¶ ¢¾©Ÿ“0©$Û†©ì1qᓦEÑPJ‚yŽNfïJ6¹éÈ%íD€)u…­ ÛéòÞ ÛÛZ")óç 1Ÿñvȃ&’ ¶é¯3×C©¹~‚k‚4¨IÞw9 À±-hgîFlÌæo¿„„M„èggv{nŸú´£œï¢Þ÷ø¥G嬕#)Ín&PßÔ Å'­}Ÿn³¡´ÝÎõVìÂs¡ãzšÑnÉÕ#ú jvÌš]Š„ØDr–1}‰±¯ß½yŽ8¯¨Þ!º5 ÃýÁcþ°W ¼“†zE1Aë%=ˆˆ|Bþ^ìyÄbžÜ(AÑG‘g üÈ}mkïU²éØ{Äľ¬¢ð؆ úâ´WnE8 £ãºi_@`J¶ÈÊ8(T£¨êPhi!Cž2Ö£“;W•¦HÛÙd»hÂF„bÃþ…´o2 hÚc\ŽÓ)²}—P§yŒ¡j ‰i ¨X¼å°íÚ·Ç´õ’ 'ÕhW”[~É`×ë#S ì°—‹ ÷ W9nŸjA4*î+~°Ý“¸‚òlè>¤í—¼;Æ^S?ìÎàq=ìã"ç¡cRYÝcJCj:Âù,ûkùb(Ê›P7m,%[× 2*«‰y%aµÕÖ*þ.>ÕŒÔ"7³‡Î%){oá§0•·"i?üôËI$D²äø¸¼ïöÑv]ú੉RBî*\…’] ½ìBÜó†Ec4Þ+†]Ŧ[Ún.v°ÄrÎÍVÖ¼¢½¤ýhÁã‹ Ññ2Ó¾=Îza¶¢=ºÆlÔß>˜†µ‡– o)_ÈÂí (š˜öõÐ:~±Y•8 Ñ‘]´êƒäýâ6âqêñ¸5RIé2ùŠ…*…ûN Îl:n²’ôôT»µÃêªÊ4‹pkÚÄío ©úûj—‡E†¶è8Õ`R­“ ¦;ì±vI¬t’JM@ãjëv1¦_žöçÈ´§úÁŽ0FŬ2Àh&º¿–™LjÆQ F0¦6SF”žÂ„~år@ˆS4¿Ph-²1ö爙 J6Êu„ *\Lq”€iP“µ‚U°$.Ç2OñÄ÷MÞqÔ”n]¦¼Ý£ÁŠ.?줘GEHAƒµL¾—0lE‹€H&÷âePØ6×â$ÝHÑÓnj\ç¹ÁÅTv‡D-- …¤üÁDÖHÉ ‰)òn’îBÐà¦c‹©7¹^s§/äÆ@h´܇¨Û ”sŒlÛ¢þ-ÏÉ•$+|p}„§—ñh¨’aÈcS+°`íMƽ¶¬aþ„rÞ¦)é{O®  °ia”`»×k>A«/¬,p±,•‹w(9×LUginøJ¼gÜfË×~ï¤4¬iÂ[üøgc5ZÂA€r[74Ôi ñy…¨¿J(ÚB§ºå`àOmÒ‘°z>?Õ6Ëéö*(!ò-žº;†Ë,þÒÇ“ýC5YQuÔémÊBQ,ù-ÀµñÖª‚¶„àz|)äÔ‡wE‡–T#§îl»oE¤ç¾h’G—r81*K}>ƒ€ÈÀ‰9çeµM0ŽÍê^4!^['›Ž`æcCÀz^Û“œsq¶0&´dé½…Žp.“Þû–‹¥è4 Û¬«Sh‘ŠÎO0W6Áfak, G; ¶ÑNI¡NwÀ6ÅùÿƒnQôûÍþ&iÂYØhOËIÙCÇôT/^?ÿ½éMI³RÁ¿Œö|É’äÚtÄ9¨É¤«¼ñ|í ¬¶³§oiÃA£qÏê‹ ?áòÜ„(S8‰]ã&0|>ÆÖöû)K '\áñóÞ}ÉÉÔC¤„û+Izˆâ!Ô|âßåĨ)Ê “˜ÅC2ÿÒ We¾Hƒ¹ŽüLgêtC“ µÚs('ù$()6„ÑøBP#ÄàFîAc<¬m™¶¸³†4Íuè ušC‚•Æ®ˆê¬UU0®î½m²t¨YÒšÙ43àHÒ¹Ip%­e–#;P²»Z¤‚lÍ §…ÄÇ'm¥‡Ž`Ûa‰&8X³ hô‘„ê”t9•Š*#ÅæÓ¦ÇÂáÄ*ÄqÁ †ðštþ°S¶½t“Á[%ºŠÄìq.kZ˜Ì,O@‹Ô±ÊØ4OÙ·ŸM„Wªœ`D ­Ïnc#ͤö^;µ÷{Vh‚Ça?*ñÌ ûó¶²Ó_JÏ•”(&[j•¦=ØNà?oð;•¤àÓï\‚º¬ ˈ{M-•Û.Ç‹‹?Ùdi<Š”¤E’bœññý&Ò¥Ÿ-áÁ_Õ§{Åö±¡TQ¨„eä\Õ3DÒ4XŽâÔP_™d«Ãš5h%NT+èWŽFj™Un,_ÆG3®ùx£Ó­ÇG|7ã’âG …ä3ž›µò‰qG=I´Ÿ2úêkñÉÁË„ƒQ(ŠZæf#”G‚>p*˜"síñ~®Ž’ƒ‹mã\VÉü[›GaÓåt€Ä4 —Ð ƒ0\¦O¥â*Ô•HB.?¾™½ÐD‡ÐD=ž£\Ô4*ûÖkF£8Ÿ™×Àu” °4köQ¥Bœ—¢Ëk·½˜š„yXËmŒ‘(ÐWjý…œ>­|d:})\àë“NŽjƒtfy ”ö,J_ÊÏAfâ0®pw³ë²åaq;öm2y¬w—ÝŠL­‰âƵÔWžo±á¥zÖR¬HÏe3Y»‰ø)%Ì;Û ŒÕtî)£v,4i8¯&Ei¡,3„aY›ÇM—þØ·ò*§Ýh×`Ï«³J¬ýÆ÷çÆ±CƨŒÓÀ ǵӎC"GÝ…¦"O¯¶8Né×ÇÎj… ù¤)©-nùùÎÊË5 €¼•`§F;‚Î.‹›ì’Fƒ=ÈÜeÔ”€Ô@sÓš !0&±ÆÕš¥ÇÇR×;ûUyýÂj67G,F&h’oœ`Œ|pz~YT\ž7Z8?Ùµ—]µ»šð„äWøÍJ˜-ŒYqœ iã»Fûáóg7†;˜F Lán=£õÚjÁÜ :`1iÁ÷v¿ÆrºµM/ Ϫ&7tµ§«Z.ú:=ðÝö EÝkcPÏ>hW¶xm´Zþñ)O*µ%}$»î—HøT|ô®ê6õqÝì‰ØyüôúúPºgS13F“»XŒusöÊ€$ÖT!ÝâL =ydbë*Ú<š_âðÔÕy A…$KW~AÞüÂI9 ª ÈJ(´Dµ–Fó:PÜëx£–3ˆªÍ AleŸñ@Ohr…µÐžÑ7À ‚M@¿k梄ª†…žuA} á(¤õ¬;xvßþÇÊÕÅ©P²e¸7‚|&Ɖ°nD2 > “Øõ{'‰ý·³\xc©»…ÄXx\(Òeh )ƒ‹-ôÌ`²,¶í’éž‚è0'‚ظêïJ0¯Á4о‘v.­ÇxB’(Öø+ØÃŸôE‹N MÑ`ÊNfm·%5ÝÑ÷³ÛáëÂxšåK¼«!Rü;€ *42˽¶0A˜Pƒìkwd‰ {ØAJ”NoS¼fyÛôÂ#-Œ§Ùól0¿ßž‘&òlâ¯î¼èï¥#¼ðS¦Þë¨2ƒ†×ò€«Ú)ù‚„i³“ ?¶”6(†Wk-ÙWXYG¯Eb/*Z‘ÍL{e›÷„*8žØâ\1ìt¡¸‹::µ^œ¬|ip¶cén!+‘c—·ˆnÂ>ª¹ö£ÏŽ»/FÁȳ€Ý¼•X›x5ÌQLÕæ¤úIÚ—I+êCdùß#°¨ÿ{9¦Á–¢NÌó‘ýÞU£ÖMf"+ÞùÕ´S%ø_%¯ ÷UÝZÏxPš0I±§ÈivˆÒkÛ¤iŸkÜÄeÑäU!\¸Ö?€ý.‰Â0£aKÄ`nñŠÀ¨T1§ýûPÏ_!Õt%<Ž«°î`dƽe¿mºlje#ŒËÿèeš†<ßL~óádD´çwoóÿœyL‡à=T›ù±›>#p›ï/Ž /Æ]§k”=AΪåÚÎýõ×?ý ÙÀ™ÜbˆÝ‚ïÛá‘g9íjö€I‘2d19×fí# ¨ ÿÀWÿ6~L0Ú¦g:18¼ã_%!·Ä$döòL»15¤ì÷òœ€¹h*—ŸêDJ¹Ÿ)<`è2—Æ‚2)”ö–MÜhÓÕ~7/îíÆI Z9e9°1¡Û ¹©í’¸bI2|È ,Ÿ÷ìX2ûhOùЏIóR=ù6º‘͘w™¦}m ôIæÇp")ЇZ¬´g9.¶!ǧÕ/O¶Q=<Ãü¢Í_•¦K˼öés›Éó¢²0î0¼ÔÖ6\Ù¹çMœvxÀB[£kü@À19mNl%q Óz`U4-H¡TäÍhcš Ÿ}ô>·Á¥«# ‘³”ÞLR¼Â<íª#K:¯¦Ó&2’Ò HðxRÉ:9Ÿ†à áêMb™ò¹ó6ì5ì LÚ›bkÏà@»J 4ßÍB/R´8âDµ[dHŸßþ/\œœíSNÎÆòÚÖ6À/÷ã„èâ·‡‚,›Öº&j7ÅÌyëG@íå^ ”}U$`Ü2a&v‰=L†É?¸J¸(–¶â7¾êÇ¥_Ï'x¯­®i(‚“¾ó²–V¤M…—͈äœó¾haÃf§ÌM×ð·‰ô¿9™ðdø~5~0³€¶ ¬&Fžs(({E×ê±²ÆøÎ¯PÓ—áö¨iÊ,õÅol½¢Û&í#•zqįÔ<MÀ¾éÄÆ {wÖ43*Ôûm†Éh(bøXNïH·/L¥³me5mÒÁ)!GÜ ®¾—u$¯äÏ 78ƒæg¦Ì¥‚Îv;.ÍKGÏÔGνÈþ hSòÀ ‘‡ÝlÓCÆבÜÝ24Óæš 0ìÂoS£#ƒš˜x;Ù‚¸‡µ`B‰ùø, –Õ„„R0‚…ú/x­+*(^Bþ x¶Ž¦cÕí ½‡RIĈ'aé((DgÒg˜yí*–Œëäi±§Ô—j›PX¬ãÏ짆DVK¤×:ZF=€¿ÓèâZÃM«7U†…¼Ø«Ïž>¸hhpè3°Q¼Y­Ô» ¸ ôÂL²x+¢¡¬lVŠ_u]™³ùJáâå/nM!"¹°\,ˆÎP¸·óìNèݱèÐ@¯KÓC¶À²Â y×H°MS ™$h‹ál—ö¼è/£lÛ|–S‡ï‚ƒÆA>~·ŸíäÕ!_Å›-ç":r»³-[<º[=oØ©—f¢:9{‹i¾Ô€vêëx–‚õdL¡Uè012Bîpß«â0ƒé—¨Ø°ø bÄM7R}|gFÍÊ6¡*èHlGˆÝö(~’·›'Í0Á3yÂPŒÓÚã|Äûln€½1n3$SYDhÇ3/¶—–º‰¦Þ™èÀ®23½³'Ù|<çTæˆ],o:³œg³q|8ýàëÌÃpØzÍüq@Ú<Zœ)›dYåTIjˆ8ÂjV“éBþÐwÏ¶È ñh¾“0Q“ÍÆ·ð§ëß‹wP'FO@4‚UjûÀuîÂ8Æþºs š9ùù½„”NT7L$ñpÛdÈŒ[lÝxÏͽƒÎd½Ë¥"’Ä÷Zµ¤*är-Cy 2WRÈΊ€m°¤(Wàb1Cª0-ZÄ!ò[}èÒãD{(S³ãZòE£Î‡|°‡Ã,Þgû@:3(æ5³ó„DD¢)×Xµ¨Âž¹Õàí³Ñ%¨ôk9ÓÍ>ÚÆÖ è3ŸSõEq¾uÿl©<Íf“êa;><ÁÇ'¢ÁÕ‚ÿ.à¤+²–ûsæ§W‹›O¢ÂoPFXÐèÙEÊJ©™cȨ¡j­Ï"í^Û"…Rl®†˜jï °óûüí9 òiQÕ³¼ŒæNî*f($£ò¦å™,?$w… ãÈxH–#U]RÈb)õôÐ# û}È?öàmk”XÊ›±} ‹ª³' bGÈby®Æeb¼¤/~4ˆFÝ<Ò°èxÊÁÈnÑIèàfßDµ™FÞ¸Ïルîëã)ü»YyšÔ ›?IP÷ú!3Ž>L$’ÔÿÆ]G– è¸¹\q^\H³4ÅX9 7ÏŠø|¶{‚˜–‚°–àTàA8ÜEçÎNôƺ"¤–°ÅtDDIÒ™ a3Œ«KÎUAÇíP‹†_ÎAm6ð;õÜÉñª°uv‘aÒ{$÷M6¬¬ÈÍð4v,Õ .Ò7€ˆ‰°¹/Š5Á¥Nͼ±áU©;+Zjú >­‰Kª^ÙD¬­}®‘λúÛÕ×|õ ‡¶fô0Ü~Sô€Š–l] øV=×[«Tí¯d¨ †[‚EþÖnÅ&õžÊÿ{žWŸtšPFL ¿ÉÍ&|»€¬äuD6®½7òðìA°¾ ÃåÛ±üôÏûï]çÍô$ÕБžqáÀøÕM Pàl–“¼3XÜ=uŒf—#|˜ÀÀ ¢‰•M ƒU„#IÒ/|+-<Ê…ëÄž_‘w…Õÿå¯NkÏtYÔ‰v†­vö@ ?ªÄ 2 D!¤{9OÖ4hS£(Ó8†A Ç.^Äå’dõ{ÇÖ.o{`vï¼›UŽQEe΋½ƒ(ê[”ÕÄ8œÒ䇘gˆ¯—Å­…îÚ¦¯ÇÌ ¤ ˜|ˆ'ý•iô Ó-ÁÒÿâ»CÙ ™(ä=ôóçßf“ˆP&ü˜o„%½&íÎÄbÂkÌxÏÕ 4EQí $Ï7Ëy5¯7H˜r¹æ_™ù“}ô9d$»ósø,~ >üFDÅöE~bÐÍhb⫘õ à§¿|¸ùd@¨ë5Vúâ\â¥àr$:BE7§ÛT‰Bºðä\DOä­„öL»‚¤ÓìŒ xÝgñ–ždÍÊšÎ'o.ÍHÏN†àqžÞûu4‡±+ïâÏÌÓÛ:ýÒ5QÄ¡˜ÛJõÕ>)8IëÌq)ÝIÂÊ3;–v•“ë ÙûÁ_ñR[ëäzýáÅòõ@õ±q‘û¨Œÿë“y3Í}¥:ùu× ÷¨7 _9Ÿ«îê«‘·`ÖSoמ°Gö®î®zE½s&ù„³Å×âýØó÷#ŽÑ앜Žb¹‚E‘\GGAn<{e‡¨Ã&+oÖkGúöÌÝzÍöõz×¹ñTwEñ4QÊ@dIÀ)‰ aˆˆ&¢i”šsÏ(cHqÕÛÀÐÄ¢£¬ÛÆ6Y©¤T½%*ȉæ£õ•›Wo"ŒóÞ¼/gÌÞƒ/G7†ŽÓC«¼,ZàÃDõ{*¦æÆ ÐÊw;D~­O—O´QÝ7´^ZóÉ·a®;M@ÕZñüîù˜áމØÂAýkj©Ý#û¶e\EW± ¶ðïkp[Öß/­X^vÔðL rXF-`²ú÷äãh¶æ{6›þD4 ‰€Ô$nÂõd®¼FŽ  Ó…Å#.#ã»oQÃjbE!ÅÄý½S¹¿¯!Q«:õLý±qvÙžäT7Í}ŸF/M• o¤-Öµ«îV¶bþ³R‚ðq@ŸÌ ó9íWsq>ûŸ):mŒ¦Š2ùsè‘#â^HDoâ_g}oÆBš8{~-Þ±wÍÎc„îx£çûðOˆ,ÑKÛ–B†‰,ü‡7v#Kl\Á áOhÌIßQoã¹"ö÷çÏ«p÷ò-!ŠÇU o¶=¿Ä«#® (·³Üü*ÊPå[B1ÓûàÌNDk¶.í£bØ#>^oõ}@o¢ÿ!þ?œ'„[£ì( ew|¥Mšfä“ćXùu`ÝñBÜêõ¦vÕõŒ‘ p]PUX褱ß@è•e_>Ê·­j ²BÅòÒ‹]êý3Ý:* ö)+ŸXåó‹-런jO´=/ÖzUùx>”)S_,§Ý(ÙŒò¹½ƒ$ýË¡z0·Ñ×zb÷ãbÕ6̸R‡ühËÉ”¥}ö¿ÚZ'Ë<àåði×Å® Ç#¢o¶Ó~p÷î{ϰGK\ضß$l6¼ëì@»³±¤G…BЉDd1u_‹E« Üo @ÌøÊÀŒæ”å‚Nµã½^3ç*aF[`Š Ççfª©_}N¸a—ëµ(³+ø 8ÐÁ»K:j¼èt:ØÆOËÈÂl2íߨom,l³Q$zd/ ñ;GÎÚO̤´+h×½+mÄ.’P‡úÏöÛ&OOn@1ñâåoÜ>è[².cð„ka^ÌúvQ’›'ÝÔHy|‘c=cŽ–ŒÈc›V( ¯ Õmô²k!Jgƒ»p=!ªqH…l’9Y;OZ;\Ù2 g|ȆæXç'Ù¼9@OÚ¿Á‹`A.Zð¶0TñÃG0+"Ú ¶f¦Ý0jk³u×ZÍÜqê8X,‚¶Û£.~ V-¹5yúü ÑŒBû:˜ùà+ÃÇÄ ×‰E&šäìuœúä¡’Áo[DÅW¼±ñd@æèBÙæûY€Ñ™Ã„r$%" ´²Ñ´nÛfF]¢¼Ã[qIžÈCuj ‚*-Ηê.<÷-¡Ç¸ÒUïI'â…õÎßó~ðE7¯Í‹@¡_h>T`¿WØë®¯æOª·Î¥SìPrŸ]’Y$•AŒrµzË£Ârôêe#`f(ÀƒÁ8Ë|6¾[xˆ ³"]$ÉÁ ,ÜI›Ä]¦¡Ú—‰I aI°ý æ`Éî$>ÌÔ­{ØìY+8ÅWL–¤°Îš€ +e/#ˆ÷,5~X‘FOôb©]$8eÃqFÍŠeþa êo줘œ¯ñd=¬Åî8Ù†³ùçÓÇàùcdÛ‹ˆ$]R%ø~GÔŸôˆ¡±2×ùíð£ÁL¼ž²‰Ú"³µ «úe»‰¶' #£!aJfBøÚ" " zömݫQû6[ÈX ˆÈiAL1¸Ÿá‹”-å!|Ùe<•ÑÖÐg¢•yn4ŽZš‹åù/MÔì ÖBgá.Þûs¢‹Û{ȃba[5à³8¿švm·Cæ¡Àâ:¤@E_¹V­®{FqÙ>Ú½˜÷ZSÃm¢:9‹Öñ£ç*%JÛUäa8A8õà³lSŒá¾¼Ç¸ «èÎÍ¿þ臓ÇüÑ_gsž}ÝŒ×Ò´Ù¶È¡eÓìñ@',HezÚÖŸ%Ð8¾*!Õu’Æ8;·‡$?|ì3c7¶Ù­3%ºß9;Ç“ Žº£þ»Oc½ö]§IôA0ŠÎcô½ÉvÇC4ô*ød9.boø[Ó-úëëÜOß~ÒÿøœûŽh¨êTãV ï t﫬صAy„¹ÆÇÄaã*êÖ¸ùȲx˜~#óa`½Zè"»ÃÖâgÄõX\Ù9ç%I×¢â÷]쾘ü/˜Ò®2µ¤÷°ËØŠ·®ÈÅÿÕ‰dÁ8A="F(ÒÓø<m‹ì7ÑRhB8F4IäË$8PJ«– FNì‡êÊó3é"Ô†éwRJÓÌ£ßÞõ:­uV•¾Xˆ’I‚·b׫Tjí£)4}ÓgabwënV/ÞRƦŸ%àOE§qP?Ú—áHÞÕoÚŽ¨¼"VoïÛ’¨ˆQø›‘$¬+ã…LzÚ×ÙxÄ+Ę‚ôÆI u ½ÒÓónº¥í-ÀW‰ßñ¾³2ÎÔùì³íÚ"(Ÿy;Ì¿OAuÏçá‚Ê^¾ T=ÁO9ÖÅx¨ïT8Å.Sp3`3&ÆÕë–”pÏ.M@±oÜf¨‡GSÑ;ÖZ³³Iu°³®º]ç\ÑñžüS;Õ2ȳK¤XÕ²¨ œ¶#5¤_?Wbè­PPºû“ïk1a¹œ®¨>ÍL ¾D;2Þ SÜÃ[t.N…LyÑÐÑ88^÷*¤»E-\}Q„WRv²Úªt©5åÂݘ·c?"bØšqŸŽ†•Õ;ßqx^!e†&ñŽ#¿q»øøíêÜ•¿ýöíöùÛ›Oö³‹2üÈë‰U¡Å\"¿ø{·9*3ÈFû;çeDRvÔ×Eþ J-ÝÈN«à4c>] ^ëxÂâUÂïç}¤÷ðU; p È\„¢[*\·½¸—P…Õ LTö³¢Ï°ò¢ü" bHÃ[ÜmÌ£E‰¢çã·å¢”‘»ùL£êþ†Êùe5~ÎÅÅ“ù”Ç*IÐj:N$² ¯aÀ<탼iÛ~œçž;;DÓS^—Þ‘*:ûiÜ_6ã‘&[Õ×Öº ¯|NÙªb¨R¤BEXÆXM×âûPìP«xg)$éñ¬çò¥b¶œ^.^MzC“¢†¾(VþПрaçó7™ÁÅ[ž¡J¾¼ârºDt#xê ÇÐÛt›åœ1ïC!ØìÓÅ(R3ûªŒ|ƒ±ü¦b4iVÅI,ñå*CºØÁÀì»Geöžg§+u$k’M'Ž%§CSð#ME\çÅùÎk;|>M±ægGq_ý YeغgoûVíö:ºQ%¥¸G€˜!½"–ýê!k#ú²&e$÷úi{u³ ÕÒTƒÿ¤ ÏOèà‰¨)°ü\X¶Æk×Ç•—C2'Ç2W‘x >.³…Wÿž»Ì†EЧoƒ»\óŠHÐŒ‹µÖ½ÇbûÕ¯ý¥Û¨ƒKË¢BbÆ#2ÄlŠ\C¶ÚA »TÑ8bQE†ç©†× CÚÀ£¦'_5•(p”Dö€cRÆ a¬œ€­Lv º(xz¡UÚ7iÒˆ­!GÓ ñ‡CèŸgý­[1õiUÇ­.Ñ<§Ö%2:™c|Ÿk@ !ð¥Þ'¥¤«º4P« ŠvºQ«òíóÇÕâ÷nTð—ëÏŸ¯?|ñ›Õ»îvöàª5d©ÿf“ó:|üд¡IÄ´ٗǰVó€ªeftÎxáfç·è<܇v7kbå×ýËaçÙTvÐX)¥,cèBÁÝŠùÕQZñMÚð¨ `±{‘UiMåÅ. !·U·Ãš}ÐéÅãÍ>øgÇ:_Þ^‡ý÷·o׿³­þ=þqóù¯øÃ‡Ÿ?®Î¹fêô'pÑjÕG&^Ô: •ºåâ1»íÞ3§ÁŠ¿ë!ð}–2œÕÖGPV>&?É%Aì¾å»š e3vöý -JxÏS†+![a ~hÁöVG–)¸Œ”!$ë2ÎxìpVø¼hé4„w‹kœöäÜoœÇ¾‡aV-T~. i[»"Þ•Áv<@êþ~cÀ;þ½ÿwµMqlEø»Uþ‡IJ‹`-˜x½¾Ä‘À&Y%€@Œ–eY; £ËÌÖÌ.Üõ×Ûýt?Ûsv$å}©{ÝóÒçœ~ï§ÀõWŽ“Éz3Éwh=u&Ê+áYú•Km½Œßˆ¢ŠŒ,ûAÐtjâˆR–â5å^fÿùÛ\9BVLKcìXË[¯£håÃôf¡ޣžx­au£9Ø’ó/PÊkéøÄB^ . LË•á!UEKª^ÜòKb›[”Ô…‚h»ªíPï–ùìp÷ã+Ÿ#Ò85ûoêç²°šÃ/„"ù¿wò8‚y@Qé¤0aȇ7|a‘1–æg–KO èßQApœ.Ç„“Ðq@ê†Ù“T6ýŽ…±ÄÂõ`w8vŠCß¾¤Ïi9 ä_ÓTÇ"¿Ø† g¥<"Ÿ§ ö85¶”qâ–#éÓô‘âé¼v•Úȧãç³rP¡,Ûù@ Ì‹m7ÙŠ†3/÷»ž €þ$’V \ðÁCuŒ€3GCm"}^á~R«B[¨iUB]b(j'ºZÁ}™4ä‚¥muI,q%zK Ýúx$ólˆ$"cûf¥_(ržFë¢KÊ3'ˆxa5ضáÚï¿ì'¸Ì ‰a¥¤òk暦VÌa”QŸ ùŒÒb8ð"G,±Š Eön7ïðÂlKbët5/ÖÆv)áNæF–WjÄB½ï0«fQ2Øÿ-ð2Dò9†‡ck²°Ñk_¤~®‰˜ —ÅŠ ¡â­é:-VEÙ‚°œžs°[„A7#Ž8Êf¼®lº¼õxŽ^ú»réý ò $-äÃo“µqWšüT /é_U9³èƒ0/—G¹¢÷Ë”dìÖŽe)Wy0’½¿÷ “W-öšªl…=¸W³ÙP^}'%MÕÖˆU@cy—á}^­ú1„’vÙíå€NY°çá»Ïˆ¼Õ…æ´¡”ÜÖ%Šˆ¦l r>Ýóº¸3Ò„ <~ •ï]úžÃrØ9• ¤ Eð±göjë$o¿ {^­ïG•}[ã>¹ß?* ‘DŒ8ï×rY*-½H‰å>R}ôN* j6b#X7’ÐÚÙ¾ìàjŒ^v¢Sµ7Z)¶öŒ¬G¶F‹NÃÐÅÕìdr";ªÊYŒHDüäMœÞ[ ÃjŽEMéÅDŠ‹‡—¨O±\F5àVúÇöÇÛû…=EžzÑFøÀO‹wúQLâw ~WÉ^|Û³L· 0{5ÿRëÏú+âDÂG圎pÒÞüÃ4|¡ô0)ʮᡲ±>æìX­-þ ÞùŽD ’‚ œU3kÞˆÏUµF1o‘4ÛZÖ»g~ìÿD½½½_޳‹Ó/gΰÅb\1a«Ÿ˜ç‚2}ÓPáÑg°Òˆ\^T¸s G iÙ”ù|‡hnˆ¡B™jËò[â%$ 5®j¤Ž£€q5è)—¥¥ép(›¤*æZ%V[³»i—Ùüïm(s|²—# \ú_jâwä¡l¶`ˆ&:ï'9ïkÞtJvG´° ì~!€îü>À‹@_QêTã#†[<¢üÛ!GÉÀjk ÝÛQÝ:iÜüu!¸ÒÍ ‹ªüÞE2€3#Êg$‡z«má¹F¢n%ž?àǬdXM„¿CØ ^ ‹¤Sô\“RA|ïÖÇ ®ü†Ã;¦Ó 5Ì¥±PÐ-©•‰]â'øÎ~ÐaVÚHÙ,R¸Ú”ĦćqhH^)Ï…Žª¶FÙ)+­Ðpj•ÄÄÕ¹˜ ”Àt1ŒaÑûÓ!x¶½ÆððÔJÛ¿–*Òó,ð=^·Žu©û–qØF%í•YAt½”-‹æL’Gïáe²3œ£*ÂaIDãöö;2"m_PXf³Ôáò„g_R“®Ê×dr´ŒÞmQ5ЧÑ=šA›kºÊ¡ D‚F háNÕ,L@g’Ö p 5eØà¹¼aE ÷UÄK¬–7Û.;8½2¤B‡Á ®à¤´—¥ÏFçv2eÙ.J±"+GÀmYˆLJ•8»öïQÁq`8F ª2ÿº[umͯKrÙu)Ô¬÷®ê†š–á“ðù?(úy'“X+$xrŒ²J¢ó©Ì!æ¢CãK¸ ¡˜W¥´Vô뀿ƒ‚­ÉÞw¬øËð¬I.¼nÄA§B"ÄÂAŒºa‡-è®v(›fØ€T½Q^_³ð"ª‘,XŸa®šõâîTpžYñÀøÊMÙÌÁŸ-x~cw×¥W®Â(i@Úé`°ƒ³§¾ÓDž¢‹¾0òu"pÙ±ÑâLŒÙsøü†ÄÔªñÞ²,„Ar÷ZÚ `;«†)K×y· ÁŽ£—´‚ŽBŠ$ºÑŠA9fË_„˜ÄóÞ.‹5•ðÖ:O&³ÆÅÌšî6´ 1FÃE„ëaÅårµ(§[@ý)’ëj: ¤ØBÁßÔøOà”Éh†íÛ“t®à!r°sa¸:2f)מÀ, /….,,–_/ók&$˜\ £¤w›È{ˆ=öº .½%¡œa7¸@”d_e³i¶ÐR šW"½x¸t<Ê6%lhAv M¾ø¬°Ø&é4«TæÇì>ðF«£Óœ5Ÿ‰¨ÂÑ+Ž…åpRjï‘+¡4îtaˆ5r¡² –úÝJõ†d…;¦)«« ÚlXƒÈ§\3iã…z 9ò’žqþä „“¡b4rKè7ˆ$^(e©GÐÎ;®þí€ucž¦¨3OûOZƒÖ3·/ØRRCÅ9ò=Ⱦ·ŽÎÕÀ¯!—Ãö²W.oÞ+f†ùfÊ‹"¸›Å&xªh öÄÖVK^À¡ÌF^ásënŽˆ{²;Ù­k«!rpïŠpqÎJàŸóF383G±l}œ˜$7µÒS€³Ñ@U++\ðÛ‹›AÜ-+›ŽØ9¹PïÌʽ¼Øêßë³öYƒHî—PW*¹ZŸŒZq•5àÑÔ,8 }Øud–€å_l˜G`ÞÛvÊ(ÊŒ­0¿d¸ë@õnR­î¤’JÉ÷ÖþvIÓ¡;”ÒÇí¤ºí%K#É©z¸s´Ò= À/z"ÓòZÃ$!U·ÂÙ!m*û- |»Ó•vª’p[vGŠõÜ1SA½Þ¨¨Ææ&‚´¾-‘Ö%â 3ï3$;^<‘i-ÖkMa‚xé‡àµ©^ÄpêcÕÄÊà"”M8÷äÑ\nj!n+YõêÎ à¾|!hù `—×Þé¬Ä)d #¤°ßyÍCeJžƒ~ý˜r%Ã’=Ÿ,}žÌ›š¡Œša7IDÛí¸¥ëpŒtÃÏA!E¬Ê©(ËÄäûè ´:¾œêõÙÜN@m^mRÉàv¸ç-qÅ]þ/ÆÒ–“Á`)›Ù >L6g ƒ"¦ ªrË‹A¢+² ïëHøª ˜›ìª*rØ> E‚¾^V†Ù=ßüµ¤kˆLrµt×@ðÓVþ ¶˜CÂCyõ¬UÇ?íøoÞMôß3ÅíÝ=ð ?…cgRªÍ)³]ÕÚŸ·ôÑP¤íªaÍq CÉúKfäPçySݨœ¿sèiïÿÇQà4&n;%–•WãpmªuÍkÃñ ¦¦®ï¬Ñ¿“ÐgàçlxÉfˆ*äÅ VhŒ£9± ÿ€ËAy&¶+3Ÿ, /©fØqøÏ´®€®Å¢eØ1‹”¤2AH*{ËÑý4óÀB÷#û7m N gù•¸[1-3,¼E—€\GÁ†—ø+^—ÀäWæ\Œn=`½ïTSŠúe™@3Eø«+oH‰ôJ\ ³-Vb]Î- 8(ÅÔ¬FJΨL\²ìFÕå)ývŠaކIøEºâjbÊQ‚™–¥Ž/´n̈KØ SúÒæeX- V?ÁÛ„ø~¡f<ÙñŠ5`Ï}Íld#Î#”üªb|U{B Ê%t]O0eòãÈ\q,TYx—[‰ Y1"W8»Cdßʽ+rgÜ咽Âþ¤oÙFõ/ÍO‰Ò ”®QôP ¨¹¥ª‘ŒtO¨2k³¯€§AÇf±ðßÖº,é—v ÊÖ<„m*zœúÇLï¶d5 ÛÖÁ±’íGilZZ“XîòËm»ý :ÅPÈ¿CHùZ7š!‹éÅÐá¤ë ¨ µµKw¿ø’ ïiýÞ"·©Æ‰’bкý©±‰¦”A%Э/úª‡.ª5ðg‡_šØú) ö“!í„e©T o’½DCkÚ¯žÝb”pnrˆvÐÀV·l¦ÝÁWOÉ=¨a c'k0hxšÓ6îŽß4]H¡Z‚ÓAryãdH…žåÆ#{O@h¿µg#ã£Æë€ÌÌóŒ~wì”ÜW­ãxŠ$fZœQŸ…Vñ•»²-O<Ši—j(]PBz[f¦ŠngĦáksìi.Æë~Е_2”® @0jäRÞ¬ÊJ ú­ ·W?uôÆ*ïÄÞ(ÑzaŒ¦€dbÇ!ÊŸT@f¯ZöÃdl"‡æâM¯&–Þ#ÂO¶Ï4¯¥¡JÑÓ)QûÃTnõïUMÌo¨r.qUW ÊX”kçj—2‘·´ˆËÖýíóç”R`dm N†ïŠ=Bj@×î’•yӌӀ­zí/ˆ{¢l)º–èJ»È¾™Âùš¿¶ºº!°–H­nÍ%¹òA-ž>µ¬whá"0\ f5³‹\íÍAY½T%ª'³.üÕ¼(q%¤¶9ƒiߣÀBpñ ß(î†v!zŦÆÒ4› Ókè÷dç&ƒäܓ˞gòO"L°Î}è?ˆ g¤Á¸2DÏö«ê¹Ú Ëù œP]WžŠ¸ÑcX—éãºîѧƦÙ#£* ±m¡—†qA4˜Ž^Hp?Á¯»©·¹‡!&fÏ| e7€¼"&ô 0bY=Œ³uÕDp0êZÐ…¡C°ïôgFuðh»GÏ ua^cÞkJ3®³×¡§ñ/e×4£b¾g^êD…AFßæo½Üú¦N:PÊDµuz±Ô!ºÃ dÔ@6Œ{¦óG³5½Q¸vz’Áy¹–õ\ü÷©êÅÜ›sŸt¾Tb 2tÁžg:W˜þªÏ·û7ì7I9êÅóeÏ÷Œ ‹AzýÈpE) 9“ÀšçêQÓÞÑŽèWí•G°Û"Qn}uqkFЖçë>ÕÁ›BlücžAy[Û›°[Ñi½ øµ¯äå¡"¼ÐãSt9MïÄ;M>ÿ oUúKnØÆÙ…³QuV¿ÈÎJ¡µýk·÷"lÑVô$|ØêjÙa6¬É¾*Üb#[]m2€¸Y Ç'½%­ÚF¾;w캞H[ý2"ÙïÙÈÄt]Þt›=t¸tÙôÑýÉcXùÂÛKÏ8‹£!›Q)¾¬“^rç…õå»[«ÇMݦZ)Ú!=f&ž[Æ8€Ç #(S¹µä'á]/¢T„ÝÐzÖ‰f+±¥éL¡œ!ôÒ:D{¤û†‰™²½U19Ý@jMîÑ5QSQghàxdLÙ_Ç—£Á.ƒ²Ko!®ü5Z? <ÙÛïH‚ŠhòK3÷=ðÊÇ ¦‹ü®j8)s²¿éU<ö%ã#ˆȨ̂e{w7¼É%µ5W]öÑK{[K’”d¦¯Á•V%ÞÉõá Wªb)¢-à7 ?Å,âÈa[Bcæ±FêU8Ø" äýW M`µÐwD¡ÂïÝ¡%“ß_Èzª£–@ œ¬¬ ƒ–רšä‘àYÎÚúâXHpO¸DC½(ƒT¶=+[Ùi#Ao¥ÿpoX§Î‡Ñb:-ùß?BzHÑpR”Æåþp—J×Dì¢â£NG™·ÌW¼’ÈÇáRÙhc³Dˆ ¡k`M-Ï–`Ë÷¹(DÓ^o¤F¬7Жס¿—âßÑ•—2 ng¯±;zJ#Zä•«Fü³¦¾.úáÏÍFYU±‘|È5Lo d!Ö`ðbr[3ßkq88>6™ÞEoY`G"†“DÖË6â,°K‰õ©ä'ã˜Ö‘yñ×dûlÜðà¸A¤¿æH|K.‹ìûTn‚åA~aï°—£âu›yK`ÊRM>Å[ŠÆˆ‰¦"žûiØ=žs$WÉ«…Ö‰”Õ5I0P˜F ½@uªþh ê„T¾jqÕSèCåûQ€˜™!Gž¶ŒÚK €¾ÐS~’RFñP|9‰E%þ;øQRT¦Dÿ÷Ô·¦@€`+uåI›´2ˆà ÿê­1ò¥aá·[ëƒJÉ2bJØ?EJ/fa%áŸDLÈš‡èßVAG˜…sˆ_~U=öCç_w3ÃÚ¤7 Ûà±'8 Q&g'4Û7 ™»Ôýk9åÒs6;w R±wbð¯§i¤vÊÑ6oa{ÛÖDPã‰+ ï¬ægGÌ;O˜Ð0|ZÑU@ÁÉÒÈ:ÉŸ¢[«à¸wfA3I•ñÿJ°§ÅZ‡„VÃ`6LÒõ¼\ê±Æõ{ö•óÕ¼€Uµ\ïI²ù~&¢3;Ÿ_œž\dŸOÏÇÚ7ç$;È>OŽÇÙ§ÉÇOÙåiv<$E€‘ ÈÒ¥±çè< ž½óY‹vZ &¦ÿ¢ÙM|سpbv³¬c´†QXA)BC{ëÞÄ$ ’L­ÈM…:˜Ú‘y2AV}†ì6Ö™Þ‚#–Õ}ÁÖhàðƒ¦˜õ\†ZâK”o ÷à¦.­StÒÞñÄZœŸö[Ç¥› 3 ÊäN¸Bš ¨ü…Š2\?Ê—Q嘂ˆP.6Eï}Ö'\{!Û”‹GOqÜW‹^8N ¼øÇuC¬k,L“6·?} xCƒÙ,$CÄW—>‹n„|“» "Ç)åö ¯  }3=þÛ u­—žf§OröÄkeª´lŒu×ÊФeÚÀÄ“uâƒoy„\(ŠQt;VFI¦öÁñ©ˆ9ánõß@†ºÔª™¦±2° z™‚C‡¡ U¡wYÝ€j:¥ÁKraαT -h²•â©l÷ŸˆÃåìÍ’]k/%OÊSçÞòý2ýš°B­¹ãÁ‚•‡þ5å¦sôë„¶ÏÎOÿ<¾°*¿U9'}züÍÏ %vPTO‹×µ7; ¸ ÄÀ@„œ]€hÜXse©ƒÑË™‡£ék®ÂÌEÃè×Ä”oH¿Œ{ø"ªç¯Ÿõ;Zû¶Öož„ùÚŸ#`CàŒ·<· x­ƒ!Š¡’&ã OÉýƒi…Â:spM•º!ñ1øsUñ+ånᣦ[£6úÝ]¦NJ]¬ö õï4‹ˆ;•6Rä ùT Çdª´J=‡NIm˜W_ 3ðJÓ;Ó_ìÚ2&(¨¢%¿Xµ·×&´ô¥¨À¥yÏZÝù;_É„(¼Ÿs“ù†X«¢ÉÙ8x¶øõªõÌíºz!z¶Õk‹l S{¨ÈÞyÎÀˆu¦µÓ¶Czu8ò¸³‰ŽÛcá1ˆ`Mªûz~—^²6°Õ(É»T$ Ö'7¶“{&‹÷ÕŒSƒ?íÓ¹†Ïkö”`×Úäºøù¼ÇºéC²uÌÊ¥°l\¢L€Kðž0Ê'4€ü°àSé@ÉÖ"\xKl¹Kᜰqt60?{ä°½|;b%` &¼ÚÖ¢‰´r4#¸"ílè´kb€÷¶¸hY®‹!ˆ(y¢ù„AõT5…–’LáØ¡¼E&ÁH”ï«+FèÀ“EOikÕÖu»”îÑÖ0iIŞȳ>t}ò¯Rçz[#1EfÄÍ.tö„¶díY]liPä4P7¼£¹¥¿–¶üÁʲLþa›Õ”´.±3ku±~²µP7Üxöoì\|Ó°íO"÷ËgÉ’IFÖO®ê‘™*Ð=IÏPùöIýPbȧ´€6W—+w›OÍ%ÞHáyÒÍ ÈÕÃÀª|=l…Æíɺ­´.< ËF"´<]æsÈjÈÖ²WŽ2×-3·hÁ" /W‹â+¥v?{¹·wq09úñ&awrz™]||ÞÛ{ÉÞb;ÆöGúïàº;Œ¦^/¼ (qaBÊOž¹Ë\ ´(b6çû'0[Šu?"6`xèQ¤/ª›.¾gĨUÂ5"‚ ÙqUç µÚååønÑÓ¦Î[]ýï4Ǻ^9î8ŠpM½Õ1ãì'§_³É¥º*Þ‹ŸâüËXˆ¶ 0­1g˜üy’}T§†âèžžŒGêÝø«ÿjò9»üzªC(°6Æa´Ä¢–3eÌ#õq#»Q*ƒí½ÔIàr9_~9?eÎO?ÛŒ§Š`=z9ЭØQ4'°jVúJÃ:š-ê²òô›X±Ö Xd?£ÃÕyÙ¯‰º'Ãgêap!Æûñõ £ì©ƒ#K¹ø0¹ôûIhL¤Ò-ÛˆH³¦ì]â{ˆD¥Nۊ܃™ÅŒç €(Ï“á,¹,ï:Ð…K‰ÖI‹æÌNT Ÿó¸j±P&ìÓËf\(%l±_¢;r{a«°âöwl±$ÇO7Œn!R]¡Ëǵrc¡Ñ0`·ã(¼˜Î O4r3‘n¤€»5†¦§¡GÑ{[äUÌ ¨bäÜ•z%ºÆÎ Â¼Š´4¬Ù*)Q÷€pw.Y„—PÛJ £’‘Ž;™]èn®¥µ¸h içYå7RÀPŠÞ»NèåY-eKnžííùtƉ™  …™i,,vIŒ)à,°-b«f Þ—Ñj´3­©OÞK¡QIt¶·Äàã ®?'é­KÓ‹>sˆäëÙp0¶·ÜÎf(6é=sIt‡úžöV×)ó„*DKÇpœÛòßû…G8žÝŒÉÊUq«<ÅvRâÐKÞ€jê¶HYÌÎ2 x6_óé _™to£!£…Þ~s$KŸ#6ƒ<Â@ü•‚²ÚØ="V‘Kx½Þø#8@´Ïµ ,׷ͱÜâÈÅØ°&r4=€x%fˆL#è SäãhíµJ°Ø9Ä#S•f_¬†"V0…ºÔ- ²£’Ûr=€½Ž/GîØ¦Ý”ÌͶåê0œO•Ò>m$sÙHcs&°¯Á<}S”h]´…fw;÷€MT°¨‰a% ¿1U:°T¾G+duöjC°1‘|yëWÂØ|“_.¶w+l Ò.€Œ“ qЬ,§ö—_ä¿3&|TÚ(,PíÑ*­ BT? ˆ:âÝ‘|^e ˽9lá#OÓÖHú+J]ÑOºnÚÊ4}ž„¤•©ºúJVãe½V«?²1Џ–%O+"rùÄ#&;da¤Xqœïˆ½j2’á¨B³ðÏ_/3°V|ÖÝ6+@ž†ýº˜n/ˆï#yBÌ ÙÓÚöë 2×"•‘FÅ'ؘŒ­GJhí¯Ý¢Wr\5u%`êÁ¹ã=…W@ªÏ9²Hê–Ÿ–ÿ°Ì¿Ø9ǃ/õ>†µËçÂzp5¹2[8¶q=øRÙáb +óäÛïGÁÈ‘¨CPwŒxU€yçÚ)j4ß6Q»âisg~QwüÍѸ£R l[ý4sHûåI‰ìa¢_(×ø×3£ahÂÈ,7–8ÉËÑú:ñ6õÍÞU~µDä‰×UšÝnÞ™Ò"²k¦ÊPYDTèHW›·æñôb.žx¢øÀCd8%SôeVhè{Há rè7>Þ©ÜègŒ”ß* µ‰»ò¼á¸Š†|ºZ0‡cåWÚ×Ú3<; ^ cÍg¦x<£P3ܹêƒFö¿‚íiwÑ_wA )«p¨Å„PŒ,.6CÜø6o,[hj.oÇ.¹?ºoY±N 5 ªi’¦–»T…=‰ä´ r#þ–V&à}cã]ß^Ô’µ¦²­b±'Ôp½¸º;̵•çÔ”°÷{:ôë ¡ ì£^G=-néæ3z{=Ewø’‘vB c¤U]¶rjéê_á…ë™ìrI–v^LU Ü©lçÌ«Xåw{뼑ÒÍè=‡ ÅÛ`T ·ëó.s “}Ö=(ï æý×ñ8G]€ÒGÃåÊE»ùІE†èŒðß”­ºÈhý&¸*HØ=´ÁÖ[Ñr7j –A©ÒÁAH%'<"Û¤Œ×ˤi Â"[åå:îSÓ‹þ¹Ü¢5‡8®¦oû“ˆŽ/C¸aˆáqŒÜ¾Ú8††5TGÏäÜdñU˜ÐönïÔ3œo„Ñ ²fé¡Ñ"dæfó’:„®4^°üäfûM=°b$Åc"iŃˆÙ MíA¸ãÒ‚d«—¤÷…ÖkågÔ–©é¹`ï{órêÚ€¬;ðX-ñ¤ùŒOÄÿۮΚZ[UdW"'e¶›–yíuMÖIj°Ðâ¸xw˜7 û¥þìg?Ó9¬ÅÑäBc^f¿\ŽOÞÏ?fcéùGq•I#¤i…yôçñ‰xÌ$:9‘Þ§'Gãócí)Ùx:â%D¨)ÇäqT¼7oö—RdP7úoÿ.¾àÈ.dÂëëRÃílÿ>W¦°„ÂZ6§¨zÔ»].oþ󇇇ýh_’øþæçoÞüœ¤ü²˜b6¤°›OÜSŠ{Ì)JŸqÕzŸ—»çKh ì 'x(×E/HMõÎk‹Õ•œØˆÀ5Y4¢ÑÅÌêv›©ª‘‹«÷F]<_ªR×x±„4¿®k<g­MÀ^LøA‰@ÁXÍ+88^ìª_ߥ: µÈ0¹Ç(o¨{ènѶ¥·Ïeaáž:Ëiêu>_–_Î.…œÝIXú"4>§êÎ-çúèÁûKGû¥µ†kÄ΄RÀ4A}횬̅9“È/úOaoy—šGßÔ‚Ðh º\Ãпæú-@q,Œ Ë™¢*}?ó)„›9ÐÉ\Îýìºè|œÝSˆž‹>GÓ« TcU°þÞ°Œ-åH?ÂÞ Ù—3Y¬dâãLk áKã ¡äHJ›B›» “”•žÅ+¬m-3°U–Ý#[ñœÎzc™»¡5;AÖ¨óª²{/?Ýóvê£Ràqðn¼_¡sKȯ¶Pí{ßCÍhº­:éTvï]žñZìß“«à”AÃp|~ŠÃ•ˆoyj’ƒ“¿ /;ùˆ½¦/ÏI.³¼FX7|ÍhDH¨8ӏޓ\×ʺõÙD2øyëÅåÁùåÛLZ gÊBO.Æd•ÂÞñûþeôOŠ{ÿL>åçcºùÍ÷æªG“‹ËóÉ{ùRvzž}¹ã“Ïÿˆáë,¹ñCï„»žégïê çšw0v…ÏôžDŠ9Óë&y–¸;ø0¢nc ü.¼û«Q>*ÿâ¶Ñ¤¨—ý‡÷r×b;ù p™k»ØþUêK2°DÕ½Ó¸ûc;ÖìU~Ÿ—s5'¸J¯ºQÕ\3Ô,w¹’Ê¿»~êÅ5¨õf?dt¸Ä¸{O²0 bn[G³ ã¾_ÃædýAć>kPÅ×r©×sŒˆE‚Tw„ýkpË>Õƒ…c»ˆ°£²IR%½`h¹–{±y`?ßH’]›Cst6ì.3ngž_•Sx·alõæŽ!°=/ÁDši5õt\Û¢ŠX¢^pÏgÊ•²ò²4j[{ý¾ì…•üÎ cøõ•z#1?$§NR7 bÛb[qMŸšË"Ë•ÐDå¿R þiªäk_$ÙlUEñŽ·ÂEy™Æv–VPs[ßÅÆÀ% Ôl%^.¡Ì”/nå Œ÷³o×ö½œæËI_>.Gõô—ºHØÆHw$ŪžâyÕ#ØÕšu÷@·$Ø·ÈDZ!8OIÒƒ¼ 774¬i ‡æy×G¨«ÒŒjU_H#˜óĺ.”Kw)~hµ›[Kž×à ¶èÞzŸuëZ×aéô)z;ÞBÅ{sy¢›Q³$nü#’hØ:³í3½X¶M¿\‡¶ÝþpÂáey#*A#FØ}WAÎ^½L5æ—ê¨ËÎ>|عþ#)çø{Oûf½¦ Çg‘›±b òMp_É®Þ脦û'†×c䤉:3¨¾ƒq"UhkhymÏ…lFçVØÅk¼¶þ'áœ)ÈŒP›åf¸hôŠã‘€½l4ùQ¢/lTCþe17C5ZšT¡XúÍ¿’óZ[H½g-¢õèu1|ßÔª‚΂®Y¶bÞnëEÈD˜‡ÖM¢¡Fþ´J…ë÷¾w)餟™¯Ù>qømð‚«(U&¯ˆçîï¼Q*wCCÃ<'_º]Ò–± FM¯'˜¶æ“‡RÈ{×,‚¨žÔ¼µ³ÿ®º=8ÒȦ-™)m#Œ ͳunqäÜ"žó<ÀÊÃÁÜQÞþŒ—?Ÿ£¥˜K@jì³~lô:LÇ/+ hÄg*,#)—©ÜÇ­J–âŽN¼d.ƒXd+£Ð8çà›>lÐ)`Qe…”½Ò\ UÔüqN”½" õ8 ]*K¢ýÖWø|^À>cñµa«/ôÄàM»øÛ.¦›;YLy%Ò}_Èq‚J?M ÔbŠ3¸ ÿF Ç"û¨úí¨+øVŒÀ7'TiÍ?D·1Þ4. Ž iÝ"k£­•”áQA5­. 6ÎÀ[iŠ=ä/ÍØêÛ°}ņgYzsX:ÂcG]Ó5ë¹ByAe¡£†Nå†öKYö‘)þ«J—iþUßäß\&†TvIqéØ4l„˜‰Mi- D6‚G–x»ìed:I…Á%l¶ó}lGɲ‘,¹2êðÛ`üì눛èùæzŠ€KÈcÎN­ºÁSî–7.—Å¡î¨ ÁœÝ ÅÓêö†R¿JäZ%‚Æ^ÊuyŒ¡aI]ãÉwž2òNóȈ¬êJ݃¢/åsÆUãGœ€K<çÒ8÷©¡µôCß®ÕÁ(Pfø†¬”MòèÂ0VÝ¡xQ,œ^À'´nŠÆ ®œlX­î—g¨´ùs^ÉニÃÉäe¨nö Uƒ©13Y¥>Î ¬Ó¿?²˜âJágÄÝó»BkJ‹‘«0Á›dŸŽ '«Ó !{‡5ù~3 R™Ý.cx°²žéä†Cw=þÞsxzR°J'­LK¤ *Žôª–7¥®ìIÊaùÉ(¯ËF*•Q›õkòlæ“·Œ¸Ógµ¹ø¿&‹ Þ¸N¦|nÛKà¾4˜@~Ü—zp©tF¨0N[^W6•­¿‰+ÜD\ýD§éXXñ7`y‘¦†{¦«ÚÃz²žÜãÇPºtê_¼þé »©[|[JY°m×:!(<äjʤÓNYÒ™¢ ê?î a¼SÊÝ\Ãÿ¬ãKP.¯ ×+ÿ¡hiÀ)º{ë£Ôì‰(~`<­0¼lÅG¹ÍÝ(ÇXÓZ/x„Ù7†BÙÆÑcRðmÎP‡Úè@qôaXmµÈ½ÊJǯ^#c%ìV´sFǧ0…^>„pú…êÌ„;¥¸AÕÔTlŸÙõhŠq-‚ÙñxM;°P¦˜°pj¾¦HÉ[O‡¯ü„!CA¦oöçË ­²P•^)Ë`äï—‚>©À‹ð3¢ôñ£>¡¸¶ßxJ/ùn:2 ÜÍ8ôj܉¡³Æ‘!J^.Ǿ¥ëåáZð5W8 ù|±'ÖÛ|—§ýŸv=³ä„…ãÒµ?'=P®±¨¾‡éiŒ!¨a€KáÖñ~à ¹‰e‘;Nnñ», ßEú Cs‰\Ü®[ÍÈT#ZùôÔ\®ƒ‘¥0))8 dPÌeC<Ázö{ǦjŒ(Ê }¸)ÿ°ÿÝhðl}²8a=áGc“¼ ª±å Ud3]]YÅ=´éË–P~²qÏÜ(¦^2Á;ñ[»>LÜŒ.åc×½ =÷£ï®†Ãœç›&!×´×£ƒÏ%u/(=_/ÏuóH?žª 8G~)ûÄ ƒhé±ïÔœ~Ö›M^èSöxšnN¸Ö‰~þÜ´ª”)£Ò¡Óµ\›y)¹7ZÅ@y0Ûp;‚9‰—L[²íšëPû´ëºªÍH`;øx>£À?~Ò’ÿ“SIvû<>šÈ‰H¦[v2þx<ù8>9`Mw;¼£mrð~r<Ñ£|/™q‡Ÿôy¼š ¿=<=¹`àò9Sr …$ØÑÙéÅÅÄh¨¿ºørøéÇ?²ïnÞÎw·s.ð“—úÉóñÙñÁ!ö"SËd›Ù¥-µ}Ö¡p ù¼B“HµKÇZyhUø3b™¥Ä—a*Ыj ‡Ð‡Ê¥*²­jéb‡Ç>ª „F®S'ùv~ •À¼Ûþê2Ä;•Õ`ôØ·ó«E™AùW™§_Ò†ÆðŸ'·µAX€Ø²A~e@-Ôƒ™—ÅJÈ©2Úr«C¤ˆ´>Ëþ:…Û„vˆÆ$ès¨k˜Ô™©Í& l-o|%N|xêtåY,×ÂÇÄ%¦EÐ>H¹oÊ?«iܪP»èUÈ ó6ŽjVþ൨&w|3pˆoIŠdHR\óD–pzjSr¦ƒ²$\©áK!}w.ö&;Â\.?)=•'.lÁ9¢pRy¹`O~2Vx&åú~'ŸÏŽ'cù°ã/Gš3-Ì ŠOÿòt{$‘›‡ŸôG2s…»8‘!ÁÃd"aOçÂb‚—¨g­ÅÛ˜ºˆT2ŠëŠÅ²í¬Ô DtòF445Üÿ•E1#0O÷ÛËõÆ¥“,Å‘¦bºôÛ<‚!…]T“•¦K޹6”ø…êô»¦vÇŽ¼wU8fµx>LcC2ƒC½ýPÞ­î_)¬…å2zÁŸ"Y¢ñÅÍÍçåT²…«B}×9ÌJ>\F†b±dœØ}í©ŸhÐkèübËãFx`r""òDïlÀp ”•<½J´w¯È\}ªÈJ„B4²ÀÚ?Ú XmúuôvxÑ·¦õ¦Ö9cŠ MBñ #f;ÚD!Mͱ0 ï}®p|öÚd1eïÔ po½ìYJbÛ#ï@‘k‡œãŠ”ò *ç‡Ç/œ|¨¾€Rî.ÓZ-·’²ÛÛ HX7:¹à~uµkn};P¯¹¶ŸtÆ00ÚTHDךÝåG]vj¯®w±3¤° ¬~+…?Së¡ïÉCågQ‡÷ñÈ´Ús]ÕÕZ{!ÙaµEB,x•†n¥-Æ:Éáµ³¿X©n0Éö¨iB)b_q k³åÙi±õt:/ö`ž_§xˆï![.‘,KÆP˜€H ¹‹ÊPyxMa-dRƒ>œžîɰ,l–O>HÿêÖ0fFû?Ç×½ì2¯ä=EÚeh4Ý©"¾-…»°£+íy¢T3 ÕAp ª·ú±È…òÞÔùÜŸPÕ®&©<êè&? ZÚÛ—{Jg”—Ñ»d!p7î™D=^¿ùf«s² r„2ýjÕȆÌÈ5²liØXËðn@z|¦»Ö}T©ƒ] Mûí~)Ï·nä /Rz]TñGùÀþ Ûì;|ì—6DÊõ]äêâ[v @Ò't¦¡Ð­Çý]ú¶¿e7‰'LÇüöÜs•$í¶À–ô÷¯ßWzõÝnVˆ´È7ñÙF®½}E¶W‚ê4¥¡¹žv÷™; æ¤ÿdd+™Sí©˜j0Ež³N&A’yLTA®Ê¹&k¯”âͽ8ä{ —Ú6KLfuÐê—›ë tÇ•×N‘ úºÉ_ýrïW¿øÅ›ïùܤÍbŸ†&™;Ý «Í¡ý|¦Ì%/5^çÞH²Hæ0 %u?ém ,zZ@,SecˆBÒÛU±²/ûûÜ1ˆÎäøÆ´­¿ÅÛ&ª¯ËE>÷v—²åÈ(^f¿üþû_gŸµPvÔH— äC^6Š^ÕŠcá£ì·¿ýõ›_ìÃɇ ù£^S¸e`”Ò0É –ŒœH!?ØÄ Î#ÚÊô׆óøÍëßf'P‰ß|ÿúuöqû ÍÙ±*«‡Ð8Ä`øÍ/ß¼ù•Èíß¼~³›}ÿÛ_í½ùÍo~=Ê” Ê­â$¿Ç(f’þ-»vÿ22U°ÕOYÿ(,*ý"Mi›ÞL‡ÀhØj4ƒ7¤WÌ{FZD,*¦eu9Žè£Ô+i3!©i:)®’9TŒóll .÷6ý©M¨êæªÒ/RžôËAžt$kú.ü£ºÈ´P»A”ébéV‰w/CÐ †j!Üh¬Lð¤@x`KÍhæÖHfß•L_ëðbcÜ]mpr~1ÀI ÅÔwΔ–u¯ZŒòSɰzs8-@WûIé{peL3móz6ùæË’“$Á›¶ ÀT7œk@¦´pïG‡zã µ ‡x“TzǬ[Ъ(p» -kµˆ¼Î6™^öuS}$Ê­¼öe²»`y2†¡jHÙÇ7èæ¢/82esþî°z(ZÆÑé üÁžðX4ììIÑš|º,“¸n§«é<Ì”$kŽÅâ]‚Ûû¼vy:6‘;‘Ê­ÞÔ'¿Œ&¤DÍÈeñôÈÛòª4&ˆ£véE¿èSùHŶ¢¯?ÖMËn–\“_.dXÍá¯5~ÚF’ ®¾i$Tƒbô\uR c©¬^ûbŠ)dc žôgÃÔ¶;–„ðõ-Ua6'Ü,fˆ aË@Í ´×æšYÞ>äw E|7>¡sϬë”7ù@•Î#:æWS_ëSÔvÄ”±.Ðß•ðŽ™~‚E%´!AðªƒÇ2kxrÁè°žvÄ:&Ô¯hDù9N7lž{FØ ~›õT7ûX*ᾪøDÒ@Ò=³Ô%ÚLHP7 qgräÎ ¶”™„ËúÉX7¸“’Ì|Éóò R´u@)ÄL£N±“ÎÝí´¿ë—HÁ•6~ 3´ñ5sœ Ï׺:xЫb© J¬mÊcfXÜs@’Íä÷´m÷z(PVÁö‰MÀO ý:Y¤%X™ô>ê’âe0U1 $³ú£Ý®ÀÄX%5¤U¡b[DÕÜK¹ö´î‘4ïH\“(ÄÕqj|óÂê¿ÁœUkYâ•~¥¢h[»u0@läìcf¹ Ü Ï*†ž„bIQ ì›Ô•§ZÝð]u\5°éNT¬'mT¡Ø@XÙƒIÍ‹ˆ:êÄ…gÌ?$‹lËOø 4yýxëvFË ÷Åqð:ÿPK?ý¸ÖFc~L¡à^Ž $ alice.txt Ðl®‚/­ÐîD¥‚/­Ðð<˜‚/­ÐPK[ÈàPyro5-5.15/examples/distributed-computing2/client.py000066400000000000000000000017501451404116400225420ustar00rootroot00000000000000import zipfile import time from collections import Counter from Pyro5.api import Proxy def wordfreq(book, counter_uri): begin = time.time() with Proxy(counter_uri) as counter: totals = counter.count(book) totals = Counter(totals) time_taken = round(time.time()-begin, 2) print("Top five words:") for word, counts in totals.most_common(5): print(" %s (%d)" % (word, counts)) print("Time taken:", time_taken, "sec.") if __name__ == "__main__": book = zipfile.ZipFile("alice.zip").open("alice.txt", "r").read().decode("utf-8") book = book.splitlines() print("(book text consists of %d lines total)" % len(book)) print("(artificial delays are used to dramatize the differences in execution time)") print("\nCounting the words using a single counter...") wordfreq(book, "PYRONAME:example.dc2.wordcount.1") print("\nCounting words using multiple parallel counters...") wordfreq(book, "PYRONAME:example.dc2.dispatcher") Pyro5-5.15/examples/distributed-computing2/servers.py000066400000000000000000000066031451404116400227570ustar00rootroot00000000000000import string import time from collections import Counter from itertools import cycle, zip_longest from concurrent import futures from Pyro5.api import expose, serve, locate_ns, Proxy, config import Pyro5.errors class WordCounter(object): filter_words = {'a', 'an', 'at', 'the', 'i', 'he', 'she', 's', 'but', 'was', 'has', 'had', 'have', 'and', 'are', 'as', 'be', 'by', 'for', 'if', 'in', 'is', 'it', 'of', 'or', 'that', 'the', 'to', 'with', 'his', 'all', 'any', 'this', 'that', 'not', 'from', 'on', 'me', 'him', 'her', 'their', 'so', 'you', 'there', 'now', 'then', 'no', 'yes', 'one', 'were', 'they', 'them', 'which', 'what', 'when', 'who', 'how', 'where', 'some', 'my', 'into', 'up', 'out', 'some', 'we', 'us', 't', 'do'} trans_punc = {ord(punc): u' ' for punc in string.punctuation} @expose def count(self, lines): counts = Counter() for num, line in enumerate(lines): if line: line = line.translate(self.trans_punc).lower() interesting_words = [w for w in line.split() if w.isalpha() and w not in self.filter_words] counts.update(interesting_words) if num % 10 == 0: time.sleep(0.01) # artificial delay to show execution time differences (and make this not cpu-bound) return counts def grouper(n, iterable, padvalue=None): """grouper(3, 'abcdefg', 'x') --> ('a','b','c'), ('d','e','f'), ('g','x','x')""" return zip_longest(*[iter(iterable)]*n, fillvalue=padvalue) class Dispatcher(object): def count_chunk(self, counter, chunk): with Proxy(counter) as c: return c.count(chunk) @expose def count(self, lines): # use the name server's prefix lookup to get all registered wordcounters with locate_ns() as ns: all_counters = ns.list(prefix="example.dc2.wordcount.") # chop the text into chunks that can be distributed across the workers # uses futures so that it runs the counts in parallel # counter is selected in a round-robin fashion from list of all available counters with futures.ThreadPoolExecutor() as pool: roundrobin_counters = cycle(all_counters.values()) tasks = [] for chunk in grouper(200, lines): tasks.append(pool.submit(self.count_chunk, next(roundrobin_counters), chunk)) # gather the results print("Collecting %d results (counted in parallel)..." % len(tasks)) totals = Counter() for task in futures.as_completed(tasks): try: totals.update(task.result()) except Pyro5.errors.CommunicationError as x: raise Pyro5.errors.PyroError("Something went wrong in the server when collecting the responses: "+str(x)) return totals if __name__ == "__main__": print("Spinning up 5 word counters, and 1 dispatcher.") config.SERVERTYPE = "thread" serve( { WordCounter(): "example.dc2.wordcount.1", WordCounter(): "example.dc2.wordcount.2", WordCounter(): "example.dc2.wordcount.3", WordCounter(): "example.dc2.wordcount.4", WordCounter(): "example.dc2.wordcount.5", Dispatcher: "example.dc2.dispatcher" }, verbose=False ) Pyro5-5.15/examples/distributed-computing3/000077500000000000000000000000001451404116400207105ustar00rootroot00000000000000Pyro5-5.15/examples/distributed-computing3/Readme.txt000066400000000000000000000013571451404116400226540ustar00rootroot00000000000000A simple distributed computing example where many client jobs are automatically distributed over a pool of workers. The load distribution is done by not connecting to a particular pyro object, or using a dispatcher service, but it is simply using the yellow-pages function (metadata lookup) to find one randomly chosen object that has the required metadata tag. It's pretty simple but is also a bit dumb; it doesn't know if the chosen worker is idle or busy with another client's request. Also it doesn't deal with a worker that crashed or is unreachable. Optimizing these things is an excercise left for the reader. *** Starting up *** - We're using a Name Server, so start one. - start one or more workers (the more the merrier) - run the client Pyro5-5.15/examples/distributed-computing3/client.py000066400000000000000000000010161451404116400225360ustar00rootroot00000000000000import random from Pyro5.api import Proxy for _ in range(100): # this submits 100 factorization requests to a random available pyro server that can factorize. # we do this in sequence but you can imagine that a whole pool of clients is submitting work in parallel. with Proxy("PYROMETA:example3.worker.factorizer") as w: n = number = random.randint(3211, 12000) * random.randint(4567, 21000) result = w.factorize(n) print("%s factorized %d: %s" % (w._pyroConnection.objectId, n, result)) Pyro5-5.15/examples/distributed-computing3/worker.py000066400000000000000000000024431451404116400225760ustar00rootroot00000000000000import os import socket from math import sqrt from Pyro5.api import expose, Daemon, locate_ns import Pyro5.socketutil class Worker(object): @expose def factorize(self, n): print("factorize request received for", n) result = self._factorize(n) print(" -->", result) return result def _factorize(self, n): """simple algorithm to find the prime factorials of the given number n""" def isPrime(n): return not any(x for x in range(2, int(sqrt(n)) + 1) if n % x == 0) primes = [] candidates = range(2, n + 1) candidate = 2 while not primes and candidate in candidates: if n % candidate == 0 and isPrime(candidate): primes = primes + [candidate] + self._factorize(n // candidate) candidate += 1 return primes with Daemon(host=Pyro5.socketutil.get_ip_address(None)) as daemon: # create a unique name for this worker (otherwise it overwrites other workers in the name server) worker_name = "Worker_%d@%s" % (os.getpid(), socket.gethostname()) print("Starting up worker", worker_name) uri = daemon.register(Worker) with locate_ns() as ns: ns.register(worker_name, uri, metadata={"example3.worker.factorizer"}) daemon.requestLoop() Pyro5-5.15/examples/distributed-mandelbrot/000077500000000000000000000000001451404116400207475ustar00rootroot00000000000000Pyro5-5.15/examples/distributed-mandelbrot/Readme.txt000066400000000000000000000033361451404116400227120ustar00rootroot00000000000000These examples are about calculating the Mandelbrot fractal set (z=z^2+c). NOTE: use the "launch_servers.sh" shell script to launch the name server and a reasonable number of Pyro mandelbrot server processes. First, a few notes: - The ascii animation runs at 100x40 resolution so make sure your console window is large enough. - The maximum iteration count is set to a quite high value to make the calculations more time consuming. If you want you can change both maxiter values in server.py down to something more reasonable such as 256. - try using Pypy instead of CPython to improve the speed dramatically The 'normal' code simply runs the calculation in a single Python process. It calculates every frame of the animation in one go and prints it to the screen. The 'client_asciizoom' program uses Pyro to offload the calculations to whatever mandelbrot server processes that are available. It discovers the available servers by using the metadata in the name server. To distribute the load evenly, it hands out the calculation of a single line in the frame to each server in a cyclic sequence. It uses Pyro batch calls to cluster these calls again to avoid having to do hundreds of remote calls per second, instead it will just call every server once per frame. The calls will return a bunch of resulting lines that are merged into the final animation frame, which is then printed to the screen. The graphics version is interesting too because it actually creates a nice picture! On my 8c/16t cpu the speedup of the distributed calculation of the graphical picture is massive. The normal single core version takes 22 seconds, while the distributed version only takes 2.6 seconds (and utilizes all cores of the cpu for nearly 100%). Pyro5-5.15/examples/distributed-mandelbrot/client_asciizoom.py000066400000000000000000000047351451404116400246650ustar00rootroot00000000000000# ascii animation of zooming a mandelbrot fractal, z=z^2+c import os import time import platform from concurrent import futures from Pyro5.api import locate_ns, Proxy, BatchProxy class MandelZoomer(object): res_x = 100 res_y = 40 def __init__(self): self.result = [] with locate_ns() as ns: mandels = ns.yplookup(meta_any={"class:mandelbrot_calc"}) self.mandels = [uri for _, (uri, meta) in mandels.items()] print("{0} mandelbrot calculation servers found.".format(len(self.mandels))) if not mandels: raise ValueError("launch at least one mandelbrot calculation server before starting this") time.sleep(2) def screen(self, start, width): dr = width / self.res_x di = dr*(self.res_x/self.res_y) di *= 0.8 # aspect ratio correction self.result = ["?"] * self.res_y servers = [BatchProxy(Proxy(uri)) for uri in self.mandels] with futures.ThreadPoolExecutor(max_workers=len(servers)*2) as pool: for i in range(self.res_y): server = servers[i % len(servers)] server.calc_line(start, self.res_x, i*di, dr, i) tasks = [pool.submit(server) for server in servers] for task in futures.as_completed(tasks): lines = task.result() for (linenr, line) in lines: self.result[linenr] = line return "\n".join(self.result) def cls(self): if platform.platform().startswith("Windows"): os.system("cls") else: print(chr(27)+"[2J"+chr(27)+"[1;1H", end="") # ansi clear screen if __name__ == "__main__": start = -2.0-1.0j width = 3.0 duration = 30.0 wallclock_start = time.time() frames = 0 zoomer = MandelZoomer() zoomer.cls() print("This is a mandelbrot zoom animation running using Pyro, it will use all calculation server processes that are available.") while True: time_passed = time.time() - wallclock_start if time_passed >= duration: break actual_width = width * (1-time_passed/duration/1.1) actual_start = start + (0.06-0.002j)*time_passed frame = zoomer.screen(actual_start, actual_width) zoomer.cls() fps = frames/time_passed if time_passed > 0 else 0 print("%.1f FPS time=%.2f width=%.2f" % (fps, time_passed, actual_width)) print(frame) frames += 1 print("Final FPS: %.2f" % fps) Pyro5-5.15/examples/distributed-mandelbrot/client_graphics.py000066400000000000000000000036641451404116400244700ustar00rootroot00000000000000# mandelbrot fractal, z=z^2+c import time import tkinter from concurrent import futures from Pyro5.api import Proxy, locate_ns res_x = 1000 res_y = 800 class MandelWindow(object): def __init__(self): self.root = tkinter.Tk() self.root.title("Mandelbrot (Pyro multi CPU core version)") canvas = tkinter.Canvas(self.root, width=res_x, height=res_y, bg="#000000") canvas.pack() self.img = tkinter.PhotoImage(width=res_x, height=res_y) canvas.create_image((res_x/2, res_y/2), image=self.img, state="normal") with locate_ns() as ns: mandels = ns.yplookup(meta_any={"class:mandelbrot_calc_color"}) mandels = list(mandels.items()) print("{0} mandelbrot calculation servers found.".format(len(mandels))) if not mandels: raise ValueError("launch at least one mandelbrot calculation server before starting this") self.mandels = [uri for _, (uri, meta) in mandels] self.pool = futures.ThreadPoolExecutor(max_workers=len(self.mandels)) self.tasks = [] self.start_time = time.time() for line in range(res_y): self.tasks.append(self.calc_new_line(line)) self.root.after(100, self.draw_results) tkinter.mainloop() def draw_results(self): for task in futures.as_completed(self.tasks): y, pixeldata = task.result() self.img.put(pixeldata, (0, y)) self.root.update() duration = time.time() - self.start_time print("Calculation took: %.2f seconds" % duration) def calc_new_line(self, y): def line_task(server_uri, y): with Proxy(server_uri) as calcproxy: return calcproxy.calc_photoimage_line(y, res_x, res_y) uri = self.mandels[y % len(self.mandels)] # round robin server selection return self.pool.submit(line_task, uri, y) if __name__ == "__main__": window = MandelWindow() Pyro5-5.15/examples/distributed-mandelbrot/launch_servers.sh000077500000000000000000000004431451404116400243320ustar00rootroot00000000000000#!/usr/bin/env bash python -m Pyro5.nameserver & sleep 0.5 NUM_CPUS=$(python -c "import os; print(os.cpu_count())") echo "Launching ${NUM_CPUS} mandelbrot server processes..." for id in $(seq 1 ${NUM_CPUS}) do python server.py ${id} & done sleep 1 echo "" echo "Now start a client." Pyro5-5.15/examples/distributed-mandelbrot/normal.py000066400000000000000000000025631451404116400226170ustar00rootroot00000000000000# ascii animation of zooming a mandelbrot fractal, z=z^2+c import os import time import platform from server import Mandelbrot res_x = 100 res_y = 40 def screen(start, width): mandel = Mandelbrot() dr = width / res_x di = dr*(res_x/res_y) di *= 0.8 # aspect ratio correction lines = mandel.calc_lines(start, res_x, dr, di, 0, res_y) return "\n".join(x[1] for x in lines) def cls(): if platform.platform().startswith("Windows"): os.system("cls") else: print(chr(27)+"[2J"+chr(27)+"[1;1H", end="") # ansi clear screen def zoom(): start = -2.0-1.0j width = 3.0 duration = 30.0 wallclock_start = time.time() frames = 0 fps = 0 cls() print("This is a mandelbrot zoom animation running without Pyro, in a single Python process.") time.sleep(2) while True: time_passed = time.time() - wallclock_start if time_passed >= duration: break actual_width = width * (1-time_passed/duration/1.1) actual_start = start + (0.06-0.002j)*time_passed frame = screen(actual_start, actual_width) cls() fps = frames/time_passed if time_passed > 0 else 0 print("%.1f FPS time=%.2f width=%.2f" % (fps, time_passed, actual_width)) print(frame) frames += 1 print("Final FPS: %.2f" % fps) if __name__ == "__main__": zoom() Pyro5-5.15/examples/distributed-mandelbrot/normal_graphics.py000066400000000000000000000021151451404116400244700ustar00rootroot00000000000000# mandelbrot fractal, z=z^2+c import time import tkinter from server import MandelbrotColorPixels res_x = 1000 res_y = 800 class MandelWindow(object): def __init__(self): self.root = tkinter.Tk() self.root.title("Mandelbrot (Single CPU core)") canvas = tkinter.Canvas(self.root, width=res_x, height=res_y, bg="#000000") canvas.pack() self.img = tkinter.PhotoImage(width=res_x, height=res_y) canvas.create_image((res_x/2, res_y/2), image=self.img, state="normal") self.mandel = MandelbrotColorPixels() self.start_time = time.time() self.root.after(1000, lambda: self.draw_line(0)) tkinter.mainloop() def draw_line(self, y): _, pixeldata = self.mandel.calc_photoimage_line(y, res_x, res_y) self.img.put(pixeldata, (0, y)) if y < res_y: self.root.after_idle(lambda: self.draw_line(y+1)) else: duration = time.time() - self.start_time print("Calculation took: %.2f seconds" % duration) if __name__ == "__main__": window = MandelWindow() Pyro5-5.15/examples/distributed-mandelbrot/server.py000066400000000000000000000052641451404116400226360ustar00rootroot00000000000000from Pyro5.api import expose, Daemon, locate_ns import sys @expose class Mandelbrot(object): maxiters = 500 def calc_line(self, start, res_x, ii, dr, line_nr): line = "" z = start + complex(0, ii) for r in range(res_x): z += complex(dr, 0) iters = self.iterations(z) line += " " if iters >= self.maxiters else chr(iters % 64 + 32) return line_nr, line def calc_lines(self, start, res_x, dr, di, start_line_nr, num_lines): lines = [] for i in range(num_lines): line = "" for r in range(res_x): z = start + complex(r*dr, i*di) iters = self.iterations(z) line += " " if iters >= self.maxiters else chr(iters % 64 + 32) lines.append((i+start_line_nr, line)) return lines def iterations(self, z): c = z for n in range(self.maxiters): if abs(z) > 2: return n z = z*z + c return self.maxiters @expose class MandelbrotColorPixels(object): maxiters = 500 def calc_photoimage_line(self, y, res_x, res_y): line = [] for x in range(res_x): rgb = self.mandel_iterate(x, y, res_x, res_y) line.append(rgb) # tailored response for easy drawing into a tkinter PhotoImage: return y, "{"+" ".join("#%02x%02x%02x" % rgb for rgb in line)+"}" def mandel_iterate(self, x, y, res_x, res_y): zr = (x/res_x - 0.5) * 1 - 0.3 zi = (y/res_y - 0.5) * 1 - 0.9 zi *= res_y/res_x # aspect correction z = complex(zr, zi) c = z iters = 0 for iters in range(self.maxiters+1): if abs(z) > 2: break z = z*z + c if iters >= self.maxiters: return 0, 0, 0 r = (iters+32) % 255 g = iters % 255 b = (iters+40) % 255 return int(r), int(g), int(b) if __name__ == "__main__": # spawn a Pyro daemon process # (can't use threads, because of the GIL) if len(sys.argv) != 2: raise SystemExit("give argument: server_id number") server_id = int(sys.argv[1]) with Daemon() as d: with locate_ns() as ns: mandel_server = d.register(Mandelbrot) mandel_color_server = d.register(MandelbrotColorPixels) ns.register("mandelbrot_"+str(server_id), mandel_server, safe=True, metadata={"class:mandelbrot_calc"}) ns.register("mandelbrot_color_"+str(server_id), mandel_color_server, safe=True, metadata={"class:mandelbrot_calc_color"}) print("Mandelbrot calculation server #{} ready.".format(server_id)) d.requestLoop() Pyro5-5.15/examples/echoserver/000077500000000000000000000000001451404116400164455ustar00rootroot00000000000000Pyro5-5.15/examples/echoserver/Readme.txt000077500000000000000000000003501451404116400204040ustar00rootroot00000000000000Shows how you might use the built-in test echo server. So, this example only contains some client code. You are supposed to start the echo server with something like: $ python -m Pyro5.utils.echoserver or: $ pyro5-echoserver Pyro5-5.15/examples/echoserver/client.py000066400000000000000000000014501451404116400202750ustar00rootroot00000000000000from Pyro5.api import Proxy import Pyro5.errors print("First start the built-in test echo server with something like:") print("$ python -m Pyro5.utils.echoserver") print("Enter the server's uri that was printed:") uri = input().strip() echoserver = Proxy(uri) response = echoserver.echo("hello") print("\ngot back from the server: %s" % response) response = echoserver.echo([1, 2, 3, 4]) print("got back from the server: %s" % response) for element in echoserver.generator(): print("got element from remote iterator:", element) try: echoserver.error() except Exception: print("\ncaught an exception (expected), traceback:") print("".join(Pyro5.errors.get_pyro_traceback())) print("\nshutting down the test echo server. (restart it if you want to run this again)") echoserver.shutdown() Pyro5-5.15/examples/eventloop/000077500000000000000000000000001451404116400163135ustar00rootroot00000000000000Pyro5-5.15/examples/eventloop/Readme.txt000066400000000000000000000013551451404116400202550ustar00rootroot00000000000000This example shows a possible use of a custom 'event loop'. That means that your own program takes care of the main event loop, and that it needs to detect when 'events' happen on the appropriate Pyro objects. This particular example uses select to wait for the set of objects (sockets, really) and calls the correct event handler. You can add your own application's sockets easily this way. See the 'sever_threads.py' how this is done. With Pyro it is possible to easily merge/combine the event loops of different daemons. This way you don't have to write your own event loop multiplexer if you're only dealing with Pyro daemons. See the 'server_multiplexed.py' how this is done. (this only works for the multiplex server type, not for threaded). Pyro5-5.15/examples/eventloop/client.py000066400000000000000000000005561451404116400201510ustar00rootroot00000000000000from Pyro5.api import Proxy with Proxy("PYRONAME:example.eventloop.server") as proxy: print("5*11=%d" % proxy.multiply(5, 11)) print("'x'*10=%s" % proxy.multiply('x', 10)) input("press enter to do a loop of some more calls:") for i in range(1, 20): print("2*i=%d" % proxy.multiply(2, i)) print("'@'*i=%s" % proxy.multiply('@', i)) Pyro5-5.15/examples/eventloop/server_multiplexed.py000066400000000000000000000030611451404116400226070ustar00rootroot00000000000000import socket import time import Pyro5.socketutil import Pyro5.api Pyro5.config.SERVERTYPE = "multiplex" Pyro5.config.POLLTIMEOUT = 3 hostname = socket.gethostname() my_ip = Pyro5.socketutil.get_ip_address(None, workaround127=True) @Pyro5.api.expose class EmbeddedServer(object): def multiply(self, x, y): return x * y print("MULTIPLEXED server type. Initializing services...") print("Make sure that you don't have a name server running already!\n") # start a name server with broadcast server nameserverUri, nameserverDaemon, broadcastServer = Pyro5.nameserver.start_ns(host=my_ip) assert broadcastServer is not None, "expect a broadcast server to be created" print("got a Nameserver, uri=%s" % nameserverUri) # create a Pyro daemon pyrodaemon = Pyro5.api.Daemon(host=hostname) serveruri = pyrodaemon.register(EmbeddedServer()) print("server uri=%s" % serveruri) # register it with the embedded nameserver nameserverDaemon.nameserver.register("example.eventloop.server", serveruri) print("") # Because this server runs the different daemons using the "multiplex" server type, # we can use the built in support (since Pyro 4.44) to combine multiple daemon event loops. # We can then simply run the event loop of the 'master daemon'. It will dispatch correctly. pyrodaemon.combine(nameserverDaemon) pyrodaemon.combine(broadcastServer) def loopcondition(): print(time.asctime(), "Waiting for requests...") return True pyrodaemon.requestLoop(loopcondition) # clean up nameserverDaemon.close() broadcastServer.close() pyrodaemon.close() print("done") Pyro5-5.15/examples/eventloop/server_threads.py000066400000000000000000000052351451404116400217120ustar00rootroot00000000000000import socket import select import time import Pyro5.socketutil import Pyro5.api Pyro5.config.SERVERTYPE = "thread" hostname = socket.gethostname() my_ip = Pyro5.socketutil.get_ip_address(None, workaround127=True) @Pyro5.api.expose class EmbeddedServer(object): def multiply(self, x, y): return x * y print("THREADED server type. Initializing services...") print("Make sure that you don't have a name server running already!\n") # start a name server with broadcast server nameserverUri, nameserverDaemon, broadcastServer = Pyro5.nameserver.start_ns(host=my_ip) assert broadcastServer is not None, "expect a broadcast server to be created" print("got a Nameserver, uri=%s" % nameserverUri) # create a Pyro daemon pyrodaemon = Pyro5.api.Daemon(host=hostname) serveruri = pyrodaemon.register(EmbeddedServer()) print("server uri=%s" % serveruri) # register it with the embedded nameserver nameserverDaemon.nameserver.register("example.eventloop.server", serveruri) print("") # Below is our custom event loop. # Because this particular server runs the different daemons using the "tread" server type, # there is no built in way of combining the different event loops and server sockets. # We have to write our own multiplexing server event loop, and dispatch the requests # to the server that they belong to. # It is a bit silly to do it this way because the choice for a threaded server type # has already been made-- so you could just as well run the different daemons' request loops # each in their own thread and avoid writing this integrated event loop altogether. # But for the sake of example we write out our own loop: while True: print(time.asctime(), "Waiting for requests...") # create sets of the socket objects we will be waiting on # (a set provides fast lookup compared to a list) nameserverSockets = set(nameserverDaemon.sockets) pyroSockets = set(pyrodaemon.sockets) rs = [broadcastServer] # only the broadcast server is directly usable as a select() object rs.extend(nameserverSockets) rs.extend(pyroSockets) rs, _, _ = select.select(rs, [], [], 3) eventsForNameserver = [] eventsForDaemon = [] for s in rs: if s is broadcastServer: print("Broadcast server received a request") broadcastServer.processRequest() elif s in nameserverSockets: eventsForNameserver.append(s) elif s in pyroSockets: eventsForDaemon.append(s) if eventsForNameserver: print("Nameserver received a request") nameserverDaemon.events(eventsForNameserver) if eventsForDaemon: print("Daemon received a request") pyrodaemon.events(eventsForDaemon) Pyro5-5.15/examples/exceptions/000077500000000000000000000000001451404116400164615ustar00rootroot00000000000000Pyro5-5.15/examples/exceptions/Readme.txt000066400000000000000000000012661451404116400204240ustar00rootroot00000000000000This test is to show PYRO's remote exception capabilities. The remote object contains various member functions which raise various kinds of exceptions. The client will print those. Note the special handling of the Pyro exception. It is possible to extract and print the *remote* traceback. You can then see where in the code on the remote side the error occured! By installing Pyro's excepthook you can even see the remote traceback when you're not catching any exceptions. Also try to set PYRO_DETAILED_TRACEBACK to True (on the server) to get a very detailed traceback in your client. This can help debugging. Also, this example shows the use of a custom exception handler in the server. Pyro5-5.15/examples/exceptions/client.py000066400000000000000000000030211451404116400203050ustar00rootroot00000000000000import sys import Pyro5.api import Pyro5.errors test = Pyro5.api.Proxy("PYRONAME:example.exceptions") print(test.div(2.0, 9.0)) try: print(2 // 0) except ZeroDivisionError as x: print("DIVIDE BY ZERO: ", x) try: print(test.div(2, 0)) except ZeroDivisionError as x: print("DIVIDE BY ZERO: ", x) try: result = test.error() print("%r, %s" % (result, result)) except ValueError as x: print("VALUERROR: ", x) try: result = test.error2() print("%r, %s" % (result, result)) except ValueError as x: print("VALUERROR: ", x) try: result = test.othererr() print("%r, %s" % (result, result)) except Exception as x: print("ANOTHER ERROR: ", x) try: result = test.onewayerr() print("oneway call simply succeeded") except Exception as x: print("SHOULD NOT HAPPEN: exception from oneway call", x) try: result = test.unserializable() print("%r, %s" % (result, result)) except Exception as x: print("UNSERIALIZABLE ERROR: ", x) print("\n*** invoking server method that crashes, catching traceback ***") try: print(test.complexerror()) except Exception as x: print("CAUGHT ERROR >>> ", x) print("Printing Pyro traceback >>>>>>") print("".join(Pyro5.errors.get_pyro_traceback())) print("<<<<<<< end of Pyro traceback") print("\n*** installing pyro's excepthook") sys.excepthook = Pyro5.errors.excepthook print("*** invoking server method that crashes, not catching anything ***") print(test.complexerror()) # due to the excepthook, the exception will show the pyro error Pyro5-5.15/examples/exceptions/excep.py000066400000000000000000000013521451404116400201400ustar00rootroot00000000000000from Pyro5.api import expose, oneway @expose class TestClass(object): def div(self, arg1, arg2): return arg1 / arg2 def error(self): raise ValueError('a valueerror! Great!') def error2(self): return ValueError('a valueerror! Great!') def othererr(self): raise RuntimeError('a runtime error!') @oneway def onewayerr(self): raise ValueError('error in oneway call!') def complexerror(self): x = Foo() x.crash() def unserializable(self): return TestClass.unserializable class Foo(object): def crash(self): self.crash2('going down...') def crash2(self, arg): # this statement will crash on purpose: x = arg // 2 Pyro5-5.15/examples/exceptions/server.py000066400000000000000000000007241451404116400203440ustar00rootroot00000000000000from Pyro5.api import Daemon, serve import excep def my_error_handler(daemon, client_sock, method, vargs, kwargs, exception): print("\nERROR IN METHOD CALL USER CODE:") print(" client={} method={} exception={}".format(client_sock, method.__qualname__, repr(exception))) daemon = Daemon() daemon.methodcall_error_handler = my_error_handler serve( { excep.TestClass: "example.exceptions" }, daemon=daemon, use_ns=True, verbose=True) Pyro5-5.15/examples/filetransfer/000077500000000000000000000000001451404116400167645ustar00rootroot00000000000000Pyro5-5.15/examples/filetransfer/Readme.txt000066400000000000000000000054521451404116400207300ustar00rootroot00000000000000Pyro isn't ideal for transfering large amounts of data or lots of binary data. In some situations it's okay such as sending the occasional PNG file of a forum profile portrait, but generally for intensive file transfer it's better to use one of the established protocols that are optimized for this (rsync, ftp, http, etcetera). That being said you could opt for a hybrid approach: use Pyro for regular remote calls and provide a second network interface for the large data transfers, that will avoid the Pyro protocol and serialization overhead and size limitations (2 Gb). This example does exactly that: it runs a Pyro server that also serves a raw socket interface over which the large binary data files are sent. They're prepared in the regular Pyro server code and identified via a guid. The client then obtains the binary data by first sending the guid and then receiving the data over the raw socket connection in a streaming manner. If the binary data is very large it is better to store it first as temporary files on the disk in the server, otherwise you risk running out of system memory which will crash your python process. The client code as given selects the file storage approach. It will then stream the data from the server, thereby avoiding the need to allocate a huge amount of memory. (If you need to process all of the data at once you end up collecting it together anyway, but you'll be able to do this yourself in the most efficient way suitable for your application) As the data transfer averages at the end will show, the raw socket transfer is much faster than transferring the data via regular Pyro calls, and it will use a lot less memory and CPU as well. The speed does depend a bit on the performance and fragmentation of your hard drive where the temporary files are created. Also if your OS supports the os.sendfile() function (usually on Linux, BSD and OSX, but not Windows) you'll benefit even more from optimized data transfer. Note: Performance of the download via iterator is almost identical to the normal transfer speed of regular python/pyro calls. It is still a lot slower than raw data transfer, but at least you avoid having to load all of the data in memory at once. Note: The annotation stream is somewhere in the middle of the pack. Here the annotation mechanism is (ab)used to transfer binary file data chunks, thereby almost completely avoiding the overhead of the serialization mechanism. Note: the only "security" on the raw socket interface is that you have to know the id of a data file that you want to obtain. It's not advised to use this example as-is in a production environment. A better solution could be 2-way SSL to encrypt the data transfer and provide mutual authentication. For more benchmark numbers regarding large binary data transfer using Pyro, see the 'hugetransfer' example. Pyro5-5.15/examples/filetransfer/client.py000066400000000000000000000107511451404116400206200ustar00rootroot00000000000000import time import threading import socket import zlib import sys import serpent from Pyro5.api import Proxy, current_context def regular_pyro(uri): blobsize = 10*1024*1024 num_blobs = 10 total_size = 0 start = time.time() name = threading.current_thread().name with Proxy(uri) as p: for _ in range(num_blobs): print("thread {0} getting a blob using regular Pyro call...".format(name)) data = p.get_with_pyro(blobsize) data = serpent.tobytes(data) # in case of serpent encoded bytes total_size += len(data) assert total_size == blobsize*num_blobs duration = time.time() - start print("thread {0} done, {1:.2f} Mb/sec.".format(name, total_size/1024.0/1024.0/duration)) def via_iterator(uri): blobsize = 10*1024*1024 num_blobs = 10 total_size = 0 start = time.time() name = threading.current_thread().name with Proxy(uri) as p: for _ in range(num_blobs): print("thread {0} getting a blob using remote iterators...".format(name)) for chunk in p.iterator(blobsize): chunk = serpent.tobytes(chunk) # in case of serpent encoded bytes total_size += len(chunk) assert total_size == blobsize*num_blobs duration = time.time() - start print("thread {0} done, {1:.2f} Mb/sec.".format(name, total_size/1024.0/1024.0/duration)) def via_annotation_stream(uri): name = threading.current_thread().name start = time.time() total_size = 0 print("thread {0} downloading via annotation stream...".format(name)) with Proxy(uri) as p: perform_checksum = False for progress, checksum in p.annotation_stream(perform_checksum): chunk = current_context.response_annotations["FDAT"] if perform_checksum and zlib.crc32(chunk) != checksum: raise ValueError("checksum error") total_size += len(chunk) assert progress == total_size current_context.response_annotations.clear() # clean them up once we're done with them duration = time.time() - start print("thread {0} done, {1:.2f} Mb/sec.".format(name, total_size/1024.0/1024.0/duration)) def raw_socket(uri): blobsize = 40*1024*1024 num_blobs = 10 total_size = 0 name = threading.current_thread().name with Proxy(uri) as p: print("thread {0} preparing {1} blobs of size {2} Mb".format(name, num_blobs, blobsize/1024.0/1024.0)) blobs = {} for _ in range(num_blobs): file_id, blob_address = p.prepare_file_blob(blobsize) blobs[file_id] = blob_address start = time.time() for file_id in blobs: print("thread {0} retrieving blob using raw socket...".format(name)) blob_address = blobs[file_id] sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(tuple(blob_address)) sock.sendall(file_id.encode()) size = 0 chunk = b"dummy" while chunk: chunk = sock.recv(60000) size += len(chunk) sock.close() assert size == blobsize total_size += size duration = time.time() - start assert total_size == blobsize * num_blobs print("thread {0} done, {1:.2f} Mb/sec.".format(name, total_size/1024.0/1024.0/duration)) if __name__ == "__main__": uri = input("Uri of filetransfer server? ").strip() print("\n\n**** regular pyro calls ****\n") t1 = threading.Thread(target=regular_pyro, args=(uri, )) t2 = threading.Thread(target=regular_pyro, args=(uri, )) t1.start() t2.start() t1.join() t2.join() input("enter to continue:") print("\n\n**** transfer via iterators ****\n") t1 = threading.Thread(target=via_iterator, args=(uri, )) t2 = threading.Thread(target=via_iterator, args=(uri, )) t1.start() t2.start() t1.join() t2.join() input("enter to continue:") print("\n\n**** transfer via annotation stream ****\n") t1 = threading.Thread(target=via_annotation_stream, args=(uri, )) t2 = threading.Thread(target=via_annotation_stream, args=(uri, )) t1.start() t2.start() t1.join() t2.join() input("enter to continue:") print("\n\n**** raw socket transfers ****\n") t1 = threading.Thread(target=raw_socket, args=(uri, )) t2 = threading.Thread(target=raw_socket, args=(uri, )) t1.start() t2.start() t1.join() t2.join() input("enter to exit:") Pyro5-5.15/examples/filetransfer/server.py000066400000000000000000000121431451404116400206450ustar00rootroot00000000000000import select import tempfile import uuid import io import os import threading import zlib from Pyro5.api import expose, current_context, Daemon, config import Pyro5.socketutil datafiles = {} # temporary files datablobs = {} # in-memory @expose class FileServer(object): def get_with_pyro(self, size): print("sending %d bytes" % size) data = b"x" * size return data def iterator(self, size): chunksize = size//100 print("sending %d bytes via iterator, chunks of %d bytes" % (size, chunksize)) data = b"x" * size i = 0 while i < size: yield data[i:i+chunksize] i += chunksize def annotation_stream(self, with_checksum=False): # create a large temporary file f = tempfile.TemporaryFile() for _ in range(5000): f.write(b"1234567890!" * 1000) filesize = f.tell() f.seek(os.SEEK_SET, 0) # return the file data via annotation stream (remote iterator) annotation_size = 500000 print("transmitting file via annotations stream (%d bytes in chunks of %d)..." % (filesize, annotation_size)) with f: while True: chunk = f.read(annotation_size) if not chunk: break # store the file data chunk in the FDAT response annotation, # and return the current file position and checksum (if asked). current_context.response_annotations = {"FDAT": chunk} yield f.tell(), zlib.crc32(chunk) if with_checksum else 0 def prepare_file_blob(self, size): print("preparing file-based blob of size %d" % size) file_id = str(uuid.uuid4()) f = tempfile.TemporaryFile() chunk = b"x" * 100000 for _ in range(size//100000): f.write(chunk) f.write(b"x"*(size % 100000)) f.flush() f.seek(0, io.SEEK_SET) # os.fsync(f) datafiles[file_id] = f blobsock_info = self._pyroDaemon.blobsocket.getsockname() # return the port info for the blob socket as well return file_id, blobsock_info def prepare_memory_blob(self, size): print("preparing in-memory blob of size %d" % size) file_id = str(uuid.uuid4()) datablobs[file_id] = b"x" * size blobsock_info = self._pyroDaemon.blobsocket.getsockname() # return the port info for the blob socket as well return file_id, blobsock_info class FileServerDaemon(Daemon): def __init__(self, host=None, port=0): super(FileServerDaemon, self).__init__(host, port) host = self.transportServer.sock.getsockname()[0] self.blobsocket = Pyro5.socketutil.create_socket(bind=(host, 0), timeout=config.COMMTIMEOUT, nodelay=False) print("Blob socket available on:", self.blobsocket.getsockname()) def close(self): self.blobsocket.close() super(FileServerDaemon, self).close() def requestLoop(self, loopCondition=lambda: True): while loopCondition: rs = [self.blobsocket] rs.extend(self.sockets) rs, _, _ = select.select(rs, [], [], 3) daemon_events = [] for sock in rs: if sock in self.sockets: daemon_events.append(sock) elif sock is self.blobsocket: self.handle_blob_connect(sock) if daemon_events: self.events(daemon_events) def handle_blob_connect(self, sock): csock, caddr = sock.accept() thread = threading.Thread(target=self.blob_client, args=(csock,)) thread.daemon = True thread.start() def blob_client(self, csock): file_id = Pyro5.socketutil.receive_data(csock, 36).decode() print("{0} requesting file id {1}".format(csock.getpeername(), file_id)) is_file, data = self.find_blob_data(file_id) if is_file: if hasattr(os, "sendfile"): print("...from file using sendfile()") out_fn = csock.fileno() in_fn = data.fileno() sent = 1 offset = 0 while sent: sent = os.sendfile(out_fn, in_fn, offset, 512000) offset += sent else: print("...from file using plain old read(); your os doesn't have sendfile()") while True: chunk = data.read(512000) if not chunk: break csock.sendall(chunk) else: print("...from memory") csock.sendall(data) csock.close() def find_blob_data(self, file_id): if file_id in datablobs: return False, datablobs.pop(file_id) elif file_id in datafiles: return True, datafiles.pop(file_id) else: raise KeyError("no data for given id") with FileServerDaemon(host=Pyro5.socketutil.get_ip_address("")) as daemon: uri = daemon.register(FileServer, "example.filetransfer") print("Filetransfer server URI:", uri) daemon.requestLoop() Pyro5-5.15/examples/gui_eventloop/000077500000000000000000000000001451404116400171575ustar00rootroot00000000000000Pyro5-5.15/examples/gui_eventloop/Readme.txt000066400000000000000000000021571451404116400211220ustar00rootroot00000000000000This example shows two ways of embedding Pyro's event loop in another application, in this case a GUI application (written using Tkinter). There's one application where a background thread is used for the Pyro daemon. This means you can't directly update the GUI from the Pyro objects (because GUI update calls need to be performed from the GUI mainloop thread). So the threaded gui server submits the gui update calls via a Queue to the actual gui thread. There is a nice thing however, the GUI won't freeze up if a Pyro method call takes a while to execute. The other application doesn't use any threads besides the normal GUI thread. It uses a Tkinter-callback to check Pyro's sockets at a fast interval rate to see if it should dispatch any events to the daemon. Not using threads means you can directly update the GUI from Pyro calls but it also means the GUI will freeze if a Pyro method call takes a while. You also can't use Pyro's requestloop anymore, as it will lock up the GUI while it waits for incoming calls. You'll need to check yourself, using select() on the Pyro socket(s) and dispatching to the daemon manually. Pyro5-5.15/examples/gui_eventloop/client.py000066400000000000000000000007221451404116400210100ustar00rootroot00000000000000import time from Pyro5.api import Proxy print("First make sure one of the gui servers is running.") print("Enter the object uri that was printed:") uri = input().strip() guiserver = Proxy(uri) guiserver.message("Hello there!") time.sleep(0.5) guiserver.message("How's it going?") time.sleep(2) for i in range(20): guiserver.message("Counting {0}".format(i)) guiserver.message("now calling the sleep method with 5 seconds") guiserver.sleep(5) print("done!") Pyro5-5.15/examples/gui_eventloop/gui_nothreads.py000066400000000000000000000125211451404116400223650ustar00rootroot00000000000000""" This example shows a Tkinter GUI application that uses event loop callbacks to integrate Pyro's event loop into the Tkinter GUI mainloop. No threads are used. The Pyro event callback is called every so often to check if there are Pyro events to handle, and handles them synchronously. """ import time import select from tkinter import * import tkinter.simpledialog as simpledialog from Pyro5.api import expose, Daemon, config # Set the Pyro servertype to the multiplexing select-based server that doesn't # use a threadpool to service method calls. This way the method calls are # handled inside the main thread as well. config.SERVERTYPE = "multiplex" # The frequency with which the GUI loop calls the Pyro event handler. PYRO_EVENTLOOP_HZ = 50 class PyroGUI(object): """ The Tkinter GUI application that also listens for Pyro calls. """ def __init__(self): self.tk = Tk() self.tk.wm_title("Pyro in a Tkinter GUI eventloop - without threads") self.tk.wm_geometry("500x500") buttonframe = Frame(self.tk) button = Button(buttonframe, text="Messagebox", command=self.button_msgbox_clicked) button.pack(side=LEFT) button = Button(buttonframe, text="Add some text", command=self.button_text_clicked) button.pack(side=LEFT) button = Button(buttonframe, text="Clear all text", command=self.button_clear_clicked) button.pack(side=LEFT) quitbutton = Button(buttonframe, text="Quit", command=self.tk.quit) quitbutton.pack(side=RIGHT) frame = Frame(self.tk, padx=2, pady=2) buttonframe.pack(fill=X) rlabel = Label(frame, text="Pyro server messages:") rlabel.pack(fill=X) self.msg = Message(frame, anchor=NW, width=500, aspect=80, background="white", fg="black", relief="sunken") self.msg.pack(fill=BOTH, expand=1) frame.pack(fill=BOTH) self.serveroutput = [] def install_pyro_event_callback(self, daemon): """ Add a callback to the tkinter event loop that is invoked every so often. The callback checks the Pyro sockets for activity and dispatches to the daemon's event process method if needed. """ def pyro_event(): while True: # for as long as the pyro socket triggers, dispatch events s, _, _ = select.select(daemon.sockets, [], [], 0.01) if s: daemon.events(s) else: # no more events, stop the loop, we'll get called again soon anyway break self.tk.after(1000 // PYRO_EVENTLOOP_HZ, pyro_event) self.tk.after(1000 // PYRO_EVENTLOOP_HZ, pyro_event) def mainloop(self): self.tk.mainloop() def button_msgbox_clicked(self): # this button event handler is here only to show that gui events are still processed normally number = simpledialog.askinteger("A normal popup", "Hi there enter a number", parent=self.tk) def button_clear_clicked(self): self.serveroutput = [] self.msg.config(text="") def button_text_clicked(self): # add some random text to the message list self.add_message("The quick brown fox jumps over the lazy dog!") def add_message(self, message): message = "[{0}] {1}".format(time.strftime("%X"), message) self.serveroutput.append(message) self.serveroutput = self.serveroutput[-27:] self.msg.config(text="\n".join(self.serveroutput)) @expose class MessagePrinter(object): """ The Pyro object that interfaces with the GUI application. """ def __init__(self, gui): self.gui = gui def message(self, messagetext): # Add the message to the screen. # Note that you can't do anything that requires gui interaction # (such as popping a dialog box asking for user input), # because the gui (tkinter) is busy processing this pyro call. # It can't do two things at the same time when embedded this way. # If you do something in this method call that takes a long time # to process, the GUI is frozen during that time (because no GUI update # events are handled while this callback is active). self.gui.add_message("from Pyro: " + messagetext) def sleep(self, duration): # Note that you can't perform blocking stuff at all because the method # call is running in the gui mainloop thread and will freeze the GUI. # Try it - you will see the first message but everything locks up until # the sleep returns and the method call ends self.gui.add_message("from Pyro: sleeping {0} seconds...".format(duration)) self.gui.tk.update() time.sleep(duration) self.gui.add_message("from Pyro: woke up!") def main(): gui = PyroGUI() # create a pyro daemon with object daemon = Daemon() obj = MessagePrinter(gui) uri = daemon.register(obj, "pyrogui.message") gui.add_message("Pyro server started. Not using threads.") gui.add_message("Use the command line client to send messages.") urimsg = "Pyro object uri = {0}".format(uri) gui.add_message(urimsg) print(urimsg) # add a Pyro event callback to the gui's mainloop gui.install_pyro_event_callback(daemon) # enter the mainloop gui.mainloop() if __name__ == "__main__": main() Pyro5-5.15/examples/gui_eventloop/gui_threads.py000066400000000000000000000136641451404116400220410ustar00rootroot00000000000000""" This example shows a Tkinter GUI application that uses a worker thread to run Pyro's event loop. Usually, the GUI toolkit requires that GUI operations are done from within the GUI thread. So, if Pyro interfaces with the GUI, it cannot do that directly because the method calls are done from a different thread. This means we need a layer between them, this example uses a Queue to submit GUI operations to Tkinter's main loop. For this example, the mainloop runs a callback function every so often to check for new work in that Queue and will process it if the Pyro worker thread has put something in it. """ import time import threading import queue from tkinter import * import tkinter.simpledialog as simpledialog from Pyro5.api import expose, Daemon # The frequency with which the GUI mainloop checks for work in the Pyro queue. PYRO_QUEUE_HZ = 50 class PyroGUI(object): """ The Tkinter GUI application that also listens for Pyro calls. """ def __init__(self): self.pyro_queue = queue.Queue() self.tk = Tk() self.tk.wm_title("Pyro in a Tkinter GUI eventloop - with threads") self.tk.wm_geometry("500x500") buttonframe = Frame(self.tk) button = Button(buttonframe, text="Messagebox", command=self.button_msgbox_clicked) button.pack(side=LEFT) button = Button(buttonframe, text="Add some text", command=self.button_text_clicked) button.pack(side=LEFT) button = Button(buttonframe, text="Clear all text", command=self.button_clear_clicked) button.pack(side=LEFT) quitbutton = Button(buttonframe, text="Quit", command=self.tk.quit) quitbutton.pack(side=RIGHT) frame = Frame(self.tk, padx=2, pady=2) buttonframe.pack(fill=X) rlabel = Label(frame, text="Pyro server messages:") rlabel.pack(fill=X) self.msg = Message(frame, anchor=NW, width=500, aspect=80, background="white", fg="black", relief="sunken") self.msg.pack(fill=BOTH, expand=1) frame.pack(fill=BOTH) self.serveroutput = [] def install_pyro_queue_callback(self): """ Add a callback to the tkinter event loop that is invoked every so often. The callback checks the Pyro work queue for work and processes it. """ def check_pyro_queue(): try: while True: # get a work item from the queue (until it is empty) workitem = self.pyro_queue.get_nowait() # execute it in the gui's mainloop thread workitem["callable"](*workitem["vargs"], **workitem["kwargs"]) except queue.Empty: pass self.tk.after(1000 // PYRO_QUEUE_HZ, check_pyro_queue) self.tk.after(1000 // PYRO_QUEUE_HZ, check_pyro_queue) def mainloop(self): self.tk.mainloop() def button_msgbox_clicked(self): # this button event handler is here only to show that gui events are still processed normally number = simpledialog.askinteger("A normal popup", "Hi there enter a number", parent=self.tk) def button_clear_clicked(self): self.serveroutput = [] self.msg.config(text="") def button_text_clicked(self): # add some random text to the message list self.add_message("The quick brown fox jumps over the lazy dog!") def add_message(self, message): message = "[{0}] {1}".format(time.strftime("%X"), message) self.serveroutput.append(message) self.serveroutput = self.serveroutput[-27:] self.msg.config(text="\n".join(self.serveroutput)) @expose class MessagePrinter(object): """ The Pyro object that interfaces with the GUI application. It uses a Queue to transfer GUI update calls to Tkinter's mainloop. """ def __init__(self, gui): self.gui = gui def message(self, messagetext): # put a gui-update work item in the queue self.gui.pyro_queue.put({ "callable": self.gui.add_message, "vargs": ("from Pyro: " + messagetext,), "kwargs": {} }) def sleep(self, duration): # Note that you *can* perform blocking stuff now because the method # call is running in its own thread. It won't freeze the GUI anymore. # However you cannot do anything that requires GUI interaction because # that needs to go through the queue so the mainloop can pick that up. # (opening a dialog from this worker thread will still freeze the GUI) # But a simple sleep() call works fine and the GUI stays responsive. self.gui.pyro_queue.put({ "callable": self.gui.add_message, "vargs": ("from Pyro: sleeping {0} seconds...".format(duration),), "kwargs": {} }) time.sleep(duration) self.gui.pyro_queue.put({ "callable": self.gui.add_message, "vargs": ("from Pyro: woke up!",), "kwargs": {} }) class MyPyroDaemon(threading.Thread): def __init__(self, gui): threading.Thread.__init__(self) self.gui = gui self.started = threading.Event() def run(self): daemon = Daemon() obj = MessagePrinter(self.gui) self.uri = daemon.register(obj, "pyrogui.message2") self.started.set() daemon.requestLoop() def main(): gui = PyroGUI() # create a pyro daemon with object, running in its own worker thread pyro_thread = MyPyroDaemon(gui) pyro_thread.daemon = True pyro_thread.start() pyro_thread.started.wait() gui.add_message("Pyro server started. Using Pyro worker thread.") gui.add_message("Use the command line client to send messages.") urimsg = "Pyro object uri = {0}".format(pyro_thread.uri) gui.add_message(urimsg) print(urimsg) # add a Pyro event callback to the gui's mainloop gui.install_pyro_queue_callback() # enter the mainloop gui.mainloop() if __name__ == "__main__": main() Pyro5-5.15/examples/handshake/000077500000000000000000000000001451404116400162265ustar00rootroot00000000000000Pyro5-5.15/examples/handshake/Readme.txt000066400000000000000000000006001451404116400201600ustar00rootroot00000000000000This example shows how you can customize the connection handshake mechanism. The proxy is overridden to send custom handshake data to the daemon, in this case, a "secret" string to gain access. The daemon is overridden to check the handshake string and only allow a client connection if it sends the correct "secret" string. Don't use a "security" mechanism like this for real. Pyro5-5.15/examples/handshake/client.py000066400000000000000000000011271451404116400200570ustar00rootroot00000000000000from Pyro5.api import Proxy class CustomHandshakeProxy(Proxy): def _pyroValidateHandshake(self, response): # this will get called if the connection is okay by the server print("Proxy received handshake response data: ", response) uri = input("Enter the URI of the server object: ") secret = input("Enter the secret code of the server (or make a mistake on purpose to see what happens): ") with CustomHandshakeProxy(uri) as proxy: proxy._pyroHandshake = secret print("connecting...") proxy._pyroBind() proxy.ping() print("Connection ok!") print("done.") Pyro5-5.15/examples/handshake/server.py000066400000000000000000000026041451404116400201100ustar00rootroot00000000000000import Pyro5.core import Pyro5.api secret_code = "pancakes" class CustomDaemon(Pyro5.api.Daemon): def validateHandshake(self, conn, data): print("Daemon received handshake request from:", conn.sock.getpeername()) print("Handshake data:", data) # if needed, you can inspect Pyro5.callcontext.current_context: ctx = Pyro5.api.current_context print(" context.client: ", ctx.client) print(" context.client_sock_addr: ", ctx.client_sock_addr) print(" context.seq: ", ctx.seq) print(" context.msg_flags: ", ctx.msg_flags) print(" context.serializer_id: ", ctx.serializer_id) print(" context.correlation_id:", ctx.correlation_id) if data == secret_code: print("Secret code okay! Connection accepted.") # return some custom handshake data: return ["how", "are", "you", "doing"] else: print("Secret code wrong! Connection refused.") raise ValueError("wrong secret code, connection refused") def clientDisconnect(self, conn): print("Daemon client disconnects:", conn.sock.getpeername()) with CustomDaemon() as daemon: print("Server is ready. You can use the following URI to connect:") print(daemon.uriFor(Pyro5.core.DAEMON_NAME)) print("When asked, enter the following secret code: ", secret_code) daemon.requestLoop() Pyro5-5.15/examples/http/000077500000000000000000000000001451404116400152575ustar00rootroot00000000000000Pyro5-5.15/examples/http/Readme.txt000066400000000000000000000017511451404116400172210ustar00rootroot00000000000000A few client programs that demonstrate the use of Pyro's http gateway. Make sure you first start the gateway, for instance: python -m Pyro5.utils.httpgateway -e 'Pyro.|test.' or: pyro5-httpgateway -e 'Pyro.|test.' The code assumes the gateway runs on the default location. The '-e' option tells it to expose all Pyro objects (such as the name server) and the test objects (such as the test echo server). For completeness, also start the test echoserver (pyro5-echoserver -n, or python -m Pyro5.utils.echoserver -n) Then run a client of your choosing: client.js: javascript client code, for node.js client.py: python (3.x) client code ... and ofcourse, try opening the url that the server printed in your web browser. Javascript client code that runs in a browser is problematic due to the same origin policy. The gateway's web page does contain some examples of this that you can run in your browser. Simply navigate to the url that is printed when you start the http gateway server. Pyro5-5.15/examples/http/client.js000066400000000000000000000050361451404116400170770ustar00rootroot00000000000000/** Client side javascript example that talks to Pyro's http gateway. You can run this with node.js. **/ var http = require('http'); function obtainCharset (headers) { // Find the charset, if specified. var charset; var contentType = headers['content-type'] || ''; var matches = contentType.match(/charset=([^;,\r\n]+)/i); if (matches && matches[1]) { charset = matches[1]; } return charset || 'utf-8'; } function pyro_call(object, method, callback) { http.get({ hostname: "localhost", port: 8080, path: "/pyro/"+object+"/"+method, headers: { // "X-Pyro-Options": "options,here" // "X-Pyro-Gateway-Key": "secretgatewaykey" // "X-Pyro-Correlation-Id": "03e5899c-1117-11e5-b1fa-001e8c7827a6" } }, function(res) { var charset = obtainCharset(res.headers); res.setEncoding(charset); buffer=''; res.on('data', function(d) { buffer += d.toString(); }); res.on('end', function() { if(res.statusCode==200) { // all was well, process the response data as json if(buffer) { var parsed = JSON.parse(buffer); callback(parsed); } else { callback(null); } } else { // 404, 500 or some other server error occurred console.error("Server returned error response:"); console.error(buffer); } }); }).on('error', function(e) { // connection error of some sort console.error("ERROR:", e.toString()); }); } /*--------- do some pyro calls: ----------*/ pyro_call("Pyro.NameServer", "list", function(response) { console.log("\nLIST--->"); console.log(JSON.stringify(response, null, 4)); }); pyro_call("Pyro.NameServer", "$meta", function(response) { console.log("\nMETA--->"); console.log(JSON.stringify(response, null, 4)); }); pyro_call("Pyro.NameServer", "lookup?name=Pyro.NameServer", function(response) { console.log("\nLOOKUP--->"); console.log(JSON.stringify(response, null, 4)); }); pyro_call("test.echoserver", "oneway_slow", function(response) { console.log("\nONEWAY_SLOW--->"); console.log(JSON.stringify(response, null, 4)); }); pyro_call("test.echoserver", "slow", function(response) { console.log("\nSLOW--->"); console.log(JSON.stringify(response, null, 4)); }); Pyro5-5.15/examples/http/client.py000066400000000000000000000040601451404116400171070ustar00rootroot00000000000000import json import re import pprint from urllib.request import urlopen, Request from urllib.error import HTTPError def get_charset(req): charset = "utf-8" match = re.match(r".* charset=(.+)", req.getheader("Content-Type")) if match: charset = match.group(1) return charset def pyro_call(object_name, method, callback): request = Request("http://127.0.0.1:8080/pyro/{0}/{1}".format(object_name, method), # headers={"x-pyro-options": "oneway", "x-pyro-gateway-key": "secretgatewaykey"} ) with urlopen(request) as req: charset = get_charset(req) data = req.read().decode(charset) if data: callback(json.loads(data)) else: callback(None) def write_result(result): pprint.pprint(result, width=40) try: print("\nLIST--->") pyro_call("Pyro.NameServer", "list", write_result) except HTTPError as x: print("Error:", x) print("Error response data:", x.read()) try: print("\nMETA--->") pyro_call("Pyro.NameServer", "$meta", write_result) except HTTPError as x: print("Error:", x) print("Error response data:", x.read()) try: print("\nLOOKUP--->") pyro_call("Pyro.NameServer", "lookup?name=Pyro.NameServer", write_result) except HTTPError as x: print("Error:", x) print("Error response data:", x.read()) try: print("\nONEWAY_SLOW--->") pyro_call("test.echoserver", "oneway_slow", write_result) except HTTPError as x: print("Error:", x) print("Error response data:", x.read()) try: print("\nSLOW--->") pyro_call("test.echoserver", "slow", write_result) except HTTPError as x: print("Error:", x) print("Error response data:", x.read()) # Note that there is a nicer way to pass the parameters, you can probably # grab them from a function's vargs and/or kwargs and convert those to # a querystring using the appropriate library function. # Then you can call the method as usual and don't have to worry about adding the querystring # (or sticking it in a POST request if the params are too large)... Pyro5-5.15/examples/hugetransfer/000077500000000000000000000000001451404116400167755ustar00rootroot00000000000000Pyro5-5.15/examples/hugetransfer/Readme.txt000066400000000000000000000022641451404116400207370ustar00rootroot00000000000000This test transfers huge data structures to see how Pyro handles those. It sets a socket timeout as well to see how Pyro handles that. A couple of problems could be exposed by this test: - Some systems don't really seem to like non blocking sockets and large data transfers. For instance Mac OS X seems eager to cause EAGAIN errors when your data exceeds 'the devils number' number of bytes. Note that this problem only occurs when using specific socket code. Pyro contains a workaround. More info: http://old.nabble.com/The-Devil%27s-Number-td9169165.html http://www.cherrypy.org/ticket/598 - Other systems seemed to have problems receiving large chunks of data. Windows causes memory errors when the receive buffer is too large. Pyro's receive loop works with comfortable smaller data chunks, to avoid these kind of problems. Note that performance of the download via iterator is almost identical to the normal transfer speed. Note: For a possible approach on transferring large amounts of binary data *efficiently*, see the 'filetransfer' example. It contains an example how to work with a raw socket connection that avoids the Pyro protocol and serialization overhead. Pyro5-5.15/examples/hugetransfer/client.py000066400000000000000000000037761451404116400206420ustar00rootroot00000000000000import time import warnings import serpent from Pyro5.api import Proxy, config warnings.filterwarnings("ignore") print("Enter the server's uri that was printed:") uri = input().strip() datasize = 5 * 1024 * 1024 # 5 mb def do_test(data): assert len(data) == datasize totalsize = 0 with Proxy(uri) as obj: obj._pyroBind() begin = time.time() for i in range(10): print("transferring %d bytes" % datasize) size = obj.transfer(data) assert size == datasize totalsize += datasize duration = time.time() - begin totalsize = float(totalsize) print("It took %.2f seconds to transfer %d mb." % (duration, totalsize / 1024 / 1024)) print("That is %.0f kb/sec. = %.1f mb/sec. (serializer: %s)" % (totalsize / 1024 / duration, totalsize / 1024 / 1024 / duration, config.SERIALIZER)) def do_test_chunks(): with Proxy(uri) as p: totalsize = 0 begin = time.time() for chunk in p.download_chunks(datasize*10): chunk = serpent.tobytes(chunk) # in case of serpent encoded bytes totalsize += len(chunk) print(".", end="", flush=True) assert totalsize == datasize*10 duration = time.time() - begin totalsize = float(totalsize) print("\nIt took %.2f seconds to transfer %d mb." % (duration, totalsize / 1024 / 1024)) print("That is %.0f kb/sec. = %.1f mb/sec. (serializer: %s)" % (totalsize / 1024 / duration, totalsize / 1024 / 1024 / duration, config.SERIALIZER)) data = 'x' * datasize print("\n\n----test with string data----") do_test(data) print("\n\n----test with byte data----") data = b'x' * datasize do_test(data) data = bytearray(b'x' * datasize) print("\n\n----test with bytearray data----") do_test(data) print("\n\n----test download via iterator----") do_test_chunks() print("\n\n (tip: also see the 'filetransfer' example for more efficient ways to transfer large amounts of binary data)") Pyro5-5.15/examples/hugetransfer/server.py000066400000000000000000000015531451404116400206610ustar00rootroot00000000000000import serpent from Pyro5.api import expose, serve, config import Pyro5.socketutil class Testclass(object): @expose def transfer(self, data): if config.SERIALIZER == "serpent" and type(data) is dict: data = serpent.tobytes(data) # in case of serpent encoded bytes print("received %d bytes" % len(data)) return len(data) @expose def download_chunks(self, size): print("client requests a 'streaming' download of %d bytes" % size) data = bytearray(size) i = 0 chunksize = 200000 print(" using chunks of size", chunksize) while i < size: yield data[i:i+chunksize] i += chunksize serve( { Testclass: "example.hugetransfer" }, host=Pyro5.socketutil.get_ip_address("localhost", workaround127=True), use_ns=False, verbose=True) Pyro5-5.15/examples/instancemode/000077500000000000000000000000001451404116400167515ustar00rootroot00000000000000Pyro5-5.15/examples/instancemode/Readme.txt000066400000000000000000000007551451404116400207160ustar00rootroot00000000000000This example shows the use of the instance_mode option when exposing a class. The client will report the id of the object that handled the request. The server will print this as well, but will also show exactly when Pyro is creating a new instance of your server class. This makes it more clear in situations where Python itself is recycling objects and therefore ending up with the same id. Please make sure a name server is running somewhere first, before starting the server and client. Pyro5-5.15/examples/instancemode/client.py000066400000000000000000000025651451404116400206110ustar00rootroot00000000000000from Pyro5.api import Proxy print("Showing the different instancing modes.") print("The number printed, is the id of the instance that handled the call.") print("\n-----PERCALL (different number *possible* every time) -----") with Proxy("PYRONAME:instance.percall") as p: print(p.msg("hello1")) print(p.msg("hello1")) print(p.msg("hello1")) with Proxy("PYRONAME:instance.percall") as p: print(p.msg("hello2")) print(p.msg("hello2")) print(p.msg("hello2")) with Proxy("PYRONAME:instance.percall") as p: print(p.msg("hello3")) print(p.msg("hello3")) print(p.msg("hello3")) print("\n-----SESSION (same numbers within session) -----") with Proxy("PYRONAME:instance.session") as p: print(p.msg("hello1")) print(p.msg("hello1")) print(p.msg("hello1")) print(" ..new proxy..") with Proxy("PYRONAME:instance.session") as p: print(p.msg("hello2")) print(p.msg("hello2")) print(p.msg("hello2")) print("\n-----SINGLE (same number always, even over proxies) -----") with Proxy("PYRONAME:instance.single") as p: print(p.msg("hello1")) print(p.msg("hello1")) print(p.msg("hello1")) with Proxy("PYRONAME:instance.single") as p: print(p.msg("hello2")) print(p.msg("hello2")) print(p.msg("hello2")) with Proxy("PYRONAME:instance.single") as p: print(p.msg("hello3")) print(p.msg("hello3")) print(p.msg("hello3")) Pyro5-5.15/examples/instancemode/server.py000066400000000000000000000026561451404116400206420ustar00rootroot00000000000000from Pyro5.api import expose, behavior, serve, current_context @behavior(instance_mode="single") class SingleInstance(object): def __init__(self): print("created SingleInstance instance with id", id(self)) @expose def msg(self, message): print("[%s] %s.msg: %s" % (id(self), self.__class__.__name__, message)) return id(self) @behavior(instance_mode="session", instance_creator=lambda clazz: clazz.create_instance()) class SessionInstance(object): def __init__(self): print("created SessionInstance instance with id", id(self)) @expose def msg(self, message): print("[%s] %s.msg: %s" % (id(self), self.__class__.__name__, message)) return id(self), self.correlation_id @classmethod def create_instance(cls): obj = cls() obj.correlation_id = current_context.correlation_id return obj @behavior(instance_mode="percall") class PercallInstance(object): def __init__(self): print("created PercallInstance instance with id", id(self)) @expose def msg(self, message): print("[%s] %s.msg: %s" % (id(self), self.__class__.__name__, message)) return id(self) if __name__ == "__main__": # please make sure a name server is running somewhere first. serve({ SingleInstance: "instance.single", SessionInstance: "instance.session", PercallInstance: "instance.percall" }, verbose=True) Pyro5-5.15/examples/maxsize/000077500000000000000000000000001451404116400157605ustar00rootroot00000000000000Pyro5-5.15/examples/maxsize/Readme.txt000077500000000000000000000011661451404116400177250ustar00rootroot00000000000000Shows how the MAX_MESSAGE_SIZE config item works. The client sends a big message first without a limit, then with a limit set on the message size. The second attempt will fail with a protocol error. The client talks to the echo server so you'll have to start the echo server first in another window: $ python -m Pyro5.utils.echoserver or: $ pyro5-echoserver You can try to set the PYRO_MAX_MESSAGE_SIZE environment variable to a small value (such as 2000) before starting the echo server, to see how it deals with receiving messages that are too large on the server. (Pyro will log an error and close the connection). Pyro5-5.15/examples/maxsize/client.py000066400000000000000000000014641451404116400176150ustar00rootroot00000000000000import Pyro5.errors import Pyro5.api huge_object = [42] * 10000 simple_object = {"message": "hello", "irrelevant": huge_object} print("First start the built-in test echo server with something like:") print("$ python -m Pyro5.utils.echoserver") print("Enter the server's uri that was printed:") uri = input().strip() echoserver = Pyro5.api.Proxy(uri) Pyro5.config.MAX_MESSAGE_SIZE = 2**32 print("\nSending big data with virtually no limit on message size...") response = echoserver.echo(simple_object) print("success.") try: Pyro5.config.MAX_MESSAGE_SIZE = 2500 print("\nSending big data with a limit on message size...") response = echoserver.echo(simple_object) print("Hmm, this should have raised an exception") except Pyro5.errors.MessageTooLargeError as x: print("EXCEPTION (expected):", x) Pyro5-5.15/examples/messagebus/000077500000000000000000000000001451404116400164365ustar00rootroot00000000000000Pyro5-5.15/examples/messagebus/Readme.txt000066400000000000000000000040021451404116400203700ustar00rootroot00000000000000Shows how to build a simple asynchronous pubsub message bus. (Note that it is NOT aiming to be a reliable high performance msgbus to compete with solutions such as zmq, rabbitmq, celery) It uses a few Pyro features to achieve this: - autoproxy (for subscribers) - instance_mode - auto reconnect Start the message bus server from this example's directory with: python -m messagebus.server [options] Use -h to get a help screen of available options. You can run multiple publishers at the same time, make sure you give a different location argument when you start each of them to see the results. You can also run multiple subscribers at the same time, the published messages will be delivered to each subscriber. There are two kinds of subscribers: - one that automatically consumes the messages as soon as they arrive on the bus, - one that has a manual message processing loop The messagebus is a bit simplistic if you use the in-memory storage: it only keeps messages and subscribers in memory. If the message bus server dies, everything is lost. If an error occurs when forwarding a message to subscribers, the message is immediately discarded. If it was a communication error, the subscriber is immediately removed from the topic as well. The in-memory storage is very fast though, so if you're after a very high message throughput, it may be the storage option for you. However you can also use the SqliteStorage which uses a database on disk to store topics, messages and subscriptions. If the message bus server dies, it will simply continue where it was. No messages will get lost, and it also remembers the subscribers. So simply restarting the message bus server is enough to get everything back on track without lost data. The sqlite storage is slower than the in-memory storage (and MUCH slower when running on Windows), so if you need a high message throughput, it may not be suitable. There's no queue mechanism, this is left as an exercise for the reader. (A queue is 1-to-1 communication whereas pubsub is 1-to-many) Pyro5-5.15/examples/messagebus/manytopics_publisher.py000066400000000000000000000012411451404116400232510ustar00rootroot00000000000000""" This is the producer for the 'many topics' messages example. """ import time import random import sys import Pyro5.errors from Pyro5.api import Proxy from messagebus import PYRO_MSGBUS_NAME sys.excepthook = Pyro5.errors.excepthook # add a bunch of topics to the bus bus = Proxy("PYRONAME:"+PYRO_MSGBUS_NAME) for letter in "abcdefghijklmnopqrstuvwxyz": bus.add_topic("msg.topic.%s" % letter) print("publishing messages on a random topic") seq = 1 while True: time.sleep(0.05) letter = random.choice("abcdefghijklmnopqrstuvwxyz") topic = "msg.topic.%s" % letter bus.send(topic, "message %d" % seq) seq += 1 print("", letter, seq, end="\r") Pyro5-5.15/examples/messagebus/manytopics_subscriber.py000066400000000000000000000032721451404116400234250ustar00rootroot00000000000000""" This is the subscriber for the 'many topics' messages example. For code with more explanations, see the regular 'weather' message example code. """ import os import time import threading from operator import itemgetter from Pyro5.api import expose, Daemon from messagebus.messagebus import Subscriber @expose class Subber(Subscriber): def init_counters(self, topics): self.message_counter = {} self.last_message = {} for t in topics: self.message_counter[t] = 0 self.last_message[t] = None def consume_message(self, topic, message): self.message_counter[topic] += 1 self.last_message[topic] = message def clear_screen(): os.system(['clear', 'cls'][os.name == 'nt']) subber = Subber() d = Daemon() d.register(subber) daemon_thread = threading.Thread(target=d.requestLoop) daemon_thread.daemon = True daemon_thread.start() # mass subscribe to all available topics topics = list(sorted(subber.bus.topics())) subber.init_counters(topics) for t in topics: subber.bus.subscribe(t, subber) # show a table of the active topics on the bus while True: clear_screen() print(time.ctime(), "-- active topics on the messagebus:") print("{:20} : {:5} {} {}".format("topic", "count", "last_recv", "last message data")) for topic, count in sorted(subber.message_counter.items(), key=itemgetter(1), reverse=True): msg = subber.last_message[topic] if msg: print("{:20} : {:5d} - {} {!r:.20}".format(topic, count, msg.created.time(), msg.data)) else: print("{:20} : {:5d}".format(topic, count)) print("(restart me to refresh the list of topics)") time.sleep(1) Pyro5-5.15/examples/messagebus/messagebus/000077500000000000000000000000001451404116400205745ustar00rootroot00000000000000Pyro5-5.15/examples/messagebus/messagebus/__init__.py000066400000000000000000000004041451404116400227030ustar00rootroot00000000000000""" Pyro MessageBus: a simple pub/sub message bus. Provides a way of cummunicating where the sender and receivers are fully decoupled. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ PYRO_MSGBUS_NAME = "Pyro.MessageBus" Pyro5-5.15/examples/messagebus/messagebus/messagebus.py000066400000000000000000000470571451404116400233210ustar00rootroot00000000000000""" Pyro MessageBus: a simple pub/sub message bus. Provides a way of cummunicating where the sender and receivers are fully decoupled. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import threading import uuid import datetime import time import logging import traceback import sys import pickle import contextlib from collections import defaultdict from contextlib import closing import sqlite3 import queue from Pyro5.api import expose, behavior, oneway, Proxy, register_class_to_dict, register_dict_to_class import Pyro5.errors from . import PYRO_MSGBUS_NAME __all__ = ["MessageBus", "Message"] log = logging.getLogger("Pyro5.MessageBus") class Message(object): __slots__ = ("msgid", "created", "data") def __init__(self, msgid, created, data): self.msgid = msgid self.created = created self.data = data @staticmethod def to_dict(obj): return { "__class__": "Pyro5.utils.messagebus.message", "msgid": str(obj.msgid), "created": obj.created.isoformat(), "data": obj.data } @classmethod def from_dict(cls, classname, d): return cls(uuid.UUID(d["msgid"]), datetime.datetime.strptime(d["created"], "%Y-%m-%dT%H:%M:%S.%f"), d["data"]) # make sure Pyro knows how to serialize the custom Message class register_class_to_dict(Message, Message.to_dict) register_dict_to_class("Pyro5.utils.messagebus.message", Message.from_dict) @expose class Subscriber(object): def __init__(self, auto_consume=True, max_queue_size=5000): self.bus = Proxy("PYRONAME:"+PYRO_MSGBUS_NAME) self.received_messages = queue.Queue(maxsize=max_queue_size) if auto_consume: self.__bus_consumer_thread = threading.Thread(target=self.__bus_consume_message) self.__bus_consumer_thread.daemon = True self.__bus_consumer_thread.start() def incoming_message(self, topic, message): self.received_messages.put((topic, message)) def incoming_messages(self, topic, messages): # this is an optimization to receive multiple messages for the topic at the same time for msg in messages: self.incoming_message(topic, msg) def __bus_consume_message(self): # this runs in a thread, to pick up and process incoming messages while True: try: topic, message = self.received_messages.get(timeout=1) except queue.Empty: time.sleep(0.002) continue try: self.consume_message(topic, message) except Exception: print("Error while consuming message:", file=sys.stderr) traceback.print_exc(file=sys.stderr) def consume_message(self, topic, message): # this is called from a background thread raise NotImplementedError("subclass should implement this") class MemoryStorage(object): """ Storage implementation that just uses in-memory dicts. It is very fast. Stopping the message bus server will make it instantly forget about every topic and pending messages. """ def __init__(self): self.messages = {} # topic -> list of pending messages self.subscribers = {} # topic -> set of subscribers self.proxy_cache = {} self.total_msg_count = 0 def topics(self): return self.messages.keys() def create_topic(self, topic): if topic in self.messages: return self.messages[topic] = [] self.subscribers[topic] = set() def remove_topic(self, topic): if topic not in self.messages: return del self.messages[topic] for sub in self.subscribers.get(topic, set()): if hasattr(sub, "_pyroRelease"): sub._pyroRelease() if hasattr(sub, "_pyroUri"): with contextlib.suppress(KeyError): proxy = self.proxy_cache[sub._pyroUri] proxy._pyroRelease() del self.proxy_cache[sub._pyroUri] del self.subscribers[topic] def add_message(self, topic, message): self.messages[topic].append(message) self.total_msg_count += 1 def add_subscriber(self, topic, subscriber): if hasattr(subscriber, "_pyroUri"): # if we already have a subscriber proxy for this uri, use that instead subscriber = self.proxy_cache.get(subscriber._pyroUri, subscriber) self.proxy_cache[subscriber._pyroUri] = subscriber self.subscribers[topic].add(subscriber) def remove_subscriber(self, topic, subscriber): if subscriber in self.subscribers[topic]: if hasattr(subscriber, "_pyroRelease"): subscriber._pyroRelease() if hasattr(subscriber, "_pyroUri"): with contextlib.suppress(KeyError): proxy = self.proxy_cache[subscriber._pyroUri] proxy._pyroRelease() del self.proxy_cache[subscriber._pyroUri] self.subscribers[topic].discard(subscriber) def all_pending_messages(self): return {topic: list(msgs) for topic, msgs in self.messages.items()} def has_pending_messages(self, topic): return topic in self.messages and any(self.messages[topic]) def has_subscribers(self, topic): return topic in self.subscribers and any(self.subscribers[topic]) def all_subscribers(self): all_subs = {} for topic, subs in self.subscribers.items(): all_subs[topic] = set(subs) return all_subs def remove_messages(self, topics_messages): for topic in topics_messages: if topic in self.messages: msg_list = self.messages[topic] for message in topics_messages[topic]: with contextlib.suppress(ValueError): msg_list.remove(message) def stats(self): subscribers = pending = 0 for subs in self.subscribers.values(): subscribers += len(subs) for msgs in self.messages.values(): pending += len(msgs) return len(self.messages), subscribers, pending, self.total_msg_count class SqliteStorage(object): """ Storage implementation that uses a sqlite database to store the messages and subscribers. It is a lot slower than the in-memory storage, but no data is lost if the messagebus dies. If you restart it, it will also reconnect to the subscribers and carry on from where it stopped. """ dbconnections = {} def __init__(self): conn = self.dbconn() conn.execute("PRAGMA foreign_keys=ON") conn.execute(""" CREATE TABLE IF NOT EXISTS Topic ( id INTEGER PRIMARY KEY, topic NVARCHAR(500) UNIQUE NOT NULL ); """) conn.execute(""" CREATE TABLE IF NOT EXISTS Message( id CHAR(36) PRIMARY KEY, created DATETIME NOT NULL, topic INTEGER NOT NULL, msgdata BLOB NOT NULL, FOREIGN KEY(topic) REFERENCES Topic(id) ); """) conn.execute(""" CREATE TABLE IF NOT EXISTS Subscription( id INTEGER PRIMARY KEY, topic INTEGER NOT NULL, subscriber NVARCHAR(500) NOT NULL, FOREIGN KEY(topic) REFERENCES Topic(id) ); """) conn.commit() self.proxy_cache = {} self.total_msg_count = 0 def dbconn(self): # return the db-connection for the current thread thread = threading.current_thread() try: return self.dbconnections[thread] except KeyError: conn = sqlite3.connect("messages.sqlite", detect_types=sqlite3.PARSE_DECLTYPES) self.dbconnections[thread] = conn return conn def topics(self): conn = self.dbconn() with closing(conn.cursor()) as cursor: return [r[0] for r in cursor.execute("SELECT topic FROM Topic").fetchall()] def create_topic(self, topic): conn = self.dbconn() with closing(conn.cursor()) as cursor: if not cursor.execute("SELECT EXISTS(SELECT 1 FROM Topic WHERE topic=?)", [topic]).fetchone()[0]: cursor.execute("INSERT INTO Topic(topic) VALUES(?)", [topic]) conn.commit() def remove_topic(self, topic): conn = self.dbconn() conn.execute("PRAGMA foreign_keys=ON") with closing(conn.cursor()) as cursor: topic_id = cursor.execute("SELECT id FROM Topic WHERE topic=?", [topic]).fetchone() if not topic_id: return else: topic_id = topic_id[0] sub_uris = [r[0] for r in cursor.execute("SELECT subscriber FROM Subscription WHERE topic=?", [topic_id]).fetchall()] cursor.execute("DELETE FROM Subscription WHERE topic=?", [topic_id]) cursor.execute("DELETE FROM Message WHERE topic=?", [topic_id]) cursor.execute("DELETE FROM Topic WHERE id=?", [topic_id]) conn.commit() for uri in sub_uris: with contextlib.suppress(KeyError): proxy = self.proxy_cache[uri] proxy._pyroRelease() del self.proxy_cache[uri] def add_message(self, topic, message): msg_data = pickle.dumps(message.data, pickle.HIGHEST_PROTOCOL) conn = self.dbconn() conn.execute("PRAGMA foreign_keys=ON") with closing(conn.cursor()) as cursor: res = cursor.execute("SELECT id FROM Topic WHERE topic=?", [topic]).fetchone() if not res: raise KeyError(topic) topic_id = res[0] if cursor.execute("SELECT EXISTS(SELECT 1 FROM Subscription WHERE topic=?)", [topic_id]).fetchone()[0]: # there is at least one subscriber for this topic, insert the message (otherwise just discard it) cursor.execute("INSERT INTO Message(id, created, topic, msgdata) VALUES (?,?,?,?)", [str(message.msgid), message.created, topic_id, msg_data]) conn.commit() self.total_msg_count += 1 def add_subscriber(self, topic, subscriber): if not hasattr(subscriber, "_pyroUri"): raise ValueError("can only store subscribers that are a Pyro proxy") uri = str(subscriber._pyroUri) conn = self.dbconn() conn.execute("PRAGMA foreign_keys=ON") with closing(conn.cursor()) as cursor: topic_id = cursor.execute("SELECT id FROM Topic WHERE topic=?", [topic]).fetchone()[0] if not cursor.execute("SELECT EXISTS(SELECT 1 FROM Subscription WHERE topic=? AND subscriber=?)", [topic_id, uri]).fetchone()[0]: cursor.execute("INSERT INTO Subscription(topic, subscriber) VALUES (?,?)", [topic_id, uri]) self.proxy_cache[uri] = subscriber conn.commit() def remove_subscriber(self, topic, subscriber): conn = self.dbconn() conn.execute("PRAGMA foreign_keys=ON") uri = str(subscriber._pyroUri) with closing(conn.cursor()) as cursor: cursor.execute("DELETE FROM Subscription WHERE topic=(SELECT id FROM Topic WHERE topic=?) AND subscriber=?", [topic, uri]) conn.commit() with contextlib.suppress(KeyError): proxy = self.proxy_cache[uri] proxy._pyroClaimOwnership() # because it has been used from a different thread. This also kills any connection. del self.proxy_cache[uri] def all_pending_messages(self): conn = self.dbconn() with closing(conn.cursor()) as cursor: msgs = cursor.execute("SELECT t.topic, m.id, m.created, m.msgdata FROM Message AS m, Topic as t WHERE m.topic=t.id").fetchall() result = defaultdict(list) for msg in msgs: blob_data = msg[3] result[msg[0]].append(Message(uuid.UUID(msg[1]), datetime.datetime.strptime(msg[2], "%Y-%m-%d %H:%M:%S.%f"), pickle.loads(blob_data))) return result def has_pending_messages(self, topic): conn = self.dbconn() with closing(conn.cursor()) as cursor: return cursor.execute("SELECT EXISTS(SELECT 1 FROM Message WHERE topic=(SELECT id FROM Topic WHERE topic=?))", [topic]).fetchone()[0] def has_subscribers(self, topic): conn = self.dbconn() with closing(conn.cursor()) as cursor: return cursor.execute("SELECT EXISTS(SELECT 1 FROM Subscription WHERE topic=(SELECT id FROM Topic WHERE topic=?))", [topic]).fetchone()[0] def all_subscribers(self): conn = self.dbconn() with closing(conn.cursor()) as cursor: result = cursor.execute("SELECT s.id, t.topic, s.subscriber FROM Subscription AS s, Topic AS t WHERE t.id=s.topic").fetchall() subs = defaultdict(list) for sub_id, topic, uri in result: if uri in self.proxy_cache: proxy = self.proxy_cache[uri] subs[topic].append(proxy) else: try: proxy = Proxy(uri) except Exception: log.exception("Cannot create pyro proxy, sub_id=%d, uri=%s", sub_id, uri) cursor.execute("DELETE FROM Subscription WHERE id=?", [sub_id]) else: self.proxy_cache[uri] = proxy subs[topic].append(proxy) conn.commit() return subs def remove_messages(self, topics_messages): if not topics_messages: return all_guids = [[str(message.msgid)] for msglist in topics_messages.values() for message in msglist] conn = self.dbconn() conn.execute("PRAGMA foreign_keys=ON") with closing(conn.cursor()) as cursor: cursor.executemany("DELETE FROM Message WHERE id = ?", all_guids) conn.commit() def stats(self): conn = self.dbconn() with closing(conn.cursor()) as cursor: topics = cursor.execute("SELECT COUNT(*) FROM Topic").fetchone()[0] subscribers = cursor.execute("SELECT COUNT(*) FROM Subscription").fetchone()[0] pending = cursor.execute("SELECT COUNT(*) FROM Message").fetchone()[0] return topics, subscribers, pending, self.total_msg_count def make_messagebus(clazz): if make_messagebus.storagetype == "sqlite": return clazz(storage=SqliteStorage()) elif make_messagebus.storagetype == "memory": return clazz(storage=MemoryStorage()) else: raise ValueError("invalid storagetype") @behavior(instance_mode="single", instance_creator=make_messagebus) @expose class MessageBus(object): def __init__(self, storage=None): if storage is None: storage = MemoryStorage() self.storage = storage # topic -> list of pending messages log.info("using storage: %s", self.storage.__class__.__name__) self.msg_lock = threading.Lock() self.msg_added = threading.Event() self.sender = threading.Thread(target=self.__sender, name="messagebus.sender") self.sender.daemon = True self.sender.start() log.info("started") def add_topic(self, topic): if not isinstance(topic, str): raise TypeError("topic must be str") with self.msg_lock: self.storage.create_topic(topic) def remove_topic(self, topic): try: if self.storage.has_pending_messages(topic) or self.storage.has_subscribers(topic): raise ValueError("topic still has pending messages and/or subscribers") except KeyError: pass else: with self.msg_lock: self.storage.remove_topic(topic) def topics(self): with self.msg_lock: return set(self.storage.topics()) def send(self, topic, message): message = Message(uuid.uuid4(), datetime.datetime.now(), message) with self.msg_lock: self.storage.add_message(topic, message) self.msg_added.set() # signal that a new message has arrived self.msg_added.clear() @oneway def send_no_ack(self, topic, message): self.send(topic, message) def subscribe(self, topic, subscriber): """Add a subscription to a topic.""" meth = getattr(subscriber, "incoming_message", None) if not meth or not callable(meth): raise TypeError("subscriber must have incoming_message() method") self.add_topic(topic) # make sure the topic exists with self.msg_lock: self.storage.add_subscriber(topic, subscriber) log.debug("subscribed: %s -> %s" % (topic, subscriber)) def unsubscribe(self, topic, subscriber): """Remove a subscription to a topic.""" with self.msg_lock: self.storage.remove_subscriber(topic, subscriber) log.debug("unsubscribed %s from topic %s" % (subscriber, topic)) def _unsubscribe_many(self, subscribers): if subscribers: topics = self.storage.topics() with self.msg_lock: for topic in topics: for subscriber in subscribers: self.storage.remove_subscriber(topic, subscriber) log.debug("unsubscribed from all topics: %s" % subscribers) def __sender(self): # this runs in a thread, to pick up and forward incoming messages prev_print_stats = 0 while True: self.msg_added.wait(timeout=2.01) if time.time() - prev_print_stats >= 10: prev_print_stats = time.time() self._print_stats() with self.msg_lock: msgs_per_topic = self.storage.all_pending_messages() subs_per_topic = self.storage.all_subscribers() subs_to_remove = set() for topic, messages in msgs_per_topic.items(): if topic not in subs_per_topic or not messages: continue for subscriber in subs_per_topic[topic]: if subscriber in subs_to_remove: # skipping because subscriber is scheduled for removal continue try: subscriber._pyroClaimOwnership() # because we run in a different thread now try: # send the batch of messages pending for this topic in one go subscriber.incoming_messages(topic, messages) except Pyro5.errors.MessageTooLargeError: # the batch doesn't fit in the configured max msg size, send them one by one instead for message in messages: subscriber.incoming_message(topic, message) except Exception as x: # can't deliver them, drop the subscription log.warning("error delivering message(s) for topic=%s, subscriber=%s, error=%r" % (topic, subscriber, x)) log.warning("removing subscription because of that error") subs_to_remove.add(subscriber) # remove processed messages if msgs_per_topic: with self.msg_lock: self.storage.remove_messages(msgs_per_topic) # remove broken subscribers self._unsubscribe_many(subs_to_remove) def _print_stats(self): topics, subscribers, pending, messages = self.storage.stats() timestamp = datetime.datetime.now() timestamp = timestamp.replace(microsecond=0) print("\r[%s] stats: %d topics, %d subs, %d pending, %d total " % (timestamp, topics, subscribers, pending, messages), end="") Pyro5-5.15/examples/messagebus/messagebus/server.py000066400000000000000000000027731451404116400224650ustar00rootroot00000000000000""" Pyro MessageBus: a simple pub/sub message bus. Provides a way of cummunicating where the sender and receivers are fully decoupled. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ from optparse import OptionParser from Pyro5.api import Daemon, locate_ns, config from . import PYRO_MSGBUS_NAME from .messagebus import make_messagebus, MessageBus config.COMMTIMEOUT = 20.0 config.POLLTIMEOUT = 10.0 config.MAX_MESSAGE_SIZE = 256*1024 # 256 kb config.MAX_RETRIES = 3 if __name__ == "__main__": parser = OptionParser() parser.add_option("-n", "--host", dest="host", default="localhost", help="hostname to bind server on") parser.add_option("-p", "--port", dest="port", type="int", default=0, help="port to bind server on (0=random)") parser.add_option("-u", "--unixsocket", help="Unix domain socket name to bind server on") parser.add_option("-s", "--storage", dest="storage", type="choice", choices=["sqlite", "memory"], default="sqlite", help="storage type (default=%default)") options, args = parser.parse_args() make_messagebus.storagetype = options.storage daemon = Daemon(host=options.host, port=options.port, unixsocket=options.unixsocket) uri = daemon.register(MessageBus) print("Pyro Message Bus.") print(" uri =", uri) ns = locate_ns() ns.register(PYRO_MSGBUS_NAME, uri) print(" name =", PYRO_MSGBUS_NAME) print("Server running, storage is {}.".format(make_messagebus.storagetype)) daemon.requestLoop() Pyro5-5.15/examples/messagebus/publisher.py000066400000000000000000000014371451404116400210120ustar00rootroot00000000000000""" This is the publisher meant for the 'weather' messages example. """ import time import random import sys import Pyro5.errors from Pyro5.api import Proxy from messagebus import PYRO_MSGBUS_NAME sys.excepthook = Pyro5.errors.excepthook location = input("Give city or country to use as location: ").strip() or 'Amsterdam' bus = Proxy("PYRONAME:"+PYRO_MSGBUS_NAME) bus.add_topic("weather-forecast") while True: time.sleep(0.01) forecast = (location, random.choice(["sunny", "cloudy", "storm", "rainy", "hail", "thunder", "calm", "mist", "cold", "hot"])) print("Forecast:", forecast) try: bus.send("weather-forecast", forecast) except Pyro5.errors.CommunicationError: print("connection to the messagebus is lost, reconnecting...") bus._pyroReconnect() Pyro5-5.15/examples/messagebus/subscriber.py000066400000000000000000000023301451404116400211510ustar00rootroot00000000000000""" This is a subscriber meant for the 'weather' messages example. It uses a callback to process incoming messages. """ import sys import Pyro5.errors from Pyro5.api import expose, Daemon from messagebus.messagebus import Subscriber sys.excepthook = Pyro5.errors.excepthook @expose class Subber(Subscriber): def consume_message(self, topic, message): # This callback-method is called automatically when a message arrives on the bus. print("\nGOT MESSAGE:") print(" topic:", topic) print(" msgid:", message.msgid) print(" created:", message.created) print(" data:", message.data) hostname = input("hostname to bind on (empty=localhost): ").strip() or "localhost" # create a messagebus subscriber that uses automatic message retrieval (via a callback) subber = Subber() d = Daemon(host=hostname) d.register(subber) topics = subber.bus.topics() print("Topics on the bus: ", topics) print("Subscribing to weather-forecast.") subber.bus.subscribe("weather-forecast", subber) # note: we subscribe on the bus *after* registering the subber as a Pyro object # this results in Pyro automatically making a proxy for the subber print("Subscribed on weather-forecast") d.requestLoop() Pyro5-5.15/examples/messagebus/subscriber_manual_consume.py000066400000000000000000000044221451404116400242430ustar00rootroot00000000000000""" This is a subscriber meant for the 'weather' messages example. It uses a custom code loop to get and process messages. """ import sys import threading import time import Pyro5.errors from Pyro5.api import expose, Daemon from messagebus.messagebus import Subscriber sys.excepthook = Pyro5.errors.excepthook @expose class Subber(Subscriber): def consume_message(self, topic, message): # In this case, this consume message method is called by our own code loop. print("\nPROCESSING MESSAGE:") print(" topic:", topic) print(" msgid:", message.msgid) print(" created:", message.created) print(" data:", message.data) def manual_message_loop(self): print("Entering manual message processing loop (5 messages).") processed = 0 while processed < 5: time.sleep(0.5) print("\nApprox. number of received messages:", self.received_messages.qsize()) topic, message = self.received_messages.get() # get a message from the queue (they are put there by the Pyro messagebus) self.consume_message(topic, message) processed += 1 print("\nEnd.") hostname = input("hostname to bind on (empty=localhost): ").strip() or "localhost" # create a messagebus subscriber that uses manual message retrieval (via explicit call) # because we're doing the message loop ourselves, the Pyro daemon has to run in a separate thread subber = Subber(auto_consume=False) d = Daemon(host=hostname) d.register(subber) daemon_thread = threading.Thread(target=d.requestLoop) daemon_thread.daemon = True daemon_thread.start() topics = subber.bus.topics() print("Topics on the bus: ", topics) print("Subscribing to weather-forecast.") subber.bus.subscribe("weather-forecast", subber) # note: we subscribe on the bus *after* registering the subber as a Pyro object # this results in Pyro automatically making a proxy for the subber print("Subscribed on weather-forecast") # run the manual message loop print("Entering message loop, you should see the msg count increasing.") subber.manual_message_loop() subber.bus.unsubscribe("weather-forecast", subber) print("Unsubscribed from the topic.") print("Entering message loop again, you should see the msg count decrease.") subber.manual_message_loop() Pyro5-5.15/examples/nameserverstress/000077500000000000000000000000001451404116400177135ustar00rootroot00000000000000Pyro5-5.15/examples/nameserverstress/Readme.txt000066400000000000000000000002451451404116400216520ustar00rootroot00000000000000This example contains a stress test for the Naming Server. It creates a bunch of threads that connect to the NS and create/delete registrations randomly, very fast. Pyro5-5.15/examples/nameserverstress/stress.py000066400000000000000000000042411451404116400216110ustar00rootroot00000000000000import sys import random import time import threading import contextlib from Pyro5.api import Proxy, locate_ns import Pyro5.errors def randomname(): def partname(): return str(random.random())[-2:] parts = ["stresstest"] for i in range(random.randint(1, 10)): parts.append(partname()) return ".".join(parts) class NamingTrasher(threading.Thread): def __init__(self, nsuri, number): threading.Thread.__init__(self) self.daemon = True self.number = number self.ns = Proxy(nsuri) self.mustStop = False def list(self): items = self.ns.list() def register(self): for i in range(4): with contextlib.suppress(Pyro5.errors.NamingError): self.ns.register(randomname(), 'PYRO:objname@host:555') def remove(self): self.ns.remove(randomname()) def lookup(self): with contextlib.suppress(Pyro5.errors.NamingError): uri = self.ns.lookup(randomname()) def listprefix(self): entries = self.ns.list(prefix="stresstest.51") def listregex(self): entries = self.ns.list(regex=r"stresstest\.??\.41.*") def run(self): print("Name Server trasher running.") self.ns._pyroClaimOwnership() while not self.mustStop: random.choice((self.list, self.register, self.remove, self.lookup, self.listregex, self.listprefix))() sys.stdout.write("%d " % self.number) sys.stdout.flush() time.sleep(0.001) print("Trasher exiting.") def main(): threads = [] ns = locate_ns() print("Removing previous stresstest registrations...") ns.remove(prefix="stresstest.") print("Done. Starting.") for i in range(5): nt = NamingTrasher(ns._pyroUri, i) nt.start() threads.append(nt) try: while True: time.sleep(1) except KeyboardInterrupt: pass print("Break-- waiting for threads to stop.") for nt in threads: nt.mustStop = True nt.join() count = ns.remove(prefix="stresstest.") print("cleaned up %d names." % count) if __name__ == '__main__': main() Pyro5-5.15/examples/nonameserver/000077500000000000000000000000001451404116400170045ustar00rootroot00000000000000Pyro5-5.15/examples/nonameserver/Readme.txt000066400000000000000000000004231451404116400207410ustar00rootroot00000000000000This example shows a way to use Pyro without a Name server. Look at the simplicity of the client. The only thing you need to figure out is how to get the correct URI in the client. This example just lets you enter it on the console. You can copy it from the server's output. Pyro5-5.15/examples/nonameserver/client.py000066400000000000000000000004111451404116400206300ustar00rootroot00000000000000# Client that doesn't use the Name Server. Uses URI directly. from Pyro5.api import Proxy uri = input("Enter the URI of the quote object: ") with Proxy(uri) as quotegen: print("Getting some quotes...") print(quotegen.quote()) print(quotegen.quote()) Pyro5-5.15/examples/nonameserver/server.py000066400000000000000000000015551451404116400206720ustar00rootroot00000000000000# The server that doesn't use the Name Server. import os from Pyro5.api import expose, Daemon class QuoteGen(object): @expose def quote(self): try: quote = os.popen('fortune').read() if len(quote) > 0: return quote return "This system cannot provide you a good fortune, install 'fortune'" except Exception: return "This system knows no witty quotes :-(" with Daemon() as daemon: quote1 = QuoteGen() quote2 = QuoteGen() uri1 = daemon.register(quote1) # let Pyro create a unique name for this one uri2 = daemon.register(quote2, "example.quotegen") # provide a logical name ourselves print("QuoteGen is ready, not using the Name Server.") print("You can use the following two URIs to connect to me:") print(uri1) print(uri2) daemon.requestLoop() Pyro5-5.15/examples/ns-metadata/000077500000000000000000000000001451404116400164765ustar00rootroot00000000000000Pyro5-5.15/examples/ns-metadata/Readme.txt000066400000000000000000000004401451404116400204320ustar00rootroot00000000000000This example shows the metadata capabilities of the name server and the use of the PYROMETA magic uri protocol. Before running the example.py, make sure the name server is running. You should choose an appropriate storage type for the name server (dbm storage doesn't support metadata). Pyro5-5.15/examples/ns-metadata/example.py000066400000000000000000000072001451404116400205020ustar00rootroot00000000000000from Pyro5.api import locate_ns, Daemon, type_meta, Proxy from resources import LaserPrinter, MatrixPrinter, PhotoPrinter, TapeStorage, DiskStorage, Telephone, Faxmachine # register various objects with some metadata describing their resource class ns = locate_ns() d = Daemon() uri = d.register(LaserPrinter) ns.register("example.resource.laserprinter", uri, metadata=type_meta(LaserPrinter) | {"resource:printer", "performance:fast"}) uri = d.register(MatrixPrinter) ns.register("example.resource.matrixprinter", uri, metadata=type_meta(MatrixPrinter) | {"resource:printer", "performance:slow"}) uri = d.register(PhotoPrinter) ns.register("example.resource.photoprinter", uri, metadata=type_meta(PhotoPrinter) | {"resource:printer", "performance:slow"}) uri = d.register(TapeStorage) ns.register("example.resource.tapestorage", uri, metadata=type_meta(TapeStorage) | {"resource:storage", "performance:slow"}) uri = d.register(DiskStorage) ns.register("example.resource.diskstorage", uri, metadata=type_meta(DiskStorage) | {"resource:storage", "performance:fast"}) uri = d.register(Telephone) ns.register("example.resource.telephone", uri, metadata=type_meta(Telephone) | {"resource:communication"}) uri = d.register(Faxmachine) ns.register("example.resource.faxmachine", uri, metadata=type_meta(Faxmachine) | {"resource:communication"}) # check that the name server is actually capable of storing metadata uri, metadata = ns.lookup("example.resource.laserprinter", return_metadata=True) if not metadata: raise NameError("The name server doesn't support storing metadata. Check its storage type.") # list all registrations with their metadata entries = ns.list(return_metadata=True) for name in entries: uri, metadata = entries[name] print(name) print(" uri:", uri) print(" meta:", ", ".join(metadata)) print() # query for various metadata via yplookup (Yellow-Pages type lookup) print("\nall storage:") devices = ns.yplookup(meta_all={"resource:storage"}) for name, uri in devices.items(): print(" {} -> {}".format(name, uri)) print("\nall FAST printers:") devices = ns.yplookup(meta_all={"resource:printer", "performance:fast"}) for name, uri in devices.items(): print(" {} -> {}".format(name, uri)) print("\nall storage OR communication devices :") devices = ns.yplookup(meta_any={"resource:storage", "resource:communication"}) for name, uri in devices.items(): print(" {} -> {}".format(name, uri)) # upgrade the photo printer uri, meta = ns.lookup("example.resource.photoprinter", return_metadata=True) meta = set(meta) meta.discard("performance:slow") meta.add("performance:fast") ns.set_metadata("example.resource.photoprinter", meta) print("\nall FAST printers (after photoprinter upgrade):") devices = ns.yplookup(meta_all={"resource:printer", "performance:fast"}) for name, uri in devices.items(): print(" {} -> {}".format(name, uri)) print("\nall resource types:") devices = ns.yplookup(meta_all={"class:resources.Resource"}) for name, uri in devices.items(): print(" {} -> {}".format(name, uri)) print("\n\nPYROMETA protocol for easy yellow-pages lookup:\n") nameserver = Proxy("PYROMETA:class:Pyro5.nameserver.NameServer") print("Proxy to look up 'any nameserver' via its class metadata:") print(" ", nameserver) nameserver._pyroBind() print("Proxy for 'any namesever' bound to candidate:") print(" ", nameserver._pyroUri) printer = Proxy("PYROMETA:resource:printer,performance:slow") print("Proxy for 'any slow printer':") print(" ", printer) print("(this example doesn't actually implement these objects so we leave it at that)") Pyro5-5.15/examples/ns-metadata/resources.py000066400000000000000000000006241451404116400210640ustar00rootroot00000000000000from Pyro5.server import expose class Resource(object): pass @expose class LaserPrinter(Resource): pass @expose class MatrixPrinter(Resource): pass @expose class PhotoPrinter(Resource): pass @expose class TapeStorage(Resource): pass @expose class DiskStorage(Resource): pass @expose class Telephone(Resource): pass @expose class Faxmachine(Resource): pass Pyro5-5.15/examples/oneway/000077500000000000000000000000001451404116400156025ustar00rootroot00000000000000Pyro5-5.15/examples/oneway/Readme.txt000066400000000000000000000015651451404116400175470ustar00rootroot00000000000000This example shows the use of 'oneway' method calls. If you flag a method call 'oneway' (with the oneway decorator), Pyro will not wait for a response from the remote object. This means that your client program can continue to work, while the remote object is still busy processing the method call. (Normal remote method calls are synchronous and will always block until the remote object is done with the method call). Oneway method calls are executed in their own separate thread, so the server remains responsive for additional calls from the same client even when the oneway call is still running. This may lead to complex behavior in the server because a new thread is created to handle the request regardless of the type of server you are running. But it does allow the client proxy to continue after doing subsequent oneway calls, as you might expect it to behave with these. Pyro5-5.15/examples/oneway/client.py000066400000000000000000000020211451404116400174250ustar00rootroot00000000000000import time from Pyro5.api import Proxy with Proxy("PYRONAME:example.oneway") as serv: print("starting server using a oneway call") serv.oneway_start(6) print("doing some more oneway calls inbetween (this should be finished really quick)") serv.nothing() serv.nothing() serv.nothing() serv.nothing() print("oneway calls done, this should have taken almost no time.") time.sleep(2) print("\nNow contacting the server to see if it's done.") print("we are faster, so you should see a few attempts,") print("until the server is finished.") while True: print("server done?") if serv.ready(): print("yes!") break else: print("no, trying again") time.sleep(1) print("getting the result from the server: %s" % serv.result()) print("\nCalling oneway work method, server will continue working while we are done " "(quickly check the server console output now to see it running!).") serv.oneway_work() Pyro5-5.15/examples/oneway/client2.py000066400000000000000000000015751451404116400175240ustar00rootroot00000000000000import time from Pyro5.api import Proxy with Proxy("PYRONAME:example.oneway2") as serv: print("incrementing a few times normally") serv.increment() serv.increment() serv.increment() counter = serv.getcount() print("counter in server is now: ", counter) print("incrementing a few times via oneway call (should be almost instantaneous)") serv.increment_oneway() serv.increment_oneway() serv.increment_oneway() counter2 = serv.getcount() print("counter is now: ", counter2) if counter2 == counter: print("ok, the oneway calls are still being processed in the background.") print(" we'll wait a bit till they're done...") time.sleep(2) counter2 = serv.getcount() print("counter is now: ", counter2) else: raise SystemExit("!? the oneway calls have not been processed in the background!?") Pyro5-5.15/examples/oneway/server.py000066400000000000000000000015441451404116400174660ustar00rootroot00000000000000import time from Pyro5.api import expose, oneway, serve @expose class Server(object): def __init__(self): self.busy = False @oneway def oneway_start(self, duration): print("start request received. Starting work...") self.busy = True for i in range(duration): time.sleep(1) print(duration - i) print("work is done!") self.busy = False def ready(self): print("ready status requested (%r)" % (not self.busy)) return not self.busy def result(self): return "The result :)" def nothing(self): print("nothing got called, doing nothing") @oneway def oneway_work(self): for i in range(10): print("work work..", i+1) time.sleep(1) print("work's done!") serve({ Server: "example.oneway" }) Pyro5-5.15/examples/oneway/server2.py000066400000000000000000000010701451404116400175420ustar00rootroot00000000000000import time import threading from Pyro5.api import expose, oneway, behavior, serve @expose @behavior("single") class Server(object): def __init__(self): self.counter = 0 @oneway def increment_oneway(self): print("oneway call executing in thread", threading.get_ident()) time.sleep(0.5) self.counter += 1 def increment(self): time.sleep(0.5) self.counter += 1 def getcount(self): return self.counter print("main thread:", threading.get_ident()) serve({ Server: "example.oneway2" }) Pyro5-5.15/examples/privilege-separation/000077500000000000000000000000001451404116400204315ustar00rootroot00000000000000Pyro5-5.15/examples/privilege-separation/Readme.txt000066400000000000000000000025431451404116400223730ustar00rootroot00000000000000An example of using Pyro to implement a form of privilege separation http://en.wikipedia.org/wiki/Privilege_separation One server provides an api to access a part of the system by regular client code that would normally require elevated privileges. In this case you only need to run the confined server with elevated privileges, and the client code can run with normal user level privileges. Such a server should only expose just that tiny bit of functionality that requires the elevated privileges. This then avoids having to run the entire program (the client in this case) as an elevated user. The other example does the opposite: the server drops privileges after it started to voluntarily make it more restrictive in what it can do. This is used to mitigate the potential damage of a computer security vulnerability. The way it's done here is rather primitive, but it works. It just switches uid/gid to 'nobody'. If you're using Linux, there are more sophisticated ways to do this. For example see https://deescalate.readthedocs.io/en/latest/ The example client calls the server to perform an operation but the dropped privileges will now prevent the server from carrying out the request if it is not allowed to do so. This example was developed on a Linux system. It should work on MacOS as well, but is not compatible with Windows. You can still get the idea though. Pyro5-5.15/examples/privilege-separation/drop_privs_client.py000066400000000000000000000005111451404116400245250ustar00rootroot00000000000000import Pyro5.api uri = input("Enter the uri of the restricted server: ") restricted = Pyro5.api.Proxy(uri) print("server is running as:", restricted.who_is_server()) print("attempting to write a file:") try: restricted.write_file() print("???? this should fail!") except OSError as x: print("ERROR (expected):", x) Pyro5-5.15/examples/privilege-separation/drop_privs_server.py000066400000000000000000000024651451404116400245670ustar00rootroot00000000000000import os import pwd import Pyro5.api class RestrictedService: @Pyro5.api.expose def who_is_server(self): return os.getuid(), os.getgid(), pwd.getpwuid(os.getuid()).pw_name @Pyro5.api.expose def write_file(self): # this should fail ("permission denied") because of the dropped privileges with open("dummy-test-file.bin", "w"): pass class RestrictedDaemon(Pyro5.api.Daemon): def __init__(self): super().__init__() print("Server started as:") print(" uid/gid", os.getuid(), os.getgid()) print(" euid/egid", os.geteuid(), os.getegid()) self.drop_privileges("nobody") def drop_privileges(self, user): nobody = pwd.getpwnam(user) try: os.setgid(nobody.pw_uid) os.setuid(nobody.pw_gid) except OSError: print("Failed to drop privileges. You'll have to start this program as root to be able to do this.") raise print("Privileges dropped. Server now running as", user) print(" uid/gid", os.getuid(), os.getgid()) print(" euid/egid", os.geteuid(), os.getegid()) if __name__ == "__main__": rdaemon = RestrictedDaemon() Pyro5.api.serve({ RestrictedService: "restricted" }, host="localhost", daemon=rdaemon, use_ns=False) Pyro5-5.15/examples/privilege-separation/elevated_client.py000066400000000000000000000010271451404116400241320ustar00rootroot00000000000000import Pyro5.api uri = input("Enter the uri of the dmesg server: ") dmesg = Pyro5.api.Proxy(uri) try: print("Last few lines of the dmesg kernel buffer:\n") lines = dmesg.dmesg() print("\n".join(lines)) print("\nNormally you can't read this info from a non-root user. " "But the server is running as root and is able to access it for you.") print("TIP: now kill the server if you no longer need it!") except Exception as x: print("ERROR:", x) print("Is the server running with root privileges?") Pyro5-5.15/examples/privilege-separation/elevated_server.py000066400000000000000000000020731451404116400241640ustar00rootroot00000000000000import os import subprocess import Pyro5.api class DmesgServer: @Pyro5.api.expose def dmesg(self): # reading last 20 lines of the kernel's dmesg buffer... (requires root privilege) try: result = subprocess.check_output(["dmesg", "--nopager", "--level", "info"]) return result.decode().splitlines()[-20:] except subprocess.SubprocessError as x: raise OSError("couldn't run the dmesg command in the server: " + str(x)) if __name__ == "__main__": print("Server is running as:") print(" uid/gid", os.getuid(), os.getgid()) print(" euid/egid", os.geteuid(), os.getegid()) if os.getuid() != 0: print("Warning: lacking root privileges to run the 'dmesg' command to read the kernel's buffer. " "Executing the command will fail. For the desired outcome, run this program as root.") else: print("Running as root. This is okay as we're just running the 'dmesg' command for you.") Pyro5.api.serve({ DmesgServer: "dmesg" }, host="localhost", use_ns=False) Pyro5-5.15/examples/resourcetracking/000077500000000000000000000000001451404116400176525ustar00rootroot00000000000000Pyro5-5.15/examples/resourcetracking/Readme.txt000066400000000000000000000007641451404116400216170ustar00rootroot00000000000000This example shows how you can utilize the resource tracking feature to properly close allocated resources when a client connection gets closed forcefully before the client can properly free the resources itself. The client allocates and frees some (fictional) resources. The server registers them as tracked resources for the current client connection. If you kill the client before it can cleanly free the resources, Pyro will free them for you as soon as the connection to the server is closed. Pyro5-5.15/examples/resourcetracking/client.py000066400000000000000000000012261451404116400215030ustar00rootroot00000000000000import random from Pyro5.api import Proxy uri = input("Enter the URI of the server object: ") with Proxy(uri) as proxy: print("currently allocated resources:", proxy.list()) name1 = hex(random.randint(0, 999999))[-4:] name2 = hex(random.randint(0, 999999))[-4:] print("allocating resource...", name1) proxy.allocate(name1) print("allocating resource...", name2) proxy.allocate(name2) input("\nhit Enter now to continue normally or ^C/break to abort the connection forcefully:") print("free resources normally...") proxy.free(name1) proxy.free(name2) print("allocated resources:", proxy.list()) print("done.") Pyro5-5.15/examples/resourcetracking/server.py000066400000000000000000000035731451404116400215420ustar00rootroot00000000000000from Pyro5.api import Daemon, serve, expose, behavior, current_context class CustomDaemon(Daemon): def clientDisconnect(self, conn): # If required, you *can* override this to do custom resource freeing. # But this is not needed if your resource objects have a proper 'close' method; # this method is called by Pyro itself once the client connection gets closed. # In this example this override is only used to print out some info. print("client disconnects:", conn.sock.getpeername()) print(" resources: ", [r.name for r in conn.tracked_resources]) class Resource(object): # a fictional resource that gets allocated and must be freed again later. def __init__(self, name, collection): self.name = name self.collection = collection def close(self): # Pyro will call this on a tracked resource once the client's connection gets closed! # (Unless the resource can be carbage collected normally by Python.) print("Resource: closing", self.name) self.collection.discard(self) @expose @behavior(instance_mode="single") class Service(object): def __init__(self): self.resources = set() # the allocated resources def allocate(self, name): resource = Resource(name, self.resources) self.resources.add(resource) current_context.track_resource(resource) print("service: allocated resource", name, " for client", current_context.client_sock_addr) def free(self, name): resources = {r for r in self.resources if r.name == name} self.resources -= resources for r in resources: r.close() current_context.untrack_resource(r) def list(self): return [r.name for r in self.resources] with CustomDaemon() as daemon: serve({ Service: "service" }, use_ns=False, daemon=daemon) Pyro5-5.15/examples/robots/000077500000000000000000000000001451404116400156105ustar00rootroot00000000000000Pyro5-5.15/examples/robots/Readme.txt000066400000000000000000000032371451404116400175530ustar00rootroot00000000000000This is an example that more or less presents an online multiplayer game. The game is a robot destruction derby. It is played on a grid. There are some obstructing walls on the grid that hurt when you collide into them. If you collide into another robot, the other robot takes damage. All robots start with a certain amount of health. If it reaches zero, the robot dies. The last man standing wins! Before starting the gameserver, you need to start a nameserver, if you want to connect remotely to the game server! If you don't have a nameserver running, you can still launch the gameserver but you won't be able to connect to it with the Pyro clients. You can click a button to add a couple of robots that are controlled by the server itself. But it is more interesting to actually connect remote robots to the server! Use client.py for that (provide a name and a robot type). The client supports a few robot types that have different behaviors. The robot behavior is controlled by the client! The server only handles game mechanics. In the game server, the Pyro calls are handled by a daemon thread. The GUI updates are done by Tkinter using after() calls. The most interesting parts of this example are perhaps these: - server uses identical code to work with local and remote robots (it did require a few minor tweaks to work around serialization requirements) - Pyro used together with an interactive GUI application (Tkinter) - game state handled by the server, influenced by the clients (robot behavior) - this example uses Pyro's AutoProxy feature. Registering observers and getting a robot object back is done via proxies automatically because those are Pyro objects. Pyro5-5.15/examples/robots/client.py000066400000000000000000000062231451404116400174430ustar00rootroot00000000000000import sys import random from Pyro5.api import expose, oneway, Daemon, Proxy, config, register_dict_to_class, register_class_to_dict import robot import remote config.SERVERTYPE = "multiplex" # to make sure all calls run in the same thread class DrunkenGameObserver(remote.GameObserver): @oneway @expose def world_update(self, iteration, world, robotdata): # change directions randomly if random.random() > 0.8: self.robot._pyroClaimOwnership() # lets our thread do the proxy calls if random.random() >= 0.5: dx, dy = random.randint(-1, 1), 0 else: dx, dy = 0, random.randint(-1, 1) if random.random() > 0.7: self.robot.emote("..Hic! *burp*") self.robot.change_direction((dx, dy)) class AngryGameObserver(remote.GameObserver): def __init__(self): super(AngryGameObserver, self).__init__() self.directions = [(1, 0), (0, 1), (-1, 0), (0, -1)] # clockwise motion self.directioncounter = 0 @oneway @expose def world_update(self, iteration, world, robotdata): # move in a loop yelling angry stuff self.robot._pyroClaimOwnership() # lets our thread do the proxy calls if iteration % 50 == 0: self.robot.emote("I'll kill you all! GRR") if iteration % 10 == 0: self.directioncounter = (self.directioncounter + 1) % 4 self.robot.change_direction(self.directions[self.directioncounter]) class ScaredGameObserver(remote.GameObserver): def __init__(self): super(ScaredGameObserver, self).__init__() # run to a corner self.direction = random.choice([(-1, -1), (1, -1), (1, 1), (-1, 1)]) @oneway @expose def start(self): super(ScaredGameObserver, self).start() self.robot.change_direction(self.direction) @oneway @expose def world_update(self, iteration, world, robotdata): if iteration % 50 == 0: self.robot._pyroClaimOwnership() # lets our thread do the proxy calls self.robot.emote("I'm scared!") observers = { "drunk": DrunkenGameObserver, "angry": AngryGameObserver, "scared": ScaredGameObserver, } # register the Robot class with Pyro's serializers: register_class_to_dict(robot.Robot, robot.Robot.robot_to_dict) register_dict_to_class("robot.Robot", robot.Robot.dict_to_robot) def main(args): if len(args) != 3: print("usage: client.py ") print(" type is one of: %s" % list(observers.keys())) return name = args[1] observertype = args[2] with Daemon() as daemon: observer = observers[observertype]() daemon.register(observer) gameserver = Proxy("PYRONAME:example.robotserver") robot = gameserver.register(name, observer) with robot: # make sure it disconnects, before the daemon thread uses it later robot.emote("Hi there! I'm here to kick your ass") observer.robot = robot print("Pyro server registered on %s" % daemon.locationStr) daemon.requestLoop() if __name__ == "__main__": main(sys.argv) Pyro5-5.15/examples/robots/gameserver.py000066400000000000000000000243061451404116400203270ustar00rootroot00000000000000import random import time import threading from tkinter import * import Pyro5.errors import Pyro5.api import robot import remote class VisibleRobot(robot.Robot): """represents a robot that is visible on the screen.""" def __init__(self, name, position, direction, grid, color='red'): super(VisibleRobot, self).__init__(name, (grid.width, grid.height), position, direction) self.grid = grid x = self.x * grid.squaresize y = self.y * grid.squaresize self.tkid = grid.create_rectangle(x, y, x + grid.squaresize, y + grid.squaresize, fill=color, outline='black') self.text_tkid = None def popuptext(self, text, sticky=False): if self.text_tkid: self.grid.delete(self.text_tkid) self.text_tkid = self.grid.create_text(self.x * self.grid.squaresize, self.y * self.grid.squaresize, text=text, anchor=CENTER, fill='red') self.text_timer = time.time() if not sticky: self.grid.after(1000, self.__removetext, self.text_tkid) def delete_from_grid(self): self.grid.delete(self.tkid) if self.text_tkid: self.grid.delete(self.text_tkid) def __removetext(self, textid): self.grid.delete(textid) if textid == self.text_tkid: self.text_tkid = None def move(self, world=None): super(VisibleRobot, self).move(world) x = self.x * self.grid.squaresize y = self.y * self.grid.squaresize self.grid.coords(self.tkid, x, y, x + self.grid.squaresize, y + self.grid.squaresize) if self.text_tkid: # also move the popup text self.grid.coords(self.text_tkid, self.x * self.grid.squaresize, self.y * self.grid.squaresize) def died(self, killer, world): self.popuptext("ARGH I died") self.observer.death(killer=killer) self.grid.after(800, lambda: self.grid.delete(self.tkid)) def collision(self, other): self.popuptext("Bam!") other.popuptext("ouch") def emote(self, text): self.popuptext(text, False) class RobotGrid(Canvas): def __init__(self, parent, width, height, squaresize=20): self.squaresize = squaresize self.width = width self.height = height pixwidth = width * self.squaresize pixheight = height * self.squaresize Canvas.__init__(self, parent, width=pixwidth, height=pixheight, background='#e0e0e0') self.xview_moveto(0) self.yview_moveto(0) for x in range(width): self.create_line(x * self.squaresize, 0, x * self.squaresize, pixheight, fill='#d0d0d0') for y in range(height): self.create_line(0, y * self.squaresize, pixwidth, y * self.squaresize, fill='#d0d0d0') def draw_wall(self, wall, color='navy'): x = wall.x * self.squaresize y = wall.y * self.squaresize self.create_rectangle(x, y, x + self.squaresize, y + self.squaresize, fill=color, outline=color) class GameEngine(object): def __init__(self, gui, world): self.gui = gui self.grid = gui.grid self.world = world self.build_walls() self.gui.buttonhandler = self self.survivor = None self.open_for_signups = True self.iteration = 0 def button_clicked(self, button): if button == "add_bot" and self.open_for_signups: for i in range(5): name = "local_bot_%d" % self.gui.listbox.size() gameobserver = remote.LocalGameObserver(name) robot = self.signup_robot(name, gameobserver) gameobserver.robot = robot elif button == "start_round": self.open_for_signups = False if self.survivor: self.survivor.delete_from_grid() self.gui.enable_buttons(False) self.start_round() def start_round(self): self.gui.statuslabel.config(text="new round!") print("WORLD:") for line in self.world.dump(): print("|", line.decode(), "|") print("NUMBER OF ROBOTS: %d" % len(self.world.robots)) # make sure the observers of all robots are owned by this thread (if they're a Pyro proxy) for robot in self.world.robots: if isinstance(robot.observer, Pyro5.api.Proxy): robot.observer._pyroClaimOwnership() txtid = self.grid.create_text(20, 20, text="GO!", font=("Courier", 120, "bold"), anchor=NW, fill='purple') self.grid.after(1500, lambda: self.grid.delete(txtid)) self.grid.after(2000, self.update) self.grid.after(2000, self.notify_start) self.iteration = 0 def notify_start(self): for robot in self.world.robots: robot.observer.start() def notify_worldupdate(self): self.iteration += 1 for robot in self.world.robots: robot.observer.world_update(self.iteration, self.world, robot) def notify_winner(self, winner): winner.observer.victory() def update(self): for robot in self.world.robots: robot.move(self.world) self.notify_worldupdate() self.gui.statuslabel.config(text="survivors: %d" % len(self.world.robots)) if len(self.world.robots) < 1: print("[server] No results.") self.round_ends() elif len(self.world.robots) == 1: self.survivor = self.world.robots[0] self.world.remove(self.survivor) self.survivor.popuptext("I WIN! HURRAH!", True) print("[server] %s wins!" % self.survivor.name) self.gui.statuslabel.config(text="winner: %s" % self.survivor.name) self.notify_winner(self.survivor) self.round_ends() else: self.gui.tk.after(40, self.update) def round_ends(self): self.gui.listbox.delete(0, END) self.gui.enable_buttons(True) self.open_for_signups = True def build_walls(self): wall_offset = 4 wall_size = 10 for x in range(wall_size): wall = robot.Wall((x + wall_offset, wall_offset)) self.world.add_wall(wall) self.grid.draw_wall(wall) wall = robot.Wall((x + wall_offset, wall_size + wall_offset + 1)) self.world.add_wall(wall) self.grid.draw_wall(wall) wall = robot.Wall((wall_offset, x + wall_offset + 1)) self.world.add_wall(wall) self.grid.draw_wall(wall) wall = robot.Wall((wall_size + wall_offset + 2, x + wall_offset + 1)) self.world.add_wall(wall) self.grid.draw_wall(wall) def signup_robot(self, name, observer=None): if not self.open_for_signups: raise RuntimeError("signups are closed, try again later") for robot in self.world.robots: if robot.name == name: raise ValueError("that name is already taken") colorint = random.randint(0, 0xFFFFFF) color = '#%06x' % colorint inversecolor = 'black' self.gui.listbox.insert(END, name) self.gui.listbox.itemconfig(END, bg=color, fg=inversecolor) while True: x = random.randint(0, self.grid.width - 1) y = random.randint(int(self.grid.height * 0), self.grid.height - 1) if not self.world.collides(x, y): break robot = VisibleRobot(name, (x, y), (0, 0), self.grid, color=color) self.world.add_robot(robot) robot.observer = observer robot.popuptext(name) return remote.RemoteBot(robot, self) def remove_robot(self, robot): robot.delete_from_grid() self.world.remove(robot) # listnames=list(self.gui.listbox.get(0,END)) # listnames.remove(robot.name) # self.gui.listbox.delete(0,END) # self.gui.listbox.insert(END,*listnames) class GUI(object): def __init__(self, width, height): self.tk = Tk() self.tk.wm_title("bot destruction derby") lframe = Frame(self.tk, borderwidth=3, relief="raised", padx=2, pady=2, background='#808080') self.grid = RobotGrid(lframe, width, height, squaresize=16) rframe = Frame(self.tk, padx=2, pady=2) rlabel = Label(rframe, text="Signups:") rlabel.pack(fill=X) self.listbox = Listbox(rframe, width=15, height=20, font=(None, 8)) self.listbox.pack() self.addrobotbutton = Button(rframe, text="Add 5 local bots", command=lambda: self.buttonhandler.button_clicked("add_bot")) self.addrobotbutton.pack() self.startbutton = Button(rframe, text="Start round!", command=lambda: self.buttonhandler.button_clicked("start_round")) self.startbutton.pack() self.statuslabel = Label(rframe, width=20) self.statuslabel.pack(side=BOTTOM) self.grid.pack() lframe.pack(side=LEFT) rframe.pack(side=RIGHT, fill=BOTH) self.buttonhandler = None def enable_buttons(self, enabled=True): if enabled: self.addrobotbutton.config(state=NORMAL) self.startbutton.config(state=NORMAL) else: self.addrobotbutton.config(state=DISABLED) self.startbutton.config(state=DISABLED) class PyroDaemonThread(threading.Thread): def __init__(self, engine): threading.Thread.__init__(self) self.pyroserver = remote.GameServer(engine) self.pyrodaemon = Pyro5.api.Daemon() self.daemon = True def run(self): ns = Pyro5.api.locate_ns() with self.pyrodaemon: with ns: uri = self.pyrodaemon.register(self.pyroserver) ns.register("example.robotserver", uri) print("Pyro server registered on %s" % self.pyrodaemon.locationStr) self.pyrodaemon.requestLoop() # register the Robot class with Pyro's serializers: Pyro5.api.SerializerBase.register_class_to_dict(VisibleRobot, robot.Robot.robot_to_dict) def main(): width = 25 height = 25 gui = GUI(width, height) world = robot.World(width, height) engine = GameEngine(gui, world) try: PyroDaemonThread(engine).start() except Pyro5.errors.NamingError: print("Can't find the Pyro Nameserver. Running without remote connections.") gui.tk.mainloop() if __name__ == "__main__": main() Pyro5-5.15/examples/robots/remote.py000066400000000000000000000035051451404116400174600ustar00rootroot00000000000000import random from Pyro5.api import expose, oneway @expose class GameServer(object): def __init__(self, engine): self.engine = engine def register(self, name, observer): robot = self.engine.signup_robot(name, observer) self._pyroDaemon.register(robot) # make the robot a pyro object return robot @expose class RemoteBot(object): def __init__(self, robot, engine): self.robot = robot self.engine = engine def get_data(self): return self.robot def change_direction(self, direction): self.robot.dx, self.robot.dy = direction def emote(self, text): self.robot.emote(text) def terminate(self): self.engine.remove_robot(self.robot) @expose class LocalGameObserver(object): def __init__(self, name): self.name = name self.robot = None @oneway def world_update(self, iteration, world, robotdata): # change directions randomly if random.random() > 0.8: if random.random() >= 0.5: dx, dy = random.randint(-1, 1), 0 else: dx, dy = 0, random.randint(-1, 1) self.robot.change_direction((dx, dy)) def start(self): self.robot.emote("Here we go!") def victory(self): print("[%s] I WON!!!" % self.name) def death(self, killer): if killer: print("[%s] I DIED (%s did it)" % (self.name, killer.name)) else: print("[%s] I DIED" % self.name) @expose class GameObserver(object): def world_update(self, iteration, world, robotdata): pass def start(self): print("Battle starts!") def victory(self): print("I WON!!!") def death(self, killer): print("I DIED") if killer: print("%s KILLED ME :(" % killer.name) Pyro5-5.15/examples/robots/robot.py000066400000000000000000000075151451404116400173170ustar00rootroot00000000000000class Wall(object): """an obstructing static wall""" def __init__(self, position): self.x, self.y = position def __getstate__(self): return self.x, self.y def __setstate__(self, state): self.x, self.y = state class Robot(object): """represents a robot moving on a grid.""" def __init__(self, name, grid_dimensions, position, direction=(0, 0), strength=5): self.name = name self.x, self.y = position self.dx, self.dy = direction self.gridw, self.gridh = grid_dimensions self.strength = strength def __str__(self): return "ROBOT '%s'; pos(%d,%d); dir(%d,%d); strength %d" % (self.name, self.x, self.y, self.dx, self.dy, self.strength) @staticmethod def dict_to_robot(classname, data): assert classname == "robot.Robot" return Robot(data["name"], data["grid_dimensions"], data["position"], data["direction"], data["strength"]) @staticmethod def robot_to_dict(robot): return { "__class__": "robot.Robot", "name": robot.name, "grid_dimensions": (robot.gridw, robot.gridh), "position": (robot.x, robot.y), "direction": (robot.dx, robot.dy), "strength": robot.strength, } def move(self, world=None): # minmax to avoid moving off the sides x = min(self.gridw - 1, max(0, self.x + self.dx)) y = min(self.gridh - 1, max(0, self.y + self.dy)) if x == self.x and y == self.y: return if world and self.__process_collision(x, y, world): return self.x, self.y = x, y def __process_collision(self, newx, newy, world): other = world.collides(newx, newy) if not other: return False # we didn't hit anything self.dx, self.dy = 0, 0 # come to a standstill when we hit something if isinstance(other, Wall): self.strength -= 1 # hit wall, decrease our strength if self.strength <= 0: print("[server] %s killed himself!" % self.name) world.remove(self) self.died(None, world) else: other.strength -= 1 # hit other robot, decrease other robot's strength self.collision(other) if other.strength <= 0: world.remove(other) other.died(self, world) print("[server] %s killed %s!" % (self.name, other.name)) return True def killed(self, victim, world): """you can override this to react on kills""" pass def collision(self, other): """you can override this to react on collisions between bots""" pass def emote(self, text): """you can override this""" print("[server] %s says: '%s'" % (self.name, text)) class World(object): """the world the robots move in (Cartesian grid)""" def __init__(self, width, height): self.width = width self.height = height self.all = [] self.robots = [] def add_wall(self, wall): self.all.append(wall) def add_robot(self, bot): self.all.append(bot) self.robots.append(bot) def collides(self, x, y): for obj in self.all: if obj.x == x and obj.y == y: return obj return None def remove(self, obj): self.all.remove(obj) self.robots.remove(obj) def dump(self): line = b' ' * self.width grid = [bytearray(line) for _ in range(self.height)] for obj in self.all: grid[obj.y][obj.x] = ord('R') if isinstance(obj, Robot) else ord('#') return grid def __getstate__(self): return self.width, self.height, self.all, self.robots def __setstate__(self, state): self.width, self.height, self.all, self.robots = state Pyro5-5.15/examples/servertypes/000077500000000000000000000000001451404116400166735ustar00rootroot00000000000000Pyro5-5.15/examples/servertypes/Readme.txt000066400000000000000000000004111451404116400206250ustar00rootroot00000000000000Shows the different behaviors of Pyro's server types. First start the server, it will ask what type of server you want to run. The client will print some information about what's happening. Try it with different server types and see how that changes the behavior. Pyro5-5.15/examples/servertypes/client.py000066400000000000000000000056071451404116400205330ustar00rootroot00000000000000import time import threading from Pyro5.api import Proxy serv = Proxy("PYRONAME:example.servertypes") print("--------------------------------------------------------------") print(" This part is independent of the type of the server. ") print("--------------------------------------------------------------") print("Calling 5 times oneway method. Should return immediately.") serv.reset() begin = time.time() serv.onewaydelay() serv.onewaydelay() serv.onewaydelay() serv.onewaydelay() serv.onewaydelay() print("Done with the oneway calls.") completed = serv.getcount() print("Number of completed calls in the server: %d" % completed) print(" (this should be 0, because all 5 calls are still busy in the background)") if completed > 0: raise SystemExit("error: oneway calls should run in the background!") print() print("Calling normal delay 5 times. They will all be processed by the same server thread because we're using the same proxy.") r = serv.delay() print(" call processed by: %s" % r) r = serv.delay() print(" call processed by: %s" % r) r = serv.delay() print(" call processed by: %s" % r) r = serv.delay() print(" call processed by: %s" % r) r = serv.delay() print(" call processed by: %s" % r) time.sleep(2) print("Number of completed calls in the server: %d" % serv.getcount()) print(" (this should be 10, because by now the 5 oneway calls have completed as well)") serv.reset() print("\n--------------------------------------------------------------") print(" This part depends on the type of the server. ") print("--------------------------------------------------------------") print("Creating 5 threads that each call the server at the same time.") serverconfig = serv.getconfig() if serverconfig["SERVERTYPE"] == "thread": print("Servertype is thread. All calls will run in parallel.") print("The time this will take is 1 second (every thread takes 1 second in parallel).") print("You will see that the requests are handled by different server threads.") elif serverconfig["SERVERTYPE"] == "multiplex": print("Servertype is multiplex. The calls will need to get in line.") print("The time this will take is 5 seconds (every thread takes 1 second sequentially).") print("You will see that the requests are handled by a single server thread.") else: print("Unknown servertype") def func(uri): # This will run in a thread. Create a proxy just for this thread: with Proxy(uri) as p: processed = p.delay() print("[ thread %s called delay, processed by: %s ] " % (threading.current_thread().name, processed)) serv._pyroBind() # simplify the uri threads = [] for i in range(5): t = threading.Thread(target=func, args=[serv._pyroUri]) t.daemon = True threads.append(t) t.start() print("Waiting for threads to finish:") for t in threads: t.join() print("Done. Number of completed calls in the server: %d" % serv.getcount()) Pyro5-5.15/examples/servertypes/server.py000066400000000000000000000020571451404116400205570ustar00rootroot00000000000000import time import threading from Pyro5.api import expose, behavior, oneway, serve, config @expose @behavior(instance_mode="single") class Server(object): def __init__(self): self.callcount = 0 def reset(self): self.callcount = 0 def getcount(self): return self.callcount # the number of completed calls def getconfig(self): return config.as_dict() def delay(self): threadname = threading.current_thread().name print("delay called in thread %s" % threadname) time.sleep(1) self.callcount += 1 return threadname @oneway def onewaydelay(self): threadname = threading.current_thread().name print("onewaydelay called in thread %s" % threadname) time.sleep(1) self.callcount += 1 # main program config.SERVERTYPE = "undefined" servertype = input("Servertype threaded or multiplex (t/m)?") if servertype == "t": config.SERVERTYPE = "thread" else: config.SERVERTYPE = "multiplex" serve({ Server: "example.servertypes" }) Pyro5-5.15/examples/shoppingcart/000077500000000000000000000000001451404116400170015ustar00rootroot00000000000000Pyro5-5.15/examples/shoppingcart/Readme.txt000066400000000000000000000013101451404116400207320ustar00rootroot00000000000000A very simple example that shows the creation and manipulation of new objects in the server. It is a shop where the clients need to take a shopping cart (created in the shop server) and put items in it from the shop's inventory. After that they take it to the shop's counter to pay and get a receipt. Due to Pyro's autoproxy feature the shopping carts are automatically returned to the client as a proxy. The Shoppingcart objects remain in the shop server. The client code interacts with them (and with the shop) remotely. The shop returns a receipt (just a text list of purchased goods) at checkout time, and puts back the shopping cart (unregisters and deletes the object) when the client leaves the store. Pyro5-5.15/examples/shoppingcart/clients.py000066400000000000000000000040201451404116400210100ustar00rootroot00000000000000import random from Pyro5.api import Proxy import Pyro5.errors shop = Proxy("PYRONAME:example.shop") print("Simulating some customers.") harrysCart = shop.enter("Harry") sallysCart = shop.enter("Sally") shoplifterCart = shop.enter("shoplifter") # harry buys 4 things and sally 5, shoplifter takes 3 items # note that we put the item directly in the shopping cart. goods = list(shop.goods().keys()) for i in range(4): item = random.choice(goods) print("Harry buys %s" % item) harrysCart.purchase(item) for i in range(5): item = random.choice(goods) print("Sally buys %s" % item) sallysCart.purchase(item) for i in range(3): item = random.choice(goods) print("Shoplifter takes %s" % item) shoplifterCart.purchase(item) print("Customers currently in the shop: %s" % shop.customers()) # Go to the counter to pay and get a receipt. # The shopping cart is still 'inside the shop' (=on the server) # so it knows what is in there for every customer in the store. # Harry pays by just telling his name (and the shop looks up # harry's shoppingcart). # Sally just hands in her shopping cart directly. # The shoplifter tries to leave without paying. try: receipt = shop.payByName("Harry") except Exception: print("ERROR: %s" % ("".join(Pyro5.errors.get_pyro_traceback()))) print("Harry payed. The cart now contains: %s (should be empty)" % harrysCart.getContents()) print("Harry got this receipt:") print(receipt) receipt = shop.payCart(sallysCart) print("Sally payed. The cart now contains: %s (should be empty)" % sallysCart.getContents()) print("Sally got this receipt:") print(receipt) print("Harry is leaving.") shop.leave("Harry") print("Sally is leaving.") shop.leave("Sally") print("Shoplifter is leaving. (should be impossible i.e. give an error)") try: shop.leave("shoplifter") except Exception: print("".join(Pyro5.errors.get_pyro_traceback())) print("Harry is attempting to put stuff back in his cart again,") print("which should fail because the cart does no longer exist.") harrysCart.purchase("crap") Pyro5-5.15/examples/shoppingcart/shoppingcart.py000066400000000000000000000006301451404116400220530ustar00rootroot00000000000000from Pyro5.api import expose @expose class ShoppingCart(object): def __init__(self): self.contents = [] print("(shoppingcart %d taken)" % id(self)) def purchase(self, item): self.contents.append(item) print("(%s put into shoppingcart %d)" % (item, id(self))) def empty(self): self.contents = [] def getContents(self): return self.contents Pyro5-5.15/examples/shoppingcart/shopserver.py000066400000000000000000000043141451404116400215550ustar00rootroot00000000000000import time from Pyro5.api import expose, behavior, serve from shoppingcart import ShoppingCart @expose @behavior(instance_mode="single") class Shop(object): inventory = { "paper": 1.25, "bread": 1.50, "meat": 5.99, "milk": 0.80, "fruit": 2.65, "chocolate": 3.99, "pasta": 0.50, "sauce": 1.20, "vegetables": 1.40, "cookies": 1.99, "pizza": 3.60, "shampoo": 2.22, "whiskey": 24.99 } customersInStore = {} def enter(self, name): print("Customer %s enters the store." % name) print("Customer takes a shopping cart.") # create a cart and return it as a pyro object to the client cart = ShoppingCart() self.customersInStore[name] = cart self._pyroDaemon.register(cart) # make cart a pyro object return cart def customers(self): return list(self.customersInStore.keys()) def goods(self): return self.inventory def payByName(self, name): print("Customer %s goes to the counter to pay." % name) cart = self.customersInStore[name] return self.payCart(cart, name) def payCart(self, cart, name=None): receipt = [] if name: receipt.append("Receipt for %s." % name) receipt.append("Receipt Date: " + time.asctime()) total = 0.0 for item in cart.getContents(): price = self.inventory[item] total += price receipt.append("%13s %.2f" % (item, price)) receipt.append("") receipt.append("%13s %.2f" % ("total:", total)) cart.empty() return "\n".join(receipt) def leave(self, name): print("Customer %s leaves." % name) cart = self.customersInStore[name] print(" their shopping cart contains: %s" % cart.getContents()) if cart.getContents(): print(" it is not empty, they are trying to shoplift!") raise Exception("attempt to steal a full cart prevented") # delete the cart and unregister it with pyro del self.customersInStore[name] self._pyroDaemon.unregister(cart) # main program serve({ Shop: "example.shop" }) Pyro5-5.15/examples/socketpair/000077500000000000000000000000001451404116400164445ustar00rootroot00000000000000Pyro5-5.15/examples/socketpair/pair-fork.py000066400000000000000000000026741451404116400207210ustar00rootroot00000000000000# this example forks() and thus won't work on Windows. import os import signal import socket from Pyro5.api import Daemon, Proxy, expose # create our own socket pair (server-client sockets that are already connected) sock1, sock2 = socket.socketpair() pid = os.fork() if pid == 0: # we are the child process, we host the daemon. class Echo(object): @expose def echo(self, message): print("server got message: ", message) return "thank you" # create a daemon with some Pyro object running on our custom server socket daemon = Daemon(connected_socket=sock1) daemon.register(Echo, "echo") print("Process PID={:d}: Pyro daemon running on {:s}\n".format(os.getpid(), daemon.locationStr)) daemon.requestLoop() else: # we are the parent process, we create a Pyro client proxy print("Process PID={:d}: Pyro client.\n".format(os.getpid())) # create a client running on the client socket with Proxy("echo", connected_socket=sock2) as p: reply = p.echo("hello!") print("client got reply:", reply) reply = p.echo("hello again!") print("client got reply:", reply) with Proxy("echo", connected_socket=sock2) as p: reply = p.echo("hello2!") print("client got reply:", reply) reply = p.echo("hello2 again!") print("client got reply:", reply) os.kill(pid, signal.SIGTERM) os.waitpid(pid, 0) print("\nThe end.") Pyro5-5.15/examples/socketpair/pair-thread.py000066400000000000000000000022321451404116400212150ustar00rootroot00000000000000# this example uses a background thread to run the daemon in, also works on Windows import socket import threading from Pyro5.api import expose, Daemon, Proxy # create our own socket pair (server-client sockets that are already connected) sock1, sock2 = socket.socketpair() class Echo(object): @expose def echo(self, message): print("server got message: ", message) return "thank you" # create a daemon with some Pyro objectrunning on our custom server socket daemon = Daemon(connected_socket=sock1) daemon.register(Echo, "echo") print("(Pyro daemon running on", daemon.locationStr, ")\n") daemonthread = threading.Thread(target=daemon.requestLoop) daemonthread.daemon = True daemonthread.start() # create a client running on the client socket with Proxy("echo", connected_socket=sock2) as p: reply = p.echo("hello!") print("client got reply:", reply) reply = p.echo("hello again!") print("client got reply:", reply) with Proxy("echo", connected_socket=sock2) as p: reply = p.echo("hello2!") print("client got reply:", reply) reply = p.echo("hello2 again!") print("client got reply:", reply) print("\nThe end.") Pyro5-5.15/examples/socketpair/readme.txt000066400000000000000000000007021451404116400204410ustar00rootroot00000000000000It is possible to run a Pyro Daemon and Proxy over a user-supplied, already connected socket, such as those produced by the socket.socketpair() function. This makes it easy to communicate efficiently with a child process or background thread, using Pyro. The pair-fork.py program uses fork() to run a background process (Windows doesn't support this) The pair-thread.py program uses a background thread for the Pyro daemon (works on Windows too). Pyro5-5.15/examples/ssl/000077500000000000000000000000001451404116400151015ustar00rootroot00000000000000Pyro5-5.15/examples/ssl/Readme.txt000066400000000000000000000015671451404116400170500ustar00rootroot00000000000000SSL example showing how to configure 2-way-SSL with custom certificate validation. What this means is that the server has a certificate, and the client as well. The server only accepts connections from clients that provide the proper certificate (and ofcourse, clients only connect to servers having a proper certificate). By using Pyro's handshake mechanism you can easily add custom certificate verification steps in both the client (proxy) and server (daemon). This is more or less required, because you should be checking if the certificate is indeed from the party you expected... This example uses the self-signed demo certs that come with Pyro, so in the code you'll also see that we configure the SSL_CACERTS so that SSL will accept the self-signed certificate as a valid cert. If the connection is successfully established, all communication is then encrypted and secure. Pyro5-5.15/examples/ssl/client.py000066400000000000000000000052561451404116400167410ustar00rootroot00000000000000import Pyro5.errors import Pyro5.api Pyro5.config.SSL = True Pyro5.config.SSL_CACERTS = "../../certs/server_cert.pem" # to make ssl accept the self-signed server cert Pyro5.config.SSL_CLIENTCERT = "../../certs/client_cert.pem" Pyro5.config.SSL_CLIENTKEY = "../../certs/client_key.pem" print("SSL enabled (2-way).") def verify_cert(cert): if not cert: raise Pyro5.errors.CommunicationError("cert missing") # note: hostname and expiry date validation is already successfully performed by the SSL layer itself # not_before = datetime.datetime.utcfromtimestamp(ssl.cert_time_to_seconds(cert["notBefore"])) # print("not before:", not_before) # not_after = datetime.datetime.utcfromtimestamp(ssl.cert_time_to_seconds(cert["notAfter"])) # print("not after:", not_after) # today = datetime.datetime.now() # if today > not_after or today < not_before: # raise Pyro5.errors.CommunicationError("cert not yet valid or expired") if cert["serialNumber"] != "63B5A2EB71E2C78CBE4200C7EAB9634E7B669407": raise Pyro5.errors.CommunicationError("cert serial number incorrect", cert["serialNumber"]) issuer = dict(p[0] for p in cert["issuer"]) subject = dict(p[0] for p in cert["subject"]) if issuer["organizationName"] != "Razorvine.net": # issuer is not often relevant I guess, but just to show that you have the data raise Pyro5.errors.CommunicationError("cert not issued by Razorvine.net") if subject["countryName"] != "NL": raise Pyro5.errors.CommunicationError("cert not for country NL") if subject["organizationName"] != "Razorvine.net": raise Pyro5.errors.CommunicationError("cert not for Razorvine.net") print("(SSL server cert is ok: serial={ser}, subject={subj})" .format(ser=cert["serialNumber"], subj=subject["organizationName"])) # to make Pyro verify the certificate on new connections, use the handshake mechanism: class CertCheckingProxy(Pyro5.api.Proxy): def _pyroValidateHandshake(self, response): cert = self._pyroConnection.getpeercert() verify_cert(cert) # Note: to automatically enforce certificate verification for all proxy objects you create, # you can also monkey-patch the method in the Proxy class itself. # Then you don't have to make sure that you're using CertCheckingProxy every time. # However some other Proxy subclass can (will) override this again! # # def certverifier(self, response): # cert = self._pyroConnection.getpeercert() # verify_cert(cert) # Pyro5.api.Proxy._pyroValidateHandshake = certverifier uri = input("Server uri: ").strip() with CertCheckingProxy(uri) as p: response = p.echo("client speaking") print("response:", response) Pyro5-5.15/examples/ssl/server.py000066400000000000000000000045721451404116400167710ustar00rootroot00000000000000import Pyro5.errors import Pyro5.api class Safe(object): @Pyro5.api.expose def echo(self, message): print("got message:", message) return "hi!" Pyro5.config.SSL = True Pyro5.config.SSL_REQUIRECLIENTCERT = True # enable 2-way ssl Pyro5.config.SSL_SERVERCERT = "../../certs/server_cert.pem" Pyro5.config.SSL_SERVERKEY = "../../certs/server_key.pem" Pyro5.config.SSL_CACERTS = "../../certs/client_cert.pem" # to make ssl accept the self-signed client cert print("SSL enabled (2-way).") class CertValidatingDaemon(Pyro5.api.Daemon): def validateHandshake(self, conn, data): cert = conn.getpeercert() if not cert: raise Pyro5.errors.CommunicationError("client cert missing") # note: hostname and expiry date validation is already successfully performed by the SSL layer itself # not_before = datetime.datetime.utcfromtimestamp(ssl.cert_time_to_seconds(cert["notBefore"])) # print("not before:", not_before) # not_after = datetime.datetime.utcfromtimestamp(ssl.cert_time_to_seconds(cert["notAfter"])) # print("not after:", not_after) # today = datetime.datetime.now() # if today > not_after or today < not_before: # raise Pyro5.errors.CommunicationError("cert not yet valid or expired") if cert["serialNumber"] != "1FABD733200A52DCED0702B7A856773F37AAC0E3": raise Pyro5.errors.CommunicationError("cert serial number incorrect", cert["serialNumber"]) issuer = dict(p[0] for p in cert["issuer"]) subject = dict(p[0] for p in cert["subject"]) if issuer["organizationName"] != "Razorvine.net": # issuer is not often relevant I guess, but just to show that you have the data raise Pyro5.errors.CommunicationError("cert not issued by Razorvine.net") if subject["countryName"] != "NL": raise Pyro5.errors.CommunicationError("cert not for country NL") if subject["organizationName"] != "Razorvine.net": raise Pyro5.errors.CommunicationError("cert not for Razorvine.net") print("(SSL client cert is ok: serial={ser}, subject={subj})" .format(ser=cert["serialNumber"], subj=subject["organizationName"])) return super(CertValidatingDaemon, self).validateHandshake(conn, data) d = CertValidatingDaemon() uri = d.register(Safe) print("server uri:", uri) d.requestLoop() Pyro5-5.15/examples/stockquotes/000077500000000000000000000000001451404116400166645ustar00rootroot00000000000000Pyro5-5.15/examples/stockquotes/Readme.txt000066400000000000000000000056071451404116400206320ustar00rootroot00000000000000This example is the code from the Pyro tutorial where a simple stock quote system is built. It processes a stream of stock symbol quotes. The idea is that we have multiple stock markets producing stock symbol quotes. The viewer application aggregates all quotes from all the markets and filters them so you only see the ones you're interested in. Stockmarket ->-----\ Stockmarket ->------>------> Aggregator/Filter/Viewer Stockmarket ->-----/ There's a big simplification here in that the stockmarkets not really produce quote events themselves, instead they return quotes when asked for. But as this is an example, it is sufficient. A more elaborate example where there can be more viewers and the stockmarkets decide themselves when a new quote is available, can be found in the example 'stockquotes-old'. It uses callbacks instead of generators. The tutorial here consists of 3 phases: phase 1: Simple prototype code where everything is running in a single process. viewer.py is the main program that creates all objects, connects them together, and runs the main loop to display stock quotes. This code is fully operational but contains no Pyro code at all. It just shows what the system is going to look like later on. phase 2: The components are now distributed and we use Pyro to make them talk to each other. You have to start both component by itself (in separate console windows for instance): - start a Pyro name server (python -m Pyro5.nameserver). - start the stockmarket.py (it will create several different markets) - start the viewer.py to see the stream of quotes coming in. The code of the classes themselves is almost identical to phase1, including attribute access and the use of generator functions. The only thing we had to change is to create properties for the attributes that are accessed, and adding an expose decorator. In the viewer we didn't hardcode the stock market names but instead we ask the name server for all available stock markets. phase 3: Similar to phase2, but now we make two small changes: a) we use the Pyro name server in such a way that it is accessible from other machines, and b) we run the stock market server in a way that the host is not "localhost" by default and can be accessed by different machines. To do this, create the daemon with the arguments 'host' and 'port' set (i.e. host=HOST_IP, port=HOST_PORT). Again, you have to start both component by itself (in separate console windows for instance): - start a Pyro name server like this: (python -m Pyro5.nameserver -n 192.168.1.99 -p 9091) or (pyro5-ns -n 192.168.1.99 -p 9091) - start the stockmarket.py (set HOST_IP and HOST_PORT accordingly. Also, make sure HOST_PORT is already open). - start the viewer.py in different remote machines to see the stream of quotes coming in on each window. Pyro5-5.15/examples/stockquotes/phase1/000077500000000000000000000000001451404116400200455ustar00rootroot00000000000000Pyro5-5.15/examples/stockquotes/phase1/stockmarket.py000066400000000000000000000005451451404116400227520ustar00rootroot00000000000000import random import time class StockMarket(object): def __init__(self, marketname, symbols): self.name = marketname self.symbols = symbols def quotes(self): while True: symbol = random.choice(self.symbols) yield symbol, round(random.uniform(5, 150), 2) time.sleep(random.random()/2.0) Pyro5-5.15/examples/stockquotes/phase1/viewer.py000066400000000000000000000016511451404116400217230ustar00rootroot00000000000000from stockmarket import StockMarket class Viewer(object): def __init__(self): self.markets = set() self.symbols = set() def start(self): print("Shown quotes:", self.symbols) quote_sources = { market.name: market.quotes() for market in self.markets } while True: for market, quote_source in quote_sources.items(): quote = next(quote_source) # get a new stock quote from the source symbol, value = quote if symbol in self.symbols: print("{0}.{1}: {2}".format(market, symbol, value)) def main(): nasdaq = StockMarket("NASDAQ", ["AAPL", "CSCO", "MSFT", "GOOG"]) newyork = StockMarket("NYSE", ["IBM", "HPQ", "BP"]) viewer = Viewer() viewer.markets = {nasdaq, newyork} viewer.symbols = {"IBM", "AAPL", "MSFT"} viewer.start() if __name__ == "__main__": main() Pyro5-5.15/examples/stockquotes/phase2/000077500000000000000000000000001451404116400200465ustar00rootroot00000000000000Pyro5-5.15/examples/stockquotes/phase2/stockmarket.py000066400000000000000000000021551451404116400227520ustar00rootroot00000000000000import random import time from Pyro5.api import expose, Daemon, locate_ns @expose class StockMarket(object): def __init__(self, marketname, symbols): self._name = marketname self._symbols = symbols def quotes(self): while True: symbol = random.choice(self.symbols) yield symbol, round(random.uniform(5, 150), 2) time.sleep(random.random()/2.0) @property def name(self): return self._name @property def symbols(self): return self._symbols if __name__ == "__main__": nasdaq = StockMarket("NASDAQ", ["AAPL", "CSCO", "MSFT", "GOOG"]) newyork = StockMarket("NYSE", ["IBM", "HPQ", "BP"]) # for example purposes we will access the daemon and name server ourselves with Daemon() as daemon: nasdaq_uri = daemon.register(nasdaq) newyork_uri = daemon.register(newyork) with locate_ns() as ns: ns.register("example.stockmarket.nasdaq", nasdaq_uri) ns.register("example.stockmarket.newyork", newyork_uri) print("Stockmarkets available.") daemon.requestLoop() Pyro5-5.15/examples/stockquotes/phase2/viewer.py000066400000000000000000000024701451404116400217240ustar00rootroot00000000000000from Pyro5.api import locate_ns, Proxy class Viewer(object): def __init__(self): self.markets = set() self.symbols = set() def start(self): print("Shown quotes:", self.symbols) quote_sources = { market.name: market.quotes() for market in self.markets } while True: for market, quote_source in quote_sources.items(): quote = next(quote_source) # get a new stock quote from the source symbol, value = quote if symbol in self.symbols: print("{0}.{1}: {2}".format(market, symbol, value)) def find_stockmarkets(): # You can hardcode the stockmarket names for nasdaq and newyork, but it # is more flexible if we just look for every available stockmarket. markets = [] with locate_ns() as ns: for market, market_uri in ns.list(prefix="example.stockmarket.").items(): print("found market", market) markets.append(Proxy(market_uri)) if not markets: raise ValueError("no markets found! (have you started the stock markets first?)") return markets def main(): viewer = Viewer() viewer.markets = find_stockmarkets() viewer.symbols = {"IBM", "AAPL", "MSFT"} viewer.start() if __name__ == "__main__": main() Pyro5-5.15/examples/stockquotes/phase3/000077500000000000000000000000001451404116400200475ustar00rootroot00000000000000Pyro5-5.15/examples/stockquotes/phase3/stockmarket.py000066400000000000000000000024551451404116400227560ustar00rootroot00000000000000import random import time from Pyro5.api import expose, Daemon, locate_ns HOST_IP = "127.0.0.1" # Set accordingly (i.e. "192.168.1.99") HOST_PORT = 9092 # Set accordingly (i.e. 9876) @expose class StockMarket(object): def __init__(self, marketname, symbols): self._name = marketname self._symbols = symbols def quotes(self): while True: symbol = random.choice(self.symbols) yield symbol, round(random.uniform(5, 150), 2) time.sleep(random.random()/2.0) @property def name(self): return self._name @property def symbols(self): return self._symbols if __name__ == "__main__": nasdaq = StockMarket("NASDAQ", ["AAPL", "CSCO", "MSFT", "GOOG"]) newyork = StockMarket("NYSE", ["IBM", "HPQ", "BP"]) # Add the proper "host" and "port" arguments for the construction # of the Daemon so it can be accessed remotely with Daemon(host=HOST_IP, port=HOST_PORT) as daemon: nasdaq_uri = daemon.register(nasdaq) newyork_uri = daemon.register(newyork) with locate_ns() as ns: ns.register("example.stockmarket.nasdaq", nasdaq_uri) ns.register("example.stockmarket.newyork", newyork_uri) print("Stockmarkets available.") daemon.requestLoop() Pyro5-5.15/examples/stockquotes/phase3/viewer.py000066400000000000000000000024701451404116400217250ustar00rootroot00000000000000from Pyro5.api import locate_ns, Proxy class Viewer(object): def __init__(self): self.markets = set() self.symbols = set() def start(self): print("Shown quotes:", self.symbols) quote_sources = { market.name: market.quotes() for market in self.markets } while True: for market, quote_source in quote_sources.items(): quote = next(quote_source) # get a new stock quote from the source symbol, value = quote if symbol in self.symbols: print("{0}.{1}: {2}".format(market, symbol, value)) def find_stockmarkets(): # You can hardcode the stockmarket names for nasdaq and newyork, but it # is more flexible if we just look for every available stockmarket. markets = [] with locate_ns() as ns: for market, market_uri in ns.list(prefix="example.stockmarket.").items(): print("found market", market) markets.append(Proxy(market_uri)) if not markets: raise ValueError("no markets found! (have you started the stock markets first?)") return markets def main(): viewer = Viewer() viewer.markets = find_stockmarkets() viewer.symbols = {"IBM", "AAPL", "MSFT"} viewer.start() if __name__ == "__main__": main() Pyro5-5.15/examples/streaming/000077500000000000000000000000001451404116400162715ustar00rootroot00000000000000Pyro5-5.15/examples/streaming/Readme.txt000066400000000000000000000004401451404116400202250ustar00rootroot00000000000000Show the iterator item streaming support. If enabled in the server (it is enabled by default), you can return an iterator or generator from a remote call. The client receives a real iterator as a result and can iterate over it to stream the elements one by one from the remote iterator. Pyro5-5.15/examples/streaming/client.py000066400000000000000000000006641451404116400201270ustar00rootroot00000000000000import sys import Pyro5.errors import Pyro5.api sys.excepthook = Pyro5.errors.excepthook uri = input("Enter streaming server uri: ").strip() with Pyro5.api.Proxy(uri) as p: print("\nnormal list:") print(p.list()) print("\nvia iterator:") print(list(p.iterator())) print("\nvia generator:") print(list(p.generator())) print("\nslow generator:") for number in p.slow_generator(): print(number) Pyro5-5.15/examples/streaming/server.py000066400000000000000000000015161451404116400201540ustar00rootroot00000000000000import time from Pyro5.api import expose, serve, config if config.ITER_STREAMING: print("Note: iter-streaming has been enabled in the Pyro config.") else: print("Note: iter-streaming has not been enabled in the Pyro config (PYRO_ITER_STREAMING).") @expose class Streamer(object): def list(self): return [1, 2, 3, 4, 5, 6, 7, 8, 9] def iterator(self): return iter([1, 2, 3, 4, 5, 6, 7, 8, 9]) def generator(self): i = 1 while i < 10: yield i i += 1 def slow_generator(self): i = 1 while i < 10: time.sleep(0.5) yield i i += 1 def fibonacci(self): a, b = 0, 1 while True: yield a a, b = b, a + b serve({ Streamer: "example.streamer" }, use_ns=False) Pyro5-5.15/examples/thirdpartylib/000077500000000000000000000000001451404116400171615ustar00rootroot00000000000000Pyro5-5.15/examples/thirdpartylib/Readme.txt000066400000000000000000000012361451404116400211210ustar00rootroot00000000000000This example shows two ways of dealing with a third party library whose source code you cannot or don't want to change, and still use its classes directly in Pyro. The first server uses the @expose decorator but applies it as a regular function to create a wrapped, eposed class from the library class. That wrapped class is then registered instead. There are a couple of caveats when using this approach, see the relevant paragraph in the server chapter in the documentation for details. The second server2 shows the approach that I personally prefer: creating explicit adapter classes that call out to the library. You then have full control over what is happening. Pyro5-5.15/examples/thirdpartylib/awesome_thirdparty_library.py000066400000000000000000000011241451404116400251670ustar00rootroot00000000000000# This is an AWESOME LIBRARY. # You can use its AWESOME CLASSES to do Great Things. # The author however DOESN'T allow you to CHANGE the source code and taint it with Pyro decorators! class WeirdReturnType(object): def __init__(self, value): self.value = value class AwesomeClass(object): def method(self, arg): print("Awesome object is called with: ", arg) return "awesome" def private(self): print("This should be a private method...") return "boo" def weird(self): print("Weird!") return WeirdReturnType("awesome") Pyro5-5.15/examples/thirdpartylib/client.py000066400000000000000000000010651451404116400210130ustar00rootroot00000000000000from Pyro5.api import Proxy import Pyro5.errors uri = input("Enter the URI of the thirdparty library object: ").strip() with Proxy(uri) as remote: print(remote.method("how are you?")) try: print(remote.weird()) except Pyro5.errors.SerializeError: print("couldn't call weird() due to serialization error of the result value! (is ok)") try: print(remote.private()) # we can call this if full class is exposed... except AttributeError: print("couldn't call private(), it doesn't seem to be exposed! (is ok)") Pyro5-5.15/examples/thirdpartylib/server.py000066400000000000000000000006631451404116400210460ustar00rootroot00000000000000import Pyro5.api from awesome_thirdparty_library import AwesomeClass # expose the class from the library using @expose as wrapper function: ExposedClass = Pyro5.api.expose(AwesomeClass) with Pyro5.api.Daemon() as daemon: # register the wrapped class instead of the library class itself: uri = daemon.register(ExposedClass, "example.thirdpartylib") print("wrapped class registered, uri: ", uri) daemon.requestLoop() Pyro5-5.15/examples/thirdpartylib/server2.py000066400000000000000000000016501451404116400211250ustar00rootroot00000000000000from Pyro5.api import expose, Daemon from awesome_thirdparty_library import AwesomeClass # create adapter class that only exposes what should be accessible, # and calls into the library class from there: class AwesomeAdapterClass(AwesomeClass): @expose def method(self, arg): print("Adapter class is called...") return super(AwesomeAdapterClass, self).method(arg) @expose def weird(self): result = super(AwesomeAdapterClass, self).weird() # we have full control over what is returned and can turn the custom # result class into a normal string value that has no issues traveling over the wire return "weird " + result.value with Daemon() as daemon: # register the adapter class instead of the library class itself: uri = daemon.register(AwesomeAdapterClass, "example.thirdpartylib") print("adapter class registered, uri: ", uri) daemon.requestLoop() Pyro5-5.15/examples/threadproxysharing/000077500000000000000000000000001451404116400202255ustar00rootroot00000000000000Pyro5-5.15/examples/threadproxysharing/Readme.txt000066400000000000000000000011671451404116400221700ustar00rootroot00000000000000This example shows how Pyro deals with sharing proxies in different threads. Pyro does NOT allow you to share the same proxy across different threads, because concurrent access to the same network connection will likely corrupt the data sequence. Pyro's proxy object doesn't have an internal lock to guard against this - because locks are expensive. You will have to make sure yourself, that: - you make sure each thread uses their own new proxy object - or, you transfer a proxy object from one thread to another. This example shows both techniques. You'll have to start a Pyro name server first, before running the client. Pyro5-5.15/examples/threadproxysharing/client.py000066400000000000000000000033671451404116400220660ustar00rootroot00000000000000import time import threading import Pyro5.api import Pyro5.errors proxy = Pyro5.api.locate_ns() # grab a proxy for the name server print("Main thread:", threading.current_thread()) proxy.ping() # call it, the proxy is now connected and bound to the main thread # trying to use the proxy in a different thread is not possible, # and Pyro will raise an exception to tell you that: def other_thread_call(): try: proxy.ping() print("You should not see this!! the call succeeded in thread", threading.current_thread()) except Pyro5.errors.PyroError as x: print("Expected exception in thread", threading.current_thread()) print("Exception was: ", x) print() threading.Thread(target=other_thread_call).start() time.sleep(1) # SOLUTION 1: create a new proxy in the other thread. def new_proxy_thread_call(uri): proxy = Pyro5.api.Proxy(uri) proxy.ping() print("Solution 1. The call succeeded in thread", threading.current_thread()) print() threading.Thread(target=new_proxy_thread_call, args=(proxy._pyroUri,)).start() time.sleep(1) # SOLUTION 2: transfer ownership of our proxy to the other thread. def new_owner_thread_call(proxy): proxy._pyroClaimOwnership() proxy.ping() print("Solution 2. The call succeeded in thread", threading.current_thread()) print() threading.Thread(target=new_owner_thread_call, args=(proxy,)).start() time.sleep(1) # however, we are no longer the owner of the proxy now, so any new calls will fail for us print() try: proxy.ping() print("You should not see this!! the call succeeded in thread", threading.current_thread()) except Pyro5.errors.PyroError as x: print("Expected exception in thread", threading.current_thread()) print("Exception was: ", x) Pyro5-5.15/examples/timeout/000077500000000000000000000000001451404116400157665ustar00rootroot00000000000000Pyro5-5.15/examples/timeout/Readme.txt000066400000000000000000000005651451404116400177320ustar00rootroot00000000000000This is an example that shows the connection timeout handling (in the client). server.py -- the server you need to run for this example client.py -- client that uses timeout settings The client disables and enables timeouts to show what happens. It shows timeouts during long remote method calls, but also timeouts when trying to connect to a unresponsive server. Pyro5-5.15/examples/timeout/client.py000066400000000000000000000055361451404116400176270ustar00rootroot00000000000000import time import sys import Pyro5.errors from Pyro5.api import Proxy # NOTE: the timer in IronPython seems to be wacky. # So we use wider margins for that, to check if the delays are ok. def approxEqual(x, y): return abs(x - y) < 0.2 # disable timeout globally Pyro5.config.COMMTIMEOUT = 0 obj = Proxy("PYRONAME:example.timeout") obj._pyroBind() print("No timeout is configured. Calling delay with 2 seconds.") start = time.time() result = obj.delay(2) assert result == "slept 2 seconds" duration = time.time() - start if sys.platform != "cli": assert approxEqual(duration, 2), "expected 2 seconds duration" else: assert 1.0 < duration < 3.0, "expected about 2 seconds duration" # override timeout for this object obj._pyroTimeout = 1 print("Timeout set to 1 seconds. Calling delay with 2 seconds.") start = time.time() try: result = obj.delay(2) print("!?should have raised TimeoutError!?") except Pyro5.errors.TimeoutError: print("TimeoutError! As expected!") duration = time.time() - start if sys.platform != "cli": assert approxEqual(duration, 1), "expected 1 seconds duration" else: assert 0.9 < duration < 1.9, "expected about 1 second duration" # set timeout globally Pyro5.config.COMMTIMEOUT = 1 obj = Proxy("PYRONAME:example.timeout") print("COMMTIMEOUT is set globally. Calling delay with 2 seconds.") start = time.time() try: result = obj.delay(2) print("!?should have raised TimeoutError!?") except Pyro5.errors.TimeoutError: print("TimeoutError! As expected!") duration = time.time() - start if sys.platform != "cli": assert approxEqual(duration, 1), "expected 1 seconds duration" else: assert 0.9 < duration < 1.9, "expected about 1 second duration" # override again for this object obj._pyroTimeout = None print("No timeout is configured. Calling delay with 3 seconds.") start = time.time() result = obj.delay(3) assert result == "slept 3 seconds" duration = time.time() - start if sys.platform != "cli": assert approxEqual(duration, 3), "expected 3 seconds duration" else: assert 2.5 < duration < 3.5, "expected about 3 second duration" print("Trying to connect to the frozen daemon.") obj = Proxy("PYRONAME:example.timeout.frozendaemon") obj._pyroTimeout = 1 print("Timeout set to 1 seconds. Trying to connect.") start = time.time() try: result = obj.delay(5) print("!?should have raised TimeoutError!?") except Pyro5.errors.TimeoutError: print("TimeoutError! As expected!") duration = time.time() - start if sys.platform != "cli": assert approxEqual(duration, 1), "expected 1 seconds duration" else: assert 0.9 < duration < 1.9, "expected about 1 second duration" print("Disabling timeout and trying to connect again. This may take forever now.") print("Feel free to abort with ctrl-c or ctrl-break.") obj._pyroTimeout = None obj.delay(1) Pyro5-5.15/examples/timeout/server.py000066400000000000000000000013321451404116400176450ustar00rootroot00000000000000import time from Pyro5.api import expose, locate_ns, Daemon, config @expose class TimeoutServer(object): def delay(self, amount): print("sleeping %d" % amount) time.sleep(amount) print("done.") return "slept %d seconds" % amount config.COMMTIMEOUT = 0 # the server won't be using timeouts ns = locate_ns() daemon = Daemon() daemon2 = Daemon() obj = TimeoutServer() obj2 = TimeoutServer() uri = daemon.register(obj) uri2 = daemon2.register(obj2) ns.register("example.timeout", uri) ns.register("example.timeout.frozendaemon", uri2) print("Server ready.") # Note that we're only starting one of the 2 daemons. # daemon2 is not started, to simulate connection timeouts. daemon.requestLoop() Pyro5-5.15/examples/timezones/000077500000000000000000000000001451404116400163155ustar00rootroot00000000000000Pyro5-5.15/examples/timezones/Readme.txt000066400000000000000000000005471451404116400202610ustar00rootroot00000000000000PREREQUISITES: install the 'pytz', 'python-dateutil' and 'pendulum' libraries This example shows how datetime and timezones could be handled. The default serpent serializer will serialize them as a string in ISO date/time format. You will have to either parse the string yourself, or perhaps use a custom serializer/deserializer (not shown in this example). Pyro5-5.15/examples/timezones/client.py000066400000000000000000000017251451404116400201520ustar00rootroot00000000000000import pendulum import datetime from Pyro5.api import Proxy uri = input("What is the server uri? ").strip() fmt = '%Y-%m-%d %H:%M:%S %Z%z' print("local time without timezone: ", datetime.datetime.now().strftime(fmt)) with Proxy(uri) as serv: print("\n1. no timezone") datestr = serv.echo(datetime.datetime.now()) print("Got from server:", repr(datestr)) dt = pendulum.parse(datestr) print(" parsed:", repr(dt)) print("\n2. PyTz timezones") datestr = serv.pytz() print("Got from server:", repr(datestr)) dt = pendulum.parse(datestr) print(" parsed:", repr(dt)) print("\n3. DateUtil timezones") datestr = serv.dateutil() print("Got from server:", repr(datestr)) dt = pendulum.parse(datestr) print(" parsed:", repr(dt)) print("\n4. Pendulum timezones") datestr = serv.pendulum() print("Got from server:", repr(datestr)) dt = pendulum.parse(datestr) print(" parsed:", repr(dt)) print() Pyro5-5.15/examples/timezones/server.py000066400000000000000000000015121451404116400201740ustar00rootroot00000000000000import datetime import pytz import dateutil.tz import pendulum from Pyro5.api import expose, serve fmt = '%Y-%m-%d %H:%M:%S %Z%z' @expose class Server(object): def echo(self, date): print("RETURNING:", repr(date)) return date def pytz(self): tz_nl = pytz.timezone("Europe/Amsterdam") result = tz_nl.localize(datetime.datetime.now()) print("RETURNING:", repr(result)) return result def dateutil(self): tz_nl = dateutil.tz.gettz("Europe/Amsterdam") result = datetime.datetime.now(tz_nl) print("RETURNING:", repr(result)) return result def pendulum(self): tz_nl = pendulum.now("Europe/Amsterdam") print("RETURNING:", repr(tz_nl)) return tz_nl # main program serve({ Server: "example.timezones" }, use_ns=False) Pyro5-5.15/examples/unixdomainsock/000077500000000000000000000000001451404116400173335ustar00rootroot00000000000000Pyro5-5.15/examples/unixdomainsock/Readme.txt000066400000000000000000000005651451404116400212770ustar00rootroot00000000000000This is a very simple example that uses a Unix domain socket instead of a normal tcp/ip socket for server communications. The only difference is the parameter passed to the Daemon class. The client code is unaware of any special socket because you just feed it any Pyro URI. This time the URI will encode a Unix domain socket however, instead of a hostname+port number. Pyro5-5.15/examples/unixdomainsock/abstract_namespace_server.py000066400000000000000000000011311451404116400251060ustar00rootroot00000000000000# this only works on Linux # it uses the abstract namespace socket feature. from Pyro5.api import expose, Daemon @expose class Thingy(object): def message(self, arg): print("Message received:", arg) return "Roger!" with Daemon(unixsocket="\0example_unix.sock") as d: # notice the 0-byte at the start uri = d.register(Thingy, "example.unixsock") print("Server running, uri=", uri) string_uri = str(uri) print("Actually, the uri contains a 0-byte, make sure you copy the part between the quotes to the client:") print(repr(string_uri)) d.requestLoop() Pyro5-5.15/examples/unixdomainsock/client.py000066400000000000000000000004141451404116400211620ustar00rootroot00000000000000from Pyro5.api import Proxy uri = input("enter the server uri: ").strip() if "\\x00" in uri: uri=uri.replace("\\x00", "\x00") print("(uri contains 0-byte)") with Proxy(uri) as p: response = p.message("Hello there!") print("Response was:", response) Pyro5-5.15/examples/unixdomainsock/server.py000066400000000000000000000006271451404116400212200ustar00rootroot00000000000000import os from Pyro5.api import expose, Daemon @expose class Thingy(object): def message(self, arg): print("Message received:", arg) return "Roger!" if os.path.exists("example_unix.sock"): os.remove("example_unix.sock") with Daemon(unixsocket="example_unix.sock") as d: uri = d.register(Thingy, "example.unixsock") print("Server running, uri=", uri) d.requestLoop() Pyro5-5.15/examples/usersession/000077500000000000000000000000001451404116400166625ustar00rootroot00000000000000Pyro5-5.15/examples/usersession/Readme.txt000066400000000000000000000014021451404116400206150ustar00rootroot00000000000000This example shows the use of a couple of advanced Pyro constructs to achieve a thread safe, per-user resource connection in the Pyro server. It utilizes: - instance_mode "session" - annotations to pass the 'user token' from client to server - current_context to access the annotations in the server code - explicitly converting the annotation memoryview on the data to bytes for further processing - a silly global key-value database to trigger concurrency issues There are probably other ways of achieving the same thing (for instance, using the client connection on the current_context instead of explicitly passing along the user token) but it's just an example to give some inspiration. Before starting the server make sure you have a Pyro name server running. Pyro5-5.15/examples/usersession/client.py000066400000000000000000000043271451404116400205200ustar00rootroot00000000000000import threading import time import sys import Pyro5.errors import Pyro5.api sys.excepthook = Pyro5.errors.excepthook def get_user_token(): return "user123" class DbAccessor(threading.Thread): def __init__(self, uri): super(DbAccessor, self).__init__() self.uri = uri self.daemon = True def run(self): proxy = Pyro5.api.Proxy(self.uri) for i in range(3): try: Pyro5.api.current_context.annotations = {"USER": get_user_token().encode("utf-8")} proxy.store("number", 100+i) num = proxy.retrieve("number") print("[%s] num=%s" % (self.name, num)) except Exception: import traceback traceback.print_exc() print("\n***** Sequential access using multiple proxies on the Session-Bound Database... (no issues)") with Pyro5.api.Proxy("PYRONAME:example.usersession.sessiondb") as p1, \ Pyro5.api.Proxy("PYRONAME:example.usersession.sessiondb") as p2: Pyro5.api.current_context.annotations = {"USER": get_user_token().encode("utf-8")} p1.store("number", 42) p1.retrieve("number") p2.store("number", 43) p2.retrieve("number") print("\n***** Sequential access using multiple proxies on the Singleton Database... (no issues)") with Pyro5.api.Proxy("PYRONAME:example.usersession.singletondb") as p1, \ Pyro5.api.Proxy("PYRONAME:example.usersession.singletondb") as p2: Pyro5.api.current_context.annotations = {"USER": get_user_token().encode("utf-8")} p1.store("number", 42) p1.retrieve("number") p2.store("number", 43) p2.retrieve("number") print("\n***** Multiple concurrent proxies on the Session-Bound Database... (no issues)") input("enter to start: ") t1 = DbAccessor("PYRONAME:example.usersession.sessiondb") t2 = DbAccessor("PYRONAME:example.usersession.sessiondb") t1.start() t2.start() time.sleep(1) t1.join() t2.join() print("\n***** Multiple concurrent proxies on the Singleton Database... (concurrency errors will occur!)") input("enter to start: ") t1 = DbAccessor("PYRONAME:example.usersession.singletondb") t2 = DbAccessor("PYRONAME:example.usersession.singletondb") t1.start() t2.start() time.sleep(1) t1.join() t2.join() Pyro5-5.15/examples/usersession/database.py000066400000000000000000000052251451404116400210040ustar00rootroot00000000000000import threading import time import random import traceback class DummyDatabase(object): """Key-value datastore""" def __init__(self): self.storage = {} self.allowed_users = ["user123", "admin"] def connect(self, user): return Connection(self, user) def __setitem__(self, key, value): time.sleep(random.random()/10) # artificial delay self.storage[key] = value def __getitem__(self, item): time.sleep(random.random()/10) # artificial delay return self.storage[item] class Connection(object): """ Connection to the key-value datastore with artificial limitation that only a single thread may use the connection at the same time """ def __init__(self, db, user=None): self.db = db self.user = user self.lock = threading.RLock() def store(self, key, value, user=None): user = user or self.user assert user in self.db.allowed_users, "access denied" if self.lock.acquire(blocking=False): print("DB: user %s stores: %s = %s" % (user, key, value)) self.db[key] = value self.lock.release() else: raise RuntimeError("ERROR: concurrent connection access (write) by multiple different threads") def retrieve(self, key, user=None): user = user or self.user assert user in self.db.allowed_users, "access denied" if self.lock.acquire(blocking=False): print("DB: user %s retrieve: %s" % (user, key)) value = self.db[key] self.lock.release() return value else: raise RuntimeError("ERROR: concurrent connection access (read) by multiple different threads") if __name__ == "__main__": # first single threaded access db = DummyDatabase() conn = db.connect("user123") for i in range(5): conn.store("amount", 100+i) conn.retrieve("amount") # now multiple threads, should crash class ClientThread(threading.Thread): def __init__(self, conn): super(ClientThread, self).__init__() self.conn = conn self.daemon = True def run(self): for i in range(5): try: self.conn.store("amount", 100+i) except Exception: traceback.print_exc() try: self.conn.retrieve("amount") except Exception: traceback.print_exc() client1 = ClientThread(conn) client2 = ClientThread(conn) client1.start() client2.start() time.sleep(0.1) client1.join() client2.join() Pyro5-5.15/examples/usersession/server.py000066400000000000000000000041531451404116400205450ustar00rootroot00000000000000from Pyro5.api import behavior, expose, current_context, serve, config from database import DummyDatabase config.SERVERTYPE = "thread" database = DummyDatabase() @behavior(instance_mode="single") @expose class SingletonDatabase(object): """ This pyro object will exhibit problems when used from multiple proxies at the same time because it will access the database connection concurrently from different threads """ def __init__(self): print("[%s] new instance and connection" % self.__class__.__name__) self.conn = database.connect(user=None) # user is per-call, not global def store(self, key, value): # get the user-token from the USER annotation user_annotation = current_context.annotations["USER"] # because we will be storing it for a longer time, make an explicit textual copy of it user = bytes(user_annotation).decode("utf-8") self.conn.store(key, value, user=user) def retrieve(self, key): # get the user-token from the USER annotation user_annotation = current_context.annotations["USER"] return self.conn.retrieve(key, user=bytes(user_annotation).decode("utf-8")) def ping(self): return "hi" @behavior(instance_mode="session") @expose class SessionboundDatabase(object): """ This pyro object will work fine when used from multiple proxies at the same time because you'll get a new instance for every new session (proxy connection) """ def __init__(self): # get the user-token from the USER annotation user_annotation = current_context.annotations["USER"] user = bytes(user_annotation).decode("utf-8") self.connection = database.connect(user) print("[%s] new instance and connection for user: %s" % (self.__class__.__name__, user)) def store(self, key, value): self.connection.store(key, value) def retrieve(self, key): return self.connection.retrieve(key) def ping(self): return "hi" serve({ SingletonDatabase: "example.usersession.singletondb", SessionboundDatabase: "example.usersession.sessiondb" }) Pyro5-5.15/examples/warehouse/000077500000000000000000000000001451404116400163025ustar00rootroot00000000000000Pyro5-5.15/examples/warehouse/Readme.txt000077500000000000000000000026251451404116400202500ustar00rootroot00000000000000This example is the code from the Pyro tutorial where we build a simple warehouse that stores items. The idea is that there is one big warehouse that everyone can store items in, and retrieve other items from (if they're in the warehouse). The tutorial consists of 3 phases: phase 1: Simple prototype code where everything is running in a single process. visit.py creates the warehouse and two visitors. This code is fully operational but contains no Pyro code at all and shows what the system is going to look like later on. phase 2: Pyro is now used to make the warehouse a standalone component. You can still visit it of course. visit.py does need the URI of the warehouse however. (It is printed as soon as the warehouse is started) The code of the Warehouse and the Person classes is unchanged. phase 3: Phase 2 works fine but is a bit cumbersome because you need to copy-paste the warehouse URI to be able to visit it. Phase 3 simplifies things a bit by using the Pyro name server. Also, it uses the Pyro excepthook to print a nicer exception message if anything goes wrong. (Try taking something from the warehouse that is not present!) The code of the Warehouse and the Person classes is still unchanged. Note: to avoid having to deal with serialization issues, this example only passes primitive types (strings in this case) to the remote method calls. Pyro5-5.15/examples/warehouse/phase1/000077500000000000000000000000001451404116400174635ustar00rootroot00000000000000Pyro5-5.15/examples/warehouse/phase1/person.py000066400000000000000000000013521451404116400213440ustar00rootroot00000000000000class Person(object): def __init__(self, name): self.name = name def visit(self, warehouse): print("This is {0}.".format(self.name)) self.deposit(warehouse) self.retrieve(warehouse) print("Thank you, come again!") def deposit(self, warehouse): print("The warehouse contains:", warehouse.list_contents()) item = input("Type a thing you want to store (or empty): ").strip() if item: warehouse.store(self.name, item) def retrieve(self, warehouse): print("The warehouse contains:", warehouse.list_contents()) item = input("Type something you want to take (or empty): ").strip() if item: warehouse.take(self.name, item) Pyro5-5.15/examples/warehouse/phase1/visit.py000066400000000000000000000003341451404116400211730ustar00rootroot00000000000000# This is the code that runs this example. from warehouse import Warehouse from person import Person warehouse = Warehouse() janet = Person("Janet") henry = Person("Henry") janet.visit(warehouse) henry.visit(warehouse) Pyro5-5.15/examples/warehouse/phase1/warehouse.py000066400000000000000000000006561451404116400220460ustar00rootroot00000000000000class Warehouse(object): def __init__(self): self.contents = ["chair", "bike", "flashlight", "laptop", "couch"] def list_contents(self): return self.contents def take(self, name, item): self.contents.remove(item) print("{0} took the {1}.".format(name, item)) def store(self, name, item): self.contents.append(item) print("{0} stored the {1}.".format(name, item)) Pyro5-5.15/examples/warehouse/phase2/000077500000000000000000000000001451404116400174645ustar00rootroot00000000000000Pyro5-5.15/examples/warehouse/phase2/person.py000066400000000000000000000013521451404116400213450ustar00rootroot00000000000000class Person(object): def __init__(self, name): self.name = name def visit(self, warehouse): print("This is {0}.".format(self.name)) self.deposit(warehouse) self.retrieve(warehouse) print("Thank you, come again!") def deposit(self, warehouse): print("The warehouse contains:", warehouse.list_contents()) item = input("Type a thing you want to store (or empty): ").strip() if item: warehouse.store(self.name, item) def retrieve(self, warehouse): print("The warehouse contains:", warehouse.list_contents()) item = input("Type something you want to take (or empty): ").strip() if item: warehouse.take(self.name, item) Pyro5-5.15/examples/warehouse/phase2/visit.py000066400000000000000000000004231451404116400211730ustar00rootroot00000000000000# This is the code that visits the warehouse. from Pyro5.api import Proxy from person import Person uri = input("Enter the uri of the warehouse: ").strip() warehouse = Proxy(uri) janet = Person("Janet") henry = Person("Henry") janet.visit(warehouse) henry.visit(warehouse) Pyro5-5.15/examples/warehouse/phase2/warehouse.py000066400000000000000000000011271451404116400220410ustar00rootroot00000000000000from Pyro5.api import expose, behavior, serve @expose @behavior(instance_mode="single") class Warehouse(object): def __init__(self): self.contents = ["chair", "bike", "flashlight", "laptop", "couch"] def list_contents(self): return self.contents def take(self, name, item): self.contents.remove(item) print("{0} took the {1}.".format(name, item)) def store(self, name, item): self.contents.append(item) print("{0} stored the {1}.".format(name, item)) serve( { Warehouse: "example.warehouse" }, use_ns=False) Pyro5-5.15/examples/warehouse/phase3/000077500000000000000000000000001451404116400174655ustar00rootroot00000000000000Pyro5-5.15/examples/warehouse/phase3/person.py000066400000000000000000000013521451404116400213460ustar00rootroot00000000000000class Person(object): def __init__(self, name): self.name = name def visit(self, warehouse): print("This is {0}.".format(self.name)) self.deposit(warehouse) self.retrieve(warehouse) print("Thank you, come again!") def deposit(self, warehouse): print("The warehouse contains:", warehouse.list_contents()) item = input("Type a thing you want to store (or empty): ").strip() if item: warehouse.store(self.name, item) def retrieve(self, warehouse): print("The warehouse contains:", warehouse.list_contents()) item = input("Type something you want to take (or empty): ").strip() if item: warehouse.take(self.name, item) Pyro5-5.15/examples/warehouse/phase3/visit.py000066400000000000000000000004751451404116400212030ustar00rootroot00000000000000# This is the code that visits the warehouse. import sys import Pyro5.errors from Pyro5.api import Proxy from person import Person sys.excepthook = Pyro5.errors.excepthook warehouse = Proxy("PYRONAME:example.warehouse") janet = Person("Janet") henry = Person("Henry") janet.visit(warehouse) henry.visit(warehouse) Pyro5-5.15/examples/warehouse/phase3/warehouse.py000066400000000000000000000011261451404116400220410ustar00rootroot00000000000000from Pyro5.api import expose, behavior, serve @expose @behavior(instance_mode="single") class Warehouse(object): def __init__(self): self.contents = ["chair", "bike", "flashlight", "laptop", "couch"] def list_contents(self): return self.contents def take(self, name, item): self.contents.remove(item) print("{0} took the {1}.".format(name, item)) def store(self, name, item): self.contents.append(item) print("{0} stored the {1}.".format(name, item)) serve( { Warehouse: "example.warehouse" }, use_ns=True) Pyro5-5.15/mypy.ini000066400000000000000000000002461451404116400141630ustar00rootroot00000000000000[mypy] follow_imports = normal ignore_missing_imports = True incremental = True [mypy-setup] ignore_errors = True [mypy-Pyro5/compatibility/*] ignore_errors = True Pyro5-5.15/requirements.txt000066400000000000000000000000161451404116400157430ustar00rootroot00000000000000serpent>=1.41 Pyro5-5.15/setup.cfg000066400000000000000000000026721451404116400143120ustar00rootroot00000000000000[metadata] name = Pyro5 version = attr: Pyro5.__version__ description = Remote object communication library, fifth major version long_description = file: Readme.rst long_description_content_type = text/x-rst url = https://github.com/irmen/Pyro5 author = Irmen de Jong author_email = irmen@razorvine.net keywords = distributed objects, RPC, remote method call, IPC license = MIT license_file = LICENSE classifiers = Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: MIT License Natural Language :: English Natural Language :: Dutch Operating System :: OS Independent Programming Language :: Python Programming Language :: Python :: 3 Topic :: Software Development :: Object Brokering Topic :: System :: Distributed Computing Topic :: System :: Networking [options] zip_safe = True include_package_data = False packages = Pyro5, Pyro5.utils, Pyro5.compatibility python_requires = >=3.7 install_requires = serpent>=1.41 [options.entry_points] console_scripts = pyro5-ns = Pyro5.nameserver:main pyro5-nsc = Pyro5.nsc:main pyro5-echoserver = Pyro5.utils.echoserver:main pyro5-check-config = Pyro5.configure:dump pyro5-httpgateway = Pyro5.utils.httpgateway:main [pycodestyle] max-line-length = 140 exclude = .git,__pycache__,.tox,docs,tests,build,dist,.eggs,.cache,examples [tool:pytest] markers = network: Mark a test as requiring network access Pyro5-5.15/setup.py000066400000000000000000000000611451404116400141710ustar00rootroot00000000000000import sys from setuptools import setup setup() Pyro5-5.15/test-requirements.txt000066400000000000000000000000441451404116400167210ustar00rootroot00000000000000serpent>=1.41 msgpack>=0.5.2 pytest Pyro5-5.15/tests/000077500000000000000000000000001451404116400136245ustar00rootroot00000000000000Pyro5-5.15/tests/support.py000066400000000000000000000131761451404116400157220ustar00rootroot00000000000000""" Support code for the test suite. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import threading from Pyro5 import errors from Pyro5.api import expose, behavior, current_context, config, oneway __all__ = ["NonserializableError", "MyThingPartlyExposed", "MyThingFullExposed", "MyThingExposedSub", "MyThingPartlyExposedSub", "ConnectionMock", "AtomicCounter", "ResourceService", "Resource"] config.reset(False) # reset the config to default class NonserializableError(Exception): def __reduce__(self): raise Exception("to make this error non-serializable") class MyThingPartlyExposed(object): c_attr = "hi" propvalue = 42 _private_attr1 = "hi" __private_attr2 = "hi" name = "" def __init__(self, name="dummy"): self.name = name def __eq__(self, other): if type(other) is MyThingPartlyExposed: return self.name == other.name return False def method(self, arg, default=99, **kwargs): pass @staticmethod def staticmethod(arg): pass @classmethod def classmethod(cls, arg): pass def __dunder__(self): pass def __private(self): pass def _private(self): pass @expose @property def prop1(self): return self.propvalue @expose @prop1.setter def prop1(self, value): self.propvalue = value @expose @property def readonly_prop1(self): return self.propvalue @property def prop2(self): return self.propvalue @prop2.setter def prop2(self, value): self.propvalue = value @oneway @expose def oneway(self, arg): pass @expose def exposed(self): pass __hash__ = object.__hash__ @expose class MyThingFullExposed(object): """this is the same as MyThingPartlyExposed but the whole class should be exposed""" c_attr = "hi" propvalue = 42 _private_attr1 = "hi" __private_attr2 = "hi" name = "" def __init__(self, name="dummy"): self.name = name # note: not affected by @expose, only real properties are def __eq__(self, other): if type(other) is MyThingFullExposed: return self.name == other.name return False def method(self, arg, default=99, **kwargs): pass @staticmethod def staticmethod(arg): pass @classmethod def classmethod(cls, arg): pass def __dunder__(self): pass def __private(self): pass def _private(self): pass @property def prop1(self): return self.propvalue @prop1.setter def prop1(self, value): self.propvalue = value @property def readonly_prop1(self): return self.propvalue @property def prop2(self): return self.propvalue @prop2.setter def prop2(self, value): self.propvalue = value @oneway def oneway(self, arg): pass def exposed(self): pass __hash__ = object.__hash__ @expose class MyThingExposedSub(MyThingFullExposed): def sub_exposed(self): pass def sub_unexposed(self): pass @oneway def oneway2(self): pass class MyThingPartlyExposedSub(MyThingPartlyExposed): @expose def sub_exposed(self): pass def sub_unexposed(self): pass @oneway def oneway2(self): pass class ConnectionMock(object): def __init__(self, initial_msg=None): self.keep_open = False if not initial_msg: self.received = b"" elif isinstance(initial_msg, (str, bytes)): self.received = initial_msg else: self.received = initial_msg.data # it's probably a Message object def send(self, data): self.received += data def recv(self, datasize): chunk = self.received[:datasize] self.received = self.received[datasize:] if len(chunk) < datasize: raise errors.ConnectionClosedError("receiving: not enough data") return chunk class AtomicCounter(object): def __init__(self, value=0): self.__initial = value self.__value = value self.__lock = threading.Lock() def reset(self): self.__value = self.__initial def incr(self, amount=1): with self.__lock: self.__value += amount return self.__value def decr(self, amount=1): with self.__lock: self.__value -= amount return self.__value @property def value(self): return self.__value class Resource(object): # a fictional resource that gets allocated and must be freed again later. def __init__(self, name, collection): self.name = name self.collection = collection self.close_called = False def close(self): # Pyro will call this on a tracked resource once the client's connection gets closed! self.collection.discard(self) self.close_called = True @expose @behavior(instance_mode="single") class ResourceService(object): def __init__(self): self.resources = set() # the allocated resources def allocate(self, name): resource = Resource(name, self.resources) self.resources.add(resource) current_context.track_resource(resource) def free(self, name): resources = {r for r in self.resources if r.name == name} self.resources -= resources for r in resources: r.close() current_context.untrack_resource(r) def list(self): return [r.name for r in self.resources] Pyro5-5.15/tests/test_api.py000066400000000000000000000016451451404116400160140ustar00rootroot00000000000000import Pyro5.api import Pyro5.core import Pyro5.client import Pyro5.server import Pyro5.nameserver import Pyro5.callcontext import Pyro5.serializers from Pyro5.serializers import SerializerBase def test_api(): assert hasattr(Pyro5.api, "__version__") assert Pyro5.api.config.SERIALIZER == "serpent" assert Pyro5.api.URI is Pyro5.core.URI assert Pyro5.api.Proxy is Pyro5.client.Proxy assert Pyro5.api.Daemon is Pyro5.server.Daemon assert Pyro5.api.start_ns is Pyro5.nameserver.start_ns assert Pyro5.api.current_context is Pyro5.callcontext.current_context assert Pyro5.api.register_dict_to_class == SerializerBase.register_dict_to_class assert Pyro5.api.register_class_to_dict == SerializerBase.register_class_to_dict assert Pyro5.api.unregister_dict_to_class == SerializerBase.unregister_dict_to_class assert Pyro5.api.unregister_class_to_dict == SerializerBase.unregister_class_to_dict Pyro5-5.15/tests/test_client.py000066400000000000000000000267651451404116400165330ustar00rootroot00000000000000import copy import pytest import time import Pyro5.client import Pyro5.server import Pyro5.errors from Pyro5 import config class TestProxy: def testBasics(self): with pytest.raises(Pyro5.errors.PyroError): Pyro5.client.Proxy("burp") p1 = Pyro5.client.Proxy("PYRO:obj@host:5555") p1._pyroHandshake = "milkshake" p1._pyroTimeout = 42 p1._pyroSeq = 100 p1._pyroMaxRetries = 99 p1._pyroRawWireResponse = True p2 = copy.copy(p1) assert p1 == p2 assert p1 is not p2 assert p1._pyroUri == p2._pyroUri assert p1._pyroHandshake == p2._pyroHandshake assert p1._pyroTimeout == p2._pyroTimeout assert p1._pyroMaxRetries == p2._pyroMaxRetries assert p1._pyroRawWireResponse == p2._pyroRawWireResponse assert p2._pyroSeq == 0 def testProxyCopy(self): u = Pyro5.core.URI("PYRO:12345@hostname:9999") p1 = Pyro5.client.Proxy(u) p2 = copy.copy(p1) # check that most basic copy also works assert p2 == p1 assert p2._pyroOneway == set() p1._pyroAttrs = set("abc") p1._pyroTimeout = 42 p1._pyroOneway = set("def") p1._pyroMethods = set("ghi") p1._pyroHandshake = "apples" p2 = copy.copy(p1) assert p2 == p1 assert p2._pyroUri == p1._pyroUri assert p2._pyroOneway == p1._pyroOneway assert p2._pyroMethods == p1._pyroMethods assert p2._pyroAttrs == p1._pyroAttrs assert p2._pyroTimeout == p1._pyroTimeout assert p2._pyroHandshake == p1._pyroHandshake p1._pyroRelease() p2._pyroRelease() def testProxySubclassCopy(self): class ProxySub(Pyro5.client.Proxy): pass p = ProxySub("PYRO:12345@hostname:9999") p2 = copy.copy(p) assert isinstance(p2, ProxySub) p._pyroRelease() p2._pyroRelease() def testBatchProxyAdapterCopy(self): with Pyro5.client.Proxy("PYRO:12345@hostname:9999") as proxy: batchproxy = Pyro5.client.BatchProxy(proxy) p2 = copy.copy(batchproxy) assert isinstance(p2, Pyro5.client.BatchProxy) def testProxyOffline(self): # only offline stuff here. # online stuff needs a running daemon, so we do that in another test, to keep this one simple with pytest.raises(Pyro5.errors.PyroError): Pyro5.client.Proxy("999") p1 = Pyro5.client.Proxy("PYRO:9999@localhost:15555") p2 = Pyro5.client.Proxy(Pyro5.core.URI("PYRO:9999@localhost:15555")) assert p2._pyroUri == p1._pyroUri assert p1._pyroConnection is None p1._pyroRelease() p1._pyroRelease() # try copying a not-connected proxy p3 = copy.copy(p1) assert p3._pyroConnection is None assert p1._pyroConnection is None assert p1._pyroUri == p3._pyroUri assert p1._pyroUri is not p3._pyroUri p3._pyroRelease() def testProxySerializerOverride(self): serializer = config.SERIALIZER try: with pytest.raises(ValueError) as x: config.SERIALIZER = "~invalid~" Pyro5.client.Proxy("PYRO:obj@localhost:5555") assert "unknown" in str(x.value) finally: config.SERIALIZER = serializer try: with pytest.raises(KeyError) as x: proxy = Pyro5.client.Proxy("PYRO:obj@localhost:5555") proxy._pyroSerializer = "~invalidoverride~" proxy._pyroConnection = "FAKE" proxy.methodcall() assert "invalidoverride" in str(x.value) finally: proxy._pyroConnection = None config.SERIALIZER = serializer def testProxyDirMetadata(self): with Pyro5.client.Proxy("PYRO:9999@localhost:15555") as p: # metadata isn't loaded assert '__hash__' in dir(p) assert "ping" not in dir(p) # emulate obtaining metadata p._pyroAttrs = {"prop"} p._pyroMethods = {"ping"} assert "__hash__" in dir(p) assert "prop" in dir(p) assert "ping" in dir(p) def testProxySettings(self): p1 = Pyro5.client.Proxy("PYRO:9999@localhost:15555") p2 = Pyro5.client.Proxy("PYRO:9999@localhost:15555") p1._pyroOneway.add("method") p1._pyroAttrs.add("attr") p1._pyroMethods.add("method2") assert "method" in p1._pyroOneway assert "attr" in p1._pyroAttrs assert "method2" in p1._pyroMethods assert "method" not in p2._pyroOneway assert "attr" not in p2._pyroAttrs assert "method2" not in p2._pyroMethods assert p1._pyroOneway is not p2._pyroOneway, "p1 and p2 should have different oneway tables" assert p1._pyroAttrs is not p2._pyroAttrs, "p1 and p2 should have different attr tables" assert p1._pyroMethods is not p2._pyroMethods, "p1 and p2 should have different method tables" p1._pyroRelease() p2._pyroRelease() def testProxyWithStmt(self): class ConnectionMock(object): closeCalled = False keep_open = False def close(self): self.closeCalled = True connMock = ConnectionMock() # first without a 'with' statement p = Pyro5.client.Proxy("PYRO:9999@localhost:15555") p._pyroConnection = connMock assert not(connMock.closeCalled) p._pyroRelease() assert p._pyroConnection is None assert connMock.closeCalled connMock = ConnectionMock() with Pyro5.client.Proxy("PYRO:9999@localhost:15555") as p: p._pyroConnection = connMock assert p._pyroConnection is None assert connMock.closeCalled connMock = ConnectionMock() with pytest.raises(ZeroDivisionError): with Pyro5.client.Proxy("PYRO:9999@localhost:15555") as p: p._pyroConnection = connMock print(1 // 0) # cause an error assert p._pyroConnection is None assert connMock.closeCalled p = Pyro5.client.Proxy("PYRO:9999@localhost:15555") with p: assert p._pyroUri p._pyroRelease() def testNoConnect(self): wrongUri = Pyro5.core.URI("PYRO:foobar@localhost:59999") with pytest.raises(Pyro5.errors.CommunicationError): with Pyro5.client.Proxy(wrongUri) as p: p.ping() def testTimeoutGetSet(self): class ConnectionMock(object): def __init__(self): self.timeout = config.COMMTIMEOUT self.keep_open = False def close(self): pass config.COMMTIMEOUT = None p = Pyro5.client.Proxy("PYRO:obj@host:555") assert p._pyroTimeout is None p._pyroTimeout = 5 assert p._pyroTimeout == 5 p = Pyro5.client.Proxy("PYRO:obj@host:555") p._pyroConnection = ConnectionMock() assert p._pyroTimeout is None p._pyroTimeout = 5 assert p._pyroTimeout == 5 assert p._pyroConnection.timeout == 5 config.COMMTIMEOUT = 2 p = Pyro5.client.Proxy("PYRO:obj@host:555") p._pyroConnection = ConnectionMock() assert p._pyroTimeout == 2 assert p._pyroConnection.timeout == 2 p._pyroTimeout = None assert p._pyroTimeout is None assert p._pyroConnection.timeout is None config.COMMTIMEOUT = None p._pyroRelease() def testCallbackDecorator(self): # just test the decorator itself, testing the callback # exception handling is kinda hard in unit tests. Maybe later. class Test(object): @Pyro5.server.callback def method(self): pass def method2(self): pass t = Test() assert getattr(t.method, "_pyroCallback", False) assert not getattr(t.method2, "_pyroCallback", False) def testProxyEquality(self): p1 = Pyro5.client.Proxy("PYRO:thing@localhost:15555") p2 = Pyro5.client.Proxy("PYRO:thing@localhost:15555") p3 = Pyro5.client.Proxy("PYRO:other@machine:16666") assert p1 == p2 assert not(p1 != p2) assert not(p1 == p3) assert p1 != p3 assert hash(p1) == hash(p2) assert not(hash(p1) == hash(p3)) assert not(p1 == 42) assert p1 != 42 p1._pyroRelease() p2._pyroRelease() p3._pyroRelease() class TestRemoteMethod: class BatchProxyMock(object): def __init__(self): self.result = [] self._pyroMaxRetries = 0 def __copy__(self): return self def __enter__(self): return self def __exit__(self, *args): pass def _pyroClaimOwnership(self): pass def _pyroInvokeBatch(self, calls, oneway=False): self.result = [] for methodname, args, kwargs in calls: if methodname == "error": self.result.append(Pyro5.core._ExceptionWrapper(ValueError("some exception"))) break # stop processing the rest, this is what Pyro should do in case of an error in a batch elif methodname == "pause": time.sleep(args[0]) self.result.append("INVOKED %s args=%s kwargs=%s" % (methodname, args, kwargs)) if oneway: return else: return self.result def testBatchMethod(self): proxy = self.BatchProxyMock() batch = Pyro5.client.BatchProxy(proxy) assert batch.foo(42) is None assert batch.bar("abc") is None assert batch.baz(42, "abc", arg=999) is None assert batch.error() is None # generate an exception assert batch.foo(42) is None # this call should not be performed after the error results = batch() result = next(results) assert result == "INVOKED foo args=(42,) kwargs={}" result = next(results) assert result == "INVOKED bar args=('abc',) kwargs={}" result = next(results) assert result == "INVOKED baz args=(42, 'abc') kwargs={'arg': 999}" with pytest.raises(ValueError): next(results) with pytest.raises(StopIteration): next(results) assert len(proxy.result) == 4 # should have done 4 calls, not 5 batch._pyroRelease() def testBatchMethodOneway(self): proxy = self.BatchProxyMock() batch = Pyro5.client.BatchProxy(proxy) assert batch.foo(42) is None assert batch.bar("abc") is None assert batch.baz(42, "abc", arg=999) is None assert batch.error() is None # generate an exception assert batch.foo(42) is None # this call should not be performed after the error results = batch(oneway=True) assert results is None # oneway always returns None assert len(proxy.result) == 4 # should have done 4 calls, not 5 def testBatchMethodReuse(self): proxy = self.BatchProxyMock() batch = Pyro5.client.BatchProxy(proxy) batch.foo(1) batch.foo(2) results = batch() assert list(results) == ['INVOKED foo args=(1,) kwargs={}', 'INVOKED foo args=(2,) kwargs={}'] # re-use the batch proxy: batch.foo(3) batch.foo(4) results = batch() assert list(results) == ['INVOKED foo args=(3,) kwargs={}', 'INVOKED foo args=(4,) kwargs={}'] results = batch() assert len(list(results)) == 0 Pyro5-5.15/tests/test_core.py000066400000000000000000000342101451404116400161650ustar00rootroot00000000000000""" Tests for the core logic. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import os import copy import uuid import pytest import importlib import logging import Pyro5.core import Pyro5.callcontext import Pyro5.client import Pyro5.errors import Pyro5.configure import Pyro5.server from Pyro5 import config class TestCore: def test_uri(self): with pytest.raises(Pyro5.errors.PyroError): Pyro5.core.URI("burp") u1 = Pyro5.core.URI("PYRO:obj@host:5555") u2 = copy.copy(u1) assert str(u1) == str(u2) assert u1 == u2 assert u1 is not u2 def test_unix_uri(self): p = Pyro5.core.URI("PYRO:12345@./u:/tmp/sockname") assert p.object == "12345" assert p.sockname == "/tmp/sockname" p = Pyro5.core.URI("PYRO:12345@./u:../sockname") assert p.object == "12345" assert p.sockname == "../sockname" p = Pyro5.core.URI("PYRO:12345@./u:/path with spaces/sockname ") assert p.object == "12345" assert p.sockname == "/path with spaces/sockname " def testConfig(self): assert type(config.COMPRESSION) is bool assert type(config.NS_PORT) is int cfgdict = config.as_dict() assert type(cfgdict) is dict assert "COMPRESSION" in cfgdict assert config.COMPRESSION == cfgdict["COMPRESSION"] def testConfigDefaults(self): # some security sensitive settings: config.reset(False) # reset the config to default assert config.HOST == "localhost" assert config.NS_HOST == "localhost" assert config.SERIALIZER == "serpent" def testConfigValid(self): with pytest.raises(AttributeError): config.XYZ_FOOBAR = True # don't want to allow weird config names def testConfigParseBool(self): config = Pyro5.configure.Configuration() assert type(config.COMPRESSION) is bool os.environ["PYRO_COMPRESSION"] = "yes" config.reset() assert config.COMPRESSION os.environ["PYRO_COMPRESSION"] = "off" config.reset() assert not config.COMPRESSION os.environ["PYRO_COMPRESSION"] = "foobar" with pytest.raises(ValueError): config.reset() del os.environ["PYRO_COMPRESSION"] config.reset() def testConfigDump(self): config = Pyro5.configure.Configuration() dump = config.dump() assert "version:" in dump assert "LOGLEVEL" in dump def testLogInit(self): _ = logging.getLogger("Pyro5") os.environ["PYRO_LOGLEVEL"] = "DEBUG" os.environ["PYRO_LOGFILE"] = "{stderr}" importlib.reload(Pyro5) _ = logging.getLogger("Pyro5") os.environ["PYRO_LOGFILE"] = "Pyro.log" importlib.reload(Pyro5) _ = logging.getLogger("Pyro5") del os.environ["PYRO_LOGLEVEL"] del os.environ["PYRO_LOGFILE"] importlib.reload(Pyro5) _ = logging.getLogger("Pyro5") def testUriStrAndRepr(self): uri = "PYRONAME:some_obj_name" p = Pyro5.core.URI(uri) assert str(p) == uri uri = "PYRONAME:some_obj_name@host.com" p = Pyro5.core.URI(uri) assert str(p) == uri + ":" + str(config.NS_PORT) # a PYRONAME uri with a hostname gets a port too if omitted uri = "PYRONAME:some_obj_name@host.com:8888" p = Pyro5.core.URI(uri) assert str(p) == uri expected = "" % id(p) assert repr(p) == expected uri = "PYRO:12345@host.com:9999" p = Pyro5.core.URI(uri) assert str(p) == uri uri = "PYRO:12345@./u:sockname" p = Pyro5.core.URI(uri) assert str(p) == uri uri = "PYRO:12345@./u:sockname" assert str(p) == uri assert type(p.sockname) is str uri = "PYRO:12345@./u:sock name with strings" p = Pyro5.core.URI(uri) assert str(p) == uri def testUriParsingPyro(self): p = Pyro5.core.URI("PYRONAME:some_obj_name") assert p.protocol == "PYRONAME" assert p.object == "some_obj_name" assert p.host is None assert p.sockname is None assert p.port is None p = Pyro5.core.URI("PYRONAME:some_obj_name@host.com:9999") assert p.protocol == "PYRONAME" assert p.object == "some_obj_name" assert p.host == "host.com" assert p.port == 9999 p = Pyro5.core.URI("PYRO:12345@host.com:4444") assert p.protocol == "PYRO" assert p.object == "12345" assert p.host == "host.com" assert p.sockname is None assert p.port == 4444 assert p.location == "host.com:4444" p = Pyro5.core.URI("PYRO:12345@./u:sockname") assert p.object == "12345" assert p.sockname == "sockname" p = Pyro5.core.URI("PYRO:12345@./u:/tmp/sockname") assert p.object == "12345" assert p.sockname == "/tmp/sockname" p = Pyro5.core.URI("PYRO:12345@./u:/path with spaces/sockname ") assert p.object == "12345" assert p.sockname == "/path with spaces/sockname " p = Pyro5.core.URI("PYRO:12345@./u:../sockname") assert p.object == "12345" assert p.sockname == "../sockname" p = Pyro5.core.URI("pyro:12345@host.com:4444") assert p.protocol == "PYRO" assert p.object == "12345" assert p.host == "host.com" assert p.sockname is None assert p.port == 4444 def testUriParsingIpv6(self): p = Pyro5.core.URI("pyro:12345@[::1]:4444") assert p.host == "::1" assert p.location == "[::1]:4444" with pytest.raises(Pyro5.errors.PyroError) as e: Pyro5.core.URI("pyro:12345@[[::1]]:4444") assert str(e.value) == "invalid ipv6 address: enclosed in too many brackets" with pytest.raises(Pyro5.errors.PyroError) as e: Pyro5.core.URI("pyro:12345@[must_be_numeric_here]:4444") assert str(e.value) == "invalid ipv6 address: the part between brackets must be a numeric ipv6 address" def testUriParsingPyroname(self): p = Pyro5.core.URI("PYRONAME:objectname") assert p.protocol == "PYRONAME" assert p.object == "objectname" assert p.host is None assert p.port is None p = Pyro5.core.URI("PYRONAME:objectname@nameserverhost") assert p.protocol == "PYRONAME" assert p.object == "objectname" assert p.host == "nameserverhost" assert p.port == config.NS_PORT # Pyroname uri with host gets a port too if not specified p = Pyro5.core.URI("PYRONAME:objectname@nameserverhost:4444") assert p.protocol == "PYRONAME" assert p.object == "objectname" assert p.host == "nameserverhost" assert p.port == 4444 p = Pyro5.core.URI("PyroName:some_obj_name@host.com:9999") assert p.protocol == "PYRONAME" p = Pyro5.core.URI("pyroname:some_obj_name@host.com:9999") assert p.protocol == "PYRONAME" def testUriParsingPyrometa(self): p = Pyro5.core.URI("PYROMETA:meta") assert p.protocol == "PYROMETA" assert p.object == {"meta"} assert p.host is None assert p.port is None p = Pyro5.core.URI("PYROMETA:meta1,meta2,meta2@nameserverhost") assert p.protocol == "PYROMETA" assert p.object == {"meta1", "meta2"} assert p.host == "nameserverhost" assert p.port == config.NS_PORT # PyroMeta uri with host gets a port too if not specified p = Pyro5.core.URI("PYROMETA:meta@nameserverhost:4444") assert p.protocol == "PYROMETA" assert p.object == {"meta"} assert p.host == "nameserverhost" assert p.port == 4444 p = Pyro5.core.URI("PyroMeta:meta1,meta2@host.com:9999") assert p.protocol == "PYROMETA" p = Pyro5.core.URI("PyroMeta:meta1,meta2@host.com:9999") assert p.protocol == "PYROMETA" def testInvalidUris(self): with pytest.raises(TypeError): Pyro5.core.URI(None) with pytest.raises(TypeError): Pyro5.core.URI(99999) with pytest.raises(Pyro5.errors.PyroError): Pyro5.core.URI("") with pytest.raises(Pyro5.errors.PyroError): Pyro5.core.URI(" ") with pytest.raises(Pyro5.errors.PyroError): Pyro5.core.URI("a") with pytest.raises(Pyro5.errors.PyroError): Pyro5.core.URI("PYR") with pytest.raises(Pyro5.errors.PyroError): Pyro5.core.URI("PYRO") with pytest.raises(Pyro5.errors.PyroError): Pyro5.core.URI("PYRO:") with pytest.raises(Pyro5.errors.PyroError): Pyro5.core.URI("PYRO::") with pytest.raises(Pyro5.errors.PyroError): Pyro5.core.URI("PYRO:a") with pytest.raises(Pyro5.errors.PyroError): Pyro5.core.URI("PYRO:x@") with pytest.raises(Pyro5.errors.PyroError): Pyro5.core.URI("PYRO:x@hostname") with pytest.raises(Pyro5.errors.PyroError): Pyro5.core.URI("PYRO:@hostname:portstr") with pytest.raises(Pyro5.errors.PyroError): Pyro5.core.URI("PYRO:@hostname:7766") with pytest.raises(Pyro5.errors.PyroError): Pyro5.core.URI("PYRO:objid@hostname:7766:bogus") with pytest.raises(Pyro5.errors.PyroError): Pyro5.core.URI("PYRO:obj id@hostname:7766") with pytest.raises(Pyro5.errors.PyroError): Pyro5.core.URI("PYROLOC:objname") with pytest.raises(Pyro5.errors.PyroError): Pyro5.core.URI("PYROLOC:objname@host") with pytest.raises(Pyro5.errors.PyroError): Pyro5.core.URI("PYROLOC:objectname@hostname:4444") with pytest.raises(Pyro5.errors.PyroError): Pyro5.core.URI("PYRONAME:") with pytest.raises(Pyro5.errors.PyroError): Pyro5.core.URI("PYRONAME:obj name@nameserver:bogus") with pytest.raises(Pyro5.errors.PyroError): Pyro5.core.URI("PYRONAME:objname@nameserver:bogus") with pytest.raises(Pyro5.errors.PyroError): Pyro5.core.URI("PYRONAME:objname@nameserver:7766:bogus") with pytest.raises(Pyro5.errors.PyroError): Pyro5.core.URI("PYROMETA:") with pytest.raises(Pyro5.errors.PyroError): Pyro5.core.URI("PYROMETA:meta@nameserver:bogus") with pytest.raises(Pyro5.errors.PyroError): Pyro5.core.URI("PYROMETA:meta@nameserver:7766:bogus") with pytest.raises(Pyro5.errors.PyroError): Pyro5.core.URI("PYROMETA:meta1, m2 ,m3@nameserver:7766:bogus") with pytest.raises(Pyro5.errors.PyroError): Pyro5.core.URI("FOOBAR:") with pytest.raises(Pyro5.errors.PyroError): Pyro5.core.URI("FOOBAR:objid@hostname:7766") with pytest.raises(Pyro5.errors.PyroError): Pyro5.core.URI("PYRO:12345@./u:sockname:9999") def testUriUnicode(self): p = Pyro5.core.URI("PYRO:12345@host.com:4444") assert p.protocol == "PYRO" assert p.object == "12345" assert p.host == "host.com" assert type(p.protocol) is str assert type(p.object) is str assert type(p.host) is str assert p.sockname is None assert p.port == 4444 uri = "PYRO:12345@hostname:9999" p = Pyro5.core.URI(uri) unicodeuri = "PYRO:weirdchars" + chr(0x20ac) + "@host" + chr(0x20AC) + ".com:4444" pu = Pyro5.core.URI(unicodeuri) assert pu.protocol == "PYRO" assert pu.host == "host" + chr(0x20AC) + ".com" assert pu.object == "weirdchars" + chr(0x20AC) assert str(pu) == "PYRO:weirdchars" + chr(0x20ac) + "@host" + chr(0x20ac) + ".com:4444" expected = ("") % id(pu) assert repr(pu) == expected assert str(pu) == "PYRO:weirdchars" + chr(0x20ac) + "@host" + chr(0x20ac) + ".com:4444" def testUriCopy(self): p1 = Pyro5.core.URI("PYRO:12345@hostname:9999") p2 = Pyro5.core.URI(p1) p3 = copy.copy(p1) assert p2.protocol == p1.protocol assert p2.host == p1.host assert p2.port == p1.port assert p2.object == p1.object assert p2 == p1 assert p3.protocol == p1.protocol assert p3.host == p1.host assert p3.port == p1.port assert p3.object == p1.object assert p3 == p1 def testUriSubclassCopy(self): class SubURI(Pyro5.core.URI): pass u = SubURI("PYRO:12345@hostname:9999") u2 = copy.copy(u) assert isinstance(u2, SubURI) def testUriEqual(self): p1 = Pyro5.core.URI("PYRO:12345@host.com:9999") p2 = Pyro5.core.URI("PYRO:12345@host.com:9999") p3 = Pyro5.core.URI("PYRO:99999@host.com:4444") assert p2 == p1 assert p1 != p3 assert p2 != p3 assert p1 == p2 assert p1 != p3 assert p2 != p3 assert p1 == p2 assert p1 != p3 assert p2 != p3 assert hash(p1) == hash(p2) assert hash(p1) != hash(p3) p2.port = 4444 p2.object = "99999" assert p1 != p2 assert p3 == p2 assert p1 != p2 assert p2 == p3 assert p1 != p2 assert p2 == p3 assert hash(p1) != hash(p2) assert hash(p2) == hash(p3) assert p1 != 42 def testLocation(self): assert Pyro5.core.URI.isUnixsockLocation("./u:name") assert not(Pyro5.core.URI.isUnixsockLocation("./p:name")) assert not(Pyro5.core.URI.isUnixsockLocation("./x:name")) assert not(Pyro5.core.URI.isUnixsockLocation("foobar")) def testCallContext(self): ctx = Pyro5.callcontext.current_context corr_id = uuid.UUID('1897022f-c481-4117-a4cc-cbd1ca100582') ctx.correlation_id = corr_id d = ctx.to_global() assert isinstance(d, dict) assert d["correlation_id"] == corr_id corr_id2 = uuid.UUID('67b05ad9-2d6a-4ed8-8ed5-95cba68b4cf9') d["correlation_id"] = corr_id2 ctx.from_global(d) assert Pyro5.callcontext.current_context.correlation_id == corr_id2 Pyro5.callcontext.current_context.correlation_id = None Pyro5-5.15/tests/test_daemon.py000066400000000000000000000665051451404116400165140ustar00rootroot00000000000000""" Tests for the daemon. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import time import socket import uuid import pytest import Pyro5.core import Pyro5.client import Pyro5.server import Pyro5.nameserver import Pyro5.protocol import Pyro5.socketutil import Pyro5.serializers from Pyro5.errors import DaemonError, PyroError from Pyro5 import config from Pyro5.callcontext import current_context from support import * class MyObj(object): def __init__(self, arg): self.arg = arg def __eq__(self, other): return self.arg == other.arg __hash__ = object.__hash__ class CustomDaemonInterface(Pyro5.server.DaemonObject): def __init__(self, daemon): super(CustomDaemonInterface, self).__init__(daemon) def custom_daemon_method(self): return 42 class TestDaemon: # We create a daemon, but notice that we are not actually running the requestloop. # 'on-line' tests are all taking place in another test, to keep this one simple. def setUp(self): config.POLLTIMEOUT = 0.1 def sendHandshakeMessage(self, conn, correlation_id=None): ser = Pyro5.serializers.serializers_by_id[Pyro5.serializers.MarshalSerializer.serializer_id] data = ser.dumps({"handshake": "hello", "object": Pyro5.core.DAEMON_NAME}) current_context.correlation_id = correlation_id msg = Pyro5.protocol.SendingMessage(Pyro5.protocol.MSG_CONNECT, 0, 99, Pyro5.serializers.MarshalSerializer.serializer_id, data) conn.send(msg.data) def testSerializerAccepted(self): with Pyro5.server.Daemon(port=0) as d: msg = Pyro5.protocol.SendingMessage(Pyro5.protocol.MSG_INVOKE, 0, 0, Pyro5.serializers.MarshalSerializer.serializer_id, b"") cm = ConnectionMock(msg) d.handleRequest(cm) msg = Pyro5.protocol.SendingMessage(Pyro5.protocol.MSG_INVOKE, 0, 0, Pyro5.serializers.JsonSerializer.serializer_id, b"") cm = ConnectionMock(msg) d.handleRequest(cm) msg = Pyro5.protocol.SendingMessage(Pyro5.protocol.MSG_INVOKE, 0, 0, Pyro5.serializers.SerpentSerializer.serializer_id, b"") cm = ConnectionMock(msg) d.handleRequest(cm) if "msgpack" in Pyro5.serializers.serializers: msg = Pyro5.protocol.SendingMessage(Pyro5.protocol.MSG_INVOKE, 0, 0, Pyro5.serializers.MsgpackSerializer.serializer_id, b"") cm = ConnectionMock(msg) d.handleRequest(cm) def testDaemon(self): with Pyro5.server.Daemon(port=0) as d: hostname, port = d.locationStr.split(":") port = int(port) assert Pyro5.core.DAEMON_NAME in d.objectsById assert str(d.uriFor(Pyro5.core.DAEMON_NAME)) == "PYRO:" + Pyro5.core.DAEMON_NAME + "@" + d.locationStr # check the string representations expected = "" % (id(d), d.locationStr, Pyro5.socketutil.family_str(d.sock)) assert str(d) == expected assert repr(d) == expected sockname = d.sock.getsockname() assert sockname[1] == port daemonobj = d.objectsById[Pyro5.core.DAEMON_NAME] daemonobj.ping() daemonobj.registered() def testDaemonCustomInterface(self): with Pyro5.server.Daemon(port=0, interface=CustomDaemonInterface) as d: obj = d.objectsById[Pyro5.core.DAEMON_NAME] assert obj.custom_daemon_method() == 42 def testDaemonConnectedSocket(self): try: Pyro5.config.SERVERTYPE = "thread" with Pyro5.server.Daemon() as d: assert "Thread" in d.transportServer.__class__.__name__ s1, s2 = socket.socketpair() with Pyro5.server.Daemon(connected_socket=s1) as d: assert d.locationStr=="./u:<>" or d.locationStr.startswith("127.0.") assert not("Thread" in d.transportServer.__class__.__name__) assert "Existing" in d.transportServer.__class__.__name__ Pyro5.config.SERVERTYPE = "multiplex" with Pyro5.server.Daemon() as d: assert "Multiplex" in d.transportServer.__class__.__name__ s1, s2 = socket.socketpair() with Pyro5.server.Daemon(connected_socket=s1) as d: assert d.locationStr=="./u:<>" or d.locationStr.startswith("127.0.") assert not("Multiplex" in d.transportServer.__class__.__name__) assert "Existing" in d.transportServer.__class__.__name__ finally: Pyro5.config.SERVERTYPE = "thread" def testDaemonUnixSocket(self): if hasattr(socket, "AF_UNIX"): SOCKNAME = "test_unixsocket" with Pyro5.server.Daemon(unixsocket=SOCKNAME) as d: locationstr = "./u:" + SOCKNAME assert d.locationStr == locationstr assert str(d.uriFor(Pyro5.core.DAEMON_NAME)) == "PYRO:" + Pyro5.core.DAEMON_NAME + "@" + locationstr # check the string representations expected = "" % (id(d), locationstr) assert str(d) == expected assert d.sock.getsockname() == SOCKNAME assert d.sock.family == socket.AF_UNIX def testDaemonUnixSocketAbstractNS(self): if hasattr(socket, "AF_UNIX"): SOCKNAME = "\0test_unixsocket" # mind the \0 at the start, for a Linux abstract namespace socket with Pyro5.server.Daemon(unixsocket=SOCKNAME) as d: locationstr = "./u:" + SOCKNAME assert d.locationStr == locationstr assert str(d.uriFor(Pyro5.core.DAEMON_NAME)) == "PYRO:" + Pyro5.core.DAEMON_NAME + "@" + locationstr # check the string representations expected = "" % (id(d), locationstr) assert str(d) == expected sn_bytes = bytes(SOCKNAME, "ascii") assert d.sock.getsockname() == sn_bytes assert d.sock.family == socket.AF_UNIX def testServertypeThread(self): old_servertype = config.SERVERTYPE config.SERVERTYPE = "thread" with Pyro5.server.Daemon(port=0) as d: assert d.sock in d.sockets, "daemon's socketlist should contain the server socket" assert len(d.sockets) == 1, "daemon without connections should have just 1 socket" config.SERVERTYPE = old_servertype def testServertypeMultiplex(self): old_servertype = config.SERVERTYPE config.SERVERTYPE = "multiplex" with Pyro5.server.Daemon(port=0) as d: assert d.sock in d.sockets, "daemon's socketlist should contain the server socket" assert len(d.sockets) == 1, "daemon without connections should have just 1 socket" config.SERVERTYPE = old_servertype def testServertypeFoobar(self): old_servertype = config.SERVERTYPE config.SERVERTYPE = "foobar" try: with pytest.raises(PyroError): Pyro5.server.Daemon() finally: config.SERVERTYPE = old_servertype def testRegisterTwice(self): with Pyro5.server.Daemon(port=0) as d: o1 = MyObj("object1") d.register(o1) with pytest.raises(DaemonError) as x: d.register(o1) assert str(x.value) == "object or class already has a Pyro id" d.unregister(o1) d.register(o1, "samename") o2 = MyObj("object2") with pytest.raises(DaemonError) as x: d.register(o2, "samename") assert str(x.value) == "an object or class is already registered with that id" assert hasattr(o1, "_pyroId") assert hasattr(o1, "_pyroDaemon") d.unregister(o1) assert not(hasattr(o1, "_pyroId")) assert not(hasattr(o1, "_pyroDaemon")) o1._pyroId = "FOOBAR" with pytest.raises(DaemonError) as x: d.register(o1) assert str(x.value) == "object or class already has a Pyro id" o1._pyroId = "" d.register(o1) # with empty-string _pyroId register should work def testRegisterTwiceForced(self): with Pyro5.server.Daemon(port=0) as d: o1 = MyObj("object1") d.register(o1, "name1") d.register(o1, "name2", force=True) d.register(o1, "name1", force=True) assert d.objectsById["name1"] is d.objectsById["name2"] d.unregister(o1) o1._pyroId = "FOOBAR_ID" d.register(o1, "newname", force=True) assert o1._pyroId == "newname" assert "newname" in d.objectsById def testRegisterEtc(self): with Pyro5.server.Daemon(port=0) as d: assert len(d.objectsById) == 1 o1 = MyObj("object1") o2 = MyObj("object2") d.register(o1) with pytest.raises(DaemonError): d.register(o2, Pyro5.core.DAEMON_NAME) # cannot use daemon name d.register(o2, "obj2a") assert len(d.objectsById) == 3 assert d.objectsById[o1._pyroId] == o1 assert d.objectsById["obj2a"] == o2 assert o2._pyroId == "obj2a" assert o2._pyroDaemon == d # test unregister d.unregister("unexisting_thingie") with pytest.raises(ValueError): d.unregister(None) d.unregister("obj2a") d.unregister(o1._pyroId) assert len(d.objectsById) == 1 assert o1._pyroId not in d.objectsById assert o2._pyroId not in d.objectsById # test unregister objects del o2._pyroId d.register(o2) objectid = o2._pyroId assert objectid in d.objectsById assert len(d.objectsById) == 2 d.unregister(o2) # no more _pyro attributes must remain after unregistering for attr in vars(o2): assert not(attr.startswith("_pyro")) assert len(d.objectsById) == 1 assert objectid not in d.objectsById with pytest.raises(DaemonError): d.unregister([1,2,3]) # test unregister daemon name d.unregister(Pyro5.core.DAEMON_NAME) assert Pyro5.core.DAEMON_NAME in d.objectsById # weird args w = MyObj("weird") with pytest.raises(AttributeError): d.register(None) with pytest.raises(AttributeError): d.register(4444) with pytest.raises(TypeError): d.register(w, 666) # uri return value from register uri = d.register(MyObj("xyz")) assert isinstance(uri, Pyro5.core.URI) uri = d.register(MyObj("xyz"), "test.register") assert uri.object == "test.register" def testRegisterClass(self): with Pyro5.server.Daemon(port=0) as d: assert len(d.objectsById) == 1 d.register(MyObj) with pytest.raises(DaemonError): d.register(MyObj) assert len(d.objectsById) == 2 d.uriFor(MyObj) # unregister: d.unregister(MyObj) assert len(d.objectsById) == 1 def testRegisterUnicode(self): with Pyro5.server.Daemon(port=0) as d: myobj1 = MyObj("hello1") myobj3 = MyObj("hello3") uri1 = d.register(myobj1, "str_name") uri3 = d.register(myobj3, "unicode_" + chr(0x20ac)) assert len(d.objectsById) == 3 uri = d.uriFor(myobj1) assert uri == uri1 _ = Pyro5.client.Proxy(uri) uri = d.uriFor(myobj3) assert uri == uri3 _ = Pyro5.client.Proxy(uri) uri = d.uriFor("str_name") assert uri == uri1 _ = Pyro5.client.Proxy(uri) _ = Pyro5.client.Proxy(uri) uri = d.uriFor("unicode_" + chr(0x20ac)) assert uri == uri3 _ = Pyro5.client.Proxy(uri) def testDaemonObject(self): with Pyro5.server.Daemon(port=0) as d: daemon = Pyro5.server.DaemonObject(d) obj1 = MyObj("object1") obj2 = MyObj("object2") obj3 = MyObj("object2") d.register(obj1, "obj1") d.register(obj2, "obj2") d.register(obj3) daemon.ping() registered = daemon.registered() assert type(registered) is list assert len(registered) == 4 assert "obj1" in registered assert "obj2" in registered assert obj3._pyroId in registered d.shutdown() def testUriFor(self): d = Pyro5.server.Daemon(port=0) try: o1 = MyObj("object1") o2 = MyObj("object2") with pytest.raises(DaemonError): d.uriFor(o1) with pytest.raises(DaemonError): d.uriFor(o2) d.register(o1, None) d.register(o2, "object_two") o3 = MyObj("object3") with pytest.raises(DaemonError): d.uriFor(o3) # can't get an uri for an unregistered object (note: unregistered name is ok) u1 = d.uriFor(o1) u2 = d.uriFor(o2._pyroId) u3 = d.uriFor("unexisting_thingie") # unregistered name is no problem, it's just an uri we're requesting u4 = d.uriFor(o2) assert type(u1) == Pyro5.core.URI assert u1.protocol == "PYRO" assert u2.protocol == "PYRO" assert u3.protocol == "PYRO" assert u4.protocol == "PYRO" assert u4.object == "object_two" assert u3 == Pyro5.core.URI("PYRO:unexisting_thingie@" + d.locationStr) finally: d.close() def testDaemonWithStmt(self): d = Pyro5.server.Daemon() assert d.transportServer d.close() # closes the transportserver and sets it to None assert d.transportServer is None with Pyro5.server.Daemon() as d: assert d.transportServer pass assert d.transportServer is None with pytest.raises(ZeroDivisionError): with Pyro5.server.Daemon() as d: print(1 // 0) # cause an error assert d.transportServer is None d = Pyro5.server.Daemon() with d: pass with pytest.raises(Pyro5.errors.PyroError): with d: pass d.close() def testRequestloopCondition(self): with Pyro5.server.Daemon(port=0) as d: condition = lambda: False start = time.time() d.requestLoop(loopCondition=condition) # this should return almost immediately duration = time.time() - start assert duration < 0.4 def testSimpleHandshake(self): conn = ConnectionMock() with Pyro5.server.Daemon(port=0) as d: self.sendHandshakeMessage(conn) success = d._handshake(conn) assert success msg = Pyro5.protocol.recv_stub(conn) assert msg.type == Pyro5.protocol.MSG_CONNECTOK assert msg.seq == 99 def testHandshakeDenied(self): class HandshakeFailDaemon(Pyro5.server.Daemon): def validateHandshake(self, conn, data): raise ValueError("handshake fail validation error") conn = ConnectionMock() with HandshakeFailDaemon(port=0) as d: self.sendHandshakeMessage(conn) success = d._handshake(conn) assert not(success) msg = Pyro5.protocol.recv_stub(conn) assert msg.type == Pyro5.protocol.MSG_CONNECTFAIL assert msg.seq == 99 assert b"handshake fail validation error" in msg.data with Pyro5.server.Daemon(port=0) as d: self.sendHandshakeMessage(conn) success = d._handshake(conn, denied_reason="no way, handshake denied") assert not(success) msg = Pyro5.protocol.recv_stub(conn) assert msg.type == Pyro5.protocol.MSG_CONNECTFAIL assert msg.seq == 99 assert b"no way, handshake denied" in msg.data def testCustomHandshake(self): conn = ConnectionMock() class CustomHandshakeDaemon(Pyro5.server.Daemon): def validateHandshake(self, conn, data): return ["sure", "have", "fun"] def annotations(self): return {"XYZZ": b"custom annotation set by daemon"} with CustomHandshakeDaemon(port=0) as d: corr_id = uuid.uuid4() self.sendHandshakeMessage(conn, correlation_id=corr_id) assert current_context.correlation_id == corr_id success = d._handshake(conn) assert success msg = Pyro5.protocol.recv_stub(conn) assert msg.type == Pyro5.protocol.MSG_CONNECTOK assert msg.seq == 99 assert len(msg.annotations) == 1 assert msg.annotations["XYZZ"] == b"custom annotation set by daemon" ser = Pyro5.serializers.serializers_by_id[msg.serializer_id] data = ser.loads(msg.data) assert data["handshake"] == ["sure", "have", "fun"] def testNAT(self): with Pyro5.server.Daemon() as d: assert d.natLocationStr is None with Pyro5.server.Daemon(nathost="nathosttest", natport=12345) as d: assert d.natLocationStr == "nathosttest:12345" assert d.natLocationStr != d.locationStr uri = d.register(MyObj(1)) assert uri.location == "nathosttest:12345" uri = d.uriFor("object") assert uri.location == "nathosttest:12345" uri = d.uriFor("object", nat=False) assert uri.location != "nathosttest:12345" d = Pyro5.server.Daemon(nathost="bla") assert d.natLocationStr.startswith("bla:") with pytest.raises(ValueError): Pyro5.server.Daemon(natport=5555) with pytest.raises(ValueError): Pyro5.server.Daemon(nathost="bla", natport=5555, unixsocket="testsock") def testNATzeroPort(self): servertype = config.SERVERTYPE try: config.SERVERTYPE = "multiplex" with Pyro5.server.Daemon(nathost="nathosttest", natport=99999) as d: host, port = d.locationStr.split(":") assert port != 99999 assert d.natLocationStr == "nathosttest:99999" with Pyro5.server.Daemon(nathost="nathosttest", natport=0) as d: host, port = d.locationStr.split(":") assert d.natLocationStr == "nathosttest:%s" % port config.SERVERTYPE = "thread" with Pyro5.server.Daemon(nathost="nathosttest", natport=99999) as d: host, port = d.locationStr.split(":") assert port != 99999 assert d.natLocationStr == "nathosttest:99999" with Pyro5.server.Daemon(nathost="nathosttest", natport=0) as d: host, port = d.locationStr.split(":") assert d.natLocationStr == "nathosttest:%s" % port finally: config.SERVERTYPE = servertype def testNATconfig(self): try: config.NATHOST = None config.NATPORT = 0 with Pyro5.server.Daemon() as d: assert d.natLocationStr is None config.NATHOST = "nathosttest" config.NATPORT = 12345 with Pyro5.server.Daemon() as d: assert d.natLocationStr == "nathosttest:12345" finally: config.NATHOST = None config.NATPORT = 0 def testBehaviorDefaults(self): class TestClass: pass with Pyro5.server.Daemon() as d: d.register(TestClass) instance_mode, instance_creator = TestClass._pyroInstancing assert instance_mode == "session" assert instance_creator is None def testInstanceCreationSingle(self): def creator(clazz): return clazz("testname") @Pyro5.server.behavior(instance_mode="single", instance_creator=creator) class TestClass: def __init__(self, name): self.name = name conn = Pyro5.socketutil.SocketConnection(socket.socket()) d = Pyro5.server.Daemon() instance1 = d._getInstance(TestClass, conn) instance2 = d._getInstance(TestClass, conn) assert instance1.name == "testname" assert instance1 is instance2 assert TestClass in d._pyroInstances assert instance1 is d._pyroInstances[TestClass] assert not(TestClass in conn.pyroInstances) def testBehaviorDefaultsIsSession(self): class ClassWithDefaults: def __init__(self): self.name = "yep" conn1 = Pyro5.socketutil.SocketConnection(socket.socket()) conn2 = Pyro5.socketutil.SocketConnection(socket.socket()) d = Pyro5.server.Daemon() d.register(ClassWithDefaults) instance1a = d._getInstance(ClassWithDefaults, conn1) instance1b = d._getInstance(ClassWithDefaults, conn1) instance2a = d._getInstance(ClassWithDefaults, conn2) instance2b = d._getInstance(ClassWithDefaults, conn2) assert instance1a is instance1b assert instance2a is instance2b assert instance1a is not instance2a assert not(ClassWithDefaults in d._pyroInstances) assert ClassWithDefaults in conn1.pyroInstances assert ClassWithDefaults in conn2.pyroInstances assert instance1a is conn1.pyroInstances[ClassWithDefaults] assert instance2a is conn2.pyroInstances[ClassWithDefaults] def testInstanceCreationSession(self): def creator(clazz): return clazz("testname") @Pyro5.server.behavior(instance_mode="session", instance_creator=creator) class ClassWithDecorator: def __init__(self, name): self.name = name conn1 = Pyro5.socketutil.SocketConnection(socket.socket()) conn2 = Pyro5.socketutil.SocketConnection(socket.socket()) d = Pyro5.server.Daemon() d.register(ClassWithDecorator) # check the class with the decorator first instance1a = d._getInstance(ClassWithDecorator, conn1) instance1b = d._getInstance(ClassWithDecorator, conn1) instance2a = d._getInstance(ClassWithDecorator, conn2) instance2b = d._getInstance(ClassWithDecorator, conn2) assert instance1a is instance1b assert instance2a is instance2b assert instance1a is not instance2a assert not(ClassWithDecorator in d._pyroInstances) assert ClassWithDecorator in conn1.pyroInstances assert ClassWithDecorator in conn2.pyroInstances assert instance1a is conn1.pyroInstances[ClassWithDecorator] assert instance2a is conn2.pyroInstances[ClassWithDecorator] def testInstanceCreationPerCall(self): def creator(clazz): return clazz("testname") @Pyro5.server.behavior(instance_mode="percall", instance_creator=creator) class TestClass: def __init__(self, name): self.name = name with Pyro5.socketutil.SocketConnection(socket.socket()) as conn: with Pyro5.server.Daemon() as d: instance1 = d._getInstance(TestClass, conn) instance2 = d._getInstance(TestClass, conn) assert instance1 is not instance2 assert not(TestClass in d._pyroInstances) assert not(TestClass in conn.pyroInstances) def testInstanceCreationWrongType(self): def creator(clazz): return Pyro5.core.URI("PYRO:test@localhost:9999") @Pyro5.server.behavior(instance_creator=creator) class TestClass: def method(self): pass with Pyro5.socketutil.SocketConnection(socket.socket()) as conn: with Pyro5.server.Daemon() as d: with pytest.raises(TypeError): d._getInstance(TestClass, conn) def testCombine(self): d1 = Pyro5.server.Daemon() d2 = Pyro5.server.Daemon() with pytest.raises(TypeError): d1.combine(d2) d1.close() d2.close() try: config.SERVERTYPE = "multiplex" d1 = Pyro5.server.Daemon() d2 = Pyro5.server.Daemon() nsuri, nsd, bcd = Pyro5.nameserver.start_ns(host="", bchost="") d1_selector = d1.transportServer.selector d1.combine(d2) d1.combine(nsd) d1.combine(bcd) assert d1_selector is d1.transportServer.selector assert d1_selector is d2.transportServer.selector assert d1_selector is nsd.transportServer.selector assert d1_selector is bcd.transportServer.selector assert len(d1.sockets) == 4 assert d1.sock in d1.sockets assert d2.sock in d1.sockets assert nsd.sock in d1.sockets assert bcd in d1.sockets bcd.close() nsd.close() d2.close() d1.close() finally: config.SERVERTYPE = "thread" class TestMetaInfo: def testMeta(self): with Pyro5.server.Daemon() as d: daemon_obj = d.objectsById[Pyro5.core.DAEMON_NAME] assert len(daemon_obj.info()) > 10 meta = daemon_obj.get_metadata(Pyro5.core.DAEMON_NAME) assert meta["methods"] == {"get_metadata", "get_next_stream_item", "close_stream", "info", "ping", "registered"} def testMetaSerialization(self): with Pyro5.server.Daemon() as d: daemon_obj = d.objectsById[Pyro5.core.DAEMON_NAME] meta = daemon_obj.get_metadata(Pyro5.core.DAEMON_NAME) for ser_id in [Pyro5.serializers.JsonSerializer.serializer_id, Pyro5.serializers.MarshalSerializer.serializer_id, Pyro5.serializers.SerpentSerializer.serializer_id]: serializer = Pyro5.serializers.serializers_by_id[ser_id] data = serializer.dumps(meta) _ = serializer.loads(data) def testMetaResetCache(self): class Dummy: @Pyro5.server.expose def method(self): pass with Pyro5.server.Daemon() as d: dummy = Dummy() uri = d.register(dummy) daemon_obj = d.objectsById[Pyro5.core.DAEMON_NAME] meta = daemon_obj.get_metadata(uri.object) assert "newly_added_method" not in meta["methods"] assert "newly_added_method_two" not in meta["methods"] Dummy.newly_added_method = Pyro5.server.expose(lambda self: None) meta = daemon_obj.get_metadata(uri.object) assert "newly_added_method" not in meta["methods"] d.resetMetadataCache(uri.object) meta = daemon_obj.get_metadata(uri.object) assert "newly_added_method" in meta["methods"] Dummy.newly_added_method_two = Pyro5.server.expose(lambda self: None) d.resetMetadataCache(dummy) meta = daemon_obj.get_metadata(uri.object) assert "newly_added_method_two" in meta["methods"] del Dummy.newly_added_method del Dummy.newly_added_method_two Pyro5-5.15/tests/test_echoserver.py000066400000000000000000000052541451404116400174100ustar00rootroot00000000000000""" Tests for the built-in test echo server. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import time import pytest from threading import Thread, Event import Pyro5.client import Pyro5.errors import Pyro5.utils.echoserver as echoserver from Pyro5 import config class EchoServerThread(Thread): def __init__(self): super(EchoServerThread, self).__init__() self.daemon = True self.started = Event() self.echodaemon = self.echoserver = self.uri = None def run(self): self.echodaemon, self.echoserver, self.uri = echoserver.main(args=["-q"], returnWithoutLooping=True) self.started.set() self.echodaemon.requestLoop(loopCondition=lambda: not self.echoserver._must_shutdown) class TestEchoserver: def setup_method(self): self.echoserverthread = EchoServerThread() self.echoserverthread.start() self.echoserverthread.started.wait() self.uri = self.echoserverthread.uri def teardown_method(self): self.echoserverthread.echodaemon.shutdown() time.sleep(0.02) self.echoserverthread.join() config.SERVERTYPE = "thread" def testExposed(self): e = Pyro5.utils.echoserver.EchoServer() assert hasattr(e, "_pyroExposed") def testEcho(self): with Pyro5.client.Proxy(self.uri) as echo: try: assert echo.echo("hello") == "hello" assert echo.echo(None) is None assert echo.echo([1,2,3]) == [1,2,3] finally: echo.shutdown() def testError(self): with Pyro5.client.Proxy(self.uri) as echo: with pytest.raises(Exception) as x: echo.error() tb = "".join(Pyro5.errors.get_pyro_traceback(x.type, x.value, x.tb)) assert "Remote traceback" in tb assert "ValueError" in tb assert str(x.value) == "this is the generated error from echoserver echo() method" with pytest.raises(Exception) as x: echo.error_with_text() tb = "".join(Pyro5.errors.get_pyro_traceback(x.type, x.value, x.tb)) assert "Remote traceback" in tb assert "ValueError" in tb assert str(x.value) == "the message of the error" def testGenerator(self): with Pyro5.client.Proxy(self.uri) as echo: remotegenerator = echo.generator() assert isinstance(remotegenerator, Pyro5.client._StreamResultIterator) next(remotegenerator) next(remotegenerator) next(remotegenerator) with pytest.raises(StopIteration): next(remotegenerator) Pyro5-5.15/tests/test_errors.py000066400000000000000000000121011451404116400165440ustar00rootroot00000000000000import sys import os import io import pytest from Pyro5 import config, errors def crash(arg=100): pre1 = "black" pre2 = 999 def nest(p1, p2): q = "white" + pre1 x = pre2 y = arg // 2 p3 = p1 // p2 return p3 a = 10 b = 0 s = "hello" c = nest(a, b) return c class TestErrors: def testFormatTracebackNormal(self): with pytest.raises(ZeroDivisionError) as x: crash() tb = "".join(errors.format_traceback(x.type, x.value, x.tb, detailed=False)) assert "p3 = p1 // p2" in tb assert "ZeroDivisionError" in tb assert " a = 10" not in tb assert " s = 'whiteblack'" not in tb assert " pre2 = 999" not in tb assert " x = 999" not in tb def testFormatTracebackDetail(self): with pytest.raises(ZeroDivisionError) as x: crash() tb = "".join(errors.format_traceback(x.type, x.value, x.tb, detailed=True)) assert "p3 = p1 // p2" in tb assert "ZeroDivisionError" in tb assert " a = 10" in tb assert " q = 'whiteblack'" in tb assert " pre2 = 999" in tb assert " x = 999" in tb def testPyroTraceback(self): try: crash() except ZeroDivisionError: pyro_tb = errors.format_traceback(detailed=True) assert " Extended stacktrace follows (most recent call last)\n" in pyro_tb try: crash("stringvalue") except TypeError as x: x._pyroTraceback = pyro_tb # set the remote traceback info pyrotb = "".join(errors.get_pyro_traceback()) assert "Remote traceback" in pyrotb assert "crash(\"stringvalue\")" in pyrotb assert "TypeError:" in pyrotb assert "ZeroDivisionError" in pyrotb del x._pyroTraceback pyrotb = "".join(errors.get_pyro_traceback()) assert "Remote traceback" not in pyrotb assert "ZeroDivisionError" not in pyrotb assert "crash(\"stringvalue\")" in pyrotb assert "TypeError:" in pyrotb def testPyroTracebackArgs(self): try: crash() except ZeroDivisionError: ex_type, ex_value, ex_tb = sys.exc_info() tb1 = errors.get_pyro_traceback() tb2 = errors.get_pyro_traceback(ex_type, ex_value, ex_tb) assert tb2 == tb1 tb1 = errors.format_traceback() tb2 = errors.format_traceback(ex_type, ex_value, ex_tb) assert tb2 == tb1 tb2 = errors.format_traceback(detailed=True) assert tb1 != tb2 def testExcepthook(self): # simply test the excepthook by calling it the way Python would try: crash() except ZeroDivisionError: pyro_tb = errors.format_traceback() with pytest.raises(TypeError) as x: crash("stringvalue") ex_type, ex_value, ex_tb = x.type, x.value, x.tb ex_value._pyroTraceback = pyro_tb # set the remote traceback info oldstderr = sys.stderr try: sys.stderr = io.StringIO() errors.excepthook(ex_type, ex_value, ex_tb) output = sys.stderr.getvalue() assert "Remote traceback" in output assert "crash(\"stringvalue\")" in output assert "TypeError:" in output assert "ZeroDivisionError" in output finally: sys.stderr = oldstderr def clearEnv(self): if "PYRO_HOST" in os.environ: del os.environ["PYRO_HOST"] if "PYRO_NS_PORT" in os.environ: del os.environ["PYRO_NS_PORT"] if "PYRO_COMPRESSION" in os.environ: del os.environ["PYRO_COMPRESSION"] config.reset() def testConfig(self): self.clearEnv() try: assert config.NS_PORT == 9090 assert config.HOST == "localhost" assert config.COMPRESSION == False os.environ["NS_PORT"] = "4444" config.reset() assert config.NS_PORT == 9090 os.environ["PYRO_NS_PORT"] = "4444" os.environ["PYRO_HOST"] = "something.com" os.environ["PYRO_COMPRESSION"] = "OFF" config.reset() assert config.NS_PORT == 4444 assert config.HOST == "something.com" assert config.COMPRESSION == False finally: self.clearEnv() assert config.NS_PORT == 9090 assert config.HOST == "localhost" assert config.COMPRESSION == False def testConfigReset(self): try: config.reset() assert config.HOST == "localhost" config.HOST = "foobar" assert config.HOST == "foobar" config.reset() assert config.HOST == "localhost" os.environ["PYRO_HOST"] = "foobar" config.reset() assert config.HOST == "foobar" del os.environ["PYRO_HOST"] config.reset() assert config.HOST == "localhost" finally: self.clearEnv() Pyro5-5.15/tests/test_httpgateway.py000066400000000000000000000156421451404116400176060ustar00rootroot00000000000000""" Tests for the http gateway. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import pytest import json from wsgiref.util import setup_testing_defaults import io import Pyro5.utils.httpgateway import Pyro5.errors import Pyro5.core from Pyro5.nameserver import NameServer class WSGITestBase: """Helper class for wsgi unit-tests. Provides up a simple interface to make requests as though they came through a wsgi interface from a user.""" def __init__(self): """Set up a fresh testing environment before each test.""" self.cookies = [] def request(self, application, url, query_string="", post_data=b""): """Hand a request to the application as if sent by a client. @param application: The callable wsgi application to test. @param url: The URL to make the request against. @param query_string: Url parameters. @param post_data: bytes to post.""" self.response_started = False method = 'POST' if post_data else 'GET' temp = io.BytesIO(post_data) environ = { 'PATH_INFO': url, 'REQUEST_METHOD': method, 'CONTENT_LENGTH': len(post_data), 'QUERY_STRING': query_string, 'wsgi.input': temp, } if method == "POST": environ["CONTENT_TYPE"] = "application/x-www-form-urlencoded" setup_testing_defaults(environ) if self.cookies: environ['HTTP_COOKIE'] = ';'.join(self.cookies) response = b'' for ret in application(environ, self._start_response): assert self.response_started response += ret temp.close() return response def _start_response(self, status, headers): """A callback passed into the application, to simulate a wsgi environment. @param status: The response status of the application ("200", "404", etc) @param headers: Any headers to begin the response with. """ assert not self.response_started self.response_started = True self.status = status self.headers = headers for header in headers: # Parse out any cookies and save them to send with later requests. if header[0] == 'Set-Cookie': var = header[1].split(';', 1) if len(var) > 1 and var[1][0:9] == ' Max-Age=': if int(var[1][9:]) > 0: # An approximation, since our cookies never expire unless # explicitly deleted (by setting Max-Age=0). self.cookies.append(var[0]) else: index = self.cookies.index(var[0]) self.cookies.pop(index) def new_session(self): """Start a new session (or pretend to be a different user) by deleting all current cookies.""" self.cookies = [] @pytest.fixture(scope="module") def wsgiserver(): # a bit of hackery to avoid having to launch a live name server class NameServerDummyProxy(NameServer): def __init__(self): super(NameServerDummyProxy, self).__init__() self._pyroUri = Pyro5.core.URI("PYRO:dummy12345@localhost:59999") self.register("http.ObjectName", "PYRO:dummy12345@localhost:59999") def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): pass def __call__(self, *args, **kwargs): return ["Name1", "Name2", "Name3"] def _pyroInvokeBatch(self, calls, oneway=False): return ["Name1"] def _pyroClaimOwnership(self): pass ws = WSGITestBase() old_get_ns = Pyro5.utils.httpgateway.get_nameserver Pyro5.utils.httpgateway.get_nameserver = lambda: NameServerDummyProxy() Pyro5.config.COMMTIMEOUT = 0.3 yield ws Pyro5.utils.httpgateway.get_nameserver = old_get_ns Pyro5.config.COMMTIMEOUT = 0.0 class TestHttpGateway: def teardown_class(self): Pyro5.config.SERIALIZER = "serpent" def test_params(self): multiparams = { "first": [1], "second": [1, 2, 3], "third": 42 } checkparams = { "first": 1, "second": [1, 2, 3], "third": 42 } params = Pyro5.utils.httpgateway.singlyfy_parameters(multiparams) assert checkparams == params params = Pyro5.utils.httpgateway.singlyfy_parameters(multiparams) assert checkparams == params def test_redirect(self, wsgiserver): result = wsgiserver.request(Pyro5.utils.httpgateway.pyro_app, "/") assert wsgiserver.status == "302 Found" assert wsgiserver.headers == [('Location', '/pyro/')] assert result == b"" def test_webpage(self, wsgiserver): result = wsgiserver.request(Pyro5.utils.httpgateway.pyro_app, "/pyro/") assert wsgiserver.status == "200 OK" assert result.startswith(b"") assert len(result) > 1000 def test_methodCallGET(self, wsgiserver): result = wsgiserver.request(Pyro5.utils.httpgateway.pyro_app, "/pyro/http.ObjectName/method", query_string="param=42¶m2=hello") # the call will result in a communication error because the dummy uri points to something that is not available assert wsgiserver.status == "500 Internal Server Error" j = json.loads(result.decode("utf-8")) assert j["__exception__"] assert j["__class__"] == "Pyro5.errors.CommunicationError" def test_methodCallPOST(self, wsgiserver): result = wsgiserver.request(Pyro5.utils.httpgateway.pyro_app, "/pyro/http.ObjectName/method", post_data=b"param=42¶m2=hello") # the call will result in a communication error because the dummy uri points to something that is not available assert wsgiserver.status == "500 Internal Server Error" j = json.loads(result.decode("utf-8")) assert j["__exception__"] assert j["__class__"] == "Pyro5.errors.CommunicationError" def test_nameDeniedPattern(self, wsgiserver): result = wsgiserver.request(Pyro5.utils.httpgateway.pyro_app, "/pyro/Pyro.NameServer/method") # the call will result in a access denied error because the uri points to a non-exposed name assert wsgiserver.status == "403 Forbidden" def test_nameDeniedNotRegistered(self, wsgiserver): result = wsgiserver.request(Pyro5.utils.httpgateway.pyro_app, "/pyro/http.NotRegisteredName/method") # the call will result in a communication error because the dummy uri points to something that is not registered assert wsgiserver.status == "500 Internal Server Error" j = json.loads(result.decode("utf-8")) assert j["__exception__"] assert j["__class__"] == "Pyro5.errors.NamingError" def test_exposedPattern(self): assert Pyro5.utils.httpgateway.pyro_app.ns_regex == r"http\." Pyro5-5.15/tests/test_naming.py000066400000000000000000000627701451404116400165220ustar00rootroot00000000000000""" Tests for the name server. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import time import pytest import threading import os import sys from io import StringIO import Pyro5.core import Pyro5.client import Pyro5.nsc import Pyro5.nameserver import Pyro5.socketutil from Pyro5.errors import CommunicationError, NamingError, PyroError from Pyro5 import config class NSLoopThread(threading.Thread): def __init__(self, nameserver): super(NSLoopThread, self).__init__() self.daemon = True self.nameserver = nameserver self.running = threading.Event() self.running.clear() def run(self): self.running.set() try: self.nameserver.requestLoop() except CommunicationError: pass # ignore pyro communication errors class TestBCSetup: @pytest.mark.network def testBCstart(self): myIpAddress = Pyro5.socketutil.get_ip_address("", workaround127=True) nsUri, nameserver, bcserver = Pyro5.nameserver.start_ns(host=myIpAddress, port=0, bcport=0, enableBroadcast=False) assert bcserver is None nameserver.close() nsUri, nameserver, bcserver = Pyro5.nameserver.start_ns(host=myIpAddress, port=0, bcport=0, enableBroadcast=True) assert bcserver is not None assert bcserver.fileno() > 1 assert bcserver.sock is not None nameserver.close() bcserver.close() class TestNameServer: def setup_method(self): config.POLLTIMEOUT = 0.1 myIpAddress = Pyro5.socketutil.get_ip_address("", workaround127=True) self.nsUri, self.nameserver, self.bcserver = Pyro5.nameserver.start_ns(host=myIpAddress, port=0, bcport=0) assert self.bcserver is not None self.bcserver.runInThread() self.daemonthread = NSLoopThread(self.nameserver) self.daemonthread.start() self.daemonthread.running.wait() time.sleep(0.05) self.old_bcPort = config.NS_BCPORT self.old_nsPort = config.NS_PORT self.old_nsHost = config.NS_HOST config.NS_PORT = self.nsUri.port config.NS_HOST = str(myIpAddress) config.NS_BCPORT = self.bcserver.getPort() def teardown_method(self): time.sleep(0.01) self.nameserver.shutdown() self.bcserver.close() self.daemonthread.join() config.NS_HOST = self.old_nsHost config.NS_PORT = self.old_nsPort config.NS_BCPORT = self.old_bcPort @pytest.mark.network def testLookupUnixsockParsing(self): # this must not raise AttributeError, it did before because of a parse bug with pytest.raises(NamingError): Pyro5.core.locate_ns("./u:/tmp/Pyro5-naming.usock") @pytest.mark.network def testLookupAndRegister(self): ns = Pyro5.core.locate_ns() # broadcast lookup assert isinstance(ns, Pyro5.client.Proxy) ns._pyroRelease() ns = Pyro5.core.locate_ns(self.nsUri.host) # normal lookup assert isinstance(ns, Pyro5.client.Proxy) uri = ns._pyroUri assert uri.protocol == "PYRO" assert uri.host == self.nsUri.host assert uri.port == config.NS_PORT ns._pyroRelease() ns = Pyro5.core.locate_ns(self.nsUri.host, config.NS_PORT) uri = ns._pyroUri assert uri.protocol == "PYRO" assert uri.host == self.nsUri.host assert uri.port == config.NS_PORT # check that we cannot register a stupid type with pytest.raises(TypeError): ns.register("unittest.object1", 5555) # we can register str or URI, lookup always returns URI ns.register("unittest.object2", "PYRO:55555@host.com:4444") assert ns.lookup("unittest.object2") == Pyro5.core.URI("PYRO:55555@host.com:4444") ns.register("unittest.object3", Pyro5.core.URI("PYRO:66666@host.com:4444")) assert ns.lookup("unittest.object3") == Pyro5.core.URI("PYRO:66666@host.com:4444") ns._pyroRelease() @pytest.mark.network def testDaemonPyroObj(self): uri = self.nsUri uri.object = Pyro5.core.DAEMON_NAME with Pyro5.client.Proxy(uri) as daemonobj: daemonobj.ping() daemonobj.registered() with pytest.raises(AttributeError): daemonobj.shutdown() @pytest.mark.network def testMulti(self): uristr = str(self.nsUri) p = Pyro5.client.Proxy(uristr) p._pyroBind() p._pyroRelease() uri = Pyro5.core.resolve(uristr) p = Pyro5.client.Proxy(uri) p._pyroBind() p._pyroRelease() uri = Pyro5.core.resolve(uristr) p = Pyro5.client.Proxy(uri) p._pyroBind() p._pyroRelease() uri = Pyro5.core.resolve(uristr) p = Pyro5.client.Proxy(uri) p._pyroBind() p._pyroRelease() uri = Pyro5.core.resolve(uristr) p = Pyro5.client.Proxy(uri) p._pyroBind() p._pyroRelease() uri = Pyro5.core.resolve(uristr) p = Pyro5.client.Proxy(uri) p._pyroBind() p._pyroRelease() daemonUri = "PYRO:" + Pyro5.core.DAEMON_NAME + "@" + uri.location _ = Pyro5.core.resolve(daemonUri) _ = Pyro5.core.resolve(daemonUri) _ = Pyro5.core.resolve(daemonUri) _ = Pyro5.core.resolve(daemonUri) _ = Pyro5.core.resolve(daemonUri) _ = Pyro5.core.resolve(daemonUri) uri = Pyro5.core.resolve(daemonUri) pyronameUri = "PYRONAME:" + Pyro5.core.NAMESERVER_NAME + "@" + uri.location _ = Pyro5.core.resolve(pyronameUri) _ = Pyro5.core.resolve(pyronameUri) _ = Pyro5.core.resolve(pyronameUri) _ = Pyro5.core.resolve(pyronameUri) _ = Pyro5.core.resolve(pyronameUri) _ = Pyro5.core.resolve(pyronameUri) @pytest.mark.network def testResolve(self): resolved1 = Pyro5.core.resolve(Pyro5.core.URI("PYRO:12345@host.com:4444")) resolved2 = Pyro5.core.resolve("PYRO:12345@host.com:4444") assert type(resolved1) is Pyro5.core.URI assert resolved1 == resolved2 assert str(resolved1) == "PYRO:12345@host.com:4444" ns = Pyro5.core.locate_ns(self.nsUri.host, self.nsUri.port) host = "[" + self.nsUri.host + "]" if ":" in self.nsUri.host else self.nsUri.host uri = Pyro5.core.resolve("PYRONAME:" + Pyro5.core.NAMESERVER_NAME + "@" + host + ":" + str(self.nsUri.port)) assert uri.protocol == "PYRO" assert uri.host == self.nsUri.host assert uri.object == Pyro5.core.NAMESERVER_NAME assert ns._pyroUri == uri ns._pyroRelease() # broadcast lookup with pytest.raises(NamingError): Pyro5.core.resolve("PYRONAME:unknown_object") uri = Pyro5.core.resolve("PYRONAME:" + Pyro5.core.NAMESERVER_NAME) assert isinstance(uri, Pyro5.core.URI) assert uri.protocol == "PYRO" # test some errors with pytest.raises(NamingError): Pyro5.core.resolve("PYRONAME:unknown_object@" + host) with pytest.raises(TypeError): Pyro5.core.resolve(999) @pytest.mark.network def testRefuseDottedNames(self): with Pyro5.core.locate_ns(self.nsUri.host, self.nsUri.port) as ns: # the name server should never have dotted names enabled with pytest.raises(AttributeError): _ = ns.namespace.keys assert ns._pyroConnection is not None assert ns._pyroConnection is None @pytest.mark.network def testAutoClean(self): try: config.NS_AUTOCLEAN = 0.0 config.COMMTIMEOUT = 0.5 Pyro5.nameserver.AutoCleaner.max_unreachable_time = 1 Pyro5.nameserver.AutoCleaner.loop_delay = 0.5 Pyro5.nameserver.AutoCleaner.override_autoclean_min = True with Pyro5.nameserver.NameServerDaemon(port=0) as ns: assert ns.cleaner_thread is None config.NS_AUTOCLEAN = 0.2 with Pyro5.nameserver.NameServerDaemon(port=0) as ns: assert ns.cleaner_thread is not None ns.nameserver.register("test", "PYRO:test@localhost:59999") assert ns.nameserver.count() == 2 time.sleep(4) assert ns.nameserver.count() == 1 assert ns.cleaner_thread is None finally: Pyro5.nameserver.AutoCleaner.override_autoclean_min = False Pyro5.nameserver.AutoCleaner.max_unreachable_time = 20 Pyro5.nameserver.AutoCleaner.loop_delay = 2 config.NS_AUTOCLEAN = 0.0 config.COMMTIMEOUT = 0.0 class TestNameServer0000: def setup_method(self): config.POLLTIMEOUT = 0.1 self.nsUri, self.nameserver, self.bcserver = Pyro5.nameserver.start_ns(host="", port=0, bcport=0) host_check = self.nsUri.host assert host_check == "0.0.0.0" assert self.bcserver is not None self.bcthread = self.bcserver.runInThread() self.old_bcPort = config.NS_BCPORT self.old_nsPort = config.NS_PORT self.old_nsHost = config.NS_HOST config.NS_PORT = self.nsUri.port config.NS_HOST = self.nsUri.host config.NS_BCPORT = self.bcserver.getPort() def teardown_method(self): time.sleep(0.01) self.nameserver.shutdown() self.bcserver.close() self.bcthread.join() config.NS_HOST = self.old_nsHost config.NS_PORT = self.old_nsPort config.NS_BCPORT = self.old_bcPort @pytest.mark.network def testBCLookup0000(self): ns = Pyro5.core.locate_ns() # broadcast lookup assert isinstance(ns, Pyro5.client.Proxy) assert ns._pyroUri.host != "0.0.0.0" ns._pyroRelease() class TestOfflineNameServer: def setup_method(self): self.storageProvider = Pyro5.nameserver.MemoryStorage() def teardown_method(self): self.clearStorage() self.clearStorage() self.storageProvider.close() def clearStorage(self): try: self.storageProvider.clear() except AttributeError: pass def testRegister(self): ns = Pyro5.nameserver.NameServer(storageProvider=self.storageProvider) self.clearStorage() ns.ping() ns.register("test.object1", "PYRO:000000@host.com:4444") ns.register("test.object2", "PYRO:222222@host.com:4444") ns.register("test.object3", "PYRO:333333@host.com:4444") assert str(ns.lookup("test.object1")) == "PYRO:000000@host.com:4444" ns.register("test.object1", "PYRO:111111@host.com:4444") # registering again should be ok by default assert str(ns.lookup("test.object1")), "should be new uri" == "PYRO:111111@host.com:4444" ns.register("test.sub.objectA", Pyro5.core.URI("PYRO:AAAAAA@host.com:4444")) ns.register("test.sub.objectB", Pyro5.core.URI("PYRO:BBBBBB@host.com:4444")) # if safe=True, a registration of an existing name should give a NamingError with pytest.raises(NamingError): ns.register("test.object1", "PYRO:X@Y:5555", safe=True) with pytest.raises(TypeError): ns.register(None, None) with pytest.raises(TypeError): ns.register(4444, 4444) with pytest.raises(TypeError): ns.register("test.wrongtype", 4444) with pytest.raises(TypeError): ns.register(4444, "PYRO:X@Y:5555") with pytest.raises(NamingError): ns.lookup("unknown_object") uri = ns.lookup("test.object3") assert uri == Pyro5.core.URI("PYRO:333333@host.com:4444") # lookup always returns URI ns.remove("unknown_object") ns.remove("test.object1") ns.remove("test.object2") ns.remove("test.object3") all_objs = ns.list() assert len(all_objs) == 2 # 2 leftover objects with pytest.raises(PyroError): ns.register("test.nonurivalue", "THISVALUEISNOTANURI") ns.storage.close() def testRemove(self): ns = Pyro5.nameserver.NameServer(storageProvider=self.storageProvider) self.clearStorage() ns.register(Pyro5.core.NAMESERVER_NAME, "PYRO:nameserver@host:555") for i in range(20): ns.register("test.%d" % i, "PYRO:obj@host:555") assert len(ns.list()) == 21 assert ns.remove("wrong") == 0 assert ns.remove(prefix="wrong") == 0 assert ns.remove(regex="wrong.*") == 0 assert ns.remove("test.0") == 1 assert len(ns.list()) == 20 assert ns.remove(prefix="test.1") == 11 # 1, 10-19 assert ns.remove(regex=r"test\..") == 8 # 2-9 assert len(ns.list()) == 1 ns.storage.close() def testRemoveProtected(self): ns = Pyro5.nameserver.NameServer(storageProvider=self.storageProvider) self.clearStorage() ns.register(Pyro5.core.NAMESERVER_NAME, "PYRO:nameserver@host:555") assert ns.remove(Pyro5.core.NAMESERVER_NAME) == 0 assert ns.remove(prefix="Pyro") == 0 assert ns.remove(regex="Pyro.*") == 0 assert Pyro5.core.NAMESERVER_NAME in ns.list() ns.storage.close() def testUnicodeNames(self): ns = Pyro5.nameserver.NameServer(storageProvider=self.storageProvider) self.clearStorage() uri = Pyro5.core.URI("PYRO:unicode" + chr(0x20ac) + "@host:5555") ns.register("unicodename" + chr(0x20ac), uri) x = ns.lookup("unicodename" + chr(0x20ac)) assert x == uri ns.storage.close() def testList(self): ns = Pyro5.nameserver.NameServer(storageProvider=self.storageProvider) self.clearStorage() ns.register("test.objects.1", "PYRONAME:something1") ns.register("test.objects.2", "PYRONAME:something2") ns.register("test.objects.3", "PYRONAME:something3") ns.register("test.other.a", "PYRONAME:somethingA") ns.register("test.other.b", "PYRONAME:somethingB") ns.register("test.other.c", "PYRONAME:somethingC") ns.register("entirely.else", "PYRONAME:meh") objects = ns.list() assert len(objects) == 7 objects = ns.list(prefix="nothing") assert len(objects) == 0 objects = ns.list(prefix="test.") assert len(objects) == 6 objects = ns.list(regex=r".+other..") assert len(objects) == 3 assert "test.other.a" in objects assert objects["test.other.a"] == "PYRONAME:somethingA" objects = ns.list(regex=r"\d\d\d\d\d\d\d\d\d\d") assert len(objects) == 0 with pytest.raises(NamingError): ns.list(regex="((((((broken") ns.storage.close() def testNameserverWithStmt(self): ns = Pyro5.nameserver.NameServerDaemon(port=0) assert ns.nameserver is not None ns.close() assert ns.nameserver is None with Pyro5.nameserver.NameServerDaemon(port=0) as ns: assert ns.nameserver is not None pass assert ns.nameserver is None with pytest.raises(ZeroDivisionError): with Pyro5.nameserver.NameServerDaemon(port=0) as ns: assert ns.nameserver is not None print(1 // 0) # cause an error assert ns.nameserver is None ns = Pyro5.nameserver.NameServerDaemon(port=0) with ns: pass with pytest.raises(PyroError): with ns: pass ns.close() @pytest.mark.network def testStartNSfunc(self): myIpAddress = Pyro5.socketutil.get_ip_address("", workaround127=True) uri1, ns1, bc1 = Pyro5.nameserver.start_ns(host=myIpAddress, port=0, bcport=0, enableBroadcast=False) uri2, ns2, bc2 = Pyro5.nameserver.start_ns(host=myIpAddress, port=0, bcport=0, enableBroadcast=True) assert isinstance(uri1, Pyro5.core.URI) assert isinstance(ns1, Pyro5.nameserver.NameServerDaemon) assert bc1 is None assert isinstance(bc2, Pyro5.nameserver.BroadcastServer) sock = bc2.sock assert hasattr(sock, "fileno") _ = bc2.processRequest ns1.close() ns2.close() bc2.close() def testNSmain(self): oldstdout = sys.stdout oldstderr = sys.stderr try: sys.stdout = StringIO() sys.stderr = StringIO() with pytest.raises(SystemExit): Pyro5.nameserver.main(["--invalidarg"]) assert "usage" in sys.stderr.getvalue() sys.stderr.truncate(0) sys.stdout.truncate(0) with pytest.raises(SystemExit): Pyro5.nameserver.main(["-h"]) assert "show this help message" in sys.stdout.getvalue() finally: sys.stdout = oldstdout sys.stderr = oldstderr def testNSCmain(self): oldstdout = sys.stdout oldstderr = sys.stderr try: sys.stdout = StringIO() sys.stderr = StringIO() with pytest.raises(SystemExit): Pyro5.nsc.main(["--invalidarg"]) assert "usage" in sys.stderr.getvalue() sys.stderr.truncate(0) sys.stdout.truncate(0) with pytest.raises(SystemExit): Pyro5.nsc.main(["-h"]) assert "show this help message" in sys.stdout.getvalue() finally: sys.stdout = oldstdout sys.stderr = oldstderr def testNSCfunctions(self): oldstdout = sys.stdout ns = None try: sys.stdout = StringIO() ns = Pyro5.nameserver.NameServer(storageProvider=self.storageProvider) with pytest.raises(KeyError): Pyro5.nsc.handle_command(ns, "foo", []) assert sys.stdout.getvalue().startswith("Error: KeyError ") Pyro5.nsc.handle_command(ns, "ping", []) assert sys.stdout.getvalue().endswith("ping ok.\n") with pytest.raises(NamingError): Pyro5.nsc.handle_command(ns, "lookup", ["WeirdName"]) assert sys.stdout.getvalue().endswith("Error: NamingError - unknown name: WeirdName\n") Pyro5.nsc.handle_command(ns, "list", []) assert sys.stdout.getvalue().endswith("END LIST \n") Pyro5.nsc.handle_command(ns, "listmatching", ["name.$"]) assert sys.stdout.getvalue().endswith("END LIST - regex 'name.$'\n") assert "name1" not in sys.stdout.getvalue() Pyro5.nsc.handle_command(ns, "register", ["name1", "PYRO:obj1@hostname:9999"]) assert sys.stdout.getvalue().endswith("Registered name1\n") Pyro5.nsc.handle_command(ns, "remove", ["name2"]) assert sys.stdout.getvalue().endswith("Nothing removed\n") Pyro5.nsc.handle_command(ns, "listmatching", ["name.$"]) assert "name1 --> PYRO:obj1@hostname:9999" in sys.stdout.getvalue() # Pyro5.nsc.handle_command(ns, None, ["removematching", "name.?"]) # can't be tested, required user input finally: sys.stdout = oldstdout if ns: ns.storage.close() def testNAT(self): uri, ns, bc = Pyro5.nameserver.start_ns(host="", port=0, enableBroadcast=True, nathost="nathosttest", natport=12345) assert uri.location == "nathosttest:12345" assert ns.uriFor("thing").location == "nathosttest:12345" assert bc.nsUri.location != "nathosttest:12345", "broadcast location must not be the NAT location" ns.close() bc.close() def testMetadataRegisterInvalidTypes(self): ns = Pyro5.nameserver.NameServer(storageProvider=self.storageProvider) with pytest.raises(TypeError): ns.register("meta1", "PYRO:meta1@localhost:1111", metadata=12345) # metadata must be iterable with pytest.raises(TypeError): ns.register("meta1", "PYRO:meta1@localhost:1111", metadata="string") # metadata must not be str def testMetadataLookupInvalidTypes(self): ns = Pyro5.nameserver.NameServer(storageProvider=self.storageProvider) with pytest.raises(TypeError): ns.yplookup(meta_all=12345) with pytest.raises(TypeError): ns.yplookup(meta_all="string") with pytest.raises(TypeError): ns.yplookup(meta_any=12345) with pytest.raises(TypeError): ns.yplookup(meta_any="string") def testMetadata(self): self.clearStorage() ns = Pyro5.nameserver.NameServer(storageProvider=self.storageProvider) # register some names with metadata, and perform simple lookups ns.register("meta1", "PYRO:meta1@localhost:1111", metadata={"a", "b", "c"}) ns.register("meta2", "PYRO:meta2@localhost:2222", metadata={"x", "y", "z"}) ns.register("meta3", "PYRO:meta3@localhost:3333", metadata=["p", "q", "r", "r", "q"]) uri = ns.lookup("meta1") assert uri.object == "meta1" uri, metadata = ns.lookup("meta1", return_metadata=True) assert uri.object == "meta1" assert set(metadata) == {"a", "b", "c"} uri = ns.lookup("meta2") assert uri.object == "meta2" uri, metadata = ns.lookup("meta2", return_metadata=True) assert uri.object == "meta2" assert set(metadata) == {"x", "y", "z"} uri, metadata = ns.lookup("meta3", return_metadata=True) assert uri.object == "meta3" assert isinstance(metadata, set) assert set(metadata) == {"p", "q", "r"} # get a list of everything, without and with metadata reg = ns.list() assert reg == {'meta1': 'PYRO:meta1@localhost:1111', 'meta2': 'PYRO:meta2@localhost:2222', 'meta3': 'PYRO:meta3@localhost:3333'} reg = ns.list(return_metadata=True) uri1, meta1 = reg["meta1"] uri2, meta2 = reg["meta2"] assert uri1 == "PYRO:meta1@localhost:1111" assert set(meta1) == {"a", "b", "c"} assert uri2 == "PYRO:meta2@localhost:2222" assert set(meta2) == {"x", "y", "z"} # filter on metadata subset reg = ns.yplookup(meta_all={"a", "c"}, return_metadata=False) assert len(reg) == 1 assert reg["meta1"] == "PYRO:meta1@localhost:1111" reg = ns.yplookup(meta_all={"a", "c"}, return_metadata=True) assert len(reg) == 1 uri1, meta1 = reg["meta1"] assert uri1 == "PYRO:meta1@localhost:1111" assert set(meta1) == {"a", "b", "c"} reg = ns.yplookup(meta_all={"a", "wrong"}) assert reg == {} reg = ns.yplookup(meta_all={"a", "b", "c", "wrong"}) assert reg == {} reg = ns.yplookup(meta_all={"a", "c", "x"}) assert reg == {} # update some metadata with pytest.raises(NamingError): ns.set_metadata("notexistingname", set()) ns.set_metadata("meta1", {"one", "two", "three"}) uri, meta = ns.lookup("meta1", return_metadata=True) assert set(meta) == {"one", "two", "three"} # check that a collection is converted to a set ns.set_metadata("meta1", ["one", "two", "three", "three", "two"]) uri, meta = ns.lookup("meta1", return_metadata=True) assert isinstance(meta, set) assert set(meta) == {"one", "two", "three"} # remove record that has some metadata ns.remove("meta1") ns.remove("meta3") assert list(ns.list().keys()) == ["meta2"] # other list filters reg = ns.list(prefix="meta", return_metadata=True) assert len(reg) == 1 assert set(reg["meta2"][1]) == {"x", "y", "z"} reg = ns.list(regex="meta2.*", return_metadata=True) assert len(reg) == 1 assert set(reg["meta2"][1]) == {"x", "y", "z"} assert ns.count() == 1 def testMetadataAny(self): self.clearStorage() ns = Pyro5.nameserver.NameServer(storageProvider=self.storageProvider) # register some names with metadata, and perform simple lookups ns.register("meta1", "PYRO:meta1@localhost:1111", metadata={"a", "b", "c"}) ns.register("meta2", "PYRO:meta2@localhost:2222", metadata={"x", "y", "z"}) ns.register("meta3", "PYRO:meta3@localhost:2222", metadata={"k", "l", "m"}) result = ns.yplookup(meta_any={"1", "2", "3"}) assert result == {} result = ns.yplookup(meta_any={"1", "2", "a"}) assert len(result) == 1 assert "meta1" in result result = ns.yplookup(meta_any={"1", "2", "a", "z"}) assert len(result) == 2 assert "meta1" in result assert "meta2" in result def testEmptyMetadata(self): self.clearStorage() ns = Pyro5.nameserver.NameServer(storageProvider=self.storageProvider) # register some names with metadata, and perform simple lookups ns.register("meta1", "PYRO:meta1@localhost:1111", metadata=set()) uri, meta = ns.lookup("meta1", return_metadata=True) assert meta == set() registrations = ns.list(return_metadata=True) for name in registrations: uri, meta = registrations[name] assert meta == set() ns.set_metadata("meta1", set()) def testListNoMultipleFilters(self): ns = Pyro5.nameserver.NameServer(storageProvider=self.storageProvider) with pytest.raises(ValueError): ns.list(prefix="a", regex="a") with pytest.raises(ValueError): ns.yplookup(meta_any={"a"}, meta_all={"a"}) class TestOfflineNameServerTestsSqlStorage(TestOfflineNameServer): def setup_method(self): super().setup_method() self.storageProvider = Pyro5.nameserver.SqlStorage("pyro-test.sqlite") def teardown_method(self): try: super().teardown_method() except AttributeError: pass import glob for file in glob.glob("pyro-test.sqlite*"): os.remove(file) Pyro5-5.15/tests/test_protocol.py000066400000000000000000000176631451404116400171130ustar00rootroot00000000000000import zlib import pytest import Pyro5.protocol import Pyro5.errors import Pyro5.protocol import Pyro5.serializers import Pyro5.errors from Pyro5.protocol import SendingMessage, ReceivingMessage from support import ConnectionMock class TestSendingMessage: def test_create(self): msg = Pyro5.protocol.SendingMessage(Pyro5.protocol.MSG_INVOKE, 0, 42, 99, b"abcdefg") assert len(msg.data) > 1 def test_annotations_errors(self): with pytest.raises(Pyro5.errors.ProtocolError): Pyro5.protocol.SendingMessage(Pyro5.protocol.MSG_INVOKE, 0, 42, 99, b"abcdefg", annotations={"zxcv": "no_bytes"}) with pytest.raises(Pyro5.errors.ProtocolError): Pyro5.protocol.SendingMessage(Pyro5.protocol.MSG_INVOKE, 0, 42, 99, b"abcdefg", annotations={"err": b"bytes"}) def test_annotations(self): msg = Pyro5.protocol.SendingMessage(Pyro5.protocol.MSG_INVOKE, 0, 42, 99, b"abcdefg", annotations={"zxcv": b"bytes"}) assert len(msg.data) > 1 def test_compression(self): compr_orig = Pyro5.config.COMPRESSION try: Pyro5.config.COMPRESSION = False msg_uncompressed = Pyro5.protocol.SendingMessage(Pyro5.protocol.MSG_INVOKE, 0, 42, 99, b"abcdefg"*100) Pyro5.config.COMPRESSION = True msg_compressed = Pyro5.protocol.SendingMessage(Pyro5.protocol.MSG_INVOKE, 0, 42, 99, b"abcdefg"*100) msg_notcompressed = Pyro5.protocol.SendingMessage(Pyro5.protocol.MSG_INVOKE, 0, 42, 99, b"abcdefg") assert not (msg_uncompressed.flags & Pyro5.protocol.FLAGS_COMPRESSED) assert not (msg_notcompressed.flags & Pyro5.protocol.FLAGS_COMPRESSED) assert msg_compressed.flags & Pyro5.protocol.FLAGS_COMPRESSED assert len(msg_uncompressed.data) > len(msg_compressed.data) finally: Pyro5.config.COMPRESSION = compr_orig class TestReceivingMessage: def createmessage(self, compression=False): compr_orig = Pyro5.config.COMPRESSION Pyro5.config.COMPRESSION = compression annotations = { "TEST": b"0123456789qwertyu", "UNIT": b"aaaaaaa" } msg = Pyro5.protocol.SendingMessage(Pyro5.protocol.MSG_INVOKE, 255, 42, 99, b"abcdefg"*100, annotations) Pyro5.config.COMPRESSION = compr_orig return msg def test_validate(self): with pytest.raises(ValueError): Pyro5.protocol.ReceivingMessage.validate(b"abc") with pytest.raises(Pyro5.errors.ProtocolError): Pyro5.protocol.ReceivingMessage.validate(b"ZXCV") Pyro5.protocol.ReceivingMessage.validate(b"PYRO") with pytest.raises(Pyro5.errors.ProtocolError): Pyro5.protocol.ReceivingMessage.validate(b"PYRO__") msg = self.createmessage() msg.data = bytearray(msg.data) Pyro5.protocol.ReceivingMessage.validate(msg.data) orig_magic = msg.data[38] msg.data[38] = 0xff # kill the magic number with pytest.raises(Pyro5.errors.ProtocolError) as x: Pyro5.protocol.ReceivingMessage.validate(msg.data) assert "magic number" in str(x.value) msg.data[38] = orig_magic # repair the magic number msg.data[5] = 0xff # invalid protocol version with pytest.raises(Pyro5.errors.ProtocolError) as x: Pyro5.protocol.ReceivingMessage.validate(msg.data) assert "protocol version" in str(x.value) def test_create_nopayload(self): send_msg = self.createmessage(compression=True) header = send_msg.data[:Pyro5.protocol._header_size] msg = Pyro5.protocol.ReceivingMessage(header) assert msg.data is None assert msg.type == Pyro5.protocol.MSG_INVOKE assert msg.flags == 255 assert msg.seq == 42 assert msg.serializer_id == 99 assert len(msg.annotations) == 0 def test_create_payload(self): send_msg = self.createmessage(compression=True) header = send_msg.data[:Pyro5.protocol._header_size] payload = send_msg.data[Pyro5.protocol._header_size:] msg = Pyro5.protocol.ReceivingMessage(header, payload) assert len(msg.data) == 700 assert msg.flags == 255 & ~Pyro5.protocol.FLAGS_COMPRESSED assert len(msg.annotations) == 2 assert msg.annotations["TEST"] == b"0123456789qwertyu" assert msg.annotations["UNIT"] == b"aaaaaaa" assert type(msg.data) is bytes def test_create_payload_memview(self): send_msg = self.createmessage(compression=False) header = send_msg.data[:Pyro5.protocol._header_size] payload = send_msg.data[Pyro5.protocol._header_size:] msg = Pyro5.protocol.ReceivingMessage(header, payload) assert type(msg.data) is memoryview class TestProtocolMessages: def testMessage(self): SendingMessage(99, 0, 0, Pyro5.serializers.SerpentSerializer.serializer_id, b"") # doesn't check msg type here with pytest.raises(TypeError): ReceivingMessage("FOOBAR") with pytest.raises(Pyro5.errors.ProtocolError): ReceivingMessage(b"FOOBARFOOBARFOOBARFOOBARFOOBARFOOBAR1234") msg = SendingMessage(Pyro5.protocol.MSG_CONNECT, 0, 0, Pyro5.serializers.SerpentSerializer.serializer_id, b"hello") assert msg.type == Pyro5.protocol.MSG_CONNECT assert len(msg.data) == Pyro5.protocol._header_size + 5 msg = ReceivingMessage(msg.data[:Pyro5.protocol._header_size], msg.data[Pyro5.protocol._header_size:]) assert msg.type == Pyro5.protocol.MSG_CONNECT assert msg.data == b"hello" msg = SendingMessage(Pyro5.protocol.MSG_RESULT, 0, 0, Pyro5.serializers.SerpentSerializer.serializer_id, b"") msg = ReceivingMessage(msg.data[:Pyro5.protocol._header_size], msg.data[Pyro5.protocol._header_size:]) assert msg.type == Pyro5.protocol.MSG_RESULT assert len(msg.data) == 0 msg = SendingMessage(255, 0, 255, Pyro5.serializers.SerpentSerializer.serializer_id, b"").data assert len(msg) == 40 msg = SendingMessage(1, 0, 255, Pyro5.serializers.SerpentSerializer.serializer_id, b"").data assert len(msg) == 40 data = b"x" * 1000 Pyro5.config.COMPRESSION = True msg = SendingMessage(Pyro5.protocol.MSG_INVOKE, 0, 0, Pyro5.serializers.SerpentSerializer.serializer_id, data).data assert len(msg) < len(data) Pyro5.config.COMPRESSION = False def testAnnotationsIdLength4(self): with pytest.raises(Pyro5.errors.ProtocolError): SendingMessage(Pyro5.protocol.MSG_CONNECT, 0, 0, Pyro5.serializers.SerpentSerializer.serializer_id, b"hello", {"TOOLONG": b"abcde"}) with pytest.raises(Pyro5.errors.ProtocolError): SendingMessage(Pyro5.protocol.MSG_CONNECT, 0, 0, Pyro5.serializers.SerpentSerializer.serializer_id, b"hello", {"QQ": b"abcde"}) def testRecvAnnotations(self): annotations = {"TEST": b"abcde"} msg = SendingMessage(Pyro5.protocol.MSG_CONNECT, 0, 0, Pyro5.serializers.SerpentSerializer.serializer_id, b"hello", annotations) c = ConnectionMock() c.send(msg.data) msg = Pyro5.protocol.recv_stub(c) assert len(c.received) == 0 assert msg.data == b"hello" assert msg.annotations["TEST"] == b"abcde" def testCompression(self): data = b"The quick brown fox jumps over the lazy dog."*10 compressed_data = zlib.compress(data) flags = Pyro5.protocol.FLAGS_COMPRESSED msg = SendingMessage(Pyro5.protocol.MSG_INVOKE, 42, flags, 1, compressed_data) assert msg.data != data assert len(msg.data) < len(data) def testRecvNoAnnotations(self): msg = SendingMessage(Pyro5.protocol.MSG_CONNECT, 42, 0, 0, b"hello") c = ConnectionMock() c.send(msg.data) msg = Pyro5.protocol.recv_stub(c) assert len(c.received) == 0 assert msg.data_size == 5 assert msg.data == b"hello" assert msg.annotations_size == 0 assert len(msg.annotations) == 0 Pyro5-5.15/tests/test_pyro4compat.py000066400000000000000000000030411451404116400175140ustar00rootroot00000000000000import socket import pytest from Pyro5.compatibility import Pyro4 def test_compat_config(): import Pyro4 conf = Pyro4.config.asDict() assert conf["NS_PORT"] == 9090 Pyro4.config.NS_PORT = 12345 conf = Pyro4.config.asDict() assert conf["NS_PORT"] == 12345 Pyro4.config.NS_PORT = 9090 def test_compat_layer(): from Pyro4 import naming from Pyro4 import socketutil from Pyro4 import util try: _ = 1//0 except ZeroDivisionError: tb = util.getPyroTraceback() assert len(tb) == 3 assert "Traceback" in tb[0] assert "zero" in tb[2] assert 4 == socketutil.getIpVersion("127.0.0.1") assert 6 == socketutil.getIpVersion("::1") Pyro4.URI("PYRO:test@localhost:5555") p = Pyro4.Proxy("PYRO:test@localhost:5555") Pyro4.BatchProxy(p) Pyro4.Daemon() assert socketutil.getIpAddress("localhost", ipVersion=4).startswith("127.0") if socket.has_ipv6: try: assert ":" in socketutil.getIpAddress("localhost", ipVersion=6) except socket.error as x: if str(x) != "unable to determine IPV6 address": raise assert "127.0.0.1" == socketutil.getIpAddress("127.0.0.1") assert "::1" == socketutil.getIpAddress("::1") assert "127.0.0.1" == socketutil.getInterfaceAddress("127.0.0.1") with pytest.raises(NotImplementedError): naming.NameServer() with pytest.raises(NotImplementedError): _ = p._pyroHmacKey with pytest.raises(NotImplementedError): p._pyroHmacKey = b"fail" Pyro5-5.15/tests/test_serialize.py000066400000000000000000000720751451404116400172370ustar00rootroot00000000000000""" Tests for the data serializer. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import array import collections import copy import math import uuid import pytest import Pyro5.errors import Pyro5.core import Pyro5.client import Pyro5.server import Pyro5.serializers from Pyro5 import config from support import * class TestSerpentSerializer: serializer = Pyro5.serializers.serializers["serpent"] def testSourceByteTypes(self): call_ser = self.serializer.dumpsCall("object", "method", [1, 2, 3], {"kwarg": 42}) ser = self.serializer.dumps([4, 5, 6]) _, _, vargs, _ = self.serializer.loadsCall(bytearray(call_ser)) assert vargs == [1, 2, 3] d = self.serializer.loads(bytearray(ser)) assert d == [4, 5, 6] _, _, vargs, _ = self.serializer.loadsCall(memoryview(call_ser)) assert vargs == [1, 2, 3] d = self.serializer.loads(memoryview(ser)) assert d == [4, 5, 6] def testSerializePyroTypes(self): uri = Pyro5.core.URI("PYRO:obj@host:9999") ser = self.serializer.dumps(uri) uri2 = self.serializer.loads(ser) assert isinstance(uri2, Pyro5.core.URI) assert uri2 == uri proxy = Pyro5.client.Proxy("PYRO:obj@host:9999") proxy._pyroHandshake = "handshake" ser = self.serializer.dumps(proxy) proxy2 = self.serializer.loads(ser) assert isinstance(proxy2, Pyro5.client.Proxy) assert proxy2 == proxy assert proxy2._pyroHandshake == "handshake" with Pyro5.server.Daemon(host="localhost", port=12345, nathost="localhost", natport=9876) as daemon: ser = self.serializer.dumps(daemon) daemon2 = self.serializer.loads(ser) assert isinstance(daemon2, Pyro5.server.Daemon) def testSerializeDumpsAndDumpsCall(self): self.serializer.dumps(uuid.uuid4()) self.serializer.dumps(Pyro5.core.URI("PYRO:test@test:4444")) self.serializer.dumps(Pyro5.client.Proxy("PYRONAME:foobar")) self.serializer.dumpsCall("obj", "method", (1, 2, 3), {"arg1": 999}) self.serializer.dumpsCall("obj", "method", (1, 2, array.array('i', [1, 2, 3])), {"arg1": 999}) self.serializer.dumpsCall("obj", "method", (1, 2, array.array('i', [1, 2, 3])), {"arg1": array.array('i', [1, 2, 3])}) self.serializer.dumpsCall("obj", "method", (1, 2, Pyro5.core.URI("PYRO:test@test:4444")), {"arg1": 999}) self.serializer.dumpsCall("obj", "method", (1, 2, Pyro5.core.URI("PYRO:test@test:4444")), {"arg1": Pyro5.core.URI("PYRO:test@test:4444")}) self.serializer.dumpsCall("obj", "method", (1, 2, Pyro5.client.Proxy("PYRONAME:foobar")), {"arg1": 999}) self.serializer.dumpsCall("obj", "method", (1, 2, Pyro5.client.Proxy("PYRONAME:foobar")), {"arg1": Pyro5.client.Proxy("PYRONAME:foobar")}) def testArrays(self): a1 = array.array('u', "hello") ser = self.serializer.dumps(a1) a2 = self.serializer.loads(ser) if type(a2) is array.array: assert a2 == a1 else: assert a2 == "hello" a1 = array.array('h', [222, 333, 444, 555]) ser = self.serializer.dumps(a1) a2 = self.serializer.loads(ser) if type(a2) is array.array: assert a2 == a1 else: assert a2 == [222, 333, 444, 555] def testArrays2(self): a1 = array.array('u', "hello") ser = self.serializer.dumpsCall("obj", "method", [a1], {}) a2 = self.serializer.loads(ser) a2 = a2["params"][0] if self.serializer.serializer_id == 3 else a2[2][0] # 3=json serializer if type(a2) is array.array: assert a2 == a1 else: assert a2 == "hello" a1 = array.array('h', [222, 333, 444, 555]) ser = self.serializer.dumpsCall("obj", "method", [a1], {}) a2 = self.serializer.loads(ser) a2 = a2["params"][0] if self.serializer.serializer_id == 3 else a2[2][0] # 3=json serializer if type(a2) is array.array: assert a2 == a1 else: assert a2 == [222, 333, 444, 555] class TestMarshalSerializer(TestSerpentSerializer): serializer = Pyro5.serializers.serializers["marshal"] class TestJsonSerializer(TestSerpentSerializer): serializer = Pyro5.serializers.serializers["json"] if "msgpack" in Pyro5.serializers.serializers: class TestMsgpackSerializer(TestSerpentSerializer): serializer = Pyro5.serializers.serializers["msgpack"] class TestSerializer2_serpent: SERIALIZER = "serpent" def setup_method(self): self.previous_serializer = config.SERIALIZER config.SERIALIZER = self.SERIALIZER self.serializer = Pyro5.serializers.serializers[config.SERIALIZER] def teardown_method(self): config.SERIALIZER = self.previous_serializer def testSerErrors(self): e1 = Pyro5.errors.NamingError("x") e1._pyroTraceback = ["this is the remote traceback"] orig_e = copy.copy(e1) e2 = Pyro5.errors.PyroError("x") e3 = Pyro5.errors.ProtocolError("x") p = self.serializer.dumps(e1) e = self.serializer.loads(p) assert isinstance(e, Pyro5.errors.NamingError) assert repr(e == repr(orig_e)) assert e._pyroTraceback, "remote traceback info should be present" == ["this is the remote traceback"] p = self.serializer.dumps(e2) e = self.serializer.loads(p) assert isinstance(e, Pyro5.errors.PyroError) assert repr(e == repr(e2)) p = self.serializer.dumps(e3) e = self.serializer.loads(p) assert isinstance(e, Pyro5.errors.ProtocolError) assert repr(e == repr(e3)) def testSerializeExceptionWithAttr(self): ex = ZeroDivisionError("test error") ex._pyroTraceback = ["test traceback payload"] data = self.serializer.dumps(ex) ex2 = self.serializer.loads(data) assert type(ex2 == ZeroDivisionError) assert hasattr(ex2, "_pyroTraceback") assert ex2._pyroTraceback == ["test traceback payload"] def testSerCoreOffline(self): uri = Pyro5.core.URI("PYRO:9999@host.com:4444") p = self.serializer.dumps(uri) uri2 = self.serializer.loads(p) assert uri2 == uri assert uri2.protocol == "PYRO" assert uri2.object == "9999" assert uri2.location == "host.com:4444" assert uri2.port == 4444 assert uri2.sockname is None uri = Pyro5.core.URI("PYRO:12345@./u:/tmp/socketname") p = self.serializer.dumps(uri) uri2 = self.serializer.loads(p) assert uri2 == uri assert uri2.protocol == "PYRO" assert uri2.object == "12345" assert uri2.location == "./u:/tmp/socketname" assert uri2.port is None assert uri2.sockname == "/tmp/socketname" proxy = Pyro5.client.Proxy("PYRO:9999@host.com:4444") proxy._pyroMaxRetries = 78 assert not proxy._pyroConnection p = self.serializer.dumps(proxy) proxy2 = self.serializer.loads(p) assert not proxy._pyroConnection assert not proxy2._pyroConnection assert proxy._pyroUri == proxy2._pyroUri assert proxy2._pyroMaxRetries==0, "must be reset to defaults" def testNested(self): uri1 = Pyro5.core.URI("PYRO:1111@host.com:111") uri2 = Pyro5.core.URI("PYRO:2222@host.com:222") _ = self.serializer.dumps(uri1) data = [uri1, uri2] p = self.serializer.dumps(data) [u1, u2] = self.serializer.loads(p) assert u1 == uri1 assert u2 == uri2 def testSerDaemonHack(self): # This tests the hack that a Daemon should be serializable, # but only to support serializing Pyro objects. # The serialized form of a Daemon should be empty (and thus, useless) with Pyro5.server.Daemon(port=0) as daemon: d = self.serializer.dumps(daemon) d2 = self.serializer.loads(d) assert len(d2.__dict__) == 0, "deserialized daemon should be empty" assert "Pyro5.server.Daemon" in repr(d2) assert "unusable" in repr(d2) obj = MyThingFullExposed("hello") daemon.register(obj) _ = self.serializer.dumps(obj) def testPyroClasses(self): uri = Pyro5.core.URI("PYRO:object@host:4444") s = self.serializer.dumps(uri) x = self.serializer.loads(s) assert isinstance(x, Pyro5.core.URI) assert x == uri assert "URI" in repr(uri) assert str(uri == "PYRO:object@host:4444") uri = Pyro5.core.URI("PYRO:12345@./u:/tmp/socketname") s = self.serializer.dumps(uri) x = self.serializer.loads(s) assert isinstance(x, Pyro5.core.URI) assert x == uri proxy = Pyro5.client.Proxy(uri) proxy._pyroAttrs = set("abc") proxy._pyroMethods = set("def") proxy._pyroOneway = set("ghi") proxy._pyroHandshake = "apples" proxy._pyroMaxRetries = 78 proxy._pyroSerializer = "serializer" s = self.serializer.dumps(proxy) x = self.serializer.loads(s) assert isinstance(x, Pyro5.client.Proxy) assert x._pyroUri == proxy._pyroUri assert x._pyroAttrs == set("abc") assert x._pyroMethods == set("def") assert x._pyroOneway == set("ghi") assert x._pyroHandshake == "apples" assert x._pyroSerializer == "serializer" assert x._pyroMaxRetries == 0, "must be reset to defaults" assert "Pyro5.client.Proxy" in repr(x) assert "Pyro5.client.Proxy" in str(x) daemon = Pyro5.server.Daemon() s = self.serializer.dumps(daemon) x = self.serializer.loads(s) assert isinstance(x, Pyro5.server.Daemon) assert "Pyro5.server.Daemon" in repr(x) assert "unusable" in repr(x) assert "Pyro5.server.Daemon" in str(x) assert "unusable" in str(x) wrapper = Pyro5.core._ExceptionWrapper(ZeroDivisionError("divided by zero")) s = self.serializer.dumps(wrapper) x = self.serializer.loads(s) assert isinstance(x, Pyro5.core._ExceptionWrapper) assert str(x.exception == "divided by zero") assert "ExceptionWrapper" in repr(x) assert "ExceptionWrapper" in str(x) def testProxySerializationCompat(self): proxy = Pyro5.client.Proxy("PYRO:object@host:4444") proxy._pyroSerializer = "serializer" pickle_state = proxy.__getstate__() assert len(pickle_state) == 6 proxy.__setstate__(pickle_state) def testAutoProxyPartlyExposed(self): self.serializer.register_type_replacement(MyThingPartlyExposed, Pyro5.server._pyro_obj_to_auto_proxy) t1 = MyThingPartlyExposed("1") t2 = MyThingPartlyExposed("2") with Pyro5.server.Daemon() as d: d.register(t1, "thingy1") d.register(t2, "thingy2") data = [t1, ["apple", t2]] s = self.serializer.dumps(data) data = self.serializer.loads(s) assert data[1][0] == "apple" p1 = data[0] p2 = data[1][1] assert isinstance(p1, Pyro5.client.Proxy) assert isinstance(p2, Pyro5.client.Proxy) assert p1._pyroUri.object == "thingy1" assert p2._pyroUri.object == "thingy2" assert p1._pyroAttrs == {"readonly_prop1", "prop1"} assert p1._pyroMethods == {"exposed", "oneway"} assert p1._pyroOneway == {'oneway'} def testAutoProxyFullExposed(self): self.serializer.register_type_replacement(MyThingPartlyExposed, Pyro5.server._pyro_obj_to_auto_proxy) t1 = MyThingFullExposed("1") t2 = MyThingFullExposed("2") with Pyro5.server.Daemon() as d: d.register(t1, "thingy1") d.register(t2, "thingy2") data = [t1, ["apple", t2]] s = self.serializer.dumps(data) data = self.serializer.loads(s) assert data[1][0] == "apple" p1 = data[0] p2 = data[1][1] assert isinstance(p1, Pyro5.client.Proxy) assert isinstance(p2, Pyro5.client.Proxy) assert p1._pyroUri.object == "thingy1" assert p2._pyroUri.object == "thingy2" assert p1._pyroAttrs == {"prop1", "prop2", "readonly_prop1"} assert p1._pyroMethods == {'classmethod', 'method', 'oneway', 'staticmethod', 'exposed', "__dunder__"} assert p1._pyroOneway == {'oneway'} def testRegisterTypeReplacementSanity(self): self.serializer.register_type_replacement(int, lambda: None) with pytest.raises(ValueError): self.serializer.register_type_replacement(type, lambda: None) with pytest.raises(ValueError): self.serializer.register_type_replacement(42, lambda: None) def testCustomClassFail(self): o = MyThingFullExposed() s = self.serializer.dumps(o) with pytest.raises(Pyro5.errors.ProtocolError): _ = self.serializer.loads(s) def testCustomClassOk(self): o = MyThingPartlyExposed("test") Pyro5.serializers.SerializerBase.register_class_to_dict(MyThingPartlyExposed, mything_dict) Pyro5.serializers.SerializerBase.register_dict_to_class("CUSTOM-Mythingymabob", mything_creator) s = self.serializer.dumps(o) o2 = self.serializer.loads(s) assert isinstance(o2, MyThingPartlyExposed) assert o2.name == "test" # unregister the deserializer Pyro5.serializers.SerializerBase.unregister_dict_to_class("CUSTOM-Mythingymabob") with pytest.raises(Pyro5.errors.ProtocolError): self.serializer.loads(s) # unregister the serializer Pyro5.serializers.SerializerBase.unregister_class_to_dict(MyThingPartlyExposed) s = self.serializer.dumps(o) with pytest.raises(Pyro5.errors.SerializeError) as x: self.serializer.loads(s) msg = str(x.value) assert msg in ["unsupported serialized class: support.MyThingPartlyExposed", "unsupported serialized class: PyroTests.support.MyThingPartlyExposed"] def testData(self): data = [42, "hello"] ser = self.serializer.dumps(data) data2 = self.serializer.loads(ser) assert data2 == data def testUnicodeData(self): data = "euro\u20aclowbytes\u0000\u0001\u007f\u0080\u00ff" ser = self.serializer.dumps(data) data2 = self.serializer.loads(ser) assert data2 == data def testUUID(self): data = uuid.uuid1() ser = self.serializer.dumps(data) data2 = self.serializer.loads(ser) uuid_as_str = str(data) assert data2==data or data2==uuid_as_str def testSet(self): data = {111, 222, 333} ser = self.serializer.dumps(data) data2 = self.serializer.loads(ser) assert data2 == data def testDeque(self): # serpent converts a deque into a primitive list deq = collections.deque([1, 2, 3, 4]) ser = self.serializer.dumps(deq) data2 = self.serializer.loads(ser) assert data2 == [1, 2, 3, 4] def testCircularRefsValueError(self): with pytest.raises(ValueError): data = [42, "hello", Pyro5.client.Proxy("PYRO:dummy@dummy:4444")] data.append(data) ser = self.serializer.dumps(data) def testCallPlain(self): ser = self.serializer.dumpsCall("object", "method", ("vargs1", "vargs2"), {"kwargs": 999}) obj, method, vargs, kwargs = self.serializer.loadsCall(ser) assert obj == "object" assert method == "method" assert len(vargs) == 2 assert vargs[0] == "vargs1" assert vargs[1] == "vargs2" assert kwargs == {"kwargs": 999} def testCallPyroObjAsArg(self): uri = Pyro5.core.URI("PYRO:555@localhost:80") ser = self.serializer.dumpsCall("object", "method", [uri], {"thing": uri}) obj, method, vargs, kwargs = self.serializer.loadsCall(ser) assert obj == "object" assert method == "method" assert vargs == [uri] assert kwargs == {"thing": uri} def testCallCustomObjAsArg(self): e = ZeroDivisionError("hello") ser = self.serializer.dumpsCall("object", "method", [e], {"thing": e}) obj, method, vargs, kwargs = self.serializer.loadsCall(ser) assert obj == "object" assert method == "method" assert isinstance(vargs, list) assert isinstance(vargs[0], ZeroDivisionError) assert str(vargs[0] == "hello") assert isinstance(kwargs["thing"], ZeroDivisionError) assert str(kwargs["thing"] == "hello") def testSerializeException(self): e = ZeroDivisionError() d = self.serializer.dumps(e) e2 = self.serializer.loads(d) assert isinstance(e2, ZeroDivisionError) assert str(e2 == "") e = ZeroDivisionError("hello") d = self.serializer.dumps(e) e2 = self.serializer.loads(d) assert isinstance(e2, ZeroDivisionError) assert str(e2 == "hello") e = ZeroDivisionError("hello", 42) d = self.serializer.dumps(e) e2 = self.serializer.loads(d) assert isinstance(e2, ZeroDivisionError) assert str(e2) == "('hello', 42)" e.custom_attribute = 999 ser = self.serializer.dumps(e) e2 = self.serializer.loads(ser) assert isinstance(e2, ZeroDivisionError) assert str(e2) == "('hello', 42)" assert e2.custom_attribute == 999 def testSerializeSpecialException(self): assert "GeneratorExit" in Pyro5.serializers.all_exceptions e = GeneratorExit() d = self.serializer.dumps(e) e2 = self.serializer.loads(d) assert isinstance(e2, GeneratorExit) def testRecreateClasses(self): assert self.serializer.recreate_classes([1, 2, 3]) == [1, 2, 3] d = {"__class__": "invalid"} with pytest.raises(Pyro5.errors.ProtocolError): self.serializer.recreate_classes(d) d = {"__class__": "Pyro5.core.URI", "state": ['PYRO', '555', None, 'localhost', 80]} uri = self.serializer.recreate_classes(d) assert uri == Pyro5.core.URI("PYRO:555@localhost:80") number, uri = self.serializer.recreate_classes([1, {"uri": d}]) assert number == 1 assert uri["uri"] == Pyro5.core.URI("PYRO:555@localhost:80") def testUriSerializationWithoutSlots(self): u = Pyro5.core.URI("PYRO:obj@localhost:1234") d = self.serializer.dumps(u) u2 = self.serializer.loads(d) assert isinstance(u2, Pyro5.core.URI) assert str(u2) == "PYRO:obj@localhost:1234" def testFloatPrecision(self): f1 = 1482514078.54635912345 f2 = 9876543212345.12345678987654321 f3 = 11223344.556677889988776655e33 floats = [f1, f2, f3] d = self.serializer.dumps(floats) v = self.serializer.loads(d) assert v, "float precision must not be compromised in any serializer" == floats def testSourceByteTypes_deserialize(self): call_ser = self.serializer.dumpsCall("object", "method", [1, 2, 3], {"kwarg": 42}) ser = self.serializer.dumps([4, 5, 6]) _, _, vargs, _ = self.serializer.loadsCall(bytearray(call_ser)) assert vargs == [1, 2, 3] d = self.serializer.loads(bytearray(ser)) assert d == [4, 5, 6] def testSourceByteTypes_deserialize_memoryview(self): call_ser = self.serializer.dumpsCall("object", "method", [1, 2, 3], {"kwarg": 42}) ser = self.serializer.dumps([4, 5, 6]) _, _, vargs, _ = self.serializer.loadsCall(memoryview(call_ser)) assert vargs == [1, 2, 3] d = self.serializer.loads(memoryview(ser)) assert d == [4, 5, 6] def testSourceByteTypes_loads(self): call_ser = self.serializer.dumpsCall("object", "method", [1, 2, 3], {"kwarg": 42}) ser= self.serializer.dumps([4, 5, 6]) _, _, vargs, _ = self.serializer.loadsCall(bytearray(call_ser)) assert vargs == [1, 2, 3] d = self.serializer.loads(bytearray(ser)) assert d == [4, 5, 6] def testSourceByteTypes_loads_memoryview(self): call_ser = self.serializer.dumpsCall("object", "method", [1, 2, 3], {"kwarg": 42}) ser = self.serializer.dumps([4, 5, 6]) _, _, vargs, _ = self.serializer.loadsCall(memoryview(call_ser)) assert vargs == [1, 2, 3] d = self.serializer.loads(memoryview(ser)) assert d == [4, 5, 6] def testSerializeDumpsAndDumpsCall(self): self.serializer.dumps(uuid.uuid4()) self.serializer.dumps(Pyro5.core.URI("PYRO:test@test:4444")) self.serializer.dumps(Pyro5.client.Proxy("PYRONAME:foobar")) self.serializer.dumpsCall("obj", "method", (1, 2, 3), {"arg1": 999}) self.serializer.dumpsCall("obj", "method", (1, 2, array.array('i', [1, 2, 3])), {"arg1": 999}) self.serializer.dumpsCall("obj", "method", (1, 2, array.array('i', [1, 2, 3])), {"arg1": array.array('i', [1, 2, 3])}) self.serializer.dumpsCall("obj", "method", (1, 2, Pyro5.core.URI("PYRO:test@test:4444")), {"arg1": 999}) self.serializer.dumpsCall("obj", "method", (1, 2, Pyro5.core.URI("PYRO:test@test:4444")), {"arg1": Pyro5.core.URI("PYRO:test@test:4444")}) self.serializer.dumpsCall("obj", "method", (1, 2, Pyro5.client.Proxy("PYRONAME:foobar")), {"arg1": 999}) self.serializer.dumpsCall("obj", "method", (1, 2, Pyro5.client.Proxy("PYRONAME:foobar")), {"arg1": Pyro5.client.Proxy("PYRONAME:foobar")}) def testArrays(self): a1 = array.array('u', "hello") ser = self.serializer.dumps(a1) a2 = self.serializer.loads(ser) if type(a2) is array.array: assert a2 == a1 else: assert a2 == "hello" a1 = array.array('h', [222, 333, 444, 555]) ser = self.serializer.dumps(a1) a2 = self.serializer.loads(ser) if type(a2) is array.array: assert a2 == a1 else: assert a2 == [222, 333, 444, 555] def testArrays2(self): a1 = array.array('u', "hello") ser = self.serializer.dumpsCall("obj", "method", [a1], {}) a2 = self.serializer.loads(ser) a2 = a2["params"][0] if self.SERIALIZER == "json" else a2[2][0] if type(a2) is array.array: assert a2 == a1 else: assert a2 == "hello" a1 = array.array('h', [222, 333, 444, 555]) ser = self.serializer.dumpsCall("obj", "method", [a1], {}) a2 = self.serializer.loads(ser) a2 = a2["params"][0] if self.SERIALIZER == "json" else a2[2][0] if type(a2) is array.array: assert a2 == a1 else: assert a2 == [222, 333, 444, 555] class TestSerializer2_json(TestSerializer2_serpent): SERIALIZER = "json" def testSet(self): data = {111, 222, 333} ser = self.serializer.dumps(data) data2 = self.serializer.loads(ser) assert sorted(data2) == [111, 222, 333] def testDeque(self): pass # can't serialize this in json class TestSerializer2_marshal(TestSerializer2_serpent): SERIALIZER = "marshal" def testNested(self): pass # marshall can't serialize custom objects def testAutoProxyPartlyExposed(self): pass # marshall can't serialize custom objects def testAutoProxyFullExposed(self): pass # marshall can't serialize custom objects def testRegisterTypeReplacementSanity(self): pass # marshall doesn't support this feature at all def testDeque(self): pass # marshall can't serialize custom objects if "msgpack" in Pyro5.serializers.serializers: class TestSerializer2_msgpack(TestSerializer2_serpent): SERIALIZER = "msgpack" def testDeque(self): pass # msgpack can't serialize this def testSet(self): data = {111, 222, 333} ser = self.serializer.dumps(data) data2 = self.serializer.loads(ser) assert sorted(data2) == [111, 222, 333] class TestGenericCases: def testSerializersAvailable(self): _ = Pyro5.serializers.serializers["serpent"] _ = Pyro5.serializers.serializers["marshal"] _ = Pyro5.serializers.serializers["json"] def testAssignedSerializerIds(self): assert Pyro5.serializers.SerpentSerializer.serializer_id == 1 assert Pyro5.serializers.MarshalSerializer.serializer_id == 2 assert Pyro5.serializers.JsonSerializer.serializer_id == 3 assert Pyro5.serializers.MsgpackSerializer.serializer_id == 4 def testSerializersAvailableById(self): _ = Pyro5.serializers.serializers_by_id[1] # serpent _ = Pyro5.serializers.serializers_by_id[2] # marshal _ = Pyro5.serializers.serializers_by_id[3] # json if "msgpack" in Pyro5.serializers.serializers: _ = Pyro5.serializers.serializers_by_id[4] # msgpack assert 0 not in Pyro5.serializers.serializers_by_id assert 5 not in Pyro5.serializers.serializers_by_id def testDictClassFail(self): o = MyThingFullExposed("hello") d = Pyro5.serializers.SerializerBase.class_to_dict(o) assert d["name"] == "hello" assert d["__class__"] == "support.MyThingFullExposed" with pytest.raises(Pyro5.errors.ProtocolError): _ = Pyro5.serializers.SerializerBase.dict_to_class(d) def testDictException(self): x = ZeroDivisionError("hello", 42) expected = { "__class__": None, "__exception__": True, "args": ("hello", 42), "attributes": {} } expected["__class__"] = "builtins.ZeroDivisionError" d = Pyro5.serializers.SerializerBase.class_to_dict(x) assert d == expected x.custom_attribute = 999 expected["attributes"] = {"custom_attribute": 999} d = Pyro5.serializers.SerializerBase.class_to_dict(x) assert d == expected def testDictClassOk(self): uri = Pyro5.core.URI("PYRO:object@host:4444") d = Pyro5.serializers.SerializerBase.class_to_dict(uri) assert d["__class__"] == "Pyro5.core.URI" assert "state" in d x = Pyro5.serializers.SerializerBase.dict_to_class(d) assert isinstance(x, Pyro5.core.URI) assert x == uri assert x.port == 4444 uri = Pyro5.core.URI("PYRO:12345@./u:/tmp/socketname") d = Pyro5.serializers.SerializerBase.class_to_dict(uri) assert d["__class__"] == "Pyro5.core.URI" assert "state" in d x = Pyro5.serializers.SerializerBase.dict_to_class(d) assert isinstance(x, Pyro5.core.URI) assert x == uri assert x.sockname == "/tmp/socketname" def testCustomDictClass(self): o = MyThingPartlyExposed("test") Pyro5.serializers.SerializerBase.register_class_to_dict(MyThingPartlyExposed, mything_dict) Pyro5.serializers.SerializerBase.register_dict_to_class("CUSTOM-Mythingymabob", mything_creator) d = Pyro5.serializers.SerializerBase.class_to_dict(o) assert d["__class__"] == "CUSTOM-Mythingymabob" assert d["name"] == "test" x = Pyro5.serializers.SerializerBase.dict_to_class(d) assert isinstance(x, MyThingPartlyExposed) assert x.name == "test" # unregister the conversion functions and try again Pyro5.serializers.SerializerBase.unregister_class_to_dict(MyThingPartlyExposed) Pyro5.serializers.SerializerBase.unregister_dict_to_class("CUSTOM-Mythingymabob") d_orig = Pyro5.serializers.SerializerBase.class_to_dict(o) clsname = d_orig["__class__"] assert clsname.endswith("support.MyThingPartlyExposed") with pytest.raises(Pyro5.errors.ProtocolError): _ = Pyro5.serializers.SerializerBase.dict_to_class(d) def testExceptionNamespace(self): data = {'__class__': 'builtins.ZeroDivisionError', '__exception__': True, 'args': ('hello', 42), 'attributes': {"test_attribute": 99}} exc = Pyro5.serializers.SerializerBase.dict_to_class(data) assert isinstance(exc, ZeroDivisionError) assert repr(exc) == "ZeroDivisionError('hello', 42)" assert exc.test_attribute == 99 def testExceptionNotTagged(self): data = {'__class__': 'builtins.ZeroDivisionError', 'args': ('hello', 42), 'attributes': {}} with pytest.raises(Pyro5.errors.SerializeError) as cm: _ = Pyro5.serializers.SerializerBase.dict_to_class(data) assert str(cm.value) == "unsupported serialized class: builtins.ZeroDivisionError" def testWeirdFloats(self): ser = Pyro5.serializers.serializers[config.SERIALIZER] p = ser.dumps([float("+inf"), float("-inf"), float("nan")]) s2 = ser.loads(p) assert math.isinf(s2[0]) assert math.copysign(1, s2[0]) == 1.0 assert math.isinf(s2[1]) assert math.copysign(1, s2[1]) == -1.0 assert math.isnan(s2[2]) def mything_dict(obj): return { "__class__": "CUSTOM-Mythingymabob", "name": obj.name } def mything_creator(classname, d): assert classname == "CUSTOM-Mythingymabob" assert d["__class__"] == "CUSTOM-Mythingymabob" return MyThingPartlyExposed(d["name"]) Pyro5-5.15/tests/test_server.py000066400000000000000000001450501451404116400165500ustar00rootroot00000000000000""" Tests for a running Pyro server, without timeouts. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import time import threading import serpent import pytest import Pyro5.core import Pyro5.client import Pyro5.server import Pyro5.errors import Pyro5.serializers import Pyro5.protocol import Pyro5.callcontext import Pyro5.socketutil from Pyro5 import config from support import * @Pyro5.server.expose class ServerTestObject(object): something = 99 dict_attr = {} def __init__(self): self._dictionary = {"number": 42} self.dict_attr = {"number2": 43} self._value = 12345 self.items = list("qwerty") def getDict(self): return self._dictionary def getDictAttr(self): return self.dict_attr def multiply(self, x, y): return x * y def divide(self, x, y): return x // y def ping(self): pass def echo(self, obj): return obj def blob(self, blob): return blob.info, blob.deserialized() @Pyro5.server.oneway def oneway_delay(self, delay): time.sleep(delay) def delay(self, delay): time.sleep(delay) return "slept %d seconds" % delay def delayAndId(self, delay, id): time.sleep(delay) return "slept for " + str(id) def testargs(self, x, *args, **kwargs): return [x, list(args), kwargs] # don't return tuples, this enables us to test json serialization as well. def nonserializableException(self): raise NonserializableError(("xantippe", lambda x: 0)) @Pyro5.server.oneway def oneway_multiply(self, x, y): return x * y @property def value(self): return self._value @value.setter def value(self, newvalue): self._value = newvalue @property def dictionary(self): return self._dictionary def iterator(self): return iter(["one", "two", "three"]) def generator(self): yield "one" yield "two" yield "three" yield "four" yield "five" def response_annotation(self): # part of the annotations tests if "XYZZ" not in Pyro5.callcontext.current_context.annotations: raise ValueError("XYZZ should be present in annotations in the daemon") if Pyro5.callcontext.current_context.annotations["XYZZ"] != b"data from proxy via new api": raise ValueError("XYZZ annotation has wrong data") Pyro5.callcontext.current_context.response_annotations["ANN2"] = b"daemon annotation via new api" return {"annotations_in_daemon": Pyro5.callcontext.current_context.annotations} def new_test_object(self): return ServerTestObject() def __len__(self): return len(self.items) def __iter__(self): return iter(self.items) def __getitem__(self, item): return self.items[item] class NotEverythingExposedClass(object): def __init__(self, name): self.name = name @Pyro5.server.expose def getName(self): return self.name def unexposed(self): return "you should not see this" class DaemonLoopThread(threading.Thread): def __init__(self, pyrodaemon): super().__init__() self.daemon = True self.pyrodaemon = pyrodaemon self.running = threading.Event() self.running.clear() def run(self): self.running.set() try: self.pyrodaemon.requestLoop() except Pyro5.errors.CommunicationError: pass # ignore pyro communication errors class DaemonWithSabotagedHandshake(Pyro5.server.Daemon): def _handshake(self, conn, denied_reason=None): # receive the client's handshake data msg = Pyro5.protocol.recv_stub(conn, [Pyro5.protocol.MSG_CONNECT]) # return a CONNECTFAIL always serializer = Pyro5.serializers.serializers_by_id[msg.serializer_id] data = serializer.dumps("rigged connection failure") msg = Pyro5.protocol.SendingMessage(Pyro5.protocol.MSG_CONNECTFAIL, 0, 1, serializer.serializer_id, data) conn.send(msg.data) return False class TestServerBrokenHandshake: def setup_method(self): config.LOGWIRE = True self.daemon = DaemonWithSabotagedHandshake(port=0) obj = ServerTestObject() uri = self.daemon.register(obj, "something") self.objectUri = uri self.daemonthread = DaemonLoopThread(self.daemon) self.daemonthread.start() self.daemonthread.running.wait() time.sleep(0.05) def teardown_method(self): time.sleep(0.05) self.daemon.shutdown() self.daemonthread.join() def testDaemonConnectFail(self): # check what happens when the daemon responds with a failed connection msg with Pyro5.client.Proxy(self.objectUri) as p: with pytest.raises(Pyro5.errors.CommunicationError) as x: p.ping() message = str(x.value) assert "rejected:" in message assert "rigged connection failure" in message class TestServerOnce: """tests that are fine to run with just a single server type""" def setup_method(self): config.SERIALIZER = "serpent" config.LOGWIRE = True Pyro5.serializers.SerializerBase.register_class_to_dict(ServerTestObject, lambda x: {}) Pyro5.serializers.SerializerBase.register_dict_to_class("test_server.ServerTestObject", lambda cn, d: ServerTestObject()) self.daemon = Pyro5.server.Daemon(port=0) obj = ServerTestObject() uri = self.daemon.register(obj, "something") self.objectUri = uri obj2 = NotEverythingExposedClass("hello") self.daemon.register(obj2, "unexposed") self.daemonthread = DaemonLoopThread(self.daemon) self.daemonthread.start() self.daemonthread.running.wait() time.sleep(0.05) def teardown_method(self): time.sleep(0.05) Pyro5.serializers.SerializerBase.unregister_class_to_dict(ServerTestObject) Pyro5.serializers.SerializerBase.unregister_dict_to_class("test_server.ServerTestObject") if self.daemon is not None: self.daemon.shutdown() self.daemonthread.join() def testPingMessage(self): with Pyro5.client.Proxy(self.objectUri) as p: p._pyroBind() conn = p._pyroConnection msg = Pyro5.protocol.SendingMessage(Pyro5.protocol.MSG_PING, 42, 999, 0, b"something") conn.send(msg.data) msg = Pyro5.protocol.recv_stub(conn, [Pyro5.protocol.MSG_PING]) assert msg.type == Pyro5.protocol.MSG_PING assert msg.seq == 999 assert msg.data == b"pong" Pyro5.protocol.SendingMessage.ping(p._pyroConnection) # the convenience method that does the above def testSequence(self): with Pyro5.client.Proxy(self.objectUri) as p: p.echo(1) p.echo(2) p.echo(3) assert p._pyroSeq, "should have 3 method calls" == 3 p._pyroSeq = 999 # hacking the seq nr won't have any effect because it is the reply from the server that is checked assert p.echo(42) == 42 def testMetaOnAttrs(self): with Pyro5.client.Proxy(self.objectUri) as p: assert p.multiply(5, 11) == 55 # property x = p.getDict() assert x == {"number": 42} p.dictionary.update({"more": 666}) # should not fail because metadata is enabled and the dictionary property is retrieved as local copy x = p.getDict() assert x == {"number": 42} # not updated remotely because we had a local copy with Pyro5.client.Proxy(self.objectUri) as p: with pytest.raises(AttributeError): # attribute should fail (meta only works for exposed properties) p.dict_attr.update({"more": 666}) def testSomeArgumentTypes(self): with Pyro5.client.Proxy(self.objectUri) as p: assert p.testargs(1) == [1, [], {}] assert p.testargs(1, 2, 3, a=4) == [1, [2, 3], {'a': 4}] assert p.testargs(1, **{'a': 2}) == [1, [], {'a': 2}] def testUnicodeKwargs(self): with Pyro5.client.Proxy(self.objectUri) as p: assert p.testargs(1, **{chr(65): 2}) == [1, [], {chr(65): 2}] result = p.testargs(chr(0x20ac), **{chr(0x20ac): 2}) assert chr(0x20ac) == result[0] key = list(result[2].keys())[0] assert chr(0x20ac) == key def testNormalProxy(self): with Pyro5.client.Proxy(self.objectUri) as p: assert p.multiply(7, 6) == 42 def testExceptions(self): with Pyro5.client.Proxy(self.objectUri) as p: with pytest.raises(ZeroDivisionError): p.divide(1, 0) with pytest.raises(TypeError): p.multiply("a", "b") def testProxyMetadata(self): with Pyro5.client.Proxy(self.objectUri) as p: # unconnected proxies have empty metadata assert p._pyroAttrs == set() assert p._pyroMethods == set() assert p._pyroOneway == set() # connecting it should obtain metadata (as long as METADATA is true) p._pyroBind() assert p._pyroAttrs == {'value', 'dictionary'} assert p._pyroMethods == {'echo', 'getDict', 'divide', 'nonserializableException', 'ping', 'oneway_delay', 'delayAndId', 'delay', 'testargs', 'multiply', 'oneway_multiply', 'getDictAttr', 'iterator', 'generator', 'response_annotation', 'blob', 'new_test_object', '__iter__', '__len__', '__getitem__'} assert p._pyroOneway == {'oneway_multiply', 'oneway_delay'} p._pyroAttrs = None p._pyroGetMetadata() assert p._pyroAttrs == {'value', 'dictionary'} p._pyroAttrs = None p._pyroGetMetadata(self.objectUri.object) assert p._pyroAttrs == {'value', 'dictionary'} p._pyroAttrs = None p._pyroGetMetadata(known_metadata={"attrs": set(), "oneway": set(), "methods": {"ping"}}) assert p._pyroAttrs == set() def testProxyAttrsMetadataOn(self): # read attributes with Pyro5.client.Proxy(self.objectUri) as p: # unconnected proxy still has empty metadata. # but, as soon as an attribute is used, the metadata is obtained (as long as METADATA is true) a = p.value assert a == 12345 a = p.multiply assert isinstance(a, Pyro5.client._RemoteMethod) # multiply is still a regular method with pytest.raises(AttributeError): _ = p.non_existing_attribute # set attributes, should also trigger getting metadata with Pyro5.client.Proxy(self.objectUri) as p: p.value = 42 assert p.value == 42 assert "value" in p._pyroAttrs def testProxyAnnotations(self): with Pyro5.client.Proxy(self.objectUri) as p: Pyro5.callcontext.current_context.annotations = {"XYZZ": b"invalid test data"} with pytest.raises(ValueError): p.response_annotation() Pyro5.callcontext.current_context.annotations = {"XYZZ": b"data from proxy via new api"} response = p.response_annotation() assert Pyro5.callcontext.current_context.response_annotations["ANN2"] == b"daemon annotation via new api" # check that the daemon received both the old and the new annotation api data: daemon_annotations = response["annotations_in_daemon"] assert serpent.tobytes(daemon_annotations["XYZZ"]) == b"data from proxy via new api" def testExposedRequired(self): with self.daemon.proxyFor("unexposed") as p: assert p._pyroMethods == {"getName"} assert p.getName() == "hello" with pytest.raises(AttributeError) as e: p.unexposed() expected_msg = "remote object '%s' has no exposed attribute or method 'unexposed'" % p._pyroUri assert str(e.value) == expected_msg with pytest.raises(AttributeError) as e: p.unexposed_set = 999 expected_msg = "remote object '%s' has no exposed attribute 'unexposed_set'" % p._pyroUri assert str(e.value) == expected_msg def testProperties(self): with Pyro5.client.Proxy(self.objectUri) as p: _ = p.value # metadata should be loaded now assert p._pyroAttrs == {"value", "dictionary"} with pytest.raises(AttributeError): _ = p.something with pytest.raises(AttributeError): _ = p._dictionary with pytest.raises(AttributeError): _ = p._value assert p.value == 12345 assert p.dictionary == {"number": 42} def testHasAttr(self): with Pyro5.client.Proxy(self.objectUri) as p: # with metadata on, hasattr actually gives proper results assert hasattr(p, "multiply") assert hasattr(p, "oneway_multiply") assert hasattr(p, "value") assert not hasattr(p, "_value") assert not hasattr(p, "_dictionary") assert not hasattr(p, "non_existing_attribute") def testProxyMetadataKnown(self): with Pyro5.client.Proxy(self.objectUri) as p: # unconnected proxies have empty metadata assert p._pyroAttrs == set() assert p._pyroMethods == set() assert p._pyroOneway == set() # set some metadata manually, they should be overwritten at connection time p._pyroMethods = set("abc") p._pyroAttrs = set("xyz") p._pyroBind() assert p._pyroAttrs != set("xyz") assert p._pyroMethods != set("abc") assert p._pyroOneway != set() def testNonserializableException_other(self): with Pyro5.client.Proxy(self.objectUri) as p: with pytest.raises(Pyro5.errors.PyroError) as x: p.nonserializableException() tblines = "\n".join(Pyro5.errors.get_pyro_traceback(x.type, x.value, x.tb)) assert "unsupported serialized class" in tblines def testBatchProxy(self): with Pyro5.client.Proxy(self.objectUri) as p: batch = Pyro5.client.BatchProxy(p) assert batch.multiply(7, 6) is None assert batch.divide(999, 3) is None assert batch.ping() is None assert batch.divide(999, 0) is None # force an error here assert batch.multiply(3, 4) is None # this call should not be performed anymore results = batch() assert next(results) == 42 assert next(results) == 333 assert next(results) is None with pytest.raises(ZeroDivisionError): next(results) # 999//0 should raise this error with pytest.raises(StopIteration): next(results) # no more results should be available after the error def testBatchOneway(self): with Pyro5.client.Proxy(self.objectUri) as p: batch = Pyro5.client.BatchProxy(p) assert batch.multiply(7, 6) is None assert batch.delay(1) is None # a delay shouldn't matter with oneway assert batch.multiply(3, 4) is None begin = time.time() results = batch(oneway=True) duration = time.time() - begin assert duration < 0.1, "oneway batch with delay should return almost immediately" assert results is None def testPyroTracebackNormal(self): with Pyro5.client.Proxy(self.objectUri) as p: with pytest.raises(ZeroDivisionError) as x: p.divide(999, 0) # force error here # going to check if the magic pyro traceback attribute is available for batch methods too tb = "".join(Pyro5.errors.get_pyro_traceback(x.type, x.value, x.tb)) assert "Remote traceback:" in tb # validate if remote tb is present assert "ZeroDivisionError" in tb # the error assert "return x // y" in tb # the statement def testPyroTracebackBatch(self): with Pyro5.client.Proxy(self.objectUri) as p: batch = Pyro5.client.BatchProxy(p) assert batch.divide(999, 0) is None # force an exception here results = batch() with pytest.raises(ZeroDivisionError) as x: next(results) # going to check if the magic pyro traceback attribute is available for batch methods too tb = "".join(Pyro5.errors.get_pyro_traceback(x.type, x.value, x.tb)) assert "Remote traceback:" in tb # validate if remote tb is present assert "ZeroDivisionError" in tb # the error assert "return x // y" in tb # the statement with pytest.raises(StopIteration): next(results) # no more results should be available after the error def testAutoProxy(self): obj = ServerTestObject() with Pyro5.client.Proxy(self.objectUri) as p: result = p.echo(obj) assert isinstance(result, ServerTestObject), "non-pyro object must be returned as normal class" self.daemon.register(obj) result = p.echo(obj) assert isinstance(result, Pyro5.client.Proxy), "serialized pyro object must be a proxy" self.daemon.register(ServerTestObject) new_result = result.new_test_object() assert isinstance(new_result, Pyro5.client.Proxy), "serialized pyro object must be a proxy" self.daemon.unregister(ServerTestObject) self.daemon.unregister(obj) result = p.echo(obj) assert isinstance(result, ServerTestObject), "unregistered pyro object must be normal class again" def testRegisterWeak(self): obj=ServerTestObject() uri=self.daemon.register(obj,weak=True) with Pyro5.client.Proxy(uri) as p: result = p.getDict() assert isinstance(result, dict), "getDict() is proxied normally" del obj # weak registration should not prevent the obj from being garbage-collected with pytest.raises(Pyro5.errors.DaemonError): result = p.getDict() def testConnectOnce(self): with Pyro5.client.Proxy(self.objectUri) as proxy: assert proxy._pyroBind(), "first bind should always connect" assert not proxy._pyroBind(), "second bind should not connect again" def testMaxMsgSize(self): with Pyro5.client.Proxy(self.objectUri) as p: bigobject = [42] * 1000 result = p.echo(bigobject) assert bigobject == result try: config.MAX_MESSAGE_SIZE = 999 with pytest.raises(Pyro5.errors.ProtocolError): _ = p.echo(bigobject) # message too large finally: config.MAX_MESSAGE_SIZE = 1024* 1024* 1024 def testIterator(self): with Pyro5.client.Proxy(self.objectUri) as p: iterator = p.iterator() assert isinstance(iterator, Pyro5.client._StreamResultIterator) assert next(iterator) == "one" assert next(iterator) == "two" assert next(iterator) == "three" with pytest.raises(StopIteration): next(iterator) iterator.close() def testLenAndIterAndIndexing(self): with Pyro5.client.Proxy(self.objectUri) as p: assert len(p) == 6 values = list(iter(p)) assert values == ['q', 'w', 'e', 'r', 't', 'y'] assert p[0] == 'q' assert p[1] == 'w' assert p[2] == 'e' assert p[3] == 'r' assert p[4] == 't' assert p[5] == 'y' with pytest.raises(IndexError): _ = p[6] def testGenerator(self): with Pyro5.client.Proxy(self.objectUri) as p: generator = p.generator() assert isinstance(generator, Pyro5.client._StreamResultIterator) assert next(generator) == "one" assert next(generator) == "two" assert next(generator) == "three" assert next(generator) == "four" assert next(generator) == "five" with pytest.raises(StopIteration): next(generator) with pytest.raises(StopIteration): next(generator) generator.close() generator = p.generator() _ = [v for v in generator] with pytest.raises(StopIteration): next(generator) generator.close() def testCleanup(self): p1 = Pyro5.client.Proxy(self.objectUri) p2 = Pyro5.client.Proxy(self.objectUri) p3 = Pyro5.client.Proxy(self.objectUri) p1.echo(42) p2.echo(42) p3.echo(42) # we have several active connections still up, see if we can cleanly shutdown the daemon # (it should interrupt the worker's socket connections) time.sleep(0.1) self.daemon.shutdown() self.daemon = None p1._pyroRelease() p2._pyroRelease() p3._pyroRelease() def testSerializedBlob(self): sb = Pyro5.client.SerializedBlob("blobname", [1, 2, 3]) assert sb.info == "blobname" assert sb.deserialized() == [1, 2, 3] def testSerializedBlobMessage(self): serializer = Pyro5.serializers.serializers["serpent"] data = serializer.dumpsCall("object", "method", ([1, 2, 3],), {"kwarg": 42}) msg = Pyro5.protocol.SendingMessage(Pyro5.protocol.MSG_INVOKE, 0, 42, serializer.serializer_id, data) sb = Pyro5.client.SerializedBlob("blobname", msg, is_blob=True) assert sb.info == "blobname" assert sb.deserialized() == ([1, 2, 3], ) def testProxySerializedBlobArg(self): with Pyro5.client.Proxy(self.objectUri) as p: blobinfo, blobdata = p.blob(Pyro5.client.SerializedBlob("blobname", [1, 2, 3])) assert blobinfo == "blobname" assert blobdata == [1, 2, 3] def testResourceFreeing(self): rsvc = ResourceService() uri = self.daemon.register(rsvc) with Pyro5.client.Proxy(uri) as p: p.allocate("r1") p.allocate("r2") resources = {r.name: r for r in rsvc.resources} p.free("r1") rsc = p.list() assert rsc == ["r2"] assert resources["r1"].close_called assert not resources["r2"].close_called time.sleep(0.02) assert resources["r1"].close_called assert resources["r2"].close_called with Pyro5.client.Proxy(uri) as p: rsc = p.list() assert rsc == [], "r2 must now be freed due to connection loss earlier" class TestServerThreadNoTimeout: SERVERTYPE = "thread" COMMTIMEOUT = None def setup_method(self): config.SERIALIZER = "serpent" config.LOGWIRE = True config.POLLTIMEOUT = 0.1 config.SERVERTYPE = self.SERVERTYPE config.COMMTIMEOUT = self.COMMTIMEOUT self.daemon = Pyro5.server.Daemon(port=0) obj = ServerTestObject() uri = self.daemon.register(obj, "something") self.objectUri = uri self.daemonthread = DaemonLoopThread(self.daemon) self.daemonthread.start() self.daemonthread.running.wait() time.sleep(0.05) def teardown_method(self): time.sleep(0.05) self.daemon.shutdown() self.daemonthread.join() config.SERVERTYPE = "thread" config.COMMTIMEOUT = None def testConnectionStuff(self): p1 = Pyro5.client.Proxy(self.objectUri) p2 = Pyro5.client.Proxy(self.objectUri) assert not p1._pyroConnection assert not p2._pyroConnection p1.ping() p2.ping() _ = p1.multiply(11, 5) _ = p2.multiply(11, 5) assert p1._pyroConnection assert p2._pyroConnection p1._pyroRelease() p1._pyroRelease() p2._pyroRelease() p2._pyroRelease() assert not p1._pyroConnection assert not p2._pyroConnection p1._pyroBind() _ = p1.multiply(11, 5) _ = p2.multiply(11, 5) assert p1._pyroConnection assert p2._pyroConnection assert p1._pyroUri.protocol == "PYRO" assert p2._pyroUri.protocol == "PYRO" p1._pyroRelease() p2._pyroRelease() def testReconnectAndCompression(self): # try reconnects with Pyro5.client.Proxy(self.objectUri) as p: assert not p._pyroConnection p._pyroReconnect(tries=100) assert p._pyroConnection assert not p._pyroConnection # test compression: try: with Pyro5.client.Proxy(self.objectUri) as p: config.COMPRESSION = True assert p.multiply(5, 11) == 55 assert p.multiply("*" * 500, 2) == "*" * 1000 finally: config.COMPRESSION = False def testOnewayMetaOn(self): with Pyro5.client.Proxy(self.objectUri) as p: assert p._pyroOneway == set() # when not bound, no meta info exchange has been done p._pyroBind() assert "oneway_multiply" in p._pyroOneway # after binding, meta info has been processed assert p.multiply(5, 11) == 55 # not tagged as @Pyro5.server.oneway assert p.oneway_multiply(5, 11) is None # tagged as @Pyro5.server.oneway p._pyroOneway = set() assert p.multiply(5, 11) == 55 assert p.oneway_multiply(5, 11) == 55 # check nonexisting method behavoir for oneway methods with pytest.raises(AttributeError): p.nonexisting_method() p._pyroOneway.add("nonexisting_method") # now it should still fail because of metadata telling Pyro what methods actually exist with pytest.raises(AttributeError): p.nonexisting_method() def testOnewayWithProxySubclass(self): class ProxyWithOneway(Pyro5.client.Proxy): def __init__(self, arg): super(ProxyWithOneway, self).__init__(arg) self._pyroOneway = {"oneway_multiply", "multiply"} with ProxyWithOneway(self.objectUri) as p: assert p.oneway_multiply(5, 11) is None assert p.multiply(5, 11) == 55 p._pyroOneway = set() assert p.oneway_multiply(5, 11) == 55 assert p.multiply(5, 11) == 55 def testOnewayDelayed(self): with Pyro5.client.Proxy(self.objectUri) as p: p.ping() now = time.time() p.oneway_delay(1) # oneway so we should continue right away time.sleep(0.01) assert time.time() - now < 0.2, "delay should be running as oneway" now = time.time() assert p.multiply(5, 11), "expected a normal result from a non-oneway call" == 55 assert time.time() - now < 0.2, "delay should be running in its own thread" def testSerializeConnected(self): # online serialization tests ser = Pyro5.serializers.serializers[config.SERIALIZER] proxy = Pyro5.client.Proxy(self.objectUri) proxy._pyroBind() assert proxy._pyroConnection p = ser.dumps(proxy) proxy2 = ser.loads(p) assert proxy2._pyroConnection is None assert proxy._pyroConnection assert proxy._pyroUri == proxy2._pyroUri proxy2._pyroBind() assert proxy2._pyroConnection assert proxy2._pyroConnection is not proxy._pyroConnection proxy._pyroRelease() proxy2._pyroRelease() assert proxy._pyroConnection is None assert proxy2._pyroConnection is None proxy.ping() proxy2.ping() # try copying a connected proxy import copy proxy3 = copy.copy(proxy) assert proxy3._pyroConnection is None assert proxy._pyroConnection assert proxy._pyroUri == proxy3._pyroUri assert proxy3._pyroUri is not proxy._pyroUri proxy._pyroRelease() proxy2._pyroRelease() proxy3._pyroRelease() def testException(self): with Pyro5.client.Proxy(self.objectUri) as p: with pytest.raises(ZeroDivisionError) as x: p.divide(1, 0) pyrotb = "".join(Pyro5.errors.get_pyro_traceback(x.type, x.value, x.tb)) assert "Remote traceback" in pyrotb assert "ZeroDivisionError" in pyrotb def testTimeoutCall(self): config.COMMTIMEOUT = None with Pyro5.client.Proxy(self.objectUri) as p: p.ping() start = time.time() p.delay(0.5) duration = time.time() - start assert 0.4 < duration < 0.6 p._pyroTimeout = 0.1 start = time.time() with pytest.raises(Pyro5.errors.TimeoutError): p.delay(1) duration = time.time() - start assert duration < 0.3 def testTimeoutConnect(self): # set up a unresponsive daemon with Pyro5.server.Daemon(port=0) as d: time.sleep(0.5) obj = ServerTestObject() uri = d.register(obj) # we're not going to start the daemon's event loop p = Pyro5.client.Proxy(uri) p._pyroTimeout = 0.2 start = time.time() with pytest.raises(Pyro5.errors.TimeoutError) as e: p.ping() assert str(e.value) == "receiving: timeout" # XXX todo: add test about proxy thread ownership transfer def testServerConnections(self): # check if the server allows to grow the number of connections proxies = [Pyro5.client.Proxy(self.objectUri) for _ in range(10)] try: for p in proxies: p._pyroTimeout = 0.5 p._pyroBind() for p in proxies: p.ping() finally: for p in proxies: p._pyroRelease() def testGeneratorProxyClose(self): p = Pyro5.client.Proxy(self.objectUri) generator = p.generator() p._pyroRelease() with pytest.raises(Pyro5.errors.ConnectionClosedError): next(generator) def testGeneratorLinger(self): orig_linger = config.ITER_STREAM_LINGER orig_commt = config.COMMTIMEOUT orig_pollt = config.POLLTIMEOUT try: config.ITER_STREAM_LINGER = 0.5 config.COMMTIMEOUT = 0.2 config.POLLTIMEOUT = 0.2 p = Pyro5.client.Proxy(self.objectUri) generator = p.generator() assert next(generator) == "one" p._pyroRelease() with pytest.raises(Pyro5.errors.ConnectionClosedError): next(generator) p._pyroReconnect() assert next(generator), "generator should resume after reconnect" == "two" # check that after the linger time passes, the generator *is* gone p._pyroRelease() time.sleep(2) p._pyroReconnect() with pytest.raises(Pyro5.errors.PyroError): # should not be resumable anymore next(generator) finally: config.ITER_STREAM_LINGER = orig_linger config.COMMTIMEOUT = orig_commt config.POLLTIMEOUT = orig_pollt def testGeneratorNoLinger(self): orig_linger = config.ITER_STREAM_LINGER try: p = Pyro5.client.Proxy(self.objectUri) config.ITER_STREAM_LINGER = 0 # disable linger generator = p.generator() assert next(generator) == "one" p._pyroRelease() time.sleep(0.2) with pytest.raises(Pyro5.errors.ConnectionClosedError): next(generator) p._pyroReconnect() with pytest.raises(Pyro5.errors.PyroError): # should not be resumable after reconnect next(generator) generator.close() finally: config.ITER_STREAM_LINGER = orig_linger class TestServerMultiplexNoTimeout(TestServerThreadNoTimeout): SERVERTYPE = "multiplex" COMMTIMEOUT = None def testException(self): pass class TestMetaAndExpose: def testBasic(self): o = MyThingFullExposed("irmen") m1 = Pyro5.server._get_exposed_members(o) m2 = Pyro5.server._get_exposed_members(MyThingFullExposed) assert m1 == m2 keys = m1.keys() assert len(keys) == 3 assert "methods" in keys assert "attrs" in keys assert "oneway" in keys def testGetExposedCacheWorks(self): class Thingy(object): def method1(self): pass @property def prop(self): return 1 def notexposed(self): pass m1 = Pyro5.server._get_exposed_members(Thingy, only_exposed=False) def new_method(self, arg): return arg Thingy.new_method = new_method m2 = Pyro5.server._get_exposed_members(Thingy, only_exposed=False) assert m2, "should still be equal because result from cache" == m1 def testPrivateNotExposed(self): o = MyThingFullExposed("irmen") m = Pyro5.server._get_exposed_members(o) assert m["methods"] == {"classmethod", "staticmethod", "method", "__dunder__", "oneway", "exposed"} assert m["attrs"] == {"prop1", "readonly_prop1", "prop2"} assert m["oneway"] == {"oneway"} o = MyThingPartlyExposed("irmen") m = Pyro5.server._get_exposed_members(o) assert m["methods"] == {"oneway", "exposed"} assert m["attrs"] == {"prop1", "readonly_prop1"} assert m["oneway"] == {"oneway"} def testNotOnlyExposed(self): o = MyThingPartlyExposed("irmen") m = Pyro5.server._get_exposed_members(o, only_exposed=False) assert m["methods"] == {"classmethod", "staticmethod", "method", "__dunder__", "oneway", "exposed"} assert m["attrs"] == {"prop1", "readonly_prop1", "prop2"} assert m["oneway"] == {"oneway"} def testPartlyExposedSubclass(self): o = MyThingPartlyExposedSub("irmen") m = Pyro5.server._get_exposed_members(o) assert m["attrs"] == {"prop1", "readonly_prop1"} assert m["oneway"] == {"oneway"} assert m["methods"] == {"sub_exposed", "exposed", "oneway"} def testExposedSubclass(self): o = MyThingExposedSub("irmen") m = Pyro5.server._get_exposed_members(o) assert m["attrs"] == {"readonly_prop1", "prop1", "prop2"} assert m["oneway"] == {"oneway", "oneway2"} assert m["methods"] == {"classmethod", "staticmethod", "oneway", "__dunder__", "method", "exposed", "oneway2", "sub_exposed", "sub_unexposed"} def testExposePrivateFails(self): with pytest.raises(AttributeError): class Test1(object): @Pyro5.server.expose def _private(self): pass with pytest.raises(AttributeError): class Test3(object): @Pyro5.server.expose def __private(self): pass with pytest.raises(AttributeError): @Pyro5.server.expose class _Test4(object): pass with pytest.raises(AttributeError): @Pyro5.server.expose class __Test5(object): pass def testExposeDunderOk(self): class Test1(object): @Pyro5.server.expose def __dunder__(self): pass assert Test1.__dunder__._pyroExposed @Pyro5.server.expose class Test2(object): def __dunder__(self): pass assert Test2._pyroExposed assert Test2.__dunder__._pyroExposed def testClassmethodExposeWrongOrderFail(self): with pytest.raises(AttributeError) as ax: class TestClass: @Pyro5.server.expose @classmethod def cmethod(cls): pass assert "must be done after" in str(ax.value) with pytest.raises(AttributeError) as ax: class TestClass: @Pyro5.server.expose @staticmethod def smethod(cls): pass assert "must be done after" in str(ax.value) def testClassmethodExposeCorrectOrderOkay(self): class TestClass: @classmethod @Pyro5.server.expose def cmethod(cls): pass @staticmethod @Pyro5.server.expose def smethod(cls): pass assert TestClass.cmethod._pyroExposed assert TestClass.smethod._pyroExposed def testGetExposedProperty(self): o = MyThingFullExposed("irmen") with pytest.raises(AttributeError): Pyro5.server._get_exposed_property_value(o, "name") with pytest.raises(AttributeError): Pyro5.server._get_exposed_property_value(o, "c_attr") with pytest.raises(AttributeError): Pyro5.server._get_exposed_property_value(o, "propvalue") with pytest.raises(AttributeError): Pyro5.server._get_exposed_property_value(o, "unexisting_attribute") assert Pyro5.server._get_exposed_property_value(o, "prop1") == 42 assert Pyro5.server._get_exposed_property_value(o, "prop2") == 42 def testGetExposedPropertyFromPartiallyExposed(self): o = MyThingPartlyExposed("irmen") with pytest.raises(AttributeError): Pyro5.server._get_exposed_property_value(o, "name") with pytest.raises(AttributeError): Pyro5.server._get_exposed_property_value(o, "c_attr") with pytest.raises(AttributeError): Pyro5.server._get_exposed_property_value(o, "propvalue") with pytest.raises(AttributeError): Pyro5.server._get_exposed_property_value(o, "unexisting_attribute") assert Pyro5.server._get_exposed_property_value(o, "prop1") == 42 with pytest.raises(AttributeError): Pyro5.server._get_exposed_property_value(o, "prop2") def testSetExposedProperty(self): o = MyThingFullExposed("irmen") with pytest.raises(AttributeError): Pyro5.server._set_exposed_property_value(o, "name", "erorr") with pytest.raises(AttributeError): Pyro5.server._set_exposed_property_value(o, "unexisting_attribute", 42) with pytest.raises(AttributeError): Pyro5.server._set_exposed_property_value(o, "readonly_prop1", 42) with pytest.raises(AttributeError): Pyro5.server._set_exposed_property_value(o, "propvalue", 999) assert o.prop1 == 42 assert o.prop2 == 42 Pyro5.server._set_exposed_property_value(o, "prop1", 999) assert o.propvalue == 999 Pyro5.server._set_exposed_property_value(o, "prop2", 8888) assert o.propvalue == 8888 def testSetExposedPropertyFromPartiallyExposed(self): o = MyThingPartlyExposed("irmen") with pytest.raises(AttributeError): Pyro5.server._set_exposed_property_value(o, "name", "erorr") with pytest.raises(AttributeError): Pyro5.server._set_exposed_property_value(o, "unexisting_attribute", 42) with pytest.raises(AttributeError): Pyro5.server._set_exposed_property_value(o, "readonly_prop1", 42) with pytest.raises(AttributeError): Pyro5.server._set_exposed_property_value(o, "propvalue", 999) assert o.prop1 == 42 assert o.prop2 == 42 Pyro5.server._set_exposed_property_value(o, "prop1", 999) assert o.propvalue == 999 with pytest.raises(AttributeError): Pyro5.server._set_exposed_property_value(o, "prop2", 8888) def testIsPrivateName(self): assert Pyro5.server.is_private_attribute("_") assert Pyro5.server.is_private_attribute("__") assert Pyro5.server.is_private_attribute("___") assert Pyro5.server.is_private_attribute("_p") assert Pyro5.server.is_private_attribute("_pp") assert Pyro5.server.is_private_attribute("_p_") assert Pyro5.server.is_private_attribute("_p__") assert Pyro5.server.is_private_attribute("__p") assert Pyro5.server.is_private_attribute("___p") assert not Pyro5.server.is_private_attribute("__dunder__") # dunder methods should not be private except a list of exceptions as tested below assert Pyro5.server.is_private_attribute("__init__") assert Pyro5.server.is_private_attribute("__call__") assert Pyro5.server.is_private_attribute("__new__") assert Pyro5.server.is_private_attribute("__del__") assert Pyro5.server.is_private_attribute("__repr__") assert Pyro5.server.is_private_attribute("__str__") assert Pyro5.server.is_private_attribute("__format__") assert Pyro5.server.is_private_attribute("__nonzero__") assert Pyro5.server.is_private_attribute("__bool__") assert Pyro5.server.is_private_attribute("__coerce__") assert Pyro5.server.is_private_attribute("__cmp__") assert Pyro5.server.is_private_attribute("__eq__") assert Pyro5.server.is_private_attribute("__ne__") assert Pyro5.server.is_private_attribute("__lt__") assert Pyro5.server.is_private_attribute("__gt__") assert Pyro5.server.is_private_attribute("__le__") assert Pyro5.server.is_private_attribute("__ge__") assert Pyro5.server.is_private_attribute("__hash__") assert Pyro5.server.is_private_attribute("__dir__") assert Pyro5.server.is_private_attribute("__enter__") assert Pyro5.server.is_private_attribute("__exit__") assert Pyro5.server.is_private_attribute("__copy__") assert Pyro5.server.is_private_attribute("__deepcopy__") assert Pyro5.server.is_private_attribute("__sizeof__") assert Pyro5.server.is_private_attribute("__getattr__") assert Pyro5.server.is_private_attribute("__setattr__") assert Pyro5.server.is_private_attribute("__hasattr__") assert Pyro5.server.is_private_attribute("__delattr__") assert Pyro5.server.is_private_attribute("__getattribute__") assert Pyro5.server.is_private_attribute("__instancecheck__") assert Pyro5.server.is_private_attribute("__subclasscheck__") assert Pyro5.server.is_private_attribute("__subclasshook__") assert Pyro5.server.is_private_attribute("__getinitargs__") assert Pyro5.server.is_private_attribute("__getnewargs__") assert Pyro5.server.is_private_attribute("__getstate__") assert Pyro5.server.is_private_attribute("__setstate__") assert Pyro5.server.is_private_attribute("__reduce__") assert Pyro5.server.is_private_attribute("__reduce_ex__") def testResolveAttr(self): @Pyro5.server.expose class Exposed(object): def __init__(self, value): self.propvalue = value self.__value__ = value # is not affected by the @expose def __str__(self): return "<%s>" % self.value def _p(self): return "should not be allowed" def __p(self): return "should not be allowed" def __p__(self): return "should be allowed (dunder)" @property def value(self): return self.propvalue class Unexposed(object): def __init__(self): self.value = 42 def __value__(self): return self.value obj = Exposed("hello") obj.a = Exposed("a") obj.a.b = Exposed("b") obj.a.b.c = Exposed("c") obj.a._p = Exposed("p1") obj.a._p.q = Exposed("q1") obj.a.__p = Exposed("p2") obj.a.__p.q = Exposed("q2") obj.u = Unexposed() obj.u.v = Unexposed() # check the accessible attributes assert str(Pyro5.server._get_attribute(obj, "a")) == "" dunder = str(Pyro5.server._get_attribute(obj, "__p__")) assert dunder.startswith(" 4 myip = socketutil.get_ip_address("", workaround127=True) assert len(str(myip)) > 4 assert not str(myip).startswith("127.") addr = socketutil.get_ip_address("127.0.0.1", workaround127=False) assert "127.0.0.1" == str(addr) assert addr.version == 4 addr = socketutil.get_ip_address("127.0.0.1", workaround127=True) assert "127.0.0.1" != str(addr) assert addr.version == 4 def testGetIP6(self): if not has_ipv6: pytest.skip("no ipv6 capability") addr = socketutil.get_ip_address("::1", version=6) assert addr.version == 6 assert ":" in str(addr) addr = socketutil.get_ip_address("localhost", version=6) assert addr.version == 6 assert ":" in str(addr) def testGetInterface(self): addr = socketutil.get_interface("localhost") assert addr.version == 4 assert str(addr).startswith("127.") assert str(addr.ip).startswith("127.0") assert str(addr.network).startswith("127.0") if has_ipv6: addr = socketutil.get_interface("::1") assert addr.version == 6 assert ":" in str(addr) assert ":" in str(addr.ip) assert ":" in str(addr.network) def testUnusedPort(self): port1 = socketutil.find_probably_unused_port() port2 = socketutil.find_probably_unused_port() assert port1 > 0 assert port1 != port2 port1 = socketutil.find_probably_unused_port(socktype=socket.SOCK_DGRAM) port2 = socketutil.find_probably_unused_port(socktype=socket.SOCK_DGRAM) assert port1 > 0 assert port1 != port2 def testUnusedPort6(self): if not has_ipv6: pytest.skip("no ipv6 capability") port1 = socketutil.find_probably_unused_port(family=socket.AF_INET6) port2 = socketutil.find_probably_unused_port(family=socket.AF_INET6) assert port1 > 0 assert port1 != port2 port1 = socketutil.find_probably_unused_port(family=socket.AF_INET6, socktype=socket.SOCK_DGRAM) port2 = socketutil.find_probably_unused_port(family=socket.AF_INET6, socktype=socket.SOCK_DGRAM) assert port1 > 0 assert port1 != port2 def testBindUnusedPort(self): sock1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) port1 = socketutil.bind_unused_port(sock1) port2 = socketutil.bind_unused_port(sock2) assert port1 > 0 assert port1 != port2 assert sock1.getsockname() == ("127.0.0.1", port1) sock1.close() sock2.close() def testBindUnusedPort6(self): if not has_ipv6: pytest.skip("no ipv6 capability") sock1 = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) sock2 = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) port1 = socketutil.bind_unused_port(sock1) port2 = socketutil.bind_unused_port(sock2) assert port1 > 0 assert port1 != port2 host, port, _, _ = sock1.getsockname() assert ":" in host assert port1 == port sock1.close() sock2.close() def testCreateUnboundSockets(self): s = socketutil.create_socket() assert socket.AF_INET == s.family bs = socketutil.create_bc_socket() assert socket.AF_INET == bs.family with contextlib.suppress(socket.error): host, port = s.getsockname() # can either fail with socket.error or return (host,0) assert 0 == port with contextlib.suppress(socket.error): host, port = bs.getsockname() # can either fail with socket.error or return (host,0) assert 0 == port s.close() bs.close() def testCreateUnboundSockets6(self): if not has_ipv6: pytest.skip("no ipv6 capability") s = socketutil.create_socket(ipv6=True) assert socket.AF_INET6 == s.family bs = socketutil.create_bc_socket(ipv6=True) assert socket.AF_INET6 == bs.family with contextlib.suppress(socket.error): host, port, _, _ = s.getsockname() # can either fail with socket.error or return (host,0) assert 0 == port with contextlib.suppress(socket.error): host, port, _, _ = bs.getsockname() # can either fail with socket.error or return (host,0) assert 0 == port s.close() bs.close() def testCreateBoundSockets(self): s = socketutil.create_socket(bind=('127.0.0.1', 0)) assert socket.AF_INET == s.family bs = socketutil.create_bc_socket(bind=('127.0.0.1', 0)) assert '127.0.0.1' == s.getsockname()[0] assert '127.0.0.1' == bs.getsockname()[0] s.close() bs.close() with pytest.raises(ValueError): socketutil.create_socket(bind=('localhost', 12345), connect=('localhost', 1234)) def testCreateBoundSockets6(self): if not has_ipv6: pytest.skip("no ipv6 capability") s = socketutil.create_socket(bind=('::1', 0)) assert socket.AF_INET6 == s.family bs = socketutil.create_bc_socket(bind=('::1', 0)) assert ':' in s.getsockname()[0] assert ':' in bs.getsockname()[0] s.close() bs.close() with pytest.raises(ValueError): socketutil.create_socket(bind=('::1', 12345), connect=('::1', 1234)) def testCreateBoundUnixSockets(self): if not hasattr(socket, "AF_UNIX"): pytest.skip("no unix domain sockets capability") SOCKNAME = "test_unixsocket" if os.path.exists(SOCKNAME): os.remove(SOCKNAME) s = socketutil.create_socket(bind=SOCKNAME) assert socket.AF_UNIX == s.family assert SOCKNAME == s.getsockname() s.close() if os.path.exists(SOCKNAME): os.remove(SOCKNAME) with pytest.raises(ValueError): socketutil.create_socket(bind=SOCKNAME, connect=SOCKNAME) def testAbstractNamespace(self): if not hasattr(socket, "AF_UNIX") and not sys.platform.startswith("linux"): pytest.skip("no unix domain sockets capability, and not Linux") SOCKNAME = "\0test_unixsocket_abstract_ns" # mind the \0 at the start s = socketutil.create_socket(bind=SOCKNAME) assert bytes(SOCKNAME, "ascii") == s.getsockname() s.close() def testSend(self): ss = socketutil.create_socket(bind=("localhost", 0)) port = ss.getsockname()[1] cs = socketutil.create_socket(connect=("localhost", port)) socketutil.send_data(cs, b"foobar!" * 10) cs.shutdown(socket.SHUT_WR) a = ss.accept() data = socketutil.receive_data(a[0], 5) assert b"fooba" == data data = socketutil.receive_data(a[0], 5) assert b"r!foo" == data a[0].close() ss.close() cs.close() def testSendUnix(self): if not hasattr(socket, "AF_UNIX"): pytest.skip("no unix domain sockets capability") SOCKNAME = "test_unixsocket" if os.path.exists(SOCKNAME): os.remove(SOCKNAME) ss = socketutil.create_socket(bind=SOCKNAME) cs = socketutil.create_socket(connect=SOCKNAME) socketutil.send_data(cs, b"foobar!" * 10) cs.shutdown(socket.SHUT_WR) a = ss.accept() data = socketutil.receive_data(a[0], 5) assert b"fooba" == data data = socketutil.receive_data(a[0], 5) assert b"r!foo" == data a[0].close() ss.close() cs.close() if os.path.exists(SOCKNAME): os.remove(SOCKNAME) @pytest.mark.network def testBroadcast(self): ss = socketutil.create_bc_socket((None, 0)) port = ss.getsockname()[1] cs = socketutil.create_bc_socket() for bcaddr in config.BROADCAST_ADDRS: try: cs.sendto(b"monkey", 0, (bcaddr, port)) except socket.error as x: err = getattr(x, "errno", x.args[0]) # handle some errno that some platforms like to throw if err not in socketutil.ERRNO_EADDRNOTAVAIL and err not in socketutil.ERRNO_EADDRINUSE: raise data, _ = ss.recvfrom(500) assert b"monkey" == data cs.close() ss.close() def testMsgWaitallProblems(self): ss = socketutil.create_socket(bind=("localhost", 0), timeout=2) port = ss.getsockname()[1] cs = socketutil.create_socket(connect=("localhost", port), timeout=2) a = ss.accept() # test some sizes that might be problematic with MSG_WAITALL and check that they work fine for size in [1000, 10000, 32000, 32768, 32780, 41950, 41952, 42000, 65000, 65535, 65600, 80000]: socketutil.send_data(cs, b"x" * size) data = socketutil.receive_data(a[0], size) socketutil.send_data(a[0], data) data = socketutil.receive_data(cs, size) assert size == len(data) a[0].close() ss.close() cs.close() def testMsgWaitallProblems2(self): class ReceiveThread(threading.Thread): def __init__(self, sock, sizes): super(ReceiveThread, self).__init__() self.sock = sock self.sizes = sizes def run(self): cs, _ = self.sock.accept() for size in self.sizes: data = socketutil.receive_data(cs, size) socketutil.send_data(cs, data) cs.close() ss = socketutil.create_socket(bind=("localhost", 0)) SIZES = [1000, 10000, 32000, 32768, 32780, 41950, 41952, 42000, 65000, 65535, 65600, 80000, 999999] serverthread = ReceiveThread(ss, SIZES) serverthread.daemon = True serverthread.start() port = ss.getsockname()[1] cs = socketutil.create_socket(connect=("localhost", port), timeout=2) # test some sizes that might be problematic with MSG_WAITALL and check that they work fine for size in SIZES: socketutil.send_data(cs, b"x" * size) data = socketutil.receive_data(cs, size) assert size == len(data) serverthread.join() ss.close() cs.close() def testMsgWaitAllConfig(self): if platform.system() == "Windows": # default config should be False on these platforms even though socket.MSG_WAITALL might exist assert not socketutil.USE_MSG_WAITALL else: # on all other platforms, default config should be True (as long as socket.MSG_WAITALL exists) if hasattr(socket, "MSG_WAITALL"): assert socketutil.USE_MSG_WAITALL else: assert not socketutil.USE_MSG_WAITALL class ServerTestDaemon(server.Daemon): pass class ServerCallback(object): def _handshake(self, connection, denied_reason=None): raise RuntimeError("this handshake method should never be called") def handleRequest(self, connection): if not isinstance(connection, socketutil.SocketConnection): raise TypeError("handleRequest expected SocketConnection parameter") msg = protocol.recv_stub(connection, [protocol.MSG_PING]) if msg.type == protocol.MSG_PING: msg = protocol.SendingMessage(protocol.MSG_PING, 0, msg.seq, msg.serializer_id, b"ping") connection.send(msg.data) else: print("unhandled message type", msg.type) connection.close() def _housekeeping(self): pass class ServerCallback_BrokenHandshake(ServerCallback): def _handshake(self, connection, denied_reason=None): raise ZeroDivisionError("handshake crashed (on purpose)") class TestSocketServer: def testServer_thread(self): daemon = ServerCallback() port = socketutil.find_probably_unused_port() serv = SocketServer_Threadpool() serv.init(daemon, "localhost", port) assert serv.locationStr == "localhost:" + str(port) assert serv.sock is not None conn = socketutil.SocketConnection(serv.sock, "ID12345") assert conn.objectId == "ID12345" assert conn.sock is not None conn.close() conn.close() assert conn.sock is not None, "connections keep their socket object even if it's closed" serv.close() serv.close() assert serv.sock is None def testServer_multiplex(self): daemon = ServerCallback() port = socketutil.find_probably_unused_port() serv = SocketServer_Multiplex() serv.init(daemon, "localhost", port) assert serv.locationStr == "localhost:" + str(port) assert serv.sock is not None conn = socketutil.SocketConnection(serv.sock, "ID12345") assert conn.objectId == "ID12345" assert conn.sock is not None conn.close() conn.close() assert conn.sock is not None, "connections keep their socket object even if it's closed" serv.close() serv.close() assert serv.sock is None class TestServerDOS_multiplex: def setup_method(self): self.orig_poll_timeout = config.POLLTIMEOUT self.orig_comm_timeout = config.COMMTIMEOUT config.POLLTIMEOUT = 0.5 config.COMMTIMEOUT = 0.5 self.socket_server = SocketServer_Multiplex def teardown_method(self): config.POLLTIMEOUT = self.orig_poll_timeout config.COMMTIMEOUT = self.orig_comm_timeout class ServerThread(threading.Thread): def __init__(self, server, daemon): threading.Thread.__init__(self) self.serv = server() self.serv.init(daemon(), "localhost", 0) self.locationStr = self.serv.locationStr self.stop_loop = threading.Event() def run(self): self.serv.loop(loopCondition=lambda: not self.stop_loop.is_set()) self.serv.close() def testConnectCrash(self): serv_thread = TestServerDOS_multiplex.ServerThread(self.socket_server, ServerCallback_BrokenHandshake) serv_thread.start() time.sleep(0.2) assert serv_thread.is_alive(), "server thread failed to start" threadpool = getattr(serv_thread.serv, "pool", None) if threadpool: assert len(threadpool.idle) == 1 assert len(threadpool.busy) == 0 try: host, port = serv_thread.locationStr.split(':') port = int(port) try: # first connection attempt (will fail because server daemon _handshake crashes) csock = socketutil.create_socket(connect=(host, port)) conn = socketutil.SocketConnection(csock, "uri") protocol.recv_stub(conn, [protocol.MSG_CONNECTOK]) except errors.ConnectionClosedError: pass conn.close() time.sleep(0.1) if threadpool: assert len(threadpool.idle) == 1 assert len(threadpool.busy) == 0 try: # second connection attempt, should still work (i.e. server should still be running) csock = socketutil.create_socket(connect=(host, port)) conn = socketutil.SocketConnection(csock, "uri") protocol.recv_stub(conn, [protocol.MSG_CONNECTOK]) except errors.ConnectionClosedError: pass finally: if conn: conn.close() serv_thread.stop_loop.set() serv_thread.join() def testInvalidMessageCrash(self): serv_thread = TestServerDOS_multiplex.ServerThread(self.socket_server, ServerTestDaemon) serv_thread.start() time.sleep(0.2) assert serv_thread.is_alive(), "server thread failed to start" threadpool = getattr(serv_thread.serv, "pool", None) if threadpool: assert len(threadpool.idle) == 1 assert len(threadpool.busy) == 0 def connect(host, port): # connect to the server csock = socketutil.create_socket(connect=(host, port)) conn = socketutil.SocketConnection(csock, "uri") # send the handshake/connect data ser = serializers.serializers_by_id[serializers.MarshalSerializer.serializer_id] data = ser.dumps({"handshake": "hello", "object": core.DAEMON_NAME}) msg = protocol.SendingMessage(protocol.MSG_CONNECT, 0, 0, serializers.MarshalSerializer.serializer_id, data) conn.send(msg.data) # get the handshake/connect response protocol.recv_stub(conn, [protocol.MSG_CONNECTOK]) return conn conn = None try: host, port = serv_thread.locationStr.split(':') port = int(port) conn = connect(host, port) # invoke something, but screw up the message (in this case, mess with the protocol version) orig_protocol_version = protocol.PROTOCOL_VERSION protocol.PROTOCOL_VERSION = 9999 msgbytes = protocol.SendingMessage(protocol.MSG_PING, 42, 0, 0, b"something").data protocol.PROTOCOL_VERSION = orig_protocol_version conn.send(msgbytes) # this should cause an error in the server because of invalid msg try: msg = protocol.recv_stub(conn, [protocol.MSG_RESULT]) data = msg.data.decode("ascii", errors="ignore") # convert raw message to string to check some stuff assert "Traceback" in data assert "ProtocolError" in data assert "version" in data except errors.ConnectionClosedError: # invalid message can cause the connection to be closed, this is fine pass # invoke something again, this should still work (server must still be running, but our client connection was terminated) conn.close() time.sleep(0.1) if threadpool: assert len(threadpool.idle) == 1 assert len(threadpool.busy) == 0 conn = connect(host, port) msg = protocol.SendingMessage(protocol.MSG_PING, 42, 999, 0, b"something") # a valid message this time conn.send(msg.data) msg = protocol.recv_stub(conn, [protocol.MSG_PING]) assert msg.type == protocol.MSG_PING assert msg.seq == 999 assert msg.data == b"pong" finally: if conn: conn.close() serv_thread.stop_loop.set() serv_thread.join() class TestServerDOS_threading(TestServerDOS_multiplex): def setup_method(self): super().setup_method() self.socket_server = SocketServer_Threadpool self.orig_numthreads = config.THREADPOOL_SIZE self.orig_numthreads_min = config.THREADPOOL_SIZE_MIN config.THREADPOOL_SIZE = 1 config.THREADPOOL_SIZE_MIN = 1 def teardown_method(self): config.THREADPOOL_SIZE = self.orig_numthreads config.THREADPOOL_SIZE_MIN = self.orig_numthreads_min class TestSSL: def testContextAndSock(self): cert_dir = "../../certs" if not os.path.isdir(cert_dir): cert_dir = "../certs" if not os.path.isdir(cert_dir): cert_dir = "./certs" if not os.path.isdir(cert_dir): raise IOError("cannot locate test certs directory") try: config.SSL = True config.SSL_REQUIRECLIENTCERT = True server_ctx = socketutil.get_ssl_context(cert_dir+"/server_cert.pem", cert_dir+"/server_key.pem") client_ctx = socketutil.get_ssl_context(clientcert=cert_dir+"/client_cert.pem", clientkey=cert_dir+"/client_key.pem") assert server_ctx.verify_mode == ssl.CERT_REQUIRED assert client_ctx.verify_mode == ssl.CERT_REQUIRED assert client_ctx.check_hostname sock = socketutil.create_socket(sslContext=server_ctx) try: assert hasattr(sock, "getpeercert") finally: sock.close() finally: config.SSL = False Pyro5-5.15/tests/test_threadpool.py000066400000000000000000000103121451404116400173730ustar00rootroot00000000000000""" Tests for the thread pool. Pyro - Python Remote Objects. Copyright by Irmen de Jong (irmen@razorvine.net). """ import time import random import pytest from Pyro5 import socketutil, server from Pyro5.svr_threads import Pool, PoolError, NoFreeWorkersError, SocketServer_Threadpool from Pyro5 import config JOB_TIME = 0.2 class Job(object): def __init__(self, name="unnamed"): self.name = name def __call__(self): time.sleep(JOB_TIME - random.random() / 10.0) class SlowJob(object): def __init__(self, name="unnamed"): self.name = name def __call__(self): time.sleep(5*JOB_TIME - random.random() / 10.0) class TestThreadPool: def setup_method(self): config.THREADPOOL_SIZE_MIN = 2 config.THREADPOOL_SIZE = 4 def teardown_method(self): config.reset() def testCreate(self): with Pool() as jq: _ = repr(jq) assert jq.closed def testSingle(self): with Pool() as p: job = Job() p.process(job) time.sleep(0.02) # let it pick up the job assert len(p.busy) == 1 def testAllBusy(self): try: config.COMMTIMEOUT = 0.2 with Pool() as p: for i in range(config.THREADPOOL_SIZE): p.process(SlowJob(str(i+1))) # putting one more than the number of workers should raise an error: with pytest.raises(NoFreeWorkersError): p.process(SlowJob("toomuch")) finally: config.COMMTIMEOUT = 0.0 def testClose(self): with Pool() as p: for i in range(config.THREADPOOL_SIZE): p.process(Job(str(i + 1))) with pytest.raises(PoolError): p.process(Job("1")) # must not allow new jobs after closing assert len(p.busy) == 0 assert len(p.idle) == 0 def testScaling(self): with Pool() as p: for i in range(config.THREADPOOL_SIZE_MIN-1): p.process(Job("x")) assert len(p.idle) == 1 assert len(p.busy) == config.THREADPOOL_SIZE_MIN-1 p.process(Job("x")) assert len(p.idle) == 0 assert len(p.busy) == config.THREADPOOL_SIZE_MIN # grow until no more free workers while True: try: p.process(Job("x")) except NoFreeWorkersError: break assert len(p.idle) == 0 assert len(p.busy) == config.THREADPOOL_SIZE # wait till jobs are done and check ending situation time.sleep(JOB_TIME*1.5) assert len(p.busy) == 0 assert len(p.idle) == config.THREADPOOL_SIZE_MIN class ServerCallback(server.Daemon): def __init__(self): super().__init__() self.received_denied_reasons = [] def _handshake(self, connection, denied_reason=None): self.received_denied_reasons.append(denied_reason) # store the denied reason return True def handleRequest(self, connection): time.sleep(0.05) def _housekeeping(self): pass class TestThreadPoolServer: def setup_method(self): config.THREADPOOL_SIZE_MIN = 1 config.THREADPOOL_SIZE = 1 config.POLLTIMEOUT = 0.5 config.COMMTIMEOUT = 0.5 def teardown_method(self): config.reset() def testServerPoolFull(self): port = socketutil.find_probably_unused_port() serv = SocketServer_Threadpool() daemon = ServerCallback() serv.init(daemon, "localhost", port) serversock = serv.sock.getsockname() csock1 = socketutil.create_socket(connect=serversock) csock2 = socketutil.create_socket(connect=serversock) try: serv.events([serv.sock]) time.sleep(0.2) assert daemon.received_denied_reasons == [None] serv.events([serv.sock]) time.sleep(0.2) assert len(daemon.received_denied_reasons) == 2 assert "no free workers, increase server threadpool size" in daemon.received_denied_reasons finally: csock1.close() csock2.close() serv.shutdown() Pyro5-5.15/tox.ini000066400000000000000000000004271451404116400140000ustar00rootroot00000000000000[tox] envlist=py38,py39,py310,py311,pypy3 [testenv] deps= -rtest-requirements.txt pytest setenv= PYTHONPATH={toxinidir} commands=python -E -Wall -tt -bb -m pytest tests [testenv:pypy3] # pypy3 doesn't have the -tt option commands=pypy3 -E -Wall -bb -m pytest tests