timeexecution 8.3.0__tar.gz → 8.4.0__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 (40) hide show
  1. {timeexecution-8.3.0/timeexecution.egg-info → timeexecution-8.4.0}/PKG-INFO +1 -1
  2. {timeexecution-8.3.0 → timeexecution-8.4.0}/tests/test_elasticsearch.py +10 -3
  3. {timeexecution-8.3.0 → timeexecution-8.4.0}/tests/test_hooks.py +31 -1
  4. {timeexecution-8.3.0 → timeexecution-8.4.0}/time_execution/decorator.py +6 -2
  5. timeexecution-8.4.0/time_execution/timed.py +168 -0
  6. {timeexecution-8.3.0 → timeexecution-8.4.0/timeexecution.egg-info}/PKG-INFO +1 -1
  7. timeexecution-8.3.0/time_execution/timed.py +0 -117
  8. {timeexecution-8.3.0 → timeexecution-8.4.0}/.editorconfig +0 -0
  9. {timeexecution-8.3.0 → timeexecution-8.4.0}/.gitchangelog.rc +0 -0
  10. {timeexecution-8.3.0 → timeexecution-8.4.0}/.github/workflows/lint.yml +0 -0
  11. {timeexecution-8.3.0 → timeexecution-8.4.0}/.github/workflows/publish.yml +0 -0
  12. {timeexecution-8.3.0 → timeexecution-8.4.0}/.github/workflows/tests.yml +0 -0
  13. {timeexecution-8.3.0 → timeexecution-8.4.0}/.gitignore +0 -0
  14. {timeexecution-8.3.0 → timeexecution-8.4.0}/CHANGELOG.md +0 -0
  15. {timeexecution-8.3.0 → timeexecution-8.4.0}/LICENSE +0 -0
  16. {timeexecution-8.3.0 → timeexecution-8.4.0}/MANIFEST.in +0 -0
  17. {timeexecution-8.3.0 → timeexecution-8.4.0}/Makefile +0 -0
  18. {timeexecution-8.3.0 → timeexecution-8.4.0}/README.md +0 -0
  19. {timeexecution-8.3.0 → timeexecution-8.4.0}/pyproject.toml +0 -0
  20. {timeexecution-8.3.0 → timeexecution-8.4.0}/requirements.txt +0 -0
  21. {timeexecution-8.3.0 → timeexecution-8.4.0}/setup.cfg +0 -0
  22. {timeexecution-8.3.0 → timeexecution-8.4.0}/setup.py +0 -0
  23. {timeexecution-8.3.0 → timeexecution-8.4.0}/tests/__init__.py +0 -0
  24. {timeexecution-8.3.0 → timeexecution-8.4.0}/tests/conftest.py +0 -0
  25. {timeexecution-8.3.0 → timeexecution-8.4.0}/tests/dummy_process.py +0 -0
  26. {timeexecution-8.3.0 → timeexecution-8.4.0}/tests/test_base_backend.py +0 -0
  27. {timeexecution-8.3.0 → timeexecution-8.4.0}/tests/test_decorator_async.py +0 -0
  28. {timeexecution-8.3.0 → timeexecution-8.4.0}/tests/test_threaded_backend.py +0 -0
  29. {timeexecution-8.3.0 → timeexecution-8.4.0}/time_execution/__init__.py +0 -0
  30. {timeexecution-8.3.0 → timeexecution-8.4.0}/time_execution/backends/__init__.py +0 -0
  31. {timeexecution-8.3.0 → timeexecution-8.4.0}/time_execution/backends/base.py +0 -0
  32. {timeexecution-8.3.0 → timeexecution-8.4.0}/time_execution/backends/elasticsearch.py +0 -0
  33. {timeexecution-8.3.0 → timeexecution-8.4.0}/time_execution/backends/threaded.py +0 -0
  34. {timeexecution-8.3.0 → timeexecution-8.4.0}/time_execution/py.typed +0 -0
  35. {timeexecution-8.3.0 → timeexecution-8.4.0}/timeexecution.egg-info/SOURCES.txt +0 -0
  36. {timeexecution-8.3.0 → timeexecution-8.4.0}/timeexecution.egg-info/dependency_links.txt +0 -0
  37. {timeexecution-8.3.0 → timeexecution-8.4.0}/timeexecution.egg-info/not-zip-safe +0 -0
  38. {timeexecution-8.3.0 → timeexecution-8.4.0}/timeexecution.egg-info/requires.txt +0 -0
  39. {timeexecution-8.3.0 → timeexecution-8.4.0}/timeexecution.egg-info/top_level.txt +0 -0
  40. {timeexecution-8.3.0 → timeexecution-8.4.0}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: timeexecution
3
- Version: 8.3.0
3
+ Version: 8.4.0
4
4
  Summary: Python project
5
5
  Home-page: https://github.com/kpn/py-timeexecution
