timeexecution 8.2.0__tar.gz → 8.3.1__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.
Files changed (39) hide show
  1. {timeexecution-8.2.0/timeexecution.egg-info → timeexecution-8.3.1}/PKG-INFO +27 -7
  2. {timeexecution-8.2.0 → timeexecution-8.3.1}/README.md +26 -6
  3. {timeexecution-8.2.0 → timeexecution-8.3.1}/pyproject.toml +3 -0
  4. {timeexecution-8.2.0 → timeexecution-8.3.1}/tests/test_hooks.py +38 -1
  5. {timeexecution-8.2.0 → timeexecution-8.3.1}/time_execution/decorator.py +28 -4
  6. {timeexecution-8.2.0 → timeexecution-8.3.1}/time_execution/timed.py +43 -23
  7. {timeexecution-8.2.0 → timeexecution-8.3.1/timeexecution.egg-info}/PKG-INFO +27 -7
  8. {timeexecution-8.2.0 → timeexecution-8.3.1}/.editorconfig +0 -0
  9. {timeexecution-8.2.0 → timeexecution-8.3.1}/.gitchangelog.rc +0 -0
  10. {timeexecution-8.2.0 → timeexecution-8.3.1}/.github/workflows/lint.yml +0 -0
  11. {timeexecution-8.2.0 → timeexecution-8.3.1}/.github/workflows/publish.yml +0 -0
  12. {timeexecution-8.2.0 → timeexecution-8.3.1}/.github/workflows/tests.yml +0 -0
  13. {timeexecution-8.2.0 → timeexecution-8.3.1}/.gitignore +0 -0
  14. {timeexecution-8.2.0 → timeexecution-8.3.1}/CHANGELOG.md +0 -0
  15. {timeexecution-8.2.0 → timeexecution-8.3.1}/LICENSE +0 -0
  16. {timeexecution-8.2.0 → timeexecution-8.3.1}/MANIFEST.in +0 -0
  17. {timeexecution-8.2.0 → timeexecution-8.3.1}/Makefile +0 -0
  18. {timeexecution-8.2.0 → timeexecution-8.3.1}/requirements.txt +0 -0
  19. {timeexecution-8.2.0 → timeexecution-8.3.1}/setup.cfg +0 -0
  20. {timeexecution-8.2.0 → timeexecution-8.3.1}/setup.py +0 -0
  21. {timeexecution-8.2.0 → timeexecution-8.3.1}/tests/__init__.py +0 -0
  22. {timeexecution-8.2.0 → timeexecution-8.3.1}/tests/conftest.py +0 -0
  23. {timeexecution-8.2.0 → timeexecution-8.3.1}/tests/dummy_process.py +0 -0
  24. {timeexecution-8.2.0 → timeexecution-8.3.1}/tests/test_base_backend.py +0 -0
  25. {timeexecution-8.2.0 → timeexecution-8.3.1}/tests/test_decorator_async.py +0 -0
  26. {timeexecution-8.2.0 → timeexecution-8.3.1}/tests/test_elasticsearch.py +0 -0
  27. {timeexecution-8.2.0 → timeexecution-8.3.1}/tests/test_threaded_backend.py +0 -0
  28. {timeexecution-8.2.0 → timeexecution-8.3.1}/time_execution/__init__.py +0 -0
  29. {timeexecution-8.2.0 → timeexecution-8.3.1}/time_execution/backends/__init__.py +0 -0
  30. {timeexecution-8.2.0 → timeexecution-8.3.1}/time_execution/backends/base.py +0 -0
  31. {timeexecution-8.2.0 → timeexecution-8.3.1}/time_execution/backends/elasticsearch.py +0 -0
  32. {timeexecution-8.2.0 → timeexecution-8.3.1}/time_execution/backends/threaded.py +0 -0
  33. {timeexecution-8.2.0 → timeexecution-8.3.1}/time_execution/py.typed +0 -0
  34. {timeexecution-8.2.0 → timeexecution-8.3.1}/timeexecution.egg-info/SOURCES.txt +0 -0
  35. {timeexecution-8.2.0 → timeexecution-8.3.1}/timeexecution.egg-info/dependency_links.txt +0 -0
  36. {timeexecution-8.2.0 → timeexecution-8.3.1}/timeexecution.egg-info/not-zip-safe +0 -0
  37. {timeexecution-8.2.0 → timeexecution-8.3.1}/timeexecution.egg-info/requires.txt +0 -0
  38. {timeexecution-8.2.0 → timeexecution-8.3.1}/timeexecution.egg-info/top_level.txt +0 -0
  39. {timeexecution-8.2.0 → timeexecution-8.3.1}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: timeexecution
