feldera 0.96.0__py3-none-any.whl → 0.98.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.

Potentially problematic release.


This version of feldera might be problematic. Click here for more details.

@@ -36,8 +36,8 @@ class FelderaClient:
36
36
  """
37
37
  A client for the Feldera HTTP API
38
38
 
39
- A client instance is needed for every Feldera API method to know the location of
40
- Feldera and its permissions.
39
+ A client instance is needed for every Feldera API method to know the
40
+ location of Feldera and its permissions.
41
41
  """
42
42
 
43
43
  def __init__(
@@ -50,7 +50,8 @@ class FelderaClient:
50
50
  """
51
51
  :param url: The url to Feldera API (ex: https://try.feldera.com)
52
52
  :param api_key: The optional API key for Feldera
53
- :param timeout: (optional) The amount of time in seconds that the client will wait for a response before timing
53
+ :param timeout: (optional) The amount of time in seconds that the client
54
+ will wait for a response before timing
54
55
  out.
55
56
  :param requests_verify: The `verify` parameter passed to the requests
56
57
  library. `True` by default.
@@ -162,7 +163,8 @@ class FelderaClient:
162
163
 
163
164
  def create_or_update_pipeline(self, pipeline: Pipeline) -> Pipeline:
164
165
  """
165
- Create a pipeline if it doesn't exist or update a pipeline and wait for it to compile
166
+ Create a pipeline if it doesn't exist or update a pipeline and wait for
167
+ it to compile
166
168
  """
167
169
 
168
170
  body = {
@@ -187,7 +189,8 @@ class FelderaClient:
187
189
  Incrementally update the pipeline SQL
188
190
 
189
191
  :param name: The name of the pipeline
190
- :param sql: The SQL snippet. Replaces the existing SQL code with this one.
192
+ :param sql: The SQL snippet. Replaces the existing SQL code with this
193
+ one.
191
194
  """
192
195
 
193
196
  self.http.patch(
@@ -222,7 +225,8 @@ class FelderaClient:
222
225
  """
223
226
 
224
227
  :param pipeline_name: The name of the pipeline to start
225
- :param timeout_s: The amount of time in seconds to wait for the pipeline to start. 300 seconds by default.
228
+ :param timeout_s: The amount of time in seconds to wait for the pipeline
229
+ to start. 300 seconds by default.
226
230
  """
227
231
 
228
232
  if timeout_s is None:
@@ -250,7 +254,7 @@ class FelderaClient:
250
254
  elif status == "Failed":
251
255
  raise RuntimeError(
252
256
  f"""Unable to START the pipeline.
253
- Reason: The pipeline is in a FAILED state due to the following error:
257
+ Reason: The pipeline is in a STOPPED state due to the following error:
254
258
  {resp.deployment_error.get("message", "")}"""
255
259
  )
256
260
 
@@ -269,8 +273,10 @@ Reason: The pipeline is in a FAILED state due to the following error:
269
273
  Stop a pipeline
270
274
 
271
275
  :param pipeline_name: The name of the pipeline to stop
272
- :param error_message: The error message to show if the pipeline is in FAILED state
273
- :param timeout_s: The amount of time in seconds to wait for the pipeline to pause. 300 seconds by default.
276
+ :param error_message: The error message to show if the pipeline is in
277
+ STOPPED state due to a failure.
278
+ :param timeout_s: The amount of time in seconds to wait for the pipeline
279
+ to pause. 300 seconds by default.
274
280
  """
275
281
 
276
282
  if timeout_s is None:
@@ -298,10 +304,14 @@ Reason: The pipeline is in a FAILED state due to the following error:
298
304
 
299
305
  if status == "Paused":
300
306
  break
301
- elif status == "Failed":
307
+ elif (
308
+ status == "Stopped"
309
+ and len(resp.deployment_error or {}) > 0
310
+ and resp.deployment_desired_status == "Stopped"
311
+ ):
302
312
  raise RuntimeError(
303
313
  error_message
304
- + f"""Reason: The pipeline is in a FAILED state due to the following error:
314
+ + f"""Reason: The pipeline is in a STOPPED state due to the following error:
305
315
  {resp.deployment_error.get("message", "")}"""
306
316
  )
307
317
 
@@ -310,19 +320,27 @@ Reason: The pipeline is in a FAILED state due to the following error:
310
320
  )
