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
+ [![PyPI - Version](https://img.shields.io/pypi/v/iceberg-loader.svg)](https://pypi.org/project/iceberg-loader)
51
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/iceberg-loader.svg)](https://pypi.org/project/iceberg-loader)
52
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/iceberg-loader.svg)](https://pypi.org/project/iceberg-loader)
53
+ [![Coverage](https://img.shields.io/badge/coverage-93%25-brightgreen)](coverage.xml)
54
+ [![CI](https://github.com/IvanMatveev/iceberg-loader/actions/workflows/ci.yml/badge.svg)](https://github.com/IvanMatveev/iceberg-loader/actions/workflows/ci.yml)
55
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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
+ [![PyPI - Version](https://img.shields.io/pypi/v/iceberg-loader.svg)](https://pypi.org/project/iceberg-loader)
6
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/iceberg-loader.svg)](https://pypi.org/project/iceberg-loader)
7
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/iceberg-loader.svg)](https://pypi.org/project/iceberg-loader)
8
+ [![Coverage](https://img.shields.io/badge/coverage-93%25-brightgreen)](coverage.xml)
9
+ [![CI](https://github.com/IvanMatveev/iceberg-loader/actions/workflows/ci.yml/badge.svg)](https://github.com/IvanMatveev/iceberg-loader/actions/workflows/ci.yml)
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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
+