cross-web 0.2.3__py3-none-any.whl
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.
- cross/__init__.py +36 -0
- cross/exceptions.py +8 -0
- cross/protocols.py +20 -0
- cross/py.typed +0 -0
- cross/request/__init__.py +123 -0
- cross/request/_aiohttp.py +111 -0
- cross/request/_base.py +148 -0
- cross/request/_chalice.py +88 -0
- cross/request/_django.py +103 -0
- cross/request/_flask.py +99 -0
- cross/request/_litestar.py +51 -0
- cross/request/_quart.py +45 -0
- cross/request/_sanic.py +83 -0
- cross/request/_starlette.py +58 -0
- cross/request/_testing.py +64 -0
- cross/response.py +82 -0
- cross_web-0.2.3.dist-info/METADATA +32 -0
- cross_web-0.2.3.dist-info/RECORD +20 -0
- cross_web-0.2.3.dist-info/WHEEL +4 -0
- cross_web-0.2.3.dist-info/licenses/LICENSE +22 -0
cross/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from .exceptions import HTTPException
|
|
2
|
+
from .protocols import BaseRequestProtocol
|
|
3
|
+
from .request import AsyncHTTPRequest
|
|
4
|
+
from .request._aiohttp import AiohttpHTTPRequestAdapter
|
|
5
|
+
from .request._base import AsyncHTTPRequestAdapter, FormData, SyncHTTPRequestAdapter
|
|
6
|
+
from .request._chalice import ChaliceHTTPRequestAdapter
|
|
7
|
+
from .request._django import AsyncDjangoHTTPRequestAdapter, DjangoHTTPRequestAdapter
|
|
8
|
+
from .request._flask import AsyncFlaskHTTPRequestAdapter, FlaskHTTPRequestAdapter
|
|
9
|
+
from .request._litestar import LitestarRequestAdapter
|
|
10
|
+
from .request._quart import QuartHTTPRequestAdapter
|
|
11
|
+
from .request._sanic import SanicHTTPRequestAdapter
|
|
12
|
+
from .request._starlette import StarletteRequestAdapter
|
|
13
|
+
from .request._testing import TestingRequestAdapter
|
|
14
|
+
from .response import Cookie, Response
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"AiohttpHTTPRequestAdapter",
|
|
18
|
+
"AsyncDjangoHTTPRequestAdapter",
|
|
19
|
+
"AsyncFlaskHTTPRequestAdapter",
|
|
20
|
+
"AsyncHTTPRequest",
|
|
21
|
+
"AsyncHTTPRequestAdapter",
|
|
22
|
+
"BaseRequestProtocol",
|
|
23
|
+
"ChaliceHTTPRequestAdapter",
|
|
24
|
+
"Cookie",
|
|
25
|
+
"DjangoHTTPRequestAdapter",
|
|
26
|
+
"FlaskHTTPRequestAdapter",
|
|
27
|
+
"FormData",
|
|
28
|
+
"HTTPException",
|
|
29
|
+
"LitestarRequestAdapter",
|
|
30
|
+
"QuartHTTPRequestAdapter",
|
|
31
|
+
"Response",
|
|
32
|
+
"SanicHTTPRequestAdapter",
|
|
33
|
+
"StarletteRequestAdapter",
|
|
34
|
+
"SyncHTTPRequestAdapter",
|
|
35
|
+
"TestingRequestAdapter",
|
|
36
|
+
]
|
cross/exceptions.py
ADDED
cross/protocols.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from typing import Optional, Protocol, Union
|
|
3
|
+
|
|
4
|
+
from .request._base import HTTPMethod
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseRequestProtocol(Protocol):
|
|
8
|
+
"""Protocol defining the minimal interface for HTTP requests."""
|
|
9
|
+
|
|
10
|
+
@property
|
|
11
|
+
def query_params(self) -> Mapping[str, Optional[Union[str, list[str]]]]: ...
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def method(self) -> HTTPMethod: ...
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def headers(self) -> Mapping[str, str]: ...
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = ["BaseRequestProtocol"]
|
cross/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Mapping, Optional, Any
|
|
4
|
+
|
|
5
|
+
from typing_extensions import Self
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from starlette.requests import Request as StarletteRequest
|
|
9
|
+
|
|
10
|
+
from ._base import AsyncHTTPRequestAdapter, FormData, HTTPMethod, QueryParams
|
|
11
|
+
from ._starlette import StarletteRequestAdapter
|
|
12
|
+
from ._testing import TestingRequestAdapter
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AsyncHTTPRequest:
|
|
16
|
+
def __init__(self, adapter: AsyncHTTPRequestAdapter) -> None:
|
|
17
|
+
self._adapter = adapter
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def from_starlette(cls, request: StarletteRequest) -> Self:
|
|
21
|
+
adapter = StarletteRequestAdapter(request)
|
|
22
|
+
|
|
23
|
+
return cls(adapter)
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def from_fastapi(cls, request: StarletteRequest) -> Self:
|
|
27
|
+
return cls.from_starlette(request)
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_django(cls, request: Any) -> Self:
|
|
31
|
+
# Import here to avoid circular imports and optional Django dependency
|
|
32
|
+
from ._django import AsyncDjangoHTTPRequestAdapter
|
|
33
|
+
|
|
34
|
+
adapter = AsyncDjangoHTTPRequestAdapter(request)
|
|
35
|
+
return cls(adapter)
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def from_flask(cls, request: Any) -> Self:
|
|
39
|
+
# Import here to avoid circular imports and optional Flask dependency
|
|
40
|
+
from ._flask import AsyncFlaskHTTPRequestAdapter
|
|
41
|
+
|
|
42
|
+
adapter = AsyncFlaskHTTPRequestAdapter(request)
|
|
43
|
+
return cls(adapter)
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def from_sanic(cls, request: Any) -> Self:
|
|
47
|
+
# Import here to avoid circular imports and optional Sanic dependency
|
|
48
|
+
from ._sanic import SanicHTTPRequestAdapter
|
|
49
|
+
|
|
50
|
+
adapter = SanicHTTPRequestAdapter(request)
|
|
51
|
+
return cls(adapter)
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def from_aiohttp(cls, request: Any) -> Self:
|
|
55
|
+
# Import here to avoid circular imports and optional aiohttp dependency
|
|
56
|
+
from ._aiohttp import AiohttpHTTPRequestAdapter
|
|
57
|
+
|
|
58
|
+
adapter = AiohttpHTTPRequestAdapter(request)
|
|
59
|
+
return cls(adapter)
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def from_quart(cls, request: Any) -> Self:
|
|
63
|
+
# Import here to avoid circular imports and optional Quart dependency
|
|
64
|
+
from ._quart import QuartHTTPRequestAdapter
|
|
65
|
+
|
|
66
|
+
adapter = QuartHTTPRequestAdapter(request)
|
|
67
|
+
return cls(adapter)
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def from_litestar(cls, request: Any) -> Self:
|
|
71
|
+
# Import here to avoid circular imports and optional Litestar dependency
|
|
72
|
+
from ._litestar import LitestarRequestAdapter
|
|
73
|
+
|
|
74
|
+
adapter = LitestarRequestAdapter(request)
|
|
75
|
+
return cls(adapter)
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def from_form_data(cls, data: Mapping[str, str]) -> Self:
|
|
79
|
+
adapter = TestingRequestAdapter(
|
|
80
|
+
form_data=FormData(files={}, form=data),
|
|
81
|
+
content_type="application/x-www-form-urlencoded",
|
|
82
|
+
)
|
|
83
|
+
return cls(adapter)
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def method(self) -> HTTPMethod:
|
|
87
|
+
"""The HTTP method of the request."""
|
|
88
|
+
return self._adapter.method
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def query_params(self) -> QueryParams:
|
|
92
|
+
"""The query parameters of the request."""
|
|
93
|
+
return self._adapter.query_params
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def headers(self) -> Mapping[str, str]:
|
|
97
|
+
"""The request headers (case-insensitive keys recommended)."""
|
|
98
|
+
return self._adapter.headers
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def content_type(self) -> Optional[str]:
|
|
102
|
+
"""The 'Content-Type' header value, if present."""
|
|
103
|
+
return self._adapter.content_type
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def url(self) -> str:
|
|
107
|
+
"""The URL of the request."""
|
|
108
|
+
return self._adapter.url
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def cookies(self) -> Mapping[str, str]:
|
|
112
|
+
"""The request cookies."""
|
|
113
|
+
return self._adapter.cookies
|
|
114
|
+
|
|
115
|
+
async def get_body(self) -> bytes:
|
|
116
|
+
"""Return the raw request body as bytes."""
|
|
117
|
+
return await self._adapter.get_body()
|
|
118
|
+
|
|
119
|
+
async def get_form_data(self) -> FormData:
|
|
120
|
+
"""
|
|
121
|
+
Return parsed form data (multipart/form-data or application/x-www-form-urlencoded).
|
|
122
|
+
"""
|
|
123
|
+
return await self._adapter.get_form_data()
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Mapping, Optional, cast
|
|
5
|
+
|
|
6
|
+
from ._base import AsyncHTTPRequestAdapter, FormData, HTTPMethod, QueryParams
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from aiohttp import web
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AiohttpHTTPRequestAdapter(AsyncHTTPRequestAdapter):
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
request: web.Request,
|
|
16
|
+
body: Optional[bytes] = None,
|
|
17
|
+
form_data: Optional[FormData] = None,
|
|
18
|
+
) -> None:
|
|
19
|
+
self.request = request
|
|
20
|
+
self._body = body
|
|
21
|
+
self._form_data = form_data
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
async def create(cls, request: web.Request) -> "AiohttpHTTPRequestAdapter":
|
|
25
|
+
"""Create an adapter and pre-read the body to avoid PayloadAccessError"""
|
|
26
|
+
content_type = request.headers.get("content-type", "")
|
|
27
|
+
form_data = None
|
|
28
|
+
body = None
|
|
29
|
+
|
|
30
|
+
if content_type.startswith("multipart/form-data"):
|
|
31
|
+
# Pre-process multipart data
|
|
32
|
+
reader = await request.multipart()
|
|
33
|
+
data: dict[str, Any] = {}
|
|
34
|
+
files: dict[str, Any] = {}
|
|
35
|
+
|
|
36
|
+
while field := await reader.next():
|
|
37
|
+
from aiohttp.multipart import BodyPartReader
|
|
38
|
+
|
|
39
|
+
assert isinstance(field, BodyPartReader)
|
|
40
|
+
assert field.name
|
|
41
|
+
|
|
42
|
+
if field.filename:
|
|
43
|
+
files[field.name] = BytesIO(await field.read(decode=False))
|
|
44
|
+
else:
|
|
45
|
+
data[field.name] = await field.text()
|
|
46
|
+
|
|
47
|
+
form_data = FormData(files=files, form=data)
|
|
48
|
+
else:
|
|
49
|
+
# For non-multipart requests, read the body
|
|
50
|
+
body = await request.read()
|
|
51
|
+
|
|
52
|
+
return cls(request, body, form_data)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def query_params(self) -> QueryParams:
|
|
56
|
+
return cast(QueryParams, self.request.query.copy()) # type: ignore[attr-defined]
|
|
57
|
+
|
|
58
|
+
async def get_body(self) -> bytes:
|
|
59
|
+
if self._form_data is not None:
|
|
60
|
+
return b""
|
|
61
|
+
if self._body is None:
|
|
62
|
+
self._body = await self.request.read()
|
|
63
|
+
return self._body
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def method(self) -> HTTPMethod:
|
|
67
|
+
return cast("HTTPMethod", self.request.method.upper())
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def headers(self) -> Mapping[str, str]:
|
|
71
|
+
return self.request.headers
|
|
72
|
+
|
|
73
|
+
async def get_form_data(self) -> FormData:
|
|
74
|
+
if self._form_data is not None:
|
|
75
|
+
return self._form_data
|
|
76
|
+
|
|
77
|
+
if self.content_type and self.content_type.startswith("multipart/form-data"):
|
|
78
|
+
# Process multipart data
|
|
79
|
+
reader = await self.request.multipart()
|
|
80
|
+
data: dict[str, Any] = {}
|
|
81
|
+
files: dict[str, Any] = {}
|
|
82
|
+
|
|
83
|
+
while field := await reader.next():
|
|
84
|
+
from aiohttp.multipart import BodyPartReader
|
|
85
|
+
|
|
86
|
+
assert isinstance(field, BodyPartReader)
|
|
87
|
+
assert field.name
|
|
88
|
+
|
|
89
|
+
if field.filename:
|
|
90
|
+
files[field.name] = BytesIO(await field.read(decode=False))
|
|
91
|
+
else:
|
|
92
|
+
data[field.name] = await field.text()
|
|
93
|
+
|
|
94
|
+
self._form_data = FormData(files=files, form=data)
|
|
95
|
+
return self._form_data
|
|
96
|
+
else:
|
|
97
|
+
# For URL-encoded form data
|
|
98
|
+
post_data = await self.request.post()
|
|
99
|
+
return FormData(files={}, form=dict(post_data))
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def content_type(self) -> Optional[str]:
|
|
103
|
+
return self.headers.get("content-type")
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def url(self) -> str:
|
|
107
|
+
return str(self.request.url)
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def cookies(self) -> Mapping[str, str]:
|
|
111
|
+
return self.request.cookies
|
cross/request/_base.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
from collections.abc import Mapping
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Literal, Optional, Union
|
|
5
|
+
|
|
6
|
+
HTTPMethod = Literal[
|
|
7
|
+
"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "TRACE"
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
QueryParams = Mapping[str, Optional[str]]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class FormData:
|
|
15
|
+
files: Mapping[str, Any]
|
|
16
|
+
form: Mapping[str, Any]
|
|
17
|
+
|
|
18
|
+
def get(self, key: str) -> Any:
|
|
19
|
+
return self.form.get(key)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SyncHTTPRequestAdapter(abc.ABC):
|
|
23
|
+
"""
|
|
24
|
+
Abstract Base Class defining the interface for accessing HTTP request data
|
|
25
|
+
in a framework-agnostic way for synchronous operations.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
@abc.abstractmethod
|
|
30
|
+
def method(self) -> HTTPMethod:
|
|
31
|
+
"""The HTTP method of the request (e.g., 'GET', 'POST')."""
|
|
32
|
+
raise NotImplementedError
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
@abc.abstractmethod
|
|
36
|
+
def query_params(self) -> QueryParams:
|
|
37
|
+
"""The query parameters of the request."""
|
|
38
|
+
raise NotImplementedError
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
@abc.abstractmethod
|
|
42
|
+
def headers(self) -> Mapping[str, str]:
|
|
43
|
+
"""The request headers. Header names should ideally be case-insensitive."""
|
|
44
|
+
# Note: Real implementations might need to handle case-insensitivity
|
|
45
|
+
raise NotImplementedError
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
@abc.abstractmethod
|
|
49
|
+
def content_type(self) -> Optional[str]:
|
|
50
|
+
"""The 'Content-Type' header value, if present."""
|
|
51
|
+
raise NotImplementedError
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
@abc.abstractmethod
|
|
55
|
+
def body(self) -> Union[str, bytes]:
|
|
56
|
+
"""Return the raw request body as bytes or string."""
|
|
57
|
+
raise NotImplementedError
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
@abc.abstractmethod
|
|
61
|
+
def post_data(self) -> Mapping[str, Union[str, bytes]]:
|
|
62
|
+
"""Return the parsed POST data as a mapping of field names to values."""
|
|
63
|
+
raise NotImplementedError
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
@abc.abstractmethod
|
|
67
|
+
def files(self) -> Mapping[str, Any]:
|
|
68
|
+
"""Return uploaded files from the request."""
|
|
69
|
+
raise NotImplementedError
|
|
70
|
+
|
|
71
|
+
@abc.abstractmethod
|
|
72
|
+
def get_form_data(self) -> FormData:
|
|
73
|
+
"""
|
|
74
|
+
Return parsed form data (multipart/form-data or application/x-www-form-urlencoded).
|
|
75
|
+
Returns an empty FormData object if the content type is not form data
|
|
76
|
+
or if parsing fails.
|
|
77
|
+
"""
|
|
78
|
+
raise NotImplementedError
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
@abc.abstractmethod
|
|
82
|
+
def url(self) -> str:
|
|
83
|
+
"""The URL of the request."""
|
|
84
|
+
raise NotImplementedError
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
@abc.abstractmethod
|
|
88
|
+
def cookies(self) -> Mapping[str, str]:
|
|
89
|
+
"""The request cookies."""
|
|
90
|
+
raise NotImplementedError
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class AsyncHTTPRequestAdapter(abc.ABC):
|
|
94
|
+
"""
|
|
95
|
+
Abstract Base Class defining the interface for accessing HTTP request data
|
|
96
|
+
in a framework-agnostic way.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
@abc.abstractmethod
|
|
101
|
+
def method(self) -> HTTPMethod:
|
|
102
|
+
"""The HTTP method of the request (e.g., 'GET', 'POST')."""
|
|
103
|
+
raise NotImplementedError
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
@abc.abstractmethod
|
|
107
|
+
def query_params(self) -> QueryParams:
|
|
108
|
+
"""The query parameters of the request."""
|
|
109
|
+
raise NotImplementedError
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
@abc.abstractmethod
|
|
113
|
+
def headers(self) -> Mapping[str, str]:
|
|
114
|
+
"""The request headers. Header names should ideally be case-insensitive."""
|
|
115
|
+
# Note: Real implementations might need to handle case-insensitivity
|
|
116
|
+
raise NotImplementedError
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
@abc.abstractmethod
|
|
120
|
+
def content_type(self) -> Optional[str]:
|
|
121
|
+
"""The 'Content-Type' header value, if present."""
|
|
122
|
+
raise NotImplementedError
|
|
123
|
+
|
|
124
|
+
@abc.abstractmethod
|
|
125
|
+
async def get_body(self) -> bytes:
|
|
126
|
+
"""Return the raw request body as bytes."""
|
|
127
|
+
raise NotImplementedError
|
|
128
|
+
|
|
129
|
+
@abc.abstractmethod
|
|
130
|
+
async def get_form_data(self) -> FormData:
|
|
131
|
+
"""
|
|
132
|
+
Return parsed form data (multipart/form-data or application/x-www-form-urlencoded).
|
|
133
|
+
Returns an empty FormData object if the content type is not form data
|
|
134
|
+
or if parsing fails.
|
|
135
|
+
"""
|
|
136
|
+
raise NotImplementedError
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
@abc.abstractmethod
|
|
140
|
+
def url(self) -> str:
|
|
141
|
+
"""The URL of the request."""
|
|
142
|
+
raise NotImplementedError
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
@abc.abstractmethod
|
|
146
|
+
def cookies(self) -> Mapping[str, str]:
|
|
147
|
+
"""The request cookies."""
|
|
148
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Mapping, Optional, Union, cast
|
|
4
|
+
|
|
5
|
+
from ._base import FormData, HTTPMethod, QueryParams, SyncHTTPRequestAdapter
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from chalice.app import Request
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ChaliceHTTPRequestAdapter(SyncHTTPRequestAdapter):
|
|
12
|
+
def __init__(self, request: Request) -> None:
|
|
13
|
+
self.request = request
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def query_params(self) -> QueryParams:
|
|
17
|
+
return self.request.query_params or {}
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def body(self) -> Union[str, bytes]:
|
|
21
|
+
return self.request.raw_body
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def method(self) -> HTTPMethod:
|
|
25
|
+
return cast("HTTPMethod", self.request.method.upper())
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def headers(self) -> Mapping[str, str]:
|
|
29
|
+
return self.request.headers
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def post_data(self) -> Mapping[str, Union[str, bytes]]:
|
|
33
|
+
# Chalice doesn't support traditional form data
|
|
34
|
+
raise NotImplementedError("Chalice does not support form data")
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def files(self) -> Mapping[str, Any]:
|
|
38
|
+
# Chalice doesn't support file uploads out of the box
|
|
39
|
+
raise NotImplementedError("Chalice does not support file uploads")
|
|
40
|
+
|
|
41
|
+
def get_form_data(self) -> FormData:
|
|
42
|
+
# Chalice doesn't support form data
|
|
43
|
+
raise NotImplementedError("Chalice does not support form data")
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def content_type(self) -> Optional[str]:
|
|
47
|
+
return self.request.headers.get("Content-Type", None)
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def url(self) -> str:
|
|
51
|
+
# Construct URL from context
|
|
52
|
+
context = self.request.context
|
|
53
|
+
stage = context.get("stage", "")
|
|
54
|
+
domain = context.get("domainName", "")
|
|
55
|
+
path = context.get("path", "")
|
|
56
|
+
|
|
57
|
+
# Build the URL
|
|
58
|
+
protocol = "https" # API Gateway typically uses HTTPS
|
|
59
|
+
if stage and stage != "prod":
|
|
60
|
+
url = f"{protocol}://{domain}/{stage}{path}"
|
|
61
|
+
else:
|
|
62
|
+
url = f"{protocol}://{domain}{path}"
|
|
63
|
+
|
|
64
|
+
# Add query string if present
|
|
65
|
+
if self.request.query_params:
|
|
66
|
+
from urllib.parse import urlencode
|
|
67
|
+
|
|
68
|
+
query_string = urlencode(self.request.query_params)
|
|
69
|
+
url = f"{url}?{query_string}"
|
|
70
|
+
|
|
71
|
+
return url
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def cookies(self) -> Mapping[str, str]:
|
|
75
|
+
# Chalice doesn't have direct cookie support
|
|
76
|
+
# Cookies would come in the Cookie header
|
|
77
|
+
cookie_header = self.request.headers.get("Cookie", "")
|
|
78
|
+
if not cookie_header:
|
|
79
|
+
return {}
|
|
80
|
+
|
|
81
|
+
cookies = {}
|
|
82
|
+
for cookie in cookie_header.split(";"):
|
|
83
|
+
cookie = cookie.strip()
|
|
84
|
+
if "=" in cookie:
|
|
85
|
+
name, value = cookie.split("=", 1)
|
|
86
|
+
cookies[name] = value
|
|
87
|
+
|
|
88
|
+
return cookies
|
cross/request/_django.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Mapping, Optional, Union, cast
|
|
4
|
+
|
|
5
|
+
from ._base import (
|
|
6
|
+
AsyncHTTPRequestAdapter,
|
|
7
|
+
FormData,
|
|
8
|
+
HTTPMethod,
|
|
9
|
+
QueryParams,
|
|
10
|
+
SyncHTTPRequestAdapter,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from django.http import HttpRequest
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DjangoHTTPRequestAdapter(SyncHTTPRequestAdapter):
|
|
18
|
+
def __init__(self, request: HttpRequest) -> None:
|
|
19
|
+
self.request = request
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def query_params(self) -> QueryParams:
|
|
23
|
+
return cast(QueryParams, self.request.GET.dict())
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def body(self) -> Union[str, bytes]:
|
|
27
|
+
return cast(Union[str, bytes], self.request.body.decode())
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def method(self) -> HTTPMethod:
|
|
31
|
+
assert self.request.method is not None
|
|
32
|
+
|
|
33
|
+
return cast("HTTPMethod", self.request.method.upper())
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def headers(self) -> Mapping[str, str]:
|
|
37
|
+
return cast(Mapping[str, str], self.request.headers)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def post_data(self) -> Mapping[str, Union[str, bytes]]:
|
|
41
|
+
return cast(Mapping[str, Union[str, bytes]], self.request.POST)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def files(self) -> Mapping[str, Any]:
|
|
45
|
+
return cast(Mapping[str, Any], self.request.FILES)
|
|
46
|
+
|
|
47
|
+
def get_form_data(self) -> FormData:
|
|
48
|
+
return FormData(
|
|
49
|
+
files=cast(Mapping[str, Any], self.request.FILES),
|
|
50
|
+
form=cast(Mapping[str, Union[str, bytes]], self.request.POST),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def content_type(self) -> Optional[str]:
|
|
55
|
+
return cast(Optional[str], self.request.content_type)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def url(self) -> str:
|
|
59
|
+
return cast(str, self.request.build_absolute_uri())
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def cookies(self) -> Mapping[str, str]:
|
|
63
|
+
return cast(Mapping[str, str], self.request.COOKIES)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class AsyncDjangoHTTPRequestAdapter(AsyncHTTPRequestAdapter):
|
|
67
|
+
def __init__(self, request: HttpRequest) -> None:
|
|
68
|
+
self.request = request
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def query_params(self) -> QueryParams:
|
|
72
|
+
return cast(QueryParams, self.request.GET.dict())
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def method(self) -> HTTPMethod:
|
|
76
|
+
assert self.request.method is not None
|
|
77
|
+
|
|
78
|
+
return cast("HTTPMethod", self.request.method.upper())
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def headers(self) -> Mapping[str, str]:
|
|
82
|
+
return cast(Mapping[str, str], self.request.headers)
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def content_type(self) -> Optional[str]:
|
|
86
|
+
return self.headers.get("Content-type")
|
|
87
|
+
|
|
88
|
+
async def get_body(self) -> bytes:
|
|
89
|
+
return cast(bytes, self.request.body)
|
|
90
|
+
|
|
91
|
+
async def get_form_data(self) -> FormData:
|
|
92
|
+
return FormData(
|
|
93
|
+
files=cast(Mapping[str, Any], self.request.FILES),
|
|
94
|
+
form=cast(Mapping[str, Union[str, bytes]], self.request.POST),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def url(self) -> str:
|
|
99
|
+
return cast(str, self.request.build_absolute_uri())
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def cookies(self) -> Mapping[str, str]:
|
|
103
|
+
return cast(Mapping[str, str], self.request.COOKIES)
|
cross/request/_flask.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Mapping, Optional, Union, cast
|
|
4
|
+
|
|
5
|
+
from ._base import (
|
|
6
|
+
AsyncHTTPRequestAdapter,
|
|
7
|
+
FormData,
|
|
8
|
+
HTTPMethod,
|
|
9
|
+
QueryParams,
|
|
10
|
+
SyncHTTPRequestAdapter,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from flask import Request
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FlaskHTTPRequestAdapter(SyncHTTPRequestAdapter):
|
|
18
|
+
def __init__(self, request: Request) -> None:
|
|
19
|
+
self.request = request
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def query_params(self) -> QueryParams:
|
|
23
|
+
return self.request.args.to_dict()
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def body(self) -> Union[str, bytes]:
|
|
27
|
+
return self.request.data.decode()
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def method(self) -> HTTPMethod:
|
|
31
|
+
return cast("HTTPMethod", self.request.method.upper())
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def headers(self) -> Mapping[str, str]:
|
|
35
|
+
return self.request.headers # type: ignore
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def post_data(self) -> Mapping[str, Union[str, bytes]]:
|
|
39
|
+
return self.request.form
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def files(self) -> Mapping[str, Any]:
|
|
43
|
+
return self.request.files
|
|
44
|
+
|
|
45
|
+
def get_form_data(self) -> FormData:
|
|
46
|
+
return FormData(
|
|
47
|
+
files=self.request.files,
|
|
48
|
+
form=self.request.form,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def content_type(self) -> Optional[str]:
|
|
53
|
+
return self.request.content_type
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def url(self) -> str:
|
|
57
|
+
return self.request.url
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def cookies(self) -> Mapping[str, str]:
|
|
61
|
+
return self.request.cookies
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AsyncFlaskHTTPRequestAdapter(AsyncHTTPRequestAdapter):
|
|
65
|
+
def __init__(self, request: Request) -> None:
|
|
66
|
+
self.request = request
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def query_params(self) -> QueryParams:
|
|
70
|
+
return self.request.args.to_dict()
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def method(self) -> HTTPMethod:
|
|
74
|
+
return cast("HTTPMethod", self.request.method.upper())
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def content_type(self) -> Optional[str]:
|
|
78
|
+
return self.request.content_type
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def headers(self) -> Mapping[str, str]:
|
|
82
|
+
return self.request.headers # type: ignore
|
|
83
|
+
|
|
84
|
+
async def get_body(self) -> bytes:
|
|
85
|
+
return self.request.data
|
|
86
|
+
|
|
87
|
+
async def get_form_data(self) -> FormData:
|
|
88
|
+
return FormData(
|
|
89
|
+
files=self.request.files,
|
|
90
|
+
form=self.request.form,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def url(self) -> str:
|
|
95
|
+
return self.request.url
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def cookies(self) -> Mapping[str, str]:
|
|
99
|
+
return self.request.cookies
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Mapping, Optional, cast
|
|
4
|
+
|
|
5
|
+
from ._base import AsyncHTTPRequestAdapter, FormData, HTTPMethod, QueryParams
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from litestar import Request
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LitestarRequestAdapter(AsyncHTTPRequestAdapter):
|
|
12
|
+
def __init__(self, request: Request[Any, Any, Any]) -> None:
|
|
13
|
+
self.request = request
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def query_params(self) -> QueryParams:
|
|
17
|
+
return self.request.query_params
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def method(self) -> HTTPMethod:
|
|
21
|
+
return cast("HTTPMethod", self.request.method.upper())
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def headers(self) -> Mapping[str, str]:
|
|
25
|
+
return self.request.headers
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def content_type(self) -> Optional[str]:
|
|
29
|
+
content_type, params = self.request.content_type
|
|
30
|
+
|
|
31
|
+
# combine content type and params
|
|
32
|
+
if params:
|
|
33
|
+
content_type += "; " + "; ".join(f"{k}={v}" for k, v in params.items())
|
|
34
|
+
|
|
35
|
+
return content_type
|
|
36
|
+
|
|
37
|
+
async def get_body(self) -> bytes:
|
|
38
|
+
return await self.request.body()
|
|
39
|
+
|
|
40
|
+
async def get_form_data(self) -> FormData:
|
|
41
|
+
multipart_data = await self.request.form()
|
|
42
|
+
|
|
43
|
+
return FormData(form=multipart_data, files=multipart_data)
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def url(self) -> str:
|
|
47
|
+
return str(self.request.url)
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def cookies(self) -> Mapping[str, str]:
|
|
51
|
+
return self.request.cookies
|
cross/request/_quart.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Mapping, Optional, cast
|
|
4
|
+
|
|
5
|
+
from ._base import AsyncHTTPRequestAdapter, FormData, HTTPMethod, QueryParams
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from quart import Request
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class QuartHTTPRequestAdapter(AsyncHTTPRequestAdapter):
|
|
12
|
+
def __init__(self, request: Request) -> None:
|
|
13
|
+
self.request = request
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def query_params(self) -> QueryParams:
|
|
17
|
+
return self.request.args.to_dict()
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def method(self) -> HTTPMethod:
|
|
21
|
+
return cast("HTTPMethod", self.request.method.upper())
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def content_type(self) -> Optional[str]:
|
|
25
|
+
return self.request.content_type
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def headers(self) -> Mapping[str, str]:
|
|
29
|
+
return self.request.headers # type: ignore
|
|
30
|
+
|
|
31
|
+
async def get_body(self) -> bytes:
|
|
32
|
+
return await self.request.data
|
|
33
|
+
|
|
34
|
+
async def get_form_data(self) -> FormData:
|
|
35
|
+
files = await self.request.files
|
|
36
|
+
form = await self.request.form
|
|
37
|
+
return FormData(files=files, form=form)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def url(self) -> str:
|
|
41
|
+
return self.request.url
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def cookies(self) -> Mapping[str, str]:
|
|
45
|
+
return self.request.cookies
|
cross/request/_sanic.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Mapping, Optional, Union, cast
|
|
4
|
+
|
|
5
|
+
from ._base import AsyncHTTPRequestAdapter, FormData, HTTPMethod, QueryParams
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from sanic.request import File, Request
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def convert_request_to_files_dict(request: Request) -> dict[str, Any]:
|
|
12
|
+
"""Converts the request.files dictionary to a dictionary of sanic Request objects.
|
|
13
|
+
|
|
14
|
+
`request.files` has the following format, even if only a single file is uploaded:
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
{
|
|
18
|
+
"textFile": [
|
|
19
|
+
sanic.request.File(type="text/plain", body=b"strawberry", name="textFile.txt")
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Note that the dictionary entries are lists.
|
|
25
|
+
"""
|
|
26
|
+
request_files = cast("Optional[dict[str, list[File]]]", request.files)
|
|
27
|
+
|
|
28
|
+
if not request_files:
|
|
29
|
+
return {}
|
|
30
|
+
|
|
31
|
+
files_dict: dict[str, Union[File, list[File]]] = {}
|
|
32
|
+
|
|
33
|
+
for field_name, file_list in request_files.items():
|
|
34
|
+
assert len(file_list) == 1
|
|
35
|
+
|
|
36
|
+
files_dict[field_name] = file_list[0]
|
|
37
|
+
|
|
38
|
+
return files_dict
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SanicHTTPRequestAdapter(AsyncHTTPRequestAdapter):
|
|
42
|
+
def __init__(self, request: Request) -> None:
|
|
43
|
+
self.request = request
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def query_params(self) -> QueryParams:
|
|
47
|
+
# Just a heads up, Sanic's request.args uses urllib.parse.parse_qs
|
|
48
|
+
# to parse query string parameters. This returns a dictionary where
|
|
49
|
+
# the keys are the unique variable names and the values are lists
|
|
50
|
+
# of values for each variable name. To ensure consistency, we're
|
|
51
|
+
# enforcing the use of the first value in each list.
|
|
52
|
+
args = self.request.get_args(keep_blank_values=True)
|
|
53
|
+
return {k: args.get(k, None) for k in args}
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def method(self) -> HTTPMethod:
|
|
57
|
+
return cast("HTTPMethod", self.request.method.upper())
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def headers(self) -> Mapping[str, str]:
|
|
61
|
+
return self.request.headers
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def content_type(self) -> Optional[str]:
|
|
65
|
+
return self.request.content_type
|
|
66
|
+
|
|
67
|
+
async def get_body(self) -> bytes:
|
|
68
|
+
return self.request.body
|
|
69
|
+
|
|
70
|
+
async def get_form_data(self) -> FormData:
|
|
71
|
+
assert self.request.form is not None
|
|
72
|
+
|
|
73
|
+
files = convert_request_to_files_dict(self.request)
|
|
74
|
+
|
|
75
|
+
return FormData(form=self.request.form, files=files)
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def url(self) -> str:
|
|
79
|
+
return self.request.url
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def cookies(self) -> Mapping[str, str]:
|
|
83
|
+
return self.request.cookies
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Mapping, Optional, cast
|
|
4
|
+
|
|
5
|
+
from ._base import AsyncHTTPRequestAdapter, FormData, HTTPMethod, QueryParams
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from starlette.requests import Request
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class StarletteRequestAdapter(AsyncHTTPRequestAdapter):
|
|
12
|
+
def __init__(self, request: Request) -> None:
|
|
13
|
+
self._request = request
|
|
14
|
+
# Starlette Headers are case-insensitive Mapping
|
|
15
|
+
self._headers: Optional[Mapping[str, str]] = None
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def method(self) -> HTTPMethod:
|
|
19
|
+
# Starlette method is already uppercase string
|
|
20
|
+
return cast(HTTPMethod, self._request.method)
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def query_params(self) -> QueryParams:
|
|
24
|
+
# Starlette QueryParams behaves like a MultiDict Mapping
|
|
25
|
+
return cast(QueryParams, self._request.query_params)
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def headers(self) -> Mapping[str, str]:
|
|
29
|
+
# Cache the immutable headers object for direct access
|
|
30
|
+
# Starlette Headers are case-insensitive
|
|
31
|
+
if self._headers is None:
|
|
32
|
+
self._headers = self._request.headers
|
|
33
|
+
|
|
34
|
+
return self._headers
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def content_type(self) -> Optional[str]:
|
|
38
|
+
# Access directly via headers property for consistency
|
|
39
|
+
return self.headers.get("content-type")
|
|
40
|
+
|
|
41
|
+
async def get_body(self) -> bytes:
|
|
42
|
+
return await self._request.body()
|
|
43
|
+
|
|
44
|
+
async def get_form_data(self) -> FormData:
|
|
45
|
+
multipart_data = await self._request.form()
|
|
46
|
+
|
|
47
|
+
return FormData(
|
|
48
|
+
files=multipart_data,
|
|
49
|
+
form=multipart_data,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def url(self) -> str:
|
|
54
|
+
return str(self._request.url)
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def cookies(self) -> Mapping[str, str]:
|
|
58
|
+
return cast(Mapping[str, str], self._request.cookies)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, Mapping, Optional
|
|
5
|
+
|
|
6
|
+
from ._base import AsyncHTTPRequestAdapter, FormData, HTTPMethod, QueryParams
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestingRequestAdapter(AsyncHTTPRequestAdapter):
|
|
10
|
+
__test__ = False
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
*,
|
|
15
|
+
method: HTTPMethod = "POST",
|
|
16
|
+
query_params: QueryParams | None = None,
|
|
17
|
+
headers: Mapping[str, str] | None = None,
|
|
18
|
+
content_type: str | None = None,
|
|
19
|
+
url: str = "http://testserver/",
|
|
20
|
+
cookies: Mapping[str, str] | None = None,
|
|
21
|
+
form_data: FormData | None = None,
|
|
22
|
+
json: dict[str, Any] | None = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
self._method = method
|
|
25
|
+
self._query_params = query_params or {}
|
|
26
|
+
self._headers = headers or {}
|
|
27
|
+
self._content_type = content_type
|
|
28
|
+
self._url = url
|
|
29
|
+
self._cookies = cookies or {}
|
|
30
|
+
self._form_data = form_data or FormData(files={}, form={})
|
|
31
|
+
self._json = json
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def method(self) -> HTTPMethod:
|
|
35
|
+
return self._method
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def query_params(self) -> QueryParams:
|
|
39
|
+
return self._query_params
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def headers(self) -> Mapping[str, str]:
|
|
43
|
+
return self._headers
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def content_type(self) -> Optional[str]:
|
|
47
|
+
return self._content_type
|
|
48
|
+
|
|
49
|
+
async def get_body(self) -> bytes:
|
|
50
|
+
if self._json is not None:
|
|
51
|
+
return json.dumps(self._json).encode("utf-8")
|
|
52
|
+
|
|
53
|
+
return b""
|
|
54
|
+
|
|
55
|
+
async def get_form_data(self) -> FormData:
|
|
56
|
+
return self._form_data
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def url(self) -> str:
|
|
60
|
+
return self._url
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def cookies(self) -> Mapping[str, str]:
|
|
64
|
+
return self._cookies
|
cross/response.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import TYPE_CHECKING, Literal, Mapping, List, Union, cast
|
|
6
|
+
from typing_extensions import Self
|
|
7
|
+
from urllib.parse import urlencode
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from fastapi import Response as FastAPIResponse
|
|
11
|
+
|
|
12
|
+
JsonType = Union[
|
|
13
|
+
str, int, float, bool, None, Mapping[str, "JsonType"], List["JsonType"]
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Cookie:
|
|
19
|
+
name: str
|
|
20
|
+
value: str
|
|
21
|
+
secure: bool
|
|
22
|
+
path: str | None = None
|
|
23
|
+
domain: str | None = None
|
|
24
|
+
max_age: int | None = None
|
|
25
|
+
httponly: bool = True
|
|
26
|
+
samesite: Literal["lax", "strict", "none"] = "lax"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class Response:
|
|
31
|
+
status_code: int
|
|
32
|
+
body: str | None = None
|
|
33
|
+
cookies: list[Cookie] | None = None
|
|
34
|
+
headers: Mapping[str, str] | None = None
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def redirect(
|
|
38
|
+
cls,
|
|
39
|
+
url: str,
|
|
40
|
+
query_params: Mapping[str, str | list[str]] | None = None,
|
|
41
|
+
headers: Mapping[str, str] | None = None,
|
|
42
|
+
cookies: list[Cookie] | None = None,
|
|
43
|
+
) -> Self:
|
|
44
|
+
headers = headers or {}
|
|
45
|
+
|
|
46
|
+
if query_params:
|
|
47
|
+
url = url + "?" + urlencode(query_params)
|
|
48
|
+
|
|
49
|
+
return cls(
|
|
50
|
+
status_code=302,
|
|
51
|
+
headers={"Location": url, **headers},
|
|
52
|
+
cookies=cookies,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def json(self) -> JsonType:
|
|
56
|
+
if self.body is None:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
return cast(JsonType, json.loads(self.body))
|
|
60
|
+
|
|
61
|
+
def to_fastapi(self) -> FastAPIResponse:
|
|
62
|
+
from fastapi import Response as FastAPIResponse
|
|
63
|
+
|
|
64
|
+
response = FastAPIResponse(
|
|
65
|
+
status_code=self.status_code,
|
|
66
|
+
headers=self.headers,
|
|
67
|
+
content=self.body,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
for cookie in self.cookies or []:
|
|
71
|
+
response.set_cookie(
|
|
72
|
+
cookie.name,
|
|
73
|
+
cookie.value,
|
|
74
|
+
secure=cookie.secure,
|
|
75
|
+
path=cookie.path,
|
|
76
|
+
domain=cookie.domain,
|
|
77
|
+
max_age=cookie.max_age,
|
|
78
|
+
httponly=cookie.httponly,
|
|
79
|
+
samesite=cookie.samesite,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return response
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cross-web
|
|
3
|
+
Version: 0.2.3
|
|
4
|
+
Summary: A library for working with web frameworks
|
|
5
|
+
Author-email: Patrick Arminio <patrick.arminio@gmail.com>
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
11
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Requires-Dist: typing-extensions>=4.14.0
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# Cross
|
|
17
|
+
|
|
18
|
+
**Write once, run everywhere** - A universal web framework adapter for Python that lets you write code once and use it across multiple web frameworks.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
uv add cross-web
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Overview
|
|
27
|
+
|
|
28
|
+
Cross provides a unified interface for common web framework operations, allowing you to write framework-agnostic code that can be easily adapted to work with FastAPI, Flask, Django, and other popular Python web frameworks.
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
This project is in early development!
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
cross/__init__.py,sha256=Lqg9wnEAyU1Kk67dF7dlU_ZmyQvMJt5PisTFCeXBG2Y,1336
|
|
2
|
+
cross/exceptions.py,sha256=uVWXNel6yjRHIEazdtthgFBKCF36mskOU3kmhJR1eXU,226
|
|
3
|
+
cross/protocols.py,sha256=NEItrqqj1LKKUjBOzpNTn3vLCDUWsV9e_yaDlN9rq1I,478
|
|
4
|
+
cross/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
cross/response.py,sha256=8kiJjv6OoL2lKKZaay5MBx8BxDxW6poyA1rA6Gca_48,2096
|
|
6
|
+
cross/request/__init__.py,sha256=pgyFbIFYAP3RpZkS947AvUJ8d-2ev4WWhj_9r2E6_d8,3965
|
|
7
|
+
cross/request/_aiohttp.py,sha256=sPRuQnInX64LviaLAKXEsyNQ-sYLVGdPliLIWi4J3sc,3573
|
|
8
|
+
cross/request/_base.py,sha256=kllXikX7S6ZFQWcNs2KYhAbKibEbTsAEtRlp0smoXhg,4330
|
|
9
|
+
cross/request/_chalice.py,sha256=SN62ryIFYOdHO6nJx6lE_4MuluAq_1L5PnQsi-Uwb1c,2730
|
|
10
|
+
cross/request/_django.py,sha256=CMlRlC7m4n_pfclUMuo7r6-TPLki9soF-tu41ac-o6w,2961
|
|
11
|
+
cross/request/_flask.py,sha256=c80Xovg31XSsKO5o9og_RgM4kli1I1NXVe7P8MGYy1k,2433
|
|
12
|
+
cross/request/_litestar.py,sha256=mSPMaqAuR2J-vBDgeB6QN5pFZ1jfuDrWOZnFbkH90FQ,1404
|
|
13
|
+
cross/request/_quart.py,sha256=9ICIpoALCk7Op5ggyIuU7CdHqlLaaowanH5ZNnSdDkQ,1211
|
|
14
|
+
cross/request/_sanic.py,sha256=QfVpXwdj0RF20xkZysq1LuLV6V0r_j0I10ukEtVsiRc,2485
|
|
15
|
+
cross/request/_starlette.py,sha256=bxL4TYkWh-OAjZSht_benXr40TNLDtVj7UaqGBfaXf0,1775
|
|
16
|
+
cross/request/_testing.py,sha256=ja10a1pQSYBzYAN10ce9e5aEpyD9-HBLTWH76hqWK5g,1712
|
|
17
|
+
cross_web-0.2.3.dist-info/METADATA,sha256=uOEg3q9a78xO_XLG4f8ni32dRUB0-LN4JllC3vXSczA,1064
|
|
18
|
+
cross_web-0.2.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
19
|
+
cross_web-0.2.3.dist-info/licenses/LICENSE,sha256=a-9NtzEY0MYsJLrhMKR7HQsqmfTjYx_A1Mw6dwXer18,1073
|
|
20
|
+
cross_web-0.2.3.dist-info/RECORD,,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Patrick Arminio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|