Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add snowflake to application parameter to configuration #1266

Merged
merged 13 commits into from
Apr 24, 2024
Merged
14 changes: 14 additions & 0 deletions dlt/destinations/impl/snowflake/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ def _read_private_key(private_key: str, password: Optional[str] = None) -> bytes
)


SNOWFLAKE_APPLICATION_ID = "dltHub_dlt"


@configspec(init=False)
class SnowflakeCredentials(ConnectionStringCredentials):
drivername: Final[str] = dataclasses.field(default="snowflake", init=False, repr=False, compare=False) # type: ignore[misc]
Expand All @@ -60,6 +63,7 @@ class SnowflakeCredentials(ConnectionStringCredentials):
authenticator: Optional[str] = None
private_key: Optional[TSecretStrValue] = None
private_key_passphrase: Optional[TSecretStrValue] = None
application: Optional[str] = SNOWFLAKE_APPLICATION_ID

__config_gen_annotations__: ClassVar[List[str]] = ["password", "warehouse", "role"]

Expand All @@ -85,6 +89,10 @@ def to_url(self) -> URL:
query["warehouse"] = self.warehouse
if self.role and "role" not in query:
query["role"] = self.role

if self.application != "" and "application" not in query:
query["application"] = self.application

return URL.create(
self.drivername,
self.username,
Expand All @@ -99,6 +107,7 @@ def to_connector_params(self) -> Dict[str, Any]:
private_key: Optional[bytes] = None
if self.private_key:
private_key = _read_private_key(self.private_key, self.private_key_passphrase)

conn_params = dict(
self.query or {},
user=self.username,
Expand All @@ -109,8 +118,13 @@ def to_connector_params(self) -> Dict[str, Any]:
role=self.role,
private_key=private_key,
)

if self.authenticator:
conn_params["authenticator"] = self.authenticator

if self.application != "" and "application" not in conn_params:
conn_params["application"] = self.application

return conn_params


Expand Down
5 changes: 5 additions & 0 deletions docs/website/docs/dlt-ecosystem/destinations/snowflake.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,16 @@ username = "loader"
host = "kgiotue-wn98412"
warehouse = "COMPUTE_WH"
role = "DLT_LOADER_ROLE"
application = "dltHub_dlt"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should put it here, because for most users it's not relevant and it just will be a default value.

Copy link
Contributor Author

@sultaniman sultaniman Apr 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it I will remove this :)

```
In the case of Snowflake, the **host** is your [Account Identifier](https://docs.snowflake.com/en/user-guide/admin-account-identifier). You can get it in **Admin**/**Accounts** by copying the account URL: https://kgiotue-wn98412.snowflakecomputing.com and extracting the host name (**kgiotue-wn98412**).

The **warehouse** and **role** are optional if you assign defaults to your user. In the example below, we do not do that, so we set them explicitly.

:::note
We use `application = "dltHub_dlt"` to let Snowflake to know about dlt users if you would like to disable this behavior
please set it to empty string `application = ""` this will disable it.
:::
sultaniman marked this conversation as resolved.
Show resolved Hide resolved

### Setup the database user and permissions
The instructions below assume that you use the default account setup that you get after creating a Snowflake account. You should have a default warehouse named **COMPUTE_WH** and a Snowflake account. Below, we create a new database, user, and assign permissions. The permissions are very generous. A more experienced user can easily reduce `dlt` permissions to just one schema in the database.
Expand Down
31 changes: 30 additions & 1 deletion tests/load/snowflake/test_snowflake_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from dlt.common.utils import digest128

from dlt.destinations.impl.snowflake.configuration import (
SNOWFLAKE_APPLICATION_ID,
SnowflakeClientConfiguration,
SnowflakeCredentials,
)
Expand All @@ -21,7 +22,7 @@


def test_connection_string_with_all_params() -> None:
url = "snowflake://user1:pass1@host1/db1?warehouse=warehouse1&role=role1&private_key=cGs%3D&private_key_passphrase=paphr"
url = "snowflake://user1:pass1@host1/db1?application=dltHub_dlt&warehouse=warehouse1&role=role1&private_key=cGs%3D&private_key_passphrase=paphr"

creds = SnowflakeCredentials()
creds.parse_native_representation(url)
Expand All @@ -36,9 +37,20 @@ def test_connection_string_with_all_params() -> None:
assert creds.private_key_passphrase == "paphr"

expected = make_url(url)
to_url_value = str(creds.to_url())

# Test URL components regardless of query param order
assert make_url(creds.to_native_representation()) == expected
assert to_url_value == str(expected)

creds.application = "custom"
url = "snowflake://user1:pass1@host1/db1?application=custom&warehouse=warehouse1&role=role1&private_key=cGs%3D&private_key_passphrase=paphr"
creds.parse_native_representation(url)
expected = make_url(url)
to_url_value = str(creds.to_url())
assert make_url(creds.to_native_representation()) == expected
assert to_url_value == str(expected)
assert "application=custom" in str(expected)


def test_to_connector_params() -> None:
Expand Down Expand Up @@ -66,6 +78,8 @@ def test_to_connector_params() -> None:
password=None,
warehouse="warehouse1",
role="role1",
# default application identifier will be used
application=SNOWFLAKE_APPLICATION_ID,
)

# base64 encoded DER key
Expand All @@ -79,6 +93,8 @@ def test_to_connector_params() -> None:
creds.host = "host1"
creds.warehouse = "warehouse1"
creds.role = "role1"
# set application identifier and check it
creds.application = "custom_app_id"

params = creds.to_connector_params()

Expand All @@ -92,6 +108,7 @@ def test_to_connector_params() -> None:
password=None,
warehouse="warehouse1",
role="role1",
application="custom_app_id",
)


Expand All @@ -103,12 +120,14 @@ def test_snowflake_credentials_native_value(environment) -> None:
)
# set password via env
os.environ["CREDENTIALS__PASSWORD"] = "pass"
os.environ["CREDENTIALS__APPLICATION"] = "dlt"
c = resolve_configuration(
SnowflakeCredentials(),
explicit_value="snowflake://user1@host1/db1?warehouse=warehouse1&role=role1",
)
assert c.is_resolved()
assert c.password == "pass"
assert "application=dlt" in str(c.to_url())
rudolfix marked this conversation as resolved.
Show resolved Hide resolved
# # but if password is specified - it is final
c = resolve_configuration(
SnowflakeCredentials(),
Expand All @@ -126,6 +145,16 @@ def test_snowflake_credentials_native_value(environment) -> None:
)
assert c.is_resolved()
assert c.private_key == "pk"
assert "application=dlt" in str(c.to_url())

# check with application = "" it should not be in connection string
os.environ["CREDENTIALS__APPLICATION"] = ""
c = resolve_configuration(
SnowflakeCredentials(),
explicit_value="snowflake://user1@host1/db1?warehouse=warehouse1&role=role1",
)
assert c.is_resolved()
assert "application=" not in str(c.to_url())


def test_snowflake_configuration() -> None:
Expand Down
Loading