Repository: py-cov-action/python-coverage-comment-action
Pull request #466 (2024-08-31 15:36:00)

Branch coverage
This PR builds on @ewjoachim 's WIP #413, completing the changes and updating the relevant tests so that all tests pass successfully when run locally with `poetry run pytest`. The pre-commit hooks were also run on all commits, and any ruff changes incorporated. I have not, however, run the E2E tests; I figured that could wait until  after the PR was opened.

The PR branch contains four commits, the first being @ewjoachim's WIP changes to `coverage_comment.coverage`. The second commit is my own changes to complete the branch-coverage support.
 
A single new unit test, `test_compute_coverage_with_branches`, is added to `tests/unit/test_coverage.py` in the third commit, to exercise the updated `coverage_comment.coverage.compute_coverage` functionality in cases where (non-zero) branch coverage values are included.

The fourth commit adjusts the `conftest.py` fixtures and the various tests in `test_template.py` to both supply and expect coverage data computed accounting for branch coverage, raising the actual coverage for the fixture(s) data from 60% to 
53.84%
 62.5%.

I'll add a bit of commentary on specific changes as review comments, so they're easier to discuss in the proper context.


Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5b06df5..8901382 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -23,7 +23,7 @@ jobs:
           password: ${{ secrets.DOCKER_PASSWORD }}
 
       - name: Build and push Docker image
-        uses: docker/build-push-action@v6
+        uses: docker/build-push-action@v5
         with:
           # See Dockerfile.build for instructions on bumping this.
           tags: ewjoachim/python-coverage-comment-action-base:v6
diff --git a/.gitignore b/.gitignore
index 95d0cfe..d1b1e7f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,3 @@
 .coverage
 dev-env-vars
 .coverage*
-__pycache__
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 3536c50..8d3a7c6 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -18,7 +18,7 @@ repos:
   - id: check-added-large-files
 
 - repo: https://github.com/astral-sh/ruff-pre-commit
-  rev: v0.6.3
+  rev: v0.4.5
   hooks:
     - id: ruff
       args: [ --fix ]
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index bbd6c82..8f81769 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -64,3 +64,16 @@ To run the end-to-end tests, you'll need:
 - Please be aware that the tests will launch `gh auth setup-git` which might be
   surprising if you use `https` remotes (sadly, setting `GIT_CONFIG_GLOBAL`
   seems not to be enough to isolate tests.)
+
+## Coverage labs
+
+### Computing the coverage rate
+
+The coverage rate is `covered_lines / total_lines` (as one would expect).
+In case "branch coverage" is enabled, the coverage rate is
+`(covered_lines + covered_branches) / (total_lines + total_branches)`.
+In order to display coverage rates, we need to round the values. Depending on
+the situation, we either round to 0 or 2 decimal places. Rounding rules are:
+- We always round down (truncate) the value.
+- We don't display the trailing zeros in the decimal part (nor the decimal point
+  if the decimal part is 0).
diff --git a/Makefile b/Makefile
deleted file mode 100644
index e7ee4bd..0000000
--- a/Makefile
+++ /dev/null
@@ -1,12 +0,0 @@
-.PHONY: install
-install: ## install dependencies
-	poetry install --with dev
-	pre-commit install
-
-.PHONY: lint
-lint: ## lint code
-	pre-commit
-
-.PHONY: test
-test: ## run all tests
-	poetry run pytest tests
diff --git a/coverage_comment/coverage.py b/coverage_comment/coverage.py
index 564649a..591e9de 100644
--- a/coverage_comment/coverage.py
+++ b/coverage_comment/coverage.py
@@ -13,7 +13,7 @@ from coverage_comment import log, subprocess
 # The dataclasses in this module are accessible in the template, which is overridable by the user.
 # As a coutesy, we should do our best to keep the existing fields for backward compatibility,
 # and if we really can't and can't add properties, at least bump the major version.
-@dataclasses.dataclass
+@dataclasses.dataclass(kw_only=True)
 class CoverageMetadata:
     version: str
     timestamp: datetime.datetime
@@ -21,26 +21,28 @@ class CoverageMetadata:
     show_contexts: bool
 
 
-@dataclasses.dataclass
+@dataclasses.dataclass(kw_only=True)
 class CoverageInfo:
     covered_lines: int
     num_statements: int
     percent_covered: decimal.Decimal
     missing_lines: int
     excluded_lines: int
-    num_branches: int | None
-    num_partial_branches: int | None
-    covered_branches: int | None
-    missing_branches: int | None
+    num_branches: int = 0
+    num_partial_branches: int = 0
+    covered_branches: int = 0
+    missing_branches: int = 0
 
 
-@dataclasses.dataclass
+@dataclasses.dataclass(kw_only=True)
 class FileCoverage:
     path: pathlib.Path
     executed_lines: list[int]
     missing_lines: list[int]
     excluded_lines: list[int]
     info: CoverageInfo
+    executed_branches: list[list[int]] | None = None
+    missing_branches: list[list[int]] | None = None
 
 
 @dataclasses.dataclass
@@ -56,7 +58,7 @@ class Coverage:
 # Maybe in v4, we can change it to a simpler format.
 
 
-@dataclasses.dataclass
+@dataclasses.dataclass(kw_only=True)
 class FileDiffCoverage:
     path: pathlib.Path
     percent_covered: decimal.Decimal
@@ -73,7 +75,7 @@ class FileDiffCoverage:
         return self.missing_statements
 
 
-@dataclasses.dataclass
+@dataclasses.dataclass(kw_only=True)
 class DiffCoverage:
     total_num_lines: int
     total_num_violations: int
@@ -82,10 +84,18 @@ class DiffCoverage:
     files: dict[pathlib.Path, FileDiffCoverage]
 
 
-def compute_coverage(num_covered: int, num_total: int) -> decimal.Decimal:
-    if num_total == 0:
+def compute_coverage(
+    num_covered: int,
+    num_total: int,
+    num_branches_covered: int = 0,
+    num_branches_total: int = 0,
+) -> decimal.Decimal:
+    """Compute the coverage percentage, with or without branch coverage."""
+    numerator = decimal.Decimal(num_covered + num_branches_covered)
+    denominator = decimal.Decimal(num_total + num_branches_total)
+    if denominator == 0:
         return decimal.Decimal("1")
-    return decimal.Decimal(num_covered) / decimal.Decimal(num_total)
+    return numerator / denominator
 
 
 def get_coverage_info(
@@ -138,6 +148,26 @@ def generate_coverage_markdown(coverage_path: pathlib.Path) -> str:
     )
 
 
