Skip to content

Commit 56dd7c5

Browse files
committed
Refactor lib
1 parent 8a8f7cf commit 56dd7c5

26 files changed

+291
-82
lines changed

.github/workflows/_publish.yaml

+1-2
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@ jobs:
77
publish-to-pypi:
88
name: Publish Python distribution to PyPI
99
runs-on: ubuntu-22.04
10-
# if: github.repository == 'andy-takker/base-http-client'
1110
environment:
1211
name: pypi
13-
url: https://pypi.org/p/base-http-client/
12+
url: https://pypi.org/p/asyncly/
1413
permissions:
1514
id-token: write
1615
contents: write

.pre-commit-config.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ repos:
3838
hooks:
3939
- id: mypy
4040
name: mypy
41-
entry: mypy ./base_http_client --config-file ./pyproject.toml
41+
entry: mypy ./asyncly --config-file ./pyproject.toml
4242
language: python
4343
language_version: python3.10
4444
require_serial: true

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
PROJECT_NAME = base_http_client
1+
PROJECT_NAME = asyncly
22

33
develop: clean_dev
44
python3.10 -m venv .venv

README.rst

+92-33
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
1-
Base HTTP Client
2-
================
1+
Asyncly
2+
=======
33

4-
.. image:: https://img.shields.io/pypi/v/base-http-client.svg
5-
:target: https://pypi.python.org/pypi/base-http-client/
4+
.. image:: https://img.shields.io/pypi/v/asyncly.svg
5+
:target: https://pypi.python.org/pypi/asyncly/
66
:alt: Latest Version
77

8-
.. image:: https://img.shields.io/pypi/wheel/base-http-client.svg
9-
:target: https://pypi.python.org/pypi/base-http-client/
8+
.. image:: https://img.shields.io/pypi/wheel/asyncly.svg
9+
:target: https://pypi.python.org/pypi/asyncly/
1010

11-
.. image:: https://img.shields.io/pypi/pyversions/base-http-client.svg
12-
:target: https://pypi.python.org/pypi/base-http-client/
11+
.. image:: https://img.shields.io/pypi/pyversions/asyncly.svg
12+
:target: https://pypi.python.org/pypi/asyncly/
1313

14-
.. image:: https://img.shields.io/pypi/l/base-http-client.svg
15-
:target: https://pypi.python.org/pypi/base-http-client/
14+
.. image:: https://img.shields.io/pypi/l/asyncly.svg
15+
:target: https://pypi.python.org/pypi/asyncly/
1616

17-
Base HTTP client for your integrations based on aiohttp_.
17+
Simple HTTP client and server for your integrations based on aiohttp_.
1818

1919
Installation
2020
------------
@@ -26,13 +26,13 @@ Installing from PyPI_:
2626

2727
.. code-block:: bash
2828
29-
pip install base-http-client
29+
pip install asyncly
3030
3131
Installing from github.com:
3232

3333
.. code-block:: bash
3434
35-
pip install git+https://github.com/andy-takker/base_http_client
35+
pip install git+https://github.com/andy-takker/asyncly
3636
3737
The package contains several extras and you can install additional dependencies
3838
if you specify them in this way.
@@ -41,37 +41,33 @@ For example, with msgspec_:
4141

4242
.. code-block:: bash
4343
44-
pip3 install "base-http-client[msgspec]"
44+
pip install "asyncly[msgspec]"
4545
4646
Complete table of extras below:
4747

48-
+----------------------------------------------+----------------------------------+
49-
| example | description |
50-
+==============================================+==================================+
51-
| ``pip install "base-http-client[msgspec]"`` | For using msgspec_ structs |
52-
+----------------------------------------------+----------------------------------+
53-
| ``pip install "base-http-client[orjson]"`` | For fast parsing json by orjson_ |
54-
+----------------------------------------------+----------------------------------+
55-
| ``pip install "base-http-client[pydantic]"`` | For using pydantic_ models |
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+
+-------------------------------------+----------------------------------+
5757

5858
Quick start guide
5959
-----------------
6060

61-
BaseHttpClient
61+
HttpClient
6262
~~~~~~~~~~~~~~
6363

64-
Simple HTTP Client for `https://catfact.ninja`. See full example in `examples/base_http_client.py`_
64+
Simple HTTP Client for `https://catfact.ninja`. See full example in `examples/catfact_client.py`_
6565

