Skip to content

Commit 9934979

Browse files
authored
🐛 Source Klaviyo: have region also be a number (#44930)
1 parent 59d4255 commit 9934979

File tree

8 files changed

+245
-46
lines changed

8 files changed

+245
-46
lines changed

airbyte-integrations/connectors/source-klaviyo/metadata.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ data:
88
definitionId: 95e8cffd-b8c4-4039-968e-d32fb4a69bde
99
connectorBuildOptions:
1010
baseImage: docker.io/airbyte/python-connector-base:2.0.0@sha256:c44839ba84406116e8ba68722a0f30e8f6e7056c726f447681bb9e9ece8bd916
11-
dockerImageTag: 2.10.1
11+
dockerImageTag: 2.10.2
1212
dockerRepository: airbyte/source-klaviyo
1313
githubIssueLabel: source-klaviyo
1414
icon: klaviyo.svg

airbyte-integrations/connectors/source-klaviyo/pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ requires = [ "poetry-core>=1.0.0",]
33
build-backend = "poetry.core.masonry.api"
44

55
[tool.poetry]
6-
version = "2.10.1"
6+
version = "2.10.2"
77
name = "source-klaviyo"
88
description = "Source implementation for Klaviyo."
99
authors = [ "Airbyte <[email protected]>",]

airbyte-integrations/connectors/source-klaviyo/source_klaviyo/manifest.yaml

+5-1
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,11 @@ definitions:
135135
schema_loader:
136136
type: InlineSchemaLoader
137137
schema: "#/definitions/profiles_schema"
138-
retriever: "#/definitions/profiles_retriever"
138+
retriever:
139+
$ref: "#/definitions/profiles_retriever"
140+
record_selector:
141+
$ref: "#/definitions/selector"
142+
schema_normalization: Default
139143
$parameters:
140144
path: "profiles"
141145

airbyte-integrations/connectors/source-klaviyo/unit_tests/integration/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Copyright (c) 2024 Airbyte, Inc., all rights reserved.
2+
3+
from typing import Any, Dict
4+
5+
6+
class KlaviyoConfigBuilder:
7+
def __init__(self) -> None:
8+
self._config = {"api_key":"an_api_key","start_date":"2021-01-01T00:00:00Z"}
9+
10+
def build(self) -> Dict[str, Any]:
11+
return self._config
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
2+
3+
from typing import Any, Dict, Optional
4+
from unittest import TestCase
5+
6+
from airbyte_cdk.test.catalog_builder import CatalogBuilder
7+
from airbyte_cdk.test.entrypoint_wrapper import EntrypointOutput, read
8+
from airbyte_cdk.test.mock_http import HttpMocker, HttpRequest
9+
from airbyte_cdk.test.mock_http.request import ANY_QUERY_PARAMS
10+
from airbyte_cdk.test.mock_http.response_builder import (
11+
FieldPath,
12+
HttpResponseBuilder,
13+
NestedPath,
14+
RecordBuilder,
15+
create_record_builder,
16+
create_response_builder,
17+
find_template,
18+
)
19+
from airbyte_protocol.models import ConfiguredAirbyteCatalog, SyncMode
20+
from integration.config import KlaviyoConfigBuilder
21+
from source_klaviyo import SourceKlaviyo
22+
23+
_ENDPOINT_TEMPLATE_NAME = "profiles"
24+
_STREAM_NAME = "profiles"
25+
_RECORDS_PATH = FieldPath("data")
26+
27+
28+
def _config() -> KlaviyoConfigBuilder:
29+
return KlaviyoConfigBuilder()
30+
31+
32+
def _catalog(sync_mode: SyncMode) -> ConfiguredAirbyteCatalog:
33+
return CatalogBuilder().with_stream(_STREAM_NAME, sync_mode).build()
34+
35+
36+
def _a_profile_request() -> HttpRequest:
37+
return HttpRequest(
38+
url=f"https://a.klaviyo.com/api/profiles",
39+
query_params=ANY_QUERY_PARAMS
40+
)
41+
42+
43+
def _a_profile() -> RecordBuilder:
44+
return create_record_builder(
45+
find_template(_ENDPOINT_TEMPLATE_NAME, __file__),
46+
_RECORDS_PATH,
47+
record_id_path=FieldPath("id"),
48+
record_cursor_path=NestedPath(["attributes", "updated"]),
49+
)
50+
51+
52+
def _profiles_response() -> HttpResponseBuilder:
53+
return create_response_builder(
54+
find_template(_ENDPOINT_TEMPLATE_NAME, __file__),
55+
_RECORDS_PATH,
56+
)
57+
58+
59+
def _read(
60+
config_builder: KlaviyoConfigBuilder, sync_mode: SyncMode, state: Optional[Dict[str, Any]] = None, expecting_exception: bool = False
61+
) -> EntrypointOutput:
62+
catalog = _catalog(sync_mode)
63+
config = config_builder.build()
64+
return read(SourceKlaviyo(), config, catalog, state, expecting_exception)
65+
66+
67+
class FullRefreshTest(TestCase):
68+
@HttpMocker()
69+
def test_when_read_then_extract_records(self, http_mocker: HttpMocker) -> None:
70+
http_mocker.get(
71+
_a_profile_request(),
72+
_profiles_response().with_record(_a_profile()).build(),
73+
)
74+
75+
output = _read(_config(), SyncMode.full_refresh)
76+
77+
assert len(output.records) == 1
78+
79+
@HttpMocker()
80+
def test_given_region_is_number_when_read_then_cast_as_string(self, http_mocker: HttpMocker) -> None:
81+
http_mocker.get(
82+
_a_profile_request(),
83+
_profiles_response().with_record(_a_profile().with_field(NestedPath(["attributes", "location", "region"]), 10)).build(),
84+
)
85+
86+
output = _read(_config(), SyncMode.full_refresh)
87+
88+
assert len(output.records) == 1
89+
assert isinstance(output.records[0].record.data["attributes"]["location"]["region"], str)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
{
2+
"data": [
3+
{
4+
"type": "profile",
5+
"id": "01G4CDTEP140TDXZ0692XSA61K",
6+
"attributes": {
7+
"email": "[email protected]",
8+
"phone_number": "+13523772689",
9+
"external_id": null,
10+
"anonymous_id": null,
11+
"first_name": "Tracey",
12+
"last_name": "Witting",
13+
"organization": null,
14+
"locale": null,
15+
"title": null,
16+
"image": null,
17+
"created": "2022-05-31T06:46:01+00:00",
18+
"updated": "2022-05-31T06:46:01+00:00",
19+
"last_event_date": null,
20+
"location": {
21+
"address1": "2088 Rempel Road",
22+
"region": null,
23+
"zip": null,
24+
"country": "United States",
25+
"address2": null,
26+
"longitude": null,
27+
"city": null,
28+
"latitude": null,
29+
"timezone": null,
30+
"ip": null
31+
},
32+
"properties": {
33+
"Accepts Marketing": false,
34+
"Shopify Tags": ["developer-tools-generator"]
35+
},
36+
"subscriptions": {
37+
"email": {
38+
"marketing": {
39+
"consent": "NEVER_SUBSCRIBED",
40+
"timestamp": null,
41+
"method": null,
42+
"method_detail": null,
43+
"custom_method_detail": null,
44+
"double_optin": null,
45+
"suppressions": [],
46+
"list_suppressions": []
47+
}
48+
},
49+
"sms": {
50+
"marketing": {
51+
"consent": "NEVER_SUBSCRIBED",
52+
"timestamp": null,
53+
"method": null,
54+
"method_detail": null
55+
}
56+
}
57+
},
58+
"predictive_analytics": {
59+
"historic_clv": null,
60+
"predicted_clv": null,
61+
"total_clv": null,
62+
"historic_number_of_orders": null,
63+
"predicted_number_of_orders": null,
64+
"average_days_between_orders": null,
65+
"average_order_value": null,
66+
"churn_probability": null,
67+
"expected_date_of_next_order": null
68+
}
69+
},
70+
"relationships": {
71+
"lists": {
72+
"links": {
73+
"self": "https://a.klaviyo.com/api/profiles/01G4CDTEP140TDXZ0692XSA61K/relationships/lists/",
74+
"related": "https://a.klaviyo.com/api/profiles/01G4CDTEP140TDXZ0692XSA61K/lists/"
75+
}
76+
},
77+
"segments": {
78+
"links": {
79+
"self": "https://a.klaviyo.com/api/profiles/01G4CDTEP140TDXZ0692XSA61K/relationships/segments/",
80+
"related": "https://a.klaviyo.com/api/profiles/01G4CDTEP140TDXZ0692XSA61K/segments/"
81+
}
82+
}
83+
},
84+
"links": {
85+
"self": "https://a.klaviyo.com/api/profiles/01G4CDTEP140TDXZ0692XSA61K/"
86+
}
87+
}
88+
],
89+
"links": {
90+
"self": "https://a.klaviyo.com/api/profiles?additional-fields%5Bprofile%5D=predictive_analytics&page%5Bsize%5D=100&filter=greater-than%28updated%2C2021-01-01T00%3A00%3A00%2B0000%29&sort=updated&page%5Bcursor%5D=bmV4dDo6dXBkYXRlZDo6MjAyMi0wNS0zMSAwNjo0NjowMSswMDowMDo6aWQ6OjAxRzRDRFRFTTZKMjBHRzVRNU41Q0Q4V0pW",
91+
"next": null,
92+
"prev": null
93+
}
94+
}

0 commit comments

Comments
 (0)