gooddata-flight-server 1.34.1.dev1__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.

Potentially problematic release.


This version of gooddata-flight-server might be problematic. Click here for more details.

Files changed (49) hide show
  1. gooddata_flight_server/__init__.py +23 -0
  2. gooddata_flight_server/_version.py +7 -0
  3. gooddata_flight_server/cli.py +137 -0
  4. gooddata_flight_server/config/__init__.py +1 -0
  5. gooddata_flight_server/config/config.py +536 -0
  6. gooddata_flight_server/errors/__init__.py +1 -0
  7. gooddata_flight_server/errors/error_code.py +209 -0
  8. gooddata_flight_server/errors/error_info.py +475 -0
  9. gooddata_flight_server/exceptions.py +16 -0
  10. gooddata_flight_server/health/__init__.py +1 -0
  11. gooddata_flight_server/health/health_check_http_server.py +103 -0
  12. gooddata_flight_server/health/server_health_monitor.py +83 -0
  13. gooddata_flight_server/metrics.py +16 -0
  14. gooddata_flight_server/py.typed +1 -0
  15. gooddata_flight_server/server/__init__.py +1 -0
  16. gooddata_flight_server/server/auth/__init__.py +1 -0
  17. gooddata_flight_server/server/auth/auth_middleware.py +83 -0
  18. gooddata_flight_server/server/auth/token_verifier.py +62 -0
  19. gooddata_flight_server/server/auth/token_verifier_factory.py +55 -0
  20. gooddata_flight_server/server/auth/token_verifier_impl.py +41 -0
  21. gooddata_flight_server/server/base.py +63 -0
  22. gooddata_flight_server/server/default.logging.ini +28 -0
  23. gooddata_flight_server/server/flight_rpc/__init__.py +1 -0
  24. gooddata_flight_server/server/flight_rpc/flight_middleware.py +162 -0
  25. gooddata_flight_server/server/flight_rpc/flight_server.py +228 -0
  26. gooddata_flight_server/server/flight_rpc/flight_service.py +279 -0
  27. gooddata_flight_server/server/flight_rpc/server_methods.py +200 -0
  28. gooddata_flight_server/server/server_base.py +321 -0
  29. gooddata_flight_server/server/server_main.py +116 -0
  30. gooddata_flight_server/tasks/__init__.py +1 -0
  31. gooddata_flight_server/tasks/base.py +21 -0
  32. gooddata_flight_server/tasks/metrics.py +115 -0
  33. gooddata_flight_server/tasks/task.py +193 -0
  34. gooddata_flight_server/tasks/task_error.py +60 -0
  35. gooddata_flight_server/tasks/task_executor.py +96 -0
  36. gooddata_flight_server/tasks/task_result.py +363 -0
  37. gooddata_flight_server/tasks/temporal_container.py +247 -0
  38. gooddata_flight_server/tasks/thread_task_executor.py +639 -0
  39. gooddata_flight_server/utils/__init__.py +1 -0
  40. gooddata_flight_server/utils/libc_utils.py +35 -0
  41. gooddata_flight_server/utils/logging.py +158 -0
  42. gooddata_flight_server/utils/methods_discovery.py +98 -0
  43. gooddata_flight_server/utils/otel_tracing.py +142 -0
  44. gooddata_flight_server-1.34.1.dev1.data/scripts/gooddata-flight-server +10 -0
  45. gooddata_flight_server-1.34.1.dev1.dist-info/LICENSE.txt +7 -0
  46. gooddata_flight_server-1.34.1.dev1.dist-info/METADATA +749 -0
  47. gooddata_flight_server-1.34.1.dev1.dist-info/RECORD +49 -0
  48. gooddata_flight_server-1.34.1.dev1.dist-info/WHEEL +5 -0
  49. gooddata_flight_server-1.34.1.dev1.dist-info/top_level.txt +1 -0