+def _make_coverage_info(data: dict) -> CoverageInfo:
+    """Build a CoverageInfo object from a "summary" or "totals" key."""
+    return CoverageInfo(
+        covered_lines=data["covered_lines"],
+        num_statements=data["num_statements"],
+        percent_covered=compute_coverage(
+            num_covered=data["covered_lines"],
+            num_total=data["num_statements"],
+            num_branches_covered=data.get("covered_branches", 0),
+            num_branches_total=data.get("num_branches", 0),
+        ),
+        missing_lines=data["missing_lines"],
+        excluded_lines=data["excluded_lines"],
+        num_branches=data.get("num_branches", 0),
+        num_partial_branches=data.get("num_partial_branches", 0),
+        covered_branches=data.get("covered_branches", 0),
+        missing_branches=data.get("missing_branches", 0),
+    )
+
+
 def extract_info(data: dict, coverage_path: pathlib.Path) -> Coverage:
     """
     {
@@ -191,39 +221,13 @@ def extract_info(data: dict, coverage_path: pathlib.Path) -> Coverage:
                 excluded_lines=file_data["excluded_lines"],
                 executed_lines=file_data["executed_lines"],
                 missing_lines=file_data["missing_lines"],
-                info=CoverageInfo(
-                    covered_lines=file_data["summary"]["covered_lines"],
-                    num_statements=file_data["summary"]["num_statements"],
-                    percent_covered=compute_coverage(
-                        file_data["summary"]["covered_lines"],
-                        file_data["summary"]["num_statements"],
-                    ),
-                    missing_lines=file_data["summary"]["missing_lines"],
-                    excluded_lines=file_data["summary"]["excluded_lines"],
-                    num_branches=file_data["summary"].get("num_branches"),
-                    num_partial_branches=file_data["summary"].get(
-                        "num_partial_branches"
-                    ),
-                    covered_branches=file_data["summary"].get("covered_branches"),
-                    missing_branches=file_data["summary"].get("missing_branches"),
-                ),
+                executed_branches=file_data.get("executed_branches"),
+                missing_branches=file_data.get("missing_branches"),
+                info=_make_coverage_info(file_data["summary"]),
             )
             for path, file_data in data["files"].items()
         },
-        info=CoverageInfo(
-            covered_lines=data["totals"]["covered_lines"],
-            num_statements=data["totals"]["num_statements"],
-            percent_covered=compute_coverage(
-                data["totals"]["covered_lines"],
-                data["totals"]["num_statements"],
-            ),
-            missing_lines=data["totals"]["missing_lines"],
-            excluded_lines=data["totals"]["excluded_lines"],
-            num_branches=data["totals"].get("num_branches"),
-            num_partial_branches=data["totals"].get("num_partial_branches"),
-            covered_branches=data["totals"].get("covered_branches"),
-            missing_branches=data["totals"].get("missing_branches"),
-        ),
+        info=_make_coverage_info(data["totals"]),
     )
 
 
@@ -256,7 +260,8 @@ def get_diff_coverage_info(
         total_num_violations += count_missing
 
         percent_covered = compute_coverage(
-            num_covered=count_executed, num_total=count_total
+            num_covered=count_executed,
+            num_total=count_total,
         )
 
         files[path] = FileDiffCoverage(
diff --git a/coverage_comment/github.py b/coverage_comment/github.py
index 9a06931..4ca3591 100644
--- a/coverage_comment/github.py
+++ b/coverage_comment/github.py
@@ -54,15 +54,15 @@ def download_artifact(
     filename: pathlib.Path,
 ) -> str:
     repo_path = github.repos(repository)
-
+    artifacts = repo_path.actions.runs(run_id).artifacts.get().artifacts
     try:
         artifact = next(
-            artifact
-            for artifact in _fetch_artifacts(repo_path, run_id)
-            if artifact.name == artifact_name
+            iter(artifact for artifact in artifacts if artifact.name == artifact_name),
         )
     except StopIteration:
-        raise NoArtifact(f"No artifact found with name {artifact_name} in run {run_id}")
+        raise NoArtifact(
+            f"Not artifact found with name {artifact_name} in run {run_id}"
+        )
 
     zip_bytes = io.BytesIO(repo_path.actions.artifacts(artifact.id).zip.get(bytes=True))
     zipf = zipfile.ZipFile(zip_bytes)
@@ -73,24 +73,6 @@ def download_artifact(
         raise NoArtifact(f"File named {filename} not found in artifact {artifact_name}")
 
 
-def _fetch_artifacts(repo_path, run_id):
-    page = 1
-    total_fetched = 0
-
-    while True:
-        result = repo_path.actions.runs(run_id).artifacts.get(page=str(page))
-        if not result or not result.artifacts:
-            break
-
-        yield from result.artifacts
-
-        total_fetched += len(result.artifacts)
-        if total_fetched >= result.total_count:
-            break
-
-        page += 1
-
-
 def get_branch_from_workflow_run(
     github: github_client.GitHub, repository: str, run_id: int
 ) -> tuple[str, str]:
diff --git a/coverage_comment/main.py b/coverage_comment/main.py
index 71c5bed..6b08dae 100644
--- a/coverage_comment/main.py
+++ b/coverage_comment/main.py
@@ -182,25 +182,6 @@ def process_pr(
             marker=marker,
             subproject_id=config.SUBPROJECT_ID,
         )
-        # Same as above except `max_files` is None
-        summary_comment = template.get_comment_markdown(
-            coverage=coverage,
-            diff_coverage=diff_coverage,
-            previous_coverage=previous_coverage,
-            previous_coverage_rate=previous_coverage_rate,
-            files=files_info,
-            count_files=count_files,
-            max_files=None,
-            minimum_green=config.MINIMUM_GREEN,
-            minimum_orange=config.MINIMUM_ORANGE,
-            repo_name=config.GITHUB_REPOSITORY,
-            pr_number=config.GITHUB_PR_NUMBER,
-            base_template=template.read_template_file("comment.md.j2"),
-            custom_template=config.COMMENT_TEMPLATE,
-            pr_targets_default_branch=pr_targets_default_branch,
-            marker=marker,
-            subproject_id=config.SUBPROJECT_ID,
-        )
     except template.MissingMarker:
         log.error(
             "Marker not found. This error can happen if you defined a custom comment "
@@ -219,7 +200,7 @@ def process_pr(
         return 1
 
     github.add_job_summary(
-        content=summary_comment, github_step_summary=config.GITHUB_STEP_SUMMARY
+        content=comment, github_step_summary=config.GITHUB_STEP_SUMMARY
     )
     pr_number: int | None = config.GITHUB_PR_NUMBER
     if pr_number is None:
diff --git a/coverage_comment/settings.py b/coverage_comment/settings.py
index 5d4360b..ed0c27c 100644
--- a/coverage_comment/settings.py
+++ b/coverage_comment/settings.py
@@ -125,14 +125,14 @@ class Config:
     @property
     def GITHUB_PR_NUMBER(self) -> int | None:
         # "refs/pull/2/merge"
-        if self.GITHUB_REF.startswith("refs/pull"):
+        if "pull" in self.GITHUB_REF:
             return int(self.GITHUB_REF.split("/")[2])
         return None
 
     @property
     def GITHUB_BRANCH_NAME(self) -> str | None:
         # "refs/heads/my_branch_name"
-        if self.GITHUB_REF.startswith("refs/heads"):
+        if "heads" in self.GITHUB_REF:
             return self.GITHUB_REF.split("/", 2)[2]
         return None
 
diff --git a/coverage_comment/template.py b/coverage_comment/template.py
index ed680cf..7f682aa 100644
--- a/coverage_comment/template.py
+++ b/coverage_comment/template.py
@@ -74,16 +74,6 @@ def delta(val: int) -> str:
     return f"({sign(val)}{val})"
 
 
-def compact(val: int) -> str:
-    if val < 1_000:
-        return str(val)
-    if val < 10_000:
-        return f"{val / 1_000:.1f}k"
-    if val < 1_000_000:
-        return f"{val / 1_000:.0f}k"
-    return f"{val / 1_000_000:.0f}M"
-
-
 def remove_exponent(val: decimal.Decimal) -> decimal.Decimal:
     # From https://docs.python.org/3/library/decimal.html#decimal-faq
     return (
@@ -146,7 +136,6 @@ def get_comment_markdown(
     env.filters["get_evolution_color"] = badge.get_evolution_badge_color
     env.filters["generate_badge"] = badge.get_static_badge_url
     env.filters["pluralize"] = pluralize
-    env.filters["compact"] = compact
     env.filters["file_url"] = functools.partial(
         get_file_url, repo_name=repo_name, pr_number=pr_number
     )
diff --git a/coverage_comment/template_files/comment.md.j2 b/coverage_comment/template_files/comment.md.j2
index 53816f9..6fb2fcf 100644
--- a/coverage_comment/template_files/comment.md.j2
+++ b/coverage_comment/template_files/comment.md.j2
@@ -45,7 +45,7 @@
 {% set color = "007ec6" -%}
 {% set message = statements_count %}
 {% endif -%}
-<td align="center"><a href="{{ path | file_url }}"><img title="{{ text }}" src="{{ statements_count | compact | generate_badge(message=(message | delta), color=color) }}"></a></td>
+<td align="center"><a href="{{ path | file_url }}"><img title="{{ text }}" src="{{ statements_count | string | generate_badge(message=(message | delta), color=color) }}"></a></td>
 
 {%- endmacro -%}
 
@@ -65,7 +65,7 @@
 {%- set message = missing_lines_count -%}
 {%- endif -%}
 {%- set color = message | get_evolution_color(up_is_good=false) -%}
-<td align="center"><a href="{{ path | file_url }}"><img title="{{ text }}" src="{{ missing_lines_count | compact | generate_badge(message=(message | delta), color=color) }}"></a></td>
+<td align="center"><a href="{{ path | file_url }}"><img title="{{ text }}" src="{{ missing_lines_count | string | generate_badge(message=(message | delta), color=color) }}"></a></td>
 
 {%- endmacro -%}
 
@@ -80,10 +80,10 @@
 {%- set text = "This PR doesn't change the coverage rate in " ~ path ~ ", which is " ~ percent_covered | pct ~ " (" ~ covered_statements_count ~ "/" ~ statements_count ~ ")." -%}
 {%- endif -%}
 {%- set color = coverage_diff | get_evolution_color() -%}
-{%- set message = "(" ~ previous_covered_statements_count | compact ~ "/" ~ previous_statements_count | compact ~ " > " ~ covered_statements_count | compact ~ "/" ~ statements_count | compact ~ ")" -%}
+{%- set message = "(" ~ previous_covered_statements_count ~ "/" ~ previous_statements_count ~ " > " ~ covered_statements_count ~ "/" ~ statements_count ~ ")" -%}
 {%- else -%}
 {%- set text = "The coverage rate of " ~ path ~ " is " ~ percent_covered | pct ~ " (" ~ covered_statements_count ~ "/" ~ statements_count ~ "). The file did not seem to exist on the base branch." -%}
-{%- set message = "(" ~ covered_statements_count | compact ~ "/" ~ statements_count | compact ~ ")" -%}
+{%- set message = "(" ~ covered_statements_count ~ "/" ~ statements_count ~ ")" -%}
 {%- set color = percent_covered | x100 | get_badge_color -%}
 {%- endif -%}
 <td align="center"><a href="{{ path | file_url }}"><img title="{{ text }}" src="{{ percent_covered | pct(precision=0) | generate_badge(message=message, color=color) }}"></a></td>
@@ -94,7 +94,7 @@
 {% if added_statements_count -%}
 {% set text = "In this PR, " ~ (added_statements_count) ~ " new statements are added to " ~ path ~ ", " ~ covered_statements_count ~ " of which are covered (" ~ (percent_covered | pct) ~ ")." -%}
 {% set label = (percent_covered | pct(precision=0)) -%}
-{% set message = "(" ~ covered_statements_count | compact ~ "/" ~ added_statements_count | compact ~ ")" -%}
+{% set message = "(" ~ covered_statements_count ~ "/" ~ added_statements_count ~ ")" -%}
 {%- set color = (percent_covered | x100 | get_badge_color()) -%}
 {% else -%}
 {% set text = "This PR does not seem to add statements to " ~ path ~ "." -%}
diff --git a/poetry.lock b/poetry.lock
index ddf9db0..9bcb569 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -2,13 +2,13 @@
 
 [[package]]
 name = "anyio"
-version = "4.4.0"
+version = "4.3.0"
 description = "High level compatibility layer for multiple asynchronous event loop implementations"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"},
-    {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"},
+    {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"},
+    {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"},
 ]
 
 [package.dependencies]
@@ -22,13 +22,13 @@ trio = ["trio (>=0.23)"]
 
 [[package]]
 name = "certifi"
-version = "2024.7.4"
+version = "2024.2.2"
 description = "Python package for providing Mozilla's CA Bundle."
 optional = false
 python-versions = ">=3.6"
 files = [
-    {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
-    {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
+    {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"},
+    {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"},
 ]
 
 [[package]]
@@ -44,83 +44,63 @@ files = [
 
 [[package]]
 name = "coverage"
-version = "7.6.1"
+version = "7.5.1"
 description = "Code coverage measurement for Python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"},
-    {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"},
-    {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"},
-    {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"},
-    {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"},
-    {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"},
-    {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"},
-    {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"},
-    {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"},
-    {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"},
-    {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"},
-    {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"},
-    {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"},
-    {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"},
-    {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"},
-    {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"},
-    {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"},
-    {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"},
-    {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"},
-    {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"},
-    {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"},
-    {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"},
-    {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"},
-    {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"},
-    {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"},
-    {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"},
-    {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"},
-    {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"},
-    {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"},
-    {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"},
-    {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"},
-    {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"},
-    {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"},
-    {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"},
-    {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"},
-    {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"},
-    {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"},
-    {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"},
-    {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"},
-    {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"},
-    {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"},
-    {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"},
-    {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"},
-    {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"},
-    {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"},
-    {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"},
-    {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"},
-    {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"},
-    {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"},
-    {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"},
-    {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"},
-    {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"},
-    {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"},
-    {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"},
-    {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"},
-    {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"},
-    {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"},
-    {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"},
-    {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"},
-    {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"},
-    {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"},
-    {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"},
-    {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"},
-    {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"},
-    {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"},
-    {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"},
-    {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"},
-    {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"},
-    {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"},
-    {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"},
-    {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"},
-    {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"},
+    {file = "coverage-7.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0884920835a033b78d1c73b6d3bbcda8161a900f38a488829a83982925f6c2e"},
+    {file = "coverage-7.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:39afcd3d4339329c5f58de48a52f6e4e50f6578dd6099961cf22228feb25f38f"},
+    {file = "coverage-7.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b0ceee8147444347da6a66be737c9d78f3353b0681715b668b72e79203e4a"},
+    {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a9ca3f2fae0088c3c71d743d85404cec8df9be818a005ea065495bedc33da35"},
+    {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd215c0c7d7aab005221608a3c2b46f58c0285a819565887ee0b718c052aa4e"},
+    {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4bf0655ab60d754491004a5efd7f9cccefcc1081a74c9ef2da4735d6ee4a6223"},
+    {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61c4bf1ba021817de12b813338c9be9f0ad5b1e781b9b340a6d29fc13e7c1b5e"},
+    {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db66fc317a046556a96b453a58eced5024af4582a8dbdc0c23ca4dbc0d5b3146"},
+    {file = "coverage-7.5.1-cp310-cp310-win32.whl", hash = "sha256:b016ea6b959d3b9556cb401c55a37547135a587db0115635a443b2ce8f1c7228"},
+    {file = "coverage-7.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:df4e745a81c110e7446b1cc8131bf986157770fa405fe90e15e850aaf7619bc8"},
+    {file = "coverage-7.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:796a79f63eca8814ca3317a1ea443645c9ff0d18b188de470ed7ccd45ae79428"},
+    {file = "coverage-7.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fc84a37bfd98db31beae3c2748811a3fa72bf2007ff7902f68746d9757f3746"},
+    {file = "coverage-7.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6175d1a0559986c6ee3f7fccfc4a90ecd12ba0a383dcc2da30c2b9918d67d8a3"},
+    {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fc81d5878cd6274ce971e0a3a18a8803c3fe25457165314271cf78e3aae3aa2"},
+    {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:556cf1a7cbc8028cb60e1ff0be806be2eded2daf8129b8811c63e2b9a6c43bca"},
+    {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9981706d300c18d8b220995ad22627647be11a4276721c10911e0e9fa44c83e8"},
+    {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d7fed867ee50edf1a0b4a11e8e5d0895150e572af1cd6d315d557758bfa9c057"},
+    {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef48e2707fb320c8f139424a596f5b69955a85b178f15af261bab871873bb987"},
+    {file = "coverage-7.5.1-cp311-cp311-win32.whl", hash = "sha256:9314d5678dcc665330df5b69c1e726a0e49b27df0461c08ca12674bcc19ef136"},
+    {file = "coverage-7.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fa567e99765fe98f4e7d7394ce623e794d7cabb170f2ca2ac5a4174437e90dd"},
+    {file = "coverage-7.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b6cf3764c030e5338e7f61f95bd21147963cf6aa16e09d2f74f1fa52013c1206"},
+    {file = "coverage-7.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ec92012fefebee89a6b9c79bc39051a6cb3891d562b9270ab10ecfdadbc0c34"},
+    {file = "coverage-7.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16db7f26000a07efcf6aea00316f6ac57e7d9a96501e990a36f40c965ec7a95d"},
+    {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beccf7b8a10b09c4ae543582c1319c6df47d78fd732f854ac68d518ee1fb97fa"},
+    {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8748731ad392d736cc9ccac03c9845b13bb07d020a33423fa5b3a36521ac6e4e"},
+    {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7352b9161b33fd0b643ccd1f21f3a3908daaddf414f1c6cb9d3a2fd618bf2572"},
+    {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7a588d39e0925f6a2bff87154752481273cdb1736270642aeb3635cb9b4cad07"},
+    {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:68f962d9b72ce69ea8621f57551b2fa9c70509af757ee3b8105d4f51b92b41a7"},
+    {file = "coverage-7.5.1-cp312-cp312-win32.whl", hash = "sha256:f152cbf5b88aaeb836127d920dd0f5e7edff5a66f10c079157306c4343d86c19"},
+    {file = "coverage-7.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:5a5740d1fb60ddf268a3811bcd353de34eb56dc24e8f52a7f05ee513b2d4f596"},
+    {file = "coverage-7.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e2213def81a50519d7cc56ed643c9e93e0247f5bbe0d1247d15fa520814a7cd7"},
+    {file = "coverage-7.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5037f8fcc2a95b1f0e80585bd9d1ec31068a9bcb157d9750a172836e98bc7a90"},
+    {file = "coverage-7.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3721c2c9e4c4953a41a26c14f4cef64330392a6d2d675c8b1db3b645e31f0e"},
+    {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca498687ca46a62ae590253fba634a1fe9836bc56f626852fb2720f334c9e4e5"},
+    {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cdcbc320b14c3e5877ee79e649677cb7d89ef588852e9583e6b24c2e5072661"},
+    {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:57e0204b5b745594e5bc14b9b50006da722827f0b8c776949f1135677e88d0b8"},
+    {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fe7502616b67b234482c3ce276ff26f39ffe88adca2acf0261df4b8454668b4"},
+    {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9e78295f4144f9dacfed4f92935fbe1780021247c2fabf73a819b17f0ccfff8d"},
+    {file = "coverage-7.5.1-cp38-cp38-win32.whl", hash = "sha256:1434e088b41594baa71188a17533083eabf5609e8e72f16ce8c186001e6b8c41"},
+    {file = "coverage-7.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:0646599e9b139988b63704d704af8e8df7fa4cbc4a1f33df69d97f36cb0a38de"},
+    {file = "coverage-7.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4cc37def103a2725bc672f84bd939a6fe4522310503207aae4d56351644682f1"},
+    {file = "coverage-7.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc0b4d8bfeabd25ea75e94632f5b6e047eef8adaed0c2161ada1e922e7f7cece"},
+    {file = "coverage-7.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d0a0f5e06881ecedfe6f3dd2f56dcb057b6dbeb3327fd32d4b12854df36bf26"},
+    {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9735317685ba6ec7e3754798c8871c2f49aa5e687cc794a0b1d284b2389d1bd5"},
+    {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d21918e9ef11edf36764b93101e2ae8cc82aa5efdc7c5a4e9c6c35a48496d601"},
+    {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c3e757949f268364b96ca894b4c342b41dc6f8f8b66c37878aacef5930db61be"},
+    {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:79afb6197e2f7f60c4824dd4b2d4c2ec5801ceb6ba9ce5d2c3080e5660d51a4f"},
+    {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d1d0d98d95dd18fe29dc66808e1accf59f037d5716f86a501fc0256455219668"},
+    {file = "coverage-7.5.1-cp39-cp39-win32.whl", hash = "sha256:1cc0fe9b0b3a8364093c53b0b4c0c2dd4bb23acbec4c9240b5f284095ccf7981"},
+    {file = "coverage-7.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:dde0070c40ea8bb3641e811c1cfbf18e265d024deff6de52c5950677a8fb1e0f"},
+    {file = "coverage-7.5.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:6537e7c10cc47c595828b8a8be04c72144725c383c4702703ff4e42e44577312"},
+    {file = "coverage-7.5.1.tar.gz", hash = "sha256:54de9ef3a9da981f7af93eafde4ede199e0846cd819eb27c88e2b712aae9708c"},
 ]
 
 [package.extras]
@@ -186,13 +166,13 @@ trio = ["trio (>=0.22.0,<0.26.0)"]
 
 [[package]]
 name = "httpx"
-version = "0.27.2"
+version = "0.27.0"
 description = "The next generation HTTP client."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"},
-    {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"},
+    {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"},
+    {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"},
 ]
 
 [package.dependencies]
@@ -208,7 +188,6 @@ brotli = ["brotli", "brotlicffi"]
 cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
 http2 = ["h2 (>=3,<5)"]
 socks = ["socksio (==1.*)"]
-zstd = ["zstandard (>=0.18.0)"]
 
 [[package]]
 name = "hyperframe"
@@ -223,13 +202,13 @@ files = [
 
 [[package]]
 name = "idna"
-version = "3.8"
+version = "3.7"
 description = "Internationalized Domain Names in Applications (IDNA)"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.5"
 files = [
-    {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"},
-    {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"},
+    {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
+    {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
 ]
 
 [[package]]
@@ -331,43 +310,43 @@ files = [
 
 [[package]]
 name = "mypy"
-version = "1.11.2"
+version = "1.10.0"
 description = "Optional static typing for Python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a"},
-    {file = "mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef"},
-    {file = "mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383"},
-    {file = "mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8"},
-    {file = "mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7"},
-    {file = "mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385"},
-    {file = "mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca"},
-    {file = "mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104"},
-    {file = "mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4"},
-    {file = "mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6"},
-    {file = "mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318"},
-    {file = "mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36"},
-    {file = "mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987"},
-    {file = "mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca"},
-    {file = "mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70"},
-    {file = "mypy-1.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b"},
-    {file = "mypy-1.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86"},
-    {file = "mypy-1.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce"},
-    {file = "mypy-1.11.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1"},
-    {file = "mypy-1.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b"},
-    {file = "mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6"},
-    {file = "mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70"},
-    {file = "mypy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d"},
-    {file = "mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d"},
-    {file = "mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24"},
-    {file = "mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12"},
-    {file = "mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79"},
+    {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"},
+    {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"},
+    {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"},
+    {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"},
+    {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"},
+    {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"},
+    {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"},
+    {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"},
+    {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"},
+    {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"},
+    {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"},
+    {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"},
+    {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"},
+    {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"},
+    {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"},
+    {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"},
+    {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"},
+    {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"},
+    {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"},
+    {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"},
+    {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"},
+    {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"},
+    {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"},
+    {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"},
+    {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"},
+    {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"},
+    {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"},
 ]
 
 [package.dependencies]
 mypy-extensions = ">=1.0.0"
-typing-extensions = ">=4.6.0"
+typing-extensions = ">=4.1.0"
 
 [package.extras]
 dmypy = ["psutil (>=4.0)"]
@@ -388,13 +367,13 @@ files = [
 
 [[package]]
 name = "packaging"
-version = "24.1"
+version = "24.0"
 description = "Core utilities for Python packages"
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.7"
 files = [
-    {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
-    {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
+    {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"},
+    {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"},
 ]
 
 [[package]]
@@ -414,20 +393,20 @@ testing = ["pytest", "pytest-benchmark"]
 
 [[package]]
 name = "pytest"
-version = "8.3.2"
+version = "8.2.1"
 description = "pytest: simple powerful testing with Python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"},
-    {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"},
+    {file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"},
+    {file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"},
 ]
 
 [package.dependencies]
 colorama = {version = "*", markers = "sys_platform == \"win32\""}
 iniconfig = "*"
 packaging = "*"
-pluggy = ">=1.5,<2"
+pluggy = ">=1.5,<2.0"
 
 [package.extras]
 dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
@@ -469,29 +448,28 @@ dev = ["pre-commit", "pytest-asyncio", "tox"]
 
 [[package]]
 name = "ruff"
-version = "0.6.3"
+version = "0.4.5"
 description = "An extremely fast Python linter and code formatter, written in Rust."
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "ruff-0.6.3-py3-none-linux_armv6l.whl", hash = "sha256:97f58fda4e309382ad30ede7f30e2791d70dd29ea17f41970119f55bdb7a45c3"},
-    {file = "ruff-0.6.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3b061e49b5cf3a297b4d1c27ac5587954ccb4ff601160d3d6b2f70b1622194dc"},
-    {file = "ruff-0.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:34e2824a13bb8c668c71c1760a6ac7d795ccbd8d38ff4a0d8471fdb15de910b1"},
-    {file = "ruff-0.6.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bddfbb8d63c460f4b4128b6a506e7052bad4d6f3ff607ebbb41b0aa19c2770d1"},
-    {file = "ruff-0.6.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ced3eeb44df75353e08ab3b6a9e113b5f3f996bea48d4f7c027bc528ba87b672"},
-    {file = "ruff-0.6.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47021dff5445d549be954eb275156dfd7c37222acc1e8014311badcb9b4ec8c1"},
-    {file = "ruff-0.6.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d7bd20dc07cebd68cc8bc7b3f5ada6d637f42d947c85264f94b0d1cd9d87384"},
-    {file = "ruff-0.6.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:500f166d03fc6d0e61c8e40a3ff853fa8a43d938f5d14c183c612df1b0d6c58a"},
-    {file = "ruff-0.6.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42844ff678f9b976366b262fa2d1d1a3fe76f6e145bd92c84e27d172e3c34500"},
-    {file = "ruff-0.6.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70452a10eb2d66549de8e75f89ae82462159855e983ddff91bc0bce6511d0470"},
-    {file = "ruff-0.6.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65a533235ed55f767d1fc62193a21cbf9e3329cf26d427b800fdeacfb77d296f"},
-    {file = "ruff-0.6.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2e2c23cef30dc3cbe9cc5d04f2899e7f5e478c40d2e0a633513ad081f7361b5"},
-    {file = "ruff-0.6.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d8a136aa7d228975a6aee3dd8bea9b28e2b43e9444aa678fb62aeb1956ff2351"},
-    {file = "ruff-0.6.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f92fe93bc72e262b7b3f2bba9879897e2d58a989b4714ba6a5a7273e842ad2f8"},
-    {file = "ruff-0.6.3-py3-none-win32.whl", hash = "sha256:7a62d3b5b0d7f9143d94893f8ba43aa5a5c51a0ffc4a401aa97a81ed76930521"},
-    {file = "ruff-0.6.3-py3-none-win_amd64.whl", hash = "sha256:746af39356fee2b89aada06c7376e1aa274a23493d7016059c3a72e3b296befb"},
-    {file = "ruff-0.6.3-py3-none-win_arm64.whl", hash = "sha256:14a9528a8b70ccc7a847637c29e56fd1f9183a9db743bbc5b8e0c4ad60592a82"},
-    {file = "ruff-0.6.3.tar.gz", hash = "sha256:183b99e9edd1ef63be34a3b51fee0a9f4ab95add123dbf89a71f7b1f0c991983"},
+    {file = "ruff-0.4.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8f58e615dec58b1a6b291769b559e12fdffb53cc4187160a2fc83250eaf54e96"},
+    {file = "ruff-0.4.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:84dd157474e16e3a82745d2afa1016c17d27cb5d52b12e3d45d418bcc6d49264"},
+    {file = "ruff-0.4.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25f483ad9d50b00e7fd577f6d0305aa18494c6af139bce7319c68a17180087f4"},
+    {file = "ruff-0.4.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:63fde3bf6f3ad4e990357af1d30e8ba2730860a954ea9282c95fc0846f5f64af"},
+    {file = "ruff-0.4.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78e3ba4620dee27f76bbcad97067766026c918ba0f2d035c2fc25cbdd04d9c97"},
+    {file = "ruff-0.4.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:441dab55c568e38d02bbda68a926a3d0b54f5510095c9de7f95e47a39e0168aa"},
+    {file = "ruff-0.4.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1169e47e9c4136c997f08f9857ae889d614c5035d87d38fda9b44b4338909cdf"},
+    {file = "ruff-0.4.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:755ac9ac2598a941512fc36a9070a13c88d72ff874a9781493eb237ab02d75df"},
+    {file = "ruff-0.4.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4b02a65985be2b34b170025a8b92449088ce61e33e69956ce4d316c0fe7cce0"},
+    {file = "ruff-0.4.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:75a426506a183d9201e7e5664de3f6b414ad3850d7625764106f7b6d0486f0a1"},
+    {file = "ruff-0.4.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6e1b139b45e2911419044237d90b60e472f57285950e1492c757dfc88259bb06"},
+    {file = "ruff-0.4.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a6f29a8221d2e3d85ff0c7b4371c0e37b39c87732c969b4d90f3dad2e721c5b1"},
+    {file = "ruff-0.4.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d6ef817124d72b54cc923f3444828ba24fa45c3164bc9e8f1813db2f3d3a8a11"},
+    {file = "ruff-0.4.5-py3-none-win32.whl", hash = "sha256:aed8166c18b1a169a5d3ec28a49b43340949e400665555b51ee06f22813ef062"},
+    {file = "ruff-0.4.5-py3-none-win_amd64.whl", hash = "sha256:b0b03c619d2b4350b4a27e34fd2ac64d0dabe1afbf43de57d0f9d8a05ecffa45"},
+    {file = "ruff-0.4.5-py3-none-win_arm64.whl", hash = "sha256:9d15de3425f53161b3f5a5658d4522e4eee5ea002bf2ac7aa380743dd9ad5fba"},
+    {file = "ruff-0.4.5.tar.gz", hash = "sha256:286eabd47e7d4d521d199cab84deca135557e6d1e0f0d01c29e757c3cb151b54"},
 ]
 
 [[package]]
@@ -507,13 +485,13 @@ files = [
 
 [[package]]
 name = "tenacity"
-version = "9.0.0"
+version = "8.3.0"
 description = "Retry code until it succeeds"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539"},
-    {file = "tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b"},
+    {file = "tenacity-8.3.0-py3-none-any.whl", hash = "sha256:3649f6443dbc0d9b01b9d8020a9c4ec7a1ff5f6f3c6c8a036ef371f573fe9185"},
+    {file = "tenacity-8.3.0.tar.gz", hash = "sha256:953d4e6ad24357bceffbc9707bc74349aca9d245f68eb65419cf0c249a1949a2"},
 ]
 
 [package.extras]
@@ -522,13 +500,13 @@ test = ["pytest", "tornado (>=4.5)", "typeguard"]
 
 [[package]]
 name = "typing-extensions"
-version = "4.12.2"
+version = "4.11.0"
 description = "Backported and Experimental Type Hints for Python 3.8+"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
-    {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
+    {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"},
+    {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"},
 ]
 
 [metadata]


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 7aead5f..84ac63c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -282,10 +282,10 @@ def make_coverage():
                 percent_covered=decimal.Decimal("1.0"),
                 missing_lines=0,
                 excluded_lines=0,
-                num_branches=0 if has_branches else None,
-                num_partial_branches=0 if has_branches else None,
-                covered_branches=0 if has_branches else None,
-                missing_branches=0 if has_branches else None,
+                num_branches=0,
+                num_partial_branches=0,
+                covered_branches=0,
+                missing_branches=0,
             ),
             files={},
         )
@@ -313,10 +313,10 @@ def make_coverage():
                         percent_covered=decimal.Decimal("1.0"),
                         missing_lines=0,
                         excluded_lines=0,
-                        num_branches=0 if has_branches else None,
-                        num_partial_branches=0 if has_branches else None,
-                        covered_branches=0 if has_branches else None,
-                        missing_branches=0 if has_branches else None,
+                        num_branches=0,
+                        num_partial_branches=0,
+                        covered_branches=0,
+                        missing_branches=0,
                     ),
                 )
             if set(line.split()) & {
@@ -340,7 +340,6 @@ def make_coverage():
                 coverage_obj.files[current_file].excluded_lines.append(line_number)
                 coverage_obj.files[current_file].info.excluded_lines += 1
                 coverage_obj.info.excluded_lines += 1
-
             if has_branches and "branch" in line:
                 coverage_obj.files[current_file].info.num_branches += 1
                 coverage_obj.info.num_branches += 1
@@ -353,21 +352,22 @@ def make_coverage():
                 elif "branch missing" in line:
                     coverage_obj.files[current_file].info.missing_branches += 1
                     coverage_obj.info.missing_branches += 1
-
             info = coverage_obj.files[current_file].info
             coverage_obj.files[
                 current_file
             ].info.percent_covered = coverage_module.compute_coverage(
                 num_covered=info.covered_lines,
                 num_total=info.num_statements,
+                num_branches_covered=info.covered_branches,
+                num_branches_total=info.num_branches,
             )
-
             info = coverage_obj.info
             coverage_obj.info.percent_covered = coverage_module.compute_coverage(
                 num_covered=info.covered_lines,
                 num_total=info.num_statements,
+                num_branches_covered=info.covered_branches,
+                num_branches_total=info.num_branches,
             )
-
         return coverage_obj
 
     return _
@@ -425,9 +425,19 @@ def coverage_code():
         9
         10 branch missing
         11 missing
-        12
+        12 covered
         13 branch covered
         14 covered
+        15 branch partial
+        16 branch covered
+        17 branch missing
+        18 covered
+        19 covered
+        20 branch partial
+        21 branch missing
+        22 branch covered
+        23 branch covered
+        24 branch covered
         """
 
 
