Skip to content

Commit f09437a

Browse files
Source Google Analytics Data API: fix errors; improve acceptance tests; test coverage 90%
1 parent 27bd807 commit f09437a

File tree

7 files changed

+395
-46
lines changed

7 files changed

+395
-46
lines changed

airbyte-integrations/connectors/source-google-analytics-data-api/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,5 @@ COPY source_google_analytics_data_api ./source_google_analytics_data_api
2828
ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py"
2929
ENTRYPOINT ["python", "/airbyte/integration_code/main.py"]
3030

31-
LABEL io.airbyte.version=0.0.3
31+
LABEL io.airbyte.version=0.0.4
3232
LABEL io.airbyte.name=airbyte/source-google-analytics-data-api
Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,41 @@
11
# See [Source Acceptance Tests](https://docs.airbyte.com/connector-development/testing-connectors/source-acceptance-tests-reference)
22
# for more information about how to configure these tests
33
connector_image: airbyte/source-google-analytics-data-api:dev
4-
tests:
4+
acceptance_tests:
55
spec:
6+
tests:
67
- spec_path: "source_google_analytics_data_api/spec.json"
78
connection:
8-
- config_path: "secrets/config.json"
9-
status: "succeed"
10-
- config_path: "integration_tests/invalid_config.json"
11-
status: "failed"
9+
tests:
10+
- config_path: "secrets/config.json"
11+
status: "succeed"
12+
- config_path: "integration_tests/invalid_config.json"
13+
status: "failed"
1214
discovery:
13-
- config_path: "secrets/config.json"
14-
backward_compatibility_tests_config:
15-
disable_for_version: "0.0.2"
15+
tests:
16+
- config_path: "secrets/config.json"
17+
backward_compatibility_tests_config:
18+
disable_for_version: "0.0.2"
1619
basic_read:
17-
- config_path: "secrets/config.json"
18-
configured_catalog_path: "integration_tests/configured_catalog.json"
19-
empty_streams: []
20+
tests:
21+
- config_path: "secrets/config.json"
22+
empty_streams: []
23+
incremental:
24+
tests:
25+
- config_path: "secrets/config.json"
26+
configured_catalog_path: "integration_tests/configured_catalog.json"
27+
future_state:
28+
future_state_path: "integration_tests/abnormal_state.json"
2029
full_refresh:
21-
- config_path: "secrets/config.json"
22-
configured_catalog_path: "integration_tests/configured_catalog.json"
23-
ignored_fields:
24-
"daily_active_users": ["uuid"]
25-
"weekly_active_users": ["uuid"]
26-
"four_weekly_active_users": ["uuid"]
27-
"devices": ["uuid"]
28-
"locations": ["uuid"]
29-
"pages": ["uuid"]
30-
"traffic_sources": ["uuid"]
31-
"website_overview": ["uuid"]
30+
tests:
31+
- config_path: "secrets/config.json"
32+
configured_catalog_path: "integration_tests/configured_catalog.json"
33+
ignored_fields:
34+
"daily_active_users": ["uuid"]
35+
"weekly_active_users": ["uuid"]
36+
"four_weekly_active_users": ["uuid"]
37+
"devices": ["uuid"]
38+
"locations": ["uuid"]
39+
"pages": ["uuid"]
40+
"traffic_sources": ["uuid"]
41+
"website_overview": ["uuid"]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"daily_active_users": {"date": "20501201"},
3+
"weekly_active_users": {"date": "20501201"},
4+
"four_weekly_active_users": {"date": "20501201"},
5+
"devices": {"date": "20501201"},
6+
"locations": {"date": "20501201"},
7+
"pages": {"date": "20501201"},
8+
"traffic_sources": {"date": "20501201"},
9+
"website_overview": {"date": "20501201"}
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"daily_active_users": {"date": "20221201"},
3+
"weekly_active_users": {"date": "20221201"},
4+
"four_weekly_active_users": {"date": "20221201"},
5+
"devices": {"date": "20221201"},
6+
"locations": {"date": "20221201"},
7+
"pages": {"date": "20221201"},
8+
"traffic_sources": {"date": "20221201"},
9+
"website_overview": {"date": "20221201"}
10+
}

