Data¶
The data resource manages all table data operations. Every write (upload, update) is processed by a background job on the server — ineepy handles the polling for you by default, or you can manage job IDs yourself for more control.
Methods¶
| Method | Returns | Description |
|---|---|---|
info(table_id) |
DatasetInfoResponse |
Table statistics, no data scan |
upload(table_id, data, format) |
DataJobResponse |
Initial load, block until complete |
submit_upload(table_id, data, format) |
str |
Initial load, return job ID immediately |
append(table_id, data, format) |
DataJobResponse |
Append rows, block until complete |
submit_append(table_id, data, format) |
str |
Append rows, return job ID immediately |
overwrite(table_id, data, format) |
DataJobResponse |
Overwrite all rows, block until complete |
submit_overwrite(table_id, data, format) |
str |
Overwrite all rows, return job ID immediately |
upsert(table_id, data, format, key_columns) |
DataJobResponse |
Upsert rows by key columns, block until complete |
submit_upsert(table_id, data, format, key_columns) |
str |
Upsert rows, return job ID immediately |
update(table_id, data, format, update_mode, ...) |
DataJobResponse |
Update data (generic), block until complete |
submit_update(table_id, data, format, update_mode, ...) |
str |
Update data (generic), return job ID immediately |
query(table_id, *, filters, format, limit, offset) |
QueryResponse or bytes |
Query a single table; JSON is sync, parquet/csv are async |
submit_query(table_id, *, filters, format) |
str |
Submit single-table async query, return job ID |
cross_query(request) |
bytes |
Cross-table async query, block until bytes ready |
submit_cross_query(request) |
str |
Submit cross-table async query, return job ID |
poll_download(job_id) |
bytes |
Poll a query job and download result |
poll_download_to(job_id, path) |
Path |
Poll a query job and stream result to a file |
get_job(job_id) |
DataJobResponse |
Get current job status |
delete_rows(table_id, filters) |
None |
Delete rows matching filters |
truncate(table_id) |
None |
Delete all rows, preserve schema |
Dataset info¶
info() returns snapshot statistics without scanning the full table.
stats = client.data.info(table_id='raw.events.log')
print(f'Rows: {stats.row_count}')
print(f'Size: {stats.total_size_bytes} bytes')
Fields available on DatasetInfoResponse: row_count, total_size_bytes, data_files, last_updated, snapshot_id.
Uploading data¶
upload() and submit_upload() perform the initial load of a
table: the server rejects them with a ConflictError (409) once the
table has ever ingested data — even after a
truncate — so every subsequent write goes through
append, upsert, or overwrite.
Blocks until the server finishes ingesting the data:
Returns a job ID immediately after the data reaches the server. Use this when you want to submit multiple uploads in parallel or integrate with your own job-tracking logic:
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 patterns around managing job IDs.
Querying data¶
Returns a QueryResponse with .items (list of row dicts)
and .metadata (pagination info including total_count and has_next):
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)
Use limit and offset to paginate (server maximum: 10 000 rows per page):
For binary formats, the server processes the query asynchronously via
POST /data/queries. submit_query returns the job ID; poll_download
polls GET /data/query/{job_id} until the server returns a 307 redirect
to the presigned result file, then downloads and returns the raw bytes:
Conditional queries (caching)¶
Every format='json' response includes an ETag tied to the table's current
snapshot. Pass it back as if_none_match on subsequent calls — the server
returns 304 Not Modified and query() returns None when the data has not
changed, letting you skip re-processing and avoid unnecessary transfer:
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')
When the data has changed, the response is a fresh
QueryResponse — read result.etag again to update your cached
ETag.
etag is '' when the table has no snapshot yet (no data has been ingested).
if_none_match is ignored for format='parquet' and format='csv'.
Filtering results¶
Filters use field:operator:value format and are combined with AND semantics:
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}')
Common operators: eq, ne, lt, lte, gt, gte.
Cross-table queries¶
cross_query() and submit_cross_query() use the POST /data/queries
endpoint to run async queries across multiple tables with joins and nested
AND/OR filter predicates. Results are always returned as a file (parquet, csv,
or json) via a presigned S3 URL.
from ineepy import CrossQueryRequest, FilterGroup, FilterLeaf, JoinSpec
req = CrossQueryRequest(
table_id='raw.sales.orders',
filters=FilterGroup(
type='and',
predicates=[
FilterLeaf(field='amount', operator='gt', value=50),
FilterLeaf(field='status', operator='eq', value='active'),
],
),
joins=[
JoinSpec(
table_id='raw.customers',
how='inner',
left_on='customer_id',
right_on='id',
)
],
select=['order_id', 'name', 'amount'],
format='parquet',
)
# Blocking — waits until the result is ready and returns bytes
parquet_bytes = client.data.cross_query(req)
# Non-blocking — returns the job ID immediately
job_id = client.data.submit_cross_query(req)
parquet_bytes = client.data.poll_download(job_id)
FilterLeaf holds a single condition. FilterGroup combines two or more
predicates with type='and' or type='or' and requires at least two
predicates. Use FilterLeaf directly for a single condition.
Supported JoinSpec.how values: inner, left, right, full, cross,
semi, anti.
Updating data¶
Three explicit write methods cover the most common modes.
Append¶
Inserts new rows without deduplication — existing rows are untouched:
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 that match all key_columns; 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¶
Deletes all existing rows and replaces them with the new dataset:
job = client.data.overwrite(
table_id='raw.events.log',
data=json.dumps(fresh_data).encode(),
format='json',
)
print(f'Overwritten: {job.status}')
Non-blocking writes¶
Every write method has a submit_* counterpart that returns a job ID immediately
instead of polling. Use this when you want to submit multiple operations in
parallel or integrate with your own job-tracking logic:
job_id = client.data.submit_append(
table_id='raw.events.log',
data=json.dumps(extra_update).encode(),
format='json',
)
print(f'Update submitted: {job_id}')
while True:
job = client.data.get_job(job_id)
if job.status == DataJobStatus.COMPLETED:
print('Update complete')
break
if job.status == DataJobStatus.FAILED:
raise RuntimeError(f'Update failed: {job.error}')
time.sleep(2)
Generic update() / submit_update()¶
The generic method is available when the mode is determined at runtime:
client.data.update(
table_id='raw.events.log',
data=data,
format='csv',
update_mode=mode, # 'append', 'overwrite', or 'upsert'
key_columns=keys, # required when mode == 'upsert'
)
Deleting rows¶
Deletes all rows matching every filter (AND semantics). At least one filter is required:
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. Truncation is processed
asynchronously — poll info() until the table reports empty before
writing again. A truncated table is not eligible for upload()
again; reload it with append() or overwrite():
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}')
Streaming large transfers¶
Uploads and downloads can stream data instead of buffering entire payloads in memory, so transfers are bounded by disk, not RAM.
Uploads¶
Every write method's data parameter accepts, on the sync client:
bytes— simplest, for payloads that already fit in memory.-
A file-like object (e.g. an
open(..., 'rb')handle) — the file is streamed to storage; its size is announced viaContent-Length, which every S3-compatible endpoint accepts. Prefer this for large files: -
An iterable of
byteschunks — sent with chunked transfer encoding, because the total size is unknown up front. Some S3-compatible endpoints reject chunked uploads (HTTP411 Length Required) — prefer a file-like object when the content lives on disk.
The async client accepts bytes or an async-iterable of bytes
(the same chunked-transfer caveat applies to async iterables). Note
the asymmetry: sync accepts file-likes and plain iterables; async
accepts async-iterables.
Downloads¶
poll_download() buffers the whole result in memory and returns
bytes. For large results, poll_download_to() streams the file
straight to disk and returns the written Path:
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 may leave a partial file at the destination — no atomic temp-file-plus-rename is performed.
Supported formats¶
| Format | query() return type |
Description |
|---|---|---|
'json' |
QueryResponse (synchronous) |
Items + pagination metadata |
'parquet' |
bytes (async job via POST /data/queries) |
Apache Parquet columnar |
'csv' |
bytes (async job via POST /data/queries) |
Comma-separated values |
JSON queries are returned synchronously from GET /tables/{id}/data/query.
Parquet and CSV queries submit an async job via POST /data/queries, then
poll GET /data/query/{job_id} until the server issues a 307 redirect to
the presigned result file.
Update modes¶
| Mode | Behaviour | Requires key_columns |
|---|---|---|
'append' |
Add rows; existing data untouched | No |
'overwrite' |
Delete all rows then insert new data | No |
'upsert' |
Match rows by key_columns, update or insert |
Yes |