@@ -0,0 +1 @@
1
+ # (C) 2024 GoodData Corporation
@@ -0,0 +1,209 @@
1
+ # (C) 2024 GoodData Corporation
2
+ from typing import Any, Literal, Optional
3
+
4
+ from typing_extensions import TypeAlias
5
+
6
+ _CUSTOM_ERROR_START = 1000
7
+ _ERROR_CODE_MASK = 0x0000_FFFF
8
+ _ERROR_FLAGS_MASK = 0xFFFF_0000
9
+
10
+ _RETRY_HERE_FLAG = 0x0001_0000
11
+ """
12
+ Request that failed should be retried on the same node where the call failed
13
+ """
14
+
15
+ _RETRY_OTHER_FLAG = 0x0002_0000
16
+ """
17
+ Request that failed should be retried on other applicable nodes.
18
+ """
19
+
20
+ _RETRY_ANY_FLAG = 0x0003_0000
21
+ """
22
+ Request that failed can be retried either on this node or any other applicable node.
23
+ """
24
+
25
+
26
+ def _init_names(cls: Any) -> Any:
27
+ cls._init_names()
28
+ return cls
29
+
30
+
31
+ _RetryFlagsLiteral: TypeAlias = Literal["any", "other", "here"]
32
+ _FlagsMapping: dict[_RetryFlagsLiteral, int] = {
33
+ "any": _RETRY_ANY_FLAG,
34
+ "here": _RETRY_HERE_FLAG,
35
+ "other": _RETRY_OTHER_FLAG,
36
+ }
37
+
38
+
39
+ def _get_flags(retry: Optional[_RetryFlagsLiteral] = None) -> int:
40
+ if retry is None:
41
+ return 0x0
42
+ return _FlagsMapping.get(retry, 0x0)
43
+
44
+
45
+ def _error_code(code: int, retry: Optional[_RetryFlagsLiteral] = None) -> int:
46
+ return _get_flags(retry) | code
47
+
48
+
49
+ # TODO: revisit codes
50
+ @_init_names
51
+ class ErrorCode:
52
+ """
53
+ Error codes for different failures that may occur while servicing requests.
54
+ """
55
+
56
+ UNKNOWN = 0
57
+ """
58
+ Unknown error. Probably something very bad happened on the server. The message and detail of the error should
59
+ contain diagnostic information.
60
+ """
61
+
62
+ BAD_ARGUMENT = _error_code(1)
63
+ """
64
+ User error: input received from the client failed server-side validations.
65
+ """
66
+
67
+ BAD_REQUEST = _error_code(2)
68
+ """
69
+ User error: client sent request to a node that just does not know how to deal with it at all.
70
+ """
71
+
72
+ UNSUPPORTED_SERVICE = _error_code(3)
73
+ """
74
+ User error: client wants to generate data using a service that is not running on the contacted server.
75
+ """
76
+
77
+ INVALID_TICKET = _error_code(4)
78
+ """
79
+ User error: client wants to get data using an invalid ticket.
80
+ """
81
+
82
+ COMMAND_FAILED = _error_code(5)
83
+ """
84
+ Service run into an error while performing the command.
85
+ """
86
+
87
+ COMMAND_CANCELLED = _error_code(6)
88
+ """
89
+ Command was cancelled by the user.
90
+ """
91
+
92
+ COMMAND_CANCEL_NOT_POSSIBLE = _error_code(7)
93
+ """
94
+ User wanted to cancel the command but it was not possible - probably because the
95
+ command already finished or is at a stage where it cannot be cancelled.
96
+ """
97
+
98
+ COMMAND_RESULT_CONSUMED = _error_code(8)
99
+ """
100
+ User wanted to cancel the command but it was not possible - probably because the
101
+ command already finished or is at a stage where it cannot be cancelled.
102
+ """
103
+
104
+ TIMEOUT = _error_code(100)
105
+ """
106
+ Request has timed out.
107
+ """
108
+
109
+ POLL = _error_code(101, retry="here")
110
+ """
111
+ Request has timed out but can be retried on the same node using the contents of RetryInfo,
112
+ which are serialized and stored in the error body.
113
+ """
114
+
115
+ BACKPRESSURE = _error_code(102, retry="any")
116
+ """
117
+ Server dropped the request as it is currently in state where it must exert backpressure.
118
+ """
119
+
120
+ INTERNAL_ERROR = _error_code(500, retry="other")
121
+ """
122
+ Internal server error.
123
+ """
124
+
125
+ NOT_READY = _error_code(501, retry="any")
126
+ """
127
+ Server is not yet ready to serve requests.
128
+ """
129
+
130
+ SHUTTING_DOWN = _error_code(502, retry="other")
131
+ """
132
+ Server is shutting down.
133
+ """
134
+
135
+ NOT_IMPLEMENTED = _error_code(503)
136
+ """
137
+ Functionality not implemented
138
+ """
139
+
140
+ @classmethod
141
+ def name(cls, code: int) -> str:
142
+ """
143
+ Gets error name for the provided code. This may be used for debug purposes or when printing
144
+ somewhat more meaningful error messages to the end users.
145
+
146
+ :param code: error code
147
+ :return: error name
148
+ """
149
+ if code < 0:
150
+ return f"unknown ({code})"
151
+
152
+ status = code & _ERROR_CODE_MASK
153
+ res = cls._GENERATED_NAMES.get(status, "unknown") # type: ignore
154
+
155
+ if status >= _CUSTOM_ERROR_START:
156
+ res = "custom"
157
+
158
+ return f"{res} ({status})"
159
+
160
+ @classmethod
161
+ def is_retry_here(cls, code: int) -> bool:
162
+ """
163
+ :param code: error code
164
+ :return: true if the request can be retried either on the node where it originally failed
165
+ or on any other applicable node
166
+ """
167
+ return code & _RETRY_HERE_FLAG == _RETRY_HERE_FLAG
168
+
169
+ @classmethod
170
+ def is_retry_other(cls, code: int) -> bool:
171
+ """
172
+ :param code: error code
173
+ :return: true if the request can be retried, but only on other nodes - e.g. not on the same
174
+ node where it just failed
175
+ """
176
+ return code & _RETRY_OTHER_FLAG == _RETRY_OTHER_FLAG
177
+
178
+ @classmethod
179
+ def is_retry_any(cls, code: int) -> bool:
180
+ """
181
+ :param code: error code
182
+ :return: true if the request can be retried on any node; both the original node where the
183
+ call failed and the other applicable are valid candidates
184
+ """
185
+ return code & _RETRY_ANY_FLAG == _RETRY_ANY_FLAG
186
+
187
+ @classmethod
188
+ def is_retryable(cls, code: int) -> bool:
189
+ """
190
+ :param code: error code
191
+ :return: true if the request can be retried - either on the node where it originally failed
192
+ or on other applicable nodes
193
+ """
194
+ return code & _RETRY_ANY_FLAG > 0
195
+
196
+ @classmethod
197
+ def _init_names(cls) -> None:
198
+ names = {}
199
+ codes = {}
200
+
201
+ for key in dir(cls):
202
+ value = getattr(cls, key)
203
+ if isinstance(value, int) and not key.startswith("_"):
204
+ status = value & _ERROR_CODE_MASK
205
+ names[status] = key.lower()
206
+ codes[key] = value
207
+
208
+ setattr(cls, "_GENERATED_NAMES", names)
209
+ setattr(cls, "_CODE_MAPPING", codes)
@@ -0,0 +1,475 @@
1
+ # (C) 2024 GoodData Corporation
2
+ import base64
3
+ import traceback
4
+ from typing import Callable, Optional, Union
5
+
6
+ import orjson
7
+ import pyarrow.flight
8
+
9
+ from gooddata_flight_server.errors.error_code import ErrorCode
10
+
11
+
12
+ class ErrorInfo:
13
+ """
14
+ Error info that should be used to construct Flight RPC errors. Never create Flight RPC Errors directly, instead use
15
+ this class to construct a detailed error and then use one of the to_*_error() factories.
16
+ """
17
+
18
+ def __init__(
19
+ self,
20
+ msg: str,
21
+ detail: Optional[str] = None,
22
+ body: Optional[bytes] = None,
23
+ code: int = 0,
24
+ ) -> None:
25
+ self._msg = msg
26
+ self._detail: Optional[str] = detail
27
+ self._body: Optional[bytes] = body
28
+ self._code: int = code
29
+
30
+ @property
31
+ def msg(self) -> str:
32
+ """
33
+ :return: human readable error message
34
+ """
35
+ return self._msg
36
+
37
+ @property
38
+ def code(self) -> int:
39
+ """
40
+ :return: error code for categorization; see ErrorCode for more
41
+ """
42
+ return self._code
43
+
44
+ @property
45
+ def detail(self) -> Optional[str]:
46
+ """
47
+ :return: a human-readable error detail; if included may help with error diagnostics
48
+ """
49
+ return self._detail
50
+
51
+ @property
52
+ def body(self) -> Optional[bytes]:
53
+ """
54
+ :return: error body, suitable for programmatic consumption; used by server to send structured information
55
+ which the client code may want to work with
56
+ """
57
+ return self._body
58
+
59
+ def with_msg(self, msg: str) -> "ErrorInfo":
60
+ """
61
+ Updates error message.
62
+
63
+ :param msg: new message
64
+ :return: self, for call chaining sakes
65
+ """
66
+ self._msg = msg
67
+ return self
68
+
69
+ def with_detail(self, detail: Optional[str] = None) -> "ErrorInfo":
70
+ """
71
+ Updates or resets the error detail.
72
+
73
+ :param detail: detail to set; if None, the detail stored in the meta will be removed; default is None
74
+ :return: self, for call chaining sakes
75
+ """
76
+ self._detail = detail
77
+ return self
78
+
79
+ def with_body(self, body: Optional[Union[bytes, str]]) -> "ErrorInfo":
80
+ """
81
+ Updates or resets the error body.
82
+
83
+ :param body: body to set; if None, the body stored in the meta will be removed; default is None
84
+ :return: self, for call chaining sakes
85
+ """
86
+ if isinstance(body, str):
87
+ self._body = body.encode("utf-8")
88
+ else:
89
+ self._body = body
90
+
91
+ return self
92
+
93
+ def with_code(self, code: int = 0) -> "ErrorInfo":
94
+ """
95
+ Updates or resets the error code.
96
+
97
+ Resetting error code means setting the code to 0 - the error code for unknown errors.
98
+
99
+ :param code: code to set; default is 0
100
+ :return: self, for call chaining sakes
101
+ """
102
+ self._code = code
103
+ return self
104
+
105
+ def to_bytes(self) -> bytes:
106
+ """
107
+ :return: binary representation of the meta; this can be sent over the wire (for instance in extra_info)
108
+ """
109
+ _json = {
110
+ "msg": self._msg,
111
+ "detail": self._detail,
112
+ "code": self._code,
113
+ "body": base64.b64encode(self._body).decode("ascii") if self._body else None,
114
+ }
115
+
116
+ return orjson.dumps(_json)
117
+
118
+ def to_user_error(self) -> pyarrow.flight.FlightError:
119
+ """
120
+ Returns this error meta wrapped into a flight error that communicates failure due to bad user input.
121
+
122
+ Since the Flight RPC does not have a dedicated user error, this method creates a FlightServerError.
123
+ For a long time the code was creating FlightCancelledError - which somewhat fits (server cancels
124
+ request because it is wrong) - it is saner to only use FlightCancelledError for the actual
125
+ cancellation as it is described by Flight RPC itself.
126
+
127
+ :return: this error meta wrapped into a Flight Error that communicates user error
128
+ """
129
+ return pyarrow.flight.FlightServerError(self.msg, extra_info=self.to_bytes())
130
+
131
+ def to_server_error(self) -> pyarrow.flight.FlightServerError:
132
+ """
133
+ :return: this error meta wrapped into a FlightServerError
134
+ """
135
+ return pyarrow.flight.FlightServerError(self.msg, extra_info=self.to_bytes())
136
+
137
+ def to_timeout_error(self) -> pyarrow.flight.FlightTimedOutError:
138
+ """
139
+ :return: this error meta wrapped into a FlightTimedOutError
140
+ """
141
+ return pyarrow.flight.FlightTimedOutError(self.msg, extra_info=self.to_bytes())
142
+
143
+ def to_internal_error(self) -> pyarrow.flight.FlightInternalError:
144
+ """
145
+ :return: this error meta wrapped into a FlightInternalError
146
+ """
147
+ return pyarrow.flight.FlightInternalError(self.msg, extra_info=self.to_bytes())
148
+
149
+ def to_cancelled_error(self) -> pyarrow.flight.FlightCancelledError:
150
+ """
151
+ :return: this error meta wrapped into a FlightCancelledError
152
+ """
153
+ return pyarrow.flight.FlightCancelledError(self.msg, extra_info=self.to_bytes())
154
+
155
+ def to_unavailable_error(self) -> pyarrow.flight.FlightUnavailableError:
156
+ """
157
+ :return: this error meta wrapped into a FlightUnavailableError
158
+ """
159
+ return pyarrow.flight.FlightUnavailableError(self.msg, extra_info=self.to_bytes())
160
+
161
+ def to_unauthenticated_error(
162
+ self,
163
+ ) -> pyarrow.flight.FlightUnauthenticatedError:
164
+ """
165
+ :return: this error meta wrapped into a FlightUnauthenticatedError
166
+ """
167
+ return pyarrow.flight.FlightUnauthenticatedError(self.msg, extra_info=self.to_bytes())
168
+
169
+ def to_unauthorized_error(self) -> pyarrow.flight.FlightUnauthorizedError:
170
+ """
171
+ :return: this error meta wrapped into a FlightUnauthorizedError
172
+ """
173
+ return pyarrow.flight.FlightUnauthorizedError(self.msg, extra_info=self.to_bytes())
174
+
175
+ def to_flight_error(
176
+ self,
177
+ error_factory: Callable[[str, Optional[bytes]], pyarrow.flight.FlightError],
178
+ ) -> pyarrow.flight.FlightError:
179
+ """
180
+ Uses the provided error factory - which can be for example the FlightError subclass, to create an
181
+ exception.
182
+
183
+ :param error_factory: factory to create FlightError
184
+ :return: new instance of error
185
+ """
186
+ return error_factory(self.msg, self.to_bytes())
187
+
188
+ @staticmethod
189
+ def from_bytes(val: bytes) -> "ErrorInfo":
190
+ """
191
+ Read error metadata from binary representation.
192
+
193
+ :param val: binary representation
194
+ :return: new error meta
195
+ """
196
+ try:
197
+ _json = orjson.loads(val)
198
+ body: Optional[bytes] = base64.b64decode(_json["body"]) if _json.get("body") is not None else None
199
+
200
+ return ErrorInfo(
201
+ msg=_json.get("msg"),
202
+ detail=_json.get("detail"),
203
+ body=body,
204
+ code=_json.get("code"),
205
+ )
206
+ except Exception as e:
207
+ raise ValueError(f"Unable to parser ErrorInfo from binary representation: '{str(e)}'.")
208
+
209
+ @staticmethod
210
+ def from_pyarrow_error(error: pyarrow.flight.FlightError) -> "ErrorInfo":
211
+ """
212
+ Reads ErrorInfo that is serialized inside the provided PyArrows FlightError (extra_info).
213
+ If the extra error info is not included or is malformed, a placeholder with error code unknown will
214
+ be returned.
215
+
216
+ :param error: flight error
217
+ :return: new instance
218
+ """
219
+ extra_info = error.extra_info
220
+
221
+ if extra_info is None or not len(extra_info):
222
+ return ErrorInfo(
223
+ msg=f"Call failed with error that does not contain ErrorInfo. The error message was: {str(error)}"
224
+ )
225
+
226
+ try:
227
+ return ErrorInfo.from_bytes(extra_info)
228
+ except Exception:
229
+ return ErrorInfo(
230
+ msg=f"Call failed with error that does not contain ErrorInfo. The error message was: {str(error)}"
231
+ )
232
+
233
+ @staticmethod
234
+ def maybe_from_pyarrow_error(
235
+ error: pyarrow.flight.FlightError,
236
+ ) -> Optional["ErrorInfo"]:
237
+ """
238
+ Reads ErrorInfo that may be serialized inside the provided PyArrow's FlightError (in extra_info).
239
+ This method will return None if the error info is not present.
240
+
241
+ :param error: flight error
242
+ :return: new instance, None if the flight error info is not included
243
+ """
244
+ extra_info = error.extra_info
245
+
246
+ if extra_info is None or not len(extra_info):
247
+ # careful: when there is no extra info available, PyArrow will return
248
+ # empty bytes. testing for both scenarios just in case
249
+ return None
250
+
251
+ try:
252
+ return ErrorInfo.from_bytes(extra_info)
253
+ except Exception:
254
+ return None
255
+
256
+ @staticmethod
257
+ def for_exc(
258
+ code: int,
259
+ e: BaseException,
260
+ extra_msg: Optional[str] = None,
261
+ include_traceback: bool = True,
262
+ ) -> "ErrorInfo":
263
+ """
264
+ A convenience factory which creates error meta as a response to some exception happening.
265
+
266
+ The meta msg will be set to message included in the exception; this can be optionally augmented
267
+ using extra_msg parameter. If extra_msg is present then:
268
+
269
+ - if the message in the exception is empty, the extra msg will be used as-is
270
+ - if the message in the exception is not empty, the extra msg will serve as prefix, or a lead in
271
+ and the resulting msg in meta will be in format "extra_msg: exception_msg"
272
+
273
+ Note: the above implies that it is better not to include trailing punctuation in the extra_msg.
274
+
275
+ The traceback from the exception will be included automatically - unless you pass the include_traceback
276
+ as False.
277
+
278
+ :param code: error code
279
+ :param e: exception that caused this whole unfortunate situation
280
+ :param extra_msg: extra message to using during construction of error msg (see method docs for more info)
281
+ :param include_traceback: whether to include exception traceback as the error detail; default is true
282
+ :return: new error meta
283
+ """
284
+ msg = str(e)
285
+ if not len(msg) and extra_msg is not None:
286
+ msg = extra_msg + "."
287
+ elif len(msg) and extra_msg is not None:
288
+ msg = f"{extra_msg}: {msg}"
289
+
290
+ if include_traceback:
291
+ detail: Optional[str] = "".join(traceback.format_exception(None, e, e.__traceback__))
292
+ else:
293
+ detail = None
294
+
295
+ return ErrorInfo(msg=msg, detail=detail, code=code)
296
+
297
+ @staticmethod
298
+ def for_reason(code: int, msg: str) -> "ErrorInfo":
299
+ """
300
+ A convenience factory which creates error meta that should be included in errors that fail
301
+ request for some arbitrary reason. For example server does input validation and finds
302
+ it incorrect.
303
+
304
+ :param code: error code
305
+ :param msg: error message, will be used as-is
306
+ :return: new error meta
307
+ """
308
+ return ErrorInfo(msg=msg, code=code)
309
+
310
+ @staticmethod
311
+ def for_response(code: int, msg: str, body: bytes) -> "ErrorInfo":
312
+ """
313
+ A convenience factory which creates error meta that should be included in errors for which
314
+ server wants to send some structured body to the client - so that client can inspect the body
315
+ and act accordingly.
316
+
317
+ For example client comes to get data with some ticket. The request times out and the server
318
+ wants to tell the client to retry the DoGet later but with a different ticket.
319
+
320
+ :param code: error code
321
+ :param msg: error message, will be used as-is
322
+ :param body: response body
323
+ :return: new error meta
324
+ """
325
+ return ErrorInfo(msg=msg, body=body, code=code)
326
+
327
+ @staticmethod
328
+ def bad_argument(msg: str) -> pyarrow.flight.FlightError:
329
+ """
330
+ A convenience factory that creates a flight error as a response to bad input received from the user.
331
+
332
+ :param msg: message to use as-is in the error
333
+ :return: an instance of FlightError - ready to raise
334
+ """
335
+ return ErrorInfo(msg=msg, code=ErrorCode.BAD_ARGUMENT).to_user_error()
336
+
337
+ @staticmethod
338
+ def poll(
339
+ flight_info: Optional[pyarrow.flight.FlightInfo] = None,
340
+ retry_descriptor: Optional[pyarrow.flight.FlightDescriptor] = None,
341
+ cancel_descriptor: Optional[pyarrow.flight.FlightDescriptor] = None,
342
+ ) -> pyarrow.flight.FlightTimedOutError:
343
+ """
344
+ Convenience factory that creates FlightTimedOut error with POLL error code and `RetryInfo` which
345
+ includes the provided values.
346
+
347
+ :param flight_info: flight info available so far
348
+ :param retry_descriptor: descriptor to use for retry
349
+ :param cancel_descriptor: descriptor to use for cancellation
350
+ :return: new flight error - ready to raise
351
+ """
352
+ retry_info = RetryInfo(
353
+ flight_info=flight_info,
354
+ retry_descriptor=retry_descriptor,
355
+ cancel_descriptor=cancel_descriptor,
356
+ )
357
+
358
+ return ErrorInfo(
359
+ msg="Work in progress. Retry.",
360
+ code=ErrorCode.POLL,
361
+ body=retry_info.to_bytes(),
362
+ ).to_timeout_error()
363
+
364
+
365
+ class RetryInfo:
366
+ """
367
+ This message is included in the body of ErrorInfo when the GetFlightInfo fails with
368
+ 'POLL' error code.
369
+
370
+ This happens for long-running commands which are submitted via GetFlightInfo
371
+ and take arbitrary time to complete. Clients typically do not want to wait forever
372
+ and instead poll on the status of the work
373
+
374
+ It includes the information necessary for the client to either continue or cancel.
375
+ """
376
+
377
+ def __init__(
378
+ self,
379
+ flight_info: Optional[pyarrow.flight.FlightInfo] = None,
380
+ retry_descriptor: Optional[pyarrow.flight.FlightDescriptor] = None,
381
+ cancel_descriptor: Optional[pyarrow.flight.FlightDescriptor] = None,
382
+ ) -> None:
383
+ self._flight_info = flight_info
384
+ self._retry_descriptor = retry_descriptor
385
+ self._cancel_descriptor = cancel_descriptor
386
+
387
+ @property
388
+ def flight_info(self) -> Optional[pyarrow.flight.FlightInfo]:
389
+ """
390
+ FlightInfo available at the time of the poll timeout. The information
391
+ may be incomplete. The full FlightInfo is built in cumulative fashion - the subsequent
392
+ retries will add more information as it becomes available.
393
+
394
+ Note: this is always full image of the FlightInfo available at the moment. It is
395
+ not a delta containing just the new information available since the last call.
396
+
397
+ :return: flight info
398
+ """
399
+ return self._flight_info
400
+
401
+ @property
402
+ def retry_descriptor(self) -> Optional[pyarrow.flight.FlightDescriptor]:
403
+ """
404
+ Returns descriptor that the client should use to retry the GetFlightInfo call
405
+ in order to see whether the command has completed.
406
+
407
+ Note: the retry descriptor will not be present if the command has completed.
408
+
409
+ :return: flight descriptor if retry is possible
410
+ """
411
+ return self._retry_descriptor
412
+
413
+ @property
414
+ def cancel_descriptor(self) -> Optional[pyarrow.flight.FlightDescriptor]:
415
+ """
416
+ Returns descriptor that the client can use to cancel the command that is
417
+ in progress.
418
+
419
+ Note: the retry descriptor will not be present if the command is not cancellable.
420
+
421
+ :return: flight descriptor if cancellation is possible
422
+ """
423
+ return self._cancel_descriptor
424
+
425
+ def to_bytes(self) -> bytes:
426
+ """
427
+ :return: binary representation of the retry info
428
+ """
429
+ _json = {
430
+ "flight_info": base64.b64encode(self._flight_info.serialize()).decode("ascii")
431
+ if self._flight_info is not None
432
+ else None,
433
+ "retry_descriptor": base64.b64encode(self._retry_descriptor.serialize()).decode("ascii")
434
+ if self._retry_descriptor is not None
435
+ else None,
436
+ "cancel_descriptor": base64.b64encode(self._cancel_descriptor.serialize()).decode("ascii")
437
+ if self._cancel_descriptor is not None
438
+ else None,
439
+ }
440
+
441
+ return orjson.dumps(_json)
442
+
443
+ @staticmethod
444
+ def from_bytes(val: bytes) -> "RetryInfo":
445
+ """
446
+ Reads the retry info from binary representation, as received in the error body.
447
+
448
+ :param val: binary representation
449
+ :return: new instance of retry info
450
+ """
451
+ _json = orjson.loads(val)
452
+ try:
453
+ flight_info = (
454
+ pyarrow.flight.FlightDescriptor.deserialize(base64.b64decode(_json["flight_info"]))
455
+ if _json.get("flight_info") is not None
456
+ else None
457
+ )
458
+ retry_descriptor = (
459
+ pyarrow.flight.FlightDescriptor.deserialize(base64.b64decode(_json["retry_descriptor"]))
460
+ if _json.get("retry_descriptor") is not None
461
+ else None
462
+ )
463
+ cancel_descriptor = (
464
+ pyarrow.flight.FlightDescriptor.deserialize(base64.b64decode(_json["cancel_descriptor"]))
465
+ if _json.get("cancel_descriptor") is not None
466
+ else None
467
+ )
468
+
469
+ return RetryInfo(
470
+ flight_info=flight_info,
471
+ retry_descriptor=retry_descriptor,
472
+ cancel_descriptor=cancel_descriptor,
473
+ )
474
+ except Exception as e:
475
+ raise ValueError(f"Unable to decode retry info from binary representation: {str(e)}")
@@ -0,0 +1,16 @@
1
+ # (C) 2024 GoodData Corporation
2
+
3
+
4
+ class ServerStartupInterrupted(Exception):
5
+ """
6
+ This exception is thrown when server startup was interrupted due to unrecoverable condition. Different init and
7
+ preparation logic may raise this to stop the server startup. The message included in the exception will be
8
+ printed to stderr and server stops with exit code 1.
9
+ """
10
+
11
+
12
+ class FlightMethodsModuleError(ServerStartupInterrupted):
13
+ """
14
+ This exception is thrown whenever there is an error related the flight methods module: loading the module, finding
15
+ the factory, validating the factory, etc.
16
+ """