311
321
  time.sleep(0.1)
312
322
 
313
- def shutdown_pipeline(self, pipeline_name: str, timeout_s: Optional[float] = 300):
323
+ def stop_pipeline(
324
+ self, pipeline_name: str, force: bool, timeout_s: Optional[float] = 300
325
+ ):
314
326
  """
315
- Shutdown a pipeline
327
+ Stop a pipeline
316
328
 
317
- :param pipeline_name: The name of the pipeline to shut down
318
- :param timeout_s: The amount of time in seconds to wait for the pipeline to shut down. Default is 300 seconds.
329
+ :param pipeline_name: The name of the pipeline to stop
330
+ :param force: Set True to immediately scale compute resources to zero.
331
+ Set False to automatically checkpoint before stopping.
332
+ :param timeout_s: The amount of time in seconds to wait for the pipeline
333
+ to stop. Default is 300 seconds.
319
334
  """
320
335
 
321
336
  if timeout_s is None:
322
337
  timeout_s = 300
323
338
 
339
+ params = {"force": str(force).lower()}
340
+
324
341
  self.http.post(
325
- path=f"/pipelines/{pipeline_name}/shutdown",
342
+ path=f"/pipelines/{pipeline_name}/stop",
343
+ params=params,
326
344
  )
327
345
 
328
346
  start = time.monotonic()
@@ -330,56 +348,55 @@ Reason: The pipeline is in a FAILED state due to the following error:
330
348
  while time.monotonic() - start < timeout_s:
331
349
  status = self.get_pipeline(pipeline_name).deployment_status
332
350
 
333
- if status == "Shutdown":
351
+ if status == "Stopped":
334
352
  return
335
353
 
336
354
  logging.debug(
337
- "still shutting down %s, waiting for 100 more milliseconds",
355
+ "still stopping %s, waiting for 100 more milliseconds",
338
356
  pipeline_name,
339
357
  )
340
358
  time.sleep(0.1)
341
359
 
342
360
  raise FelderaTimeoutError(
343
- f"timeout error: pipeline '{pipeline_name}' did not shutdown in {timeout_s} seconds"
361
+ f"timeout error: pipeline '{pipeline_name}' did not stop in {
362
+ timeout_s
363
+ } seconds"
344
364
  )
345
365
 
346
- def suspend_pipeline(self, pipeline_name: str, timeout_s: Optional[float] = 300):
366
+ def clear_storage(self, pipeline_name: str, timeout_s: Optional[float] = 300):
347
367
  """
348
- Suspend a pipeline
368
+ Clears the storage from the pipeline.
369
+ This operation cannot be canceled.
349
370
 
350
- :param pipeline_name: The name of the pipeline to suspend
351
- :param timeout_s: The amount of time in seconds to wait for the pipeline to suspend. Default is 300 seconds.
371
+ :param pipeline_name: The name of the pipeline
372
+ :param timeout_s: The amount of time in seconds to wait for the storage
373
+ to clear. Default is 300 seconds.
352
374
  """
353
-
354
375
  if timeout_s is None:
355
376
  timeout_s = 300
356
377
 
357
378
  self.http.post(
358
- path=f"/pipelines/{pipeline_name}/suspend",
379
+ path=f"/pipelines/{pipeline_name}/clear",
359
380
  )
360
381
 
361
382
  start = time.monotonic()
362
383
 
363
384
  while time.monotonic() - start < timeout_s:
364
- resp = self.get_pipeline(pipeline_name)
365
- status = resp.deployment_status
385
+ status = self.get_pipeline(pipeline_name).storage_status
366
386
 
367
- if status == "Suspended":
387
+ if status == "Cleared":
368
388
  return
369
- elif status == "Failed":
370
- raise RuntimeError(
371
- f"""Unable to Suspend pipeline '{pipeline_name}'.\nReason: The pipeline is in a FAILED state due to the following error:
372
- {resp.deployment_error.get("message", "")}"""
373
- )
374
389
 
375
390
  logging.debug(
376
- "still suspending %s, waiting for 100 more milliseconds",
391
+ "still clearing %s, waiting for 100 more milliseconds",
377
392
  pipeline_name,
378
393
  )
379
394
  time.sleep(0.1)
380
395
 
381
396
  raise FelderaTimeoutError(
382
- f"timeout error: pipeline '{pipeline_name}' did not suspend in {timeout_s} seconds"
397
+ f"timeout error: pipeline '{pipeline_name}' did not clear storage in {
398
+ timeout_s
399
+ } seconds"
383
400
  )
384
401
 
385
402
  def checkpoint_pipeline(self, pipeline_name: str) -> int:
@@ -685,7 +702,9 @@ Reason: The pipeline is in a FAILED state due to the following error:
685
702
  """
686
703
 
687
704
  self.http.post(
688
- path=f"/pipelines/{pipeline_name}/tables/{table_name}/connectors/{connector_name}/pause",
705
+ path=f"/pipelines/{pipeline_name}/tables/{table_name}/connectors/{
706
+ connector_name
707
+ }/pause",
689
708
  )
690
709
 
691
710
  def resume_connector(
@@ -709,7 +728,9 @@ Reason: The pipeline is in a FAILED state due to the following error:
709
728
  """
710
729
 
711
730
  self.http.post(
712
- path=f"/pipelines/{pipeline_name}/tables/{table_name}/connectors/{connector_name}/start",
731
+ path=f"/pipelines/{pipeline_name}/tables/{table_name}/connectors/{
732
+ connector_name
733
+ }/start",
713
734
  )
714
735
 
715
736
  def get_config(self) -> FelderaConfig:
feldera/rest/pipeline.py CHANGED
@@ -56,6 +56,7 @@ class Pipeline:
56
56
  self.program_status: Optional[str] = None
57
57
  self.program_status_since: Optional[str] = None
58
58
  self.program_error: Optional[dict] = None
59
+ self.storage_status: Optional[str] = None
59
60
 
60
61
  @classmethod
61
62
  def from_dict(cls, d: Mapping[str, Any]):
