Skip to content

Commit 5a7f662

Browse files
geruhhpal
authored andcommitted
Add Support for Custom Header Configurations in RESTCatalog (apache#467)
1 parent 36874d0 commit 5a7f662

3 files changed

Lines changed: 65 additions & 4 deletions

File tree

mkdocs/docs/configuration.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,19 @@ catalog:
156156
| rest.signing-name | execute-api | The service signing name to use when SigV4 signing a request |
157157
| rest.authorization-url | https://auth-service/cc | Authentication URL to use for client credentials authentication (default: uri + 'v1/oauth/tokens') |
158158

159+
### Headers in RESTCatalog
160+
161+
To configure custom headers in RESTCatalog, include them in the catalog properties with the prefix `header.`. This
162+
ensures that all HTTP requests to the REST service include the specified headers.
163+
164+
```yaml
165+
catalog:
166+
default:
167+
uri: http://rest-catalog/ws/
168+
credential: t-1234:secret
169+
header.content-type: application/vnd.api+json
170+
```
171+
159172
## SQL Catalog
160173

161174
The SQL catalog requires a database for its backend. PyIceberg supports PostgreSQL and SQLite through psycopg2. The database connection has to be configured using the `uri` property. See SQLAlchemy's [documentation for URL format](https://docs.sqlalchemy.org/en/20/core/engines.html#backend-specific-urls):

pyiceberg/catalog/rest.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ class Endpoints:
115115
SIGV4_REGION = "rest.signing-region"
116116
SIGV4_SERVICE = "rest.signing-name"
117117
AUTH_URL = "rest.authorization-url"
118+
HEADER_PREFIX = "header."
118119

119120
NAMESPACE_SEPARATOR = b"\x1f".decode(UTF8)
120121

@@ -242,10 +243,7 @@ def _create_session(self) -> Session:
242243
self._refresh_token(session, self.properties.get(TOKEN))
243244

244245
# Set HTTP headers
245-
session.headers["Content-type"] = "application/json"
246-
session.headers["X-Client-Version"] = ICEBERG_REST_SPEC_VERSION
247-
session.headers["User-Agent"] = f"PyIceberg/{__version__}"
248-
session.headers["X-Iceberg-Access-Delegation"] = "vended-credentials"
246+
self._config_headers(session)
249247

250248
# Configure SigV4 Request Signing
251249
if str(self.properties.get(SIGV4, False)).lower() == "true":
@@ -462,6 +460,17 @@ def _refresh_token(self, session: Optional[Session] = None, new_token: Optional[
462460
if token := self.properties.get(TOKEN):
463461
session.headers[AUTHORIZATION_HEADER] = f"{BEARER_PREFIX} {token}"
464462

463+
def _config_headers(self, session: Session) -> None:
464+
session.headers["Content-type"] = "application/json"
465+
session.headers["X-Client-Version"] = ICEBERG_REST_SPEC_VERSION
466+
session.headers["User-Agent"] = f"PyIceberg/{__version__}"
467+
session.headers["X-Iceberg-Access-Delegation"] = "vended-credentials"
468+
header_properties = self._extract_headers_from_properties()
469+
session.headers.update(header_properties)
470+
471+
def _extract_headers_from_properties(self) -> Dict[str, str]:
472+
return {key[len(HEADER_PREFIX) :]: value for key, value in self.properties.items() if key.startswith(HEADER_PREFIX)}
473+
465474
@retry(**_RETRY_ARGS)
466475
def create_table(
467476
self,

tests/catalog/test_rest.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,45 @@ def test_config_200(requests_mock: Mocker) -> None:
205205
assert history[1].url == "https://iceberg-test-catalog/v1/config?warehouse=s3%3A%2F%2Fsome-bucket"
206206

207207

208+
def test_properties_sets_headers(requests_mock: Mocker) -> None:
209+
requests_mock.get(
210+
f"{TEST_URI}v1/config",
211+
json={"defaults": {}, "overrides": {}},
212+
status_code=200,
213+
)
214+
215+
catalog = RestCatalog(
216+
"rest", uri=TEST_URI, warehouse="s3://some-bucket", **{"header.Content-Type": "application/vnd.api+json"}
217+
)
218+
219+
assert (
220+
catalog._session.headers.get("Content-type") == "application/vnd.api+json"
221+
), "Expected 'Content-Type' header to be 'application/vnd.api+json'"
222+
223+
assert (
224+
requests_mock.last_request.headers["Content-type"] == "application/vnd.api+json"
225+
), "Config request did not include expected 'Content-Type' header"
226+
227+
228+
def test_config_sets_headers(requests_mock: Mocker) -> None:
229+
namespace = "leden"
230+
requests_mock.get(
231+
f"{TEST_URI}v1/config",
232+
json={"defaults": {"header.Content-Type": "application/vnd.api+json"}, "overrides": {}},
233+
status_code=200,
234+
)
235+
requests_mock.post(f"{TEST_URI}v1/namespaces", json={"namespace": [namespace], "properties": {}}, status_code=200)
236+
catalog = RestCatalog("rest", uri=TEST_URI, warehouse="s3://some-bucket")
237+
catalog.create_namespace(namespace)
238+
239+
assert (
240+
catalog._session.headers.get("Content-type") == "application/vnd.api+json"
241+
), "Expected 'Content-Type' header to be 'application/vnd.api+json'"
242+
assert (
243+
requests_mock.last_request.headers["Content-type"] == "application/vnd.api+json"
244+
), "Create namespace request did not include expected 'Content-Type' header"
245+
246+
208247
def test_token_400(rest_mock: Mocker) -> None:
209248
rest_mock.post(
210249
f"{TEST_URI}v1/oauth/tokens",

0 commit comments

Comments
 (0)