plain.postgres 0.103.3__tar.gz → 0.103.5__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 (211) hide show
  1. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/.gitignore +1 -0
  2. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/PKG-INFO +1 -1
  3. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/CHANGELOG.md +21 -0
  4. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/forms.py +2 -2
  5. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/preflight.py +10 -3
  6. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/sources.py +4 -0
  7. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/pyproject.toml +2 -2
  8. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/urls.py +5 -5
  9. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/views.py +6 -5
  10. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/settings.py +1 -0
  11. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/urls.py +3 -3
  12. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_connection_lifecycle.py +14 -14
  13. plain_postgres-0.103.5/tests/internal/test_preflight_duplicate_indexes.py +102 -0
  14. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_modelform_roundtrip.py +18 -18
  15. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/CLAUDE.md +0 -0
  16. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/LICENSE +0 -0
  17. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/README.md +0 -0
  18. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/README.md +0 -0
  19. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/__init__.py +0 -0
  20. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/adapters.py +0 -0
  21. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/agents/.claude/rules/plain-postgres.md +0 -0
  22. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
  23. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/aggregates.py +0 -0
  24. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/base.py +0 -0
  25. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/cli/__init__.py +0 -0
  26. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/cli/converge.py +0 -0
  27. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/cli/core.py +0 -0
  28. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/cli/decorators.py +0 -0
  29. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/cli/diagnose.py +0 -0
  30. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/cli/migrations.py +0 -0
  31. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/cli/schema.py +0 -0
  32. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/cli/sync.py +0 -0
  33. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/config.py +0 -0
  34. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/connection.py +0 -0
  35. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/constants.py +0 -0
  36. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/constraints.py +0 -0
  37. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/convergence/__init__.py +0 -0
  38. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/convergence/analysis.py +0 -0
  39. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/convergence/fixes.py +0 -0
  40. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/convergence/planning.py +0 -0
  41. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/database_url.py +0 -0
  42. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/db.py +0 -0
  43. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/ddl.py +0 -0
  44. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/default_settings.py +0 -0
  45. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/deletion.py +0 -0
  46. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/dialect.py +0 -0
  47. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/entrypoints.py +0 -0
  48. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/enums.py +0 -0
  49. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/exceptions.py +0 -0
  50. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/expressions.py +0 -0
  51. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/__init__.py +0 -0
  52. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/base.py +0 -0
  53. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/binary.py +0 -0
  54. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/boolean.py +0 -0
  55. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/duration.py +0 -0
  56. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/encrypted.py +0 -0
  57. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/json.py +0 -0
  58. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/mixins.py +0 -0
  59. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/network.py +0 -0
  60. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/numeric.py +0 -0
  61. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/primary_key.py +0 -0
  62. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/related.py +0 -0
  63. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/related_descriptors.py +0 -0
  64. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/related_lookups.py +0 -0
  65. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/related_managers.py +0 -0
  66. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/reverse_descriptors.py +0 -0
  67. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/reverse_related.py +0 -0
  68. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/temporal.py +0 -0
  69. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/text.py +0 -0
  70. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/timezones.py +0 -0
  71. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/uuid.py +0 -0
  72. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/functions/__init__.py +0 -0
  73. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/functions/comparison.py +0 -0
  74. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/functions/datetime.py +0 -0
  75. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/functions/math.py +0 -0
  76. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/functions/mixins.py +0 -0
  77. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/functions/random.py +0 -0
  78. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/functions/text.py +0 -0
  79. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/functions/uuid.py +0 -0
  80. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/functions/window.py +0 -0
  81. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/indexes.py +0 -0
  82. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/introspection/__init__.py +0 -0
  83. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/introspection/health/__init__.py +0 -0
  84. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/introspection/health/checks_cumulative.py +0 -0
  85. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/introspection/health/checks_snapshot.py +0 -0
  86. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/introspection/health/checks_structural.py +0 -0
  87. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/introspection/health/context.py +0 -0
  88. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/introspection/health/helpers.py +0 -0
  89. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/introspection/health/ownership.py +0 -0
  90. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/introspection/health/runner.py +0 -0
  91. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/introspection/health/types.py +0 -0
  92. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/introspection/schema.py +0 -0
  93. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/lookups.py +0 -0
  94. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/meta.py +0 -0
  95. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/middleware.py +0 -0
  96. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/__init__.py +0 -0
  97. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/autodetector.py +0 -0
  98. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/exceptions.py +0 -0
  99. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/executor.py +0 -0
  100. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/graph.py +0 -0
  101. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/loader.py +0 -0
  102. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/migration.py +0 -0
  103. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/operations/__init__.py +0 -0
  104. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/operations/base.py +0 -0
  105. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/operations/fields.py +0 -0
  106. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/operations/models.py +0 -0
  107. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/operations/special.py +0 -0
  108. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/optimizer.py +0 -0
  109. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/questioner.py +0 -0
  110. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/recorder.py +0 -0
  111. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/serializer.py +0 -0
  112. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/state.py +0 -0
  113. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/utils.py +0 -0
  114. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/writer.py +0 -0
  115. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/options.py +0 -0
  116. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/otel.py +0 -0
  117. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/query.py +0 -0
  118. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/query_utils.py +0 -0
  119. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/registry.py +0 -0
  120. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/schema.py +0 -0
  121. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/sql/__init__.py +0 -0
  122. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/sql/compiler.py +0 -0
  123. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/sql/constants.py +0 -0
  124. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/sql/datastructures.py +0 -0
  125. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/sql/query.py +0 -0
  126. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/sql/where.py +0 -0
  127. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/test/__init__.py +0 -0
  128. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/test/database.py +0 -0
  129. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/test/pytest.py +0 -0
  130. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/transaction.py +0 -0
  131. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/types.py +0 -0
  132. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/types.pyi +0 -0
  133. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/utils.py +0 -0
  134. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/forms.py +0 -0
  135. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0001_initial.py +0 -0
  136. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
  137. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
  138. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
  139. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
  140. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0006_secretstore.py +0 -0
  141. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +0 -0
  142. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0008_setsentinelparent_diamondparenta_midparent_and_more.py +0 -0
  143. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0009_circb_circa_circb_partner.py +0 -0
  144. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0010_hideableitem.py +0 -0
  145. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0011_defaultsexample.py +0 -0
  146. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0012_iterationexample.py +0 -0
  147. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0013_indexexample_constraintexample_nullabilityexample.py +0 -0
  148. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0014_widget_rename_feature_tag_remove_carfeature_car_and_more.py +0 -0
  149. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0015_dbdefaultsexample.py +0 -0
  150. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0016_formsexample.py +0 -0
  151. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0017_random_string_token.py +0 -0
  152. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0018_storageparametersexample.py +0 -0
  153. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/__init__.py +0 -0
  154. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/models/__init__.py +0 -0
  155. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/models/constraints.py +0 -0
  156. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/models/defaults.py +0 -0
  157. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/models/delete.py +0 -0
  158. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/models/encrypted.py +0 -0
  159. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/models/forms.py +0 -0
  160. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/models/indexes.py +0 -0
  161. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/models/iteration.py +0 -0
  162. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/models/mixins.py +0 -0
  163. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/models/nullability.py +0 -0
  164. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/models/querysets.py +0 -0
  165. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/models/relationships.py +0 -0
  166. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/models/storage_parameters.py +0 -0
  167. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/models/trees.py +0 -0
  168. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/models/unregistered.py +0 -0
  169. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/conftest.py +0 -0
  170. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/conftest_convergence.py +0 -0
  171. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_autodetector_not_null_errors.py +0 -0
  172. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_autodetector_type_change.py +0 -0
  173. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_connection_isolation.py +0 -0
  174. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_connection_pool.py +0 -0
  175. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_constraint_violation_error.py +0 -0
  176. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_convergence.py +0 -0
  177. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_convergence_constraints.py +0 -0
  178. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_convergence_defaults.py +0 -0
  179. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_convergence_fk.py +0 -0
  180. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_convergence_indexes.py +0 -0
  181. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_convergence_nullability.py +0 -0
  182. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_convergence_storage_parameters.py +0 -0
  183. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_convergence_timeouts.py +0 -0
  184. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_db_expression_defaults.py +0 -0
  185. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_diagnose.py +0 -0
  186. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_executor_connection_hook.py +0 -0
  187. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_health.py +0 -0
  188. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_introspection.py +0 -0
  189. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_literal_default_persistence.py +0 -0
  190. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_management_connection.py +0 -0
  191. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_migration_executor.py +0 -0
  192. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_no_callable_defaults.py +0 -0
  193. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_otel_metrics.py +0 -0
  194. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_preflight_fk_coverage.py +0 -0
  195. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_schema_normalize_type.py +0 -0
  196. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_schema_timeouts.py +0 -0
  197. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_database_url.py +0 -0
  198. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_delete_behaviors.py +0 -0
  199. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_encrypted_fields.py +0 -0
  200. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_exceptions.py +0 -0
  201. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_field_defaults.py +0 -0
  202. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_functions_uuid.py +0 -0
  203. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_iterator.py +0 -0
  204. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_m2m.py +0 -0
  205. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_manager_assignment.py +0 -0
  206. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_mixins.py +0 -0
  207. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_queryset_repr.py +0 -0
  208. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_random_string_field.py +0 -0
  209. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_raw_query.py +0 -0
  210. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_read_only_transactions.py +0 -0
  211. {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_related.py +0 -0
@@ -3,6 +3,7 @@
3
3
  *.egg-info
4
4
  *.py[co]
5
5
  __pycache__
6
+ .pytest_cache
6
7
  *.DS_Store
7
8
  *.swp
8
9
  *.swo
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.postgres
3
- Version: 0.103.3
3
+ Version: 0.103.5
4
4
  Summary: Model your data and store it in a database.
5
5
  Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
6
  License-Expression: BSD-3-Clause
@@ -1,5 +1,26 @@
1
1
  # plain-postgres changelog
2
2
 
3
+ ## [0.103.5](https://github.com/dropseed/plain/releases/plain-postgres@0.103.5) (2026-05-19)
4
+
5
+ ### What's changed
6
+
7
+ - **Pooled connections are now validated on checkout.** The connection pool runs `check_connection` on each `getconn()`, so a connection closed server-side while idle in the pool (a server or pooler idle timeout) is discarded and replaced rather than handed out dead. This closes a class of `OperationalError: the connection is closed` failures on the first query of a request after an idle period. ([31ad84f423](https://github.com/dropseed/plain/commit/31ad84f423))
8
+ - Standardized `__all__` in `forms.py` to a list for consistency with the rest of the codebase. ([64ee8a4de0](https://github.com/dropseed/plain/commit/64ee8a4de0))
9
+
10
+ ### Upgrade instructions
11
+
12
+ - No changes required.
13
+
14
+ ## [0.103.4](https://github.com/dropseed/plain/releases/plain-postgres@0.103.4) (2026-05-12)
15
+
16
+ ### What's changed
17
+
18
+ - **`postgres.duplicate_indexes` preflight check now skips partial indexes** (those with a `condition=`). Previously a bare `Index(fields=[fk])` carried for FK coverage was flagged as redundant with a partial composite `Index(fields=[fk, ...], condition=Q(...))`, contradicting the `postgres.missing_fk_index` check. The two warnings are now mutually consistent — partials don't cover full-column lookups, so they can't shadow a single-column index. ([1e8a3f72db](https://github.com/dropseed/plain/commit/1e8a3f72db))
19
+
20
+ ### Upgrade instructions
21
+
22
+ - No changes required. Apps that were silencing `postgres.duplicate_indexes` to work around the false positive can drop the silence.
23
+
3
24
  ## [0.103.3](https://github.com/dropseed/plain/releases/plain-postgres@0.103.3) (2026-05-08)
4
25
 
5
26
  ### What's changed
@@ -23,14 +23,14 @@ from plain.postgres.fields.base import ColumnField, DefaultableField
23
23
  if TYPE_CHECKING:
24
24
  from plain.postgres.fields import Field as ModelField
25
25
 
26
- __all__ = (
26
+ __all__ = [
27
27
  "ModelForm",
28
28
  "BaseModelForm",
29
29
  "model_to_dict",
30
30
  "fields_for_model",
31
31
  "ModelChoiceField",
32
32
  "ModelMultipleChoiceField",
33
- )
33
+ ]
34
34
 
35
35
 
36
36
  def construct_instance(
@@ -27,16 +27,23 @@ def _get_app_models() -> list[Any]:
27
27
 
28
28
 
29
29
  def _collect_model_indexes(model: Any) -> list[tuple[str, list[str], bool]]:
30
- """Collect all index field-lists for a model as (name, fields, is_unique) tuples."""
30
+ """Collect (name, fields, is_unique) for non-partial indexes/constraints.
31
+
32
+ Partials are skipped for the same reason as in ``_fk_covered_field_names``.
33
+ """
31
34
  all_indexes: list[tuple[str, list[str], bool]] = []
32
35
 
33
36
  for index in model.model_options.indexes:
34
- if index.fields:
37
+ if index.fields and not index.is_partial:
35
38
  fields = [f.lstrip("-") for f in index.fields]
36
39
  all_indexes.append((index.name, fields, False))
37
40
 
38
41
  for constraint in model.model_options.constraints:
39
- if isinstance(constraint, UniqueConstraint) and constraint.fields:
42
+ if (
43
+ isinstance(constraint, UniqueConstraint)
44
+ and constraint.fields
45
+ and not constraint.is_partial
46
+ ):
40
47
  all_indexes.append((constraint.name, list(constraint.fields), True))
41
48
 
42
49
  return all_indexes
@@ -175,6 +175,10 @@ class PoolSource(ConnectionSource):
175
175
  kwargs=params,
176
176
  open=False,
177
177
  reset=_reset_pooled_connection,
178
+ # Validate each connection on checkout. One closed server-side
179
+ # while idle in the pool (server or pooler idle timeout) is
180
+ # discarded and replaced rather than handed out dead.
181
+ check=ConnectionPool.check_connection,
178
182
  min_size=plain_settings.POSTGRES_POOL_MIN_SIZE,
179
183
  max_size=plain_settings.POSTGRES_POOL_MAX_SIZE,
180
184
  max_lifetime=plain_settings.POSTGRES_POOL_MAX_LIFETIME,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "plain.postgres"
3
- version = "0.103.3"
3
+ version = "0.103.5"
4
4
  description = "Model your data and store it in a database."
5
5
  authors = [{ name = "Dave Gaeddert", email = "dave.gaeddert@dropseed.dev" }]
6
6
  readme = "README.md"
@@ -16,7 +16,7 @@ dependencies = ["plain>=0.134.0,<1.0.0", "psycopg>=3.2", "psycopg-pool>=3.2"]
16
16
  "plain.postgres" = "plain.postgres.test.pytest"
17
17
 
18
18
  [dependency-groups]
19
- dev = ["plain.pytest<1.0.0", "opentelemetry-sdk>=1.34.1"]
19
+ dev = ["plain.pytest<1.0.0", "plain.templates>=0.1.0,<1.0.0", "opentelemetry-sdk>=1.34.1"]
20
20
 
21
21
  [tool.hatch.build.targets.wheel]
22
22
  packages = ["plain"]
@@ -9,27 +9,27 @@ class ExamplesRouter(Router):
9
9
  namespace = "examples"
10
10
  urls = [
11
11
  path(
12
- "forms/create/",
12
+ "forms/create",
13
13
  views.FormsExampleCreateView,
14
14
  name="forms_create",
15
15
  ),
16
16
  path(
17
- "forms/<int:pk>/update/",
17
+ "forms/<int:pk>/update",
18
18
  views.FormsExampleUpdateView,
19
19
  name="forms_update",
20
20
  ),
21
21
  path(
22
- "child-cascade/create/",
22
+ "child-cascade/create",
23
23
  views.ChildCascadeCreateView,
24
24
  name="child_cascade_create",
25
25
  ),
26
26
  path(
27
- "db-defaults/create/",
27
+ "db-defaults/create",
28
28
  views.DBDefaultsExampleCreateView,
29
29
  name="db_defaults_create",
30
30
  ),
31
31
  path(
32
- "secret-store/create/",
32
+ "secret-store/create",
33
33
  views.SecretStoreCreateView,
34
34
  name="secret_store_create",
35
35
  ),
@@ -3,9 +3,8 @@ from __future__ import annotations
3
3
  import json
4
4
  from typing import Any
5
5
 
6
- from plain.forms import BaseForm
7
6
  from plain.http import Response
8
- from plain.views import CreateView, UpdateView
7
+ from plain.templates.views import CreateView, UpdateView
9
8
 
10
9
  from .forms import (
11
10
  ChildCascadeForm,
@@ -20,14 +19,16 @@ class _NoTemplateFormView:
20
19
  """Bypass template requirements in test views.
21
20
 
22
21
  CreateView/UpdateView normally need a template for GET and for
23
- form-invalid responses. These tests only exercise POST behavior, so
24
- we return plain Responses instead of rendering templates.
22
+ rendering an invalid form. These tests only exercise POST behavior,
23
+ so we return plain Responses instead of rendering templates.
25
24
  """
26
25
 
27
26
  def get(self) -> Response:
28
27
  return Response("ok")
29
28
 
30
- def form_invalid(self, form: BaseForm) -> Response:
29
+ def render(self, **context: Any) -> Response:
30
+ """Invalid-form re-render: serialize the form errors as JSON."""
31
+ form = context["form"]
31
32
  errors: dict[str, list[str]] = {
32
33
  name: [str(err) for err in errs] for name, errs in form.errors.items()
33
34
  }
@@ -1,6 +1,7 @@
1
1
  SECRET_KEY = "test"
2
2
  URLS_ROUTER = "app.urls.AppRouter"
3
3
  INSTALLED_PACKAGES = [
4
+ "plain.templates",
4
5
  "plain.postgres",
5
6
  "app.examples",
6
7
  ]
@@ -17,7 +17,7 @@ class LogoutView(View):
17
17
  class AppRouter(Router):
18
18
  namespace = ""
19
19
  urls = [
20
- include("examples/", ExamplesRouter),
21
- path("login/", LoginView, name="login"),
22
- path("logout/", LogoutView, name="logout"),
20
+ include("examples", ExamplesRouter),
21
+ path("login", LoginView, name="login"),
22
+ path("logout", LogoutView, name="logout"),
23
23
  ]
@@ -92,10 +92,10 @@ class StreamingDBQueryView(View):
92
92
  class TestRouter(Router):
93
93
  namespace = ""
94
94
  urls = [
95
- path("db-query/", DBQueryView, name="db_query"),
96
- path("async-db-query/", AsyncDBQueryView, name="async_db_query"),
97
- path("sse-db-query/", DBQuerySSEView, name="sse_db_query"),
98
- path("streaming-db-query/", StreamingDBQueryView, name="streaming_db_query"),
95
+ path("db-query", DBQueryView, name="db_query"),
96
+ path("async-db-query", AsyncDBQueryView, name="async_db_query"),
97
+ path("sse-db-query", DBQuerySSEView, name="sse_db_query"),
98
+ path("streaming-db-query", StreamingDBQueryView, name="streaming_db_query"),
99
99
  ]
100
100
 
101
101
 
@@ -192,7 +192,7 @@ class TestConnectionLifecycle:
192
192
 
193
193
  with _patched_init_counter() as count:
194
194
  client = _fresh_client()
195
- response = client.get("/db-query/")
195
+ response = client.get("/db-query")
196
196
 
197
197
  assert response.status_code == 200
198
198
  assert response.content == b"1"
@@ -205,14 +205,14 @@ class TestConnectionLifecycle:
205
205
  with _patched_init_counter() as count:
206
206
  client = _fresh_client()
207
207
 
208
- response1 = client.get("/db-query/")
208
+ response1 = client.get("/db-query")
209
209
  assert response1.status_code == 200
210
210
  first_conn_id = id(_db_conn.get())
211
211
 
212
- response2 = client.get("/db-query/")
212
+ response2 = client.get("/db-query")
213
213
  assert response2.status_code == 200
214
214
 
215
- response3 = client.get("/db-query/")
215
+ response3 = client.get("/db-query")
216
216
  assert response3.status_code == 200
217
217
 
218
218
  assert count[0] == 1, f"Expected 1 connection for 3 requests, got {count[0]}"
@@ -235,7 +235,7 @@ class TestConnectionLifecycle:
235
235
  with _patched_init_counter() as count:
236
236
  client = _fresh_client()
237
237
 
238
- response1 = client.get("/db-query/")
238
+ response1 = client.get("/db-query")
239
239
  assert response1.status_code == 200
240
240
 
241
241
  # Wrapper persists; inner connection was returned to the pool.
@@ -246,7 +246,7 @@ class TestConnectionLifecycle:
246
246
  )
247
247
 
248
248
  # Second request: same wrapper, checks out a connection, returns it.
249
- response2 = client.get("/db-query/")
249
+ response2 = client.get("/db-query")
250
250
  assert response2.status_code == 200
251
251
  assert conn is _db_conn.get()
252
252
 
@@ -265,7 +265,7 @@ class TestConnectionLifecycle:
265
265
  ] + original
266
266
  try:
267
267
  client = _fresh_client()
268
- response = client.get("/db-query/")
268
+ response = client.get("/db-query")
269
269
  assert response.status_code == 200
270
270
 
271
271
  assert len(_tracking_seen) == 1
@@ -286,7 +286,7 @@ class TestAsyncViewConnectionLifecycle:
286
286
  """
287
287
  with _patched_init_counter() as count:
288
288
  client = _fresh_client()
289
- response = client.get("/async-db-query/")
289
+ response = client.get("/async-db-query")
290
290
 
291
291
  assert response.status_code == 200
292
292
  assert response.content == b"1"
@@ -302,7 +302,7 @@ class TestAsyncViewConnectionLifecycle:
302
302
  """
303
303
  with _patched_init_counter() as count:
304
304
  client = _fresh_client()
305
- response = client.get("/sse-db-query/")
305
+ response = client.get("/sse-db-query")
306
306
 
307
307
  assert response.status_code == 200
308
308
  assert "text/event-stream" in response.headers["Content-Type"]
@@ -349,7 +349,7 @@ class TestStreamingResponseCleanup:
349
349
  ):
350
350
  handler = BaseHandler()
351
351
  handler.load_middleware()
352
- request = RequestFactory().get("/streaming-db-query/")
352
+ request = RequestFactory().get("/streaming-db-query")
353
353
 
354
354
  async def run() -> Response:
355
355
  with concurrent.futures.ThreadPoolExecutor(
@@ -0,0 +1,102 @@
1
+ """Unit tests for `_collect_model_indexes` — the helper that feeds
2
+ `postgres.duplicate_indexes`. Partial indexes/constraints must be
3
+ excluded so the check doesn't contradict `postgres.missing_fk_indexes`,
4
+ which already treats partials as non-covering.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from types import SimpleNamespace
10
+
11
+ from plain.postgres import Q
12
+ from plain.postgres.constraints import UniqueConstraint
13
+ from plain.postgres.indexes import Index
14
+ from plain.postgres.preflight import _collect_model_indexes
15
+
16
+
17
+ def _model(*, indexes=(), constraints=()) -> SimpleNamespace:
18
+ """Minimal model_options stand-in for the helper."""
19
+ return SimpleNamespace(
20
+ model_options=SimpleNamespace(
21
+ indexes=list(indexes), constraints=list(constraints)
22
+ )
23
+ )
24
+
25
+
26
+ def _names(collected) -> set[str]:
27
+ return {name for name, _fields, _unique in collected}
28
+
29
+
30
+ def test_non_partial_index_collected():
31
+ model = _model(indexes=[Index(name="t_team_idx", fields=["team"])])
32
+ assert _names(_collect_model_indexes(model)) == {"t_team_idx"}
33
+
34
+
35
+ def test_non_partial_unique_constraint_collected():
36
+ model = _model(constraints=[UniqueConstraint(fields=["team"], name="t_team_uniq")])
37
+ assert _names(_collect_model_indexes(model)) == {"t_team_uniq"}
38
+
39
+
40
+ def test_partial_index_excluded():
41
+ model = _model(
42
+ indexes=[
43
+ Index(
44
+ name="t_team_open_idx",
45
+ fields=["team", "created_at"],
46
+ condition=Q(resolved_at__isnull=True),
47
+ )
48
+ ]
49
+ )
50
+ assert _collect_model_indexes(model) == []
51
+
52
+
53
+ def test_partial_unique_constraint_excluded():
54
+ model = _model(
55
+ constraints=[
56
+ UniqueConstraint(
57
+ fields=["team"],
58
+ name="t_team_active_uniq",
59
+ condition=Q(deleted_at__isnull=True),
60
+ )
61
+ ]
62
+ )
63
+ assert _collect_model_indexes(model) == []
64
+
65
+
66
+ def test_bare_index_not_flagged_against_partial_prefix():
67
+ """A bare `Index(fields=[fk])` carried for FK coverage must survive
68
+ alongside a partial composite `Index(fields=[fk, other], condition=...)`
69
+ — otherwise the duplicate check fights the missing-FK check."""
70
+ model = _model(
71
+ indexes=[
72
+ Index(name="t_team_idx", fields=["team"]),
73
+ Index(
74
+ name="t_team_open_idx",
75
+ fields=["team", "created_at"],
76
+ condition=Q(resolved_at__isnull=True),
77
+ ),
78
+ ]
79
+ )
80
+ assert _names(_collect_model_indexes(model)) == {"t_team_idx"}
81
+
82
+
83
+ def test_mixed_keeps_only_non_partial():
84
+ model = _model(
85
+ indexes=[
86
+ Index(name="t_a_idx", fields=["a"]),
87
+ Index(
88
+ name="t_a_partial_idx",
89
+ fields=["a"],
90
+ condition=Q(deleted_at__isnull=True),
91
+ ),
92
+ ],
93
+ constraints=[
94
+ UniqueConstraint(fields=["b"], name="t_b_uniq"),
95
+ UniqueConstraint(
96
+ fields=["b"],
97
+ name="t_b_active_uniq",
98
+ condition=Q(deleted_at__isnull=True),
99
+ ),
100
+ ],
101
+ )
102
+ assert _names(_collect_model_indexes(model)) == {"t_a_idx", "t_b_uniq"}
@@ -42,7 +42,7 @@ def _valid_post_data() -> dict[str, str]:
42
42
  class TestFormsExampleCreate:
43
43
  def test_create_roundtrip_all_field_types(self, db):
44
44
  client = Client()
45
- response = client.post("/examples/forms/create/", data=_valid_post_data())
45
+ response = client.post("/examples/forms/create", data=_valid_post_data())
46
46
 
47
47
  assert response.status_code == 302, response.content
48
48
  assert response.headers["Location"] == "/ok/"
@@ -69,7 +69,7 @@ class TestFormsExampleCreate:
69
69
  client = Client()
70
70
  data = _valid_post_data()
71
71
  del data["is_active"] # unchecked checkboxes aren't posted
72
- response = client.post("/examples/forms/create/", data=data)
72
+ response = client.post("/examples/forms/create", data=data)
73
73
 
74
74
  assert response.status_code == 302, response.content
75
75
  assert FormsExample.query.get().is_active is False
@@ -78,7 +78,7 @@ class TestFormsExampleCreate:
78
78
  client = Client()
79
79
  data = _valid_post_data()
80
80
  data["count"] = "not-an-int"
81
- response = client.post("/examples/forms/create/", data=data)
81
+ response = client.post("/examples/forms/create", data=data)
82
82
 
83
83
  assert response.status_code == 400
84
84
  errors = json.loads(response.content)
@@ -88,7 +88,7 @@ class TestFormsExampleCreate:
88
88
  client = Client()
89
89
  data = _valid_post_data()
90
90
  data["status"] = "archived" # not in the declared choices
91
- response = client.post("/examples/forms/create/", data=data)
91
+ response = client.post("/examples/forms/create", data=data)
92
92
 
93
93
  assert response.status_code == 400
94
94
  errors = json.loads(response.content)
@@ -98,7 +98,7 @@ class TestFormsExampleCreate:
98
98
  client = Client()
99
99
  data = _valid_post_data()
100
100
  data["external_id"] = "not-a-uuid"
101
- response = client.post("/examples/forms/create/", data=data)
101
+ response = client.post("/examples/forms/create", data=data)
102
102
 
103
103
  assert response.status_code == 400
104
104
  errors = json.loads(response.content)
@@ -108,18 +108,18 @@ class TestFormsExampleCreate:
108
108
  client = Client()
109
109
  data = _valid_post_data()
110
110
  data["event_date"] = "not-a-date"
111
- response = client.post("/examples/forms/create/", data=data)
111
+ response = client.post("/examples/forms/create", data=data)
112
112
 
113
113
  assert response.status_code == 400
114
114
  errors = json.loads(response.content)
115
115
  assert "event_date" in errors
116
116
 
117
117
  def test_blank_required_field_returns_400_with_error(self, db):
118
- """Required field sent as empty string → form_invalid path with errors."""
118
+ """Required field sent as empty string → invalid-form path with errors."""
119
119
  client = Client()
120
120
  data = _valid_post_data()
121
121
  data["name"] = ""
122
- response = client.post("/examples/forms/create/", data=data)
122
+ response = client.post("/examples/forms/create", data=data)
123
123
 
124
124
  assert response.status_code == 400
125
125
  errors = json.loads(response.content)
@@ -130,7 +130,7 @@ class TestFormsExampleCreate:
130
130
  client = Client()
131
131
  data = _valid_post_data()
132
132
  del data["name"]
133
- response = client.post("/examples/forms/create/", data=data)
133
+ response = client.post("/examples/forms/create", data=data)
134
134
 
135
135
  assert response.status_code == 400
136
136
 
@@ -157,7 +157,7 @@ class TestFormsExampleUpdate:
157
157
  data["name"] = "After"
158
158
  data["count"] = "100"
159
159
 
160
- response = client.post(f"/examples/forms/{existing.id}/update/", data=data)
160
+ response = client.post(f"/examples/forms/{existing.id}/update", data=data)
161
161
 
162
162
  assert response.status_code == 302, response.content
163
163
 
@@ -173,7 +173,7 @@ class TestFormsExampleUpdate:
173
173
  data = _valid_post_data()
174
174
  data["count"] = "not-an-int"
175
175
 
176
- response = client.post(f"/examples/forms/{existing.id}/update/", data=data)
176
+ response = client.post(f"/examples/forms/{existing.id}/update", data=data)
177
177
 
178
178
  assert response.status_code == 400
179
179
  existing.refresh_from_db()
@@ -188,7 +188,7 @@ class TestForeignKeyRoundTrip:
188
188
  parent = DeleteParent.query.create(name="parent-1")
189
189
  client = Client()
190
190
  response = client.post(
191
- "/examples/child-cascade/create/", data={"parent": str(parent.id)}
191
+ "/examples/child-cascade/create", data={"parent": str(parent.id)}
192
192
  )
193
193
 
194
194
  assert response.status_code == 302, response.content
@@ -198,7 +198,7 @@ class TestForeignKeyRoundTrip:
198
198
  def test_create_with_nonexistent_fk_returns_400(self, db):
199
199
  client = Client()
200
200
  response = client.post(
201
- "/examples/child-cascade/create/", data={"parent": "999999"}
201
+ "/examples/child-cascade/create", data={"parent": "999999"}
202
202
  )
203
203
 
204
204
  assert response.status_code == 400
@@ -207,7 +207,7 @@ class TestForeignKeyRoundTrip:
207
207
 
208
208
  def test_create_with_blank_required_fk_returns_400(self, db):
209
209
  client = Client()
210
- response = client.post("/examples/child-cascade/create/", data={"parent": ""})
210
+ response = client.post("/examples/child-cascade/create", data={"parent": ""})
211
211
 
212
212
  assert response.status_code == 400
213
213
  errors = json.loads(response.content)
@@ -222,7 +222,7 @@ class TestDBExpressionDefaultsRoundTrip:
222
222
  def test_blank_db_default_fields_are_filled_by_database(self, db):
223
223
  client = Client()
224
224
  response = client.post(
225
- "/examples/db-defaults/create/",
225
+ "/examples/db-defaults/create",
226
226
  data={"name": "sample", "db_uuid": "", "created_at": ""},
227
227
  )
228
228
 
@@ -236,7 +236,7 @@ class TestDBExpressionDefaultsRoundTrip:
236
236
  supplied = "11111111-1111-1111-1111-111111111111"
237
237
  client = Client()
238
238
  response = client.post(
239
- "/examples/db-defaults/create/",
239
+ "/examples/db-defaults/create",
240
240
  data={
241
241
  "name": "sample",
242
242
  "db_uuid": supplied,
@@ -259,7 +259,7 @@ class TestEncryptedFieldsRoundTrip:
259
259
  def test_create_roundtrip_with_encrypted_text(self, db):
260
260
  client = Client()
261
261
  response = client.post(
262
- "/examples/secret-store/create/",
262
+ "/examples/secret-store/create",
263
263
  data={
264
264
  "name": "prod-key",
265
265
  "api_key": "sk-live-abc123",
@@ -278,7 +278,7 @@ class TestEncryptedFieldsRoundTrip:
278
278
  def test_blank_optional_encrypted_text_accepted(self, db):
279
279
  client = Client()
280
280
  response = client.post(
281
- "/examples/secret-store/create/",
281
+ "/examples/secret-store/create",
282
282
  data={
283
283
  "name": "minimal",
284
284
  "api_key": "sk-test",