From 3b3f8b54bf832c5fd51a1441e2c691db59455c7c Mon Sep 17 00:00:00 2001 From: David Blain Date: Tue, 3 Mar 2026 14:46:06 +0100 Subject: [PATCH 01/14] refactor: Add IBM MQ provider --- .../3-airflow_providers_bug_report.yml | 1 + providers/ibm/mq/LICENSE | 201 ++++++++++++++++++ providers/ibm/mq/NOTICE | 5 + providers/ibm/mq/README.rst | 71 +++++++ .../ibm/mq/docs/.latest-doc-only-change.txt | 1 + providers/ibm/mq/docs/changelog.rst | 49 +++++ providers/ibm/mq/docs/commits.rst | 35 +++ providers/ibm/mq/docs/conf.py | 27 +++ providers/ibm/mq/docs/connections/mq.rst | 23 ++ providers/ibm/mq/docs/index.rst | 125 +++++++++++ .../installing-providers-from-sources.rst | 18 ++ .../ibm/mq/docs/integration-logos/ibm-mq.png | Bin 0 -> 67759 bytes providers/ibm/mq/docs/redirects.txt | 1 + providers/ibm/mq/docs/security.rst | 18 ++ providers/ibm/mq/provider.yaml | 55 +++++ providers/ibm/mq/pyproject.toml | 113 ++++++++++ providers/ibm/mq/src/airflow/__init__.py | 17 ++ .../ibm/mq/src/airflow/providers/__init__.py | 17 ++ .../mq/src/airflow/providers/ibm/__init__.py | 17 ++ .../mq/src/airflow/providers/ibm/mq/LICENSE | 201 ++++++++++++++++++ .../src/airflow/providers/ibm/mq/__init__.py | 39 ++++ .../providers/ibm/mq/get_provider_info.py | 58 +++++ .../providers/ibm/mq/hooks/__init__.py | 16 ++ .../src/airflow/providers/ibm/mq/hooks/mq.py | 194 +++++++++++++++++ .../providers/ibm/mq/queues/__init__.py | 16 ++ .../src/airflow/providers/ibm/mq/queues/mq.py | 76 +++++++ .../providers/ibm/mq/triggers/__init__.py | 0 .../airflow/providers/ibm/mq/triggers/mq.py | 65 ++++++ .../providers/ibm/mq/version_compat.py | 39 ++++ providers/ibm/mq/tests/conftest.py | 19 ++ providers/ibm/mq/tests/system/__init__.py | 17 ++ providers/ibm/mq/tests/system/ibm/__init__.py | 17 ++ .../ibm/mq/tests/system/ibm/mq/__init__.py | 16 ++ providers/ibm/mq/tests/unit/__init__.py | 17 ++ providers/ibm/mq/tests/unit/ibm/__init__.py | 17 ++ .../ibm/mq/tests/unit/ibm/mq/__init__.py | 16 ++ .../mq/tests/unit/ibm/mq/hooks/__init__.py | 16 ++ pyproject.toml | 4 + scripts/ci/docker-compose/tests-sources.yml | 1 + 39 files changed, 1638 insertions(+) create mode 100644 providers/ibm/mq/LICENSE create mode 100644 providers/ibm/mq/NOTICE create mode 100644 providers/ibm/mq/README.rst create mode 100644 providers/ibm/mq/docs/.latest-doc-only-change.txt create mode 100644 providers/ibm/mq/docs/changelog.rst create mode 100644 providers/ibm/mq/docs/commits.rst create mode 100644 providers/ibm/mq/docs/conf.py create mode 100644 providers/ibm/mq/docs/connections/mq.rst create mode 100644 providers/ibm/mq/docs/index.rst create mode 100644 providers/ibm/mq/docs/installing-providers-from-sources.rst create mode 100644 providers/ibm/mq/docs/integration-logos/ibm-mq.png create mode 100644 providers/ibm/mq/docs/redirects.txt create mode 100644 providers/ibm/mq/docs/security.rst create mode 100644 providers/ibm/mq/provider.yaml create mode 100644 providers/ibm/mq/pyproject.toml create mode 100644 providers/ibm/mq/src/airflow/__init__.py create mode 100644 providers/ibm/mq/src/airflow/providers/__init__.py create mode 100644 providers/ibm/mq/src/airflow/providers/ibm/__init__.py create mode 100644 providers/ibm/mq/src/airflow/providers/ibm/mq/LICENSE create mode 100644 providers/ibm/mq/src/airflow/providers/ibm/mq/__init__.py create mode 100644 providers/ibm/mq/src/airflow/providers/ibm/mq/get_provider_info.py create mode 100644 providers/ibm/mq/src/airflow/providers/ibm/mq/hooks/__init__.py create mode 100644 providers/ibm/mq/src/airflow/providers/ibm/mq/hooks/mq.py create mode 100644 providers/ibm/mq/src/airflow/providers/ibm/mq/queues/__init__.py create mode 100644 providers/ibm/mq/src/airflow/providers/ibm/mq/queues/mq.py create mode 100644 providers/ibm/mq/src/airflow/providers/ibm/mq/triggers/__init__.py create mode 100644 providers/ibm/mq/src/airflow/providers/ibm/mq/triggers/mq.py create mode 100644 providers/ibm/mq/src/airflow/providers/ibm/mq/version_compat.py create mode 100644 providers/ibm/mq/tests/conftest.py create mode 100644 providers/ibm/mq/tests/system/__init__.py create mode 100644 providers/ibm/mq/tests/system/ibm/__init__.py create mode 100644 providers/ibm/mq/tests/system/ibm/mq/__init__.py create mode 100644 providers/ibm/mq/tests/unit/__init__.py create mode 100644 providers/ibm/mq/tests/unit/ibm/__init__.py create mode 100644 providers/ibm/mq/tests/unit/ibm/mq/__init__.py create mode 100644 providers/ibm/mq/tests/unit/ibm/mq/hooks/__init__.py diff --git a/.github/ISSUE_TEMPLATE/3-airflow_providers_bug_report.yml b/.github/ISSUE_TEMPLATE/3-airflow_providers_bug_report.yml index f7c6642519d3a..8249e5bcb84c5 100644 --- a/.github/ISSUE_TEMPLATE/3-airflow_providers_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/3-airflow_providers_bug_report.yml @@ -77,6 +77,7 @@ body: - grpc - hashicorp - http + - ibm-mq - imap - influxdb - informatica diff --git a/providers/ibm/mq/LICENSE b/providers/ibm/mq/LICENSE new file mode 100644 index 0000000000000..11069edd79019 --- /dev/null +++ b/providers/ibm/mq/LICENSE @@ -0,0 +1,201 @@ + 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. diff --git a/providers/ibm/mq/NOTICE b/providers/ibm/mq/NOTICE new file mode 100644 index 0000000000000..e02aab0589f0d --- /dev/null +++ b/providers/ibm/mq/NOTICE @@ -0,0 +1,5 @@ +Apache Airflow +Copyright 2016-2025 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). diff --git a/providers/ibm/mq/README.rst b/providers/ibm/mq/README.rst new file mode 100644 index 0000000000000..71d2cd6e3df0f --- /dev/null +++ b/providers/ibm/mq/README.rst @@ -0,0 +1,71 @@ + +.. 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. + +.. NOTE! THIS FILE IS AUTOMATICALLY GENERATED AND WILL BE OVERWRITTEN! + +.. IF YOU WANT TO MODIFY TEMPLATE FOR THIS FILE, YOU SHOULD MODIFY THE TEMPLATE + ``PROVIDER_README_TEMPLATE.rst.jinja2`` IN the ``dev/breeze/src/airflow_breeze/templates`` DIRECTORY + +Package ``apache-airflow-providers-ibm-mq`` + +Release: ``0.1.0`` + + +`IBM MQ `__ + + +Provider package +---------------- + +This is a provider package for ``ibm.mq`` provider. All classes for this provider package +are in ``airflow.providers.ibm.mq`` python package. + +You can find package information and changelog for the provider +in the `documentation `_. + +Installation +------------ + +You can install this package on top of an existing Airflow 2 installation (see ``Requirements`` below +for the minimum Airflow version supported) via +``pip install apache-airflow-providers-ibm-mq`` + +The package supports the following python versions: 3.9,3.10,3.11,3.12 + +Requirements +------------ + +============================================= ===================================== +PIP package Version required +============================================= ===================================== +``apache-airflow`` ``>=2.11.0`` +``apache-airflow-providers-common-messaging`` ``>=2.0.0`` +``importlib-resources`` ``>=1.3`` +``ibmmq`` ``>=2.0.4`` +============================================= ===================================== + + +======================================================================================================================== ==================== +Dependent package Extra +======================================================================================================================== ==================== +`apache-airflow-providers-common-compat `_ ``common.compat`` +`apache-airflow-providers-common-messaging `_ ``common.messaging`` +======================================================================================================================== ==================== + +The changelog for the provider package can be found in the +`changelog `_. diff --git a/providers/ibm/mq/docs/.latest-doc-only-change.txt b/providers/ibm/mq/docs/.latest-doc-only-change.txt new file mode 100644 index 0000000000000..f41e3226a6f43 --- /dev/null +++ b/providers/ibm/mq/docs/.latest-doc-only-change.txt @@ -0,0 +1 @@ +7b2ec33c7ad4998d9c9735b79593fcdcd3b9dd1f diff --git a/providers/ibm/mq/docs/changelog.rst b/providers/ibm/mq/docs/changelog.rst new file mode 100644 index 0000000000000..099b5b4ddded4 --- /dev/null +++ b/providers/ibm/mq/docs/changelog.rst @@ -0,0 +1,49 @@ + .. 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. + + +.. NOTE TO CONTRIBUTORS: + Please, only add notes to the Changelog just below the "Changelog" header when there are some breaking changes + and you want to add an explanation to the users on how they are supposed to deal with them. + The changelog is updated and maintained semi-automatically by release manager. + +``apache-airflow-providers-ibm-mq`` + +Changelog +--------- + +0.1.0 +..... + +.. note:: + This release of provider is only available for Airflow 2.10+ as explained in the + Apache Airflow providers support policy _. + +Misc +~~~~ + +* ``Bump min Airflow version in providers to 2.10 (#49843)`` + +.. Below changes are excluded from the changelog. Move them to + appropriate section above if needed. Do not delete the lines(!): + * ``Update description of provider.yaml dependencies (#50231)`` + * ``Avoid committing history for providers (#49907)`` + +0.1.0 +..... + +Initial version of the provider. diff --git a/providers/ibm/mq/docs/commits.rst b/providers/ibm/mq/docs/commits.rst new file mode 100644 index 0000000000000..1b08b59cb402c --- /dev/null +++ b/providers/ibm/mq/docs/commits.rst @@ -0,0 +1,35 @@ + + .. 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. + + .. NOTE! THIS FILE IS AUTOMATICALLY GENERATED AND WILL BE OVERWRITTEN! + + .. IF YOU WANT TO MODIFY THIS FILE, YOU SHOULD MODIFY THE TEMPLATE + `PROVIDER_COMMITS_TEMPLATE.rst.jinja2` IN the `dev/breeze/src/airflow_breeze/templates` DIRECTORY + + .. THE REMAINDER OF THE FILE IS AUTOMATICALLY GENERATED. IT WILL BE OVERWRITTEN! + +Package apache-airflow-providers-ibm-mq +------------------------------------------------------ + +`IBM MQ `__ + + +This is detailed commit list of changes for versions provider package: ``mq``. +For high-level changelog, see :doc:`package information including changelog `. + +.. airflow-providers-commits:: diff --git a/providers/ibm/mq/docs/conf.py b/providers/ibm/mq/docs/conf.py new file mode 100644 index 0000000000000..425d9e512683d --- /dev/null +++ b/providers/ibm/mq/docs/conf.py @@ -0,0 +1,27 @@ +# Disable Flake8 because of all the sphinx imports +# +# 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. +"""Configuration of Providers docs building.""" + +from __future__ import annotations + +import os + +os.environ["AIRFLOW_PACKAGE_NAME"] = "apache-airflow-providers-ibm-mq" + +from docs.provider_conf import * # noqa: F403 diff --git a/providers/ibm/mq/docs/connections/mq.rst b/providers/ibm/mq/docs/connections/mq.rst new file mode 100644 index 0000000000000..68d928013952c --- /dev/null +++ b/providers/ibm/mq/docs/connections/mq.rst @@ -0,0 +1,23 @@ + .. 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/connection:mq: + +MQ connection +============= + +The MQ connection type enables connection to an IBM MQ. diff --git a/providers/ibm/mq/docs/index.rst b/providers/ibm/mq/docs/index.rst new file mode 100644 index 0000000000000..7c6d0b8535d03 --- /dev/null +++ b/providers/ibm/mq/docs/index.rst @@ -0,0 +1,125 @@ + .. 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-ibm-mq`` +=================================== + + +.. toctree:: + :hidden: + :maxdepth: 1 + :caption: Basics + + Home + Changelog + Security + +.. toctree:: + :hidden: + :maxdepth: 1 + :caption: References + + Python API <_api/airflow/providers/ibm/mq/index> + +.. toctree:: + :hidden: + :maxdepth: 1 + :caption: System tests + + System Tests <_api/tests/system/ibm/mq/index> + +.. toctree:: + :hidden: + :maxdepth: 1 + :caption: Resources + + PyPI Repository + Example Dags + Installing from sources + +.. toctree:: + :hidden: + :maxdepth: 1 + :caption: Guides + + Connection types + +.. THE REMAINDER OF THE FILE IS AUTOMATICALLY GENERATED. IT WILL BE OVERWRITTEN AT RELEASE TIME! + + +.. toctree:: + :hidden: + :maxdepth: 1 + :caption: Commits + + Detailed list of commits + + +apache-airflow-providers-ibm-mq package +--------------------------------------- + +`IBM MQ `__ + + +Release: 0.1.0 + +Provider package +---------------- + +This package is for the ``ibm.mq`` provider. +All classes for this package are included in the ``airflow.providers.ibm.mq`` python package. + +Installation +------------ + +This provider requires the `IBM MQ Redistributable Client `_ to be installed. + +You can install this package on top of an existing Airflow installation via +``pip install apache-airflow-providers-ibm-mq``. +For the minimum Airflow version supported, see ``Requirements`` below. + + +Requirements +------------ + +The minimum Apache Airflow version supported by this provider distribution is ``2.11.0``. + +============================================= ===================================== +PIP package Version required +============================================= ===================================== +``apache-airflow`` ``>=2.11.0`` +``apache-airflow-providers-common-messaging`` ``>=2.0.0`` +``importlib-resources`` ``>=1.3`` +``ibmmq`` ``>=2.0.4`` +============================================= ===================================== + + +======================================================================================================================== ==================== +Dependent package Extra +======================================================================================================================== ==================== +`apache-airflow-providers-common-compat `_ ``common.compat`` +`apache-airflow-providers-common-messaging `_ ``common.messaging`` +======================================================================================================================== ==================== + +Downloading official packages +----------------------------- + +You can download officially released packages and verify their checksums and signatures from the +`Official Apache Download site `_ + +* `The apache-airflow-providers-ibm-mq 0.1.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-ibm-mq 0.1.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/ibm/mq/docs/installing-providers-from-sources.rst b/providers/ibm/mq/docs/installing-providers-from-sources.rst new file mode 100644 index 0000000000000..fdbb17d017579 --- /dev/null +++ b/providers/ibm/mq/docs/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:: /../../../../devel-common/src/sphinx_exts/includes/installing-providers-from-sources.rst diff --git a/providers/ibm/mq/docs/integration-logos/ibm-mq.png b/providers/ibm/mq/docs/integration-logos/ibm-mq.png new file mode 100644 index 0000000000000000000000000000000000000000..e5c1a0820df2dd91886bb497b65caeec891e1829 GIT binary patch literal 67759 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 UHq> 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 literal 0 HcmV?d00001 diff --git a/providers/ibm/mq/docs/redirects.txt b/providers/ibm/mq/docs/redirects.txt new file mode 100644 index 0000000000000..e37560e5ca516 --- /dev/null +++ b/providers/ibm/mq/docs/redirects.txt @@ -0,0 +1 @@ +connections/index.rst connections/mq.rst diff --git a/providers/ibm/mq/docs/security.rst b/providers/ibm/mq/docs/security.rst new file mode 100644 index 0000000000000..351ff007ebf2f --- /dev/null +++ b/providers/ibm/mq/docs/security.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:: /../../../../devel-common/src/sphinx_exts/includes/security.rst diff --git a/providers/ibm/mq/provider.yaml b/providers/ibm/mq/provider.yaml new file mode 100644 index 0000000000000..b2e2d1b9cdc32 --- /dev/null +++ b/providers/ibm/mq/provider.yaml @@ -0,0 +1,55 @@ +# 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-ibm-mq +name: IBM MQ + +state: ready +lifecycle: incubation +source-date-epoch: 1758787200 +description: | + `IBM MQ `__ +# Note that those versions are maintained by release manager - do not update them manually +# with the exception of case where other provider in sources has >= new provider version. +# In such case adding >= NEW_VERSION and bumping to NEW_VERSION in a provider have +# to be done in the same PR +versions: + - 0.1.0 + +integrations: + - integration-name: IBM MQ + external-doc-url: https://www.ibm.com/products/mq + logo: /docs/integration-logos/ibm-mq.png + tags: [apache] + +hooks: + - integration-name: IBM MQ + python-modules: + - airflow.providers.ibm.mq.hooks.mq + +connection-types: + - hook-class-name: airflow.providers.ibm.mq.hooks.mq.IBMMQHook + connection-type: mq + +triggers: + - integration-name: IBM MQ + python-modules: + - airflow.providers.ibm.mq.triggers.mq + +queues: + - airflow.providers.ibm.mq.queues.mq.IBMMQMessageQueueProvider diff --git a/providers/ibm/mq/pyproject.toml b/providers/ibm/mq/pyproject.toml new file mode 100644 index 0000000000000..4333b27c15088 --- /dev/null +++ b/providers/ibm/mq/pyproject.toml @@ -0,0 +1,113 @@ +# 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. + +# NOTE! THIS FILE IS AUTOMATICALLY GENERATED AND WILL BE OVERWRITTEN! + +# IF YOU WANT TO MODIFY THIS FILE EXCEPT DEPENDENCIES, YOU SHOULD MODIFY THE TEMPLATE +# `pyproject_TEMPLATE.toml.jinja2` IN the `dev/breeze/src/airflow_breeze/templates` DIRECTORY +[build-system] +requires = ["flit_core==3.12.0"] +build-backend = "flit_core.buildapi" + +[project] +name = "apache-airflow-providers-ibm-mq" +version = "0.1.0" +description = "Provider package apache-airflow-providers-ibm-mq for Apache Airflow" +readme = "README.rst" +license = "Apache-2.0" +license-files = ['LICENSE', 'NOTICE'] +authors = [ + {name="Apache Software Foundation", email="dev@airflow.apache.org"}, +] +maintainers = [ + {name="Apache Software Foundation", email="dev@airflow.apache.org"}, +] +keywords = [ "airflow-provider", "ibm.mq", "airflow", "integration" ] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "Framework :: Apache Airflow", + "Framework :: Apache Airflow :: Provider", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: System :: Monitoring", +] +requires-python = ">=3.10" + +# The dependencies should be modified in place in the generated file. +# Any change in the dependencies is preserved when the file is regenerated +# Make sure to run ``prek update-providers-dependencies --all-files`` +# After you modify the dependencies, and rebuild your Breeze CI image with ``breeze ci-image build`` +dependencies = [ + "apache-airflow>=2.11.0", + "apache-airflow-providers-common-messaging>=2.0.0", + "importlib-resources>=1.3", + "ibmmq>=2.0.4", +] + +[dependency-groups] +dev = [ + "apache-airflow", + "apache-airflow-task-sdk", + "apache-airflow-devel-common", + "apache-airflow-providers-common-messaging", +] + +# To build docs: +# +# uv run --group docs build-docs +# +# To enable auto-refreshing build with server: +# +# uv run --group docs build-docs --autobuild +# +# To see more options: +# +# uv run --group docs build-docs --help +# +docs = [ + "apache-airflow-devel-common[docs]" +] + +[tool.uv.sources] +# These names must match the names as defined in the pyproject.toml of the workspace items, +# *not* the workspace folder paths +apache-airflow = {workspace = true} +apache-airflow-devel-common = {workspace = true} +apache-airflow-task-sdk = {workspace = true} +apache-airflow-providers-common-messaging = {workspace = true} +apache-airflow-providers-standard = {workspace = true} + +[project.urls] +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-ibm-mq/0.1.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-ibm-mq/0.1.0/changelog.html" +"Bug Tracker" = "https://github.com/apache/airflow/issues" +"Source Code" = "https://github.com/apache/airflow" +"Slack Chat" = "https://s.apache.org/airflow-slack" +"Mastodon" = "https://fosstodon.org/@airflow" +"YouTube" = "https://www.youtube.com/channel/UCSXwxpWZQ7XZ1WL3wqevChA/" + +[project.entry-points."apache_airflow_provider"] +provider_info = "airflow.providers.ibm.mq.get_provider_info:get_provider_info" + +[tool.flit.module] +name = "airflow.providers.ibm.mq" diff --git a/providers/ibm/mq/src/airflow/__init__.py b/providers/ibm/mq/src/airflow/__init__.py new file mode 100644 index 0000000000000..e8fd22856438c --- /dev/null +++ b/providers/ibm/mq/src/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. +__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore diff --git a/providers/ibm/mq/src/airflow/providers/__init__.py b/providers/ibm/mq/src/airflow/providers/__init__.py new file mode 100644 index 0000000000000..e8fd22856438c --- /dev/null +++ b/providers/ibm/mq/src/airflow/providers/__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. +__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore diff --git a/providers/ibm/mq/src/airflow/providers/ibm/__init__.py b/providers/ibm/mq/src/airflow/providers/ibm/__init__.py new file mode 100644 index 0000000000000..e8fd22856438c --- /dev/null +++ b/providers/ibm/mq/src/airflow/providers/ibm/__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. +__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore diff --git a/providers/ibm/mq/src/airflow/providers/ibm/mq/LICENSE b/providers/ibm/mq/src/airflow/providers/ibm/mq/LICENSE new file mode 100644 index 0000000000000..11069edd79019 --- /dev/null +++ b/providers/ibm/mq/src/airflow/providers/ibm/mq/LICENSE @@ -0,0 +1,201 @@ + 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. diff --git a/providers/ibm/mq/src/airflow/providers/ibm/mq/__init__.py b/providers/ibm/mq/src/airflow/providers/ibm/mq/__init__.py new file mode 100644 index 0000000000000..f809790763f0c --- /dev/null +++ b/providers/ibm/mq/src/airflow/providers/ibm/mq/__init__.py @@ -0,0 +1,39 @@ +# 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. +# +# NOTE! THIS FILE IS AUTOMATICALLY GENERATED AND WILL BE +# OVERWRITTEN WHEN PREPARING DOCUMENTATION FOR THE PACKAGES. +# +# IF YOU WANT TO MODIFY THIS FILE, YOU SHOULD MODIFY THE TEMPLATE +# `PROVIDER__INIT__PY_TEMPLATE.py.jinja2` IN the `dev/breeze/src/airflow_breeze/templates` DIRECTORY +# +from __future__ import annotations + +import packaging.version + +from airflow import __version__ as airflow_version + +__all__ = ["__version__"] + +__version__ = "0.1.0" + +if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse( + "2.11.0" +): + raise RuntimeError( + f"The package `apache-airflow-providers-ibm-mq:{__version__}` needs Apache Airflow 2.11.0+" + ) diff --git a/providers/ibm/mq/src/airflow/providers/ibm/mq/get_provider_info.py b/providers/ibm/mq/src/airflow/providers/ibm/mq/get_provider_info.py new file mode 100644 index 0000000000000..6b96bc91d2abb --- /dev/null +++ b/providers/ibm/mq/src/airflow/providers/ibm/mq/get_provider_info.py @@ -0,0 +1,58 @@ +# 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. + +# NOTE! THIS FILE IS AUTOMATICALLY GENERATED AND WILL BE OVERWRITTEN! +# +# IF YOU WANT TO MODIFY THIS FILE, YOU SHOULD MODIFY THE TEMPLATE +# `get_provider_info_TEMPLATE.py.jinja2` IN the `dev/breeze/src/airflow_breeze/templates` DIRECTORY + + +def get_provider_info(): + return { + "package-name": "apache-airflow-providers-ibm-mq", + "name": "IBM MQ", + "description": "`IBM MQ `__\n", + "integrations": [ + { + "integration-name": "IBM MQ", + "external-doc-url": "https://www.ibm.com/products/mq/", + "logo": "/docs/integration-logos/ibm-mq.png", + "tags": ["apache"], + } + ], + "hooks": [ + { + "integration-name": "IBM MQ", + "python-modules": ["airflow.providers.ibm.mq.hooks.mq"], + } + ], + "triggers": [ + { + "integration-name": "IBM MQ", + "python-modules": [ + "airflow.providers.ibm.mq.triggers.mq", + ], + } + ], + "connection-types": [ + { + "hook-class-name": "airflow.providers.ibm.mq.hooks.mq.IBMMQHook", + "connection-type": "mq", + } + ], + "queues": ["airflow.providers.ibm.mq.queues.mq.IBMMQMessageQueueProvider"], + } diff --git a/providers/ibm/mq/src/airflow/providers/ibm/mq/hooks/__init__.py b/providers/ibm/mq/src/airflow/providers/ibm/mq/hooks/__init__.py new file mode 100644 index 0000000000000..13a83393a9124 --- /dev/null +++ b/providers/ibm/mq/src/airflow/providers/ibm/mq/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/providers/ibm/mq/src/airflow/providers/ibm/mq/hooks/mq.py b/providers/ibm/mq/src/airflow/providers/ibm/mq/hooks/mq.py new file mode 100644 index 0000000000000..06855c6ade205 --- /dev/null +++ b/providers/ibm/mq/src/airflow/providers/ibm/mq/hooks/mq.py @@ -0,0 +1,194 @@ +# +# 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 __future__ import annotations + +import asyncio +import json +from contextlib import suppress, asynccontextmanager +from typing import TYPE_CHECKING, Any + +from asgiref.sync import sync_to_async + +from airflow.providers.common.compat.connection import get_async_connection +from airflow.sdk.bases.hook import BaseHook + +if TYPE_CHECKING: + from airflow.providers.common.compat.sdk import Connection + + +class IBMMQHook(BaseHook): + conn_name_attr = "conn_id" + default_conn_name = "mq_default" + conn_type = "mq" + hook_name = "IBM MQ" + + def __init__(self, conn_id: str = default_conn_name): + super().__init__() + self.conn_id = conn_id + self._conn = None + + @classmethod + def get_ui_field_behaviour(cls) -> dict[str, Any]: + """Return custom UI field behaviour for IBM Connection.""" + return { + "hidden_fields": ["schema"], + "placeholders": { + "host": "mq.example.com", + "port": "1414", + "login": "app_user", + "extra": json.dumps( + { + "queue_manager": "QM1", + "channel": "DEV.APP.SVRCONN", + }, + indent=2, + ), + }, + } + + @classmethod + def _connect(cls, conn: Connection): + import ibmmq + + csp = ibmmq.CSP() + csp.CSPUserId = conn.login + csp.CSPPassword = conn.password + + config = conn.extra_dejson + + return ibmmq.connect( + config["queue_manager"], + config["channel"], + f"{conn.host}({conn.port})", + csp=csp, + ) + + @asynccontextmanager + async def get_conn(self): + connection = await get_async_connection(conn_id=self.conn_id) + conn = None + try: + conn = self._connect(connection) + yield conn + finally: + if conn: + with suppress(Exception): + conn.disconnect() + + @classmethod + def _process_message(cls, message): + import ibmmq + + try: + rfh2 = ibmmq.RFH2() + rfh2.unpack(message) + payload_offset = rfh2.get_length() + payload = message[payload_offset:] + return payload.decode("utf-8", errors="ignore") + except Exception: + return message + + async def consume(self, queue_name: str, poll_interval: float = 5) -> str: + """ + Wait for a single message and return its decoded payload. + Retries automatically on connection loss. + """ + import ibmmq + + od = ibmmq.OD() + od.ObjectName = queue_name + + md = ibmmq.MD() + md.Format = ibmmq.CMQC.MQFMT_STRING + md.CodedCharSetId = 1208 + md.Encoding = ibmmq.CMQC.MQENC_NATIVE + + gmo = ibmmq.GMO() + gmo.Options = ( + ibmmq.CMQC.MQGMO_WAIT + | ibmmq.CMQC.MQGMO_NO_SYNCPOINT + | ibmmq.CMQC.MQGMO_CONVERT + ) + gmo.WaitInterval = int(poll_interval * 1000) + + while True: + try: + async with self.get_conn() as qmgr: + q = ibmmq.Queue( + qmgr, + od, + ibmmq.CMQC.MQOO_INPUT_AS_Q_DEF, + ) + + async_get = sync_to_async(q.get) + + try: + while True: + try: + message = await async_get(None, md, gmo) + + if message: + return self._process_message(message) + + except ibmmq.MQMIError as e: + if e.reason == ibmmq.CMQC.MQRC_CONNECTION_BROKEN: + self.log.warning( + "MQ connection broken, retrying..." + ) + break + elif e.reason == ibmmq.CMQC.MQRC_NO_MSG_AVAILABLE: + await asyncio.sleep(poll_interval) + continue + else: + raise + finally: + with suppress(Exception): + q.close() + + except Exception: + self.log.exception("MQ consume failed, retrying...") + await asyncio.sleep(poll_interval) + + async def produce(self, queue_name: str, payload: str) -> None: + """ + Put a message on the queue. + """ + import ibmmq + + od = ibmmq.OD() + od.ObjectName = queue_name + + md = ibmmq.MD() + md.Format = ibmmq.CMQC.MQFMT_STRING + md.CodedCharSetId = 1208 + md.Encoding = ibmmq.CMQC.MQENC_NATIVE + + async with self.get_conn() as qmgr: + q = ibmmq.Queue( + qmgr, + od, + ibmmq.CMQC.MQOO_OUTPUT, + ) + + async_put = sync_to_async(q.put) + + try: + await async_put(payload.encode("utf-8"), md) + finally: + with suppress(Exception): + q.close() diff --git a/providers/ibm/mq/src/airflow/providers/ibm/mq/queues/__init__.py b/providers/ibm/mq/src/airflow/providers/ibm/mq/queues/__init__.py new file mode 100644 index 0000000000000..13a83393a9124 --- /dev/null +++ b/providers/ibm/mq/src/airflow/providers/ibm/mq/queues/__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/providers/ibm/mq/src/airflow/providers/ibm/mq/queues/mq.py b/providers/ibm/mq/src/airflow/providers/ibm/mq/queues/mq.py new file mode 100644 index 0000000000000..6db39856e9064 --- /dev/null +++ b/providers/ibm/mq/src/airflow/providers/ibm/mq/queues/mq.py @@ -0,0 +1,76 @@ +# +# 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 __future__ import annotations + +import re +from typing import Any +from urllib.parse import urlparse + +from airflow.providers.common.messaging.providers.base_provider import BaseMessageQueueProvider +from airflow.providers.ibm.mq.hooks.mq import IBMMQHook +from airflow.providers.ibm.mq.triggers.mq import AwaitMessageTrigger +from airflow.providers.ibm.mq.version_compat import AIRFLOW_V_3_0_PLUS + +if AIRFLOW_V_3_0_PLUS: + from airflow.triggers.base import BaseEventTrigger +else: + from airflow.triggers.base import BaseTrigger as BaseEventTrigger # type: ignore + + +# [START queue_regexp] +QUEUE_REGEXP = r"^mq://" +# [END queue_regexp] + + +class IBMMQMessageQueueProvider(BaseMessageQueueProvider): + scheme = "mq" + + def queue_matches(self, queue: str) -> bool: + return bool(re.match(QUEUE_REGEXP, queue)) + + def trigger_class(self) -> type[BaseEventTrigger]: + return AwaitMessageTrigger + + def trigger_kwargs(self, queue: str, **kwargs) -> dict[str, Any]: + """ + Parse URI of format: + mq:/// + """ + + parsed = urlparse(queue) + + if not parsed.netloc: + raise ValueError( + "MQ URI must contain connection id. " + "Expected format: mq:///" + ) + + conn_id = parsed.netloc + + queue_name = parsed.path.lstrip("/") + if not queue_name: + raise ValueError( + "MQ URI must contain queue name. " + "Expected format: mq:///" + ) + + return { + "mq_conn_id": conn_id, + "queue_name": queue_name, + "poll_interval": kwargs.get("poll_interval", 5), + } diff --git a/providers/ibm/mq/src/airflow/providers/ibm/mq/triggers/__init__.py b/providers/ibm/mq/src/airflow/providers/ibm/mq/triggers/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/providers/ibm/mq/src/airflow/providers/ibm/mq/triggers/mq.py b/providers/ibm/mq/src/airflow/providers/ibm/mq/triggers/mq.py new file mode 100644 index 0000000000000..a3cfd40afaac6 --- /dev/null +++ b/providers/ibm/mq/src/airflow/providers/ibm/mq/triggers/mq.py @@ -0,0 +1,65 @@ +# +# 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 __future__ import annotations + +import asyncio +from typing import Any + +from airflow.providers.ibm.mq.hooks.mq import IBMMQHook +from airflow.providers.ibm.mq.version_compat import AIRFLOW_V_3_0_PLUS +from airflow.triggers.base import TriggerEvent + +if AIRFLOW_V_3_0_PLUS: + from airflow.triggers.base import BaseEventTrigger +else: + from airflow.triggers.base import BaseTrigger as BaseEventTrigger # type: ignore + + +class AwaitMessageTrigger(BaseEventTrigger): + def __init__( + self, + mq_conn_id: str, + queue_name: str, + poll_interval: float = 5, + ) -> None: + super().__init__() + self.mq_conn_id = mq_conn_id + self.queue_name = queue_name + self.poll_interval = poll_interval + + def serialize(self) -> tuple[str, dict[str, Any]]: + return ( + f"{self.__class__.__module__}.{self.__class__.__name__}", + { + "mq_conn_id": self.mq_conn_id, + "queue_name": self.queue_name, + "poll_interval": self.poll_interval, + }, + ) + + async def run(self): + try: + event = await IBMMQHook(self.mq_conn_id).consume( + queue_name=self.queue_name, + poll_interval=self.poll_interval, + ) + yield TriggerEvent(event) + + except asyncio.CancelledError: + self.log.info("MQ trigger cancelled") + return diff --git a/providers/ibm/mq/src/airflow/providers/ibm/mq/version_compat.py b/providers/ibm/mq/src/airflow/providers/ibm/mq/version_compat.py new file mode 100644 index 0000000000000..0956edd21112f --- /dev/null +++ b/providers/ibm/mq/src/airflow/providers/ibm/mq/version_compat.py @@ -0,0 +1,39 @@ +# 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. +# +# NOTE! THIS FILE IS COPIED MANUALLY IN OTHER PROVIDERS DELIBERATELY TO AVOID ADDING UNNECESSARY +# DEPENDENCIES BETWEEN PROVIDERS. IF YOU WANT TO ADD CONDITIONAL CODE IN YOUR PROVIDER THAT DEPENDS +# ON AIRFLOW VERSION, PLEASE COPY THIS FILE TO THE ROOT PACKAGE OF YOUR PROVIDER AND IMPORT +# THOSE CONSTANTS FROM IT RATHER THAN IMPORTING THEM FROM ANOTHER PROVIDER OR TEST CODE +# +from __future__ import annotations + + +def get_base_airflow_version_tuple() -> tuple[int, int, int]: + from packaging.version import Version + + from airflow import __version__ + + airflow_version = Version(__version__) + return airflow_version.major, airflow_version.minor, airflow_version.micro + + +AIRFLOW_V_3_0_PLUS = get_base_airflow_version_tuple() >= (3, 0, 0) + +__all__ = [ + "AIRFLOW_V_3_0_PLUS", +] diff --git a/providers/ibm/mq/tests/conftest.py b/providers/ibm/mq/tests/conftest.py new file mode 100644 index 0000000000000..f56ccce0a3f69 --- /dev/null +++ b/providers/ibm/mq/tests/conftest.py @@ -0,0 +1,19 @@ +# 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 __future__ import annotations + +pytest_plugins = "tests_common.pytest_plugin" diff --git a/providers/ibm/mq/tests/system/__init__.py b/providers/ibm/mq/tests/system/__init__.py new file mode 100644 index 0000000000000..e8fd22856438c --- /dev/null +++ b/providers/ibm/mq/tests/system/__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. +__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore diff --git a/providers/ibm/mq/tests/system/ibm/__init__.py b/providers/ibm/mq/tests/system/ibm/__init__.py new file mode 100644 index 0000000000000..e8fd22856438c --- /dev/null +++ b/providers/ibm/mq/tests/system/ibm/__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. +__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore diff --git a/providers/ibm/mq/tests/system/ibm/mq/__init__.py b/providers/ibm/mq/tests/system/ibm/mq/__init__.py new file mode 100644 index 0000000000000..13a83393a9124 --- /dev/null +++ b/providers/ibm/mq/tests/system/ibm/mq/__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/providers/ibm/mq/tests/unit/__init__.py b/providers/ibm/mq/tests/unit/__init__.py new file mode 100644 index 0000000000000..e8fd22856438c --- /dev/null +++ b/providers/ibm/mq/tests/unit/__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. +__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore diff --git a/providers/ibm/mq/tests/unit/ibm/__init__.py b/providers/ibm/mq/tests/unit/ibm/__init__.py new file mode 100644 index 0000000000000..e8fd22856438c --- /dev/null +++ b/providers/ibm/mq/tests/unit/ibm/__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. +__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore diff --git a/providers/ibm/mq/tests/unit/ibm/mq/__init__.py b/providers/ibm/mq/tests/unit/ibm/mq/__init__.py new file mode 100644 index 0000000000000..13a83393a9124 --- /dev/null +++ b/providers/ibm/mq/tests/unit/ibm/mq/__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/providers/ibm/mq/tests/unit/ibm/mq/hooks/__init__.py b/providers/ibm/mq/tests/unit/ibm/mq/hooks/__init__.py new file mode 100644 index 0000000000000..13a83393a9124 --- /dev/null +++ b/providers/ibm/mq/tests/unit/ibm/mq/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/pyproject.toml b/pyproject.toml index e633cba7f4470..02ca60fa55c4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -240,6 +240,9 @@ packages = [] "http" = [ "apache-airflow-providers-http>=4.13.2" ] +"ibm.mq" = [ + "apache-airflow-providers-ibm-mq>=0.1.0" +] "imap" = [ "apache-airflow-providers-imap>=3.8.0" ] @@ -439,6 +442,7 @@ packages = [] "apache-airflow-providers-grpc>=3.7.0", "apache-airflow-providers-hashicorp>=4.0.0", "apache-airflow-providers-http>=4.13.2", + "apache-airflow-providers-ibm-mq>=0.1.0", "apache-airflow-providers-imap>=3.8.0", "apache-airflow-providers-influxdb>=2.8.0", "apache-airflow-providers-informatica>=0.1.0", # Set from MIN_VERSION_OVERRIDE in update_airflow_pyproject_toml.py diff --git a/scripts/ci/docker-compose/tests-sources.yml b/scripts/ci/docker-compose/tests-sources.yml index eb9da3cacebf5..caab1b2a7a19c 100644 --- a/scripts/ci/docker-compose/tests-sources.yml +++ b/scripts/ci/docker-compose/tests-sources.yml @@ -89,6 +89,7 @@ services: - ../../../providers/grpc/tests:/opt/airflow/providers/grpc/tests - ../../../providers/hashicorp/tests:/opt/airflow/providers/hashicorp/tests - ../../../providers/http/tests:/opt/airflow/providers/http/tests + - ../../../providers/ibm/mq/tests:/opt/airflow/providers/ibm/mq/tests - ../../../providers/imap/tests:/opt/airflow/providers/imap/tests - ../../../providers/influxdb/tests:/opt/airflow/providers/influxdb/tests - ../../../providers/informatica/tests:/opt/airflow/providers/informatica/tests From 3b7ade1210d893c61934606b841487d92c8b2da4 Mon Sep 17 00:00:00 2001 From: David Blain Date: Thu, 5 Mar 2026 10:44:32 +0100 Subject: [PATCH 02/14] refactor: Renamed apache-airflow-providers-ibm-mq provider to apache-airflow-providers-ibm --- .../3-airflow_providers_bug_report.yml | 2 +- providers/ibm/mq/README.rst | 6 +++--- providers/ibm/mq/docs/changelog.rst | 2 +- providers/ibm/mq/docs/commits.rst | 2 +- providers/ibm/mq/docs/conf.py | 2 +- providers/ibm/mq/docs/index.rst | 12 ++++++------ providers/ibm/mq/provider.yaml | 2 +- providers/ibm/mq/pyproject.toml | 8 ++++---- .../ibm/mq/src/airflow/providers/ibm/mq/__init__.py | 2 +- .../airflow/providers/ibm/mq/get_provider_info.py | 2 +- pyproject.toml | 6 +++--- 11 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/3-airflow_providers_bug_report.yml b/.github/ISSUE_TEMPLATE/3-airflow_providers_bug_report.yml index 8249e5bcb84c5..2352515e01f0a 100644 --- a/.github/ISSUE_TEMPLATE/3-airflow_providers_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/3-airflow_providers_bug_report.yml @@ -77,7 +77,7 @@ body: - grpc - hashicorp - http - - ibm-mq + - ibm - imap - influxdb - informatica diff --git a/providers/ibm/mq/README.rst b/providers/ibm/mq/README.rst index 71d2cd6e3df0f..602b55bc6c254 100644 --- a/providers/ibm/mq/README.rst +++ b/providers/ibm/mq/README.rst @@ -21,7 +21,7 @@ .. IF YOU WANT TO MODIFY TEMPLATE FOR THIS FILE, YOU SHOULD MODIFY THE TEMPLATE ``PROVIDER_README_TEMPLATE.rst.jinja2`` IN the ``dev/breeze/src/airflow_breeze/templates`` DIRECTORY -Package ``apache-airflow-providers-ibm-mq`` +Package ``apache-airflow-providers-ibm`` Release: ``0.1.0`` @@ -36,14 +36,14 @@ This is a provider package for ``ibm.mq`` provider. All classes for this provide are in ``airflow.providers.ibm.mq`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ You can install this package on top of an existing Airflow 2 installation (see ``Requirements`` below for the minimum Airflow version supported) via -``pip install apache-airflow-providers-ibm-mq`` +``pip install apache-airflow-providers-ibm`` The package supports the following python versions: 3.9,3.10,3.11,3.12 diff --git a/providers/ibm/mq/docs/changelog.rst b/providers/ibm/mq/docs/changelog.rst index 099b5b4ddded4..9a385656940f3 100644 --- a/providers/ibm/mq/docs/changelog.rst +++ b/providers/ibm/mq/docs/changelog.rst @@ -21,7 +21,7 @@ and you want to add an explanation to the users on how they are supposed to deal with them. The changelog is updated and maintained semi-automatically by release manager. -``apache-airflow-providers-ibm-mq`` +``apache-airflow-providers-ibm`` Changelog --------- diff --git a/providers/ibm/mq/docs/commits.rst b/providers/ibm/mq/docs/commits.rst index 1b08b59cb402c..012f250fc9a85 100644 --- a/providers/ibm/mq/docs/commits.rst +++ b/providers/ibm/mq/docs/commits.rst @@ -23,7 +23,7 @@ .. THE REMAINDER OF THE FILE IS AUTOMATICALLY GENERATED. IT WILL BE OVERWRITTEN! -Package apache-airflow-providers-ibm-mq +Package apache-airflow-providers-ibm ------------------------------------------------------ `IBM MQ `__ diff --git a/providers/ibm/mq/docs/conf.py b/providers/ibm/mq/docs/conf.py index 425d9e512683d..681b823b91eef 100644 --- a/providers/ibm/mq/docs/conf.py +++ b/providers/ibm/mq/docs/conf.py @@ -22,6 +22,6 @@ import os -os.environ["AIRFLOW_PACKAGE_NAME"] = "apache-airflow-providers-ibm-mq" +os.environ["AIRFLOW_PACKAGE_NAME"] = "apache-airflow-providers-ibm" from docs.provider_conf import * # noqa: F403 diff --git a/providers/ibm/mq/docs/index.rst b/providers/ibm/mq/docs/index.rst index 7c6d0b8535d03..195639ee5615c 100644 --- a/providers/ibm/mq/docs/index.rst +++ b/providers/ibm/mq/docs/index.rst @@ -15,7 +15,7 @@ specific language governing permissions and limitations under the License. -``apache-airflow-providers-ibm-mq`` +``apache-airflow-providers-ibm`` =================================== @@ -47,7 +47,7 @@ :maxdepth: 1 :caption: Resources - PyPI Repository + PyPI Repository Example Dags Installing from sources @@ -69,7 +69,7 @@ Detailed list of commits -apache-airflow-providers-ibm-mq package +apache-airflow-providers-ibm package --------------------------------------- `IBM MQ `__ @@ -89,7 +89,7 @@ Installation This provider requires the `IBM MQ Redistributable Client `_ to be installed. You can install this package on top of an existing Airflow installation via -``pip install apache-airflow-providers-ibm-mq``. +``pip install apache-airflow-providers-ibm``. For the minimum Airflow version supported, see ``Requirements`` below. @@ -121,5 +121,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-ibm-mq 0.1.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-ibm-mq 0.1.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-ibm 0.1.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-ibm 0.1.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/ibm/mq/provider.yaml b/providers/ibm/mq/provider.yaml index b2e2d1b9cdc32..0f0fb3496779b 100644 --- a/providers/ibm/mq/provider.yaml +++ b/providers/ibm/mq/provider.yaml @@ -16,7 +16,7 @@ # under the License. --- -package-name: apache-airflow-providers-ibm-mq +package-name: apache-airflow-providers-ibm name: IBM MQ state: ready diff --git a/providers/ibm/mq/pyproject.toml b/providers/ibm/mq/pyproject.toml index 4333b27c15088..557c1c1318040 100644 --- a/providers/ibm/mq/pyproject.toml +++ b/providers/ibm/mq/pyproject.toml @@ -24,9 +24,9 @@ requires = ["flit_core==3.12.0"] build-backend = "flit_core.buildapi" [project] -name = "apache-airflow-providers-ibm-mq" +name = "apache-airflow-providers-ibm" version = "0.1.0" -description = "Provider package apache-airflow-providers-ibm-mq for Apache Airflow" +description = "Provider package apache-airflow-providers-ibm for Apache Airflow" readme = "README.rst" license = "Apache-2.0" license-files = ['LICENSE', 'NOTICE'] @@ -98,8 +98,8 @@ apache-airflow-providers-common-messaging = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-ibm-mq/0.1.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-ibm-mq/0.1.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-ibm/0.1.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-ibm/0.1.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/ibm/mq/src/airflow/providers/ibm/mq/__init__.py b/providers/ibm/mq/src/airflow/providers/ibm/mq/__init__.py index f809790763f0c..caa82eef88aed 100644 --- a/providers/ibm/mq/src/airflow/providers/ibm/mq/__init__.py +++ b/providers/ibm/mq/src/airflow/providers/ibm/mq/__init__.py @@ -35,5 +35,5 @@ "2.11.0" ): raise RuntimeError( - f"The package `apache-airflow-providers-ibm-mq:{__version__}` needs Apache Airflow 2.11.0+" + f"The package `apache-airflow-providers-ibm:{__version__}` needs Apache Airflow 2.11.0+" ) diff --git a/providers/ibm/mq/src/airflow/providers/ibm/mq/get_provider_info.py b/providers/ibm/mq/src/airflow/providers/ibm/mq/get_provider_info.py index 6b96bc91d2abb..2339dbfc31d62 100644 --- a/providers/ibm/mq/src/airflow/providers/ibm/mq/get_provider_info.py +++ b/providers/ibm/mq/src/airflow/providers/ibm/mq/get_provider_info.py @@ -23,7 +23,7 @@ def get_provider_info(): return { - "package-name": "apache-airflow-providers-ibm-mq", + "package-name": "apache-airflow-providers-ibm", "name": "IBM MQ", "description": "`IBM MQ `__\n", "integrations": [ diff --git a/pyproject.toml b/pyproject.toml index 02ca60fa55c4a..7f95851efbcb3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -240,8 +240,8 @@ packages = [] "http" = [ "apache-airflow-providers-http>=4.13.2" ] -"ibm.mq" = [ - "apache-airflow-providers-ibm-mq>=0.1.0" +"ibm" = [ + "apache-airflow-providers-ibm>=0.1.0" ] "imap" = [ "apache-airflow-providers-imap>=3.8.0" @@ -442,7 +442,7 @@ packages = [] "apache-airflow-providers-grpc>=3.7.0", "apache-airflow-providers-hashicorp>=4.0.0", "apache-airflow-providers-http>=4.13.2", - "apache-airflow-providers-ibm-mq>=0.1.0", + "apache-airflow-providers-ibm>=0.1.0", "apache-airflow-providers-imap>=3.8.0", "apache-airflow-providers-influxdb>=2.8.0", "apache-airflow-providers-informatica>=0.1.0", # Set from MIN_VERSION_OVERRIDE in update_airflow_pyproject_toml.py From 071a583c18c9c820fdb6b17594ea38fbc74854ba Mon Sep 17 00:00:00 2001 From: David Blain Date: Thu, 5 Mar 2026 10:46:13 +0100 Subject: [PATCH 03/14] refactor: Added myself as code owner for the IBM MQ part --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c976ec55a57d6..0e8a1ec00d195 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -94,6 +94,7 @@ airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/ @Lee-W @jason810496 @guan /providers/fab/ @vincbeck /providers/google/ @shahar1 /providers/hashicorp/ @hussein-awala +/providers/ibm/mq/ @dabla /providers/informatica/ @RNHTTR # + @cetingokhan @sertaykabuk @umutozel /providers/keycloak/ @vincbeck @bugraoz93 /providers/microsoft/azure/ @dabla From 05c979b4596760794aafa95be569744cb0287c11 Mon Sep 17 00:00:00 2001 From: David Blain Date: Fri, 6 Mar 2026 23:41:44 +0100 Subject: [PATCH 04/14] Revert "refactor: Renamed apache-airflow-providers-ibm-mq provider to apache-airflow-providers-ibm" This reverts commit 3b7ade1210d893c61934606b841487d92c8b2da4. --- .../3-airflow_providers_bug_report.yml | 2 +- providers/ibm/mq/README.rst | 6 +++--- providers/ibm/mq/docs/changelog.rst | 2 +- providers/ibm/mq/docs/commits.rst | 2 +- providers/ibm/mq/docs/conf.py | 2 +- providers/ibm/mq/docs/index.rst | 12 ++++++------ providers/ibm/mq/provider.yaml | 2 +- providers/ibm/mq/pyproject.toml | 8 ++++---- .../ibm/mq/src/airflow/providers/ibm/mq/__init__.py | 2 +- .../airflow/providers/ibm/mq/get_provider_info.py | 2 +- pyproject.toml | 6 +++--- 11 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/3-airflow_providers_bug_report.yml b/.github/ISSUE_TEMPLATE/3-airflow_providers_bug_report.yml index 2352515e01f0a..8249e5bcb84c5 100644 --- a/.github/ISSUE_TEMPLATE/3-airflow_providers_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/3-airflow_providers_bug_report.yml @@ -77,7 +77,7 @@ body: - grpc - hashicorp - http - - ibm + - ibm-mq - imap - influxdb - informatica diff --git a/providers/ibm/mq/README.rst b/providers/ibm/mq/README.rst index 602b55bc6c254..71d2cd6e3df0f 100644 --- a/providers/ibm/mq/README.rst +++ b/providers/ibm/mq/README.rst @@ -21,7 +21,7 @@ .. IF YOU WANT TO MODIFY TEMPLATE FOR THIS FILE, YOU SHOULD MODIFY THE TEMPLATE ``PROVIDER_README_TEMPLATE.rst.jinja2`` IN the ``dev/breeze/src/airflow_breeze/templates`` DIRECTORY -Package ``apache-airflow-providers-ibm`` +Package ``apache-airflow-providers-ibm-mq`` Release: ``0.1.0`` @@ -36,14 +36,14 @@ This is a provider package for ``ibm.mq`` provider. All classes for this provide are in ``airflow.providers.ibm.mq`` python package. You can find package information and changelog for the provider -in the `documentation `_. +in the `documentation `_. Installation ------------ You can install this package on top of an existing Airflow 2 installation (see ``Requirements`` below for the minimum Airflow version supported) via -``pip install apache-airflow-providers-ibm`` +``pip install apache-airflow-providers-ibm-mq`` The package supports the following python versions: 3.9,3.10,3.11,3.12 diff --git a/providers/ibm/mq/docs/changelog.rst b/providers/ibm/mq/docs/changelog.rst index 9a385656940f3..099b5b4ddded4 100644 --- a/providers/ibm/mq/docs/changelog.rst +++ b/providers/ibm/mq/docs/changelog.rst @@ -21,7 +21,7 @@ and you want to add an explanation to the users on how they are supposed to deal with them. The changelog is updated and maintained semi-automatically by release manager. -``apache-airflow-providers-ibm`` +``apache-airflow-providers-ibm-mq`` Changelog --------- diff --git a/providers/ibm/mq/docs/commits.rst b/providers/ibm/mq/docs/commits.rst index 012f250fc9a85..1b08b59cb402c 100644 --- a/providers/ibm/mq/docs/commits.rst +++ b/providers/ibm/mq/docs/commits.rst @@ -23,7 +23,7 @@ .. THE REMAINDER OF THE FILE IS AUTOMATICALLY GENERATED. IT WILL BE OVERWRITTEN! -Package apache-airflow-providers-ibm +Package apache-airflow-providers-ibm-mq ------------------------------------------------------ `IBM MQ `__ diff --git a/providers/ibm/mq/docs/conf.py b/providers/ibm/mq/docs/conf.py index 681b823b91eef..425d9e512683d 100644 --- a/providers/ibm/mq/docs/conf.py +++ b/providers/ibm/mq/docs/conf.py @@ -22,6 +22,6 @@ import os -os.environ["AIRFLOW_PACKAGE_NAME"] = "apache-airflow-providers-ibm" +os.environ["AIRFLOW_PACKAGE_NAME"] = "apache-airflow-providers-ibm-mq" from docs.provider_conf import * # noqa: F403 diff --git a/providers/ibm/mq/docs/index.rst b/providers/ibm/mq/docs/index.rst index 195639ee5615c..7c6d0b8535d03 100644 --- a/providers/ibm/mq/docs/index.rst +++ b/providers/ibm/mq/docs/index.rst @@ -15,7 +15,7 @@ specific language governing permissions and limitations under the License. -``apache-airflow-providers-ibm`` +``apache-airflow-providers-ibm-mq`` =================================== @@ -47,7 +47,7 @@ :maxdepth: 1 :caption: Resources - PyPI Repository + PyPI Repository Example Dags Installing from sources @@ -69,7 +69,7 @@ Detailed list of commits -apache-airflow-providers-ibm package +apache-airflow-providers-ibm-mq package --------------------------------------- `IBM MQ `__ @@ -89,7 +89,7 @@ Installation This provider requires the `IBM MQ Redistributable Client `_ to be installed. You can install this package on top of an existing Airflow installation via -``pip install apache-airflow-providers-ibm``. +``pip install apache-airflow-providers-ibm-mq``. For the minimum Airflow version supported, see ``Requirements`` below. @@ -121,5 +121,5 @@ Downloading official packages You can download officially released packages and verify their checksums and signatures from the `Official Apache Download site `_ -* `The apache-airflow-providers-ibm 0.1.0 sdist package `_ (`asc `__, `sha512 `__) -* `The apache-airflow-providers-ibm 0.1.0 wheel package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-ibm-mq 0.1.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-ibm-mq 0.1.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/ibm/mq/provider.yaml b/providers/ibm/mq/provider.yaml index 0f0fb3496779b..b2e2d1b9cdc32 100644 --- a/providers/ibm/mq/provider.yaml +++ b/providers/ibm/mq/provider.yaml @@ -16,7 +16,7 @@ # under the License. --- -package-name: apache-airflow-providers-ibm +package-name: apache-airflow-providers-ibm-mq name: IBM MQ state: ready diff --git a/providers/ibm/mq/pyproject.toml b/providers/ibm/mq/pyproject.toml index 557c1c1318040..4333b27c15088 100644 --- a/providers/ibm/mq/pyproject.toml +++ b/providers/ibm/mq/pyproject.toml @@ -24,9 +24,9 @@ requires = ["flit_core==3.12.0"] build-backend = "flit_core.buildapi" [project] -name = "apache-airflow-providers-ibm" +name = "apache-airflow-providers-ibm-mq" version = "0.1.0" -description = "Provider package apache-airflow-providers-ibm for Apache Airflow" +description = "Provider package apache-airflow-providers-ibm-mq for Apache Airflow" readme = "README.rst" license = "Apache-2.0" license-files = ['LICENSE', 'NOTICE'] @@ -98,8 +98,8 @@ apache-airflow-providers-common-messaging = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] -"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-ibm/0.1.0" -"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-ibm/0.1.0/changelog.html" +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-ibm-mq/0.1.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-ibm-mq/0.1.0/changelog.html" "Bug Tracker" = "https://github.com/apache/airflow/issues" "Source Code" = "https://github.com/apache/airflow" "Slack Chat" = "https://s.apache.org/airflow-slack" diff --git a/providers/ibm/mq/src/airflow/providers/ibm/mq/__init__.py b/providers/ibm/mq/src/airflow/providers/ibm/mq/__init__.py index caa82eef88aed..f809790763f0c 100644 --- a/providers/ibm/mq/src/airflow/providers/ibm/mq/__init__.py +++ b/providers/ibm/mq/src/airflow/providers/ibm/mq/__init__.py @@ -35,5 +35,5 @@ "2.11.0" ): raise RuntimeError( - f"The package `apache-airflow-providers-ibm:{__version__}` needs Apache Airflow 2.11.0+" + f"The package `apache-airflow-providers-ibm-mq:{__version__}` needs Apache Airflow 2.11.0+" ) diff --git a/providers/ibm/mq/src/airflow/providers/ibm/mq/get_provider_info.py b/providers/ibm/mq/src/airflow/providers/ibm/mq/get_provider_info.py index 2339dbfc31d62..6b96bc91d2abb 100644 --- a/providers/ibm/mq/src/airflow/providers/ibm/mq/get_provider_info.py +++ b/providers/ibm/mq/src/airflow/providers/ibm/mq/get_provider_info.py @@ -23,7 +23,7 @@ def get_provider_info(): return { - "package-name": "apache-airflow-providers-ibm", + "package-name": "apache-airflow-providers-ibm-mq", "name": "IBM MQ", "description": "`IBM MQ `__\n", "integrations": [ diff --git a/pyproject.toml b/pyproject.toml index 2396b36949bb7..7893db40de488 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -240,8 +240,8 @@ packages = [] "http" = [ "apache-airflow-providers-http>=4.13.2" ] -"ibm" = [ - "apache-airflow-providers-ibm>=0.1.0" +"ibm.mq" = [ + "apache-airflow-providers-ibm-mq>=0.1.0" ] "imap" = [ "apache-airflow-providers-imap>=3.8.0" @@ -442,7 +442,7 @@ packages = [] "apache-airflow-providers-grpc>=3.7.0", "apache-airflow-providers-hashicorp>=4.0.0", "apache-airflow-providers-http>=4.13.2", - "apache-airflow-providers-ibm>=0.1.0", + "apache-airflow-providers-ibm-mq>=0.1.0", "apache-airflow-providers-imap>=3.8.0", "apache-airflow-providers-influxdb>=2.8.0", "apache-airflow-providers-informatica>=0.1.1", From 04698a0bb54920b582e430854daea798129ac75c Mon Sep 17 00:00:00 2001 From: David Blain Date: Sat, 7 Mar 2026 00:08:55 +0100 Subject: [PATCH 05/14] refactor: Added ibm-mq provider in pyproject.toml --- pyproject.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 440979bf57d35..ff75f966c5667 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -442,7 +442,7 @@ packages = [] "apache-airflow-providers-grpc>=3.7.0", "apache-airflow-providers-hashicorp>=4.0.0", "apache-airflow-providers-http>=4.13.2", - "apache-airflow-providers-ibm-mq>=0.1.0", + # "apache-airflow-providers-ibm-mq>=0.1.0", # TODO: reactivate "apache-airflow-providers-imap>=3.8.0", "apache-airflow-providers-influxdb>=2.8.0", "apache-airflow-providers-informatica>=0.1.1", @@ -1145,6 +1145,8 @@ mypy_path = [ "$MYPY_CONFIG_FILE_DIR/providers/hashicorp/tests", "$MYPY_CONFIG_FILE_DIR/providers/http/src", "$MYPY_CONFIG_FILE_DIR/providers/http/tests", + "$MYPY_CONFIG_FILE_DIR/providers/ibm/mq/src", + "$MYPY_CONFIG_FILE_DIR/providers/ibm/mq/tests", "$MYPY_CONFIG_FILE_DIR/providers/imap/src", "$MYPY_CONFIG_FILE_DIR/providers/imap/tests", "$MYPY_CONFIG_FILE_DIR/providers/influxdb/src", @@ -1419,6 +1421,7 @@ apache-airflow-providers-google = { workspace = true } apache-airflow-providers-grpc = { workspace = true } apache-airflow-providers-hashicorp = { workspace = true } apache-airflow-providers-http = { workspace = true } +apache-airflow-providers-ibm-mq = { workspace = true } apache-airflow-providers-imap = { workspace = true } apache-airflow-providers-influxdb = { workspace = true } apache-airflow-providers-informatica = { workspace = true } @@ -1548,6 +1551,7 @@ members = [ "providers/grpc", "providers/hashicorp", "providers/http", + "providers/ibm/mq", "providers/imap", "providers/influxdb", "providers/informatica", From c8af5d87b716309cfded13c1f3400721a020d00a Mon Sep 17 00:00:00 2001 From: David Blain Date: Tue, 10 Mar 2026 16:30:00 +0100 Subject: [PATCH 06/14] refactor: Refactored MQ hooks and trigger and added docstrings to consume and produce methods --- .../src/airflow/providers/ibm/mq/hooks/mq.py | 104 +++++++++++------- .../airflow/providers/ibm/mq/triggers/mq.py | 2 - 2 files changed, 62 insertions(+), 44 deletions(-) diff --git a/providers/ibm/mq/src/airflow/providers/ibm/mq/hooks/mq.py b/providers/ibm/mq/src/airflow/providers/ibm/mq/hooks/mq.py index 06855c6ade205..27367d97d643a 100644 --- a/providers/ibm/mq/src/airflow/providers/ibm/mq/hooks/mq.py +++ b/providers/ibm/mq/src/airflow/providers/ibm/mq/hooks/mq.py @@ -44,7 +44,7 @@ def __init__(self, conn_id: str = default_conn_name): @classmethod def get_ui_field_behaviour(cls) -> dict[str, Any]: - """Return custom UI field behaviour for IBM Connection.""" + """Return custom UI field behaviour for IBM MQ Connection.""" return { "hidden_fields": ["schema"], "placeholders": { @@ -103,10 +103,17 @@ def _process_message(cls, message): except Exception: return message - async def consume(self, queue_name: str, poll_interval: float = 5) -> str: + async def consume(self, queue_name: str, poll_interval: float = 5) -> str | None: """ - Wait for a single message and return its decoded payload. - Retries automatically on connection loss. + Wait for a single message from the specified IBM MQ queue and return its decoded payload. + + If the MQ connection is lost or another recoverable error occurs, the method logs the + issue and exits so that the next trigger instance can attempt to reconnect. + + :param queue_name: Name of the IBM MQ queue to consume messages from. + :param poll_interval: Interval in seconds used to wait for messages and to control + how long the underlying MQ get operation blocks before checking again. + :return: The decoded message payload if a message is received, otherwise ``None``. """ import ibmmq @@ -126,47 +133,60 @@ async def consume(self, queue_name: str, poll_interval: float = 5) -> str: ) gmo.WaitInterval = int(poll_interval * 1000) - while True: - try: - async with self.get_conn() as qmgr: - q = ibmmq.Queue( - qmgr, - od, - ibmmq.CMQC.MQOO_INPUT_AS_Q_DEF, - ) - - async_get = sync_to_async(q.get) - - try: - while True: - try: - message = await async_get(None, md, gmo) - - if message: - return self._process_message(message) - - except ibmmq.MQMIError as e: - if e.reason == ibmmq.CMQC.MQRC_CONNECTION_BROKEN: - self.log.warning( - "MQ connection broken, retrying..." - ) - break - elif e.reason == ibmmq.CMQC.MQRC_NO_MSG_AVAILABLE: - await asyncio.sleep(poll_interval) - continue - else: - raise - finally: - with suppress(Exception): - q.close() - - except Exception: - self.log.exception("MQ consume failed, retrying...") - await asyncio.sleep(poll_interval) + try: + async with self.get_conn() as qmgr: + q = ibmmq.Queue( + qmgr, + od, + ibmmq.CMQC.MQOO_INPUT_AS_Q_DEF, + ) + + async_get = sync_to_async(q.get) + + try: + while True: + try: + message = await asyncio.wait_for( + async_get(None, md, gmo), + timeout=poll_interval + 1, + ) + + if message: + return self._process_message(message) + + except ibmmq.MQMIError as e: + if e.reason == ibmmq.CMQC.MQRC_NO_MSG_AVAILABLE: + await asyncio.sleep(poll_interval) + continue + elif e.reason == ibmmq.CMQC.MQRC_CONNECTION_BROKEN: + self.log.warning( + "MQ connection broken, will exit consume; next trigger instance will reconnect" + ) + return None + else: + raise + + finally: + with suppress(Exception): + q.close() + + except asyncio.CancelledError: + raise + except Exception: + self.log.exception("MQ consume failed, exiting; next trigger instance will retry") + return None async def produce(self, queue_name: str, payload: str) -> None: """ - Put a message on the queue. + Put a message onto the specified IBM MQ queue. + + This method connects to the configured MQ queue manager and sends the + provided payload as a UTF-8 encoded message to the given queue. + + :param queue_name: Name of the IBM MQ queue to which the message should be sent. + :param payload: Message payload to send. The payload will be encoded as UTF-8 + before being placed on the queue. + :return: None """ import ibmmq diff --git a/providers/ibm/mq/src/airflow/providers/ibm/mq/triggers/mq.py b/providers/ibm/mq/src/airflow/providers/ibm/mq/triggers/mq.py index a3cfd40afaac6..bc6ecd37efe31 100644 --- a/providers/ibm/mq/src/airflow/providers/ibm/mq/triggers/mq.py +++ b/providers/ibm/mq/src/airflow/providers/ibm/mq/triggers/mq.py @@ -59,7 +59,5 @@ async def run(self): poll_interval=self.poll_interval, ) yield TriggerEvent(event) - except asyncio.CancelledError: - self.log.info("MQ trigger cancelled") return From 08cf2516e3c25a2e32f8a32c2631e98a4c488dbc Mon Sep 17 00:00:00 2001 From: David Blain Date: Tue, 10 Mar 2026 16:49:30 +0100 Subject: [PATCH 07/14] refactor: Added unit tests --- .../ibm/mq/tests/unit/ibm/mq/__init__.py | 29 ++++ .../ibm/mq/tests/unit/ibm/mq/hooks/test_mq.py | 138 ++++++++++++++++++ .../mq/tests/unit/ibm/mq/queues}/__init__.py | 0 .../mq/tests/unit/ibm/mq/queues/test_mq.py | 121 +++++++++++++++ .../mq/tests/unit/ibm/mq/triggers/__init__.py | 16 ++ .../mq/tests/unit/ibm/mq/triggers/test_mq.py | 89 +++++++++++ 6 files changed, 393 insertions(+) create mode 100644 providers/ibm/mq/tests/unit/ibm/mq/hooks/test_mq.py rename {airflow-core/src/airflow/_shared => providers/ibm/mq/tests/unit/ibm/mq/queues}/__init__.py (100%) create mode 100644 providers/ibm/mq/tests/unit/ibm/mq/queues/test_mq.py create mode 100644 providers/ibm/mq/tests/unit/ibm/mq/triggers/__init__.py create mode 100644 providers/ibm/mq/tests/unit/ibm/mq/triggers/test_mq.py diff --git a/providers/ibm/mq/tests/unit/ibm/mq/__init__.py b/providers/ibm/mq/tests/unit/ibm/mq/__init__.py index 13a83393a9124..1bb0a6198ee59 100644 --- a/providers/ibm/mq/tests/unit/ibm/mq/__init__.py +++ b/providers/ibm/mq/tests/unit/ibm/mq/__init__.py @@ -14,3 +14,32 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. + +try: + import ibmmq # real one might exist +except ModuleNotFoundError: + import sys + from unittest.mock import MagicMock + + + class MQMIError(Exception): + def __init__(self, msg="", reason=None): + super().__init__(msg) + self.reason = reason + + fake_ibmmq = MagicMock() + fake_ibmmq.CMQC.MQRC_NO_MSG_AVAILABLE = 2033 + fake_ibmmq.CMQC.MQRC_CONNECTION_BROKEN = 2009 + fake_ibmmq.CMQC.MQGMO_WAIT = 1 + fake_ibmmq.CMQC.MQGMO_NO_SYNCPOINT = 2 + fake_ibmmq.CMQC.MQGMO_CONVERT = 4 + fake_ibmmq.MQMIError = MQMIError + fake_ibmmq.OD = MagicMock() + fake_ibmmq.MD = MagicMock() + fake_ibmmq.GMO = MagicMock() + fake_ibmmq.CSP = MagicMock() + fake_ibmmq.Queue = MagicMock() + fake_ibmmq.connect = MagicMock() + fake_ibmmq.RFH2 = MagicMock() + + sys.modules["ibmmq"] = fake_ibmmq diff --git a/providers/ibm/mq/tests/unit/ibm/mq/hooks/test_mq.py b/providers/ibm/mq/tests/unit/ibm/mq/hooks/test_mq.py new file mode 100644 index 0000000000000..684ea87e3eba9 --- /dev/null +++ b/providers/ibm/mq/tests/unit/ibm/mq/hooks/test_mq.py @@ -0,0 +1,138 @@ +# 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 __future__ import annotations + +from unittest import mock +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from airflow.models import Connection +from airflow.providers.ibm.mq.hooks.mq import IBMMQHook + +MQ_PAYLOAD = """RFH x"MQSTR jms_map topic://localhost/topic17721219474762414D5143514D4941303054202020202069774D7092F81057Llocal26.01.00 4topic {}""" + +async def fake_get(*args, **kwargs): + import ibmmq + + raise ibmmq.MQMIError("connection broken", reason=ibmmq.CMQC.MQRC_CONNECTION_BROKEN) + + +@pytest.mark.asyncio +class TestIBMMQHook: + """Tests for the IBM MQ hook.""" + + @pytest.fixture(autouse=True) + def setup_connections(self, create_connection_without_db): + # Add a valid MQ connection + create_connection_without_db( + Connection( + conn_id="mq_conn", + conn_type="mq", + host="mq.example.com", + login="user", + password="pass", + port=1414, + extra='{"queue_manager": "QM1", "channel": "DEV.APP.SVRCONN"}', + ) + ) + self.hook = IBMMQHook("mq_conn") + + @patch("airflow.providers.ibm.mq.hooks.mq.get_async_connection", new_callable=AsyncMock) + @patch("ibmmq.connect") + @patch("ibmmq.Queue") + @patch("airflow.providers.ibm.mq.hooks.mq.sync_to_async") + async def test_consume_message( + self, mock_sync_to_async, mock_queue_class, mock_connect, mock_get_async_conn + ): + """Test consuming a single message.""" + + # Mock connection and queue + mock_qmgr = MagicMock() + mock_get_async_conn.return_value = MagicMock() # Connection object for _connect + mock_connect.return_value = mock_qmgr + + # Mock queue instance + mock_queue = MagicMock() + mock_queue_class.return_value = mock_queue + + # Simulate async get returning a message once, then None + async def fake_get(*args, **kwargs): + return MQ_PAYLOAD.format("test message").encode() + + mock_sync_to_async.return_value = fake_get + + result = await self.hook.consume(queue_name="QUEUE1", poll_interval=0.1) + assert isinstance(result, str) + assert "test message" in result + + mock_connect.assert_called_once() # connection established + mock_queue_class.assert_called_once_with( + mock_qmgr, + mock.ANY, + mock.ANY, + ) + + @patch("airflow.providers.ibm.mq.hooks.mq.get_async_connection", new_callable=AsyncMock) + @patch("ibmmq.connect") + @patch("ibmmq.Queue") + @patch("airflow.providers.ibm.mq.hooks.mq.sync_to_async") + async def test_produce_message( + self, mock_sync_to_async, mock_queue_class, mock_connect, mock_get_async_conn + ): + """Test producing a message to the queue.""" + + mock_qmgr = MagicMock() + mock_get_async_conn.return_value = MagicMock() + mock_connect.return_value = mock_qmgr + + mock_queue = MagicMock() + mock_queue_class.return_value = mock_queue + + async def fake_put(msg, md): + assert isinstance(msg, bytes) + assert b"payload" in msg + + mock_sync_to_async.return_value = fake_put + + await self.hook.produce(queue_name="QUEUE1", payload="payload") + + mock_connect.assert_called_once() + mock_queue_class.assert_called_once_with( + mock_qmgr, + mock.ANY, + mock.ANY, + ) + mock_sync_to_async.assert_called_once() # ensure async put is wrapped + + @patch("airflow.providers.ibm.mq.hooks.mq.get_async_connection", new_callable=AsyncMock) + @patch("ibmmq.connect") + @patch("ibmmq.Queue") + @patch("airflow.providers.ibm.mq.hooks.mq.sync_to_async") + async def test_consume_connection_broken(self, mock_sync_to_async, mock_queue_class, mock_connect, mock_get_async_conn, caplog): + """Test that consume exits gracefully on connection broken.""" + + mock_get_async_conn.return_value = MagicMock() + mock_qmgr = MagicMock() + mock_connect.return_value = mock_qmgr + mock_queue = MagicMock() + mock_queue_class.return_value = mock_queue + mock_sync_to_async.return_value = fake_get + + result = await self.hook.consume(queue_name="QUEUE1", poll_interval=0.1) + assert result is None + assert "MQ connection broken, will exit consume; next trigger instance will reconnect" in caplog.text diff --git a/airflow-core/src/airflow/_shared/__init__.py b/providers/ibm/mq/tests/unit/ibm/mq/queues/__init__.py similarity index 100% rename from airflow-core/src/airflow/_shared/__init__.py rename to providers/ibm/mq/tests/unit/ibm/mq/queues/__init__.py diff --git a/providers/ibm/mq/tests/unit/ibm/mq/queues/test_mq.py b/providers/ibm/mq/tests/unit/ibm/mq/queues/test_mq.py new file mode 100644 index 0000000000000..4f43a10d99b37 --- /dev/null +++ b/providers/ibm/mq/tests/unit/ibm/mq/queues/test_mq.py @@ -0,0 +1,121 @@ +# 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 __future__ import annotations + +import pytest + +from airflow.providers.ibm.mq.triggers.mq import AwaitMessageTrigger + +pytest.importorskip("airflow.providers.common.messaging.providers.base_provider") + + +class TestIBMMQMessageQueueProvider: + """Tests for IBMMQMessageQueueProvider.""" + + def setup_method(self): + """Set up the test environment.""" + from airflow.providers.ibm.mq.queues.mq import IBMMQMessageQueueProvider + + self.provider = IBMMQMessageQueueProvider() + + def test_queue_create(self): + """Test the creation of the provider.""" + from airflow.providers.common.messaging.providers.base_provider import BaseMessageQueueProvider + + assert isinstance(self.provider, BaseMessageQueueProvider) + + @pytest.mark.parametrize( + ("queue_uri", "expected_result"), + [ + pytest.param("mq://mq_conn/QUEUE1", True, id="valid_mq_uri"), + pytest.param("http://example.com", False, id="http_url"), + pytest.param("not-a-url", False, id="invalid_url"), + ], + ) + def test_queue_matches(self, queue_uri, expected_result): + """Test the queue_matches method with various URLs.""" + assert self.provider.queue_matches(queue_uri) == expected_result + + @pytest.mark.parametrize( + ("scheme", "expected_result"), + [ + pytest.param("kafka", False, id="kafka_scheme"), + pytest.param("mq", True, id="mq_scheme"), + pytest.param("redis+pubsub", False, id="redis_scheme"), + pytest.param("sqs", False, id="sqs_scheme"), + pytest.param("unknown", False, id="unknown_scheme"), + ], + ) + def test_scheme_matches(self, scheme, expected_result): + """Test the scheme_matches method with various schemes.""" + assert self.provider.scheme_matches(scheme) == expected_result + + def test_trigger_class(self): + """Test the trigger_class method.""" + assert self.provider.trigger_class() == AwaitMessageTrigger + + @pytest.mark.parametrize( + ("queue_uri", "extra_kwargs", "expected_result"), + [ + pytest.param( + "mq://my_conn/QUEUE1", + {}, + { + "mq_conn_id": "my_conn", + "queue_name": "QUEUE1", + "poll_interval": 5, + }, + id="default_poll_interval", + ), + pytest.param( + "mq://my_conn/QUEUE1", + {"poll_interval": 60}, + { + "mq_conn_id": "my_conn", + "queue_name": "QUEUE1", + "poll_interval": 60, + }, + id="override_poll_interval", + ), + ], + ) + def test_trigger_kwargs_valid_cases(self, queue_uri, extra_kwargs, expected_result): + """Test the trigger_kwargs method with valid parameters.""" + kwargs = self.provider.trigger_kwargs(queue_uri, **extra_kwargs) + assert kwargs == expected_result + + @pytest.mark.parametrize( + ("queue_uri", "expected_error", "error_match"), + [ + pytest.param( + "mq:///QUEUE1", + ValueError, + "MQ URI must contain connection id", + id="missing_conn_id", + ), + pytest.param( + "mq://my_conn/", + ValueError, + "MQ URI must contain queue name", + id="missing_queue_name", + ), + ], + ) + def test_trigger_kwargs_error_cases(self, queue_uri, expected_error, error_match): + """Test that trigger_kwargs raises appropriate errors with invalid parameters.""" + with pytest.raises(expected_error, match=error_match): + self.provider.trigger_kwargs(queue_uri) diff --git a/providers/ibm/mq/tests/unit/ibm/mq/triggers/__init__.py b/providers/ibm/mq/tests/unit/ibm/mq/triggers/__init__.py new file mode 100644 index 0000000000000..13a83393a9124 --- /dev/null +++ b/providers/ibm/mq/tests/unit/ibm/mq/triggers/__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/providers/ibm/mq/tests/unit/ibm/mq/triggers/test_mq.py b/providers/ibm/mq/tests/unit/ibm/mq/triggers/test_mq.py new file mode 100644 index 0000000000000..788a81e08a1f9 --- /dev/null +++ b/providers/ibm/mq/tests/unit/ibm/mq/triggers/test_mq.py @@ -0,0 +1,89 @@ +# 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 __future__ import annotations + +from unittest.mock import patch, AsyncMock + +import pytest + +from airflow.providers.ibm.mq.hooks.mq import IBMMQHook +from airflow.providers.ibm.mq.triggers.mq import AwaitMessageTrigger +from airflow.triggers.base import TriggerEvent + + +async def fake_get(*args, **kwargs): + import ibmmq + + raise ibmmq.MQMIError("connection broken", reason=ibmmq.CMQC.MQRC_CONNECTION_BROKEN) + + +class TestMQTrigger: + @pytest.mark.asyncio + async def test_trigger_serialization(self): + trigger = AwaitMessageTrigger( + mq_conn_id="mq_default", + queue_name="QUEUE1", + poll_interval=2, + ) + assert isinstance(trigger, AwaitMessageTrigger) + + classpath, kwargs = trigger.serialize() + assert classpath == "airflow.providers.ibm.mq.triggers.mq.AwaitMessageTrigger" + assert kwargs == { + "mq_conn_id": "mq_default", + "queue_name": "QUEUE1", + "poll_interval": 2, + } + + @pytest.mark.asyncio + @patch.object(IBMMQHook, "consume", return_value="test message") + async def test_trigger_run_message_yielded(self, mock_consume): + trigger = AwaitMessageTrigger( + mq_conn_id="mq_default", + queue_name="QUEUE1", + poll_interval=0.1, + ) + + event = await anext(trigger.run()) + assert isinstance(event, TriggerEvent) + assert event.payload == "test message" + mock_consume.assert_called_once_with(queue_name="QUEUE1", poll_interval=0.1) + + @pytest.mark.asyncio + @patch("airflow.providers.ibm.mq.hooks.mq.sync_to_async", return_value=fake_get) + @patch("airflow.providers.ibm.mq.hooks.mq.get_async_connection", new_callable=AsyncMock) + @patch("ibmmq.Queue") + async def test_trigger_run_none_on_connection_error( + self, mock_queue, mock_get_async_conn, mock_sync_to_async, caplog + ): + """Test that the trigger yields None when consume encounters a connection problem.""" + + trigger = AwaitMessageTrigger( + mq_conn_id="mq_default", + queue_name="QUEUE1", + poll_interval=0.1, + ) + + with caplog.at_level("WARNING"): + event = await anext(trigger.run()) + + # The trigger yields a TriggerEvent with payload None because consume handles the exception + assert isinstance(event, TriggerEvent) + assert event.payload is None + + # The consume warning log should be present + assert "MQ connection broken" in caplog.text From c9f1d2107327d8c2d68d234f3c71a17115aec323 Mon Sep 17 00:00:00 2001 From: David Blain Date: Tue, 10 Mar 2026 19:22:18 +0100 Subject: [PATCH 08/14] refactor: Made ibmmq dependency optional as it is only required at runtime --- providers/ibm/mq/pyproject.toml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/providers/ibm/mq/pyproject.toml b/providers/ibm/mq/pyproject.toml index 4333b27c15088..0592611fbacca 100644 --- a/providers/ibm/mq/pyproject.toml +++ b/providers/ibm/mq/pyproject.toml @@ -61,6 +61,13 @@ dependencies = [ "apache-airflow>=2.11.0", "apache-airflow-providers-common-messaging>=2.0.0", "importlib-resources>=1.3", +] + +# The optional dependencies should be modified in place in the generated file +# Any change in the dependencies is preserved when the file is regenerated +[project.optional-dependencies] +"ibmmq" = [ + # Required at Runtime "ibmmq>=2.0.4", ] From 94762e6436ab2f60f3ded21bbbc0edf2d1fe8856 Mon Sep 17 00:00:00 2001 From: David Blain Date: Tue, 10 Mar 2026 19:48:44 +0100 Subject: [PATCH 09/14] docs: Update installation instructions --- providers/ibm/mq/README.rst | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/providers/ibm/mq/README.rst b/providers/ibm/mq/README.rst index 71d2cd6e3df0f..188044fe7fc6f 100644 --- a/providers/ibm/mq/README.rst +++ b/providers/ibm/mq/README.rst @@ -41,10 +41,23 @@ in the `documentation `_ Python package requires the native `IBM MQ Redistributable Client `_ libraries to be installed on the system. + +Refer to the IBM MQ documentation for installation instructions for your platform. + The package supports the following python versions: 3.9,3.10,3.11,3.12 Requirements @@ -56,7 +69,6 @@ PIP package Version required ``apache-airflow`` ``>=2.11.0`` ``apache-airflow-providers-common-messaging`` ``>=2.0.0`` ``importlib-resources`` ``>=1.3`` -``ibmmq`` ``>=2.0.4`` ============================================= ===================================== @@ -67,5 +79,14 @@ Dependent package `apache-airflow-providers-common-messaging `_ ``common.messaging`` ======================================================================================================================== ==================== +Optional dependencies +---------------------- + +========== ================ +Extra Dependencies +========== ================ +``ibmmq`` ``ibmmq>=2.0.4`` +========== ================ + The changelog for the provider package can be found in the `changelog `_. From 277b28dc849fc8cdbd5974c88351c8f1d7e059c9 Mon Sep 17 00:00:00 2001 From: David Blain Date: Tue, 10 Mar 2026 19:54:47 +0100 Subject: [PATCH 10/14] docs: Added IBM MQ Connection screenshot --- providers/ibm/mq/docs/connections/mq.rst | 4 ++++ .../ibm/mq/docs/connections/mq_connection.png | Bin 0 -> 31251 bytes 2 files changed, 4 insertions(+) create mode 100644 providers/ibm/mq/docs/connections/mq_connection.png diff --git a/providers/ibm/mq/docs/connections/mq.rst b/providers/ibm/mq/docs/connections/mq.rst index 68d928013952c..fbd8f9cad0ad0 100644 --- a/providers/ibm/mq/docs/connections/mq.rst +++ b/providers/ibm/mq/docs/connections/mq.rst @@ -21,3 +21,7 @@ MQ connection ============= The MQ connection type enables connection to an IBM MQ. + +.. |Kafka Connection| image:: mq_connection.png + :width: 400 + :alt: IBM MQ Connection Screenshot diff --git a/providers/ibm/mq/docs/connections/mq_connection.png b/providers/ibm/mq/docs/connections/mq_connection.png new file mode 100644 index 0000000000000000000000000000000000000000..37a2a6a55443c60b7149066f458523f4d1150726 GIT binary patch literal 31251 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 literal 0 HcmV?d00001 From 55b67e5d26c9cf69727ca683c986a863f537ab3c Mon Sep 17 00:00:00 2001 From: David Blain Date: Tue, 10 Mar 2026 20:24:21 +0100 Subject: [PATCH 11/14] docs: Added more docstrings and documentation --- providers/ibm/mq/docs/message-queues.rst | 90 +++++++++++++++++++ .../src/airflow/providers/ibm/mq/queues/mq.py | 24 ++++- .../mq/example_dag_message_queue_trigger.py | 47 ++++++++++ 3 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 providers/ibm/mq/docs/message-queues.rst create mode 100644 providers/ibm/mq/tests/system/ibm/mq/example_dag_message_queue_trigger.py diff --git a/providers/ibm/mq/docs/message-queues.rst b/providers/ibm/mq/docs/message-queues.rst new file mode 100644 index 0000000000000..314413f5d9966 --- /dev/null +++ b/providers/ibm/mq/docs/message-queues.rst @@ -0,0 +1,90 @@ +.. 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. + +.. NOTE TO CONTRIBUTORS: + Please, only add notes to the Changelog just below the "Changelog" header when there are some breaking changes + and you want to add an explanation to the users on how they are supposed to deal with them. + The changelog is updated and maintained semi-automatically by release manager. + + +IBM MQ Message Queue +==================== + +.. contents:: + :local: + :depth: 2 + + +IBM MQ Queue Provider +--------------------- + +Implemented by :class:`~airflow.providers.ibm.mq.queues.mq.IBMMQMessageQueueProvider` + +The IBM MQ Queue Provider is a +:class:`~airflow.providers.common.messaging.providers.base_provider.BaseMessageQueueProvider` +that uses IBM MQ as the underlying message queue system. + +It allows you to send and receive messages using IBM MQ queues in your Airflow workflows +via the common message queue interface +:class:`~airflow.providers.common.messaging.triggers.msg_queue.MessageQueueTrigger`. + + +.. include:: /../src/airflow/providers/ibm/mq/queues/mq.py + :start-after: [START ibmmq_message_queue_provider_description] + :end-before: [END ibmmq_message_queue_provider_description] + + +.. _howto/triggers:IBMMQMessageQueueTrigger: + + +IBM MQ Message Queue Trigger +---------------------------- + +Implemented by :class:`~airflow.providers.ibm.mq.triggers.mq.AwaitMessageTrigger` + +Inherited from +:class:`~airflow.providers.common.messaging.triggers.msg_queue.MessageQueueTrigger` + +Wait for a message in a queue +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Below is an example of how you can configure an Airflow DAG to be triggered +by a message arriving in an IBM MQ queue. + +.. exampleinclude:: /../tests/system/ibm/mq/example_dag_message_queue_trigger.py + :language: python + :start-after: [START howto_trigger_message_queue] + :end-before: [END howto_trigger_message_queue] + + +How it works +------------ + +1. **IBM MQ Message Queue Trigger** + The ``AwaitMessageTrigger`` listens for messages from an IBM MQ queue. + +2. **Asset and Watcher** + The ``Asset`` abstracts the external entity, the IBM MQ queue in this example. + The ``AssetWatcher`` associates a trigger with a name. This name helps you + identify which trigger is associated with which asset. + +3. **Event-Driven DAG** + Instead of running on a fixed schedule, the DAG executes when the asset receives + an update (for example, when a new message arrives in the queue). + +For how to use the trigger, refer to the documentation of the +:ref:`Messaging Trigger `. diff --git a/providers/ibm/mq/src/airflow/providers/ibm/mq/queues/mq.py b/providers/ibm/mq/src/airflow/providers/ibm/mq/queues/mq.py index 6db39856e9064..0f08b03d501db 100644 --- a/providers/ibm/mq/src/airflow/providers/ibm/mq/queues/mq.py +++ b/providers/ibm/mq/src/airflow/providers/ibm/mq/queues/mq.py @@ -22,7 +22,6 @@ from urllib.parse import urlparse from airflow.providers.common.messaging.providers.base_provider import BaseMessageQueueProvider -from airflow.providers.ibm.mq.hooks.mq import IBMMQHook from airflow.providers.ibm.mq.triggers.mq import AwaitMessageTrigger from airflow.providers.ibm.mq.version_compat import AIRFLOW_V_3_0_PLUS @@ -38,6 +37,29 @@ class IBMMQMessageQueueProvider(BaseMessageQueueProvider): + """ + Configuration for IBM MQ integration with common-messaging. + + [START ibmmq_message_queue_provider_description] + + * It uses ``mq`` as scheme for identifying IBM MQ queues. + * For parameter definitions take a look at + :class:`~airflow.providers.ibm.mq.triggers.mq.AwaitMessageTrigger`. + + .. code-block:: python + + from airflow.providers.common.messaging.triggers.msg_queue import MessageQueueTrigger + from airflow.sdk import Asset, AssetWatcher + + trigger = MessageQueueTrigger( + queue="mq://mq_default/MY.QUEUE.NAME", + ) + + asset = Asset("mq_topic_asset", watchers=[AssetWatcher(name="mq_watcher", trigger=trigger)]) + + [END ibmmq_message_queue_provider_description] + """ + scheme = "mq" def queue_matches(self, queue: str) -> bool: diff --git a/providers/ibm/mq/tests/system/ibm/mq/example_dag_message_queue_trigger.py b/providers/ibm/mq/tests/system/ibm/mq/example_dag_message_queue_trigger.py new file mode 100644 index 0000000000000..def48c4d9b7cb --- /dev/null +++ b/providers/ibm/mq/tests/system/ibm/mq/example_dag_message_queue_trigger.py @@ -0,0 +1,47 @@ +# 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 __future__ import annotations + +# [START howto_trigger_message_queue] +from airflow.providers.common.messaging.triggers.msg_queue import MessageQueueTrigger +from airflow.sdk import DAG, Asset, AssetWatcher, task + +# Define a trigger that listens to an external message queue (IBM MQ in this case) +trigger = MessageQueueTrigger( + queue="mq://mq_default/MY.QUEUE.NAME", +) + +mq_topic_asset = Asset( + "mq_topic_asset", + watchers=[AssetWatcher(name="mq_watcher", trigger=trigger)], +) + +with DAG(dag_id="example_ibm_mq_watcher", schedule=[mq_topic_asset]) as dag: + @task + def process_message(**context): + for event in context["triggering_asset_events"][mq_topic_asset]: + # Get the message from the TriggerEvent payload + print("Processing event: ", event) + payload = event["payload"] + print("Actual payload: ", payload) +# [END howto_trigger_message_queue] + + +from tests_common.test_utils.system_tests import get_test_run # noqa: E402 + +# Needed to run the example DAG with pytest (see: tests/system/README.md#run_via_pytest) +test_run = get_test_run(dag) From ccb662ab5b29ac60ef48103fdd91edd937374e28 Mon Sep 17 00:00:00 2001 From: David Blain Date: Wed, 11 Mar 2026 08:08:41 +0100 Subject: [PATCH 12/14] refactor: Re-added apache-airflow-providers-ibm-mq dependencies in pyproject.toml --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index da0743b8b7dec..9daade9b1a582 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -241,7 +241,7 @@ packages = [] "apache-airflow-providers-http>=4.13.2" ] "ibm.mq" = [ - "apache-airflow-providers-ibm-mq>=0.1.0" + "apache-airflow-providers-ibm-mq>=0.1.0" # Set from local provider pyproject.toml ] "imap" = [ "apache-airflow-providers-imap>=3.8.0" @@ -442,7 +442,7 @@ packages = [] "apache-airflow-providers-grpc>=3.7.0", "apache-airflow-providers-hashicorp>=4.0.0", "apache-airflow-providers-http>=4.13.2", - # "apache-airflow-providers-ibm-mq>=0.1.0", # TODO: reactivate + "apache-airflow-providers-ibm-mq>=0.1.0", # Set from local provider pyproject.toml "apache-airflow-providers-imap>=3.8.0", "apache-airflow-providers-influxdb>=2.8.0", "apache-airflow-providers-informatica>=0.1.1", From 385d789d9955a997a0ad13f0b2bbbf17bce35d45 Mon Sep 17 00:00:00 2001 From: David Blain Date: Wed, 11 Mar 2026 08:21:57 +0100 Subject: [PATCH 13/14] refactor: Reformatted files and docs --- .../output_workflow-run_publish-docs.txt | 2 +- providers/ibm/mq/docs/index.rst | 24 ++++++++++++------- providers/ibm/mq/pyproject.toml | 7 +++++- .../providers/ibm/mq/get_provider_info.py | 23 ++++-------------- .../src/airflow/providers/ibm/mq/hooks/mq.py | 13 ++++------ .../src/airflow/providers/ibm/mq/queues/mq.py | 9 ++----- .../mq/example_dag_message_queue_trigger.py | 1 + .../ibm/mq/tests/unit/ibm/mq/__init__.py | 1 - .../ibm/mq/tests/unit/ibm/mq/hooks/test_mq.py | 5 +++- .../mq/tests/unit/ibm/mq/triggers/test_mq.py | 2 +- 10 files changed, 40 insertions(+), 47 deletions(-) diff --git a/dev/breeze/doc/images/output_workflow-run_publish-docs.txt b/dev/breeze/doc/images/output_workflow-run_publish-docs.txt index 4d64c05aefdbd..8640ca78ad92d 100644 --- a/dev/breeze/doc/images/output_workflow-run_publish-docs.txt +++ b/dev/breeze/doc/images/output_workflow-run_publish-docs.txt @@ -1 +1 @@ -46e32fc6adb71c667231a207e063e291 +7cc9afce1e4ae2311f163402d807ad45 diff --git a/providers/ibm/mq/docs/index.rst b/providers/ibm/mq/docs/index.rst index 7c6d0b8535d03..c20caabf78321 100644 --- a/providers/ibm/mq/docs/index.rst +++ b/providers/ibm/mq/docs/index.rst @@ -70,7 +70,7 @@ apache-airflow-providers-ibm-mq package ---------------------------------------- +------------------------------------------------------ `IBM MQ `__ @@ -86,26 +86,34 @@ All classes for this package are included in the ``airflow.providers.ibm.mq`` py Installation ------------ -This provider requires the `IBM MQ Redistributable Client `_ to be installed. - You can install this package on top of an existing Airflow installation via ``pip install apache-airflow-providers-ibm-mq``. For the minimum Airflow version supported, see ``Requirements`` below. - Requirements ------------ The minimum Apache Airflow version supported by this provider distribution is ``2.11.0``. -============================================= ===================================== +============================================= ================== PIP package Version required -============================================= ===================================== +============================================= ================== ``apache-airflow`` ``>=2.11.0`` ``apache-airflow-providers-common-messaging`` ``>=2.0.0`` ``importlib-resources`` ``>=1.3`` -``ibmmq`` ``>=2.0.4`` -============================================= ===================================== +============================================= ================== + +Cross provider package dependencies +----------------------------------- + +Those are dependencies that might be needed in order to use all the features of the package. +You need to install the specified provider distributions in order to use them. + +You can install such cross-provider dependencies when installing from PyPI. For example: + +.. code-block:: bash + + pip install apache-airflow-providers-ibm-mq[common.compat] ======================================================================================================================== ==================== diff --git a/providers/ibm/mq/pyproject.toml b/providers/ibm/mq/pyproject.toml index 0592611fbacca..c761e1fb15263 100644 --- a/providers/ibm/mq/pyproject.toml +++ b/providers/ibm/mq/pyproject.toml @@ -70,13 +70,18 @@ dependencies = [ # Required at Runtime "ibmmq>=2.0.4", ] +"common.compat" = [ + "apache-airflow-providers-common-compat" +] [dependency-groups] dev = [ "apache-airflow", "apache-airflow-task-sdk", "apache-airflow-devel-common", + "apache-airflow-providers-common-compat", "apache-airflow-providers-common-messaging", + # Additional devel dependencies (do not remove this line and add extra development dependencies) ] # To build docs: @@ -101,7 +106,7 @@ docs = [ apache-airflow = {workspace = true} apache-airflow-devel-common = {workspace = true} apache-airflow-task-sdk = {workspace = true} -apache-airflow-providers-common-messaging = {workspace = true} +apache-airflow-providers-common-sql = {workspace = true} apache-airflow-providers-standard = {workspace = true} [project.urls] diff --git a/providers/ibm/mq/src/airflow/providers/ibm/mq/get_provider_info.py b/providers/ibm/mq/src/airflow/providers/ibm/mq/get_provider_info.py index 6b96bc91d2abb..b5cf84bc43329 100644 --- a/providers/ibm/mq/src/airflow/providers/ibm/mq/get_provider_info.py +++ b/providers/ibm/mq/src/airflow/providers/ibm/mq/get_provider_info.py @@ -29,30 +29,17 @@ def get_provider_info(): "integrations": [ { "integration-name": "IBM MQ", - "external-doc-url": "https://www.ibm.com/products/mq/", + "external-doc-url": "https://www.ibm.com/products/mq", "logo": "/docs/integration-logos/ibm-mq.png", "tags": ["apache"], } ], - "hooks": [ - { - "integration-name": "IBM MQ", - "python-modules": ["airflow.providers.ibm.mq.hooks.mq"], - } + "hooks": [{"integration-name": "IBM MQ", "python-modules": ["airflow.providers.ibm.mq.hooks.mq"]}], + "connection-types": [ + {"hook-class-name": "airflow.providers.ibm.mq.hooks.mq.IBMMQHook", "connection-type": "mq"} ], "triggers": [ - { - "integration-name": "IBM MQ", - "python-modules": [ - "airflow.providers.ibm.mq.triggers.mq", - ], - } - ], - "connection-types": [ - { - "hook-class-name": "airflow.providers.ibm.mq.hooks.mq.IBMMQHook", - "connection-type": "mq", - } + {"integration-name": "IBM MQ", "python-modules": ["airflow.providers.ibm.mq.triggers.mq"]} ], "queues": ["airflow.providers.ibm.mq.queues.mq.IBMMQMessageQueueProvider"], } diff --git a/providers/ibm/mq/src/airflow/providers/ibm/mq/hooks/mq.py b/providers/ibm/mq/src/airflow/providers/ibm/mq/hooks/mq.py index 27367d97d643a..e709ca5861d0e 100644 --- a/providers/ibm/mq/src/airflow/providers/ibm/mq/hooks/mq.py +++ b/providers/ibm/mq/src/airflow/providers/ibm/mq/hooks/mq.py @@ -19,7 +19,7 @@ import asyncio import json -from contextlib import suppress, asynccontextmanager +from contextlib import asynccontextmanager, suppress from typing import TYPE_CHECKING, Any from asgiref.sync import sync_to_async @@ -126,11 +126,7 @@ async def consume(self, queue_name: str, poll_interval: float = 5) -> str | None md.Encoding = ibmmq.CMQC.MQENC_NATIVE gmo = ibmmq.GMO() - gmo.Options = ( - ibmmq.CMQC.MQGMO_WAIT - | ibmmq.CMQC.MQGMO_NO_SYNCPOINT - | ibmmq.CMQC.MQGMO_CONVERT - ) + gmo.Options = ibmmq.CMQC.MQGMO_WAIT | ibmmq.CMQC.MQGMO_NO_SYNCPOINT | ibmmq.CMQC.MQGMO_CONVERT gmo.WaitInterval = int(poll_interval * 1000) try: @@ -158,13 +154,12 @@ async def consume(self, queue_name: str, poll_interval: float = 5) -> str | None if e.reason == ibmmq.CMQC.MQRC_NO_MSG_AVAILABLE: await asyncio.sleep(poll_interval) continue - elif e.reason == ibmmq.CMQC.MQRC_CONNECTION_BROKEN: + if e.reason == ibmmq.CMQC.MQRC_CONNECTION_BROKEN: self.log.warning( "MQ connection broken, will exit consume; next trigger instance will reconnect" ) return None - else: - raise + raise finally: with suppress(Exception): diff --git a/providers/ibm/mq/src/airflow/providers/ibm/mq/queues/mq.py b/providers/ibm/mq/src/airflow/providers/ibm/mq/queues/mq.py index 0f08b03d501db..117f2ea52fd2d 100644 --- a/providers/ibm/mq/src/airflow/providers/ibm/mq/queues/mq.py +++ b/providers/ibm/mq/src/airflow/providers/ibm/mq/queues/mq.py @@ -73,23 +73,18 @@ def trigger_kwargs(self, queue: str, **kwargs) -> dict[str, Any]: Parse URI of format: mq:/// """ - parsed = urlparse(queue) if not parsed.netloc: raise ValueError( - "MQ URI must contain connection id. " - "Expected format: mq:///" + "MQ URI must contain connection id. Expected format: mq:///" ) conn_id = parsed.netloc queue_name = parsed.path.lstrip("/") if not queue_name: - raise ValueError( - "MQ URI must contain queue name. " - "Expected format: mq:///" - ) + raise ValueError("MQ URI must contain queue name. Expected format: mq:///") return { "mq_conn_id": conn_id, diff --git a/providers/ibm/mq/tests/system/ibm/mq/example_dag_message_queue_trigger.py b/providers/ibm/mq/tests/system/ibm/mq/example_dag_message_queue_trigger.py index def48c4d9b7cb..c24fcba2052ce 100644 --- a/providers/ibm/mq/tests/system/ibm/mq/example_dag_message_queue_trigger.py +++ b/providers/ibm/mq/tests/system/ibm/mq/example_dag_message_queue_trigger.py @@ -31,6 +31,7 @@ ) with DAG(dag_id="example_ibm_mq_watcher", schedule=[mq_topic_asset]) as dag: + @task def process_message(**context): for event in context["triggering_asset_events"][mq_topic_asset]: diff --git a/providers/ibm/mq/tests/unit/ibm/mq/__init__.py b/providers/ibm/mq/tests/unit/ibm/mq/__init__.py index 1bb0a6198ee59..289afa96fdc1c 100644 --- a/providers/ibm/mq/tests/unit/ibm/mq/__init__.py +++ b/providers/ibm/mq/tests/unit/ibm/mq/__init__.py @@ -21,7 +21,6 @@ import sys from unittest.mock import MagicMock - class MQMIError(Exception): def __init__(self, msg="", reason=None): super().__init__(msg) diff --git a/providers/ibm/mq/tests/unit/ibm/mq/hooks/test_mq.py b/providers/ibm/mq/tests/unit/ibm/mq/hooks/test_mq.py index 684ea87e3eba9..77e013d319980 100644 --- a/providers/ibm/mq/tests/unit/ibm/mq/hooks/test_mq.py +++ b/providers/ibm/mq/tests/unit/ibm/mq/hooks/test_mq.py @@ -26,6 +26,7 @@ MQ_PAYLOAD = """RFH x"MQSTR jms_map topic://localhost/topic17721219474762414D5143514D4941303054202020202069774D7092F81057Llocal26.01.00 4topic {}""" + async def fake_get(*args, **kwargs): import ibmmq @@ -123,7 +124,9 @@ async def fake_put(msg, md): @patch("ibmmq.connect") @patch("ibmmq.Queue") @patch("airflow.providers.ibm.mq.hooks.mq.sync_to_async") - async def test_consume_connection_broken(self, mock_sync_to_async, mock_queue_class, mock_connect, mock_get_async_conn, caplog): + async def test_consume_connection_broken( + self, mock_sync_to_async, mock_queue_class, mock_connect, mock_get_async_conn, caplog + ): """Test that consume exits gracefully on connection broken.""" mock_get_async_conn.return_value = MagicMock() diff --git a/providers/ibm/mq/tests/unit/ibm/mq/triggers/test_mq.py b/providers/ibm/mq/tests/unit/ibm/mq/triggers/test_mq.py index 788a81e08a1f9..f6ce0e02c30fd 100644 --- a/providers/ibm/mq/tests/unit/ibm/mq/triggers/test_mq.py +++ b/providers/ibm/mq/tests/unit/ibm/mq/triggers/test_mq.py @@ -16,7 +16,7 @@ # under the License. from __future__ import annotations -from unittest.mock import patch, AsyncMock +from unittest.mock import AsyncMock, patch import pytest From 95c63b0335c1549569c03377f410619aba981535 Mon Sep 17 00:00:00 2001 From: David Blain Date: Wed, 11 Mar 2026 09:35:42 +0100 Subject: [PATCH 14/14] refactor: Added ibm and mq keywords in spelling wordlist --- docs/spelling_wordlist.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index aa0bd36de11ad..cdda668048414 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -761,6 +761,8 @@ hyperparameter hyperparameters IaC iam +IBM +ibm ibmcloudant ideation idempotence @@ -1017,6 +1019,8 @@ mongo mongodb monospace moto +MQ +mq msfabric msg msgraph