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.
Files changed (62) hide show
  1. {snowlib-0.2.0 → snowlib-0.3.0}/PKG-INFO +8 -6
  2. {snowlib-0.2.0 → snowlib-0.3.0}/README.md +6 -1
  3. {snowlib-0.2.0 → snowlib-0.3.0}/pyproject.toml +1 -5
  4. snowlib-0.3.0/src/snowlib/__init__.py +68 -0
  5. snowlib-0.3.0/src/snowlib/connection/__init__.py +15 -0
  6. {snowlib-0.2.0 → snowlib-0.3.0}/src/snowlib/connection/base.py +7 -71
  7. snowlib-0.3.0/src/snowlib/connection/connection.py +93 -0
  8. snowlib-0.3.0/src/snowlib/connection/paths.py +64 -0
  9. snowlib-0.2.0/src/snowlib/config/config.py → snowlib-0.3.0/src/snowlib/connection/profiles.py +3 -34
  10. snowlib-0.3.0/src/snowlib/context.py +124 -0
  11. snowlib-0.3.0/src/snowlib/models/__init__.py +19 -0
  12. snowlib-0.3.0/src/snowlib/models/base/__init__.py +12 -0
  13. snowlib-0.3.0/src/snowlib/models/base/core.py +152 -0
  14. snowlib-0.3.0/src/snowlib/models/base/fqn.py +98 -0
  15. snowlib-0.3.0/src/snowlib/models/base/schema_child.py +85 -0
  16. snowlib-0.3.0/src/snowlib/models/base/show.py +98 -0
  17. snowlib-0.3.0/src/snowlib/models/column.py +69 -0
  18. snowlib-0.3.0/src/snowlib/models/database.py +56 -0
  19. snowlib-0.3.0/src/snowlib/models/schema.py +100 -0
  20. snowlib-0.3.0/src/snowlib/models/table/__init__.py +15 -0
  21. snowlib-0.3.0/src/snowlib/models/table/base.py +105 -0
  22. snowlib-0.3.0/src/snowlib/models/table/dynamic_table.py +44 -0
  23. snowlib-0.3.0/src/snowlib/models/table/materialized_view.py +20 -0
  24. snowlib-0.3.0/src/snowlib/models/table/table.py +51 -0
  25. snowlib-0.3.0/src/snowlib/models/table/view.py +20 -0
  26. snowlib-0.3.0/src/snowlib/primitives/__init__.py +29 -0
  27. snowlib-0.3.0/src/snowlib/primitives/async_query.py +75 -0
  28. snowlib-0.3.0/src/snowlib/primitives/execute.py +98 -0
  29. snowlib-0.3.0/src/snowlib/primitives/result.py +83 -0
  30. snowlib-0.3.0/src/snowlib/sqlalchemy.py +53 -0
  31. snowlib-0.3.0/src/snowlib/utils/identifiers.py +13 -0
  32. snowlib-0.3.0/src/snowlib/utils/query.py +32 -0
  33. {snowlib-0.2.0 → snowlib-0.3.0}/src/snowlib.egg-info/PKG-INFO +8 -6
  34. snowlib-0.3.0/src/snowlib.egg-info/SOURCES.txt +41 -0
  35. {snowlib-0.2.0 → snowlib-0.3.0}/src/snowlib.egg-info/requires.txt +1 -7
  36. snowlib-0.2.0/src/snowlib/__init__.py +0 -85
  37. snowlib-0.2.0/src/snowlib/config/__init__.py +0 -12
  38. snowlib-0.2.0/src/snowlib/config/paths.py +0 -133
  39. snowlib-0.2.0/src/snowlib/connection/__init__.py +0 -5
  40. snowlib-0.2.0/src/snowlib/connection/connection.py +0 -161
  41. snowlib-0.2.0/src/snowlib/io/__init__.py +0 -12
  42. snowlib-0.2.0/src/snowlib/io/names.py +0 -242
  43. snowlib-0.2.0/src/snowlib/io/read.py +0 -125
  44. snowlib-0.2.0/src/snowlib/io/write.py +0 -103
  45. snowlib-0.2.0/src/snowlib/primitives/__init__.py +0 -60
  46. snowlib-0.2.0/src/snowlib/primitives/context.py +0 -150
  47. snowlib-0.2.0/src/snowlib/primitives/data.py +0 -176
  48. snowlib-0.2.0/src/snowlib/primitives/execution.py +0 -274
  49. snowlib-0.2.0/src/snowlib/primitives/job.py +0 -121
  50. snowlib-0.2.0/src/snowlib/primitives/metadata.py +0 -277
  51. snowlib-0.2.0/src/snowlib/primitives/result.py +0 -134
  52. snowlib-0.2.0/src/snowlib/primitives/streaming.py +0 -63
  53. snowlib-0.2.0/src/snowlib/sqlalchemy.py +0 -94
  54. snowlib-0.2.0/src/snowlib.egg-info/SOURCES.txt +0 -32
  55. {snowlib-0.2.0 → snowlib-0.3.0}/LICENSE +0 -0
  56. {snowlib-0.2.0 → snowlib-0.3.0}/licenses/THIRD_PARTY.md +0 -0
  57. {snowlib-0.2.0 → snowlib-0.3.0}/setup.cfg +0 -0
  58. {snowlib-0.2.0 → snowlib-0.3.0}/src/snowlib/_data/connections.toml.example +0 -0
  59. {snowlib-0.2.0 → snowlib-0.3.0}/src/snowlib/_data/test_config.toml.example +0 -0
  60. {snowlib-0.2.0 → snowlib-0.3.0}/src/snowlib/py.typed +0 -0
  61. {snowlib-0.2.0 → snowlib-0.3.0}/src/snowlib.egg-info/dependency_links.txt +0 -0
  62. {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.2.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,pandas,snowpark,sqlalchemy]; extra == "dev"
61
+ Requires-Dist: snowlib[excel,snowpark,sqlalchemy]; extra == "dev"
65
62
  Dynamic: license-file
66
63
 
67
64
  # ❄️ SnowLib
68
65
 
66
+ [![PyPI](https://img.shields.io/pypi/v/snowlib.svg)](https://pypi.org/project/snowlib/)
67
+ [![Python Versions](https://img.shields.io/pypi/pyversions/snowlib.svg)](https://pypi.org/project/snowlib/)
68
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
69
+ [![DOI (latest)](https://zenodo.org/badge/DOI/10.5281/zenodo.17354788.svg)](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
+ [![PyPI](https://img.shields.io/pypi/v/snowlib.svg)](https://pypi.org/project/snowlib/)
4
+ [![Python Versions](https://img.shields.io/pypi/pyversions/snowlib.svg)](https://pypi.org/project/snowlib/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
6
+ [![DOI (latest)](https://zenodo.org/badge/DOI/10.5281/zenodo.17354788.svg)](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,pandas,snowpark,excel]",
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 ..config import load_profile
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
- # --- Passphrase retrieval logic ---
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)
@@ -1,4 +1,4 @@
1
- """Configuration loading for Snowflake connection profiles."""
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
+ ]
@@ -0,0 +1,12 @@
1
+ """Base classes for Snowflake object models"""
2
+
3
+ from .core import SnowflakeObject, Container
4
+ from .schema_child import SchemaChild
5
+ from .fqn import FQN
6
+
7
+ __all__ = [
8
+ "SnowflakeObject",
9
+ "Container",
10
+ "SchemaChild",
11
+ "FQN",
12
+ ]