asyncly 0.4.0__tar.gz → 0.5.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.
- {asyncly-0.4.0 → asyncly-0.5.0}/PKG-INFO +21 -10
- {asyncly-0.4.0 → asyncly-0.5.0}/README.rst +15 -9
- {asyncly-0.4.0 → asyncly-0.5.0}/asyncly/client/base.py +2 -9
- {asyncly-0.4.0 → asyncly-0.5.0}/asyncly/client/handlers/base.py +2 -4
- asyncly-0.5.0/asyncly/client/metrics/instrumentable_client.py +148 -0
- asyncly-0.5.0/asyncly/client/metrics/route_resolver.py +13 -0
- asyncly-0.5.0/asyncly/client/metrics/sinks/base.py +14 -0
- asyncly-0.5.0/asyncly/client/metrics/sinks/noop.py +14 -0
- asyncly-0.5.0/asyncly/client/metrics/sinks/opentelemetry.py +52 -0
- asyncly-0.5.0/asyncly/client/metrics/sinks/prometheus.py +64 -0
- {asyncly-0.4.0 → asyncly-0.5.0}/asyncly/client/timeout.py +1 -1
- asyncly-0.5.0/asyncly/client/typing.py +21 -0
- asyncly-0.5.0/asyncly/srvmocker/responses/__init__.py +0 -0
- asyncly-0.5.0/asyncly/srvmocker/serialization/__init__.py +0 -0
- {asyncly-0.4.0 → asyncly-0.5.0}/pyproject.toml +24 -1
- {asyncly-0.4.0 → asyncly-0.5.0}/.gitignore +0 -0
- {asyncly-0.4.0 → asyncly-0.5.0}/asyncly/__init__.py +0 -0
- {asyncly-0.4.0 → asyncly-0.5.0}/asyncly/client/__init__.py +0 -0
- {asyncly-0.4.0 → asyncly-0.5.0}/asyncly/client/handlers/__init__.py +0 -0
- {asyncly-0.4.0 → asyncly-0.5.0}/asyncly/client/handlers/exceptions.py +0 -0
- {asyncly-0.4.0 → asyncly-0.5.0}/asyncly/client/handlers/json.py +0 -0
- {asyncly-0.4.0 → asyncly-0.5.0}/asyncly/client/handlers/msgspec.py +0 -0
- {asyncly-0.4.0 → asyncly-0.5.0}/asyncly/client/handlers/pydantic.py +0 -0
- {asyncly-0.4.0/asyncly/srvmocker/responses → asyncly-0.5.0/asyncly/client/metrics}/__init__.py +0 -0
- {asyncly-0.4.0/asyncly/srvmocker/serialization → asyncly-0.5.0/asyncly/client/metrics/sinks}/__init__.py +0 -0
- {asyncly-0.4.0 → asyncly-0.5.0}/asyncly/py.typed +0 -0
- {asyncly-0.4.0 → asyncly-0.5.0}/asyncly/srvmocker/__init__.py +0 -0
- {asyncly-0.4.0 → asyncly-0.5.0}/asyncly/srvmocker/constants.py +0 -0
- {asyncly-0.4.0 → asyncly-0.5.0}/asyncly/srvmocker/handlers.py +0 -0
- {asyncly-0.4.0 → asyncly-0.5.0}/asyncly/srvmocker/models.py +0 -0
- {asyncly-0.4.0 → asyncly-0.5.0}/asyncly/srvmocker/responses/base.py +0 -0
- {asyncly-0.4.0 → asyncly-0.5.0}/asyncly/srvmocker/responses/content.py +0 -0
- {asyncly-0.4.0 → asyncly-0.5.0}/asyncly/srvmocker/responses/json.py +0 -0
- {asyncly-0.4.0 → asyncly-0.5.0}/asyncly/srvmocker/responses/msgpack.py +0 -0
- {asyncly-0.4.0 → asyncly-0.5.0}/asyncly/srvmocker/responses/sequence.py +0 -0
- {asyncly-0.4.0 → asyncly-0.5.0}/asyncly/srvmocker/responses/timeout.py +0 -0
- {asyncly-0.4.0 → asyncly-0.5.0}/asyncly/srvmocker/responses/toml.py +0 -0
- {asyncly-0.4.0 → asyncly-0.5.0}/asyncly/srvmocker/responses/yaml.py +0 -0
- {asyncly-0.4.0 → asyncly-0.5.0}/asyncly/srvmocker/serialization/base.py +0 -0
- {asyncly-0.4.0 → asyncly-0.5.0}/asyncly/srvmocker/serialization/json.py +0 -0
- {asyncly-0.4.0 → asyncly-0.5.0}/asyncly/srvmocker/serialization/msgpack.py +0 -0
- {asyncly-0.4.0 → asyncly-0.5.0}/asyncly/srvmocker/serialization/toml.py +0 -0
- {asyncly-0.4.0 → asyncly-0.5.0}/asyncly/srvmocker/serialization/yaml.py +0 -0
- {asyncly-0.4.0 → asyncly-0.5.0}/asyncly/srvmocker/service.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: asyncly
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Simple HTTP client and server for your integrations based on aiohttp
|
|
5
5
|
Project-URL: Homepage, https://github.com/andy-takker/asyncly
|
|
6
6
|
Project-URL: Source, https://github.com/andy-takker/asyncly
|
|
@@ -22,6 +22,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.10
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.11
|
|
24
24
|
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
26
|
Classifier: Topic :: Internet
|
|
26
27
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
27
28
|
Classifier: Topic :: Software Development
|
|
@@ -31,8 +32,12 @@ Requires-Python: <4,>=3.10
|
|
|
31
32
|
Requires-Dist: aiohttp<4,>=3.9.5
|
|
32
33
|
Provides-Extra: msgspec
|
|
33
34
|
Requires-Dist: msgspec<0.20,>=0.19.0; extra == 'msgspec'
|
|
35
|
+
Provides-Extra: opentelemetry
|
|
36
|
+
Requires-Dist: opentelemetry-sdk>=1.37.0; extra == 'opentelemetry'
|
|
34
37
|
Provides-Extra: orjson
|
|
35
38
|
Requires-Dist: orjson<4,>=3.10.6; extra == 'orjson'
|
|
39
|
+
Provides-Extra: prometheus
|
|
40
|
+
Requires-Dist: prometheus-client>=0.22.1; extra == 'prometheus'
|
|
36
41
|
Provides-Extra: pydantic
|
|
37
42
|
Requires-Dist: pydantic<3,>=2.8.2; extra == 'pydantic'
|
|
38
43
|
Description-Content-Type: text/x-rst
|
|
@@ -84,15 +89,19 @@ For example, with msgspec_:
|
|
|
84
89
|
|
|
85
90
|
Complete table of extras below:
|
|
86
91
|
|
|
87
|
-
|
|
88
|
-
| example
|
|
89
|
-
|
|
90
|
-
| ``pip install "asyncly[msgspec]"``
|
|
91
|
-
|
|
92
|
-
| ``pip install "asyncly[orjson]"``
|
|
93
|
-
|
|
94
|
-
| ``pip install "asyncly[pydantic]"``
|
|
95
|
-
|
|
92
|
+
+------------------------------------------+-----------------------------------+
|
|
93
|
+
| example | description |
|
|
94
|
+
+==========================================+===================================+
|
|
95
|
+
| ``pip install "asyncly[msgspec]"`` | For using msgspec_ structs |
|
|
96
|
+
+------------------------------------------+-----------------------------------+
|
|
97
|
+
| ``pip install "asyncly[orjson]"`` | For fast parsing json by orjson_ |
|
|
98
|
+
+------------------------------------------+-----------------------------------+
|
|
99
|
+
| ``pip install "asyncly[pydantic]"`` | For using pydantic_ models |
|
|
100
|
+
+------------------------------------------+-----------------------------------+
|
|
101
|
+
| ``pip install "asyncly[prometheus]"`` | To collect Prometheus_ metrics |
|
|
102
|
+
+------------------------------------------+-----------------------------------+
|
|
103
|
+
| ``pip install "asyncly[opentelemetry]"`` | To collect OpenTelemetry_ metrics |
|
|
104
|
+
+------------------------------------------+-----------------------------------+
|
|
96
105
|
|
|
97
106
|
Quick start guide
|
|
98
107
|
-----------------
|
|
@@ -218,6 +227,8 @@ Useful responses and serializers
|
|
|
218
227
|
.. _msgspec: https://github.com/jcrist/msgspec
|
|
219
228
|
.. _orjson: https://github.com/ijl/orjson
|
|
220
229
|
.. _pydantic: https://github.com/pydantic/pydantic
|
|
230
|
+
.. _Prometheus: https://prometheus.io
|
|
231
|
+
.. _OpenTelemetry: https://opentelemetry.io
|
|
221
232
|
|
|
222
233
|
.. _examples/catfact_client.py: https://github.com/andy-takker/asyncly/blob/master/examples/catfact_client.py
|
|
223
234
|
.. _examples/test_catfact_client.py: https://github.com/andy-takker/asyncly/blob/master/examples/test_catfact_client.py
|
|
@@ -45,15 +45,19 @@ For example, with msgspec_:
|
|
|
45
45
|
|
|
46
46
|
Complete table of extras below:
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
| example
|
|
50
|
-
|
|
51
|
-
| ``pip install "asyncly[msgspec]"``
|
|
52
|
-
|
|
53
|
-
| ``pip install "asyncly[orjson]"``
|
|
54
|
-
|
|
55
|
-
| ``pip install "asyncly[pydantic]"``
|
|
56
|
-
|
|
48
|
+
+------------------------------------------+-----------------------------------+
|
|
49
|
+
| example | description |
|
|
50
|
+
+==========================================+===================================+
|
|
51
|
+
| ``pip install "asyncly[msgspec]"`` | For using msgspec_ structs |
|
|
52
|
+
+------------------------------------------+-----------------------------------+
|
|
53
|
+
| ``pip install "asyncly[orjson]"`` | For fast parsing json by orjson_ |
|
|
54
|
+
+------------------------------------------+-----------------------------------+
|
|
55
|
+
| ``pip install "asyncly[pydantic]"`` | For using pydantic_ models |
|
|
56
|
+
+------------------------------------------+-----------------------------------+
|
|
57
|
+
| ``pip install "asyncly[prometheus]"`` | To collect Prometheus_ metrics |
|
|
58
|
+
+------------------------------------------+-----------------------------------+
|
|
59
|
+
| ``pip install "asyncly[opentelemetry]"`` | To collect OpenTelemetry_ metrics |
|
|
60
|
+
+------------------------------------------+-----------------------------------+
|
|
57
61
|
|
|
58
62
|
Quick start guide
|
|
59
63
|
-----------------
|
|
@@ -179,6 +183,8 @@ Useful responses and serializers
|
|
|
179
183
|
.. _msgspec: https://github.com/jcrist/msgspec
|
|
180
184
|
.. _orjson: https://github.com/ijl/orjson
|
|
181
185
|
.. _pydantic: https://github.com/pydantic/pydantic
|
|
186
|
+
.. _Prometheus: https://prometheus.io
|
|
187
|
+
.. _OpenTelemetry: https://opentelemetry.io
|
|
182
188
|
|
|
183
189
|
.. _examples/catfact_client.py: https://github.com/andy-takker/asyncly/blob/master/examples/catfact_client.py
|
|
184
190
|
.. _examples/test_catfact_client.py: https://github.com/andy-takker/asyncly/blob/master/examples/test_catfact_client.py
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
from typing import Any, Literal
|
|
1
|
+
from typing import Any
|
|
3
2
|
|
|
4
3
|
from aiohttp import ClientSession
|
|
5
4
|
from aiohttp.client import DEFAULT_TIMEOUT
|
|
@@ -10,13 +9,7 @@ from asyncly.client.handlers.base import (
|
|
|
10
9
|
apply_handler,
|
|
11
10
|
)
|
|
12
11
|
from asyncly.client.timeout import TimeoutType, get_timeout
|
|
13
|
-
|
|
14
|
-
if sys.version_info >= (3, 11):
|
|
15
|
-
from http import HTTPMethod
|
|
16
|
-
|
|
17
|
-
MethodType = HTTPMethod | Literal["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD"]
|
|
18
|
-
else:
|
|
19
|
-
MethodType = Literal["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD"]
|
|
12
|
+
from asyncly.client.typing import MethodType
|
|
20
13
|
|
|
21
14
|
|
|
22
15
|
class BaseHttpClient:
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
from collections.abc import Callable
|
|
2
|
-
from http import HTTPStatus
|
|
1
|
+
from collections.abc import Callable
|
|
3
2
|
from typing import Any
|
|
4
3
|
|
|
5
4
|
from aiohttp import ClientResponse
|
|
6
5
|
|
|
7
6
|
from asyncly.client.handlers.exceptions import UnhandledStatusException
|
|
8
|
-
|
|
9
|
-
ResponseHandlersType = Mapping[HTTPStatus | int | str, Callable]
|
|
7
|
+
from asyncly.client.typing import ResponseHandlersType
|
|
10
8
|
|
|
11
9
|
|
|
12
10
|
async def apply_handler(
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from http import HTTPStatus
|
|
2
|
+
from time import perf_counter
|
|
3
|
+
from types import TracebackType
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from aiohttp import ClientResponse, ClientSession
|
|
7
|
+
from aiohttp.client import DEFAULT_TIMEOUT
|
|
8
|
+
from yarl import URL
|
|
9
|
+
|
|
10
|
+
from asyncly.client.base import BaseHttpClient, MethodType
|
|
11
|
+
from asyncly.client.metrics.route_resolver import default_route_resolver
|
|
12
|
+
from asyncly.client.metrics.sinks.base import MetricsSink
|
|
13
|
+
from asyncly.client.metrics.sinks.noop import NoopSink
|
|
14
|
+
from asyncly.client.timeout import TimeoutType
|
|
15
|
+
from asyncly.client.typing import ResponseHandler, ResponseHandlersType, RouteResolver
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class InstrumentableHttpClient(BaseHttpClient):
|
|
19
|
+
__slots__ = ("_metrics_sink", "_resolve_route") + BaseHttpClient.__slots__
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
url: URL | str,
|
|
24
|
+
session: ClientSession,
|
|
25
|
+
client_name: str,
|
|
26
|
+
) -> None:
|
|
27
|
+
super().__init__(url=url, session=session, client_name=client_name)
|
|
28
|
+
self._metrics_sink: MetricsSink = NoopSink()
|
|
29
|
+
self._resolve_route: RouteResolver = default_route_resolver
|
|
30
|
+
|
|
31
|
+
def enable_metrics(
|
|
32
|
+
self, sink: MetricsSink, *, route_resolver: RouteResolver | None = None
|
|
33
|
+
) -> None:
|
|
34
|
+
self._metrics_sink = sink
|
|
35
|
+
if route_resolver is not None:
|
|
36
|
+
self._resolve_route = route_resolver
|
|
37
|
+
|
|
38
|
+
def disable_metrics(self) -> None:
|
|
39
|
+
self._metrics_sink = NoopSink()
|
|
40
|
+
self._resolve_route = default_route_resolver
|
|
41
|
+
|
|
42
|
+
def instrument( # type: ignore[no-untyped-def]
|
|
43
|
+
self, sink: MetricsSink, *, route_resolver: RouteResolver | None = None
|
|
44
|
+
):
|
|
45
|
+
client = self
|
|
46
|
+
|
|
47
|
+
class _Ctx:
|
|
48
|
+
def __enter__(self) -> "InstrumentableHttpClient":
|
|
49
|
+
self._prev_sink = client._metrics_sink
|
|
50
|
+
self._prev_resolver = client._resolve_route
|
|
51
|
+
client.enable_metrics(sink, route_resolver=route_resolver)
|
|
52
|
+
return client
|
|
53
|
+
|
|
54
|
+
def __exit__(
|
|
55
|
+
self,
|
|
56
|
+
exc_type: type[BaseException] | None,
|
|
57
|
+
exc: BaseException | None,
|
|
58
|
+
tb: TracebackType | None,
|
|
59
|
+
) -> None:
|
|
60
|
+
client._metrics_sink = self._prev_sink
|
|
61
|
+
client._resolve_route = self._prev_resolver
|
|
62
|
+
|
|
63
|
+
return _Ctx()
|
|
64
|
+
|
|
65
|
+
async def _make_req(
|
|
66
|
+
self,
|
|
67
|
+
/,
|
|
68
|
+
method: MethodType,
|
|
69
|
+
url: URL,
|
|
70
|
+
handlers: ResponseHandlersType,
|
|
71
|
+
timeout: TimeoutType = DEFAULT_TIMEOUT,
|
|
72
|
+
**kwargs: Any,
|
|
73
|
+
) -> Any:
|
|
74
|
+
# Быстрый путь: метрики Noop → почти нулевая накладная
|
|
75
|
+
sink = self._metrics_sink
|
|
76
|
+
if isinstance(sink, NoopSink):
|
|
77
|
+
return await super()._make_req(
|
|
78
|
+
method=method,
|
|
79
|
+
url=url,
|
|
80
|
+
handlers=handlers,
|
|
81
|
+
timeout=timeout,
|
|
82
|
+
**kwargs,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
route_label = self._resolve_route(url)
|
|
86
|
+
start = perf_counter()
|
|
87
|
+
chosen_status: dict[str, int | HTTPStatus | str | None] = {"value": None}
|
|
88
|
+
|
|
89
|
+
# Заворачиваем хэндлеры, чтобы знать какой статус сработал
|
|
90
|
+
wrapped_handlers = _wrap_handlers_with_status_mark(handlers, chosen_status)
|
|
91
|
+
|
|
92
|
+
error_type: str | None = None
|
|
93
|
+
status_for_metrics: int | str = "unknown"
|
|
94
|
+
try:
|
|
95
|
+
result = await super()._make_req(
|
|
96
|
+
method=method, url=url, handlers=wrapped_handlers, timeout=timeout
|
|
97
|
+
)
|
|
98
|
+
v = chosen_status["value"]
|
|
99
|
+
if isinstance(v, HTTPStatus):
|
|
100
|
+
status_for_metrics = int(v)
|
|
101
|
+
elif isinstance(v, int):
|
|
102
|
+
status_for_metrics = v
|
|
103
|
+
else:
|
|
104
|
+
status_for_metrics = "ok"
|
|
105
|
+
return result
|
|
106
|
+
except Exception as e:
|
|
107
|
+
status = (
|
|
108
|
+
chosen_status["value"]
|
|
109
|
+
or getattr(e, "status", None)
|
|
110
|
+
or getattr(e, "status_code", None)
|
|
111
|
+
)
|
|
112
|
+
status_for_metrics = int(status) if isinstance(status, int) else "exception"
|
|
113
|
+
error_type = type(e).__name__
|
|
114
|
+
raise
|
|
115
|
+
finally:
|
|
116
|
+
duration = perf_counter() - start
|
|
117
|
+
sink.observe_request(
|
|
118
|
+
client=self._client_name,
|
|
119
|
+
method=method,
|
|
120
|
+
route=route_label,
|
|
121
|
+
status=status_for_metrics,
|
|
122
|
+
duration_seconds=duration,
|
|
123
|
+
error_type=error_type,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _wrap_handlers_with_status_mark(
|
|
128
|
+
handlers: ResponseHandlersType,
|
|
129
|
+
chosen_status: dict[str, int | HTTPStatus | str | None],
|
|
130
|
+
) -> ResponseHandlersType:
|
|
131
|
+
try:
|
|
132
|
+
wrapped: dict[int | HTTPStatus | str, ResponseHandler] = {}
|
|
133
|
+
for k, handler in handlers.items():
|
|
134
|
+
wrapped[k] = _wrap_one(handler, chosen_status)
|
|
135
|
+
return wrapped
|
|
136
|
+
except AttributeError:
|
|
137
|
+
return handlers
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _wrap_one(
|
|
141
|
+
handler: ResponseHandler,
|
|
142
|
+
chosen_status: dict[str, int | HTTPStatus | str | None],
|
|
143
|
+
) -> ResponseHandler:
|
|
144
|
+
async def _wrapped(response: ClientResponse) -> Any:
|
|
145
|
+
chosen_status["value"] = response.status
|
|
146
|
+
return await handler(response)
|
|
147
|
+
|
|
148
|
+
return _wrapped
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from yarl import URL
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def default_route_resolver(url: URL) -> str:
|
|
5
|
+
parts: list[str] = []
|
|
6
|
+
for p in url.path.split("/"):
|
|
7
|
+
if not p:
|
|
8
|
+
continue
|
|
9
|
+
if p.isdigit() or (len(p) in (8, 16, 32, 36) and any(ch.isalpha() for ch in p)):
|
|
10
|
+
parts.append(":id")
|
|
11
|
+
else:
|
|
12
|
+
parts.append(p)
|
|
13
|
+
return "/" + "/".join(parts) if parts else "/"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from opentelemetry.metrics import Meter
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class OpenTelemetrySink:
|
|
5
|
+
def __init__(self, meter: Meter) -> None:
|
|
6
|
+
# Counter: количество
|
|
7
|
+
self._req_counter = meter.create_counter(
|
|
8
|
+
name="http_client_requests_total",
|
|
9
|
+
unit="1",
|
|
10
|
+
description="Total HTTP client requests",
|
|
11
|
+
)
|
|
12
|
+
# Histogram: длительность
|
|
13
|
+
self._req_hist = meter.create_histogram(
|
|
14
|
+
name="http_client_request_seconds",
|
|
15
|
+
unit="s",
|
|
16
|
+
description="HTTP client request duration including handler",
|
|
17
|
+
)
|
|
18
|
+
# Counter: ошибки
|
|
19
|
+
self._err_counter = meter.create_counter(
|
|
20
|
+
name="http_client_errors_total",
|
|
21
|
+
unit="1",
|
|
22
|
+
description="Total HTTP client errors",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def observe_request(
|
|
26
|
+
self,
|
|
27
|
+
*,
|
|
28
|
+
client: str,
|
|
29
|
+
method: str,
|
|
30
|
+
route: str,
|
|
31
|
+
status: int | str,
|
|
32
|
+
duration_seconds: float,
|
|
33
|
+
error_type: str | None = None,
|
|
34
|
+
) -> None:
|
|
35
|
+
attrs = {
|
|
36
|
+
"client": client,
|
|
37
|
+
"method": method,
|
|
38
|
+
"route": route,
|
|
39
|
+
"status": str(status),
|
|
40
|
+
}
|
|
41
|
+
self._req_counter.add(1, attributes=attrs)
|
|
42
|
+
self._req_hist.record(duration_seconds, attributes=attrs)
|
|
43
|
+
if error_type:
|
|
44
|
+
self._err_counter.add(
|
|
45
|
+
1,
|
|
46
|
+
attributes={
|
|
47
|
+
"client": client,
|
|
48
|
+
"method": method,
|
|
49
|
+
"route": route,
|
|
50
|
+
"error_type": error_type,
|
|
51
|
+
},
|
|
52
|
+
)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
|
|
3
|
+
from prometheus_client import Counter, Histogram
|
|
4
|
+
from prometheus_client.registry import REGISTRY, CollectorRegistry
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PrometheusSink:
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
namespace: str = "asyncly",
|
|
11
|
+
subsystem: str = "client",
|
|
12
|
+
buckets: Iterable[float] = (
|
|
13
|
+
0.005,
|
|
14
|
+
0.01,
|
|
15
|
+
0.025,
|
|
16
|
+
0.05,
|
|
17
|
+
0.1,
|
|
18
|
+
0.25,
|
|
19
|
+
0.5,
|
|
20
|
+
1.0,
|
|
21
|
+
2.5,
|
|
22
|
+
5.0,
|
|
23
|
+
10.0,
|
|
24
|
+
),
|
|
25
|
+
registry: CollectorRegistry = REGISTRY,
|
|
26
|
+
) -> None:
|
|
27
|
+
metric_prefix = f"{namespace}_{subsystem}"
|
|
28
|
+
self._latency = Histogram(
|
|
29
|
+
f"{metric_prefix}_request_seconds",
|
|
30
|
+
"HTTP client request duration including handler",
|
|
31
|
+
("client", "method", "route", "status"),
|
|
32
|
+
buckets=tuple(buckets),
|
|
33
|
+
registry=registry,
|
|
34
|
+
)
|
|
35
|
+
self._total = Counter(
|
|
36
|
+
f"{metric_prefix}_requests_total",
|
|
37
|
+
"Total HTTP client requests",
|
|
38
|
+
("client", "method", "route", "status"),
|
|
39
|
+
registry=registry,
|
|
40
|
+
)
|
|
41
|
+
self._errors = Counter(
|
|
42
|
+
f"{metric_prefix}_errors_total",
|
|
43
|
+
"Total HTTP client errors",
|
|
44
|
+
("client", "method", "route", "error_type"),
|
|
45
|
+
registry=registry,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def observe_request(
|
|
49
|
+
self,
|
|
50
|
+
*,
|
|
51
|
+
client: str,
|
|
52
|
+
method: str,
|
|
53
|
+
route: str,
|
|
54
|
+
status: int | str,
|
|
55
|
+
duration_seconds: float,
|
|
56
|
+
error_type: str | None = None,
|
|
57
|
+
) -> None:
|
|
58
|
+
status_label = str(status)
|
|
59
|
+
self._total.labels(client, method, route, status_label).inc()
|
|
60
|
+
self._latency.labels(client, method, route, status_label).observe(
|
|
61
|
+
duration_seconds
|
|
62
|
+
)
|
|
63
|
+
if error_type:
|
|
64
|
+
self._errors.labels(client, method, route, error_type).inc()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from collections.abc import Callable, Mapping
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
from http import HTTPStatus
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from aiohttp import ClientTimeout
|
|
8
|
+
from yarl import URL
|
|
9
|
+
|
|
10
|
+
ResponseHandler = Callable
|
|
11
|
+
ResponseHandlersType = Mapping[HTTPStatus | int | str, ResponseHandler]
|
|
12
|
+
|
|
13
|
+
TimeoutType = ClientTimeout | timedelta | int | float
|
|
14
|
+
RouteResolver = Callable[[URL], str]
|
|
15
|
+
|
|
16
|
+
if sys.version_info >= (3, 11):
|
|
17
|
+
from http import HTTPMethod
|
|
18
|
+
|
|
19
|
+
MethodType = HTTPMethod | Literal["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD"]
|
|
20
|
+
else:
|
|
21
|
+
MethodType = Literal["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD"]
|
|
File without changes
|
|
File without changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "asyncly"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.0"
|
|
4
4
|
description = "Simple HTTP client and server for your integrations based on aiohttp"
|
|
5
5
|
authors = [{ name = "Sergey Natalenko", email = "sergey.natalenko@mail.ru" }]
|
|
6
6
|
requires-python = ">=3.10, <4"
|
|
@@ -26,6 +26,7 @@ classifiers = [
|
|
|
26
26
|
"Programming Language :: Python :: 3.10",
|
|
27
27
|
"Programming Language :: Python :: 3.11",
|
|
28
28
|
"Programming Language :: Python :: 3.12",
|
|
29
|
+
"Programming Language :: Python :: 3.13",
|
|
29
30
|
"Topic :: Internet",
|
|
30
31
|
"Topic :: Internet :: WWW/HTTP",
|
|
31
32
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
@@ -38,6 +39,12 @@ dependencies = ["aiohttp>=3.9.5,<4"]
|
|
|
38
39
|
msgspec = ["msgspec>=0.19.0,<0.20"]
|
|
39
40
|
pydantic = ["pydantic>=2.8.2,<3"]
|
|
40
41
|
orjson = ["orjson>=3.10.6,<4"]
|
|
42
|
+
prometheus = [
|
|
43
|
+
"prometheus-client>=0.22.1",
|
|
44
|
+
]
|
|
45
|
+
opentelemetry = [
|
|
46
|
+
"opentelemetry-sdk>=1.37.0",
|
|
47
|
+
]
|
|
41
48
|
|
|
42
49
|
[project.urls]
|
|
43
50
|
Homepage = "https://github.com/andy-takker/asyncly"
|
|
@@ -82,7 +89,23 @@ source = ["asyncly"]
|
|
|
82
89
|
command_line = "-m pytest"
|
|
83
90
|
|
|
84
91
|
[tool.coverage.report]
|
|
92
|
+
fail_under = 90
|
|
85
93
|
show_missing = true
|
|
94
|
+
skip_covered = false
|
|
95
|
+
skip_empty = true
|
|
96
|
+
exclude_also = [
|
|
97
|
+
"def __repr__",
|
|
98
|
+
"if self.debug:",
|
|
99
|
+
"if settings.DEBUG",
|
|
100
|
+
"raise AssertionError",
|
|
101
|
+
"raise NotImplementedError",
|
|
102
|
+
"if 0:",
|
|
103
|
+
"if __name__ == __main__:",
|
|
104
|
+
"if TYPE_CHECKING:",
|
|
105
|
+
"class .*\\bProtocol\\):",
|
|
106
|
+
"@(abc\\.)?abstractmethod",
|
|
107
|
+
"pass",
|
|
108
|
+
]
|
|
86
109
|
|
|
87
110
|
[tool.coverage.xml]
|
|
88
111
|
output = "coverage.xml"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{asyncly-0.4.0/asyncly/srvmocker/responses → asyncly-0.5.0/asyncly/client/metrics}/__init__.py
RENAMED
|
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
|