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"