apache-airflow-providers-google 15.1.0rc1__py3-none-any.whl → 16.0.0__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.
- airflow/providers/google/__init__.py +3 -3
- airflow/providers/google/ads/hooks/ads.py +34 -0
- airflow/providers/google/cloud/hooks/bigquery.py +63 -76
- airflow/providers/google/cloud/hooks/dataflow.py +67 -5
- airflow/providers/google/cloud/hooks/gcs.py +3 -3
- airflow/providers/google/cloud/hooks/looker.py +5 -0
- airflow/providers/google/cloud/hooks/vertex_ai/auto_ml.py +0 -36
- airflow/providers/google/cloud/hooks/vertex_ai/generative_model.py +1 -66
- airflow/providers/google/cloud/hooks/vertex_ai/ray.py +223 -0
- airflow/providers/google/cloud/links/cloud_run.py +59 -0
- airflow/providers/google/cloud/links/vertex_ai.py +49 -0
- airflow/providers/google/cloud/log/gcs_task_handler.py +7 -5
- airflow/providers/google/cloud/operators/bigquery.py +49 -10
- airflow/providers/google/cloud/operators/cloud_run.py +20 -2
- airflow/providers/google/cloud/operators/gcs.py +1 -0
- airflow/providers/google/cloud/operators/kubernetes_engine.py +4 -86
- airflow/providers/google/cloud/operators/pubsub.py +2 -1
- airflow/providers/google/cloud/operators/vertex_ai/generative_model.py +0 -92
- airflow/providers/google/cloud/operators/vertex_ai/pipeline_job.py +4 -0
- airflow/providers/google/cloud/operators/vertex_ai/ray.py +388 -0
- airflow/providers/google/cloud/transfers/bigquery_to_bigquery.py +9 -5
- airflow/providers/google/cloud/transfers/facebook_ads_to_gcs.py +1 -1
- airflow/providers/google/cloud/transfers/gcs_to_bigquery.py +2 -0
- airflow/providers/google/cloud/transfers/http_to_gcs.py +193 -0
- airflow/providers/google/cloud/transfers/s3_to_gcs.py +11 -5
- airflow/providers/google/cloud/triggers/bigquery.py +32 -5
- airflow/providers/google/cloud/triggers/dataflow.py +122 -0
- airflow/providers/google/cloud/triggers/dataproc.py +62 -10
- airflow/providers/google/get_provider_info.py +18 -5
- airflow/providers/google/leveldb/hooks/leveldb.py +25 -0
- airflow/providers/google/version_compat.py +0 -1
- {apache_airflow_providers_google-15.1.0rc1.dist-info → apache_airflow_providers_google-16.0.0.dist-info}/METADATA +92 -85
- {apache_airflow_providers_google-15.1.0rc1.dist-info → apache_airflow_providers_google-16.0.0.dist-info}/RECORD +35 -32
- airflow/providers/google/cloud/links/automl.py +0 -193
- {apache_airflow_providers_google-15.1.0rc1.dist-info → apache_airflow_providers_google-16.0.0.dist-info}/WHEEL +0 -0
- {apache_airflow_providers_google-15.1.0rc1.dist-info → apache_airflow_providers_google-16.0.0.dist-info}/entry_points.txt +0 -0
@@ -181,21 +181,27 @@ class S3ToGCSOperator(S3ListOperator):
|
|
181
181
|
'The destination Google Cloud Storage path must end with a slash "/" or be empty.'
|
182
182
|
)
|
183
183
|
|
184
|
-
def
|
185
|
-
self._check_inputs()
|
184
|
+
def _get_files(self, context: Context, gcs_hook: GCSHook) -> list[str]:
|
186
185
|
# use the super method to list all the files in an S3 bucket/key
|
187
186
|
s3_objects = super().execute(context)
|
188
187
|
|
188
|
+
if not self.replace:
|
189
|
+
s3_objects = self.exclude_existing_objects(s3_objects=s3_objects, gcs_hook=gcs_hook)
|
190
|
+
|
191
|
+
return s3_objects
|
192
|
+
|
193
|
+
def execute(self, context: Context):
|
194
|
+
self._check_inputs()
|
189
195
|
gcs_hook = GCSHook(
|
190
196
|
gcp_conn_id=self.gcp_conn_id,
|
191
197
|
impersonation_chain=self.google_impersonation_chain,
|
192
198
|
)
|
193
|
-
if not self.replace:
|
194
|
-
s3_objects = self.exclude_existing_objects(s3_objects=s3_objects, gcs_hook=gcs_hook)
|
195
|
-
|
196
199
|
s3_hook = S3Hook(aws_conn_id=self.aws_conn_id, verify=self.verify)
|
200
|
+
|
201
|
+
s3_objects = self._get_files(context, gcs_hook)
|
197
202
|
if not s3_objects:
|
198
203
|
self.log.info("In sync, no files needed to be uploaded to Google Cloud Storage")
|
204
|
+
|
199
205
|
elif self.deferrable:
|
200
206
|
self.transfer_files_async(s3_objects, gcs_hook, s3_hook)
|
201
207
|
else:
|
@@ -22,10 +22,12 @@ from typing import TYPE_CHECKING, Any, SupportsAbs
|
|
22
22
|
|
23
23
|
from aiohttp import ClientSession
|
24
24
|
from aiohttp.client_exceptions import ClientResponseError
|
25
|
+
from asgiref.sync import sync_to_async
|
25
26
|
|
26
27
|
from airflow.exceptions import AirflowException
|
27
28
|
from airflow.models.taskinstance import TaskInstance
|
28
29
|
from airflow.providers.google.cloud.hooks.bigquery import BigQueryAsyncHook, BigQueryTableAsyncHook
|
30
|
+
from airflow.providers.google.version_compat import AIRFLOW_V_3_0_PLUS
|
29
31
|
from airflow.triggers.base import BaseTrigger, TriggerEvent
|
30
32
|
from airflow.utils.session import provide_session
|
31
33
|
from airflow.utils.state import TaskInstanceState
|
@@ -116,16 +118,41 @@ class BigQueryInsertJobTrigger(BaseTrigger):
|
|
116
118
|
)
|
117
119
|
return task_instance
|
118
120
|
|
119
|
-
def
|
121
|
+
async def get_task_state(self):
|
122
|
+
from airflow.sdk.execution_time.task_runner import RuntimeTaskInstance
|
123
|
+
|
124
|
+
task_states_response = await sync_to_async(RuntimeTaskInstance.get_task_states)(
|
125
|
+
dag_id=self.task_instance.dag_id,
|
126
|
+
task_ids=[self.task_instance.task_id],
|
127
|
+
run_ids=[self.task_instance.run_id],
|
128
|
+
map_index=self.task_instance.map_index,
|
129
|
+
)
|
130
|
+
try:
|
131
|
+
task_state = task_states_response[self.task_instance.run_id][self.task_instance.task_id]
|
132
|
+
except Exception:
|
133
|
+
raise AirflowException(
|
134
|
+
"TaskInstance with dag_id: %s, task_id: %s, run_id: %s and map_index: %s is not found",
|
135
|
+
self.task_instance.dag_id,
|
136
|
+
self.task_instance.task_id,
|
137
|
+
self.task_instance.run_id,
|
138
|
+
self.task_instance.map_index,
|
139
|
+
)
|
140
|
+
return task_state
|
141
|
+
|
142
|
+
async def safe_to_cancel(self) -> bool:
|
120
143
|
"""
|
121
144
|
Whether it is safe to cancel the external job which is being executed by this trigger.
|
122
145
|
|
123
146
|
This is to avoid the case that `asyncio.CancelledError` is called because the trigger itself is stopped.
|
124
147
|
Because in those cases, we should NOT cancel the external job.
|
125
148
|
"""
|
126
|
-
|
127
|
-
|
128
|
-
|
149
|
+
if AIRFLOW_V_3_0_PLUS:
|
150
|
+
task_state = await self.get_task_state()
|
151
|
+
else:
|
152
|
+
# Database query is needed to get the latest state of the task instance.
|
153
|
+
task_instance = self.get_task_instance() # type: ignore[call-arg]
|
154
|
+
task_state = task_instance.state
|
155
|
+
return task_state != TaskInstanceState.DEFERRED
|
129
156
|
|
130
157
|
async def run(self) -> AsyncIterator[TriggerEvent]: # type: ignore[override]
|
131
158
|
"""Get current job execution status and yields a TriggerEvent."""
|
@@ -155,7 +182,7 @@ class BigQueryInsertJobTrigger(BaseTrigger):
|
|
155
182
|
)
|
156
183
|
await asyncio.sleep(self.poll_interval)
|
157
184
|
except asyncio.CancelledError:
|
158
|
-
if self.job_id and self.cancel_on_kill and self.safe_to_cancel():
|
185
|
+
if self.job_id and self.cancel_on_kill and await self.safe_to_cancel():
|
159
186
|
self.log.info(
|
160
187
|
"The job is safe to cancel the as airflow TaskInstance is not in deferred state."
|
161
188
|
)
|
@@ -788,3 +788,125 @@ class DataflowJobMessagesTrigger(BaseTrigger):
|
|
788
788
|
poll_sleep=self.poll_sleep,
|
789
789
|
impersonation_chain=self.impersonation_chain,
|
790
790
|
)
|
791
|
+
|
792
|
+
|
793
|
+
class DataflowJobStateCompleteTrigger(BaseTrigger):
|
794
|
+
"""
|
795
|
+
Trigger that monitors if a Dataflow job has reached any of successful terminal state meant for that job.
|
796
|
+
|
797
|
+
:param job_id: Required. ID of the job.
|
798
|
+
:param project_id: Required. The Google Cloud project ID in which the job was started.
|
799
|
+
:param location: Optional. The location where the job is executed. If set to None then
|
800
|
+
the value of DEFAULT_DATAFLOW_LOCATION will be used.
|
801
|
+
:param wait_until_finished: Optional. Dataflow option to block pipeline until completion.
|
802
|
+
:param gcp_conn_id: The connection ID to use for connecting to Google Cloud.
|
803
|
+
:param poll_sleep: Time (seconds) to wait between two consecutive calls to check the job.
|
804
|
+
:param impersonation_chain: Optional. Service account to impersonate using short-term
|
805
|
+
credentials, or chained list of accounts required to get the access_token
|
806
|
+
of the last account in the list, which will be impersonated in the request.
|
807
|
+
If set as a string, the account must grant the originating account
|
808
|
+
the Service Account Token Creator IAM role.
|
809
|
+
If set as a sequence, the identities from the list must grant
|
810
|
+
Service Account Token Creator IAM role to the directly preceding identity, with first
|
811
|
+
account from the list granting this role to the originating account (templated).
|
812
|
+
"""
|
813
|
+
|
814
|
+
def __init__(
|
815
|
+
self,
|
816
|
+
job_id: str,
|
817
|
+
project_id: str | None,
|
818
|
+
location: str = DEFAULT_DATAFLOW_LOCATION,
|
819
|
+
wait_until_finished: bool | None = None,
|
820
|
+
gcp_conn_id: str = "google_cloud_default",
|
821
|
+
poll_sleep: int = 10,
|
822
|
+
impersonation_chain: str | Sequence[str] | None = None,
|
823
|
+
):
|
824
|
+
super().__init__()
|
825
|
+
self.job_id = job_id
|
826
|
+
self.project_id = project_id
|
827
|
+
self.location = location
|
828
|
+
self.wait_until_finished = wait_until_finished
|
829
|
+
self.gcp_conn_id = gcp_conn_id
|
830
|
+
self.poll_sleep = poll_sleep
|
831
|
+
self.impersonation_chain = impersonation_chain
|
832
|
+
|
833
|
+
def serialize(self) -> tuple[str, dict[str, Any]]:
|
834
|
+
"""Serialize class arguments and classpath."""
|
835
|
+
return (
|
836
|
+
"airflow.providers.google.cloud.triggers.dataflow.DataflowJobStateCompleteTrigger",
|
837
|
+
{
|
838
|
+
"job_id": self.job_id,
|
839
|
+
"project_id": self.project_id,
|
840
|
+
"location": self.location,
|
841
|
+
"wait_until_finished": self.wait_until_finished,
|
842
|
+
"gcp_conn_id": self.gcp_conn_id,
|
843
|
+
"poll_sleep": self.poll_sleep,
|
844
|
+
"impersonation_chain": self.impersonation_chain,
|
845
|
+
},
|
846
|
+
)
|
847
|
+
|
848
|
+
async def run(self):
|
849
|
+
"""
|
850
|
+
Loop until the job reaches successful final or error state.
|
851
|
+
|
852
|
+
Yields a TriggerEvent with success status, if the job reaches successful state for own type.
|
853
|
+
|
854
|
+
Yields a TriggerEvent with error status, if the client returns an unexpected terminal
|
855
|
+
job status or any exception is raised while looping.
|
856
|
+
|
857
|
+
In any other case the Trigger will wait for a specified amount of time
|
858
|
+
stored in self.poll_sleep variable.
|
859
|
+
"""
|
860
|
+
try:
|
861
|
+
while True:
|
862
|
+
job = await self.async_hook.get_job(
|
863
|
+
project_id=self.project_id,
|
864
|
+
job_id=self.job_id,
|
865
|
+
location=self.location,
|
866
|
+
)
|
867
|
+
job_state = job.current_state.name
|
868
|
+
job_type_name = job.type_.name
|
869
|
+
|
870
|
+
FAILED_STATES = DataflowJobStatus.FAILED_END_STATES | {DataflowJobStatus.JOB_STATE_DRAINED}
|
871
|
+
if job_state in FAILED_STATES:
|
872
|
+
yield TriggerEvent(
|
873
|
+
{
|
874
|
+
"status": "error",
|
875
|
+
"message": (
|
876
|
+
f"Job with id '{self.job_id}' is in failed terminal state: {job_state}"
|
877
|
+
),
|
878
|
+
}
|
879
|
+
)
|
880
|
+
return
|
881
|
+
|
882
|
+
if self.async_hook.job_reached_terminal_state(
|
883
|
+
job={"id": self.job_id, "currentState": job_state, "type": job_type_name},
|
884
|
+
wait_until_finished=self.wait_until_finished,
|
885
|
+
):
|
886
|
+
yield TriggerEvent(
|
887
|
+
{
|
888
|
+
"status": "success",
|
889
|
+
"message": (
|
890
|
+
f"Job with id '{self.job_id}' has reached successful final state: {job_state}"
|
891
|
+
),
|
892
|
+
}
|
893
|
+
)
|
894
|
+
return
|
895
|
+
self.log.info("Sleeping for %s seconds.", self.poll_sleep)
|
896
|
+
await asyncio.sleep(self.poll_sleep)
|
897
|
+
except Exception as e:
|
898
|
+
self.log.error("Exception occurred while checking for job state!")
|
899
|
+
yield TriggerEvent(
|
900
|
+
{
|
901
|
+
"status": "error",
|
902
|
+
"message": str(e),
|
903
|
+
}
|
904
|
+
)
|
905
|
+
|
906
|
+
@cached_property
|
907
|
+
def async_hook(self) -> AsyncDataflowHook:
|
908
|
+
return AsyncDataflowHook(
|
909
|
+
gcp_conn_id=self.gcp_conn_id,
|
910
|
+
poll_sleep=self.poll_sleep,
|
911
|
+
impersonation_chain=self.impersonation_chain,
|
912
|
+
)
|
@@ -25,6 +25,7 @@ import time
|
|
25
25
|
from collections.abc import AsyncIterator, Sequence
|
26
26
|
from typing import TYPE_CHECKING, Any
|
27
27
|
|
28
|
+
from asgiref.sync import sync_to_async
|
28
29
|
from google.api_core.exceptions import NotFound
|
29
30
|
from google.cloud.dataproc_v1 import Batch, Cluster, ClusterStatus, JobStatus
|
30
31
|
|
@@ -33,6 +34,7 @@ from airflow.models.taskinstance import TaskInstance
|
|
33
34
|
from airflow.providers.google.cloud.hooks.dataproc import DataprocAsyncHook, DataprocHook
|
34
35
|
from airflow.providers.google.cloud.utils.dataproc import DataprocOperationType
|
35
36
|
from airflow.providers.google.common.hooks.base_google import PROVIDE_PROJECT_ID
|
37
|
+
from airflow.providers.google.version_compat import AIRFLOW_V_3_0_PLUS
|
36
38
|
from airflow.triggers.base import BaseTrigger, TriggerEvent
|
37
39
|
from airflow.utils.session import provide_session
|
38
40
|
from airflow.utils.state import TaskInstanceState
|
@@ -141,16 +143,41 @@ class DataprocSubmitTrigger(DataprocBaseTrigger):
|
|
141
143
|
)
|
142
144
|
return task_instance
|
143
145
|
|
144
|
-
def
|
146
|
+
async def get_task_state(self):
|
147
|
+
from airflow.sdk.execution_time.task_runner import RuntimeTaskInstance
|
148
|
+
|
149
|
+
task_states_response = await sync_to_async(RuntimeTaskInstance.get_task_states)(
|
150
|
+
dag_id=self.task_instance.dag_id,
|
151
|
+
task_ids=[self.task_instance.task_id],
|
152
|
+
run_ids=[self.task_instance.run_id],
|
153
|
+
map_index=self.task_instance.map_index,
|
154
|
+
)
|
155
|
+
try:
|
156
|
+
task_state = task_states_response[self.task_instance.run_id][self.task_instance.task_id]
|
157
|
+
except Exception:
|
158
|
+
raise AirflowException(
|
159
|
+
"TaskInstance with dag_id: %s, task_id: %s, run_id: %s and map_index: %s is not found",
|
160
|
+
self.task_instance.dag_id,
|
161
|
+
self.task_instance.task_id,
|
162
|
+
self.task_instance.run_id,
|
163
|
+
self.task_instance.map_index,
|
164
|
+
)
|
165
|
+
return task_state
|
166
|
+
|
167
|
+
async def safe_to_cancel(self) -> bool:
|
145
168
|
"""
|
146
169
|
Whether it is safe to cancel the external job which is being executed by this trigger.
|
147
170
|
|
148
171
|
This is to avoid the case that `asyncio.CancelledError` is called because the trigger itself is stopped.
|
149
172
|
Because in those cases, we should NOT cancel the external job.
|
150
173
|
"""
|
151
|
-
|
152
|
-
|
153
|
-
|
174
|
+
if AIRFLOW_V_3_0_PLUS:
|
175
|
+
task_state = await self.get_task_state()
|
176
|
+
else:
|
177
|
+
# Database query is needed to get the latest state of the task instance.
|
178
|
+
task_instance = self.get_task_instance() # type: ignore[call-arg]
|
179
|
+
task_state = task_instance.state
|
180
|
+
return task_state != TaskInstanceState.DEFERRED
|
154
181
|
|
155
182
|
async def run(self):
|
156
183
|
try:
|
@@ -167,7 +194,7 @@ class DataprocSubmitTrigger(DataprocBaseTrigger):
|
|
167
194
|
except asyncio.CancelledError:
|
168
195
|
self.log.info("Task got cancelled.")
|
169
196
|
try:
|
170
|
-
if self.job_id and self.cancel_on_kill and self.safe_to_cancel():
|
197
|
+
if self.job_id and self.cancel_on_kill and await self.safe_to_cancel():
|
171
198
|
self.log.info(
|
172
199
|
"Cancelling the job as it is safe to do so. Note that the airflow TaskInstance is not"
|
173
200
|
" in deferred state."
|
@@ -243,16 +270,41 @@ class DataprocClusterTrigger(DataprocBaseTrigger):
|
|
243
270
|
)
|
244
271
|
return task_instance
|
245
272
|
|
246
|
-
def
|
273
|
+
async def get_task_state(self):
|
274
|
+
from airflow.sdk.execution_time.task_runner import RuntimeTaskInstance
|
275
|
+
|
276
|
+
task_states_response = await sync_to_async(RuntimeTaskInstance.get_task_states)(
|
277
|
+
dag_id=self.task_instance.dag_id,
|
278
|
+
task_ids=[self.task_instance.task_id],
|
279
|
+
run_ids=[self.task_instance.run_id],
|
280
|
+
map_index=self.task_instance.map_index,
|
281
|
+
)
|
282
|
+
try:
|
283
|
+
task_state = task_states_response[self.task_instance.run_id][self.task_instance.task_id]
|
284
|
+
except Exception:
|
285
|
+
raise AirflowException(
|
286
|
+
"TaskInstance with dag_id: %s, task_id: %s, run_id: %s and map_index: %s is not found",
|
287
|
+
self.task_instance.dag_id,
|
288
|
+
self.task_instance.task_id,
|
289
|
+
self.task_instance.run_id,
|
290
|
+
self.task_instance.map_index,
|
291
|
+
)
|
292
|
+
return task_state
|
293
|
+
|
294
|
+
async def safe_to_cancel(self) -> bool:
|
247
295
|
"""
|
248
296
|
Whether it is safe to cancel the external job which is being executed by this trigger.
|
249
297
|
|
250
298
|
This is to avoid the case that `asyncio.CancelledError` is called because the trigger itself is stopped.
|
251
299
|
Because in those cases, we should NOT cancel the external job.
|
252
300
|
"""
|
253
|
-
|
254
|
-
|
255
|
-
|
301
|
+
if AIRFLOW_V_3_0_PLUS:
|
302
|
+
task_state = await self.get_task_state()
|
303
|
+
else:
|
304
|
+
# Database query is needed to get the latest state of the task instance.
|
305
|
+
task_instance = self.get_task_instance() # type: ignore[call-arg]
|
306
|
+
task_state = task_instance.state
|
307
|
+
return task_state != TaskInstanceState.DEFERRED
|
256
308
|
|
257
309
|
async def run(self) -> AsyncIterator[TriggerEvent]:
|
258
310
|
try:
|
@@ -283,7 +335,7 @@ class DataprocClusterTrigger(DataprocBaseTrigger):
|
|
283
335
|
await asyncio.sleep(self.polling_interval_seconds)
|
284
336
|
except asyncio.CancelledError:
|
285
337
|
try:
|
286
|
-
if self.delete_on_error and self.safe_to_cancel():
|
338
|
+
if self.delete_on_error and await self.safe_to_cancel():
|
287
339
|
self.log.info(
|
288
340
|
"Deleting the cluster as it is safe to delete as the airflow TaskInstance is not in "
|
289
341
|
"deferred state."
|
@@ -675,6 +675,7 @@ def get_provider_info():
|
|
675
675
|
"airflow.providers.google.cloud.operators.vertex_ai.pipeline_job",
|
676
676
|
"airflow.providers.google.cloud.operators.vertex_ai.generative_model",
|
677
677
|
"airflow.providers.google.cloud.operators.vertex_ai.feature_store",
|
678
|
+
"airflow.providers.google.cloud.operators.vertex_ai.ray",
|
678
679
|
],
|
679
680
|
},
|
680
681
|
{
|
@@ -1041,6 +1042,7 @@ def get_provider_info():
|
|
1041
1042
|
"airflow.providers.google.cloud.hooks.vertex_ai.generative_model",
|
1042
1043
|
"airflow.providers.google.cloud.hooks.vertex_ai.prediction_service",
|
1043
1044
|
"airflow.providers.google.cloud.hooks.vertex_ai.feature_store",
|
1045
|
+
"airflow.providers.google.cloud.hooks.vertex_ai.ray",
|
1044
1046
|
],
|
1045
1047
|
},
|
1046
1048
|
{
|
@@ -1336,6 +1338,12 @@ def get_provider_info():
|
|
1336
1338
|
"python-module": "airflow.providers.google.cloud.transfers.azure_blob_to_gcs",
|
1337
1339
|
"how-to-guide": "/docs/apache-airflow-providers-google/operators/transfer/azure_blob_to_gcs.rst",
|
1338
1340
|
},
|
1341
|
+
{
|
1342
|
+
"source-integration-name": "Hypertext Transfer Protocol (HTTP)",
|
1343
|
+
"target-integration-name": "Google Cloud Storage (GCS)",
|
1344
|
+
"python-module": "airflow.providers.google.cloud.transfers.http_to_gcs",
|
1345
|
+
"how-to-guide": "/docs/apache-airflow-providers-google/operators/transfer/http_to_gcs.rst",
|
1346
|
+
},
|
1339
1347
|
],
|
1340
1348
|
"connection-types": [
|
1341
1349
|
{
|
@@ -1366,6 +1374,14 @@ def get_provider_info():
|
|
1366
1374
|
"hook-class-name": "airflow.providers.google.leveldb.hooks.leveldb.LevelDBHook",
|
1367
1375
|
"connection-type": "leveldb",
|
1368
1376
|
},
|
1377
|
+
{
|
1378
|
+
"hook-class-name": "airflow.providers.google.ads.hooks.ads.GoogleAdsHook",
|
1379
|
+
"connection-type": "google_ads",
|
1380
|
+
},
|
1381
|
+
{
|
1382
|
+
"hook-class-name": "airflow.providers.google.cloud.hooks.looker.LookerHook",
|
1383
|
+
"connection-type": "gcp_looker",
|
1384
|
+
},
|
1369
1385
|
],
|
1370
1386
|
"extra-links": [
|
1371
1387
|
"airflow.providers.google.cloud.links.alloy_db.AlloyDBBackupsLink",
|
@@ -1427,6 +1443,8 @@ def get_provider_info():
|
|
1427
1443
|
"airflow.providers.google.cloud.links.vertex_ai.VertexAIEndpointListLink",
|
1428
1444
|
"airflow.providers.google.cloud.links.vertex_ai.VertexAIPipelineJobLink",
|
1429
1445
|
"airflow.providers.google.cloud.links.vertex_ai.VertexAIPipelineJobListLink",
|
1446
|
+
"airflow.providers.google.cloud.links.vertex_ai.VertexAIRayClusterLink",
|
1447
|
+
"airflow.providers.google.cloud.links.vertex_ai.VertexAIRayClusterListLink",
|
1430
1448
|
"airflow.providers.google.cloud.links.workflows.WorkflowsWorkflowDetailsLink",
|
1431
1449
|
"airflow.providers.google.cloud.links.workflows.WorkflowsListOfWorkflowsLink",
|
1432
1450
|
"airflow.providers.google.cloud.links.workflows.WorkflowsExecutionLink",
|
@@ -1457,11 +1475,6 @@ def get_provider_info():
|
|
1457
1475
|
"airflow.providers.google.cloud.links.cloud_build.CloudBuildListLink",
|
1458
1476
|
"airflow.providers.google.cloud.links.cloud_build.CloudBuildTriggersListLink",
|
1459
1477
|
"airflow.providers.google.cloud.links.cloud_build.CloudBuildTriggerDetailsLink",
|
1460
|
-
"airflow.providers.google.cloud.links.automl.AutoMLDatasetLink",
|
1461
|
-
"airflow.providers.google.cloud.links.automl.AutoMLDatasetListLink",
|
1462
|
-
"airflow.providers.google.cloud.links.automl.AutoMLModelLink",
|
1463
|
-
"airflow.providers.google.cloud.links.automl.AutoMLModelTrainLink",
|
1464
|
-
"airflow.providers.google.cloud.links.automl.AutoMLModelPredictLink",
|
1465
1478
|
"airflow.providers.google.cloud.links.life_sciences.LifeSciencesLink",
|
1466
1479
|
"airflow.providers.google.cloud.links.cloud_functions.CloudFunctionsDetailsLink",
|
1467
1480
|
"airflow.providers.google.cloud.links.cloud_functions.CloudFunctionsListLink",
|
@@ -18,6 +18,8 @@
|
|
18
18
|
|
19
19
|
from __future__ import annotations
|
20
20
|
|
21
|
+
from typing import Any
|
22
|
+
|
21
23
|
from airflow.exceptions import AirflowException, AirflowOptionalProviderFeatureException
|
22
24
|
from airflow.hooks.base import BaseHook
|
23
25
|
|
@@ -46,6 +48,29 @@ class LevelDBHook(BaseHook):
|
|
46
48
|
conn_type = "leveldb"
|
47
49
|
hook_name = "LevelDB"
|
48
50
|
|
51
|
+
@classmethod
|
52
|
+
def get_connection_form_widgets(cls) -> dict[str, Any]:
|
53
|
+
"""Return connection widgets to add to LevelDB connection form."""
|
54
|
+
from flask_babel import lazy_gettext
|
55
|
+
from wtforms import BooleanField
|
56
|
+
|
57
|
+
return {
|
58
|
+
"create_if_missing": BooleanField(
|
59
|
+
lazy_gettext("Create a database if it does not exist"), default=False
|
60
|
+
),
|
61
|
+
"error_if_exists": BooleanField(
|
62
|
+
lazy_gettext("Raise an exception if the database already exists"), default=False
|
63
|
+
),
|
64
|
+
}
|
65
|
+
|
66
|
+
@classmethod
|
67
|
+
def get_ui_field_behaviour(cls) -> dict[str, Any]:
|
68
|
+
"""Return custom UI field behaviour for LevelDB connection."""
|
69
|
+
return {
|
70
|
+
"hidden_fields": ["login", "password", "schema", "port"],
|
71
|
+
"relabeling": {},
|
72
|
+
}
|
73
|
+
|
49
74
|
def __init__(self, leveldb_conn_id: str = default_conn_name):
|
50
75
|
super().__init__()
|
51
76
|
self.leveldb_conn_id = leveldb_conn_id
|
@@ -32,5 +32,4 @@ def get_base_airflow_version_tuple() -> tuple[int, int, int]:
|
|
32
32
|
return airflow_version.major, airflow_version.minor, airflow_version.micro
|
33
33
|
|
34
34
|
|
35
|
-
AIRFLOW_V_2_10_PLUS = get_base_airflow_version_tuple() >= (2, 10, 0)
|
36
35
|
AIRFLOW_V_3_0_PLUS = get_base_airflow_version_tuple() >= (3, 0, 0)
|