3
- Version: 8.2.0
3
+ Version: 8.3.1
4
4
  Summary: Python project
5
5
  Home-page: https://github.com/kpn/py-timeexecution
6
6
  Author: KPN DE Platform
@@ -207,13 +207,16 @@ With a hook you can add additional and change existing fields. This can
207
207
  be useful for cases where you would like to add a column to the metric
208
208
  based on the response of the wrapped function.
209
209
 
210
- A hook will always get 3 arguments:
210
+ ### Simple callable hook
211
211
 
212
- * `response` - The returned value of the wrapped function
213
- * `exception` - The raised exception of the wrapped function
214
- * `metric` - A dict containing the data to be send to the backend
215
- * `func_args` - Original args received by the wrapped function.
216
- * `func_kwargs` - Original kwargs received by the wrapped function.
212
+ A function which is called after calling the decorated function and before sending the metrics. It receives the following arguments:
213
+
214
+ * `response`: the returned value of the wrapped function
215
+ * `exception`: exception raised the wrapped function, if any
216
+ * `metric`: a dictionary containing the data to be sent to the backend
217
+ * `func`: the decorated function itself
218
+ * `func_args`: original args received by the wrapped function.
219
+ * `func_kwargs`: original kwargs received by the wrapped function.
217
220
 
218
221
  From within a hook you can change the `name` if you want the metrics to be
219
222
  split into multiple series.
@@ -275,6 +278,23 @@ def celery_task(self, **kwargs):
275
278
  return True
276
279
  ```
277
280
 
281
+ ### Generator hook
282
+
283
+ A generator hook works similarly to a simple hook, but also allows to run an arbitrary code just before the decorated function is called.
284
+ It receives `func`, `func_args`, and `func_kwargs` as its arguments, whilst `response`, `exception`, and `metric` are sent later at the `yield` site:
285
+
286
+ ```python
287
+ def generator_hook(func, func_args, func_kwargs) -> Generator[
288
+ None,
289
+ Tuple[Any, Optional[BaseException], Dict[str, Any]], # response, exception, and metrics
290
+ Optional[Dict[str, Any]],
291
+ ]:
292
+ print("This runs before the decorated function")
293
+ (response, exception, metrics) = yield
294
+ print("This runs after the decorated function")
295
+ ...
296
+ ```
297
+
278
298
  ## Manually sending metrics
279
299
 
280
300
  You can also send any metric you have manually to the backend. These
@@ -176,13 +176,16 @@ With a hook you can add additional and change existing fields. This can
176
176
  be useful for cases where you would like to add a column to the metric
177
177
  based on the response of the wrapped function.
178
178
 
179
- A hook will always get 3 arguments:
179
+ ### Simple callable hook
180
180
 
181
- * `response` - The returned value of the wrapped function
182
- * `exception` - The raised exception of the wrapped function
183
- * `metric` - A dict containing the data to be send to the backend
184
- * `func_args` - Original args received by the wrapped function.
185
- * `func_kwargs` - Original kwargs received by the wrapped function.
181
+ A function which is called after calling the decorated function and before sending the metrics. It receives the following arguments:
182
+
183
+ * `response`: the returned value of the wrapped function
184
+ * `exception`: exception raised the wrapped function, if any
185
+ * `metric`: a dictionary containing the data to be sent to the backend
186
+ * `func`: the decorated function itself
187
+ * `func_args`: original args received by the wrapped function.
188
+ * `func_kwargs`: original kwargs received by the wrapped function.
186
189
 
187
190
  From within a hook you can change the `name` if you want the metrics to be
188
191
  split into multiple series.
@@ -244,6 +247,23 @@ def celery_task(self, **kwargs):
244
247
  return True
245
248
  ```
246
249
 
