feldera 0.115.0__tar.gz → 0.117.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.

Files changed (33) hide show
  1. {feldera-0.115.0 → feldera-0.117.0}/PKG-INFO +1 -1
  2. {feldera-0.115.0 → feldera-0.117.0}/feldera/pipeline.py +87 -34
  3. {feldera-0.115.0 → feldera-0.117.0}/feldera/rest/feldera_client.py +117 -2
  4. {feldera-0.115.0 → feldera-0.117.0}/feldera/rest/pipeline.py +0 -1
  5. {feldera-0.115.0 → feldera-0.117.0}/feldera.egg-info/PKG-INFO +1 -1
  6. {feldera-0.115.0 → feldera-0.117.0}/feldera.egg-info/SOURCES.txt +1 -0
  7. {feldera-0.115.0 → feldera-0.117.0}/pyproject.toml +1 -1
  8. {feldera-0.115.0 → feldera-0.117.0}/tests/test_shared_pipeline1.py +31 -14
  9. feldera-0.117.0/tests/test_shared_pipeline_stress.py +26 -0
  10. {feldera-0.115.0 → feldera-0.117.0}/README.md +0 -0
  11. {feldera-0.115.0 → feldera-0.117.0}/feldera/__init__.py +0 -0
  12. {feldera-0.115.0 → feldera-0.117.0}/feldera/_callback_runner.py +0 -0
  13. {feldera-0.115.0 → feldera-0.117.0}/feldera/_helpers.py +0 -0
  14. {feldera-0.115.0 → feldera-0.117.0}/feldera/enums.py +0 -0
  15. {feldera-0.115.0 → feldera-0.117.0}/feldera/output_handler.py +0 -0
  16. {feldera-0.115.0 → feldera-0.117.0}/feldera/pipeline_builder.py +0 -0
  17. {feldera-0.115.0 → feldera-0.117.0}/feldera/rest/__init__.py +0 -0
  18. {feldera-0.115.0 → feldera-0.117.0}/feldera/rest/_helpers.py +0 -0
  19. {feldera-0.115.0 → feldera-0.117.0}/feldera/rest/_httprequests.py +0 -0
  20. {feldera-0.115.0 → feldera-0.117.0}/feldera/rest/config.py +0 -0
  21. {feldera-0.115.0 → feldera-0.117.0}/feldera/rest/errors.py +0 -0
  22. {feldera-0.115.0 → feldera-0.117.0}/feldera/rest/feldera_config.py +0 -0
  23. {feldera-0.115.0 → feldera-0.117.0}/feldera/rest/sql_table.py +0 -0
  24. {feldera-0.115.0 → feldera-0.117.0}/feldera/rest/sql_view.py +0 -0
  25. {feldera-0.115.0 → feldera-0.117.0}/feldera/runtime_config.py +0 -0
  26. {feldera-0.115.0 → feldera-0.117.0}/feldera/stats.py +0 -0
  27. {feldera-0.115.0 → feldera-0.117.0}/feldera.egg-info/dependency_links.txt +0 -0
  28. {feldera-0.115.0 → feldera-0.117.0}/feldera.egg-info/requires.txt +0 -0
  29. {feldera-0.115.0 → feldera-0.117.0}/feldera.egg-info/top_level.txt +0 -0
  30. {feldera-0.115.0 → feldera-0.117.0}/setup.cfg +0 -0
  31. {feldera-0.115.0 → feldera-0.117.0}/tests/test_pipeline_builder.py +0 -0
  32. {feldera-0.115.0 → feldera-0.117.0}/tests/test_shared_pipeline0.py +0 -0
  33. {feldera-0.115.0 → feldera-0.117.0}/tests/test_udf.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: feldera
3
- Version: 0.115.0
3
+ Version: 0.117.0
4
4
  Summary: The feldera python client
5
5
  Author-email: Feldera Team <dev@feldera.com>
6
6
  License: MIT
@@ -78,6 +78,11 @@ class Pipeline:
78
78
 
79
79
  return PipelineStatistics.from_dict(self.client.get_pipeline_stats(self.name))
80
80
 
81
+ def logs(self) -> Generator[str, None, None]:
82
+ """Gets the pipeline logs."""
83
+
84
+ return self.client.get_pipeline_logs(self.name)
85
+
81
86
  def input_pandas(self, table_name: str, df: pandas.DataFrame, force: bool = False):
82
87
  """
83
88
  Push all rows in a pandas DataFrame to the pipeline.
@@ -113,7 +118,8 @@ class Pipeline:
113
118
  tbl.name.lower() for tbl in pipeline.tables
114
119
  ]:
115
120
  raise ValueError(
116
- f"Cannot push to table '{table_name}': table with this name does not exist in the '{self.name}' pipeline"
121
+ f"Cannot push to table '{table_name}': table with this name"
122
+ f" does not exist in the '{self.name}' pipeline"
117
123
  )
118
124
  else:
119
125
  # consider validating the schema here
@@ -308,10 +314,12 @@ class Pipeline:
308
314
  elapsed = time.monotonic() - start_time
309
315
  if elapsed > timeout_s:
310
316
  raise TimeoutError(
311
- f"timeout ({timeout_s}s) reached while waiting for pipeline '{self.name}' to complete"
317
+ f"timeout ({timeout_s}s) reached while waiting for"
318
+ f" pipeline '{self.name}' to complete"
312
319
  )
313
320
  logging.debug(
314
- f"waiting for pipeline {self.name} to complete: elapsed time {elapsed}s, timeout: {timeout_s}s"
321
+ f"waiting for pipeline {self.name} to complete: elapsed"
322
+ f" time {elapsed}s, timeout: {timeout_s}s"
315
323
  )
316
324
 
317
325
  pipeline_complete: bool = self.stats().global_metrics.pipeline_complete
@@ -327,7 +335,7 @@ class Pipeline:
327
335
  if force_stop:
328
336
  self.stop(force=True)
329
337
 
330
- def start(self, timeout_s: Optional[float] = None):
338
+ def start(self, wait: bool = True, timeout_s: Optional[float] = None):
331
339
  """
332
340
  .. _start:
333
341
 
@@ -339,6 +347,7 @@ class Pipeline:
339
347
 
340
348
  :param timeout_s: The maximum time (in seconds) to wait for the
341
349
  pipeline to start.
350
+ :param wait: Set True to wait for the pipeline to start. True by default
342
351
 
343
352
  :raises RuntimeError: If the pipeline is not in STOPPED state.
344
353
  """
@@ -352,11 +361,21 @@ started. You can either stop the pipeline using the `Pipeline.stop()` \
352
361
  method or use `Pipeline.resume()` to resume a paused pipeline."""
353
362
  )
354
363
 
364
+ if not wait:
365
+ if len(self.views_tx) > 0:
366
+ raise ValueError(
367
+ "cannot start with 'wait=False' when output listeners are configured. Try setting 'wait=True'."
368
+ )
369
+
370
+ self.client.start_pipeline(self.name, wait=wait)
371
+
372
+ return
373
+
355
374
  self.client.pause_pipeline(
356
375
  self.name, "Unable to START the pipeline.\n", timeout_s
357
376
  )
358
377
  self.__setup_output_listeners()
359
- self.resume(timeout_s)
378
+ self.resume(timeout_s=timeout_s)
360
379
 
361
380
  def restart(self, timeout_s: Optional[float] = None):
362
381
  """
@@ -371,7 +390,7 @@ method or use `Pipeline.resume()` to resume a paused pipeline."""
371
390
  """
372
391
 
373
392
  self.stop(force=True, timeout_s=timeout_s)
374
- self.start(timeout_s)
393
+ self.start(timeout_s=timeout_s)
375
394
 
376
395
  def wait_for_idle(
377
396
  self,
@@ -398,11 +417,13 @@ method or use `Pipeline.resume()` to resume a paused pipeline."""
398
417
  """
399
418
  if idle_interval_s > timeout_s:
400
419
  raise ValueError(
401
- f"idle interval ({idle_interval_s}s) cannot be larger than timeout ({timeout_s}s)"
420
+ f"idle interval ({idle_interval_s}s) cannot be larger than"
421
+ f" timeout ({timeout_s}s)"
402
422
  )
403
423
  if poll_interval_s > timeout_s:
404
424
  raise ValueError(
405
- f"poll interval ({poll_interval_s}s) cannot be larger than timeout ({timeout_s}s)"
425
+ f"poll interval ({poll_interval_s}s) cannot be larger than"
426
+ f" timeout ({timeout_s}s)"
406
427
  )
407
428
  if poll_interval_s > idle_interval_s:
408
429
  raise ValueError(
@@ -449,7 +470,7 @@ metrics"""
449
470
  raise RuntimeError(f"waiting for idle reached timeout ({timeout_s}s)")
450
471
  time.sleep(poll_interval_s)
451
472
 
452
- def pause(self, timeout_s: Optional[float] = None):
473
+ def pause(self, wait: bool = True, timeout_s: Optional[float] = None):
453
474
  """
454
475
  Pause the pipeline.
455
476
 
@@ -457,13 +478,14 @@ metrics"""
457
478
  state. If the pipeline is already paused, it will remain in the PAUSED
458
479
  state.
459
480
 
481
+ :param wait: Set True to wait for the pipeline to pause. True by default
460
482
  :param timeout_s: The maximum time (in seconds) to wait for the
461
483
  pipeline to pause.
462
484
  """
463
485
 
464
- self.client.pause_pipeline(self.name, timeout_s=timeout_s)
486
+ self.client.pause_pipeline(self.name, wait=wait, timeout_s=timeout_s)
465
487
 
466
- def stop(self, force: bool, timeout_s: Optional[float] = None):
488
+ def stop(self, force: bool, wait: bool = True, timeout_s: Optional[float] = None):
467
489
  """
468
490
  Stops the pipeline.
469
491
 
@@ -471,34 +493,75 @@ metrics"""
471
493
 
472
494
  :param force: Set True to immediately scale compute resources to zero.
473
495
  Set False to automatically checkpoint before stopping.
496
+ :param wait: Set True to gracefully shutdown listeners and wait for the
497
+ pipeline to stop. True by default.
474
498
  :param timeout_s: The maximum time (in seconds) to wait for the
475
499
  pipeline to stop.
476
500
  """
477
501
 
478
- for view_queue in self.views_tx:
479
- for _, queue in view_queue.items():
480
- # sends a message to the callback runner to stop listening
481
- queue.put(_CallbackRunnerInstruction.RanToCompletion)
502
+ if wait:
503
+ for view_queue in self.views_tx:
504
+ for _, queue in view_queue.items():
505
+ # sends a message to the callback runner to stop listening
506
+ queue.put(_CallbackRunnerInstruction.RanToCompletion)
482
507
 
483
- if len(self.views_tx) > 0:
484
- for view_name, queue in self.views_tx.pop().items():
485
- # block until the callback runner has been stopped
486
- queue.join()
487
- import time
508
+ if len(self.views_tx) > 0:
509
+ for view_name, queue in self.views_tx.pop().items():
510
+ # block until the callback runner has been stopped
511
+ queue.join()
488
512
 
489
513
  time.sleep(3)
490
- self.client.stop_pipeline(self.name, force=force, timeout_s=timeout_s)
514
+ self.client.stop_pipeline(
515
+ self.name, force=force, wait=wait, timeout_s=timeout_s
516
+ )
491
517
 
492
- def resume(self, timeout_s: Optional[float] = None):
518
+ def resume(self, wait: bool = True, timeout_s: Optional[float] = None):
493
519
  """
494
520
  Resumes the pipeline from the PAUSED state. If the pipeline is already
495
521
  running, it will remain in the RUNNING state.
496
522
 
523
+ :param wait: Set True to wait for the pipeline to resume. True by default
497
524
  :param timeout_s: The maximum time (in seconds) to wait for the
498
525
  pipeline to resume.
499
526
  """
500
527
 
501
- self.client.start_pipeline(self.name, timeout_s=timeout_s)
528
+ self.client.start_pipeline(self.name, wait=wait, timeout_s=timeout_s)
529
+
530
+ def start_transaction(self):
531
+ """
532
+ Start a new transaction.
533
+
534
+ Returns:
535
+ Transaction ID.
536
+ """
537
+
538
+ self.client.start_transaction(self.name)
539
+
540
+ def commit_transaction(
541
+ self,
542
+ transaction_id: Optional[int] = None,
543
+ wait: bool = True,
544
+ timeout_s: Optional[float] = None,
545
+ ):
546
+ """
547
+ Commits the currently active transaction.
548
+
549
+ :param transaction_id: If provided, the function verifies that the currently active transaction matches this ID.
550
+ If the active transaction ID does not match, the function raises an error.
551
+
552
+ :param wait: If True, the function blocks until the transaction either commits successfully or the timeout is reached.
553
+ If False, the function initiates the commit and returns immediately without waiting for completion. The default value is True.
554
+
555
+ :param timeout_s: Maximum time (in seconds) to wait for the transaction to commit when `wait` is True.
556
+ If None, the function will wait indefinitely.
557
+
558
+ :raises RuntimeError: If there is currently no transaction in progress.
559
+ :raises ValueError: If the provided `transaction_id` does not match the current transaction.
560
+ :raises TimeoutError: If the transaction does not commit within the specified timeout (when `wait` is True).
561
+ :raises FelderaAPIError: If the pipeline fails to start a transaction.
562
+ """
563
+
564
+ self.client.commit_transaction(self.name, transaction_id, wait, timeout_s)
502
565
 
503
566
  def delete(self, clear_storage: bool = False):
504
567
  """
@@ -597,7 +660,7 @@ pipeline '{self.name}' to make checkpoint '{seq}'"""
597
660
  """
598
661
  Syncs this checkpoint to object store.
599
662
 
600
- :param wait: If true, will block until the checkpoint sync opeartion
663
+ :param wait: If true, will block until the checkpoint sync operation
601
664
  completes.
602
665
  :param timeout_s: The maximum time (in seconds) to wait for the
603
666
  checkpoint to complete syncing.
@@ -956,16 +1019,6 @@ pipeline '{self.name}' to sync checkpoint '{uuid}'"""
956
1019
  self.refresh()
957
1020
  return self._inner.deployment_location
958
1021
 
959
- def program_binary_url(self) -> str:
960
- """
961
- Return the program binary URL of the pipeline.
962
- This is the URL where the compiled program binary can be downloaded
963
- from.
964
- """
965
-
966
- self.refresh()
967
- return self._inner.program_binary_url
968
-
969
1022
  def program_info(self) -> Mapping[str, Any]:
970
1023
  """
971
1024
  Return the program info of the pipeline.
@@ -242,10 +242,29 @@ class FelderaClient:
242
242
 
243
243
  return resp
244
244
 
245
- def start_pipeline(self, pipeline_name: str, timeout_s: Optional[float] = 300):
245
+ def get_pipeline_logs(self, pipeline_name: str) -> Generator[str, None, None]:
246
+ """
247
+ Get the pipeline logs
248
+
249
+ :param name: The name of the pipeline
250
+ :return: A generator yielding the logs, one line at a time.
251
+ """
252
+ chunk: bytes
253
+ with self.http.get(
254
+ path=f"/pipelines/{pipeline_name}/logs",
255
+ stream=True,
256
+ ) as resp:
257
+ for chunk in resp.iter_lines(chunk_size=50000000):
258
+ if chunk:
259
+ yield chunk.decode("utf-8")
260
+
261
+ def start_pipeline(
262
+ self, pipeline_name: str, wait: bool = True, timeout_s: Optional[float] = 300
263
+ ):
246
264
  """
247
265
 
248
266
  :param pipeline_name: The name of the pipeline to start
267
+ :param wait: Set True to wait for the pipeline to start. True by default
249
268
  :param timeout_s: The amount of time in seconds to wait for the pipeline
250
269
  to start. 300 seconds by default.
251
270
  """
@@ -257,6 +276,9 @@ class FelderaClient:
257
276
  path=f"/pipelines/{pipeline_name}/start",
258
277
  )
259
278
 
279
+ if not wait:
280
+ return
281
+
260
282
  start_time = time.monotonic()
261
283
 
262
284
  while True:
@@ -292,6 +314,7 @@ Reason: The pipeline is in a STOPPED state due to the following error:
292
314
  self,
293
315
  pipeline_name: str,
294
316
  error_message: str = None,
317
+ wait: bool = True,
295
318
  timeout_s: Optional[float] = 300,
296
319
  ):
297
320
  """
@@ -300,6 +323,7 @@ Reason: The pipeline is in a STOPPED state due to the following error:
300
323
  :param pipeline_name: The name of the pipeline to stop
301
324
  :param error_message: The error message to show if the pipeline is in
302
325
  STOPPED state due to a failure.
326
+ :param wait: Set True to wait for the pipeline to pause. True by default
303
327
  :param timeout_s: The amount of time in seconds to wait for the pipeline
304
328
  to pause. 300 seconds by default.
305
329
  """
@@ -311,6 +335,9 @@ Reason: The pipeline is in a STOPPED state due to the following error:
311
335
  path=f"/pipelines/{pipeline_name}/pause",
312
336
  )
313
337
 
338
+ if not wait:
339
+ return
340
+
314
341
  if error_message is None:
315
342
  error_message = "Unable to PAUSE the pipeline.\n"
316
343
 
@@ -346,7 +373,11 @@ Reason: The pipeline is in a STOPPED state due to the following error:
346
373
  time.sleep(0.1)
347
374
 
348
375
  def stop_pipeline(
349
- self, pipeline_name: str, force: bool, timeout_s: Optional[float] = 300
376
+ self,
377
+ pipeline_name: str,
378
+ force: bool,
379
+ wait: bool = True,
380
+ timeout_s: Optional[float] = 300,
350
381
  ):
351
382
  """
352
383
  Stop a pipeline
@@ -354,6 +385,7 @@ Reason: The pipeline is in a STOPPED state due to the following error:
354
385
  :param pipeline_name: The name of the pipeline to stop
355
386
  :param force: Set True to immediately scale compute resources to zero.
356
387
  Set False to automatically checkpoint before stopping.
388
+ :param wait: Set True to wait for the pipeline to stop. True by default
357
389
  :param timeout_s: The amount of time in seconds to wait for the pipeline
358
390
  to stop. Default is 300 seconds.
359
391
  """
@@ -368,6 +400,9 @@ Reason: The pipeline is in a STOPPED state due to the following error:
368
400
  params=params,
369
401
  )
370
402
 
403
+ if not wait:
404
+ return
405
+
371
406
  start = time.monotonic()
372
407
 
373
408
  while time.monotonic() - start < timeout_s:
@@ -420,6 +455,86 @@ Reason: The pipeline is in a STOPPED state due to the following error:
420
455
  f"timeout error: pipeline '{pipeline_name}' did not clear storage in {timeout_s} seconds"
421
456
  )
422
457
 
458
+ def start_transaction(self, pipeline_name: str) -> int:
459
+ """
460
+ Start a new transaction.
461
+
462
+ Transaction ID.
463
+
464
+ :param pipeline_name: The name of the pipeline.
465
+ """
466
+
467
+ resp = self.http.post(
468
+ path=f"/pipelines/{pipeline_name}/start_transaction",
469
+ )
470
+
471
+ return int(resp.get("transaction_id"))
472
+
473
+ def commit_transaction(
474
+ self,
475
+ pipeline_name: str,
476
+ transaction_id: Optional[int] = None,
477
+ wait: bool = True,
478
+ timeout_s: Optional[float] = None,
479
+ ):
480
+ """
481
+ Commits the currently active transaction.
482
+
483
+ :param pipeline_name: The name of the pipeline.
484
+
485
+ :param transaction_id: If provided, the function verifies that the currently active transaction matches this ID.
486
+ If the active transaction ID does not match, the function raises an error.
487
+
488
+ :param wait: If True, the function blocks until the transaction either commits successfully or the timeout is reached.
489
+ If False, the function initiates the commit and returns immediately without waiting for completion. The default value is True.
490
+
491
+ :param timeout_s: Maximum time (in seconds) to wait for the transaction to commit when `wait` is True.
492
+ If None, the function will wait indefinitely.
493
+
494
+ :raises RuntimeError: If there is currently no transaction in progress.
495
+ :raises ValueError: If the provided `transaction_id` does not match the current transaction.
496
+ :raises TimeoutError: If the transaction does not commit within the specified timeout (when `wait` is True).
497
+ :raises FelderaAPIError: If the pipeline fails to start a transaction.
498
+ """
499
+
500
+ # TODO: implement this without using /stats when we have a better pipeline status reporting API.
501
+ stats = self.get_pipeline_stats(pipeline_name)
502
+ current_transaction_id = stats["global_metrics"]["transaction_id"]
503
+
504
+ if current_transaction_id == 0:
505
+ raise RuntimeError(
506
+ "Attempting to commit a transaction, but there is no transaction in progress"
507
+ )
508
+
509
+ if transaction_id and current_transaction_id != transaction_id:
510
+ raise ValueError(
511
+ f"Specified transaction id {transaction_id} doesn't match current active transaction id {current_transaction_id}"
512
+ )
513
+
514
+ transaction_id = current_transaction_id
515
+
516
+ self.http.post(
517
+ path=f"/pipelines/{pipeline_name}/commit_transaction",
518
+ )
519
+
520
+ start_time = time.monotonic()
521
+
522
+ if not wait:
523
+ return
524
+
525
+ while True:
526
+ if timeout_s is not None:
527
+ elapsed = time.monotonic() - start_time
528
+ if elapsed > timeout_s:
529
+ raise TimeoutError("Timed out waiting for transaction to commit")
530
+
531
+ stats = self.get_pipeline_stats(pipeline_name)
532
+ if stats["global_metrics"]["transaction_id"] != transaction_id:
533
+ return
534
+
535
+ logging.debug("commit hasn't completed, waiting for 1 more second")
536
+ time.sleep(1.0)
537
+
423
538
  def checkpoint_pipeline(self, pipeline_name: str) -> int:
424
539
  """
425
540
  Checkpoint a pipeline.
@@ -49,7 +49,6 @@ class Pipeline:
49
49
  self.deployment_desired_status: Optional[str] = None
50
50
  self.deployment_error: Optional[dict] = None
51
51
  self.deployment_location: Optional[str] = None
52
- self.program_binary_url: Optional[str] = None
53
52
  self.program_info: Optional[dict] = (
54
53
  None # info about input & output connectors and the schema
55
54
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: feldera
3
- Version: 0.115.0
3
+ Version: 0.117.0
4
4
  Summary: The feldera python client
5
5
  Author-email: Feldera Team <dev@feldera.com>
6
6
  License: MIT
@@ -27,4 +27,5 @@ feldera/rest/sql_view.py
27
27
  tests/test_pipeline_builder.py
28
28
  tests/test_shared_pipeline0.py
29
29
  tests/test_shared_pipeline1.py
30
+ tests/test_shared_pipeline_stress.py
30
31
  tests/test_udf.py
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
  name = "feldera"
7
7
  readme = "README.md"
8
8
  description = "The feldera python client"
9
- version = "0.115.0"
9
+ version = "0.117.0"
10
10
  license = { text = "MIT" }
11
11
  requires-python = ">=3.10"
12
12
  authors = [
@@ -1,11 +1,11 @@
1
- import random
2
- from uuid import uuid4
3
- import time
4
- import os
5
- from typing import Optional
6
- from feldera.runtime_config import RuntimeConfig, Storage
7
- from tests import enterprise_only
8
1
  from tests.shared_test_pipeline import SharedTestPipeline
2
+ from tests import enterprise_only
3
+ from feldera.runtime_config import RuntimeConfig, Storage
4
+ from typing import Optional
5
+ import os
6
+ import time
7
+ from uuid import uuid4
8
+ import random
9
9
 
10
10
 
11
11
  DEFAULT_ENDPOINT = os.environ.get(
@@ -19,6 +19,7 @@ SECRET_KEY = "miniopasswd"
19
19
  def storage_cfg(
20
20
  endpoint: Optional[str] = None,
21
21
  start_from_checkpoint: Optional[str] = None,
22
+ strict: bool = False,
22
23
  auth_err: bool = False,
23
24
  ) -> dict:
24
25
  return {
@@ -32,6 +33,7 @@ def storage_cfg(
32
33
  "provider": "Minio",
33
34
  "endpoint": endpoint or DEFAULT_ENDPOINT,
34
35
  "start_from_checkpoint": start_from_checkpoint,
36
+ "fail_if_no_checkpoint": strict,
35
37
  }
36
38
  },
37
39
  }
@@ -46,6 +48,8 @@ class TestCheckpointSync(SharedTestPipeline):
46
48
  random_uuid: bool = False,
47
49
  clear_storage: bool = True,
48
50
  auth_err: bool = False,
51
+ strict: bool = False,
52
+ expect_empty: bool = False,
49
53
  ):
50
54
  """
51
55
  CREATE TABLE t0 (c0 INT, c1 VARCHAR);
@@ -75,12 +79,17 @@ class TestCheckpointSync(SharedTestPipeline):
75
79
 
76
80
  # Restart pipeline from checkpoint
77
81
  storage_config = storage_cfg(
78
- start_from_checkpoint=uuid if from_uuid else "latest", auth_err=auth_err
82
+ start_from_checkpoint=uuid if from_uuid else "latest",
83
+ auth_err=auth_err,
84
+ strict=strict,
79
85
  )
80
86
  self.set_runtime_config(RuntimeConfig(storage=Storage(config=storage_config)))
81
87
  self.pipeline.start()
82
88
  got_after = list(self.pipeline.query("SELECT * FROM v0"))
83
89
 
90
+ if expect_empty:
91
+ got_before = []
92
+
84
93
  self.assertCountEqual(got_before, got_after)
85
94
 
86
95
  self.pipeline.stop(force=True)
@@ -89,19 +98,27 @@ class TestCheckpointSync(SharedTestPipeline):
89
98
  self.pipeline.clear_storage()
90
99
 
91
100
  @enterprise_only
92
- def test_checkpoint_sync_from_uuid(self):
101
+ def test_from_uuid(self):
93
102
  self.test_checkpoint_sync(from_uuid=True)
94
103
 
95
104
  @enterprise_only
96
- def test_checkpoint_sync_without_clearing_storage(self):
105
+ def test_without_clearing_storage(self):
97
106
  self.test_checkpoint_sync(clear_storage=False)
98
107
 
99
108
  @enterprise_only
100
- def test_checkpoint_sync_err(self):
109
+ def test_autherr_fail(self):
101
110
  with self.assertRaisesRegex(RuntimeError, "SignatureDoesNotMatch"):
102
- self.test_checkpoint_sync(auth_err=True)
111
+ self.test_checkpoint_sync(auth_err=True, strict=True)
103
112
 
104
113
  @enterprise_only
105
- def test_checkpoint_sync_err_nonexistent_checkpoint(self):
114
+ def test_autherr(self):
115
+ self.test_checkpoint_sync(auth_err=True, strict=False, expect_empty=True)
116
+
117
+ @enterprise_only
118
+ def test_nonexistent_checkpoint_fail(self):
106
119
  with self.assertRaisesRegex(RuntimeError, "were not found in source"):
107
- self.test_checkpoint_sync(random_uuid=True, from_uuid=True)
120
+ self.test_checkpoint_sync(random_uuid=True, from_uuid=True, strict=True)
121
+
122
+ @enterprise_only
123
+ def test_nonexistent_checkpoint(self):
124
+ self.test_checkpoint_sync(random_uuid=True, from_uuid=True, expect_empty=True)
@@ -0,0 +1,26 @@
1
+ import unittest
2
+
3
+ from tests.shared_test_pipeline import SharedTestPipeline
4
+
5
+
6
+ class TestPipeline(SharedTestPipeline):
7
+ def test_create_pipeline(self):
8
+ """
9
+ CREATE TABLE tbl(id INT) WITH ('materialized' = 'true');
10
+ CREATE MATERIALIZED VIEW v0 AS SELECT * FROM tbl;
11
+ """
12
+ pass
13
+
14
+ def test_get_pipeline_logs(self):
15
+ self.pipeline.start()
16
+ # regression test for https://github.com/feldera/feldera/issues/4394
17
+ for _ in range(200):
18
+ logs = self.pipeline.logs()
19
+ start = next(logs)
20
+ assert "Fresh start of pipeline log" in start
21
+ self.pipeline.pause()
22
+ self.pipeline.stop(force=True)
23
+
24
+
25
+ if __name__ == "__main__":
26
+ unittest.main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes