sarj-python-lint 1.88.0__tar.gz → 1.88.20__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 (302) hide show
  1. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_fastapi.py +185 -10
  4. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_opaque_contracts.py +96 -4
  5. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +5 -5
  6. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +59 -21
  7. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_precise_object_contract.py +2 -0
  8. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_typed_cli.py +15 -1
  9. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/.gitignore +0 -0
  10. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/LICENSE +0 -0
  11. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/README.md +0 -0
  12. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/__init__.py +0 -0
  13. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/__main__.py +0 -0
  14. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_analysis_session.py +0 -0
  15. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix.py +0 -0
  16. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_aliases.py +0 -0
  17. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_annotations.py +0 -0
  18. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_comments.py +0 -0
  19. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_declared_contracts.py +0 -0
  20. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_declared_tests.py +0 -0
  21. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_docs.py +0 -0
  22. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_enums.py +0 -0
  23. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_exhaustiveness.py +0 -0
  24. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_inert.py +0 -0
  25. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_parameters.py +0 -0
  26. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_repeated.py +0 -0
  27. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_security.py +0 -0
  28. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_sql_projection.py +0 -0
  29. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_autofix_tests.py +0 -0
  30. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_file_context.py +0 -0
  31. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_filesystem.py +0 -0
  32. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_fixes.py +0 -0
  33. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_parallel.py +0 -0
  34. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_prepared_sources.py +0 -0
  35. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_python_target.py +0 -0
  36. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  37. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_secret_names.py +0 -0
  38. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_source.py +0 -0
  39. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/_version.py +0 -0
  40. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/application_logging.py +0 -0
  41. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/autofix.py +0 -0
  42. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/benchmark.py +0 -0
  43. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/cast_access.py +0 -0
  44. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/interpreter_argv.py +0 -0
  45. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/json_boundary.py +0 -0
  46. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/py.typed +0 -0
  47. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/ratchet.py +0 -0
  48. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rule_base.py +0 -0
  49. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/__init__.py +0 -0
  50. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_annotation_origins.py +0 -0
  51. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  52. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  53. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_ast_names.py +0 -0
  54. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  55. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_async_lifecycle.py +0 -0
  56. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_binding_inventory.py +0 -0
  57. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  58. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_comment_narration.py +0 -0
  59. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_comments.py +0 -0
  60. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  61. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  62. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
  63. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_declared_contracts.py +0 -0
  64. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  65. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  66. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_exports.py +0 -0
  67. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_first_party.py +0 -0
  68. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  69. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_imports.py +0 -0
  70. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  71. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  72. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_local_alias_fix.py +0 -0
  73. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_local_source.py +0 -0
  74. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_logging.py +0 -0
  75. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
  76. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  77. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  78. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_native_object_contracts.py +0 -0
  79. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  80. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_numeric_proof.py +0 -0
  81. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_paths.py +0 -0
  82. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_postgres.py +0 -0
  83. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_project_index.py +0 -0
  84. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  85. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  86. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_pytest.py +0 -0
  87. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_registry.py +0 -0
  88. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  89. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  90. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
  91. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  92. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_service_http_callbacks.py +0 -0
  93. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  94. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_sql.py +0 -0
  95. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_sql_aggregate_identity.py +0 -0
  96. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_sql_explain.py +0 -0
  97. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_sql_policy_source.py +0 -0
  98. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  99. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_store_sql.py +0 -0
  100. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  101. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  102. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  103. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  104. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  105. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_transactional_sql.py +0 -0
  106. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  107. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  108. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/async_executor_shutdown.py +0 -0
  109. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  110. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/async_subprocess_cancellation_cleanup.py +0 -0
  111. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/cancel_owned_task_without_join.py +0 -0
  112. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  113. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  114. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
  115. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  116. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  117. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  118. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  119. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py +0 -0
  120. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  121. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/failed_probe_zero_threshold.py +0 -0
  122. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  123. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  124. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  125. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  126. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  127. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/nan_hidden_by_builtin_min.py +0 -0
  128. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  129. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  130. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  131. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  132. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  133. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
  134. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  135. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  136. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  137. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  138. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  139. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  140. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  141. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +0 -0
  142. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_custom_control_flow_exception.py +0 -0
  143. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  144. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
  145. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  146. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_discarded_tagged_outcome.py +0 -0
  147. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
  148. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +0 -0
  149. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  150. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  151. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
  152. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +0 -0
  153. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  154. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  155. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  156. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  157. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  158. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  159. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +0 -0
  160. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  161. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  162. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
  163. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  164. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  165. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  166. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  167. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  168. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  169. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  170. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  171. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  172. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  173. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  174. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
  175. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  176. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  177. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  178. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  179. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  180. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  181. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  182. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  183. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  184. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_recording_store.py +0 -0
  185. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  186. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_redundant_local_alias.py +0 -0
  187. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  188. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_redundant_sql_alias.py +0 -0
  189. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  190. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  191. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  192. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  193. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  194. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  195. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  196. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  197. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +0 -0
  198. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  199. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  200. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  201. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  202. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  203. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  204. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  205. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  206. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  207. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  208. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  209. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  210. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  211. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
  212. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  213. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  214. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  215. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_application_model_joins.py +0 -0
  216. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  217. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  218. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  219. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  220. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  221. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  222. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_direct_import_module.py +0 -0
  223. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  224. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  225. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_grouped_settings.py +0 -0
  226. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  227. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  228. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  229. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  230. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  231. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  232. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  233. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  234. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  235. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  236. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  237. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  238. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  239. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  240. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_optional_composite.py +0 -0
  241. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  242. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  243. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  244. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_pydantic_model_assertion.py +0 -0
  245. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_pytest_dependency_fixtures.py +0 -0
  246. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  247. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  248. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  249. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_runtime_validation.py +0 -0
  250. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  251. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  252. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  253. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  254. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  255. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_typed_mapping_get.py +0 -0
  256. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_typed_settings_field.py +0 -0
  257. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  258. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  259. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  260. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  261. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  262. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  263. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/pytest_fixture_returns_closed_resource.py +0 -0
  264. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  265. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  266. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  267. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  268. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  269. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  270. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  271. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  272. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +0 -0
  273. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  274. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  275. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  276. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  277. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  278. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  279. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  280. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  281. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  282. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  283. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
  284. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  285. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  286. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  287. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  288. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/stepdown.py +0 -0
  289. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
  290. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  291. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/store_insert_requires_explicit_conflict_policy.py +0 -0
  292. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  293. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/store_mutation_command_fanout.py +0 -0
  294. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  295. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  296. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  297. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  298. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  299. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  300. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  301. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
  302. {sarj_python_lint-1.88.0 → sarj_python_lint-1.88.20}/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.88.0
