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.
- {timeexecution-8.3.0/timeexecution.egg-info → timeexecution-8.4.0}/PKG-INFO +1 -1
- {timeexecution-8.3.0 → timeexecution-8.4.0}/tests/test_elasticsearch.py +10 -3
- {timeexecution-8.3.0 → timeexecution-8.4.0}/tests/test_hooks.py +31 -1
- {timeexecution-8.3.0 → timeexecution-8.4.0}/time_execution/decorator.py +6 -2
- timeexecution-8.4.0/time_execution/timed.py +168 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0/timeexecution.egg-info}/PKG-INFO +1 -1
- timeexecution-8.3.0/time_execution/timed.py +0 -117
- {timeexecution-8.3.0 → timeexecution-8.4.0}/.editorconfig +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/.gitchangelog.rc +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/.github/workflows/lint.yml +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/.github/workflows/publish.yml +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/.github/workflows/tests.yml +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/.gitignore +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/CHANGELOG.md +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/LICENSE +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/MANIFEST.in +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/Makefile +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/README.md +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/pyproject.toml +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/requirements.txt +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/setup.cfg +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/setup.py +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/tests/__init__.py +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/tests/conftest.py +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/tests/dummy_process.py +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/tests/test_base_backend.py +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/tests/test_decorator_async.py +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/tests/test_threaded_backend.py +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/time_execution/__init__.py +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/time_execution/backends/__init__.py +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/time_execution/backends/base.py +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/time_execution/backends/elasticsearch.py +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/time_execution/backends/threaded.py +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/time_execution/py.typed +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/timeexecution.egg-info/SOURCES.txt +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/timeexecution.egg-info/dependency_links.txt +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/timeexecution.egg-info/not-zip-safe +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/timeexecution.egg-info/requires.txt +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/timeexecution.egg-info/top_level.txt +0 -0
- {timeexecution-8.3.0 → timeexecution-8.4.0}/tox.ini +0 -0
|
@@ -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
|
-
#
|
|
15
|
-
ELASTICSEARCH_HOST = os.getenv("ELASTICSEARCH_HOST"
|
|
16
|
-
|
|
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
|
|
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,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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|