polars-etl-kit
High-performance ETL toolkit for Python — Polars + DuckDB powered.
Overview
polars-etl-kit is a collection of battle-tested ETL building blocks
extracted from real-world financial data pipelines processing 1500+ Excel
files per run. Each module is framework-agnostic and composable — use
one or use them all.
Modules
| Module | What it does | Core deps |
|---|---|---|
excel |
Parallel Excel parser with MD5 caching | polars, fastexcel |
schema |
YAML-driven schema registry → DDL generation | pyyaml |
warehouse |
DuckDB star-schema builder (dim/fact) | duckdb, polars |
matching |
Multi-tier name matching engine | pyyaml |
tree |
Hierarchical tree from flat data | polars |
geo |
Multi-provider geocoder (AMap/Baidu/Tencent) | aiohttp, polars |
db |
DuckDB connection with retry | duckdb |
kedro |
Custom Kedro datasets & dynamic hooks | kedro |
Install
# Core (polars + duckdb)
pip install polars-etl-kit
# With Excel support
pip install polars-etl-kit[excel]
# With Kedro integration
pip install polars-etl-kit[kedro]
# With geocoding
pip install polars-etl-kit[geo]
# Everything
pip install polars-etl-kit[all]
Quick start
Parallel Excel processing
from polars_etl_kit.excel import ExcelProcessor, FileCache, scan_files
# Scan for files
files = scan_files("data/01_raw", patterns=[r"(?P<code>\w+)_(?P<month>\d{6})\.xlsx"])
# Set up cache
cache = FileCache("cache/excel")
# Define your sheet handler
def handle_sheet(sheet_name, df, file_meta):
# Your parsing logic here
return df.with_columns(pl.lit(file_meta["code"]).alias("source_code"))
# Process in parallel
processor = ExcelProcessor(max_workers=8, file_cache=cache)
results = processor.process_files(
files,
process_sheet=handle_sheet,
cache_suffixes=["parsed"],
)
Schema registry + DuckDB warehouse
from polars_etl_kit import SchemaRegistry, WarehouseBuilder
# Load schema from YAML
registry = SchemaRegistry("schemas/dimensions.yaml", "schemas/facts.yaml")
# Generate DDL
print(registry.generate_all_ddl())
# Build warehouse
wh = WarehouseBuilder("analytics.duckdb", schema="analytics")
wh.create_schema()
wh.load_dimension("dim_customer", "data/dim_customer.parquet", pk="customer_id")
wh.load_fact("fact_sales", "data/fact_sales.parquet", indexes=["customer_id", "date"])
df = wh.query("SELECT * FROM analytics.v_summary")
Name matching (with fuzzy fallback)
from polars_etl_kit.matching import NameMatcher
# enable_fuzzy=True: Levenshtein 4th-tier fallback for typos
matcher = NameMatcher("mappings/standard_names.yaml", enable_fuzzy=True)
result = matcher.match({
"raw_text": "营业总收入",
"clean_name": "营业总收人", # 错字: "入"→"人"
"category": "利润表",
})
# → {"code": "B001", "name": "营业收入", "fuzzy_score": 0.89, ...}
Schema inference
from polars_etl_kit.schema import infer_schema_from_polars
import polars as pl
df = pl.DataFrame({"id": [1, 2], "name": ["Alice", "Bob"], "score": [95.0, 82.5]})
schema = infer_schema_from_polars(df, "dim_students", "学生维度")
print(schema.generate_duckdb_ddl())
# → CREATE TABLE IF NOT EXISTS dim_students (
# id BIGINT NOT NULL, name VARCHAR(255), score DOUBLE);
Tree building
from polars_etl_kit.tree import build_tree, find_parent_by_prefix
import polars as pl
nodes = pl.DataFrame([
{"node_id": "A_9", "parent_id": "#", "node_name": "Group"},
{"node_id": "A_1", "parent_id": "A_9", "node_name": "Sub A"},
{"node_id": "B_9", "parent_id": "A_9", "node_name": "Sub B"},
])
tree = build_tree(nodes, node_id_col="node_id", parent_id_col="parent_id",
node_name_col="node_name", find_parent=find_parent_by_prefix)
Multi-provider geocoding
from polars_etl_kit.geo import BatchGeocoder, get_geocoder
import polars as pl
entities = pl.DataFrame({
"code": ["E001"], "suffix": ["9"], "address": ["北京市朝阳区"],
})
# Switch providers: "amap" / "baidu" / "tencent"
geocoder = BatchGeocoder(
api_key="your_key",
cache_path="geo_cache.parquet",
provider="baidu", # 百度地图
key_cols=["code", "suffix"],
)
results = geocoder.run(entities, address_col="address")
# Or use factory for single queries
client = get_geocoder("tencent", api_key="your_key") # 腾讯地图
# async with client as c:
# result = await c.geocode("北京市朝阳区")
Philosophy
- Polars-first: all DataFrame operations use Polars for speed
- Framework-agnostic: modules work standalone; Kedro is optional
- Composable: each module has minimal internal dependencies
- Battle-tested: extracted from production pipelines processing 1500+ files
- Zero hardcoded paths: everything is parameterized
License
MIT — see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
polars_etl_kit-0.2.0.tar.gz
(32.6 kB
view details)
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file polars_etl_kit-0.2.0.tar.gz.
File metadata
- Download URL: polars_etl_kit-0.2.0.tar.gz
- Upload date:
- Size: 32.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
09e0032eee80fc778a85c3fab73f5a680e6d9e22bdff8708ae7649111ce0133f
|
|
| MD5 |
64b1693502981681d4c0bb02d94bb678
|
|
| BLAKE2b-256 |
9df99e86b11a9a3d04a00bc72068b36a79cc8ce65cb076187ab30228c28e778b
|
File details
Details for the file polars_etl_kit-0.2.0-py3-none-any.whl.
File metadata
- Download URL: polars_etl_kit-0.2.0-py3-none-any.whl
- Upload date:
- Size: 35.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
418f1ad3694c5decb414f3e3281e48f5b926bc27949bd01761d9cb759efb1207
|
|
| MD5 |
4b192993fa25fdaeeb87a33c71520350
|
|
| BLAKE2b-256 |
32d1bb3bd23f14796c2bddb4a8e722ebf814b4dbd18724ac29da7cda90ebae18
|