6
6
  Author: KPN DE Platform
@@ -11,9 +11,16 @@ from tests.test_base_backend import TestBaseBackend
11
11
  from time_execution import settings
12
12
  from time_execution.backends.elasticsearch import ElasticsearchBackend
13
13
 
14
- # This variable is set by tox-docker. See https://tox-docker.readthedocs.io/en/latest/#configuration
15
- ELASTICSEARCH_HOST = os.getenv("ELASTICSEARCH_HOST", "localhost")
16
- ELASTICSEARCH_URI = f"http://{ELASTICSEARCH_HOST}:9200"
14
+ # These variables are set by tox-docker. See https://tox-docker.readthedocs.io/en/latest/#configuration
15
+ ELASTICSEARCH_HOST = os.getenv("ELASTICSEARCH_HOST")
16
+ if not ELASTICSEARCH_HOST:
17
+ raise RuntimeError("`ELASTICSEARCH_HOST` is not set")
18
+
19
+ ELASTICSEARCH_PORT = os.getenv("ELASTICSEARCH_9200_TCP_PORT")
20
+ if not ELASTICSEARCH_PORT:
21
+ raise RuntimeError("`ELASTICSEARCH_9200_TCP_PORT` is not set")
22
+
23
+ ELASTICSEARCH_URI = f"http://{ELASTICSEARCH_HOST}:{ELASTICSEARCH_PORT}"
17
24
 
18
25
 
19
26
  class TestConnectionErrors(TestBaseBackend):
@@ -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 GeneratorHookReturnType, settings, time_execution
5
+ from time_execution import GeneratorHookReturnType, settings, time_execution, time_execution_async
6
6
  from time_execution.backends.base import BaseMetricsBackend
7
7
 
8
8
 
@@ -29,10 +29,40 @@ def local_hook(**kwargs):
29
29
  return dict(local_hook_key="local hook value")
30
30
 
31
31
 
32
+ async def async_local_hook(**kwargs):
33
+ return dict(async_local_hook="async_local_hook value")
34
+
35
+
36
+ async def async_global_hook(**kwargs):
37
+ return dict(async_global_hook="async_global_hook value")
38
+
39
+
32
40
  def global_hook(**kwargs):
33
41
  return dict(global_hook_key="global hook value")
34
42
 
35
43
 
44
+ class TestAsyncHooks:
45
+ pytestmark = pytest.mark.asyncio
46
+
47
+ async def test_async_hooks(self):
48
+ with settings(backends=[CollectorBackend()], hooks=[global_hook, async_global_hook]):
49
+ collector = settings.backends[0]
50
+
51
+ @time_execution_async(extra_hooks=[local_hook, async_local_hook])
52
+ async def func_local_hook(*args, **kwargs):
53
+ return True
54
+
55
+ await func_local_hook()
56
+
57
+ assert len(collector.metrics) == 1
58
+ metadata = collector.metrics[0][func_local_hook.get_fqn()]
59
+ assert metadata["local_hook_key"] == "local hook value"
60
+ assert metadata["global_hook_key"] == "global hook value"
61
+ assert metadata["async_local_hook"] == "async_local_hook value"
62
+ assert metadata["async_global_hook"] == "async_global_hook value"
63
+ collector.clean()
64
+
65
+
36
66
  class TestTimeExecution:
37
67
  def test_custom_hook(self):
38
68
  with settings(backends=[CollectorBackend()], hooks=[global_hook]):