3
+ Version: 1.88.20
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.88.0"
3
+ version = "1.88.20"
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" }]
@@ -3,6 +3,10 @@ from __future__ import annotations
3
3
  import ast
4
4
  from collections import Counter
5
5
  from dataclasses import dataclass
6
+ from functools import cached_property
7
+ from http import HTTPStatus
8
+ import re
9
+ from types import MappingProxyType
6
10
  from typing import TYPE_CHECKING, Literal, NamedTuple
7
11
 
8
12
  from sarj_python_lint._source import read_python_source
@@ -19,19 +23,24 @@ HTTP_METHODS = frozenset({"get", "post", "put", "patch", "delete", "options", "h
19
23
  PARAM_MARKERS = frozenset({"Path", "Query", "Header", "Cookie", "Body", "Form", "File", "Depends", "Security"})
20
24
  SCHEMA_MARKERS = PARAM_MARKERS - {"Depends", "Security"}
21
25
  INJECTION_TYPES = frozenset({"Request", "Response", "WebSocket", "HTTPConnection", "BackgroundTasks", "SecurityScopes"})
22
- RESPONSE_TYPES = frozenset(
26
+ _RESPONSE_DEFAULT_STATUSES = MappingProxyType(
23
27
  {
24
- "Response",
25
- "JSONResponse",
26
- "HTMLResponse",
27
- "PlainTextResponse",
28
- "RedirectResponse",
29
- "StreamingResponse",
30
- "FileResponse",
31
- "ORJSONResponse",
32
- "UJSONResponse",
28
+ "Response": HTTPStatus.OK,
29
+ "JSONResponse": HTTPStatus.OK,
30
+ "HTMLResponse": HTTPStatus.OK,
31
+ "PlainTextResponse": HTTPStatus.OK,
32
+ "RedirectResponse": HTTPStatus.TEMPORARY_REDIRECT,
33
+ "StreamingResponse": HTTPStatus.OK,
34
+ "FileResponse": HTTPStatus.OK,
35
+ "ORJSONResponse": HTTPStatus.OK,
36
+ "UJSONResponse": HTTPStatus.OK,
33
37
  }
34
38
  )
39
+ RESPONSE_TYPES = frozenset(_RESPONSE_DEFAULT_STATUSES)
40
+ _STARLETTE_RESPONSE_TYPES = RESPONSE_TYPES - {"ORJSONResponse", "UJSONResponse"}
41
+ _JSON_RESPONSE_TYPES = frozenset({"JSONResponse", "ORJSONResponse", "UJSONResponse"})
42
+ NO_BODY_STATUSES = frozenset({204, 205, 304})
43
+ _STATUS_MEMBER = re.compile(r"HTTP_(\d{3})_[A-Z0-9_]+")
35
44
  _DEPENDENCY_MARKERS = frozenset({"Depends", "Security"})
36
45
  _CONSTRUCTOR_SOURCES = frozenset({"fastapi", "fastapi.applications", "fastapi.routing"})
37
46
  _MAX_IMPORTED_MODULE_BYTES = 500_000
@@ -50,6 +59,7 @@ class Route:
50
59
  inherited_responses: ast.expr | None = None
51
60
  inherited_response_class: ast.expr | None = None
52
61
  inherited_prefix: ast.expr | None = None
62
+ inherited_has_unpack: bool = False
53
63
 
54
64
  @property
55
65
  def keywords(self) -> dict[str, ast.expr]:
@@ -154,6 +164,7 @@ class FastapiIndex:
154
164
  self.receiver_kinds: dict[_ReceiverKey, Literal["FastAPI", "APIRouter"]] = {}
155
165
  self.hidden_receivers: set[_ReceiverKey] = set()
156
166
  self.receiver_defaults: dict[_ReceiverKey, dict[str, ast.expr]] = {}
167
+ self.unpacked_receivers: set[_ReceiverKey] = set()
157
168
  self.decorators: dict[_ReceiverKey, _DecoratorBinding] = {}
158
169
  self.bound_names: set[_ReceiverKey] = set()
159
170
  self._node_scopes: dict[int, int] = {}
@@ -394,6 +405,8 @@ class FastapiIndex:
394
405
  for keyword in value.keywords
395
406
  if keyword.arg in {"responses", "default_response_class", "prefix"}
396
407
  }
408
+ if any(keyword.arg is None for keyword in value.keywords):
409
+ self.unpacked_receivers.add(key)
397
410
  if self._constructor_is_hidden(value):
398
411
  self.hidden_receivers.add(key)
399
412
 
@@ -497,6 +510,7 @@ class FastapiIndex:
497
510
  inherited_responses=defaults.get("responses"),
498
511
  inherited_response_class=defaults.get("default_response_class"),
499
512
  inherited_prefix=defaults.get("prefix"),
513
+ inherited_has_unpack=origin in self.unpacked_receivers,
500
514
  )
501
515
  for route_method in route_methods
502
516
  )
