snowlib 0.2.0__tar.gz → 0.3.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.
- {snowlib-0.2.0 → snowlib-0.3.0}/PKG-INFO +8 -6
- {snowlib-0.2.0 → snowlib-0.3.0}/README.md +6 -1
- {snowlib-0.2.0 → snowlib-0.3.0}/pyproject.toml +1 -5
- snowlib-0.3.0/src/snowlib/__init__.py +68 -0
- snowlib-0.3.0/src/snowlib/connection/__init__.py +15 -0
- {snowlib-0.2.0 → snowlib-0.3.0}/src/snowlib/connection/base.py +7 -71
- snowlib-0.3.0/src/snowlib/connection/connection.py +93 -0
- snowlib-0.3.0/src/snowlib/connection/paths.py +64 -0
- snowlib-0.2.0/src/snowlib/config/config.py → snowlib-0.3.0/src/snowlib/connection/profiles.py +3 -34
- snowlib-0.3.0/src/snowlib/context.py +124 -0
- snowlib-0.3.0/src/snowlib/models/__init__.py +19 -0
- snowlib-0.3.0/src/snowlib/models/base/__init__.py +12 -0
- snowlib-0.3.0/src/snowlib/models/base/core.py +152 -0
- snowlib-0.3.0/src/snowlib/models/base/fqn.py +98 -0
- snowlib-0.3.0/src/snowlib/models/base/schema_child.py +85 -0
- snowlib-0.3.0/src/snowlib/models/base/show.py +98 -0
- snowlib-0.3.0/src/snowlib/models/column.py +69 -0
- snowlib-0.3.0/src/snowlib/models/database.py +56 -0
- snowlib-0.3.0/src/snowlib/models/schema.py +100 -0
- snowlib-0.3.0/src/snowlib/models/table/__init__.py +15 -0
- snowlib-0.3.0/src/snowlib/models/table/base.py +105 -0
- snowlib-0.3.0/src/snowlib/models/table/dynamic_table.py +44 -0
- snowlib-0.3.0/src/snowlib/models/table/materialized_view.py +20 -0
- snowlib-0.3.0/src/snowlib/models/table/table.py +51 -0
- snowlib-0.3.0/src/snowlib/models/table/view.py +20 -0
- snowlib-0.3.0/src/snowlib/primitives/__init__.py +29 -0
- snowlib-0.3.0/src/snowlib/primitives/async_query.py +75 -0
- snowlib-0.3.0/src/snowlib/primitives/execute.py +98 -0
- snowlib-0.3.0/src/snowlib/primitives/result.py +83 -0
- snowlib-0.3.0/src/snowlib/sqlalchemy.py +53 -0
- snowlib-0.3.0/src/snowlib/utils/identifiers.py +13 -0
- snowlib-0.3.0/src/snowlib/utils/query.py +32 -0
- {snowlib-0.2.0 → snowlib-0.3.0}/src/snowlib.egg-info/PKG-INFO +8 -6
- snowlib-0.3.0/src/snowlib.egg-info/SOURCES.txt +41 -0
- {snowlib-0.2.0 → snowlib-0.3.0}/src/snowlib.egg-info/requires.txt +1 -7
- snowlib-0.2.0/src/snowlib/__init__.py +0 -85
- snowlib-0.2.0/src/snowlib/config/__init__.py +0 -12
- snowlib-0.2.0/src/snowlib/config/paths.py +0 -133
- snowlib-0.2.0/src/snowlib/connection/__init__.py +0 -5
- snowlib-0.2.0/src/snowlib/connection/connection.py +0 -161
- snowlib-0.2.0/src/snowlib/io/__init__.py +0 -12
- snowlib-0.2.0/src/snowlib/io/names.py +0 -242
- snowlib-0.2.0/src/snowlib/io/read.py +0 -125
- snowlib-0.2.0/src/snowlib/io/write.py +0 -103
- snowlib-0.2.0/src/snowlib/primitives/__init__.py +0 -60
- snowlib-0.2.0/src/snowlib/primitives/context.py +0 -150
- snowlib-0.2.0/src/snowlib/primitives/data.py +0 -176
- snowlib-0.2.0/src/snowlib/primitives/execution.py +0 -274
- snowlib-0.2.0/src/snowlib/primitives/job.py +0 -121
- snowlib-0.2.0/src/snowlib/primitives/metadata.py +0 -277
- snowlib-0.2.0/src/snowlib/primitives/result.py +0 -134
- snowlib-0.2.0/src/snowlib/primitives/streaming.py +0 -63
- snowlib-0.2.0/src/snowlib/sqlalchemy.py +0 -94
- snowlib-0.2.0/src/snowlib.egg-info/SOURCES.txt +0 -32
- {snowlib-0.2.0 → snowlib-0.3.0}/LICENSE +0 -0
- {snowlib-0.2.0 → snowlib-0.3.0}/licenses/THIRD_PARTY.md +0 -0
- {snowlib-0.2.0 → snowlib-0.3.0}/setup.cfg +0 -0
- {snowlib-0.2.0 → snowlib-0.3.0}/src/snowlib/_data/connections.toml.example +0 -0
- {snowlib-0.2.0 → snowlib-0.3.0}/src/snowlib/_data/test_config.toml.example +0 -0
- {snowlib-0.2.0 → snowlib-0.3.0}/src/snowlib/py.typed +0 -0
- {snowlib-0.2.0 → snowlib-0.3.0}/src/snowlib.egg-info/dependency_links.txt +0 -0
- {snowlib-0.2.0 → snowlib-0.3.0}/src/snowlib.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: snowlib
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Python-Snowflake utilities
|
|
5
5
|
Author: Rashad Aziz
|
|
6
6
|
Maintainer: Rashad Aziz
|
|
@@ -39,9 +39,6 @@ Requires-Dist: importlib_resources>=1.3; python_version < "3.9"
|
|
|
39
39
|
Provides-Extra: sqlalchemy
|
|
40
40
|
Requires-Dist: sqlalchemy>=2.0; extra == "sqlalchemy"
|
|
41
41
|
Requires-Dist: snowflake-sqlalchemy>=1.6.0; extra == "sqlalchemy"
|
|
42
|
-
Provides-Extra: pandas
|
|
43
|
-
Requires-Dist: pandas; extra == "pandas"
|
|
44
|
-
Requires-Dist: pyarrow; python_version < "3.14" and extra == "pandas"
|
|
45
42
|
Provides-Extra: snowpark
|
|
46
43
|
Requires-Dist: snowflake-connector-python[pandas]<4.0.0,>=3.17.0; python_version < "3.14" and extra == "snowpark"
|
|
47
44
|
Requires-Dist: snowflake-connector-python<4.0.0,>=3.17.0; python_version >= "3.14" and extra == "snowpark"
|
|
@@ -61,11 +58,16 @@ Requires-Dist: pandas-stubs; extra == "dev"
|
|
|
61
58
|
Requires-Dist: ipykernel; extra == "dev"
|
|
62
59
|
Requires-Dist: tox; extra == "dev"
|
|
63
60
|
Requires-Dist: pip-licenses; extra == "dev"
|
|
64
|
-
Requires-Dist: snowlib[excel,
|
|
61
|
+
Requires-Dist: snowlib[excel,snowpark,sqlalchemy]; extra == "dev"
|
|
65
62
|
Dynamic: license-file
|
|
66
63
|
|
|
67
64
|
# ❄️ SnowLib
|
|
68
65
|
|
|
66
|
+
[](https://pypi.org/project/snowlib/)
|
|
67
|
+
[](https://pypi.org/project/snowlib/)
|
|
68
|
+
[](LICENSE)
|
|
69
|
+
[](https://doi.org/10.5281/zenodo.17354788)
|
|
70
|
+
|
|
69
71
|
Python-Snowflake utilities
|
|
70
72
|
|
|
71
73
|
```bash
|
|
@@ -83,8 +85,8 @@ pip install snowlib[excel]
|
|
|
83
85
|
| **snowflake-snowpark-python** | - | ≥1.9.0 |
|
|
84
86
|
| **sqlalchemy** | ≥2.0 | ≥2.0 |
|
|
85
87
|
| **snowflake-sqlalchemy** | ≥1.6.0 | ≥1.6.0 |
|
|
88
|
+
| **pandas** | 2.3.3 | 2.3.1 |
|
|
86
89
|
| **pyarrow** | - | ✓ |
|
|
87
|
-
| **pandas** | ✓ | ✓ |
|
|
88
90
|
|
|
89
91
|
**Notes:**
|
|
90
92
|
- Version ranges shown when specified in `pyproject.toml`
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# ❄️ SnowLib
|
|
2
2
|
|
|
3
|
+
[](https://pypi.org/project/snowlib/)
|
|
4
|
+
[](https://pypi.org/project/snowlib/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://doi.org/10.5281/zenodo.17354788)
|
|
7
|
+
|
|
3
8
|
Python-Snowflake utilities
|
|
4
9
|
|
|
5
10
|
```bash
|
|
@@ -17,8 +22,8 @@ pip install snowlib[excel]
|
|
|
17
22
|
| **snowflake-snowpark-python** | - | ≥1.9.0 |
|
|
18
23
|
| **sqlalchemy** | ≥2.0 | ≥2.0 |
|
|
19
24
|
| **snowflake-sqlalchemy** | ≥1.6.0 | ≥1.6.0 |
|
|
25
|
+
| **pandas** | 2.3.3 | 2.3.1 |
|
|
20
26
|
| **pyarrow** | - | ✓ |
|
|
21
|
-
| **pandas** | ✓ | ✓ |
|
|
22
27
|
|
|
23
28
|
**Notes:**
|
|
24
29
|
- Version ranges shown when specified in `pyproject.toml`
|
|
@@ -52,10 +52,6 @@ sqlalchemy = [
|
|
|
52
52
|
"sqlalchemy>=2.0",
|
|
53
53
|
"snowflake-sqlalchemy>=1.6.0",
|
|
54
54
|
]
|
|
55
|
-
pandas = [
|
|
56
|
-
"pandas",
|
|
57
|
-
"pyarrow; python_version<'3.14'", # No Windows wheel for 3.14 yet
|
|
58
|
-
]
|
|
59
55
|
snowpark = [
|
|
60
56
|
"snowflake-connector-python[pandas]<4.0.0,>=3.17.0; python_version < '3.14'",
|
|
61
57
|
"snowflake-connector-python<4.0.0,>=3.17.0; python_version >= '3.14'",
|
|
@@ -80,7 +76,7 @@ dev = [
|
|
|
80
76
|
# Distribution
|
|
81
77
|
"pip-licenses",
|
|
82
78
|
# All optional dependencies
|
|
83
|
-
"snowlib[sqlalchemy,
|
|
79
|
+
"snowlib[sqlalchemy,snowpark,excel]",
|
|
84
80
|
]
|
|
85
81
|
|
|
86
82
|
[tool.setuptools]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
snowlib - Python-Snowflake utilities
|
|
3
|
+
|
|
4
|
+
Code is organized in layers
|
|
5
|
+
- config/ and connection/ as the interface for Snowflake packages
|
|
6
|
+
- primitives/ wraps these in low-level functions
|
|
7
|
+
- client/ is the beginning of higher-level convenience functions
|
|
8
|
+
|
|
9
|
+
Likely to expand to cortex and leaning more into snowpark
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# Layer 1: Core connectivity
|
|
13
|
+
from snowlib.connection import load_profile, list_profiles
|
|
14
|
+
from snowlib.connection import SnowflakeConnector, SnowparkConnector
|
|
15
|
+
from snowlib.context import SnowflakeContext
|
|
16
|
+
|
|
17
|
+
# Layer 2: Primitives
|
|
18
|
+
from snowlib.primitives import (
|
|
19
|
+
# Query results
|
|
20
|
+
QueryResult,
|
|
21
|
+
AsyncQuery,
|
|
22
|
+
# Execution
|
|
23
|
+
execute_sql,
|
|
24
|
+
execute_sql_async,
|
|
25
|
+
execute_block,
|
|
26
|
+
query,
|
|
27
|
+
Executor,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Layer 3: Models (OOP interface for Snowflake objects)
|
|
31
|
+
from snowlib.models import (
|
|
32
|
+
Database,
|
|
33
|
+
Schema,
|
|
34
|
+
Table,
|
|
35
|
+
View,
|
|
36
|
+
MaterializedView,
|
|
37
|
+
DynamicTable,
|
|
38
|
+
Column,
|
|
39
|
+
Show,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
__version__ = "0.3.0"
|
|
43
|
+
__all__ = [
|
|
44
|
+
# Layer 1: Configuration & Connection
|
|
45
|
+
"load_profile",
|
|
46
|
+
"list_profiles",
|
|
47
|
+
"SnowflakeConnector",
|
|
48
|
+
"SnowparkConnector",
|
|
49
|
+
# Layer 2: Context
|
|
50
|
+
"SnowflakeContext",
|
|
51
|
+
"QueryResult",
|
|
52
|
+
"AsyncQuery",
|
|
53
|
+
# Layer 2: Execution
|
|
54
|
+
"execute_sql",
|
|
55
|
+
"execute_sql_async",
|
|
56
|
+
"execute_block",
|
|
57
|
+
"query",
|
|
58
|
+
"Executor",
|
|
59
|
+
# Layer 3: Models (OOP interface)
|
|
60
|
+
"Database",
|
|
61
|
+
"Schema",
|
|
62
|
+
"Table",
|
|
63
|
+
"View",
|
|
64
|
+
"MaterializedView",
|
|
65
|
+
"DynamicTable",
|
|
66
|
+
"Column",
|
|
67
|
+
"Show",
|
|
68
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Connection module exports."""
|
|
2
|
+
|
|
3
|
+
from .connection import SnowflakeConnector, SnowparkConnector
|
|
4
|
+
from .profiles import load_profile, list_profiles
|
|
5
|
+
from .paths import resolve_config_path, get_default_config_path, CONF_DIR
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"SnowflakeConnector",
|
|
9
|
+
"SnowparkConnector",
|
|
10
|
+
"load_profile",
|
|
11
|
+
"list_profiles",
|
|
12
|
+
"resolve_config_path",
|
|
13
|
+
"get_default_config_path",
|
|
14
|
+
"CONF_DIR",
|
|
15
|
+
]
|
|
@@ -8,57 +8,24 @@ import keyring
|
|
|
8
8
|
from cryptography.hazmat.primitives import serialization
|
|
9
9
|
from cryptography.hazmat.backends import default_backend
|
|
10
10
|
|
|
11
|
-
from
|
|
11
|
+
from .profiles import load_profile
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class BaseConnector:
|
|
15
|
-
"""
|
|
16
|
-
Base class for Snowflake connectors with TOML profile support.
|
|
17
|
-
|
|
18
|
-
This class handles common functionality for both SnowflakeConnector and
|
|
19
|
-
SnowparkConnector, including:
|
|
20
|
-
- Loading connection profiles from TOML configuration
|
|
21
|
-
- Merging runtime parameter overrides
|
|
22
|
-
- Processing keypair authentication with secure passphrase handling
|
|
23
|
-
|
|
24
|
-
Derived classes should implement their specific connection logic.
|
|
25
|
-
|
|
26
|
-
Args:
|
|
27
|
-
profile: Name of the profile to load from connections.toml
|
|
28
|
-
**kwargs: Additional parameters to override profile settings
|
|
29
|
-
"""
|
|
15
|
+
"""Base class for Snowflake connectors with TOML profile support and authentication handling"""
|
|
30
16
|
|
|
31
17
|
def __init__(self, profile: str, **kwargs: Any) -> None:
|
|
32
|
-
"""
|
|
33
|
-
Initialize the connector with a configuration profile.
|
|
34
|
-
|
|
35
|
-
Args:
|
|
36
|
-
profile: Name of the connection profile to load
|
|
37
|
-
**kwargs: Override any connection parameters from the profile
|
|
38
|
-
"""
|
|
39
|
-
# Public attributes for processed authentication credentials
|
|
18
|
+
"""Initialize the connector with a configuration profile and optional parameter overrides"""
|
|
40
19
|
self.password: Optional[str] = None
|
|
41
20
|
self.private_key: Optional[Any] = None
|
|
42
21
|
|
|
43
|
-
# Load configuration from TOML file
|
|
44
22
|
self._cfg: Dict[str, Any] = load_profile(profile)
|
|
45
|
-
|
|
46
|
-
# Allow runtime overrides
|
|
47
23
|
self._cfg.update(kwargs)
|
|
48
|
-
|
|
49
|
-
# Store profile name for debugging/repr
|
|
50
24
|
self._profile = profile
|
|
51
|
-
|
|
52
|
-
# Process authentication credentials
|
|
53
25
|
self._process_auth()
|
|
54
26
|
|
|
55
27
|
def _process_auth(self) -> None:
|
|
56
|
-
"""
|
|
57
|
-
Processes authentication credentials from the profile.
|
|
58
|
-
|
|
59
|
-
Handles key-pair authentication by loading the private key and
|
|
60
|
-
handles password authentication by retrieving it from the config.
|
|
61
|
-
"""
|
|
28
|
+
"""Process authentication credentials from the profile for keypair or password authentication"""
|
|
62
29
|
auth = self._cfg.get("authenticator", "").upper()
|
|
63
30
|
|
|
64
31
|
if auth == "SNOWFLAKE_JWT":
|
|
@@ -67,34 +34,12 @@ class BaseConnector:
|
|
|
67
34
|
self.password = self._cfg["password"]
|
|
68
35
|
|
|
69
36
|
def _process_keypair_auth(self) -> None:
|
|
70
|
-
"""
|
|
71
|
-
Process keypair authentication configuration.
|
|
72
|
-
|
|
73
|
-
If authenticator is SNOWFLAKE_JWT and private_key_file is specified,
|
|
74
|
-
retrieves the passphrase from configured sources and prepares parameters
|
|
75
|
-
for the Snowflake connector.
|
|
76
|
-
|
|
77
|
-
Supported passphrase sources (opt-in only):
|
|
78
|
-
- Environment variable (if private_key_passphrase_env is set)
|
|
79
|
-
- System keyring/Windows Credential Manager (if use_keyring or keyring_* params are set)
|
|
80
|
-
- None (for unencrypted keys on encrypted drives)
|
|
81
|
-
|
|
82
|
-
Security:
|
|
83
|
-
- Only absolute paths or home directory (~) are allowed
|
|
84
|
-
- Relative paths are rejected to prevent keys in version control
|
|
85
|
-
- Direct passphrase in config is NOT supported
|
|
86
|
-
|
|
87
|
-
Note: This method populates self.private_key with a deserialized
|
|
88
|
-
key object from the cryptography library and adds the passphrase
|
|
89
|
-
to self._cfg for the Snowflake connector.
|
|
90
|
-
"""
|
|
37
|
+
"""Process keypair authentication by loading and deserializing the private key with optional passphrase"""
|
|
91
38
|
key_path, passphrase = self._get_key_details()
|
|
92
39
|
|
|
93
|
-
# If we found a passphrase, add it to config for the Snowflake connector
|
|
94
40
|
if passphrase:
|
|
95
41
|
self._cfg["private_key_file_pwd"] = passphrase.get_secret_value()
|
|
96
42
|
|
|
97
|
-
# Read the private key bytes and deserialize for SQLAlchemy
|
|
98
43
|
try:
|
|
99
44
|
with open(key_path, "rb") as key_file:
|
|
100
45
|
p_key_bytes = key_file.read()
|
|
@@ -108,20 +53,13 @@ class BaseConnector:
|
|
|
108
53
|
raise IOError(f"Failed to read or decrypt private key from {key_path}: {e}") from e
|
|
109
54
|
|
|
110
55
|
def _get_key_details(self) -> tuple[Path, Optional[SecretStr]]:
|
|
111
|
-
"""
|
|
112
|
-
Validates key path and retrieves the private key passphrase.
|
|
113
|
-
|
|
114
|
-
Returns:
|
|
115
|
-
A tuple containing the resolved Path object for the key file
|
|
116
|
-
and a SecretStr with the passphrase (or None).
|
|
117
|
-
"""
|
|
56
|
+
"""Validate key path and retrieve the private key passphrase from environment variable or keyring"""
|
|
118
57
|
private_key_file = self._cfg.get("private_key_file")
|
|
119
58
|
if not private_key_file:
|
|
120
59
|
raise ValueError(
|
|
121
60
|
"Keypair authentication requires 'private_key_file' in profile configuration"
|
|
122
61
|
)
|
|
123
62
|
|
|
124
|
-
# Resolve file path
|
|
125
63
|
key_path = Path(private_key_file).expanduser()
|
|
126
64
|
|
|
127
65
|
# Only allow absolute paths or home directory expansion
|
|
@@ -133,17 +71,15 @@ class BaseConnector:
|
|
|
133
71
|
if not key_path.exists():
|
|
134
72
|
raise FileNotFoundError(f"Private key file not found: {key_path}")
|
|
135
73
|
|
|
136
|
-
#
|
|
74
|
+
# Passphrase retrieval logic
|
|
137
75
|
passphrase: Optional[SecretStr] = None
|
|
138
76
|
|
|
139
|
-
# 1. Check environment variable
|
|
140
77
|
passphrase_env_var = self._cfg.get("private_key_passphrase_env")
|
|
141
78
|
if passphrase_env_var:
|
|
142
79
|
env_pass = os.environ.get(passphrase_env_var)
|
|
143
80
|
if env_pass:
|
|
144
81
|
passphrase = SecretStr(env_pass)
|
|
145
82
|
|
|
146
|
-
# 2. Check keyring (if env var not found or not set)
|
|
147
83
|
use_keyring = self._cfg.get("use_keyring", False)
|
|
148
84
|
keyring_service = self._cfg.get("keyring_service", f"snowlib.{self._profile}")
|
|
149
85
|
keyring_username = self._cfg.get("keyring_username", self._cfg.get("user"))
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Snowflake connection management with profile support."""
|
|
2
|
+
|
|
3
|
+
from snowflake.connector import SnowflakeConnection
|
|
4
|
+
from snowflake.connector.cursor import SnowflakeCursor
|
|
5
|
+
import snowflake.connector
|
|
6
|
+
from typing import Optional, Tuple, Any, Literal
|
|
7
|
+
|
|
8
|
+
from .base import BaseConnector
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SnowflakeConnector(BaseConnector):
|
|
12
|
+
"""Snowflake connection manager with TOML profile support and context manager protocol"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, profile: str, **kwargs: Any) -> None:
|
|
15
|
+
"""Initialize the connector with a configuration profile and optional parameter overrides"""
|
|
16
|
+
super().__init__(profile, **kwargs)
|
|
17
|
+
|
|
18
|
+
self._connection: Optional[SnowflakeConnection] = None
|
|
19
|
+
self._cursor: Optional[SnowflakeCursor] = None
|
|
20
|
+
|
|
21
|
+
def connect(self) -> Tuple[SnowflakeConnection, SnowflakeCursor]:
|
|
22
|
+
"""Establish connection to Snowflake if not already connected and return connection and cursor"""
|
|
23
|
+
if self._connection is None:
|
|
24
|
+
self._connection = snowflake.connector.connect(**self._cfg) # type: ignore[misc]
|
|
25
|
+
self._cursor = self._connection.cursor()
|
|
26
|
+
|
|
27
|
+
assert self._connection is not None
|
|
28
|
+
assert self._cursor is not None
|
|
29
|
+
return self._connection, self._cursor
|
|
30
|
+
|
|
31
|
+
def close(self) -> None:
|
|
32
|
+
"""Close the cursor and connection to release resources"""
|
|
33
|
+
if self._cursor:
|
|
34
|
+
self._cursor.close()
|
|
35
|
+
self._cursor = None
|
|
36
|
+
if self._connection:
|
|
37
|
+
self._connection.close()
|
|
38
|
+
self._connection = None
|
|
39
|
+
|
|
40
|
+
def __enter__(self) -> Tuple[SnowflakeConnection, SnowflakeCursor]:
|
|
41
|
+
"""Establish connection and return connection and cursor for context manager"""
|
|
42
|
+
return self.connect()
|
|
43
|
+
|
|
44
|
+
def __exit__(
|
|
45
|
+
self,
|
|
46
|
+
exc_type: Any,
|
|
47
|
+
exc_val: Any,
|
|
48
|
+
exc_tb: Any
|
|
49
|
+
) -> Literal[False]:
|
|
50
|
+
"""Close connection and propagate any exceptions"""
|
|
51
|
+
self.close()
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
def __repr__(self) -> str:
|
|
55
|
+
"""Return string representation showing profile name and connection status"""
|
|
56
|
+
status = "connected" if self._connection else "not connected"
|
|
57
|
+
return f"SnowflakeConnector(profile='{self._profile}', {status})"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class SnowparkConnector(BaseConnector):
|
|
61
|
+
"""Snowpark session manager with TOML profile support"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, profile: str, **kwargs: Any) -> None:
|
|
64
|
+
"""Initialize the Snowpark connector with a configuration profile and optional parameter overrides"""
|
|
65
|
+
try:
|
|
66
|
+
from snowflake.snowpark import Session
|
|
67
|
+
except ImportError:
|
|
68
|
+
raise ImportError(
|
|
69
|
+
"Snowpark is not installed. " +
|
|
70
|
+
"Install it with: pip install snowflake-snowpark-python"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
super().__init__(profile, **kwargs)
|
|
74
|
+
|
|
75
|
+
self._session: Optional[Any] = None
|
|
76
|
+
self._Session = Session
|
|
77
|
+
|
|
78
|
+
def session(self) -> Any:
|
|
79
|
+
"""Get or create a Snowpark Session"""
|
|
80
|
+
if self._session is None:
|
|
81
|
+
self._session = self._Session.builder.configs(self._cfg).create()
|
|
82
|
+
return self._session
|
|
83
|
+
|
|
84
|
+
def close(self) -> None:
|
|
85
|
+
"""Close the Snowpark session"""
|
|
86
|
+
if self._session:
|
|
87
|
+
self._session.close()
|
|
88
|
+
self._session = None
|
|
89
|
+
|
|
90
|
+
def __repr__(self) -> str:
|
|
91
|
+
"""Return string representation showing profile name and session status"""
|
|
92
|
+
status = "active" if self._session else "inactive"
|
|
93
|
+
return f"SnowparkConnector(profile='{self._profile}', {status})"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Path resolution for snowlib configuration files."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional, Union
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from importlib.resources import files as importlib_files
|
|
9
|
+
except ImportError:
|
|
10
|
+
# Python < 3.9 fallback
|
|
11
|
+
from importlib_resources import files as importlib_files # type: ignore
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _get_config_directory() -> Path:
|
|
15
|
+
"""Get the configuration directory for snowlib using SNOWLIB_CONFIG_DIR environment variable or ~/.snowlib dotfile directory"""
|
|
16
|
+
env_config_dir = os.getenv("SNOWLIB_CONFIG_DIR")
|
|
17
|
+
if env_config_dir:
|
|
18
|
+
return Path(env_config_dir)
|
|
19
|
+
|
|
20
|
+
dotfile_config = Path.home() / ".snowlib"
|
|
21
|
+
dotfile_config.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
return dotfile_config
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _get_example_files_dir() -> Path:
|
|
26
|
+
"""Get the directory containing example configuration files from the installed package"""
|
|
27
|
+
package_data = importlib_files("snowlib") / "_data"
|
|
28
|
+
return Path(str(package_data))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Configuration directory (dynamically resolved)
|
|
32
|
+
CONF_DIR = _get_config_directory()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_default_config_path() -> Path:
|
|
36
|
+
"""Get the path to connections.toml configuration file"""
|
|
37
|
+
config_path = CONF_DIR / "connections.toml"
|
|
38
|
+
|
|
39
|
+
if not config_path.exists():
|
|
40
|
+
example_dir = _get_example_files_dir()
|
|
41
|
+
example_file = example_dir / "connections.toml.example"
|
|
42
|
+
|
|
43
|
+
error_msg = (
|
|
44
|
+
f"Configuration file 'connections.toml' not found at: {config_path}\n\n"
|
|
45
|
+
f"To create it:\n"
|
|
46
|
+
f"1. Copy example: {example_file}\n"
|
|
47
|
+
f"2. To: {config_path}\n"
|
|
48
|
+
f"3. Edit with your connection details\n\n"
|
|
49
|
+
f"Configuration directory priority:\n"
|
|
50
|
+
f" 1. SNOWLIB_CONFIG_DIR environment variable (if set)\n"
|
|
51
|
+
f" 2. ~/.snowlib/ (dotfile directory)\n"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
raise FileNotFoundError(error_msg)
|
|
55
|
+
|
|
56
|
+
return config_path
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def resolve_config_path(path: Optional[Union[str, Path]] = None) -> Path:
|
|
60
|
+
"""Resolve the path to connections.toml file using explicit path or default config path"""
|
|
61
|
+
if path is None:
|
|
62
|
+
return get_default_config_path()
|
|
63
|
+
|
|
64
|
+
return Path(path)
|
snowlib-0.2.0/src/snowlib/config/config.py → snowlib-0.3.0/src/snowlib/connection/profiles.py
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Profile loading for Snowflake connections."""
|
|
2
2
|
|
|
3
3
|
import sys
|
|
4
4
|
from pathlib import Path
|
|
@@ -23,26 +23,7 @@ def load_profile(
|
|
|
23
23
|
profile: str,
|
|
24
24
|
path: Optional[Union[str, Path]] = None
|
|
25
25
|
) -> Dict[str, Any]:
|
|
26
|
-
"""
|
|
27
|
-
Load a Snowflake connection profile from connections.toml file.
|
|
28
|
-
|
|
29
|
-
Args:
|
|
30
|
-
profile: Name of the profile to load
|
|
31
|
-
path: Optional explicit path to connections.toml file.
|
|
32
|
-
If None, searches in standard locations.
|
|
33
|
-
|
|
34
|
-
Returns:
|
|
35
|
-
Dictionary containing connection parameters for the profile
|
|
36
|
-
|
|
37
|
-
Raises:
|
|
38
|
-
FileNotFoundError: If connections.toml file is not found
|
|
39
|
-
KeyError: If the specified profile doesn't exist in the file
|
|
40
|
-
|
|
41
|
-
Example:
|
|
42
|
-
>>> config = load_profile("dev")
|
|
43
|
-
>>> config
|
|
44
|
-
{'account': 'myaccount', 'user': 'myuser', ...}
|
|
45
|
-
"""
|
|
26
|
+
"""Load a Snowflake connection profile from connections.toml file"""
|
|
46
27
|
config_file = resolve_config_path(path)
|
|
47
28
|
|
|
48
29
|
if not config_file.exists():
|
|
@@ -65,19 +46,7 @@ def load_profile(
|
|
|
65
46
|
|
|
66
47
|
|
|
67
48
|
def list_profiles(path: Optional[Union[str, Path]] = None) -> list[str]:
|
|
68
|
-
"""
|
|
69
|
-
List all available profiles in connections.toml file.
|
|
70
|
-
|
|
71
|
-
Args:
|
|
72
|
-
path: Optional explicit path to connections.toml file
|
|
73
|
-
|
|
74
|
-
Returns:
|
|
75
|
-
List of profile names
|
|
76
|
-
|
|
77
|
-
Example:
|
|
78
|
-
>>> list_profiles()
|
|
79
|
-
['default', 'dev', 'prod']
|
|
80
|
-
"""
|
|
49
|
+
"""List all available profile names in connections.toml file"""
|
|
81
50
|
config_file = resolve_config_path(path)
|
|
82
51
|
|
|
83
52
|
if not config_file.exists():
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Snowflake connection context management"""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from snowlib.connection import SnowflakeConnector
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SnowflakeContext:
|
|
10
|
+
"""Manages Snowflake connection and cursor lifecycle with lazy initialization"""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
profile: Optional[str] = None,
|
|
15
|
+
connection: Optional[Any] = None,
|
|
16
|
+
cursor: Optional[Any] = None,
|
|
17
|
+
**overrides: Any,
|
|
18
|
+
):
|
|
19
|
+
"""Initialize Snowflake context with profile or connection"""
|
|
20
|
+
if profile is None and connection is None:
|
|
21
|
+
raise ValueError(
|
|
22
|
+
"SnowflakeContext requires either 'profile' or 'connection'"
|
|
23
|
+
)
|
|
24
|
+
if profile is not None and connection is not None:
|
|
25
|
+
raise ValueError(
|
|
26
|
+
"SnowflakeContext: provide either 'profile' or 'connection', not both"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
self._profile = profile
|
|
30
|
+
self._connection = connection
|
|
31
|
+
self._cursor = cursor
|
|
32
|
+
self._overrides = overrides
|
|
33
|
+
self._connector: Optional["SnowflakeConnector"] = None
|
|
34
|
+
self._owns_connector = False
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def connection(self) -> Any:
|
|
38
|
+
"""Get Snowflake connection, creating if needed"""
|
|
39
|
+
if self._connection is None:
|
|
40
|
+
from snowlib.connection import SnowflakeConnector
|
|
41
|
+
|
|
42
|
+
assert self._profile is not None
|
|
43
|
+
self._connector = SnowflakeConnector(
|
|
44
|
+
profile=self._profile, **self._overrides
|
|
45
|
+
)
|
|
46
|
+
conn, cur = self._connector.connect()
|
|
47
|
+
self._connection = conn
|
|
48
|
+
self._cursor = cur
|
|
49
|
+
self._owns_connector = True
|
|
50
|
+
|
|
51
|
+
return self._connection
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def cursor(self) -> Any:
|
|
55
|
+
"""Get Snowflake cursor, creating if needed"""
|
|
56
|
+
if self._cursor is None:
|
|
57
|
+
self._cursor = self.connection.cursor()
|
|
58
|
+
|
|
59
|
+
return self._cursor
|
|
60
|
+
|
|
61
|
+
def close(self) -> None:
|
|
62
|
+
"""Close connection if owned by this context"""
|
|
63
|
+
if self._owns_connector and self._connector is not None:
|
|
64
|
+
self._connector.close()
|
|
65
|
+
self._connector = None
|
|
66
|
+
self._connection = None
|
|
67
|
+
self._cursor = None
|
|
68
|
+
|
|
69
|
+
def __enter__(self) -> "SnowflakeContext":
|
|
70
|
+
"""Context manager entry"""
|
|
71
|
+
return self
|
|
72
|
+
|
|
73
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
74
|
+
"""Context manager exit"""
|
|
75
|
+
self.close()
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def current_database(self) -> str:
|
|
79
|
+
"""Get current database from session context"""
|
|
80
|
+
result = self.cursor.execute("SELECT CURRENT_DATABASE()").fetchone()
|
|
81
|
+
return str(result[0]) if result and result[0] else ""
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def current_schema(self) -> str:
|
|
85
|
+
"""Get current schema from session context"""
|
|
86
|
+
result = self.cursor.execute("SELECT CURRENT_SCHEMA()").fetchone()
|
|
87
|
+
return str(result[0]) if result and result[0] else ""
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def current_warehouse(self) -> str:
|
|
91
|
+
"""Get current warehouse from session context"""
|
|
92
|
+
result = self.cursor.execute("SELECT CURRENT_WAREHOUSE()").fetchone()
|
|
93
|
+
return str(result[0]) if result and result[0] else ""
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def current_role(self) -> str:
|
|
97
|
+
"""Get current role from session context"""
|
|
98
|
+
result = self.cursor.execute("SELECT CURRENT_ROLE()").fetchone()
|
|
99
|
+
return str(result[0]) if result and result[0] else ""
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def current_user(self) -> str:
|
|
103
|
+
"""Get current user from session context"""
|
|
104
|
+
result = self.cursor.execute("SELECT CURRENT_USER()").fetchone()
|
|
105
|
+
return str(result[0]) if result and result[0] else ""
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def current_account(self) -> str:
|
|
109
|
+
"""Get current account identifier from session context"""
|
|
110
|
+
result = self.cursor.execute("SELECT CURRENT_ACCOUNT()").fetchone()
|
|
111
|
+
return str(result[0]) if result and result[0] else ""
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def current_region(self) -> str:
|
|
115
|
+
"""Get current region from session context"""
|
|
116
|
+
result = self.cursor.execute("SELECT CURRENT_REGION()").fetchone()
|
|
117
|
+
return str(result[0]) if result and result[0] else ""
|
|
118
|
+
|
|
119
|
+
def __repr__(self) -> str:
|
|
120
|
+
"""String representation"""
|
|
121
|
+
if self._connection is not None:
|
|
122
|
+
return f"SnowflakeContext(connection=<active>)"
|
|
123
|
+
else:
|
|
124
|
+
return f"SnowflakeContext(profile='{self._profile}')"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Snowflake object-oriented interface for working with Snowflake objects"""
|
|
2
|
+
|
|
3
|
+
from .database import Database
|
|
4
|
+
from .schema import Schema
|
|
5
|
+
from .table import Table, View, MaterializedView, DynamicTable, TableLike
|
|
6
|
+
from .column import Column
|
|
7
|
+
from .base.show import Show
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"Database",
|
|
11
|
+
"Schema",
|
|
12
|
+
"Table",
|
|
13
|
+
"View",
|
|
14
|
+
"MaterializedView",
|
|
15
|
+
"DynamicTable",
|
|
16
|
+
"Column",
|
|
17
|
+
"TableLike",
|
|
18
|
+
"Show",
|
|
19
|
+
]
|