├── reactivex ├── py.typed ├── operators │ ├── connectable │ │ ├── __init__.py │ │ └── _refcount.py │ ├── _onerrorresumenext.py │ ├── _concat.py │ ├── _startswith.py │ ├── _isempty.py │ ├── _asobservable.py │ ├── _dowhile.py │ ├── _publishvalue.py │ ├── _forkjoin.py │ ├── _sum.py │ ├── _ignoreelements.py │ ├── _delaysubscription.py │ ├── _withlatestfrom.py │ ├── _maxby.py │ ├── _all.py │ ├── _combinelatest.py │ ├── _count.py │ ├── _bufferwithtimeorcount.py │ ├── _dematerialize.py │ ├── _whiledo.py │ ├── _repeat.py │ ├── _retry.py │ ├── _max.py │ ├── _bufferwithtime.py │ ├── _toset.py │ ├── _contains.py │ ├── _toiterable.py │ ├── _pluck.py │ ├── _observeon.py │ ├── _materialize.py │ ├── _finallyaction.py │ ├── _timestamp.py │ ├── _min.py │ ├── _last.py │ ├── _skip.py │ ├── _first.py │ ├── _single.py │ ├── _defaultifempty.py │ ├── _takeuntil.py │ ├── _take.py │ ├── _groupby.py │ ├── _takelast.py │ ├── _timeinterval.py │ ├── _skiplast.py │ ├── _pairwise.py │ ├── _some.py │ ├── _takelastbuffer.py │ ├── _reduce.py │ ├── _takeuntilwithtime.py │ ├── _average.py │ ├── _subscribeon.py │ ├── _find.py │ ├── _scan.py │ └── _takewithtime.py ├── _version.py ├── internal │ ├── constants.py │ ├── basic.py │ ├── concurrency.py │ ├── __init__.py │ ├── exceptions.py │ ├── priorityqueue.py │ ├── curry.py │ └── utils.py ├── observable │ ├── __init__.py │ ├── interval.py │ ├── merge.py │ ├── startasync.py │ ├── empty.py │ ├── never.py │ ├── amb.py │ ├── throw.py │ ├── repeat.py │ ├── case.py │ ├── mixins │ │ └── __init__.py │ ├── start.py │ ├── groupedobservable.py │ ├── defer.py │ ├── fromfuture.py │ ├── generate.py │ ├── fromiterable.py │ ├── ifthen.py │ ├── using.py │ └── toasync.py ├── subject │ ├── __init__.py │ └── innersubscription.py ├── observer │ ├── __init__.py │ ├── observeonobserver.py │ └── autodetachobserver.py ├── abc │ ├── startable.py │ ├── __init__.py │ ├── disposable.py │ ├── observer.py │ ├── periodicscheduler.py │ └── observable.py ├── scheduler │ ├── mainloop │ │ └── __init__.py │ ├── eventloop │ │ └── __init__.py │ ├── historicalscheduler.py │ ├── __init__.py │ ├── threadpoolscheduler.py │ ├── scheduleditem.py │ ├── trampoline.py │ └── periodicscheduler.py ├── testing │ ├── __init__.py │ ├── mockdisposable.py │ ├── subscription.py │ ├── mockobserver.py │ └── recorded.py └── disposable │ ├── booleandisposable.py │ ├── __init__.py │ ├── disposable.py │ ├── scheduleddisposable.py │ ├── multipleassignmentdisposable.py │ ├── singleassignmentdisposable.py │ └── serialdisposable.py ├── tests ├── __init__.py ├── test_core │ └── __init__.py ├── test_observable │ ├── __init__.py │ ├── test_blocking │ │ ├── __init__.py │ │ └── test_blocking.py │ ├── test_never.py │ ├── test_flatmap_async.py │ ├── test_empty.py │ ├── test_throw.py │ ├── test_of.py │ ├── test_error_handling_fluent.py │ ├── test_fromcallback.py │ └── test_forin.py ├── test_scheduler │ ├── __init__.py │ ├── test_eventloop │ │ └── __init__.py │ ├── test_mainloop │ │ └── __init__.py │ └── test_scheduler.py ├── test_subject │ └── __init__.py ├── test_testing │ └── __init__.py ├── test_disposables │ └── __init__.py ├── test_version.py └── test_integration │ ├── test_concat_repeat.py │ └── test_group_reduce.py ├── examples ├── chess │ ├── chess_king_black.png │ ├── chess_king_white.png │ ├── chess_pawn_black.png │ ├── chess_pawn_white.png │ ├── chess_queen_black.png │ ├── chess_queen_white.png │ ├── chess_rook_black.png │ ├── chess_rook_white.png │ ├── chess_bishop_black.png │ ├── chess_bishop_white.png │ ├── chess_knight_black.png │ └── chess_knight_white.png ├── asyncio │ └── await.py ├── konamicode │ └── konamicode.js ├── marbles │ ├── frommarbles_merge.py │ ├── frommarbles_error.py │ ├── tomarbles.py │ ├── hot_datetime.py │ ├── frommarbles_lookup.py │ ├── frommarbles_flatmap.py │ ├── testing_debounce.py │ └── testing_flatmap.py ├── parallel │ └── timer.py ├── errors │ └── failing.py ├── autocomplete │ ├── autocomplete.js │ └── index.html └── timeflies │ ├── timeflies_tkinter.py │ ├── timeflies_wx.py │ ├── timeflies_qt.py │ └── timeflies_gtk.py ├── docs ├── reference_typing.rst ├── reference_operators.rst ├── reference_observable.rst ├── requirements.txt ├── reference_observable_factory.rst ├── reference.rst ├── reference_subject.rst ├── .rstcheck.cfg ├── installation.rst ├── contributing.rst ├── Makefile ├── index.rst ├── reference_scheduler.rst ├── license.rst ├── rationale.rst └── additional_reading.rst ├── notebooks └── reactivex.io │ └── assets │ ├── img │ ├── threading.png │ └── publishConnect.png │ └── js │ └── ipython_notebook_toc.js ├── .pylintrc ├── authors.txt ├── .coveragerc ├── setup.cfg ├── .gitattributes ├── .pre-commit-config.yaml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── workflows │ ├── python-publish.yml │ └── python-package.yml └── lock.yml ├── .readthedocs.yaml ├── .gitignore └── LICENSE /reactivex/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_observable/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_scheduler/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_subject/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_testing/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_disposables/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /reactivex/operators/connectable/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_observable/test_blocking/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_scheduler/test_eventloop/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_scheduler/test_mainloop/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /reactivex/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.0.0" # NOTE: version will be written by publish script 2 | -------------------------------------------------------------------------------- /examples/chess/chess_king_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveX/RxPY/HEAD/examples/chess/chess_king_black.png -------------------------------------------------------------------------------- /examples/chess/chess_king_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveX/RxPY/HEAD/examples/chess/chess_king_white.png -------------------------------------------------------------------------------- /examples/chess/chess_pawn_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveX/RxPY/HEAD/examples/chess/chess_pawn_black.png -------------------------------------------------------------------------------- /examples/chess/chess_pawn_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveX/RxPY/HEAD/examples/chess/chess_pawn_white.png -------------------------------------------------------------------------------- /examples/chess/chess_queen_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveX/RxPY/HEAD/examples/chess/chess_queen_black.png -------------------------------------------------------------------------------- /examples/chess/chess_queen_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveX/RxPY/HEAD/examples/chess/chess_queen_white.png -------------------------------------------------------------------------------- /examples/chess/chess_rook_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveX/RxPY/HEAD/examples/chess/chess_rook_black.png -------------------------------------------------------------------------------- /examples/chess/chess_rook_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveX/RxPY/HEAD/examples/chess/chess_rook_white.png -------------------------------------------------------------------------------- /examples/chess/chess_bishop_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveX/RxPY/HEAD/examples/chess/chess_bishop_black.png -------------------------------------------------------------------------------- /examples/chess/chess_bishop_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveX/RxPY/HEAD/examples/chess/chess_bishop_white.png -------------------------------------------------------------------------------- /examples/chess/chess_knight_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveX/RxPY/HEAD/examples/chess/chess_knight_black.png -------------------------------------------------------------------------------- /examples/chess/chess_knight_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveX/RxPY/HEAD/examples/chess/chess_knight_white.png -------------------------------------------------------------------------------- /docs/reference_typing.rst: -------------------------------------------------------------------------------- 1 | .. _reference_typing: 2 | 3 | Typing 4 | ======= 5 | 6 | .. automodule:: reactivex.typing 7 | :members: 8 | -------------------------------------------------------------------------------- /notebooks/reactivex.io/assets/img/threading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveX/RxPY/HEAD/notebooks/reactivex.io/assets/img/threading.png -------------------------------------------------------------------------------- /docs/reference_operators.rst: -------------------------------------------------------------------------------- 1 | .. _reference_operators: 2 | 3 | Operators 4 | ========= 5 | 6 | .. automodule:: reactivex.operators 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/reference_observable.rst: -------------------------------------------------------------------------------- 1 | .. _reference_observable: 2 | 3 | Observable 4 | =========== 5 | 6 | .. autoclass:: reactivex.Observable 7 | :members: 8 | -------------------------------------------------------------------------------- /notebooks/reactivex.io/assets/img/publishConnect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveX/RxPY/HEAD/notebooks/reactivex.io/assets/img/publishConnect.png -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [BASIC] 2 | 3 | good-names=n,x,xs,ys,zs,ex,obv,obs,T_in,T_out,do,_ 4 | 5 | [MESSAGES CONTROL] 6 | 7 | disable=missing-docstring,unused-argument,invalid-name -------------------------------------------------------------------------------- /authors.txt: -------------------------------------------------------------------------------- 1 | ReactiveX for Python maintainers 2 | 3 | Dag Brattli @dbrattli 4 | Erik Kemperman, @erikkemperman 5 | Jérémie Fache, @jcafhe 6 | Romain Picard, @MainRo 7 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx>=2.0 2 | sphinx-autodoc-typehints>=1.10.3 3 | guzzle_sphinx_theme>=0.7.11 4 | sphinxcontrib_dooble>=1.0 5 | tomli>=2.0 6 | dunamai>=1.9.0 -------------------------------------------------------------------------------- /docs/reference_observable_factory.rst: -------------------------------------------------------------------------------- 1 | .. _reference_observable_factory: 2 | 3 | Observable Factory 4 | ===================== 5 | 6 | .. automodule:: reactivex 7 | :members: 8 | -------------------------------------------------------------------------------- /reactivex/internal/constants.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta, timezone 2 | 3 | DELTA_ZERO = timedelta(0) 4 | UTC_ZERO = datetime.fromtimestamp(0, tz=timezone.utc) 5 | -------------------------------------------------------------------------------- /tests/test_version.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from reactivex import __version__ 4 | 5 | 6 | class VersionTest(unittest.TestCase): 7 | def test_version(self): 8 | assert __version__ 9 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | omit = 3 | */python?.?/* 4 | */site-packages/nose/* 5 | exclude_lines = 6 | pragma: no cover 7 | return NotImplemented 8 | raise NotImplementedError 9 | \.\.\. 10 | [xml] 11 | output = coverage.xml 12 | -------------------------------------------------------------------------------- /reactivex/observable/__init__.py: -------------------------------------------------------------------------------- 1 | from .connectableobservable import ConnectableObservable 2 | from .groupedobservable import GroupedObservable 3 | from .observable import Observable 4 | 5 | __all__ = ["Observable", "ConnectableObservable", "GroupedObservable"] 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [egg_info] 2 | #tag_build = dev 3 | #tag_svn_revision = 1 4 | 5 | [aliases] 6 | test = pytest 7 | 8 | [tool:pytest] 9 | testpaths = tests 10 | asyncio_mode = strict 11 | 12 | [mypy] 13 | python_version = 3.10 14 | follow_imports = silent 15 | -------------------------------------------------------------------------------- /reactivex/subject/__init__.py: -------------------------------------------------------------------------------- 1 | from .asyncsubject import AsyncSubject 2 | from .behaviorsubject import BehaviorSubject 3 | from .replaysubject import ReplaySubject 4 | from .subject import Subject 5 | 6 | __all__ = ["Subject", "AsyncSubject", "BehaviorSubject", "ReplaySubject"] 7 | -------------------------------------------------------------------------------- /reactivex/observable/interval.py: -------------------------------------------------------------------------------- 1 | from reactivex import Observable, abc, timer, typing 2 | 3 | 4 | def interval_( 5 | period: typing.RelativeTime, scheduler: abc.SchedulerBase | None = None 6 | ) -> Observable[int]: 7 | return timer(period, period, scheduler) 8 | 9 | 10 | __all__ = ["interval_"] 11 | -------------------------------------------------------------------------------- /reactivex/observer/__init__.py: -------------------------------------------------------------------------------- 1 | from .autodetachobserver import AutoDetachObserver 2 | from .observeonobserver import ObserveOnObserver 3 | from .observer import Observer 4 | from .scheduledobserver import ScheduledObserver 5 | 6 | __all__ = ["AutoDetachObserver", "ObserveOnObserver", "Observer", "ScheduledObserver"] 7 | -------------------------------------------------------------------------------- /reactivex/abc/startable.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class StartableBase(ABC): 5 | """Abstract base class for Thread- and Process-like objects.""" 6 | 7 | __slots__ = () 8 | 9 | @abstractmethod 10 | def start(self) -> None: 11 | raise NotImplementedError 12 | 13 | 14 | __all__ = ["StartableBase"] 15 | -------------------------------------------------------------------------------- /docs/reference.rst: -------------------------------------------------------------------------------- 1 | .. reference: 2 | 3 | Reference 4 | ========== 5 | 6 | 7 | .. toctree:: 8 | :name: reference 9 | 10 | Observable Factory 11 | Observable 12 | Subject 13 | Scheduler 14 | Operators 15 | Typing 16 | -------------------------------------------------------------------------------- /reactivex/observable/merge.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | import reactivex 4 | from reactivex import Observable 5 | from reactivex import operators as ops 6 | 7 | _T = TypeVar("_T") 8 | 9 | 10 | def merge_(*sources: Observable[_T]) -> Observable[_T]: 11 | return reactivex.from_iterable(sources).pipe(ops.merge_all()) 12 | 13 | 14 | __all__ = ["merge_"] 15 | -------------------------------------------------------------------------------- /docs/reference_subject.rst: -------------------------------------------------------------------------------- 1 | .. _reference_subject: 2 | 3 | Subject 4 | ======== 5 | 6 | .. autoclass:: reactivex.subject.Subject 7 | :members: 8 | 9 | .. autoclass:: reactivex.subject.BehaviorSubject 10 | :members: 11 | 12 | .. autoclass:: reactivex.subject.ReplaySubject 13 | :members: 14 | 15 | .. autoclass:: reactivex.subject.AsyncSubject 16 | :members: 17 | -------------------------------------------------------------------------------- /examples/asyncio/await.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import reactivex 4 | 5 | stream = reactivex.just("Hello, world!") 6 | 7 | 8 | async def hello_world(): 9 | n = await stream 10 | print(n) 11 | 12 | 13 | loop = asyncio.get_event_loop() 14 | # Blocking call which returns when the hello_world() coroutine is done 15 | loop.run_until_complete(hello_world()) 16 | loop.close() 17 | -------------------------------------------------------------------------------- /reactivex/scheduler/mainloop/__init__.py: -------------------------------------------------------------------------------- 1 | from .gtkscheduler import GtkScheduler 2 | from .pygamescheduler import PyGameScheduler 3 | from .qtscheduler import QtScheduler 4 | from .tkinterscheduler import TkinterScheduler 5 | from .wxscheduler import WxScheduler 6 | 7 | __all__ = [ 8 | "GtkScheduler", 9 | "PyGameScheduler", 10 | "QtScheduler", 11 | "TkinterScheduler", 12 | "WxScheduler", 13 | ] 14 | -------------------------------------------------------------------------------- /examples/konamicode/konamicode.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | var result = $('#result'); 3 | var ws = new WebSocket("ws://localhost:8080/ws"); 4 | 5 | $(document).keyup(function(ev) { 6 | msg = { keycode: ev.keyCode }; 7 | ws.send(JSON.stringify(msg)); 8 | }); 9 | 10 | ws.onmessage = function(msg) { 11 | result.html(msg.data).show().fadeOut(2000); // print the result 12 | }; 13 | }); -------------------------------------------------------------------------------- /reactivex/testing/__init__.py: -------------------------------------------------------------------------------- 1 | from .mockdisposable import MockDisposable 2 | from .reactivetest import OnErrorPredicate, OnNextPredicate, ReactiveTest, is_prime 3 | from .recorded import Recorded 4 | from .testscheduler import TestScheduler 5 | 6 | __all__ = [ 7 | "MockDisposable", 8 | "OnErrorPredicate", 9 | "OnNextPredicate", 10 | "ReactiveTest", 11 | "Recorded", 12 | "TestScheduler", 13 | "is_prime", 14 | ] 15 | -------------------------------------------------------------------------------- /docs/.rstcheck.cfg: -------------------------------------------------------------------------------- 1 | [rstcheck] 2 | ignore_directives= 3 | ignore_roles= 4 | ignore_messages=(Unknown directive type "autoclass".|No directive entry for "autoclass" in module "docutils.parsers.rst.languages.en".|Unknown directive type "automodule".|Unknown directive type "autofunction".|No directive entry for "autofunction" in module "docutils.parsers.rst.languages.en".|No directive entry for "automodule" in module "docutils.parsers.rst.languages.en".) 5 | report=warning -------------------------------------------------------------------------------- /examples/marbles/frommarbles_merge.py: -------------------------------------------------------------------------------- 1 | import reactivex 2 | from reactivex import operators as ops 3 | 4 | """ 5 | simple example that merges two cold observables. 6 | """ 7 | 8 | source0 = reactivex.cold("a-----d---1--------4-|", timespan=0.01) 9 | source1 = reactivex.cold("--b-c-------2---3-| ", timespan=0.01) 10 | 11 | observable = reactivex.merge(source0, source1).pipe(ops.to_iterable()) 12 | elements = observable.run() 13 | print(f"received {list(elements)}") 14 | -------------------------------------------------------------------------------- /examples/marbles/frommarbles_error.py: -------------------------------------------------------------------------------- 1 | import reactivex 2 | from reactivex import operators as ops 3 | 4 | """ 5 | Specify the error to be raised in place of the # symbol. 6 | """ 7 | 8 | err = ValueError("I don't like 5!") 9 | 10 | src0 = reactivex.from_marbles("12-----4-----67--|", timespan=0.2) 11 | src1 = reactivex.from_marbles("----3----5-# ", timespan=0.2, error=err) 12 | 13 | source = reactivex.merge(src0, src1).pipe(ops.do_action(print)) 14 | source.run() 15 | -------------------------------------------------------------------------------- /reactivex/testing/mockdisposable.py: -------------------------------------------------------------------------------- 1 | from reactivex import abc, typing 2 | from reactivex.scheduler import VirtualTimeScheduler 3 | 4 | 5 | class MockDisposable(abc.DisposableBase): 6 | def __init__(self, scheduler: VirtualTimeScheduler): 7 | self.scheduler = scheduler 8 | self.disposes: list[typing.AbsoluteTime] = [] 9 | self.disposes.append(self.scheduler.clock) 10 | 11 | def dispose(self) -> None: 12 | self.disposes.append(self.scheduler.clock) 13 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. Installation 2 | 3 | Installation 4 | ============ 5 | 6 | ReactiveX for Python (RxPY) v4.x runs on `Python 7 | `__ 3. To install: 8 | 9 | .. code:: console 10 | 11 | pip3 install reactivex 12 | 13 | RxPY v3.x runs on `Python `__ 3. To install RxPY: 14 | 15 | .. code:: console 16 | 17 | pip3 install rx 18 | 19 | For Python 2.x you need to use version 1.6 20 | 21 | .. code:: console 22 | 23 | pip install rx==1.6.1 24 | -------------------------------------------------------------------------------- /examples/marbles/tomarbles.py: -------------------------------------------------------------------------------- 1 | import reactivex 2 | from reactivex import operators as ops 3 | 4 | source0 = reactivex.cold("a-----d---1--------4-|", timespan=0.1) 5 | source1 = reactivex.cold("--b-c-------2---3-| ", timespan=0.1) 6 | 7 | print("to_marbles() is a blocking operator, we need to wait for completion...") 8 | print('expecting "a-b-c-d---1-2---3--4-|"') 9 | observable = reactivex.merge(source0, source1).pipe(ops.to_marbles(timespan=0.1)) 10 | diagram = observable.run() 11 | print(f'got "{diagram}"') 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Explicitly declare text files you want to always be normalized and converted 5 | # to native line endings on checkout. 6 | *.py text 7 | *.js text 8 | *.xml text 9 | *.yml text 10 | *.md text 11 | 12 | # Declare files that will always have CRLF line endings on checkout. 13 | *.sln text eol=crlf 14 | *.pyproj text eol=crlf 15 | 16 | # Denote all files that are truly binary and should not be modified. 17 | *.png binary 18 | *.jpg binary -------------------------------------------------------------------------------- /reactivex/observable/startasync.py: -------------------------------------------------------------------------------- 1 | from asyncio import Future 2 | from collections.abc import Callable 3 | from typing import TypeVar 4 | 5 | from reactivex import Observable, from_future, throw 6 | 7 | _T = TypeVar("_T") 8 | 9 | 10 | def start_async_(function_async: Callable[[], "Future[_T]"]) -> Observable[_T]: 11 | try: 12 | future = function_async() 13 | except Exception as ex: # pylint: disable=broad-except 14 | return throw(ex) 15 | 16 | return from_future(future) 17 | 18 | 19 | __all__ = ["start_async_"] 20 | -------------------------------------------------------------------------------- /reactivex/operators/_onerrorresumenext.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from typing import TypeVar 3 | 4 | import reactivex 5 | from reactivex import Observable 6 | 7 | _T = TypeVar("_T") 8 | 9 | 10 | def on_error_resume_next_( 11 | second: Observable[_T], 12 | ) -> Callable[[Observable[_T]], Observable[_T]]: 13 | def on_error_resume_next(source: Observable[_T]) -> Observable[_T]: 14 | return reactivex.on_error_resume_next(source, second) 15 | 16 | return on_error_resume_next 17 | 18 | 19 | __all__ = ["on_error_resume_next_"] 20 | -------------------------------------------------------------------------------- /reactivex/scheduler/eventloop/__init__.py: -------------------------------------------------------------------------------- 1 | from .asyncioscheduler import AsyncIOScheduler 2 | from .asynciothreadsafescheduler import AsyncIOThreadSafeScheduler 3 | from .eventletscheduler import EventletScheduler 4 | from .geventscheduler import GEventScheduler 5 | from .ioloopscheduler import IOLoopScheduler 6 | from .twistedscheduler import TwistedScheduler 7 | 8 | __all__ = [ 9 | "AsyncIOScheduler", 10 | "AsyncIOThreadSafeScheduler", 11 | "EventletScheduler", 12 | "GEventScheduler", 13 | "IOLoopScheduler", 14 | "TwistedScheduler", 15 | ] 16 | -------------------------------------------------------------------------------- /examples/marbles/hot_datetime.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import reactivex 4 | import reactivex.operators as ops 5 | 6 | """ 7 | Delay the emission of elements to the specified datetime. 8 | """ 9 | 10 | now = datetime.datetime.now(datetime.timezone.utc) 11 | dt = datetime.timedelta(seconds=3.0) 12 | duetime = now + dt 13 | 14 | print(f"{now} -> now\n" f"{duetime} -> start of emission in {dt.total_seconds()}s") 15 | 16 | hot = reactivex.hot("10--11--12--13--(14,|)", timespan=0.2, duetime=duetime) 17 | 18 | source = hot.pipe(ops.do_action(print)) 19 | source.run() 20 | -------------------------------------------------------------------------------- /reactivex/disposable/booleandisposable.py: -------------------------------------------------------------------------------- 1 | from threading import RLock 2 | 3 | from reactivex.abc import DisposableBase 4 | 5 | 6 | class BooleanDisposable(DisposableBase): 7 | """Represents a Disposable that can be checked for status.""" 8 | 9 | def __init__(self) -> None: 10 | """Initializes a new instance of the BooleanDisposable class.""" 11 | 12 | self.is_disposed = False 13 | self.lock = RLock() 14 | 15 | super().__init__() 16 | 17 | def dispose(self) -> None: 18 | """Sets the status to disposed""" 19 | 20 | self.is_disposed = True 21 | -------------------------------------------------------------------------------- /examples/marbles/frommarbles_lookup.py: -------------------------------------------------------------------------------- 1 | import reactivex 2 | import reactivex.operators as ops 3 | 4 | """ 5 | Use a dictionnary to convert elements declared in the marbles diagram to 6 | the specified values. 7 | """ 8 | 9 | lookup0 = {"a": 1, "b": 3, "c": 5} 10 | lookup1 = {"x": 2, "y": 4, "z": 6} 11 | source0 = reactivex.cold("a---b----c----|", timespan=0.01, lookup=lookup0) 12 | source1 = reactivex.cold("---x---y---z--|", timespan=0.01, lookup=lookup1) 13 | 14 | observable = reactivex.merge(source0, source1).pipe(ops.to_iterable()) 15 | elements = observable.run() 16 | print(f"received {list(elements)}") 17 | -------------------------------------------------------------------------------- /reactivex/observer/observeonobserver.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from .scheduledobserver import ScheduledObserver 4 | 5 | _T = TypeVar("_T") 6 | 7 | 8 | class ObserveOnObserver(ScheduledObserver[_T]): 9 | def _on_next_core(self, value: _T) -> None: 10 | super()._on_next_core(value) 11 | self.ensure_active() 12 | 13 | def _on_error_core(self, error: Exception) -> None: 14 | super()._on_error_core(error) 15 | self.ensure_active() 16 | 17 | def _on_completed_core(self) -> None: 18 | super()._on_completed_core() 19 | self.ensure_active() 20 | -------------------------------------------------------------------------------- /examples/marbles/frommarbles_flatmap.py: -------------------------------------------------------------------------------- 1 | import reactivex 2 | from reactivex import operators as ops 3 | 4 | a = reactivex.cold(" ---a0---a1----------------a2-| ") 5 | b = reactivex.cold(" ---b1---b2---| ") 6 | c = reactivex.cold(" ---c1---c2---| ") 7 | d = reactivex.cold(" -----d1---d2---|") 8 | e1 = reactivex.cold("a--b--------c-----d-------| ") 9 | 10 | observableLookup = {"a": a, "b": b, "c": c, "d": d} 11 | 12 | source = e1.pipe( 13 | ops.flat_map(lambda value: observableLookup[value]), 14 | ops.do_action(lambda v: print(v)), 15 | ) 16 | 17 | source.run() 18 | -------------------------------------------------------------------------------- /examples/parallel/timer.py: -------------------------------------------------------------------------------- 1 | import concurrent.futures 2 | import time 3 | 4 | import reactivex 5 | from reactivex import operators as ops 6 | 7 | seconds = [5, 1, 2, 4, 3] 8 | 9 | 10 | def sleep(tm: float) -> float: 11 | time.sleep(tm) 12 | return tm 13 | 14 | 15 | def output(result: str) -> None: 16 | print("%d seconds" % result) 17 | 18 | 19 | with concurrent.futures.ProcessPoolExecutor(5) as executor: 20 | reactivex.from_(seconds).pipe( 21 | ops.flat_map(lambda s: executor.submit(sleep, s)) 22 | ).subscribe(output) 23 | 24 | # 1 seconds 25 | # 2 seconds 26 | # 3 seconds 27 | # 4 seconds 28 | # 5 seconds 29 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============= 3 | 4 | You can contribute by reviewing and sending feedback on code checkins, 5 | suggesting and trying out new features as they are implemented, register issues 6 | and help us verify fixes as they are checked in, as well as submit code fixes or 7 | code contributions of your own. 8 | 9 | The main repository is at `ReactiveX/RxPY `_. 10 | Please register any issues to `ReactiveX/RxPY/issues `_. 11 | 12 | Please submit any pull requests against the 13 | `master `_ branch. 14 | -------------------------------------------------------------------------------- /tests/test_integration/test_concat_repeat.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from reactivex import operators as ops 4 | from reactivex.testing.marbles import marbles_testing 5 | 6 | 7 | class TestConcatIntegration(unittest.TestCase): 8 | def test_concat_repeat(self) -> None: 9 | with marbles_testing() as (start, cold, _hot, exp): 10 | e1 = cold("-e11-e12|", None, None) 11 | e2 = cold("-e21-e22|", None, None) 12 | ex = exp("-e11-e12-e21-e22-e11-e12-e21-e22|", None, None) 13 | 14 | obs = e1.pipe(ops.concat(e2), ops.repeat(2)) 15 | 16 | results = start(obs) 17 | assert results == ex 18 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = RxPY 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /reactivex/scheduler/historicalscheduler.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from .scheduler import UTC_ZERO 4 | from .virtualtimescheduler import VirtualTimeScheduler 5 | 6 | 7 | class HistoricalScheduler(VirtualTimeScheduler): 8 | """Provides a virtual time scheduler that uses datetime for absolute time 9 | and timedelta for relative time.""" 10 | 11 | def __init__(self, initial_clock: datetime | None = None) -> None: 12 | """Creates a new historical scheduler with the specified initial clock 13 | value. 14 | 15 | Args: 16 | initial_clock: Initial value for the clock. 17 | """ 18 | 19 | super().__init__(initial_clock or UTC_ZERO) 20 | -------------------------------------------------------------------------------- /reactivex/abc/__init__.py: -------------------------------------------------------------------------------- 1 | from .disposable import DisposableBase 2 | from .observable import ObservableBase, Subscription 3 | from .observer import ObserverBase, OnCompleted, OnError, OnNext 4 | from .periodicscheduler import PeriodicSchedulerBase 5 | from .scheduler import ScheduledAction, SchedulerBase 6 | from .startable import StartableBase 7 | from .subject import SubjectBase 8 | 9 | __all__ = [ 10 | "DisposableBase", 11 | "ObserverBase", 12 | "ObservableBase", 13 | "OnCompleted", 14 | "OnError", 15 | "OnNext", 16 | "SchedulerBase", 17 | "PeriodicSchedulerBase", 18 | "SubjectBase", 19 | "Subscription", 20 | "ScheduledAction", 21 | "StartableBase", 22 | ] 23 | -------------------------------------------------------------------------------- /reactivex/observable/empty.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from reactivex import Observable, abc 4 | from reactivex.scheduler import ImmediateScheduler 5 | 6 | 7 | def empty_(scheduler: abc.SchedulerBase | None = None) -> Observable[Any]: 8 | def subscribe( 9 | observer: abc.ObserverBase[Any], scheduler_: abc.SchedulerBase | None = None 10 | ) -> abc.DisposableBase: 11 | _scheduler = scheduler or scheduler_ or ImmediateScheduler.singleton() 12 | 13 | def action(_: abc.SchedulerBase, __: Any) -> None: 14 | observer.on_completed() 15 | 16 | return _scheduler.schedule(action) 17 | 18 | return Observable(subscribe) 19 | 20 | 21 | __all__ = ["empty_"] 22 | -------------------------------------------------------------------------------- /reactivex/observable/never.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from reactivex import Observable, abc 4 | from reactivex.disposable import Disposable 5 | 6 | 7 | def never_() -> Observable[Any]: 8 | """Returns a non-terminating observable sequence, which can be used 9 | to denote an infinite duration (e.g. when using reactive joins). 10 | 11 | Returns: 12 | An observable sequence whose observers will never get called. 13 | """ 14 | 15 | def subscribe( 16 | observer: abc.ObserverBase[Any], scheduler: abc.SchedulerBase | None = None 17 | ) -> abc.DisposableBase: 18 | return Disposable() 19 | 20 | return Observable(subscribe) 21 | 22 | 23 | __all__ = ["never_"] 24 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: v0.8.0 4 | hooks: 5 | # Run the linter 6 | - id: ruff 7 | args: [--fix] 8 | # Run the formatter 9 | - id: ruff-format 10 | - hooks: 11 | - id: pyright 12 | name: pyright 13 | entry: pyright 14 | language: node 15 | pass_filenames: false 16 | types: [python] 17 | additional_dependencies: ["pyright@1.1.407"] 18 | repo: local 19 | - hooks: 20 | - id: mypy 21 | exclude: (^docs/|^examples/|^notebooks/|^tests/|^reactivex/operators/_\w.*\.py$) 22 | repo: https://github.com/pre-commit/mirrors-mypy 23 | rev: v1.18.2 24 | -------------------------------------------------------------------------------- /tests/test_observable/test_never.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from reactivex import never 4 | from reactivex.testing import ReactiveTest, TestScheduler 5 | 6 | on_next = ReactiveTest.on_next 7 | on_completed = ReactiveTest.on_completed 8 | on_error = ReactiveTest.on_error 9 | subscribe = ReactiveTest.subscribe 10 | subscribed = ReactiveTest.subscribed 11 | disposed = ReactiveTest.disposed 12 | created = ReactiveTest.created 13 | 14 | 15 | class TestNever(unittest.TestCase): 16 | def test_never_basic(self): 17 | scheduler = TestScheduler() 18 | xs = never() 19 | results = scheduler.create_observer() 20 | xs.subscribe(results) 21 | scheduler.start() 22 | assert results.messages == [] 23 | -------------------------------------------------------------------------------- /reactivex/disposable/__init__.py: -------------------------------------------------------------------------------- 1 | from .booleandisposable import BooleanDisposable 2 | from .compositedisposable import CompositeDisposable 3 | from .disposable import Disposable 4 | from .multipleassignmentdisposable import MultipleAssignmentDisposable 5 | from .refcountdisposable import RefCountDisposable 6 | from .scheduleddisposable import ScheduledDisposable 7 | from .serialdisposable import SerialDisposable 8 | from .singleassignmentdisposable import SingleAssignmentDisposable 9 | 10 | __all__ = [ 11 | "BooleanDisposable", 12 | "CompositeDisposable", 13 | "Disposable", 14 | "MultipleAssignmentDisposable", 15 | "RefCountDisposable", 16 | "ScheduledDisposable", 17 | "SerialDisposable", 18 | "SingleAssignmentDisposable", 19 | ] 20 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. RxPY documentation master file, created by 2 | sphinx-quickstart on Sat Feb 17 23:30:45 2018. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | ReactiveX for Python (RxPY) 7 | =========================== 8 | 9 | ReactiveX for Python (RxPY) is a library for composing asynchronous and 10 | event-based programs using observable collections and pipable query 11 | operators in Python. 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | 16 | installation 17 | rationale 18 | get_started 19 | style_guide 20 | testing 21 | migration 22 | operators 23 | additional_reading 24 | reference 25 | contributing 26 | license 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/reference_scheduler.rst: -------------------------------------------------------------------------------- 1 | .. _reference_scheduler: 2 | 3 | Schedulers 4 | =========== 5 | 6 | .. automodule:: reactivex.scheduler 7 | :members: CatchScheduler, CurrentThreadScheduler, EventLoopScheduler, 8 | HistoricalScheduler, ImmediateScheduler, NewThreadScheduler, 9 | ThreadPoolScheduler, TimeoutScheduler, TrampolineScheduler, 10 | VirtualTimeScheduler 11 | 12 | .. automodule:: reactivex.scheduler.eventloop 13 | :members: AsyncIOScheduler, AsyncIOThreadSafeScheduler, EventletScheduler, 14 | GEventScheduler, IOLoopScheduler, TwistedScheduler 15 | 16 | .. automodule:: reactivex.scheduler.mainloop 17 | :members: GtkScheduler, PyGameScheduler, QtScheduler, 18 | TkinterScheduler, WxScheduler 19 | -------------------------------------------------------------------------------- /examples/marbles/testing_debounce.py: -------------------------------------------------------------------------------- 1 | from reactivex import operators as ops 2 | from reactivex.testing.marbles import marbles_testing 3 | 4 | """ 5 | Tests debounceTime from reactivexjs 6 | https://github.com/ReactiveX/rxjs/blob/master/spec/operators/debounceTime-spec.ts 7 | 8 | it should delay all element by the specified time 9 | """ 10 | with marbles_testing(timespan=1.0) as (start, cold, hot, exp): 11 | e1 = cold("-a--------b------c----|") 12 | ex = exp("------a--------b------(c,|)") 13 | expected = ex 14 | 15 | def create(): 16 | return e1.pipe( 17 | ops.debounce(5), 18 | ) 19 | 20 | results = start(create) 21 | assert results == expected 22 | 23 | print("debounce: results vs expected") 24 | for r, e in zip(results, expected): 25 | print(r, e) 26 | -------------------------------------------------------------------------------- /reactivex/testing/subscription.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from typing import Any 3 | 4 | 5 | class Subscription: 6 | def __init__(self, start: int, end: int | None = None): 7 | self.subscribe = start 8 | self.unsubscribe = end or sys.maxsize 9 | 10 | def equals(self, other: Any) -> bool: 11 | return ( 12 | self.subscribe == other.subscribe and self.unsubscribe == other.unsubscribe 13 | ) 14 | 15 | def __eq__(self, other: Any) -> bool: 16 | return self.equals(other) 17 | 18 | def __repr__(self) -> str: 19 | return str(self) 20 | 21 | def __str__(self) -> str: 22 | unsubscribe = ( 23 | "Infinite" if self.unsubscribe == sys.maxsize else self.unsubscribe 24 | ) 25 | return f"({self.subscribe}, {unsubscribe})" 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Code or Screenshots** 20 | If applicable, add a minimal and self contained code example or screenshots to help explain your problem. 21 | 22 | ```python 23 | def foo(self) -> str: 24 | return 3 25 | ``` 26 | 27 | **Additional context** 28 | Add any other context about the problem here. 29 | 30 | - OS [e.g. Windows] 31 | - RxPY version [e.g 4.0.0] 32 | - Python version [e.g. 3.10.2] 33 | -------------------------------------------------------------------------------- /reactivex/observable/amb.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from reactivex import Observable, never 4 | from reactivex import operators as _ 5 | 6 | _T = TypeVar("_T") 7 | 8 | 9 | def amb_(*sources: Observable[_T]) -> Observable[_T]: 10 | """Propagates the observable sequence that reacts first. 11 | 12 | Example: 13 | >>> winner = amb(xs, ys, zs) 14 | 15 | Returns: 16 | An observable sequence that surfaces any of the given sequences, 17 | whichever reacted first. 18 | """ 19 | 20 | acc: Observable[_T] = never() 21 | 22 | def func(previous: Observable[_T], current: Observable[_T]) -> Observable[_T]: 23 | return _.amb(previous)(current) 24 | 25 | for source in sources: 26 | acc = func(acc, source) 27 | 28 | return acc 29 | 30 | 31 | __all__ = ["amb_"] 32 | -------------------------------------------------------------------------------- /reactivex/subject/innersubscription.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from typing import TYPE_CHECKING, TypeVar 3 | 4 | from .. import abc 5 | 6 | if TYPE_CHECKING: 7 | from .subject import Subject 8 | 9 | _T = TypeVar("_T") 10 | 11 | 12 | class InnerSubscription(abc.DisposableBase): 13 | def __init__( 14 | self, subject: "Subject[_T]", observer: abc.ObserverBase[_T] | None = None 15 | ): 16 | self.subject = subject 17 | self.observer = observer 18 | 19 | self.lock = threading.RLock() 20 | 21 | def dispose(self) -> None: 22 | with self.lock: 23 | if not self.subject.is_disposed and self.observer: 24 | if self.observer in self.subject.observers: 25 | self.subject.observers.remove(self.observer) 26 | self.observer = None 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen, and also include a minimal code example if applicable. 15 | 16 | ```python 17 | def foo(self) -> str: 18 | return 3 19 | ``` 20 | 21 | **Describe alternatives you've considered** 22 | A clear and concise description of any alternative solutions or features you've considered. 23 | 24 | **Additional context** 25 | Add any other context or screenshots about the feature request here. 26 | -------------------------------------------------------------------------------- /reactivex/internal/basic.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone 2 | from typing import Any, NoReturn, TypeVar 3 | 4 | _T = TypeVar("_T") 5 | 6 | 7 | def noop(*args: Any, **kw: Any) -> None: 8 | """No operation. Returns nothing""" 9 | 10 | 11 | def identity(x: _T) -> _T: 12 | """Returns argument x""" 13 | return x 14 | 15 | 16 | def default_now() -> datetime: 17 | return datetime.now(timezone.utc) 18 | 19 | 20 | def default_comparer(x: _T, y: _T) -> bool: 21 | return x == y 22 | 23 | 24 | def default_sub_comparer(x: Any, y: Any) -> Any: 25 | return x - y 26 | 27 | 28 | def default_key_serializer(x: Any) -> str: 29 | return str(x) 30 | 31 | 32 | def default_error(err: Exception | str) -> NoReturn: 33 | if isinstance(err, BaseException): 34 | raise err 35 | 36 | raise Exception(err) 37 | -------------------------------------------------------------------------------- /reactivex/observable/throw.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from reactivex import Observable, abc 4 | from reactivex.scheduler import ImmediateScheduler 5 | 6 | 7 | def throw_( 8 | exception: str | Exception, scheduler: abc.SchedulerBase | None = None 9 | ) -> Observable[Any]: 10 | exception_ = exception if isinstance(exception, Exception) else Exception(exception) 11 | 12 | def subscribe( 13 | observer: abc.ObserverBase[Any], scheduler: abc.SchedulerBase | None = None 14 | ) -> abc.DisposableBase: 15 | _scheduler = scheduler or ImmediateScheduler.singleton() 16 | 17 | def action(scheduler: abc.SchedulerBase, state: Any) -> None: 18 | observer.on_error(exception_) 19 | 20 | return _scheduler.schedule(action) 21 | 22 | return Observable(subscribe) 23 | 24 | 25 | __all__ = ["throw_"] 26 | -------------------------------------------------------------------------------- /examples/errors/failing.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how you can retry subscriptions that might 3 | sometime fail with an error. 4 | 5 | Note: you cannot use ref_count() in this case since that would 6 | make publish() re-subscribe the cold-observable and it would start 7 | looping forever. 8 | """ 9 | 10 | import time 11 | 12 | import reactivex 13 | from reactivex import operators as ops 14 | 15 | 16 | def failing(x): 17 | x = int(x) 18 | if not x % 2: 19 | raise Exception("Error") 20 | return x 21 | 22 | 23 | def main(): 24 | xs = reactivex.from_marbles("1-2-3-4-5-6-7-9-|").pipe(ops.publish()) 25 | xs.pipe(ops.map(failing), ops.retry()).subscribe(print) 26 | 27 | xs.connect() # Must connect. Cannot use ref_count() with publish() 28 | 29 | time.sleep(5) 30 | 31 | 32 | if __name__ == "__main__": 33 | main() 34 | -------------------------------------------------------------------------------- /reactivex/internal/concurrency.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from threading import RLock, Thread 3 | from typing import Any, TypeVar 4 | 5 | from typing_extensions import ParamSpec 6 | 7 | from reactivex.typing import StartableTarget 8 | 9 | _T = TypeVar("_T") 10 | _P = ParamSpec("_P") 11 | 12 | 13 | def default_thread_factory(target: StartableTarget) -> Thread: 14 | return Thread(target=target, daemon=True) 15 | 16 | 17 | def synchronized(lock: RLock) -> Callable[[Callable[_P, _T]], Callable[_P, _T]]: 18 | """A decorator for synchronizing access to a given function.""" 19 | 20 | def wrapper(fn: Callable[_P, _T]) -> Callable[_P, _T]: 21 | def inner(*args: _P.args, **kw: _P.kwargs) -> Any: 22 | with lock: 23 | return fn(*args, **kw) 24 | 25 | return inner 26 | 27 | return wrapper 28 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.12" 12 | 13 | # Build documentation in the "docs/" directory with Sphinx 14 | sphinx: 15 | configuration: docs/conf.py 16 | 17 | # Optionally build your docs in additional formats such as PDF and ePub 18 | # formats: 19 | # - pdf 20 | # - epub 21 | 22 | # Optional but recommended, declare the Python requirements required 23 | # to build your documentation 24 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 25 | python: 26 | install: 27 | - requirements: docs/requirements.txt 28 | - method: pip 29 | path: . 30 | -------------------------------------------------------------------------------- /examples/autocomplete/autocomplete.js: -------------------------------------------------------------------------------- 1 | (function (global, $, undefined) { 2 | function main() { 3 | var $input = $('#textInput'), 4 | $results = $('#results'); 5 | var ws = new WebSocket("ws://localhost:8080/ws"); 6 | 7 | $input.keyup(function(ev) { 8 | var msg = { term: ev.target.value }; 9 | ws.send(JSON.stringify(msg)); 10 | }); 11 | 12 | ws.onmessage = function(msg) { 13 | var data = JSON.parse(msg.data); 14 | var res = data[1]; 15 | 16 | // Append the results 17 | $results.empty(); 18 | 19 | $.each(res, function (_, value) { 20 | $('
  • ' + value + '
  • ').appendTo($results); 21 | }); 22 | $results.show(); 23 | } 24 | } 25 | main(); 26 | }(window, jQuery)); -------------------------------------------------------------------------------- /reactivex/operators/_concat.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | import reactivex 4 | from reactivex import Observable 5 | from reactivex.internal import curry_flip 6 | 7 | _T = TypeVar("_T") 8 | 9 | 10 | @curry_flip 11 | def concat_(source: Observable[_T], *sources: Observable[_T]) -> Observable[_T]: 12 | """Concatenates all the observable sequences. 13 | 14 | Examples: 15 | >>> result = source.pipe(concat(xs, ys, zs)) 16 | >>> result = concat(xs, ys, zs)(source) 17 | 18 | Args: 19 | source: The source observable sequence. 20 | sources: Additional observable sequences to concatenate. 21 | 22 | Returns: 23 | An observable sequence that contains the elements of 24 | each given sequence, in sequential order. 25 | """ 26 | return reactivex.concat(source, *sources) 27 | 28 | 29 | __all__ = ["concat_"] 30 | -------------------------------------------------------------------------------- /reactivex/testing/mockobserver.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from reactivex import abc 4 | from reactivex.notification import OnCompleted, OnError, OnNext 5 | from reactivex.scheduler import VirtualTimeScheduler 6 | 7 | from .recorded import Recorded 8 | 9 | _T = TypeVar("_T") 10 | 11 | 12 | class MockObserver(abc.ObserverBase[_T]): 13 | def __init__(self, scheduler: VirtualTimeScheduler) -> None: 14 | self.scheduler = scheduler 15 | self.messages: list[Recorded[_T]] = [] 16 | 17 | def on_next(self, value: _T) -> None: 18 | self.messages.append(Recorded(self.scheduler.clock, OnNext(value))) 19 | 20 | def on_error(self, error: Exception) -> None: 21 | self.messages.append(Recorded(self.scheduler.clock, OnError(error))) 22 | 23 | def on_completed(self) -> None: 24 | self.messages.append(Recorded(self.scheduler.clock, OnCompleted())) 25 | -------------------------------------------------------------------------------- /reactivex/operators/_startswith.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | import reactivex 4 | from reactivex import Observable 5 | from reactivex.internal import curry_flip 6 | 7 | _T = TypeVar("_T") 8 | 9 | 10 | @curry_flip 11 | def start_with_(source: Observable[_T], *args: _T) -> Observable[_T]: 12 | """Prepends a sequence of values to an observable sequence. 13 | 14 | Example: 15 | >>> result = source.pipe(start_with(1, 2, 3)) 16 | >>> result = start_with(1, 2, 3)(source) 17 | 18 | Args: 19 | source: The source observable sequence. 20 | args: Values to prepend to the source sequence. 21 | 22 | Returns: 23 | The source sequence prepended with the specified values. 24 | """ 25 | start = reactivex.from_iterable(args) 26 | sequence = [start, source] 27 | return reactivex.concat(*sequence) 28 | 29 | 30 | __all__ = ["start_with_"] 31 | -------------------------------------------------------------------------------- /reactivex/operators/_isempty.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from reactivex import Observable 4 | from reactivex import operators as ops 5 | from reactivex.internal import curry_flip 6 | 7 | _T = TypeVar("_T") 8 | 9 | 10 | @curry_flip 11 | def is_empty_(source: Observable[_T]) -> Observable[bool]: 12 | """Determines whether an observable sequence is empty. 13 | 14 | Examples: 15 | >>> res = source.pipe(is_empty()) 16 | >>> res = is_empty()(source) 17 | 18 | Args: 19 | source: The source observable sequence. 20 | 21 | Returns: 22 | An observable sequence containing a single element 23 | determining whether the source sequence is empty. 24 | """ 25 | 26 | def mapper(b: bool) -> bool: 27 | return not b 28 | 29 | return source.pipe( 30 | ops.some(), 31 | ops.map(mapper), 32 | ) 33 | 34 | 35 | __all__ = ["is_empty_"] 36 | -------------------------------------------------------------------------------- /reactivex/internal/__init__.py: -------------------------------------------------------------------------------- 1 | from .basic import default_comparer, default_error, noop 2 | from .concurrency import default_thread_factory, synchronized 3 | from .constants import DELTA_ZERO, UTC_ZERO 4 | from .curry import curry_flip 5 | from .exceptions import ( 6 | ArgumentOutOfRangeException, 7 | DisposedException, 8 | SequenceContainsNoElementsError, 9 | ) 10 | from .priorityqueue import PriorityQueue 11 | from .utils import NotSet, add_ref, alias, infinite 12 | 13 | __all__ = [ 14 | "add_ref", 15 | "alias", 16 | "ArgumentOutOfRangeException", 17 | "curry_flip", 18 | "DisposedException", 19 | "default_comparer", 20 | "default_error", 21 | "infinite", 22 | "noop", 23 | "NotSet", 24 | "SequenceContainsNoElementsError", 25 | "concurrency", 26 | "DELTA_ZERO", 27 | "UTC_ZERO", 28 | "synchronized", 29 | "default_thread_factory", 30 | "PriorityQueue", 31 | ] 32 | -------------------------------------------------------------------------------- /reactivex/abc/disposable.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC, abstractmethod 4 | from types import TracebackType 5 | 6 | 7 | class DisposableBase(ABC): 8 | """Disposable abstract base class.""" 9 | 10 | __slots__ = () 11 | 12 | @abstractmethod 13 | def dispose(self) -> None: 14 | """Dispose the object: stop whatever we're doing and release all of the 15 | resources we might be using. 16 | """ 17 | raise NotImplementedError 18 | 19 | def __enter__(self) -> DisposableBase: 20 | """Context management protocol.""" 21 | return self 22 | 23 | def __exit__( 24 | self, 25 | exctype: type[BaseException] | None, 26 | excinst: BaseException | None, 27 | exctb: TracebackType | None, 28 | ) -> None: 29 | """Context management protocol.""" 30 | self.dispose() 31 | 32 | 33 | __all__ = ["DisposableBase"] 34 | -------------------------------------------------------------------------------- /reactivex/scheduler/__init__.py: -------------------------------------------------------------------------------- 1 | from .catchscheduler import CatchScheduler 2 | from .currentthreadscheduler import CurrentThreadScheduler 3 | from .eventloopscheduler import EventLoopScheduler 4 | from .historicalscheduler import HistoricalScheduler 5 | from .immediatescheduler import ImmediateScheduler 6 | from .newthreadscheduler import NewThreadScheduler 7 | from .scheduleditem import ScheduledItem 8 | from .threadpoolscheduler import ThreadPoolScheduler 9 | from .timeoutscheduler import TimeoutScheduler 10 | from .trampolinescheduler import TrampolineScheduler 11 | from .virtualtimescheduler import VirtualTimeScheduler 12 | 13 | __all__ = [ 14 | "CatchScheduler", 15 | "CurrentThreadScheduler", 16 | "EventLoopScheduler", 17 | "HistoricalScheduler", 18 | "ImmediateScheduler", 19 | "NewThreadScheduler", 20 | "ScheduledItem", 21 | "ThreadPoolScheduler", 22 | "TimeoutScheduler", 23 | "TrampolineScheduler", 24 | "VirtualTimeScheduler", 25 | ] 26 | -------------------------------------------------------------------------------- /reactivex/operators/_asobservable.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from reactivex import Observable, abc 4 | from reactivex.internal import curry_flip 5 | 6 | _T = TypeVar("_T") 7 | 8 | 9 | @curry_flip 10 | def as_observable_(source: Observable[_T]) -> Observable[_T]: 11 | """Hides the identity of an observable sequence. 12 | 13 | Examples: 14 | >>> res = source.pipe(as_observable()) 15 | >>> res = as_observable()(source) 16 | 17 | Args: 18 | source: Observable source to hide identity from. 19 | 20 | Returns: 21 | An observable sequence that hides the identity of the 22 | source sequence. 23 | """ 24 | 25 | def subscribe( 26 | observer: abc.ObserverBase[_T], 27 | scheduler: abc.SchedulerBase | None = None, 28 | ) -> abc.DisposableBase: 29 | return source.subscribe(observer, scheduler=scheduler) 30 | 31 | return Observable(subscribe) 32 | 33 | 34 | __all__ = ["as_observable_"] 35 | -------------------------------------------------------------------------------- /reactivex/operators/_dowhile.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from typing import TypeVar 3 | 4 | from reactivex import Observable 5 | from reactivex import operators as ops 6 | 7 | _T = TypeVar("_T") 8 | 9 | 10 | def do_while_( 11 | condition: Callable[[Observable[_T]], bool], 12 | ) -> Callable[[Observable[_T]], Observable[_T]]: 13 | """Repeats source as long as condition holds emulating a do while 14 | loop. 15 | 16 | Args: 17 | condition: The condition which determines if the source will be 18 | repeated. 19 | 20 | Returns: 21 | An observable sequence which is repeated as long 22 | as the condition holds. 23 | """ 24 | 25 | def do_while(source: Observable[_T]) -> Observable[_T]: 26 | return source.pipe( 27 | ops.concat( 28 | source.pipe( 29 | ops.while_do(condition), 30 | ), 31 | ) 32 | ) 33 | 34 | return do_while 35 | 36 | 37 | __all__ = ["do_while_"] 38 | -------------------------------------------------------------------------------- /reactivex/observable/repeat.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | import reactivex 4 | from reactivex import Observable 5 | from reactivex import operators as ops 6 | 7 | _T = TypeVar("_T") 8 | 9 | 10 | def repeat_value_(value: _T, repeat_count: int | None = None) -> Observable[_T]: 11 | """Generates an observable sequence that repeats the given element 12 | the specified number of times. 13 | 14 | Examples: 15 | 1 - res = repeat_value(42) 16 | 2 - res = repeat_value(42, 4) 17 | 18 | Args: 19 | value: Element to repeat. 20 | repeat_count: [Optional] Number of times to repeat the element. 21 | If not specified, repeats indefinitely. 22 | 23 | Returns: 24 | An observable sequence that repeats the given element the 25 | specified number of times. 26 | """ 27 | 28 | if repeat_count == -1: 29 | repeat_count = None 30 | 31 | xs = reactivex.return_value(value) 32 | return xs.pipe(ops.repeat(repeat_count)) 33 | 34 | 35 | __all__ = ["repeat_value_"] 36 | -------------------------------------------------------------------------------- /reactivex/internal/exceptions.py: -------------------------------------------------------------------------------- 1 | # Rx Exceptions 2 | 3 | 4 | class SequenceContainsNoElementsError(Exception): 5 | def __init__(self, msg: str | None = None): 6 | super().__init__(msg or "Sequence contains no elements") 7 | 8 | 9 | class ArgumentOutOfRangeException(ValueError): 10 | def __init__(self, msg: str | None = None): 11 | super().__init__(msg or "Argument out of range") 12 | 13 | 14 | class DisposedException(Exception): 15 | def __init__(self, msg: str | None = None): 16 | super().__init__(msg or "Object has been disposed") 17 | 18 | 19 | class ReEntracyException(Exception): 20 | def __init__(self, msg: str | None = None): 21 | super().__init__(msg or "Re-entrancy detected") 22 | 23 | 24 | class CompletedException(Exception): 25 | def __init__(self, msg: str | None = None): 26 | super().__init__(msg or "Observer completed") 27 | 28 | 29 | class WouldBlockException(Exception): 30 | def __init__(self, msg: str | None = None): 31 | super().__init__(msg or "Would block") 32 | -------------------------------------------------------------------------------- /reactivex/observable/case.py: -------------------------------------------------------------------------------- 1 | from asyncio import Future 2 | from collections.abc import Callable, Mapping 3 | from typing import TypeVar, Union 4 | 5 | from reactivex import Observable, abc, defer, empty, from_future 6 | 7 | _Key = TypeVar("_Key") 8 | _T = TypeVar("_T") 9 | 10 | 11 | def case_( 12 | mapper: Callable[[], _Key], 13 | sources: Mapping[_Key, Observable[_T]], 14 | default_source: Union[Observable[_T], "Future[_T]"] | None = None, 15 | ) -> Observable[_T]: 16 | default_source_: Observable[_T] | Future[_T] = default_source or empty() 17 | 18 | def factory(_: abc.SchedulerBase) -> Observable[_T]: 19 | try: 20 | result: Observable[_T] | Future[_T] = sources[mapper()] 21 | except KeyError: 22 | result = default_source_ 23 | 24 | if isinstance(result, Future): 25 | result_: Observable[_T] = from_future(result) 26 | else: 27 | result_ = result 28 | 29 | return result_ 30 | 31 | return defer(factory) 32 | 33 | 34 | __all__ = ["case_"] 35 | -------------------------------------------------------------------------------- /reactivex/operators/_publishvalue.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from typing import TypeVar 3 | 4 | from reactivex import ConnectableObservable, Observable, abc 5 | from reactivex import operators as ops 6 | from reactivex.subject import BehaviorSubject 7 | from reactivex.typing import Mapper 8 | 9 | _T1 = TypeVar("_T1") 10 | _T2 = TypeVar("_T2") 11 | 12 | 13 | def publish_value_( 14 | initial_value: _T1, 15 | mapper: Mapper[Observable[_T1], Observable[_T2]] | None = None, 16 | ) -> ( 17 | Callable[[Observable[_T1]], ConnectableObservable[_T1]] 18 | | Callable[[Observable[_T1]], Observable[_T2]] 19 | ): 20 | if mapper: 21 | 22 | def subject_factory( 23 | scheduler: abc.SchedulerBase | None = None, 24 | ) -> BehaviorSubject[_T1]: 25 | return BehaviorSubject(initial_value) 26 | 27 | return ops.multicast(subject_factory=subject_factory, mapper=mapper) 28 | 29 | subject = BehaviorSubject(initial_value) 30 | return ops.multicast(subject) 31 | 32 | 33 | __all__ = ["publish_value_"] 34 | -------------------------------------------------------------------------------- /reactivex/operators/_forkjoin.py: -------------------------------------------------------------------------------- 1 | from typing import Any, cast 2 | 3 | import reactivex 4 | from reactivex import Observable 5 | from reactivex.internal import curry_flip 6 | 7 | 8 | @curry_flip 9 | def fork_join_( 10 | source: Observable[Any], 11 | *args: Observable[Any], 12 | ) -> Observable[tuple[Any, ...]]: 13 | """Wait for observables to complete and then combine last values 14 | they emitted into a tuple. Whenever any of that observables 15 | completes without emitting any value, result sequence will 16 | complete at that moment as well. 17 | 18 | Examples: 19 | >>> source.pipe(fork_join(obs1, obs2)) 20 | >>> fork_join(obs1, obs2)(source) 21 | 22 | Args: 23 | source: Source observable. 24 | *args: Additional observables to fork_join with. 25 | 26 | Returns: 27 | An observable sequence containing the result of combining 28 | last element from each source in given sequence. 29 | """ 30 | return cast(Observable[tuple[Any, ...]], reactivex.fork_join(source, *args)) 31 | 32 | 33 | __all__ = ["fork_join_"] 34 | -------------------------------------------------------------------------------- /tests/test_scheduler/test_scheduler.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from reactivex.internal.constants import DELTA_ZERO, UTC_ZERO 4 | from reactivex.scheduler.scheduler import Scheduler 5 | 6 | 7 | class TestScheduler(unittest.TestCase): 8 | def test_base_to_seconds(self): 9 | val = Scheduler.to_seconds(0.0) 10 | assert val == 0.0 11 | val = Scheduler.to_seconds(DELTA_ZERO) 12 | assert val == 0.0 13 | val = Scheduler.to_seconds(UTC_ZERO) 14 | assert val == 0.0 15 | 16 | def test_base_to_datetime(self): 17 | val = Scheduler.to_datetime(0.0) 18 | assert val == UTC_ZERO 19 | val = Scheduler.to_datetime(DELTA_ZERO) 20 | assert val == UTC_ZERO 21 | val = Scheduler.to_datetime(UTC_ZERO) 22 | assert val == UTC_ZERO 23 | 24 | def test_base_to_timedelta(self): 25 | val = Scheduler.to_timedelta(0.0) 26 | assert val == DELTA_ZERO 27 | val = Scheduler.to_timedelta(DELTA_ZERO) 28 | assert val == DELTA_ZERO 29 | val = Scheduler.to_timedelta(UTC_ZERO) 30 | assert val == DELTA_ZERO 31 | -------------------------------------------------------------------------------- /reactivex/observable/mixins/__init__.py: -------------------------------------------------------------------------------- 1 | """Mixins for Observable method chaining. 2 | 3 | This module contains mixin classes that provide operator methods for Observable. 4 | Each mixin focuses on a specific category of operators, making the codebase 5 | more maintainable and organized. 6 | """ 7 | 8 | from .combination import CombinationMixin 9 | from .conditional import ConditionalMixin 10 | from .error_handling import ErrorHandlingMixin 11 | from .filtering import FilteringMixin 12 | from .mathematical import MathematicalMixin 13 | from .multicasting import MulticastingMixin 14 | from .testing import TestingMixin 15 | from .time_based import TimeBasedMixin 16 | from .transformation import TransformationMixin 17 | from .utility import UtilityMixin 18 | from .windowing import WindowingMixin 19 | 20 | __all__ = [ 21 | "CombinationMixin", 22 | "ConditionalMixin", 23 | "ErrorHandlingMixin", 24 | "FilteringMixin", 25 | "MathematicalMixin", 26 | "MulticastingMixin", 27 | "TestingMixin", 28 | "TimeBasedMixin", 29 | "TransformationMixin", 30 | "UtilityMixin", 31 | "WindowingMixin", 32 | ] 33 | -------------------------------------------------------------------------------- /reactivex/operators/_sum.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from reactivex import Observable 4 | from reactivex import operators as ops 5 | from reactivex.internal import curry_flip 6 | from reactivex.typing import Mapper 7 | 8 | 9 | @curry_flip 10 | def sum_( 11 | source: Observable[Any], 12 | key_mapper: Mapper[Any, float] | None = None, 13 | ) -> Observable[float]: 14 | """Computes the sum of a sequence of values. 15 | 16 | Examples: 17 | >>> result = source.pipe(sum()) 18 | >>> result = sum()(source) 19 | >>> result = source.pipe(sum(lambda x: x.value)) 20 | 21 | Args: 22 | source: The source observable. 23 | key_mapper: Optional mapper to extract numeric values. 24 | 25 | Returns: 26 | An observable sequence containing a single element with the sum. 27 | """ 28 | if key_mapper: 29 | return source.pipe(ops.map(key_mapper), ops.sum()) 30 | 31 | def accumulator(prev: float, cur: float) -> float: 32 | return prev + cur 33 | 34 | return source.pipe(ops.reduce(seed=0, accumulator=accumulator)) 35 | 36 | 37 | __all__ = ["sum_"] 38 | -------------------------------------------------------------------------------- /reactivex/operators/_ignoreelements.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from reactivex import Observable, abc 4 | from reactivex.internal import curry_flip, noop 5 | 6 | _T = TypeVar("_T") 7 | 8 | 9 | @curry_flip 10 | def ignore_elements_(source: Observable[_T]) -> Observable[_T]: 11 | """Ignores all elements in an observable sequence leaving only the 12 | termination messages. 13 | 14 | Examples: 15 | >>> res = source.pipe(ignore_elements()) 16 | >>> res = ignore_elements()(source) 17 | 18 | Args: 19 | source: The source observable sequence. 20 | 21 | Returns: 22 | An empty observable sequence that signals 23 | termination, successful or exceptional, of the source sequence. 24 | """ 25 | 26 | def subscribe( 27 | observer: abc.ObserverBase[_T], 28 | scheduler: abc.SchedulerBase | None = None, 29 | ) -> abc.DisposableBase: 30 | return source.subscribe( 31 | noop, observer.on_error, observer.on_completed, scheduler=scheduler 32 | ) 33 | 34 | return Observable(subscribe) 35 | 36 | 37 | __all__ = ["ignore_elements_"] 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | .eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | develop-eggs 18 | .installed.cfg 19 | lib 20 | lib64 21 | *tmp_dir 22 | 23 | # Installer logs 24 | pip-log.txt 25 | 26 | # Unit test / coverage reports 27 | .coverage 28 | .tox 29 | nosetests.xml 30 | coverage.xml 31 | htmlcov 32 | .mypy_cache 33 | .pytest_cache 34 | 35 | # Virtual env 36 | venv 37 | 38 | # Translations 39 | *.mo 40 | 41 | # Mr Developer 42 | .mr.developer.cfg 43 | .project 44 | .pydevproject 45 | 46 | # Visual studio 47 | *.suo 48 | *.DotSettings.user 49 | TestResults/ 50 | 51 | # Log files 52 | *.log 53 | Rx.pyperf 54 | *.coverage 55 | UpgradeLog.htm 56 | TestResults/Rx.TE.Tests.mdf 57 | TestResults/Rx.TE.Tests_log.ldf 58 | *.user 59 | 60 | # Cloud9 61 | .c9 62 | 63 | # PyCharm 64 | .idea 65 | .zedstate 66 | 67 | # Spyder IDE 68 | .spyproject/ 69 | 70 | .ipynb_checkpoints 71 | .cache/ 72 | .vscode/ 73 | .noseids 74 | _build 75 | 76 | # Mac OS 77 | .DS_Store 78 | 79 | # Type checkers 80 | .pyre 81 | 82 | .ionide/ 83 | 84 | .python-version 85 | __pycache__ -------------------------------------------------------------------------------- /examples/marbles/testing_flatmap.py: -------------------------------------------------------------------------------- 1 | from reactivex import operators as ops 2 | from reactivex.testing.marbles import marbles_testing 3 | 4 | """ 5 | Tests MergeMap from reactivexjs 6 | https://github.com/ReactiveX/rxjs/blob/master/spec/operators/mergeMap-spec.ts 7 | 8 | it should flat_map many regular interval inners 9 | """ 10 | with marbles_testing(timespan=1.0) as context: 11 | start, cold, hot, exp = context 12 | 13 | a = cold(" ----a---a----a----(a,|) ") 14 | b = cold(" ----1----b----(b,|) ") 15 | c = cold(" -------c---c---c----c---(c,|)") 16 | d = cold(" -------(d,|) ") 17 | e1 = hot("-a---b-----------c-------d------------| ") 18 | ex = exp("-----a---(a,1)(a,b)(a,b)c---c---(c,d)c---(c,|)") 19 | expected = ex 20 | 21 | observableLookup = {"a": a, "b": b, "c": c, "d": d} 22 | 23 | obs = e1.pipe(ops.flat_map(lambda value: observableLookup[value])) 24 | 25 | results = start(obs) 26 | assert results == expected 27 | 28 | print("flat_map: results vs expected") 29 | for r, e in zip(results, expected): 30 | print(r, e) 31 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | name: "Publish library" 11 | steps: 12 | - name: Check out 13 | uses: actions/checkout@v4 14 | with: 15 | token: "${{ secrets.GITHUB_TOKEN }}" 16 | fetch-depth: 0 17 | 18 | - name: Setup Python Env 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: "3.12" 22 | 23 | - name: Install uv 24 | uses: astral-sh/setup-uv@v4 25 | 26 | - name: Install dependencies 27 | run: uv tool install dunamai 28 | 29 | - name: Set version 30 | run: | 31 | RX_VERSION=$(uvx dunamai from any --no-metadata --style pep440) 32 | sed -i "s/version = \".*\"/version = \"$RX_VERSION\"/" pyproject.toml 33 | echo "__version__ = \"$RX_VERSION\"" > reactivex/_version.py 34 | 35 | - name: Build package 36 | run: uv build 37 | 38 | - name: Release to PyPI 39 | run: | 40 | uv publish --token ${{ secrets.PYPI_API_TOKEN }} || echo 'Version exists' -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | 3 | Copyright 2013-2022, Dag Brattli, Microsoft Corp., and Contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /reactivex/operators/_delaysubscription.py: -------------------------------------------------------------------------------- 1 | from typing import Any, TypeVar 2 | 3 | import reactivex 4 | from reactivex import Observable, abc, typing 5 | from reactivex import operators as ops 6 | from reactivex.internal import curry_flip 7 | 8 | _T = TypeVar("_T") 9 | 10 | 11 | @curry_flip 12 | def delay_subscription_( 13 | source: Observable[_T], 14 | duetime: typing.AbsoluteOrRelativeTime, 15 | scheduler: abc.SchedulerBase | None = None, 16 | ) -> Observable[_T]: 17 | """Time shifts the observable sequence by delaying the subscription. 18 | 19 | Examples: 20 | >>> source.pipe(delay_subscription(5)) 21 | >>> delay_subscription(5)(source) 22 | 23 | Args: 24 | source: Source subscription to delay. 25 | duetime: Time to delay subscription. 26 | scheduler: Scheduler to use for timing. 27 | 28 | Returns: 29 | Time-shifted sequence. 30 | """ 31 | 32 | def mapper(_: Any) -> Observable[_T]: 33 | return reactivex.empty() 34 | 35 | return source.pipe( 36 | ops.delay_with_mapper(reactivex.timer(duetime, scheduler=scheduler), mapper) 37 | ) 38 | 39 | 40 | __all__ = ["delay_subscription_"] 41 | -------------------------------------------------------------------------------- /reactivex/operators/_withlatestfrom.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | import reactivex 4 | from reactivex import Observable 5 | from reactivex.internal import curry_flip 6 | 7 | 8 | @curry_flip 9 | def with_latest_from_( 10 | source: Observable[Any], 11 | *sources: Observable[Any], 12 | ) -> Observable[Any]: 13 | """With latest from operator. 14 | 15 | Merges the specified observable sequences into one observable 16 | sequence by creating a tuple only when the first 17 | observable sequence produces an element. The observables can be 18 | passed either as seperate arguments or as a list. 19 | 20 | Examples: 21 | >>> res = source.pipe(with_latest_from(obs1)) 22 | >>> res = source.pipe(with_latest_from(obs1, obs2, obs3)) 23 | >>> res = with_latest_from(obs1)(source) 24 | 25 | Args: 26 | source: Source observable. 27 | *sources: Additional observables to combine with. 28 | 29 | Returns: 30 | An observable sequence containing the result of combining 31 | elements of the sources into a tuple. 32 | """ 33 | return reactivex.with_latest_from(source, *sources) 34 | 35 | 36 | __all__ = ["with_latest_from_"] 37 | -------------------------------------------------------------------------------- /tests/test_observable/test_flatmap_async.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import unittest 3 | 4 | from reactivex import operators as ops 5 | from reactivex.scheduler.eventloop import AsyncIOScheduler 6 | from reactivex.subject import Subject 7 | 8 | 9 | class TestFlatMapAsync(unittest.TestCase): 10 | def test_flat_map_async(self): 11 | actual_next = None 12 | loop = asyncio.new_event_loop() 13 | scheduler = AsyncIOScheduler(loop=loop) 14 | 15 | def mapper(i: int): 16 | async def _mapper(i: int): 17 | return i + 1 18 | 19 | return asyncio.ensure_future(_mapper(i)) 20 | 21 | def on_next(i: int): 22 | nonlocal actual_next 23 | actual_next = i 24 | 25 | def on_error(ex): 26 | print("Error", ex) 27 | 28 | async def test_flat_map(): 29 | x: Subject[int] = Subject() 30 | x.pipe(ops.flat_map(mapper)).subscribe( 31 | on_next, on_error, scheduler=scheduler 32 | ) 33 | x.on_next(10) 34 | await asyncio.sleep(0.1) 35 | 36 | loop.run_until_complete(test_flat_map()) 37 | assert actual_next == 11 38 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | The MIT License 2 | =============== 3 | 4 | Copyright 2013-2022, Dag Brattli, Microsoft Corp., and Contributors. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /reactivex/operators/_maxby.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from typing import TypeVar 3 | 4 | from reactivex import Observable, typing 5 | from reactivex.internal.basic import default_sub_comparer 6 | 7 | from ._minby import extrema_by 8 | 9 | _T = TypeVar("_T") 10 | _TKey = TypeVar("_TKey") 11 | 12 | 13 | def max_by_( 14 | key_mapper: typing.Mapper[_T, _TKey], 15 | comparer: typing.SubComparer[_TKey] | None = None, 16 | ) -> Callable[[Observable[_T]], Observable[list[_T]]]: 17 | cmp = comparer or default_sub_comparer 18 | 19 | def max_by(source: Observable[_T]) -> Observable[list[_T]]: 20 | """Partially applied max_by operator. 21 | 22 | Returns the elements in an observable sequence with the maximum 23 | key value. 24 | 25 | Examples: 26 | >>> res = max_by(source) 27 | 28 | Args: 29 | source: The source observable sequence to. 30 | 31 | Returns: 32 | An observable sequence containing a list of zero or more 33 | elements that have a maximum key value. 34 | """ 35 | return extrema_by(source, key_mapper, cmp) 36 | 37 | return max_by 38 | 39 | 40 | __all__ = ["max_by_"] 41 | -------------------------------------------------------------------------------- /reactivex/operators/_all.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from reactivex import Observable 4 | from reactivex import operators as ops 5 | from reactivex.internal import curry_flip 6 | from reactivex.typing import Predicate 7 | 8 | _T = TypeVar("_T") 9 | 10 | 11 | @curry_flip 12 | def all_(source: Observable[_T], predicate: Predicate[_T]) -> Observable[bool]: 13 | """Determines whether all elements of an observable sequence satisfy a condition. 14 | 15 | Examples: 16 | >>> result = source.pipe(all(lambda x: x > 0)) 17 | >>> result = all(lambda x: x > 0)(source) 18 | 19 | Args: 20 | source: The source observable sequence. 21 | predicate: A function to test each element for a condition. 22 | 23 | Returns: 24 | An observable sequence containing a single element determining 25 | whether all elements in the source sequence pass the test. 26 | """ 27 | 28 | def filter_fn(v: _T): 29 | return not predicate(v) 30 | 31 | def mapping(b: bool) -> bool: 32 | return not b 33 | 34 | return source.pipe( 35 | ops.filter(filter_fn), 36 | ops.some(), 37 | ops.map(mapping), 38 | ) 39 | 40 | 41 | __all__ = ["all_"] 42 | -------------------------------------------------------------------------------- /reactivex/operators/_combinelatest.py: -------------------------------------------------------------------------------- 1 | from typing import Any, cast 2 | 3 | import reactivex 4 | from reactivex import Observable 5 | from reactivex.internal import curry_flip 6 | 7 | 8 | @curry_flip 9 | def combine_latest_( 10 | source: Observable[Any], 11 | *others: Observable[Any], 12 | ) -> Observable[tuple[Any, ...]]: 13 | """Merges the specified observable sequences into one 14 | observable sequence by creating a tuple whenever any 15 | of the observable sequences produces an element. 16 | 17 | Examples: 18 | >>> result = source.pipe(combine_latest(other1, other2)) 19 | >>> result = combine_latest(other1, other2)(source) 20 | 21 | Args: 22 | source: The source observable sequence. 23 | others: Additional observable sequences to combine. 24 | 25 | Returns: 26 | An observable sequence containing the result of combining 27 | elements of the sources into a tuple. 28 | """ 29 | 30 | sources: tuple[Observable[Any], ...] = (source, *others) 31 | 32 | ret: Observable[tuple[Any, ...]] = cast( 33 | Observable[tuple[Any, ...]], reactivex.combine_latest(*sources) 34 | ) 35 | return ret 36 | 37 | 38 | __all__ = ["combine_latest_"] 39 | -------------------------------------------------------------------------------- /reactivex/operators/_count.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from reactivex import Observable 4 | from reactivex import operators as ops 5 | from reactivex.internal import curry_flip 6 | from reactivex.typing import Predicate 7 | 8 | _T = TypeVar("_T") 9 | 10 | 11 | @curry_flip 12 | def count_( 13 | source: Observable[_T], 14 | predicate: Predicate[_T] | None = None, 15 | ) -> Observable[int]: 16 | """Returns an observable sequence containing a single element with the 17 | number of elements in the source sequence. 18 | 19 | Examples: 20 | >>> result = source.pipe(count()) 21 | >>> result = count()(source) 22 | >>> result = source.pipe(count(lambda x: x > 5)) 23 | 24 | Args: 25 | source: The source observable. 26 | predicate: Optional predicate to filter elements before counting. 27 | 28 | Returns: 29 | An observable sequence containing a single element with the count. 30 | """ 31 | if predicate: 32 | return source.pipe( 33 | ops.filter(predicate), 34 | ops.count(), 35 | ) 36 | 37 | def reducer(n: int, _: _T) -> int: 38 | return n + 1 39 | 40 | return source.pipe(ops.reduce(reducer, seed=0)) 41 | 42 | 43 | __all__ = ["count_"] 44 | -------------------------------------------------------------------------------- /reactivex/operators/_bufferwithtimeorcount.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from reactivex import Observable, abc, compose, typing 4 | from reactivex import operators as ops 5 | from reactivex.internal import curry_flip 6 | 7 | _T = TypeVar("_T") 8 | 9 | 10 | @curry_flip 11 | def buffer_with_time_or_count_( 12 | source: Observable[_T], 13 | timespan: typing.RelativeTime, 14 | count: int, 15 | scheduler: abc.SchedulerBase | None = None, 16 | ) -> Observable[list[_T]]: 17 | """Buffers elements based on timing and count information. 18 | 19 | Examples: 20 | >>> source.pipe(buffer_with_time_or_count(1.0, 10)) 21 | >>> buffer_with_time_or_count(1.0, 10)(source) 22 | 23 | Args: 24 | source: Source observable to buffer. 25 | timespan: Maximum time length of each buffer. 26 | count: Maximum element count of each buffer. 27 | scheduler: Scheduler to use for timing. 28 | 29 | Returns: 30 | An observable sequence of buffers. 31 | """ 32 | return source.pipe( 33 | compose( 34 | ops.window_with_time_or_count(timespan, count, scheduler), 35 | ops.flat_map(ops.to_iterable()), 36 | ) 37 | ) 38 | 39 | 40 | __all__ = ["buffer_with_time_or_count_"] 41 | -------------------------------------------------------------------------------- /reactivex/observable/start.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from typing import TypeVar 3 | 4 | from reactivex import Observable, abc, to_async 5 | 6 | _T = TypeVar("_T") 7 | 8 | 9 | def start_( 10 | func: Callable[[], _T], scheduler: abc.SchedulerBase | None = None 11 | ) -> Observable[_T]: 12 | """Invokes the specified function asynchronously on the specified 13 | scheduler, surfacing the result through an observable sequence. 14 | 15 | Example: 16 | >>> res = reactivex.start(lambda: pprint('hello')) 17 | >>> res = reactivex.start(lambda: pprint('hello'), rx.Scheduler.timeout) 18 | 19 | Args: 20 | func: Function to run asynchronously. 21 | scheduler: [Optional] Scheduler to run the function on. If 22 | not specified, defaults to Scheduler.timeout. 23 | 24 | Remarks: 25 | The function is called immediately, not during the subscription 26 | of the resulting sequence. Multiple subscriptions to the 27 | resulting sequence can observe the function's result. 28 | 29 | Returns: 30 | An observable sequence exposing the function's result value, 31 | or an exception. 32 | """ 33 | 34 | return to_async(func, scheduler)() 35 | 36 | 37 | __all__ = ["start_"] 38 | -------------------------------------------------------------------------------- /reactivex/operators/_dematerialize.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from reactivex import Notification, Observable, abc 4 | from reactivex.internal import curry_flip 5 | 6 | _T = TypeVar("_T") 7 | 8 | 9 | @curry_flip 10 | def dematerialize_(source: Observable[Notification[_T]]) -> Observable[_T]: 11 | """Dematerializes the explicit notification values of an 12 | observable sequence as implicit notifications. 13 | 14 | Examples: 15 | >>> res = source.pipe(dematerialize()) 16 | >>> res = dematerialize()(source) 17 | 18 | Args: 19 | source: Source observable with notifications to dematerialize. 20 | 21 | Returns: 22 | An observable sequence exhibiting the behavior 23 | corresponding to the source sequence's notification values. 24 | """ 25 | 26 | def subscribe( 27 | observer: abc.ObserverBase[_T], 28 | scheduler: abc.SchedulerBase | None = None, 29 | ): 30 | def on_next(value: Notification[_T]) -> None: 31 | return value.accept(observer) 32 | 33 | return source.subscribe( 34 | on_next, observer.on_error, observer.on_completed, scheduler=scheduler 35 | ) 36 | 37 | return Observable(subscribe) 38 | 39 | 40 | __all__ = ["dematerialize_"] 41 | -------------------------------------------------------------------------------- /reactivex/operators/_whiledo.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | from asyncio import Future 3 | from collections.abc import Callable 4 | from typing import TypeVar, Union 5 | 6 | import reactivex 7 | from reactivex import Observable 8 | from reactivex.internal.utils import infinite 9 | from reactivex.typing import Predicate 10 | 11 | _T = TypeVar("_T") 12 | 13 | 14 | def while_do_( 15 | condition: Predicate[Observable[_T]], 16 | ) -> Callable[[Observable[_T]], Observable[_T]]: 17 | def while_do(source: Union[Observable[_T], "Future[_T]"]) -> Observable[_T]: 18 | """Repeats source as long as condition holds emulating a while 19 | loop. 20 | 21 | Args: 22 | source: The observable sequence that will be run if the 23 | condition function returns true. 24 | 25 | Returns: 26 | An observable sequence which is repeated as long as the 27 | condition holds. 28 | """ 29 | if isinstance(source, Future): 30 | obs = reactivex.from_future(source) 31 | else: 32 | obs = source 33 | it = itertools.takewhile(condition, (obs for _ in infinite())) 34 | return reactivex.concat_with_iterable(it) 35 | 36 | return while_do 37 | 38 | 39 | __all__ = ["while_do_"] 40 | -------------------------------------------------------------------------------- /reactivex/testing/recorded.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Any, Generic, TypeVar, Union, cast 2 | 3 | from reactivex import Notification 4 | 5 | if TYPE_CHECKING: 6 | from .reactivetest import OnErrorPredicate, OnNextPredicate 7 | 8 | 9 | _T = TypeVar("_T") 10 | 11 | 12 | class Recorded(Generic[_T]): 13 | def __init__( 14 | self, 15 | time: int, 16 | value: Union[Notification[_T], "OnNextPredicate[_T]", "OnErrorPredicate[_T]"], 17 | # comparer: Optional[typing.Comparer[_T]] = None, 18 | ): 19 | self.time = time 20 | self.value = value 21 | # self.comparer = comparer or default_comparer 22 | 23 | def __eq__(self, other: Any) -> bool: 24 | """Returns true if a recorded value matches another recorded value""" 25 | 26 | if isinstance(other, Recorded): 27 | other_ = cast(Recorded[_T], other) 28 | time_match = self.time == other_.time 29 | if not time_match: 30 | return False 31 | return self.value == other_.value 32 | 33 | return False 34 | 35 | equals = __eq__ 36 | 37 | def __repr__(self) -> str: 38 | return str(self) 39 | 40 | def __str__(self) -> str: 41 | return f"{self.value}@{self.time}" 42 | -------------------------------------------------------------------------------- /reactivex/disposable/disposable.py: -------------------------------------------------------------------------------- 1 | from threading import RLock 2 | 3 | from reactivex import typing 4 | from reactivex.abc import DisposableBase 5 | from reactivex.internal import noop 6 | from reactivex.typing import Action 7 | 8 | 9 | class Disposable(DisposableBase): 10 | """Main disposable class""" 11 | 12 | def __init__(self, action: typing.Action | None = None) -> None: 13 | """Creates a disposable object that invokes the specified 14 | action when disposed. 15 | 16 | Args: 17 | action: Action to run during the first call to dispose. 18 | The action is guaranteed to be run at most once. 19 | 20 | Returns: 21 | The disposable object that runs the given action upon 22 | disposal. 23 | """ 24 | 25 | self.is_disposed = False 26 | self.action: Action = action or noop 27 | 28 | self.lock = RLock() 29 | 30 | super().__init__() 31 | 32 | def dispose(self) -> None: 33 | """Performs the task of cleaning up resources.""" 34 | 35 | dispose = False 36 | with self.lock: 37 | if not self.is_disposed: 38 | dispose = True 39 | self.is_disposed = True 40 | 41 | if dispose: 42 | self.action() 43 | -------------------------------------------------------------------------------- /.github/lock.yml: -------------------------------------------------------------------------------- 1 | # Configuration for Lock Threads - https://github.com/dessant/lock-threads 2 | 3 | # Number of days of inactivity before a closed issue or pull request is locked 4 | daysUntilLock: 365 5 | 6 | # Skip issues and pull requests created before a given timestamp. Timestamp must 7 | # follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable 8 | skipCreatedBefore: false 9 | 10 | # Issues and pull requests with these labels will be ignored. Set to `[]` to disable 11 | exemptLabels: [] 12 | 13 | # Label to add before locking, such as `outdated`. Set to `false` to disable 14 | lockLabel: false 15 | 16 | # Comment to post before locking. Set to `false` to disable 17 | lockComment: > 18 | This thread has been automatically locked since there has not been 19 | any recent activity after it was closed. Please open a new issue for 20 | related bugs. 21 | 22 | # Assign `resolved` as the reason for locking. Set to `false` to disable 23 | setLockReason: true 24 | 25 | # Limit to only `issues` or `pulls` 26 | # only: issues 27 | 28 | # Optionally, specify configuration settings just for `issues` or `pulls` 29 | # issues: 30 | # exemptLabels: 31 | # - help-wanted 32 | # lockLabel: outdated 33 | 34 | # pulls: 35 | # daysUntilLock: 30 36 | 37 | # Repository to extend settings from 38 | # _extends: repo 39 | -------------------------------------------------------------------------------- /reactivex/operators/_repeat.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | import reactivex 4 | from reactivex import Observable 5 | from reactivex.internal import curry_flip 6 | from reactivex.internal.utils import infinite 7 | 8 | _T = TypeVar("_T") 9 | 10 | 11 | @curry_flip 12 | def repeat_( 13 | source: Observable[_T], 14 | repeat_count: int | None = None, 15 | ) -> Observable[_T]: 16 | """Repeats the observable sequence a specified number of times. 17 | If the repeat count is not specified, the sequence repeats 18 | indefinitely. 19 | 20 | Examples: 21 | >>> result = source.pipe(repeat()) 22 | >>> result = repeat()(source) 23 | >>> result = source.pipe(repeat(42)) 24 | 25 | Args: 26 | source: The observable source to repeat. 27 | repeat_count: Optional number of times to repeat the sequence. 28 | If not provided, repeats indefinitely. 29 | 30 | Returns: 31 | The observable sequence producing the elements of the given 32 | sequence repeatedly. 33 | """ 34 | 35 | if repeat_count is None: 36 | gen = infinite() 37 | else: 38 | gen = range(repeat_count) 39 | 40 | return reactivex.defer( 41 | lambda _: reactivex.concat_with_iterable(source for _ in gen) 42 | ) 43 | 44 | 45 | __all = ["repeat"] 46 | -------------------------------------------------------------------------------- /reactivex/disposable/scheduleddisposable.py: -------------------------------------------------------------------------------- 1 | from threading import RLock 2 | from typing import Any 3 | 4 | from reactivex import abc 5 | 6 | from .singleassignmentdisposable import SingleAssignmentDisposable 7 | 8 | 9 | class ScheduledDisposable(abc.DisposableBase): 10 | """Represents a disposable resource whose disposal invocation will 11 | be scheduled on the specified Scheduler""" 12 | 13 | def __init__( 14 | self, scheduler: abc.SchedulerBase, disposable: abc.DisposableBase 15 | ) -> None: 16 | """Initializes a new instance of the ScheduledDisposable class 17 | that uses a Scheduler on which to dispose the disposable.""" 18 | 19 | self.scheduler = scheduler 20 | self.disposable = SingleAssignmentDisposable() 21 | self.disposable.disposable = disposable 22 | self.lock = RLock() 23 | 24 | super().__init__() 25 | 26 | @property 27 | def is_disposed(self) -> bool: 28 | return self.disposable.is_disposed 29 | 30 | def dispose(self) -> None: 31 | """Disposes the wrapped disposable on the provided scheduler.""" 32 | 33 | def action(scheduler: abc.SchedulerBase, state: Any) -> None: 34 | """Scheduled dispose action""" 35 | 36 | self.disposable.dispose() 37 | 38 | self.scheduler.schedule(action) 39 | -------------------------------------------------------------------------------- /reactivex/operators/_retry.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | import reactivex 4 | from reactivex import Observable 5 | from reactivex.internal import curry_flip 6 | from reactivex.internal.utils import infinite 7 | 8 | _T = TypeVar("_T") 9 | 10 | 11 | @curry_flip 12 | def retry_( 13 | source: Observable[_T], 14 | retry_count: int | None = None, 15 | ) -> Observable[_T]: 16 | """Repeats the source observable sequence the specified number of 17 | times or until it successfully terminates. If the retry count is 18 | not specified, it retries indefinitely. 19 | 20 | Examples: 21 | >>> result = source.pipe(retry()) 22 | >>> result = retry()(source) 23 | >>> result = source.pipe(retry(42)) 24 | 25 | Args: 26 | source: The source observable sequence. 27 | retry_count: Optional number of times to retry the sequence. 28 | If not provided, retry the sequence indefinitely. 29 | 30 | Returns: 31 | An observable sequence producing the elements of the given 32 | sequence repeatedly until it terminates successfully. 33 | """ 34 | 35 | if retry_count is None: 36 | gen = infinite() 37 | else: 38 | gen = range(retry_count) 39 | 40 | return reactivex.catch_with_iterable(source for _ in gen) 41 | 42 | 43 | __all__ = ["retry_"] 44 | -------------------------------------------------------------------------------- /reactivex/operators/_max.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from typing import TypeVar, cast 3 | 4 | from reactivex import Observable 5 | from reactivex import operators as ops 6 | from reactivex.internal import curry_flip 7 | from reactivex.internal.basic import identity 8 | from reactivex.typing import Comparer 9 | 10 | from ._min import first_only 11 | 12 | _T = TypeVar("_T") 13 | 14 | 15 | @curry_flip 16 | def max_( 17 | source: Observable[_T], 18 | comparer: Comparer[_T] | None = None, 19 | ) -> Observable[_T]: 20 | """Returns the maximum value in an observable sequence. 21 | 22 | Returns the maximum value in an observable sequence according to 23 | the specified comparer. 24 | 25 | Examples: 26 | >>> result = source.pipe(max()) 27 | >>> result = max()(source) 28 | >>> result = source.pipe(max(lambda x, y: x.value - y.value)) 29 | 30 | Args: 31 | source: The source observable. 32 | comparer: Optional comparer used to compare elements. 33 | 34 | Returns: 35 | An observable sequence containing a single element with the 36 | maximum element in the source sequence. 37 | """ 38 | return source.pipe( 39 | ops.max_by(cast(Callable[[_T], _T], identity), comparer), 40 | ops.map(first_only), 41 | ) 42 | 43 | 44 | __all__ = ["max_"] 45 | -------------------------------------------------------------------------------- /reactivex/operators/_bufferwithtime.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from reactivex import Observable, abc, compose, typing 4 | from reactivex import operators as ops 5 | from reactivex.internal import curry_flip 6 | 7 | _T = TypeVar("_T") 8 | 9 | 10 | @curry_flip 11 | def buffer_with_time_( 12 | source: Observable[_T], 13 | timespan: typing.RelativeTime, 14 | timeshift: typing.RelativeTime | None = None, 15 | scheduler: abc.SchedulerBase | None = None, 16 | ) -> Observable[list[_T]]: 17 | """Buffers elements based on timing information. 18 | 19 | Examples: 20 | >>> source.pipe(buffer_with_time(1.0)) 21 | >>> source.pipe(buffer_with_time(1.0, 0.5)) 22 | >>> buffer_with_time(1.0)(source) 23 | 24 | Args: 25 | source: Source observable to buffer. 26 | timespan: Length of each buffer. 27 | timeshift: Interval between creation of consecutive buffers. 28 | scheduler: Scheduler to use for timing. 29 | 30 | Returns: 31 | An observable sequence of buffers. 32 | """ 33 | if not timeshift: 34 | timeshift = timespan 35 | 36 | return source.pipe( 37 | compose( 38 | ops.window_with_time(timespan, timeshift, scheduler), 39 | ops.flat_map(ops.to_list()), 40 | ) 41 | ) 42 | 43 | 44 | __all__ = ["buffer_with_time_"] 45 | -------------------------------------------------------------------------------- /reactivex/operators/_toset.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from reactivex import Observable, abc 4 | from reactivex.internal import curry_flip 5 | 6 | _T = TypeVar("_T") 7 | 8 | 9 | @curry_flip 10 | def to_set_(source: Observable[_T]) -> Observable[set[_T]]: 11 | """Converts the observable sequence to a set. 12 | 13 | Returns an observable sequence with a single value of a set 14 | containing the values from the observable sequence. 15 | 16 | Examples: 17 | >>> res = source.pipe(to_set()) 18 | >>> res = to_set()(source) 19 | 20 | Args: 21 | source: Source observable. 22 | 23 | Returns: 24 | An observable sequence with a single value of a set 25 | containing the values from the observable sequence. 26 | """ 27 | 28 | def subscribe( 29 | observer: abc.ObserverBase[set[_T]], 30 | scheduler: abc.SchedulerBase | None = None, 31 | ) -> abc.DisposableBase: 32 | s: set[_T] = set() 33 | 34 | def on_completed() -> None: 35 | nonlocal s 36 | observer.on_next(s) 37 | s = set() 38 | observer.on_completed() 39 | 40 | return source.subscribe( 41 | s.add, observer.on_error, on_completed, scheduler=scheduler 42 | ) 43 | 44 | return Observable(subscribe) 45 | 46 | 47 | __all__ = ["to_set_"] 48 | -------------------------------------------------------------------------------- /reactivex/operators/_contains.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from reactivex import Observable, typing 4 | from reactivex import operators as ops 5 | from reactivex.internal import curry_flip 6 | from reactivex.internal.basic import default_comparer 7 | 8 | _T = TypeVar("_T") 9 | 10 | 11 | @curry_flip 12 | def contains_( 13 | source: Observable[_T], 14 | value: _T, 15 | comparer: typing.Comparer[_T] | None = None, 16 | ) -> Observable[bool]: 17 | """Determines whether an observable sequence contains a specified element. 18 | 19 | Examples: 20 | >>> result = source.pipe(contains(42)) 21 | >>> result = contains(42)(source) 22 | >>> result = source.pipe(contains(42, custom_comparer)) 23 | 24 | Args: 25 | source: The source observable sequence. 26 | value: The value to locate in the source sequence. 27 | comparer: Optional equality comparer to compare elements. 28 | 29 | Returns: 30 | An observable sequence containing a single element determining 31 | whether the source sequence contains the specified value. 32 | """ 33 | comparer_ = comparer or default_comparer 34 | 35 | def predicate(v: _T) -> bool: 36 | return comparer_(v, value) 37 | 38 | return source.pipe( 39 | ops.filter(predicate), 40 | ops.some(), 41 | ) 42 | 43 | 44 | __all__ = ["contains_"] 45 | -------------------------------------------------------------------------------- /reactivex/operators/_toiterable.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from reactivex import Observable, abc 4 | from reactivex.internal import curry_flip 5 | 6 | _T = TypeVar("_T") 7 | 8 | 9 | @curry_flip 10 | def to_iterable_(source: Observable[_T]) -> Observable[list[_T]]: 11 | """Creates an iterable from an observable sequence. 12 | 13 | Examples: 14 | >>> res = source.pipe(to_iterable()) 15 | >>> res = to_iterable()(source) 16 | 17 | Args: 18 | source: Source observable. 19 | 20 | Returns: 21 | An observable sequence containing a single element with an 22 | iterable containing all the elements of the source 23 | sequence. 24 | """ 25 | 26 | def subscribe( 27 | observer: abc.ObserverBase[list[_T]], 28 | scheduler: abc.SchedulerBase | None = None, 29 | ): 30 | nonlocal source 31 | 32 | queue: list[_T] = [] 33 | 34 | def on_next(item: _T): 35 | queue.append(item) 36 | 37 | def on_completed(): 38 | nonlocal queue 39 | observer.on_next(queue) 40 | queue = [] 41 | observer.on_completed() 42 | 43 | return source.subscribe( 44 | on_next, observer.on_error, on_completed, scheduler=scheduler 45 | ) 46 | 47 | return Observable(subscribe) 48 | 49 | 50 | __all__ = ["to_iterable_"] 51 | -------------------------------------------------------------------------------- /reactivex/abc/observer.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from collections.abc import Callable 3 | from typing import Generic, TypeVar 4 | 5 | _T = TypeVar("_T") 6 | _T_in = TypeVar("_T_in", contravariant=True) 7 | 8 | OnNext = Callable[[_T], None] 9 | OnError = Callable[[Exception], None] 10 | OnCompleted = Callable[[], None] 11 | 12 | 13 | class ObserverBase(Generic[_T_in], ABC): 14 | """Observer abstract base class 15 | 16 | An Observer is the entity that receives all emissions of a 17 | subscribed Observable. 18 | """ 19 | 20 | __slots__ = () 21 | 22 | @abstractmethod 23 | def on_next(self, value: _T_in) -> None: 24 | """Notifies the observer of a new element in the sequence. 25 | 26 | Args: 27 | value: The received element. 28 | """ 29 | 30 | raise NotImplementedError 31 | 32 | @abstractmethod 33 | def on_error(self, error: Exception) -> None: 34 | """Notifies the observer that an exception has occurred. 35 | 36 | Args: 37 | error: The error that has occurred. 38 | """ 39 | 40 | raise NotImplementedError 41 | 42 | @abstractmethod 43 | def on_completed(self) -> None: 44 | """Notifies the observer of the end of the sequence.""" 45 | 46 | raise NotImplementedError 47 | 48 | 49 | __all__ = ["ObserverBase", "OnNext", "OnError", "OnCompleted"] 50 | -------------------------------------------------------------------------------- /reactivex/scheduler/threadpoolscheduler.py: -------------------------------------------------------------------------------- 1 | from concurrent.futures import Future, ThreadPoolExecutor 2 | from typing import Any 3 | 4 | from reactivex import abc, typing 5 | 6 | from .newthreadscheduler import NewThreadScheduler 7 | 8 | 9 | class ThreadPoolScheduler(NewThreadScheduler): 10 | """A scheduler that schedules work via the thread pool.""" 11 | 12 | class ThreadPoolThread(abc.StartableBase): 13 | """Wraps a concurrent future as a thread.""" 14 | 15 | def __init__( 16 | self, executor: ThreadPoolExecutor, target: typing.StartableTarget 17 | ): 18 | self.executor: ThreadPoolExecutor = executor 19 | self.target: typing.StartableTarget = target 20 | self.future: Future[Any] | None = None 21 | 22 | def start(self) -> None: 23 | self.future = self.executor.submit(self.target) 24 | 25 | def cancel(self) -> None: 26 | if self.future: 27 | self.future.cancel() 28 | 29 | def __init__(self, max_workers: int | None = None) -> None: 30 | self.executor: ThreadPoolExecutor = ThreadPoolExecutor(max_workers=max_workers) 31 | 32 | def thread_factory( 33 | target: typing.StartableTarget, 34 | ) -> ThreadPoolScheduler.ThreadPoolThread: 35 | return self.ThreadPoolThread(self.executor, target) 36 | 37 | super().__init__(thread_factory) 38 | -------------------------------------------------------------------------------- /docs/rationale.rst: -------------------------------------------------------------------------------- 1 | .. Rationale 2 | 3 | Rationale 4 | ========== 5 | 6 | Reactive Extensions for Python (RxPY) is a set of libraries for composing 7 | asynchronous and event-based programs using observable sequences and pipable 8 | query operators in Python. Using Rx, developers represent asynchronous data 9 | streams with Observables, query asynchronous data streams using operators, and 10 | parameterize concurrency in data/event streams using Schedulers. 11 | 12 | Using Rx, you can represent multiple asynchronous data streams (that come from 13 | diverse sources, e.g., stock quote, Tweets, computer events, web service 14 | requests, etc.), and subscribe to the event stream using the Observer object. 15 | The Observable notifies the subscribed Observer instance whenever an event 16 | occurs. You can put various transformations in-between the source Observable and 17 | the consuming Observer as well. 18 | 19 | Because Observable sequences are data streams, you can query them using standard 20 | query operators implemented as functions that can be chained with the pipe 21 | operator. Thus you can filter, map, reduce, compose and perform time-based 22 | operations on multiple events easily by using these operators. In 23 | addition, there are a number of other reactive stream specific operators that 24 | allow powerful queries to be written. Cancellation, exceptions, and 25 | synchronization are also handled gracefully by using dedicated operators. 26 | -------------------------------------------------------------------------------- /reactivex/operators/_pluck.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from typing import Any, TypeVar 3 | 4 | from reactivex import Observable 5 | from reactivex import operators as ops 6 | 7 | _TKey = TypeVar("_TKey") 8 | _TValue = TypeVar("_TValue") 9 | 10 | 11 | def pluck_( 12 | key: _TKey, 13 | ) -> Callable[[Observable[dict[_TKey, _TValue]]], Observable[_TValue]]: 14 | """Retrieves the value of a specified key using dict-like access (as in 15 | element[key]) from all elements in the Observable sequence. 16 | 17 | Args: 18 | key: The key to pluck. 19 | 20 | Returns a new Observable {Observable} sequence of key values. 21 | 22 | To pluck an attribute of each element, use pluck_attr. 23 | """ 24 | 25 | def mapper(x: dict[_TKey, _TValue]) -> _TValue: 26 | return x[key] 27 | 28 | return ops.map(mapper) 29 | 30 | 31 | def pluck_attr_(prop: str) -> Callable[[Observable[Any]], Observable[Any]]: 32 | """Retrieves the value of a specified property (using getattr) from 33 | all elements in the Observable sequence. 34 | 35 | Args: 36 | property: The property to pluck. 37 | 38 | Returns a new Observable {Observable} sequence of property values. 39 | 40 | To pluck values using dict-like access (as in element[key]) on each 41 | element, use pluck. 42 | """ 43 | 44 | return ops.map(lambda x: getattr(x, prop)) 45 | 46 | 47 | __all__ = ["pluck_", "pluck_attr_"] 48 | -------------------------------------------------------------------------------- /reactivex/operators/_observeon.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from reactivex import Observable, abc 4 | from reactivex.internal import curry_flip 5 | from reactivex.observer import ObserveOnObserver 6 | 7 | _T = TypeVar("_T") 8 | 9 | 10 | @curry_flip 11 | def observe_on_( 12 | source: Observable[_T], 13 | scheduler: abc.SchedulerBase, 14 | ) -> Observable[_T]: 15 | """Wraps the source sequence in order to run its observer 16 | callbacks on the specified scheduler. 17 | 18 | This only invokes observer callbacks on a scheduler. In case 19 | the subscription and/or unsubscription actions have 20 | side-effects that require to be run on a scheduler, use 21 | subscribe_on. 22 | 23 | Examples: 24 | >>> res = source.pipe(observe_on(scheduler)) 25 | >>> res = observe_on(scheduler)(source) 26 | 27 | Args: 28 | source: Source observable. 29 | scheduler: Scheduler to observe on. 30 | 31 | Returns: 32 | Returns the source sequence whose observations happen on 33 | the specified scheduler. 34 | """ 35 | 36 | def subscribe( 37 | observer: abc.ObserverBase[_T], 38 | subscribe_scheduler: abc.SchedulerBase | None = None, 39 | ): 40 | return source.subscribe( 41 | ObserveOnObserver(scheduler, observer), scheduler=subscribe_scheduler 42 | ) 43 | 44 | return Observable(subscribe) 45 | 46 | 47 | __all__ = ["observe_on_"] 48 | -------------------------------------------------------------------------------- /examples/autocomplete/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Rx for Python Rocks! 10 | 11 | 12 | 13 | 14 |
    15 | 19 |
    20 |
    21 |
    22 | 23 | 24 |
    25 | 28 |
    29 |
    30 |
    31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /reactivex/observable/groupedobservable.py: -------------------------------------------------------------------------------- 1 | from typing import Generic, TypeVar 2 | 3 | from reactivex import abc 4 | from reactivex.disposable import CompositeDisposable, Disposable, RefCountDisposable 5 | 6 | from .observable import Observable 7 | 8 | _T = TypeVar("_T") 9 | _TKey = TypeVar("_TKey") 10 | 11 | 12 | class GroupedObservable(Generic[_TKey, _T], Observable[_T]): 13 | def __init__( 14 | self, 15 | key: _TKey, 16 | underlying_observable: Observable[_T], 17 | merged_disposable: RefCountDisposable | None = None, 18 | ): 19 | super().__init__() 20 | self.key = key 21 | 22 | def subscribe( 23 | observer: abc.ObserverBase[_T], 24 | scheduler: abc.SchedulerBase | None = None, 25 | ) -> abc.DisposableBase: 26 | return CompositeDisposable( 27 | merged_disposable.disposable if merged_disposable else Disposable(), 28 | underlying_observable.subscribe(observer, scheduler=scheduler), 29 | ) 30 | 31 | self.underlying_observable: Observable[_T] = ( 32 | underlying_observable if not merged_disposable else Observable(subscribe) 33 | ) 34 | 35 | def _subscribe_core( 36 | self, 37 | observer: abc.ObserverBase[_T], 38 | scheduler: abc.SchedulerBase | None = None, 39 | ) -> abc.DisposableBase: 40 | return self.underlying_observable.subscribe(observer, scheduler=scheduler) 41 | -------------------------------------------------------------------------------- /tests/test_observable/test_empty.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from reactivex import empty 4 | from reactivex.testing import ReactiveTest, TestScheduler 5 | 6 | on_next = ReactiveTest.on_next 7 | on_completed = ReactiveTest.on_completed 8 | on_error = ReactiveTest.on_error 9 | subscribe = ReactiveTest.subscribe 10 | subscribed = ReactiveTest.subscribed 11 | disposed = ReactiveTest.disposed 12 | created = ReactiveTest.created 13 | 14 | 15 | class RxException(Exception): 16 | pass 17 | 18 | 19 | # Helper function for raising exceptions within lambdas 20 | def _raise(ex): 21 | raise RxException(ex) 22 | 23 | 24 | class TestEmpty(unittest.TestCase): 25 | def test_empty_basic(self): 26 | scheduler = TestScheduler() 27 | 28 | def factory(): 29 | return empty() 30 | 31 | results = scheduler.start(factory) 32 | 33 | assert results.messages == [on_completed(200)] 34 | 35 | def test_empty_disposed(self): 36 | scheduler = TestScheduler() 37 | 38 | def factory(): 39 | return empty() 40 | 41 | results = scheduler.start(factory, disposed=200) 42 | assert results.messages == [] 43 | 44 | def test_empty_observer_throw_exception(self): 45 | scheduler = TestScheduler() 46 | xs = empty() 47 | xs.subscribe( 48 | lambda x: None, lambda ex: None, lambda: _raise("ex"), scheduler=scheduler 49 | ) 50 | 51 | with self.assertRaises(RxException): 52 | scheduler.start() 53 | -------------------------------------------------------------------------------- /tests/test_observable/test_throw.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from reactivex import throw 4 | from reactivex.testing import ReactiveTest, TestScheduler 5 | 6 | on_next = ReactiveTest.on_next 7 | on_completed = ReactiveTest.on_completed 8 | on_error = ReactiveTest.on_error 9 | subscribe = ReactiveTest.subscribe 10 | subscribed = ReactiveTest.subscribed 11 | disposed = ReactiveTest.disposed 12 | created = ReactiveTest.created 13 | 14 | 15 | class RxException(Exception): 16 | pass 17 | 18 | 19 | # Helper function for raising exceptions within lambdas 20 | def _raise(ex): 21 | raise RxException(ex) 22 | 23 | 24 | class TestThrow(unittest.TestCase): 25 | def test_throw_exception_basic(self): 26 | scheduler = TestScheduler() 27 | ex = "ex" 28 | 29 | def factory(): 30 | return throw(ex) 31 | 32 | results = scheduler.start(factory) 33 | assert results.messages == [on_error(200, ex)] 34 | 35 | def test_throw_disposed(self): 36 | scheduler = TestScheduler() 37 | 38 | def factory(): 39 | return throw("ex") 40 | 41 | results = scheduler.start(factory, disposed=200) 42 | assert results.messages == [] 43 | 44 | def test_throw_observer_throws(self): 45 | scheduler = TestScheduler() 46 | xs = throw("ex") 47 | xs.subscribe( 48 | lambda x: None, lambda ex: _raise("ex"), lambda: None, scheduler=scheduler 49 | ) 50 | 51 | self.assertRaises(RxException, scheduler.start) 52 | -------------------------------------------------------------------------------- /reactivex/operators/_materialize.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from reactivex import Observable, abc 4 | from reactivex.internal import curry_flip 5 | from reactivex.notification import Notification, OnCompleted, OnError, OnNext 6 | 7 | _T = TypeVar("_T") 8 | 9 | 10 | @curry_flip 11 | def materialize_(source: Observable[_T]) -> Observable[Notification[_T]]: 12 | """Materializes the implicit notifications of an observable 13 | sequence as explicit notification values. 14 | 15 | Examples: 16 | >>> res = source.pipe(materialize()) 17 | >>> res = materialize()(source) 18 | 19 | Args: 20 | source: Source observable to materialize. 21 | 22 | Returns: 23 | An observable sequence containing the materialized 24 | notification values from the source sequence. 25 | """ 26 | 27 | def subscribe( 28 | observer: abc.ObserverBase[Notification[_T]], 29 | scheduler: abc.SchedulerBase | None = None, 30 | ): 31 | def on_next(value: _T) -> None: 32 | observer.on_next(OnNext(value)) 33 | 34 | def on_error(error: Exception) -> None: 35 | observer.on_next(OnError(error)) 36 | observer.on_completed() 37 | 38 | def on_completed() -> None: 39 | observer.on_next(OnCompleted()) 40 | observer.on_completed() 41 | 42 | return source.subscribe(on_next, on_error, on_completed, scheduler=scheduler) 43 | 44 | return Observable(subscribe) 45 | 46 | 47 | __all__ = ["materialize_"] 48 | -------------------------------------------------------------------------------- /reactivex/operators/_finallyaction.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from typing import TypeVar 3 | 4 | from reactivex import Observable, abc, typing 5 | from reactivex.disposable import Disposable 6 | 7 | _T = TypeVar("_T") 8 | 9 | 10 | def finally_action_( 11 | action: typing.Action, 12 | ) -> Callable[[Observable[_T]], Observable[_T]]: 13 | def finally_action(source: Observable[_T]) -> Observable[_T]: 14 | """Invokes a specified action after the source observable 15 | sequence terminates gracefully or exceptionally. 16 | 17 | Example: 18 | res = finally(source) 19 | 20 | Args: 21 | source: Observable sequence. 22 | 23 | Returns: 24 | An observable sequence with the action-invoking termination 25 | behavior applied. 26 | """ 27 | 28 | def subscribe( 29 | observer: abc.ObserverBase[_T], 30 | scheduler: abc.SchedulerBase | None = None, 31 | ) -> abc.DisposableBase: 32 | try: 33 | subscription = source.subscribe(observer, scheduler=scheduler) 34 | except Exception: 35 | action() 36 | raise 37 | 38 | def dispose(): 39 | try: 40 | subscription.dispose() 41 | finally: 42 | action() 43 | 44 | return Disposable(dispose) 45 | 46 | return Observable(subscribe) 47 | 48 | return finally_action 49 | 50 | 51 | __all__ = ["finally_action_"] 52 | -------------------------------------------------------------------------------- /examples/timeflies/timeflies_tkinter.py: -------------------------------------------------------------------------------- 1 | import tkinter 2 | from tkinter import Event, Frame, Label, Tk 3 | from typing import Any 4 | 5 | import reactivex 6 | from reactivex import Observable 7 | from reactivex import operators as ops 8 | from reactivex.scheduler.mainloop import TkinterScheduler 9 | from reactivex.subject import Subject 10 | 11 | 12 | def main() -> None: 13 | root = Tk() 14 | root.title("Rx for Python rocks") 15 | scheduler = TkinterScheduler(root) 16 | 17 | mousemoves: Subject[Event[Any]] = Subject() 18 | 19 | frame = Frame(root, width=600, height=600) 20 | frame.bind("", mousemoves.on_next) 21 | 22 | text = "TIME FLIES LIKE AN ARROW" 23 | 24 | def on_next(info: tuple[tkinter.Label, "Event[Frame]", int]) -> None: 25 | label, ev, i = info 26 | label.place(x=ev.x + i * 12 + 15, y=ev.y) 27 | 28 | def label2stream( 29 | label: tkinter.Label, index: int 30 | ) -> Observable[tuple[tkinter.Label, "Event[Frame]", int]]: 31 | return mousemoves.pipe( 32 | ops.map(lambda ev: (label, ev, index)), 33 | ops.delay(index * 0.1), 34 | ) 35 | 36 | def char2label(char: str) -> Label: 37 | return Label(frame, text=char, borderwidth=0, padx=0, pady=0) 38 | 39 | reactivex.from_(text).pipe( 40 | ops.map(char2label), 41 | ops.flat_map_indexed(label2stream), 42 | ).subscribe(on_next, on_error=print, scheduler=scheduler) 43 | 44 | frame.pack() 45 | root.mainloop() 46 | 47 | 48 | if __name__ == "__main__": 49 | main() 50 | -------------------------------------------------------------------------------- /reactivex/abc/periodicscheduler.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from collections.abc import Callable 3 | from typing import TypeVar 4 | 5 | from .disposable import DisposableBase 6 | from .scheduler import RelativeTime, ScheduledAction 7 | 8 | _TState = TypeVar("_TState") # Can be anything 9 | 10 | ScheduledPeriodicAction = Callable[[_TState | None], _TState | None] 11 | ScheduledSingleOrPeriodicAction = ( 12 | ScheduledAction[_TState] | ScheduledPeriodicAction[_TState] 13 | ) 14 | 15 | 16 | class PeriodicSchedulerBase(ABC): 17 | """PeriodicScheduler abstract base class.""" 18 | 19 | __slots__ = () 20 | 21 | @abstractmethod 22 | def schedule_periodic( 23 | self, 24 | period: RelativeTime, 25 | action: ScheduledPeriodicAction[_TState], 26 | state: _TState | None = None, 27 | ) -> DisposableBase: 28 | """Schedules a periodic piece of work. 29 | 30 | Args: 31 | period: Period in seconds or timedelta for running the 32 | work periodically. 33 | action: Action to be executed. 34 | state: [Optional] Initial state passed to the action upon 35 | the first iteration. 36 | 37 | Returns: 38 | The disposable object used to cancel the scheduled 39 | recurring action (best effort). 40 | """ 41 | 42 | return NotImplemented 43 | 44 | 45 | __all__ = [ 46 | "PeriodicSchedulerBase", 47 | "ScheduledPeriodicAction", 48 | "ScheduledSingleOrPeriodicAction", 49 | "RelativeTime", 50 | ] 51 | -------------------------------------------------------------------------------- /reactivex/operators/_timestamp.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime 3 | from typing import Generic, TypeVar 4 | 5 | from reactivex import Observable, abc, defer, operators 6 | from reactivex.internal import curry_flip 7 | from reactivex.scheduler import TimeoutScheduler 8 | 9 | _T = TypeVar("_T") 10 | 11 | 12 | @dataclass 13 | class Timestamp(Generic[_T]): 14 | value: _T 15 | timestamp: datetime 16 | 17 | 18 | @curry_flip 19 | def timestamp_( 20 | source: Observable[_T], 21 | scheduler: abc.SchedulerBase | None = None, 22 | ) -> Observable[Timestamp[_T]]: 23 | """Records the timestamp for each value in an observable sequence. 24 | 25 | Examples: 26 | >>> result = source.pipe(timestamp()) 27 | >>> result = timestamp()(source) 28 | 29 | Produces objects with attributes `value` and `timestamp`, where 30 | value is the original value. 31 | 32 | Args: 33 | source: Observable source to timestamp. 34 | scheduler: Optional scheduler to use for timestamping. 35 | 36 | Returns: 37 | An observable sequence with timestamp information on values. 38 | """ 39 | 40 | def factory(scheduler_: abc.SchedulerBase | None = None): 41 | _scheduler = scheduler or scheduler_ or TimeoutScheduler.singleton() 42 | 43 | def mapper(value: _T) -> Timestamp[_T]: 44 | return Timestamp(value=value, timestamp=_scheduler.now) 45 | 46 | return source.pipe(operators.map(mapper)) 47 | 48 | return defer(factory) 49 | 50 | 51 | __all__ = ["timestamp_"] 52 | -------------------------------------------------------------------------------- /reactivex/operators/_min.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from typing import TypeVar, cast 3 | 4 | from reactivex import Observable 5 | from reactivex import operators as ops 6 | from reactivex.internal import curry_flip 7 | from reactivex.internal.basic import identity 8 | from reactivex.internal.exceptions import SequenceContainsNoElementsError 9 | from reactivex.typing import Comparer 10 | 11 | _T = TypeVar("_T") 12 | 13 | 14 | def first_only(x: list[_T]) -> _T: 15 | if not x: 16 | raise SequenceContainsNoElementsError() 17 | 18 | return x[0] 19 | 20 | 21 | @curry_flip 22 | def min_( 23 | source: Observable[_T], 24 | comparer: Comparer[_T] | None = None, 25 | ) -> Observable[_T]: 26 | """Returns the minimum element in an observable sequence. 27 | 28 | Returns the minimum element in an observable sequence according to 29 | the optional comparer else a default greater than less than check. 30 | 31 | Examples: 32 | >>> result = source.pipe(min()) 33 | >>> result = min()(source) 34 | >>> result = source.pipe(min(lambda x, y: x.value - y.value)) 35 | 36 | Args: 37 | source: The source observable. 38 | comparer: Optional comparer used to compare elements. 39 | 40 | Returns: 41 | An observable sequence containing a single element 42 | with the minimum element in the source sequence. 43 | """ 44 | return source.pipe( 45 | ops.min_by(cast(Callable[[_T], _T], identity), comparer), 46 | ops.map(first_only), 47 | ) 48 | 49 | 50 | __all__ = ["min_"] 51 | -------------------------------------------------------------------------------- /reactivex/operators/_last.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from reactivex import Observable, operators 4 | from reactivex.internal import curry_flip 5 | from reactivex.typing import Predicate 6 | 7 | from ._lastordefault import last_or_default_async 8 | 9 | _T = TypeVar("_T") 10 | 11 | 12 | @curry_flip 13 | def last_( 14 | source: Observable[_T], 15 | predicate: Predicate[_T] | None = None, 16 | ) -> Observable[_T]: 17 | """Returns the last element of an observable sequence that 18 | satisfies the condition in the predicate if specified, else 19 | the last element. 20 | 21 | Examples: 22 | >>> res = source.pipe(last()) 23 | >>> res = last()(source) 24 | >>> res = source.pipe(last(lambda x: x > 3)) 25 | 26 | Args: 27 | source: Source observable to get last item from. 28 | predicate: [Optional] A predicate function to evaluate for 29 | elements in the source sequence. 30 | 31 | Returns: 32 | An observable sequence containing the last element in the 33 | observable sequence that satisfies the condition in the 34 | predicate. 35 | """ 36 | from typing import cast 37 | 38 | if predicate: 39 | return source.pipe( 40 | operators.filter(predicate), 41 | operators.last(), 42 | ) 43 | 44 | # last_or_default_async returns Observable[_T | None], but when 45 | # has_default=False it never emits None. Safe cast to Observable[_T]. 46 | return cast(Observable[_T], last_or_default_async(source, False)) 47 | 48 | 49 | __all__ = ["last_"] 50 | -------------------------------------------------------------------------------- /tests/test_observable/test_of.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import reactivex 4 | from reactivex.testing import ReactiveTest, TestScheduler 5 | 6 | on_next = ReactiveTest.on_next 7 | on_completed = ReactiveTest.on_completed 8 | on_error = ReactiveTest.on_error 9 | subscribe = ReactiveTest.subscribe 10 | subscribed = ReactiveTest.subscribed 11 | disposed = ReactiveTest.disposed 12 | created = ReactiveTest.created 13 | 14 | 15 | class TestOf(unittest.TestCase): 16 | def test_of(self): 17 | results = [] 18 | 19 | reactivex.of(1, 2, 3, 4, 5).subscribe(results.append) 20 | 21 | assert str([1, 2, 3, 4, 5]) == str(results) 22 | 23 | def test_of_empty(self): 24 | results = [] 25 | 26 | reactivex.of().subscribe(results.append) 27 | 28 | assert len(results) == 0 29 | 30 | def teest_of_with_scheduler(self): 31 | scheduler = TestScheduler() 32 | 33 | def create(): 34 | return reactivex.of(1, 2, 3, 4, 5) 35 | 36 | results = scheduler.start(create=create) 37 | 38 | assert results.messages == [ 39 | on_next(201, 1), 40 | on_next(202, 2), 41 | on_next(203, 3), 42 | on_next(204, 4), 43 | on_next(205, 5), 44 | on_completed(206), 45 | ] 46 | 47 | def teest_of_with_scheduler_empty(self): 48 | scheduler = TestScheduler() 49 | 50 | def create(): 51 | return reactivex.of(scheduler=scheduler) 52 | 53 | results = scheduler.start(create=create) 54 | 55 | assert results.messages == [on_completed(201)] 56 | -------------------------------------------------------------------------------- /reactivex/operators/_skip.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from reactivex import Observable, abc 4 | from reactivex.internal import ArgumentOutOfRangeException, curry_flip 5 | 6 | _T = TypeVar("_T") 7 | 8 | 9 | @curry_flip 10 | def skip_(source: Observable[_T], count: int) -> Observable[_T]: 11 | """Bypasses a specified number of elements in an observable sequence 12 | and then returns the remaining elements. 13 | 14 | Examples: 15 | >>> result = source.pipe(skip(5)) 16 | >>> result = skip(5)(source) 17 | 18 | Args: 19 | source: The source observable. 20 | count: The number of elements to skip. 21 | 22 | Returns: 23 | An observable sequence that contains the elements that occur 24 | after the specified index in the input sequence. 25 | 26 | Raises: 27 | ArgumentOutOfRangeException: If count is negative. 28 | """ 29 | if count < 0: 30 | raise ArgumentOutOfRangeException() 31 | 32 | def subscribe( 33 | observer: abc.ObserverBase[_T], 34 | scheduler: abc.SchedulerBase | None = None, 35 | ) -> abc.DisposableBase: 36 | remaining = count 37 | 38 | def on_next(value: _T) -> None: 39 | nonlocal remaining 40 | 41 | if remaining <= 0: 42 | observer.on_next(value) 43 | else: 44 | remaining -= 1 45 | 46 | return source.subscribe( 47 | on_next, observer.on_error, observer.on_completed, scheduler=scheduler 48 | ) 49 | 50 | return Observable(subscribe) 51 | 52 | 53 | __all__ = ["skip_"] 54 | -------------------------------------------------------------------------------- /reactivex/observable/defer.py: -------------------------------------------------------------------------------- 1 | from asyncio import Future 2 | from collections.abc import Callable 3 | from typing import TypeVar, Union 4 | 5 | from reactivex import Observable, abc, from_future, throw 6 | from reactivex.scheduler import ImmediateScheduler 7 | 8 | _T = TypeVar("_T") 9 | 10 | 11 | def defer_( 12 | factory: Callable[[abc.SchedulerBase], Union[Observable[_T], "Future[_T]"]], 13 | ) -> Observable[_T]: 14 | """Returns an observable sequence that invokes the specified factory 15 | function whenever a new observer subscribes. 16 | 17 | Example: 18 | >>> res = defer(lambda scheduler: of(1, 2, 3)) 19 | 20 | Args: 21 | observable_factory: Observable factory function to invoke for 22 | each observer that subscribes to the resulting sequence. The 23 | factory takes a single argument, the scheduler used. 24 | 25 | Returns: 26 | An observable sequence whose observers trigger an invocation 27 | of the given observable factory function. 28 | """ 29 | 30 | def subscribe( 31 | observer: abc.ObserverBase[_T], scheduler: abc.SchedulerBase | None = None 32 | ) -> abc.DisposableBase: 33 | try: 34 | result = factory(scheduler or ImmediateScheduler.singleton()) 35 | except Exception as ex: # By design. pylint: disable=W0703 36 | return throw(ex).subscribe(observer) 37 | 38 | result = from_future(result) if isinstance(result, Future) else result 39 | return result.subscribe(observer, scheduler=scheduler) 40 | 41 | return Observable(subscribe) 42 | 43 | 44 | __all__ = ["defer_"] 45 | -------------------------------------------------------------------------------- /reactivex/observable/fromfuture.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from asyncio import Future 3 | from typing import Any, TypeVar, cast 4 | 5 | from reactivex import Observable, abc 6 | from reactivex.disposable import Disposable 7 | 8 | _T = TypeVar("_T") 9 | 10 | 11 | def from_future_(future: "Future[_T]") -> Observable[_T]: 12 | """Converts a Future to an Observable sequence 13 | 14 | Args: 15 | future -- A Python 3 compatible future. 16 | https://docs.python.org/3/library/asyncio-task.html#future 17 | 18 | Returns: 19 | An Observable sequence which wraps the existing future success 20 | and failure. 21 | """ 22 | 23 | def subscribe( 24 | observer: abc.ObserverBase[Any], scheduler: abc.SchedulerBase | None = None 25 | ) -> abc.DisposableBase: 26 | def done(future: "Future[_T]") -> None: 27 | try: 28 | value: Any = future.result() 29 | except Exception as ex: 30 | observer.on_error(ex) 31 | except asyncio.CancelledError as ex: # pylint: disable=broad-except 32 | # asyncio.CancelledError is a BaseException, so need to cast 33 | observer.on_error(cast(Exception, ex)) 34 | else: 35 | observer.on_next(value) 36 | observer.on_completed() 37 | 38 | future.add_done_callback(done) 39 | 40 | def dispose() -> None: 41 | if future: 42 | future.cancel() 43 | 44 | return Disposable(dispose) 45 | 46 | return Observable(subscribe) 47 | 48 | 49 | __all__ = ["from_future_"] 50 | -------------------------------------------------------------------------------- /reactivex/disposable/multipleassignmentdisposable.py: -------------------------------------------------------------------------------- 1 | from threading import RLock 2 | 3 | from reactivex.abc import DisposableBase 4 | 5 | 6 | class MultipleAssignmentDisposable(DisposableBase): 7 | """Represents a disposable resource whose underlying disposable 8 | resource can be replaced by another disposable resource.""" 9 | 10 | def __init__(self) -> None: 11 | self.current: DisposableBase | None = None 12 | self.is_disposed = False 13 | self.lock = RLock() 14 | 15 | super().__init__() 16 | 17 | def get_disposable(self) -> DisposableBase | None: 18 | return self.current 19 | 20 | def set_disposable(self, value: DisposableBase) -> None: 21 | """If the MultipleAssignmentDisposable has already been 22 | disposed, assignment to this property causes immediate disposal 23 | of the given disposable object.""" 24 | 25 | with self.lock: 26 | should_dispose = self.is_disposed 27 | if not should_dispose: 28 | self.current = value 29 | 30 | if should_dispose: 31 | value.dispose() 32 | 33 | disposable = property(get_disposable, set_disposable) 34 | 35 | def dispose(self) -> None: 36 | """Disposes the underlying disposable as well as all future 37 | replacements.""" 38 | 39 | old = None 40 | 41 | with self.lock: 42 | if not self.is_disposed: 43 | self.is_disposed = True 44 | old = self.current 45 | self.current = None 46 | 47 | if old is not None: 48 | old.dispose() 49 | -------------------------------------------------------------------------------- /reactivex/scheduler/scheduleditem.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Any 3 | 4 | from reactivex import abc 5 | from reactivex.disposable import SingleAssignmentDisposable 6 | 7 | from .scheduler import Scheduler 8 | 9 | 10 | class ScheduledItem: 11 | def __init__( 12 | self, 13 | scheduler: Scheduler, 14 | state: Any | None, 15 | action: abc.ScheduledAction[Any], 16 | duetime: datetime, 17 | ) -> None: 18 | self.scheduler: Scheduler = scheduler 19 | self.state: Any | None = state 20 | self.action: abc.ScheduledAction[Any] = action 21 | self.duetime: datetime = duetime 22 | self.disposable: SingleAssignmentDisposable = SingleAssignmentDisposable() 23 | 24 | def invoke(self) -> None: 25 | ret = self.scheduler.invoke_action(self.action, state=self.state) 26 | self.disposable.disposable = ret 27 | 28 | def cancel(self) -> None: 29 | """Cancels the work item by disposing the resource returned by 30 | invoke_core as soon as possible.""" 31 | 32 | self.disposable.dispose() 33 | 34 | def is_cancelled(self) -> bool: 35 | return self.disposable.is_disposed 36 | 37 | def __lt__(self, other: "ScheduledItem") -> bool: 38 | return self.duetime < other.duetime 39 | 40 | def __gt__(self, other: "ScheduledItem") -> bool: 41 | return self.duetime > other.duetime 42 | 43 | def __eq__(self, other: Any) -> bool: 44 | try: 45 | return self.duetime == other.duetime 46 | except AttributeError: 47 | return NotImplemented 48 | -------------------------------------------------------------------------------- /reactivex/operators/_first.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, cast 2 | 3 | from reactivex import Observable 4 | from reactivex import operators as ops 5 | from reactivex.internal import curry_flip 6 | from reactivex.typing import Predicate 7 | 8 | from ._firstordefault import first_or_default_async_ 9 | 10 | _T = TypeVar("_T") 11 | 12 | 13 | @curry_flip 14 | def first_( 15 | source: Observable[_T], 16 | predicate: Predicate[_T] | None = None, 17 | ) -> Observable[_T]: 18 | """Returns the first element of an observable sequence that 19 | satisfies the condition in the predicate if present else the first 20 | item in the sequence. 21 | 22 | Examples: 23 | >>> res = source.pipe(first()) 24 | >>> res = first()(source) 25 | >>> res = source.pipe(first(lambda x: x > 3)) 26 | 27 | Args: 28 | source: The source observable sequence. 29 | predicate: [Optional] A predicate function to evaluate for 30 | elements in the source sequence. 31 | 32 | Returns: 33 | An observable sequence containing the first element in the 34 | observable sequence that satisfies the condition in the predicate if 35 | provided, else the first item in the sequence. 36 | """ 37 | 38 | if predicate: 39 | return source.pipe(ops.filter(predicate), ops.first()) 40 | 41 | # first_or_default_async_(False) returns 42 | # Callable[[Observable[_T]], Observable[_T]] but the type checker 43 | # cannot infer it. This cast is safe - implementation preserves type. 44 | return cast(Observable[_T], first_or_default_async_(False)(source)) 45 | 46 | 47 | __all__ = ["first_"] 48 | -------------------------------------------------------------------------------- /reactivex/operators/connectable/_refcount.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from typing import TypeVar 3 | 4 | from reactivex import ConnectableObservable, Observable, abc 5 | from reactivex.disposable import Disposable 6 | 7 | _T = TypeVar("_T") 8 | 9 | 10 | def ref_count_() -> Callable[[ConnectableObservable[_T]], Observable[_T]]: 11 | """Returns an observable sequence that stays connected to the 12 | source as long as there is at least one subscription to the 13 | observable sequence. 14 | """ 15 | 16 | connectable_subscription: abc.DisposableBase | None = None 17 | count = 0 18 | 19 | def ref_count(source: ConnectableObservable[_T]) -> Observable[_T]: 20 | def subscribe( 21 | observer: abc.ObserverBase[_T], 22 | scheduler: abc.SchedulerBase | None = None, 23 | ) -> abc.DisposableBase: 24 | nonlocal connectable_subscription, count 25 | 26 | count += 1 27 | should_connect = count == 1 28 | subscription = source.subscribe(observer, scheduler=scheduler) 29 | if should_connect: 30 | connectable_subscription = source.connect(scheduler) 31 | 32 | def dispose() -> None: 33 | nonlocal connectable_subscription, count 34 | 35 | subscription.dispose() 36 | count -= 1 37 | if not count and connectable_subscription: 38 | connectable_subscription.dispose() 39 | 40 | return Disposable(dispose) 41 | 42 | return Observable(subscribe) 43 | 44 | return ref_count 45 | 46 | 47 | __all__ = ["ref_count_"] 48 | -------------------------------------------------------------------------------- /reactivex/abc/observable.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from collections.abc import Callable 3 | from typing import Generic, TypeVar 4 | 5 | from typing_extensions import TypeAliasType 6 | 7 | from .disposable import DisposableBase 8 | from .observer import ObserverBase, OnCompleted, OnError, OnNext 9 | from .scheduler import SchedulerBase 10 | 11 | _T_out = TypeVar("_T_out", covariant=True) 12 | 13 | 14 | class ObservableBase(Generic[_T_out], ABC): 15 | """Observable abstract base class. 16 | 17 | Represents a push-style collection.""" 18 | 19 | __slots__ = () 20 | 21 | @abstractmethod 22 | def subscribe( 23 | self, 24 | on_next: OnNext[_T_out] | ObserverBase[_T_out] | None = None, 25 | on_error: OnError | None = None, 26 | on_completed: OnCompleted | None = None, 27 | *, 28 | scheduler: SchedulerBase | None = None, 29 | ) -> DisposableBase: 30 | """Subscribe an observer to the observable sequence. 31 | 32 | Args: 33 | observer: [Optional] The object that is to receive 34 | notifications. 35 | scheduler: [Optional] The default scheduler to use for this 36 | subscription. 37 | 38 | Returns: 39 | Disposable object representing an observer's subscription 40 | to the observable sequence. 41 | """ 42 | 43 | raise NotImplementedError 44 | 45 | 46 | Subscription = TypeAliasType( 47 | "Subscription", 48 | Callable[[ObserverBase[_T_out], SchedulerBase | None], DisposableBase], 49 | type_params=(_T_out,), 50 | ) 51 | 52 | __all__ = ["ObservableBase", "Subscription"] 53 | -------------------------------------------------------------------------------- /reactivex/operators/_single.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, cast 2 | 3 | from reactivex import Observable 4 | from reactivex import operators as ops 5 | from reactivex.internal import curry_flip 6 | from reactivex.typing import Predicate 7 | 8 | from ._singleordefault import single_or_default_async_ 9 | 10 | _T = TypeVar("_T") 11 | 12 | 13 | @curry_flip 14 | def single_( 15 | source: Observable[_T], 16 | predicate: Predicate[_T] | None = None, 17 | ) -> Observable[_T]: 18 | """Returns the only element of an observable sequence that satisfies the 19 | condition in the optional predicate, and reports an exception if there 20 | is not exactly one element in the observable sequence. 21 | 22 | Examples: 23 | >>> res = source.pipe(single()) 24 | >>> res = single()(source) 25 | >>> res = source.pipe(single(lambda x: x == 42)) 26 | 27 | Args: 28 | source: The source observable sequence. 29 | predicate: [Optional] A predicate function to evaluate for 30 | elements in the source sequence. 31 | 32 | Returns: 33 | An observable sequence containing the single element in the 34 | observable sequence that satisfies the condition in the predicate. 35 | """ 36 | 37 | if predicate: 38 | return source.pipe(ops.filter(predicate), ops.single()) 39 | else: 40 | # single_or_default_async_(False) returns 41 | # Callable[[Observable[_T]], Observable[_T]] but the type checker 42 | # cannot infer it. This cast is safe - implementation preserves type. 43 | return cast(Observable[_T], single_or_default_async_(False)(source)) 44 | 45 | 46 | __all__ = ["single_"] 47 | -------------------------------------------------------------------------------- /reactivex/operators/_defaultifempty.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from reactivex import Observable, abc 4 | from reactivex.internal import curry_flip 5 | 6 | _T = TypeVar("_T") 7 | 8 | 9 | @curry_flip 10 | def default_if_empty_( 11 | source: Observable[_T], 12 | default_value: _T | None = None, 13 | ) -> Observable[_T | None]: 14 | """Returns the elements of the specified sequence or the 15 | specified value in a singleton sequence if the sequence is 16 | empty. 17 | 18 | Examples: 19 | >>> obs = source.pipe(default_if_empty()) 20 | >>> obs = default_if_empty()(source) 21 | >>> obs = source.pipe(default_if_empty(42)) 22 | 23 | Args: 24 | source: Source observable. 25 | default_value: The value to return if the sequence is empty. 26 | 27 | Returns: 28 | An observable sequence that contains the specified default 29 | value if the source is empty otherwise, the elements of the 30 | source. 31 | """ 32 | 33 | def subscribe( 34 | observer: abc.ObserverBase[_T | None], 35 | scheduler: abc.SchedulerBase | None = None, 36 | ) -> abc.DisposableBase: 37 | found = [False] 38 | 39 | def on_next(x: _T): 40 | found[0] = True 41 | observer.on_next(x) 42 | 43 | def on_completed(): 44 | if not found[0]: 45 | observer.on_next(default_value) 46 | observer.on_completed() 47 | 48 | return source.subscribe( 49 | on_next, observer.on_error, on_completed, scheduler=scheduler 50 | ) 51 | 52 | return Observable(subscribe) 53 | 54 | 55 | __all__ = ["default_if_empty_"] 56 | -------------------------------------------------------------------------------- /tests/test_integration/test_group_reduce.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from collections.abc import Callable 3 | from typing import Any 4 | 5 | import reactivex 6 | from reactivex import Observable 7 | from reactivex import operators as ops 8 | 9 | 10 | class TestGroupByReduce(unittest.TestCase): 11 | def test_groupby_count(self) -> None: 12 | res: list[Any] = [] 13 | 14 | # Integration test: GroupedObservable types validated at runtime 15 | def flat_map_fn(i: Any) -> Any: 16 | return i.pipe( 17 | ops.count(), 18 | ops.map(lambda ii: (i.key, ii)), 19 | ) 20 | 21 | flat_map_op: Callable[[Observable[Any]], Observable[Any]] = ops.flat_map( 22 | flat_map_fn 23 | ) 24 | 25 | counts: Any = reactivex.from_(range(10)).pipe( 26 | ops.group_by(lambda i: "even" if i % 2 == 0 else "odd"), 27 | flat_map_op, 28 | ) 29 | 30 | counts.subscribe(on_next=res.append) 31 | assert res == [("even", 5), ("odd", 5)] 32 | 33 | def test_window_sum(self) -> None: 34 | res: list[Any] = [] 35 | 36 | # Integration test: windowed Observable types validated at runtime 37 | def flat_map_fn(i: Any) -> Any: 38 | return i.pipe(ops.sum()) 39 | 40 | flat_map_op: Callable[[Observable[Any]], Observable[Any]] = ops.flat_map( 41 | flat_map_fn 42 | ) 43 | 44 | obs: Any = reactivex.from_(range(6)).pipe( 45 | ops.window_with_count(count=3, skip=1), 46 | flat_map_op, 47 | ) 48 | obs.subscribe(on_next=res.append) 49 | 50 | assert res == [3, 6, 9, 12, 9, 5, 0] 51 | -------------------------------------------------------------------------------- /reactivex/operators/_takeuntil.py: -------------------------------------------------------------------------------- 1 | from asyncio import Future 2 | from typing import TypeVar 3 | 4 | from reactivex import Observable, abc, from_future 5 | from reactivex.disposable import CompositeDisposable 6 | from reactivex.internal import curry_flip, noop 7 | 8 | _T = TypeVar("_T") 9 | 10 | 11 | @curry_flip 12 | def take_until_( 13 | source: Observable[_T], 14 | other: Observable[_T] | Future[_T], 15 | ) -> Observable[_T]: 16 | """Returns the values from the source observable sequence until 17 | the other observable sequence produces a value. 18 | 19 | Examples: 20 | >>> source.pipe(take_until(other)) 21 | >>> take_until(other)(source) 22 | 23 | Args: 24 | source: The source observable sequence. 25 | other: Observable or Future that terminates propagation. 26 | 27 | Returns: 28 | An observable sequence containing the elements of the source 29 | sequence up to the point the other sequence interrupted 30 | further propagation. 31 | """ 32 | if isinstance(other, Future): 33 | obs: Observable[_T] = from_future(other) 34 | else: 35 | obs = other 36 | 37 | def subscribe( 38 | observer: abc.ObserverBase[_T], 39 | scheduler: abc.SchedulerBase | None = None, 40 | ) -> abc.DisposableBase: 41 | def on_completed(_: _T) -> None: 42 | observer.on_completed() 43 | 44 | return CompositeDisposable( 45 | source.subscribe(observer, scheduler=scheduler), 46 | obs.subscribe(on_completed, observer.on_error, noop, scheduler=scheduler), 47 | ) 48 | 49 | return Observable(subscribe) 50 | 51 | 52 | __all__ = ["take_until_"] 53 | -------------------------------------------------------------------------------- /tests/test_observable/test_error_handling_fluent.py: -------------------------------------------------------------------------------- 1 | """Tests for ErrorHandlingMixin fluent API methods. 2 | 3 | This module tests the error handling operators fluent syntax from ErrorHandlingMixin, 4 | ensuring they produce identical results to the pipe-based functional syntax. 5 | """ 6 | 7 | import reactivex as rx 8 | from reactivex import Observable, operators as ops 9 | 10 | 11 | class TestCatchMethodChaining: 12 | """Tests for catch() method.""" 13 | 14 | def test_catch_equivalence(self) -> None: 15 | """Verify catch fluent and functional styles are equivalent.""" 16 | source: Observable[int] = rx.throw(Exception("Error")) 17 | fallback: Observable[int] = rx.of(99) 18 | 19 | fluent_result: Observable[int] = source.catch(fallback) 20 | pipe_result: Observable[int] = source.pipe(ops.catch(fallback)) 21 | 22 | fluent_values: list[int] = [] 23 | pipe_values: list[int] = [] 24 | 25 | fluent_result.subscribe(on_next=fluent_values.append, on_error=lambda e: None) 26 | pipe_result.subscribe(on_next=pipe_values.append, on_error=lambda e: None) 27 | 28 | assert fluent_values == pipe_values == [99] 29 | 30 | def test_catch_with_normal_completion(self) -> None: 31 | """Test catch when source completes normally.""" 32 | source: Observable[int] = rx.of(1, 2, 3) 33 | fallback: Observable[int] = rx.of(99) 34 | 35 | result: Observable[int] = source.catch(fallback) 36 | 37 | values: list[int] = [] 38 | result.subscribe(on_next=values.append, on_error=lambda e: None) 39 | 40 | # Should return original values since no error 41 | assert values == [1, 2, 3] 42 | -------------------------------------------------------------------------------- /reactivex/operators/_take.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from reactivex import Observable, abc, empty 4 | from reactivex.internal import ArgumentOutOfRangeException, curry_flip 5 | 6 | _T = TypeVar("_T") 7 | 8 | 9 | @curry_flip 10 | def take_(source: Observable[_T], count: int) -> Observable[_T]: 11 | """Returns a specified number of contiguous elements from the start of 12 | an observable sequence. 13 | 14 | Example: 15 | >>> result = source.pipe(take(5)) 16 | >>> result = take(5)(source) 17 | 18 | Args: 19 | source: The source observable sequence. 20 | count: The number of elements to return. 21 | 22 | Returns: 23 | An observable sequence that contains the specified number of 24 | elements from the start of the input sequence. 25 | 26 | Raises: 27 | ArgumentOutOfRangeException: If count is negative. 28 | """ 29 | if count < 0: 30 | raise ArgumentOutOfRangeException() 31 | 32 | if not count: 33 | return empty() 34 | 35 | def subscribe( 36 | observer: abc.ObserverBase[_T], 37 | scheduler: abc.SchedulerBase | None = None, 38 | ): 39 | remaining = count 40 | 41 | def on_next(value: _T) -> None: 42 | nonlocal remaining 43 | 44 | if remaining > 0: 45 | remaining -= 1 46 | observer.on_next(value) 47 | if not remaining: 48 | observer.on_completed() 49 | 50 | return source.subscribe( 51 | on_next, observer.on_error, observer.on_completed, scheduler=scheduler 52 | ) 53 | 54 | return Observable(subscribe) 55 | 56 | 57 | __all__ = ["take_"] 58 | -------------------------------------------------------------------------------- /reactivex/internal/priorityqueue.py: -------------------------------------------------------------------------------- 1 | import heapq 2 | from sys import maxsize 3 | from typing import Generic, TypeVar 4 | 5 | _T1 = TypeVar("_T1") 6 | 7 | 8 | class PriorityQueue(Generic[_T1]): 9 | """Priority queue for scheduling. Note that methods aren't thread-safe.""" 10 | 11 | MIN_COUNT = ~maxsize 12 | 13 | def __init__(self) -> None: 14 | self.items: list[tuple[_T1, int]] = [] 15 | self.count = PriorityQueue.MIN_COUNT # Monotonic increasing for sort stability 16 | 17 | def __len__(self) -> int: 18 | """Returns length of queue""" 19 | 20 | return len(self.items) 21 | 22 | def peek(self) -> _T1: 23 | """Returns first item in queue without removing it""" 24 | return self.items[0][0] 25 | 26 | def dequeue(self) -> _T1: 27 | """Returns and removes item with lowest priority from queue""" 28 | 29 | item: _T1 = heapq.heappop(self.items)[0] 30 | if not self.items: 31 | self.count = PriorityQueue.MIN_COUNT 32 | return item 33 | 34 | def enqueue(self, item: _T1) -> None: 35 | """Adds item to queue""" 36 | 37 | heapq.heappush(self.items, (item, self.count)) 38 | self.count += 1 39 | 40 | def remove(self, item: _T1) -> bool: 41 | """Remove given item from queue""" 42 | 43 | for index, _item in enumerate(self.items): 44 | if _item[0] == item: 45 | self.items.pop(index) 46 | heapq.heapify(self.items) 47 | return True 48 | 49 | return False 50 | 51 | def clear(self) -> None: 52 | """Remove all items from the queue.""" 53 | self.items = [] 54 | self.count = PriorityQueue.MIN_COUNT 55 | -------------------------------------------------------------------------------- /reactivex/operators/_groupby.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from typing import Any, TypeVar 3 | 4 | from reactivex import GroupedObservable, Observable, typing 5 | from reactivex.internal import curry_flip 6 | from reactivex.subject import Subject 7 | 8 | _T = TypeVar("_T") 9 | _TKey = TypeVar("_TKey") 10 | _TValue = TypeVar("_TValue") 11 | 12 | # pylint: disable=import-outside-toplevel 13 | 14 | 15 | @curry_flip 16 | def group_by_( 17 | source: Observable[_T], 18 | key_mapper: typing.Mapper[_T, _TKey], 19 | element_mapper: typing.Mapper[_T, _TValue] | None = None, 20 | subject_mapper: Callable[[], Subject[_TValue]] | None = None, 21 | ) -> Observable[GroupedObservable[_TKey, _TValue]]: 22 | """Groups the elements of an observable sequence according to a 23 | specified key mapper function. 24 | 25 | Examples: 26 | >>> res = source.pipe(group_by(lambda x: x.id)) 27 | >>> res = group_by(lambda x: x.id)(source) 28 | 29 | Args: 30 | source: Source observable to group. 31 | key_mapper: A function to extract the key for each element. 32 | element_mapper: Optional function to map elements to values. 33 | subject_mapper: Optional function that returns a subject used to initiate 34 | a grouped observable. 35 | 36 | Returns: 37 | An observable sequence of grouped observables. 38 | """ 39 | from reactivex import operators as ops 40 | 41 | def duration_mapper(_: GroupedObservable[Any, Any]) -> Observable[Any]: 42 | import reactivex 43 | 44 | return reactivex.never() 45 | 46 | return source.pipe( 47 | ops.group_by_until(key_mapper, element_mapper, duration_mapper, subject_mapper) 48 | ) 49 | 50 | 51 | __all__ = ["group_by_"] 52 | -------------------------------------------------------------------------------- /reactivex/operators/_takelast.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from reactivex import Observable, abc 4 | from reactivex.internal import curry_flip 5 | 6 | _T = TypeVar("_T") 7 | 8 | 9 | @curry_flip 10 | def take_last_(source: Observable[_T], count: int) -> Observable[_T]: 11 | """Returns a specified number of contiguous elements from the end of an 12 | observable sequence. 13 | 14 | Examples: 15 | >>> res = source.pipe(take_last(5)) 16 | >>> res = take_last(5)(source) 17 | 18 | This operator accumulates a buffer with a length enough to store 19 | elements count elements. Upon completion of the source sequence, this 20 | buffer is drained on the result sequence. This causes the elements to be 21 | delayed. 22 | 23 | Args: 24 | source: The source observable sequence. 25 | count: Number of elements to take from the end of the source 26 | sequence. 27 | 28 | Returns: 29 | An observable sequence containing the specified number of elements 30 | from the end of the source sequence. 31 | """ 32 | 33 | def subscribe( 34 | observer: abc.ObserverBase[_T], 35 | scheduler: abc.SchedulerBase | None = None, 36 | ) -> abc.DisposableBase: 37 | q: list[_T] = [] 38 | 39 | def on_next(x: _T) -> None: 40 | q.append(x) 41 | if len(q) > count: 42 | q.pop(0) 43 | 44 | def on_completed(): 45 | while q: 46 | observer.on_next(q.pop(0)) 47 | observer.on_completed() 48 | 49 | return source.subscribe( 50 | on_next, observer.on_error, on_completed, scheduler=scheduler 51 | ) 52 | 53 | return Observable(subscribe) 54 | 55 | 56 | __all__ = ["take_last_"] 57 | -------------------------------------------------------------------------------- /reactivex/operators/_timeinterval.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import timedelta 3 | from typing import Generic, TypeVar 4 | 5 | from reactivex import Observable, abc 6 | from reactivex import operators as ops 7 | from reactivex.internal import curry_flip 8 | from reactivex.scheduler import TimeoutScheduler 9 | 10 | _T = TypeVar("_T") 11 | 12 | 13 | @dataclass 14 | class TimeInterval(Generic[_T]): 15 | value: _T 16 | interval: timedelta 17 | 18 | 19 | @curry_flip 20 | def time_interval_( 21 | source: Observable[_T], 22 | scheduler: abc.SchedulerBase | None = None, 23 | ) -> Observable[TimeInterval[_T]]: 24 | """Records the time interval between consecutive values in an 25 | observable sequence. 26 | 27 | Examples: 28 | >>> res = source.pipe(time_interval()) 29 | >>> res = time_interval()(source) 30 | 31 | Args: 32 | source: The source observable sequence. 33 | scheduler: Scheduler to use for timing. 34 | 35 | Returns: 36 | An observable sequence with time interval information on 37 | values. 38 | """ 39 | 40 | def subscribe( 41 | observer: abc.ObserverBase[TimeInterval[_T]], 42 | scheduler_: abc.SchedulerBase | None = None, 43 | ) -> abc.DisposableBase: 44 | _scheduler = scheduler or scheduler_ or TimeoutScheduler.singleton() 45 | last = _scheduler.now 46 | 47 | def mapper(value: _T) -> TimeInterval[_T]: 48 | nonlocal last 49 | 50 | now = _scheduler.now 51 | span = now - last 52 | last = now 53 | return TimeInterval(value=value, interval=span) 54 | 55 | return source.pipe(ops.map(mapper)).subscribe(observer, scheduler=_scheduler) 56 | 57 | return Observable(subscribe) 58 | -------------------------------------------------------------------------------- /examples/timeflies/timeflies_wx.py: -------------------------------------------------------------------------------- 1 | import wx 2 | 3 | import reactivex 4 | from reactivex import operators as ops 5 | from reactivex.scheduler.mainloop import WxScheduler 6 | from reactivex.subject import Subject 7 | 8 | 9 | class Frame(wx.Frame): 10 | def __init__(self): 11 | super().__init__(None) 12 | self.SetTitle("Rx for Python rocks") 13 | self.SetSize((600, 600)) 14 | 15 | # This Subject is used to transmit mouse moves to labels 16 | self.mousemove = Subject() 17 | 18 | self.Bind(wx.EVT_MOTION, self.OnMotion) 19 | 20 | def OnMotion(self, event): 21 | self.mousemove.on_next((event.GetX(), event.GetY())) 22 | 23 | 24 | def main(): 25 | app = wx.App() 26 | scheduler = WxScheduler(wx) 27 | 28 | app.TopWindow = frame = Frame() 29 | frame.Show() 30 | 31 | text = "TIME FLIES LIKE AN ARROW" 32 | 33 | def on_next(info): 34 | label, (x, y), i = info 35 | label.Move(x + i * 12 + 15, y) 36 | label.Show() 37 | 38 | def handle_label(label, i): 39 | delayer = ops.delay(i * 0.100) 40 | mapper = ops.map(lambda xy: (label, xy, i)) 41 | 42 | return frame.mousemove.pipe( 43 | delayer, 44 | mapper, 45 | ) 46 | 47 | def make_label(char): 48 | label = wx.StaticText(frame, label=char) 49 | label.Hide() 50 | return label 51 | 52 | mapper = ops.map(make_label) 53 | labeler = ops.flat_map_indexed(handle_label) 54 | 55 | reactivex.from_(text).pipe( 56 | mapper, 57 | labeler, 58 | ).subscribe(on_next, on_error=print, scheduler=scheduler) 59 | 60 | frame.Bind(wx.EVT_CLOSE, lambda e: (scheduler.cancel_all(), e.Skip())) 61 | app.MainLoop() 62 | 63 | 64 | if __name__ == "__main__": 65 | main() 66 | -------------------------------------------------------------------------------- /reactivex/disposable/singleassignmentdisposable.py: -------------------------------------------------------------------------------- 1 | from threading import RLock 2 | 3 | from reactivex.abc import DisposableBase 4 | 5 | 6 | class SingleAssignmentDisposable(DisposableBase): 7 | """Single assignment disposable. 8 | 9 | Represents a disposable resource which only allows a single 10 | assignment of its underlying disposable resource. If an underlying 11 | disposable resource has already been set, future attempts to set the 12 | underlying disposable resource will throw an Error.""" 13 | 14 | def __init__(self) -> None: 15 | """Initializes a new instance of the SingleAssignmentDisposable 16 | class. 17 | """ 18 | self.is_disposed: bool = False 19 | self.current: DisposableBase | None = None 20 | self.lock = RLock() 21 | 22 | super().__init__() 23 | 24 | def get_disposable(self) -> DisposableBase | None: 25 | return self.current 26 | 27 | def set_disposable(self, value: DisposableBase) -> None: 28 | if self.current: 29 | raise Exception("Disposable has already been assigned") 30 | 31 | with self.lock: 32 | should_dispose = self.is_disposed 33 | if not should_dispose: 34 | self.current = value 35 | 36 | if self.is_disposed and value: 37 | value.dispose() 38 | 39 | disposable = property(get_disposable, set_disposable) 40 | 41 | def dispose(self) -> None: 42 | """Sets the status to disposed""" 43 | old: DisposableBase | None = None 44 | 45 | with self.lock: 46 | if not self.is_disposed: 47 | self.is_disposed = True 48 | old = self.current 49 | self.current = None 50 | 51 | if old is not None: 52 | old.dispose() 53 | -------------------------------------------------------------------------------- /reactivex/operators/_skiplast.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from reactivex import Observable, abc 4 | from reactivex.internal import curry_flip 5 | 6 | _T = TypeVar("_T") 7 | 8 | 9 | @curry_flip 10 | def skip_last_(source: Observable[_T], count: int) -> Observable[_T]: 11 | """Bypasses a specified number of elements at the end of an 12 | observable sequence. 13 | 14 | This operator accumulates a queue with a length enough to store 15 | the first `count` elements. As more elements are received, 16 | elements are taken from the front of the queue and produced on 17 | the result sequence. This causes elements to be delayed. 18 | 19 | Examples: 20 | >>> res = source.pipe(skip_last(5)) 21 | >>> res = skip_last(5)(source) 22 | 23 | Args: 24 | source: Source observable. 25 | count: Number of elements to bypass at the end of the 26 | source sequence. 27 | 28 | Returns: 29 | An observable sequence containing the source sequence 30 | elements except for the bypassed ones at the end. 31 | """ 32 | 33 | def subscribe( 34 | observer: abc.ObserverBase[_T], 35 | scheduler: abc.SchedulerBase | None = None, 36 | ) -> abc.DisposableBase: 37 | q: list[_T] = [] 38 | 39 | def on_next(value: _T) -> None: 40 | front = None 41 | with source.lock: 42 | q.append(value) 43 | if len(q) > count: 44 | front = q.pop(0) 45 | 46 | if front is not None: 47 | observer.on_next(front) 48 | 49 | return source.subscribe( 50 | on_next, observer.on_error, observer.on_completed, scheduler=scheduler 51 | ) 52 | 53 | return Observable(subscribe) 54 | 55 | 56 | __all__ = ["skip_last_"] 57 | -------------------------------------------------------------------------------- /notebooks/reactivex.io/assets/js/ipython_notebook_toc.js: -------------------------------------------------------------------------------- 1 | // Converts integer to roman numeral 2 | function romanize(num) { 3 | var lookup = {M:1000,CM:900,D:500,CD:400,C:100,XC:90,L:50,XL:40,X:10,IX:9,V:5,IV:4,I:1}, 4 | roman = '', 5 | i; 6 | for ( i in lookup ) { 7 | while ( num >= lookup[i] ) { 8 | roman += i; 9 | num -= lookup[i]; 10 | } 11 | } 12 | return roman; 13 | } 14 | 15 | // Builds a
      Table of Contents from all in DOM 16 | function createTOC(){ 17 | var toc = ""; 18 | var level = 0; 19 | var levels = {} 20 | $('#toc').html(''); 21 | 22 | $(":header").each(function(i){ 23 | if (this.id=='tocheading'){return;} 24 | 25 | titleText = this.innerHTML; 26 | openLevel = this.tagName[1]; 27 | 28 | if (levels[openLevel]){ 29 | levels[openLevel] += 1; 30 | } else{ 31 | levels[openLevel] = 1; 32 | } 33 | 34 | if (openLevel > level) { 35 | toc += (new Array(openLevel - level + 1)).join('
        '); 36 | } else if (openLevel < level) { 37 | toc += (new Array(level - openLevel + 1)).join("
      "); 38 | for (i=level;i>openLevel;i--){levels[i]=0;} 39 | } 40 | 41 | level = parseInt(openLevel); 42 | 43 | 44 | if (this.id==''){this.id = this.innerHTML.replace(/ /g,"-")} 45 | var anchor = this.id; 46 | 47 | toc += '
    • ' + romanize(levels[openLevel].toString()) + '. ' + titleText 48 | + '
    • '; 49 | 50 | }); 51 | 52 | 53 | if (level) { 54 | toc += (new Array(level + 1)).join("
    "); 55 | } 56 | 57 | 58 | $('#toc').append(toc); 59 | 60 | }; 61 | 62 | // Executes the createToc function 63 | setTimeout(function(){createTOC();},100); 64 | 65 | // Rebuild to TOC every minute 66 | setInterval(function(){createTOC();},60000); -------------------------------------------------------------------------------- /reactivex/operators/_pairwise.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, cast 2 | 3 | from reactivex import Observable, abc 4 | from reactivex.internal import curry_flip 5 | 6 | _T = TypeVar("_T") 7 | 8 | 9 | @curry_flip 10 | def pairwise_(source: Observable[_T]) -> Observable[tuple[_T, _T]]: 11 | """Returns a new observable that triggers on the second and 12 | subsequent triggerings of the input observable. The Nth 13 | triggering of the input observable passes the arguments from 14 | the N-1th and Nth triggering as a pair. The argument passed to 15 | the N-1th triggering is held in hidden internal state until the 16 | Nth triggering occurs. 17 | 18 | Examples: 19 | >>> res = source.pipe(pairwise()) 20 | >>> res = pairwise()(source) 21 | 22 | Args: 23 | source: The source observable sequence. 24 | 25 | Returns: 26 | An observable that triggers on successive pairs of 27 | observations from the input observable as an array. 28 | """ 29 | 30 | def subscribe( 31 | observer: abc.ObserverBase[tuple[_T, _T]], 32 | scheduler: abc.SchedulerBase | None = None, 33 | ) -> abc.DisposableBase: 34 | has_previous = False 35 | previous: _T = cast(_T, None) 36 | 37 | def on_next(x: _T) -> None: 38 | nonlocal has_previous, previous 39 | pair = None 40 | 41 | with source.lock: 42 | if has_previous: 43 | pair = (previous, x) 44 | else: 45 | has_previous = True 46 | 47 | previous = x 48 | 49 | if pair: 50 | observer.on_next(pair) 51 | 52 | return source.subscribe(on_next, observer.on_error, observer.on_completed) 53 | 54 | return Observable(subscribe) 55 | 56 | 57 | __all__ = ["pairwise_"] 58 | -------------------------------------------------------------------------------- /reactivex/operators/_some.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from reactivex import Observable, abc 4 | from reactivex import operators as ops 5 | from reactivex.internal import curry_flip 6 | from reactivex.typing import Predicate 7 | 8 | _T = TypeVar("_T") 9 | 10 | 11 | @curry_flip 12 | def some_( 13 | source: Observable[_T], 14 | predicate: Predicate[_T] | None = None, 15 | ) -> Observable[bool]: 16 | """Determines whether some element of an observable sequence satisfies a 17 | condition if present, else if some items are in the sequence. 18 | 19 | Examples: 20 | >>> res = source.pipe(some()) 21 | >>> res = some()(source) 22 | >>> res = source.pipe(some(lambda x: x > 3)) 23 | 24 | Args: 25 | source: The source observable sequence. 26 | predicate: A function to test each element for a condition. 27 | 28 | Returns: 29 | An observable sequence containing a single element 30 | determining whether some elements in the source sequence 31 | pass the test in the specified predicate if given, else if 32 | some items are in the sequence. 33 | """ 34 | 35 | def subscribe( 36 | observer: abc.ObserverBase[bool], 37 | scheduler: abc.SchedulerBase | None = None, 38 | ): 39 | def on_next(_: _T): 40 | observer.on_next(True) 41 | observer.on_completed() 42 | 43 | def on_error(): 44 | observer.on_next(False) 45 | observer.on_completed() 46 | 47 | return source.subscribe( 48 | on_next, observer.on_error, on_error, scheduler=scheduler 49 | ) 50 | 51 | if predicate: 52 | return source.pipe( 53 | ops.filter(predicate), 54 | ops.some(), 55 | ) 56 | 57 | return Observable(subscribe) 58 | 59 | 60 | __all__ = ["some_"] 61 | -------------------------------------------------------------------------------- /reactivex/operators/_takelastbuffer.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from reactivex import Observable, abc 4 | from reactivex.internal import curry_flip 5 | 6 | _T = TypeVar("_T") 7 | 8 | 9 | @curry_flip 10 | def take_last_buffer_(source: Observable[_T], count: int) -> Observable[list[_T]]: 11 | """Returns an array with the specified number of contiguous 12 | elements from the end of an observable sequence. 13 | 14 | Example: 15 | >>> res = source.pipe(take_last_buffer(5)) 16 | >>> res = take_last_buffer(5)(source) 17 | 18 | This operator accumulates a buffer with a length enough to 19 | store elements count elements. Upon completion of the source 20 | sequence, this buffer is drained on the result sequence. This 21 | causes the elements to be delayed. 22 | 23 | Args: 24 | source: Source observable to take elements from. 25 | count: Number of elements to take from the end. 26 | 27 | Returns: 28 | An observable sequence containing a single list with the 29 | specified number of elements from the end of the source 30 | sequence. 31 | """ 32 | 33 | def subscribe( 34 | observer: abc.ObserverBase[list[_T]], 35 | scheduler: abc.SchedulerBase | None = None, 36 | ) -> abc.DisposableBase: 37 | q: list[_T] = [] 38 | 39 | def on_next(x: _T) -> None: 40 | with source.lock: 41 | q.append(x) 42 | if len(q) > count: 43 | q.pop(0) 44 | 45 | def on_completed() -> None: 46 | observer.on_next(q) 47 | observer.on_completed() 48 | 49 | return source.subscribe( 50 | on_next, observer.on_error, on_completed, scheduler=scheduler 51 | ) 52 | 53 | return Observable(subscribe) 54 | 55 | 56 | __all__ = ["take_last_buffer_"] 57 | -------------------------------------------------------------------------------- /tests/test_observable/test_blocking/test_blocking.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import pytest 4 | 5 | import reactivex 6 | from reactivex import operators as ops 7 | from reactivex.internal.exceptions import SequenceContainsNoElementsError 8 | from reactivex.testing import ReactiveTest 9 | 10 | on_next = ReactiveTest.on_next 11 | on_completed = ReactiveTest.on_completed 12 | on_error = ReactiveTest.on_error 13 | subscribe = ReactiveTest.subscribe 14 | subscribed = ReactiveTest.subscribed 15 | disposed = ReactiveTest.disposed 16 | created = ReactiveTest.created 17 | 18 | 19 | class RxException(Exception): 20 | pass 21 | 22 | 23 | # Helper function for raising exceptions within lambdas 24 | def _raise(ex): 25 | raise RxException(ex) 26 | 27 | 28 | class TestBlocking(unittest.TestCase): 29 | def test_run_empty(self): 30 | with pytest.raises(SequenceContainsNoElementsError): 31 | reactivex.empty().run() 32 | 33 | def test_run_error(self): 34 | with pytest.raises(RxException): 35 | reactivex.throw(RxException()).run() 36 | 37 | def test_run_just(self): 38 | result = reactivex.just(42).run() 39 | assert result == 42 40 | 41 | def test_run_range(self): 42 | result = reactivex.range(42).run() 43 | assert result == 41 44 | 45 | def test_run_range_to_iterable(self): 46 | result = reactivex.range(42).pipe(ops.to_iterable()).run() 47 | assert list(result) == list(range(42)) 48 | 49 | def test_run_from(self): 50 | result = reactivex.from_([1, 2, 3]).run() 51 | assert result == 3 52 | 53 | def test_run_from_first(self): 54 | result = reactivex.from_([1, 2, 3]).pipe(ops.first()).run() 55 | assert result == 1 56 | 57 | def test_run_of(self): 58 | result = reactivex.of(1, 2, 3).run() 59 | assert result == 3 60 | -------------------------------------------------------------------------------- /tests/test_observable/test_fromcallback.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import reactivex 4 | from reactivex.testing import ReactiveTest 5 | 6 | on_next = ReactiveTest.on_next 7 | on_completed = ReactiveTest.on_completed 8 | on_error = ReactiveTest.on_error 9 | subscribe = ReactiveTest.subscribe 10 | subscribed = ReactiveTest.subscribed 11 | disposed = ReactiveTest.disposed 12 | created = ReactiveTest.created 13 | 14 | 15 | class RxException(Exception): 16 | pass 17 | 18 | 19 | # Helper function for raising exceptions within lambdas 20 | def _raise(ex): 21 | raise RxException(ex) 22 | 23 | 24 | class TestFromCallback(unittest.TestCase): 25 | def test_from_callback(self): 26 | res = reactivex.from_callback(lambda cb: cb(True))() 27 | 28 | def on_next(r): 29 | self.assertEqual(r, True) 30 | 31 | def on_error(err): 32 | assert False 33 | 34 | def on_completed(): 35 | assert True 36 | 37 | res.subscribe(on_next, on_error, on_completed) 38 | 39 | def test_from_callback_single(self): 40 | res = reactivex.from_callback(lambda file, cb: cb(file))("file.txt") 41 | 42 | def on_next(r): 43 | self.assertEqual(r, "file.txt") 44 | 45 | def on_error(err): 46 | assert False 47 | 48 | def on_completed(): 49 | assert True 50 | 51 | res.subscribe(on_next, on_error, on_completed) 52 | 53 | def test_from_node_callback_mapper(self): 54 | res = reactivex.from_callback(lambda f, s, t, cb: cb(f, s, t), lambda r: r[0])( 55 | 1, 2, 3 56 | ) 57 | 58 | def on_next(r): 59 | self.assertEqual(r, 1) 60 | 61 | def on_error(err): 62 | assert False 63 | 64 | def on_completed(): 65 | assert True 66 | 67 | res.subscribe(on_next, on_error, on_completed) 68 | -------------------------------------------------------------------------------- /reactivex/observable/generate.py: -------------------------------------------------------------------------------- 1 | from typing import Any, TypeVar, cast 2 | 3 | from reactivex import Observable, abc, typing 4 | from reactivex.disposable import MultipleAssignmentDisposable 5 | from reactivex.scheduler import CurrentThreadScheduler 6 | 7 | _TState = TypeVar("_TState") 8 | 9 | 10 | def generate_( 11 | initial_state: _TState, 12 | condition: typing.Predicate[_TState], 13 | iterate: typing.Mapper[_TState, _TState], 14 | ) -> Observable[_TState]: 15 | def subscribe( 16 | observer: abc.ObserverBase[_TState], 17 | scheduler: abc.SchedulerBase | None = None, 18 | ) -> abc.DisposableBase: 19 | scheduler = scheduler or CurrentThreadScheduler.singleton() 20 | first = True 21 | state = initial_state 22 | mad = MultipleAssignmentDisposable() 23 | 24 | def action(scheduler: abc.SchedulerBase, state1: Any = None) -> None: 25 | nonlocal first 26 | nonlocal state 27 | 28 | has_result = False 29 | result: _TState = cast(_TState, None) 30 | 31 | try: 32 | if first: 33 | first = False 34 | else: 35 | state = iterate(state) 36 | 37 | has_result = condition(state) 38 | if has_result: 39 | result = state 40 | 41 | except Exception as exception: # pylint: disable=broad-except 42 | observer.on_error(exception) 43 | return 44 | 45 | if has_result: 46 | observer.on_next(result) 47 | mad.disposable = scheduler.schedule(action) 48 | else: 49 | observer.on_completed() 50 | 51 | mad.disposable = scheduler.schedule(action) 52 | return mad 53 | 54 | return Observable(subscribe) 55 | 56 | 57 | __all__ = ["generate_"] 58 | -------------------------------------------------------------------------------- /reactivex/operators/_reduce.py: -------------------------------------------------------------------------------- 1 | from typing import Any, TypeVar, cast 2 | 3 | from reactivex import Observable 4 | from reactivex import operators as ops 5 | from reactivex.internal import curry_flip 6 | from reactivex.internal.utils import NotSet 7 | from reactivex.typing import Accumulator 8 | 9 | _T = TypeVar("_T") 10 | _TState = TypeVar("_TState") 11 | 12 | 13 | @curry_flip 14 | def reduce_( 15 | source: Observable[_T], 16 | accumulator: Accumulator[_TState, _T], 17 | seed: _TState | type[NotSet] = NotSet, 18 | ) -> Observable[Any]: 19 | """Applies an accumulator function over an observable sequence, 20 | returning the result of the aggregation as a single element in the 21 | result sequence. The specified seed value is used as the initial 22 | accumulator value. 23 | 24 | For aggregation behavior with incremental intermediate results, see 25 | `scan()`. 26 | 27 | Examples: 28 | >>> result = source.pipe(reduce(lambda acc, x: acc + x)) 29 | >>> result = reduce(lambda acc, x: acc + x)(source) 30 | >>> result = source.pipe(reduce(lambda acc, x: acc + x, 0)) 31 | 32 | Args: 33 | source: The source observable. 34 | accumulator: An accumulator function to be invoked on each element. 35 | seed: Optional initial accumulator value. 36 | 37 | Returns: 38 | An observable sequence containing a single element with the 39 | final accumulator value. 40 | """ 41 | if seed is not NotSet: 42 | seed_: _TState = cast(_TState, seed) 43 | scanner = ops.scan(accumulator, seed=seed_) 44 | return source.pipe( 45 | scanner, 46 | ops.last_or_default(default_value=seed_), 47 | ) 48 | 49 | return source.pipe( 50 | ops.scan(cast(Accumulator[_T, _T], accumulator)), 51 | ops.last(), 52 | ) 53 | 54 | 55 | __all__ = ["reduce_"] 56 | -------------------------------------------------------------------------------- /tests/test_observable/test_forin.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import reactivex 4 | from reactivex.observable.observable import Observable 5 | from reactivex.testing import ReactiveTest, TestScheduler 6 | 7 | on_next = ReactiveTest.on_next 8 | on_completed = ReactiveTest.on_completed 9 | on_error = ReactiveTest.on_error 10 | subscribe = ReactiveTest.subscribe 11 | subscribed = ReactiveTest.subscribed 12 | disposed = ReactiveTest.disposed 13 | created = ReactiveTest.created 14 | 15 | 16 | class TestForIn(unittest.TestCase): 17 | def test_for_basic(self): 18 | scheduler = TestScheduler() 19 | 20 | def create(): 21 | def mapper(x: int) -> Observable[int]: 22 | return scheduler.create_cold_observable( 23 | on_next(x * 100 + 10, x * 10 + 1), 24 | on_next(x * 100 + 20, x * 10 + 2), 25 | on_next(x * 100 + 30, x * 10 + 3), 26 | on_completed(x * 100 + 40), 27 | ) 28 | 29 | return reactivex.for_in([1, 2, 3], mapper) 30 | 31 | results = scheduler.start(create=create) 32 | assert results.messages == [ 33 | on_next(310, 11), 34 | on_next(320, 12), 35 | on_next(330, 13), 36 | on_next(550, 21), 37 | on_next(560, 22), 38 | on_next(570, 23), 39 | on_next(890, 31), 40 | on_next(900, 32), 41 | on_next(910, 33), 42 | on_completed(920), 43 | ] 44 | 45 | def test_for_throws(self): 46 | ex = "ex" 47 | scheduler = TestScheduler() 48 | 49 | def create(): 50 | def mapper(x: int): 51 | raise Exception(ex) 52 | 53 | return reactivex.for_in([1, 2, 3], mapper) 54 | 55 | results = scheduler.start(create=create) 56 | assert results.messages == [on_error(200, ex)] 57 | -------------------------------------------------------------------------------- /docs/additional_reading.rst: -------------------------------------------------------------------------------- 1 | .. _additional_reading: 2 | 3 | Additional Reading 4 | ================== 5 | 6 | Open Material 7 | ------------- 8 | 9 | The RxPY source repository contains `example notebooks 10 | `_. 11 | 12 | The official ReactiveX website contains additional tutorials and documentation: 13 | 14 | * `Introduction `_ 15 | * `Tutorials `_ 16 | * `Operators `_ 17 | 18 | Several commercial contents have their associated example code available freely: 19 | 20 | * `Packt Reactive Programming in Python `_ 21 | 22 | RxPY 3.0.0 has removed support for backpressure here are the known community projects supporting backpressure: 23 | 24 | * `rxbackpressure rxpy extension `_ 25 | * `rxpy_backpressure observer decorators `_ 26 | 27 | Commercial Material 28 | -------------------- 29 | 30 | **O\'Reilly Video** 31 | 32 | O'Reilly has published the video *Reactive Python for Data Science* which is 33 | available on both the `O\'Reilly Store 34 | `_ as well as `O\'Reilly 35 | Safari `_. 36 | This video teaches RxPY from scratch with applications towards data science, but 37 | should be helpful for anyone seeking to learn RxPY and reactive programming. 38 | 39 | **Packt Video** 40 | 41 | Packt has published the video *Reactive Programming in Python*, available on 42 | `Packt store 43 | `_. 44 | This video teaches how to write reactive GUI and network applications. 45 | 46 | -------------------------------------------------------------------------------- /reactivex/observable/fromiterable.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterable 2 | from typing import Any, TypeVar 3 | 4 | from reactivex import Observable, abc 5 | from reactivex.disposable import CompositeDisposable, Disposable 6 | from reactivex.scheduler import CurrentThreadScheduler 7 | 8 | _T = TypeVar("_T") 9 | 10 | 11 | def from_iterable_( 12 | iterable: Iterable[_T], scheduler: abc.SchedulerBase | None = None 13 | ) -> Observable[_T]: 14 | """Converts an iterable to an observable sequence. 15 | 16 | Example: 17 | >>> from_iterable([1,2,3]) 18 | 19 | Args: 20 | iterable: A Python iterable 21 | scheduler: An optional scheduler to schedule the values on. 22 | 23 | Returns: 24 | The observable sequence whose elements are pulled from the 25 | given iterable sequence. 26 | """ 27 | 28 | def subscribe( 29 | observer: abc.ObserverBase[_T], scheduler_: abc.SchedulerBase | None = None 30 | ) -> abc.DisposableBase: 31 | _scheduler = scheduler or scheduler_ or CurrentThreadScheduler.singleton() 32 | iterator = iter(iterable) 33 | disposed = False 34 | 35 | def action(_: abc.SchedulerBase, __: Any = None) -> None: 36 | nonlocal disposed 37 | 38 | try: 39 | while not disposed: 40 | value = next(iterator) 41 | observer.on_next(value) 42 | except StopIteration: 43 | observer.on_completed() 44 | except Exception as error: # pylint: disable=broad-except 45 | observer.on_error(error) 46 | 47 | def dispose() -> None: 48 | nonlocal disposed 49 | disposed = True 50 | 51 | disp = Disposable(dispose) 52 | return CompositeDisposable(_scheduler.schedule(action), disp) 53 | 54 | return Observable(subscribe) 55 | 56 | 57 | __all__ = ["from_iterable_"] 58 | -------------------------------------------------------------------------------- /reactivex/observable/ifthen.py: -------------------------------------------------------------------------------- 1 | from asyncio import Future 2 | from collections.abc import Callable 3 | from typing import TypeVar, Union 4 | 5 | import reactivex 6 | from reactivex import Observable, abc 7 | 8 | _T = TypeVar("_T") 9 | 10 | 11 | def if_then_( 12 | condition: Callable[[], bool], 13 | then_source: Union[Observable[_T], "Future[_T]"], 14 | else_source: Union[None, Observable[_T], "Future[_T]"] = None, 15 | ) -> Observable[_T]: 16 | """Determines whether an observable collection contains values. 17 | 18 | Example: 19 | 1 - res = reactivex.if_then(condition, obs1) 20 | 2 - res = reactivex.if_then(condition, obs1, obs2) 21 | 22 | Args: 23 | condition: The condition which determines if the then_source or 24 | else_source will be run. 25 | then_source: The observable sequence or Promise that 26 | will be run if the condition function returns true. 27 | else_source: [Optional] The observable sequence or 28 | Promise that will be run if the condition function returns 29 | False. If this is not provided, it defaults to 30 | reactivex.empty 31 | 32 | Returns: 33 | An observable sequence which is either the then_source or 34 | else_source. 35 | """ 36 | 37 | else_source_: Observable[_T] | Future[_T] = else_source or reactivex.empty() 38 | 39 | then_source = ( 40 | reactivex.from_future(then_source) 41 | if isinstance(then_source, Future) 42 | else then_source 43 | ) 44 | else_source_ = ( 45 | reactivex.from_future(else_source_) 46 | if isinstance(else_source_, Future) 47 | else else_source_ 48 | ) 49 | 50 | def factory(_: abc.SchedulerBase) -> Union[Observable[_T], "Future[_T]"]: 51 | return then_source if condition() else else_source_ 52 | 53 | return reactivex.defer(factory) 54 | 55 | 56 | __all__ = ["if_then_"] 57 | -------------------------------------------------------------------------------- /reactivex/observable/using.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from typing import TypeVar 3 | 4 | import reactivex 5 | from reactivex import Observable, abc 6 | from reactivex.disposable import CompositeDisposable, Disposable 7 | 8 | _T = TypeVar("_T") 9 | 10 | 11 | def using_( 12 | resource_factory: Callable[[], abc.DisposableBase | None], 13 | observable_factory: Callable[[abc.DisposableBase | None], Observable[_T]], 14 | ) -> Observable[_T]: 15 | """Constructs an observable sequence that depends on a resource 16 | object, whose lifetime is tied to the resulting observable 17 | sequence's lifetime. 18 | 19 | Example: 20 | >>> res = reactivex.using(lambda: AsyncSubject(), lambda: s: s) 21 | 22 | Args: 23 | resource_factory: Factory function to obtain a resource object. 24 | observable_factory: Factory function to obtain an observable 25 | sequence that depends on the obtained resource. 26 | 27 | Returns: 28 | An observable sequence whose lifetime controls the lifetime 29 | of the dependent resource object. 30 | """ 31 | 32 | def subscribe( 33 | observer: abc.ObserverBase[_T], scheduler: abc.SchedulerBase | None = None 34 | ) -> abc.DisposableBase: 35 | disp: abc.DisposableBase = Disposable() 36 | 37 | try: 38 | resource = resource_factory() 39 | if resource is not None: 40 | disp = resource 41 | 42 | source = observable_factory(resource) 43 | except Exception as exception: # pylint: disable=broad-except 44 | d = reactivex.throw(exception).subscribe(observer, scheduler=scheduler) 45 | return CompositeDisposable(d, disp) 46 | 47 | return CompositeDisposable( 48 | source.subscribe(observer, scheduler=scheduler), disp 49 | ) 50 | 51 | return Observable(subscribe) 52 | 53 | 54 | __all__ = ["using_"] 55 | -------------------------------------------------------------------------------- /reactivex/disposable/serialdisposable.py: -------------------------------------------------------------------------------- 1 | from threading import RLock 2 | 3 | from reactivex import abc 4 | 5 | 6 | class SerialDisposable(abc.DisposableBase): 7 | """Represents a disposable resource whose underlying disposable 8 | resource can be replaced by another disposable resource, causing 9 | automatic disposal of the previous underlying disposable resource. 10 | """ 11 | 12 | def __init__(self) -> None: 13 | self.current: abc.DisposableBase | None = None 14 | self.is_disposed = False 15 | self.lock = RLock() 16 | 17 | super().__init__() 18 | 19 | def get_disposable(self) -> abc.DisposableBase | None: 20 | return self.current 21 | 22 | def set_disposable(self, value: abc.DisposableBase) -> None: 23 | """If the SerialDisposable has already been disposed, assignment 24 | to this property causes immediate disposal of the given 25 | disposable object. Assigning this property disposes the previous 26 | disposable object.""" 27 | 28 | old: abc.DisposableBase | None = None 29 | 30 | with self.lock: 31 | should_dispose = self.is_disposed 32 | if not should_dispose: 33 | old = self.current 34 | self.current = value 35 | 36 | if old is not None: 37 | old.dispose() 38 | 39 | if should_dispose: 40 | value.dispose() 41 | 42 | disposable = property(get_disposable, set_disposable) 43 | 44 | def dispose(self) -> None: 45 | """Disposes the underlying disposable as well as all future 46 | replacements.""" 47 | 48 | old: abc.DisposableBase | None = None 49 | 50 | with self.lock: 51 | if not self.is_disposed: 52 | self.is_disposed = True 53 | old = self.current 54 | self.current = None 55 | 56 | if old is not None: 57 | old.dispose() 58 | -------------------------------------------------------------------------------- /reactivex/operators/_takeuntilwithtime.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Any, TypeVar 3 | 4 | from reactivex import Observable, abc, typing 5 | from reactivex.disposable import CompositeDisposable 6 | from reactivex.internal import curry_flip 7 | from reactivex.scheduler import TimeoutScheduler 8 | 9 | _T = TypeVar("_T") 10 | 11 | 12 | @curry_flip 13 | def take_until_with_time_( 14 | source: Observable[_T], 15 | end_time: typing.AbsoluteOrRelativeTime, 16 | scheduler: abc.SchedulerBase | None = None, 17 | ) -> Observable[_T]: 18 | """Takes elements for the specified duration until the specified end 19 | time, using the specified scheduler to run timers. 20 | 21 | Examples: 22 | >>> source.pipe(take_until_with_time(dt)) 23 | >>> take_until_with_time(5.0)(source) 24 | 25 | Args: 26 | source: Source observable to take elements from. 27 | end_time: Absolute or relative time when to complete. 28 | scheduler: Scheduler to use for timing. 29 | 30 | Returns: 31 | An observable sequence with the elements taken 32 | until the specified end time. 33 | """ 34 | 35 | def subscribe( 36 | observer: abc.ObserverBase[_T], 37 | scheduler_: abc.SchedulerBase | None = None, 38 | ) -> abc.DisposableBase: 39 | _scheduler = scheduler or scheduler_ or TimeoutScheduler.singleton() 40 | 41 | def action(scheduler: abc.SchedulerBase, state: Any = None): 42 | observer.on_completed() 43 | 44 | if isinstance(end_time, datetime): 45 | task = _scheduler.schedule_absolute(end_time, action) 46 | else: 47 | task = _scheduler.schedule_relative(end_time, action) 48 | 49 | return CompositeDisposable( 50 | task, source.subscribe(observer, scheduler=scheduler_) 51 | ) 52 | 53 | return Observable(subscribe) 54 | 55 | 56 | __all__ = ["take_until_with_time_"] 57 | -------------------------------------------------------------------------------- /reactivex/observable/toasync.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from typing import Any, TypeVar 3 | 4 | from reactivex import Observable, abc 5 | from reactivex import operators as ops 6 | from reactivex.scheduler import TimeoutScheduler 7 | from reactivex.subject import AsyncSubject 8 | 9 | _T = TypeVar("_T") 10 | 11 | 12 | def to_async_( 13 | func: Callable[..., _T], scheduler: abc.SchedulerBase | None = None 14 | ) -> Callable[..., Observable[_T]]: 15 | """Converts the function into an asynchronous function. Each 16 | invocation of the resulting asynchronous function causes an 17 | invocation of the original synchronous function on the specified 18 | scheduler. 19 | 20 | Examples: 21 | res = reactivex.to_async(lambda x, y: x + y)(4, 3) 22 | res = reactivex.to_async(lambda x, y: x + y, Scheduler.timeout)(4, 3) 23 | res = reactivex.to_async(lambda x: log.debug(x), Scheduler.timeout)('hello') 24 | 25 | Args: 26 | func: Function to convert to an asynchronous function. 27 | scheduler: [Optional] Scheduler to run the function on. If not 28 | specified, defaults to Scheduler.timeout. 29 | 30 | Returns: 31 | Aynchronous function. 32 | """ 33 | 34 | _scheduler = scheduler or TimeoutScheduler.singleton() 35 | 36 | def wrapper(*args: Any) -> Observable[_T]: 37 | subject: AsyncSubject[_T] = AsyncSubject() 38 | 39 | def action(scheduler: abc.SchedulerBase, state: Any = None) -> None: 40 | try: 41 | result = func(*args) 42 | except Exception as ex: # pylint: disable=broad-except 43 | subject.on_error(ex) 44 | return 45 | 46 | subject.on_next(result) 47 | subject.on_completed() 48 | 49 | _scheduler.schedule(action) 50 | return subject.pipe(ops.as_observable()) 51 | 52 | return wrapper 53 | 54 | 55 | __all__ = ["to_async_"] 56 | -------------------------------------------------------------------------------- /reactivex/internal/curry.py: -------------------------------------------------------------------------------- 1 | """Simplified curry_flip decorator for RxPY operators. 2 | 3 | Adapted from Expression library (https://github.com/dbrattli/Expression) 4 | Simplified to always curry_flip with 1 argument as that's the only case 5 | needed for RxPY operators. 6 | """ 7 | 8 | import functools 9 | from collections.abc import Callable 10 | from typing import Concatenate, TypeVar 11 | 12 | from typing_extensions import ParamSpec 13 | 14 | _P = ParamSpec("_P") 15 | _A = TypeVar("_A") 16 | _B = TypeVar("_B") 17 | 18 | 19 | def curry_flip( 20 | fun: Callable[Concatenate[_A, _P], _B], 21 | ) -> Callable[_P, Callable[[_A], _B]]: 22 | """A flipped curry decorator for single-argument currying. 23 | 24 | Makes a function curried, but flips the curried argument to become 25 | the last argument. This is perfect for RxPY operators where the source 26 | Observable should be the last argument to enable piping. 27 | 28 | This is a simplified version that always curries exactly 1 argument, 29 | as that's all RxPY operators need. 30 | 31 | Args: 32 | fun: The function to curry-flip. Must take at least one argument. 33 | 34 | Returns: 35 | A curried function where the first argument becomes the last. 36 | 37 | Example: 38 | >>> @curry_flip 39 | ... def take(count: int, source: Observable[int]) -> Observable[int]: 40 | ... # implementation 41 | ... pass 42 | >>> 43 | >>> # Now can be used in pipe: 44 | >>> result = source.pipe(take(5)) 45 | >>> # Or called directly: 46 | >>> result = take(5)(source) 47 | """ 48 | 49 | @functools.wraps(fun) 50 | def _wrap_args(*args: _P.args, **kwargs: _P.kwargs) -> Callable[[_A], _B]: 51 | def _wrap_curried(curry_arg: _A) -> _B: 52 | return fun(curry_arg, *args, **kwargs) 53 | 54 | return _wrap_curried 55 | 56 | return _wrap_args 57 | 58 | 59 | __all__ = ["curry_flip"] 60 | -------------------------------------------------------------------------------- /examples/timeflies/timeflies_qt.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import reactivex 4 | from reactivex import operators as ops 5 | from reactivex.scheduler.mainloop import QtScheduler 6 | from reactivex.subject import Subject 7 | 8 | try: 9 | from PySide2 import QtCore 10 | from PySide2.QtWidgets import QApplication, QLabel, QWidget 11 | except ImportError: 12 | try: 13 | from PyQt5 import QtCore 14 | from PyQt5.QtWidgets import QApplication, QLabel, QWidget 15 | except ImportError: 16 | raise ImportError("Please ensure either PySide2 or PyQt5 is available!") 17 | 18 | 19 | class Window(QWidget): 20 | def __init__(self): 21 | QWidget.__init__(self) 22 | self.setWindowTitle("Rx for Python rocks") 23 | self.resize(600, 600) 24 | self.setMouseTracking(True) 25 | 26 | # This Subject is used to transmit mouse moves to labels 27 | self.mousemove = Subject() 28 | 29 | def mouseMoveEvent(self, event): 30 | self.mousemove.on_next((event.x(), event.y())) 31 | 32 | 33 | def main(): 34 | app = QApplication(sys.argv) 35 | scheduler = QtScheduler(QtCore) 36 | 37 | window = Window() 38 | window.show() 39 | 40 | text = "TIME FLIES LIKE AN ARROW" 41 | 42 | def on_next(info): 43 | label, (x, y), i = info 44 | label.move(x + i * 12 + 15, y) 45 | label.show() 46 | 47 | def handle_label(label, i): 48 | delayer = ops.delay(i * 0.100) 49 | mapper = ops.map(lambda xy: (label, xy, i)) 50 | 51 | return window.mousemove.pipe( 52 | delayer, 53 | mapper, 54 | ) 55 | 56 | labeler = ops.flat_map_indexed(handle_label) 57 | mapper = ops.map(lambda c: QLabel(c, window)) 58 | 59 | reactivex.from_(text).pipe( 60 | mapper, 61 | labeler, 62 | ).subscribe(on_next, on_error=print, scheduler=scheduler) 63 | 64 | sys.exit(app.exec_()) 65 | 66 | 67 | if __name__ == "__main__": 68 | main() 69 | -------------------------------------------------------------------------------- /reactivex/observer/autodetachobserver.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | from reactivex.disposable import SingleAssignmentDisposable 4 | from reactivex.internal import default_error, noop 5 | 6 | from .. import abc, typing 7 | 8 | _T_in = TypeVar("_T_in", contravariant=True) 9 | 10 | 11 | class AutoDetachObserver(abc.ObserverBase[_T_in]): 12 | def __init__( 13 | self, 14 | on_next: typing.OnNext[_T_in] | None = None, 15 | on_error: typing.OnError | None = None, 16 | on_completed: typing.OnCompleted | None = None, 17 | ) -> None: 18 | self._on_next = on_next or noop 19 | self._on_error = on_error or default_error 20 | self._on_completed = on_completed or noop 21 | 22 | self._subscription = SingleAssignmentDisposable() 23 | self.is_stopped = False 24 | 25 | def on_next(self, value: _T_in) -> None: 26 | if self.is_stopped: 27 | return 28 | self._on_next(value) 29 | 30 | def on_error(self, error: Exception) -> None: 31 | if self.is_stopped: 32 | return 33 | self.is_stopped = True 34 | 35 | try: 36 | self._on_error(error) 37 | finally: 38 | self.dispose() 39 | 40 | def on_completed(self) -> None: 41 | if self.is_stopped: 42 | return 43 | self.is_stopped = True 44 | 45 | try: 46 | self._on_completed() 47 | finally: 48 | self.dispose() 49 | 50 | def set_disposable(self, value: abc.DisposableBase) -> None: 51 | self._subscription.disposable = value 52 | 53 | subscription = property(fset=set_disposable) 54 | 55 | def dispose(self) -> None: 56 | self.is_stopped = True 57 | self._subscription.dispose() 58 | 59 | def fail(self, exn: Exception) -> bool: 60 | if self.is_stopped: 61 | return False 62 | 63 | self.is_stopped = True 64 | self._on_error(exn) 65 | return True 66 | -------------------------------------------------------------------------------- /examples/timeflies/timeflies_gtk.py: -------------------------------------------------------------------------------- 1 | import gi 2 | from gi.repository import Gdk, GLib, Gtk 3 | 4 | import reactivex 5 | from reactivex import operators as ops 6 | from reactivex.scheduler.mainloop import GtkScheduler 7 | from reactivex.subject import Subject 8 | 9 | gi.require_version("Gtk", "3.0") 10 | 11 | 12 | class Window(Gtk.Window): 13 | def __init__(self): 14 | super().__init__() 15 | self.resize(600, 600) 16 | 17 | self.add_events(Gdk.EventMask.POINTER_MOTION_MASK) 18 | self.connect("motion-notify-event", self.on_mouse_move) 19 | 20 | self.mousemove = Subject() 21 | 22 | def on_mouse_move(self, widget, event): 23 | self.mousemove.on_next((event.x, event.y)) 24 | 25 | 26 | def main(): 27 | scheduler = GtkScheduler(GLib) 28 | scrolled_window = Gtk.ScrolledWindow() 29 | 30 | window = Window() 31 | window.connect("delete-event", Gtk.main_quit) 32 | 33 | container = Gtk.Fixed() 34 | 35 | scrolled_window.add(container) 36 | window.add(scrolled_window) 37 | text = "TIME FLIES LIKE AN ARROW" 38 | 39 | def on_next(info): 40 | label, (x, y), i = info 41 | container.move(label, x + i * 12 + 15, y) 42 | label.show() 43 | 44 | def handle_label(label, i): 45 | delayer = ops.delay(i * 0.100) 46 | mapper = ops.map(lambda xy: (label, xy, i)) 47 | 48 | return window.mousemove.pipe( 49 | delayer, 50 | mapper, 51 | ) 52 | 53 | def make_label(char): 54 | label = Gtk.Label(label=char) 55 | container.put(label, 0, 0) 56 | label.hide() 57 | return label 58 | 59 | mapper = ops.map(make_label) 60 | labeler = ops.flat_map_indexed(handle_label) 61 | 62 | reactivex.from_(text).pipe( 63 | mapper, 64 | labeler, 65 | ).subscribe(on_next, on_error=print, scheduler=scheduler) 66 | 67 | window.show_all() 68 | 69 | Gtk.main() 70 | 71 | 72 | if __name__ == "__main__": 73 | main() 74 | -------------------------------------------------------------------------------- /reactivex/operators/_average.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Any, TypeVar, cast 3 | 4 | from reactivex import Observable, operators, typing 5 | from reactivex.internal import curry_flip 6 | 7 | _T = TypeVar("_T") 8 | 9 | 10 | @dataclass 11 | class AverageValue: 12 | sum: float 13 | count: int 14 | 15 | 16 | @curry_flip 17 | def average_( 18 | source: Observable[_T], 19 | key_mapper: typing.Mapper[_T, float] | None = None, 20 | ) -> Observable[float]: 21 | """Computes the average of an observable sequence of values. 22 | 23 | Computes the average of an observable sequence of values that 24 | are in the sequence or obtained by invoking a transform 25 | function on each element of the input sequence if present. 26 | 27 | Examples: 28 | >>> result = source.pipe(average()) 29 | >>> result = average()(source) 30 | >>> result = source.pipe(average(lambda x: x.value)) 31 | 32 | Args: 33 | source: Source observable to average. 34 | key_mapper: Optional mapper to extract numeric values. 35 | 36 | Returns: 37 | An observable sequence containing a single element with the 38 | average of the sequence of values. 39 | """ 40 | 41 | key_mapper_: typing.Mapper[_T, float] = key_mapper or ( 42 | lambda x: float(cast(Any, x)) 43 | ) 44 | 45 | def accumulator(prev: AverageValue, cur: float) -> AverageValue: 46 | return AverageValue(sum=prev.sum + cur, count=prev.count + 1) 47 | 48 | def mapper(s: AverageValue) -> float: 49 | if s.count == 0: 50 | raise Exception("The input sequence was empty") 51 | 52 | return s.sum / float(s.count) 53 | 54 | seed = AverageValue(sum=0, count=0) 55 | 56 | ret = source.pipe( 57 | operators.map(key_mapper_), 58 | operators.scan(accumulator, seed), 59 | operators.last(), 60 | operators.map(mapper), 61 | ) 62 | return ret 63 | 64 | 65 | __all__ = ["average_"] 66 | -------------------------------------------------------------------------------- /reactivex/operators/_subscribeon.py: -------------------------------------------------------------------------------- 1 | from typing import Any, TypeVar 2 | 3 | from reactivex import Observable, abc 4 | from reactivex.disposable import ( 5 | ScheduledDisposable, 6 | SerialDisposable, 7 | SingleAssignmentDisposable, 8 | ) 9 | from reactivex.internal import curry_flip 10 | 11 | _T = TypeVar("_T") 12 | 13 | 14 | @curry_flip 15 | def subscribe_on_( 16 | source: Observable[_T], 17 | scheduler: abc.SchedulerBase, 18 | ) -> Observable[_T]: 19 | """Subscribe on the specified scheduler. 20 | 21 | Wrap the source sequence in order to run its subscription and 22 | unsubscription logic on the specified scheduler. This operation 23 | is not commonly used; see the remarks section for more 24 | information on the distinction between subscribe_on and 25 | observe_on. 26 | 27 | This only performs the side-effects of subscription and 28 | unsubscription on the specified scheduler. In order to invoke 29 | observer callbacks on a scheduler, use observe_on. 30 | 31 | Examples: 32 | >>> res = source.pipe(subscribe_on(scheduler)) 33 | >>> res = subscribe_on(scheduler)(source) 34 | 35 | Args: 36 | source: The source observable. 37 | scheduler: Scheduler to use for subscription/unsubscription. 38 | 39 | Returns: 40 | The source sequence whose subscriptions and 41 | un-subscriptions happen on the specified scheduler. 42 | """ 43 | 44 | def subscribe(observer: abc.ObserverBase[_T], _: abc.SchedulerBase | None = None): 45 | m = SingleAssignmentDisposable() 46 | d = SerialDisposable() 47 | d.disposable = m 48 | 49 | def action(scheduler: abc.SchedulerBase, state: Any | None = None): 50 | d.disposable = ScheduledDisposable( 51 | scheduler, source.subscribe(observer, scheduler=scheduler) 52 | ) 53 | 54 | m.disposable = scheduler.schedule(action) 55 | return d 56 | 57 | return Observable(subscribe) 58 | 59 | 60 | __all__ = ["subscribe_on_"] 61 | -------------------------------------------------------------------------------- /reactivex/operators/_find.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable 2 | from typing import TypeVar 3 | 4 | from reactivex import Observable, abc 5 | from reactivex.internal import curry_flip 6 | 7 | _T = TypeVar("_T") 8 | 9 | 10 | @curry_flip 11 | def find_value_( 12 | source: Observable[_T], 13 | predicate: Callable[[_T, int, Observable[_T]], bool], 14 | yield_index: bool, 15 | ) -> Observable[_T | int | None]: 16 | """Searches for an element in an observable sequence. 17 | 18 | Examples: 19 | >>> res = source.pipe(find_value(lambda x, i, s: x > 3, False)) 20 | >>> res = find_value(lambda x, i, s: x > 3, False)(source) 21 | 22 | Args: 23 | source: The source observable sequence. 24 | predicate: A function to test each element. 25 | yield_index: Whether to yield the index or the value. 26 | 27 | Returns: 28 | An observable sequence containing the found element or index. 29 | """ 30 | 31 | def subscribe( 32 | observer: abc.ObserverBase[_T | int | None], 33 | scheduler: abc.SchedulerBase | None = None, 34 | ) -> abc.DisposableBase: 35 | index = 0 36 | 37 | def on_next(x: _T) -> None: 38 | nonlocal index 39 | should_run = False 40 | try: 41 | should_run = predicate(x, index, source) 42 | except Exception as ex: # pylint: disable=broad-except 43 | observer.on_error(ex) 44 | return 45 | 46 | if should_run: 47 | observer.on_next(index if yield_index else x) 48 | observer.on_completed() 49 | else: 50 | index += 1 51 | 52 | def on_completed(): 53 | observer.on_next(-1 if yield_index else None) 54 | observer.on_completed() 55 | 56 | return source.subscribe( 57 | on_next, observer.on_error, on_completed, scheduler=scheduler 58 | ) 59 | 60 | return Observable(subscribe) 61 | 62 | 63 | __all__ = ["find_value_"] 64 | -------------------------------------------------------------------------------- /reactivex/operators/_scan.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, cast 2 | 3 | from reactivex import Observable, abc, defer 4 | from reactivex import operators as ops 5 | from reactivex.internal import curry_flip 6 | from reactivex.internal.utils import NotSet 7 | from reactivex.typing import Accumulator 8 | 9 | _T = TypeVar("_T") 10 | _TState = TypeVar("_TState") 11 | 12 | 13 | @curry_flip 14 | def scan_( 15 | source: Observable[_T], 16 | accumulator: Accumulator[_TState, _T], 17 | seed: _TState | type[NotSet] = NotSet, 18 | ) -> Observable[_TState]: 19 | """Applies an accumulator function over an observable sequence and 20 | returns each intermediate result. 21 | 22 | Examples: 23 | >>> result = source.pipe(scan(lambda acc, x: acc + x, 0)) 24 | >>> result = scan(lambda acc, x: acc + x, 0)(source) 25 | >>> result = source.pipe(scan(lambda acc, x: acc + x)) 26 | 27 | Args: 28 | source: The observable source to scan. 29 | accumulator: An accumulator function to invoke on each element. 30 | seed: Optional initial accumulator value. 31 | 32 | Returns: 33 | An observable sequence containing the accumulated values. 34 | """ 35 | has_seed = seed is not NotSet 36 | 37 | def factory(scheduler: abc.SchedulerBase) -> Observable[_TState]: 38 | has_accumulation = False 39 | accumulation: _TState = cast(_TState, None) 40 | 41 | def projection(x: _T) -> _TState: 42 | nonlocal has_accumulation 43 | nonlocal accumulation 44 | 45 | if has_accumulation: 46 | accumulation = accumulator(accumulation, x) 47 | else: 48 | accumulation = ( 49 | accumulator(cast(_TState, seed), x) 50 | if has_seed 51 | else cast(_TState, x) 52 | ) 53 | has_accumulation = True 54 | 55 | return accumulation 56 | 57 | return source.pipe(ops.map(projection)) 58 | 59 | return defer(factory) 60 | 61 | 62 | __all__ = ["scan_"] 63 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | code-quality: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | 11 | - name: Set up Python 3.12 12 | uses: actions/setup-python@v4 13 | with: 14 | python-version: "3.12" 15 | 16 | - name: Install uv 17 | uses: astral-sh/setup-uv@v4 18 | 19 | - name: Install dependencies 20 | run: | 21 | uv sync 22 | 23 | - name: Code checks 24 | run: | 25 | uv run pre-commit run --all-files --show-diff-on-failure 26 | 27 | build: 28 | strategy: 29 | matrix: 30 | platform: [ubuntu-latest, macos-latest, windows-latest] 31 | python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "pypy-3.10"] 32 | runs-on: ${{ matrix.platform }} 33 | 34 | steps: 35 | - uses: actions/checkout@v4 36 | - name: Set up Python ${{ matrix.python-version }} 37 | uses: actions/setup-python@v5 38 | with: 39 | python-version: ${{ matrix.python-version }} 40 | 41 | - name: Install uv 42 | uses: astral-sh/setup-uv@v4 43 | 44 | - name: Install dependencies 45 | run: | 46 | uv sync 47 | 48 | - name: Test with pytest 49 | run: | 50 | uv run pytest -n auto 51 | 52 | coverage: 53 | runs-on: ubuntu-latest 54 | steps: 55 | - uses: actions/checkout@v4 56 | 57 | - name: Set up Python 3.12 58 | uses: actions/setup-python@v5 59 | with: 60 | python-version: "3.12" 61 | 62 | - name: Install uv 63 | uses: astral-sh/setup-uv@v4 64 | 65 | - name: Install dependencies 66 | run: | 67 | uv sync 68 | 69 | - name: Run coverage tests 70 | run: | 71 | uv run coverage run -m pytest 72 | 73 | - name: Coveralls 74 | env: 75 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 76 | COVERALLS_SERVICE_NAME: github 77 | run: | 78 | uv run coveralls 79 | -------------------------------------------------------------------------------- /reactivex/internal/utils.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable, Iterable 2 | from functools import update_wrapper 3 | from types import FunctionType 4 | from typing import TYPE_CHECKING, Any, TypeVar, cast 5 | 6 | from typing_extensions import ParamSpec 7 | 8 | from reactivex import abc 9 | from reactivex.disposable import CompositeDisposable 10 | from reactivex.disposable.refcountdisposable import RefCountDisposable 11 | 12 | if TYPE_CHECKING: 13 | from reactivex import Observable 14 | 15 | _T = TypeVar("_T") 16 | _P = ParamSpec("_P") 17 | 18 | 19 | def add_ref(xs: "Observable[_T]", r: RefCountDisposable) -> "Observable[_T]": 20 | from reactivex import Observable 21 | 22 | def subscribe( 23 | observer: abc.ObserverBase[Any], scheduler: abc.SchedulerBase | None = None 24 | ) -> abc.DisposableBase: 25 | return CompositeDisposable(r.disposable, xs.subscribe(observer)) 26 | 27 | return Observable(subscribe) 28 | 29 | 30 | def infinite() -> Iterable[int]: 31 | n = 0 32 | while True: 33 | yield n 34 | n += 1 35 | 36 | 37 | def alias(name: str, doc: str, fun: Callable[_P, _T]) -> Callable[_P, _T]: 38 | # Adapted from 39 | # https://stackoverflow.com/questions/13503079/how-to-create-a-copy-of-a-python-function# 40 | # See also help(type(lambda: 0)) 41 | _fun = cast(FunctionType, fun) 42 | args = (_fun.__code__, _fun.__globals__) 43 | kwargs = {"name": name, "argdefs": _fun.__defaults__, "closure": _fun.__closure__} 44 | alias_ = FunctionType(*args, **kwargs) # type: ignore 45 | alias_ = update_wrapper(alias_, _fun) # type: ignore 46 | alias_.__kwdefaults__ = _fun.__kwdefaults__ # type: ignore 47 | alias_.__doc__ = doc 48 | alias_.__annotations__ = _fun.__annotations__ 49 | return cast(Callable[_P, _T], alias_) 50 | 51 | 52 | class NotSet: 53 | """Sentinel value.""" 54 | 55 | def __eq__(self, other: Any) -> bool: 56 | return self is other 57 | 58 | def __repr__(self) -> str: 59 | return "NotSet" 60 | 61 | 62 | __all__ = ["add_ref", "infinite", "alias", "NotSet"] 63 | -------------------------------------------------------------------------------- /reactivex/scheduler/trampoline.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | from threading import Condition, Lock 3 | 4 | from reactivex.internal.priorityqueue import PriorityQueue 5 | 6 | from .scheduleditem import ScheduledItem 7 | 8 | 9 | class Trampoline: 10 | def __init__(self) -> None: 11 | self._idle: bool = True 12 | self._queue: PriorityQueue[ScheduledItem] = PriorityQueue() 13 | self._lock: Lock = Lock() 14 | self._condition: Condition = Condition(self._lock) 15 | 16 | def idle(self) -> bool: 17 | with self._lock: 18 | return self._idle 19 | 20 | def run(self, item: ScheduledItem) -> None: 21 | with self._lock: 22 | self._queue.enqueue(item) 23 | if self._idle: 24 | self._idle = False 25 | else: 26 | self._condition.notify() 27 | return 28 | try: 29 | self._run() 30 | finally: 31 | with self._lock: 32 | self._idle = True 33 | self._queue.clear() 34 | 35 | def _run(self) -> None: 36 | ready: deque[ScheduledItem] = deque() 37 | while True: 38 | with self._lock: 39 | while len(self._queue) > 0: 40 | item: ScheduledItem = self._queue.peek() 41 | if item.duetime <= item.scheduler.now: 42 | self._queue.dequeue() 43 | ready.append(item) 44 | else: 45 | break 46 | 47 | while len(ready) > 0: 48 | item = ready.popleft() 49 | if not item.is_cancelled(): 50 | item.invoke() 51 | 52 | with self._lock: 53 | if len(self._queue) == 0: 54 | break 55 | item = self._queue.peek() 56 | seconds = (item.duetime - item.scheduler.now).total_seconds() 57 | if seconds > 0.0: 58 | self._condition.wait(seconds) 59 | 60 | 61 | __all__ = ["Trampoline"] 62 | -------------------------------------------------------------------------------- /reactivex/operators/_takewithtime.py: -------------------------------------------------------------------------------- 1 | from typing import Any, TypeVar 2 | 3 | from reactivex import Observable, abc, typing 4 | from reactivex.disposable import CompositeDisposable 5 | from reactivex.internal import curry_flip 6 | from reactivex.scheduler import TimeoutScheduler 7 | 8 | _T = TypeVar("_T") 9 | 10 | 11 | @curry_flip 12 | def take_with_time_( 13 | source: Observable[_T], 14 | duration: typing.RelativeTime, 15 | scheduler: abc.SchedulerBase | None = None, 16 | ) -> Observable[_T]: 17 | """Takes elements for the specified duration from the start of 18 | the observable source sequence. 19 | 20 | Examples: 21 | >>> source.pipe(take_with_time(5.0)) 22 | >>> take_with_time(5.0)(source) 23 | 24 | This operator accumulates a queue with a length enough to store 25 | elements received during the initial duration window. As more 26 | elements are received, elements older than the specified 27 | duration are taken from the queue and produced on the result 28 | sequence. This causes elements to be delayed with duration. 29 | 30 | Args: 31 | source: Source observable to take elements from. 32 | duration: Duration for taking elements from the start of 33 | the sequence. 34 | scheduler: Scheduler to use for timing. 35 | 36 | Returns: 37 | An observable sequence with the elements taken during the 38 | specified duration from the start of the source sequence. 39 | """ 40 | 41 | def subscribe( 42 | observer: abc.ObserverBase[_T], 43 | scheduler_: abc.SchedulerBase | None = None, 44 | ) -> abc.DisposableBase: 45 | _scheduler = scheduler or scheduler_ or TimeoutScheduler.singleton() 46 | 47 | def action(scheduler: abc.SchedulerBase, state: Any = None): 48 | observer.on_completed() 49 | 50 | disp = _scheduler.schedule_relative(duration, action) 51 | return CompositeDisposable( 52 | disp, source.subscribe(observer, scheduler=scheduler_) 53 | ) 54 | 55 | return Observable(subscribe) 56 | 57 | 58 | __all__ = ["take_with_time_"] 59 | -------------------------------------------------------------------------------- /reactivex/scheduler/periodicscheduler.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import TypeVar 3 | 4 | from reactivex import abc, typing 5 | from reactivex.disposable import Disposable, MultipleAssignmentDisposable 6 | 7 | from .scheduler import Scheduler 8 | 9 | _TState = TypeVar("_TState") 10 | 11 | 12 | class PeriodicScheduler(Scheduler, abc.PeriodicSchedulerBase): 13 | """Base class for the various periodic scheduler implementations in this 14 | package as well as the mainloop sub-package. 15 | """ 16 | 17 | def schedule_periodic( 18 | self, 19 | period: typing.RelativeTime, 20 | action: typing.ScheduledPeriodicAction[_TState], 21 | state: _TState | None = None, 22 | ) -> abc.DisposableBase: 23 | """Schedules a periodic piece of work. 24 | 25 | Args: 26 | period: Period in seconds or timedelta for running the 27 | work periodically. 28 | action: Action to be executed. 29 | state: [Optional] Initial state passed to the action upon 30 | the first iteration. 31 | 32 | Returns: 33 | The disposable object used to cancel the scheduled 34 | recurring action (best effort). 35 | """ 36 | 37 | disp: MultipleAssignmentDisposable = MultipleAssignmentDisposable() 38 | seconds: float = self.to_seconds(period) 39 | 40 | def periodic( 41 | scheduler: abc.SchedulerBase, state: _TState | None = None 42 | ) -> Disposable | None: 43 | if disp.is_disposed: 44 | return None 45 | 46 | now: datetime = scheduler.now 47 | 48 | try: 49 | state = action(state) 50 | except Exception: 51 | disp.dispose() 52 | raise 53 | 54 | time = seconds - (scheduler.now - now).total_seconds() 55 | disp.disposable = scheduler.schedule_relative(time, periodic, state=state) 56 | 57 | return None 58 | 59 | disp.disposable = self.schedule_relative(period, periodic, state=state) 60 | return disp 61 | --------------------------------------------------------------------------------