@@ -541,6 +555,167 @@ class FastapiIndex:
541
555
  case _:
542
556
  return ""
543
557
 
558
+ def status_code(self, node: ast.expr | None) -> int | None:
559
+ if isinstance(node, ast.Constant) and isinstance(node.value, int):
560
+ return node.value
561
+ if not isinstance(node, (ast.Name, ast.Attribute)) or self._is_shadowed_in_enclosing_scope(node):
562
+ return None
563
+ qualified = self.imports.resolved_qualified_name(node) or ""
564
+ module, _, member = qualified.rpartition(".")
565
+ if (
566
+ module == "starlette.status"
567
+ and module not in self._unstable_native_providers
568
+ and (match := _STATUS_MEMBER.fullmatch(member))
569
+ ):
570
+ return int(match.group(1))
571
+ if not isinstance(node, ast.Attribute):
572
+ return None
573
+ owner = self._status_provider(node.value)
574
+ if owner in self._unstable_native_providers:
575
+ return None
576
+ if owner == "starlette.status" and (match := _STATUS_MEMBER.fullmatch(node.attr)):
577
+ return int(match.group(1))
578
+ if owner == "http.HTTPStatus":
579
+ status = HTTPStatus.__members__.get(node.attr)
580
+ return status.value if status is not None else None
581
+ return None
582
+
583
+ def operation_status_code(self, route: Route) -> int | None:
584
+ node = route.keywords.get("status_code")
585
+ if node is None and route.has_unpack:
586
+ return None
587
+ if node is None or (isinstance(node, ast.Constant) and node.value is None):
588
+ if not self.response_classes_are_stable:
589
+ return None
590
+ if route.has_unpack and "response_class" not in route.keywords:
591
+ return None
592
+ response_class = route.keywords.get("response_class") or route.inherited_response_class
593
+ if response_class is None:
594
+ return None if route.inherited_has_unpack else HTTPStatus.OK
595
+ response_name = self.response_class_name(response_class)
596
+ return _RESPONSE_DEFAULT_STATUSES.get(response_name) if response_name is not None else None
597
+ return self.status_code(node)
598
+
599
+ def response_class_name(self, node: ast.expr) -> str | None:
600
+ if not self.response_classes_are_stable:
601
+ return None
602
+ return self._native_response_name(self._response_import(node))
603
+
604
+ def can_infer_json_response(self, route: Route) -> bool:
605
+ if route.has_unpack:
606
+ return False
607
+ status = self.operation_status_code(route)
608
+ if status is None or status in NO_BODY_STATUSES:
609
+ return False
610
+ response_class = route.keywords.get("response_class") or route.inherited_response_class
611
+ if response_class is None:
612
+ return not route.inherited_has_unpack and self.response_classes_are_stable
613
+ return self.response_class_name(response_class) in _JSON_RESPONSE_TYPES
614
+
615
+ @property
616
+ def response_classes_are_stable(self) -> bool:
617
+ return "responses" not in self._unstable_native_providers
618
+
619
+ @staticmethod
620
+ def _native_response_name(qualified: str | None) -> str | None:
621
+ if qualified is None:
622
+ return None
623
+ module, _, name = qualified.rpartition(".")
624
+ if module == "fastapi" and name == "Response":
625
+ return name
626
+ if module == "fastapi.responses" and name in RESPONSE_TYPES:
627
+ return name
628
+ if module == "starlette.responses" and name in _STARLETTE_RESPONSE_TYPES:
629
+ return name
630
+ return None
631
+
632
+ def _response_import(self, node: ast.expr) -> str | None:
633
+ if not isinstance(node, (ast.Name, ast.Attribute)) or self._is_shadowed_in_enclosing_scope(node):
634
+ return None
635
+ qualified = self.imports.resolved_qualified_name(node)
636
+ if isinstance(node, ast.Name) and qualified is None:
637
+ binding = self.imports.bindings.get(node.id)
638
+ return binding.module if binding is not None and binding.symbol is None else None
639
+ if isinstance(node, ast.Attribute) and qualified is None:
640
+ module = self.imports.resolved_qualified_name(node.value)
641
+ return f"{module}.{node.attr}" if module is not None else None
642
+ return qualified
643
+
644
+ def _status_provider(self, node: ast.expr) -> str | None:
645
+ if not isinstance(node, (ast.Name, ast.Attribute)) or self._is_shadowed_in_enclosing_scope(node):
646
+ return None
647
+ qualified = self.imports.resolved_qualified_name(node)
648
+ if isinstance(node, ast.Name) and qualified is None:
649
+ binding = self.imports.bindings.get(node.id)
650
+ qualified = (
651
+ binding.module
652
+ if binding is not None and binding.symbol is None and binding.module != "fastapi.status"
653
+ else None
654
+ )
655
+ # FastAPI exports Starlette's status module rather than a status submodule.
656
+ if qualified == "fastapi.status":
657
+ return "starlette.status"
658
+ return qualified if qualified in {"starlette.status", "http.HTTPStatus"} else None
659
+
660
+ @cached_property
661
+ def _unstable_native_providers(self) -> frozenset[str]:
662
+ providers: set[str] = set()
663
+ route_calls: set[int] = set()
664
+ for node in walk_ast(self.tree):
665
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
666
+ route_calls.update(id(route.decorator) for route in self.routes(node))
667
+ continue
668
+ values = self._provider_values(node)
669
+ if not values:
670
+ continue
671
+ for value in values:
672
+ if (provider := self._status_provider(value)) is not None:
673
+ providers.add(provider)
674
+ for value in self._response_provider_values(node, values, route_calls):
675
+ qualified = self._response_import(value)
676
+ if qualified in {"fastapi.responses", "starlette.responses"} or self._native_response_name(qualified):
677
+ providers.add("responses")
678
+ return frozenset(providers)
679
+
680
+ @staticmethod
681
+ def _provider_values(node: ast.AST) -> tuple[ast.expr, ...]:
682
+ match node:
683
+ case ast.Attribute(value=value, ctx=ast.Store() | ast.Del()):
684
+ return node, value
685
+ case ast.Assign(value=value) | ast.AnnAssign(value=ast.expr() as value) | ast.NamedExpr(value=value):
686
+ return (value,)
687
+ case ast.Return(value=ast.expr() as value):
688
+ return (value,)
689
+ case ast.Call(args=args, keywords=keywords):
690
+ return (*args, *(keyword.value for keyword in keywords))
691
+ case _:
692
+ return ()
693
+
694
+ def _response_provider_values(
695
+ self, node: ast.AST, values: tuple[ast.expr, ...], route_calls: set[int]
696
+ ) -> tuple[ast.expr, ...]:
697
+ if not isinstance(node, ast.Call):
698
+ return values
699
+ trusted_keyword = self._trusted_response_keyword(node, route_calls)
700
+ return (*node.args, *(keyword.value for keyword in node.keywords if keyword.arg != trusted_keyword))
701
+
702
+ def _trusted_response_keyword(self, node: ast.Call, route_calls: set[int]) -> str | None:
703
+ trusted_keyword = "response_class" if id(node) in route_calls else None
704
+ if (
705
+ any(keyword.arg == "default_response_class" for keyword in node.keywords)
706
+ and self.constructor_kind(node) is not None
707
+ ):
708
+ return "default_response_class"
709
+ if not isinstance(node.func, ast.Attribute) or node.func.attr not in {"include_router", "add_api_route"}:
710
+ return trusted_keyword
711
+ scope = self._node_scopes.get(id(node), 0)
712
+ receiver = _binding_name(node.func.value)
713
+ if receiver.startswith("self."):
714
+ scope = self._node_classes.get(id(node)) or scope
715
+ if self._receiver_key(scope, receiver) is None:
716
+ return trusted_keyword
717
+ return "default_response_class" if node.func.attr == "include_router" else "response_class"
718
+
544
719
  @staticmethod
