Package: aioreactive
aioreactive - ReactiveX for asyncio using async and awaitNEWS: Project rebooted Nov. 2020. Rebuilt usingExpression.Aioreactive isRxPYfor asyncio.
It's an asynchronous and reactive Python library for asyncio using async
and await. Aioreactive is built on theExpressionfunctional library
and, integrates naturally with the Python language.aioreactive is the unification of RxPY and reactive programming with
asyncio using async and await.The design goals for aioreactive:Python 3.11+ only. We have a hard dependencyExpression v5.All operators and tools are implemented as plain old functions.Everything isasync. Sending values is async, subscribing to
observables is async. Disposing subscriptions is async.One scheduler to rule them all. Everything runs on the asyncio base
event-loop.No multi-threading. Only async and await with concurrency using
asyncio. Threads are hard, and in many cases it doesn’t make sense to
use multi-threading in Python applications. If you need to use threads
you may wrap them withconcurrent.futuresand compose them into the chain withflat_map()or similar. Seeparallel.pyfor an example.Simple, clean and use few abstractions. Try to align with the
itertools package, and reuse as much from the Python standard library
as possible.Support type hints and static type checking usingPylance.Implicit synchronous back-pressure ™. Producers of events will
simply be awaited until the event can be processed by the down-stream
consumers.AsyncObservable and AsyncObserverWith aioreactive you subscribe observers to observables, and the key
abstractions of aioreactive can be seen in this single line of code:subscription=awaitobservable.subscribe_async(observer)The difference from RxPY can be seen with theawaitexpression.
Aioreactive is built around the asynchronous duals, or opposites of the
AsyncIterable and AsyncIterator abstract base classes. These async
classes are called AsyncObservable and AsyncObserver.AsyncObservable is a producer of events. It may be seen as the dual or
opposite of AsyncIterable and provides a single setter method calledsubscribe_async()that is the dual of the__aiter__()getter method:fromabcimportABC,abstractmethodclassAsyncObservable(ABC):@abstractmethodasyncdefsubscribe_async(self,observer):returnNotImplementedAsyncObserver is a consumer of events and is modeled after the
so-calledconsumer interface, the
enhanced generator interface inPEP-342and async
generators inPEP-525. It
is the dual of the AsyncIterator__anext__()method, and expands to
three async methodsasend(), that is the opposite of__anext__(),athrow()that is the opposite of anraise Exception()andaclose()that is the opposite ofraise StopAsyncIteration:fromabcimportABC,abstractmethodclassAsyncObserver(ABC):@abstractmethodasyncdefasend(self,value):returnNotImplemented@abstractmethodasyncdefathrow(self,error):returnNotImplemented@abstractmethodasyncdefaclose(self):returnNotImplementedSubscribing to observablesAn observable becomes hot and starts streaming items by using thesubscribe_async()method. Thesubscribe_async()method takes an
observable and returns a disposable subscription. So thesubscribe_async()method is used to attach a observer to the
observable.asyncdefasend(value):print(value)disposable=awaitsubscribe_async(source,AsyncAnonymousObserver(asend))AsyncAnonymousObserveris an anonymous observer that constructs anAsyncObserverout of plain async functions, so you don't have to
implement a new named observer every time you need one.The subscription returned bysubscribe_async()is disposable, so to
unsubscribe you need to await thedispose_async()method on the
subscription.awaitsubscription.dispose_async()Asynchronous iterationEven more interesting, withto_async_iterableyou can flip around fromAsyncObservableto anAsyncIterableand useasync-forto consume
the stream of events.obv=AsyncIteratorObserver()subscription=subscribe_async(source,obv)asyncforxinobv:print(x)They effectively transform us from an async push model to an async pull
model, and lets us use the awesome new language features such asasync forandasync-with. We do this without any queueing, as a push by theAsyncObservablewill await the pull by the `AsyncIterator.  This
effectively applies so-called "back-pressure" up the subscription as the
producer will await the iterator to pick up the item send.The for-loop may be wrapped with async-with to control the lifetime of
the subscription:importaioreactiveasrxxs=rx.from_iterable([1,2,3])result=[]obv=rx.AsyncIteratorObserver()asyncwithawaitxs.subscribe_async(obv)assubscription:asyncforxinobv:result.append(x)assertresult==[1,2,3]Async streamsAn async stream is both an async observer and an async observable.
Aioreactive lets you create streams explicitly.importaioreactiveasrxstream=AsyncSubject()# Alias for AsyncMultiStreamsink=rx.AsyncAnonymousObserver()awaitstream.subscribe_async(sink)awaitstream.asend(42)You can create streams directly fromAsyncMultiStreamorAsyncSingleStream.AsyncMultiStreamsupports multiple observers, and
is hot in the sense that it will drop any event that is sent if there
are currently no observers attached.AsyncSingleStreamon the other
hand supports a single observer, and is cold in the sense that it will
await any producer until there is an observer attached.OperatorsThe Rx operators in aioreactive are all plain old functions. You can
apply them to an observable and compose it into a transformed, filtered,
aggregated or combined observable. This transformed observable can be
streamed into an observer.Observable -> Operator -> Operator -> Operator -> ObserverAioreactive contains many of the same operators as you know from RxPY.
Our goal is not to implement them all, but to provide the most essential
ones.concat-- Concatenates two or more observables.choose-- Filters and/or transforms the observable.choose_asnc-- Asynchronously filters and/or transforms the observable.debounce-- Throttles an observable.delay-- delays the items within an observable.distinct_until_changed-- an observable with continuously distinct values.filter-- filters an observable.filteri-- filters an observable with index.flat_map-- transforms an observable into a stream of observables and flattens the resulting observable.flat_map_latest-- transforms an observable into a stream of
observables and flattens the resulting observable by producing values
from the latest observable.from_iterable-- Create an observable from an (async) iterable.subscribe-- Subscribes an observer to an observable. Returns a subscription.map-- transforms an observable.mapi-- transforms an observable with index.map_async-- transforms an observable asynchronously.mapi_async-- transforms an observable asynchronously with index.merge_inner-- Merges an observable of observables.merge-- Merge one observable with another observable.merge_seq-- Merge a sequence of observables.run-- Awaits the future returned by subscribe. Returns when the subscription closes.slice-- Slices an observable.skip-- Skip items from the start of the observable stream.skip_last-- Skip items from the end of the observable stream.starfilter-- Filters an observable with a predicate and spreads the arguments.starmap-- Transforms and async observable and spreads the arguments to the mapper.switch_latest-- Merges the latest stream in an observable of streams.take-- Take a number of items from the start of the observable stream.take_last-- Take a number of items from the end of the observable stream.unit-- Converts a value or future to an observable.with_latest_from-- Combines two observables into one.Functional or object-oriented, reactive or interactiveWith aioreactive you can choose to program functionally with plain old
functions, or object-oriented with classes and methods. Aioreactive
supports both method chaining or forward pipe programming styles.Pipe forward programming styleAsyncObservablemay compose operators using forward pipelining with
thepipeoperator provided by the amazingExpressionlibrary. This works
by having the operators partially applied with their arguments before
being given the source stream as the last curried argument.ys=pipe(xs,filter(predicate),map(mapper),flat_map(request))Longer pipelines may break lines as for binary operators:importaioreactveasrxasyncdefmain():stream=rx.AsyncSubject()obv=rx.AsyncIteratorObserver()xs=pipe(stream,rx.map(lambdax:x["term"]),rx.filter(lambdatext:len(text)>2),rx.debounce(0.75),rx.distinct_until_changed(),rx.map(search_wikipedia),rx.switch_latest(),)asyncwithxs.subscribe_async(obv)asysasyncforvalueinobv:print(value)AsyncObservable also supports slicing using the Python slice notation.@pytest.mark.asyncioasyncdeftest_slice_special():xs=rx.from_iterable([1,2,3,4,5])values=[]asyncdefasend(value):values.append(value)ys=xs[1:-1]result=awaitrun(ys,AsyncAnonymousObserver(asend))assertresult==4assertvalues==[2,3,4]Fluent and chained programming styleAn alternative to pipelining is to use the classic and fluent method
chaining as we know fromReactiveX.AnAsyncObservablecreated from class methods such asAsyncRx.from_iterable()returns aAsyncChainedObservable.
where we may use methods such as.filter()and.map().fromaioreactiveimportAsyncRx@pytest.mark.asyncioasyncdeftest_observable_simple_pipe():xs=AsyncRx.from_iterable([1,2,3])result=[]asyncdefmapper(value):awaitasyncio.sleep(0.1)returnvalue*10asyncdefpredicate(value):awaitasyncio.sleep(0.1)returnvalue>1ys=xs.filter(predicate).map(mapper)asyncdefon_next(value):result.append(value)subscription=awaitys.subscribe_async(AsyncAnonymousObserver(on_next))awaitsubsubscriptionassertresult==[20,30]Virtual time testingAioreactive also provides a virtual time event loop
(VirtualTimeEventLoop) that enables you to write asyncio unit-tests
that run in virtual time. Virtual time means that time is emulated, so
tests run as quickly as possible even if they sleep or awaits long-lived
operations. A test using virtual time still gives the same result as it
would have done if it had been run in real-time.For example the following test still gives the correct result even if it
takes 0 seconds to run:@pytest.fixture()defevent_loop():loop=VirtualTimeEventLoop()yieldlooploop.close()@pytest.mark.asyncioasyncdeftest_call_later():result=[]defaction(value):result.append(value)loop=asyncio.get_event_loop()loop.call_later(10,partial(action,1))loop.call_later(1,partial(action,2))loop.call_later(5,partial(action,3))awaitasyncio.sleep(10)assertresult==[2,3,1]The aioreactive testing module provides a testAsyncSubjectthat may
delay sending values, and a testAsyncTestObserverthat records all
events. These two classes helps you with testing in virtual time.@pytest.fixture()defevent_loop():loop=VirtualTimeEventLoop()yieldlooploop.close()@pytest.mark.asyncioasyncdeftest_delay_done():xs=AsyncSubject()# Test streamasyncdefmapper(value):returnvalue*10ys=delay(0.5,xs)lis=AsyncTestObserver()# Test AsyncAnonymousObserversub=awaitsubscribe_async(ys,lis)awaitxs.asend_later(0,10)awaitxs.asend_later(1,20)awaitxs.aclose_later(1)awaitsubassertlis.values==[(0.5,OnNext(10)),(1.5,OnNext(20)),(2.5,OnCompleted)]Why not use AsyncIterable for everything?AsyncIterableandAsyncObservableare closely related (in fact they
are duals).AsyncIterableis an async iterable (pull) world, whileAsyncObservableis an async reactive (push) based world. There are
many operations such asmap()andfilter()that may be simpler to
implement usingAsyncIterable, but once we start to include time, thenAsyncObservablereally starts to shine. Operators such asdelay()makes much more sense forAsyncObservablethan forAsyncIterable.However, aioreactive makes it easy for you to flip-around to async
iterable just before you need to consume the stream, thus giving you the
best of both worlds.Will aioreactive replace RxPY?Aioreactive will not replaceRxPY.
RxPY is an implementation ofObservable. Aioreactive is an
implementation ofAsyncObservable.Rx and RxPY has hundreds of different query operators, and we currently
have no plans to implementing all of them for aioreactive.Many ideas from aioreactive have already been ported back into "classic" RxPY.ReferencesAioreactive was inspired by:AsyncRx- Aioreactive is a direct port of AsyncRx from F#.Expression- Functional programming for Python.Is it really Pythonic to continue using LINQ operators instead of plain old functions?Reactive Extensions (Rx)andRxPY.Dart StreamsUnderscore.js.itertoolsandfunctools.dbrattli/OSlashkriskowal/q.LicenseThe MIT License (MIT)
Copyright (c) 2016 Børge Lanes, Dag Brattli.