feldera 0.67.0__tar.gz → 0.69.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.67.0 → feldera-0.69.0}/PKG-INFO +1 -1
- {feldera-0.67.0 → feldera-0.69.0}/feldera/enums.py +21 -0
- {feldera-0.67.0 → feldera-0.69.0}/feldera/pipeline.py +124 -8
- {feldera-0.67.0 → feldera-0.69.0}/feldera/rest/feldera_client.py +48 -2
- feldera-0.69.0/feldera/rest/feldera_config.py +49 -0
- {feldera-0.67.0 → feldera-0.69.0}/feldera/runtime_config.py +5 -2
- {feldera-0.67.0 → feldera-0.69.0}/feldera.egg-info/PKG-INFO +1 -1
- {feldera-0.67.0 → feldera-0.69.0}/feldera.egg-info/SOURCES.txt +1 -0
- {feldera-0.67.0 → feldera-0.69.0}/pyproject.toml +1 -1
- {feldera-0.67.0 → feldera-0.69.0}/tests/test_pipeline_builder.py +3 -7
- {feldera-0.67.0 → feldera-0.69.0}/README.md +0 -0
- {feldera-0.67.0 → feldera-0.69.0}/feldera/__init__.py +0 -0
- {feldera-0.67.0 → feldera-0.69.0}/feldera/_callback_runner.py +0 -0
- {feldera-0.67.0 → feldera-0.69.0}/feldera/_helpers.py +0 -0
- {feldera-0.67.0 → feldera-0.69.0}/feldera/output_handler.py +0 -0
- {feldera-0.67.0 → feldera-0.69.0}/feldera/pipeline_builder.py +0 -0
- {feldera-0.67.0 → feldera-0.69.0}/feldera/rest/__init__.py +0 -0
- {feldera-0.67.0 → feldera-0.69.0}/feldera/rest/_httprequests.py +0 -0
- {feldera-0.67.0 → feldera-0.69.0}/feldera/rest/config.py +0 -0
- {feldera-0.67.0 → feldera-0.69.0}/feldera/rest/errors.py +0 -0
- {feldera-0.67.0 → feldera-0.69.0}/feldera/rest/pipeline.py +0 -0
- {feldera-0.67.0 → feldera-0.69.0}/feldera/rest/sql_table.py +0 -0
- {feldera-0.67.0 → feldera-0.69.0}/feldera/rest/sql_view.py +0 -0
- {feldera-0.67.0 → feldera-0.69.0}/feldera.egg-info/dependency_links.txt +0 -0
- {feldera-0.67.0 → feldera-0.69.0}/feldera.egg-info/requires.txt +0 -0
- {feldera-0.67.0 → feldera-0.69.0}/feldera.egg-info/top_level.txt +0 -0
- {feldera-0.67.0 → feldera-0.69.0}/setup.cfg +0 -0
- {feldera-0.67.0 → feldera-0.69.0}/tests/test_pipeline.py +0 -0
- {feldera-0.67.0 → feldera-0.69.0}/tests/test_udf.py +0 -0
- {feldera-0.67.0 → feldera-0.69.0}/tests/test_variant.py +0 -0
|
@@ -237,3 +237,24 @@ class ProgramStatus(Enum):
|
|
|
237
237
|
"""
|
|
238
238
|
|
|
239
239
|
return self.error
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class CheckpointStatus(Enum):
|
|
243
|
+
Success = 1
|
|
244
|
+
Failure = 2
|
|
245
|
+
InProgress = 3
|
|
246
|
+
Unknown = 4
|
|
247
|
+
|
|
248
|
+
def __init__(self, value):
|
|
249
|
+
self.error: Optional[str] = None
|
|
250
|
+
self._value_ = value
|
|
251
|
+
|
|
252
|
+
def __eq__(self, other):
|
|
253
|
+
return self.value == other.value
|
|
254
|
+
|
|
255
|
+
def get_error(self) -> Optional[str]:
|
|
256
|
+
"""
|
|
257
|
+
Returns the error, if any.
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
return self.error
|
|
@@ -9,7 +9,7 @@ from collections import deque
|
|
|
9
9
|
from queue import Queue
|
|
10
10
|
|
|
11
11
|
from feldera.rest.errors import FelderaAPIError
|
|
12
|
-
from feldera.enums import PipelineStatus, ProgramStatus
|
|
12
|
+
from feldera.enums import PipelineStatus, ProgramStatus, CheckpointStatus
|
|
13
13
|
from feldera.rest.pipeline import Pipeline as InnerPipeline
|
|
14
14
|
from feldera.rest.feldera_client import FelderaClient
|
|
15
15
|
from feldera._callback_runner import _CallbackRunnerInstruction, CallbackRunner
|
|
@@ -104,7 +104,9 @@ class Pipeline:
|
|
|
104
104
|
tbl.name.lower() for tbl in pipeline.tables
|
|
105
105
|
]:
|
|
106
106
|
raise ValueError(
|
|
107
|
-
f"Cannot push to table '{
|
|
107
|
+
f"Cannot push to table '{
|
|
108
|
+
table_name
|
|
109
|
+
}': table with this name does not exist in the '{self.name}' pipeline"
|
|
108
110
|
)
|
|
109
111
|
else:
|
|
110
112
|
# consider validating the schema here
|
|
@@ -297,10 +299,14 @@ class Pipeline:
|
|
|
297
299
|
elapsed = time.monotonic() - start_time
|
|
298
300
|
if elapsed > timeout_s:
|
|
299
301
|
raise TimeoutError(
|
|
300
|
-
f"timeout ({timeout_s}s) reached while waiting for pipeline '{
|
|
302
|
+
f"timeout ({timeout_s}s) reached while waiting for pipeline '{
|
|
303
|
+
self.name
|
|
304
|
+
}' to complete"
|
|
301
305
|
)
|
|
302
306
|
logging.debug(
|
|
303
|
-
f"waiting for pipeline {self.name} to complete: elapsed time {
|
|
307
|
+
f"waiting for pipeline {self.name} to complete: elapsed time {
|
|
308
|
+
elapsed
|
|
309
|
+
}s, timeout: {timeout_s}s"
|
|
304
310
|
)
|
|
305
311
|
|
|
306
312
|
metrics: dict = self.client.get_pipeline_stats(self.name).get(
|
|
@@ -407,11 +413,15 @@ resume a paused pipeline."""
|
|
|
407
413
|
"""
|
|
408
414
|
if idle_interval_s > timeout_s:
|
|
409
415
|
raise ValueError(
|
|
410
|
-
f"idle interval ({idle_interval_s}s) cannot be larger than timeout ({
|
|
416
|
+
f"idle interval ({idle_interval_s}s) cannot be larger than timeout ({
|
|
417
|
+
timeout_s
|
|
418
|
+
}s)"
|
|
411
419
|
)
|
|
412
420
|
if poll_interval_s > timeout_s:
|
|
413
421
|
raise ValueError(
|
|
414
|
-
f"poll interval ({poll_interval_s}s) cannot be larger than timeout ({
|
|
422
|
+
f"poll interval ({poll_interval_s}s) cannot be larger than timeout ({
|
|
423
|
+
timeout_s
|
|
424
|
+
}s)"
|
|
415
425
|
)
|
|
416
426
|
if poll_interval_s > idle_interval_s:
|
|
417
427
|
raise ValueError(
|
|
@@ -547,15 +557,121 @@ resume a paused pipeline."""
|
|
|
547
557
|
if err.status_code == 404:
|
|
548
558
|
raise RuntimeError(f"Pipeline with name {name} not found")
|
|
549
559
|
|
|
550
|
-
def checkpoint(self):
|
|
560
|
+
def checkpoint(self, wait: bool = False, timeout_s=300) -> int:
|
|
551
561
|
"""
|
|
552
562
|
Checkpoints this pipeline, if fault-tolerance is enabled.
|
|
553
563
|
Fault Tolerance in Feldera: <https://docs.feldera.com/pipelines/fault-tolerance/>
|
|
554
564
|
|
|
565
|
+
:param wait: If true, will block until the checkpoint completes.
|
|
566
|
+
:param timeout_s: The maximum time (in seconds) to wait for the checkpoint to complete.
|
|
567
|
+
|
|
555
568
|
:raises FelderaAPIError: If checkpointing is not enabled.
|
|
556
569
|
"""
|
|
557
570
|
|
|
558
|
-
self.client.checkpoint_pipeline(self.name)
|
|
571
|
+
seq = self.client.checkpoint_pipeline(self.name)
|
|
572
|
+
|
|
573
|
+
if not wait:
|
|
574
|
+
return seq
|
|
575
|
+
|
|
576
|
+
start = time.time()
|
|
577
|
+
|
|
578
|
+
while True:
|
|
579
|
+
elapsed = time.monotonic() - start
|
|
580
|
+
if elapsed > timeout_s:
|
|
581
|
+
raise TimeoutError(
|
|
582
|
+
f"timeout ({timeout_s}s) reached while waiting for pipeline '{
|
|
583
|
+
self.name
|
|
584
|
+
}' to make checkpoint '{seq}'"
|
|
585
|
+
)
|
|
586
|
+
status = self.checkpoint_status(seq)
|
|
587
|
+
if status == CheckpointStatus.InProgress:
|
|
588
|
+
time.sleep(0.1)
|
|
589
|
+
continue
|
|
590
|
+
|
|
591
|
+
return status
|
|
592
|
+
|
|
593
|
+
return seq
|
|
594
|
+
|
|
595
|
+
def checkpoint_status(self, seq: int) -> CheckpointStatus:
|
|
596
|
+
"""
|
|
597
|
+
Checks the status of the given checkpoint.
|
|
598
|
+
|
|
599
|
+
:param seq: The checkpoint sequence number.
|
|
600
|
+
"""
|
|
601
|
+
|
|
602
|
+
resp = self.client.checkpoint_pipeline_status(self.name)
|
|
603
|
+
success = resp.get("success")
|
|
604
|
+
if seq == success:
|
|
605
|
+
return CheckpointStatus.Success
|
|
606
|
+
|
|
607
|
+
fail = resp.get("failure") or {}
|
|
608
|
+
if seq == fail.get("sequence_number"):
|
|
609
|
+
failure = CheckpointStatus.Failure
|
|
610
|
+
failure.error = fail.get("error", "")
|
|
611
|
+
return failure
|
|
612
|
+
|
|
613
|
+
if (success is None) or seq > success:
|
|
614
|
+
return CheckpointStatus.InProgress
|
|
615
|
+
|
|
616
|
+
if seq < success:
|
|
617
|
+
return CheckpointStatus.Unknown
|
|
618
|
+
|
|
619
|
+
def sync_checkpoint(self, wait: bool = False, timeout_s=300) -> str:
|
|
620
|
+
"""
|
|
621
|
+
Syncs this checkpoint to object store.
|
|
622
|
+
|
|
623
|
+
:param wait: If true, will block until the checkpoint sync opeartion completes.
|
|
624
|
+
:param timeout_s: The maximum time (in seconds) to wait for the checkpoint to complete syncing.
|
|
625
|
+
|
|
626
|
+
:raises FelderaAPIError: If no checkpoints have been made.
|
|
627
|
+
"""
|
|
628
|
+
|
|
629
|
+
uuid = self.client.sync_checkpoint(self.name)
|
|
630
|
+
|
|
631
|
+
if not wait:
|
|
632
|
+
return uuid
|
|
633
|
+
|
|
634
|
+
start = time.time()
|
|
635
|
+
|
|
636
|
+
while True:
|
|
637
|
+
elapsed = time.monotonic() - start
|
|
638
|
+
if elapsed > timeout_s:
|
|
639
|
+
raise TimeoutError(
|
|
640
|
+
f"timeout ({timeout_s}s) reached while waiting for pipeline '{
|
|
641
|
+
self.name
|
|
642
|
+
}' to sync checkpoint '{uuid}'"
|
|
643
|
+
)
|
|
644
|
+
status = self.sync_checkpoint_status(uuid)
|
|
645
|
+
if status in [CheckpointStatus.InProgress, CheckpointStatus.Unknown]:
|
|
646
|
+
time.sleep(0.1)
|
|
647
|
+
continue
|
|
648
|
+
|
|
649
|
+
return status
|
|
650
|
+
|
|
651
|
+
return uuid
|
|
652
|
+
|
|
653
|
+
def sync_checkpoint_status(self, uuid: str) -> CheckpointStatus:
|
|
654
|
+
"""
|
|
655
|
+
Checks the status of the given checkpoint sync operation.
|
|
656
|
+
If the checkpoint is currently being synchronized, returns
|
|
657
|
+
`CheckpointStatus.Unknown`.
|
|
658
|
+
|
|
659
|
+
:param uuid: The checkpoint uuid.
|
|
660
|
+
"""
|
|
661
|
+
|
|
662
|
+
resp = self.client.sync_checkpoint_status(self.name)
|
|
663
|
+
success = resp.get("success")
|
|
664
|
+
|
|
665
|
+
if uuid == success:
|
|
666
|
+
return CheckpointStatus.Success
|
|
667
|
+
|
|
668
|
+
fail = resp.get("failure") or {}
|
|
669
|
+
if uuid == fail.get("uuid"):
|
|
670
|
+
failure = CheckpointStatus.Failure
|
|
671
|
+
failure.error = fail.get("error", "")
|
|
672
|
+
return failure
|
|
673
|
+
|
|
674
|
+
return CheckpointStatus.Unknown
|
|
559
675
|
|
|
560
676
|
def query(self, query: str) -> Generator[Mapping[str, Any], None, None]:
|
|
561
677
|
"""
|
|
@@ -7,6 +7,7 @@ from decimal import Decimal
|
|
|
7
7
|
from typing import Generator
|
|
8
8
|
|
|
9
9
|
from feldera.rest.config import Config
|
|
10
|
+
from feldera.rest.feldera_config import FelderaConfig
|
|
10
11
|
from feldera.rest.errors import FelderaTimeoutError
|
|
11
12
|
from feldera.rest.pipeline import Pipeline
|
|
12
13
|
from feldera.rest._httprequests import HttpRequests
|
|
@@ -381,17 +382,53 @@ Reason: The pipeline is in a FAILED state due to the following error:
|
|
|
381
382
|
f"timeout error: pipeline '{pipeline_name}' did not suspend in {timeout_s} seconds"
|
|
382
383
|
)
|
|
383
384
|
|
|
384
|
-
def checkpoint_pipeline(self, pipeline_name: str):
|
|
385
|
+
def checkpoint_pipeline(self, pipeline_name: str) -> int:
|
|
385
386
|
"""
|
|
386
387
|
Checkpoint a fault-tolerant pipeline
|
|
387
388
|
|
|
388
389
|
:param pipeline_name: The name of the pipeline to checkpoint
|
|
389
390
|
"""
|
|
390
391
|
|
|
391
|
-
self.http.post(
|
|
392
|
+
resp = self.http.post(
|
|
392
393
|
path=f"/pipelines/{pipeline_name}/checkpoint",
|
|
393
394
|
)
|
|
394
395
|
|
|
396
|
+
return int(resp.get("checkpoint_sequence_number"))
|
|
397
|
+
|
|
398
|
+
def checkpoint_pipeline_status(self, pipeline_name: str) -> dict:
|
|
399
|
+
"""
|
|
400
|
+
Gets the checkpoint status
|
|
401
|
+
|
|
402
|
+
:param pipeline_name: The name of the pipeline to check the checkpoint status of.
|
|
403
|
+
"""
|
|
404
|
+
|
|
405
|
+
return self.http.get(path=f"/pipelines/{pipeline_name}/checkpoint_status")
|
|
406
|
+
|
|
407
|
+
def sync_checkpoint(self, pipeline_name: str) -> str:
|
|
408
|
+
"""
|
|
409
|
+
Triggers a checkpoint synchronization for the specified pipeline.
|
|
410
|
+
Check the status by calling `pipeline_sync_checkpoint_status`.
|
|
411
|
+
|
|
412
|
+
:param pipeline_name: Name of the pipeline whose checkpoint should be synchronized.
|
|
413
|
+
"""
|
|
414
|
+
|
|
415
|
+
resp = self.http.post(
|
|
416
|
+
path=f"/pipelines/{pipeline_name}/checkpoint/sync",
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
return resp.get("checkpoint_uuid")
|
|
420
|
+
|
|
421
|
+
def sync_checkpoint_status(self, pipeline_name: str) -> dict:
|
|
422
|
+
"""
|
|
423
|
+
Gets the checkpoint sync status of the pipeline
|
|
424
|
+
|
|
425
|
+
:param pipeline_name: The name of the pipeline to check the checkpoint synchronization status of.
|
|
426
|
+
"""
|
|
427
|
+
|
|
428
|
+
return self.http.get(
|
|
429
|
+
path=f"/pipelines/{pipeline_name}/checkpoint/sync_status",
|
|
430
|
+
)
|
|
431
|
+
|
|
395
432
|
def push_to_pipeline(
|
|
396
433
|
self,
|
|
397
434
|
pipeline_name: str,
|
|
@@ -674,3 +711,12 @@ Reason: The pipeline is in a FAILED state due to the following error:
|
|
|
674
711
|
self.http.post(
|
|
675
712
|
path=f"/pipelines/{pipeline_name}/tables/{table_name}/connectors/{connector_name}/start",
|
|
676
713
|
)
|
|
714
|
+
|
|
715
|
+
def get_config(self) -> FelderaConfig:
|
|
716
|
+
"""
|
|
717
|
+
Get general feldera configuration.
|
|
718
|
+
"""
|
|
719
|
+
|
|
720
|
+
resp = self.http.get(path="/config")
|
|
721
|
+
|
|
722
|
+
return FelderaConfig(resp)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class FelderaEdition(Enum):
|
|
5
|
+
"""
|
|
6
|
+
The compilation profile to use when compiling the program.
|
|
7
|
+
Represents the Feldera edition between Enterprise and Open source.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
ENTERPRISE = "Enterprise"
|
|
11
|
+
"""
|
|
12
|
+
The Enterprise version of Feldera.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
OPEN_SOURCE = "Open source"
|
|
16
|
+
"""
|
|
17
|
+
The open source version of Feldera.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def from_value(value):
|
|
22
|
+
error = None
|
|
23
|
+
if isinstance(value, dict):
|
|
24
|
+
error = value
|
|
25
|
+
value = list(value.keys())[0]
|
|
26
|
+
|
|
27
|
+
for member in FelderaEdition:
|
|
28
|
+
if member.value.lower() == value.lower():
|
|
29
|
+
member.error = error
|
|
30
|
+
return member
|
|
31
|
+
raise ValueError(f"Unknown value '{value}' for enum {FelderaEdition.__name__}")
|
|
32
|
+
|
|
33
|
+
def is_enterprise(self):
|
|
34
|
+
return self == FelderaEdition.ENTERPRISE
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class FelderaConfig:
|
|
38
|
+
"""
|
|
39
|
+
General configuration of the current Feldera instance.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, cfg: dict):
|
|
43
|
+
self.changelog_url = cfg.get("changelog_url")
|
|
44
|
+
self.edition = FelderaEdition.from_value(cfg.get("edition"))
|
|
45
|
+
self.license_validity = cfg.get("license_validity")
|
|
46
|
+
self.revision = cfg.get("revision")
|
|
47
|
+
self.telemetry = cfg.get("telemetry")
|
|
48
|
+
self.update_info = cfg.get("update_info")
|
|
49
|
+
self.version = cfg.get("version")
|
|
@@ -63,7 +63,7 @@ class RuntimeConfig:
|
|
|
63
63
|
def __init__(
|
|
64
64
|
self,
|
|
65
65
|
workers: Optional[int] = None,
|
|
66
|
-
storage: Optional[Storage] = None,
|
|
66
|
+
storage: Optional[Storage | bool] = None,
|
|
67
67
|
tracing: Optional[bool] = False,
|
|
68
68
|
tracing_endpoint_jaeger: Optional[str] = "",
|
|
69
69
|
cpu_profiler: bool = True,
|
|
@@ -74,7 +74,6 @@ class RuntimeConfig:
|
|
|
74
74
|
resources: Optional[Resources] = None,
|
|
75
75
|
):
|
|
76
76
|
self.workers = workers
|
|
77
|
-
self.storage = storage
|
|
78
77
|
self.tracing = tracing
|
|
79
78
|
self.tracing_endpoint_jaeger = tracing_endpoint_jaeger
|
|
80
79
|
self.cpu_profiler = cpu_profiler
|
|
@@ -84,6 +83,10 @@ class RuntimeConfig:
|
|
|
84
83
|
self.provisioning_timeout_secs = provisioning_timeout_secs
|
|
85
84
|
if resources is not None:
|
|
86
85
|
self.resources = resources.__dict__
|
|
86
|
+
if isinstance(storage, bool):
|
|
87
|
+
self.storage = storage
|
|
88
|
+
if isinstance(storage, Storage):
|
|
89
|
+
self.storage = storage.__dict__
|
|
87
90
|
|
|
88
91
|
@classmethod
|
|
89
92
|
def from_dict(cls, d: Mapping[str, Any]):
|
|
@@ -706,12 +706,8 @@ Code snippet:
|
|
|
706
706
|
|
|
707
707
|
pipeline.delete()
|
|
708
708
|
|
|
709
|
-
# doesn't run in CI
|
|
710
709
|
def test_suspend(self):
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
if in_ci == "1":
|
|
714
|
-
# if running in CI, skip the test
|
|
710
|
+
if not TEST_CLIENT.get_config().edition.is_enterprise():
|
|
715
711
|
return
|
|
716
712
|
|
|
717
713
|
TBL_NAME = "items"
|
|
@@ -1031,13 +1027,13 @@ Code snippet:
|
|
|
1031
1027
|
pipeline.pause()
|
|
1032
1028
|
|
|
1033
1029
|
got_err: str = err.exception.args[0].strip()
|
|
1034
|
-
assert "
|
|
1030
|
+
assert "causes overflow" in got_err
|
|
1035
1031
|
|
|
1036
1032
|
with self.assertRaises(RuntimeError) as err:
|
|
1037
1033
|
pipeline.start()
|
|
1038
1034
|
|
|
1039
1035
|
got_err: str = err.exception.args[0].strip()
|
|
1040
|
-
assert "
|
|
1036
|
+
assert "causes overflow" in got_err
|
|
1041
1037
|
|
|
1042
1038
|
pipeline.shutdown()
|
|
1043
1039
|
pipeline.delete()
|
|
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
|
|
File without changes
|