545
720
  def _root_name(node: ast.Attribute) -> str:
546
721
  value: ast.expr = node
@@ -192,7 +192,7 @@ class OpaqueContractFacts:
192
192
 
193
193
  def _find_checked(self, function: _Function) -> frozenset[ast.arg]:
194
194
  found = set(self.native.namespace(function).parameters())
195
- terminal_rejection = _rejects(function.body)
195
+ terminal_rejection = self._rejects(function.body)
196
196
  for node in direct_nodes(function):
197
197
  if not self.reachable(node, function):
198
198
  continue
@@ -206,8 +206,85 @@ class OpaqueContractFacts:
206
206
  found.update(self._statement_parameters(node, terminal_rejection=terminal_rejection))
207
207
  found.update(self._identity_parameters(function))
208
208
  found.update(self._key_parameters(function))
209
+ found.update(self._model_iteration_parameters(function))
209
210
  return frozenset(found)
210
211
 
212
+ def _model_iteration_parameters(self, function: _Function) -> set[ast.arg]:
213
+ found: set[ast.arg] = set()
214
+ for parameter in (*function.args.posonlyargs, *function.args.args, *function.args.kwonlyargs):
215
+ if not self._opaque_iterable(parameter):
216
+ continue
217
+ loops = self._parameter_iterations(parameter, function)
218
+ if loops and all(self._validated_iteration(loop, function) for loop in loops):
219
+ found.add(parameter)
220
+ return found
221
+
222
+ def _opaque_iterable(self, parameter: ast.arg) -> bool:
223
+ if parameter.annotation is None:
224
+ return False
225
+ resolved = self.facts.resolve_annotation(parameter.annotation)
226
+ if resolved is None or not isinstance(resolved.expression, ast.Subscript):
227
+ return False
228
+ node = resolved.expression
229
+ return self.facts.qualified(node.value, resolved.at) in {
230
+ "collections.abc.Iterable",
231
+ "typing.Iterable",
232
+ } and _unknown_slot(node.slice, self.facts, resolved.at)
233
+
234
+ def _parameter_iterations(self, parameter: ast.arg, function: _Function) -> list[ast.For] | None:
235
+ loops: list[ast.For] = []
236
+ members = set(direct_nodes(function))
237
+ for read in self.facts.provenance.reads.get(parameter.arg, ()):
238
+ runtime = self.facts.provenance.runtime_binding(read.id, read)
239
+ if runtime is not parameter and self.facts.provenance.binding(read.id, read) is not parameter:
240
+ continue
241
+ if not self.reachable(read, function):
242
+ continue
243
+ owner = self.context.parents.get(read)
244
+ if (
245
+ runtime is not parameter
246
+ or not isinstance(owner, ast.For)
247
+ or owner not in members
248
+ or owner.iter is not read
249
+ or not isinstance(owner.target, ast.Name)
250
+ ):
251
+ return None
252
+ loops.append(owner)
253
+ return loops
254
+
255
+ def _validated_iteration(self, loop: ast.For, function: _Function) -> bool:
256
+ if not isinstance(loop.target, ast.Name):
257
+ return False
258
+ reads = [
259
+ read
260
+ for read in self.facts.provenance.reads.get(loop.target.id, ())
261
+ if (
262
+ self.facts.provenance.runtime_binding(read.id, read) is loop.target
263
+ or self.facts.provenance.binding(read.id, read) is loop.target
264
+ )
265
+ and self.reachable(read, function)
266
+ ]
267
+ members = set(direct_nodes(loop))
268
+ return bool(reads) and all(
269
+ self.facts.provenance.runtime_binding(read.id, read) is loop.target
270
+ and read in members
271
+ and self._model_element_read(read)
272
+ for read in reads
273
+ )
274
+
275
+ def _model_element_read(self, read: ast.Name) -> bool:
276
+ call = self.context.parents.get(read)
277
+ if isinstance(call, ast.keyword):
278
+ call = self.context.parents.get(call)
279
+ if not isinstance(call, ast.Call) or not isinstance(call.func, ast.Attribute):
280
+ return False
281
+ arguments = call_arguments(call, ("obj",))
282
+ if call.func.attr != "model_validate" or arguments is None or arguments.get("obj") is not read:
283
+ return False
284
+ contracts = DeclaredContracts(self.context)
285
+ model = contracts.class_type(call.func.value)
286
+ return model is not None and contracts.inherits(model, _VALIDATION_BASES)
287
+
211
288
  def _key_parameters(self, function: _Function) -> set[ast.arg]:
