sain.boxed API documentation

# sain.boxed

a `Box` is a wrapper around a value that expires after the given amount of time.

View Source

      1# BSD 3-Clause License
      2#
      3# Copyright (c) 2022-Present, nxtlo
      4# All rights reserved.
      5#
      6# Redistribution and use in source and binary forms, with or without
      7# modification, are permitted provided that the following conditions are met:
      8#
      9# * Redistributions of source code must retain the above copyright notice, this
     10#   list of conditions and the following disclaimer.
     11#
     12# * Redistributions in binary form must reproduce the above copyright notice,
     13#   this list of conditions and the following disclaimer in the documentation
     14#   and/or other materials provided with the distribution.
     15#
     16# * Neither the name of the copyright holder nor the names of its
     17#   contributors may be used to endorse or promote products derived from
     18#   this software without specific prior written permission.
     19#
     20# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
     21# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
     22# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
     23# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
     24# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
     25# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
     26# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
     27# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
     28# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     29# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     30
     31
     32"""a `Box` is a wrapper around a value that expires after the given amount of time."""
     33
     34from __future__ import annotations
     35
     36__all__ = ("Box",)
     37
     38import asyncio
     39import datetime
     40import math
     41import time
     42import typing
     43import warnings
     44
     45from sain.macros import ub_checks
     46
     47from . import futures
     48from . import option
     49
     50if typing.TYPE_CHECKING:
     51    from collections import abc as collections
     52
     53    from typing_extensions import Self
     54
     55    from . import Option
     56
     57T = typing.TypeVar("T", covariant=True)
     58
     59
     60@typing.final
     61class Box(typing.Generic[T]):
     62    """The box object for expiring data. not thread-safe.
     63
     64    A box is an object that contains a value of type `T` which expires it after the given amount of time,
     65    The box won't start expiring the data until its first access with `Box.get` method.
     66
     67    Example
     68    -------
     69    ```py
     70    # Initializing a box doesn't mean it started expiring. instead,
     71    # getting the value the first time will start the process.
     72    cache: dict[str, Box[int]] = {"sora": Box(999, timedelta(seconds=5)}
     73
     74    # first start expiring here.
     75    cache["sora"].get().unwrap()
     76    time.sleep(6)
     77    assert cache["sora"].has_expired()
     78    ```
     79    """
     80
     81    __slots__ = ("_inner", "_expire_in", "_on_expire", "_mono")
     82
     83    def __init__(self, value: T, expire_in: int | float | datetime.timedelta) -> None:
     84        if isinstance(expire_in, datetime.timedelta):
     85            expire_in = expire_in.total_seconds()
     86        else:
     87            expire_in = float(expire_in)
     88
     89        if expire_in <= 0:
     90            raise ValueError("expire_in must be more than 0 seconds.")
     91
     92        # We set the last call on the first access to the value.
     93        self._mono: float | None = None
     94        self._inner: Option[T] = option.Some(value)
     95        self._on_expire: collections.Callable[[T], typing.Any] | None = None
     96        self._expire_in = expire_in
     97
     98    @property
     99    def has_expired(self) -> bool:
    100        """Returns True if the value has expired."""
    101        # return self._mono is not None and not self._expire_in <= (
    102        # time.monotonic() - self._mono
    103        # )
    104        return self._mono is not None and (
    105            not self._mono or self._expire_in <= (time.monotonic() - self._mono)
    106        )
    107
    108    def on_expire(self, callback: collections.Callable[[T], typing.Any]) -> Self:
    109        """Set a callback that will be invoked when this value gets expired.
    110
    111        Both async and sync callbacks are supported.
    112
    113        Example
    114        -------
    115        ```py
    116        async def sink(message: str) -> None:
    117            await client.create_message(message)
    118            print("Sinked", message)
    119
    120        box = Box("bluh", 5).on_expire(sink)
    121
    122        while box.get().is_some():
    123            time.sleep(5)
    124        ```
    125        First `.get` call on an expired box, the `sink` callback will be invoked,
    126        also the inner value will be set to `Some(None)`.
    127
    128        After 5 seconds.
    129        ```py
    130        assert box.get() == Some("bluh") # This last call invokes the callback.
    131        # Sinked bluh
    132        assert box.get().is_none()
    133        ```
    134        """
    135        self._on_expire = callback
    136        return self
    137
    138    def remaining(self) -> float:
    139        """Returns when this box will expire in seconds.
    140
    141        Example
    142        --------
    143        ```py
    144        jogo = Box("jogo", 3)
    145        assert jogo.get().unwrap() == "jogo"
    146
    147        time.sleep(1)
    148        assert jogo.remaining() == 2
    149        ```
    150        """
    151        if not self._mono:
    152            return 0.0
    153
    154        return math.floor(
    155            (self._expire_in - (time.monotonic() - self._mono) + 1) * 0.99
    156        )
    157
    158    def get(self) -> Option[T]:
    159        """Get the contained value if it was not expired, otherwise `Some(None)` is returned.
    160
    161        Example
    162        -------
    163        ```py
    164        pizza = Box("pizza", timedelta(days=1))
    165
    166        while not pizza.get().is_none():
    167            # Do stuff with the value while its not expired.
    168
    169        # After 1 day.
    170        assert pizza.get().is_none()
    171        ```
    172        """
    173        if self.has_expired:
    174            if self._on_expire is not None:
    175                with warnings.catch_warnings():
    176                    # ignore the warnings from `unwrap_unchecked`.
    177                    warnings.simplefilter("ignore", category=ub_checks)
    178                    try:
    179                        if asyncio.iscoroutinefunction(self._on_expire):
    180                            futures.loop().run_until_complete(
    181                                self._on_expire(self._inner.unwrap_unchecked())
    182                            )
    183                        else:
    184                            self._on_expire(self._inner.unwrap_unchecked())
    185                    finally:
    186                        self._on_expire = None
    187
    188            self._inner = option.NOTHING
    189            self._mono = None
    190            # SAFETY: The value is expired, therefore we always return None.
    191            return option.NOTHING
    192
    193        if self._mono is None:
    194            self._mono = time.monotonic()
    195
    196        return self._inner
    197
    198    def __repr__(self) -> str:
    199        return f"Box(value: {self._inner}, expired: {self.has_expired})"
    200
    201    __str__ = __repr__
    202
    203    def __hash__(self) -> int:
    204        return hash(self._inner)
    205
    206    def __bool__(self) -> bool:
    207        return not self.has_expired

