django-data-shape 0.23.0__tar.gz → 0.24.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 (219) hide show
  1. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/.github/workflows/release.yml +2 -2
  2. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/.github/workflows/tests.yml +5 -5
  3. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/.github/workflows/upstream-drift.yml +2 -2
  4. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/CHANGELOG.md +33 -1
  5. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/PKG-INFO +10 -3
  6. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/README.md +9 -2
  7. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/__init__.py +4 -0
  8. django_data_shape-0.24.0/django_data_shape/databases/derive_template.py +231 -0
  9. django_data_shape-0.24.0/django_data_shape/databases/step_failed.py +24 -0
  10. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/databases/template_database.py +33 -86
  11. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/fixtures/__init__.py +2 -0
  12. django_data_shape-0.24.0/django_data_shape/fixtures/template_fixture.py +157 -0
  13. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/utils.py +75 -0
  14. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/version.py +1 -1
  15. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/docs/pytest.md +88 -6
  16. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/docs/reference.md +6 -0
  17. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/docs/statistics.md +54 -0
  18. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/pyproject.toml +1 -1
  19. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/conftest_settings.py +31 -0
  20. django_data_shape-0.24.0/tests/databases/test_derive_template.py +368 -0
  21. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/databases/test_template_database.py +11 -8
  22. django_data_shape-0.24.0/tests/fixtures/test_template_fixture.py +119 -0
  23. django_data_shape-0.24.0/tests/fixtures/test_template_fixture_leaves_other_databases.py +89 -0
  24. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/uv.lock +56 -55
  25. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/.github/CODEOWNERS +0 -0
  26. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/.github/SECURITY.md +0 -0
  27. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/.github/dependabot.yml +0 -0
  28. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/.gitignore +0 -0
  29. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/.pre-commit-config.yaml +0 -0
  30. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/CLAUDE.md +0 -0
  31. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/LICENSE +0 -0
  32. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/Makefile +0 -0
  33. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/backends/__init__.py +0 -0
  34. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/backends/require_clone_strategy.py +0 -0
  35. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/backends/require_postgres.py +0 -0
  36. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/backends/unsupported_backend.py +0 -0
  37. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/databases/__init__.py +0 -0
  38. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/databases/clone_database.py +0 -0
  39. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/databases/drop_database.py +0 -0
  40. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/databases/require_quotable_name.py +0 -0
  41. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/databases/shape_digest.py +0 -0
  42. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/databases/unhashable_shape.py +0 -0
  43. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/databases/unusable_base.py +0 -0
  44. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/declaration/__init__.py +0 -0
  45. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/declaration/check_constraints.py +0 -0
  46. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/declaration/invalid_shape.py +0 -0
  47. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/declaration/order_derivations.py +0 -0
  48. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/declaration/order_tables.py +0 -0
  49. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/declaration/projection.py +0 -0
  50. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/declaration/shape.py +0 -0
  51. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/declaration/shape_from_factory.py +0 -0
  52. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/declaration/sql_value.py +0 -0
  53. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/declaration/table.py +0 -0
  54. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/derivations/__init__.py +0 -0
  55. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/derivations/after.py +0 -0
  56. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/derivations/aligned.py +0 -0
  57. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/derivations/copied.py +0 -0
  58. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/derivations/derivation.py +0 -0
  59. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/derivations/derived.py +0 -0
  60. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/derivations/given.py +0 -0
  61. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/derivations/offset.py +0 -0
  62. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/derivations/per_parent.py +0 -0
  63. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/derivations/product.py +0 -0
  64. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/derivations/scope.py +0 -0
  65. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/distributions/__init__.py +0 -0
  66. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/distributions/ascending.py +0 -0
  67. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/distributions/bounded.py +0 -0
  68. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/distributions/categorical.py +0 -0
  69. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/distributions/constant.py +0 -0
  70. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/distributions/distinct.py +0 -0
  71. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/distributions/distribution.py +0 -0
  72. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/distributions/sequential.py +0 -0
  73. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/distributions/skew.py +0 -0
  74. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/distributions/uniform.py +0 -0
  75. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/distributions/zipf.py +0 -0
  76. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/fixtures/scale_fixture.py +0 -0
  77. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/fixtures/shape_fixture.py +0 -0
  78. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/fixtures/skip_unless_postgres.py +0 -0
  79. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/generation/__init__.py +0 -0
  80. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/generation/derivation_queried_database.py +0 -0
  81. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/generation/generate_rows.py +0 -0
  82. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/generation/refuse_queries.py +0 -0
  83. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/invariants/__init__.py +0 -0
  84. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/invariants/check_invariants.py +0 -0
  85. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/invariants/invariant.py +0 -0
  86. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/invariants/invariant_violated.py +0 -0
  87. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/keys/__init__.py +0 -0
  88. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/keys/disjoint.py +0 -0
  89. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/keys/infer_key_strategy.py +0 -0
  90. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/keys/is_disjoint.py +0 -0
  91. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/keys/key_function.py +0 -0
  92. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/keys/key_strategy.py +0 -0
  93. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/keys/md5_keys.py +0 -0
  94. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/keys/sequential_keys.py +0 -0
  95. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/keys/sql_keys.py +0 -0
  96. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/keys/uuid_keys.py +0 -0
  97. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/loading/__init__.py +0 -0
  98. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/loading/apply_statistics_targets.py +0 -0
  99. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/loading/build.py +0 -0
  100. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/loading/shape_not_empty.py +0 -0
  101. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/py.typed +0 -0
  102. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/relations/__init__.py +0 -0
  103. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/relations/fan_out.py +0 -0
  104. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/relations/fan_out_plan.py +0 -0
  105. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/relations/fan_out_sizes.py +0 -0
  106. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/relations/paired.py +0 -0
  107. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/relations/paired_plan.py +0 -0
  108. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/relations/resolve_fan_out.py +0 -0
  109. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/relations/resolve_paired.py +0 -0
  110. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/relations/world_changed.py +0 -0
  111. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/scaling/__init__.py +0 -0
  112. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/scaling/scale_protocol.py +0 -0
  113. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/scaling/scaled_shape.py +0 -0
  114. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/scaling/scaled_world.py +0 -0
  115. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/scaling/shape_referenced.py +0 -0
  116. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/types/__init__.py +0 -0
  117. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/types/build_result.py +0 -0
  118. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/types/canonical.py +0 -0
  119. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/types/children_per_parent.py +0 -0
  120. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/django_data_shape/types/table_result.py +0 -0
  121. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/docs/derivations.md +0 -0
  122. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/docs/index.md +0 -0
  123. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/docs/invariants.md +0 -0
  124. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/docs/keys.md +0 -0
  125. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/docs/projections.md +0 -0
  126. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/docs/relations.md +0 -0
  127. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/mkdocs.yml +0 -0
  128. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/scripts/check-changelog +0 -0
  129. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/scripts/release-publish.sh +0 -0
  130. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/__init__.py +0 -0
  131. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/backends/__init__.py +0 -0
  132. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/backends/test_require_clone_strategy.py +0 -0
  133. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/backends/test_require_postgres.py +0 -0
  134. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/conftest.py +0 -0
  135. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/databases/__init__.py +0 -0
  136. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/databases/test_require_quotable_name.py +0 -0
  137. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/databases/test_shape_digest.py +0 -0
  138. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/declaration/__init__.py +0 -0
  139. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/declaration/test_check_constraints.py +0 -0
  140. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/declaration/test_order_derivations.py +0 -0
  141. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/declaration/test_order_tables.py +0 -0
  142. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/declaration/test_projection.py +0 -0
  143. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/declaration/test_projection_ceiling.py +0 -0
  144. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/declaration/test_projection_sql_percent.py +0 -0
  145. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/declaration/test_projection_values.py +0 -0
  146. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/declaration/test_projection_values_references.py +0 -0
  147. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/declaration/test_projection_values_uuid.py +0 -0
  148. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/declaration/test_shape.py +0 -0
  149. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/declaration/test_shape_from_factory.py +0 -0
  150. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/declaration/test_table.py +0 -0
  151. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/derivations/__init__.py +0 -0
  152. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/derivations/test_after.py +0 -0
  153. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/derivations/test_after_units.py +0 -0
  154. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/derivations/test_aligned.py +0 -0
  155. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/derivations/test_canonical_arithmetic.py +0 -0
  156. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/derivations/test_derived.py +0 -0
  157. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/derivations/test_given.py +0 -0
  158. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/derivations/test_per_parent.py +0 -0
  159. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/distributions/__init__.py +0 -0
  160. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/distributions/test_constant.py +0 -0
  161. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/distributions/test_sequential.py +0 -0
  162. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/distributions/test_skew.py +0 -0
  163. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/distributions/test_uniform.py +0 -0
  164. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/distributions/test_zipf.py +0 -0
  165. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/fixtures/__init__.py +0 -0
  166. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/fixtures/test_fixtures.py +0 -0
  167. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/generation/__init__.py +0 -0
  168. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/generation/test_generate_rows.py +0 -0
  169. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/generation/test_refuse_queries.py +0 -0
  170. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/invariants/__init__.py +0 -0
  171. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/invariants/test_invariant.py +0 -0
  172. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/keys/__init__.py +0 -0
  173. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/keys/test_infer_key_strategy.py +0 -0
  174. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/keys/test_is_disjoint.py +0 -0
  175. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/keys/test_key_function.py +0 -0
  176. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/keys/test_md5_keys.py +0 -0
  177. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/keys/test_sequential_keys.py +0 -0
  178. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/keys/test_uuid_keys.py +0 -0
  179. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/loading/__init__.py +0 -0
  180. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/loading/test_apply_statistics_targets.py +0 -0
  181. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/loading/test_build.py +0 -0
  182. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/loading/test_build_derivations.py +0 -0
  183. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/loading/test_build_graph.py +0 -0
  184. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/loading/test_build_invariants.py +0 -0
  185. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/loading/test_build_keys.py +0 -0
  186. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/loading/test_build_m2m.py +0 -0
  187. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/loading/test_build_portable.py +0 -0
  188. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/loading/test_build_projection.py +0 -0
  189. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/relations/__init__.py +0 -0
  190. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/relations/test_fan_out.py +0 -0
  191. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/relations/test_fan_out_sizes.py +0 -0
  192. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/relations/test_paired.py +0 -0
  193. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/relations/test_paired_parents.py +0 -0
  194. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/relations/test_paired_plan.py +0 -0
  195. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/relations/test_parents_key_spelling.py +0 -0
  196. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/relations/test_resolve_fan_out.py +0 -0
  197. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/relations/test_resolve_paired.py +0 -0
  198. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/scaling/__init__.py +0 -0
  199. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/scaling/scale_protocol_consumers.py +0 -0
  200. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/scaling/scale_protocol_impostors.py +0 -0
  201. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/scaling/test_scale_protocol.py +0 -0
  202. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/scaling/test_scaled_projection_ceiling.py +0 -0
  203. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/scaling/test_scaled_shape.py +0 -0
  204. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/scaling/test_scaled_world.py +0 -0
  205. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/scaling/test_scaled_world_disjoint_advice.py +0 -0
  206. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/scaling/test_scaled_world_leaves_undeclared_tables_alone.py +0 -0
  207. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/scaling/test_scaled_world_over_a_session_world.py +0 -0
  208. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/scaling/test_scaled_world_over_disjoint_tables.py +0 -0
  209. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/scaling/test_scaled_world_pending_trigger_events.py +0 -0
  210. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/scaling/test_scaled_world_restores_sequences.py +0 -0
  211. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/test_documentation.py +0 -0
  212. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/test_package_layout.py +0 -0
  213. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/test_reference_documents_the_public_surface.py +0 -0
  214. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/test_utils.py +0 -0
  215. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/test_version.py +0 -0
  216. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/testapp/__init__.py +0 -0
  217. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/testapp/models.py +0 -0
  218. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/types/__init__.py +0 -0
  219. {django_data_shape-0.23.0 → django_data_shape-0.24.0}/tests/types/test_children_per_parent.py +0 -0
