async-timer 1.0.1__tar.gz → 1.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: async-timer
3
- Version: 1.0.1
3
+ Version: 1.0.3
4
4
  Summary: The missing Python async timer.
5
5
  License: MIT
6
6
  Keywords: async,timer
@@ -48,16 +48,62 @@ This package is particularly useful for tasks like automatically updating caches
48
48
 
49
49
  ## Example Usage
50
50
 
51
+ ### FastAPI
52
+
53
+ This snippet starts fastapi webserver with the `refresh_db` function being executed every 5 seconds, refresing a shared `DB_CACHE` object.
54
+
55
+ ```python
56
+
57
+ import contextlib
58
+ import time
59
+
60
+ import uvicorn
61
+ from fastapi import FastAPI
62
+
63
+ import async_timer
64
+
65
+ DB_CACHE = {"initialised": False}
66
+
67
+
68
+ async def refresh_db():
69
+ global DB_CACHE
70
+ DB_CACHE |= {"initialised": True, "cur_value": time.time()}
71
+
72
+
73
+ @contextlib.asynccontextmanager
74
+ async def lifespan(_app: FastAPI):
75
+ async with async_timer.Timer(delay=5, target=refresh_db) as timer:
76
+ await timer.wait(hit_count=1) # block until the timer triggers at least once
77
+ yield
78
+
79
+
80
+ app = FastAPI(lifespan=lifespan)
81
+
82
+
83
+ @app.get("/")
84
+ async def root():
85
+ return {"message": "Hello World", "db_cache": DB_CACHE}
86
+
87
+
88
+ if __name__ == "__main__":
89
+ uvicorn.run(app, host="0.0.0.0", port=8000)
90
+
91
+ ```
92
+
93
+ ### join()
51
94
  ```python
52
95
 
53
96
  import async_timer
54
97
 
55
- # Simple timer example
56
98
  timer = async_timer.Timer(12, target=lambda: 42)
57
99
  timer.start()
58
- val = await timer.join() # `val` will be 42 after 12 seconds
100
+ val = await timer.join() # `val` will be set to 42 after 12 seconds
101
+ ```
59
102
 
103
+ ### for loop
104
+ ```python
60
105
  # Async for loop example
106
+ import async_timer
61
107
  import time
62
108
  with async_timer.Timer(14, target=time.time) as timer:
63
109
  async for time_rv in timer:
@@ -26,16 +26,62 @@ This package is particularly useful for tasks like automatically updating caches
26
26
 
27
27
  ## Example Usage
28
28
 
29
+ ### FastAPI
30
+
31
+ This snippet starts fastapi webserver with the `refresh_db` function being executed every 5 seconds, refresing a shared `DB_CACHE` object.
32
+
33
+ ```python
34
+
35
+ import contextlib
36
+ import time
37
+
38
+ import uvicorn
39
+ from fastapi import FastAPI
40
+
41
+ import async_timer
42
+
43
+ DB_CACHE = {"initialised": False}
44
+
45
+
46
+ async def refresh_db():
47
+ global DB_CACHE
48
+ DB_CACHE |= {"initialised": True, "cur_value": time.time()}
49
+
50
+
51
+ @contextlib.asynccontextmanager
52
+ async def lifespan(_app: FastAPI):
53
+ async with async_timer.Timer(delay=5, target=refresh_db) as timer:
54
+ await timer.wait(hit_count=1) # block until the timer triggers at least once
55
+ yield
56
+
57
+
58
+ app = FastAPI(lifespan=lifespan)
59
+
60
+
61
+ @app.get("/")
62
+ async def root():
63
+ return {"message": "Hello World", "db_cache": DB_CACHE}
64
+
65
+
66
+ if __name__ == "__main__":
67
+ uvicorn.run(app, host="0.0.0.0", port=8000)
68
+
69
+ ```
70
+
71
+ ### join()
29
72
  ```python
30
73
 
31
74
  import async_timer
32
75
 
33
- # Simple timer example
34
76
  timer = async_timer.Timer(12, target=lambda: 42)
35
77
  timer.start()
