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
+ [![PyPI version](https://badge.fury.io/py/timer-sentinel.svg)](https://badge.fury.io/py/timer-sentinel)
26
+ [![Python Support](https://img.shields.io/pypi/pyversions/timer-sentinel.svg)](https://pypi.org/project/timer-sentinel/)
27
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.3.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.