@@ -442,32 +452,48 @@ def coverage_json():
         },
         "files": {
             "codebase/code.py": {
-                "executed_lines": [1, 2, 3, 5, 13, 14],
+                "executed_lines": [
+                    1,
+                    2,
+                    3,
+                    5,
+                    12,
+                    13,
+                    14,
+                    15,
+                    16,
+                    18,
+                    19,
+                    20,
+                    22,
+                    23,
+                    24,
+                ],
                 "summary": {
-                    "covered_lines": 6,
-                    "num_statements": 10,
-                    "percent_covered": 60.0,
-                    "missing_lines": 4,
+                    "covered_lines": 15,
+                    "num_statements": 21,
+                    "percent_covered": 0.625,
+                    "missing_lines": 6,
                     "excluded_lines": 0,
-                    "num_branches": 3,
-                    "num_partial_branches": 1,
-                    "covered_branches": 1,
-                    "missing_branches": 1,
+                    "num_branches": 11,
+                    "num_partial_branches": 3,
+                    "covered_branches": 5,
+                    "missing_branches": 3,
                 },
-                "missing_lines": [6, 8, 10, 11],
+                "missing_lines": [6, 8, 10, 11, 17, 21],
                 "excluded_lines": [],
             }
         },
         "totals": {
-            "covered_lines": 6,
-            "num_statements": 10,
-            "percent_covered": 60.0,
-            "missing_lines": 4,
+            "covered_lines": 15,
+            "num_statements": 21,
+            "percent_covered": 0.625,
+            "missing_lines": 6,
             "excluded_lines": 0,
-            "num_branches": 3,
-            "num_partial_branches": 1,
-            "covered_branches": 1,
-            "missing_branches": 1,
+            "num_branches": 11,
+            "num_partial_branches": 3,
+            "covered_branches": 5,
+            "missing_branches": 3,
         },
     }
 
