feldera 0.162.0__tar.gz → 0.164.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.
Potentially problematic release.
This version of feldera might be problematic. Click here for more details.
- {feldera-0.162.0 → feldera-0.164.0}/PKG-INFO +1 -1
- feldera-0.164.0/feldera/_callback_runner.py +69 -0
- {feldera-0.162.0 → feldera-0.164.0}/feldera/enums.py +21 -13
- {feldera-0.162.0 → feldera-0.164.0}/feldera/output_handler.py +13 -1
- {feldera-0.162.0 → feldera-0.164.0}/feldera/pipeline.py +112 -33
- {feldera-0.162.0 → feldera-0.164.0}/feldera/rest/_helpers.py +5 -5
- {feldera-0.162.0 → feldera-0.164.0}/feldera/rest/_httprequests.py +4 -4
- {feldera-0.162.0 → feldera-0.164.0}/feldera/rest/config.py +1 -1
- {feldera-0.162.0 → feldera-0.164.0}/feldera/rest/feldera_client.py +134 -71
- {feldera-0.162.0 → feldera-0.164.0}/feldera/rest/pipeline.py +1 -0
- {feldera-0.162.0 → feldera-0.164.0}/feldera.egg-info/PKG-INFO +1 -1
- {feldera-0.162.0 → feldera-0.164.0}/pyproject.toml +1 -1
- feldera-0.162.0/feldera/_callback_runner.py +0 -64
- {feldera-0.162.0 → feldera-0.164.0}/README.md +0 -0
- {feldera-0.162.0 → feldera-0.164.0}/feldera/__init__.py +0 -0
- {feldera-0.162.0 → feldera-0.164.0}/feldera/_helpers.py +0 -0
- {feldera-0.162.0 → feldera-0.164.0}/feldera/pipeline_builder.py +0 -0
- {feldera-0.162.0 → feldera-0.164.0}/feldera/rest/__init__.py +0 -0
- {feldera-0.162.0 → feldera-0.164.0}/feldera/rest/errors.py +0 -0
- {feldera-0.162.0 → feldera-0.164.0}/feldera/rest/feldera_config.py +0 -0
- {feldera-0.162.0 → feldera-0.164.0}/feldera/rest/sql_table.py +0 -0
- {feldera-0.162.0 → feldera-0.164.0}/feldera/rest/sql_view.py +0 -0
- {feldera-0.162.0 → feldera-0.164.0}/feldera/runtime_config.py +0 -0
- {feldera-0.162.0 → feldera-0.164.0}/feldera/stats.py +0 -0
- {feldera-0.162.0 → feldera-0.164.0}/feldera/tests/test_datafusionize.py +0 -0
- {feldera-0.162.0 → feldera-0.164.0}/feldera/testutils.py +0 -0
- {feldera-0.162.0 → feldera-0.164.0}/feldera/testutils_oidc.py +0 -0
- {feldera-0.162.0 → feldera-0.164.0}/feldera.egg-info/SOURCES.txt +0 -0
- {feldera-0.162.0 → feldera-0.164.0}/feldera.egg-info/dependency_links.txt +0 -0
- {feldera-0.162.0 → feldera-0.164.0}/feldera.egg-info/requires.txt +0 -0
- {feldera-0.162.0 → feldera-0.164.0}/feldera.egg-info/top_level.txt +0 -0
- {feldera-0.162.0 → feldera-0.164.0}/setup.cfg +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from threading import Thread
|
|
2
|
+
from typing import Callable, Optional
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from feldera import FelderaClient
|
|
6
|
+
from feldera._helpers import dataframe_from_response
|
|
7
|
+
from feldera.enums import PipelineFieldSelector
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CallbackRunner(Thread):
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
client: FelderaClient,
|
|
14
|
+
pipeline_name: str,
|
|
15
|
+
view_name: str,
|
|
16
|
+
callback: Callable[[pd.DataFrame, int], None],
|
|
17
|
+
exception_callback: Callable[[BaseException], None],
|
|
18
|
+
):
|
|
19
|
+
super().__init__()
|
|
20
|
+
self.daemon = True
|
|
21
|
+
self.client: FelderaClient = client
|
|
22
|
+
self.pipeline_name: str = pipeline_name
|
|
23
|
+
self.view_name: str = view_name
|
|
24
|
+
self.callback: Callable[[pd.DataFrame, int], None] = callback
|
|
25
|
+
self.exception_callback: Callable[[BaseException], None] = exception_callback
|
|
26
|
+
self.schema: Optional[dict] = None
|
|
27
|
+
|
|
28
|
+
def run(self):
|
|
29
|
+
"""
|
|
30
|
+
The main loop of the thread. Listens for data and calls the callback function on each chunk of data received.
|
|
31
|
+
|
|
32
|
+
:meta private:
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
pipeline = self.client.get_pipeline(
|
|
37
|
+
self.pipeline_name, PipelineFieldSelector.ALL
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
schemas = pipeline.tables + pipeline.views
|
|
41
|
+
for schema in schemas:
|
|
42
|
+
if schema.name == self.view_name:
|
|
43
|
+
self.schema = schema
|
|
44
|
+
break
|
|
45
|
+
|
|
46
|
+
if self.schema is None:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"Table or View {self.view_name} not found in the pipeline schema."
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
gen_obj = self.client.listen_to_pipeline(
|
|
52
|
+
self.pipeline_name,
|
|
53
|
+
self.view_name,
|
|
54
|
+
format="json",
|
|
55
|
+
case_sensitive=self.schema.case_sensitive,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
iterator = gen_obj()
|
|
59
|
+
|
|
60
|
+
for chunk in iterator:
|
|
61
|
+
chunk: dict = chunk
|
|
62
|
+
data: Optional[list[dict]] = chunk.get("json_data")
|
|
63
|
+
seq_no: Optional[int] = chunk.get("sequence_number")
|
|
64
|
+
if data is not None and seq_no is not None:
|
|
65
|
+
self.callback(
|
|
66
|
+
dataframe_from_response([data], self.schema.fields), seq_no
|
|
67
|
+
)
|
|
68
|
+
except BaseException as e:
|
|
69
|
+
self.exception_callback(e)
|
|
@@ -122,12 +122,13 @@ class DeploymentRuntimeStatus(Enum):
|
|
|
122
122
|
|
|
123
123
|
UNAVAILABLE = 0
|
|
124
124
|
STANDBY = 1
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
125
|
+
AWAITINGAPPROVAL = 2
|
|
126
|
+
INITIALIZING = 3
|
|
127
|
+
BOOTSTRAPPING = 4
|
|
128
|
+
REPLAYING = 5
|
|
129
|
+
PAUSED = 6
|
|
130
|
+
RUNNING = 7
|
|
131
|
+
SUSPENDED = 8
|
|
131
132
|
|
|
132
133
|
@staticmethod
|
|
133
134
|
def from_str(value):
|
|
@@ -149,13 +150,14 @@ class PipelineStatus(Enum):
|
|
|
149
150
|
PROVISIONING = 2
|
|
150
151
|
UNAVAILABLE = 3
|
|
151
152
|
STANDBY = 4
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
153
|
+
AWAITINGAPPROVAL = 5
|
|
154
|
+
INITIALIZING = 6
|
|
155
|
+
BOOTSTRAPPING = 7
|
|
156
|
+
REPLAYING = 8
|
|
157
|
+
PAUSED = 9
|
|
158
|
+
RUNNING = 10
|
|
159
|
+
SUSPENDED = 11
|
|
160
|
+
STOPPING = 12
|
|
159
161
|
|
|
160
162
|
@staticmethod
|
|
161
163
|
def from_str(value):
|
|
@@ -344,3 +346,9 @@ class PipelineFieldSelector(Enum):
|
|
|
344
346
|
|
|
345
347
|
STATUS = "status"
|
|
346
348
|
"""Select only the fields required to know the status of a pipeline."""
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class BootstrapPolicy(Enum):
|
|
352
|
+
AWAIT_APPROVAL = "await_approval"
|
|
353
|
+
ALLOW = "allow"
|
|
354
|
+
REJECT = "reject"
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import pandas as pd
|
|
2
2
|
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
3
5
|
from feldera import FelderaClient
|
|
4
6
|
from feldera._callback_runner import CallbackRunner
|
|
5
7
|
|
|
@@ -20,15 +22,23 @@ class OutputHandler:
|
|
|
20
22
|
self.pipeline_name: str = pipeline_name
|
|
21
23
|
self.view_name: str = view_name
|
|
22
24
|
self.buffer: list[pd.DataFrame] = []
|
|
25
|
+
self.exception: Optional[BaseException] = None
|
|
23
26
|
|
|
24
27
|
# the callback that is passed to the `CallbackRunner`
|
|
25
28
|
def callback(df: pd.DataFrame, _: int):
|
|
26
29
|
if not df.empty:
|
|
27
30
|
self.buffer.append(df)
|
|
28
31
|
|
|
32
|
+
def exception_callback(exception: BaseException):
|
|
33
|
+
self.exception = exception
|
|
34
|
+
|
|
29
35
|
# sets up the callback runner
|
|
30
36
|
self.handler = CallbackRunner(
|
|
31
|
-
self.client,
|
|
37
|
+
self.client,
|
|
38
|
+
self.pipeline_name,
|
|
39
|
+
self.view_name,
|
|
40
|
+
callback,
|
|
41
|
+
exception_callback,
|
|
32
42
|
)
|
|
33
43
|
|
|
34
44
|
def start(self):
|
|
@@ -45,6 +55,8 @@ class OutputHandler:
|
|
|
45
55
|
:param clear_buffer: Whether to clear the buffer after getting the output.
|
|
46
56
|
"""
|
|
47
57
|
|
|
58
|
+
if self.exception is not None:
|
|
59
|
+
raise self.exception
|
|
48
60
|
if len(self.buffer) == 0:
|
|
49
61
|
return pd.DataFrame()
|
|
50
62
|
res = pd.concat(self.buffer, ignore_index=True)
|
|
@@ -11,6 +11,7 @@ from collections import deque
|
|
|
11
11
|
|
|
12
12
|
from feldera.rest.errors import FelderaAPIError
|
|
13
13
|
from feldera.enums import (
|
|
14
|
+
BootstrapPolicy,
|
|
14
15
|
PipelineFieldSelector,
|
|
15
16
|
PipelineStatus,
|
|
16
17
|
ProgramStatus,
|
|
@@ -71,6 +72,30 @@ class Pipeline:
|
|
|
71
72
|
else:
|
|
72
73
|
raise err
|
|
73
74
|
|
|
75
|
+
def wait_for_status(
|
|
76
|
+
self, expected_status: PipelineStatus, timeout: Optional[int] = None
|
|
77
|
+
) -> None:
|
|
78
|
+
"""
|
|
79
|
+
Wait for the pipeline to reach the specified status.
|
|
80
|
+
|
|
81
|
+
:param expected_status: The status to wait for
|
|
82
|
+
:param timeout: Maximum time to wait in seconds. If None, waits forever (default: None)
|
|
83
|
+
:raises TimeoutError: If the expected status is not reached within the timeout
|
|
84
|
+
"""
|
|
85
|
+
start_time = time.time()
|
|
86
|
+
|
|
87
|
+
while True:
|
|
88
|
+
current_status = self.status()
|
|
89
|
+
if current_status == expected_status:
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
if timeout is not None and time.time() - start_time >= timeout:
|
|
93
|
+
raise TimeoutError(
|
|
94
|
+
f"Pipeline did not reach {expected_status.name} status within {timeout} seconds"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
time.sleep(1)
|
|
98
|
+
|
|
74
99
|
def stats(self) -> PipelineStatistics:
|
|
75
100
|
"""Gets the pipeline metrics and performance counters."""
|
|
76
101
|
|
|
@@ -269,11 +294,13 @@ class Pipeline:
|
|
|
269
294
|
if self.status() not in [PipelineStatus.RUNNING, PipelineStatus.PAUSED]:
|
|
270
295
|
raise RuntimeError("Pipeline must be running or paused to listen to output")
|
|
271
296
|
|
|
272
|
-
handler = CallbackRunner(
|
|
297
|
+
handler = CallbackRunner(
|
|
298
|
+
self.client, self.name, view_name, callback, lambda exception: None
|
|
299
|
+
)
|
|
273
300
|
handler.start()
|
|
274
301
|
|
|
275
302
|
def wait_for_completion(
|
|
276
|
-
self, force_stop: bool = False, timeout_s:
|
|
303
|
+
self, force_stop: bool = False, timeout_s: float | None = None
|
|
277
304
|
):
|
|
278
305
|
"""
|
|
279
306
|
Block until the pipeline has completed processing all input records.
|
|
@@ -301,6 +328,7 @@ class Pipeline:
|
|
|
301
328
|
PipelineStatus.RUNNING,
|
|
302
329
|
PipelineStatus.INITIALIZING,
|
|
303
330
|
PipelineStatus.PROVISIONING,
|
|
331
|
+
PipelineStatus.BOOTSTRAPPING,
|
|
304
332
|
]:
|
|
305
333
|
raise RuntimeError("Pipeline must be running to wait for completion")
|
|
306
334
|
|
|
@@ -345,25 +373,10 @@ class Pipeline:
|
|
|
345
373
|
|
|
346
374
|
return self.stats().global_metrics.pipeline_complete
|
|
347
375
|
|
|
348
|
-
def restart(self, timeout_s: Optional[float] = None):
|
|
349
|
-
"""
|
|
350
|
-
Restarts the pipeline.
|
|
351
|
-
|
|
352
|
-
This method forcibly **STOPS** the pipeline regardless of its current
|
|
353
|
-
state and then starts it again. No checkpoints are made when stopping
|
|
354
|
-
the pipeline.
|
|
355
|
-
|
|
356
|
-
:param timeout_s: The maximum time (in seconds) to wait for the
|
|
357
|
-
pipeline to restart.
|
|
358
|
-
"""
|
|
359
|
-
|
|
360
|
-
self.stop(force=True, timeout_s=timeout_s)
|
|
361
|
-
self.start(timeout_s=timeout_s)
|
|
362
|
-
|
|
363
376
|
def wait_for_idle(
|
|
364
377
|
self,
|
|
365
378
|
idle_interval_s: float = 5.0,
|
|
366
|
-
timeout_s: float =
|
|
379
|
+
timeout_s: float | None = None,
|
|
367
380
|
poll_interval_s: float = 0.2,
|
|
368
381
|
):
|
|
369
382
|
"""
|
|
@@ -383,12 +396,12 @@ class Pipeline:
|
|
|
383
396
|
:raises RuntimeError: If the metrics are missing or the timeout was
|
|
384
397
|
reached.
|
|
385
398
|
"""
|
|
386
|
-
if idle_interval_s > timeout_s:
|
|
399
|
+
if timeout_s is not None and idle_interval_s > timeout_s:
|
|
387
400
|
raise ValueError(
|
|
388
401
|
f"idle interval ({idle_interval_s}s) cannot be larger than"
|
|
389
402
|
f" timeout ({timeout_s}s)"
|
|
390
403
|
)
|
|
391
|
-
if poll_interval_s > timeout_s:
|
|
404
|
+
if timeout_s is not None and poll_interval_s > timeout_s:
|
|
392
405
|
raise ValueError(
|
|
393
406
|
f"poll interval ({poll_interval_s}s) cannot be larger than"
|
|
394
407
|
f" timeout ({timeout_s}s)"
|
|
@@ -434,11 +447,13 @@ metrics"""
|
|
|
434
447
|
return
|
|
435
448
|
|
|
436
449
|
# Timeout
|
|
437
|
-
if now_s - start_time_s >= timeout_s:
|
|
450
|
+
if timeout_s is not None and now_s - start_time_s >= timeout_s:
|
|
438
451
|
raise RuntimeError(f"waiting for idle reached timeout ({timeout_s}s)")
|
|
439
452
|
time.sleep(poll_interval_s)
|
|
440
453
|
|
|
441
|
-
def activate(
|
|
454
|
+
def activate(
|
|
455
|
+
self, wait: bool = True, timeout_s: Optional[float] = None
|
|
456
|
+
) -> Optional[PipelineStatus]:
|
|
442
457
|
"""
|
|
443
458
|
Activates the pipeline when starting from STANDBY mode. Only applicable
|
|
444
459
|
when the pipeline is starting from a checkpoint in object store.
|
|
@@ -449,9 +464,14 @@ metrics"""
|
|
|
449
464
|
pipeline to pause.
|
|
450
465
|
"""
|
|
451
466
|
|
|
452
|
-
self.client.activate_pipeline(self.name, wait=wait, timeout_s=timeout_s)
|
|
467
|
+
return self.client.activate_pipeline(self.name, wait=wait, timeout_s=timeout_s)
|
|
453
468
|
|
|
454
|
-
def start(
|
|
469
|
+
def start(
|
|
470
|
+
self,
|
|
471
|
+
bootstrap_policy: Optional[BootstrapPolicy] = None,
|
|
472
|
+
wait: bool = True,
|
|
473
|
+
timeout_s: Optional[float] = None,
|
|
474
|
+
):
|
|
455
475
|
"""
|
|
456
476
|
.. _start:
|
|
457
477
|
|
|
@@ -461,6 +481,7 @@ metrics"""
|
|
|
461
481
|
- If the pipeline is in any other state, an error will be raised.
|
|
462
482
|
- If the pipeline is in PAUSED state, use `.meth:resume` instead.
|
|
463
483
|
|
|
484
|
+
:param bootstrap_policy: The bootstrap policy to use.
|
|
464
485
|
:param timeout_s: The maximum time (in seconds) to wait for the
|
|
465
486
|
pipeline to start.
|
|
466
487
|
:param wait: Set True to wait for the pipeline to start. True by default
|
|
@@ -468,21 +489,57 @@ metrics"""
|
|
|
468
489
|
:raises RuntimeError: If the pipeline is not in STOPPED state.
|
|
469
490
|
"""
|
|
470
491
|
|
|
471
|
-
self.client.start_pipeline(
|
|
492
|
+
self.client.start_pipeline(
|
|
493
|
+
self.name, bootstrap_policy=bootstrap_policy, wait=wait, timeout_s=timeout_s
|
|
494
|
+
)
|
|
472
495
|
|
|
473
|
-
def start_paused(
|
|
496
|
+
def start_paused(
|
|
497
|
+
self,
|
|
498
|
+
bootstrap_policy: Optional[BootstrapPolicy] = None,
|
|
499
|
+
wait: bool = True,
|
|
500
|
+
timeout_s: Optional[float] = None,
|
|
501
|
+
):
|
|
474
502
|
"""
|
|
475
503
|
Starts the pipeline in the paused state.
|
|
476
504
|
"""
|
|
477
505
|
|
|
478
|
-
self.client.start_pipeline_as_paused(
|
|
506
|
+
return self.client.start_pipeline_as_paused(
|
|
507
|
+
self.name, bootstrap_policy=bootstrap_policy, wait=wait, timeout_s=timeout_s
|
|
508
|
+
)
|
|
479
509
|
|
|
480
|
-
def start_standby(
|
|
510
|
+
def start_standby(
|
|
511
|
+
self,
|
|
512
|
+
bootstrap_policy: Optional[BootstrapPolicy] = None,
|
|
513
|
+
wait: bool = True,
|
|
514
|
+
timeout_s: Optional[float] = None,
|
|
515
|
+
):
|
|
481
516
|
"""
|
|
482
517
|
Starts the pipeline in the standby state.
|
|
483
518
|
"""
|
|
484
519
|
|
|
485
|
-
self.client.start_pipeline_as_standby(
|
|
520
|
+
self.client.start_pipeline_as_standby(
|
|
521
|
+
self.name, bootstrap_policy=bootstrap_policy, wait=wait, timeout_s=timeout_s
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
def restart(
|
|
525
|
+
self,
|
|
526
|
+
bootstrap_policy: Optional[BootstrapPolicy] = None,
|
|
527
|
+
timeout_s: Optional[float] = None,
|
|
528
|
+
):
|
|
529
|
+
"""
|
|
530
|
+
Restarts the pipeline.
|
|
531
|
+
|
|
532
|
+
This method forcibly **STOPS** the pipeline regardless of its current
|
|
533
|
+
state and then starts it again. No checkpoints are made when stopping
|
|
534
|
+
the pipeline.
|
|
535
|
+
|
|
536
|
+
:param bootstrap_policy: The bootstrap policy to use.
|
|
537
|
+
:param timeout_s: The maximum time (in seconds) to wait for the
|
|
538
|
+
pipeline to restart.
|
|
539
|
+
"""
|
|
540
|
+
|
|
541
|
+
self.stop(force=True, timeout_s=timeout_s)
|
|
542
|
+
self.start(bootstrap_policy=bootstrap_policy, timeout_s=timeout_s)
|
|
486
543
|
|
|
487
544
|
def pause(self, wait: bool = True, timeout_s: Optional[float] = None):
|
|
488
545
|
"""
|
|
@@ -517,6 +574,18 @@ metrics"""
|
|
|
517
574
|
self.name, force=force, wait=wait, timeout_s=timeout_s
|
|
518
575
|
)
|
|
519
576
|
|
|
577
|
+
def approve(self):
|
|
578
|
+
"""
|
|
579
|
+
Approves the pipeline to proceed with bootstrapping.
|
|
580
|
+
|
|
581
|
+
This method is used when a pipeline has been started with
|
|
582
|
+
`bootstrap_policy=BootstrapPolicy.AWAIT_APPROVAL` and is currently in the
|
|
583
|
+
AWAITINGAPPROVAL state. The pipeline will wait for explicit user approval
|
|
584
|
+
before proceeding with the bootstrapping process.
|
|
585
|
+
"""
|
|
586
|
+
|
|
587
|
+
self.client.approve_pipeline(self.name)
|
|
588
|
+
|
|
520
589
|
def resume(self, wait: bool = True, timeout_s: Optional[float] = None):
|
|
521
590
|
"""
|
|
522
591
|
Resumes the pipeline from the PAUSED state. If the pipeline is already
|
|
@@ -627,7 +696,7 @@ metrics"""
|
|
|
627
696
|
err.message = f"Pipeline with name {name} not found"
|
|
628
697
|
raise err
|
|
629
698
|
|
|
630
|
-
def checkpoint(self, wait: bool = False, timeout_s=
|
|
699
|
+
def checkpoint(self, wait: bool = False, timeout_s: Optional[float] = None) -> int:
|
|
631
700
|
"""
|
|
632
701
|
Checkpoints this pipeline.
|
|
633
702
|
|
|
@@ -649,7 +718,7 @@ metrics"""
|
|
|
649
718
|
|
|
650
719
|
while True:
|
|
651
720
|
elapsed = time.monotonic() - start
|
|
652
|
-
if elapsed > timeout_s:
|
|
721
|
+
if timeout_s is not None and elapsed > timeout_s:
|
|
653
722
|
raise TimeoutError(
|
|
654
723
|
f"""timeout ({timeout_s}s) reached while waiting for \
|
|
655
724
|
pipeline '{self.name}' to make checkpoint '{seq}'"""
|
|
@@ -685,7 +754,9 @@ pipeline '{self.name}' to make checkpoint '{seq}'"""
|
|
|
685
754
|
if seq < success:
|
|
686
755
|
return CheckpointStatus.Unknown
|
|
687
756
|
|
|
688
|
-
def sync_checkpoint(
|
|
757
|
+
def sync_checkpoint(
|
|
758
|
+
self, wait: bool = False, timeout_s: Optional[float] = None
|
|
759
|
+
) -> str:
|
|
689
760
|
"""
|
|
690
761
|
Syncs this checkpoint to object store.
|
|
691
762
|
|
|
@@ -707,7 +778,7 @@ pipeline '{self.name}' to make checkpoint '{seq}'"""
|
|
|
707
778
|
|
|
708
779
|
while True:
|
|
709
780
|
elapsed = time.monotonic() - start
|
|
710
|
-
if elapsed > timeout_s:
|
|
781
|
+
if timeout_s is not None and elapsed > timeout_s:
|
|
711
782
|
raise TimeoutError(
|
|
712
783
|
f"""timeout ({timeout_s}s) reached while waiting for \
|
|
713
784
|
pipeline '{self.name}' to sync checkpoint '{uuid}'"""
|
|
@@ -1177,6 +1248,14 @@ pipeline '{self.name}' to sync checkpoint '{uuid}'"""
|
|
|
1177
1248
|
self.refresh(PipelineFieldSelector.STATUS)
|
|
1178
1249
|
return DeploymentRuntimeStatus.from_str(self._inner.deployment_runtime_status)
|
|
1179
1250
|
|
|
1251
|
+
def deployment_runtime_status_details(self) -> Optional[dict]:
|
|
1252
|
+
"""
|
|
1253
|
+
Return the deployment runtime status details.
|
|
1254
|
+
"""
|
|
1255
|
+
|
|
1256
|
+
self.refresh(PipelineFieldSelector.STATUS)
|
|
1257
|
+
return self._inner.deployment_runtime_status_details
|
|
1258
|
+
|
|
1180
1259
|
def deployment_error(self) -> Mapping[str, Any]:
|
|
1181
1260
|
"""
|
|
1182
1261
|
Return the deployment error of the pipeline.
|
|
@@ -20,20 +20,20 @@ def requests_verify_from_env() -> str | bool:
|
|
|
20
20
|
if env_feldera_tls_insecure is not None and FELDERA_HTTPS_TLS_CERT is not None:
|
|
21
21
|
logging.warning(
|
|
22
22
|
"environment variables FELDERA_HTTPS_TLS_CERT and "
|
|
23
|
-
"FELDERA_TLS_INSECURE both are set."
|
|
24
|
-
"\nFELDERA_HTTPS_TLS_CERT takes priority."
|
|
23
|
+
+ "FELDERA_TLS_INSECURE both are set."
|
|
24
|
+
+ "\nFELDERA_HTTPS_TLS_CERT takes priority."
|
|
25
25
|
)
|
|
26
26
|
|
|
27
27
|
if env_feldera_tls_insecure is None:
|
|
28
|
-
|
|
28
|
+
feldera_tls_insecure = False
|
|
29
29
|
else:
|
|
30
|
-
|
|
30
|
+
feldera_tls_insecure = env_feldera_tls_insecure.strip().lower() in (
|
|
31
31
|
"1",
|
|
32
32
|
"true",
|
|
33
33
|
"yes",
|
|
34
34
|
)
|
|
35
35
|
|
|
36
|
-
requests_verify = not
|
|
36
|
+
requests_verify = not feldera_tls_insecure
|
|
37
37
|
if FELDERA_HTTPS_TLS_CERT is not None:
|
|
38
38
|
requests_verify = FELDERA_HTTPS_TLS_CERT
|
|
39
39
|
|
|
@@ -56,7 +56,7 @@ class HttpRequests:
|
|
|
56
56
|
"""
|
|
57
57
|
self.headers["Content-Type"] = content_type
|
|
58
58
|
|
|
59
|
-
prev_resp:
|
|
59
|
+
prev_resp: requests.Response | None = None
|
|
60
60
|
|
|
61
61
|
try:
|
|
62
62
|
conn_timeout = self.config.connection_timeout
|
|
@@ -156,7 +156,7 @@ class HttpRequests:
|
|
|
156
156
|
body: Optional[
|
|
157
157
|
Union[Mapping[str, Any], Sequence[Mapping[str, Any]], List[str], str]
|
|
158
158
|
] = None,
|
|
159
|
-
content_type:
|
|
159
|
+
content_type: str = "application/json",
|
|
160
160
|
params: Optional[Mapping[str, Any]] = None,
|
|
161
161
|
stream: bool = False,
|
|
162
162
|
serialize: bool = True,
|
|
@@ -177,7 +177,7 @@ class HttpRequests:
|
|
|
177
177
|
body: Optional[
|
|
178
178
|
Union[Mapping[str, Any], Sequence[Mapping[str, Any]], List[str], str]
|
|
179
179
|
] = None,
|
|
180
|
-
content_type:
|
|
180
|
+
content_type: str = "application/json",
|
|
181
181
|
params: Optional[Mapping[str, Any]] = None,
|
|
182
182
|
) -> Any:
|
|
183
183
|
return self.send_request(requests.patch, path, body, content_type, params)
|
|
@@ -188,7 +188,7 @@ class HttpRequests:
|
|
|
188
188
|
body: Optional[
|
|
189
189
|
Union[Mapping[str, Any], Sequence[Mapping[str, Any]], List[str], str]
|
|
190
190
|
] = None,
|
|
191
|
-
content_type:
|
|
191
|
+
content_type: str = "application/json",
|
|
192
192
|
params: Optional[Mapping[str, Any]] = None,
|
|
193
193
|
) -> Any:
|
|
194
194
|
return self.send_request(requests.put, path, body, content_type, params)
|
|
@@ -39,7 +39,7 @@ class Config:
|
|
|
39
39
|
)
|
|
40
40
|
self.url: str = BASE_URL
|
|
41
41
|
self.api_key: Optional[str] = os.environ.get("FELDERA_API_KEY", api_key)
|
|
42
|
-
self.version:
|
|
42
|
+
self.version: str = version or "v0"
|
|
43
43
|
self.timeout: Optional[float] = timeout
|
|
44
44
|
self.connection_timeout: Optional[float] = connection_timeout
|
|
45
45
|
env_verify = requests_verify_from_env()
|
|
@@ -6,8 +6,9 @@ import json
|
|
|
6
6
|
from decimal import Decimal
|
|
7
7
|
from typing import Generator, Mapping
|
|
8
8
|
from urllib.parse import quote
|
|
9
|
+
import requests
|
|
9
10
|
|
|
10
|
-
from feldera.enums import PipelineFieldSelector
|
|
11
|
+
from feldera.enums import BootstrapPolicy, PipelineFieldSelector, PipelineStatus
|
|
11
12
|
from feldera.rest.config import Config
|
|
12
13
|
from feldera.rest.feldera_config import FelderaConfig
|
|
13
14
|
from feldera.rest.errors import FelderaTimeoutError, FelderaAPIError
|
|
@@ -110,16 +111,20 @@ class FelderaClient:
|
|
|
110
111
|
|
|
111
112
|
return Pipeline.from_dict(resp)
|
|
112
113
|
|
|
113
|
-
def get_runtime_config(self, pipeline_name) ->
|
|
114
|
+
def get_runtime_config(self, pipeline_name) -> Mapping[str, Any]:
|
|
114
115
|
"""
|
|
115
116
|
Get the runtime config of a pipeline by name
|
|
116
117
|
|
|
117
118
|
:param pipeline_name: The name of the pipeline
|
|
118
119
|
"""
|
|
119
120
|
|
|
120
|
-
resp:
|
|
121
|
+
resp: Mapping[str, Any] = self.http.get(f"/pipelines/{pipeline_name}")
|
|
121
122
|
|
|
122
|
-
|
|
123
|
+
runtime_config: Mapping[str, Any] | None = resp.get("runtime_config")
|
|
124
|
+
if runtime_config is None:
|
|
125
|
+
raise ValueError(f"Pipeline {pipeline_name} has no runtime config")
|
|
126
|
+
|
|
127
|
+
return runtime_config
|
|
123
128
|
|
|
124
129
|
def pipelines(self) -> list[Pipeline]:
|
|
125
130
|
"""
|
|
@@ -175,7 +180,7 @@ class FelderaClient:
|
|
|
175
180
|
self,
|
|
176
181
|
pipeline_name: str,
|
|
177
182
|
state: str,
|
|
178
|
-
timeout_s: float =
|
|
183
|
+
timeout_s: Optional[float] = None,
|
|
179
184
|
start: bool = True,
|
|
180
185
|
):
|
|
181
186
|
start_time = time.monotonic()
|
|
@@ -211,6 +216,46 @@ Reason: The pipeline is in a STOPPED state due to the following error:
|
|
|
211
216
|
)
|
|
212
217
|
time.sleep(0.1)
|
|
213
218
|
|
|
219
|
+
def __wait_for_pipeline_state_one_of(
|
|
220
|
+
self,
|
|
221
|
+
pipeline_name: str,
|
|
222
|
+
states: list[str],
|
|
223
|
+
timeout_s: float | None = None,
|
|
224
|
+
start: bool = True,
|
|
225
|
+
) -> PipelineStatus:
|
|
226
|
+
start_time = time.monotonic()
|
|
227
|
+
states = [state.lower() for state in states]
|
|
228
|
+
|
|
229
|
+
while True:
|
|
230
|
+
if timeout_s is not None:
|
|
231
|
+
elapsed = time.monotonic() - start_time
|
|
232
|
+
if elapsed > timeout_s:
|
|
233
|
+
raise TimeoutError(
|
|
234
|
+
f"Timed out waiting for pipeline {pipeline_name} to"
|
|
235
|
+
f"transition to one of the states: {states}"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
resp = self.get_pipeline(pipeline_name, PipelineFieldSelector.STATUS)
|
|
239
|
+
status = resp.deployment_status
|
|
240
|
+
|
|
241
|
+
if status.lower() in states:
|
|
242
|
+
return PipelineStatus.from_str(status)
|
|
243
|
+
elif (
|
|
244
|
+
status == "Stopped"
|
|
245
|
+
and len(resp.deployment_error or {}) > 0
|
|
246
|
+
and resp.deployment_desired_status == "Stopped"
|
|
247
|
+
):
|
|
248
|
+
err_msg = "Unable to START the pipeline:\n" if start else ""
|
|
249
|
+
raise RuntimeError(
|
|
250
|
+
f"""{err_msg}Unable to transition the pipeline to one of the states: {states}.
|
|
251
|
+
Reason: The pipeline is in a STOPPED state due to the following error:
|
|
252
|
+
{resp.deployment_error.get("message", "")}"""
|
|
253
|
+
)
|
|
254
|
+
logging.debug(
|
|
255
|
+
"still starting %s, waiting for 100 more milliseconds", pipeline_name
|
|
256
|
+
)
|
|
257
|
+
time.sleep(0.1)
|
|
258
|
+
|
|
214
259
|
def create_pipeline(self, pipeline: Pipeline) -> Pipeline:
|
|
215
260
|
"""
|
|
216
261
|
Create a pipeline if it doesn't exist and wait for it to compile
|
|
@@ -342,36 +387,36 @@ Reason: The pipeline is in a STOPPED state due to the following error:
|
|
|
342
387
|
yield chunk.decode("utf-8")
|
|
343
388
|
|
|
344
389
|
def activate_pipeline(
|
|
345
|
-
self, pipeline_name: str, wait: bool = True, timeout_s: Optional[float] =
|
|
346
|
-
):
|
|
390
|
+
self, pipeline_name: str, wait: bool = True, timeout_s: Optional[float] = None
|
|
391
|
+
) -> Optional[PipelineStatus]:
|
|
347
392
|
"""
|
|
348
393
|
|
|
349
394
|
:param pipeline_name: The name of the pipeline to activate
|
|
350
395
|
:param wait: Set True to wait for the pipeline to activate. True by
|
|
351
396
|
default
|
|
352
397
|
:param timeout_s: The amount of time in seconds to wait for the
|
|
353
|
-
pipeline to activate.
|
|
398
|
+
pipeline to activate.
|
|
354
399
|
"""
|
|
355
400
|
|
|
356
|
-
if timeout_s is None:
|
|
357
|
-
timeout_s = 300
|
|
358
|
-
|
|
359
401
|
self.http.post(
|
|
360
402
|
path=f"/pipelines/{pipeline_name}/activate",
|
|
361
403
|
)
|
|
362
404
|
|
|
363
405
|
if not wait:
|
|
364
|
-
return
|
|
406
|
+
return None
|
|
365
407
|
|
|
366
|
-
self.
|
|
408
|
+
return self.__wait_for_pipeline_state_one_of(
|
|
409
|
+
pipeline_name, ["running", "AwaitingApproval"], timeout_s
|
|
410
|
+
)
|
|
367
411
|
|
|
368
412
|
def _inner_start_pipeline(
|
|
369
413
|
self,
|
|
370
414
|
pipeline_name: str,
|
|
371
415
|
initial: str = "running",
|
|
416
|
+
bootstrap_policy: Optional[BootstrapPolicy] = None,
|
|
372
417
|
wait: bool = True,
|
|
373
|
-
timeout_s: Optional[float] =
|
|
374
|
-
):
|
|
418
|
+
timeout_s: Optional[float] = None,
|
|
419
|
+
) -> Optional[PipelineStatus]:
|
|
375
420
|
"""
|
|
376
421
|
|
|
377
422
|
:param pipeline_name: The name of the pipeline to start
|
|
@@ -379,67 +424,88 @@ Reason: The pipeline is in a STOPPED state due to the following error:
|
|
|
379
424
|
by default.
|
|
380
425
|
:param wait: Set True to wait for the pipeline to start. True by default
|
|
381
426
|
:param timeout_s: The amount of time in seconds to wait for the
|
|
382
|
-
pipeline to start.
|
|
427
|
+
pipeline to start.
|
|
383
428
|
"""
|
|
384
429
|
|
|
385
|
-
|
|
386
|
-
|
|
430
|
+
params = {"initial": initial}
|
|
431
|
+
if bootstrap_policy is not None:
|
|
432
|
+
params["bootstrap_policy"] = bootstrap_policy.value
|
|
387
433
|
|
|
388
434
|
self.http.post(
|
|
389
435
|
path=f"/pipelines/{pipeline_name}/start",
|
|
390
|
-
params=
|
|
436
|
+
params=params,
|
|
391
437
|
)
|
|
392
438
|
|
|
393
439
|
if not wait:
|
|
394
|
-
return
|
|
440
|
+
return None
|
|
395
441
|
|
|
396
|
-
self.
|
|
442
|
+
return self.__wait_for_pipeline_state_one_of(
|
|
443
|
+
pipeline_name, [initial, "AwaitingApproval"], timeout_s
|
|
444
|
+
)
|
|
397
445
|
|
|
398
446
|
def start_pipeline(
|
|
399
|
-
self,
|
|
400
|
-
|
|
447
|
+
self,
|
|
448
|
+
pipeline_name: str,
|
|
449
|
+
bootstrap_policy: Optional[BootstrapPolicy] = None,
|
|
450
|
+
wait: bool = True,
|
|
451
|
+
timeout_s: Optional[float] = None,
|
|
452
|
+
) -> Optional[PipelineStatus]:
|
|
401
453
|
"""
|
|
402
454
|
|
|
403
455
|
:param pipeline_name: The name of the pipeline to start
|
|
404
456
|
:param wait: Set True to wait for the pipeline to start.
|
|
405
457
|
True by default
|
|
406
458
|
:param timeout_s: The amount of time in seconds to wait for the
|
|
407
|
-
pipeline to start.
|
|
459
|
+
pipeline to start.
|
|
408
460
|
"""
|
|
409
461
|
|
|
410
|
-
self._inner_start_pipeline(
|
|
462
|
+
return self._inner_start_pipeline(
|
|
463
|
+
pipeline_name, "running", bootstrap_policy, wait, timeout_s
|
|
464
|
+
)
|
|
411
465
|
|
|
412
466
|
def start_pipeline_as_paused(
|
|
413
|
-
self,
|
|
414
|
-
|
|
467
|
+
self,
|
|
468
|
+
pipeline_name: str,
|
|
469
|
+
bootstrap_policy: Optional[BootstrapPolicy] = None,
|
|
470
|
+
wait: bool = True,
|
|
471
|
+
timeout_s: float | None = None,
|
|
472
|
+
) -> Optional[PipelineStatus]:
|
|
415
473
|
"""
|
|
416
474
|
:param pipeline_name: The name of the pipeline to start as paused.
|
|
417
475
|
:param wait: Set True to wait for the pipeline to start as pause.
|
|
418
476
|
True by default
|
|
419
477
|
:param timeout_s: The amount of time in seconds to wait for the
|
|
420
|
-
pipeline to start.
|
|
478
|
+
pipeline to start.
|
|
421
479
|
"""
|
|
422
480
|
|
|
423
|
-
self._inner_start_pipeline(
|
|
481
|
+
return self._inner_start_pipeline(
|
|
482
|
+
pipeline_name, "paused", bootstrap_policy, wait, timeout_s
|
|
483
|
+
)
|
|
424
484
|
|
|
425
485
|
def start_pipeline_as_standby(
|
|
426
|
-
self,
|
|
486
|
+
self,
|
|
487
|
+
pipeline_name: str,
|
|
488
|
+
bootstrap_policy: Optional[BootstrapPolicy] = None,
|
|
489
|
+
wait: bool = True,
|
|
490
|
+
timeout_s: Optional[float] = None,
|
|
427
491
|
):
|
|
428
492
|
"""
|
|
429
493
|
:param pipeline_name: The name of the pipeline to start as standby.
|
|
430
494
|
:param wait: Set True to wait for the pipeline to start as standby.
|
|
431
495
|
True by default
|
|
432
496
|
:param timeout_s: The amount of time in seconds to wait for the
|
|
433
|
-
pipeline to start.
|
|
497
|
+
pipeline to start.
|
|
434
498
|
"""
|
|
435
499
|
|
|
436
|
-
self._inner_start_pipeline(
|
|
500
|
+
self._inner_start_pipeline(
|
|
501
|
+
pipeline_name, "standby", bootstrap_policy, wait, timeout_s
|
|
502
|
+
)
|
|
437
503
|
|
|
438
504
|
def resume_pipeline(
|
|
439
505
|
self,
|
|
440
506
|
pipeline_name: str,
|
|
441
507
|
wait: bool = True,
|
|
442
|
-
timeout_s: Optional[float] =
|
|
508
|
+
timeout_s: Optional[float] = None,
|
|
443
509
|
):
|
|
444
510
|
"""
|
|
445
511
|
Resume a pipeline
|
|
@@ -447,12 +513,9 @@ Reason: The pipeline is in a STOPPED state due to the following error:
|
|
|
447
513
|
:param pipeline_name: The name of the pipeline to stop
|
|
448
514
|
:param wait: Set True to wait for the pipeline to pause. True by default
|
|
449
515
|
:param timeout_s: The amount of time in seconds to wait for the pipeline
|
|
450
|
-
to pause.
|
|
516
|
+
to pause.
|
|
451
517
|
"""
|
|
452
518
|
|
|
453
|
-
if timeout_s is None:
|
|
454
|
-
timeout_s = 300
|
|
455
|
-
|
|
456
519
|
self.http.post(
|
|
457
520
|
path=f"/pipelines/{pipeline_name}/resume",
|
|
458
521
|
)
|
|
@@ -466,7 +529,7 @@ Reason: The pipeline is in a STOPPED state due to the following error:
|
|
|
466
529
|
self,
|
|
467
530
|
pipeline_name: str,
|
|
468
531
|
wait: bool = True,
|
|
469
|
-
timeout_s: Optional[float] =
|
|
532
|
+
timeout_s: Optional[float] = None,
|
|
470
533
|
):
|
|
471
534
|
"""
|
|
472
535
|
Pause a pipeline
|
|
@@ -476,12 +539,9 @@ Reason: The pipeline is in a STOPPED state due to the following error:
|
|
|
476
539
|
STOPPED state due to a failure.
|
|
477
540
|
:param wait: Set True to wait for the pipeline to pause. True by default
|
|
478
541
|
:param timeout_s: The amount of time in seconds to wait for the pipeline
|
|
479
|
-
to pause.
|
|
542
|
+
to pause.
|
|
480
543
|
"""
|
|
481
544
|
|
|
482
|
-
if timeout_s is None:
|
|
483
|
-
timeout_s = 300
|
|
484
|
-
|
|
485
545
|
self.http.post(
|
|
486
546
|
path=f"/pipelines/{pipeline_name}/pause",
|
|
487
547
|
)
|
|
@@ -491,12 +551,20 @@ Reason: The pipeline is in a STOPPED state due to the following error:
|
|
|
491
551
|
|
|
492
552
|
self.__wait_for_pipeline_state(pipeline_name, "paused", timeout_s)
|
|
493
553
|
|
|
554
|
+
def approve_pipeline(
|
|
555
|
+
self,
|
|
556
|
+
pipeline_name: str,
|
|
557
|
+
):
|
|
558
|
+
self.http.post(
|
|
559
|
+
path=f"/pipelines/{pipeline_name}/approve",
|
|
560
|
+
)
|
|
561
|
+
|
|
494
562
|
def stop_pipeline(
|
|
495
563
|
self,
|
|
496
564
|
pipeline_name: str,
|
|
497
565
|
force: bool,
|
|
498
566
|
wait: bool = True,
|
|
499
|
-
timeout_s: Optional[float] =
|
|
567
|
+
timeout_s: Optional[float] = None,
|
|
500
568
|
):
|
|
501
569
|
"""
|
|
502
570
|
Stop a pipeline
|
|
@@ -506,12 +574,9 @@ Reason: The pipeline is in a STOPPED state due to the following error:
|
|
|
506
574
|
Set False to automatically checkpoint before stopping.
|
|
507
575
|
:param wait: Set True to wait for the pipeline to stop. True by default
|
|
508
576
|
:param timeout_s: The amount of time in seconds to wait for the pipeline
|
|
509
|
-
to stop.
|
|
577
|
+
to stop.
|
|
510
578
|
"""
|
|
511
579
|
|
|
512
|
-
if timeout_s is None:
|
|
513
|
-
timeout_s = 300
|
|
514
|
-
|
|
515
580
|
params = {"force": str(force).lower()}
|
|
516
581
|
|
|
517
582
|
self.http.post(
|
|
@@ -524,7 +589,12 @@ Reason: The pipeline is in a STOPPED state due to the following error:
|
|
|
524
589
|
|
|
525
590
|
start = time.monotonic()
|
|
526
591
|
|
|
527
|
-
while
|
|
592
|
+
while True:
|
|
593
|
+
if timeout_s is not None and time.monotonic() - start > timeout_s:
|
|
594
|
+
raise FelderaTimeoutError(
|
|
595
|
+
f"timeout error: pipeline '{pipeline_name}' did not stop in {timeout_s} seconds"
|
|
596
|
+
)
|
|
597
|
+
|
|
528
598
|
status = self.get_pipeline(
|
|
529
599
|
pipeline_name, PipelineFieldSelector.STATUS
|
|
530
600
|
).deployment_status
|
|
@@ -538,29 +608,26 @@ Reason: The pipeline is in a STOPPED state due to the following error:
|
|
|
538
608
|
)
|
|
539
609
|
time.sleep(0.1)
|
|
540
610
|
|
|
541
|
-
|
|
542
|
-
f"timeout error: pipeline '{pipeline_name}' did not stop in {timeout_s} seconds"
|
|
543
|
-
)
|
|
544
|
-
|
|
545
|
-
def clear_storage(self, pipeline_name: str, timeout_s: Optional[float] = 300):
|
|
611
|
+
def clear_storage(self, pipeline_name: str, timeout_s: Optional[float] = None):
|
|
546
612
|
"""
|
|
547
613
|
Clears the storage from the pipeline.
|
|
548
614
|
This operation cannot be canceled.
|
|
549
615
|
|
|
550
616
|
:param pipeline_name: The name of the pipeline
|
|
551
617
|
:param timeout_s: The amount of time in seconds to wait for the storage
|
|
552
|
-
to clear.
|
|
618
|
+
to clear.
|
|
553
619
|
"""
|
|
554
|
-
if timeout_s is None:
|
|
555
|
-
timeout_s = 300
|
|
556
|
-
|
|
557
620
|
self.http.post(
|
|
558
621
|
path=f"/pipelines/{pipeline_name}/clear",
|
|
559
622
|
)
|
|
560
623
|
|
|
561
624
|
start = time.monotonic()
|
|
562
625
|
|
|
563
|
-
while
|
|
626
|
+
while True:
|
|
627
|
+
if timeout_s is not None and time.monotonic() - start > timeout_s:
|
|
628
|
+
raise FelderaTimeoutError(
|
|
629
|
+
f"timeout error: pipeline '{pipeline_name}' did not clear storage in {timeout_s} seconds"
|
|
630
|
+
)
|
|
564
631
|
status = self.get_pipeline(
|
|
565
632
|
pipeline_name, PipelineFieldSelector.STATUS
|
|
566
633
|
).storage_status
|
|
@@ -574,10 +641,6 @@ Reason: The pipeline is in a STOPPED state due to the following error:
|
|
|
574
641
|
)
|
|
575
642
|
time.sleep(0.1)
|
|
576
643
|
|
|
577
|
-
raise FelderaTimeoutError(
|
|
578
|
-
f"timeout error: pipeline '{pipeline_name}' did not clear storage in {timeout_s} seconds"
|
|
579
|
-
)
|
|
580
|
-
|
|
581
644
|
def start_transaction(self, pipeline_name: str) -> int:
|
|
582
645
|
"""
|
|
583
646
|
Start a new transaction.
|
|
@@ -773,15 +836,15 @@ Reason: The pipeline is in a STOPPED state due to the following error:
|
|
|
773
836
|
_validate_no_none_keys_in_map(datum.get("insert", {}))
|
|
774
837
|
_validate_no_none_keys_in_map(datum.get("delete", {}))
|
|
775
838
|
else:
|
|
776
|
-
data:
|
|
839
|
+
data: Mapping[str, Any] = data
|
|
777
840
|
_validate_no_none_keys_in_map(data.get("insert", {}))
|
|
778
841
|
_validate_no_none_keys_in_map(data.get("delete", {}))
|
|
779
842
|
else:
|
|
780
843
|
_validate_no_none_keys_in_map(data)
|
|
781
844
|
|
|
782
845
|
# python sends `True` which isn't accepted by the backend
|
|
783
|
-
array = _prepare_boolean_input(array)
|
|
784
|
-
force = _prepare_boolean_input(force)
|
|
846
|
+
array: str = _prepare_boolean_input(array)
|
|
847
|
+
force: str = _prepare_boolean_input(force)
|
|
785
848
|
|
|
786
849
|
params = {
|
|
787
850
|
"force": force,
|
|
@@ -821,7 +884,7 @@ Reason: The pipeline is in a STOPPED state due to the following error:
|
|
|
821
884
|
return token
|
|
822
885
|
|
|
823
886
|
def wait_for_token(
|
|
824
|
-
self, pipeline_name: str, token: str, timeout_s: Optional[float] =
|
|
887
|
+
self, pipeline_name: str, token: str, timeout_s: Optional[float] = None
|
|
825
888
|
):
|
|
826
889
|
"""
|
|
827
890
|
Blocks until all records represented by this completion token have
|
|
@@ -830,7 +893,7 @@ Reason: The pipeline is in a STOPPED state due to the following error:
|
|
|
830
893
|
:param pipeline_name: The name of the pipeline
|
|
831
894
|
:param token: The token to check for completion
|
|
832
895
|
:param timeout_s: The amount of time in seconds to wait for the pipeline
|
|
833
|
-
to process these records.
|
|
896
|
+
to process these records.
|
|
834
897
|
"""
|
|
835
898
|
|
|
836
899
|
params = {
|
|
@@ -849,8 +912,8 @@ Reason: The pipeline is in a STOPPED state due to the following error:
|
|
|
849
912
|
if time.monotonic() > end:
|
|
850
913
|
raise FelderaTimeoutError(
|
|
851
914
|
f"timeout error: pipeline '{pipeline_name}' did not"
|
|
852
|
-
f" process records represented by token {token} within"
|
|
853
|
-
f" {timeout_s}"
|
|
915
|
+
+ f" process records represented by token {token} within"
|
|
916
|
+
+ f" {timeout_s}"
|
|
854
917
|
)
|
|
855
918
|
|
|
856
919
|
resp = self.http.get(
|
|
@@ -914,7 +977,7 @@ Reason: The pipeline is in a STOPPED state due to the following error:
|
|
|
914
977
|
|
|
915
978
|
table_name = f'"{table_name}"' if case_sensitive else table_name
|
|
916
979
|
|
|
917
|
-
resp = self.http.post(
|
|
980
|
+
resp: requests.Response = self.http.post(
|
|
918
981
|
path=f"/pipelines/{quote(pipeline_name, safe='')}/egress/{quote(table_name, safe='')}",
|
|
919
982
|
params=params,
|
|
920
983
|
stream=True,
|
|
@@ -1019,7 +1082,7 @@ Reason: The pipeline is in a STOPPED state due to the following error:
|
|
|
1019
1082
|
|
|
1020
1083
|
def query_as_json(
|
|
1021
1084
|
self, pipeline_name: str, query: str
|
|
1022
|
-
) -> Generator[
|
|
1085
|
+
) -> Generator[Mapping[str, Any], None, None]:
|
|
1023
1086
|
"""
|
|
1024
1087
|
Executes an ad-hoc query on the specified pipeline and returns the result as a generator that yields
|
|
1025
1088
|
rows of the query as Python dictionaries.
|
|
@@ -69,6 +69,7 @@ class Pipeline:
|
|
|
69
69
|
self.deployment_runtime_desired_status: Optional[str] = None
|
|
70
70
|
self.deployment_runtime_desired_status_since: Optional[str] = None
|
|
71
71
|
self.deployment_runtime_status: Optional[str] = None
|
|
72
|
+
self.deployment_runtime_status_details: Optional[dict] = None
|
|
72
73
|
self.deployment_runtime_status_since: Optional[str] = None
|
|
73
74
|
|
|
74
75
|
@classmethod
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
from threading import Thread
|
|
2
|
-
from typing import Callable, Optional
|
|
3
|
-
|
|
4
|
-
import pandas as pd
|
|
5
|
-
from feldera import FelderaClient
|
|
6
|
-
from feldera._helpers import dataframe_from_response
|
|
7
|
-
from feldera.enums import PipelineFieldSelector
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class CallbackRunner(Thread):
|
|
11
|
-
def __init__(
|
|
12
|
-
self,
|
|
13
|
-
client: FelderaClient,
|
|
14
|
-
pipeline_name: str,
|
|
15
|
-
view_name: str,
|
|
16
|
-
callback: Callable[[pd.DataFrame, int], None],
|
|
17
|
-
):
|
|
18
|
-
super().__init__()
|
|
19
|
-
self.daemon = True
|
|
20
|
-
self.client: FelderaClient = client
|
|
21
|
-
self.pipeline_name: str = pipeline_name
|
|
22
|
-
self.view_name: str = view_name
|
|
23
|
-
self.callback: Callable[[pd.DataFrame, int], None] = callback
|
|
24
|
-
self.schema: Optional[dict] = None
|
|
25
|
-
|
|
26
|
-
def run(self):
|
|
27
|
-
"""
|
|
28
|
-
The main loop of the thread. Listens for data and calls the callback function on each chunk of data received.
|
|
29
|
-
|
|
30
|
-
:meta private:
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
pipeline = self.client.get_pipeline(
|
|
34
|
-
self.pipeline_name, PipelineFieldSelector.ALL
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
schemas = pipeline.tables + pipeline.views
|
|
38
|
-
for schema in schemas:
|
|
39
|
-
if schema.name == self.view_name:
|
|
40
|
-
self.schema = schema
|
|
41
|
-
break
|
|
42
|
-
|
|
43
|
-
if self.schema is None:
|
|
44
|
-
raise ValueError(
|
|
45
|
-
f"Table or View {self.view_name} not found in the pipeline schema."
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
gen_obj = self.client.listen_to_pipeline(
|
|
49
|
-
self.pipeline_name,
|
|
50
|
-
self.view_name,
|
|
51
|
-
format="json",
|
|
52
|
-
case_sensitive=self.schema.case_sensitive,
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
iterator = gen_obj()
|
|
56
|
-
|
|
57
|
-
for chunk in iterator:
|
|
58
|
-
chunk: dict = chunk
|
|
59
|
-
data: Optional[list[dict]] = chunk.get("json_data")
|
|
60
|
-
seq_no: Optional[int] = chunk.get("sequence_number")
|
|
61
|
-
if data is not None and seq_no is not None:
|
|
62
|
-
self.callback(
|
|
63
|
-
dataframe_from_response([data], self.schema.fields), seq_no
|
|
64
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|