apache-airflow-providers-amazon 9.9.0rc1__py3-none-any.whl → 9.10.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/amazon/__init__.py +1 -1
- airflow/providers/amazon/aws/auth_manager/avp/facade.py +8 -1
- airflow/providers/amazon/aws/auth_manager/aws_auth_manager.py +0 -55
- airflow/providers/amazon/aws/bundles/__init__.py +16 -0
- airflow/providers/amazon/aws/bundles/s3.py +152 -0
- airflow/providers/amazon/aws/executors/batch/batch_executor.py +51 -0
- airflow/providers/amazon/aws/executors/ecs/utils.py +2 -2
- airflow/providers/amazon/aws/executors/utils/exponential_backoff_retry.py +1 -1
- airflow/providers/amazon/aws/fs/s3.py +2 -1
- airflow/providers/amazon/aws/hooks/athena_sql.py +12 -2
- airflow/providers/amazon/aws/hooks/base_aws.py +29 -17
- airflow/providers/amazon/aws/hooks/batch_client.py +2 -1
- airflow/providers/amazon/aws/hooks/batch_waiters.py +2 -1
- airflow/providers/amazon/aws/hooks/chime.py +5 -1
- airflow/providers/amazon/aws/hooks/ec2.py +2 -1
- airflow/providers/amazon/aws/hooks/eks.py +1 -2
- airflow/providers/amazon/aws/hooks/glue.py +82 -7
- airflow/providers/amazon/aws/hooks/rds.py +2 -1
- airflow/providers/amazon/aws/hooks/s3.py +86 -3
- airflow/providers/amazon/aws/hooks/sagemaker.py +2 -2
- airflow/providers/amazon/aws/hooks/sagemaker_unified_studio.py +1 -1
- airflow/providers/amazon/aws/links/base_aws.py +2 -10
- airflow/providers/amazon/aws/operators/base_aws.py +1 -1
- airflow/providers/amazon/aws/operators/batch.py +6 -22
- airflow/providers/amazon/aws/operators/ecs.py +1 -1
- airflow/providers/amazon/aws/operators/glue.py +23 -8
- airflow/providers/amazon/aws/operators/redshift_data.py +1 -1
- airflow/providers/amazon/aws/operators/sagemaker.py +2 -2
- airflow/providers/amazon/aws/operators/sagemaker_unified_studio.py +1 -1
- airflow/providers/amazon/aws/sensors/base_aws.py +1 -1
- airflow/providers/amazon/aws/sensors/glue.py +57 -12
- airflow/providers/amazon/aws/sensors/s3.py +2 -2
- airflow/providers/amazon/aws/sensors/sagemaker_unified_studio.py +1 -1
- airflow/providers/amazon/aws/transfers/azure_blob_to_s3.py +1 -1
- airflow/providers/amazon/aws/transfers/base.py +1 -1
- airflow/providers/amazon/aws/transfers/dynamodb_to_s3.py +2 -2
- airflow/providers/amazon/aws/transfers/exasol_to_s3.py +1 -1
- airflow/providers/amazon/aws/transfers/ftp_to_s3.py +1 -1
- airflow/providers/amazon/aws/transfers/gcs_to_s3.py +1 -1
- airflow/providers/amazon/aws/transfers/glacier_to_gcs.py +1 -1
- airflow/providers/amazon/aws/transfers/google_api_to_s3.py +6 -2
- airflow/providers/amazon/aws/transfers/hive_to_dynamodb.py +3 -3
- airflow/providers/amazon/aws/transfers/http_to_s3.py +1 -1
- airflow/providers/amazon/aws/transfers/imap_attachment_to_s3.py +1 -1
- airflow/providers/amazon/aws/transfers/local_to_s3.py +1 -1
- airflow/providers/amazon/aws/transfers/mongo_to_s3.py +1 -1
- airflow/providers/amazon/aws/transfers/redshift_to_s3.py +1 -1
- airflow/providers/amazon/aws/transfers/s3_to_dynamodb.py +1 -1
- airflow/providers/amazon/aws/transfers/s3_to_ftp.py +1 -1
- airflow/providers/amazon/aws/transfers/s3_to_redshift.py +1 -1
- airflow/providers/amazon/aws/transfers/s3_to_sftp.py +1 -1
- airflow/providers/amazon/aws/transfers/s3_to_sql.py +3 -4
- airflow/providers/amazon/aws/transfers/salesforce_to_s3.py +1 -1
- airflow/providers/amazon/aws/transfers/sftp_to_s3.py +1 -1
- airflow/providers/amazon/aws/transfers/sql_to_s3.py +2 -5
- airflow/providers/amazon/aws/triggers/base.py +0 -1
- airflow/providers/amazon/aws/triggers/glue.py +37 -24
- airflow/providers/amazon/aws/utils/connection_wrapper.py +10 -1
- airflow/providers/amazon/aws/utils/suppress.py +2 -1
- airflow/providers/amazon/aws/utils/waiter.py +1 -1
- airflow/providers/amazon/aws/waiters/glue.json +55 -0
- airflow/providers/amazon/version_compat.py +24 -0
- {apache_airflow_providers_amazon-9.9.0rc1.dist-info → apache_airflow_providers_amazon-9.10.0.dist-info}/METADATA +14 -15
- {apache_airflow_providers_amazon-9.9.0rc1.dist-info → apache_airflow_providers_amazon-9.10.0.dist-info}/RECORD +66 -64
- {apache_airflow_providers_amazon-9.9.0rc1.dist-info → apache_airflow_providers_amazon-9.10.0.dist-info}/WHEEL +0 -0
- {apache_airflow_providers_amazon-9.9.0rc1.dist-info → apache_airflow_providers_amazon-9.10.0.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.
|
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:
|
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
|
30
|
+
from typing import TYPE_CHECKING, Any
|
31
31
|
|
32
32
|
from inflection import camelize
|
33
33
|
|
@@ -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
|
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
|
-
|
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,
|
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,
|
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
|
-
|
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
|
620
|
-
|
621
|
-
|
622
|
-
|
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,
|
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
|
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
|
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.
|
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
|
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
|