Skip to content

Commit f38df42

Browse files
committed
fix sonarcloud complain in the adapter script
1 parent cbe3786 commit f38df42

2 files changed

Lines changed: 230 additions & 182 deletions

File tree

Lines changed: 153 additions & 178 deletions
Original file line numberDiff line numberDiff line change
@@ -1,213 +1,188 @@
1+
# apimatic_core/adapters/request_adapter.py
2+
3+
from __future__ import annotations
4+
15
import asyncio
2-
from typing import Dict, List, Optional, Union, Any
6+
from typing import Any, Dict, List, Mapping, Optional, Union
7+
38
from http.cookies import SimpleCookie
4-
from apimatic_core_interfaces.http.request import Request
59

10+
from apimatic_core_interfaces.http.request import Request
611
from apimatic_core.adapters.types.django_request_like import DjangoRequestLike
712
from apimatic_core.adapters.types.flask_request_like import FlaskRequestLike
813
from apimatic_core.adapters.types.starlette_request_like import StarletteRequestLike
914

1015

11-
def _as_listdict(obj: Any) -> Dict[str, List[str]]:
12-
"""
13-
Normalize a query/form-like mapping to a plain `Dict[str, List[str]]`.
14-
15-
Supports:
16-
- Objects exposing a `getlist(key)` API (e.g., Django `QueryDict`,
17-
Werkzeug `MultiDict`) → copies each list.
18-
- Plain mappings (`Mapping[str, str]`) → wraps scalar values in single-item lists.
19-
20-
Args:
21-
obj: A mapping or MultiDict/QueryDict-like object. Falsy values return `{}`.
16+
# -----------------------
17+
# Shared utilities
18+
# -----------------------
2219

23-
Returns:
24-
A new dict where each key maps to a list of strings.
25-
"""
20+
def _as_listdict(obj: Any) -> Dict[str, List[str]]:
2621
if not obj:
2722
return {}
2823
getlist = getattr(obj, "getlist", None)
2924
if callable(getlist):
30-
return {k: list(getlist(k)) for k in obj.keys()}
31-
return {k: [obj[k]] for k in obj.keys()}
25+
return {str(k): list(getlist(k)) for k in obj.keys()}
26+
return {str(k): [str(v)] for k, v in dict(obj).items()}
3227

3328