@@ -56,7 +56,7 @@ jobs:
56
56
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
57
57
  with:
58
58
  fetch-depth: 0
59
- - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
59
+ - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
60
60
  with:
61
61
  enable-cache: true
62
62
  - name: Install
@@ -68,7 +68,7 @@ jobs:
68
68
 
69
69
  - name: Publish to PyPI
70
70
  if: steps.prepare.outputs.released == 'true'
71
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
71
+ uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
72
72
  with:
73
73
  # Idempotent on purpose. What gates a release is the git tag, not the
74
74
  # index (see `prepare`), so re-running this job after a failure in a
@@ -20,7 +20,7 @@ jobs:
20
20
  runs-on: ubuntu-latest
21
21
  steps:
22
22
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
23
- - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
23
+ - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
24
24
  with:
25
25
  enable-cache: true
26
26
  - name: Install
@@ -37,7 +37,7 @@ jobs:
37
37
  runs-on: ubuntu-latest
38
38
  steps:
39
39
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
40
- - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
40
+ - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
41
41
  with:
42
42
  enable-cache: true
43
43
  - name: Install with docs group
@@ -79,7 +79,7 @@ jobs:
79
79
  PGDATABASE: django_data_shape
80
80
  steps:
81
81
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
82
- - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
82
+ - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
83
83
  with:
84
84
  enable-cache: true
85
85
  # A floor is a claim about the *oldest* version that works, and a lockfile
@@ -199,7 +199,7 @@ jobs:
199
199
 
200
200
  steps:
201
201
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
202
- - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
202
+ - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
203
203
  with:
204
204
  enable-cache: true
205
205
  - name: Install Python ${{ matrix.python-version }}
@@ -257,7 +257,7 @@ jobs:
257
257
  PGDATABASE: django_data_shape
258
258
  steps:
259
259
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
260
- - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
260
+ - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
261
261
  with:
262
262
  enable-cache: true
263
263
  - name: Install Python 3.13
@@ -72,14 +72,14 @@ jobs:
72
72
  PGDATABASE: django_data_shape
73
73
  steps:
74
74
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
75
- - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
75
+ - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
76
76
  with:
77
77
  enable-cache: false
78
78
 
79
79
  # One cell, not the matrix. The matrix answers "does this combination
80
80
  # work"; this answers "has an upstream moved under us", and the newest
81
81
  # supported Python is where a new release lands first.
82
- - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
82
+ - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
83
83
  with:
84
84
  python-version: "3.14"
85
85
 
@@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.24.0] — 2026-11-07
11
+
12
+ ### Added
13
+
14
+ - **`derive_template(template, step, *, key)` caches a world the application
15
+ finishes.** Some columns are an application's own arithmetic, such as running
16
+ totals a replay through its services computes, and no declaration can describe
17
+ them. It copies a template `template_database` (or `derive_template`) returned,
18
+ runs `step(using)` once over the copy with the alias pointed at it, runs
19
+ `ANALYZE` over the whole database -- not only the declared tables, since the
20
+ step may write to others -- and keeps the result as a template of its own, so a
21
+ session pays a clone rather than the replay. `key` is required: a callable
22
+ cannot be hashed, so the caller names the step and changes the key when the
23
+ step changes. The derived name is a digest of the parent's name and the key,
24
+ so it also moves with everything the parent's name moves with. Steps chain, a
25
+ step that raises leaves nothing behind and is raised again as `StepFailed`
26
+ naming the key, and a parent this package did not finish is refused.
27
+ `strategy=` is the copy's, as for `clone_database`.
28
+ - **`template_fixture(template)`, a session-scoped pytest fixture that makes the
29
+ test database a clone of a template**, which is how a test reaches a derived
30
+ one. `template` is a callable taking the connection alias and returning a
31
+ template's name. It requests `django_db_setup` and then replaces the test
32
+ database pytest-django made, under the same name, so xdist suffixes and
33
+ pytest-django's teardown work as before. Only a test database is replaced: in a
34
+ run whose tests touch no database the alias still names your configured one
35
+ (pytest-django 4.11 and later), and the fixture leaves it alone. The pytest page lists what replacing the
36
+ database means for tests that ran earlier and for transactional tests.
37
+ - **`StepFailed`**, raised from the step's own exception.
38
+ - **Documentation:** the statistics page has a section on a world the
39
+ application finishes, and the pytest page one on `template_fixture`.
40
+
10
41
  ## [0.23.0] — 2026-10-23
