apache-airflow-providers-amazon 8.3.1__py3-none-any.whl → 8.4.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.
Files changed (34) hide show
  1. airflow/providers/amazon/__init__.py +4 -2
  2. airflow/providers/amazon/aws/hooks/base_aws.py +29 -12
  3. airflow/providers/amazon/aws/hooks/emr.py +17 -9
  4. airflow/providers/amazon/aws/hooks/eventbridge.py +27 -0
  5. airflow/providers/amazon/aws/hooks/redshift_data.py +10 -0
  6. airflow/providers/amazon/aws/hooks/sagemaker.py +24 -14
  7. airflow/providers/amazon/aws/notifications/chime.py +1 -1
  8. airflow/providers/amazon/aws/operators/eks.py +140 -7
  9. airflow/providers/amazon/aws/operators/emr.py +202 -22
  10. airflow/providers/amazon/aws/operators/eventbridge.py +87 -0
  11. airflow/providers/amazon/aws/operators/rds.py +120 -48
  12. airflow/providers/amazon/aws/operators/redshift_data.py +7 -0
  13. airflow/providers/amazon/aws/operators/sagemaker.py +75 -7
  14. airflow/providers/amazon/aws/operators/step_function.py +34 -2
  15. airflow/providers/amazon/aws/transfers/s3_to_redshift.py +1 -1
  16. airflow/providers/amazon/aws/triggers/batch.py +1 -1
  17. airflow/providers/amazon/aws/triggers/ecs.py +7 -5
  18. airflow/providers/amazon/aws/triggers/eks.py +174 -3
  19. airflow/providers/amazon/aws/triggers/emr.py +215 -1
  20. airflow/providers/amazon/aws/triggers/rds.py +161 -5
  21. airflow/providers/amazon/aws/triggers/sagemaker.py +84 -1
  22. airflow/providers/amazon/aws/triggers/step_function.py +59 -0
  23. airflow/providers/amazon/aws/utils/__init__.py +16 -1
  24. airflow/providers/amazon/aws/utils/rds.py +2 -2
  25. airflow/providers/amazon/aws/waiters/sagemaker.json +46 -0
  26. airflow/providers/amazon/aws/waiters/stepfunctions.json +36 -0
  27. airflow/providers/amazon/get_provider_info.py +21 -1
  28. {apache_airflow_providers_amazon-8.3.1.dist-info → apache_airflow_providers_amazon-8.4.0.dist-info}/METADATA +11 -11
  29. {apache_airflow_providers_amazon-8.3.1.dist-info → apache_airflow_providers_amazon-8.4.0.dist-info}/RECORD +34 -30
  30. {apache_airflow_providers_amazon-8.3.1.dist-info → apache_airflow_providers_amazon-8.4.0.dist-info}/WHEEL +1 -1
  31. {apache_airflow_providers_amazon-8.3.1.dist-info → apache_airflow_providers_amazon-8.4.0.dist-info}/LICENSE +0 -0
  32. {apache_airflow_providers_amazon-8.3.1.dist-info → apache_airflow_providers_amazon-8.4.0.dist-info}/NOTICE +0 -0
  33. {apache_airflow_providers_amazon-8.3.1.dist-info → apache_airflow_providers_amazon-8.4.0.dist-info}/entry_points.txt +0 -0
  34. {apache_airflow_providers_amazon-8.3.1.dist-info → apache_airflow_providers_amazon-8.4.0.dist-info}/top_level.txt +0 -0
@@ -33,6 +33,12 @@ from airflow.providers.amazon.aws.triggers.emr import (
33
33
  EmrAddStepsTrigger,
34
34
  EmrContainerTrigger,
35
35
  EmrCreateJobFlowTrigger,
36
+ EmrServerlessCancelJobsTrigger,
37
+ EmrServerlessCreateApplicationTrigger,
38
+ EmrServerlessDeleteApplicationTrigger,
39
+ EmrServerlessStartApplicationTrigger,
40
+ EmrServerlessStartJobTrigger,
41
+ EmrServerlessStopApplicationTrigger,
36
42
  EmrTerminateJobFlowTrigger,
37
43
  )
38
44
  from airflow.providers.amazon.aws.utils.waiter import waiter
@@ -972,7 +978,7 @@ class EmrServerlessCreateApplicationOperator(BaseOperator):
972
978
  :param release_label: The EMR release version associated with the application.