6666
.. code-block:: python
6767
68-
from base_http_client.client import (
69-
DEFAULT_TIMEOUT,
70-
BaseHttpClient,
71-
ResponseHandlersType,
72-
)
73-
from base_http_client.handlers.pydantic import parse_model
74-
from base_http_client.timeout import TimeoutType
68+
from asyncly import DEFAULT_TIMEOUT, BaseHttpClient, ResponseHandlersType
69+
from asyncly.client.handlers.pydantic import parse_model
70+
from asyncly.client.timeout import TimeoutType
7571
7672
7773
class CatfactClient(BaseHttpClient):
@@ -92,7 +88,69 @@ Simple HTTP Client for `https://catfact.ninja`. See full example in `examples/ba
9288
timeout=timeout,
9389
)
9490
91+
Test Async Server for client
92+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
9593

94+
For the HTTP client, we create a server to which he will go and simulate real
95+
responses. You can dynamically change the responses from the server in
96+
a specific test.
97+
98+
Let's prepare the fixtures:
99+
100+
.. code-block:: python
101+
102+
@pytest.fixture
103+
async def catafact_service() -> AsyncIterator[MockService]:
104+
routes = [
105+
MockRoute("GET", "/fact", "random_catfact"),
106+
]
107+
async with start_service(routes) as service:
108+
service.register(
109+
"random_catfact",
110+
JsonResponse({"fact": "test", "length": 4}),
111+
)
112+
yield service
113+
114+
115+
@pytest.fixture
116+
def catfact_url(catafact_service: MockService) -> URL:
117+
return catafact_service.url
118+
119+
120+
@pytest.fixture
121+
async def catfact_client(catfact_url: URL) -> AsyncIterator[CatfactClient]:
122+
async with ClientSession() as session:
123+
client = CatfactClient(
124+
client_name="catfact",
125+
session=session,
126+
url=catfact_url,
127+
)
128+
yield client
129+
130+
Now we can use them in tests. See full example in `examples/test_catfact_client.py`_
131+
132+
.. code-block:: python
133+
134+
async def test_fetch_random_catfact(catfact_client: CatfactClient) -> None:
135+
# use default registered handler
136+
fact = await catfact_client.fetch_random_cat_fact()
137+
assert fact == CatfactSchema(fact="test", length=4)
138+
139+
140+
async def test_fetch_random_catfact_timeout(
141+
catfact_client: CatfactClient,
142+
catafact_service: MockService,
143+
) -> None:
144+
# change default registered handler to time error handler
145+
catafact_service.register(
146+
"random_catfact",
147+
LatencyResponse(
148+
wrapped=JsonResponse({"fact": "test", "length": 4}),
149+
latency=1.5,
150+
),
151+
)
152+
with pytest.raises(asyncio.TimeoutError):
153+
await catfact_client.fetch_random_cat_fact(timeout=1)
96154
97155
98156
.. _PyPI: https://pypi.org/
@@ -101,4 +159,5 @@ Simple HTTP Client for `https://catfact.ninja`. See full example in `examples/ba
101159
.. _orjson: https://github.com/ijl/orjson
102160
.. _pydantic: https://github.com/pydantic/pydantic
103161

104-
.. _examples/base_http_client.py: https://github.com/andy-takker/base_http_client/blob/master/examples/base_http_client.py
162+
.. _examples/catfact_client.py: https://github.com/andy-takker/asyncly/blob/master/examples/catfact_client.py
163+
.. _examples/test_catfact_client.py: https://github.com/andy-takker/asyncly/blob/master/examples/test_catfact_client.py

asyncly/__init__.py

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from aiohttp.client import DEFAULT_TIMEOUT
2+
3+
from asyncly.client.base import BaseHttpClient
4+
from asyncly.client.handlers.base import ResponseHandlersType
5+
from asyncly.client.timeout import TimeoutType
6+
7+
__all__ = (
8+
"BaseHttpClient",
9+
"TimeoutType",
10+
"ResponseHandlersType",
11+
"DEFAULT_TIMEOUT",
12+
)
File renamed without changes.

base_http_client/client.py renamed to asyncly/client/base.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@
44
from aiohttp.client import DEFAULT_TIMEOUT
55
from yarl import URL
66

