apache-airflow-providers-amazon 9.9.0rc1__py3-none-any.whl → 9.10.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 (66) hide show
  1. airflow/providers/amazon/__init__.py +1 -1
  2. airflow/providers/amazon/aws/auth_manager/avp/facade.py +8 -1
  3. airflow/providers/amazon/aws/auth_manager/aws_auth_manager.py +0 -55
  4. airflow/providers/amazon/aws/bundles/__init__.py +16 -0
  5. airflow/providers/amazon/aws/bundles/s3.py +152 -0
  6. airflow/providers/amazon/aws/executors/batch/batch_executor.py +51 -0
  7. airflow/providers/amazon/aws/executors/ecs/utils.py +2 -2
  8. airflow/providers/amazon/aws/executors/utils/exponential_backoff_retry.py +1 -1
  9. airflow/providers/amazon/aws/fs/s3.py +2 -1
  10. airflow/providers/amazon/aws/hooks/athena_sql.py +12 -2
  11. airflow/providers/amazon/aws/hooks/base_aws.py +29 -17
  12. airflow/providers/amazon/aws/hooks/batch_client.py +2 -1
  13. airflow/providers/amazon/aws/hooks/batch_waiters.py +2 -1
  14. airflow/providers/amazon/aws/hooks/chime.py +5 -1
  15. airflow/providers/amazon/aws/hooks/ec2.py +2 -1
  16. airflow/providers/amazon/aws/hooks/eks.py +1 -2
  17. airflow/providers/amazon/aws/hooks/glue.py +82 -7
  18. airflow/providers/amazon/aws/hooks/rds.py +2 -1
  19. airflow/providers/amazon/aws/hooks/s3.py +86 -3
  20. airflow/providers/amazon/aws/hooks/sagemaker.py +2 -2
  21. airflow/providers/amazon/aws/hooks/sagemaker_unified_studio.py +1 -1
  22. airflow/providers/amazon/aws/links/base_aws.py +2 -10
  23. airflow/providers/amazon/aws/operators/base_aws.py +1 -1
  24. airflow/providers/amazon/aws/operators/batch.py +6 -22
  25. airflow/providers/amazon/aws/operators/ecs.py +1 -1
  26. airflow/providers/amazon/aws/operators/glue.py +23 -8
  27. airflow/providers/amazon/aws/operators/redshift_data.py +1 -1
  28. airflow/providers/amazon/aws/operators/sagemaker.py +2 -2
  29. airflow/providers/amazon/aws/operators/sagemaker_unified_studio.py +1 -1
  30. airflow/providers/amazon/aws/sensors/base_aws.py +1 -1
  31. airflow/providers/amazon/aws/sensors/glue.py +57 -12
  32. airflow/providers/amazon/aws/sensors/s3.py +2 -2
  33. airflow/providers/amazon/aws/sensors/sagemaker_unified_studio.py +1 -1
  34. airflow/providers/amazon/aws/transfers/azure_blob_to_s3.py +1 -1
  35. airflow/providers/amazon/aws/transfers/base.py +1 -1
  36. airflow/providers/amazon/aws/transfers/dynamodb_to_s3.py +2 -2
  37. airflow/providers/amazon/aws/transfers/exasol_to_s3.py +1 -1
  38. airflow/providers/amazon/aws/transfers/ftp_to_s3.py +1 -1
  39. airflow/providers/amazon/aws/transfers/gcs_to_s3.py +1 -1
  40. airflow/providers/amazon/aws/transfers/glacier_to_gcs.py +1 -1
  41. airflow/providers/amazon/aws/transfers/google_api_to_s3.py +6 -2
  42. airflow/providers/amazon/aws/transfers/hive_to_dynamodb.py +3 -3
  43. airflow/providers/amazon/aws/transfers/http_to_s3.py +1 -1
  44. airflow/providers/amazon/aws/transfers/imap_attachment_to_s3.py +1 -1
  45. airflow/providers/amazon/aws/transfers/local_to_s3.py +1 -1
  46. airflow/providers/amazon/aws/transfers/mongo_to_s3.py +1 -1
  47. airflow/providers/amazon/aws/transfers/redshift_to_s3.py +1 -1
  48. airflow/providers/amazon/aws/transfers/s3_to_dynamodb.py +1 -1
  49. airflow/providers/amazon/aws/transfers/s3_to_ftp.py +1 -1
  50. airflow/providers/amazon/aws/transfers/s3_to_redshift.py +1 -1
  51. airflow/providers/amazon/aws/transfers/s3_to_sftp.py +1 -1
  52. airflow/providers/amazon/aws/transfers/s3_to_sql.py +3 -4
  53. airflow/providers/amazon/aws/transfers/salesforce_to_s3.py +1 -1
  54. airflow/providers/amazon/aws/transfers/sftp_to_s3.py +1 -1
  55. airflow/providers/amazon/aws/transfers/sql_to_s3.py +2 -5
  56. airflow/providers/amazon/aws/triggers/base.py +0 -1
  57. airflow/providers/amazon/aws/triggers/glue.py +37 -24
  58. airflow/providers/amazon/aws/utils/connection_wrapper.py +10 -1
  59. airflow/providers/amazon/aws/utils/suppress.py +2 -1
  60. airflow/providers/amazon/aws/utils/waiter.py +1 -1
  61. airflow/providers/amazon/aws/waiters/glue.json +55 -0
  62. airflow/providers/amazon/version_compat.py +24 -0
  63. {apache_airflow_providers_amazon-9.9.0rc1.dist-info → apache_airflow_providers_amazon-9.10.0rc1.dist-info}/METADATA +8 -9
  64. {apache_airflow_providers_amazon-9.9.0rc1.dist-info → apache_airflow_providers_amazon-9.10.0rc1.dist-info}/RECORD +66 -64
  65. {apache_airflow_providers_amazon-9.9.0rc1.dist-info → apache_airflow_providers_amazon-9.10.0rc1.dist-info}/WHEEL +0 -0
  66. {apache_airflow_providers_amazon-9.9.0rc1.dist-info → apache_airflow_providers_amazon-9.10.0rc1.dist-info}/entry_points.txt +0 -0
