sarj-python-lint 1.61.1__tar.gz → 1.63.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 (238) hide show
  1. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/pyproject.toml +1 -1
  3. sarj_python_lint-1.63.0/src/sarj_python_lint/cast_access.py +86 -0
  4. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_fastapi.py +3 -1
  5. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_registry.py +22 -0
  6. sarj_python_lint-1.63.0/src/sarj_python_lint/rules/_route_shapes.py +76 -0
  7. sarj_python_lint-1.63.0/src/sarj_python_lint/rules/delete_route_returns_no_content.py +120 -0
  8. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +2 -4
  9. sarj_python_lint-1.63.0/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +109 -0
  10. sarj_python_lint-1.63.0/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +159 -0
  11. sarj_python_lint-1.63.0/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +389 -0
  12. sarj_python_lint-1.63.0/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +163 -0
  13. sarj_python_lint-1.63.0/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +164 -0
  14. sarj_python_lint-1.63.0/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +154 -0
  15. sarj_python_lint-1.63.0/src/sarj_python_lint/rules/no_hasattr.py +126 -0
  16. sarj_python_lint-1.63.0/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +104 -0
  17. sarj_python_lint-1.63.0/src/sarj_python_lint/rules/no_work_verb_get_route.py +126 -0
  18. sarj_python_lint-1.63.0/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +127 -0
  19. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/unused_mock_setup.py +3 -3
  20. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/.gitignore +0 -0
  21. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/LICENSE +0 -0
  22. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/README.md +0 -0
  23. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/__init__.py +0 -0
  24. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/__main__.py +0 -0
  25. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  26. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/_file_context.py +0 -0
  27. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/_filesystem.py +0 -0
  28. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/_python_target.py +0 -0
  29. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  30. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/_secret_names.py +0 -0
  31. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/_source.py +0 -0
  32. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/_version.py +0 -0
  33. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/benchmark.py +0 -0
  34. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  35. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/json_boundary.py +0 -0
  36. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/py.typed +0 -0
  37. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/ratchet.py +0 -0
  38. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rule_base.py +0 -0
  39. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  40. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  41. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  42. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  43. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  44. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  45. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  46. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  47. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
  48. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  49. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  50. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  51. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  52. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  53. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  54. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  55. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  56. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  57. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  58. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
  59. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  60. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  61. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  62. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  63. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  64. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  65. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  66. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  67. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  68. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  69. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  70. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  71. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  72. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  73. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  74. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  75. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  76. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  77. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  78. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  79. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  80. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  81. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  82. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  83. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  84. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  85. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  86. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  87. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  88. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  89. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  90. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  91. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  92. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  93. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  94. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  95. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  96. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  97. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  98. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  99. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  100. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  101. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  102. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  103. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  104. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  105. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  106. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  107. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  108. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
  109. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  110. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  111. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  112. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  113. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  114. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  115. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  116. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  117. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  118. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  119. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  120. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  121. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  122. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  123. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  124. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  125. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  126. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  127. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  128. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  129. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  130. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
  131. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  132. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  133. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  134. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  135. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  136. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  137. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  138. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  139. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  140. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  141. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  142. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  143. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  144. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  145. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  146. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  147. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  148. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  149. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  150. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  151. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  152. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  153. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  154. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  155. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  156. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  157. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  158. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  159. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  160. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  161. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  162. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  163. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  164. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  165. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  166. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  167. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  168. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  169. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  170. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  171. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  172. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  173. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  174. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  175. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  176. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  177. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  178. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  179. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  180. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  181. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  182. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  183. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  184. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  185. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  186. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  187. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  188. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  189. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  190. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  191. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  192. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  193. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  194. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  195. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  196. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  197. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  198. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  199. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  200. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  201. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  202. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  203. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  204. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  205. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  206. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  207. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  208. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  209. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  210. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  211. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  212. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  213. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  214. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  215. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  216. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  217. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  218. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  219. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  220. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  221. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  222. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  223. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
  224. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  225. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  226. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  227. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  228. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  229. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
  230. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  231. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  232. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  233. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  234. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  235. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  236. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  237. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  238. {sarj_python_lint-1.61.1 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 1.61.1
3
+ Version: 1.63.0
4
4
  Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
5
5
  Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
6
6
  Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "1.61.1"
3
+ version = "1.63.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" }]
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING, Literal
6
+
7
+ from sarj_python_lint._file_context import PythonFileContext
8
+ from sarj_python_lint.rules._resource_provenance import ResourceProvenance
9
+
10
+
11
+ if TYPE_CHECKING:
12
+ from pathlib import Path
13
+
14
+
15
+ _TYPING = frozenset({"typing", "typing_extensions"})
16
+ _GETATTR_ARITIES = frozenset({2, 3})
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class CastAccess:
21
+ node: ast.Call | ast.ImportFrom
22
+ qualified_api: str
23
+ kind: Literal["wildcard", "reflection"]
24
+
25
+
26
+ def indirect_typing_cast_accesses(path: Path, source: str) -> tuple[CastAccess, ...]:
27
+ context = PythonFileContext(path, source)
28
+ if "*" not in source and "getattr" not in context.symbol_source:
29
+ return ()
30
+ if context.tree is None:
31
+ return ()
32
+ accesses = [
33
+ CastAccess(node, f"{node.module}.cast", "wildcard")
34
+ for node in context.nodes(ast.ImportFrom)
35
+ if node.level == 0 and node.module in _TYPING and any(alias.name == "*" for alias in node.names)
36
+ ]
37
+ provenance = ResourceProvenance(context, require_static_namespace=True)
38
+ if provenance.wildcard or provenance.dynamic_namespace or not provenance.builtin_namespace_untouched:
39
+ return tuple(accesses)
40
+ for call in context.nodes(ast.Call):
41
+ if not _getattr_call(call, provenance):
42
+ continue
43
+ module = _typing_receiver(call.args[0], provenance)
44
+ if module is not None:
45
+ accesses.append(CastAccess(call, f"{module}.cast", "reflection"))
46
+ return tuple(sorted(accesses, key=lambda access: (access.node.lineno, access.node.col_offset)))
47
+
48
+
49
+ def _getattr_call(call: ast.Call, provenance: ResourceProvenance) -> bool:
50
+ if not (
51
+ isinstance(call.func, ast.Name)
52
+ and call.func.id == "getattr"
53
+ and len(call.args) in _GETATTR_ARITIES
54
+ and not call.keywords
55
+ and isinstance(call.args[1], ast.Constant)
56
+ and call.args[1].value == "cast"
57
+ ):
58
+ return False
59
+ scope = provenance.scope(call.func)
60
+ while scope is not None:
61
+ if "getattr" in provenance.bindings.get(scope, {}):
62
+ return False
63
+ scope = provenance.scope(scope)
64
+ return True
65
+
66
+
67
+ def _typing_receiver(node: ast.expr, provenance: ResourceProvenance) -> str | None:
68
+ if not isinstance(node, ast.Name) or node.id in provenance.mutated | provenance.external_bindings:
69
+ return None
70
+ binding = provenance.binding(node.id, node)
71
+ if not (
72
+ isinstance(binding, ast.alias)
73
+ and isinstance(provenance.context.parents.get(binding), ast.Import)
74
+ and binding.name in _TYPING
75
+ ):
76
+ return None
77
+ for read in provenance.reads.get(node.id, ()):
78
+ if provenance.binding(read.id, read) is not binding:
79
+ continue
80
+ parent = provenance.context.parents.get(read)
81
+ if isinstance(parent, ast.Attribute) and parent.value is read:
82
+ continue
83
+ if isinstance(parent, ast.Call) and _getattr_call(parent, provenance) and parent.args[0] is read:
84
+ continue
85
+ return None
86
+ return binding.name
@@ -47,6 +47,7 @@ class Route:
47
47
  inherited_hidden: bool = False
48
48
  inherited_responses: ast.expr | None = None
49
49
  inherited_response_class: ast.expr | None = None
50
+ inherited_prefix: ast.expr | None = None
50
51
 
51
52
  @property
52
53
  def keywords(self) -> dict[str, ast.expr]:
@@ -299,7 +300,7 @@ class FastapiIndex:
299
300
  self.receiver_defaults[key] = {
300
301
  keyword.arg: keyword.value
301
302
  for keyword in value.keywords
302
- if keyword.arg in {"responses", "default_response_class"}
303
+ if keyword.arg in {"responses", "default_response_class", "prefix"}
303
304
  }
304
305
  if self._constructor_is_hidden(value):
305
306
  self.hidden_receivers.add(key)
@@ -398,6 +399,7 @@ class FastapiIndex:
398
399
  inherited_hidden=receiver_key in self.hidden_receivers,
399
400
  inherited_responses=defaults.get("responses"),
400
401
  inherited_response_class=defaults.get("default_response_class"),
402
+ inherited_prefix=defaults.get("prefix"),
401
403
  )
402
404
  for route_method in route_methods
403
405
  )
@@ -44,6 +44,7 @@ from sarj_python_lint.rules.no_cors_wildcard_with_credentials import (
44
44
  from sarj_python_lint.rules.no_database_programmability import NoDatabaseProgrammability
45
45
  from sarj_python_lint.rules.no_delete_statement import NoDeleteStatement
46
46
  from sarj_python_lint.rules.no_discarded_validation_result import NoDiscardedValidationResult
47
+ from sarj_python_lint.rules.no_dual_shape_model_field_reader import NoDualShapeModelFieldReader
47
48
  from sarj_python_lint.rules.no_dunder_all import NoDunderAll
48
49
  from sarj_python_lint.rules.no_duplicate_dunder_all_entry import NoDuplicateDunderAllEntry
49
50
  from sarj_python_lint.rules.no_excessive_cognitive_complexity import NoExcessiveCognitiveComplexity
@@ -214,21 +215,31 @@ if TYPE_CHECKING:
214
215
  from collections.abc import Mapping
215
216
 
216
217
  from sarj_python_lint.rule_base import Rule
218
+ from sarj_python_lint.rules.delete_route_returns_no_content import DeleteRouteReturnsNoContent
217
219
  from sarj_python_lint.rules.duplicate_adjacent_exception_handler import DuplicateAdjacentExceptionHandler
218
220
  from sarj_python_lint.rules.duplicate_closed_pure_helper import DuplicateClosedPureHelper
219
221
  from sarj_python_lint.rules.no_conflicting_pydantic_alias_validation import NoConflictingPydanticAliasValidation
220
222
  from sarj_python_lint.rules.no_contradictory_type_guard import NoContradictoryTypeGuard
223
+ from sarj_python_lint.rules.no_create_verb_in_post_path import NoCreateVerbInPostPath
224
+ from sarj_python_lint.rules.no_defaulted_environment_setting import NoDefaultedEnvironmentSetting
225
+ from sarj_python_lint.rules.no_duplicate_field_validator_body import NoDuplicateFieldValidatorBody
226
+ from sarj_python_lint.rules.no_exception_text_in_500_detail import NoExceptionTextIn500Detail
227
+ from sarj_python_lint.rules.no_folded_bad_request_not_found import NoFoldedBadRequestNotFound
228
+ from sarj_python_lint.rules.no_hasattr import NoHasattr
221
229
  from sarj_python_lint.rules.no_impossible_pydantic_field_bounds import NoImpossiblePydanticFieldBounds
222
230
  from sarj_python_lint.rules.no_invalid_pydantic_bypass import NoInvalidPydanticBypass
223
231
  from sarj_python_lint.rules.no_invalid_pydantic_validator_return import NoInvalidPydanticValidatorReturn
224
232
  from sarj_python_lint.rules.no_partial_type_is import NoPartialTypeIs
225
233
  from sarj_python_lint.rules.no_shallow_record_type_guard import NoShallowRecordTypeGuard
234
+ from sarj_python_lint.rules.no_slash_collection_root_under_prefix import NoSlashCollectionRootUnderPrefix
226
235
  from sarj_python_lint.rules.no_unknown_pydantic_validator_field import NoUnknownPydanticValidatorField
236
+ from sarj_python_lint.rules.no_work_verb_get_route import NoWorkVerbGetRoute
227
237
  from sarj_python_lint.rules.prefer_bulk_upsert import PreferBulkUpsert
228
238
  from sarj_python_lint.rules.prefer_discriminated_model_union import PreferDiscriminatedModelUnion
229
239
  from sarj_python_lint.rules.prefer_psycopg_executemany import PreferPsycopgExecutemany
230
240
  from sarj_python_lint.rules.redundant_test_constructor_forwarder import RedundantTestConstructorForwarder
231
241
  from sarj_python_lint.rules.repeated_unimplemented_test_contract import RepeatedUnimplementedTestContract
242
+ from sarj_python_lint.rules.require_exp_on_jwt_decode import RequireExpOnJwtDecode
232
243
  from sarj_python_lint.rules.store_field_update_reuses_upsert import StoreFieldUpdateReusesUpsert
233
244
  from sarj_python_lint.rules.test_without_explicit_oracle import NoExplicitTestOracle
234
245
 
@@ -238,6 +249,16 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
238
249
  PreferBulkUpsert.id: PreferBulkUpsert,
239
250
  PreferPsycopgExecutemany.id: PreferPsycopgExecutemany,
240
251
  StoreFieldUpdateReusesUpsert.id: StoreFieldUpdateReusesUpsert,
252
+ DeleteRouteReturnsNoContent.id: DeleteRouteReturnsNoContent,
253
+ NoExceptionTextIn500Detail.id: NoExceptionTextIn500Detail,
254
+ RequireExpOnJwtDecode.id: RequireExpOnJwtDecode,
255
+ NoSlashCollectionRootUnderPrefix.id: NoSlashCollectionRootUnderPrefix,
256
+ NoWorkVerbGetRoute.id: NoWorkVerbGetRoute,
257
+ NoCreateVerbInPostPath.id: NoCreateVerbInPostPath,
258
+ NoHasattr.id: NoHasattr,
259
+ NoFoldedBadRequestNotFound.id: NoFoldedBadRequestNotFound,
260
+ NoDefaultedEnvironmentSetting.id: NoDefaultedEnvironmentSetting,
261
+ NoDuplicateFieldValidatorBody.id: NoDuplicateFieldValidatorBody,
241
262
  NoExplicitTestOracle.id: NoExplicitTestOracle,
242
263
  PreferDiscriminatedModelUnion.id: PreferDiscriminatedModelUnion,
243
264
  NoInvalidPydanticBypass.id: NoInvalidPydanticBypass,
@@ -307,6 +328,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
307
328
  RequireKeywordOnlySwapProneParams.id: RequireKeywordOnlySwapProneParams,
308
329
  RequirePydanticForExternalJson.id: RequirePydanticForExternalJson,
309
330
  RequirePydanticForMappingShape.id: RequirePydanticForMappingShape,
331
+ NoDualShapeModelFieldReader.id: NoDualShapeModelFieldReader,
310
332
  PreferModuleLevelConstant.id: PreferModuleLevelConstant,
311
333
  PreferImmutableModuleConstant.id: PreferImmutableModuleConstant,
312
334
  PreferInjectedDependencyOverMonkeypatch.id: PreferInjectedDependencyOverMonkeypatch,
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from dataclasses import dataclass
5
+ from http import HTTPStatus
6
+ import re
7
+ from typing import TYPE_CHECKING, Final
8
+
9
+ from sarj_python_lint.rules._paths import is_test_path
10
+
11
+
12
+ if TYPE_CHECKING:
13
+ from collections.abc import Iterator
14
+
15
+ from sarj_python_lint._file_context import PythonFileContext
16
+
17
+
18
+ _STATUS_OWNERS: Final = frozenset({"HTTPStatus", "status"})
19
+ _STARLETTE_STATUS: Final = re.compile(r"HTTP_(?P<code>[1-5][0-9]{2})_[A-Z_]+")
20
+ _SEGMENT_WORD: Final = re.compile(r"[-_.]")
21
+
22
+
23
+ @dataclass(frozen=True, slots=True)
24
+ class RouteDecorator:
25
+ decorator: ast.Call
26
+ methods: frozenset[str]
27
+ path: str | None
28
+ prefix: ast.expr | None
29
+
30
+
31
+ def route_decorators(context: PythonFileContext) -> Iterator[RouteDecorator]:
32
+ if context.tree is None or context.generated or is_test_path(context.path):
33
+ return
34
+ if "APIRouter" not in context.symbol_source and "FastAPI" not in context.symbol_source:
35
+ return
36
+ index = context.fastapi
37
+ for function in context.nodes(ast.FunctionDef, ast.AsyncFunctionDef):
38
+ grouped: dict[int, list[str]] = {}
39
+ first: dict[int, RouteDecorator] = {}
40
+ for route in index.routes(function):
41
+ if route.has_unpack:
42
+ continue
43
+ grouped.setdefault(id(route.decorator), []).append(route.method)
44
+ first.setdefault(
45
+ id(route.decorator), RouteDecorator(route.decorator, frozenset(), route.path, route.inherited_prefix)
46
+ )
47
+ for key, methods in grouped.items():
48
+ shape = first[key]
49
+ yield RouteDecorator(shape.decorator, frozenset(methods), shape.path, shape.prefix)
50
+
51
+
52
+ def final_segment_word(path: str) -> str | None:
53
+ segment = path.rstrip("/").rsplit("/", 1)[-1]
54
+ if not segment or segment.startswith("{"):
55
+ return None
56
+ return _SEGMENT_WORD.split(segment.lower(), maxsplit=1)[0]
57
+
58
+
59
+ def final_segment_words(path: str) -> list[str]:
60
+ segment = path.rstrip("/").rsplit("/", 1)[-1]
61
+ if not segment or segment.startswith("{"):
62
+ return []
63
+ return [word for word in _SEGMENT_WORD.split(segment.lower()) if word]
64
+
65
+
66
+ def http_status(node: ast.expr | None) -> int | None:
67
+ match node:
68
+ case ast.Constant(value=int() as value) if not isinstance(value, bool):
69
+ return value
70
+ case ast.Attribute(value=ast.Name(id=owner), attr=name) if owner in _STATUS_OWNERS:
71
+ if (match := _STARLETTE_STATUS.fullmatch(name)) is not None:
72
+ return int(match.group("code"))
73
+ member = HTTPStatus.__members__.get(name)
74
+ return None if member is None else member.value
75
+ case _:
76
+ return None
@@ -0,0 +1,120 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from pathlib import PurePosixPath
5
+ from typing import TYPE_CHECKING, Final, final, override
6
+
7
+ from sarj_python_lint.rule_base import (
8
+ AutofixPolicy,
9
+ Diagnostic,
10
+ ExampleFile,
11
+ ExampleOutcome,
12
+ Rule,
13
+ RuleCategory,
14
+ RuleDocumentation,
15
+ RuleExample,
16
+ Severity,
17
+ )
18
+ from sarj_python_lint.rules._route_shapes import RouteDecorator, http_status, route_decorators
19
+
20
+
21
+ if TYPE_CHECKING:
22
+ from collections.abc import Mapping
23
+
24
+ from sarj_python_lint._file_context import PythonFileContext
25
+
26
+
27
+ _NO_CONTENT: Final = 204
28
+ _MESSAGE = "DELETE route does not declare status_code=204; return no body and declare the 204 No Content status."
29
+
30
+
31
+ @final
32
+ class DeleteRouteReturnsNoContent(Rule):
33
+ id = "delete-route-returns-no-content"
34
+ code = "SARJ506"
35
+ documentation = RuleDocumentation(
36
+ default_level=Severity.WARNING,
37
+ summary="FastAPI DELETE routes declare status code 204 No Content.",
38
+ rationale=(
39
+ "Without an explicit 204, FastAPI answers a DELETE with 200 and a JSON null body. Generated clients then "
40
+ "type the result as nullable data, and API routes disagree on what a successful removal returns."
41
+ ),
42
+ remediation="Declare `status_code=204` (or `status.HTTP_204_NO_CONTENT`) on the DELETE route and return None.",
43
+ category=RuleCategory.CORRECTNESS,
44
+ autofix=AutofixPolicy.NONE,
45
+ limitations=(
46
+ "Only DELETE-only decorators on a FastAPI or APIRouter receiver created in the same file are checked.",
47
+ (
48
+ "A status written as an integer, a Starlette `status.HTTP_*` name or an `HTTPStatus` member is resolved; "
49
+ "any other status expression and decorators with `**` keyword unpacking are accepted."
50
+ ),
51
+ "Tests and generated files are excluded.",
52
+ ),
53
+ examples=(
54
+ RuleExample(
55
+ example_id="implicit-200",
56
+ title="A DELETE route answers 200 with a null body",
57
+ outcome=ExampleOutcome.MATCH,
58
+ files=(
59
+ ExampleFile.python(
60
+ "api.py",
61
+ "from fastapi import APIRouter\n\nrouter = APIRouter()\n\n"
62
+ "@router.delete('/keys/{key_id}')\nasync def revoke(key_id: str) -> None:\n return None\n",
63
+ ),
64
+ ),
65
+ focus_path=PurePosixPath("api.py"),
66
+ expected_count=1,
67
+ public=True,
68
+ ),
69
+ RuleExample(
70
+ example_id="no-content",
71
+ title="A DELETE route declares 204",
72
+ outcome=ExampleOutcome.NO_MATCH,
73
+ files=(
74
+ ExampleFile.python(
75
+ "api.py",
76
+ "from fastapi import APIRouter, status\n\nrouter = APIRouter()\n\n"
77
+ "@router.delete('/keys/{key_id}', status_code=status.HTTP_204_NO_CONTENT)\n"
78
+ "async def revoke(key_id: str) -> None:\n return None\n",
79
+ ),
80
+ ),
81
+ focus_path=PurePosixPath("api.py"),
82
+ expected_count=0,
83
+ public=True,
84
+ ),
85
+ ),
86
+ )
87
+ description = documentation.summary
88
+
89
+ @override
90
+ def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
91
+ return [
92
+ Diagnostic(
93
+ context.path,
94
+ route.decorator.lineno,
95
+ route.decorator.col_offset + 1,
96
+ self.code,
97
+ _MESSAGE,
98
+ severity=Severity.WARNING,
99
+ )
100
+ for route in route_decorators(context)
101
+ if route.methods == {"delete"} and _lacks_no_content(route) and not _declares_body(route, context.parents)
102
+ ]
103
+
104
+
105
+ def _declares_body(route: RouteDecorator, parents: Mapping[ast.AST, ast.AST]) -> bool:
106
+ model = next((keyword.value for keyword in route.decorator.keywords if keyword.arg == "response_model"), None)
107
+ if model is not None and not (isinstance(model, ast.Constant) and model.value is None):
108
+ return True
109
+ function = parents.get(route.decorator)
110
+ if not isinstance(function, (ast.FunctionDef, ast.AsyncFunctionDef)) or function.returns is None:
111
+ return False
112
+ return not (isinstance(function.returns, ast.Constant) and function.returns.value is None)
113
+
114
+
115
+ def _lacks_no_content(route: RouteDecorator) -> bool:
116
+ status = next((keyword.value for keyword in route.decorator.keywords if keyword.arg == "status_code"), None)
117
+ if status is None:
118
+ return True
119
+ resolved = http_status(status)
120
+ return resolved is not None and resolved != _NO_CONTENT
@@ -3,10 +3,10 @@ from __future__ import annotations
3
3
  import ast
4
4
  import builtins
5
5
  from pathlib import PurePosixPath
6
+ from types import MappingProxyType
6
7
  from typing import (
7
8
  TYPE_CHECKING,
8
9
  ClassVar,
9
- cast, # ruff: ignore[banned-api] -- vars exposes Any; the object view permits isinstance narrowing.
10
10
  final,
11
11
  override,
12
12
  )
@@ -31,14 +31,12 @@ from sarj_python_lint.rules._suppression_comments import scan_comments
31
31
 
32
32
 
33
33
  if TYPE_CHECKING:
34
- from collections.abc import Mapping
35
-
36
34
  from sarj_python_lint._file_context import PythonFileContext
37
35
  from sarj_python_lint.rules._project_index import SourceUnit, SymbolRef
38
36
  from sarj_python_lint.rules._suppression_comments import Comment
39
37
 
40
38
 
41
- _BUILTIN_NAMESPACE = cast("Mapping[str, object]", vars(builtins))
39
+ _BUILTIN_NAMESPACE = MappingProxyType[str, object](vars(builtins))
42
40
  _BUILTIN_EXCEPTIONS = frozenset(
43
41
  name for name, value in _BUILTIN_NAMESPACE.items() if isinstance(value, type) and issubclass(value, Exception)
44
42
  )
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import PurePosixPath
4
+ from typing import TYPE_CHECKING, Final, final, override
5
+
6
+ from sarj_python_lint.rule_base import (
7
+ AutofixPolicy,
8
+ Diagnostic,
9
+ ExampleFile,
10
+ ExampleOutcome,
11
+ Rule,
12
+ RuleCategory,
13
+ RuleDocumentation,
14
+ RuleExample,
15
+ Severity,
16
+ )
17
+ from sarj_python_lint.rules._route_shapes import final_segment_words, route_decorators
18
+
19
+
20
+ if TYPE_CHECKING:
21
+ from sarj_python_lint._file_context import PythonFileContext
22
+
23
+
24
+ _CREATE_WORDS: Final = frozenset({"create", "add", "new"})
25
+ _MESSAGE = (
26
+ "POST already means create; name the collection (POST /tickets) instead of a create, add or new segment, "
27
+ "and use PUT for an upsert."
28
+ )
29
+
30
+
31
+ @final
32
+ class NoCreateVerbInPostPath(Rule):
33
+ id = "no-create-verb-in-post-path"
34
+ code = "SARJ500"
35
+ documentation = RuleDocumentation(
36
+ default_level=Severity.WARNING,
37
+ summary="POST routes name their collection instead of ending in a create, add or new segment.",
38
+ rationale=(
39
+ "The HTTP method already states the action. A trailing create, add or new segment turns a resource API "
40
+ "into ad hoc RPC names, so clients cannot predict paths and PUT, PATCH and DELETE lose their pairing."
41
+ ),
42
+ remediation="POST to the collection path, use PUT on the item path for an upsert and PATCH for a partial update.",
43
+ category=RuleCategory.ARCHITECTURE,
44
+ autofix=AutofixPolicy.NONE,
45
+ limitations=(
46
+ "Only literal route paths on a FastAPI or APIRouter receiver created in the same file are checked.",
47
+ "The final path segment is read; its first word (split on '-', '_' or '.') must be create, add or new.",
48
+ "A final path parameter is accepted, and tests and generated files are excluded.",
49
+ "Deliberate RPC-style integration endpoints exist; keep them with an exact suppression and its reason.",
50
+ ),
51
+ examples=(
52
+ RuleExample(
53
+ example_id="create-segment",
54
+ title="A POST route repeats the verb in its path",
55
+ outcome=ExampleOutcome.MATCH,
56
+ files=(
57
+ ExampleFile.python(
58
+ "api.py",
59
+ "from fastapi import APIRouter\n\nrouter = APIRouter()\n\n"
60
+ "@router.post('/tickets/create')\nasync def create_ticket() -> None:\n return None\n",
61
+ ),
62
+ ),
63
+ focus_path=PurePosixPath("api.py"),
64
+ expected_count=1,
65
+ public=True,
66
+ ),
67
+ RuleExample(
68
+ example_id="collection-path",
69
+ title="A POST route names its collection",
70
+ outcome=ExampleOutcome.NO_MATCH,
71
+ files=(
72
+ ExampleFile.python(
73
+ "api.py",
74
+ "from fastapi import APIRouter\n\nrouter = APIRouter()\n\n"
75
+ "@router.post('/tickets')\nasync def create_ticket() -> None:\n return None\n",
76
+ ),
77
+ ),
78
+ focus_path=PurePosixPath("api.py"),
79
+ expected_count=0,
80
+ public=True,
81
+ ),
82
+ ),
83
+ )
84
+ description = documentation.summary
85
+
86
+ @override
87
+ def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
88
+ return [
89
+ Diagnostic(
90
+ context.path,
91
+ route.decorator.lineno,
92
+ route.decorator.col_offset + 1,
93
+ self.code,
94
+ _MESSAGE,
95
+ severity=Severity.WARNING,
96
+ )
97
+ for route in route_decorators(context)
98
+ if "post" in route.methods
99
+ and route.path is not None
100
+ and _is_create_segment(final_segment_words(route.path))
101
+ ]
102
+
103
+
104
+ def _is_create_segment(words: list[str]) -> bool:
105
+ if not words or words[0] not in _CREATE_WORDS:
106
+ return False
107
+ if words[0] == "new":
108
+ return len(words) == 1
109
+ return words[:2] != ["add", "ons"]