From f391343208f7f93f3f43ecea869c00b61073a291 Mon Sep 17 00:00:00 2001 From: AlejandroMorgante <62363051+AlejandroMorgante@users.noreply.github.com> Date: Tue, 21 Jul 2026 00:10:15 -0300 Subject: [PATCH 01/14] Add OCI connection and Generative AI hook Oracle users need a shared authentication foundation for OCI services and native access to Hosted Application management APIs, matching the client-based integration pattern used by other cloud providers. --- providers/oracle/docs/connections/oci.rst | 125 ++++++ providers/oracle/docs/generative_ai.rst | 143 +++++++ providers/oracle/docs/index.rst | 10 +- providers/oracle/provider.yaml | 64 ++- providers/oracle/pyproject.toml | 4 + .../providers/oracle/get_provider_info.py | 52 ++- .../providers/oracle/hooks/base_oci.py | 233 ++++++++++ .../providers/oracle/hooks/generative_ai.py | 43 ++ .../tests/unit/oracle/hooks/test_base_oci.py | 399 ++++++++++++++++++ .../unit/oracle/hooks/test_generative_ai.py | 34 ++ uv.lock | 113 ++++- 11 files changed, 1211 insertions(+), 9 deletions(-) create mode 100644 providers/oracle/docs/connections/oci.rst create mode 100644 providers/oracle/docs/generative_ai.rst create mode 100644 providers/oracle/src/airflow/providers/oracle/hooks/base_oci.py create mode 100644 providers/oracle/src/airflow/providers/oracle/hooks/generative_ai.py create mode 100644 providers/oracle/tests/unit/oracle/hooks/test_base_oci.py create mode 100644 providers/oracle/tests/unit/oracle/hooks/test_generative_ai.py diff --git a/providers/oracle/docs/connections/oci.rst b/providers/oracle/docs/connections/oci.rst new file mode 100644 index 0000000000000..8c26836a0777a --- /dev/null +++ b/providers/oracle/docs/connections/oci.rst @@ -0,0 +1,125 @@ + .. Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + .. http://www.apache.org/licenses/LICENSE-2.0 + + .. Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + +.. _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. + +OCI support is optional because the OCI Python SDK has transitive dependencies that Apache Airflow +cannot distribute as required dependencies. Install the provider with the ``oci`` extra before using +this connection or its service hooks: + +.. code-block:: bash + + pip install 'apache-airflow-providers-oracle[oci]' + +The default connection ID is ``oci_default``. + +Service hooks reuse this connection for credentials and region while selecting their own OCI SDK +client class. 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. Configure ``User OCID`` in Login, the optional private key passphrase in + Password, and ``tenancy``, ``fingerprint``, ``region``, and ``key_content`` in Extra. A Dag + author may pass ``key_file`` to the hook instead of storing ``key_content`` in the connection. + +Config file + The Dag author passes ``auth_type="config_file"`` to the hook. The optional ``config_file`` and + ``profile`` hook arguments default to ``~/.oci/config`` and ``DEFAULT``. A connection ``region`` + overrides the profile region. + +Instance principal + The Dag author passes ``auth_type="instance_principal"`` to the hook. The OCI SDK obtains + credentials from the compute instance metadata service. The connection ``region`` is optional + because the signer normally discovers it from instance metadata. + +Resource principal + The Dag author passes ``auth_type="resource_principal"`` to the hook. The OCI SDK obtains + credentials from the resource principal environment. The connection ``region`` is optional + when the signer provides one. + +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 (optional) + User OCID for ``api_key`` authentication. + +Password (optional) + Private key passphrase for ``api_key`` authentication. + +Extra + A JSON object containing connection-scoped credentials and defaults: + + * ``tenancy``: tenancy OCID for API key authentication. + * ``fingerprint``: public key fingerprint for API key authentication. + * ``key_content``: API signing private key content; prefer a secrets backend. + * ``region``: OCI region identifier, for example ``us-chicago-1``. + * ``compartment_id``: default compartment OCID used by service operations. + +Dag-controlled hook arguments +----------------------------- + +``auth_type`` + One of ``api_key``, ``config_file``, ``instance_principal``, or ``resource_principal``. + +``key_file`` + API signing private key path for API key authentication. Do not use it together with + connection ``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", + "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/oracle/docs/generative_ai.rst b/providers/oracle/docs/generative_ai.rst new file mode 100644 index 0000000000000..98f9e8af7f734 --- /dev/null +++ b/providers/oracle/docs/generative_ai.rst @@ -0,0 +1,143 @@ + .. 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.oracle.hooks.generative_ai.OciGenerativeAIHook` uses the official +`OCI Python SDK `__ to manage +`Hosted Applications and deployments +`__. +Install ``apache-airflow-providers-oracle[oci]`` and configure an +:ref:`OCI connection ` before using the hook. +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 configured +:ref:`OCI connection `. + +Management endpoints +-------------------- + +The OCI SDK derives the management endpoint as +``https://generativeai..oci.oraclecloud.com`` and adds the ``20231130`` API base path. +The hook exposes these operations without changing OCI retry, pagination, or concurrency-control +arguments. + +========================================================== =================================================== +OCI SDK client method REST operation +========================================================== =================================================== +``create_hosted_application`` ``POST /20231130/hostedApplications`` +``get_hosted_application`` ``GET /20231130/hostedApplications/{id}`` +``list_hosted_applications`` ``GET /20231130/hostedApplications`` +``update_hosted_application`` ``PUT /20231130/hostedApplications/{id}`` +``delete_hosted_application`` ``DELETE /20231130/hostedApplications/{id}`` +``create_hosted_application_iam`` ``POST /20231130/hostedApplicationsIam`` +``get_hosted_application_iam`` ``GET /20231130/hostedApplicationsIam/{id}`` +``list_hosted_applications_iam`` ``GET /20231130/hostedApplicationsIam`` +``update_hosted_application_iam`` ``PUT /20231130/hostedApplicationsIam/{id}`` +``delete_hosted_application_iam`` ``DELETE /20231130/hostedApplicationsIam/{id}`` +``create_hosted_deployment`` ``POST /20231130/hostedDeployments`` +``get_hosted_deployment`` ``GET /20231130/hostedDeployments/{id}`` +``list_hosted_deployments`` ``GET /20231130/hostedDeployments`` +``update_hosted_deployment`` ``PUT /20231130/hostedDeployments/{id}`` +``delete_hosted_deployment`` ``DELETE /20231130/hostedDeployments/{id}`` +``get_work_request`` ``GET /20231130/workRequests/{id}`` +``list_work_request_errors`` ``GET /20231130/workRequests/{id}/errors`` +``list_work_request_logs`` ``GET /20231130/workRequests/{id}/logs`` +``list_work_requests`` ``GET /20231130/workRequests`` +========================================================== =================================================== + +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 oci.generative_ai.models import ( + CreateHostedApplicationDetails, + IdcsAuthConfig, + InboundAuthConfig, + ) + + from airflow.providers.oracle.hooks.generative_ai import OciGenerativeAIHook + + hook = OciGenerativeAIHook(oci_conn_id="oci_default") + response = hook.conn.create_hosted_application( + CreateHostedApplicationDetails( + display_name="airflow-agent-oauth", + compartment_id="ocid1.compartment.oc1..example", + inbound_auth_config=InboundAuthConfig( + inbound_auth_config_type="IDCS_AUTH_CONFIG", + idcs_config=IdcsAuthConfig( + domain_url="https://idcs-example.identity.oraclecloud.com", + scope="agent.invoke", + audience="https://agent.example.com", + ), + ), + ) + ) + work_request_id = response.headers.get("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 oci.generative_ai.models import CreateHostedApplicationIamDetails + + from airflow.providers.oracle.hooks.generative_ai import OciGenerativeAIHook + + hook = OciGenerativeAIHook(oci_conn_id="oci_default") + response = hook.conn.create_hosted_application_iam( + CreateHostedApplicationIamDetails( + display_name="airflow-agent", + compartment_id="ocid1.compartment.oc1..example", + description="Hosted application managed by Airflow", + ) + ) + work_request_id = response.headers.get("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/oracle/docs/index.rst b/providers/oracle/docs/index.rst index def6b48c2c2d9..4690b6f72451a 100644 --- a/providers/oracle/docs/index.rst +++ b/providers/oracle/docs/index.rst @@ -34,7 +34,9 @@ :maxdepth: 1 :caption: Guides - Connection types + Oracle Database connection + Oracle Cloud Infrastructure connection + OCI Generative AI Operators .. toctree:: @@ -74,7 +76,8 @@ apache-airflow-providers-oracle package ------------------------------------------------------ -`Oracle `__ +`Oracle Database `__ and +`Oracle Cloud Infrastructure `__ integrations. Release: 4.6.2 @@ -133,12 +136,13 @@ Install them when installing from PyPI. For example: .. code-block:: bash - pip install apache-airflow-providers-oracle[numpy] + pip install apache-airflow-providers-oracle[oci] =============== ============================================================================================================================================================================================================================================ Extra Dependencies =============== ============================================================================================================================================================================================================================================ +``oci`` ``oci>=2.182.0`` ``numpy`` ``numpy>=1.22.4; python_version<'3.11'``, ``numpy>=1.23.2; python_version=='3.11'``, ``numpy>=1.26.0; python_version=='3.12'``, ``numpy>=2.1.0; python_version>='3.13' and python_version<'3.14'``, ``numpy>=2.4.3; python_version>='3.14'`` ``openlineage`` ``apache-airflow-providers-openlineage`` =============== ============================================================================================================================================================================================================================================ diff --git a/providers/oracle/provider.yaml b/providers/oracle/provider.yaml index 97cfe31aa01cb..7dff795238a17 100644 --- a/providers/oracle/provider.yaml +++ b/providers/oracle/provider.yaml @@ -19,7 +19,8 @@ package-name: apache-airflow-providers-oracle name: Oracle description: | - `Oracle `__ + `Oracle Database `__ and + `Oracle Cloud Infrastructure `__ integrations. state: ready lifecycle: production @@ -91,6 +92,13 @@ integrations: - /docs/apache-airflow-providers-oracle/operators.rst logo: /docs/integration-logos/Oracle.png tags: [software] + - integration-name: Oracle Cloud Infrastructure + external-doc-url: https://docs.oracle.com/en-us/iaas/Content/home.htm + how-to-guide: + - /docs/apache-airflow-providers-oracle/connections/oci.rst + - /docs/apache-airflow-providers-oracle/generative_ai.rst + logo: /docs/integration-logos/Oracle.png + tags: [generative-ai, service] operators: - integration-name: Oracle @@ -116,6 +124,10 @@ hooks: python-modules: - airflow.providers.oracle.hooks.handlers - airflow.providers.oracle.hooks.oracle + - integration-name: Oracle Cloud Infrastructure + python-modules: + - airflow.providers.oracle.hooks.base_oci + - airflow.providers.oracle.hooks.generative_ai transfers: - source-integration-name: Oracle @@ -126,3 +138,53 @@ connection-types: - hook-class-name: airflow.providers.oracle.hooks.oracle.OracleHook hook-name: "Oracle" connection-type: oracle + - hook-class-name: airflow.providers.oracle.hooks.base_oci.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' + 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/oracle/pyproject.toml b/providers/oracle/pyproject.toml index d23df241f91eb..39186d9735f10 100644 --- a/providers/oracle/pyproject.toml +++ b/providers/oracle/pyproject.toml @@ -68,6 +68,9 @@ dependencies = [ # 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" = [ + "oci>=2.182.0", +] "numpy" = [ "numpy>=1.22.4; python_version<'3.11'", "numpy>=1.23.2; python_version=='3.11'", @@ -88,6 +91,7 @@ dev = [ "apache-airflow-providers-common-sql", "apache-airflow-providers-openlineage", # Additional devel dependencies (do not remove this line and add extra development dependencies) + "apache-airflow-providers-oracle[oci]", "numpy>=1.22.4; python_version<'3.11'", "numpy>=1.23.2; python_version=='3.11'", "numpy>=1.26.0; python_version=='3.12'", diff --git a/providers/oracle/src/airflow/providers/oracle/get_provider_info.py b/providers/oracle/src/airflow/providers/oracle/get_provider_info.py index d9cf0004700ac..966ebf267a150 100644 --- a/providers/oracle/src/airflow/providers/oracle/get_provider_info.py +++ b/providers/oracle/src/airflow/providers/oracle/get_provider_info.py @@ -25,7 +25,7 @@ def get_provider_info(): return { "package-name": "apache-airflow-providers-oracle", "name": "Oracle", - "description": "`Oracle `__\n", + "description": "`Oracle Database `__ and\n`Oracle Cloud Infrastructure `__ integrations.\n", "integrations": [ { "integration-name": "Oracle", @@ -33,7 +33,17 @@ def get_provider_info(): "how-to-guide": ["/docs/apache-airflow-providers-oracle/operators.rst"], "logo": "/docs/integration-logos/Oracle.png", "tags": ["software"], - } + }, + { + "integration-name": "Oracle Cloud Infrastructure", + "external-doc-url": "https://docs.oracle.com/en-us/iaas/Content/home.htm", + "how-to-guide": [ + "/docs/apache-airflow-providers-oracle/connections/oci.rst", + "/docs/apache-airflow-providers-oracle/generative_ai.rst", + ], + "logo": "/docs/integration-logos/Oracle.png", + "tags": ["generative-ai", "service"], + }, ], "operators": [ {"integration-name": "Oracle", "python-modules": ["airflow.providers.oracle.operators.oracle"]} @@ -61,7 +71,14 @@ def get_provider_info(): "airflow.providers.oracle.hooks.handlers", "airflow.providers.oracle.hooks.oracle", ], - } + }, + { + "integration-name": "Oracle Cloud Infrastructure", + "python-modules": [ + "airflow.providers.oracle.hooks.base_oci", + "airflow.providers.oracle.hooks.generative_ai", + ], + }, ], "transfers": [ { @@ -75,6 +92,33 @@ def get_provider_info(): "hook-class-name": "airflow.providers.oracle.hooks.oracle.OracleHook", "hook-name": "Oracle", "connection-type": "oracle", - } + }, + { + "hook-class-name": "airflow.providers.oracle.hooks.base_oci.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"]}}, + "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/oracle/src/airflow/providers/oracle/hooks/base_oci.py b/providers/oracle/src/airflow/providers/oracle/hooks/base_oci.py new file mode 100644 index 0000000000000..cf35a8d6dd1fb --- /dev/null +++ b/providers/oracle/src/airflow/providers/oracle/hooks/base_oci.py @@ -0,0 +1,233 @@ +# 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 functools import cached_property +from typing import TYPE_CHECKING, Any, Generic, TypeVar + +from airflow.providers.common.compat.sdk import AirflowOptionalProviderFeatureException, BaseHook + +if TYPE_CHECKING: + from airflow.sdk import Connection + + OciSigner = Any + +OciClient = TypeVar("OciClient") + +OCI_AUTH_TYPE_API_KEY = "api_key" +OCI_AUTH_TYPE_CONFIG_FILE = "config_file" +OCI_AUTH_TYPE_INSTANCE_PRINCIPAL = "instance_principal" +OCI_AUTH_TYPE_RESOURCE_PRINCIPAL = "resource_principal" +OCI_AUTH_TYPES = ( + OCI_AUTH_TYPE_API_KEY, + OCI_AUTH_TYPE_CONFIG_FILE, + OCI_AUTH_TYPE_INSTANCE_PRINCIPAL, + OCI_AUTH_TYPE_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-oracle[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 `. + :param auth_type: OCI authentication type selected by the Dag author. + :param key_file: API signing private key path selected by the Dag author. + :param config_file: OCI SDK configuration file selected by the Dag author. + :param profile: Profile to load from the OCI SDK configuration file. + :param service_endpoint: Optional service endpoint selected by the Dag author. + """ + + conn_name_attr = "oci_conn_id" + default_conn_name = "oci_default" + conn_type = "oci" + hook_name = "Oracle Cloud Infrastructure" + client_class: Callable[..., OciClient] | None = None + + def __init__( + self, + oci_conn_id: str = default_conn_name, + *, + auth_type: str = OCI_AUTH_TYPE_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()), + "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.""" + 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 from the Airflow connection.""" + oci = _get_oci_sdk() + conn = self.connection + extras = conn.extra_dejson + auth_type = self.auth_type + + if auth_type == OCI_AUTH_TYPE_CONFIG_FILE: + 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 := extras.get("region"): + config["region"] = region + return config, None + + if auth_type == OCI_AUTH_TYPE_INSTANCE_PRINCIPAL: + signer = oci.auth.signers.InstancePrincipalsSecurityTokenSigner() + return self._build_principal_config(extras, signer), signer + + if auth_type == OCI_AUTH_TYPE_RESOURCE_PRINCIPAL: + signer = oci.auth.signers.get_resource_principals_signer() + return self._build_principal_config(extras, signer), signer + + if auth_type != OCI_AUTH_TYPE_API_KEY: + raise ValueError( + f"Unsupported OCI authentication type: {auth_type!r}. Expected one of {OCI_AUTH_TYPES}." + ) + + config = { + "tenancy": extras.get("tenancy"), + "user": conn.login, + "fingerprint": extras.get("fingerprint"), + "region": extras.get("region"), + "pass_phrase": conn.password, + } + key_file = self.key_file + key_content = extras.get("key_content") + if key_file and key_content: + raise ValueError("OCI API key authentication cannot use both 'key_file' and 'key_content'.") + if not key_file and not key_content: + raise ValueError("OCI API key authentication requires either 'key_file' or 'key_content'.") + if key_file: + config["key_file"] = key_file + else: + config["key_content"] = 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: + oci = _get_oci_sdk() + config, signer = self.get_oci_config() + client_kwargs = {"signer": signer} if signer is not None else {} + oci.identity.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.connection.extra_dejson.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 + + @staticmethod + def _build_principal_config(extras: dict[str, Any], signer: OciSigner) -> dict[str, Any]: + region = extras.get("region") or getattr(signer, "region", None) + return {"region": region} if region else {} + + 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]: + if self.client_class is None: + raise ValueError("client_class must be specified by an OCI service hook.") + return self.client_class diff --git a/providers/oracle/src/airflow/providers/oracle/hooks/generative_ai.py b/providers/oracle/src/airflow/providers/oracle/hooks/generative_ai.py new file mode 100644 index 0000000000000..18575709357d9 --- /dev/null +++ b/providers/oracle/src/airflow/providers/oracle/hooks/generative_ai.py @@ -0,0 +1,43 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +from collections.abc import Callable +from typing import TYPE_CHECKING + +from airflow.providers.oracle.hooks.base_oci 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]: + return _get_oci_sdk().generative_ai.GenerativeAiClient diff --git a/providers/oracle/tests/unit/oracle/hooks/test_base_oci.py b/providers/oracle/tests/unit/oracle/hooks/test_base_oci.py new file mode 100644 index 0000000000000..4c9d1ec01cca7 --- /dev/null +++ b/providers/oracle/tests/unit/oracle/hooks/test_base_oci.py @@ -0,0 +1,399 @@ +# 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 subprocess +import sys +from unittest import mock + +import pytest +from oci.generative_ai import GenerativeAiClient +from wtforms import PasswordField + +from airflow.models import Connection +from airflow.providers.common.compat.sdk import AirflowOptionalProviderFeatureException +from airflow.providers.oracle.get_provider_info import get_provider_info +from airflow.providers.oracle.hooks.base_oci import ( + OCI_AUTH_TYPE_CONFIG_FILE, + OCI_AUTH_TYPE_INSTANCE_PRINCIPAL, + OCI_AUTH_TYPE_RESOURCE_PRINCIPAL, + OciBaseHook, + _get_oci_sdk, +) + + +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) + + @pytest.mark.parametrize( + ("hook_kwargs", "connection_extra", "key_field", "key_value"), + [ + ({"key_file": "/keys/oci.pem"}, {}, "key_file", "/keys/oci.pem"), + ({}, {"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": OCI_AUTH_TYPE_INSTANCE_PRINCIPAL, + "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", + "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( + ("hook_kwargs", "extra", "error_message"), + [ + ( + {}, + {}, + "OCI API key authentication requires either 'key_file' or 'key_content'", + ), + ( + {"key_file": "/keys/oci.pem"}, + {"key_content": "private-key-content"}, + "OCI API key authentication cannot use both 'key_file' and '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=OCI_AUTH_TYPE_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=OCI_AUTH_TYPE_CONFIG_FILE, + config_file=config_file, + profile=profile, + ) + mock_from_file.return_value = {"region": "us-ashburn-1"} + self.set_connection(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=OCI_AUTH_TYPE_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=OCI_AUTH_TYPE_RESOURCE_PRINCIPAL) + signer = mock_get_signer.return_value + signer.region = "us-phoenix-1" + self.set_connection(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(auth_type=OCI_AUTH_TYPE_RESOURCE_PRINCIPAL) + signer = mock_get_signer.return_value + del signer.region + self.set_connection(Connection()) + + config, actual_signer = self.hook.get_oci_config() + + assert config == {} + assert actual_signer is signer + + 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() + + def test_get_client_with_signer_and_explicit_endpoint(self): + signer = mock.sentinel.signer + 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=({"region": "us-chicago-1"}, signer) + ) + self.hook.service_endpoint = "https://generativeai.example.test/" + + result = self.hook.get_client(client_class, timeout=30) + + assert result is client + client_class.assert_called_once_with( + config={"region": "us-chicago-1"}, + signer=signer, + service_endpoint="https://generativeai.example.test", + timeout=30, + ) + + def test_get_client_without_signer_or_endpoint(self): + 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=({"region": "us-chicago-1"}, None) + ) + self.set_connection(Connection()) + + result = self.hook.get_client(client_class) + + assert result is client + client_class.assert_called_once_with(config={"region": "us-chicago-1"}) + + def test_get_conn_requires_service_client_class(self): + with pytest.raises(ValueError, match="client_class must be specified by an OCI service hook"): + self.hook.get_conn() + + def test_get_client_class_returns_configured_class(self): + self.hook.client_class = GenerativeAiClient + + assert self.hook._get_client_class() is GenerativeAiClient + + @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 + + def test_get_compartment_id_prefers_explicit_value(self): + self.set_connection(Connection(extra={"compartment_id": "connection-compartment"})) + + assert self.hook.get_compartment_id("explicit-compartment") == "explicit-compartment" + + def test_get_compartment_id_from_connection(self): + self.set_connection(Connection(extra={"compartment_id": "connection-compartment"})) + + assert self.hook.get_compartment_id() == "connection-compartment" + + 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_connection_form_widgets(self): + widgets = self.hook.get_connection_form_widgets() + + assert set(widgets) == { + "tenancy", + "fingerprint", + "key_content", + "region", + "compartment_id", + } + assert widgets["key_content"].field_class is PasswordField + + 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"]["key_content"]["schema"]["format"] == "password" + + +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-oracle\[oci\]'", + ): + _get_oci_sdk() + + +def test_hook_modules_import_without_optional_oci_sdk(): + subprocess.run( + [ + sys.executable, + "-c", + """ +import sys + +sys.modules["oci"] = None +import airflow.providers.oracle.hooks.base_oci +import airflow.providers.oracle.hooks.generative_ai +""", + ], + check=True, + ) diff --git a/providers/oracle/tests/unit/oracle/hooks/test_generative_ai.py b/providers/oracle/tests/unit/oracle/hooks/test_generative_ai.py new file mode 100644 index 0000000000000..1d81f37c7793e --- /dev/null +++ b/providers/oracle/tests/unit/oracle/hooks/test_generative_ai.py @@ -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. +from __future__ import annotations + +from unittest import mock + +from oci.generative_ai import GenerativeAiClient + +from airflow.providers.oracle.hooks.generative_ai import OciGenerativeAIHook + + +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/uv.lock b/uv.lock index b66ad967fd43c..1604efd370901 100644 --- a/uv.lock +++ b/uv.lock @@ -6950,6 +6950,9 @@ numpy = [ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] +oci = [ + { name = "oci" }, +] openlineage = [ { name = "apache-airflow-providers-openlineage" }, ] @@ -6961,6 +6964,7 @@ dev = [ { name = "apache-airflow-providers-common-compat" }, { name = "apache-airflow-providers-common-sql" }, { name = "apache-airflow-providers-openlineage" }, + { name = "apache-airflow-providers-oracle", extra = ["oci"] }, { name = "apache-airflow-task-sdk" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, @@ -6981,9 +6985,10 @@ requires-dist = [ { name = "numpy", marker = "python_full_version == '3.12.*' and extra == 'numpy'", specifier = ">=1.26.0" }, { name = "numpy", marker = "python_full_version == '3.13.*' and extra == 'numpy'", specifier = ">=2.1.0" }, { name = "numpy", marker = "python_full_version >= '3.14' and extra == 'numpy'", specifier = ">=2.4.3" }, + { name = "oci", marker = "extra == 'oci'", specifier = ">=2.182.0" }, { name = "oracledb", specifier = ">=2.3.0" }, ] -provides-extras = ["numpy", "openlineage"] +provides-extras = ["oci", "numpy", "openlineage"] [package.metadata.requires-dev] dev = [ @@ -6992,6 +6997,7 @@ dev = [ { name = "apache-airflow-providers-common-compat", editable = "providers/common/compat" }, { name = "apache-airflow-providers-common-sql", editable = "providers/common/sql" }, { name = "apache-airflow-providers-openlineage", editable = "providers/openlineage" }, + { name = "apache-airflow-providers-oracle", extras = ["oci"], editable = "providers/oracle" }, { name = "apache-airflow-task-sdk", editable = "task-sdk" }, { name = "numpy", marker = "python_full_version < '3.11'", specifier = ">=1.22.4" }, { name = "numpy", marker = "python_full_version == '3.11.*'", specifier = ">=1.23.2" }, @@ -10775,6 +10781,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 +11174,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 +17519,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.182.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/a6/20/769e5fef5006ca00b0917fc7a60d143af856ace4320af567fe83ab47f72b/oci-2.182.0.tar.gz", hash = "sha256:effdd24f808179cfa15ba1084181cadda8e09332699cdbcd2f78ebbd0a2d072e", size = 17631582, upload-time = "2026-07-14T07:21:19.742Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/b2/bd2ed4b1f29b1afe1565dd19e9cc0420fede82b9aa1c409361c18d84c554/oci-2.182.0-py3-none-any.whl", hash = "sha256:f8d7d675d1fe75721dac9aa97a11bf8b670a1814afd4969b7023fef8e3ce57d2", size = 35969385, upload-time = "2026-07-14T07:21:11.372Z" }, +] + [[package]] name = "openai" version = "2.46.0" From 2cca99fa64e23e6907db199ed77159f7c1d7aa4e Mon Sep 17 00:00:00 2001 From: AlejandroMorgante <62363051+AlejandroMorgante@users.noreply.github.com> Date: Tue, 21 Jul 2026 09:56:25 -0300 Subject: [PATCH 02/14] Fix Oracle provider tests without legacy UI dependencies The lowest-direct-dependencies environment intentionally excludes optional FAB form packages, so Oracle tests must remain collectible without them. --- providers/oracle/tests/unit/oracle/hooks/test_base_oci.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/providers/oracle/tests/unit/oracle/hooks/test_base_oci.py b/providers/oracle/tests/unit/oracle/hooks/test_base_oci.py index 4c9d1ec01cca7..0d24833328e7c 100644 --- a/providers/oracle/tests/unit/oracle/hooks/test_base_oci.py +++ b/providers/oracle/tests/unit/oracle/hooks/test_base_oci.py @@ -22,7 +22,6 @@ import pytest from oci.generative_ai import GenerativeAiClient -from wtforms import PasswordField from airflow.models import Connection from airflow.providers.common.compat.sdk import AirflowOptionalProviderFeatureException @@ -331,6 +330,10 @@ def test_get_compartment_id_requires_value(self): self.hook.get_compartment_id() 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) == { @@ -340,7 +343,7 @@ def test_connection_form_widgets(self): "region", "compartment_id", } - assert widgets["key_content"].field_class is PasswordField + assert widgets["key_content"].field_class is password_field def test_ui_field_behaviour(self): assert self.hook.get_ui_field_behaviour() == { From 1286dd9f09ce422b98e092d3975fdfeca60b7d12 Mon Sep 17 00:00:00 2001 From: AlejandroMorgante <62363051+AlejandroMorgante@users.noreply.github.com> Date: Tue, 21 Jul 2026 11:39:18 -0300 Subject: [PATCH 03/14] Skip Oracle SDK tests when OCI is unavailable Provider compatibility jobs install the base Oracle package without optional extras, so OCI-specific tests must not fail collection in those environments. --- providers/oracle/tests/unit/oracle/hooks/test_base_oci.py | 3 ++- .../oracle/tests/unit/oracle/hooks/test_generative_ai.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/providers/oracle/tests/unit/oracle/hooks/test_base_oci.py b/providers/oracle/tests/unit/oracle/hooks/test_base_oci.py index 0d24833328e7c..04005c310b684 100644 --- a/providers/oracle/tests/unit/oracle/hooks/test_base_oci.py +++ b/providers/oracle/tests/unit/oracle/hooks/test_base_oci.py @@ -21,7 +21,6 @@ from unittest import mock import pytest -from oci.generative_ai import GenerativeAiClient from airflow.models import Connection from airflow.providers.common.compat.sdk import AirflowOptionalProviderFeatureException @@ -34,6 +33,8 @@ _get_oci_sdk, ) +GenerativeAiClient = pytest.importorskip("oci.generative_ai").GenerativeAiClient + class TestOciBaseHook: def setup_method(self): diff --git a/providers/oracle/tests/unit/oracle/hooks/test_generative_ai.py b/providers/oracle/tests/unit/oracle/hooks/test_generative_ai.py index 1d81f37c7793e..461337536421f 100644 --- a/providers/oracle/tests/unit/oracle/hooks/test_generative_ai.py +++ b/providers/oracle/tests/unit/oracle/hooks/test_generative_ai.py @@ -18,10 +18,12 @@ from unittest import mock -from oci.generative_ai import GenerativeAiClient +import pytest from airflow.providers.oracle.hooks.generative_ai import OciGenerativeAIHook +GenerativeAiClient = pytest.importorskip("oci.generative_ai").GenerativeAiClient + class TestOciGenerativeAIHook: @mock.patch.object(OciGenerativeAIHook, "get_client", autospec=True) From 5b5f02e56e662c75fd6ee8ea9c7e8b13d71385d6 Mon Sep 17 00:00:00 2001 From: AlejandroMorgante <62363051+AlejandroMorgante@users.noreply.github.com> Date: Tue, 21 Jul 2026 12:26:13 -0300 Subject: [PATCH 04/14] Fix Oracle provider documentation validation The provider integration registry only accepts operator, sensor, and transfer pages as how-to guides, so the OCI connection and service overview pages must remain regular provider documentation. --- providers/oracle/provider.yaml | 3 --- .../oracle/src/airflow/providers/oracle/get_provider_info.py | 4 ---- 2 files changed, 7 deletions(-) diff --git a/providers/oracle/provider.yaml b/providers/oracle/provider.yaml index 7dff795238a17..579d9ba291913 100644 --- a/providers/oracle/provider.yaml +++ b/providers/oracle/provider.yaml @@ -94,9 +94,6 @@ integrations: tags: [software] - integration-name: Oracle Cloud Infrastructure external-doc-url: https://docs.oracle.com/en-us/iaas/Content/home.htm - how-to-guide: - - /docs/apache-airflow-providers-oracle/connections/oci.rst - - /docs/apache-airflow-providers-oracle/generative_ai.rst logo: /docs/integration-logos/Oracle.png tags: [generative-ai, service] diff --git a/providers/oracle/src/airflow/providers/oracle/get_provider_info.py b/providers/oracle/src/airflow/providers/oracle/get_provider_info.py index 966ebf267a150..284fd93ff3aab 100644 --- a/providers/oracle/src/airflow/providers/oracle/get_provider_info.py +++ b/providers/oracle/src/airflow/providers/oracle/get_provider_info.py @@ -37,10 +37,6 @@ def get_provider_info(): { "integration-name": "Oracle Cloud Infrastructure", "external-doc-url": "https://docs.oracle.com/en-us/iaas/Content/home.htm", - "how-to-guide": [ - "/docs/apache-airflow-providers-oracle/connections/oci.rst", - "/docs/apache-airflow-providers-oracle/generative_ai.rst", - ], "logo": "/docs/integration-logos/Oracle.png", "tags": ["generative-ai", "service"], }, From b0199c4c685f8f1f93c81a5a4f2bf75a095276ca Mon Sep 17 00:00:00 2001 From: AlejandroMorgante <62363051+AlejandroMorgante@users.noreply.github.com> Date: Tue, 21 Jul 2026 14:49:07 -0300 Subject: [PATCH 05/14] Fix OCI Generative AI documentation spelling The generated API reference is spellchecked from source docstrings, so OCI response terminology must be marked as literal text to avoid a false spelling failure. --- .../oracle/src/airflow/providers/oracle/hooks/generative_ai.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/oracle/src/airflow/providers/oracle/hooks/generative_ai.py b/providers/oracle/src/airflow/providers/oracle/hooks/generative_ai.py index 18575709357d9..a948ab1ec11d1 100644 --- a/providers/oracle/src/airflow/providers/oracle/hooks/generative_ai.py +++ b/providers/oracle/src/airflow/providers/oracle/hooks/generative_ai.py @@ -31,7 +31,7 @@ class OciGenerativeAIHook(OciBaseHook["GenerativeAiClient"]): 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. + ``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. From 540a54787bdf39467aaa593d59a5fb5705bedc2a Mon Sep 17 00:00:00 2001 From: AlejandroMorgante <62363051+AlejandroMorgante@users.noreply.github.com> Date: Sat, 25 Jul 2026 20:15:02 -0300 Subject: [PATCH 06/14] Make OCI authentication safer and easier to configure Principal-based authentication should work without an otherwise unused Airflow connection, and private signing keys must be recognized as sensitive connection data. The documentation should describe this behavior without duplicating the OCI SDK reference. --- providers/oracle/docs/connections/oci.rst | 61 ++++--- providers/oracle/docs/generative_ai.rst | 76 ++------- providers/oracle/provider.yaml | 10 +- .../providers/oracle/get_provider_info.py | 18 ++- .../providers/oracle/hooks/base_oci.py | 94 +++++++---- .../tests/unit/oracle/hooks/test_base_oci.py | 152 ++++++++++-------- 6 files changed, 217 insertions(+), 194 deletions(-) diff --git a/providers/oracle/docs/connections/oci.rst b/providers/oracle/docs/connections/oci.rst index 8c26836a0777a..1601e1cb57694 100644 --- a/providers/oracle/docs/connections/oci.rst +++ b/providers/oracle/docs/connections/oci.rst @@ -34,32 +34,35 @@ this connection or its service hooks: The default connection ID is ``oci_default``. -Service hooks reuse this connection for credentials and region while selecting their own OCI SDK -client class. Each SDK client derives its endpoint from the configured region unless the Dag author -passes a ``service_endpoint`` argument to the hook. +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. Configure ``User OCID`` in Login, the optional private key passphrase in - Password, and ``tenancy``, ``fingerprint``, ``region``, and ``key_content`` in Extra. A Dag - author may pass ``key_file`` to the hook instead of storing ``key_content`` in the connection. + 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 the hook. The optional ``config_file`` and - ``profile`` hook arguments default to ``~/.oci/config`` and ``DEFAULT``. A connection ``region`` - overrides the profile region. + 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. The connection ``region`` is optional - because the signer normally discovers it from instance metadata. + 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. The connection ``region`` is optional - when the signer provides one. + 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, @@ -76,20 +79,26 @@ authors can call ``test_connection()`` on a hook configured for another authenti Configuring the connection -------------------------- -Login (optional) - User OCID for ``api_key`` authentication. +Login + User OCID. Required for ``api_key`` authentication and not used by the other authentication + types. -Password (optional) - Private key passphrase for ``api_key`` authentication. +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 for API key authentication. - * ``fingerprint``: public key fingerprint for API key authentication. - * ``key_content``: API signing private key content; prefer a secrets backend. - * ``region``: OCI region identifier, for example ``us-chicago-1``. - * ``compartment_id``: default compartment OCID used by service operations. + * ``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, for example ``us-chicago-1``. 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 ----------------------------- @@ -97,9 +106,13 @@ 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 ``key_content``. + connection ``private_key_content``. ``config_file`` and ``profile`` OCI SDK configuration file and profile for ``config_file`` authentication. @@ -116,7 +129,7 @@ API key example { "tenancy": "ocid1.tenancy.oc1..example", "fingerprint": "aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99", - "key_content": "", + "private_key_content": "", "region": "us-chicago-1", "compartment_id": "ocid1.compartment.oc1..example" } diff --git a/providers/oracle/docs/generative_ai.rst b/providers/oracle/docs/generative_ai.rst index 98f9e8af7f734..73682e4cb392c 100644 --- a/providers/oracle/docs/generative_ai.rst +++ b/providers/oracle/docs/generative_ai.rst @@ -22,8 +22,9 @@ OCI Generative AI Hosted Applications `OCI Python SDK `__ to manage `Hosted Applications and deployments `__. -Install ``apache-airflow-providers-oracle[oci]`` and configure an -:ref:`OCI connection ` before using the hook. +Install ``apache-airflow-providers-oracle[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. @@ -37,40 +38,8 @@ Oracle exposes separate application APIs for `two inbound authentication variant ``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 configured -:ref:`OCI connection `. - -Management endpoints --------------------- - -The OCI SDK derives the management endpoint as -``https://generativeai..oci.oraclecloud.com`` and adds the ``20231130`` API base path. -The hook exposes these operations without changing OCI retry, pagination, or concurrency-control -arguments. - -========================================================== =================================================== -OCI SDK client method REST operation -========================================================== =================================================== -``create_hosted_application`` ``POST /20231130/hostedApplications`` -``get_hosted_application`` ``GET /20231130/hostedApplications/{id}`` -``list_hosted_applications`` ``GET /20231130/hostedApplications`` -``update_hosted_application`` ``PUT /20231130/hostedApplications/{id}`` -``delete_hosted_application`` ``DELETE /20231130/hostedApplications/{id}`` -``create_hosted_application_iam`` ``POST /20231130/hostedApplicationsIam`` -``get_hosted_application_iam`` ``GET /20231130/hostedApplicationsIam/{id}`` -``list_hosted_applications_iam`` ``GET /20231130/hostedApplicationsIam`` -``update_hosted_application_iam`` ``PUT /20231130/hostedApplicationsIam/{id}`` -``delete_hosted_application_iam`` ``DELETE /20231130/hostedApplicationsIam/{id}`` -``create_hosted_deployment`` ``POST /20231130/hostedDeployments`` -``get_hosted_deployment`` ``GET /20231130/hostedDeployments/{id}`` -``list_hosted_deployments`` ``GET /20231130/hostedDeployments`` -``update_hosted_deployment`` ``PUT /20231130/hostedDeployments/{id}`` -``delete_hosted_deployment`` ``DELETE /20231130/hostedDeployments/{id}`` -``get_work_request`` ``GET /20231130/workRequests/{id}`` -``list_work_request_errors`` ``GET /20231130/workRequests/{id}/errors`` -``list_work_request_logs`` ``GET /20231130/workRequests/{id}/logs`` -``list_work_requests`` ``GET /20231130/workRequests`` -========================================================== =================================================== +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 @@ -89,30 +58,11 @@ and OAuth settings: .. code-block:: python - from oci.generative_ai.models import ( - CreateHostedApplicationDetails, - IdcsAuthConfig, - InboundAuthConfig, - ) - from airflow.providers.oracle.hooks.generative_ai import OciGenerativeAIHook hook = OciGenerativeAIHook(oci_conn_id="oci_default") - response = hook.conn.create_hosted_application( - CreateHostedApplicationDetails( - display_name="airflow-agent-oauth", - compartment_id="ocid1.compartment.oc1..example", - inbound_auth_config=InboundAuthConfig( - inbound_auth_config_type="IDCS_AUTH_CONFIG", - idcs_config=IdcsAuthConfig( - domain_url="https://idcs-example.identity.oraclecloud.com", - scope="agent.invoke", - audience="https://agent.example.com", - ), - ), - ) - ) - work_request_id = response.headers.get("opc-work-request-id") + response = hook.conn.create_hosted_application(...) + work_request_id = response.headers["opc-work-request-id"] Creating an OCI IAM Hosted Application -------------------------------------- @@ -121,19 +71,11 @@ OCI IAM applications do not require an OAuth or identity domain configuration: .. code-block:: python - from oci.generative_ai.models import CreateHostedApplicationIamDetails - from airflow.providers.oracle.hooks.generative_ai import OciGenerativeAIHook hook = OciGenerativeAIHook(oci_conn_id="oci_default") - response = hook.conn.create_hosted_application_iam( - CreateHostedApplicationIamDetails( - display_name="airflow-agent", - compartment_id="ocid1.compartment.oc1..example", - description="Hosted application managed by Airflow", - ) - ) - work_request_id = response.headers.get("opc-work-request-id") + response = hook.conn.create_hosted_application_iam(...) + work_request_id = response.headers["opc-work-request-id"] Agent invocation ---------------- diff --git a/providers/oracle/provider.yaml b/providers/oracle/provider.yaml index 579d9ba291913..c114e721d58a2 100644 --- a/providers/oracle/provider.yaml +++ b/providers/oracle/provider.yaml @@ -95,6 +95,12 @@ integrations: - integration-name: Oracle Cloud Infrastructure external-doc-url: https://docs.oracle.com/en-us/iaas/Content/home.htm logo: /docs/integration-logos/Oracle.png + tags: [service] + - integration-name: OCI Generative AI + external-doc-url: https://docs.oracle.com/en-us/iaas/Content/generative-ai/home.htm + how-to-guide: + - /docs/apache-airflow-providers-oracle/generative_ai.rst + logo: /docs/integration-logos/Oracle.png tags: [generative-ai, service] operators: @@ -124,6 +130,8 @@ hooks: - integration-name: Oracle Cloud Infrastructure python-modules: - airflow.providers.oracle.hooks.base_oci + - integration-name: OCI Generative AI + python-modules: - airflow.providers.oracle.hooks.generative_ai transfers: @@ -166,7 +174,7 @@ connection-types: type: - string - 'null' - key_content: + private_key_content: label: Private Key Content schema: type: diff --git a/providers/oracle/src/airflow/providers/oracle/get_provider_info.py b/providers/oracle/src/airflow/providers/oracle/get_provider_info.py index 284fd93ff3aab..757b46cdf1ba5 100644 --- a/providers/oracle/src/airflow/providers/oracle/get_provider_info.py +++ b/providers/oracle/src/airflow/providers/oracle/get_provider_info.py @@ -38,6 +38,13 @@ def get_provider_info(): "integration-name": "Oracle Cloud Infrastructure", "external-doc-url": "https://docs.oracle.com/en-us/iaas/Content/home.htm", "logo": "/docs/integration-logos/Oracle.png", + "tags": ["service"], + }, + { + "integration-name": "OCI Generative AI", + "external-doc-url": "https://docs.oracle.com/en-us/iaas/Content/generative-ai/home.htm", + "how-to-guide": ["/docs/apache-airflow-providers-oracle/generative_ai.rst"], + "logo": "/docs/integration-logos/Oracle.png", "tags": ["generative-ai", "service"], }, ], @@ -70,10 +77,11 @@ def get_provider_info(): }, { "integration-name": "Oracle Cloud Infrastructure", - "python-modules": [ - "airflow.providers.oracle.hooks.base_oci", - "airflow.providers.oracle.hooks.generative_ai", - ], + "python-modules": ["airflow.providers.oracle.hooks.base_oci"], + }, + { + "integration-name": "OCI Generative AI", + "python-modules": ["airflow.providers.oracle.hooks.generative_ai"], }, ], "transfers": [ @@ -108,7 +116,7 @@ def get_provider_info(): "conn-fields": { "tenancy": {"label": "Tenancy OCID", "schema": {"type": ["string", "null"]}}, "fingerprint": {"label": "Key Fingerprint", "schema": {"type": ["string", "null"]}}, - "key_content": { + "private_key_content": { "label": "Private Key Content", "schema": {"type": ["string", "null"], "format": "password"}, }, diff --git a/providers/oracle/src/airflow/providers/oracle/hooks/base_oci.py b/providers/oracle/src/airflow/providers/oracle/hooks/base_oci.py index cf35a8d6dd1fb..22b9bc1c176e4 100644 --- a/providers/oracle/src/airflow/providers/oracle/hooks/base_oci.py +++ b/providers/oracle/src/airflow/providers/oracle/hooks/base_oci.py @@ -17,10 +17,15 @@ from __future__ import annotations 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 AirflowOptionalProviderFeatureException, BaseHook +from airflow.providers.common.compat.sdk import ( + AirflowNotFoundException, + AirflowOptionalProviderFeatureException, + BaseHook, +) if TYPE_CHECKING: from airflow.sdk import Connection @@ -29,16 +34,14 @@ OciClient = TypeVar("OciClient") -OCI_AUTH_TYPE_API_KEY = "api_key" -OCI_AUTH_TYPE_CONFIG_FILE = "config_file" -OCI_AUTH_TYPE_INSTANCE_PRINCIPAL = "instance_principal" -OCI_AUTH_TYPE_RESOURCE_PRINCIPAL = "resource_principal" -OCI_AUTH_TYPES = ( - OCI_AUTH_TYPE_API_KEY, - OCI_AUTH_TYPE_CONFIG_FILE, - OCI_AUTH_TYPE_INSTANCE_PRINCIPAL, - OCI_AUTH_TYPE_RESOURCE_PRINCIPAL, -) + +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: @@ -61,24 +64,31 @@ class OciBaseHook(BaseHook, Generic[OciClient]): 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" - client_class: Callable[..., OciClient] | None = None def __init__( self, - oci_conn_id: str = default_conn_name, + oci_conn_id: str | None = default_conn_name, *, - auth_type: str = OCI_AUTH_TYPE_API_KEY, + auth_type: str = OciAuthType.API_KEY, key_file: str | None = None, config_file: str | None = None, profile: str | None = None, @@ -102,7 +112,7 @@ def get_connection_form_widgets(cls) -> dict[str, Any]: return { "tenancy": StringField(lazy_gettext("Tenancy OCID"), widget=BS3TextFieldWidget()), "fingerprint": StringField(lazy_gettext("Key Fingerprint"), widget=BS3TextFieldWidget()), - "key_content": PasswordField( + "private_key_content": PasswordField( lazy_gettext("Private Key Content"), widget=BS3PasswordFieldWidget() ), "region": StringField(lazy_gettext("Region"), widget=BS3TextFieldWidget()), @@ -131,16 +141,17 @@ def get_ui_field_behaviour(cls) -> dict[str, Any]: @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 from the Airflow connection.""" + """Build OCI SDK configuration and an optional signer for the selected authentication type.""" oci = _get_oci_sdk() - conn = self.connection - extras = conn.extra_dejson auth_type = self.auth_type - if auth_type == OCI_AUTH_TYPE_CONFIG_FILE: + 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, @@ -149,19 +160,24 @@ def get_oci_config(self) -> tuple[dict[str, Any], OciSigner | None]: config["region"] = region return config, None - if auth_type == OCI_AUTH_TYPE_INSTANCE_PRINCIPAL: + 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 == OCI_AUTH_TYPE_RESOURCE_PRINCIPAL: + 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 != OCI_AUTH_TYPE_API_KEY: + if auth_type != OciAuthType.API_KEY: raise ValueError( - f"Unsupported OCI authentication type: {auth_type!r}. Expected one of {OCI_AUTH_TYPES}." + 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, @@ -170,15 +186,19 @@ def get_oci_config(self) -> tuple[dict[str, Any], OciSigner | None]: "pass_phrase": conn.password, } key_file = self.key_file - key_content = extras.get("key_content") - if key_file and key_content: - raise ValueError("OCI API key authentication cannot use both 'key_file' and 'key_content'.") - if not key_file and not key_content: - raise ValueError("OCI API key authentication requires either 'key_file' or 'key_content'.") + 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"] = key_content + config["key_content"] = private_key_content return config, None def get_client(self, client_class: Callable[..., OciClient], **client_kwargs: Any) -> OciClient: @@ -224,10 +244,20 @@ def _build_principal_config(extras: dict[str, Any], signer: OciSigner) -> dict[s region = extras.get("region") or getattr(signer, "region", None) return {"region": region} if region else {} + 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]: - if self.client_class is None: - raise ValueError("client_class must be specified by an OCI service hook.") - return self.client_class + raise NotImplementedError("OCI service hooks must implement _get_client_class().") diff --git a/providers/oracle/tests/unit/oracle/hooks/test_base_oci.py b/providers/oracle/tests/unit/oracle/hooks/test_base_oci.py index 04005c310b684..d0599d07af06f 100644 --- a/providers/oracle/tests/unit/oracle/hooks/test_base_oci.py +++ b/providers/oracle/tests/unit/oracle/hooks/test_base_oci.py @@ -18,17 +18,19 @@ import subprocess import sys +from textwrap import dedent from unittest import mock import pytest from airflow.models import Connection -from airflow.providers.common.compat.sdk import AirflowOptionalProviderFeatureException +from airflow.providers.common.compat.sdk import ( + AirflowNotFoundException, + AirflowOptionalProviderFeatureException, +) from airflow.providers.oracle.get_provider_info import get_provider_info from airflow.providers.oracle.hooks.base_oci import ( - OCI_AUTH_TYPE_CONFIG_FILE, - OCI_AUTH_TYPE_INSTANCE_PRINCIPAL, - OCI_AUTH_TYPE_RESOURCE_PRINCIPAL, + OciAuthType, OciBaseHook, _get_oci_sdk, ) @@ -43,11 +45,22 @@ def setup_method(self): 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"), - ({}, {"key_content": "private-key-content"}, "key_content", "private-key-content"), + ( + {}, + {"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): @@ -82,7 +95,7 @@ def test_connection_extra_cannot_control_hook_configuration(self): Connection( login="ocid1.user.test", extra={ - "auth_type": OCI_AUTH_TYPE_INSTANCE_PRINCIPAL, + "auth_type": OciAuthType.INSTANCE_PRINCIPAL.value, "key_file": "/etc/hosts", "config_file": "/etc/hosts", "profile": "UNTRUSTED", @@ -90,7 +103,7 @@ def test_connection_extra_cannot_control_hook_configuration(self): "tenancy": "ocid1.tenancy.test", "fingerprint": "fingerprint", "region": "us-chicago-1", - "key_content": "private-key-content", + "private_key_content": "private-key-content", }, ) ) @@ -113,12 +126,12 @@ def test_connection_extra_cannot_control_hook_configuration(self): ( {}, {}, - "OCI API key authentication requires either 'key_file' or 'key_content'", + "OCI API key authentication requires either 'key_file' or 'private_key_content'", ), ( {"key_file": "/keys/oci.pem"}, - {"key_content": "private-key-content"}, - "OCI API key authentication cannot use both 'key_file' and 'key_content'", + {"private_key_content": "private-key-content"}, + "OCI API key authentication cannot use both 'key_file' and 'private_key_content'", ), ], ids=["missing-key", "conflicting-keys"], @@ -133,7 +146,7 @@ def test_get_oci_config_rejects_invalid_api_key_material(self, hook_kwargs, extr @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=OCI_AUTH_TYPE_CONFIG_FILE, + auth_type=OciAuthType.CONFIG_FILE, config_file="/config/oci", profile="AIRFLOW", ) @@ -159,12 +172,12 @@ def test_get_oci_config_from_file_with_region_override(self, mock_from_file): @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=OCI_AUTH_TYPE_CONFIG_FILE, + auth_type="config_file", config_file=config_file, profile=profile, ) mock_from_file.return_value = {"region": "us-ashburn-1"} - self.set_connection(Connection()) + self.set_missing_connection() config, signer = self.hook.get_oci_config() @@ -180,7 +193,7 @@ def test_get_oci_config_from_default_file(self, mock_from_file, config_file, pro autospec=True, ) def test_get_oci_config_with_instance_principal_and_connection_region(self, mock_signer_class): - self.hook = OciBaseHook(auth_type=OCI_AUTH_TYPE_INSTANCE_PRINCIPAL) + 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"})) @@ -195,10 +208,10 @@ def test_get_oci_config_with_instance_principal_and_connection_region(self, mock autospec=True, ) def test_get_oci_config_with_resource_principal_region(self, mock_get_signer): - self.hook = OciBaseHook(auth_type=OCI_AUTH_TYPE_RESOURCE_PRINCIPAL) + self.hook = OciBaseHook(auth_type=OciAuthType.RESOURCE_PRINCIPAL) signer = mock_get_signer.return_value signer.region = "us-phoenix-1" - self.set_connection(Connection()) + self.set_missing_connection() config, actual_signer = self.hook.get_oci_config() @@ -210,15 +223,25 @@ def test_get_oci_config_with_resource_principal_region(self, mock_get_signer): autospec=True, ) def test_get_oci_config_with_resource_principal_without_region(self, mock_get_signer): - self.hook = OciBaseHook(auth_type=OCI_AUTH_TYPE_RESOURCE_PRINCIPAL) + 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 - self.set_connection(Connection()) 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") @@ -227,47 +250,41 @@ def test_get_oci_config_rejects_unknown_auth_type(self): with pytest.raises(ValueError, match="Unsupported OCI authentication type: 'unknown'"): self.hook.get_oci_config() - def test_get_client_with_signer_and_explicit_endpoint(self): - signer = mock.sentinel.signer - 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=({"region": "us-chicago-1"}, signer) - ) - self.hook.service_endpoint = "https://generativeai.example.test/" - - result = self.hook.get_client(client_class, timeout=30) - - assert result is client - client_class.assert_called_once_with( - config={"region": "us-chicago-1"}, - signer=signer, - service_endpoint="https://generativeai.example.test", - timeout=30, - ) - - def test_get_client_without_signer_or_endpoint(self): + @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=({"region": "us-chicago-1"}, None) + self.hook.get_oci_config, return_value=(config, signer) ) - self.set_connection(Connection()) + self.hook.service_endpoint = service_endpoint - result = self.hook.get_client(client_class) + result = self.hook.get_client(client_class, **client_kwargs) assert result is client - client_class.assert_called_once_with(config={"region": "us-chicago-1"}) + client_class.assert_called_once_with(config=config, **expected_client_kwargs) def test_get_conn_requires_service_client_class(self): - with pytest.raises(ValueError, match="client_class must be specified by an OCI service hook"): + with pytest.raises(NotImplementedError, match="OCI service hooks must implement _get_client_class"): self.hook.get_conn() - def test_get_client_class_returns_configured_class(self): - self.hook.client_class = GenerativeAiClient - - assert self.hook._get_client_class() is GenerativeAiClient - @pytest.mark.parametrize("signer", [None, mock.sentinel.signer]) @mock.patch("oci.identity.IdentityClient", autospec=True) def test_connection_success(self, mock_identity_client, signer): @@ -314,15 +331,18 @@ def test_service_endpoint_is_controlled_by_hook_argument(self, hook_endpoint, ex assert self.hook._get_service_endpoint() == expected - def test_get_compartment_id_prefers_explicit_value(self): - self.set_connection(Connection(extra={"compartment_id": "connection-compartment"})) - - assert self.hook.get_compartment_id("explicit-compartment") == "explicit-compartment" - - def test_get_compartment_id_from_connection(self): + @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() == "connection-compartment" + assert self.hook.get_compartment_id(compartment_id) == expected def test_get_compartment_id_requires_value(self): self.set_connection(Connection()) @@ -340,11 +360,11 @@ def test_connection_form_widgets(self): assert set(widgets) == { "tenancy", "fingerprint", - "key_content", + "private_key_content", "region", "compartment_id", } - assert widgets["key_content"].field_class is password_field + assert widgets["private_key_content"].field_class is password_field def test_ui_field_behaviour(self): assert self.hook.get_ui_field_behaviour() == { @@ -374,7 +394,7 @@ def test_declarative_placeholders_match_legacy_hook(self): oci_connection["ui-field-behaviour"]["placeholders"] == self.hook.get_ui_field_behaviour()["placeholders"] ) - assert oci_connection["conn-fields"]["key_content"]["schema"]["format"] == "password" + assert oci_connection["conn-fields"]["private_key_content"]["schema"]["format"] == "password" def test_get_oci_sdk_requires_optional_extra(): @@ -391,13 +411,15 @@ def test_hook_modules_import_without_optional_oci_sdk(): [ sys.executable, "-c", - """ -import sys - -sys.modules["oci"] = None -import airflow.providers.oracle.hooks.base_oci -import airflow.providers.oracle.hooks.generative_ai -""", + dedent( + """ + import sys + + sys.modules["oci"] = None + import airflow.providers.oracle.hooks.base_oci + import airflow.providers.oracle.hooks.generative_ai + """ + ), ], check=True, ) From fee4859d081a8055a527f1a0c05c8990fafe9988 Mon Sep 17 00:00:00 2001 From: AlejandroMorgante <62363051+AlejandroMorgante@users.noreply.github.com> Date: Sun, 26 Jul 2026 03:38:19 -0300 Subject: [PATCH 07/14] Fix OCI Generative AI documentation registration The global integration registry accepts operator, sensor, and transfer guides only. The hook-specific guide remains available from the Oracle provider documentation index without being registered as a global how-to guide. --- providers/oracle/provider.yaml | 2 -- .../oracle/src/airflow/providers/oracle/get_provider_info.py | 1 - 2 files changed, 3 deletions(-) diff --git a/providers/oracle/provider.yaml b/providers/oracle/provider.yaml index c114e721d58a2..76f47b4fceadd 100644 --- a/providers/oracle/provider.yaml +++ b/providers/oracle/provider.yaml @@ -98,8 +98,6 @@ integrations: tags: [service] - integration-name: OCI Generative AI external-doc-url: https://docs.oracle.com/en-us/iaas/Content/generative-ai/home.htm - how-to-guide: - - /docs/apache-airflow-providers-oracle/generative_ai.rst logo: /docs/integration-logos/Oracle.png tags: [generative-ai, service] diff --git a/providers/oracle/src/airflow/providers/oracle/get_provider_info.py b/providers/oracle/src/airflow/providers/oracle/get_provider_info.py index 757b46cdf1ba5..47933b0e2da31 100644 --- a/providers/oracle/src/airflow/providers/oracle/get_provider_info.py +++ b/providers/oracle/src/airflow/providers/oracle/get_provider_info.py @@ -43,7 +43,6 @@ def get_provider_info(): { "integration-name": "OCI Generative AI", "external-doc-url": "https://docs.oracle.com/en-us/iaas/Content/generative-ai/home.htm", - "how-to-guide": ["/docs/apache-airflow-providers-oracle/generative_ai.rst"], "logo": "/docs/integration-logos/Oracle.png", "tags": ["generative-ai", "service"], }, From ce2dd3e1dfb0d248b909fadab7febad870ba9128 Mon Sep 17 00:00:00 2001 From: AlejandroMorgante <62363051+AlejandroMorgante@users.noreply.github.com> Date: Sun, 26 Jul 2026 06:55:31 -0300 Subject: [PATCH 08/14] Ensure Oracle records its common compat requirement The new OCI hooks rely on APIs newer than Oracle's recorded minimum. Without a release marker, provider release preparation cannot keep the direct dependency metadata aligned. --- providers/oracle/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/oracle/pyproject.toml b/providers/oracle/pyproject.toml index 39186d9735f10..278b4fac26f58 100644 --- a/providers/oracle/pyproject.toml +++ b/providers/oracle/pyproject.toml @@ -60,7 +60,7 @@ requires-python = ">=3.10" # 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.8.0", + "apache-airflow-providers-common-compat>=1.8.0", # use next version "apache-airflow-providers-common-sql>=1.32.0", "oracledb>=2.3.0", ] From c1e6a8bb81684f42cd82413a170678cc8123ce6d Mon Sep 17 00:00:00 2001 From: AlejandroMorgante <62363051+AlejandroMorgante@users.noreply.github.com> Date: Fri, 31 Jul 2026 15:03:27 -0300 Subject: [PATCH 09/14] Fix OCI hooks without optional connections or SDK Principal authentication does not require an Airflow connection, and missing-SDK coverage must remain active in minimal provider environments. --- .../providers/oracle/hooks/base_oci.py | 4 +- .../tests/unit/oracle/hooks/test_base_oci.py | 46 ++++------------ .../hooks/test_oci_optional_dependency.py | 55 +++++++++++++++++++ 3 files changed, 68 insertions(+), 37 deletions(-) create mode 100644 providers/oracle/tests/unit/oracle/hooks/test_oci_optional_dependency.py diff --git a/providers/oracle/src/airflow/providers/oracle/hooks/base_oci.py b/providers/oracle/src/airflow/providers/oracle/hooks/base_oci.py index 22b9bc1c176e4..1aa22d3bcd79c 100644 --- a/providers/oracle/src/airflow/providers/oracle/hooks/base_oci.py +++ b/providers/oracle/src/airflow/providers/oracle/hooks/base_oci.py @@ -232,7 +232,9 @@ def test_connection(self) -> tuple[bool, str]: 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.connection.extra_dejson.get("compartment_id") + 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." diff --git a/providers/oracle/tests/unit/oracle/hooks/test_base_oci.py b/providers/oracle/tests/unit/oracle/hooks/test_base_oci.py index d0599d07af06f..90dd7db0ed97d 100644 --- a/providers/oracle/tests/unit/oracle/hooks/test_base_oci.py +++ b/providers/oracle/tests/unit/oracle/hooks/test_base_oci.py @@ -16,23 +16,16 @@ # under the License. from __future__ import annotations -import subprocess -import sys -from textwrap import dedent from unittest import mock import pytest from airflow.models import Connection -from airflow.providers.common.compat.sdk import ( - AirflowNotFoundException, - AirflowOptionalProviderFeatureException, -) +from airflow.providers.common.compat.sdk import AirflowNotFoundException from airflow.providers.oracle.get_provider_info import get_provider_info from airflow.providers.oracle.hooks.base_oci import ( OciAuthType, OciBaseHook, - _get_oci_sdk, ) GenerativeAiClient = pytest.importorskip("oci.generative_ai").GenerativeAiClient @@ -350,6 +343,15 @@ def test_get_compartment_id_requires_value(self): 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") @@ -395,31 +397,3 @@ def test_declarative_placeholders_match_legacy_hook(self): == self.hook.get_ui_field_behaviour()["placeholders"] ) assert oci_connection["conn-fields"]["private_key_content"]["schema"]["format"] == "password" - - -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-oracle\[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.oracle.hooks.base_oci - import airflow.providers.oracle.hooks.generative_ai - """ - ), - ], - check=True, - ) diff --git a/providers/oracle/tests/unit/oracle/hooks/test_oci_optional_dependency.py b/providers/oracle/tests/unit/oracle/hooks/test_oci_optional_dependency.py new file mode 100644 index 0000000000000..75e684b5053f1 --- /dev/null +++ b/providers/oracle/tests/unit/oracle/hooks/test_oci_optional_dependency.py @@ -0,0 +1,55 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +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.oracle.hooks.base_oci 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-oracle\[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.oracle.hooks.base_oci + import airflow.providers.oracle.hooks.generative_ai + """ + ), + ], + check=True, + ) From b497480000868fdc1628e24e6437bd17c4c2fb53 Mon Sep 17 00:00:00 2001 From: AlejandroMorgante <62363051+AlejandroMorgante@users.noreply.github.com> Date: Sat, 1 Aug 2026 16:09:07 -0300 Subject: [PATCH 10/14] Require compatible common-compat version for Oracle hooks The OCI hooks rely on compatibility exports introduced in common-compat 1.12.0, so older accepted installations would fail during import. --- providers/oracle/README.rst | 2 +- providers/oracle/docs/index.rst | 2 +- providers/oracle/pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/providers/oracle/README.rst b/providers/oracle/README.rst index 1d41b1299b794..e981061f016c8 100644 --- a/providers/oracle/README.rst +++ b/providers/oracle/README.rst @@ -54,7 +54,7 @@ Requirements PIP package Version required ========================================== ================== ``apache-airflow`` ``>=2.11.0`` -``apache-airflow-providers-common-compat`` ``>=1.8.0`` +``apache-airflow-providers-common-compat`` ``>=1.12.0`` ``apache-airflow-providers-common-sql`` ``>=1.32.0`` ``oracledb`` ``>=2.3.0`` ========================================== ================== diff --git a/providers/oracle/docs/index.rst b/providers/oracle/docs/index.rst index 4690b6f72451a..655ae129c8bc9 100644 --- a/providers/oracle/docs/index.rst +++ b/providers/oracle/docs/index.rst @@ -104,7 +104,7 @@ The minimum Apache Airflow version supported by this provider distribution is `` PIP package Version required ========================================== ================== ``apache-airflow`` ``>=2.11.0`` -``apache-airflow-providers-common-compat`` ``>=1.8.0`` +``apache-airflow-providers-common-compat`` ``>=1.12.0`` ``apache-airflow-providers-common-sql`` ``>=1.32.0`` ``oracledb`` ``>=2.3.0`` ========================================== ================== diff --git a/providers/oracle/pyproject.toml b/providers/oracle/pyproject.toml index 278b4fac26f58..0ca1d7e6f8233 100644 --- a/providers/oracle/pyproject.toml +++ b/providers/oracle/pyproject.toml @@ -60,7 +60,7 @@ requires-python = ">=3.10" # 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.8.0", # use next version + "apache-airflow-providers-common-compat>=1.12.0", "apache-airflow-providers-common-sql>=1.32.0", "oracledb>=2.3.0", ] From 39e50b725ffaec63f0309b76e9b2296fd908647c Mon Sep 17 00:00:00 2001 From: AlejandroMorgante <62363051+AlejandroMorgante@users.noreply.github.com> Date: Wed, 5 Aug 2026 22:23:19 -0300 Subject: [PATCH 11/14] Add dedicated Oracle Cloud Infrastructure provider Separating cloud integrations from Oracle Database improves discoverability and lets OCI users avoid the database provider's dependencies. --- .../ISSUE_TEMPLATE/1-airflow_bug_report.yml | 1 + .github/boring-cyborg.yml | 3 + airflow-core/docs/extra-packages-ref.rst | 2 + providers/oci/.gitignore | 1 + providers/oci/LICENSE | 201 ++++++++++++++++++ providers/oci/NOTICE | 5 + providers/oci/README.rst | 70 ++++++ providers/oci/docs/changelog.rst | 26 +++ providers/oci/docs/commits.rst | 34 +++ providers/oci/docs/conf.py | 27 +++ .../{oracle => oci}/docs/connections/oci.rst | 2 +- .../{oracle => oci}/docs/generative_ai.rst | 8 +- providers/oci/docs/index.rst | 126 +++++++++++ .../installing-providers-from-sources.rst | 18 ++ providers/oci/docs/security.rst | 18 ++ providers/oci/provider.yaml | 101 +++++++++ providers/oci/pyproject.toml | 132 ++++++++++++ providers/oci/src/airflow/__init__.py | 17 ++ .../oci/src/airflow/providers/__init__.py | 17 ++ .../oci/src/airflow/providers/oci/__init__.py | 39 ++++ .../providers/oci/get_provider_info.py | 80 +++++++ .../airflow/providers/oci/hooks/__init__.py | 16 ++ .../src/airflow/providers/oci/hooks/base.py} | 2 +- .../providers/oci}/hooks/generative_ai.py | 2 +- providers/oci/tests/conftest.py | 19 ++ providers/oci/tests/system/__init__.py | 17 ++ providers/oci/tests/system/oci/__init__.py | 16 ++ providers/oci/tests/unit/__init__.py | 17 ++ providers/oci/tests/unit/oci/__init__.py | 16 ++ .../oci/tests/unit/oci/hooks/__init__.py | 16 ++ .../tests/unit/oci/hooks/test_base.py} | 4 +- .../unit/oci}/hooks/test_generative_ai.py | 2 +- .../oci/hooks/test_optional_dependency.py} | 8 +- providers/oracle/README.rst | 2 +- providers/oracle/docs/index.rst | 12 +- providers/oracle/provider.yaml | 67 +----- providers/oracle/pyproject.toml | 6 +- .../providers/oracle/get_provider_info.py | 55 +---- pyproject.toml | 10 + scripts/ci/docker-compose/remove-sources.yml | 1 + scripts/ci/docker-compose/tests-sources.yml | 1 + uv.lock | 70 ++++-- 42 files changed, 1130 insertions(+), 157 deletions(-) create mode 100644 providers/oci/.gitignore create mode 100644 providers/oci/LICENSE create mode 100644 providers/oci/NOTICE create mode 100644 providers/oci/README.rst create mode 100644 providers/oci/docs/changelog.rst create mode 100644 providers/oci/docs/commits.rst create mode 100644 providers/oci/docs/conf.py rename providers/{oracle => oci}/docs/connections/oci.rst (99%) rename providers/{oracle => oci}/docs/generative_ai.rst (91%) create mode 100644 providers/oci/docs/index.rst create mode 100644 providers/oci/docs/installing-providers-from-sources.rst create mode 100644 providers/oci/docs/security.rst create mode 100644 providers/oci/provider.yaml create mode 100644 providers/oci/pyproject.toml create mode 100644 providers/oci/src/airflow/__init__.py create mode 100644 providers/oci/src/airflow/providers/__init__.py create mode 100644 providers/oci/src/airflow/providers/oci/__init__.py create mode 100644 providers/oci/src/airflow/providers/oci/get_provider_info.py create mode 100644 providers/oci/src/airflow/providers/oci/hooks/__init__.py rename providers/{oracle/src/airflow/providers/oracle/hooks/base_oci.py => oci/src/airflow/providers/oci/hooks/base.py} (99%) rename providers/{oracle/src/airflow/providers/oracle => oci/src/airflow/providers/oci}/hooks/generative_ai.py (95%) create mode 100644 providers/oci/tests/conftest.py create mode 100644 providers/oci/tests/system/__init__.py create mode 100644 providers/oci/tests/system/oci/__init__.py create mode 100644 providers/oci/tests/unit/__init__.py create mode 100644 providers/oci/tests/unit/oci/__init__.py create mode 100644 providers/oci/tests/unit/oci/hooks/__init__.py rename providers/{oracle/tests/unit/oracle/hooks/test_base_oci.py => oci/tests/unit/oci/hooks/test_base.py} (99%) rename providers/{oracle/tests/unit/oracle => oci/tests/unit/oci}/hooks/test_generative_ai.py (94%) rename providers/{oracle/tests/unit/oracle/hooks/test_oci_optional_dependency.py => oci/tests/unit/oci/hooks/test_optional_dependency.py} (85%) 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/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/oracle/docs/connections/oci.rst b/providers/oci/docs/connections/oci.rst similarity index 99% rename from providers/oracle/docs/connections/oci.rst rename to providers/oci/docs/connections/oci.rst index 1601e1cb57694..0a967928f18ab 100644 --- a/providers/oracle/docs/connections/oci.rst +++ b/providers/oci/docs/connections/oci.rst @@ -30,7 +30,7 @@ this connection or its service hooks: .. code-block:: bash - pip install 'apache-airflow-providers-oracle[oci]' + pip install 'apache-airflow-providers-oci[oci]' The default connection ID is ``oci_default``. diff --git a/providers/oracle/docs/generative_ai.rst b/providers/oci/docs/generative_ai.rst similarity index 91% rename from providers/oracle/docs/generative_ai.rst rename to providers/oci/docs/generative_ai.rst index 73682e4cb392c..65a65432f1ff5 100644 --- a/providers/oracle/docs/generative_ai.rst +++ b/providers/oci/docs/generative_ai.rst @@ -18,11 +18,11 @@ OCI Generative AI Hosted Applications ===================================== -:class:`~airflow.providers.oracle.hooks.generative_ai.OciGenerativeAIHook` uses the official +:class:`~airflow.providers.oci.hooks.generative_ai.OciGenerativeAIHook` uses the official `OCI Python SDK `__ to manage `Hosted Applications and deployments `__. -Install ``apache-airflow-providers-oracle[oci]`` before using the hook. Configure an +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 @@ -58,7 +58,7 @@ and OAuth settings: .. code-block:: python - from airflow.providers.oracle.hooks.generative_ai import OciGenerativeAIHook + from airflow.providers.oci.hooks.generative_ai import OciGenerativeAIHook hook = OciGenerativeAIHook(oci_conn_id="oci_default") response = hook.conn.create_hosted_application(...) @@ -71,7 +71,7 @@ OCI IAM applications do not require an OAuth or identity domain configuration: .. code-block:: python - from airflow.providers.oracle.hooks.generative_ai import OciGenerativeAIHook + from airflow.providers.oci.hooks.generative_ai import OciGenerativeAIHook hook = OciGenerativeAIHook(oci_conn_id="oci_default") response = hook.conn.create_hosted_application_iam(...) 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..2ac513dbcaa31 --- /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: 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..6e43721ca3fc2 --- /dev/null +++ b/providers/oci/pyproject.toml @@ -0,0 +1,132 @@ +# 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" = [ + "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/oracle/src/airflow/providers/oracle/hooks/base_oci.py b/providers/oci/src/airflow/providers/oci/hooks/base.py similarity index 99% rename from providers/oracle/src/airflow/providers/oracle/hooks/base_oci.py rename to providers/oci/src/airflow/providers/oci/hooks/base.py index 1aa22d3bcd79c..a1e7ade9de7a1 100644 --- a/providers/oracle/src/airflow/providers/oracle/hooks/base_oci.py +++ b/providers/oci/src/airflow/providers/oci/hooks/base.py @@ -50,7 +50,7 @@ def _get_oci_sdk() -> Any: except ImportError as e: raise AirflowOptionalProviderFeatureException( "OCI features require the optional OCI Python SDK. " - "Install it with: pip install 'apache-airflow-providers-oracle[oci]'" + "Install it with: pip install 'apache-airflow-providers-oci[oci]'" ) from e return oci diff --git a/providers/oracle/src/airflow/providers/oracle/hooks/generative_ai.py b/providers/oci/src/airflow/providers/oci/hooks/generative_ai.py similarity index 95% rename from providers/oracle/src/airflow/providers/oracle/hooks/generative_ai.py rename to providers/oci/src/airflow/providers/oci/hooks/generative_ai.py index a948ab1ec11d1..bad2341385f88 100644 --- a/providers/oracle/src/airflow/providers/oracle/hooks/generative_ai.py +++ b/providers/oci/src/airflow/providers/oci/hooks/generative_ai.py @@ -19,7 +19,7 @@ from collections.abc import Callable from typing import TYPE_CHECKING -from airflow.providers.oracle.hooks.base_oci import OciBaseHook, _get_oci_sdk +from airflow.providers.oci.hooks.base import OciBaseHook, _get_oci_sdk if TYPE_CHECKING: from oci.generative_ai import 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/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/oracle/tests/unit/oracle/hooks/test_base_oci.py b/providers/oci/tests/unit/oci/hooks/test_base.py similarity index 99% rename from providers/oracle/tests/unit/oracle/hooks/test_base_oci.py rename to providers/oci/tests/unit/oci/hooks/test_base.py index 90dd7db0ed97d..8a8ac4c6f3518 100644 --- a/providers/oracle/tests/unit/oracle/hooks/test_base_oci.py +++ b/providers/oci/tests/unit/oci/hooks/test_base.py @@ -22,8 +22,8 @@ from airflow.models import Connection from airflow.providers.common.compat.sdk import AirflowNotFoundException -from airflow.providers.oracle.get_provider_info import get_provider_info -from airflow.providers.oracle.hooks.base_oci import ( +from airflow.providers.oci.get_provider_info import get_provider_info +from airflow.providers.oci.hooks.base import ( OciAuthType, OciBaseHook, ) diff --git a/providers/oracle/tests/unit/oracle/hooks/test_generative_ai.py b/providers/oci/tests/unit/oci/hooks/test_generative_ai.py similarity index 94% rename from providers/oracle/tests/unit/oracle/hooks/test_generative_ai.py rename to providers/oci/tests/unit/oci/hooks/test_generative_ai.py index 461337536421f..72467c14328a1 100644 --- a/providers/oracle/tests/unit/oracle/hooks/test_generative_ai.py +++ b/providers/oci/tests/unit/oci/hooks/test_generative_ai.py @@ -20,7 +20,7 @@ import pytest -from airflow.providers.oracle.hooks.generative_ai import OciGenerativeAIHook +from airflow.providers.oci.hooks.generative_ai import OciGenerativeAIHook GenerativeAiClient = pytest.importorskip("oci.generative_ai").GenerativeAiClient diff --git a/providers/oracle/tests/unit/oracle/hooks/test_oci_optional_dependency.py b/providers/oci/tests/unit/oci/hooks/test_optional_dependency.py similarity index 85% rename from providers/oracle/tests/unit/oracle/hooks/test_oci_optional_dependency.py rename to providers/oci/tests/unit/oci/hooks/test_optional_dependency.py index 75e684b5053f1..78f328a4687d3 100644 --- a/providers/oracle/tests/unit/oracle/hooks/test_oci_optional_dependency.py +++ b/providers/oci/tests/unit/oci/hooks/test_optional_dependency.py @@ -24,14 +24,14 @@ import pytest from airflow.providers.common.compat.sdk import AirflowOptionalProviderFeatureException -from airflow.providers.oracle.hooks.base_oci import _get_oci_sdk +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-oracle\[oci\]'", + match=r"pip install 'apache-airflow-providers-oci\[oci\]'", ): _get_oci_sdk() @@ -46,8 +46,8 @@ def test_hook_modules_import_without_optional_oci_sdk(): import sys sys.modules["oci"] = None - import airflow.providers.oracle.hooks.base_oci - import airflow.providers.oracle.hooks.generative_ai + import airflow.providers.oci.hooks.base + import airflow.providers.oci.hooks.generative_ai """ ), ], diff --git a/providers/oracle/README.rst b/providers/oracle/README.rst index e981061f016c8..1d41b1299b794 100644 --- a/providers/oracle/README.rst +++ b/providers/oracle/README.rst @@ -54,7 +54,7 @@ Requirements PIP package Version required ========================================== ================== ``apache-airflow`` ``>=2.11.0`` -``apache-airflow-providers-common-compat`` ``>=1.12.0`` +``apache-airflow-providers-common-compat`` ``>=1.8.0`` ``apache-airflow-providers-common-sql`` ``>=1.32.0`` ``oracledb`` ``>=2.3.0`` ========================================== ================== diff --git a/providers/oracle/docs/index.rst b/providers/oracle/docs/index.rst index 655ae129c8bc9..def6b48c2c2d9 100644 --- a/providers/oracle/docs/index.rst +++ b/providers/oracle/docs/index.rst @@ -34,9 +34,7 @@ :maxdepth: 1 :caption: Guides - Oracle Database connection - Oracle Cloud Infrastructure connection - OCI Generative AI + Connection types Operators .. toctree:: @@ -76,8 +74,7 @@ apache-airflow-providers-oracle package ------------------------------------------------------ -`Oracle Database `__ and -`Oracle Cloud Infrastructure `__ integrations. +`Oracle `__ Release: 4.6.2 @@ -104,7 +101,7 @@ The minimum Apache Airflow version supported by this provider distribution is `` PIP package Version required ========================================== ================== ``apache-airflow`` ``>=2.11.0`` -``apache-airflow-providers-common-compat`` ``>=1.12.0`` +``apache-airflow-providers-common-compat`` ``>=1.8.0`` ``apache-airflow-providers-common-sql`` ``>=1.32.0`` ``oracledb`` ``>=2.3.0`` ========================================== ================== @@ -136,13 +133,12 @@ Install them when installing from PyPI. For example: .. code-block:: bash - pip install apache-airflow-providers-oracle[oci] + pip install apache-airflow-providers-oracle[numpy] =============== ============================================================================================================================================================================================================================================ Extra Dependencies =============== ============================================================================================================================================================================================================================================ -``oci`` ``oci>=2.182.0`` ``numpy`` ``numpy>=1.22.4; python_version<'3.11'``, ``numpy>=1.23.2; python_version=='3.11'``, ``numpy>=1.26.0; python_version=='3.12'``, ``numpy>=2.1.0; python_version>='3.13' and python_version<'3.14'``, ``numpy>=2.4.3; python_version>='3.14'`` ``openlineage`` ``apache-airflow-providers-openlineage`` =============== ============================================================================================================================================================================================================================================ diff --git a/providers/oracle/provider.yaml b/providers/oracle/provider.yaml index 76f47b4fceadd..97cfe31aa01cb 100644 --- a/providers/oracle/provider.yaml +++ b/providers/oracle/provider.yaml @@ -19,8 +19,7 @@ package-name: apache-airflow-providers-oracle name: Oracle description: | - `Oracle Database `__ and - `Oracle Cloud Infrastructure `__ integrations. + `Oracle `__ state: ready lifecycle: production @@ -92,14 +91,6 @@ integrations: - /docs/apache-airflow-providers-oracle/operators.rst logo: /docs/integration-logos/Oracle.png tags: [software] - - integration-name: Oracle Cloud Infrastructure - external-doc-url: https://docs.oracle.com/en-us/iaas/Content/home.htm - logo: /docs/integration-logos/Oracle.png - tags: [service] - - integration-name: OCI Generative AI - external-doc-url: https://docs.oracle.com/en-us/iaas/Content/generative-ai/home.htm - logo: /docs/integration-logos/Oracle.png - tags: [generative-ai, service] operators: - integration-name: Oracle @@ -125,12 +116,6 @@ hooks: python-modules: - airflow.providers.oracle.hooks.handlers - airflow.providers.oracle.hooks.oracle - - integration-name: Oracle Cloud Infrastructure - python-modules: - - airflow.providers.oracle.hooks.base_oci - - integration-name: OCI Generative AI - python-modules: - - airflow.providers.oracle.hooks.generative_ai transfers: - source-integration-name: Oracle @@ -141,53 +126,3 @@ connection-types: - hook-class-name: airflow.providers.oracle.hooks.oracle.OracleHook hook-name: "Oracle" connection-type: oracle - - hook-class-name: airflow.providers.oracle.hooks.base_oci.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/oracle/pyproject.toml b/providers/oracle/pyproject.toml index 0ca1d7e6f8233..d23df241f91eb 100644 --- a/providers/oracle/pyproject.toml +++ b/providers/oracle/pyproject.toml @@ -60,7 +60,7 @@ requires-python = ">=3.10" # 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", + "apache-airflow-providers-common-compat>=1.8.0", "apache-airflow-providers-common-sql>=1.32.0", "oracledb>=2.3.0", ] @@ -68,9 +68,6 @@ dependencies = [ # 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" = [ - "oci>=2.182.0", -] "numpy" = [ "numpy>=1.22.4; python_version<'3.11'", "numpy>=1.23.2; python_version=='3.11'", @@ -91,7 +88,6 @@ dev = [ "apache-airflow-providers-common-sql", "apache-airflow-providers-openlineage", # Additional devel dependencies (do not remove this line and add extra development dependencies) - "apache-airflow-providers-oracle[oci]", "numpy>=1.22.4; python_version<'3.11'", "numpy>=1.23.2; python_version=='3.11'", "numpy>=1.26.0; python_version=='3.12'", diff --git a/providers/oracle/src/airflow/providers/oracle/get_provider_info.py b/providers/oracle/src/airflow/providers/oracle/get_provider_info.py index 47933b0e2da31..d9cf0004700ac 100644 --- a/providers/oracle/src/airflow/providers/oracle/get_provider_info.py +++ b/providers/oracle/src/airflow/providers/oracle/get_provider_info.py @@ -25,7 +25,7 @@ def get_provider_info(): return { "package-name": "apache-airflow-providers-oracle", "name": "Oracle", - "description": "`Oracle Database `__ and\n`Oracle Cloud Infrastructure `__ integrations.\n", + "description": "`Oracle `__\n", "integrations": [ { "integration-name": "Oracle", @@ -33,19 +33,7 @@ def get_provider_info(): "how-to-guide": ["/docs/apache-airflow-providers-oracle/operators.rst"], "logo": "/docs/integration-logos/Oracle.png", "tags": ["software"], - }, - { - "integration-name": "Oracle Cloud Infrastructure", - "external-doc-url": "https://docs.oracle.com/en-us/iaas/Content/home.htm", - "logo": "/docs/integration-logos/Oracle.png", - "tags": ["service"], - }, - { - "integration-name": "OCI Generative AI", - "external-doc-url": "https://docs.oracle.com/en-us/iaas/Content/generative-ai/home.htm", - "logo": "/docs/integration-logos/Oracle.png", - "tags": ["generative-ai", "service"], - }, + } ], "operators": [ {"integration-name": "Oracle", "python-modules": ["airflow.providers.oracle.operators.oracle"]} @@ -73,15 +61,7 @@ def get_provider_info(): "airflow.providers.oracle.hooks.handlers", "airflow.providers.oracle.hooks.oracle", ], - }, - { - "integration-name": "Oracle Cloud Infrastructure", - "python-modules": ["airflow.providers.oracle.hooks.base_oci"], - }, - { - "integration-name": "OCI Generative AI", - "python-modules": ["airflow.providers.oracle.hooks.generative_ai"], - }, + } ], "transfers": [ { @@ -95,33 +75,6 @@ def get_provider_info(): "hook-class-name": "airflow.providers.oracle.hooks.oracle.OracleHook", "hook-name": "Oracle", "connection-type": "oracle", - }, - { - "hook-class-name": "airflow.providers.oracle.hooks.base_oci.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/pyproject.toml b/pyproject.toml index 3a1b9d360f3d8..d8933d0fdcdf5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -293,6 +293,9 @@ apache-airflow = "airflow.__main__:main" "neo4j" = [ "apache-airflow-providers-neo4j>=3.8.0" ] +"oci" = [ + "apache-airflow-providers-oci>=0.1.0" +] "odbc" = [ "apache-airflow-providers-odbc>=4.8.0" ] @@ -472,6 +475,7 @@ apache-airflow = "airflow.__main__:main" "apache-airflow-providers-mongo>=4.2.2", "apache-airflow-providers-mysql>=5.7.2", "apache-airflow-providers-neo4j>=3.8.0", + "apache-airflow-providers-oci>=0.1.0", "apache-airflow-providers-odbc>=4.8.0", "apache-airflow-providers-openai>=1.5.0", "apache-airflow-providers-openfaas>=3.7.0", @@ -1218,6 +1222,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 +1502,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 +1655,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 +1819,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 +1961,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/uv.lock b/uv.lock index 1604efd370901..90b328764f1b0 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", @@ -1066,6 +1068,7 @@ all = [ { name = "apache-airflow-providers-mongo" }, { name = "apache-airflow-providers-mysql" }, { name = "apache-airflow-providers-neo4j" }, + { name = "apache-airflow-providers-oci" }, { name = "apache-airflow-providers-odbc" }, { name = "apache-airflow-providers-openai" }, { name = "apache-airflow-providers-openfaas" }, @@ -1340,6 +1343,9 @@ mysql = [ neo4j = [ { name = "apache-airflow-providers-neo4j" }, ] +oci = [ + { name = "apache-airflow-providers-oci" }, +] odbc = [ { name = "apache-airflow-providers-odbc" }, ] @@ -1696,6 +1702,8 @@ requires-dist = [ { name = "apache-airflow-providers-mysql", marker = "extra == 'mysql'", editable = "providers/mysql" }, { name = "apache-airflow-providers-neo4j", marker = "extra == 'all'", editable = "providers/neo4j" }, { name = "apache-airflow-providers-neo4j", marker = "extra == 'neo4j'", editable = "providers/neo4j" }, + { name = "apache-airflow-providers-oci", marker = "extra == 'all'", editable = "providers/oci" }, + { name = "apache-airflow-providers-oci", marker = "extra == 'oci'", editable = "providers/oci" }, { name = "apache-airflow-providers-odbc", marker = "extra == 'all'", editable = "providers/odbc" }, { name = "apache-airflow-providers-odbc", marker = "extra == 'odbc'", editable = "providers/odbc" }, { name = "apache-airflow-providers-openai", marker = "extra == 'all'", editable = "providers/openai" }, @@ -1780,7 +1788,7 @@ requires-dist = [ { name = "sentry-sdk", marker = "extra == 'sentry'", specifier = ">=2.30.0" }, { name = "uv", marker = "extra == 'uv'", specifier = ">=0.11.29" }, ] -provides-extras = ["all-core", "async", "graphviz", "gunicorn", "kerberos", "memray", "otel", "statsd", "all-task-sdk", "airbyte", "akeyless", "alibaba", "amazon", "anthropic", "apache-cassandra", "apache-drill", "apache-druid", "apache-flink", "apache-hdfs", "apache-hive", "apache-iceberg", "apache-impala", "apache-kafka", "apache-kylin", "apache-livy", "apache-pig", "apache-pinot", "apache-spark", "apache-tinkerpop", "apprise", "arangodb", "asana", "atlassian-jira", "celery", "clickhousedb", "cloudant", "cncf-kubernetes", "cohere", "common-ai", "common-compat", "common-io", "common-messaging", "common-sql", "databricks", "datadog", "dbt-cloud", "dingding", "discord", "docker", "edge3", "elasticsearch", "exasol", "fab", "facebook", "ftp", "git", "github", "google", "grpc", "hashicorp", "http", "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", "all", "aiobotocore", "apache-atlas", "apache-webhdfs", "amazon-aws-auth", "cloudpickle", "github-enterprise", "google-auth", "ldap", "pandas", "polars", "rabbitmq", "sentry", "s3fs", "uv"] +provides-extras = ["all-core", "async", "graphviz", "gunicorn", "kerberos", "memray", "otel", "statsd", "all-task-sdk", "airbyte", "akeyless", "alibaba", "amazon", "anthropic", "apache-cassandra", "apache-drill", "apache-druid", "apache-flink", "apache-hdfs", "apache-hive", "apache-iceberg", "apache-impala", "apache-kafka", "apache-kylin", "apache-livy", "apache-pig", "apache-pinot", "apache-spark", "apache-tinkerpop", "apprise", "arangodb", "asana", "atlassian-jira", "celery", "clickhousedb", "cloudant", "cncf-kubernetes", "cohere", "common-ai", "common-compat", "common-io", "common-messaging", "common-sql", "databricks", "datadog", "dbt-cloud", "dingding", "discord", "docker", "edge3", "elasticsearch", "exasol", "fab", "facebook", "ftp", "git", "github", "google", "grpc", "hashicorp", "http", "imap", "influxdb", "informatica", "jdbc", "jenkins", "keycloak", "microsoft-azure", "microsoft-mssql", "microsoft-psrp", "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", "all", "aiobotocore", "apache-atlas", "apache-webhdfs", "amazon-aws-auth", "cloudpickle", "github-enterprise", "google-auth", "ldap", "pandas", "polars", "rabbitmq", "sentry", "s3fs", "uv"] [package.metadata.requires-dev] ci-image = [ @@ -6684,6 +6692,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" @@ -6950,9 +7002,6 @@ numpy = [ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] -oci = [ - { name = "oci" }, -] openlineage = [ { name = "apache-airflow-providers-openlineage" }, ] @@ -6964,7 +7013,6 @@ dev = [ { name = "apache-airflow-providers-common-compat" }, { name = "apache-airflow-providers-common-sql" }, { name = "apache-airflow-providers-openlineage" }, - { name = "apache-airflow-providers-oracle", extra = ["oci"] }, { name = "apache-airflow-task-sdk" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, @@ -6985,10 +7033,9 @@ requires-dist = [ { name = "numpy", marker = "python_full_version == '3.12.*' and extra == 'numpy'", specifier = ">=1.26.0" }, { name = "numpy", marker = "python_full_version == '3.13.*' and extra == 'numpy'", specifier = ">=2.1.0" }, { name = "numpy", marker = "python_full_version >= '3.14' and extra == 'numpy'", specifier = ">=2.4.3" }, - { name = "oci", marker = "extra == 'oci'", specifier = ">=2.182.0" }, { name = "oracledb", specifier = ">=2.3.0" }, ] -provides-extras = ["oci", "numpy", "openlineage"] +provides-extras = ["numpy", "openlineage"] [package.metadata.requires-dev] dev = [ @@ -6997,7 +7044,6 @@ dev = [ { name = "apache-airflow-providers-common-compat", editable = "providers/common/compat" }, { name = "apache-airflow-providers-common-sql", editable = "providers/common/sql" }, { name = "apache-airflow-providers-openlineage", editable = "providers/openlineage" }, - { name = "apache-airflow-providers-oracle", extras = ["oci"], editable = "providers/oracle" }, { name = "apache-airflow-task-sdk", editable = "task-sdk" }, { name = "numpy", marker = "python_full_version < '3.11'", specifier = ">=1.22.4" }, { name = "numpy", marker = "python_full_version == '3.11.*'", specifier = ">=1.23.2" }, @@ -17521,7 +17567,7 @@ wheels = [ [[package]] name = "oci" -version = "2.182.0" +version = "2.183.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -17534,9 +17580,9 @@ dependencies = [ { name = "pytz" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/20/769e5fef5006ca00b0917fc7a60d143af856ace4320af567fe83ab47f72b/oci-2.182.0.tar.gz", hash = "sha256:effdd24f808179cfa15ba1084181cadda8e09332699cdbcd2f78ebbd0a2d072e", size = 17631582, upload-time = "2026-07-14T07:21:19.742Z" } +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/2f/b2/bd2ed4b1f29b1afe1565dd19e9cc0420fede82b9aa1c409361c18d84c554/oci-2.182.0-py3-none-any.whl", hash = "sha256:f8d7d675d1fe75721dac9aa97a11bf8b670a1814afd4969b7023fef8e3ce57d2", size = 35969385, upload-time = "2026-07-14T07:21:11.372Z" }, + { 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]] From e774042b6a4add8a288fc2361a8590813e7830ec Mon Sep 17 00:00:00 2001 From: AlejandroMorgante <62363051+AlejandroMorgante@users.noreply.github.com> Date: Thu, 6 Aug 2026 00:19:29 -0300 Subject: [PATCH 12/14] Make Oracle Cloud Infrastructure provider safe to install Connection-managed regions must not redirect signed OCI requests, while the OCI SDK's LGPL-transitive dependency must remain an explicit optional installation. --- dev/breeze/doc/images/output_build-docs.svg | 8 +-- dev/breeze/doc/images/output_build-docs.txt | 2 +- ...release-management_add-back-references.svg | 8 +-- ...release-management_add-back-references.txt | 2 +- ...e-management_classify-provider-changes.svg | 2 +- ...e-management_classify-provider-changes.txt | 2 +- ...ement_generate-issue-content-providers.svg | 2 +- ...ement_generate-issue-content-providers.txt | 2 +- ...management_generate-providers-metadata.svg | 10 ++-- ...management_generate-providers-metadata.txt | 2 +- ...agement_prepare-provider-distributions.svg | 2 +- ...agement_prepare-provider-distributions.txt | 2 +- ...agement_prepare-provider-documentation.svg | 2 +- ...agement_prepare-provider-documentation.txt | 2 +- ...output_release-management_publish-docs.svg | 8 +-- ...output_release-management_publish-docs.txt | 2 +- ...t_sbom_generate-providers-requirements.svg | 10 ++-- ...t_sbom_generate-providers-requirements.txt | 2 +- .../output_workflow-run_publish-docs.svg | 8 +-- .../output_workflow-run_publish-docs.txt | 2 +- docs/spelling_wordlist.txt | 1 + providers/oci/docs/connections/oci.rst | 13 +++-- providers/oci/docs/generative_ai.rst | 3 +- providers/oci/pyproject.toml | 2 + .../src/airflow/providers/oci/hooks/base.py | 25 +++++++-- .../system/oci/example_oci_generative_ai.py | 52 +++++++++++++++++++ .../oci/tests/unit/oci/hooks/test_base.py | 26 ++++++++++ pyproject.toml | 2 +- .../ci/prek/update_airflow_pyproject_toml.py | 18 ++++++- uv.lock | 4 +- 30 files changed, 171 insertions(+), 55 deletions(-) create mode 100644 providers/oci/tests/system/oci/example_oci_generative_ai.py 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_classify-provider-changes.svg b/dev/breeze/doc/images/output_release-management_classify-provider-changes.svg index cef6e4d3b1d14..c8d32b0e58572 100644 --- a/dev/breeze/doc/images/output_release-management_classify-provider-changes.svg +++ b/dev/breeze/doc/images/output_release-management_classify-provider-changes.svg @@ -168,7 +168,7 @@ clickhousedb | cloudant | cncf.kubernetes | cohere | common.ai | common.compat | common.io | common.messaging |        common.sql | databricks | datadog | dbt.cloud | dingding | discord | docker | edge3 | elasticsearch | exasol | fab |   facebook | ftp | git | github | google | grpc | hashicorp | http | imap | influxdb | informatica | jdbc | jenkins |    -keycloak | microsoft.azure | microsoft.mssql | microsoft.psrp | microsoft.winrm | mongo | mysql | neo4j | odbc |       +keycloak | microsoft.azure | microsoft.mssql | microsoft.psrp | 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 |    diff --git a/dev/breeze/doc/images/output_release-management_classify-provider-changes.txt b/dev/breeze/doc/images/output_release-management_classify-provider-changes.txt index 6e02c700afdf5..89102334dc482 100644 --- a/dev/breeze/doc/images/output_release-management_classify-provider-changes.txt +++ b/dev/breeze/doc/images/output_release-management_classify-provider-changes.txt @@ -1 +1 @@ -4cac13b21eee8b732a46c5a15aec7a4b +de1e7d40d1467ebeb47f77976d39a03b diff --git a/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.svg b/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.svg index 2f9692be54fa0..3915eb99973ac 100644 --- a/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.svg +++ b/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.svg @@ -157,7 +157,7 @@ clickhousedb | cloudant | cncf.kubernetes | cohere | common.ai | common.compat | common.io | common.messaging |        common.sql | databricks | datadog | dbt.cloud | dingding | discord | docker | edge3 | elasticsearch | exasol | fab |   facebook | ftp | git | github | google | grpc | hashicorp | http | imap | influxdb | informatica | jdbc | jenkins |    -keycloak | microsoft.azure | microsoft.mssql | microsoft.psrp | microsoft.winrm | mongo | mysql | neo4j | odbc |       +keycloak | microsoft.azure | microsoft.mssql | microsoft.psrp | 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 |    diff --git a/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.txt b/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.txt index 6afb289b5854f..652ee6574399b 100644 --- a/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.txt +++ b/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.txt @@ -1 +1 @@ -bd80bf2dd63a27111b8c5745df5dfb86 +3f797bcc74864712113297ed78d4df4a diff --git a/dev/breeze/doc/images/output_release-management_generate-providers-metadata.svg b/dev/breeze/doc/images/output_release-management_generate-providers-metadata.svg index 64efb0af6b65b..3926578e8e8cf 100644 --- a/dev/breeze/doc/images/output_release-management_generate-providers-metadata.svg +++ b/dev/breeze/doc/images/output_release-management_generate-providers-metadata.svg @@ -177,11 +177,11 @@ clickhousedb | cloudant | cncf.kubernetes | cohere | common.ai | common.compat | common.io | common.messaging |        common.sql | databricks | datadog | dbt.cloud | dingding | discord | docker | edge3 | elasticsearch | exasol | fab |   facebook | ftp | git | github | google | grpc | hashicorp | http | imap | influxdb | informatica | jdbc | jenkins |    -keycloak | microsoft.azure | microsoft.mssql | microsoft.psrp | microsoft.winrm | mongo | mysql | neo4j | odbc |       +keycloak | microsoft.azure | microsoft.mssql | microsoft.psrp | 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 |    diff --git a/dev/breeze/doc/images/output_release-management_prepare-provider-distributions.txt b/dev/breeze/doc/images/output_release-management_prepare-provider-distributions.txt index f054c03806e05..10735f9b7ce9d 100644 --- a/dev/breeze/doc/images/output_release-management_prepare-provider-distributions.txt +++ b/dev/breeze/doc/images/output_release-management_prepare-provider-distributions.txt @@ -1 +1 @@ -a27c1726f5902e5fdb501ecdee226476 +8a05d3ff0e413656eeea942fbc4a4887 diff --git a/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.svg b/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.svg index b9c71f9c81aec..74ed3a367556f 100644 --- a/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.svg +++ b/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.svg @@ -216,7 +216,7 @@ clickhousedb | cloudant | cncf.kubernetes | cohere | common.ai | common.compat | common.io | common.messaging |        common.sql | databricks | datadog | dbt.cloud | dingding | discord | docker | edge3 | elasticsearch | exasol | fab |   facebook | ftp | git | github | google | grpc | hashicorp | http | imap | influxdb | informatica | jdbc | jenkins |    -keycloak | microsoft.azure | microsoft.mssql | microsoft.psrp | microsoft.winrm | mongo | mysql | neo4j | odbc |       +keycloak | microsoft.azure | microsoft.mssql | microsoft.psrp | 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 |    diff --git a/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.txt b/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.txt index a54080b6bfbb5..b38a4940a5584 100644 --- a/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.txt +++ b/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.txt @@ -1 +1 @@ -c33a2f6d00a3a8dbec8b56c1c2d88d54 +258a134e659164924d7da7cf0983c916 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/docs/connections/oci.rst b/providers/oci/docs/connections/oci.rst index 0a967928f18ab..2e3c718839a0c 100644 --- a/providers/oci/docs/connections/oci.rst +++ b/providers/oci/docs/connections/oci.rst @@ -24,12 +24,15 @@ The Oracle Cloud Infrastructure connection configures authentication for OCI SDK It is separate from the :ref:`Oracle Database connection `, which uses the ``oracle`` connection type and the ``oracledb`` driver. -OCI support is optional because the OCI Python SDK has transitive dependencies that Apache Airflow -cannot distribute as required dependencies. Install the provider with the ``oci`` extra before using -this connection or its service hooks: +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 either +the Airflow or provider extra before using this connection or its service hooks: .. code-block:: bash + pip install 'apache-airflow[oci]' + # or pip install 'apache-airflow-providers-oci[oci]' The default connection ID is ``oci_default``. @@ -95,7 +98,9 @@ Extra * ``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, for example ``us-chicago-1``. Required for ``api_key`` + * ``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. diff --git a/providers/oci/docs/generative_ai.rst b/providers/oci/docs/generative_ai.rst index 65a65432f1ff5..b7dd2dffcde5f 100644 --- a/providers/oci/docs/generative_ai.rst +++ b/providers/oci/docs/generative_ai.rst @@ -22,7 +22,8 @@ OCI Generative AI Hosted Applications `OCI Python SDK `__ to manage `Hosted Applications and deployments `__. -Install ``apache-airflow-providers-oci[oci]`` before using the hook. Configure an +Install ``apache-airflow[oci]`` or ``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 diff --git a/providers/oci/pyproject.toml b/providers/oci/pyproject.toml index 6e43721ca3fc2..6379a53629b94 100644 --- a/providers/oci/pyproject.toml +++ b/providers/oci/pyproject.toml @@ -67,6 +67,8 @@ dependencies = [ # 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", ] diff --git a/providers/oci/src/airflow/providers/oci/hooks/base.py b/providers/oci/src/airflow/providers/oci/hooks/base.py index a1e7ade9de7a1..d47ad6e8455a0 100644 --- a/providers/oci/src/airflow/providers/oci/hooks/base.py +++ b/providers/oci/src/airflow/providers/oci/hooks/base.py @@ -16,6 +16,7 @@ # under the License. from __future__ import annotations +import re from collections.abc import Callable from enum import Enum from functools import cached_property @@ -34,6 +35,8 @@ 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.""" @@ -156,7 +159,7 @@ def get_oci_config(self) -> tuple[dict[str, Any], OciSigner | None]: file_location=self.config_file or oci.config.DEFAULT_LOCATION, profile_name=self.profile or oci.config.DEFAULT_PROFILE, ) - if region := extras.get("region"): + if region := self._get_connection_region(extras): config["region"] = region return config, None @@ -182,7 +185,7 @@ def get_oci_config(self) -> tuple[dict[str, Any], OciSigner | None]: "tenancy": extras.get("tenancy"), "user": conn.login, "fingerprint": extras.get("fingerprint"), - "region": extras.get("region"), + "region": self._get_connection_region(extras), "pass_phrase": conn.password, } key_file = self.key_file @@ -241,11 +244,23 @@ def get_compartment_id(self, compartment_id: str | None = None) -> str: ) return resolved_compartment_id - @staticmethod - def _build_principal_config(extras: dict[str, Any], signer: OciSigner) -> dict[str, Any]: - region = extras.get("region") or getattr(signer, "region", None) + @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 {} 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/oci/hooks/test_base.py b/providers/oci/tests/unit/oci/hooks/test_base.py index 8a8ac4c6f3518..f681db6ea9e77 100644 --- a/providers/oci/tests/unit/oci/hooks/test_base.py +++ b/providers/oci/tests/unit/oci/hooks/test_base.py @@ -113,6 +113,32 @@ def test_connection_extra_cannot_control_hook_configuration(self): } 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"), [ diff --git a/pyproject.toml b/pyproject.toml index d8933d0fdcdf5..92da4e53dcd68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -294,7 +294,7 @@ apache-airflow = "airflow.__main__:main" "apache-airflow-providers-neo4j>=3.8.0" ] "oci" = [ - "apache-airflow-providers-oci>=0.1.0" + "apache-airflow-providers-oci[oci]>=0.1.0" ] "odbc" = [ "apache-airflow-providers-odbc>=4.8.0" 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 90b328764f1b0..31ff92d353356 100644 --- a/uv.lock +++ b/uv.lock @@ -1344,7 +1344,7 @@ neo4j = [ { name = "apache-airflow-providers-neo4j" }, ] oci = [ - { name = "apache-airflow-providers-oci" }, + { name = "apache-airflow-providers-oci", extra = ["oci"] }, ] odbc = [ { name = "apache-airflow-providers-odbc" }, @@ -1703,7 +1703,7 @@ requires-dist = [ { name = "apache-airflow-providers-neo4j", marker = "extra == 'all'", editable = "providers/neo4j" }, { name = "apache-airflow-providers-neo4j", marker = "extra == 'neo4j'", editable = "providers/neo4j" }, { name = "apache-airflow-providers-oci", marker = "extra == 'all'", editable = "providers/oci" }, - { name = "apache-airflow-providers-oci", marker = "extra == 'oci'", editable = "providers/oci" }, + { name = "apache-airflow-providers-oci", extras = ["oci"], marker = "extra == 'oci'", editable = "providers/oci" }, { name = "apache-airflow-providers-odbc", marker = "extra == 'all'", editable = "providers/odbc" }, { name = "apache-airflow-providers-odbc", marker = "extra == 'odbc'", editable = "providers/odbc" }, { name = "apache-airflow-providers-openai", marker = "extra == 'all'", editable = "providers/openai" }, From 80c62bd671f9fbc8d3167266494e9f95ea45f7b9 Mon Sep 17 00:00:00 2001 From: AlejandroMorgante <62363051+AlejandroMorgante@users.noreply.github.com> Date: Thu, 6 Aug 2026 00:53:50 -0300 Subject: [PATCH 13/14] Prepare OCI provider for its first release New providers cannot appear in installable Airflow extras before their first PyPI publication, and users may enable the OCI SDK's selective service import mode. --- ...e-management_classify-provider-changes.svg | 2 +- ...e-management_classify-provider-changes.txt | 2 +- ...ement_generate-issue-content-providers.svg | 2 +- ...ement_generate-issue-content-providers.txt | 2 +- ...management_generate-providers-metadata.svg | 10 +++--- ...management_generate-providers-metadata.txt | 2 +- ...agement_prepare-provider-distributions.svg | 2 +- ...agement_prepare-provider-distributions.txt | 2 +- ...agement_prepare-provider-documentation.svg | 2 +- ...agement_prepare-provider-documentation.txt | 2 +- providers/oci/docs/connections/oci.rst | 6 ++-- providers/oci/docs/generative_ai.rst | 2 +- providers/oci/provider.yaml | 2 +- .../src/airflow/providers/oci/hooks/base.py | 6 ++-- .../providers/oci/hooks/generative_ai.py | 5 ++- .../oci/hooks/test_optional_dependency.py | 35 +++++++++++++++++++ pyproject.toml | 4 --- uv.lock | 8 +---- 18 files changed, 62 insertions(+), 34 deletions(-) diff --git a/dev/breeze/doc/images/output_release-management_classify-provider-changes.svg b/dev/breeze/doc/images/output_release-management_classify-provider-changes.svg index c8d32b0e58572..cef6e4d3b1d14 100644 --- a/dev/breeze/doc/images/output_release-management_classify-provider-changes.svg +++ b/dev/breeze/doc/images/output_release-management_classify-provider-changes.svg @@ -168,7 +168,7 @@ clickhousedb | cloudant | cncf.kubernetes | cohere | common.ai | common.compat | common.io | common.messaging |        common.sql | databricks | datadog | dbt.cloud | dingding | discord | docker | edge3 | elasticsearch | exasol | fab |   facebook | ftp | git | github | google | grpc | hashicorp | http | imap | influxdb | informatica | jdbc | jenkins |    -keycloak | microsoft.azure | microsoft.mssql | microsoft.psrp | microsoft.winrm | mongo | mysql | neo4j | oci | odbc | +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 |    diff --git a/dev/breeze/doc/images/output_release-management_classify-provider-changes.txt b/dev/breeze/doc/images/output_release-management_classify-provider-changes.txt index 89102334dc482..6e02c700afdf5 100644 --- a/dev/breeze/doc/images/output_release-management_classify-provider-changes.txt +++ b/dev/breeze/doc/images/output_release-management_classify-provider-changes.txt @@ -1 +1 @@ -de1e7d40d1467ebeb47f77976d39a03b +4cac13b21eee8b732a46c5a15aec7a4b diff --git a/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.svg b/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.svg index 3915eb99973ac..2f9692be54fa0 100644 --- a/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.svg +++ b/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.svg @@ -157,7 +157,7 @@ clickhousedb | cloudant | cncf.kubernetes | cohere | common.ai | common.compat | common.io | common.messaging |        common.sql | databricks | datadog | dbt.cloud | dingding | discord | docker | edge3 | elasticsearch | exasol | fab |   facebook | ftp | git | github | google | grpc | hashicorp | http | imap | influxdb | informatica | jdbc | jenkins |    -keycloak | microsoft.azure | microsoft.mssql | microsoft.psrp | microsoft.winrm | mongo | mysql | neo4j | oci | odbc | +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 |    diff --git a/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.txt b/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.txt index 652ee6574399b..6afb289b5854f 100644 --- a/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.txt +++ b/dev/breeze/doc/images/output_release-management_generate-issue-content-providers.txt @@ -1 +1 @@ -3f797bcc74864712113297ed78d4df4a +bd80bf2dd63a27111b8c5745df5dfb86 diff --git a/dev/breeze/doc/images/output_release-management_generate-providers-metadata.svg b/dev/breeze/doc/images/output_release-management_generate-providers-metadata.svg index 3926578e8e8cf..64efb0af6b65b 100644 --- a/dev/breeze/doc/images/output_release-management_generate-providers-metadata.svg +++ b/dev/breeze/doc/images/output_release-management_generate-providers-metadata.svg @@ -177,11 +177,11 @@ clickhousedb | cloudant | cncf.kubernetes | cohere | common.ai | common.compat | common.io | common.messaging |        common.sql | databricks | datadog | dbt.cloud | dingding | discord | docker | edge3 | elasticsearch | exasol | fab |   facebook | ftp | git | github | google | grpc | hashicorp | http | imap | influxdb | informatica | jdbc | jenkins |    -keycloak | microsoft.azure | microsoft.mssql | microsoft.psrp | microsoft.winrm | mongo | mysql | neo4j | oci | odbc | +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 |    diff --git a/dev/breeze/doc/images/output_release-management_prepare-provider-distributions.txt b/dev/breeze/doc/images/output_release-management_prepare-provider-distributions.txt index 10735f9b7ce9d..f054c03806e05 100644 --- a/dev/breeze/doc/images/output_release-management_prepare-provider-distributions.txt +++ b/dev/breeze/doc/images/output_release-management_prepare-provider-distributions.txt @@ -1 +1 @@ -8a05d3ff0e413656eeea942fbc4a4887 +a27c1726f5902e5fdb501ecdee226476 diff --git a/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.svg b/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.svg index 74ed3a367556f..b9c71f9c81aec 100644 --- a/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.svg +++ b/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.svg @@ -216,7 +216,7 @@ clickhousedb | cloudant | cncf.kubernetes | cohere | common.ai | common.compat | common.io | common.messaging |        common.sql | databricks | datadog | dbt.cloud | dingding | discord | docker | edge3 | elasticsearch | exasol | fab |   facebook | ftp | git | github | google | grpc | hashicorp | http | imap | influxdb | informatica | jdbc | jenkins |    -keycloak | microsoft.azure | microsoft.mssql | microsoft.psrp | microsoft.winrm | mongo | mysql | neo4j | oci | odbc | +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 |    diff --git a/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.txt b/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.txt index b38a4940a5584..a54080b6bfbb5 100644 --- a/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.txt +++ b/dev/breeze/doc/images/output_release-management_prepare-provider-documentation.txt @@ -1 +1 @@ -258a134e659164924d7da7cf0983c916 +c33a2f6d00a3a8dbec8b56c1c2d88d54 diff --git a/providers/oci/docs/connections/oci.rst b/providers/oci/docs/connections/oci.rst index 2e3c718839a0c..cfc7cb5f6e59f 100644 --- a/providers/oci/docs/connections/oci.rst +++ b/providers/oci/docs/connections/oci.rst @@ -26,13 +26,11 @@ 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 either -the Airflow or provider extra before using this connection or its service hooks: +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[oci]' - # or pip install 'apache-airflow-providers-oci[oci]' The default connection ID is ``oci_default``. diff --git a/providers/oci/docs/generative_ai.rst b/providers/oci/docs/generative_ai.rst index b7dd2dffcde5f..29dcd5e373c0f 100644 --- a/providers/oci/docs/generative_ai.rst +++ b/providers/oci/docs/generative_ai.rst @@ -22,7 +22,7 @@ OCI Generative AI Hosted Applications `OCI Python SDK `__ to manage `Hosted Applications and deployments `__. -Install ``apache-airflow[oci]`` or ``apache-airflow-providers-oci[oci]`` before using the hook. +Install ``apache-airflow-providers-oci[oci]`` before using the hook. Configure an :ref:`OCI connection ` for API key authentication or optional connection-scoped defaults. diff --git a/providers/oci/provider.yaml b/providers/oci/provider.yaml index 2ac513dbcaa31..35a2e5ce07469 100644 --- a/providers/oci/provider.yaml +++ b/providers/oci/provider.yaml @@ -21,7 +21,7 @@ name: Oracle Cloud Infrastructure (OCI) description: | `Oracle Cloud Infrastructure `__ integrations. -state: ready +state: not-ready lifecycle: incubation source-date-epoch: 1785967200 diff --git a/providers/oci/src/airflow/providers/oci/hooks/base.py b/providers/oci/src/airflow/providers/oci/hooks/base.py index d47ad6e8455a0..bbe972b273f1f 100644 --- a/providers/oci/src/airflow/providers/oci/hooks/base.py +++ b/providers/oci/src/airflow/providers/oci/hooks/base.py @@ -225,10 +225,12 @@ def get_conn(self) -> OciClient: def test_connection(self) -> tuple[bool, str]: """Test OCI credentials against the Identity service.""" try: - oci = _get_oci_sdk() + _get_oci_sdk() + from oci.identity import IdentityClient + config, signer = self.get_oci_config() client_kwargs = {"signer": signer} if signer is not None else {} - oci.identity.IdentityClient(config=config, **client_kwargs).list_regions() + 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" diff --git a/providers/oci/src/airflow/providers/oci/hooks/generative_ai.py b/providers/oci/src/airflow/providers/oci/hooks/generative_ai.py index bad2341385f88..9f059b50118ab 100644 --- a/providers/oci/src/airflow/providers/oci/hooks/generative_ai.py +++ b/providers/oci/src/airflow/providers/oci/hooks/generative_ai.py @@ -40,4 +40,7 @@ class OciGenerativeAIHook(OciBaseHook["GenerativeAiClient"]): hook_name = "OCI Generative AI" def _get_client_class(self) -> Callable[..., GenerativeAiClient]: - return _get_oci_sdk().generative_ai.GenerativeAiClient + _get_oci_sdk() + from oci.generative_ai import GenerativeAiClient + + return 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 index 78f328a4687d3..c70b99b0d2675 100644 --- a/providers/oci/tests/unit/oci/hooks/test_optional_dependency.py +++ b/providers/oci/tests/unit/oci/hooks/test_optional_dependency.py @@ -16,6 +16,7 @@ # under the License. from __future__ import annotations +import os import subprocess import sys from textwrap import dedent @@ -53,3 +54,37 @@ def test_hook_modules_import_without_optional_oci_sdk(): ], check=True, ) + + +def test_hooks_support_selective_oci_service_imports(): + 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 92da4e53dcd68..eed152f1f8158 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -293,9 +293,6 @@ apache-airflow = "airflow.__main__:main" "neo4j" = [ "apache-airflow-providers-neo4j>=3.8.0" ] -"oci" = [ - "apache-airflow-providers-oci[oci]>=0.1.0" -] "odbc" = [ "apache-airflow-providers-odbc>=4.8.0" ] @@ -475,7 +472,6 @@ apache-airflow = "airflow.__main__:main" "apache-airflow-providers-mongo>=4.2.2", "apache-airflow-providers-mysql>=5.7.2", "apache-airflow-providers-neo4j>=3.8.0", - "apache-airflow-providers-oci>=0.1.0", "apache-airflow-providers-odbc>=4.8.0", "apache-airflow-providers-openai>=1.5.0", "apache-airflow-providers-openfaas>=3.7.0", diff --git a/uv.lock b/uv.lock index 31ff92d353356..eff845cdbdf01 100644 --- a/uv.lock +++ b/uv.lock @@ -1068,7 +1068,6 @@ all = [ { name = "apache-airflow-providers-mongo" }, { name = "apache-airflow-providers-mysql" }, { name = "apache-airflow-providers-neo4j" }, - { name = "apache-airflow-providers-oci" }, { name = "apache-airflow-providers-odbc" }, { name = "apache-airflow-providers-openai" }, { name = "apache-airflow-providers-openfaas" }, @@ -1343,9 +1342,6 @@ mysql = [ neo4j = [ { name = "apache-airflow-providers-neo4j" }, ] -oci = [ - { name = "apache-airflow-providers-oci", extra = ["oci"] }, -] odbc = [ { name = "apache-airflow-providers-odbc" }, ] @@ -1702,8 +1698,6 @@ requires-dist = [ { name = "apache-airflow-providers-mysql", marker = "extra == 'mysql'", editable = "providers/mysql" }, { name = "apache-airflow-providers-neo4j", marker = "extra == 'all'", editable = "providers/neo4j" }, { name = "apache-airflow-providers-neo4j", marker = "extra == 'neo4j'", editable = "providers/neo4j" }, - { name = "apache-airflow-providers-oci", marker = "extra == 'all'", editable = "providers/oci" }, - { name = "apache-airflow-providers-oci", extras = ["oci"], marker = "extra == 'oci'", editable = "providers/oci" }, { name = "apache-airflow-providers-odbc", marker = "extra == 'all'", editable = "providers/odbc" }, { name = "apache-airflow-providers-odbc", marker = "extra == 'odbc'", editable = "providers/odbc" }, { name = "apache-airflow-providers-openai", marker = "extra == 'all'", editable = "providers/openai" }, @@ -1788,7 +1782,7 @@ requires-dist = [ { name = "sentry-sdk", marker = "extra == 'sentry'", specifier = ">=2.30.0" }, { name = "uv", marker = "extra == 'uv'", specifier = ">=0.11.29" }, ] -provides-extras = ["all-core", "async", "graphviz", "gunicorn", "kerberos", "memray", "otel", "statsd", "all-task-sdk", "airbyte", "akeyless", "alibaba", "amazon", "anthropic", "apache-cassandra", "apache-drill", "apache-druid", "apache-flink", "apache-hdfs", "apache-hive", "apache-iceberg", "apache-impala", "apache-kafka", "apache-kylin", "apache-livy", "apache-pig", "apache-pinot", "apache-spark", "apache-tinkerpop", "apprise", "arangodb", "asana", "atlassian-jira", "celery", "clickhousedb", "cloudant", "cncf-kubernetes", "cohere", "common-ai", "common-compat", "common-io", "common-messaging", "common-sql", "databricks", "datadog", "dbt-cloud", "dingding", "discord", "docker", "edge3", "elasticsearch", "exasol", "fab", "facebook", "ftp", "git", "github", "google", "grpc", "hashicorp", "http", "imap", "influxdb", "informatica", "jdbc", "jenkins", "keycloak", "microsoft-azure", "microsoft-mssql", "microsoft-psrp", "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", "all", "aiobotocore", "apache-atlas", "apache-webhdfs", "amazon-aws-auth", "cloudpickle", "github-enterprise", "google-auth", "ldap", "pandas", "polars", "rabbitmq", "sentry", "s3fs", "uv"] +provides-extras = ["all-core", "async", "graphviz", "gunicorn", "kerberos", "memray", "otel", "statsd", "all-task-sdk", "airbyte", "akeyless", "alibaba", "amazon", "anthropic", "apache-cassandra", "apache-drill", "apache-druid", "apache-flink", "apache-hdfs", "apache-hive", "apache-iceberg", "apache-impala", "apache-kafka", "apache-kylin", "apache-livy", "apache-pig", "apache-pinot", "apache-spark", "apache-tinkerpop", "apprise", "arangodb", "asana", "atlassian-jira", "celery", "clickhousedb", "cloudant", "cncf-kubernetes", "cohere", "common-ai", "common-compat", "common-io", "common-messaging", "common-sql", "databricks", "datadog", "dbt-cloud", "dingding", "discord", "docker", "edge3", "elasticsearch", "exasol", "fab", "facebook", "ftp", "git", "github", "google", "grpc", "hashicorp", "http", "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", "all", "aiobotocore", "apache-atlas", "apache-webhdfs", "amazon-aws-auth", "cloudpickle", "github-enterprise", "google-auth", "ldap", "pandas", "polars", "rabbitmq", "sentry", "s3fs", "uv"] [package.metadata.requires-dev] ci-image = [ From e9010f2488e0d02885260152cf3863444b580217 Mon Sep 17 00:00:00 2001 From: AlejandroMorgante <62363051+AlejandroMorgante@users.noreply.github.com> Date: Thu, 6 Aug 2026 09:57:56 -0300 Subject: [PATCH 14/14] Keep OCI compatibility tests optional Compatibility matrices intentionally install providers without Category X extras, so SDK-specific behavior must only be exercised when the OCI SDK is available. --- providers/oci/tests/unit/oci/hooks/test_optional_dependency.py | 1 + 1 file changed, 1 insertion(+) diff --git a/providers/oci/tests/unit/oci/hooks/test_optional_dependency.py b/providers/oci/tests/unit/oci/hooks/test_optional_dependency.py index c70b99b0d2675..b72384ec4c9cf 100644 --- a/providers/oci/tests/unit/oci/hooks/test_optional_dependency.py +++ b/providers/oci/tests/unit/oci/hooks/test_optional_dependency.py @@ -57,6 +57,7 @@ def test_hook_modules_import_without_optional_oci_sdk(): def test_hooks_support_selective_oci_service_imports(): + pytest.importorskip("oci") subprocess.run( [ sys.executable,