asyncly 0.6.2__tar.gz → 0.7.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 (50) hide show
  1. {asyncly-0.6.2 → asyncly-0.7.0}/PKG-INFO +64 -1
  2. {asyncly-0.6.2 → asyncly-0.7.0}/README.rst +63 -0
  3. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/client/base.py +17 -3
  4. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/client/metrics/instrumentable_client.py +12 -3
  5. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/pytest_plugin.py +10 -0
  6. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/srvmocker/__init__.py +3 -0
  7. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/srvmocker/models.py +2 -2
  8. asyncly-0.7.0/asyncly/srvmocker/proxy.py +164 -0
  9. {asyncly-0.6.2 → asyncly-0.7.0}/pyproject.toml +1 -1
  10. {asyncly-0.6.2 → asyncly-0.7.0}/.gitignore +0 -0
  11. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/__init__.py +0 -0
  12. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/client/__init__.py +0 -0
  13. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/client/handlers/__init__.py +0 -0
  14. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/client/handlers/base.py +0 -0
  15. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/client/handlers/exceptions.py +0 -0
  16. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/client/handlers/json.py +0 -0
  17. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/client/handlers/msgspec.py +0 -0
  18. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/client/handlers/pydantic.py +0 -0
  19. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/client/metrics/__init__.py +0 -0
  20. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/client/metrics/route_resolver.py +0 -0
  21. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/client/metrics/sinks/__init__.py +0 -0
  22. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/client/metrics/sinks/base.py +0 -0
  23. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/client/metrics/sinks/noop.py +0 -0
  24. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/client/metrics/sinks/opentelemetry.py +0 -0
  25. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/client/metrics/sinks/prometheus.py +0 -0
  26. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/client/timeout.py +0 -0
  27. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/client/typing.py +0 -0
  28. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/py.typed +0 -0
  29. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/srvmocker/assertions.py +0 -0
  30. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/srvmocker/constants.py +0 -0
  31. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/srvmocker/exceptions.py +0 -0
  32. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/srvmocker/handlers.py +0 -0
  33. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/srvmocker/matching.py +0 -0
  34. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/srvmocker/responses/__init__.py +0 -0
  35. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/srvmocker/responses/base.py +0 -0
  36. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/srvmocker/responses/content.py +0 -0
  37. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/srvmocker/responses/json.py +0 -0
  38. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/srvmocker/responses/msgpack.py +0 -0
  39. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/srvmocker/responses/raw.py +0 -0
  40. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/srvmocker/responses/sequence.py +0 -0
  41. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/srvmocker/responses/timeout.py +0 -0
  42. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/srvmocker/responses/toml.py +0 -0
  43. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/srvmocker/responses/yaml.py +0 -0
  44. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/srvmocker/serialization/__init__.py +0 -0
  45. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/srvmocker/serialization/base.py +0 -0
  46. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/srvmocker/serialization/json.py +0 -0
  47. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/srvmocker/serialization/msgpack.py +0 -0
  48. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/srvmocker/serialization/toml.py +0 -0
  49. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/srvmocker/serialization/yaml.py +0 -0
  50. {asyncly-0.6.2 → asyncly-0.7.0}/asyncly/srvmocker/service.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: asyncly
3
- Version: 0.6.2
3
+ Version: 0.7.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
@@ -383,6 +383,69 @@ Pass an ``ssl.SSLContext`` to ``start_service`` to serve over HTTPS.
383
383
  async with start_service(routes, ssl_context=ctx) as service:
384
384
  ...
385
385
 
