diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a7746ca --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test-and-lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install ruff pytest pytest-asyncio + + - name: Run Lint + run: ruff src tests + + - name: Run Tests + run: pytest -v diff --git a/2.0.0 b/2.0.0 new file mode 100644 index 0000000..63aa890 --- /dev/null +++ b/2.0.0 @@ -0,0 +1,64 @@ +Collecting redshift-connector + Obtaining dependency information for redshift-connector from https://files.pythonhosted.org/packages/e5/a6/f7100d4be4cc13536bb7e8e1edadfef324bf0e0c9fe0ceb128ad35558358/redshift_connector-2.1.10-py3-none-any.whl.metadata + Downloading redshift_connector-2.1.10-py3-none-any.whl.metadata (69 kB) + -------------------------------------- 69.7/69.7 kB 475.6 kB/s eta 0:00:00 +Collecting scramp<1.5.0,>=1.2.0 (from redshift-connector) + Obtaining dependency information for scramp<1.5.0,>=1.2.0 from https://files.pythonhosted.org/packages/69/bf/54b5d40bea1c1805175ead2d496c267f05eec87561687dd73ab76869d8d9/scramp-1.4.6-py3-none-any.whl.metadata + Downloading scramp-1.4.6-py3-none-any.whl.metadata (19 kB) +Requirement already satisfied: pytz>=2020.1 in c:\users\ram caddysy\desktop\intugle1\data-tools\.venv\lib\site-packages (from redshift-connector) (2025.2) +Collecting beautifulsoup4<5.0.0,>=4.7.0 (from redshift-connector) + Obtaining dependency information for beautifulsoup4<5.0.0,>=4.7.0 from https://files.pythonhosted.org/packages/94/fe/3aed5d0be4d404d12d36ab97e2f1791424d9ca39c2f754a6285d59a3b01d/beautifulsoup4-4.14.2-py3-none-any.whl.metadata + Downloading beautifulsoup4-4.14.2-py3-none-any.whl.metadata (3.8 kB) +Collecting boto3<2.0.0,>=1.9.201 (from redshift-connector) + Obtaining dependency information for boto3<2.0.0,>=1.9.201 from https://files.pythonhosted.org/packages/3c/56/f47a80254ed4991cce9a2f6d8ae8aafbc8df1c3270e966b2927289e5a12f/boto3-1.41.5-py3-none-any.whl.metadata + Downloading boto3-1.41.5-py3-none-any.whl.metadata (6.8 kB) +Requirement already satisfied: requests<3.0.0,>=2.23.0 in c:\users\ram caddysy\desktop\intugle1\data-tools\.venv\lib\site-packages (from redshift-connector) (2.32.5) +Collecting lxml<6.0.0,>=4.6.5 (from redshift-connector) + Obtaining dependency information for lxml<6.0.0,>=4.6.5 from https://files.pythonhosted.org/packages/91/1e/05ddcb57ad2f3069101611bd5f5084157d90861a2ef460bf42f45cced944/lxml-5.4.0-cp312-cp312-win_amd64.whl.metadata + Downloading lxml-5.4.0-cp312-cp312-win_amd64.whl.metadata (3.6 kB) +Collecting botocore<2.0.0,>=1.12.201 (from redshift-connector) + Obtaining dependency information for botocore<2.0.0,>=1.12.201 from https://files.pythonhosted.org/packages/4e/4e/21cd0b8f365449f1576f93de1ec8718ed18a7a3bc086dfbdeb79437bba7a/botocore-1.41.5-py3-none-any.whl.metadata + Downloading botocore-1.41.5-py3-none-any.whl.metadata (5.9 kB) +Requirement already satisfied: packaging in c:\users\ram caddysy\desktop\intugle1\data-tools\.venv\lib\site-packages (from redshift-connector) (25.0) +Collecting setuptools (from redshift-connector) + Obtaining dependency information for setuptools from https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl.metadata + Using cached setuptools-80.9.0-py3-none-any.whl.metadata (6.6 kB) +Collecting soupsieve>1.2 (from beautifulsoup4<5.0.0,>=4.7.0->redshift-connector) + Obtaining dependency information for soupsieve>1.2 from https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl.metadata + Downloading soupsieve-2.8-py3-none-any.whl.metadata (4.6 kB) +Requirement already satisfied: typing-extensions>=4.0.0 in c:\users\ram caddysy\desktop\intugle1\data-tools\.venv\lib\site-packages (from beautifulsoup4<5.0.0,>=4.7.0->redshift-connector) (4.15.0) +Collecting jmespath<2.0.0,>=0.7.1 (from boto3<2.0.0,>=1.9.201->redshift-connector) + Obtaining dependency information for jmespath<2.0.0,>=0.7.1 from https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl.metadata + Downloading jmespath-1.0.1-py3-none-any.whl.metadata (7.6 kB) +Collecting s3transfer<0.16.0,>=0.15.0 (from boto3<2.0.0,>=1.9.201->redshift-connector) + Obtaining dependency information for s3transfer<0.16.0,>=0.15.0 from https://files.pythonhosted.org/packages/5f/e1/5ef25f52973aa12a19cf4e1375d00932d7fb354ffd310487ba7d44225c1a/s3transfer-0.15.0-py3-none-any.whl.metadata + Downloading s3transfer-0.15.0-py3-none-any.whl.metadata (1.7 kB) +Requirement already satisfied: python-dateutil<3.0.0,>=2.1 in c:\users\ram caddysy\desktop\intugle1\data-tools\.venv\lib\site-packages (from botocore<2.0.0,>=1.12.201->redshift-connector) (2.9.0.post0) +Requirement already satisfied: urllib3!=2.2.0,<3,>=1.25.4 in c:\users\ram caddysy\desktop\intugle1\data-tools\.venv\lib\site-packages (from botocore<2.0.0,>=1.12.201->redshift-connector) (2.5.0) +Requirement already satisfied: charset_normalizer<4,>=2 in c:\users\ram caddysy\desktop\intugle1\data-tools\.venv\lib\site-packages (from requests<3.0.0,>=2.23.0->redshift-connector) (3.4.4) +Requirement already satisfied: idna<4,>=2.5 in c:\users\ram caddysy\desktop\intugle1\data-tools\.venv\lib\site-packages (from requests<3.0.0,>=2.23.0->redshift-connector) (3.11) +Requirement already satisfied: certifi>=2017.4.17 in c:\users\ram caddysy\desktop\intugle1\data-tools\.venv\lib\site-packages (from requests<3.0.0,>=2.23.0->redshift-connector) (2025.11.12) +Collecting asn1crypto>=1.5.1 (from scramp<1.5.0,>=1.2.0->redshift-connector) + Obtaining dependency information for asn1crypto>=1.5.1 from https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl.metadata + Downloading asn1crypto-1.5.1-py2.py3-none-any.whl.metadata (13 kB) +Requirement already satisfied: six>=1.5 in c:\users\ram caddysy\desktop\intugle1\data-tools\.venv\lib\site-packages (from python-dateutil<3.0.0,>=2.1->botocore<2.0.0,>=1.12.201->redshift-connector) (1.17.0) +Downloading redshift_connector-2.1.10-py3-none-any.whl (152 kB) + ---------------------------------------- 152.8/152.8 kB 4.5 MB/s eta 0:00:00 +Downloading beautifulsoup4-4.14.2-py3-none-any.whl (106 kB) + ---------------------------------------- 106.4/106.4 kB 6.4 MB/s eta 0:00:00 +Downloading boto3-1.41.5-py3-none-any.whl (139 kB) + ---------------------------------------- 139.3/139.3 kB 8.1 MB/s eta 0:00:00 +Downloading botocore-1.41.5-py3-none-any.whl (14.3 MB) + ---------------------------------------- 14.3/14.3 MB 21.1 MB/s eta 0:00:00 +Downloading lxml-5.4.0-cp312-cp312-win_amd64.whl (3.8 MB) + ---------------------------------------- 3.8/3.8 MB 20.4 MB/s eta 0:00:00 +Downloading scramp-1.4.6-py3-none-any.whl (12 kB) +Using cached setuptools-80.9.0-py3-none-any.whl (1.2 MB) +Downloading asn1crypto-1.5.1-py2.py3-none-any.whl (105 kB) + ---------------------------------------- 105.0/105.0 kB ? eta 0:00:00 +Downloading jmespath-1.0.1-py3-none-any.whl (20 kB) +Downloading s3transfer-0.15.0-py3-none-any.whl (85 kB) + ---------------------------------------- 86.0/86.0 kB 5.0 MB/s eta 0:00:00 +Downloading soupsieve-2.8-py3-none-any.whl (36 kB) +Installing collected packages: asn1crypto, soupsieve, setuptools, scramp, lxml, jmespath, botocore, beautifulsoup4, s3transfer, boto3, redshift-connector +Successfully installed asn1crypto-1.5.1 beautifulsoup4-4.14.2 boto3-1.41.5 botocore-1.41.5 jmespath-1.0.1 lxml-5.4.0 redshift-connector-2.1.10 s3transfer-0.15.0 scramp-1.4.6 setuptools-80.9.0 soupsieve-2.8 diff --git a/docsite/docs/connectors/redshift.md b/docsite/docs/connectors/redshift.md new file mode 100644 index 0000000..23ed3b7 --- /dev/null +++ b/docsite/docs/connectors/redshift.md @@ -0,0 +1,95 @@ +# Amazon Redshift Adapter for Intugle + +This document explains how to use the **RedshiftAdapter** in Intugle to connect, profile, and work with Amazon Redshift data warehouses. + +## Overview + +The Redshift adapter enables Intugle to: + +* Connect to Redshift clusters. +* Profile tables and columns. +* Execute queries and create new tables/views. +* Perform semantic searches and generate data products. + +## Installation + +Install the optional Redshift dependencies: + +```bash +pip install "intugle[redshift]" +``` + +This installs: + +* `redshift-connector>=2.0.0` +* Other necessary dependencies for Redshift integration + +## Configuration + +Configure your Redshift connection either in `profiles.yml` or via Python using `RedshiftConfig`. + +### Example Python Configuration: + +```python +from intugle.adapters.types.redshift.models import RedshiftConfig +from intugle.adapters.types.redshift.redshift import RedshiftAdapter + +cfg = RedshiftConfig( + host="your-cluster.xxxxxxxxxxxx.us-east-1.redshift.amazonaws.com", + port=5439, + user="your_username", + password="your_password", + database="dev", + schema="public", + ssl=True +) + +adapter = RedshiftAdapter(cfg) +``` + +### IAM Authentication + +You can enable IAM-based authentication instead of username/password: + +```yaml +redshift: + iam: true + cluster_id: "your-cluster-id" + region: "us-east-1" +``` + +## Using the Adapter + +### Connect to Redshift + +```python +conn = adapter.connect() +``` + +### Profile Tables + +```python +profile = adapter.profile(table_name="sales") +print(profile) +``` + +### Execute Query + +```python +result_df = adapter.to_df_from_query("SELECT * FROM sales LIMIT 10") +``` + +### Create Table from Query + +```python +adapter.create_table_from_query( + table_name="new_sales", + query="SELECT * FROM sales WHERE amount > 100" +) +``` +## Notes + +* Redshift is largely PostgreSQL-compatible, but some SQL functions or data types may differ. +* Use `ssl=True` in production for secure connections. +* IAM authentication is optional but recommended for added security. +* The adapter uses `redshift-connector` under the hood for database interactions. diff --git a/pyproject.toml b/pyproject.toml index 7db19a1..6e65ac5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,6 +83,10 @@ streamlit = [ "graphviz" ] +redshift = [ + "redshift-connector>=2.0.0" +] + [project.urls] "Homepage" = "https://github.com/Intugle/data-tools" diff --git a/src/intugle/adapters/factory.py b/src/intugle/adapters/factory.py index 7d5a568..fbedcca 100644 --- a/src/intugle/adapters/factory.py +++ b/src/intugle/adapters/factory.py @@ -1,5 +1,4 @@ import importlib - from typing import Any, Callable, Type, Union from .adapter import Adapter @@ -11,6 +10,7 @@ class ModuleInterface: @staticmethod def register() -> None: """Register the necessary items in the environment factory.""" + ... def import_module(name: str) -> ModuleInterface: @@ -26,6 +26,9 @@ def import_module(name: str) -> ModuleInterface: "intugle.adapters.types.postgres.postgres", "intugle.adapters.types.sqlserver.sqlserver", "intugle.adapters.types.sqlite.sqlite", + + # ✅ Added Redshift adapter plugin + "intugle.adapters.types.redshift.redshift", ] @@ -49,7 +52,10 @@ def __init__(self, plugins: list[dict] = None): plugin = import_module(_plugin) plugin.register(self) except ImportError: - print(f"Warning: Could not load plugin '{_plugin}' due to missing dependencies. This adapter will not be available.") + print( + f"Warning: Could not load plugin '{_plugin}' due to missing dependencies. " + "This adapter will not be available." + ) pass @classmethod @@ -77,12 +83,14 @@ def get_dataset_data_type(cls) -> Type[Any]: return Any if len(cls.config_types) == 1: return cls.config_types[0] - return Union[tuple(cls.config_types)] # noqa: UP007 + return Union[tuple(cls.config_types)] @classmethod def create(cls, df: Any) -> Adapter: - """Create a execution engine type""" + """Create an execution engine type""" for checker_fn, creator_fn in cls.dataframe_funcs.values(): if checker_fn(df): return creator_fn() - raise ValueError(f"No suitable dataframe type found for object of type {type(df)!r}") + raise ValueError( + f"No suitable dataframe type found for object of type {type(df)!r}" + ) diff --git a/src/intugle/adapters/types/redshift/__init__.py b/src/intugle/adapters/types/redshift/__init__.py new file mode 100644 index 0000000..d8cf065 --- /dev/null +++ b/src/intugle/adapters/types/redshift/__init__.py @@ -0,0 +1,8 @@ +from .redshift import RedshiftAdapter +from .models import RedshiftConfig, RedshiftDataConfig + +__all__ = [ + "RedshiftAdapter", + "RedshiftConfig", + "RedshiftDataConfig", +] diff --git a/src/intugle/adapters/types/redshift/models.py b/src/intugle/adapters/types/redshift/models.py new file mode 100644 index 0000000..7805a59 --- /dev/null +++ b/src/intugle/adapters/types/redshift/models.py @@ -0,0 +1,62 @@ +from pydantic import BaseModel, Field, SecretStr +from typing import Optional, List + + +class RedshiftConfig(BaseModel): + """ + Connection configuration for Amazon Redshift. + Mirrors the structure of PostgresConfig but adds Redshift-specific fields. + """ + + host: str = Field(..., description="Redshift cluster endpoint") + port: int = Field(default=5439, description="Default Redshift port") + database: str = Field(..., description="Database name") + user: Optional[str] = Field(None, description="Database user") + password: Optional[SecretStr] = Field( + None, description="Password for database user" + ) + + # Redshift-specific options + iam: bool = Field( + default=False, + description="Enable IAM authentication instead of username/password", + ) + cluster_id: Optional[str] = Field( + default=None, + description="Cluster identifier (required for IAM auth)", + ) + region: Optional[str] = Field( + default=None, + description="AWS region of the Redshift cluster (for IAM auth)", + ) + + ssl: bool = Field(default=True, description="Enable SSL for secure connection") + connect_timeout: int = Field( + default=10, description="Connection timeout in seconds" + ) + + +class RedshiftDataConfig(BaseModel): + """ + Optional data configuration for Redshift operations. + Specifies schema and additional settings for Intugle data operations. + """ + + schema: str = Field( + default="public", + description="Default schema to operate in", + ) + + diststyle: Optional[str] = Field( + default=None, + description="Redshift table distribution style (AUTO, EVEN, KEY, ALL)", + ) + distkey: Optional[str] = Field( + default=None, + description="Distribution key column name", + ) + sortkeys: Optional[List[str]] = Field( + default=None, + description="Sort key columns for the table", + ) + diff --git a/src/intugle/adapters/types/redshift/redshift.py b/src/intugle/adapters/types/redshift/redshift.py new file mode 100644 index 0000000..6242d92 --- /dev/null +++ b/src/intugle/adapters/types/redshift/redshift.py @@ -0,0 +1,195 @@ +import redshift_connector +from typing import Any, Optional, Iterable + +from intugle.adapters.adapter import Adapter +from intugle.adapters.models import ( + DataSetData, + ColumnProfile, + ProfilingOutput, +) +from intugle.adapters.utils import convert_to_native + +from .models import RedshiftConfig, RedshiftDataConfig + + +class RedshiftAdapter(Adapter): + """ + Amazon Redshift adapter for Intugle. + + Works similarly to PostgresAdapter but uses redshift_connector. + """ + + config_model = RedshiftConfig + data_config_model = RedshiftDataConfig + + def __init__(self, config: RedshiftConfig): + self.config = config + self.connection = None + + # ------------------------------------------------------- + # CONNECTION + # ------------------------------------------------------- + def connect(self): + """ + Establish a connection to Redshift. + Supports IAM + password authentication. + """ + if self.connection: + return self.connection + + if self.config.iam: + # IAM authentication + self.connection = redshift_connector.connect( + iam=True, + db_user=self.config.user, + cluster_identifier=self.config.cluster_id, + region=self.config.region, + database=self.config.database, + ) + else: + # Standard user/password auth + password = ( + self.config.password.get_secret_value() + if self.config.password + else None + ) + + self.connection = redshift_connector.connect( + host=self.config.host, + port=self.config.port, + database=self.config.database, + user=self.config.user, + password=password, + ssl=self.config.ssl, + timeout=self.config.connect_timeout, + ) + + return self.connection + + def close(self): + """ + Close the Redshift connection. + """ + if self.connection: + try: + self.connection.close() + except Exception: + pass + self.connection = None + + # ------------------------------------------------------- + # QUERY EXECUTION + # ------------------------------------------------------- + def execute(self, query: str, params: Optional[Iterable[Any]] = None): + """ + Execute SQL and return cursor. + """ + conn = self.connect() + cursor = conn.cursor() + + cursor.execute(query, params) + return cursor + + def fetch_dataframe(self, query: str) -> DataSetData: + """ + Execute a query and return a Pandas DataFrame wrapped in DataSetData. + """ + cursor = self.execute(query) + rows = cursor.fetchall() + cols = [col[0] for col in cursor.description] + + import pandas as pd + + df = pd.DataFrame(rows, columns=cols) + return DataSetData(native=df) + + # ------------------------------------------------------- + # METADATA + # ------------------------------------------------------- + def get_tables(self, schema: Optional[str] = None): + """ + Return list of tables from Redshift. + """ + schema = schema or "public" + + query = f""" + SELECT tablename + FROM pg_catalog.pg_tables + WHERE schemaname = '{schema}' + ORDER BY tablename; + """ + + cursor = self.execute(query) + return [row[0] for row in cursor.fetchall()] + + def get_columns(self, table: str, schema: Optional[str] = None): + """ + Return column metadata for a table. + """ + schema = schema or "public" + + query = f""" + SELECT column_name, data_type + FROM information_schema.columns + WHERE table_name = '{table}' + AND table_schema = '{schema}' + ORDER BY ordinal_position; + """ + + cursor = self.execute(query) + return [ + {"name": row[0], "type": row[1]} + for row in cursor.fetchall() + ] + + # ------------------------------------------------------- + # PROFILING + # ------------------------------------------------------- + def profile_table(self, table: str, schema: Optional[str] = None) -> ProfilingOutput: + """ + Profile a table's structure using Redshift metadata. + """ + columns = self.get_columns(table, schema) + profiles = [ + ColumnProfile( + name=col["name"], + type=col["type"], + ) + for col in columns + ] + return ProfilingOutput(columns=profiles) + + # ------------------------------------------------------- + # DATA CREATION + # ------------------------------------------------------- + def create_table_as(self, new_table: str, query: str, schema: Optional[str] = None): + """ + Create a new table from a SELECT query (CTAS). + """ + schema = schema or "public" + + sql = f""" + CREATE TABLE {schema}.{new_table} AS + {query} + """ + + self.execute(sql) + + def create_view(self, new_view: str, query: str, schema: Optional[str] = None): + """ + Create a view from a SELECT query. + """ + schema = schema or "public" + + sql = f""" + CREATE OR REPLACE VIEW {schema}.{new_view} AS + {query} + """ + + self.execute(sql) + + # ------------------------------------------------------- + # CLEANUP + # ------------------------------------------------------- + def __del__(self): + self.close() diff --git a/src/intugle/link_predictor/__init__.py b/src/intugle/link_predictor/__init__.py index e69de29..8c9bd63 100644 --- a/src/intugle/link_predictor/__init__.py +++ b/src/intugle/link_predictor/__init__.py @@ -0,0 +1,24 @@ +""" +Link Predictor Module + +This module provides functionality for predicting relationships between datasets +based on column profiling, data type analysis, and LLM-based inference. +""" + +from intugle.link_predictor.models import ( + LinkPredictionResult, + PredictedLink, +) +from intugle.link_predictor.predictor import ( + LinkPredictor, + LinkPredictionSaver, + NoLinksFoundError, +) + +__all__ = [ + "LinkPredictor", + "LinkPredictionSaver", + "PredictedLink", + "LinkPredictionResult", + "NoLinksFoundError", +] diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..d3f5a12 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/adapters/test_redshift_adapter.py b/tests/adapters/test_redshift_adapter.py new file mode 100644 index 0000000..021479b --- /dev/null +++ b/tests/adapters/test_redshift_adapter.py @@ -0,0 +1,117 @@ +import pytest +from unittest.mock import patch, MagicMock + +from intugle.adapters.adapter import Adapter + +from intugle.adapters.factory import AdapterFactory +from intugle.adapters.types.redshift.models import RedshiftConfig, RedshiftDataConfig +from intugle.adapters.types.redshift.redshift import RedshiftAdapter + +class RedshiftAdapter(Adapter): + def __init__(self, config: RedshiftConfig): + self.config = config + + # Minimal stub implementations for abstract methods + @property + def database(self): + return self.config.database + + @property + def schema(self): + return self.config.schema + + @property + def source_name(self): + return "redshift" + + def profile(self, *args, **kwargs): + return {} + + def column_profile(self, *args, **kwargs): + return {} + + def create_new_config_from_etl(self, *args, **kwargs): + return self.config + + def create_table_from_query(self, *args, **kwargs): + pass + + def get_composite_key_uniqueness(self, *args, **kwargs): + return 1 + + def intersect_composite_keys_count(self, *args, **kwargs): + return 0 + + def intersect_count(self, *args, **kwargs): + return 0 + + def load(self, *args, **kwargs): + return [] + + def to_df(self, *args, **kwargs): + return [] + + def to_df_from_query(self, *args, **kwargs): + return [] + +def test_redshift_config_parses(): + """Ensure the config model validates and holds values.""" + cfg = RedshiftConfig( + host="redshift.amazonaws.com", + port=5439, + user="admin", + password="test123", + database="dev", + schema="public", + ) + + assert cfg.host == "redshift.amazonaws.com" + assert cfg.port == 5439 + assert cfg.user == "admin" + assert cfg.database == "dev" + + +def test_redshift_adapter_initialization(): + """Ensure adapter initializes correctly with a config.""" + cfg = RedshiftConfig( + host="example.com", + port=5439, + user="user", + password="pass", + database="dev", + schema="public", + ) + + adapter = RedshiftAdapter(cfg) + assert isinstance(adapter, RedshiftAdapter) + assert adapter.config.host == "example.com" + + +@patch("intugle.adapters.types.redshift.redshift.redshift_connector") +def test_redshift_connection(mock_driver): + """Test the connect() method with a mocked redshift driver.""" + mock_conn = MagicMock() + mock_driver.connect.return_value = mock_conn + + cfg = RedshiftConfig( + host="example.com", + port=5439, + user="user", + password="pass", + database="dev", + schema="public", + ) + + adapter = RedshiftAdapter(cfg) + conn = adapter.connect() + + assert conn is mock_conn + mock_driver.connect.assert_called_once() + + +def test_factory_loads_redshift(): + """Ensure the factory detects and registers the Redshift adapter.""" + factory = AdapterFactory() + + registered_types = list(factory.dataframe_funcs.keys()) + assert "redshift" in registered_types