@@ -45,7 +45,7 @@ def time_execution(
45
45
 
46
46
 
47
47
  def time_execution(__wrapped=None, get_fqn: Callable[[Any], str] = fqn_decorators.get_fqn, **kwargs):
48
- from time_execution.timed import Timed # work around the circular dependency
48
+ from time_execution.timed import Timed, TimedAsync # work around the circular dependency
49
49
 
50
50
  def wrap(__wrapped: _F) -> _F:
51
51
  fqn = get_fqn(__wrapped)
@@ -62,7 +62,9 @@ def time_execution(__wrapped=None, get_fqn: Callable[[Any], str] = fqn_decorator
62
62
 
63
63
  @wraps(__wrapped)
64
64
  async def wrapper(*call_args, **call_kwargs):
65
- with Timed(wrapped=__wrapped, call_args=call_args, call_kwargs=call_kwargs, fqn=fqn, **kwargs) as timed:
65
+ async with TimedAsync(
66
+ wrapped=__wrapped, call_args=call_args, call_kwargs=call_kwargs, fqn=fqn, **kwargs
67
+ ) as timed:
66
68
  timed.result = await __wrapped(*call_args, **call_kwargs)
67
69
  return timed.result
68
70
 
@@ -83,6 +85,7 @@ class Hook(Protocol):
83
85
 
84
86
  def __call__(
85
87
  self,
88
+ *,
86
89
  response: Any,
87
90
  exception: Optional[BaseException],
88
91
  metric: Dict[str, Any],
@@ -108,6 +111,7 @@ class GeneratorHook(Protocol):
108
111
 
109
112
  def __call__(
110
113
  self,
114
+ *,
111
115
  func: Callable[..., Any],
112
116
  func_args: Tuple[Any, ...],
113
117
  func_kwargs: Dict[str, Any],
@@ -0,0 +1,168 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+ from contextlib import AbstractAsyncContextManager, AbstractContextManager
5
+ from inspect import iscoroutinefunction, isgenerator, isgeneratorfunction
6
+ from socket import gethostname
7
+ from timeit import default_timer
8
+ from types import TracebackType
9
+ from typing import Any, Callable, Dict, Optional, Tuple, Type, cast
10
+
11
+ from time_execution import GeneratorHook, GeneratorHookReturnType, Hook, settings, write_metric
12
+
13
+ SHORT_HOSTNAME = gethostname()
14
+
15
+
16
+ class Base:
17
+ """
18
+ Base class for context managers encapsulates the shared behaviour to avoid duplicating the code.
19
+ """
20
+
21
+ __slots__ = (
22
+ "result",
23
+ "_wrapped",
24
+ "_fqn",
25
+ "_hooks",
26
+ "_call_args",
27
+ "_call_kwargs",
28
+ "_start_time",
29
+ )
30
+
31
+ def __init__(
32
+ self,
33
+ *,
34
+ wrapped: Callable[..., Any],
35
+ fqn: str,
36
+ call_args: Tuple[Any, ...],
37
+ call_kwargs: Dict[str, Any],
38
+ extra_hooks: Optional[Iterable[Hook | GeneratorHook]] = None,
39
+ disable_default_hooks: bool = False,
40
+ ) -> None:
41
+ self.result: Optional[Any] = None
42
+ self._wrapped = wrapped
43
+ self._fqn = fqn
44
+ self._call_args = call_args
45
+ self._call_kwargs = call_kwargs
46
+
47
+ hooks = extra_hooks or ()
48
+ if not disable_default_hooks:
49
+ hooks = (*settings.hooks, *hooks)
50
+
51
+ self._hooks = tuple(
52
+ (
53
+ cast(Hook, hook)
54
+ if not isgeneratorfunction(hook) # simple hook, we'll call it in the exit
55
+ # For a generator hook, call it. We'll start in the entrance.
56
+ else cast(GeneratorHookReturnType, hook(func=wrapped, func_args=call_args, func_kwargs=call_kwargs))
57
+ )
58
+ for hook in hooks
59
+ )
60
+
61
+ def enter(self) -> Any:
62
+ self._start_time = default_timer()
63
+ for hook in self._hooks:
64
+ if isgenerator(hook):
65
+ hook.send(None) # start a generator hook
66
+ return self
67
+
68
+ def get_metric(self) -> Dict[str, Any]:
69
+ duration_millis = round(default_timer() - self._start_time, 3) * 1000.0
70
+
71
+ metric = {settings.duration_field: duration_millis, "hostname": SHORT_HOSTNAME, "name": self._fqn}
72
+
73
+ origin = getattr(settings, "origin", None)
74
+ if origin:
75
+ metric["origin"] = origin
76
+
77
+ return metric
78
+
79
+ def apply_hook(
80
+ self,
81
+ hook: Any,
82
+ exception: Optional[BaseException],
83
+ metric: Dict[str, Any],
84
+ metadata: Dict[str, Any],
85
+ ) -> None:
86
+ if not isgenerator(hook):
87
+ hook_result = cast(Hook, hook)(
88
+ response=self.result,
89
+ exception=exception,
90
+ metric=metric,
91
+ func=self._wrapped,
92
+ func_args=self._call_args,
93
+ func_kwargs=self._call_kwargs,
94
+ )
95
+ else:
96
+ # Generator hook: send the results and obtain custom metadata.
97
+ try:
98
+ hook.send((self.result, exception, metric))
99
+ except StopIteration as e:
100
+ hook_result = e.value
101
+ else:
102
+ raise RuntimeError("generator hook did not stop")
103
+ if hook_result:
104
+ metadata.update(hook_result)
105
+
106
+
107
+ class Timed(AbstractContextManager, Base):
108
+
109
+ def __enter__(self) -> Timed:
110
+ return self.enter()
111
+
112
+ def __exit__(
113
+ self,
114
+ __exc_type: Optional[Type[BaseException]],
115
+ __exc_val: Optional[BaseException],
116
+ __exc_tb: Optional[TracebackType],
117
+ ) -> None:
118
+
119
+ metadata: Dict[str, Any] = dict()
120
+ metric: Dict[str, Any] = self.get_metric()
121
+
122
+ for hook in self._hooks:
123
+ self.apply_hook(hook=hook, exception=__exc_val, metric=metric, metadata=metadata)
124
+
125
+ metric.update(metadata)
126
+ write_metric(**metric) # type: ignore[arg-type]
127
+
128
+
129
+ class TimedAsync(AbstractAsyncContextManager, Base):
130
+
131
+ async def __aenter__(self) -> Timed:
132
+ return self.enter()
133
+
134
+ async def __aexit__(
135
+ self,
136
+ __exc_type: Optional[Type[BaseException]],
137
+ __exc_val: Optional[BaseException],
138
+ __exc_tb: Optional[TracebackType],
139
+ ) -> None:
140
+
141
+ metadata: Dict[str, Any] = dict()
142
+ metric: Dict[str, Any] = self.get_metric()
143
+
144
+ for hook in self._hooks:
145
+ await self._apply_hook(hook=hook, exception=__exc_val, metric=metric, metadata=metadata)
146
+
147
+ metric.update(metadata)
148
+ write_metric(**metric) # type: ignore[arg-type]
149
+
150
+ async def _apply_hook(
151
+ self,
152
+ hook: Any,
153
+ exception: Optional[BaseException],
154
+ metric: Dict[str, Any],
155
+ metadata: Dict[str, Any],
156
+ ) -> None:
157
+ if iscoroutinefunction(hook):
158
+ hook_result = await hook(
159
+ response=self.result,
160
+ exception=exception,
161
+ metric=metric,
162
+ func=self._wrapped,
163
+ func_args=self._call_args,
164
+ func_kwargs=self._call_kwargs,
165
+ )
166
+ metadata.update(hook_result)
167
+ else:
168
+ self.apply_hook(hook=hook, exception=exception, metric=metric, metadata=metadata)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: timeexecution
3
- Version: 8.3.0
3
+ Version: 8.4.0
4
4
  Summary: Python project
5
5
  Home-page: https://github.com/kpn/py-timeexecution
6
6
  Author: KPN DE Platform
@@ -1,117 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from collections.abc import Iterable
4
- from contextlib import AbstractContextManager
5
- from inspect import isgenerator, isgeneratorfunction
6
- from socket import gethostname
7
- from timeit import default_timer
8
- from types import TracebackType
9
- from typing import Any, Callable, Dict, Optional, Tuple, Type, cast
10
-
11
- from time_execution import GeneratorHook, GeneratorHookReturnType, Hook, settings, write_metric
12
-
13
- SHORT_HOSTNAME = gethostname()
14
-
15
-
16
- class Timed(AbstractContextManager):
17
- """
18
- Both the sync and async decorators require the same logic around the wrapped function.
19
- This context manager encapsulates the shared behaviour to avoid duplicating the code.
20
- """
21
-
22
- __slots__ = (
23
- "result",
24
- "_wrapped",
25
- "_fqn",
26
- "_hooks",
27
- "_call_args",
28
- "_call_kwargs",
29
- "_start_time",
30
- )
31
-
32
- def __init__(
33
- self,
34
- *,
35
- wrapped: Callable[..., Any],
36
- fqn: str,
37
- call_args: Tuple[Any, ...],
38
- call_kwargs: Dict[str, Any],
39
- extra_hooks: Optional[Iterable[Hook | GeneratorHook]] = None,
40
- disable_default_hooks: bool = False,
41
- ) -> None:
42
- self.result: Optional[Any] = None
43
- self._wrapped = wrapped
44
- self._fqn = fqn
45
- self._call_args = call_args
46
- self._call_kwargs = call_kwargs
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
-
62
- def __enter__(self) -> Timed:
63
- self._start_time = default_timer()
64
- for hook in self._hooks:
65
- if isgenerator(hook):
66
- hook.send(None) # start a generator hook
67
- return self
68
-
69
- def __exit__(
70
- self,
71
- __exc_type: Optional[Type[BaseException]],
72
- __exc_val: Optional[BaseException],
73
- __exc_tb: Optional[TracebackType],
74
- ) -> None:
75
- duration_millis = round(default_timer() - self._start_time, 3) * 1000.0
76
-
77
- metric = {settings.duration_field: duration_millis, "hostname": SHORT_HOSTNAME, "name": self._fqn}
78
-
79
- origin = getattr(settings, "origin", None)
80
- if origin:
81
- metric["origin"] = origin
82
-
83
- # Apply the registered hooks, and collect the metadata they might
84
- # return to be stored with the metrics.
85
- metadata = self._apply_hooks(
86
- response=self.result,
87
- exception=__exc_val,
88
- metric=metric,
89
- )
90
-
91
- metric.update(metadata)
92
- write_metric(**metric) # type: ignore[arg-type]
93
-
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")
115
- if hook_result:
116
- metadata.update(hook_result)
117
- return metadata
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes