sarj-python-lint 1.99.0__tar.gz → 1.101.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 (314) hide show
  1. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/PKG-INFO +2 -2
  2. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/pyproject.toml +2 -2
  3. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/_autofix.py +41 -9
  4. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/_parallel.py +12 -7
  5. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_copy_on_write.py +6 -8
  6. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_namespace_access.py +1 -1
  7. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_project_index.py +19 -3
  8. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_resource_provenance.py +2 -1
  9. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_sql.py +9 -2
  10. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_store_sql.py +5 -1
  11. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +71 -38
  12. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +4 -3
  13. sarj_python_lint-1.101.0/src/sarj_python_lint/rules/no_custom_control_flow_exception.py +476 -0
  14. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_delete_statement.py +2 -2
  15. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_direct_import_module.py +3 -2
  16. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_optional_composite.py +122 -19
  17. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +1 -1
  18. sarj_python_lint-1.99.0/src/sarj_python_lint/rules/no_custom_control_flow_exception.py +0 -251
  19. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/.gitignore +0 -0
  20. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/LICENSE +0 -0
  21. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/README.md +0 -0
  22. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/__init__.py +0 -0
  23. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/__main__.py +0 -0
  24. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  25. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/_autofix_aliases.py +0 -0
  26. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/_autofix_annotations.py +0 -0
  27. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/_autofix_comments.py +0 -0
  28. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/_autofix_declared_contracts.py +0 -0
  29. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/_autofix_declared_tests.py +0 -0
  30. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/_autofix_docs.py +0 -0
  31. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/_autofix_enums.py +0 -0
  32. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/_autofix_exhaustiveness.py +0 -0
  33. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/_autofix_inert.py +0 -0
  34. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/_autofix_parameters.py +0 -0
  35. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/_autofix_repeated.py +0 -0
  36. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/_autofix_security.py +0 -0
  37. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/_autofix_sql_projection.py +0 -0
  38. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/_autofix_tests.py +0 -0
  39. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/_file_context.py +0 -0
  40. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/_filesystem.py +0 -0
  41. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/_fixes.py +0 -0
  42. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/_prepared_sources.py +0 -0
  43. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/_python_target.py +0 -0
  44. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  45. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/_rule_descriptors.py +0 -0
  46. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/_secret_names.py +0 -0
  47. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/_source.py +0 -0
  48. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/_version.py +0 -0
  49. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/application_logging.py +0 -0
  50. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/autofix.py +0 -0
  51. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/benchmark.py +0 -0
  52. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/cast_access.py +0 -0
  53. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  54. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/json_boundary.py +0 -0
  55. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/py.typed +0 -0
  56. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/ratchet.py +0 -0
  57. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rule_base.py +0 -0
  58. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rule_metadata.py +0 -0
  59. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  60. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_annotation_origins.py +0 -0
  61. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  62. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  63. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_ast_names.py +0 -0
  64. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  65. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_async_lifecycle.py +0 -0
  66. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_binding_inventory.py +0 -0
  67. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  68. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_comment_narration.py +0 -0
  69. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  70. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  71. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
  72. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_declared_contracts.py +0 -0
  73. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  74. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  75. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  76. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  77. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  78. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  79. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  80. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  81. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  82. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_local_alias_fix.py +0 -0
  83. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  84. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  85. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
  86. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  87. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_native_object_contracts.py +0 -0
  88. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  89. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_numeric_proof.py +0 -0
  90. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_opaque_contracts.py +0 -0
  91. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  92. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_postgres.py +0 -0
  93. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  94. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  95. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  96. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_registry.py +0 -0
  97. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  98. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
  99. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  100. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_runtime_registry.py +0 -0
  101. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_service_http_callbacks.py +0 -0
  102. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  103. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_sql_aggregate_identity.py +0 -0
  104. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_sql_explain.py +0 -0
  105. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_sql_policy_source.py +0 -0
  106. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  107. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  108. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  109. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  110. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  111. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  112. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_transactional_sql.py +0 -0
  113. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  114. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  115. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/async_executor_shutdown.py +0 -0
  116. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  117. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/async_subprocess_cancellation_cleanup.py +0 -0
  118. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/cancel_owned_task_without_join.py +0 -0
  119. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  120. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
  121. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  122. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  123. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  124. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  125. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py +0 -0
  126. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  127. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/failed_probe_zero_threshold.py +0 -0
  128. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  129. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  130. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  131. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  132. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  133. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  134. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  135. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/nan_hidden_by_builtin_min.py +0 -0
  136. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  137. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  138. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_async_client_in_constructor.py +0 -0
  139. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  140. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  141. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
  142. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  143. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  144. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  145. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  146. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  147. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  148. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  149. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +0 -0
  150. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  151. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
  152. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_discarded_tagged_outcome.py +0 -0
  153. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
  154. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +0 -0
  155. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  156. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  157. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
  158. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +0 -0
  159. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  160. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  161. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  162. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  163. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  164. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  165. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +0 -0
  166. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  167. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_generic_json_parser.py +0 -0
  168. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  169. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
  170. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  171. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  172. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  173. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  174. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  175. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  176. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  177. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  178. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  179. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  180. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  181. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
  182. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_mutating_input_with_result.py +0 -0
  183. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  184. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  185. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  186. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  187. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  188. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  189. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  190. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  191. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  192. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_recording_store.py +0 -0
  193. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_redundant_attribute_unpacking.py +0 -0
  194. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  195. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_redundant_local_alias.py +0 -0
  196. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  197. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_redundant_sql_alias.py +0 -0
  198. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  199. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  200. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  201. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  202. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  203. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  204. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  205. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  206. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +0 -0
  207. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  208. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  209. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  210. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_test_comments.py +0 -0
  211. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  212. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  213. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  214. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  215. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  216. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  217. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  218. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  219. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  220. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  221. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
  222. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  223. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  224. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  225. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_application_model_joins.py +0 -0
  226. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  227. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  228. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  229. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  230. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  231. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  232. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  233. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  234. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_grouped_settings.py +0 -0
  235. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  236. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  237. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  238. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  239. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  240. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  241. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  242. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  243. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  244. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  245. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  246. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  247. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  248. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  249. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  250. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_precise_object_contract.py +0 -0
  251. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  252. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  253. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_pydantic_model_assertion.py +0 -0
  254. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_pytest_dependency_fixtures.py +0 -0
  255. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_contract.py +0 -0
  256. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  257. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  258. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  259. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_runtime_validation.py +0 -0
  260. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  261. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  262. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  263. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  264. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  265. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_typed_cli.py +0 -0
  266. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_typed_mapping_get.py +0 -0
  267. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_typed_settings_field.py +0 -0
  268. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  269. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  270. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  271. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  272. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  273. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  274. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/pytest_fixture_returns_closed_resource.py +0 -0
  275. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  276. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  277. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  278. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  279. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  280. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  281. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  282. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  283. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/require_constrained_api_integer.py +0 -0
  284. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +0 -0
  285. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  286. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  287. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  288. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  289. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  290. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  291. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  292. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  293. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  294. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
  295. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  296. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  297. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  298. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/service_singleton_delegates_to_bulk.py +0 -0
  299. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  300. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  301. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
  302. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  303. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/store_insert_requires_explicit_conflict_policy.py +0 -0
  304. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  305. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/store_mutation_command_fanout.py +0 -0
  306. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  307. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  308. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  309. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  310. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  311. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  312. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  313. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
  314. {sarj_python_lint-1.99.0 → sarj_python_lint-1.101.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.99.0
3
+ Version: 1.101.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/
@@ -19,7 +19,7 @@ Classifier: Topic :: Software Development :: Quality Assurance
19
19
  Requires-Python: >=3.14
20
20
  Requires-Dist: packaging==26.3
21
21
  Requires-Dist: ruff>=0.17.2
22
- Requires-Dist: sarj-rule-contracts==0.5.0
22
+ Requires-Dist: sarj-rule-contracts==0.6.0
23
23
  Requires-Dist: sqlglot==30.24.0
24
24
  Requires-Dist: typer==0.27.3
25
25
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "1.99.0"
3
+ version = "1.101.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" }]
@@ -15,7 +15,7 @@ classifiers = [
15
15
  "Programming Language :: Python :: 3.14",
16
16
  "Topic :: Software Development :: Quality Assurance",
17
17
  ]
