sarj-python-lint 1.62.0__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.
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/pyproject.toml +1 -1
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_fastapi.py +3 -1
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_registry.py +20 -0
- sarj_python_lint-1.63.0/src/sarj_python_lint/rules/_route_shapes.py +76 -0
- sarj_python_lint-1.63.0/src/sarj_python_lint/rules/delete_route_returns_no_content.py +120 -0
- sarj_python_lint-1.63.0/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +109 -0
- sarj_python_lint-1.63.0/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +159 -0
- sarj_python_lint-1.63.0/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +163 -0
- sarj_python_lint-1.63.0/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +164 -0
- sarj_python_lint-1.63.0/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +154 -0
- sarj_python_lint-1.63.0/src/sarj_python_lint/rules/no_hasattr.py +126 -0
- sarj_python_lint-1.63.0/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +104 -0
- sarj_python_lint-1.63.0/src/sarj_python_lint/rules/no_work_verb_get_route.py +126 -0
- sarj_python_lint-1.63.0/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +127 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/.gitignore +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/LICENSE +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/README.md +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/cast_access.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.62.0 → sarj_python_lint-1.63.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.62.0 → 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.
|
|
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/
|
|
@@ -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
|
)
|
|
@@ -215,21 +215,31 @@ if TYPE_CHECKING:
|
|
|
215
215
|
from collections.abc import Mapping
|
|
216
216
|
|
|
217
217
|
from sarj_python_lint.rule_base import Rule
|
|
218
|
+
from sarj_python_lint.rules.delete_route_returns_no_content import DeleteRouteReturnsNoContent
|
|
218
219
|
from sarj_python_lint.rules.duplicate_adjacent_exception_handler import DuplicateAdjacentExceptionHandler
|
|
219
220
|
from sarj_python_lint.rules.duplicate_closed_pure_helper import DuplicateClosedPureHelper
|
|
220
221
|
from sarj_python_lint.rules.no_conflicting_pydantic_alias_validation import NoConflictingPydanticAliasValidation
|
|
221
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
|
|
222
229
|
from sarj_python_lint.rules.no_impossible_pydantic_field_bounds import NoImpossiblePydanticFieldBounds
|
|
223
230
|
from sarj_python_lint.rules.no_invalid_pydantic_bypass import NoInvalidPydanticBypass
|
|
224
231
|
from sarj_python_lint.rules.no_invalid_pydantic_validator_return import NoInvalidPydanticValidatorReturn
|
|
225
232
|
from sarj_python_lint.rules.no_partial_type_is import NoPartialTypeIs
|
|
226
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
|
|
227
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
|
|
228
237
|
from sarj_python_lint.rules.prefer_bulk_upsert import PreferBulkUpsert
|
|
229
238
|
from sarj_python_lint.rules.prefer_discriminated_model_union import PreferDiscriminatedModelUnion
|
|
230
239
|
from sarj_python_lint.rules.prefer_psycopg_executemany import PreferPsycopgExecutemany
|
|
231
240
|
from sarj_python_lint.rules.redundant_test_constructor_forwarder import RedundantTestConstructorForwarder
|
|
232
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
|
|
233
243
|
from sarj_python_lint.rules.store_field_update_reuses_upsert import StoreFieldUpdateReusesUpsert
|
|
234
244
|
from sarj_python_lint.rules.test_without_explicit_oracle import NoExplicitTestOracle
|
|
235
245
|
|
|
@@ -239,6 +249,16 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
239
249
|
PreferBulkUpsert.id: PreferBulkUpsert,
|
|
240
250
|
PreferPsycopgExecutemany.id: PreferPsycopgExecutemany,
|
|
241
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,
|
|
242
262
|
NoExplicitTestOracle.id: NoExplicitTestOracle,
|
|
243
263
|
PreferDiscriminatedModelUnion.id: PreferDiscriminatedModelUnion,
|
|
244
264
|
NoInvalidPydanticBypass.id: NoInvalidPydanticBypass,
|
|
@@ -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
|
|
@@ -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"]
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
from typing import TYPE_CHECKING, final, override
|
|
6
|
+
|
|
7
|
+
from sarj_python_lint.rule_base import (
|
|
8
|
+
AutofixPolicy,
|
|
9
|
+
Diagnostic,
|
|
10
|
+
ExampleFile,
|
|
11
|
+
ExampleOutcome,
|
|
12
|
+
Rule,
|
|
13
|
+
RuleCategory,
|
|
14
|
+
RuleDocumentation,
|
|
15
|
+
RuleExample,
|
|
16
|
+
Severity,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
22
|
+
from sarj_python_lint.rules._imports import ImportIndex
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_SETTINGS_SOURCES = frozenset({"pydantic_settings", "pydantic_settings.main"})
|
|
26
|
+
_FIELD_SOURCES = frozenset({"pydantic", "pydantic.fields"})
|
|
27
|
+
_ENVIRONMENT = "environment"
|
|
28
|
+
_ALIAS_KEYWORDS = frozenset({"alias", "validation_alias"})
|
|
29
|
+
_DEFAULT_KEYWORDS = frozenset({"default", "default_factory"})
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@final
|
|
33
|
+
class NoDefaultedEnvironmentSetting(Rule):
|
|
34
|
+
id = "no-defaulted-environment-setting"
|
|
35
|
+
code = "SARJ497"
|
|
36
|
+
documentation = RuleDocumentation(
|
|
37
|
+
default_level=Severity.WARNING,
|
|
38
|
+
summary="A settings field that names the deployment environment must not declare a default.",
|
|
39
|
+
rationale=(
|
|
40
|
+
"A defaulted environment setting lets a deployment that forgot the variable start as the default "
|
|
41
|
+
"environment, so it quietly behaves like local or development code and can share state with another stack."
|
|
42
|
+
),
|
|
43
|
+
remediation=(
|
|
44
|
+
'Declare the field without a default, for example `environment: Environment = Field(alias="ENVIRONMENT")`, '
|
|
45
|
+
"so a missing value fails at startup."
|
|
46
|
+
),
|
|
47
|
+
category=RuleCategory.CORRECTNESS,
|
|
48
|
+
autofix=AutofixPolicy.NONE,
|
|
49
|
+
limitations=(
|
|
50
|
+
"Only classes deriving from pydantic-settings BaseSettings directly or through a class in the same file are checked.",
|
|
51
|
+
"A field qualifies when its name or literal alias is `environment` in any letter case.",
|
|
52
|
+
"Other environment-isolating settings, such as resource-name prefixes, have no stable name and are not checked.",
|
|
53
|
+
),
|
|
54
|
+
examples=(
|
|
55
|
+
RuleExample(
|
|
56
|
+
example_id="defaulted-environment",
|
|
57
|
+
title="A defaulted environment pretends to be local",
|
|
58
|
+
outcome=ExampleOutcome.MATCH,
|
|
59
|
+
files=(
|
|
60
|
+
ExampleFile.python(
|
|
61
|
+
"service/settings.py",
|
|
62
|
+
"from pydantic import Field\nfrom pydantic_settings import BaseSettings\n\n"
|
|
63
|
+
"class Settings(BaseSettings):\n"
|
|
64
|
+
" environment: str = Field(default='local', alias='ENVIRONMENT')\n",
|
|
65
|
+
),
|
|
66
|
+
),
|
|
67
|
+
focus_path=PurePosixPath("service/settings.py"),
|
|
68
|
+
expected_count=1,
|
|
69
|
+
public=True,
|
|
70
|
+
),
|
|
71
|
+
RuleExample(
|
|
72
|
+
example_id="required-environment",
|
|
73
|
+
title="A required environment fails at startup when missing",
|
|
74
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
75
|
+
files=(
|
|
76
|
+
ExampleFile.python(
|
|
77
|
+
"service/settings.py",
|
|
78
|
+
"from pydantic import Field\nfrom pydantic_settings import BaseSettings\n\n"
|
|
79
|
+
"class Settings(BaseSettings):\n"
|
|
80
|
+
" environment: str = Field(alias='ENVIRONMENT')\n",
|
|
81
|
+
),
|
|
82
|
+
),
|
|
83
|
+
focus_path=PurePosixPath("service/settings.py"),
|
|
84
|
+
expected_count=0,
|
|
85
|
+
public=True,
|
|
86
|
+
),
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
description = documentation.summary
|
|
90
|
+
|
|
91
|
+
@override
|
|
92
|
+
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
93
|
+
tree = context.tree
|
|
94
|
+
if tree is None or context.generated or "BaseSettings" not in context.symbol_source:
|
|
95
|
+
return []
|
|
96
|
+
imports = context.imports
|
|
97
|
+
settings_classes: set[str] = set()
|
|
98
|
+
diagnostics: list[Diagnostic] = []
|
|
99
|
+
for model in tree.body:
|
|
100
|
+
if not isinstance(model, ast.ClassDef) or not _is_settings_class(model, settings_classes, imports):
|
|
101
|
+
continue
|
|
102
|
+
settings_classes.add(model.name)
|
|
103
|
+
diagnostics.extend(
|
|
104
|
+
Diagnostic(
|
|
105
|
+
context.path,
|
|
106
|
+
field.lineno,
|
|
107
|
+
field.col_offset + 1,
|
|
108
|
+
self.code,
|
|
109
|
+
"This environment setting has a default, so a deployment missing the variable starts as that "
|
|
110
|
+
"environment. Remove the default so startup fails instead.",
|
|
111
|
+
severity=Severity.WARNING,
|
|
112
|
+
)
|
|
113
|
+
for field in model.body
|
|
114
|
+
if isinstance(field, ast.AnnAssign) and _is_defaulted_environment(field, imports)
|
|
115
|
+
)
|
|
116
|
+
return diagnostics
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _is_settings_class(model: ast.ClassDef, known: set[str], imports: ImportIndex) -> bool:
|
|
120
|
+
return any(
|
|
121
|
+
(isinstance(base, ast.Name) and base.id in known)
|
|
122
|
+
or imports.resolves(base, sources=_SETTINGS_SOURCES, symbol="BaseSettings")
|
|
123
|
+
for base in model.bases
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _is_defaulted_environment(field: ast.AnnAssign, imports: ImportIndex) -> bool:
|
|
128
|
+
if not isinstance(field.target, ast.Name) or field.value is None or _is_none_default(field):
|
|
129
|
+
return False
|
|
130
|
+
value = field.value
|
|
131
|
+
if not (isinstance(value, ast.Call) and imports.resolves(value.func, sources=_FIELD_SOURCES, symbol="Field")):
|
|
132
|
+
return _is_environment_name(field.target.id, None)
|
|
133
|
+
alias = next(
|
|
134
|
+
(
|
|
135
|
+
keyword.value.value
|
|
136
|
+
for keyword in value.keywords
|
|
137
|
+
if keyword.arg in _ALIAS_KEYWORDS
|
|
138
|
+
and isinstance(keyword.value, ast.Constant)
|
|
139
|
+
and isinstance(keyword.value.value, str)
|
|
140
|
+
),
|
|
141
|
+
None,
|
|
142
|
+
)
|
|
143
|
+
if not _is_environment_name(field.target.id, alias):
|
|
144
|
+
return False
|
|
145
|
+
positional_default = bool(value.args) and not (
|
|
146
|
+
isinstance(value.args[0], ast.Constant) and value.args[0].value is Ellipsis
|
|
147
|
+
)
|
|
148
|
+
return positional_default or any(keyword.arg in _DEFAULT_KEYWORDS for keyword in value.keywords)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _is_environment_name(name: str, alias: str | None) -> bool:
|
|
152
|
+
return name.lower() == _ENVIRONMENT or (alias is not None and alias.lower() == _ENVIRONMENT)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _is_none_default(field: ast.AnnAssign) -> bool:
|
|
156
|
+
value = field.value
|
|
157
|
+
if isinstance(value, ast.Call):
|
|
158
|
+
value = next((kw.value for kw in value.keywords if kw.arg == "default"), value.args[0] if value.args else None)
|
|
159
|
+
return isinstance(value, ast.Constant) and value.value is None
|