dlt-utils 0.1.2__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.
- {dlt_utils-0.1.2 → dlt_utils-0.3.0}/PKG-INFO +3 -1
- {dlt_utils-0.1.2 → dlt_utils-0.3.0}/dlt_utils/__init__.py +6 -1
- dlt_utils-0.3.0/dlt_utils/azure_keyvault.py +93 -0
- {dlt_utils-0.1.2 → dlt_utils-0.3.0}/dlt_utils/pipeline.py +45 -0
- {dlt_utils-0.1.2 → dlt_utils-0.3.0}/dlt_utils.egg-info/PKG-INFO +3 -1
- {dlt_utils-0.1.2 → dlt_utils-0.3.0}/dlt_utils.egg-info/SOURCES.txt +2 -0
- dlt_utils-0.3.0/dlt_utils.egg-info/requires.txt +8 -0
- {dlt_utils-0.1.2 → dlt_utils-0.3.0}/pyproject.toml +3 -1
- dlt_utils-0.3.0/tests/test_azure_keyvault.py +126 -0
- dlt_utils-0.1.2/dlt_utils.egg-info/requires.txt +0 -6
- {dlt_utils-0.1.2 → dlt_utils-0.3.0}/README.md +0 -0
- {dlt_utils-0.1.2 → dlt_utils-0.3.0}/dlt_utils/args.py +0 -0
- {dlt_utils-0.1.2 → dlt_utils-0.3.0}/dlt_utils/dates.py +0 -0
- {dlt_utils-0.1.2 → dlt_utils-0.3.0}/dlt_utils/incremental.py +0 -0
- {dlt_utils-0.1.2 → dlt_utils-0.3.0}/dlt_utils/logging.py +0 -0
- {dlt_utils-0.1.2 → dlt_utils-0.3.0}/dlt_utils/schema.py +0 -0
- {dlt_utils-0.1.2 → dlt_utils-0.3.0}/dlt_utils.egg-info/dependency_links.txt +0 -0
- {dlt_utils-0.1.2 → dlt_utils-0.3.0}/dlt_utils.egg-info/top_level.txt +0 -0
- {dlt_utils-0.1.2 → dlt_utils-0.3.0}/setup.cfg +0 -0
- {dlt_utils-0.1.2 → dlt_utils-0.3.0}/tests/test_dates.py +0 -0
- {dlt_utils-0.1.2 → dlt_utils-0.3.0}/tests/test_incremental.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dlt_utils
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Shared utilities for dlt data pipelines with multi-company support
|
|
5
5
|
Author: Jay van den Bos
|
|
6
6
|
License: MIT
|
|
@@ -18,6 +18,8 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
18
18
|
Requires-Python: >=3.13
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
Requires-Dist: dlt>=1.19.1
|
|
21
|
+
Requires-Dist: azure-identity>=1.15.0
|
|
22
|
+
Requires-Dist: azure-keyvault-secrets>=4.8.0
|
|
21
23
|
Provides-Extra: dev
|
|
22
24
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
23
25
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
@@ -27,8 +27,10 @@ from .args import (
|
|
|
27
27
|
create_base_parser,
|
|
28
28
|
CommonArgs,
|
|
29
29
|
)
|
|
30
|
+
from .azure_keyvault import AzureKeyVault
|
|
30
31
|
from .logging import configure_logging
|
|
31
32
|
from .pipeline import (
|
|
33
|
+
cleanup_tombstones,
|
|
32
34
|
filter_resources,
|
|
33
35
|
get_resource_names,
|
|
34
36
|
mask_sensitive_value,
|
|
@@ -37,7 +39,7 @@ from .pipeline import (
|
|
|
37
39
|
)
|
|
38
40
|
|
|
39
41
|
|
|
40
|
-
__version__ = "0.
|
|
42
|
+
__version__ = "0.3.0"
|
|
41
43
|
|
|
42
44
|
__all__ = [
|
|
43
45
|
# Incremental
|
|
@@ -53,9 +55,12 @@ __all__ = [
|
|
|
53
55
|
"add_common_args",
|
|
54
56
|
"CommonArgs",
|
|
55
57
|
"create_base_parser",
|
|
58
|
+
# azure_keyvault
|
|
59
|
+
"AzureKeyVault",
|
|
56
60
|
# logging
|
|
57
61
|
"configure_logging",
|
|
58
62
|
# pipeline
|
|
63
|
+
"cleanup_tombstones",
|
|
59
64
|
"filter_resources",
|
|
60
65
|
"get_resource_names",
|
|
61
66
|
"mask_sensitive_value",
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Azure Key Vault utilities voor secret management.
|
|
3
|
+
|
|
4
|
+
Dit module biedt een AzureKeyVault class voor het ophalen, opslaan en beheren van
|
|
5
|
+
secrets in Azure Key Vault. Authenticatie gebeurt via Service Principal credentials
|
|
6
|
+
(client_id, client_secret, tenant_id).
|
|
7
|
+
|
|
8
|
+
Gebruik:
|
|
9
|
+
- Secrets ophalen voor database credentials, API keys, etc.
|
|
10
|
+
- Secrets opslaan/updaten vanuit pipelines
|
|
11
|
+
- Tags updaten voor secrets
|
|
12
|
+
|
|
13
|
+
Vereiste environment variables:
|
|
14
|
+
- KEYVAULT_BASE_URL: URL van de Key Vault (bijv. https://myvault.vault.azure.net/)
|
|
15
|
+
- KEYVAULT_SECRET_NAME: Naam van de secret
|
|
16
|
+
- AZURE_TENANT_ID: Azure AD tenant ID
|
|
17
|
+
- AZURE_CLIENT_ID: Service Principal client ID
|
|
18
|
+
- AZURE_CLIENT_SECRET: Service Principal client secret
|
|
19
|
+
"""
|
|
20
|
+
from azure.identity import ClientSecretCredential
|
|
21
|
+
from azure.keyvault.secrets import SecretClient
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AzureKeyVault:
|
|
27
|
+
def __init__(self):
|
|
28
|
+
"""
|
|
29
|
+
Base class for Azure
|
|
30
|
+
"""
|
|
31
|
+
super(AzureKeyVault, self).__init__()
|
|
32
|
+
|
|
33
|
+
# Azure Key Vault configuration
|
|
34
|
+
self.KEYVAULT_BASE_URL = os.getenv('KEYVAULT_BASE_URL')
|
|
35
|
+
self.KEYVAULT_SECRET_NAME = os.getenv('KEYVAULT_SECRET_NAME')
|
|
36
|
+
|
|
37
|
+
# Initialize Azure credential and Key Vault client
|
|
38
|
+
self.credential = ClientSecretCredential(
|
|
39
|
+
tenant_id=os.getenv('AZURE_TENANT_ID'),
|
|
40
|
+
client_id=os.getenv('AZURE_CLIENT_ID'),
|
|
41
|
+
client_secret=os.getenv('AZURE_CLIENT_SECRET')
|
|
42
|
+
)
|
|
43
|
+
self.secret_client = SecretClient(
|
|
44
|
+
vault_url=self.KEYVAULT_BASE_URL,
|
|
45
|
+
credential=self.credential
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def get_secret(self) -> object:
|
|
49
|
+
"""
|
|
50
|
+
Get secret from Azure Key Vault.
|
|
51
|
+
|
|
52
|
+
Returns
|
|
53
|
+
-------
|
|
54
|
+
object
|
|
55
|
+
Object with value, properties, etc. of the secret
|
|
56
|
+
"""
|
|
57
|
+
secret = self.secret_client.get_secret(self.KEYVAULT_SECRET_NAME)
|
|
58
|
+
|
|
59
|
+
return secret
|
|
60
|
+
|
|
61
|
+
def update_secret_tags(self, tags: dict):
|
|
62
|
+
"""
|
|
63
|
+
Update tags on the Key Vault secret.
|
|
64
|
+
|
|
65
|
+
Parameters
|
|
66
|
+
----------
|
|
67
|
+
tags : dict
|
|
68
|
+
Dictionary of tags to set on the secret
|
|
69
|
+
"""
|
|
70
|
+
self.secret_client.update_secret_properties(
|
|
71
|
+
self.KEYVAULT_SECRET_NAME,
|
|
72
|
+
tags=tags
|
|
73
|
+
)
|
|
74
|
+
print("Updated Key Vault tags successfully")
|
|
75
|
+
|
|
76
|
+
def set_secret(self, secret_value: dict):
|
|
77
|
+
"""
|
|
78
|
+
Set secret to Azure Key Vault.
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
secret_value : dict
|
|
83
|
+
Secret value to set to Azure Key Vault
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
self.secret_client.set_secret(
|
|
87
|
+
self.KEYVAULT_SECRET_NAME,
|
|
88
|
+
json.dumps(secret_value),
|
|
89
|
+
tags={}
|
|
90
|
+
)
|
|
91
|
+
print("Secret set to Key Vault successfully")
|
|
92
|
+
|
|
93
|
+
|
|
@@ -124,3 +124,48 @@ def mask_sensitive_value(value: str | None, show_chars: int = 2) -> str:
|
|
|
124
124
|
|
|
125
125
|
mid = len(value) // 2
|
|
126
126
|
return f"{value[:show_chars]}..{value[mid:mid+show_chars]}..***"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def cleanup_tombstones(
|
|
130
|
+
pipeline: Any,
|
|
131
|
+
resources: list[str],
|
|
132
|
+
column_name: str = "_tt_is_tombstone",
|
|
133
|
+
) -> None:
|
|
134
|
+
"""
|
|
135
|
+
Clean up tombstone records from tables after a successful pipeline run.
|
|
136
|
+
|
|
137
|
+
Tombstones are placeholder records used to ensure delete-insert semantics
|
|
138
|
+
work correctly for partitions that have no real data. After the pipeline
|
|
139
|
+
completes, these tombstone records should be removed.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
pipeline: A dlt pipeline object with sql_client() method.
|
|
143
|
+
resources: List of resource/table names to clean tombstones from.
|
|
144
|
+
column_name: The boolean column name that marks tombstone records.
|
|
145
|
+
Defaults to "_tt_is_tombstone".
|
|
146
|
+
|
|
147
|
+
Example:
|
|
148
|
+
# Clean up tombstones from all resources except 'customer'
|
|
149
|
+
tombstone_resources = [r for r in sync_resources if r != "customer"]
|
|
150
|
+
cleanup_tombstones(pipeline, tombstone_resources)
|
|
151
|
+
|
|
152
|
+
# Clean up with a custom column name
|
|
153
|
+
cleanup_tombstones(pipeline, ["my_table"], column_name="is_tombstone")
|
|
154
|
+
"""
|
|
155
|
+
if not resources:
|
|
156
|
+
logger.debug("No resources provided for tombstone cleanup")
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
with pipeline.sql_client() as sql_client:
|
|
160
|
+
for resource_name in resources:
|
|
161
|
+
table_name = sql_client.make_qualified_table_name(resource_name)
|
|
162
|
+
delete_query = f"DELETE FROM {table_name} WHERE {column_name} = TRUE"
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
sql_client.execute_sql(delete_query)
|
|
166
|
+
logger.info(f"Cleaned up tombstone records from {table_name}")
|
|
167
|
+
except Exception as e:
|
|
168
|
+
# Table might not exist on first run or if resource wasn't selected
|
|
169
|
+
logger.warning(
|
|
170
|
+
f"Could not clean tombstones from {table_name}: {e}"
|
|
171
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dlt_utils
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Shared utilities for dlt data pipelines with multi-company support
|
|
5
5
|
Author: Jay van den Bos
|
|
6
6
|
License: MIT
|
|
@@ -18,6 +18,8 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
18
18
|
Requires-Python: >=3.13
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
Requires-Dist: dlt>=1.19.1
|
|
21
|
+
Requires-Dist: azure-identity>=1.15.0
|
|
22
|
+
Requires-Dist: azure-keyvault-secrets>=4.8.0
|
|
21
23
|
Provides-Extra: dev
|
|
22
24
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
23
25
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
@@ -2,6 +2,7 @@ README.md
|
|
|
2
2
|
pyproject.toml
|
|
3
3
|
dlt_utils/__init__.py
|
|
4
4
|
dlt_utils/args.py
|
|
5
|
+
dlt_utils/azure_keyvault.py
|
|
5
6
|
dlt_utils/dates.py
|
|
6
7
|
dlt_utils/incremental.py
|
|
7
8
|
dlt_utils/logging.py
|
|
@@ -12,5 +13,6 @@ dlt_utils.egg-info/SOURCES.txt
|
|
|
12
13
|
dlt_utils.egg-info/dependency_links.txt
|
|
13
14
|
dlt_utils.egg-info/requires.txt
|
|
14
15
|
dlt_utils.egg-info/top_level.txt
|
|
16
|
+
tests/test_azure_keyvault.py
|
|
15
17
|
tests/test_dates.py
|
|
16
18
|
tests/test_incremental.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dlt_utils"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Shared utilities for dlt data pipelines with multi-company support"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -25,6 +25,8 @@ classifiers = [
|
|
|
25
25
|
]
|
|
26
26
|
dependencies = [
|
|
27
27
|
"dlt>=1.19.1",
|
|
28
|
+
"azure-identity>=1.15.0",
|
|
29
|
+
"azure-keyvault-secrets>=4.8.0",
|
|
28
30
|
]
|
|
29
31
|
|
|
30
32
|
[project.optional-dependencies]
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Tests for Azure Key Vault utilities."""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from unittest.mock import MagicMock, patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestAzureKeyVault:
|
|
10
|
+
"""Test cases for AzureKeyVault class."""
|
|
11
|
+
|
|
12
|
+
@patch.dict(os.environ, {
|
|
13
|
+
'KEYVAULT_BASE_URL': 'https://test-vault.vault.azure.net/',
|
|
14
|
+
'KEYVAULT_SECRET_NAME': 'test-secret',
|
|
15
|
+
'AZURE_TENANT_ID': 'test-tenant-id',
|
|
16
|
+
'AZURE_CLIENT_ID': 'test-client-id',
|
|
17
|
+
'AZURE_CLIENT_SECRET': 'test-client-secret'
|
|
18
|
+
})
|
|
19
|
+
@patch('dlt_utils.azure_keyvault.SecretClient')
|
|
20
|
+
@patch('dlt_utils.azure_keyvault.ClientSecretCredential')
|
|
21
|
+
def test_init(self, mock_credential, mock_secret_client):
|
|
22
|
+
"""Test AzureKeyVault initialization."""
|
|
23
|
+
from dlt_utils.azure_keyvault import AzureKeyVault
|
|
24
|
+
|
|
25
|
+
kv = AzureKeyVault()
|
|
26
|
+
|
|
27
|
+
# Verify credential was created with correct parameters
|
|
28
|
+
mock_credential.assert_called_once_with(
|
|
29
|
+
tenant_id='test-tenant-id',
|
|
30
|
+
client_id='test-client-id',
|
|
31
|
+
client_secret='test-client-secret'
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Verify SecretClient was created
|
|
35
|
+
mock_secret_client.assert_called_once()
|
|
36
|
+
assert kv.KEYVAULT_BASE_URL == 'https://test-vault.vault.azure.net/'
|
|
37
|
+
assert kv.KEYVAULT_SECRET_NAME == 'test-secret'
|
|
38
|
+
|
|
39
|
+
@patch.dict(os.environ, {
|
|
40
|
+
'KEYVAULT_BASE_URL': 'https://test-vault.vault.azure.net/',
|
|
41
|
+
'KEYVAULT_SECRET_NAME': 'test-secret',
|
|
42
|
+
'AZURE_TENANT_ID': 'test-tenant-id',
|
|
43
|
+
'AZURE_CLIENT_ID': 'test-client-id',
|
|
44
|
+
'AZURE_CLIENT_SECRET': 'test-client-secret'
|
|
45
|
+
})
|
|
46
|
+
@patch('dlt_utils.azure_keyvault.SecretClient')
|
|
47
|
+
@patch('dlt_utils.azure_keyvault.ClientSecretCredential')
|
|
48
|
+
def test_get_secret(self, mock_credential, mock_secret_client):
|
|
49
|
+
"""Test getting a secret from Key Vault."""
|
|
50
|
+
from dlt_utils.azure_keyvault import AzureKeyVault
|
|
51
|
+
|
|
52
|
+
# Setup mock
|
|
53
|
+
mock_secret = MagicMock()
|
|
54
|
+
mock_secret.value = '{"key": "value"}'
|
|
55
|
+
mock_secret_client.return_value.get_secret.return_value = mock_secret
|
|
56
|
+
|
|
57
|
+
kv = AzureKeyVault()
|
|
58
|
+
result = kv.get_secret()
|
|
59
|
+
|
|
60
|
+
# Verify get_secret was called with correct secret name
|
|
61
|
+
kv.secret_client.get_secret.assert_called_once_with('test-secret')
|
|
62
|
+
assert result == mock_secret
|
|
63
|
+
|
|
64
|
+
@patch.dict(os.environ, {
|
|
65
|
+
'KEYVAULT_BASE_URL': 'https://test-vault.vault.azure.net/',
|
|
66
|
+
'KEYVAULT_SECRET_NAME': 'test-secret',
|
|
67
|
+
'AZURE_TENANT_ID': 'test-tenant-id',
|
|
68
|
+
'AZURE_CLIENT_ID': 'test-client-id',
|
|
69
|
+
'AZURE_CLIENT_SECRET': 'test-client-secret'
|
|
70
|
+
})
|
|
71
|
+
@patch('dlt_utils.azure_keyvault.SecretClient')
|
|
72
|
+
@patch('dlt_utils.azure_keyvault.ClientSecretCredential')
|
|
73
|
+
def test_update_secret_tags(self, mock_credential, mock_secret_client):
|
|
74
|
+
"""Test updating secret tags."""
|
|
75
|
+
from dlt_utils.azure_keyvault import AzureKeyVault
|
|
76
|
+
|
|
77
|
+
kv = AzureKeyVault()
|
|
78
|
+
tags = {'environment': 'test', 'version': '1.0'}
|
|
79
|
+
kv.update_secret_tags(tags)
|
|
80
|
+
|
|
81
|
+
# Verify update_secret_properties was called
|
|
82
|
+
kv.secret_client.update_secret_properties.assert_called_once_with(
|
|
83
|
+
'test-secret',
|
|
84
|
+
tags=tags
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
@patch.dict(os.environ, {
|
|
88
|
+
'KEYVAULT_BASE_URL': 'https://test-vault.vault.azure.net/',
|
|
89
|
+
'KEYVAULT_SECRET_NAME': 'test-secret',
|
|
90
|
+
'AZURE_TENANT_ID': 'test-tenant-id',
|
|
91
|
+
'AZURE_CLIENT_ID': 'test-client-id',
|
|
92
|
+
'AZURE_CLIENT_SECRET': 'test-client-secret'
|
|
93
|
+
})
|
|
94
|
+
@patch('dlt_utils.azure_keyvault.SecretClient')
|
|
95
|
+
@patch('dlt_utils.azure_keyvault.ClientSecretCredential')
|
|
96
|
+
def test_set_secret(self, mock_credential, mock_secret_client):
|
|
97
|
+
"""Test setting a secret in Key Vault."""
|
|
98
|
+
from dlt_utils.azure_keyvault import AzureKeyVault
|
|
99
|
+
|
|
100
|
+
kv = AzureKeyVault()
|
|
101
|
+
secret_value = {'database': 'prod', 'password': 'secret123'}
|
|
102
|
+
kv.set_secret(secret_value)
|
|
103
|
+
|
|
104
|
+
# Verify set_secret was called with correct parameters
|
|
105
|
+
kv.secret_client.set_secret.assert_called_once_with(
|
|
106
|
+
'test-secret',
|
|
107
|
+
json.dumps(secret_value),
|
|
108
|
+
tags={}
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class TestAzureKeyVaultMissingEnvVars:
|
|
113
|
+
"""Test AzureKeyVault behavior with missing environment variables."""
|
|
114
|
+
|
|
115
|
+
@patch.dict(os.environ, {}, clear=True)
|
|
116
|
+
@patch('dlt_utils.azure_keyvault.SecretClient')
|
|
117
|
+
@patch('dlt_utils.azure_keyvault.ClientSecretCredential')
|
|
118
|
+
def test_init_missing_env_vars(self, mock_credential, mock_secret_client):
|
|
119
|
+
"""Test initialization with missing environment variables."""
|
|
120
|
+
from dlt_utils.azure_keyvault import AzureKeyVault
|
|
121
|
+
|
|
122
|
+
kv = AzureKeyVault()
|
|
123
|
+
|
|
124
|
+
# Should initialize but with None values
|
|
125
|
+
assert kv.KEYVAULT_BASE_URL is None
|
|
126
|
+
assert kv.KEYVAULT_SECRET_NAME is None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|