Skip to content

Commit 7141bea

Browse files
jwfingclaude
andcommitted
Add User-Agent header and logging support
- Add InsForge-Python/{VERSION} User-Agent to all HTTP requests - Read version from package metadata (importlib.metadata) to keep pyproject.toml as single source of truth - Add logging module with setup_logging() helper for INFO/DEBUG levels - INFO: SDK initialization config; DEBUG: HTTP request/response details - Document logging usage in README Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ea26659 commit 7141bea

6 files changed

Lines changed: 103 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: 13 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,18 @@
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")
1115

1216

1317
def build_headers(
1418
api_key: str,
1519
access_token: str | None = None,
1620
extra_headers: Mapping[str, str] | None = None,
1721
) -> dict[str, str]:
18-
headers: dict[str, str] = {"X-API-Key": api_key}
19-
reserved_headers = {"authorization", "x-api-key"}
22+
headers: dict[str, str] = {"X-API-Key": api_key, "User-Agent": USER_AGENT}
23+
reserved_headers = {"authorization", "x-api-key", "user-agent"}
2024

2125
if access_token:
2226
headers["Authorization"] = f"Bearer {access_token}"
@@ -33,6 +37,7 @@ def __init__(self, base_url: str, api_key: str) -> None:
3337
self.base_url = normalize_base_url(base_url)
3438
self.api_key = api_key
3539
self.http_client = httpx.AsyncClient()
40+
logger.info("InsforgeClient initialized (version=%s, base_url=%s)", VERSION, self.base_url)
3641

3742
def _build_headers(
3843
self,
@@ -125,9 +130,12 @@ async def _request(
125130
extra_headers: Mapping[str, str] | None = None,
126131
exception_cls: type[InsforgeHTTPError] = InsforgeHTTPError,
127132
) -> httpx.Response:
133+
url = self._build_url(path)
134+
logger.debug(">>> %s %s params=%s body=%s", method, url, params, json)
135+
128136
response = await self.http_client.request(
129137
method,
130-
self._build_url(path),
138+
url,
131139
params=params,
132140
json=json,
133141
headers=self._build_headers(
@@ -136,6 +144,8 @@ async def _request(
136144
),
137145
)
138146

147+
logger.debug("<<< %s %s status=%d", method, url, response.status_code)
148+
139149
if response.is_error:
140150
raise exception_cls.from_response(method, path, response)
141151

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)