py-flink-sql-gateway 0.1.0.dev0__py3-none-any.whl

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.
@@ -0,0 +1,104 @@
1
+ """Flink SQL Gateway — Python client library.
2
+
3
+ Implements PEP 249 (DB-API 2.0) on top of the Flink SQL Gateway REST API.
4
+ """
5
+
6
+ from flink_gateway.client import FlinkSqlGatewayClient
7
+ from flink_gateway.connection import Connection, connect
8
+ from flink_gateway.cursor import Cursor
9
+ from flink_gateway.exceptions import (
10
+ DatabaseError,
11
+ Error,
12
+ FlinkSqlGatewayError,
13
+ InterfaceError,
14
+ NotSupportedError,
15
+ OperationalError,
16
+ ProgrammingError,
17
+ )
18
+ from flink_gateway.models import (
19
+ ColumnInfo,
20
+ CompleteStatementRequest,
21
+ ConfigureSessionRequest,
22
+ ExecuteStatementRequest,
23
+ FetchResultsResponse,
24
+ InfoResponse,
25
+ LogicalType,
26
+ OpenSessionRequest,
27
+ RefreshMaterializedTableRequest,
28
+ ResultKind,
29
+ ResultSet,
30
+ ResultType,
31
+ RowData,
32
+ )
33
+ from flink_gateway.types import (
34
+ BINARY,
35
+ DATETIME,
36
+ NUMBER,
37
+ ROWID,
38
+ STRING,
39
+ Binary,
40
+ Date,
41
+ DateFromTicks,
42
+ DBAPITypeObject,
43
+ FlinkType,
44
+ Time,
45
+ TimeFromTicks,
46
+ Timestamp,
47
+ TimestampFromTicks,
48
+ )
49
+
50
+ # PEP 249 module-level globals
51
+ apilevel = "2.0"
52
+ threadsafety = 1 # Threads may share the module but not connections.
53
+ paramstyle = "qmark"
54
+
55
+ __all__ = [
56
+ # PEP 249
57
+ "apilevel",
58
+ "threadsafety",
59
+ "paramstyle",
60
+ "connect",
61
+ "Connection",
62
+ "Cursor",
63
+ # Exceptions
64
+ "Error",
65
+ "InterfaceError",
66
+ "DatabaseError",
67
+ "OperationalError",
68
+ "FlinkSqlGatewayError",
69
+ "ProgrammingError",
70
+ "NotSupportedError",
71
+ # Type objects
72
+ "STRING",
73
+ "BINARY",
74
+ "NUMBER",
75
+ "DATETIME",
76
+ "ROWID",
77
+ "DBAPITypeObject",
78
+ "FlinkType",
79
+ # PEP 249 constructors
80
+ "Date",
81
+ "Time",
82
+ "Timestamp",
83
+ "DateFromTicks",
84
+ "TimeFromTicks",
85
+ "TimestampFromTicks",
86
+ "Binary",
87
+ # Low-level client
88
+ "FlinkSqlGatewayClient",
89
+ "FlinkSqlGatewayError",
90
+ # Models
91
+ "ColumnInfo",
92
+ "CompleteStatementRequest",
93
+ "ConfigureSessionRequest",
94
+ "ExecuteStatementRequest",
95
+ "FetchResultsResponse",
96
+ "InfoResponse",
97
+ "LogicalType",
98
+ "OpenSessionRequest",
99
+ "RefreshMaterializedTableRequest",
100
+ "ResultKind",
101
+ "ResultSet",
102
+ "ResultType",
103
+ "RowData",
104
+ ]
@@ -0,0 +1,34 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
12
+
13
+ TYPE_CHECKING = False
14
+ if TYPE_CHECKING:
15
+ from typing import Tuple
16
+ from typing import Union
17
+
18
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
20
+ else:
21
+ VERSION_TUPLE = object
22
+ COMMIT_ID = object
23
+
24
+ version: str
25
+ __version__: str
26
+ __version_tuple__: VERSION_TUPLE
27
+ version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
30
+
31
+ __version__ = version = '0.1.0.dev0'
32
+ __version_tuple__ = version_tuple = (0, 1, 0, 'dev0')
33
+
34
+ __commit_id__ = commit_id = None
@@ -0,0 +1,302 @@
1
+ """Flink SQL Gateway REST API client.
2
+
3
+ This module provides a low-level HTTP client that wraps every endpoint of
4
+ the Apache Flink SQL Gateway REST API.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ import httpx
12
+
13
+ from flink_gateway.exceptions import FlinkSqlGatewayError
14
+ from flink_gateway.models import (
15
+ CompleteStatementRequest,
16
+ ConfigureSessionRequest,
17
+ ExecuteStatementRequest,
18
+ FetchResultsResponse,
19
+ InfoResponse,
20
+ OpenSessionRequest,
21
+ RefreshMaterializedTableRequest,
22
+ )
23
+
24
+
25
+ class FlinkSqlGatewayClient:
26
+ """Low-level client for the Flink SQL Gateway REST API.
27
+
28
+ Args:
29
+ base_url: Root URL of the SQL Gateway, e.g. ``http://localhost:8083``.
30
+ http_client: Optional pre-configured ``httpx.Client``. When *None*, a
31
+ default client is created with a 30-second timeout.
32
+ api_version: REST API version prefix (``"v1"``, ``"v2"``, or ``"v3"``).
33
+ Defaults to ``"v3"``.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ base_url: str,
39
+ http_client: httpx.Client | None = None,
40
+ api_version: str = "v3",
41
+ ) -> None:
42
+ self._base_url = base_url.rstrip("/")
43
+ self._api_version = api_version or "v3"
44
+ self._owns_client = http_client is None
45
+ self._client = http_client or httpx.Client(timeout=30.0)
46
+
47
+ # ── Context manager ────────────────────────────────────────────────
48
+
49
+ def __enter__(self) -> FlinkSqlGatewayClient:
50
+ return self
51
+
52
+ def __exit__(self, *_: Any) -> None:
53
+ self.close()
54
+
55
+ def close(self) -> None:
56
+ """Close the underlying HTTP client if we own it."""
57
+ if self._owns_client:
58
+ self._client.close()
59
+
60
+ # ── URL helpers ────────────────────────────────────────────────────
61
+
62
+ def _build_endpoint(self, *segments: str) -> str:
63
+ parts = [self._base_url, self._api_version, *segments]
64
+ return "/".join(parts)
65
+
66
+ # ── Metadata ───────────────────────────────────────────────────────
67
+
68
+ def get_info(self) -> InfoResponse:
69
+ """GET /info — cluster metadata."""
70
+ url = self._build_endpoint("info")
71
+ resp = self._client.get(url)
72
+ self._check_response(resp, "get info")
73
+ data = resp.json()
74
+ return InfoResponse(
75
+ product_name=data.get("productName", ""),
76
+ version=data.get("version", ""),
77
+ )
78
+
79
+ def get_api_versions(self) -> list[str]:
80
+ """GET /api_versions — supported REST API versions."""
81
+ url = self._build_endpoint("api_versions")
82
+ resp = self._client.get(url)
83
+ self._check_response(resp, "get api versions")
84
+ return resp.json().get("versions", [])
85
+
86
+ # ── Session management ─────────────────────────────────────────────
87
+
88
+ def open_session(
89
+ self,
90
+ request: OpenSessionRequest | None = None,
91
+ ) -> str:
92
+ """POST /sessions — open a new session.
93
+
94
+ Returns:
95
+ The session handle string.
96
+ """
97
+ url = self._build_endpoint("sessions")
98
+ body = request.to_dict() if request else {}
99
+ resp = self._client.post(url, json=body)
100
+ self._check_response(resp, "open session")
101
+ return resp.json()["sessionHandle"]
102
+
103
+ def close_session(self, session_handle: str) -> None:
104
+ """DELETE /sessions/{session_handle}."""
105
+ url = self._build_endpoint("sessions", session_handle)
106
+ resp = self._client.delete(url)
107
+ self._check_response(resp, "close session")
108
+
109
+ def get_session_config(self, session_handle: str) -> dict[str, str]:
110
+ """GET /sessions/{session_handle} — current session properties."""
111
+ url = self._build_endpoint("sessions", session_handle)
112
+ resp = self._client.get(url)
113
+ self._check_response(resp, "get session config")
114
+ return resp.json().get("properties", {})
115
+
116
+ def configure_session(
117
+ self,
118
+ session_handle: str,
119
+ request: ConfigureSessionRequest,
120
+ ) -> None:
121
+ """POST /sessions/{session_handle}/configure-session."""
122
+ url = self._build_endpoint("sessions", session_handle, "configure-session")
123
+ resp = self._client.post(url, json=request.to_dict())
124
+ self._check_response(resp, "configure session")
125
+
126
+ def heartbeat(self, session_handle: str) -> None:
127
+ """POST /sessions/{session_handle}/heartbeat."""
128
+ url = self._build_endpoint("sessions", session_handle, "heartbeat")
129
+ resp = self._client.post(url, json={})
130
+ self._check_response(resp, "heartbeat")
131
+
132
+ def complete_statement(
133
+ self,
134
+ session_handle: str,
135
+ request: CompleteStatementRequest,
136
+ ) -> list[str]:
137
+ """GET /sessions/{session_handle}/complete-statement.
138
+
139
+ Returns:
140
+ List of completion candidates.
141
+ """
142
+ url = self._build_endpoint("sessions", session_handle, "complete-statement")
143
+ # The Flink Gateway uses GET with a JSON body for this endpoint.
144
+ resp = self._client.request("GET", url, json=request.to_dict())
145
+ self._check_response(resp, "complete statement")
146
+ return resp.json().get("candidates", [])
147
+
148
+ # ── Statement execution ────────────────────────────────────────────
149
+
150
+ def execute_statement(
151
+ self,
152
+ session_handle: str,
153
+ request: ExecuteStatementRequest,
154
+ ) -> str:
155
+ """POST /sessions/{session_handle}/statements.
156
+
157
+ Returns:
158
+ The operation handle string.
159
+ """
160
+ url = self._build_endpoint("sessions", session_handle, "statements")
161
+ resp = self._client.post(url, json=request.to_dict())
162
+ self._check_response(resp, "execute statement")
163
+ return resp.json()["operationHandle"]
164
+
165
+ def fetch_results(
166
+ self,
167
+ session_handle: str,
168
+ operation_handle: str,
169
+ token: str,
170
+ row_format: str = "",
171
+ ) -> FetchResultsResponse:
172
+ """GET /sessions/{sh}/operations/{oh}/result/{token}.
173
+
174
+ Args:
175
+ session_handle: Session handle.
176
+ operation_handle: Operation handle.
177
+ token: Pagination token (``"0"`` for the first batch).
178
+ row_format: Optional row format (e.g. ``"json"``).
179
+
180
+ Returns:
181
+ Parsed :class:`FetchResultsResponse`.
182
+ """
183
+ url = self._build_endpoint(
184
+ "sessions",
185
+ session_handle,
186
+ "operations",
187
+ operation_handle,
188
+ "result",
189
+ token,
190
+ )
191
+ params: dict[str, str] = {}
192
+ if row_format:
193
+ params["rowFormat"] = row_format
194
+ resp = self._client.get(url, params=params)
195
+ self._check_response(resp, "fetch results")
196
+ return FetchResultsResponse.from_dict(resp.json())
197
+
198
+ # ── Operation management ───────────────────────────────────────────
199
+
200
+ def get_operation_status(
201
+ self,
202
+ session_handle: str,
203
+ operation_handle: str,
204
+ ) -> str:
205
+ """GET /sessions/{sh}/operations/{oh}/status.
206
+
207
+ Returns:
208
+ The status string (e.g. ``"RUNNING"``, ``"FINISHED"``).
209
+ """
210
+ url = self._build_endpoint(
211
+ "sessions",
212
+ session_handle,
213
+ "operations",
214
+ operation_handle,
215
+ "status",
216
+ )
217
+ resp = self._client.get(url)
218
+ self._check_response(resp, "get operation status")
219
+ return resp.json()["status"]
220
+
221
+ def cancel_operation(
222
+ self,
223
+ session_handle: str,
224
+ operation_handle: str,
225
+ ) -> str:
226
+ """POST /sessions/{sh}/operations/{oh}/cancel.
227
+
228
+ Returns:
229
+ The operation status after cancellation.
230
+ """
231
+ url = self._build_endpoint(
232
+ "sessions",
233
+ session_handle,
234
+ "operations",
235
+ operation_handle,
236
+ "cancel",
237
+ )
238
+ resp = self._client.post(url, json={})
239
+ self._check_response(resp, "cancel operation")
240
+ return resp.json()["status"]
241
+
242
+ def close_operation(
243
+ self,
244
+ session_handle: str,
245
+ operation_handle: str,
246
+ ) -> str:
247
+ """DELETE /sessions/{sh}/operations/{oh}.
248
+
249
+ Returns:
250
+ The operation status after closure.
251
+ """
252
+ url = self._build_endpoint(
253
+ "sessions",
254
+ session_handle,
255
+ "operations",
256
+ operation_handle,
257
+ )
258
+ resp = self._client.delete(url)
259
+ self._check_response(resp, "close operation")
260
+ return resp.json()["status"]
261
+
262
+ # ── Materialized tables ────────────────────────────────────────────
263
+
264
+ def refresh_materialized_table(
265
+ self,
266
+ session_handle: str,
267
+ identifier: str,
268
+ request: RefreshMaterializedTableRequest | None = None,
269
+ ) -> str:
270
+ """POST /sessions/{sh}/materialized-tables/{id}/refresh.
271
+
272
+ Returns:
273
+ The operation handle string.
274
+ """
275
+ url = self._build_endpoint(
276
+ "sessions",
277
+ session_handle,
278
+ "materialized-tables",
279
+ identifier,
280
+ "refresh",
281
+ )
282
+ body = request.to_dict() if request else {}
283
+ resp = self._client.post(url, json=body)
284
+ self._check_response(resp, "refresh materialized table")
285
+ return resp.json()["operationHandle"]
286
+
287
+ # ── Internal helpers ───────────────────────────────────────────────
288
+
289
+ @staticmethod
290
+ def _check_response(resp: httpx.Response, action: str) -> None:
291
+ if resp.status_code == 200:
292
+ return
293
+ detail = ""
294
+ try:
295
+ errors = resp.json().get("errors", [])
296
+ if errors:
297
+ detail = ": " + "; ".join(errors)
298
+ except (ValueError, KeyError):
299
+ pass
300
+ raise FlinkSqlGatewayError(
301
+ f"{action} failed: {resp.status_code} {resp.reason_phrase}{detail}"
302
+ )
@@ -0,0 +1,109 @@
1
+ """PEP 249 Connection implementation for the Flink SQL Gateway."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from flink_gateway.client import FlinkSqlGatewayClient
8
+ from flink_gateway.cursor import Cursor
9
+ from flink_gateway.exceptions import NotSupportedError, ProgrammingError
10
+ from flink_gateway.models import OpenSessionRequest
11
+
12
+
13
+ class Connection:
14
+ """PEP 249 Connection to a Flink SQL Gateway.
15
+
16
+ Do not instantiate directly; use :func:`flink_gateway.connect`.
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ client: FlinkSqlGatewayClient,
22
+ session_handle: str,
23
+ *,
24
+ _owns_client: bool = True,
25
+ ) -> None:
26
+ self._client = client
27
+ self._session_handle = session_handle
28
+ self._owns_client = _owns_client
29
+ self._closed = False
30
+
31
+ # ── Public read-only properties ────────────────────────────────
32
+
33
+ @property
34
+ def client(self) -> FlinkSqlGatewayClient:
35
+ """The underlying REST client."""
36
+ return self._client
37
+
38
+ @property
39
+ def session_handle(self) -> str:
40
+ """The session handle for this connection."""
41
+ return self._session_handle
42
+
43
+ @property
44
+ def closed(self) -> bool:
45
+ """Whether this connection has been closed."""
46
+ return self._closed
47
+
48
+ # ── Context manager ────────────────────────────────────────────
49
+
50
+ def __enter__(self) -> Connection:
51
+ return self
52
+
53
+ def __exit__(self, *_: Any) -> None:
54
+ self.close()
55
+
56
+ # ── PEP 249 interface ──────────────────────────────────────────
57
+
58
+ def close(self) -> None:
59
+ """Close the session and release resources."""
60
+ if not self._closed:
61
+ self._closed = True
62
+ try:
63
+ self._client.close_session(self._session_handle)
64
+ except Exception:
65
+ pass
66
+ if self._owns_client:
67
+ self._client.close()
68
+
69
+ def commit(self) -> None:
70
+ """No-op — Flink SQL Gateway does not support transactions."""
71
+
72
+ def rollback(self) -> None:
73
+ """Not supported — Flink SQL Gateway does not support transactions."""
74
+ raise NotSupportedError("Flink SQL Gateway does not support transactions")
75
+
76
+ def cursor(self) -> Cursor:
77
+ """Create a new Cursor bound to this connection."""
78
+ if self._closed:
79
+ raise ProgrammingError("connection is closed")
80
+ return Cursor(self)
81
+
82
+
83
+ def connect(
84
+ url: str,
85
+ *,
86
+ properties: dict[str, str] | None = None,
87
+ api_version: str = "v3",
88
+ ) -> Connection:
89
+ """Open a connection to a Flink SQL Gateway.
90
+
91
+ This is the PEP 249 module-level ``connect()`` function.
92
+
93
+ Args:
94
+ url: Gateway URL, e.g. ``"http://localhost:8083"``.
95
+ properties: Optional session properties.
96
+ api_version: REST API version (default ``"v3"``).
97
+
98
+ Returns:
99
+ A :class:`Connection` instance.
100
+ """
101
+ client = FlinkSqlGatewayClient(url, api_version=api_version)
102
+ try:
103
+ session_handle = client.open_session(
104
+ OpenSessionRequest(properties=properties) if properties else None
105
+ )
106
+ except Exception:
107
+ client.close()
108
+ raise
109
+ return Connection(client, session_handle, _owns_client=True)