feldera/stats.py ADDED
@@ -0,0 +1,149 @@
1
+ from typing import Mapping, Any, Optional, List
2
+ from feldera.enums import PipelineStatus
3
+ from datetime import datetime
4
+ import uuid
5
+
6
+
7
+ class PipelineStatistics:
8
+ """
9
+ Represents statistics reported by a pipeline's "/stats" endpoint.
10
+ """
11
+
12
+ def __init__(self):
13
+ """
14
+ Initializes as an empty set of statistics.
15
+ """
16
+
17
+ self.global_metrics: GlobalPipelineMetrics = GlobalPipelineMetrics()
18
+ self.suspend_error: Optional[Any] = None
19
+ self.inputs: Mapping[List[InputEndpointStatus()]] = {}
20
+ self.outputs: Mapping[List[OutputEndpointStatus]] = {}
21
+
22
+ @classmethod
23
+ def from_dict(cls, d: Mapping[str, Any]):
24
+ pipeline = cls()
25
+ pipeline.global_metrics = GlobalPipelineMetrics.from_dict(d["global_metrics"])
26
+ pipeline.inputs = [
27
+ InputEndpointStatus.from_dict(input) for input in d["inputs"]
28
+ ]
29
+ pipeline.inputs = [
30
+ OutputEndpointStatus().from_dict(output) for output in d["outputs"]
31
+ ]
32
+ return pipeline
33
+
34
+
35
+ class GlobalPipelineMetrics:
36
+ """Represents the "global_metrics" object within the pipeline's
37
+ "/stats" endpoint reply.
38
+ """
39
+
40
+ def __init__(self):
41
+ """
42
+ Initializes as an empty set of metrics.
43
+ """
44
+ self.state: Optional[PipelineStatus] = None
45
+ self.bootstrap_in_progress: Optional[bool] = None
46
+ self.rss_bytes: Optional[int] = None
47
+ self.cpu_msecs: Optional[int] = None
48
+ self.start_time: Optional[datetime] = None
49
+ self.incarnation_uuid: Optional[uuid] = None
50
+ self.storage_bytes: Optional[int] = None
51
+ self.storage_mb_secs: Optional[int] = None
52
+ self.runtime_elapsed_msecs: Optional[int] = None
53
+ self.buffered_input_records: Optional[int] = None
54
+ self.total_input_records: Optional[int] = None
55
+ self.total_processed_records: Optional[int] = None
56
+ self.total_completed_records: Optional[int] = None
57
+ self.pipeline_complete: Optional[bool] = None
58
+
59
+ @classmethod
60
+ def from_dict(cls, d: Mapping[str, Any]):
61
+ metrics = cls()
62
+ metrics.__dict__.update(d)
63
+ metrics.state = PipelineStatus.from_str(d["state"])
64
+ metrics.incarnation_uuid = uuid.UUID(d["incarnation_uuid"])
65
+ metrics.start_time = datetime.fromtimestamp(d["start_time"])
66
+ return metrics
67
+
68
+
69
+ class InputEndpointStatus:
70
+ """Represents one member of the "inputs" array within the
71
+ pipeline's "/stats" endpoint reply.
72
+ """
73
+
74
+ def __init__(self):
75
+ """Initializes an empty status."""
76
+ self.endpoint_name: Optional[str] = None
77
+ self.config: Optional[Mapping] = None
78
+ self.metrics: Optional[InputEndpointMetrics] = None
79
+ self.fatal_error: Optional[str] = None
80
+ self.paused: Optional[bool] = None
81
+ self.barrier: Optional[bool] = None
82
+
83
+ @classmethod
84
+ def from_dict(cls, d: Mapping[str, Any]):
85
+ status = cls()
86
+ status.__dict__.update(d)
87
+ status.metrics = InputEndpointMetrics.from_dict(d["metrics"])
88
+ return status
89
+
90
+
91
+ class InputEndpointMetrics:
92
+ """Represents the "metrics" member within an input endpoint status
93
+ in the pipeline's "/stats" endpoint reply.
94
+ """
95
+
96
+ def __init__(self):
97
+ self.total_bytes: Optional[int] = None
98
+ self.total_records: Optional[int] = None
99
+ self.buffered_records: Optional[int] = None
100
+ self.num_transport_errors: Optional[int] = None
101
+ self.num_parse_errors: Optional[int] = None
102
+ self.end_of_input: Optional[bool] = None
103
+
104
+ @classmethod
105
+ def from_dict(cls, d: Mapping[str, Any]):
106
+ metrics = cls()
107
+ metrics.__dict__.update(d)
108
+ return metrics
109
+
110
+
111
+ class OutputEndpointStatus:
112
+ """Represents one member of the "outputs" array within the
113
+ pipeline's "/stats" endpoint reply.
114
+ """
115
+
116
+ def __init__(self):
117
+ """Initializes an empty status."""
118
+ self.endpoint_name: Optional[str] = None
119
+ self.config: Optional[Mapping] = None
120
+ self.metrics: Optional[OutputEndpointMetrics] = None
121
+ self.fatal_error: Optional[str] = None
122
+
123
+ @classmethod
124
+ def from_dict(cls, d: Mapping[str, Any]):
125
+ status = cls()
126
+ status.__dict__.update(d)
127
+ status.metrics = OutputEndpointMetrics.from_dict(d["metrics"])
128
+ return status
129
+
130
+
131
+ class OutputEndpointMetrics:
132
+ """Represents the "metrics" member within an output endpoint status
133
+ in the pipeline's "/stats" endpoint reply.
134
+ """
135
+
136
+ def __init__(self):
137
+ self.transmitted_records: Optional[int] = None
138
+ self.transmitted_bytes: Optional[int] = None
139
+ self.queued_records: Optional[int] = None
140
+ self.queued_batches: Optional[int] = None
141
+ self.num_encode_errors: Optional[int] = None
142
+ self.num_transport_errors: Optional[int] = None
143
+ self.total_processed_input_records: Optional[int] = None
144
+
145
+ @classmethod
146
+ def from_dict(cls, d: Mapping[str, Any]):
147
+ metrics = cls()
148
+ metrics.__dict__.update(d)
149
+ return metrics
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: feldera
3
- Version: 0.96.0
3
+ Version: 0.98.0
4
4
  Summary: The feldera python client
