-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy path_base_client.py
More file actions
192 lines (157 loc) · 5.33 KB
/
Copy path_base_client.py
File metadata and controls
192 lines (157 loc) · 5.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
from __future__ import annotations
import logging
from typing import Any
from typing import Mapping
from urllib.parse import urlsplit
import httpx
from .exceptions import InsforgeHTTPError
from ._utils import normalize_base_url
from ._version import VERSION, USER_AGENT
logger = logging.getLogger("insforge")
_SENSITIVE_KEYS = frozenset({
"password",
"new_password",
"newPassword",
"token",
"otp",
"code",
"access_token",
"accessToken",
"refresh_token",
"refreshToken",
"api_key",
"apiKey",
})
_REDACTED = "***"
def _sanitize_body(body: Any) -> Any:
"""Return a copy of *body* with sensitive values replaced by ``'***'``."""
if body is None:
return None
if isinstance(body, dict):
return {
k: _REDACTED if k in _SENSITIVE_KEYS else _sanitize_body(v)
for k, v in body.items()
}
if isinstance(body, list):
return [_sanitize_body(item) for item in body]
return body
def build_headers(
api_key: str,
access_token: str | None = None,
extra_headers: Mapping[str, str] | None = None,
) -> dict[str, str]:
headers: dict[str, str] = {"X-API-Key": api_key, "User-Agent": USER_AGENT}
reserved_headers = {"authorization", "x-api-key", "user-agent"}
if access_token:
headers["Authorization"] = f"Bearer {access_token}"
for key, value in (extra_headers or {}).items():
if key.lower() not in reserved_headers:
headers[key] = value
return headers
class BaseClient:
def __init__(self, base_url: str, api_key: str) -> None:
self.base_url = normalize_base_url(base_url)
self.api_key = api_key
self.http_client = httpx.AsyncClient()
logger.info("InsforgeClient initialized (version=%s, base_url=%s)", VERSION, self.base_url)
def _build_headers(
self,
*,
access_token: str | None = None,
extra_headers: Mapping[str, str] | None = None,
) -> dict[str, str]:
return build_headers(
api_key=self.api_key,
access_token=access_token,
extra_headers=extra_headers,
)
def _build_url(self, path: str) -> httpx.URL:
parsed_path = urlsplit(path)
normalized_path = parsed_path.path.lstrip("/")
base_path = self.base_url.path.rstrip("/")
if base_path:
full_path = f"{base_path}/{normalized_path}"
else:
full_path = f"/{normalized_path}"
query = parsed_path.query.encode() if parsed_path.query else None
return self.base_url.copy_with(path=full_path, query=query)
async def _request_json(
self,
method: str,
path: str,
*,
params: Mapping[str, str] | None = None,
json: Any = None,
access_token: str | None = None,
extra_headers: Mapping[str, str] | None = None,
exception_cls: type[InsforgeHTTPError] = InsforgeHTTPError,
) -> object:
response = await self._request(
method,
path,
params=params,
json=json,
access_token=access_token,
extra_headers=extra_headers,
exception_cls=exception_cls,
)
return response.json()
async def _request_content(
self,
method: str,
path: str,
*,
params: Mapping[str, str] | None = None,
json: Any = None,
access_token: str | None = None,
extra_headers: Mapping[str, str] | None = None,
exception_cls: type[InsforgeHTTPError] = InsforgeHTTPError,
) -> object:
response = await self._request(
method,
path,
params=params,
json=json,
access_token=access_token,
extra_headers=extra_headers,
exception_cls=exception_cls,
)
content_type = response.headers.get("content-type", "").split(";", maxsplit=1)[0].strip().lower()
if content_type.startswith("text/"):
return response.text
if content_type.endswith("+json") or content_type == "application/json":
return response.json()
return response.content
async def _request(
self,
method: str,
path: str,
*,
params: Mapping[str, str] | None = None,
json: Any = None,
access_token: str | None = None,
extra_headers: Mapping[str, str] | None = None,
exception_cls: type[InsforgeHTTPError] = InsforgeHTTPError,
) -> httpx.Response:
url = self._build_url(path)
logger.debug(">>> %s %s params=%s body=%s", method, url, params, _sanitize_body(json))
response = await self.http_client.request(
method,
url,
params=params,
json=json,
headers=self._build_headers(
access_token=access_token,
extra_headers=extra_headers,
),
)
logger.debug("<<< %s %s status=%d", method, url, response.status_code)
if response.is_error:
raise exception_cls.from_response(method, path, response)
return response
async def aclose(self) -> None:
await self.http_client.aclose()
async def __aenter__(self) -> "BaseClient":
return self
async def __aexit__(self, exc_type, exc, tb) -> None:
await self.aclose()