diff --git a/.gitignore b/.gitignore index 7772b5c..55b0dc6 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,8 @@ compose.override.yml # Websocket connection credentials **/relay_credentials.json + +# Mutation testing scratch space and reports +.mutation/ +mutants/ +reports/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 99685ba..498bdd1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,8 +35,15 @@ just test-integration just test-slowest just bench just check +just mutation ``` +`just mutation` mutation-tests the relay seam and the upload queue. +It writes survivors to `reports/mutation/survivors.txt` and lists each function whose survivor count differs from `tests/mutation-baseline.txt`. +When one does, the recipe fails. +Read a new survivor with `uv run mutmut show ` from `.mutation/`, then kill it with a test or record it in the baseline. +Give each equivalent mutant you accept a one-line reason. + ## Code Quality The repository uses: diff --git a/justfile b/justfile index 3a274e1..58fba8a 100644 --- a/justfile +++ b/justfile @@ -24,6 +24,19 @@ test-slowest: bench: uv run pytest tests/benchmarks --codspeed +# Mutation-test the relay seam and the upload queue. Survivors land in reports/mutation/. +mutation: + rm -rf .mutation + mkdir -p .mutation/src reports/mutation + cp -r app scripts tests typings pyproject.toml uv.lock .mutation/ + cp -r r9cam_models/src/r9cam_models .mutation/src/ + cd .mutation && uv run --project .. mutmut run --max-children "$(nproc)" + cd .mutation && uv run --project .. mutmut results | grep -E ': (survived|no tests)$' | sed 's/^ *//; s/: .*//' | LC_ALL=C sort > ../reports/mutation/survivors.txt + @echo "Survivors and untested: $(wc -l < reports/mutation/survivors.txt)" + @echo "Survivor counts per function that differ from tests/mutation-baseline.txt (< baseline, > this run):" + @sed 's/ *#.*//; /^$/d; s/__mutmut_[0-9]*$//' tests/mutation-baseline.txt | uniq -c > .mutation/baseline-counts.txt + @sed 's/__mutmut_[0-9]*$//' reports/mutation/survivors.txt | uniq -c | diff .mutation/baseline-counts.txt - + typecheck: uv run ty check . @@ -55,7 +68,7 @@ build: uv build clean: - rm -rf dist/ .venv/ .pytest_cache/ __pycache__/ + rm -rf dist/ .venv/ .pytest_cache/ __pycache__/ .mutation/ reports/ # Print the local direct-connection API key from the local key endpoint. show-key: diff --git a/pyproject.toml b/pyproject.toml index 3e80210..5667085 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,7 @@ { include-group = "test" }, { include-group = "s3" }, "commitizen>=4.8.3", + "mutmut==3.8.0", "prek>=0.4.11", "ruff>=0.9.6", "ty>=0.0.29", @@ -90,6 +91,17 @@ version_provider = "pep621" version_scheme = "semver2" +[tool.mutmut] + # The relay trust boundary and the upload queue. `just mutation` runs this from a + # staged copy where r9cam_models sits under src/, so mutmut derives the same module + # names the tests import. + also_copy = ["scripts", "typings"] + only_mutate = ["app/delivery/*", "app/relay/*", "src/r9cam_models/*"] + # A fixed Hypothesis seed also turns off its example database, so each mutant gets the same verdict every run. + pytest_add_cli_args = ["-q", "-p", "no:cacheprovider", "-p", "no:codspeed", "--hypothesis-seed=0"] + pytest_add_cli_args_test_selection = ["tests/unit", "tests/integration"] + source_paths = ["app", "src/r9cam_models"] + [tool.ruff] fix = true line-length = 120 diff --git a/tests/mutation-baseline.txt b/tests/mutation-baseline.txt new file mode 100644 index 0000000..637d6f2 --- /dev/null +++ b/tests/mutation-baseline.txt @@ -0,0 +1,480 @@ +# Accepted mutmut survivors for `just mutation`. +# One mutant name per line. A trailing comment marks an equivalent mutant and says why. +# Names without a comment are real test gaps, accepted for now. +# Timeouts count as caught: the tests did not pass in time. They are not listed. +# mutmut numbers mutants by position in their function, so editing a function renumbers them. `just mutation` therefore compares survivor counts per function, not names. +# _normalize_relay_path, relay_command_is_allowed and _is_valid_image_id must never appear here. +app.delivery.factory.x__build_s3_sink__mutmut_13 +app.delivery.factory.x__build_s3_sink__mutmut_17 +app.delivery.factory.x__build_s3_sink__mutmut_2 +app.delivery.factory.x__build_s3_sink__mutmut_26 +app.delivery.factory.x__build_s3_sink__mutmut_27 +app.delivery.factory.x__build_s3_sink__mutmut_28 +app.delivery.factory.x__build_s3_sink__mutmut_29 +app.delivery.factory.x__build_s3_sink__mutmut_3 +app.delivery.factory.x__build_s3_sink__mutmut_30 +app.delivery.factory.x__build_s3_sink__mutmut_4 +app.delivery.factory.x__build_s3_sink__mutmut_6 +app.delivery.factory.x__build_s3_sink__mutmut_8 +app.delivery.factory.x__build_s3_sink__mutmut_9 +app.delivery.factory.x_get_image_sink__mutmut_11 +app.delivery.factory.x_get_image_sink__mutmut_12 +app.delivery.factory.x_get_image_sink__mutmut_13 +app.delivery.factory.x_get_image_sink__mutmut_14 +app.delivery.factory.x_get_image_sink__mutmut_15 +app.delivery.factory.x_get_image_sink__mutmut_16 +app.delivery.factory.x_get_image_sink__mutmut_8 +app.delivery.queue.x__dead_letter_entries__mutmut_12 +app.delivery.queue.x__dead_letter_entries__mutmut_13 +app.delivery.queue.x__dead_letter_entries__mutmut_14 +app.delivery.queue.x__dead_letter_entries__mutmut_23 +app.delivery.queue.x__dead_letter_entries__mutmut_7 +app.delivery.queue.x__dead_letter_entries__mutmut_8 +app.delivery.queue.x__directory_bytes__mutmut_7 +app.delivery.queue.x__directory_bytes__mutmut_8 +app.delivery.queue.x__metadata_payload__mutmut_1 +app.delivery.queue.x__metadata_payload__mutmut_2 +app.delivery.queue.x__metadata_payload__mutmut_3 +app.delivery.queue.x__metadata_payload__mutmut_4 +app.delivery.queue.x__metadata_payload__mutmut_6 +app.delivery.queue.x__metadata_payload__mutmut_7 +app.delivery.queue.x__path_mtime__mutmut_1 +app.delivery.queue.x__path_mtime__mutmut_2 +app.delivery.queue.x__path_size__mutmut_1 +app.delivery.queue.x__path_size__mutmut_2 +app.delivery.queue.x__unlink_queue_pair__mutmut_1 +app.delivery.queue.x__unlink_queue_pair__mutmut_2 +app.delivery.queue.x__unlink_queue_pair__mutmut_3 +app.delivery.queue.x__unlink_queue_pair__mutmut_7 +app.delivery.queue.x__validate_image_id__mutmut_3 +app.delivery.queue.x__validate_image_id__mutmut_4 +app.delivery.queue.x__validate_image_id__mutmut_5 +app.delivery.queue.x__validate_image_id__mutmut_6 +app.delivery.queue.x_run_upload_queue_worker__mutmut_2 +app.delivery.queue.x_run_upload_queue_worker__mutmut_3 +app.delivery.queue.x_run_upload_queue_worker__mutmut_5 +app.delivery.queue.x_run_upload_queue_worker__mutmut_6 +app.delivery.queue.x_run_upload_queue_worker__mutmut_7 +app.delivery.queue.x_run_upload_queue_worker__mutmut_8 +app.delivery.queue.xǁUploadQueueǁ__init____mutmut_11 +app.delivery.queue.xǁUploadQueueǁ__init____mutmut_13 +app.delivery.queue.xǁUploadQueueǁ__init____mutmut_15 +app.delivery.queue.xǁUploadQueueǁ__init____mutmut_17 +app.delivery.queue.xǁUploadQueueǁ__init____mutmut_18 +app.delivery.queue.xǁUploadQueueǁ__init____mutmut_19 +app.delivery.queue.xǁUploadQueueǁ__init____mutmut_20 +app.delivery.queue.xǁUploadQueueǁ__init____mutmut_21 +app.delivery.queue.xǁUploadQueueǁ__init____mutmut_22 +app.delivery.queue.xǁUploadQueueǁ_ensure_capacity_for__mutmut_11 +app.delivery.queue.xǁUploadQueueǁ_ensure_capacity_for__mutmut_15 +app.delivery.queue.xǁUploadQueueǁ_ensure_capacity_for__mutmut_16 +app.delivery.queue.xǁUploadQueueǁ_ensure_capacity_for__mutmut_17 +app.delivery.queue.xǁUploadQueueǁ_ensure_capacity_for__mutmut_5 +app.delivery.queue.xǁUploadQueueǁ_ensure_capacity_for__mutmut_6 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_10 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_16 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_17 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_18 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_19 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_20 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_21 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_22 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_31 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_32 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_33 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_34 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_35 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_36 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_37 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_39 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_4 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_40 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_49 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_5 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_50 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_51 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_6 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_66 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_67 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_69 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_7 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_70 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_71 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_72 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_73 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_75 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_76 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_77 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_8 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_85 +app.delivery.queue.xǁUploadQueueǁ_load_entry__mutmut_9 +app.delivery.queue.xǁUploadQueueǁ_prune_dead_letters_sync__mutmut_10 +app.delivery.queue.xǁUploadQueueǁ_prune_dead_letters_sync__mutmut_5 +app.delivery.queue.xǁUploadQueueǁ_prune_dead_letters_sync__mutmut_7 +app.delivery.queue.xǁUploadQueueǁ_prune_dead_letters_sync__mutmut_8 +app.delivery.queue.xǁUploadQueueǁ_prune_dead_letters_sync__mutmut_9 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_16 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_18 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_19 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_21 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_23 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_24 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_27 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_29 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_30 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_32 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_34 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_35 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_36 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_37 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_38 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_39 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_40 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_41 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_42 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_43 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_44 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_45 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_46 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_47 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_48 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_49 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_5 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_50 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_51 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_52 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_53 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_54 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_56 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_57 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_58 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_59 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_60 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_61 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_62 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_65 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_67 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_68 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_70 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_71 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_72 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_73 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_74 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_75 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_76 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_78 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_80 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_82 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_83 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_87 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_88 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_89 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_93 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_94 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_95 +app.delivery.queue.xǁUploadQueueǁdrain_once__mutmut_97 +app.delivery.queue.xǁUploadQueueǁenqueue__mutmut_11 +app.delivery.queue.xǁUploadQueueǁenqueue__mutmut_23 +app.delivery.queue.xǁUploadQueueǁenqueue__mutmut_39 +app.delivery.queue.xǁUploadQueueǁenqueue__mutmut_41 +app.delivery.queue.xǁUploadQueueǁenqueue__mutmut_44 +app.delivery.queue.xǁUploadQueueǁenqueue__mutmut_45 +app.delivery.queue.xǁUploadQueueǁenqueue__mutmut_46 +app.delivery.queue.xǁUploadQueueǁenqueue__mutmut_47 +app.delivery.queue.xǁUploadQueueǁenqueue__mutmut_48 +app.delivery.queue.xǁUploadQueueǁenqueue__mutmut_49 +app.delivery.queue.xǁUploadQueueǁenqueue__mutmut_50 +app.delivery.queue.xǁUploadQueueǁenqueue__mutmut_52 +app.delivery.queue.xǁUploadQueueǁenqueue__mutmut_58 +app.delivery.queue.xǁUploadQueueǁis_due__mutmut_4 +app.delivery.queue.xǁUploadQueueǁmark_attempt_failed__mutmut_13 +app.delivery.queue.xǁUploadQueueǁmark_attempt_failed__mutmut_14 +app.delivery.queue.xǁUploadQueueǁmark_attempt_failed__mutmut_15 +app.delivery.queue.xǁUploadQueueǁmark_attempt_failed__mutmut_20 +app.delivery.queue.xǁUploadQueueǁmark_attempt_failed__mutmut_26 +app.delivery.queue.xǁUploadQueueǁmark_attempt_failed__mutmut_41 +app.delivery.queue.xǁUploadQueueǁmark_attempt_failed__mutmut_43 +app.delivery.queue.xǁUploadQueueǁmark_attempt_failed__mutmut_46 +app.delivery.queue.xǁUploadQueueǁmark_attempt_failed__mutmut_48 +app.delivery.queue.xǁUploadQueueǁmark_attempt_failed__mutmut_50 +app.delivery.queue.xǁUploadQueueǁmark_attempt_failed__mutmut_56 +app.delivery.queue.xǁUploadQueueǁmark_attempt_failed__mutmut_57 +app.delivery.queue.xǁUploadQueueǁmark_attempt_failed__mutmut_58 +app.delivery.queue.xǁUploadQueueǁmark_attempt_failed__mutmut_60 +app.delivery.queue.xǁUploadQueueǁmark_attempt_failed__mutmut_61 +app.delivery.queue.xǁUploadQueueǁmark_attempt_failed__mutmut_62 +app.delivery.queue.xǁUploadQueueǁmark_attempt_failed__mutmut_7 +app.delivery.queue.xǁUploadQueueǁmark_attempt_failed__mutmut_9 +app.delivery.queue.xǁUploadQueueǁmark_attempt_succeeded__mutmut_10 +app.delivery.queue.xǁUploadQueueǁmark_attempt_succeeded__mutmut_12 +app.delivery.queue.xǁUploadQueueǁmark_attempt_succeeded__mutmut_13 +app.delivery.queue.xǁUploadQueueǁmark_attempt_succeeded__mutmut_14 +app.delivery.queue.xǁUploadQueueǁmark_attempt_succeeded__mutmut_15 +app.delivery.queue.xǁUploadQueueǁmark_attempt_succeeded__mutmut_16 +app.delivery.queue.xǁUploadQueueǁmark_attempt_succeeded__mutmut_17 +app.delivery.queue.xǁUploadQueueǁmark_attempt_succeeded__mutmut_18 +app.delivery.queue.xǁUploadQueueǁmark_attempt_succeeded__mutmut_2 +app.delivery.queue.xǁUploadQueueǁmark_attempt_succeeded__mutmut_4 +app.delivery.queue.xǁUploadQueueǁmark_attempt_succeeded__mutmut_5 +app.delivery.queue.xǁUploadQueueǁmark_attempt_succeeded__mutmut_7 +app.delivery.queue.xǁUploadQueueǁmark_attempt_succeeded__mutmut_9 +app.delivery.s3_sink.x__safe_object_key_segment__mutmut_10 +app.delivery.s3_sink.x__safe_object_key_segment__mutmut_11 +app.delivery.s3_sink.x__safe_object_key_segment__mutmut_13 +app.delivery.s3_sink.x__safe_object_key_segment__mutmut_14 +app.delivery.s3_sink.x__safe_object_key_segment__mutmut_15 +app.delivery.s3_sink.x__safe_object_key_segment__mutmut_26 +app.delivery.s3_sink.x__safe_object_key_segment__mutmut_27 +app.delivery.s3_sink.x__safe_object_key_segment__mutmut_3 +app.delivery.s3_sink.x__safe_object_key_segment__mutmut_33 +app.delivery.s3_sink.x__safe_object_key_segment__mutmut_34 +app.delivery.s3_sink.x__safe_object_key_segment__mutmut_9 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ__init____mutmut_1 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ__init____mutmut_10 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ__init____mutmut_11 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ__init____mutmut_12 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ__init____mutmut_13 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ__init____mutmut_14 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ__init____mutmut_16 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ__init____mutmut_17 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ__init____mutmut_18 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ__init____mutmut_20 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ__init____mutmut_21 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ__init____mutmut_22 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ__init____mutmut_25 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ__init____mutmut_26 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ__init____mutmut_27 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ__init____mutmut_4 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ__init____mutmut_5 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ__init____mutmut_6 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ__init____mutmut_7 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ__init____mutmut_8 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ__init____mutmut_9 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_ensure_bucket__mutmut_10 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_ensure_bucket__mutmut_11 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_ensure_bucket__mutmut_12 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_ensure_bucket__mutmut_13 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_ensure_bucket__mutmut_15 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_ensure_bucket__mutmut_16 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_ensure_bucket__mutmut_17 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_ensure_bucket__mutmut_2 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_ensure_bucket__mutmut_21 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_ensure_bucket__mutmut_22 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_ensure_bucket__mutmut_23 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_ensure_bucket__mutmut_3 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_ensure_bucket__mutmut_30 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_ensure_bucket__mutmut_35 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_ensure_bucket__mutmut_37 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_ensure_bucket__mutmut_39 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_ensure_bucket__mutmut_4 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_ensure_bucket__mutmut_41 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_ensure_bucket__mutmut_49 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_ensure_bucket__mutmut_5 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_ensure_bucket__mutmut_6 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_ensure_bucket__mutmut_7 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_ensure_bucket__mutmut_8 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_ensure_bucket__mutmut_9 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_get_client__mutmut_10 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_get_client__mutmut_12 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_get_client__mutmut_13 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_get_client__mutmut_14 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_get_client__mutmut_15 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_get_client__mutmut_16 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_get_client__mutmut_18 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_get_client__mutmut_19 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_get_client__mutmut_6 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_get_client__mutmut_7 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_get_client__mutmut_8 +app.delivery.s3_sink.xǁS3CompatibleSinkǁ_get_client__mutmut_9 +app.delivery.s3_sink.xǁS3CompatibleSinkǁaclose__mutmut_3 +app.delivery.s3_sink.xǁS3CompatibleSinkǁput__mutmut_24 +app.delivery.s3_sink.xǁS3CompatibleSinkǁput__mutmut_25 +app.delivery.s3_sink.xǁS3CompatibleSinkǁput__mutmut_26 +app.delivery.s3_sink.xǁS3CompatibleSinkǁput__mutmut_27 +app.delivery.s3_sink.xǁS3CompatibleSinkǁput__mutmut_32 +app.delivery.s3_sink.xǁS3CompatibleSinkǁput__mutmut_33 +app.delivery.s3_sink.xǁS3CompatibleSinkǁput__mutmut_34 +app.relay.credentials.x__validate_relay_identifier__mutmut_2 +app.relay.credentials.x__validate_relay_identifier__mutmut_4 +app.relay.credentials.x_load_relay_signing_private_key__mutmut_10 +app.relay.credentials.x_load_relay_signing_private_key__mutmut_17 +app.relay.credentials.x_load_relay_signing_private_key__mutmut_19 +app.relay.credentials.x_load_relay_signing_private_key__mutmut_7 +app.relay.credentials.x_load_relay_signing_private_key__mutmut_9 +app.relay.credentials.x_validate_relay_credentials__mutmut_12 +app.relay.credentials.x_validate_relay_credentials__mutmut_20 +app.relay.credentials.x_validate_relay_credentials__mutmut_3 +app.relay.device_jwt.x_build_device_assertion__mutmut_23 +app.relay.device_jwt.x_build_device_assertion__mutmut_5 +app.relay.service.x__drain_pending_tasks__mutmut_3 +app.relay.service.x__drain_pending_tasks__mutmut_5 +app.relay.service.x__drain_pending_tasks__mutmut_6 +app.relay.service.x__format_relay_connection_error__mutmut_3 +app.relay.service.x__handle_command__mutmut_10 +app.relay.service.x__handle_command__mutmut_100 +app.relay.service.x__handle_command__mutmut_105 +app.relay.service.x__handle_command__mutmut_11 +app.relay.service.x__handle_command__mutmut_133 +app.relay.service.x__handle_command__mutmut_140 +app.relay.service.x__handle_command__mutmut_155 +app.relay.service.x__handle_command__mutmut_159 +app.relay.service.x__handle_command__mutmut_164 +app.relay.service.x__handle_command__mutmut_165 +app.relay.service.x__handle_command__mutmut_167 +app.relay.service.x__handle_command__mutmut_27 +app.relay.service.x__handle_command__mutmut_31 +app.relay.service.x__handle_command__mutmut_37 +app.relay.service.x__handle_command__mutmut_38 +app.relay.service.x__handle_command__mutmut_39 +app.relay.service.x__handle_command__mutmut_42 +app.relay.service.x__handle_command__mutmut_45 +app.relay.service.x__handle_command__mutmut_48 +app.relay.service.x__handle_command__mutmut_49 +app.relay.service.x__handle_command__mutmut_5 +app.relay.service.x__handle_command__mutmut_50 +app.relay.service.x__handle_command__mutmut_51 +app.relay.service.x__handle_command__mutmut_52 +app.relay.service.x__handle_command__mutmut_53 +app.relay.service.x__handle_command__mutmut_54 +app.relay.service.x__handle_command__mutmut_55 +app.relay.service.x__handle_command__mutmut_56 +app.relay.service.x__handle_command__mutmut_57 +app.relay.service.x__handle_command__mutmut_58 +app.relay.service.x__handle_command__mutmut_64 +app.relay.service.x__handle_command__mutmut_68 +app.relay.service.x__handle_command__mutmut_69 +app.relay.service.x__handle_command__mutmut_70 +app.relay.service.x__handle_command__mutmut_9 +app.relay.service.x__handle_relay_message__mutmut_1 +app.relay.service.x__handle_relay_message__mutmut_10 +app.relay.service.x__handle_relay_message__mutmut_11 +app.relay.service.x__handle_relay_message__mutmut_13 +app.relay.service.x__handle_relay_message__mutmut_14 +app.relay.service.x__handle_relay_message__mutmut_15 +app.relay.service.x__handle_relay_message__mutmut_16 +app.relay.service.x__handle_relay_message__mutmut_18 +app.relay.service.x__handle_relay_message__mutmut_19 +app.relay.service.x__handle_relay_message__mutmut_2 +app.relay.service.x__handle_relay_message__mutmut_21 +app.relay.service.x__handle_relay_message__mutmut_22 +app.relay.service.x__handle_relay_message__mutmut_23 +app.relay.service.x__handle_relay_message__mutmut_24 +app.relay.service.x__handle_relay_message__mutmut_36 +app.relay.service.x__handle_relay_message__mutmut_4 +app.relay.service.x__handle_relay_message__mutmut_40 +app.relay.service.x__handle_relay_message__mutmut_41 +app.relay.service.x__handle_relay_message__mutmut_42 +app.relay.service.x__handle_relay_message__mutmut_5 +app.relay.service.x__handle_relay_message__mutmut_58 +app.relay.service.x__handle_relay_message__mutmut_6 +app.relay.service.x__handle_relay_message__mutmut_62 +app.relay.service.x__handle_relay_message__mutmut_68 +app.relay.service.x__handle_relay_message__mutmut_69 +app.relay.service.x__handle_relay_message__mutmut_7 +app.relay.service.x__handle_relay_message__mutmut_70 +app.relay.service.x__handle_relay_message__mutmut_77 +app.relay.service.x__handle_relay_message__mutmut_81 +app.relay.service.x__handle_relay_message__mutmut_82 +app.relay.service.x__handle_relay_message__mutmut_84 +app.relay.service.x__iter_websocket_connections__mutmut_15 +app.relay.service.x__iter_websocket_connections__mutmut_16 +app.relay.service.x__iter_websocket_connections__mutmut_17 +app.relay.service.x__iter_websocket_connections__mutmut_18 +app.relay.service.x__iter_websocket_connections__mutmut_27 +app.relay.service.x__iter_websocket_connections__mutmut_28 +app.relay.service.x__iter_websocket_connections__mutmut_3 +app.relay.service.x__iter_websocket_connections__mutmut_32 +app.relay.service.x__iter_websocket_connections__mutmut_33 +app.relay.service.x__iter_websocket_connections__mutmut_34 +app.relay.service.x__iter_websocket_connections__mutmut_36 +app.relay.service.x__iter_websocket_connections__mutmut_7 +app.relay.service.x__receive_loop__mutmut_1 +app.relay.service.x__receive_loop__mutmut_10 +app.relay.service.x__receive_loop__mutmut_11 +app.relay.service.x__receive_loop__mutmut_12 +app.relay.service.x__receive_loop__mutmut_13 +app.relay.service.x__receive_loop__mutmut_14 +app.relay.service.x__receive_loop__mutmut_15 +app.relay.service.x__receive_loop__mutmut_16 +app.relay.service.x__receive_loop__mutmut_17 +app.relay.service.x__receive_loop__mutmut_18 +app.relay.service.x__receive_loop__mutmut_19 +app.relay.service.x__receive_loop__mutmut_2 +app.relay.service.x__receive_loop__mutmut_22 +app.relay.service.x__receive_loop__mutmut_25 +app.relay.service.x__receive_loop__mutmut_27 +app.relay.service.x__receive_loop__mutmut_29 +app.relay.service.x__receive_loop__mutmut_3 +app.relay.service.x__receive_loop__mutmut_30 +app.relay.service.x__receive_loop__mutmut_31 +app.relay.service.x__receive_loop__mutmut_32 +app.relay.service.x__receive_loop__mutmut_34 +app.relay.service.x__receive_loop__mutmut_39 +app.relay.service.x__receive_loop__mutmut_41 +app.relay.service.x__receive_loop__mutmut_48 +app.relay.service.x__receive_loop__mutmut_49 +app.relay.service.x__receive_loop__mutmut_50 +app.relay.service.x__receive_loop__mutmut_53 +app.relay.service.x__receive_loop__mutmut_54 +app.relay.service.x__receive_loop__mutmut_56 +app.relay.service.x__receive_loop__mutmut_57 +app.relay.service.x__receive_loop__mutmut_7 +app.relay.service.x__receive_loop__mutmut_8 +app.relay.service.x__receive_loop__mutmut_9 +app.relay.service.x__relay_reconnect_delays__mutmut_1 +app.relay.service.x__relay_reconnect_delays__mutmut_2 +app.relay.service.x__relay_reconnect_delays__mutmut_3 +app.relay.service.x__relay_reconnect_delays__mutmut_4 +app.relay.service.x__relay_reconnect_delays__mutmut_5 +app.relay.service.x__relay_reconnect_delays__mutmut_6 +app.relay.service.x__relay_reconnect_delays__mutmut_7 +app.relay.service.x__relay_reconnect_delays__mutmut_8 +app.relay.service.x__relay_reconnect_delays__mutmut_9 +app.relay.service.x__run_command__mutmut_4 +app.relay.service.x__run_command__mutmut_8 +app.relay.service.x__send_error__mutmut_10 +app.relay.service.x__send_error__mutmut_8 +app.relay.service.xǁRelayServiceǁbuild_url__mutmut_5 +app.relay.service.xǁRelayServiceǁrun_forever__mutmut_10 +app.relay.service.xǁRelayServiceǁrun_forever__mutmut_11 +app.relay.service.xǁRelayServiceǁrun_forever__mutmut_18 +app.relay.service.xǁRelayServiceǁrun_forever__mutmut_19 +app.relay.service.xǁRelayServiceǁrun_forever__mutmut_2 +app.relay.service.xǁRelayServiceǁrun_forever__mutmut_20 +app.relay.service.xǁRelayServiceǁrun_forever__mutmut_21 +app.relay.service.xǁRelayServiceǁrun_forever__mutmut_22 +app.relay.service.xǁRelayServiceǁrun_forever__mutmut_23 +app.relay.service.xǁRelayServiceǁrun_forever__mutmut_24 +app.relay.service.xǁRelayServiceǁrun_forever__mutmut_27 +app.relay.service.xǁRelayServiceǁrun_forever__mutmut_28 +app.relay.service.xǁRelayServiceǁrun_forever__mutmut_3 +app.relay.service.xǁRelayServiceǁrun_forever__mutmut_35 +app.relay.service.xǁRelayServiceǁrun_forever__mutmut_38 +app.relay.service.xǁRelayServiceǁrun_forever__mutmut_4 +app.relay.service.xǁRelayServiceǁrun_forever__mutmut_41 +app.relay.service.xǁRelayServiceǁrun_forever__mutmut_42 +app.relay.service.xǁRelayServiceǁrun_forever__mutmut_47 +app.relay.service.xǁRelayServiceǁrun_forever__mutmut_48 +app.relay.service.xǁRelayServiceǁrun_forever__mutmut_49 +app.relay.service.xǁRelayServiceǁrun_forever__mutmut_5 +app.relay.service.xǁRelayServiceǁrun_forever__mutmut_50 +app.relay.service.xǁRelayServiceǁrun_forever__mutmut_51 +app.relay.service.xǁRelayServiceǁrun_forever__mutmut_52 +app.relay.service.xǁRelayServiceǁrun_forever__mutmut_53 +app.relay.service.xǁRelayServiceǁrun_forever__mutmut_54 +app.relay.service.xǁRelayServiceǁrun_forever__mutmut_7 +app.relay.service.xǁRelayServiceǁrun_forever__mutmut_8 +app.relay.service.xǁRelayServiceǁrun_forever__mutmut_9 +app.relay.state.xǁRelayRuntimeStateǁmark_connected__mutmut_3 +app.relay.state.xǁRelayRuntimeStateǁmark_disconnected__mutmut_1 +app.relay.state.xǁRelayRuntimeStateǁmark_disconnected__mutmut_2 +r9cam_models.device_seam.x_build_relay_command__mutmut_10 +r9cam_models.device_seam.x_build_relay_command__mutmut_11 +r9cam_models.device_seam.x_build_relay_command__mutmut_14 +r9cam_models.device_seam.x_build_relay_command__mutmut_5 +r9cam_models.device_seam.x_extract_safe_relay_headers__mutmut_6 +r9cam_models.device_seam.x_relay_content_type_is_binary__mutmut_11 # equivalent: split(";", 2) and split(";", 1) give the same first element +r9cam_models.device_seam.x_relay_content_type_is_binary__mutmut_2 +r9cam_models.device_seam.x_relay_content_type_is_binary__mutmut_8 # equivalent: split(";") and split(";", 1) give the same first element +r9cam_models.device_seam.x_relay_content_type_is_binary__mutmut_9 +r9cam_models.device_seam.xǁPairingPollResponseǁwaiting__mutmut_1 diff --git a/tests/unit/core/test_device_seam_models.py b/tests/unit/core/test_device_seam_models.py index a9b426d..ca39823 100644 --- a/tests/unit/core/test_device_seam_models.py +++ b/tests/unit/core/test_device_seam_models.py @@ -6,6 +6,7 @@ import pytest from pydantic import TypeAdapter, ValidationError +from r9cam_models.device_seam import _normalize_relay_path from r9cam_models import ( PAIRING_CODE_ALPHABET, @@ -192,6 +193,33 @@ def test_relay_command_policy_rejects_out_of_scope_commands(method: str, path: s assert not relay_command_is_allowed(method, path) +@pytest.mark.parametrize( + ("path", "expected"), + [ + ("/", "/"), + ("//", "/"), + ("/camera/", "/camera"), + ("/camera ", "/camera "), + ("/cameraX", "/cameraX"), + ("/preview/hls/X", "/preview/hls/X"), + ], +) +def test_relay_path_normalization_only_strips_trailing_slashes(path: str, expected: str) -> None: + """Only trailing slashes are dropped, and the root path stays the root path.""" + assert _normalize_relay_path(path) == expected + + +def test_relay_command_policy_accepts_trailing_slash() -> None: + """A trailing slash should reach the same allowlist entry as the bare path.""" + assert relay_command_is_allowed("GET", "/camera/") + + +@pytest.mark.parametrize("path", ["/camera ", "/camera\t", "/cameraX", "/camera/X"]) +def test_relay_command_policy_rejects_near_miss_suffixes(path: str) -> None: + """Trailing whitespace or letters must not normalize into an allowlisted path.""" + assert not relay_command_is_allowed("GET", path) + + @pytest.mark.parametrize( "content_type", [ diff --git a/uv.lock b/uv.lock index b65684c..6a0abab 100644 --- a/uv.lock +++ b/uv.lock @@ -749,6 +749,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, ] +[[package]] +name = "libcst" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml-ft" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/c0/098e5c91ff1537f00c85a6438b6cb1863d17144680cc91f47c87f104a200/libcst-1.9.0.tar.gz", hash = "sha256:087b58a9afe076bb08e2d726478e1f16cb928d67ffa9092817e033c335de522a", size = 914739, upload-time = "2026-07-29T21:28:43.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/b1/befc0544283bb3923a928accf79ed685e5a725524bdb3491826670affc07/libcst-1.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b8b9df30f524317b097dc53065b25dda33d6a4cc3c7c8bf4fc83ca7559c58cc0", size = 2043754, upload-time = "2026-07-29T19:24:59.885Z" }, + { url = "https://files.pythonhosted.org/packages/45/50/fef7c172a8457c95894edf5fb04805024899cdfea41fa01b0636587b79e1/libcst-1.9.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e465a7bc9c2b9533eb9e06d2391f8819f811b5112919d4064c9fb8565aaafa08", size = 2203548, upload-time = "2026-07-29T19:25:01.323Z" }, + { url = "https://files.pythonhosted.org/packages/18/ff/764cd2be1fd99d774fc44039c319dc0ed1d9d9afeaa02759a05121cccd4b/libcst-1.9.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8504b422c95676a8c27b517e1ac01413ece91bf356865c587ca9bdcd5708a2f7", size = 2255274, upload-time = "2026-07-29T19:25:02.764Z" }, + { url = "https://files.pythonhosted.org/packages/34/a7/474748a27a02fa83e3556b260d5f5236ca48164fa7beffecf3b2cad24dca/libcst-1.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bcb9f9d4fcfe2ec7a40d2c26e03a538d5d9dc189c38551eb3f94ab661afee7c0", size = 2269126, upload-time = "2026-07-29T19:25:04.158Z" }, + { url = "https://files.pythonhosted.org/packages/8a/b7/655e45363b8cf87b91e41e060b21c89e5316c7ef36eb14a1a498b27bb71e/libcst-1.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8d671c39a431c309476099b8ec811e412503ec0f4465f6fc907cb51c70e8e6b", size = 2378602, upload-time = "2026-07-29T19:25:06.424Z" }, + { url = "https://files.pythonhosted.org/packages/db/13/6da63f0902ece43bf9d737017251ad7ad06edd9d3c4e1856450403cb4473/libcst-1.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:4d382fba04077eb556a1ea4295a4482e192aa93639c5a07ba0885841965ea0c0", size = 2103486, upload-time = "2026-07-29T19:25:07.979Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ff/dccb1a55e38b4e47256a97242d61d2bef5366c744faf88fb087d4fa0f995/libcst-1.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:a621e261990148c1cfbe26c1798bd2375c131cf358a44a7a4659fc41c1a336e1", size = 1979907, upload-time = "2026-07-29T19:25:09.532Z" }, +] + +[[package]] +name = "linkify-it-py" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/98/7a1a5f31fd5c7ba93e963b168e244b8e3dd705b3d2a718e3c3307583bf57/linkify_it_py-2.2.0.tar.gz", hash = "sha256:907acd2d17ac1fbb9ddb62c8957ccbd6158cac602231a15c3b0cd1e215f03cee", size = 32939, upload-time = "2026-08-29T07:07:08.305Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/d4/1152d1c7ab42d8b908be64fd200ddc870dc9d4925e951198702084aa1a7d/linkify_it_py-2.2.0-py3-none-any.whl", hash = "sha256:3adc40eb5af300b2605fcfdb968c24e1d780a90f1f2221af7c15e5111e94d443", size = 21971, upload-time = "2026-08-29T07:07:07.164Z" }, +] + [[package]] name = "markdown-it-py" version = "4.2.0" @@ -761,6 +788,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, ] +[package.optional-dependencies] +linkify = [ + { name = "linkify-it-py" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -791,6 +823,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, ] +[[package]] +name = "mdit-py-plugins" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -832,6 +876,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/be/59/e26cb779be4c591d1a910f59d29aca9fba4de70349840a833beba2652371/multidict-6.9.1-py3-none-any.whl", hash = "sha256:7bf6478188f4e47bf5686e8a33da4ae28bf43b1b2528d9ee144d28492bfac60b", size = 19176, upload-time = "2026-09-21T17:59:03.501Z" }, ] +[[package]] +name = "mutmut" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "coverage" }, + { name = "libcst" }, + { name = "pytest" }, + { name = "setproctitle" }, + { name = "textual" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/30/b334a22184026d92a7549c8a76ad1d397ea65a6f8534ef486556ab861c70/mutmut-3.8.0.tar.gz", hash = "sha256:bc670276518f893ab356f1d28826c49b7e606e7cf89c300e3888c0b52fc1d2ed", size = 83631, upload-time = "2026-09-12T16:53:58.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/2a/a9904a45804c1de50c0c3baad1512b78d5b66d2befd841104b8a62bf0f35/mutmut-3.8.0-py3-none-any.whl", hash = "sha256:4c0abcef6b8d1116f2adfaad19df12e1c621e4a402d4367ef15bf8cba497b91d", size = 81469, upload-time = "2026-09-12T16:53:56.906Z" }, +] + [[package]] name = "opentelemetry-api" version = "1.45.1" @@ -1045,6 +1106,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/87/7d/8d02eba9f7ecc87f8e90f731e032cc4773802f2a843763afd0c66a535ff4/pillow-13.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:40a266371ece581e5de13b6635ef78d7487824af80561b6ca27694a88c19bc0f", size = 2698826, upload-time = "2026-10-16T22:16:25.382Z" }, ] +[[package]] +name = "platformdirs" +version = "4.13.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/e6/d0abbb256b44e6a05748c6942511ff67db8dbb068de0c35b24bf1b395b09/platformdirs-4.13.5.tar.gz", hash = "sha256:05831adf87d392760f4b5c3a0a95579ab5a10b18b9b9bd43680dcb47544788a0", size = 62362, upload-time = "2026-10-19T01:02:11.778Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/8a/1d8cb3842909dc193a73835adedf412e32e18f8ffd59f43983949622fe6f/platformdirs-4.13.5-py3-none-any.whl", hash = "sha256:53d3ec4280b815c1e0b02edad7f9ee8fda1ba9c0865dd9c2e6e63849ce82fc1e", size = 32779, upload-time = "2026-10-19T01:02:10.17Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -1373,6 +1443,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, ] +[[package]] +name = "pyyaml-ft" +version = "8.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/eb/5a0d575de784f9a1f94e2b1288c6886f13f34185e13117ed530f32b6f8a8/pyyaml_ft-8.0.0.tar.gz", hash = "sha256:0c947dce03954c7b5d38869ed4878b2e6ff1d44b08a0d84dc83fdad205ae39ab", size = 141057, upload-time = "2025-06-10T15:32:15.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/ba/a067369fe61a2e57fb38732562927d5bae088c73cb9bb5438736a9555b29/pyyaml_ft-8.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8c1306282bc958bfda31237f900eb52c9bedf9b93a11f82e1aab004c9a5657a6", size = 187027, upload-time = "2025-06-10T15:31:48.722Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c5/a3d2020ce5ccfc6aede0d45bcb870298652ac0cf199f67714d250e0cdf39/pyyaml_ft-8.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30c5f1751625786c19de751e3130fc345ebcba6a86f6bddd6e1285342f4bbb69", size = 176146, upload-time = "2025-06-10T15:31:50.584Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bb/23a9739291086ca0d3189eac7cd92b4d00e9fdc77d722ab610c35f9a82ba/pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fa992481155ddda2e303fcc74c79c05eddcdbc907b888d3d9ce3ff3e2adcfb0", size = 746792, upload-time = "2025-06-10T15:31:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c2/e8825f4ff725b7e560d62a3609e31d735318068e1079539ebfde397ea03e/pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cec6c92b4207004b62dfad1f0be321c9f04725e0f271c16247d8b39c3bf3ea42", size = 786772, upload-time = "2025-06-10T15:31:54.712Z" }, + { url = "https://files.pythonhosted.org/packages/35/be/58a4dcae8854f2fdca9b28d9495298fd5571a50d8430b1c3033ec95d2d0e/pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06237267dbcab70d4c0e9436d8f719f04a51123f0ca2694c00dd4b68c338e40b", size = 778723, upload-time = "2025-06-10T15:31:56.093Z" }, + { url = "https://files.pythonhosted.org/packages/86/ed/fed0da92b5d5d7340a082e3802d84c6dc9d5fa142954404c41a544c1cb92/pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a7f332bc565817644cdb38ffe4739e44c3e18c55793f75dddb87630f03fc254", size = 758478, upload-time = "2025-06-10T15:31:58.314Z" }, + { url = "https://files.pythonhosted.org/packages/f0/69/ac02afe286275980ecb2dcdc0156617389b7e0c0a3fcdedf155c67be2b80/pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7d10175a746be65f6feb86224df5d6bc5c049ebf52b89a88cf1cd78af5a367a8", size = 799159, upload-time = "2025-06-10T15:31:59.675Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ac/c492a9da2e39abdff4c3094ec54acac9747743f36428281fb186a03fab76/pyyaml_ft-8.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:58e1015098cf8d8aec82f360789c16283b88ca670fe4275ef6c48c5e30b22a96", size = 158779, upload-time = "2025-06-10T15:32:01.029Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9b/41998df3298960d7c67653669f37710fa2d568a5fc933ea24a6df60acaf6/pyyaml_ft-8.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5f3e2ceb790d50602b2fd4ec37abbd760a8c778e46354df647e7c5a4ebb", size = 191331, upload-time = "2025-06-10T15:32:02.602Z" }, + { url = "https://files.pythonhosted.org/packages/0f/16/2710c252ee04cbd74d9562ebba709e5a284faeb8ada88fcda548c9191b47/pyyaml_ft-8.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d445bf6ea16bb93c37b42fdacfb2f94c8e92a79ba9e12768c96ecde867046d1", size = 182879, upload-time = "2025-06-10T15:32:04.466Z" }, + { url = "https://files.pythonhosted.org/packages/9a/40/ae8163519d937fa7bfa457b6f78439cc6831a7c2b170e4f612f7eda71815/pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c56bb46b4fda34cbb92a9446a841da3982cdde6ea13de3fbd80db7eeeab8b49", size = 811277, upload-time = "2025-06-10T15:32:06.214Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/28d82dbff7f87b96f0eeac79b7d972a96b4980c1e445eb6a857ba91eda00/pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dab0abb46eb1780da486f022dce034b952c8ae40753627b27a626d803926483b", size = 831650, upload-time = "2025-06-10T15:32:08.076Z" }, + { url = "https://files.pythonhosted.org/packages/e8/df/161c4566facac7d75a9e182295c223060373d4116dead9cc53a265de60b9/pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd48d639cab5ca50ad957b6dd632c7dd3ac02a1abe0e8196a3c24a52f5db3f7a", size = 815755, upload-time = "2025-06-10T15:32:09.435Z" }, + { url = "https://files.pythonhosted.org/packages/05/10/f42c48fa5153204f42eaa945e8d1fd7c10d6296841dcb2447bf7da1be5c4/pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:052561b89d5b2a8e1289f326d060e794c21fa068aa11255fe71d65baf18a632e", size = 810403, upload-time = "2025-06-10T15:32:11.051Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d2/e369064aa51009eb9245399fd8ad2c562bd0bcd392a00be44b2a824ded7c/pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3bb4b927929b0cb162fb1605392a321e3333e48ce616cdcfa04a839271373255", size = 835581, upload-time = "2025-06-10T15:32:12.897Z" }, + { url = "https://files.pythonhosted.org/packages/c0/28/26534bed77109632a956977f60d8519049f545abc39215d086e33a61f1f2/pyyaml_ft-8.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:de04cfe9439565e32f178106c51dd6ca61afaa2907d143835d501d84703d3793", size = 171579, upload-time = "2025-06-10T15:32:14.34Z" }, +] + [[package]] name = "questionary" version = "2.1.1" @@ -1408,6 +1502,7 @@ dev = [ { name = "aioboto3" }, { name = "commitizen" }, { name = "hypothesis" }, + { name = "mutmut" }, { name = "prek" }, { name = "pytest" }, { name = "pytest-asyncio" }, @@ -1448,6 +1543,7 @@ dev = [ { name = "aioboto3", specifier = ">=13.0" }, { name = "commitizen", specifier = ">=4.8.3" }, { name = "hypothesis", specifier = ">=6.170.2" }, + { name = "mutmut", specifier = "==3.8.0" }, { name = "prek", specifier = ">=0.4.11" }, { name = "pytest", specifier = ">=8.0" }, { name = "pytest-asyncio", specifier = ">=0.25" }, @@ -1593,6 +1689,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2b/28/465ad9382be98f2172e691f5836cf87f936773913ad7ab85ba1ba1d6706e/sentry_sdk-2.68.1-py3-none-any.whl", hash = "sha256:775b78871783a0ffd758276ad01b3bb2b1ebcdad8f9d2f0a7723f76b73c99b65", size = 520851, upload-time = "2026-08-24T13:09:36.186Z" }, ] +[[package]] +name = "setproctitle" +version = "1.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/b0/6b8a516c5a9e9630bd5293db78314ac012f690305fe93beadea388626efb/setproctitle-1.3.8.tar.gz", hash = "sha256:cafe209d064a6efb88cb45a03e97981ff8832802b2b5d009dde0197a3b7b41c8", size = 27214, upload-time = "2026-10-01T21:09:27.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/46/125791eb19c85340c6d43de8c821b143aecbfa0882343f244f5744474229/setproctitle-1.3.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a4a782f9ab26e662a890c1a7d865c452c83ed554de44aa83da35fa5bdcc79921", size = 18100, upload-time = "2026-10-01T20:59:33.41Z" }, + { url = "https://files.pythonhosted.org/packages/47/51/73b1e62c3e019e683e58d31f9f8af57e6750f36ce079004d29b7790e22ce/setproctitle-1.3.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d63adfa4be8f9dbac4be6b359e2a8eb15a733545ef613d02a12ae27a4d40e975", size = 13116, upload-time = "2026-10-01T20:59:35.159Z" }, + { url = "https://files.pythonhosted.org/packages/78/ae/095c6ad159e3a7b95bc6d9a3eae18abfea1ebc79ed3377617406a1668150/setproctitle-1.3.8-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d4289e209e805d7d276a534f062092974fbe6814889d4c132c247205ebb49a65", size = 33275, upload-time = "2026-10-01T20:59:37.235Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6e/5196258efd941cb874b3f4da91460601b867fdc434eba48a70455736d4be/setproctitle-1.3.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8bb543b4b58ad839f7f0d335a7f4b40cef6989952e7239de3532de0b7771607e", size = 34124, upload-time = "2026-10-01T20:59:39.529Z" }, + { url = "https://files.pythonhosted.org/packages/36/01/e9754e0916e6dda232a8c337bda69d2fea33acee0c5d3bdf580b8155a50d/setproctitle-1.3.8-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b705f01a531dba8fbbab90e30d432af4cc70a79c0a33b238e2c061816140cad0", size = 36006, upload-time = "2026-10-01T20:59:41.501Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a9/c63d2d745f18423937429a8611743e7b3e17259173150fbcdaa59daa4a51/setproctitle-1.3.8-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8e286e4c4e90063072a6e088a8d6526a7ffceb47429db88ba50ba58eeebcc758", size = 32268, upload-time = "2026-10-01T20:59:43.795Z" }, + { url = "https://files.pythonhosted.org/packages/36/0c/a72818902f32ea9b848db5ccc828eb9cd9bc8661975610e1d46ea4aacbe2/setproctitle-1.3.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7b77c8671974288cf192e481ed63e928f1086762e97e8845d09d65ba121f8aaa", size = 33179, upload-time = "2026-10-01T20:59:45.809Z" }, + { url = "https://files.pythonhosted.org/packages/bb/55/fa59463ae6b957048e0e842d4b5513eced1b71554d8af0f5d91cd23cde23/setproctitle-1.3.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0e85b55eac9abfa3166ab00dac21e718ac2624a1df7d0b75559a2d3e3781cb33", size = 34472, upload-time = "2026-10-01T20:59:47.858Z" }, + { url = "https://files.pythonhosted.org/packages/b7/77/1f0cf55055788bf7eb9a146040a64d7ec42cd56153894856effc70e943ec/setproctitle-1.3.8-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f215ec9237d5f8e96d0205be6e2141292c20b133343c2153b6d4934f523ef422", size = 31428, upload-time = "2026-10-01T20:59:49.753Z" }, + { url = "https://files.pythonhosted.org/packages/32/98/d1d8a6cd620d72a04f5d3335125e5b2fc5667bd06a76bb5bbc50b101e804/setproctitle-1.3.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b0e70a5d294e4c8cf7dc94e015fca6d4913779a424937e66b429fba82d8eb5e2", size = 32776, upload-time = "2026-10-01T20:59:51.763Z" }, + { url = "https://files.pythonhosted.org/packages/4b/34/8b9eea1e80ac7a7f622eead8a0e4d73a60098344ed6f373e111f7ce8d29d/setproctitle-1.3.8-cp313-cp313-win32.whl", hash = "sha256:f85b4d87273e0ec63568433bbb8ccf160b548c49b53576fe010bad9f698b8586", size = 12105, upload-time = "2026-10-01T20:59:53.444Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f0/98883c658859d331faf91eccac8946cb2f8feb2590905015b129fdeb09cd/setproctitle-1.3.8-cp313-cp313-win_amd64.whl", hash = "sha256:bcdf74fe8a6e3b31bed1bc7d3e2a38cfbda3263d2750492ea0725dbc2db5aacf", size = 12933, upload-time = "2026-10-01T20:59:55.045Z" }, +] + [[package]] name = "shellingham" version = "1.5.4" @@ -1641,6 +1757,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, ] +[[package]] +name = "textual" +version = "8.2.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", extra = ["linkify"] }, + { name = "mdit-py-plugins" }, + { name = "platformdirs" }, + { name = "pygments" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/21/39a76b01bd5eea82a04baaca7580e105d8c59450df03998345bb2cfb307b/textual-8.2.8.tar.gz", hash = "sha256:3f106a9fbc73e39dd266c9712432087de78a6d644084c7c241d6a25c3169115b", size = 1860502, upload-time = "2026-06-30T06:51:24.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/be/35261223d9416a0751cdff1c7b4a6f881387218a12d439fe22fefebc8c04/textual-8.2.8-py3-none-any.whl", hash = "sha256:267375fd402dc8d981457212efa71f0e3365fd17bba144ba9bb3ed7563cb374a", size = 731418, upload-time = "2026-06-30T06:51:26.364Z" }, +] + [[package]] name = "tomlkit" version = "0.15.1"