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