36
- val = await timer.join() # `val` will be 42 after 12 seconds
78
+ val = await timer.join() # `val` will be set to 42 after 12 seconds
79
+ ```
37
80
 
81
+ ### for loop
82
+ ```python
38
83
  # Async for loop example
84
+ import async_timer
39
85
  import time
40
86
  with async_timer.Timer(14, target=time.time) as timer:
41
87
  async for time_rv in timer:
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "async-timer"
3
- version = "v1.0.1"
3
+ version = "v1.0.3"
4
4
  description = "The missing Python async timer."
5
5
  authors = ["Ilya O. <vrghost@gmail.com>"]
6
6
  license = "MIT"
@@ -66,7 +66,7 @@ line-length = 88
66
66
 
67
67
  [tool.ruff.isort]
68
68
  order-by-type = true
69
- known-first-party = ["tc_rrf", ]
69
+ known-first-party = ["async_timer", ]
70
70
  forced-separate = ["tests"]
71
71
 
72
72
  [tool.black]
@@ -2,14 +2,20 @@
2
2
  import asyncio
3
3
  import inspect
4
4
  import logging
5
- import sys
6
5
  import typing
7
6
 
8
7
  logger = logging.getLogger(__name__)
9
8
  T = typing.TypeVar("T")
9
+ TimerMainTaskT = typing.Union[
10
+ typing.Callable[[], T],
11
+ typing.Callable[[], typing.Coroutine[typing.Any, typing.Any, T]],
12
+ typing.Callable[[], typing.AsyncGenerator[T, typing.Any]],
13
+ typing.Callable[[], typing.Generator[T, typing.Any, typing.Any]],
14
+ ]
15
+ TimerCallbackT = typing.Callable[["Timer[T]", TimerMainTaskT[T]], None]
10
16
 
11
17
 
12
- class FanoutRv:
18
+ class FanoutRv(typing.Generic[T]):
13
19
  """An object that shares a result actoss all waiters"""
14
20
 
15
21
  lock: asyncio.Lock
@@ -19,20 +25,20 @@ class FanoutRv:
19
25
  self.futures = []
20
26
  self.lock = asyncio.Lock()
21
27
 
22
- async def wait(self):
28
+ async def wait(self) -> T:
23
29
  """Wait for result to be posted"""
24
30
  future = asyncio.get_running_loop().create_future()
25
31
  async with self.lock:
26
32
  self.futures.append(future)
27
33
  return await future
28
34
 
29
- async def send_result(self, result):
35
+ async def send_result(self, result: T):
30
36
  async with self.lock:
31
37
  for future in self.futures:
32
38
  future.set_result(result)
33
39
  self.futures.clear()
34
40
 
35
- async def send_exception(self, exc):
41
+ async def send_exception(self, exc: Exception):
36
42
  async with self.lock:
37
43
  for future in self.futures:
38
44
  future.set_exception(exc)
@@ -45,30 +51,36 @@ class FanoutRv:
45
51
  self.futures.clear()
46
52
 
47
53
 
48
- def _default_main_loop_exception_callback(exc_type, exc_val, exc_tb):
54
+ def _noop_cb(*_, **__):
55
+ pass
56
+
57
+
58
+ def _default_main_loop_exception_callback(*_, **__):
49
59
  logger.exception("An unexpected exception in the timer loop.")
50
60
 
51
61
 
52
- class Timer:
62
+ class Timer(typing.Generic[T]):
53
63
  delay: float
54
- target: typing.Callable[[], T]
64
+ hit_count: int = 0 # Number of times the timer has run so far
65
+ target: TimerMainTaskT[T]
55
66
 
56
- result_fanout: FanoutRv
67
+ result_fanout: FanoutRv[T]
57
68
  main_task: typing.Optional[asyncio.Task] = None
58
- exception_callback: typing.Callable[[typing.Any, typing.Any, typing.Any], None]
69
+ exception_callback: TimerCallbackT[T]
70
+ cancel_callback: TimerCallbackT[T]
59
71
 