11
42
 
12
43
  ### Added
@@ -1751,7 +1782,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1751
1782
  into `COPY FROM STDIN`, which psycopg 2 cannot do without materialising them
1752
1783
  first.
1753
1784
 
1754
- [Unreleased]: https://github.com/Artui/django-data-shape/compare/v0.23.0...HEAD
1785
+ [Unreleased]: https://github.com/Artui/django-data-shape/compare/v0.24.0...HEAD
1786
+ [0.24.0]: https://github.com/Artui/django-data-shape/compare/v0.23.0...v0.24.0
1755
1787
  [0.23.0]: https://github.com/Artui/django-data-shape/compare/v0.22.0...v0.23.0
1756
1788
  [0.22.0]: https://github.com/Artui/django-data-shape/compare/v0.21.0...v0.22.0
1757
1789
  [0.21.0]: https://github.com/Artui/django-data-shape/compare/v0.20.0...v0.21.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: django-data-shape
3
- Version: 0.23.0
3
+ Version: 0.24.0
4
4
  Summary: A realistically shaped test database from Django models: declare cardinality, skew and fan-out, load by COPY, and make the query planner believe it.
5
5
  Project-URL: Homepage, https://github.com/Artui/django-data-shape
6
6
  Project-URL: Repository, https://github.com/Artui/django-data-shape
