""" Helpers and wrappers for common RPyC tasks """ import time from rpyc.lib import spawn from rpyc.lib.colls import WeakValueDict from rpyc.lib.compat import callable from rpyc.core.consts import HANDLE_BUFFITER, HANDLE_CALL from rpyc.core.netref import syncreq, asyncreq def buffiter(obj, chunk=10, max_chunk=1000, factor=2): """Buffered iterator - reads the remote iterator in chunks starting with *chunk*, multiplying the chunk size by *factor* every time, as an exponential-backoff, up to a chunk of *max_chunk* size. ``buffiter`` is very useful for tight loops, where you fetch an element from the other side with every iterator. Instead of being limited by the network's latency after every iteration, ``buffiter`` fetches a "chunk" of elements every time, reducing the amount of network I/Os. :param obj: An iterable object (supports ``iter()``) :param chunk: the initial chunk size :param max_chunk: the maximal chunk size :param factor: the factor by which to multiply the chunk size after every iterator (up to *max_chunk*). Must be >= 1. :returns: an iterator Example:: cursor = db.get_cursor() for id, name, dob in buffiter(cursor.select("Id", "Name", "DoB")): print id, name, dob """ if factor < 1: raise ValueError(f"factor must be >= 1, got {factor!r}") it = iter(obj) count = chunk while True: items = syncreq(it, HANDLE_BUFFITER, count) count = min(count * factor, max_chunk) if not items: break for elem in items: yield elem def restricted(obj, attrs, wattrs=None): """Returns a 'restricted' version of an object, i.e., allowing access only to a subset of its attributes. This is useful when returning a "broad" or "dangerous" object, where you don't want the other party to have access to all of its attributes. .. versionadded:: 3.2 :param obj: any object :param attrs: the set of attributes exposed for reading (``getattr``) or writing (``setattr``). The same set will serve both for reading and writing, unless wattrs is explicitly given. :param wattrs: the set of attributes exposed for writing (``setattr``). If ``None``, ``wattrs`` will default to ``attrs``. To disable setting attributes completely, set to an empty tuple ``()``. :returns: a restricted view of the object Example:: class MyService(rpyc.Service): def exposed_open(self, filename): f = open(filename, "r") return rpyc.restricted(f, {"read", "close"}) # disallow access to `seek` or `write` """ if wattrs is None: wattrs = attrs class Restricted(object): def _rpyc_getattr(self, name): if name not in attrs: raise AttributeError(name) return getattr(obj, name) __getattr__ = _rpyc_getattr def _rpyc_setattr(self, name, value): if name not in wattrs: raise AttributeError(name) setattr(obj, name, value) __setattr__ = _rpyc_setattr return Restricted() class _Async(object): """Creates an async proxy wrapper over an existing proxy. Async proxies are cached. Invoking an async proxy will return an AsyncResult instead of blocking""" __slots__ = ("proxy", "__weakref__") def __init__(self, proxy): self.proxy = proxy def __call__(self, *args, **kwargs): return asyncreq(self.proxy, HANDLE_CALL, args, tuple(kwargs.items())) def __repr__(self): return f"async_({self.proxy!r})" _async_proxies_cache = WeakValueDict() def async_(proxy): """ Returns an asynchronous "version" of the given proxy. Invoking the returned proxy will not block; instead it will return an :class:`rpyc.core.async_.AsyncResult` object that you can test for completion :param proxy: any **callable** RPyC proxy :returns: the proxy, wrapped by an asynchronous wrapper Example:: async_sleep = rpyc.async_(conn.modules.time.sleep) res = async_sleep(5) .. _async_note: .. note:: In order to avoid overloading the GC, the returned asynchronous wrapper is cached as a weak reference. Therefore, do not use:: rpyc.async_(foo)(5) Always store the returned asynchronous wrapper in a variable, e.g. :: a_foo = rpyc.async_(foo) a_foo(5) .. note:: Furthermore, async requests provide **no guarantee on execution order**. In particular, multiple subsequent async requests may be executed in reverse order. """ pid = id(proxy) if pid in _async_proxies_cache: return _async_proxies_cache[pid] if not hasattr(proxy, "____conn__") or not hasattr(proxy, "____id_pack__"): raise TypeError(f"'proxy' must be a Netref: {proxy!r}") if not callable(proxy): raise TypeError(f"'proxy' must be callable: {proxy!r}") caller = _Async(proxy) _async_proxies_cache[id(caller)] = _async_proxies_cache[pid] = caller return caller async_.__doc__ = _Async.__doc__ globals()['async'] = async_ # backward compatibility alias class timed(object): """Creates a timed asynchronous proxy. Invoking the timed proxy will run in the background and will raise an :class:`rpyc.core.async_.AsyncResultTimeout` exception if the computation does not terminate within the given time frame :param proxy: any **callable** RPyC proxy :param timeout: the maximal number of seconds to allow the operation to run :returns: a ``timed`` wrapped proxy Example:: t_sleep = rpyc.timed(conn.modules.time.sleep, 6) # allow up to 6 seconds t_sleep(4) # okay t_sleep(8) # will time out and raise AsyncResultTimeout """ __slots__ = ("__weakref__", "proxy", "timeout") def __init__(self, proxy, timeout): self.proxy = async_(proxy) self.timeout = timeout def __call__(self, *args, **kwargs): res = self.proxy(*args, **kwargs) res.set_expiry(self.timeout) return res def __repr__(self): return f"timed({self.proxy.proxy!r}, {self.timeout!r})" class BgServingThread(object): """Runs an RPyC server in the background to serve all requests and replies that arrive on the given RPyC connection. The thread is started upon the the instantiation of the ``BgServingThread`` object; you can use the :meth:`stop` method to stop the server thread. CAVEAT: RPyC defaults to bind_threads as False. So, there is no guarantee that the background thread will serve the request. See issue #522 for an example of this behavior. As the bind_threads feature matures, we may change the default to to True in the future. Example:: conn = rpyc.connect(...) bg_server = BgServingThread(conn) ... bg_server.stop() .. note:: For a more detailed explanation of asynchronous operation and the role of the ``BgServingThread``, see :ref:`tut5` """ # these numbers are magical... SERVE_INTERVAL = 0.0 SLEEP_INTERVAL = 0.1 def __init__(self, conn, callback=None, serve_interval=SERVE_INTERVAL, sleep_interval=SLEEP_INTERVAL): self._conn = conn self._active = True self._callback = callback self._serve_interval = serve_interval self._sleep_interval = sleep_interval self._thread = spawn(self._bg_server) def __del__(self): if self._active: self.stop() def _bg_server(self): try: while self._active: self._conn.serve(self._serve_interval) time.sleep(self._sleep_interval) # to reduce contention except Exception: if self._active: self._active = False if self._callback is None: raise self._callback() def stop(self): """stop the server thread. once stopped, it cannot be resumed. you will have to create a new BgServingThread object later.""" assert self._active self._active = False self._thread.join() self._conn = None def classpartial(*args, **kwargs): """Bind arguments to a class's __init__.""" cls, args = args[0], args[1:] class Partial(cls): __doc__ = cls.__doc__ def __new__(self): return cls(*args, **kwargs) Partial.__name__ = cls.__name__ return Partial