diff --git a/tests/integration/test_github.py b/tests/integration/test_github.py
index a5b99ec..ca2d5ba 100644
--- a/tests/integration/test_github.py
+++ b/tests/integration/test_github.py
@@ -52,40 +52,8 @@ def test_download_artifact(gh, session, zip_bytes):
         {"name": "foo", "id": 789},
     ]
     session.register("GET", "/repos/foo/bar/actions/runs/123/artifacts")(
-        json={"artifacts": artifacts, "total_count": 2}
-    )
-
-    session.register("GET", "/repos/foo/bar/actions/artifacts/789/zip")(
-        content=zip_bytes(filename="foo.txt", content="bar")
-    )
-
-    result = github.download_artifact(
-        github=gh,
-        repository="foo/bar",
-        artifact_name="foo",
-        run_id=123,
-        filename=pathlib.Path("foo.txt"),
-    )
-
-    assert result == "bar"
-
-
-def test_download_artifact_from_page_2(gh, session, zip_bytes):
-    artifacts_page_1 = [
-        {"name": "test", "id": 000},
-    ]
-    artifacts_page_2 = [
-        {"name": "bar", "id": 456},
-        {"name": "foo", "id": 789},
-    ]
-    session.register("GET", "/repos/foo/bar/actions/runs/123/artifacts")(
-        json={"artifacts": artifacts_page_1, "total_count": 3}
+        json={"artifacts": artifacts}
     )
-    session.register(
-        "GET",
-        "/repos/foo/bar/actions/runs/123/artifacts",
-        params={"page": "2"},
-    )(json={"artifacts": artifacts_page_2, "total_count": 3})
 
     session.register("GET", "/repos/foo/bar/actions/artifacts/789/zip")(
         content=zip_bytes(filename="foo.txt", content="bar")
@@ -107,7 +75,7 @@ def test_download_artifact__no_artifact(gh, session):
         {"name": "bar", "id": 456},
     ]
     session.register("GET", "/repos/foo/bar/actions/runs/123/artifacts")(
-        json={"artifacts": artifacts, "total_count": 1}
+        json={"artifacts": artifacts}
     )
 
     with pytest.raises(github.NoArtifact):
@@ -121,15 +89,12 @@ def test_download_artifact__no_artifact(gh, session):
 
 
 def test_download_artifact__no_file(gh, session, zip_bytes):
-    artifacts = [{"name": "foo", "id": 789}]
+    artifacts = [
+        {"name": "foo", "id": 789},
+    ]
     session.register("GET", "/repos/foo/bar/actions/runs/123/artifacts")(
         json={"artifacts": artifacts}
     )
-    session.register(
-        "GET",
-        "/repos/foo/bar/actions/runs/123/artifacts",
-        params={"page": "2"},
-    )(json={})
 
     session.register("GET", "/repos/foo/bar/actions/artifacts/789/zip")(
         content=zip_bytes(filename="foo.txt", content="bar")
@@ -144,59 +109,6 @@ def test_download_artifact__no_file(gh, session, zip_bytes):
         )
 
 
