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.
Files changed (68) hide show
  1. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/.semaphore/semaphore.yml +1 -1
  2. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/CHANGELOG.md +18 -0
  3. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/CLAUDE.md +32 -1
  4. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/DBAPI_EXTENSIONS.md +240 -12
  5. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/PKG-INFO +38 -5
  6. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/README.md +37 -4
  7. confluent_sql-0.5.0/examples/byoidc_bearer_token_example.py +34 -0
  8. confluent_sql-0.5.0/examples/statement_properties_example.py +73 -0
  9. confluent_sql-0.5.0/examples/tableflow_lifecycle_example.py +50 -0
  10. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/pyproject.toml +2 -2
  11. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/src/confluent_sql/__init__.py +60 -1
  12. confluent_sql-0.5.0/src/confluent_sql/auth.py +30 -0
  13. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/src/confluent_sql/connection.py +1073 -99
  14. confluent_sql-0.5.0/src/confluent_sql/connectors.py +492 -0
  15. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/src/confluent_sql/cursor.py +9 -5
  16. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/src/confluent_sql/exceptions.py +64 -0
  17. confluent_sql-0.5.0/src/confluent_sql/statement_properties.py +322 -0
  18. confluent_sql-0.5.0/src/confluent_sql/tableflow.py +363 -0
  19. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/src/confluent_sql/types.py +18 -9
  20. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/conftest.py +8 -0
  21. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/integration/conftest.py +4 -1
  22. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/integration/test_connection.py +2 -2
  23. confluent_sql-0.5.0/tests/integration/test_connector.py +184 -0
  24. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/integration/test_cursor.py +82 -7
  25. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/integration/test_fetch.py +2 -6
  26. confluent_sql-0.5.0/tests/integration/test_tableflow.py +152 -0
  27. confluent_sql-0.5.0/tests/unit/test_auth_unit.py +28 -0
  28. confluent_sql-0.5.0/tests/unit/test_connection_byoidc_unit.py +221 -0
  29. confluent_sql-0.5.0/tests/unit/test_connection_connector_unit.py +184 -0
  30. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/unit/test_connection_unit.py +389 -133
  31. confluent_sql-0.5.0/tests/unit/test_connection_unit_properties.py +413 -0
  32. confluent_sql-0.5.0/tests/unit/test_connectors_connection_unit.py +502 -0
  33. confluent_sql-0.5.0/tests/unit/test_connectors_unit.py +191 -0
  34. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/unit/test_cursor_unit.py +9 -21
  35. confluent_sql-0.5.0/tests/unit/test_statement_properties_unit.py +396 -0
  36. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/unit/test_statement_unit.py +3 -9
  37. confluent_sql-0.5.0/tests/unit/test_tableflow_connection_unit.py +525 -0
  38. confluent_sql-0.5.0/tests/unit/test_tableflow_unit.py +322 -0
  39. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/uv.lock +6 -6
  40. confluent_sql-0.4.2/tests/unit/test_connection_unit_properties.py +0 -200
  41. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/.github/CODEOWNERS +0 -0
  42. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/.gitignore +0 -0
  43. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/.semaphore/publish_to_pypi.yml +0 -0
  44. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/ARCHITECTURE.md +0 -0
  45. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/LICENSE.txt +0 -0
  46. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/Makefile +0 -0
  47. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/STREAMING.md +0 -0
  48. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/TYPES.md +0 -0
  49. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/examples/errors.py +0 -0
  50. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/examples/simple_append_only_streaming_query_example.py +0 -0
  51. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/examples/snapshot_mode_tuple_cursor_simple_example.py +0 -0
  52. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/service.yml +0 -0
  53. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/src/confluent_sql/__version__.py +0 -0
  54. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/src/confluent_sql/changelog_compressor.py +0 -0
  55. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/src/confluent_sql/execution_mode.py +0 -0
  56. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/src/confluent_sql/polling.py +0 -0
  57. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/src/confluent_sql/result_readers.py +0 -0
  58. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/src/confluent_sql/retry.py +0 -0
  59. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/src/confluent_sql/statement.py +0 -0
  60. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/__init__.py +0 -0
  61. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/unit/conftest.py +0 -0
  62. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/unit/test_changelog_compressor_unit.py +0 -0
  63. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/unit/test_changelog_unit.py +0 -0
  64. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/unit/test_execution_mode_unit.py +0 -0
  65. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/unit/test_polling_unit.py +0 -0
  66. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/unit/test_result_readers_unit.py +0 -0
  67. {confluent_sql-0.4.2 → confluent_sql-0.5.0}/tests/unit/test_retry_unit.py +0 -0
  68. {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
- # Project-specific instructions for confluent-sql
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 allows you to set [Flink SQL statement properties](https://docs.confluent.io/cloud/current/flink/reference/statements/set.html#table-options) at query execution time. These are the same properties that can be set with Flink SQL `SET` statements.
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
- - Connection-level defaults (catalog, database) are always applied
918
- - Cursor execution mode settings (e.g., `sql.snapshot.mode` for snapshot queries) are always applied
919
- - User-provided properties in the `properties` parameter can extend these settings but cannot override system properties
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 `statement.properties`:
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
- **Examples:**
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=5000 # Wait up to 5000 seconds
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 (for batch operations)
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.4.2
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()