Skip to content

Commit d569e7d

Browse files
authored
Merge pull request #2 from InsForge/storage-admin-sdks
Add User-Agent header, logging support, and storage admin helpers
2 parents c43b98d + a1328df commit d569e7d

6 files changed

Lines changed: 134 additions & 7 deletions

File tree

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,28 @@ Exception hierarchy:
196196
- `InsforgeValidationError` - Pydantic validation failures
197197
- `InsforgeSerializationError` - serialization failures
198198

199+
## Logging
200+
201+
The SDK uses Python's built-in `logging` module under the `insforge` logger. By default no logs are emitted. Call `setup_logging` to enable output:
202+
203+
```python
204+
import insforge
205+
206+
# INFO — SDK initialization details and important operation results
207+
insforge.setup_logging("INFO")
208+
209+
# DEBUG — full HTTP request/response (method, URL, params, status code)
210+
insforge.setup_logging("DEBUG")
211+
```
212+
213+
You can also configure the `insforge` logger directly with the standard `logging` module for more advanced setups:
214+
215+
```python
216+
import logging
217+
218+
logging.getLogger("insforge").setLevel(logging.DEBUG)
219+
```
220+
199221
## Authentication Model
200222

201223
The SDK is **stateless** - it never stores or caches tokens. Every method that requires user auth takes an explicit `access_token` parameter. The API key is sent as `X-API-Key` on every request automatically.

insforge/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .client import InsforgeClient
2+
from ._logging import setup_logging
23

3-
__all__ = ["InsforgeClient"]
4+
__all__ = ["InsforgeClient", "setup_logging"]
45

insforge/_base_client.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import logging
34
from typing import Any
45
from typing import Mapping
56
from urllib.parse import urlsplit
@@ -8,15 +9,49 @@
89

910
from .exceptions import InsforgeHTTPError
1011
from ._utils import normalize_base_url
12+
from ._version import VERSION, USER_AGENT
13+
14+
logger = logging.getLogger("insforge")
15+
16+
_SENSITIVE_KEYS = frozenset({
17+
"password",
18+
"new_password",
19+
"newPassword",
20+
"token",
21+
"otp",
22+
"code",
23+
"access_token",
24+
"accessToken",
25+
"refresh_token",
26+
"refreshToken",
27+
"api_key",
28+
"apiKey",
29+
})
30+
31+
_REDACTED = "***"
32+
33+
34+
def _sanitize_body(body: Any) -> Any:
35+
"""Return a copy of *body* with sensitive values replaced by ``'***'``."""
36+
if body is None:
37+
return None
38+
if isinstance(body, dict):
39+
return {
40+
k: _REDACTED if k in _SENSITIVE_KEYS else _sanitize_body(v)
41+
for k, v in body.items()
42+
}
43+
if isinstance(body, list):
44+
return [_sanitize_body(item) for item in body]
45+
return body
1146

1247

1348
def build_headers(
1449
api_key: str,
1550
access_token: str | None = None,
1651
extra_headers: Mapping[str, str] | None = None,
1752
) -> dict[str, str]:
18-
headers: dict[str, str] = {"X-API-Key": api_key}
19-
reserved_headers = {"authorization", "x-api-key"}
53+
headers: dict[str, str] = {"X-API-Key": api_key, "User-Agent": USER_AGENT}
54+
reserved_headers = {"authorization", "x-api-key", "user-agent"}
2055

2156
if access_token:
2257
headers["Authorization"] = f"Bearer {access_token}"
@@ -33,6 +68,7 @@ def __init__(self, base_url: str, api_key: str) -> None:
3368
self.base_url = normalize_base_url(base_url)
3469
self.api_key = api_key
3570
self.http_client = httpx.AsyncClient()
71+
logger.info("InsforgeClient initialized (version=%s, base_url=%s)", VERSION, self.base_url)
3672

3773
def _build_headers(
3874
self,
@@ -125,9 +161,12 @@ async def _request(
125161
extra_headers: Mapping[str, str] | None = None,
126162
exception_cls: type[InsforgeHTTPError] = InsforgeHTTPError,
127163
) -> httpx.Response:
164+
url = self._build_url(path)
165+
logger.debug(">>> %s %s params=%s body=%s", method, url, params, _sanitize_body(json))
166+
128167
response = await self.http_client.request(
129168
method,
130-
self._build_url(path),
169+
url,
131170
params=params,
132171
json=json,
133172
headers=self._build_headers(
@@ -136,6 +175,8 @@ async def _request(
136175
),
137176
)
138177

178+
logger.debug("<<< %s %s status=%d", method, url, response.status_code)
179+
139180
if response.is_error:
140181
raise exception_cls.from_response(method, path, response)
141182

insforge/_logging.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
5+
6+
def setup_logging(level: int | str = logging.WARNING) -> None:
7+
"""Configure the ``insforge`` logger.
8+
9+
Parameters
10+
----------
11+
level:
12+
Logging level. Accepts standard :mod:`logging` constants
13+
(``logging.DEBUG``, ``logging.INFO``, …) or their string names
14+
(``"DEBUG"``, ``"INFO"``, …).
15+
16+
Examples
17+
--------
18+
Show SDK configuration and important operation results::
19+
20+
import insforge
21+
insforge.setup_logging("INFO")
22+
23+
Show full HTTP request / response details for debugging::
24+
25+
import insforge
26+
insforge.setup_logging("DEBUG")
27+
"""
28+
logger = logging.getLogger("insforge")
29+
logger.setLevel(level)
30+
31+
if not logger.handlers:
32+
handler = logging.StreamHandler()
33+
handler.setFormatter(
34+
logging.Formatter("[%(name)s %(levelname)s] %(message)s"),
35+
)
36+
logger.addHandler(handler)

insforge/_version.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from importlib.metadata import version, PackageNotFoundError
2+
3+
try:
4+
VERSION = version("insforge")
5+
except PackageNotFoundError:
6+
VERSION = "0.0.0-dev"
7+
8+
USER_AGENT = f"InsForge-Python/{VERSION}"

insforge/storage/client.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@
66

77
import httpx
88

9+
import logging
10+
911
from .._base_client import BaseClient
1012
from ..exceptions import InsforgeHTTPError
13+
14+
logger = logging.getLogger("insforge")
1115
from .._utils import quote_path_segment
1216
from .models import DownloadStrategy
1317
from .models import StorageBucketListResponse
@@ -130,16 +134,21 @@ async def upload_object(
130134
extra_headers: Mapping[str, str] | None = None,
131135
) -> StorageObjectResponse:
132136
path = self._object_url_path(bucket_name, object_key)
137+
url = self._client._build_url(path)
138+
logger.debug(">>> PUT %s file=%s size=%d", url, object_key, len(data))
139+
133140
response = await self._client.http_client.request(
134141
"PUT",
135-
self._client._build_url(path),
142+
url,
136143
files={"file": (object_key, data, content_type or "application/octet-stream")},
137144
headers=self._client._build_headers(
138145
access_token=access_token,
139146
extra_headers=extra_headers,
140147
),
141148
)
142149

150+
logger.debug("<<< PUT %s status=%d", url, response.status_code)
151+
143152
if response.is_error:
144153
raise InsforgeHTTPError.from_response(
145154
"PUT",
@@ -158,15 +167,20 @@ async def download_object(
158167
extra_headers: Mapping[str, str] | None = None,
159168
) -> StorageDownloadResult:
160169
path = self._object_url_path(bucket_name, object_key)
170+
url = self._client._build_url(path)
171+
logger.debug(">>> GET %s", url)
172+
161173
response = await self._client.http_client.request(
162174
"GET",
163-
self._client._build_url(path),
175+
url,
164176
headers=self._client._build_headers(
165177
access_token=access_token,
166178
extra_headers=extra_headers,
167179
),
168180
)
169181

182+
logger.debug("<<< GET %s status=%d", url, response.status_code)
183+
170184
if response.is_error:
171185
raise InsforgeHTTPError.from_response(
172186
"GET",
@@ -214,13 +228,18 @@ async def upload_object_auto(
214228
access_token: str | None = None,
215229
) -> StorageObjectResponse:
216230
path = f"/api/storage/buckets/{quote_path_segment(bucket_name)}/objects"
231+
url = self._client._build_url(path)
232+
logger.debug(">>> POST %s file=%s size=%d", url, filename, len(data))
233+
217234
response = await self._client.http_client.request(
218235
"POST",
219-
self._client._build_url(path),
236+
url,
220237
files={"file": (filename, data, content_type or "application/octet-stream")},
221238
headers=self._client._build_headers(access_token=access_token),
222239
)
223240

241+
logger.debug("<<< POST %s status=%d", url, response.status_code)
242+
224243
if response.is_error:
225244
raise InsforgeHTTPError.from_response("POST", path, response)
226245

0 commit comments

Comments
 (0)