@@ -29,7 +29,7 @@ from airflow import __version__ as airflow_version
29
29
 
30
30
  __all__ = ["__version__"]
31
31
 
32
- __version__ = "9.9.0"
32
+ __version__ = "9.10.0"
33
33
 
34
34
  if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse(
35
35
  "2.10.0"
@@ -37,6 +37,13 @@ from airflow.utils.log.logging_mixin import LoggingMixin
37
37
 
38
38
  if TYPE_CHECKING:
39
39
  from airflow.api_fastapi.auth.managers.base_auth_manager import ResourceMethod
40
+
41
+ try:
42
+ from airflow.api_fastapi.auth.managers.base_auth_manager import ExtendedResourceMethod
43
+ except ImportError:
44
+ from airflow.api_fastapi.auth.managers.base_auth_manager import (
45
+ ResourceMethod as ExtendedResourceMethod,
46
+ )
40
47
  from airflow.providers.amazon.aws.auth_manager.user import AwsAuthManagerUser
41
48
 
42
49
 
@@ -48,7 +55,7 @@ NB_REQUESTS_PER_BATCH = 30
48
55
  class IsAuthorizedRequest(TypedDict, total=False):
49
56
  """Represent the parameters of ``is_authorized`` method in AVP facade."""
50
57
 
51
- method: ResourceMethod
58
+ method: ExtendedResourceMethod
52
59
  entity_type: AvpEntities
53
60
  entity_id: str | None
54
61
  context: dict | None
@@ -44,10 +44,7 @@ from airflow.providers.amazon.version_compat import AIRFLOW_V_3_0_PLUS
44
44
  if TYPE_CHECKING:
45
45
  from airflow.api_fastapi.auth.managers.base_auth_manager import ResourceMethod
46
46
  from airflow.api_fastapi.auth.managers.models.batch_apis import (
47
- IsAuthorizedConnectionRequest,
48
47
  IsAuthorizedDagRequest,
49
- IsAuthorizedPoolRequest,
50
- IsAuthorizedVariableRequest,
51
48
  )
52
49
  from airflow.api_fastapi.auth.managers.models.resource_details import (
53
50
  AccessView,
@@ -247,24 +244,6 @@ class AwsAuthManager(BaseAuthManager[AwsAuthManagerUser]):
247
244
 
248
245
  return [menu_item for menu_item in menu_items if _has_access_to_menu_item(requests[menu_item.value])]
249
246
 
250
- def batch_is_authorized_connection(
251
- self,
252
- requests: Sequence[IsAuthorizedConnectionRequest],
253
- *,
254
- user: AwsAuthManagerUser,
255
- ) -> bool:
256
- facade_requests: Sequence[IsAuthorizedRequest] = [
257
- {
258
- "method": request["method"],
259
- "entity_type": AvpEntities.CONNECTION,
260
- "entity_id": cast("ConnectionDetails", request["details"]).conn_id
261
- if request.get("details")
262
- else None,
263
- }
264
- for request in requests
265
- ]
266
- return self.avp_facade.batch_is_authorized(requests=facade_requests, user=user)
267
-
268
247
  def batch_is_authorized_dag(
269
248
  self,
270
249
  requests: Sequence[IsAuthorizedDagRequest],
@@ -288,40 +267,6 @@ class AwsAuthManager(BaseAuthManager[AwsAuthManagerUser]):
288
267
  ]
289
268
  return self.avp_facade.batch_is_authorized(requests=facade_requests, user=user)
290
269
 
291
- def batch_is_authorized_pool(
292
- self,
293
- requests: Sequence[IsAuthorizedPoolRequest],
294
- *,
295
- user: AwsAuthManagerUser,
296
- ) -> bool:
297
- facade_requests: Sequence[IsAuthorizedRequest] = [
298
- {
299
- "method": request["method"],
300
- "entity_type": AvpEntities.POOL,
301
- "entity_id": cast("PoolDetails", request["details"]).name if request.get("details") else None,
302
- }
303
- for request in requests
304
- ]
305
- return self.avp_facade.batch_is_authorized(requests=facade_requests, user=user)
306
-
307
- def batch_is_authorized_variable(
308
- self,
309
- requests: Sequence[IsAuthorizedVariableRequest],
310
- *,
311
- user: AwsAuthManagerUser,
312
- ) -> bool:
313
- facade_requests: Sequence[IsAuthorizedRequest] = [
314
- {
315
- "method": request["method"],
316
- "entity_type": AvpEntities.VARIABLE,
317
- "entity_id": cast("VariableDetails", request["details"]).key
318
- if request.get("details")
319
- else None,
320
- }
321
- for request in requests
322
- ]
323
- return self.avp_facade.batch_is_authorized(requests=facade_requests, user=user)
324
-
325
270
  def filter_authorized_dag_ids(
326
271
  self,
327
272
  *,
@@ -0,0 +1,16 @@
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.
@@ -0,0 +1,152 @@
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
+ import os
20
+ from pathlib import Path
21
+
22
+ import structlog
23
+
24
+ from airflow.dag_processing.bundles.base import BaseDagBundle
25
+ from airflow.exceptions import AirflowException
26
+ from airflow.providers.amazon.aws.hooks.base_aws import AwsBaseHook
27
+ from airflow.providers.amazon.aws.hooks.s3 import S3Hook
28
+
29
+
30
+ class S3DagBundle(BaseDagBundle):
31
+ """
32
+ S3 DAG bundle - exposes a directory in S3 as a DAG bundle.
33
+
34
+ This allows Airflow to load DAGs directly from an S3 bucket.
35
+
36
+ :param aws_conn_id: Airflow connection ID for AWS. Defaults to AwsBaseHook.default_conn_name.
37
+ :param bucket_name: The name of the S3 bucket containing the DAG files.
38
+ :param prefix: Optional subdirectory within the S3 bucket where the DAGs are stored.
39
+ If None, DAGs are assumed to be at the root of the bucket (Optional).
40
+ """
41
+
42
+ supports_versioning = False
43
+
44
+ def __init__(
45
+ self,
46
+ *,
47
+ aws_conn_id: str = AwsBaseHook.default_conn_name,
48
+ bucket_name: str,
49
+ prefix: str = "",
50
+ **kwargs,
51
+ ) -> None:
52
+ super().__init__(**kwargs)
53
+ self.aws_conn_id = aws_conn_id
54
+ self.bucket_name = bucket_name
55
+ self.prefix = prefix
56
+ # Local path where S3 DAGs are downloaded
57
+ self.s3_dags_dir: Path = self.base_dir
58
+
59
+ log = structlog.get_logger(__name__)
60
+ self._log = log.bind(
61
+ bundle_name=self.name,
62
+ version=self.version,
63
+ bucket_name=self.bucket_name,
64
+ prefix=self.prefix,
65
+ aws_conn_id=self.aws_conn_id,
66
+ )
67
+ self._s3_hook: S3Hook | None = None
68
+
69
+ def _initialize(self):
70
+ with self.lock():
71
+ if not self.s3_dags_dir.exists():
72
+ self._log.info("Creating local DAGs directory: %s", self.s3_dags_dir)
73
+ os.makedirs(self.s3_dags_dir)
74
+
75
+ if not self.s3_dags_dir.is_dir():
76
+ raise AirflowException(f"Local DAGs path: {self.s3_dags_dir} is not a directory.")
77
+
78
+ if not self.s3_hook.check_for_bucket(bucket_name=self.bucket_name):
79
+ raise AirflowException(f"S3 bucket '{self.bucket_name}' does not exist.")
80
+
81
+ if self.prefix:
82
+ # don't check when prefix is ""
83
+ if not self.s3_hook.check_for_prefix(
84
+ bucket_name=self.bucket_name, prefix=self.prefix, delimiter="/"
85
+ ):
86
+ raise AirflowException(
87
+ f"S3 prefix 's3://{self.bucket_name}/{self.prefix}' does not exist."
88
+ )
89
+ self.refresh()
90
+
91
+ def initialize(self) -> None:
92
+ self._initialize()
93
+ super().initialize()
94
+
95
+ @property
96
+ def s3_hook(self):
97
+ if self._s3_hook is None:
98
+ try:
99
+ self._s3_hook: S3Hook = S3Hook(aws_conn_id=self.aws_conn_id) # Initialize S3 hook.
100
+ except AirflowException as e:
101
+ self._log.warning("Could not create S3Hook for connection %s: %s", self.aws_conn_id, e)
102
+ return self._s3_hook
103
+
104
+ def __repr__(self):
105
+ return (
106
+ f"<S3DagBundle("
107
+ f"name={self.name!r}, "
108
+ f"bucket_name={self.bucket_name!r}, "
109
+ f"prefix={self.prefix!r}, "
110
+ f"version={self.version!r}"
111
+ f")>"
112
+ )
113
+
114
+ def get_current_version(self) -> str | None:
115
+ """Return the current version of the DAG bundle. Currently not supported."""
116
+ return None
117
+
118
+ @property
119
+ def path(self) -> Path:
120
+ """Return the local path to the DAG files."""
121
+ return self.s3_dags_dir # Path where DAGs are downloaded.
122
+
123
+ def refresh(self) -> None:
124
+ """Refresh the DAG bundle by re-downloading the DAGs from S3."""
125
+ if self.version:
126
+ raise AirflowException("Refreshing a specific version is not supported")
127
+
128
+ with self.lock():
129
+ self._log.debug(
130
+ "Downloading DAGs from s3://%s/%s to %s", self.bucket_name, self.prefix, self.s3_dags_dir
131
+ )
132
+ self.s3_hook.sync_to_local_dir(
133
+ bucket_name=self.bucket_name,
134
+ s3_prefix=self.prefix,
135
+ local_dir=self.s3_dags_dir,
136
+ delete_stale=True,
137
+ )
138
+
139
+ def view_url(self, version: str | None = None) -> str | None:
140
+ """Return a URL for viewing the DAGs in S3. Currently, versioning is not supported."""
141
+ if self.version:
142
+ raise AirflowException("S3 url with version is not supported")
143
+
144
+ # https://<bucket-name>.s3.<region>.amazonaws.com/<object-key>
145
+ url = f"https://{self.bucket_name}.s3"
146
+ if self.s3_hook.region_name:
147
+ url += f".{self.s3_hook.region_name}"
148
+ url += ".amazonaws.com"
149
+ if self.prefix:
150
+ url += f"/{self.prefix}"
151
+
152
+ return url
@@ -36,11 +36,15 @@ from airflow.providers.amazon.aws.executors.utils.exponential_backoff_retry impo
36
36
  exponential_backoff_retry,
37
37
  )
38
38
  from airflow.providers.amazon.aws.hooks.batch_client import BatchClientHook
39
+ from airflow.providers.amazon.version_compat import AIRFLOW_V_3_0_PLUS
39
40
  from airflow.stats import Stats
40
41
  from airflow.utils import timezone
41
42
  from airflow.utils.helpers import merge_dicts
42
43
 
43
44
  if TYPE_CHECKING:
45
+ from sqlalchemy.orm import Session
46
+
47
+ from airflow.executors import workloads
44
48
  from airflow.models.taskinstance import TaskInstance, TaskInstanceKey
45
49
  from airflow.providers.amazon.aws.executors.batch.boto_schema import (
46
50
  BatchDescribeJobsResponseSchema,
@@ -97,6 +101,11 @@ class AwsBatchExecutor(BaseExecutor):
97
101
  # AWS only allows a maximum number of JOBs in the describe_jobs function
98
102
  DESCRIBE_JOBS_BATCH_SIZE = 99
99
103
 
104
+ if TYPE_CHECKING and AIRFLOW_V_3_0_PLUS:
105
+ # In the v3 path, we store workloads, not commands as strings.
106
+ # TODO: TaskSDK: move this type change into BaseExecutor
107
+ queued_tasks: dict[TaskInstanceKey, workloads.All] # type: ignore[assignment]
108
+
100
109
  def __init__(self, *args, **kwargs):
101
110
  super().__init__(*args, **kwargs)
102
111
  self.active_workers = BatchJobCollection()
@@ -106,6 +115,30 @@ class AwsBatchExecutor(BaseExecutor):
106
115
  self.IS_BOTO_CONNECTION_HEALTHY = False
107
116
  self.submit_job_kwargs = self._load_submit_kwargs()
108
117
 
118
+ def queue_workload(self, workload: workloads.All, session: Session | None) -> None:
119
+ from airflow.executors import workloads
120
+
121
+ if not isinstance(workload, workloads.ExecuteTask):
122
+ raise RuntimeError(f"{type(self)} cannot handle workloads of type {type(workload)}")
123
+ ti = workload.ti
124
+ self.queued_tasks[ti.key] = workload
125
+
126
+ def _process_workloads(self, workloads: Sequence[workloads.All]) -> None:
127
+ from airflow.executors.workloads import ExecuteTask
128
+
129
+ # Airflow V3 version
130
+ for w in workloads:
131
+ if not isinstance(w, ExecuteTask):
132
+ raise RuntimeError(f"{type(self)} cannot handle workloads of type {type(w)}")
133
+ command = [w]
134
+ key = w.ti.key
135
+ queue = w.ti.queue
136
+ executor_config = w.ti.executor_config or {}
137
+
138
+ del self.queued_tasks[key]
139
+ self.execute_async(key=key, command=command, queue=queue, executor_config=executor_config) # type: ignore[arg-type]
140
+ self.running.add(key)
141
+
109
142
  def check_health(self):
110
143
  """Make a test API call to check the health of the Batch Executor."""
111
144
  success_status = "succeeded."
@@ -343,6 +376,24 @@ class AwsBatchExecutor(BaseExecutor):
343
376
  if executor_config and "command" in executor_config:
344
377
  raise ValueError('Executor Config should never override "command"')
345
378
 
379
+ if len(command) == 1:
380
+ from airflow.executors.workloads import ExecuteTask
381
+
382
+ if isinstance(command[0], ExecuteTask):
383
+ workload = command[0]
384
+ ser_input = workload.model_dump_json()
385
+ command = [
386
+ "python",
387
+ "-m",
388
+ "airflow.sdk.execution_time.execute_workload",
389
+ "--json-string",
390
+ ser_input,
391
+ ]
392
+ else:
393
+ raise ValueError(
394
+ f"BatchExecutor doesn't know how to handle workload of type: {type(command[0])}"
395
+ )
396
+
346
397
  self.pending_jobs.append(
347
398
  BatchQueuedJob(
348
399
  key=key,
@@ -25,9 +25,9 @@ from __future__ import annotations
25
25
 
26
26
  import datetime
27
27
  from collections import defaultdict
28
- from collections.abc import Sequence
28
+ from collections.abc import Callable, Sequence
29
29
  from dataclasses import dataclass
30
- from typing import TYPE_CHECKING, Any, Callable
30
+ from typing import TYPE_CHECKING, Any
31
31
 
32
32
  from inflection import camelize
33
33
 
@@ -17,8 +17,8 @@
17
17
  from __future__ import annotations
18
18
 
19
19
  import logging
20
+ from collections.abc import Callable
20
21
  from datetime import datetime, timedelta
21
- from typing import Callable
22
22
 
23
23
  from airflow.utils import timezone
24
24
 
@@ -18,8 +18,9 @@ from __future__ import annotations
18
18
 
19
19
  import asyncio
20
20
  import logging
21
+ from collections.abc import Callable
21
22
  from functools import partial
22
- from typing import TYPE_CHECKING, Any, Callable
23
+ from typing import TYPE_CHECKING, Any
23
24
 
24
25
  import requests
25
26
  from botocore import UNSIGNED
@@ -111,7 +111,14 @@ class AthenaSQLHook(AwsBaseHook, DbApiHook):
111
111
  connection.login = athena_conn.login
112
112
  connection.password = athena_conn.password
113
113
  connection.schema = athena_conn.schema
114
- connection.set_extra(json.dumps({**athena_conn.extra_dejson, **connection.extra_dejson}))
114
+ merged_extra = {**athena_conn.extra_dejson, **connection.extra_dejson}
115
+ try:
116
+ extra_json = json.dumps(merged_extra)
117
+ connection.extra = extra_json
118
+ except (TypeError, ValueError):
119
+ raise ValueError(
120
+ f"Encountered non-JSON in `extra` field for connection {self.aws_conn_id!r}."
121
+ )
115
122
  except AirflowNotFoundException:
116
123
  connection = athena_conn
117
124
  connection.conn_type = "aws"
@@ -120,7 +127,10 @@ class AthenaSQLHook(AwsBaseHook, DbApiHook):
120
127
  )
121
128
 
122
129
  return AwsConnectionWrapper(
123
- conn=connection, region_name=self._region_name, botocore_config=self._config, verify=self._verify
130
+ conn=connection,
131
+ region_name=self._region_name,
132
+ botocore_config=self._config,
133
+ verify=self._verify,
124
134
  )
125
135
 
126
136
  @property
@@ -31,10 +31,11 @@ import json
31
31
  import logging
32
32
  import os
33
33
  import warnings
34
+ from collections.abc import Callable
34
35
  from copy import deepcopy
35
36
  from functools import cached_property, wraps
36
37
  from pathlib import Path
37
- from typing import TYPE_CHECKING, Any, Callable, Generic, TypeVar, Union
38
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar, Union
38
39
 
39
40
  import boto3
40
41
  import botocore
@@ -43,6 +44,8 @@ import jinja2
43
44
  import requests
44
45
  import tenacity
45
46
  from asgiref.sync import sync_to_async
47
+ from boto3.resources.base import ServiceResource
48
+ from botocore.client import BaseClient
46
49
  from botocore.config import Config
47
50
  from botocore.waiter import Waiter, WaiterModel
48
51
  from dateutil.tz import tzlocal
@@ -54,19 +57,28 @@ from airflow.exceptions import (
54
57
  AirflowNotFoundException,
55
58
  AirflowProviderDeprecationWarning,
56
59
  )
57
- from airflow.hooks.base import BaseHook
58
60
  from airflow.providers.amazon.aws.utils.connection_wrapper import AwsConnectionWrapper
59
61
  from airflow.providers.amazon.aws.utils.identifiers import generate_uuid
60
62
  from airflow.providers.amazon.aws.utils.suppress import return_on_error
63
+ from airflow.providers.amazon.version_compat import BaseHook
61
64
  from airflow.providers.common.compat.version_compat import AIRFLOW_V_3_0_PLUS
62
65
  from airflow.providers_manager import ProvidersManager
63
66
  from airflow.utils.helpers import exactly_one
64
67
  from airflow.utils.log.logging_mixin import LoggingMixin
65
68
 
66
- BaseAwsConnection = TypeVar("BaseAwsConnection", bound=Union[boto3.client, boto3.resource])
69
+ # We need to set typeignore, sadly without it Sphinx build and mypy don't agree.
70
+ # ideally the code should be:
71
+ # BaseAwsConnection = TypeVar("BaseAwsConnection", bound=BaseClient | ServiceResource)
72
+ # but if we do that Sphinx complains about:
73
+ # TypeError: unsupported operand type(s) for |: 'BaseClient' and 'ServiceResource'
74
+ # If we change to Union syntax then mypy is not happy with UP007 Use `X | Y` for type annotations
75
+ # The only way to workaround it for now is to keep the union syntax with ignore for mypy
76
+ # We should try to resolve this later.
77
+ BaseAwsConnection = TypeVar("BaseAwsConnection", bound=Union[BaseClient, ServiceResource]) # type: ignore[operator] # noqa: UP007
78
+
67
79
 
68
80
  if AIRFLOW_V_3_0_PLUS:
69
- from airflow.sdk.exceptions import AirflowRuntimeError
81
+ from airflow.sdk.exceptions import AirflowRuntimeError, ErrorType
70
82
 
71
83
  if TYPE_CHECKING:
72
84
  from aiobotocore.session import AioSession
@@ -607,19 +619,16 @@ class AwsGenericHook(BaseHook, Generic[BaseAwsConnection]):
607
619
  """Get the Airflow Connection object and wrap it in helper (cached)."""
608
620
  connection = None
609
621
  if self.aws_conn_id:
610
- possible_exceptions: tuple[type[Exception], ...]
611
-
612
- if AIRFLOW_V_3_0_PLUS:
613
- possible_exceptions = (AirflowNotFoundException, AirflowRuntimeError)
614
- else:
615
- possible_exceptions = (AirflowNotFoundException,)
616
-
617
622
  try:
618
623
  connection = self.get_connection(self.aws_conn_id)
619
- except possible_exceptions as e:
620
- if isinstance(
621
- e, AirflowNotFoundException
622
- ) or f"Connection with ID {self.aws_conn_id} not found" in str(e):
624
+ except Exception as e:
625
+ not_found_exc_via_core = isinstance(e, AirflowNotFoundException)
626
+ not_found_exc_via_task_sdk = (
627
+ AIRFLOW_V_3_0_PLUS
628
+ and isinstance(e, AirflowRuntimeError)
629
+ and e.error.error == ErrorType.CONNECTION_NOT_FOUND
630
+ )
631
+ if not_found_exc_via_core or not_found_exc_via_task_sdk:
623
632
  self.log.warning(
624
633
  "Unable to find AWS Connection ID '%s', switching to empty.", self.aws_conn_id
625
634
  )
@@ -627,7 +636,10 @@ class AwsGenericHook(BaseHook, Generic[BaseAwsConnection]):
627
636
  raise
628
637
 
629
638
  return AwsConnectionWrapper(
630
- conn=connection, region_name=self._region_name, botocore_config=self._config, verify=self._verify
639
+ conn=connection, # type: ignore[arg-type]
640
+ region_name=self._region_name,
641
+ botocore_config=self._config,
642
+ verify=self._verify,
631
643
  )
632
644
 
633
645
  def _resolve_service_name(self, is_resource_type: bool = False) -> str:
@@ -1038,7 +1050,7 @@ class AwsGenericHook(BaseHook, Generic[BaseAwsConnection]):
1038
1050
  return WaiterModel(model_config).waiter_names
1039
1051
 
1040
1052
 
1041
- class AwsBaseHook(AwsGenericHook[Union[boto3.client, boto3.resource]]):
1053
+ class AwsBaseHook(AwsGenericHook[Union[boto3.client, boto3.resource]]): # type: ignore[operator] # noqa: UP007
1042
1054
  """
1043
1055
  Base class for interact with AWS.
1044
1056
 
@@ -30,7 +30,8 @@ from __future__ import annotations
30
30
  import itertools
31
31
  import random
32
32
  import time
33
- from typing import TYPE_CHECKING, Callable, Protocol, runtime_checkable
33
+ from collections.abc import Callable
34
+ from typing import TYPE_CHECKING, Protocol, runtime_checkable
34
35
 
35
36
  import botocore.client
36
37
  import botocore.exceptions
@@ -28,9 +28,10 @@ from __future__ import annotations
28
28
 
29
29
  import json
30
30
  import sys
31
+ from collections.abc import Callable
31
32
  from copy import deepcopy
32
33
  from pathlib import Path
33
- from typing import TYPE_CHECKING, Any, Callable
34
+ from typing import TYPE_CHECKING, Any
34
35
 
35
36
  import botocore.client
36
37
  import botocore.exceptions
@@ -66,9 +66,13 @@ class ChimeWebhookHook(HttpHook):
66
66
  :return: Endpoint(str) for chime webhook.
67
67
  """
68
68
  conn = self.get_connection(conn_id)
69
- token = conn.get_password()
69
+ token = conn.password
70
70
  if token is None:
71
71
  raise AirflowException("Webhook token field is missing and is required.")
72
+ if not conn.schema:
73
+ raise AirflowException("Webook schema field is missing and is required")
74
+ if not conn.host:
75
+ raise AirflowException("Webhook host field is missing and is required.")
72
76
  url = conn.schema + "://" + conn.host
73
77
  endpoint = url + token
74
78
  # Check to make sure the endpoint matches what Chime expects
@@ -19,7 +19,8 @@ from __future__ import annotations
19
19
 
20
20
  import functools
21
21
  import time
22
- from typing import Callable, TypeVar
22
+ from collections.abc import Callable
23
+ from typing import TypeVar
23
24
 
24
25
  from airflow.exceptions import AirflowException
25
26
  from airflow.providers.amazon.aws.hooks.base_aws import AwsBaseHook
@@ -23,11 +23,10 @@ import json
23
23
  import os
24
24
  import sys
25
25
  import tempfile
26
- from collections.abc import Generator
26
+ from collections.abc import Callable, Generator
27
27
  from contextlib import contextmanager
28
28
  from enum import Enum
29
29
  from functools import partial
30
- from typing import Callable
31
30
 
32
31
  from botocore.exceptions import ClientError
33
32
  from botocore.signers import RequestSigner