asyncly 0.3.3__tar.gz → 0.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 (39) hide show
  1. asyncly-0.4.0/.gitignore +165 -0
  2. {asyncly-0.3.3 → asyncly-0.4.0}/PKG-INFO +48 -16
  3. {asyncly-0.3.3 → asyncly-0.4.0}/README.rst +36 -2
  4. {asyncly-0.3.3 → asyncly-0.4.0}/asyncly/client/base.py +9 -1
  5. {asyncly-0.3.3 → asyncly-0.4.0}/asyncly/client/handlers/base.py +1 -1
  6. asyncly-0.4.0/asyncly/client/handlers/msgspec.py +49 -0
  7. asyncly-0.4.0/asyncly/srvmocker/__init__.py +14 -0
  8. asyncly-0.4.0/asyncly/srvmocker/constants.py +5 -0
  9. {asyncly-0.3.3 → asyncly-0.4.0}/asyncly/srvmocker/handlers.py +2 -1
  10. {asyncly-0.3.3 → asyncly-0.4.0}/asyncly/srvmocker/models.py +5 -8
  11. asyncly-0.4.0/asyncly/srvmocker/responses/__init__.py +0 -0
  12. asyncly-0.4.0/asyncly/srvmocker/responses/base.py +10 -0
  13. asyncly-0.4.0/asyncly/srvmocker/responses/content.py +36 -0
  14. asyncly-0.4.0/asyncly/srvmocker/responses/json.py +30 -0
  15. asyncly-0.4.0/asyncly/srvmocker/responses/msgpack.py +30 -0
  16. asyncly-0.4.0/asyncly/srvmocker/responses/sequence.py +17 -0
  17. asyncly-0.4.0/asyncly/srvmocker/responses/timeout.py +19 -0
  18. asyncly-0.4.0/asyncly/srvmocker/responses/toml.py +30 -0
  19. asyncly-0.4.0/asyncly/srvmocker/responses/yaml.py +30 -0
  20. asyncly-0.4.0/asyncly/srvmocker/serialization/__init__.py +0 -0
  21. asyncly-0.4.0/asyncly/srvmocker/serialization/base.py +9 -0
  22. asyncly-0.4.0/asyncly/srvmocker/serialization/json.py +9 -0
  23. asyncly-0.4.0/asyncly/srvmocker/serialization/msgpack.py +10 -0
  24. asyncly-0.4.0/asyncly/srvmocker/serialization/toml.py +13 -0
  25. asyncly-0.4.0/asyncly/srvmocker/serialization/yaml.py +10 -0
  26. {asyncly-0.3.3 → asyncly-0.4.0}/asyncly/srvmocker/service.py +12 -7
  27. {asyncly-0.3.3 → asyncly-0.4.0}/pyproject.toml +42 -35
  28. asyncly-0.3.3/asyncly/client/handlers/msgspec.py +0 -15
  29. asyncly-0.3.3/asyncly/srvmocker/__init__.py +0 -10
  30. asyncly-0.3.3/asyncly/srvmocker/responses.py +0 -80
  31. asyncly-0.3.3/asyncly/srvmocker/serialization.py +0 -16
  32. {asyncly-0.3.3 → asyncly-0.4.0}/asyncly/__init__.py +0 -0
  33. {asyncly-0.3.3 → asyncly-0.4.0}/asyncly/client/__init__.py +0 -0
  34. {asyncly-0.3.3 → asyncly-0.4.0}/asyncly/client/handlers/__init__.py +0 -0
  35. {asyncly-0.3.3 → asyncly-0.4.0}/asyncly/client/handlers/exceptions.py +0 -0
  36. {asyncly-0.3.3 → asyncly-0.4.0}/asyncly/client/handlers/json.py +0 -0
  37. {asyncly-0.3.3 → asyncly-0.4.0}/asyncly/client/handlers/pydantic.py +0 -0
  38. {asyncly-0.3.3 → asyncly-0.4.0}/asyncly/client/timeout.py +0 -0
  39. {asyncly-0.3.3 → asyncly-0.4.0}/asyncly/py.typed +0 -0
