From 0a97314e98162374be00ec4fe6cda43b2e19c037 Mon Sep 17 00:00:00 2001 From: Dmytro Kazanzhy Date: Wed, 23 Feb 2022 19:12:23 +0200 Subject: [PATCH] Add Apache Kafka integration --- .../airflow_providers_bug_report.yml | 1 + airflow/providers/apache/kafka/CHANGELOG.rst | 25 +++ airflow/providers/apache/kafka/__init__.py | 16 ++ .../providers/apache/kafka/hooks/__init__.py | 16 ++ airflow/providers/apache/kafka/hooks/kafka.py | 190 ++++++++++++++++++ airflow/providers/apache/kafka/provider.yaml | 43 ++++ airflow/ui/src/views/Docs.tsx | 1 + .../commits.rst | 27 +++ .../connections/kafka.rst | 150 ++++++++++++++ .../index.rst | 79 ++++++++ .../installing-providers-from-sources.rst | 18 ++ docs/apache-airflow/extra-packages-ref.rst | 2 + docs/integration-logos/apache/kafka.png | Bin 0 -> 44753 bytes docs/spelling_wordlist.txt | 2 + setup.py | 2 + tests/providers/apache/kafka/__init__.py | 16 ++ .../providers/apache/kafka/hooks/__init__.py | 16 ++ .../apache/kafka/hooks/test_kafka.py | 36 ++++ 18 files changed, 640 insertions(+) create mode 100644 airflow/providers/apache/kafka/CHANGELOG.rst create mode 100644 airflow/providers/apache/kafka/__init__.py create mode 100644 airflow/providers/apache/kafka/hooks/__init__.py create mode 100644 airflow/providers/apache/kafka/hooks/kafka.py create mode 100644 airflow/providers/apache/kafka/provider.yaml create mode 100644 docs/apache-airflow-providers-apache-kafka/commits.rst create mode 100644 docs/apache-airflow-providers-apache-kafka/connections/kafka.rst create mode 100644 docs/apache-airflow-providers-apache-kafka/index.rst create mode 100644 docs/apache-airflow-providers-apache-kafka/installing-providers-from-sources.rst create mode 100644 docs/integration-logos/apache/kafka.png create mode 100644 tests/providers/apache/kafka/__init__.py create mode 100644 tests/providers/apache/kafka/hooks/__init__.py create mode 100644 tests/providers/apache/kafka/hooks/test_kafka.py diff --git a/.github/ISSUE_TEMPLATE/airflow_providers_bug_report.yml b/.github/ISSUE_TEMPLATE/airflow_providers_bug_report.yml index 2e5b0267b0ae7..1d33c569cfcb6 100644 --- a/.github/ISSUE_TEMPLATE/airflow_providers_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/airflow_providers_bug_report.yml @@ -32,6 +32,7 @@ body: - apache-druid - apache-hdfs - apache-hive + - apache-kafka - apache-kylin - apache-livy - apache-pig diff --git a/airflow/providers/apache/kafka/CHANGELOG.rst b/airflow/providers/apache/kafka/CHANGELOG.rst new file mode 100644 index 0000000000000..cef7dda80708a --- /dev/null +++ b/airflow/providers/apache/kafka/CHANGELOG.rst @@ -0,0 +1,25 @@ + .. 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. + + +Changelog +--------- + +1.0.0 +..... + +Initial version of the provider. diff --git a/airflow/providers/apache/kafka/__init__.py b/airflow/providers/apache/kafka/__init__.py new file mode 100644 index 0000000000000..13a83393a9124 --- /dev/null +++ b/airflow/providers/apache/kafka/__init__.py @@ -0,0 +1,16 @@ +# 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/airflow/providers/apache/kafka/hooks/__init__.py b/airflow/providers/apache/kafka/hooks/__init__.py new file mode 100644 index 0000000000000..13a83393a9124 --- /dev/null +++ b/airflow/providers/apache/kafka/hooks/__init__.py @@ -0,0 +1,16 @@ +# 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/airflow/providers/apache/kafka/hooks/kafka.py b/airflow/providers/apache/kafka/hooks/kafka.py new file mode 100644 index 0000000000000..f2c14bb16f0ed --- /dev/null +++ b/airflow/providers/apache/kafka/hooks/kafka.py @@ -0,0 +1,190 @@ +# 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 enum import Enum + +from kafka import BrokerConnection, KafkaAdminClient, KafkaClient, KafkaConsumer, KafkaProducer + +from airflow.hooks.base import BaseHook + + +class KafkaHookClient: + """Simple wrapper of Kafka classes""" + + def __init__(self, **kwargs): + """ + Take and save configs that common for Kafka classes + + 'bootstrap_servers' + 'client_id' + """ + self.configs = kwargs + + def create_broker_connection(self, **kwargs) -> BrokerConnection: + """Returns BrokerConnection instance""" + broker_connection_conf = dict(self.configs, **kwargs) + return BrokerConnection(**broker_connection_conf) + + def create_internal_client(self, **kwargs) -> KafkaClient: + """Returns KafkaClient instance""" + internal_client_conf = dict(self.configs, **kwargs) + return KafkaClient(**internal_client_conf) + + def create_admin_client(self, **kwargs) -> KafkaAdminClient: + """Returns KafkaAdminClient instance""" + admin_client_conf = dict(self.configs, **kwargs) + return KafkaAdminClient(**admin_client_conf) + + def create_producer(self, **kwargs) -> KafkaProducer: + """ + Returns KafkaProducer instance + + Valid parameters: + key_serializer: None + value_serializer: None + acks: 1 + bootstrap_topics_filter: set() + compression_type: None + retries: 0 + batch_size: 16384 + linger_ms: 0 + buffer_memory: 33554432 + max_block_ms: 60000 + max_request_size: 1048576 + partitioner: DefaultPartitioner() + """ + producer_conf = dict(self.configs, **kwargs) + return KafkaProducer(**producer_conf) + + def create_consumer(self, **kwargs) -> KafkaConsumer: + """ + Returns KafkaConsumer instance. + + Valid arguments: + group_id: None + key_deserializer: None + value_deserializer: None + fetch_max_wait_ms: 500 + fetch_min_bytes: 1 + fetch_max_bytes: 52428800 + max_partition_fetch_bytes: 1 * 1024 * 1024 + max_poll_records: 500 + max_poll_interval_ms: 300000 + auto_offset_reset: 'latest' + enable_auto_commit: True + auto_commit_interval_ms: 5000 + default_offset_commit_callback: lambda offsets, response: True + check_crcs: True + session_timeout_ms: 10000 + heartbeat_interval_ms: 3000 + consumer_timeout_ms: float('inf') + legacy_iterator: False # enable to revert to < 1.4.7 iterator + metric_group_prefix: 'consumer' + exclude_internal_topics: True + partition_assignment_strategy: (RangePartitionAssignor, RoundRobinPartitionAssignor) + """ + consumer_conf = dict(self.configs, **kwargs) + return KafkaConsumer(configs=consumer_conf) + + +class SecurityProtocol(Enum): + PLAINTEXT: str = 'PLAINTEXT' + SASL_PLAINTEXT: str = 'SASL_PLAINTEXT' + SASL_SSL: str = 'SASL_SSL' + SSL: str = 'SSL' + + +class SaslMechanism(Enum): + PLAIN: str = 'PLAIN' + GSSAPI: str = 'GSSAPI' + OAUTHBEARER: str = 'OAUTHBEARER' + SCRAM_SHA_256: str = 'SCRAM-SHA-256' + SCRAM_SHA_512: str = 'SCRAM-SHA-512' + + +class KafkaHook(BaseHook): + """ + Interact with Apache Kafka cluster using `python-kafka`. + Hook attribute `conn` returns the client which contains library classes + + .. seealso:: + - https://github.com/dpkp/kafka-python/ + + .. seealso:: + :class:`~airflow.providers.apache.kafka.hooks.kafka.KafkaHook` + + :param kafka_conn_id: The connection id to the Kafka cluster + """ + + conn_name_attr = 'kafka_conn_id' + default_conn_name = 'kafka_default' + conn_type = 'kafka' + hook_name = 'Apache Kafka' + + def __init__(self, kafka_conn_id: str = 'kafka_default') -> None: + + super().__init__() + self.kafka_conn_id = kafka_conn_id + + configs = self._get_configs() + + self.client = KafkaHookClient( + bootstrap_servers=self.get_conn_url(), + client_id='apache-airflow-kafka-hook', + **configs, + ) + + def get_conn(self) -> KafkaHookClient: + """Returns a connection object""" + return self.client + + def get_conn_url(self) -> str: + """Get Kafka connection url""" + conn = self.get_connection(self.kafka_conn_id) + + host = 'localhost' if not conn.host else conn.host + port = 9092 if not conn.port else conn.port + + servers = map(lambda h: h if ':' in h else f'{h}:{port}', host.split(',')) + + return ','.join(servers) + + def _get_configs(self) -> dict: + """Generates configs for Kafka classes""" + conn = self.get_connection(self.kafka_conn_id) + configs = conn.extra_dejson.copy() + + configs['security_protocol'] = SecurityProtocol(conn.schema.upper()).value + + if configs['security_protocol'] in ('SASL_PLAINTEXT', 'SASL_SSL'): + mechanism = configs.get('sasl_mechanism', 'PLAIN').upper() + configs['sasl_mechanism'] = SaslMechanism(mechanism) + if configs['sasl_mechanism'] == 'OAUTHBEARER': + token_provider = object() + setattr(token_provider, 'token', lambda _: conn.password) + configs['sasl_oauth_token_provider'] = token_provider + elif configs['sasl_mechanism'] in ('PLAIN', 'SCRAM-SHA-256', 'SCRAM-SHA-512'): + configs['sasl_plain_username'] = conn.login + configs['sasl_plain_password'] = conn.password + elif configs['security_protocol'] == 'SSL': + configs['ssl_keyfile'] = conn.login + configs['ssl_password'] = conn.password + + return configs + + +__all__ = ['KafkaHook'] diff --git a/airflow/providers/apache/kafka/provider.yaml b/airflow/providers/apache/kafka/provider.yaml new file mode 100644 index 0000000000000..52e3e81e9cba2 --- /dev/null +++ b/airflow/providers/apache/kafka/provider.yaml @@ -0,0 +1,43 @@ +# 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. + +--- +package-name: apache-airflow-providers-apache-kafka +name: Apache Kafka +description: | + `Apache Kafka `__ + +versions: + - 1.0.0 + +additional-dependencies: + - apache-airflow>=2.1.0 + +integrations: + - integration-name: Apache Kafka + external-doc-url: https://kafka.apache.org/ + logo: /integration-logos/apache/kafka.png + tags: [apache] + +hooks: + - integration-name: Apache Kafka + python-modules: + - airflow.providers.apache.kafka.hooks.kafka + +connection-types: + - hook-class-name: airflow.providers.apache.kafka.hooks.kafka.KafkaHook + connection-type: kafka diff --git a/airflow/ui/src/views/Docs.tsx b/airflow/ui/src/views/Docs.tsx index ea42ca6fdf96e..7bed2d2f273ae 100644 --- a/airflow/ui/src/views/Docs.tsx +++ b/airflow/ui/src/views/Docs.tsx @@ -45,6 +45,7 @@ const Docs: React.FC = () => { { path: 'apache-druid', name: 'Apache Druid' }, { path: 'apache-hdfs', name: 'Apache HDFS' }, { path: 'apache-hive', name: 'Apache Hive' }, + { path: 'apache-kafka', name: 'Apache Kafka' }, { path: 'apache-kylin', name: 'Apache Kylin' }, { path: 'apache-livy', name: 'Apache Livy' }, { path: 'apache-pig', name: 'Apache Pig' }, diff --git a/docs/apache-airflow-providers-apache-kafka/commits.rst b/docs/apache-airflow-providers-apache-kafka/commits.rst new file mode 100644 index 0000000000000..58968ee35f157 --- /dev/null +++ b/docs/apache-airflow-providers-apache-kafka/commits.rst @@ -0,0 +1,27 @@ + + .. 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. + + +Package apache-airflow-providers-apache-kafka +------------------------------------------------------ + +`Apache Kafka `__ + + +This is detailed commit list of changes for versions provider package: ``apache.kafka``. +For high-level changelog, see :doc:`package information including changelog `. diff --git a/docs/apache-airflow-providers-apache-kafka/connections/kafka.rst b/docs/apache-airflow-providers-apache-kafka/connections/kafka.rst new file mode 100644 index 0000000000000..72d8d507a919b --- /dev/null +++ b/docs/apache-airflow-providers-apache-kafka/connections/kafka.rst @@ -0,0 +1,150 @@ + .. 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. + + +.. _howto/connections:kafka: + +Kafka Connection +================ +The Kafka connection type provides connection to a Apache Kafka cluster. + +Configuring the Connection +-------------------------- + +Host (required) + The host to connect to. By default ``localhost``. + Might be a single host (example: ``host1.com``) + or comma-separated list of hosts (example: ``host1.com,host2.com``) + or comma-separated list of hosts with ports (example: ``host1.com:9092,host2.com:9092``). + +Port (optional) + The port to connect to. By default ``9092``. + If host field contains single host or comma-separated list of hosts without ports + then port will be used for each host. + +Schema (required) + Specify the **security protocol** to communicate with brokers. Valid values are: + + * ``PLAINTEXT`` - Un-authenticated, non-encrypted channel. + * ``SASL_PLAINTEXT`` - SASL authenticated, non-encrypted channel. + * ``SASL_SSL`` - SASL authenticated, SSL channel. + * ``SSL`` - SSL channel. + +Login (optional) + Specify the login to connect. It depends on security protocol. + + - ``PLAINTEXT``: leave empty. + - ``SASL_PLAINTEXT``: in addition depends on ``sasl_mechanism`` + + * if "PLAIN" or one of "SCRAM" then put the ``sasl_plain_username`` value here; + * if "GSSAPI" (kerberos) or "OAUTHBEARER" then leave empty; + + - ``SASL_SSL``: in addition depends on ``sasl_mechanism`` + + * if "PLAIN" or one of "SCRAM" then put the ``sasl_plain_username`` value here; + * if "GSSAPI" (kerberos) or "OAUTHBEARER" then leave empty; + + - ``SSL``: put the ``ssl_keyfile`` value here. + +Password (optional) + Specify the secret which used to connect. It depends on authentication provider. + + - ``PLAINTEXT``: leave empty. + - ``SASL_PLAINTEXT``: in addition depends on ``sasl_mechanism`` + + * if "PLAIN" or one of "SCRAM" then put the ``sasl_plain_password`` value here; + * if "OAUTHBEARER" then put token value here; + * if "GSSAPI" (kerberos) then leave empty; + + - ``SASL_SSL``: in addition depends on ``sasl_mechanism`` + + * if "PLAIN" or one of "SCRAM" then put the ``sasl_plain_password`` value here; + * if "OAUTHBEARER" then put token value here; + * if "GSSAPI" (kerberos) then leave empty; + + - ``SSL``: put the ``ssl_password`` value here. + +Extra (optional) + Specify the extra parameters (as json dictionary) that can be used for Kafka classes. + The following parameters are supported: + + Connection params: + + * ``request_timeout_ms`` - Client request timeout in milliseconds. By default ``30000`` + * ``connections_max_idle_ms`` - . By default ``9 * 60 * 1000`` + * ``reconnect_backoff_ms`` - The amount of time in milliseconds to wait before attempting to reconnect to a given host. By default ``50`` + * ``reconnect_backoff_max_ms`` - The maximum amount of time in milliseconds to backoff/wait when reconnecting to a broker that has repeatedly failed to connect. By default ``1000`` + * ``retry_backoff_ms`` - . By default ``100`` + * ``metadata_max_age_ms`` - . By default ``300000`` + * ``max_in_flight_requests_per_connection`` - Requests are in pipeline to Apache Kafka brokers up to this number of maximum requests per broker connection. By default ``5`` + * ``receive_buffer_bytes`` - The size of the TCP receive buffer (SO_RCVBUF) to use when reading data. By default ``None`` + * ``send_buffer_bytes`` - The size of the TCP send buffer (SO_SNDBUF) to use when sending data. By default ``None`` + * ``api_version_auto_timeout_ms`` - Number of milliseconds to throw a timeout exception from the constructor when checking the broker api version. By default ``2000`` + * ``api_version`` - Specify which Kafka API version to use. Accepted values are: ``(0, 8, 0), (0, 8, 1), (0, 8, 2), (0, 9), (0, 10)``. Default: ``(0, 8, 2)``. + * ``metrics_num_samples`` - . By default ``2`` + * ``metrics_sample_window_ms`` - . By default ``30000`` + + Additional params for SASL. + Specify only when security protocol is ``SASL_PLAINTEXT`` or ``SASL_SSL``. + + * ``sasl_mechanism`` – Authentication mechanism. Valid values are: ``PLAIN``, ``GSSAPI``, ``OAUTHBEARER``, ``SCRAM-SHA-256``, ``SCRAM-SHA-512``. + * ``sasl_plain_username`` – username for sasl authentication. Required for ``PLAIN`` or one of the ``SCRAM`` mechanisms. Is taken from login field. + * ``sasl_plain_password`` – password for sasl authentication. Required for ``PLAIN`` or one of the ``SCRAM`` mechanisms. Is taken from password field. + * ``sasl_kerberos_service_name`` – Service name to include in ``GSSAPI`` sasl mechanism handshake. Default: ``kafka``. + * ``sasl_kerberos_domain_name`` – kerberos domain name to use in ``GSSAPI`` sasl mechanism handshake. Default: one of bootstrap servers. + * ``sasl_oauth_token_provider`` – ``OAUTHBEARER`` token provider instance. Internally implemented class that returns value of password field. + + Additional params for SSL: + Specify only when security protocol is ``SSL`` or ``SASL_SSL``. + + * ``ssl_check_hostname`` – flag to configure whether ssl handshake should verify that the certificate matches the brokers hostname. By default: ``True``. + * ``ssl_cafile`` – optional filename of ca file to use in certificate verification. By default: ``None``. + * ``ssl_certfile`` – optional filename of file in pem format containing the client certificate, as well as any ca certificates needed to establish the certificate’s authenticity. By default: ``None``. + * ``ssl_crlfile`` – optional filename containing the CRL to check for certificate expiration. By default, no CRL check is done, ``None``. + * ``ssl_ciphers`` – optionally set the available ciphers for ssl connections. It should be a string in the OpenSSL cipher list format. By default ``None``. + * ``ssl_keyfile`` – optional filename containing the client private key. Is taken from ``login`` field for ``SSL`` protocol. + * ``ssl_password`` – optional password to decrypt the client private key. Is taken from ``password`` field for ``SSL`` protocol. + + + More details about these Kafka parameters can be found in + `Python API documentation `_. + Example "extras" field: + + .. code-block:: json + + { + "ssl_check_hostname": true, + "ssl_certfile": "/tmp/client-cert.pem" + } + + When specifying the connection as URI (in :envvar:`AIRFLOW_CONN_{CONN_ID}` variable) you should specify it + following the standard syntax of DB connections, where extras are passed as parameters + of the URI (note that all components of the URI should be URL-encoded), like + ``kafka://login:PASSWORD@1.1.1.1:9092/schema?extra_param=smth`` + + .. code-block:: bash + + export AIRFLOW_CONN_KAFKA_DEFAULT='kafka://:TOKEN@1.1.1.1:9092/SASL_PLAINTEXT?ssl_certfile=%2Ftmp%2Fclient-cert.pem' + + But some parameters for the client should be ``int`` or ``bool``, therefore + put URL-encoded json to ``__extra__`` parameter: + + .. code-block:: bash + + export AIRFLOW_CONN_KAFKA_DEFAULT=kafka://:TOKEN@1.1.1.1:9092/SASL_PLAINTEXT?__extra__=%7B%22request_timeout_ms%22%3A45%2C%22ssl_check_hostname%22%3A%20true%7D + + More in `library documentation `_ diff --git a/docs/apache-airflow-providers-apache-kafka/index.rst b/docs/apache-airflow-providers-apache-kafka/index.rst new file mode 100644 index 0000000000000..b9932e9b75378 --- /dev/null +++ b/docs/apache-airflow-providers-apache-kafka/index.rst @@ -0,0 +1,79 @@ + .. 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. + +``apache-airflow-providers-apache-kafka`` +========================================= + +Content +------- +.. toctree:: + :maxdepth: 1 + :caption: Guides + + Connection types + + +.. toctree:: + :maxdepth: 1 + :caption: References + + Python API <_api/airflow/providers/apache/kafka/index> + PyPI Repository + Installing from sources + + +.. THE REMAINDER OF THE FILE IS AUTOMATICALLY GENERATED. IT WILL BE OVERWRITTEN AT RELEASE TIME! + + +.. toctree:: + :maxdepth: 1 + :caption: Commits + + Detailed list of commits + + +Package apache-airflow-providers-apache-kafka +------------------------------------------------------ + +`Apache Kafka `__ + + +Release: 1.0.0 + +Provider package +---------------- + +This is a provider package for ``apache.kafka`` provider. All classes for this provider package +are in ``airflow.providers.apache.kafka`` python package. + +Installation +------------ + +You can install this package on top of an existing Airflow 2.1+ installation via +``pip install apache-airflow-providers-apache-kafka`` + +PIP requirements +---------------- + +================== ================== +PIP package Version required +================== ================== +``apache-airflow`` ``>=2.1.0`` +``kafka-python`` ``>=2.0.2`` +================== ================== + +.. include:: ../../airflow/providers/apache/kafka/CHANGELOG.rst diff --git a/docs/apache-airflow-providers-apache-kafka/installing-providers-from-sources.rst b/docs/apache-airflow-providers-apache-kafka/installing-providers-from-sources.rst new file mode 100644 index 0000000000000..1c90205d15b3a --- /dev/null +++ b/docs/apache-airflow-providers-apache-kafka/installing-providers-from-sources.rst @@ -0,0 +1,18 @@ + .. 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. + +.. include:: ../installing-providers-from-sources.rst diff --git a/docs/apache-airflow/extra-packages-ref.rst b/docs/apache-airflow/extra-packages-ref.rst index e281a45e85d1a..a58409aa34f65 100644 --- a/docs/apache-airflow/extra-packages-ref.rst +++ b/docs/apache-airflow/extra-packages-ref.rst @@ -126,6 +126,8 @@ custom bash/python providers). +---------------------+-----------------------------------------------------+------------------------------------------------+ | apache.hive | ``pip install 'apache-airflow[apache.hive]'`` | All Hive related operators | +---------------------+-----------------------------------------------------+------------------------------------------------+ +| apache.kafka | ``pip install 'apache-airflow[apache.kafka]'`` | All Kafka related operators & hooks | ++---------------------+-----------------------------------------------------+------------------------------------------------+ | apache.kylin | ``pip install 'apache-airflow[apache.kylin]'`` | All Kylin related operators & hooks | +---------------------+-----------------------------------------------------+------------------------------------------------+ | apache.livy | ``pip install 'apache-airflow[apache.livy]'`` | All Livy related operators, hooks & sensors | diff --git a/docs/integration-logos/apache/kafka.png b/docs/integration-logos/apache/kafka.png new file mode 100644 index 0000000000000000000000000000000000000000..8ead3a3845d7a82f449de28e8191c6bc54e4428e GIT binary patch literal 44753 zcmeFYg;x~b^9H<#0+I%T#DYq9gMfq}v2;nNlz{XCN?V}B0!xd4NGOsL0*eS1OS3K= z3rg;i($eo;_8B)_Wuwt_!0!PU&+#@;UQ zAO}BZNKjCau$z~MzoUbXv#__HYu3swu+lXM?9OeIhp(4#k20(SvYQWg=1l^y7T2hL zXyW^2YdLQ-U!dWSy?25B_SGaqbeNq6H0Wm-6Ld@FX})4UqHzizN6YUO`6TAewS2?d z`DZ^gevs3Z{EqBNOe`$vncwAjbKQwhP_h^DVKLmsOgM@2ZqPdc_)cT*4H6*W|Nry< z(EvQ^Ij!=3srkpL#KG<5r^O*71wXD_;V6u?39Pq{ZioLa3L) zI(6*XIQ=Vr|R<>_hFiO>OE?OP-eL|mo&xI~%cMYkvfZ6Xa zkhej7b#4|bRMO7Jk6M;}%Ddcq?qp7-09mPUl^}vWg+KVojH|XB%qZe_L9Mt=PSN(^ zc-;8FfO|m5Z?==MqBLUm&eo6IcCV@t`U*BzIZK|cjL1(4jsywS9%H-4>8@6vj8kqD zwRbi^a&Gv0^&)q*XXK>ey|=IWkC@%UzVoOZjn8vD9Tw!+(vhf@Cz@foQp+8IDj3NN z8J%Gi4}-fkg|FQ;l7Lg6jKeEZn=@6Gn%|Pr-6{$x_mmQvj-mR@@HWt1}|ZmygvpJ_M2CtL7jkmyBfbCS{*)AzK0DK#F1 zZsGS^3N2^0ixxDOr!Yb%L#~Yl``tP}TmLh5{sCdNS-DC^bAb+*d{Jr5baUa)uRoK| zX;1BWHyy=duAlBE@0UkfR85q6rkh=B+g!|6_s5O)+@Rqn=NjbEt>sBGc~4J9 zDnE?l&vr=VY-qtv3Roz@b<+3T$_GN{y7TkvqW%-fLCn5(B<6|$t((`_ks541+AEXH zEBRnGia9>_*?(jH_)t2qq-qt;Doz|E6&S15>0I{b4B2~fZo_->ru23RM(k8DYO+%4 zV$SxLT8ES{UwoPyw<}hbim}VC-uJ;$^c2ecWYi|B-t`AhjaYNNtBw;T1>l`EE_
  • 7mI!HQgdw4SQV`F5xe2 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.0.7', ] +kafka = ['kafka-python>=2.0.2'] kerberos = [ 'pykerberos>=1.1.13', 'requests_kerberos>=0.10.0', @@ -680,6 +681,7 @@ def write_version(filename: str = os.path.join(*[my_dir, "airflow", "git_version 'apache.druid': druid, 'apache.hdfs': hdfs, 'apache.hive': hive, + 'apache.kafka': kafka, 'apache.kylin': kylin, 'apache.livy': http_provider, 'apache.pig': [], diff --git a/tests/providers/apache/kafka/__init__.py b/tests/providers/apache/kafka/__init__.py new file mode 100644 index 0000000000000..13a83393a9124 --- /dev/null +++ b/tests/providers/apache/kafka/__init__.py @@ -0,0 +1,16 @@ +# 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/providers/apache/kafka/hooks/__init__.py b/tests/providers/apache/kafka/hooks/__init__.py new file mode 100644 index 0000000000000..13a83393a9124 --- /dev/null +++ b/tests/providers/apache/kafka/hooks/__init__.py @@ -0,0 +1,16 @@ +# 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/providers/apache/kafka/hooks/test_kafka.py b/tests/providers/apache/kafka/hooks/test_kafka.py new file mode 100644 index 0000000000000..1ba8d237fb846 --- /dev/null +++ b/tests/providers/apache/kafka/hooks/test_kafka.py @@ -0,0 +1,36 @@ +# +# 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 airflow.providers.apache.kafka.hooks.kafka import KafkaHook, KafkaHookClient +from tests.test_utils.config import env_vars + + +class TestKafkaHook: + def test_client_attribute(self): + conn_uri = 'kafka://1.1.1.1:9092/PLAINTEXT' + with env_vars({'AIRFLOW_CONN_KAFKA_DEFAULT': conn_uri}): + hook = KafkaHook('kafka_default') + assert hasattr(hook, 'client') + assert isinstance(hook.client, KafkaHookClient) + + def test_get_conn_url(self): + conn_uri = 'kafka://:XXXXX@1.1.1.1:9092/PLAINTEXT' + with env_vars({'AIRFLOW_CONN_KAFKA_DEFAULT': conn_uri}): + hook = KafkaHook('kafka_default') + assert hook.get_conn_url() == '1.1.1.1:9092'