diff --git a/.github/ISSUE_TEMPLATE/1-airflow_bug_report.yml b/.github/ISSUE_TEMPLATE/1-airflow_bug_report.yml index 50b2399564e0d..32bbf5313c4c4 100644 --- a/.github/ISSUE_TEMPLATE/1-airflow_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/1-airflow_bug_report.yml @@ -179,6 +179,7 @@ body: - mongo - mysql - neo4j + - oci - odbc - openai - openfaas diff --git a/.github/boring-cyborg.yml b/.github/boring-cyborg.yml index e1535a247cec3..2a5bc3e85bb7d 100644 --- a/.github/boring-cyborg.yml +++ b/.github/boring-cyborg.yml @@ -222,6 +222,9 @@ labelPRBasedOnFilePath: provider:neo4j: - providers/neo4j/** + provider:oci: + - providers/oci/** + provider:odbc: - providers/odbc/** diff --git a/airflow-core/docs/extra-packages-ref.rst b/airflow-core/docs/extra-packages-ref.rst index b6617690de233..23bd4f17e9f34 100644 --- a/airflow-core/docs/extra-packages-ref.rst +++ b/airflow-core/docs/extra-packages-ref.rst @@ -277,6 +277,8 @@ These are extras that add dependencies needed for integration with external serv +---------------------+-----------------------------------------------------+-----------------------------------------------------+ | openai | ``pip install 'apache-airflow[openai]'`` | Open AI hooks and operators | +---------------------+-----------------------------------------------------+-----------------------------------------------------+ +| oci | ``pip install 'apache-airflow[oci]'`` | Oracle Cloud Infrastructure hooks | ++---------------------+-----------------------------------------------------+-----------------------------------------------------+ | opsgenie | ``pip install 'apache-airflow[opsgenie]'`` | OpsGenie hooks and operators | +---------------------+-----------------------------------------------------+-----------------------------------------------------+ | pagerduty | ``pip install 'apache-airflow[pagerduty]'`` | Pagerduty hook | diff --git a/dev/breeze/doc/images/output_build-docs.svg b/dev/breeze/doc/images/output_build-docs.svg index 99bc5dbc0de1e..471794dc8699a 100644 --- a/dev/breeze/doc/images/output_build-docs.svg +++ b/dev/breeze/doc/images/output_build-docs.svg @@ -248,10 +248,10 @@ datadog | dbt.cloud | dingding | discord | docker | docker-stack | edge3 | elasticsearch | exasol | fab | facebook |   ftp | git | github | google | grpc | hashicorp | helm-chart | http | ibm.mq | imap | influxdb | informatica | java-sdk jdbc | jenkins | keycloak | microsoft.azure | microsoft.mssql | microsoft.psrp | microsoft.winrm | mongo | mysql |   -neo4j | odbc | openai | openfaas | openlineage | opensearch | opsgenie | oracle | pagerduty | papermill | pgvector |   -pinecone | postgres | presto | qdrant | redis | salesforce | samba | segment | sendgrid | sftp | singularity | slack | -smtp | snowflake | sqlite | ssh | standard | tableau | task-sdk | telegram | teradata | trino | vertica | vespa |      -weaviate | yandex | ydb | zendesk]...                                                                                  +neo4j | oci | odbc | openai | openfaas | openlineage | opensearch | opsgenie | oracle | pagerduty | papermill |        +pgvector | pinecone | postgres | presto | qdrant | redis | salesforce | samba | segment | sendgrid | sftp |            +singularity | slack | smtp | snowflake | sqlite | ssh | standard | tableau | task-sdk | telegram | teradata | trino |  +vertica | vespa | weaviate | yandex | ydb | zendesk]...                                                                Build documents. diff --git a/dev/breeze/doc/images/output_build-docs.txt b/dev/breeze/doc/images/output_build-docs.txt index 75dd9355bcff2..88a08d3c9defb 100644 --- a/dev/breeze/doc/images/output_build-docs.txt +++ b/dev/breeze/doc/images/output_build-docs.txt @@ -1 +1 @@ -e22969fb5e92a3efafb744881fea5ef8 +8274fbac5649f3bf08b5b886faf40591 diff --git a/dev/breeze/doc/images/output_release-management_add-back-references.svg b/dev/breeze/doc/images/output_release-management_add-back-references.svg index 9df4462030484..abeb895ac70dc 100644 --- a/dev/breeze/doc/images/output_release-management_add-back-references.svg +++ b/dev/breeze/doc/images/output_release-management_add-back-references.svg @@ -157,10 +157,10 @@ datadog | dbt.cloud | dingding | discord | docker | docker-stack | edge3 | elasticsearch | exasol | fab | facebook |   ftp | git | github | google | grpc | hashicorp | helm-chart | http | ibm.mq | imap | influxdb | informatica | java-sdk jdbc | jenkins | keycloak | microsoft.azure | microsoft.mssql | microsoft.psrp | microsoft.winrm | mongo | mysql |   -neo4j | odbc | openai | openfaas | openlineage | opensearch | opsgenie | oracle | pagerduty | papermill | pgvector |   -pinecone | postgres | presto | qdrant | redis | salesforce | samba | segment | sendgrid | sftp | singularity | slack | -smtp | snowflake | sqlite | ssh | standard | tableau | task-sdk | telegram | teradata | trino | vertica | vespa |      -weaviate | yandex | ydb | zendesk]...                                                                                  +neo4j | oci | odbc | openai | openfaas | openlineage | opensearch | opsgenie | oracle | pagerduty | papermill |        +pgvector | pinecone | postgres | presto | qdrant | redis | salesforce | samba | segment | sendgrid | sftp |            +singularity | slack | smtp | snowflake | sqlite | ssh | standard | tableau | task-sdk | telegram | teradata | trino |  +vertica | vespa | weaviate | yandex | ydb | zendesk]...                                                                Command to add back references for documentation to make it backward compatible. diff --git a/dev/breeze/doc/images/output_release-management_add-back-references.txt b/dev/breeze/doc/images/output_release-management_add-back-references.txt index 152f412a472d3..003217b799be1 100644 --- a/dev/breeze/doc/images/output_release-management_add-back-references.txt +++ b/dev/breeze/doc/images/output_release-management_add-back-references.txt @@ -1 +1 @@ -963d8b3e84cb64aa0d69026638b9fb1b +83361959dfa6b316629c4e7a61466930 diff --git a/dev/breeze/doc/images/output_release-management_publish-docs.svg b/dev/breeze/doc/images/output_release-management_publish-docs.svg index 59c03ad4d408a..e7b661bd27908 100644 --- a/dev/breeze/doc/images/output_release-management_publish-docs.svg +++ b/dev/breeze/doc/images/output_release-management_publish-docs.svg @@ -196,10 +196,10 @@ datadog | dbt.cloud | dingding | discord | docker | docker-stack | edge3 | elasticsearch | exasol | fab | facebook |   ftp | git | github | google | grpc | hashicorp | helm-chart | http | ibm.mq | imap | influxdb | informatica | java-sdk jdbc | jenkins | keycloak | microsoft.azure | microsoft.mssql | microsoft.psrp | microsoft.winrm | mongo | mysql |   -neo4j | odbc | openai | openfaas | openlineage | opensearch | opsgenie | oracle | pagerduty | papermill | pgvector |   -pinecone | postgres | presto | qdrant | redis | salesforce | samba | segment | sendgrid | sftp | singularity | slack | -smtp | snowflake | sqlite | ssh | standard | tableau | task-sdk | telegram | teradata | trino | vertica | vespa |      -weaviate | yandex | ydb | zendesk]...                                                                                  +neo4j | oci | odbc | openai | openfaas | openlineage | opensearch | opsgenie | oracle | pagerduty | papermill |        +pgvector | pinecone | postgres | presto | qdrant | redis | salesforce | samba | segment | sendgrid | sftp |            +singularity | slack | smtp | snowflake | sqlite | ssh | standard | tableau | task-sdk | telegram | teradata | trino |  +vertica | vespa | weaviate | yandex | ydb | zendesk]...                                                                Command to publish generated documentation to airflow-site diff --git a/dev/breeze/doc/images/output_release-management_publish-docs.txt b/dev/breeze/doc/images/output_release-management_publish-docs.txt index 585d41d6275cb..6f8f511a49eeb 100644 --- a/dev/breeze/doc/images/output_release-management_publish-docs.txt +++ b/dev/breeze/doc/images/output_release-management_publish-docs.txt @@ -1 +1 @@ -217c089e723c5584da63135bbf011504 +afdfcb46664d1368139752d59d58f3b7 diff --git a/dev/breeze/doc/images/output_sbom_generate-providers-requirements.svg b/dev/breeze/doc/images/output_sbom_generate-providers-requirements.svg index 8e1ee534c9b46..f63ce24b46a09 100644 --- a/dev/breeze/doc/images/output_sbom_generate-providers-requirements.svg +++ b/dev/breeze/doc/images/output_sbom_generate-providers-requirements.svg @@ -191,11 +191,11 @@ │datadog | dbt.cloud | dingding | discord | docker | edge3 | elasticsearch | exasol | fab | â”‚ │facebook | ftp | git | github | google | grpc | hashicorp | http | ibm.mq | imap | influxdb | â”‚ │informatica | jdbc | jenkins | keycloak | microsoft.azure | microsoft.mssql | microsoft.psrp | â”‚ -│microsoft.winrm | mongo | mysql | neo4j | odbc | openai | openfaas | openlineage | opensearch | â”‚ -│opsgenie | oracle | pagerduty | papermill | pgvector | pinecone | postgres | presto | qdrant | â”‚ -│redis | salesforce | samba | segment | sendgrid | sftp | singularity | slack | smtp | snowflake â”‚ -│| sqlite | ssh | standard | tableau | telegram | teradata | trino | vertica | vespa | weaviate |│ -│yandex | ydb | zendesk)│ +│microsoft.winrm | mongo | mysql | neo4j | oci | odbc | openai | openfaas | openlineage | â”‚ +│opensearch | opsgenie | oracle | pagerduty | papermill | pgvector | pinecone | postgres | presto│ +│| qdrant | redis | salesforce | samba | segment | sendgrid | sftp | singularity | slack | smtp |│ +│snowflake | sqlite | ssh | standard | tableau | telegram | teradata | trino | vertica | vespa | â”‚ +│weaviate | yandex | ydb | zendesk)│ │--provider-versionProvider version to generate the requirements for i.e `2.1.0`. `latest` is also a supported     â”‚ │value to account for the most recent version of the provider (TEXT)│ │--force           Force update providers requirements even if they already exist.│ diff --git a/dev/breeze/doc/images/output_sbom_generate-providers-requirements.txt b/dev/breeze/doc/images/output_sbom_generate-providers-requirements.txt index ca3930b91a1a2..3883631f73385 100644 --- a/dev/breeze/doc/images/output_sbom_generate-providers-requirements.txt +++ b/dev/breeze/doc/images/output_sbom_generate-providers-requirements.txt @@ -1 +1 @@ -7cbc3f1d6c0c4f1745702d867f9e7fdc +29feb9b2fc27f3b8a45168a91845f44e diff --git a/dev/breeze/doc/images/output_workflow-run_publish-docs.svg b/dev/breeze/doc/images/output_workflow-run_publish-docs.svg index 13e7e9bf14620..a24763f71a43b 100644 --- a/dev/breeze/doc/images/output_workflow-run_publish-docs.svg +++ b/dev/breeze/doc/images/output_workflow-run_publish-docs.svg @@ -211,10 +211,10 @@ datadog | dbt.cloud | dingding | discord | docker | docker-stack | edge3 | elasticsearch | exasol | fab | facebook |   ftp | git | github | google | grpc | hashicorp | helm-chart | http | ibm.mq | imap | influxdb | informatica | java-sdk jdbc | jenkins | keycloak | microsoft.azure | microsoft.mssql | microsoft.psrp | microsoft.winrm | mongo | mysql |   -neo4j | odbc | openai | openfaas | openlineage | opensearch | opsgenie | oracle | pagerduty | papermill | pgvector |   -pinecone | postgres | presto | qdrant | redis | salesforce | samba | segment | sendgrid | sftp | singularity | slack | -smtp | snowflake | sqlite | ssh | standard | tableau | task-sdk | telegram | teradata | trino | vertica | vespa |      -weaviate | yandex | ydb | zendesk]...                                                                                  +neo4j | oci | odbc | openai | openfaas | openlineage | opensearch | opsgenie | oracle | pagerduty | papermill |        +pgvector | pinecone | postgres | presto | qdrant | redis | salesforce | samba | segment | sendgrid | sftp |            +singularity | slack | smtp | snowflake | sqlite | ssh | standard | tableau | task-sdk | telegram | teradata | trino |  +vertica | vespa | weaviate | yandex | ydb | zendesk]...                                                                Trigger publish docs to S3 workflow 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 a86438aee4e27..2175f89f51441 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 @@ -cb87f41a19fbc8895cd38aa6351139ae +f58ca7887b44084a63bf1623fd1c0e6d diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 73212e3d6794d..abf0ac1e8711f 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -1164,6 +1164,7 @@ oauth objectORfile objectstorage observability +oci od odbc odps diff --git a/providers/oci/.gitignore b/providers/oci/.gitignore new file mode 100644 index 0000000000000..bff2d7629604d --- /dev/null +++ b/providers/oci/.gitignore @@ -0,0 +1 @@ +*.iml diff --git a/providers/oci/LICENSE b/providers/oci/LICENSE new file mode 100644 index 0000000000000..11069edd79019 --- /dev/null +++ b/providers/oci/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/oci/NOTICE b/providers/oci/NOTICE new file mode 100644 index 0000000000000..a51bd9390d030 --- /dev/null +++ b/providers/oci/NOTICE @@ -0,0 +1,5 @@ +Apache Airflow +Copyright 2016-2026 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). diff --git a/providers/oci/README.rst b/providers/oci/README.rst new file mode 100644 index 0000000000000..61291fd6ce6da --- /dev/null +++ b/providers/oci/README.rst @@ -0,0 +1,70 @@ + +.. 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-oci`` + +Release: ``0.1.0`` + + +`Oracle Cloud Infrastructure `__ integrations. + + +Provider package +---------------- + +This is a provider package for ``oci`` provider. All classes for this provider package +are in ``airflow.providers.oci`` 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 installation (see ``Requirements`` below +for the minimum Airflow version supported) via +``pip install apache-airflow-providers-oci`` + +The package supports the following python versions: 3.10,3.11,3.12,3.13,3.14 + +Requirements +------------ + +========================================== ================== +PIP package Version required +========================================== ================== +``apache-airflow`` ``>=2.11.0`` +``apache-airflow-providers-common-compat`` ``>=1.12.0`` +========================================== ================== + +Optional dependencies +---------------------- + +======= ================ +Extra Dependencies +======= ================ +``oci`` ``oci>=2.182.0`` +======= ================ + +The changelog for the provider package can be found in the +`changelog `_. diff --git a/providers/oci/docs/changelog.rst b/providers/oci/docs/changelog.rst new file mode 100644 index 0000000000000..1731474c36e96 --- /dev/null +++ b/providers/oci/docs/changelog.rst @@ -0,0 +1,26 @@ + .. Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + .. http://www.apache.org/licenses/LICENSE-2.0 + + .. Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + +``apache-airflow-providers-oci`` + +Changelog +--------- + +0.1.0 +..... + +Initial version of the provider. diff --git a/providers/oci/docs/commits.rst b/providers/oci/docs/commits.rst new file mode 100644 index 0000000000000..0f336ec6ff9a9 --- /dev/null +++ b/providers/oci/docs/commits.rst @@ -0,0 +1,34 @@ + .. 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-oci +------------------------------------ + +`Oracle Cloud Infrastructure `__ integrations. + + +This is the detailed commit list for the ``oci`` provider package. +For the high-level changelog, see :doc:`package information including changelog `. + +.. airflow-providers-commits:: diff --git a/providers/oci/docs/conf.py b/providers/oci/docs/conf.py new file mode 100644 index 0000000000000..cecab82858db3 --- /dev/null +++ b/providers/oci/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-oci" + +from docs.provider_conf import * # noqa: F403 diff --git a/providers/oci/docs/connections/oci.rst b/providers/oci/docs/connections/oci.rst new file mode 100644 index 0000000000000..cfc7cb5f6e59f --- /dev/null +++ b/providers/oci/docs/connections/oci.rst @@ -0,0 +1,141 @@ + .. 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:oci: + +Oracle Cloud Infrastructure Connection +====================================== + +The Oracle Cloud Infrastructure connection configures authentication for OCI SDK clients. +It is separate from the :ref:`Oracle Database connection `, which +uses the ``oracle`` connection type and the ``oracledb`` driver. + +The OCI Python SDK transitively depends on the LGPL-licensed ``crc32c`` package. Under the +`ASF third-party licensing policy `__, +this Category X dependency must remain optional. Install OCI support explicitly through the +provider extra before using this connection or its service hooks: + +.. code-block:: bash + + pip install 'apache-airflow-providers-oci[oci]' + +The default connection ID is ``oci_default``. + +Service hooks use the selected authentication type and optional connection defaults while +instantiating the appropriate OCI SDK client. Each SDK client derives its endpoint from the +configured region unless the Dag author passes a ``service_endpoint`` argument to the hook. + +Authentication types +-------------------- + +API key + This is the default. The connection must provide ``User OCID`` in Login and ``tenancy``, + ``fingerprint``, and ``region`` in Extra. It must also provide ``private_key_content`` in Extra + unless the Dag author passes ``key_file`` to the hook instead. Password is required when the + private key is encrypted and is otherwise optional. + +Config file + The Dag author passes ``auth_type="config_file"`` to load an API key configuration profile. + The optional ``config_file`` and ``profile`` hook arguments default to ``~/.oci/config`` and + ``DEFAULT``. The profile must contain the fields required by the OCI SDK for API key + authentication. An Airflow connection is optional and only provides a ``region`` override for + the profile. + +Instance principal + The Dag author passes ``auth_type="instance_principal"`` to the hook. The OCI SDK obtains + credentials from the compute instance metadata service, so no Airflow connection is required. + When present, the connection can override the ``region`` normally discovered by the signer. + +Resource principal + The Dag author passes ``auth_type="resource_principal"`` to the hook. The OCI SDK obtains + credentials from the resource principal environment, so no Airflow connection is required. + When present, the connection can provide a ``region`` if the signer does not. + +File paths, principal authentication, and custom endpoints are intentionally hook arguments rather +than connection fields. This ensures that only Dag authors can make the worker read local files, +use its ambient OCI identity, or send requests to a non-standard endpoint. + +Testing the connection +---------------------- + +The generic OCI connection test calls ``IdentityClient.list_regions`` to validate the configured +credentials independently of any service-specific hook. The identity must have the +``TENANCY_INSPECT`` permission. The connection UI tests the default API key authentication. Dag +authors can call ``test_connection()`` on a hook configured for another authentication type. + +Configuring the connection +-------------------------- + +Login + User OCID. Required for ``api_key`` authentication and not used by the other authentication + types. + +Password + Private key passphrase. Required for ``api_key`` authentication when the private key is + encrypted and otherwise optional. + +Extra + A JSON object containing connection-scoped credentials and defaults: + + * ``tenancy``: tenancy OCID. Required for ``api_key`` authentication. + * ``fingerprint``: public key fingerprint. Required for ``api_key`` authentication. + * ``private_key_content``: API signing private key content. Required for ``api_key`` + authentication unless the Dag author passes ``key_file`` to the hook instead; prefer a + secrets backend. + * ``region``: OCI region identifier containing only ASCII letters, digits, and hyphens, for + example ``us-chicago-1``. Domains and URLs are rejected; Dag authors must use the + ``service_endpoint`` hook argument for custom endpoints. The region is required for ``api_key`` + authentication, optional as a ``config_file`` profile override, and optional for principal + authentication when the signer provides one. + * ``compartment_id``: optional default compartment OCID used by service operations. + +Dag-controlled hook arguments +----------------------------- + +``auth_type`` + One of ``api_key``, ``config_file``, ``instance_principal``, or ``resource_principal``. + +``oci_conn_id`` + Defaults to ``oci_default``. Pass ``None`` to skip connection lookup when using + ``config_file``, ``instance_principal``, or ``resource_principal`` authentication. + +``key_file`` + API signing private key path for API key authentication. Do not use it together with + connection ``private_key_content``. + +``config_file`` and ``profile`` + OCI SDK configuration file and profile for ``config_file`` authentication. + +``service_endpoint`` + Explicit service endpoint, for example + ``https://generativeai.us-chicago-1.oci.oraclecloud.com``. Do not append the API version path. + +API key example +--------------- + +.. code-block:: json + + { + "tenancy": "ocid1.tenancy.oc1..example", + "fingerprint": "aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99", + "private_key_content": "", + "region": "us-chicago-1", + "compartment_id": "ocid1.compartment.oc1..example" + } + +Use an Airflow secrets backend for production credentials and avoid logging connection extras or +private key material. diff --git a/providers/oci/docs/generative_ai.rst b/providers/oci/docs/generative_ai.rst new file mode 100644 index 0000000000000..29dcd5e373c0f --- /dev/null +++ b/providers/oci/docs/generative_ai.rst @@ -0,0 +1,86 @@ + .. 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. + +OCI Generative AI Hosted Applications +===================================== + +:class:`~airflow.providers.oci.hooks.generative_ai.OciGenerativeAIHook` uses the official +`OCI Python SDK `__ to manage +`Hosted Applications and deployments +`__. +Install ``apache-airflow-providers-oci[oci]`` before using the hook. +Configure an +:ref:`OCI connection ` for API key authentication or optional +connection-scoped defaults. +The hook exposes the native :class:`oci.generative_ai.GenerativeAiClient` through ``conn`` and +``get_conn()``. Operators can therefore call OCI SDK methods directly without an Airflow wrapper +for every API operation. + +Oracle exposes separate application APIs for `two inbound authentication variants +`__: + +* Identity domain bearer tokens use the OCI SDK ``HostedApplication`` resource and client methods + ending in ``hosted_application`` or ``hosted_applications``. +* OCI IAM request signing uses ``HostedApplicationIam`` and client methods ending in + ``hosted_application_iam`` or ``hosted_applications_iam``. + +This distinction configures how clients invoke the deployed application. It does not change how +the Airflow hook authenticates to the OCI management API; both variants use the authentication +type selected on the hook. + +All client methods return the native :class:`oci.response.Response`. This preserves response data and +headers such as ``etag``, ``opc-request-id``, and ``opc-work-request-id``. Create, update, and +delete operations can be asynchronous; use ``opc-work-request-id`` with +``hook.conn.get_work_request`` to observe their status. + +Use ``hook.get_compartment_id()`` to resolve an explicit compartment or the connection default +before calling list methods. To filter deployments for a Hosted Application, pass its OCID as the +OCI SDK ``application_id`` keyword argument. + +Creating an identity domain Hosted Application +----------------------------------------------- + +Identity domain applications require an ``InboundAuthConfig`` containing the identity domain URL +and OAuth settings: + +.. code-block:: python + + from airflow.providers.oci.hooks.generative_ai import OciGenerativeAIHook + + hook = OciGenerativeAIHook(oci_conn_id="oci_default") + response = hook.conn.create_hosted_application(...) + work_request_id = response.headers["opc-work-request-id"] + +Creating an OCI IAM Hosted Application +-------------------------------------- + +OCI IAM applications do not require an OAuth or identity domain configuration: + +.. code-block:: python + + from airflow.providers.oci.hooks.generative_ai import OciGenerativeAIHook + + hook = OciGenerativeAIHook(oci_conn_id="oci_default") + response = hook.conn.create_hosted_application_iam(...) + work_request_id = response.headers["opc-work-request-id"] + +Agent invocation +---------------- + +This hook covers the Generative AI management API only. Invoking an active Hosted Application uses +the Generative AI inference endpoint and a custom application path; it is intentionally outside this +management hook's contract. diff --git a/providers/oci/docs/index.rst b/providers/oci/docs/index.rst new file mode 100644 index 0000000000000..929cca81392fb --- /dev/null +++ b/providers/oci/docs/index.rst @@ -0,0 +1,126 @@ + .. 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-oci`` +================================ + +The ``oci`` provider integrates Airflow with Oracle Cloud Infrastructure services. It provides +shared OCI authentication, native OCI SDK client construction, and a hook for the Generative AI +management API. + +.. toctree:: + :hidden: + :maxdepth: 1 + :caption: Basics + + Home + Changelog + Security + +.. toctree:: + :hidden: + :maxdepth: 1 + :caption: Guides + + Oracle Cloud Infrastructure connection + OCI Generative AI + +.. toctree:: + :hidden: + :maxdepth: 1 + :caption: Resources + + Python API <_api/airflow/providers/oci/index> + PyPI Repository + Installing from sources + +.. toctree:: + :hidden: + :maxdepth: 1 + :caption: System tests + + System Tests <_api/tests/system/oci/index> + +.. 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-oci package +------------------------------------------------------ + +`Oracle Cloud Infrastructure `__ integrations. + + +Release: 0.1.0 + +Provider package +---------------- + +This package is for the ``oci`` provider. +All classes for this package are included in the ``airflow.providers.oci`` python package. + +Installation +------------ + +You can install this package on top of an existing Airflow installation via +``pip install apache-airflow-providers-oci``. +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-compat`` ``>=1.12.0`` +========================================== ================== + +Optional dependencies +--------------------- + +These extras install optional third-party libraries that enable additional features of the provider. +Install them when installing from PyPI. For example: + +.. code-block:: bash + + pip install apache-airflow-providers-oci[oci] + + +======= ================ +Extra Dependencies +======= ================ +``oci`` ``oci>=2.182.0`` +======= ================ + +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-oci 0.1.0 sdist package `_ (`asc `__, `sha512 `__) +* `The apache-airflow-providers-oci 0.1.0 wheel package `_ (`asc `__, `sha512 `__) diff --git a/providers/oci/docs/installing-providers-from-sources.rst b/providers/oci/docs/installing-providers-from-sources.rst new file mode 100644 index 0000000000000..a72b45ffaa6e8 --- /dev/null +++ b/providers/oci/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/oci/docs/security.rst b/providers/oci/docs/security.rst new file mode 100644 index 0000000000000..15a0ebbb2d054 --- /dev/null +++ b/providers/oci/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/oci/provider.yaml b/providers/oci/provider.yaml new file mode 100644 index 0000000000000..35a2e5ce07469 --- /dev/null +++ b/providers/oci/provider.yaml @@ -0,0 +1,101 @@ +# 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-oci +name: Oracle Cloud Infrastructure (OCI) +description: | + `Oracle Cloud Infrastructure `__ integrations. + +state: not-ready +lifecycle: incubation +source-date-epoch: 1785967200 + +# 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: Oracle Cloud Infrastructure + external-doc-url: https://docs.oracle.com/en-us/iaas/Content/home.htm + tags: [service] + - integration-name: OCI Generative AI + external-doc-url: https://docs.oracle.com/en-us/iaas/Content/generative-ai/home.htm + tags: [generative-ai, service] + +hooks: + - integration-name: Oracle Cloud Infrastructure + python-modules: + - airflow.providers.oci.hooks.base + - integration-name: OCI Generative AI + python-modules: + - airflow.providers.oci.hooks.generative_ai + +connection-types: + - hook-class-name: airflow.providers.oci.hooks.base.OciBaseHook + hook-name: "Oracle Cloud Infrastructure" + connection-type: oci + ui-field-behaviour: + hidden-fields: + - host + - schema + - port + relabeling: + login: User OCID + password: Private Key Passphrase + placeholders: + login: ocid1.user... + password: Optional API key passphrase + tenancy: ocid1.tenancy... + fingerprint: aa:bb:cc:... + region: us-chicago-1 + compartment_id: ocid1.compartment... + conn-fields: + tenancy: + label: Tenancy OCID + schema: + type: + - string + - 'null' + fingerprint: + label: Key Fingerprint + schema: + type: + - string + - 'null' + private_key_content: + label: Private Key Content + schema: + type: + - string + - 'null' + format: password + region: + label: Region + schema: + type: + - string + - 'null' + compartment_id: + label: Compartment OCID + schema: + type: + - string + - 'null' diff --git a/providers/oci/pyproject.toml b/providers/oci/pyproject.toml new file mode 100644 index 0000000000000..6379a53629b94 --- /dev/null +++ b/providers/oci/pyproject.toml @@ -0,0 +1,134 @@ +# 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-oci" +version = "0.1.0" +description = "Provider package apache-airflow-providers-oci 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", "oci", "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", + "Programming Language :: Python :: 3.14", + "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-compat>=1.12.0", +] + +# 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] +"oci" = [ + # The OCI SDK transitively depends on LGPL-licensed crc32c. ASF policy permits Category X + # dependencies only for optional features, so the SDK must not be a required dependency. + "oci>=2.182.0", +] + +[dependency-groups] +dev = [ + "apache-airflow", + "apache-airflow-task-sdk", + "apache-airflow-devel-common", + "apache-airflow-providers-common-compat", + # Additional devel dependencies (do not remove this line and add extra development dependencies) + "apache-airflow-providers-oci[oci]", +] + +# 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-sql = {workspace = true} +apache-airflow-providers-standard = {workspace = true} + +[project.urls] +"Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-oci/0.1.0" +"Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-oci/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.oci.get_provider_info:get_provider_info" + +[tool.flit.module] +name = "airflow.providers.oci" + +# Explicit sdist contents so the build does not rely on VCS information +# (flit 4.0 makes --no-use-vcs the default — see https://github.com/pypa/flit/pull/782). +[tool.flit.sdist] +include = [ + "docs/", + "provider.yaml", + "src/airflow/__init__.py", + "src/airflow/providers/__init__.py", + "tests/", +] diff --git a/providers/oci/src/airflow/__init__.py b/providers/oci/src/airflow/__init__.py new file mode 100644 index 0000000000000..5966d6b1d5261 --- /dev/null +++ b/providers/oci/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__) diff --git a/providers/oci/src/airflow/providers/__init__.py b/providers/oci/src/airflow/providers/__init__.py new file mode 100644 index 0000000000000..5966d6b1d5261 --- /dev/null +++ b/providers/oci/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__) diff --git a/providers/oci/src/airflow/providers/oci/__init__.py b/providers/oci/src/airflow/providers/oci/__init__.py new file mode 100644 index 0000000000000..66b45bd08f0d2 --- /dev/null +++ b/providers/oci/src/airflow/providers/oci/__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-oci:{__version__}` needs Apache Airflow 2.11.0+" + ) diff --git a/providers/oci/src/airflow/providers/oci/get_provider_info.py b/providers/oci/src/airflow/providers/oci/get_provider_info.py new file mode 100644 index 0000000000000..74ca568e4a8e0 --- /dev/null +++ b/providers/oci/src/airflow/providers/oci/get_provider_info.py @@ -0,0 +1,80 @@ +# 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-oci", + "name": "Oracle Cloud Infrastructure (OCI)", + "description": "`Oracle Cloud Infrastructure `__ integrations.\n", + "integrations": [ + { + "integration-name": "Oracle Cloud Infrastructure", + "external-doc-url": "https://docs.oracle.com/en-us/iaas/Content/home.htm", + "tags": ["service"], + }, + { + "integration-name": "OCI Generative AI", + "external-doc-url": "https://docs.oracle.com/en-us/iaas/Content/generative-ai/home.htm", + "tags": ["generative-ai", "service"], + }, + ], + "hooks": [ + { + "integration-name": "Oracle Cloud Infrastructure", + "python-modules": ["airflow.providers.oci.hooks.base"], + }, + { + "integration-name": "OCI Generative AI", + "python-modules": ["airflow.providers.oci.hooks.generative_ai"], + }, + ], + "connection-types": [ + { + "hook-class-name": "airflow.providers.oci.hooks.base.OciBaseHook", + "hook-name": "Oracle Cloud Infrastructure", + "connection-type": "oci", + "ui-field-behaviour": { + "hidden-fields": ["host", "schema", "port"], + "relabeling": {"login": "User OCID", "password": "Private Key Passphrase"}, + "placeholders": { + "login": "ocid1.user...", + "password": "Optional API key passphrase", + "tenancy": "ocid1.tenancy...", + "fingerprint": "aa:bb:cc:...", + "region": "us-chicago-1", + "compartment_id": "ocid1.compartment...", + }, + }, + "conn-fields": { + "tenancy": {"label": "Tenancy OCID", "schema": {"type": ["string", "null"]}}, + "fingerprint": {"label": "Key Fingerprint", "schema": {"type": ["string", "null"]}}, + "private_key_content": { + "label": "Private Key Content", + "schema": {"type": ["string", "null"], "format": "password"}, + }, + "region": {"label": "Region", "schema": {"type": ["string", "null"]}}, + "compartment_id": {"label": "Compartment OCID", "schema": {"type": ["string", "null"]}}, + }, + } + ], + } diff --git a/providers/oci/src/airflow/providers/oci/hooks/__init__.py b/providers/oci/src/airflow/providers/oci/hooks/__init__.py new file mode 100644 index 0000000000000..13a83393a9124 --- /dev/null +++ b/providers/oci/src/airflow/providers/oci/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/oci/src/airflow/providers/oci/hooks/base.py b/providers/oci/src/airflow/providers/oci/hooks/base.py new file mode 100644 index 0000000000000..bbe972b273f1f --- /dev/null +++ b/providers/oci/src/airflow/providers/oci/hooks/base.py @@ -0,0 +1,282 @@ +# 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 collections.abc import Callable +from enum import Enum +from functools import cached_property +from typing import TYPE_CHECKING, Any, Generic, TypeVar + +from airflow.providers.common.compat.sdk import ( + AirflowNotFoundException, + AirflowOptionalProviderFeatureException, + BaseHook, +) + +if TYPE_CHECKING: + from airflow.sdk import Connection + + OciSigner = Any + +OciClient = TypeVar("OciClient") + +OCI_REGION_IDENTIFIER_PATTERN = re.compile(r"[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?") + + +class OciAuthType(str, Enum): + """Authentication types supported by OCI hooks.""" + + API_KEY = "api_key" + CONFIG_FILE = "config_file" + INSTANCE_PRINCIPAL = "instance_principal" + RESOURCE_PRINCIPAL = "resource_principal" + + +def _get_oci_sdk() -> Any: + try: + import oci + except ImportError as e: + raise AirflowOptionalProviderFeatureException( + "OCI features require the optional OCI Python SDK. " + "Install it with: pip install 'apache-airflow-providers-oci[oci]'" + ) from e + return oci + + +class OciBaseHook(BaseHook, Generic[OciClient]): + """ + Base hook for Oracle Cloud Infrastructure services. + + The hook supports API key, OCI configuration file, instance principal, and resource principal + authentication. API key credentials are read from the connection fields, while principal + authentication is delegated to the OCI SDK. + + :param oci_conn_id: The :ref:`OCI connection id `. + Defaults to ``oci_default``. + Pass ``None`` to skip connection lookup for authentication methods that do not require it. + :param auth_type: OCI authentication type selected by the Dag author. + Defaults to ``api_key``. + :param key_file: API signing private key path selected by the Dag author. + Defaults to ``None``. + :param config_file: OCI SDK configuration file selected by the Dag author. + If not specified, the OCI SDK default location, ``~/.oci/config``, is used for + configuration file authentication. + :param profile: Profile to load from the OCI SDK configuration file. + If not specified, the OCI SDK default profile, ``DEFAULT``, is used. + :param service_endpoint: Optional service endpoint selected by the Dag author. + If not specified, the OCI SDK derives the endpoint from the configured region. + """ + + conn_name_attr = "oci_conn_id" + default_conn_name = "oci_default" + conn_type = "oci" + hook_name = "Oracle Cloud Infrastructure" + + def __init__( + self, + oci_conn_id: str | None = default_conn_name, + *, + auth_type: str = OciAuthType.API_KEY, + key_file: str | None = None, + config_file: str | None = None, + profile: str | None = None, + service_endpoint: str | None = None, + ) -> None: + super().__init__() + self.oci_conn_id = oci_conn_id + self.auth_type = auth_type + self.key_file = key_file + self.config_file = config_file + self.profile = profile + self.service_endpoint = service_endpoint + + @classmethod + def get_connection_form_widgets(cls) -> dict[str, Any]: + """Return connection widgets to add to the connection form.""" + from flask_appbuilder.fieldwidgets import BS3PasswordFieldWidget, BS3TextFieldWidget + from flask_babel import lazy_gettext + from wtforms import PasswordField, StringField + + return { + "tenancy": StringField(lazy_gettext("Tenancy OCID"), widget=BS3TextFieldWidget()), + "fingerprint": StringField(lazy_gettext("Key Fingerprint"), widget=BS3TextFieldWidget()), + "private_key_content": PasswordField( + lazy_gettext("Private Key Content"), widget=BS3PasswordFieldWidget() + ), + "region": StringField(lazy_gettext("Region"), widget=BS3TextFieldWidget()), + "compartment_id": StringField(lazy_gettext("Compartment OCID"), widget=BS3TextFieldWidget()), + } + + @classmethod + def get_ui_field_behaviour(cls) -> dict[str, Any]: + """Return custom field behavior for the connection form.""" + return { + "hidden_fields": ["host", "schema", "port"], + "relabeling": { + "login": "User OCID", + "password": "Private Key Passphrase", + }, + "placeholders": { + "login": "ocid1.user...", + "password": "Optional API key passphrase", + "tenancy": "ocid1.tenancy...", + "fingerprint": "aa:bb:cc:...", + "region": "us-chicago-1", + "compartment_id": "ocid1.compartment...", + }, + } + + @cached_property + def connection(self) -> Connection: + """Return the configured Airflow connection.""" + if not self.oci_conn_id: + raise ValueError("An OCI connection ID is required for API key authentication.") + return self.get_connection(self.oci_conn_id) + + def get_oci_config(self) -> tuple[dict[str, Any], OciSigner | None]: + """Build OCI SDK configuration and an optional signer for the selected authentication type.""" + oci = _get_oci_sdk() + auth_type = self.auth_type + + if auth_type == OciAuthType.CONFIG_FILE: + extras = self._get_optional_connection_extras() + config = oci.config.from_file( + file_location=self.config_file or oci.config.DEFAULT_LOCATION, + profile_name=self.profile or oci.config.DEFAULT_PROFILE, + ) + if region := self._get_connection_region(extras): + config["region"] = region + return config, None + + if auth_type == OciAuthType.INSTANCE_PRINCIPAL: + extras = self._get_optional_connection_extras() + signer = oci.auth.signers.InstancePrincipalsSecurityTokenSigner() + return self._build_principal_config(extras, signer), signer + + if auth_type == OciAuthType.RESOURCE_PRINCIPAL: + extras = self._get_optional_connection_extras() + signer = oci.auth.signers.get_resource_principals_signer() + return self._build_principal_config(extras, signer), signer + + if auth_type != OciAuthType.API_KEY: + raise ValueError( + f"Unsupported OCI authentication type: {auth_type!r}. " + f"Expected one of {tuple(auth_type.value for auth_type in OciAuthType)}." + ) + + conn = self.connection + extras = conn.extra_dejson + config = { + "tenancy": extras.get("tenancy"), + "user": conn.login, + "fingerprint": extras.get("fingerprint"), + "region": self._get_connection_region(extras), + "pass_phrase": conn.password, + } + key_file = self.key_file + private_key_content = extras.get("private_key_content") + if key_file and private_key_content: + raise ValueError( + "OCI API key authentication cannot use both 'key_file' and 'private_key_content'." + ) + if not key_file and not private_key_content: + raise ValueError( + "OCI API key authentication requires either 'key_file' or 'private_key_content'." + ) + if key_file: + config["key_file"] = key_file + else: + config["key_content"] = private_key_content + return config, None + + def get_client(self, client_class: Callable[..., OciClient], **client_kwargs: Any) -> OciClient: + """Return an authenticated OCI SDK client.""" + config, signer = self.get_oci_config() + if signer is not None: + client_kwargs["signer"] = signer + if service_endpoint := self._get_service_endpoint(): + client_kwargs["service_endpoint"] = service_endpoint + return client_class(config=config, **client_kwargs) + + @cached_property + def conn(self) -> OciClient: + """Return the configured OCI SDK client.""" + return self.get_client(self._get_client_class()) + + def get_conn(self) -> OciClient: + """Return the cached OCI SDK client.""" + return self.conn + + def test_connection(self) -> tuple[bool, str]: + """Test OCI credentials against the Identity service.""" + try: + _get_oci_sdk() + from oci.identity import IdentityClient + + config, signer = self.get_oci_config() + client_kwargs = {"signer": signer} if signer is not None else {} + IdentityClient(config=config, **client_kwargs).list_regions() + except Exception as e: + return False, f"{type(e).__name__} error occurred while testing connection: {e}" + return True, "Connection successfully tested" + + def get_compartment_id(self, compartment_id: str | None = None) -> str: + """Return an explicit compartment OCID or the default from the connection.""" + resolved_compartment_id = compartment_id or self._get_optional_connection_extras().get( + "compartment_id" + ) + if not resolved_compartment_id: + raise ValueError( + "An OCI compartment OCID must be provided as a method argument or in the connection extra." + ) + return resolved_compartment_id + + @classmethod + def _build_principal_config(cls, extras: dict[str, Any], signer: OciSigner) -> dict[str, Any]: + region = cls._get_connection_region(extras) or getattr(signer, "region", None) + return {"region": region} if region else {} + + @staticmethod + def _get_connection_region(extras: dict[str, Any]) -> str | None: + region = extras.get("region") + if region in (None, ""): + return None + if not isinstance(region, str) or OCI_REGION_IDENTIFIER_PATTERN.fullmatch(region) is None: + raise ValueError( + "The OCI connection region must be a region identifier containing only ASCII letters, " + "digits, and hyphens. Dag authors can use service_endpoint for custom endpoints." + ) + return region + + def _get_optional_connection_extras(self) -> dict[str, Any]: + if not self.oci_conn_id: + return {} + try: + return self.connection.extra_dejson + except AirflowNotFoundException: + self.log.warning( + "Unable to find OCI Connection ID '%s'; continuing without connection defaults.", + self.oci_conn_id, + ) + return {} + + def _get_service_endpoint(self) -> str | None: + return self.service_endpoint.rstrip("/") if self.service_endpoint else None + + def _get_client_class(self) -> Callable[..., OciClient]: + raise NotImplementedError("OCI service hooks must implement _get_client_class().") diff --git a/providers/oci/src/airflow/providers/oci/hooks/generative_ai.py b/providers/oci/src/airflow/providers/oci/hooks/generative_ai.py new file mode 100644 index 0000000000000..9f059b50118ab --- /dev/null +++ b/providers/oci/src/airflow/providers/oci/hooks/generative_ai.py @@ -0,0 +1,46 @@ +# 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 collections.abc import Callable +from typing import TYPE_CHECKING + +from airflow.providers.oci.hooks.base import OciBaseHook, _get_oci_sdk + +if TYPE_CHECKING: + from oci.generative_ai import GenerativeAiClient + + +class OciGenerativeAIHook(OciBaseHook["GenerativeAiClient"]): + """ + Hook for OCI Generative AI Hosted Applications and Hosted Deployments. + + The hook exposes the native OCI Generative AI management client through ``conn`` and + ``get_conn()``. Client methods return OCI SDK responses so callers retain response data, + ``ETags``, request identifiers, and work request identifiers. + + :param oci_conn_id: The :ref:`OCI connection id `. + :param service_endpoint: Optional Generative AI service endpoint selected by the Dag author. + """ + + hook_name = "OCI Generative AI" + + def _get_client_class(self) -> Callable[..., GenerativeAiClient]: + _get_oci_sdk() + from oci.generative_ai import GenerativeAiClient + + return GenerativeAiClient diff --git a/providers/oci/tests/conftest.py b/providers/oci/tests/conftest.py new file mode 100644 index 0000000000000..f56ccce0a3f69 --- /dev/null +++ b/providers/oci/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/oci/tests/system/__init__.py b/providers/oci/tests/system/__init__.py new file mode 100644 index 0000000000000..5966d6b1d5261 --- /dev/null +++ b/providers/oci/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__) diff --git a/providers/oci/tests/system/oci/__init__.py b/providers/oci/tests/system/oci/__init__.py new file mode 100644 index 0000000000000..13a83393a9124 --- /dev/null +++ b/providers/oci/tests/system/oci/__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/oci/tests/system/oci/example_oci_generative_ai.py b/providers/oci/tests/system/oci/example_oci_generative_ai.py new file mode 100644 index 0000000000000..bf41824103057 --- /dev/null +++ b/providers/oci/tests/system/oci/example_oci_generative_ai.py @@ -0,0 +1,52 @@ +# 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 datetime import datetime + +try: + from airflow.sdk import DAG, task +except ImportError: + from airflow import DAG # type: ignore[attr-defined,no-redef] + from airflow.decorators import task # type: ignore[attr-defined,no-redef] + +DAG_ID = "example_oci_generative_ai" + +with DAG( + dag_id=DAG_ID, + schedule=None, + start_date=datetime(2025, 1, 1), + catchup=False, + tags=["example", "oci", "generative-ai"], +) as dag: + + @task + def list_generative_ai_resources() -> None: + """Validate read-only access to OCI Generative AI resources.""" + from airflow.providers.oci.hooks.generative_ai import OciGenerativeAIHook + + hook = OciGenerativeAIHook() + compartment_id = hook.get_compartment_id() + hook.conn.list_hosted_applications(compartment_id=compartment_id) + hook.conn.list_hosted_applications_iam(compartment_id=compartment_id) + hook.conn.list_hosted_deployments(compartment_id=compartment_id) + + list_generative_ai_resources() + +from tests_common.test_utils.system_tests import get_test_run # noqa: E402 + +test_run = get_test_run(dag) diff --git a/providers/oci/tests/unit/__init__.py b/providers/oci/tests/unit/__init__.py new file mode 100644 index 0000000000000..5966d6b1d5261 --- /dev/null +++ b/providers/oci/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__) diff --git a/providers/oci/tests/unit/oci/__init__.py b/providers/oci/tests/unit/oci/__init__.py new file mode 100644 index 0000000000000..13a83393a9124 --- /dev/null +++ b/providers/oci/tests/unit/oci/__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/oci/tests/unit/oci/hooks/__init__.py b/providers/oci/tests/unit/oci/hooks/__init__.py new file mode 100644 index 0000000000000..13a83393a9124 --- /dev/null +++ b/providers/oci/tests/unit/oci/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/oci/tests/unit/oci/hooks/test_base.py b/providers/oci/tests/unit/oci/hooks/test_base.py new file mode 100644 index 0000000000000..f681db6ea9e77 --- /dev/null +++ b/providers/oci/tests/unit/oci/hooks/test_base.py @@ -0,0 +1,425 @@ +# 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 + +import pytest + +from airflow.models import Connection +from airflow.providers.common.compat.sdk import AirflowNotFoundException +from airflow.providers.oci.get_provider_info import get_provider_info +from airflow.providers.oci.hooks.base import ( + OciAuthType, + OciBaseHook, +) + +GenerativeAiClient = pytest.importorskip("oci.generative_ai").GenerativeAiClient + + +class TestOciBaseHook: + def setup_method(self): + self.hook = OciBaseHook() + + def set_connection(self, connection: Connection) -> None: + self.hook.get_connection = mock.create_autospec(self.hook.get_connection, return_value=connection) + + def set_missing_connection(self) -> None: + self.hook.get_connection = mock.create_autospec( + self.hook.get_connection, + side_effect=AirflowNotFoundException("The conn_id `oci_default` isn't defined"), + ) + + @pytest.mark.parametrize( + ("hook_kwargs", "connection_extra", "key_field", "key_value"), + [ + ({"key_file": "/keys/oci.pem"}, {}, "key_file", "/keys/oci.pem"), + ( + {}, + {"private_key_content": "private-key-content"}, + "key_content", + "private-key-content", + ), + ], + ) + def test_get_oci_config_with_api_key(self, hook_kwargs, connection_extra, key_field, key_value): + self.hook = OciBaseHook(**hook_kwargs) + self.set_connection( + Connection( + login="ocid1.user.test", + password="passphrase", + extra={ + "tenancy": "ocid1.tenancy.test", + "fingerprint": "fingerprint", + "region": "us-chicago-1", + **connection_extra, + }, + ) + ) + + config, signer = self.hook.get_oci_config() + + assert config == { + "tenancy": "ocid1.tenancy.test", + "user": "ocid1.user.test", + "fingerprint": "fingerprint", + "region": "us-chicago-1", + "pass_phrase": "passphrase", + key_field: key_value, + } + assert signer is None + + def test_connection_extra_cannot_control_hook_configuration(self): + self.set_connection( + Connection( + login="ocid1.user.test", + extra={ + "auth_type": OciAuthType.INSTANCE_PRINCIPAL.value, + "key_file": "/etc/hosts", + "config_file": "/etc/hosts", + "profile": "UNTRUSTED", + "service_endpoint": "https://untrusted.example.test", + "tenancy": "ocid1.tenancy.test", + "fingerprint": "fingerprint", + "region": "us-chicago-1", + "private_key_content": "private-key-content", + }, + ) + ) + + config, signer = self.hook.get_oci_config() + + assert config == { + "tenancy": "ocid1.tenancy.test", + "user": "ocid1.user.test", + "fingerprint": "fingerprint", + "region": "us-chicago-1", + "pass_phrase": None, + "key_content": "private-key-content", + } + assert signer is None + + @pytest.mark.parametrize("auth_type", list(OciAuthType)) + @mock.patch("oci.config.from_file", autospec=True, return_value={}) + @mock.patch("oci.auth.signers.InstancePrincipalsSecurityTokenSigner", autospec=True) + @mock.patch("oci.auth.signers.get_resource_principals_signer", autospec=True) + def test_connection_region_cannot_control_service_domain( + self, + mock_resource_principal_signer, + mock_instance_principal_signer, + mock_from_file, + auth_type, + ): + self.hook = OciBaseHook(auth_type=auth_type, key_file="/keys/oci.pem") + self.set_connection( + Connection( + login="ocid1.user.test", + extra={ + "tenancy": "ocid1.tenancy.test", + "fingerprint": "fingerprint", + "region": "attacker.example", + }, + ) + ) + + with pytest.raises(ValueError, match="must be a region identifier"): + self.hook.get_oci_config() + + @pytest.mark.parametrize( + ("hook_kwargs", "extra", "error_message"), + [ + ( + {}, + {}, + "OCI API key authentication requires either 'key_file' or 'private_key_content'", + ), + ( + {"key_file": "/keys/oci.pem"}, + {"private_key_content": "private-key-content"}, + "OCI API key authentication cannot use both 'key_file' and 'private_key_content'", + ), + ], + ids=["missing-key", "conflicting-keys"], + ) + def test_get_oci_config_rejects_invalid_api_key_material(self, hook_kwargs, extra, error_message): + self.hook = OciBaseHook(**hook_kwargs) + self.set_connection(Connection(extra=extra)) + + with pytest.raises(ValueError, match=error_message): + self.hook.get_oci_config() + + @mock.patch("oci.config.from_file", autospec=True) + def test_get_oci_config_from_file_with_region_override(self, mock_from_file): + self.hook = OciBaseHook( + auth_type=OciAuthType.CONFIG_FILE, + config_file="/config/oci", + profile="AIRFLOW", + ) + mock_from_file.return_value = {"region": "us-ashburn-1"} + self.set_connection(Connection(extra={"region": "eu-frankfurt-1"})) + + config, signer = self.hook.get_oci_config() + + mock_from_file.assert_called_once_with( + file_location="/config/oci", + profile_name="AIRFLOW", + ) + assert config == {"region": "eu-frankfurt-1"} + assert signer is None + + @pytest.mark.parametrize( + ("config_file", "profile"), + [ + (None, None), + ("", ""), + ], + ) + @mock.patch("oci.config.from_file", autospec=True) + def test_get_oci_config_from_default_file(self, mock_from_file, config_file, profile): + self.hook = OciBaseHook( + auth_type="config_file", + config_file=config_file, + profile=profile, + ) + mock_from_file.return_value = {"region": "us-ashburn-1"} + self.set_missing_connection() + + config, signer = self.hook.get_oci_config() + + mock_from_file.assert_called_once_with( + file_location="~/.oci/config", + profile_name="DEFAULT", + ) + assert config == {"region": "us-ashburn-1"} + assert signer is None + + @mock.patch( + "oci.auth.signers.InstancePrincipalsSecurityTokenSigner", + autospec=True, + ) + def test_get_oci_config_with_instance_principal_and_connection_region(self, mock_signer_class): + self.hook = OciBaseHook(auth_type=OciAuthType.INSTANCE_PRINCIPAL) + signer = mock_signer_class.return_value + signer.region = "us-ashburn-1" + self.set_connection(Connection(extra={"region": "eu-frankfurt-1"})) + + config, actual_signer = self.hook.get_oci_config() + + assert config == {"region": "eu-frankfurt-1"} + assert actual_signer is signer + + @mock.patch( + "oci.auth.signers.get_resource_principals_signer", + autospec=True, + ) + def test_get_oci_config_with_resource_principal_region(self, mock_get_signer): + self.hook = OciBaseHook(auth_type=OciAuthType.RESOURCE_PRINCIPAL) + signer = mock_get_signer.return_value + signer.region = "us-phoenix-1" + self.set_missing_connection() + + config, actual_signer = self.hook.get_oci_config() + + assert config == {"region": "us-phoenix-1"} + assert actual_signer is signer + + @mock.patch( + "oci.auth.signers.get_resource_principals_signer", + autospec=True, + ) + def test_get_oci_config_with_resource_principal_without_region(self, mock_get_signer): + self.hook = OciBaseHook( + oci_conn_id=None, + auth_type=OciAuthType.RESOURCE_PRINCIPAL, + ) + self.hook.get_connection = mock.create_autospec(self.hook.get_connection) + signer = mock_get_signer.return_value + del signer.region + + config, actual_signer = self.hook.get_oci_config() + + assert config == {} + assert actual_signer is signer + self.hook.get_connection.assert_not_called() + + def test_get_oci_config_with_api_key_requires_connection_id(self): + self.hook = OciBaseHook(oci_conn_id=None) + + with pytest.raises(ValueError, match="An OCI connection ID is required"): + self.hook.get_oci_config() + + def test_get_oci_config_rejects_unknown_auth_type(self): + self.hook = OciBaseHook(auth_type="unknown") + self.set_connection(Connection()) + + with pytest.raises(ValueError, match="Unsupported OCI authentication type: 'unknown'"): + self.hook.get_oci_config() + + @pytest.mark.parametrize( + ("signer", "service_endpoint", "client_kwargs", "expected_client_kwargs"), + [ + ( + mock.sentinel.signer, + "https://generativeai.example.test/", + {"timeout": 30}, + { + "signer": mock.sentinel.signer, + "service_endpoint": "https://generativeai.example.test", + "timeout": 30, + }, + ), + (None, None, {}, {}), + ], + ids=["signer-and-explicit-endpoint", "no-signer-or-endpoint"], + ) + def test_get_client(self, signer, service_endpoint, client_kwargs, expected_client_kwargs): + config = {"region": "us-chicago-1"} + client = mock.sentinel.client + client_class = mock.create_autospec(GenerativeAiClient, return_value=client) + self.hook.get_oci_config = mock.create_autospec( + self.hook.get_oci_config, return_value=(config, signer) + ) + self.hook.service_endpoint = service_endpoint + + result = self.hook.get_client(client_class, **client_kwargs) + + assert result is client + client_class.assert_called_once_with(config=config, **expected_client_kwargs) + + def test_get_conn_requires_service_client_class(self): + with pytest.raises(NotImplementedError, match="OCI service hooks must implement _get_client_class"): + self.hook.get_conn() + + @pytest.mark.parametrize("signer", [None, mock.sentinel.signer]) + @mock.patch("oci.identity.IdentityClient", autospec=True) + def test_connection_success(self, mock_identity_client, signer): + config = {"region": "us-chicago-1"} + self.hook.get_oci_config = mock.create_autospec( + self.hook.get_oci_config, return_value=(config, signer) + ) + + result = self.hook.test_connection() + + assert result == (True, "Connection successfully tested") + expected_kwargs = {"config": config} + if signer is not None: + expected_kwargs["signer"] = signer + mock_identity_client.assert_called_once_with(**expected_kwargs) + mock_identity_client.return_value.list_regions.assert_called_once_with() + + @mock.patch("oci.identity.IdentityClient", autospec=True) + def test_connection_failure(self, mock_identity_client): + self.hook.get_oci_config = mock.create_autospec( + self.hook.get_oci_config, return_value=({"region": "us-chicago-1"}, None) + ) + mock_identity_client.return_value.list_regions.side_effect = ValueError("invalid credentials") + + result = self.hook.test_connection() + + assert result == (False, "ValueError error occurred while testing connection: invalid credentials") + + @pytest.mark.parametrize( + ("hook_endpoint", "expected"), + [ + ("https://hook.test/", "https://hook.test"), + (None, None), + ], + ) + def test_service_endpoint_is_controlled_by_hook_argument(self, hook_endpoint, expected): + self.hook = OciBaseHook(service_endpoint=hook_endpoint) + self.set_connection( + Connection( + host="https://connection-host.test", + extra={"service_endpoint": "https://connection-extra.test"}, + ) + ) + + assert self.hook._get_service_endpoint() == expected + + @pytest.mark.parametrize( + ("compartment_id", "expected"), + [ + ("explicit-compartment", "explicit-compartment"), + (None, "connection-compartment"), + ], + ids=["explicit", "connection"], + ) + def test_get_compartment_id(self, compartment_id, expected): + self.set_connection(Connection(extra={"compartment_id": "connection-compartment"})) + + assert self.hook.get_compartment_id(compartment_id) == expected + + def test_get_compartment_id_requires_value(self): + self.set_connection(Connection()) + + with pytest.raises(ValueError, match="An OCI compartment OCID must be provided"): + self.hook.get_compartment_id() + + def test_get_compartment_id_requires_value_without_connection(self): + self.hook = OciBaseHook(oci_conn_id=None, auth_type=OciAuthType.INSTANCE_PRINCIPAL) + self.hook.get_connection = mock.create_autospec(self.hook.get_connection) + + with pytest.raises(ValueError, match="An OCI compartment OCID must be provided"): + self.hook.get_compartment_id() + + self.hook.get_connection.assert_not_called() + + def test_connection_form_widgets(self): + pytest.importorskip("flask_appbuilder") + pytest.importorskip("flask_babel") + password_field = pytest.importorskip("wtforms").PasswordField + + widgets = self.hook.get_connection_form_widgets() + + assert set(widgets) == { + "tenancy", + "fingerprint", + "private_key_content", + "region", + "compartment_id", + } + assert widgets["private_key_content"].field_class is password_field + + def test_ui_field_behaviour(self): + assert self.hook.get_ui_field_behaviour() == { + "hidden_fields": ["host", "schema", "port"], + "relabeling": { + "login": "User OCID", + "password": "Private Key Passphrase", + }, + "placeholders": { + "login": "ocid1.user...", + "password": "Optional API key passphrase", + "tenancy": "ocid1.tenancy...", + "fingerprint": "aa:bb:cc:...", + "region": "us-chicago-1", + "compartment_id": "ocid1.compartment...", + }, + } + + def test_declarative_placeholders_match_legacy_hook(self): + oci_connection = next( + connection + for connection in get_provider_info()["connection-types"] + if connection["connection-type"] == "oci" + ) + + assert ( + oci_connection["ui-field-behaviour"]["placeholders"] + == self.hook.get_ui_field_behaviour()["placeholders"] + ) + assert oci_connection["conn-fields"]["private_key_content"]["schema"]["format"] == "password" diff --git a/providers/oci/tests/unit/oci/hooks/test_generative_ai.py b/providers/oci/tests/unit/oci/hooks/test_generative_ai.py new file mode 100644 index 0000000000000..72467c14328a1 --- /dev/null +++ b/providers/oci/tests/unit/oci/hooks/test_generative_ai.py @@ -0,0 +1,36 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +from unittest import mock + +import pytest + +from airflow.providers.oci.hooks.generative_ai import OciGenerativeAIHook + +GenerativeAiClient = pytest.importorskip("oci.generative_ai").GenerativeAiClient + + +class TestOciGenerativeAIHook: + @mock.patch.object(OciGenerativeAIHook, "get_client", autospec=True) + def test_get_conn_creates_and_caches_native_client(self, mock_get_client): + hook = OciGenerativeAIHook() + client = mock_get_client.return_value + + assert hook.get_conn() is client + assert hook.conn is client + mock_get_client.assert_called_once_with(hook, GenerativeAiClient) diff --git a/providers/oci/tests/unit/oci/hooks/test_optional_dependency.py b/providers/oci/tests/unit/oci/hooks/test_optional_dependency.py new file mode 100644 index 0000000000000..b72384ec4c9cf --- /dev/null +++ b/providers/oci/tests/unit/oci/hooks/test_optional_dependency.py @@ -0,0 +1,91 @@ +# 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 os +import subprocess +import sys +from textwrap import dedent +from unittest import mock + +import pytest + +from airflow.providers.common.compat.sdk import AirflowOptionalProviderFeatureException +from airflow.providers.oci.hooks.base import _get_oci_sdk + + +def test_get_oci_sdk_requires_optional_extra(): + with mock.patch.dict(sys.modules, {"oci": None}): + with pytest.raises( + AirflowOptionalProviderFeatureException, + match=r"pip install 'apache-airflow-providers-oci\[oci\]'", + ): + _get_oci_sdk() + + +def test_hook_modules_import_without_optional_oci_sdk(): + subprocess.run( + [ + sys.executable, + "-c", + dedent( + """ + import sys + + sys.modules["oci"] = None + import airflow.providers.oci.hooks.base + import airflow.providers.oci.hooks.generative_ai + """ + ), + ], + check=True, + ) + + +def test_hooks_support_selective_oci_service_imports(): + pytest.importorskip("oci") + subprocess.run( + [ + sys.executable, + "-c", + dedent( + """ + import oci + + assert not hasattr(oci, "generative_ai") + assert not hasattr(oci, "identity") + + from airflow.providers.oci.hooks.base import OciBaseHook + from airflow.providers.oci.hooks.generative_ai import OciGenerativeAIHook + + assert OciGenerativeAIHook()._get_client_class().__module__.startswith( + "oci.generative_ai" + ) + + hook = OciBaseHook() + hook.get_oci_config = lambda: ({}, None) + success, message = hook.test_connection() + + assert not success + assert "AttributeError" not in message + assert hasattr(oci, "identity") + """ + ), + ], + check=True, + env={**os.environ, "OCI_PYTHON_SDK_NO_SERVICE_IMPORTS": "True"}, + ) diff --git a/pyproject.toml b/pyproject.toml index 3a1b9d360f3d8..eed152f1f8158 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1218,6 +1218,8 @@ mypy_path = [ "$MYPY_CONFIG_FILE_DIR/providers/mysql/tests", "$MYPY_CONFIG_FILE_DIR/providers/neo4j/src", "$MYPY_CONFIG_FILE_DIR/providers/neo4j/tests", + "$MYPY_CONFIG_FILE_DIR/providers/oci/src", + "$MYPY_CONFIG_FILE_DIR/providers/oci/tests", "$MYPY_CONFIG_FILE_DIR/providers/odbc/src", "$MYPY_CONFIG_FILE_DIR/providers/odbc/tests", "$MYPY_CONFIG_FILE_DIR/providers/openai/src", @@ -1496,6 +1498,7 @@ apache-airflow-providers-microsoft-winrm = false apache-airflow-providers-mongo = false apache-airflow-providers-mysql = false apache-airflow-providers-neo4j = false +apache-airflow-providers-oci = false apache-airflow-providers-odbc = false apache-airflow-providers-openai = false apache-airflow-providers-openfaas = false @@ -1648,6 +1651,7 @@ apache-airflow-providers-microsoft-winrm = false apache-airflow-providers-mongo = false apache-airflow-providers-mysql = false apache-airflow-providers-neo4j = false +apache-airflow-providers-oci = false apache-airflow-providers-odbc = false apache-airflow-providers-openai = false apache-airflow-providers-openfaas = false @@ -1811,6 +1815,7 @@ apache-airflow-providers-microsoft-winrm = { workspace = true } apache-airflow-providers-mongo = { workspace = true } apache-airflow-providers-mysql = { workspace = true } apache-airflow-providers-neo4j = { workspace = true } +apache-airflow-providers-oci = { workspace = true } apache-airflow-providers-odbc = { workspace = true } apache-airflow-providers-openai = { workspace = true } apache-airflow-providers-openfaas = { workspace = true } @@ -1952,6 +1957,7 @@ members = [ "providers/mongo", "providers/mysql", "providers/neo4j", + "providers/oci", "providers/odbc", "providers/openai", "providers/openfaas", diff --git a/scripts/ci/docker-compose/remove-sources.yml b/scripts/ci/docker-compose/remove-sources.yml index edb644a179a19..50c874f5828e6 100644 --- a/scripts/ci/docker-compose/remove-sources.yml +++ b/scripts/ci/docker-compose/remove-sources.yml @@ -94,6 +94,7 @@ services: - ../../../empty:/opt/airflow/providers/mongo/src - ../../../empty:/opt/airflow/providers/mysql/src - ../../../empty:/opt/airflow/providers/neo4j/src + - ../../../empty:/opt/airflow/providers/oci/src - ../../../empty:/opt/airflow/providers/odbc/src - ../../../empty:/opt/airflow/providers/openai/src - ../../../empty:/opt/airflow/providers/openfaas/src diff --git a/scripts/ci/docker-compose/tests-sources.yml b/scripts/ci/docker-compose/tests-sources.yml index eb700548346da..1ecdfd5affbf7 100644 --- a/scripts/ci/docker-compose/tests-sources.yml +++ b/scripts/ci/docker-compose/tests-sources.yml @@ -107,6 +107,7 @@ services: - ../../../providers/mongo/tests:/opt/airflow/providers/mongo/tests - ../../../providers/mysql/tests:/opt/airflow/providers/mysql/tests - ../../../providers/neo4j/tests:/opt/airflow/providers/neo4j/tests + - ../../../providers/oci/tests:/opt/airflow/providers/oci/tests - ../../../providers/odbc/tests:/opt/airflow/providers/odbc/tests - ../../../providers/openai/tests:/opt/airflow/providers/openai/tests - ../../../providers/openfaas/tests:/opt/airflow/providers/openfaas/tests diff --git a/scripts/ci/prek/update_airflow_pyproject_toml.py b/scripts/ci/prek/update_airflow_pyproject_toml.py index f66a3d7ba5d49..d5453a309118a 100755 --- a/scripts/ci/prek/update_airflow_pyproject_toml.py +++ b/scripts/ci/prek/update_airflow_pyproject_toml.py @@ -98,6 +98,13 @@ "opensearch": parse_version("1.9.3"), } +# These same-named apache-airflow extras must activate an extra on the provider distribution. +# Keep them out of ``all_provider_lines`` so ``apache-airflow[all]`` does not install Category X +# dependencies that users have not explicitly selected. +PROVIDER_EXTRAS_FOR_AIRFLOW_EXTRAS: dict[str, tuple[str, ...]] = { + "oci": ("oci",), +} + def get_optional_dependencies(pyproject_toml_path: Path) -> list[str]: try: @@ -284,6 +291,10 @@ def get_exclusion_marker(provider_dependencies: dict[str, Any]) -> str: all_provider_lines = [] for provider_id in released_providers: distribution_name = provider_distribution_name(provider_id) + provider_extras = PROVIDER_EXTRAS_FOR_AIRFLOW_EXTRAS.get(provider_id, ()) + airflow_extra_distribution_name = distribution_name + if provider_extras: + airflow_extra_distribution_name += f"[{','.join(provider_extras)}]" min_provider_version, comment = find_min_provider_version(provider_id) exclusion_marker = get_exclusion_marker(all_providers_dependencies.get(provider_id, {})) @@ -292,10 +303,13 @@ def get_exclusion_marker(provider_dependencies: dict[str, Any]) -> str: f' "{distribution_name}>={min_provider_version}{exclusion_marker}",{comment}\n' ) all_optional_dependencies.append( - f'"{provider_id}" = [\n "{distribution_name}>={min_provider_version}{exclusion_marker}"{comment}\n]\n' + f'"{provider_id}" = [\n "{airflow_extra_distribution_name}>={min_provider_version}' + f'{exclusion_marker}"{comment}\n]\n' ) else: - all_optional_dependencies.append(f'"{provider_id}" = [\n "{distribution_name}"\n]\n') + all_optional_dependencies.append( + f'"{provider_id}" = [\n "{airflow_extra_distribution_name}"\n]\n' + ) all_provider_lines.append(f' "{distribution_name}",\n') all_optional_dependencies.append('"all" = [\n') optional_apache_airflow_dependencies = get_optional_dependencies(AIRFLOW_PYPROJECT_TOML_FILE) diff --git a/uv.lock b/uv.lock index b66ad967fd43c..eff845cdbdf01 100644 --- a/uv.lock +++ b/uv.lock @@ -64,9 +64,9 @@ apache-airflow-providers-apache-cassandra = false apache-airflow-providers-asana = false apache-airflow-providers-oracle = false apache-airflow-providers-mysql = false +apache-airflow-providers-teradata = false apache-airflow-providers-alibaba = false apache-airflow-providers-microsoft-mssql = false -apache-airflow-providers-teradata = false apache-airflow-providers-jdbc = false apache-airflow-helm-chart = false apache-airflow-providers-anthropic = false @@ -138,6 +138,7 @@ apache-airflow-providers-singularity = false apache-airflow-providers-common-compat = false apache-airflow-ctl-tests = false apache-airflow-providers-tableau = false +apache-airflow-providers-oci = false apache-airflow-providers-common-sql = false apache-airflow-shared-configuration = false apache-airflow-providers-facebook = false @@ -241,6 +242,7 @@ members = [ "apache-airflow-providers-mongo", "apache-airflow-providers-mysql", "apache-airflow-providers-neo4j", + "apache-airflow-providers-oci", "apache-airflow-providers-odbc", "apache-airflow-providers-openai", "apache-airflow-providers-openfaas", @@ -6684,6 +6686,50 @@ dev = [ ] docs = [{ name = "apache-airflow-devel-common", extras = ["docs"], editable = "devel-common" }] +[[package]] +name = "apache-airflow-providers-oci" +version = "0.1.0" +source = { editable = "providers/oci" } +dependencies = [ + { name = "apache-airflow" }, + { name = "apache-airflow-providers-common-compat" }, +] + +[package.optional-dependencies] +oci = [ + { name = "oci" }, +] + +[package.dev-dependencies] +dev = [ + { name = "apache-airflow" }, + { name = "apache-airflow-devel-common" }, + { name = "apache-airflow-providers-common-compat" }, + { name = "apache-airflow-providers-oci", extra = ["oci"] }, + { name = "apache-airflow-task-sdk" }, +] +docs = [ + { name = "apache-airflow-devel-common", extra = ["docs"] }, +] + +[package.metadata] +requires-dist = [ + { name = "apache-airflow", editable = "." }, + { name = "apache-airflow-providers-common-compat", editable = "providers/common/compat" }, + { name = "oci", marker = "extra == 'oci'", specifier = ">=2.182.0" }, +] +provides-extras = ["oci"] + +[package.metadata.requires-dev] +dev = [ + { name = "apache-airflow", editable = "." }, + { name = "apache-airflow-devel-common", editable = "devel-common" }, + { name = "apache-airflow-providers-common-compat", editable = "providers/common/compat" }, + { name = "apache-airflow-providers-oci", extras = ["oci"], editable = "providers/oci" }, + { name = "apache-airflow-task-sdk", editable = "task-sdk" }, +] +docs = [{ name = "apache-airflow-devel-common", extras = ["docs"], editable = "devel-common" }] + [[package]] name = "apache-airflow-providers-odbc" version = "4.12.3" @@ -10775,6 +10821,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, ] +[[package]] +name = "circuitbreaker" +version = "2.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/ac/de7a92c4ed39cba31fe5ad9203b76a25ca67c530797f6bb420fff5f65ccb/circuitbreaker-2.1.3.tar.gz", hash = "sha256:1a4baee510f7bea3c91b194dcce7c07805fe96c4423ed5594b75af438531d084", size = 10787, upload-time = "2025-03-31T08:12:08.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/34/15f08edd4628f65217de1fc3c1a27c82e46fe357d60c217fc9881e12ebcc/circuitbreaker-2.1.3-py3-none-any.whl", hash = "sha256:87ba6a3ed03fdc7032bc175561c2b04d52ade9d5faf94ca2b035fbdc5e6b1dd1", size = 7737, upload-time = "2025-03-31T08:12:07.802Z" }, +] + [[package]] name = "ciso8601" version = "2.3.3" @@ -11159,6 +11214,82 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] +[[package]] +name = "crc32c" +version = "2.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/66/7e97aa77af7cf6afbff26e3651b564fe41932599bc2d3dce0b2f73d4829a/crc32c-2.8.tar.gz", hash = "sha256:578728964e59c47c356aeeedee6220e021e124b9d3e8631d95d9a5e5f06e261c", size = 48179, upload-time = "2025-10-17T06:20:13.61Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/a0/28b4686a8db0bb0f77970f4c6ccede90d1d5740a1d4b4703bd54c3e75655/crc32c-2.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2c0f4eb01fe7c0a3e3f973a418e04d52101bb077dd77626fd80c658ec60aaf95", size = 66321, upload-time = "2025-10-17T06:18:53.543Z" }, + { url = "https://files.pythonhosted.org/packages/76/1f/1697f5b8b770f715ed9b264d79e36b4f77ae0527f81f3c749ef08937a32e/crc32c-2.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6baefcfbca82b1a9678455416da24f18629769a76920c640d5a538620a7d12bb", size = 62985, upload-time = "2025-10-17T06:18:54.97Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e5/333cfa5ffa8d5779733aced2b984b5e5139b4a8ceaa2c6bc563e9a1092f3/crc32c-2.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d7f959fcf6c5aad1c4a653ee1a50f05760dab1d1c35d98ec4d7f0f68643f7612", size = 61517, upload-time = "2025-10-17T06:18:55.795Z" }, + { url = "https://files.pythonhosted.org/packages/e1/d8/362a009e8140dd926a153b44d56753e3aa7cb50aca243779a84adadbff11/crc32c-2.8-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9bb678507a4e4cf3f0506607b046ecc4ed1c58a19e08a3fb3c2d25441c480bf1", size = 79385, upload-time = "2025-10-17T06:18:56.598Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0d4ea3aa71ffb15f1285669d23024cc40779388ce32157d339dc2584491c/crc32c-2.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a16f7ffa4c242a909558565567cbba95148603717b53538ea299c98da68e7a9", size = 80965, upload-time = "2025-10-17T06:18:57.384Z" }, + { url = "https://files.pythonhosted.org/packages/20/44/d77657aaca4a2c0283f2356a3da6f8e91b003567bb8f09daaf540cbf192f/crc32c-2.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0184369aad562d801f91f454c81f56b9ecb966f6b96684c4d6cf82fc8741d2ad", size = 79993, upload-time = "2025-10-17T06:18:58.503Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c0/07017a93ebf85d9408028b7e03ef96d5c6bfb14cb77cfe90d35eedcc1501/crc32c-2.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:86d2eeb5f0189bd803720abe7387019328ea34c4acde62999e5723f789bc316b", size = 79243, upload-time = "2025-10-17T06:18:59.273Z" }, + { url = "https://files.pythonhosted.org/packages/c7/1a/b3c5ac4cf2fd1f82395173d0bd8e1a15d09f0bc1eccdf10ea7f8caaccd67/crc32c-2.8-cp310-cp310-win32.whl", hash = "sha256:51da61904a9e753780a2e6011885677d601db1fa840be4b68799643a113e6f08", size = 64888, upload-time = "2025-10-17T06:19:00.089Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f2/60c45fc7bb2221d3c93c7a872e921be591f40d45228fe46f879b1d8c0424/crc32c-2.8-cp310-cp310-win_amd64.whl", hash = "sha256:b2d6a1f2500daaf2e4b08f97ad0349aa2eff5faaaa5fd3350314a26eade334cd", size = 66639, upload-time = "2025-10-17T06:19:00.974Z" }, + { url = "https://files.pythonhosted.org/packages/dc/0b/5e03b22d913698e9cc563f39b9f6bbd508606bf6b8e9122cd6bf196b87ea/crc32c-2.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e560a97fbb96c9897cb1d9b5076ef12fc12e2e25622530a1afd0de4240f17e1f", size = 66329, upload-time = "2025-10-17T06:19:01.771Z" }, + { url = "https://files.pythonhosted.org/packages/6b/38/2fe0051ffe8c6a650c8b1ac0da31b8802d1dbe5fa40a84e4b6b6f5583db5/crc32c-2.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6762d276d90331a490ef7e71ffee53b9c0eb053bd75a272d786f3b08d3fe3671", size = 62988, upload-time = "2025-10-17T06:19:02.953Z" }, + { url = "https://files.pythonhosted.org/packages/3e/30/5837a71c014be83aba1469c58820d287fc836512a0cad6b8fdd43868accd/crc32c-2.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:60670569f5ede91e39f48fb0cb4060e05b8d8704dd9e17ede930bf441b2f73ef", size = 61522, upload-time = "2025-10-17T06:19:03.796Z" }, + { url = "https://files.pythonhosted.org/packages/ca/29/63972fc1452778e2092ae998c50cbfc2fc93e3fa9798a0278650cd6169c5/crc32c-2.8-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:711743da6ccc70b3c6718c328947b0b6f34a1fe6a6c27cc6c1d69cc226bf70e9", size = 80200, upload-time = "2025-10-17T06:19:04.617Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3a/60eb49d7bdada4122b3ffd45b0df54bdc1b8dd092cda4b069a287bdfcff4/crc32c-2.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5eb4094a2054774f13b26f21bf56792bb44fa1fcee6c6ad099387a43ffbfb4fa", size = 81757, upload-time = "2025-10-17T06:19:05.496Z" }, + { url = "https://files.pythonhosted.org/packages/f5/63/6efc1b64429ef7d23bd58b75b7ac24d15df327e3ebbe9c247a0f7b1c2ed1/crc32c-2.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fff15bf2bd3e95780516baae935ed12be88deaa5ebe6143c53eb0d26a7bdc7b7", size = 80830, upload-time = "2025-10-17T06:19:06.621Z" }, + { url = "https://files.pythonhosted.org/packages/e1/eb/0ae9f436f8004f1c88f7429e659a7218a3879bd11a6b18ed1257aad7e98b/crc32c-2.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4c0e11e3826668121fa53e0745635baf5e4f0ded437e8ff63ea56f38fc4f970a", size = 80095, upload-time = "2025-10-17T06:19:07.381Z" }, + { url = "https://files.pythonhosted.org/packages/9e/81/4afc9d468977a4cd94a2eb62908553345009a7c0d30e74463a15d4b48ec3/crc32c-2.8-cp311-cp311-win32.whl", hash = "sha256:38f915336715d1f1353ab07d7d786f8a789b119e273aea106ba55355dfc9101d", size = 64886, upload-time = "2025-10-17T06:19:08.497Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e8/94e839c9f7e767bf8479046a207afd440a08f5c59b52586e1af5e64fa4a0/crc32c-2.8-cp311-cp311-win_amd64.whl", hash = "sha256:60e0a765b1caab8d31b2ea80840639253906a9351d4b861551c8c8625ea20f86", size = 66639, upload-time = "2025-10-17T06:19:09.338Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/fd18ef23c42926b79c7003e16cb0f79043b5b179c633521343d3b499e996/crc32c-2.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:572ffb1b78cce3d88e8d4143e154d31044a44be42cb3f6fbbf77f1e7a941c5ab", size = 66379, upload-time = "2025-10-17T06:19:10.115Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b8/c584958e53f7798dd358f5bdb1bbfc97483134f053ee399d3eeb26cca075/crc32c-2.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cf827b3758ee0c4aacd21ceca0e2da83681f10295c38a10bfeb105f7d98f7a68", size = 63042, upload-time = "2025-10-17T06:19:10.946Z" }, + { url = "https://files.pythonhosted.org/packages/62/e6/6f2af0ec64a668a46c861e5bc778ea3ee42171fedfc5440f791f470fd783/crc32c-2.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:106fbd79013e06fa92bc3b51031694fcc1249811ed4364ef1554ee3dd2c7f5a2", size = 61528, upload-time = "2025-10-17T06:19:11.768Z" }, + { url = "https://files.pythonhosted.org/packages/17/8b/4a04bd80a024f1a23978f19ae99407783e06549e361ab56e9c08bba3c1d3/crc32c-2.8-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6dde035f91ffbfe23163e68605ee5a4bb8ceebd71ed54bb1fb1d0526cdd125a2", size = 80028, upload-time = "2025-10-17T06:19:12.554Z" }, + { url = "https://files.pythonhosted.org/packages/21/8f/01c7afdc76ac2007d0e6a98e7300b4470b170480f8188475b597d1f4b4c6/crc32c-2.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e41ebe7c2f0fdcd9f3a3fd206989a36b460b4d3f24816d53e5be6c7dba72c5e1", size = 81531, upload-time = "2025-10-17T06:19:13.406Z" }, + { url = "https://files.pythonhosted.org/packages/32/2b/8f78c5a8cc66486be5f51b6f038fc347c3ba748d3ea68be17a014283c331/crc32c-2.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ecf66cf90266d9c15cea597d5cc86c01917cd1a238dc3c51420c7886fa750d7e", size = 80608, upload-time = "2025-10-17T06:19:14.223Z" }, + { url = "https://files.pythonhosted.org/packages/db/86/fad1a94cdeeeb6b6e2323c87f970186e74bfd6fbfbc247bf5c88ad0873d5/crc32c-2.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:59eee5f3a69ad0793d5fa9cdc9b9d743b0cd50edf7fccc0a3988a821fef0208c", size = 79886, upload-time = "2025-10-17T06:19:15.345Z" }, + { url = "https://files.pythonhosted.org/packages/d5/db/1a7cb6757a1e32376fa2dfce00c815ea4ee614a94f9bff8228e37420c183/crc32c-2.8-cp312-cp312-win32.whl", hash = "sha256:a73d03ce3604aa5d7a2698e9057a0eef69f529c46497b27ee1c38158e90ceb76", size = 64896, upload-time = "2025-10-17T06:19:16.457Z" }, + { url = "https://files.pythonhosted.org/packages/bf/8e/2024de34399b2e401a37dcb54b224b56c747b0dc46de4966886827b4d370/crc32c-2.8-cp312-cp312-win_amd64.whl", hash = "sha256:56b3b7d015247962cf58186e06d18c3d75a1a63d709d3233509e1c50a2d36aa2", size = 66645, upload-time = "2025-10-17T06:19:17.235Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d8/3ae227890b3be40955a7144106ef4dd97d6123a82c2a5310cdab58ca49d8/crc32c-2.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:36f1e03ee9e9c6938e67d3bcb60e36f260170aa5f37da1185e04ef37b56af395", size = 66380, upload-time = "2025-10-17T06:19:18.009Z" }, + { url = "https://files.pythonhosted.org/packages/bd/8b/178d3f987cd0e049b484615512d3f91f3d2caeeb8ff336bb5896ae317438/crc32c-2.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b2f3226b94b85a8dd9b3533601d7a63e9e3e8edf03a8a169830ee8303a199aeb", size = 63048, upload-time = "2025-10-17T06:19:18.853Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a1/48145ae2545ebc0169d3283ebe882da580ea4606bfb67cf4ca922ac3cfc3/crc32c-2.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6e08628bc72d5b6bc8e0730e8f142194b610e780a98c58cb6698e665cb885a5b", size = 61530, upload-time = "2025-10-17T06:19:19.974Z" }, + { url = "https://files.pythonhosted.org/packages/06/4b/cf05ed9d934cc30e5ae22f97c8272face420a476090e736615d9a6b53de0/crc32c-2.8-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:086f64793c5ec856d1ab31a026d52ad2b895ac83d7a38fce557d74eb857f0a82", size = 80001, upload-time = "2025-10-17T06:19:20.784Z" }, + { url = "https://files.pythonhosted.org/packages/15/ab/4b04801739faf36345f6ba1920be5b1c70282fec52f8280afd3613fb13e2/crc32c-2.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcf72ee7e0135b3d941c34bb2c26c3fc6bc207106b49fd89aaafaeae223ae209", size = 81543, upload-time = "2025-10-17T06:19:21.557Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1b/6e38dde5bfd2ea69b7f2ab6ec229fcd972a53d39e2db4efe75c0ac0382ce/crc32c-2.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a717dd9c3fd777d9bc6603717eae172887d402c4ab589d124ebd0184a83f89e", size = 80644, upload-time = "2025-10-17T06:19:22.325Z" }, + { url = "https://files.pythonhosted.org/packages/ce/45/012176ffee90059ae8ec7131019c71724ea472aa63e72c0c8edbd1fad1d7/crc32c-2.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0450bb845b3c3c7b9bdc0b4e95620ec9a40824abdc8c86d6285c919a90743c1a", size = 79919, upload-time = "2025-10-17T06:19:23.101Z" }, + { url = "https://files.pythonhosted.org/packages/f0/2b/f557629842f9dec2b3461cb3a0d854bb586ec45b814cea58b082c32f0dde/crc32c-2.8-cp313-cp313-win32.whl", hash = "sha256:765d220bfcbcffa6598ac11eb1e10af0ee4802b49fe126aa6bf79f8ddb9931d1", size = 64896, upload-time = "2025-10-17T06:19:23.88Z" }, + { url = "https://files.pythonhosted.org/packages/d0/db/fd0f698c15d1e21d47c64181a98290665a08fcbb3940cd559e9c15bda57e/crc32c-2.8-cp313-cp313-win_amd64.whl", hash = "sha256:171ff0260d112c62abcce29332986950a57bddee514e0a2418bfde493ea06bb3", size = 66646, upload-time = "2025-10-17T06:19:24.702Z" }, + { url = "https://files.pythonhosted.org/packages/db/b9/8e5d7054fe8e7eecab10fd0c8e7ffb01439417bdb6de1d66a81c38fc4a20/crc32c-2.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b977a32a3708d6f51703c8557008f190aaa434d7347431efb0e86fcbe78c2a50", size = 66203, upload-time = "2025-10-17T06:19:25.872Z" }, + { url = "https://files.pythonhosted.org/packages/55/5f/cc926c70057a63cc0c98a3c8a896eb15fc7e74d3034eadd53c94917c6cc3/crc32c-2.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7399b01db4adaf41da2fb36fe2408e75a8d82a179a9564ed7619412e427b26d6", size = 62956, upload-time = "2025-10-17T06:19:26.652Z" }, + { url = "https://files.pythonhosted.org/packages/a1/8a/0660c44a2dd2cb6ccbb529eb363b9280f5c766f1017bc8355ed8d695bd94/crc32c-2.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4379f73f9cdad31958a673d11a332ec725ca71572401ca865867229f5f15e853", size = 61442, upload-time = "2025-10-17T06:19:27.74Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5a/6108d2dfc0fe33522ce83ba07aed4b22014911b387afa228808a278e27cd/crc32c-2.8-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2e68264555fab19bab08331550dab58573e351a63ed79c869d455edd3b0aa417", size = 79109, upload-time = "2025-10-17T06:19:28.535Z" }, + { url = "https://files.pythonhosted.org/packages/84/1e/c054f9e390090c197abf3d2936f4f9effaf0c6ee14569ae03d6ddf86958a/crc32c-2.8-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b48f2486727b8d0e7ccbae4a34cb0300498433d2a9d6b49cb13cb57c2e3f19cb", size = 80987, upload-time = "2025-10-17T06:19:29.305Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ad/1650e5c3341e4a485f800ea83116d72965030c5d48ccc168fcc685756e4d/crc32c-2.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ecf123348934a086df8c8fde7f9f2d716d523ca0707c5a1367b8bb00d8134823", size = 79994, upload-time = "2025-10-17T06:19:30.109Z" }, + { url = "https://files.pythonhosted.org/packages/d7/3b/f2ed924b177729cbb2ab30ca2902abff653c31d48c95e7b66717a9ca9fcc/crc32c-2.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e636ac60f76de538f7a2c0d0f3abf43104ee83a8f5e516f6345dc283ed1a4df7", size = 79046, upload-time = "2025-10-17T06:19:30.894Z" }, + { url = "https://files.pythonhosted.org/packages/4b/80/413b05ee6ace613208b31b3670c3135ee1cf451f0e72a9c839b4946acc04/crc32c-2.8-cp313-cp313t-win32.whl", hash = "sha256:8dd4a19505e0253892e1b2f1425cc3bd47f79ae5a04cb8800315d00aad7197f2", size = 64837, upload-time = "2025-10-17T06:19:32.03Z" }, + { url = "https://files.pythonhosted.org/packages/3b/1b/85eddb6ac5b38496c4e35c20298aae627970c88c3c624a22ab33e84f16c7/crc32c-2.8-cp313-cp313t-win_amd64.whl", hash = "sha256:4bb18e4bd98fb266596523ffc6be9c5b2387b2fa4e505ec56ca36336f49cb639", size = 66574, upload-time = "2025-10-17T06:19:33.143Z" }, + { url = "https://files.pythonhosted.org/packages/aa/df/50e9079b532ff53dbfc0e66eed781374bd455af02ed5df8b56ad538de4ff/crc32c-2.8-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3a3b2e4bcf7b3ee333050e7d3ff38e2ba46ea205f1d73d8949b248aaffe937ac", size = 66399, upload-time = "2025-10-17T06:19:34.279Z" }, + { url = "https://files.pythonhosted.org/packages/5a/2e/67e3b0bc3d30e46ea5d16365cc81203286387671e22f2307eb41f19abb9c/crc32c-2.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:445e559e66dff16be54f8a4ef95aa6b01db799a639956d995c5498ba513fccc2", size = 63044, upload-time = "2025-10-17T06:19:35.062Z" }, + { url = "https://files.pythonhosted.org/packages/36/ea/1723b17437e4344ed8d067456382ecb1f5b535d83fdc5aaebab676c6d273/crc32c-2.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bf3040919e17afa5782e01b1875d6a05f44b8f19c05f211d8b9f8a1deb8bbd9c", size = 61541, upload-time = "2025-10-17T06:19:36.204Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6a/cbec8a235c5b46a01f319939b538958662159aec0ed3a74944e3a6de21f1/crc32c-2.8-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5607ab8221e1ffd411f64aa40dbb6850cf06dd2908c9debd05d371e1acf62ff3", size = 80139, upload-time = "2025-10-17T06:19:37.351Z" }, + { url = "https://files.pythonhosted.org/packages/21/31/d096722fe74b692d6e8206c27da1ea5f6b2a12ff92c54a62a6ba2f376254/crc32c-2.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f5db4f16816926986d3c94253314920689706ae13a9bf4888b47336c6735ce", size = 81736, upload-time = "2025-10-17T06:19:38.16Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a2/f75ef716ff7e3c22f385ba6ef30c5de80c19a21ebe699dc90824a1903275/crc32c-2.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:70b0153c4d418b673309d3529334d117e1074c4a3b2d7f676e430d72c14de67b", size = 80795, upload-time = "2025-10-17T06:19:38.948Z" }, + { url = "https://files.pythonhosted.org/packages/d8/94/6d647a12d96ab087d9b8eacee3da073f981987827d57c7072f89ffc7b6cd/crc32c-2.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c8933531442042438753755a5c8a9034e4d88b01da9eb796f7e151b31a7256c", size = 80042, upload-time = "2025-10-17T06:19:39.725Z" }, + { url = "https://files.pythonhosted.org/packages/cd/dc/32b8896b40a0afee7a3c040536d0da5a73e68df2be9fadd21770fd158e16/crc32c-2.8-cp314-cp314-win32.whl", hash = "sha256:cdc83a3fe6c4e5df9457294cfd643de7d95bd4e9382c1dd6ed1e0f0f9169172c", size = 64914, upload-time = "2025-10-17T06:19:40.527Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b4/4308b27d307e8ecaf8dd1dcc63bbb0e47ae1826d93faa3e62d1ee00ee2d5/crc32c-2.8-cp314-cp314-win_amd64.whl", hash = "sha256:509e10035106df66770fe24b9eb8d9e32b6fb967df17744402fb67772d8b2bc7", size = 66723, upload-time = "2025-10-17T06:19:42.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/d5/a19d2489fa997a143bfbbf971a5c9a43f8b1ba9e775b1fb362d8fb15260c/crc32c-2.8-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:864359a39777a07b09b28eb31337c0cc603d5c1bf0fc328c3af736a8da624ec0", size = 66201, upload-time = "2025-10-17T06:19:43.273Z" }, + { url = "https://files.pythonhosted.org/packages/98/c2/5f82f22d2c1242cb6f6fe92aa9a42991ebea86de994b8f9974d9c1d128e2/crc32c-2.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:14511d7cfc5d9f5e1a6c6b64caa6225c2bdc1ed00d725e9a374a3e84073ce180", size = 62956, upload-time = "2025-10-17T06:19:44.099Z" }, + { url = "https://files.pythonhosted.org/packages/9b/61/3d43d33489cf974fb78bfb3500845770e139ae6d1d83473b660bd8f79a6c/crc32c-2.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:918b7999b52b5dcbcea34081e9a02d46917d571921a3f209956a9a429b2e06e5", size = 61443, upload-time = "2025-10-17T06:19:44.89Z" }, + { url = "https://files.pythonhosted.org/packages/52/6d/f306ce64a352a3002f76b0fc88a1373f4541f9d34fad3668688610bab14b/crc32c-2.8-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cc445da03fc012a5a03b71da1df1b40139729e6a5571fd4215ab40bfb39689c7", size = 79106, upload-time = "2025-10-17T06:19:45.688Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b7/1f74965dd7ea762954a69d172dfb3a706049c84ffa45d31401d010a4a126/crc32c-2.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e3dde2ec59a8a830511d72a086ead95c0b0b7f0d418f93ea106244c5e77e350", size = 80983, upload-time = "2025-10-17T06:19:46.792Z" }, + { url = "https://files.pythonhosted.org/packages/1b/50/af93f0d91ccd61833ce77374ebfbd16f5805f5c17d18c6470976d9866d76/crc32c-2.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:61d51681a08b6a2a2e771b7f0cd1947fb87cb28f38ed55a01cb7c40b2ac4cdd8", size = 80009, upload-time = "2025-10-17T06:19:47.619Z" }, + { url = "https://files.pythonhosted.org/packages/ee/fa/94f394beb68a88258af694dab2f1284f55a406b615d7900bdd6235283bc4/crc32c-2.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:67c0716c3b1a02d5235be649487b637eed21f2d070f2b3f63f709dcd2fefb4c7", size = 79066, upload-time = "2025-10-17T06:19:48.409Z" }, + { url = "https://files.pythonhosted.org/packages/91/c6/a6050e0c64fd73c67a97da96cb59f08b05111e00b958fb87ecdce99f17ac/crc32c-2.8-cp314-cp314t-win32.whl", hash = "sha256:2e8fe863fbbd8bdb6b414a2090f1b0f52106e76e9a9c96a413495dbe5ebe492a", size = 64869, upload-time = "2025-10-17T06:19:49.197Z" }, + { url = "https://files.pythonhosted.org/packages/08/1f/c7735034e401cb1ea14f996a224518e3a3fa9987cb13680e707328a7d779/crc32c-2.8-cp314-cp314t-win_amd64.whl", hash = "sha256:20a9cfb897693eb6da19e52e2a7be2026fd4d9fc8ae318f086c0d71d5dd2d8e0", size = 66633, upload-time = "2025-10-17T06:19:50.003Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1d/dd926c68eb8aac8b142a1a10b8eb62d95212c1cf81775644373fe7cceac2/crc32c-2.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5833f4071da7ea182c514ba17d1eee8aec3c5be927d798222fbfbbd0f5eea02c", size = 62345, upload-time = "2025-10-17T06:20:09.39Z" }, + { url = "https://files.pythonhosted.org/packages/51/be/803404e5abea2ef2c15042edca04bbb7f625044cca879e47f186b43887c2/crc32c-2.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:1dc4da036126ac07b39dd9d03e93e585ec615a2ad28ff12757aef7de175295a8", size = 61229, upload-time = "2025-10-17T06:20:10.236Z" }, + { url = "https://files.pythonhosted.org/packages/fc/3a/00cc578cd27ed0b22c9be25cef2c24539d92df9fa80ebd67a3fc5419724c/crc32c-2.8-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:15905fa78344654e241371c47e6ed2411f9eeb2b8095311c68c88eccf541e8b4", size = 64108, upload-time = "2025-10-17T06:20:11.072Z" }, + { url = "https://files.pythonhosted.org/packages/6b/bc/0587ef99a1c7629f95dd0c9d4f3d894de383a0df85831eb16c48a6afdae4/crc32c-2.8-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c596f918688821f796434e89b431b1698396c38bf0b56de873621528fe3ecb1e", size = 64815, upload-time = "2025-10-17T06:20:11.919Z" }, + { url = "https://files.pythonhosted.org/packages/73/42/94f2b8b92eae9064fcfb8deef2b971514065bd606231f8857ff8ae02bebd/crc32c-2.8-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8d23c4fe01b3844cb6e091044bc1cebdef7d16472e058ce12d9fadf10d2614af", size = 66659, upload-time = "2025-10-17T06:20:12.766Z" }, +] + [[package]] name = "crcmod-plus" version = "2.3.1" @@ -17428,6 +17559,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, ] +[[package]] +name = "oci" +version = "2.183.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "circuitbreaker" }, + { name = "crc32c" }, + { name = "cryptography" }, + { name = "pyjwt" }, + { name = "pyopenssl" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/2a/77bd6cbf1c69b2f368fe3d6462d84369b0cba15e37ce713cdc08d459b95a/oci-2.183.0.tar.gz", hash = "sha256:ff572ef5f2030a788796bb509d257e6a41c6510ef9b4b6a75a079efd06e533ce", size = 17759723, upload-time = "2026-07-28T06:02:29.76Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/de/8574b3e527996a099d196e87794a4652d91a0c3185fcc7fdbb5649b75a8a/oci-2.183.0-py3-none-any.whl", hash = "sha256:bd789c98a94d7c5ea08c20d11dcf68c9cd1ad479b134727d80a930b84387070b", size = 36133501, upload-time = "2026-07-28T06:02:18.239Z" }, +] + [[package]] name = "openai" version = "2.46.0"