airbyte-source-google-ads 4.1.0rc7.dev202510212244__py3-none-any.whl → 4.1.1__py3-none-any.whl

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: airbyte-source-google-ads
3
- Version: 4.1.0rc7.dev202510212244
3
+ Version: 4.1.1
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.0.5,<8.0.0)
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)
@@ -1,8 +1,8 @@
1
1
  source_google_ads/__init__.py,sha256=Nlo5H6LlaSgg7tx_LyqMIy3MXiAagfk3izZ9o44VvSE,1201
2
- source_google_ads/components.py,sha256=bp2uW_dxo5hT8NXnBAWWDNi3saVCEbh7uU7XHa2nSyI,40037
2
+ source_google_ads/components.py,sha256=8Vbomr6tbkF12lZh2ocTLWbtxB0ajrO143qYkpSFgQ4,40831
3
3
  source_google_ads/config_migrations.py,sha256=oBi_qNqBpLS8GNCaIOBo0stNdYuyqVl6lkrhdXRwMX8,4405
4
4
  source_google_ads/google_ads.py,sha256=cxS18tz0fFJjmIhlhFQ3Zvu2K8bhDtmsl1kFeO7nNhk,11595
5
- source_google_ads/manifest.yaml,sha256=u0qwByoWFe8GLuxqYqRqkUGV2FOB5n9rioZhHx4cqTc,218452
5
+ source_google_ads/manifest.yaml,sha256=12ZHj08Fijt_4fktAvvkgRrcIZiE6CCaDO4_fYgMnTE,219177
6
6
  source_google_ads/models.py,sha256=ZmdS3z_2roaEQgV2Mx1CDm33MztpQ66SfHDzP8XwZog,1658
7
7
  source_google_ads/run.py,sha256=ydIyq_vSNV5Z4mJYnsO5GyNDsLDd0qibBsq6wnvuFAo,2002
8
8
  source_google_ads/schemas/customer_client.json,sha256=oThcyUDO1yWpxtWPWdoAFqTXEIweF8N4q6mRI73Q6yU,984
@@ -11,7 +11,7 @@ source_google_ads/source.py,sha256=hz5ep6stMWHNvD73PIF_7bjnee49sY9YHHjaYNAPnOQ,1
11
11
  source_google_ads/spec.json,sha256=8hbc7smbaffIkYCkX2BYJLB9kgaH8vYKCg-H0y1FvUs,7810
12
12
  source_google_ads/streams.py,sha256=FB-DNJlXhjQADptT-wrv3iGWoliyRuvDuHGeqiN9HsY,13349
13
13
  source_google_ads/utils.py,sha256=-KpgGv2W8WueXvGRC3xbVreDl5-5-vU9OwzC5SZDKVc,21409
