dlt-utils 0.1.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.0/PKG-INFO +181 -0
- dlt_utils-0.1.0/README.md +156 -0
- dlt_utils-0.1.0/dlt_utils/__init__.py +32 -0
- dlt_utils-0.1.0/dlt_utils/dates.py +160 -0
- dlt_utils-0.1.0/dlt_utils/incremental.py +161 -0
- dlt_utils-0.1.0/dlt_utils/schema.py +157 -0
- dlt_utils-0.1.0/dlt_utils.egg-info/PKG-INFO +181 -0
- dlt_utils-0.1.0/dlt_utils.egg-info/SOURCES.txt +13 -0
- dlt_utils-0.1.0/dlt_utils.egg-info/dependency_links.txt +1 -0
- dlt_utils-0.1.0/dlt_utils.egg-info/requires.txt +6 -0
- dlt_utils-0.1.0/dlt_utils.egg-info/top_level.txt +1 -0
- dlt_utils-0.1.0/pyproject.toml +50 -0
- dlt_utils-0.1.0/setup.cfg +4 -0
- dlt_utils-0.1.0/tests/test_dates.py +168 -0
- dlt_utils-0.1.0/tests/test_incremental.py +210 -0
dlt_utils-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://badge.fury.io/py/dlt_utils)
|
|
29
|
+
[](https://www.python.org/downloads/)
|
|
30
|
+
[](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,156 @@
|
|
|
1
|
+
# dlt_utils
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/py/dlt_utils)
|
|
4
|
+
[](https://www.python.org/downloads/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
Shared utilities for [dlt](https://dlthub.com/) data pipelines with multi-company support.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **PartitionedIncremental**: Incremental state tracking per partition key (e.g., company_id)
|
|
12
|
+
- **Date utilities**: Generate (year, week) and (year, month) tuples for time-based partitioning
|
|
13
|
+
- **Schema utilities**: Ensure tables exist in destination database
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# From PyPI
|
|
19
|
+
pip install dlt_utils
|
|
20
|
+
|
|
21
|
+
# For development
|
|
22
|
+
pip install -e ".[dev]"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### PartitionedIncremental
|
|
28
|
+
|
|
29
|
+
Track incremental state per company (or any partition key):
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import dlt
|
|
33
|
+
from dlt_utils import PartitionedIncremental
|
|
34
|
+
|
|
35
|
+
@dlt.resource
|
|
36
|
+
def sync_resource():
|
|
37
|
+
state = dlt.current.resource_state()
|
|
38
|
+
inc = PartitionedIncremental(
|
|
39
|
+
state=state,
|
|
40
|
+
state_key="sequences",
|
|
41
|
+
cursor_path="sequenceNumber",
|
|
42
|
+
initial_value=0,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
for company_id in ["company_a", "company_b"]:
|
|
46
|
+
start_seq = inc.get_last_value(company_id)
|
|
47
|
+
for record in fetch_data(company_id, since=start_seq):
|
|
48
|
+
inc.track(company_id, record["sequenceNumber"])
|
|
49
|
+
yield record
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Date utilities
|
|
53
|
+
|
|
54
|
+
Generate time periods for partitioned data extraction:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from dlt_utils import generate_year_weeks, generate_year_months
|
|
58
|
+
|
|
59
|
+
# Generate weeks from 2024 to now + 52 weeks
|
|
60
|
+
weeks = generate_year_weeks(start_year=2024)
|
|
61
|
+
# [(2024, 1), (2024, 2), ..., (2025, 52)]
|
|
62
|
+
|
|
63
|
+
# Generate months from October 2024 to February 2025
|
|
64
|
+
months = generate_year_months(2024, 10, 2025, 2)
|
|
65
|
+
# [(2024, 10), (2024, 11), (2024, 12), (2025, 1), (2025, 2)]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Schema utilities
|
|
69
|
+
|
|
70
|
+
Ensure tables exist before running pipeline:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from dlt_utils import ensure_all_tables_exist, ensure_tables_for_resources
|
|
74
|
+
|
|
75
|
+
# Create all tables from schema
|
|
76
|
+
ensure_all_tables_exist(pipeline)
|
|
77
|
+
|
|
78
|
+
# Create only specific resource tables (including child tables)
|
|
79
|
+
ensure_tables_for_resources(pipeline, ["trade_items", "organizations"])
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Development
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# Install dev dependencies
|
|
86
|
+
pip install -e ".[dev]"
|
|
87
|
+
|
|
88
|
+
# Run tests
|
|
89
|
+
pytest
|
|
90
|
+
|
|
91
|
+
# Run linter
|
|
92
|
+
ruff check dlt_utils/
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## CI/CD Pipeline
|
|
96
|
+
|
|
97
|
+
De pipeline draait automatisch bij:
|
|
98
|
+
- **Push naar `main`**: Voert tests uit
|
|
99
|
+
- **Tag met `v*` prefix**: Voert tests uit én publiceert naar PyPI
|
|
100
|
+
|
|
101
|
+
### Pipeline Workflow
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
105
|
+
│ Push/Tag │────▶│ Test Stage │────▶│ Publish Stage │
|
|
106
|
+
│ naar repo │ │ (altijd) │ │ (alleen tags) │
|
|
107
|
+
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
|
108
|
+
│ │
|
|
109
|
+
▼ ▼
|
|
110
|
+
- Install deps - Build package
|
|
111
|
+
- Run pytest - Upload to PyPI
|
|
112
|
+
- Publish results
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Nieuwe Versie Releasen
|
|
116
|
+
|
|
117
|
+
#### Optie 1: Via Git CLI
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# 1. Zorg dat alle changes gecommit zijn
|
|
121
|
+
git add .
|
|
122
|
+
git commit -m "Release v0.2.0"
|
|
123
|
+
|
|
124
|
+
# 2. Maak een tag aan
|
|
125
|
+
git tag v0.2.0
|
|
126
|
+
|
|
127
|
+
# 3. Push commit én tag naar remote
|
|
128
|
+
git push origin main
|
|
129
|
+
git push origin v0.2.0
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
#### Optie 2: Via Azure DevOps
|
|
133
|
+
|
|
134
|
+
1. Ga naar **Repos** → **Tags**
|
|
135
|
+
2. Klik op **New tag**
|
|
136
|
+
3. Vul in:
|
|
137
|
+
- **Name**: `v0.2.0` (moet beginnen met `v`)
|
|
138
|
+
- **Based on**: selecteer de commit of branch (bijv. `main`)
|
|
139
|
+
- **Description**: optioneel, bijv. "Added new feature X"
|
|
140
|
+
4. Klik op **Create**
|
|
141
|
+
|
|
142
|
+
De pipeline wordt automatisch getriggered en publiceert naar PyPI.
|
|
143
|
+
|
|
144
|
+
### Versienummering
|
|
145
|
+
|
|
146
|
+
Gebruik [Semantic Versioning](https://semver.org/):
|
|
147
|
+
- `vMAJOR.MINOR.PATCH` (bijv. `v1.2.3`)
|
|
148
|
+
- **MAJOR**: Breaking changes
|
|
149
|
+
- **MINOR**: Nieuwe features (backwards compatible)
|
|
150
|
+
- **PATCH**: Bugfixes
|
|
151
|
+
|
|
152
|
+
> ⚠️ **Belangrijk**: Vergeet niet de versie in `pyproject.toml` bij te werken vóór het taggen!
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
|
@@ -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
|
+
]
|
|
@@ -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]
|