250
+ ### Generator hook
251
+
252
+ A generator hook works similarly to a simple hook, but also allows to run an arbitrary code just before the decorated function is called.
253
+ It receives `func`, `func_args`, and `func_kwargs` as its arguments, whilst `response`, `exception`, and `metric` are sent later at the `yield` site:
254
+
255
+ ```python
256
+ def generator_hook(func, func_args, func_kwargs) -> Generator[
257
+ None,
258
+ Tuple[Any, Optional[BaseException], Dict[str, Any]], # response, exception, and metrics
259
+ Optional[Dict[str, Any]],
260
+ ]:
261
+ print("This runs before the decorated function")
262
+ (response, exception, metrics) = yield
263
+ print("This runs after the decorated function")
264
+ ...
265
+ ```
266
+
247
267
  ## Manually sending metrics
248
268
 
249
269
  You can also send any metric you have manually to the backend. These
@@ -28,6 +28,9 @@ omit = ["tests/*"]
28
28
  [build-system]
29
29
  requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"]
30
30
 
31
+ [tool.mypy]
32
+ warn_unused_configs = true
33
+
31
34
  [[tool.mypy.overrides]]
32
35
  module = ["mock", "freezegun", "elasticsearch.*", "fqn_decorators.*", "pkgsettings", "setuptools", "Queue"]
33
36
  ignore_missing_imports = true
@@ -2,7 +2,7 @@ import pytest
2
2
  from fqn_decorators import get_fqn
3
3
 
4
4
  from tests.conftest import go
5
- from time_execution import settings, time_execution
5
+ from time_execution import GeneratorHookReturnType, settings, time_execution
6
6
  from time_execution.backends.base import BaseMetricsBackend
7
7
 
8
8
 
@@ -231,3 +231,40 @@ class TestTimeExecution:
231
231
 
232
232
  with settings(hooks=[hook]):
233
233
  go(param1=param)
234
+
235
+ def test_generator_hook(self) -> None:
236
+ is_started = False
237
+
238
+ def generator_hook(func, func_args, func_kwargs) -> GeneratorHookReturnType:
239
+ assert func_args == (42,)
240
+ assert func_kwargs == {"bar": 100500}
241
+ assert not is_started, "the decorated function should not run just yet"
242
+ (response, _exception, _metrics) = yield
243
+ assert is_started
244
+ assert response == "response"
245
+ return {"key": "value"}
246
+
247
+ @time_execution(disable_default_hooks=True, extra_hooks=(generator_hook,))
248
+ def go(foo: int, *, bar: int) -> str:
249
+ nonlocal is_started
250
+ is_started = True
251
+ return "response"
252
+
253
+ def asserts(_name, **data):
254
+ assert data["key"] == "value"
255
+
256
+ with settings(backends=[AssertBackend(asserts)]):
257
+ assert go(42, bar=100500) == "response"
258
+
259
+ def test_generator_hook_did_not_stop(self) -> None:
260
+ def generator_hook(func, func_args, func_kwargs) -> GeneratorHookReturnType:
261
+ yield
262
+ yield # this extra `yield` is incorrect
263
+ return {}
264
+
265
+ @time_execution(disable_default_hooks=True, extra_hooks=(generator_hook,))
266
+ def go() -> None:
267
+ return None
268
+
269
+ with pytest.raises(RuntimeError, match="generator hook did not stop"):
270
+ go()
@@ -5,11 +5,11 @@ from __future__ import annotations
5
5
  from asyncio import iscoroutinefunction
6
6
  from collections.abc import Iterable
7
7
  from functools import wraps
8
- from typing import Any, Callable, Dict, Optional, Tuple, TypeVar, cast
8
+ from typing import Any, Callable, Dict, Generator, Optional, Tuple, TypeVar, cast
9
9
 
10
10
  import fqn_decorators
11
11
  from pkgsettings import Settings
12
- from typing_extensions import Protocol, overload
12
+ from typing_extensions import Protocol, TypeAlias, overload
13
13
 
14
14
  _F = TypeVar("_F", bound=Callable[..., Any])
15
15
 
@@ -31,7 +31,7 @@ def time_execution(__wrapped: _F) -> _F:
31
31
  def time_execution(
32
32
  *,
33
33
  get_fqn: Callable[[Any], str] = fqn_decorators.get_fqn,
34
- extra_hooks: Optional[Iterable[Hook]] = None,
34
+ extra_hooks: Optional[Iterable[Hook | GeneratorHook]] = None,
35
35
  disable_default_hooks: bool = False,
36
36
  ) -> Callable[[_F], _F]:
