dbt-logger 0.1.1__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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tristan Schwartz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.1
2
+ Name: dbt-logger
3
+ Version: 0.1.1
4
+ Summary: A lightweight logging utility for dbt projects
5
+ Home-page: https://github.com/TristanSchwartz/dbt-logger
6
+ License: MIT
7
+ Keywords: dbt,logging,data,analytics
8
+ Author: Tristan Schwartz
9
+ Requires-Python: >=3.9.2,<3.13
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Requires-Dist: cryptography (>=41.0.0,<42.0.0)
20
+ Requires-Dist: pyyaml (>=6.0.0,<7.0.0)
21
+ Requires-Dist: snowflake-connector-python (>=3.0.0,<4.0.0)
22
+ Project-URL: Repository, https://github.com/TristanSchwartz/dbt-logger
23
+ Description-Content-Type: text/markdown
24
+
25
+ # dbt-logger
26
+
27
+ A logging utility that captures dbt command output and stores it in Snowflake for auditing and monitoring. **Uses your existing dbt credentials - no additional configuration needed!**
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install dbt-logger
33
+ ```
34
+
35
+ ## Quick Start
36
+
37
+ The simplest way - uses your existing dbt profiles.yml:
38
+
39
+ ```python
40
+ from dbt_logger import DbtLogger
41
+
42
+ # Automatically uses credentials from dbt profiles.yml
43
+ logger = DbtLogger(repo_name="my_analytics_project")
44
+
45
+ # Run dbt commands and log them
46
+ logger.run_command("dbt build")
47
+ logger.run_command("dbt run --select +sales_model")
48
+ ```
49
+
50
+ That's it! Logs are automatically saved to `[your_dbt_database].logging.dbt_logging`
51
+
52
+ ## Configuration File (Optional)
53
+
54
+ Create `dbt/dbt_logger.yml` in your dbt project to configure logging destination:
55
+
56
+ ```yaml
57
+ # dbt/dbt_logger.yml
58
+
59
+ # Option 1: Custom schema in dbt database
60
+ schema: audit_logs
61
+
62
+ # Option 2: Completely separate database for logs
63
+ database: ADMIN_DB
64
+ schema: LOGGING
65
+ table_name: dbt_execution_logs
66
+ ```
67
+
68
+ **Precedence order:**
69
+ 1. Parameters passed to `DbtLogger()` (highest)
70
+ 2. Settings in `dbt/dbt_logger.yml`
71
+ 3. Defaults: `{dbt_database}.logging.dbt_logging` (lowest)
72
+
73
+ ## Configuration Options
74
+
75
+ ### Use Specific dbt Profile/Target
76
+
77
+ ```python
78
+ # Use a specific profile and target from profiles.yml
79
+ logger = DbtLogger(
80
+ repo_name="my_project",
81
+ profile_name="my_profile", # Optional: defaults to first profile
82
+ target="prod" # Optional: defaults to profile's default target
83
+ )
84
+ ```
85
+
86
+ ### Override Logging Location in Code
87
+
88
+ ```python
89
+ # Override config file settings
90
+ logger = DbtLogger(
91
+ repo_name="my_project",
92
+ database="ADMIN_DB",
93
+ schema="LOGGING",
94
+ table_name="my_custom_logs"
95
+ )
96
+ ```
97
+
98
+ ### Manual Connection Parameters (Advanced)
99
+
100
+ If you need to use different credentials than dbt:
101
+
102
+ ```python
103
+ from dbt_logger import DbtLogger, get_connection_params_from_env
104
+
105
+ # Option 1: From environment variables
106
+ connection_params = get_connection_params_from_env()
107
+
108
+ # Option 2: Manual dict
109
+ connection_params = {
110
+ 'account': 'your_account',
111
+ 'user': 'your_user',
112
+ 'password': 'your_password',
113
+ 'database': 'your_database',
114
+ 'warehouse': 'your_warehouse',
115
+ }
116
+
117
+ logger = DbtLogger(
118
+ repo_name="my_project",
119
+ connection_params=connection_params
120
+ )
121
+ ```
122
+
123
+ ## How It Works
124
+
125
+ 1. **Reads your dbt profiles.yml** - Automatically finds and parses your dbt configuration
126
+ 2. **Supports private key auth** - Works with your existing DBT_USER, DBT_PVK_PATH, DBT_PVK_PASS env vars
127
+ 3. **Creates logging infrastructure** - Auto-creates schema and table on first run
128
+ 4. **Captures everything** - Runs dbt commands and logs all output in real-time
129
+ 5. **Stores in Snowflake** - Saves command history for auditing and monitoring
130
+
131
+ ## Logging Table Schema
132
+
133
+ ```sql
134
+ -- Default location: [your_dbt_database].logging.dbt_logging
135
+ CREATE TABLE dbt_logging (
136
+ repo_name STRING, -- Your dbt project/repo name
137
+ command STRING, -- The dbt command executed
138
+ stdout_log STRING, -- Complete command output
139
+ log_time TIMESTAMP_LTZ -- Execution timestamp
140
+ );
141
+ ```
142
+
143
+ ## dbt profiles.yml Locations
144
+
145
+ The logger checks for profiles.yml in this order:
146
+ 1. `$DBT_PROFILES_DIR/profiles.yml`
147
+ 2. `./dbt/profiles/profiles.yml` (current directory)
148
+ 3. `~/.dbt/profiles.yml` (default dbt location)
149
+
150
+ ## Features
151
+
152
+ - ✅ **Zero Config** - Uses your existing dbt credentials
153
+ - ✅ **Private Key Auth** - Full support for private key authentication
154
+ - ✅ **Auto Setup** - Creates schema and table automatically
155
+ - ✅ **Real-time Output** - See command output live while logging
156
+ - ✅ **Flexible Storage** - Default to dbt database or use separate logging database
157
+ - ✅ **Environment Variables** - Resolves Jinja env_var() expressions from profiles.yml
158
+
159
+ ## License
160
+
161
+ MIT License - see LICENSE file for details.
@@ -0,0 +1,137 @@
1
+ # dbt-logger
2
+
3
+ A logging utility that captures dbt command output and stores it in Snowflake for auditing and monitoring. **Uses your existing dbt credentials - no additional configuration needed!**
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install dbt-logger
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ The simplest way - uses your existing dbt profiles.yml:
14
+
15
+ ```python
16
+ from dbt_logger import DbtLogger
17
+
18
+ # Automatically uses credentials from dbt profiles.yml
19
+ logger = DbtLogger(repo_name="my_analytics_project")
20
+
21
+ # Run dbt commands and log them
22
+ logger.run_command("dbt build")
23
+ logger.run_command("dbt run --select +sales_model")
24
+ ```
25
+
26
+ That's it! Logs are automatically saved to `[your_dbt_database].logging.dbt_logging`
27
+
28
+ ## Configuration File (Optional)
29
+
30
+ Create `dbt/dbt_logger.yml` in your dbt project to configure logging destination:
31
+
32
+ ```yaml
33
+ # dbt/dbt_logger.yml
34
+
35
+ # Option 1: Custom schema in dbt database
36
+ schema: audit_logs
37
+
38
+ # Option 2: Completely separate database for logs
39
+ database: ADMIN_DB
40
+ schema: LOGGING
41
+ table_name: dbt_execution_logs
42
+ ```
43
+
44
+ **Precedence order:**
45
+ 1. Parameters passed to `DbtLogger()` (highest)
46
+ 2. Settings in `dbt/dbt_logger.yml`
47
+ 3. Defaults: `{dbt_database}.logging.dbt_logging` (lowest)
48
+
49
+ ## Configuration Options
50
+
51
+ ### Use Specific dbt Profile/Target
52
+
53
+ ```python
54
+ # Use a specific profile and target from profiles.yml
55
+ logger = DbtLogger(
56
+ repo_name="my_project",
57
+ profile_name="my_profile", # Optional: defaults to first profile
58
+ target="prod" # Optional: defaults to profile's default target
59
+ )
60
+ ```
61
+
62
+ ### Override Logging Location in Code
63
+
64
+ ```python
65
+ # Override config file settings
66
+ logger = DbtLogger(
67
+ repo_name="my_project",
68
+ database="ADMIN_DB",
69
+ schema="LOGGING",
70
+ table_name="my_custom_logs"
71
+ )
72
+ ```
73
+
74
+ ### Manual Connection Parameters (Advanced)
75
+
76
+ If you need to use different credentials than dbt:
77
+
78
+ ```python
79
+ from dbt_logger import DbtLogger, get_connection_params_from_env
80
+
81
+ # Option 1: From environment variables
82
+ connection_params = get_connection_params_from_env()
83
+
84
+ # Option 2: Manual dict
85
+ connection_params = {
86
+ 'account': 'your_account',
87
+ 'user': 'your_user',
88
+ 'password': 'your_password',
89
+ 'database': 'your_database',
90
+ 'warehouse': 'your_warehouse',
91
+ }
92
+
93
+ logger = DbtLogger(
94
+ repo_name="my_project",
95
+ connection_params=connection_params
96
+ )
97
+ ```
98
+
99
+ ## How It Works
100
+
101
+ 1. **Reads your dbt profiles.yml** - Automatically finds and parses your dbt configuration
102
+ 2. **Supports private key auth** - Works with your existing DBT_USER, DBT_PVK_PATH, DBT_PVK_PASS env vars
103
+ 3. **Creates logging infrastructure** - Auto-creates schema and table on first run
104
+ 4. **Captures everything** - Runs dbt commands and logs all output in real-time
105
+ 5. **Stores in Snowflake** - Saves command history for auditing and monitoring
106
+
107
+ ## Logging Table Schema
108
+
109
+ ```sql
110
+ -- Default location: [your_dbt_database].logging.dbt_logging
111
+ CREATE TABLE dbt_logging (
112
+ repo_name STRING, -- Your dbt project/repo name
113
+ command STRING, -- The dbt command executed
114
+ stdout_log STRING, -- Complete command output
115
+ log_time TIMESTAMP_LTZ -- Execution timestamp
116
+ );
117
+ ```
118
+
119
+ ## dbt profiles.yml Locations
120
+
121
+ The logger checks for profiles.yml in this order:
122
+ 1. `$DBT_PROFILES_DIR/profiles.yml`
123
+ 2. `./dbt/profiles/profiles.yml` (current directory)
124
+ 3. `~/.dbt/profiles.yml` (default dbt location)
125
+
126
+ ## Features
127
+
128
+ - ✅ **Zero Config** - Uses your existing dbt credentials
129
+ - ✅ **Private Key Auth** - Full support for private key authentication
130
+ - ✅ **Auto Setup** - Creates schema and table automatically
131
+ - ✅ **Real-time Output** - See command output live while logging
132
+ - ✅ **Flexible Storage** - Default to dbt database or use separate logging database
133
+ - ✅ **Environment Variables** - Resolves Jinja env_var() expressions from profiles.yml
134
+
135
+ ## License
136
+
137
+ MIT License - see LICENSE file for details.
@@ -0,0 +1,7 @@
1
+ """dbt-logger: A logger for dbt projects"""
2
+
3
+ from .logger import DbtLogger
4
+ from .utils import get_connection_params_from_dbt, get_connection_params_from_env
5
+
6
+ __version__ = "0.1.0"
7
+ __all__ = ["DbtLogger", "get_connection_params_from_dbt", "get_connection_params_from_env"]
@@ -0,0 +1,6 @@
1
+ """Allow running dbt-logger as a module: python -m dbt_logger"""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == '__main__':
6
+ main()
@@ -0,0 +1,159 @@
1
+ """CLI interface for dbt-logger."""
2
+
3
+ import argparse
4
+ import sys
5
+ from typing import Optional
6
+ from .utils import get_connection_params_from_dbt, get_logger_config, get_dbt_profile_path
7
+ from .logger import DbtLogger
8
+
9
+
10
+ def show_config(repo_name: str, target: Optional[str] = None):
11
+ """Show the current configuration and where logs will be written."""
12
+ print("=" * 60)
13
+ print("dbt-logger Configuration")
14
+ print("=" * 60)
15
+
16
+ try:
17
+ # Find profiles.yml
18
+ profiles_path = get_dbt_profile_path()
19
+ print(f"✓ profiles.yml: {profiles_path}")
20
+
21
+ # Get connection params
22
+ connection_params = get_connection_params_from_dbt(target=target)
23
+ dbt_database = connection_params.get('database')
24
+ print(f"✓ dbt database: {dbt_database}")
25
+ print(f"✓ target: {target or 'default'}")
26
+
27
+ # Get logger config
28
+ config = get_logger_config()
29
+ if config:
30
+ print(f"✓ Config file found with: {config}")
31
+ else:
32
+ print(" No dbt_logger.yml config file found (using defaults)")
33
+
34
+ # Determine final logging location
35
+ database = config.get('database') or dbt_database
36
+ schema = config.get('schema') or "logging"
37
+ table_name = config.get('table_name') or "dbt_logging"
38
+
39
+ print("\n" + "=" * 60)
40
+ print("Logs will be written to:")
41
+ print("=" * 60)
42
+ print(f" Database: {database}")
43
+ print(f" Schema: {schema}")
44
+ print(f" Table: {table_name}")
45
+ print(f" Full path: {database}.{schema}.{table_name}")
46
+ print("=" * 60)
47
+
48
+ except Exception as e:
49
+ print(f"✗ Error: {e}", file=sys.stderr)
50
+ sys.exit(1)
51
+
52
+
53
+ def test_connection(repo_name: str, target: Optional[str] = None):
54
+ """Test the Snowflake connection."""
55
+ print("=" * 60)
56
+ print("Testing Snowflake Connection")
57
+ print("=" * 60)
58
+
59
+ try:
60
+ connection_params = get_connection_params_from_dbt(target=target)
61
+ print(f"✓ Loaded credentials for target: {target or 'default'}")
62
+
63
+ # Try to create logger (this will test connection)
64
+ logger = DbtLogger(repo_name=repo_name, target=target)
65
+ print(f"✓ Connected to Snowflake")
66
+ print(f"✓ Verified/created: {logger.full_table_name}")
67
+ print("\n" + "=" * 60)
68
+ print("Connection test PASSED ✓")
69
+ print("=" * 60)
70
+
71
+ except Exception as e:
72
+ print(f"\n✗ Connection test FAILED", file=sys.stderr)
73
+ print(f"✗ Error: {e}", file=sys.stderr)
74
+ print("=" * 60)
75
+ sys.exit(1)
76
+
77
+
78
+ def run_test_command(repo_name: str, target: Optional[str] = None):
79
+ """Run a test dbt command and log it."""
80
+ print("=" * 60)
81
+ print("Running Test Command")
82
+ print("=" * 60)
83
+
84
+ try:
85
+ logger = DbtLogger(repo_name=repo_name, target=target)
86
+ print(f"Logging to: {logger.full_table_name}\n")
87
+
88
+ # Run a simple dbt command
89
+ exit_code = logger.run_command("dbt --version")
90
+
91
+ if exit_code == 0:
92
+ print("\n" + "=" * 60)
93
+ print("Test PASSED ✓")
94
+ print(f"Check {logger.full_table_name} for the logged output")
95
+ print("=" * 60)
96
+ else:
97
+ print(f"\nCommand exited with code: {exit_code}")
98
+
99
+ except Exception as e:
100
+ print(f"✗ Error: {e}", file=sys.stderr)
101
+ sys.exit(1)
102
+
103
+
104
+ def main():
105
+ """Main CLI entry point."""
106
+ parser = argparse.ArgumentParser(
107
+ prog='dbt-logger',
108
+ description='Test and configure dbt-logger'
109
+ )
110
+
111
+ parser.add_argument(
112
+ '--show-config',
113
+ action='store_true',
114
+ help='Show where logs will be written'
115
+ )
116
+
117
+ parser.add_argument(
118
+ '--test-connection',
119
+ action='store_true',
120
+ help='Test the Snowflake connection'
121
+ )
122
+
123
+ parser.add_argument(
124
+ '--test-run',
125
+ action='store_true',
126
+ help='Run a test command (dbt --version) and log it'
127
+ )
128
+
129
+ parser.add_argument(
130
+ '--repo-name',
131
+ default='test_repo',
132
+ help='Repository name for testing (default: test_repo)'
133
+ )
134
+
135
+ parser.add_argument(
136
+ '--target',
137
+ help='dbt target to use (e.g., dev, prod)'
138
+ )
139
+
140
+ args = parser.parse_args()
141
+
142
+ # If no action specified, show help
143
+ if not any([args.show_config, args.test_connection, args.test_run]):
144
+ parser.print_help()
145
+ sys.exit(0)
146
+
147
+ # Run requested actions
148
+ if args.show_config:
149
+ show_config(args.repo_name, args.target)
150
+
151
+ if args.test_connection:
152
+ test_connection(args.repo_name, args.target)
153
+
154
+ if args.test_run:
155
+ run_test_command(args.repo_name, args.target)
156
+
157
+
158
+ if __name__ == '__main__':
159
+ main()
@@ -0,0 +1,161 @@
1
+ """Main logger implementation for dbt projects."""
2
+
3
+ import subprocess
4
+ import sys
5
+ from datetime import datetime
6
+ from typing import Optional, Dict, Any
7
+ import snowflake.connector
8
+ from .utils import get_connection_params_from_dbt, get_logger_config
9
+
10
+
11
+ class DbtLogger:
12
+ """A logger that captures dbt command output and logs to Snowflake."""
13
+
14
+ def __init__(
15
+ self,
16
+ repo_name: str,
17
+ connection_params: Optional[Dict[str, Any]] = None,
18
+ profile_name: Optional[str] = None,
19
+ target: Optional[str] = None,
20
+ table_name: Optional[str] = None,
21
+ schema: Optional[str] = None,
22
+ database: Optional[str] = None
23
+ ):
24
+ """
25
+ Initialize the DbtLogger.
26
+
27
+ Args:
28
+ repo_name: Name of the dbt project/repo
29
+ connection_params: Snowflake connection parameters (if None, reads from dbt profiles.yml)
30
+ profile_name: dbt profile name (only used if connection_params is None)
31
+ target: dbt target name like 'dev' or 'prod' (only used if connection_params is None)
32
+ table_name: Name of the logging table (overrides config file)
33
+ schema: Schema name (overrides config file)
34
+ database: Database name (overrides config file)
35
+ """
36
+ self.repo_name = repo_name
37
+
38
+ # Get connection params from dbt profiles if not provided
39
+ if connection_params is None:
40
+ connection_params = get_connection_params_from_dbt(
41
+ profile_name=profile_name,
42
+ target=target
43
+ )
44
+
45
+ self.connection_params = connection_params
46
+
47
+ # Load config file settings
48
+ config = get_logger_config()
49
+
50
+ # Determine database, schema, and table name with precedence:
51
+ # 1. Explicit parameter
52
+ # 2. Config file
53
+ # 3. Defaults
54
+ dbt_database = connection_params.get('database')
55
+
56
+ self.database = database or config.get('database') or dbt_database
57
+ self.schema = schema or config.get('schema') or "logging"
58
+ self.table_name = table_name or config.get('table_name') or "dbt_logging"
59
+
60
+ self.full_table_name = f"{self.database}.{self.schema}.{self.table_name}"
61
+
62
+ # Ensure the logging table exists
63
+ self._ensure_table_exists()
64
+
65
+ def _get_connection(self):
66
+ """Get a Snowflake connection."""
67
+ return snowflake.connector.connect(**self.connection_params)
68
+
69
+ def _ensure_table_exists(self):
70
+ """Create the logging schema and table if they don't exist."""
71
+ create_schema_sql = f"CREATE SCHEMA IF NOT EXISTS {self.database}.{self.schema}"
72
+
73
+ create_table_sql = f"""
74
+ CREATE TABLE IF NOT EXISTS {self.full_table_name} (
75
+ repo_name STRING,
76
+ command STRING,
77
+ stdout_log STRING,
78
+ log_time TIMESTAMP_LTZ DEFAULT CURRENT_TIMESTAMP()
79
+ )
80
+ """
81
+
82
+ try:
83
+ conn = self._get_connection()
84
+ cursor = conn.cursor()
85
+ cursor.execute(create_schema_sql)
86
+ cursor.execute(create_table_sql)
87
+ cursor.close()
88
+ conn.close()
89
+ except Exception as e:
90
+ print(f"Warning: Could not create logging table: {e}")
91
+
92
+ def run_command(self, command: str) -> int:
93
+ """
94
+ Run a dbt command and log its output to Snowflake.
95
+
96
+ Args:
97
+ command: The full dbt command to run (e.g., "dbt build", "dbt run --select model_name")
98
+
99
+ Returns:
100
+ Exit code of the command
101
+ """
102
+ print(f"Running: {command}")
103
+ print("=" * 60)
104
+
105
+ # Run the command and capture output
106
+ try:
107
+ process = subprocess.Popen(
108
+ command,
109
+ shell=True,
110
+ stdout=subprocess.PIPE,
111
+ stderr=subprocess.STDOUT,
112
+ text=True,
113
+ bufsize=1,
114
+ universal_newlines=True
115
+ )
116
+
117
+ output_lines = []
118
+ for line in process.stdout:
119
+ # Print in real-time
120
+ print(line, end='')
121
+ output_lines.append(line)
122
+
123
+ process.wait()
124
+ exit_code = process.returncode
125
+
126
+ # Combine all output
127
+ full_output = ''.join(output_lines)
128
+
129
+ # Log to Snowflake
130
+ self._log_to_snowflake(command, full_output)
131
+
132
+ print("=" * 60)
133
+ print(f"Command completed with exit code: {exit_code}")
134
+
135
+ return exit_code
136
+
137
+ except Exception as e:
138
+ error_msg = f"Error running command: {e}"
139
+ print(error_msg)
140
+ self._log_to_snowflake(command, error_msg)
141
+ return 1
142
+
143
+ def _log_to_snowflake(self, command: str, stdout_log: str):
144
+ """Insert the command and output into Snowflake."""
145
+ insert_sql = f"""
146
+ INSERT INTO {self.full_table_name} (repo_name, command, stdout_log, log_time)
147
+ VALUES (%s, %s, %s, %s)
148
+ """
149
+
150
+ try:
151
+ conn = self._get_connection()
152
+ cursor = conn.cursor()
153
+ cursor.execute(
154
+ insert_sql,
155
+ (self.repo_name, command, stdout_log, datetime.now())
156
+ )
157
+ conn.commit()
158
+ cursor.close()
159
+ conn.close()
160
+ except Exception as e:
161
+ print(f"Warning: Could not log to Snowflake: {e}")
@@ -0,0 +1,234 @@
1
+ """Utility functions for dbt-logger."""
2
+
3
+ from typing import Dict, Any, Optional
4
+ import os
5
+ import yaml
6
+ import re
7
+ from pathlib import Path
8
+
9
+
10
+ def get_logger_config() -> Dict[str, Any]:
11
+ """
12
+ Load configuration from dbt/dbt_logger.yml if it exists.
13
+
14
+ Returns:
15
+ Dictionary with optional keys: database, schema, table_name
16
+ """
17
+ config_paths = [
18
+ Path.cwd() / 'dbt' / 'dbt_logger.yml',
19
+ Path.cwd() / 'dbt_logger.yml',
20
+ ]
21
+
22
+ # Also check next to profiles.yml if found
23
+ try:
24
+ from .utils import get_dbt_profile_path
25
+ profiles_path = get_dbt_profile_path()
26
+ profiles_dir = profiles_path.parent.parent if profiles_path.name == 'profiles.yml' and profiles_path.parent.name == 'profiles' else profiles_path.parent
27
+ config_paths.append(profiles_dir / 'dbt_logger.yml')
28
+ except Exception:
29
+ pass
30
+
31
+ for config_path in config_paths:
32
+ if config_path.exists():
33
+ with open(config_path, 'r') as f:
34
+ config = yaml.safe_load(f) or {}
35
+ return config
36
+
37
+ return {}
38
+
39
+
40
+ def _resolve_env_var(value: str) -> str:
41
+ """
42
+ Resolve Jinja-style environment variable references.
43
+
44
+ Handles patterns like {{ env_var('VAR_NAME') }}
45
+ """
46
+ if not isinstance(value, str):
47
+ return value
48
+
49
+ # Match {{ env_var('VAR_NAME') }} or {{ env_var("VAR_NAME") }}
50
+ pattern = r"{{\s*env_var\(['\"]([^'\"]+)['\"]\)\s*}}"
51
+
52
+ def replacer(match):
53
+ env_var_name = match.group(1)
54
+ env_value = os.getenv(env_var_name)
55
+ if env_value is None:
56
+ raise ValueError(f"Environment variable '{env_var_name}' is not set")
57
+ return env_value
58
+
59
+ return re.sub(pattern, replacer, value)
60
+
61
+
62
+ def get_dbt_profile_path() -> Path:
63
+ """
64
+ Get the path to the dbt profiles.yml file.
65
+
66
+ Checks in order:
67
+ 1. DBT_PROFILES_DIR environment variable
68
+ 2. Current directory ./dbt/profiles/profiles.yml
69
+ 3. ~/.dbt/profiles.yml (default location)
70
+ """
71
+ # Check environment variable
72
+ profiles_dir = os.getenv('DBT_PROFILES_DIR')
73
+ if profiles_dir:
74
+ profiles_path = Path(profiles_dir) / 'profiles.yml'
75
+ if profiles_path.exists():
76
+ return profiles_path
77
+
78
+ # Check local dbt/profiles directory
79
+ local_path = Path.cwd() / 'dbt' / 'profiles' / 'profiles.yml'
80
+ if local_path.exists():
81
+ return local_path
82
+
83
+ # Check default home directory
84
+ home_path = Path.home() / '.dbt' / 'profiles.yml'
85
+ if home_path.exists():
86
+ return home_path
87
+
88
+ raise FileNotFoundError(
89
+ "Could not find profiles.yml. Checked:\n"
90
+ f" - $DBT_PROFILES_DIR/profiles.yml\n"
91
+ f" - {local_path}\n"
92
+ f" - {home_path}"
93
+ )
94
+
95
+
96
+ def get_connection_params_from_dbt(
97
+ profile_name: Optional[str] = None,
98
+ target: Optional[str] = None
99
+ ) -> Dict[str, Any]:
100
+ """
101
+ Get Snowflake connection parameters from dbt profiles.yml.
102
+
103
+ Args:
104
+ profile_name: Name of the profile in profiles.yml (if None, uses DBT_PROFILE env var or first profile)
105
+ target: Target name (dev/prod) (if None, uses profile's default target)
106
+
107
+ Returns:
108
+ Dictionary of connection parameters suitable for snowflake.connector.connect()
109
+ """
110
+ profiles_path = get_dbt_profile_path()
111
+
112
+ with open(profiles_path, 'r') as f:
113
+ profiles = yaml.safe_load(f)
114
+
115
+ # Determine which profile to use
116
+ if profile_name is None:
117
+ profile_name = os.getenv('DBT_PROFILE')
118
+
119
+ if profile_name is None:
120
+ # Use the first profile (excluding 'config' key if present)
121
+ available_profiles = [k for k in profiles.keys() if k != 'config']
122
+ if not available_profiles:
123
+ raise ValueError("No profiles found in profiles.yml")
124
+ profile_name = available_profiles[0]
125
+
126
+ if profile_name not in profiles:
127
+ raise ValueError(f"Profile '{profile_name}' not found in profiles.yml")
128
+
129
+ profile = profiles[profile_name]
130
+
131
+ # Determine which target to use
132
+ if target is None:
133
+ target = profile.get('target')
134
+
135
+ if target is None:
136
+ raise ValueError(f"No target specified and profile '{profile_name}' has no default target")
137
+
138
+ if target not in profile.get('outputs', {}):
139
+ raise ValueError(f"Target '{target}' not found in profile '{profile_name}'")
140
+
141
+ output = profile['outputs'][target]
142
+
143
+ # Resolve environment variables in the output
144
+ resolved_output = {}
145
+ for key, value in output.items():
146
+ if isinstance(value, str):
147
+ resolved_output[key] = _resolve_env_var(value)
148
+ else:
149
+ resolved_output[key] = value
150
+
151
+ # Build connection parameters for Snowflake
152
+ params = {
153
+ 'account': resolved_output.get('account'),
154
+ 'user': resolved_output.get('user'),
155
+ 'warehouse': resolved_output.get('warehouse'),
156
+ 'database': resolved_output.get('database'),
157
+ }
158
+
159
+ # Handle authentication - private key or password
160
+ if 'private_key_path' in resolved_output:
161
+ # Private key authentication
162
+ private_key_path = resolved_output['private_key_path']
163
+ private_key_passphrase = resolved_output.get('private_key_passphrase')
164
+
165
+ # Read and parse the private key
166
+ from cryptography.hazmat.backends import default_backend
167
+ from cryptography.hazmat.primitives import serialization
168
+
169
+ with open(private_key_path, 'rb') as key_file:
170
+ private_key_data = key_file.read()
171
+
172
+ passphrase = private_key_passphrase.encode() if private_key_passphrase else None
173
+
174
+ private_key = serialization.load_pem_private_key(
175
+ private_key_data,
176
+ password=passphrase,
177
+ backend=default_backend()
178
+ )
179
+
180
+ pkb = private_key.private_bytes(
181
+ encoding=serialization.Encoding.DER,
182
+ format=serialization.PrivateFormat.PKCS8,
183
+ encryption_algorithm=serialization.NoEncryption()
184
+ )
185
+
186
+ params['private_key'] = pkb
187
+ elif 'password' in resolved_output:
188
+ params['password'] = resolved_output['password']
189
+ else:
190
+ raise ValueError("No authentication method found (private_key_path or password)")
191
+
192
+ # Optional parameters
193
+ if 'role' in resolved_output:
194
+ params['role'] = resolved_output['role']
195
+
196
+ if 'schema' in resolved_output:
197
+ params['schema'] = resolved_output['schema']
198
+
199
+ return params
200
+
201
+
202
+ def get_connection_params_from_env() -> Dict[str, Any]:
203
+ """
204
+ Get Snowflake connection parameters from environment variables.
205
+
206
+ Expected environment variables:
207
+ - SNOWFLAKE_ACCOUNT
208
+ - SNOWFLAKE_USER
209
+ - SNOWFLAKE_PASSWORD
210
+ - SNOWFLAKE_DATABASE
211
+ - SNOWFLAKE_WAREHOUSE
212
+ - SNOWFLAKE_ROLE (optional)
213
+
214
+ Returns:
215
+ Dictionary of connection parameters
216
+ """
217
+ params = {
218
+ 'account': os.getenv('SNOWFLAKE_ACCOUNT'),
219
+ 'user': os.getenv('SNOWFLAKE_USER'),
220
+ 'password': os.getenv('SNOWFLAKE_PASSWORD'),
221
+ 'database': os.getenv('SNOWFLAKE_DATABASE'),
222
+ 'warehouse': os.getenv('SNOWFLAKE_WAREHOUSE'),
223
+ }
224
+
225
+ role = os.getenv('SNOWFLAKE_ROLE')
226
+ if role:
227
+ params['role'] = role
228
+
229
+ # Validate required parameters
230
+ missing = [k for k, v in params.items() if v is None and k != 'role']
231
+ if missing:
232
+ raise ValueError(f"Missing required environment variables: {', '.join(f'SNOWFLAKE_{k.upper()}' for k in missing)}")
233
+
234
+ return params
@@ -0,0 +1,41 @@
1
+ [tool.poetry]
2
+ name = "dbt-logger"
3
+ version = "0.1.1"
4
+ description = "A lightweight logging utility for dbt projects"
5
+ authors = ["Tristan Schwartz"]
6
+ readme = "README.md"
7
+ license = "MIT"
8
+ homepage = "https://github.com/TristanSchwartz/dbt-logger"
9
+ repository = "https://github.com/TristanSchwartz/dbt-logger"
10
+ keywords = ["dbt", "logging", "data", "analytics"]
11
+ classifiers = [
12
+ "Development Status :: 3 - Alpha",
13
+ "Intended Audience :: Developers",
14
+ "License :: OSI Approved :: MIT License",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.8",
17
+ "Programming Language :: Python :: 3.9",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ ]
21
+
22
+ [tool.poetry.scripts]
23
+ dbt-logger = "dbt_logger.cli:main"
24
+
25
+ [tool.poetry.dependencies]
26
+ python = "^3.9.2,<3.13"
27
+ snowflake-connector-python = "^3.0.0"
28
+ pyyaml = "^6.0.0"
29
+ cryptography = "^41.0.0"
30
+
31
+ [tool.poetry.group.dev.dependencies]
32
+ pytest = "^7.0.0"
33
+ pre-commit = "^3.3.3"
34
+
35
+ [build-system]
36
+ requires = ["poetry-core"]
37
+ build-backend = "poetry.core.masonry.api"
38
+
39
+ [[tool.poetry.source]]
40
+ name = "PyPI"
41
+ priority = "primary"