starrocks-br 0.4.0__tar.gz → 0.5.1__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.
Files changed (51) hide show
  1. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/PKG-INFO +3 -2
  2. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/pyproject.toml +4 -3
  3. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/src/starrocks_br/cli.py +106 -57
  4. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/src/starrocks_br/concurrency.py +2 -9
  5. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/src/starrocks_br/config.py +11 -7
  6. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/src/starrocks_br/db.py +3 -3
  7. starrocks_br-0.5.1/src/starrocks_br/error_handler.py +307 -0
  8. starrocks_br-0.5.1/src/starrocks_br/exceptions.py +109 -0
  9. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/src/starrocks_br/executor.py +4 -4
  10. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/src/starrocks_br/history.py +4 -6
  11. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/src/starrocks_br/labels.py +2 -2
  12. starrocks_br-0.5.1/src/starrocks_br/logger.py +66 -0
  13. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/src/starrocks_br/planner.py +5 -10
  14. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/src/starrocks_br/restore.py +13 -16
  15. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/src/starrocks_br/timezone.py +1 -2
  16. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/src/starrocks_br.egg-info/PKG-INFO +3 -2
  17. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/src/starrocks_br.egg-info/SOURCES.txt +8 -1
  18. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/src/starrocks_br.egg-info/requires.txt +1 -0
  19. starrocks_br-0.5.1/tests/test_cli_backup.py +205 -0
  20. starrocks_br-0.5.1/tests/test_cli_exceptions.py +1113 -0
  21. starrocks_br-0.5.1/tests/test_cli_general.py +43 -0
  22. starrocks_br-0.5.1/tests/test_cli_init.py +16 -0
  23. starrocks_br-0.5.1/tests/test_cli_restore.py +156 -0
  24. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/tests/test_concurrency.py +22 -12
  25. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/tests/test_config.py +8 -6
  26. starrocks_br-0.5.1/tests/test_error_handler.py +182 -0
  27. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/tests/test_executor.py +3 -3
  28. starrocks_br-0.5.1/tests/test_logger.py +78 -0
  29. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/tests/test_planner.py +5 -3
  30. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/tests/test_restore.py +39 -11
  31. starrocks_br-0.4.0/src/starrocks_br/logger.py +0 -36
  32. starrocks_br-0.4.0/tests/test_cli.py +0 -1227
  33. starrocks_br-0.4.0/tests/test_logger.py +0 -276
  34. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/README.md +0 -0
  35. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/setup.cfg +0 -0
  36. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/src/starrocks_br/__init__.py +0 -0
  37. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/src/starrocks_br/health.py +0 -0
  38. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/src/starrocks_br/repository.py +0 -0
  39. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/src/starrocks_br/schema.py +0 -0
  40. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/src/starrocks_br/utils.py +0 -0
  41. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/src/starrocks_br.egg-info/dependency_links.txt +0 -0
  42. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/src/starrocks_br.egg-info/entry_points.txt +0 -0
  43. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/src/starrocks_br.egg-info/top_level.txt +0 -0
  44. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/tests/test_db.py +0 -0
  45. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/tests/test_health_checks.py +0 -0
  46. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/tests/test_history.py +0 -0
  47. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/tests/test_labels.py +0 -0
  48. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/tests/test_repository_sql.py +0 -0
  49. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/tests/test_schema_setup.py +0 -0
  50. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/tests/test_timezone.py +0 -0
  51. {starrocks_br-0.4.0 → starrocks_br-0.5.1}/tests/test_utils.py +0 -0
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: starrocks-br
3
- Version: 0.4.0
3
+ Version: 0.5.1
4
4
  Summary: StarRocks Backup and Restore automation tool
5
- Requires-Python: >=3.9
5
+ Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
7
7
  Requires-Dist: click<9,>=8.1.7
8
8
  Requires-Dist: PyYAML<7,>=6.0.1
@@ -12,6 +12,7 @@ Requires-Dist: pytest<9,>=8.3.2; extra == "dev"
12
12
  Requires-Dist: pytest-mock<4,>=3.14.0; extra == "dev"
13
13
  Requires-Dist: pytest-cov<6,>=5.0.0; extra == "dev"
14
14
  Requires-Dist: ruff<1,>=0.8.0; extra == "dev"
