omniload 0.0.0.dev0__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.
Files changed (218) hide show
  1. omniload/conftest.py +72 -0
  2. omniload/main.py +810 -0
  3. omniload/src/.gitignore +10 -0
  4. omniload/src/adjust/__init__.py +108 -0
  5. omniload/src/adjust/adjust_helpers.py +122 -0
  6. omniload/src/airtable/__init__.py +84 -0
  7. omniload/src/allium/__init__.py +128 -0
  8. omniload/src/anthropic/__init__.py +277 -0
  9. omniload/src/anthropic/helpers.py +525 -0
  10. omniload/src/applovin/__init__.py +316 -0
  11. omniload/src/applovin_max/__init__.py +117 -0
  12. omniload/src/appsflyer/__init__.py +325 -0
  13. omniload/src/appsflyer/client.py +110 -0
  14. omniload/src/appstore/__init__.py +142 -0
  15. omniload/src/appstore/client.py +126 -0
  16. omniload/src/appstore/errors.py +15 -0
  17. omniload/src/appstore/models.py +117 -0
  18. omniload/src/appstore/resources.py +179 -0
  19. omniload/src/arrow/__init__.py +81 -0
  20. omniload/src/asana_source/__init__.py +281 -0
  21. omniload/src/asana_source/helpers.py +30 -0
  22. omniload/src/asana_source/settings.py +158 -0
  23. omniload/src/attio/__init__.py +102 -0
  24. omniload/src/attio/helpers.py +65 -0
  25. omniload/src/blob.py +95 -0
  26. omniload/src/bruin/__init__.py +76 -0
  27. omniload/src/chess/__init__.py +180 -0
  28. omniload/src/chess/helpers.py +35 -0
  29. omniload/src/chess/settings.py +18 -0
  30. omniload/src/clickup/__init__.py +85 -0
  31. omniload/src/clickup/helpers.py +47 -0
  32. omniload/src/collector/spinner.py +43 -0
  33. omniload/src/couchbase_source/__init__.py +118 -0
  34. omniload/src/couchbase_source/helpers.py +135 -0
  35. omniload/src/cursor/__init__.py +83 -0
  36. omniload/src/cursor/helpers.py +188 -0
  37. omniload/src/customer_io/__init__.py +486 -0
  38. omniload/src/customer_io/helpers.py +530 -0
  39. omniload/src/destinations.py +982 -0
  40. omniload/src/docebo/__init__.py +589 -0
  41. omniload/src/docebo/client.py +435 -0
  42. omniload/src/docebo/helpers.py +97 -0
  43. omniload/src/dune/__init__.py +104 -0
  44. omniload/src/dune/helpers.py +108 -0
  45. omniload/src/dynamodb/__init__.py +86 -0
  46. omniload/src/elasticsearch/__init__.py +80 -0
  47. omniload/src/elasticsearch/helpers.py +141 -0
  48. omniload/src/errors.py +26 -0
  49. omniload/src/facebook_ads/__init__.py +403 -0
  50. omniload/src/facebook_ads/exceptions.py +19 -0
  51. omniload/src/facebook_ads/helpers.py +296 -0
  52. omniload/src/facebook_ads/settings.py +224 -0
  53. omniload/src/facebook_ads/utils.py +53 -0
  54. omniload/src/factory.py +305 -0
  55. omniload/src/filesystem/__init__.py +133 -0
  56. omniload/src/filesystem/helpers.py +114 -0
  57. omniload/src/filesystem/readers.py +187 -0
  58. omniload/src/filters.py +62 -0
  59. omniload/src/fireflies/__init__.py +151 -0
  60. omniload/src/fireflies/helpers.py +753 -0
  61. omniload/src/fluxx/__init__.py +10013 -0
  62. omniload/src/fluxx/helpers.py +233 -0
  63. omniload/src/frankfurter/__init__.py +157 -0
  64. omniload/src/frankfurter/helpers.py +48 -0
  65. omniload/src/freshdesk/__init__.py +103 -0
  66. omniload/src/freshdesk/freshdesk_client.py +151 -0
  67. omniload/src/freshdesk/settings.py +23 -0
  68. omniload/src/fundraiseup/__init__.py +95 -0
  69. omniload/src/fundraiseup/client.py +81 -0
  70. omniload/src/github/__init__.py +202 -0
  71. omniload/src/github/helpers.py +207 -0
  72. omniload/src/github/queries.py +129 -0
  73. omniload/src/github/settings.py +24 -0
  74. omniload/src/google_ads/__init__.py +198 -0
  75. omniload/src/google_ads/field.py +17 -0
  76. omniload/src/google_ads/metrics.py +254 -0
  77. omniload/src/google_ads/predicates.py +37 -0
  78. omniload/src/google_ads/reports.py +411 -0
  79. omniload/src/google_ads/test_google_ads.py +184 -0
  80. omniload/src/google_analytics/__init__.py +144 -0
  81. omniload/src/google_analytics/helpers.py +312 -0
  82. omniload/src/google_sheets/README.md +95 -0
  83. omniload/src/google_sheets/__init__.py +166 -0
  84. omniload/src/google_sheets/helpers/__init__.py +15 -0
  85. omniload/src/google_sheets/helpers/api_calls.py +160 -0
  86. omniload/src/google_sheets/helpers/data_processing.py +316 -0
  87. omniload/src/gorgias/__init__.py +595 -0
  88. omniload/src/gorgias/helpers.py +166 -0
  89. omniload/src/hostaway/__init__.py +302 -0
  90. omniload/src/hostaway/client.py +288 -0
  91. omniload/src/http/__init__.py +38 -0
  92. omniload/src/http/readers.py +146 -0
  93. omniload/src/http_client.py +24 -0
  94. omniload/src/hubspot/__init__.py +800 -0
  95. omniload/src/hubspot/helpers.py +417 -0
  96. omniload/src/hubspot/settings.py +329 -0
  97. omniload/src/indeed/__init__.py +153 -0
  98. omniload/src/indeed/helpers.py +228 -0
  99. omniload/src/influxdb/__init__.py +46 -0
  100. omniload/src/influxdb/client.py +34 -0
  101. omniload/src/intercom/__init__.py +142 -0
  102. omniload/src/intercom/helpers.py +674 -0
  103. omniload/src/intercom/settings.py +279 -0
  104. omniload/src/isoc_pulse/__init__.py +159 -0
  105. omniload/src/jira_source/__init__.py +377 -0
  106. omniload/src/jira_source/helpers.py +510 -0
  107. omniload/src/jira_source/settings.py +184 -0
  108. omniload/src/kafka/__init__.py +120 -0
  109. omniload/src/kafka/helpers.py +241 -0
  110. omniload/src/kinesis/__init__.py +153 -0
  111. omniload/src/kinesis/helpers.py +96 -0
  112. omniload/src/klaviyo/__init__.py +237 -0
  113. omniload/src/klaviyo/client.py +212 -0
  114. omniload/src/klaviyo/helpers.py +19 -0
  115. omniload/src/linear/__init__.py +634 -0
  116. omniload/src/linear/helpers.py +111 -0
  117. omniload/src/linkedin_ads/__init__.py +266 -0
  118. omniload/src/linkedin_ads/dimension_time_enum.py +17 -0
  119. omniload/src/linkedin_ads/helpers.py +246 -0
  120. omniload/src/loader.py +69 -0
  121. omniload/src/mailchimp/__init__.py +126 -0
  122. omniload/src/mailchimp/helpers.py +226 -0
  123. omniload/src/mailchimp/settings.py +164 -0
  124. omniload/src/masking.py +344 -0
  125. omniload/src/mixpanel/__init__.py +62 -0
  126. omniload/src/mixpanel/client.py +104 -0
  127. omniload/src/monday/__init__.py +246 -0
  128. omniload/src/monday/helpers.py +392 -0
  129. omniload/src/monday/settings.py +325 -0
  130. omniload/src/mongodb/__init__.py +281 -0
  131. omniload/src/mongodb/helpers.py +975 -0
  132. omniload/src/notion/__init__.py +69 -0
  133. omniload/src/notion/helpers/__init__.py +14 -0
  134. omniload/src/notion/helpers/client.py +178 -0
  135. omniload/src/notion/helpers/database.py +92 -0
  136. omniload/src/notion/settings.py +17 -0
  137. omniload/src/partition.py +32 -0
  138. omniload/src/personio/__init__.py +345 -0
  139. omniload/src/personio/helpers.py +100 -0
  140. omniload/src/phantombuster/__init__.py +65 -0
  141. omniload/src/phantombuster/client.py +87 -0
  142. omniload/src/pinterest/__init__.py +82 -0
  143. omniload/src/pipedrive/__init__.py +212 -0
  144. omniload/src/pipedrive/helpers/__init__.py +37 -0
  145. omniload/src/pipedrive/helpers/custom_fields_munger.py +116 -0
  146. omniload/src/pipedrive/helpers/pages.py +129 -0
  147. omniload/src/pipedrive/settings.py +41 -0
  148. omniload/src/pipedrive/typing.py +17 -0
  149. omniload/src/plusvibeai/__init__.py +335 -0
  150. omniload/src/plusvibeai/helpers.py +544 -0
  151. omniload/src/plusvibeai/settings.py +252 -0
  152. omniload/src/primer/__init__.py +45 -0
  153. omniload/src/primer/helpers.py +79 -0
  154. omniload/src/quickbooks/__init__.py +117 -0
  155. omniload/src/reddit_ads/__init__.py +183 -0
  156. omniload/src/reddit_ads/helpers.py +232 -0
  157. omniload/src/resource.py +40 -0
  158. omniload/src/revenuecat/__init__.py +83 -0
  159. omniload/src/revenuecat/helpers.py +237 -0
  160. omniload/src/salesforce/__init__.py +170 -0
  161. omniload/src/salesforce/helpers.py +78 -0
  162. omniload/src/shopify/__init__.py +1953 -0
  163. omniload/src/shopify/exceptions.py +17 -0
  164. omniload/src/shopify/helpers.py +202 -0
  165. omniload/src/shopify/settings.py +19 -0
  166. omniload/src/slack/__init__.py +290 -0
  167. omniload/src/slack/helpers.py +218 -0
  168. omniload/src/slack/settings.py +36 -0
  169. omniload/src/smartsheets/__init__.py +82 -0
  170. omniload/src/snapchat_ads/__init__.py +455 -0
  171. omniload/src/snapchat_ads/client.py +72 -0
  172. omniload/src/snapchat_ads/helpers.py +630 -0
  173. omniload/src/snapchat_ads/settings.py +130 -0
  174. omniload/src/socrata_source/__init__.py +83 -0
  175. omniload/src/socrata_source/helpers.py +85 -0
  176. omniload/src/socrata_source/settings.py +8 -0
  177. omniload/src/solidgate/__init__.py +219 -0
  178. omniload/src/solidgate/helpers.py +154 -0
  179. omniload/src/sources.py +5408 -0
  180. omniload/src/sql_database/__init__.py +0 -0
  181. omniload/src/sql_database/callbacks.py +66 -0
  182. omniload/src/stripe_analytics/__init__.py +183 -0
  183. omniload/src/stripe_analytics/helpers.py +386 -0
  184. omniload/src/stripe_analytics/settings.py +80 -0
  185. omniload/src/table_definition.py +15 -0
  186. omniload/src/testdata/fakebqcredentials.json +14 -0
  187. omniload/src/tiktok_ads/__init__.py +150 -0
  188. omniload/src/tiktok_ads/tiktok_helpers.py +130 -0
  189. omniload/src/time.py +11 -0
  190. omniload/src/trustpilot/__init__.py +48 -0
  191. omniload/src/trustpilot/client.py +48 -0
  192. omniload/src/version.py +6 -0
  193. omniload/src/wise/__init__.py +68 -0
  194. omniload/src/wise/client.py +63 -0
  195. omniload/src/zendesk/__init__.py +480 -0
  196. omniload/src/zendesk/helpers/__init__.py +39 -0
  197. omniload/src/zendesk/helpers/api_helpers.py +119 -0
  198. omniload/src/zendesk/helpers/credentials.py +68 -0
  199. omniload/src/zendesk/helpers/talk_api.py +132 -0
  200. omniload/src/zendesk/settings.py +71 -0
  201. omniload/src/zoom/__init__.py +99 -0
  202. omniload/src/zoom/helpers.py +102 -0
  203. omniload/testdata/.gitignore +2 -0
  204. omniload/testdata/create_replace.csv +21 -0
  205. omniload/testdata/delete_insert_expected.csv +6 -0
  206. omniload/testdata/delete_insert_part1.csv +5 -0
  207. omniload/testdata/delete_insert_part2.csv +6 -0
  208. omniload/testdata/merge_expected.csv +5 -0
  209. omniload/testdata/merge_part1.csv +4 -0
  210. omniload/testdata/merge_part2.csv +5 -0
  211. omniload/tests/unit/test_smartsheets.py +133 -0
  212. omniload-0.0.0.dev0.dist-info/METADATA +439 -0
  213. omniload-0.0.0.dev0.dist-info/RECORD +218 -0
  214. omniload-0.0.0.dev0.dist-info/WHEEL +4 -0
  215. omniload-0.0.0.dev0.dist-info/entry_points.txt +2 -0
  216. omniload-0.0.0.dev0.dist-info/licenses/LICENSE.Apache-2.0 +201 -0
  217. omniload-0.0.0.dev0.dist-info/licenses/LICENSE.md +21 -0
  218. omniload-0.0.0.dev0.dist-info/licenses/NOTICE +35 -0
