apache-airflow-providers-google 15.1.0rc1__py3-none-any.whl → 16.0.0rc1__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.
Files changed (36) hide show
  1. airflow/providers/google/__init__.py +3 -3
  2. airflow/providers/google/ads/hooks/ads.py +34 -0
  3. airflow/providers/google/cloud/hooks/bigquery.py +63 -76
  4. airflow/providers/google/cloud/hooks/dataflow.py +67 -5
  5. airflow/providers/google/cloud/hooks/gcs.py +3 -3
  6. airflow/providers/google/cloud/hooks/looker.py +5 -0
  7. airflow/providers/google/cloud/hooks/vertex_ai/auto_ml.py +0 -36
  8. airflow/providers/google/cloud/hooks/vertex_ai/generative_model.py +1 -66
  9. airflow/providers/google/cloud/hooks/vertex_ai/ray.py +223 -0
  10. airflow/providers/google/cloud/links/cloud_run.py +59 -0
  11. airflow/providers/google/cloud/links/vertex_ai.py +49 -0
  12. airflow/providers/google/cloud/log/gcs_task_handler.py +7 -5
  13. airflow/providers/google/cloud/operators/bigquery.py +49 -10
  14. airflow/providers/google/cloud/operators/cloud_run.py +20 -2
  15. airflow/providers/google/cloud/operators/gcs.py +1 -0
  16. airflow/providers/google/cloud/operators/kubernetes_engine.py +4 -86
  17. airflow/providers/google/cloud/operators/pubsub.py +2 -1
  18. airflow/providers/google/cloud/operators/vertex_ai/generative_model.py +0 -92
  19. airflow/providers/google/cloud/operators/vertex_ai/pipeline_job.py +4 -0
  20. airflow/providers/google/cloud/operators/vertex_ai/ray.py +388 -0
  21. airflow/providers/google/cloud/transfers/bigquery_to_bigquery.py +9 -5
  22. airflow/providers/google/cloud/transfers/facebook_ads_to_gcs.py +1 -1
  23. airflow/providers/google/cloud/transfers/gcs_to_bigquery.py +2 -0
  24. airflow/providers/google/cloud/transfers/http_to_gcs.py +193 -0
  25. airflow/providers/google/cloud/transfers/s3_to_gcs.py +11 -5
  26. airflow/providers/google/cloud/triggers/bigquery.py +32 -5
  27. airflow/providers/google/cloud/triggers/dataflow.py +122 -0
  28. airflow/providers/google/cloud/triggers/dataproc.py +62 -10
  29. airflow/providers/google/get_provider_info.py +18 -5
  30. airflow/providers/google/leveldb/hooks/leveldb.py +25 -0
  31. airflow/providers/google/version_compat.py +0 -1
  32. {apache_airflow_providers_google-15.1.0rc1.dist-info → apache_airflow_providers_google-16.0.0rc1.dist-info}/METADATA +97 -90
  33. {apache_airflow_providers_google-15.1.0rc1.dist-info → apache_airflow_providers_google-16.0.0rc1.dist-info}/RECORD +35 -32
  34. airflow/providers/google/cloud/links/automl.py +0 -193
  35. {apache_airflow_providers_google-15.1.0rc1.dist-info → apache_airflow_providers_google-16.0.0rc1.dist-info}/WHEEL +0 -0
  36. {apache_airflow_providers_google-15.1.0rc1.dist-info → apache_airflow_providers_google-16.0.0rc1.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 execute(self, context: Context):
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 safe_to_cancel(self) -> bool:
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
- # Database query is needed to get the latest state of the task instance.
127
- task_instance = self.get_task_instance() # type: ignore[call-arg]
128
- return task_instance.state != TaskInstanceState.DEFERRED
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 safe_to_cancel(self) -> bool:
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
- # Database query is needed to get the latest state of the task instance.
152
- task_instance = self.get_task_instance() # type: ignore[call-arg]
153
- return task_instance.state != TaskInstanceState.DEFERRED
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 safe_to_cancel(self) -> bool:
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
- # Database query is needed to get the latest state of the task instance.
254
- task_instance = self.get_task_instance() # type: ignore[call-arg]
255
- return task_instance.state != TaskInstanceState.DEFERRED
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)