7-
from base_http_client.handlers.base import (
7+
from asyncly.client.handlers.base import (
88
ResponseHandlersType,
99
apply_handler,
1010
)
11-
12-
from .timeout import TimeoutType, get_timeout
11+
from asyncly.client.timeout import TimeoutType, get_timeout
1312

1413

1514
class BaseHttpClient:
@@ -19,8 +18,10 @@ class BaseHttpClient:
1918
_session: ClientSession
2019
_client_name: str
2120

22-
def __init__(self, url: URL, session: ClientSession, client_name: str) -> None:
23-
self._url = url
21+
def __init__(
22+
self, url: URL | str, session: ClientSession, client_name: str
23+
) -> None:
24+
self._url = url if isinstance(url, URL) else URL(url)
2425
self._session = session
2526
self._client_name = client_name
2627

File renamed without changes.

base_http_client/handlers/base.py renamed to asyncly/client/handlers/base.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from aiohttp import ClientResponse
66

7-
from base_http_client.exceptions import UnhandledStatusException
7+
from asyncly.client.handlers.exceptions import UnhandledStatusException
88

99
ResponseHandlersType = Mapping[HTTPStatus | int | str, Callable]
1010

File renamed without changes.
File renamed without changes.
File renamed without changes.

asyncly/srvmocker/__init__.py

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from asyncly.srvmocker.models import MockRoute, MockService
2+
from asyncly.srvmocker.responses import JsonResponse
3+
from asyncly.srvmocker.service import start_service
4+
5+
__all__ = (
6+
"MockRoute",
7+
"MockService",
8+
"JsonResponse",
9+
"start_service",
10+
)

test_http_service/handlers.py renamed to asyncly/srvmocker/handlers.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
from collections.abc import Awaitable, Callable
2+
13
from aiohttp.web_request import Request
4+
from aiohttp.web_response import Response
25

3-
from test_http_service.models import MockService, RequestHistory
6+
from asyncly.srvmocker.models import MockService, RequestHistory
47

58

6-
def get_default_handler(handler_name: str):
7-
async def _handler(request: Request):
9+
def get_default_handler(handler_name: str) -> Callable[[Request], Awaitable[Response]]:
10+
async def _handler(request: Request) -> Response:
811
history = RequestHistory(
912
request=request,
1013
body=await request.read(),

test_http_service/models.py renamed to asyncly/srvmocker/models.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,5 @@ class MockService:
3333
url: URL
3434
handlers: MutableMapping[str, BaseMockResponse]
3535

36-
def register(self, name: str, resp: BaseMockResponse):
36+
def register(self, name: str, resp: BaseMockResponse) -> None:
3737
self.handlers[name] = resp

test_http_service/responses.py renamed to asyncly/srvmocker/responses.py

+26-6
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
from aiohttp.web_request import Request
99
from aiohttp.web_response import Response
1010

11-
from test_http_service.models import BaseMockResponse
12-
from test_http_service.serialization import Serializer
11+
from asyncly.srvmocker.models import BaseMockResponse
12+
from asyncly.srvmocker.serialization import JsonSerializer, Serializer
1313

1414
TimeoutType = int | float
1515

@@ -21,7 +21,7 @@ class ContentResponse(BaseMockResponse):
2121
headers: Mapping[str, str] | None = None
2222
serializer: Serializer | None = None
2323

24-
async def response(self, request: Request):
24+
async def response(self, request: Request) -> Response:
2525
headers: MutableMapping[str, str] = dict()
2626
if self.headers:
2727
headers.update(self.headers)
@@ -44,17 +44,37 @@ class LatencyResponse(BaseMockResponse):
4444
wrapped: BaseMockResponse
4545
latency: TimeoutType
4646

47-
async def response(self, request: Request):
47+
async def response(self, request: Request) -> Response:
4848
await sleep(self.latency)
4949
return await self.wrapped.response(request)
5050

5151

5252
class MockSeqResponse(BaseMockResponse):
5353
responses: Iterator[BaseMockResponse]
5454

55-
def __init__(self, responses: Iterable[BaseMockResponse]):
55+
def __init__(self, responses: Iterable[BaseMockResponse]) -> None:
5656
self.responses = iter(responses)
5757

58-
async def response(self, request: Request):
58+
async def response(self, request: Request) -> Response:
5959
resp = next(self.responses)
6060
return await resp.response(request)
61+
62+
63+
class JsonResponse(BaseMockResponse):
64+
__content: ContentResponse
65+
66+
def __init__(
67+
self,
68+
body: Any,
69+
status: int = HTTPStatus.OK,
70+
headers: Mapping[str, str] | None = None,
71+
) -> None:
72+
self.__content = ContentResponse(
73+
body=body,
74+
status=status,
75+
headers=headers,
76+
serializer=JsonSerializer,
77+
)
78+
79+
async def response(self, request: Request) -> Response:
80+
return await self.__content.response(request)
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import json
22
from collections.abc import Callable
33
from dataclasses import dataclass
4+
from typing import Any, Final
45

56

67
@dataclass(frozen=True)
78
class Serializer:
8-
dumps: Callable
9+
dumps: Callable[[Any], str]
910
content_type: str
1011

1112

12-
JsonSerializer = Serializer(
13+
JsonSerializer: Final = Serializer(
1314
dumps=json.dumps,
1415
content_type="application/json",
1516
)

test_http_service/service.py renamed to asyncly/srvmocker/service.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
from aiohttp.test_utils import TestServer
66
from aiohttp.web_app import Application
77

8-
from test_http_service.handlers import get_default_handler
9-
from test_http_service.models import MockRoute, MockService
8+
from asyncly.srvmocker.handlers import get_default_handler
9+
from asyncly.srvmocker.models import MockRoute, MockService
1010

1111

1212
@asynccontextmanager

base_http_client/__init__.py

-5
This file was deleted.

0 commit comments

Comments
 (0)