Skip to content

Changelog

All notable changes to this project will be documented in this file.

The format follows Keep a Changelog, and this project adheres to Semantic Versioning.

Unreleased

[0.4.1] - 2026-07-13

Changed

  • Lowered the minimum supported Python version from 3.14 to 3.12. The two except JSONDecodeError, KeyError: clauses in src/ineepy/_auth.py (PEP 758, 3.14-only) are now parenthesized. CI now runs the test suite across 3.12, 3.13, and 3.14; local development continues to use 3.14 (.python-version).

[0.4.0] - 2026-07-10

Added

  • data.poll_download_to(job_id, path) (sync and async) streams a query result directly to disk instead of buffering it fully in memory.
  • data.upload/update/overwrite/upsert/append (and their submit_* counterparts) now accept a file-like object or iterable of bytes in addition to bytes, so large uploads can be streamed from disk without loading the whole payload into memory. Async methods accept bytes or an AsyncIterable[bytes] (httpx cannot send a sync file-like/iterator on an async client).
  • ineepy now exports UserResponse, UsersListResponse, APIKeyResponse, APIKeysListResponse, NamespaceResponse, NamespacesListResponse, TableResponse, TablesListResponse, DataJobResponse, DatasetInfoResponse, and ColumnSchema at the top level. ineepy.models now also exports every other request/response model (NamespaceResponseMinimal, NamespaceMetadata, TableResponseSummary, TableResponseMinimal, QueryJobResponse, PaginationMetadata, Filter, Token, UpdateOptions), so every public model is importable from a public path instead of only ineepy._models.
  • data.query now has precise @overload signatures: format='json' (default) returns QueryResponse | None, format='parquet'|'csv' returns bytes. Runtime behavior is unchanged.

Changed

  • tables.update_schema signature is now (table_id, columns, etag=None); etag is optional and auto-fetched like every other update method.
  • BREAKING: users.create now returns a single UserResponse instead of list[UserResponse].
  • BREAKING: get_token/async_get_token/api_keys.create now always return the access token string. Previously they returned None when save_token=True; now the token is saved to ineepy.toml (as before) AND returned.
  • BREAKING: QueryResponse.items is now typed list[dict[str, Any]] instead of list[Any].
  • Every parameter that previously required a sequence of strings (scopes, tags, sources, key_columns, filters, order_by) now also accepts a single string across all resources — previously a bare string was silently iterated character-by-character (e.g. scopes='editor' became ['e', 'd', 'i', 't', 'o', 'r']).
  • config_file parameters (Ineepy, AsyncIneepy, get_token, async_get_token, api_keys.create) now accept str in addition to Path.
  • api_keys.create(expires_at=...) now accepts an ISO-8601 string in addition to datetime.
  • data.submit_cross_query/data.cross_query now accept a plain dict/mapping in addition to CrossQueryRequest.
  • Async methods now carry their own concise docstrings (visible in IDE hover and in the rendered docs) instead of a runtime __doc__ copy that static tooling (mkdocstrings, IDE hover) couldn't see. This also changes help()/.__doc__ at runtime: async methods now show the short form instead of the full sync text.
  • Filter.value and FilterLeaf.value now share the widened type str | bool | int | float | datetime | None (previously one lacked float, the other datetime); datetime filter values are serialized to ISO-8601 strings in cross-query bodies.

Fixed

  • The underlying httpx client is now closed when the startup health check fails, instead of leaking the connection pool.
  • get_token/async_get_token now use the library-wide 60 s timeout instead of httpx's 5 s default, fixing spurious ReadTimeout on slow auth endpoints.
  • data.poll_download raises a typed JobFailedError when a redirect response is missing its Location header, instead of crashing with a bare KeyError.
  • write_config now escapes quotes, backslashes, and control characters, so saved config values can no longer corrupt ineepy.toml.
  • tables.update_metadata no longer resets title, sensitivity, and refresh_frequency when they are not passed.
  • exceptions.TimeoutError now also subclasses the builtin TimeoutError, so except TimeoutError: using the builtin catches it too.
  • tables._meta_body's type annotations now correctly reflect update_metadata's optional parameters (fixes 6 pyright errors introduced by the update_metadata patch-semantics fix above).
  • Fixed pyright errors in _config.py (settings_customise_sources override signature) and _resources/_base.py (S3 upload seek/tell duck-typing) — make lint now runs pyright in addition to ruff.

Removed

  • Broken ineepy console script (declared entry point had no implementation).
  • NamespaceResponseSummary model (field-identical to NamespaceResponse, unreachable via pydantic's union resolution).

Documentation

  • README rewritten: every example is now copy-paste correct against the current API (the old version called a nonexistent users.me(), named an unread INEEPY_API_VERSION env var, and misstated the default base URL), and it now covers the ineepy.toml flow, data upload/query/streaming, configuration precedence, and typed error handling.
  • The three "WIP" guide stubs are now full guides: Create & use an API key, Create a table, and Data pipeline — all code validated against a live server via make docs.
  • New "Streaming large transfers" section in the data resource page covering file-like uploads, the chunked-transfer caveat for plain iterables, poll_download_to, and the sync/async input-type asymmetry.
  • authentication.md and the Manage namespaces & tables guide were rendering empty code blocks — their snippet scripts had been deleted in an earlier docs consolidation; the scripts are restored and validated.
  • upload/submit_upload docstrings and docs now state that upload is the initial load only (the server rejects it with 409 once the table has ever ingested data, even after truncate), and that truncate is processed asynchronously.

0.3.1 - 2026-06-11

Fixed

  • S3 transfers for data.upload/update/overwrite/upsert/append/ query (parquet/csv) now reuse the configured client with a 600 s transfer timeout instead of a short-lived client with httpx's 5 s default, fixing httpx.WriteTimeout on large payloads. The Authorization header is stripped from these requests since presigned S3 URLs are self-authenticating.
  • Ineepy/AsyncIneepy now configure a 60 s (10 s connect) default timeout on the main API client instead of httpx's 5 s default, fixing spurious httpx.ReadTimeout on slower endpoints such as tables.delete.

0.3.0 - 2026-06-04

Changed

  • data.submit_update / data.update now correctly sends merge_mode in the request body (was update_mode, which the server silently ignored causing every upsert to behave as an overwrite). Callers pass the same update_mode parameter — no API change.
  • data.update(..., update_mode='append') now routes to PATCH /tables/{id}/data as required by ineep_lakehouse ≥ 0.1.14 (the server removed append support from PUT). Transparent to callers.
  • Default client timeout raised from 300 s to 600 s to match the server-side write timeout introduced in ineep_lakehouse v0.1.13.
  • namespaces.list() namespace_id is now optional. Omit it to list root-level namespaces via GET /namespaces; existing calls that pass a namespace ID are unaffected.
  • etag is now optional in namespaces.update(), tables.update_metadata(), tables.update_schema(), and users.update(). When omitted the current ETag is fetched automatically with a HEAD request before the write. Existing callers that supply an explicit ETag continue to work without change.

Added

  • data.submit_overwrite(table_id, data, format) / data.overwrite(table_id, data, format) — explicit overwrite shorthand.
  • data.submit_upsert(table_id, data, format, key_columns) / data.upsert(table_id, data, format, key_columns) — explicit upsert shorthand.
  • data.submit_append(table_id, data, format) / data.append(table_id, data, format) — explicit append via PATCH /tables/{id}/data (non-deduplicating row insert).
  • namespaces.get_etag(namespace_id) — fetch only the ETag via HEAD /namespaces/{id}/metadata (no body transfer).
  • tables.get_etag(table_id) — fetch only the ETag via HEAD /tables/{id}/metadata.
  • users.get_etag(user_id) — fetch only the ETag via HEAD /users/{id}.

0.2.0 - 2026-06-01

Changed

⚠ BREAKING — data.query(format='json') now returns QueryResponse

The sync query endpoint (GET /tables/{table_id}/data/query) now returns a pagination envelope. To expose total row count and has_next to callers, query(format='json') now returns QueryResponse instead of a bare list.

Migration: Replace for row in client.data.query(...) with for row in client.data.query(...).items. Read result.metadata.total_count for the total row count after filtering.

New limit and offset parameters are also available for pagination (max limit=10_000).


  • data.query(format='parquet'|'csv') now routes through POST /data/queries internally. The method signature and bytes return type are unchanged.
  • data.submit_query() now uses POST /data/queries internally. The signature (table_id, *, filters, format) is unchanged; simple field:operator:value filter strings are automatically converted to the structured FilterNode predicate the new endpoint requires.

Added

  • data.submit_cross_query(request: CrossQueryRequest) -> str — submit a full async cross-table query job and return its job ID.
  • data.cross_query(request: CrossQueryRequest) -> bytes — blocking high-level wrapper that submits and polls until bytes are ready.
  • CrossQueryRequest, JoinSpec, FilterLeaf, FilterGroup, FilterNode models (mirroring POST /data/queries server schema).
  • QueryResponse model with .items: list and .metadata: PaginationMetadata.
  • DataJobType.QUERY enum value — jobs created via POST /data/queries return job_type: 'query'; get_job() now accepts these without a validation error.
  • All new models are exported from the top-level ineepy package.
  • data.query(format='json') populates QueryResponse.etag from the server's ETag response header (the table's current snapshot ID). Pass it back as if_none_match=result.etag on a subsequent call to make a conditional request: the method returns None when the server responds with 304 Not Modified, allowing callers to skip re-processing when the data has not changed.

0.1.1 - 2026-05-28

Changed

  • Upload endpoints (submit_upload, submit_update) now expect 202 Accepted with a JSON body (job_id, upload_url) from the server instead of a 307 Temporary Redirect with X-Job-Id / Location headers.
  • Upload format is now sent as a ?format= query parameter instead of an Accept header.
  • Query endpoint (submit_query, query) switched from POST with a JSON body to GET with filters as repeatable ?filters=field:op:value query parameters; format selection still uses Prefer: format=<fmt> (RFC 7240).
  • delete_rows now issues DELETE /tables/{id}/data/rows instead of PUT.
  • POST /namespaces/{id} and POST /tables/{id} now return 201 Created; handled transparently — no caller changes required.

No public API changes — all method signatures, return types, and exceptions are unchanged. These are internal HTTP protocol adaptations to track breaking changes in ineep_lakehouse (unreleased).

0.1.0 - 2026-05-20

Added

  • Initial public release
  • Synchronous (Ineepy) and asynchronous (AsyncIneepy) clients
  • Full support for users, API keys, namespaces, tables, and data resources
  • Token-based authentication with automatic credential persistence
  • Optimistic locking with ETags
  • Async job polling for long-running data operations
  • Comprehensive type hints and Pydantic models