soda-bigquery 4.0.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- soda_bigquery-4.0.5/PKG-INFO +6 -0
- soda_bigquery-4.0.5/setup.cfg +4 -0
- soda_bigquery-4.0.5/setup.py +24 -0
- soda_bigquery-4.0.5/src/soda_bigquery/common/data_sources/bigquery_data_source.py +245 -0
- soda_bigquery-4.0.5/src/soda_bigquery/common/data_sources/bigquery_data_source_connection.py +152 -0
- soda_bigquery-4.0.5/src/soda_bigquery/test_helpers/bigquery_data_source_test_helper.py +35 -0
- soda_bigquery-4.0.5/src/soda_bigquery.egg-info/PKG-INFO +6 -0
- soda_bigquery-4.0.5/src/soda_bigquery.egg-info/SOURCES.txt +10 -0
- soda_bigquery-4.0.5/src/soda_bigquery.egg-info/dependency_links.txt +1 -0
- soda_bigquery-4.0.5/src/soda_bigquery.egg-info/entry_points.txt +2 -0
- soda_bigquery-4.0.5/src/soda_bigquery.egg-info/requires.txt +2 -0
- soda_bigquery-4.0.5/src/soda_bigquery.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
from setuptools import setup
|
|
4
|
+
|
|
5
|
+
package_name = "soda-bigquery"
|
|
6
|
+
package_version = "4.0.5"
|
|
7
|
+
description = "Soda BigQuery V4"
|
|
8
|
+
|
|
9
|
+
requires = [
|
|
10
|
+
f"soda-core=={package_version}",
|
|
11
|
+
"google-cloud-bigquery>=2.25.0",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
setup(
|
|
15
|
+
name=package_name,
|
|
16
|
+
version=package_version,
|
|
17
|
+
install_requires=requires,
|
|
18
|
+
package_dir={"": "src"},
|
|
19
|
+
entry_points={
|
|
20
|
+
"soda.plugins.data_source.bigquery": [
|
|
21
|
+
"BigqueryDataSourceImpl = soda_bigquery.common.data_sources.bigquery_data_source:BigQueryDataSourceImpl",
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
)
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from soda_bigquery.common.data_sources.bigquery_data_source_connection import (
|
|
6
|
+
BigQueryDataSource as BigQueryDataSourceModel,
|
|
7
|
+
)
|
|
8
|
+
from soda_bigquery.common.data_sources.bigquery_data_source_connection import (
|
|
9
|
+
BigQueryDataSourceConnection,
|
|
10
|
+
)
|
|
11
|
+
from soda_core.common.data_source_connection import DataSourceConnection
|
|
12
|
+
from soda_core.common.data_source_impl import DataSourceImpl
|
|
13
|
+
from soda_core.common.logging_constants import soda_logger
|
|
14
|
+
from soda_core.common.metadata_types import DataSourceNamespace, SodaDataTypeName
|
|
15
|
+
from soda_core.common.sql_ast import (
|
|
16
|
+
COLUMN,
|
|
17
|
+
CONCAT_WS,
|
|
18
|
+
COUNT,
|
|
19
|
+
DISTINCT,
|
|
20
|
+
LITERAL,
|
|
21
|
+
REGEX_LIKE,
|
|
22
|
+
STRING_HASH,
|
|
23
|
+
TUPLE,
|
|
24
|
+
VALUES,
|
|
25
|
+
WITH,
|
|
26
|
+
)
|
|
27
|
+
from soda_core.common.sql_dialect import SqlDialect
|
|
28
|
+
from soda_core.common.statements.metadata_tables_query import MetadataTablesQuery
|
|
29
|
+
|
|
30
|
+
logger: logging.Logger = soda_logger
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class BigQueryDataSourceNamespace(DataSourceNamespace):
|
|
35
|
+
project_id: str
|
|
36
|
+
dataset: str
|
|
37
|
+
location: Optional[str] = None
|
|
38
|
+
|
|
39
|
+
def get_namespace_elements(self) -> list[str]:
|
|
40
|
+
if self.location:
|
|
41
|
+
return [self.location, self.project_id]
|
|
42
|
+
else:
|
|
43
|
+
return [self.project_id, self.dataset]
|
|
44
|
+
|
|
45
|
+
def get_database_for_metadata_query(self) -> str | None:
|
|
46
|
+
return self.project_id
|
|
47
|
+
|
|
48
|
+
def get_schema_for_metadata_query(self) -> str:
|
|
49
|
+
return self.dataset
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class BigQueryDataSourceImpl(DataSourceImpl, model_class=BigQueryDataSourceModel):
|
|
53
|
+
def __init__(self, data_source_model: BigQueryDataSourceModel, connection: Optional[DataSourceConnection] = None):
|
|
54
|
+
super().__init__(data_source_model=data_source_model, connection=connection)
|
|
55
|
+
self.cached_location = None
|
|
56
|
+
|
|
57
|
+
def _create_sql_dialect(self) -> SqlDialect:
|
|
58
|
+
return BigQuerySqlDialect(data_source_impl=self)
|
|
59
|
+
|
|
60
|
+
def _create_data_source_connection(self) -> DataSourceConnection:
|
|
61
|
+
return BigQueryDataSourceConnection(
|
|
62
|
+
name=self.data_source_model.name, connection_properties=self.data_source_model.connection_properties
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def get_location(self) -> str:
|
|
66
|
+
if self.cached_location is not None:
|
|
67
|
+
location = self.cached_location
|
|
68
|
+
elif self.data_source_model.connection_properties.location is not None:
|
|
69
|
+
location = self.data_source_model.connection_properties.location
|
|
70
|
+
else:
|
|
71
|
+
result = self.execute_query("SELECT @@location")
|
|
72
|
+
location = result.rows[0][0]
|
|
73
|
+
logger.info(f"Detected BigQuery location: {location}")
|
|
74
|
+
self.cached_location = location
|
|
75
|
+
return location
|
|
76
|
+
|
|
77
|
+
def create_metadata_tables_query(self) -> MetadataTablesQuery:
|
|
78
|
+
super_metadata_tables_query = MetadataTablesQuery(
|
|
79
|
+
sql_dialect=self.sql_dialect,
|
|
80
|
+
data_source_connection=self.data_source_connection,
|
|
81
|
+
prefixes=[f"region-{self.get_location()}"],
|
|
82
|
+
)
|
|
83
|
+
return super_metadata_tables_query
|
|
84
|
+
|
|
85
|
+
def build_columns_metadata_query_str(self, dataset_prefixes: list[str], dataset_name: str) -> str:
|
|
86
|
+
table_namespace: DataSourceNamespace = BigQueryDataSourceNamespace(
|
|
87
|
+
project_id=dataset_prefixes[0], dataset=dataset_prefixes[1]
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# BigQuery must be able to override to get the location
|
|
91
|
+
return self.sql_dialect.build_columns_metadata_query_str(
|
|
92
|
+
table_namespace=table_namespace, table_name=dataset_name
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def _build_table_namespace_for_schema_query(self, prefixes: list[str]) -> tuple[DataSourceNamespace, str]:
|
|
96
|
+
table_namespace: DataSourceNamespace = BigQueryDataSourceNamespace(
|
|
97
|
+
project_id=prefixes[0],
|
|
98
|
+
dataset=None, # We only need the project id to query the schemas, as it's always in the `INFORMATION_SCHEMA`
|
|
99
|
+
)
|
|
100
|
+
schema_name = self.extract_schema_from_prefix(prefixes)
|
|
101
|
+
if schema_name is None:
|
|
102
|
+
raise ValueError(f"Cannot determine schema name from prefixes: {prefixes}")
|
|
103
|
+
|
|
104
|
+
return table_namespace, schema_name
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class BigQuerySqlDialect(SqlDialect):
|
|
108
|
+
DEFAULT_QUOTE_CHAR = "`"
|
|
109
|
+
|
|
110
|
+
SODA_DATA_TYPE_SYNONYMS = (
|
|
111
|
+
(SodaDataTypeName.TEXT, SodaDataTypeName.VARCHAR, SodaDataTypeName.CHAR),
|
|
112
|
+
(SodaDataTypeName.INTEGER, SodaDataTypeName.BIGINT, SodaDataTypeName.SMALLINT),
|
|
113
|
+
(SodaDataTypeName.NUMERIC, SodaDataTypeName.DECIMAL),
|
|
114
|
+
(SodaDataTypeName.TIMESTAMP, SodaDataTypeName.TIMESTAMP_TZ), # Bigquery does not support timezones
|
|
115
|
+
(SodaDataTypeName.FLOAT, SodaDataTypeName.DOUBLE),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def get_data_source_data_type_name_by_soda_data_type_names(self) -> dict[str, str]:
|
|
119
|
+
return {
|
|
120
|
+
SodaDataTypeName.CHAR: "string", # BigQuery only has STRING
|
|
121
|
+
SodaDataTypeName.VARCHAR: "string",
|
|
122
|
+
SodaDataTypeName.TEXT: "string", # alias for varchar
|
|
123
|
+
SodaDataTypeName.SMALLINT: "int64", # all integers → INT64
|
|
124
|
+
SodaDataTypeName.INTEGER: "int64",
|
|
125
|
+
SodaDataTypeName.BIGINT: "int64",
|
|
126
|
+
SodaDataTypeName.DECIMAL: "numeric", # NUMERIC (fixed precision: 38 digits, 9 decimals)
|
|
127
|
+
SodaDataTypeName.NUMERIC: "numeric",
|
|
128
|
+
SodaDataTypeName.FLOAT: "float64", # FLOAT & DOUBLE → FLOAT64
|
|
129
|
+
SodaDataTypeName.DOUBLE: "float64",
|
|
130
|
+
SodaDataTypeName.TIMESTAMP: "timestamp", # UTC-based
|
|
131
|
+
SodaDataTypeName.TIMESTAMP_TZ: "timestamp", # still just TIMESTAMP in BigQuery
|
|
132
|
+
SodaDataTypeName.DATE: "date",
|
|
133
|
+
SodaDataTypeName.TIME: "time",
|
|
134
|
+
SodaDataTypeName.BOOLEAN: "bool",
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
def get_soda_data_type_name_by_data_source_data_type_names(self) -> dict[str, SodaDataTypeName]:
|
|
138
|
+
return {
|
|
139
|
+
"string": SodaDataTypeName.VARCHAR,
|
|
140
|
+
"smallint": SodaDataTypeName.SMALLINT,
|
|
141
|
+
"int64": SodaDataTypeName.BIGINT,
|
|
142
|
+
"numeric": SodaDataTypeName.NUMERIC,
|
|
143
|
+
"float64": SodaDataTypeName.DOUBLE,
|
|
144
|
+
"double precision": SodaDataTypeName.DOUBLE,
|
|
145
|
+
"timestamp": SodaDataTypeName.TIMESTAMP,
|
|
146
|
+
"date": SodaDataTypeName.DATE,
|
|
147
|
+
"time": SodaDataTypeName.TIME,
|
|
148
|
+
"bool": SodaDataTypeName.BOOLEAN,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
def _get_data_type_name_synonyms(self) -> list[list[str]]:
|
|
152
|
+
return [
|
|
153
|
+
["int64", "integer"],
|
|
154
|
+
["bool", "boolean"],
|
|
155
|
+
["numeric", "decimal"],
|
|
156
|
+
["bignumeric", "bigdecimal"],
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
def information_schema_namespace_elements(self, data_source_namespace: BigQueryDataSourceNamespace) -> list[str]:
|
|
160
|
+
return [data_source_namespace.project_id, data_source_namespace.dataset, "INFORMATION_SCHEMA"]
|
|
161
|
+
|
|
162
|
+
def default_casify(self, identifier: str) -> str:
|
|
163
|
+
return identifier.upper()
|
|
164
|
+
|
|
165
|
+
def _build_tuple_sql(self, tuple: TUPLE) -> str:
|
|
166
|
+
if tuple.check_context(COUNT) and tuple.check_context(DISTINCT):
|
|
167
|
+
return self._build_tuple_sql_in_distinct(tuple)
|
|
168
|
+
return f"{super()._build_tuple_sql(tuple)}"
|
|
169
|
+
|
|
170
|
+
def _build_tuple_sql_in_distinct(self, tuple: TUPLE) -> str:
|
|
171
|
+
return f"TO_JSON_STRING(STRUCT({super()._build_tuple_sql(tuple)}))"
|
|
172
|
+
|
|
173
|
+
def _build_regex_like_sql(self, matches: REGEX_LIKE) -> str:
|
|
174
|
+
expression: str = self.build_expression_sql(matches.expression)
|
|
175
|
+
return f"REGEXP_CONTAINS({expression}, r'{matches.regex_pattern}')"
|
|
176
|
+
|
|
177
|
+
def supports_data_type_character_maximum_length(self) -> bool:
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
def supports_data_type_numeric_precision(self) -> bool:
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
def supports_cte_alias_columns(self) -> bool:
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
def supports_data_type_numeric_scale(self) -> bool:
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
def supports_data_type_datetime_precision(self) -> bool:
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
def sql_expr_timestamp_literal(self, datetime_in_iso8601: str) -> str:
|
|
193
|
+
return f"timestamp('{datetime_in_iso8601}')"
|
|
194
|
+
|
|
195
|
+
def sql_expr_timestamp_truncate_day(self, timestamp_literal: str) -> str:
|
|
196
|
+
return f"date_trunc(timestamp({timestamp_literal}), day)"
|
|
197
|
+
|
|
198
|
+
def sql_expr_timestamp_add_day(self, timestamp_literal: str) -> str:
|
|
199
|
+
return f"{timestamp_literal} + interval 1 day"
|
|
200
|
+
|
|
201
|
+
def literal_string(self, value: str) -> Optional[str]:
|
|
202
|
+
if value is None:
|
|
203
|
+
return None
|
|
204
|
+
return "'''" + self.escape_string(value) + "'''"
|
|
205
|
+
|
|
206
|
+
def build_cte_values_sql(self, values: VALUES, alias_columns: list[COLUMN] | None) -> str:
|
|
207
|
+
# The first select row should have column aliases
|
|
208
|
+
# Remaining rows don't need aliases
|
|
209
|
+
def build_literal_with_alias(literal, alias: COLUMN | None) -> str:
|
|
210
|
+
return f"{self.literal(literal)} AS {self.quote_column(alias.name)}"
|
|
211
|
+
|
|
212
|
+
literal_rows: list[str] = []
|
|
213
|
+
for tuple in values.values:
|
|
214
|
+
if alias_columns:
|
|
215
|
+
literal_sqls: list[str] = []
|
|
216
|
+
for i in range(len(tuple.expressions)):
|
|
217
|
+
literal: LITERAL = tuple.expressions[i]
|
|
218
|
+
alias: COLUMN = alias_columns[i]
|
|
219
|
+
literal_sqls.append(build_literal_with_alias(literal=literal, alias=alias))
|
|
220
|
+
literal_rows.append(", ".join(literal_sql for literal_sql in literal_sqls))
|
|
221
|
+
alias_columns = None
|
|
222
|
+
else:
|
|
223
|
+
literal_rows.append(", ".join(self.build_expression_sql(e) for e in tuple.expressions))
|
|
224
|
+
|
|
225
|
+
select_rows: list[str] = [f"SELECT {literal_row}" for literal_row in literal_rows]
|
|
226
|
+
|
|
227
|
+
return "\nUNION ALL ".join([select_row for select_row in select_rows])
|
|
228
|
+
|
|
229
|
+
def _build_cte_with_sql_line(self, with_element: WITH) -> str:
|
|
230
|
+
return f"WITH {self.quote_default(with_element.alias)} AS ("
|
|
231
|
+
|
|
232
|
+
def get_max_sql_statement_length(self) -> int:
|
|
233
|
+
# This is a VERY conservative limit. We can probably get away with more
|
|
234
|
+
# However, BigQuery started complaining that the statement was "too complicated" with a length of 1MB.
|
|
235
|
+
return 200 * 1024 # 200KB
|
|
236
|
+
|
|
237
|
+
def get_preferred_number_of_rows_for_insert(self) -> int:
|
|
238
|
+
return 500
|
|
239
|
+
|
|
240
|
+
def _build_concat_ws_sql(self, concat_ws: CONCAT_WS) -> str:
|
|
241
|
+
elements: str = f", '{concat_ws.separator}', ".join(self.build_expression_sql(e) for e in concat_ws.expressions)
|
|
242
|
+
return f"CONCAT({elements})"
|
|
243
|
+
|
|
244
|
+
def _build_string_hash_sql(self, string_hash: STRING_HASH) -> str:
|
|
245
|
+
return f"to_hex({super()._build_string_hash_sql(string_hash)})"
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from abc import ABC
|
|
6
|
+
from typing import Literal, Optional, Union
|
|
7
|
+
|
|
8
|
+
from google.api_core.client_info import ClientInfo
|
|
9
|
+
from google.auth import default, impersonated_credentials
|
|
10
|
+
from google.cloud import bigquery
|
|
11
|
+
from google.cloud.bigquery import dbapi
|
|
12
|
+
from google.cloud.bigquery.table import Row
|
|
13
|
+
from google.oauth2.service_account import Credentials
|
|
14
|
+
from pydantic import Field, SecretStr
|
|
15
|
+
from soda_core.common.data_source_connection import DataSourceConnection
|
|
16
|
+
from soda_core.common.logging_constants import soda_logger
|
|
17
|
+
from soda_core.model.data_source.data_source import DataSourceBase
|
|
18
|
+
from soda_core.model.data_source.data_source_connection_properties import (
|
|
19
|
+
DataSourceConnectionProperties,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
logger: logging.Logger = soda_logger
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
CONTEXT_AUTHENTICATION_DESCRIPTION = "Use context authentication"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BigQueryConnectionProperties(DataSourceConnectionProperties, ABC):
|
|
29
|
+
project_id: Optional[str] = Field(None, description="BigQuery project ID")
|
|
30
|
+
storage_project_id: Optional[str] = Field(None, description="BigQuery storage project ID")
|
|
31
|
+
location: Optional[str] = Field(None, description="BigQuery location")
|
|
32
|
+
client_options: Optional[dict] = Field(None, description="Client options")
|
|
33
|
+
labels: Optional[dict] = Field({}, description="Labels")
|
|
34
|
+
auth_scopes: Optional[list[str]] = Field(
|
|
35
|
+
[
|
|
36
|
+
"https://www.googleapis.com/auth/bigquery",
|
|
37
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
38
|
+
"https://www.googleapis.com/auth/drive",
|
|
39
|
+
],
|
|
40
|
+
description="Authentication scopes",
|
|
41
|
+
)
|
|
42
|
+
impersonation_account: Optional[str] = Field(None, description="Impersonation account")
|
|
43
|
+
delegates: Optional[list[str]] = Field(None, description="Delegates")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class BigQueryJSONStringAuth(BigQueryConnectionProperties):
|
|
47
|
+
"""BigQuery authentication using JSON string"""
|
|
48
|
+
|
|
49
|
+
use_context_auth: Optional[Literal[False]] = Field(False, description=CONTEXT_AUTHENTICATION_DESCRIPTION)
|
|
50
|
+
account_info_json: SecretStr = Field(..., description="Service account JSON as string", min_length=1)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class BigQueryJSONFileAuth(BigQueryConnectionProperties):
|
|
54
|
+
"""BigQuery authentication using JSON file path"""
|
|
55
|
+
|
|
56
|
+
use_context_auth: Optional[Literal[False]] = Field(False, description=CONTEXT_AUTHENTICATION_DESCRIPTION)
|
|
57
|
+
account_info_json_path: str = Field(..., description="Path to service account JSON file", min_length=1)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class BigQueryContextAuth(BigQueryConnectionProperties):
|
|
61
|
+
"""BigQuery authentication using context.
|
|
62
|
+
|
|
63
|
+
If use_context_auth is True, then application default credentials will be used.
|
|
64
|
+
The user may optionally provide JSON credentials; they will be ignored.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
use_context_auth: Literal[True] = Field(description=CONTEXT_AUTHENTICATION_DESCRIPTION)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class BigQueryDataSource(DataSourceBase, ABC):
|
|
71
|
+
type: Literal["bigquery"] = Field("bigquery")
|
|
72
|
+
|
|
73
|
+
connection_properties: Union[
|
|
74
|
+
BigQueryJSONStringAuth,
|
|
75
|
+
BigQueryJSONFileAuth,
|
|
76
|
+
BigQueryContextAuth,
|
|
77
|
+
] = Field(..., alias="connection", description="BigQuery connection configuration")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class BigQueryDataSourceConnection(DataSourceConnection):
|
|
81
|
+
def __init__(self, name: str, connection_properties: DataSourceConnectionProperties):
|
|
82
|
+
super().__init__(name, connection_properties)
|
|
83
|
+
|
|
84
|
+
def _load_project_id_and_credentials(self, config: BigQueryConnectionProperties):
|
|
85
|
+
if isinstance(config, BigQueryContextAuth):
|
|
86
|
+
logger.info("Using application default credentials.")
|
|
87
|
+
self.credentials, self.project_id = default()
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
if isinstance(config, BigQueryJSONFileAuth):
|
|
91
|
+
account_info_dict = json.load(open(config.account_info_json_path))
|
|
92
|
+
elif isinstance(config, BigQueryJSONStringAuth):
|
|
93
|
+
account_info_dict = json.loads(config.account_info_json.get_secret_value())
|
|
94
|
+
self.credentials = Credentials.from_service_account_info(
|
|
95
|
+
account_info_dict,
|
|
96
|
+
scopes=config.auth_scopes,
|
|
97
|
+
)
|
|
98
|
+
self.project_id = account_info_dict.get("project_id")
|
|
99
|
+
|
|
100
|
+
def _load_optional_impersonated_credentials(self, config: BigQueryConnectionProperties):
|
|
101
|
+
if config.impersonation_account:
|
|
102
|
+
logger.info("Using impersonation of Service Account.")
|
|
103
|
+
if config.delegates:
|
|
104
|
+
logger.info("Using Service Account delegates.")
|
|
105
|
+
delegates = config.delegates
|
|
106
|
+
else:
|
|
107
|
+
delegates = None
|
|
108
|
+
self.credentials = impersonated_credentials.Credentials(
|
|
109
|
+
source_credentials=self.credentials,
|
|
110
|
+
target_principal=str(config.impersonation_account),
|
|
111
|
+
target_scopes=config.auth_scopes,
|
|
112
|
+
delegates=delegates,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def _apply_optional_params(self, config: BigQueryConnectionProperties):
|
|
116
|
+
# Users can optionally overwrite in the connection properties
|
|
117
|
+
self.project_id = config.project_id if config.project_id else self.project_id
|
|
118
|
+
self.location = config.location
|
|
119
|
+
self.client_options = config.client_options
|
|
120
|
+
|
|
121
|
+
# Storage project ID is currently not used, because the project is configured via the DQN in the data contract.
|
|
122
|
+
# When we implement discovery, we'll need to use this value.
|
|
123
|
+
self.storage_project_id = config.storage_project_id if config.storage_project_id else self.project_id
|
|
124
|
+
|
|
125
|
+
self.labels = config.labels
|
|
126
|
+
|
|
127
|
+
def _create_connection(
|
|
128
|
+
self,
|
|
129
|
+
config: BigQueryConnectionProperties,
|
|
130
|
+
):
|
|
131
|
+
self._load_project_id_and_credentials(config)
|
|
132
|
+
self._load_optional_impersonated_credentials(config)
|
|
133
|
+
self._apply_optional_params(config)
|
|
134
|
+
|
|
135
|
+
client_info = ClientInfo(
|
|
136
|
+
user_agent="soda-core",
|
|
137
|
+
)
|
|
138
|
+
default_query_job_config = bigquery.QueryJobConfig(labels=self.labels)
|
|
139
|
+
self.client = bigquery.Client(
|
|
140
|
+
project=self.project_id,
|
|
141
|
+
credentials=self.credentials,
|
|
142
|
+
default_query_job_config=default_query_job_config,
|
|
143
|
+
client_info=client_info,
|
|
144
|
+
location=config.location,
|
|
145
|
+
client_options=self.client_options,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return dbapi.Connection(self.client)
|
|
149
|
+
|
|
150
|
+
def format_rows(self, rows: list[Row]) -> list[tuple]:
|
|
151
|
+
formatted_rows = [tuple(r.values()) for r in rows]
|
|
152
|
+
return formatted_rows
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from helpers.data_source_test_helper import DataSourceTestHelper
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BigQueryDataSourceTestHelper(DataSourceTestHelper):
|
|
10
|
+
def _create_database_name(self) -> str | None:
|
|
11
|
+
# Parse the dataset name from the account info json
|
|
12
|
+
account_info_json = json.loads(os.getenv("BIGQUERY_ACCOUNT_INFO_JSON", "{}"))
|
|
13
|
+
database_name = account_info_json.get("project_id", "soda-testing-dataset")
|
|
14
|
+
return database_name
|
|
15
|
+
|
|
16
|
+
def _create_data_source_yaml_str(self) -> str:
|
|
17
|
+
"""
|
|
18
|
+
Called in _create_data_source_impl to initialized self.data_source_impl
|
|
19
|
+
self.database_name and self.schema_name are available if appropriate for the data source type
|
|
20
|
+
"""
|
|
21
|
+
return f"""
|
|
22
|
+
type: bigquery
|
|
23
|
+
name: {self.name}
|
|
24
|
+
connection:
|
|
25
|
+
account_info_json: '{os.getenv("BIGQUERY_ACCOUNT_INFO_JSON", "")}'
|
|
26
|
+
|
|
27
|
+
"""
|
|
28
|
+
# location: '{os.getenv("BIGQUERY_LOCATION", "US")}'
|
|
29
|
+
|
|
30
|
+
def _get_contract_data_type_dict(self) -> dict[str, str]:
|
|
31
|
+
"""
|
|
32
|
+
DataSourceTestHelpers can override this method as an easy way
|
|
33
|
+
to customize the get_schema_check_sql_type behavior
|
|
34
|
+
"""
|
|
35
|
+
return self._get_create_table_sql_type_dict()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
setup.py
|
|
2
|
+
src/soda_bigquery.egg-info/PKG-INFO
|
|
3
|
+
src/soda_bigquery.egg-info/SOURCES.txt
|
|
4
|
+
src/soda_bigquery.egg-info/dependency_links.txt
|
|
5
|
+
src/soda_bigquery.egg-info/entry_points.txt
|
|
6
|
+
src/soda_bigquery.egg-info/requires.txt
|
|
7
|
+
src/soda_bigquery.egg-info/top_level.txt
|
|
8
|
+
src/soda_bigquery/common/data_sources/bigquery_data_source.py
|
|
9
|
+
src/soda_bigquery/common/data_sources/bigquery_data_source_connection.py
|
|
10
|
+
src/soda_bigquery/test_helpers/bigquery_data_source_test_helper.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
soda_bigquery
|