1# ruff: noqa: BLE001
2__lazy_modules__ = frozenset(('asyncio',))
3import asyncutils as A, asyncio as I, collections as c
4from asyncutils.constants import _NO_DEFAULT
5from asyncutils._internal import helpers as H, patch as P
6from asyncutils._internal.submodules import misc_all as __all__
7from sys import audit, exc_info, intern
8from time import monotonic
[docs]
9class StateMachine:
10 __slots__ = '__et', '__ex', '__lock', '__state', '__ts'
11 def __init__(self, state): self.__state, self.__ts, self.__et, self.__ex, self.__lock = intern(state), c.defaultdict(lambda: c.defaultdict(set)), {}, {}, I.Lock()
[docs]
12 def add(self, from_state, to_state, condition=None): self.__ts[intern(from_state)][intern(to_state)].add(condition)
13 on_enter, on_exit = map(lambda attr: lambda self, state: lambda h, /: dict.__setitem__(getattr(self, attr), state, h) or h, __slots__[:2])
[docs]
14 async def transition(self, state):
15 state, s = intern(state), self.__state
16 async with self.__lock:
17 if None not in (S := self.__ts[s][state]):
18 for _ in S:
19 if await _(s, state): break
20 else: return False
21 await self._helper(1); self.__state = state; await self._helper(0); return True
22 async def _helper(self, i, /, _=A.IgnoreErrors(KeyError), s=__slots__):
23 async with _: await getattr(self, s[i])[self.__state]()
24 P.patch_method_signatures((_helper, 'attr'))
[docs]
25async def gather_with_limited_concurrency(n=None, *a, ret_exc=False):
26 async def wrapped(c, s=I.Semaphore(A.getcontext().GATHER_WITH_LIMITED_CONCURRENCY_DEFAULT_MAX_CONCURRENT if n is None else n)): # noqa: B008
27 async with s: return await c
28 return await I.gather(*map(wrapped, a), return_exceptions=ret_exc)
[docs]
29class CallbackAccumulator(c.deque, A.ExecutorRequiredAsyncContextMixin):
30 __slots__ = 'call_once', 'default_getter', 't'
31 def __init__(self, name, it=(), maxlen=None, default=_NO_DEFAULT, call_once=True, default_getter=None): super().__init__(A.aiter_to_gen(it, use_futures=True), maxlen); self.t, self.call_once, self.default_getter = tuple(H.filter_out(name, default, s=_NO_DEFAULT)), call_once, (lambda: (exc_info(), {}) if name == '__exit__' else ((), {})) if default_getter is None else default_getter
[docs]
32 def __call__(self, *a, **k):
33 for f in self: f(*a, **k)
[docs]
34 def __enter__(self): return self
[docs]
35 def __exit__(self, /, *_): a, k = self.default_getter(); self(*a, **k)
[docs]
36 def add(self, o, /): self.append(getattr(o, *self.t))
[docs]
37 def offer_last(self, o, /):
38 if (x := self.maxlen) is None or x > len(self): self.add(o); return True
39 return False
40 @property
41 def callbacks(self): return self.copy()
[docs]
42 def __iter__(self):
43 if self.call_once:
44 p = self.popleft
45 while self: yield p()
46 else: yield from self.callbacks
[docs]
47class CacheWithBackgroundRefresh(A.LoopContextMixin):
48 _executor = None; __slots__ = '__cache', '__evt', '__ld', '__lock', '__processor', '__refresh', '__timer', '__tk', '__ttl'
49 def __init__(self, ttl=None, refresh=None, *, processor=None, default_loader=None, timer=monotonic):
50 C = A.getcontext()
51 if ttl is None: ttl = C.BACKGROUND_REFRESH_CACHE_DEFAULT_TTL
52 if refresh is None: refresh = C.BACKGROUND_REFRESH_CACHE_DEFAULT_REFRESH
53 audit(H.fullname(self), ttl, refresh); super().__init__(); self.__cache, self.__lock, self.__ld, self.__tk, self.__evt, self.__timer = {}, I.Lock(), c.defaultdict(lambda: default_loader), None, I.Event(), timer; self.configure(ttl, refresh, processor)
[docs]
54 def __contains__(self, key): return key in self.__cache
[docs]
55 def register_loader(self, key, loader): self.__ld[key] = loader
[docs]
56 def expired(self, key): return self.time_past(key) > self.__ttl
[docs]
57 def should_refresh(self, key): return self.time_past(key) > self.__ttl-self.__refresh if key in self else False
[docs]
58 def time_past(self, key): return self.__timer()-self.__cache[key].timestamp
[docs]
60 def get_loader(self, key):
61 if (k := self.__ld[key]) is None: raise LookupError(f'asyncutils.misc.CacheWithBackgroundRefresh: no loader registered for key {key!r}')
62 return k
63 async def _process_error(self, e, b, /):
64 if (x := (c := type(self))._executor) is None: x = H.create_executor(c)
65 if I.iscoroutine(r := await self.loop.run_in_executor(x, self.__processor, e, b)): await r
[docs]
66 async def get(self, key, loader=None):
67 async with self.__lock:
68 if loader is not None: self.register_loader(key, loader)
69 if self.should_refresh(key): await self.refresh_item(key)
70 if key not in self.__cache or self.expired(key): await self.load_item(key)
71 return self.__cache[key].value
[docs]
72 async def __setup__(self):
73 if self.__tk is None: self.__evt.clear(); self.__tk = self.make(self.refresh_loop())
[docs]
74 async def __cleanup__(self):
75 if self.__tk: self.__evt.set(); await A.safe_cancel(self.__tk); self.__tk = None
[docs]
76 async def load_item(self, key, _=c.namedtuple('CacheEntry', 'value timestamp loading', module='asyncutils.misc')):
77 _ = _(await self.get_loader(key)(key), self.__timer(), False)
78 async with self.__lock: self.__cache[key] = _
[docs]
79 async def refresh_item(self, key):
80 async with self.__lock:
81 if key not in self.__ld or (t := self.__cache.get(key)) is None or t.loading: return
82 t.loading = True
83 try: await self.load_item(key)
84 except A.CRITICAL: raise A.Critical
85 except BaseException as e: t.loading = False; await self._process_error(e, False)
[docs]
86 async def refresh_loop(self):
87 r, t = self.__refresh, []
88 while True:
89 await I.sleep(r)
90 try:
91 async with self.__lock: d, f = self.__timer()-self.__ttl+self.__refresh, self.refresh_item; t.extend(self.make_multiple(f(k) for k, v in self.__cache.items() if not v.loading and d > v.timestamp))
92 if t: await I.gather(*t); t.clear()
93 except A.CRITICAL: raise A.Critical
94 except I.CancelledError: raise
95 except BaseException as e: await self._process_error(e, True)
[docs]
96 async def invalidate(self, key):
97 async with self.__lock: return self.__cache.pop(key, None)
[docs]
98 async def clear(self):
99 async with self.__lock: self.__cache.clear()
100 P.patch_method_signatures((__init__, 'ttl=None, refresh=None, *, processor=None, default_loader=None, timer={}'), (configure, 'ttl, refresh, processor=None'), (load_item, 'key'))