common.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. # Copyright Materialize, Inc. and contributors. All rights reserved.
  2. #
  3. # Use of this software is governed by the Business Source License
  4. # included in the LICENSE file at the root of this repository.
  5. #
  6. # As of the Change Date specified in that file, in accordance with
  7. # the Business Source License, use of this software will be governed
  8. # by the Apache License, Version 2.0.
  9. import logging
  10. import subprocess
  11. from collections.abc import Callable
  12. from textwrap import dedent
  13. from time import sleep
  14. from typing import Any, cast
  15. LOGGER = logging.getLogger(__name__)
  16. def retry(
  17. f: Callable[[], Any],
  18. max_attempts: int,
  19. exception_types: list[type[Exception]],
  20. sleep_secs: int = 1,
  21. message: str | None = None,
  22. ) -> Any:
  23. result: Any = None
  24. for attempt in range(1, max_attempts + 1):
  25. try:
  26. result = f()
  27. break
  28. except tuple(exception_types) as e:
  29. if attempt == max_attempts:
  30. if message:
  31. LOGGER.info(message)
  32. else:
  33. LOGGER.error(f"Exception in attempt {attempt}: ", exc_info=e)
  34. raise
  35. sleep(sleep_secs)
  36. return result
  37. def is_subdict(
  38. larger_dict: dict[str, Any],
  39. smaller_dict: dict[str, Any],
  40. key_path: str = "",
  41. ) -> bool:
  42. def is_sublist(
  43. larger_list: list[Any], smaller_list: list[Any], key_path: str = ""
  44. ) -> bool:
  45. # All members of list must exist in larger_dict's list,
  46. # but if they are dicts, they are allowed to be subdicts,
  47. # rather than exact matches.
  48. if len(larger_list) < len(smaller_list):
  49. LOGGER.warning(f"{key_path}: smaller_list is larger than larger_list")
  50. return False
  51. for i, value in enumerate(smaller_list):
  52. current_key = f"{key_path}.{i}"
  53. if isinstance(value, dict):
  54. if not is_subdict(
  55. larger_list[i],
  56. cast(dict[str, Any], value),
  57. current_key,
  58. ):
  59. return False
  60. elif isinstance(value, list):
  61. if not is_sublist(
  62. larger_list[i],
  63. value,
  64. current_key,
  65. ):
  66. return False
  67. else:
  68. if value != larger_list[i]:
  69. LOGGER.warning(
  70. f"{key_path}.{i}: scalar value does not match: {value} != {larger_list[i]}",
  71. )
  72. return False
  73. return True
  74. for key, value in smaller_dict.items():
  75. current_key = f"{key_path}.{key}"
  76. if key not in larger_dict:
  77. LOGGER.warning(f"{key_path}.{key}: key not found in larger_dict")
  78. return False
  79. if isinstance(value, dict):
  80. if not is_subdict(
  81. larger_dict[key],
  82. cast(dict[str, Any], value),
  83. current_key,
  84. ):
  85. return False
  86. elif isinstance(value, list):
  87. if not is_sublist(
  88. larger_dict[key],
  89. value,
  90. current_key,
  91. ):
  92. return False
  93. else:
  94. if value != larger_dict[key]:
  95. LOGGER.warning(
  96. f"{current_key}: scalar value does not match: {value} != {larger_dict[key]}",
  97. )
  98. return False
  99. return True
  100. def run_process_with_error_information(
  101. cmd: list[str], input: str | None = None, capture_output: bool = False
  102. ) -> None:
  103. try:
  104. subprocess.run(
  105. cmd, text=True, input=input, check=True, capture_output=capture_output
  106. )
  107. except subprocess.CalledProcessError as e:
  108. log_subprocess_error(e)
  109. raise e
  110. def log_subprocess_error(e: subprocess.CalledProcessError) -> None:
  111. LOGGER.error(
  112. dedent(
  113. f"""
  114. cmd: {e.cmd}
  115. returncode: {e.returncode}
  116. stdout: {e.stdout}
  117. stderr: {e.stderr}
  118. """
  119. )
  120. )