973
979
  :param job_type: The type of application you want to start, such as Spark or Hive.
974
980
  :param wait_for_completion: If true, wait for the Application to start before returning. Default to True.
975
- If set to False, ``waiter_countdown`` and ``waiter_check_interval_seconds`` will only be applied when
981
+ If set to False, ``waiter_max_attempts`` and ``waiter_delay`` will only be applied when
976
982
  waiting for the application to be in the ``CREATED`` state.
977
983
  :param client_request_token: The client idempotency token of the application to create.
978
984
  Its value must be unique for each request.
@@ -985,6 +991,9 @@ class EmrServerlessCreateApplicationOperator(BaseOperator):
985
991
  :waiter_max_attempts: Number of times the waiter should poll the application to check the state.
986
992
  If not set, the waiter will use its default value.
987
993
  :param waiter_delay: Number of seconds between polling the state of the application.
994
+ :param deferrable: If True, the operator will wait asynchronously for application to be created.
995
+ This implies waiting for completion. This mode requires aiobotocore module to be installed.
996
+ (default: False, but can be overridden in config file by setting default_deferrable to True)
988
997
  """
989
998
 
990
999
  def __init__(
@@ -999,6 +1008,7 @@ class EmrServerlessCreateApplicationOperator(BaseOperator):
999
1008
  waiter_check_interval_seconds: int | ArgNotSet = NOTSET,
1000
1009
  waiter_max_attempts: int | ArgNotSet = NOTSET,
1001
1010
  waiter_delay: int | ArgNotSet = NOTSET,
1011
+ deferrable: bool = conf.getboolean("operators", "default_deferrable", fallback=False),
1002
1012
  **kwargs,
1003
1013
  ):
1004
1014
  if waiter_check_interval_seconds is NOTSET:
@@ -1030,6 +1040,7 @@ class EmrServerlessCreateApplicationOperator(BaseOperator):
1030
1040
  self.config = config or {}
1031
1041
  self.waiter_max_attempts = int(waiter_max_attempts) # type: ignore[arg-type]
1032
1042
  self.waiter_delay = int(waiter_delay) # type: ignore[arg-type]
1043
+ self.deferrable = deferrable
1033
1044
  super().__init__(**kwargs)
1034
1045
 
1035
1046
  self.client_request_token = client_request_token or str(uuid4())
@@ -1052,8 +1063,19 @@ class EmrServerlessCreateApplicationOperator(BaseOperator):
1052
1063
  raise AirflowException(f"Application Creation failed: {response}")
1053
1064
 
1054
1065
  self.log.info("EMR serverless application created: %s", application_id)
1055
- waiter = self.hook.get_waiter("serverless_app_created")
1066
+ if self.deferrable:
1067
+ self.defer(
1068
+ trigger=EmrServerlessCreateApplicationTrigger(
1069
+ application_id=application_id,
1070
+ aws_conn_id=self.aws_conn_id,
1071
+ waiter_delay=self.waiter_delay,
1072
+ waiter_max_attempts=self.waiter_max_attempts,
1073
+ ),
1074
+ timeout=timedelta(seconds=self.waiter_max_attempts * self.waiter_delay),
1075
+ method_name="start_application_deferred",
1076
+ )
1056
1077
 
1078
+ waiter = self.hook.get_waiter("serverless_app_created")
1057
1079
  wait(
1058
1080
  waiter=waiter,
1059
1081
  waiter_delay=self.waiter_delay,
@@ -1079,6 +1101,32 @@ class EmrServerlessCreateApplicationOperator(BaseOperator):
1079
1101
  )
1080
1102
  return application_id
1081
1103
 
1104
+ def start_application_deferred(self, context: Context, event: dict[str, Any] | None = None) -> None:
1105
+ if event is None:
1106
+ self.log.error("Trigger error: event is None")
1107
+ raise AirflowException("Trigger error: event is None")
1108
+ elif event["status"] != "success":
1109
+ raise AirflowException(f"Application {event['application_id']} failed to create")
1110
+ self.log.info("Starting application %s", event["application_id"])
1111
+ self.hook.conn.start_application(applicationId=event["application_id"])
1112
+ self.defer(
1113
+ trigger=EmrServerlessStartApplicationTrigger(
1114
+ application_id=event["application_id"],
1115
+ aws_conn_id=self.aws_conn_id,
1116
+ waiter_delay=self.waiter_delay,
1117
+ waiter_max_attempts=self.waiter_max_attempts,
1118
+ ),
1119
+ timeout=timedelta(seconds=self.waiter_max_attempts * self.waiter_delay),
1120
+ method_name="execute_complete",
1121
+ )
1122
+
1123
+ def execute_complete(self, context: Context, event: dict[str, Any] | None = None) -> None:
1124
+ if event is None or event["status"] != "success":
1125
+ raise AirflowException(f"Trigger error: Application failed to start, event is {event}")
1126
+
1127
+ self.log.info("Application %s started", event["application_id"])
1128
+ return event["application_id"]
1129
+
1082
1130
 
1083
1131
  class EmrServerlessStartJobOperator(BaseOperator):
1084
1132
  """
