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.
Files changed (13) hide show
  1. {interloper_google_cloud-0.110.2 → interloper_google_cloud-0.112.0}/PKG-INFO +1 -1
  2. {interloper_google_cloud-0.110.2 → interloper_google_cloud-0.112.0}/pyproject.toml +1 -1
  3. {interloper_google_cloud-0.110.2 → interloper_google_cloud-0.112.0}/pyproject.toml.orig +1 -1
  4. {interloper_google_cloud-0.110.2 → interloper_google_cloud-0.112.0}/src/interloper_google_cloud/bigquery/destination.py +68 -10
  5. {interloper_google_cloud-0.110.2 → interloper_google_cloud-0.112.0}/README.md +0 -0
  6. {interloper_google_cloud-0.110.2 → interloper_google_cloud-0.112.0}/src/interloper_google_cloud/__init__.py +0 -0
  7. {interloper_google_cloud-0.110.2 → interloper_google_cloud-0.112.0}/src/interloper_google_cloud/bigquery/__init__.py +0 -0
  8. {interloper_google_cloud-0.110.2 → interloper_google_cloud-0.112.0}/src/interloper_google_cloud/bigquery/types.py +0 -0
  9. {interloper_google_cloud-0.110.2 → interloper_google_cloud-0.112.0}/src/interloper_google_cloud/connection.py +0 -0
  10. {interloper_google_cloud-0.110.2 → interloper_google_cloud-0.112.0}/src/interloper_google_cloud/gcs/__init__.py +0 -0
  11. {interloper_google_cloud-0.110.2 → interloper_google_cloud-0.112.0}/src/interloper_google_cloud/gcs/destination.py +0 -0
  12. {interloper_google_cloud-0.110.2 → interloper_google_cloud-0.112.0}/src/interloper_google_cloud/google_sheets/__init__.py +0 -0
  13. {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.110.2
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>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "interloper-google-cloud"
3
- version = "0.110.2"
3
+ version = "0.112.0"
4
4
  description = "Interloper Google Cloud integration: BigQuery, Cloud Storage and Google Sheets destinations"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -3,7 +3,7 @@
3
3
  # ###############
4
4
  [project]
5
5
  name = "interloper-google-cloud"
6
- version = "0.110.2"
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
- ds = self._resolve_dataset(dataset)
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 param_type in ("TIMESTAMP", "DATETIME") and isinstance(value, datetime.date) and not isinstance(
581
- value, datetime.datetime
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
-