@@ -0,0 +1,165 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+ junit.xml
54
+
55
+ # Translations
56
+ *.mo
57
+ *.pot
58
+
59
+ # Django stuff:
60
+ *.log
61
+ local_settings.py
62
+ db.sqlite3
63
+ db.sqlite3-journal
64
+
65
+ # Flask stuff:
66
+ instance/
67
+ .webassets-cache
68
+
69
+ # Scrapy stuff:
70
+ .scrapy
71
+
72
+ # Sphinx documentation
73
+ docs/_build/
74
+
75
+ # PyBuilder
76
+ .pybuilder/
77
+ target/
78
+
79
+ # Jupyter Notebook
80
+ .ipynb_checkpoints
81
+
82
+ # IPython
83
+ profile_default/
84
+ ipython_config.py
85
+
86
+ # pyenv
87
+ # For a library or package, you might want to ignore these files since the code is
88
+ # intended to run in multiple environments; otherwise, check them in:
89
+ # .python-version
90
+
91
+ # pipenv
92
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
93
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
94
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
95
+ # install all needed dependencies.
96
+ #Pipfile.lock
97
+
98
+ # poetry
99
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
100
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
101
+ # commonly ignored for libraries.
102
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
103
+ #poetry.lock
104
+
105
+ # pdm
106
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
107
+ #pdm.lock
108
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
109
+ # in version control.
110
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
111
+ .pdm.toml
112
+ .pdm-python
113
+ .pdm-build/
114
+
115
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
116
+ __pypackages__/
117
+
118
+ # Celery stuff
119
+ celerybeat-schedule
120
+ celerybeat.pid
121
+
122
+ # SageMath parsed files
123
+ *.sage.py
124
+
125
+ # Environments
126
+ .env
127
+ .venv
128
+ env/
129
+ venv/
130
+ ENV/
131
+ env.bak/
132
+ venv.bak/
133
+
134
+ # Spyder project settings
135
+ .spyderproject
136
+ .spyproject
137
+
138
+ # Rope project settings
139
+ .ropeproject
140
+
141
+ # mkdocs documentation
142
+ /site
143
+
144
+ # mypy
145
+ .mypy_cache/
146
+ .dmypy.json
147
+ dmypy.json
148
+
149
+ # Pyre type checker
150
+ .pyre/
151
+
152
+ # pytype static type analyzer
153
+ .pytype/
154
+
155
+ # Cython debug symbols
156
+ cython_debug/
157
+
158
+ # PyCharm
159
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
160
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
161
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
162
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
163
+ .idea/
164
+ .vscode/
165
+ .DS_Store
@@ -1,13 +1,13 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: asyncly
3
- Version: 0.3.3
3
+ Version: 0.4.0
4
4
  Summary: Simple HTTP client and server for your integrations based on aiohttp
5
- Home-page: https://github.com/andy-takker/asyncly
6
- License: MIT
7
- Keywords: aiohttp,http,client
8
- Author: Sergey Natalenko
9
- Author-email: sergey.natalenko@mail.ru
10
- Requires-Python: >=3.10,<4.0
5
+ Project-URL: Homepage, https://github.com/andy-takker/asyncly
6
+ Project-URL: Source, https://github.com/andy-takker/asyncly
7
+ Project-URL: Bug Tracker, https://github.com/andy-takker/asyncly/issues
8
+ Author-email: Sergey Natalenko <sergey.natalenko@mail.ru>
9
+ License-Expression: MIT
10
+ Keywords: aiohttp,client,http
11
11
  Classifier: Development Status :: 4 - Beta
12
12
  Classifier: Framework :: AsyncIO
13
13
  Classifier: Framework :: Pytest
@@ -22,21 +22,19 @@ 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
26
25
  Classifier: Topic :: Internet
27
26
  Classifier: Topic :: Internet :: WWW/HTTP
28
27
  Classifier: Topic :: Software Development
29
28
  Classifier: Topic :: Software Development :: Libraries
30
29
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
30
+ Requires-Python: <4,>=3.10
31
+ Requires-Dist: aiohttp<4,>=3.9.5
31
32
  Provides-Extra: msgspec
33
+ Requires-Dist: msgspec<0.20,>=0.19.0; extra == 'msgspec'
32
34
  Provides-Extra: orjson
35
+ Requires-Dist: orjson<4,>=3.10.6; extra == 'orjson'
33
36
  Provides-Extra: pydantic
34
- Requires-Dist: aiohttp (>=3.9.5,<4.0.0)
35
- Requires-Dist: msgspec (>=0.18.6,<0.19.0) ; extra == "msgspec"
36
- Requires-Dist: orjson (>=3.10.6,<4.0.0) ; extra == "orjson"
37
- Requires-Dist: pydantic (>=2.8.2,<3.0.0) ; extra == "pydantic"
38
- Project-URL: Bug Tracker, https://github.com/andy-takker/asyncly/issues
39
- Project-URL: Source, https://github.com/andy-takker/asyncly
37
+ Requires-Dist: pydantic<3,>=2.8.2; extra == 'pydantic'
40
38
  Description-Content-Type: text/x-rst
41
39
 
42
40
  Asyncly
@@ -100,7 +98,7 @@ Quick start guide
100
98
  -----------------
101
99
 
102
100
  HttpClient
103
- ~~~~~~~~~~~~~~
101
+ ~~~~~~~~~~
104
102
 
105
103
  Simple HTTP Client for `https://catfact.ninja`. See full example in `examples/catfact_client.py`_
106
104
 
@@ -132,6 +130,9 @@ Simple HTTP Client for `https://catfact.ninja`. See full example in `examples/ca
132
130
  Test Async Server for client
133
131
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
134
132
 
133
+ Example
134
+ *******
135
+
135
136
  For the HTTP client, we create a server to which he will go and simulate real
136
137
  responses. You can dynamically change the responses from the server in
137
138
  a specific test.
@@ -193,12 +194,43 @@ Now we can use them in tests. See full example in `examples/test_catfact_client.
193
194
  with pytest.raises(asyncio.TimeoutError):
194
195
  await catfact_client.fetch_random_cat_fact(timeout=1)
195
196
 
