airbyte-source-google-ads 4.1.0rc7.dev202510212244__tar.gz → 4.1.0rc8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {airbyte_source_google_ads-4.1.0rc7.dev202510212244 → airbyte_source_google_ads-4.1.0rc8}/PKG-INFO +2 -2
- {airbyte_source_google_ads-4.1.0rc7.dev202510212244 → airbyte_source_google_ads-4.1.0rc8}/pyproject.toml +2 -2
- {airbyte_source_google_ads-4.1.0rc7.dev202510212244 → airbyte_source_google_ads-4.1.0rc8}/source_google_ads/components.py +210 -180
- {airbyte_source_google_ads-4.1.0rc7.dev202510212244 → airbyte_source_google_ads-4.1.0rc8}/source_google_ads/manifest.yaml +22 -17
- {airbyte_source_google_ads-4.1.0rc7.dev202510212244 → airbyte_source_google_ads-4.1.0rc8}/README.md +0 -0
- {airbyte_source_google_ads-4.1.0rc7.dev202510212244 → airbyte_source_google_ads-4.1.0rc8}/source_google_ads/__init__.py +0 -0
- {airbyte_source_google_ads-4.1.0rc7.dev202510212244 → airbyte_source_google_ads-4.1.0rc8}/source_google_ads/config_migrations.py +0 -0
- {airbyte_source_google_ads-4.1.0rc7.dev202510212244 → airbyte_source_google_ads-4.1.0rc8}/source_google_ads/google_ads.py +0 -0
- {airbyte_source_google_ads-4.1.0rc7.dev202510212244 → airbyte_source_google_ads-4.1.0rc8}/source_google_ads/models.py +0 -0
- {airbyte_source_google_ads-4.1.0rc7.dev202510212244 → airbyte_source_google_ads-4.1.0rc8}/source_google_ads/run.py +0 -0
- {airbyte_source_google_ads-4.1.0rc7.dev202510212244 → airbyte_source_google_ads-4.1.0rc8}/source_google_ads/schemas/customer_client.json +0 -0
- {airbyte_source_google_ads-4.1.0rc7.dev202510212244 → airbyte_source_google_ads-4.1.0rc8}/source_google_ads/schemas/service_accounts.json +0 -0
- {airbyte_source_google_ads-4.1.0rc7.dev202510212244 → airbyte_source_google_ads-4.1.0rc8}/source_google_ads/source.py +0 -0
- {airbyte_source_google_ads-4.1.0rc7.dev202510212244 → airbyte_source_google_ads-4.1.0rc8}/source_google_ads/spec.json +0 -0
- {airbyte_source_google_ads-4.1.0rc7.dev202510212244 → airbyte_source_google_ads-4.1.0rc8}/source_google_ads/streams.py +0 -0
- {airbyte_source_google_ads-4.1.0rc7.dev202510212244 → airbyte_source_google_ads-4.1.0rc8}/source_google_ads/utils.py +0 -0
{airbyte_source_google_ads-4.1.0rc7.dev202510212244 → airbyte_source_google_ads-4.1.0rc8}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: airbyte-source-google-ads
|
|
3
|
-
Version: 4.1.
|
|
3
|
+
Version: 4.1.0rc8
|
|
4
4
|
Summary: Source implementation for Google Ads.
|
|
5
5
|
Home-page: https://airbyte.com
|
|
6
6
|
License: Elv2
|
|
@@ -11,7 +11,7 @@ Classifier: License :: Other/Proprietary License
|
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.10
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
-
Requires-Dist: airbyte-cdk (>=7.
|
|
14
|
+
Requires-Dist: airbyte-cdk (>=7.4.1,<8.0.0)
|
|
15
15
|
Requires-Dist: google-ads (==27.0.0)
|
|
16
16
|
Requires-Dist: pendulum (<3.0.0)
|
|
17
17
|
Requires-Dist: protobuf (==4.25.2)
|
|
@@ -3,7 +3,7 @@ requires = [ "poetry-core>=1.0.0",]
|
|
|
3
3
|
build-backend = "poetry.core.masonry.api"
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
|
-
version = "4.1.0-rc.
|
|
6
|
+
version = "4.1.0-rc.8"
|
|
7
7
|
name = "airbyte-source-google-ads"
|
|
8
8
|
description = "Source implementation for Google Ads."
|
|
9
9
|
authors = [ "Airbyte <contact@airbyte.io>",]
|
|
@@ -20,7 +20,7 @@ python = "^3.10,<3.12"
|
|
|
20
20
|
google-ads = "==27.0.0"
|
|
21
21
|
protobuf = "==4.25.2"
|
|
22
22
|
pendulum = "<3.0.0"
|
|
23
|
-
airbyte-cdk = "^7.
|
|
23
|
+
airbyte-cdk = "^7.4.1"
|
|
24
24
|
|
|
25
25
|
[tool.poetry.scripts]
|
|
26
26
|
source-google-ads = "source_google_ads.run:run"
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
|
|
3
3
|
#
|
|
4
4
|
|
|
5
|
+
import io
|
|
5
6
|
import json
|
|
6
7
|
import logging
|
|
7
8
|
import re
|
|
8
9
|
import threading
|
|
9
|
-
from dataclasses import
|
|
10
|
+
from dataclasses import dataclass, field
|
|
10
11
|
from itertools import groupby
|
|
11
12
|
from typing import Any, Callable, Dict, Generator, Iterable, List, Mapping, MutableMapping, Optional, Tuple, Union
|
|
12
13
|
|
|
@@ -14,6 +15,8 @@ import anyascii
|
|
|
14
15
|
import requests
|
|
15
16
|
|
|
16
17
|
from airbyte_cdk import AirbyteTracedException, FailureType, InterpolatedString
|
|
18
|
+
from airbyte_cdk.sources.declarative.decoders.composite_raw_decoder import JsonParser
|
|
19
|
+
from airbyte_cdk.sources.declarative.decoders.decoder import Decoder
|
|
17
20
|
from airbyte_cdk.sources.declarative.extractors.record_extractor import RecordExtractor
|
|
18
21
|
from airbyte_cdk.sources.declarative.extractors.record_filter import RecordFilter
|
|
19
22
|
from airbyte_cdk.sources.declarative.migrations.state_migration import StateMigration
|
|
@@ -25,8 +28,6 @@ from airbyte_cdk.sources.declarative.transformations import RecordTransformation
|
|
|
25
28
|
from airbyte_cdk.sources.streams.concurrent.default_stream import DefaultStream
|
|
26
29
|
from airbyte_cdk.sources.types import Config, Record, StreamSlice, StreamState
|
|
27
30
|
from airbyte_cdk.sources.utils.transform import TransformConfig, TypeTransformer
|
|
28
|
-
from airbyte_cdk.sources.declarative.decoders.decoder import Decoder
|
|
29
|
-
|
|
30
31
|
|
|
31
32
|
from .google_ads import GoogleAds
|
|
32
33
|
|
|
@@ -414,54 +415,6 @@ class KeysToSnakeCaseGoogleAdsTransformation(RecordTransformation):
|
|
|
414
415
|
return "_".join(token.lower() for token in tokens)
|
|
415
416
|
|
|
416
417
|
|
|
417
|
-
@dataclass
|
|
418
|
-
class ChangeStatusRetriever(SimpleRetriever):
|
|
419
|
-
"""
|
|
420
|
-
Retrieves change status records from the Google Ads API.
|
|
421
|
-
ChangeStatus stream requires custom retriever because Google Ads API requires limit for this stream to be set to 10,000.
|
|
422
|
-
When the number of records exceeds this limit, we need to adjust the start date to the last record's cursor.
|
|
423
|
-
"""
|
|
424
|
-
|
|
425
|
-
QUERY_LIMIT = 10000
|
|
426
|
-
cursor_field: str = "change_status.last_change_date_time"
|
|
427
|
-
|
|
428
|
-
def _read_pages(
|
|
429
|
-
self,
|
|
430
|
-
records_generator_fn: Callable[[Optional[Mapping]], Iterable[Record]],
|
|
431
|
-
stream_state: StreamState,
|
|
432
|
-
stream_slice: StreamSlice,
|
|
433
|
-
) -> Iterable[Record]:
|
|
434
|
-
"""
|
|
435
|
-
Since this stream doesn’t support “real” pagination, we treat each HTTP
|
|
436
|
-
call as a slice defined by a start_date / end_date. If we hit the
|
|
437
|
-
QUERY_LIMIT exactly, we assume there may be more data at the end of that
|
|
438
|
-
slice, so we bump start_date forward to the last-record cursor and retry.
|
|
439
|
-
"""
|
|
440
|
-
while True:
|
|
441
|
-
record_count = 0
|
|
442
|
-
last_record = None
|
|
443
|
-
response = self._fetch_next_page(stream_state, stream_slice)
|
|
444
|
-
|
|
445
|
-
# Yield everything we got
|
|
446
|
-
for rec in records_generator_fn(response):
|
|
447
|
-
record_count += 1
|
|
448
|
-
last_record = rec
|
|
449
|
-
yield rec
|
|
450
|
-
|
|
451
|
-
if record_count < self.QUERY_LIMIT:
|
|
452
|
-
break
|
|
453
|
-
|
|
454
|
-
# Update the stream slice start time to the last record's cursor
|
|
455
|
-
last_cursor = last_record[self.cursor_field]
|
|
456
|
-
cursor_slice = stream_slice.cursor_slice
|
|
457
|
-
cursor_slice["start_time"] = last_cursor
|
|
458
|
-
stream_slice = StreamSlice(
|
|
459
|
-
partition=stream_slice.partition,
|
|
460
|
-
cursor_slice=cursor_slice,
|
|
461
|
-
extra_fields=stream_slice.extra_fields,
|
|
462
|
-
)
|
|
463
|
-
|
|
464
|
-
|
|
465
418
|
@dataclass
|
|
466
419
|
class ChangeStatusRequester(GoogleAdsHttpRequester):
|
|
467
420
|
CURSOR_FIELD: str = "change_status.last_change_date_time"
|
|
@@ -507,7 +460,6 @@ class CriterionRetriever(SimpleRetriever):
|
|
|
507
460
|
def _read_pages(
|
|
508
461
|
self,
|
|
509
462
|
records_generator_fn: Callable[[Optional[Mapping]], Iterable[Record]],
|
|
510
|
-
stream_state: StreamState,
|
|
511
463
|
stream_slice: StreamSlice,
|
|
512
464
|
) -> Iterable[Record]:
|
|
513
465
|
"""
|
|
@@ -537,6 +489,7 @@ class CriterionRetriever(SimpleRetriever):
|
|
|
537
489
|
self.primary_key[0]: _id,
|
|
538
490
|
"deleted_at": ts,
|
|
539
491
|
},
|
|
492
|
+
associated_slice=stream_slice,
|
|
540
493
|
stream_name=self.name,
|
|
541
494
|
)
|
|
542
495
|
else:
|
|
@@ -556,7 +509,7 @@ class CriterionRetriever(SimpleRetriever):
|
|
|
556
509
|
cursor_slice=stream_slice.cursor_slice,
|
|
557
510
|
extra_fields={"change_status.last_change_date_time": updated_times},
|
|
558
511
|
)
|
|
559
|
-
response = self._fetch_next_page(
|
|
512
|
+
response = self._fetch_next_page(new_slice)
|
|
560
513
|
for rec in records_generator_fn(response):
|
|
561
514
|
# attach timestamp from ChangeStatus
|
|
562
515
|
rec.data[self.cursor_field] = time_map.get(rec.data.get(self.primary_key[0]))
|
|
@@ -624,13 +577,26 @@ class GoogleAdsCriterionParentStateMigration(StateMigration):
|
|
|
624
577
|
"""
|
|
625
578
|
|
|
626
579
|
def should_migrate(self, stream_state: Mapping[str, Any]) -> bool:
|
|
627
|
-
return stream_state and "parent_state"
|
|
580
|
+
return stream_state and not stream_state.get("parent_state")
|
|
628
581
|
|
|
629
582
|
def migrate(self, stream_state: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
630
583
|
if not self.should_migrate(stream_state):
|
|
631
584
|
return stream_state
|
|
632
585
|
|
|
633
|
-
return {"parent_state": stream_state}
|
|
586
|
+
return {"parent_state": {"change_status": stream_state}}
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
class GoogleAdsGlobalStateMigration(StateMigration):
|
|
590
|
+
"""
|
|
591
|
+
Migrates global state to include use_global_cursor key. Previously legacy GlobalSubstreamCursor was used.
|
|
592
|
+
"""
|
|
593
|
+
|
|
594
|
+
def should_migrate(self, stream_state: Mapping[str, Any]) -> bool:
|
|
595
|
+
return stream_state and not stream_state.get("use_global_cursor")
|
|
596
|
+
|
|
597
|
+
def migrate(self, stream_state: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
598
|
+
stream_state["use_global_cursor"] = True
|
|
599
|
+
return stream_state
|
|
634
600
|
|
|
635
601
|
|
|
636
602
|
@dataclass(repr=False, eq=False, frozen=True)
|
|
@@ -898,145 +864,209 @@ class CustomGAQuerySchemaLoader(SchemaLoader):
|
|
|
898
864
|
|
|
899
865
|
|
|
900
866
|
@dataclass
|
|
901
|
-
class
|
|
902
|
-
|
|
867
|
+
class StringParseState:
|
|
868
|
+
inside_string: bool = False
|
|
869
|
+
escape_next_character: bool = False
|
|
870
|
+
collected_string_chars: List[str] = field(default_factory=list)
|
|
871
|
+
last_parsed_key: Optional[str] = None
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
@dataclass
|
|
875
|
+
class TopLevelObjectState:
|
|
876
|
+
depth: int = 0
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
@dataclass
|
|
880
|
+
class ResultsArrayState:
|
|
881
|
+
inside_results_array: bool = False
|
|
882
|
+
array_nesting_depth: int = 0
|
|
883
|
+
expecting_results_array_start: bool = False
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
@dataclass
|
|
887
|
+
class RecordParseState:
|
|
888
|
+
inside_record: bool = False
|
|
889
|
+
record_text_buffer: List[str] = field(default_factory=list)
|
|
890
|
+
record_nesting_depth: int = 0
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
@dataclass
|
|
894
|
+
class GoogleAdsStreamingDecoder(Decoder):
|
|
895
|
+
"""
|
|
896
|
+
JSON streaming decoder optimized for Google Ads API responses.
|
|
897
|
+
|
|
898
|
+
Uses a fast JSON parse when the full payload fits within max_direct_decode_bytes;
|
|
899
|
+
otherwise streams records incrementally from the `results` array.
|
|
900
|
+
Ensures truncated or structurally invalid JSON is detected and reported.
|
|
901
|
+
"""
|
|
902
|
+
|
|
903
|
+
chunk_size: int = 5 * 1024 * 1024 # 5 MB
|
|
904
|
+
# Fast-path threshold: if whole body < 20 MB, decode with json.loads
|
|
905
|
+
max_direct_decode_bytes: int = 20 * 1024 * 1024 # 20 MB
|
|
906
|
+
|
|
907
|
+
def __post_init__(self):
|
|
908
|
+
self.parser = JsonParser()
|
|
903
909
|
|
|
904
910
|
def is_stream_response(self) -> bool:
|
|
905
911
|
return True
|
|
906
912
|
|
|
907
|
-
def decode(
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
913
|
+
def decode(self, response: requests.Response) -> Generator[MutableMapping[str, Any], None, None]:
|
|
914
|
+
data, complete = self._buffer_up_to_limit(response)
|
|
915
|
+
if complete:
|
|
916
|
+
yield from self.parser.parse(io.BytesIO(data))
|
|
917
|
+
return
|
|
912
918
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
nonlocal item_buf, collecting_item, item_depth
|
|
944
|
-
obj_text = "".join(item_buf).strip()
|
|
945
|
-
item_buf = []
|
|
946
|
-
collecting_item = False
|
|
947
|
-
item_depth = 0
|
|
948
|
-
if obj_text:
|
|
949
|
-
return json.loads(obj_text)
|
|
919
|
+
records_batch: List[Dict[str, Any]] = []
|
|
920
|
+
for record in self._parse_records_from_stream(data):
|
|
921
|
+
records_batch.append(record)
|
|
922
|
+
if len(records_batch) >= 100:
|
|
923
|
+
yield {"results": records_batch}
|
|
924
|
+
records_batch = []
|
|
925
|
+
|
|
926
|
+
if records_batch:
|
|
927
|
+
yield {"results": records_batch}
|
|
928
|
+
|
|
929
|
+
def _buffer_up_to_limit(self, response: requests.Response) -> Tuple[Union[bytes, Iterable[bytes]], bool]:
|
|
930
|
+
buf = bytearray()
|
|
931
|
+
response_stream = response.iter_content(chunk_size=self.chunk_size)
|
|
932
|
+
|
|
933
|
+
while chunk := next(response_stream, None):
|
|
934
|
+
buf.extend(chunk)
|
|
935
|
+
if len(buf) >= self.max_direct_decode_bytes:
|
|
936
|
+
return (self._chain_prefix_and_stream(bytes(buf), response_stream), False)
|
|
937
|
+
return (bytes(buf), True)
|
|
938
|
+
|
|
939
|
+
@staticmethod
|
|
940
|
+
def _chain_prefix_and_stream(prefix: bytes, rest_stream: Iterable[bytes]) -> Iterable[bytes]:
|
|
941
|
+
yield prefix
|
|
942
|
+
yield from rest_stream
|
|
943
|
+
|
|
944
|
+
def _parse_records_from_stream(self, byte_iter: Iterable[bytes], encoding: str = "utf-8") -> Generator[Dict[str, Any], None, None]:
|
|
945
|
+
string_state = StringParseState()
|
|
946
|
+
results_state = ResultsArrayState()
|
|
947
|
+
record_state = RecordParseState()
|
|
948
|
+
top_level_state = TopLevelObjectState()
|
|
950
949
|
|
|
951
950
|
for chunk in byte_iter:
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
# Always feed characters to item buffer if we're inside an item
|
|
956
|
-
if collecting_item:
|
|
957
|
-
item_buf.append(ch)
|
|
958
|
-
|
|
959
|
-
# --- String handling (so braces inside strings are ignored) ---
|
|
960
|
-
if in_str:
|
|
961
|
-
if esc:
|
|
962
|
-
esc = False
|
|
963
|
-
continue
|
|
964
|
-
if ch == "\\":
|
|
965
|
-
esc = True
|
|
966
|
-
continue
|
|
967
|
-
if ch == '"':
|
|
968
|
-
# string ended
|
|
969
|
-
in_str = False
|
|
970
|
-
last_string = "".join(str_buf)
|
|
971
|
-
str_buf = []
|
|
972
|
-
else:
|
|
973
|
-
str_buf.append(ch)
|
|
974
|
-
continue
|
|
951
|
+
for char in chunk.decode(encoding, errors="replace"):
|
|
952
|
+
self._append_to_current_record_if_any(char, record_state)
|
|
975
953
|
|
|
976
|
-
if
|
|
977
|
-
in_str = True
|
|
978
|
-
str_buf = []
|
|
979
|
-
# If we are collecting an item, we already appended the quote to item_buf above
|
|
954
|
+
if self._update_string_state(char, string_state):
|
|
980
955
|
continue
|
|
981
956
|
|
|
982
|
-
#
|
|
983
|
-
if
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
results_array_depth = depth # this '[' depth
|
|
989
|
-
awaiting_results_array = False
|
|
990
|
-
|
|
991
|
-
# Detect the start of an item object directly inside "results"
|
|
992
|
-
if (
|
|
993
|
-
ch == "{"
|
|
994
|
-
and results_array_depth is not None
|
|
995
|
-
and not collecting_item
|
|
996
|
-
and depth == results_array_depth + 1
|
|
997
|
-
):
|
|
998
|
-
collecting_item = True
|
|
999
|
-
item_buf = ["{"] # start buffer anew
|
|
1000
|
-
item_depth = 1
|
|
1001
|
-
elif collecting_item and ch in "{[":
|
|
1002
|
-
# Nested structure inside item
|
|
1003
|
-
item_depth += 1
|
|
957
|
+
# Track outer braces only outside results array
|
|
958
|
+
if not results_state.inside_results_array:
|
|
959
|
+
if char == "{":
|
|
960
|
+
top_level_state.depth += 1
|
|
961
|
+
elif char == "}":
|
|
962
|
+
top_level_state.depth = max(0, top_level_state.depth - 1)
|
|
1004
963
|
|
|
964
|
+
if not results_state.inside_results_array:
|
|
965
|
+
self._detect_results_array(char, string_state, results_state)
|
|
1005
966
|
continue
|
|
1006
967
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
968
|
+
record = self._parse_record_structure(char, results_state, record_state)
|
|
969
|
+
if record is not None:
|
|
970
|
+
yield record
|
|
971
|
+
|
|
972
|
+
# EOF validation
|
|
973
|
+
if (
|
|
974
|
+
string_state.inside_string
|
|
975
|
+
or record_state.inside_record
|
|
976
|
+
or record_state.record_nesting_depth != 0
|
|
977
|
+
or results_state.inside_results_array
|
|
978
|
+
or results_state.array_nesting_depth != 0
|
|
979
|
+
or top_level_state.depth != 0
|
|
980
|
+
):
|
|
981
|
+
raise AirbyteTracedException(
|
|
982
|
+
message="Response JSON stream ended prematurely and is incomplete.",
|
|
983
|
+
internal_message=(
|
|
984
|
+
"Detected truncated JSON stream: one or more structural elements were not fully closed before the response ended."
|
|
985
|
+
),
|
|
986
|
+
failure_type=FailureType.system_error,
|
|
987
|
+
)
|
|
988
|
+
|
|
989
|
+
def _update_string_state(self, char: str, state: StringParseState) -> bool:
|
|
990
|
+
"""Return True if char was handled as part of string parsing."""
|
|
991
|
+
if state.inside_string:
|
|
992
|
+
if state.escape_next_character:
|
|
993
|
+
state.escape_next_character = False
|
|
994
|
+
return True
|
|
995
|
+
if char == "\\":
|
|
996
|
+
state.escape_next_character = True
|
|
997
|
+
return True
|
|
998
|
+
if char == '"':
|
|
999
|
+
state.inside_string = False
|
|
1000
|
+
state.last_parsed_key = "".join(state.collected_string_chars)
|
|
1001
|
+
state.collected_string_chars.clear()
|
|
1002
|
+
return True
|
|
1003
|
+
state.collected_string_chars.append(char)
|
|
1004
|
+
return True
|
|
1028
1005
|
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1006
|
+
if char == '"':
|
|
1007
|
+
state.inside_string = True
|
|
1008
|
+
state.collected_string_chars.clear()
|
|
1009
|
+
return True
|
|
1033
1010
|
|
|
1034
|
-
|
|
1011
|
+
return False
|
|
1035
1012
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1013
|
+
def _detect_results_array(self, char: str, string_state: StringParseState, results_state: ResultsArrayState) -> None:
|
|
1014
|
+
if char == ":" and string_state.last_parsed_key == "results":
|
|
1015
|
+
results_state.expecting_results_array_start = True
|
|
1016
|
+
elif char == "[" and results_state.expecting_results_array_start:
|
|
1017
|
+
results_state.inside_results_array = True
|
|
1018
|
+
results_state.array_nesting_depth = 1
|
|
1019
|
+
results_state.expecting_results_array_start = False
|
|
1020
|
+
|
|
1021
|
+
def _parse_record_structure(
|
|
1022
|
+
self, char: str, results_state: ResultsArrayState, record_state: RecordParseState
|
|
1023
|
+
) -> Optional[Dict[str, Any]]:
|
|
1024
|
+
if char == "{":
|
|
1025
|
+
if record_state.inside_record:
|
|
1026
|
+
record_state.record_nesting_depth += 1
|
|
1027
|
+
else:
|
|
1028
|
+
self._start_record(record_state)
|
|
1029
|
+
return None
|
|
1030
|
+
|
|
1031
|
+
if char == "}":
|
|
1032
|
+
if record_state.inside_record:
|
|
1033
|
+
record_state.record_nesting_depth -= 1
|
|
1034
|
+
if record_state.record_nesting_depth == 0:
|
|
1035
|
+
return self._finish_record(record_state)
|
|
1036
|
+
return None
|
|
1037
|
+
|
|
1038
|
+
if char == "[":
|
|
1039
|
+
if record_state.inside_record:
|
|
1040
|
+
record_state.record_nesting_depth += 1
|
|
1041
|
+
else:
|
|
1042
|
+
results_state.array_nesting_depth += 1
|
|
1043
|
+
return None
|
|
1044
|
+
|
|
1045
|
+
if char == "]":
|
|
1046
|
+
if record_state.inside_record:
|
|
1047
|
+
record_state.record_nesting_depth -= 1
|
|
1048
|
+
else:
|
|
1049
|
+
results_state.array_nesting_depth -= 1
|
|
1050
|
+
if results_state.array_nesting_depth == 0:
|
|
1051
|
+
results_state.inside_results_array = False
|
|
1052
|
+
|
|
1053
|
+
return None
|
|
1042
1054
|
|
|
1055
|
+
@staticmethod
|
|
1056
|
+
def _append_to_current_record_if_any(char: str, record_state: RecordParseState):
|
|
1057
|
+
if record_state.inside_record:
|
|
1058
|
+
record_state.record_text_buffer.append(char)
|
|
1059
|
+
|
|
1060
|
+
@staticmethod
|
|
1061
|
+
def _start_record(record_state: RecordParseState):
|
|
1062
|
+
record_state.inside_record = True
|
|
1063
|
+
record_state.record_text_buffer = ["{"]
|
|
1064
|
+
record_state.record_nesting_depth = 1
|
|
1065
|
+
|
|
1066
|
+
@staticmethod
|
|
1067
|
+
def _finish_record(record_state: RecordParseState) -> Optional[Dict[str, Any]]:
|
|
1068
|
+
text = "".join(record_state.record_text_buffer).strip()
|
|
1069
|
+
record_state.inside_record = False
|
|
1070
|
+
record_state.record_text_buffer.clear()
|
|
1071
|
+
record_state.record_nesting_depth = 0
|
|
1072
|
+
return json.loads(text) if text else None
|
|
@@ -56,7 +56,7 @@ definitions:
|
|
|
56
56
|
action: IGNORE
|
|
57
57
|
http_codes:
|
|
58
58
|
- 403
|
|
59
|
-
# error_message_contains: "The customer account can\\'t be accessed because it is not yet enabled or has been deactivated."
|
|
59
|
+
# error_message_contains: "The customer account can\\'t be accessed because it is not yet enabled or has been deactivated."
|
|
60
60
|
|
|
61
61
|
base_selector:
|
|
62
62
|
type: RecordSelector
|
|
@@ -98,11 +98,6 @@ definitions:
|
|
|
98
98
|
type: DeclarativeStream
|
|
99
99
|
retriever:
|
|
100
100
|
$ref: "#/definitions/base_retriever"
|
|
101
|
-
paginator:
|
|
102
|
-
type: NoPagination
|
|
103
|
-
decoder:
|
|
104
|
-
type: CustomDecoder
|
|
105
|
-
class_name: "source_google_ads.components.RowsStreamingDecoder"
|
|
106
101
|
requester:
|
|
107
102
|
$ref: "#/definitions/stream_requester"
|
|
108
103
|
record_selector:
|
|
@@ -136,11 +131,6 @@ definitions:
|
|
|
136
131
|
$ref: "#/definitions/base_retriever"
|
|
137
132
|
requester:
|
|
138
133
|
$ref: "#/definitions/stream_requester"
|
|
139
|
-
paginator:
|
|
140
|
-
type: NoPagination
|
|
141
|
-
decoder:
|
|
142
|
-
type: CustomDecoder
|
|
143
|
-
class_name: "source_google_ads.components.RowsStreamingDecoder"
|
|
144
134
|
record_selector:
|
|
145
135
|
extractor:
|
|
146
136
|
type: DpathExtractor
|
|
@@ -295,6 +285,8 @@ definitions:
|
|
|
295
285
|
state_migrations:
|
|
296
286
|
- type: CustomStateMigration
|
|
297
287
|
class_name: source_google_ads.components.GoogleAdsCriterionParentStateMigration
|
|
288
|
+
- type: CustomStateMigration
|
|
289
|
+
class_name: source_google_ads.components.GoogleAdsGlobalStateMigration
|
|
298
290
|
|
|
299
291
|
accessible_accounts:
|
|
300
292
|
$ref: "#/definitions/stream_base"
|
|
@@ -411,7 +403,7 @@ definitions:
|
|
|
411
403
|
class_name: "source_google_ads.components.CustomGAQueryHttpRequester"
|
|
412
404
|
authenticator:
|
|
413
405
|
$ref: "#/definitions/authenticator"
|
|
414
|
-
url_base: "https://googleads.googleapis.com/v20/{{ stream_partition['customer_id'] }}/googleAds:
|
|
406
|
+
url_base: "https://googleads.googleapis.com/v20/{{ stream_partition['customer_id'] }}/googleAds:searchStream"
|
|
415
407
|
http_method: POST
|
|
416
408
|
error_handler:
|
|
417
409
|
$ref: "#/definitions/base_error_handler"
|
|
@@ -430,8 +422,11 @@ definitions:
|
|
|
430
422
|
parent_key: "clientCustomer"
|
|
431
423
|
partition_field: "customer_id"
|
|
432
424
|
stream: "#/definitions/customer_client"
|
|
425
|
+
decoder:
|
|
426
|
+
type: CustomDecoder
|
|
427
|
+
class_name: "source_google_ads.components.GoogleAdsStreamingDecoder"
|
|
433
428
|
paginator:
|
|
434
|
-
|
|
429
|
+
type: NoPagination
|
|
435
430
|
transformations:
|
|
436
431
|
- type: CustomTransformation
|
|
437
432
|
class_name: "source_google_ads.components.KeysToSnakeCaseGoogleAdsTransformation"
|
|
@@ -492,6 +487,13 @@ definitions:
|
|
|
492
487
|
|
|
493
488
|
ad_group_ad_stream:
|
|
494
489
|
$ref: "#/definitions/incremental_stream_base"
|
|
490
|
+
retriever:
|
|
491
|
+
$ref: "#/definitions/incremental_stream_base/retriever"
|
|
492
|
+
paginator:
|
|
493
|
+
type: NoPagination
|
|
494
|
+
decoder:
|
|
495
|
+
type: CustomDecoder
|
|
496
|
+
class_name: "source_google_ads.components.GoogleAdsStreamingDecoder"
|
|
495
497
|
name: ad_group_ad
|
|
496
498
|
primary_key:
|
|
497
499
|
- ad_group.id
|
|
@@ -669,8 +671,6 @@ definitions:
|
|
|
669
671
|
$ref: "#/definitions/base_error_handler"
|
|
670
672
|
paginator:
|
|
671
673
|
$ref: "#/definitions/cursor_paginator"
|
|
672
|
-
decoder:
|
|
673
|
-
type: JsonDecoder
|
|
674
674
|
incremental_sync:
|
|
675
675
|
type: DatetimeBasedCursor
|
|
676
676
|
cursor_field: segments.date
|
|
@@ -845,8 +845,7 @@ definitions:
|
|
|
845
845
|
$parameters:
|
|
846
846
|
url_base: "https://googleads.googleapis.com/v20/{{ stream_partition['customer_id'] }}/googleAds:search"
|
|
847
847
|
retriever:
|
|
848
|
-
type:
|
|
849
|
-
class_name: "source_google_ads.components.ChangeStatusRetriever"
|
|
848
|
+
type: SimpleRetriever
|
|
850
849
|
requester:
|
|
851
850
|
type: CustomRequester
|
|
852
851
|
class_name: "source_google_ads.components.ChangeStatusRequester"
|
|
@@ -862,6 +861,12 @@ definitions:
|
|
|
862
861
|
name: change_status
|
|
863
862
|
paginator:
|
|
864
863
|
$ref: "#/definitions/cursor_paginator"
|
|
864
|
+
pagination_reset:
|
|
865
|
+
type: PaginationReset
|
|
866
|
+
action: SPLIT_USING_CURSOR
|
|
867
|
+
limits:
|
|
868
|
+
type: PaginationResetLimits
|
|
869
|
+
number_of_records: 10000
|
|
865
870
|
record_selector:
|
|
866
871
|
type: RecordSelector
|
|
867
872
|
$parameters:
|
{airbyte_source_google_ads-4.1.0rc7.dev202510212244 → airbyte_source_google_ads-4.1.0rc8}/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|