dycw-utilities 0.148.4__tar.gz → 0.149.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.
Files changed (214) hide show
  1. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/PKG-INFO +1 -1
  2. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/pyproject.toml +2 -2
  3. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_postgres.py +42 -51
  4. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/__init__.py +1 -1
  5. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/postgres.py +217 -110
  6. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/.gitignore +0 -0
  7. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/LICENSE +0 -0
  8. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/README.md +0 -0
  9. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/__init__.py +0 -0
  10. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/conftest.py +0 -0
  11. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/modules/__init__.py +0 -0
  12. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/modules/package_missing/__init__.py +0 -0
  13. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/modules/package_missing/module.py +0 -0
  14. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/modules/package_with/__init__.py +0 -0
  15. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/modules/package_with/outer_1.py +0 -0
  16. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/modules/package_with/outer_2.py +0 -0
  17. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/modules/package_with/subpackage/__init__.py +0 -0
  18. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/modules/package_with/subpackage/inner_1.py +0 -0
  19. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/modules/package_with/subpackage/inner_2.py +0 -0
  20. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/modules/package_with/subpackage/inner_3.py +0 -0
  21. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/modules/package_without/__init__.py +0 -0
  22. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/modules/package_without/module_1.py +0 -0
  23. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/modules/package_without/module_2.py +0 -0
  24. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/modules/standalone.py +0 -0
  25. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/modules/with_imports.py +0 -0
  26. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/regressions/test_pytest_regressions/TestMultipleRegressionFixtures__test_main__obj.json +0 -0
  27. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/regressions/test_pytest_regressions/TestMultipleRegressionFixtures__test_main__series.json +0 -0
  28. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_int.json +0 -0
  29. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_literal__false.json +0 -0
  30. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_literal__true.json +0 -0
  31. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/regressions/test_pytest_regressions/TestOrjsonRegressionFixture__test_dataclass_nested.json +0 -0
  32. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/regressions/test_pytest_regressions/TestPolarsRegressionFixture__test_dataframe.json +0 -0
  33. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/regressions/test_pytest_regressions/TestPolarsRegressionFixture__test_series.json +0 -0
  34. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_altair.py +0 -0
  35. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_asyncio.py +0 -0
  36. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_atomicwrites.py +0 -0
  37. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_atools.py +0 -0
  38. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_cachetools.py +0 -0
  39. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_click.py +0 -0
  40. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_concurrent.py +0 -0
  41. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_contextlib.py +0 -0
  42. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_contextvars.py +0 -0
  43. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_cryptography.py +0 -0
  44. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_cvxpy.py +0 -0
  45. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_dataclasses.py +0 -0
  46. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_enum.py +0 -0
  47. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_errors.py +0 -0
  48. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_eventkit.py +0 -0
  49. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_fastapi.py +0 -0
  50. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_fpdf2.py +0 -0
  51. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_functions.py +0 -0
  52. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_functools.py +0 -0
  53. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_getpass.py +0 -0
  54. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_gzip.py +0 -0
  55. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_hashlib.py +0 -0
  56. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_http.py +0 -0
  57. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_hypothesis.py +0 -0
  58. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_importlib.py +0 -0
  59. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_inflect.py +0 -0
  60. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_ipython.py +0 -0
  61. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_iterables.py +0 -0
  62. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_json.py +0 -0
  63. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_jupyter.py +0 -0
  64. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_libcst.py +0 -0
  65. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_lightweight_charts.py +0 -0
  66. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_logging.py +0 -0
  67. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_math.py +0 -0
  68. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_memory_profiler.py +0 -0
  69. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_modules.py +0 -0
  70. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_more_itertools.py +0 -0
  71. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_numpy.py +0 -0
  72. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_objects/__init__.py +0 -0
  73. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_objects/objects.py +0 -0
  74. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_operator.py +0 -0
  75. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_optuna.py +0 -0
  76. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_orjson.py +0 -0
  77. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_os.py +0 -0
  78. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_parse.py +0 -0
  79. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_pathlib.py +0 -0
  80. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_period.py +0 -0
  81. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_pickle.py +0 -0
  82. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_platform.py +0 -0
  83. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_polars.py +0 -0
  84. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_polars_ols.py +0 -0
  85. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_pottery.py +0 -0
  86. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_pqdm.py +0 -0
  87. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_psutil.py +0 -0
  88. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_pyinstrument.py +0 -0
  89. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_pytest.py +0 -0
  90. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_pytest_randomly.py +0 -0
  91. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_pytest_regressions.py +0 -0
  92. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_random.py +0 -0
  93. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_re.py +0 -0
  94. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_redis.py +0 -0
  95. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_reprlib.py +0 -0
  96. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_scipy.py +0 -0
  97. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_sentinel.py +0 -0
  98. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_shelve.py +0 -0
  99. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_slack_sdk.py +0 -0
  100. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_socket.py +0 -0
  101. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_sqlalchemy.py +0 -0
  102. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_sqlalchemy_polars.py +0 -0
  103. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_statsmodels.py +0 -0
  104. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_string.py +0 -0
  105. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_tempfile.py +0 -0
  106. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_text.py +0 -0
  107. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_threading.py +0 -0
  108. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_timer.py +0 -0
  109. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_traceback.py +0 -0
  110. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_typed_settings.py +0 -0
  111. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_types.py +0 -0
  112. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_typing.py +0 -0
  113. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_typing_funcs/__init__.py +0 -0
  114. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_typing_funcs/no_future.py +0 -0
  115. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_typing_funcs/with_future.py +0 -0
  116. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_tzdata.py +0 -0
  117. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_tzlocal.py +0 -0
  118. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_uuid.py +0 -0
  119. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_version.py +0 -0
  120. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_warnings.py +0 -0
  121. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_whenever.py +0 -0
  122. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_yield_access/__init__.py +0 -0
  123. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_yield_access/script.py +0 -0
  124. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_yield_access/script.sh +0 -0
  125. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_zipfile.py +0 -0
  126. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/tests/test_zoneinfo.py +0 -0
  127. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/altair.py +0 -0
  128. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/asyncio.py +0 -0
  129. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/atomicwrites.py +0 -0
  130. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/atools.py +0 -0
  131. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/cachetools.py +0 -0
  132. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/click.py +0 -0
  133. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/concurrent.py +0 -0
  134. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/contextlib.py +0 -0
  135. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/contextvars.py +0 -0
  136. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/cryptography.py +0 -0
  137. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/cvxpy.py +0 -0
  138. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/dataclasses.py +0 -0
  139. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/enum.py +0 -0
  140. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/errors.py +0 -0
  141. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/eventkit.py +0 -0
  142. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/fastapi.py +0 -0
  143. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/fpdf2.py +0 -0
  144. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/functions.py +0 -0
  145. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/functools.py +0 -0
  146. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/getpass.py +0 -0
  147. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/gzip.py +0 -0
  148. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/hashlib.py +0 -0
  149. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/http.py +0 -0
  150. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/hypothesis.py +0 -0
  151. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/importlib.py +0 -0
  152. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/inflect.py +0 -0
  153. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/ipython.py +0 -0
  154. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/iterables.py +0 -0
  155. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/json.py +0 -0
  156. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/jupyter.py +0 -0
  157. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/libcst.py +0 -0
  158. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/lightweight_charts.py +0 -0
  159. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/logging.py +0 -0
  160. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/math.py +0 -0
  161. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/memory_profiler.py +0 -0
  162. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/modules.py +0 -0
  163. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/more_itertools.py +0 -0
  164. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/numpy.py +0 -0
  165. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/operator.py +0 -0
  166. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/optuna.py +0 -0
  167. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/orjson.py +0 -0
  168. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/os.py +0 -0
  169. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/parse.py +0 -0
  170. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/pathlib.py +0 -0
  171. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/period.py +0 -0
  172. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/pickle.py +0 -0
  173. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/platform.py +0 -0
  174. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/polars.py +0 -0
  175. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/polars_ols.py +0 -0
  176. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/pottery.py +0 -0
  177. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/pqdm.py +0 -0
  178. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/psutil.py +0 -0
  179. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/py.typed +0 -0
  180. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/pyinstrument.py +0 -0
  181. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/pytest.py +0 -0
  182. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/pytest_plugins/__init__.py +0 -0
  183. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/pytest_plugins/pytest_randomly.py +0 -0
  184. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/pytest_plugins/pytest_regressions.py +0 -0
  185. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/pytest_regressions.py +0 -0
  186. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/random.py +0 -0
  187. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/re.py +0 -0
  188. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/redis.py +0 -0
  189. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/reprlib.py +0 -0
  190. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/scipy.py +0 -0
  191. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/sentinel.py +0 -0
  192. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/shelve.py +0 -0
  193. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/slack_sdk.py +0 -0
  194. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/socket.py +0 -0
  195. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/sqlalchemy.py +0 -0
  196. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/sqlalchemy_polars.py +0 -0
  197. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/statsmodels.py +0 -0
  198. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/string.py +0 -0
  199. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/tempfile.py +0 -0
  200. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/text.py +0 -0
  201. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/threading.py +0 -0
  202. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/timer.py +0 -0
  203. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/traceback.py +0 -0
  204. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/typed_settings.py +0 -0
  205. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/types.py +0 -0
  206. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/typing.py +0 -0
  207. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/tzdata.py +0 -0
  208. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/tzlocal.py +0 -0
  209. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/uuid.py +0 -0
  210. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/version.py +0 -0
  211. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/warnings.py +0 -0
  212. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/whenever.py +0 -0
  213. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/zipfile.py +0 -0
  214. {dycw_utilities-0.148.4 → dycw_utilities-0.149.0}/src/utilities/zoneinfo.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dycw-utilities
