sarj-python-lint 0.81.18__tar.gz → 0.82.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-0.81.18 → sarj_python_lint-0.82.0}/PKG-INFO +1 -1
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/pyproject.toml +1 -1
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +101 -30
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +61 -3
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/.gitignore +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/LICENSE +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/README.md +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_deleted_only_override_parameter.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-0.81.18 → sarj_python_lint-0.82.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: 0.
|
|
3
|
+
Version: 0.82.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/
|
|
@@ -18,8 +18,9 @@ from sarj_python_lint.rule_base import (
|
|
|
18
18
|
parse_or_none,
|
|
19
19
|
)
|
|
20
20
|
from sarj_python_lint.rules._ast_index import nodes, walk
|
|
21
|
-
from sarj_python_lint.rules._paths import is_generated
|
|
21
|
+
from sarj_python_lint.rules._paths import is_generated, is_test_path
|
|
22
22
|
from sarj_python_lint.rules._sql import is_store_module, sql_string_value, strip_sql_noise
|
|
23
|
+
from sarj_python_lint.rules.no_analytical_aggregation_in_postgres_store import analytical_signal
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
if TYPE_CHECKING:
|
|
@@ -29,7 +30,6 @@ if TYPE_CHECKING:
|
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
_QUERY_SHAPE = re.compile(r"\bSELECT\b[\s\S]*?\bFROM\b", re.IGNORECASE)
|
|
32
|
-
_DERIVED_RELATION_GATE = re.compile(r"\b(?:FROM|JOIN)\s*\(", re.IGNORECASE)
|
|
33
33
|
_POSTGRES_IMPORT_PREFIXES = (
|
|
34
34
|
"asyncpg",
|
|
35
35
|
"psycopg",
|
|
@@ -37,13 +37,7 @@ _POSTGRES_IMPORT_PREFIXES = (
|
|
|
37
37
|
"psycopg_pool",
|
|
38
38
|
"sqlalchemy.dialects.postgresql",
|
|
39
39
|
)
|
|
40
|
-
|
|
41
|
-
"aiosqlite",
|
|
42
|
-
"clickhouse_connect",
|
|
43
|
-
"clickhouse_driver",
|
|
44
|
-
"google.cloud.bigquery",
|
|
45
|
-
"sqlite3",
|
|
46
|
-
)
|
|
40
|
+
_AMBIGUOUS_RELATIONAL_IMPORT_PREFIXES = ("aiosqlite", "sqlite3")
|
|
47
41
|
_QUERY_SINKS = frozenset({"execute", "executemany", "fetch", "fetchrow", "fetchval", "prepare"})
|
|
48
42
|
_QUERY_KEYWORDS = frozenset({"command", "operation", "query", "statement"})
|
|
49
43
|
_QUERY_RECEIVER_TOKENS = frozenset(
|
|
@@ -61,6 +55,12 @@ _BIGQUERY_SQL = re.compile(
|
|
|
61
55
|
)
|
|
62
56
|
_COMPOSABLE_HOLE = re.compile(r"(?<!\{)\{(?:[A-Za-z_]\w*|\d*)\}(?!\})")
|
|
63
57
|
_SQL_HOLE = "__sarj_sql_hole__"
|
|
58
|
+
_JOIN_LIMIT = 4
|
|
59
|
+
_STAGE_LIMIT = 6
|
|
60
|
+
_COMBINED_JOIN_LIMIT = 3
|
|
61
|
+
_COMBINED_STAGE_LIMIT = 4
|
|
62
|
+
_WIDE_PROJECTION_LIMIT = 25
|
|
63
|
+
_WIDE_PROJECTION_JOIN_LIMIT = 2
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
class _ConstructorImport(NamedTuple):
|
|
@@ -308,7 +308,7 @@ def _expression_args(node: exp.Expr, key: str) -> tuple[exp.Expr, ...]:
|
|
|
308
308
|
return tuple(child for child in node.iter_expressions() if child.arg_key == key)
|
|
309
309
|
|
|
310
310
|
|
|
311
|
-
def _parse_signal(sql: str) -> str | None:
|
|
311
|
+
def _parse_signal(sql: str, *, include_extended_shapes: bool) -> str | None:
|
|
312
312
|
import sqlglot # ruff: ignore[import-outside-top-level] -- parse only SQL that passes cheap ownership and shape gates
|
|
313
313
|
from sqlglot.errors import SqlglotError # ruff: ignore[import-outside-top-level] -- paired with lazy parser import
|
|
314
314
|
|
|
@@ -318,16 +318,31 @@ def _parse_signal(sql: str) -> str | None:
|
|
|
318
318
|
except SqlglotError:
|
|
319
319
|
return None
|
|
320
320
|
for statement in statements:
|
|
321
|
-
if
|
|
321
|
+
if (
|
|
322
|
+
statement is not None
|
|
323
|
+
and (signal := _architecture_signal(statement, include_extended_shapes=include_extended_shapes)) is not None
|
|
324
|
+
):
|
|
322
325
|
return signal
|
|
323
326
|
return None
|
|
324
327
|
|
|
325
328
|
|
|
326
|
-
def _architecture_signal(statement: exp.Expr) -> str | None:
|
|
329
|
+
def _architecture_signal(statement: exp.Expr, *, include_extended_shapes: bool) -> str | None:
|
|
327
330
|
from sqlglot import exp # ruff: ignore[import-outside-top-level] -- keep parser startup off unrelated lint runs
|
|
328
331
|
|
|
329
332
|
if not isinstance(statement, exp.Query):
|
|
330
333
|
return None
|
|
334
|
+
maximum_joins = max(
|
|
335
|
+
(
|
|
336
|
+
sum(isinstance(join, exp.Join) and "pivots" in join.args for join in _expression_args(select, "joins"))
|
|
337
|
+
for select in statement.find_all(exp.Select)
|
|
338
|
+
),
|
|
339
|
+
default=0,
|
|
340
|
+
)
|
|
341
|
+
if maximum_joins >= _JOIN_LIMIT:
|
|
342
|
+
return f"SELECT block has {maximum_joins} JOINs (4+ explicit JOINs)"
|
|
343
|
+
if not include_extended_shapes:
|
|
344
|
+
return None
|
|
345
|
+
nested_queries = sum(1 for query in statement.walk() if isinstance(query, exp.Query)) - 1
|
|
331
346
|
for select in _root_selects(statement):
|
|
332
347
|
for join in _expression_args(select, "joins"):
|
|
333
348
|
if not isinstance(join, exp.Join):
|
|
@@ -339,6 +354,15 @@ def _architecture_signal(statement: exp.Expr) -> str | None:
|
|
|
339
354
|
continue
|
|
340
355
|
if (derived := _derived_query(_expression_arg(from_clause, "this"))) is not None and _has_complex_body(derived):
|
|
341
356
|
return "Complex FROM-derived query"
|
|
357
|
+
if nested_queries >= _STAGE_LIMIT:
|
|
358
|
+
return f"Query has {nested_queries} CTE/subquery stages"
|
|
359
|
+
if maximum_joins >= _COMBINED_JOIN_LIMIT and nested_queries >= _COMBINED_STAGE_LIMIT:
|
|
360
|
+
return f"Query combines {maximum_joins} JOINs with {nested_queries} CTE/subquery stages"
|
|
361
|
+
for select in _root_selects(statement):
|
|
362
|
+
projections = tuple(_expression_args(select, "expressions"))
|
|
363
|
+
joins = tuple(join for join in _expression_args(select, "joins") if isinstance(join, exp.Join))
|
|
364
|
+
if len(projections) >= _WIDE_PROJECTION_LIMIT and len(joins) >= _WIDE_PROJECTION_JOIN_LIMIT:
|
|
365
|
+
return f"Joined query block has {len(projections)} projected expressions"
|
|
342
366
|
return None
|
|
343
367
|
|
|
344
368
|
|
|
@@ -389,29 +413,64 @@ class ComplexPostgresQueryRequiresArchitectureReview(Rule):
|
|
|
389
413
|
id: str = "complex-postgres-query-requires-architecture-review"
|
|
390
414
|
code: str = "SARJ437"
|
|
391
415
|
documentation = RuleDocumentation(
|
|
392
|
-
summary="Complex
|
|
416
|
+
summary="Complex executable PostgreSQL query shapes require architecture review.",
|
|
393
417
|
rationale=(
|
|
394
|
-
"
|
|
395
|
-
"
|
|
396
|
-
"
|
|
418
|
+
"Join-heavy, deeply staged, or wide joined reads can obscure cardinality, bounds, ordering, and locking "
|
|
419
|
+
"semantics and can signal repeated read-time reconstruction. Syntax alone does not establish runtime "
|
|
420
|
+
"cost, a bad data model, or the right datastore."
|
|
397
421
|
),
|
|
398
422
|
remediation=(
|
|
399
|
-
"
|
|
400
|
-
"
|
|
401
|
-
"
|
|
402
|
-
"
|
|
423
|
+
"Review whether the schema exposes the operational fact directly, together with production-like "
|
|
424
|
+
"cardinality and EXPLAIN output. Simplify the query or read model when warranted, but do not mechanically "
|
|
425
|
+
"replace database joins with application joins. Keep atomic coordination in one statement; consider a "
|
|
426
|
+
"columnar store only for measured repeated analytical reads with an explicit freshness contract."
|
|
403
427
|
),
|
|
404
428
|
category=RuleCategory.ARCHITECTURE,
|
|
405
429
|
autofix=AutofixPolicy.NONE,
|
|
406
430
|
limitations=(
|
|
407
|
-
"Only statically recoverable SQL passed to execute, executemany, fetch, fetchrow, fetchval, or prepare in
|
|
431
|
+
"Only statically recoverable SQL passed to execute, executemany, fetch, fetchrow, fetchval, or prepare in non-test modules with explicit PostgreSQL imports is analyzed; extended derived, staged, and wide-shape review requires a recognized store module.",
|
|
408
432
|
"Execution receivers must use a conventional database name; custom wrappers and dynamically obtained receivers abstain.",
|
|
409
433
|
"One direct, unambiguous simple-name binding in the same lexical scope is followed; standalone constants, branches, aliases, attributes, containers, wildcard imports, and cross-scope flow abstain.",
|
|
410
|
-
"A derived relation
|
|
411
|
-
"
|
|
412
|
-
"
|
|
434
|
+
"A query is reviewed for a complex derived relation, at least four joins in one query block, at least six nested query stages, three joins plus four stages, or 25 projections plus two joins.",
|
|
435
|
+
"Projection width alone is accepted, and UNION branches are measured independently; execution plans and production cardinality remain authoritative.",
|
|
436
|
+
"ClickHouse and BigQuery syntax is excluded per query, including in mixed-backend modules.",
|
|
437
|
+
"The rule does not infer optimizer behavior, materialization, performance, data-model quality, or datastore placement.",
|
|
413
438
|
),
|
|
414
439
|
examples=(
|
|
440
|
+
RuleExample(
|
|
441
|
+
example_id="multi-join-root-discovery",
|
|
442
|
+
title="A read repeatedly discovers a derived root through multiple joins",
|
|
443
|
+
outcome=ExampleOutcome.MATCH,
|
|
444
|
+
files=(
|
|
445
|
+
ExampleFile.python(
|
|
446
|
+
"app/call_store.py",
|
|
447
|
+
'import psycopg\ncursor.execute("SELECT call.id FROM simulated_batch '
|
|
448
|
+
"JOIN batch_call ON batch_call.batch_id = simulated_batch.batch_id "
|
|
449
|
+
"JOIN call ON call.batch_call_id = batch_call.id "
|
|
450
|
+
"JOIN campaign_call ON campaign_call.call_id = call.id "
|
|
451
|
+
'JOIN campaign ON campaign.id = campaign_call.campaign_id WHERE simulated_batch.batch_id = %s")\n',
|
|
452
|
+
),
|
|
453
|
+
),
|
|
454
|
+
focus_path=PurePosixPath("app/call_store.py"),
|
|
455
|
+
expected_count=1,
|
|
456
|
+
public=True,
|
|
457
|
+
scenario="multi-join-review",
|
|
458
|
+
),
|
|
459
|
+
RuleExample(
|
|
460
|
+
example_id="stored-root-lookup",
|
|
461
|
+
title="A read uses a root resolved and stored by its owning write workflow",
|
|
462
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
463
|
+
files=(
|
|
464
|
+
ExampleFile.python(
|
|
465
|
+
"app/call_store.py",
|
|
466
|
+
'import psycopg\ncursor.execute("SELECT root_call_id FROM canary_run WHERE id = %s")\n',
|
|
467
|
+
),
|
|
468
|
+
),
|
|
469
|
+
focus_path=PurePosixPath("app/call_store.py"),
|
|
470
|
+
expected_count=0,
|
|
471
|
+
public=True,
|
|
472
|
+
scenario="multi-join-review",
|
|
473
|
+
),
|
|
415
474
|
RuleExample(
|
|
416
475
|
example_id="ranked-queue-claim",
|
|
417
476
|
title="A queue claim hides ranking inside nested derived relations",
|
|
@@ -453,14 +512,14 @@ class ComplexPostgresQueryRequiresArchitectureReview(Rule):
|
|
|
453
512
|
|
|
454
513
|
@override
|
|
455
514
|
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
456
|
-
if
|
|
515
|
+
if is_test_path(path) or is_generated(path, source):
|
|
457
516
|
return []
|
|
458
517
|
tree = parse_or_none(path, source)
|
|
459
518
|
if tree is None:
|
|
460
519
|
return []
|
|
461
520
|
imports = _import_names(tree)
|
|
462
521
|
if not _has_import_prefix(imports, _POSTGRES_IMPORT_PREFIXES) or _has_import_prefix(
|
|
463
|
-
imports,
|
|
522
|
+
imports, _AMBIGUOUS_RELATIONAL_IMPORT_PREFIXES
|
|
464
523
|
):
|
|
465
524
|
return []
|
|
466
525
|
|
|
@@ -481,13 +540,24 @@ class ComplexPostgresQueryRequiresArchitectureReview(Rule):
|
|
|
481
540
|
sql_without_noise = strip_sql_noise(text_value)
|
|
482
541
|
if (
|
|
483
542
|
_QUERY_SHAPE.search(sql_without_noise) is None
|
|
484
|
-
or _DERIVED_RELATION_GATE.search(sql_without_noise) is None
|
|
485
543
|
or _CLICKHOUSE_SQL.search(sql_without_noise)
|
|
486
544
|
or _BIGQUERY_SQL.search(sql_without_noise)
|
|
545
|
+
or (is_store_module(path) and analytical_signal(sql_without_noise) is not None)
|
|
487
546
|
):
|
|
488
547
|
continue
|
|
489
|
-
if (signal := _parse_signal(text_value)) is None:
|
|
548
|
+
if (signal := _parse_signal(text_value, include_extended_shapes=is_store_module(path))) is None:
|
|
490
549
|
continue
|
|
550
|
+
coordination = re.search(
|
|
551
|
+
r"\b(?:FOR\s+(?:NO\s+KEY\s+)?UPDATE|FOR\s+SHARE|SKIP\s+LOCKED)\b", text_value, re.IGNORECASE
|
|
552
|
+
)
|
|
553
|
+
guidance = (
|
|
554
|
+
" preserve the atomic statement; review bounds, ordering, and locks, then document its cardinality, "
|
|
555
|
+
"supporting indexes, and production-like plan."
|
|
556
|
+
if coordination is not None
|
|
557
|
+
else " review bounds, ordering, and locks; review cardinality and a production-like query plan, then "
|
|
558
|
+
"review whether repeated read-time reconstruction should become a stable derived fact maintained at "
|
|
559
|
+
"write-time, or otherwise simplify the query or read model when warranted."
|
|
560
|
+
)
|
|
491
561
|
diagnostics.append(
|
|
492
562
|
Diagnostic(
|
|
493
563
|
path=path,
|
|
@@ -496,8 +566,9 @@ class ComplexPostgresQueryRequiresArchitectureReview(Rule):
|
|
|
496
566
|
code=self.code,
|
|
497
567
|
severity=Severity.WARNING,
|
|
498
568
|
message=(
|
|
499
|
-
f"{signal}
|
|
500
|
-
"
|
|
569
|
+
f"{signal} requires architecture review;{guidance} "
|
|
570
|
+
"Review warning only, not a defect or cost claim; syntax does not prove a bad data model or "
|
|
571
|
+
"an offload decision."
|
|
501
572
|
),
|
|
502
573
|
)
|
|
503
574
|
)
|
|
@@ -25,6 +25,8 @@ from sarj_python_lint.rules._sql import is_store_module, sql_string_value, strip
|
|
|
25
25
|
if TYPE_CHECKING:
|
|
26
26
|
from pathlib import Path
|
|
27
27
|
|
|
28
|
+
from sqlglot import exp
|
|
29
|
+
|
|
28
30
|
|
|
29
31
|
_QUERY_SHAPE = re.compile(r"\bSELECT\b[\s\S]*?\bFROM\b", re.IGNORECASE)
|
|
30
32
|
_POSTGRES_OWNER = re.compile(
|
|
@@ -49,6 +51,11 @@ _COMMON_AGGREGATE = re.compile(
|
|
|
49
51
|
r"\b(?:COUNT|SUM|AVG|MIN|MAX|ARRAY_AGG|STRING_AGG|JSONB?_AGG|BOOL_AND|BOOL_OR|EVERY)\s*\(",
|
|
50
52
|
re.IGNORECASE,
|
|
51
53
|
)
|
|
54
|
+
_LOCKING_READ = re.compile(r"\b(?:FOR\s+(?:NO\s+KEY\s+)?UPDATE|FOR\s+SHARE|SKIP\s+LOCKED)\b", re.IGNORECASE)
|
|
55
|
+
_PLACEHOLDER = re.compile(r"%(?:\([^)]+\))?s")
|
|
56
|
+
_GROUP_KEYS_LIMIT = 2
|
|
57
|
+
_GROUP_AGGREGATES_LIMIT = 2
|
|
58
|
+
_SINGLE_GROUP_AGGREGATES_LIMIT = 3
|
|
52
59
|
_STRONG_ANALYTICAL: tuple[tuple[str, re.Pattern[str]], ...] = (
|
|
53
60
|
("STDDEV", re.compile(r"\bSTDDEV(?:_POP|_SAMP)?\s*\(", re.IGNORECASE)),
|
|
54
61
|
("VARIANCE", re.compile(r"\b(?:VARIANCE|VAR_POP|VAR_SAMP)\s*\(", re.IGNORECASE)),
|
|
@@ -71,15 +78,66 @@ def _docstring_node_ids(tree: ast.AST) -> set[int]:
|
|
|
71
78
|
return result
|
|
72
79
|
|
|
73
80
|
|
|
74
|
-
def
|
|
81
|
+
def analytical_signal(sql: str) -> str | None:
|
|
82
|
+
if _LOCKING_READ.search(sql) is not None:
|
|
83
|
+
return None
|
|
75
84
|
for label, pattern in _STRONG_ANALYTICAL:
|
|
76
85
|
if pattern.search(sql):
|
|
77
86
|
return label
|
|
78
87
|
if _TIME_BUCKET.search(sql) and _GROUP_BY.search(sql) and _COMMON_AGGREGATE.search(sql):
|
|
79
88
|
return "time-bucketed GROUP BY"
|
|
89
|
+
return _grouped_reporting_signal(sql)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _grouped_reporting_signal(sql: str) -> str | None:
|
|
93
|
+
import sqlglot # ruff: ignore[import-outside-top-level] -- parse only aggregate-shaped SQL after cheap gates
|
|
94
|
+
from sqlglot import exp # ruff: ignore[import-outside-top-level] -- paired with lazy parser import
|
|
95
|
+
from sqlglot.errors import SqlglotError # ruff: ignore[import-outside-top-level] -- paired with lazy parser import
|
|
96
|
+
|
|
97
|
+
if _GROUP_BY.search(sql) is None or _COMMON_AGGREGATE.search(sql) is None:
|
|
98
|
+
return None
|
|
99
|
+
try:
|
|
100
|
+
statements = sqlglot.parse(_PLACEHOLDER.sub("?", sql), read="postgres")
|
|
101
|
+
except SqlglotError:
|
|
102
|
+
return None
|
|
103
|
+
for statement in statements:
|
|
104
|
+
if not isinstance(statement, exp.Query):
|
|
105
|
+
continue
|
|
106
|
+
for select in statement.find_all(exp.Select):
|
|
107
|
+
if _is_identity_bounded(select):
|
|
108
|
+
continue
|
|
109
|
+
group = select.args.get("group")
|
|
110
|
+
group_count = len(group.expressions) if isinstance(group, exp.Group) else 0
|
|
111
|
+
aggregate_count = sum(1 for aggregate in select.find_all(exp.AggFunc) if aggregate.parent_select is select)
|
|
112
|
+
if (
|
|
113
|
+
group_count >= _GROUP_KEYS_LIMIT and aggregate_count >= _GROUP_AGGREGATES_LIMIT
|
|
114
|
+
) or aggregate_count >= _SINGLE_GROUP_AGGREGATES_LIMIT:
|
|
115
|
+
return "grouped reporting shape"
|
|
80
116
|
return None
|
|
81
117
|
|
|
82
118
|
|
|
119
|
+
def _is_identity_bounded(select: object) -> bool:
|
|
120
|
+
from sqlglot import exp # ruff: ignore[import-outside-top-level] -- shared with lazy parser path
|
|
121
|
+
|
|
122
|
+
if not isinstance(select, exp.Select):
|
|
123
|
+
return False
|
|
124
|
+
where = select.args.get("where")
|
|
125
|
+
if not isinstance(where, exp.Where):
|
|
126
|
+
return False
|
|
127
|
+
equality = _expression_arg(where, "this")
|
|
128
|
+
if not isinstance(equality, exp.EQ):
|
|
129
|
+
return False
|
|
130
|
+
left = _expression_arg(equality, "this")
|
|
131
|
+
right = _expression_arg(equality, "expression")
|
|
132
|
+
return (isinstance(left, exp.Column) and left.name.casefold() == "id" and not isinstance(right, exp.Column)) or (
|
|
133
|
+
isinstance(right, exp.Column) and right.name.casefold() == "id" and not isinstance(left, exp.Column)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _expression_arg(node: exp.Expr, key: str) -> exp.Expr | None:
|
|
138
|
+
return next((child for child in node.iter_expressions() if child.arg_key == key), None)
|
|
139
|
+
|
|
140
|
+
|
|
83
141
|
@final
|
|
84
142
|
class NoAnalyticalAggregationInPostgresStore(Rule):
|
|
85
143
|
id: str = "no-analytical-aggregation-in-postgres-store"
|
|
@@ -100,7 +158,7 @@ class NoAnalyticalAggregationInPostgresStore(Rule):
|
|
|
100
158
|
aliases=("no-aggregation-in-store-query",),
|
|
101
159
|
limitations=(
|
|
102
160
|
"Only SQL string literals in recognized store modules with positive PostgreSQL ownership evidence are analyzed.",
|
|
103
|
-
"
|
|
161
|
+
"Statistical aggregates, time-bucketed rollups, and broad grouped reporting shapes are reported; locked coordination reads and identity-bounded aggregates are excluded.",
|
|
104
162
|
"ClickHouse- and BigQuery-specific query syntax is excluded per query, including in mixed-backend modules.",
|
|
105
163
|
),
|
|
106
164
|
examples=(
|
|
@@ -166,7 +224,7 @@ class NoAnalyticalAggregationInPostgresStore(Rule):
|
|
|
166
224
|
or _BIGQUERY_SQL.search(sql)
|
|
167
225
|
):
|
|
168
226
|
continue
|
|
169
|
-
signal =
|
|
227
|
+
signal = analytical_signal(sql)
|
|
170
228
|
if signal is None:
|
|
171
229
|
continue
|
|
172
230
|
diagnostics.append(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/_analysis_session.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/_comments.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/_docstrings.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/_first_party.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/_fixed_record.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/_project_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/_prose_budget.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/_registry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/_test_assertions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/mock_without_spec.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_comment_cruft.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_dunder_all.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_secret_in_log.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/no_select_star.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/over_mocked_test.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/prefer_class_row.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/prefer_or_pattern.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/prefer_str_enum.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.18 → sarj_python_lint-0.82.0}/src/sarj_python_lint/rules/unused_mock_setup.py
RENAMED
|
File without changes
|
|
File without changes
|