sqliac-snowflake 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: sqliac-snowflake
3
+ Version: 0.1.0
4
+ Requires-Python: >=3.12
5
+ Requires-Dist: sqliac<0.2,>=0.1.0
6
+ Requires-Dist: snowflake-connector-python[secure-local-storage]
7
+ Requires-Dist: cryptography
@@ -0,0 +1,22 @@
1
+ [project]
2
+ name = "sqliac-snowflake"
3
+ version = "0.1.0"
4
+ requires-python = ">=3.12"
5
+ dependencies = [
6
+ "sqliac>=0.1.0,<0.2",
7
+ "snowflake-connector-python[secure-local-storage]",
8
+ "cryptography",
9
+ ]
10
+
11
+ [tool.setuptools.packages.find]
12
+ where = ["src"]
13
+
14
+ [project.entry-points."sqliac.adapters"]
15
+ snowflake = "sqliac_snowflake.adapter:SnowflakeAdapter"
16
+
17
+ [tool.uv.sources]
18
+ sqliac = { workspace = true }
19
+
20
+ [build-system]
21
+ requires = ["setuptools>=61.0.0", "wheel"]
22
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,15 @@
1
+ """Snowflake adapter package for sqliac."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from .src.adapter import (
6
+ SnowflakeAdapter,
7
+ SnowflakeConnectionManager,
8
+ SnowflakeCredentials,
9
+ )
10
+
11
+ __all__ = [
12
+ "SnowflakeAdapter",
13
+ "SnowflakeConnectionManager",
14
+ "SnowflakeCredentials",
15
+ ]
@@ -0,0 +1,169 @@
1
+ import logging
2
+ from dataclasses import dataclass
3
+ from pathlib import Path
4
+ from typing import Any, Type
5
+
6
+ import snowflake.connector as sc
7
+ from cryptography.hazmat.backends import default_backend
8
+ from cryptography.hazmat.primitives import serialization
9
+ from snowflake.connector.cursor import SnowflakeCursor
10
+
11
+ from sqliac.adapters.base import BaseAdapter, BaseCredentials
12
+ from sqliac.adapters.connection_manager import ConnectionState, ThreadConnectionManager
13
+ from sqliac.constants import Paths
14
+ from sqliac.errors import RustyError
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class SnowflakeCredentials(BaseCredentials):
21
+ account: str
22
+ user: str
23
+ warehouse: str | None = None
24
+ database: str | None = None
25
+ schema: str | None = None
26
+ role: str | None = None
27
+ password: str | None = None
28
+ private_key_path: str | None = None
29
+ private_key: str | None = None
30
+ private_key_passphrase: str | None = None
31
+
32
+ @property
33
+ def dialect(self) -> str:
34
+ return "snowflake"
35
+
36
+ def __post_init__(self):
37
+ if not all([self.account, self.user]):
38
+ raise RustyError(error="Snowflake credentials must include account, user.")
39
+
40
+
41
+ class SnowflakeConnectionManager(ThreadConnectionManager):
42
+ """Manages Snowflake connections with thread safety."""
43
+
44
+ CredentialsClass = SnowflakeCredentials
45
+
46
+ def open(self, connection: ConnectionState) -> ConnectionState:
47
+ """Open a new Snowflake database connection."""
48
+ try:
49
+ conn_kwargs = self._build_connect_args()
50
+ account = conn_kwargs.get("account", "unknown-account")
51
+ logger.info(f"'{account}' account connection thread: '{connection.name}'")
52
+ conn = sc.connect(**conn_kwargs)
53
+ connection.handle = conn
54
+ connection.state = "open"
55
+ return connection
56
+
57
+ except Exception as e:
58
+ logger.error(f"failed to open connection for thread {connection.name}: {e}")
59
+ connection.state = "fail"
60
+ connection.handle = None
61
+ raise RustyError(
62
+ error="Failed to open Snowflake connection", help=str(e)
63
+ ) from e
64
+
65
+ def _build_connect_args(self) -> dict[str, Any]:
66
+ """Build connection arguments for Snowflake."""
67
+ connect_args: dict[str, Any] = {
68
+ "account": self.credentials.account,
69
+ "user": self.credentials.user,
70
+ }
71
+
72
+ # Add optional parameters if provided
73
+ if self.credentials.database:
74
+ connect_args["database"] = self.credentials.database
75
+ if self.credentials.schema:
76
+ connect_args["schema"] = self.credentials.schema
77
+ if self.credentials.warehouse:
78
+ connect_args["warehouse"] = self.credentials.warehouse
79
+ if self.credentials.role:
80
+ connect_args["role"] = self.credentials.role
81
+
82
+ # Authentication: prioritize private key over password
83
+ private_key_bytes = self._get_private_key_bytes()
84
+ if private_key_bytes:
85
+ connect_args["private_key"] = private_key_bytes
86
+ connect_args["authenticator"] = "SNOWFLAKE_JWT"
87
+ elif self.credentials.password:
88
+ connect_args["password"] = self.credentials.password
89
+ else:
90
+ raise RustyError(
91
+ error="credentials error",
92
+ help="either password or private_key must be provided for authentication",
93
+ )
94
+
95
+ return connect_args
96
+
97
+ def _get_private_key_bytes(self) -> bytes | None:
98
+ """Process private key from path or string."""
99
+ private_key_path = self.credentials.private_key_path
100
+ private_key_str = self.credentials.private_key
101
+ passphrase_str = self.credentials.private_key_passphrase
102
+
103
+ if not private_key_path and not private_key_str:
104
+ return None
105
+
106
+ passphrase: bytes | None = None
107
+ if passphrase_str:
108
+ passphrase = passphrase_str.encode("utf-8")
109
+
110
+ p_key = None
111
+ if private_key_path:
112
+ # Resolve path relative to project config directory
113
+ path_obj = Paths.CONFIG_DIR / Paths.PROVIDER_DIR / private_key_path
114
+ target_dir = self._resolve_path(path_obj)
115
+
116
+ if not target_dir.exists() or not target_dir.is_file():
117
+ raise RustyError(
118
+ error="not a valid `private_key_path`",
119
+ file=str(target_dir),
120
+ help=f"save the key file in the configuration directory: `{Paths.CONFIG_DIR / Paths.PROVIDER_DIR}`",
121
+ )
122
+
123
+ with open(target_dir, "rb") as key_file:
124
+ p_key = serialization.load_pem_private_key(
125
+ key_file.read(),
126
+ password=passphrase,
127
+ backend=default_backend(),
128
+ )
129
+ elif private_key_str:
130
+ private_key_clean = private_key_str.replace("\\n", "\n")
131
+ p_key_bytes = private_key_clean.encode("utf-8")
132
+ p_key = serialization.load_pem_private_key(
133
+ p_key_bytes,
134
+ password=passphrase,
135
+ backend=default_backend(),
136
+ )
137
+
138
+ if p_key is None:
139
+ return None
140
+
141
+ return p_key.private_bytes(
142
+ encoding=serialization.Encoding.DER,
143
+ format=serialization.PrivateFormat.PKCS8,
144
+ encryption_algorithm=serialization.NoEncryption(),
145
+ )
146
+
147
+ @staticmethod
148
+ def _resolve_path(path: Path):
149
+ """Resolve path relative to current working directory."""
150
+ return path if path.is_absolute() else Path.cwd() / path
151
+
152
+
153
+ class SnowflakeAdapter(BaseAdapter):
154
+ """Snowflake implementation handling enterprise credential variables."""
155
+
156
+ credentials: SnowflakeCredentials
157
+ ConnectionManager = SnowflakeConnectionManager
158
+
159
+ @property
160
+ def dialect(self) -> str:
161
+ return "snowflake"
162
+
163
+ @property
164
+ def credentials_class(self) -> Type[SnowflakeCredentials]:
165
+ return SnowflakeCredentials
166
+
167
+ def _fetch_row(self, cursor: SnowflakeCursor) -> BaseAdapter.Row:
168
+ """Fetch row from the Snowflake cursor."""
169
+ return cursor.fetchone()
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: sqliac-snowflake
3
+ Version: 0.1.0
4
+ Requires-Python: >=3.12
5
+ Requires-Dist: sqliac<0.2,>=0.1.0
6
+ Requires-Dist: snowflake-connector-python[secure-local-storage]
7
+ Requires-Dist: cryptography
@@ -0,0 +1,9 @@
1
+ pyproject.toml
2
+ src/sqliac_snowflake/__init__.py
3
+ src/sqliac_snowflake/adapter.py
4
+ src/sqliac_snowflake.egg-info/PKG-INFO
5
+ src/sqliac_snowflake.egg-info/SOURCES.txt
6
+ src/sqliac_snowflake.egg-info/dependency_links.txt
7
+ src/sqliac_snowflake.egg-info/entry_points.txt
8
+ src/sqliac_snowflake.egg-info/requires.txt
9
+ src/sqliac_snowflake.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [sqliac.adapters]
2
+ snowflake = sqliac_snowflake.adapter:SnowflakeAdapter
@@ -0,0 +1,3 @@
1
+ sqliac<0.2,>=0.1.0
2
+ snowflake-connector-python[secure-local-storage]
3
+ cryptography
@@ -0,0 +1 @@
1
+ sqliac_snowflake