Data pipeline¶
This guide walks a table through its full life cycle: stream an initial load from disk, inspect and query it, keep a cached copy fresh with ETags, apply the three write modes, stream a large result back to disk, and clean up rows.
1. Set up the table¶
ns, _ = client.namespaces.create(
namespace_id='raw.events',
title='Events',
)
schema = [
ColumnSchema(name='user_id', data_type='int64'),
ColumnSchema(name='event_type', data_type='string'),
ColumnSchema(name='recorded_at', data_type='datetime'),
ColumnSchema(name='value', data_type='float64'),
]
table, _ = client.tables.create(
table_id='raw.events.log',
columns=schema,
sort_by=[{'name': 'recorded_at'}],
)
2. Initial load, streamed from disk¶
upload() performs the initial load of a table. data accepts
raw bytes or an open file object — files are streamed to storage
without buffering the whole payload in memory, so this works for
files larger than RAM:
with open('events.csv', 'rb') as f:
job = client.data.upload(
table_id='raw.events.log',
data=f,
format='csv',
)
print(f'Streamed upload from disk: {job.status}')
Note
The server accepts upload() only while the table has never
ingested data; every later write must use
append, upsert, or overwrite. See
Streaming large transfers
for the accepted input types.
3. Inspect and query¶
info() returns snapshot statistics without scanning data:
stats = client.data.info(table_id='raw.events.log')
print(f'Rows: {stats.row_count}')
print(f'Size: {stats.total_size_bytes} bytes')
JSON queries return rows and pagination metadata synchronously:
result = client.data.query(
table_id='raw.events.log',
format='json',
)
print(f'Total events: {result.metadata.total_count}')
for row in result.items:
print(row)
Filters use field:operator:value syntax (a single string or an
iterable of them):
login_events = client.data.query(
table_id='raw.events.log',
filters=['event_type:eq:login'],
format='json',
)
print(f'Login events: {login_events.metadata.total_count}')
4. Cache with ETags¶
Every JSON query carries an ETag tied to the table's current
snapshot. Send it back as if_none_match — when nothing changed, the
server answers 304 and query() returns None, so you skip the
transfer and reuse your cached rows:
result = client.data.query(
table_id='raw.events.log',
format='json',
)
etag = result.etag
cached = client.data.query(
table_id='raw.events.log',
format='json',
if_none_match=etag,
)
if cached is None:
print('Data unchanged — using the cached copy')
5. Apply updates¶
append() inserts rows without deduplication:
job = client.data.append(
table_id='raw.events.log',
data=json.dumps(new_events).encode(),
format='json',
)
print(f'Appended: {job.status}')
upsert() updates rows matching all key_columns and inserts the
rest:
job = client.data.upsert(
table_id='raw.events.log',
data=json.dumps(upsert_data).encode(),
format='json',
key_columns=['user_id', 'event_type'],
)
print(f'Upserted: {job.status}')
overwrite() replaces the entire dataset:
job = client.data.overwrite(
table_id='raw.events.log',
data=json.dumps(fresh_data).encode(),
format='json',
)
print(f'Overwritten: {job.status}')
6. Download large results to disk¶
For parquet or CSV output, submit the query as a job and stream the
result straight to a file — poll_download_to() writes chunks as
they arrive instead of holding the whole result in memory:
job_id = client.data.submit_query(
table_id='raw.events.log',
format='parquet',
)
path = client.data.poll_download_to(job_id, 'events.parquet')
print(f'Saved {path.stat().st_size} bytes to {path}')
Note
A failed download can leave a partial file at the destination — no atomic rename is performed.
7. Delete rows and truncate¶
delete_rows() removes rows matching every filter:
client.data.delete_rows(
table_id='raw.events.log',
filters=['event_type:eq:logout'],
)
print('Deleted logout events')
truncate() removes all rows while preserving the schema. It is
processed asynchronously — poll info() before writing again, and
reload with append() or overwrite() (the table is no longer
eligible for upload()):
client.data.truncate(table_id='raw.events.log')
# Truncation is processed asynchronously — wait until it lands
while client.data.info(table_id='raw.events.log').row_count:
time.sleep(1)
print('Table truncated')
job = client.data.append(
table_id='raw.events.log',
data=json.dumps(fresh_data).encode(),
format='json',
)
print(f'Reloaded: {job.status}')
8. Monitor jobs yourself¶
Every write has a submit_* variant returning a job ID immediately,
which you poll with get_job():
job_id = client.data.submit_upload(
table_id='raw.events.log',
data=json.dumps(extra_events).encode(),
format='json',
)
print(f'Upload submitted: {job_id}')
while True:
job = client.data.get_job(job_id)
if job.status == DataJobStatus.COMPLETED:
print('Upload complete')
break
if job.status == DataJobStatus.FAILED:
raise RuntimeError(f'Upload failed: {job.error}')
time.sleep(2)
See Background jobs for concurrent-job patterns.
See also¶
- Data resource reference
dataAPI reference- Create a table — schema design for the table you load