34-
async def to_unified_request(
35-
req: Union[StarletteRequestLike, FlaskRequestLike, DjangoRequestLike]
36-
) -> Request:
37-
"""
38-
Convert a framework request (Starlette/FastAPI, Flask/Werkzeug, or Django) to a unified snapshot.
29+
def _content_type(headers: Mapping[str, str]) -> str:
30+
"""Return lower-cased Content-Type value or empty string."""
31+
return (headers.get("content-type") or headers.get("Content-Type") or "").lower()
3932

40-
This function uses structural typing (Protocols) to detect the request "shape"
41-
at runtime and extracts a compact, immutable snapshot that excludes file uploads.
42-
43-
Extraction rules:
44-
• Method/path/url: copied verbatim (with Starlette `url` stringified).
45-
• Headers: copied into a plain `dict` (case preserved as-provided by the framework).
46-
• Raw body: captured as bytes (Starlette: `await req.body()`; Flask: `get_data(cache=True)`;
47-
Django: `req.body`).
48-
• Query/form: normalized to `Dict[str, List[str]]` via `_as_listdict(...)`.
49-
For Starlette, form parsing only occurs for `multipart/form-data` or
50-
`application/x-www-form-urlencoded` content types, and file parts are ignored.
51-
• Cookies: copied to a `dict`. Flask additionally falls back to parsing the
52-
`Cookie` header if `req.cookies` is empty.
53-
• Django headers: if `req.headers` is missing/empty (very old Django),
54-
a best-effort fallback uses `req.META["HTTP_*"]`.
55-
56-
Args:
57-
req: A request object structurally compatible with one of the supported
58-
frameworks' request shapes.
59-
60-
Returns:
61-
Request: The framework-agnostic snapshot.
62-
63-
Raises:
64-
TypeError: If `req` does not match any supported Protocol.
65-
"""
66-
# --- Starlette/FastAPI ---
67-
if isinstance(req, StarletteRequestLike):
68-
headers = dict(req.headers)
69-
raw = await req.body()
70-
query = _as_listdict(req.query_params)
71-
cookies = dict(req.cookies)
72-
url_str = str(req.url)
73-
path = req.url.path
74-
ct = (headers.get("content-type") or headers.get("Content-Type") or "").lower()
75-
form: Dict[str, List[str]] = {}
76-
if ct.startswith(("multipart/form-data", "application/x-www-form-urlencoded")):
77-
form_data = await req.form()
78-
for k in form_data.keys():
79-
for v in form_data.getlist(k):
80-
# Ignore file-like values (UploadFile or similar)
81-
if not (hasattr(v, "filename") and hasattr(v, "read")):
82-
form.setdefault(k, []).append(str(v))
83-
return Request(
84-
method=req.method,
85-
path=path,
86-
url=url_str,
87-
headers=headers,
88-
raw_body=raw,
89-
query=query,
90-
cookies=cookies,
91-
form=form,
92-
)
93-
94-
# --- Flask ---
95-
if isinstance(req, FlaskRequestLike):
96-
headers = dict(req.headers)
97-
url_str: Optional[str] = getattr(req, "url", None)
98-
path: str = req.path
99-
raw: bytes = req.get_data(cache=True)
100-
query = _as_listdict(req.args)
101-
cookies = dict(req.cookies)
102-
# Best-effort cookie header fallback if the jar is empty
103-
if not cookies:
104-
cookie_header = headers.get("Cookie") or headers.get("cookie")
105-
if cookie_header:
106-
jar = SimpleCookie()
107-
jar.load(cookie_header)
108-
cookies = {k: morsel.value for k, morsel in jar.items()}
109-
form = _as_listdict(req.form)
110-
return Request(
111-
method=req.method,
112-
path=path,
113-
url=url_str,
114-
headers=headers,
115-
raw_body=raw,
116-
query=query,
117-
cookies=cookies,
118-
form=form,
119-
)
120-
121-
# --- Django ---
122-
if isinstance(req, DjangoRequestLike):
123-
headers = dict(getattr(req, "headers", {}) or {})
124-
# Fallback for very old Django: META → headers
125-
if not headers:
126-
meta = getattr(req, "META", {}) or {}
127-
headers = {
128-
k[5:].replace("_", "-"): str(v)
129-
for k, v in meta.items()
130-
if k.startswith("HTTP_")
131-
}
132-
url_str = req.build_absolute_uri()
133-
path = req.path
134-
raw = bytes(getattr(req, "body", b"") or b"")
135-
query = _as_listdict(getattr(req, "GET", {}))
136-
cookies = dict(getattr(req, "COOKIES", {}) or {})
137-
form = _as_listdict(getattr(req, "POST", {}))
138-
return Request(
139-
method=req.method,
140-
path=path,
141-
url=url_str,
142-
headers=headers,
143-
raw_body=raw,
144-
query=query,
145-
cookies=cookies,
146-
form=form,
147-
)
14833

149-
raise TypeError(f"Unsupported request type: {type(req)!r}")
34+
def _is_urlencoded_or_multipart(headers: Mapping[str, str]) -> bool:
35+
"""Check if body is form-like (urlencoded/multipart)."""
36+
ct = _content_type(headers)
37+
return ct.startswith(("multipart/form-data", "application/x-www-form-urlencoded"))
15038

15139