386
+ Testing through a proxy
387
+ ~~~~~~~~~~~~~~~~~~~~~~~
388
+
389
+ ``BaseHttpClient`` accepts ``proxy`` and ``proxy_auth`` (forwarded to aiohttp_).
390
+ Set them once on the client, or override per request:
391
+
392
+ .. code-block:: python
393
+
394
+ from aiohttp import BasicAuth, ClientSession
395
+ from asyncly import BaseHttpClient
396
+
397
+ async with ClientSession() as session:
398
+ client = CatfactClient(
399
+ url=url,
400
+ session=session,
401
+ client_name="catfact",
402
+ proxy="http://127.0.0.1:8080",
403
+ proxy_auth=BasicAuth("user", "secret"),
404
+ )
405
+
406
+ To test that a client genuinely routes through a proxy, ``start_proxy`` spins
407
+ up an in-process forwarding HTTP proxy. It records every request passing
408
+ through it and forwards it to the real target (typically another
409
+ ``start_service``). Pair it with the ``mock_proxy`` fixture or use it directly:
410
+
411
+ .. code-block:: python
412
+
413
+ from aiohttp import BasicAuth, ClientSession
414
+ from asyncly.srvmocker import (
415
+ JsonResponse,
416
+ MockRoute,
417
+ start_proxy,
418
+ start_service,
419
+ )
420
+
421
+ async def test_routes_through_proxy() -> None:
422
+ routes = [MockRoute("GET", "/fact", "fact")]
423
+ async with start_service(routes) as target:
424
+ target.register("fact", JsonResponse({"fact": "ok"}))
425
+ async with start_proxy(auth=BasicAuth("user", "secret")) as proxy:
426
+ async with ClientSession() as s:
427
+ resp = await s.get(
428
+ target.url / "fact",
429
+ proxy=proxy.url,
430
+ proxy_auth=BasicAuth("user", "secret"),
431
+ )
432
+ assert (await resp.json()) == {"fact": "ok"}
433
+
434
+ proxy.assert_called(times=1, method="GET")
435
+
436
+ ``MockProxyService`` mirrors ``MockService``'s assertion helpers, reading from
437
+ the recorded history of forwarded requests:
438
+
439
+ - ``get_calls() -> list[RequestHistory]``
440
+ - ``last_call() -> RequestHistory`` (raises ``AssertionError`` if empty)
441
+ - ``assert_called(*, times=, target=, method=, json=, body=, headers=, query=)``
442
+ - ``assert_not_called()``
443
+
444
+ When ``start_proxy(auth=...)`` is set, requests missing or carrying a wrong
445
+ ``Proxy-Authorization`` header get a ``407 Proxy Authentication Required`` and
446
+ are **not** forwarded. Only plain HTTP targets are supported (no ``CONNECT`` /
447
+ HTTPS tunnelling).
448
+
386
449
  .. _PyPI: https://pypi.org/
387
450
  .. _aiohttp: https://pypi.org/project/aiohttp/
388
451
  .. _msgpack: https://msgpack.org
@@ -339,6 +339,69 @@ Pass an ``ssl.SSLContext`` to ``start_service`` to serve over HTTPS.
339
339
  async with start_service(routes, ssl_context=ctx) as service:
340
340
  ...
341
341
 
342
+ Testing through a proxy
343
+ ~~~~~~~~~~~~~~~~~~~~~~~
344
+
345
+ ``BaseHttpClient`` accepts ``proxy`` and ``proxy_auth`` (forwarded to aiohttp_).
346
+ Set them once on the client, or override per request:
347
+
348
+ .. code-block:: python
349
+
350
+ from aiohttp import BasicAuth, ClientSession
351
+ from asyncly import BaseHttpClient
352
+
353
+ async with ClientSession() as session:
354
+ client = CatfactClient(
355
+ url=url,
356
+ session=session,
357
+ client_name="catfact",
358
+ proxy="http://127.0.0.1:8080",
359
+ proxy_auth=BasicAuth("user", "secret"),
360
+ )
361
+
362
+ To test that a client genuinely routes through a proxy, ``start_proxy`` spins
363
+ up an in-process forwarding HTTP proxy. It records every request passing
364
+ through it and forwards it to the real target (typically another
365
+ ``start_service``). Pair it with the ``mock_proxy`` fixture or use it directly:
366
+
367
+ .. code-block:: python
368
+
369
+ from aiohttp import BasicAuth, ClientSession
370
+ from asyncly.srvmocker import (
371
+ JsonResponse,
372
+ MockRoute,
373
+ start_proxy,
374
+ start_service,
375
+ )
376
+
377
+ async def test_routes_through_proxy() -> None:
378
+ routes = [MockRoute("GET", "/fact", "fact")]
379
+ async with start_service(routes) as target:
380
+ target.register("fact", JsonResponse({"fact": "ok"}))
381
+ async with start_proxy(auth=BasicAuth("user", "secret")) as proxy:
382
+ async with ClientSession() as s:
383
+ resp = await s.get(
384
+ target.url / "fact",
385
+ proxy=proxy.url,
386
+ proxy_auth=BasicAuth("user", "secret"),
387
+ )
388
+ assert (await resp.json()) == {"fact": "ok"}
389
+
390
+ proxy.assert_called(times=1, method="GET")
391
+
392
+ ``MockProxyService`` mirrors ``MockService``'s assertion helpers, reading from
393
+ the recorded history of forwarded requests:
394
+
395
+ - ``get_calls() -> list[RequestHistory]``
396
+ - ``last_call() -> RequestHistory`` (raises ``AssertionError`` if empty)
397
+ - ``assert_called(*, times=, target=, method=, json=, body=, headers=, query=)``
398
+ - ``assert_not_called()``
399
+
400
+ When ``start_proxy(auth=...)`` is set, requests missing or carrying a wrong
401
+ ``Proxy-Authorization`` header get a ``407 Proxy Authentication Required`` and
402
+ are **not** forwarded. Only plain HTTP targets are supported (no ``CONNECT`` /
403
+ HTTPS tunnelling).
404
+
342
405
  .. _PyPI: https://pypi.org/