5
5
  Author-email: Feldera Team <dev@feldera.com>
6
6
  License: MIT
@@ -10,10 +10,9 @@ Project-URL: Repository, https://github.com/feldera/feldera
10
10
  Project-URL: Issues, https://github.com/feldera/feldera/issues
11
11
  Keywords: feldera,python
12
12
  Classifier: License :: OSI Approved :: MIT License
13
- Classifier: Programming Language :: Python :: 3.10
14
- Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
15
14
  Classifier: Operating System :: OS Independent
16
- Requires-Python: >=3.10
15
+ Requires-Python: >=3.12
17
16
  Description-Content-Type: text/markdown
18
17
  Requires-Dist: requests
19
18
  Requires-Dist: pandas>=2.1.2
@@ -81,6 +80,8 @@ To run unit tests:
81
80
  (cd python && python3 -m unittest)
82
81
  ```
83
82
 
83
+ > ⚠️ Running the unit tests will **delete all existing pipelines**.
84
+
84
85
  The following command runs end-to-end tests. You'll need a pipeline
85
86
  manager running at `http://localhost:8080`. For the pipeline builder
86
87
  tests, you'll also need a broker available at `localhost:9092` and
@@ -88,13 +89,13 @@ tests, you'll also need a broker available at `localhost:9092` and
88
89
  set the environment variables listed in `python/tests/__init__.py`.)
89
90
 
90
91
  ```bash
91
- (cd python/tests && python3 -m pytest .)
92
+ (cd python && python3 -m pytest tests)
92
93
  ```
93
94
 
94
95
  To run tests from a specific file:
95
96
 
96
97
  ```bash
97
- (cd python/tests && python3 -m unittest ./tests/path-to-file.py)
98
+ (cd python && python3 -m unittest ./tests/path-to-file.py)
98
99
  ```
99
100
 
100
101
  To run the aggregate tests use:
@@ -103,3 +104,20 @@ To run the aggregate tests use:
103
104
  cd python
104
105
  PYTHONPATH=`pwd` python3 ./tests/aggregate_tests/main.py
