iceberg-loader 0.0.4__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.
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
*.manifest
|
|
31
|
+
*.spec
|
|
32
|
+
|
|
33
|
+
# Installer logs
|
|
34
|
+
pip-log.txt
|
|
35
|
+
pip-delete-this-directory.txt
|
|
36
|
+
|
|
37
|
+
# Unit test / coverage reports
|
|
38
|
+
htmlcov/
|
|
39
|
+
.tox/
|
|
40
|
+
.nox/
|
|
41
|
+
.coverage
|
|
42
|
+
.coverage.*
|
|
43
|
+
.cache
|
|
44
|
+
nosetests.xml
|
|
45
|
+
coverage.xml
|
|
46
|
+
*.cover
|
|
47
|
+
*.py,cover
|
|
48
|
+
.hypothesis/
|
|
49
|
+
.pytest_cache/
|
|
50
|
+
|
|
51
|
+
# Translations
|
|
52
|
+
*.mo
|
|
53
|
+
*.pot
|
|
54
|
+
|
|
55
|
+
# Django stuff:
|
|
56
|
+
*.log
|
|
57
|
+
local_settings.py
|
|
58
|
+
db.sqlite3
|
|
59
|
+
|
|
60
|
+
# Flask stuff:
|
|
61
|
+
instance/
|
|
62
|
+
.webassets-cache
|
|
63
|
+
|
|
64
|
+
# Scrapy stuff:
|
|
65
|
+
.scrapy
|
|
66
|
+
|
|
67
|
+
# Sphinx documentation
|
|
68
|
+
docs/_build/
|
|
69
|
+
|
|
70
|
+
# PyBuilder
|
|
71
|
+
target/
|
|
72
|
+
|
|
73
|
+
# IPython
|
|
74
|
+
profile_default/
|
|
75
|
+
ipython_config.py
|
|
76
|
+
|
|
77
|
+
# pyenv
|
|
78
|
+
.python-version
|
|
79
|
+
|
|
80
|
+
# Pipenv
|
|
81
|
+
Pipfile.lock
|
|
82
|
+
|
|
83
|
+
# Hatch / virtual envs
|
|
84
|
+
.venv/
|
|
85
|
+
.hatch/
|
|
86
|
+
|
|
87
|
+
# PDM
|
|
88
|
+
.pdm.toml
|
|
89
|
+
.pdm-python
|
|
90
|
+
.pdm/
|
|
91
|
+
|
|
92
|
+
# poetry
|
|
93
|
+
poetry.lock
|
|
94
|
+
|
|
95
|
+
# mkdocs
|
|
96
|
+
site/
|
|
97
|
+
|
|
98
|
+
# VS Code
|
|
99
|
+
.vscode/
|
|
100
|
+
|
|
101
|
+
# Mac
|
|
102
|
+
.DS_Store
|
|
103
|
+
|
|
104
|
+
# Hatch env cache
|
|
105
|
+
*.hatch_env_cache
|
|
106
|
+
.cursor/
|
|
107
|
+
uv.lock
|
|
108
|
+
|
|
109
|
+
# Pytest
|
|
110
|
+
.pytest_cache/
|
|
111
|
+
|
|
112
|
+
# Coverage
|
|
113
|
+
coverage.xml
|
|
114
|
+
|
|
115
|
+
# Ruff
|
|
116
|
+
.ruff_cache/
|
|
117
|
+
|
|
118
|
+
# Mypy
|
|
119
|
+
.mypy_cache/
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Ivan Matveev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: iceberg-loader
|
|
3
|
+
Version: 0.0.4
|
|
4
|
+
Summary: Utilities for loading data into Iceberg tables using PyArrow
|
|
5
|
+
Project-URL: Documentation, https://github.com/Ivan Matveev/iceberg-loader#readme
|
|
6
|
+
Project-URL: Issues, https://github.com/Ivan Matveev/iceberg-loader/issues
|
|
7
|
+
Project-URL: Source, https://github.com/Ivan Matveev/iceberg-loader
|
|
8
|
+
Author-email: Ivan Matveev <skioneim@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: arrow,etl,iceberg
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
22
|
+
Classifier: Topic :: Database
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: pyarrow>=18.0.0
|
|
27
|
+
Requires-Dist: pyiceberg>=0.7.1
|
|
28
|
+
Provides-Extra: all
|
|
29
|
+
Requires-Dist: pyiceberg[hive,pyiceberg-core,s3fs]>=0.6.0; extra == 'all'
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: mypy>=1.10.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pre-commit>=3.5.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: ruff>=0.5.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: twine>=5.0.0; extra == 'dev'
|
|
37
|
+
Provides-Extra: hive
|
|
38
|
+
Requires-Dist: pyiceberg[hive]>=0.6.0; extra == 'hive'
|
|
39
|
+
Provides-Extra: s3
|
|
40
|
+
Requires-Dist: pyiceberg[s3fs]>=0.6.0; extra == 's3'
|
|
41
|
+
Provides-Extra: test
|
|
42
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'test'
|
|
43
|
+
Requires-Dist: pytest>=7.0; extra == 'test'
|
|
44
|
+
Description-Content-Type: text/markdown
|
|
45
|
+
|
|
46
|
+
# iceberg-loader
|
|
47
|
+
|
|
48
|
+
Utilities for loading data into Iceberg tables using PyArrow. Focus on robust ingestion (messy JSON, schema evolution, idempotent loads, upsert) with a simple facade and configurable defaults.
|
|
49
|
+
|
|
50
|
+
[](https://pypi.org/project/iceberg-loader)
|
|
51
|
+
[](https://pypi.org/project/iceberg-loader)
|
|
52
|
+
[](https://pypi.org/project/iceberg-loader)
|
|
53
|
+
[](coverage.xml)
|
|
54
|
+
[](https://github.com/IvanMatveev/iceberg-loader/actions/workflows/ci.yml)
|
|
55
|
+
[](LICENSE)
|
|
56
|
+
|
|
57
|
+
## Why iceberg-loader?
|
|
58
|
+
- Handles messy JSON: auto-serializes dict/list/mixed fields to strings so writes don’t fail.
|
|
59
|
+
- Schema evolution: add columns on the fly (opt-in), preserves field ids.
|
|
60
|
+
- Safe writes: append/overwrite, idempotent replace via `replace_filter`, upsert.
|
|
61
|
+
- Stream friendly: commit intervals, batches, IPC streams.
|
|
62
|
+
- One config (`LoaderConfig`) to set defaults; override per-call if needed.
|
|
63
|
+
|
|
64
|
+
## Features
|
|
65
|
+
- Arrow-first: `pa.Table`, `RecordBatch`, IPC.
|
|
66
|
+
- Messy JSON friendly: dict/list/mixed -> JSON strings.
|
|
67
|
+
- Schema evolution (opt-in).
|
|
68
|
+
- Idempotent replace (`replace_filter`) and upsert.
|
|
69
|
+
- Commit interval for long streams.
|
|
70
|
+
- Maintenance helpers (expire snapshots).
|
|
71
|
+
|
|
72
|
+
## Quickstart
|
|
73
|
+
```bash
|
|
74
|
+
pip install "iceberg-loader[all]"
|
|
75
|
+
```
|
|
76
|
+
```python
|
|
77
|
+
import pyarrow as pa
|
|
78
|
+
from pyiceberg.catalog import load_catalog
|
|
79
|
+
from iceberg_loader import LoaderConfig, load_data_to_iceberg
|
|
80
|
+
|
|
81
|
+
catalog = load_catalog("default")
|
|
82
|
+
data = pa.Table.from_pydict({"id": [1, 2], "signup_date": ["2023-01-01", "2023-01-02"]})
|
|
83
|
+
|
|
84
|
+
config = LoaderConfig(write_mode="append", partition_col="signup_date", schema_evolution=True)
|
|
85
|
+
load_data_to_iceberg(data, ("db", "users"), catalog, config=config)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Compatibility
|
|
89
|
+
- Python: 3.10, 3.11, 3.12, 3.13, 3.14
|
|
90
|
+
- PyArrow: >= 18.0.0
|
|
91
|
+
- PyIceberg: >= 0.7.1 (use extras `hive`, `s3fs`, `pyiceberg-core` as needed)
|
|
92
|
+
|
|
93
|
+
## Usage
|
|
94
|
+
|
|
95
|
+
### Basic Example
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
import pyarrow as pa
|
|
99
|
+
from pyiceberg.catalog import load_catalog
|
|
100
|
+
from iceberg_loader import LoaderConfig, load_data_to_iceberg
|
|
101
|
+
|
|
102
|
+
# 1. Connect to your catalog
|
|
103
|
+
catalog = load_catalog("default")
|
|
104
|
+
|
|
105
|
+
# 2. Prepare data
|
|
106
|
+
data = pa.Table.from_pydict({
|
|
107
|
+
"id": [1, 2, 3],
|
|
108
|
+
"name": ["Alice", "Bob", "Charlie"],
|
|
109
|
+
"created_at": [1672531200000, 1672617600000, 1672704000000],
|
|
110
|
+
"signup_date": ["2023-01-01", "2023-01-01", "2023-01-02"]
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
# 3. Load data
|
|
114
|
+
config = LoaderConfig(write_mode="append", partition_col="signup_date", schema_evolution=True)
|
|
115
|
+
result = load_data_to_iceberg(
|
|
116
|
+
table_data=data,
|
|
117
|
+
table_identifier=("my_db", "my_table"),
|
|
118
|
+
catalog=catalog,
|
|
119
|
+
config=config,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
print(result)
|
|
123
|
+
# {'rows_loaded': 3, 'write_mode': 'append', 'partition_col': 'signup_date', ...}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Idempotent Load (Replace Partition)
|
|
127
|
+
|
|
128
|
+
To safely re-load data for a specific day (avoiding duplicates):
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
config = LoaderConfig(
|
|
132
|
+
write_mode="append",
|
|
133
|
+
replace_filter="signup_date == '2023-01-01'",
|
|
134
|
+
partition_col="signup_date",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
load_data_to_iceberg(table_data=data, table_identifier=("my_db", "my_table"), catalog=catalog, config=config)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Upsert (Merge Into)
|
|
141
|
+
|
|
142
|
+
Perform a merge operation (update existing rows, insert new ones) based on key columns. Requires PyIceberg >= 0.7.0.
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
config = LoaderConfig(write_mode="upsert", join_cols=["id"])
|
|
146
|
+
load_data_to_iceberg(table_data=data, table_identifier=("my_db", "my_table"), catalog=catalog, config=config)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Batch Loading
|
|
150
|
+
For large datasets, use `load_batches_to_iceberg` with an iterator of RecordBatches:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from iceberg_loader import load_batches_to_iceberg
|
|
154
|
+
|
|
155
|
+
def batch_generator():
|
|
156
|
+
for i in range(10):
|
|
157
|
+
yield some_record_batch
|
|
158
|
+
|
|
159
|
+
result = load_batches_to_iceberg(
|
|
160
|
+
batch_iterator=batch_generator(),
|
|
161
|
+
table_identifier=("my_db", "large_table"),
|
|
162
|
+
catalog=catalog,
|
|
163
|
+
config=LoaderConfig(write_mode="append", commit_interval=100),
|
|
164
|
+
)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Stream Loading (Arrow IPC)
|
|
168
|
+
Load data directly from an Apache Arrow IPC stream (e.g., from a file or network socket):
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from iceberg_loader import load_ipc_stream_to_iceberg
|
|
172
|
+
|
|
173
|
+
# stream_source can be a file path or a file-like object (BytesIO)
|
|
174
|
+
result = load_ipc_stream_to_iceberg(
|
|
175
|
+
stream_source="data.arrow",
|
|
176
|
+
table_identifier=("my_db", "stream_table"),
|
|
177
|
+
catalog=catalog,
|
|
178
|
+
config=LoaderConfig(write_mode="append"),
|
|
179
|
+
)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Custom Settings
|
|
183
|
+
|
|
184
|
+
You can override default table properties:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
custom_props = {
|
|
188
|
+
'write.parquet.compression-codec': 'snappy',
|
|
189
|
+
'history.expire.min-snapshots-to-keep': 5,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
config = LoaderConfig(table_properties=custom_props, write_mode="append")
|
|
193
|
+
load_data_to_iceberg(..., config=config)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Maintenance helper
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
from iceberg_loader import expire_snapshots
|
|
200
|
+
|
|
201
|
+
table = catalog.load_table(("db", "users"))
|
|
202
|
+
expire_snapshots(table, keep_last=2)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## API Reference
|
|
206
|
+
|
|
207
|
+
### `load_batches_to_iceberg()`
|
|
208
|
+
|
|
209
|
+
Main function for loading a stream of batches into an Iceberg table.
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
def load_batches_to_iceberg(
|
|
213
|
+
batch_iterator: Iterator[pa.RecordBatch] | pa.RecordBatchReader,
|
|
214
|
+
table_identifier: tuple[str, str],
|
|
215
|
+
catalog: Catalog,
|
|
216
|
+
write_mode: Literal['overwrite', 'append', 'upsert'] = 'overwrite',
|
|
217
|
+
partition_col: str | None = None,
|
|
218
|
+
replace_filter: str | None = None,
|
|
219
|
+
schema_evolution: bool = False,
|
|
220
|
+
table_properties: dict[str, Any] | None = None,
|
|
221
|
+
commit_interval: int = 0,
|
|
222
|
+
join_cols: list[str] | None = None,
|
|
223
|
+
) -> dict[str, Any]
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
#### Parameters
|
|
227
|
+
|
|
228
|
+
**`batch_iterator`** *(Iterator[pa.RecordBatch] | pa.RecordBatchReader)*
|
|
229
|
+
- Iterator or reader that returns PyArrow RecordBatch objects
|
|
230
|
+
- Can be a generator, list of batches, or `pa.RecordBatchReader`
|
|
231
|
+
- Enables processing large volumes of data without loading the entire dataset into memory
|
|
232
|
+
|
|
233
|
+
**`table_identifier`** *(tuple[str, str])*
|
|
234
|
+
- Table identifier in the format `(namespace, table_name)`
|
|
235
|
+
- Example: `("my_database", "my_table")`
|
|
236
|
+
- If the table doesn't exist, it will be created automatically
|
|
237
|
+
|
|
238
|
+
**`catalog`** *(Catalog)*
|
|
239
|
+
- PyIceberg catalog instance (from `pyiceberg.catalog.load_catalog()`)
|
|
240
|
+
- Manages metadata and connection to the table storage
|
|
241
|
+
- Supports Hive, REST, Glue, and other catalog types
|
|
242
|
+
|
|
243
|
+
**`write_mode`** *(Literal['overwrite', 'append', 'upsert'], default='overwrite')*
|
|
244
|
+
- Data write mode:
|
|
245
|
+
- `'overwrite'`: First batch overwrites the table, subsequent batches are appended
|
|
246
|
+
- `'append'`: All batches are appended to existing data
|
|
247
|
+
- `'upsert'`: Merge operation (update/insert) based on `join_cols`
|
|
248
|
+
- Can be combined with `replace_filter` for idempotent writes (only for `'append'`)
|
|
249
|
+
|
|
250
|
+
**`partition_col`** *(str | None, default=None)*
|
|
251
|
+
- Column name (and optional transform) for table partitioning
|
|
252
|
+
- Used only when creating a new table
|
|
253
|
+
- Supported syntax:
|
|
254
|
+
- `"col_name"` (Identity transform)
|
|
255
|
+
- `"year(col_name)"`
|
|
256
|
+
- `"month(col_name)"`
|
|
257
|
+
- `"day(col_name)"`
|
|
258
|
+
- `"hour(col_name)"`
|
|
259
|
+
- `"bucket(N, col_name)"` (e.g., `"bucket(16, id)"`)
|
|
260
|
+
- `"truncate(W, col_name)"` (e.g., `"truncate(4, name)"`)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
**`replace_filter`** *(str | None, default=None)*
|
|
264
|
+
- SQL-like filter for idempotent writes (works only with `write_mode='append'`)
|
|
265
|
+
- Deletes existing rows matching the filter before the first write
|
|
266
|
+
- Example: `"event_date == '2023-01-01'"` or `"year == 2023 AND month == 1"`
|
|
267
|
+
- Used for safe partition reloading
|
|
268
|
+
|
|
269
|
+
**`join_cols`** *(list[str] | None, default=None)*
|
|
270
|
+
- List of column names to use as keys for upsert operations
|
|
271
|
+
- Required when `write_mode='upsert'`
|
|
272
|
+
- Example: `["id"]` or `["user_id", "date"]`
|
|
273
|
+
|
|
274
|
+
**`schema_evolution`** *(bool, default=False)*
|
|
275
|
+
- Enables automatic table schema evolution
|
|
276
|
+
- When `True`: new columns from incoming data are automatically added to the table
|
|
277
|
+
- When `False`: incoming data must match the existing schema
|
|
278
|
+
- When schema changes, buffer is flushed and committed
|
|
279
|
+
|
|
280
|
+
**`table_properties`** *(dict[str, Any] | None, default=None)*
|
|
281
|
+
- Additional Iceberg table properties
|
|
282
|
+
- Applied only when creating a new table
|
|
283
|
+
- Examples:
|
|
284
|
+
```python
|
|
285
|
+
{
|
|
286
|
+
'write.parquet.compression-codec': 'zstd',
|
|
287
|
+
'write.metadata.compression-codec': 'gzip',
|
|
288
|
+
'history.expire.min-snapshots-to-keep': 10
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**`commit_interval`** *(int, default=0)*
|
|
293
|
+
- Transaction commit frequency (number of batches)
|
|
294
|
+
- `0`: all batches are written in a single transaction (default)
|
|
295
|
+
- `> 0`: commit is performed every N batches
|
|
296
|
+
- Useful for long data streams to manage memory and create checkpoints
|
|
297
|
+
- Example: `commit_interval=100` will create a snapshot every 100 batches
|
|
298
|
+
|
|
299
|
+
#### Return Value
|
|
300
|
+
|
|
301
|
+
Dictionary with loading results:
|
|
302
|
+
```python
|
|
303
|
+
{
|
|
304
|
+
'rows_loaded': int, # Total number of rows loaded
|
|
305
|
+
'batches_processed': int, # Number of batches processed
|
|
306
|
+
'write_mode': str, # Write mode used
|
|
307
|
+
'partition_col': str | None, # Partitioning column
|
|
308
|
+
'schema_evolution': bool, # Whether schema_evolution was enabled
|
|
309
|
+
'commit_interval': int # Commit interval used
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
#### Usage Examples
|
|
314
|
+
|
|
315
|
+
**Basic stream loading:**
|
|
316
|
+
```python
|
|
317
|
+
def generate_batches():
|
|
318
|
+
for i in range(100):
|
|
319
|
+
data = {"id": [i], "value": [f"row_{i}"]}
|
|
320
|
+
yield pa.RecordBatch.from_pydict(data)
|
|
321
|
+
|
|
322
|
+
result = load_batches_to_iceberg(
|
|
323
|
+
batch_iterator=generate_batches(),
|
|
324
|
+
table_identifier=("db", "table"),
|
|
325
|
+
catalog=catalog,
|
|
326
|
+
write_mode="append"
|
|
327
|
+
)
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
**With commits for memory management:**
|
|
331
|
+
```python
|
|
332
|
+
result = load_batches_to_iceberg(
|
|
333
|
+
batch_iterator=large_batch_stream,
|
|
334
|
+
table_identifier=("db", "large_table"),
|
|
335
|
+
catalog=catalog,
|
|
336
|
+
commit_interval=50, # Commit every 50 batches
|
|
337
|
+
schema_evolution=True
|
|
338
|
+
)
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
**Idempotent partition loading:**
|
|
342
|
+
```python
|
|
343
|
+
result = load_batches_to_iceberg(
|
|
344
|
+
batch_iterator=daily_batches,
|
|
345
|
+
table_identifier=("db", "events"),
|
|
346
|
+
catalog=catalog,
|
|
347
|
+
write_mode="append",
|
|
348
|
+
replace_filter="event_date == '2023-12-09'",
|
|
349
|
+
partition_col="event_date"
|
|
350
|
+
)
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Development
|
|
354
|
+
|
|
355
|
+
This project uses [Hatch](https://hatch.pypa.io/) as build backend. For local workflows prefer [uv](https://docs.astral.sh/uv/).
|
|
356
|
+
|
|
357
|
+
### Run Tests
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
uv run python -m pytest
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Linting
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
uv run ruff check
|
|
367
|
+
uv run ruff format --check
|
|
368
|
+
uv run mypy
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Release
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
# Build artifacts
|
|
375
|
+
hatch build
|
|
376
|
+
|
|
377
|
+
# (Optional) check README/metadata
|
|
378
|
+
twine check dist/*
|
|
379
|
+
|
|
380
|
+
# Upload to TestPyPI
|
|
381
|
+
twine upload --repository testpypi dist/*
|
|
382
|
+
|
|
383
|
+
# Upload to PyPI
|
|
384
|
+
twine upload dist/*
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## Status / TestPyPI
|
|
388
|
+
|
|
389
|
+
Work in progress. Test build:
|
|
390
|
+
```bash
|
|
391
|
+
pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple "iceberg-loader[all]==0.0.1"
|
|
392
|
+
```
|
|
393
|
+
Package page: https://test.pypi.org/project/iceberg-loader/0.0.1/
|
|
394
|
+
|
|
395
|
+
GitHub Actions release runs on tag `v*` (see `.github/workflows/release.yml`). Required secrets:
|
|
396
|
+
- `PYPI_API_TOKEN` for PyPI
|
|
397
|
+
- `TEST_PYPI_API_TOKEN` (optional) for TestPyPI
|
|
398
|
+
|
|
399
|
+
### Examples
|
|
400
|
+
|
|
401
|
+
See `examples/` directory for runnable scripts (requires local docker environment):
|
|
402
|
+
|
|
403
|
+
```bash
|
|
404
|
+
cd examples && docker-compose up -d
|
|
405
|
+
hatch run python examples/load_example.py
|
|
406
|
+
hatch run python examples/advanced_scenarios.py
|
|
407
|
+
hatch run python examples/load_complex_json.py
|
|
408
|
+
hatch run python examples/load_stream.py
|
|
409
|
+
hatch run python examples/load_with_commits.py
|
|
410
|
+
hatch run python examples/load_from_api.py
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
## Documentation
|
|
414
|
+
|
|
415
|
+
Rendered docs (MkDocs) live in `docs/` (serve locally with `mkdocs serve`). The homepage mirrors the README for quick onboarding.
|
|
416
|
+
|
|
417
|
+
## Contributing
|
|
418
|
+
|
|
419
|
+
We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, coding style, and PR guidelines. Quick checks:
|
|
420
|
+
|
|
421
|
+
```bash
|
|
422
|
+
hatch run lint
|
|
423
|
+
hatch run test
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
## Contributors
|
|
427
|
+
|
|
428
|
+
Thanks to all contributors who have helped make this project better!
|
|
429
|
+
|
|
430
|
+
<a href="https://github.com/IvanMatveev/iceberg-loader/graphs/contributors">
|
|
431
|
+
<img src="https://contrib.rocks/image?repo=IvanMatveev/iceberg-loader" />
|
|
432
|
+
</a>
|
|
433
|
+
|
|
434
|
+
Made with [contrib.rocks](https://contrib.rocks).
|
|
435
|
+
|
|
436
|
+
## License
|
|
437
|
+
|
|
438
|
+
`iceberg-loader` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
# iceberg-loader
|
|
2
|
+
|
|
3
|
+
Utilities for loading data into Iceberg tables using PyArrow. Focus on robust ingestion (messy JSON, schema evolution, idempotent loads, upsert) with a simple facade and configurable defaults.
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/iceberg-loader)
|
|
6
|
+
[](https://pypi.org/project/iceberg-loader)
|
|
7
|
+
[](https://pypi.org/project/iceberg-loader)
|
|
8
|
+
[](coverage.xml)
|
|
9
|
+
[](https://github.com/IvanMatveev/iceberg-loader/actions/workflows/ci.yml)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
|
|
12
|
+
## Why iceberg-loader?
|
|
13
|
+
- Handles messy JSON: auto-serializes dict/list/mixed fields to strings so writes don’t fail.
|
|
14
|
+
- Schema evolution: add columns on the fly (opt-in), preserves field ids.
|
|
15
|
+
- Safe writes: append/overwrite, idempotent replace via `replace_filter`, upsert.
|
|
16
|
+
- Stream friendly: commit intervals, batches, IPC streams.
|
|
17
|
+
- One config (`LoaderConfig`) to set defaults; override per-call if needed.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
- Arrow-first: `pa.Table`, `RecordBatch`, IPC.
|
|
21
|
+
- Messy JSON friendly: dict/list/mixed -> JSON strings.
|
|
22
|
+
- Schema evolution (opt-in).
|
|
23
|
+
- Idempotent replace (`replace_filter`) and upsert.
|
|
24
|
+
- Commit interval for long streams.
|
|
25
|
+
- Maintenance helpers (expire snapshots).
|
|
26
|
+
|
|
27
|
+
## Quickstart
|
|
28
|
+
```bash
|
|
29
|
+
pip install "iceberg-loader[all]"
|
|
30
|
+
```
|
|
31
|
+
```python
|
|
32
|
+
import pyarrow as pa
|
|
33
|
+
from pyiceberg.catalog import load_catalog
|
|
34
|
+
from iceberg_loader import LoaderConfig, load_data_to_iceberg
|
|
35
|
+
|
|
36
|
+
catalog = load_catalog("default")
|
|
37
|
+
data = pa.Table.from_pydict({"id": [1, 2], "signup_date": ["2023-01-01", "2023-01-02"]})
|
|
38
|
+
|
|
39
|
+
config = LoaderConfig(write_mode="append", partition_col="signup_date", schema_evolution=True)
|
|
40
|
+
load_data_to_iceberg(data, ("db", "users"), catalog, config=config)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Compatibility
|
|
44
|
+
- Python: 3.10, 3.11, 3.12, 3.13, 3.14
|
|
45
|
+
- PyArrow: >= 18.0.0
|
|
46
|
+
- PyIceberg: >= 0.7.1 (use extras `hive`, `s3fs`, `pyiceberg-core` as needed)
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
### Basic Example
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import pyarrow as pa
|
|
54
|
+
from pyiceberg.catalog import load_catalog
|
|
55
|
+
from iceberg_loader import LoaderConfig, load_data_to_iceberg
|
|
56
|
+
|
|
57
|
+
# 1. Connect to your catalog
|
|
58
|
+
catalog = load_catalog("default")
|
|
59
|
+
|
|
60
|
+
# 2. Prepare data
|
|
61
|
+
data = pa.Table.from_pydict({
|
|
62
|
+
"id": [1, 2, 3],
|
|
63
|
+
"name": ["Alice", "Bob", "Charlie"],
|
|
64
|
+
"created_at": [1672531200000, 1672617600000, 1672704000000],
|
|
65
|
+
"signup_date": ["2023-01-01", "2023-01-01", "2023-01-02"]
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
# 3. Load data
|
|
69
|
+
config = LoaderConfig(write_mode="append", partition_col="signup_date", schema_evolution=True)
|
|
70
|
+
result = load_data_to_iceberg(
|
|
71
|
+
table_data=data,
|
|
72
|
+
table_identifier=("my_db", "my_table"),
|
|
73
|
+
catalog=catalog,
|
|
74
|
+
config=config,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
print(result)
|
|
78
|
+
# {'rows_loaded': 3, 'write_mode': 'append', 'partition_col': 'signup_date', ...}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Idempotent Load (Replace Partition)
|
|
82
|
+
|
|
83
|
+
To safely re-load data for a specific day (avoiding duplicates):
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
config = LoaderConfig(
|
|
87
|
+
write_mode="append",
|
|
88
|
+
replace_filter="signup_date == '2023-01-01'",
|
|
89
|
+
partition_col="signup_date",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
load_data_to_iceberg(table_data=data, table_identifier=("my_db", "my_table"), catalog=catalog, config=config)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Upsert (Merge Into)
|
|
96
|
+
|
|
97
|
+
Perform a merge operation (update existing rows, insert new ones) based on key columns. Requires PyIceberg >= 0.7.0.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
config = LoaderConfig(write_mode="upsert", join_cols=["id"])
|
|
101
|
+
load_data_to_iceberg(table_data=data, table_identifier=("my_db", "my_table"), catalog=catalog, config=config)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Batch Loading
|
|
105
|
+
For large datasets, use `load_batches_to_iceberg` with an iterator of RecordBatches:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from iceberg_loader import load_batches_to_iceberg
|
|
109
|
+
|
|
110
|
+
def batch_generator():
|
|
111
|
+
for i in range(10):
|
|
112
|
+
yield some_record_batch
|
|
113
|
+
|
|
114
|
+
result = load_batches_to_iceberg(
|
|
115
|
+
batch_iterator=batch_generator(),
|
|
116
|
+
table_identifier=("my_db", "large_table"),
|
|
117
|
+
catalog=catalog,
|
|
118
|
+
config=LoaderConfig(write_mode="append", commit_interval=100),
|
|
119
|
+
)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Stream Loading (Arrow IPC)
|
|
123
|
+
Load data directly from an Apache Arrow IPC stream (e.g., from a file or network socket):
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from iceberg_loader import load_ipc_stream_to_iceberg
|
|
127
|
+
|
|
128
|
+
# stream_source can be a file path or a file-like object (BytesIO)
|
|
129
|
+
result = load_ipc_stream_to_iceberg(
|
|
130
|
+
stream_source="data.arrow",
|
|
131
|
+
table_identifier=("my_db", "stream_table"),
|
|
132
|
+
catalog=catalog,
|
|
133
|
+
config=LoaderConfig(write_mode="append"),
|
|
134
|
+
)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Custom Settings
|
|
138
|
+
|
|
139
|
+
You can override default table properties:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
custom_props = {
|
|
143
|
+
'write.parquet.compression-codec': 'snappy',
|
|
144
|
+
'history.expire.min-snapshots-to-keep': 5,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
config = LoaderConfig(table_properties=custom_props, write_mode="append")
|
|
148
|
+
load_data_to_iceberg(..., config=config)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Maintenance helper
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from iceberg_loader import expire_snapshots
|
|
155
|
+
|
|
156
|
+
table = catalog.load_table(("db", "users"))
|
|
157
|
+
expire_snapshots(table, keep_last=2)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## API Reference
|
|
161
|
+
|
|
162
|
+
### `load_batches_to_iceberg()`
|
|
163
|
+
|
|
164
|
+
Main function for loading a stream of batches into an Iceberg table.
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
def load_batches_to_iceberg(
|
|
168
|
+
batch_iterator: Iterator[pa.RecordBatch] | pa.RecordBatchReader,
|
|
169
|
+
table_identifier: tuple[str, str],
|
|
170
|
+
catalog: Catalog,
|
|
171
|
+
write_mode: Literal['overwrite', 'append', 'upsert'] = 'overwrite',
|
|
172
|
+
partition_col: str | None = None,
|
|
173
|
+
replace_filter: str | None = None,
|
|
174
|
+
schema_evolution: bool = False,
|
|
175
|
+
table_properties: dict[str, Any] | None = None,
|
|
176
|
+
commit_interval: int = 0,
|
|
177
|
+
join_cols: list[str] | None = None,
|
|
178
|
+
) -> dict[str, Any]
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
#### Parameters
|
|
182
|
+
|
|
183
|
+
**`batch_iterator`** *(Iterator[pa.RecordBatch] | pa.RecordBatchReader)*
|
|
184
|
+
- Iterator or reader that returns PyArrow RecordBatch objects
|
|
185
|
+
- Can be a generator, list of batches, or `pa.RecordBatchReader`
|
|
186
|
+
- Enables processing large volumes of data without loading the entire dataset into memory
|
|
187
|
+
|
|
188
|
+
**`table_identifier`** *(tuple[str, str])*
|
|
189
|
+
- Table identifier in the format `(namespace, table_name)`
|
|
190
|
+
- Example: `("my_database", "my_table")`
|
|
191
|
+
- If the table doesn't exist, it will be created automatically
|
|
192
|
+
|
|
193
|
+
**`catalog`** *(Catalog)*
|
|
194
|
+
- PyIceberg catalog instance (from `pyiceberg.catalog.load_catalog()`)
|
|
195
|
+
- Manages metadata and connection to the table storage
|
|
196
|
+
- Supports Hive, REST, Glue, and other catalog types
|
|
197
|
+
|
|
198
|
+
**`write_mode`** *(Literal['overwrite', 'append', 'upsert'], default='overwrite')*
|
|
199
|
+
- Data write mode:
|
|
200
|
+
- `'overwrite'`: First batch overwrites the table, subsequent batches are appended
|
|
201
|
+
- `'append'`: All batches are appended to existing data
|
|
202
|
+
- `'upsert'`: Merge operation (update/insert) based on `join_cols`
|
|
203
|
+
- Can be combined with `replace_filter` for idempotent writes (only for `'append'`)
|
|
204
|
+
|
|
205
|
+
**`partition_col`** *(str | None, default=None)*
|
|
206
|
+
- Column name (and optional transform) for table partitioning
|
|
207
|
+
- Used only when creating a new table
|
|
208
|
+
- Supported syntax:
|
|
209
|
+
- `"col_name"` (Identity transform)
|
|
210
|
+
- `"year(col_name)"`
|
|
211
|
+
- `"month(col_name)"`
|
|
212
|
+
- `"day(col_name)"`
|
|
213
|
+
- `"hour(col_name)"`
|
|
214
|
+
- `"bucket(N, col_name)"` (e.g., `"bucket(16, id)"`)
|
|
215
|
+
- `"truncate(W, col_name)"` (e.g., `"truncate(4, name)"`)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
**`replace_filter`** *(str | None, default=None)*
|
|
219
|
+
- SQL-like filter for idempotent writes (works only with `write_mode='append'`)
|
|
220
|
+
- Deletes existing rows matching the filter before the first write
|
|
221
|
+
- Example: `"event_date == '2023-01-01'"` or `"year == 2023 AND month == 1"`
|
|
222
|
+
- Used for safe partition reloading
|
|
223
|
+
|
|
224
|
+
**`join_cols`** *(list[str] | None, default=None)*
|
|
225
|
+
- List of column names to use as keys for upsert operations
|
|
226
|
+
- Required when `write_mode='upsert'`
|
|
227
|
+
- Example: `["id"]` or `["user_id", "date"]`
|
|
228
|
+
|
|
229
|
+
**`schema_evolution`** *(bool, default=False)*
|
|
230
|
+
- Enables automatic table schema evolution
|
|
231
|
+
- When `True`: new columns from incoming data are automatically added to the table
|
|
232
|
+
- When `False`: incoming data must match the existing schema
|
|
233
|
+
- When schema changes, buffer is flushed and committed
|
|
234
|
+
|
|
235
|
+
**`table_properties`** *(dict[str, Any] | None, default=None)*
|
|
236
|
+
- Additional Iceberg table properties
|
|
237
|
+
- Applied only when creating a new table
|
|
238
|
+
- Examples:
|
|
239
|
+
```python
|
|
240
|
+
{
|
|
241
|
+
'write.parquet.compression-codec': 'zstd',
|
|
242
|
+
'write.metadata.compression-codec': 'gzip',
|
|
243
|
+
'history.expire.min-snapshots-to-keep': 10
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**`commit_interval`** *(int, default=0)*
|
|
248
|
+
- Transaction commit frequency (number of batches)
|
|
249
|
+
- `0`: all batches are written in a single transaction (default)
|
|
250
|
+
- `> 0`: commit is performed every N batches
|
|
251
|
+
- Useful for long data streams to manage memory and create checkpoints
|
|
252
|
+
- Example: `commit_interval=100` will create a snapshot every 100 batches
|
|
253
|
+
|
|
254
|
+
#### Return Value
|
|
255
|
+
|
|
256
|
+
Dictionary with loading results:
|
|
257
|
+
```python
|
|
258
|
+
{
|
|
259
|
+
'rows_loaded': int, # Total number of rows loaded
|
|
260
|
+
'batches_processed': int, # Number of batches processed
|
|
261
|
+
'write_mode': str, # Write mode used
|
|
262
|
+
'partition_col': str | None, # Partitioning column
|
|
263
|
+
'schema_evolution': bool, # Whether schema_evolution was enabled
|
|
264
|
+
'commit_interval': int # Commit interval used
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
#### Usage Examples
|
|
269
|
+
|
|
270
|
+
**Basic stream loading:**
|
|
271
|
+
```python
|
|
272
|
+
def generate_batches():
|
|
273
|
+
for i in range(100):
|
|
274
|
+
data = {"id": [i], "value": [f"row_{i}"]}
|
|
275
|
+
yield pa.RecordBatch.from_pydict(data)
|
|
276
|
+
|
|
277
|
+
result = load_batches_to_iceberg(
|
|
278
|
+
batch_iterator=generate_batches(),
|
|
279
|
+
table_identifier=("db", "table"),
|
|
280
|
+
catalog=catalog,
|
|
281
|
+
write_mode="append"
|
|
282
|
+
)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**With commits for memory management:**
|
|
286
|
+
```python
|
|
287
|
+
result = load_batches_to_iceberg(
|
|
288
|
+
batch_iterator=large_batch_stream,
|
|
289
|
+
table_identifier=("db", "large_table"),
|
|
290
|
+
catalog=catalog,
|
|
291
|
+
commit_interval=50, # Commit every 50 batches
|
|
292
|
+
schema_evolution=True
|
|
293
|
+
)
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**Idempotent partition loading:**
|
|
297
|
+
```python
|
|
298
|
+
result = load_batches_to_iceberg(
|
|
299
|
+
batch_iterator=daily_batches,
|
|
300
|
+
table_identifier=("db", "events"),
|
|
301
|
+
catalog=catalog,
|
|
302
|
+
write_mode="append",
|
|
303
|
+
replace_filter="event_date == '2023-12-09'",
|
|
304
|
+
partition_col="event_date"
|
|
305
|
+
)
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Development
|
|
309
|
+
|
|
310
|
+
This project uses [Hatch](https://hatch.pypa.io/) as build backend. For local workflows prefer [uv](https://docs.astral.sh/uv/).
|
|
311
|
+
|
|
312
|
+
### Run Tests
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
uv run python -m pytest
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Linting
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
uv run ruff check
|
|
322
|
+
uv run ruff format --check
|
|
323
|
+
uv run mypy
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Release
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
# Build artifacts
|
|
330
|
+
hatch build
|
|
331
|
+
|
|
332
|
+
# (Optional) check README/metadata
|
|
333
|
+
twine check dist/*
|
|
334
|
+
|
|
335
|
+
# Upload to TestPyPI
|
|
336
|
+
twine upload --repository testpypi dist/*
|
|
337
|
+
|
|
338
|
+
# Upload to PyPI
|
|
339
|
+
twine upload dist/*
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
## Status / TestPyPI
|
|
343
|
+
|
|
344
|
+
Work in progress. Test build:
|
|
345
|
+
```bash
|
|
346
|
+
pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple "iceberg-loader[all]==0.0.1"
|
|
347
|
+
```
|
|
348
|
+
Package page: https://test.pypi.org/project/iceberg-loader/0.0.1/
|
|
349
|
+
|
|
350
|
+
GitHub Actions release runs on tag `v*` (see `.github/workflows/release.yml`). Required secrets:
|
|
351
|
+
- `PYPI_API_TOKEN` for PyPI
|
|
352
|
+
- `TEST_PYPI_API_TOKEN` (optional) for TestPyPI
|
|
353
|
+
|
|
354
|
+
### Examples
|
|
355
|
+
|
|
356
|
+
See `examples/` directory for runnable scripts (requires local docker environment):
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
cd examples && docker-compose up -d
|
|
360
|
+
hatch run python examples/load_example.py
|
|
361
|
+
hatch run python examples/advanced_scenarios.py
|
|
362
|
+
hatch run python examples/load_complex_json.py
|
|
363
|
+
hatch run python examples/load_stream.py
|
|
364
|
+
hatch run python examples/load_with_commits.py
|
|
365
|
+
hatch run python examples/load_from_api.py
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
## Documentation
|
|
369
|
+
|
|
370
|
+
Rendered docs (MkDocs) live in `docs/` (serve locally with `mkdocs serve`). The homepage mirrors the README for quick onboarding.
|
|
371
|
+
|
|
372
|
+
## Contributing
|
|
373
|
+
|
|
374
|
+
We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, coding style, and PR guidelines. Quick checks:
|
|
375
|
+
|
|
376
|
+
```bash
|
|
377
|
+
hatch run lint
|
|
378
|
+
hatch run test
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
## Contributors
|
|
382
|
+
|
|
383
|
+
Thanks to all contributors who have helped make this project better!
|
|
384
|
+
|
|
385
|
+
<a href="https://github.com/IvanMatveev/iceberg-loader/graphs/contributors">
|
|
386
|
+
<img src="https://contrib.rocks/image?repo=IvanMatveev/iceberg-loader" />
|
|
387
|
+
</a>
|
|
388
|
+
|
|
389
|
+
Made with [contrib.rocks](https://contrib.rocks).
|
|
390
|
+
|
|
391
|
+
## License
|
|
392
|
+
|
|
393
|
+
`iceberg-loader` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "iceberg-loader"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = 'Utilities for loading data into Iceberg tables using PyArrow'
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
keywords = ["iceberg", "arrow", "etl"]
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Ivan Matveev", email = "skioneim@gmail.com" },
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Typing :: Typed",
|
|
20
|
+
"Programming Language :: Python",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
"Programming Language :: Python :: Implementation :: CPython",
|
|
27
|
+
"Operating System :: OS Independent",
|
|
28
|
+
"Topic :: Database",
|
|
29
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
30
|
+
]
|
|
31
|
+
dependencies = [
|
|
32
|
+
"pyarrow>=18.0.0",
|
|
33
|
+
"pyiceberg>=0.7.1",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
hive = [
|
|
38
|
+
"pyiceberg[hive]>=0.6.0",
|
|
39
|
+
]
|
|
40
|
+
s3 = [
|
|
41
|
+
"pyiceberg[s3fs]>=0.6.0",
|
|
42
|
+
]
|
|
43
|
+
all = [
|
|
44
|
+
"pyiceberg[hive,s3fs,pyiceberg-core]>=0.6.0",
|
|
45
|
+
]
|
|
46
|
+
test = [
|
|
47
|
+
"pytest>=7.0",
|
|
48
|
+
"pytest-cov>=4.0",
|
|
49
|
+
]
|
|
50
|
+
dev = [
|
|
51
|
+
"ruff>=0.5.0",
|
|
52
|
+
"mypy>=1.10.0",
|
|
53
|
+
"pytest>=7.0",
|
|
54
|
+
"pytest-cov>=4.0",
|
|
55
|
+
"pre-commit>=3.5.0",
|
|
56
|
+
"twine>=5.0.0",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[project.urls]
|
|
60
|
+
Documentation = "https://github.com/Ivan Matveev/iceberg-loader#readme"
|
|
61
|
+
Issues = "https://github.com/Ivan Matveev/iceberg-loader/issues"
|
|
62
|
+
Source = "https://github.com/Ivan Matveev/iceberg-loader"
|
|
63
|
+
|
|
64
|
+
[tool.hatch.version]
|
|
65
|
+
path = "src/iceberg_loader/__about__.py"
|
|
66
|
+
|
|
67
|
+
[tool.hatch.envs.default]
|
|
68
|
+
features = ["all"]
|
|
69
|
+
dependencies = [
|
|
70
|
+
"pytest>=7.0",
|
|
71
|
+
"pytest-cov>=4.0",
|
|
72
|
+
"ruff>=0.5.0",
|
|
73
|
+
"mypy>=1.10.0",
|
|
74
|
+
"pre-commit>=3.5.0",
|
|
75
|
+
]
|
|
76
|
+
[tool.hatch.envs.default.scripts]
|
|
77
|
+
test = "pytest {args:tests}"
|
|
78
|
+
lint = "ruff check"
|
|
79
|
+
format = "ruff format"
|
|
80
|
+
|
|
81
|
+
[tool.hatch.envs.types]
|
|
82
|
+
extra-dependencies = [
|
|
83
|
+
"mypy>=1.0.0",
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
[tool.hatch.envs.types.scripts]
|
|
87
|
+
check = "mypy --install-types --non-interactive {args:src/iceberg_loader tests}"
|
|
88
|
+
|
|
89
|
+
[tool.coverage.run]
|
|
90
|
+
source_pkgs = ["iceberg_loader", "tests"]
|
|
91
|
+
branch = true
|
|
92
|
+
parallel = true
|
|
93
|
+
omit = [
|
|
94
|
+
"src/iceberg_loader/__about__.py",
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
[tool.coverage.paths]
|
|
98
|
+
iceberg_loader = ["src/iceberg_loader", "*/iceberg-loader/src/iceberg_loader"]
|
|
99
|
+
tests = ["tests", "*/iceberg-loader/tests"]
|
|
100
|
+
|
|
101
|
+
[tool.coverage.report]
|
|
102
|
+
exclude_lines = [
|
|
103
|
+
"no cov",
|
|
104
|
+
"if __name__ == .__main__.:",
|
|
105
|
+
"if TYPE_CHECKING:",
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
[tool.ruff]
|
|
109
|
+
line-length = 120
|
|
110
|
+
indent-width = 4
|
|
111
|
+
target-version = "py310"
|
|
112
|
+
|
|
113
|
+
[tool.ruff.lint]
|
|
114
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "N", "TID"]
|
|
115
|
+
ignore = ["BLE001", "UP017"]
|
|
116
|
+
|
|
117
|
+
[tool.ruff.format]
|
|
118
|
+
quote-style = "single"
|
|
119
|
+
indent-style = "space"
|
|
120
|
+
|
|
121
|
+
[tool.mypy]
|
|
122
|
+
python_version = "3.12"
|
|
123
|
+
warn_unused_configs = true
|
|
124
|
+
warn_redundant_casts = true
|
|
125
|
+
warn_return_any = false
|
|
126
|
+
strict_optional = true
|
|
127
|
+
disallow_untyped_defs = true
|
|
128
|
+
disallow_untyped_calls = false
|
|
129
|
+
follow_imports = "normal"
|
|
130
|
+
show_error_codes = true
|
|
131
|
+
exclude = ["examples/"]
|
|
132
|
+
|
|
133
|
+
[[tool.mypy.overrides]]
|
|
134
|
+
module = "tests.*"
|
|
135
|
+
disallow_untyped_defs = false
|
|
136
|
+
warn_return_any = false
|
|
137
|
+
|
|
138
|
+
[[tool.mypy.overrides]]
|
|
139
|
+
module = ["pyarrow", "pyarrow.*", "pyiceberg.*", "requests"]
|
|
140
|
+
ignore_missing_imports = true
|
|
141
|
+
|
|
142
|
+
[tool.hatch.build.targets.wheel]
|
|
143
|
+
packages = ["src/iceberg_loader"]
|
|
144
|
+
include = [
|
|
145
|
+
"src/iceberg_loader/py.typed",
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
[tool.hatch.build.targets.sdist]
|
|
149
|
+
include = [
|
|
150
|
+
"src/iceberg_loader/py.typed",
|
|
151
|
+
]
|
|
152
|
+
|