212
289
  found: set[ast.arg] = set()
213
290
  for parameter in (*function.args.posonlyargs, *function.args.args, *function.args.kwonlyargs):
@@ -337,7 +414,7 @@ class OpaqueContractFacts:
337
414
  return set()
338
415
 
339
416
  def _if_parameters(self, node: ast.If) -> set[ast.arg]:
340
- if _rejects(node.body):
417
+ if self._rejects(node.body):
341
418
  candidate = node.test
342
419
  if isinstance(candidate, ast.BoolOp) and isinstance(candidate.op, ast.Or):
343
420
  candidate = candidate.values[0]
@@ -345,7 +422,7 @@ class OpaqueContractFacts:
345
422
  return {parameter}
346
423
  if isinstance(candidate, ast.UnaryOp) and isinstance(candidate.op, ast.Not):
347
424
  return self._predicate(candidate.operand)
348
- return self._predicate(node.test) if _rejects(node.orelse) else set()
425
+ return self._predicate(node.test) if self._rejects(node.orelse) else set()
349
426
 
350
427
  def _match_parameters(self, node: ast.Match) -> set[ast.arg]:
351
428
  if not node.cases:
@@ -355,7 +432,7 @@ class OpaqueContractFacts:
355
432
  isinstance(last.pattern, ast.MatchAs)
356
433
  and last.pattern.name is None
357
434
  and last.guard is None
358
- and _rejects(last.body)
435
+ and self._rejects(last.body)
359
436
  ):
360
437
  return set()
361
438
  parameter = self.parameter(node.subject)
@@ -363,6 +440,21 @@ class OpaqueContractFacts:
363
440
  return {parameter}
364
441
  return set()
365
442
 
443
+ def _rejects(self, body: list[ast.stmt]) -> bool:
444
+ if _rejects(body):
445
+ return True
446
+ if not body or not isinstance(body[-1], ast.Expr) or not isinstance(body[-1].value, ast.Call):
447
+ return False
448
+ source = self._source(body[-1].value.func)
449
+ if source is None:
450
+ return False
451
+ _owner, function = source
452
+ return (
453
+ bool(function.body)
454
+ and isinstance(function.body[-1], ast.Raise)
455
+ and not any(isinstance(node, ast.Return | ast.Yield | ast.YieldFrom) for node in direct_nodes(function))
456
+ )
457
+
366
458
  def _predicate(self, expression: ast.expr) -> set[ast.arg]:
367
459
  if isinstance(expression, ast.BoolOp):
368
460
  return self._predicate(expression.values[0])
@@ -27,7 +27,6 @@ if TYPE_CHECKING:
27
27
  from sarj_python_lint.rules._ast_index import NodeIndex
28
28
 
29
29
 
30
- _NO_BODY = frozenset({204, 304})
31
30
  _NON_JSON_RESPONSES = frozenset(
32
31
  {"FileResponse", "HTMLResponse", "PlainTextResponse", "RedirectResponse", "StreamingResponse"}
33
32
  )
@@ -56,7 +55,9 @@ class FastapiClassRouterContract(Rule):
56
55
  autofix=AutofixPolicy.NONE,
