sunstone-data-platform 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sunstone_data/__init__.py +36 -0
- sunstone_data/_version.py +24 -0
- sunstone_data/auth/__init__.py +43 -0
- sunstone_data/auth/client_creds.py +239 -0
- sunstone_data/auth/pkce.py +307 -0
- sunstone_data/auth/store.py +97 -0
- sunstone_data/catalog.py +175 -0
- sunstone_data/cli/__init__.py +5 -0
- sunstone_data/cli/_app.py +26 -0
- sunstone_data/cli/_errors.py +31 -0
- sunstone_data/cli/commands/__init__.py +22 -0
- sunstone_data/cli/commands/data_auth.py +120 -0
- sunstone_data/cli/commands/data_import.py +157 -0
- sunstone_data/cli/commands/data_info.py +166 -0
- sunstone_data/cli/commands/data_publish.py +69 -0
- sunstone_data/cli/commands/ls.py +49 -0
- sunstone_data/cli/commands/prov.py +58 -0
- sunstone_data/cli/commands/refs.py +142 -0
- sunstone_data/cli/commands/show.py +139 -0
- sunstone_data/cli/commands/snapshots.py +77 -0
- sunstone_data/cli/commands/sparql.py +68 -0
- sunstone_data/cli/main.py +30 -0
- sunstone_data/config.py +250 -0
- sunstone_data/discovery.py +317 -0
- sunstone_data/env_commands.py +97 -0
- sunstone_data/env_section.py +86 -0
- sunstone_data/graph/__init__.py +36 -0
- sunstone_data/graph/client.py +328 -0
- sunstone_data/graph/dataset.py +438 -0
- sunstone_data/graph/terms.py +34 -0
- sunstone_data/identifiers.py +155 -0
- sunstone_data/import_/__init__.py +28 -0
- sunstone_data/import_/fetcher.py +259 -0
- sunstone_data/import_/owner.py +130 -0
- sunstone_data/import_/parser.py +492 -0
- sunstone_data/import_/pipeline.py +275 -0
- sunstone_data/import_/storage.py +135 -0
- sunstone_data/metadata/__init__.py +41 -0
- sunstone_data/metadata/builder.py +276 -0
- sunstone_data/metadata/models.py +87 -0
- sunstone_data/metadata/provenance.py +171 -0
- sunstone_data/metadata/provenance_rdf.py +102 -0
- sunstone_data/plugin.py +62 -0
- sunstone_data/publish.py +134 -0
- sunstone_data/query.py +624 -0
- sunstone_data/resolve.py +178 -0
- sunstone_data/url.py +78 -0
- sunstone_data/utils/__init__.py +12 -0
- sunstone_data/utils/checksum.py +14 -0
- sunstone_data/utils/encoding.py +16 -0
- sunstone_data/utils/naming.py +38 -0
- sunstone_data/versioning/__init__.py +17 -0
- sunstone_data/versioning/schema_evolution.py +180 -0
- sunstone_data_platform-1.0.0.dist-info/METADATA +302 -0
- sunstone_data_platform-1.0.0.dist-info/RECORD +66 -0
- sunstone_data_platform-1.0.0.dist-info/WHEEL +4 -0
- sunstone_data_platform-1.0.0.dist-info/entry_points.txt +2 -0
- sunstone_pipeline/__init__.py +1 -0
- sunstone_pipeline/assets/__init__.py +11 -0
- sunstone_pipeline/assets/mirrors.py +123 -0
- sunstone_pipeline/assets/transforms.py +74 -0
- sunstone_pipeline/definitions.py +25 -0
- sunstone_pipeline/io_manager.py +56 -0
- sunstone_pipeline/resources/__init__.py +6 -0
- sunstone_pipeline/resources/iceberg.py +29 -0
- sunstone_pipeline/resources/refs.py +88 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
"""CSV and Excel parsing to PyArrow, with Iceberg table creation."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
import fastexcel
|
|
9
|
+
import pyarrow as pa
|
|
10
|
+
import pyarrow.csv as pa_csv
|
|
11
|
+
from pyiceberg.exceptions import BadRequestError
|
|
12
|
+
from pyiceberg.io.fsspec import FsspecFileIO
|
|
13
|
+
from pyiceberg.table import Table
|
|
14
|
+
|
|
15
|
+
from sunstone_data.catalog import create_namespace_if_not_exists, get_catalog
|
|
16
|
+
from sunstone_data.config import Settings, get_settings
|
|
17
|
+
from sunstone_data.import_.fetcher import FileType, SourceInfo
|
|
18
|
+
from sunstone_data.import_.pipeline import ImportResult
|
|
19
|
+
from sunstone_data.metadata.provenance import (
|
|
20
|
+
make_activity_uri,
|
|
21
|
+
make_file_uri,
|
|
22
|
+
make_table_uri,
|
|
23
|
+
make_url_uri,
|
|
24
|
+
)
|
|
25
|
+
from sunstone_data.utils import sanitize_table_name
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from sunstone_data.metadata import MetadataBuilder
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _fix_table_io_endpoint(table: Table, settings: Settings) -> Table:
|
|
32
|
+
"""Fix table IO endpoint for local development.
|
|
33
|
+
|
|
34
|
+
When running PyIceberg on the host against Nessie in Docker, Nessie vends
|
|
35
|
+
the internal Docker endpoint. This function replaces the table's IO with
|
|
36
|
+
one using the host-accessible endpoint.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
table: PyIceberg Table object loaded from REST catalog.
|
|
40
|
+
settings: Platform settings with correct endpoint.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The same table with IO endpoint fixed.
|
|
44
|
+
"""
|
|
45
|
+
props = dict(table.io.properties)
|
|
46
|
+
changed = False
|
|
47
|
+
|
|
48
|
+
if settings.storage_backend == "gcs":
|
|
49
|
+
# Fix fake-gcs-server internal endpoint
|
|
50
|
+
gcs_host = props.get("gcs.service.host", "")
|
|
51
|
+
if "fake-gcs-server" in gcs_host and settings.gcs_endpoint:
|
|
52
|
+
props["gcs.service.host"] = settings.gcs_endpoint
|
|
53
|
+
changed = True
|
|
54
|
+
else:
|
|
55
|
+
# Fix the object store's internal endpoint
|
|
56
|
+
s3_endpoint = props.get("s3.endpoint", "")
|
|
57
|
+
if "object-store:9000" in s3_endpoint:
|
|
58
|
+
props["s3.endpoint"] = settings.s3_endpoint
|
|
59
|
+
changed = True
|
|
60
|
+
|
|
61
|
+
if changed:
|
|
62
|
+
new_io = FsspecFileIO(properties=props)
|
|
63
|
+
object.__setattr__(table, "io", new_io)
|
|
64
|
+
|
|
65
|
+
return table
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def parse_csv(file_path: Path, encoding: str | None = None) -> pa.Table:
|
|
69
|
+
"""Parse CSV file to PyArrow table with streaming.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
file_path: Path to CSV file.
|
|
73
|
+
encoding: File encoding (auto-detect if None).
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
PyArrow Table with parsed data.
|
|
77
|
+
"""
|
|
78
|
+
read_options = pa_csv.ReadOptions(
|
|
79
|
+
encoding=encoding or "utf-8",
|
|
80
|
+
block_size=64 * 1024 * 1024, # 64MB chunks for type inference
|
|
81
|
+
)
|
|
82
|
+
parse_options = pa_csv.ParseOptions()
|
|
83
|
+
convert_options = pa_csv.ConvertOptions()
|
|
84
|
+
|
|
85
|
+
# Use streaming reader
|
|
86
|
+
reader = pa_csv.open_csv(
|
|
87
|
+
file_path,
|
|
88
|
+
read_options=read_options,
|
|
89
|
+
parse_options=parse_options,
|
|
90
|
+
convert_options=convert_options,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return reader.read_all()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def parse_excel(file_path: Path) -> dict[str, pa.Table]:
|
|
97
|
+
"""Parse Excel file, returning dict of sheet_name -> PyArrow table.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
file_path: Path to Excel file.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Dict mapping sheet names to PyArrow tables.
|
|
104
|
+
"""
|
|
105
|
+
excel_file = fastexcel.read_excel(file_path)
|
|
106
|
+
sheet_names = excel_file.sheet_names
|
|
107
|
+
|
|
108
|
+
tables = {}
|
|
109
|
+
for sheet_name in sheet_names:
|
|
110
|
+
sheet = excel_file.load_sheet_by_name(sheet_name)
|
|
111
|
+
arrow_data = sheet.to_arrow()
|
|
112
|
+
|
|
113
|
+
# fastexcel may return RecordBatch - convert to Table
|
|
114
|
+
if isinstance(arrow_data, pa.RecordBatch):
|
|
115
|
+
arrow_table = pa.Table.from_batches([arrow_data])
|
|
116
|
+
else:
|
|
117
|
+
arrow_table = arrow_data
|
|
118
|
+
|
|
119
|
+
# Skip empty sheets
|
|
120
|
+
if arrow_table.num_rows > 0:
|
|
121
|
+
tables[sheet_name] = arrow_table
|
|
122
|
+
|
|
123
|
+
return tables
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def create_iceberg_table(
|
|
127
|
+
namespace: str,
|
|
128
|
+
table_name: str,
|
|
129
|
+
arrow_table: pa.Table,
|
|
130
|
+
properties: dict[str, str],
|
|
131
|
+
branch: str = "main",
|
|
132
|
+
force_schema: bool = False,
|
|
133
|
+
settings: Settings | None = None,
|
|
134
|
+
builder: "MetadataBuilder | None" = None,
|
|
135
|
+
) -> tuple[int | None, bool]:
|
|
136
|
+
"""Create Iceberg table from Arrow table with metadata.
|
|
137
|
+
|
|
138
|
+
If table exists with same checksum, returns without modification.
|
|
139
|
+
If table exists with different checksum, appends as new snapshot.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
namespace: Target namespace.
|
|
143
|
+
table_name: Target table name.
|
|
144
|
+
arrow_table: Data to write.
|
|
145
|
+
properties: Metadata properties to set.
|
|
146
|
+
branch: Branch to import to (default: "main").
|
|
147
|
+
force_schema: Allow unsafe schema changes (default: False).
|
|
148
|
+
settings: Optional settings override.
|
|
149
|
+
builder: Optional MetadataBuilder. On a same-checksum no-op its
|
|
150
|
+
``activity_id`` is reset to the one the original committing import
|
|
151
|
+
recorded on the table, so a retry does not mint a second activity
|
|
152
|
+
for a snapshot that never changed (DP-27).
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
``(snapshot_id, already_existed)``. The snapshot id is the table's
|
|
156
|
+
current one after the operation — for an unchanged (same-checksum)
|
|
157
|
+
table, the existing one; None only if the table somehow carries no
|
|
158
|
+
snapshot. ``already_existed`` is True only when the same-checksum
|
|
159
|
+
short-circuit fired and nothing was written.
|
|
160
|
+
|
|
161
|
+
Raises:
|
|
162
|
+
ValueError: The schema is not flat, or an unsafe schema change was
|
|
163
|
+
detected without ``force_schema``.
|
|
164
|
+
"""
|
|
165
|
+
settings = settings or get_settings()
|
|
166
|
+
catalog = get_catalog(branch=branch)
|
|
167
|
+
|
|
168
|
+
# Ensure namespace exists
|
|
169
|
+
create_namespace_if_not_exists(catalog, namespace)
|
|
170
|
+
|
|
171
|
+
identifier = f"{namespace}.{table_name}"
|
|
172
|
+
|
|
173
|
+
# Check if table exists
|
|
174
|
+
existing_tables = [t for _, t in catalog.list_tables(namespace)]
|
|
175
|
+
|
|
176
|
+
if table_name in existing_tables:
|
|
177
|
+
# Table exists - check checksum
|
|
178
|
+
table = catalog.load_table(identifier)
|
|
179
|
+
existing_checksum = table.metadata.properties.get("sunstone.import.checksum")
|
|
180
|
+
|
|
181
|
+
if existing_checksum == properties.get("sunstone.import.checksum"):
|
|
182
|
+
# Same checksum - already imported, skip. Adopt the committing
|
|
183
|
+
# import's activity id so provenance for this unchanged snapshot
|
|
184
|
+
# keeps pointing at the activity that actually produced it.
|
|
185
|
+
if builder is not None:
|
|
186
|
+
committed_activity_id = table.metadata.properties.get("sunstone.import.activity_id")
|
|
187
|
+
if committed_activity_id:
|
|
188
|
+
builder.activity_id = committed_activity_id
|
|
189
|
+
return table.metadata.current_snapshot_id, True
|
|
190
|
+
|
|
191
|
+
# Fix IO endpoint for local development (the catalog vends object-store:9000)
|
|
192
|
+
table = _fix_table_io_endpoint(table, settings)
|
|
193
|
+
|
|
194
|
+
# Check for schema changes
|
|
195
|
+
existing_schema = table.schema().as_arrow()
|
|
196
|
+
new_schema = arrow_table.schema
|
|
197
|
+
|
|
198
|
+
if not existing_schema.equals(new_schema):
|
|
199
|
+
# Schema differs - validate evolution
|
|
200
|
+
from sunstone_data.versioning.schema_evolution import (
|
|
201
|
+
detect_schema_changes,
|
|
202
|
+
validate_schema_evolution,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
changes = detect_schema_changes(existing_schema, new_schema)
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
validate_schema_evolution(changes, force=force_schema)
|
|
209
|
+
except ValueError as e:
|
|
210
|
+
# Unsafe schema changes detected
|
|
211
|
+
raise ValueError(
|
|
212
|
+
f"Schema change detected that could cause data loss. {e}\nUse --force-schema to override."
|
|
213
|
+
) from e
|
|
214
|
+
|
|
215
|
+
# Different checksum - append as new snapshot (version)
|
|
216
|
+
table.append(arrow_table)
|
|
217
|
+
|
|
218
|
+
# Update properties (a property-only commit adds no snapshot)
|
|
219
|
+
with table.transaction() as txn:
|
|
220
|
+
for key, value in properties.items():
|
|
221
|
+
txn.set_properties({key: value})
|
|
222
|
+
return table.metadata.current_snapshot_id, False
|
|
223
|
+
|
|
224
|
+
# Create new table. The catalog enforces the flat-schema rule server-side.
|
|
225
|
+
try:
|
|
226
|
+
table = catalog.create_table(
|
|
227
|
+
identifier=identifier,
|
|
228
|
+
schema=arrow_table.schema,
|
|
229
|
+
properties=properties,
|
|
230
|
+
)
|
|
231
|
+
except BadRequestError as e:
|
|
232
|
+
raise ValueError(
|
|
233
|
+
f"Cannot create table {identifier!r}: Iceberg tables must have a flat schema "
|
|
234
|
+
f"(no struct, list, or map columns). {e}"
|
|
235
|
+
) from e
|
|
236
|
+
|
|
237
|
+
# Fix IO endpoint for local development (the catalog vends object-store:9000)
|
|
238
|
+
table = _fix_table_io_endpoint(table, settings)
|
|
239
|
+
|
|
240
|
+
table.append(arrow_table)
|
|
241
|
+
return table.metadata.current_snapshot_id, False
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def schema_summary(arrow_table: pa.Table) -> str:
|
|
245
|
+
"""Create human-readable schema summary.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
arrow_table: PyArrow table.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
String like "col1 (int64), col2 (string), col3 (float64)"
|
|
252
|
+
"""
|
|
253
|
+
parts = []
|
|
254
|
+
for field in arrow_table.schema:
|
|
255
|
+
parts.append(f"{field.name} ({field.type})")
|
|
256
|
+
return ", ".join(parts[:5]) + ("..." if len(parts) > 5 else "")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _extract_schema_columns(arrow_table: pa.Table) -> list[dict]:
|
|
260
|
+
"""Extract column metadata from PyArrow schema.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
arrow_table: PyArrow table.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
List of {name: str, type: str} for each column.
|
|
267
|
+
"""
|
|
268
|
+
return [{"name": field.name, "type": str(field.type)} for field in arrow_table.schema]
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _build_extended_properties(
|
|
272
|
+
base_properties: dict[str, str],
|
|
273
|
+
arrow_table: pa.Table,
|
|
274
|
+
namespace: str,
|
|
275
|
+
table_name: str,
|
|
276
|
+
builder: "MetadataBuilder | None",
|
|
277
|
+
) -> dict[str, str]:
|
|
278
|
+
"""Build extended Iceberg properties including PROV-DM URIs.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
base_properties: Base properties dict to extend.
|
|
282
|
+
arrow_table: Parsed PyArrow table.
|
|
283
|
+
namespace: Target namespace.
|
|
284
|
+
table_name: Target table name.
|
|
285
|
+
builder: Optional MetadataBuilder with source info.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Extended properties dict including sunstone.prov.* fields.
|
|
289
|
+
"""
|
|
290
|
+
properties = dict(base_properties)
|
|
291
|
+
|
|
292
|
+
if builder is None:
|
|
293
|
+
return properties
|
|
294
|
+
|
|
295
|
+
# Schema columns (same for original and iceberg in direct import)
|
|
296
|
+
columns = _extract_schema_columns(arrow_table)
|
|
297
|
+
|
|
298
|
+
# Extended sunstone.import.* properties
|
|
299
|
+
properties.update(
|
|
300
|
+
{
|
|
301
|
+
"sunstone.import.activity_id": builder.activity_id,
|
|
302
|
+
"sunstone.import.user": builder.user,
|
|
303
|
+
"sunstone.import.system_version": builder.system_version,
|
|
304
|
+
"sunstone.import.row_count": str(arrow_table.num_rows),
|
|
305
|
+
"sunstone.import.column_count": str(arrow_table.num_columns),
|
|
306
|
+
"sunstone.import.schema_original": json.dumps(columns),
|
|
307
|
+
"sunstone.import.schema_iceberg": json.dumps(columns),
|
|
308
|
+
}
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Add source_normalized and file_size if source info is available
|
|
312
|
+
if builder.source_info is not None:
|
|
313
|
+
properties["sunstone.import.source_normalized"] = builder.source_info.normalized_path
|
|
314
|
+
properties["sunstone.import.file_size"] = str(builder.source_info.file_size)
|
|
315
|
+
|
|
316
|
+
# Determine source entity URI
|
|
317
|
+
original_path = builder.source_info.original_path
|
|
318
|
+
if original_path.startswith(("http://", "https://")):
|
|
319
|
+
source_uri = make_url_uri(original_path)
|
|
320
|
+
else:
|
|
321
|
+
source_uri = make_file_uri(builder.source_info.normalized_path)
|
|
322
|
+
|
|
323
|
+
# PROV-DM properties
|
|
324
|
+
properties.update(
|
|
325
|
+
{
|
|
326
|
+
"sunstone.prov.activity_uri": make_activity_uri(builder.activity_id),
|
|
327
|
+
"sunstone.prov.source_entity_uri": source_uri,
|
|
328
|
+
"sunstone.prov.table_entity_uri": make_table_uri(namespace, table_name),
|
|
329
|
+
"sunstone.prov.user_agent_uri": f"urn:user:{builder.user}",
|
|
330
|
+
"sunstone.prov.system_agent_uri": f"urn:system:sunstone-data:{builder.system_version}",
|
|
331
|
+
}
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
return properties
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def parse_and_create_tables(
|
|
338
|
+
source_info: SourceInfo,
|
|
339
|
+
base_name: str,
|
|
340
|
+
namespace: str,
|
|
341
|
+
checksum: str,
|
|
342
|
+
source: str,
|
|
343
|
+
tags: list[str] | None = None,
|
|
344
|
+
branch: str = "main",
|
|
345
|
+
force_schema: bool = False,
|
|
346
|
+
settings: Settings | None = None,
|
|
347
|
+
builder: "MetadataBuilder | None" = None,
|
|
348
|
+
owner_org: str | None = None,
|
|
349
|
+
) -> list[ImportResult]:
|
|
350
|
+
"""Parse file and create Iceberg tables.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
source_info: Source information from fetcher.
|
|
354
|
+
base_name: Base table name.
|
|
355
|
+
namespace: Target namespace.
|
|
356
|
+
checksum: File checksum.
|
|
357
|
+
source: Original source path/URL.
|
|
358
|
+
tags: Optional tags.
|
|
359
|
+
branch: Branch to import to (default: "main").
|
|
360
|
+
force_schema: Allow unsafe schema changes (default: False).
|
|
361
|
+
settings: Optional settings override.
|
|
362
|
+
builder: Optional MetadataBuilder for extended metadata capture.
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
List of ImportResult (one for CSV, multiple for Excel sheets).
|
|
366
|
+
"""
|
|
367
|
+
settings = settings or get_settings()
|
|
368
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
369
|
+
|
|
370
|
+
# Build base properties
|
|
371
|
+
base_properties = {
|
|
372
|
+
"sunstone.import.checksum": checksum,
|
|
373
|
+
"sunstone.import.source": source,
|
|
374
|
+
"sunstone.import.timestamp": now,
|
|
375
|
+
"sunstone.import.format": source_info.file_type.value,
|
|
376
|
+
}
|
|
377
|
+
if source_info.encoding:
|
|
378
|
+
base_properties["sunstone.import.encoding"] = source_info.encoding
|
|
379
|
+
if tags:
|
|
380
|
+
base_properties["sunstone.import.tags"] = ",".join(tags)
|
|
381
|
+
if owner_org:
|
|
382
|
+
base_properties["sunstone.owner.org"] = owner_org
|
|
383
|
+
|
|
384
|
+
results = []
|
|
385
|
+
|
|
386
|
+
if source_info.file_type == FileType.CSV:
|
|
387
|
+
# Parse CSV
|
|
388
|
+
arrow_table = parse_csv(source_info.local_path, source_info.encoding)
|
|
389
|
+
|
|
390
|
+
# Extract schema columns for metadata
|
|
391
|
+
columns = _extract_schema_columns(arrow_table)
|
|
392
|
+
|
|
393
|
+
# Capture schema info in builder
|
|
394
|
+
if builder is not None:
|
|
395
|
+
builder.set_schema_info(
|
|
396
|
+
original_columns=columns,
|
|
397
|
+
iceberg_columns=columns,
|
|
398
|
+
row_count=arrow_table.num_rows,
|
|
399
|
+
column_count=arrow_table.num_columns,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
# Build properties with extended metadata
|
|
403
|
+
properties = {
|
|
404
|
+
**base_properties,
|
|
405
|
+
"sunstone.import.original_filename": source_info.filename,
|
|
406
|
+
}
|
|
407
|
+
properties = _build_extended_properties(properties, arrow_table, namespace, base_name, builder)
|
|
408
|
+
|
|
409
|
+
snapshot_id, already_existed = create_iceberg_table(
|
|
410
|
+
namespace=namespace,
|
|
411
|
+
table_name=base_name,
|
|
412
|
+
arrow_table=arrow_table,
|
|
413
|
+
properties=properties,
|
|
414
|
+
branch=branch,
|
|
415
|
+
force_schema=force_schema,
|
|
416
|
+
settings=settings,
|
|
417
|
+
builder=builder,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
results.append(
|
|
421
|
+
ImportResult(
|
|
422
|
+
table_name=base_name,
|
|
423
|
+
namespace=namespace,
|
|
424
|
+
row_count=arrow_table.num_rows,
|
|
425
|
+
column_count=arrow_table.num_columns,
|
|
426
|
+
checksum=checksum,
|
|
427
|
+
original_path="", # Will be filled by pipeline
|
|
428
|
+
schema_summary=schema_summary(arrow_table),
|
|
429
|
+
already_existed=already_existed,
|
|
430
|
+
snapshot_id=snapshot_id,
|
|
431
|
+
)
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
elif source_info.file_type == FileType.EXCEL:
|
|
435
|
+
# Parse Excel - each sheet becomes a table
|
|
436
|
+
sheets = parse_excel(source_info.local_path)
|
|
437
|
+
|
|
438
|
+
# Track if this is first sheet (for schema metadata)
|
|
439
|
+
first_sheet = True
|
|
440
|
+
|
|
441
|
+
for sheet_name, arrow_table in sheets.items():
|
|
442
|
+
# Sanitize sheet name for table suffix
|
|
443
|
+
safe_sheet = sanitize_table_name(sheet_name)
|
|
444
|
+
table_name = f"{base_name}_{safe_sheet}" if len(sheets) > 1 else base_name
|
|
445
|
+
|
|
446
|
+
# Extract schema columns
|
|
447
|
+
columns = _extract_schema_columns(arrow_table)
|
|
448
|
+
|
|
449
|
+
# Capture schema info from first sheet (representative)
|
|
450
|
+
if builder is not None and first_sheet:
|
|
451
|
+
builder.set_schema_info(
|
|
452
|
+
original_columns=columns,
|
|
453
|
+
iceberg_columns=columns,
|
|
454
|
+
row_count=arrow_table.num_rows,
|
|
455
|
+
column_count=arrow_table.num_columns,
|
|
456
|
+
)
|
|
457
|
+
first_sheet = False
|
|
458
|
+
|
|
459
|
+
# Build properties with extended metadata
|
|
460
|
+
properties = {
|
|
461
|
+
**base_properties,
|
|
462
|
+
"sunstone.import.original_filename": source_info.filename,
|
|
463
|
+
"sunstone.import.sheet_name": sheet_name,
|
|
464
|
+
}
|
|
465
|
+
properties = _build_extended_properties(properties, arrow_table, namespace, table_name, builder)
|
|
466
|
+
|
|
467
|
+
snapshot_id, already_existed = create_iceberg_table(
|
|
468
|
+
namespace=namespace,
|
|
469
|
+
table_name=table_name,
|
|
470
|
+
arrow_table=arrow_table,
|
|
471
|
+
properties=properties,
|
|
472
|
+
branch=branch,
|
|
473
|
+
force_schema=force_schema,
|
|
474
|
+
settings=settings,
|
|
475
|
+
builder=builder,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
results.append(
|
|
479
|
+
ImportResult(
|
|
480
|
+
table_name=table_name,
|
|
481
|
+
namespace=namespace,
|
|
482
|
+
row_count=arrow_table.num_rows,
|
|
483
|
+
column_count=arrow_table.num_columns,
|
|
484
|
+
checksum=checksum,
|
|
485
|
+
original_path="", # Will be filled by pipeline
|
|
486
|
+
schema_summary=schema_summary(arrow_table),
|
|
487
|
+
already_existed=already_existed,
|
|
488
|
+
snapshot_id=snapshot_id,
|
|
489
|
+
)
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
return results
|