sarj-python-lint 1.109.0__tar.gz → 1.110.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 (325) hide show
  1. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/__main__.py +1 -0
  4. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/_rule_descriptors.py +28 -1
  5. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_registry.py +6 -0
  6. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_store_sql.py +109 -4
  7. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_custom_control_flow_exception.py +50 -13
  8. sarj_python_lint-1.110.0/src/sarj_python_lint/rules/no_psycopg_execution_in_loop.py +121 -0
  9. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +19 -2
  10. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +95 -0
  11. sarj_python_lint-1.110.0/src/sarj_python_lint/rules/prefer_single_psycopg_cursor.py +118 -0
  12. sarj_python_lint-1.110.0/src/sarj_python_lint/rules/prefer_validated_uuid_comparison.py +97 -0
  13. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +86 -0
  14. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/.gitignore +0 -0
  15. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/LICENSE +0 -0
  16. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/README.md +0 -0
  17. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/__init__.py +0 -0
  18. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  19. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/_autofix.py +0 -0
  20. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/_autofix_aliases.py +0 -0
  21. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/_autofix_annotations.py +0 -0
  22. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/_autofix_comments.py +0 -0
  23. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/_autofix_declared_contracts.py +0 -0
  24. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/_autofix_declared_tests.py +0 -0
  25. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/_autofix_docs.py +0 -0
  26. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/_autofix_enums.py +0 -0
  27. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/_autofix_exhaustiveness.py +0 -0
  28. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/_autofix_inert.py +0 -0
  29. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/_autofix_parameters.py +0 -0
  30. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/_autofix_repeated.py +0 -0
  31. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/_autofix_security.py +0 -0
  32. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/_autofix_sql_projection.py +0 -0
  33. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/_autofix_tests.py +0 -0
  34. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/_closed_sql_literal.py +0 -0
  35. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/_file_context.py +0 -0
  36. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/_filesystem.py +0 -0
  37. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/_fixes.py +0 -0
  38. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/_parallel.py +0 -0
  39. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/_prepared_sources.py +0 -0
  40. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/_python_target.py +0 -0
  41. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  42. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/_secret_names.py +0 -0
  43. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/_source.py +0 -0
  44. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/_version.py +0 -0
  45. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/application_logging.py +0 -0
  46. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/autofix.py +0 -0
  47. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/benchmark.py +0 -0
  48. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/cast_access.py +0 -0
  49. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  50. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/json_boundary.py +0 -0
  51. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/py.typed +0 -0
  52. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/ratchet.py +0 -0
  53. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rule_base.py +0 -0
  54. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rule_metadata.py +0 -0
  55. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  56. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_annotation_origins.py +0 -0
  57. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  58. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  59. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_ast_names.py +0 -0
  60. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  61. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_async_lifecycle.py +0 -0
  62. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_binding_inventory.py +0 -0
  63. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  64. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_comment_narration.py +0 -0
  65. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  66. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  67. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  68. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
  69. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_declared_contracts.py +0 -0
  70. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  71. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  72. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  73. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  74. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  75. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  76. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_fixture_contracts.py +0 -0
  77. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  78. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  79. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  80. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_local_alias_fix.py +0 -0
  81. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  82. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  83. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_loguru.py +0 -0
  84. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
  85. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  86. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  87. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_native_object_contracts.py +0 -0
  88. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  89. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_numeric_proof.py +0 -0
  90. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_opaque_contracts.py +0 -0
  91. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  92. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_postgres.py +0 -0
  93. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_postgres_ctas.py +0 -0
  94. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  95. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  96. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  97. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  98. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_pytest_contract_parameters.py +0 -0
  99. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  100. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  101. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
  102. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  103. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_runtime_names.py +0 -0
  104. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_runtime_registry.py +0 -0
  105. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_service_http_callbacks.py +0 -0
  106. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  107. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  108. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_sql_aggregate_identity.py +0 -0
  109. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_sql_explain.py +0 -0
  110. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_sql_policy_source.py +0 -0
  111. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  112. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  113. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  114. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  115. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  116. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  117. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_transactional_sql.py +0 -0
  118. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  119. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  120. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/async_executor_shutdown.py +0 -0
  121. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  122. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/async_subprocess_cancellation_cleanup.py +0 -0
  123. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/cancel_owned_task_without_join.py +0 -0
  124. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  125. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  126. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
  127. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  128. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  129. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  130. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  131. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py +0 -0
  132. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  133. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/failed_probe_zero_threshold.py +0 -0
  134. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  135. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  136. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  137. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  138. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  139. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  140. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  141. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/nan_hidden_by_builtin_min.py +0 -0
  142. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  143. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  144. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  145. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_async_client_in_constructor.py +0 -0
  146. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  147. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  148. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
  149. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  150. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  151. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  152. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  153. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  154. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  155. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  156. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +0 -0
  157. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  158. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
  159. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  160. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_discarded_tagged_outcome.py +0 -0
  161. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
  162. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +0 -0
  163. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  164. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  165. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
  166. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_dynamic_builtins.py +0 -0
  167. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +0 -0
  168. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  169. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  170. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  171. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  172. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  173. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  174. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +0 -0
  175. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  176. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_generic_json_parser.py +0 -0
  177. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  178. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
  179. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  180. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  181. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  182. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  183. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  184. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  185. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  186. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  187. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  188. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  189. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  190. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
  191. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_mutating_input_with_result.py +0 -0
  192. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  193. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  194. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  195. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  196. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  197. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  198. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  199. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  200. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_recording_store.py +0 -0
  201. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_redundant_attribute_unpacking.py +0 -0
  202. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  203. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_redundant_local_alias.py +0 -0
  204. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  205. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_redundant_sql_alias.py +0 -0
  206. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  207. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  208. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  209. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  210. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  211. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  212. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  213. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  214. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_shared_mutable_default.py +0 -0
  215. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +0 -0
  216. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  217. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  218. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_sys_path_mutation.py +0 -0
  219. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  220. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_test_comments.py +0 -0
  221. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  222. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  223. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  224. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  225. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  226. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  227. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  228. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  229. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  230. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  231. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
  232. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  233. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  234. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  235. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_application_model_joins.py +0 -0
  236. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  237. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  238. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  239. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  240. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  241. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  242. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_direct_import_module.py +0 -0
  243. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  244. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  245. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_grouped_settings.py +0 -0
  246. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  247. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  248. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  249. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  250. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  251. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  252. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  253. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  254. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  255. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  256. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  257. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  258. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  259. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_optional_composite.py +0 -0
  260. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  261. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_precise_object_contract.py +0 -0
  262. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  263. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  264. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_pydantic_model_assertion.py +0 -0
  265. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_pytest_dependency_fixtures.py +0 -0
  266. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_contract.py +0 -0
  267. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  268. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  269. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  270. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_runtime_validation.py +0 -0
  271. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  272. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  273. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  274. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  275. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  276. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_typed_cli.py +0 -0
  277. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_typed_mapping_get.py +0 -0
  278. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_typed_settings_field.py +0 -0
  279. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  280. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  281. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  282. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  283. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  284. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/pytest_fixture_returns_closed_resource.py +0 -0
  285. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  286. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  287. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  288. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  289. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  290. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  291. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  292. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  293. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/require_constrained_api_integer.py +0 -0
  294. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +0 -0
  295. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  296. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  297. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  298. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  299. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  300. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  301. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  302. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  303. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  304. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  305. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
  306. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  307. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  308. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  309. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/service_singleton_delegates_to_bulk.py +0 -0
  310. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  311. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  312. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
  313. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  314. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/store_insert_requires_explicit_conflict_policy.py +0 -0
  315. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  316. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/store_mutation_command_fanout.py +0 -0
  317. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  318. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  319. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  320. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  321. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  322. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  323. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  324. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
  325. {sarj_python_lint-1.109.0 → sarj_python_lint-1.110.0}/src/sarj_python_lint/suppression_policy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 1.109.0
3
+ Version: 1.110.0
4
4
  Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
5
5
  Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
6
6
  Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "1.109.0"
3
+ version = "1.110.0"
4
4
  description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "sarj-ai" }]
@@ -289,6 +289,7 @@ _DIAGNOSTIC_PRECEDENCE = MappingProxyType(
289
289
  "SARJ457": frozenset({"SARJ045", "SARJ526"}),
290
290
  "SARJ465": frozenset({"SARJ071"}),
291
291
  "SARJ491": frozenset({"SARJ400"}),
292
+ "SARJ518": frozenset({"SARJ560"}),
292
293
  }
293
294
  )
294
295
 
@@ -8,6 +8,33 @@ from sarj_python_lint.rule_metadata import RuleDescriptor
8
8
 
9
9
  SCHEMA_VERSION = 1
10
10
  DESCRIPTORS: tuple[RuleDescriptor, ...] = (
11
+ RuleDescriptor(
12
+ rule_id=RuleId("prefer-validated-uuid-comparison"),
13
+ module="sarj_python_lint.rules.prefer_validated_uuid_comparison",
14
+ class_name="PreferValidatedUuidComparison",
15
+ code="SARJ562",
16
+ description="Validate UUID identifiers at input boundaries before comparing them.",
17
+ aliases=(),
18
+ enabled_by_default=True,
19
+ ),
20
+ RuleDescriptor(
21
+ rule_id=RuleId("prefer-single-psycopg-cursor"),
22
+ module="sarj_python_lint.rules.prefer_single_psycopg_cursor",
23
+ class_name="PreferSinglePsycopgCursor",
24
+ code="SARJ561",
25
+ description="Review repeated Psycopg cursor creation within one function.",
26
+ aliases=(),
27
+ enabled_by_default=True,
28
+ ),
29
+ RuleDescriptor(
30
+ rule_id=RuleId("no-psycopg-execution-in-loop"),
31
+ module="sarj_python_lint.rules.no_psycopg_execution_in_loop",
32
+ class_name="NoPsycopgExecutionInLoop",
33
+ code="SARJ560",
34
+ description="Use bounded bulk queries instead of executing Psycopg operations inside item loops.",
35
+ aliases=(),
36
+ enabled_by_default=True,
37
+ ),
11
38
  RuleDescriptor(
12
39
  rule_id=RuleId("no-shared-mutable-default"),
13
40
  module="sarj_python_lint.rules.no_shared_mutable_default",
@@ -112,7 +139,7 @@ DESCRIPTORS: tuple[RuleDescriptor, ...] = (
112
139
  module="sarj_python_lint.rules.no_custom_control_flow_exception",
113
140
  class_name="NoCustomControlFlowException",
114
141
  code="SARJ542",
115
- description="Return typed outcomes for expected domain failures instead of name-only exceptions.",
142
+ description="Return typed outcomes for expected domain failures instead of marker or fixed-message exceptions.",
116
143
  aliases=(),
117
144
  enabled_by_default=True,
118
145
  ),
@@ -248,6 +248,7 @@ from sarj_python_lint.rules.no_impossible_pydantic_field_bounds import NoImpossi
248
248
  from sarj_python_lint.rules.no_invalid_pydantic_bypass import NoInvalidPydanticBypass
249
249
  from sarj_python_lint.rules.no_invalid_pydantic_validator_return import NoInvalidPydanticValidatorReturn
250
250
  from sarj_python_lint.rules.no_partial_type_is import NoPartialTypeIs
251
+ from sarj_python_lint.rules.no_psycopg_execution_in_loop import NoPsycopgExecutionInLoop
251
252
  from sarj_python_lint.rules.no_recording_store import NoRecordingStore
252
253
  from sarj_python_lint.rules.no_redundant_local_alias import NoRedundantLocalAlias
253
254
  from sarj_python_lint.rules.no_redundant_sql_alias import NoRedundantSqlAlias
@@ -268,9 +269,11 @@ from sarj_python_lint.rules.prefer_psycopg_executemany import PreferPsycopgExecu
268
269
  from sarj_python_lint.rules.prefer_pydantic_model_assertion import PreferPydanticModelAssertion
269
270
  from sarj_python_lint.rules.prefer_pytest_dependency_fixtures import PreferPytestDependencyFixtures
270
271
  from sarj_python_lint.rules.prefer_runtime_validation import PreferRuntimeValidation
272
+ from sarj_python_lint.rules.prefer_single_psycopg_cursor import PreferSinglePsycopgCursor
271
273
  from sarj_python_lint.rules.prefer_typed_cli import PreferTypedCli
272
274
  from sarj_python_lint.rules.prefer_typed_mapping_get import PreferTypedMappingGet
273
275
  from sarj_python_lint.rules.prefer_typed_settings_field import PreferTypedSettingsField
276
+ from sarj_python_lint.rules.prefer_validated_uuid_comparison import PreferValidatedUuidComparison
274
277
  from sarj_python_lint.rules.pytest_fixture_returns_closed_resource import PytestFixtureReturnsClosedResource
275
278
  from sarj_python_lint.rules.redundant_test_constructor_forwarder import RedundantTestConstructorForwarder
276
279
  from sarj_python_lint.rules.repeated_unimplemented_test_contract import RepeatedUnimplementedTestContract
@@ -286,6 +289,9 @@ from sarj_python_lint.rules.test_without_explicit_oracle import NoExplicitTestOr
286
289
 
287
290
  _AUTHORING_REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
288
291
  {
292
+ PreferValidatedUuidComparison.id: PreferValidatedUuidComparison,
293
+ PreferSinglePsycopgCursor.id: PreferSinglePsycopgCursor,
294
+ NoPsycopgExecutionInLoop.id: NoPsycopgExecutionInLoop,
289
295
  NoSharedMutableDefault.id: NoSharedMutableDefault,
290
296
  NoSysPathMutation.id: NoSysPathMutation,
291
297
  NoDynamicBuiltins.id: NoDynamicBuiltins,
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING
8
8
  from sqlglot import exp
9
9
 
10
10
  from sarj_python_lint.rules._sql import sql_string_value
11
- from sarj_python_lint.rules._store_writes import Function, methods
11
+ from sarj_python_lint.rules._store_writes import Function, methods, own_nodes
12
12
  from sarj_python_lint.rules.no_psycopg_execution_outside_injected_owner import psycopg_execution_calls
13
13
 
14
14
 
@@ -21,6 +21,7 @@ if TYPE_CHECKING:
21
21
  _HOLE = "__sarj_store_sql_hole__"
22
22
  _FORMAT_HOLE = re.compile(r"(?<!\{)\{(?:[A-Za-z_]\w*|\d*)\}(?!\})")
23
23
  _MAX_RECOVERY_DEPTH = 8
24
+ _MAX_APPEND_VARIANTS = 16
24
25
 
25
26
 
26
27
  @dataclass(frozen=True)
@@ -44,6 +45,25 @@ class _MethodWrites:
44
45
  instances: frozenset[tuple[ast.ClassDef, str]]
45
46
 
46
47
 
48
+ @dataclass(frozen=True)
49
+ class _QueryVariants:
50
+ origin: ast.expr
51
+ texts: tuple[str, ...]
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class _AppendDefinition:
56
+ assignment: ast.Assign
57
+ writes: list[ast.AST | None]
58
+ function: Function
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class _LiteralAppend:
63
+ text: str
64
+ conditional: bool
65
+
66
+
47
67
  def store_queries(context: PythonFileContext) -> Iterator[StoreQuery]:
48
68
  if context.store_queries is None:
49
69
  try:
@@ -151,8 +171,17 @@ def query_owner(node: ast.AST, context: PythonFileContext) -> QueryOwner:
151
171
 
152
172
  def _parsed_query(expression: ast.expr, call: ast.Call, context: PythonFileContext) -> Iterator[StoreQuery]:
153
173
  text = _query_text(expression, context, seen=frozenset())
154
- if text is None:
155
- return
174
+ origin = _query_origin(expression, context)
175
+ texts = (text,) if text is not None else ()
176
+ if not texts and isinstance(expression, ast.Name):
177
+ appended = _literal_append_variants(expression, context)
178
+ if appended is not None:
179
+ origin, texts = appended.origin, appended.texts
180
+ for query_text in texts:
181
+ yield from _parse_query_text(query_text, origin, call, context)
182
+
183
+
184
+ def _parse_query_text(text: str, origin: ast.expr, call: ast.Call, context: PythonFileContext) -> Iterator[StoreQuery]:
156
185
  from sqlglot.errors import SqlglotError # ruff: ignore[import-outside-top-level] -- malformed input abstains.
157
186
 
158
187
  from sarj_python_lint.rules._postgres import parse_postgres # ruff: ignore[import-outside-top-level] -- avoid private-source unsupported SQL logs.
@@ -164,7 +193,81 @@ def _parsed_query(expression: ast.expr, call: ast.Call, context: PythonFileConte
164
193
  scope = query_owner(call, context)
165
194
  for statement in statements:
166
195
  if statement is not None:
167
- yield StoreQuery(_query_origin(expression, context), scope.owner, scope.method, text, statement)
196
+ yield StoreQuery(origin, scope.owner, scope.method, text, statement)
197
+
198
+
199
+ def _literal_append_variants(expression: ast.Name, context: PythonFileContext) -> _QueryVariants | None:
200
+ definition = _append_definition(expression, context)
201
+ if definition is None:
202
+ return None
203
+ value = definition.assignment.value
204
+ if not isinstance(value, ast.Constant) or not isinstance(value.value, str):
205
+ return None
206
+ variants = (value.value,)
207
+ for write in sorted(definition.writes, key=lambda node: node.lineno if isinstance(node, ast.stmt) else 0):
208
+ if write is definition.assignment:
209
+ continue
210
+ append = _literal_append(write, definition, expression, context)
211
+ if append is None:
212
+ return None
213
+ appended = tuple(text + append.text for text in variants)
214
+ variants = (*variants, *appended) if append.conditional else appended
215
+ if len(variants) > _MAX_APPEND_VARIANTS:
216
+ return None
217
+ return _QueryVariants(value, tuple(dict.fromkeys(variants)))
218
+
219
+
220
+ def _append_definition(expression: ast.Name, context: PythonFileContext) -> _AppendDefinition | None:
221
+ if context.static_resource_provenance.dynamic_namespace or context.static_resource_provenance.wildcard:
222
+ return None
223
+ function = query_owner(expression, context).method
224
+ if function is None:
225
+ return None
226
+ writes = [
227
+ node
228
+ for node in own_nodes(function)
229
+ if isinstance(node, ast.Name) and node.id == expression.id and isinstance(node.ctx, ast.Store | ast.Del)
230
+ ]
231
+ assignments = [context.parents.get(node) for node in writes]
232
+ initial = [
233
+ node
234
+ for node in assignments
235
+ if isinstance(node, ast.Assign)
236
+ and len(node.targets) == 1
237
+ and node in function.body
238
+ and node.lineno < expression.lineno
239
+ ]
240
+ if len(initial) != 1:
241
+ return None
242
+ if any(expression.id in node.names for node in context.nodes(ast.Global, ast.Nonlocal)):
243
+ return None
244
+ return _AppendDefinition(initial[0], assignments, function)
245
+
246
+
247
+ def _literal_append(
248
+ write: ast.AST | None,
249
+ definition: _AppendDefinition,
250
+ expression: ast.Name,
251
+ context: PythonFileContext,
252
+ ) -> _LiteralAppend | None:
253
+ if not (
254
+ isinstance(write, ast.AugAssign)
255
+ and isinstance(write.op, ast.Add)
256
+ and isinstance(write.value, ast.Constant)
257
+ and isinstance(write.value.value, str)
258
+ and definition.assignment.lineno < write.lineno < expression.lineno
259
+ ):
260
+ return None
261
+ parent = context.parents.get(write)
262
+ conditional = (
263
+ isinstance(parent, ast.If)
264
+ and parent.body == [write]
265
+ and not parent.orelse
266
+ and parent in definition.function.body
267
+ )
268
+ if parent is not definition.function and not conditional:
269
+ return None
270
+ return _LiteralAppend(write.value.value, conditional)
168
271
 
169
272
 
170
273
  def _query_origin(expression: ast.expr, context: PythonFileContext) -> ast.expr:
@@ -226,6 +329,8 @@ def _query_text(expression: ast.expr, context: PythonFileContext, *, seen: froze
226
329
  return literal
227
330
  match expression:
228
331
  case ast.Name():
332
+ if context.static_resource_provenance.dynamic_namespace:
333
+ return None
229
334
  value = _bound_value(expression, context)
230
335
  return _query_text(value, context, seen=seen) if value is not None else None
231
336
  case ast.Call(func=ast.Attribute(attr="format", value=value)):
@@ -58,9 +58,9 @@ class NoCustomControlFlowException(Rule):
58
58
  code = "SARJ542"
59
59
  documentation = RuleDocumentation(
60
60
  default_level=Severity.WARNING,
61
- summary="Return typed outcomes for expected domain failures instead of name-only exceptions.",
61
+ summary="Return typed outcomes for expected domain failures instead of marker or fixed-message exceptions.",
62
62
  rationale=(
63
- "An empty custom exception creates another control-flow type without declaring failure data in "
63
+ "A marker or fixed-message custom exception creates another control-flow type without declaring failure data in "
64
64
  "the function's return contract. Expected domain outcomes can often use tagged result variants "
65
65
  "that callers handle explicitly, while exceptional failures and framework rejection still need exceptions."
66
66
  ),
@@ -73,7 +73,7 @@ class NoCustomControlFlowException(Rule):
73
73
  category=RuleCategory.ARCHITECTURE,
74
74
  autofix=AutofixPolicy.NONE,
75
75
  limitations=(
76
- "This review warning checks stable module-level local Exception/ValueError subclasses whose bodies contain only pass, ellipsis or docstring expressions, including direct from-builtins exception imports and empty local subclasses of those markers. A source-visible direct raise of the class or its constructor is required; imported errors, payload-bearing classes, methods, decorators, generic/ambiguous bases and unused definitions abstain.",
76
+ "This review warning checks stable module-level Exception/ValueError markers and fixed-message subclasses whose only behavior is a zero-argument initializer calling super().__init__ with one literal string. A source-visible direct raise is required. Fixed-message subclasses of a directly imported base also qualify for review; imported base semantics remain a manual contract check. Payload fields, dynamic messages, extra behavior, decorators, generic/ambiguous bases and unused definitions abstain.",
77
77
  "Raising a ValueError-derived marker from an import-proven Pydantic field/model validator or legacy validator preserves that rejection protocol. Direct synchronous module callbacks used only as Annotated validator metadata on plain direct BaseModel fields, and stable nonescaping direct nested helpers, also preserve rejection; lexical helper ownership is bounded to 32 levels. Unknown external bases and visible mutations or escapes of the class or a local base to registrations, callbacks or other value consumers abstain; HTTPException and other framework-required exception types remain valid.",
78
78
  "Expectedness is not inferred from a name, message or prefix. Genuine exceptional markers may also match and require review rather than conversion. Cross-module callers and registrations, indirect raises, dynamically mutated namespaces and class aliases are not inferred. Only simple stable module assignment aliases and nongeneric annotation aliases consumed directly by model fields are followed. Explicit string annotations, custom model hooks, async/generator callbacks, escaped helpers and broader call graphs are not followed. Generic lexical resolution can miss a module marker shadowed only in a method's class namespace.",
79
79
  "Tests, generated and vendor code are excluded. No automatic rewrite is safe: a result return must preserve failure handling and transaction/resource unwinding. SARJ435 separately reviews joined-string reason identities.",
@@ -144,7 +144,7 @@ class NoCustomControlFlowException(Rule):
144
144
  marker.lineno,
145
145
  marker.col_offset + 1,
146
146
  self.code,
147
- f"Review `{marker.name}`: it adds only an exception identity and is directly raised. "
147
+ f"Review `{marker.name}`: it adds only an exception identity or fixed message and is directly raised. "
148
148
  "If this is an expected domain outcome, return a typed tagged result; preserve genuine "
149
149
  "exceptional failures, validation and framework contracts.",
150
150
  severity=Severity.WARNING,
@@ -158,17 +158,15 @@ def _markers(context: PythonFileContext, provenance: ResourceProvenance) -> dict
158
158
  return {}
159
159
  markers: dict[str, ast.ClassDef] = {}
160
160
  for node in context.tree.body:
161
- if not isinstance(node, ast.ClassDef) or not _name_only(node):
161
+ if not isinstance(node, ast.ClassDef) or not _marker_body(node):
162
162
  continue
163
163
  if provenance.binding(node.name, node) is not node:
164
164
  continue
165
165
  base = node.bases[0]
166
- local = (
167
- isinstance(base, ast.Name)
168
- and provenance.binding(base.id, base) is markers.get(base.id)
169
- and base.id in markers
170
- )
171
- if not (local or _builtin_base(base, context, provenance)):
166
+ fixed_message = any(isinstance(member, ast.FunctionDef) for member in node.body)
167
+ if not _marker_base(base, markers, context, fixed_message=fixed_message):
168
+ continue
169
+ if fixed_message and not context.imports.builtin_is_unshadowed("super"):
172
170
  continue
173
171
  if node.name in provenance.mutated or node.name in provenance.external_bindings:
174
172
  continue
@@ -178,7 +176,22 @@ def _markers(context: PythonFileContext, provenance: ResourceProvenance) -> dict
178
176
  return markers
179
177
 
180
178
 
181
- def _name_only(node: ast.ClassDef) -> bool:
179
+ def _marker_base(
180
+ base: ast.expr, markers: dict[str, ast.ClassDef], context: PythonFileContext, *, fixed_message: bool
181
+ ) -> bool:
182
+ provenance = context.static_resource_provenance
183
+ local = isinstance(base, ast.Name) and base.id in markers and provenance.binding(base.id, base) is markers[base.id]
184
+ if local or _builtin_base(base, context, provenance):
185
+ return True
186
+ imported = provenance.runtime_import_name(base)
187
+ return (
188
+ fixed_message
189
+ and imported is not None
190
+ and not imported.startswith(("builtins.", "asyncio.", "fastapi.", "starlette.", "concurrent.futures."))
191
+ )
192
+
193
+
194
+ def _marker_body(node: ast.ClassDef) -> bool:
182
195
  return (
183
196
  len(node.bases) == 1
184
197
  and not node.decorator_list
@@ -186,6 +199,7 @@ def _name_only(node: ast.ClassDef) -> bool:
186
199
  and not node.type_params
187
200
  and all(
188
201
  isinstance(member, ast.Pass)
202
+ or (isinstance(member, ast.FunctionDef) and _fixed_message_initializer(member))
189
203
  or (
190
204
  isinstance(member, ast.Expr)
191
205
  and isinstance(member.value, ast.Constant)
@@ -196,6 +210,29 @@ def _name_only(node: ast.ClassDef) -> bool:
196
210
  )
197
211
 
198
212
 
213
+ def _fixed_message_initializer(node: ast.FunctionDef) -> bool:
214
+ arguments = [*node.args.posonlyargs, *node.args.args]
215
+ if node.name != "__init__" or node.decorator_list or node.type_params or len(arguments) != 1:
216
+ return False
217
+ if node.args.vararg or node.args.kwarg or node.args.kwonlyargs or node.args.defaults:
218
+ return False
219
+ match node.body:
220
+ case [
221
+ ast.Expr(
222
+ value=ast.Call(
223
+ func=ast.Attribute(
224
+ value=ast.Call(func=ast.Name(id="super"), args=[], keywords=[]), attr="__init__"
225
+ ),
226
+ args=[ast.Constant(value=str())],
227
+ keywords=[],
228
+ )
229
+ )
230
+ ]:
231
+ return True
232
+ case _:
233
+ return False
234
+
235
+
199
236
  def _builtin_namespace_safe(context: PythonFileContext) -> bool:
200
237
  # Direct exception imports expose immutable class objects.
201
238
  for node in context.nodes(ast.Import, ast.ImportFrom, ast.Name):
@@ -250,7 +287,7 @@ def _value_error_marker(marker: ast.ClassDef, context: PythonFileContext, proven
250
287
  ) or provenance.imported(base, "builtins.ValueError"):
251
288
  return True
252
289
  bound = provenance.binding(base.id, base) if isinstance(base, ast.Name) else None
253
- if not isinstance(bound, ast.ClassDef) or not _name_only(bound):
290
+ if not isinstance(bound, ast.ClassDef) or not _marker_body(bound):
254
291
  return False
255
292
  current = bound
256
293
  return False
@@ -0,0 +1,121 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from pathlib import PurePosixPath
5
+ from typing import TYPE_CHECKING, final, override
6
+
7
+ from sarj_python_lint.rule_base import (
8
+ AutofixPolicy,
9
+ Diagnostic,
10
+ ExampleFile,
11
+ ExampleOutcome,
12
+ Rule,
13
+ RuleCategory,
14
+ RuleDocumentation,
15
+ RuleExample,
16
+ Severity,
17
+ is_suppressed,
18
+ )
19
+ from sarj_python_lint.rules._store_writes import known_single_iteration
20
+ from sarj_python_lint.rules.no_psycopg_execution_outside_injected_owner import psycopg_execution_calls
21
+
22
+
23
+ if TYPE_CHECKING:
24
+ from sarj_python_lint._file_context import PythonFileContext
25
+
26
+
27
+ _BAD = "from psycopg import AsyncConnection\nasync def remove(connection: AsyncConnection, rows):\n for row in rows:\n await connection.execute('DELETE FROM item WHERE id = %s', (row.id,))\n"
28
+ _GOOD = "from psycopg import AsyncConnection\nasync def remove(connection: AsyncConnection, rows):\n identifiers = [row.id for row in rows]\n await connection.execute('DELETE FROM item WHERE id = ANY(%s)', (identifiers,))\n"
29
+
30
+
31
+ @final
32
+ class NoPsycopgExecutionInLoop(Rule):
33
+ id = "no-psycopg-execution-in-loop"
34
+ code = "SARJ560"
35
+ documentation = RuleDocumentation(
36
+ default_level=Severity.WARNING,
37
+ summary="Use bounded bulk queries instead of executing Psycopg operations inside item loops.",
38
+ rationale="Executing a database operation for each input item makes round trips scale with collection size, including loops that prepare each row before executing.",
39
+ remediation="Collect inputs and use a bounded set-based query, typed bulk store operation or executemany batch. Preserve tenant checks, revision predicates, transaction boundaries, returned rows, ordering and deliberate sequential effects.",
40
+ category=RuleCategory.PERFORMANCE,
41
+ autofix=AutofixPolicy.NONE,
42
+ limitations=(
43
+ "Checks import-proven Psycopg execute calls in for/async-for bodies and repeated comprehension expressions, including injected pools, connections and cursors. Explicit executemany batch loops remain valid for bounded chunking.",
44
+ "In-memory preparation, one-shot iteration inputs, known zero/single-item loops, deferred nested functions, unrelated execute methods and generated code are excluded. While retry/stream loops and interprocedural helper execution are not inferred.",
45
+ "SARJ518 owns its narrower invariant-write cases, so one location receives only that specific executemany diagnostic. Unknown SQL and result-consuming loops still require bulk-query review; no automatic rewrite changes database semantics.",
46
+ ),
47
+ examples=(
48
+ RuleExample(
49
+ example_id="per-item-write",
50
+ title="A connection executes once per item",
51
+ outcome=ExampleOutcome.MATCH,
52
+ files=(ExampleFile.python("app/store.py", _BAD),),
53
+ focus_path=PurePosixPath("app/store.py"),
54
+ expected_count=1,
55
+ public=True,
56
+ ),
57
+ RuleExample(
58
+ example_id="set-based-write",
59
+ title="One query consumes the collected identifiers",
60
+ outcome=ExampleOutcome.NO_MATCH,
61
+ files=(ExampleFile.python("app/store.py", _GOOD),),
62
+ focus_path=PurePosixPath("app/store.py"),
63
+ expected_count=0,
64
+ public=True,
65
+ ),
66
+ ),
67
+ )
68
+ description = documentation.summary
69
+
70
+ @override
71
+ def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
72
+ if context.generated or context.tree is None:
73
+ return []
74
+ calls = [
75
+ call
76
+ for call in psycopg_execution_calls(context, include_injected=True, include_probes=False)
77
+ if isinstance(call.func, ast.Attribute) and call.func.attr == "execute" and _repeated(call, context)
78
+ ]
79
+ if not calls:
80
+ return []
81
+ unique = {(call.lineno, call.col_offset): call for call in calls}
82
+ return [
83
+ Diagnostic(
84
+ context.path,
85
+ call.lineno,
86
+ call.col_offset + 1,
87
+ self.code,
88
+ "This item loop executes a Psycopg operation per iteration; collect inputs and prefer a bounded bulk query or bulk store method, preserving transaction, result and failure semantics.",
89
+ severity=Severity.WARNING,
90
+ )
91
+ for call in unique.values()
92
+ if not is_suppressed(context.source_lines, call.lineno, self.code)
93
+ ]
94
+
95
+
96
+ def _repeated(call: ast.Call, context: PythonFileContext) -> bool:
97
+ child: ast.AST = call
98
+ parent = context.parents.get(child)
99
+ while parent is not None:
100
+ if isinstance(parent, ast.FunctionDef | ast.AsyncFunctionDef | ast.Lambda | ast.ClassDef):
101
+ return False
102
+ if (
103
+ isinstance(parent, ast.For | ast.AsyncFor)
104
+ and child in parent.body
105
+ and not known_single_iteration(parent.iter)
106
+ ):
107
+ return True
108
+ if isinstance(parent, ast.comprehension):
109
+ owner = context.parents.get(parent)
110
+ if child in parent.ifs or (
111
+ child is parent.iter
112
+ and isinstance(owner, ast.ListComp | ast.SetComp | ast.DictComp | ast.GeneratorExp)
113
+ and owner.generators.index(parent) > 0
114
+ ):
115
+ return True
116
+ if isinstance(parent, ast.ListComp | ast.SetComp | ast.GeneratorExp) and child is parent.elt:
117
+ return True
118
+ if isinstance(parent, ast.DictComp) and child in {parent.key, parent.value}:
119
+ return True
120
+ child, parent = parent, context.parents.get(parent)
121
+ return False
@@ -156,12 +156,26 @@ class NoPsycopgExecutionOutsideInjectedOwner(Rule):
156
156
 
157
157
  def psycopg_execution_calls(
158
158
  context: PythonFileContext, *, include_injected: bool, include_probes: bool
159
+ ) -> list[ast.Call]:
160
+ return _psycopg_operation_calls(
161
+ context, include_injected=include_injected, include_probes=include_probes, operations=_EXECUTE_METHODS
162
+ )
163
+
164
+
165
+ def psycopg_cursor_calls(context: PythonFileContext) -> list[ast.Call]:
166
+ return _psycopg_operation_calls(
167
+ context, include_injected=True, include_probes=True, operations=frozenset({"cursor"})
168
+ )
169
+
170
+
171
+ def _psycopg_operation_calls(
172
+ context: PythonFileContext, *, include_injected: bool, include_probes: bool, operations: frozenset[str]
159
173
  ) -> list[ast.Call]:
160
174
  tree = context.tree
161
175
  if tree is None:
162
176
  return []
163
177
  if not any(
164
- isinstance(call.func, ast.Attribute) and call.func.attr in _EXECUTE_METHODS for call in context.nodes(ast.Call)
178
+ isinstance(call.func, ast.Attribute) and call.func.attr in operations for call in context.nodes(ast.Call)
165
179
  ):
166
180
  return []
167
181
  mutated_modules: frozenset[str] | None = frozenset()
@@ -184,6 +198,7 @@ def psycopg_execution_calls(
184
198
  include_probes=include_probes,
185
199
  track_cursor_parameters=include_injected or scope.owner is None,
186
200
  constructor=scope.owner is not None and scope.function.name == "__init__",
201
+ operations=operations,
187
202
  ).analyze(scope.function)
188
203
  )
189
204
  return calls
@@ -495,6 +510,7 @@ class _FunctionAnalyzer:
495
510
  include_probes: bool,
496
511
  track_cursor_parameters: bool,
497
512
  constructor: bool,
513
+ operations: frozenset[str],
498
514
  ) -> None:
499
515
  self._types = types
500
516
  self._annotation_types = annotation_types
@@ -503,6 +519,7 @@ class _FunctionAnalyzer:
503
519
  self._include_probes = include_probes
504
520
  self._track_cursor_parameters = track_cursor_parameters
505
521
  self._constructor = constructor
522
+ self._operations = operations
506
523
  self._findings: list[ast.Call] = []
507
524
  self._shadowed: set[str] = set()
508
525
 
@@ -673,7 +690,7 @@ class _FunctionAnalyzer:
673
690
  def visit_Call(self, node: ast.Call) -> None:
674
691
  if (
675
692
  isinstance(node.func, ast.Attribute)
676
- and node.func.attr in _EXECUTE_METHODS
693
+ and node.func.attr in analyzer._operations
677
694
  and analyzer._expression_origin(node.func.value, state) in analyzer._allowed_origins
678
695
  and (analyzer._include_probes or not _is_connection_probe(node))
679
696
  ):