343
406
  .. _aiohttp: https://pypi.org/project/aiohttp/
344
407
  .. _msgpack: https://msgpack.org
@@ -1,6 +1,6 @@
1
1
  from typing import Any
2
2
 
3
- from aiohttp import ClientSession
3
+ from aiohttp import BasicAuth, ClientSession
4
4
  from aiohttp.client import DEFAULT_TIMEOUT
5
5
  from yarl import URL
6
6
 
@@ -13,18 +13,28 @@ from asyncly.client.typing import MethodType
13
13
 
14
14
 
15
15
  class BaseHttpClient:
16
- __slots__ = ("_url", "_session", "_client_name")
16
+ __slots__ = ("_url", "_session", "_client_name", "_proxy", "_proxy_auth")
17
17
 
18
18
  _url: URL
19
19
  _session: ClientSession
20
20
  _client_name: str
21
+ _proxy: URL | None
22
+ _proxy_auth: BasicAuth | None
21
23
 
22
24
  def __init__(
23
- self, url: URL | str, session: ClientSession, client_name: str
25
+ self,
26
+ url: URL | str,
27
+ session: ClientSession,
28
+ client_name: str,
29
+ *,
30
+ proxy: URL | str | None = None,
31
+ proxy_auth: BasicAuth | None = None,
24
32
  ) -> None:
25
33
  self._url = url if isinstance(url, URL) else URL(url)
26
34
  self._session = session
27
35
  self._client_name = client_name
36
+ self._proxy = URL(proxy) if isinstance(proxy, str) else proxy
37
+ self._proxy_auth = proxy_auth
28
38
 
29
39
  @property
30
40
  def url(self) -> URL:
@@ -38,6 +48,10 @@ class BaseHttpClient:
38
48
  timeout: TimeoutType = DEFAULT_TIMEOUT,
39
49
  **kwargs: Any,
40
50
  ) -> Any:
