interloper-google-cloud 0.110.2__tar.gz → 0.112.0__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.
- {interloper_google_cloud-0.110.2 → interloper_google_cloud-0.112.0}/PKG-INFO +1 -1
- {interloper_google_cloud-0.110.2 → interloper_google_cloud-0.112.0}/pyproject.toml +1 -1
- {interloper_google_cloud-0.110.2 → interloper_google_cloud-0.112.0}/pyproject.toml.orig +1 -1
- {interloper_google_cloud-0.110.2 → interloper_google_cloud-0.112.0}/src/interloper_google_cloud/bigquery/destination.py +68 -10
- {interloper_google_cloud-0.110.2 → interloper_google_cloud-0.112.0}/README.md +0 -0
- {interloper_google_cloud-0.110.2 → interloper_google_cloud-0.112.0}/src/interloper_google_cloud/__init__.py +0 -0
- {interloper_google_cloud-0.110.2 → interloper_google_cloud-0.112.0}/src/interloper_google_cloud/bigquery/__init__.py +0 -0
- {interloper_google_cloud-0.110.2 → interloper_google_cloud-0.112.0}/src/interloper_google_cloud/bigquery/types.py +0 -0
- {interloper_google_cloud-0.110.2 → interloper_google_cloud-0.112.0}/src/interloper_google_cloud/connection.py +0 -0
- {interloper_google_cloud-0.110.2 → interloper_google_cloud-0.112.0}/src/interloper_google_cloud/gcs/__init__.py +0 -0
- {interloper_google_cloud-0.110.2 → interloper_google_cloud-0.112.0}/src/interloper_google_cloud/gcs/destination.py +0 -0
- {interloper_google_cloud-0.110.2 → interloper_google_cloud-0.112.0}/src/interloper_google_cloud/google_sheets/__init__.py +0 -0
- {interloper_google_cloud-0.110.2 → interloper_google_cloud-0.112.0}/src/interloper_google_cloud/google_sheets/destination.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: interloper-google-cloud
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.112.0
|
|
4
4
|
Summary: Interloper Google Cloud integration: BigQuery, Cloud Storage and Google Sheets destinations
|
|
5
5
|
Author: Guillaume Onfroy
|
|
6
6
|
Author-email: Guillaume Onfroy <guillaume@digitlcloud.com>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# ###############
|
|
4
4
|
[project]
|
|
5
5
|
name = "interloper-google-cloud"
|
|
6
|
-
version = "0.
|
|
6
|
+
version = "0.112.0"
|
|
7
7
|
description = "Interloper Google Cloud integration: BigQuery, Cloud Storage and Google Sheets destinations"
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
authors = [{ name = "Guillaume Onfroy", email = "guillaume@digitlcloud.com" }]
|
|
@@ -8,14 +8,14 @@ import json
|
|
|
8
8
|
import warnings
|
|
9
9
|
from collections.abc import Sequence
|
|
10
10
|
from functools import cached_property
|
|
11
|
-
from typing import Any
|
|
11
|
+
from typing import Any, ClassVar
|
|
12
12
|
|
|
13
13
|
import google.auth
|
|
14
14
|
import pandas as pd
|
|
15
15
|
from google.cloud import bigquery
|
|
16
16
|
from google.cloud.exceptions import Conflict, NotFound
|
|
17
17
|
from google.oauth2 import service_account
|
|
18
|
-
from interloper.destination import IOContext, destination
|
|
18
|
+
from interloper.destination import Destination, IOContext, destination
|
|
19
19
|
from interloper.destination.database import DatabaseDestination, PartitionFilter
|
|
20
20
|
from interloper.errors import ConfigError, DataNotFoundError
|
|
21
21
|
from interloper.partitioning import PartitionConfig, TimeGranularity, TimePartitionConfig
|
|
@@ -35,6 +35,8 @@ from interloper_google_cloud.connection import GoogleCloudConnection
|
|
|
35
35
|
class BigQueryDestination(DatabaseDestination):
|
|
36
36
|
"""BigQuery destination."""
|
|
37
37
|
|
|
38
|
+
dialect: ClassVar[str | None] = "bigquery"
|
|
39
|
+
|
|
38
40
|
connection: GoogleCloudConnection
|
|
39
41
|
|
|
40
42
|
project: str = FetchField(
|
|
@@ -108,10 +110,9 @@ class BigQueryDestination(DatabaseDestination):
|
|
|
108
110
|
dataset: The BigQuery dataset, or ``None`` for the destination's default.
|
|
109
111
|
|
|
110
112
|
Returns:
|
|
111
|
-
``project.dataset.table`` string.
|
|
113
|
+
``project.dataset.table`` string, as the API wants it.
|
|
112
114
|
"""
|
|
113
|
-
|
|
114
|
-
return f"{self.project}.{ds}.{table}"
|
|
115
|
+
return ".".join(self.table_parts(table, dataset))
|
|
115
116
|
|
|
116
117
|
def _get_table(self, table: str, dataset: str | None) -> bigquery.Table | None:
|
|
117
118
|
"""Fetch a BigQuery table.
|
|
@@ -219,6 +220,63 @@ class BigQueryDestination(DatabaseDestination):
|
|
|
219
220
|
except Conflict:
|
|
220
221
|
pass # Created by a concurrent asset — already exists
|
|
221
222
|
|
|
223
|
+
# -- Views -----------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
def table_parts(self, table: str, dataset: str | None) -> tuple[str, str, str]:
|
|
226
|
+
"""The project, dataset and table a reference names.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
table: Table name.
|
|
230
|
+
dataset: The BigQuery dataset, or ``None`` for the destination's default.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
``(project, dataset, table)``.
|
|
234
|
+
"""
|
|
235
|
+
return (self.project, self._resolve_dataset(dataset), table)
|
|
236
|
+
|
|
237
|
+
def qualified_name(self, table: str, dataset: str | None) -> str:
|
|
238
|
+
"""The whole path in one pair of backticks, the form a wildcard table needs.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
table: Table name.
|
|
242
|
+
dataset: The BigQuery dataset, or ``None`` for the destination's default.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
The path ``project.dataset.table`` wrapped in backticks.
|
|
246
|
+
"""
|
|
247
|
+
return f"`{self._table_ref(table, dataset)}`"
|
|
248
|
+
|
|
249
|
+
def replace_view(self, table: str, dataset: str | None, sql: str) -> None:
|
|
250
|
+
"""Create or replace a view, creating its dataset first.
|
|
251
|
+
|
|
252
|
+
A view whose query is already ``sql`` is left alone: a backfill
|
|
253
|
+
recreates every view once per partition, and BigQuery rate-limits
|
|
254
|
+
metadata updates.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
table: The view's name.
|
|
258
|
+
dataset: The BigQuery dataset, or ``None`` for the destination's default.
|
|
259
|
+
sql: The view's query.
|
|
260
|
+
"""
|
|
261
|
+
existing = self._get_table(table, dataset)
|
|
262
|
+
if existing is not None and existing.view_query == sql:
|
|
263
|
+
return
|
|
264
|
+
self._ensure_dataset(dataset)
|
|
265
|
+
self.client.query(f"CREATE OR REPLACE VIEW {self.qualified_name(table, dataset)} AS {sql}").result()
|
|
266
|
+
|
|
267
|
+
def reaches(self, other: Destination) -> bool:
|
|
268
|
+
"""Whether *other* is a BigQuery destination in the same location.
|
|
269
|
+
|
|
270
|
+
BigQuery queries across projects but not across locations.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
other: The destination holding an upstream's table.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
True for BigQuery in the same location.
|
|
277
|
+
"""
|
|
278
|
+
return isinstance(other, BigQueryDestination) and other.location == self.location
|
|
279
|
+
|
|
222
280
|
# -- DatabaseDestination hooks ---------------------------------------------
|
|
223
281
|
|
|
224
282
|
def insert(self, table: str, dataset: str | None, data: Any, context: IOContext) -> None:
|
|
@@ -466,8 +524,7 @@ def _time_partitioning(
|
|
|
466
524
|
+ (" (hourly partitioning needs DATETIME or TIMESTAMP)" if field.field_type == "DATE" else "")
|
|
467
525
|
)
|
|
468
526
|
warnings.warn(
|
|
469
|
-
f"Partition column '{config.column}' is {reason}; "
|
|
470
|
-
"the BigQuery table will not be time-partitioned.",
|
|
527
|
+
f"Partition column '{config.column}' is {reason}; the BigQuery table will not be time-partitioned.",
|
|
471
528
|
UserWarning,
|
|
472
529
|
stacklevel=2,
|
|
473
530
|
)
|
|
@@ -577,8 +634,10 @@ def _partition_param(
|
|
|
577
634
|
value = datetime.datetime.fromisoformat(value)
|
|
578
635
|
except ValueError:
|
|
579
636
|
pass
|
|
580
|
-
if
|
|
581
|
-
|
|
637
|
+
if (
|
|
638
|
+
param_type in ("TIMESTAMP", "DATETIME")
|
|
639
|
+
and isinstance(value, datetime.date)
|
|
640
|
+
and not isinstance(value, datetime.datetime)
|
|
582
641
|
):
|
|
583
642
|
# A date bound against a datetime-typed column: promote to midnight so
|
|
584
643
|
# the client serializes it in the parameter's declared type.
|
|
@@ -602,4 +661,3 @@ def _iso_string(value: Any) -> str:
|
|
|
602
661
|
The ISO rendering of *value*.
|
|
603
662
|
"""
|
|
604
663
|
return value.isoformat() if isinstance(value, (datetime.date, datetime.datetime)) else str(value)
|
|
605
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|