@typing.final
class Box(typing.Generic[+T]): View Source

     61@typing.final
     62class Box(typing.Generic[T]):
     63    """The box object for expiring data. not thread-safe.
     64
     65    A box is an object that contains a value of type `T` which expires it after the given amount of time,
     66    The box won't start expiring the data until its first access with `Box.get` method.
     67
     68    Example
     69    -------
     70    ```py
     71    # Initializing a box doesn't mean it started expiring. instead,
     72    # getting the value the first time will start the process.
     73    cache: dict[str, Box[int]] = {"sora": Box(999, timedelta(seconds=5)}
     74
     75    # first start expiring here.
     76    cache["sora"].get().unwrap()
     77    time.sleep(6)
     78    assert cache["sora"].has_expired()
     79    ```
     80    """
     81
     82    __slots__ = ("_inner", "_expire_in", "_on_expire", "_mono")
     83
     84    def __init__(self, value: T, expire_in: int | float | datetime.timedelta) -> None:
     85        if isinstance(expire_in, datetime.timedelta):
     86            expire_in = expire_in.total_seconds()
     87        else:
     88            expire_in = float(expire_in)
     89
     90        if expire_in <= 0:
     91            raise ValueError("expire_in must be more than 0 seconds.")
     92
     93        # We set the last call on the first access to the value.
     94        self._mono: float | None = None
     95        self._inner: Option[T] = option.Some(value)
     96        self._on_expire: collections.Callable[[T], typing.Any] | None = None
     97        self._expire_in = expire_in
     98
     99    @property
    100    def has_expired(self) -> bool:
    101        """Returns True if the value has expired."""
    102        # return self._mono is not None and not self._expire_in <= (
    103        # time.monotonic() - self._mono
    104        # )
    105        return self._mono is not None and (
    106            not self._mono or self._expire_in <= (time.monotonic() - self._mono)
    107        )
    108
    109    def on_expire(self, callback: collections.Callable[[T], typing.Any]) -> Self:
    110        """Set a callback that will be invoked when this value gets expired.
    111
    112        Both async and sync callbacks are supported.
    113
    114        Example
    115        -------
    116        ```py
    117        async def sink(message: str) -> None:
    118            await client.create_message(message)
    119            print("Sinked", message)
    120
    121        box = Box("bluh", 5).on_expire(sink)
    122
    123        while box.get().is_some():
    124            time.sleep(5)
    125        ```
    126        First `.get` call on an expired box, the `sink` callback will be invoked,
    127        also the inner value will be set to `Some(None)`.
    128
    129        After 5 seconds.
    130        ```py
    131        assert box.get() == Some("bluh") # This last call invokes the callback.
    132        # Sinked bluh
    133        assert box.get().is_none()
    134        ```
    135        """
    136        self._on_expire = callback
    137        return self
    138
    139    def remaining(self) -> float:
    140        """Returns when this box will expire in seconds.
    141
    142        Example
    143        --------
    144        ```py
    145        jogo = Box("jogo", 3)
    146        assert jogo.get().unwrap() == "jogo"
    147
    148        time.sleep(1)
    149        assert jogo.remaining() == 2
    150        ```
    151        """
    152        if not self._mono:
    153            return 0.0
    154
    155        return math.floor(
    156            (self._expire_in - (time.monotonic() - self._mono) + 1) * 0.99
    157        )
    158
    159    def get(self) -> Option[T]:
    160        """Get the contained value if it was not expired, otherwise `Some(None)` is returned.
    161
    162        Example
    163        -------
    164        ```py
    165        pizza = Box("pizza", timedelta(days=1))
    166
    167        while not pizza.get().is_none():
    168            # Do stuff with the value while its not expired.
    169
    170        # After 1 day.
    171        assert pizza.get().is_none()
    172        ```
    173        """
    174        if self.has_expired:
    175            if self._on_expire is not None:
    176                with warnings.catch_warnings():
    177                    # ignore the warnings from `unwrap_unchecked`.
    178                    warnings.simplefilter("ignore", category=ub_checks)
    179                    try:
    180                        if asyncio.iscoroutinefunction(self._on_expire):
    181                            futures.loop().run_until_complete(
    182                                self._on_expire(self._inner.unwrap_unchecked())
    183                            )
    184                        else:
    185                            self._on_expire(self._inner.unwrap_unchecked())
    186                    finally:
    187                        self._on_expire = None
    188
    189            self._inner = option.NOTHING
    190            self._mono = None
    191            # SAFETY: The value is expired, therefore we always return None.
    192            return option.NOTHING
    193
    194        if self._mono is None:
    195            self._mono = time.monotonic()
    196
    197        return self._inner
    198
    199    def __repr__(self) -> str:
    200        return f"Box(value: {self._inner}, expired: {self.has_expired})"
    201
    202    __str__ = __repr__
    203
    204    def __hash__(self) -> int:
    205        return hash(self._inner)
    206
    207    def __bool__(self) -> bool:
    208        return not self.has_expired

