feldera 0.166.0__py3-none-any.whl → 0.177.0__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.
- feldera/_callback_runner.py +51 -25
- feldera/_helpers.py +7 -1
- feldera/enums.py +12 -0
- feldera/output_handler.py +4 -0
- feldera/pipeline.py +44 -1
- feldera/pipeline_builder.py +11 -7
- feldera/rest/_httprequests.py +11 -11
- feldera/rest/feldera_client.py +85 -29
- feldera/testutils.py +0 -1
- {feldera-0.166.0.dist-info → feldera-0.177.0.dist-info}/METADATA +1 -1
- {feldera-0.166.0.dist-info → feldera-0.177.0.dist-info}/RECORD +13 -13
- {feldera-0.166.0.dist-info → feldera-0.177.0.dist-info}/WHEEL +0 -0
- {feldera-0.166.0.dist-info → feldera-0.177.0.dist-info}/top_level.txt +0 -0
feldera/_callback_runner.py
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
from threading import Thread
|
|
2
|
-
from typing import Callable, Optional
|
|
1
|
+
from threading import Thread, Event
|
|
2
|
+
from typing import Callable, List, Optional, Mapping, Any
|
|
3
3
|
|
|
4
4
|
import pandas as pd
|
|
5
5
|
from feldera import FelderaClient
|
|
6
6
|
from feldera._helpers import dataframe_from_response
|
|
7
7
|
from feldera.enums import PipelineFieldSelector
|
|
8
|
+
from feldera.rest.sql_table import SQLTable
|
|
9
|
+
from feldera.rest.sql_view import SQLView
|
|
10
|
+
from feldera.rest.pipeline import Pipeline
|
|
8
11
|
|
|
9
12
|
|
|
10
13
|
class CallbackRunner(Thread):
|
|
@@ -15,7 +18,17 @@ class CallbackRunner(Thread):
|
|
|
15
18
|
view_name: str,
|
|
16
19
|
callback: Callable[[pd.DataFrame, int], None],
|
|
17
20
|
exception_callback: Callable[[BaseException], None],
|
|
21
|
+
event: Event,
|
|
18
22
|
):
|
|
23
|
+
"""
|
|
24
|
+
:param client: The :class:`.FelderaClient` to use.
|
|
25
|
+
:param pipeline_name: The name of the current pipeline.
|
|
26
|
+
:param view_name: The name of the view we are listening to.
|
|
27
|
+
:param callback: The callback function to call on the data we receive.
|
|
28
|
+
:param exception_callback: The callback function to call when an exception occurs.
|
|
29
|
+
:param event: The event to wait for before starting the callback runner.
|
|
30
|
+
"""
|
|
31
|
+
|
|
19
32
|
super().__init__()
|
|
20
33
|
self.daemon = True
|
|
21
34
|
self.client: FelderaClient = client
|
|
@@ -23,7 +36,32 @@ class CallbackRunner(Thread):
|
|
|
23
36
|
self.view_name: str = view_name
|
|
24
37
|
self.callback: Callable[[pd.DataFrame, int], None] = callback
|
|
25
38
|
self.exception_callback: Callable[[BaseException], None] = exception_callback
|
|
26
|
-
self.
|
|
39
|
+
self.event: Event = event
|
|
40
|
+
|
|
41
|
+
self.pipeline: Pipeline = self.client.get_pipeline(
|
|
42
|
+
self.pipeline_name, PipelineFieldSelector.ALL
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
view_schema = None
|
|
46
|
+
|
|
47
|
+
schemas: List[SQLTable | SQLView] = self.pipeline.tables + self.pipeline.views
|
|
48
|
+
for schema in schemas:
|
|
49
|
+
if schema.name == self.view_name:
|
|
50
|
+
view_schema = schema
|
|
51
|
+
break
|
|
52
|
+
|
|
53
|
+
if view_schema is None:
|
|
54
|
+
raise ValueError(
|
|
55
|
+
f"Table or View {self.view_name} not found in the pipeline schema."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
self.schema: SQLTable | SQLView = view_schema
|
|
59
|
+
|
|
60
|
+
def to_callback(self, chunk: Mapping[str, Any]):
|
|
61
|
+
data: Optional[list[Mapping[str, Any]]] = chunk.get("json_data")
|
|
62
|
+
seq_no: Optional[int] = chunk.get("sequence_number")
|
|
63
|
+
if data is not None and seq_no is not None:
|
|
64
|
+
self.callback(dataframe_from_response([data], self.schema.fields), seq_no)
|
|
27
65
|
|
|
28
66
|
def run(self):
|
|
29
67
|
"""
|
|
@@ -33,21 +71,6 @@ class CallbackRunner(Thread):
|
|
|
33
71
|
"""
|
|
34
72
|
|
|
35
73
|
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
74
|
gen_obj = self.client.listen_to_pipeline(
|
|
52
75
|
self.pipeline_name,
|
|
53
76
|
self.view_name,
|
|
@@ -57,13 +80,16 @@ class CallbackRunner(Thread):
|
|
|
57
80
|
|
|
58
81
|
iterator = gen_obj()
|
|
59
82
|
|
|
83
|
+
# Trigger the HTTP call
|
|
84
|
+
chunk = next(iterator)
|
|
85
|
+
|
|
86
|
+
# Unblock the main thread
|
|
87
|
+
self.event.set()
|
|
88
|
+
|
|
89
|
+
self.to_callback(chunk)
|
|
90
|
+
|
|
60
91
|
for chunk in iterator:
|
|
61
|
-
chunk
|
|
62
|
-
|
|
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
|
-
)
|
|
92
|
+
self.to_callback(chunk)
|
|
93
|
+
|
|
68
94
|
except BaseException as e:
|
|
69
95
|
self.exception_callback(e)
|
feldera/_helpers.py
CHANGED
|
@@ -2,6 +2,7 @@ import uuid
|
|
|
2
2
|
|
|
3
3
|
import pandas as pd
|
|
4
4
|
from decimal import Decimal
|
|
5
|
+
from typing import Mapping, Any
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
def sql_type_to_pandas_type(sql_type: str):
|
|
@@ -60,9 +61,14 @@ def ensure_dataframe_has_columns(df: pd.DataFrame):
|
|
|
60
61
|
)
|
|
61
62
|
|
|
62
63
|
|
|
63
|
-
def dataframe_from_response(
|
|
64
|
+
def dataframe_from_response(
|
|
65
|
+
buffer: list[list[Mapping[str, Any]]], fields: list[Mapping[str, Any]]
|
|
66
|
+
):
|
|
64
67
|
"""
|
|
65
68
|
Converts the response from Feldera to a pandas DataFrame.
|
|
69
|
+
|
|
70
|
+
:param buffer: A buffer of a list of JSON formatted output of the view you are listening to.
|
|
71
|
+
:param fields: The schema (list of fields) of the view you are listening to.
|
|
66
72
|
"""
|
|
67
73
|
|
|
68
74
|
pd_schema = {}
|
feldera/enums.py
CHANGED
|
@@ -352,3 +352,15 @@ class BootstrapPolicy(Enum):
|
|
|
352
352
|
AWAIT_APPROVAL = "await_approval"
|
|
353
353
|
ALLOW = "allow"
|
|
354
354
|
REJECT = "reject"
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class CompletionTokenStatus(Enum):
|
|
358
|
+
COMPLETE = "complete"
|
|
359
|
+
"""
|
|
360
|
+
Feldera has completed processing all inputs represented by this token.
|
|
361
|
+
"""
|
|
362
|
+
|
|
363
|
+
IN_PROGRESS = "inprogress"
|
|
364
|
+
"""
|
|
365
|
+
Feldera is still processing the inputs represented by this token.
|
|
366
|
+
"""
|
feldera/output_handler.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import pandas as pd
|
|
2
2
|
|
|
3
3
|
from typing import Optional
|
|
4
|
+
from threading import Event
|
|
4
5
|
|
|
5
6
|
from feldera import FelderaClient
|
|
6
7
|
from feldera._callback_runner import CallbackRunner
|
|
@@ -23,6 +24,7 @@ class OutputHandler:
|
|
|
23
24
|
self.view_name: str = view_name
|
|
24
25
|
self.buffer: list[pd.DataFrame] = []
|
|
25
26
|
self.exception: Optional[BaseException] = None
|
|
27
|
+
self.event = Event()
|
|
26
28
|
|
|
27
29
|
# the callback that is passed to the `CallbackRunner`
|
|
28
30
|
def callback(df: pd.DataFrame, _: int):
|
|
@@ -39,6 +41,7 @@ class OutputHandler:
|
|
|
39
41
|
self.view_name,
|
|
40
42
|
callback,
|
|
41
43
|
exception_callback,
|
|
44
|
+
self.event,
|
|
42
45
|
)
|
|
43
46
|
|
|
44
47
|
def start(self):
|
|
@@ -47,6 +50,7 @@ class OutputHandler:
|
|
|
47
50
|
"""
|
|
48
51
|
|
|
49
52
|
self.handler.start()
|
|
53
|
+
_ = self.event.wait()
|
|
50
54
|
|
|
51
55
|
def to_pandas(self, clear_buffer: bool = True):
|
|
52
56
|
"""
|
feldera/pipeline.py
CHANGED
|
@@ -7,11 +7,13 @@ import pandas
|
|
|
7
7
|
from uuid import UUID
|
|
8
8
|
|
|
9
9
|
from typing import List, Dict, Callable, Optional, Generator, Mapping, Any
|
|
10
|
+
from threading import Event
|
|
10
11
|
from collections import deque
|
|
11
12
|
|
|
12
13
|
from feldera.rest.errors import FelderaAPIError
|
|
13
14
|
from feldera.enums import (
|
|
14
15
|
BootstrapPolicy,
|
|
16
|
+
CompletionTokenStatus,
|
|
15
17
|
PipelineFieldSelector,
|
|
16
18
|
PipelineStatus,
|
|
17
19
|
ProgramStatus,
|
|
@@ -294,10 +296,12 @@ class Pipeline:
|
|
|
294
296
|
if self.status() not in [PipelineStatus.RUNNING, PipelineStatus.PAUSED]:
|
|
295
297
|
raise RuntimeError("Pipeline must be running or paused to listen to output")
|
|
296
298
|
|
|
299
|
+
event = Event()
|
|
297
300
|
handler = CallbackRunner(
|
|
298
|
-
self.client, self.name, view_name, callback, lambda exception: None
|
|
301
|
+
self.client, self.name, view_name, callback, lambda exception: None, event
|
|
299
302
|
)
|
|
300
303
|
handler.start()
|
|
304
|
+
event.wait()
|
|
301
305
|
|
|
302
306
|
def wait_for_completion(
|
|
303
307
|
self, force_stop: bool = False, timeout_s: float | None = None
|
|
@@ -696,6 +700,17 @@ metrics"""
|
|
|
696
700
|
err.message = f"Pipeline with name {name} not found"
|
|
697
701
|
raise err
|
|
698
702
|
|
|
703
|
+
@staticmethod
|
|
704
|
+
def all(client: FelderaClient) -> List["Pipeline"]:
|
|
705
|
+
"""
|
|
706
|
+
Get all pipelines.
|
|
707
|
+
|
|
708
|
+
:param client: The FelderaClient instance.
|
|
709
|
+
:return: A list of Pipeline objects.
|
|
710
|
+
"""
|
|
711
|
+
|
|
712
|
+
return [Pipeline._from_inner(p, client) for p in client.pipelines()]
|
|
713
|
+
|
|
699
714
|
def checkpoint(self, wait: bool = False, timeout_s: Optional[float] = None) -> int:
|
|
700
715
|
"""
|
|
701
716
|
Checkpoints this pipeline.
|
|
@@ -1377,3 +1392,31 @@ pipeline '{self.name}' to sync checkpoint '{uuid}'"""
|
|
|
1377
1392
|
print(f"Support bundle written to {path}")
|
|
1378
1393
|
|
|
1379
1394
|
return support_bundle_bytes
|
|
1395
|
+
|
|
1396
|
+
def generate_completion_token(self, table_name: str, connector_name: str) -> str:
|
|
1397
|
+
"""
|
|
1398
|
+
Returns a completion token that can be passed to :meth:`.Pipeline.completion_token_status` to
|
|
1399
|
+
check whether the pipeline has finished processing all inputs received from the connector before
|
|
1400
|
+
the token was generated.
|
|
1401
|
+
"""
|
|
1402
|
+
|
|
1403
|
+
return self.client.generate_completion_token(
|
|
1404
|
+
self.name, table_name, connector_name
|
|
1405
|
+
)
|
|
1406
|
+
|
|
1407
|
+
def completion_token_status(self, token: str) -> CompletionTokenStatus:
|
|
1408
|
+
"""
|
|
1409
|
+
Returns the status of the completion token.
|
|
1410
|
+
"""
|
|
1411
|
+
|
|
1412
|
+
if self.client.completion_token_processed(self.name, token):
|
|
1413
|
+
return CompletionTokenStatus.COMPLETE
|
|
1414
|
+
else:
|
|
1415
|
+
return CompletionTokenStatus.IN_PROGRESS
|
|
1416
|
+
|
|
1417
|
+
def wait_for_token(self, token: str):
|
|
1418
|
+
"""
|
|
1419
|
+
Blocks until the pipeline processes all inputs represented by the completion token.
|
|
1420
|
+
"""
|
|
1421
|
+
|
|
1422
|
+
self.client.wait_for_token(self.name, token)
|
feldera/pipeline_builder.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from typing import Optional
|
|
3
3
|
|
|
4
|
+
from feldera.enums import CompilationProfile, PipelineFieldSelector
|
|
5
|
+
from feldera.pipeline import Pipeline
|
|
6
|
+
from feldera.rest.errors import FelderaAPIError
|
|
4
7
|
from feldera.rest.feldera_client import FelderaClient
|
|
5
8
|
from feldera.rest.pipeline import Pipeline as InnerPipeline
|
|
6
|
-
from feldera.pipeline import Pipeline
|
|
7
|
-
from feldera.enums import CompilationProfile, PipelineFieldSelector
|
|
8
9
|
from feldera.runtime_config import RuntimeConfig
|
|
9
|
-
from feldera.rest.errors import FelderaAPIError
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class PipelineBuilder:
|
|
@@ -49,10 +49,11 @@ class PipelineBuilder:
|
|
|
49
49
|
"FELDERA_RUNTIME_VERSION", runtime_version
|
|
50
50
|
)
|
|
51
51
|
|
|
52
|
-
def create(self) -> Pipeline:
|
|
52
|
+
def create(self, wait: bool = True) -> Pipeline:
|
|
53
53
|
"""
|
|
54
54
|
Create the pipeline if it does not exist.
|
|
55
55
|
|
|
56
|
+
:param wait: Whether to wait for the pipeline to be created. True by default
|
|
56
57
|
:return: The created pipeline
|
|
57
58
|
"""
|
|
58
59
|
|
|
@@ -82,17 +83,20 @@ class PipelineBuilder:
|
|
|
82
83
|
runtime_config=self.runtime_config.to_dict(),
|
|
83
84
|
)
|
|
84
85
|
|
|
85
|
-
inner = self.client.create_pipeline(inner)
|
|
86
|
+
inner = self.client.create_pipeline(inner, wait=wait)
|
|
86
87
|
pipeline = Pipeline(self.client)
|
|
87
88
|
pipeline._inner = inner
|
|
88
89
|
|
|
89
90
|
return pipeline
|
|
90
91
|
|
|
91
|
-
def create_or_replace(self) -> Pipeline:
|
|
92
|
+
def create_or_replace(self, wait: bool = True) -> Pipeline:
|
|
92
93
|
"""
|
|
93
94
|
Creates a pipeline if it does not exist and replaces it if it exists.
|
|
94
95
|
|
|
95
96
|
If the pipeline exists and is running, it will be stopped and replaced.
|
|
97
|
+
|
|
98
|
+
:param wait: Whether to wait for the pipeline to be created. True by default
|
|
99
|
+
:return: The created pipeline
|
|
96
100
|
"""
|
|
97
101
|
|
|
98
102
|
if self.name is None or self.sql is None:
|
|
@@ -121,7 +125,7 @@ class PipelineBuilder:
|
|
|
121
125
|
runtime_config=self.runtime_config.to_dict(),
|
|
122
126
|
)
|
|
123
127
|
|
|
124
|
-
inner = self.client.create_or_update_pipeline(inner)
|
|
128
|
+
inner = self.client.create_or_update_pipeline(inner, wait=wait)
|
|
125
129
|
pipeline = Pipeline(self.client)
|
|
126
130
|
pipeline._inner = inner
|
|
127
131
|
|
feldera/rest/_httprequests.py
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import logging
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any, Callable, List, Mapping, Optional, Sequence, Union
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
import requests
|
|
7
|
+
from requests.packages import urllib3
|
|
4
8
|
|
|
9
|
+
from feldera.rest.config import Config
|
|
5
10
|
from feldera.rest.errors import (
|
|
6
11
|
FelderaAPIError,
|
|
7
|
-
FelderaTimeoutError,
|
|
8
12
|
FelderaCommunicationError,
|
|
13
|
+
FelderaTimeoutError,
|
|
9
14
|
)
|
|
10
15
|
|
|
11
|
-
import json
|
|
12
|
-
import requests
|
|
13
|
-
from requests.packages import urllib3
|
|
14
|
-
from typing import Callable, Optional, Any, Union, Mapping, Sequence, List
|
|
15
|
-
import time
|
|
16
|
-
|
|
17
16
|
|
|
18
17
|
def json_serialize(body: Any) -> str:
|
|
19
18
|
# serialize as string if this object cannot be serialized (e.g. UUID)
|
|
@@ -111,11 +110,12 @@ class HttpRequests:
|
|
|
111
110
|
logging.debug("got response: %s", str(resp))
|
|
112
111
|
return resp
|
|
113
112
|
except FelderaAPIError as err:
|
|
114
|
-
# Only retry on 503
|
|
115
|
-
if err.status_code
|
|
113
|
+
# Only retry on 503 and 408
|
|
114
|
+
if err.status_code in [503, 408]:
|
|
116
115
|
if attempt < max_retries:
|
|
117
116
|
logging.warning(
|
|
118
|
-
"HTTP
|
|
117
|
+
"HTTP %d received for %s, retrying (%d/%d)...",
|
|
118
|
+
err.status_code,
|
|
119
119
|
path,
|
|
120
120
|
attempt + 1,
|
|
121
121
|
max_retries,
|
feldera/rest/feldera_client.py
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import
|
|
2
|
-
from typing import Any, Dict, Optional
|
|
1
|
+
import json
|
|
3
2
|
import logging
|
|
3
|
+
import pathlib
|
|
4
4
|
import time
|
|
5
|
-
import json
|
|
6
5
|
from decimal import Decimal
|
|
7
|
-
from typing import Generator, Mapping
|
|
6
|
+
from typing import Any, Dict, Generator, Mapping, Optional
|
|
8
7
|
from urllib.parse import quote
|
|
8
|
+
|
|
9
9
|
import requests
|
|
10
10
|
|
|
11
11
|
from feldera.enums import BootstrapPolicy, PipelineFieldSelector, PipelineStatus
|
|
12
|
+
from feldera.rest._helpers import client_version
|
|
13
|
+
from feldera.rest._httprequests import HttpRequests
|
|
12
14
|
from feldera.rest.config import Config
|
|
15
|
+
from feldera.rest.errors import FelderaAPIError, FelderaTimeoutError
|
|
13
16
|
from feldera.rest.feldera_config import FelderaConfig
|
|
14
|
-
from feldera.rest.errors import FelderaTimeoutError, FelderaAPIError
|
|
15
17
|
from feldera.rest.pipeline import Pipeline
|
|
16
|
-
from feldera.rest._httprequests import HttpRequests
|
|
17
|
-
from feldera.rest._helpers import client_version
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def _validate_no_none_keys_in_map(data):
|
|
@@ -126,13 +126,15 @@ class FelderaClient:
|
|
|
126
126
|
|
|
127
127
|
return runtime_config
|
|
128
128
|
|
|
129
|
-
def pipelines(
|
|
129
|
+
def pipelines(
|
|
130
|
+
self, selector: PipelineFieldSelector = PipelineFieldSelector.STATUS
|
|
131
|
+
) -> list[Pipeline]:
|
|
130
132
|
"""
|
|
131
133
|
Get all pipelines
|
|
132
134
|
"""
|
|
133
135
|
|
|
134
136
|
resp = self.http.get(
|
|
135
|
-
path="/pipelines",
|
|
137
|
+
path=f"/pipelines?selector={selector.value}",
|
|
136
138
|
)
|
|
137
139
|
|
|
138
140
|
return [Pipeline.from_dict(pipeline) for pipeline in resp]
|
|
@@ -256,12 +258,12 @@ Reason: The pipeline is in a STOPPED state due to the following error:
|
|
|
256
258
|
)
|
|
257
259
|
time.sleep(0.1)
|
|
258
260
|
|
|
259
|
-
def create_pipeline(self, pipeline: Pipeline) -> Pipeline:
|
|
261
|
+
def create_pipeline(self, pipeline: Pipeline, wait: bool = True) -> Pipeline:
|
|
260
262
|
"""
|
|
261
263
|
Create a pipeline if it doesn't exist and wait for it to compile
|
|
262
264
|
|
|
263
|
-
|
|
264
|
-
:
|
|
265
|
+
:param pipeline: The pipeline to create
|
|
266
|
+
:param wait: Whether to wait for the pipeline to compile. True by default
|
|
265
267
|
"""
|
|
266
268
|
|
|
267
269
|
body = {
|
|
@@ -279,12 +281,21 @@ Reason: The pipeline is in a STOPPED state due to the following error:
|
|
|
279
281
|
body=body,
|
|
280
282
|
)
|
|
281
283
|
|
|
284
|
+
if not wait:
|
|
285
|
+
return pipeline
|
|
286
|
+
|
|
282
287
|
return self.__wait_for_compilation(pipeline.name)
|
|
283
288
|
|
|
284
|
-
def create_or_update_pipeline(
|
|
289
|
+
def create_or_update_pipeline(
|
|
290
|
+
self, pipeline: Pipeline, wait: bool = True
|
|
291
|
+
) -> Pipeline:
|
|
285
292
|
"""
|
|
286
293
|
Create a pipeline if it doesn't exist or update a pipeline and wait for
|
|
287
294
|
it to compile
|
|
295
|
+
|
|
296
|
+
:param pipeline: The pipeline to create or update
|
|
297
|
+
:param wait: Whether to wait for the pipeline to compile. True by default
|
|
298
|
+
:return: The created or updated pipeline
|
|
288
299
|
"""
|
|
289
300
|
|
|
290
301
|
body = {
|
|
@@ -302,6 +313,9 @@ Reason: The pipeline is in a STOPPED state due to the following error:
|
|
|
302
313
|
body=body,
|
|
303
314
|
)
|
|
304
315
|
|
|
316
|
+
if not wait:
|
|
317
|
+
return pipeline
|
|
318
|
+
|
|
305
319
|
return self.__wait_for_compilation(pipeline.name)
|
|
306
320
|
|
|
307
321
|
def patch_pipeline(
|
|
@@ -883,6 +897,35 @@ Reason: The pipeline is in a STOPPED state due to the following error:
|
|
|
883
897
|
|
|
884
898
|
return token
|
|
885
899
|
|
|
900
|
+
def completion_token_processed(self, pipeline_name: str, token: str) -> bool:
|
|
901
|
+
"""
|
|
902
|
+
Check whether the pipeline has finished processing all inputs received from the connector before
|
|
903
|
+
the token was generated.
|
|
904
|
+
|
|
905
|
+
:param pipeline_name: The name of the pipeline
|
|
906
|
+
:param token: The token to check for completion
|
|
907
|
+
:return: True if the pipeline has finished processing all inputs represented by the token, False otherwise
|
|
908
|
+
"""
|
|
909
|
+
|
|
910
|
+
params = {
|
|
911
|
+
"token": token,
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
resp = self.http.get(
|
|
915
|
+
path=f"/pipelines/{quote(pipeline_name, safe='')}/completion_status",
|
|
916
|
+
params=params,
|
|
917
|
+
)
|
|
918
|
+
|
|
919
|
+
status: Optional[str] = resp.get("status")
|
|
920
|
+
|
|
921
|
+
if status is None:
|
|
922
|
+
raise FelderaAPIError(
|
|
923
|
+
f"got empty status when checking for completion status for token: {token}",
|
|
924
|
+
resp,
|
|
925
|
+
)
|
|
926
|
+
|
|
927
|
+
return status.lower() == "complete"
|
|
928
|
+
|
|
886
929
|
def wait_for_token(
|
|
887
930
|
self, pipeline_name: str, token: str, timeout_s: Optional[float] = None
|
|
888
931
|
):
|
|
@@ -896,10 +939,6 @@ Reason: The pipeline is in a STOPPED state due to the following error:
|
|
|
896
939
|
to process these records.
|
|
897
940
|
"""
|
|
898
941
|
|
|
899
|
-
params = {
|
|
900
|
-
"token": token,
|
|
901
|
-
}
|
|
902
|
-
|
|
903
942
|
start = time.monotonic()
|
|
904
943
|
end = start + timeout_s if timeout_s else None
|
|
905
944
|
initial_backoff = 0.1
|
|
@@ -916,18 +955,7 @@ Reason: The pipeline is in a STOPPED state due to the following error:
|
|
|
916
955
|
+ f" {timeout_s}"
|
|
917
956
|
)
|
|
918
957
|
|
|
919
|
-
|
|
920
|
-
path=f"/pipelines/{pipeline_name}/completion_status", params=params
|
|
921
|
-
)
|
|
922
|
-
|
|
923
|
-
status: Optional[str] = resp.get("status")
|
|
924
|
-
if status is None:
|
|
925
|
-
raise FelderaAPIError(
|
|
926
|
-
f"got empty status when checking for completion status for token: {token}",
|
|
927
|
-
resp,
|
|
928
|
-
)
|
|
929
|
-
|
|
930
|
-
if status.lower() == "complete":
|
|
958
|
+
if self.completion_token_processed(pipeline_name, token):
|
|
931
959
|
break
|
|
932
960
|
|
|
933
961
|
elapsed = time.monotonic() - start
|
|
@@ -1191,3 +1219,31 @@ Reason: The pipeline is in a STOPPED state due to the following error:
|
|
|
1191
1219
|
buffer += chunk
|
|
1192
1220
|
|
|
1193
1221
|
return buffer
|
|
1222
|
+
|
|
1223
|
+
def generate_completion_token(
|
|
1224
|
+
self, pipeline_name: str, table_name: str, connector_name: str
|
|
1225
|
+
) -> str:
|
|
1226
|
+
"""
|
|
1227
|
+
Generate a completion token that can be passed to :meth:`.FelderaClient.completion_token_processed` to
|
|
1228
|
+
check whether the pipeline has finished processing all inputs received from the connector before
|
|
1229
|
+
the token was generated.
|
|
1230
|
+
|
|
1231
|
+
:param pipeline_name: The name of the pipeline
|
|
1232
|
+
:param table_name: The name of the table associated with this connector.
|
|
1233
|
+
:param connector_name: The name of the connector.
|
|
1234
|
+
|
|
1235
|
+
:raises FelderaAPIError: If the connector cannot be found, or if the pipeline is not running.
|
|
1236
|
+
"""
|
|
1237
|
+
|
|
1238
|
+
resp = self.http.get(
|
|
1239
|
+
path=f"/pipelines/{pipeline_name}/tables/{table_name}/connectors/{connector_name}/completion_token",
|
|
1240
|
+
)
|
|
1241
|
+
|
|
1242
|
+
token: str | None = resp.get("token")
|
|
1243
|
+
|
|
1244
|
+
if token is None:
|
|
1245
|
+
raise ValueError(
|
|
1246
|
+
"got invalid response from feldera when generating completion token"
|
|
1247
|
+
)
|
|
1248
|
+
|
|
1249
|
+
return token
|
feldera/testutils.py
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
feldera/__init__.py,sha256=EiY3bTj_mnfNhCGrZo6J__brfovIJ-YYAdy77PyaEoo,378
|
|
2
|
-
feldera/_callback_runner.py,sha256=
|
|
3
|
-
feldera/_helpers.py,sha256=
|
|
4
|
-
feldera/enums.py,sha256=
|
|
5
|
-
feldera/output_handler.py,sha256=
|
|
6
|
-
feldera/pipeline.py,sha256=
|
|
7
|
-
feldera/pipeline_builder.py,sha256=
|
|
2
|
+
feldera/_callback_runner.py,sha256=cTYbUxSILZDcTrjsWWPiTFdMDlQ9FPoWiMqPfnvQGDg,3226
|
|
3
|
+
feldera/_helpers.py,sha256=wQX2d-C4an7xGXTg6VKo9VMBROXN7TUrS90LITFKQEw,3262
|
|
4
|
+
feldera/enums.py,sha256=p11z4PgdcL4UQLnaPPnZBB_dHTBpNaJLqsT7qJ-R0QA,8835
|
|
5
|
+
feldera/output_handler.py,sha256=VvDkPSfhllH3nEwg3nPkPk-uXGAXs0fdrZ0mrMqijJI,2229
|
|
6
|
+
feldera/pipeline.py,sha256=dDKu-RPjFx0PMpZet0_vaZh4FfuGNoIKP4xB3eM59mo,51855
|
|
7
|
+
feldera/pipeline_builder.py,sha256=qrXaQz0HGGNMo0wPJJSH-gooTVSlm1XODaK9643eoL8,4620
|
|
8
8
|
feldera/runtime_config.py,sha256=w6rPkZyijca9jY1G8PKeqP8txXjnn5MPJYmM7B8iE3U,4602
|
|
9
9
|
feldera/stats.py,sha256=YeDQwE_CixTMb2DjBCgt5jTaJAZRsrHtG-3pYuuII-8,5256
|
|
10
|
-
feldera/testutils.py,sha256=
|
|
10
|
+
feldera/testutils.py,sha256=ITIqXTRD44j4AhjF-41GOWKOYh3_BAV-GQzQ7HOkgDw,12571
|
|
11
11
|
feldera/testutils_oidc.py,sha256=hv0IQAcVOt4uysWW3iE-5p46ZGfL1X7Vt7PjRefkXz8,12552
|
|
12
12
|
feldera/rest/__init__.py,sha256=Eg-EKUU3RSTDcdxTR_7wNDnCly8VpXEzsZCQUmf-y2M,308
|
|
13
13
|
feldera/rest/_helpers.py,sha256=51oJV2pF4U1NBpjIEZuc9nIEcS0qlbUNJoefhiY-PmM,1129
|
|
14
|
-
feldera/rest/_httprequests.py,sha256=
|
|
14
|
+
feldera/rest/_httprequests.py,sha256=vMnWJEiXB5xd3zMdLE3y5w2Zg1lvTDGOE9Hqw5D6ByU,8641
|
|
15
15
|
feldera/rest/config.py,sha256=1_D22PQ1x50_aNGo-WwI-q0gBQID4qJGDJr4MURyYB4,1992
|
|
16
16
|
feldera/rest/errors.py,sha256=wKWwlL5WI3SVXC8rQTy3rS3wQ9Hjjn_EY7iRHXZFIVE,2341
|
|
17
|
-
feldera/rest/feldera_client.py,sha256=
|
|
17
|
+
feldera/rest/feldera_client.py,sha256=AA6wgmiHtzaj-71cyN7HZPS4TFsUPe8Hehfizrz6g6o,44350
|
|
18
18
|
feldera/rest/feldera_config.py,sha256=1pnGbLFMSLvp7Qh_OlPLALSKCSHIktNWKvx6gYU00U4,1374
|
|
19
19
|
feldera/rest/pipeline.py,sha256=Q6TM44a7-SYRhLWbBGQMriCOcMRDk4WVKjfxd8vumys,3696
|
|
20
20
|
feldera/rest/sql_table.py,sha256=qrw-YwMzx5T81zDefNO1KOx7EyypFz1vPwGBzSUB7kc,652
|
|
21
21
|
feldera/rest/sql_view.py,sha256=hN12mPM0mvwLCIPYywpb12s9Hd2Ws31IlTMXPriMisw,644
|
|
22
22
|
feldera/tests/test_datafusionize.py,sha256=NGriTaTWf_WnXFud1wmpFwLFa_-XGjfCh6La3dWc3QA,1337
|
|
23
|
-
feldera-0.
|
|
24
|
-
feldera-0.
|
|
25
|
-
feldera-0.
|
|
26
|
-
feldera-0.
|
|
23
|
+
feldera-0.177.0.dist-info/METADATA,sha256=QexyupSHzqF4FDRhGB_n18odxKrXPmsIygzNcBi3JbE,2306
|
|
24
|
+
feldera-0.177.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
25
|
+
feldera-0.177.0.dist-info/top_level.txt,sha256=fB6yTqrQiO6RCbY1xP2T_mpPoTjDFtJvkJJodiee7d0,8
|
|
26
|
+
feldera-0.177.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|