3
- Version: 0.148.4
3
+ Version: 0.149.0
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -102,7 +102,7 @@ dependencies = [
102
102
  name = "dycw-utilities"
103
103
  readme = "README.md"
104
104
  requires-python = ">= 3.12"
105
- version = "0.148.4"
105
+ version = "0.149.0"
106
106
 
107
107
  [project.entry-points.pytest11]
108
108
  pytest-randomly = "utilities.pytest_plugins.pytest_randomly"
@@ -135,7 +135,7 @@ test = [
135
135
  # bump-my-version
136
136
  [tool.bumpversion]
137
137
  allow_dirty = true
138
- current_version = "0.148.4"
138
+ current_version = "0.149.0"
139
139
 
140
140
  [[tool.bumpversion.files]]
141
141
  filename = "src/utilities/__init__.py"
@@ -9,15 +9,15 @@ from sqlalchemy import URL, Column, Integer, MetaData, Table
9
9
 
10
10
  from utilities.hypothesis import integers, temp_paths, text_ascii
11
11
  from utilities.postgres import (
12
- _PGDumpDatabaseError,
12
+ _build_pg_dump,
13
+ _build_pg_restore_or_psql,
14
+ _extract_url,
15
+ _ExtractURLDatabaseError,
16
+ _ExtractURLHostError,
17
+ _ExtractURLPortError,
13
18
  _PGDumpFormat,
14
- _PGDumpHostError,
15
- _PGDumpPortError,
16
- _PGRestoreDatabaseError,
17
- _PGRestoreHostError,
18
- _PGRestorePortError,
19
19
  pg_dump,
20
- pg_restore,
20
+ restore,
21
21
  )
22
22
  from utilities.typing import get_literal_elements
23
23
 
@@ -53,10 +53,13 @@ def urls(draw: DrawFn, /) -> URL:
53
53
 
54
54
 
55
55
  class TestPGDump:
56
+ @given(url=urls(), path=temp_paths(), logger=text_ascii(min_size=1) | none())
57
+ async def test_main(self, *, url: URL, path: Path, logger: str | None) -> None:
58
+ _ = await pg_dump(url, path, dry_run=True, logger=logger)
59
+
56
60
  @given(
57
61
  url=urls(),
58
62
  path=temp_paths(),
59
- docker=text_ascii(min_size=1) | none(),
60
63
  format_=sampled_from(get_literal_elements(_PGDumpFormat)),
61
64
  jobs=integers(min_value=0) | none(),
62
65
  schemas=lists(text_ascii(min_size=1)) | none(),
@@ -64,14 +67,14 @@ class TestPGDump:
64
67
  tables=tables() | none(),
65
68
  tables_exc=tables() | none(),
66
69
  inserts=booleans(),
67
- logger=text_ascii(min_size=1) | none(),
70
+ on_conflict_do_nothing=booleans(),
71
+ docker=text_ascii(min_size=1) | none(),
68
72
  )
69
- async def test_main(
73
+ def test_build(
70
74
  self,
71
75
  *,
72
76
  url: URL,
73
77
  path: Path,
74
- docker: str | None,
75
78
  format_: _PGDumpFormat,
76
79
  jobs: int | None,
77
80
  schemas: list[str] | None,
@@ -79,12 +82,12 @@ class TestPGDump:
79
82
  tables: list[Table | str] | None,
80
83
  tables_exc: list[Table | str] | None,
81
84
  inserts: bool,
82
- logger: str | None,
85
+ on_conflict_do_nothing: bool,
86
+ docker: str | None,
83
87
  ) -> None:
84
- _ = await pg_dump(
88
+ _ = _build_pg_dump(
85
89
  url,
86
90
  path,
87
- docker=docker,
88
91
  format_=format_,
89
92
  jobs=jobs,
90
93
  schemas=schemas,
@@ -92,87 +95,75 @@ class TestPGDump:
92
95
  tables=tables,
93
96
  tables_exc=tables_exc,
94
97
  inserts=inserts,
95
- logger=logger,
96
- dry_run=True,
98
+ on_conflict_do_nothing=on_conflict_do_nothing,
99
+ docker=docker,
97
100
  )
98
101
 
99
- async def test_error_database(self, *, tmp_path: Path) -> None:
100
- url = URL.create("postgres")
101
- with raises(
102
- _PGDumpDatabaseError, match="Expected URL to contain a 'database'; got .*"
103
- ):
104
- _ = await pg_dump(url, tmp_path, dry_run=True)
105
102
 
106
- async def test_error_host(self, *, tmp_path: Path) -> None:
107
- url = URL.create("postgres", database="database")
108
- with raises(_PGDumpHostError, match="Expected URL to contain a 'host'; got .*"):
109
- _ = await pg_dump(url, tmp_path, dry_run=True)
103
+ class TestRestore:
104
+ @given(url=urls(), path=temp_paths(), logger=text_ascii(min_size=1) | none())
105
+ async def test_main(self, *, url: URL, path: Path, logger: str | None) -> None:
106
+ _ = await restore(url, path, dry_run=True, logger=logger)
110
107
 
111
- async def test_error_port(self, *, tmp_path: Path) -> None:
112
- url = URL.create("postgres", database="database", host="host")
113
- with raises(_PGDumpPortError, match="Expected URL to contain a 'port'; got .*"):
114
- _ = await pg_dump(url, tmp_path, dry_run=True)
115
-
116
-
117
- class TestPGRestore:
118
108
  @given(
119
109
  url=urls(),
120
110
  path=temp_paths(),
111
+ psql=booleans(),
121
112
  database=text_ascii(min_size=1) | none(),
122
- docker=text_ascii(min_size=1) | none(),
123
113
  data_only=booleans(),
124
114
  jobs=integers(min_value=0) | none(),
125
115
  schemas=lists(text_ascii(min_size=1)) | none(),
126
116
  schemas_exc=lists(text_ascii(min_size=1)) | none(),
127
117
  tables=tables() | none(),
128
- logger=text_ascii(min_size=1) | none(),
118
+ docker=text_ascii(min_size=1) | none(),
129
119
  )
130
- async def test_main(
120
+ def test_build(
131
121
  self,
132
122
  *,
133
123
  url: URL,
134
124
  path: Path,
125
+ psql: bool,
135
126
  database: str | None,
136
- docker: str | None,
137
127
  data_only: bool,
138
128
  jobs: int | None,
139
129
  schemas: list[str] | None,
140
130
  schemas_exc: list[str] | None,
141
131
  tables: list[Table | str] | None,
142
- logger: str | None,
132
+ docker: str | None,
143
133
  ) -> None:
144
- _ = await pg_restore(
134
+ _ = _build_pg_restore_or_psql(
145
135
  url,
146
136
  path,
137
+ psql=psql,
147
138
  database=database,
148
- docker=docker,
149
139
  data_only=data_only,
150
140
  jobs=jobs,
151
141
  schemas=schemas,
152
142
  schemas_exc=schemas_exc,
153
143
  tables=tables,
154
- logger=logger,
155
- dry_run=True,
144
+ docker=docker,
156
145
  )
157
146
 
158
- async def test_error_database(self, *, tmp_path: Path) -> None:
147
+
148
+ class TestExtractURL:
149
+ def test_database(self) -> None:
159
150
  url = URL.create("postgres")
160
151
  with raises(
161
- _PGRestoreDatabaseError,
152
+ _ExtractURLDatabaseError,
162
153
  match="Expected URL to contain a 'database'; got .*",
163
154
  ):
164
- _ = await pg_restore(url, tmp_path, dry_run=True)
155
+ _ = _extract_url(url)
165
156
 
166
- async def test_error_host(self, *, tmp_path: Path) -> None:
157
+ def test_host(self) -> None:
167
158
  url = URL.create("postgres", database="database")
168
159
  with raises(
169
- _PGRestoreHostError, match="Expected URL to contain a 'host'; got .*"
160
+ _ExtractURLHostError, match="Expected URL to contain a 'host'; got .*"
170
161
  ):
171
- _ = await pg_restore(url, tmp_path, dry_run=True)
162
+ _ = _extract_url(url)
172
163
 
173
- async def test_error_port(self, *, tmp_path: Path) -> None:
164
+ def test_port(self) -> None:
174
165
  url = URL.create("postgres", database="database", host="host")
175
166
  with raises(
176
- _PGRestorePortError, match="Expected URL to contain a 'port'; got .*"
167
+ _ExtractURLPortError, match="Expected URL to contain a 'port'; got .*"
177
168
  ):
178
- _ = await pg_restore(url, tmp_path, dry_run=True)
169
+ _ = _extract_url(url)
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.148.4"
3
+ __version__ = "0.149.0"
@@ -12,6 +12,7 @@ from utilities.asyncio import stream_command
12
12
  from utilities.iterables import always_iterable
13
13
  from utilities.logging import get_logger
14
14
  from utilities.os import temp_environ
15
+ from utilities.pathlib import ensure_suffix
15
16
  from utilities.sqlalchemy import get_table_name
16
17
  from utilities.timer import Timer
17
18
  from utilities.types import PathLike
@@ -31,7 +32,6 @@ async def pg_dump(
31
32
  path: PathLike,
32
33
  /,
33
34
  *,
34
- docker: str | None = None,
35
35
  format_: _PGDumpFormat = "plain",
36
36
  jobs: int | None = None,
37
37
  schemas: MaybeListStr | None = None,
@@ -39,26 +39,93 @@ async def pg_dump(
39
39
  tables: MaybeSequence[TableOrORMInstOrClass | str] | None = None,
40
40
  tables_exc: MaybeSequence[TableOrORMInstOrClass | str] | None = None,
41
41
  inserts: bool = False,
42
- logger: LoggerOrName | None = None,
42
+ on_conflict_do_nothing: bool = False,
43
+ docker: str | None = None,
43
44
  dry_run: bool = False,
45
+ logger: LoggerOrName | None = None,
44
46
  ) -> None:
45
47
  """Run `pg_dump`."""
46
48
  path = Path(path)
47
49
  path.parent.mkdir(parents=True, exist_ok=True)
48
- if url.database is None:
49
- raise _PGDumpDatabaseError(url=url)
50
- if url.host is None:
51
- raise _PGDumpHostError(url=url)
52
- if url.port is None:
53
- raise _PGDumpPortError(url=url)
54
- parts: list[str] = []
55
- if docker is not None:
56
- parts.extend(["docker", "exec", "-it", docker])
57
- parts.extend([
50
+ cmd = _build_pg_dump(
51
+ url,
52
+ path,
53
+ format_=format_,
54
+ jobs=jobs,
55
+ schemas=schemas,
56
+ schemas_exc=schemas_exc,
57
+ tables=tables,
58
+ tables_exc=tables_exc,
59
+ inserts=inserts,
60
+ on_conflict_do_nothing=on_conflict_do_nothing,
61
+ docker=docker,
62
+ )
63
+ if dry_run:
64
+ if logger is not None:
65
+ get_logger(logger=logger).info("Would run %r", str(cmd))
66
+ return
67
+ with temp_environ(PGPASSWORD=url.password), Timer() as timer: # pragma: no cover
68
+ try:
69
+ output = await stream_command(cmd)
70
+ except KeyboardInterrupt:
71
+ if logger is not None:
72
+ get_logger(logger=logger).info(
73
+ "Cancelled backup to %r after %s", str(path), timer
74
+ )
75
+ rmtree(path, ignore_errors=True)
76
+ else:
77
+ match output.return_code:
78
+ case 0:
79
+ if logger is not None:
80
+ get_logger(logger=logger).info(
81
+ "Backup to %r finished after %s", str(path), timer
82
+ )
83
+ case _:
84
+ if logger is not None:
85
+ get_logger(logger=logger).exception(
86
+ "Backup to %r failed after %s\nstderr:\n%s",
87
+ str(path),
88
+ timer,
89
+ output.stderr,
90
+ )
91
+ rmtree(path, ignore_errors=True)
92
+
93
+
94
+ def _build_pg_dump(
95
+ url: URL,
96
+ path: PathLike,
97
+ /,
98
+ *,
99
+ format_: _PGDumpFormat = "plain",
100
+ jobs: int | None = None,
101
+ schemas: MaybeListStr | None = None,
102
+ schemas_exc: MaybeListStr | None = None,
103
+ tables: MaybeSequence[TableOrORMInstOrClass | str] | None = None,
104
+ tables_exc: MaybeSequence[TableOrORMInstOrClass | str] | None = None,
105
+ inserts: bool = False,
106
+ on_conflict_do_nothing: bool = False,
107
+ docker: str | None = None,
108
+ ) -> str:
109
+ database, host, port = _extract_url(url)
110
+ match format_:
111
+ case "plain":
112
+ suffix = ".sql"
113
+ case "custom":
114
+ suffix = ".pgdump"
115
+ case "directory":
116
+ suffix = None
117
+ case "tar":
118
+ suffix = ".tar"
119
+ case _ as never:
120
+ assert_never(never)
121
+ file = Path(path)
122
+ if suffix is not None:
123
+ file = ensure_suffix(file, suffix)
124
+ parts: list[str] = [
58
125
  "pg_dump",
59
126
  # general options
60
- f"--dbname={url.database}",
61
- f"--file={str(path)!r}",
127
+ f"--dbname={database}",
128
+ f"--file={str(file)!r}",
62
129
  f"--format={format_}",
63
130
  "--verbose",
64
131
  # output options
@@ -68,10 +135,10 @@ async def pg_dump(
68
135
  "--no-privileges",
69
136
  "--if-exists",
70
137
  # connection options
71
- f"--host={url.host}",
72
- f"--port={url.port}",
138
+ f"--host={host}",
139
+ f"--port={port}",
73
140
  "--no-password",
74
- ])
141
+ ]
75
142
  if (format_ == "directory") and (jobs is not None):
76
143
  parts.append(f"--jobs={jobs}")
77
144
  if schemas is not None:
@@ -86,9 +153,47 @@ async def pg_dump(
86
153
  ])
87
154
  if inserts:
88
155
  parts.append("--inserts")
156
+ if on_conflict_do_nothing:
157
+ parts.append("--on-conflict-do-nothing")
89
158
  if url.username is not None:
90
159
  parts.append(f"--username={url.username}")
91
- cmd = " ".join(parts)
160
+ if docker is not None:
161
+ parts = _wrap_docker(parts, docker)
162
+ return " ".join(parts)
163
+
164
+
165
+ ##
166
+
167
+
168
+ async def restore(
169
+ url: URL,
170
+ path: PathLike,
171
+ /,
172
+ *,
173
+ psql: bool = False,
174
+ database: str | None = None,
175
+ data_only: bool = False,
176
+ jobs: int | None = None,
177
+ schemas: MaybeListStr | None = None,
178
+ schemas_exc: MaybeListStr | None = None,
179
+ tables: MaybeSequence[TableOrORMInstOrClass | str] | None = None,
180
+ docker: str | None = None,
181
+ dry_run: bool = False,
182
+ logger: LoggerOrName | None = None,
183
+ ) -> None:
184
+ """Run `pg_restore`/`psql`."""
185
+ cmd = _build_pg_restore_or_psql(
186
+ url,
187
+ path,
188
+ psql=psql,
189
+ database=database,
190
+ data_only=data_only,
191
+ jobs=jobs,
192
+ schemas=schemas,
193
+ schemas_exc=schemas_exc,
194
+ tables=tables,
195
+ docker=docker,
196
+ )
92
197
  if dry_run:
93
198
  if logger is not None:
94
199
  get_logger(logger=logger).info("Would run %r", str(cmd))
@@ -99,89 +204,75 @@ async def pg_dump(
99
204
  except KeyboardInterrupt:
100
205
  if logger is not None:
101
206
  get_logger(logger=logger).info(
102
- "Cancelled backup to %r after %s", str(path), timer
207
+ "Cancelled restore from %r after %s", str(path), timer
103
208
  )
104
- rmtree(path, ignore_errors=True)
105
209
  else:
106
210
  match output.return_code:
107
211
  case 0:
108
212
  if logger is not None:
109
213
  get_logger(logger=logger).info(
110
- "Backup to %r finished after %s", str(path), timer
214
+ "Restore from %r finished after %s", str(path), timer
111
215
  )
112
216
  case _:
113
217
  if logger is not None:
114
218
  get_logger(logger=logger).exception(
115
- "Backup to %r failed after %s\nstderr:\n%s",
219
+ "Restore from %r failed after %s\nstderr:\n%s",
116
220
  str(path),
117
221
  timer,
118
222
  output.stderr,
119
223
  )
120
- rmtree(path, ignore_errors=True)
121
-
122
-
123
- @dataclass(kw_only=True, slots=True)
124
- class PGDumpError(Exception):
125
- url: URL
126
-
127
-
128
- @dataclass(kw_only=True, slots=True)
129
- class _PGDumpDatabaseError(PGDumpError):
130
- @override
131
- def __str__(self) -> str:
132
- return f"Expected URL to contain a 'database'; got {self.url}"
133
-
134
-
135
- @dataclass(kw_only=True, slots=True)
136
- class _PGDumpHostError(PGDumpError):
137
- @override
138
- def __str__(self) -> str:
139
- return f"Expected URL to contain a 'host'; got {self.url}"
140
-
141
-
142
- @dataclass(kw_only=True, slots=True)
143
- class _PGDumpPortError(PGDumpError):
144
- @override
145
- def __str__(self) -> str:
146
- return f"Expected URL to contain a 'port'; got {self.url}"
147
224
 
148
225
 
149
226
  ##
150
227
 
151
228
 
152
- async def pg_restore(
229
+ def _build_pg_restore_or_psql(
153
230
  url: URL,
154
231
  path: PathLike,
155
232
  /,
156
233
  *,
234
+ psql: bool = False,
157
235
  database: str | None = None,
236
+ data_only: bool = False,
237
+ jobs: int | None = None,
238
+ schemas: MaybeListStr | None = None,
239
+ schemas_exc: MaybeListStr | None = None,
240
+ tables: MaybeSequence[TableOrORMInstOrClass | str] | None = None,
158
241
  docker: str | None = None,
242
+ ) -> str:
243
+ path = Path(path)
244
+ if (path.suffix == ".sql") or psql:
245
+ return _build_psql(url, path, database=database, docker=docker)
246
+ return _build_pg_restore(
247
+ url,
248
+ path,
249
+ database=database,
250
+ data_only=data_only,
251
+ jobs=jobs,
252
+ schemas=schemas,
253
+ schemas_exc=schemas_exc,
254
+ tables=tables,
255
+ docker=docker,
256
+ )
257
+
258
+
259
+ def _build_pg_restore(
260
+ url: URL,
261
+ path: PathLike,
262
+ /,
263
+ *,
264
+ database: str | None = None,
159
265
  data_only: bool = False,
160
266
  jobs: int | None = None,
161
267
  schemas: MaybeListStr | None = None,
162
268
  schemas_exc: MaybeListStr | None = None,
163
269
  tables: MaybeSequence[TableOrORMInstOrClass | str] | None = None,
164
- logger: LoggerOrName | None = None,
165
- dry_run: bool = False,
166
- ) -> None:
270
+ docker: str | None = None,
271
+ ) -> str:
167
272
  """Run `pg_restore`."""
168
- match database, url.database:
169
- case str() as database_use, _:
170
- ...
171
- case None, str() as database_use:
172
- ...
173
- case None, None:
174
- raise _PGRestoreDatabaseError(url=url)
175
- case _ as never:
176
- assert_never(never)
177
- if url.host is None:
178
- raise _PGRestoreHostError(url=url)
179
- if url.port is None:
180
- raise _PGRestorePortError(url=url)
181
- parts: list[str] = []
182
- if docker is not None:
183
- parts.extend(["docker", "exec", "-it", docker])
184
- parts.extend([
273
+ url_database, host, port = _extract_url(url)
274
+ database_use = url_database if database is None else database
275
+ parts: list[str] = [
185
276
  "pg_restore",
186
277
  # general options
187
278
  f"--dbname={database_use}",
@@ -191,10 +282,10 @@ async def pg_restore(
191
282
  "--no-owner",
192
283
  "--no-privileges",
193
284
  # connection options
194
- f"--host={url.host}",
195
- f"--port={url.port}",
285
+ f"--host={host}",
286
+ f"--port={port}",
196
287
  "--no-password",
197
- ])
288
+ ]
198
289
  if data_only:
199
290
  parts.append("--data-only")
200
291
  else:
@@ -209,68 +300,80 @@ async def pg_restore(
209
300
  parts.extend([f"--table={_get_table_name(t)}" for t in always_iterable(tables)])
210
301
  if url.username is not None:
211
302
  parts.append(f"--username={url.username}")
303
+ if docker is not None:
304
+ parts = _wrap_docker(parts, docker)
212
305
  parts.append(str(path))
213
- cmd = " ".join(parts)
214
- if dry_run:
215
- if logger is not None:
216
- get_logger(logger=logger).info("Would run %r", str(cmd))
217
- return
218
- with temp_environ(PGPASSWORD=url.password), Timer() as timer: # pragma: no cover
219
- try:
220
- output = await stream_command(cmd)
221
- except KeyboardInterrupt:
222
- if logger is not None:
223
- get_logger(logger=logger).info(
224
- "Cancelled restore from %r after %s", str(path), timer
225
- )
226
- else:
227
- match output.return_code:
228
- case 0:
229
- if logger is not None:
230
- get_logger(logger=logger).info(
231
- "Restore from %r finished after %s", str(path), timer
232
- )
233
- case _:
234
- if logger is not None:
235
- get_logger(logger=logger).exception(
236
- "Restore from %r failed after %s\nstderr:\n%s",
237
- str(path),
238
- timer,
239
- output.stderr,
240
- )
306
+ return " ".join(parts)
307
+
308
+
309
+ def _build_psql(
310
+ url: URL,
311
+ path: PathLike,
312
+ /,
313
+ *,
314
+ database: str | None = None,
315
+ docker: str | None = None,
316
+ ) -> str:
317
+ """Run `psql`."""
318
+ url_database, host, port = _extract_url(url)
319
+ database_use = url_database if database is None else database
320
+ parts: list[str] = [
321
+ "psql",
322
+ # general options
323
+ f"--dbname={database_use}",
324
+ f"--file={str(path)!r}",
325
+ # connection options
326
+ f"--host={host}",
327
+ f"--port={port}",
328
+ "--no-password",
329
+ ]
330
+ if url.username is not None:
331
+ parts.append(f"--username={url.username}")
332
+ if docker is not None:
333
+ parts = _wrap_docker(parts, docker)
334
+ return " ".join(parts)
335
+
336
+
337
+ ##
338
+
339
+
340
+ def _extract_url(url: URL, /) -> tuple[str, str, int]:
341
+ if url.database is None:
342
+ raise _ExtractURLDatabaseError(url=url)
343
+ if url.host is None:
344
+ raise _ExtractURLHostError(url=url)
345
+ if url.port is None:
346
+ raise _ExtractURLPortError(url=url)
347
+ return url.database, url.host, url.port
241
348
 
242
349
 
243
350
  @dataclass(kw_only=True, slots=True)
244
- class PGRestoreError(Exception):
351
+ class ExtractURLError(Exception):
245
352
  url: URL
246
353
 
247
354
 
248
355
  @dataclass(kw_only=True, slots=True)
249
- class _PGRestoreDatabaseError(PGRestoreError):
356
+ class _ExtractURLDatabaseError(ExtractURLError):
250
357
  @override
251
358
  def __str__(self) -> str:
252
359
  return f"Expected URL to contain a 'database'; got {self.url}"
253
360
 
254
361
 
255
362
  @dataclass(kw_only=True, slots=True)
256
- class _PGRestoreHostError(PGRestoreError):
363
+ class _ExtractURLHostError(ExtractURLError):
257
364
  @override
258
365
  def __str__(self) -> str:
259
366
  return f"Expected URL to contain a 'host'; got {self.url}"
260
367
 
261
368
 
262
369
  @dataclass(kw_only=True, slots=True)
263
- class _PGRestorePortError(PGRestoreError):
370
+ class _ExtractURLPortError(ExtractURLError):
264
371
  @override
265
372
  def __str__(self) -> str:
266
373
  return f"Expected URL to contain a 'port'; got {self.url}"
267
374
 
268
375
 
269
- ##
270
-
271
-
272
376
  def _get_table_name(obj: TableOrORMInstOrClass | str, /) -> str:
273
- """Get the table name from a Table or mapped class."""
274
377
  match obj:
275
378
  case Table() | DeclarativeBase() | type() as table_or_orm:
276
379
  return get_table_name(table_or_orm)
@@ -280,4 +383,8 @@ def _get_table_name(obj: TableOrORMInstOrClass | str, /) -> str:
280
383
  assert_never(never)
281
384
 
282
385
 
283
- __all__ = ["PGDumpError", "PGRestoreError", "pg_dump", "pg_restore"]
386
+ def _wrap_docker(parts: list[str], container: str, /) -> list[str]:
387
+ return ["docker", "exec", "-it", container, *parts]
388
+
389
+
390
+ __all__ = ["ExtractURLError", "pg_dump", "restore"]