@@ -1107,6 +1155,9 @@ class EmrServerlessStartJobOperator(BaseOperator):
1107
1155
  :waiter_max_attempts: Number of times the waiter should poll the application to check the state.
1108
1156
  If not set, the waiter will use its default value.
1109
1157
  :param waiter_delay: Number of seconds between polling the state of the job run.
1158
+ :param deferrable: If True, the operator will wait asynchronously for the crawl to complete.
1159
+ This implies waiting for completion. This mode requires aiobotocore module to be installed.
1160
+ (default: False, but can be overridden in config file by setting default_deferrable to True)
1110
1161
  """
1111
1162
 
1112
1163
  template_fields: Sequence[str] = (
@@ -1137,6 +1188,7 @@ class EmrServerlessStartJobOperator(BaseOperator):
1137
1188
  waiter_check_interval_seconds: int | ArgNotSet = NOTSET,
1138
1189
  waiter_max_attempts: int | ArgNotSet = NOTSET,
1139
1190
  waiter_delay: int | ArgNotSet = NOTSET,
1191
+ deferrable: bool = conf.getboolean("operators", "default_deferrable", fallback=False),
1140
1192
  **kwargs,
1141
1193
  ):
1142
1194
  if waiter_check_interval_seconds is NOTSET:
@@ -1171,6 +1223,7 @@ class EmrServerlessStartJobOperator(BaseOperator):
1171
1223
  self.waiter_max_attempts = int(waiter_max_attempts) # type: ignore[arg-type]
1172
1224
  self.waiter_delay = int(waiter_delay) # type: ignore[arg-type]
1173
1225
  self.job_id: str | None = None
1226
+ self.deferrable = deferrable
1174
1227
  super().__init__(**kwargs)
1175
1228
 
1176
1229
  self.client_request_token = client_request_token or str(uuid4())
@@ -1180,14 +1233,25 @@ class EmrServerlessStartJobOperator(BaseOperator):
1180
1233
  """Create and return an EmrServerlessHook."""
1181
1234
  return EmrServerlessHook(aws_conn_id=self.aws_conn_id)
1182
1235
 
1183
- def execute(self, context: Context) -> str | None:
1184
- self.log.info("Starting job on Application: %s", self.application_id)
1236
+ def execute(self, context: Context, event: dict[str, Any] | None = None) -> str | None:
1185
1237
 
1186
1238
  app_state = self.hook.conn.get_application(applicationId=self.application_id)["application"]["state"]
1187
1239
  if app_state not in EmrServerlessHook.APPLICATION_SUCCESS_STATES:
1240
+ self.log.info("Application state is %s", app_state)
1241
+ self.log.info("Starting application %s", self.application_id)
1188
1242
  self.hook.conn.start_application(applicationId=self.application_id)
1189
1243
  waiter = self.hook.get_waiter("serverless_app_started")
1190
-
1244
+ if self.deferrable:
1245
+ self.defer(
1246
+ trigger=EmrServerlessStartApplicationTrigger(
1247
+ application_id=self.application_id,
1248
+ waiter_delay=self.waiter_delay,
1249
+ waiter_max_attempts=self.waiter_max_attempts,
1250
+ aws_conn_id=self.aws_conn_id,
1251
+ ),
1252
+ method_name="execute",
1253
+ timeout=timedelta(seconds=self.waiter_max_attempts * self.waiter_delay),
1254
+ )
1191
1255
  wait(
1192
1256
  waiter=waiter,
1193
1257
  waiter_max_attempts=self.waiter_max_attempts,
@@ -1197,7 +1261,7 @@ class EmrServerlessStartJobOperator(BaseOperator):
1197
1261
  status_message="Serverless Application status is",
1198
1262
  status_args=["application.state", "application.stateDetails"],
1199
1263
  )
1200
-
1264
+ self.log.info("Starting job on Application: %s", self.application_id)
1201
1265
  response = self.hook.conn.start_job_run(
1202
1266
  clientToken=self.client_request_token,
1203
1267
  applicationId=self.application_id,
@@ -1213,6 +1277,18 @@ class EmrServerlessStartJobOperator(BaseOperator):
1213
1277
 
1214
1278
  self.job_id = response["jobRunId"]
1215
1279
  self.log.info("EMR serverless job started: %s", self.job_id)
1280
+ if self.deferrable:
1281
+ self.defer(
1282
+ trigger=EmrServerlessStartJobTrigger(
1283
+ application_id=self.application_id,
1284
+ job_id=self.job_id,
1285
+ waiter_delay=self.waiter_delay,
1286
+ waiter_max_attempts=self.waiter_max_attempts,
1287
+ aws_conn_id=self.aws_conn_id,
1288
+ ),
1289
+ method_name="execute_complete",
1290
+ timeout=timedelta(seconds=self.waiter_max_attempts * self.waiter_delay),
1291
+ )
1216
1292
  if self.wait_for_completion:
1217
1293
  waiter = self.hook.get_waiter("serverless_job_completed")
1218
1294
  wait(
@@ -1227,8 +1303,20 @@ class EmrServerlessStartJobOperator(BaseOperator):
1227
1303
 
1228
1304
  return self.job_id
1229
1305
 
1306
+ def execute_complete(self, context: Context, event: dict[str, Any] | None = None) -> None:
1307
+ if event is None:
1308
+ self.log.error("Trigger error: event is None")
1309
+ raise AirflowException("Trigger error: event is None")
1310
+ elif event["status"] == "success":
1311
+ self.log.info("Serverless job completed")
1312
+ return event["job_id"]
1313
+
1230
1314
  def on_kill(self) -> None:
1231
- """Cancel the submitted job run."""
1315
+ """
1316
+ Cancel the submitted job run.
1317
+
1318
+ Note: this method will not run in deferrable mode.
1319
+ """
1232
1320
  if self.job_id:
1233
1321
  self.log.info("Stopping job run with jobId - %s", self.job_id)
1234
1322
  response = self.hook.conn.cancel_job_run(applicationId=self.application_id, jobRunId=self.job_id)
@@ -1270,14 +1358,21 @@ class EmrServerlessStopApplicationOperator(BaseOperator):
1270
1358
  :param application_id: ID of the EMR Serverless application to stop.
1271
1359
  :param wait_for_completion: If true, wait for the Application to stop before returning. Default to True
1272
1360
  :param aws_conn_id: AWS connection to use
1273
- :param waiter_countdown: Total amount of time, in seconds, the operator will wait for
1361
+ :param waiter_countdown: (deprecated) Total amount of time, in seconds, the operator will wait for
1274
1362
  the application be stopped. Defaults to 5 minutes.
1275
- :param waiter_check_interval_seconds: Number of seconds between polling the state of the application.
1276
- Defaults to 30 seconds.
1363
+ :param waiter_check_interval_seconds: (deprecated) Number of seconds between polling the state of the
1364
+ application. Defaults to 60 seconds.
1277
1365
  :param force_stop: If set to True, any job for that app that is not in a terminal state will be cancelled.
1278
1366
  Otherwise, trying to stop an app with running jobs will return an error.
1279
1367
  If you want to wait for the jobs to finish gracefully, use
1280
1368
  :class:`airflow.providers.amazon.aws.sensors.emr.EmrServerlessJobSensor`
1369
+ :waiter_max_attempts: Number of times the waiter should poll the application to check the state.
1370
+ Default is 25.
1371
+ :param waiter_delay: Number of seconds between polling the state of the application.
1372
+ Default is 60 seconds.
1373
+ :param deferrable: If True, the operator will wait asynchronously for the application to stop.
1374
+ This implies waiting for completion. This mode requires aiobotocore module to be installed.
1375
+ (default: False, but can be overridden in config file by setting default_deferrable to True)
1281
1376
  """
