sain.boxed
a Box is a wrapper around a value that expires after the given amount of time.
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
Box61@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)
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
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:
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:
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]':
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()