Skip to content

Commit fa847ec

Browse files
add ability to provide custom path
1 parent da4db5c commit fa847ec

File tree

6 files changed

+91
-5
lines changed

6 files changed

+91
-5
lines changed

docs/guides/configuration.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,19 @@ See the [overrides](#overrides) section for a detailed explanation of how these
121121

122122
The rest of the `.env` file variables can be used in your configuration files with `{{ env_var('VARIABLE_NAME') }}` syntax in YAML or accessed via `os.environ['VARIABLE_NAME']` in Python.
123123

124+
#### Custom dot env file location and name
125+
126+
By default, SQLMesh loads `.env` files from each project directory. Alternatively, you can export the `SQLMESH_DOTENV_PATH` environment variable to specify a custom path and persist it across commands:
127+
128+
```bash
129+
export SQLMESH_DOTENV_PATH=/path/to/custom/.custom_env
130+
sqlmesh plan
131+
```
132+
124133
**Important considerations:**
125134
- Add `.env` to your `.gitignore` file to avoid committing sensitive information
126-
- SQLMesh will only load the `.env` file if it exists in the project directory
135+
- SQLMesh will only load the `.env` file if it exists in the project directory (unless a custom path is specified)
136+
- When using a custom path, that specific file takes precedence over any `.env` file in the project directory.
127137

128138
### Configuration file
129139

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ dev = [
7979
"PyAthena[Pandas]",
8080
"PyGithub>=2.6.0",
8181
"pyperf",
82+
"python-dotenv",
8283
"pyspark~=3.5.0",
8384
"pytest",
8485
"pytest-asyncio",

sqlmesh/cli/main.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ def _sqlmesh_version() -> str:
8484
type=str,
8585
help="The directory to write log files to.",
8686
)
87+
@click.option(
88+
"--dotenv",
89+
type=click.Path(exists=True, path_type=Path),
90+
help="Path to a custom .env file to load environment variables.",
91+
envvar="SQLMESH_DOTENV_PATH",
92+
)
8793
@click.pass_context
8894
@error_handler
8995
def cli(
@@ -95,6 +101,7 @@ def cli(
95101
debug: bool = False,
96102
log_to_stdout: bool = False,
97103
log_file_dir: t.Optional[str] = None,
104+
dotenv: t.Optional[Path] = None,
98105
) -> None:
99106
"""SQLMesh command line tool."""
100107
if "--help" in sys.argv:
@@ -118,7 +125,7 @@ def cli(
118125
)
119126
configure_console(ignore_warnings=ignore_warnings)
120127

121-
configs = load_configs(config, Context.CONFIG_TYPE, paths)
128+
configs = load_configs(config, Context.CONFIG_TYPE, paths, dotenv_path=dotenv)
122129
log_limit = list(configs.values())[0].log_limit
123130

124131
remove_excess_logs(log_file_dir, log_limit)

sqlmesh/core/config/loader.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ def load_configs(
2626
config_type: t.Type[C],
2727
paths: t.Union[str | Path, t.Iterable[str | Path]],
2828
sqlmesh_path: t.Optional[Path] = None,
29+
dotenv_path: t.Optional[Path] = None,
2930
) -> t.Dict[Path, C]:
3031
sqlmesh_path = sqlmesh_path or c.SQLMESH_PATH
3132
config = config or "config"
@@ -36,8 +37,11 @@ def load_configs(
3637
for p in (glob.glob(str(path)) or [str(path)])
3738
]
3839

39-
for path in absolute_paths:
40-
load_dotenv(dotenv_path=path / ".env", override=True)
40+
if dotenv_path:
41+
load_dotenv(dotenv_path=dotenv_path, override=True)
42+
else:
43+
for path in absolute_paths:
44+
load_dotenv(dotenv_path=path / ".env", override=True)
4145

4246
if not isinstance(config, str):
4347
if type(config) != config_type:

sqlmesh/magics.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from argparse import Namespace, SUPPRESS
99
from collections import defaultdict
1010
from copy import deepcopy
11+
from pathlib import Path
1112

1213
from hyperscript import h
1314

@@ -166,6 +167,9 @@ def _shell(self) -> t.Any:
166167
@argument("--ignore-warnings", action="store_true", help="Ignore warnings.")
167168
@argument("--debug", action="store_true", help="Enable debug mode.")
168169
@argument("--log-file-dir", type=str, help="The directory to write the log file to.")
170+
@argument(
171+
"--dotenv", type=str, help="Path to a custom .env file to load environment variables from."
172+
)
169173
@line_magic
170174
def context(self, line: str) -> None:
171175
"""Sets the context in the user namespace."""
@@ -181,7 +185,10 @@ def context(self, line: str) -> None:
181185
)
182186
configure_console(ignore_warnings=args.ignore_warnings)
183187

184-
configs = load_configs(args.config, Context.CONFIG_TYPE, args.paths)
188+
dotenv_path = Path(args.dotenv) if args.dotenv else None
189+
configs = load_configs(
190+
args.config, Context.CONFIG_TYPE, args.paths, dotenv_path=dotenv_path
191+
)
185192
log_limit = list(configs.values())[0].log_limit
186193

187194
remove_excess_logs(log_file_dir, log_limit)

tests/core/test_config.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1236,3 +1236,60 @@ def test_load_yaml_config_dot_env_vars(tmp_path_factory):
12361236
default_gateway="duckdb_gateway",
12371237
model_defaults=ModelDefaultsConfig(dialect="athena"),
12381238
)
1239+
1240+
1241+
def test_load_yaml_config_custom_dotenv_path(tmp_path_factory):
1242+
main_dir = tmp_path_factory.mktemp("yaml_config_2")
1243+
config_path = main_dir / "config.yaml"
1244+
with open(config_path, "w", encoding="utf-8") as fd:
1245+
fd.write(
1246+
"""gateways:
1247+
test_gateway:
1248+
connection:
1249+
type: duckdb
1250+
database: {{ env_var('DB_NAME') }}
1251+
"""
1252+
)
1253+
1254+
# Create a custom dot env file in a different location
1255+
custom_env_dir = tmp_path_factory.mktemp("custom_env")
1256+
custom_env_path = custom_env_dir / ".my_env"
1257+
with open(custom_env_path, "w", encoding="utf-8") as fd:
1258+
fd.write(
1259+
"""DB_NAME="custom_database.db"
1260+
SQLMESH__DEFAULT_GATEWAY="test_gateway"
1261+
SQLMESH__MODEL_DEFAULTS__DIALECT="postgres"
1262+
"""
1263+
)
1264+
1265+
# Test that without custom dotenv path, env vars are not loaded
1266+
with mock.patch.dict(os.environ, {}, clear=True):
1267+
with pytest.raises(
1268+
ConfigError, match=r"Default model SQL dialect is a required configuratio*"
1269+
):
1270+
load_configs(
1271+
"config",
1272+
Config,
1273+
paths=[main_dir],
1274+
)
1275+
1276+
# Test that with custom dotenv path, env vars are loaded correctly
1277+
with mock.patch.dict(os.environ, {}, clear=True):
1278+
configs = load_configs(
1279+
"config",
1280+
Config,
1281+
paths=[main_dir],
1282+
dotenv_path=custom_env_path,
1283+
)
1284+
1285+
assert next(iter(configs.values())) == Config(
1286+
gateways={
1287+
"test_gateway": GatewayConfig(
1288+
connection=DuckDBConnectionConfig(
1289+
database="custom_database.db",
1290+
),
1291+
),
1292+
},
1293+
default_gateway="test_gateway",
1294+
model_defaults=ModelDefaultsConfig(dialect="postgres"),
1295+
)

0 commit comments

Comments
 (0)