1282
1377
 
1283
1378
  template_fields: Sequence[str] = ("application_id",)
@@ -1292,6 +1387,7 @@ class EmrServerlessStopApplicationOperator(BaseOperator):
1292
1387
  waiter_max_attempts: int | ArgNotSet = NOTSET,
1293
1388
  waiter_delay: int | ArgNotSet = NOTSET,
1294
1389
  force_stop: bool = False,
1390
+ deferrable: bool = conf.getboolean("operators", "default_deferrable", fallback=False),
1295
1391
  **kwargs,
1296
1392
  ):
1297
1393
  if waiter_check_interval_seconds is NOTSET:
@@ -1317,10 +1413,11 @@ class EmrServerlessStopApplicationOperator(BaseOperator):
1317
1413
  )
1318
1414
  self.aws_conn_id = aws_conn_id
1319
1415
  self.application_id = application_id
1320
- self.wait_for_completion = wait_for_completion
1416
+ self.wait_for_completion = False if deferrable else wait_for_completion
1321
1417
  self.waiter_max_attempts = int(waiter_max_attempts) # type: ignore[arg-type]
1322
1418
  self.waiter_delay = int(waiter_delay) # type: ignore[arg-type]
1323
1419
  self.force_stop = force_stop
1420
+ self.deferrable = deferrable
1324
1421
  super().__init__(**kwargs)
