timer-sentinel 1.0.1__py3-none-any.whl
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.
|
File without changes
|
timer_sentinel/core.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import time
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from logging import WARNING, Logger, getLogger
|
|
6
|
+
from typing import Any
|
|
7
|
+
from uuid import uuid4
|
|
8
|
+
|
|
9
|
+
DEFAULT_TIMER_NAME = "TimerSentinel"
|
|
10
|
+
default_logger = getLogger("TimerSentinel")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TimerSentinel:
|
|
14
|
+
"""Monitor execution time and log when threshold is exceeded.
|
|
15
|
+
|
|
16
|
+
Can be used as a context manager, decorator, or called manually.
|
|
17
|
+
Supports both sync and async functions.
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
# As context manager
|
|
21
|
+
with TimerSentinel(threshold=1.0, name="task"):
|
|
22
|
+
do_work()
|
|
23
|
+
|
|
24
|
+
# As decorator (sync)
|
|
25
|
+
@TimerSentinel(threshold=1.0, name="task")
|
|
26
|
+
def do_work():
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
# As decorator (async)
|
|
30
|
+
@TimerSentinel(threshold=1.0, name="task")
|
|
31
|
+
async def do_work():
|
|
32
|
+
await async_task()
|
|
33
|
+
|
|
34
|
+
# Manual usage
|
|
35
|
+
timer = TimerSentinel(threshold=1.0, name="task")
|
|
36
|
+
timer.start()
|
|
37
|
+
do_work()
|
|
38
|
+
timer.end()
|
|
39
|
+
timer.report()
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
__slots__ = (
|
|
43
|
+
"threshold",
|
|
44
|
+
"name",
|
|
45
|
+
"_logger",
|
|
46
|
+
"_on_exceed_keyword",
|
|
47
|
+
"_on_exceed_level",
|
|
48
|
+
"_on_exceed_callback",
|
|
49
|
+
"_callback_args",
|
|
50
|
+
"_execution_id",
|
|
51
|
+
"_timer",
|
|
52
|
+
"_total_time",
|
|
53
|
+
"_use_func_name",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
threshold: float,
|
|
59
|
+
name: str | None = None,
|
|
60
|
+
logger: Logger | None = None,
|
|
61
|
+
on_exceed_keyword: str = "OVERTIME",
|
|
62
|
+
on_exceed_level: int = WARNING,
|
|
63
|
+
on_exceed_callback: Callable[[], None] | None = None,
|
|
64
|
+
callback_args: dict[str, Any] | None = None,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Initialize the timer sentinel.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
threshold: Maximum allowed execution time in seconds.
|
|
70
|
+
name: Name identifier for the timer. Empty string by default.
|
|
71
|
+
If empty or used as decorator, will use "TimerSentinel"
|
|
72
|
+
or the function name respectively.
|
|
73
|
+
logger: Logger instance to use. If None, uses default
|
|
74
|
+
TimerSentinel logger.
|
|
75
|
+
on_exceed_keyword: Keyword to include in log message when
|
|
76
|
+
threshold exceeded. Default is "OVERTIME".
|
|
77
|
+
on_exceed_level: Logging level (integer) to use when threshold
|
|
78
|
+
exceeded. Default is WARNING (30).
|
|
79
|
+
on_exceed_callback: Optional callback function to execute when
|
|
80
|
+
threshold exceeded. Must be callable without arguments.
|
|
81
|
+
callback_args: Optional keyword arguments to pass to the
|
|
82
|
+
callback function when invoked.
|
|
83
|
+
"""
|
|
84
|
+
self.threshold = threshold
|
|
85
|
+
self._use_func_name = not name
|
|
86
|
+
self.name = name or DEFAULT_TIMER_NAME
|
|
87
|
+
self._logger = logger or default_logger
|
|
88
|
+
self._on_exceed_keyword = on_exceed_keyword
|
|
89
|
+
self._on_exceed_level = on_exceed_level
|
|
90
|
+
self._on_exceed_callback = on_exceed_callback
|
|
91
|
+
self._callback_args = callback_args or {}
|
|
92
|
+
self._execution_id = uuid4().hex[:8]
|
|
93
|
+
self._timer = None
|
|
94
|
+
self._total_time = None
|
|
95
|
+
|
|
96
|
+
def __call__(self, func: Callable) -> Callable:
|
|
97
|
+
"""Enable TimerSentinel to be used as a decorator.
|
|
98
|
+
|
|
99
|
+
Automatically detects async functions and wraps appropriately.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
func: The function to wrap and time.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Wrapped function that automatically times execution.
|
|
106
|
+
|
|
107
|
+
Example:
|
|
108
|
+
@TimerSentinel(threshold=1.0)
|
|
109
|
+
def my_function():
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
@TimerSentinel(threshold=1.0)
|
|
113
|
+
async def my_async_function():
|
|
114
|
+
await something()
|
|
115
|
+
"""
|
|
116
|
+
# Check if function is async
|
|
117
|
+
if inspect.iscoroutinefunction(func):
|
|
118
|
+
return self._wrap_async(func)
|
|
119
|
+
return self._wrap_sync(func)
|
|
120
|
+
|
|
121
|
+
def _wrap_sync(self, func: Callable) -> Callable:
|
|
122
|
+
"""Wrap a synchronous function."""
|
|
123
|
+
|
|
124
|
+
@wraps(func)
|
|
125
|
+
def wrapper(*args, **kwargs):
|
|
126
|
+
if self._use_func_name:
|
|
127
|
+
self.name = func.__name__
|
|
128
|
+
|
|
129
|
+
self.start()
|
|
130
|
+
try:
|
|
131
|
+
return func(*args, **kwargs)
|
|
132
|
+
finally:
|
|
133
|
+
self.end()
|
|
134
|
+
self.report()
|
|
135
|
+
|
|
136
|
+
return wrapper
|
|
137
|
+
|
|
138
|
+
def _wrap_async(self, func: Callable) -> Callable:
|
|
139
|
+
"""Wrap an asynchronous function."""
|
|
140
|
+
|
|
141
|
+
@wraps(func)
|
|
142
|
+
async def wrapper(*args, **kwargs):
|
|
143
|
+
if self._use_func_name:
|
|
144
|
+
self.name = func.__name__
|
|
145
|
+
|
|
146
|
+
self.start()
|
|
147
|
+
try:
|
|
148
|
+
return await func(*args, **kwargs)
|
|
149
|
+
finally:
|
|
150
|
+
self.end()
|
|
151
|
+
self.report()
|
|
152
|
+
|
|
153
|
+
return wrapper
|
|
154
|
+
|
|
155
|
+
def __enter__(self) -> "TimerSentinel":
|
|
156
|
+
"""Enter the context manager and start timing.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Self instance for use in with statement.
|
|
160
|
+
"""
|
|
161
|
+
self.start()
|
|
162
|
+
return self
|
|
163
|
+
|
|
164
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
165
|
+
"""Exit the context manager, stop timing and report.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
exc_type: Exception type if an exception occurred.
|
|
169
|
+
exc_val: Exception value if an exception occurred.
|
|
170
|
+
exc_tb: Exception traceback if an exception occurred.
|
|
171
|
+
|
|
172
|
+
Note:
|
|
173
|
+
Exceptions are propagated (not suppressed).
|
|
174
|
+
"""
|
|
175
|
+
self.end()
|
|
176
|
+
self.report()
|
|
177
|
+
|
|
178
|
+
def start(self) -> None:
|
|
179
|
+
"""Start the timer.
|
|
180
|
+
|
|
181
|
+
Records the current time using perf_counter for high precision.
|
|
182
|
+
"""
|
|
183
|
+
self._timer = time.perf_counter()
|
|
184
|
+
|
|
185
|
+
def end(self) -> None:
|
|
186
|
+
"""Stop the timer and calculate total execution time.
|
|
187
|
+
|
|
188
|
+
Raises:
|
|
189
|
+
RuntimeError: If start() was not called before end().
|
|
190
|
+
"""
|
|
191
|
+
if self._timer is None:
|
|
192
|
+
raise RuntimeError("Timer not started")
|
|
193
|
+
self._total_time = time.perf_counter() - self._timer
|
|
194
|
+
|
|
195
|
+
def report(self) -> None:
|
|
196
|
+
"""Log a message and execute callback if threshold was exceeded.
|
|
197
|
+
|
|
198
|
+
Logs a message with execution time details if the threshold was
|
|
199
|
+
exceeded. Executes the configured callback function if provided.
|
|
200
|
+
|
|
201
|
+
Raises:
|
|
202
|
+
RuntimeError: If end() was not called before report().
|
|
203
|
+
"""
|
|
204
|
+
if self._total_time is None:
|
|
205
|
+
raise RuntimeError("Timer not ended")
|
|
206
|
+
|
|
207
|
+
if self._total_time > self.threshold:
|
|
208
|
+
msg = (
|
|
209
|
+
f"{self._on_exceed_keyword} | {self.name}.{self._execution_id}"
|
|
210
|
+
f" | {self._total_time:.4f}s > {self.threshold:.4f}s"
|
|
211
|
+
)
|
|
212
|
+
self._logger.log(self._on_exceed_level, msg)
|
|
213
|
+
|
|
214
|
+
if self._on_exceed_callback:
|
|
215
|
+
self._on_exceed_callback(**self._callback_args)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: timer-sentinel
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: A simple, efficient timer for monitoring execution time and logging threshold violations
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: timer,monitoring,performance,logging,decorator
|
|
8
|
+
Author: Aaron Ojeda
|
|
9
|
+
Requires-Python: >=3.10,<3.15
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Project-URL: Issues, https://github.com/aaronojedait/timer-sentinel/issues
|
|
20
|
+
Project-URL: Repository, https://github.com/aaronojedait/timer-sentinel
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# Timer Sentinel
|
|
24
|
+
|
|
25
|
+
[](https://badge.fury.io/py/timer-sentinel)
|
|
26
|
+
[](https://pypi.org/project/timer-sentinel/)
|
|
27
|
+
[](https://opensource.org/licenses/MIT)
|
|
28
|
+
|
|
29
|
+
A dead-simple, **fast**, and **lightweight** Python timer for catching slow code. Zero dependencies, optimized performance, just pure Python doing one thing well.
|
|
30
|
+
|
|
31
|
+
## Why?
|
|
32
|
+
|
|
33
|
+
I built this after spending too much time debugging inconsistent latency in complex systems. Sometimes a function that should take 100ms randomly takes 5 seconds, and you need to know **when** and **where**.
|
|
34
|
+
|
|
35
|
+
This timer helped me catch those issues. Works great with **Grafana + Loki** for analyzing logs and creating alerts, or use the callback for whatever custom logic you need.
|
|
36
|
+
|
|
37
|
+
## What it does
|
|
38
|
+
|
|
39
|
+
- โฑ๏ธ Times your code with high precision
|
|
40
|
+
- ๐ Logs when it's too slow
|
|
41
|
+
- ๐ Runs callbacks when thresholds are exceeded
|
|
42
|
+
- ๐ฏ Works as decorator, context manager, or manual timer
|
|
43
|
+
- ๐ Supports async functions
|
|
44
|
+
- ๐ **Fast** - optimized with `__slots__`, minimal overhead
|
|
45
|
+
- ๐ชถ **Lightweight** - zero dependencies, tiny footprint
|
|
46
|
+
- โก **Efficient** - uses `time.perf_counter()` for microsecond precision
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install timer-sentinel
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from timer_sentinel import TimerSentinel
|
|
58
|
+
|
|
59
|
+
# Context manager
|
|
60
|
+
with TimerSentinel(threshold=1.0, name="db_query"):
|
|
61
|
+
slow_database_call()
|
|
62
|
+
|
|
63
|
+
# Decorator
|
|
64
|
+
@TimerSentinel(threshold=0.5)
|
|
65
|
+
def api_call():
|
|
66
|
+
return requests.get(url)
|
|
67
|
+
|
|
68
|
+
# Async
|
|
69
|
+
@TimerSentinel(threshold=0.5)
|
|
70
|
+
async def async_operation():
|
|
71
|
+
await some_io()
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Real-world Examples
|
|
75
|
+
|
|
76
|
+
### Catch slow database queries
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
@TimerSentinel(threshold=0.1, name="db_query")
|
|
80
|
+
def get_user(user_id):
|
|
81
|
+
return db.query(f"SELECT * FROM users WHERE id={user_id}")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Integration with Grafana + Loki
|
|
85
|
+
|
|
86
|
+
Just log to your standard logger and ship logs to Loki:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
import logging
|
|
90
|
+
|
|
91
|
+
logger = logging.getLogger("production")
|
|
92
|
+
|
|
93
|
+
with TimerSentinel(threshold=1.0, logger=logger, name="api_call"):
|
|
94
|
+
call_external_api()
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Create alerts in Grafana based on the "OVERTIME" keyword or parse execution times. Good for catching performance regressions in production.
|
|
98
|
+
|
|
99
|
+
## API
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
TimerSentinel(
|
|
103
|
+
threshold: float, # Max time in seconds
|
|
104
|
+
name: str = "", # Timer name (uses function name if decorator)
|
|
105
|
+
logger: Logger | None = None, # Custom logger
|
|
106
|
+
on_exceed_keyword: str = "OVERTIME", # Log keyword
|
|
107
|
+
on_exceed_level: int = WARNING, # Log level
|
|
108
|
+
on_exceed_callback: Callable = None, # Callback function
|
|
109
|
+
callback_args: dict = None, # Args for callback
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Methods:**
|
|
114
|
+
- `start()` - Start timing
|
|
115
|
+
- `end()` - Stop timing
|
|
116
|
+
- `report()` - Log if threshold exceeded
|
|
117
|
+
|
|
118
|
+
**Use as:**
|
|
119
|
+
- Decorator: `@TimerSentinel(...)`
|
|
120
|
+
- Context manager: `with TimerSentinel(...)`
|
|
121
|
+
- Manual: `timer.start()` โ `timer.end()` โ `timer.report()`
|
|
122
|
+
|
|
123
|
+
## Requirements
|
|
124
|
+
|
|
125
|
+
- Python 3.10+
|
|
126
|
+
- No external dependencies
|
|
127
|
+
|
|
128
|
+
## Contributing
|
|
129
|
+
|
|
130
|
+
Found a bug? Want a feature? Open an issue or PR on [GitHub](https://github.com/aaronojedait/timer-sentinel).
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
MIT License - do whatever you want with it.
|
|
135
|
+
|
|
136
|
+
## Author
|
|
137
|
+
|
|
138
|
+
**Aaron Ojeda** ยท [GitHub](https://github.com/aaronojedait)
|
|
139
|
+
|
|
140
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
timer_sentinel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
timer_sentinel/core.py,sha256=OyPH7nn6GG7OlWNghVmDdcLCHzXM9TBulF7HZbissB0,6717
|
|
3
|
+
timer_sentinel-1.0.1.dist-info/METADATA,sha256=E9qG7LllSr5DvXup_k-B22uQjsHou2NhdlxTje0doCo,4322
|
|
4
|
+
timer_sentinel-1.0.1.dist-info/WHEEL,sha256=kJCRJT_g0adfAJzTx2GUMmS80rTJIVHRCfG0DQgLq3o,88
|
|
5
|
+
timer_sentinel-1.0.1.dist-info/licenses/LICENSE,sha256=M78NOSIjiIj0bhd_AasXH3CJUoljIUJ8yFWbLno7NDo,1068
|
|
6
|
+
timer_sentinel-1.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aaron Ojeda
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|