105
106
  ```
107
+
108
+ ## Linting and formatting
109
+
110
+ Use [Ruff] to run the lint checks that will be executed by the
111
+ precommit hook when a PR is submitted:
112
+
113
+ ```bash
114
+ ruff check python/
115
+ ```
116
+
117
+ To reformat the code in the same way as the precommit hook:
118
+
119
+ ```bash
120
+ ruff format
121
+ ```
122
+
123
+ [Ruff]: https://github.com/astral-sh/ruff
@@ -1,21 +1,22 @@
1
1
  feldera/__init__.py,sha256=PxkgCtEAuFwo4u8NGEDio-bF3M-GnbeV45tAQVoBbqE,297
2
2
  feldera/_callback_runner.py,sha256=Tdf6BXN4zppyoy8t_y-Ooa3B0wEfvyezMHU9jxY2ZhA,4713
3
3
  feldera/_helpers.py,sha256=rN0WuGSCCQlXWFMimZUQrgs-LJAfUo074d79sLElncQ,3023
4
- feldera/enums.py,sha256=ydzCQHno6as8rXUuoRYw4QE1QXCfpADJwGQZNjMS58A,7774
4
+ feldera/enums.py,sha256=CWPvFIdxMuadS548IAiYjPjBrf3fZe7DXi2VCJGjlhc,8671
5
5
  feldera/output_handler.py,sha256=64J3ljhOaKIhxdjOKYi-BUz_HnMwROfmN8eE-btYygU,1930
6
- feldera/pipeline.py,sha256=SRUT9p6_-00zCljPNNRkA7eLKUqSJszGovEnFhssnk4,34731
7
- feldera/pipeline_builder.py,sha256=4rmklRZ0-otvTUb-HTESfNsJopEK-E2jxpJXiYlKpps,3664
6
+ feldera/pipeline.py,sha256=OClrsP1Fgg-FjDLCh1doORq6-dQT4lIX1yUDDZtQR0o,34523
7
+ feldera/pipeline_builder.py,sha256=2nu88M6W-a2tBTTwBdUG5Bcqe8y5Gsnvlc5j6BW4OfU,3726
8
8
  feldera/runtime_config.py,sha256=EDvnakyaRTHUS8GLd6TqLZ_jsGp7_4fbcPnSfQCw1k0,3501
9
+ feldera/stats.py,sha256=XBhkRsV7FXErwWuPP0i3q9W77mzkMo-oThPVEZy5y3U,5028
9
10
  feldera/rest/__init__.py,sha256=Eg-EKUU3RSTDcdxTR_7wNDnCly8VpXEzsZCQUmf-y2M,308
10
11
  feldera/rest/_httprequests.py,sha256=e22YbpzOzy7MGo7hk9MOU7ZRTj3F314grY0Ygr-_goI,6636
11
12
  feldera/rest/config.py,sha256=DYzZKngDEhouTEwqVFd-rDrBN9tWqsU07Jl_BTT4mXs,1008
12
13
  feldera/rest/errors.py,sha256=b4i2JjrbSmej7jdko_FL8UeXklLKenSipwMT80jowaM,1720
13
- feldera/rest/feldera_client.py,sha256=03XZ8gjgviy3uVvu_IWSoz61yVN8zeH1-ugNHdBMldc,24693
14
+ feldera/rest/feldera_client.py,sha256=Vax97r4ZP5ZQ0MsRYFjAxmIXXlCaLNH7EiMN5SfxtJg,25057
14
15
  feldera/rest/feldera_config.py,sha256=1pnGbLFMSLvp7Qh_OlPLALSKCSHIktNWKvx6gYU00U4,1374
15
- feldera/rest/pipeline.py,sha256=a1lx-64SYak5mHX5yKElVijdfaAt5sDYVhVIXCJ97QQ,2839
16
+ feldera/rest/pipeline.py,sha256=-dGGUdtHMABKrQEclaeuwGI_FOCrQOk6p2aCFV0FdU8,2890
16
17
  feldera/rest/sql_table.py,sha256=qrw-YwMzx5T81zDefNO1KOx7EyypFz1vPwGBzSUB7kc,652
17
18
  feldera/rest/sql_view.py,sha256=hN12mPM0mvwLCIPYywpb12s9Hd2Ws31IlTMXPriMisw,644
18
- feldera-0.96.0.dist-info/METADATA,sha256=_CW7wPtCfGj0Lv7uqIO68xM5aKA1KKsJ560CvDPMlqg,2587
19
- feldera-0.96.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
- feldera-0.96.0.dist-info/top_level.txt,sha256=fB6yTqrQiO6RCbY1xP2T_mpPoTjDFtJvkJJodiee7d0,8
21
- feldera-0.96.0.dist-info/RECORD,,
19
+ feldera-0.98.0.dist-info/METADATA,sha256=-fCOuWRvRjySTJpooV4_TDu0IqXSbDLGZN1Eay_boXE,2892
20
+ feldera-0.98.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
+ feldera-0.98.0.dist-info/top_level.txt,sha256=fB6yTqrQiO6RCbY1xP2T_mpPoTjDFtJvkJJodiee7d0,8
22
+ feldera-0.98.0.dist-info/RECORD,,