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.
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/.gitignore +1 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/PKG-INFO +1 -1
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/CHANGELOG.md +21 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/forms.py +2 -2
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/preflight.py +10 -3
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/sources.py +4 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/pyproject.toml +2 -2
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/urls.py +5 -5
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/views.py +6 -5
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/settings.py +1 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/urls.py +3 -3
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_connection_lifecycle.py +14 -14
- plain_postgres-0.103.5/tests/internal/test_preflight_duplicate_indexes.py +102 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_modelform_roundtrip.py +18 -18
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/CLAUDE.md +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/LICENSE +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/README.md +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/README.md +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/__init__.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/adapters.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/agents/.claude/rules/plain-postgres.md +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/aggregates.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/base.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/cli/__init__.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/cli/converge.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/cli/core.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/cli/decorators.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/cli/diagnose.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/cli/migrations.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/cli/schema.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/cli/sync.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/config.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/connection.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/constants.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/constraints.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/convergence/__init__.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/convergence/analysis.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/convergence/fixes.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/convergence/planning.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/database_url.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/db.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/ddl.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/default_settings.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/deletion.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/dialect.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/entrypoints.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/enums.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/exceptions.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/expressions.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/__init__.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/base.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/binary.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/boolean.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/duration.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/encrypted.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/json.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/mixins.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/network.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/numeric.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/primary_key.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/related.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/related_descriptors.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/related_lookups.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/related_managers.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/reverse_descriptors.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/reverse_related.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/temporal.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/text.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/timezones.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/fields/uuid.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/functions/__init__.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/functions/comparison.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/functions/datetime.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/functions/math.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/functions/mixins.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/functions/random.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/functions/text.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/functions/uuid.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/functions/window.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/indexes.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/introspection/__init__.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/introspection/health/__init__.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/introspection/health/checks_cumulative.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/introspection/health/checks_snapshot.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/introspection/health/checks_structural.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/introspection/health/context.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/introspection/health/helpers.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/introspection/health/ownership.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/introspection/health/runner.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/introspection/health/types.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/introspection/schema.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/lookups.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/meta.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/middleware.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/__init__.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/autodetector.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/exceptions.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/executor.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/graph.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/loader.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/migration.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/operations/__init__.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/operations/base.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/operations/fields.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/operations/models.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/operations/special.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/optimizer.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/questioner.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/recorder.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/serializer.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/state.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/utils.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/migrations/writer.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/options.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/otel.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/query.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/query_utils.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/registry.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/schema.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/sql/__init__.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/sql/compiler.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/sql/constants.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/sql/datastructures.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/sql/query.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/sql/where.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/test/__init__.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/test/database.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/test/pytest.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/transaction.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/types.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/types.pyi +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/plain/postgres/utils.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/forms.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0001_initial.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0006_secretstore.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0008_setsentinelparent_diamondparenta_midparent_and_more.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0009_circb_circa_circb_partner.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0010_hideableitem.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0011_defaultsexample.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0012_iterationexample.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0013_indexexample_constraintexample_nullabilityexample.py +0 -0
- {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
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0015_dbdefaultsexample.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0016_formsexample.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0017_random_string_token.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/0018_storageparametersexample.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/migrations/__init__.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/models/__init__.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/models/constraints.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/models/defaults.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/models/delete.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/models/encrypted.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/models/forms.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/models/indexes.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/models/iteration.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/models/mixins.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/models/nullability.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/models/querysets.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/models/relationships.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/models/storage_parameters.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/models/trees.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/app/examples/models/unregistered.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/conftest.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/conftest_convergence.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_autodetector_not_null_errors.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_autodetector_type_change.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_connection_isolation.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_connection_pool.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_constraint_violation_error.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_convergence.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_convergence_constraints.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_convergence_defaults.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_convergence_fk.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_convergence_indexes.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_convergence_nullability.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_convergence_storage_parameters.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_convergence_timeouts.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_db_expression_defaults.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_diagnose.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_executor_connection_hook.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_health.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_introspection.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_literal_default_persistence.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_management_connection.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_migration_executor.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_no_callable_defaults.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_otel_metrics.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_preflight_fk_coverage.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_schema_normalize_type.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_schema_timeouts.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_database_url.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_delete_behaviors.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_encrypted_fields.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_exceptions.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_field_defaults.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_functions_uuid.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_iterator.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_m2m.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_manager_assignment.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_mixins.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_queryset_repr.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_random_string_field.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_raw_query.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_read_only_transactions.py +0 -0
- {plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/public/test_related.py +0 -0
|
@@ -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
|
|
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
|
|
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
|
+
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
|
-
|
|
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
|
|
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
|
}
|
|
@@ -17,7 +17,7 @@ class LogoutView(View):
|
|
|
17
17
|
class AppRouter(Router):
|
|
18
18
|
namespace = ""
|
|
19
19
|
urls = [
|
|
20
|
-
include("examples
|
|
21
|
-
path("login
|
|
22
|
-
path("logout
|
|
20
|
+
include("examples", ExamplesRouter),
|
|
21
|
+
path("login", LoginView, name="login"),
|
|
22
|
+
path("logout", LogoutView, name="logout"),
|
|
23
23
|
]
|
{plain_postgres-0.103.3 → plain_postgres-0.103.5}/tests/internal/test_connection_lifecycle.py
RENAMED
|
@@ -92,10 +92,10 @@ class StreamingDBQueryView(View):
|
|
|
92
92
|
class TestRouter(Router):
|
|
93
93
|
namespace = ""
|
|
94
94
|
urls = [
|
|
95
|
-
path("db-query
|
|
96
|
-
path("async-db-query
|
|
97
|
-
path("sse-db-query
|
|
98
|
-
path("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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 →
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|