airbyte-integrations/connectors/source-google-analytics-data-api/source_google_analytics_data_api/source.py

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -222,44 +222,56 @@ def parse_response(
222222
rows = []
223223

224224
for row in r.get("rows", []):
225-
rows.append(
226-
collections.ChainMap(
227-
*[
228-
self.add_primary_key(),
229-
self.add_property_id(self.config["property_id"]),
230-
self.add_dimensions(dimensions, row),
231-
self.add_metrics(metrics, metrics_type_map, row),
232-
]
233-
)
225+
chain_row = collections.ChainMap(
226+
*[
227+
self.add_primary_key(),
228+
self.add_property_id(self.config["property_id"]),
229+
self.add_dimensions(dimensions, row),
230+
self.add_metrics(metrics, metrics_type_map, row),
231+
]
234232
)
233+
rows.append(dict(chain_row))
235234
r["records"] = rows
236235

237236
yield r
238237

239238

240239
class IncrementalGoogleAnalyticsDataApiStream(GoogleAnalyticsDataApiBaseStream, IncrementalMixin, ABC):
241-
_date_format = "%Y-%m-%d"
240+
_date_format: str = "%Y-%m-%d"
242241

243242
def __init__(self, *args, **kwargs):
244243
super(IncrementalGoogleAnalyticsDataApiStream, self).__init__(*args, **kwargs)
245-
self._cursor_value = None
244+
self._cursor_value: str = ""
246245

247246

248247
class GoogleAnalyticsDataApiGenericStream(IncrementalGoogleAnalyticsDataApiStream):
249-
_default_window_in_days = 1
250-
_record_date_format = "%Y%m%d"
248+
_default_window_in_days: int = 1
249+
_record_date_format: str = "%Y%m%d"
251250

252251
@property
253252
def cursor_field(self) -> Union[str, List[str]]:
254253
return "date"
255254

256255
@property
257256
def state(self) -> MutableMapping[str, Any]:
258-
return {self.cursor_field: self._cursor_value or utils.string_to_date(self.config["date_ranges_start_date"], self._date_format)}
257+
if self._cursor_value:
258+
return {self.cursor_field: self._cursor_value}
259+
return {
260+
self.cursor_field: utils.date_to_string(self._date_parse_probe(self.config["date_ranges_start_date"]), self._record_date_format)
261+
}
259262

260263
@state.setter
261-
def state(self, value):
262-
self._cursor_value = utils.string_to_date(value[self.cursor_field], self._date_format) + datetime.timedelta(days=1)
264+
def state(self, value: dict):
265+
self._cursor_value = utils.date_to_string(
266+
self._date_parse_probe(value[self.cursor_field]) + datetime.timedelta(days=1),
267+
self._record_date_format
268+
)
269+
270+
def _date_parse_probe(self, date_string: str) -> datetime.date:
271+
try:
272+
return utils.string_to_date(date_string, self._record_date_format)
273+
except ValueError:
274+
return utils.string_to_date(date_string, self._date_format)
263275

264276
def request_body_json(
265277
self,
@@ -285,19 +297,18 @@ def read_records(
285297
records = super().read_records(sync_mode=sync_mode, cursor_field=cursor_field, stream_slice=stream_slice, stream_state=stream_state)
286298
for record in records:
287299
for row in record["records"]:
288-
next_cursor_value = utils.string_to_date(row[self.cursor_field], self._record_date_format)
289-
self._cursor_value = max(self._cursor_value, next_cursor_value) if self._cursor_value else next_cursor_value
300+
self._cursor_value: str = max(self._cursor_value, row[self.cursor_field]) if self._cursor_value else row[self.cursor_field]
290301
yield row
291302

292303
def stream_slices(
293304
self, *, sync_mode: SyncMode, cursor_field: List[str] = None, stream_state: Mapping[str, Any] = None
294305
) -> Iterable[Optional[Mapping[str, Any]]]:
295306
dates = []
296307

297-
today: datetime.date = datetime.date.today()
298-
start_date: datetime.date = self.state[self.cursor_field]
308+
today: datetime.date = datetime.date.today()
309+
start_date: datetime.date = utils.string_to_date(self.state[self.cursor_field], self._record_date_format)
299310

300-
timedelta: int = self.config["window_in_days"] or self._default_window_in_days
311+
timedelta: int = self.config.get("window_in_days", self._default_window_in_days)
301312

302313
while start_date <= today:
303314
end_date: datetime.date = start_date + datetime.timedelta(days=timedelta)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import datetime
2+
3+
from source_google_analytics_data_api import utils
4+
from source_google_analytics_data_api.authenticator import GoogleServiceKeyAuthenticator
5+
6+
7+
private_key = """
8+
-----BEGIN RSA PRIVATE KEY-----
9+
MIIBPAIBAAJBAIy64eoS8VCwNnu6+kcyvRc7w/Cw20fnZWcpftLIl33ZWdXl/Q+W
10+
sEQkm2RRWO2R9CGC2bJRZYEbiAabuG4T1LkCAwEAAQJBAIbr5Qv1fUZOqu2VJb58
11+
9qz/r6ti49jcEGwHbH/JsPQFpXWTyKdonibIblLDB4AbZdR25zEM2k2G42OErWjJ
12+
0wECIQD21o4uOMJtAe7GLvg3AeQrb4t9sWlCPuNNxnmKfn8PfQIhAJH0F7vSyD7D
13+
UwRcMht0PU65zWUZArBaI7BlpmFhgNbtAiEA1rIZ6vQtkDjtIW37MYUwm+Milgo4
14+
vokKljx6vM5339UCIEb1Owyvn3cUEypNgHbkfmHl5zu9exct26gI42j4tGDJAiEA
15+
3uFuRTUY3W5s8hxyHcATsGhWfGa4VCSjlKE+sSchksc=
16+
-----END RSA PRIVATE KEY-----
17+
"""
18+
19+
20+
def test_authenticator(mocker):
21+
requests = mocker.MagicMock()
22+
requests.request.return_value.json.side_effect = [
23+
{
24+
"expires_in": GoogleServiceKeyAuthenticator._default_token_lifetime_secs,
25+
"access_token": "ga-access-token-1"
26+
},
27+
{
28+
"expires_in": GoogleServiceKeyAuthenticator._default_token_lifetime_secs,
29+
"access_token": "ga-access-token-2"
30+
}
31+
]
32+
33+
mocker.patch("source_google_analytics_data_api.authenticator.requests", requests)
34+
35+
authenticator = GoogleServiceKeyAuthenticator(credentials={
36+
"client_email": "[email protected]",
37+
"private_key": private_key,
38+
"client_id": "c-airbyte-001"
39+
})
40+
41+
request_object = mocker.MagicMock()
42+
request_object.headers = {}
43+
44+
authenticator(request_object)
45+
assert requests.request.call_count == 1
46+
assert request_object.headers["Authorization"] == f"Bearer ga-access-token-1"
47+
48+
authenticator(request_object)
49+
assert requests.request.call_count == 1
50+
assert request_object.headers["Authorization"] == f"Bearer ga-access-token-1"
51+
52+
authenticator._token["expires_at"] = utils.datetime_to_secs(datetime.datetime.utcnow()) - GoogleServiceKeyAuthenticator._default_token_lifetime_secs
53+
54+
authenticator(request_object)
55+
assert requests.request.call_count == 2
56+
assert request_object.headers["Authorization"] == f"Bearer ga-access-token-2"

0 commit comments

Comments
 (0)