18
- dependencies = ["sarj-rule-contracts==0.5.0", "packaging==26.3", "ruff>=0.17.2", "sqlglot==30.24.0", "typer==0.27.3"]
18
+ dependencies = ["sarj-rule-contracts==0.6.0", "packaging==26.3", "ruff>=0.17.2", "sqlglot==30.24.0", "typer==0.27.3"]
19
19
 
20
20
  [project.scripts]
21
21
  sarj-python-lint = "sarj_python_lint.__main__:main"
@@ -2,13 +2,14 @@ from __future__ import annotations
2
2
 
3
3
  import ast
4
4
  from bisect import bisect_left, insort
5
+ from dataclasses import dataclass
5
6
  from functools import cached_property
6
7
  from hashlib import sha256
7
8
  from io import StringIO
8
9
  from itertools import pairwise
9
10
  import re
10
11
  import tokenize
11
- from typing import TYPE_CHECKING, Final, NamedTuple
12
+ from typing import TYPE_CHECKING, Final, Literal, NamedTuple
12
13
  from unicodedata import normalize
13
14
 
14
15
  from sarj_rule_contracts import TextEdit, apply_edits
@@ -80,6 +81,8 @@ _WALRUS_CODES = frozenset({"SARJ081", "SARJ432"})
80
81
  _JWT_POSITIONAL_PREFIX_ARGUMENTS = 3
81
82
  _STREAM_MIN_BODY_LENGTH = 3
82
83
  _CONSTANT_TRUE_DEPTH_LIMIT = 32
84
+ # Bound accepted revisions; each rule still owns its work and replacement size.
85
+ _MAX_FIX_PASSES: Final = 256
83
86
  _PURE_STRING_TRANSFORMS: Final[dict[str, str]] = {
84
87
  "strip": "stripped_value",
85
88
  "lower": "lowered_value",
@@ -1360,6 +1363,18 @@ _BUILDERS: Final[dict[str, Callable[[_Source, Diagnostic], list[TextEdit]]]] = {
1360
1363
  }
1361
1364
 
1362
1365
 
1366
+ @dataclass(frozen=True, slots=True)
1367
+ class _FixCandidate:
1368
+ code: str
1369
+ edits: tuple[TextEdit, ...]
1370
+
1371
+
1372
+ @dataclass(frozen=True, slots=True)
1373
+ class _FixPass:
1374
+ edits: tuple[TextEdit, ...]
1375
+ codes: tuple[str, ...]
1376
+
1377
+
1363
1378
  def fix_source(path: Path, source: str, *, rule_ids: Sequence[str] | None = None) -> str:
1364
1379
  from sarj_python_lint.__main__ import check_source # ruff: ignore[import-outside-top-level] -- the CLI imports this entry point lazily.
1365
1380
 
@@ -1371,6 +1386,8 @@ def fix_source(path: Path, source: str, *, rule_ids: Sequence[str] | None = None
1371
1386
  # Only the closed alias builder owns aligned physical-line buffers and token coordinates here.
1372
1387
  rules = [rule for rule in rules if rule.code == "SARJ531"]
1373
1388
  seen: set[bytes] = set()
1389
+ applied_codes: set[str] = set()
1390
+ passes = 0
1374
1391
  current = source
1375
1392
  while (fingerprint := sha256(current.encode("utf-8")).digest()) not in seen:
1376
1393
  seen.add(fingerprint)
@@ -1380,30 +1397,43 @@ def fix_source(path: Path, source: str, *, rule_ids: Sequence[str] | None = None
1380
1397
  if not candidates:
1381
1398
  return current
1382
1399
  # Batch independent candidates; overlapping candidates get fresh offsets on the next pass.
1383
- updated = apply_edits(current, _coherent_edits(candidates))
1400
+ selected_pass = _coherent_edits(candidates)
1401
+ updated = apply_edits(current, selected_pass.edits)
1384
1402
  compile(updated, str(path), "exec", dont_inherit=True)
1385
1403
  if updated == current:
1386
1404
  return current
1405
+ if passes == _MAX_FIX_PASSES:
1406
+ raise _convergence_error(path, reason="pass budget exhausted", passes=passes, codes=applied_codes)
1407
+ passes += 1
1408
+ applied_codes.update(selected_pass.codes)
1387
1409
  current = updated
1388
- msg = f"Python autofixes did not converge: {path}"
1389
- raise ValueError(msg)
1410
+ raise _convergence_error(path, reason="source cycle", passes=passes, codes=applied_codes)
1411
+
1412
+
1413
+ def _convergence_error(
1414
+ path: Path, *, reason: Literal["pass budget exhausted", "source cycle"], passes: int, codes: set[str]
1415
+ ) -> ValueError:
1416
+ participants = ", ".join(sorted(codes)) or "none"
1417
+ msg = f"Python autofixes did not converge: {path} ({reason}; passes={passes}; rules={participants})"
1418
+ return ValueError(msg)
1390
1419
 
1391
1420
 
1392
- def _candidate_edits(context: PythonFileContext, diagnostics: Sequence[Diagnostic]) -> list[list[TextEdit]]:
1421
+ def _candidate_edits(context: PythonFileContext, diagnostics: Sequence[Diagnostic]) -> list[_FixCandidate]:
1393
1422
  view = _Source(context, diagnostics)
1394
1423
  return [
1395
- edits
1424
+ _FixCandidate(diagnostic.code, tuple(edits))
1396
1425
  for diagnostic in diagnostics
1397
1426
  if (builder := _BUILDERS.get(diagnostic.code)) is not None
1398
1427
  if (edits := builder(view, diagnostic))
1399
1428
  ]
1400
1429
 
1401
1430
 
1402
- def _coherent_edits(candidates: list[list[TextEdit]]) -> list[TextEdit]:
1431
+ def _coherent_edits(candidates: list[_FixCandidate]) -> _FixPass:
1403
1432
  accepted: list[TextEdit] = []
1433
+ codes: list[str] = []
1404
1434
  known: set[TextEdit] = set()
1405
1435
  for candidate in candidates:
1406
- additions = sorted(set(candidate) - known)
1436
+ additions = sorted(set(candidate.edits) - known)
1407
1437
  if any(map(_overlap, additions, additions[1:], strict=False)):
1408
1438
  continue
1409
1439
  for edit in additions:
@@ -1416,7 +1446,9 @@ def _coherent_edits(candidates: list[list[TextEdit]]) -> list[TextEdit]:
1416
1446
  for edit in additions:
1417
1447
  insort(accepted, edit)
1418
1448
  known.update(additions)
1419
- return accepted
1449
+ if additions:
1450
+ codes.append(candidate.code)
1451
+ return _FixPass(tuple(accepted), tuple(dict.fromkeys(codes)))
1420
1452
 
1421
1453
 
1422
1454
  def _overlap(first: TextEdit, second: TextEdit) -> bool:
@@ -143,19 +143,24 @@ def check_parallel(
143
143
  for path, source in loaded.items()
144
144
  )
145
145
  for result in pool.map(_check_file, items, chunksize=1, buffersize=workers * 4):
146
- if result.needs_serial:
147
- # Drain the actual worker results so another initializer/file
148
- # failure cannot be disguised as a clean serial replay.
149
- needs_serial = True
146
+ # Drain actual results after either replay signal so a later worker
147
+ # failure cannot be disguised as a clean serial replay.
148
+ if needs_serial:
150
149
  continue
151
150
  resolutions.update(result.resolutions)
152
151
  modules.update(result.modules)
153
152
  # A worker cannot hit either order-dependent limit when the whole
154
153
  # union stays strictly below it. At the boundary, discard all output.
155
- if not needs_serial and (
156
- len(resolutions) >= LOCAL_SOURCE_CACHE_LIMIT or len(modules) >= LOCAL_SOURCE_CACHE_LIMIT
154
+ if (
155
+ result.needs_serial
156
+ or len(resolutions) >= LOCAL_SOURCE_CACHE_LIMIT
157
+ or len(modules) >= LOCAL_SOURCE_CACHE_LIMIT
157
158
  ):
158
- return None
159
+ needs_serial = True
160
+ diagnostics.clear()
161
+ resolutions.clear()
162
+ modules.clear()
163
+ continue
159
164
  diagnostics.extend(result.diagnostics)
160
165
  return None if needs_serial else diagnostics
161
166
  finally:
@@ -973,10 +973,8 @@ class CopyOnWriteAnalysis:
973
973
  summaries[key] = _summary_value(_OwnershipSummary(alternatives))
974
974
  bindings[name] = summaries[key]
975
975
 
976
- def _scan(self, statements: list[ast.stmt], bindings: dict[str, _Value]) -> None:
977
- for statement in statements:
978
- if self._statement(statement, bindings):
979
- return
976
+ def _scan(self, statements: list[ast.stmt], bindings: dict[str, _Value]) -> bool:
977
+ return any(self._statement(statement, bindings) for statement in statements)
980
978
 
981
979
  def _statement(self, statement: ast.stmt, bindings: dict[str, _Value]) -> bool:
982
980
  match statement:
@@ -991,7 +989,7 @@ class CopyOnWriteAnalysis:
991
989
  self._write(target, bindings)
992
990
  self._bind(target, None, bindings)
993
991
  case ast.If():
994
- self._branches(statement, bindings)
992
+ return self._branches(statement, bindings)
995
993
  case ast.For() | ast.AsyncFor():
996
994
  self._for(statement, bindings)
997
995
  case ast.Expr(value=ast.Call() as call):
@@ -1026,16 +1024,16 @@ class CopyOnWriteAnalysis:
1026
1024
  self._write(target, bindings)
1027
1025
  self._bind(target, value, bindings)
1028
1026
 
1029
- def _branches(self, statement: ast.If, bindings: dict[str, _Value]) -> None:
1027
+ def _branches(self, statement: ast.If, bindings: dict[str, _Value]) -> bool:
1030
1028
  self._expression(statement.test, bindings)
1031
1029
  branches: list[dict[str, _Value]] = []
1032
1030
  for positive, block in ((True, statement.body), (False, statement.orelse)):
1033
1031
  branch = dict(bindings)
1034
1032
  self._refine(statement.test, branch, positive=positive)
1035
- self._scan(block, branch)
1036
- if not block or not isinstance(block[-1], (ast.Return, ast.Raise)):
1033
+ if not self._scan(block, branch):
1037
1034
  branches.append(branch)
1038
1035
  self._join(branches, bindings)
1036
+ return not branches
1039
1037
 
1040
1038
  def _for(self, statement: ast.For | ast.AsyncFor, bindings: dict[str, _Value]) -> None:
1041
1039
  self._expression(statement.iter, bindings)
@@ -48,7 +48,7 @@ def dynamic_namespace_access(
48
48
  def _namespace_api_aliases(nodes: Iterable[ast.AST]) -> set[str]:
49
49
  aliases = set(_NAMESPACE_APIS) | {"__builtins__"}
50
50
  for node in nodes:
51
- if not isinstance(node, ast.ImportFrom) or node.module not in _MODULE_APIS:
51
+ if not isinstance(node, ast.ImportFrom) or node.level != 0 or node.module not in _MODULE_APIS:
52
52
  continue
53
53
  members = _MODULE_APIS[node.module]
54
54
  aliases.update(alias.asname or alias.name for alias in node.names if alias.name in members)
@@ -156,6 +156,11 @@ class SourceUnit:
156
156
  captured: CapturedSource | None = None
157
157
 
158
158
 
159
+ @dataclass(frozen=True, slots=True)
160
+ class _AmbiguousModuleSources:
161
+ paths: tuple[Path, ...]
162
+
163
+
159
164
  @dataclass(frozen=True, slots=True)
160
165
  class LoadedSource:
161
166
  path: Path
@@ -276,8 +281,18 @@ class ProjectIndexSet:
276
281
  self._bound_value_prefixes: dict[str, bool] = {}
277
282
  self._value_origins_incomplete = False
278
283
  self.import_namespace_mutations = ImportNamespaceMutations(WeakMethod(self._namespace_write_inventory))
279
- by_module = {unit.module: unit for unit in units.values() if unit.module is not None}
280
- self._by_module = MappingProxyType(by_module)
284
+ by_module: dict[str, dict[Path, SourceUnit]] = {}
285
+ for unit in units.values():
286
+ if unit.module is not None:
287
+ by_module.setdefault(unit.module, {})[unit.path] = unit
288
+ self._by_module = MappingProxyType(
289
+ {
290
+ module: next(iter(candidates.values()))
291
+ if len(candidates) == 1
292
+ else _AmbiguousModuleSources(tuple(candidates))
293
+ for module, candidates in by_module.items()
294
+ }
295
+ )
281
296
  classes: dict[SymbolRef, ClassSummary] = {}
282
297
  nominals: dict[str, set[SymbolRef]] = {}
283
298
  _collect_unit_symbols(units, classes, nominals)
@@ -429,7 +444,8 @@ class ProjectIndexSet:
429
444
  return False
430
445
 
431
446
  def source_unit(self, module: str) -> SourceUnit | None:
432
- return self._by_module.get(module)
447
+ source = self._by_module.get(module)
448
+ return source if isinstance(source, SourceUnit) else None
433
449
 
434
450
  def _namespace_write_inventory(self) -> Mapping[str, _ImportNamespaceWrites]:
435
451
  def resolve(origin: SymbolRef) -> tuple[SymbolRef, ...] | None:
@@ -308,7 +308,8 @@ class ResourceProvenance:
308
308
  or (
309
309
  isinstance(parent, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef)
310
310
  and node in parent.decorator_list
311
- and self.context.imports.resolved_qualified_name(node) in {"typing.final", "typing_extensions.final"}
311
+ and self.context.imports.resolved_qualified_name(node)
312
+ in {"typing.final", "typing_extensions.final", "dataclasses.dataclass"}
312
313
  )
313
314
  )
314
315
 
@@ -27,11 +27,18 @@ def is_store_module(path: Path) -> bool:
27
27
 
28
28
 
29
29
  def sql_string_value(node: ast.expr, *, interpolation_placeholder: str = " ") -> str | None:
30
+ try:
31
+ return _sql_string_value(node, interpolation_placeholder=interpolation_placeholder)
32
+ except RecursionError:
33
+ return None
34
+
35
+
36
+ def _sql_string_value(node: ast.expr, *, interpolation_placeholder: str) -> str | None:
30
37
  if isinstance(node, ast.Constant) and isinstance(node.value, str):
31
38
  return node.value
32
39
  if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
33
- left = sql_string_value(node.left, interpolation_placeholder=interpolation_placeholder)
34
- right = sql_string_value(node.right, interpolation_placeholder=interpolation_placeholder)
40
+ left = _sql_string_value(node.left, interpolation_placeholder=interpolation_placeholder)
41
+ right = _sql_string_value(node.right, interpolation_placeholder=interpolation_placeholder)
35
42
  if left is not None and right is not None:
36
43
  return left + right
37
44
  if isinstance(node, ast.JoinedStr):
@@ -46,7 +46,11 @@ class _MethodWrites:
46
46
 
47
47
  def store_queries(context: PythonFileContext) -> Iterator[StoreQuery]:
48
48
  if context.store_queries is None:
49
- context.store_queries = tuple(recover_store_queries(context))
49
+ try:
50
+ queries = tuple(recover_store_queries(context))
51
+ except RecursionError:
52
+ queries = ()
53
+ context.store_queries = queries
50
54
  return iter(context.store_queries)
51
55
 
52
56
 
@@ -85,7 +85,11 @@ def _is_query_context(node: ast.expr, parents: Mapping[ast.AST, ast.AST], roots:
85
85
 
86
86
 
87
87
  def _query_roots(
88
- tree: ast.Module, parents: Mapping[ast.AST, ast.AST], *, node_index: NodeIndex | None = None
88
+ tree: ast.Module,
89
+ parents: Mapping[ast.AST, ast.AST],
90
+ *,
91
+ context: PythonFileContext,
92
+ node_index: NodeIndex | None = None,
89
93
  ) -> frozenset[int]:
90
94
  binding_counts: dict[tuple[int, str], int] = {}
91
95
  binding_values: dict[tuple[int, str], ast.expr] = {}
@@ -99,11 +103,12 @@ def _query_roots(
99
103
 
100
104
  constructor_imports = _postgres_sql_constructors(tree, parents, node_index=node_index)
101
105
  _query_binding_values(tree, parents, binding_values, node_index=node_index)
106
+ has_local_classes = bool(nodes(tree, ast.ClassDef, index=node_index))
102
107
 
103
108
  roots: set[int] = set()
104
109
  for call in nodes(tree, ast.Call, index=node_index):
105
110
  argument = _query_argument(call)
106
- if argument is None:
111
+ if argument is None or (has_local_classes and _source_local_receiver(call, context)):
107
112
  continue
108
113
  owner = _scope(call, parents)
109
114
  constructors = _visible_constructors(owner, constructor_imports, parents, binding_counts, wildcard_scopes)
@@ -151,6 +156,21 @@ def _query_argument(call: ast.Call) -> ast.expr | None:
151
156
  return next((keyword.value for keyword in call.keywords if keyword.arg in _QUERY_KEYWORDS), None)
152
157
 
153
158
 
159
+ def _source_local_receiver(call: ast.Call, context: PythonFileContext) -> bool:
160
+ if not isinstance(call.func, ast.Attribute) or not isinstance(call.func.value, (ast.Name, ast.Call)):
161
+ return False
162
+ provenance = context.static_resource_provenance
163
+ receiver: ast.expr | None = call.func.value
164
+ if isinstance(receiver, ast.Name):
165
+ binding = provenance.runtime_binding(receiver.id, call)
166
+ receiver = provenance.assignment_value(binding, call) if isinstance(binding, ast.Name) else None
167
+ return (
168
+ isinstance(receiver, ast.Call)
169
+ and isinstance(receiver.func, ast.Name)
170
+ and isinstance(provenance.runtime_binding(receiver.func.id, receiver), ast.ClassDef)
171
+ )
172
+
173
+
154
174
  def _postgres_sql_constructors(
155
175
  tree: ast.Module, parents: Mapping[ast.AST, ast.AST], *, node_index: NodeIndex | None = None
156
176
  ) -> frozenset[_ConstructorImport]:
@@ -261,13 +281,16 @@ class ComplexPostgresQueryRequiresArchitectureReview(Rule):
261
281
  autofix=AutofixPolicy.NONE,
262
282
  limitations=(
263
283
  "Only statically recoverable SQL passed to execute, executemany, fetch, fetchrow, fetchval, or prepare in non-test modules with explicit PostgreSQL imports is analyzed, regardless of module filename.",
264
- "Execution receivers use a conventional database name or import-proven Psycopg ownership. Native execution additionally follows stable module/local SQL bindings and direct same-class query-forwarding wrappers; unknown receivers abstain.",
284
+ "Execution receivers use a conventional database name or import-proven Psycopg ownership. Conventional names are a heuristic and can warn on unrelated objects with matching methods. Native execution additionally follows stable module/local SQL bindings and direct same-class query-forwarding wrappers; unknown receivers abstain.",
285
+ "Modules mixing SQLite and PostgreSQL abstain for conventional receivers; import-proven Psycopg execution remains eligible.",
286
+ "Stable source-local class construction abstains from conventional receiver inference, including custom delegating instances unless native recovery independently proves execution.",
265
287
  "Conventional receivers follow one direct, unambiguous same-scope binding. Native Psycopg recovery may follow bounded stable lexical bindings, including module constants. Unexecuted constants, branch-local assignments, mutable attributes, containers and unknown dynamic composition abstain.",
266
- "Review covers derived relations in every SELECT (including LATERAL, CTEs, predicates and writes), four explicit or comma joins, six query stages, three joins plus four stages, or 25 projections plus two joins. A self join over the same explicit table also merits review at 16 projected expressions.",
288
+ "Review covers derived relations in every SELECT (including LATERAL, CTEs, predicates and writes), four explicit or comma joins (including parenthesized joined tables), six query stages, three joins plus four stages, or 25 projections plus two joins. A self join over the same explicit table also merits review at 16 projected expressions.",
267
289
  "Derived-relation review requires grouping, HAVING or windowing combined with joins or nested reads; expression density is not a separate trigger.",
268
- "Projection width alone is accepted, and UNION branches are measured independently; execution plans and production cardinality remain authoritative.",
269
- "ClickHouse and BigQuery syntax is excluded per query, including in mixed-backend modules.",
290
+ "Projection width alone is accepted, and UNION branches are measured independently. Set operators do not add read stages beyond their SELECT blocks; execution plans and production cardinality remain authoritative.",
291
+ "Conventional receivers exclude ClickHouse and BigQuery syntax per query, including in mixed-backend modules. Import-proven Psycopg execution uses PostgreSQL parsing even when identifiers or function names overlap another dialect.",
270
292
  "The rule does not infer optimizer behavior, materialization, performance, data-model quality, or datastore placement.",
293
+ "SQL parsing and static query recovery abstain on recursion limits; deeply nested source can withhold native recovery for the file.",
271
294
  ),
272
295
  examples=(
273
296
  RuleExample(
@@ -351,12 +374,17 @@ class ComplexPostgresQueryRequiresArchitectureReview(Rule):
351
374
  tree = context.tree
352
375
  if tree is None:
353
376
  return []
354
- if not _is_postgres_module(tree, node_index=context.node_index):
377
+ imports = _import_names(tree, node_index=context.node_index)
378
+ if not _has_import_prefix(imports, _POSTGRES_IMPORT_PREFIXES):
355
379
  return []
356
380
 
357
- diagnostics = _conventional_findings(context, self.code)
381
+ diagnostics = (
382
+ []
383
+ if _has_import_prefix(imports, _AMBIGUOUS_RELATIONAL_IMPORT_PREFIXES)
384
+ else _conventional_findings(context, self.code)
385
+ )
358
386
  for query in store_queries(context):
359
- signal = _query_architecture_signal(query.sql, path)
387
+ signal = _query_architecture_signal(query.sql, path, native_postgres=True)
360
388
  if signal is None:
361
389
  continue
362
390
  node = query.node
@@ -372,7 +400,7 @@ def _conventional_findings(context: PythonFileContext, code: str) -> list[Diagno
372
400
  tree = context.tree
373
401
  if tree is None:
374
402
  return []
375
- query_roots = _query_roots(tree, parents, node_index=context.node_index)
403
+ query_roots = _query_roots(tree, parents, context=context, node_index=context.node_index)
376
404
  diagnostics: list[Diagnostic] = []
377
405
  consumed: set[int] = set()
378
406
  for node in context.nodes(ast.Constant, ast.BinOp, ast.JoinedStr):
@@ -477,14 +505,13 @@ def _record_sql_constructors(
477
505
  )
478
506
 
479
507
 
480
- def _query_architecture_signal(text_value: str, path: Path) -> str | None:
508
+ def _query_architecture_signal(text_value: str, path: Path, *, native_postgres: bool = False) -> str | None:
481
509
  sql_without_noise = strip_sql_noise(text_value)
482
- if (
483
- _QUERY_SHAPE.search(sql_without_noise) is None
484
- or _CLICKHOUSE_SQL.search(sql_without_noise)
485
- or _BIGQUERY_SQL.search(sql_without_noise)
486
- or (is_store_module(path) and analytical_signal(sql_without_noise) is not None)
487
- ):
510
+ if _QUERY_SHAPE.search(sql_without_noise) is None:
511
+ return None
512
+ if not native_postgres and (_CLICKHOUSE_SQL.search(sql_without_noise) or _BIGQUERY_SQL.search(sql_without_noise)):
513
+ return None
514
+ if is_store_module(path) and analytical_signal(sql_without_noise) is not None:
488
515
  return None
489
516
  return _parse_signal(text_value)
490
517
 
@@ -512,8 +539,8 @@ def _architecture_signal(statement: exp.Expr) -> str | None:
512
539
  maximum_joins = _maximum_query_joins(statement)
513
540
  if maximum_joins >= _JOIN_LIMIT:
514
541
  return f"SELECT block has {maximum_joins} JOINs (4+ explicit JOINs or comma relations)"
515
- nested_queries = sum(1 for query in statement.walk() if isinstance(query, (exp.Select, exp.SetOperation)))
516
- nested_queries -= isinstance(statement, (exp.Select, exp.SetOperation))
542
+ nested_queries = sum(1 for query in statement.walk() if isinstance(query, exp.Select))
543
+ nested_queries -= isinstance(_derived_query(statement), exp.Select)
517
544
  if (derived_signal := _derived_architecture_signal(statement)) is not None:
518
545
  return derived_signal
519
546
  if nested_queries >= _STAGE_LIMIT:
@@ -528,7 +555,7 @@ def _projection_width_signal(statement: exp.Expr) -> str | None:
528
555
 
529
556
  for select in statement.find_all(exp.Select):
530
557
  projections = tuple(_expression_args(select, "expressions"))
531
- joins = tuple(join for join in _expression_args(select, "joins") if isinstance(join, exp.Join))
558
+ joins = _select_joins(select)
532
559
  if len(projections) >= _WIDE_PROJECTION_LIMIT and len(joins) >= _WIDE_PROJECTION_JOIN_LIMIT:
533
560
  return f"Joined query block has {len(projections)} projected expressions"
534
561
  if len(projections) >= _WIDE_SELF_JOIN_PROJECTION_LIMIT and _self_join(select, joins):
@@ -559,7 +586,10 @@ def _self_join(select: exp.Select, joins: tuple[exp.Join, ...]) -> bool:
559
586
  relations.extend(_expression_arg(join, "this") for join in joins)
560
587
  seen: set[tuple[str, ...]] = set()
561
588
  for relation in relations:
562
- key = relation_key(relation)
589
+ base = relation
590
+ while isinstance(base, (exp.Paren, exp.Subquery)):
591
+ base = _expression_arg(base, "this")
592
+ key = relation_key(base)
563
593
  if key is None or any(_SQL_HOLE in name for name in key):
564
594
  continue
565
595
  if key in seen:
@@ -572,9 +602,7 @@ def _derived_architecture_signal(statement: exp.Expr) -> str | None:
572
602
  from sqlglot import exp # ruff: ignore[import-outside-top-level] -- keep parser startup off unrelated lint runs
573
603
 
574
604
  for select in statement.find_all(exp.Select):
575
- for join in _expression_args(select, "joins"):
576
- if not isinstance(join, exp.Join):
577
- continue
605
+ for join in _select_joins(select):
578
606
  if (derived := _derived_query(_expression_arg(join, "this"))) is not None and _has_complex_body(derived):
579
607
  return "JOIN-derived query combines grouping, HAVING or windowing with joins or nested reads"
580
608
  from_clause = _expression_arg(select, "from_")
@@ -597,11 +625,11 @@ def _has_complex_body(query: exp.Query) -> bool:
597
625
  if not isinstance(query, exp.Select):
598
626
  return False
599
627
  if _has_row_transform(query) and (
600
- _expression_args(query, "joins") or any(select is not query for select in query.find_all(exp.Select))
628
+ _select_joins(query) or any(select is not query for select in query.find_all(exp.Select))
601
629
  ):
602
630
  return True
603
- for relation in query.find_all(exp.From, exp.Join):
604
- if relation.parent is not query:
631
+ for relation in (_expression_arg(query, "from_"), *_select_joins(query)):
632
+ if not isinstance(relation, (exp.From, exp.Join)):
605
633
  continue
606
634
  child = _derived_query(_expression_arg(relation, "this"))
607
635
  if child is not None and (_has_row_transform(child) or _has_complex_body(child)):
@@ -624,14 +652,26 @@ def _maximum_query_joins(statement: exp.Expr) -> int:
624
652
  from sqlglot import exp # ruff: ignore[import-outside-top-level] -- keep parser startup off unrelated lint runs
625
653
 
626
654
  return max(
627
- (
628
- sum(isinstance(join, exp.Join) for join in _expression_args(select, "joins"))
629
- for select in statement.find_all(exp.Select)
630
- ),
655
+ (len(_select_joins(select)) for select in statement.find_all(exp.Select)),
631
656
  default=0,
632
657
  )
633
658
 
634
659
 
660
+ def _select_joins(select: exp.Select) -> tuple[exp.Join, ...]:
661
+ from sqlglot import exp # ruff: ignore[import-outside-top-level] -- grouped joined tables retain their SELECT owner.
662
+
663
+ relations = (_expression_arg(select, "from_"), *_expression_args(select, "joins"))
664
+ return tuple(
665
+ node
666
+ for relation in relations
667
+ if relation is not None
668
+ for node in relation.walk(
669
+ prune=lambda node: isinstance(node, (exp.Select, exp.SetOperation, exp.Insert, exp.Update, exp.Delete))
670
+ )
671
+ if isinstance(node, exp.Join)
672
+ )
673
+
674
+
635
675
  def _count_import_bindings(
636
676
  tree: ast.Module,
637
677
  parents: Mapping[ast.AST, ast.AST],
@@ -671,13 +711,6 @@ def _query_review_guidance(text_value: str) -> str:
671
711
  )
672
712
 
673
713
 
674
- def _is_postgres_module(tree: ast.Module, *, node_index: NodeIndex | None = None) -> bool:
675
- imports = _import_names(tree, node_index=node_index)
676
- return _has_import_prefix(imports, _POSTGRES_IMPORT_PREFIXES) and not _has_import_prefix(
677
- imports, _AMBIGUOUS_RELATIONAL_IMPORT_PREFIXES
678
- )
679
-
680
-
681
714
  def _has_import_prefix(imports: frozenset[str], prefixes: tuple[str, ...]) -> bool:
682
715
  return any(name == prefix or name.startswith(f"{prefix}.") for name in imports for prefix in prefixes)
683
716
 
@@ -60,15 +60,16 @@ def analytical_signal(sql: str) -> str | None:
60
60
 
61
61
 
62
62
  def _grouped_reporting_signal(sql: str) -> str | None:
63
- import sqlglot # ruff: ignore[import-outside-top-level] -- parse only aggregate-shaped SQL after cheap gates
64
63
  from sqlglot import exp # ruff: ignore[import-outside-top-level] -- paired with lazy parser import
65
64
  from sqlglot.errors import SqlglotError # ruff: ignore[import-outside-top-level] -- paired with lazy parser import
66
65
 
66
+ from sarj_python_lint.rules._postgres import parse_postgres # ruff: ignore[import-outside-top-level] -- unsupported private SQL must not enter dependency logs.
67
+
67
68
  if _GROUP_BY.search(sql) is None or _COMMON_AGGREGATE.search(sql) is None:
68
69
  return None
69
70
  try:
70
- statements = sqlglot.parse(_PLACEHOLDER.sub("?", sql), read="postgres")
71
- except SqlglotError:
71
+ statements = parse_postgres(_PLACEHOLDER.sub("?", sql))
72
+ except SqlglotError, RecursionError:
72
73
  return None
73
74
  for statement in statements:
74
75
  if not isinstance(statement, exp.Query):