@@ -279,7 +279,12 @@ history no forward migration can fix, such as a base ahead of the migrations on
279
279
  disk, is refused.
280
280
  A shape holding a `Derived` or a `KeyFunction` is refused rather than hashed --
281
281
  there is no honest digest of a callable, and every way of guessing one agrees
282
- while the data has changed. See
282
+ while the data has changed. A world your own code finishes after the build -- a
283
+ replay that fills running totals -- is cached too:
284
+ `derive_template(template, step, key="replay-3")` runs the step once over a copy
285
+ of the template, analyzes the whole database afterwards and keeps the result as
286
+ a template of its own, under a name keyed on the parent and the key you change
287
+ when the step does. See
283
288
  [Statistics and reuse](https://artui.github.io/django-data-shape/statistics/).
284
289
 
285
290
  ## From pytest
@@ -294,7 +299,9 @@ world = scale_fixture(Shape(Table(Order, rows=100, status=Constant("complete")))
294
299
  ```
295
300
 
296
301
  `orders` is one world built once for the whole session, composed with
297
- pytest-django rather than replacing it. `world` is the **scale protocol**: make
302
+ pytest-django rather than replacing it. `template_fixture` is the other way in:
303
+ it makes the test database a clone of a template, a derived one included, for
304
+ the whole session. `world` is the **scale protocol**: make
298
305
  the world be at factor F, then let the caller run its block, which is what a
299
306
  query count asserted to be `O(1)` rather than `O(N)` needs.
300
307
 
@@ -236,7 +236,12 @@ history no forward migration can fix, such as a base ahead of the migrations on
236
236
  disk, is refused.
237
237
  A shape holding a `Derived` or a `KeyFunction` is refused rather than hashed --
238
238
  there is no honest digest of a callable, and every way of guessing one agrees
239
- while the data has changed. See
239
+ while the data has changed. A world your own code finishes after the build -- a
240
+ replay that fills running totals -- is cached too:
241
+ `derive_template(template, step, key="replay-3")` runs the step once over a copy
242
+ of the template, analyzes the whole database afterwards and keeps the result as
243
+ a template of its own, under a name keyed on the parent and the key you change
244
+ when the step does. See
240
245
  [Statistics and reuse](https://artui.github.io/django-data-shape/statistics/).
241
246
 
242
247
  ## From pytest
@@ -251,7 +256,9 @@ world = scale_fixture(Shape(Table(Order, rows=100, status=Constant("complete")))
251
256
  ```
252
257
 
253
258
  `orders` is one world built once for the whole session, composed with
254
- pytest-django rather than replacing it. `world` is the **scale protocol**: make
259
+ pytest-django rather than replacing it. `template_fixture` is the other way in:
260
+ it makes the test database a clone of a template, a derived one included, for
261
+ the whole session. `world` is the **scale protocol**: make
255
262
  the world be at factor F, then let the caller run its block, which is what a
256
263
  query count asserted to be `O(1)` rather than `O(N)` needs.
257
264
 
@@ -3,8 +3,10 @@
3
3
  from django_data_shape.backends.require_clone_strategy import require_clone_strategy
4
4
  from django_data_shape.backends.unsupported_backend import UnsupportedBackend
5
5
  from django_data_shape.databases.clone_database import clone_database
6
+ from django_data_shape.databases.derive_template import derive_template
6
7
  from django_data_shape.databases.drop_database import drop_database
7
8
  from django_data_shape.databases.shape_digest import shape_digest
9
+ from django_data_shape.databases.step_failed import StepFailed
8
10
  from django_data_shape.databases.template_database import template_database
9
11
  from django_data_shape.databases.unhashable_shape import UnhashableShape
10
12
  from django_data_shape.databases.unusable_base import UnusableBase
@@ -103,6 +105,7 @@ __all__ = [
103
105
  "Skew",
104
106
  "SqlKeys",
105
107
  "SqlValue",
108
+ "StepFailed",
106
109
  "Table",
107
110
  "TableResult",
108
111
  "UnhashableShape",
@@ -117,6 +120,7 @@ __all__ = [
117
120
  "check_constraints",
118
121
  "check_invariants",
119
122
  "clone_database",
123
+ "derive_template",
120
124
  "drop_database",
121
125
  "fan_out_sizes",
122
126
  "require_clone_strategy",
@@ -0,0 +1,231 @@
1
+ """A template made from another by running the application's own code over it."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from collections.abc import Callable
7
+ from typing import Any
8
+
9
+ from django.db import DEFAULT_DB_ALIAS, connections
10
+ from django.db.transaction import TransactionManagementError
11
+
12
+ from django_data_shape.backends.require_clone_strategy import require_clone_strategy
13
+ from django_data_shape.backends.require_postgres import require_postgres
14
+ from django_data_shape.databases.clone_database import clone_database
15
+ from django_data_shape.databases.step_failed import StepFailed
16
+ from django_data_shape.utils import (
17
+ FINISHED_TEMPLATE,
18
+ PARTIAL_SUFFIX,
19
+ TEMPLATE_KEY_BYTES,
20
+ TEMPLATE_PREFIX,
21
+ absorb,
22
+ pointed_at,
23
+ template_lock_id,
24
+ )
25
+
26
+ # Its own format tag, so a derived name can never be the name template_database
27
+ # would give some shape: the two digests absorb different things, and the tag is
28
+ # what keeps them in different spaces.
29
+ _FORMAT = "django-data-shape derived template 1"
30
+
31
+
32
+ def derive_template(
33
+ template: str,
34
+ step: Callable[[str], object],
35
+ *,
36
+ key: str,
37
+ using: str = DEFAULT_DB_ALIAS,
38
+ strategy: str | None = "file_copy",
39
+ ) -> str:
40
+ """Make sure a template holding ``template`` with ``step`` run over it exists, and return its name.
41
+
42
+ For the world an application finishes itself. A shape describes what can be
43
+ declared; some columns are the application's own arithmetic -- running
44
+ totals a replay through its services fills in -- and no declaration can
45
+ describe them. Run that code over a fresh test database every session and
46
+ the cache pays for the cheap half only: measured on a consumer's world, the
47
+ template took 2.5 s to build and the replay over it 93 s. This puts the
48
+ replay inside the cache. The step runs once per machine, over a copy of
49
+ ``template``, and the result is a template like any other, cloned with
50
+ :func:`~django_data_shape.databases.clone_database.clone_database` in a
51
+ fraction of a second::
52
+
53
+ template = template_database(shape)
54
+ finished = derive_template(template, replay_draws, key="replay-draws-3")
55
+ clone_database(finished, "test_myapp", replace=True)
56
+
57
+ **What the step is handed.** ``step(using)`` is called with the connection
58
+ alias pointed at the copy, under its working name, the way the build itself
59
+ reaches a template: the ORM, ``connections[using]`` and anything else going
60
+ through that alias write into the copy. Its return value is ignored. It runs
61
+ outside any transaction this function opens, so what it commits is what the
62
+ template holds; a step that leaves another connection attached to the copy
63
+ stops the copy being renamed, as anything attached to a database does.
64
+
65
+ **``key`` is required, and it is the step's identity.** A callable cannot be
66
+ hashed, for the reason a shape holding a
67
+ :class:`~django_data_shape.derivations.derived.Derived` is refused: two
68
+ closures agree while the data differs, which is the one direction a cache key
69
+ must never be wrong. So the caller names the step, and **changes the key when
70
+ the step changes**. The template's name is a digest of ``template``'s name
71
+ and ``key``, and ``template``'s name is itself a digest of the shape, the
72
+ schema, the settings, the base and this package's version, so a change to any
73
+ of those makes a new template here as well, without the key having to say so.
74
+
75
+ **The cache trusts the step to be a function of the world it is given.** A
76
+ step reading the clock, drawing unseeded randomness or reading anything
77
+ outside the database produces whatever its first run produced, under a key
78
+ that claims more. That is stated rather than detected, as the rows a base
79
+ holds are for ``template_database``.
80
+
81
+ **The whole database is analyzed after the step**, not the tables the shape
82
+ declared: the step may have written to tables the shape never named, and a
83
+ table with no statistics is worse than a small one. Anything else that
84
+ writes to a world after it is built leaves its statistics stale, whether or
85
+ not it goes through this function.
86
+
87
+ **Steps chain**, because a derived template is a template: derive from one
88
+ to run a second step over the first, and changing the second step's key
89
+ re-runs only the second step. Changing ``key`` never rebuilds what is under
90
+ it, which is why this derives from a template rather than taking the step as
91
+ an argument to ``template_database``.
92
+
93
+ **A step that raises leaves nothing behind.** The copy is dropped, so the
94
+ next call starts again from ``template``, and the exception is raised again
95
+ as :class:`~django_data_shape.databases.step_failed.StepFailed`, naming the
96
+ key, with the step's own exception as its cause. A ``KeyboardInterrupt`` or
97
+ any other ``BaseException`` drops the copy and goes on unwrapped.
98
+
99
+ **What it refuses**, each before anything is created:
100
+
101
+ - **A ``template`` this package did not finish.** Its name has to be one
102
+ ``template_database`` or this function returned, a prefix and a digest,
103
+ because the derived name is keyed on it: a database of the caller's own
104
+ keeps its name while its rows change, and a template derived from it would
105
+ be served stale. A partial is refused for the same reason, and so is a
106
+ name holding a double quote, which no generated name does. ``ValueError``,
107
+ because what is wrong is an argument.
108
+ - **A ``template`` that does not exist**, when the derived one is not there
109
+ either. ``ValueError`` again, naming it. On a hit the parent is not
110
+ needed and not looked for, so a parent dropped after deriving from it
111
+ costs nothing until the derived template goes too.
112
+ - **Anything but PostgreSQL**, and **being called inside a transaction**,
113
+ for the reasons ``template_database`` gives: there is no ``CREATE
114
+ DATABASE ... TEMPLATE`` elsewhere, and pointing the connection at the copy
115
+ closes it, which leaves an atomic block unusable.
116
+ - **A ``strategy`` the server cannot take.** It means what it means for
117
+ :func:`~django_data_shape.databases.clone_database.clone_database`, which
118
+ makes the copy: ``file_copy`` by default, because a world worth deriving
119
+ is a world worth copying cheaply, and ``None`` for a PostgreSQL older than
120
+ 15. It is not part of the key, because it decides what the copy costs and
121
+ not what it holds.
122
+
123
+ Parallel workers asking for the same derived template wait on one advisory
124
+ lock, taken on its name, while one of them runs the step. The finished
125
+ template refuses connections, as every template this package makes does;
126
+ nothing removes it, and :func:`~django_data_shape.databases.drop_database.drop_database`
127
+ is how a stale one goes.
128
+ """
129
+ # Loosely typed for the reason every other backend-specific reach in this
130
+ # package is: ``_nodb_cursor`` is the wrapper's, not the base class's.
131
+ connection: Any = connections[using]
132
+ require_postgres(connection, "Deriving a template database")
133
+ if connection.in_atomic_block:
134
+ raise TransactionManagementError(
135
+ "Deriving a template database points the connection at a copy and closes it, which "
136
+ "cannot be done inside an atomic block -- the connection would be unusable for the "
137
+ "rest of it. Call this from test session setup, before anything has opened a "
138
+ "transaction."
139
+ )
140
+ # Before the name is looked at, so a strategy this server cannot take is
141
+ # refused on a hit as well as on a miss
142
+ # (test_a_strategy_it_cannot_take_is_refused_before_anything_is_looked_up).
143
+ require_clone_strategy(connection, strategy)
144
+ if not FINISHED_TEMPLATE.fullmatch(template):
145
+ raise ValueError(
146
+ f"{template!r} is not a template this package finished, so nothing can be derived "
147
+ "from it. A derived template is named after its parent's name and the key, which "
148
+ "keeps up with the data only when the parent's name is itself a digest of "
149
+ "everything under it, as the names template_database and derive_template return "
150
+ "are: a database of your own keeps its name while its rows change, and a template "
151
+ "derived from it would be served stale. Pass a name one of those two returned."
152
+ )
153
+ name = f"{TEMPLATE_PREFIX}{_key(template, key)}"
154
+ quote = connection.ops.quote_name
155
+ with connection._nodb_cursor() as cursor:
156
+ # Held for the whole check-then-create, for the reason template_database
157
+ # holds its own: two workers that both read "not there" would both run
158
+ # the step, and the second would fail on a name the first had taken.
159
+ cursor.execute("SELECT pg_advisory_lock(%s)", [template_lock_id(name)])
160
+ try:
161
+ cursor.execute("SELECT 1 FROM pg_database WHERE datname = %s", [name])
162
+ if cursor.fetchone() is None:
163
+ _create(connection, cursor, template, step, key, name, using, quote, strategy)
164
+ finally:
165
+ cursor.execute("SELECT pg_advisory_unlock(%s)", [template_lock_id(name)])
166
+ return name
167
+
168
+
169
+ def _create(
170
+ connection: Any,
171
+ cursor: Any,
172
+ template: str,
173
+ step: Callable[[str], object],
174
+ key: str,
175
+ name: str,
176
+ using: str,
177
+ quote: Any,
178
+ strategy: str | None,
179
+ ) -> None:
180
+ """Copy the parent under a working name, run the step over it, analyze, then adopt it.
181
+
182
+ The rename is the commit, as it is for a built template: the derived name
183
+ exists only once the step and the ``ANALYZE`` after it have finished, so a
184
+ copy interrupted halfway is never mistaken for a finished one, and one a
185
+ killed process left behind is dropped first.
186
+ """
187
+ partial = f"{name}{PARTIAL_SUFFIX}"
188
+ cursor.execute(f"DROP DATABASE IF EXISTS {quote(partial)}")
189
+ # Looked up here rather than left to CREATE DATABASE, whose own refusal
190
+ # names neither this function nor what to do about it
191
+ # (test_a_parent_that_does_not_exist_is_refused_by_name).
192
+ cursor.execute("SELECT 1 FROM pg_database WHERE datname = %s", [template])
193
+ if cursor.fetchone() is None:
194
+ raise ValueError(
195
+ f"The template {template!r} does not exist, so nothing can be derived from it. Make "
196
+ "it again with template_database, or with derive_template if it was derived, and "
197
+ "derive from the name that returns."
198
+ )
199
+ clone_database(template, partial, using=using, strategy=strategy)
200
+ try:
201
+ with pointed_at(connection, partial):
202
+ step(using)
203
+ # The whole database rather than the tables the shape declared,
204
+ # because the step may have written to tables the shape never named
205
+ # (test_the_statistics_describe_the_world_after_the_step).
206
+ with connection.cursor() as analyzing:
207
+ analyzing.execute("ANALYZE")
208
+ except Exception as failed:
209
+ cursor.execute(f"DROP DATABASE IF EXISTS {quote(partial)}")
210
+ raise StepFailed(
211
+ f"The step deriving a template from {template!r} under key {key!r} raised "
212
+ f"{type(failed).__name__}: {failed}. Nothing was kept: the copy it ran in was "
213
+ "dropped, so the next call runs the step again from the same template."
214
+ ) from failed
215
+ except BaseException:
216
+ # Not a failure of the step, so not wrapped; the copy goes all the same,
217
+ # because a partial is never a finished template
218
+ # (test_an_interrupt_is_not_wrapped_and_leaves_nothing).
219
+ cursor.execute(f"DROP DATABASE IF EXISTS {quote(partial)}")
220
+ raise
221
+ cursor.execute(f"ALTER DATABASE {quote(partial)} RENAME TO {quote(name)}")
222
+ cursor.execute(f"ALTER DATABASE {quote(name)} WITH ALLOW_CONNECTIONS false")
223
+
224
+
225
+ def _key(template: str, key: str) -> str:
226
+ """The parent's name and the caller's key, digested to the length a template name takes."""
227
+ hasher = hashlib.blake2b(digest_size=TEMPLATE_KEY_BYTES)
228
+ absorb(hasher, _FORMAT)
229
+ absorb(hasher, template)
230
+ absorb(hasher, key)
231
+ return hasher.hexdigest()
@@ -0,0 +1,24 @@
1
+ """Raised when the step a derived template is made with fails."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class StepFailed(Exception):
7
+ """The step given to ``derive_template`` raised, and nothing was kept.
8
+
9
+ Raised from the step's own exception, which is its ``__cause__``, so the
10
+ traceback shows both: what went wrong inside the step, and which template and
11
+ key it was deriving. The key is the part worth naming, because it is the only
12
+ thing that tells one step from another -- a callable has no name a cache can
13
+ trust, which is why ``derive_template`` asks for a key at all.
14
+
15
+ Its own type rather than the step's exception passed through, because the
16
+ step's exception says what failed and not where: a ``KeyError`` from a
17
+ replay deep inside a session fixture reads the same as one from the test.
18
+ Only an ``Exception`` is wrapped. A ``KeyboardInterrupt`` or ``SystemExit``
19
+ is not a failure of the step, and goes on as it came.
20
+
21
+ Nothing is left behind either way: the partial copy the step ran in is
22
+ dropped, so the next call starts again from the parent template and runs the
23
+ step again.
24
+ """