dlt-utils 0.2.0__tar.gz → 0.4.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dlt_utils
3
- Version: 0.2.0
3
+ Version: 0.4.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
@@ -26,10 +26,13 @@ from .args import (
26
26
  add_common_args,
27
27
  create_base_parser,
28
28
  CommonArgs,
29
+ filter_by_indices,
30
+ resolve_start_year,
29
31
  )
30
32
  from .azure_keyvault import AzureKeyVault
31
33
  from .logging import configure_logging
32
34
  from .pipeline import (
35
+ cleanup_tombstones,
33
36
  filter_resources,
34
37
  get_resource_names,
35
38
  mask_sensitive_value,
@@ -38,7 +41,7 @@ from .pipeline import (
38
41
  )
39
42
 
40
43
 
41
- __version__ = "0.2.0"
44
+ __version__ = "0.4.0"
42
45
 
43
46
  __all__ = [
44
47
  # Incremental
@@ -54,11 +57,14 @@ __all__ = [
54
57
  "add_common_args",
55
58
  "CommonArgs",
56
59
  "create_base_parser",
60
+ "filter_by_indices",
61
+ "resolve_start_year",
57
62
  # azure_keyvault
58
63
  "AzureKeyVault",
59
64
  # logging
60
65
  "configure_logging",
61
66
  # pipeline
67
+ "cleanup_tombstones",
62
68
  "filter_resources",
63
69
  "get_resource_names",
64
70
  "mask_sensitive_value",
@@ -0,0 +1,373 @@
1
+ """
2
+ Common CLI arguments for dlt pipelines.
3
+
4
+ This module provides reusable argument definitions that can be added to any
5
+ dlt pipeline's argument parser.
6
+
7
+ Usage:
8
+ from dlt_utils import create_base_parser, add_common_args
9
+
10
+ # Option 1: Use pre-configured base parser
11
+ parser = create_base_parser(description="My pipeline")
12
+ parser.add_argument("--my-custom-arg", ...)
13
+ args = parser.parse_args()
14
+
15
+ # Option 2: Add common args to existing parser
16
+ parser = argparse.ArgumentParser(description="My pipeline")
17
+ add_common_args(parser)
18
+ parser.add_argument("--my-custom-arg", ...)
19
+ args = parser.parse_args()
20
+
21
+ # Option 3: Exclude certain args and include optional ones
22
+ parser = create_base_parser(
23
+ description="My pipeline",
24
+ exclude=["dev_mode", "refresh"],
25
+ include=["companies", "start_year"],
26
+ )
27
+ args = parser.parse_args()
28
+
29
+ # Option 4: Filter items by indices from --companies
30
+ all_configs = get_company_configs()
31
+ selected = filter_by_indices(all_configs, args.companies)
32
+ """
33
+ import argparse
34
+ import logging
35
+ from dataclasses import dataclass
36
+ from datetime import datetime, timezone
37
+ from typing import Literal, TypeVar
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+ T = TypeVar("T")
42
+
43
+ # All available argument names for validation
44
+ CORE_ARGS = {"debug", "resources", "destination", "dev_mode", "refresh", "list_resources"}
45
+ OPTIONAL_ARGS = {"companies", "start_year"}
46
+ ALL_ARGS = CORE_ARGS | OPTIONAL_ARGS
47
+
48
+
49
+ @dataclass
50
+ class CommonArgs:
51
+ """
52
+ Dataclass representing common CLI arguments.
53
+
54
+ Attributes:
55
+ debug: Enable debug logging
56
+ destination: Target database type
57
+ dev_mode: Reset schema between runs
58
+ resources: List of resources to load (None = all)
59
+ refresh: Refresh mode for tables
60
+ list_resources: Show available resources and exit
61
+ companies: List of company indices to load (None = all)
62
+ start_year: Start year for partitioned resources (None = source default)
63
+ """
64
+ debug: bool = False
65
+ destination: Literal["duckdb", "mssql"] = "mssql"
66
+ dev_mode: bool = False
67
+ resources: list[str] | None = None
68
+ refresh: Literal["drop_sources", "drop_resources"] | None = None
69
+ list_resources: bool = False
70
+ companies: list[int] | None = None
71
+ start_year: int | None = None
72
+
73
+
74
+ def add_common_args(
75
+ parser: argparse.ArgumentParser,
76
+ default_destination: str = "mssql",
77
+ destinations: list[str] | None = None,
78
+ exclude: list[str] | None = None,
79
+ include: list[str] | None = None,
80
+ ) -> None:
81
+ """
82
+ Add common dlt pipeline arguments to an existing parser.
83
+
84
+ Args:
85
+ parser: The argument parser to add arguments to.
86
+ default_destination: Default destination database (default: mssql).
87
+ destinations: List of allowed destinations (default: ["duckdb", "mssql"]).
88
+ exclude: List of argument names to exclude from core args.
89
+ Valid names: debug, resources, destination, dev_mode, refresh, list_resources
90
+ include: List of optional argument names to include.
91
+ Valid names: companies, start_year
92
+
93
+ Core arguments (added by default unless excluded):
94
+ --debug: Enable debug logging
95
+ --resources: List of resources to load
96
+ --destination: Target database
97
+ --dev-mode: Reset schema between runs
98
+ --refresh: Refresh mode (drop_sources or drop_resources)
99
+ --list-resources: Show available resources and exit
100
+
101
+ Optional arguments (only added when included):
102
+ --companies: List of company indices to load
103
+ --start-year: Start year for partitioned resources
104
+
105
+ Raises:
106
+ ValueError: If invalid argument names are provided in exclude or include.
107
+ """
108
+ if destinations is None:
109
+ destinations = ["duckdb", "mssql"]
110
+
111
+ exclude_set = set(exclude) if exclude else set()
112
+ include_set = set(include) if include else set()
113
+
114
+ # Validate exclude names
115
+ invalid_exclude = exclude_set - CORE_ARGS
116
+ if invalid_exclude:
117
+ raise ValueError(
118
+ f"Invalid exclude argument(s): {invalid_exclude}. "
119
+ f"Valid options: {CORE_ARGS}"
120
+ )
121
+
122
+ # Validate include names
123
+ invalid_include = include_set - OPTIONAL_ARGS
124
+ if invalid_include:
125
+ raise ValueError(
126
+ f"Invalid include argument(s): {invalid_include}. "
127
+ f"Valid options: {OPTIONAL_ARGS}"
128
+ )
129
+
130
+ # Core arguments (added unless excluded)
131
+ if "debug" not in exclude_set:
132
+ parser.add_argument(
133
+ "--debug",
134
+ action="store_true",
135
+ help="Enable debug logging",
136
+ )
137
+
138
+ if "resources" not in exclude_set:
139
+ parser.add_argument(
140
+ "--resources",
141
+ nargs="*",
142
+ default=None,
143
+ help="List of resources to load (default: all)",
144
+ )
145
+
146
+ if "destination" not in exclude_set:
147
+ parser.add_argument(
148
+ "--destination",
149
+ choices=destinations,
150
+ default=default_destination,
151
+ help=f"Destination database (default: {default_destination})",
152
+ )
153
+
154
+ if "dev_mode" not in exclude_set:
155
+ parser.add_argument(
156
+ "--dev-mode",
157
+ action="store_true",
158
+ default=False,
159
+ help="Enable dev mode (resets schema between runs)",
160
+ )
161
+
162
+ if "refresh" not in exclude_set:
163
+ parser.add_argument(
164
+ "--refresh",
165
+ choices=["drop_sources", "drop_resources"],
166
+ default=None,
167
+ help=(
168
+ "Refresh mode: 'drop_sources' drops all tables and state for the source "
169
+ "(full rebuild), 'drop_resources' only drops tables and state for "
170
+ "selected resources"
171
+ ),
172
+ )
173
+
174
+ if "list_resources" not in exclude_set:
175
+ parser.add_argument(
176
+ "--list-resources",
177
+ action="store_true",
178
+ help="List all available resources and exit",
179
+ )
180
+
181
+ # Optional arguments (only added when included)
182
+ if "companies" in include_set:
183
+ parser.add_argument(
184
+ "--companies",
185
+ nargs="*",
186
+ type=int,
187
+ default=None,
188
+ help="List of company indices to load (default: all), e.g. 1 2",
189
+ )
190
+
191
+ if "start_year" in include_set:
192
+ parser.add_argument(
193
+ "--start-year",
194
+ type=int,
195
+ default=None,
196
+ help=(
197
+ "Start year for partitioned resources. "
198
+ "Can be an absolute year (e.g., 2023) or a negative number for years back "
199
+ "(e.g., -0 = current year, -1 = last year, -3 = 3 years ago). "
200
+ "Defaults to source default if not set."
201
+ ),
202
+ )
203
+
204
+
205
+ def create_base_parser(
206
+ description: str = "DLT data pipeline",
207
+ default_destination: str = "mssql",
208
+ destinations: list[str] | None = None,
209
+ exclude: list[str] | None = None,
210
+ include: list[str] | None = None,
211
+ ) -> argparse.ArgumentParser:
212
+ """
213
+ Create an argument parser with common dlt pipeline arguments pre-configured.
214
+
215
+ Args:
216
+ description: Description for the argument parser.
217
+ default_destination: Default destination database (default: mssql).
218
+ destinations: List of allowed destinations (default: ["duckdb", "mssql"]).
219
+ exclude: List of argument names to exclude from core args.
220
+ Valid names: debug, resources, destination, dev_mode, refresh, list_resources
221
+ include: List of optional argument names to include.
222
+ Valid names: companies, start_year
223
+
224
+ Returns:
225
+ ArgumentParser with common arguments already added.
226
+
227
+ Example:
228
+ # Basic usage
229
+ parser = create_base_parser("My Pipeline")
230
+ args = parser.parse_args()
231
+
232
+ # With custom args and exclusions
233
+ parser = create_base_parser(
234
+ "My Pipeline",
235
+ exclude=["dev_mode"],
236
+ include=["companies", "start_year"],
237
+ )
238
+ parser.add_argument("--my-custom-arg", type=int, default=10)
239
+ args = parser.parse_args()
240
+ """
241
+ parser = argparse.ArgumentParser(description=description)
242
+ add_common_args(parser, default_destination, destinations, exclude, include)
243
+ return parser
244
+
245
+
246
+ def get_common_args(args: argparse.Namespace) -> CommonArgs:
247
+ """
248
+ Extract common arguments from a parsed Namespace into a CommonArgs dataclass.
249
+
250
+ Args:
251
+ args: Parsed argument namespace.
252
+
253
+ Returns:
254
+ CommonArgs dataclass with extracted values.
255
+ """
256
+ return CommonArgs(
257
+ debug=getattr(args, "debug", False),
258
+ destination=getattr(args, "destination", "mssql"),
259
+ dev_mode=getattr(args, "dev_mode", False),
260
+ resources=getattr(args, "resources", None),
261
+ refresh=getattr(args, "refresh", None),
262
+ list_resources=getattr(args, "list_resources", False),
263
+ companies=getattr(args, "companies", None),
264
+ start_year=getattr(args, "start_year", None),
265
+ )
266
+
267
+
268
+ def resolve_start_year(start_year: int | None) -> int | None:
269
+ """
270
+ Resolve start_year argument to an absolute year.
271
+
272
+ Supports both absolute years (e.g., 2023) and negative offsets
273
+ (e.g., -0 = current year, -1 = last year, -3 = 3 years ago).
274
+
275
+ Args:
276
+ start_year: The start year argument from CLI. Can be:
277
+ - None: Returns None (let the source use its default)
278
+ - Positive int: Interpreted as absolute year
279
+ - Zero or negative int: Interpreted as years back from current year
280
+ (-0 = current year, -1 = last year, -2 = 2 years ago, etc.)
281
+
282
+ Returns:
283
+ Resolved absolute year, or None if not specified.
284
+
285
+ Example:
286
+ # Assuming current year is 2026
287
+ resolve_start_year(None) # Returns None
288
+ resolve_start_year(2023) # Returns 2023
289
+ resolve_start_year(0) # Returns 2026 (current year)
290
+ resolve_start_year(-1) # Returns 2025 (last year)
291
+ resolve_start_year(-3) # Returns 2023 (3 years ago)
292
+ """
293
+ if start_year is None:
294
+ return None
295
+
296
+ current_year = datetime.now(timezone.utc).year
297
+
298
+ if start_year <= 0:
299
+ # Negative or zero: interpret as years back
300
+ # 0 = current year, -1 = last year, etc.
301
+ resolved = current_year + start_year # start_year is negative, so this subtracts
302
+ logger.debug(
303
+ f"Resolved start_year {start_year} to {resolved} (current year: {current_year})"
304
+ )
305
+ return resolved
306
+ else:
307
+ # Positive: interpret as absolute year
308
+ return start_year
309
+
310
+
311
+ def filter_by_indices(
312
+ items: list[T],
313
+ indices: list[int] | None,
314
+ item_name: str = "item",
315
+ ) -> list[T]:
316
+ """
317
+ Filter a list of items by 1-based indices.
318
+
319
+ If indices is None or empty, returns all items.
320
+
321
+ Args:
322
+ items: The list of items to filter.
323
+ indices: List of 1-based indices to select, or None for all items.
324
+ item_name: Name of the items for error messages (e.g., "company", "resource").
325
+
326
+ Returns:
327
+ Filtered list of items.
328
+
329
+ Raises:
330
+ ValueError: If no items are available or no valid indices are provided.
331
+
332
+ Example:
333
+ configs = [config1, config2, config3]
334
+
335
+ # Select all
336
+ filter_by_indices(configs, None) # Returns [config1, config2, config3]
337
+
338
+ # Select specific items
339
+ filter_by_indices(configs, [1, 3]) # Returns [config1, config3]
340
+
341
+ # With custom name for errors
342
+ filter_by_indices(configs, [5], item_name="company")
343
+ # Raises: ValueError: No valid companies. Available: 1-3
344
+ """
345
+ if not items:
346
+ raise ValueError(f"No {item_name}s available.")
347
+
348
+ if not indices:
349
+ logger.info(f"Loading all {len(items)} {item_name}(s)")
350
+ return items
351
+
352
+ # Filter by 1-based indices
353
+ selected = [
354
+ items[i - 1]
355
+ for i in indices
356
+ if 0 < i <= len(items)
357
+ ]
358
+
359
+ if not selected:
360
+ raise ValueError(
361
+ f"No valid {item_name}s. Available: 1-{len(items)}"
362
+ )
363
+
364
+ # Log any invalid indices
365
+ invalid = [i for i in indices if i < 1 or i > len(items)]
366
+ if invalid:
367
+ logger.warning(
368
+ f"Ignored invalid {item_name} indices: {invalid}. "
369
+ f"Valid range: 1-{len(items)}"
370
+ )
371
+
372
+ logger.info(f"Selected {item_name}(s): {indices}")
373
+ return selected
@@ -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.2.0
3
+ Version: 0.4.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dlt_utils"
7
- version = "0.2.0"
7
+ version = "0.4.0"
8
8
  description = "Shared utilities for dlt data pipelines with multi-company support"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -1,154 +0,0 @@
1
- """
2
- Common CLI arguments for dlt pipelines.
3
-
4
- This module provides reusable argument definitions that can be added to any
5
- dlt pipeline's argument parser.
6
-
7
- Usage:
8
- from dlt_utils import create_base_parser, add_common_args
9
-
10
- # Option 1: Use pre-configured base parser
11
- parser = create_base_parser(description="My pipeline")
12
- parser.add_argument("--my-custom-arg", ...)
13
- args = parser.parse_args()
14
-
15
- # Option 2: Add common args to existing parser
16
- parser = argparse.ArgumentParser(description="My pipeline")
17
- add_common_args(parser)
18
- parser.add_argument("--my-custom-arg", ...)
19
- args = parser.parse_args()
20
- """
21
- import argparse
22
- from dataclasses import dataclass
23
- from typing import Literal
24
-
25
-
26
- @dataclass
27
- class CommonArgs:
28
- """
29
- Dataclass representing common CLI arguments.
30
-
31
- Attributes:
32
- debug: Enable debug logging
33
- destination: Target database type
34
- dev_mode: Reset schema between runs
35
- resources: List of resources to load (None = all)
36
- refresh: Refresh mode for tables
37
- list_resources: Show available resources and exit
38
- """
39
- debug: bool = False
40
- destination: Literal["duckdb", "mssql"] = "mssql"
41
- dev_mode: bool = False
42
- resources: list[str] | None = None
43
- refresh: Literal["drop_sources", "drop_resources"] | None = None
44
- list_resources: bool = False
45
-
46
-
47
- def add_common_args(
48
- parser: argparse.ArgumentParser,
49
- default_destination: str = "mssql",
50
- destinations: list[str] | None = None,
51
- ) -> None:
52
- """
53
- Add common dlt pipeline arguments to an existing parser.
54
-
55
- Args:
56
- parser: The argument parser to add arguments to.
57
- default_destination: Default destination database (default: mssql).
58
- destinations: List of allowed destinations (default: ["duckdb", "mssql"]).
59
-
60
- Added arguments:
61
- --debug: Enable debug logging
62
- --resources: List of resources to load
63
- --destination: Target database
64
- --dev-mode: Reset schema between runs
65
- --refresh: Refresh mode (drop_sources or drop_resources)
66
- --list-resources: Show available resources and exit
67
- """
68
- if destinations is None:
69
- destinations = ["duckdb", "mssql"]
70
-
71
- parser.add_argument(
72
- "--debug",
73
- action="store_true",
74
- help="Enable debug logging",
75
- )
76
- parser.add_argument(
77
- "--resources",
78
- nargs="*",
79
- default=None,
80
- help="List of resources to load (default: all)",
81
- )
82
- parser.add_argument(
83
- "--destination",
84
- choices=destinations,
85
- default=default_destination,
86
- help=f"Destination database (default: {default_destination})",
87
- )
88
- parser.add_argument(
89
- "--dev-mode",
90
- action="store_true",
91
- default=False,
92
- help="Enable dev mode (resets schema between runs)",
93
- )
94
- parser.add_argument(
95
- "--refresh",
96
- choices=["drop_sources", "drop_resources"],
97
- default=None,
98
- help=(
99
- "Refresh mode: 'drop_sources' drops all tables and state for the source "
100
- "(full rebuild), 'drop_resources' only drops tables and state for "
101
- "selected resources"
102
- ),
103
- )
104
- parser.add_argument(
105
- "--list-resources",
106
- action="store_true",
107
- help="List all available resources and exit",
108
- )
109
-
110
-
111
- def create_base_parser(
112
- description: str = "DLT data pipeline",
113
- default_destination: str = "mssql",
114
- destinations: list[str] | None = None,
115
- ) -> argparse.ArgumentParser:
116
- """
117
- Create an argument parser with common dlt pipeline arguments pre-configured.
118
-
119
- Args:
120
- description: Description for the argument parser.
121
- default_destination: Default destination database (default: mssql).
122
- destinations: List of allowed destinations (default: ["duckdb", "mssql"]).
123
-
124
- Returns:
125
- ArgumentParser with common arguments already added.
126
-
127
- Example:
128
- parser = create_base_parser("My Pipeline")
129
- parser.add_argument("--my-custom-arg", type=int, default=10)
130
- args = parser.parse_args()
131
- """
132
- parser = argparse.ArgumentParser(description=description)
133
- add_common_args(parser, default_destination, destinations)
134
- return parser
135
-
136
-
137
- def get_common_args(args: argparse.Namespace) -> CommonArgs:
138
- """
139
- Extract common arguments from a parsed Namespace into a CommonArgs dataclass.
140
-
141
- Args:
142
- args: Parsed argument namespace.
143
-
144
- Returns:
145
- CommonArgs dataclass with extracted values.
146
- """
147
- return CommonArgs(
148
- debug=getattr(args, "debug", False),
149
- destination=getattr(args, "destination", "mssql"),
150
- dev_mode=getattr(args, "dev_mode", False),
151
- resources=getattr(args, "resources", None),
152
- refresh=getattr(args, "refresh", None),
153
- list_resources=getattr(args, "list_resources", False),
154
- )
File without changes
File without changes
File without changes
File without changes
File without changes