rsgiadapter 0.0.3__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.
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.1
2
+ Name: rsgiadapter
3
+ Version: 0.0.3
4
+ Summary: An adapter for asgi to rsgi
5
+ Keywords: asgi,rsgi,adapter,asgi adapter,rsgi adapter,asgi to rsgi,rsgi to asgi,asyncio
6
+ Author-Email: belingud <im.victor@qq.com>
7
+ License: BSD-3-Clause
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.8
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: License :: OSI Approved :: BSD License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Development Status :: 3 - Alpha
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: Intended Audience :: Information Technology
19
+ Classifier: Intended Audience :: System Administrators
20
+ Classifier: Topic :: Internet
21
+ Classifier: Topic :: Internet :: WWW/HTTP
22
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
23
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Classifier: Topic :: Software Development :: Libraries
26
+ Classifier: Topic :: Software Development
27
+ Classifier: Typing :: Typed
28
+ Project-URL: Homepage, https://github.com/belingud/rsgiadapter
29
+ Requires-Python: <4.0,>=3.8
30
+ Requires-Dist: asgiref>=3.8.1
31
+ Description-Content-Type: text/markdown
32
+
33
+ # rsgiadapter
34
+
35
+ An Asgi to rsgi adapter.
36
+
37
+ RSGI Specification ref: https://github.com/emmett-framework/granian/blob/master/docs/spec/RSGI.md
38
+
39
+ `rsgiadapter` is an adapter for [RSGI](https://github.com/emmett-framework/granian/blob/master/docs/spec/RSGI.md) server run [ASGI](https://asgi.readthedocs.io) application like FastAPI and BlackSheep.
40
+
41
+ Usage:
42
+
43
+ `app.py`
44
+ ```python
45
+ import granian
46
+ from granian.constants import Interfaces
47
+ from rsgiadapter import ASGIToRSGI
48
+
49
+ app = None # Define your asgi application here
50
+
51
+ rsgi_app = ASGIToRSGI(app)
52
+
53
+ serve = granian.Granian("app:rsgi_app", interface=Interfaces.RSGI)
54
+ serve.serve()
55
+
56
+ ```
57
+
58
+ Supported Feature:
59
+
60
+ - [x] HTTP Request Response
61
+ - [x] ASGI scope
62
+ - [x] ASGI receive
63
+ - [x] ASGI send
64
+ - [x] Extensions
65
+ - [x] http.response.pathsend
66
+ - [ ] websocket.http.response
67
+ - [ ] http.response.push
68
+ - [ ] http.response.zerocopysend
69
+ - [ ] http.response.early_hint
70
+ - [ ] http.response.trailers
71
+ - [ ] http.response.debug
72
+ - [ ] Lifespan
73
+ - [ ] lifespan.startup
74
+ - [ ] lifespan.startup.complete
75
+ - [ ] lifespan.startup.failed
76
+ - [ ] lifespan.shutdown
77
+ - [ ] lifespan.shutdown.complete
78
+ - [ ] lifespan.shutdown.failed
79
+
80
+ Ref:
81
+
82
+ - Granian: https://github.com/emmett-framework/granian
@@ -0,0 +1,50 @@
1
+ # rsgiadapter
2
+
3
+ An Asgi to rsgi adapter.
4
+
5
+ RSGI Specification ref: https://github.com/emmett-framework/granian/blob/master/docs/spec/RSGI.md
6
+
7
+ `rsgiadapter` is an adapter for [RSGI](https://github.com/emmett-framework/granian/blob/master/docs/spec/RSGI.md) server run [ASGI](https://asgi.readthedocs.io) application like FastAPI and BlackSheep.
8
+
9
+ Usage:
10
+
11
+ `app.py`
12
+ ```python
13
+ import granian
14
+ from granian.constants import Interfaces
15
+ from rsgiadapter import ASGIToRSGI
16
+
17
+ app = None # Define your asgi application here
18
+
19
+ rsgi_app = ASGIToRSGI(app)
20
+
21
+ serve = granian.Granian("app:rsgi_app", interface=Interfaces.RSGI)
22
+ serve.serve()
23
+
24
+ ```
25
+
26
+ Supported Feature:
27
+
28
+ - [x] HTTP Request Response
29
+ - [x] ASGI scope
30
+ - [x] ASGI receive
31
+ - [x] ASGI send
32
+ - [x] Extensions
33
+ - [x] http.response.pathsend
34
+ - [ ] websocket.http.response
35
+ - [ ] http.response.push
36
+ - [ ] http.response.zerocopysend
37
+ - [ ] http.response.early_hint
38
+ - [ ] http.response.trailers
39
+ - [ ] http.response.debug
40
+ - [ ] Lifespan
41
+ - [ ] lifespan.startup
42
+ - [ ] lifespan.startup.complete
43
+ - [ ] lifespan.startup.failed
44
+ - [ ] lifespan.shutdown
45
+ - [ ] lifespan.shutdown.complete
46
+ - [ ] lifespan.shutdown.failed
47
+
48
+ Ref:
49
+
50
+ - Granian: https://github.com/emmett-framework/granian
@@ -0,0 +1,92 @@
1
+ [project]
2
+ name = "rsgiadapter"
3
+ dynamic = []
4
+ description = "An adapter for asgi to rsgi"
5
+ authors = [
6
+ { name = "belingud", email = "im.victor@qq.com" },
7
+ ]
8
+ dependencies = [
9
+ "asgiref>=3.8.1",
10
+ ]
11
+ requires-python = ">=3.8,<4.0"
12
+ readme = "README.md"
13
+ keywords = [
14
+ "asgi",
15
+ "rsgi",
16
+ "adapter",
17
+ "asgi adapter",
18
+ "rsgi adapter",
19
+ "asgi to rsgi",
20
+ "rsgi to asgi",
21
+ "asyncio",
22
+ ]
23
+ classifiers = [
24
+ "Programming Language :: Python :: 3",
25
+ "Programming Language :: Python :: 3.8",
26
+ "Programming Language :: Python :: 3.9",
27
+ "Programming Language :: Python :: 3.10",
28
+ "Programming Language :: Python :: 3.11",
29
+ "Programming Language :: Python :: 3.12",
30
+ "License :: OSI Approved :: BSD License",
31
+ "Operating System :: OS Independent",
32
+ "Development Status :: 3 - Alpha",
33
+ "Intended Audience :: Developers",
34
+ "Intended Audience :: Information Technology",
35
+ "Intended Audience :: System Administrators",
36
+ "Topic :: Internet",
37
+ "Topic :: Internet :: WWW/HTTP",
38
+ "Topic :: Internet :: WWW/HTTP :: HTTP Servers",
39
+ "Topic :: Software Development :: Libraries :: Application Frameworks",
40
+ "Topic :: Software Development :: Libraries :: Python Modules",
41
+ "Topic :: Software Development :: Libraries",
42
+ "Topic :: Software Development",
43
+ "Typing :: Typed",
44
+ ]
45
+ version = "0.0.3"
46
+
47
+ [project.license]
48
+ text = "BSD-3-Clause"
49
+
50
+ [project.urls]
51
+ Homepage = "https://github.com/belingud/rsgiadapter"
52
+
53
+ [build-system]
54
+ requires = [
55
+ "pdm-backend",
56
+ ]
57
+ build-backend = "pdm.backend"
58
+
59
+ [tool.pdm]
60
+ distribution = true
61
+
62
+ [tool.pdm.build]
63
+ excludes = [
64
+ "./**/.git",
65
+ ]
66
+ package-dir = "src"
67
+ includes = [
68
+ "src/rsgiadapter",
69
+ ]
70
+
71
+ [tool.pdm.version]
72
+ source = "file"
73
+ path = "src/rsgiadapter/__init__.py"
74
+
75
+ [tool.pdm.dev-dependencies]
76
+ dev = [
77
+ "fastapi>=0.111.0",
78
+ "black>=24.4.2",
79
+ "isort>=5.13.2",
80
+ "granian>=1.4.1",
81
+ "uvicorn>=0.30.1",
82
+ "bump2version>=1.0.1",
83
+ ]
84
+
85
+ [tool.pytest.ini_options]
86
+ addopts = "-rsxX -l --tb=short --strict"
87
+ testpaths = [
88
+ "tests/",
89
+ ]
90
+ python_files = [
91
+ "tests/*/test*.py",
92
+ ]
@@ -0,0 +1,5 @@
1
+ from .asgi import ASGIToRSGI
2
+
3
+ __all__ = ["ASGIToRSGI"]
4
+
5
+ __version__ = "0.0.3"
@@ -0,0 +1,216 @@
1
+ import asyncio
2
+ import logging
3
+ from os import PathLike, environ
4
+ from typing import AsyncGenerator, TYPE_CHECKING, Union
5
+
6
+ from asgiref.compatibility import guarantee_single_callable
7
+
8
+ from .constant import EventTypeEnum
9
+
10
+ if TYPE_CHECKING:
11
+ from .protocol import (
12
+ ASGIScope,
13
+ RSGIHTTPProtocol,
14
+ RSGIHTTPScope,
15
+ RSGIWebsocketProtocol,
16
+ RSGIWebsocketScope,
17
+ )
18
+ from .response import BodyIter, Response
19
+
20
+ logger = logging.getLogger("rsgiadapter")
21
+ if environ.get("RSGI_ADAPTER_DEBUG", "0") == "1":
22
+ logger.setLevel(logging.DEBUG)
23
+
24
+ DEFAULT_ASGI_VERSION = "3.0"
25
+ DEFAULT_SPEC_VERSION = "2.3"
26
+
27
+
28
+ class ASGIToRSGI:
29
+
30
+ def __init__(
31
+ self,
32
+ asgi_application,
33
+ asgi_version=DEFAULT_ASGI_VERSION,
34
+ spec_version=DEFAULT_SPEC_VERSION,
35
+ ):
36
+ self.asgi_application = asgi_application
37
+ self.asgi_version = asgi_version
38
+ self.spec_version = spec_version
39
+
40
+ async def __call__(self, scope, protocol):
41
+ await ASGIToRSGIAdapter(
42
+ self.asgi_application, self.asgi_version, self.spec_version
43
+ )(scope, protocol)
44
+
45
+
46
+ class ASGIToRSGIAdapter:
47
+ def __init__(
48
+ self,
49
+ asgi_app,
50
+ asgi_version=DEFAULT_ASGI_VERSION,
51
+ spec_version=DEFAULT_SPEC_VERSION,
52
+ ):
53
+ self.asgi_app = asgi_app
54
+ self.asgi_version = asgi_version
55
+ self.spec_version = spec_version
56
+ self.event_status = EventTypeEnum.HTTP_REQUEST
57
+
58
+ self.state = {}
59
+ self.response_started = False
60
+ self.response_content_length = None
61
+
62
+ async def yield_body(
63
+ self, protocol: Union["RSGIHTTPProtocol", "RSGIWebsocketProtocol"]
64
+ ) -> AsyncGenerator:
65
+ """
66
+ Asynchronously yields messages from the given rsgi `protocol`.
67
+
68
+ Args:
69
+ protocol (RSGIHTTPProtocol | RSGIWebsocketProtocol): RSGIHTTPProtocol or RSGIWebsocketProtocol instance.
70
+
71
+ Yields:
72
+ Any: The next message from the rsgi protocol.
73
+
74
+ """
75
+ async for msg in protocol:
76
+ yield msg
77
+
78
+ def make_asgi_scope(
79
+ self, scope: Union["RSGIHTTPScope", "RSGIWebsocketScope"]
80
+ ) -> "ASGIScope":
81
+ """
82
+ Generates an ASGI scope based on RSGI scope, extracting relevant information,
83
+ such as versions, protocol, HTTP version, server details, client details, scheme,
84
+ method, path, query string, headers, and state.
85
+
86
+ Args:
87
+ scope (Union["RSGIHTTPScope", "RSGIWebsocketScope"]): The scope object containing the necessary information.
88
+
89
+ Returns:
90
+ dict: A dictionary representing the ASGI scope with version details
91
+
92
+ Raises:
93
+ ValueError: If the scope is None
94
+ """
95
+ if not scope:
96
+ raise ValueError("Scope cannot be None")
97
+
98
+ asgi_version = self.asgi_version
99
+ spec_version = self.spec_version
100
+ proto = scope.proto
101
+ http_version = scope.http_version
102
+ server = scope.server.split(":") if scope.server else []
103
+ client = scope.client.split(":") if scope.client else []
104
+ scheme = scope.scheme
105
+ method = scope.method
106
+ path = scope.path
107
+ raw_path = path.encode("latin-1") if path else b""
108
+ query_string = (
109
+ scope.query_string.encode("latin-1") if scope.query_string else b""
110
+ )
111
+ headers = (
112
+ [
113
+ (k.encode("latin-1"), v.encode("latin-1"))
114
+ for k, v in scope.headers.items()
115
+ ]
116
+ if scope.headers
117
+ else []
118
+ )
119
+
120
+ return {
121
+ "asgi": {"version": asgi_version, "spec_version": spec_version},
122
+ "extensions": {"http.response.pathsend": {}},
123
+ "type": proto,
124
+ "http_version": http_version,
125
+ "server": server,
126
+ "client": client,
127
+ "scheme": scheme,
128
+ "method": method,
129
+ "path": path,
130
+ "raw_path": raw_path,
131
+ "query_string": query_string,
132
+ "headers": headers,
133
+ "root_path": "",
134
+ "state": self.state,
135
+ }
136
+
137
+ async def __call__(
138
+ self,
139
+ scope: Union["RSGIHTTPScope", "RSGIWebsocketScope"],
140
+ protocol: Union["RSGIHTTPProtocol", "RSGIWebsocketProtocol"],
141
+ ):
142
+ asgi_scope = self.make_asgi_scope(scope)
143
+ send_queue = asyncio.Queue()
144
+ asgi_body = self.yield_body(protocol)
145
+
146
+ async def receive():
147
+ try:
148
+ return {
149
+ "type": self.event_status,
150
+ "body": await anext(asgi_body),
151
+ "more_body": True,
152
+ }
153
+ except StopAsyncIteration:
154
+ return {
155
+ "type": self.event_status,
156
+ "body": b"",
157
+ "more_body": False,
158
+ }
159
+
160
+ async def send(msg):
161
+ if msg.get("more_body", None) is False:
162
+ self.event_status = EventTypeEnum.HTTP_DISCONNECT
163
+ await send_queue.put(msg)
164
+
165
+ try:
166
+ await guarantee_single_callable(self.asgi_app)(asgi_scope, receive, send)
167
+ except asyncio.CancelledError:
168
+ logger.debug("ASGI app cancelled")
169
+ except Exception:
170
+ logger.debug("ASGI app raised an exception", exc_info=True)
171
+ response = await self.get_response(send_queue)
172
+
173
+ await self.perform_response(protocol, response)
174
+
175
+ async def get_response(self, send_queue: asyncio.Queue):
176
+ response = Response(
177
+ status=None, headers=[], body=BodyIter(), path=None, stream=None, type=None
178
+ )
179
+ while not send_queue.empty():
180
+ message = await send_queue.get()
181
+ if message["type"] == EventTypeEnum.HTTP_RESP_START:
182
+ response.status = message["status"]
183
+ response.headers = [
184
+ (k.decode(), v.decode()) for k, v in message["headers"]
185
+ ]
186
+ elif message["type"] == EventTypeEnum.HTTP_RESP_BODY:
187
+ response.body.append(message["body"])
188
+ elif message["type"] == EventTypeEnum.PATH_SEND:
189
+ response.path = message["path"]
190
+ response.type = EventTypeEnum.PATH_SEND
191
+ return response
192
+
193
+ async def perform_response(
194
+ self,
195
+ protocol: Union["RSGIHTTPProtocol", "RSGIWebsocketProtocol"],
196
+ response: Response,
197
+ ) -> None:
198
+ if response.path is not None and isinstance(response.path, (str, PathLike)):
199
+ protocol.response_file(
200
+ status=response.status, headers=response.headers, file=response.path
201
+ )
202
+ return
203
+ if len(response.body) > 1:
204
+ trx = protocol.response_stream(
205
+ status=response.status,
206
+ headers=response.headers,
207
+ )
208
+ async for chunk in response.body:
209
+ await trx.send_bytes(chunk)
210
+ else:
211
+ protocol.response_bytes(
212
+ status=response.status,
213
+ headers=response.headers,
214
+ body=b"".join(response.get_body()),
215
+ )
216
+ response.clear_body()
@@ -0,0 +1,22 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class EventTypeEnum(StrEnum):
5
+ """
6
+ ASGI event types
7
+ """
8
+ # http
9
+ HTTP_REQUEST = "http.request"
10
+ HTTP_DISCONNECT = "http.disconnect"
11
+ HTTP_RESP_START = "http.response.start"
12
+ HTTP_RESP_END = "http.response.end"
13
+ HTTP_RESP_BODY = "http.response.body"
14
+
15
+ # websocket
16
+ WEBSOCKET_CONNECT = "websocket.connect"
17
+ WEBSOCKET_DISCONNECT = "websocket.disconnect"
18
+ WEBSOCKET_RECEIVE = "websocket.receive"
19
+ WEBSOCKET_SEND = "websocket.send"
20
+
21
+ # extensions
22
+ PATH_SEND = "http.response.pathsend"
@@ -0,0 +1,141 @@
1
+ from typing import Any, Dict, Iterable, List, Optional, Tuple, TypedDict, Union
2
+
3
+
4
+ class ASGIScope(TypedDict):
5
+ asgi: Dict[str, str]
6
+ extensions: Dict[str, Dict[str, Any]]
7
+ type: str
8
+ http_version: str
9
+ server: Iterable[Union[str, int, None]]
10
+ client: Iterable[Union[str, int, None]]
11
+ scheme: str
12
+ method: str
13
+ path: str
14
+ raw_path: bytes
15
+ query_string: bytes
16
+ headers: Iterable[Tuple[Union[str, bytes], Union[str, bytes]]]
17
+ root_path: str
18
+ state: Optional[Dict[str, Any]]
19
+
20
+
21
+ class RSGIHTTPScope(object):
22
+ """RSGI HTTP Scope template, for type hinting"""
23
+
24
+ def __init__(self, *args, **kwargs):
25
+ pass
26
+
27
+ @staticmethod
28
+ def __new__(cls, *args, **kwargs):
29
+ pass
30
+
31
+ authority = property(
32
+ lambda self: object(), lambda self, v: None, lambda self: None
33
+ ) # default
34
+
35
+ client = property(
36
+ lambda self: object(), lambda self, v: None, lambda self: None
37
+ ) # default
38
+
39
+ headers = property(
40
+ lambda self: object(), lambda self, v: None, lambda self: None
41
+ ) # default
42
+
43
+ http_version = property(
44
+ lambda self: object(), lambda self, v: None, lambda self: None
45
+ ) # default
46
+
47
+ method = property(
48
+ lambda self: object(), lambda self, v: None, lambda self: None
49
+ ) # default
50
+
51
+ path = property(
52
+ lambda self: object(), lambda self, v: None, lambda self: None
53
+ ) # default
54
+
55
+ proto = property(
56
+ lambda self: object(), lambda self, v: None, lambda self: None
57
+ ) # default
58
+
59
+ query_string = property(
60
+ lambda self: object(), lambda self, v: None, lambda self: None
61
+ ) # default
62
+
63
+ rsgi_version = property(
64
+ lambda self: object(), lambda self, v: None, lambda self: None
65
+ ) # default
66
+
67
+ scheme = property(
68
+ lambda self: object(), lambda self, v: None, lambda self: None
69
+ ) # default
70
+
71
+ server = property(
72
+ lambda self: object(), lambda self, v: None, lambda self: None
73
+ ) # default
74
+
75
+
76
+ class RSGIHTTPProtocol(object):
77
+ """RSGI HTTP Protocol template, for type hinting"""
78
+
79
+ proto: str
80
+ http_version: str
81
+ rsgi_version: str
82
+ server: str
83
+ client: str
84
+ scheme: str
85
+ method: str
86
+ path: str
87
+ query_string: str
88
+ headers: List
89
+ body: bytes
90
+
91
+ def response_bytes(self, *args, **kwargs):
92
+ pass
93
+
94
+ def response_empty(self, *args, **kwargs):
95
+ pass
96
+
97
+ def response_file(self, *args, **kwargs):
98
+ pass
99
+
100
+ def response_str(self, *args, **kwargs):
101
+ pass
102
+
103
+ def response_stream(self, *args, **kwargs):
104
+ pass
105
+
106
+ def __aiter__(self, *args, **kwargs):
107
+ return self
108
+
109
+ async def __anext__(self, *args, **kwargs):
110
+ if self.data:
111
+ return self.data.pop(0)
112
+ raise StopAsyncIteration
113
+
114
+ def __call__(self, *args, **kwargs):
115
+ pass
116
+
117
+ def __init__(self, *args, **kwargs):
118
+ self.data = list(args)
119
+
120
+ @staticmethod
121
+ def __new__(cls, *args, **kwargs):
122
+ pass
123
+
124
+
125
+ class RSGIWebsocketProtocol(object):
126
+ def accept(self, *args, **kwargs):
127
+ pass
128
+
129
+ def close(self, *args, **kwargs):
130
+ pass
131
+
132
+ def __init__(self, *args, **kwargs):
133
+ pass
134
+
135
+ @staticmethod
136
+ def __new__(cls, *args, **kwargs):
137
+ pass
138
+
139
+
140
+ class RSGIWebsocketScope(RSGIHTTPScope):
141
+ pass
File without changes
@@ -0,0 +1,51 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Dict, List, Optional, Tuple, Union
3
+
4
+
5
+ class BodyIter:
6
+ def __init__(self):
7
+ self._body = []
8
+
9
+ def __len__(self):
10
+ return len(self._body)
11
+
12
+ def __iter__(self):
13
+ return self
14
+
15
+ def __next__(self):
16
+ if not self._body:
17
+ raise StopIteration
18
+ return self._body.pop(0)
19
+
20
+ def __aiter__(self):
21
+ return self
22
+
23
+ async def __anext__(self):
24
+ if not self._body:
25
+ raise StopAsyncIteration
26
+ return self._body.pop(0)
27
+
28
+ def append(self, data: Union[Dict[str, Union[bytes, str]], bytes]):
29
+ self._body.append(data)
30
+
31
+ def get_body(self):
32
+ return self._body
33
+
34
+ def clear_body(self):
35
+ self._body.clear()
36
+
37
+
38
+ @dataclass
39
+ class Response:
40
+ status: Optional[int] = None
41
+ headers: Union[List[tuple], Tuple[tuple]] = field(default_factory=list)
42
+ body: BodyIter = field(default_factory=BodyIter)
43
+ path: Optional[str] = b""
44
+ stream: Optional[bool] = False
45
+ type: Optional[str] = None
46
+
47
+ def get_body(self):
48
+ return self.body.get_body()
49
+
50
+ def clear_body(self):
51
+ self.body.clear_body()
File without changes
@@ -0,0 +1,180 @@
1
+ import inspect
2
+ import unittest
3
+ from pathlib import Path
4
+ from types import CodeType
5
+ from unittest.mock import AsyncMock, MagicMock, Mock, NonCallableMock, call
6
+
7
+ from rsgiadapter.asgi import ASGIToRSGIAdapter
8
+ from rsgiadapter.response import BodyIter, Response
9
+
10
+
11
+ class Stream(Mock):
12
+
13
+ send_bytes = AsyncMock()
14
+
15
+ send_str = AsyncMock()
16
+
17
+
18
+ class MockAsyncIterator:
19
+ """
20
+ Wraps an iterator in an asynchronous iterator.
21
+ """
22
+
23
+ def __init__(self, iterator):
24
+ self.iterator = iterator
25
+ code_mock = NonCallableMock(spec_set=CodeType)
26
+ code_mock.co_flags = inspect.CO_ITERABLE_COROUTINE
27
+ self.__dict__["__code__"] = code_mock
28
+
29
+ async def __anext__(self):
30
+ try:
31
+ return next(self.iterator)
32
+ except StopIteration:
33
+ pass
34
+ raise StopAsyncIteration
35
+
36
+ def __aiter__(self):
37
+ return self
38
+
39
+
40
+ class TestMakeASGIScope(unittest.TestCase):
41
+
42
+ def setUp(self):
43
+ self.test_scope = Mock()
44
+ self.test_scope.proto = "http"
45
+ self.test_scope.http_version = "1.1"
46
+ self.test_scope.server = "example.com:80"
47
+ self.test_scope.client = "127.0.0.1:1234"
48
+ self.test_scope.scheme = "https"
49
+ self.test_scope.method = "GET"
50
+ self.test_scope.path = "/test"
51
+ self.test_scope.query_string = "key=value"
52
+ self.test_scope.headers = {"Content-Type": "application/json"}
53
+
54
+ self.asgi_app = ASGIToRSGIAdapter(None)
55
+
56
+ def test_scope_not_none(self):
57
+ result = self.asgi_app.make_asgi_scope(self.test_scope)
58
+ self.assertIsNotNone(result)
59
+
60
+ def test_raise_value_error_if_scope_none(self):
61
+ with self.assertRaises(ValueError):
62
+ self.asgi_app.make_asgi_scope(None)
63
+
64
+ def test_correct_attribute_assignment(self):
65
+ result = self.asgi_app.make_asgi_scope(self.test_scope)
66
+ self.assertEqual(result["type"], "http")
67
+ self.assertEqual(result["http_version"], "1.1")
68
+ self.assertEqual(result["server"], ["example.com", "80"])
69
+ self.assertEqual(result["client"], ["127.0.0.1", "1234"])
70
+ self.assertEqual(result["scheme"], "https")
71
+ self.assertEqual(result["method"], "GET")
72
+ self.assertEqual(result["path"], "/test")
73
+ self.assertEqual(result["raw_path"], b"/test")
74
+ self.assertEqual(result["query_string"], b"key=value")
75
+ self.assertEqual(result["headers"], [(b"Content-Type", b"application/json")])
76
+
77
+
78
+ class TestYieldBody(unittest.IsolatedAsyncioTestCase):
79
+ async def test_yield_body(self):
80
+ # Test case: yielding messages from a protocol
81
+ mock_protocol = AsyncMock()
82
+
83
+ async def mock_protocol_async_iter():
84
+ yield 1
85
+ yield 2
86
+ yield 3
87
+ # 模拟迭代结束
88
+ raise StopAsyncIteration
89
+
90
+ mock_protocol = AsyncMock()
91
+
92
+ mock_protocol.__aiter__ = AsyncMock(return_value=mock_protocol_async_iter())
93
+
94
+ mock_protocol.__anext__.side_effect = mock_protocol_async_iter()
95
+ mock_protocol = MockAsyncIterator(iter([1, 2, 3]))
96
+ adapter = ASGIToRSGIAdapter(None)
97
+ result = []
98
+ async for msg in adapter.yield_body(mock_protocol):
99
+ result.append(msg)
100
+
101
+ self.assertEqual(result, [1, 2, 3])
102
+
103
+ # Test case: yielding an empty result
104
+ protocol = MockAsyncIterator(iter([]))
105
+
106
+ adapter = ASGIToRSGIAdapter(None)
107
+ result = []
108
+ async for msg in adapter.yield_body(protocol):
109
+ result.append(msg)
110
+
111
+ self.assertEqual(result, [])
112
+
113
+ # Test case: yielding a single message
114
+ protocol = MockAsyncIterator(iter([4]))
115
+
116
+ adapter = ASGIToRSGIAdapter(None)
117
+ result = []
118
+ async for msg in adapter.yield_body(protocol):
119
+ result.append(msg)
120
+
121
+ self.assertEqual(result, [4])
122
+
123
+
124
+ class TestPerformResponse(unittest.IsolatedAsyncioTestCase):
125
+
126
+ def setUp(self):
127
+ self.protocol = AsyncMock()
128
+ body = BodyIter()
129
+ body.append(b"hello")
130
+ body.append(b"world")
131
+ self.response = Response(
132
+ status=200,
133
+ headers=[("Content-Type", "text/plain")],
134
+ body=body,
135
+ path=None,
136
+ stream=None,
137
+ type=None,
138
+ )
139
+ self.mock_response_stream_return = Stream()
140
+ self.protocol.response_stream = Mock(
141
+ return_value=self.mock_response_stream_return
142
+ )
143
+ self.protocol.response_bytes = Mock()
144
+ self.protocol.response_file = Mock()
145
+ self.adapter = ASGIToRSGIAdapter(None, None, None)
146
+
147
+ async def test_response_file(self):
148
+ self.response.path = Path("test.txt")
149
+ await self.adapter.perform_response(self.protocol, self.response)
150
+ self.protocol.response_file.assert_called_once_with(
151
+ status=200,
152
+ headers=[("Content-Type", "text/plain")],
153
+ file=Path("test.txt"),
154
+ )
155
+
156
+ async def test_response_stream(self):
157
+ await self.adapter.perform_response(self.protocol, self.response)
158
+ self.protocol.response_stream.assert_called_once_with(
159
+ status=200,
160
+ headers=[("Content-Type", "text/plain")],
161
+ )
162
+ self.mock_response_stream_return.send_bytes.assert_has_calls(
163
+ [
164
+ call(b"hello"),
165
+ call(b"world"),
166
+ ]
167
+ )
168
+
169
+ async def test_response_bytes(self):
170
+ self.response.body = BodyIter()
171
+ await self.adapter.perform_response(self.protocol, self.response)
172
+ self.protocol.response_bytes.assert_called_once_with(
173
+ status=200,
174
+ headers=[("Content-Type", "text/plain")],
175
+ body=b"",
176
+ )
177
+
178
+
179
+ if __name__ == "__main__":
180
+ unittest.main()