Skip to content

Commit dff62c9

Browse files
committed
feat(rest_api): custom client for specific resources
1 parent f13e3f1 commit dff62c9

File tree

5 files changed

+88
-1
lines changed

5 files changed

+88
-1
lines changed

dlt/sources/rest_api/__init__.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ def create_resources(
266266
client = RESTClient(
267267
base_url=client_config["base_url"],
268268
headers=client_config.get("headers"),
269-
auth=create_auth(client_config.get("auth")),
269+
auth=create_auth(endpoint_config.get("auth", client_config.get("auth"))),
270270
paginator=create_paginator(client_config.get("paginator")),
271271
session=client_config.get("session"),
272272
)
@@ -410,6 +410,16 @@ def _validate_config(config: RESTAPIConfig) -> None:
410410
auth = client_config.get("auth")
411411
if auth:
412412
auth = _mask_secrets(auth)
413+
resources = c.get("resources", [])
414+
for resource in resources:
415+
if isinstance(resource, str) or isinstance(resource, DltResource):
416+
continue
417+
if "endpoint" in resource:
418+
endpoint = resource.get("endpoint")
419+
if not isinstance(endpoint, str):
420+
auth = endpoint.get("auth")
421+
if auth:
422+
auth = _mask_secrets(auth)
413423

414424
validate_dict(RESTAPIConfig, c, path=".")
415425

dlt/sources/rest_api/typing.py

+1
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ class Endpoint(TypedDict, total=False):
263263
data_selector: Optional[jsonpath.TJsonPath]
264264
response_actions: Optional[List[ResponseAction]]
265265
incremental: Optional[IncrementalConfig]
266+
auth: Optional[AuthConfig]
266267

267268

268269
class ProcessingSteps(TypedDict):

docs/website/docs/dlt-ecosystem/verified-sources/rest_api/basic.md

+26
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,32 @@ A resource configuration is used to define a [dlt resource](../../../general-usa
308308
- `include_from_parent`: A list of fields from the parent resource to be included in the resource output. See the [resource relationships](#include-fields-from-the-parent-resource) section for more details.
309309
- `processing_steps`: A list of [processing steps](#processing-steps-filter-and-transform-data) to filter and transform the data.
310310
- `selected`: A flag to indicate if the resource is selected for loading. This could be useful when you want to load data only from child resources and not from the parent resource.
311+
- `auth`: An optional `AuthConfig` instance. If passed, is used over the one defined in the [dlt resource](../../../general-usage/resource.md) definition. Example:
312+
```py
313+
from dlt.sources.helpers.rest_client.auth import HttpBasicAuth
314+
315+
config = {
316+
"client": {
317+
"auth": {
318+
"type": "bearer",
319+
"token": dlt.secrets["your_api_token"],
320+
}
321+
},
322+
"resources": [
323+
"resource-using-bearer-auth",
324+
{
325+
"name": "my-resource-with-special-auth",
326+
"endpoint": {
327+
# ...
328+
"auth": HttpBasicAuth("user", dlt.secrets["your_basic_auth_password"])
329+
},
330+
# ...
331+
}
332+
]
333+
# ...
334+
}
335+
```
336+
This would use `Bearer` auth as defined in the `client` for `resource-using-bearer-auth` and `Http Basic` auth for `my-resource-with-special-auth`.
311337

312338
You can also pass additional resource parameters that will be used to configure the dlt resource. See [dlt resource API reference](../../../api_reference/extract/decorators#resource) for more details.
313339

tests/sources/rest_api/configurations/source_configs.py

+12
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,18 @@ def repositories():
395395
repositories(),
396396
],
397397
},
398+
{
399+
"client": {"base_url": "https://github.com/api/v2"},
400+
"resources": [
401+
{
402+
"name": "issues",
403+
"endpoint": {
404+
"path": "dlt-hub/{repository}/issues/",
405+
"auth": HttpBasicAuth("", "BASIC_AUTH_TOKEN"),
406+
},
407+
}
408+
],
409+
},
398410
]
399411

400412

tests/sources/rest_api/integration/test_response_actions.py

+38
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import base64
12
import pytest
23
from dlt.common import json
34
from dlt.sources.helpers.requests import Response
@@ -316,3 +317,40 @@ def add_field(response: Response, *args, **kwargs) -> Response:
316317
mock_response_hook_2.assert_called_once()
317318

318319
assert all(record["custom_field"] == "foobar" for record in data)
320+
321+
322+
def test_auth_overwrites_for_specific_endpoints(mock_api_server, mocker):
323+
def custom_hook(response: Response, *args, **kwargs) -> Response:
324+
assert response.request.headers["Authorization"] == f"Basic {base64.b64encode(b'U:P').decode('ascii')}"
325+
return response
326+
327+
mock_response_hook = mocker.Mock(side_effect=custom_hook)
328+
mock_source = rest_api_source(
329+
{
330+
"client": {
331+
"base_url": "https://api.example.com",
332+
"auth": {
333+
"type": "bearer",
334+
"token": "T",
335+
},
336+
},
337+
"resources": [
338+
{
339+
"name": "posts",
340+
"endpoint": {
341+
"auth": {
342+
"type": "http_basic",
343+
"username": "U",
344+
"password": "P",
345+
},
346+
"response_actions": [
347+
mock_response_hook,
348+
],
349+
},
350+
},
351+
],
352+
}
353+
)
354+
355+
list(mock_source.with_resources("posts").add_limit(1))
356+
mock_response_hook.assert_called_once()

0 commit comments

Comments
 (0)