confluent-sql 0.4.2__tar.gz → 0.5.0__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.0}/.semaphore/semaphore.yml +1 -1
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/CHANGELOG.md +18 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/CLAUDE.md +32 -1
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/DBAPI_EXTENSIONS.md +240 -12
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/PKG-INFO +38 -5
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/README.md +37 -4
- confluent_sql-0.5.0/examples/byoidc_bearer_token_example.py +34 -0
- confluent_sql-0.5.0/examples/statement_properties_example.py +73 -0
- confluent_sql-0.5.0/examples/tableflow_lifecycle_example.py +50 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/pyproject.toml +2 -2
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/src/confluent_sql/__init__.py +60 -1
- confluent_sql-0.5.0/src/confluent_sql/auth.py +30 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/src/confluent_sql/connection.py +1073 -99
- confluent_sql-0.5.0/src/confluent_sql/connectors.py +492 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/src/confluent_sql/cursor.py +9 -5
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/src/confluent_sql/exceptions.py +64 -0
- confluent_sql-0.5.0/src/confluent_sql/statement_properties.py +322 -0
- confluent_sql-0.5.0/src/confluent_sql/tableflow.py +363 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/src/confluent_sql/types.py +18 -9
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/conftest.py +8 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/integration/conftest.py +4 -1
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/integration/test_connection.py +2 -2
- confluent_sql-0.5.0/tests/integration/test_connector.py +184 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/integration/test_cursor.py +82 -7
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/integration/test_fetch.py +2 -6
- confluent_sql-0.5.0/tests/integration/test_tableflow.py +152 -0
- confluent_sql-0.5.0/tests/unit/test_auth_unit.py +28 -0
- confluent_sql-0.5.0/tests/unit/test_connection_byoidc_unit.py +221 -0
- confluent_sql-0.5.0/tests/unit/test_connection_connector_unit.py +184 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/unit/test_connection_unit.py +389 -133
- confluent_sql-0.5.0/tests/unit/test_connection_unit_properties.py +413 -0
- confluent_sql-0.5.0/tests/unit/test_connectors_connection_unit.py +502 -0
- confluent_sql-0.5.0/tests/unit/test_connectors_unit.py +191 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/unit/test_cursor_unit.py +9 -21
- confluent_sql-0.5.0/tests/unit/test_statement_properties_unit.py +396 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/unit/test_statement_unit.py +3 -9
- confluent_sql-0.5.0/tests/unit/test_tableflow_connection_unit.py +525 -0
- confluent_sql-0.5.0/tests/unit/test_tableflow_unit.py +322 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/uv.lock +6 -6
- confluent_sql-0.4.2/tests/unit/test_connection_unit_properties.py +0 -200
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/.github/CODEOWNERS +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/.gitignore +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/.semaphore/publish_to_pypi.yml +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/ARCHITECTURE.md +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/LICENSE.txt +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/Makefile +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/STREAMING.md +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/TYPES.md +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/examples/errors.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/examples/simple_append_only_streaming_query_example.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/examples/snapshot_mode_tuple_cursor_simple_example.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/service.yml +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/src/confluent_sql/__version__.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/src/confluent_sql/changelog_compressor.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/src/confluent_sql/execution_mode.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/src/confluent_sql/polling.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/src/confluent_sql/result_readers.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/src/confluent_sql/retry.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/src/confluent_sql/statement.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/__init__.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/unit/conftest.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/unit/test_changelog_compressor_unit.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/unit/test_changelog_unit.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/unit/test_execution_mode_unit.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/unit/test_polling_unit.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/unit/test_result_readers_unit.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/unit/test_retry_unit.py +0 -0
- {confluent_sql-0.4.2 → confluent_sql-0.5.0}/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:
|
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this dbapi driver will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.5.0, 2026-08-07
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `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.
|
|
10
|
+
- `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)
|
|
11
|
+
- 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)
|
|
12
|
+
- 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.
|
|
13
|
+
- `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.
|
|
14
|
+
- `Connection.get_tableflow(table_name)` returns the current `TableflowTopic` (phase, spec, status), raising `TableflowTopicNotFoundError` if Tableflow is not enabled for the topic.
|
|
15
|
+
- `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.
|
|
16
|
+
- `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)
|
|
17
|
+
- 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)
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- 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)
|
|
22
|
+
|
|
5
23
|
## 0.4.2, 2026-07-17
|
|
6
24
|
|
|
7
25
|
### Changed
|
|
@@ -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
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: confluent-sql
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
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
|
```
|
|
@@ -22,9 +22,11 @@ The driver defaults to [snapshot mode](https://docs.confluent.io/cloud/current/f
|
|
|
22
22
|
- **Existing Flink Database** (Confluent Cloud Kafka cluster)
|
|
23
23
|
- **API credentials**, one of:
|
|
24
24
|
- a **"Global" Confluent Cloud API key** and secret, passed as `global_api_key` / `global_api_secret`, or
|
|
25
|
-
- a **Flink Region API key** and secret, passed as `flink_api_key` / `flink_api_secret
|
|
25
|
+
- a **Flink Region API key** and secret, passed as `flink_api_key` / `flink_api_secret`, or
|
|
26
|
+
- 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).
|
|
26
27
|
|
|
27
28
|
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.
|
|
29
|
+
- **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.
|
|
28
30
|
|
|
29
31
|
### How to Obtain a Flink Region API Key
|
|
30
32
|
|
|
@@ -45,6 +47,35 @@ To create or find a Flink Region API key:
|
|
|
45
47
|
|
|
46
48
|
API keys may also be generated using the Confluent CLI or by API access, outside the scope of this document.
|
|
47
49
|
|
|
50
|
+
### BYOIDC bearer-token authentication
|
|
51
|
+
|
|
52
|
+
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:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import os
|
|
56
|
+
import confluent_sql
|
|
57
|
+
|
|
58
|
+
connection = confluent_sql.connect(
|
|
59
|
+
external_access_token=os.environ["MY_IDP_BEARER_TOKEN"],
|
|
60
|
+
identity_pool_id="pool-abc123",
|
|
61
|
+
environment_id="env-...",
|
|
62
|
+
organization_id="org-...", # required under BYOIDC (see below)
|
|
63
|
+
cloud_provider="aws",
|
|
64
|
+
cloud_region="us-east-2",
|
|
65
|
+
database="your-database-name", # optional, and works under BYOIDC: it only sets the default
|
|
66
|
+
# database, which needs no CMK cluster-id lookup
|
|
67
|
+
)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
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).
|
|
71
|
+
|
|
72
|
+
Things to know:
|
|
73
|
+
|
|
74
|
+
- **`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.
|
|
75
|
+
- **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.
|
|
76
|
+
- **`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.
|
|
77
|
+
- **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.
|
|
78
|
+
|
|
48
79
|
## Installation
|
|
49
80
|
|
|
50
81
|
```bash
|
|
@@ -64,7 +95,7 @@ import confluent_sql
|
|
|
64
95
|
|
|
65
96
|
# Connect to Confluent Cloud Flink SQL
|
|
66
97
|
connection = confluent_sql.connect(
|
|
67
|
-
organization_id="your-org-uuid",
|
|
98
|
+
organization_id="your-org-uuid", # optional if using a Global key -- see Prerequisites
|
|
68
99
|
environment_id="env-123456",
|
|
69
100
|
cloud_provider="aws",
|
|
70
101
|
cloud_region="us-east-2",
|
|
@@ -148,6 +179,7 @@ This driver extends the standard DB-API v2 interface with additional features:
|
|
|
148
179
|
- **Streaming cursors** - Non-blocking result consumption from continuous queries
|
|
149
180
|
- **Changelog compression** - Automatic state management for aggregations and joins
|
|
150
181
|
- **Statement lifecycle management** - Named statements, labels, and resource management
|
|
182
|
+
- **Statement properties** - Execution controls not expressible inline within the SQL statement
|
|
151
183
|
- **Type system** - Full support for all Flink SQL types including streaming-specific types
|
|
152
184
|
- **Performance monitoring** - Built-in fetch metrics and introspection
|
|
153
185
|
|
|
@@ -206,14 +238,15 @@ Set required environment variables for integration tests.
|
|
|
206
238
|
If any of the variables is not set, integration tests will be skipped.
|
|
207
239
|
|
|
208
240
|
```bash
|
|
209
|
-
export CONFLUENT_ORG_ID="org-123456"
|
|
241
|
+
export CONFLUENT_ORG_ID="org-123456" # Optional if using a Global key that can see exactly one org.
|
|
210
242
|
export CONFLUENT_ENV_ID="env-123456"
|
|
211
243
|
export CONFLUENT_CLOUD_PROVIDER="aws"
|
|
212
244
|
export CONFLUENT_CLOUD_REGION="us-east-2"
|
|
213
245
|
export CONFLUENT_FLINK_API_KEY="your-key" # Flink Region API key for the above cloud/region ...
|
|
214
246
|
export CONFLUENT_FLINK_API_SECRET="your-secret" # and associated secret.
|
|
215
247
|
# Alternatively, a "Global" Confluent Cloud API key (used in preference to the Flink pair if both
|
|
216
|
-
# are set): export CONFLUENT_GLOBAL_API_KEY / CONFLUENT_GLOBAL_API_SECRET instead.
|
|
248
|
+
# are set): export CONFLUENT_GLOBAL_API_KEY / CONFLUENT_GLOBAL_API_SECRET instead. With a Global
|
|
249
|
+
# key, CONFLUENT_ORG_ID may be omitted -- the integration suite infers it, same as connect() does.
|
|
217
250
|
export CONFLUENT_TEST_DBNAME="test-db" # A database/kafka cluster name within the above cloud/region.
|
|
218
251
|
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.
|
|
219
252
|
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Example of BYOIDC authentication: authenticate to the Flink data plane with a bearer token
|
|
2
|
+
minted by your own OAuth/OIDC identity provider, instead of a Confluent API key + secret.
|
|
3
|
+
|
|
4
|
+
The driver stamps `Authorization: Bearer <external_access_token>` and `Confluent-Identity-Pool-Id`
|
|
5
|
+
on every Flink request. BYOIDC reaches the Flink data plane only -- Tableflow, Connectors, and the
|
|
6
|
+
CMK cluster-id lookup require an API-key connection. organization_id is mandatory (there is no
|
|
7
|
+
control-plane reach to infer it), and the token is used verbatim with no refresh (when it
|
|
8
|
+
expires, open a fresh connection with a fresh token).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
import confluent_sql
|
|
14
|
+
|
|
15
|
+
# os.environ[...] on the required params so a missing var fails fast naming itself, rather than an
|
|
16
|
+
# empty string slipping through: an empty external_access_token is falsy, so the driver would never
|
|
17
|
+
# enter BYOIDC mode and would instead raise a misleading API-key/org-id error.
|
|
18
|
+
conn = confluent_sql.connect(
|
|
19
|
+
external_access_token=os.environ["CONFLUENT_EXTERNAL_ACCESS_TOKEN"],
|
|
20
|
+
identity_pool_id=os.environ["CONFLUENT_IDENTITY_POOL_ID"],
|
|
21
|
+
environment_id=os.environ["CONFLUENT_ENV_ID"],
|
|
22
|
+
organization_id=os.environ["CONFLUENT_ORG_ID"],
|
|
23
|
+
cloud_provider=os.environ["CONFLUENT_CLOUD_PROVIDER"],
|
|
24
|
+
cloud_region=os.environ["CONFLUENT_CLOUD_REGION"],
|
|
25
|
+
compute_pool_id=os.getenv("CONFLUENT_COMPUTE_POOL_ID"), # optional; None -> default pool
|
|
26
|
+
)
|
|
27
|
+
cursor = conn.cursor()
|
|
28
|
+
try:
|
|
29
|
+
cursor.execute("SELECT 1 as test_value_1, 2 as test_value_2, 3 as test_value_3")
|
|
30
|
+
for row in cursor:
|
|
31
|
+
print(f"iterating over cursor results: {row}")
|
|
32
|
+
finally:
|
|
33
|
+
cursor.close()
|
|
34
|
+
conn.close()
|