The box object for expiring data. not thread-safe.

A box is an object that contains a value of type `T` which expires it after the given amount of time, The box won't start expiring the data until its first access with `Box.get` method.

###### Example

    # Initializing a box doesn't mean it started expiring. instead,
    # getting the value the first time will start the process.
    cache: dict[str, Box[int]] = {"sora": Box(999, timedelta(seconds=5)}

    # first start expiring here.
    cache["sora"].get().unwrap()
    time.sleep(6)
    assert cache["sora"].has_expired()

Box(value: +T, expire_in: int | float | datetime.timedelta) View Source

    84    def __init__(self, value: T, expire_in: int | float | datetime.timedelta) -> None:
    85        if isinstance(expire_in, datetime.timedelta):
    86            expire_in = expire_in.total_seconds()
    87        else:
    88            expire_in = float(expire_in)
    89
    90        if expire_in <= 0:
    91            raise ValueError("expire_in must be more than 0 seconds.")
    92
    93        # We set the last call on the first access to the value.
    94        self._mono: float | None = None
    95        self._inner: Option[T] = option.Some(value)
    96        self._on_expire: collections.Callable[[T], typing.Any] | None = None
    97        self._expire_in = expire_in

has_expired: bool View Source

     99    @property
    100    def has_expired(self) -> bool:
    101        """Returns True if the value has expired."""
    102        # return self._mono is not None and not self._expire_in <= (
    103        # time.monotonic() - self._mono
    104        # )
    105        return self._mono is not None and (
    106            not self._mono or self._expire_in <= (time.monotonic() - self._mono)
    107        )

Returns True if the value has expired.

def on_expire(self, callback: Callable[[+T], typing.Any]) -> Self: View Source

    109    def on_expire(self, callback: collections.Callable[[T], typing.Any]) -> Self:
    110        """Set a callback that will be invoked when this value gets expired.
    111
    112        Both async and sync callbacks are supported.
    113
    114        Example
    115        -------
    116        ```py
    117        async def sink(message: str) -> None:
    118            await client.create_message(message)
    119            print("Sinked", message)
    120
    121        box = Box("bluh", 5).on_expire(sink)
    122
    123        while box.get().is_some():
    124            time.sleep(5)
    125        ```
    126        First `.get` call on an expired box, the `sink` callback will be invoked,
    127        also the inner value will be set to `Some(None)`.
    128
    129        After 5 seconds.
    130        ```py
    131        assert box.get() == Some("bluh") # This last call invokes the callback.
    132        # Sinked bluh
    133        assert box.get().is_none()
    134        ```
    135        """
    136        self._on_expire = callback
    137        return self

Set a callback that will be invoked when this value gets expired.

Both async and sync callbacks are supported.

###### Example

    async def sink(message: str) -> None:
        await client.create_message(message)
        print("Sinked", message)

    box = Box("bluh", 5).on_expire(sink)

    while box.get().is_some():
        time.sleep(5)

First `.get` call on an expired box, the `sink` callback will be invoked, also the inner value will be set to `Some(None)`.

After 5 seconds.

    assert box.get() == Some("bluh") # This last call invokes the callback.
    # Sinked bluh
    assert box.get().is_none()

def remaining(self) -> float: View Source

    139    def remaining(self) -> float:
    140        """Returns when this box will expire in seconds.
    141
    142        Example
    143        --------
    144        ```py
    145        jogo = Box("jogo", 3)
    146        assert jogo.get().unwrap() == "jogo"
    147
    148        time.sleep(1)
    149        assert jogo.remaining() == 2
    150        ```
    151        """
    152        if not self._mono:
    153            return 0.0
    154
    155        return math.floor(
    156            (self._expire_in - (time.monotonic() - self._mono) + 1) * 0.99
    157        )

Returns when this box will expire in seconds.

###### Example

    jogo = Box("jogo", 3)
    assert jogo.get().unwrap() == "jogo"

    time.sleep(1)
    assert jogo.remaining() == 2

def get(self) -> 'Option[T]': View Source

    159    def get(self) -> Option[T]:
    160        """Get the contained value if it was not expired, otherwise `Some(None)` is returned.
    161
    162        Example
    163        -------
    164        ```py
    165        pizza = Box("pizza", timedelta(days=1))
    166
    167        while not pizza.get().is_none():
    168            # Do stuff with the value while its not expired.
    169
    170        # After 1 day.
    171        assert pizza.get().is_none()
    172        ```
    173        """
    174        if self.has_expired:
    175            if self._on_expire is not None:
    176                with warnings.catch_warnings():
    177                    # ignore the warnings from `unwrap_unchecked`.
    178                    warnings.simplefilter("ignore", category=ub_checks)
    179                    try:
    180                        if asyncio.iscoroutinefunction(self._on_expire):
    181                            futures.loop().run_until_complete(
    182                                self._on_expire(self._inner.unwrap_unchecked())
    183                            )
    184                        else:
    185                            self._on_expire(self._inner.unwrap_unchecked())
    186                    finally:
    187                        self._on_expire = None
    188
    189            self._inner = option.NOTHING
    190            self._mono = None
    191            # SAFETY: The value is expired, therefore we always return None.
    192            return option.NOTHING
    193
    194        if self._mono is None:
    195            self._mono = time.monotonic()
    196
    197        return self._inner

Get the contained value if it was not expired, otherwise `Some(None)` is returned.

###### Example

    pizza = Box("pizza", timedelta(days=1))

    while not pizza.get().is_none():
        # Do stuff with the value while its not expired.

    # After 1 day.
    assert pizza.get().is_none()