Skip to content

Commit 330757c

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

5 files changed

Lines changed: 119 additions & 0 deletions

File tree

apimatic_core/adapters/__init__.py

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