57
56
  limitations=(
58
57
  "Tests, generated code, and hidden routes are excluded. FastAPI application construction and include_router composition are allowed.",
59
- "Response-model checks exclude 204/304 responses and explicit stream/file/HTML/text/redirect responses.",
58
+ "Response-model checks exclude literal or import-proven 204/205/304 statuses and proven non-JSON response classes, including raw Response.",
59
+ "Unpacked metadata or an explicit unresolved status cannot prove a JSON response; receiver ownership remains checked while response-model inference abstains.",
60
+ "Response classes require unshadowed native import provenance; direct provider writes/escapes, custom classes, and unresolved defaults disable JSON-model inference.",
60
61
  ),
61
62
  examples=(
62
63
  RuleExample(
@@ -211,12 +212,11 @@ def _response_problem(
211
212
  function: ast.FunctionDef | ast.AsyncFunctionDef, route: Route, index: FastapiIndex
212
213
  ) -> str | None:
213
214
  keywords = route.keywords
214
- status = keywords.get("status_code")
215
- if isinstance(status, ast.Constant) and status.value in _NO_BODY:
216
- return None
217
215
  response_class = keywords.get("response_class") or route.inherited_response_class
218
216
  if response_class is not None and flat_name(response_class) in _NON_JSON_RESPONSES:
219
217
  return None
218
+ if not index.can_infer_json_response(route):
219
+ return None
220
220
  response_model = keywords.get("response_model")
221
221
  if response_model is None or (isinstance(response_model, ast.Constant) and response_model.value is None):
222
222
  return "JSON operation requires explicit `response_model=NamedResponse`"
@@ -20,6 +20,7 @@ from sarj_python_lint.rule_base import (
20
20
  )
21
21
  from sarj_python_lint.rules._ast_index import children
22
22
  from sarj_python_lint.rules._fastapi import (
23
+ NO_BODY_STATUSES,
23
24
  SCHEMA_MARKERS,
24
25
  FastapiIndex,
25
26
  Route,
@@ -89,6 +90,9 @@ class FastapiExplicitOpenapiContract(Rule):
89
90
  limitations=(
90
91
  "Hidden routes, WebSocket handlers, tests, generated files, documentation-source examples, and unrelated decorators are excluded.",
91
92
  "Dynamic response mappings are accepted when their contents cannot be resolved statically.",
93
+ "Response-body/model inference abstains for unresolved status/class metadata, direct native-provider writes/escapes, and opaque unpacked defaults; no-body JSON metadata includes 205.",
94
+ "Direct-status alias/callee/catch recognition retains its existing limits; broader coverage requires separate reachability and runtime-dispatch evidence.",
95
+ "Route conflicts require unconditional registrations on the same local router; conditional composition requires trusted route/schema validation.",
92
96
  "Operation ID conflicts require unconditional registrations with literal IDs and paths on the same local router; conditional registrations, dynamic IDs, OpenAPI overrides, and cross-router composition require generated-schema validation.",
93
97
  "Explicit status and parameter-location findings are organizational policy even when FastAPI inference would generate valid OpenAPI.",
94
98
  (
@@ -268,7 +272,7 @@ class FastapiExplicitOpenapiContract(Rule):
268
272
  findings.extend(_check_metadata(route, index))
269
273
  findings.extend(_check_projection(route))
270
274
  findings.extend(_check_response_statuses(route, index))
271
- findings.extend(_check_route_conflicts(declared))
275
+ findings.extend(_check_route_conflicts(declared, context.parents))
272
276
  findings.extend(_check_operation_ids(declared, context.parents))
273
277
  return [
274
278
  Diagnostic(
@@ -548,7 +552,9 @@ def _check_inferred_return(
548
552
  if not builds_fixed_record(function, imports=index.imports):
549
553
  return []
550
554
  for route in _operations(routes):
551
- if route.has_unpack:
555
+ if route.inherited_has_unpack and route.inherited_responses is None:
556
+ continue
557
+ if not index.can_infer_json_response(route):
552
558
  continue
553
559
  response_model = route.keywords.get("response_model")
554
560
  status = _status_code(route.keywords.get("status_code"), index) or HTTPStatus.OK
@@ -572,13 +578,22 @@ def _check_return_route(route: Route, contract: _ReturnContract, index: FastapiI
572
578
  if contract.erases_schema and not _concrete_response_model(response_model, index):
573
579
  problems.add("response annotation erases its OpenAPI schema; define an output model")
574
580
  status = _status_code(keywords.get("status_code"), index)
581
+ proven_status = index.operation_status_code(route)
582
+ body_status_is_known = proven_status is not None and proven_status not in NO_BODY_STATUSES
575
583
  response_class = keywords.get("response_class") or route.inherited_response_class
576
- if contract.is_response and status not in _NO_CONTENT_STATUSES and response_class is None:
584
+ default_class_is_known = not route.inherited_has_unpack and index.response_classes_are_stable
585
+ if (
586
+ contract.is_response
587
+ and status not in _NO_CONTENT_STATUSES
588
+ and body_status_is_known
589
+ and response_class is None
590
+ and default_class_is_known
591
+ ):
577
592
  problems.add("direct Response return requires response_class= for accurate OpenAPI")
578
593
  _check_response_body(route, contract, index, problems)
579
594
  if response_model is not None and not response_model_none and _schema_erasing(response_model, index):
580
595
  problems.add("response_model erases its OpenAPI schema; define an output model")
581
- if status in _NO_CONTENT_STATUSES:
596
+ if status in _NO_CONTENT_STATUSES and status == proven_status:
582
597
  if response_model is not None and not response_model_none:
583
598
  problems.add(f"status {status} must not declare a response model")
584
599
  if not (contract.is_none or contract.is_response):
@@ -589,19 +604,30 @@ def _check_response_body(route: Route, contract: _ReturnContract, index: Fastapi
589
604
  response_model = route.keywords.get("response_model")
590
605
  response_model_none = response_model is not None and _literal_none(response_model)
591
606
  status = _status_code(route.keywords.get("status_code"), index)
607
+ proven_status = index.operation_status_code(route)
592
608
  response_class = route.keywords.get("response_class") or route.inherited_response_class
593
609
  response_name = index.response_name(contract.annotation)
594
610
  class_name = index.response_name(response_class)
595
- response_class_is_unresolved = response_class is not None and not class_name
611
+ proven_class_name = index.response_class_name(response_class) if response_class is not None else None
612
+ response_class_is_unresolved = (response_class is not None and (not class_name or proven_class_name is None)) or (
613
+ response_class is None and (route.inherited_has_unpack or not index.response_classes_are_stable)
614
+ )
596
615
  has_documented_body = (
597
616
  _concrete_response_model(response_model, index)
598
617
  or any(_documents_response_content(node, status, index) for node in _route_response_nodes(route))
599
618
  or class_name in _SELF_DOCUMENTING_RESPONSE_CLASSES
619
+ or proven_class_name in _SELF_DOCUMENTING_RESPONSE_CLASSES
600
620
  or response_class_is_unresolved
601
621
  )
602
- if response_name not in {"", "Response"} and class_name and response_name != class_name:
622
+ if (
623
+ response_name not in {"", "Response"}
624
+ and class_name
625
+ and proven_class_name is not None
626
+ and response_name != class_name
627
+ ):
603
628
  problems.add(f"return type {response_name} conflicts with response_class={class_name}")
604
- if contract.is_response and status not in _NO_CONTENT_STATUSES and not has_documented_body:
629
+ body_status_is_known = proven_status is not None and proven_status not in NO_BODY_STATUSES
630
+ if contract.is_response and status not in _NO_CONTENT_STATUSES and body_status_is_known and not has_documented_body:
605
631
  problems.add("direct Response return requires a concrete response_model or responses content schema")
606
632
  elif not contract.is_response and not contract.is_none and response_model_none:
607
633
  problems.add("response_model=None suppresses the declared response schema")
@@ -725,14 +751,7 @@ def _check_direct_responses(
725
751
  if not direct.statuses:
726
752
  return findings
727
753
  for route in _operations(routes):
728
- responses = _route_response_nodes(route)
729
- if any(not isinstance(node, ast.Dict) for node in responses):
730
- continue
731
- documented = {
732
- status for node in responses if isinstance(node, ast.Dict) for status in _response_codes(node, index)
733
- }
734
- primary = _status_code(route.keywords.get("status_code"), index)
735
- missing = sorted(direct.statuses - documented - ({primary} if primary is not None else set()))
754
+ missing = _undocumented_direct_statuses(route, direct.statuses, index)
736
755
  if missing:
737
756
  findings.append(
738
757
  _Finding(
@@ -743,6 +762,23 @@ def _check_direct_responses(
743
762
  return findings
744
763
 
745
764
 
765
+ def _undocumented_direct_statuses(route: Route, statuses: set[int], index: FastapiIndex) -> list[int]:
766
+ if route.has_unpack or (route.inherited_has_unpack and route.inherited_responses is None):
767
+ return []
768
+ proven_primary = index.operation_status_code(route)
769
+ if proven_primary is None:
770
+ return []
771
+ responses = _route_response_nodes(route)
772
+ if any(not isinstance(node, ast.Dict) for node in responses):
773
+ return []
774
+ documented = {status for node in responses if isinstance(node, ast.Dict) for status in _response_codes(node, index)}
775
+ primary = _status_code(route.keywords.get("status_code"), index)
776
+ primary_statuses = {proven_primary}
777
+ if primary is not None:
778
+ primary_statuses.add(primary)
779
+ return sorted(statuses - documented - primary_statuses)
780
+
781
+
746
782
  @dataclass(frozen=True, slots=True)
747
783
  class _DirectResponseStatuses:
748
784
  statuses: set[int]
@@ -794,13 +830,15 @@ def _direct_statuses(
794
830
  statuses: list[tuple[ast.Call, int]] = []
795
831
  call = current.exc if isinstance(current, ast.Raise) else None
796
832
  if isinstance(call, ast.Call) and index.is_http_exception(call.func) and not catches_http_exception:
797
- status = _status_code(_keyword(call, "status_code") or (call.args[0] if call.args else None), index)
798
- if status is not None:
833
+ status_node = _keyword(call, "status_code") or (call.args[0] if call.args else None)
834
+ status = _status_code(status_node, index)
835
+ if status is not None and index.status_code(status_node) == status:
799
836
  statuses.append((call, status))
800
837
  returned = current.value if isinstance(current, ast.Return) else None
801
838
  if isinstance(returned, ast.Call) and index.is_response(returned.func):
802
- status = _status_code(_keyword(returned, "status_code"), index)
803
- if status is not None:
839
+ status_node = _keyword(returned, "status_code")
840
+ status = _status_code(status_node, index)
841
+ if status is not None and index.status_code(status_node) == status:
804
842
  statuses.append((returned, status))
805
843
  return statuses
806
844
 
@@ -890,12 +928,12 @@ def _function_scopes(tree: ast.Module) -> dict[int, int]:
890
928
  return scopes
891
929
 
892
930
 
893
- def _check_route_conflicts(routes: list[tuple[int, Route]]) -> list[_Finding]:
931
+ def _check_route_conflicts(routes: list[tuple[int, Route]], parents: Mapping[ast.AST, ast.AST]) -> list[_Finding]:
894
932
  findings: list[_Finding] = []
895
933
  seen: dict[tuple[int, str, str, str], Route] = {}
896
934
  dynamic: dict[tuple[int, str, str], list[tuple[Route, re.Pattern[str]]]] = {}
897
935
  for scope, route in sorted(routes, key=lambda item: item[1].decorator.lineno):
898
- if route.path is None or route.method == "*":
936
+ if route.path is None or route.method == "*" or _conditional_registration(route.decorator, parents):
899
937
  continue
900
938
  key = scope, route.receiver, route.method, route.path
901
939
  if key in seen:
@@ -54,6 +54,8 @@ class PreferPreciseObjectContract(Rule):
54
54
  "Ordinary raw validators retain object inputs tied to builtin isinstance, exact builtin type comparisons or an actual source-resolved TypeGuard/TypeIs through rejecting branches, actual type/refinement assertions, accepting terminal branches with a rejecting fallback, or conditional expressions. Stable projections from containers with unchecked value slots retain their raw root contract when those values are validated. Source-visible local and imported validators may delegate those inputs and raw results; precise result narrowing does not justify a bare object output. Related unchecked mapping projections and transforms retain unknown values; import-proven native parsers and JSONDecoder preserve raw outputs. Universal builtin and stdlib interoperation may retain raw inputs. Independent object parameters, domain constructors and unproven parser names remain checked. Source resolution and dataflow are bounded with cycle guards; rebinding, decorators and namespace mutation invalidate delegated proof.",
55
55
  "Native opaque contracts include canonical ast.Constant.value reads and captures, FunctionType invocations, identity-only inputs and sys.getrefcount. Actual exec/eval globals and locals retain unknown values only through bounded native dict construction and alias flow; unrelated dictionaries and source domain factories do not borrow that ownership. Stub property returns on an import-proven runtime_checkable Protocol retain unchecked incoming attributes. Generic callable results, ordinary Protocol methods, application properties and unused parameters remain checked.",
56
56
  "Raw input unions may include scalar or None branches; only genuine unchecked container branches seed raw projections. Mapping consumers that inspect only keys retain uninspected value slots. Projected aliases require simple statement dominance, and precise active guards permit typed consumption after validation. Native namespace output reads require a preceding direct function-scope execution and abstain after authored mutations. Benign builtins imports do not hide builtin object contracts; explicit object shadowing and source-visible object mutations remain outside scope.",
57
+ "An import-proven Iterable of unchecked values retains its raw element contract when every input use is a direct synchronous iteration and every reachable element read directly enters source-proven Pydantic model_validate. Deferred closures, rebinding, other iterator/element uses, unknown model providers and JSON-text parsers do not establish that boundary.",
58
+ "Rejecting branches may end in a stable source-visible synchronous helper whose body ends with an unconditional raise and contains no return or yield path. Names and NoReturn annotations alone do not establish termination; decorated, mutated, asynchronous, generator and returning helpers remain unknown.",
57
59
  ),
58
60
  examples=(
59
61
  RuleExample(
@@ -42,7 +42,7 @@ class PreferTypedCli(Rule):
42
42
  category=RuleCategory.MAINTAINABILITY,
43
43
  autofix=AutofixPolicy.NONE,
44
44
  limitations=(
45
- "Checks import-proven sys.argv reads, argparse/optparse parser construction, getopt parsing and Click command construction. sys.argv[0], assignment targets and forwarding through args= to a proven local Typer application are retained.",
45
+ "Checks import-proven sys.argv reads, argparse/optparse parser construction, getopt parsing and Click command construction. sys.argv[0] and its direct nonempty guard with a literal string fallback, assignment targets and forwarding through args= to a proven local Typer application are retained.",
46
46
  "Checks parameters of source-visible functions passed directly to typer.run or registered with a stable local Typer application's command/callback decorator. Missing annotations and bare Any/object, including Annotated wrappers, are reported. Ordinary functions and unrelated command decorators are excluded.",
47
47
  "Stable local import and application aliases are followed. Shadowed or mutated imports, wildcard imports, dynamic namespaces and generated/vendor files abstain. Imported command functions, dynamic registrations and composite erased types require separate type checking.",
48
48
  "No automatic rewrite: positional/option compatibility and conversion behavior require review. This policy does not claim every argparse or Click program is incorrectly validated.",
@@ -191,6 +191,20 @@ def _retained_argv(node: ast.expr, facts: AnnotationOrigins) -> bool:
191
191
  if isinstance(parent.slice, ast.Constant) and type(parent.slice.value) is int and parent.slice.value == 0:
192
192
  return True
193
193
  parent = facts.context.parents.get(parent)
194
+ match parent:
195
+ case ast.IfExp(
196
+ test=test,
197
+ body=ast.Subscript(value=arguments, slice=ast.Constant(value=index)),
198
+ orelse=ast.Constant(value=str()),
199
+ ):
200
+ return (
201
+ test is node
202
+ and type(index) is int
203
+ and index == 0
204
+ and facts.qualified(arguments, arguments) == "sys.argv"
205
+ )
206
+ case _:
207
+ pass
194
208
  if not isinstance(parent, ast.keyword) or parent.arg != "args":
195
209
  return False
196
210
  call = facts.context.parents.get(parent)