15
+ Requires-Dist: pre-commit<5,>=4.0.0; extra == "dev"
15
16
 
16
17
  # StarRocks Backup & Restore
17
18
 
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "starrocks-br"
7
- version = "0.4.0"
7
+ version = "0.5.1"
8
8
  description = "StarRocks Backup and Restore automation tool"
9
9
  readme = "README.md"
10
- requires-python = ">=3.9"
10
+ requires-python = ">=3.10"
11
11
  dependencies = [
12
12
  "click>=8.1.7,<9",
13
13
  "PyYAML>=6.0.1,<7",
@@ -20,6 +20,7 @@ dev = [
20
20
  "pytest-mock>=3.14.0,<4",
21
21
  "pytest-cov>=5.0.0,<6",
22
22
  "ruff>=0.8.0,<1",
23
+ "pre-commit>=4.0.0,<5",
23
24
  ]
24
25
 
25
26
  [project.scripts]
@@ -47,7 +48,7 @@ line-length = 100
47
48
 
48
49
  [tool.ruff]
49
50
  line-length = 100
50
- target-version = "py39"
51
+ target-version = "py310"
51
52
 
52
53
  [tool.ruff.lint]
53
54
  select = [
@@ -6,6 +6,8 @@ import click
6
6
  from . import (
7
7
  concurrency,
8
8
  db,
9
+ error_handler,
10
+ exceptions,
9
11
  executor,
10
12
  health,
11
13
  labels,
@@ -63,9 +65,20 @@ def _handle_snapshot_exists_error(
63
65
 
64
66
 
65
67
  @click.group()
66
- def cli():
68
+ @click.option("--verbose", is_flag=True, help="Enable verbose debug logging")
69
+ @click.pass_context
70
+ def cli(ctx, verbose):
67
71
  """StarRocks Backup & Restore automation tool."""
68
- pass
72
+ ctx.ensure_object(dict)
73
+ ctx.obj["verbose"] = verbose
74
+
75
+ if verbose:
76
+ import logging
77
+
78
+ logger.setup_logging(level=logging.DEBUG)
79
+ logger.debug("Verbose logging enabled")
80
+ else:
81
+ logger.setup_logging()
69
82
 
70
83
 
71
84
  @cli.command("init")
@@ -112,11 +125,14 @@ def init(config):
112
125
  " starrocks-br backup incremental --group my_daily_incremental --config config.yaml"
113
126
  )
114
127
 
115
- except FileNotFoundError as e:
116
- logger.error(f"Config file not found: {e}")
128
+ except exceptions.ConfigFileNotFoundError as e:
129
+ error_handler.handle_config_file_not_found_error(e)
117
130
  sys.exit(1)
118
- except ValueError as e:
119
- logger.error(f"Configuration error: {e}")
131
+ except exceptions.ConfigValidationError as e:
132
+ error_handler.handle_config_validation_error(e, config)
133
+ sys.exit(1)
134
+ except FileNotFoundError as e:
135
+ error_handler.handle_config_file_not_found_error(exceptions.ConfigFileNotFoundError(str(e)))
120
136
  sys.exit(1)
121
137
  except Exception as e:
122
138
  logger.error(f"Failed to initialize schema: {e}")
@@ -262,14 +278,26 @@ def backup_incremental(config, baseline_backup, group, name):
262
278
  logger.error(f"{result['error_message']}")
263
279
  sys.exit(1)
264
280
 
281
+ except exceptions.ConcurrencyConflictError as e:
282
+ error_handler.handle_concurrency_conflict_error(e, config)
283
+ sys.exit(1)
284
+ except exceptions.BackupLabelNotFoundError as e:
285
+ error_handler.handle_backup_label_not_found_error(e, config)
286
+ sys.exit(1)
287
+ except exceptions.NoFullBackupFoundError as e:
288
+ error_handler.handle_no_full_backup_found_error(e, config, group)
289
+ sys.exit(1)
290
+ except exceptions.ConfigFileNotFoundError as e:
291
+ error_handler.handle_config_file_not_found_error(e)
292
+ sys.exit(1)
293
+ except exceptions.ConfigValidationError as e:
294
+ error_handler.handle_config_validation_error(e, config)
295
+ sys.exit(1)
265
296
  except FileNotFoundError as e:
266
- logger.error(f"Config file not found: {e}")
297
+ error_handler.handle_config_file_not_found_error(exceptions.ConfigFileNotFoundError(str(e)))
267
298
  sys.exit(1)
268
299
  except ValueError as e:
269
- logger.error(f"Configuration error: {e}")
270
- sys.exit(1)
271
- except RuntimeError as e:
272
- logger.error(f"{e}")
300
+ logger.error(f"Error: {e}")
273
301
  sys.exit(1)
274
302
  except Exception as e:
275
303
  logger.error(f"Unexpected error: {e}")
@@ -381,15 +409,23 @@ def backup_full(config, group, name):
381
409
  logger.error(f"{result['error_message']}")
382
410
  sys.exit(1)
383
411
 
384
- except (FileNotFoundError, ValueError, RuntimeError, Exception) as e:
385
- if isinstance(e, FileNotFoundError):
386
- logger.error(f"Config file not found: {e}")
387
- elif isinstance(e, ValueError):
388
- logger.error(f"Configuration error: {e}")
389
- elif isinstance(e, RuntimeError):
390
- logger.error(f"{e}")
391
- else:
392
- logger.error(f"Unexpected error: {e}")
412
+ except exceptions.ConcurrencyConflictError as e:
413
+ error_handler.handle_concurrency_conflict_error(e, config)
414
+ sys.exit(1)
415
+ except exceptions.ConfigFileNotFoundError as e:
416
+ error_handler.handle_config_file_not_found_error(e)
417
+ sys.exit(1)
418
+ except exceptions.ConfigValidationError as e:
419
+ error_handler.handle_config_validation_error(e, config)
420
+ sys.exit(1)
421
+ except FileNotFoundError as e:
422
+ error_handler.handle_config_file_not_found_error(exceptions.ConfigFileNotFoundError(str(e)))
423
+ sys.exit(1)
424
+ except ValueError as e:
425
+ logger.error(f"Error: {e}")
426
+ sys.exit(1)
427
+ except Exception as e:
428
+ logger.error(f"Unexpected error: {e}")
393
429
  sys.exit(1)
394
430
 
395
431
 
@@ -429,14 +465,13 @@ def restore_command(config, target_label, group, table, rename_suffix, yes):
429
465
  if table:
430
466
  table = table.strip()
431
467
  if not table:
432
- logger.error("Table name cannot be empty")
433
- sys.exit(1)
468
+ raise exceptions.InvalidTableNameError("", "Table name cannot be empty")
434
469
 
435
470
  if "." in table:
436
- logger.error(
437
- "Table name must not include database prefix. Use 'table_name' not 'database.table_name'. Database comes from config file."
471
+ raise exceptions.InvalidTableNameError(
472
+ table,
473
+ "Table name must not include database prefix. Use 'table_name' not 'database.table_name'",
438
474
  )
439
- sys.exit(1)
440
475
 
441
476
  cfg = config_module.load_config(config)
442
477
  config_module.validate_config(cfg)
@@ -472,39 +507,21 @@ def restore_command(config, target_label, group, table, rename_suffix, yes):
472
507
 
473
508
  logger.info(f"Finding restore sequence for target backup: {target_label}")
474
509
 
475
- try:
476
- restore_pair = restore.find_restore_pair(database, target_label)
477
- logger.success(f"Found restore sequence: {' -> '.join(restore_pair)}")
478
- except ValueError as e:
479
- logger.error(f"Failed to find restore sequence: {e}")
480
- sys.exit(1)
510
+ restore_pair = restore.find_restore_pair(database, target_label)
511
+ logger.success(f"Found restore sequence: {' -> '.join(restore_pair)}")
481
512
 
482
513
  logger.info("Determining tables to restore from backup manifest...")
483
514
 
484
- try:
485
- tables_to_restore = restore.get_tables_from_backup(
486
- database,
487
- target_label,
488
- group=group,
489
- table=table,
490
- database=cfg["database"] if table else None,
491
- )
492
- except ValueError as e:
493
- logger.error(str(e))
494
- sys.exit(1)
515
+ tables_to_restore = restore.get_tables_from_backup(
516
+ database,
517
+ target_label,
518
+ group=group,
519
+ table=table,
520
+ database=cfg["database"] if table else None,
521
+ )
495
522
 
496
523
  if not tables_to_restore:
497
- if group:
498
- logger.warning(
499
- f"No tables found in backup '{target_label}' for group '{group}'"
500
- )
501
- elif table:
502
- logger.warning(
503
- f"No tables found in backup '{target_label}' for table '{table}'"
504
- )
505
- else:
506
- logger.warning(f"No tables found in backup '{target_label}'")
507
- sys.exit(1)
524
+ raise exceptions.NoTablesFoundError(group=group, label=target_label)
508
525
 
509
526
  logger.success(
510
527
  f"Found {len(tables_to_restore)} table(s) to restore: {', '.join(tables_to_restore)}"
@@ -527,14 +544,46 @@ def restore_command(config, target_label, group, table, rename_suffix, yes):
527
544
  logger.error(f"Restore failed: {result['error_message']}")
528
545
  sys.exit(1)
529
546
 
547
+ except exceptions.InvalidTableNameError as e:
548
+ error_handler.handle_invalid_table_name_error(e)
549
+ sys.exit(1)
550
+ except exceptions.BackupLabelNotFoundError as e:
551
+ error_handler.handle_backup_label_not_found_error(e, config)
552
+ sys.exit(1)
553
+ except exceptions.NoSuccessfulFullBackupFoundError as e:
554
+ error_handler.handle_no_successful_full_backup_found_error(e, config)
555
+ sys.exit(1)
556
+ except exceptions.TableNotFoundInBackupError as e:
557
+ error_handler.handle_table_not_found_in_backup_error(e, config)
558
+ sys.exit(1)
559
+ except exceptions.NoTablesFoundError as e:
560
+ error_handler.handle_no_tables_found_error(e, config, target_label)
561
+ sys.exit(1)
562
+ except exceptions.SnapshotNotFoundError as e:
563
+ error_handler.handle_snapshot_not_found_error(e, config)
564
+ sys.exit(1)
565
+ except exceptions.RestoreOperationCancelledError:
566
+ error_handler.handle_restore_operation_cancelled_error()
567
+ sys.exit(1)
568
+ except exceptions.ConfigFileNotFoundError as e:
569
+ error_handler.handle_config_file_not_found_error(e)
570
+ sys.exit(1)
571
+ except exceptions.ConfigValidationError as e:
572
+ error_handler.handle_config_validation_error(e, config)
573
+ sys.exit(1)
574
+ except exceptions.ClusterHealthCheckFailedError as e:
575
+ error_handler.handle_cluster_health_check_failed_error(e, config)
576
+ sys.exit(1)
530
577
  except FileNotFoundError as e:
531
- logger.error(f"Config file not found: {e}")
578
+ error_handler.handle_config_file_not_found_error(exceptions.ConfigFileNotFoundError(str(e)))
532
579
  sys.exit(1)
533
580
  except ValueError as e:
534
- logger.error(f"Configuration error: {e}")
581
+ error_handler.handle_config_validation_error(
582
+ exceptions.ConfigValidationError(str(e)), config
583
+ )
535
584
  sys.exit(1)
536
- except RuntimeError as e:
537
- logger.error(f"{e}")
585
+ except exceptions.ConcurrencyConflictError as e:
586
+ error_handler.handle_concurrency_conflict_error(e, config)
538
587
  sys.exit(1)
539
588
  except Exception as e:
540
589
  logger.error(f"Unexpected error: {e}")
@@ -1,6 +1,6 @@
1
1
  from typing import Literal
2
2
 
3
- from . import logger, utils
3
+ from . import exceptions, logger, utils
4
4
 
5
5
 
6
6
  def reserve_job_slot(db, scope: str, label: str) -> None:
@@ -46,14 +46,7 @@ def _can_heal_stale_job(scope: str, label: str, db) -> bool:
46
46
 
47
47
  def _raise_concurrency_conflict(scope: str, active_jobs: list[tuple[str, str, str]]) -> None:
48
48
  """Raise a concurrency conflict error with helpful message."""
49
- active_job_strings = [f"{job[0]}:{job[1]}" for job in active_jobs]
50
- active_labels = [job[1] for job in active_jobs]
51
-
52
- raise RuntimeError(
53
- f"Concurrency conflict: Another '{scope}' job is already ACTIVE: {', '.join(active_job_strings)}. "
54
- f"Wait for it to complete or cancel it via: UPDATE ops.run_status SET state='CANCELLED' "
55
- f"WHERE label='{active_labels[0]}' AND state='ACTIVE'"
56
- )
49
+ raise exceptions.ConcurrencyConflictError(scope, active_jobs)
57
50
 
58
51
 
59
52
  def _insert_new_job(db, scope: str, label: str) -> None:
@@ -2,6 +2,8 @@ from typing import Any
2
2
 
3
3
  import yaml
4
4
 
5
+ from . import exceptions
6
+
5
7
 
6
8
  def load_config(config_path: str) -> dict[str, Any]:
7
9
  """Load and parse YAML configuration file.
@@ -20,7 +22,7 @@ def load_config(config_path: str) -> dict[str, Any]:
20
22
  config = yaml.safe_load(f)
21
23
 
22
24
  if not isinstance(config, dict):
23
- raise ValueError("Config must be a dictionary")
25
+ raise exceptions.ConfigValidationError("Config must be a dictionary")
24
26
 
25
27
  return config
26
28
 
@@ -32,13 +34,13 @@ def validate_config(config: dict[str, Any]) -> None:
32
34
  config: Configuration dictionary
33
35
 
34
36
  Raises:
35
- ValueError: If required fields are missing
37
+ ConfigValidationError: If required fields are missing
36
38
  """
37
39
  required_fields = ["host", "port", "user", "database", "repository"]
38
40
 
39
41
  for field in required_fields:
40
42
  if field not in config:
41
- raise ValueError(f"Missing required config field: {field}")
43
+ raise exceptions.ConfigValidationError(f"Missing required config field: {field}")
42
44
 
43
45
  _validate_tls_section(config.get("tls"))
44
46
 
@@ -48,17 +50,19 @@ def _validate_tls_section(tls_config) -> None:
48
50
  return
49
51
 
50
52
  if not isinstance(tls_config, dict):
51
- raise ValueError("TLS configuration must be a dictionary")
53
+ raise exceptions.ConfigValidationError("TLS configuration must be a dictionary")
52
54
 
53
55
  enabled = bool(tls_config.get("enabled", False))
54
56
 
55
57
  if enabled and not tls_config.get("ca_cert"):
56
- raise ValueError("TLS configuration requires 'ca_cert' when 'enabled' is true")
58
+ raise exceptions.ConfigValidationError(
59
+ "TLS configuration requires 'ca_cert' when 'enabled' is true"
60
+ )
57
61
 
58
62
  if "verify_server_cert" in tls_config and not isinstance(
59
63
  tls_config["verify_server_cert"], bool
60
64
  ):
61
- raise ValueError(
65
+ raise exceptions.ConfigValidationError(
62
66
  "TLS configuration field 'verify_server_cert' must be a boolean if provided"
63
67
  )
64
68
 
@@ -67,6 +71,6 @@ def _validate_tls_section(tls_config) -> None:
67
71
  if not isinstance(tls_versions, list) or not all(
68
72
  isinstance(version, str) for version in tls_versions
69
73
  ):
70
- raise ValueError(
74
+ raise exceptions.ConfigValidationError(
71
75
  "TLS configuration field 'tls_versions' must be a list of strings if provided"
72
76
  )
@@ -1,4 +1,4 @@
1
- from typing import Any, Optional
1
+ from typing import Any
2
2
 
3
3
  import mysql.connector
4
4
 
@@ -13,7 +13,7 @@ class StarRocksDB:
13
13
  user: str,
14
14
  password: str,
15
15
  database: str,
16
- tls_config: Optional[dict[str, Any]] = None,
16
+ tls_config: dict[str, Any] | None = None,
17
17
  ):
18
18
  """Initialize database connection.
19
19
 
@@ -31,7 +31,7 @@ class StarRocksDB:
31
31
  self.database = database
32
32
  self._connection = None
33
33
  self.tls_config = tls_config or {}
34
- self._timezone: Optional[str] = None
34
+ self._timezone: str | None = None
35
35
 
36
36
  def connect(self) -> None:
37
37
  """Establish database connection."""