@@ -0,0 +1,129 @@
1
+ # Copyright 2022-2025 ScaleVector
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ RATE_LIMIT = """
16
+ rateLimit {
17
+ limit
18
+ cost
19
+ remaining
20
+ resetAt
21
+ }
22
+ """
23
+
24
+ ISSUES_QUERY = """
25
+ query($owner: String!, $name: String!, $issues_per_page: Int!, $first_reactions: Int!, $first_comments: Int!, $page_after: String) {
26
+ repository(owner: $owner, name: $name) {
27
+ %s(first: $issues_per_page, orderBy: {field: CREATED_AT, direction: DESC}, after: $page_after) {
28
+ totalCount
29
+ pageInfo {
30
+ endCursor
31
+ startCursor
32
+ }
33
+ nodes {
34
+ # id
35
+ number
36
+ url
37
+ title
38
+ body
39
+ author {login avatarUrl url}
40
+ authorAssociation
41
+ closed
42
+ closedAt
43
+ createdAt
44
+ state
45
+ updatedAt
46
+ reactions(first: $first_reactions) {
47
+ totalCount
48
+ nodes {
49
+ # id
50
+ user {login avatarUrl url}
51
+ content
52
+ createdAt
53
+ }
54
+ }
55
+ comments(first: $first_comments) {
56
+ totalCount
57
+ nodes {
58
+ id
59
+ url
60
+ body
61
+ author {avatarUrl login url}
62
+ authorAssociation
63
+ createdAt
64
+ reactionGroups {content createdAt}
65
+ # reactions(first: 0) {
66
+ # totalCount
67
+ # nodes {
68
+ # # id
69
+ # user {login avatarUrl url}
70
+ # content
71
+ # createdAt
72
+ # }
73
+ # }
74
+ }
75
+ }
76
+ }
77
+ }
78
+ }
79
+ rateLimit {
80
+ limit
81
+ cost
82
+ remaining
83
+ resetAt
84
+ }
85
+ }
86
+ """
87
+
88
+ COMMENT_REACTIONS_QUERY = """
89
+ node_%s: node(id:"%s") {
90
+ ... on IssueComment {
91
+ id
92
+ reactions(first: 100) {
93
+ totalCount
94
+ nodes {
95
+ user {login avatarUrl url}
96
+ content
97
+ createdAt
98
+ }
99
+ }
100
+ }
101
+ }
102
+ """
103
+
104
+ STARGAZERS_QUERY = """
105
+ query($owner: String!, $name: String!, $items_per_page: Int!, $page_after: String) {
106
+ repository(owner: $owner, name: $name) {
107
+ stargazers(first: $items_per_page, orderBy: {field: STARRED_AT, direction: DESC}, after: $page_after) {
108
+ pageInfo {
109
+ endCursor
110
+ startCursor
111
+ }
112
+ edges {
113
+ starredAt
114
+ node {
115
+ login
116
+ avatarUrl
117
+ url
118
+ }
119
+ }
120
+ }
121
+ }
122
+ rateLimit {
123
+ limit
124
+ cost
125
+ remaining
126
+ resetAt
127
+ }
128
+ }
129
+ """
@@ -0,0 +1,24 @@
1
+ # Copyright 2022-2025 ScaleVector
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Github source settings and constants."""
16
+
17
+ START_DATE = "1970-01-01T00:00:00Z"
18
+
19
+ # rest queries
20
+ REST_API_BASE_URL = "https://api.github.com"
21
+ REPO_EVENTS_PATH = "/repos/%s/%s/events"
22
+
23
+ # graphql queries
24
+ GRAPHQL_API_BASE_URL = "https://api.github.com/graphql"
@@ -0,0 +1,198 @@
1
+ # Copyright 2022-2025 ScaleVector
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import json
16
+ from datetime import date, datetime
17
+ from typing import Any, Iterator, Optional
18
+
19
+ import dlt
20
+ import proto # type: ignore
21
+ from dlt.common.exceptions import MissingDependencyException
22
+ from dlt.common.typing import TDataItem
23
+ from dlt.sources import DltResource
24
+ from flatten_json import flatten # type: ignore
25
+ from googleapiclient.discovery import Resource # type: ignore
26
+
27
+ from . import field
28
+ from .metrics import dlt_metrics_schema
29
+ from .predicates import date_predicate
30
+ from .reports import BUILTIN_REPORTS, Report
31
+
32
+ try:
33
+ from google.ads.googleads.client import GoogleAdsClient # type: ignore
34
+ except ImportError:
35
+ raise MissingDependencyException("Requests-OAuthlib", ["google-ads"])
36
+
37
+
38
+ @dlt.source
39
+ def google_ads(
40
+ client: GoogleAdsClient,
41
+ customer_ids: list[str],
42
+ report_spec: Optional[str] = None,
43
+ gaql_query: Optional[str] = None,
44
+ start_date: Optional[datetime] = None,
45
+ end_date: Optional[datetime] = None,
46
+ ) -> Iterator[DltResource]:
47
+ date_range = dlt.sources.incremental(
48
+ "segments_date",
49
+ initial_value=start_date.date(), # type: ignore
50
+ end_value=end_date.date() if end_date is not None else None, # type: ignore
51
+ range_start="closed",
52
+ range_end="closed",
53
+ )
54
+ if report_spec is not None:
55
+ custom_report, _ = Report.from_spec(report_spec)
56
+ yield dlt.resource(
57
+ daily_report,
58
+ name="daily_report",
59
+ write_disposition="merge",
60
+ primary_key=custom_report.primary_keys() + ["customer_id"],
61
+ columns=dlt_metrics_schema(custom_report.metrics),
62
+ )(client, customer_ids, custom_report, date_range)
63
+
64
+ if gaql_query is not None:
65
+ yield dlt.resource(
66
+ run_gaql_query,
67
+ name="gaql_query",
68
+ write_disposition="append",
69
+ max_table_nesting=0,
70
+ )(client, customer_ids, gaql_query, start_date, end_date)
71
+
72
+ for report_name, report in BUILTIN_REPORTS.items():
73
+ yield dlt.resource(
74
+ daily_report,
75
+ name=report_name,
76
+ write_disposition="merge",
77
+ primary_key=report.primary_keys() + ["customer_id"],
78
+ columns=dlt_metrics_schema(report.metrics),
79
+ )(client, customer_ids, report, date_range)
80
+
81
+
82
+ def daily_report(
83
+ client: Resource,
84
+ customer_ids: list[str],
85
+ report: Report,
86
+ date: dlt.sources.incremental[date],
87
+ ) -> Iterator[TDataItem]:
88
+ ga_service = client.get_service("GoogleAdsService")
89
+ fields = report.dimensions + report.metrics + report.segments
90
+ criteria = date_predicate("segments.date", date.last_value, date.end_value) # type:ignore
91
+ query = f"""
92
+ SELECT
93
+ {", ".join(fields)}
94
+ FROM
95
+ {report.resource}
96
+ WHERE
97
+ {criteria}
98
+ """
99
+ if report.unfilterable is True:
100
+ i = query.index("WHERE", 0)
101
+ query = query[:i]
102
+
103
+ allowed_keys = set([field.to_column(k) for k in fields])
104
+ for customer_id in customer_ids:
105
+ stream = ga_service.search_stream(customer_id=customer_id, query=query)
106
+ for batch in stream:
107
+ for row in batch.results:
108
+ data = flatten(merge_lists(to_dict(row)))
109
+ if "segments_date" in data:
110
+ data["segments_date"] = datetime.strptime(
111
+ data["segments_date"], "%Y-%m-%d"
112
+ ).date()
113
+ row_data = {k: v for k, v in data.items() if k in allowed_keys}
114
+ for pk in report.primary_keys():
115
+ if pk not in row_data or row_data[pk] is None or row_data[pk] == "":
116
+ row_data[pk] = "-"
117
+ row_data["customer_id"] = customer_id
118
+ yield row_data
119
+
120
+
121
+ def to_dict(item: Any) -> TDataItem:
122
+ """
123
+ Processes a batch result (page of results per dimension) accordingly
124
+ :param batch:
125
+ :return:
126
+ """
127
+ return json.loads(
128
+ proto.Message.to_json(
129
+ item,
130
+ preserving_proto_field_name=True,
131
+ use_integers_for_enums=False,
132
+ including_default_value_fields=False,
133
+ )
134
+ )
135
+
136
+
137
+ def merge_lists(item: dict) -> dict:
138
+ replacements = {}
139
+ for k, v in item.get("metrics", {}).items():
140
+ if isinstance(v, list):
141
+ replacements[k] = ",".join(v)
142
+ if len(replacements) == 0:
143
+ return item
144
+ item["metrics"].update(replacements)
145
+ return item
146
+
147
+
148
+ def extract_fields(data: dict, field_paths: list[str]) -> dict:
149
+ result = {}
150
+ for path in field_paths:
151
+ parts = path.split(".")
152
+ value: Any = data
153
+ for part in parts:
154
+ if isinstance(value, dict):
155
+ value = value.get(part) if part in value else value.get(part + "_")
156
+ else:
157
+ value = None
158
+ break
159
+ column_name = path.replace(".", "_")
160
+ result[column_name] = value
161
+ return result
162
+
163
+
164
+ def run_gaql_query(
165
+ client: GoogleAdsClient,
166
+ customer_ids: list[str],
167
+ query: str,
168
+ start_date: Optional[datetime] = None,
169
+ end_date: Optional[datetime] = None,
170
+ ) -> Iterator[TDataItem]:
171
+ """
172
+ Execute a raw Google Ads Query Language (GAQL) query.
173
+ Supports :interval_start and :interval_end placeholders for date filtering.
174
+ """
175
+ ga_service = client.get_service("GoogleAdsService")
176
+
177
+ if ":interval_start" in query:
178
+ start_str = start_date.strftime("%Y-%m-%d") if start_date else "1970-01-01"
179
+ query = query.replace(":interval_start", f"'{start_str}'")
180
+
181
+ if ":interval_end" in query:
182
+ end_str = (
183
+ end_date.strftime("%Y-%m-%d")
184
+ if end_date
185
+ else date.today().strftime("%Y-%m-%d")
186
+ )
187
+ query = query.replace(":interval_end", f"'{end_str}'")
188
+
189
+ field_paths = None
190
+ for customer_id in customer_ids:
191
+ stream = ga_service.search_stream(customer_id=customer_id, query=query)
192
+ for batch in stream:
193
+ if field_paths is None:
194
+ field_paths = list(batch.field_mask.paths)
195
+ for row in batch.results:
196
+ data = extract_fields(to_dict(row), field_paths)
197
+ data["customer_id"] = customer_id
198
+ yield data
@@ -0,0 +1,17 @@
1
+ # Copyright 2022-2025 ScaleVector
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ def to_column(field: str) -> str:
17
+ return field.replace(".", "_")
@@ -0,0 +1,254 @@
1
+ # Copyright 2022-2025 ScaleVector
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from typing import List
16
+
17
+ from . import field
18
+
19
+ METRICS_SCHEMA = {
20
+ "metrics.absolute_top_impression_percentage": "DOUBLE",
21
+ "metrics.active_view_cpm": "DOUBLE",
22
+ "metrics.active_view_ctr": "DOUBLE",
23
+ "metrics.active_view_impressions": "INT64",
24
+ "metrics.active_view_measurability": "DOUBLE",
25
+ "metrics.active_view_measurable_cost_micros": "INT64",
26
+ "metrics.active_view_measurable_impressions": "INT64",
27
+ "metrics.active_view_viewability": "DOUBLE",
28
+ "metrics.all_conversions": "DOUBLE",
29
+ "metrics.all_conversions_by_conversion_date": "DOUBLE",
30
+ "metrics.all_conversions_from_click_to_call": "DOUBLE",
31
+ "metrics.all_conversions_from_directions": "DOUBLE",
32
+ "metrics.all_conversions_from_interactions_rate": "DOUBLE",
33
+ "metrics.all_conversions_from_interactions_value_per_interaction": "DOUBLE",
34
+ "metrics.all_conversions_from_location_asset_click_to_call": "DOUBLE",
35
+ "metrics.all_conversions_from_location_asset_directions": "DOUBLE",
36
+ "metrics.all_conversions_from_location_asset_menu": "DOUBLE",
37
+ "metrics.all_conversions_from_location_asset_order": "DOUBLE",
38
+ "metrics.all_conversions_from_location_asset_other_engagement": "DOUBLE",
39
+ "metrics.all_conversions_from_location_asset_store_visits": "DOUBLE",
40
+ "metrics.all_conversions_from_location_asset_website": "DOUBLE",
41
+ "metrics.all_conversions_from_menu": "DOUBLE",
42
+ "metrics.all_conversions_from_order": "DOUBLE",
43
+ "metrics.all_conversions_from_other_engagement": "DOUBLE",
44
+ "metrics.all_conversions_from_store_visit": "DOUBLE",
45
+ "metrics.all_conversions_from_store_website": "DOUBLE",
46
+ "metrics.all_conversions_value": "DOUBLE",
47
+ "metrics.all_conversions_value_by_conversion_date": "DOUBLE",
48
+ "metrics.all_conversions_value_per_cost": "DOUBLE",
49
+ "metrics.all_new_customer_lifetime_value": "DOUBLE",
50
+ "metrics.asset_best_performance_cost_percentage": "DOUBLE",
51
+ "metrics.asset_best_performance_impression_percentage": "DOUBLE",
52
+ "metrics.asset_good_performance_cost_percentage": "DOUBLE",
53
+ "metrics.asset_good_performance_impression_percentage": "DOUBLE",
54
+ "metrics.asset_learning_performance_cost_percentage": "DOUBLE",
55
+ "metrics.asset_learning_performance_impression_percentage": "DOUBLE",
56
+ "metrics.asset_low_performance_cost_percentage": "DOUBLE",
57
+ "metrics.asset_low_performance_impression_percentage": "DOUBLE",
58
+ "metrics.asset_pinned_as_description_position_one_count": "INT64",
59
+ "metrics.asset_pinned_as_description_position_two_count": "INT64",
60
+ "metrics.asset_pinned_as_headline_position_one_count": "INT64",
61
+ "metrics.asset_pinned_as_headline_position_three_count": "INT64",
62
+ "metrics.asset_pinned_as_headline_position_two_count": "INT64",
63
+ "metrics.asset_pinned_total_count": "INT64",
64
+ "metrics.asset_unrated_performance_cost_percentage": "DOUBLE",
65
+ "metrics.asset_unrated_performance_impression_percentage": "DOUBLE",
66
+ "metrics.auction_insight_search_absolute_top_impression_percentage": "DOUBLE",
67
+ "metrics.auction_insight_search_impression_share": "DOUBLE",
68
+ "metrics.auction_insight_search_outranking_share": "DOUBLE",
69
+ "metrics.auction_insight_search_overlap_rate": "DOUBLE",
70
+ "metrics.auction_insight_search_position_above_rate": "DOUBLE",
71
+ "metrics.auction_insight_search_top_impression_percentage": "DOUBLE",
72
+ "metrics.average_cart_size": "DOUBLE",
73
+ "metrics.average_cost": "DOUBLE",
74
+ "metrics.average_cpc": "DOUBLE",
75
+ "metrics.average_cpe": "DOUBLE",
76
+ "metrics.average_cpm": "DOUBLE",
77
+ "metrics.average_cpv": "DOUBLE",
78
+ "metrics.average_impression_frequency_per_user": "DOUBLE",
79
+ "metrics.average_order_value_micros": "INT64",
80
+ "metrics.average_page_views": "DOUBLE",
81
+ "metrics.average_target_cpa_micros": "INT64",
82
+ "metrics.average_target_roas": "DOUBLE",
83
+ "metrics.average_time_on_site": "DOUBLE",
84
+ "metrics.benchmark_average_max_cpc": "DOUBLE",
85
+ "metrics.benchmark_ctr": "DOUBLE",
86
+ "metrics.biddable_app_install_conversions": "DOUBLE",
87
+ "metrics.biddable_app_post_install_conversions": "DOUBLE",
88
+ "metrics.bounce_rate": "DOUBLE",
89
+ "metrics.clicks": "INT64",
90
+ "metrics.combined_clicks": "INT64",
91
+ "metrics.combined_clicks_per_query": "DOUBLE",
92
+ "metrics.combined_queries": "INT64",
93
+ "metrics.content_budget_lost_impression_share": "DOUBLE",
94
+ "metrics.content_impression_share": "DOUBLE",
95
+ "metrics.content_rank_lost_impression_share": "DOUBLE",
96
+ "metrics.conversion_last_conversion_date": "DATE",
97
+ "metrics.conversion_last_received_request_date_time": "DATE",
98
+ "metrics.conversions": "DOUBLE",
99
+ "metrics.conversions_by_conversion_date": "DOUBLE",
100
+ "metrics.conversions_from_interactions_rate": "DOUBLE",
101
+ "metrics.conversions_from_interactions_value_per_interaction": "DOUBLE",
102
+ "metrics.conversions_value": "DOUBLE",
103
+ "metrics.conversions_value_by_conversion_date": "DOUBLE",
104
+ "metrics.conversions_value_per_cost": "DOUBLE",
105
+ "metrics.cost_micros": "INT64",
106
+ "metrics.cost_of_goods_sold_micros": "INT64",
107
+ "metrics.cost_per_all_conversions": "DOUBLE",
108
+ "metrics.cost_per_conversion": "DOUBLE",
109
+ "metrics.cost_per_current_model_attributed_conversion": "DOUBLE",
110
+ "metrics.cross_device_conversions": "DOUBLE",
111
+ "metrics.cross_device_conversions_value_micros": "INT64",
112
+ "metrics.cross_sell_cost_of_goods_sold_micros": "INT64",
113
+ "metrics.cross_sell_gross_profit_micros": "INT64",
114
+ "metrics.cross_sell_revenue_micros": "INT64",
115
+ "metrics.cross_sell_units_sold": "DOUBLE",
116
+ "metrics.ctr": "DOUBLE",
117
+ "metrics.current_model_attributed_conversions": "DOUBLE",
118
+ "metrics.current_model_attributed_conversions_from_interactions_rate": "DOUBLE",
119
+ "metrics.current_model_attributed_conversions_from_interactions_value_per_interaction": "DOUBLE",
120
+ "metrics.current_model_attributed_conversions_value": "DOUBLE",
121
+ "metrics.current_model_attributed_conversions_value_per_cost": "DOUBLE",
122
+ "metrics.eligible_impressions_from_location_asset_store_reach": "INT64",
123
+ "metrics.engagement_rate": "DOUBLE",
124
+ "metrics.engagements": "INT64",
125
+ "metrics.general_invalid_click_rate": "DOUBLE",
126
+ "metrics.general_invalid_clicks": "INT64",
127
+ "metrics.gmail_forwards": "INT64",
128
+ "metrics.gmail_saves": "INT64",
129
+ "metrics.gmail_secondary_clicks": "INT64",
130
+ "metrics.gross_profit_margin": "DOUBLE",
131
+ "metrics.gross_profit_micros": "INT64",
132
+ "metrics.historical_creative_quality_score": "ENUM",
133
+ "metrics.historical_landing_page_quality_score": "ENUM",
134
+ "metrics.historical_quality_score": "INT64",
135
+ "metrics.historical_search_predicted_ctr": "ENUM",
136
+ "metrics.hotel_average_lead_value_micros": "DOUBLE",
137
+ "metrics.hotel_commission_rate_micros": "INT64",
138
+ "metrics.hotel_eligible_impressions": "INT64",
139
+ "metrics.hotel_expected_commission_cost": "DOUBLE",
140
+ "metrics.hotel_price_difference_percentage": "DOUBLE",
141
+ "metrics.impressions": "INT64",
142
+ "metrics.impressions_from_store_reach": "INT64",
143
+ "metrics.interaction_event_types": "ENUM",
144
+ "metrics.interaction_rate": "DOUBLE",
145
+ "metrics.interactions": "INT64",
146
+ "metrics.invalid_click_rate": "DOUBLE",
147
+ "metrics.invalid_clicks": "INT64",
148
+ "metrics.lead_cost_of_goods_sold_micros": "INT64",
149
+ "metrics.lead_gross_profit_micros": "INT64",
150
+ "metrics.lead_revenue_micros": "INT64",
151
+ "metrics.lead_units_sold": "DOUBLE",
152
+ "metrics.linked_entities_count": "INT64",
153
+ "metrics.linked_sample_entities": "STRING",
154
+ "metrics.message_chat_rate": "DOUBLE",
155
+ "metrics.message_chats": "INT64",
156
+ "metrics.message_impressions": "INT64",
157
+ "metrics.mobile_friendly_clicks_percentage": "DOUBLE",
158
+ "metrics.new_customer_lifetime_value": "DOUBLE",
159
+ "metrics.optimization_score_uplift": "DOUBLE",
160
+ "metrics.optimization_score_url": "STRING",
161
+ "metrics.orders": "DOUBLE",
162
+ "metrics.organic_clicks": "INT64",
163
+ "metrics.organic_clicks_per_query": "DOUBLE",
164
+ "metrics.organic_impressions": "INT64",
165
+ "metrics.organic_impressions_per_query": "DOUBLE",
166
+ "metrics.organic_queries": "INT64",
167
+ "metrics.percent_new_visitors": "DOUBLE",
168
+ "metrics.phone_calls": "INT64",
169
+ "metrics.phone_impressions": "INT64",
170
+ "metrics.phone_through_rate": "DOUBLE",
171
+ "metrics.publisher_organic_clicks": "INT64",
172
+ "metrics.publisher_purchased_clicks": "INT64",
173
+ "metrics.publisher_unknown_clicks": "INT64",
174
+ "metrics.relative_ctr": "DOUBLE",
175
+ "metrics.results_conversions_purchase": "DOUBLE",
176
+ "metrics.revenue_micros": "INT64",
177
+ "metrics.sample_best_performance_entities": "STRING",
178
+ "metrics.sample_good_performance_entities": "STRING",
179
+ "metrics.sample_learning_performance_entities": "STRING",
180
+ "metrics.sample_low_performance_entities": "STRING",
181
+ "metrics.sample_unrated_performance_entities": "STRING",
182
+ "metrics.search_absolute_top_impression_share": "DOUBLE",
183
+ "metrics.search_budget_lost_absolute_top_impression_share": "DOUBLE",
184
+ "metrics.search_budget_lost_impression_share": "DOUBLE",
185
+ "metrics.search_budget_lost_top_impression_share": "DOUBLE",
186
+ "metrics.search_click_share": "DOUBLE",
187
+ "metrics.search_exact_match_impression_share": "DOUBLE",
188
+ "metrics.search_impression_share": "DOUBLE",
189
+ "metrics.search_rank_lost_absolute_top_impression_share": "DOUBLE",
190
+ "metrics.search_rank_lost_impression_share": "DOUBLE",
191
+ "metrics.search_rank_lost_top_impression_share": "DOUBLE",
192
+ "metrics.search_top_impression_share": "DOUBLE",
193
+ "metrics.search_volume": "MESSAGE",
194
+ "metrics.sk_ad_network_installs": "INT64",
195
+ "metrics.sk_ad_network_total_conversions": "INT64",
196
+ "metrics.speed_score": "INT64",
197
+ "metrics.store_visits_last_click_model_attributed_conversions": "DOUBLE",
198
+ "metrics.top_impression_percentage": "DOUBLE",
199
+ "metrics.unique_users": "INT64",
200
+ "metrics.units_sold": "DOUBLE",
201
+ "metrics.valid_accelerated_mobile_pages_clicks_percentage": "DOUBLE",
202
+ "metrics.value_per_all_conversions": "DOUBLE",
203
+ "metrics.value_per_all_conversions_by_conversion_date": "DOUBLE",
204
+ "metrics.value_per_conversion": "DOUBLE",
205
+ "metrics.value_per_conversions_by_conversion_date": "DOUBLE",
206
+ "metrics.value_per_current_model_attributed_conversion": "DOUBLE",
207
+ "metrics.video_quartile_p100_rate": "DOUBLE",
208
+ "metrics.video_quartile_p25_rate": "DOUBLE",
209
+ "metrics.video_quartile_p50_rate": "DOUBLE",
210
+ "metrics.video_quartile_p75_rate": "DOUBLE",
211
+ "metrics.video_view_rate": "DOUBLE",
212
+ "metrics.video_view_rate_in_feed": "DOUBLE",
213
+ "metrics.video_view_rate_in_stream": "DOUBLE",
214
+ "metrics.video_view_rate_shorts": "DOUBLE",
215
+ "metrics.video_views": "INT64",
216
+ "metrics.view_through_conversions": "INT64",
217
+ "metrics.view_through_conversions_from_location_asset_click_to_call": "DOUBLE",
218
+ "metrics.view_through_conversions_from_location_asset_directions": "DOUBLE",
219
+ "metrics.view_through_conversions_from_location_asset_menu": "DOUBLE",
220
+ "metrics.view_through_conversions_from_location_asset_order": "DOUBLE",
221
+ "metrics.view_through_conversions_from_location_asset_other_engagement": "DOUBLE",
222
+ "metrics.view_through_conversions_from_location_asset_store_visits": "DOUBLE",
223
+ "metrics.view_through_conversions_from_location_asset_website": "DOUBLE",
224
+ }
225
+
226
+ METRIC_TO_DLT_TYPE = {
227
+ "INT64": "bigint",
228
+ "DOUBLE": "double",
229
+ "STRING": "text",
230
+ "ENUM": "text",
231
+ # TODO: support message types
232
+ # "MESSAGE": "string",
233
+ }
234
+
235
+
236
+ def dlt_metrics_schema(metrics: List[str]):
237
+ """
238
+ Returns a dictionary with only the metrics that are
239
+ present in the given list of metrics.
240
+ """
241
+ schema = {}
242
+ for metric in metrics:
243
+ typ = METRICS_SCHEMA.get(metric)
244
+ if typ is None:
245
+ raise ValueError(f"Unsupported metric {metric}")
246
+
247
+ if typ not in METRIC_TO_DLT_TYPE:
248
+ raise ValueError(f"Unsupported metric '{metric}' of type '{typ}'")
249
+
250
+ # ???: can we make these non-nullable?
251
+ schema[field.to_column(metric)] = {
252
+ "data_type": METRIC_TO_DLT_TYPE[typ],
253
+ }
254
+ return schema
@@ -0,0 +1,37 @@
1
+ # Copyright 2022-2025 ScaleVector
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from datetime import date, datetime, timezone
16
+ from typing import Optional
17
+
18
+
19
+ def date_predicate(column: str, start_date: date, end_date: Optional[date]) -> str:
20
+ """
21
+ Generates a date predicate for the WHERE clause of a
22
+ GAQL query.
23
+ """
24
+ if start_date is None:
25
+ raise ValueError("start_date must be provided")
26
+
27
+ if end_date is None:
28
+ end_date = datetime.now(tz=timezone.utc).date()
29
+
30
+ clauses = []
31
+ if start_date is not None:
32
+ clauses.append(f"""{column} >= '{start_date.strftime("%Y-%m-%d")}'""")
33
+
34
+ if end_date is not None:
35
+ clauses.append(f"""{column} <= '{end_date.strftime("%Y-%m-%d")}'""")
36
+
37
+ return " AND ".join(clauses)