51
+ if "proxy" not in kwargs and self._proxy is not None:
52
+ kwargs["proxy"] = self._proxy
53
+ if "proxy_auth" not in kwargs and self._proxy_auth is not None:
54
+ kwargs["proxy_auth"] = self._proxy_auth
41
55
  async with self._session.request(
42
56
  method=method,
43
57
  url=url,
@@ -3,7 +3,7 @@ from time import perf_counter
3
3
  from types import TracebackType
4
4
  from typing import Any
5
5
 
6
- from aiohttp import ClientResponse, ClientSession
6
+ from aiohttp import BasicAuth, ClientResponse, ClientSession
7
7
  from aiohttp.client import DEFAULT_TIMEOUT
8
8
  from yarl import URL
9
9
 
@@ -16,15 +16,24 @@ from asyncly.client.typing import ResponseHandler, ResponseHandlersType, RouteRe
16
16
 
17
17
 
18
18
  class InstrumentableHttpClient(BaseHttpClient):
19
- __slots__ = ("_metrics_sink", "_resolve_route") + BaseHttpClient.__slots__
19
+ __slots__ = ("_metrics_sink", "_resolve_route")
20
20
 
21
21
  def __init__(
22
22
  self,
23
23
  url: URL | str,
24
24
  session: ClientSession,
25
25
  client_name: str,
26
+ *,
27
+ proxy: URL | str | None = None,
28
+ proxy_auth: BasicAuth | None = None,
26
29
  ) -> None:
27
- super().__init__(url=url, session=session, client_name=client_name)
30
+ super().__init__(
31
+ url=url,
32
+ session=session,
33
+ client_name=client_name,
34
+ proxy=proxy,
35
+ proxy_auth=proxy_auth,
36
+ )
28
37
  self._metrics_sink: MetricsSink = NoopSink()
29
38
  self._resolve_route: RouteResolver = default_route_resolver
30
39
 
@@ -18,6 +18,7 @@ import pytest
18
18
 
19
19
  if TYPE_CHECKING:
20
20
  from asyncly.srvmocker.models import MockRoute, MockService
21
+ from asyncly.srvmocker.proxy import MockProxyService
21
22
 
22
23
 
23
24
  @pytest.fixture
@@ -34,3 +35,12 @@ async def mock_service(
34
35
 
35
36
  async with start_service(mock_routes) as service:
36
37
  yield service
38
+
39
+
40
+ @pytest.fixture
41
+ async def mock_proxy() -> "AsyncIterator[MockProxyService]":
42
+ """A forwarding mock proxy; point a client at it via ``proxy=mock_proxy.url``."""
43
+ from asyncly.srvmocker import start_proxy
44
+
45
+ async with start_proxy() as proxy:
46
+ yield proxy
@@ -5,6 +5,7 @@ from asyncly.srvmocker.exceptions import (
5
5
  )
6
6
  from asyncly.srvmocker.matching import Match
7
7
  from asyncly.srvmocker.models import MockRoute, MockService
8
+ from asyncly.srvmocker.proxy import MockProxyService, start_proxy
8
9
  from asyncly.srvmocker.responses.base import BaseMockResponse
9
10
  from asyncly.srvmocker.responses.content import ContentResponse
10
11
  from asyncly.srvmocker.responses.json import JsonResponse
@@ -17,6 +18,7 @@ __all__ = (
17
18
  "ContentResponse",
18
19
  "JsonResponse",
19
20
  "Match",
21
+ "MockProxyService",
20
22
  "MockRoute",
21
23
  "MockService",
22
24
  "RawResponse",
@@ -24,5 +26,6 @@ __all__ = (
24
26
  "SequenceResponse",
25
27
  "SrvMockerError",
26
28
  "UnknownHandlerError",
29
+ "start_proxy",
27
30
  "start_service",
28
31
  )
@@ -1,7 +1,7 @@
1
1
  from collections.abc import MutableMapping, MutableSequence
2
2
  from dataclasses import dataclass
3
3
 
4
- from aiohttp.web_request import Request
4
+ from aiohttp.web_request import BaseRequest
5
5
  from yarl import URL
6
6
 
7
7
  from asyncly.srvmocker.matching import Match
@@ -18,7 +18,7 @@ class MockRoute:
18
18
 
19
19
  @dataclass
20
20
  class RequestHistory:
21
- request: Request
21
+ request: BaseRequest
22
22
  body: bytes
23
23
 
24
24
 
@@ -0,0 +1,164 @@
1
+ """In-process forwarding HTTP proxy for tests.
2
+
3
+ `start_proxy` spins up a real forward proxy (via aiohttp's ``RawTestServer``)
4
+ that records every request passing through it and forwards it to the absolute
5
+ target URL the client requested. Combined with :func:`start_service` it lets
6
+ you test that a client genuinely routes through a proxy::
7
+
8
+ async with start_service([MockRoute("GET", "/x", "ok")]) as target:
9
+ target.register("ok", JsonResponse({"ok": True}))
10
+ async with start_proxy() as proxy:
11
+ async with ClientSession() as s:
12
+ resp = await s.get(target.url / "x", proxy=proxy.url)
13
+ proxy.assert_called(times=1, method="GET")
14
+
15
+ Only plain HTTP targets are supported (no ``CONNECT`` / HTTPS tunnelling).
16
+ """
17
+
18
+ from collections.abc import AsyncGenerator
19
+ from contextlib import asynccontextmanager
20
+ from dataclasses import dataclass, field
21
+ from http import HTTPStatus
22
+
23
+ from aiohttp import BasicAuth, ClientSession
24
+ from aiohttp.test_utils import RawTestServer
25
+ from aiohttp.web_request import BaseRequest
26
+ from aiohttp.web_response import Response
27
+ from multidict import CIMultiDict, CIMultiDictProxy
28
+ from yarl import URL
29
+
30
+ from asyncly.srvmocker.assertions import call_matches
31
+ from asyncly.srvmocker.models import RequestHistory
32
+
33
+ # Hop-by-hop headers must not be forwarded end-to-end (RFC 9110 section 7.6.1),
34
+ # plus the proxy-specific ones.
35
+ _HOP_BY_HOP = frozenset(
36
+ h.lower()
37
+ for h in (
38
+ "connection",
39
+ "keep-alive",
40
+ "proxy-authenticate",
41
+ "proxy-authorization",
42
+ "proxy-connection",
43
+ "te",
44
+ "trailer",
45
+ "transfer-encoding",
46
+ "upgrade",
47
+ "host",
48
+ )
49
+ )
50
+ # On the response we additionally drop Content-Length so aiohttp recomputes it
51
+ # from the (verbatim) body we relay.
52
+ _HOP_BY_HOP_RESPONSE = _HOP_BY_HOP | frozenset({"content-length"})
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class MockProxyService:
57
+ url: URL
58
+ history: list[RequestHistory] = field(default_factory=list)
59
+
60
+ def set_url(self, url: URL) -> None:
61
+ object.__setattr__(self, "url", url)
62
+
63
+ def get_calls(self) -> list[RequestHistory]:
64
+ return list(self.history)
65
+
66
+ def last_call(self) -> RequestHistory:
67
+ if not self.history:
68
+ raise AssertionError("no requests recorded by proxy")
69
+ return self.history[-1]
70
+
71
+ def assert_called(
72
+ self,
73
+ *,
74
+ times: int | None = None,
75
+ target: URL | str | None = None,
76
+ method: str | None = None,
77
+ json: object = None,
78
+ body: bytes | None = None,
79
+ headers: dict[str, str] | None = None,
80
+ query: dict[str, str] | None = None,
81
+ ) -> None:
82
+ calls = self.get_calls()
83
+ if times is not None and len(calls) != times:
84
+ raise AssertionError(f"proxy: expected {times} call(s), got {len(calls)}")
85
+ criteria = (target, method, json, body, headers, query)
86
+ if times is not None and all(v is None for v in criteria):
87
+ return
88
+ if not calls:
89
+ raise AssertionError("proxy: expected at least one call, got 0")
90
+ for call in calls:
91
+ if target is not None and str(call.request.url) != str(URL(target)):
92
+ continue
93
+ if method is not None and call.request.method != method:
94
+ continue
95
+ if call_matches(call, json=json, body=body, headers=headers, query=query):
96
+ return
97
+ raise AssertionError(
98
+ f"proxy: none of {len(calls)} call(s) matched the given criteria"
99
+ )
100
+
101
+ def assert_not_called(self) -> None:
102
+ if self.history:
103
+ raise AssertionError(f"proxy: expected no calls, got {len(self.history)}")
104
+
105
+
106
+ def _relay_headers(
107
+ source: CIMultiDictProxy[str], *, drop: frozenset[str]
108
+ ) -> CIMultiDict[str]:
109
+ """Copy headers preserving duplicates (e.g. multiple ``Set-Cookie``)."""
110
+ relayed: CIMultiDict[str] = CIMultiDict()
111
+ for key, value in source.items():
112
+ if key.lower() not in drop:
113
+ relayed.add(key, value)
114
+ return relayed
115
+
116
+
117
+ @asynccontextmanager
118
+ async def start_proxy(
119
+ *,
120
+ auth: BasicAuth | None = None,
121
+ ) -> AsyncGenerator[MockProxyService, None]:
122
+ proxy = MockProxyService(url=URL())
123
+ expected_auth = auth.encode() if auth is not None else None
124
+
125
+ # Relay target responses verbatim (HTTP, uncompressed): keep the body and
126
+ # its Content-Encoding untouched instead of auto-decompressing.
127
+ forward_session = ClientSession(auto_decompress=False)
128
+
129
+ async def _handler(request: BaseRequest) -> Response:
130
+ body = await request.read()
131
+ proxy.history.append(RequestHistory(request=request, body=body))
132
+
133
+ if expected_auth is not None:
134
+ provided = request.headers.get("Proxy-Authorization")
135
+ if provided != expected_auth:
136
+ return Response(
137
+ status=HTTPStatus.PROXY_AUTHENTICATION_REQUIRED,
138
+ reason="Proxy Authentication Required",
139
+ headers={"Proxy-Authenticate": "Basic"},
140
+ )
141
+
142
+ async with forward_session.request(
143
+ method=request.method,
144
+ url=request.url,
145
+ headers=_relay_headers(request.headers, drop=_HOP_BY_HOP),
146
+ data=body or None,
147
+ allow_redirects=False,
148
+ ) as upstream:
149
+ payload = await upstream.read()
150
+ return Response(
151
+ status=upstream.status,
152
+ reason=upstream.reason,
153
+ headers=_relay_headers(upstream.headers, drop=_HOP_BY_HOP_RESPONSE),
154
+ body=payload,
155
+ )
156
+
157
+ server = RawTestServer(_handler)
158
+ try:
159
+ await server.start_server()
160
+ proxy.set_url(server.make_url(""))
161
+ yield proxy
162
+ finally:
163
+ await server.close()
164
+ await forward_session.close()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "asyncly"
3
- version = "0.6.2"
3
+ version = "0.7.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"
File without changes
File without changes
File without changes