1325
1422
 
1326
1423
  @cached_property
@@ -1332,16 +1429,46 @@ class EmrServerlessStopApplicationOperator(BaseOperator):
1332
1429
  self.log.info("Stopping application: %s", self.application_id)
1333
1430
 
1334
1431
  if self.force_stop:
1335
- self.hook.cancel_running_jobs(
1336
- self.application_id,
1337
- waiter_config={
1338
- "Delay": self.waiter_delay,
1339
- "MaxAttempts": self.waiter_max_attempts,
1340
- },
1432
+ count = self.hook.cancel_running_jobs(
1433
+ application_id=self.application_id,
1434
+ wait_for_completion=False,
1341
1435
  )
1436
+ if count > 0:
1437
+ self.log.info("now waiting for the %s cancelled job(s) to terminate", count)
1438
+ if self.deferrable:
1439
+ self.defer(
1440
+ trigger=EmrServerlessCancelJobsTrigger(
1441
+ application_id=self.application_id,
1442
+ aws_conn_id=self.aws_conn_id,
1443
+ waiter_delay=self.waiter_delay,
1444
+ waiter_max_attempts=self.waiter_max_attempts,
1445
+ ),
1446
+ timeout=timedelta(seconds=self.waiter_max_attempts * self.waiter_delay),
1447
+ method_name="stop_application",
1448
+ )
1449
+ self.hook.get_waiter("no_job_running").wait(
1450
+ applicationId=self.application_id,
1451
+ states=list(self.hook.JOB_INTERMEDIATE_STATES.union({"CANCELLING"})),
1452
+ WaiterConfig={
1453
+ "Delay": self.waiter_delay,
1454
+ "MaxAttempts": self.waiter_max_attempts,
1455
+ },
1456
+ )
1457
+ else:
1458
+ self.log.info("no running jobs found with application ID %s", self.application_id)
1342
1459
 
1343
1460
  self.hook.conn.stop_application(applicationId=self.application_id)
1344
-
1461
+ if self.deferrable:
1462
+ self.defer(
1463
+ trigger=EmrServerlessStopApplicationTrigger(
1464
+ application_id=self.application_id,
1465
+ aws_conn_id=self.aws_conn_id,
1466
+ waiter_delay=self.waiter_delay,
1467
+ waiter_max_attempts=self.waiter_max_attempts,
1468
+ ),
1469
+ timeout=timedelta(seconds=self.waiter_max_attempts * self.waiter_delay),
1470
+ method_name="execute_complete",
1471
+ )
1345
1472
  if self.wait_for_completion:
1346
1473
  waiter = self.hook.get_waiter("serverless_app_stopped")
1347
1474
  wait(
@@ -1355,6 +1482,30 @@ class EmrServerlessStopApplicationOperator(BaseOperator):
1355
1482
  )
1356
1483
  self.log.info("EMR serverless application %s stopped successfully", self.application_id)
1357
1484
 
1485
+ def stop_application(self, context: Context, event: dict[str, Any] | None = None) -> None:
1486
+ if event is None:
1487
+ self.log.error("Trigger error: event is None")
1488
+ raise AirflowException("Trigger error: event is None")
1489
+ elif event["status"] == "success":
1490
+ self.hook.conn.stop_application(applicationId=self.application_id)
1491
+ self.defer(
1492
+ trigger=EmrServerlessStopApplicationTrigger(
1493
+ application_id=self.application_id,
1494
+ aws_conn_id=self.aws_conn_id,
1495
+ waiter_delay=self.waiter_delay,
1496
+ waiter_max_attempts=self.waiter_max_attempts,
1497
+ ),
1498
+ timeout=timedelta(seconds=self.waiter_max_attempts * self.waiter_delay),
1499
+ method_name="execute_complete",
1500
+ )
1501
+
1502
+ def execute_complete(self, context: Context, event: dict[str, Any] | None = None) -> None:
1503
+ if event is None:
1504
+ self.log.error("Trigger error: event is None")
1505
+ raise AirflowException("Trigger error: event is None")
1506
+ elif event["status"] == "success":
1507
+ self.log.info("EMR serverless application %s stopped successfully", self.application_id)
1508
+
1358
1509
 
1359
1510
  class EmrServerlessDeleteApplicationOperator(EmrServerlessStopApplicationOperator):
1360
1511
  """
@@ -1368,10 +1519,17 @@ class EmrServerlessDeleteApplicationOperator(EmrServerlessStopApplicationOperato
1368
1519
  :param wait_for_completion: If true, wait for the Application to be deleted before returning.
1369
1520
  Defaults to True. Note that this operator will always wait for the application to be STOPPED first.
1370
1521
  :param aws_conn_id: AWS connection to use
1371
- :param waiter_countdown: Total amount of time, in seconds, the operator will wait for each step of first,
1372
- the application to be stopped, and then deleted. Defaults to 25 minutes.
1373
- :param waiter_check_interval_seconds: Number of seconds between polling the state of the application.
1522
+ :param waiter_countdown: (deprecated) Total amount of time, in seconds, the operator will wait for each
1523
+ step of first,the application to be stopped, and then deleted. Defaults to 25 minutes.
1524
+ :param waiter_check_interval_seconds: (deprecated) Number of seconds between polling the state
1525
+ of the application. Defaults to 60 seconds.
1526
+ :waiter_max_attempts: Number of times the waiter should poll the application to check the state.
1527
+ Defaults to 25.
1528
+ :param waiter_delay: Number of seconds between polling the state of the application.
1374
1529
  Defaults to 60 seconds.
1530
+ :param deferrable: If True, the operator will wait asynchronously for application to be deleted.
1531
+ This implies waiting for completion. This mode requires aiobotocore module to be installed.
1532
+ (default: False, but can be overridden in config file by setting default_deferrable to True)
1375
1533
  :param force_stop: If set to True, any job for that app that is not in a terminal state will be cancelled.
1376
1534
  Otherwise, trying to delete an app with running jobs will return an error.
1377
1535
  If you want to wait for the jobs to finish gracefully, use
@@ -1390,6 +1548,7 @@ class EmrServerlessDeleteApplicationOperator(EmrServerlessStopApplicationOperato
1390
1548
  waiter_max_attempts: int | ArgNotSet = NOTSET,
1391
1549
  waiter_delay: int | ArgNotSet = NOTSET,
1392
1550
  force_stop: bool = False,
1551
+ deferrable: bool = conf.getboolean("operators", "default_deferrable", fallback=False),
1393
1552
  **kwargs,
1394
1553
  ):
1395
1554
  if waiter_check_interval_seconds is NOTSET:
@@ -1425,6 +1584,8 @@ class EmrServerlessDeleteApplicationOperator(EmrServerlessStopApplicationOperato
1425
1584
  force_stop=force_stop,
1426
1585
  **kwargs,
1427
1586
  )
1587
+ self.deferrable = deferrable
1588
+ self.wait_for_delete_completion = False if deferrable else wait_for_completion
1428
1589
 
1429
1590
  def execute(self, context: Context) -> None:
1430
1591
  # super stops the app (or makes sure it's already stopped)
@@ -1436,7 +1597,19 @@ class EmrServerlessDeleteApplicationOperator(EmrServerlessStopApplicationOperato
1436
1597
  if response["ResponseMetadata"]["HTTPStatusCode"] != 200:
1437
1598
  raise AirflowException(f"Application deletion failed: {response}")
1438
1599
 
1439
- if self.wait_for_delete_completion:
1600
+ if self.deferrable:
1601
+ self.defer(
1602
+ trigger=EmrServerlessDeleteApplicationTrigger(
1603
+ application_id=self.application_id,
1604
+ aws_conn_id=self.aws_conn_id,
1605
+ waiter_delay=self.waiter_delay,
1606
+ waiter_max_attempts=self.waiter_max_attempts,
1607
+ ),
1608
+ timeout=timedelta(seconds=self.waiter_max_attempts * self.waiter_delay),
1609
+ method_name="execute_complete",
1610
+ )
1611
+
1612
+ elif self.wait_for_delete_completion:
1440
1613
  waiter = self.hook.get_waiter("serverless_app_terminated")
1441
1614
 
1442
1615
  wait(
@@ -1450,3 +1623,10 @@ class EmrServerlessDeleteApplicationOperator(EmrServerlessStopApplicationOperato
1450
1623
  )
1451
1624
 
1452
1625
  self.log.info("EMR serverless application deleted")
1626
+
1627
+ def execute_complete(self, context: Context, event: dict[str, Any] | None = None) -> None:
1628
+ if event is None:
1629
+ self.log.error("Trigger error: event is None")
1630
+ raise AirflowException("Trigger error: event is None")
1631
+ elif event["status"] == "success":
1632
+ self.log.info("EMR serverless application %s deleted successfully", self.application_id)
@@ -0,0 +1,87 @@
1
+ # Licensed to the Apache Software Foundation (ASF) under one
2
+ # or more contributor license agreements. See the NOTICE file
3
+ # distributed with this work for additional information
4
+ # regarding copyright ownership. The ASF licenses this file
5
+ # to you under the Apache License, Version 2.0 (the
6
+ # "License"); you may not use this file except in compliance
7
+ # with the License. You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing,
12
+ # software distributed under the License is distributed on an
13
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
+ # KIND, either express or implied. See the License for the
15
+ # specific language governing permissions and limitations
16
+ # under the License.
17
+ from __future__ import annotations
18
+
19
+ from functools import cached_property
20
+ from typing import TYPE_CHECKING, Sequence
21
+
22
+ from airflow import AirflowException
23
+ from airflow.models import BaseOperator
24
+ from airflow.providers.amazon.aws.hooks.eventbridge import EventBridgeHook
25
+ from airflow.providers.amazon.aws.utils import trim_none_values
26
+
27
+ if TYPE_CHECKING:
28
+ from airflow.utils.context import Context
29
+
30
+
31
+ class EventBridgePutEventsOperator(BaseOperator):
32
+ """
33
+ Put Events onto Amazon EventBridge.
34
+
35
+ :param entries: the list of events to be put onto EventBridge, each event is a dict (required)
36
+ :param endpoint_id: the URL subdomain of the endpoint
37
+ :param aws_conn_id: the AWS connection to use
38
+ :param region_name: the region where events are to be sent
39
+
40
+ """
41
+
42
+ template_fields: Sequence[str] = ("entries", "endpoint_id", "aws_conn_id", "region_name")
43
+
44
+ def __init__(
45
+ self,
46
+ *,
47
+ entries: list[dict],
48
+ endpoint_id: str | None = None,
49
+ aws_conn_id: str = "aws_default",
50
+ region_name: str | None = None,
51
+ **kwargs,
52
+ ):
53
+ super().__init__(**kwargs)
54
+ self.entries = entries
55
+ self.endpoint_id = endpoint_id
56
+ self.aws_conn_id = aws_conn_id
57
+ self.region_name = region_name
58
+
59
+ @cached_property
60
+ def hook(self) -> EventBridgeHook:
61
+ """Create and return an EventBridgeHook."""
62
+ return EventBridgeHook(aws_conn_id=self.aws_conn_id, region_name=self.region_name)
63
+
64
+ def execute(self, context: Context):
65
+
66
+ response = self.hook.conn.put_events(
67
+ **trim_none_values(
68
+ {
69
+ "Entries": self.entries,
70
+ "EndpointId": self.endpoint_id,
71
+ }
72
+ )
73
+ )
74
+
75
+ self.log.info("Sent %d events to EventBridge.", len(self.entries))
76
+
77
+ if response.get("FailedEntryCount"):
78
+ for event in response["Entries"]:
79
+ if "ErrorCode" in event:
80
+ self.log.error(event)
81
+
82
+ raise AirflowException(
83
+ f"{response['FailedEntryCount']} entries in this request have failed to send."
84
+ )
85
+
86
+ if self.do_xcom_push:
87
+ return [e["EventId"] for e in response["Entries"]]