dlt-utils 0.1.0__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,32 @@
1
+ """
2
+ dlt_utils: Shared utilities for dlt data pipelines with multi-company support.
3
+
4
+ This package provides common utilities for building dlt pipelines that work with
5
+ multiple companies/tenants, including:
6
+
7
+ - PartitionedIncremental: State tracking per partition key
8
+ - Date utilities: Generate (year, week) and (year, month) sequences
9
+ - Schema utilities: Ensure database tables exist
10
+ """
11
+
12
+ from .incremental import PartitionedIncremental
13
+ from .dates import generate_year_weeks, generate_year_months
14
+ from .schema import (
15
+ ensure_all_tables_exist,
16
+ ensure_tables_for_resources,
17
+ get_tables_for_resources,
18
+ )
19
+
20
+ __version__ = "0.1.0"
21
+
22
+ __all__ = [
23
+ # Incremental
24
+ "PartitionedIncremental",
25
+ # Dates
26
+ "generate_year_weeks",
27
+ "generate_year_months",
28
+ # Schema
29
+ "ensure_all_tables_exist",
30
+ "ensure_tables_for_resources",
31
+ "get_tables_for_resources",
32
+ ]
dlt_utils/dates.py ADDED
@@ -0,0 +1,160 @@
1
+ """
2
+ Utility functions for date-based partitioning in dlt data extraction.
3
+
4
+ This module provides helper functions to generate sequences of (year, week) and
5
+ (year, month) tuples for partitioning data extraction across time periods.
6
+ These are used by resources that require parametric date-based queries.
7
+ """
8
+
9
+ from datetime import date, datetime, timedelta
10
+ from typing import List, Optional, Tuple
11
+
12
+
13
+ def generate_year_weeks(
14
+ start_year: Optional[int] = None,
15
+ start_week: Optional[int] = 1,
16
+ end_year: Optional[int] = None,
17
+ end_week: Optional[int] = None,
18
+ years_back: int = 3,
19
+ weeks_forward: int = 52,
20
+ ) -> List[Tuple[int, int]]:
21
+ """
22
+ Generate a list of (year, week) tuples using ISO week numbers.
23
+
24
+ Uses ISO 8601 week numbers where weeks start on Monday and the first
25
+ week of the year contains at least 4 days in that year.
26
+
27
+ Args:
28
+ start_year: Start year. Defaults to `years_back` years ago.
29
+ start_week: Start ISO week number 1-53. Defaults to 1.
30
+ end_year: End year. Defaults to the year of calculated end date.
31
+ end_week: End ISO week number 1-53. Defaults to current + `weeks_forward`.
32
+ years_back: Years to go back for default start_year. Defaults to 3.
33
+ weeks_forward: Weeks to add for default end calculation. Defaults to 52.
34
+
35
+ Returns:
36
+ List of (year, week) tuples in chronological order.
37
+
38
+ Examples:
39
+ >>> # Specific range
40
+ >>> generate_year_weeks(2024, 1, 2024, 4)
41
+ [(2024, 1), (2024, 2), (2024, 3), (2024, 4)]
42
+
43
+ >>> # Default: 3 years back to 52 weeks forward
44
+ >>> weeks = generate_year_weeks()
45
+ >>> weeks[0] # First week
46
+ (2022, 1)
47
+
48
+ >>> # Custom defaults
49
+ >>> generate_year_weeks(years_back=1, weeks_forward=4)
50
+ # From 1 year ago to 4 weeks from now
51
+ """
52
+ today = date.today()
53
+
54
+ # Default start_year: years_back years ago
55
+ if start_year is None:
56
+ start_year = today.year - years_back
57
+
58
+ # Default end: current date + weeks_forward
59
+ if end_year is None or end_week is None:
60
+ future_date = today + timedelta(weeks=weeks_forward)
61
+ iso_cal = future_date.isocalendar()
62
+ end_year = end_year or iso_cal.year
63
+ end_week = end_week or iso_cal.week
64
+
65
+ # Start from start_week of start_year
66
+ start_date = datetime.strptime(f"{start_year}-W{start_week:02d}-1", "%G-W%V-%u").date()
67
+ end_date = datetime.strptime(f"{end_year}-W{end_week:02d}-1", "%G-W%V-%u").date()
68
+
69
+ year_weeks = []
70
+ current = start_date
71
+ while current <= end_date:
72
+ iso_cal = current.isocalendar()
73
+ year_weeks.append((iso_cal.year, iso_cal.week))
74
+ current += timedelta(weeks=1)
75
+
76
+ return year_weeks
77
+
78
+
79
+ def generate_year_months(
80
+ start_year: Optional[int] = None,
81
+ start_month: Optional[int] = 1,
82
+ end_year: Optional[int] = None,
83
+ end_month: Optional[int] = None,
84
+ years_back: int = 3,
85
+ months_forward: int = 1,
86
+ ) -> List[Tuple[int, int]]:
87
+ """
88
+ Generate a list of (year, month) tuples from start to end.
89
+
90
+ Args:
91
+ start_year: Start year. Defaults to `years_back` years ago.
92
+ start_month: Start month 1-12. Defaults to 1 (January).
93
+ end_year: End year. Defaults to the year of calculated end date.
94
+ end_month: End month 1-12. Defaults to current + `months_forward`.
95
+ years_back: Years to go back for default start_year. Defaults to 3.
96
+ months_forward: Months to add for default end calculation. Defaults to 1.
97
+
98
+ Returns:
99
+ List of (year, month) tuples in chronological order.
100
+
101
+ Examples:
102
+ >>> generate_year_months(2024, 10, 2025, 2)
103
+ [(2024, 10), (2024, 11), (2024, 12), (2025, 1), (2025, 2)]
104
+
105
+ >>> # Default: 3 years back to 1 month forward
106
+ >>> months = generate_year_months()
107
+ >>> months[0]
108
+ (2022, 1)
109
+ """
110
+ today = date.today()
111
+
112
+ # Default start_year: years_back years ago
113
+ if start_year is None:
114
+ start_year = today.year - years_back
115
+
116
+ # Default end: current date + months_forward
117
+ if end_year is None or end_month is None:
118
+ # Calculate future date by adding months
119
+ future_month = today.month + months_forward
120
+ future_year = today.year
121
+ while future_month > 12:
122
+ future_month -= 12
123
+ future_year += 1
124
+ end_year = end_year or future_year
125
+ end_month = end_month or future_month
126
+
127
+ year_months = []
128
+ current_year = start_year
129
+ current_month = start_month
130
+
131
+ while (current_year, current_month) <= (end_year, end_month):
132
+ year_months.append((current_year, current_month))
133
+ current_month += 1
134
+ if current_month > 12:
135
+ current_month = 1
136
+ current_year += 1
137
+
138
+ return year_months
139
+
140
+
141
+ def get_current_iso_week() -> Tuple[int, int]:
142
+ """
143
+ Get the current ISO year and week number.
144
+
145
+ Returns:
146
+ Tuple of (year, week) for today.
147
+ """
148
+ iso_cal = date.today().isocalendar()
149
+ return (iso_cal.year, iso_cal.week)
150
+
151
+
152
+ def get_current_year_month() -> Tuple[int, int]:
153
+ """
154
+ Get the current year and month.
155
+
156
+ Returns:
157
+ Tuple of (year, month) for today.
158
+ """
159
+ today = date.today()
160
+ return (today.year, today.month)
@@ -0,0 +1,161 @@
1
+ """
2
+ Incremental state tracking voor dlt resources met partitionering.
3
+
4
+ Dit module biedt een PartitionedIncremental class die werkt als dlt.sources.incremental,
5
+ maar state per partition key (bijv. company_id) bijhoudt in plaats van per resource.
6
+ """
7
+
8
+ from typing import Any, Callable, Dict, List, TypeVar
9
+
10
+ TCursorValue = TypeVar("TCursorValue")
11
+
12
+
13
+ class PartitionedIncremental:
14
+ """
15
+ Incremental state tracking per partition key (bijv. company_id).
16
+
17
+ Werkt als dlt.sources.incremental, maar partitioneert state per key zodat
18
+ meerdere companies onafhankelijke cursors kunnen hebben binnen dezelfde resource.
19
+
20
+ Voorbeeld:
21
+ ```python
22
+ @dlt.resource
23
+ def sync_resource():
24
+ state = dlt.current.resource_state()
25
+ inc = PartitionedIncremental(
26
+ state=state,
27
+ state_key="sequences",
28
+ cursor_path="sequenceNumber",
29
+ initial_value=0,
30
+ )
31
+
32
+ for company_id in ["company_a", "company_b"]:
33
+ start_seq = inc.get_last_value(company_id)
34
+ for record in fetch_data(company_id, since=start_seq):
35
+ inc.track(company_id, record["sequenceNumber"])
36
+ yield record
37
+ ```
38
+
39
+ State structuur in dlt:
40
+ ```json
41
+ {
42
+ "sequences": {
43
+ "company_a": 12345,
44
+ "company_b": 67890
45
+ }
46
+ }
47
+ ```
48
+
49
+ Use cases:
50
+ - sequenceNumber tracking per company (Floriday sync endpoints)
51
+ - last_modified_at per company + period (Easyflex journaalposten)
52
+ - Any cursor that needs to be tracked per partition
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ state: Dict[str, Any],
58
+ state_key: str,
59
+ cursor_path: str = None,
60
+ initial_value: TCursorValue = None,
61
+ last_value_func: Callable[[List[TCursorValue]], TCursorValue] = max,
62
+ ):
63
+ """
64
+ Initialiseer PartitionedIncremental.
65
+
66
+ Args:
67
+ state: Resource state dict van dlt.current.resource_state().
68
+ state_key: Sleutel in state dict waar partition values worden opgeslagen.
69
+ cursor_path: Optioneel pad naar cursor veld in records (voor track_record).
70
+ initial_value: Waarde voor partitions zonder bestaande state.
71
+ last_value_func: Functie om "laatste" waarde te bepalen (default: max).
72
+ Gebruik min() voor aflopende cursors.
73
+ """
74
+ self._state = state
75
+ self._state_key = state_key
76
+ self._cursor_path = cursor_path
77
+ self._initial_value = initial_value
78
+ self._last_value_func = last_value_func
79
+
80
+ # Zorg dat state structuur bestaat
81
+ if state_key not in state:
82
+ state[state_key] = {}
83
+ self._partition_state = state[state_key]
84
+
85
+ # Track huidige waarden tijdens streaming (voor running max/min)
86
+ self._current_values: Dict[str, TCursorValue] = {}
87
+
88
+ def get_last_value(self, partition_key: str) -> TCursorValue:
89
+ """
90
+ Haal de laatst opgeslagen waarde op voor een partition.
91
+
92
+ Args:
93
+ partition_key: Identifier voor de partition (bijv. company_id).
94
+
95
+ Returns:
96
+ De opgeslagen waarde, of initial_value als er geen state is.
97
+ """
98
+ return self._partition_state.get(partition_key, self._initial_value)
99
+
100
+ def track(self, partition_key: str, cursor_value: TCursorValue) -> None:
101
+ """
102
+ Track een cursor waarde voor een partition.
103
+
104
+ Werkt running max/min bij en persisted naar state. Roep dit aan voor
105
+ elk record tijdens het streamen zodat de hoogste/laagste waarde
106
+ wordt onthouden.
107
+
108
+ Args:
109
+ partition_key: Identifier voor de partition (bijv. company_id).
110
+ cursor_value: De te tracken waarde (bijv. sequenceNumber).
111
+ """
112
+ current = self._current_values.get(partition_key)
113
+ if current is None:
114
+ current = self.get_last_value(partition_key)
115
+
116
+ if cursor_value is not None:
117
+ new_value = self._last_value_func(
118
+ [cursor_value, current] if current is not None else [cursor_value]
119
+ )
120
+ self._current_values[partition_key] = new_value
121
+ # Direct persisten naar state (dlt commit na resource completion)
122
+ self._partition_state[partition_key] = new_value
123
+
124
+ def track_record(self, partition_key: str, record: Dict[str, Any]) -> Dict[str, Any]:
125
+ """
126
+ Track cursor waarde uit een record via cursor_path.
127
+
128
+ Convenience methode voor gebruik in map functies.
129
+
130
+ Args:
131
+ partition_key: Identifier voor de partition.
132
+ record: Record dict met cursor veld.
133
+
134
+ Returns:
135
+ Het record ongewijzigd (voor chaining in pipelines).
136
+ """
137
+ if self._cursor_path:
138
+ cursor_value = record.get(self._cursor_path)
139
+ self.track(partition_key, cursor_value)
140
+ return record
141
+
142
+ def get_all_partitions(self) -> Dict[str, TCursorValue]:
143
+ """
144
+ Haal alle opgeslagen partition states op.
145
+
146
+ Returns:
147
+ Dict van partition_key -> cursor_value.
148
+ """
149
+ return dict(self._partition_state)
150
+
151
+ def reset_partition(self, partition_key: str) -> None:
152
+ """
153
+ Reset state voor een specifieke partition.
154
+
155
+ Args:
156
+ partition_key: Identifier voor de partition om te resetten.
157
+ """
158
+ if partition_key in self._partition_state:
159
+ del self._partition_state[partition_key]
160
+ if partition_key in self._current_values:
161
+ del self._current_values[partition_key]
dlt_utils/schema.py ADDED
@@ -0,0 +1,157 @@
1
+ """
2
+ Schema utilities for dlt pipelines.
3
+
4
+ Provides functions to ensure database tables exist before running pipelines,
5
+ which is useful for scenarios where you need tables created without data load.
6
+ """
7
+
8
+ import logging
9
+ from typing import Any, Dict, Iterable, List, Optional, TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ from dlt import Pipeline
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def ensure_all_tables_exist(
18
+ pipeline: "Pipeline",
19
+ only_tables: Optional[Iterable[str]] = None,
20
+ ) -> Dict[str, Any]:
21
+ """
22
+ Ensure all tables from the schema exist in the database.
23
+
24
+ Uses dlt's own SQL generation and executes CREATE TABLE statements for
25
+ tables that don't exist yet.
26
+
27
+ IMPORTANT: This bypasses dlt's schema hash check, so tables are created
28
+ even if the schema appears "up to date".
29
+
30
+ Args:
31
+ pipeline: An initialized dlt pipeline with a loaded schema.
32
+ only_tables: Optional - only create these specific tables.
33
+
34
+ Returns:
35
+ Dict with info about created tables (schema_update).
36
+
37
+ Example:
38
+ ```python
39
+ pipeline = dlt.pipeline(
40
+ pipeline_name="my_pipeline",
41
+ destination="postgres",
42
+ dataset_name="my_dataset",
43
+ )
44
+
45
+ # Load schema from file or run once
46
+ pipeline.run(my_source().with_resources("__nothing__"))
47
+
48
+ # Now ensure all tables exist
49
+ ensure_all_tables_exist(pipeline)
50
+ ```
51
+ """
52
+ tables_to_check = (
53
+ list(only_tables) if only_tables else list(pipeline.default_schema.tables.keys())
54
+ )
55
+
56
+ with pipeline.destination_client() as client:
57
+ # Get existing tables from the database
58
+ # get_storage_tables returns (table_name, columns_dict) tuples
59
+ storage_tables = list(client.get_storage_tables(tables_to_check))
60
+
61
+ # Build CREATE/ALTER statements
62
+ sql_scripts, schema_update = client._build_schema_update_sql(storage_tables)
63
+
64
+ if sql_scripts:
65
+ logger.info(f"Executing {len(sql_scripts)} SQL statements to create/update tables")
66
+ for sql in sql_scripts:
67
+ logger.debug(f"SQL: {sql[:100]}...")
68
+
69
+ # Execute the SQL
70
+ client.sql_client.execute_many(sql_scripts)
71
+
72
+ logger.info(f"Created/updated {len(schema_update)} tables: {list(schema_update.keys())}")
73
+ else:
74
+ logger.info("All tables already exist and are up to date")
75
+
76
+ return schema_update or {}
77
+
78
+
79
+ def get_tables_for_resources(
80
+ pipeline: "Pipeline",
81
+ resource_names: List[str],
82
+ ) -> List[str]:
83
+ """
84
+ Find all tables (root + children) for given resources.
85
+
86
+ This is useful when you need to know which tables will be created
87
+ for a set of resources, including nested/child tables.
88
+
89
+ Args:
90
+ pipeline: An initialized dlt pipeline with a loaded schema.
91
+ resource_names: List of resource names to find tables for.
92
+
93
+ Returns:
94
+ List of table names including child tables.
95
+
96
+ Example:
97
+ ```python
98
+ tables = get_tables_for_resources(pipeline, ["trade_items"])
99
+ # Returns: ["trade_items", "trade_items__photos", "trade_items__characteristics", ...]
100
+ ```
101
+ """
102
+ schema = pipeline.default_schema
103
+ tables = schema.tables
104
+
105
+ relevant_tables = set()
106
+
107
+ for resource_name in resource_names:
108
+ # Root table
109
+ if resource_name in tables:
110
+ relevant_tables.add(resource_name)
111
+
112
+ # Child tables (tables with parent relation or matching prefix)
113
+ for table_name, table_def in tables.items():
114
+ # Check prefix (common pattern for nested tables)
115
+ if table_name.startswith(f"{resource_name}__"):
116
+ relevant_tables.add(table_name)
117
+
118
+ # Or via parent chain
119
+ parent = table_def.get("parent")
120
+ checked = set()
121
+ while parent and parent not in checked:
122
+ checked.add(parent)
123
+ if parent == resource_name:
124
+ relevant_tables.add(table_name)
125
+ break
126
+ parent = tables.get(parent, {}).get("parent")
127
+
128
+ return list(relevant_tables)
129
+
130
+
131
+ def ensure_tables_for_resources(
132
+ pipeline: "Pipeline",
133
+ resource_names: List[str],
134
+ ) -> Dict[str, Any]:
135
+ """
136
+ Ensure tables exist for specific resources (including child tables).
137
+
138
+ Combines get_tables_for_resources and ensure_all_tables_exist for
139
+ convenient resource-based table creation.
140
+
141
+ Args:
142
+ pipeline: An initialized dlt pipeline with a loaded schema.
143
+ resource_names: List of resource names to ensure tables for.
144
+
145
+ Returns:
146
+ Dict with info about created tables.
147
+
148
+ Example:
149
+ ```python
150
+ # Only create tables for trade_items and organizations
151
+ ensure_tables_for_resources(pipeline, ["trade_items", "organizations"])
152
+ ```
153
+ """
154
+ tables = get_tables_for_resources(pipeline, resource_names)
155
+ logger.info(f"Ensuring {len(tables)} tables exist for resources {resource_names}")
156
+
157
+ return ensure_all_tables_exist(pipeline, only_tables=tables)
@@ -0,0 +1,181 @@
1
+ Metadata-Version: 2.4
2
+ Name: dlt_utils
3
+ Version: 0.1.0
4
+ Summary: Shared utilities for dlt data pipelines with multi-company support
5
+ Author: Jay van den Bos
6
+ License: MIT
7
+ Project-URL: Homepage, https://pypi.org/project/dlt_utils/
8
+ Project-URL: Repository, https://dev.azure.com/your-org/your-project/_git/dlt_utils
9
+ Keywords: dlt,data,pipeline,etl,incremental
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Topic :: Database
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.13
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: dlt>=1.19.1
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=7.0; extra == "dev"
23
+ Requires-Dist: pytest-cov; extra == "dev"
24
+ Requires-Dist: ruff; extra == "dev"
25
+
26
+ # dlt_utils
27
+
28
+ [![PyPI version](https://badge.fury.io/py/dlt_utils.svg)](https://badge.fury.io/py/dlt_utils)
29
+ [![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)
30
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
31
+
32
+ Shared utilities for [dlt](https://dlthub.com/) data pipelines with multi-company support.
33
+
34
+ ## Features
35
+
36
+ - **PartitionedIncremental**: Incremental state tracking per partition key (e.g., company_id)
37
+ - **Date utilities**: Generate (year, week) and (year, month) tuples for time-based partitioning
38
+ - **Schema utilities**: Ensure tables exist in destination database
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ # From PyPI
44
+ pip install dlt_utils
45
+
46
+ # For development
47
+ pip install -e ".[dev]"
48
+ ```
49
+
50
+ ## Usage
51
+
52
+ ### PartitionedIncremental
53
+
54
+ Track incremental state per company (or any partition key):
55
+
56
+ ```python
57
+ import dlt
58
+ from dlt_utils import PartitionedIncremental
59
+
60
+ @dlt.resource
61
+ def sync_resource():
62
+ state = dlt.current.resource_state()
63
+ inc = PartitionedIncremental(
64
+ state=state,
65
+ state_key="sequences",
66
+ cursor_path="sequenceNumber",
67
+ initial_value=0,
68
+ )
69
+
70
+ for company_id in ["company_a", "company_b"]:
71
+ start_seq = inc.get_last_value(company_id)
72
+ for record in fetch_data(company_id, since=start_seq):
73
+ inc.track(company_id, record["sequenceNumber"])
74
+ yield record
75
+ ```
76
+
77
+ ### Date utilities
78
+
79
+ Generate time periods for partitioned data extraction:
80
+
81
+ ```python
82
+ from dlt_utils import generate_year_weeks, generate_year_months
83
+
84
+ # Generate weeks from 2024 to now + 52 weeks
85
+ weeks = generate_year_weeks(start_year=2024)
86
+ # [(2024, 1), (2024, 2), ..., (2025, 52)]
87
+
88
+ # Generate months from October 2024 to February 2025
89
+ months = generate_year_months(2024, 10, 2025, 2)
90
+ # [(2024, 10), (2024, 11), (2024, 12), (2025, 1), (2025, 2)]
91
+ ```
92
+
93
+ ### Schema utilities
94
+
95
+ Ensure tables exist before running pipeline:
96
+
97
+ ```python
98
+ from dlt_utils import ensure_all_tables_exist, ensure_tables_for_resources
99
+
100
+ # Create all tables from schema
101
+ ensure_all_tables_exist(pipeline)
102
+
103
+ # Create only specific resource tables (including child tables)
104
+ ensure_tables_for_resources(pipeline, ["trade_items", "organizations"])
105
+ ```
106
+
107
+ ## Development
108
+
109
+ ```bash
110
+ # Install dev dependencies
111
+ pip install -e ".[dev]"
112
+
113
+ # Run tests
114
+ pytest
115
+
116
+ # Run linter
117
+ ruff check dlt_utils/
118
+ ```
119
+
120
+ ## CI/CD Pipeline
121
+
122
+ De pipeline draait automatisch bij:
123
+ - **Push naar `main`**: Voert tests uit
124
+ - **Tag met `v*` prefix**: Voert tests uit én publiceert naar PyPI
125
+
126
+ ### Pipeline Workflow
127
+
128
+ ```
129
+ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
130
+ │ Push/Tag │────▶│ Test Stage │────▶│ Publish Stage │
131
+ │ naar repo │ │ (altijd) │ │ (alleen tags) │
132
+ └─────────────────┘ └─────────────────┘ └─────────────────┘
133
+ │ │
134
+ ▼ ▼
135
+ - Install deps - Build package
136
+ - Run pytest - Upload to PyPI
137
+ - Publish results
138
+ ```
139
+
140
+ ### Nieuwe Versie Releasen
141
+
142
+ #### Optie 1: Via Git CLI
143
+
144
+ ```bash
145
+ # 1. Zorg dat alle changes gecommit zijn
146
+ git add .
147
+ git commit -m "Release v0.2.0"
148
+
149
+ # 2. Maak een tag aan
150
+ git tag v0.2.0
151
+
152
+ # 3. Push commit én tag naar remote
153
+ git push origin main
154
+ git push origin v0.2.0
155
+ ```
156
+
157
+ #### Optie 2: Via Azure DevOps
158
+
159
+ 1. Ga naar **Repos** → **Tags**
160
+ 2. Klik op **New tag**
161
+ 3. Vul in:
162
+ - **Name**: `v0.2.0` (moet beginnen met `v`)
163
+ - **Based on**: selecteer de commit of branch (bijv. `main`)
164
+ - **Description**: optioneel, bijv. "Added new feature X"
165
+ 4. Klik op **Create**
166
+
167
+ De pipeline wordt automatisch getriggered en publiceert naar PyPI.
168
+
169
+ ### Versienummering
170
+
171
+ Gebruik [Semantic Versioning](https://semver.org/):
172
+ - `vMAJOR.MINOR.PATCH` (bijv. `v1.2.3`)
173
+ - **MAJOR**: Breaking changes
174
+ - **MINOR**: Nieuwe features (backwards compatible)
175
+ - **PATCH**: Bugfixes
176
+
177
+ > ⚠️ **Belangrijk**: Vergeet niet de versie in `pyproject.toml` bij te werken vóór het taggen!
178
+
179
+ ## License
180
+
181
+ MIT
@@ -0,0 +1,8 @@
1
+ dlt_utils/__init__.py,sha256=WDJm8BP6dgqBTuvm3Lj7igKoDP2lm5mJHX8ZZT05yV8,875
2
+ dlt_utils/dates.py,sha256=HX27XaRE68tEVazKWmE8NFjOQutTcKjpBZ7ILU36qS0,5231
3
+ dlt_utils/incremental.py,sha256=qzk5_JDP21BV3XSnJnelM1FxNXpU_VeYL0OePHslTqU,5677
4
+ dlt_utils/schema.py,sha256=udCmTYGnRfTQWRfMFTBJetRf6lrEy-rbEx7sc0PrR4Y,5042
5
+ dlt_utils-0.1.0.dist-info/METADATA,sha256=1teOYnnZDtwbYk3JMlDEta7atN4qPYF7gUAIBMKvQMg,5377
6
+ dlt_utils-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
+ dlt_utils-0.1.0.dist-info/top_level.txt,sha256=KQIiBkukfDFtKV6NmbNK651_k5Oj0jLk5dpU0ZO1xuQ,10
8
+ dlt_utils-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ dlt_utils