14
- airbyte_source_google_ads-4.1.0rc7.dev202510212244.dist-info/METADATA,sha256=jWTT3CuzoqUYd--izNeWytf-dHqGyaorZfu_ephGHec,5379
15
- airbyte_source_google_ads-4.1.0rc7.dev202510212244.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
16
- airbyte_source_google_ads-4.1.0rc7.dev202510212244.dist-info/entry_points.txt,sha256=pP4Llir93XGkHFDZfXXxK7qOWo9_U1ssCJToyxEUB4w,63
17
- airbyte_source_google_ads-4.1.0rc7.dev202510212244.dist-info/RECORD,,
14
+ airbyte_source_google_ads-4.1.1.dist-info/METADATA,sha256=1VEiA-Dj4Q0xFMw9LmRe9WqwwQkhBoQAG8J4A5UCOQA,5360
15
+ airbyte_source_google_ads-4.1.1.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
16
+ airbyte_source_google_ads-4.1.1.dist-info/entry_points.txt,sha256=pP4Llir93XGkHFDZfXXxK7qOWo9_U1ssCJToyxEUB4w,63
17
+ airbyte_source_google_ads-4.1.1.dist-info/RECORD,,
@@ -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 InitVar, dataclass
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(stream_state, new_slice)
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" not in stream_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 RowsStreamingDecoder(Decoder):
902
- parameters: InitVar[Mapping[str, Any]]
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
- self, response: requests.Response
909
- ) -> Generator[MutableMapping[str, Any], None, None]:
910
- for row in self._iter_rows_from_bytes(response.iter_content(chunk_size=65536)):
911
- yield {"results": [row]}
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
- def _iter_rows_from_bytes(self, byte_iter: Iterable[bytes], encoding: str = "utf-8") -> Generator[Dict[str, Any], None, None]:
914
- """
915
- Incrementally scan the searchStream response and yield each object from the
916
- top-level "results" array as soon as that object is complete, without waiting
917
- for the enclosing message object to finish.
918
-
919
- This is a character-level state machine:
920
- - Handles split chunks and concatenated JSON objects
921
- - Tracks strings/escapes so braces inside strings don't confuse depth
922
- - Detects the `"results": [` array and streams its items one-by-one
923
- """
924
- # Global scanning state
925
- depth = 0
926
- in_str = False
927
- esc = False
928
-
929
- # Detect the "results" array
930
- last_string = None # last completed JSON string token
931
- awaiting_results_array = False
932
- results_array_depth = None # the depth level of the '[' that starts the array
933
-
934
- # Per-item buffering state
935
- collecting_item = False
936
- item_buf = [] # characters of the current item
937
- item_depth = 0 # nesting within the item (starts at 1 when we see '{')
938
-
939
- # Temp buffer for current string token
940
- str_buf = []
941
-
942
- def finish_item():
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
- text = chunk.decode(encoding, errors="replace")
953
- for ch in text:
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 ch == '"':
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
- # --- Structural characters outside strings ---
983
- if ch in "{[":
984
- depth += 1
985
-
986
- # Detect the start of the "results" array: we just saw '[' after key "results": ...
987
- if ch == "[" and awaiting_results_array and results_array_depth is None:
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
- if ch in "}]":
1008
- # If we're collecting an item, adjust its own nesting counter
1009
- if collecting_item:
1010
- item_depth -= 1
1011
- if item_depth == 0:
1012
- # Item just finished -> emit it immediately
1013
- item = finish_item()
1014
- if item is not None:
1015
- yield item
1016
- # Note: we do NOT 'continue' here; we still need to update global depth below
1017
-
1018
- depth -= 1
1019
-
1020
- # If we closed the results array, reset array tracking
1021
- if (
1022
- ch == "]"
1023
- and results_array_depth is not None
1024
- and depth < results_array_depth
1025
- ):
1026
- results_array_depth = None
1027
- continue
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
- # Detect `"results":` key just seen (outside strings)
1030
- if ch == ":" and last_string == "results" and results_array_depth is None:
1031
- awaiting_results_array = True
1032
- # don't 'continue'; normal flow is fine
1006
+ if char == '"':
1007
+ state.inside_string = True
1008
+ state.collected_string_chars.clear()
1009
+ return True
1033
1010
 
1034
- # Commas/whitespace are irrelevant; any other chars just pass through
1011
+ return False
1035
1012
 
1036
- # End of stream: if we somehow have a finished item without seeing the closing bracket
1037
- # (rare, but be defensive), try to flush.
1038
- if collecting_item and item_depth == 0:
1039
- item = finish_item()
1040
- if item is not None:
1041
- yield item
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:search"
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
- $ref: "#/definitions/cursor_paginator"
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: CustomRetriever
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:
@@ -1011,7 +1016,19 @@ definitions:
1011
1016
  "datetime_format": "%Y-%m-%d"
1012
1017
  }
1013
1018
  }
1014
- condition: "{{ (components_values.get('query', '').count('segments.date') == 1 and (components_values.get('query') | regex_search('SELECT.*segments\\.date.*FROM')) is not none) or (components_values.get('query', '').count('segments.date') == 2 and (components_values.get('query') | regex_search('SELECT.*segments\\.date.*FROM')) is not none and (components_values.get('query') | regex_search('ORDER BY.*?segments\\.date.*?LIMIT')) is not none) }}"
1019
+ condition: >-
1020
+ {{
1021
+ (
1022
+ components_values.get('query', '').count('segments.date') == 1
1023
+ and (components_values.get('query') | regex_search('(SELECT[\s\S]*?segments\\.date[\s\S]*?FROM)'))
1024
+ )
1025
+ or
1026
+ (
1027
+ components_values.get('query', '').count('segments.date') == 2
1028
+ and (components_values.get('query') | regex_search('(SELECT[\s\S]*?segments\\.date[\s\S]*?FROM)'))
1029
+ and (components_values.get('query') | regex_search('(ORDER BY[\s\S]*?segments\\.date)'))
1030
+ )
1031
+ }}
1015
1032
  create_or_update: true
1016
1033
  - type: ComponentMappingDefinition
1017
1034
  field_path:
@@ -1028,7 +1045,19 @@ definitions:
1028
1045
  - $parameters
1029
1046
  - cursor_field
1030
1047
  value: "segments.date"
1031
- condition: "{{ (components_values.get('query', '').count('segments.date') == 1 and (components_values.get('query') | regex_search('SELECT.*segments\\.date.*FROM')) is not none) or (components_values.get('query', '').count('segments.date') == 2 and (components_values.get('query') | regex_search('SELECT.*segments\\.date.*FROM')) is not none and (components_values.get('query') | regex_search('ORDER BY.*?segments\\.date.*?LIMIT')) is not none) }}"
1048
+ condition: >-
1049
+ {{
1050
+ (
1051
+ components_values.get('query', '').count('segments.date') == 1
1052
+ and (components_values.get('query') | regex_search('(SELECT[\s\S]*?segments\\.date[\s\S]*?FROM)'))
1053
+ )
1054
+ or
1055
+ (
1056
+ components_values.get('query', '').count('segments.date') == 2
1057
+ and (components_values.get('query') | regex_search('(SELECT[\s\S]*?segments\\.date[\s\S]*?FROM)'))
1058
+ and (components_values.get('query') | regex_search('(ORDER BY[\s\S]*?segments\\.date)'))
1059
+ )
1060
+ }}
1032
1061
  create_or_update: true
1033
1062
  - type: ComponentMappingDefinition
1034
1063
  field_path:
@@ -1042,7 +1071,19 @@ definitions:
1042
1071
  - schema_loader
1043
1072
  - cursor_field
1044
1073
  value: "segments.date"
1045
- condition: "{{ (components_values.get('query', '').count('segments.date') == 1 and (components_values.get('query') | regex_search('SELECT.*segments\\.date.*FROM')) is not none) or (components_values.get('query', '').count('segments.date') == 2 and (components_values.get('query') | regex_search('SELECT.*segments\\.date.*FROM')) is not none and (components_values.get('query') | regex_search('ORDER BY.*?segments\\.date.*?LIMIT')) is not none) }}"
1074
+ condition: >-
1075
+ {{
1076
+ (
1077
+ components_values.get('query', '').count('segments.date') == 1
1078
+ and (components_values.get('query') | regex_search('(SELECT[\s\S]*?segments\\.date[\s\S]*?FROM)'))
1079
+ )
1080
+ or
1081
+ (
1082
+ components_values.get('query', '').count('segments.date') == 2
1083
+ and (components_values.get('query') | regex_search('(SELECT[\s\S]*?segments\\.date[\s\S]*?FROM)'))
1084
+ and (components_values.get('query') | regex_search('(ORDER BY[\s\S]*?segments\\.date)'))
1085
+ )
1086
+ }}
1046
1087
  create_or_update: true
1047
1088
  - type: ComponentMappingDefinition
1048
1089
  field_path: