apache-airflow-providers-teradata 2.3.0__tar.gz → 2.4.0__tar.gz

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 (19) hide show
  1. {apache_airflow_providers_teradata-2.3.0 → apache_airflow_providers_teradata-2.4.0}/PKG-INFO +6 -6
  2. {apache_airflow_providers_teradata-2.3.0 → apache_airflow_providers_teradata-2.4.0}/README.rst +3 -3
  3. {apache_airflow_providers_teradata-2.3.0 → apache_airflow_providers_teradata-2.4.0}/airflow/providers/teradata/__init__.py +1 -1
  4. {apache_airflow_providers_teradata-2.3.0 → apache_airflow_providers_teradata-2.4.0}/airflow/providers/teradata/get_provider_info.py +16 -4
  5. {apache_airflow_providers_teradata-2.3.0 → apache_airflow_providers_teradata-2.4.0}/airflow/providers/teradata/hooks/teradata.py +6 -3
  6. apache_airflow_providers_teradata-2.4.0/airflow/providers/teradata/operators/teradata_compute_cluster.py +513 -0
  7. apache_airflow_providers_teradata-2.4.0/airflow/providers/teradata/triggers/__init__.py +16 -0
  8. apache_airflow_providers_teradata-2.4.0/airflow/providers/teradata/triggers/teradata_compute_cluster.py +155 -0
  9. apache_airflow_providers_teradata-2.4.0/airflow/providers/teradata/utils/__init__.py +16 -0
  10. apache_airflow_providers_teradata-2.4.0/airflow/providers/teradata/utils/constants.py +46 -0
  11. {apache_airflow_providers_teradata-2.3.0 → apache_airflow_providers_teradata-2.4.0}/pyproject.toml +3 -3
  12. {apache_airflow_providers_teradata-2.3.0 → apache_airflow_providers_teradata-2.4.0}/airflow/providers/teradata/LICENSE +0 -0
  13. {apache_airflow_providers_teradata-2.3.0 → apache_airflow_providers_teradata-2.4.0}/airflow/providers/teradata/hooks/__init__.py +0 -0
  14. {apache_airflow_providers_teradata-2.3.0 → apache_airflow_providers_teradata-2.4.0}/airflow/providers/teradata/operators/__init__.py +0 -0
  15. {apache_airflow_providers_teradata-2.3.0 → apache_airflow_providers_teradata-2.4.0}/airflow/providers/teradata/operators/teradata.py +0 -0
  16. {apache_airflow_providers_teradata-2.3.0 → apache_airflow_providers_teradata-2.4.0}/airflow/providers/teradata/transfers/__init__.py +0 -0
  17. {apache_airflow_providers_teradata-2.3.0 → apache_airflow_providers_teradata-2.4.0}/airflow/providers/teradata/transfers/azure_blob_to_teradata.py +0 -0
  18. {apache_airflow_providers_teradata-2.3.0 → apache_airflow_providers_teradata-2.4.0}/airflow/providers/teradata/transfers/s3_to_teradata.py +0 -0
  19. {apache_airflow_providers_teradata-2.3.0 → apache_airflow_providers_teradata-2.4.0}/airflow/providers/teradata/transfers/teradata_to_teradata.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: apache-airflow-providers-teradata
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Summary: Provider package apache-airflow-providers-teradata for Apache Airflow
5
5
  Keywords: airflow-provider,teradata,airflow,integration
6
6
  Author-email: Apache Software Foundation <dev@airflow.apache.org>
@@ -29,8 +29,8 @@ Requires-Dist: apache-airflow-providers-amazon ; extra == "amazon"
29
29
  Requires-Dist: apache-airflow-providers-common-sql ; extra == "common.sql"
30
30
  Requires-Dist: apache-airflow-providers-microsoft-azure ; extra == "microsoft.azure"
31
31
  Project-URL: Bug Tracker, https://github.com/apache/airflow/issues
32
- Project-URL: Changelog, https://airflow.apache.org/docs/apache-airflow-providers-teradata/2.3.0/changelog.html
33
- Project-URL: Documentation, https://airflow.apache.org/docs/apache-airflow-providers-teradata/2.3.0
32
+ Project-URL: Changelog, https://airflow.apache.org/docs/apache-airflow-providers-teradata/2.4.0/changelog.html
33
+ Project-URL: Documentation, https://airflow.apache.org/docs/apache-airflow-providers-teradata/2.4.0
34
34
  Project-URL: Slack Chat, https://s.apache.org/airflow-slack
35
35
  Project-URL: Source Code, https://github.com/apache/airflow
36
36
  Project-URL: Twitter, https://twitter.com/ApacheAirflow
@@ -83,7 +83,7 @@ Provides-Extra: microsoft.azure
83
83
 
84
84
  Package ``apache-airflow-providers-teradata``
85
85
 
86
- Release: ``2.3.0``
86
+ Release: ``2.4.0``
87
87
 
88
88
 
89
89
  `Teradata <https://www.teradata.com/>`__
@@ -96,7 +96,7 @@ This is a provider package for ``teradata`` provider. All classes for this provi
96
96
  are in ``airflow.providers.teradata`` python package.
97
97
 
98
98
  You can find package information and changelog for the provider
99
- in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-teradata/2.3.0/>`_.
99
+ in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-teradata/2.4.0/>`_.
100
100
 
101
101
  Installation
102
102
  ------------
@@ -141,4 +141,4 @@ Dependent package
141
141
  ====================================================================================================================== ===================
