|
18 | 18 | """This module contains a Google Cloud Spanner Hook.""" |
19 | 19 | from __future__ import annotations |
20 | 20 |
|
21 | | -from typing import Callable, Sequence |
| 21 | +from typing import Callable, NamedTuple, Sequence |
22 | 22 |
|
23 | 23 | from google.api_core.exceptions import AlreadyExists, GoogleAPICallError |
24 | 24 | from google.cloud.spanner_v1.client import Client |
25 | 25 | from google.cloud.spanner_v1.database import Database |
26 | 26 | from google.cloud.spanner_v1.instance import Instance |
27 | 27 | from google.cloud.spanner_v1.transaction import Transaction |
28 | 28 | from google.longrunning.operations_grpc_pb2 import Operation |
| 29 | +from sqlalchemy import create_engine |
29 | 30 |
|
30 | 31 | from airflow.exceptions import AirflowException |
| 32 | +from airflow.providers.common.sql.hooks.sql import DbApiHook |
31 | 33 | from airflow.providers.google.common.consts import CLIENT_INFO |
32 | | -from airflow.providers.google.common.hooks.base_google import GoogleBaseHook |
| 34 | +from airflow.providers.google.common.hooks.base_google import GoogleBaseHook, get_field |
33 | 35 |
|
34 | 36 |
|
35 | | -class SpannerHook(GoogleBaseHook): |
| 37 | +class SpannerConnectionParams(NamedTuple): |
| 38 | + """Information about Google Spanner connection parameters.""" |
| 39 | + |
| 40 | + project_id: str | None |
| 41 | + instance_id: str | None |
| 42 | + database_id: str | None |
| 43 | + |
| 44 | + |
| 45 | +class SpannerHook(GoogleBaseHook, DbApiHook): |
36 | 46 | """ |
37 | 47 | Hook for Google Cloud Spanner APIs. |
38 | 48 |
|
39 | 49 | All the methods in the hook where project_id is used must be called with |
40 | 50 | keyword arguments rather than positional. |
41 | 51 | """ |
42 | 52 |
|
| 53 | + conn_name_attr = "gcp_conn_id" |
| 54 | + default_conn_name = "google_cloud_spanner_default" |
| 55 | + conn_type = "gcpspanner" |
| 56 | + hook_name = "Google Cloud Spanner" |
| 57 | + |
43 | 58 | def __init__( |
44 | 59 | self, |
45 | 60 | gcp_conn_id: str = "google_cloud_default", |
@@ -70,6 +85,34 @@ def _get_client(self, project_id: str) -> Client: |
70 | 85 | ) |
71 | 86 | return self._client |
72 | 87 |
|
| 88 | + def _get_conn_params(self) -> SpannerConnectionParams: |
| 89 | + """Extract spanner database connection parameters.""" |
| 90 | + extras = self.get_connection(self.gcp_conn_id).extra_dejson |
| 91 | + project_id = get_field(extras, "project_id") or self.project_id |
| 92 | + instance_id = get_field(extras, "instance_id") |
| 93 | + database_id = get_field(extras, "database_id") |
| 94 | + return SpannerConnectionParams(project_id, instance_id, database_id) |
| 95 | + |
| 96 | + def get_uri(self) -> str: |
| 97 | + """Override DbApiHook get_uri method for get_sqlalchemy_engine().""" |
| 98 | + project_id, instance_id, database_id = self._get_conn_params() |
| 99 | + if not all([instance_id, database_id]): |
| 100 | + raise AirflowException("The instance_id or database_id were not specified") |
| 101 | + return f"spanner+spanner:///projects/{project_id}/instances/{instance_id}/databases/{database_id}" |
| 102 | + |
| 103 | + def get_sqlalchemy_engine(self, engine_kwargs=None): |
| 104 | + """ |
| 105 | + Get an sqlalchemy_engine object. |
| 106 | +
|
| 107 | + :param engine_kwargs: Kwargs used in :func:`~sqlalchemy.create_engine`. |
| 108 | + :return: the created engine. |
| 109 | + """ |
| 110 | + if engine_kwargs is None: |
| 111 | + engine_kwargs = {} |
| 112 | + project_id, _, _ = self._get_conn_params() |
| 113 | + spanner_client = self._get_client(project_id=project_id) |
| 114 | + return create_engine(self.get_uri(), connect_args={"client": spanner_client}, **engine_kwargs) |
| 115 | + |
73 | 116 | @GoogleBaseHook.fallback_to_default_project_id |
74 | 117 | def get_instance( |
75 | 118 | self, |
|
0 commit comments