-def test_fetch_artifacts_empty_response(gh, session):
-    session.register("GET", "/repos/foo/bar/actions/runs/123/artifacts")(
-        json={"artifacts": [], "total_count": 0}
-    )
-
-    repo_path = gh.repos("foo/bar")
-
-    result = github._fetch_artifacts(
-        repo_path=repo_path,
-        run_id=123,
-    )
-
-    assert not list(result)
-
-
-def test_fetch_artifacts_single_page(gh, session):
-    artifacts = [{"name": "bar", "id": 456}]
-
-    session.register("GET", "/repos/foo/bar/actions/runs/123/artifacts")(
-        json={"artifacts": artifacts, "total_count": 1}
-    )
-
-    repo_path = gh.repos("foo/bar")
-
-    result = github._fetch_artifacts(
-        repo_path=repo_path,
-        run_id=123,
-    )
-
-    assert list(result) == artifacts
-
-
-def test_fetch_artifacts_multiple_pages(gh, session):
-    artifacts_page_1 = [{"name": "bar", "id": 456}]
-    artifacts_page_2 = [{"name": "bar", "id": 789}]
-
-    session.register("GET", "/repos/foo/bar/actions/runs/123/artifacts")(
-        json={"artifacts": artifacts_page_1, "total_count": 2}
-    )
-    session.register(
-        "GET", "/repos/foo/bar/actions/runs/123/artifacts", params={"page": "2"}
-    )(json={"artifacts": artifacts_page_2, "total_count": 2})
-
-    repo_path = gh.repos("foo/bar")
-
-    result = github._fetch_artifacts(
-        repo_path=repo_path,
-        run_id=123,
-    )
-
-    assert list(result) == artifacts_page_1 + artifacts_page_2
-
-
 def test_get_branch_from_workflow_run(gh, session):
     json = {
         "head_branch": "other",
diff --git a/tests/integration/test_main.py b/tests/integration/test_main.py
index f925e43..10e563f 100644
--- a/tests/integration/test_main.py
+++ b/tests/integration/test_main.py
@@ -811,7 +811,7 @@ def test_action__workflow_run__no_artifact(
     session.register(
         "GET",
         "/repos/py-cov-action/foobar/actions/runs/123/artifacts",
-    )(json={"artifacts": [{"name": "wrong_name"}], "total_count": 1})
+    )(json={"artifacts": [{"name": "wrong_name"}]})
 
     result = main.action(
         config=workflow_run_config(),
@@ -852,12 +852,7 @@ def test_action__workflow_run__post_comment(
     session.register(
         "GET",
         "/repos/py-cov-action/foobar/actions/runs/123/artifacts",
-    )(
-        json={
-            "artifacts": [{"name": "python-coverage-comment-action", "id": 789}],
-            "total_count": 1,
-        }
-    )
+    )(json={"artifacts": [{"name": "python-coverage-comment-action", "id": 789}]})
 
     session.register(
         "GET",
diff --git a/tests/unit/test_coverage.py b/tests/unit/test_coverage.py
index 6ea249a..621219d 100644
--- a/tests/unit/test_coverage.py
+++ b/tests/unit/test_coverage.py
@@ -39,6 +39,24 @@ def test_compute_coverage(num_covered, num_total, expected_coverage):
     )
 
 
+@pytest.mark.parametrize(
+    "num_covered, num_total, branch_covered, branch_total, expected_coverage",
+    [
+        (0, 10, 0, 15, "0"),
+        (0, 0, 0, 0, "1"),
+        (5, 0, 5, 0, "1"),
+        (5, 10, 5, 10, "0.5"),
+        (1, 50, 1, 50, "0.02"),
+    ],
+)
+def test_compute_coverage_with_branches(
+    num_covered, num_total, branch_covered, branch_total, expected_coverage
+):
+    assert coverage.compute_coverage(
+        num_covered, num_total, branch_covered, branch_total
+    ) == decimal.Decimal(expected_coverage)
+
+
 def test_get_coverage_info(mocker, coverage_json, coverage_obj):
     run = mocker.patch(
         "coverage_comment.subprocess.run", return_value=json.dumps(coverage_json)
@@ -132,6 +150,42 @@ def test_generate_coverage_markdown(mocker):
     assert result == "foo"
 
 
+def test__make_coverage_info():
+    result = coverage._make_coverage_info(
+        {
+            "covered_lines": 14,
+            "num_statements": 20,
+            "missing_lines": 6,
+            "excluded_lines": 0,
+        }
+    )
+    assert isinstance(result, coverage.CoverageInfo)
+    assert result.percent_covered == decimal.Decimal(14) / decimal.Decimal(20)
+    assert result.num_branches == 0
+    assert result.num_partial_branches == 0
+    assert result.covered_branches == 0
+    assert result.missing_branches == 0
+
+
+def test__make_coverage_info__with_branches():
+    result = coverage._make_coverage_info(
+        {
+            "covered_lines": 4,
+            "num_statements": 10,
+            "missing_lines": 1,
+            "excluded_lines": 0,
+            "covered_branches": 4,
+            "num_branches": 6,
+            "num_partial_branches": 2,
+        }
+    )
+    assert isinstance(result, coverage.CoverageInfo)
+    assert result.percent_covered == decimal.Decimal(4 + 4) / decimal.Decimal(10 + 6)
+    assert result.covered_branches == 4
+    assert result.missing_branches == 0
+    assert result.excluded_lines == 0
+
+
 @pytest.mark.parametrize(
     "added_lines, update_obj, expected",
     [
diff --git a/tests/unit/test_settings.py b/tests/unit/test_settings.py
index b283d99..a335827 100644
--- a/tests/unit/test_settings.py
+++ b/tests/unit/test_settings.py
@@ -127,8 +127,6 @@ def config():
     "github_ref, github_pr_number",
     [
         ("foo", None),
-        ("refs/heads/branch-with-pull", None),
-        ("refs/tags/tag-with-pull", None),
         ("refs/pull/2/merge", 2),
     ],
 )
@@ -140,8 +138,6 @@ def test_config__GITHUB_PR_NUMBER(config, github_ref, github_pr_number):
     "github_ref, github_branch_name",
     [
         ("refs/pull/2/merge", None),
-        ("refs/pull/2/head", None),
-        ("refs/tags/tag-with-heads", None),
         ("refs/heads/a/b", "a/b"),
     ],
 )
diff --git a/tests/unit/test_template.py b/tests/unit/test_template.py
index 6282042..41d2b54 100644
--- a/tests/unit/test_template.py
+++ b/tests/unit/test_template.py
@@ -44,7 +44,7 @@ def test_get_comment_markdown(coverage_obj, diff_coverage_obj):
         .split(maxsplit=4)
     )
 
-    expected = ["92%", "60%", "50%", "bar", "<!-- foo -->"]
+    expected = ["92%", "62.5%", "60%", "bar", "<!-- foo -->"]
 
     assert result == expected
 
@@ -79,17 +79,17 @@ def test_template(coverage_obj, diff_coverage_obj):
     expected = """## Coverage report (foo)
 
 
-<img title="Coverage for the whole project went from 92% to 60%" src="https://img.shields.io/badge/Coverage%20evolution-92%25%20%3E%2060%25-red.svg"> <img title="50% of the statement lines added by this PR are covered" src="https://img.shields.io/badge/PR%20Coverage-50%25-orange.svg"><details><summary>Click to see where and how coverage changed</summary><table><thead>
+<img title="Coverage for the whole project went from 92% to 62.5%" src="https://img.shields.io/badge/Coverage%20evolution-92%25%20%3E%2062%25-red.svg"> <img title="60% of the statement lines added by this PR are covered" src="https://img.shields.io/badge/PR%20Coverage-60%25-orange.svg"><details><summary>Click to see where and how coverage changed</summary><table><thead>
   <tr><th>File</th><th>Statements</th><th>Missing</th><th>Coverage</th><th>Coverage<br>(new stmts)</th><th>Lines missing</th></tr>
 </thead>
 <tbody><tr>
 <td colspan="6">&nbsp;&nbsp;<b>codebase</b></td><tr>
 <td>&nbsp;&nbsp;<a href="https://github.com/org/repo/pull/5/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3">code.py</a></td>
-<td align="center"><a href="https://github.com/org/repo/pull/5/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3"><img title="This PR adds 10 statements to codebase/code.py. The file did not seem to exist on the base branch." src="https://img.shields.io/badge/10-%28%2B10%29-007ec6.svg"></a></td><td align="center"><a href="https://github.com/org/repo/pull/5/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3"><img title="This PR adds 4 statements missing coverage to codebase/code.py. The file did not seem to exist on the base branch." src="https://img.shields.io/badge/4-%28%2B4%29-red.svg"></a></td><td align="center"><a href="https://github.com/org/repo/pull/5/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3"><img title="The coverage rate of codebase/code.py is 60% (6/10). The file did not seem to exist on the base branch." src="https://img.shields.io/badge/60%25-%286/10%29-orange.svg"></a></td><td align="center"><a href="https://github.com/org/repo/pull/5/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3"><img title="In this PR, 4 new statements are added to codebase/code.py, 2 of which are covered (50%)." src="https://img.shields.io/badge/50%25-%282/4%29-orange.svg"></a></td><td><a href="https://github.com/org/repo/pull/5/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3R6-R8">6-8</a></td></tbody>
+<td align="center"><a href="https://github.com/org/repo/pull/5/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3"><img title="This PR adds 21 statements to codebase/code.py. The file did not seem to exist on the base branch." src="https://img.shields.io/badge/21-%28%2B21%29-007ec6.svg"></a></td><td align="center"><a href="https://github.com/org/repo/pull/5/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3"><img title="This PR adds 6 statements missing coverage to codebase/code.py. The file did not seem to exist on the base branch." src="https://img.shields.io/badge/6-%28%2B6%29-red.svg"></a></td><td align="center"><a href="https://github.com/org/repo/pull/5/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3"><img title="The coverage rate of codebase/code.py is 62.5% (15/21). The file did not seem to exist on the base branch." src="https://img.shields.io/badge/62%25-%2815/21%29-orange.svg"></a></td><td align="center"><a href="https://github.com/org/repo/pull/5/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3"><img title="In this PR, 5 new statements are added to codebase/code.py, 3 of which are covered (60%)." src="https://img.shields.io/badge/60%25-%283/5%29-orange.svg"></a></td><td><a href="https://github.com/org/repo/pull/5/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3R6-R8">6-8</a></td></tbody>
 <tfoot>
 <tr>
 <td><b>Project Total</b></td>
-<td align="center"><a href="https://github.com/org/repo/pull/5/files#diff-4b0bf2efa3367c0072ac2bf1e234e703dc46b47aaa4fe9d3b01737b1a15752b1"><img title="This PR adds 10 statements to the whole project. The file did not seem to exist on the base branch." src="https://img.shields.io/badge/10-%28%2B10%29-007ec6.svg"></a></td><td align="center"><a href="https://github.com/org/repo/pull/5/files#diff-4b0bf2efa3367c0072ac2bf1e234e703dc46b47aaa4fe9d3b01737b1a15752b1"><img title="This PR adds 4 statements missing coverage to the whole project. The file did not seem to exist on the base branch." src="https://img.shields.io/badge/4-%28%2B4%29-red.svg"></a></td><td align="center"><a href="https://github.com/org/repo/pull/5/files#diff-4b0bf2efa3367c0072ac2bf1e234e703dc46b47aaa4fe9d3b01737b1a15752b1"><img title="The coverage rate of the whole project is 60% (6/10). The file did not seem to exist on the base branch." src="https://img.shields.io/badge/60%25-%286/10%29-orange.svg"></a></td><td align="center"><a href="https://github.com/org/repo/pull/5/files#diff-4b0bf2efa3367c0072ac2bf1e234e703dc46b47aaa4fe9d3b01737b1a15752b1"><img title="In this PR, 4 new statements are added to the whole project, 2 of which are covered (50%)." src="https://img.shields.io/badge/50%25-%282/4%29-orange.svg"></a></td><td>&nbsp;</td>
+<td align="center"><a href="https://github.com/org/repo/pull/5/files#diff-4b0bf2efa3367c0072ac2bf1e234e703dc46b47aaa4fe9d3b01737b1a15752b1"><img title="This PR adds 21 statements to the whole project. The file did not seem to exist on the base branch." src="https://img.shields.io/badge/21-%28%2B21%29-007ec6.svg"></a></td><td align="center"><a href="https://github.com/org/repo/pull/5/files#diff-4b0bf2efa3367c0072ac2bf1e234e703dc46b47aaa4fe9d3b01737b1a15752b1"><img title="This PR adds 6 statements missing coverage to the whole project. The file did not seem to exist on the base branch." src="https://img.shields.io/badge/6-%28%2B6%29-red.svg"></a></td><td align="center"><a href="https://github.com/org/repo/pull/5/files#diff-4b0bf2efa3367c0072ac2bf1e234e703dc46b47aaa4fe9d3b01737b1a15752b1"><img title="The coverage rate of the whole project is 62.5% (15/21). The file did not seem to exist on the base branch." src="https://img.shields.io/badge/62%25-%2815/21%29-orange.svg"></a></td><td align="center"><a href="https://github.com/org/repo/pull/5/files#diff-4b0bf2efa3367c0072ac2bf1e234e703dc46b47aaa4fe9d3b01737b1a15752b1"><img title="In this PR, 5 new statements are added to the whole project, 3 of which are covered (60%)." src="https://img.shields.io/badge/60%25-%283/5%29-orange.svg"></a></td><td>&nbsp;</td>
 </tr>
 </tfoot>
 </table>
@@ -264,17 +264,17 @@ def test_template__no_previous(coverage_obj_no_branch, diff_coverage_obj):
     expected = """## Coverage report
 
 
-<img title="Coverage for the whole project is 50%. Previous coverage rate is not available, cannot report on evolution." src="https://img.shields.io/badge/Coverage-50%25-red.svg"> <img title="50% of the statement lines added by this PR are covered" src="https://img.shields.io/badge/PR%20Coverage-50%25-red.svg"><details><summary>Click to see where and how coverage changed</summary><table><thead>
+<img title="Coverage for the whole project is 50%. Previous coverage rate is not available, cannot report on evolution." src="https://img.shields.io/badge/Coverage-50%25-red.svg"> <img title="60% of the statement lines added by this PR are covered" src="https://img.shields.io/badge/PR%20Coverage-60%25-red.svg"><details><summary>Click to see where and how coverage changed</summary><table><thead>
   <tr><th>File</th><th>Statements</th><th>Missing</th><th>Coverage</th><th>Coverage<br>(new stmts)</th><th>Lines missing</th></tr>
 </thead>
 <tbody><tr>
 <td colspan="6">&nbsp;&nbsp;<b>codebase</b></td><tr>
 <td>&nbsp;&nbsp;<a href="https://github.com/org/repo/pull/3/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3">code.py</a></td>
-<td align="center"><a href="https://github.com/org/repo/pull/3/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3"><img title="This PR adds 8 statements to codebase/code.py. The file did not seem to exist on the base branch." src="https://img.shields.io/badge/8-%28%2B8%29-007ec6.svg"></a></td><td align="center"><a href="https://github.com/org/repo/pull/3/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3"><img title="This PR adds 4 statements missing coverage to codebase/code.py. The file did not seem to exist on the base branch." src="https://img.shields.io/badge/4-%28%2B4%29-red.svg"></a></td><td align="center"><a href="https://github.com/org/repo/pull/3/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3"><img title="The coverage rate of codebase/code.py is 50% (4/8). The file did not seem to exist on the base branch." src="https://img.shields.io/badge/50%25-%284/8%29-red.svg"></a></td><td align="center"><a href="https://github.com/org/repo/pull/3/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3"><img title="In this PR, 4 new statements are added to codebase/code.py, 2 of which are covered (50%)." src="https://img.shields.io/badge/50%25-%282/4%29-red.svg"></a></td><td><a href="https://github.com/org/repo/pull/3/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3R6-R8">6-8</a></td></tbody>
+<td align="center"><a href="https://github.com/org/repo/pull/3/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3"><img title="This PR adds 8 statements to codebase/code.py. The file did not seem to exist on the base branch." src="https://img.shields.io/badge/8-%28%2B8%29-007ec6.svg"></a></td><td align="center"><a href="https://github.com/org/repo/pull/3/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3"><img title="This PR adds 4 statements missing coverage to codebase/code.py. The file did not seem to exist on the base branch." src="https://img.shields.io/badge/4-%28%2B4%29-red.svg"></a></td><td align="center"><a href="https://github.com/org/repo/pull/3/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3"><img title="The coverage rate of codebase/code.py is 50% (4/8). The file did not seem to exist on the base branch." src="https://img.shields.io/badge/50%25-%284/8%29-red.svg"></a></td><td align="center"><a href="https://github.com/org/repo/pull/3/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3"><img title="In this PR, 5 new statements are added to codebase/code.py, 3 of which are covered (60%)." src="https://img.shields.io/badge/60%25-%283/5%29-red.svg"></a></td><td><a href="https://github.com/org/repo/pull/3/files#diff-c05d5557f0c1ff3761df2f49e3b541cfc161f4f0d63e2a66d568f090065bc3d3R6-R8">6-8</a></td></tbody>
 <tfoot>
 <tr>
 <td><b>Project Total</b></td>
-<td align="center"><a href="https://github.com/org/repo/pull/3/files#diff-4b0bf2efa3367c0072ac2bf1e234e703dc46b47aaa4fe9d3b01737b1a15752b1"><img title="This PR adds 8 statements to the whole project. The file did not seem to exist on the base branch." src="https://img.shields.io/badge/8-%28%2B8%29-007ec6.svg"></a></td><td align="center"><a href="https://github.com/org/repo/pull/3/files#diff-4b0bf2efa3367c0072ac2bf1e234e703dc46b47aaa4fe9d3b01737b1a15752b1"><img title="This PR adds 4 statements missing coverage to the whole project. The file did not seem to exist on the base branch." src="https://img.shields.io/badge/4-%28%2B4%29-red.svg"></a></td><td align="center"><a href="https://github.com/org/repo/pull/3/files#diff-4b0bf2efa3367c0072ac2bf1e234e703dc46b47aaa4fe9d3b01737b1a15752b1"><img title="The coverage rate of the whole project is 50% (4/8). The file did not seem to exist on the base branch." src="https://img.shields.io/badge/50%25-%284/8%29-red.svg"></a></td><td align="center"><a href="https://github.com/org/repo/pull/3/files#diff-4b0bf2efa3367c0072ac2bf1e234e703dc46b47aaa4fe9d3b01737b1a15752b1"><img title="In this PR, 4 new statements are added to the whole project, 2 of which are covered (50%)." src="https://img.shields.io/badge/50%25-%282/4%29-red.svg"></a></td><td>&nbsp;</td>
+<td align="center"><a href="https://github.com/org/repo/pull/3/files#diff-4b0bf2efa3367c0072ac2bf1e234e703dc46b47aaa4fe9d3b01737b1a15752b1"><img title="This PR adds 8 statements to the whole project. The file did not seem to exist on the base branch." src="https://img.shields.io/badge/8-%28%2B8%29-007ec6.svg"></a></td><td align="center"><a href="https://github.com/org/repo/pull/3/files#diff-4b0bf2efa3367c0072ac2bf1e234e703dc46b47aaa4fe9d3b01737b1a15752b1"><img title="This PR adds 4 statements missing coverage to the whole project. The file did not seem to exist on the base branch." src="https://img.shields.io/badge/4-%28%2B4%29-red.svg"></a></td><td align="center"><a href="https://github.com/org/repo/pull/3/files#diff-4b0bf2efa3367c0072ac2bf1e234e703dc46b47aaa4fe9d3b01737b1a15752b1"><img title="The coverage rate of the whole project is 50% (4/8). The file did not seem to exist on the base branch." src="https://img.shields.io/badge/50%25-%284/8%29-red.svg"></a></td><td align="center"><a href="https://github.com/org/repo/pull/3/files#diff-4b0bf2efa3367c0072ac2bf1e234e703dc46b47aaa4fe9d3b01737b1a15752b1"><img title="In this PR, 5 new statements are added to the whole project, 3 of which are covered (60%)." src="https://img.shields.io/badge/60%25-%283/5%29-red.svg"></a></td><td>&nbsp;</td>
 </tr>
 </tfoot>
 </table>
@@ -836,20 +836,3 @@ def test_get_log_message():
         subproject_id="foo",
     )
     assert result.startswith("Coverage info for foo:")
-
-
-@pytest.mark.parametrize(
-    "value, expected",
-    [
-        (0, "0"),
-        (1, "1"),
-        (999, "999"),
-        (1_042, "1.0k"),
-        (9_900, "9.9k"),
-        (12_345, "12k"),
-        (999_999, "1000k"),
-        (1_234_567, "1M"),
-    ],
-)
-def test_compact(value, expected):
-    assert template.compact(value) == expected