152-
def _unwrap_local_proxy(obj: Any) -> Any:
153-
"""
154-
Best-effort, dependency-free unwrapping for Flask's `LocalProxy`.
40+
def _cookies_from_header(headers: Mapping[str, str]) -> Dict[str, str]:
41+
"""Parse Cookie header into a dict, returns {} if absent/empty."""
42+
cookie_header = headers.get("Cookie") or headers.get("cookie")
43+
if not cookie_header:
44+
return {}
45+
jar = SimpleCookie()
46+
jar.load(cookie_header)
47+
return {k: morsel.value for k, morsel in jar.items()}
48+
15549

156-
We intentionally do not import `werkzeug.local.LocalProxy` to keep this adapter
157-
framework-agnostic. Instead, we use duck typing: if the object exposes
158-
`_get_current_object()` (the LocalProxy API), we call it to retrieve the real
159-
underlying request. If the object is not a LocalProxy, or unwrapping fails,
160-
the original object is returned.
50+
def _django_headers_fallback(req: DjangoRequestLike) -> Dict[str, str]:
51+
"""
52+
Fallback for very old Django where `request.headers` is missing/empty.
53+
Builds headers from META['HTTP_*'] entries.
54+
"""
55+
meta = getattr(req, "META", {}) or {}
56+
return {
57+
k[5:].replace("_", "-"): str(v)
58+
for k, v in meta.items()
59+
if isinstance(k, str) and k.startswith("HTTP_")
60+
}
16161

162-
Args:
163-
obj: Potentially a `LocalProxy` or any object.
16462

165-
Returns:
166-
The unwrapped underlying object when possible; otherwise `obj` unchanged.
63+
def _unwrap_local_proxy(obj: Any) -> Any:
16764
"""
168-
getter = getattr(obj, "_get_current_object", None)
169-
if callable(getter):
65+
Best-effort unwrapping for LocalProxy-like objects (e.g., Werkzeug/Flask).
66+
If `_get_current_object` exists and works, return the underlying object.
67+
If calling it raises, swallow and return the original object.
68+
If it doesn't exist, return the original object.
69+
"""
70+
get_current = getattr(obj, "_get_current_object", None)
71+
if callable(get_current):
17072
try:
171-
return getter()
73+
return get_current()
17274
except Exception:
173-
# If unwrapping fails for any reason, fall back to the original object.
174-
# The async core will still attempt structural dispatch via Protocols.
17575
return obj
17676
return obj
17777

17878

179-
def to_unified_request_sync(
180-
req: Union[FlaskRequestLike, DjangoRequestLike]
79+
# -----------------------
80+
# Per-framework converters
81+
# -----------------------
82+
83+
async def _from_starlette(req: StarletteRequestLike) -> Request:
84+
headers = dict(req.headers)
85+
raw = await req.body()
86+
query = _as_listdict(req.query_params)
87+
cookies = dict(req.cookies)
88+
url_str = str(req.url)
89+
path = req.url.path
90+
91+
form: Dict[str, List[str]] = {}
92+
if _is_urlencoded_or_multipart(headers):
93+
form_data = await req.form()
94+
for k in form_data.keys():
95+
# Filter out file-like parts (e.g., UploadFile: has filename & read)
96+
values = [
97+
str(v)
98+
for v in form_data.getlist(k)
99+
if not (hasattr(v, "filename") and hasattr(v, "read"))
100+
]
101+
if values:
102+
form[k] = values
103+
104+
return Request(
105+
method=req.method,
106+
path=path,
107+
url=url_str,
108+
headers=headers,
109+
raw_body=raw,
110+
query=query,
111+
cookies=cookies,
112+
form=form,
113+
)
114+
115+
116+
def _from_flask(req: FlaskRequestLike) -> Request:
117+
headers = dict(req.headers)
118+
url_str: Optional[str] = getattr(req, "url", None)
119+
path: str = req.path
120+
raw: bytes = req.get_data(cache=True)
121+
query = _as_listdict(req.args)
122+
cookies = dict(req.cookies) or _cookies_from_header(headers)
123+
form = _as_listdict(req.form)
124+
125+
return Request(
126+
method=req.method,
127+
path=path,
128+
url=url_str,
129+
headers=headers,
130+
raw_body=raw,
131+
query=query,
132+
cookies=cookies,
133+
form=form,
134+
)
135+
136+
137+
def _from_django(req: DjangoRequestLike) -> Request:
138+
headers = dict(getattr(req, "headers", {}) or {}) or _django_headers_fallback(req)
139+
url_str = req.build_absolute_uri()
140+
path = req.path
141+
raw = bytes(getattr(req, "body", b"") or b"")
142+
query = _as_listdict(getattr(req, "GET", {}))
143+
cookies = dict(getattr(req, "COOKIES", {}) or {})
144+
form = _as_listdict(getattr(req, "POST", {}))
145+
146+
return Request(
147+
method=req.method,
148+
path=path,
149+
url=url_str,
150+
headers=headers,
151+
raw_body=raw,
152+
query=query,
153+
cookies=cookies,
154+
form=form,
155+
)
156+
157+
158+
# -----------------------
159+
# Public API
160+
# -----------------------
161+
162+
async def to_unified_request(
163+
req: Union[StarletteRequestLike, FlaskRequestLike, DjangoRequestLike]
181164
) -> Request:
182165
"""
183-
Synchronous wrapper around `to_unified_request(...)` for WSGI-style apps.
184-
185-
This bridges sync Flask/Django views to the async core by:
186-
• Unwrapping Flask's `LocalProxy` via duck typing (no Werkzeug import).
187-
• Reusing the running event loop when present; otherwise creating one.
188-
• Delegating to the async adapter which handles structural dispatch.
166+
Convert a framework request (Starlette/FastAPI, Flask/Werkzeug, or Django) to a unified snapshot.
189167
190-
Args:
191-
req: A Flask- or Django-like request object (structurally typed). Passing an
192-
already unified `Request` snapshot is an error.
168+
Uses structural typing to detect the request “shape” and extracts an immutable snapshot
169+
(no file uploads). See per-framework helpers for exact extraction rules.
170+
"""
171+
if isinstance(req, StarletteRequestLike):
172+
return await _from_starlette(req)
173+
if isinstance(req, FlaskRequestLike):
174+
return _from_flask(req)
175+
if isinstance(req, DjangoRequestLike):
176+
return _from_django(req)
177+
raise TypeError(f"Unsupported request type: {type(req)!r}")
193178

194-
Returns:
195-
Request: The framework-agnostic snapshot.
196179

197-
Raises:
198-
TypeError: If the provided object is not a supported request shape.
180+
def to_unified_request_sync(
181+
req: Union[StarletteRequestLike, FlaskRequestLike, DjangoRequestLike, Any]
182+
) -> Request:
183+
"""
184+
Synchronous wrapper around `to_unified_request` with LocalProxy unwrapping.
199185
"""
200-
# 1) Unwrap LocalProxy-like objects (no werkzeug import; duck-typing)
201-
req = _unwrap_local_proxy(req)
202-
203-
# 3) Bridge sync -> async: run the async core in (or with) an event loop.
204-
try:
205-
loop = asyncio.get_event_loop()
206-
except RuntimeError:
207-
# No running loop in this thread (typical in WSGI). Create one.
208-
loop = asyncio.new_event_loop()
209-
asyncio.set_event_loop(loop)
210-
211-
# 4) Delegate to the single async entrypoint that handles Starlette/Flask/Django
212-
# via structural typing (Protocols).
213-
return loop.run_until_complete(to_unified_request(req))
186+
unwrapped = _unwrap_local_proxy(req)
187+
# We expect to be called from sync code; create and run a fresh loop.
188+
return asyncio.run(to_unified_request(unwrapped))

0 commit comments

Comments
 (0)