60
72
  def __init__(
61
73
  self,
62
74
  delay: float,
63
- target: typing.Callable[[], T],
64
- exc_cb: typing.Callable[
65
- [typing.Any, typing.Any, typing.Any], None
66
- ] = _default_main_loop_exception_callback,
75
+ target: TimerMainTaskT[T],
76
+ exc_cb: TimerCallbackT[T] = _default_main_loop_exception_callback,
77
+ cancel_cb: TimerCallbackT[T] = _noop_cb,
67
78
  ):
68
79
  self.delay = delay
69
80
  self.target = target
70
81
  self.result_fanout = FanoutRv()
71
82
  self.exception_callback = exc_cb
83
+ self.cancel_callback = cancel_cb
72
84
 
73
85
  def start(self):
74
86
  """Schedule the timer to run."""
@@ -82,17 +94,17 @@ class Timer:
82
94
  """Return `True` if the timer is currently scheduled"""
83
95
  return (self.main_task is not None) and (not self.main_task.done())
84
96
 
85
- async def __aenter__(self):
97
+ async def __aenter__(self) -> "Timer[T]":
86
98
  self.start()
87
99
  return self
88
100
 
89
101
  async def __aexit__(self, exc_type, exc_val, exc_tb):
90
102
  await self.cancel()
91
103
 
92
- def __aiter__(self):
104
+ def __aiter__(self) -> typing.AsyncIterator[T]:
93
105
  return self
94
106
 
95
- async def join(self):
107
+ async def join(self) -> T:
96
108
  """Wait for the next tick of the timer"""
97
109
  if not self.is_running():
98
110
  raise asyncio.CancelledError("The timer is not running.")
@@ -100,7 +112,30 @@ class Timer:
100
112
  await self.result_fanout.wait()
101
113
  ) # this can raise `asyncio.CancelledError`
102
114
 
103
- async def __anext__(self):
115
+ async def wait(
116
+ self, /, hit_count: int = None, hits: int = None
117
+ ) -> typing.Optional[T]:
118
+ """
119
+ Wait for the timer to reach certain hit count
120
+ or wait for a certain number of hits.
121
+
122
+ Returns the last generated result IF there was a need to wait.
123
+ Returns `None` otherwise.
124
+ """
125
+ if hit_count is not None:
126
+ target_hit_count = max(0, hit_count)
127
+ elif hits is not None:
128
+ target_hit_count = self.hit_count + max(0, hits)
129
+ else:
130
+ raise RuntimeError("Please provide either `hits` or `hit_count`")
131
+ need_to_wait_for = target_hit_count - self.hit_count
132
+ last_rv = None
133
+ while need_to_wait_for > 0:
134
+ last_rv = await self.join()
135
+ need_to_wait_for -= 1
136
+ return last_rv
137
+
138
+ async def __anext__(self) -> T:
104
139
  try:
105
140
  return await self.join()
106
141
  except asyncio.CancelledError as err:
@@ -108,7 +143,7 @@ class Timer:
108
143
 
109
144
  def _maybe_detect_generator(
110
145
  self, target_rv
111
- ) -> typing.Tuple[typing.Any, typing.Callable[[], T]]:
146
+ ) -> typing.Tuple[T, typing.Callable[[], T]]:
112
147
  """Check if the value returned by the `self.target` call is a
113
148
  kind of generator (sync or async).
114
149
 
@@ -149,19 +184,21 @@ class Timer:
149
184
  rv = await next_val
150
185
  else:
151
186
  rv = next_val
152
- if isinstance(rv, (StopIteration, StopAsyncIteration)):
153
- break
187
+ except (StopIteration, StopAsyncIteration):
188
+ break
154
189
  except Exception as err:
155
190
  await self.result_fanout.send_exception(err)
156
- self.exception_callback(*sys.exc_info())
191
+ self.exception_callback(self, self.target)
157
192
  break
158
193
  else:
159
194
  await self.result_fanout.send_result(rv)
160
195
  first_iter = False
196
+ self.hit_count += 1
161
197
  await asyncio.sleep(self.delay)
162
198
  finally:
163
199
  # Main loop finished - cancel all watchers
164
200
  await self.result_fanout.cancel()
201
+ self.cancel_callback(self, self.target)
165
202
 
166
203
  async def cancel(self):
167
204
  """Unshedule the timer"""