apache-airflow-providers-microsoft-azure 10.0.0rc1__py3-none-any.whl → 10.1.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/microsoft/azure/__init__.py +5 -8
- airflow/providers/microsoft/azure/fs/adls.py +56 -6
- airflow/providers/microsoft/azure/get_provider_info.py +27 -2
- airflow/providers/microsoft/azure/hooks/msgraph.py +353 -0
- airflow/providers/microsoft/azure/hooks/synapse.py +1 -0
- airflow/providers/microsoft/azure/hooks/wasb.py +3 -31
- airflow/providers/microsoft/azure/operators/container_instances.py +17 -0
- airflow/providers/microsoft/azure/operators/msgraph.py +281 -0
- airflow/providers/microsoft/azure/sensors/msgraph.py +177 -0
- airflow/providers/microsoft/azure/triggers/msgraph.py +231 -0
- airflow/providers/microsoft/azure/utils.py +34 -0
- {apache_airflow_providers_microsoft_azure-10.0.0rc1.dist-info → apache_airflow_providers_microsoft_azure-10.1.0.dist-info}/METADATA +10 -8
- {apache_airflow_providers_microsoft_azure-10.0.0rc1.dist-info → apache_airflow_providers_microsoft_azure-10.1.0.dist-info}/RECORD +15 -12
- airflow/providers/microsoft/azure/serialization/__init__.py +0 -16
- {apache_airflow_providers_microsoft_azure-10.0.0rc1.dist-info → apache_airflow_providers_microsoft_azure-10.1.0.dist-info}/WHEEL +0 -0
- {apache_airflow_providers_microsoft_azure-10.0.0rc1.dist-info → apache_airflow_providers_microsoft_azure-10.1.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,281 @@
|
|
1
|
+
#
|
2
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
3
|
+
# or more contributor license agreements. See the NOTICE file
|
4
|
+
# distributed with this work for additional information
|
5
|
+
# regarding copyright ownership. The ASF licenses this file
|
6
|
+
# to you under the Apache License, Version 2.0 (the
|
7
|
+
# "License"); you may not use this file except in compliance
|
8
|
+
# with the License. You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing,
|
13
|
+
# software distributed under the License is distributed on an
|
14
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
15
|
+
# KIND, either express or implied. See the License for the
|
16
|
+
# specific language governing permissions and limitations
|
17
|
+
# under the License.
|
18
|
+
from __future__ import annotations
|
19
|
+
|
20
|
+
from copy import deepcopy
|
21
|
+
from typing import (
|
22
|
+
TYPE_CHECKING,
|
23
|
+
Any,
|
24
|
+
Callable,
|
25
|
+
Sequence,
|
26
|
+
)
|
27
|
+
|
28
|
+
from airflow.exceptions import AirflowException, TaskDeferred
|
29
|
+
from airflow.models import BaseOperator
|
30
|
+
from airflow.providers.microsoft.azure.hooks.msgraph import KiotaRequestAdapterHook
|
31
|
+
from airflow.providers.microsoft.azure.triggers.msgraph import (
|
32
|
+
MSGraphTrigger,
|
33
|
+
ResponseSerializer,
|
34
|
+
)
|
35
|
+
from airflow.utils.xcom import XCOM_RETURN_KEY
|
36
|
+
|
37
|
+
if TYPE_CHECKING:
|
38
|
+
from io import BytesIO
|
39
|
+
|
40
|
+
from kiota_abstractions.request_adapter import ResponseType
|
41
|
+
from kiota_abstractions.request_information import QueryParams
|
42
|
+
from msgraph_core import APIVersion
|
43
|
+
|
44
|
+
from airflow.utils.context import Context
|
45
|
+
|
46
|
+
|
47
|
+
class MSGraphAsyncOperator(BaseOperator):
|
48
|
+
"""
|
49
|
+
A Microsoft Graph API operator which allows you to execute REST call to the Microsoft Graph API.
|
50
|
+
|
51
|
+
https://learn.microsoft.com/en-us/graph/use-the-api
|
52
|
+
|
53
|
+
.. seealso::
|
54
|
+
For more information on how to use this operator, take a look at the guide:
|
55
|
+
:ref:`howto/operator:MSGraphAsyncOperator`
|
56
|
+
|
57
|
+
:param url: The url being executed on the Microsoft Graph API (templated).
|
58
|
+
:param response_type: The expected return type of the response as a string. Possible value are: `bytes`,
|
59
|
+
`str`, `int`, `float`, `bool` and `datetime` (default is None).
|
60
|
+
:param method: The HTTP method being used to do the REST call (default is GET).
|
61
|
+
:param conn_id: The HTTP Connection ID to run the operator against (templated).
|
62
|
+
:param key: The key that will be used to store `XCom's` ("return_value" is default).
|
63
|
+
:param timeout: The HTTP timeout being used by the `KiotaRequestAdapter` (default is None).
|
64
|
+
When no timeout is specified or set to None then there is no HTTP timeout on each request.
|
65
|
+
:param proxies: A dict defining the HTTP proxies to be used (default is None).
|
66
|
+
:param api_version: The API version of the Microsoft Graph API to be used (default is v1).
|
67
|
+
You can pass an enum named APIVersion which has 2 possible members v1 and beta,
|
68
|
+
or you can pass a string as `v1.0` or `beta`.
|
69
|
+
:param result_processor: Function to further process the response from MS Graph API
|
70
|
+
(default is lambda: context, response: response). When the response returned by the
|
71
|
+
`KiotaRequestAdapterHook` are bytes, then those will be base64 encoded into a string.
|
72
|
+
:param serializer: Class which handles response serialization (default is ResponseSerializer).
|
73
|
+
Bytes will be base64 encoded into a string, so it can be stored as an XCom.
|
74
|
+
"""
|
75
|
+
|
76
|
+
template_fields: Sequence[str] = (
|
77
|
+
"url",
|
78
|
+
"response_type",
|
79
|
+
"path_parameters",
|
80
|
+
"url_template",
|
81
|
+
"query_parameters",
|
82
|
+
"headers",
|
83
|
+
"data",
|
84
|
+
"conn_id",
|
85
|
+
)
|
86
|
+
|
87
|
+
def __init__(
|
88
|
+
self,
|
89
|
+
*,
|
90
|
+
url: str,
|
91
|
+
response_type: ResponseType | None = None,
|
92
|
+
path_parameters: dict[str, Any] | None = None,
|
93
|
+
url_template: str | None = None,
|
94
|
+
method: str = "GET",
|
95
|
+
query_parameters: dict[str, QueryParams] | None = None,
|
96
|
+
headers: dict[str, str] | None = None,
|
97
|
+
data: dict[str, Any] | str | BytesIO | None = None,
|
98
|
+
conn_id: str = KiotaRequestAdapterHook.default_conn_name,
|
99
|
+
key: str = XCOM_RETURN_KEY,
|
100
|
+
timeout: float | None = None,
|
101
|
+
proxies: dict | None = None,
|
102
|
+
api_version: APIVersion | None = None,
|
103
|
+
pagination_function: Callable[[MSGraphAsyncOperator, dict], tuple[str, dict]] | None = None,
|
104
|
+
result_processor: Callable[[Context, Any], Any] = lambda context, result: result,
|
105
|
+
serializer: type[ResponseSerializer] = ResponseSerializer,
|
106
|
+
**kwargs: Any,
|
107
|
+
):
|
108
|
+
super().__init__(**kwargs)
|
109
|
+
self.url = url
|
110
|
+
self.response_type = response_type
|
111
|
+
self.path_parameters = path_parameters
|
112
|
+
self.url_template = url_template
|
113
|
+
self.method = method
|
114
|
+
self.query_parameters = query_parameters
|
115
|
+
self.headers = headers
|
116
|
+
self.data = data
|
117
|
+
self.conn_id = conn_id
|
118
|
+
self.key = key
|
119
|
+
self.timeout = timeout
|
120
|
+
self.proxies = proxies
|
121
|
+
self.api_version = api_version
|
122
|
+
self.pagination_function = pagination_function or self.paginate
|
123
|
+
self.result_processor = result_processor
|
124
|
+
self.serializer: ResponseSerializer = serializer()
|
125
|
+
self.results: list[Any] | None = None
|
126
|
+
|
127
|
+
def execute(self, context: Context) -> None:
|
128
|
+
self.defer(
|
129
|
+
trigger=MSGraphTrigger(
|
130
|
+
url=self.url,
|
131
|
+
response_type=self.response_type,
|
132
|
+
path_parameters=self.path_parameters,
|
133
|
+
url_template=self.url_template,
|
134
|
+
method=self.method,
|
135
|
+
query_parameters=self.query_parameters,
|
136
|
+
headers=self.headers,
|
137
|
+
data=self.data,
|
138
|
+
conn_id=self.conn_id,
|
139
|
+
timeout=self.timeout,
|
140
|
+
proxies=self.proxies,
|
141
|
+
api_version=self.api_version,
|
142
|
+
serializer=type(self.serializer),
|
143
|
+
),
|
144
|
+
method_name=self.execute_complete.__name__,
|
145
|
+
)
|
146
|
+
|
147
|
+
def execute_complete(
|
148
|
+
self,
|
149
|
+
context: Context,
|
150
|
+
event: dict[Any, Any] | None = None,
|
151
|
+
) -> Any:
|
152
|
+
"""
|
153
|
+
Execute callback when MSGraphTrigger finishes execution.
|
154
|
+
|
155
|
+
This method gets executed automatically when MSGraphTrigger completes its execution.
|
156
|
+
"""
|
157
|
+
self.log.debug("context: %s", context)
|
158
|
+
|
159
|
+
if event:
|
160
|
+
self.log.debug("%s completed with %s: %s", self.task_id, event.get("status"), event)
|
161
|
+
|
162
|
+
if event.get("status") == "failure":
|
163
|
+
raise AirflowException(event.get("message"))
|
164
|
+
|
165
|
+
response = event.get("response")
|
166
|
+
|
167
|
+
self.log.debug("response: %s", response)
|
168
|
+
|
169
|
+
if response:
|
170
|
+
response = self.serializer.deserialize(response)
|
171
|
+
|
172
|
+
self.log.debug("deserialize response: %s", response)
|
173
|
+
|
174
|
+
result = self.result_processor(context, response)
|
175
|
+
|
176
|
+
self.log.debug("processed response: %s", result)
|
177
|
+
|
178
|
+
event["response"] = result
|
179
|
+
|
180
|
+
try:
|
181
|
+
self.trigger_next_link(response, method_name=self.pull_execute_complete.__name__)
|
182
|
+
except TaskDeferred as exception:
|
183
|
+
self.append_result(
|
184
|
+
result=result,
|
185
|
+
append_result_as_list_if_absent=True,
|
186
|
+
)
|
187
|
+
self.push_xcom(context=context, value=self.results)
|
188
|
+
raise exception
|
189
|
+
|
190
|
+
self.append_result(result=result)
|
191
|
+
self.log.debug("results: %s", self.results)
|
192
|
+
|
193
|
+
return self.results
|
194
|
+
return None
|
195
|
+
|
196
|
+
def append_result(
|
197
|
+
self,
|
198
|
+
result: Any,
|
199
|
+
append_result_as_list_if_absent: bool = False,
|
200
|
+
):
|
201
|
+
self.log.debug("value: %s", result)
|
202
|
+
|
203
|
+
if isinstance(self.results, list):
|
204
|
+
if isinstance(result, list):
|
205
|
+
self.results.extend(result)
|
206
|
+
else:
|
207
|
+
self.results.append(result)
|
208
|
+
else:
|
209
|
+
if append_result_as_list_if_absent:
|
210
|
+
if isinstance(result, list):
|
211
|
+
self.results = result
|
212
|
+
else:
|
213
|
+
self.results = [result]
|
214
|
+
else:
|
215
|
+
self.results = result
|
216
|
+
|
217
|
+
def push_xcom(self, context: Context, value) -> None:
|
218
|
+
self.log.debug("do_xcom_push: %s", self.do_xcom_push)
|
219
|
+
if self.do_xcom_push:
|
220
|
+
self.log.info("Pushing XCom with key '%s': %s", self.key, value)
|
221
|
+
self.xcom_push(context=context, key=self.key, value=value)
|
222
|
+
|
223
|
+
def pull_execute_complete(self, context: Context, event: dict[Any, Any] | None = None) -> Any:
|
224
|
+
self.results = list(
|
225
|
+
self.xcom_pull(
|
226
|
+
context=context,
|
227
|
+
task_ids=self.task_id,
|
228
|
+
dag_id=self.dag_id,
|
229
|
+
key=self.key,
|
230
|
+
)
|
231
|
+
or []
|
232
|
+
)
|
233
|
+
self.log.info(
|
234
|
+
"Pulled XCom with task_id '%s' and dag_id '%s' and key '%s': %s",
|
235
|
+
self.task_id,
|
236
|
+
self.dag_id,
|
237
|
+
self.key,
|
238
|
+
self.results,
|
239
|
+
)
|
240
|
+
return self.execute_complete(context, event)
|
241
|
+
|
242
|
+
@staticmethod
|
243
|
+
def paginate(operator: MSGraphAsyncOperator, response: dict) -> tuple[Any, dict[str, Any] | None]:
|
244
|
+
odata_count = response.get("@odata.count")
|
245
|
+
if odata_count and operator.query_parameters:
|
246
|
+
query_parameters = deepcopy(operator.query_parameters)
|
247
|
+
top = query_parameters.get("$top")
|
248
|
+
odata_count = response.get("@odata.count")
|
249
|
+
|
250
|
+
if top and odata_count:
|
251
|
+
if len(response.get("value", [])) == top:
|
252
|
+
skip = (
|
253
|
+
sum(map(lambda result: len(result["value"]), operator.results)) + top
|
254
|
+
if operator.results
|
255
|
+
else top
|
256
|
+
)
|
257
|
+
query_parameters["$skip"] = skip
|
258
|
+
return operator.url, query_parameters
|
259
|
+
return response.get("@odata.nextLink"), operator.query_parameters
|
260
|
+
|
261
|
+
def trigger_next_link(self, response, method_name="execute_complete") -> None:
|
262
|
+
if isinstance(response, dict):
|
263
|
+
url, query_parameters = self.pagination_function(self, response)
|
264
|
+
|
265
|
+
self.log.debug("url: %s", url)
|
266
|
+
self.log.debug("query_parameters: %s", query_parameters)
|
267
|
+
|
268
|
+
if url:
|
269
|
+
self.defer(
|
270
|
+
trigger=MSGraphTrigger(
|
271
|
+
url=url,
|
272
|
+
query_parameters=query_parameters,
|
273
|
+
response_type=self.response_type,
|
274
|
+
conn_id=self.conn_id,
|
275
|
+
timeout=self.timeout,
|
276
|
+
proxies=self.proxies,
|
277
|
+
api_version=self.api_version,
|
278
|
+
serializer=type(self.serializer),
|
279
|
+
),
|
280
|
+
method_name=method_name,
|
281
|
+
)
|
@@ -0,0 +1,177 @@
|
|
1
|
+
#
|
2
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
3
|
+
# or more contributor license agreements. See the NOTICE file
|
4
|
+
# distributed with this work for additional information
|
5
|
+
# regarding copyright ownership. The ASF licenses this file
|
6
|
+
# to you under the Apache License, Version 2.0 (the
|
7
|
+
# "License"); you may not use this file except in compliance
|
8
|
+
# with the License. You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing,
|
13
|
+
# software distributed under the License is distributed on an
|
14
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
15
|
+
# KIND, either express or implied. See the License for the
|
16
|
+
# specific language governing permissions and limitations
|
17
|
+
# under the License.
|
18
|
+
from __future__ import annotations
|
19
|
+
|
20
|
+
from typing import TYPE_CHECKING, Any, Callable, Sequence
|
21
|
+
|
22
|
+
from airflow.exceptions import AirflowException
|
23
|
+
from airflow.providers.microsoft.azure.hooks.msgraph import KiotaRequestAdapterHook
|
24
|
+
from airflow.providers.microsoft.azure.triggers.msgraph import MSGraphTrigger, ResponseSerializer
|
25
|
+
from airflow.sensors.base import BaseSensorOperator
|
26
|
+
from airflow.triggers.temporal import TimeDeltaTrigger
|
27
|
+
|
28
|
+
if TYPE_CHECKING:
|
29
|
+
from datetime import timedelta
|
30
|
+
from io import BytesIO
|
31
|
+
|
32
|
+
from kiota_abstractions.request_information import QueryParams
|
33
|
+
from kiota_http.httpx_request_adapter import ResponseType
|
34
|
+
from msgraph_core import APIVersion
|
35
|
+
|
36
|
+
from airflow.utils.context import Context
|
37
|
+
|
38
|
+
|
39
|
+
class MSGraphSensor(BaseSensorOperator):
|
40
|
+
"""
|
41
|
+
A Microsoft Graph API sensor which allows you to poll an async REST call to the Microsoft Graph API.
|
42
|
+
|
43
|
+
:param url: The url being executed on the Microsoft Graph API (templated).
|
44
|
+
:param response_type: The expected return type of the response as a string. Possible value are: `bytes`,
|
45
|
+
`str`, `int`, `float`, `bool` and `datetime` (default is None).
|
46
|
+
:param method: The HTTP method being used to do the REST call (default is GET).
|
47
|
+
:param conn_id: The HTTP Connection ID to run the operator against (templated).
|
48
|
+
:param proxies: A dict defining the HTTP proxies to be used (default is None).
|
49
|
+
:param api_version: The API version of the Microsoft Graph API to be used (default is v1).
|
50
|
+
You can pass an enum named APIVersion which has 2 possible members v1 and beta,
|
51
|
+
or you can pass a string as `v1.0` or `beta`.
|
52
|
+
:param event_processor: Function which checks the response from MS Graph API (default is the
|
53
|
+
`default_event_processor` method) and returns a boolean. When the result is True, the sensor
|
54
|
+
will stop poking, otherwise it will continue until it's True or times out.
|
55
|
+
:param result_processor: Function to further process the response from MS Graph API
|
56
|
+
(default is lambda: context, response: response). When the response returned by the
|
57
|
+
`KiotaRequestAdapterHook` are bytes, then those will be base64 encoded into a string.
|
58
|
+
:param serializer: Class which handles response serialization (default is ResponseSerializer).
|
59
|
+
Bytes will be base64 encoded into a string, so it can be stored as an XCom.
|
60
|
+
"""
|
61
|
+
|
62
|
+
template_fields: Sequence[str] = (
|
63
|
+
"url",
|
64
|
+
"response_type",
|
65
|
+
"path_parameters",
|
66
|
+
"url_template",
|
67
|
+
"query_parameters",
|
68
|
+
"headers",
|
69
|
+
"data",
|
70
|
+
"conn_id",
|
71
|
+
)
|
72
|
+
|
73
|
+
def __init__(
|
74
|
+
self,
|
75
|
+
url: str,
|
76
|
+
response_type: ResponseType | None = None,
|
77
|
+
path_parameters: dict[str, Any] | None = None,
|
78
|
+
url_template: str | None = None,
|
79
|
+
method: str = "GET",
|
80
|
+
query_parameters: dict[str, QueryParams] | None = None,
|
81
|
+
headers: dict[str, str] | None = None,
|
82
|
+
data: dict[str, Any] | str | BytesIO | None = None,
|
83
|
+
conn_id: str = KiotaRequestAdapterHook.default_conn_name,
|
84
|
+
proxies: dict | None = None,
|
85
|
+
api_version: APIVersion | None = None,
|
86
|
+
event_processor: Callable[[Context, Any], bool] = lambda context, e: e.get("status") == "Succeeded",
|
87
|
+
result_processor: Callable[[Context, Any], Any] = lambda context, result: result,
|
88
|
+
serializer: type[ResponseSerializer] = ResponseSerializer,
|
89
|
+
retry_delay: timedelta | float = 60,
|
90
|
+
**kwargs,
|
91
|
+
):
|
92
|
+
super().__init__(retry_delay=retry_delay, **kwargs)
|
93
|
+
self.url = url
|
94
|
+
self.response_type = response_type
|
95
|
+
self.path_parameters = path_parameters
|
96
|
+
self.url_template = url_template
|
97
|
+
self.method = method
|
98
|
+
self.query_parameters = query_parameters
|
99
|
+
self.headers = headers
|
100
|
+
self.data = data
|
101
|
+
self.conn_id = conn_id
|
102
|
+
self.proxies = proxies
|
103
|
+
self.api_version = api_version
|
104
|
+
self.event_processor = event_processor
|
105
|
+
self.result_processor = result_processor
|
106
|
+
self.serializer = serializer()
|
107
|
+
|
108
|
+
def execute(self, context: Context):
|
109
|
+
self.defer(
|
110
|
+
trigger=MSGraphTrigger(
|
111
|
+
url=self.url,
|
112
|
+
response_type=self.response_type,
|
113
|
+
path_parameters=self.path_parameters,
|
114
|
+
url_template=self.url_template,
|
115
|
+
method=self.method,
|
116
|
+
query_parameters=self.query_parameters,
|
117
|
+
headers=self.headers,
|
118
|
+
data=self.data,
|
119
|
+
conn_id=self.conn_id,
|
120
|
+
timeout=self.timeout,
|
121
|
+
proxies=self.proxies,
|
122
|
+
api_version=self.api_version,
|
123
|
+
serializer=type(self.serializer),
|
124
|
+
),
|
125
|
+
method_name=self.execute_complete.__name__,
|
126
|
+
)
|
127
|
+
|
128
|
+
def retry_execute(
|
129
|
+
self,
|
130
|
+
context: Context,
|
131
|
+
) -> Any:
|
132
|
+
self.execute(context=context)
|
133
|
+
|
134
|
+
def execute_complete(
|
135
|
+
self,
|
136
|
+
context: Context,
|
137
|
+
event: dict[Any, Any] | None = None,
|
138
|
+
) -> Any:
|
139
|
+
"""
|
140
|
+
Execute callback when MSGraphSensor finishes execution.
|
141
|
+
|
142
|
+
This method gets executed automatically when MSGraphTrigger completes its execution.
|
143
|
+
"""
|
144
|
+
self.log.debug("context: %s", context)
|
145
|
+
|
146
|
+
if event:
|
147
|
+
self.log.debug("%s completed with %s: %s", self.task_id, event.get("status"), event)
|
148
|
+
|
149
|
+
if event.get("status") == "failure":
|
150
|
+
raise AirflowException(event.get("message"))
|
151
|
+
|
152
|
+
response = event.get("response")
|
153
|
+
|
154
|
+
self.log.debug("response: %s", response)
|
155
|
+
|
156
|
+
if response:
|
157
|
+
response = self.serializer.deserialize(response)
|
158
|
+
|
159
|
+
self.log.debug("deserialize response: %s", response)
|
160
|
+
|
161
|
+
is_done = self.event_processor(context, response)
|
162
|
+
|
163
|
+
self.log.debug("is_done: %s", is_done)
|
164
|
+
|
165
|
+
if is_done:
|
166
|
+
result = self.result_processor(context, response)
|
167
|
+
|
168
|
+
self.log.debug("processed response: %s", result)
|
169
|
+
|
170
|
+
return result
|
171
|
+
|
172
|
+
self.defer(
|
173
|
+
trigger=TimeDeltaTrigger(self.retry_delay),
|
174
|
+
method_name=self.retry_execute.__name__,
|
175
|
+
)
|
176
|
+
|
177
|
+
return None
|
@@ -0,0 +1,231 @@
|
|
1
|
+
#
|
2
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
3
|
+
# or more contributor license agreements. See the NOTICE file
|
4
|
+
# distributed with this work for additional information
|
5
|
+
# regarding copyright ownership. The ASF licenses this file
|
6
|
+
# to you under the Apache License, Version 2.0 (the
|
7
|
+
# "License"); you may not use this file except in compliance
|
8
|
+
# with the License. You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing,
|
13
|
+
# software distributed under the License is distributed on an
|
14
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
15
|
+
# KIND, either express or implied. See the License for the
|
16
|
+
# specific language governing permissions and limitations
|
17
|
+
# under the License.
|
18
|
+
from __future__ import annotations
|
19
|
+
|
20
|
+
import json
|
21
|
+
import locale
|
22
|
+
from base64 import b64encode
|
23
|
+
from contextlib import suppress
|
24
|
+
from datetime import datetime
|
25
|
+
from json import JSONDecodeError
|
26
|
+
from typing import (
|
27
|
+
TYPE_CHECKING,
|
28
|
+
Any,
|
29
|
+
AsyncIterator,
|
30
|
+
Sequence,
|
31
|
+
)
|
32
|
+
from uuid import UUID
|
33
|
+
|
34
|
+
import pendulum
|
35
|
+
|
36
|
+
from airflow.providers.microsoft.azure.hooks.msgraph import KiotaRequestAdapterHook
|
37
|
+
from airflow.triggers.base import BaseTrigger, TriggerEvent
|
38
|
+
from airflow.utils.module_loading import import_string
|
39
|
+
|
40
|
+
if TYPE_CHECKING:
|
41
|
+
from io import BytesIO
|
42
|
+
|
43
|
+
from kiota_abstractions.request_adapter import RequestAdapter
|
44
|
+
from kiota_abstractions.request_information import QueryParams
|
45
|
+
from kiota_http.httpx_request_adapter import ResponseType
|
46
|
+
from msgraph_core import APIVersion
|
47
|
+
|
48
|
+
|
49
|
+
class ResponseSerializer:
|
50
|
+
"""ResponseSerializer serializes the response as a string."""
|
51
|
+
|
52
|
+
def __init__(self, encoding: str | None = None):
|
53
|
+
self.encoding = encoding or locale.getpreferredencoding()
|
54
|
+
|
55
|
+
def serialize(self, response) -> str | None:
|
56
|
+
def convert(value) -> str | None:
|
57
|
+
if value is not None:
|
58
|
+
if isinstance(value, UUID):
|
59
|
+
return str(value)
|
60
|
+
if isinstance(value, datetime):
|
61
|
+
return value.isoformat()
|
62
|
+
if isinstance(value, pendulum.DateTime):
|
63
|
+
return value.to_iso8601_string() # Adjust the format as needed
|
64
|
+
raise TypeError(f"Object of type {type(value)} is not JSON serializable!")
|
65
|
+
return None
|
66
|
+
|
67
|
+
if response is not None:
|
68
|
+
if isinstance(response, bytes):
|
69
|
+
return b64encode(response).decode(self.encoding)
|
70
|
+
with suppress(JSONDecodeError):
|
71
|
+
return json.dumps(response, default=convert)
|
72
|
+
return response
|
73
|
+
return None
|
74
|
+
|
75
|
+
def deserialize(self, response) -> Any:
|
76
|
+
if isinstance(response, str):
|
77
|
+
with suppress(JSONDecodeError):
|
78
|
+
response = json.loads(response)
|
79
|
+
return response
|
80
|
+
|
81
|
+
|
82
|
+
class MSGraphTrigger(BaseTrigger):
|
83
|
+
"""
|
84
|
+
A Microsoft Graph API trigger which allows you to execute an async REST call to the Microsoft Graph API.
|
85
|
+
|
86
|
+
:param url: The url being executed on the Microsoft Graph API (templated).
|
87
|
+
:param response_type: The expected return type of the response as a string. Possible value are: `bytes`,
|
88
|
+
`str`, `int`, `float`, `bool` and `datetime` (default is None).
|
89
|
+
:param method: The HTTP method being used to do the REST call (default is GET).
|
90
|
+
:param conn_id: The HTTP Connection ID to run the operator against (templated).
|
91
|
+
:param timeout: The HTTP timeout being used by the `KiotaRequestAdapter` (default is None).
|
92
|
+
When no timeout is specified or set to None then there is no HTTP timeout on each request.
|
93
|
+
:param proxies: A dict defining the HTTP proxies to be used (default is None).
|
94
|
+
:param api_version: The API version of the Microsoft Graph API to be used (default is v1).
|
95
|
+
You can pass an enum named APIVersion which has 2 possible members v1 and beta,
|
96
|
+
or you can pass a string as `v1.0` or `beta`.
|
97
|
+
:param serializer: Class which handles response serialization (default is ResponseSerializer).
|
98
|
+
Bytes will be base64 encoded into a string, so it can be stored as an XCom.
|
99
|
+
"""
|
100
|
+
|
101
|
+
template_fields: Sequence[str] = (
|
102
|
+
"url",
|
103
|
+
"response_type",
|
104
|
+
"path_parameters",
|
105
|
+
"url_template",
|
106
|
+
"query_parameters",
|
107
|
+
"headers",
|
108
|
+
"data",
|
109
|
+
"conn_id",
|
110
|
+
)
|
111
|
+
|
112
|
+
def __init__(
|
113
|
+
self,
|
114
|
+
url: str,
|
115
|
+
response_type: ResponseType | None = None,
|
116
|
+
path_parameters: dict[str, Any] | None = None,
|
117
|
+
url_template: str | None = None,
|
118
|
+
method: str = "GET",
|
119
|
+
query_parameters: dict[str, QueryParams] | None = None,
|
120
|
+
headers: dict[str, str] | None = None,
|
121
|
+
data: dict[str, Any] | str | BytesIO | None = None,
|
122
|
+
conn_id: str = KiotaRequestAdapterHook.default_conn_name,
|
123
|
+
timeout: float | None = None,
|
124
|
+
proxies: dict | None = None,
|
125
|
+
api_version: APIVersion | None = None,
|
126
|
+
serializer: type[ResponseSerializer] = ResponseSerializer,
|
127
|
+
):
|
128
|
+
super().__init__()
|
129
|
+
self.hook = KiotaRequestAdapterHook(
|
130
|
+
conn_id=conn_id,
|
131
|
+
timeout=timeout,
|
132
|
+
proxies=proxies,
|
133
|
+
api_version=api_version,
|
134
|
+
)
|
135
|
+
self.url = url
|
136
|
+
self.response_type = response_type
|
137
|
+
self.path_parameters = path_parameters
|
138
|
+
self.url_template = url_template
|
139
|
+
self.method = method
|
140
|
+
self.query_parameters = query_parameters
|
141
|
+
self.headers = headers
|
142
|
+
self.data = data
|
143
|
+
self.serializer: ResponseSerializer = self.resolve_type(serializer, default=ResponseSerializer)()
|
144
|
+
|
145
|
+
@classmethod
|
146
|
+
def resolve_type(cls, value: str | type, default) -> type:
|
147
|
+
if isinstance(value, str):
|
148
|
+
with suppress(ImportError):
|
149
|
+
return import_string(value)
|
150
|
+
return default
|
151
|
+
return value or default
|
152
|
+
|
153
|
+
def serialize(self) -> tuple[str, dict[str, Any]]:
|
154
|
+
"""Serialize the HttpTrigger arguments and classpath."""
|
155
|
+
api_version = self.api_version.value if self.api_version else None
|
156
|
+
return (
|
157
|
+
f"{self.__class__.__module__}.{self.__class__.__name__}",
|
158
|
+
{
|
159
|
+
"conn_id": self.conn_id,
|
160
|
+
"timeout": self.timeout,
|
161
|
+
"proxies": self.proxies,
|
162
|
+
"api_version": api_version,
|
163
|
+
"serializer": f"{self.serializer.__class__.__module__}.{self.serializer.__class__.__name__}",
|
164
|
+
"url": self.url,
|
165
|
+
"path_parameters": self.path_parameters,
|
166
|
+
"url_template": self.url_template,
|
167
|
+
"method": self.method,
|
168
|
+
"query_parameters": self.query_parameters,
|
169
|
+
"headers": self.headers,
|
170
|
+
"data": self.data,
|
171
|
+
"response_type": self.response_type,
|
172
|
+
},
|
173
|
+
)
|
174
|
+
|
175
|
+
def get_conn(self) -> RequestAdapter:
|
176
|
+
return self.hook.get_conn()
|
177
|
+
|
178
|
+
@property
|
179
|
+
def conn_id(self) -> str:
|
180
|
+
return self.hook.conn_id
|
181
|
+
|
182
|
+
@property
|
183
|
+
def timeout(self) -> float | None:
|
184
|
+
return self.hook.timeout
|
185
|
+
|
186
|
+
@property
|
187
|
+
def proxies(self) -> dict | None:
|
188
|
+
return self.hook.proxies
|
189
|
+
|
190
|
+
@property
|
191
|
+
def api_version(self) -> APIVersion:
|
192
|
+
return self.hook.api_version
|
193
|
+
|
194
|
+
async def run(self) -> AsyncIterator[TriggerEvent]:
|
195
|
+
"""Make a series of asynchronous HTTP calls via a KiotaRequestAdapterHook."""
|
196
|
+
try:
|
197
|
+
response = await self.hook.run(
|
198
|
+
url=self.url,
|
199
|
+
response_type=self.response_type,
|
200
|
+
path_parameters=self.path_parameters,
|
201
|
+
method=self.method,
|
202
|
+
query_parameters=self.query_parameters,
|
203
|
+
headers=self.headers,
|
204
|
+
data=self.data,
|
205
|
+
)
|
206
|
+
|
207
|
+
self.log.debug("response: %s", response)
|
208
|
+
|
209
|
+
if response:
|
210
|
+
response_type = type(response)
|
211
|
+
|
212
|
+
self.log.debug("response type: %s", response_type)
|
213
|
+
|
214
|
+
yield TriggerEvent(
|
215
|
+
{
|
216
|
+
"status": "success",
|
217
|
+
"type": f"{response_type.__module__}.{response_type.__name__}",
|
218
|
+
"response": self.serializer.serialize(response),
|
219
|
+
}
|
220
|
+
)
|
221
|
+
else:
|
222
|
+
yield TriggerEvent(
|
223
|
+
{
|
224
|
+
"status": "success",
|
225
|
+
"type": None,
|
226
|
+
"response": None,
|
227
|
+
}
|
228
|
+
)
|
229
|
+
except Exception as e:
|
230
|
+
self.log.exception("An error occurred: %s", e)
|
231
|
+
yield TriggerEvent({"status": "failure", "message": str(e)})
|