confluent-sql 0.4.2__tar.gz → 0.5.1__tar.gz
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.
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/.semaphore/semaphore.yml +1 -1
- confluent_sql-0.5.1/CHANGELOG.md +104 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/CLAUDE.md +32 -1
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/DBAPI_EXTENSIONS.md +240 -12
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/PKG-INFO +39 -6
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/README.md +37 -4
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/STREAMING.md +1 -1
- confluent_sql-0.5.1/examples/byoidc_bearer_token_example.py +34 -0
- confluent_sql-0.5.1/examples/statement_properties_example.py +73 -0
- confluent_sql-0.5.1/examples/tableflow_lifecycle_example.py +50 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/pyproject.toml +2 -2
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/src/confluent_sql/__init__.py +60 -1
- confluent_sql-0.5.1/src/confluent_sql/auth.py +30 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/src/confluent_sql/changelog_compressor.py +58 -128
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/src/confluent_sql/connection.py +1073 -99
- confluent_sql-0.5.1/src/confluent_sql/connectors.py +492 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/src/confluent_sql/cursor.py +9 -5
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/src/confluent_sql/exceptions.py +64 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/src/confluent_sql/statement.py +19 -0
- confluent_sql-0.5.1/src/confluent_sql/statement_properties.py +322 -0
- confluent_sql-0.5.1/src/confluent_sql/tableflow.py +363 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/src/confluent_sql/types.py +18 -9
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/tests/conftest.py +8 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/tests/integration/conftest.py +4 -1
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/tests/integration/test_connection.py +2 -2
- confluent_sql-0.5.1/tests/integration/test_connector.py +184 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/tests/integration/test_cursor.py +444 -12
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/tests/integration/test_fetch.py +2 -6
- confluent_sql-0.5.1/tests/integration/test_tableflow.py +152 -0
- confluent_sql-0.5.1/tests/unit/test_auth_unit.py +28 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/tests/unit/test_changelog_compressor_unit.py +335 -330
- confluent_sql-0.5.1/tests/unit/test_connection_byoidc_unit.py +221 -0
- confluent_sql-0.5.1/tests/unit/test_connection_connector_unit.py +184 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/tests/unit/test_connection_unit.py +389 -133
- confluent_sql-0.5.1/tests/unit/test_connection_unit_properties.py +413 -0
- confluent_sql-0.5.1/tests/unit/test_connectors_connection_unit.py +502 -0
- confluent_sql-0.5.1/tests/unit/test_connectors_unit.py +191 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/tests/unit/test_cursor_unit.py +9 -21
- confluent_sql-0.5.1/tests/unit/test_statement_properties_unit.py +396 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/tests/unit/test_statement_unit.py +17 -9
- confluent_sql-0.5.1/tests/unit/test_tableflow_connection_unit.py +525 -0
- confluent_sql-0.5.1/tests/unit/test_tableflow_unit.py +322 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/uv.lock +6 -6
- confluent_sql-0.4.2/CHANGELOG.md +0 -76
- confluent_sql-0.4.2/tests/unit/test_connection_unit_properties.py +0 -200
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/.github/CODEOWNERS +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/.gitignore +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/.semaphore/publish_to_pypi.yml +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/ARCHITECTURE.md +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/LICENSE.txt +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/Makefile +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/TYPES.md +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/examples/errors.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/examples/simple_append_only_streaming_query_example.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/examples/snapshot_mode_tuple_cursor_simple_example.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/service.yml +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/src/confluent_sql/__version__.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/src/confluent_sql/execution_mode.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/src/confluent_sql/polling.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/src/confluent_sql/result_readers.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/src/confluent_sql/retry.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/tests/__init__.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/tests/unit/conftest.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/tests/unit/test_changelog_unit.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/tests/unit/test_execution_mode_unit.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/tests/unit/test_polling_unit.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/tests/unit/test_result_readers_unit.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/tests/unit/test_retry_unit.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.1}/tests/unit/test_types_unit.py +0 -0
|
@@ -31,7 +31,7 @@ blocks:
|
|
|
31
31
|
jobs:
|
|
32
32
|
- name: "Lint, Type Check, and Test"
|
|
33
33
|
commands:
|
|
34
|
-
- uv run --frozen ruff check && uv run --frozen pyright && uv run --frozen pytest -m unit --junitxml=test-results.xml
|
|
34
|
+
- uv run --frozen ruff format --check && uv run --frozen ruff check && uv run --frozen pyright && uv run --frozen pytest -m unit --junitxml=test-results.xml
|
|
35
35
|
epilogue:
|
|
36
36
|
always:
|
|
37
37
|
commands:
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
|
|
3
|
+
All notable changes to this dbapi driver will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## Unreleased
|
|
6
|
+
|
|
7
|
+
## 0.5.1, 2026-08-25
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Streaming changelog compressor fixes for nontrivial changelog streams written to Kafka topics with multiple partitions:
|
|
12
|
+
- The changelog compressor for streaming non-append-only queries **without** upsert columns (`cursor.changelog_compressor()` on a keyless result, e.g. a global aggregation) no longer raises `InterfaceError` on legitimately out-of-order changelog events. When such a changelog is sinked to a multi-partition keyless topic, partitions are assigned by whole-row hash, so an updated row's `+U`/`-D` spelling can land on a different partition than its original `+I`/`-U` spelling; since Kafka only guarantees ordering within a partition, the events can be observed in a surprising order across spellings (a `+U` before its logical `-U`, or a `-D` before a later `+I`). The compressor previously assumed each `UPDATE_BEFORE` was immediately followed by its `UPDATE_AFTER` and failed on those sequences. It now treats the two additive ops (`+I`, `+U`) as inserts and the two retracting ops (`-U`, `-D`) as deletions, makes no ordering assumptions across different rows' spellings, and converges to the correct result set (intermediate snapshots may transiently show an extra row). Adds `Op.treat_as_insert` / `Op.treat_as_delete` helper properties. (#184)
|
|
13
|
+
- The changelog compressor for streaming non-append-only queries **with** upsert columns (`cursor.changelog_compressor()` on a keyed result, e.g. a `GROUP BY`) no longer raises `InterfaceError` when an unrelated key's changelog event arrives between one key's `UPDATE_BEFORE` and its own `UPDATE_AFTER`. A consumer reading a keyed upsert topic drains multiple partitions per poll; same-key events stay ordered (a key always hashes to the same partition), but a single fetch batch can still interleave *different* keys' events. The compressor previously modeled `UPDATE_BEFORE`/`UPDATE_AFTER` as a single-slot pending pair that had to be adjacent, and failed on that interleaving even though nothing was actually wrong. `UPDATE_BEFORE` is now treated as an unconditional no-op (it carries no information the matching insert/update doesn't already supply under key-based upsert semantics), and `UPDATE_AFTER` is handled exactly like `INSERT` (last write for the key wins); `DELETE` is unaffected and still validates that the key exists. As part of the same cleanup, the now-unused pending-update tracking hooks are removed from the shared compressor base class. (#185)
|
|
14
|
+
|
|
15
|
+
## 0.5.0, 2026-08-07
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- `connect()`/`Connection()` accept a `local_time_zone` keyword argument that seeds a read/write `Connection.local_time_zone` property: a connection-level default for the `sql.local-time-zone` statement property, applied to every statement created by the connection unless that statement's own `properties=`/`StatementProperties` sets `local_time_zone`, which takes precedence.
|
|
20
|
+
- `StatementProperties` -- a frozen, keyword-only dataclass giving a typed, autocomplete-friendly way to set the curated statement options instead of hand-building a `sql.*` dict. Fields (all optional): `snapshot_write_mode`, `state_ttl` (a `timedelta`, rendered to a Flink duration such as `"3600 s"`), `scan_startup_mode`, `local_time_zone`, plus an `extra` dict escape hatch for options not yet modeled. Only set fields are emitted, so an instance never pins a default nor collides with the driver-owned overlay. The enum-typed fields also accept a bare `str` so a Flink value newer than this driver can still be passed; a wrong-property enum value, a field of the wrong Python type, or an `extra` key that duplicates a modeled field, raises at construction. `extra` is copied into a read-only mapping, so the frozen guarantee holds after construction too. Pass one anywhere a `properties=` dict is accepted (`Cursor.execute`, `execute_snapshot_ddl`, `execute_streaming_ddl`, ...); it is downgraded to a dict and validated identically. Adds the `ScanStartupMode` value enum (`earliest-offset`/`latest-offset`/`timestamp`/`specific-offsets`). (#163)
|
|
21
|
+
- New module `confluent_sql.statement_properties` gives statement `SET` options a discoverable, type-checkable face alongside the existing open-ended `PropertiesDict`. `Property` is a `str` enum of the `sql.*` option keys from the [SET-options reference](https://docs.confluent.io/cloud/current/flink/reference/statements/set.html) (e.g. `Property.SNAPSHOT_WRITE_MODE`); `SnapshotWriteMode` (`default`/`fast-write`) and `SnapshotMode` (`now`/`off`) enumerate the fixed value sets, sharing the `PropertyValue` base. Members are `str` instances that compare, hash, JSON-serialize, and stringify as their wire string, so they drop straight into a `properties=` dict with no `.value` unwrapping. `PropertiesDict` now advertises `Property` keys and `PropertyValue` values. (#162)
|
|
22
|
+
- class `Connection` now has methods to enable / inspect / disable [Tableflow](https://www.confluent.io/product/tableflow/) materialization of the Kafka topic backing a Flink table (#117). Tableflow-enabled topics/tables can be snapshot queried in an optimized fashion.
|
|
23
|
+
- `Connection.enable_tableflow(table_name, *, tableflow_formats, storage, config=None, wait_for_running=True, timeout=300)` adds an Iceberg/Delta sink. `tableflow_formats` takes a single `TableFormat` (e.g. `TableFormat.ICEBERG`) or a collection for several; `storage` is one of `ManagedStorage()` (zero-config), `ByobAwsStorage`, or `AzureAdlsStorage`; `config` is an optional `TableflowTopicConfig`. By default it blocks until the topic reaches `RUNNING`; pass `wait_for_running=False` to return as soon as the create is accepted (topic in `PENDING`). Raises `TableflowTopicAlreadyExistsError` if Tableflow is already enabled.
|
|
24
|
+
- `Connection.get_tableflow(table_name)` returns the current `TableflowTopic` (phase, spec, status), raising `TableflowTopicNotFoundError` if Tableflow is not enabled for the topic.
|
|
25
|
+
- `Connection.disable_tableflow(table_name, *, wait_for_removal=True, timeout=300)` tears the sink down (all-or-nothing in v1, no per-format disable). By default it blocks until the topic is confirmed gone; pass `wait_for_removal=False` to return as soon as the delete is accepted.
|
|
26
|
+
- `organization_id` is now optional in `connect()`/`Connection()` when a global API key is supplied: if omitted, it's inferred via `GET /org/v2/organizations` -- lazily, on first use of the connection (not at `connect()` time) -- and used when exactly one organization is visible to the key. Raises `OperationalError` on first use if zero or multiple organizations are visible. Unchanged (still required, validated eagerly by `connect()`) for a Flink-region-only key or a dedicated Tableflow/Connect key, neither of which has `/org/v2` reach. (#132)
|
|
27
|
+
- BYOIDC bearer-token authentication for the Flink data plane: `connect()`/`Connection()` now accept an `external_access_token` / `identity_pool_id` pair, letting you authenticate with a bearer token minted by your own OAuth/OIDC identity provider (an "external" token) instead of a Confluent API key + secret. The `external_access_token` name mirrors the Confluent Flink Table API plugin's `client.oauth.external-access-token` option. When supplied, every Flink request carries `Authorization: Bearer <external_access_token>` and `Confluent-Identity-Pool-Id: <identity_pool_id>`. The two are mutually exclusive with every API-key parameter and must be provided together. Scope is the Flink data plane only -- Confluent's authorization model accepts no external token on the control-plane routes this driver calls, so Tableflow, Connectors, and the CMK cluster-id lookup fail closed under BYOIDC (they require an API-key connection), and `organization_id` stays mandatory (there is no control-plane reach to infer it). The token is used verbatim with no refresh; an expired token starts failing requests (surfaced as `OperationalError`) and the connection must be re-opened with a fresh token. (#100)
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- Every network-level transport failure from the Flink gateway (`httpx.ConnectError`, `httpx.ReadError`, `httpx.RemoteProtocolError`, timeouts, etc.) is now translated to `OperationalError` instead of leaking the raw `httpx` exception -- this applies uniformly to idempotent GETs (once #137's retry budget is exhausted) and to non-idempotent POST/PATCH/DELETE calls (statement submission, `stop_statement()`, `delete_statement()`), matching the DB-API v2 contract that every exception the driver raises is one of `confluent_sql`'s own `Error` subclasses. The original exception remains available via `__cause__`. (#138)
|
|
32
|
+
|
|
33
|
+
## 0.4.2, 2026-07-17
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- Snapshot queries on Confluent Cloud Flink SQL are now Generally Available. Removed the Early Access warning previously emitted on first creation of a snapshot-mode cursor, along with the Early Access advisories throughout the documentation (`README.md`, `ARCHITECTURE.md`, `DBAPI_EXTENSIONS.md`, `STREAMING.md`). (#119, #160)
|
|
38
|
+
|
|
39
|
+
## 0.4.1, 2026-07-07
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- Idempotent GET requests (`list_statements()`'s page-fetch loop, `get_statement()`, and result-page fetching) now retry transient transport errors -- connection resets (`httpx.NetworkError`) and servers that close pooled connections without responding (`httpx.RemoteProtocolError`) -- up to 3 times with a short exponential backoff, instead of failing on the first blip. POST/PATCH/DELETE requests (statement submission, `stop_statement()`, `delete_statement()`) are deliberately left unretried, since re-issuing them after a connection reset could double-submit or double-mutate state. (#137)
|
|
44
|
+
- The same idempotent GET requests now also retry transient HTTP response statuses (429, 500, 502, 503, 504), not just transport-level exceptions -- a request that reaches Confluent Cloud but gets a momentarily-overloaded gateway response was previously failing on the first such response instead of being retried like a dropped connection. (#140)
|
|
45
|
+
- Increased the default HTTP timeout from 5 to 10s for safety. `connect()` and `Connection.__init__()` no longer accept explicit `None` for `http_timeout_secs`.
|
|
46
|
+
|
|
47
|
+
## 0.4.0, 2026-06-15
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- Support for "Global" Confluent Cloud API keys: `confluent_sql.connect()` (and `Connection`) now accept `global_api_key` / `global_api_secret`. A Global key works against every route this driver touches, so it is preferred over a Flink Region key when both are supplied. `flink_api_key` / `flink_api_secret` remain supported and are now optional; at least one fully-specified pair must be provided. If both pairs are supplied, the Global pair is used (and the Flink pair ignored, with a warning); a half-specified pair (key without secret, or vice versa) is rejected. (#112)
|
|
52
|
+
- Support for "poolless Flink": `confluent_sql.connect()` now treats `compute_pool_id` as optional. Statements submitted w/o their own overriding `compute_pool_id` via a connection w/o a default compute pool id will make use of the default compute pool in the environment+cloud region (provisioning one if necessary). See [the Confluent documentation](https://docs.confluent.io/cloud/current/flink/concepts/compute-pools.html#default-compute-pools) for more details.
|
|
53
|
+
- New `Connection.stop_statement(statement, *, wait_for_stopped=True, timeout=300)` method to stop a running statement without deleting it, leaving the statement resource around for inspection (unlike `delete_statement()`, which also destroys it). Accepts a statement name or a `Statement` object. By default it blocks until the statement reaches `STOPPED`; pass `wait_for_stopped=False` to return as soon as the stop is accepted. A matching `Cursor.stop_statement()` stops the cursor's current statement. New `Statement.is_stopped`, `Statement.is_stopping`, and `Statement.stop_requested` properties expose the relevant state. (#61)
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
|
|
57
|
+
- `Connection.list_statements()`:
|
|
58
|
+
- New optional parameter `compute_pool_id` to list statements only in a single compute pool (otherwise environment-wide).
|
|
59
|
+
- New optional parameter `name_contains: str` to filter statements server-side to those whose name contains the given substring (case-sensitive).
|
|
60
|
+
- Existing parameter `label` is now optional.
|
|
61
|
+
- The end result is that callers can now provide between zero and all of the possible kwargs to vary between 'no filtering at all, return all current statements in the environment' and 'apply all the possible filters as if ANDed together.'
|
|
62
|
+
|
|
63
|
+
## 0.3.1, 2026-05-21
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
|
|
67
|
+
- New `http_timeout_secs` parameter for `connect()` to let the caller control how long to wait in HTTP requests.
|
|
68
|
+
|
|
69
|
+
## 0.3.0, 2026-04-09
|
|
70
|
+
|
|
71
|
+
### Changed - Breaking
|
|
72
|
+
|
|
73
|
+
- `connect()` / `Connection.__init__()`: Renamed `environment` parameter to `environment_id` to clarify that an environment ID (_not_ name) is expected. The internal attribute `Connection.environment` has also been renamed to `Connection.environment_id`. Update all calls from `connect(environment="env-123")` to `connect(environment_id="env-123")`. (#92)
|
|
74
|
+
- `Cursor.execute()` and peers: Respelled and re-typed the `statement_label: str | None` parameter to be `statement_labels: list[str] | None` to allow multiple labels to be applied to a statement, including `HIDDEN_LABEL`.
|
|
75
|
+
|
|
76
|
+
### Added
|
|
77
|
+
|
|
78
|
+
- New `Connection.get_statement(statement)` method to retrieve statement metadata by name or refresh a Statement object with the latest server state. Accepts either a statement name (string) or a Statement object. Returns a Statement object with current phase, schema, and execution traits. (#86)
|
|
79
|
+
- New `StatementNotFoundError` exception, a subclass of `OperationalError`, raised by `Connection.get_statement(statement)` when attempting to retrieve a statement that does not exist. Provides programmatic access to the statement name via the `statement_name` attribute.
|
|
80
|
+
- New constant `confluent_sql.HIDDEN_LABEL` used for driving `Cursor.execute()` to indicate that the statement should be hidden in default listings in Confluent Cloud UIs. This feature is intended to be used for minor queries, such as when investigating `INFORMATION_SCHEMA`.
|
|
81
|
+
- Added documentation regarding use of `connect(endpoint=)` parameter to make use of private networking endpoints (README.md, docstrings).
|
|
82
|
+
|
|
83
|
+
## 0.2.0, 2026-03-26
|
|
84
|
+
|
|
85
|
+
### Changed
|
|
86
|
+
|
|
87
|
+
- Respelled the `connect()` parameter `dbname` to `database`. The old spelling `dbname` is deprecated and will be removed in after one release cycle.
|
|
88
|
+
- Class `SqlNone` now gracefully strips trailing `NOT NULL` constraints from type names (case-insensitively), so that `str(SqlNone("DATE NOT NULL"))` returns valid FlinkSQL `"cast (null as DATE)"`.
|
|
89
|
+
- `connect()` is now keyword-only callable.
|
|
90
|
+
- The `host` parameter for `Connection.__init__()` has been renamed to `endpoint`.
|
|
91
|
+
- Clarified and improved documentation around Flink region API key use.
|
|
92
|
+
|
|
93
|
+
### Added
|
|
94
|
+
|
|
95
|
+
- New optional keyword parameter `properties: PropertiesDict | None` on `Cursor.execute()` and related methods to allow callers to provide [statement execution properties](https://docs.confluent.io/cloud/current/flink/reference/statements/set.html#table-options). Note: connection or cursor-level properties for default catalog, database, and execution mode cannot be overridden by this parameter.
|
|
96
|
+
- New optional `endpoint` parameter on `connect()` and `Connection.__init__` to allow users to specify a custom Confluent Cloud API base endpoint (e.g., for private networking, staging, etc.). Mutually exclusive with (`cloud_provider`, `cloud_region`) -- either `endpoint` or (`cloud_provider`, `cloud_region`) must be provided. This replaces the `host` parameter in `Connection.__init__()`. (#66)
|
|
97
|
+
|
|
98
|
+
### Removed
|
|
99
|
+
|
|
100
|
+
- The unused control-plane `api_key` and `api_secret` `connect()` parameters have been removed. The Flink Region API key params `flink_api_key` and `flink_api_secret` remain.
|
|
101
|
+
|
|
102
|
+
## 0.1.x
|
|
103
|
+
|
|
104
|
+
Early access release of the driver.
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Repository conventions for Claude
|
|
2
|
+
|
|
3
|
+
Guidance for any Claude working in this repository. Keep it short and concrete; add a convention
|
|
4
|
+
here only once the codebase actually follows it in more than one place.
|
|
2
5
|
|
|
3
6
|
## HTTP request routing: idempotent GETs vs mutating requests
|
|
4
7
|
|
|
@@ -27,3 +30,31 @@ those are equally idempotent GETs even though they weren't the reported failure.
|
|
|
27
30
|
the same call sites to also retry on retryable HTTP status codes, not just transport exceptions.
|
|
28
31
|
The reasoning, and the case for a plain function over a decorator, is captured in `retry.py`'s
|
|
29
32
|
module docstring and `_request_get`'s docstring.
|
|
33
|
+
|
|
34
|
+
## Blocking vs. non-blocking lifecycle methods
|
|
35
|
+
|
|
36
|
+
Methods that drive a server-side resource toward a steady state (start a statement, stop it,
|
|
37
|
+
enable/disable a Tableflow sink, etc.) follow a uniform shape:
|
|
38
|
+
|
|
39
|
+
- **Block by default; return the settled state.** The common call should leave the resource in its
|
|
40
|
+
intended terminal/ready state, not hand back a transient one the caller must then poll itself.
|
|
41
|
+
- **Expose a `wait_for_<settled-state>: bool = True` keyword-only argument.** The suffix names the
|
|
42
|
+
exact condition awaited — *not* a generic `wait`/`block`. Existing names: `wait_for_stopped`
|
|
43
|
+
(statement reaches STOPPED), `wait_for_running` (Tableflow topic reaches RUNNING),
|
|
44
|
+
`wait_for_removal` (Tableflow topic is gone). A new method picks the suffix that is literally
|
|
45
|
+
true of what it waits for.
|
|
46
|
+
- **Pair it with a uniform `timeout` keyword arg** (seconds; the standardized knob across all such
|
|
47
|
+
methods).
|
|
48
|
+
- **`=False` is the opt-out**: return as soon as the request is accepted (resource typically in a
|
|
49
|
+
PENDING/just-requested state).
|
|
50
|
+
- **While waiting, raise on terminal failure and on timeout** — surface the failure detail
|
|
51
|
+
(`OperationalError`, e.g. a statement/topic that went FAILED), don't silently return a broken
|
|
52
|
+
resource.
|
|
53
|
+
- **Implement the wait by polling with `sleep_with_backoff`** from `polling.py`; don't hand-roll a
|
|
54
|
+
sleep loop.
|
|
55
|
+
|
|
56
|
+
Exemplars: `Connection.stop_statement` / `Cursor.stop_statement` (`wait_for_stopped=True`),
|
|
57
|
+
`Connection.enable_tableflow` (`wait_for_running=True`), `Connection.disable_tableflow`
|
|
58
|
+
(`wait_for_removal=True`). The DDL convenience methods (`execute_snapshot_ddl`,
|
|
59
|
+
`execute_streaming_ddl`) block unconditionally and offer no opt-out — that's also acceptable when a
|
|
60
|
+
non-blocking variant would be meaningless.
|
|
@@ -34,6 +34,7 @@ For comprehensive details on streaming queries, polling patterns, and changelog
|
|
|
34
34
|
- [Result Format Extensions](#result-format-extensions) - Dictionary rows, custom types
|
|
35
35
|
- [Streaming Query Support](#streaming-query-support) - Comprehensive streaming guide
|
|
36
36
|
- [Statement Lifecycle Management](#statement-lifecycle-management) - DDL, naming, stopping, deletion
|
|
37
|
+
- [Tableflow Lifecycle](#tableflow-lifecycle) - Enable, read, and disable Iceberg/Delta sinks
|
|
37
38
|
- [Introspection and Metadata](#introspection-and-metadata) - Properties for query state
|
|
38
39
|
- [Performance Monitoring](#performance-monitoring) - Fetch metrics
|
|
39
40
|
- [Type System Extensions](#type-system-extensions) - Flink type support
|
|
@@ -565,6 +566,147 @@ cursor.delete_statement()
|
|
|
565
566
|
|
|
566
567
|
---
|
|
567
568
|
|
|
569
|
+
## Tableflow Lifecycle
|
|
570
|
+
|
|
571
|
+
[Tableflow](https://www.confluent.io/product/tableflow/) materializes the Kafka topic backing a
|
|
572
|
+
Flink table into an Iceberg or Delta table. Three `Connection` methods manage that sink. Enabling
|
|
573
|
+
it also unlocks
|
|
574
|
+
[efficiency gains for snapshot queries](https://docs.confluent.io/cloud/current/flink/concepts/snapshot-queries.html#snapshot-queries-and-tableflow)
|
|
575
|
+
against the table.
|
|
576
|
+
|
|
577
|
+
In Confluent Flink a table is backed by a like-named Kafka topic, so the `table_name` you pass is
|
|
578
|
+
both the Flink table and the topic — no escaping or casing translation.
|
|
579
|
+
|
|
580
|
+
A runnable example covering the full enable/get/disable lifecycle is in
|
|
581
|
+
[examples/tableflow_lifecycle_example.py](examples/tableflow_lifecycle_example.py).
|
|
582
|
+
|
|
583
|
+
> **Not available under BYOIDC.** Tableflow is a control-plane surface, and Confluent's
|
|
584
|
+
> authorization model accepts no BYOIDC bearer token there. A connection authenticated with
|
|
585
|
+
> `external_access_token` / `identity_pool_id` (see the README's [BYOIDC bearer-token
|
|
586
|
+
> authentication](README.md#byoidc-bearer-token-authentication)) fails closed on these methods —
|
|
587
|
+
> use an API-key connection for Tableflow.
|
|
588
|
+
|
|
589
|
+
### Selecting formats: `TableFormat`
|
|
590
|
+
|
|
591
|
+
There is a single format vocabulary, `TableFormat` (`ICEBERG` / `DELTA`), on both the request and
|
|
592
|
+
response sides. A topic can carry **both** formats at once (there is no per-format config), so
|
|
593
|
+
`enable_tableflow`'s `tableflow_formats` argument accepts either a single `TableFormat` for the
|
|
594
|
+
common case or a collection for both:
|
|
595
|
+
|
|
596
|
+
```python
|
|
597
|
+
connection.enable_tableflow("orders", tableflow_formats=TableFormat.ICEBERG, storage=...)
|
|
598
|
+
connection.enable_tableflow(
|
|
599
|
+
"orders", tableflow_formats={TableFormat.ICEBERG, TableFormat.DELTA}, storage=...
|
|
600
|
+
)
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
Responses name the same `TableFormat`s (`topic.spec.table_formats`,
|
|
604
|
+
`topic.status.failing_table_formats`), so checking what you got against what you asked for is a
|
|
605
|
+
plain set comparison:
|
|
606
|
+
|
|
607
|
+
```python
|
|
608
|
+
topic = connection.enable_tableflow(
|
|
609
|
+
"orders", tableflow_formats={TableFormat.ICEBERG, TableFormat.DELTA},
|
|
610
|
+
storage=ManagedStorage(), wait_for_running=True,
|
|
611
|
+
)
|
|
612
|
+
assert set(topic.spec.table_formats) == {TableFormat.ICEBERG, TableFormat.DELTA}
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
### Storage variants
|
|
616
|
+
|
|
617
|
+
`enable_tableflow` requires an explicit, frozen storage spec — no silent default:
|
|
618
|
+
|
|
619
|
+
- `ManagedStorage()` — Confluent-managed bucket, zero config.
|
|
620
|
+
- `ByobAwsStorage(bucket_name=..., provider_integration_id=...)` — bring-your-own AWS S3 bucket.
|
|
621
|
+
- `AzureAdlsStorage(storage_account_name=..., container_name=..., provider_integration_id=...)` —
|
|
622
|
+
customer-owned Azure Data Lake Storage Gen2.
|
|
623
|
+
|
|
624
|
+
### Cluster-id resolution
|
|
625
|
+
|
|
626
|
+
The Tableflow API addresses the cluster by its `lkc-…` id, which the connection must know. Either:
|
|
627
|
+
|
|
628
|
+
- Pass `database_kafka_cluster_id` to `connect()` (works with only a `tableflow_api_key` pair), or
|
|
629
|
+
- Let it resolve lazily from `database` (the cluster name) via CMK on first use — this path
|
|
630
|
+
requires a **global** API key, and the resolved id is cached for the connection's life. A name
|
|
631
|
+
that matches more than one cluster raises, listing the candidate ids so you can disambiguate with
|
|
632
|
+
`database_kafka_cluster_id`.
|
|
633
|
+
|
|
634
|
+
### `enable_tableflow()` — add an Iceberg/Delta sink
|
|
635
|
+
|
|
636
|
+
```python
|
|
637
|
+
from confluent_sql import ManagedStorage, TableFormat, TableflowPhase
|
|
638
|
+
|
|
639
|
+
topic = connection.enable_tableflow(
|
|
640
|
+
"orders",
|
|
641
|
+
tableflow_formats=TableFormat.ICEBERG,
|
|
642
|
+
storage=ManagedStorage(),
|
|
643
|
+
)
|
|
644
|
+
assert topic.phase is TableflowPhase.RUNNING # blocked to RUNNING by default
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
**Behavior notes:**
|
|
648
|
+
|
|
649
|
+
- `tableflow_formats` and `storage` are required (no defaults); `tableflow_formats` must name at
|
|
650
|
+
least one format. `config` is an optional `TableflowTopicConfig` (retention, error-handling)
|
|
651
|
+
shared across all enabled formats.
|
|
652
|
+
- Blocks until `RUNNING` by default (`wait_for_running=True`), raising `OperationalError` on
|
|
653
|
+
`FAILED` (surfacing `status.error_message` and `failing_table_formats`) — consistent with
|
|
654
|
+
`stop_statement`'s wait-by-default. Pass `wait_for_running=False` to return as soon as the create
|
|
655
|
+
is accepted (topic in `PENDING`).
|
|
656
|
+
- Raises `TableflowTopicAlreadyExistsError` if Tableflow is already enabled (HTTP 409), or
|
|
657
|
+
`ProgrammingError` if no management credential is available or the cluster id can't be resolved.
|
|
658
|
+
|
|
659
|
+
### `get_tableflow()` — read current state
|
|
660
|
+
|
|
661
|
+
```python
|
|
662
|
+
topic = connection.get_tableflow("orders")
|
|
663
|
+
print(topic.phase) # TableflowPhase.PENDING / RUNNING / FAILED
|
|
664
|
+
print(topic.spec.table_formats) # [TableFormat.ICEBERG, ...]
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
Raises `TableflowTopicNotFoundError` if Tableflow is not enabled for the topic (HTTP 404). There is
|
|
668
|
+
no separate health check — health is read off `get_tableflow(...).phase`.
|
|
669
|
+
|
|
670
|
+
### `disable_tableflow()` — tear down the sink
|
|
671
|
+
|
|
672
|
+
```python
|
|
673
|
+
connection.disable_tableflow("orders") # blocks until confirmed gone by default
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
**Behavior notes:**
|
|
677
|
+
|
|
678
|
+
- All-or-nothing in v1: removes the entire Tableflow topic. (Removing just one of two enabled
|
|
679
|
+
formats needs a future API and is not yet supported.)
|
|
680
|
+
- Deletion is asynchronous. Blocks until removal is confirmed by default (`wait_for_removal=True`),
|
|
681
|
+
polling `get_tableflow` until it `404`s. This is why a following `DROP TABLE` is safe by default —
|
|
682
|
+
dropping the Flink table drops its backing topic, so Tableflow must be confirmed gone first to
|
|
683
|
+
avoid racing an active materialization. Pass `wait_for_removal=False` to return as soon as the
|
|
684
|
+
`DELETE` is accepted.
|
|
685
|
+
- Raises `TableflowTopicNotFoundError` if Tableflow was not enabled (HTTP 404).
|
|
686
|
+
|
|
687
|
+
### Reusing format and config across many tables
|
|
688
|
+
|
|
689
|
+
Every input is a reusable value — the selection is an enum member, and `storage`/`config` are
|
|
690
|
+
frozen — so hoist them out of the loop. The cluster-id lookup resolves once and is cached, so the
|
|
691
|
+
loop hits CMK at most once:
|
|
692
|
+
|
|
693
|
+
```python
|
|
694
|
+
from confluent_sql import ManagedStorage, TableFormat, TableflowTopicConfig
|
|
695
|
+
|
|
696
|
+
storage = ManagedStorage()
|
|
697
|
+
config = TableflowTopicConfig(retention_ms="604800000")
|
|
698
|
+
|
|
699
|
+
for table in ("orders", "shipments", "returns"):
|
|
700
|
+
connection.enable_tableflow(
|
|
701
|
+
table,
|
|
702
|
+
tableflow_formats=TableFormat.ICEBERG,
|
|
703
|
+
storage=storage,
|
|
704
|
+
config=config,
|
|
705
|
+
)
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
---
|
|
709
|
+
|
|
568
710
|
## Introspection and Metadata
|
|
569
711
|
|
|
570
712
|
### Connection Properties
|
|
@@ -894,7 +1036,8 @@ cursor.execute(
|
|
|
894
1036
|
timeout: int = 3000,
|
|
895
1037
|
statement_name: str | None = None,
|
|
896
1038
|
statement_labels: list[str] | None = None,
|
|
897
|
-
properties: dict[str, str | int | bool] | None = None,
|
|
1039
|
+
properties: dict[str, str | int | bool] | StatementProperties | None = None,
|
|
1040
|
+
compute_pool_id: str | None = None,
|
|
898
1041
|
) -> None
|
|
899
1042
|
```
|
|
900
1043
|
|
|
@@ -907,20 +1050,73 @@ cursor.execute(
|
|
|
907
1050
|
| `timeout` | `int` | 3000 | Max seconds to wait for statement to reach RUNNING/COMPLETED phase |
|
|
908
1051
|
| `statement_name` | `str \| None` | None | Custom statement identifier (defaults to UUID) |
|
|
909
1052
|
| `statement_labels` | `list[str] \| None` | None | List of labels for grouping related statements |
|
|
910
|
-
| `properties` | `dict[str, str \| int \| bool] \| None` | None | [Statement properties](#statement-properties) to set for execution |
|
|
1053
|
+
| `properties` | `dict[str, str \| int \| bool] \| StatementProperties \| None` | None | [Statement properties](#statement-properties) to set for execution |
|
|
1054
|
+
| `compute_pool_id` | `str \| None` | None | Compute pool to run this statement on, overriding the connection's default |
|
|
911
1055
|
|
|
912
1056
|
### Statement Properties
|
|
913
1057
|
|
|
914
|
-
The `properties` parameter
|
|
1058
|
+
The `properties` parameter sets [Flink SQL statement properties](https://docs.confluent.io/cloud/current/flink/reference/statements/set.html#table-options) at query execution time — the same properties Flink SQL `SET` statements control. There are two ways to provide them:
|
|
1059
|
+
|
|
1060
|
+
- **`StatementProperties`** (recommended) — a frozen, keyword-only dataclass covering the curated
|
|
1061
|
+
options below, discoverable via autocomplete and validated at construction time instead of at
|
|
1062
|
+
the server. A wrong-property enum value (e.g. a `SnapshotMode` passed to `scan_startup_mode`), a
|
|
1063
|
+
field of the wrong Python type, or an `extra` key that duplicates a modeled field all raise
|
|
1064
|
+
immediately:
|
|
1065
|
+
|
|
1066
|
+
```python
|
|
1067
|
+
from confluent_sql import Property, ScanStartupMode, SnapshotWriteMode, StatementProperties
|
|
1068
|
+
from datetime import timedelta
|
|
1069
|
+
|
|
1070
|
+
cursor.execute(
|
|
1071
|
+
"SELECT * FROM orders WHERE status = %s",
|
|
1072
|
+
("pending",),
|
|
1073
|
+
properties=StatementProperties(
|
|
1074
|
+
state_ttl=timedelta(hours=1), # -> "3600 s"
|
|
1075
|
+
snapshot_write_mode=SnapshotWriteMode.FAST_WRITE,
|
|
1076
|
+
scan_startup_mode=ScanStartupMode.EARLIEST_OFFSET,
|
|
1077
|
+
# `extra` escape hatch for a property not yet a typed field; Keys can
|
|
1078
|
+
# either be Property enums or strings.
|
|
1079
|
+
extra={Property.SCAN_IDLE_TIMEOUT: "30 s"},
|
|
1080
|
+
),
|
|
1081
|
+
)
|
|
1082
|
+
```
|
|
1083
|
+
|
|
1084
|
+
The set of modeled fields grows over time -- see the `StatementProperties` docstring/source for
|
|
1085
|
+
the current list -- but the shape is uniform: only fields you actually set are emitted, so an
|
|
1086
|
+
unset field never pins a server default or collides with the driver's own overlay, and each
|
|
1087
|
+
enum-typed field also accepts a bare `str`, so a Flink value newer than this driver's enum can
|
|
1088
|
+
still be passed through without waiting for a driver release.
|
|
1089
|
+
|
|
1090
|
+
- **A raw `dict[str, str | int | bool]`** — the original, open-ended form. Any `sql.*` key is
|
|
1091
|
+
accepted, keyed by the string from the [SET-options
|
|
1092
|
+
reference](https://docs.confluent.io/cloud/current/flink/reference/statements/set.html), which is
|
|
1093
|
+
useful for options `StatementProperties` doesn't model yet (equivalent to `extra` above, without
|
|
1094
|
+
needing to go via the dataclass):
|
|
1095
|
+
|
|
1096
|
+
```python
|
|
1097
|
+
cursor.execute(query, properties={"sql.state-ttl": "3600 s"})
|
|
1098
|
+
```
|
|
1099
|
+
|
|
1100
|
+
`confluent_sql.Property` enumerates the known `sql.*` keys (e.g. `Property.STATE_TTL`) if you
|
|
1101
|
+
want autocomplete on the keys without adopting the full dataclass; members are plain `str`
|
|
1102
|
+
instances, so they drop straight into the dict with no `.value` unwrapping.
|
|
1103
|
+
|
|
1104
|
+
Both forms are validated identically — a `StatementProperties` is downgraded to a dict internally
|
|
1105
|
+
before the same checks run, so a reserved key smuggled through `extra` is rejected the same way a
|
|
1106
|
+
raw dict would be.
|
|
915
1107
|
|
|
916
1108
|
**Important Precedence Rules:**
|
|
917
|
-
-
|
|
918
|
-
|
|
919
|
-
-
|
|
920
|
-
|
|
1109
|
+
- System properties are always applied and cannot be overridden by the caller: the connection's
|
|
1110
|
+
catalog/database and the cursor's execution mode (e.g. `sql.snapshot.mode` for snapshot queries).
|
|
1111
|
+
- The connection-level `local_time_zone` default (see [`Connection.local_time_zone`](#connection-level-local_time_zone-default) below) fills in `sql.local-time-zone` only when the
|
|
1112
|
+
call's own `properties` didn't already set it.
|
|
1113
|
+
- User-provided properties in the `properties` parameter can set anything not covered by the two
|
|
1114
|
+
rules above, but attempting to set a system property (e.g. `sql.current-catalog`) raises
|
|
1115
|
+
`InterfaceError` rather than being silently overridden.
|
|
921
1116
|
|
|
922
1117
|
**Accessing Properties After Execution:**
|
|
923
|
-
The properties are stored in the Statement object and can be accessed via
|
|
1118
|
+
The properties are stored in the cursor-captured Statement object and can be accessed via
|
|
1119
|
+
`statement.properties`, a `dict[str, str | int | bool]`:
|
|
924
1120
|
|
|
925
1121
|
```python
|
|
926
1122
|
cursor.execute(query, properties={"sql.state-ttl": "100 ms"})
|
|
@@ -928,7 +1124,31 @@ props = cursor.statement.properties
|
|
|
928
1124
|
assert props["sql.state-ttl"] == "100 ms"
|
|
929
1125
|
```
|
|
930
1126
|
|
|
931
|
-
|
|
1127
|
+
### Connection-level `local_time_zone` default
|
|
1128
|
+
|
|
1129
|
+
`Connection.local_time_zone` (also settable via `connect(local_time_zone=...)`) is a read/write
|
|
1130
|
+
property that seeds `sql.local-time-zone` for every statement the connection executes, so you don't
|
|
1131
|
+
have to repeat it on each `execute()` call:
|
|
1132
|
+
|
|
1133
|
+
```python
|
|
1134
|
+
connection.local_time_zone = "America/Chicago"
|
|
1135
|
+
cursor.execute("SELECT CURRENT_TIMESTAMP") # runs with sql.local-time-zone = America/Chicago
|
|
1136
|
+
|
|
1137
|
+
# A statement can still override it for itself:
|
|
1138
|
+
cursor.execute(
|
|
1139
|
+
"SELECT CURRENT_TIMESTAMP",
|
|
1140
|
+
properties=StatementProperties(local_time_zone="America/Los_Angeles"),
|
|
1141
|
+
)
|
|
1142
|
+
```
|
|
1143
|
+
|
|
1144
|
+
The connection-level value only fills in where a statement's own `properties`
|
|
1145
|
+
(dict or `StatementProperties`) didn't already set `sql.local-time-zone` — it never overrides an
|
|
1146
|
+
explicit per-call value. Set it to `None` to stop emitting a default.
|
|
1147
|
+
|
|
1148
|
+
A runnable example covering both property forms and the connection-level default is in
|
|
1149
|
+
[examples/statement_properties_example.py](examples/statement_properties_example.py).
|
|
1150
|
+
|
|
1151
|
+
**General Usage Examples:**
|
|
932
1152
|
|
|
933
1153
|
```python
|
|
934
1154
|
# Basic execution
|
|
@@ -940,7 +1160,7 @@ cursor.execute("SELECT * FROM users WHERE age > %s", (18,))
|
|
|
940
1160
|
# With custom timeout
|
|
941
1161
|
cursor.execute(
|
|
942
1162
|
"SELECT * FROM users",
|
|
943
|
-
timeout=
|
|
1163
|
+
timeout=100 # Wait up to 100 seconds
|
|
944
1164
|
)
|
|
945
1165
|
|
|
946
1166
|
# With statement naming
|
|
@@ -950,7 +1170,7 @@ cursor.execute(
|
|
|
950
1170
|
statement_name="completed-orders-daily"
|
|
951
1171
|
)
|
|
952
1172
|
|
|
953
|
-
# With statement labeling
|
|
1173
|
+
# With statement labeling
|
|
954
1174
|
cursor.execute(
|
|
955
1175
|
"CREATE TABLE orders_backup AS SELECT * FROM orders",
|
|
956
1176
|
statement_labels=["daily-backups", "batch-job"]
|
|
@@ -965,13 +1185,21 @@ cursor.execute(
|
|
|
965
1185
|
statement_labels=["analytics", "hourly"]
|
|
966
1186
|
)
|
|
967
1187
|
|
|
968
|
-
# With statement properties
|
|
1188
|
+
# With statement properties (raw dict)
|
|
969
1189
|
cursor.execute(
|
|
970
1190
|
"SELECT * FROM orders WHERE status = %s",
|
|
971
1191
|
("pending",),
|
|
972
1192
|
statement_name="pending-orders-query",
|
|
973
1193
|
properties={"sql.state-ttl": "100 ms"}
|
|
974
1194
|
)
|
|
1195
|
+
|
|
1196
|
+
# With statement properties (StatementProperties)
|
|
1197
|
+
cursor.execute(
|
|
1198
|
+
"SELECT * FROM orders WHERE status = %s",
|
|
1199
|
+
("pending",),
|
|
1200
|
+
statement_name="pending-orders-query",
|
|
1201
|
+
properties=StatementProperties(state_ttl=timedelta(milliseconds=100)),
|
|
1202
|
+
)
|
|
975
1203
|
```
|
|
976
1204
|
|
|
977
1205
|
---
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: confluent-sql
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: DB-API v2 compliant driver for Confluent Cloud Flink SQL
|
|
5
5
|
Project-URL: Repository, https://github.com/confluentinc/confluent-sql
|
|
6
6
|
Project-URL: Documentation, https://github.com/confluentinc/confluent-sql?tab=readme-ov-file#confluent-sql
|
|
@@ -253,9 +253,11 @@ The driver defaults to [snapshot mode](https://docs.confluent.io/cloud/current/f
|
|
|
253
253
|
- **Existing Flink Database** (Confluent Cloud Kafka cluster)
|
|
254
254
|
- **API credentials**, one of:
|
|
255
255
|
- a **"Global" Confluent Cloud API key** and secret, passed as `global_api_key` / `global_api_secret`, or
|
|
256
|
-
- a **Flink Region API key** and secret, passed as `flink_api_key` / `flink_api_secret
|
|
256
|
+
- a **Flink Region API key** and secret, passed as `flink_api_key` / `flink_api_secret`, or
|
|
257
|
+
- a **BYOIDC bearer token** minted by your own OAuth/OIDC identity provider, passed as `external_access_token` / `identity_pool_id` (see [BYOIDC bearer-token authentication](#byoidc-bearer-token-authentication) below).
|
|
257
258
|
|
|
258
259
|
A Global key works against every route this driver touches, so it is the more future-proof choice; a Flink Region key works against the Flink SQL routes that are the driver's focus today. Provide at least one pair. If you supply both, the Global pair is used and the Flink pair is ignored. A half-supplied pair (a key without its secret, or vice versa) is rejected.
|
|
260
|
+
- **Organization ID** — required, *unless* you're using a Global API key and it can see exactly one organization, in which case you can omit `organization_id` and it's inferred automatically on first use of the connection. A Flink Region key has no way to discover it, so it always requires `organization_id` explicitly.
|
|
259
261
|
|
|
260
262
|
### How to Obtain a Flink Region API Key
|
|
261
263
|
|
|
@@ -276,6 +278,35 @@ To create or find a Flink Region API key:
|
|
|
276
278
|
|
|
277
279
|
API keys may also be generated using the Confluent CLI or by API access, outside the scope of this document.
|
|
278
280
|
|
|
281
|
+
### BYOIDC bearer-token authentication
|
|
282
|
+
|
|
283
|
+
If your Confluent Cloud organization is configured with your own registered OAuth/OIDC identity provider, you can authenticate to the **Flink data plane** with a bearer token you mint yourself (an "external" token in Confluent's authorization vocabulary) rather than a Confluent API key. Pass the token and the identity-pool id that scopes it:
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
import os
|
|
287
|
+
import confluent_sql
|
|
288
|
+
|
|
289
|
+
connection = confluent_sql.connect(
|
|
290
|
+
external_access_token=os.environ["MY_IDP_BEARER_TOKEN"],
|
|
291
|
+
identity_pool_id="pool-abc123",
|
|
292
|
+
environment_id="env-...",
|
|
293
|
+
organization_id="org-...", # required under BYOIDC (see below)
|
|
294
|
+
cloud_provider="aws",
|
|
295
|
+
cloud_region="us-east-2",
|
|
296
|
+
database="your-database-name", # optional, and works under BYOIDC: it only sets the default
|
|
297
|
+
# database, which needs no CMK cluster-id lookup
|
|
298
|
+
)
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
The driver stamps `Authorization: Bearer <external_access_token>` and `Confluent-Identity-Pool-Id: <identity_pool_id>` on every Flink request. The parameter name mirrors the Confluent Flink Table API plugin's `client.oauth.external-access-token` option. A runnable example is in [examples/byoidc_bearer_token_example.py](examples/byoidc_bearer_token_example.py).
|
|
302
|
+
|
|
303
|
+
Things to know:
|
|
304
|
+
|
|
305
|
+
- **`external_access_token` and `identity_pool_id` are a pair** and are **mutually exclusive** with every API-key parameter (`global_*`, `flink_*`, `tableflow_*`, `connect_*`). Supplying a bearer token alongside any API key is rejected.
|
|
306
|
+
- **Flink data plane only.** Confluent's authorization model does not accept a raw external token on any control-plane route this driver calls. So under BYOIDC the control-plane surfaces — Tableflow (`enable_tableflow` / `get_tableflow` / `disable_tableflow`), Connectors, and the CMK cluster-id lookup that resolves a `database` name to its `lkc-…` id — fail closed with a clear error. Use an API-key connection for those, and pass `database_kafka_cluster_id` to `connect()` if you need the driver to know the cluster id without the CMK lookup.
|
|
307
|
+
- **`organization_id` is required** under BYOIDC. Inferring it needs a control-plane call an external token can't make, so it must be supplied explicitly.
|
|
308
|
+
- **No token refresh.** The token is used verbatim on every request. When it expires, requests begin failing (surfaced as `OperationalError`) and the connection is effectively dead — open a fresh connection with a fresh token.
|
|
309
|
+
|
|
279
310
|
## Installation
|
|
280
311
|
|
|
281
312
|
```bash
|
|
@@ -295,7 +326,7 @@ import confluent_sql
|
|
|
295
326
|
|
|
296
327
|
# Connect to Confluent Cloud Flink SQL
|
|
297
328
|
connection = confluent_sql.connect(
|
|
298
|
-
organization_id="your-org-uuid",
|
|
329
|
+
organization_id="your-org-uuid", # optional if using a Global key -- see Prerequisites
|
|
299
330
|
environment_id="env-123456",
|
|
300
331
|
cloud_provider="aws",
|
|
301
332
|
cloud_region="us-east-2",
|
|
@@ -379,6 +410,7 @@ This driver extends the standard DB-API v2 interface with additional features:
|
|
|
379
410
|
- **Streaming cursors** - Non-blocking result consumption from continuous queries
|
|
380
411
|
- **Changelog compression** - Automatic state management for aggregations and joins
|
|
381
412
|
- **Statement lifecycle management** - Named statements, labels, and resource management
|
|
413
|
+
- **Statement properties** - Execution controls not expressible inline within the SQL statement
|
|
382
414
|
- **Type system** - Full support for all Flink SQL types including streaming-specific types
|
|
383
415
|
- **Performance monitoring** - Built-in fetch metrics and introspection
|
|
384
416
|
|
|
@@ -437,14 +469,15 @@ Set required environment variables for integration tests.
|
|
|
437
469
|
If any of the variables is not set, integration tests will be skipped.
|
|
438
470
|
|
|
439
471
|
```bash
|
|
440
|
-
export CONFLUENT_ORG_ID="org-123456"
|
|
472
|
+
export CONFLUENT_ORG_ID="org-123456" # Optional if using a Global key that can see exactly one org.
|
|
441
473
|
export CONFLUENT_ENV_ID="env-123456"
|
|
442
474
|
export CONFLUENT_CLOUD_PROVIDER="aws"
|
|
443
475
|
export CONFLUENT_CLOUD_REGION="us-east-2"
|
|
444
476
|
export CONFLUENT_FLINK_API_KEY="your-key" # Flink Region API key for the above cloud/region ...
|
|
445
477
|
export CONFLUENT_FLINK_API_SECRET="your-secret" # and associated secret.
|
|
446
478
|
# Alternatively, a "Global" Confluent Cloud API key (used in preference to the Flink pair if both
|
|
447
|
-
# are set): export CONFLUENT_GLOBAL_API_KEY / CONFLUENT_GLOBAL_API_SECRET instead.
|
|
479
|
+
# are set): export CONFLUENT_GLOBAL_API_KEY / CONFLUENT_GLOBAL_API_SECRET instead. With a Global
|
|
480
|
+
# key, CONFLUENT_ORG_ID may be omitted -- the integration suite infers it, same as connect() does.
|
|
448
481
|
export CONFLUENT_TEST_DBNAME="test-db" # A database/kafka cluster name within the above cloud/region.
|
|
449
482
|
export CONFLUENT_COMPUTE_POOL_ID="lfcp-789012" # Optional. If set, the integration suite runs against this pool; if unset, the suite runs against the environment's default pool. The driver treats it as optional at connect() either way.
|
|
450
483
|
```
|