This is an automated email from the ASF dual-hosted git repository. jbonofre pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/incubator-liminal.git
commit 2887b1993ae5d17cc0e172a6f9f828ec4f362e04 Author: aviemzur <[email protected]> AuthorDate: Sun Mar 8 15:46:22 2020 +0200 First code commit --- .gitignore | 8 + LICENSE | 250 +++++++++++++++++++ rainbow/__init__.py | 17 ++ rainbow/cli/__init__.py | 17 ++ rainbow/core/__init__.py | 17 ++ rainbow/docker/__init__.py | 17 ++ rainbow/http/__init__.py | 17 ++ rainbow/monitoring/__init__.py | 17 ++ rainbow/runners/__init__.py | 17 ++ rainbow/runners/airflow/__init__.py | 17 ++ rainbow/runners/airflow/compiler/__init__.py | 17 ++ .../runners/airflow/compiler/rainbow_compiler.py | 26 ++ rainbow/runners/airflow/dag/__init__.py | 17 ++ rainbow/runners/airflow/operators/__init__.py | 17 ++ .../runners/airflow/operators/cloudformation.py | 270 +++++++++++++++++++++ .../airflow/operators/job_status_operator.py | 180 ++++++++++++++ .../airflow/operators/kubernetes_pod_operator.py | 140 +++++++++++ rainbow/sql/__init__.py | 17 ++ tests/__init__.py | 17 ++ tests/runners/__init__.py | 17 ++ tests/runners/airflow/__init__.py | 17 ++ tests/runners/airflow/compiler/__init__.py | 17 ++ tests/runners/airflow/compiler/rainbow.yml | 115 +++++++++ .../airflow/compiler/test_rainbow_compiler.py | 33 +++ tests/runners/airflow/operators/__init__.py | 17 ++ 25 files changed, 1311 insertions(+) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e14e323 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.idea +bin +include +lib +venv +.Python +*.pyc +pip-selfcheck.json diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8f1552e --- /dev/null +++ b/LICENSE @@ -0,0 +1,250 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +============================================================================ + APACHE AIRFLOW SUBCOMPONENTS: + + The Apache Airflow project contains subcomponents with separate copyright + notices and license terms. Your use of the source code for the these + subcomponents is subject to the terms and conditions of the following + licenses. + + +======================================================================== +Third party Apache 2.0 licenses +======================================================================== + +The following components are provided under the Apache 2.0 License. +See project link for details. The text of each license is also included +at licenses/LICENSE-[project].txt. + + (ALv2 License) hue v4.3.0 (https://github.com/cloudera/hue/) + (ALv2 License) jqclock v2.3.0 (https://github.com/JohnRDOrazio/jQuery-Clock-Plugin) + (ALv2 License) bootstrap3-typeahead v4.0.2 (https://github.com/bassjobsen/Bootstrap-3-Typeahead) + (ALv2 License) airflow.contrib.auth.backends.github_enterprise_auth + +======================================================================== +MIT licenses +======================================================================== + +The following components are provided under the MIT License. See project link for details. +The text of each license is also included at licenses/LICENSE-[project].txt. + + (MIT License) jquery v3.4.1 (https://jquery.org/license/) + (MIT License) dagre-d3 v0.6.4 (https://github.com/cpettitt/dagre-d3) + (MIT License) bootstrap v3.2 (https://github.com/twbs/bootstrap/) + (MIT License) d3-tip v0.9.1 (https://github.com/Caged/d3-tip) + (MIT License) dataTables v1.10.20 (https://datatables.net) + (MIT License) Bootstrap Toggle v2.2.2 (http://www.bootstraptoggle.com) + (MIT License) normalize.css v3.0.2 (http://necolas.github.io/normalize.css/) + (MIT License) ElasticMock v1.3.2 (https://github.com/vrcmarcos/elasticmock) + (MIT License) MomentJS v2.24.0 (http://momentjs.com/) + (MIT License) python-slugify v2.0.1 (https://github.com/un33k/python-slugify) + (MIT License) python-nvd3 v0.15.0 (https://github.com/areski/python-nvd3) + +======================================================================== +BSD 3-Clause licenses +======================================================================== +The following components are provided under the BSD 3-Clause license. See project links for details. +The text of each license is also included at licenses/LICENSE-[project].txt. + + (BSD 3 License) d3 v5.15.0 (https://d3js.org) diff --git a/rainbow/__init__.py b/rainbow/__init__.py new file mode 100644 index 0000000..217e5db --- /dev/null +++ b/rainbow/__init__.py @@ -0,0 +1,17 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/rainbow/cli/__init__.py b/rainbow/cli/__init__.py new file mode 100644 index 0000000..217e5db --- /dev/null +++ b/rainbow/cli/__init__.py @@ -0,0 +1,17 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/rainbow/core/__init__.py b/rainbow/core/__init__.py new file mode 100644 index 0000000..217e5db --- /dev/null +++ b/rainbow/core/__init__.py @@ -0,0 +1,17 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/rainbow/docker/__init__.py b/rainbow/docker/__init__.py new file mode 100644 index 0000000..217e5db --- /dev/null +++ b/rainbow/docker/__init__.py @@ -0,0 +1,17 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/rainbow/http/__init__.py b/rainbow/http/__init__.py new file mode 100644 index 0000000..217e5db --- /dev/null +++ b/rainbow/http/__init__.py @@ -0,0 +1,17 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/rainbow/monitoring/__init__.py b/rainbow/monitoring/__init__.py new file mode 100644 index 0000000..217e5db --- /dev/null +++ b/rainbow/monitoring/__init__.py @@ -0,0 +1,17 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/rainbow/runners/__init__.py b/rainbow/runners/__init__.py new file mode 100644 index 0000000..217e5db --- /dev/null +++ b/rainbow/runners/__init__.py @@ -0,0 +1,17 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/rainbow/runners/airflow/__init__.py b/rainbow/runners/airflow/__init__.py new file mode 100644 index 0000000..217e5db --- /dev/null +++ b/rainbow/runners/airflow/__init__.py @@ -0,0 +1,17 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/rainbow/runners/airflow/compiler/__init__.py b/rainbow/runners/airflow/compiler/__init__.py new file mode 100644 index 0000000..217e5db --- /dev/null +++ b/rainbow/runners/airflow/compiler/__init__.py @@ -0,0 +1,17 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/rainbow/runners/airflow/compiler/rainbow_compiler.py b/rainbow/runners/airflow/compiler/rainbow_compiler.py new file mode 100644 index 0000000..818fdc5 --- /dev/null +++ b/rainbow/runners/airflow/compiler/rainbow_compiler.py @@ -0,0 +1,26 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +""" +Compiler for rainbows. +""" +import yaml + + +def parse_yaml(path): + with open(path, 'r') as stream: + return yaml.safe_load(stream) diff --git a/rainbow/runners/airflow/dag/__init__.py b/rainbow/runners/airflow/dag/__init__.py new file mode 100644 index 0000000..217e5db --- /dev/null +++ b/rainbow/runners/airflow/dag/__init__.py @@ -0,0 +1,17 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/rainbow/runners/airflow/operators/__init__.py b/rainbow/runners/airflow/operators/__init__.py new file mode 100644 index 0000000..217e5db --- /dev/null +++ b/rainbow/runners/airflow/operators/__init__.py @@ -0,0 +1,17 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/rainbow/runners/airflow/operators/cloudformation.py b/rainbow/runners/airflow/operators/cloudformation.py new file mode 100644 index 0000000..0a70e5a --- /dev/null +++ b/rainbow/runners/airflow/operators/cloudformation.py @@ -0,0 +1,270 @@ +# -*- coding: utf-8 -*- +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +""" +This module contains CloudFormation create/delete stack operators. +Can be removed when Airflow 2.0.0 is released. +""" +from typing import List + +from airflow.contrib.hooks.aws_hook import AwsHook +from airflow.models import BaseOperator +from airflow.sensors.base_sensor_operator import BaseSensorOperator +from airflow.utils.decorators import apply_defaults +from botocore.exceptions import ClientError + + +# noinspection PyAbstractClass +class CloudFormationHook(AwsHook): + """ + Interact with AWS CloudFormation. + """ + + def __init__(self, region_name=None, *args, **kwargs): + self.region_name = region_name + self.conn = None + super().__init__(*args, **kwargs) + + def get_conn(self): + self.conn = self.get_client_type('cloudformation', self.region_name) + return self.conn + + +class BaseCloudFormationOperator(BaseOperator): + """ + Base operator for CloudFormation operations. + + :param params: parameters to be passed to CloudFormation. + :type dict + :param aws_conn_id: aws connection to uses + :type aws_conn_id: str + """ + template_fields: List[str] = [] + template_ext = () + ui_color = '#1d472b' + ui_fgcolor = '#FFF' + + @apply_defaults + def __init__( + self, + params, + aws_conn_id='aws_default', + *args, **kwargs): + super().__init__(*args, **kwargs) + self.params = params + self.aws_conn_id = aws_conn_id + + def execute(self, context): + self.log.info('Parameters: %s', self.params) + + self.cloudformation_op(CloudFormationHook(aws_conn_id=self.aws_conn_id).get_conn()) + + def cloudformation_op(self, cloudformation): + """ + This is the main method to run CloudFormation operation. + """ + raise NotImplementedError() + + +class CloudFormationCreateStackOperator(BaseCloudFormationOperator): + """ + An operator that creates a CloudFormation stack. + + :param params: parameters to be passed to CloudFormation. For possible arguments see: + https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cloudformation.html#CloudFormation.Client.create_stack + :type dict + :param aws_conn_id: aws connection to uses + :type aws_conn_id: str + """ + template_fields: List[str] = [] + template_ext = () + ui_color = '#6b9659' + + @apply_defaults + def __init__( + self, + params, + aws_conn_id='aws_default', + *args, **kwargs): + super().__init__(params=params, aws_conn_id=aws_conn_id, *args, **kwargs) + + def cloudformation_op(self, cloudformation): + cloudformation.create_stack(**self.params) + + +class CloudFormationDeleteStackOperator(BaseCloudFormationOperator): + """ + An operator that deletes a CloudFormation stack. + + :param params: parameters to be passed to CloudFormation. For possible arguments see: + https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cloudformation.html#CloudFormation.Client.delete_stack + :type dict + :param aws_conn_id: aws connection to uses + :type aws_conn_id: str + """ + template_fields: List[str] = [] + template_ext = () + ui_color = '#1d472b' + ui_fgcolor = '#FFF' + + @apply_defaults + def __init__( + self, + params, + aws_conn_id='aws_default', + *args, **kwargs): + super().__init__(params=params, aws_conn_id=aws_conn_id, *args, **kwargs) + + def cloudformation_op(self, cloudformation): + cloudformation.delete_stack(**self.params) + + +class BaseCloudFormationSensor(BaseSensorOperator): + """ + Waits for a stack operation to complete on AWS CloudFormation. + + :param stack_name: The name of the stack to wait for (templated) + :type stack_name: str + :param aws_conn_id: ID of the Airflow connection where credentials and extra configuration are + stored + :type aws_conn_id: str + :param poke_interval: Time in seconds that the job should wait between each try + :type poke_interval: int + """ + + @apply_defaults + def __init__(self, + stack_name, + complete_status, + in_progress_status, + aws_conn_id='aws_default', + poke_interval=30, + *args, + **kwargs): + super().__init__(poke_interval=poke_interval, *args, **kwargs) + self.aws_conn_id = aws_conn_id + self.stack_name = stack_name + self.complete_status = complete_status + self.in_progress_status = in_progress_status + self.hook = None + + def poke(self, context): + """ + Checks for existence of the stack in AWS CloudFormation. + """ + cloudformation = self.get_hook().get_conn() + + self.log.info('Poking for stack %s', self.stack_name) + + try: + stacks = cloudformation.describe_stacks(StackName=self.stack_name)['Stacks'] + stack_status = stacks[0]['StackStatus'] + if stack_status == self.complete_status: + return True + elif stack_status == self.in_progress_status: + return False + else: + raise ValueError(f'Stack {self.stack_name} in bad state: {stack_status}') + except ClientError as e: + if 'does not exist' in str(e): + if not self.allow_non_existing_stack_status(): + raise ValueError(f'Stack {self.stack_name} does not exist') + else: + return True + else: + raise e + + def get_hook(self): + """ + Gets the AwsGlueCatalogHook + """ + if not self.hook: + self.hook = CloudFormationHook(aws_conn_id=self.aws_conn_id) + + return self.hook + + def allow_non_existing_stack_status(self): + """ + Boolean value whether or not sensor should allow non existing stack responses. + """ + return False + + +class CloudFormationCreateStackSensor(BaseCloudFormationSensor): + """ + Waits for a stack to be created successfully on AWS CloudFormation. + + :param stack_name: The name of the stack to wait for (templated) + :type stack_name: str + :param aws_conn_id: ID of the Airflow connection where credentials and extra configuration are + stored + :type aws_conn_id: str + :param poke_interval: Time in seconds that the job should wait between each try + :type poke_interval: int + """ + + template_fields = ['stack_name'] + ui_color = '#C5CAE9' + + @apply_defaults + def __init__(self, + stack_name, + aws_conn_id='aws_default', + poke_interval=30, + *args, + **kwargs): + super().__init__(stack_name=stack_name, + complete_status='CREATE_COMPLETE', + in_progress_status='CREATE_IN_PROGRESS', + aws_conn_id=aws_conn_id, + poke_interval=poke_interval, + *args, + **kwargs) + + +class CloudFormationDeleteStackSensor(BaseCloudFormationSensor): + """ + Waits for a stack to be deleted successfully on AWS CloudFormation. + + :param stack_name: The name of the stack to wait for (templated) + :type stack_name: str + :param aws_conn_id: ID of the Airflow connection where credentials and extra configuration are + stored + :type aws_conn_id: str + :param poke_interval: Time in seconds that the job should wait between each try + :type poke_interval: int + """ + + template_fields = ['stack_name'] + ui_color = '#C5CAE9' + + @apply_defaults + def __init__(self, + stack_name, + aws_conn_id='aws_default', + poke_interval=30, + *args, + **kwargs): + super().__init__(stack_name=stack_name, + complete_status='DELETE_COMPLETE', + in_progress_status='DELETE_IN_PROGRESS', + aws_conn_id=aws_conn_id, + poke_interval=poke_interval, *args, **kwargs) + + def allow_non_existing_stack_status(self): + return True diff --git a/rainbow/runners/airflow/operators/job_status_operator.py b/rainbow/runners/airflow/operators/job_status_operator.py new file mode 100644 index 0000000..dc318e5 --- /dev/null +++ b/rainbow/runners/airflow/operators/job_status_operator.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from datetime import datetime + +import pytz +from airflow.contrib.hooks.aws_hook import AwsHook +from airflow.exceptions import AirflowException +from airflow.models import BaseOperator +from airflow.utils.decorators import apply_defaults + + +class JobStatusOperator(BaseOperator): + """ + Base operator for job status operators. + """ + template_ext = () + + @apply_defaults + def __init__( + self, + backends, + *args, **kwargs): + super().__init__(*args, **kwargs) + self.backends = backends + self.cloudwatch = CloudWatchHook() + + def execute(self, context): + for backend in self.backends: + if backend in self.report_functions: + for metric in self.metrics(context): + self.report_functions[backend](self, metric) + else: + raise AirflowException('No such metrics backend: {}'.format(backend)) + + def metrics(self, context): + raise NotImplementedError + + def send_metric_to_cloudwatch(self, metric): + self.cloudwatch.put_metric_data(metric) + + report_functions = { + 'cloudwatch': send_metric_to_cloudwatch + } + + +class JobStartOperator(JobStatusOperator): + ui_color = '#c5e5e8' + + def __init__( + self, + namespace, + application_name, + backends, + *args, **kwargs): + super().__init__(backends=backends, *args, **kwargs) + self.namespace = namespace + self.application_name = application_name + + def metrics(self, context): + return [Metric(self.namespace, 'JobStarted', 1, + [Tag('ApplicationName', self.application_name)])] + + +class JobEndOperator(JobStatusOperator): + ui_color = '#6d8fad' + + def __init__( + self, + namespace, + application_name, + backends, + *args, **kwargs): + super().__init__(backends=backends, *args, **kwargs) + self.namespace = namespace + self.application_name = application_name + + def metrics(self, context): + duration = round((pytz.utc.localize(datetime.utcnow()) - context[ + 'ti'].get_dagrun().start_date).total_seconds()) + + self.log.info('Elapsed time: %s' % duration) + + task_instances = context['dag_run'].get_task_instances() + task_states = [task_instance.state for task_instance in task_instances[:-1]] + + job_result = 0 + if all(state == 'success' for state in task_states): + job_result = 1 + + return [ + Metric(self.namespace, 'JobResult', job_result, + [Tag('ApplicationName', self.application_name)]), + Metric(self.namespace, 'JobDuration', duration, + [Tag('ApplicationName', self.application_name)]) + ] + + +# noinspection PyAbstractClass +class CloudWatchHook(AwsHook): + """ + Interact with AWS CloudWatch. + """ + + def __init__(self, region_name=None, *args, **kwargs): + super().__init__(*args, **kwargs) + self.region_name = region_name + self.conn = self.get_client_type('cloudwatch', self.region_name) + + def get_conn(self): + return self.conn + + def put_metric_data(self, metric): + value = metric.value + + cloudwatch = self.get_conn() + + dimensions = [{'Name': tag.name, 'Value': tag.value} for tag in metric.tags] + + cloudwatch.put_metric_data( + Namespace=metric.namespace, + MetricData=[ + { + 'MetricName': metric.name, + 'Dimensions': dimensions, + 'Timestamp': datetime.utcnow(), + 'Value': value, + 'Unit': 'None' + } + ] + ) + + +class Metric: + """ + Metric. + :param namespace: namespace. + :type name: str + :param name: name. + :type name: str + :param value: value. + :type value: float + :param tags: list of tags. + :type tags: List[str] + """ + + def __init__( + self, + namespace, + name, + value, + tags): + self.namespace = namespace + self.name = name + self.value = value + self.tags = tags + + +class Tag: + def __init__( + self, + name, + value): + self.name = name + self.value = value diff --git a/rainbow/runners/airflow/operators/kubernetes_pod_operator.py b/rainbow/runners/airflow/operators/kubernetes_pod_operator.py new file mode 100644 index 0000000..a7b0bdd --- /dev/null +++ b/rainbow/runners/airflow/operators/kubernetes_pod_operator.py @@ -0,0 +1,140 @@ +from airflow.contrib.operators.kubernetes_pod_operator import KubernetesPodOperator +import json +import traceback +from airflow.models import DAG, TaskInstance +from airflow.utils import timezone +from random import randint + + +def split_list(seq, num): + avg = len(seq) / float(num) + out = [] + last = 0.0 + + while last < len(seq): + out.append(seq[int(last):int(last + avg)]) + last += avg + + return out + + +class ConfigureParallelExecutionOperator(KubernetesPodOperator): + + def __init__(self, + config_type=None, + config_path=None, + executors=1, + *args, + **kwargs): + namespace = kwargs['namespace'] + image = kwargs['image'] + name = kwargs['name'] + + del kwargs['namespace'] + del kwargs['image'] + del kwargs['name'] + + super().__init__( + namespace=namespace, + image=image, + name=name, + *args, + **kwargs) + self.config_type = config_type + self.config_path = config_path + self.executors = executors + + def execute(self, context): + config_dict = {} + + self.log.info(f'config type: {self.config_type}') + + if self.config_type: + if self.config_type == 'file': + config_dict = {} # future feature: return config from file + elif self.config_type == 'sql': + config_dict = {} # future feature: return from sql config + elif self.config_type == 'task': + ti = context['task_instance'] + self.log.info(self.config_path) + config_dict = ti.xcom_pull(task_ids=self.config_path) + elif self.config_type == 'static': + config_dict = json.loads(self.config_path) + else: + raise ValueError(f'Unknown config type: {self.config_type}') + + run_id = context['dag_run'].run_id + + return_conf = {'config_type': self.config_type, + 'splits': {'0': {'run_id': run_id, 'configs': []}}} + + if config_dict: + self.log.info(f'configs dict: {config_dict}') + + configs = config_dict['configs'] + + self.log.info(f'configs: {configs}') + + config_splits = split_list(configs, self.executors) + + for i in range(self.executors): + return_conf['splits'][str(i)] = {'run_id': run_id, 'configs': config_splits[i]} + + return return_conf + + def run_pod(self, context): + return super().execute(context) + + +class ConfigurableKubernetesPodOperator(KubernetesPodOperator): + + def __init__(self, + config_task_id, + task_split, + *args, + **kwargs): + namespace = kwargs['namespace'] + image = kwargs['image'] + name = kwargs['name'] + + del kwargs['namespace'] + del kwargs['image'] + del kwargs['name'] + + super().__init__( + namespace=namespace, + image=image, + name=name, + *args, + **kwargs) + + self.config_task_id = config_task_id + self.task_split = task_split + + def execute(self, context): + if self.config_task_id: + ti = context['task_instance'] + + config = ti.xcom_pull(task_ids=self.config_task_id) + + if config: + split = {} + + if 'configs' in config: + split = configs + else: + split = config['splits'][str(self.task_split)] + + self.log.info(split) + + if split and split['configs']: + self.env_vars.update({'DATA_PIPELINE_CONFIG': json.dumps(split)}) + return super().execute(context) + else: + self.log.info( + f'Empty split config for split {self.task_split}. split config: {split}. config: {config}') + else: + raise ValueError('Config not found in task: ' + self.config_task_id) + else: + self.env_vars.update({'DATA_PIPELINE_CONFIG': '{}'}) + return super().execute(context) diff --git a/rainbow/sql/__init__.py b/rainbow/sql/__init__.py new file mode 100644 index 0000000..217e5db --- /dev/null +++ b/rainbow/sql/__init__.py @@ -0,0 +1,17 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..217e5db --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,17 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/tests/runners/__init__.py b/tests/runners/__init__.py new file mode 100644 index 0000000..217e5db --- /dev/null +++ b/tests/runners/__init__.py @@ -0,0 +1,17 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/tests/runners/airflow/__init__.py b/tests/runners/airflow/__init__.py new file mode 100644 index 0000000..217e5db --- /dev/null +++ b/tests/runners/airflow/__init__.py @@ -0,0 +1,17 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/tests/runners/airflow/compiler/__init__.py b/tests/runners/airflow/compiler/__init__.py new file mode 100644 index 0000000..217e5db --- /dev/null +++ b/tests/runners/airflow/compiler/__init__.py @@ -0,0 +1,17 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. diff --git a/tests/runners/airflow/compiler/rainbow.yml b/tests/runners/airflow/compiler/rainbow.yml new file mode 100644 index 0000000..45333a8 --- /dev/null +++ b/tests/runners/airflow/compiler/rainbow.yml @@ -0,0 +1,115 @@ + +--- +name: MyPipeline +owner: Bosco Albert Baracus +pipeline: + timeout-minutes: 45 + schedule: 0 * 1 * * + metrics-namespace: TestNamespace + tasks: + - name: mytask1 + type: sql + description: mytask1 is cool + query: "select * from mytable" + overrides: + - prod: + partition-columns: dt + output-table: test.test_impression_prod + output-path: s3://mybucket/myproject-test/impression + emr-cluster-name: spark-playground-prod + - stg: + query: "select * from mytable" + partition-columns: dt + output-table: test.test_impression_stg + output-path: s3://mybucket/haya-test/impression + emr-cluster-name: spark-playground-staging + tasks: + - name: my_static_config_task + type: python + description: my 1st ds task + artifact-id: mytask1artifactid + source: mytask1folder + env-vars: + env1: "a" + env2: "b" + config-type: static + config-path: "{\"configs\": [ { \"campaign_id\": 10 }, { \"campaign_id\": 20 } ]}" + cmd: python -u my_app.py + - task: + name: my_no_config_task + type: python + description: my 2nd ds task + artifact-id: mytask1artifactid + env-vars: + env1: "a" + env2: "b" + request-cpu: 100m + request-memory: 65M + cmd: python -u my_app.py foo bar + - task: + name: my_create_custom_config_task + type: python + description: my 2nd ds task + artifact-id: myconftask + source: myconftask + output-config-path: /my_conf.json + env-vars: + env1: "a" + env2: "b" + cmd: python -u my_app.py foo bar + - task: + name: my_custom_config_task + type: python + description: my 2nd ds task + artifact-id: mytask1artifactid + config-type: task + config-path: my_create_custom_config_task + env-vars: + env1: "a" + env2: "b" + cmd: python -u my_app.py foo bar + - task: + name: my_parallelized_static_config_task + type: python + description: my 3rd ds task + artifact-id: mytask1artifactid + executors: 5 + env-vars: + env1: "x" + env2: "y" + myconf: $CONFIG_FILE + config-type: static + config-path: "{\"configs\": [ { \"campaign_id\": 10 }, { \"campaign_id\": 20 }, { \"campaign_id\": 30 }, { \"campaign_id\": 40 }, { \"campaign_id\": 50 }, { \"campaign_id\": 60 }, { \"campaign_id\": 70 }, { \"campaign_id\": 80 } ]}" + cmd: python -u my_app.py $CONFIG_FILE + - task: + name: my_parallelized_custom_config_task + type: python + description: my 4th ds task + artifact-id: mytask1artifactid + executors: 5 + config-type: task + config-path: my_create_custom_config_task + cmd: python -u my_app.py + - task: + name: my_parallelized_no_config_task + type: python + description: my 4th ds task + artifact-id: mytask1artifactid + executors: 5 + cmd: python -u my_app.py +services: + - service: + name: myserver1 + type: python-server + description: my python server + artifact-id: myserver1artifactid + source: myserver1logicfolder + endpoints: + - endpoint: + path: /myendpoint1 + module: mymodule1 + function: myfun1 + - endpoint: + path: /myendpoint2 + module: mymodule2 + function: myfun2 diff --git a/tests/runners/airflow/compiler/test_rainbow_compiler.py b/tests/runners/airflow/compiler/test_rainbow_compiler.py new file mode 100644 index 0000000..6e73d8f --- /dev/null +++ b/tests/runners/airflow/compiler/test_rainbow_compiler.py @@ -0,0 +1,33 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import unittest + +from rainbow.runners.airflow.compiler import rainbow_compiler + + +class TestRainbowCompiler(unittest.TestCase): + + def test_parse(self): + expected = {'name': 'MyPipeline', 'owner': 'Bosco Albert Baracus', 'pipeline': {'timeout-minutes': 45, 'schedule': '0 * 1 * *', 'metrics-namespace': 'TestNamespace', 'tasks': [{'name': 'mytask1', 'type': 'sql', 'description': 'mytask1 is cool', 'query': 'select * from mytable', 'overrides': [{'prod': None, 'partition-columns': 'dt', 'output-table': 'test.test_impression_prod', 'output-path': 's3://mybucket/myproject-test/impression', 'emr-cluster-name': 'spark-playground-prod'}, [...] + actual = rainbow_compiler.parse_yaml('tests/runners/airflow/compiler/rainbow.yml') + self.assertEqual(expected, actual) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/runners/airflow/operators/__init__.py b/tests/runners/airflow/operators/__init__.py new file mode 100644 index 0000000..217e5db --- /dev/null +++ b/tests/runners/airflow/operators/__init__.py @@ -0,0 +1,17 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License.