142
142
 
143
143
  The changelog for the provider package can be found in the
144
- `changelog <https://airflow.apache.org/docs/apache-airflow-providers-teradata/2.3.0/changelog.html>`_.
144
+ `changelog <https://airflow.apache.org/docs/apache-airflow-providers-teradata/2.4.0/changelog.html>`_.
@@ -42,7 +42,7 @@
42
42
 
43
43
  Package ``apache-airflow-providers-teradata``
44
44
 
45
- Release: ``2.3.0``
45
+ Release: ``2.4.0``
46
46
 
47
47
 
48
48
  `Teradata <https://www.teradata.com/>`__
@@ -55,7 +55,7 @@ This is a provider package for ``teradata`` provider. All classes for this provi
55
55
  are in ``airflow.providers.teradata`` python package.
56
56
 
57
57
  You can find package information and changelog for the provider
58
- in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-teradata/2.3.0/>`_.
58
+ in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-teradata/2.4.0/>`_.
59
59
 
60
60
  Installation
61
61
  ------------
@@ -100,4 +100,4 @@ Dependent package
100
100
  ====================================================================================================================== ===================
101
101
 
102
102
  The changelog for the provider package can be found in the
103
- `changelog <https://airflow.apache.org/docs/apache-airflow-providers-teradata/2.3.0/changelog.html>`_.
103
+ `changelog <https://airflow.apache.org/docs/apache-airflow-providers-teradata/2.4.0/changelog.html>`_.
@@ -29,7 +29,7 @@ from airflow import __version__ as airflow_version
29
29
 
30
30
  __all__ = ["__version__"]
31
31
 
32
- __version__ = "2.3.0"
32
+ __version__ = "2.4.0"
33
33
 
34
34
  if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse(
35
35
  "2.7.0"
@@ -28,8 +28,8 @@ def get_provider_info():
28
28
  "name": "Teradata",
29
29
  "description": "`Teradata <https://www.teradata.com/>`__\n",
30
30
  "state": "ready",
31
- "source-date-epoch": 1719054575,
32
- "versions": ["2.3.0", "2.2.0", "2.1.1", "2.1.0", "2.0.0"],
31
+ "source-date-epoch": 1720423902,
32
+ "versions": ["2.4.0", "2.3.0", "2.2.0", "2.1.1", "2.1.0", "2.0.0"],
33
33
  "dependencies": [
34
34
  "apache-airflow>=2.7.0",
35
35
  "apache-airflow-providers-common-sql>=1.3.1",
@@ -44,7 +44,10 @@ def get_provider_info():
44
44
  {
45
45
  "integration-name": "Teradata",
46
46
  "external-doc-url": "https://www.teradata.com/",
47
- "how-to-guide": ["/docs/apache-airflow-providers-teradata/operators/teradata.rst"],
47
+ "how-to-guide": [
48
+ "/docs/apache-airflow-providers-teradata/operators/teradata.rst",
49
+ "/docs/apache-airflow-providers-teradata/operators/compute_cluster.rst",
50
+ ],
48
51
  "logo": "/integration-logos/teradata/Teradata.png",
49
52
  "tags": ["software"],
50
53
  }
@@ -52,7 +55,10 @@ def get_provider_info():
52
55
  "operators": [
53
56
  {
54
57
  "integration-name": "Teradata",
55
- "python-modules": ["airflow.providers.teradata.operators.teradata"],
58
+ "python-modules": [
59
+ "airflow.providers.teradata.operators.teradata",
60
+ "airflow.providers.teradata.operators.teradata_compute_cluster",
61
+ ],
56
62
  }
57
63
  ],
58
64
  "hooks": [
@@ -84,4 +90,10 @@ def get_provider_info():
84
90
  "connection-type": "teradata",
85
91
  }
86
92
  ],
93
+ "triggers": [
94
+ {
95
+ "integration-name": "Teradata",
96
+ "python-modules": ["airflow.providers.teradata.triggers.teradata_compute_cluster"],
97
+ }
98
+ ],
87
99
  }
@@ -45,7 +45,8 @@ def _map_param(value):
45
45
 
46
46
 
47
47
  class TeradataHook(DbApiHook):
48
- """General hook for interacting with Teradata SQL Database.
48
+ """
49
+ General hook for interacting with Teradata SQL Database.
49
50
 
50
51
  This module contains basic APIs to connect to and interact with Teradata SQL Database. It uses teradatasql
51
52
  client internally as a database driver for connecting to Teradata database. The config parameters like
@@ -96,7 +97,8 @@ class TeradataHook(DbApiHook):
96
97
  super().__init__(*args, schema=database, **kwargs)
97
98
 
98
99
  def get_conn(self) -> TeradataConnection:
99
- """Create and return a Teradata Connection object using teradatasql client.
100
+ """
101
+ Create and return a Teradata Connection object using teradatasql client.
100
102
 
101
103
  Establishes connection to a Teradata SQL database using config corresponding to teradata_conn_id.
102
104
 
@@ -113,7 +115,8 @@ class TeradataHook(DbApiHook):
113
115
  target_fields: list[str] | None = None,
114
116
  commit_every: int = 5000,
115
117
  ):
116
- """Use :func:`insert_rows` instead, this is deprecated.
118
+ """
119
+ Use :func:`insert_rows` instead, this is deprecated.
117
120
 
118
121
  Insert bulk of records into Teradata SQL Database.
119
122
 