37
37
  """
@@ -83,6 +83,7 @@ class Hook(Protocol):
83
83
 
84
84
  def __call__(
85
85
  self,
86
+ *,
86
87
  response: Any,
87
88
  exception: Optional[BaseException],
88
89
  metric: Dict[str, Any],
@@ -90,4 +91,27 @@ class Hook(Protocol):
90
91
  func_args: Tuple[Any, ...],
91
92
  func_kwargs: Dict[str, Any],
92
93
  ) -> Optional[Dict[str, Any]]:
93
- ...
94
+ ... # fmt:skip
95
+
96
+
97
+ GeneratorHookReturnType: TypeAlias = Generator[
98
+ None, Tuple[Any, Optional[BaseException], Dict[str, Any]], Optional[Dict[str, Any]]
99
+ ]
100
+
101
+
102
+ class GeneratorHook(Protocol):
103
+ """
104
+ Generator-type hook.
105
+
106
+ This kind of hook gets called before the target function.
107
+ Response, exception, and metrics are sent into the generator after the target function finishes.
108
+ """
109
+
110
+ def __call__(
111
+ self,
112
+ *,
113
+ func: Callable[..., Any],
114
+ func_args: Tuple[Any, ...],
115
+ func_kwargs: Dict[str, Any],
116
+ ) -> GeneratorHookReturnType:
117
+ ... # fmt:skip
@@ -2,12 +2,13 @@ from __future__ import annotations
2
2
 
3
3
  from collections.abc import Iterable
4
4
  from contextlib import AbstractContextManager
5
+ from inspect import isgenerator, isgeneratorfunction
5
6
  from socket import gethostname
6
7
  from timeit import default_timer
7
8
  from types import TracebackType
8
- from typing import Any, Callable, Dict, Optional, Tuple, Type
9
+ from typing import Any, Callable, Dict, Optional, Tuple, Type, cast
9
10
 
10
- from time_execution import Hook, settings, write_metric
11
+ from time_execution import GeneratorHook, GeneratorHookReturnType, Hook, settings, write_metric
11
12
 
12
13
  SHORT_HOSTNAME = gethostname()
13
14
 
@@ -22,8 +23,7 @@ class Timed(AbstractContextManager):
22
23
  "result",
23
24
  "_wrapped",
24
25
  "_fqn",
25
- "_extra_hooks",
26
- "_disable_default_hooks",
26
+ "_hooks",
27
27
  "_call_args",
28
28
  "_call_kwargs",
29
29
  "_start_time",
@@ -36,19 +36,34 @@ class Timed(AbstractContextManager):
36
36
  fqn: str,
37
37
  call_args: Tuple[Any, ...],
38
38
  call_kwargs: Dict[str, Any],
39
- extra_hooks: Optional[Iterable[Hook]] = None,
39
+ extra_hooks: Optional[Iterable[Hook | GeneratorHook]] = None,
40
40
  disable_default_hooks: bool = False,
41
41
  ) -> None:
42
42
  self.result: Optional[Any] = None
43
43
  self._wrapped = wrapped
44
44
  self._fqn = fqn
45
- self._extra_hooks = extra_hooks
46
- self._disable_default_hooks = disable_default_hooks
47
45
  self._call_args = call_args
48
46
  self._call_kwargs = call_kwargs
49
47
 
48
+ hooks = extra_hooks or ()
49
+ if not disable_default_hooks:
50
+ hooks = (*settings.hooks, *hooks)
51
+
52
+ self._hooks = tuple(
53
+ (
54
+ cast(Hook, hook)
55
+ if not isgeneratorfunction(hook) # simple hook, we'll call it in the exit
56
+ # For a generator hook, call it. We'll start in the entrance.
57
+ else cast(GeneratorHookReturnType, hook(func=wrapped, func_args=call_args, func_kwargs=call_kwargs))
58
+ )
59
+ for hook in hooks
60
+ )
61
+
50
62
  def __enter__(self) -> Timed:
51
63
  self._start_time = default_timer()
64
+ for hook in self._hooks:
65
+ if isgenerator(hook):
66
+ hook.send(None) # start a generator hook
52
67
  return self
53
68
 
54
69
  def __exit__(
@@ -65,14 +80,9 @@ class Timed(AbstractContextManager):
65
80
  if origin:
66
81
  metric["origin"] = origin
67
82
 
68
- hooks = self._extra_hooks or ()
69
- if not self._disable_default_hooks:
70
- hooks = (*settings.hooks, *hooks)
71
-
72
83
  # Apply the registered hooks, and collect the metadata they might
73
84
  # return to be stored with the metrics.
74
85
  metadata = self._apply_hooks(
75
- hooks=hooks,
76
86
  response=self.result,
77
87
  exception=__exc_val,
78
88
  metric=metric,
@@ -81,17 +91,27 @@ class Timed(AbstractContextManager):
81
91
  metric.update(metadata)
82
92
  write_metric(**metric) # type: ignore[arg-type]
83
93
 
84
- def _apply_hooks(self, hooks, response, exception, metric) -> Dict:
85
- metadata = dict()
86
- for hook in hooks:
87
- hook_result = hook(
88
- response=response,
89
- exception=exception,
90
- metric=metric,
91
- func=self._wrapped,
92
- func_args=self._call_args,
93
- func_kwargs=self._call_kwargs,
94
- )
94
+ def _apply_hooks(self, response, exception, metric) -> Dict:
95
+ metadata: Dict[str, Any] = dict()
96
+ for hook in self._hooks:
97
+ if not isgenerator(hook):
98
+ # Simple exit hook, call it directly.
99
+ hook_result = cast(Hook, hook)(
100
+ response=response,
101
+ exception=exception,
102
+ metric=metric,
103
+ func=self._wrapped,
104
+ func_args=self._call_args,
105
+ func_kwargs=self._call_kwargs,
106
+ )
107
+ else:
108
+ # Generator hook: send the results and obtain custom metadata.
109
+ try:
110
+ hook.send((response, exception, metric))
111
+ except StopIteration as e:
112
+ hook_result = e.value
113
+ else:
114
+ raise RuntimeError("generator hook did not stop")
95
115
  if hook_result:
96
116
  metadata.update(hook_result)
97
117
  return metadata
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: timeexecution
3
- Version: 8.2.0
3
+ Version: 8.3.1
4
4
  Summary: Python project
5
5
  Home-page: https://github.com/kpn/py-timeexecution
6
6
  Author: KPN DE Platform
@@ -207,13 +207,16 @@ With a hook you can add additional and change existing fields. This can
207
207
  be useful for cases where you would like to add a column to the metric
208
208
  based on the response of the wrapped function.
209
209
 
210
- A hook will always get 3 arguments:
210
+ ### Simple callable hook
211
211
 
212
- * `response` - The returned value of the wrapped function
213
- * `exception` - The raised exception of the wrapped function
214
- * `metric` - A dict containing the data to be send to the backend
215
- * `func_args` - Original args received by the wrapped function.
216
- * `func_kwargs` - Original kwargs received by the wrapped function.
212
+ A function which is called after calling the decorated function and before sending the metrics. It receives the following arguments:
213
+
214
+ * `response`: the returned value of the wrapped function
215
+ * `exception`: exception raised the wrapped function, if any
216
+ * `metric`: a dictionary containing the data to be sent to the backend
217
+ * `func`: the decorated function itself
218
+ * `func_args`: original args received by the wrapped function.
219
+ * `func_kwargs`: original kwargs received by the wrapped function.
217
220
 
218
221
  From within a hook you can change the `name` if you want the metrics to be
219
222
  split into multiple series.
@@ -275,6 +278,23 @@ def celery_task(self, **kwargs):
275
278
  return True
276
279
  ```
277
280
 
281
+ ### Generator hook
282
+
283
+ A generator hook works similarly to a simple hook, but also allows to run an arbitrary code just before the decorated function is called.
284
+ It receives `func`, `func_args`, and `func_kwargs` as its arguments, whilst `response`, `exception`, and `metric` are sent later at the `yield` site:
285
+
286
+ ```python
287
+ def generator_hook(func, func_args, func_kwargs) -> Generator[
288
+ None,
289
+ Tuple[Any, Optional[BaseException], Dict[str, Any]], # response, exception, and metrics
290
+ Optional[Dict[str, Any]],
291
+ ]:
292
+ print("This runs before the decorated function")
293
+ (response, exception, metrics) = yield
294
+ print("This runs after the decorated function")
295
+ ...
296
+ ```
297
+
278
298
  ## Manually sending metrics
279
299
 
280
300
  You can also send any metric you have manually to the backend. These
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes