Skip to content

Commit 2cd19a3

Browse files
committed
revision: adds structural typing and adapter for requests
1 parent e803faf commit 2cd19a3

5 files changed

Lines changed: 127 additions & 0 deletions

File tree

apimatic_core/adapters/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
__all__ = [
2+
'request_adapter',
3+
'flask_like',
4+
'django_like',
5+
'starlette_like'
6+
]
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from typing import Any, Mapping
2+
from typing_extensions import Protocol, runtime_checkable
3+
4+
@runtime_checkable
5+
class DjangoRequestLike(Protocol):
6+
method: str
7+
headers: Mapping[str, str]
8+
COOKIES: Mapping[str, str]
9+
GET: Mapping[str, Any]
10+
POST: Mapping[str, Any]
11+
path: str
12+
body: bytes
13+
def build_absolute_uri(self) -> str: ...
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from typing import Any, Mapping
2+
from typing_extensions import Protocol, runtime_checkable
3+
4+
@runtime_checkable
5+
class FlaskRequestLike(Protocol):
6+
method: str
7+
headers: Mapping[str, str]
8+
cookies: Mapping[str, str]
9+
args: Mapping[str, Any]
10+
url: str
11+
path: str
12+
def get_data(self, cache: bool = ...) -> bytes: ...
13+
form: Mapping[str, Any]
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# adapter.py
2+
from typing import Dict, List, Optional, Union
3+
from http.cookies import SimpleCookie
4+
from apimatic_core_interfaces.http.request import Request, _as_listdict
5+
6+
from apimatic_core.adapters.django_like import DjangoRequestLike
7+
from apimatic_core.adapters.flask_like import FlaskRequestLike
8+
from apimatic_core.adapters.starlette_like import StarletteRequestLike
9+
10+
11+
async def to_unified_request(req: Union[StarletteRequestLike, FlaskRequestLike, DjangoRequestLike]) -> Request:
12+
# --- Starlette/FastAPI ---
13+
if isinstance(req, StarletteRequestLike):
14+
headers = dict(req.headers)
15+
raw = await req.body()
16+
query = _as_listdict(req.query_params)
17+
cookies = dict(req.cookies)
18+
url_str = str(req.url)
19+
path = req.url.path
20+
ct = (headers.get("content-type") or headers.get("Content-Type") or "").lower()
21+
form: Dict[str, List[str]] = {}
22+
if ct.startswith(("multipart/form-data", "application/x-www-form-urlencoded")):
23+
formdata = await req.form()
24+
for k in formdata.keys():
25+
for v in formdata.getlist(k):
26+
if not (hasattr(v, "filename") and hasattr(v, "read")):
27+
form.setdefault(k, []).append(str(v))
28+
return Request(method=req.method, path=path, url=url_str, headers=headers,
29+
raw_body=raw, query=query, cookies=cookies, form=form)
30+
31+
# --- Flask ---
32+
if isinstance(req, FlaskRequestLike):
33+
headers = dict(req.headers)
34+
url_str: Optional[str] = getattr(req, "url", None)
35+
path: str = req.path
36+
raw: bytes = req.get_data(cache=True)
37+
query = _as_listdict(req.args)
38+
cookies = dict(req.cookies)
39+
# best-effort cookie header fallback if the jar is empty
40+
if not cookies:
41+
cookie_header = headers.get("Cookie") or headers.get("cookie")
42+
if cookie_header:
43+
jar = SimpleCookie(); jar.load(cookie_header)
44+
cookies = {k: morsel.value for k, morsel in jar.items()}
45+
form = _as_listdict(req.form)
46+
return Request(method=req.method, path=path, url=url_str, headers=headers,
47+
raw_body=raw, query=query, cookies=cookies, form=form)
48+
49+
# --- Django ---
50+
if isinstance(req, DjangoRequestLike):
51+
headers = dict(getattr(req, "headers", {}) or {})
52+
# fallback for very old Django: META → headers
53+
if not headers:
54+
meta = getattr(req, "META", {}) or {}
55+
headers = {k[5:].replace("_", "-"): str(v) for k, v in meta.items() if k.startswith("HTTP_")}
56+
url_str = req.build_absolute_uri()
57+
path = req.path
58+
raw = bytes(getattr(req, "body", b"") or b"")
59+
query = _as_listdict(getattr(req, "GET", {}))
60+
cookies = dict(getattr(req, "COOKIES", {}) or {})
61+
form = _as_listdict(getattr(req, "POST", {}))
62+
return Request(method=req.method, path=path, url=url_str, headers=headers,
63+
raw_body=raw, query=query, cookies=cookies, form=form)
64+
65+
raise TypeError(f"Unsupported request type: {type(req)!r}")
66+
67+
# Optional convenience wrappers (sync for Flask/Django hosts)
68+
def to_unified_request_sync(req: Union[FlaskRequestLike, DjangoRequestLike]) -> Request:
69+
import asyncio
70+
try:
71+
loop = asyncio.get_event_loop()
72+
except RuntimeError:
73+
loop = asyncio.new_event_loop(); asyncio.set_event_loop(loop)
74+
return loop.run_until_complete(to_unified_request(req))
75+
76+
async def from_starlette(req: StarletteRequestLike) -> Request:
77+
return await to_unified_request(req)
78+
79+
def from_flask(req: FlaskRequestLike) -> Request:
80+
return to_unified_request_sync(req)
81+
82+
def from_django(req: DjangoRequestLike) -> Request:
83+
return to_unified_request_sync(req)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from typing import Any, Mapping, Coroutine
2+
from typing_extensions import Protocol, runtime_checkable
3+
4+
@runtime_checkable
5+
class StarletteRequestLike(Protocol):
6+
method: str
7+
headers: Mapping[str, str]
8+
cookies: Mapping[str, str]
9+
query_params: Mapping[str, Any]
10+
url: Any # __str__ -> URL; has .path: str
11+
def body(self) -> Coroutine[Any, Any, bytes]: ...
12+
def form(self) -> Coroutine[Any, Any, Any]: ...

0 commit comments

Comments
 (0)