@@ -0,0 +1,513 @@
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 re
21
+ from abc import abstractmethod
22
+ from enum import Enum
23
+ from functools import cached_property
24
+ from typing import TYPE_CHECKING
25
+
26
+ from airflow.models import BaseOperator
27
+ from airflow.providers.teradata.hooks.teradata import TeradataHook
28
+ from airflow.providers.teradata.utils.constants import Constants
29
+
30
+ if TYPE_CHECKING:
31
+ from airflow.utils.context import Context
32
+
33
+ from datetime import timedelta
34
+ from typing import TYPE_CHECKING, Any, Sequence, cast
35
+
36
+ from airflow.providers.teradata.triggers.teradata_compute_cluster import TeradataComputeClusterSyncTrigger
37
+
38
+ if TYPE_CHECKING:
39
+ from airflow.utils.context import Context
40
+
41
+ from airflow.exceptions import AirflowException
42
+
43
+
44
+ # Represents
45
+ # 1. Compute Cluster Setup - Provision and Decomission operations
46
+ # 2. Compute Cluster State - Resume and Suspend operations
47
+ class _Operation(Enum):
48
+ SETUP = 1
49
+ STATE = 2
50
+
51
+
52
+ # Handler to handle single result set of a SQL query
53
+ def _single_result_row_handler(cursor):
54
+ records = cursor.fetchone()
55
+ if isinstance(records, list):
56
+ return records[0]
57
+ if records is None:
58
+ return records
59
+ raise TypeError(f"Unexpected results: {cursor.fetchone()!r}")
60
+
61
+
62
+ # Providers given operation is setup or state operation
63
+ def _determine_operation_context(operation):
64
+ if operation == Constants.CC_CREATE_OPR or operation == Constants.CC_DROP_OPR:
65
+ return _Operation.SETUP
66
+ return _Operation.STATE
67
+
68
+
69
+ class _TeradataComputeClusterOperator(BaseOperator):
70
+ """
71
+ Teradata Compute Cluster Base Operator to set up and status operations of compute cluster.
72
+
73
+ :param compute_profile_name: Name of the Compute Profile to manage.
74
+ :param compute_group_name: Name of compute group to which compute profile belongs.
75
+ :param teradata_conn_id: The :ref:`Teradata connection id <howto/connection:teradata>`
76
+ reference to a specific Teradata database.
77
+ :param timeout: Time elapsed before the task times out and fails.
78
+ """
79
+
80
+ template_fields: Sequence[str] = (
81
+ "compute_profile_name",
82
+ "compute_group_name",
83
+ "teradata_conn_id",
84
+ "timeout",
85
+ )
86
+
87
+ ui_color = "#e07c24"
88
+
89
+ def __init__(
90
+ self,
91
+ compute_profile_name: str,
92
+ compute_group_name: str | None = None,
93
+ teradata_conn_id: str = TeradataHook.default_conn_name,
94
+ timeout: int = Constants.CC_OPR_TIME_OUT,
95
+ **kwargs,
96
+ ) -> None:
97
+ super().__init__(**kwargs)
98
+ self.compute_profile_name = compute_profile_name
99
+ self.compute_group_name = compute_group_name
100
+ self.teradata_conn_id = teradata_conn_id
101
+ self.timeout = timeout
102
+
103
+ @cached_property
104
+ def hook(self) -> TeradataHook:
105
+ return TeradataHook(teradata_conn_id=self.teradata_conn_id)
106
+
107
+ @abstractmethod
108
+ def execute(self, context: Context):
109
+ pass
110
+
111
+ def execute_complete(self, context: Context, event: dict[str, Any]) -> None:
112
+ """
113
+ Execute when the trigger fires - returns immediately.
114
+
115
+ Relies on trigger to throw an exception, otherwise it assumes execution was successful.
116
+ """
117
+ self._compute_cluster_execute_complete(event)
118
+
119
+ def _compute_cluster_execute(self):
120
+ # Verifies the provided compute profile name.
121
+ if (
122
+ self.compute_profile_name is None
123
+ or self.compute_profile_name == "None"
124
+ or self.compute_profile_name == ""
125
+ ):
126
+ self.log.info("Invalid compute cluster profile name")
127
+ raise AirflowException(Constants.CC_OPR_EMPTY_PROFILE_ERROR_MSG)
128
+ # Verifies if the provided Teradata instance belongs to Vantage Cloud Lake.
129
+ lake_support_find_sql = "SELECT count(1) from DBC.StorageV WHERE StorageName='TD_OFSSTORAGE'"
130
+ lake_support_result = self.hook.run(lake_support_find_sql, handler=_single_result_row_handler)
131
+ if lake_support_result is None:
132
+ raise AirflowException(Constants.CC_GRP_LAKE_SUPPORT_ONLY_MSG)
133
+ # Getting teradata db version. Considering teradata instance is Lake when db version is 20 or above
134
+ db_version_get_sql = "SELECT InfoData AS Version FROM DBC.DBCInfoV WHERE InfoKey = 'VERSION'"
135
+ try:
136
+ db_version_result = self.hook.run(db_version_get_sql, handler=_single_result_row_handler)
137
+ if db_version_result is not None:
138
+ db_version_result = str(db_version_result)
139
+ db_version = db_version_result.split(".")[0]
140
+ if db_version is not None and int(db_version) < 20:
141
+ raise AirflowException(Constants.CC_GRP_LAKE_SUPPORT_ONLY_MSG)
142
+ else:
143
+ raise AirflowException("Error occurred while getting teradata database version")
144
+ except Exception as ex:
145
+ self.log.error("Error occurred while getting teradata database version: %s ", str(ex))
146
+ raise AirflowException("Error occurred while getting teradata database version")
147
+
148
+ def _compute_cluster_execute_complete(self, event: dict[str, Any]) -> None:
149
+ if event["status"] == "success":
150
+ return event["message"]
151
+ elif event["status"] == "error":
152
+ raise AirflowException(event["message"])
153
+
154
+ def _handle_cc_status(self, operation_type, sql):
155
+ create_sql_result = self._hook_run(sql, handler=_single_result_row_handler)
156
+ self.log.info(
157
+ "%s query ran successfully. Differing to trigger to check status in db. Result from sql: %s",
158
+ operation_type,
159
+ create_sql_result,
160
+ )
161
+ self.defer(
162
+ timeout=timedelta(minutes=self.timeout),
163
+ trigger=TeradataComputeClusterSyncTrigger(
164
+ teradata_conn_id=cast(str, self.teradata_conn_id),
165
+ compute_profile_name=self.compute_profile_name,
166
+ compute_group_name=self.compute_group_name,
167
+ operation_type=operation_type,
168
+ poll_interval=Constants.CC_POLL_INTERVAL,
169
+ ),
170
+ method_name="execute_complete",
171
+ )
172
+
173
+ return create_sql_result
174
+
175
+ def _hook_run(self, query, handler=None):
176
+ try:
177
+ if handler is not None:
178
+ return self.hook.run(query, handler=handler)
179
+ else:
180
+ return self.hook.run(query)
181
+ except Exception as ex:
182
+ self.log.error(str(ex))
183
+ raise
184
+
185
+ def _get_initially_suspended(self, create_cp_query):
186
+ initially_suspended = "FALSE"
187
+ pattern = r"INITIALLY_SUSPENDED\s*\(\s*'(TRUE|FALSE)'\s*\)"
188
+ # Search for the pattern in the input string
189
+ match = re.search(pattern, create_cp_query, re.IGNORECASE)
190
+ if match:
191
+ # Get the value of INITIALLY_SUSPENDED
192
+ initially_suspended = match.group(1).strip().upper()
193
+ return initially_suspended
194
+
195
+
196
+ class TeradataComputeClusterProvisionOperator(_TeradataComputeClusterOperator):
197
+ """
198
+
199
+ Creates the new Computer Cluster with specified Compute Group Name and Compute Profile Name.
200
+
201
+ .. seealso::
202
+ For more information on how to use this operator, take a look at the guide:
203
+ :ref:`howto/operator:TeradataComputeClusterProvisionOperator`
204
+
205
+ :param compute_profile_name: Name of the Compute Profile to manage.
206
+ :param compute_group_name: Name of compute group to which compute profile belongs.
207
+ :param query_strategy: Query strategy to use. Refers to the approach or method used by the
208
+ Teradata Optimizer to execute SQL queries efficiently within a Teradata computer cluster.
209
+ Valid query_strategy value is either 'STANDARD' or 'ANALYTIC'. Default at database level is STANDARD.
210
+ :param compute_map: ComputeMapName of the compute map. The compute_map in a compute cluster profile refers
211
+ to the mapping of compute resources to a specific node or set of nodes within the cluster.
212
+ :param compute_attribute: Optional attributes of compute profile. Example compute attribute
213
+ MIN_COMPUTE_COUNT(1) MAX_COMPUTE_COUNT(5) INITIALLY_SUSPENDED('FALSE')
214
+ :param teradata_conn_id: The :ref:`Teradata connection id <howto/connection:teradata>`
215
+ reference to a specific Teradata database.
216
+ :param timeout: Time elapsed before the task times out and fails.
217
+ """
218
+
219
+ template_fields: Sequence[str] = (
220
+ "compute_profile_name",
221
+ "compute_group_name",
222
+ "query_strategy",
223
+ "compute_map",
224
+ "compute_attribute",
225
+ "teradata_conn_id",
226
+ "timeout",
227
+ )
228
+
229
+ ui_color = "#e07c24"
230
+
231
+ def __init__(
232
+ self,
233
+ query_strategy: str | None = None,
234
+ compute_map: str | None = None,
235
+ compute_attribute: str | None = None,
236
+ **kwargs,
237
+ ) -> None:
238
+ super().__init__(**kwargs)
239
+ self.query_strategy = query_strategy
240
+ self.compute_map = compute_map
241
+ self.compute_attribute = compute_attribute
242
+
243
+ def _build_ccp_setup_query(self):
244
+ create_cp_query = "CREATE COMPUTE PROFILE " + self.compute_profile_name
245
+ if self.compute_group_name:
246
+ create_cp_query = create_cp_query + " IN " + self.compute_group_name
247
+ if self.compute_map is not None:
248
+ create_cp_query = create_cp_query + ", INSTANCE = " + self.compute_map
249
+ if self.query_strategy is not None:
250
+ create_cp_query = create_cp_query + ", INSTANCE TYPE = " + self.query_strategy
251
+ if self.compute_attribute is not None:
252
+ create_cp_query = create_cp_query + " USING " + self.compute_attribute
253
+ return create_cp_query
254
+
255
+ def execute(self, context: Context):
256
+ """
257
+ Initiate the execution of CREATE COMPUTE SQL statement.
258
+
259
+ Initiate the execution of the SQL statement for provisioning the compute cluster within Teradata Vantage
260
+ Lake, effectively creates the compute cluster.
261
+ Airflow runs this method on the worker and defers using the trigger.
262
+ """
263
+ super().execute(context)
264
+ return self._compute_cluster_execute()
265
+
266
+ def _compute_cluster_execute(self):
267
+ super()._compute_cluster_execute()
268
+ if self.compute_group_name:
269
+ cg_status_query = (
270
+ "SELECT count(1) FROM DBC.ComputeGroups WHERE UPPER(ComputeGroupName) = UPPER('"
271
+ + self.compute_group_name
272
+ + "')"
273
+ )
274
+ cg_status_result = self._hook_run(cg_status_query, _single_result_row_handler)
275
+ if cg_status_result is not None:
276
+ cg_status_result = str(cg_status_result)
277
+ else:
278
+ cg_status_result = 0
279
+ if int(cg_status_result) == 0:
280
+ create_cg_query = "CREATE COMPUTE GROUP " + self.compute_group_name
281
+ if self.query_strategy is not None:
282
+ create_cg_query = (
283
+ create_cg_query + " USING QUERY_STRATEGY ('" + self.query_strategy + "')"
284
+ )
285
+ self._hook_run(create_cg_query, _single_result_row_handler)
286
+ cp_status_query = (
287
+ "SEL ComputeProfileState FROM DBC.ComputeProfilesVX WHERE UPPER(ComputeProfileName) = UPPER('"
288
+ + self.compute_profile_name
289
+ + "')"
290
+ )
291
+ if self.compute_group_name:
292
+ cp_status_query += " AND UPPER(ComputeGroupName) = UPPER('" + self.compute_group_name + "')"
293
+ cp_status_result = self._hook_run(cp_status_query, handler=_single_result_row_handler)
294
+ if cp_status_result is not None:
295
+ cp_status_result = str(cp_status_result)
296
+ msg = f"Compute Profile {self.compute_profile_name} is already exists under Compute Group {self.compute_group_name}. Status is {cp_status_result}"
297
+ self.log.info(msg)
298
+ return cp_status_result
299
+ else:
300
+ create_cp_query = self._build_ccp_setup_query()
301
+ operation = Constants.CC_CREATE_OPR
302
+ initially_suspended = self._get_initially_suspended(create_cp_query)
303
+ if initially_suspended == "TRUE":
304
+ operation = Constants.CC_CREATE_SUSPEND_OPR
305
+ return self._handle_cc_status(operation, create_cp_query)
306
+
307
+
308
+ class TeradataComputeClusterDecommissionOperator(_TeradataComputeClusterOperator):
309
+ """
310
+ Drops the compute cluster with specified Compute Group Name and Compute Profile Name.
311
+
312
+ .. seealso::
313
+ For more information on how to use this operator, take a look at the guide:
314
+ :ref:`howto/operator:TeradataComputeClusterDecommissionOperator`
315
+
316
+ :param compute_profile_name: Name of the Compute Profile to manage.
317
+ :param compute_group_name: Name of compute group to which compute profile belongs.
318
+ :param delete_compute_group: Indicates whether the compute group should be deleted.
319
+ When set to True, it signals the system to remove the specified compute group.
320
+ Conversely, when set to False, no action is taken on the compute group.
321
+ :param teradata_conn_id: The :ref:`Teradata connection id <howto/connection:teradata>`
322
+ reference to a specific Teradata database.
323
+ :param timeout: Time elapsed before the task times out and fails.
324
+ """
325
+
326
+ template_fields: Sequence[str] = (
327
+ "compute_profile_name",
328
+ "compute_group_name",
329
+ "delete_compute_group",
330
+ "teradata_conn_id",
331
+ "timeout",
332
+ )
333
+
334
+ ui_color = "#e07c24"
335
+
336
+ def __init__(
337
+ self,
338
+ delete_compute_group: bool = False,
339
+ **kwargs,
340
+ ) -> None:
341
+ super().__init__(**kwargs)
342
+ self.delete_compute_group = delete_compute_group
343
+
344
+ def execute(self, context: Context):
345
+ """
346
+ Initiate the execution of DROP COMPUTE SQL statement.
347
+
348
+ Initiate the execution of the SQL statement for decommissioning the compute cluster within Teradata Vantage
349
+ Lake, effectively drops the compute cluster.
350
+ Airflow runs this method on the worker and defers using the trigger.
351
+ """
352
+ super().execute(context)
353
+ return self._compute_cluster_execute()
354
+
355
+ def _compute_cluster_execute(self):
356
+ super()._compute_cluster_execute()
357
+ cp_drop_query = "DROP COMPUTE PROFILE " + self.compute_profile_name
358
+ if self.compute_group_name:
359
+ cp_drop_query = cp_drop_query + " IN COMPUTE GROUP " + self.compute_group_name
360
+ self._hook_run(cp_drop_query, handler=_single_result_row_handler)
361
+ self.log.info(
362
+ "Compute Profile %s IN Compute Group %s is successfully dropped",
363
+ self.compute_profile_name,
364
+ self.compute_group_name,
365
+ )
366
+ if self.delete_compute_group:
367
+ cg_drop_query = "DROP COMPUTE GROUP " + self.compute_group_name
368
+ self._hook_run(cg_drop_query, handler=_single_result_row_handler)
369
+ self.log.info("Compute Group %s is successfully dropped", self.compute_group_name)
370
+
371
+
372
+ class TeradataComputeClusterResumeOperator(_TeradataComputeClusterOperator):
373
+ """
374
+ Teradata Compute Cluster Operator to Resume the specified Teradata Vantage Cloud Lake Compute Cluster.
375
+
376
+ Resumes the Teradata Vantage Lake Computer Cluster by employing the RESUME SQL statement within the
377
+ Teradata Vantage Lake Compute Cluster SQL Interface.
378
+
379
+ .. seealso::
380
+ For more information on how to use this operator, take a look at the guide:
381
+ :ref:`howto/operator:TeradataComputeClusterResumeOperator`
382
+
383
+ :param compute_profile_name: Name of the Compute Profile to manage.
384
+ :param compute_group_name: Name of compute group to which compute profile belongs.
385
+ :param teradata_conn_id: The :ref:`Teradata connection id <howto/connection:teradata>`
386
+ reference to a specific Teradata database.
387
+ :param timeout: Time elapsed before the task times out and fails. Time is in minutes.
388
+ """
389
+
390
+ template_fields: Sequence[str] = (
391
+ "compute_profile_name",
392
+ "compute_group_name",
393
+ "teradata_conn_id",
394
+ "timeout",
395
+ )
396
+
397
+ ui_color = "#e07c24"
398
+
399
+ def __init__(
400
+ self,
401
+ **kwargs,
402
+ ) -> None:
403
+ super().__init__(**kwargs)
404
+
405
+ def execute(self, context: Context):
406
+ """
407
+ Initiate the execution of RESUME COMPUTE SQL statement.
408
+
409
+ Initiate the execution of the SQL statement for resuming the compute cluster within Teradata Vantage
410
+ Lake, effectively resumes the compute cluster.
411
+ Airflow runs this method on the worker and defers using the trigger.
412
+ """
413
+ super().execute(context)
414
+ return self._compute_cluster_execute()
415
+
416
+ def _compute_cluster_execute(self):
417
+ super()._compute_cluster_execute()
418
+ cc_status_query = (
419
+ "SEL ComputeProfileState FROM DBC.ComputeProfilesVX WHERE UPPER(ComputeProfileName) = UPPER('"
420
+ + self.compute_profile_name
421
+ + "')"
422
+ )
423
+ if self.compute_group_name:
424
+ cc_status_query += " AND UPPER(ComputeGroupName) = UPPER('" + self.compute_group_name + "')"
425
+ cc_status_result = self._hook_run(cc_status_query, handler=_single_result_row_handler)
426
+ if cc_status_result is not None:
427
+ cp_status_result = str(cc_status_result)
428
+ # Generates an error message if the compute cluster does not exist for the specified
429
+ # compute profile and compute group.
430
+ else:
431
+ self.log.info(Constants.CC_GRP_PRP_NON_EXISTS_MSG)
432
+ raise AirflowException(Constants.CC_GRP_PRP_NON_EXISTS_MSG)
433
+ if cp_status_result != Constants.CC_RESUME_DB_STATUS:
434
+ cp_resume_query = f"RESUME COMPUTE FOR COMPUTE PROFILE {self.compute_profile_name}"
435
+ if self.compute_group_name:
436
+ cp_resume_query = f"{cp_resume_query} IN COMPUTE GROUP {self.compute_group_name}"
437
+ return self._handle_cc_status(Constants.CC_RESUME_OPR, cp_resume_query)
438
+ else:
439
+ self.log.info(
440
+ "Compute Cluster %s already %s", self.compute_profile_name, Constants.CC_RESUME_DB_STATUS
441
+ )
442
+
443
+
444
+ class TeradataComputeClusterSuspendOperator(_TeradataComputeClusterOperator):
445
+ """
446
+ Teradata Compute Cluster Operator to suspend the specified Teradata Vantage Cloud Lake Compute Cluster.
447
+
448
+ Suspends the Teradata Vantage Lake Computer Cluster by employing the SUSPEND SQL statement within the
449
+ Teradata Vantage Lake Compute Cluster SQL Interface.
450
+
451
+ .. seealso::
452
+ For more information on how to use this operator, take a look at the guide:
453
+ :ref:`howto/operator:TeradataComputeClusterSuspendOperator`
454
+
455
+ :param compute_profile_name: Name of the Compute Profile to manage.
456
+ :param compute_group_name: Name of compute group to which compute profile belongs.
457
+ :param teradata_conn_id: The :ref:`Teradata connection id <howto/connection:teradata>`
458
+ reference to a specific Teradata database.
459
+ :param timeout: Time elapsed before the task times out and fails.
460
+ """
461
+
462
+ template_fields: Sequence[str] = (
463
+ "compute_profile_name",
464
+ "compute_group_name",
465
+ "teradata_conn_id",
466
+ "timeout",
467
+ )
468
+
469
+ ui_color = "#e07c24"
470
+
471
+ def __init__(
472
+ self,
473
+ **kwargs,
474
+ ) -> None:
475
+ super().__init__(**kwargs)
476
+
477
+ def execute(self, context: Context):
478
+ """
479
+ Initiate the execution of SUSPEND COMPUTE SQL statement.
480
+
481
+ Initiate the execution of the SQL statement for suspending the compute cluster within Teradata Vantage
482
+ Lake, effectively suspends the compute cluster.
483
+ Airflow runs this method on the worker and defers using the trigger.
484
+ """
485
+ super().execute(context)
486
+ return self._compute_cluster_execute()
487
+
488
+ def _compute_cluster_execute(self):
489
+ super()._compute_cluster_execute()
490
+ sql = (
491
+ "SEL ComputeProfileState FROM DBC.ComputeProfilesVX WHERE UPPER(ComputeProfileName) = UPPER('"
492
+ + self.compute_profile_name
493
+ + "')"
494
+ )
495
+ if self.compute_group_name:
496
+ sql += " AND UPPER(ComputeGroupName) = UPPER('" + self.compute_group_name + "')"
497
+ result = self._hook_run(sql, handler=_single_result_row_handler)
498
+ if result is not None:
499
+ result = str(result)
500
+ # Generates an error message if the compute cluster does not exist for the specified
501
+ # compute profile and compute group.
502
+ else:
503
+ self.log.info(Constants.CC_GRP_PRP_NON_EXISTS_MSG)
504
+ raise AirflowException(Constants.CC_GRP_PRP_NON_EXISTS_MSG)
505
+ if result != Constants.CC_SUSPEND_DB_STATUS:
506
+ sql = f"SUSPEND COMPUTE FOR COMPUTE PROFILE {self.compute_profile_name}"
507
+ if self.compute_group_name:
508
+ sql = f"{sql} IN COMPUTE GROUP {self.compute_group_name}"
509
+ return self._handle_cc_status(Constants.CC_SUSPEND_OPR, sql)
510
+ else:
511
+ self.log.info(
512
+ "Compute Cluster %s already %s", self.compute_profile_name, Constants.CC_SUSPEND_DB_STATUS
513
+ )
@@ -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,155 @@
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 asyncio
20
+ from typing import Any, AsyncIterator
21
+
22
+ from airflow.exceptions import AirflowException
23
+ from airflow.providers.common.sql.hooks.sql import fetch_one_handler
24
+ from airflow.providers.teradata.hooks.teradata import TeradataHook
25
+ from airflow.providers.teradata.utils.constants import Constants
26
+ from airflow.triggers.base import BaseTrigger, TriggerEvent
27
+
28
+
29
+ class TeradataComputeClusterSyncTrigger(BaseTrigger):
30
+ """
31
+ Fetch the status of the suspend or resume operation for the specified compute cluster.
32
+
33
+ :param teradata_conn_id: The :ref:`Teradata connection id <howto/connection:teradata>`
34
+ reference to a specific Teradata database.
35
+ :param compute_profile_name: Name of the Compute Profile to manage.
36
+ :param compute_group_name: Name of compute group to which compute profile belongs.
37
+ :param opr_type: Compute cluster operation - SUSPEND/RESUME
38
+ :param poll_interval: polling period in minutes to check for the status
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ teradata_conn_id: str,
44
+ compute_profile_name: str,
45
+ compute_group_name: str | None = None,
46
+ operation_type: str | None = None,
47
+ poll_interval: float | None = None,
48
+ ):
49
+ super().__init__()
50
+ self.teradata_conn_id = teradata_conn_id
51
+ self.compute_profile_name = compute_profile_name
52
+ self.compute_group_name = compute_group_name
53
+ self.operation_type = operation_type
54
+ self.poll_interval = poll_interval
55
+
56
+ def serialize(self) -> tuple[str, dict[str, Any]]:
57
+ """Serialize TeradataComputeClusterSyncTrigger arguments and classpath."""
58
+ return (
59
+ "airflow.providers.teradata.triggers.teradata_compute_cluster.TeradataComputeClusterSyncTrigger",
60
+ {
61
+ "teradata_conn_id": self.teradata_conn_id,
62
+ "compute_profile_name": self.compute_profile_name,
63
+ "compute_group_name": self.compute_group_name,
64
+ "operation_type": self.operation_type,
65
+ "poll_interval": self.poll_interval,
66
+ },
67
+ )
68
+
69
+ async def run(self) -> AsyncIterator[TriggerEvent]:
70
+ """Wait for Compute Cluster operation to complete."""
71
+ try:
72
+ while True:
73
+ status = await self.get_status()
74
+ if status is None or len(status) == 0:
75
+ self.log.info(Constants.CC_GRP_PRP_NON_EXISTS_MSG)
76
+ raise AirflowException(Constants.CC_GRP_PRP_NON_EXISTS_MSG)
77
+ if (
78
+ self.operation_type == Constants.CC_SUSPEND_OPR
79
+ or self.operation_type == Constants.CC_CREATE_SUSPEND_OPR
80
+ ):
81
+ if status == Constants.CC_SUSPEND_DB_STATUS:
82
+ break
83
+ elif (
84
+ self.operation_type == Constants.CC_RESUME_OPR
85
+ or self.operation_type == Constants.CC_CREATE_OPR
86
+ ):
87
+ if status == Constants.CC_RESUME_DB_STATUS:
88
+ break
89
+ if self.poll_interval is not None:
90
+ self.poll_interval = float(self.poll_interval)
91
+ else:
92
+ self.poll_interval = float(Constants.CC_POLL_INTERVAL)
93
+ await asyncio.sleep(self.poll_interval)
94
+ if (
95
+ self.operation_type == Constants.CC_SUSPEND_OPR
96
+ or self.operation_type == Constants.CC_CREATE_SUSPEND_OPR
97
+ ):
98
+ if status == Constants.CC_SUSPEND_DB_STATUS:
99
+ yield TriggerEvent(
100
+ {
101
+ "status": "success",
102
+ "message": Constants.CC_OPR_SUCCESS_STATUS_MSG
103
+ % (self.compute_profile_name, self.operation_type),
104
+ }
105
+ )
106
+ else:
107
+ yield TriggerEvent(
108
+ {
109
+ "status": "error",
110
+ "message": Constants.CC_OPR_FAILURE_STATUS_MSG
111
+ % (self.compute_profile_name, self.operation_type),
112
+ }
113
+ )
114
+ elif (
115
+ self.operation_type == Constants.CC_RESUME_OPR
116
+ or self.operation_type == Constants.CC_CREATE_OPR
117
+ ):
118
+ if status == Constants.CC_RESUME_DB_STATUS:
119
+ yield TriggerEvent(
120
+ {
121
+ "status": "success",
122
+ "message": Constants.CC_OPR_SUCCESS_STATUS_MSG
123
+ % (self.compute_profile_name, self.operation_type),
124
+ }
125
+ )
126
+ else:
127
+ yield TriggerEvent(
128
+ {
129
+ "status": "error",
130
+ "message": Constants.CC_OPR_FAILURE_STATUS_MSG
131
+ % (self.compute_profile_name, self.operation_type),
132
+ }
133
+ )
134
+ else:
135
+ yield TriggerEvent({"status": "error", "message": "Invalid operation"})
136
+ except Exception as e:
137
+ yield TriggerEvent({"status": "error", "message": str(e)})
138
+ except asyncio.CancelledError:
139
+ self.log.error(Constants.CC_OPR_TIMEOUT_ERROR, self.operation_type)
140
+
141
+ async def get_status(self) -> str:
142
+ """Return compute cluster SUSPEND/RESUME operation status."""
143
+ sql = (
144
+ "SEL ComputeProfileState FROM DBC.ComputeProfilesVX WHERE UPPER(ComputeProfileName) = UPPER('"
145
+ + self.compute_profile_name
146
+ + "')"
147
+ )
148
+ if self.compute_group_name:
149
+ sql += " AND UPPER(ComputeGroupName) = UPPER('" + self.compute_group_name + "')"
150
+ hook = TeradataHook(teradata_conn_id=self.teradata_conn_id)
151
+ result_set = hook.run(sql, handler=fetch_one_handler)
152
+ status = ""
153
+ if isinstance(result_set, list) and isinstance(result_set[0], str):
154
+ status = str(result_set[0])
155
+ return status
@@ -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,46 @@
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
+
20
+ class Constants:
21
+ """Define constants for Teradata Provider."""
22
+
23
+ CC_CREATE_OPR = "CREATE"
24
+ CC_CREATE_SUSPEND_OPR = "CREATE_SUSPEND"
25
+ CC_DROP_OPR = "DROP"
26
+ CC_SUSPEND_OPR = "SUSPEND"
27
+ CC_RESUME_OPR = "RESUME"
28
+ CC_INITIALIZE_DB_STATUS = "Initializing"
29
+ CC_SUSPEND_DB_STATUS = "Suspended"
30
+ CC_RESUME_DB_STATUS = "Running"
31
+ CC_OPR_SUCCESS_STATUS_MSG = "Compute Cluster %s %s operation completed successfully."
32
+ CC_OPR_FAILURE_STATUS_MSG = "Compute Cluster %s %s operation has failed."
33
+ CC_OPR_INITIALIZING_STATUS_MSG = "The environment is currently initializing. Please wait."
34
+ CC_OPR_EMPTY_PROFILE_ERROR_MSG = "Please provide a valid name for the compute cluster profile."
35
+ CC_GRP_PRP_NON_EXISTS_MSG = "The specified Compute cluster is not present or The user doesn't have permission to access compute cluster."
36
+ CC_GRP_PRP_UN_AUTHORIZED_MSG = "The %s operation is not authorized for the user."
37
+ CC_GRP_LAKE_SUPPORT_ONLY_MSG = "Compute Groups is supported only on Vantage Cloud Lake."
38
+ CC_OPR_TIMEOUT_ERROR = (
39
+ "There is an issue with the %s operation. Kindly consult the administrator for assistance."
40
+ )
41
+ CC_GRP_PRP_EXISTS_MSG = "The specified Compute cluster is already exists."
42
+ CC_OPR_EMPTY_COPY_PROFILE_ERROR_MSG = (
43
+ "Please provide a valid name for the source and target compute profile."
44
+ )
45
+ CC_OPR_TIME_OUT = 1200
46
+ CC_POLL_INTERVAL = 60
@@ -28,7 +28,7 @@ build-backend = "flit_core.buildapi"
28
28
 
29
29
  [project]
30
30
  name = "apache-airflow-providers-teradata"
31
- version = "2.3.0"
31
+ version = "2.4.0"
32
32
  description = "Provider package apache-airflow-providers-teradata for Apache Airflow"
33
33
  readme = "README.rst"
34
34
  authors = [
@@ -63,8 +63,8 @@ dependencies = [
63
63
  ]
64
64
 
65
65
  [project.urls]
66
- "Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-teradata/2.3.0"
67
- "Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-teradata/2.3.0/changelog.html"
66
+ "Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-teradata/2.4.0"
67
+ "Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-teradata/2.4.0/changelog.html"
68
68
  "Bug Tracker" = "https://github.com/apache/airflow/issues"
69
69
  "Source Code" = "https://github.com/apache/airflow"
70
70
  "Slack Chat" = "https://s.apache.org/airflow-slack"