197
+ Useful responses and serializers
198
+ ********************************
199
+
200
+ - JsonResponse_: simple JSON response from any object.
201
+ You can setup status code and serializer for it. Using JsonSerializer_
202
+
203
+ - MsgpackResponse_: response in msgpack_ format with It's like JSON.
204
+ But fast and small. Using MsgpackSerializer_.
205
+
206
+ - SequenceResponse_: useful response if you want return different responses
207
+ on next request. Accepts BaseMockResponse_'s input.
208
+
209
+ - TimeoutResponse_: response with latency. For slow testing
210
+
211
+ - TomlResponse_: return TOML format text response. Using TomlSerializer_.
212
+
213
+ - YamlResponse_: return YAML format text response. Using YamlSerializer_.
196
214
 
197
215
  .. _PyPI: https://pypi.org/
198
216
  .. _aiohttp: https://pypi.org/project/aiohttp/
217
+ .. _msgpack: https://msgpack.org
199
218
  .. _msgspec: https://github.com/jcrist/msgspec
200
219
  .. _orjson: https://github.com/ijl/orjson
201
220
  .. _pydantic: https://github.com/pydantic/pydantic
202
221
 
203
222
  .. _examples/catfact_client.py: https://github.com/andy-takker/asyncly/blob/master/examples/catfact_client.py
204
223
  .. _examples/test_catfact_client.py: https://github.com/andy-takker/asyncly/blob/master/examples/test_catfact_client.py
224
+
225
+ .. _BaseMockResponse: https://github.com/andy-takker/asyncly/blob/master/asyncly/srvmocker/responses/base.py
226
+ .. _JsonResponse: https://github.com/andy-takker/asyncly/blob/master/asyncly/srvmocker/responses/json.py
227
+ .. _MsgpackResponse: https://github.com/andy-takker/asyncly/blob/master/asyncly/srvmocker/responses/msgpack.py
228
+ .. _SequenceResponse: https://github.com/andy-takker/asyncly/blob/master/asyncly/srvmocker/responses/sequence.py
229
+ .. _TimeoutResponse: https://github.com/andy-takker/asyncly/blob/master/asyncly/srvmocker/responses/timeout.py
230
+ .. _TomlResponse: https://github.com/andy-takker/asyncly/blob/master/asyncly/srvmocker/responses/toml.py
231
+ .. _YamlResponse: https://github.com/andy-takker/asyncly/blob/master/asyncly/srvmocker/responses/yaml.py
232
+
233
+ .. _JsonSerializer: https://github.com/andy-takker/asyncly/blob/master/asyncly/srvmocker/serialization/json.py
234
+ .. _MsgpackSerializer: https://github.com/andy-takker/asyncly/blob/master/asyncly/srvmocker/serialization/msgpack.py
235
+ .. _TomlSerializer: https://github.com/andy-takker/asyncly/blob/master/asyncly/srvmocker/serialization/toml.py
236
+ .. _YamlSerializer: https://github.com/andy-takker/asyncly/blob/master/asyncly/srvmocker/serialization/yaml.py
@@ -59,7 +59,7 @@ Quick start guide
59
59
  -----------------
60
60
 
61
61
  HttpClient
62
- ~~~~~~~~~~~~~~
62
+ ~~~~~~~~~~
63
63
 
64
64
  Simple HTTP Client for `https://catfact.ninja`. See full example in `examples/catfact_client.py`_
65
65
 
@@ -91,6 +91,9 @@ Simple HTTP Client for `https://catfact.ninja`. See full example in `examples/ca
91
91
  Test Async Server for client
92
92
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
93
93
 
94
+ Example
95
+ *******
96
+
94
97
  For the HTTP client, we create a server to which he will go and simulate real
95
98
  responses. You can dynamically change the responses from the server in
96
99
  a specific test.
@@ -152,12 +155,43 @@ Now we can use them in tests. See full example in `examples/test_catfact_client.
152
155
  with pytest.raises(asyncio.TimeoutError):
153
156
  await catfact_client.fetch_random_cat_fact(timeout=1)
154
157
 
158
+ Useful responses and serializers
159
+ ********************************
160
+
161
+ - JsonResponse_: simple JSON response from any object.
162
+ You can setup status code and serializer for it. Using JsonSerializer_
163
+
164
+ - MsgpackResponse_: response in msgpack_ format with It's like JSON.
165
+ But fast and small. Using MsgpackSerializer_.
166
+
167
+ - SequenceResponse_: useful response if you want return different responses
168
+ on next request. Accepts BaseMockResponse_'s input.
169
+
170
+ - TimeoutResponse_: response with latency. For slow testing
171
+
172
+ - TomlResponse_: return TOML format text response. Using TomlSerializer_.
173
+
174
+ - YamlResponse_: return YAML format text response. Using YamlSerializer_.
155
175
 
156
176
  .. _PyPI: https://pypi.org/
157
177
  .. _aiohttp: https://pypi.org/project/aiohttp/
178
+ .. _msgpack: https://msgpack.org
158
179
  .. _msgspec: https://github.com/jcrist/msgspec
159
180
  .. _orjson: https://github.com/ijl/orjson
160
181
  .. _pydantic: https://github.com/pydantic/pydantic
161
182
 
162
183
  .. _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
184
+ .. _examples/test_catfact_client.py: https://github.com/andy-takker/asyncly/blob/master/examples/test_catfact_client.py
185
+
186
+ .. _BaseMockResponse: https://github.com/andy-takker/asyncly/blob/master/asyncly/srvmocker/responses/base.py
187
+ .. _JsonResponse: https://github.com/andy-takker/asyncly/blob/master/asyncly/srvmocker/responses/json.py
188
+ .. _MsgpackResponse: https://github.com/andy-takker/asyncly/blob/master/asyncly/srvmocker/responses/msgpack.py
189
+ .. _SequenceResponse: https://github.com/andy-takker/asyncly/blob/master/asyncly/srvmocker/responses/sequence.py
190
+ .. _TimeoutResponse: https://github.com/andy-takker/asyncly/blob/master/asyncly/srvmocker/responses/timeout.py
191
+ .. _TomlResponse: https://github.com/andy-takker/asyncly/blob/master/asyncly/srvmocker/responses/toml.py
192
+ .. _YamlResponse: https://github.com/andy-takker/asyncly/blob/master/asyncly/srvmocker/responses/yaml.py
193
+
194
+ .. _JsonSerializer: https://github.com/andy-takker/asyncly/blob/master/asyncly/srvmocker/serialization/json.py
195
+ .. _MsgpackSerializer: https://github.com/andy-takker/asyncly/blob/master/asyncly/srvmocker/serialization/msgpack.py
196
+ .. _TomlSerializer: https://github.com/andy-takker/asyncly/blob/master/asyncly/srvmocker/serialization/toml.py
197
+ .. _YamlSerializer: https://github.com/andy-takker/asyncly/blob/master/asyncly/srvmocker/serialization/yaml.py
@@ -1,3 +1,4 @@
1
+ import sys
1
2
  from typing import Any, Literal
2
3
 
3
4
  from aiohttp import ClientSession
@@ -10,6 +11,13 @@ from asyncly.client.handlers.base import (
10
11
  )
11
12
  from asyncly.client.timeout import TimeoutType, get_timeout
12
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"]
20
+
13
21
 
14
22
  class BaseHttpClient:
15
23
  __slots__ = ("_url", "_session", "_client_name")
@@ -31,7 +39,7 @@ class BaseHttpClient:
31
39
 
32
40
  async def _make_req(
33
41
  self,
34
- method: Literal["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD"],
42
+ method: MethodType,
35
43
  url: URL,
36
44
  handlers: ResponseHandlersType,
37
45
  timeout: TimeoutType = DEFAULT_TIMEOUT,
@@ -17,7 +17,7 @@ async def apply_handler(
17
17
  handler = _find_handler(handlers=handlers, status=response.status)
18
18
  if not handler:
19
19
  raise UnhandledStatusException(
20
- f"Unexpected resposne {response.status} from {response.url}",
20
+ f"Unexpected response {response.status} from {response.url}",
21
21
  status=response.status,
22
22
  url=response.url,
23
23
  client_name=client_name,
@@ -0,0 +1,49 @@
1
+ from collections.abc import Awaitable, Callable
2
+ from typing import Any, Literal, Protocol, TypeVar
3
+
4
+ from aiohttp import ClientResponse
5
+ from msgspec import Struct
6
+ from msgspec.json import decode as decode_json
7
+ from msgspec.msgpack import decode as decode_msgpack
8
+ from msgspec.toml import decode as decode_toml
9
+ from msgspec.yaml import decode as decode_yaml
10
+
11
+ T = TypeVar("T", bound=Struct)
12
+
13
+ DataFormat = Literal["json", "msgpack", "toml", "yaml"]
14
+
15
+
16
+ class DataFormatDecode(Protocol):
17
+ def __call__(
18
+ self,
19
+ buf: bytes | str,
20
+ *,
21
+ type: type[T] = ...,
22
+ strict: bool = True,
23
+ dec_hook: Callable[[type, Any], Any] | None = None,
24
+ ) -> Any: ...
25
+
26
+
27
+ def parse_struct(
28
+ struct: type[T],
29
+ data_format: DataFormat = "json",
30
+ strict: bool = True,
31
+ ) -> Callable[[ClientResponse], Awaitable[T]]:
32
+ decode = _choose_decoder(data_format)
33
+
34
+ async def _parse(response: ClientResponse) -> T:
35
+ return decode(await response.read(), type=struct, strict=strict)
36
+
37
+ return _parse
38
+
39
+
40
+ def _choose_decoder(data_format: DataFormat) -> DataFormatDecode:
41
+ if data_format == "json":
42
+ return decode_json # type: ignore[return-value]
43
+ elif data_format == "msgpack":
44
+ return decode_msgpack # type: ignore[return-value]
45
+ elif data_format == "toml":
46
+ return decode_toml
47
+ elif data_format == "yaml":
48
+ return decode_yaml
49
+ return decode_json
@@ -0,0 +1,14 @@
1
+ from asyncly.srvmocker.models import MockRoute, MockService
2
+ from asyncly.srvmocker.responses.base import BaseMockResponse
3
+ from asyncly.srvmocker.responses.content import ContentResponse
4
+ from asyncly.srvmocker.responses.json import JsonResponse
5
+ from asyncly.srvmocker.service import start_service
6
+
7
+ __all__ = (
8
+ "BaseMockResponse",
9
+ "ContentResponse",
10
+ "MockRoute",
11
+ "MockService",
12
+ "JsonResponse",
13
+ "start_service",
14
+ )
@@ -0,0 +1,5 @@
1
+ from aiohttp.web import AppKey
2
+
3
+ from asyncly.srvmocker.models import MockService
4
+
5
+ SERVICE_KEY = AppKey("service", MockService)
@@ -3,6 +3,7 @@ from collections.abc import Awaitable, Callable
3
3
  from aiohttp.web_request import Request
4
4
  from aiohttp.web_response import Response
5
5
 
6
+ from asyncly.srvmocker.constants import SERVICE_KEY
6
7
  from asyncly.srvmocker.models import MockService, RequestHistory
7
8
 
8
9
 
@@ -12,7 +13,7 @@ def get_default_handler(handler_name: str) -> Callable[[Request], Awaitable[Resp
12
13
  request=request,
13
14
  body=await request.read(),
14
15
  )
15
- context: MockService = request.app["service"]
16
+ context: MockService = request.app[SERVICE_KEY]
16
17
  context.history.append(history)
17
18
  context.history_map[handler_name].append(history)
18
19
  handler = context.handlers[handler_name]
@@ -1,11 +1,11 @@
1
- from abc import ABC, abstractmethod
2
1
  from collections.abc import MutableMapping, MutableSequence
3
2
  from dataclasses import dataclass
4
3
 
5
4
  from aiohttp.web_request import Request
6
- from aiohttp.web_response import Response
7
5
  from yarl import URL
8
6
 
7
+ from asyncly.srvmocker.responses.base import BaseMockResponse
8
+
9
9
 
10
10
  @dataclass(frozen=True)
11
11
  class MockRoute:
@@ -20,12 +20,6 @@ class RequestHistory:
20
20
  body: bytes
21
21
 
22
22
 
23
- class BaseMockResponse(ABC):
24
- @abstractmethod
25
- async def response(self, request: Request) -> Response:
26
- pass
27
-
28
-
29
23
  @dataclass(frozen=True)
30
24
  class MockService:
31
25
  history: MutableSequence[RequestHistory]
@@ -35,3 +29,6 @@ class MockService:
35
29
 
36
30
  def register(self, name: str, resp: BaseMockResponse) -> None:
37
31
  self.handlers[name] = resp
32
+
33
+ def set_url(self, url: URL) -> None:
34
+ object.__setattr__(self, "url", url)
File without changes
@@ -0,0 +1,10 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from aiohttp.web_request import Request
4
+ from aiohttp.web_response import Response
5
+
6
+
7
+ class BaseMockResponse(ABC):
8
+ @abstractmethod
9
+ async def response(self, request: Request) -> Response:
10
+ pass
@@ -0,0 +1,36 @@
1
+ from collections.abc import Mapping, MutableMapping
2
+ from dataclasses import dataclass
3
+ from http import HTTPStatus
4
+ from typing import Any
5
+
6
+ from aiohttp import hdrs
7
+ from aiohttp.web_request import Request
8
+ from aiohttp.web_response import Response
9
+
10
+ from asyncly.srvmocker.responses.base import BaseMockResponse
11
+ from asyncly.srvmocker.serialization.base import Serializer
12
+
13
+
14
+ @dataclass
15
+ class ContentResponse(BaseMockResponse):
16
+ body: Any = None
17
+ status: int = HTTPStatus.OK
18
+ headers: Mapping[str, str] | None = None
19
+ serializer: Serializer | None = None
20
+
21
+ async def response(self, request: Request) -> Response:
22
+ headers: MutableMapping[str, str] = dict()
23
+ if self.headers:
24
+ headers.update(self.headers)
25
+ if self.serializer:
26
+ headers[hdrs.CONTENT_TYPE] = self.serializer.content_type
27
+ return Response(
28
+ status=self.status,
29
+ body=self.serialize(),
30
+ headers=headers,
31
+ )
32
+
33
+ def serialize(self) -> Any:
34
+ if not self.serializer:
35
+ return self.body
36
+ return self.serializer.dumps(self.body)
@@ -0,0 +1,30 @@
1
+ from collections.abc import Mapping
2
+ from http import HTTPStatus
3
+ from typing import Any
4
+
5
+ from aiohttp.web_request import Request
6
+ from aiohttp.web_response import Response
7
+
8
+ from asyncly.srvmocker.responses.base import BaseMockResponse
9
+ from asyncly.srvmocker.responses.content import ContentResponse
10
+ from asyncly.srvmocker.serialization.json import JsonSerializer
11
+
12
+
13
+ class JsonResponse(BaseMockResponse):
14
+ _content: ContentResponse
15
+
16
+ def __init__(
17
+ self,
18
+ body: Any,
19
+ status: int = HTTPStatus.OK,
20
+ headers: Mapping[str, str] | None = None,
21
+ ) -> None:
22
+ self._content = ContentResponse(
23
+ body=body,
24
+ status=status,
25
+ headers=headers,
26
+ serializer=JsonSerializer,
27
+ )
28
+
29
+ async def response(self, request: Request) -> Response:
30
+ return await self._content.response(request)
@@ -0,0 +1,30 @@
1
+ from collections.abc import Mapping
2
+ from http import HTTPStatus
3
+ from typing import Any
4
+
5
+ from aiohttp.web_request import Request
6
+ from aiohttp.web_response import Response
7
+
8
+ from asyncly.srvmocker.responses.base import BaseMockResponse
9
+ from asyncly.srvmocker.responses.content import ContentResponse
10
+ from asyncly.srvmocker.serialization.msgpack import MsgpackSerializer
11
+
12
+
13
+ class MsgpackResponse(BaseMockResponse):
14
+ __content: ContentResponse
15
+
16
+ def __init__(
17
+ self,
18
+ body: Any,
19
+ status: int = HTTPStatus.OK,
20
+ headers: Mapping[str, str] | None = None,
21
+ ) -> None:
22
+ self.__content = ContentResponse(
23
+ body=body,
24
+ status=status,
25
+ headers=headers,
26
+ serializer=MsgpackSerializer,
27
+ )
28
+
29
+ async def response(self, request: Request) -> Response:
30
+ return await self.__content.response(request)
@@ -0,0 +1,17 @@
1
+ from collections.abc import Iterable, Iterator
2
+
3
+ from aiohttp.web_request import Request
4
+ from aiohttp.web_response import Response
5
+
6
+ from asyncly.srvmocker.responses.base import BaseMockResponse
7
+
8
+
9
+ class SequenceResponse(BaseMockResponse):
10
+ responses: Iterator[BaseMockResponse]
11
+
12
+ def __init__(self, responses: Iterable[BaseMockResponse]) -> None:
13
+ self.responses = iter(responses)
14
+
15
+ async def response(self, request: Request) -> Response:
16
+ resp = next(self.responses)
17
+ return await resp.response(request)
@@ -0,0 +1,19 @@
1
+ from asyncio import sleep
2
+ from dataclasses import dataclass
3
+
4
+ from aiohttp.web_request import Request
5
+ from aiohttp.web_response import Response
6
+
7
+ from asyncly.srvmocker.responses.base import BaseMockResponse
8
+
9
+ TimeoutType = int | float
10
+
11
+
12
+ @dataclass
13
+ class LatencyResponse(BaseMockResponse):
14
+ wrapped: BaseMockResponse
15
+ latency: TimeoutType
16
+
17
+ async def response(self, request: Request) -> Response:
18
+ await sleep(self.latency)
19
+ return await self.wrapped.response(request)
@@ -0,0 +1,30 @@
1
+ from collections.abc import Mapping
2
+ from http import HTTPStatus
3
+ from typing import Any
4
+
5
+ from aiohttp.web_request import Request
6
+ from aiohttp.web_response import Response
7
+
8
+ from asyncly.srvmocker.responses.base import BaseMockResponse
9
+ from asyncly.srvmocker.responses.content import ContentResponse
10
+ from asyncly.srvmocker.serialization.toml import TomlSerializer
11
+
12
+
13
+ class TomlResponse(BaseMockResponse):
14
+ _content: ContentResponse
15
+
16
+ def __init__(
17
+ self,
18
+ body: Any,
19
+ status: int = HTTPStatus.OK,
20
+ headers: Mapping[str, str] | None = None,
21
+ ) -> None:
22
+ self._content = ContentResponse(
23
+ body=body,
24
+ status=status,
25
+ headers=headers,
26
+ serializer=TomlSerializer,
27
+ )
28
+
29
+ async def response(self, request: Request) -> Response:
30
+ return await self._content.response(request)
@@ -0,0 +1,30 @@
1
+ from collections.abc import Mapping
2
+ from http import HTTPStatus
3
+ from typing import Any
4
+
5
+ from aiohttp.web_request import Request
6
+ from aiohttp.web_response import Response
7
+
8
+ from asyncly.srvmocker.responses.base import BaseMockResponse
9
+ from asyncly.srvmocker.responses.content import ContentResponse
10
+ from asyncly.srvmocker.serialization.yaml import YamlSerializer
11
+
12
+
13
+ class YamlResponse(BaseMockResponse):
14
+ _content: ContentResponse
15
+
16
+ def __init__(
17
+ self,
18
+ body: Any,
19
+ status: int = HTTPStatus.OK,
20
+ headers: Mapping[str, str] | None = None,
21
+ ) -> None:
22
+ self._content = ContentResponse(
23
+ body=body,
24
+ status=status,
25
+ headers=headers,
26
+ serializer=YamlSerializer,
27
+ )
28
+
29
+ async def response(self, request: Request) -> Response:
30
+ return await self._content.response(request)
@@ -0,0 +1,9 @@
1
+ from collections.abc import Callable
2
+ from dataclasses import dataclass
3
+ from typing import Any
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class Serializer:
8
+ dumps: Callable[[Any], str | bytes]
9
+ content_type: str
@@ -0,0 +1,9 @@
1
+ import json
2
+ from typing import Final
3
+
4
+ from asyncly.srvmocker.serialization.base import Serializer
5
+
6
+ JsonSerializer: Final = Serializer(
7
+ dumps=json.dumps,
8
+ content_type="application/json",
9
+ )
@@ -0,0 +1,10 @@
1
+ from typing import Final
2
+
3
+ import msgspec
4
+
5
+ from asyncly.srvmocker.serialization.base import Serializer
6
+
7
+ MsgpackSerializer: Final = Serializer(
8
+ dumps=msgspec.msgpack.encode,
9
+ content_type="application/msgpack",
10
+ )
@@ -0,0 +1,13 @@
1
+ from typing import Final
2
+
3
+ try:
4
+ import toml
5
+ except ImportError:
6
+ raise ImportError("toml is not installed")
7
+
8
+ from asyncly.srvmocker.serialization.base import Serializer
9
+
10
+ TomlSerializer: Final = Serializer(
11
+ dumps=toml.dumps,
12
+ content_type="application/toml",
13
+ )
@@ -0,0 +1,10 @@
1
+ from typing import Final
2
+
3
+ import yaml
4
+
5
+ from asyncly.srvmocker.serialization.base import Serializer
6
+
7
+ YamlSerializer: Final = Serializer(
8
+ dumps=yaml.dump,
9
+ content_type="application/yaml",
10
+ )
@@ -4,7 +4,9 @@ from contextlib import asynccontextmanager
4
4
 
5
5
  from aiohttp.test_utils import TestServer
6
6
  from aiohttp.web_app import Application
7
+ from yarl import URL
7
8
 
9
+ from asyncly.srvmocker.constants import SERVICE_KEY
8
10
  from asyncly.srvmocker.handlers import get_default_handler
9
11
  from asyncly.srvmocker.models import MockRoute, MockService
10
12
 
@@ -14,6 +16,13 @@ async def start_service(
14
16
  routes: Iterable[MockRoute],
15
17
  ) -> AsyncGenerator[MockService, None]:
16
18
  app = Application()
19
+ mock_service = MockService(
20
+ history=list(),
21
+ history_map=defaultdict(list),
22
+ url=URL(),
23
+ handlers=dict(),
24
+ )
25
+ app[SERVICE_KEY] = mock_service
17
26
  server = TestServer(app)
18
27
  for route in routes:
19
28
  app.router.add_route(
@@ -22,13 +31,9 @@ async def start_service(
22
31
  handler=get_default_handler(route.handler_name),
23
32
  )
24
33
  await server.start_server()
25
- mock_service = MockService(
26
- history=list(),
27
- history_map=defaultdict(list),
28
- url=server.make_url(""),
29
- handlers=dict(),
30
- )
31
- app["service"] = mock_service
34
+
35
+ mock_service.set_url(server.make_url(""))
36
+
32
37
  try:
33
38
  yield mock_service
34
39
  finally:
@@ -1,12 +1,16 @@
1
- [tool.poetry]
1
+ [project]
2
2
  name = "asyncly"
3
- version = "0.3.3"
3
+ version = "0.4.0"
4
4
  description = "Simple HTTP client and server for your integrations based on aiohttp"
5
- authors = ["Sergey Natalenko <sergey.natalenko@mail.ru>"]
6
- license = "MIT"
7
- homepage = "https://github.com/andy-takker/asyncly"
5
+ authors = [{ name = "Sergey Natalenko", email = "sergey.natalenko@mail.ru" }]
6
+ requires-python = ">=3.10, <4"
8
7
  readme = "README.rst"
9
- keywords = ["aiohttp", "http", "client"]
8
+ license = "MIT"
9
+ keywords = [
10
+ "aiohttp",
11
+ "http",
12
+ "client",
13
+ ]
10
14
  classifiers = [
11
15
  "Development Status :: 4 - Beta",
12
16
  "Framework :: AsyncIO",
@@ -28,39 +32,42 @@ classifiers = [
28
32
  "Topic :: Software Development :: Libraries",
29
33
  "Topic :: Software Development",
30
34
  ]
31
- packages = [
32
- { include = "asyncly" },
33
- ]
35
+ dependencies = ["aiohttp>=3.9.5,<4"]
34
36
 
35
- [tool.poetry.urls]
36
- "Source" = "https://github.com/andy-takker/asyncly"
37
+ [project.optional-dependencies]
38
+ msgspec = ["msgspec>=0.19.0,<0.20"]
39
+ pydantic = ["pydantic>=2.8.2,<3"]
40
+ orjson = ["orjson>=3.10.6,<4"]
41
+
42
+ [project.urls]
43
+ Homepage = "https://github.com/andy-takker/asyncly"
44
+ Source = "https://github.com/andy-takker/asyncly"
37
45
  "Bug Tracker" = "https://github.com/andy-takker/asyncly/issues"
38
46
 
39
- [tool.poetry.dependencies]
40
- python = "^3.10"
41
- aiohttp = "^3.9.5"
42
- msgspec = {version = "^0.18.6", optional = true}
43
- orjson = {version = "^3.10.6", optional = true}
44
- pydantic = {version = "^2.8.2", optional = true}
45
-
46
- [tool.poetry.extras]
47
- msgspec = ["msgspec"]
48
- pydantic = ["pydantic"]
49
- orjson = ["orjson"]
50
-
51
- [tool.poetry.group.dev.dependencies]
52
- pre-commit = ">=3.7.1,<5.0.0"
53
- mypy = "^1.10.1"
54
- ruff = ">=0.5.2,<0.9.0"
55
- restructuredtext-lint = "^1.4.0"
56
- pygments = "^2.18.0"
57
- pytest = "^8.3.3"
58
- pytest-asyncio = "^0.24.0"
59
- pytest-cov = "^6.0.0"
47
+ [dependency-groups]
48
+ dev = [
49
+ "pre-commit>=3.7.1,<5.0.0",
50
+ "mypy>=1.17.1,<2",
51
+ "ruff>=0.5.2,<0.9.0",
52
+ "restructuredtext-lint>=1.4.0,<2",
53
+ "pygments>=2.18.0,<3",
54
+ "pytest>=8.3.3,<9",
55
+ "pytest-asyncio>=0.24.0,<0.25",
56
+ "pytest-cov>=6.0.0,<7",
57
+ "types-pyyaml>=6.0.12.20250822",
58
+ "types-toml>=0.10.8.20240310",
59
+ "toml>=0.10.2",
60
+ ]
61
+
62
+ [tool.hatch.build.targets.sdist]
63
+ include = ["asyncly"]
64
+
65
+ [tool.hatch.build.targets.wheel]
66
+ include = ["asyncly"]
60
67
 
61
68
  [build-system]
62
- requires = ["poetry-core"]
63
- build-backend = "poetry.core.masonry.api"
69
+ requires = ["hatchling"]
70
+ build-backend = "hatchling.build"
64
71
 
65
72
  [tool.pytest.ini_options]
66
73
  asyncio_mode = "auto"
@@ -134,4 +141,4 @@ module = [
134
141
  "msgspec.*",
135
142
  "orjson.*",
136
143
  ]
137
- ignore_missing_imports = true
144
+ ignore_missing_imports = true
@@ -1,15 +0,0 @@
1
- from collections.abc import Awaitable, Callable
2
- from typing import TypeVar
3
-
4
- from aiohttp import ClientResponse
5
- from msgspec import Struct
6
- from msgspec.json import decode
7
-
8
- T = TypeVar("T", bound=Struct)
9
-
10
-
11
- def parse_struct(struct: type[T]) -> Callable[[ClientResponse], Awaitable[T]]:
12
- async def _parse(response: ClientResponse) -> T:
13
- return decode(await response.read(), type=struct)
14
-
15
- return _parse
@@ -1,10 +0,0 @@
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
- )
@@ -1,80 +0,0 @@
1
- from asyncio import sleep
2
- from collections.abc import Iterable, Iterator, Mapping, MutableMapping
3
- from dataclasses import dataclass
4
- from http import HTTPStatus
5
- from typing import Any
6
-
7
- from aiohttp import hdrs
8
- from aiohttp.web_request import Request
9
- from aiohttp.web_response import Response
10
-
11
- from asyncly.srvmocker.models import BaseMockResponse
12
- from asyncly.srvmocker.serialization import JsonSerializer, Serializer
13
-
14
- TimeoutType = int | float
15
-
16
-
17
- @dataclass
18
- class ContentResponse(BaseMockResponse):
19
- body: Any = None
20
- status: int = HTTPStatus.OK
21
- headers: Mapping[str, str] | None = None
22
- serializer: Serializer | None = None
23
-
24
- async def response(self, request: Request) -> Response:
25
- headers: MutableMapping[str, str] = dict()
26
- if self.headers:
27
- headers.update(self.headers)
28
- if self.serializer:
29
- headers[hdrs.CONTENT_TYPE] = self.serializer.content_type
30
- return Response(
31
- status=self.status,
32
- body=self.serialize(),
33
- headers=headers,
34
- )
35
-
36
- def serialize(self) -> Any:
37
- if not self.serializer:
38
- return self.body
39
- return self.serializer.dumps(self.body)
40
-
41
-
42
- @dataclass
43
- class LatencyResponse(BaseMockResponse):
44
- wrapped: BaseMockResponse
45
- latency: TimeoutType
46
-
47
- async def response(self, request: Request) -> Response:
48
- await sleep(self.latency)
49
- return await self.wrapped.response(request)
50
-
51
-
52
- class MockSeqResponse(BaseMockResponse):
53
- responses: Iterator[BaseMockResponse]
54
-
55
- def __init__(self, responses: Iterable[BaseMockResponse]) -> None:
56
- self.responses = iter(responses)
57
-
58
- async def response(self, request: Request) -> Response:
59
- resp = next(self.responses)
60
- 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)
@@ -1,16 +0,0 @@
1
- import json
2
- from collections.abc import Callable
3
- from dataclasses import dataclass
4
- from typing import Any, Final
5
-
6
-
7
- @dataclass(frozen=True)
8
- class Serializer:
9
- dumps: Callable[[Any], str]
10
- content_type: str
11
-
12
-
13
- JsonSerializer: Final = Serializer(
14
- dumps=json.dumps,
15
- content_type="application/json",
16
- )
File without changes
File without changes