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.
- omniload/conftest.py +72 -0
- omniload/main.py +810 -0
- omniload/src/.gitignore +10 -0
- omniload/src/adjust/__init__.py +108 -0
- omniload/src/adjust/adjust_helpers.py +122 -0
- omniload/src/airtable/__init__.py +84 -0
- omniload/src/allium/__init__.py +128 -0
- omniload/src/anthropic/__init__.py +277 -0
- omniload/src/anthropic/helpers.py +525 -0
- omniload/src/applovin/__init__.py +316 -0
- omniload/src/applovin_max/__init__.py +117 -0
- omniload/src/appsflyer/__init__.py +325 -0
- omniload/src/appsflyer/client.py +110 -0
- omniload/src/appstore/__init__.py +142 -0
- omniload/src/appstore/client.py +126 -0
- omniload/src/appstore/errors.py +15 -0
- omniload/src/appstore/models.py +117 -0
- omniload/src/appstore/resources.py +179 -0
- omniload/src/arrow/__init__.py +81 -0
- omniload/src/asana_source/__init__.py +281 -0
- omniload/src/asana_source/helpers.py +30 -0
- omniload/src/asana_source/settings.py +158 -0
- omniload/src/attio/__init__.py +102 -0
- omniload/src/attio/helpers.py +65 -0
- omniload/src/blob.py +95 -0
- omniload/src/bruin/__init__.py +76 -0
- omniload/src/chess/__init__.py +180 -0
- omniload/src/chess/helpers.py +35 -0
- omniload/src/chess/settings.py +18 -0
- omniload/src/clickup/__init__.py +85 -0
- omniload/src/clickup/helpers.py +47 -0
- omniload/src/collector/spinner.py +43 -0
- omniload/src/couchbase_source/__init__.py +118 -0
- omniload/src/couchbase_source/helpers.py +135 -0
- omniload/src/cursor/__init__.py +83 -0
- omniload/src/cursor/helpers.py +188 -0
- omniload/src/customer_io/__init__.py +486 -0
- omniload/src/customer_io/helpers.py +530 -0
- omniload/src/destinations.py +982 -0
- omniload/src/docebo/__init__.py +589 -0
- omniload/src/docebo/client.py +435 -0
- omniload/src/docebo/helpers.py +97 -0
- omniload/src/dune/__init__.py +104 -0
- omniload/src/dune/helpers.py +108 -0
- omniload/src/dynamodb/__init__.py +86 -0
- omniload/src/elasticsearch/__init__.py +80 -0
- omniload/src/elasticsearch/helpers.py +141 -0
- omniload/src/errors.py +26 -0
- omniload/src/facebook_ads/__init__.py +403 -0
- omniload/src/facebook_ads/exceptions.py +19 -0
- omniload/src/facebook_ads/helpers.py +296 -0
- omniload/src/facebook_ads/settings.py +224 -0
- omniload/src/facebook_ads/utils.py +53 -0
- omniload/src/factory.py +305 -0
- omniload/src/filesystem/__init__.py +133 -0
- omniload/src/filesystem/helpers.py +114 -0
- omniload/src/filesystem/readers.py +187 -0
- omniload/src/filters.py +62 -0
- omniload/src/fireflies/__init__.py +151 -0
- omniload/src/fireflies/helpers.py +753 -0
- omniload/src/fluxx/__init__.py +10013 -0
- omniload/src/fluxx/helpers.py +233 -0
- omniload/src/frankfurter/__init__.py +157 -0
- omniload/src/frankfurter/helpers.py +48 -0
- omniload/src/freshdesk/__init__.py +103 -0
- omniload/src/freshdesk/freshdesk_client.py +151 -0
- omniload/src/freshdesk/settings.py +23 -0
- omniload/src/fundraiseup/__init__.py +95 -0
- omniload/src/fundraiseup/client.py +81 -0
- omniload/src/github/__init__.py +202 -0
- omniload/src/github/helpers.py +207 -0
- omniload/src/github/queries.py +129 -0
- omniload/src/github/settings.py +24 -0
- omniload/src/google_ads/__init__.py +198 -0
- omniload/src/google_ads/field.py +17 -0
- omniload/src/google_ads/metrics.py +254 -0
- omniload/src/google_ads/predicates.py +37 -0
- omniload/src/google_ads/reports.py +411 -0
- omniload/src/google_ads/test_google_ads.py +184 -0
- omniload/src/google_analytics/__init__.py +144 -0
- omniload/src/google_analytics/helpers.py +312 -0
- omniload/src/google_sheets/README.md +95 -0
- omniload/src/google_sheets/__init__.py +166 -0
- omniload/src/google_sheets/helpers/__init__.py +15 -0
- omniload/src/google_sheets/helpers/api_calls.py +160 -0
- omniload/src/google_sheets/helpers/data_processing.py +316 -0
- omniload/src/gorgias/__init__.py +595 -0
- omniload/src/gorgias/helpers.py +166 -0
- omniload/src/hostaway/__init__.py +302 -0
- omniload/src/hostaway/client.py +288 -0
- omniload/src/http/__init__.py +38 -0
- omniload/src/http/readers.py +146 -0
- omniload/src/http_client.py +24 -0
- omniload/src/hubspot/__init__.py +800 -0
- omniload/src/hubspot/helpers.py +417 -0
- omniload/src/hubspot/settings.py +329 -0
- omniload/src/indeed/__init__.py +153 -0
- omniload/src/indeed/helpers.py +228 -0
- omniload/src/influxdb/__init__.py +46 -0
- omniload/src/influxdb/client.py +34 -0
- omniload/src/intercom/__init__.py +142 -0
- omniload/src/intercom/helpers.py +674 -0
- omniload/src/intercom/settings.py +279 -0
- omniload/src/isoc_pulse/__init__.py +159 -0
- omniload/src/jira_source/__init__.py +377 -0
- omniload/src/jira_source/helpers.py +510 -0
- omniload/src/jira_source/settings.py +184 -0
- omniload/src/kafka/__init__.py +120 -0
- omniload/src/kafka/helpers.py +241 -0
- omniload/src/kinesis/__init__.py +153 -0
- omniload/src/kinesis/helpers.py +96 -0
- omniload/src/klaviyo/__init__.py +237 -0
- omniload/src/klaviyo/client.py +212 -0
- omniload/src/klaviyo/helpers.py +19 -0
- omniload/src/linear/__init__.py +634 -0
- omniload/src/linear/helpers.py +111 -0
- omniload/src/linkedin_ads/__init__.py +266 -0
- omniload/src/linkedin_ads/dimension_time_enum.py +17 -0
- omniload/src/linkedin_ads/helpers.py +246 -0
- omniload/src/loader.py +69 -0
- omniload/src/mailchimp/__init__.py +126 -0
- omniload/src/mailchimp/helpers.py +226 -0
- omniload/src/mailchimp/settings.py +164 -0
- omniload/src/masking.py +344 -0
- omniload/src/mixpanel/__init__.py +62 -0
- omniload/src/mixpanel/client.py +104 -0
- omniload/src/monday/__init__.py +246 -0
- omniload/src/monday/helpers.py +392 -0
- omniload/src/monday/settings.py +325 -0
- omniload/src/mongodb/__init__.py +281 -0
- omniload/src/mongodb/helpers.py +975 -0
- omniload/src/notion/__init__.py +69 -0
- omniload/src/notion/helpers/__init__.py +14 -0
- omniload/src/notion/helpers/client.py +178 -0
- omniload/src/notion/helpers/database.py +92 -0
- omniload/src/notion/settings.py +17 -0
- omniload/src/partition.py +32 -0
- omniload/src/personio/__init__.py +345 -0
- omniload/src/personio/helpers.py +100 -0
- omniload/src/phantombuster/__init__.py +65 -0
- omniload/src/phantombuster/client.py +87 -0
- omniload/src/pinterest/__init__.py +82 -0
- omniload/src/pipedrive/__init__.py +212 -0
- omniload/src/pipedrive/helpers/__init__.py +37 -0
- omniload/src/pipedrive/helpers/custom_fields_munger.py +116 -0
- omniload/src/pipedrive/helpers/pages.py +129 -0
- omniload/src/pipedrive/settings.py +41 -0
- omniload/src/pipedrive/typing.py +17 -0
- omniload/src/plusvibeai/__init__.py +335 -0
- omniload/src/plusvibeai/helpers.py +544 -0
- omniload/src/plusvibeai/settings.py +252 -0
- omniload/src/primer/__init__.py +45 -0
- omniload/src/primer/helpers.py +79 -0
- omniload/src/quickbooks/__init__.py +117 -0
- omniload/src/reddit_ads/__init__.py +183 -0
- omniload/src/reddit_ads/helpers.py +232 -0
- omniload/src/resource.py +40 -0
- omniload/src/revenuecat/__init__.py +83 -0
- omniload/src/revenuecat/helpers.py +237 -0
- omniload/src/salesforce/__init__.py +170 -0
- omniload/src/salesforce/helpers.py +78 -0
- omniload/src/shopify/__init__.py +1953 -0
- omniload/src/shopify/exceptions.py +17 -0
- omniload/src/shopify/helpers.py +202 -0
- omniload/src/shopify/settings.py +19 -0
- omniload/src/slack/__init__.py +290 -0
- omniload/src/slack/helpers.py +218 -0
- omniload/src/slack/settings.py +36 -0
- omniload/src/smartsheets/__init__.py +82 -0
- omniload/src/snapchat_ads/__init__.py +455 -0
- omniload/src/snapchat_ads/client.py +72 -0
- omniload/src/snapchat_ads/helpers.py +630 -0
- omniload/src/snapchat_ads/settings.py +130 -0
- omniload/src/socrata_source/__init__.py +83 -0
- omniload/src/socrata_source/helpers.py +85 -0
- omniload/src/socrata_source/settings.py +8 -0
- omniload/src/solidgate/__init__.py +219 -0
- omniload/src/solidgate/helpers.py +154 -0
- omniload/src/sources.py +5408 -0
- omniload/src/sql_database/__init__.py +0 -0
- omniload/src/sql_database/callbacks.py +66 -0
- omniload/src/stripe_analytics/__init__.py +183 -0
- omniload/src/stripe_analytics/helpers.py +386 -0
- omniload/src/stripe_analytics/settings.py +80 -0
- omniload/src/table_definition.py +15 -0
- omniload/src/testdata/fakebqcredentials.json +14 -0
- omniload/src/tiktok_ads/__init__.py +150 -0
- omniload/src/tiktok_ads/tiktok_helpers.py +130 -0
- omniload/src/time.py +11 -0
- omniload/src/trustpilot/__init__.py +48 -0
- omniload/src/trustpilot/client.py +48 -0
- omniload/src/version.py +6 -0
- omniload/src/wise/__init__.py +68 -0
- omniload/src/wise/client.py +63 -0
- omniload/src/zendesk/__init__.py +480 -0
- omniload/src/zendesk/helpers/__init__.py +39 -0
- omniload/src/zendesk/helpers/api_helpers.py +119 -0
- omniload/src/zendesk/helpers/credentials.py +68 -0
- omniload/src/zendesk/helpers/talk_api.py +132 -0
- omniload/src/zendesk/settings.py +71 -0
- omniload/src/zoom/__init__.py +99 -0
- omniload/src/zoom/helpers.py +102 -0
- omniload/testdata/.gitignore +2 -0
- omniload/testdata/create_replace.csv +21 -0
- omniload/testdata/delete_insert_expected.csv +6 -0
- omniload/testdata/delete_insert_part1.csv +5 -0
- omniload/testdata/delete_insert_part2.csv +6 -0
- omniload/testdata/merge_expected.csv +5 -0
- omniload/testdata/merge_part1.csv +4 -0
- omniload/testdata/merge_part2.csv +5 -0
- omniload/tests/unit/test_smartsheets.py +133 -0
- omniload-0.0.0.dev0.dist-info/METADATA +439 -0
- omniload-0.0.0.dev0.dist-info/RECORD +218 -0
- omniload-0.0.0.dev0.dist-info/WHEEL +4 -0
- omniload-0.0.0.dev0.dist-info/entry_points.txt +2 -0
- omniload-0.0.0.dev0.dist-info/licenses/LICENSE.Apache-2.0 +201 -0
- omniload-0.0.0.dev0.dist-info/licenses/LICENSE.md +21 -0
- 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)
|