Repository: dbrattli/aioreactive (language: python)

Issue:
rx.delay frequently destroys tasks
Hi, i'm unsure if this is the intended behaviour, but rx.delay causes asyncio to frequently throw the following error.

if i'm not mistaken, rx.delay(x: seconds) is supposed to be an operator that delays the parent for x seconds.

this is how i think rx.delay should behave:

```python
def setup():
    return pipe(
        rx.interval(0, 2),
        rx.flat_map(many),
        rx.subscribe_async(observer),
    )


async def observer(x):
    print(x)
```

```python
async def delayed(x, y):
    await asyncio.sleep(y * 0.1)
    return (x, y)


def many(x: int):
    return rx.merge_seq(
        [
            pipe(
                delayed(x, 1),
                rx.from_async,
            ),
            pipe(
                delayed(x, 2),
                rx.from_async,
            ),
        ]
    )
```

however, when we use rx.delay instead of calling asyncio.sleep

```python
async def delayed(x, y):
    return (x, y)


def many(x: int):
    return rx.merge_seq(
        [
            pipe(
                delayed(x, 1),
                rx.from_async,
                rx.delay(0.1),
            ),
            pipe(
                delayed(x, 2),
                rx.from_async,
                rx.delay(0.2),
            ),
        ]
    )
```

the exceptions are thrown
```
(0, 1)
(0, 2)
ERROR:asyncio:Task was destroyed but it is pending!
task: <Task pending name='Task-17' coro=<delay.<locals>.subscribe_async.<locals>.worker.<locals>.loop() done, defined at /Users/shawnkoh/repos/ninjacado/.venv/lib/python3.10/site-packages/expression/core/fn.py:59> wait_for=<Future pending cb=[Task.task_wakeup()]>>
ERROR:asyncio:Task was destroyed but it is pending!
task: <Task pending name='Task-18' coro=<delay.<locals>.subscribe_async.<locals>.worker.<locals>.loop() done, defined at /Users/shawnkoh/repos/ninjacado/.venv/lib/python3.10/site-packages/expression/core/fn.py:59> wait_for=<Future pending cb=[Task.task_wakeup()]>>
(1, 1)
(1, 2)
(2, 1)
(2, 2)
(3, 1)
(3, 2)
ERROR:asyncio:Task was destroyed but it is pending!
task: <Task pending name='Task-43' coro=<delay.<locals>.subscribe_async.<locals>.worker.<locals>.loop() done, defined at /Users/shawnkoh/repos/ninjacado/.venv/lib/python3.10/site-packages/expression/core/fn.py:59> wait_for=<Future pending cb=[Task.task_wakeup()]>>
ERROR:asyncio:Task was destroyed but it is pending!
task: <Task pending name='Task-44' coro=<delay.<locals>.subscribe_async.<locals>.worker.<locals>.loop() done, defined at /Users/shawnkoh/repos/ninjacado/.venv/lib/python3.10/site-packages/expression/core/fn.py:59> wait_for=<Future pending cb=[Task.task_wakeup()]>>
ERROR:asyncio:Task was destroyed but it is pending!
task: <Task pending name='Task-56' coro=<delay.<locals>.subscribe_async.<locals>.worker.<locals>.loop() done, defined at /Users/shawnkoh/repos/ninjacado/.venv/lib/python3.10/site-packages/expression/core/fn.py:59> wait_for=<Future pending cb=[Task.task_wakeup()]>>
ERROR:asyncio:Task was destroyed but it is pending!
task: <Task pending name='Task-57' coro=<delay.<locals>.subscribe_async.<locals>.worker.<locals>.loop() done, defined at /Users/shawnkoh/repos/ninjacado/.venv/lib/python3.10/site-packages/expression/core/fn.py:59> wait_for=<Future pending cb=[Task.task_wakeup()]>>
(4, 1)
(4, 2)
```


Discussion / hints:
A workaround i've been using is this function

```python
@curry_flipped(1)
def sleep(
    source: AsyncObservable[_TSource],
    seconds: float,
    sleep_on_start: bool = False,
) -> AsyncObservable[_TSource]:
    started = sleep_on_start

    @curry_flipped(1)
    async def _sleep(source, seconds: float):
        nonlocal started
        if started:
            await asyncio.sleep(seconds)
        else:
            started = True
        return source

    return pipe(
        source,
        rx.map_async(_sleep(seconds)),
    )
```
example:
```python
def setup(token: str):
    session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=None))

    return pipe(
        rx.interval(0, 2),
        rx.flat_map(
            lambda _: pipe(
                range(14),
                rx.from_iterable,
                rx_helpers.sleep(0.1),
                rx.map(lambda y: arrow.now().shift(days=y)),
                rx.map_async(
                    schedules(token, session),
                ),
            )
        ),
        rx.subscribe_async(rx.AsyncAnonymousObserver()),
    )
```

Fix patch:
diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml
index 0138559..2d15da0 100644
--- a/.github/workflows/python-package.yml
+++ b/.github/workflows/python-package.yml
@@ -14,7 +14,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: [3.9, "3.10"]
+        python-version: ["3.11", "3.12"]
 
     steps:
       - uses: actions/checkout@v2
diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml
index b657205..c0d2ca3 100644
--- a/.github/workflows/python-publish.yml
+++ b/.github/workflows/python-publish.yml
@@ -20,7 +20,7 @@ jobs:
       - name: Setup Python Env
         uses: actions/setup-python@v3
         with:
-          python-version: "3.9"
+          python-version: "3.11"
 
       - name: Install dependencies
         run: pip install poetry dunamai
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 081963c..f04bb60 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,24 +1,12 @@
+exclude: "_version.py|versioneer.py"
 repos:
   - hooks:
-      - args:
-          - --remove-all-unused-imports
-          - --in-place
-        id: autoflake
-    repo: https://github.com/humitos/mirrors-autoflake
-    rev: v1.1
-  - hooks:
-      - id: isort
-    repo: https://github.com/timothycrosley/isort
-    rev: 5.10.1
-  - hooks:
-      - id: black
-    repo: https://github.com/psf/black
-    rev: 22.3.0
-  - hooks:
-      - id: flake8
-        exclude: (^docs/|^examples/|^notebooks/|^tests/)
-    repo: https://gitlab.com/pycqa/flake8
-    rev: 3.9.2
+      - id: ruff
+        args: ["--fix"]
+      - id: ruff-format
+        args: [--check]
+    repo: https://github.com/astral-sh/ruff-pre-commit
+    rev: v0.1.11
   - hooks:
       - id: pyright
         name: pyright
@@ -26,5 +14,5 @@ repos:
         language: node
         pass_filenames: false
         types: [python]
-        additional_dependencies: ["pyright@1.1.255"]
+        additional_dependencies: ["pyright@1.1.344"]
     repo: local
diff --git a/.python-version b/.python-version
index 30291cb..171a6a9 100644
--- a/.python-version
+++ b/.python-version
@@ -1,1 +1,1 @@
-3.10.0
+3.12.1
diff --git a/aioreactive/__init__.py b/aioreactive/__init__.py
index cdca1e8..75cf439 100644
--- a/aioreactive/__init__.py
+++ b/aioreactive/__init__.py
@@ -14,21 +14,12 @@
 """
 from __future__ import annotations
 
-from typing import (
-    Any,
-    AsyncIterable,
-    Awaitable,
-    Callable,
-    Iterable,
-    Optional,
-    Tuple,
-    TypeVar,
-    Union,
-    overload,
-)
+from collections.abc import AsyncIterable, Awaitable, Callable, Iterable
+from typing import Any, TypeVar, TypeVarTuple
 
-from expression import Option, curry_flipped, pipe
+from expression import Option, curry_flip, pipe
 from expression.system.disposable import AsyncDisposable
+from typing_extensions import Unpack
 
 from .observables import AsyncAnonymousObservable, AsyncIterableObservable
 from .observers import (
@@ -41,10 +32,12 @@
 from .subscription import run
 from .types import AsyncObservable, AsyncObserver, CloseAsync, SendAsync, ThrowAsync
 
+
 _A = TypeVar("_A")
 _B = TypeVar("_B")
 _C = TypeVar("_C")
 _D = TypeVar("_D")
+_V = TypeVarTuple("_V")
 _TSource = TypeVar("_TSource")
 _TResult = TypeVar("_TResult")
 _TOther = TypeVar("_TOther")
@@ -67,9 +60,9 @@ def __init__(self, source: AsyncObservable[_TSource]) -> None:
 
     async def subscribe_async(
         self,
-        send: Optional[Union[SendAsync[_TSource], AsyncObserver[_TSource]]] = None,
-        throw: Optional[ThrowAsync] = None,
-        close: Optional[CloseAsync] = None,
+        send: SendAsync[_TSource] | AsyncObserver[_TSource] | None = None,
+        throw: ThrowAsync | None = None,
+        close: CloseAsync | None = None,
     ) -> AsyncDisposable:
         """Subscribe to the async observable.
 
@@ -77,21 +70,21 @@ async def subscribe_async(
         observable.
 
         Args:
-            observer: The async observer to subscribe.
+            send: The async observer or the send function to subscribe.
+            throw: The throw function to subscribe.
+            close: The close function to subscribe.
 
         Returns:
             An async disposable that can be used to dispose the
             subscription.
         """
-        observer = (
-            send
-            if isinstance(send, AsyncObserver)
-            else AsyncAnonymousObserver(send, throw, close)
-        )
+        observer = send if isinstance(send, AsyncObserver) else AsyncAnonymousObserver(send, throw, close)
         return await self._source.subscribe_async(observer)
 
-    def __getitem__(self, key: Union[slice, int]) -> AsyncRx[_TSource]:
-        """Slices the given source stream using Python slice notation.
+    def __getitem__(self, key: slice | int) -> AsyncRx[_TSource]:
+        """Slice observable.
+
+        Slices the given source stream using Python slice notation.
         The arguments to slice is start, stop and step given within
         brackets [] and separated with the ':' character. It is
         basically a wrapper around the operators skip(), skip_last(),
@@ -118,7 +111,6 @@ def __getitem__(self, key: Union[slice, int]) -> AsyncRx[_TSource]:
         Returns:
             The sliced source stream.
         """
-
         from .filtering import slice as _slice
 
         if isinstance(key, slice):
@@ -126,7 +118,7 @@ def __getitem__(self, key: Union[slice, int]) -> AsyncRx[_TSource]:
         else:
             start, stop, step = key, key + 1, 1
 
-        return AsyncRx(pipe(self, _slice(start, stop, step)))
+        return AsyncRx(pipe(self, _slice(start, stop, step or 1)))
 
     @classmethod
     def create(cls, source: AsyncObservable[_TSource]) -> AsyncRx[_TSource]:
@@ -145,9 +137,7 @@ def from_iterable(cls, iter: Iterable[_TSource]) -> AsyncRx[_TSource]:
         return AsyncRx(from_iterable(iter))
 
     @classmethod
-    def from_async_iterable(
-        cls, iter: AsyncIterable[_TSource]
-    ) -> AsyncObservable[_TSource]:
+    def from_async_iterable(cls, iter: AsyncIterable[_TSource]) -> AsyncObservable[_TSource]:
         """Convert an async iterable to an async observable stream.
 
         Example:
@@ -157,7 +147,6 @@ def from_async_iterable(
             The source stream whose elements are pulled from the given
             (async) iterable sequence.
         """
-
         return AsyncRx(from_async_iterable(iter))
 
     @classmethod
@@ -169,9 +158,7 @@ def single(cls, value: _TSource) -> AsyncRx[_TSource]:
     def as_async_observable(self) -> AsyncObservable[_TSource]:
         return AsyncAnonymousObservable(self.subscribe_async)
 
-    def choose(
-        self, chooser: Callable[[_TSource], Option[_TSource]]
-    ) -> AsyncObservable[_TSource]:
+    def choose(self, chooser: Callable[[_TSource], Option[_TSource]]) -> AsyncObservable[_TSource]:
         """Choose.
 
         Applies the given function to each element of the stream and returns
@@ -187,9 +174,7 @@ def choose(
         """
         return AsyncRx(pipe(self, choose(chooser)))
 
-    def choose_async(
-        self, chooser: Callable[[_TSource], Awaitable[Option[_TSource]]]
-    ) -> AsyncObservable[_TSource]:
+    def choose_async(self, chooser: Callable[[_TSource], Awaitable[Option[_TSource]]]) -> AsyncObservable[_TSource]:
         """Choose async.
 
         Applies the given async function to each element of the stream and
@@ -205,9 +190,7 @@ def choose_async(
         """
         return AsyncRx(pipe(self, choose_async(chooser)))
 
-    def combine_latest(
-        self, other: AsyncObservable[_TOther]
-    ) -> AsyncRx[Tuple[_TSource, _TOther]]:
+    def combine_latest(self, other: AsyncObservable[_TOther]) -> AsyncRx[tuple[_TSource, _TOther]]:
         from .combine import combine_latest
 
         xs = pipe(
@@ -233,7 +216,6 @@ def debounce(self, seconds: float) -> AsyncRx[_TSource]:
         Returns:
             The debounced stream.
         """
-
         from .timeshift import debounce
 
         return AsyncRx(pipe(self, debounce(seconds)))
@@ -263,14 +245,11 @@ def filter(self, predicate: Callable[[_TSource], bool]) -> AsyncRx[_TSource]:
         Returns:
             The filtered stream.
         """
-
         from .filtering import filter as _filter
 
         return AsyncRx(pipe(self, _filter(predicate)))
 
-    def filteri(
-        self, predicate: Callable[[_TSource, int], bool]
-    ) -> AsyncObservable[_TSource]:
+    def filteri(self, predicate: Callable[[_TSource, int], bool]) -> AsyncObservable[_TSource]:
         """Filter with index.
 
         Filters the elements of an observable sequence based on a predicate
@@ -285,23 +264,17 @@ def filteri(
         """
         return AsyncRx(pipe(self, filteri(predicate)))
 
-    def filter_async(
-        self, predicate: Callable[[_TSource], Awaitable[bool]]
-    ) -> AsyncRx[_TSource]:
+    def filter_async(self, predicate: Callable[[_TSource], Awaitable[bool]]) -> AsyncRx[_TSource]:
         from .filtering import filter_async
 
         return AsyncRx(pipe(self, filter_async(predicate)))
 
-    def flat_map(
-        self, selector: Callable[[_TSource], AsyncObservable[_TResult]]
-    ) -> AsyncRx[_TResult]:
+    def flat_map(self, selector: Callable[[_TSource], AsyncObservable[_TResult]]) -> AsyncRx[_TResult]:
         from .transform import flat_map
 
         return AsyncRx.create(pipe(self, flat_map(selector)))
 
-    def flat_map_async(
-        self, selector: Callable[[_TSource], Awaitable[AsyncObservable[_TResult]]]
-    ) -> AsyncRx[_TResult]:
+    def flat_map_async(self, selector: Callable[[_TSource], Awaitable[AsyncObservable[_TResult]]]) -> AsyncRx[_TResult]:
         from .transform import flat_map_async
 
         return AsyncRx.create(pipe(self, flat_map_async(selector)))
@@ -361,7 +334,9 @@ def skip(self, count: int) -> AsyncObservable[_TSource]:
         )
 
     def skip_last(self, count: int) -> AsyncRx[_TSource]:
-        """Bypasses a specified number of elements at the end of an
+        """Skip the last items of the observable sequencerm .
+
+        Bypasses a specified number of elements at the end of an
         observable sequence.
 
         This operator accumulates a queue with a length enough to store
@@ -380,11 +355,12 @@ def skip_last(self, count: int) -> AsyncRx[_TSource]:
         return AsyncRx(pipe(self, skip_last(count)))
 
     def starfilter(
-        self: AsyncObservable[Any], predicate: Callable[..., bool]
-    ) -> AsyncRx[Any]:
+        self: AsyncObservable[tuple[Unpack[_V]]], predicate: Callable[[Unpack[_V]], bool]
+    ) -> AsyncRx[_TSource]:
         """Filter and spread the arguments to the predicate.
 
         Filters the elements of an observable sequence based on a predicate.
+
         Returns:
             An observable sequence that contains elements from the input
             sequence that satisfy the condition.
@@ -392,16 +368,13 @@ def starfilter(
         xs = pipe(self, starfilter(predicate))
         return AsyncRx.create(xs)
 
-    def starmap(
-        self: AsyncRx[Tuple[Any, ...]], mapper: Callable[..., _TResult]
-    ) -> AsyncRx[_TResult]:
+    def starmap(self: AsyncRx[tuple[Unpack[_V]]], mapper: Callable[[Unpack[_V]], _TResult]) -> AsyncRx[_TResult]:
         """Map and spread the arguments to the mapper.
 
         Returns:
             An observable sequence whose elements are the result of
             invoking the mapper function on each element of the source.
         """
-
         return AsyncRx(pipe(self, starmap(mapper)))
 
     def take(self, count: int) -> AsyncObservable[_TSource]:
@@ -411,7 +384,7 @@ def take(self, count: int) -> AsyncObservable[_TSource]:
         an observable sequence.
 
         Args:
-            count Number of elements to take.
+            count: Number of elements to take.
 
         Returns:
             An observable sequence that contains the specified number of
@@ -458,9 +431,7 @@ def to_async_iterable(self) -> AsyncIterable[_TSource]:
 
         return to_async_iterable(self)
 
-    def with_latest_from(
-        self, other: AsyncObservable[_TOther]
-    ) -> AsyncRx[Tuple[_TSource, _TOther]]:
+    def with_latest_from(self, other: AsyncObservable[_TOther]) -> AsyncRx[tuple[_TSource, _TOther]]:
         from .combine import with_latest_from
 
         return AsyncRx.create(pipe(self, with_latest_from(other)))
@@ -475,7 +446,7 @@ def as_chained(source: AsyncObservable[_TSource]) -> AsyncRx[_TSource]:
 
 
 def choose(
-    chooser: Callable[[_TSource], Option[_TResult]]
+    chooser: Callable[[_TSource], Option[_TResult]],
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TResult]]:
     """Choose.
 
@@ -490,14 +461,13 @@ def choose(
     Returns:
         The filtered and/or transformed stream.
     """
-
     from .filtering import choose
 
     return choose(chooser)
 
 
 def choose_async(
-    chooser: Callable[[_TSource], Awaitable[Option[_TResult]]]
+    chooser: Callable[[_TSource], Awaitable[Option[_TResult]]],
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TResult]]:
     """Choose async.
 
@@ -512,16 +482,15 @@ def choose_async(
     Returns:
         The filtered and/or transformed stream.
     """
-
     from .filtering import choose_async
 
     return choose_async(chooser)
 
 
-@curry_flipped(1)
+@curry_flip(1)
 def combine_latest(
     source: AsyncObservable[_TSource], other: AsyncObservable[_TOther]
-) -> AsyncObservable[Tuple[_TSource, _TOther]]:
+) -> AsyncObservable[tuple[_TSource, _TOther]]:
     from .combine import combine_latest
 
     return pipe(source, combine_latest(other))
@@ -545,14 +514,13 @@ def debounce(
         A partially applied debounce function that takes the source
         observable to debounce.
     """
-
     from .timeshift import debounce
 
     return debounce(seconds)
 
 
 def catch(
-    handler: Callable[[Exception], AsyncObservable[_TSource]]
+    handler: Callable[[Exception], AsyncObservable[_TSource]],
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TSource]]:
     from .transform import catch
 
@@ -562,8 +530,11 @@ def catch(
 def concat(
     other: AsyncObservable[_TSource],
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TSource]]:
-    """Concatenates an observable sequence with another observable
-    sequence."""
+    """Concatenate observables.
+
+    Concatenates an observable sequence with another observable
+    sequence.
+    """
 
     def _concat(source: AsyncObservable[_TSource]) -> AsyncObservable[_TSource]:
         from .combine import concat_seq
@@ -577,17 +548,17 @@ def concat_seq(
     sources: Iterable[AsyncObservable[_TSource]],
 ) -> AsyncObservable[_TSource]:
     """Concatenates an iterable of observable sequences."""
-
     from .combine import concat_seq
 
     return concat_seq(sources)
 
 
-def defer(
-    factory: Callable[[], AsyncObservable[_TSource]]
-) -> AsyncObservable[_TSource]:
-    """Returns an observable sequence that invokes the specified factory
-    function whenever a new observer subscribes."""
+def defer(factory: Callable[[], AsyncObservable[_TSource]]) -> AsyncObservable[_TSource]:
+    """Defer observable.
+
+    Returns an observable sequence that invokes the specified factory
+    function whenever a new observer subscribes.
+    """
     from .create import defer
 
     return defer(factory)
@@ -609,15 +580,13 @@ def distinct_until_changed(
     return distinct_until_changed(source)
 
 
-def empty() -> "AsyncObservable[Any]":
+def empty() -> AsyncObservable[Any]:
     from .create import empty
 
     return empty()
 
 
-def filter(
-    predicate: Callable[[_TSource], bool]
-) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TSource]]:
+def filter(predicate: Callable[[_TSource], bool]) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TSource]]:
     """Filter stream.
 
     Filters the elements of an observable sequence based on a predicate.
@@ -638,7 +607,7 @@ def filter(
 
 
 def filteri(
-    predicate: Callable[[_TSource, int], bool]
+    predicate: Callable[[_TSource, int], bool],
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TSource]]:
     """Filter with index.
 
@@ -658,7 +627,7 @@ def filteri(
 
 
 def filter_async(
-    predicate: Callable[[_TSource], Awaitable[bool]]
+    predicate: Callable[[_TSource], Awaitable[bool]],
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TSource]]:
     from .filtering import filter_async
 
@@ -687,7 +656,7 @@ def from_iterable(iterable: Iterable[_TSource]) -> AsyncObservable[_TSource]:
 
 
 def flat_map(
-    mapper: Callable[[_TSource], AsyncObservable[_TResult]]
+    mapper: Callable[[_TSource], AsyncObservable[_TResult]],
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TResult]]:
     from .transform import flat_map
 
@@ -695,7 +664,7 @@ def flat_map(
 
 
 def flat_mapi(
-    mapper: Callable[[_TSource, int], AsyncObservable[_TResult]]
+    mapper: Callable[[_TSource, int], AsyncObservable[_TResult]],
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TResult]]:
     from .transform import flat_mapi
 
@@ -703,7 +672,7 @@ def flat_mapi(
 
 
 def flat_map_async(
-    mapper: Callable[[_TSource], Awaitable[AsyncObservable[_TResult]]]
+    mapper: Callable[[_TSource], Awaitable[AsyncObservable[_TResult]]],
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TResult]]:
     """Flap map async.
 
@@ -711,10 +680,8 @@ def flat_map_async(
     an observable sequence and merges the resulting observable sequences
     back into one observable sequence.
 
-
     Args:
-        mapperCallable ([type]): [description]
-        Awaitable ([type]): [description]
+        mapper: A transform function to apply to each element or an
 
     Returns:
         Stream[TSource, TResult]: [description]
@@ -725,7 +692,7 @@ def flat_map_async(
 
 
 def flat_map_latest_async(
-    mapper: Callable[[_TSource], Awaitable[AsyncObservable[_TResult]]]
+    mapper: Callable[[_TSource], Awaitable[AsyncObservable[_TResult]]],
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TResult]]:
     """Flat map latest async.
 
@@ -761,52 +728,60 @@ def from_async_iterable(iter: AsyncIterable[_TSource]) -> AsyncObservable[_TSour
 
 
 def interval(seconds: float, period: int) -> AsyncObservable[int]:
-    """Returns an observable sequence that triggers the increasing
+    """Observable interval.
+
+    Returns an observable sequence that triggers the increasing
     sequence starting with 0 after the given msecs, and the after each
-    period."""
+    period.
+    """
     from .create import interval
 
     return interval(seconds, period)
 
 
-def map(
-    fn: Callable[[_TSource], _TResult]
-) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TResult]]:
+def map(fn: Callable[[_TSource], _TResult]) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TResult]]:
     from .transform import map as _map
 
     return _map(fn)
 
 
 def map_async(
-    mapper: Callable[[_TSource], Awaitable[_TResult]]
+    mapper: Callable[[_TSource], Awaitable[_TResult]],
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TResult]]:
     """Map asynchrnously.
 
     Returns an observable sequence whose elements are the result of
     invoking the async mapper function on each element of the
-    source."""
+    source.
+    """
     from .transform import map_async as map_async_
 
     return map_async_(mapper)
 
 
 def mapi_async(
-    mapper: Callable[[_TSource, int], Awaitable[_TResult]]
+    mapper: Callable[[_TSource, int], Awaitable[_TResult]],
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TResult]]:
-    """Returns an observable sequence whose elements are the result of
+    """Map indexed asynchronously.
+
+    Returns an observable sequence whose elements are the result of
     invoking the async mapper function by incorporating the element's
-    index on each element of the source."""
+    index on each element of the source.
+    """
     from .transform import mapi_async
 
     return mapi_async(mapper)
 
 
 def mapi(
-    mapper: Callable[[_TSource, int], _TResult]
+    mapper: Callable[[_TSource, int], _TResult],
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TResult]]:
-    """Returns an observable sequence whose elements are the result of
+    """Map indexed.
+
+    Returns an observable sequence whose elements are the result of
     invoking the mapper function and incorporating the element's index
-    on each element of the source."""
+    on each element of the source.
+    """
     from .transform import mapi
 
     return mapi(mapper)
@@ -851,7 +826,7 @@ def merge_seq(
     )
 
 
-def never() -> "AsyncObservable[Any]":
+def never() -> AsyncObservable[Any]:
     from .create import never
 
     return never()
@@ -875,7 +850,7 @@ def scan(
     accumulator: Callable[[_TResult, _TSource], _TResult],
     initial: _TResult,
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TResult]]:
-    """The scan operator
+    """The scan operator.
 
     This operator runs the accumulator for every value from the source with the current state. After every run, the new
     computed value is returned.
@@ -951,7 +926,9 @@ def skip(
 def skip_last(
     count: int,
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TSource]]:
-    """Bypasses a specified number of elements at the end of an
+    """Skip the last items of the observable sequence.
+
+    Bypasses a specified number of elements at the end of an
     observable sequence.
 
     This operator accumulates a queue with a length enough to store
@@ -972,35 +949,13 @@ def skip_last(
     return skip_last(count)
 
 
-@overload
-def starfilter(
-    predicate: Callable[[Tuple[_A, _B]], bool]
-) -> Callable[[AsyncObservable[Tuple[_A, _B]]], AsyncObservable[Tuple[_A, _B]]]:
-    ...
-
-
-@overload
 def starfilter(
-    predicate: Callable[[Tuple[_A, _B, _C]], bool]
-) -> Callable[[AsyncObservable[Tuple[_A, _B, _C]]], AsyncObservable[Tuple[_A, _B, _C]]]:
-    ...
-
-
-@overload
-def starfilter(
-    predicate: Callable[[Tuple[_A, _B, _C, _D]], bool]
-) -> Callable[
-    [AsyncObservable[Tuple[_A, _B, _C, _D]]], AsyncObservable[Tuple[_A, _B, _C, _D]]
-]:
-    ...
-
-
-def starfilter(
-    predicate: Callable[..., bool]
-) -> Callable[[AsyncObservable[Any]], AsyncObservable[Any]]:
+    predicate: Callable[[Unpack[_V]], bool],
+) -> Callable[[AsyncObservable[tuple[Unpack[_V]]]], AsyncObservable[Any]]:
     """Filter and spread the arguments to the predicate.
 
     Filters the elements of an observable sequence based on a predicate.
+
     Returns:
         An observable sequence that contains elements from the input
         sequence that satisfy the condition.
@@ -1010,43 +965,20 @@ def starfilter(
     return starfilter(predicate)
 
 
-@overload
-def starmap(
-    mapper: Callable[[_A, _B], _TResult]
-) -> Callable[[AsyncObservable[Tuple[_A, _B]]], AsyncObservable[_TResult]]:
-    ...
-
-
-@overload
-def starmap(
-    mapper: Callable[[_A, _B, _C], _TResult]
-) -> Callable[[AsyncObservable[Tuple[_A, _B, _C]]], AsyncObservable[_TResult]]:
-    ...
-
-
-@overload
-def starmap(
-    mapper: Callable[[_A, _B, _C, _D], _TResult]
-) -> Callable[[AsyncObservable[Tuple[_A, _B, _C, _D]]], AsyncObservable[_TResult]]:
-    ...
-
-
 def starmap(
-    mapper: Callable[..., _TResult]
-) -> Callable[[AsyncObservable[Any]], AsyncObservable[_TResult]]:
+    mapper: Callable[[Unpack[_V]], _TResult],
+) -> Callable[[AsyncObservable[tuple[Unpack[_V]]]], AsyncObservable[_TResult]]:
     """Map and spread the arguments to the mapper.
 
     Returns an observable sequence whose elements are the result of
-    invoking the mapper function on each element of the source."""
-
+    invoking the mapper function on each element of the source.
+    """
     from .transform import starmap
 
     return starmap(mapper)
 
 
-def switch_latest() -> Callable[
-    [AsyncObservable[AsyncObservable[_TSource]]], AsyncObservable[_TSource]
-]:
+def switch_latest() -> Callable[[AsyncObservable[AsyncObservable[_TSource]]], AsyncObservable[_TSource]]:
     from .transform import switch_latest as switch_latest_
 
     return switch_latest_
@@ -1061,7 +993,7 @@ def take(
     an observable sequence.
 
     Args:
-        count Number of elements to take.
+        count: Number of elements to take.
 
     Returns:
         An observable sequence that contains the specified number of
@@ -1112,7 +1044,9 @@ def take_until(
 
 
 def timer(due_time: float) -> AsyncObservable[int]:
-    """Returns an observable sequence that triggers the value 0
+    """Observable timer.
+
+    Returns an observable sequence that triggers the value 0
     after the given duetime in milliseconds.
     """
     from .create import timer
@@ -1126,11 +1060,11 @@ def to_async_iterable(source: AsyncObservable[_TSource]) -> AsyncIterable[_TSour
     return to_async_iterable(source)
 
 
-@curry_flipped(1)
+@curry_flip(1)
 def with_latest_from(
     source: AsyncObservable[_TSource],
     other: AsyncObservable[_TOther],
-) -> AsyncObservable[Tuple[_TSource, _TOther]]:
+) -> AsyncObservable[tuple[_TSource, _TOther]]:
     from .combine import with_latest_from
 
     return pipe(
diff --git a/aioreactive/combine.py b/aioreactive/combine.py
index 2b8ff6d..5c2bf9b 100644
--- a/aioreactive/combine.py
+++ b/aioreactive/combine.py
@@ -1,9 +1,10 @@
 import dataclasses
 import logging
+from collections.abc import Callable, Iterable
 from dataclasses import dataclass
-from typing import Any, Callable, Generic, Iterable, NoReturn, Tuple, TypeVar, cast
+from typing import Any, Generic, NoReturn, TypeVar
 
-from expression import curry_flipped
+from expression import curry_flip
 from expression.collections import Block, Map, block, map
 from expression.core import (
     MailboxProcessor,
@@ -12,23 +13,13 @@
     Some,
     TailCall,
     TailCallResult,
-    match,
     pipe,
     tailrec_async,
 )
 from expression.system import AsyncDisposable
 
 from .create import of_seq
-from .msg import (
-    CompletedMsg,
-    DisposeMsg,
-    InnerCompletedMsg,
-    InnerObservableMsg,
-    Key,
-    Msg,
-    OtherMsg,
-    SourceMsg,
-)
+from .msg import Key, Msg
 from .notification import Notification, OnError, OnNext
 from .observables import AsyncAnonymousObservable
 from .observers import (
@@ -38,6 +29,7 @@
 )
 from .types import AsyncObservable, AsyncObserver
 
+
 _TSource = TypeVar("_TSource")
 _TOther = TypeVar("_TOther")
 
@@ -82,30 +74,23 @@ async def athrow(error: Exception) -> None:
                         await safe_obv.athrow(error)
 
                     async def aclose() -> None:
-                        inbox.post(InnerCompletedMsg(key))
+                        inbox.post(Msg(inner_completed=key))
 
                     return AsyncAnonymousObserver(asend, athrow, aclose)
 
-                async def update(
-                    msg: Msg[_TSource], model: Model[_TSource]
-                ) -> Model[_TSource]:
+                async def update(msg: Msg[_TSource], model: Model[_TSource]) -> Model[_TSource]:
                     # log.debug("update: %s, model: %s", msg, model)
-                    with match(msg) as case:
-                        for xs in case(InnerObservableMsg[_TSource]):
-                            if (
-                                max_concurrent == 0
-                                or len(model.subscriptions) < max_concurrent
-                            ):
+                    match msg:
+                        case Msg(tag="inner_observable", inner_observable=xs):
+                            if max_concurrent == 0 or len(model.subscriptions) < max_concurrent:
                                 inner = await xs.subscribe_async(obv(model.key))
                                 return model.replace(
-                                    subscriptions=model.subscriptions.add(
-                                        model.key, inner
-                                    ),
+                                    subscriptions=model.subscriptions.add(model.key, inner),
                                     key=Key(model.key + 1),
                                 )
                             lst = Block.singleton(xs)
                             return model.replace(queue=model.queue.append(lst))
-                        for key in case(InnerCompletedMsg[Key]):
+                        case Msg(tag="inner_completed", inner_completed=key):
                             subscriptions = model.subscriptions.remove(key)
                             if len(model.queue):
                                 xs = model.queue[0]
@@ -122,18 +107,18 @@ async def update(
                                 if model.is_stopped:
                                     await safe_obv.aclose()
                                 return model.replace(subscriptions=map.empty)
-                        while case(CompletedMsg):
+                        case Msg(tag="completed"):
                             if not model.subscriptions:
                                 log.debug("merge_inner: closing!")
                                 await safe_obv.aclose()
 
                             return model.replace(is_stopped=True)
 
-                        while case.default():
+                        case _:
                             for dispose in model.subscriptions.values():
                                 await dispose.dispose_async()
 
-                        return initial_model.replace(is_stopped=True)
+                    return initial_model.replace(is_stopped=True)
 
                 async def message_loop(model: Model[_TSource]) -> None:
                     while True:
@@ -149,21 +134,21 @@ async def message_loop(model: Model[_TSource]) -> None:
 
             async def asend(xs: AsyncObservable[_TSource]) -> None:
                 log.debug("merge_inner:asend(%s)", xs)
-                agent.post(InnerObservableMsg(inner_observable=xs))
+                agent.post(Msg(inner_observable=xs))
 
             async def athrow(error: Exception) -> None:
                 await safe_obv.athrow(error)
-                agent.post(DisposeMsg)
+                agent.post(Msg(dispose=True))
 
             async def aclose() -> None:
-                agent.post(CompletedMsg)
+                agent.post(Msg(completed=True))
 
             obv = AsyncAnonymousObserver(asend, athrow, aclose)
             dispose = await auto_detach(source.subscribe_async(obv))
 
             async def cancel() -> None:
                 await dispose.dispose_async()
-                agent.post(DisposeMsg)
+                agent.post(Msg(dispose=True))
 
             return AsyncDisposable.create(cancel)
 
@@ -175,19 +160,21 @@ async def cancel() -> None:
 def concat_seq(
     sources: Iterable[AsyncObservable[_TSource]],
 ) -> AsyncObservable[_TSource]:
-    """Returns an observable sequence that contains the elements of each
-    given sequences, in sequential order."""
+    """Concatenate sequences.
 
+    Returns an observable sequence that contains the elements of each
+    given sequences, in sequential order.
+    """
     return pipe(
         of_seq(sources),
         merge_inner(1),
     )
 
 
-@curry_flipped(1)
+@curry_flip(1)
 def combine_latest(
     source: AsyncObservable[_TSource], other: AsyncObservable[_TOther]
-) -> AsyncObservable[Tuple[_TSource, _TOther]]:
+) -> AsyncObservable[tuple[_TSource, _TOther]]:
     """Combine latest values.
 
     Merges the specified observable sequences into one observable
@@ -195,6 +182,7 @@ def combine_latest(
     an observable sequence containing the combined results.
 
     Args:
+        source: The first observable to combine.
         other: The other observable to combine with.
 
     Returns:
@@ -202,9 +190,7 @@ def combine_latest(
         returns the combined observable.
     """
 
-    async def subscribe_async(
-        aobv: AsyncObserver[Tuple[_TSource, _TOther]]
-    ) -> AsyncDisposable:
+    async def subscribe_async(aobv: AsyncObserver[tuple[_TSource, _TOther]]) -> AsyncDisposable:
         safe_obv, auto_detach = auto_detach_observer(aobv)
 
         async def worker(inbox: MailboxProcessor[Msg[_TSource]]) -> None:
@@ -215,28 +201,26 @@ async def message_loop(
                 cn = await inbox.receive()
 
                 async def get_value(n: Notification[Any]) -> Option[Any]:
-                    with match(n) as m:
-                        for value in case(OnNext[_TSource]):
+                    match n:
+                        case OnNext(value=value):
                             return Some(value)
-
-                        for err in case(OnError):
+                        case OnError(exception=err):
                             await safe_obv.athrow(err)
-
-                        while m.default():
+                        case _:
                             await safe_obv.aclose()
                     return Nothing
 
-                with match(cn) as case:
-                    for value in case(SourceMsg[_TSource]):
+                match cn:
+                    case Msg(tag="source", source=value):
                         source_value = await get_value(value)
-                        break
 
-                    for value in case(OtherMsg[_TOther]):
+                    case Msg(tag="other", other=value):
                         other_value = await get_value(value)
-                        break
+                    case _:
+                        raise ValueError(f"Unexpected message: {cn}")
 
-                def binder(s: _TSource) -> Option[Tuple[_TSource, _TOther]]:
-                    def mapper(o: _TOther) -> Tuple[_TSource, _TOther]:
+                def binder(s: _TSource) -> Option[tuple[_TSource, _TOther]]:
+                    def mapper(o: _TOther) -> tuple[_TSource, _TOther]:
                         return (s, o)
 
                     return other_value.map(mapper)
@@ -245,19 +229,17 @@ def mapper(o: _TOther) -> Tuple[_TSource, _TOther]:
                 for x in combined.to_list():
                     await safe_obv.asend(x)
 
-                return TailCall[Option[_TSource], Option[_TOther]](
-                    source_value, other_value
-                )
+                return TailCall[Option[_TSource], Option[_TOther]](source_value, other_value)
 
             await message_loop(Nothing, Nothing)
 
         agent = MailboxProcessor.start(worker)
 
         async def obv_fn1(n: Notification[_TSource]) -> None:
-            pipe(SourceMsg(n), agent.post)
+            pipe(Msg(source=n), agent.post)
 
         async def obv_fn2(n: Notification[_TOther]) -> None:
-            pipe(OtherMsg(n), agent.post)
+            pipe(Msg(other=n), agent.post)
 
         obv1: AsyncObserver[_TSource] = AsyncNotificationObserver(obv_fn1)
         obv2: AsyncObserver[_TOther] = AsyncNotificationObserver(obv_fn2)
@@ -269,10 +251,10 @@ async def obv_fn2(n: Notification[_TOther]) -> None:
     return AsyncAnonymousObservable(subscribe_async)
 
 
-@curry_flipped(1)
+@curry_flip(1)
 def with_latest_from(
     source: AsyncObservable[_TSource], other: AsyncObservable[_TOther]
-) -> AsyncObservable[Tuple[_TSource, _TOther]]:
+) -> AsyncObservable[tuple[_TSource, _TOther]]:
     """With latest from.
 
     Merges the specified observable sequences into one observable
@@ -281,6 +263,7 @@ def with_latest_from(
     observable sequence.
 
     Args:
+        source: The first observable to combine.
         other (AsyncObservable[TOther]): The other observable to merge
             with.
 
@@ -288,9 +271,7 @@ def with_latest_from(
         The merged observable.
     """
 
-    async def subscribe_async(
-        aobv: AsyncObserver[Tuple[_TSource, _TOther]]
-    ) -> AsyncDisposable:
+    async def subscribe_async(aobv: AsyncObserver[tuple[_TSource, _TOther]]) -> AsyncDisposable:
         safe_obv, auto_detach = auto_detach_observer(aobv)
 
         async def worker(inbox: MailboxProcessor[Msg[_TSource]]) -> None:
@@ -301,27 +282,28 @@ async def message_loop(
                 cn = await inbox.receive()
 
                 async def get_value(n: Notification[Any]) -> Option[Any]:
-                    with match(n) as case:
-                        for value in case(OnNext[_TSource]):
+                    match n:
+                        case OnNext(value=value):
                             return Some(value)
 
-                        for err in case(OnError[_TSource]):
+                        case OnError(exception=err):
                             await safe_obv.athrow(err)
 
-                        if case.default():
+                        case _:
                             await safe_obv.aclose()
                     return Nothing
 
                 source_value = Nothing
-                if isinstance(cn, SourceMsg):
-                    cn = cast(SourceMsg[_TSource], cn)
-                    source_value = await get_value(cn.value)
-                else:
-                    cn = cast(OtherMsg[_TOther], cn)
-                    latest = await get_value(cn.value)
-
-                def binder(s: _TSource) -> Option[Tuple[_TSource, _TOther]]:
-                    def mapper(o: _TOther) -> Tuple[_TSource, _TOther]:
+                match cn:
+                    case Msg(tag="source", source=value):
+                        source_value = await get_value(value)
+                    case Msg(tag="other", other=value):
+                        latest = await get_value(value)
+                    case _:
+                        raise ValueError(f"Unexpected message: {cn}")
+
+                def binder(s: _TSource) -> Option[tuple[_TSource, _TOther]]:
+                    def mapper(o: _TOther) -> tuple[_TSource, _TOther]:
                         return (s, o)
 
                     return latest.map(mapper)
@@ -337,10 +319,10 @@ def mapper(o: _TOther) -> Tuple[_TSource, _TOther]:
         agent = MailboxProcessor.start(worker)
 
         async def obv_fn1(n: Notification[_TSource]) -> None:
-            pipe(SourceMsg(n), agent.post)
+            pipe(Msg(source=n), agent.post)
 
         async def obv_fn2(n: Notification[_TOther]) -> None:
-            pipe(OtherMsg(n), agent.post)
+            pipe(Msg(other=n), agent.post)
 
         obv1: AsyncObserver[_TSource] = AsyncNotificationObserver(obv_fn1)
         obv2: AsyncObserver[_TOther] = AsyncNotificationObserver(obv_fn2)
@@ -351,13 +333,11 @@ async def obv_fn2(n: Notification[_TOther]) -> None:
     return AsyncAnonymousObservable(subscribe_async)
 
 
-@curry_flipped(1)
+@curry_flip(1)
 def zip_seq(
     source: AsyncObservable[_TSource], sequence: Iterable[_TOther]
-) -> AsyncObservable[Tuple[_TSource, _TOther]]:
-    async def subscribe_async(
-        aobv: AsyncObserver[Tuple[_TSource, _TOther]]
-    ) -> AsyncDisposable:
+) -> AsyncObservable[tuple[_TSource, _TOther]]:
+    async def subscribe_async(aobv: AsyncObserver[tuple[_TSource, _TOther]]) -> AsyncDisposable:
         safe_obv, auto_detach = auto_detach_observer(aobv)
 
         enumerator = iter(sequence)
diff --git a/aioreactive/create.py b/aioreactive/create.py
index 891739f..4658a6c 100644
--- a/aioreactive/create.py
+++ b/aioreactive/create.py
@@ -1,19 +1,11 @@
 import asyncio
 import logging
-from asyncio import Future
-from typing import (
-    Any,
-    AsyncIterable,
-    Awaitable,
-    Callable,
-    Iterable,
-    Optional,
-    Tuple,
-    TypeVar,
-)
+from asyncio import Task
+from collections.abc import AsyncIterable, Awaitable, Callable, Iterable
+from typing import Any, TypeVar
 
-from expression.core import TailCallResult, aiotools, tailrec_async
-from expression.core.fn import TailCall
+from expression import TailCall, TailCallResult, tailrec_async
+from expression.core import aiotools
 from expression.system import (
     AsyncDisposable,
     CancellationToken,
@@ -24,12 +16,13 @@
 from .observers import AsyncObserver, safe_observer
 from .types import AsyncObservable
 
+
 TSource = TypeVar("TSource")
 
 log = logging.getLogger(__name__)
 
 
-def canceller() -> Tuple[AsyncDisposable, CancellationToken]:
+def canceller() -> tuple[AsyncDisposable, CancellationToken]:
     cts = CancellationTokenSource()
 
     async def cancel() -> None:
@@ -39,9 +32,7 @@ async def cancel() -> None:
     return AsyncDisposable.create(cancel), cts.token
 
 
-def create(
-    subscribe: Callable[[AsyncObserver[TSource]], Awaitable[AsyncDisposable]]
-) -> AsyncObservable[TSource]:
+def create(subscribe: Callable[[AsyncObserver[TSource]], Awaitable[AsyncDisposable]]) -> AsyncObservable[TSource]:
     """Create an async observable.
 
     Creates an `AsyncObservable[TSource]` from the given subscribe
@@ -50,11 +41,8 @@ def create(
     return AsyncAnonymousObservable(subscribe)
 
 
-def of_async_worker(
-    worker: Callable[[AsyncObserver[Any], CancellationToken], Awaitable[None]]
-) -> AsyncObservable[Any]:
-    """Create async observable from async worker function"""
-
+def of_async_worker(worker: Callable[[AsyncObserver[Any], CancellationToken], Awaitable[None]]) -> AsyncObservable[Any]:
+    """Create async observable from async worker function."""
     log.debug("of_async_worker()")
 
     async def subscribe_async(aobv: AsyncObserver[Any]) -> AsyncDisposable:
@@ -69,8 +57,11 @@ async def subscribe_async(aobv: AsyncObserver[Any]) -> AsyncDisposable:
 
 
 def of_async(workflow: Awaitable[TSource]) -> AsyncObservable[TSource]:
-    """Returns the async observable sequence whose single element is the
-    result of the given async workflow."""
+    """Create from async workflow.
+
+    Returns the async observable sequence whose single element is the
+    result of the given async workflow.
+    """
 
     async def worker(obv: AsyncObserver[TSource], _: CancellationToken) -> None:
         try:
@@ -86,9 +77,9 @@ async def worker(obv: AsyncObserver[TSource], _: CancellationToken) -> None:
 
 
 def of_async_iterable(iterable: AsyncIterable[TSource]) -> AsyncObservable[TSource]:
-    async def subscribe_async(observer: AsyncObserver[TSource]) -> AsyncDisposable:
-        task: Optional[Future[None]] = None
+    tasks: set[Task[Any]] = set()
 
+    async def subscribe_async(observer: AsyncObserver[TSource]) -> AsyncDisposable:
         async def cancel() -> None:
             if task:
                 task.cancel()
@@ -104,20 +95,26 @@ async def worker() -> None:
                     return
 
             await observer.aclose()
+            tasks.remove(task)
 
         try:
             task = asyncio.create_task(worker())
         except Exception as ex:
             log.debug("FromIterable:worker(), Exception: %s" % ex)
             await observer.athrow(ex)
+        else:
+            tasks.add(task)
         return sub
 
     return AsyncAnonymousObservable(subscribe_async)
 
 
 def single(value: TSource) -> AsyncObservable[TSource]:
-    """Returns an observable sequence containing the single specified
-    element."""
+    """Create from a single item.
+
+    Returns an observable sequence containing the single specified
+    element.
+    """
 
     async def subscribe_async(aobv: AsyncObserver[TSource]) -> AsyncDisposable:
         safe_obv = safe_observer(aobv, AsyncDisposable.empty())
@@ -149,8 +146,11 @@ async def subscribe_async(_: AsyncObserver[Any]) -> AsyncDisposable:
 
 
 def fail(error: Exception) -> AsyncObservable[Any]:
-    """Returns the observable sequence that terminates exceptionally
-    with the specified exception."""
+    """Create failing observable.
+
+    Returns the observable sequence that terminates exceptionally
+    with the specified exception.
+    """
 
     async def worker(obv: AsyncObserver[Any], _: CancellationToken) -> None:
         await obv.athrow(error)
@@ -162,7 +162,8 @@ def of_seq(xs: Iterable[TSource]) -> AsyncObservable[TSource]:
     """Create async observable from sequence.
 
     Returns the async observable sequence whose elements are pulled from
-    the given enumerable sequence."""
+    the given enumerable sequence.
+    """
 
     async def worker(obv: AsyncObserver[TSource], token: CancellationToken) -> None:
         log.debug("of_seq:worker()")
@@ -181,8 +182,11 @@ async def worker(obv: AsyncObserver[TSource], token: CancellationToken) -> None:
 
 
 def defer(factory: Callable[[], AsyncObservable[TSource]]) -> AsyncObservable[TSource]:
-    """Returns an observable sequence that invokes the specified factory
-    function whenever a new observer subscribes."""
+    """Defer observable.
+
+    Returns an observable sequence that invokes the specified factory
+    function whenever a new observer subscribes.
+    """
 
     async def subscribe_async(aobv: AsyncObserver[TSource]) -> AsyncDisposable:
         try:
@@ -196,17 +200,18 @@ async def subscribe_async(aobv: AsyncObserver[TSource]) -> AsyncDisposable:
 
 
 def interval(seconds: float, period: float) -> AsyncObservable[int]:
-    """Returns an observable sequence that triggers the increasing
+    """Create observable interval.
+
+    Returns an observable sequence that triggers the increasing
     sequence starting with 0 after the given msecs, and the after each
-    period."""
+    period.
+    """
 
     async def subscribe_async(aobv: AsyncObserver[int]) -> AsyncDisposable:
         cancel, token = canceller()
 
         @tailrec_async
-        async def handler(
-            seconds: float, next: int
-        ) -> "TailCallResult[None, [float, int]]":
+        async def handler(seconds: float, next: int) -> "TailCallResult[None, [float, int]]":
             await asyncio.sleep(seconds)
             await aobv.asend(next)
 
@@ -223,7 +228,9 @@ async def handler(
 
 
 def timer(due_time: float) -> AsyncObservable[int]:
-    """Returns an observable sequence that triggers the value 0
-    after the given duetime in milliseconds."""
+    """Create observable timer.
 
+    Returns an observable sequence that triggers the value 0
+    after the given duetime in milliseconds.
+    """
     return interval(due_time, 0)
diff --git a/aioreactive/filtering.py b/aioreactive/filtering.py
index 608d6e8..846f3ee 100644
--- a/aioreactive/filtering.py
+++ b/aioreactive/filtering.py
@@ -1,4 +1,5 @@
-from typing import Any, Awaitable, Callable, Iterable, List, NoReturn, Optional, TypeVar
+from collections.abc import Awaitable, Callable, Iterable
+from typing import Any, NoReturn, TypeVar
 
 from expression.collections import seq
 from expression.core import (
@@ -8,7 +9,6 @@
     TailCallResult,
     aiotools,
     compose,
-    match,
     pipe,
     tailrec_async,
 )
@@ -25,16 +25,15 @@
 from .transform import map, transform
 from .types import AsyncObservable, AsyncObserver
 
+
 _TSource = TypeVar("_TSource")
 _TResult = TypeVar("_TResult")
 
 
 def choose_async(
-    chooser: Callable[[_TSource], Awaitable[Option[_TResult]]]
+    chooser: Callable[[_TSource], Awaitable[Option[_TResult]]],
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TResult]]:
-    async def handler(
-        next: Callable[[_TResult], Awaitable[None]], xs: _TSource
-    ) -> None:
+    async def handler(next: Callable[[_TResult], Awaitable[None]], xs: _TSource) -> None:
         result = await chooser(xs)
         for x in result.to_list():
             await next(x)
@@ -43,11 +42,9 @@ async def handler(
 
 
 def choose(
-    chooser: Callable[[_TSource], Option[_TResult]]
+    chooser: Callable[[_TSource], Option[_TResult]],
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TResult]]:
-    def handler(
-        next: Callable[[_TResult], Awaitable[None]], xs: _TSource
-    ) -> Awaitable[None]:
+    def handler(next: Callable[[_TResult], Awaitable[None]], xs: _TSource) -> Awaitable[None]:
         for x in chooser(xs).to_list():
             return next(x)
         return aiotools.empty()
@@ -56,7 +53,7 @@ def handler(
 
 
 def filter_async(
-    predicate: Callable[[_TSource], Awaitable[bool]]
+    predicate: Callable[[_TSource], Awaitable[bool]],
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TSource]]:
     """Filter async.
 
@@ -78,12 +75,8 @@ async def handler(next: Callable[[_TSource], Awaitable[None]], x: _TSource) -> N
     return transform(handler)
 
 
-def filter(
-    predicate: Callable[[_TSource], bool]
-) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TSource]]:
-    def handler(
-        next: Callable[[_TSource], Awaitable[None]], x: _TSource
-    ) -> Awaitable[None]:
+def filter(predicate: Callable[[_TSource], bool]) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TSource]]:
+    def handler(next: Callable[[_TSource], Awaitable[None]], x: _TSource) -> Awaitable[None]:
         if predicate(x):
             return next(x)
         return aiotools.empty()
@@ -91,20 +84,17 @@ def handler(
     return transform(handler)
 
 
-def starfilter(
-    predicate: Callable[..., bool]
-) -> Callable[[AsyncObservable[Any]], AsyncObservable[Any]]:
+def starfilter(predicate: Callable[..., bool]) -> Callable[[AsyncObservable[Any]], AsyncObservable[Any]]:
     """Filter and spread the arguments to the predicate.
 
     Filters the elements of an observable sequence based on a predicate.
+
     Returns:
         An observable sequence that contains elements from the input
         sequence that satisfy the condition.
     """
 
-    def handler(
-        next: Callable[[Iterable[Any]], Awaitable[None]], args: Iterable[Any]
-    ) -> Awaitable[None]:
+    def handler(next: Callable[[Iterable[Any]], Awaitable[None]], args: Iterable[Any]) -> Awaitable[None]:
         if predicate(*args):
             return next(args)
         return aiotools.empty()
@@ -113,7 +103,7 @@ def handler(
 
 
 def filteri(
-    predicate: Callable[[_TSource, int], bool]
+    predicate: Callable[[_TSource, int], bool],
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TSource]]:
     ret = compose(
         zip_seq(seq.infinite),
@@ -145,34 +135,29 @@ async def worker(inbox: MailboxProcessor[Notification[_TSource]]) -> None:
             @tailrec_async
             async def message_loop(
                 latest: Notification[_TSource],
-            ) -> "TailCallResult[NoReturn, [Notification[_TSource]]]":
+            ) -> TailCallResult[NoReturn, [Notification[_TSource]]]:
                 n = await inbox.receive()
 
                 async def get_latest() -> Notification[_TSource]:
-                    with match(n) as case:
-                        for x in case(OnNext[_TSource]):
-                            if n == latest:
-                                break
-                            try:
-                                await safe_obv.asend(x)
-                            except Exception as ex:
-                                await safe_obv.athrow(ex)
-                            break
-                        for err in case(OnError[_TSource]):
+                    match n:
+                        case OnNext(value=x):
+                            if n != latest:
+                                try:
+                                    await safe_obv.asend(x)
+                                except Exception as ex:
+                                    await safe_obv.athrow(ex)
+                        case OnError(exception=err):
                             await safe_obv.athrow(err)
-                            break
-                        while case(OnCompleted):
+
+                        case _:
                             await safe_obv.aclose()
-                            break
 
                     return n
 
                 latest = await get_latest()
                 return TailCall[Notification[_TSource]](latest)
 
-            await message_loop(
-                OnCompleted
-            )  # Use as sentinel value as it will not match any OnNext value
+            await message_loop(OnCompleted())
 
         agent = MailboxProcessor.start(worker)
 
@@ -188,7 +173,7 @@ async def notification(n: Notification[_TSource]) -> None:
 def skip(
     count: int,
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TSource]]:
-    """[summary]
+    """Skip items from observable sequence.
 
     Bypasses a specified number of elements in an observable sequence
     and then returns the remaining elements.
@@ -228,10 +213,10 @@ def _skip_last(source: AsyncObservable[_TSource]) -> AsyncObservable[_TSource]:
         async def subscribe_async(observer: AsyncObserver[_TSource]) -> AsyncDisposable:
             safe_obv, auto_detach = auto_detach_observer(observer)
 
-            q: List[_TSource] = []
+            q: list[_TSource] = []
 
             async def asend(value: _TSource) -> None:
-                front = None
+                front: _TSource | None = None
                 q.append(value)
                 if len(q) > count:
                     front = q.pop(0)
@@ -294,7 +279,7 @@ def take_last(
     def _take_last(source: AsyncObservable[_TSource]) -> AsyncObservable[_TSource]:
         async def subscribe_async(aobv: AsyncObserver[_TSource]) -> AsyncDisposable:
             safe_obv, auto_detach = auto_detach_observer(aobv)
-            queue: List[_TSource] = []
+            queue: list[_TSource] = []
 
             async def asend(value: _TSource) -> None:
                 queue.append(value)
@@ -348,7 +333,7 @@ async def asend(value: _TSource) -> None:
 
 
 def slice(
-    start: Optional[int] = None, stop: Optional[int] = None, step: int = 1
+    start: int | None = None, stop: int | None = None, step: int = 1
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TSource]]:
     """Slices the given source stream.
 
@@ -395,14 +380,16 @@ def _slice(source: AsyncObservable[_TSource]) -> AsyncObservable[_TSource]:
             else:
                 source = pipe(source, skip_last(abs(stop)))
 
-        if step is not None:
-            if step > 1:
-                mapper: Callable[[Any, int], bool] = lambda _, i: i % step == 0
-                xs = pipe(source, filteri(mapper))
-                source = xs
-            elif step < 0:
-                # Reversing streams is not supported
-                raise TypeError("Negative step not supported.")
+        if step > 1:
+
+            def mapper(_: Any, i: int) -> bool:
+                return i % step == 0
+
+            xs = pipe(source, filteri(mapper))
+            source = xs
+        elif step < 0:
+            # Reversing streams is not supported
+            raise TypeError("Negative step not supported.")
 
         return source
 
diff --git a/aioreactive/iterable/__init__.py b/aioreactive/iterable/__init__.py
index e69de29..0185c9b 100644
--- a/aioreactive/iterable/__init__.py
+++ b/aioreactive/iterable/__init__.py
@@ -0,0 +1,5 @@
+"""Iterable module.
+
+This module contains functions to create async observables from
+iterables.
+"""
diff --git a/aioreactive/iterable/to_async_observable.py b/aioreactive/iterable/to_async_observable.py
index 9216c72..6a2cd11 100644
--- a/aioreactive/iterable/to_async_observable.py
+++ b/aioreactive/iterable/to_async_observable.py
@@ -1,16 +1,18 @@
-from typing import AsyncIterable, TypeVar
+from collections.abc import AsyncIterable
+from typing import TypeVar
 
 from aioreactive import AsyncObservable, AsyncRx
 
+
 TSource = TypeVar("TSource")
 
 
 def to_async_observable(source: AsyncIterable[TSource]) -> AsyncObservable[TSource]:
     """Convert to async observable.
 
-    Keyword arguments:
-    source -- Async iterable to convert to async observable.
-
-    Returns async observable"""
+    Keyword Arguments:
+    source: Async iterable to convert to async observable.
 
+    Returns async observable
+    """
     return AsyncRx.from_async_iterable(source)
diff --git a/aioreactive/leave.py b/aioreactive/leave.py
index 1a34bcd..58ca389 100644
--- a/aioreactive/leave.py
+++ b/aioreactive/leave.py
@@ -1,5 +1,6 @@
 import asyncio
-from typing import AsyncIterable, Optional, TypeVar
+from collections.abc import AsyncIterable
+from typing import Any, TypeVar
 
 import reactivex
 from expression.system.disposable import AsyncDisposable
@@ -7,11 +8,8 @@
 from reactivex.abc import DisposableBase, ObserverBase, SchedulerBase
 from reactivex.disposable import Disposable
 
-from .observables import (
-    AsyncAnonymousObserver,
-    AsyncIterableObservable,
-    AsyncObservable,
-)
+from .observables import AsyncAnonymousObserver, AsyncIterableObservable, AsyncObservable
+
 
 _TSource = TypeVar("_TSource")
 
@@ -20,6 +18,7 @@ def to_async_iterable(source: AsyncObservable[_TSource]) -> AsyncIterable[_TSour
     """Convert async observable to async iterable.
 
     Args:
+        source: The source observable.
         count: The number of elements to skip before returning the
             remaining values.
 
@@ -27,15 +26,15 @@ def to_async_iterable(source: AsyncObservable[_TSource]) -> AsyncIterable[_TSour
         A source stream that contains the values that occur
         after the specified index in the input source stream.
     """
-
     return AsyncIterableObservable(source)
 
 
 def to_observable(source: AsyncObservable[_TSource]) -> Observable[_TSource]:
-    def subscribe(
-        obv: ObserverBase[_TSource], scheduler: Optional[SchedulerBase] = None
-    ) -> DisposableBase:
-        subscription: Optional[AsyncDisposable] = None
+    """Convert async observable to observable."""
+    tasks: set[asyncio.Task[Any]] = set()
+
+    def subscribe(obv: ObserverBase[_TSource], scheduler: SchedulerBase | None = None) -> DisposableBase:
+        subscription: AsyncDisposable | None = None
 
         async def start() -> None:
             nonlocal subscription
@@ -49,16 +48,23 @@ async def athrow(error: Exception) -> None:
             async def aclose() -> None:
                 obv.on_completed()
 
-            subscription = await source.subscribe_async(
-                AsyncAnonymousObserver(asend, athrow, aclose)
-            )
+            subscription = await source.subscribe_async(AsyncAnonymousObserver(asend, athrow, aclose))
+            tasks.remove(task)
 
-        asyncio.create_task(start())
+        task = asyncio.create_task(start())
+        tasks.add(task)
+        task.add_done_callback(lambda _: tasks.remove(task))
 
         def dispose() -> None:
             if subscription:
-                asyncio.create_task(subscription.dispose_async())
+                task = asyncio.create_task(subscription.dispose_async())
+                tasks.add(task)
 
         return Disposable(dispose)
 
     return reactivex.create(subscribe)
+
+
+# def to_iterable(source: AsyncObservable[_TSource]) -> Iterable[_TSource]:
+#     """Convert async observable to iterable."""
+#     return to_observable(source).to_iterable()
diff --git a/aioreactive/msg.py b/aioreactive/msg.py
index 0575af7..dec4858 100644
--- a/aioreactive/msg.py
+++ b/aioreactive/msg.py
@@ -1,148 +1,39 @@
-"""Internal messages used by mailbox processors. Do not import or use.
-"""
-from abc import ABC
-from dataclasses import dataclass
-from typing import Any, Iterable, NewType, TypeVar, get_origin
+"""Internal messages used by mailbox processors. Do not import or use."""
+from typing import Generic, Literal, NewType, TypeVar
 
-from expression.core import SupportsMatch
+from expression import case, tag, tagged_union
 from expression.system import AsyncDisposable
 
 from .notification import Notification
 from .types import AsyncObservable
 
-TSource = TypeVar("TSource")
-TOther = TypeVar("TOther")
 
-Key = NewType("Key", int)
-
-
-class Msg(SupportsMatch[TSource], ABC):
-    """Message base class."""
-
-
-@dataclass
-class SourceMsg(Msg[Notification[TSource]], SupportsMatch[TSource]):
-    value: Notification[TSource]
-
-    def __match__(self, pattern: Any) -> Iterable[Notification[TSource]]:
-        origin: Any = get_origin(pattern)
-        try:
-            if isinstance(self, origin or pattern):
-                return [self.value]
-        except TypeError:
-            pass
-        return []
-
-
-@dataclass
-class OtherMsg(Msg[Notification[TOther]], SupportsMatch[TOther]):
-    value: Notification[TOther]
-
-    def __match__(self, pattern: Any) -> Iterable[Notification[TOther]]:
-        origin: Any = get_origin(pattern)
-        try:
-            if isinstance(self, origin or pattern):
-                return [self.value]
-        except TypeError:
-            pass
-        return []
-
-
-@dataclass
-class DisposableMsg(Msg[AsyncDisposable], SupportsMatch[AsyncDisposable]):
-    """Message containing a diposable."""
-
-    disposable: AsyncDisposable
-
-    def __match__(self, pattern: Any) -> Iterable[AsyncDisposable]:
-        try:
-            if isinstance(self, pattern):
-                return [self.disposable]
-        except TypeError:
-            pass
-        return []
-
-
-@dataclass
-class InnerObservableMsg(
-    Msg[AsyncObservable[TSource]], SupportsMatch[AsyncObservable[TSource]]
-):
-    """Message containing an inner observable."""
-
-    inner_observable: AsyncObservable[TSource]
+_TSource = TypeVar("_TSource")
 
-    def __match__(self, pattern: Any) -> Iterable[AsyncObservable[TSource]]:
-        origin: Any = get_origin(pattern)
-        try:
-            if isinstance(self, origin or pattern):
-                return [self.inner_observable]
-        except TypeError:
-            pass
-        return []
-
-
-@dataclass
-class InnerCompletedMsg(Msg[TSource]):
-    """Message notifying that the inner observable completed."""
-
-    key: Key
-
-    def __match__(self, pattern: Any) -> Iterable[Any]:
-        origin: Any = get_origin(pattern)
-        try:
-            if isinstance(self, origin or pattern):
-                return [self.key]
-        except TypeError:
-            pass
-        return []
-
-
-class CompletedMsg_(Msg[Any]):
-    """Message notifying that the observable sequence completed."""
-
-    def __match__(self, pattern: Any) -> Iterable[bool]:
-        if self is pattern:
-            return [True]
-
-        origin: Any = get_origin(pattern)
-        try:
-            if isinstance(self, origin or pattern):
-                return [True]
-        except TypeError:
-            pass
-
-        return []
-
-
-CompletedMsg = CompletedMsg_()  # Singleton
-
-
-class DisposeMsg_(Msg[None]):
-    """Message notifying that the operator got disposed."""
-
-    def __match__(self, pattern: Any) -> Iterable[Any]:
-
-        if self is pattern:
-            return [True]
+Key = NewType("Key", int)
 
-        origin: Any = get_origin(pattern)
-        try:
-            if isinstance(self, origin or pattern):
-                return [True]
-        except TypeError:
-            pass
 
-        return []
+@tagged_union(frozen=True)
+class Msg(Generic[_TSource]):
+    """Message tagged union."""
 
+    tag: Literal[
+        "source",
+        "other",
+        "dispose",
+        "disposable",
+        "inner_observable",
+        "inner_completed",
+        "completed",
+    ] = tag()
 
-DisposeMsg = DisposeMsg_()  # Singleton
+    source: Notification[_TSource] = case()
+    other: Notification[_TSource] = case()
+    dispose: Literal[True] = case()
+    disposable: AsyncDisposable = case()
+    inner_observable: AsyncObservable[_TSource] = case()
+    inner_completed: Key = case()
+    completed: Literal[True] = case()
 
 
-__all__ = [
-    "Msg",
-    "DisposeMsg",
-    "CompletedMsg",
-    "InnerCompletedMsg",
-    "InnerObservableMsg",
-    "DisposableMsg",
-]
+__all__ = ["Msg"]
diff --git a/aioreactive/notification.py b/aioreactive/notification.py
index ea62fb5..16d3f04 100644
--- a/aioreactive/notification.py
+++ b/aioreactive/notification.py
@@ -1,11 +1,11 @@
 from abc import ABC, abstractmethod
+from collections.abc import Awaitable, Callable, Iterable
 from enum import Enum
-from typing import Any, Awaitable, Callable, Generic, Iterable, TypeVar, get_origin
-
-from expression.core import SupportsMatch
+from typing import Any, Generic, TypeVar, get_origin
 
 from .types import AsyncObserver
 
+
 _TSource = TypeVar("_TSource")
 
 
@@ -38,9 +38,11 @@ def __repr__(self) -> str:
         return str(self)
 
 
-class OnNext(Notification[_TSource], SupportsMatch[_TSource]):
+class OnNext(Notification[_TSource]):
     """Represents an OnNext notification to an observer."""
 
+    __match_args__ = ("value",)
+
     def __init__(self, value: _TSource) -> None:
         """Constructs a notification of a new value."""
         super().__init__(MsgKind.ON_NEXT)
@@ -75,9 +77,11 @@ def __str__(self) -> str:
         return f"OnNext({self.value})"
 
 
-class OnError(Notification[_TSource], SupportsMatch[Exception]):
+class OnError(Notification[_TSource]):
     """Represents an OnError notification to an observer."""
 
+    __match_args__ = ("exception",)
+
     def __init__(self, exception: Exception) -> None:
         """Constructs a notification of an exception."""
         super().__init__(MsgKind.ON_ERROR)
@@ -112,7 +116,7 @@ def __str__(self) -> str:
         return f"OnError({self.exception})"
 
 
-class _OnCompleted(Notification[_TSource], SupportsMatch[bool]):
+class OnCompleted(Notification[_TSource]):
     """Represents an OnCompleted notification to an observer.
 
     Note: Do not use. Use the singleton `OnCompleted` instance instead.
@@ -120,7 +124,6 @@ class _OnCompleted(Notification[_TSource], SupportsMatch[bool]):
 
     def __init__(self) -> None:
         """Constructs a notification of the end of a sequence."""
-
         super().__init__(MsgKind.ON_COMPLETED)
 
     async def accept(
@@ -147,13 +150,9 @@ def __match__(self, pattern: Any) -> Iterable[bool]:
         return []
 
     def __eq__(self, other: Any) -> bool:
-        if isinstance(other, _OnCompleted):
+        if isinstance(other, OnCompleted):
             return True
         return False
 
     def __str__(self) -> str:
         return "OnCompleted"
-
-
-OnCompleted: _OnCompleted[Any] = _OnCompleted()
-"""OnCompleted singleton instance."""
diff --git a/aioreactive/observables.py b/aioreactive/observables.py
index 5da9f85..c6a75d5 100644
--- a/aioreactive/observables.py
+++ b/aioreactive/observables.py
@@ -1,47 +1,34 @@
 import logging
-from typing import (
-    AsyncIterable,
-    AsyncIterator,
-    Awaitable,
-    Callable,
-    Optional,
-    TypeVar,
-    Union,
-)
+from collections.abc import AsyncIterable, AsyncIterator, Awaitable, Callable
+from typing import TypeVar
 
 from expression.system import AsyncDisposable
 
 from .observers import AsyncAnonymousObserver, AsyncIteratorObserver
 from .types import AsyncObservable, AsyncObserver, CloseAsync, SendAsync, ThrowAsync
 
+
 _TSource = TypeVar("_TSource")
 
 log = logging.getLogger(__name__)
 
 
 class AsyncAnonymousObservable(AsyncObservable[_TSource]):
-
     """An anonymous AsyncObservable.
 
     Uses a custom subscribe method.
     """
 
-    def __init__(
-        self, subscribe: Callable[[AsyncObserver[_TSource]], Awaitable[AsyncDisposable]]
-    ) -> None:
+    def __init__(self, subscribe: Callable[[AsyncObserver[_TSource]], Awaitable[AsyncDisposable]]) -> None:
         self._subscribe = subscribe
 
     async def subscribe_async(
         self,
-        send: Optional[Union[SendAsync[_TSource], AsyncObserver[_TSource]]] = None,
-        throw: Optional[ThrowAsync] = None,
-        close: Optional[CloseAsync] = None,
+        send: SendAsync[_TSource] | AsyncObserver[_TSource] | None = None,
+        throw: ThrowAsync | None = None,
+        close: CloseAsync | None = None,
     ) -> AsyncDisposable:
-        observer = (
-            send
-            if isinstance(send, AsyncObserver)
-            else AsyncAnonymousObserver(send, throw, close)
-        )
+        observer = send if isinstance(send, AsyncObserver) else AsyncAnonymousObserver(send, throw, close)
         log.debug("AsyncAnonymousObservable:subscribe_async(%s)", self._subscribe)
         return await self._subscribe(observer)
 
@@ -52,15 +39,11 @@ def __init__(self, source: AsyncObservable[_TSource]) -> None:
 
     async def subscribe_async(
         self,
-        send: Optional[Union[SendAsync[_TSource], AsyncObserver[_TSource]]] = None,
-        throw: Optional[ThrowAsync] = None,
-        close: Optional[CloseAsync] = None,
+        send: SendAsync[_TSource] | AsyncObserver[_TSource] | None = None,
+        throw: ThrowAsync | None = None,
+        close: CloseAsync | None = None,
     ) -> AsyncDisposable:
-        observer = (
-            send
-            if isinstance(send, AsyncObserver)
-            else AsyncAnonymousObserver(send, throw, close)
-        )
+        observer = send if isinstance(send, AsyncObserver) else AsyncAnonymousObserver(send, throw, close)
         return await self._source.subscribe_async(observer)
 
     def __aiter__(self) -> AsyncIterator[_TSource]:
@@ -73,7 +56,6 @@ def __aiter__(self) -> AsyncIterator[_TSource]:
         Returns:
             An async iterator.
         """
-
         return AsyncIteratorObserver(self)
 
 
diff --git a/aioreactive/observers.py b/aioreactive/observers.py
index 3af9dd8..d62ee76 100644
--- a/aioreactive/observers.py
+++ b/aioreactive/observers.py
@@ -1,35 +1,23 @@
 import logging
 from asyncio import Future, iscoroutinefunction
-from typing import (
-    Any,
-    AsyncIterable,
-    AsyncIterator,
-    Awaitable,
-    Callable,
-    Coroutine,
-    List,
-    Optional,
-    Tuple,
-    TypeVar,
-    cast,
-)
+from collections.abc import AsyncIterable, AsyncIterator, Awaitable, Callable, Coroutine
+from typing import Any, TypeVar, cast
 
 from expression.core import MailboxProcessor, TailCall, tailrec_async
 from expression.system import AsyncDisposable, CancellationTokenSource, Disposable
 
-from .msg import DisposableMsg, DisposeMsg, Msg
+from .msg import Msg
 from .notification import MsgKind, Notification, OnCompleted, OnError, OnNext
 from .types import AsyncObservable, AsyncObserver
 from .utils import anoop
 
+
 log = logging.getLogger(__name__)
 
 _TSource = TypeVar("_TSource")
 
 
-class AsyncIteratorObserver(
-    AsyncObserver[_TSource], AsyncIterable[_TSource], AsyncDisposable
-):
+class AsyncIteratorObserver(AsyncObserver[_TSource], AsyncIterable[_TSource], AsyncDisposable):
     """An async observer that might be iterated asynchronously."""
 
     def __init__(self, source: AsyncObservable[_TSource]) -> None:
@@ -38,8 +26,8 @@ def __init__(self, source: AsyncObservable[_TSource]) -> None:
         self._push: Future[_TSource] = Future()
         self._pull: Future[bool] = Future()
 
-        self._awaiters: List[Future[bool]] = []
-        self._subscription: Optional[AsyncDisposable] = None
+        self._awaiters: list[Future[bool]] = []
+        self._subscription: AsyncDisposable | None = None
         self._source = source
         self._busy = False
 
@@ -111,13 +99,14 @@ class AsyncAnonymousObserver(AsyncObserver[_TSource]):
 
     Creates as sink where the implementation is provided by three
     optional and anonymous functions, asend, athrow and aclose. Used for
-    listening to a source."""
+    listening to a source.
+    """
 
     def __init__(
         self,
-        asend: Optional[Callable[[_TSource], Awaitable[None]]] = None,
-        athrow: Optional[Callable[[Exception], Awaitable[None]]] = None,
-        aclose: Optional[Callable[[], Awaitable[None]]] = None,
+        asend: Callable[[_TSource], Awaitable[None]] | None = None,
+        athrow: Callable[[Exception], Awaitable[None]] | None = None,
+        aclose: Callable[[], Awaitable[None]] | None = None,
     ) -> None:
         super().__init__()
         self._asend = asend or anoop
@@ -140,7 +129,7 @@ async def aclose(self) -> None:
 
 
 class AsyncNotificationObserver(AsyncObserver[_TSource]):
-    """Observer created from an async notification processing function"""
+    """Observer created from an async notification processing function."""
 
     def __init__(self, fn: Callable[[Notification[_TSource]], Awaitable[None]]) -> None:
         self._fn = fn
@@ -152,17 +141,17 @@ async def athrow(self, error: Exception) -> None:
         await self._fn(OnError(error))
 
     async def aclose(self) -> None:
-        await self._fn(OnCompleted)
+        await self._fn(OnCompleted())
 
 
 def noop() -> AsyncObserver[Any]:
     return AsyncAnonymousObserver(anoop, anoop, anoop)
 
 
-def safe_observer(
-    obv: AsyncObserver[_TSource], disposable: AsyncDisposable
-) -> AsyncObserver[_TSource]:
-    """Safe observer that wraps the given observer. Makes sure that
+def safe_observer(obv: AsyncObserver[_TSource], disposable: AsyncDisposable) -> AsyncObserver[_TSource]:
+    """Safe observer that wraps the given observer.
+
+    Makes sure that
     invocations are serialized and that the Rx grammar is not violated:
 
         `(OnNext*(OnError|OnCompleted)?)`
@@ -205,18 +194,16 @@ async def athrow(ex: Exception) -> None:
         agent.post(OnError(ex))
 
     async def aclose() -> None:
-        agent.post(OnCompleted)
+        agent.post(OnCompleted())
 
     return AsyncAnonymousObserver(asend, athrow, aclose)
 
 
 def auto_detach_observer(
     obv: AsyncObserver[_TSource],
-) -> Tuple[
+) -> tuple[
     AsyncObserver[_TSource],
-    Callable[
-        [Coroutine[None, None, AsyncDisposable]], Coroutine[None, None, AsyncDisposable]
-    ],
+    Callable[[Coroutine[None, None, AsyncDisposable]], Coroutine[None, None, AsyncDisposable]],
 ]:
     cts = CancellationTokenSource()
     token = cts.token
@@ -224,19 +211,20 @@ def auto_detach_observer(
     async def worker(inbox: MailboxProcessor[Msg[_TSource]]) -> None:
         @tailrec_async
         async def message_loop(
-            disposables: List[AsyncDisposable],
+            disposables: list[AsyncDisposable],
         ) -> Any:
             if token.is_cancellation_requested:
                 return
 
             cmd = await inbox.receive()
-            if isinstance(cmd, DisposableMsg):
-                disposables.append(cmd.disposable)
-            else:
-                for disp in disposables:
-                    await disp.dispose_async()
-                return
-            return TailCall[List[AsyncDisposable]](disposables)
+            match cmd:
+                case Msg(tag="disposable", disposable=disposable):
+                    disposables.append(disposable)
+                case _:
+                    for disp in disposables:
+                        await disp.dispose_async()
+                    return
+            return TailCall[list[AsyncDisposable]](disposables)
 
         await message_loop([])
 
@@ -244,17 +232,15 @@ async def message_loop(
 
     async def cancel() -> None:
         cts.cancel()
-        agent.post(DisposeMsg)
+        agent.post(Msg(dispose=True))
 
     canceller = AsyncDisposable.create(cancel)
     safe_obv = safe_observer(obv, canceller)
 
     # Auto-detaches (disposes) the disposable when the observer completes with success or error.
-    async def auto_detach(
-        async_disposable: Coroutine[None, None, AsyncDisposable]
-    ) -> AsyncDisposable:
+    async def auto_detach(async_disposable: Coroutine[None, None, AsyncDisposable]) -> AsyncDisposable:
         disposable = await async_disposable
-        agent.post(DisposableMsg(disposable))
+        agent.post(Msg(disposable=disposable))
         return disposable
 
     return safe_obv, auto_detach
@@ -265,7 +251,8 @@ class AsyncAwaitableObserver(Future[_TSource], AsyncObserver[_TSource], Disposab
 
     Both a future and async observer. The future resolves with the last
     value before the observer is closed. A close without any values sent
-    is the same as cancelling the future."""
+    is the same as cancelling the future.
+    """
 
     def __init__(
         self,
diff --git a/aioreactive/subject.py b/aioreactive/subject.py
index 560ee05..d3d7eb1 100644
--- a/aioreactive/subject.py
+++ b/aioreactive/subject.py
@@ -1,21 +1,19 @@
 import logging
 from asyncio import Future
-from typing import List, Optional, TypeVar, Union
+from typing import TypeVar
 
 from expression.system import AsyncDisposable, ObjectDisposedException
 
 from .observables import AsyncAnonymousObserver, AsyncObservable
 from .types import AsyncObserver, CloseAsync, SendAsync, ThrowAsync
 
+
 log = logging.getLogger(__name__)
 
 _TSource = TypeVar("_TSource")
 
 
-class AsyncSingleSubject(
-    AsyncObserver[_TSource], AsyncObservable[_TSource], AsyncDisposable
-):
-
+class AsyncSingleSubject(AsyncObserver[_TSource], AsyncObservable[_TSource], AsyncDisposable):
     """An stream with a single sink.
 
     Both an async observable and async observer.
@@ -28,7 +26,7 @@ def __init__(self) -> None:
         super().__init__()
 
         self._wait: Future[bool] = Future()
-        self._observer: Optional[AsyncObserver[_TSource]] = None
+        self._observer: AsyncObserver[_TSource] | None = None
         self._is_disposed = False
         self._is_stopped = False
 
@@ -82,18 +80,13 @@ async def dispose_async(self) -> None:
 
     async def subscribe_async(
         self,
-        send: Optional[Union[SendAsync[_TSource], AsyncObserver[_TSource]]] = None,
-        throw: Optional[ThrowAsync] = None,
-        close: Optional[CloseAsync] = None,
+        send: SendAsync[_TSource] | AsyncObserver[_TSource] | None = None,
+        throw: ThrowAsync | None = None,
+        close: CloseAsync | None = None,
     ) -> AsyncDisposable:
         """Start streaming."""
-
         self.check_disposed()
-        self._observer = (
-            send
-            if isinstance(send, AsyncObserver)
-            else AsyncAnonymousObserver(send, throw, close)
-        )
+        self._observer = send if isinstance(send, AsyncObserver) else AsyncAnonymousObserver(send, throw, close)
 
         if not self._wait.done():
             self._wait.set_result(True)
@@ -101,9 +94,7 @@ async def subscribe_async(
         return AsyncDisposable.create(self.dispose_async)
 
 
-class AsyncMultiSubject(
-    AsyncObserver[_TSource], AsyncObservable[_TSource], AsyncDisposable
-):
+class AsyncMultiSubject(AsyncObserver[_TSource], AsyncObservable[_TSource], AsyncDisposable):
     """An stream with a multiple observers.
 
     Both an async observable and async observer.
@@ -114,7 +105,7 @@ class AsyncMultiSubject(
 
     def __init__(self) -> None:
         super().__init__()
-        self._observers: List[AsyncObserver[_TSource]] = []
+        self._observers: list[AsyncObserver[_TSource]] = []
         self._is_disposed = False
         self._is_stopped = False
 
@@ -153,20 +144,15 @@ async def aclose(self) -> None:
 
     async def subscribe_async(
         self,
-        send: Optional[Union[SendAsync[_TSource], AsyncObserver[_TSource]]] = None,
-        throw: Optional[ThrowAsync] = None,
-        close: Optional[CloseAsync] = None,
+        send: SendAsync[_TSource] | AsyncObserver[_TSource] | None = None,
+        throw: ThrowAsync | None = None,
+        close: CloseAsync | None = None,
     ) -> AsyncDisposable:
         """Subscribe."""
-
         log.debug("AsyncMultiStream:subscribe_async()")
         self.check_disposed()
 
-        observer = (
-            send
-            if isinstance(send, AsyncObserver)
-            else AsyncAnonymousObserver(send, throw, close)
-        )
+        observer = send if isinstance(send, AsyncObserver) else AsyncAnonymousObserver(send, throw, close)
         self._observers.append(observer)
 
         async def dispose() -> None:
diff --git a/aioreactive/subscription.py b/aioreactive/subscription.py
index 8c1a86b..904a086 100644
--- a/aioreactive/subscription.py
+++ b/aioreactive/subscription.py
@@ -1,12 +1,14 @@
 import asyncio
 import logging
-from typing import Awaitable, Callable, Optional, TypeVar
+from collections.abc import Awaitable, Callable
+from typing import TypeVar
 
 from expression.system.disposable import AsyncDisposable
 
 from .observers import AsyncAwaitableObserver
 from .types import AsyncObservable, AsyncObserver
 
+
 log = logging.getLogger(__name__)
 
 _TSource = TypeVar("_TSource")
@@ -14,7 +16,7 @@
 
 async def run(
     source: AsyncObservable[_TSource],
-    observer: Optional[AsyncAwaitableObserver[_TSource]] = None,
+    observer: AsyncAwaitableObserver[_TSource] | None = None,
     timeout: int = 2,
 ) -> _TSource:
     """Run the source with the given observer.
@@ -23,7 +25,9 @@ async def run(
     closes and returns the final value received.
 
     Args:
-        timeout -- Seconds before timing out in case source never closes.
+        source: The source observable.
+        observer: The observer to subscribe to the source.
+        timeout: Seconds before timing out in case source never closes.
 
     Returns:
         The last event sent through the stream. If any values have been
@@ -32,7 +36,6 @@ async def run(
         `StopAsyncIteration`. For any other errors it will throw the
         exception.
     """
-
     # For run we need a noopobserver if no observer is specified to avoid
     # blocking the last single stream in the chain.
     obv: AsyncAwaitableObserver[_TSource] = observer or AsyncAwaitableObserver()
diff --git a/aioreactive/timeshift.py b/aioreactive/timeshift.py
index 5cc5ad8..b5b1f94 100644
--- a/aioreactive/timeshift.py
+++ b/aioreactive/timeshift.py
@@ -1,7 +1,8 @@
 import asyncio
 import logging
-from datetime import datetime, timedelta
-from typing import Callable, Iterable, NoReturn, Tuple, TypeVar
+from collections.abc import Awaitable, Callable, Iterable
+from datetime import UTC, datetime, timedelta
+from typing import NoReturn, TypeVar
 
 from expression import curry_flipped
 from expression.collections import seq
@@ -11,7 +12,6 @@
     TailCallResult,
     aiotools,
     fst,
-    match,
     pipe,
     tailrec_async,
 )
@@ -25,6 +25,7 @@
 from .transform import map
 from .types import AsyncDisposable, AsyncObservable, AsyncObserver
 
+
 _TSource = TypeVar("_TSource")
 log = logging.getLogger(__name__)
 
@@ -40,52 +41,50 @@ def delay(
     relative time intervals between the values are preserved.
 
     Args:
+        source: The source observable.
         seconds (float): Number of seconds to delay.
 
     Returns:
         Delayed stream.
     """
-
     cts = CancellationTokenSource()
     token = cts.token
 
     async def subscribe_async(aobv: AsyncObserver[_TSource]) -> AsyncDisposable:
-        async def worker(
-            inbox: MailboxProcessor[Tuple[Notification[_TSource], datetime]]
-        ) -> None:
+        def worker(inbox: MailboxProcessor[tuple[Notification[_TSource], datetime]]) -> Awaitable[None]:
             @tailrec_async
-            async def loop() -> "TailCallResult[None, ...]":
+            async def loop() -> TailCallResult[None, ...]:
                 if token.is_cancellation_requested:
                     return
 
                 ns, due_time = await inbox.receive()
 
-                diff = due_time - datetime.utcnow()
+                diff = due_time - datetime.now(UTC)
                 seconds = diff.total_seconds()
                 if seconds > 0:
                     await asyncio.sleep(seconds)
 
                 async def matcher() -> None:
-                    with match(ns) as case:
-                        for x in case(OnNext[_TSource]):
+                    match ns:
+                        case OnNext(value=x):
                             await aobv.asend(x)
                             return
-                        for err in case(OnError[_TSource]):
+                        case OnError(exception=err):
                             await aobv.athrow(err)
                             return
-                        for x in case(OnCompleted):
+                        case _:
                             await aobv.aclose()
                             return
 
                 await matcher()
                 return TailCall["..."]()
 
-            asyncio.ensure_future(loop())
+            return asyncio.ensure_future(loop())
 
         agent = MailboxProcessor.start(worker, token)
 
         async def fn(ns: Notification[_TSource]) -> None:
-            due_time = datetime.utcnow() + timedelta(seconds=seconds)
+            due_time = datetime.now(UTC) + timedelta(seconds=seconds)
             agent.post((ns, due_time))
 
         obv: AsyncNotificationObserver[_TSource] = AsyncNotificationObserver(fn)
@@ -109,28 +108,26 @@ async def subscribe_async(aobv: AsyncObserver[_TSource]) -> AsyncDisposable:
             safe_obv, auto_detach = auto_detach_observer(aobv)
             infinite: Iterable[int] = seq.infinite
 
-            async def worker(
-                inbox: MailboxProcessor[Tuple[Notification[_TSource], int]]
-            ) -> None:
+            async def worker(inbox: MailboxProcessor[tuple[Notification[_TSource], int]]) -> None:
                 @tailrec_async
                 async def message_loop(
                     current_index: int,
                 ) -> "TailCallResult[NoReturn, [int]]":
                     n, index = await inbox.receive()
 
-                    with match(n) as case:
-                        log.debug("debounce: %s, %d, %d", n, index, current_index)
-                        for x in case(OnNext[_TSource]):
+                    match n:
+                        case OnNext(value=x):
+                            log.debug("debounce: %s, %d, %d", n, index, current_index)
                             if index == current_index:
                                 await safe_obv.asend(x)
                                 current_index = index
                             elif index > current_index:
                                 current_index = index
 
-                        for err in case(OnError[_TSource]):
+                        case OnError(exception=err):
                             await safe_obv.athrow(err)
 
-                        while case(OnCompleted):
+                        case _:
                             await safe_obv.aclose()
 
                     return TailCall[int](current_index)
diff --git a/aioreactive/transform.py b/aioreactive/transform.py
index c563277..87f60a3 100644
--- a/aioreactive/transform.py
+++ b/aioreactive/transform.py
@@ -1,4 +1,7 @@
-from typing import Any, Awaitable, Callable, Iterable, TypeVar
+from __future__ import annotations
+
+from collections.abc import Awaitable, Callable, Iterable
+from typing import Any, TypeVar
 
 from expression.collections import seq
 from expression.core import (
@@ -9,7 +12,6 @@
     TailCall,
     TailCallResult,
     compose,
-    match,
     pipe,
     tailrec_async,
 )
@@ -18,10 +20,6 @@
 from .combine import merge_inner, zip_seq
 from .create import fail
 from .msg import (
-    CompletedMsg,
-    DisposeMsg,
-    InnerCompletedMsg,
-    InnerObservableMsg,
     Key,
     Msg,
 )
@@ -29,6 +27,7 @@
 from .observers import AsyncAnonymousObserver, auto_detach_observer
 from .types import AsyncObserver
 
+
 _TSource = TypeVar("_TSource")
 _TResult = TypeVar("_TResult")
 
@@ -43,16 +42,14 @@ def transform(
             _TSource,
         ],
         Awaitable[None],
-    ]
+    ],
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TResult]]:
     def _(source: AsyncObservable[_TSource]) -> AsyncObservable[_TResult]:
         async def subscribe_async(aobv: AsyncObserver[_TResult]) -> AsyncDisposable:
             async def asend(value: _TSource) -> None:
                 return await anext(aobv.asend, value)
 
-            obv: AsyncObserver[_TSource] = AsyncAnonymousObserver(
-                asend, aobv.athrow, aobv.aclose
-            )
+            obv: AsyncObserver[_TSource] = AsyncAnonymousObserver(asend, aobv.athrow, aobv.aclose)
             sub = await source.subscribe_async(obv)
             return sub
 
@@ -62,11 +59,14 @@ async def asend(value: _TSource) -> None:
 
 
 def map_async(
-    amapper: Callable[[_TSource], Awaitable[_TResult]]
+    amapper: Callable[[_TSource], Awaitable[_TResult]],
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TResult]]:
-    """Returns an observable sequence whose elements are the result of
+    """Map async.
+
+    Returns an observable sequence whose elements are the result of
     invoking the async mapper function on each element of the
-    source."""
+    source.
+    """
 
     async def handler(next: Callable[[_TResult], Awaitable[None]], x: _TSource) -> None:
         b = await amapper(x)
@@ -76,57 +76,50 @@ async def handler(next: Callable[[_TResult], Awaitable[None]], x: _TSource) -> N
 
 
 def starmap_async(
-    amapper: Callable[..., Awaitable[_TResult]]
+    amapper: Callable[..., Awaitable[_TResult]],
 ) -> Callable[[AsyncObservable[Any]], AsyncObservable[_TResult]]:
     """Map async spreading arguments to the async mapper.
 
     Returns an observable sequence whose elements are the result of
     invoking the async mapper function on each element of the
-    source."""
+    source.
+    """
 
-    async def handler(
-        next: Callable[[_TResult], Awaitable[None]], args: Iterable[Any]
-    ) -> None:
+    async def handler(next: Callable[[_TResult], Awaitable[None]], args: Iterable[Any]) -> None:
         b = await amapper(*args)
         await next(b)
 
     return transform(handler)
 
 
-def map(
-    mapper: Callable[[_TSource], _TResult]
-) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TResult]]:
+def map(mapper: Callable[[_TSource], _TResult]) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TResult]]:
     """Map each element in the stream.
 
     Returns an observable sequence whose elements are the result of
-    invoking the mapper function on each element of the source."""
+    invoking the mapper function on each element of the source.
+    """
 
-    def handler(
-        next: Callable[[_TResult], Awaitable[None]], x: _TSource
-    ) -> Awaitable[None]:
+    def handler(next: Callable[[_TResult], Awaitable[None]], x: _TSource) -> Awaitable[None]:
         return next(mapper(x))
 
     return transform(handler)
 
 
-def starmap(
-    mapper: Callable[..., _TResult]
-) -> Callable[[AsyncObservable[Any]], AsyncObservable[_TResult]]:
+def starmap(mapper: Callable[..., _TResult]) -> Callable[[AsyncObservable[Any]], AsyncObservable[_TResult]]:
     """Map and spread the arguments to the mapper.
 
     Returns an observable sequence whose elements are the result of
-    invoking the mapper function on each element of the source."""
+    invoking the mapper function on each element of the source.
+    """
 
-    def handler(
-        next: Callable[[_TResult], Awaitable[None]], args: Iterable[Any]
-    ) -> Awaitable[None]:
+    def handler(next: Callable[[_TResult], Awaitable[None]], args: Iterable[Any]) -> Awaitable[None]:
         return next(mapper(*args))
 
     return transform(handler)
 
 
 def mapi_async(
-    mapper: Callable[[_TSource, int], Awaitable[_TResult]]
+    mapper: Callable[[_TSource, int], Awaitable[_TResult]],
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TResult]]:
     """Map with index async.
 
@@ -134,7 +127,6 @@ def mapi_async(
     invoking the async mapper function by incorporating the element's
     index on each element of the source.
     """
-
     return compose(
         zip_seq(seq.infinite),
         starmap_async(mapper),
@@ -142,7 +134,7 @@ def mapi_async(
 
 
 def mapi(
-    mapper: Callable[[_TSource, int], _TResult]
+    mapper: Callable[[_TSource, int], _TResult],
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TResult]]:
     """Map with index.
 
@@ -157,7 +149,7 @@ def mapi(
 
 
 def flat_map(
-    mapper: Callable[[_TSource], AsyncObservable[_TResult]]
+    mapper: Callable[[_TSource], AsyncObservable[_TResult]],
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TResult]]:
     """Flap map the observable sequence.
 
@@ -171,7 +163,6 @@ def flat_map(
     Returns:
         The result stream.
     """
-
     return compose(
         map(mapper),
         merge_inner(0),
@@ -179,7 +170,7 @@ def flat_map(
 
 
 def flat_mapi(
-    mapper: Callable[[_TSource, int], AsyncObservable[_TResult]]
+    mapper: Callable[[_TSource, int], AsyncObservable[_TResult]],
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TResult]]:
     """Flat map with index.
 
@@ -195,7 +186,6 @@ def flat_mapi(
     Returns:
         Stream[TSource, TResult]: [description]
     """
-
     return compose(
         mapi(mapper),
         merge_inner(0),
@@ -203,7 +193,7 @@ def flat_mapi(
 
 
 def flat_map_async(
-    mapper: Callable[[_TSource], Awaitable[AsyncObservable[_TResult]]]
+    mapper: Callable[[_TSource], Awaitable[AsyncObservable[_TResult]]],
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TResult]]:
     """Flap map async.
 
@@ -213,8 +203,7 @@ def flat_map_async(
 
 
     Args:
-        mapperCallable ([type]): [description]
-        Awaitable ([type]): [description]
+        mapper: Function to transform each item in the stream.
 
     Returns:
         Stream[TSource, TResult]: [description]
@@ -226,7 +215,7 @@ def flat_map_async(
 
 
 def flat_mapi_async(
-    mapper: Callable[[_TSource, int], Awaitable[AsyncObservable[_TResult]]]
+    mapper: Callable[[_TSource, int], Awaitable[AsyncObservable[_TResult]]],
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TResult]]:
     """Flat map async with index.
 
@@ -236,8 +225,7 @@ def flat_mapi_async(
     back into one observable sequence.
 
     Args:
-        mapperAsync ([type]): [description]
-        Awaitable ([type]): [description]
+        mapper: Function to transform each item in the stream.
 
     Returns:
         Stream[TSource, TResult]: [description]
@@ -249,7 +237,7 @@ def flat_mapi_async(
 
 
 def concat_map(
-    mapper: Callable[[_TSource], AsyncObservable[_TResult]]
+    mapper: Callable[[_TSource], AsyncObservable[_TResult]],
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TResult]]:
     return compose(
         map(mapper),
@@ -276,9 +264,7 @@ def switch_latest(
     async def subscribe_async(aobv: AsyncObserver[_TSource]) -> AsyncDisposable:
         safe_obv, auto_detach = auto_detach_observer(aobv)
 
-        def obv(
-            mb: MailboxProcessor[Msg[_TSource]], id: int
-        ) -> AsyncObserver[_TSource]:
+        def obv(mb: MailboxProcessor[Msg[_TSource]], id: int) -> AsyncObserver[_TSource]:
             async def asend(value: _TSource) -> None:
                 await safe_obv.asend(value)
 
@@ -287,8 +273,7 @@ async def athrow(error: Exception) -> None:
 
             async def aclose() -> None:
                 pipe(
-                    Key(id),
-                    InnerCompletedMsg,
+                    Msg[Any](inner_completed=Key(id)),
                     mb.post,
                 )
 
@@ -298,35 +283,34 @@ async def worker(inbox: MailboxProcessor[Msg[_TSource]]) -> None:
             @tailrec_async
             async def message_loop(
                 current: Option[AsyncDisposable], is_stopped: bool, current_id: int
-            ) -> "TailCallResult[None, [Option[AsyncDisposable], bool, int]]":
+            ) -> TailCallResult[None, [Option[AsyncDisposable], bool, int]]:
                 cmd = await inbox.receive()
 
-                with match(cmd) as case:
-                    for xs in case(InnerObservableMsg[_TSource]):
+                match cmd:
+                    case Msg(tag="inner_observable", inner_observable=xs):
                         next_id = current_id + 1
                         for disp in current.to_list():
                             await disp.dispose_async()
                         inner = await xs.subscribe_async(obv(inbox, next_id))
                         current, current_id = Some(inner), next_id
-                        break
-                    for idx in case(InnerCompletedMsg[Key]):
+
+                    case Msg(tag="inner_completed", inner_completed=idx):
                         if is_stopped and idx == current_id:
                             await safe_obv.aclose()
                             current, is_stopped = Nothing, True
-                        break
-                    while case(CompletedMsg):
+
+                    case Msg(tag="completed"):
                         if current.is_none():
                             await safe_obv.aclose()
-                        break
-                    while case(DisposeMsg):
+
+                    case Msg(tag="dispose"):
                         if current.is_some():
                             await current.value.dispose_async()
                         current, is_stopped = Nothing, True
-                        break
+                    case _:
+                        raise ValueError(f"Unknown message: {cmd}")
 
-                return TailCall[Option[AsyncDisposable], bool, int](
-                    current, is_stopped, current_id
-                )
+                return TailCall[Option[AsyncDisposable], bool, int](current, is_stopped, current_id)
 
             await message_loop(Nothing, False, 0)
 
@@ -334,7 +318,7 @@ async def message_loop(
 
         async def asend(xs: AsyncObservable[_TSource]) -> None:
             pipe(
-                InnerObservableMsg(xs),
+                Msg(inner_observable=xs),
                 inner_agent.post,
             )
 
@@ -342,19 +326,18 @@ async def athrow(error: Exception) -> None:
             await safe_obv.athrow(error)
 
         async def aclose() -> None:
-            inner_agent.post(CompletedMsg)
+            inner_agent.post(Msg(completed=True))
 
         _obv = AsyncAnonymousObserver(asend, athrow, aclose)
         dispose = await pipe(
             _obv,
-            AsyncObserver,
             source.subscribe_async,
             auto_detach,
         )
 
         async def cancel() -> None:
             await dispose.dispose_async()
-            inner_agent.post(DisposeMsg)
+            inner_agent.post(Msg(dispose=True))
 
         return AsyncDisposable.create(cancel)
 
@@ -362,7 +345,7 @@ async def cancel() -> None:
 
 
 def flat_map_latest_async(
-    mapper: Callable[[_TSource], Awaitable[AsyncObservable[_TResult]]]
+    mapper: Callable[[_TSource], Awaitable[AsyncObservable[_TResult]]],
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TResult]]:
     """Flat map latest async.
 
@@ -381,11 +364,10 @@ def flat_map_latest_async(
 
 
 def flat_map_latest(
-    mapper: Callable[[_TSource], AsyncObservable[_TResult]]
+    mapper: Callable[[_TSource], AsyncObservable[_TResult]],
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TResult]]:
     """Flat map latest.
 
-
     Transforms the items emitted by an source sequence into observable
     streams, and mirror those items emitted by the most-recently
     transformed observable sequence.
@@ -400,7 +382,7 @@ def flat_map_latest(
 
 
 def catch(
-    handler: Callable[[Exception], AsyncObservable[_TSource]]
+    handler: Callable[[Exception], AsyncObservable[_TSource]],
 ) -> Callable[[AsyncObservable[_TSource]], AsyncObservable[_TSource]]:
     """Catch Exception.
 
diff --git a/aioreactive/types.py b/aioreactive/types.py
index 417f4b8..c8d41e2 100644
--- a/aioreactive/types.py
+++ b/aioreactive/types.py
@@ -1,8 +1,10 @@
 from abc import abstractmethod
-from typing import Awaitable, Callable, Generic, Optional, Protocol, TypeVar, Union
+from collections.abc import Awaitable, Callable
+from typing import Generic, Protocol, TypeVar
 
 from expression.system import AsyncDisposable
 
+
 _T = TypeVar("_T")
 _TSource = TypeVar("_TSource")
 _T_out = TypeVar("_T_out", covariant=True)  # Any type covariant containers.
@@ -37,22 +39,22 @@ class AsyncObservable(Generic[_T_out]):
     @abstractmethod
     async def subscribe_async(
         self,
-        send: Optional[Union[SendAsync[_T_out], AsyncObserver[_T_out]]] = None,
-        throw: Optional[ThrowAsync] = None,
-        close: Optional[CloseAsync] = None,
+        send: SendAsync[_T_out] | AsyncObserver[_T_out] | None = None,
+        throw: ThrowAsync | None = None,
+        close: CloseAsync | None = None,
     ) -> AsyncDisposable:
         raise NotImplementedError
 
 
 class Flatten(Protocol):
-    """A zipping projetion is a function that projects from one observable to a zipped, i.e:
+    """Flatten protocol.
+
+    A zipping projetion is a function that projects from one observable to a zipped, i.e:
 
     `AsyncObservable[AsyncObservable[TSource]]) -> AsyncObservable[Tuple[TSource, TResult]]`
     """
 
-    def __call__(
-        self, __source: AsyncObservable[AsyncObservable[_TSource]]
-    ) -> AsyncObservable[_TSource]:
+    def __call__(self, __source: AsyncObservable[AsyncObservable[_TSource]]) -> AsyncObservable[_TSource]:
         raise NotImplementedError
 
 
diff --git a/aioreactive/utils.py b/aioreactive/utils.py
index 2700182..d49fd67 100644
--- a/aioreactive/utils.py
+++ b/aioreactive/utils.py
@@ -1,19 +1,20 @@
 import logging
-from typing import Any, Optional, TypeVar
+from typing import Any, TypeVar
 
 from .types import AsyncObserver
 
+
 _TSource = TypeVar("_TSource")
 
 log = logging.getLogger(__name__)
 
 
 def noop(*args: Any, **kw: Any) -> None:
-    """No operation. Returns nothing"""
+    """No operation. Returns nothing."""
 
 
-async def anoop(value: Optional[Any] = None) -> None:
-    """Async no operation. Returns nothing"""
+async def anoop(value: Any | None = None) -> None:
+    """Async no operation. Returns nothing."""
 
 
 class NoopObserver(AsyncObserver[_TSource]):
diff --git a/examples/autocomplete/autocomplete.py b/examples/autocomplete/autocomplete.py
index a2beb9a..5e031bd 100644
--- a/examples/autocomplete/autocomplete.py
+++ b/examples/autocomplete/autocomplete.py
@@ -1,8 +1,7 @@
-"""
-Example running an aiohttp server doing search queries against
+"""Example running an aiohttp server doing search queries against
 Wikipedia to populate the autocomplete dropdown in the web UI. Start
 using `python autocomplete.py` and navigate your web browser to
-http://localhost:8080
+http://localhost:8080.
 
 Requirements:
 > pip3 install aiohttp
@@ -12,7 +11,7 @@
 import json
 import os
 from asyncio.events import AbstractEventLoop
-from typing import Any, Callable, Dict
+from typing import Any
 
 import aiohttp
 import aiohttp_jinja2
@@ -25,7 +24,8 @@
 
 import aioreactive as rx
 
-Msg = Dict[str, str]
+
+Msg = dict[str, str]
 
 
 async def search_wikipedia(term: str) -> rx.AsyncObservable[str]:
@@ -45,7 +45,9 @@ async def websocket_handler(request: Request) -> WebSocketResponse:
 
     stream: rx.AsyncObservable[Msg] = rx.AsyncSubject()
 
-    mapper: Callable[[Msg], str] = lambda x: x["term"]
+    def mapper(x: Msg) -> str:
+        return x["term"]
+
     xs = pipe(
         stream,
         rx.map(mapper),
@@ -81,7 +83,7 @@ async def athrow(ex: Exception) -> None:
 
 
 @aiohttp_jinja2.template("index.html")
-async def index(request: Request) -> Dict[str, Any]:
+async def index(request: Request) -> dict[str, Any]:
     return dict()
 
 
diff --git a/examples/parallel/parallel.py b/examples/parallel/parallel.py
index df4e82e..ca3bc59 100644
--- a/examples/parallel/parallel.py
+++ b/examples/parallel/parallel.py
@@ -9,6 +9,7 @@
 
 import aioreactive as rx
 
+
 log = logging.getLogger(__name__)
 logging.basicConfig(level=logging.INFO)
 
@@ -16,9 +17,9 @@
 
 
 def long_running(value: int) -> int:
-    print("Long running ({0}) on thread {1}".format(value, current_thread().name))
+    print(f"Long running ({value}) on thread {current_thread().name}")
     time.sleep(3)
-    print("Long running, done ({0}) on thread {1}".format(value, current_thread().name))
+    print(f"Long running, done ({value}) on thread {current_thread().name}")
     return value
 
 
diff --git a/examples/timeflies/timeflies.py b/examples/timeflies/timeflies.py
index d7e166c..c31bfc6 100644
--- a/examples/timeflies/timeflies.py
+++ b/examples/timeflies/timeflies.py
@@ -3,15 +3,16 @@
 import sys
 from tkinter import Event, Frame, Label, Misc, Tk
 from types import FrameType
-from typing import Optional, Tuple
+from typing import Any
 
-from expression.core import MailboxProcessor, pipe
+from expression import MailboxProcessor, pipe
 from expression.system import AsyncDisposable
 
 import aioreactive as rx
 from aioreactive import AsyncAnonymousObserver, AsyncSubject
 from aioreactive.types import AsyncObservable
 
+
 # logging.basicConfig(format="%(levelname)s:%(message)s", level=logging.DEBUG)
 
 
@@ -19,7 +20,7 @@ async def main() -> None:
     root = Tk()
     root.title("aioreactive")
 
-    mousemoves: AsyncSubject[Tuple[int, int]] = AsyncSubject()
+    mousemoves: AsyncSubject[tuple[int, int]] = AsyncSubject()
 
     frame = Frame(root, width=800, height=600)  # , bg="white")
 
@@ -34,10 +35,10 @@ async def worker(mb: MailboxProcessor["Event[Misc]"]) -> None:
     text = "TIME FLIES LIKE AN ARROW"
     labels = [Label(frame, text=c, borderwidth=0, padx=0, pady=0) for c in text]
 
-    def handle_label(label: Label, i: int) -> AsyncObservable[Tuple[Label, int, int]]:
+    def handle_label(label: Label, i: int) -> AsyncObservable[tuple[Label, int, int]]:
         label.config(dict(borderwidth=0, padx=0, pady=0))
 
-        def mapper(x: int, y: int) -> Tuple[Label, int, int]:
+        def mapper(x: int, y: int) -> tuple[Label, int, int]:
             """Map mouse-move pos to label and new pos for label."""
             return label, x + i * 12 + 15, y
 
@@ -55,15 +56,16 @@ def mapper(x: int, y: int) -> Tuple[Label, int, int]:
 
     subscription: AsyncDisposable
     running = True
+    tasks: list[asyncio.Task[Any]] = []
 
-    async def asend(value: Tuple[Label, int, int]) -> None:
+    async def asend(value: tuple[Label, int, int]) -> None:
         """Perform side effect."""
         label, x, y = value
         label.place(x=x, y=y)
 
     async def athrow(ex: Exception):
-        nonlocal running
-        print("Exception: ", ex)
+        nonlocal running, subscription
+        print("Exception: ", ex)  # noqa
         await subscription.dispose_async()
         running = False
 
@@ -80,17 +82,21 @@ async def stop():
         await subscription.dispose_async()
 
     def handle_focus_in(event: "Event[Misc]"):
-        asyncio.ensure_future(start())
+        task = asyncio.ensure_future(start())
+        tasks.append(task)
+        task.add_done_callback(lambda _: tasks.remove(task))
 
     def handle_focus_out(event: "Event[Misc]"):
-        asyncio.ensure_future(stop())
+        task = asyncio.ensure_future(stop())
+        tasks.append(task)
+        task.add_done_callback(lambda _: tasks.remove(task))
 
     root.bind("<FocusIn>", handle_focus_in)
     root.bind("<FocusOut>", handle_focus_out)
 
     frame.pack()
 
-    def signal_handler(signal: int, frame: Optional[FrameType] = None) -> None:
+    def signal_handler(signal: int, frame: FrameType | None = None) -> None:
         nonlocal running
         running = False
         sys.stderr.write("Exiting...\n")
diff --git a/poetry.lock b/poetry.lock
index 7119ecb..71c6cea 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,111 +1,209 @@
-[[package]]
-name = "atomicwrites"
-version = "1.4.0"
-description = "Atomic file writes."
-category = "dev"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-
-[[package]]
-name = "attrs"
-version = "21.4.0"
-description = "Classes Without Boilerplate"
-category = "dev"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-
-[package.extras]
-dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
-docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
-tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
-tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]
+# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
 
 [[package]]
 name = "autoflake"
-version = "1.4"
+version = "1.7.8"
 description = "Removes unused imports and unused variables"
-category = "dev"
 optional = false
-python-versions = "*"
-
-[package.dependencies]
-pyflakes = ">=1.1.0"
-
-[[package]]
-name = "black"
-version = "22.3.0"
-description = "The uncompromising code formatter."
-category = "dev"
-optional = false
-python-versions = ">=3.6.2"
+python-versions = ">=3.7"
+files = [
+    {file = "autoflake-1.7.8-py3-none-any.whl", hash = "sha256:46373ef69b6714f5064c923bb28bd797c4f8a9497f557d87fc36665c6d956b39"},
+    {file = "autoflake-1.7.8.tar.gz", hash = "sha256:e7e46372dee46fa1c97acf310d99d922b63d369718a270809d7c278d34a194cf"},
+]
 
 [package.dependencies]
-click = ">=8.0.0"
-mypy-extensions = ">=0.4.3"
-pathspec = ">=0.9.0"
-platformdirs = ">=2"
-tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
-typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
-
-[package.extras]
-colorama = ["colorama (>=0.4.3)"]
-d = ["aiohttp (>=3.7.4)"]
-jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
-uvloop = ["uvloop (>=0.15.2)"]
+pyflakes = ">=1.1.0,<3"
 
 [[package]]
 name = "certifi"
-version = "2022.6.15"
+version = "2023.11.17"
 description = "Python package for providing Mozilla's CA Bundle."
-category = "dev"
 optional = false
 python-versions = ">=3.6"
+files = [
+    {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"},
+    {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"},
+]
 
 [[package]]
 name = "cfgv"
-version = "3.3.1"
+version = "3.4.0"
 description = "Validate configuration and produce human readable error messages."
-category = "dev"
 optional = false
-python-versions = ">=3.6.1"
+python-versions = ">=3.8"
+files = [
+    {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
+    {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
+]
 
 [[package]]
 name = "charset-normalizer"
-version = "2.0.12"
+version = "3.3.2"
 description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
-category = "dev"
 optional = false
-python-versions = ">=3.5.0"
-
-[package.extras]
-unicode_backport = ["unicodedata2"]
-
-[[package]]
-name = "click"
-version = "8.1.3"
-description = "Composable command line interface toolkit"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
-
-[package.dependencies]
-colorama = {version = "*", markers = "platform_system == \"Windows\""}
+python-versions = ">=3.7.0"
+files = [
+    {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"},
+    {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
+]
 
 [[package]]
 name = "colorama"
-version = "0.4.5"
+version = "0.4.6"
 description = "Cross-platform colored terminal text."
-category = "dev"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+files = [
+    {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+    {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
 
 [[package]]
 name = "coverage"
-version = "6.4.1"
+version = "6.5.0"
 description = "Code coverage measurement for Python"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
+files = [
+    {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"},
+    {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"},
+    {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"},
+    {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"},
+    {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"},
+    {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"},
+    {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"},
+    {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"},
+    {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"},
+    {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"},
+    {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"},
+    {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"},
+    {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"},
+    {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"},
+    {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"},
+    {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"},
+    {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"},
+    {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"},
+    {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"},
+    {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"},
+    {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"},
+    {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"},
+    {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"},
+    {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"},
+    {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"},
+    {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"},
+    {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"},
+    {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"},
+    {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"},
+    {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"},
+    {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"},
+    {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"},
+    {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"},
+    {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"},
+    {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"},
+    {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"},
+    {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"},
+    {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"},
+    {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"},
+    {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"},
+    {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"},
+    {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"},
+    {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"},
+    {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"},
+    {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"},
+    {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"},
+    {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"},
+    {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"},
+    {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"},
+    {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"},
+]
 
 [package.extras]
 toml = ["tomli"]
@@ -114,12 +212,15 @@ toml = ["tomli"]
 name = "coveralls"
 version = "3.3.1"
 description = "Show coverage stats online via coveralls.io"
-category = "dev"
 optional = false
 python-versions = ">= 3.5"
+files = [
+    {file = "coveralls-3.3.1-py2.py3-none-any.whl", hash = "sha256:f42015f31d386b351d4226389b387ae173207058832fbf5c8ec4b40e27b16026"},
+    {file = "coveralls-3.3.1.tar.gz", hash = "sha256:b32a8bb5d2df585207c119d6c01567b81fba690c9c10a753bfe27a335bfc43ea"},
+]
 
 [package.dependencies]
-coverage = ">=4.1,<6.0.0 || >6.1,<6.1.1 || >6.1.1,<7.0"
+coverage = ">=4.1,<6.0.dev0 || >6.1,<6.1.1 || >6.1.1,<7.0"
 docopt = ">=0.6.1"
 requests = ">=1.0.0"
 
@@ -128,198 +229,159 @@ yaml = ["PyYAML (>=3.10)"]
 
 [[package]]
 name = "distlib"
-version = "0.3.4"
+version = "0.3.8"
 description = "Distribution utilities"
-category = "dev"
 optional = false
 python-versions = "*"
+files = [
+    {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
+    {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
+]
 
 [[package]]
 name = "docopt"
 version = "0.6.2"
 description = "Pythonic argument parser, that will make you smile"
-category = "dev"
 optional = false
 python-versions = "*"
+files = [
+    {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"},
+]
 
 [[package]]
 name = "dunamai"
-version = "1.12.0"
+version = "1.19.0"
 description = "Dynamic version generation"
-category = "dev"
 optional = false
 python-versions = ">=3.5,<4.0"
+files = [
+    {file = "dunamai-1.19.0-py3-none-any.whl", hash = "sha256:1ed948676bbf0812bfaafe315a134634f8d6eb67138513c75aa66e747404b9c6"},
+    {file = "dunamai-1.19.0.tar.gz", hash = "sha256:6ad99ae34f7cd290550a2ef1305d2e0292e6e6b5b1b830dfc07ceb7fd35fec09"},
+]
 
 [package.dependencies]
 packaging = ">=20.9"
 
-[[package]]
-name = "execnet"
-version = "1.9.0"
-description = "execnet: rapid multi-Python deployment"
-category = "dev"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-
-[package.extras]
-testing = ["pre-commit"]
-
 [[package]]
 name = "expression"
-version = "3.3.1"
+version = "5.0.1"
 description = "Practical functional programming for Python 3.9+"
-category = "main"
 optional = false
-python-versions = ">=3.9,<3.11"
+python-versions = ">=3.11,<4"
+files = [
+    {file = "expression-5.0.1-py3-none-any.whl", hash = "sha256:b5fe712a2e8c7515e4a688cff885f2e24b597f522099b3256e83e1edd6c99965"},
+    {file = "expression-5.0.1.tar.gz", hash = "sha256:3755d9115242d1b6c51f2fc6facc769b9b5ce30077abe861e836639c1e935906"},
+]
 
 [package.dependencies]
 typing-extensions = ">=4.1.1,<5.0.0"
 
+[package.extras]
+all = ["pydantic (>=2.0.0,<3.0.0)"]
+pydantic = ["pydantic (>=2.0.0,<3.0.0)"]
+
 [[package]]
 name = "filelock"
-version = "3.7.1"
+version = "3.13.1"
 description = "A platform independent file lock."
-category = "dev"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
+files = [
+    {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"},
+    {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"},
+]
 
 [package.extras]
-docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"]
-testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"]
-
-[[package]]
-name = "flake8"
-version = "4.0.1"
-description = "the modular source code checker: pep8 pyflakes and co"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-mccabe = ">=0.6.0,<0.7.0"
-pycodestyle = ">=2.8.0,<2.9.0"
-pyflakes = ">=2.4.0,<2.5.0"
+docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"]
+testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"]
+typing = ["typing-extensions (>=4.8)"]
 
 [[package]]
 name = "identify"
-version = "2.5.1"
+version = "2.5.33"
 description = "File identification library for Python"
-category = "dev"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
+files = [
+    {file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"},
+    {file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"},
+]
 
 [package.extras]
 license = ["ukkonen"]
 
 [[package]]
 name = "idna"
-version = "3.3"
+version = "3.6"
 description = "Internationalized Domain Names in Applications (IDNA)"
-category = "dev"
 optional = false
 python-versions = ">=3.5"
+files = [
+    {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"},
+    {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
+]
 
 [[package]]
 name = "iniconfig"
-version = "1.1.1"
-description = "iniconfig: brain-dead simple config-ini parsing"
-category = "dev"
-optional = false
-python-versions = "*"
-
-[[package]]
-name = "isort"
-version = "5.10.1"
-description = "A Python utility / library to sort Python imports."
-category = "dev"
-optional = false
-python-versions = ">=3.6.1,<4.0"
-
-[package.extras]
-pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
-requirements_deprecated_finder = ["pipreqs", "pip-api"]
-colors = ["colorama (>=0.4.3,<0.5.0)"]
-plugins = ["setuptools"]
-
-[[package]]
-name = "mccabe"
-version = "0.6.1"
-description = "McCabe checker, plugin for flake8"
-category = "dev"
+version = "2.0.0"
+description = "brain-dead simple config-ini parsing"
 optional = false
-python-versions = "*"
-
-[[package]]
-name = "mypy"
-version = "0.931"
-description = "Optional static typing for Python"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-mypy-extensions = ">=0.4.3"
-tomli = ">=1.1.0"
-typing-extensions = ">=3.10"
-
-[package.extras]
-dmypy = ["psutil (>=4.0)"]
-python2 = ["typed-ast (>=1.4.0,<2)"]
-
-[[package]]
-name = "mypy-extensions"
-version = "0.4.3"
-description = "Experimental type system extensions for programs checked with the mypy typechecker."
-category = "dev"
-optional = false
-python-versions = "*"
+python-versions = ">=3.7"
+files = [
+    {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
+    {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
+]
 
 [[package]]
 name = "nodeenv"
-version = "1.6.0"
+version = "1.8.0"
 description = "Node.js virtual environment builder"
-category = "dev"
 optional = false
-python-versions = "*"
-
-[[package]]
-name = "packaging"
-version = "21.3"
-description = "Core utilities for Python packages"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
+files = [
+    {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"},
+    {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"},
+]
 
 [package.dependencies]
-pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
+setuptools = "*"
 
 [[package]]
-name = "pathspec"
-version = "0.9.0"
-description = "Utility library for gitignore style pattern matching of file paths."
-category = "dev"
+name = "packaging"
+version = "23.2"
+description = "Core utilities for Python packages"
 optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+python-versions = ">=3.7"
+files = [
+    {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
+    {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
+]
 
 [[package]]
 name = "platformdirs"
-version = "2.5.2"
-description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
-category = "dev"
+version = "4.1.0"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
+files = [
+    {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"},
+    {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"},
+]
 
 [package.extras]
-docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"]
-test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"]
+docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"]
 
 [[package]]
 name = "pluggy"
-version = "1.0.0"
+version = "1.3.0"
 description = "plugin and hook calling mechanisms for python"
-category = "dev"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.8"
+files = [
+    {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"},
+    {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"},
+]
 
 [package.extras]
 dev = ["pre-commit", "tox"]
@@ -327,542 +389,229 @@ testing = ["pytest", "pytest-benchmark"]
 
 [[package]]
 name = "pre-commit"
-version = "2.19.0"
+version = "2.21.0"
 description = "A framework for managing and maintaining multi-language pre-commit hooks."
-category = "dev"
 optional = false
 python-versions = ">=3.7"
+files = [
+    {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"},
+    {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"},
+]
 
 [package.dependencies]
 cfgv = ">=2.0.0"
 identify = ">=1.0.0"
 nodeenv = ">=0.11.1"
 pyyaml = ">=5.1"
-toml = "*"
-virtualenv = ">=20.0.8"
-
-[[package]]
-name = "py"
-version = "1.11.0"
-description = "library with cross-python path, ini-parsing, io, code, log facilities"
-category = "dev"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-
-[[package]]
-name = "pycodestyle"
-version = "2.8.0"
-description = "Python style guide checker"
-category = "dev"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+virtualenv = ">=20.10.0"
 
 [[package]]
 name = "pyflakes"
-version = "2.4.0"
+version = "2.5.0"
 description = "passive checker of Python programs"
-category = "dev"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
-
-[[package]]
-name = "pyparsing"
-version = "3.0.9"
-description = "pyparsing module - Classes and methods to define and execute parsing grammars"
-category = "dev"
-optional = false
-python-versions = ">=3.6.8"
-
-[package.extras]
-diagrams = ["railroad-diagrams", "jinja2"]
-
-[[package]]
-name = "pyright"
-version = "0.0.13.post0"
-description = "Command line wrapper for pyright"
-category = "dev"
-optional = false
-python-versions = ">=3"
-
-[package.dependencies]
-nodeenv = ">=1.6.0"
-
-[package.extras]
-all = ["twine (>=3.4.1)"]
-dev = ["twine (>=3.4.1)"]
+python-versions = ">=3.6"
+files = [
+    {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"},
+    {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"},
+]
 
 [[package]]
 name = "pytest"
-version = "7.1.2"
+version = "7.4.4"
 description = "pytest: simple powerful testing with Python"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
+files = [
+    {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"},
+    {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"},
+]
 
 [package.dependencies]
-atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
-attrs = ">=19.2.0"
 colorama = {version = "*", markers = "sys_platform == \"win32\""}
 iniconfig = "*"
 packaging = "*"
 pluggy = ">=0.12,<2.0"
-py = ">=1.8.2"
-tomli = ">=1.0.0"
 
 [package.extras]
-testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
+testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
 
 [[package]]
 name = "pytest-asyncio"
 version = "0.18.3"
 description = "Pytest support for asyncio"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
+files = [
+    {file = "pytest-asyncio-0.18.3.tar.gz", hash = "sha256:7659bdb0a9eb9c6e3ef992eef11a2b3e69697800ad02fb06374a210d85b29f91"},
+    {file = "pytest_asyncio-0.18.3-1-py3-none-any.whl", hash = "sha256:16cf40bdf2b4fb7fc8e4b82bd05ce3fbcd454cbf7b92afc445fe299dabb88213"},
+    {file = "pytest_asyncio-0.18.3-py3-none-any.whl", hash = "sha256:8fafa6c52161addfd41ee7ab35f11836c5a16ec208f93ee388f752bea3493a84"},
+]
 
 [package.dependencies]
 pytest = ">=6.1.0"
 
 [package.extras]
-testing = ["coverage (==6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (==0.931)", "pytest-trio (>=0.7.0)"]
-
-[[package]]
-name = "pytest-forked"
-version = "1.4.0"
-description = "run tests in isolated forked subprocesses"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-py = "*"
-pytest = ">=3.10"
-
-[[package]]
-name = "pytest-xdist"
-version = "2.5.0"
-description = "pytest xdist plugin for distributed testing and loop-on-failing modes"
-category = "dev"
-optional = false
-python-versions = ">=3.6"
-
-[package.dependencies]
-execnet = ">=1.1"
-pytest = ">=6.2.0"
-pytest-forked = "*"
-
-[package.extras]
-psutil = ["psutil (>=3.0)"]
-setproctitle = ["setproctitle"]
-testing = ["filelock"]
+testing = ["coverage (==6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (==0.931)", "pytest-trio (>=0.7.0)"]
 
 [[package]]
 name = "pyyaml"
-version = "6.0"
+version = "6.0.1"
 description = "YAML parser and emitter for Python"
-category = "dev"
 optional = false
 python-versions = ">=3.6"
+files = [
+    {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
+    {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
+    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
+    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
+    {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
+    {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
+    {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
+    {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
+    {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
+    {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"},
+    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
+    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
+    {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
+    {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
+    {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
+    {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
+    {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
+    {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
+    {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
+    {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
+    {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
+    {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"},
+    {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"},
+    {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"},
+    {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"},
+    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
+    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
+    {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
+    {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
+    {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
+    {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
+    {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
+    {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"},
+    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
+    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
+    {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
+    {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
+    {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
+    {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
+    {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
+]
 
 [[package]]
 name = "reactivex"
-version = "4.0.2"
+version = "4.0.4"
 description = "ReactiveX (Rx) for Python"
-category = "main"
 optional = false
-python-versions = ">=3.7,<3.11"
+python-versions = ">=3.7,<4.0"
+files = [
+    {file = "reactivex-4.0.4-py3-none-any.whl", hash = "sha256:0004796c420bd9e68aad8e65627d85a8e13f293de76656165dffbcb3a0e3fb6a"},
+    {file = "reactivex-4.0.4.tar.gz", hash = "sha256:e912e6591022ab9176df8348a653fe8c8fa7a301f26f9931c9d8c78a650e04e8"},
+]
 
 [package.dependencies]
 typing-extensions = ">=4.1.1,<5.0.0"
 
 [[package]]
 name = "requests"
-version = "2.28.0"
+version = "2.31.0"
 description = "Python HTTP for Humans."
-category = "dev"
 optional = false
-python-versions = ">=3.7, <4"
+python-versions = ">=3.7"
+files = [
+    {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
+    {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
+]
 
 [package.dependencies]
 certifi = ">=2017.4.17"
-charset-normalizer = ">=2.0.0,<2.1.0"
+charset-normalizer = ">=2,<4"
 idna = ">=2.5,<4"
-urllib3 = ">=1.21.1,<1.27"
+urllib3 = ">=1.21.1,<3"
 
 [package.extras]
 socks = ["PySocks (>=1.5.6,!=1.5.7)"]
-use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
-
-[[package]]
-name = "six"
-version = "1.16.0"
-description = "Python 2 and 3 compatibility utilities"
-category = "dev"
-optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
 
 [[package]]
-name = "toml"
-version = "0.10.2"
-description = "Python Library for Tom's Obvious, Minimal Language"
-category = "dev"
+name = "setuptools"
+version = "69.0.3"
+description = "Easily download, build, install, upgrade, and uninstall Python packages"
 optional = false
-python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+python-versions = ">=3.8"
+files = [
+    {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"},
+    {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"},
+]
 
-[[package]]
-name = "tomli"
-version = "2.0.1"
-description = "A lil' TOML parser"
-category = "dev"
-optional = false
-python-versions = ">=3.7"
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
+testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
 
 [[package]]
 name = "typing-extensions"
-version = "4.2.0"
-description = "Backported and Experimental Type Hints for Python 3.7+"
-category = "main"
+version = "4.9.0"
+description = "Backported and Experimental Type Hints for Python 3.8+"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
+files = [
+    {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"},
+    {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"},
+]
 
 [[package]]
 name = "urllib3"
-version = "1.26.9"
+version = "2.1.0"
 description = "HTTP library with thread-safe connection pooling, file post, and more."
-category = "dev"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
+python-versions = ">=3.8"
+files = [
+    {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"},
+    {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"},
+]
 
 [package.extras]
-brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"]
-secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
-socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
+brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
+socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
+zstd = ["zstandard (>=0.18.0)"]
 
 [[package]]
 name = "virtualenv"
-version = "20.14.1"
+version = "20.25.0"
 description = "Virtual Python Environment builder"
-category = "dev"
 optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+python-versions = ">=3.7"
+files = [
+    {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"},
+    {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"},
+]
 
 [package.dependencies]
-distlib = ">=0.3.1,<1"
-filelock = ">=3.2,<4"
-platformdirs = ">=2,<3"
-six = ">=1.9.0,<2"
+distlib = ">=0.3.7,<1"
+filelock = ">=3.12.2,<4"
+platformdirs = ">=3.9.1,<5"
 
 [package.extras]
-docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"]
-testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"]
+docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
+test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
 
 [metadata]
-lock-version = "1.1"
-python-versions = ">= 3.9, < 3.11"
-content-hash = "31e22b0a63cc844a2381c28d28d973861e54423864c0c0bd2efd2022500a46fa"
-
-[metadata.files]
-atomicwrites = [
-    {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
-    {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
-]
-attrs = [
-    {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
-    {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
-]
-autoflake = [
-    {file = "autoflake-1.4.tar.gz", hash = "sha256:61a353012cff6ab94ca062823d1fb2f692c4acda51c76ff83a8d77915fba51ea"},
-]
-black = [
-    {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"},
-    {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"},
-    {file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"},
-    {file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"},
-    {file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"},
-    {file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"},
-    {file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"},
-    {file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"},
-    {file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"},
-    {file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"},
-    {file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"},
-    {file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"},
-    {file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"},
-    {file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"},
-    {file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"},
-    {file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"},
-    {file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"},
-    {file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"},
-    {file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"},
-    {file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"},
-    {file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"},
-    {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"},
-    {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"},
-]
-certifi = [
-    {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"},
-    {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"},
-]
-cfgv = [
-    {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
-    {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
-]
-charset-normalizer = [
-    {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
-    {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
-]
-click = [
-    {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
-    {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
-]
-colorama = [
-    {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
-    {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
-]
-coverage = [
-    {file = "coverage-6.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f1d5aa2703e1dab4ae6cf416eb0095304f49d004c39e9db1d86f57924f43006b"},
-    {file = "coverage-6.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ce1b258493cbf8aec43e9b50d89982346b98e9ffdfaae8ae5793bc112fb0068"},
-    {file = "coverage-6.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c4e737f60c6936460c5be330d296dd5b48b3963f48634c53b3f7deb0f34ec4"},
-    {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84e65ef149028516c6d64461b95a8dbcfce95cfd5b9eb634320596173332ea84"},
-    {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f69718750eaae75efe506406c490d6fc5a6161d047206cc63ce25527e8a3adad"},
-    {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e57816f8ffe46b1df8f12e1b348f06d164fd5219beba7d9433ba79608ef011cc"},
-    {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:01c5615d13f3dd3aa8543afc069e5319cfa0c7d712f6e04b920431e5c564a749"},
-    {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75ab269400706fab15981fd4bd5080c56bd5cc07c3bccb86aab5e1d5a88dc8f4"},
-    {file = "coverage-6.4.1-cp310-cp310-win32.whl", hash = "sha256:a7f3049243783df2e6cc6deafc49ea123522b59f464831476d3d1448e30d72df"},
-    {file = "coverage-6.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:ee2ddcac99b2d2aec413e36d7a429ae9ebcadf912946b13ffa88e7d4c9b712d6"},
-    {file = "coverage-6.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb73e0011b8793c053bfa85e53129ba5f0250fdc0392c1591fd35d915ec75c46"},
-    {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106c16dfe494de3193ec55cac9640dd039b66e196e4641fa8ac396181578b982"},
-    {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87f4f3df85aa39da00fd3ec4b5abeb7407e82b68c7c5ad181308b0e2526da5d4"},
-    {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:961e2fb0680b4f5ad63234e0bf55dfb90d302740ae9c7ed0120677a94a1590cb"},
-    {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cec3a0f75c8f1031825e19cd86ee787e87cf03e4fd2865c79c057092e69e3a3b"},
-    {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:129cd05ba6f0d08a766d942a9ed4b29283aff7b2cccf5b7ce279d50796860bb3"},
-    {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bf5601c33213d3cb19d17a796f8a14a9eaa5e87629a53979a5981e3e3ae166f6"},
-    {file = "coverage-6.4.1-cp37-cp37m-win32.whl", hash = "sha256:269eaa2c20a13a5bf17558d4dc91a8d078c4fa1872f25303dddcbba3a813085e"},
-    {file = "coverage-6.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f02cbbf8119db68455b9d763f2f8737bb7db7e43720afa07d8eb1604e5c5ae28"},
-    {file = "coverage-6.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ffa9297c3a453fba4717d06df579af42ab9a28022444cae7fa605af4df612d54"},
-    {file = "coverage-6.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:145f296d00441ca703a659e8f3eb48ae39fb083baba2d7ce4482fb2723e050d9"},
-    {file = "coverage-6.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d44996140af8b84284e5e7d398e589574b376fb4de8ccd28d82ad8e3bea13"},
-    {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2bd9a6fc18aab8d2e18f89b7ff91c0f34ff4d5e0ba0b33e989b3cd4194c81fd9"},
-    {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3384f2a3652cef289e38100f2d037956194a837221edd520a7ee5b42d00cc605"},
-    {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9b3e07152b4563722be523e8cd0b209e0d1a373022cfbde395ebb6575bf6790d"},
-    {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1480ff858b4113db2718848d7b2d1b75bc79895a9c22e76a221b9d8d62496428"},
-    {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:865d69ae811a392f4d06bde506d531f6a28a00af36f5c8649684a9e5e4a85c83"},
-    {file = "coverage-6.4.1-cp38-cp38-win32.whl", hash = "sha256:664a47ce62fe4bef9e2d2c430306e1428ecea207ffd68649e3b942fa8ea83b0b"},
-    {file = "coverage-6.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:26dff09fb0d82693ba9e6231248641d60ba606150d02ed45110f9ec26404ed1c"},
-    {file = "coverage-6.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9c80df769f5ec05ad21ea34be7458d1dc51ff1fb4b2219e77fe24edf462d6df"},
-    {file = "coverage-6.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:39ee53946bf009788108b4dd2894bf1349b4e0ca18c2016ffa7d26ce46b8f10d"},
-    {file = "coverage-6.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5b66caa62922531059bc5ac04f836860412f7f88d38a476eda0a6f11d4724f4"},
-    {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd180ed867e289964404051a958f7cccabdeed423f91a899829264bb7974d3d3"},
-    {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84631e81dd053e8a0d4967cedab6db94345f1c36107c71698f746cb2636c63e3"},
-    {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8c08da0bd238f2970230c2a0d28ff0e99961598cb2e810245d7fc5afcf1254e8"},
-    {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d42c549a8f41dc103a8004b9f0c433e2086add8a719da00e246e17cbe4056f72"},
-    {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:309ce4a522ed5fca432af4ebe0f32b21d6d7ccbb0f5fcc99290e71feba67c264"},
-    {file = "coverage-6.4.1-cp39-cp39-win32.whl", hash = "sha256:fdb6f7bd51c2d1714cea40718f6149ad9be6a2ee7d93b19e9f00934c0f2a74d9"},
-    {file = "coverage-6.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:342d4aefd1c3e7f620a13f4fe563154d808b69cccef415415aece4c786665397"},
-    {file = "coverage-6.4.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:4803e7ccf93230accb928f3a68f00ffa80a88213af98ed338a57ad021ef06815"},
-    {file = "coverage-6.4.1.tar.gz", hash = "sha256:4321f075095a096e70aff1d002030ee612b65a205a0a0f5b815280d5dc58100c"},
-]
-coveralls = [
-    {file = "coveralls-3.3.1-py2.py3-none-any.whl", hash = "sha256:f42015f31d386b351d4226389b387ae173207058832fbf5c8ec4b40e27b16026"},
-    {file = "coveralls-3.3.1.tar.gz", hash = "sha256:b32a8bb5d2df585207c119d6c01567b81fba690c9c10a753bfe27a335bfc43ea"},
-]
-distlib = [
-    {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"},
-    {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"},
-]
-docopt = [
-    {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"},
-]
-dunamai = [
-    {file = "dunamai-1.12.0-py3-none-any.whl", hash = "sha256:00b9c1ef58d4950204f76c20f84afe7a28d095f77feaa8512dbb172035415e61"},
-    {file = "dunamai-1.12.0.tar.gz", hash = "sha256:fac4f09e2b8a105bd01f8c50450fea5aa489a6c439c949950a65f0dd388b0d20"},
-]
-execnet = [
-    {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"},
-    {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"},
-]
-expression = [
-    {file = "Expression-3.3.1-py3-none-any.whl", hash = "sha256:bdc4dbb8bd5927ba8795aabe8749ba04c17d45bd2e811034d4859236214fede5"},
-    {file = "Expression-3.3.1.tar.gz", hash = "sha256:dcb36ba1090e4c8711ba5eb60c5800dc15145c1b6a64e71aebfd9b13db7d1be1"},
-]
-filelock = [
-    {file = "filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404"},
-    {file = "filelock-3.7.1.tar.gz", hash = "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04"},
-]
-flake8 = [
-    {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"},
-    {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"},
-]
-identify = [
-    {file = "identify-2.5.1-py2.py3-none-any.whl", hash = "sha256:0dca2ea3e4381c435ef9c33ba100a78a9b40c0bab11189c7cf121f75815efeaa"},
-    {file = "identify-2.5.1.tar.gz", hash = "sha256:3d11b16f3fe19f52039fb7e39c9c884b21cb1b586988114fbe42671f03de3e82"},
-]
-idna = [
-    {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
-    {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
-]
-iniconfig = [
-    {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
-    {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
-]
-isort = [
-    {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
-    {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
-]
-mccabe = [
-    {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
-    {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
-]
-mypy = [
-    {file = "mypy-0.931-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3c5b42d0815e15518b1f0990cff7a705805961613e701db60387e6fb663fe78a"},
-    {file = "mypy-0.931-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c89702cac5b302f0c5d33b172d2b55b5df2bede3344a2fbed99ff96bddb2cf00"},
-    {file = "mypy-0.931-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:300717a07ad09525401a508ef5d105e6b56646f7942eb92715a1c8d610149714"},
-    {file = "mypy-0.931-cp310-cp310-win_amd64.whl", hash = "sha256:7b3f6f557ba4afc7f2ce6d3215d5db279bcf120b3cfd0add20a5d4f4abdae5bc"},
-    {file = "mypy-0.931-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1bf752559797c897cdd2c65f7b60c2b6969ffe458417b8d947b8340cc9cec08d"},
-    {file = "mypy-0.931-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4365c60266b95a3f216a3047f1d8e3f895da6c7402e9e1ddfab96393122cc58d"},
-    {file = "mypy-0.931-cp36-cp36m-win_amd64.whl", hash = "sha256:1b65714dc296a7991000b6ee59a35b3f550e0073411ac9d3202f6516621ba66c"},
-    {file = "mypy-0.931-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e839191b8da5b4e5d805f940537efcaa13ea5dd98418f06dc585d2891d228cf0"},
-    {file = "mypy-0.931-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:50c7346a46dc76a4ed88f3277d4959de8a2bd0a0fa47fa87a4cde36fe247ac05"},
-    {file = "mypy-0.931-cp37-cp37m-win_amd64.whl", hash = "sha256:d8f1ff62f7a879c9fe5917b3f9eb93a79b78aad47b533911b853a757223f72e7"},
-    {file = "mypy-0.931-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9fe20d0872b26c4bba1c1be02c5340de1019530302cf2dcc85c7f9fc3252ae0"},
-    {file = "mypy-0.931-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1b06268df7eb53a8feea99cbfff77a6e2b205e70bf31743e786678ef87ee8069"},
-    {file = "mypy-0.931-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8c11003aaeaf7cc2d0f1bc101c1cc9454ec4cc9cb825aef3cafff8a5fdf4c799"},
-    {file = "mypy-0.931-cp38-cp38-win_amd64.whl", hash = "sha256:d9d2b84b2007cea426e327d2483238f040c49405a6bf4074f605f0156c91a47a"},
-    {file = "mypy-0.931-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ff3bf387c14c805ab1388185dd22d6b210824e164d4bb324b195ff34e322d166"},
-    {file = "mypy-0.931-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b56154f8c09427bae082b32275a21f500b24d93c88d69a5e82f3978018a0266"},
-    {file = "mypy-0.931-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8ca7f8c4b1584d63c9a0f827c37ba7a47226c19a23a753d52e5b5eddb201afcd"},
-    {file = "mypy-0.931-cp39-cp39-win_amd64.whl", hash = "sha256:74f7eccbfd436abe9c352ad9fb65872cc0f1f0a868e9d9c44db0893440f0c697"},
-    {file = "mypy-0.931-py3-none-any.whl", hash = "sha256:1171f2e0859cfff2d366da2c7092b06130f232c636a3f7301e3feb8b41f6377d"},
-    {file = "mypy-0.931.tar.gz", hash = "sha256:0038b21890867793581e4cb0d810829f5fd4441aa75796b53033af3aa30430ce"},
-]
-mypy-extensions = [
-    {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
-    {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
-]
-nodeenv = [
-    {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"},
-    {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"},
-]
-packaging = [
-    {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
-    {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
-]
-pathspec = [
-    {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
-    {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
-]
-platformdirs = [
-    {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
-    {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
-]
-pluggy = [
-    {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
-    {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
-]
-pre-commit = [
-    {file = "pre_commit-2.19.0-py2.py3-none-any.whl", hash = "sha256:10c62741aa5704faea2ad69cb550ca78082efe5697d6f04e5710c3c229afdd10"},
-    {file = "pre_commit-2.19.0.tar.gz", hash = "sha256:4233a1e38621c87d9dda9808c6606d7e7ba0e087cd56d3fe03202a01d2919615"},
-]
-py = [
-    {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
-    {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
-]
-pycodestyle = [
-    {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"},
-    {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"},
-]
-pyflakes = [
-    {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"},
-    {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"},
-]
-pyparsing = [
-    {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
-    {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
-]
-pyright = [
-    {file = "pyright-0.0.13.post0-py3-none-any.whl", hash = "sha256:47c29623f208226f4437bf02276475b816beff6f539ea166e3cade57d6717598"},
-    {file = "pyright-0.0.13.post0.tar.gz", hash = "sha256:dda7602f3692f07fdff8c6d30d704f1b1ed20b5b3bb55c069599988d1ec9bd5a"},
-]
-pytest = [
-    {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"},
-    {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"},
-]
-pytest-asyncio = [
-    {file = "pytest-asyncio-0.18.3.tar.gz", hash = "sha256:7659bdb0a9eb9c6e3ef992eef11a2b3e69697800ad02fb06374a210d85b29f91"},
-    {file = "pytest_asyncio-0.18.3-1-py3-none-any.whl", hash = "sha256:16cf40bdf2b4fb7fc8e4b82bd05ce3fbcd454cbf7b92afc445fe299dabb88213"},
-    {file = "pytest_asyncio-0.18.3-py3-none-any.whl", hash = "sha256:8fafa6c52161addfd41ee7ab35f11836c5a16ec208f93ee388f752bea3493a84"},
-]
-pytest-forked = [
-    {file = "pytest-forked-1.4.0.tar.gz", hash = "sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e"},
-    {file = "pytest_forked-1.4.0-py3-none-any.whl", hash = "sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8"},
-]
-pytest-xdist = [
-    {file = "pytest-xdist-2.5.0.tar.gz", hash = "sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf"},
-    {file = "pytest_xdist-2.5.0-py3-none-any.whl", hash = "sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65"},
-]
-pyyaml = [
-    {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
-    {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
-    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
-    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"},
-    {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
-    {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
-    {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
-    {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
-    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
-    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
-    {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"},
-    {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"},
-    {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"},
-    {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"},
-    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"},
-    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"},
-    {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"},
-    {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"},
-    {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"},
-    {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"},
-    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"},
-    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"},
-    {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"},
-    {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"},
-    {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"},
-    {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"},
-    {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"},
-    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"},
-    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"},
-    {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"},
-    {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"},
-    {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
-    {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
-]
-reactivex = [
-    {file = "reactivex-4.0.2-py3-none-any.whl", hash = "sha256:04d17b55652caf8b6c911f7588b4fe8fa69b6fc48e312e0e3462597fb93bb588"},
-    {file = "reactivex-4.0.2.tar.gz", hash = "sha256:e4db0f7b1646c2198fb7cceade05be7d2e1bd8c0284ae5dffdae449055400310"},
-]
-requests = [
-    {file = "requests-2.28.0-py3-none-any.whl", hash = "sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f"},
-    {file = "requests-2.28.0.tar.gz", hash = "sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b"},
-]
-six = [
-    {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
-    {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
-]
-toml = [
-    {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
-    {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
-]
-tomli = [
-    {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
-    {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
-]
-typing-extensions = [
-    {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"},
-    {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"},
-]
-urllib3 = [
-    {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"},
-    {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"},
-]
-virtualenv = [
-    {file = "virtualenv-20.14.1-py2.py3-none-any.whl", hash = "sha256:e617f16e25b42eb4f6e74096b9c9e37713cf10bf30168fb4a739f3fa8f898a3a"},
-    {file = "virtualenv-20.14.1.tar.gz", hash = "sha256:ef589a79795589aada0c1c5b319486797c03b67ac3984c48c669c0e4f50df3a5"},
-]
+lock-version = "2.0"
+python-versions = ">= 3.11, < 4"
+content-hash = "f58815388ad47a96fe03eb2d6a1fc23f057dd008ec0bade544bee51ac50ce224"
diff --git a/pyproject.toml b/pyproject.toml
index 4db11c0..89893ce 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,67 +1,61 @@
 [tool.poetry]
 name = "aioreactive"
 version = "0.0.0" # NOTE: will be updated by publish script
-description = "sync/await Reactive Tools for Python 3.9+"
+description = "sync/await Reactive Tools for Python 3.11+"
 readme = "README.md"
 authors = ["Dag Brattli <dag@brattli.net>", "Børge Lanes"]
 license = "MIT License"
 repository = "https://github.com/dbrattli/aioreactive"
 classifiers = [
-        "Development Status :: 3 - Alpha",
-        "Environment :: Other Environment",
-        "Intended Audience :: Developers",
-        "License :: OSI Approved :: MIT License",
-        "Operating System :: OS Independent",
-        "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.9",
-        "Programming Language :: Python :: Implementation :: CPython",
-        "Topic :: Software Development :: Libraries :: Python Modules",
-]
-packages = [
-    { include = "aioreactive" },
-    { include = "aioreactive/py.typed" }
+    "Development Status :: 3 - Alpha",
+    "Environment :: Other Environment",
+    "Intended Audience :: Developers",
+    "License :: OSI Approved :: MIT License",
+    "Operating System :: OS Independent",
+    "Programming Language :: Python :: 3",
+    "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: Implementation :: CPython",
+    "Topic :: Software Development :: Libraries :: Python Modules",
 ]
+packages = [{ include = "aioreactive" }, { include = "aioreactive/py.typed" }]
 
 [tool.poetry.dependencies]
-python = ">= 3.9, < 3.11"
+python = ">= 3.11, < 4"
 typing-extensions = "^4.1.1"
-Expression = "^3.0.0"
+Expression = "^5.0.1"
 reactivex = "^4.0.0"
 
 [tool.poetry.dev-dependencies]
 pytest-asyncio = "^0.18.1"
-pytest = "^7.0.1"
+pytest = "^7.4.4"
 coverage = "^6.3.2"
-pytest-xdist = "^2.5.0"
-black = "^22.1.0"
-isort = "^5.10.1"
-pyright = "^0.0.13"
-mypy = "^0.931"
-flake8 = "^4.0.1"
 coveralls = "^3.3.1"
 pre-commit = "^2.17.0"
 autoflake = "^1.4"
 dunamai = "^1.9.0"
 
-[tool.black]
-line-length = 88
-target_version = ['py39']
-include = '\.py$'
+[tool.ruff]
+# Keep in sync with .pre-commit-config.yaml
+line-length = 120
+# D100: Missing docstring in public module
+# D104: Missing docstring in public package
+# D105: Missing docstring in magic method
+ignore = ["D100", "D101", "D102", "D103", "D105", "D107"]
+target-version = "py310"
+select = ["D", "E", "W", "F", "I", "T", "RUF", "TID", "UP"]
+exclude = ["tests", "docs", "examples"]
+
+[tool.ruff.pydocstyle]
+convention = "google"
 
-[tool.isort]
-profile = "black"
-line_length=88                 # corresponds to -w  flag
-multi_line_output=3            # corresponds to -m  flag
-include_trailing_comma=true    # corresponds to -tc flag
-skip_glob = '^((?!py$).)*$'    # isort all Python files
-float_to_top=true
+[tool.ruff.isort]
+lines-after-imports = 2
+known-third-party = ["pytest"]
 
 [tool.pytest.ini_options]
 testpaths = ["tests"]
 asyncio_mode = "strict"
-#addopts = "-n auto"
 
 [build-system]
 requires = ["poetry-core>=1.0.0"]
 build-backend = "poetry.core.masonry.api"
-
diff --git a/pyrightconfig.json b/pyrightconfig.json
index 7d1cd3c..8a07b5b 100644
--- a/pyrightconfig.json
+++ b/pyrightconfig.json
@@ -7,6 +7,6 @@
   ],
   "reportImportCycles": false,
   "reportMissingImports": false,
-  "pythonVersion": "3.9",
+  "pythonVersion": "3.11",
   "typeCheckingMode": "strict"
 }
\ No newline at end of file


Test patch:
diff --git a/aioreactive/testing/__init__.py b/aioreactive/testing/__init__.py
index 938ff43..abda9d2 100644
--- a/aioreactive/testing/__init__.py
+++ b/aioreactive/testing/__init__.py
@@ -7,6 +7,7 @@
 from .utils import ca
 from .virtual_events import VirtualTimeEventLoop
 
+
 __all__ = [
     "ca",
     "VirtualTimeEventLoop",
diff --git a/aioreactive/testing/observer.py b/aioreactive/testing/observer.py
index dc3eed0..43ac07a 100644
--- a/aioreactive/testing/observer.py
+++ b/aioreactive/testing/observer.py
@@ -1,10 +1,12 @@
 import logging
-from typing import Awaitable, Callable, List, Tuple, TypeVar
+from collections.abc import Awaitable, Callable
+from typing import TypeVar
 
 from aioreactive import AsyncAwaitableObserver
 from aioreactive.notification import Notification, OnCompleted, OnError, OnNext
 from aioreactive.utils import anoop
 
+
 log = logging.getLogger(__name__)
 
 
@@ -33,7 +35,7 @@ def __init__(
     ) -> None:
         super().__init__(asend, athrow, aclose)
 
-        self._values: List[Tuple[float, Notification[TSource]]] = []
+        self._values: list[tuple[float, Notification[TSource]]] = []
 
         self._send = asend
         self._throw = athrow
@@ -62,11 +64,11 @@ async def aclose(self) -> None:
         log.debug("AsyncAnonymousObserver:aclose()")
 
         time = self.time()
-        self._values.append((time, OnCompleted))
+        self._values.append((time, OnCompleted()))
 
         await self._close()
         await super().aclose()
 
     @property
-    def values(self) -> List[Tuple[float, Notification[TSource]]]:
+    def values(self) -> list[tuple[float, Notification[TSource]]]:
         return self._values
diff --git a/aioreactive/testing/subject.py b/aioreactive/testing/subject.py
index 49a53e8..b5d8d3b 100644
--- a/aioreactive/testing/subject.py
+++ b/aioreactive/testing/subject.py
@@ -1,9 +1,11 @@
 import asyncio
-from typing import TypeVar
+from asyncio import Task
+from typing import Any, TypeVar
 
 from aioreactive import AsyncObserver
 from aioreactive.subject import AsyncSingleSubject, AsyncSubject
 
+
 TSource = TypeVar("TSource")
 
 
@@ -16,13 +18,20 @@ class AsyncSubjectBase(AsyncObserver[TSource]):
 
     def __init__(self) -> None:
         self._loop = asyncio.get_event_loop()
+        self._running_tasks: set[Task[Any]] = set()
 
     async def asend_at(self, when: float, value: TSource) -> None:
-        async def task() -> None:
+        task: Task[Any] | None = None
+
+        async def worker() -> None:
             await self.asend(value)
+            if task:
+                self._running_tasks.remove(task)
 
         def callback() -> None:
-            asyncio.ensure_future(task())
+            nonlocal task
+            task = asyncio.ensure_future(worker())
+            self._running_tasks.add(task)
 
         self._loop.call_at(when, callback)
 
@@ -31,18 +40,29 @@ async def asend_later(self, delay: float, value: TSource) -> None:
         await self.asend(value)
 
     async def asend_later_scheduled(self, delay: float, value: TSource) -> None:
-        async def task() -> None:
+        task: Task[Any] | None = None
+
+        async def worker() -> None:
             await asyncio.sleep(delay)
             await self.asend(value)
+            if task:
+                self._running_tasks.remove(task)
 
-        asyncio.ensure_future(task())
+        task = asyncio.ensure_future(worker())
+        self._running_tasks.add(task)
 
     async def athrow_at(self, when: float, err: Exception) -> None:
-        async def task() -> None:
+        task: Task[Any] | None = None
+
+        async def worker() -> None:
             await self.athrow(err)
+            if task:
+                self._running_tasks.remove(task)
 
         def callback() -> None:
-            asyncio.ensure_future(task())
+            nonlocal task
+            task = asyncio.ensure_future(worker())
+            self._running_tasks.add(task)
 
         self._loop.call_at(when, callback)
 
@@ -51,18 +71,29 @@ async def athrow_later(self, delay: float, err: Exception) -> None:
         await self.athrow(err)
 
     async def athrow_later_scheduled(self, delay: float, err: Exception) -> None:
-        async def task() -> None:
+        task: Task[Any] | None = None
+
+        async def worker() -> None:
+            nonlocal task
             await asyncio.sleep(delay)
             await self.athrow(err)
+            if task:
+                self._running_tasks.remove(task)
 
-        asyncio.ensure_future(task())
+        task = asyncio.ensure_future(worker())
+        self._running_tasks.add(task)
 
     async def aclose_at(self, when: float) -> None:
-        async def task() -> None:
+        task: Task[Any] | None = None
+
+        async def worker() -> None:
             await self.aclose()
+            if task:
+                self._running_tasks.remove(task)
 
         def callback() -> None:
-            asyncio.ensure_future(task())
+            task = asyncio.ensure_future(worker())
+            self._running_tasks.add(task)
 
         self._loop.call_at(when, callback)
 
@@ -71,11 +102,16 @@ async def aclose_later(self, delay: float) -> None:
         await self.aclose()
 
     async def close_later_scheduled(self, delay: float) -> None:
-        async def task() -> None:
+        task: Task[Any] | None = None
+
+        async def worker() -> None:
             await asyncio.sleep(delay)
             await self.aclose()
+            if task:
+                self._running_tasks.remove(task)
 
-        asyncio.ensure_future(task())
+        task = asyncio.ensure_future(worker())
+        self._running_tasks.add(task)
 
 
 class AsyncTestSubject(AsyncSubject[TSource], AsyncSubjectBase[TSource]):
diff --git a/aioreactive/testing/virtual_events.py b/aioreactive/testing/virtual_events.py
index a9e350f..4aeccc2 100644
--- a/aioreactive/testing/virtual_events.py
+++ b/aioreactive/testing/virtual_events.py
@@ -6,6 +6,7 @@
 from asyncio import tasks
 from asyncio.log import logger
 
+
 log = logging.getLogger(__name__)
 
 __all__ = "VirtualTimeEventLoop"
@@ -52,18 +53,18 @@ def time(self):
         return self._time
 
     def _run_once(self):
-        """Run one full iteration of the event loop. This calls all
-        currently ready callbacks, polls for I/O, schedules the
-        resulting callbacks, and finally schedules 'call_later'
-        callbacks.
+        """Run one full iteration of the event loop.
+
+        This calls all currently ready callbacks, polls for I/O,
+        schedules the resulting callbacks, and finally schedules
+        'call_later' callbacks.
         """
         # log.debug("run_once()")
 
         sched_count = len(self._scheduled)
         if (
             sched_count > _MIN_SCHEDULED_TIMER_HANDLES
-            and self._timer_cancelled_count / sched_count
-            > _MIN_CANCELLED_TIMER_HANDLES_FRACTION
+            and self._timer_cancelled_count / sched_count > _MIN_CANCELLED_TIMER_HANDLES_FRACTION
         ):
             # Remove delayed calls that were cancelled if their number
             # is too high
@@ -114,9 +115,7 @@ def _run_once(self):
                     handle._run()
                     dt = self.time() - t0
                     if dt >= self.slow_callback_duration:
-                        logger.warning(
-                            "Executing %s took %.3f seconds", _format_handle(handle), dt
-                        )
+                        logger.warning("Executing %s took %.3f seconds", _format_handle(handle), dt)
                 finally:
                     self._current_handle = None
             else:
diff --git a/tests/test_async_iteration.py b/tests/test_async_iteration.py
index ee1a526..b351d95 100644
--- a/tests/test_async_iteration.py
+++ b/tests/test_async_iteration.py
@@ -19,7 +19,7 @@ def event_loop():
 @pytest.mark.asyncio
 async def test_async_iteration() -> None:
     xs = rx.from_iterable([1, 2, 3])
-    result = []
+    result: list[int] = []
 
     async for x in rx.to_async_iterable(xs):
         result.append(x)
@@ -39,7 +39,7 @@ async def test_async_comprehension() -> None:
 @pytest.mark.asyncio
 async def test_async_iteration_aync_with() -> None:
     xs = rx.from_iterable([1, 2, 3])
-    result = []
+    result: list[int] = []
 
     obv = rx.AsyncIteratorObserver(xs)
     async for x in obv:
@@ -55,7 +55,7 @@ async def test_async_iteration_inception() -> None:
     obv = rx.AsyncIteratorObserver(xs)
 
     ys = rx.from_async_iterable(obv)
-    result = []
+    result: list[int] = []
 
     async for y in rx.to_async_iterable(ys):
         result.append(y)
diff --git a/tests/test_chain.py b/tests/test_chain.py
index 61cc4db..1451cf5 100644
--- a/tests/test_chain.py
+++ b/tests/test_chain.py
@@ -34,7 +34,7 @@ def mapper(value: int) -> int:
         (0, OnNext(10)),
         (0, OnNext(20)),
         (0, OnNext(30)),
-        (0, OnCompleted),
+        (0, OnCompleted()),
     ]
 
 
@@ -58,7 +58,7 @@ async def predicate(value: int) -> bool:
     assert obv.values == [
         (ca(0.2), OnNext(20)),
         (ca(0.3), OnNext(30)),
-        (ca(0.3), OnCompleted),
+        (ca(0.3), OnCompleted()),
     ]
 
 
@@ -84,5 +84,5 @@ async def long_running(value: int) -> AsyncObservable[int]:
     assert obv.values == [
         (ca(0.2), OnNext(20)),
         (ca(0.3), OnNext(30)),
-        (ca(0.3), OnCompleted),
+        (ca(0.3), OnCompleted()),
     ]
diff --git a/tests/test_debounce.py b/tests/test_debounce.py
index f4344c9..b75a51b 100644
--- a/tests/test_debounce.py
+++ b/tests/test_debounce.py
@@ -44,7 +44,7 @@ async def test_debounce():
     assert obv.values == [
         (ca(0.5), OnNext(1)),
         (ca(1.1), OnNext(2)),
-        (ca(1.2), OnCompleted),
+        (ca(1.2), OnCompleted()),
     ]
 
     await subscription.dispose_async()
@@ -68,7 +68,7 @@ async def test_debounce_filter():
 
     assert obv.values == [
         (ca(0.8), OnNext(2)),
-        (ca(0.9), OnCompleted),
+        (ca(0.9), OnCompleted()),
     ]
 
     await subscription.dispose_async()
diff --git a/tests/test_delay.py b/tests/test_delay.py
index 5dd4738..63ab45d 100644
--- a/tests/test_delay.py
+++ b/tests/test_delay.py
@@ -39,7 +39,7 @@ async def test_delay_done():
     assert obv.values == [
         (ca(1), OnNext(10)),
         (ca(2), OnNext(20)),
-        (ca(3), OnCompleted),
+        (ca(3), OnCompleted()),
     ]
 
 
diff --git a/tests/test_distinct_until_changed.py b/tests/test_distinct_until_changed.py
index b89404a..85eaa86 100644
--- a/tests/test_distinct_until_changed.py
+++ b/tests/test_distinct_until_changed.py
@@ -30,7 +30,7 @@ async def test_distinct_until_changed_different():
         (0, OnNext(1)),
         (0, OnNext(2)),
         (0, OnNext(3)),
-        (0, OnCompleted),
+        (0, OnCompleted()),
     ]
 
 
@@ -49,5 +49,5 @@ async def test_distinct_until_changed_changed():
         (0, OnNext(3)),
         (0, OnNext(1)),
         (0, OnNext(2)),
-        (0, OnCompleted),
+        (0, OnCompleted()),
     ]
diff --git a/tests/test_flat_map.py b/tests/test_flat_map.py
index 1f93cfd..1251bef 100644
--- a/tests/test_flat_map.py
+++ b/tests/test_flat_map.py
@@ -31,7 +31,7 @@ def mapper(value: int) -> rx.AsyncObservable[int]:
     await xs.aclose()
     await obv
 
-    assert obv.values == [(0, OnNext(10)), (0, OnNext(20)), (0, OnCompleted)]
+    assert obv.values == [(0, OnNext(10)), (0, OnNext(20)), (0, OnCompleted())]
 
 
 @pytest.mark.asyncio
diff --git a/tests/test_forward_pipe.py b/tests/test_forward_pipe.py
index 6869a15..e82fe58 100644
--- a/tests/test_forward_pipe.py
+++ b/tests/test_forward_pipe.py
@@ -34,7 +34,7 @@ def mapper(value: int) -> int:
         (0, OnNext(10)),
         (0, OnNext(20)),
         (0, OnNext(30)),
-        (0, OnCompleted),
+        (0, OnCompleted()),
     ]
 
 
@@ -57,7 +57,7 @@ async def predicate(value: int) -> bool:
 
     obv: AsyncTestObserver[int] = AsyncTestObserver()
     await rx.run(ys, obv)
-    assert obv.values == [(0, OnNext(20)), (0, OnNext(30)), (0, OnCompleted)]
+    assert obv.values == [(0, OnNext(20)), (0, OnNext(30)), (0, OnCompleted())]
 
 
 @pytest.mark.asyncio
diff --git a/tests/test_from_iterable.py b/tests/test_from_iterable.py
index 86d6450..cd32ae2 100644
--- a/tests/test_from_iterable.py
+++ b/tests/test_from_iterable.py
@@ -22,7 +22,7 @@ async def test_from_iterable_happy():
         (0, OnNext(1)),
         (0, OnNext(2)),
         (0, OnNext(3)),
-        (0, OnCompleted),
+        (0, OnCompleted()),
     ]
 
 
@@ -62,4 +62,4 @@ async def asend(value: int) -> None:
 #     # with pytest.raises(asyncio.CancelledError):
 #     await obv
 
-#     assert obv.values == [(0, OnNext(0)), (0, OnCompleted)]
+#     assert obv.values == [(0, OnNext(0)), (0, OnCompleted())]
diff --git a/tests/test_map.py b/tests/test_map.py
index 90aaa92..d5cf1d0 100644
--- a/tests/test_map.py
+++ b/tests/test_map.py
@@ -20,7 +20,7 @@ def event_loop():
 @pytest.mark.asyncio
 async def test_map_works():
     xs: AsyncObservable[int] = rx.from_iterable([1, 2, 3])
-    values = []
+    values: list[int] = []
 
     async def asend(value: int) -> None:
         values.append(value)
@@ -69,7 +69,7 @@ def mapper(x: int):
 async def test_map_subscription_cancel():
     xs: rx.AsyncSubject[int] = rx.AsyncSubject()
     sub: Optional[AsyncDisposable] = None
-    result = []
+    result: list[int] = []
 
     def mapper(value: int) -> int:
         return value * 10
@@ -93,7 +93,7 @@ async def asend(value: int) -> None:
 @pytest.mark.asyncio
 async def test_mapi_works():
     xs: AsyncObservable[int] = rx.from_iterable([1, 2, 3])
-    values = []
+    values: list[int] = []
 
     async def asend(value: int) -> None:
         values.append(value)
diff --git a/tests/test_merge.py b/tests/test_merge.py
index 6dbb994..e782ec1 100644
--- a/tests/test_merge.py
+++ b/tests/test_merge.py
@@ -35,7 +35,7 @@ async def test_merge_done():
     await xs.aclose()
     await obv
 
-    assert obv.values == [(0, OnNext(10)), (0, OnNext(20)), (0, OnCompleted)]
+    assert obv.values == [(0, OnNext(10)), (0, OnNext(20)), (0, OnCompleted())]
 
 
 @pytest.mark.asyncio
@@ -71,7 +71,7 @@ async def test_merge_streams():
         (3, OnNext(50)),
         (4, OnNext(30)),
         (5, OnNext(60)),
-        (6, OnCompleted),
+        (6, OnCompleted()),
     ]
 
 
@@ -101,5 +101,5 @@ async def test_merge_streams_concat():
         (6, OnNext(1)),
         (6, OnNext(2)),
         (6, OnNext(3)),
-        (6, OnCompleted),
+        (6, OnCompleted()),
     ]
diff --git a/tests/test_pipe.py b/tests/test_pipe.py
index 209b5e0..5c953c9 100644
--- a/tests/test_pipe.py
+++ b/tests/test_pipe.py
@@ -37,7 +37,7 @@ def mapper(value: int) -> int:
         (0, OnNext(10)),
         (0, OnNext(20)),
         (0, OnNext(30)),
-        (0, OnCompleted),
+        (0, OnCompleted()),
     ]
 
 
@@ -59,7 +59,7 @@ async def predicate(value: int) -> bool:
     assert obv.values == [
         (approx(0.2), OnNext(20)),
         (approx(0.3), OnNext(30)),
-        (approx(0.3), OnCompleted),
+        (approx(0.3), OnCompleted()),
     ]
 
 
diff --git a/tests/test_scan.py b/tests/test_scan.py
index 888b062..0ed251f 100644
--- a/tests/test_scan.py
+++ b/tests/test_scan.py
@@ -50,7 +50,7 @@ async def test_scan():
         OnNext(3),
         OnNext(6),
         OnNext(10),
-        OnCompleted,
+        OnCompleted(),
     ]
 
 
@@ -72,5 +72,5 @@ async def test_scan_async():
         OnNext(3),
         OnNext(6),
         OnNext(10),
-        OnCompleted,
+        OnCompleted(),
     ]
diff --git a/tests/test_single.py b/tests/test_single.py
index a65c96c..e387a60 100644
--- a/tests/test_single.py
+++ b/tests/test_single.py
@@ -27,7 +27,7 @@ async def test_unit_happy():
 
     obv: AsyncTestObserver[int] = AsyncTestObserver()
     await rx.run(xs, obv)
-    assert obv.values == [(0, OnNext(42)), (0, OnCompleted)]
+    assert obv.values == [(0, OnNext(42)), (0, OnCompleted())]
 
 
 @pytest.mark.asyncio
@@ -65,7 +65,7 @@ async def asend(value: int) -> None:
 
     assert obv.values == [
         (0, OnNext(42)),
-        (0, OnCompleted),
+        (0, OnCompleted()),
     ]
 
 
@@ -79,7 +79,7 @@ async def test_unit_happy_resolved_future():
     await rx.run(xs, obv)
     assert obv.values == [
         (0, OnNext(42)),
-        (0, OnCompleted),
+        (0, OnCompleted()),
     ]
 
 
@@ -95,7 +95,7 @@ async def test_unit_happy_future_resolve():
 
     assert obv.values == [
         (0, OnNext(42)),
-        (0, OnCompleted),
+        (0, OnCompleted()),
     ]
 
 
@@ -125,4 +125,4 @@ async def test_unit_future_cancel():
         with pytest.raises(asyncio.CancelledError):
             await obv
 
-    assert obv.values == [(approx(1), OnCompleted)]
+    assert obv.values == [(approx(1), OnCompleted())]
diff --git a/tests/test_single_stream.py b/tests/test_single_stream.py
index 77b9e17..3c71a7b 100644
--- a/tests/test_single_stream.py
+++ b/tests/test_single_stream.py
@@ -82,7 +82,7 @@ async def test_stream_send_after_close():
         (1, OnNext(10)),
         (2, OnNext(20)),
         (3, OnNext(30)),
-        (5, OnCompleted),
+        (5, OnCompleted()),
     ]
 
 
@@ -222,4 +222,4 @@ async def aclose():
     async with await xs.subscribe_async(sink):
         await xs.asend_later(1, 20)
 
-    assert sink.values == [(10, OnCompleted)]
+    assert sink.values == [(10, OnCompleted())]
diff --git a/tests/test_slice.py b/tests/test_slice.py
index f5ce4da..c19ee66 100644
--- a/tests/test_slice.py
+++ b/tests/test_slice.py
@@ -32,7 +32,7 @@ async def test_slice_special():
         (0, OnNext(2)),
         (0, OnNext(3)),
         (0, OnNext(4)),
-        (0, OnCompleted),
+        (0, OnCompleted()),
     ]
 
 
@@ -50,5 +50,5 @@ async def test_slice_step():
         (0, OnNext(1)),
         (0, OnNext(3)),
         (0, OnNext(5)),
-        (0, OnCompleted),
+        (0, OnCompleted()),
     ]
diff --git a/tests/test_stream.py b/tests/test_stream.py
index b1e15ce..778ddc3 100644
--- a/tests/test_stream.py
+++ b/tests/test_stream.py
@@ -84,7 +84,7 @@ async def test_stream_send_after_close() -> None:
         (1, OnNext(10)),
         (2, OnNext(20)),
         (3, OnNext(30)),
-        (5, OnCompleted),
+        (5, OnCompleted()),
     ]
 
 
diff --git a/tests/test_take.py b/tests/test_take.py
index 5b3258a..dc2b6a5 100644
--- a/tests/test_take.py
+++ b/tests/test_take.py
@@ -30,7 +30,7 @@ async def test_take_zero() -> None:
     with pytest.raises(CancelledError):
         await rx.run(ys, obv)
 
-    assert obv.values == [(0, OnCompleted)]
+    assert obv.values == [(0, OnCompleted())]
 
 
 @pytest.mark.asyncio
@@ -43,7 +43,7 @@ async def test_take_empty() -> None:
     with pytest.raises(CancelledError):
         await rx.run(ys, obv)
 
-    assert obv.values == [(0, OnCompleted)]
+    assert obv.values == [(0, OnCompleted())]
 
 
 @pytest.mark.asyncio
@@ -64,4 +64,4 @@ async def test_take_normal() -> None:
     result = await rx.run(ys, obv)
 
     assert result == 2
-    assert obv.values == [(0, OnNext(1)), (0, OnNext(2)), (0, OnCompleted)]
+    assert obv.values == [(0, OnNext(1)), (0, OnNext(2)), (0, OnCompleted())]
diff --git a/tests/test_with_latest_from.py b/tests/test_with_latest_from.py
index e687e44..3e39850 100644
--- a/tests/test_with_latest_from.py
+++ b/tests/test_with_latest_from.py
@@ -52,7 +52,7 @@ async def test_withlatestfrom_never_never():
 #     with pytest.raises(CancelledError):
 #         await rx.run(zs, obv)
 
-#     assert obv.values == [(0, OnCompleted)]
+#     assert obv.values == [(0, OnCompleted())]
 
 
 # @pytest.mark.asyncio
@@ -70,4 +70,4 @@ async def test_withlatestfrom_never_never():
 #         await xs.aclose()
 #         await obv
 
-#     assert obv.values == [(0, OnNext(5)), (0, OnCompleted)]
+#     assert obv.values == [(0, OnNext(5)), (0, OnCompleted())]