diff --git a/.github/actions/commit-visualizations/action.yml b/.github/actions/commit-visualizations/action.yml
deleted file mode 100644
index fc9c215..0000000
--- a/.github/actions/commit-visualizations/action.yml
+++ /dev/null
@@ -1,47 +0,0 @@
-name: 'Commit Visualizations'
-description: 'Commits generated visualization PNGs and optionally a DB file to the repo'
-
-inputs:
- path:
- description: 'Path to the directory containing PNG files'
- required: true
- db_path:
- description: 'Path to the database file to commit (optional)'
- required: false
- default: ''
-
-runs:
- using: composite
- steps:
- - name: Configure Git
- shell: bash
- run: |
- git config --global user.name 'GitHub Actions'
- git config --global user.email 'github-actions[bot]@users.noreply.github.com'
-
- - name: Sync with remote main
- shell: bash
- run: |
- git fetch origin main
- git reset --hard origin/main
-
- - name: Commit visualization PNG files and optional DB
- shell: bash
- run: |
- echo "Checking for PNG files in ${{ inputs.path }}"
- find ${{ inputs.path }} -name "*.png" -type f
-
- # Stage PNG files (if any)
- git add -v ${{ inputs.path }}/*.png || true
-
- # If db_path is provided and file exists, stage it
- if [ -n "${{ inputs.db_path }}" ] && [ -f "${{ inputs.db_path }}" ]; then
- echo "Staging DB file at ${{ inputs.db_path }}"
- git add -v "${{ inputs.db_path }}" || true
- else
- echo "No DB file to commit or file not found."
- fi
-
- # Commit changes (if any)
- git commit -m "Update visualization PNGs${{ inputs.db_path && ' and DB file' || '' }}" || echo "No changes to commit"
- git push
diff --git a/.github/actions/setup-python-poetry/action.yml b/.github/actions/setup-python-poetry/action.yml
deleted file mode 100644
index 403c49b..0000000
--- a/.github/actions/setup-python-poetry/action.yml
+++ /dev/null
@@ -1,58 +0,0 @@
-name: 'Setup Python and Poetry'
-description: 'Sets up Python, Poetry, and handles dependency caching'
-
-inputs:
- python-version:
- description: 'Python version to use'
- required: false
- default: '3.13'
- working-directory:
- description: 'Directory containing poetry.lock and pyproject.toml'
- required: true
- cache-key-suffix:
- description: 'Additional string to append to cache keys for uniqueness'
- required: false
- default: ''
-
-runs:
- using: composite
- steps:
- - name: Set up Python
- id: setup-python
- uses: actions/setup-python@v5
- with:
- python-version: ${{ inputs.python-version }}
-
- - name: Load cached Poetry installation
- id: cached-poetry
- uses: actions/cache@v4
- with:
- path: ~/.local
- key: poetry-0${{ inputs.cache-key-suffix }}
-
- - name: Install Poetry
- if: steps.cached-poetry.outputs.cache-hit != 'true'
- uses: snok/install-poetry@v1
- with:
- version: 1.7.1
- virtualenvs-create: true
- virtualenvs-in-project: true
-
- - name: Configure poetry
- if: steps.cached-poetry.outputs.cache-hit == 'true'
- shell: bash
- run: poetry config virtualenvs.in-project true
-
- - name: Cache Poetry virtualenv
- id: cached-poetry-dependencies
- uses: actions/cache@v4
- with:
- path: ${{ inputs.working-directory }}/.venv
- # Cache based on CI runner OS version, Python version, lock file, and optional suffix
- key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles(format('{0}/poetry.lock', inputs.working-directory)) }}${{ inputs.cache-key-suffix }}
-
- - name: Install dependencies
- if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
- shell: bash
- working-directory: ${{ inputs.working-directory }}
- run: poetry install
diff --git a/.github/workflows/cat-test-examples.yml b/.github/workflows/cat-test-examples.yml
index b71256c..d0e86c3 100644
--- a/.github/workflows/cat-test-examples.yml
+++ b/.github/workflows/cat-test-examples.yml
@@ -21,6 +21,9 @@ jobs:
- name: Install uv
uses: astral-sh/setup-uv@v5
+ with:
+ enable-cache: true
+ prune-cache: false
- name: "Set up Python"
uses: actions/setup-python@v5
@@ -39,6 +42,8 @@ jobs:
id: set-number-of-runs
run: |
ROUNDS=${{ inputs.rounds || 10 }}
+ [[ $GITHUB_REF_NAME == ci-experiment* ]] && ROUNDS=1
+
echo "::notice::Starting $ROUNDS runs"
echo "number_of_runs=$ROUNDS" >> "$GITHUB_OUTPUT"
echo "CAT_AI_SAMPLE_SIZE=$ROUNDS" >> $GITHUB_ENV
@@ -58,19 +63,24 @@ jobs:
# -H "Authorization: AWS minioadmin:minioadmin" \
# http://localhost:9000/yourbucket/yourfile.zip
- - name: Show number of test failures
+ - name: Show CAT AI Statistical Report
if: always()
run: |
- FAILURES=$(find examples/team_recommender/tests/test_runs -type f -name "fail-*" | wc -l)
- uv run python src/cat_ai/reporter.py $FAILURES $CAT_AI_SAMPLE_SIZE >> $GITHUB_STEP_SUMMARY
+ FOLDER=examples/team_recommender/tests/test_runs
+ FAILURE_COUNT=$(find "$FOLDER" -type f -name "fail-*" | wc -l)
+ PYTHONPATH=src uv run python -m cat_ai.reporter \
+ "$FAILURE_COUNT" \
+ "$CAT_AI_SAMPLE_SIZE" \
+ >> "$GITHUB_STEP_SUMMARY"
- - name: Upload artifacts to Google Drive
- if: always()
+ - name: Upload main artifacts to Google Drive
+ if: always() && github.ref == 'refs/heads/main'
run: |
- zip -r test-output-${{ github.run_number }}.zip examples/team_recommender/tests/test_runs
- uv run python src/cat_ai/publish_to_gdrive.py test-output-${{ github.run_number }}.zip
+ zip -r "$FILENAME" examples/team_recommender/tests/test_runs
+ uv run python src/cat_ai/publish_to_gdrive.py "$FILENAME"
env:
- GOOGLE_DRIVE_TEST_OUTPUT_FOLDER_ID: ${{ vars.GOOGLE_DRIVE_TEST_OUTPUT_FOLDER_ID }}
+ PARENT_FOLDER_IDS: ${{ vars.GOOGLE_DRIVE_TEST_OUTPUT_FOLDER_ID }}
+ FILENAME: test-output-${{ github.run_number }}.zip
- name: Upload artifacts
uses: actions/upload-artifact@v4
diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml
index e4ddce4..e8a1b69 100644
--- a/.github/workflows/python-tests.yml
+++ b/.github/workflows/python-tests.yml
@@ -17,6 +17,9 @@ jobs:
- name: Install uv
uses: astral-sh/setup-uv@v5
+ with:
+ enable-cache: true
+ prune-cache: false
- name: "Set up Python"
uses: actions/setup-python@v5
diff --git a/.run/Template Python tests.run.xml b/.run/Template Python tests.run.xml
new file mode 100644
index 0000000..4fc3b4d
--- /dev/null
+++ b/.run/Template Python tests.run.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/team_recommender/tests/example_1_text_response/test_good_fit_for_project.py b/examples/team_recommender/tests/example_1_text_response/test_good_fit_for_project.py
index e581907..c54e03c 100644
--- a/examples/team_recommender/tests/example_1_text_response/test_good_fit_for_project.py
+++ b/examples/team_recommender/tests/example_1_text_response/test_good_fit_for_project.py
@@ -32,6 +32,12 @@ def test_response_shows_developer_names():
)
response = completion.choices[0].message.content
print(response)
+ # For the iOS Native project starting on June 3rd, the best developers based on the given list would be:
+ #
+ # 1. Sam Thomas - Specializes in Swift and Objective-C, and is available for the project.
+ # 2. Drew Anderson - Specializes in Swift but will be on vacation from June 1st to June 10th, so they are not available when the project starts.
+ #
+ # Therefore, Sam Thomas is the most suitable developer for this project.
assert "Sam Thomas" in response
assert "Drew Anderson" in response, "Surprisingly Drew Anderson is on vacation but still in the response"
@@ -63,6 +69,29 @@ def test_llm_will_hallucinate_given_no_data():
)
response = completion.choices[0].message.content
print(response)
+ # Here is the list of developers with their skills and availability:
+ #
+ # 1. Sarah Johnson
+ # - Skills: iOS Native, Mobile Development
+ # - Availability: Available starting May 1st
+ #
+ # 2. Alex Kim
+ # - Skills: iOS Native, iPhone Development, Video Processing
+ # - Availability: Available starting June 10th
+ #
+ # 3. Jamie Smith
+ # - Skills: iOS Native, Mobile UI Design
+ # - Availability: Available starting May 20th
+ #
+ # Based on the project requirements and availability, the best developer for this mobile iOS project for the telecom company would be:
+ #
+ # 1. Sarah Johnson
+ # - Skills: iOS Native, Mobile Development
+ # - Availability: Available starting May 1st
+ #
+ # 2. Jamie Smith
+ # - Skills: iOS Native, Mobile UI Design
+ # - Availability: Available starting May 20th
assert "Sam Thomas" not in response, "LLM obviously could not get our expected developer and will hallucinate"
assert "Drew Anderson" not in response, "Response will contain made up names"
assert len(response.split('\n')) > 5, "response contains list of made up developers in multiple lines"
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 06c8594..e6a14c2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -3,37 +3,40 @@ name = "cat-ai"
version = "0.0.5-alpha"
description = "Python client for running CAT tests in a Python codebase"
authors = [
- { name = "Mike Gehard", email = "mikegehard@artium.ai" },
- { name = "Randy Lutcavich", email = "randylutcavich@artium.ai" },
- { name = "Erik Luetkehans", email = "erikluetkehans@artium.ai" },
- { name = "Paul Zabelin", email = "paulzabelin@artium.ai" },
- { name = "Tim Kersey", email = "timkersey@artium.ai" },
- { name = "Michael Harris", email = "michaelharris@artium.ai" },
+ { name = "Mike Gehard", email = "mikegehard@artium.ai" },
+ { name = "Randy Lutcavich", email = "randylutcavich@artium.ai" },
+ { name = "Erik Luetkehans", email = "erikluetkehans@artium.ai" },
+ { name = "Paul Zabelin", email = "paulzabelin@artium.ai" },
+ { name = "Tim Kersey", email = "timkersey@artium.ai" },
+ { name = "Michael Harris", email = "michaelharris@artium.ai" },
]
requires-python = "~=3.13"
readme = "README.md"
dependencies = [
- # this small library should be kept independent
- # consider adding dependencies to on of the dependency groups
+ # this small library should be kept independent
+ # consider adding dependencies to on of the dependency groups
]
packages = [{ include = "cat_ai", from = "src" }]
license = "MIT"
[dependency-groups]
test = [
- "pytest>=8.3.4,<9",
- "pytest-asyncio>=0.21.0,<0.22",
- "mypy>=1.8.0,<2",
- "black>=24.2.0,<25",
+ "matplotlib>=3.10.1",
+ "pytest>=8.3.4,<9",
+ "pytest-asyncio>=0.21.0,<0.22",
+ "mypy>=1.8.0,<2",
+ "black>=24.2.0,<25",
+ "pytest-snapshot>=0.9.0",
]
examples = ["openai>=1.63.2,<2", "python-dotenv>=1.0.1,<2"]
dev = [
- "sphinx>=8.1.3,<9",
- "sphinx-rtd-theme>=3.0.2,<4",
- "sphinx-markdown-builder>=0.6.8,<0.7",
- "notebook>=7.3.2",
- "pydrive2>=1.21.3,<2",
- "pydantic>=2.10.6,<3",
+ "sphinx>=8.1.3,<9",
+ "sphinx-rtd-theme>=3.0.2,<4",
+ "sphinx-markdown-builder>=0.6.8,<0.7",
+ "notebook>=7.3.2",
+ "pydrive2>=1.21.3,<2",
+ "pydantic>=2.10.6,<3",
+ "ruff>=0.9.10",
]
[tool.uv]
diff --git a/src/cat_ai/__init__.py b/src/cat_ai/__init__.py
index 9fc5e05..8198748 100644
--- a/src/cat_ai/__init__.py
+++ b/src/cat_ai/__init__.py
@@ -1,5 +1,6 @@
from .reporter import Reporter
from .runner import Runner
from .validator import Validator
+from .statistical_analysis import StatisticalAnalysis, analyse_sample_from_test
-__all__ = ["Reporter", "Runner", "Validator"]
+__all__ = ["Reporter", "Runner", "Validator", "StatisticalAnalysis", "analyse_sample_from_test"]
diff --git a/src/cat_ai/publish_to_gdrive.py b/src/cat_ai/publish_to_gdrive.py
index 2c6a3f2..593b3fa 100644
--- a/src/cat_ai/publish_to_gdrive.py
+++ b/src/cat_ai/publish_to_gdrive.py
@@ -24,9 +24,12 @@ def login_with_service_account(credentials_path: str) -> GoogleAuth:
return gauth
+PARENT_FOLDER_IDS = "PARENT_FOLDER_IDS"
+
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: python publish_to_gdrive.py ")
+ print(f"{PARENT_FOLDER_IDS} - comma-separated list of google folder IDs")
sys.exit(1)
file_path = sys.argv[1]
@@ -43,8 +46,13 @@ def login_with_service_account(credentials_path: str) -> GoogleAuth:
drive = GoogleDrive(google_auth)
file_name = os.path.basename(file_path)
- PARENT_FOLDER_ID = os.environ.get("GOOGLE_DRIVE_TEST_OUTPUT_FOLDER_ID")
- gfile = drive.CreateFile({"title": file_name, "parents": [{"id": PARENT_FOLDER_ID}]})
+ parent_ids = os.environ.get(PARENT_FOLDER_IDS)
+ if not parent_ids:
+ print(f"Error: {PARENT_FOLDER_IDS} environment variable is not set.")
+ sys.exit(2)
+ parents = [{"id": pid.strip()} for pid in (parent_ids.split(","))]
+ gfile = drive.CreateFile({"title": file_name, "parents": parents})
+
gfile.SetContentFile(file_path)
gfile.Upload()
diff --git a/src/cat_ai/reporter.py b/src/cat_ai/reporter.py
index b47e96c..1eb6cdc 100644
--- a/src/cat_ai/reporter.py
+++ b/src/cat_ai/reporter.py
@@ -1,10 +1,11 @@
import json
-import math
import os
import sys
from datetime import datetime
from typing import Optional, Any, Dict
+from .statistical_analysis import StatisticalAnalysis, analyse_sample_from_test
+
class Reporter:
run_number: int = 0
@@ -60,48 +61,27 @@ def report(self, response: str, results: Dict[str, bool]) -> bool:
return final_result
@staticmethod
- def error_margin_summary(failure_count, sample_size):
+ def format_summary(analysis: StatisticalAnalysis) -> str:
"""
- Calculate the error margin and confidence interval for a given sample.
+ Format the statistical analysis as a markdown string.
Args:
- failure_count (int): Number of failures in the sample
- sample_size (int): Total size of the sample
+ analysis: StatisticalAnalysis object containing analysis data
Returns:
str: Formatted string with the error margin calculations and confidence interval
"""
- # Calculate sample proportion
- p_hat = failure_count / sample_size
-
- # Determine z-score for 90% confidence level (approximately 1.645)
- z = 1.645
-
- # Calculate standard error
- se = math.sqrt(p_hat * (1 - p_hat) / sample_size)
-
- # Calculate margin of error
- me = z * se
-
- # Calculate confidence interval bounds as proportions
- lower_bound_prop = p_hat - me
- upper_bound_prop = p_hat + me
-
- # Convert proportion bounds to integer counts
- lower_bound_count = math.ceil(lower_bound_prop * sample_size)
- upper_bound_count = int(upper_bound_prop * sample_size)
-
- # Format the output string
output = f"> [!NOTE]\n"
- output += f"> ### There are {failure_count} failures out of {sample_size} generations.\n"
- output += f"> Sample Proportion (p̂): {p_hat:.4f}\n"
- output += f"> Standard Error (SE): {se:.6f}\n"
- output += f"> Margin of Error (ME): {me:.6f}\n"
- output += f"> 90% Confidence Interval: [{lower_bound_prop:.6f}, {upper_bound_prop:.6f}]\n"
- output += f"> 90% Confidence Interval (Count): [{lower_bound_count}, {upper_bound_count}]"
+ output += f"> ### There are {analysis.failure_count} failures out of {analysis.sample_size} generations.\n"
+ output += f"> Sample Proportion (p̂): {analysis.proportion:.4f}\n"
+ output += f"> Standard Error (SE): {analysis.standard_error:.6f}\n"
+ output += f"> Margin of Error (ME): {analysis.margin_of_error:.6f}\n"
+ output += f"> 90% Confidence Interval: [{analysis.confidence_interval_prop[0]:.6f}, {analysis.confidence_interval_prop[1]:.6f}]\n"
+ output += f"> 90% Confidence Interval (Count): [{analysis.confidence_interval_count[0]}, {analysis.confidence_interval_count[1]}]"
return output
+
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: python reporter.py failure_count sample_size")
@@ -110,4 +90,6 @@ def error_margin_summary(failure_count, sample_size):
failure_count = int(sys.argv[1])
sample_size = int(sys.argv[2])
- print(Reporter.error_margin_summary(failure_count, sample_size))
+ analysis = analyse_sample_from_test(failure_count, sample_size)
+
+ print(Reporter.format_summary(analysis))
diff --git a/src/cat_ai/statistical_analysis.py b/src/cat_ai/statistical_analysis.py
new file mode 100644
index 0000000..4823950
--- /dev/null
+++ b/src/cat_ai/statistical_analysis.py
@@ -0,0 +1,86 @@
+import math
+from dataclasses import astuple, dataclass
+from typing import Tuple, Any
+from statistics import NormalDist
+
+
+@dataclass
+class StatisticalAnalysis:
+ """Class for statistical analysis results of test runs."""
+
+ failure_count: int
+ sample_size: int
+ proportion: float
+ standard_error: float
+ margin_of_error: float
+ confidence_interval_prop: Tuple[float, float]
+ confidence_interval_count: Tuple[int, int]
+
+ def as_csv_row(self) -> list:
+ """Return a flat tuple representation suitable for CSV writing."""
+ # Unpack nested tuples for CSV-friendly format
+ flat_data: list[Any] = []
+ for item in astuple(self):
+ if isinstance(item, tuple):
+ flat_data.extend(item)
+ else:
+ flat_data.append(item)
+ return flat_data
+
+ @classmethod
+ def get_csv_headers(cls) -> list[str]:
+ """Generate CSV headers based on class fields."""
+ headers = [
+ "failure_count",
+ "sample_size",
+ "proportion",
+ "standard_error",
+ "margin_of_error",
+ "confidence_proportion_lower",
+ "confidence_proportion_upper",
+ "confidence_lower",
+ "confidence_upper",
+ ]
+ return headers
+
+
+def analyse_sample_from_test(failure_count: int, sample_size: int) -> StatisticalAnalysis:
+ """
+ Calculate the error margin and confidence interval for a given sample.
+
+ Args:
+ failure_count (int): Number of failures in the sample
+ sample_size (int): Total size of the sample
+
+ Returns:
+ StatisticalAnalysis: Object containing all statistical analysis data
+ """
+ # Calculate sample proportion
+ p_hat = failure_count / sample_size
+
+ # Determine z-score for 90% confidence level using NormalDist
+ z = NormalDist().inv_cdf(0.95) # For 90% CI, we need 95% percentile (two-tailed)
+
+ # Calculate standard error
+ se = math.sqrt(p_hat * (1 - p_hat) / sample_size)
+
+ # Calculate margin of error
+ me = z * se
+
+ # Calculate confidence interval bounds as proportions
+ lower_bound_prop = p_hat - me
+ upper_bound_prop = p_hat + me
+
+ # Convert proportion bounds to integer counts
+ lower_bound_count = math.ceil(lower_bound_prop * sample_size)
+ upper_bound_count = int(upper_bound_prop * sample_size)
+
+ return StatisticalAnalysis(
+ failure_count=failure_count,
+ sample_size=sample_size,
+ proportion=p_hat,
+ standard_error=se,
+ margin_of_error=me,
+ confidence_interval_prop=(lower_bound_prop, upper_bound_prop),
+ confidence_interval_count=(lower_bound_count, upper_bound_count),
+ )
diff --git a/tests/snapshots/test_statistical_analysis/test_failure_rate_bar_graph/failure_rate_bar_graph.svg b/tests/snapshots/test_statistical_analysis/test_failure_rate_bar_graph/failure_rate_bar_graph.svg
new file mode 100644
index 0000000..30aa52e
--- /dev/null
+++ b/tests/snapshots/test_statistical_analysis/test_failure_rate_bar_graph/failure_rate_bar_graph.svg
@@ -0,0 +1,2354 @@
+
+
+
diff --git a/tests/snapshots/test_statistical_analysis/test_failure_rate_bar_graph/failure_rate_results.csv b/tests/snapshots/test_statistical_analysis/test_failure_rate_bar_graph/failure_rate_results.csv
new file mode 100644
index 0000000..2a2a1a7
--- /dev/null
+++ b/tests/snapshots/test_statistical_analysis/test_failure_rate_bar_graph/failure_rate_results.csv
@@ -0,0 +1,102 @@
+failure_count,sample_size,proportion,standard_error,margin_of_error,confidence_proportion_lower,confidence_proportion_upper,confidence_lower,confidence_upper
+0,100,0.0,0.0,0.0,0.0,0.0,0,0
+1,100,0.01,0.0099498743710662,0.01636608694695973,-0.006366086946959731,0.02636608694695973,0,2
+2,100,0.02,0.014,0.023027950777320602,-0.0030279507773206017,0.043027950777320606,0,4
+3,100,0.03,0.01705872210923198,0.02805910093252748,0.00194089906747252,0.058059100932527474,1,5
+4,100,0.04,0.019595917942265423,0.0322324167007787,0.007767583299221302,0.0722324167007787,1,7
+5,100,0.05,0.021794494717703367,0.03584875368398907,0.014151246316010932,0.08584875368398907,2,8
+6,100,0.06,0.023748684174075833,0.03906310929905365,0.02093689070094635,0.09906310929905365,3,9
+7,100,0.07,0.02551470164434615,0.04196794954028744,0.02803205045971257,0.11196794954028744,3,11
+8,100,0.08,0.027129319932501072,0.044623760287701236,0.035376239712298765,0.12462376028770124,4,12
+9,100,0.09,0.02861817604250837,0.047072710660255604,0.04292728933974439,0.13707271066025561,5,13
+10,100,0.1,0.030000000000000002,0.04934560880854415,0.05065439119145586,0.14934560880854414,6,14
+11,100,0.11,0.03128897569432403,0.05146578515440532,0.05853421484559468,0.16146578515440532,6,16
+12,100,0.12,0.03249615361854384,0.053451416141434026,0.06654858385856596,0.17345141614143403,7,17
+13,100,0.13,0.03363034344160047,0.05531699238554017,0.07468300761445984,0.18531699238554017,8,18
+14,100,0.14,0.03469870314579494,0.057074287719873246,0.08292571228012677,0.19707428771987326,9,19
+15,100,0.15,0.035707142142714254,0.05873302226151528,0.09126697773848472,0.20873302226151527,10,20
+16,100,0.16,0.03666060555964672,0.060301330021022184,0.09969866997897782,0.2203013300210222,10,22
+17,100,0.17,0.0375632799419859,0.061786097252768964,0.10821390274723106,0.23178609725276897,11,23
+18,100,0.18,0.03841874542459709,0.06319321275457378,0.11680678724542622,0.24319321275457378,12,24
+19,100,0.19,0.039230090491866064,0.06452775663118034,0.12547224336881968,0.2545277566311803,13,25
+20,100,0.2,0.04,0.06579414507805886,0.13420585492194115,0.2657941450780589,14,26
+21,100,0.21,0.0407308237088326,0.0669962431061943,0.14300375689380568,0.2769962431061943,15,27
+22,100,0.22,0.04142463035441596,0.06813745348358512,0.1518625465164149,0.2881374534835851,16,28
+23,100,0.23,0.042083250825001625,0.06922078775341244,0.1607792122465876,0.29922078775341243,17,29
+24,100,0.24,0.04270831300812525,0.07024892355239352,0.16975107644760645,0.31024892355239353,17,31
+25,100,0.25,0.04330127018922193,0.07122425132234733,0.17877574867765267,0.32122425132234733,18,32
+26,100,0.26,0.04386342439892262,0.07214891271307955,0.18785108728692046,0.33214891271307956,19,33
+27,100,0.27,0.044395945760846225,0.07302483240666872,0.19697516759333128,0.34302483240666876,20,34
+28,100,0.28,0.0448998886412873,0.0738537446813386,0.20614625531866143,0.3538537446813386,21,35
+29,100,0.29,0.045376205218153706,0.0746372157303744,0.21536278426962557,0.3646372157303744,22,36
+30,100,0.3,0.0458257569495584,0.07537666252627774,0.22462333747372226,0.3753766625262777,23,37
+31,100,0.31,0.04624932431938871,0.07607336885080142,0.2339266311491986,0.3860733688508014,24,38
+32,100,0.32,0.0466476151587624,0.07672849898252677,0.24327150101747325,0.39672849898252677,25,39
+33,100,0.33,0.04702127178203499,0.07734310943455114,0.2526568905654489,0.40734310943455115,26,40
+34,100,0.34,0.04737087712930804,0.07791815905801484,0.26208184094198517,0.4179181590580149,27,41
+35,100,0.35,0.047696960070847276,0.07845451776709265,0.2715454822329073,0.42845451776709265,28,42
+36,100,0.36,0.048,0.07895297409367064,0.28104702590632935,0.4389529740936706,29,43
+37,100,0.37,0.048280430818293245,0.07941424174224924,0.29058575825775074,0.44941424174224925,30,44
+38,100,0.38,0.048538644398046386,0.07983896528543433,0.3001610347145657,0.45983896528543433,31,45
+39,100,0.39,0.048774993593028795,0.0802277251160282,0.3097722748839718,0.47022772511602823,31,47
+40,100,0.4,0.04898979485566356,0.08058104175194675,0.3194189582480533,0.48058104175194677,32,48
+41,100,0.41,0.04918333050943175,0.08089937957399178,0.3291006204260082,0.49089937957399177,33,49
+42,100,0.42,0.04935585071701227,0.08118315006315302,0.33881684993684696,0.501183150063153,34,50
+43,100,0.43,0.04950757517794625,0.08143271459301754,0.34856728540698245,0.5114327145930175,35,51
+44,100,0.44,0.04963869458396343,0.08164838682356862,0.3583516131764314,0.5216483868235686,36,52
+45,100,0.45,0.049749371855331,0.08183043473479866,0.36816956526520134,0.5318304347347986,37,53
+46,100,0.46,0.04983974317750845,0.08197908233185464,0.3780209176681454,0.5419790823318547,38,54
+47,100,0.47,0.04990991885387112,0.08209451104764354,0.38790548895235644,0.5520945110476435,39,55
+48,100,0.48,0.049959983987187186,0.08217686086376229,0.3978231391362377,0.5621768608637623,40,56
+49,100,0.49,0.04998999899979995,0.08222623116612139,0.4077737688338786,0.5722262311661214,41,57
+50,100,0.5,0.05,0.08224268134757358,0.41775731865242643,0.5822426813475736,42,58
+51,100,0.51,0.04998999899979995,0.08222623116612139,0.42777376883387863,0.5922262311661214,43,59
+52,100,0.52,0.049959983987187186,0.08217686086376229,0.4378231391362377,0.6021768608637623,44,60
+53,100,0.53,0.04990991885387112,0.08209451104764354,0.4479054889523565,0.6120945110476436,45,61
+54,100,0.54,0.04983974317750845,0.08197908233185464,0.4580209176681454,0.6219790823318547,46,62
+55,100,0.55,0.049749371855330994,0.08183043473479865,0.46816956526520137,0.6318304347347987,47,63
+56,100,0.56,0.04963869458396342,0.0816483868235686,0.47835161317643143,0.6416483868235686,48,64
+57,100,0.57,0.04950757517794625,0.08143271459301754,0.4885672854069824,0.6514327145930174,49,65
+58,100,0.58,0.04935585071701227,0.08118315006315302,0.49881684993684694,0.661183150063153,50,66
+59,100,0.59,0.04918333050943175,0.08089937957399178,0.5091006204260082,0.6708993795739917,51,67
+60,100,0.6,0.04898979485566356,0.08058104175194675,0.5194189582480533,0.6805810417519467,52,68
+61,100,0.61,0.048774993593028795,0.0802277251160282,0.5297722748839718,0.6902277251160281,53,69
+62,100,0.62,0.048538644398046386,0.07983896528543433,0.5401610347145657,0.6998389652854343,55,69
+63,100,0.63,0.048280430818293245,0.07941424174224924,0.5505857582577508,0.7094142417422492,56,70
+64,100,0.64,0.048,0.07895297409367064,0.5610470259063294,0.7189529740936706,57,71
+65,100,0.65,0.047696960070847276,0.07845451776709265,0.5715454822329074,0.7284545177670927,58,72
+66,100,0.66,0.04737087712930804,0.07791815905801484,0.5820818409419852,0.7379181590580148,59,73
+67,100,0.67,0.04702127178203499,0.07734310943455114,0.5926568905654489,0.7473431094345512,60,74
+68,100,0.68,0.0466476151587624,0.07672849898252677,0.6032715010174733,0.7567284989825268,61,75
+69,100,0.69,0.04624932431938871,0.07607336885080142,0.6139266311491985,0.7660733688508014,62,76
+70,100,0.7,0.045825756949558406,0.07537666252627774,0.6246233374737222,0.7753766625262777,63,77
+71,100,0.71,0.04537620521815371,0.07463721573037442,0.6353627842696256,0.7846372157303744,64,78
+72,100,0.72,0.0448998886412873,0.0738537446813386,0.6461462553186614,0.7938537446813385,65,79
+73,100,0.73,0.044395945760846225,0.07302483240666872,0.6569751675933313,0.8030248324066687,66,80
+74,100,0.74,0.04386342439892262,0.07214891271307955,0.6678510872869204,0.8121489127130795,67,81
+75,100,0.75,0.04330127018922193,0.07122425132234733,0.6787757486776527,0.8212242513223473,68,82
+76,100,0.76,0.04270831300812525,0.07024892355239352,0.6897510764476065,0.8302489235523935,69,83
+77,100,0.77,0.042083250825001625,0.06922078775341244,0.7007792122465876,0.8392207877534125,71,83
+78,100,0.78,0.04142463035441595,0.0681374534835851,0.7118625465164149,0.8481374534835852,72,84
+79,100,0.79,0.0407308237088326,0.0669962431061943,0.7230037568938057,0.8569962431061944,73,85
+80,100,0.8,0.04,0.06579414507805886,0.7342058549219412,0.8657941450780589,74,86
+81,100,0.81,0.03923009049186606,0.06452775663118032,0.7454722433688197,0.8745277566311804,75,87
+82,100,0.82,0.0384187454245971,0.06319321275457379,0.7568067872454262,0.8831932127545737,76,88
+83,100,0.83,0.037563279941985904,0.06178609725276898,0.768213902747231,0.8917860972527689,77,89
+84,100,0.84,0.036660605559646724,0.0603013300210222,0.7796986699789777,0.9003013300210222,78,90
+85,100,0.85,0.035707142142714254,0.05873302226151528,0.7912669777384846,0.9087330222615153,80,90
+86,100,0.86,0.03469870314579494,0.057074287719873246,0.8029257122801268,0.9170742877198732,81,91
+87,100,0.87,0.03363034344160047,0.05531699238554017,0.8146830076144598,0.9253169923855402,82,92
+88,100,0.88,0.03249615361854384,0.053451416141434026,0.826548583858566,0.933451416141434,83,93
+89,100,0.89,0.031288975694324025,0.05146578515440531,0.8385342148455948,0.9414657851544053,84,94
+90,100,0.9,0.03,0.04934560880854414,0.8506543911914559,0.9493456088085441,86,94
+91,100,0.91,0.028618176042508364,0.04707271066025559,0.8629272893397444,0.9570727106602557,87,95
+92,100,0.92,0.027129319932501065,0.04462376028770123,0.8753762397122988,0.9646237602877012,88,96
+93,100,0.93,0.02551470164434614,0.04196794954028742,0.8880320504597127,0.9719679495402874,89,97
+94,100,0.94,0.023748684174075843,0.03906310929905366,0.9009368907009463,0.9790631092990536,91,97
+95,100,0.95,0.021794494717703377,0.035848753683989085,0.9141512463160109,0.985848753683989,92,98
+96,100,0.96,0.019595917942265433,0.03223241670077871,0.9277675832992213,0.9922324167007787,93,99
+97,100,0.97,0.017058722109231986,0.02805910093252749,0.9419408990674725,0.9980591009325275,95,99
+98,100,0.98,0.014000000000000005,0.02302795077732061,0.9569720492226794,1.0030279507773205,96,100
+99,100,0.99,0.009949874371066205,0.016366086946959738,0.9736339130530403,1.0063660869469597,98,100
+100,100,1.0,0.0,0.0,1.0,1.0,100,100
diff --git a/tests/snapshots/test_statistical_analysis/test_failure_rate_bar_graph/failure_rate_results.json b/tests/snapshots/test_statistical_analysis/test_failure_rate_bar_graph/failure_rate_results.json
new file mode 100644
index 0000000..ba674c7
--- /dev/null
+++ b/tests/snapshots/test_statistical_analysis/test_failure_rate_bar_graph/failure_rate_results.json
@@ -0,0 +1,1517 @@
+[
+ {
+ "failure_count": 0,
+ "sample_size": 100,
+ "proportion": 0.0,
+ "standard_error": 0.0,
+ "margin_of_error": 0.0,
+ "confidence_interval_prop": [
+ 0.0,
+ 0.0
+ ],
+ "confidence_interval_count": [
+ 0,
+ 0
+ ]
+ },
+ {
+ "failure_count": 1,
+ "sample_size": 100,
+ "proportion": 0.01,
+ "standard_error": 0.0099498743710662,
+ "margin_of_error": 0.01636608694695973,
+ "confidence_interval_prop": [
+ -0.006366086946959731,
+ 0.02636608694695973
+ ],
+ "confidence_interval_count": [
+ 0,
+ 2
+ ]
+ },
+ {
+ "failure_count": 2,
+ "sample_size": 100,
+ "proportion": 0.02,
+ "standard_error": 0.014,
+ "margin_of_error": 0.023027950777320602,
+ "confidence_interval_prop": [
+ -0.0030279507773206017,
+ 0.043027950777320606
+ ],
+ "confidence_interval_count": [
+ 0,
+ 4
+ ]
+ },
+ {
+ "failure_count": 3,
+ "sample_size": 100,
+ "proportion": 0.03,
+ "standard_error": 0.01705872210923198,
+ "margin_of_error": 0.02805910093252748,
+ "confidence_interval_prop": [
+ 0.00194089906747252,
+ 0.058059100932527474
+ ],
+ "confidence_interval_count": [
+ 1,
+ 5
+ ]
+ },
+ {
+ "failure_count": 4,
+ "sample_size": 100,
+ "proportion": 0.04,
+ "standard_error": 0.019595917942265423,
+ "margin_of_error": 0.0322324167007787,
+ "confidence_interval_prop": [
+ 0.007767583299221302,
+ 0.0722324167007787
+ ],
+ "confidence_interval_count": [
+ 1,
+ 7
+ ]
+ },
+ {
+ "failure_count": 5,
+ "sample_size": 100,
+ "proportion": 0.05,
+ "standard_error": 0.021794494717703367,
+ "margin_of_error": 0.03584875368398907,
+ "confidence_interval_prop": [
+ 0.014151246316010932,
+ 0.08584875368398907
+ ],
+ "confidence_interval_count": [
+ 2,
+ 8
+ ]
+ },
+ {
+ "failure_count": 6,
+ "sample_size": 100,
+ "proportion": 0.06,
+ "standard_error": 0.023748684174075833,
+ "margin_of_error": 0.03906310929905365,
+ "confidence_interval_prop": [
+ 0.02093689070094635,
+ 0.09906310929905365
+ ],
+ "confidence_interval_count": [
+ 3,
+ 9
+ ]
+ },
+ {
+ "failure_count": 7,
+ "sample_size": 100,
+ "proportion": 0.07,
+ "standard_error": 0.02551470164434615,
+ "margin_of_error": 0.04196794954028744,
+ "confidence_interval_prop": [
+ 0.02803205045971257,
+ 0.11196794954028744
+ ],
+ "confidence_interval_count": [
+ 3,
+ 11
+ ]
+ },
+ {
+ "failure_count": 8,
+ "sample_size": 100,
+ "proportion": 0.08,
+ "standard_error": 0.027129319932501072,
+ "margin_of_error": 0.044623760287701236,
+ "confidence_interval_prop": [
+ 0.035376239712298765,
+ 0.12462376028770124
+ ],
+ "confidence_interval_count": [
+ 4,
+ 12
+ ]
+ },
+ {
+ "failure_count": 9,
+ "sample_size": 100,
+ "proportion": 0.09,
+ "standard_error": 0.02861817604250837,
+ "margin_of_error": 0.047072710660255604,
+ "confidence_interval_prop": [
+ 0.04292728933974439,
+ 0.13707271066025561
+ ],
+ "confidence_interval_count": [
+ 5,
+ 13
+ ]
+ },
+ {
+ "failure_count": 10,
+ "sample_size": 100,
+ "proportion": 0.1,
+ "standard_error": 0.030000000000000002,
+ "margin_of_error": 0.04934560880854415,
+ "confidence_interval_prop": [
+ 0.05065439119145586,
+ 0.14934560880854414
+ ],
+ "confidence_interval_count": [
+ 6,
+ 14
+ ]
+ },
+ {
+ "failure_count": 11,
+ "sample_size": 100,
+ "proportion": 0.11,
+ "standard_error": 0.03128897569432403,
+ "margin_of_error": 0.05146578515440532,
+ "confidence_interval_prop": [
+ 0.05853421484559468,
+ 0.16146578515440532
+ ],
+ "confidence_interval_count": [
+ 6,
+ 16
+ ]
+ },
+ {
+ "failure_count": 12,
+ "sample_size": 100,
+ "proportion": 0.12,
+ "standard_error": 0.03249615361854384,
+ "margin_of_error": 0.053451416141434026,
+ "confidence_interval_prop": [
+ 0.06654858385856596,
+ 0.17345141614143403
+ ],
+ "confidence_interval_count": [
+ 7,
+ 17
+ ]
+ },
+ {
+ "failure_count": 13,
+ "sample_size": 100,
+ "proportion": 0.13,
+ "standard_error": 0.03363034344160047,
+ "margin_of_error": 0.05531699238554017,
+ "confidence_interval_prop": [
+ 0.07468300761445984,
+ 0.18531699238554017
+ ],
+ "confidence_interval_count": [
+ 8,
+ 18
+ ]
+ },
+ {
+ "failure_count": 14,
+ "sample_size": 100,
+ "proportion": 0.14,
+ "standard_error": 0.03469870314579494,
+ "margin_of_error": 0.057074287719873246,
+ "confidence_interval_prop": [
+ 0.08292571228012677,
+ 0.19707428771987326
+ ],
+ "confidence_interval_count": [
+ 9,
+ 19
+ ]
+ },
+ {
+ "failure_count": 15,
+ "sample_size": 100,
+ "proportion": 0.15,
+ "standard_error": 0.035707142142714254,
+ "margin_of_error": 0.05873302226151528,
+ "confidence_interval_prop": [
+ 0.09126697773848472,
+ 0.20873302226151527
+ ],
+ "confidence_interval_count": [
+ 10,
+ 20
+ ]
+ },
+ {
+ "failure_count": 16,
+ "sample_size": 100,
+ "proportion": 0.16,
+ "standard_error": 0.03666060555964672,
+ "margin_of_error": 0.060301330021022184,
+ "confidence_interval_prop": [
+ 0.09969866997897782,
+ 0.2203013300210222
+ ],
+ "confidence_interval_count": [
+ 10,
+ 22
+ ]
+ },
+ {
+ "failure_count": 17,
+ "sample_size": 100,
+ "proportion": 0.17,
+ "standard_error": 0.0375632799419859,
+ "margin_of_error": 0.061786097252768964,
+ "confidence_interval_prop": [
+ 0.10821390274723106,
+ 0.23178609725276897
+ ],
+ "confidence_interval_count": [
+ 11,
+ 23
+ ]
+ },
+ {
+ "failure_count": 18,
+ "sample_size": 100,
+ "proportion": 0.18,
+ "standard_error": 0.03841874542459709,
+ "margin_of_error": 0.06319321275457378,
+ "confidence_interval_prop": [
+ 0.11680678724542622,
+ 0.24319321275457378
+ ],
+ "confidence_interval_count": [
+ 12,
+ 24
+ ]
+ },
+ {
+ "failure_count": 19,
+ "sample_size": 100,
+ "proportion": 0.19,
+ "standard_error": 0.039230090491866064,
+ "margin_of_error": 0.06452775663118034,
+ "confidence_interval_prop": [
+ 0.12547224336881968,
+ 0.2545277566311803
+ ],
+ "confidence_interval_count": [
+ 13,
+ 25
+ ]
+ },
+ {
+ "failure_count": 20,
+ "sample_size": 100,
+ "proportion": 0.2,
+ "standard_error": 0.04,
+ "margin_of_error": 0.06579414507805886,
+ "confidence_interval_prop": [
+ 0.13420585492194115,
+ 0.2657941450780589
+ ],
+ "confidence_interval_count": [
+ 14,
+ 26
+ ]
+ },
+ {
+ "failure_count": 21,
+ "sample_size": 100,
+ "proportion": 0.21,
+ "standard_error": 0.0407308237088326,
+ "margin_of_error": 0.0669962431061943,
+ "confidence_interval_prop": [
+ 0.14300375689380568,
+ 0.2769962431061943
+ ],
+ "confidence_interval_count": [
+ 15,
+ 27
+ ]
+ },
+ {
+ "failure_count": 22,
+ "sample_size": 100,
+ "proportion": 0.22,
+ "standard_error": 0.04142463035441596,
+ "margin_of_error": 0.06813745348358512,
+ "confidence_interval_prop": [
+ 0.1518625465164149,
+ 0.2881374534835851
+ ],
+ "confidence_interval_count": [
+ 16,
+ 28
+ ]
+ },
+ {
+ "failure_count": 23,
+ "sample_size": 100,
+ "proportion": 0.23,
+ "standard_error": 0.042083250825001625,
+ "margin_of_error": 0.06922078775341244,
+ "confidence_interval_prop": [
+ 0.1607792122465876,
+ 0.29922078775341243
+ ],
+ "confidence_interval_count": [
+ 17,
+ 29
+ ]
+ },
+ {
+ "failure_count": 24,
+ "sample_size": 100,
+ "proportion": 0.24,
+ "standard_error": 0.04270831300812525,
+ "margin_of_error": 0.07024892355239352,
+ "confidence_interval_prop": [
+ 0.16975107644760645,
+ 0.31024892355239353
+ ],
+ "confidence_interval_count": [
+ 17,
+ 31
+ ]
+ },
+ {
+ "failure_count": 25,
+ "sample_size": 100,
+ "proportion": 0.25,
+ "standard_error": 0.04330127018922193,
+ "margin_of_error": 0.07122425132234733,
+ "confidence_interval_prop": [
+ 0.17877574867765267,
+ 0.32122425132234733
+ ],
+ "confidence_interval_count": [
+ 18,
+ 32
+ ]
+ },
+ {
+ "failure_count": 26,
+ "sample_size": 100,
+ "proportion": 0.26,
+ "standard_error": 0.04386342439892262,
+ "margin_of_error": 0.07214891271307955,
+ "confidence_interval_prop": [
+ 0.18785108728692046,
+ 0.33214891271307956
+ ],
+ "confidence_interval_count": [
+ 19,
+ 33
+ ]
+ },
+ {
+ "failure_count": 27,
+ "sample_size": 100,
+ "proportion": 0.27,
+ "standard_error": 0.044395945760846225,
+ "margin_of_error": 0.07302483240666872,
+ "confidence_interval_prop": [
+ 0.19697516759333128,
+ 0.34302483240666876
+ ],
+ "confidence_interval_count": [
+ 20,
+ 34
+ ]
+ },
+ {
+ "failure_count": 28,
+ "sample_size": 100,
+ "proportion": 0.28,
+ "standard_error": 0.0448998886412873,
+ "margin_of_error": 0.0738537446813386,
+ "confidence_interval_prop": [
+ 0.20614625531866143,
+ 0.3538537446813386
+ ],
+ "confidence_interval_count": [
+ 21,
+ 35
+ ]
+ },
+ {
+ "failure_count": 29,
+ "sample_size": 100,
+ "proportion": 0.29,
+ "standard_error": 0.045376205218153706,
+ "margin_of_error": 0.0746372157303744,
+ "confidence_interval_prop": [
+ 0.21536278426962557,
+ 0.3646372157303744
+ ],
+ "confidence_interval_count": [
+ 22,
+ 36
+ ]
+ },
+ {
+ "failure_count": 30,
+ "sample_size": 100,
+ "proportion": 0.3,
+ "standard_error": 0.0458257569495584,
+ "margin_of_error": 0.07537666252627774,
+ "confidence_interval_prop": [
+ 0.22462333747372226,
+ 0.3753766625262777
+ ],
+ "confidence_interval_count": [
+ 23,
+ 37
+ ]
+ },
+ {
+ "failure_count": 31,
+ "sample_size": 100,
+ "proportion": 0.31,
+ "standard_error": 0.04624932431938871,
+ "margin_of_error": 0.07607336885080142,
+ "confidence_interval_prop": [
+ 0.2339266311491986,
+ 0.3860733688508014
+ ],
+ "confidence_interval_count": [
+ 24,
+ 38
+ ]
+ },
+ {
+ "failure_count": 32,
+ "sample_size": 100,
+ "proportion": 0.32,
+ "standard_error": 0.0466476151587624,
+ "margin_of_error": 0.07672849898252677,
+ "confidence_interval_prop": [
+ 0.24327150101747325,
+ 0.39672849898252677
+ ],
+ "confidence_interval_count": [
+ 25,
+ 39
+ ]
+ },
+ {
+ "failure_count": 33,
+ "sample_size": 100,
+ "proportion": 0.33,
+ "standard_error": 0.04702127178203499,
+ "margin_of_error": 0.07734310943455114,
+ "confidence_interval_prop": [
+ 0.2526568905654489,
+ 0.40734310943455115
+ ],
+ "confidence_interval_count": [
+ 26,
+ 40
+ ]
+ },
+ {
+ "failure_count": 34,
+ "sample_size": 100,
+ "proportion": 0.34,
+ "standard_error": 0.04737087712930804,
+ "margin_of_error": 0.07791815905801484,
+ "confidence_interval_prop": [
+ 0.26208184094198517,
+ 0.4179181590580149
+ ],
+ "confidence_interval_count": [
+ 27,
+ 41
+ ]
+ },
+ {
+ "failure_count": 35,
+ "sample_size": 100,
+ "proportion": 0.35,
+ "standard_error": 0.047696960070847276,
+ "margin_of_error": 0.07845451776709265,
+ "confidence_interval_prop": [
+ 0.2715454822329073,
+ 0.42845451776709265
+ ],
+ "confidence_interval_count": [
+ 28,
+ 42
+ ]
+ },
+ {
+ "failure_count": 36,
+ "sample_size": 100,
+ "proportion": 0.36,
+ "standard_error": 0.048,
+ "margin_of_error": 0.07895297409367064,
+ "confidence_interval_prop": [
+ 0.28104702590632935,
+ 0.4389529740936706
+ ],
+ "confidence_interval_count": [
+ 29,
+ 43
+ ]
+ },
+ {
+ "failure_count": 37,
+ "sample_size": 100,
+ "proportion": 0.37,
+ "standard_error": 0.048280430818293245,
+ "margin_of_error": 0.07941424174224924,
+ "confidence_interval_prop": [
+ 0.29058575825775074,
+ 0.44941424174224925
+ ],
+ "confidence_interval_count": [
+ 30,
+ 44
+ ]
+ },
+ {
+ "failure_count": 38,
+ "sample_size": 100,
+ "proportion": 0.38,
+ "standard_error": 0.048538644398046386,
+ "margin_of_error": 0.07983896528543433,
+ "confidence_interval_prop": [
+ 0.3001610347145657,
+ 0.45983896528543433
+ ],
+ "confidence_interval_count": [
+ 31,
+ 45
+ ]
+ },
+ {
+ "failure_count": 39,
+ "sample_size": 100,
+ "proportion": 0.39,
+ "standard_error": 0.048774993593028795,
+ "margin_of_error": 0.0802277251160282,
+ "confidence_interval_prop": [
+ 0.3097722748839718,
+ 0.47022772511602823
+ ],
+ "confidence_interval_count": [
+ 31,
+ 47
+ ]
+ },
+ {
+ "failure_count": 40,
+ "sample_size": 100,
+ "proportion": 0.4,
+ "standard_error": 0.04898979485566356,
+ "margin_of_error": 0.08058104175194675,
+ "confidence_interval_prop": [
+ 0.3194189582480533,
+ 0.48058104175194677
+ ],
+ "confidence_interval_count": [
+ 32,
+ 48
+ ]
+ },
+ {
+ "failure_count": 41,
+ "sample_size": 100,
+ "proportion": 0.41,
+ "standard_error": 0.04918333050943175,
+ "margin_of_error": 0.08089937957399178,
+ "confidence_interval_prop": [
+ 0.3291006204260082,
+ 0.49089937957399177
+ ],
+ "confidence_interval_count": [
+ 33,
+ 49
+ ]
+ },
+ {
+ "failure_count": 42,
+ "sample_size": 100,
+ "proportion": 0.42,
+ "standard_error": 0.04935585071701227,
+ "margin_of_error": 0.08118315006315302,
+ "confidence_interval_prop": [
+ 0.33881684993684696,
+ 0.501183150063153
+ ],
+ "confidence_interval_count": [
+ 34,
+ 50
+ ]
+ },
+ {
+ "failure_count": 43,
+ "sample_size": 100,
+ "proportion": 0.43,
+ "standard_error": 0.04950757517794625,
+ "margin_of_error": 0.08143271459301754,
+ "confidence_interval_prop": [
+ 0.34856728540698245,
+ 0.5114327145930175
+ ],
+ "confidence_interval_count": [
+ 35,
+ 51
+ ]
+ },
+ {
+ "failure_count": 44,
+ "sample_size": 100,
+ "proportion": 0.44,
+ "standard_error": 0.04963869458396343,
+ "margin_of_error": 0.08164838682356862,
+ "confidence_interval_prop": [
+ 0.3583516131764314,
+ 0.5216483868235686
+ ],
+ "confidence_interval_count": [
+ 36,
+ 52
+ ]
+ },
+ {
+ "failure_count": 45,
+ "sample_size": 100,
+ "proportion": 0.45,
+ "standard_error": 0.049749371855331,
+ "margin_of_error": 0.08183043473479866,
+ "confidence_interval_prop": [
+ 0.36816956526520134,
+ 0.5318304347347986
+ ],
+ "confidence_interval_count": [
+ 37,
+ 53
+ ]
+ },
+ {
+ "failure_count": 46,
+ "sample_size": 100,
+ "proportion": 0.46,
+ "standard_error": 0.04983974317750845,
+ "margin_of_error": 0.08197908233185464,
+ "confidence_interval_prop": [
+ 0.3780209176681454,
+ 0.5419790823318547
+ ],
+ "confidence_interval_count": [
+ 38,
+ 54
+ ]
+ },
+ {
+ "failure_count": 47,
+ "sample_size": 100,
+ "proportion": 0.47,
+ "standard_error": 0.04990991885387112,
+ "margin_of_error": 0.08209451104764354,
+ "confidence_interval_prop": [
+ 0.38790548895235644,
+ 0.5520945110476435
+ ],
+ "confidence_interval_count": [
+ 39,
+ 55
+ ]
+ },
+ {
+ "failure_count": 48,
+ "sample_size": 100,
+ "proportion": 0.48,
+ "standard_error": 0.049959983987187186,
+ "margin_of_error": 0.08217686086376229,
+ "confidence_interval_prop": [
+ 0.3978231391362377,
+ 0.5621768608637623
+ ],
+ "confidence_interval_count": [
+ 40,
+ 56
+ ]
+ },
+ {
+ "failure_count": 49,
+ "sample_size": 100,
+ "proportion": 0.49,
+ "standard_error": 0.04998999899979995,
+ "margin_of_error": 0.08222623116612139,
+ "confidence_interval_prop": [
+ 0.4077737688338786,
+ 0.5722262311661214
+ ],
+ "confidence_interval_count": [
+ 41,
+ 57
+ ]
+ },
+ {
+ "failure_count": 50,
+ "sample_size": 100,
+ "proportion": 0.5,
+ "standard_error": 0.05,
+ "margin_of_error": 0.08224268134757358,
+ "confidence_interval_prop": [
+ 0.41775731865242643,
+ 0.5822426813475736
+ ],
+ "confidence_interval_count": [
+ 42,
+ 58
+ ]
+ },
+ {
+ "failure_count": 51,
+ "sample_size": 100,
+ "proportion": 0.51,
+ "standard_error": 0.04998999899979995,
+ "margin_of_error": 0.08222623116612139,
+ "confidence_interval_prop": [
+ 0.42777376883387863,
+ 0.5922262311661214
+ ],
+ "confidence_interval_count": [
+ 43,
+ 59
+ ]
+ },
+ {
+ "failure_count": 52,
+ "sample_size": 100,
+ "proportion": 0.52,
+ "standard_error": 0.049959983987187186,
+ "margin_of_error": 0.08217686086376229,
+ "confidence_interval_prop": [
+ 0.4378231391362377,
+ 0.6021768608637623
+ ],
+ "confidence_interval_count": [
+ 44,
+ 60
+ ]
+ },
+ {
+ "failure_count": 53,
+ "sample_size": 100,
+ "proportion": 0.53,
+ "standard_error": 0.04990991885387112,
+ "margin_of_error": 0.08209451104764354,
+ "confidence_interval_prop": [
+ 0.4479054889523565,
+ 0.6120945110476436
+ ],
+ "confidence_interval_count": [
+ 45,
+ 61
+ ]
+ },
+ {
+ "failure_count": 54,
+ "sample_size": 100,
+ "proportion": 0.54,
+ "standard_error": 0.04983974317750845,
+ "margin_of_error": 0.08197908233185464,
+ "confidence_interval_prop": [
+ 0.4580209176681454,
+ 0.6219790823318547
+ ],
+ "confidence_interval_count": [
+ 46,
+ 62
+ ]
+ },
+ {
+ "failure_count": 55,
+ "sample_size": 100,
+ "proportion": 0.55,
+ "standard_error": 0.049749371855330994,
+ "margin_of_error": 0.08183043473479865,
+ "confidence_interval_prop": [
+ 0.46816956526520137,
+ 0.6318304347347987
+ ],
+ "confidence_interval_count": [
+ 47,
+ 63
+ ]
+ },
+ {
+ "failure_count": 56,
+ "sample_size": 100,
+ "proportion": 0.56,
+ "standard_error": 0.04963869458396342,
+ "margin_of_error": 0.0816483868235686,
+ "confidence_interval_prop": [
+ 0.47835161317643143,
+ 0.6416483868235686
+ ],
+ "confidence_interval_count": [
+ 48,
+ 64
+ ]
+ },
+ {
+ "failure_count": 57,
+ "sample_size": 100,
+ "proportion": 0.57,
+ "standard_error": 0.04950757517794625,
+ "margin_of_error": 0.08143271459301754,
+ "confidence_interval_prop": [
+ 0.4885672854069824,
+ 0.6514327145930174
+ ],
+ "confidence_interval_count": [
+ 49,
+ 65
+ ]
+ },
+ {
+ "failure_count": 58,
+ "sample_size": 100,
+ "proportion": 0.58,
+ "standard_error": 0.04935585071701227,
+ "margin_of_error": 0.08118315006315302,
+ "confidence_interval_prop": [
+ 0.49881684993684694,
+ 0.661183150063153
+ ],
+ "confidence_interval_count": [
+ 50,
+ 66
+ ]
+ },
+ {
+ "failure_count": 59,
+ "sample_size": 100,
+ "proportion": 0.59,
+ "standard_error": 0.04918333050943175,
+ "margin_of_error": 0.08089937957399178,
+ "confidence_interval_prop": [
+ 0.5091006204260082,
+ 0.6708993795739917
+ ],
+ "confidence_interval_count": [
+ 51,
+ 67
+ ]
+ },
+ {
+ "failure_count": 60,
+ "sample_size": 100,
+ "proportion": 0.6,
+ "standard_error": 0.04898979485566356,
+ "margin_of_error": 0.08058104175194675,
+ "confidence_interval_prop": [
+ 0.5194189582480533,
+ 0.6805810417519467
+ ],
+ "confidence_interval_count": [
+ 52,
+ 68
+ ]
+ },
+ {
+ "failure_count": 61,
+ "sample_size": 100,
+ "proportion": 0.61,
+ "standard_error": 0.048774993593028795,
+ "margin_of_error": 0.0802277251160282,
+ "confidence_interval_prop": [
+ 0.5297722748839718,
+ 0.6902277251160281
+ ],
+ "confidence_interval_count": [
+ 53,
+ 69
+ ]
+ },
+ {
+ "failure_count": 62,
+ "sample_size": 100,
+ "proportion": 0.62,
+ "standard_error": 0.048538644398046386,
+ "margin_of_error": 0.07983896528543433,
+ "confidence_interval_prop": [
+ 0.5401610347145657,
+ 0.6998389652854343
+ ],
+ "confidence_interval_count": [
+ 55,
+ 69
+ ]
+ },
+ {
+ "failure_count": 63,
+ "sample_size": 100,
+ "proportion": 0.63,
+ "standard_error": 0.048280430818293245,
+ "margin_of_error": 0.07941424174224924,
+ "confidence_interval_prop": [
+ 0.5505857582577508,
+ 0.7094142417422492
+ ],
+ "confidence_interval_count": [
+ 56,
+ 70
+ ]
+ },
+ {
+ "failure_count": 64,
+ "sample_size": 100,
+ "proportion": 0.64,
+ "standard_error": 0.048,
+ "margin_of_error": 0.07895297409367064,
+ "confidence_interval_prop": [
+ 0.5610470259063294,
+ 0.7189529740936706
+ ],
+ "confidence_interval_count": [
+ 57,
+ 71
+ ]
+ },
+ {
+ "failure_count": 65,
+ "sample_size": 100,
+ "proportion": 0.65,
+ "standard_error": 0.047696960070847276,
+ "margin_of_error": 0.07845451776709265,
+ "confidence_interval_prop": [
+ 0.5715454822329074,
+ 0.7284545177670927
+ ],
+ "confidence_interval_count": [
+ 58,
+ 72
+ ]
+ },
+ {
+ "failure_count": 66,
+ "sample_size": 100,
+ "proportion": 0.66,
+ "standard_error": 0.04737087712930804,
+ "margin_of_error": 0.07791815905801484,
+ "confidence_interval_prop": [
+ 0.5820818409419852,
+ 0.7379181590580148
+ ],
+ "confidence_interval_count": [
+ 59,
+ 73
+ ]
+ },
+ {
+ "failure_count": 67,
+ "sample_size": 100,
+ "proportion": 0.67,
+ "standard_error": 0.04702127178203499,
+ "margin_of_error": 0.07734310943455114,
+ "confidence_interval_prop": [
+ 0.5926568905654489,
+ 0.7473431094345512
+ ],
+ "confidence_interval_count": [
+ 60,
+ 74
+ ]
+ },
+ {
+ "failure_count": 68,
+ "sample_size": 100,
+ "proportion": 0.68,
+ "standard_error": 0.0466476151587624,
+ "margin_of_error": 0.07672849898252677,
+ "confidence_interval_prop": [
+ 0.6032715010174733,
+ 0.7567284989825268
+ ],
+ "confidence_interval_count": [
+ 61,
+ 75
+ ]
+ },
+ {
+ "failure_count": 69,
+ "sample_size": 100,
+ "proportion": 0.69,
+ "standard_error": 0.04624932431938871,
+ "margin_of_error": 0.07607336885080142,
+ "confidence_interval_prop": [
+ 0.6139266311491985,
+ 0.7660733688508014
+ ],
+ "confidence_interval_count": [
+ 62,
+ 76
+ ]
+ },
+ {
+ "failure_count": 70,
+ "sample_size": 100,
+ "proportion": 0.7,
+ "standard_error": 0.045825756949558406,
+ "margin_of_error": 0.07537666252627774,
+ "confidence_interval_prop": [
+ 0.6246233374737222,
+ 0.7753766625262777
+ ],
+ "confidence_interval_count": [
+ 63,
+ 77
+ ]
+ },
+ {
+ "failure_count": 71,
+ "sample_size": 100,
+ "proportion": 0.71,
+ "standard_error": 0.04537620521815371,
+ "margin_of_error": 0.07463721573037442,
+ "confidence_interval_prop": [
+ 0.6353627842696256,
+ 0.7846372157303744
+ ],
+ "confidence_interval_count": [
+ 64,
+ 78
+ ]
+ },
+ {
+ "failure_count": 72,
+ "sample_size": 100,
+ "proportion": 0.72,
+ "standard_error": 0.0448998886412873,
+ "margin_of_error": 0.0738537446813386,
+ "confidence_interval_prop": [
+ 0.6461462553186614,
+ 0.7938537446813385
+ ],
+ "confidence_interval_count": [
+ 65,
+ 79
+ ]
+ },
+ {
+ "failure_count": 73,
+ "sample_size": 100,
+ "proportion": 0.73,
+ "standard_error": 0.044395945760846225,
+ "margin_of_error": 0.07302483240666872,
+ "confidence_interval_prop": [
+ 0.6569751675933313,
+ 0.8030248324066687
+ ],
+ "confidence_interval_count": [
+ 66,
+ 80
+ ]
+ },
+ {
+ "failure_count": 74,
+ "sample_size": 100,
+ "proportion": 0.74,
+ "standard_error": 0.04386342439892262,
+ "margin_of_error": 0.07214891271307955,
+ "confidence_interval_prop": [
+ 0.6678510872869204,
+ 0.8121489127130795
+ ],
+ "confidence_interval_count": [
+ 67,
+ 81
+ ]
+ },
+ {
+ "failure_count": 75,
+ "sample_size": 100,
+ "proportion": 0.75,
+ "standard_error": 0.04330127018922193,
+ "margin_of_error": 0.07122425132234733,
+ "confidence_interval_prop": [
+ 0.6787757486776527,
+ 0.8212242513223473
+ ],
+ "confidence_interval_count": [
+ 68,
+ 82
+ ]
+ },
+ {
+ "failure_count": 76,
+ "sample_size": 100,
+ "proportion": 0.76,
+ "standard_error": 0.04270831300812525,
+ "margin_of_error": 0.07024892355239352,
+ "confidence_interval_prop": [
+ 0.6897510764476065,
+ 0.8302489235523935
+ ],
+ "confidence_interval_count": [
+ 69,
+ 83
+ ]
+ },
+ {
+ "failure_count": 77,
+ "sample_size": 100,
+ "proportion": 0.77,
+ "standard_error": 0.042083250825001625,
+ "margin_of_error": 0.06922078775341244,
+ "confidence_interval_prop": [
+ 0.7007792122465876,
+ 0.8392207877534125
+ ],
+ "confidence_interval_count": [
+ 71,
+ 83
+ ]
+ },
+ {
+ "failure_count": 78,
+ "sample_size": 100,
+ "proportion": 0.78,
+ "standard_error": 0.04142463035441595,
+ "margin_of_error": 0.0681374534835851,
+ "confidence_interval_prop": [
+ 0.7118625465164149,
+ 0.8481374534835852
+ ],
+ "confidence_interval_count": [
+ 72,
+ 84
+ ]
+ },
+ {
+ "failure_count": 79,
+ "sample_size": 100,
+ "proportion": 0.79,
+ "standard_error": 0.0407308237088326,
+ "margin_of_error": 0.0669962431061943,
+ "confidence_interval_prop": [
+ 0.7230037568938057,
+ 0.8569962431061944
+ ],
+ "confidence_interval_count": [
+ 73,
+ 85
+ ]
+ },
+ {
+ "failure_count": 80,
+ "sample_size": 100,
+ "proportion": 0.8,
+ "standard_error": 0.04,
+ "margin_of_error": 0.06579414507805886,
+ "confidence_interval_prop": [
+ 0.7342058549219412,
+ 0.8657941450780589
+ ],
+ "confidence_interval_count": [
+ 74,
+ 86
+ ]
+ },
+ {
+ "failure_count": 81,
+ "sample_size": 100,
+ "proportion": 0.81,
+ "standard_error": 0.03923009049186606,
+ "margin_of_error": 0.06452775663118032,
+ "confidence_interval_prop": [
+ 0.7454722433688197,
+ 0.8745277566311804
+ ],
+ "confidence_interval_count": [
+ 75,
+ 87
+ ]
+ },
+ {
+ "failure_count": 82,
+ "sample_size": 100,
+ "proportion": 0.82,
+ "standard_error": 0.0384187454245971,
+ "margin_of_error": 0.06319321275457379,
+ "confidence_interval_prop": [
+ 0.7568067872454262,
+ 0.8831932127545737
+ ],
+ "confidence_interval_count": [
+ 76,
+ 88
+ ]
+ },
+ {
+ "failure_count": 83,
+ "sample_size": 100,
+ "proportion": 0.83,
+ "standard_error": 0.037563279941985904,
+ "margin_of_error": 0.06178609725276898,
+ "confidence_interval_prop": [
+ 0.768213902747231,
+ 0.8917860972527689
+ ],
+ "confidence_interval_count": [
+ 77,
+ 89
+ ]
+ },
+ {
+ "failure_count": 84,
+ "sample_size": 100,
+ "proportion": 0.84,
+ "standard_error": 0.036660605559646724,
+ "margin_of_error": 0.0603013300210222,
+ "confidence_interval_prop": [
+ 0.7796986699789777,
+ 0.9003013300210222
+ ],
+ "confidence_interval_count": [
+ 78,
+ 90
+ ]
+ },
+ {
+ "failure_count": 85,
+ "sample_size": 100,
+ "proportion": 0.85,
+ "standard_error": 0.035707142142714254,
+ "margin_of_error": 0.05873302226151528,
+ "confidence_interval_prop": [
+ 0.7912669777384846,
+ 0.9087330222615153
+ ],
+ "confidence_interval_count": [
+ 80,
+ 90
+ ]
+ },
+ {
+ "failure_count": 86,
+ "sample_size": 100,
+ "proportion": 0.86,
+ "standard_error": 0.03469870314579494,
+ "margin_of_error": 0.057074287719873246,
+ "confidence_interval_prop": [
+ 0.8029257122801268,
+ 0.9170742877198732
+ ],
+ "confidence_interval_count": [
+ 81,
+ 91
+ ]
+ },
+ {
+ "failure_count": 87,
+ "sample_size": 100,
+ "proportion": 0.87,
+ "standard_error": 0.03363034344160047,
+ "margin_of_error": 0.05531699238554017,
+ "confidence_interval_prop": [
+ 0.8146830076144598,
+ 0.9253169923855402
+ ],
+ "confidence_interval_count": [
+ 82,
+ 92
+ ]
+ },
+ {
+ "failure_count": 88,
+ "sample_size": 100,
+ "proportion": 0.88,
+ "standard_error": 0.03249615361854384,
+ "margin_of_error": 0.053451416141434026,
+ "confidence_interval_prop": [
+ 0.826548583858566,
+ 0.933451416141434
+ ],
+ "confidence_interval_count": [
+ 83,
+ 93
+ ]
+ },
+ {
+ "failure_count": 89,
+ "sample_size": 100,
+ "proportion": 0.89,
+ "standard_error": 0.031288975694324025,
+ "margin_of_error": 0.05146578515440531,
+ "confidence_interval_prop": [
+ 0.8385342148455948,
+ 0.9414657851544053
+ ],
+ "confidence_interval_count": [
+ 84,
+ 94
+ ]
+ },
+ {
+ "failure_count": 90,
+ "sample_size": 100,
+ "proportion": 0.9,
+ "standard_error": 0.03,
+ "margin_of_error": 0.04934560880854414,
+ "confidence_interval_prop": [
+ 0.8506543911914559,
+ 0.9493456088085441
+ ],
+ "confidence_interval_count": [
+ 86,
+ 94
+ ]
+ },
+ {
+ "failure_count": 91,
+ "sample_size": 100,
+ "proportion": 0.91,
+ "standard_error": 0.028618176042508364,
+ "margin_of_error": 0.04707271066025559,
+ "confidence_interval_prop": [
+ 0.8629272893397444,
+ 0.9570727106602557
+ ],
+ "confidence_interval_count": [
+ 87,
+ 95
+ ]
+ },
+ {
+ "failure_count": 92,
+ "sample_size": 100,
+ "proportion": 0.92,
+ "standard_error": 0.027129319932501065,
+ "margin_of_error": 0.04462376028770123,
+ "confidence_interval_prop": [
+ 0.8753762397122988,
+ 0.9646237602877012
+ ],
+ "confidence_interval_count": [
+ 88,
+ 96
+ ]
+ },
+ {
+ "failure_count": 93,
+ "sample_size": 100,
+ "proportion": 0.93,
+ "standard_error": 0.02551470164434614,
+ "margin_of_error": 0.04196794954028742,
+ "confidence_interval_prop": [
+ 0.8880320504597127,
+ 0.9719679495402874
+ ],
+ "confidence_interval_count": [
+ 89,
+ 97
+ ]
+ },
+ {
+ "failure_count": 94,
+ "sample_size": 100,
+ "proportion": 0.94,
+ "standard_error": 0.023748684174075843,
+ "margin_of_error": 0.03906310929905366,
+ "confidence_interval_prop": [
+ 0.9009368907009463,
+ 0.9790631092990536
+ ],
+ "confidence_interval_count": [
+ 91,
+ 97
+ ]
+ },
+ {
+ "failure_count": 95,
+ "sample_size": 100,
+ "proportion": 0.95,
+ "standard_error": 0.021794494717703377,
+ "margin_of_error": 0.035848753683989085,
+ "confidence_interval_prop": [
+ 0.9141512463160109,
+ 0.985848753683989
+ ],
+ "confidence_interval_count": [
+ 92,
+ 98
+ ]
+ },
+ {
+ "failure_count": 96,
+ "sample_size": 100,
+ "proportion": 0.96,
+ "standard_error": 0.019595917942265433,
+ "margin_of_error": 0.03223241670077871,
+ "confidence_interval_prop": [
+ 0.9277675832992213,
+ 0.9922324167007787
+ ],
+ "confidence_interval_count": [
+ 93,
+ 99
+ ]
+ },
+ {
+ "failure_count": 97,
+ "sample_size": 100,
+ "proportion": 0.97,
+ "standard_error": 0.017058722109231986,
+ "margin_of_error": 0.02805910093252749,
+ "confidence_interval_prop": [
+ 0.9419408990674725,
+ 0.9980591009325275
+ ],
+ "confidence_interval_count": [
+ 95,
+ 99
+ ]
+ },
+ {
+ "failure_count": 98,
+ "sample_size": 100,
+ "proportion": 0.98,
+ "standard_error": 0.014000000000000005,
+ "margin_of_error": 0.02302795077732061,
+ "confidence_interval_prop": [
+ 0.9569720492226794,
+ 1.0030279507773205
+ ],
+ "confidence_interval_count": [
+ 96,
+ 100
+ ]
+ },
+ {
+ "failure_count": 99,
+ "sample_size": 100,
+ "proportion": 0.99,
+ "standard_error": 0.009949874371066205,
+ "margin_of_error": 0.016366086946959738,
+ "confidence_interval_prop": [
+ 0.9736339130530403,
+ 1.0063660869469597
+ ],
+ "confidence_interval_count": [
+ 98,
+ 100
+ ]
+ },
+ {
+ "failure_count": 100,
+ "sample_size": 100,
+ "proportion": 1.0,
+ "standard_error": 0.0,
+ "margin_of_error": 0.0,
+ "confidence_interval_prop": [
+ 1.0,
+ 1.0
+ ],
+ "confidence_interval_count": [
+ 100,
+ 100
+ ]
+ }
+]
\ No newline at end of file
diff --git a/tests/snapshots/test_statistical_analysis/test_failure_rate_graph/failure_rate_graph.png b/tests/snapshots/test_statistical_analysis/test_failure_rate_graph/failure_rate_graph.png
new file mode 100644
index 0000000..44f1a59
Binary files /dev/null and b/tests/snapshots/test_statistical_analysis/test_failure_rate_graph/failure_rate_graph.png differ
diff --git a/tests/snapshots/test_statistical_analysis/test_failure_rate_graph/failure_rate_graph.svg b/tests/snapshots/test_statistical_analysis/test_failure_rate_graph/failure_rate_graph.svg
new file mode 100644
index 0000000..1c7bbc4
--- /dev/null
+++ b/tests/snapshots/test_statistical_analysis/test_failure_rate_graph/failure_rate_graph.svg
@@ -0,0 +1,1500 @@
+
+
+
diff --git a/tests/test_reporter.py b/tests/test_reporter.py
index 727a108..56a7d03 100644
--- a/tests/test_reporter.py
+++ b/tests/test_reporter.py
@@ -1,6 +1,8 @@
import json
import time
from unittest.mock import mock_open, patch, MagicMock
+
+from cat_ai import analyse_sample_from_test
from src.cat_ai.reporter import Reporter
from src.cat_ai.helpers.helpers import root_dir
@@ -16,8 +18,8 @@ def test_reporter_creates_a_unique_folder_path() -> None:
def test_reporter_can_accept_unique_id_override() -> None:
- test_name = "id_override"
- unique_id = "some_string"
+ test_name = "example_test"
+ unique_id = "timestamp_or_any_unique_id"
reporter1 = Reporter(test_name=test_name, output_dir=root_dir(), unique_id=unique_id)
expected_dir_path = f"{root_dir()}/test_runs/{test_name}-{unique_id}"
assert str(expected_dir_path) == str(reporter1.folder_path)
@@ -50,13 +52,15 @@ def test_report_creates_correct_json(mock_open: MagicMock, mock_makedirs: MagicM
mock_open().write.assert_called_with(expected_json_string)
-def test_margin_of_error():
- notice = Reporter.error_margin_summary(6, 100)
- assert notice == ('> [!NOTE]\n'
- '> ### There are 6 failures out of 100 generations.\n'
- '> Sample Proportion (p̂): 0.0600\n'
- '> Standard Error (SE): 0.023749\n'
- '> Margin of Error (ME): 0.039067\n'
- '> 90% Confidence Interval: [0.020933, 0.099067]\n'
- '> 90% Confidence Interval (Count): [3, 9]'
- )
+
+def test_format_summary():
+ analysis = analyse_sample_from_test(6, 100)
+ assert Reporter.format_summary(analysis) == (
+ "> [!NOTE]\n"
+ "> ### There are 6 failures out of 100 generations.\n"
+ "> Sample Proportion (p̂): 0.0600\n"
+ "> Standard Error (SE): 0.023749\n"
+ "> Margin of Error (ME): 0.039063\n"
+ "> 90% Confidence Interval: [0.020937, 0.099063]\n"
+ "> 90% Confidence Interval (Count): [3, 9]"
+ )
diff --git a/tests/test_statistical_analysis.py b/tests/test_statistical_analysis.py
new file mode 100644
index 0000000..672e216
--- /dev/null
+++ b/tests/test_statistical_analysis.py
@@ -0,0 +1,195 @@
+import csv
+import io
+import os
+
+import pytest
+from statistics import NormalDist
+import math
+import matplotlib.pyplot as plt
+import numpy as np
+
+from cat_ai.statistical_analysis import analyse_sample_from_test, StatisticalAnalysis
+
+
+@pytest.mark.parametrize(
+ "failure_count,sample_size,expected_proportion",
+ [
+ (0, 100, 0.0),
+ (6, 100, 0.06),
+ (100, 100, 1.0),
+ ],
+)
+def test_analyse_sample_from_test(failure_count, sample_size, expected_proportion):
+ """Test the statistical analysis function with various edge cases."""
+ result = analyse_sample_from_test(failure_count, sample_size)
+
+ # Basic assertions
+ assert result.failure_count == failure_count
+ assert result.sample_size == sample_size
+ assert result.proportion == expected_proportion
+
+ # Calculate expected values for validation
+ p_hat = failure_count / sample_size
+ z = NormalDist().inv_cdf(0.95) # 95th percentile for 90% CI
+ expected_se = math.sqrt(p_hat * (1 - p_hat) / sample_size) if p_hat * (1 - p_hat) > 0 else 0
+ expected_me = z * expected_se
+
+ # Validate calculations
+ assert result.standard_error == pytest.approx(expected_se)
+ assert result.margin_of_error == pytest.approx(expected_me)
+
+ # Check confidence interval bounds
+ expected_lower = max(0.0, p_hat - expected_me)
+ expected_upper = min(1.0, p_hat + expected_me)
+ assert result.confidence_interval_prop[0] == pytest.approx(expected_lower)
+ assert result.confidence_interval_prop[1] == pytest.approx(expected_upper)
+
+ # Validate integer confidence bounds
+ expected_lower_count = math.ceil(expected_lower * sample_size)
+ expected_upper_count = int(expected_upper * sample_size)
+ assert result.confidence_interval_count[0] == expected_lower_count
+ assert result.confidence_interval_count[1] == expected_upper_count
+
+ # Test boundary conditions
+ if failure_count == 0:
+ assert result.confidence_interval_prop[0] == 0.0
+ if failure_count == sample_size:
+ assert result.confidence_interval_prop[1] == 1.0
+
+
+@pytest.mark.parametrize(
+ "failures, total, expected_error, expected_ci",
+ [
+ (0, 100, 0.0, (0, 0)),
+ (6, 100, 0.023748684174075833, (3, 9)),
+ (50, 100, 0.05, (42, 58)),
+ (95, 100, 0.021794494717703377, (92, 98)),
+ (100, 100, 0.0, (100, 100)),
+ ],
+)
+def test_edges_cases(failures, total, expected_error, expected_ci):
+ result = analyse_sample_from_test(failures, total)
+ assert result.standard_error == expected_error
+ assert result.confidence_interval_count == expected_ci
+
+
+def export_results_to_csv_string(results: list[StatisticalAnalysis]) -> str:
+ """Export a list of StatisticalAnalysis objects to a CSV-formatted string."""
+ # Create a CSV writer with MacOS-style newlines to match the snapshot
+ output = io.StringIO(newline="\n") # Let CSV writer handle newline translation
+ writer = csv.writer(output, lineterminator="\n") # Explicitly set line terminator
+
+ # Write header
+ writer.writerow(StatisticalAnalysis.get_csv_headers())
+
+ # Write rows
+ for result in results:
+ writer.writerow(result.as_csv_row())
+
+ return output.getvalue()
+
+
+def test_failure_rate_bar_graph(snapshot):
+ # Sample data points - choosing strategic values to test boundary conditions
+ failure_counts = list(range(101))
+ assert failure_counts[0] == 0
+ assert failure_counts[100] == 100
+ sample_size = 100
+
+ # Calculate results for each data point
+ results = [analyse_sample_from_test(f, sample_size) for f in failure_counts]
+ csv = export_results_to_csv_string(results)
+ csv_bytes = io.BytesIO(csv.encode("utf-8"))
+ snapshot.assert_match(csv_bytes.getvalue(), "failure_rate_results.csv")
+ rates = [r.proportion for r in results]
+ errors = [r.margin_of_error for r in results]
+
+ # Create the bar plot
+ fig, ax = plt.subplots(figsize=(10, 6))
+
+ # Plot bars with error bars
+ bars = ax.bar(
+ failure_counts, rates, yerr=errors, capsize=5, color="steelblue", alpha=0.7, width=8
+ )
+
+ # # Add annotations on top of each bar
+ # for bar, rate, error in zip(bars, rates, errors):
+ # height = bar.get_height()
+ # ax.text(
+ # bar.get_x() + bar.get_width() / 2.0,
+ # height + error + 0.01,
+ # f"{rate:.2f}±{error:.2f}",
+ # ha="center",
+ # va="bottom",
+ # rotation=0,
+ # fontsize=9,
+ # )
+
+ # Add labels and title
+ ax.set_xlabel("Number of Failures")
+ ax.set_ylabel("Failure Rate")
+ ax.set_title("Failure Rate with Error Margins")
+ ax.set_ylim(0, 1.2) # Set y-axis to accommodate annotations
+ ax.grid(True, linestyle="--", alpha=0.7, axis="both")
+
+ # Deterministic rendering for snapshot testing
+ plt.tight_layout()
+ buf = io.BytesIO()
+ plt.rcParams["svg.hashsalt"] = "matplotlib"
+ os.environ["SOURCE_DATE_EPOCH"] = "1234567890"
+ fig.savefig(buf, format="svg")
+ buf.seek(0)
+
+ # Compare with snapshot
+ snapshot.assert_match(buf.read(), "failure_rate_bar_graph.svg")
+
+ plt.close()
+
+
+def test_failure_rate_graph(snapshot):
+ # Generate a series of failure rates
+ totals = np.ones(100) * 100
+ failures = np.arange(0, 100)
+
+ # Calculate results for each rate
+ results = [analyse_sample_from_test(f, t) for f, t in zip(failures, totals)]
+
+ # Extract data for plotting
+ rates = [r.proportion for r in results]
+ errors = [r.standard_error for r in results]
+ failing_errors = [e for e in errors if e > 0.05]
+ assert len(failing_errors) == 0, f"Errors exceeding threshold 0.05: {failing_errors}"
+ lower_bounds = [r.confidence_interval_prop[0] for r in results]
+ upper_bounds = [r.confidence_interval_prop[1] for r in results]
+
+ # Create the plot
+ fig, ax = plt.subplots(figsize=(10, 6))
+ x = np.arange(0, 100)
+
+ # Plot the rate line
+ ax.plot(x, rates, "b-", label="Failure Rate")
+
+ # Plot confidence interval as shaded region
+ ax.fill_between(
+ x, lower_bounds, upper_bounds, alpha=0.3, color="blue", label="90% Confidence Interval"
+ )
+
+ # Add labels and title
+ ax.set_xlabel("Number of Failures")
+ ax.set_ylabel("Failure Rate")
+ ax.set_title("Failure Rate with Confidence Intervals")
+ ax.legend()
+ ax.grid(True, linestyle="--", alpha=0.7)
+
+ # Save to buffer for snapshot comparison
+ plt.tight_layout()
+ buf = io.BytesIO()
+ plt.rcParams["svg.hashsalt"] = "matplotlib"
+ os.environ["SOURCE_DATE_EPOCH"] = "1234567890"
+ fig.savefig(buf, format="svg")
+ buf.seek(0)
+
+ # Compare with snapshot
+ snapshot.assert_match(buf.read(), "failure_rate_graph.svg")
+
+ plt.close()
diff --git a/uv.lock b/uv.lock
index a29680c..5304823 100644
--- a/uv.lock
+++ b/uv.lock
@@ -192,6 +192,7 @@ dev = [
{ name = "notebook" },
{ name = "pydantic" },
{ name = "pydrive2" },
+ { name = "ruff" },
{ name = "sphinx" },
{ name = "sphinx-markdown-builder" },
{ name = "sphinx-rtd-theme" },
@@ -202,9 +203,11 @@ examples = [
]
test = [
{ name = "black" },
+ { name = "matplotlib" },
{ name = "mypy" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
+ { name = "pytest-snapshot" },
]
[package.metadata]
@@ -214,6 +217,7 @@ dev = [
{ name = "notebook", specifier = ">=7.3.2" },
{ name = "pydantic", specifier = ">=2.10.6,<3" },
{ name = "pydrive2", specifier = ">=1.21.3,<2" },
+ { name = "ruff", specifier = ">=0.9.10" },
{ name = "sphinx", specifier = ">=8.1.3,<9" },
{ name = "sphinx-markdown-builder", specifier = ">=0.6.8,<0.7" },
{ name = "sphinx-rtd-theme", specifier = ">=3.0.2,<4" },
@@ -224,9 +228,11 @@ examples = [
]
test = [
{ name = "black", specifier = ">=24.2.0,<25" },
+ { name = "matplotlib", specifier = ">=3.10.1" },
{ name = "mypy", specifier = ">=1.8.0,<2" },
{ name = "pytest", specifier = ">=8.3.4,<9" },
{ name = "pytest-asyncio", specifier = ">=0.21.0,<0.22" },
+ { name = "pytest-snapshot", specifier = ">=0.9.0" },
]
[[package]]
@@ -315,6 +321,37 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/75/49e5bfe642f71f272236b5b2d2691cf915a7283cc0ceda56357b61daa538/comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3", size = 7180 },
]
+[[package]]
+name = "contourpy"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/25/c2/fc7193cc5383637ff390a712e88e4ded0452c9fbcf84abe3de5ea3df1866/contourpy-1.3.1.tar.gz", hash = "sha256:dfd97abd83335045a913e3bcc4a09c0ceadbe66580cf573fe961f4a825efa699", size = 13465753 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9a/e7/de62050dce687c5e96f946a93546910bc67e483fe05324439e329ff36105/contourpy-1.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a761d9ccfc5e2ecd1bf05534eda382aa14c3e4f9205ba5b1684ecfe400716ef2", size = 271548 },
+ { url = "https://files.pythonhosted.org/packages/78/4d/c2a09ae014ae984c6bdd29c11e74d3121b25eaa117eca0bb76340efd7e1c/contourpy-1.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:523a8ee12edfa36f6d2a49407f705a6ef4c5098de4f498619787e272de93f2d5", size = 255576 },
+ { url = "https://files.pythonhosted.org/packages/ab/8a/915380ee96a5638bda80cd061ccb8e666bfdccea38d5741cb69e6dbd61fc/contourpy-1.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece6df05e2c41bd46776fbc712e0996f7c94e0d0543af1656956d150c4ca7c81", size = 306635 },
+ { url = "https://files.pythonhosted.org/packages/29/5c/c83ce09375428298acd4e6582aeb68b1e0d1447f877fa993d9bf6cd3b0a0/contourpy-1.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:573abb30e0e05bf31ed067d2f82500ecfdaec15627a59d63ea2d95714790f5c2", size = 345925 },
+ { url = "https://files.pythonhosted.org/packages/29/63/5b52f4a15e80c66c8078a641a3bfacd6e07106835682454647aca1afc852/contourpy-1.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fa36448e6a3a1a9a2ba23c02012c43ed88905ec80163f2ffe2421c7192a5d7", size = 318000 },
+ { url = "https://files.pythonhosted.org/packages/9a/e2/30ca086c692691129849198659bf0556d72a757fe2769eb9620a27169296/contourpy-1.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ea9924d28fc5586bf0b42d15f590b10c224117e74409dd7a0be3b62b74a501c", size = 322689 },
+ { url = "https://files.pythonhosted.org/packages/6b/77/f37812ef700f1f185d348394debf33f22d531e714cf6a35d13d68a7003c7/contourpy-1.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b75aa69cb4d6f137b36f7eb2ace9280cfb60c55dc5f61c731fdf6f037f958a3", size = 1268413 },
+ { url = "https://files.pythonhosted.org/packages/3f/6d/ce84e79cdd128542ebeb268f84abb4b093af78e7f8ec504676673d2675bc/contourpy-1.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:041b640d4ec01922083645a94bb3b2e777e6b626788f4095cf21abbe266413c1", size = 1326530 },
+ { url = "https://files.pythonhosted.org/packages/72/22/8282f4eae20c73c89bee7a82a19c4e27af9b57bb602ecaa00713d5bdb54d/contourpy-1.3.1-cp313-cp313-win32.whl", hash = "sha256:36987a15e8ace5f58d4d5da9dca82d498c2bbb28dff6e5d04fbfcc35a9cb3a82", size = 175315 },
+ { url = "https://files.pythonhosted.org/packages/e3/d5/28bca491f65312b438fbf076589dcde7f6f966b196d900777f5811b9c4e2/contourpy-1.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:a7895f46d47671fa7ceec40f31fae721da51ad34bdca0bee83e38870b1f47ffd", size = 220987 },
+ { url = "https://files.pythonhosted.org/packages/2f/24/a4b285d6adaaf9746e4700932f579f1a7b6f9681109f694cfa233ae75c4e/contourpy-1.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9ddeb796389dadcd884c7eb07bd14ef12408aaae358f0e2ae24114d797eede30", size = 285001 },
+ { url = "https://files.pythonhosted.org/packages/48/1d/fb49a401b5ca4f06ccf467cd6c4f1fd65767e63c21322b29b04ec40b40b9/contourpy-1.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19c1555a6801c2f084c7ddc1c6e11f02eb6a6016ca1318dd5452ba3f613a1751", size = 268553 },
+ { url = "https://files.pythonhosted.org/packages/79/1e/4aef9470d13fd029087388fae750dccb49a50c012a6c8d1d634295caa644/contourpy-1.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:841ad858cff65c2c04bf93875e384ccb82b654574a6d7f30453a04f04af71342", size = 310386 },
+ { url = "https://files.pythonhosted.org/packages/b0/34/910dc706ed70153b60392b5305c708c9810d425bde12499c9184a1100888/contourpy-1.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4318af1c925fb9a4fb190559ef3eec206845f63e80fb603d47f2d6d67683901c", size = 349806 },
+ { url = "https://files.pythonhosted.org/packages/31/3c/faee6a40d66d7f2a87f7102236bf4780c57990dd7f98e5ff29881b1b1344/contourpy-1.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:14c102b0eab282427b662cb590f2e9340a9d91a1c297f48729431f2dcd16e14f", size = 321108 },
+ { url = "https://files.pythonhosted.org/packages/17/69/390dc9b20dd4bb20585651d7316cc3054b7d4a7b4f8b710b2b698e08968d/contourpy-1.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05e806338bfeaa006acbdeba0ad681a10be63b26e1b17317bfac3c5d98f36cda", size = 327291 },
+ { url = "https://files.pythonhosted.org/packages/ef/74/7030b67c4e941fe1e5424a3d988080e83568030ce0355f7c9fc556455b01/contourpy-1.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4d76d5993a34ef3df5181ba3c92fabb93f1eaa5729504fb03423fcd9f3177242", size = 1263752 },
+ { url = "https://files.pythonhosted.org/packages/f0/ed/92d86f183a8615f13f6b9cbfc5d4298a509d6ce433432e21da838b4b63f4/contourpy-1.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:89785bb2a1980c1bd87f0cb1517a71cde374776a5f150936b82580ae6ead44a1", size = 1318403 },
+ { url = "https://files.pythonhosted.org/packages/b3/0e/c8e4950c77dcfc897c71d61e56690a0a9df39543d2164040301b5df8e67b/contourpy-1.3.1-cp313-cp313t-win32.whl", hash = "sha256:8eb96e79b9f3dcadbad2a3891672f81cdcab7f95b27f28f1c67d75f045b6b4f1", size = 185117 },
+ { url = "https://files.pythonhosted.org/packages/c1/31/1ae946f11dfbd229222e6d6ad8e7bd1891d3d48bde5fbf7a0beb9491f8e3/contourpy-1.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:287ccc248c9e0d0566934e7d606201abd74761b5703d804ff3df8935f523d546", size = 236668 },
+]
+
[[package]]
name = "cryptography"
version = "43.0.3"
@@ -344,6 +381,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/87/5c/3dab83cc4aba1f4b0e733e3f0c3e7d4386440d660ba5b1e3ff995feb734d/cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362", size = 3068026 },
]
+[[package]]
+name = "cycler"
+version = "0.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321 },
+]
+
[[package]]
name = "debugpy"
version = "1.8.12"
@@ -411,6 +457,23 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/90/2b/0817a2b257fe88725c25589d89aec060581aabf668707a8d03b2e9e0cb2a/fastjsonschema-2.21.1-py3-none-any.whl", hash = "sha256:c9e5b7e908310918cf494a434eeb31384dd84a98b57a30bcb1f535015b554667", size = 23924 },
]
+[[package]]
+name = "fonttools"
+version = "4.56.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1c/8c/9ffa2a555af0e5e5d0e2ed7fdd8c9bef474ed676995bb4c57c9cd0014248/fonttools-4.56.0.tar.gz", hash = "sha256:a114d1567e1a1586b7e9e7fc2ff686ca542a82769a296cef131e4c4af51e58f4", size = 3462892 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a5/55/f06b48d48e0b4ec3a3489efafe9bd4d81b6e0802ac51026e3ee4634e89ba/fonttools-4.56.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f20e2c0dfab82983a90f3d00703ac0960412036153e5023eed2b4641d7d5e692", size = 2735127 },
+ { url = "https://files.pythonhosted.org/packages/59/db/d2c7c9b6dd5cbd46f183e650a47403ffb88fca17484eb7c4b1cd88f9e513/fonttools-4.56.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f36a0868f47b7566237640c026c65a86d09a3d9ca5df1cd039e30a1da73098a0", size = 2272519 },
+ { url = "https://files.pythonhosted.org/packages/4d/a2/da62d779c34a0e0c06415f02eab7fa3466de5d46df459c0275a255cefc65/fonttools-4.56.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62b4c6802fa28e14dba010e75190e0e6228513573f1eeae57b11aa1a39b7e5b1", size = 4762423 },
+ { url = "https://files.pythonhosted.org/packages/be/6a/fd4018e0448c8a5e12138906411282c5eab51a598493f080a9f0960e658f/fonttools-4.56.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a05d1f07eb0a7d755fbe01fee1fd255c3a4d3730130cf1bfefb682d18fd2fcea", size = 4834442 },
+ { url = "https://files.pythonhosted.org/packages/6d/63/fa1dec8efb35bc11ef9c39b2d74754b45d48a3ccb2cf78c0109c0af639e8/fonttools-4.56.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0073b62c3438cf0058488c002ea90489e8801d3a7af5ce5f7c05c105bee815c3", size = 4742800 },
+ { url = "https://files.pythonhosted.org/packages/dd/f4/963247ae8c73ccc4cf2929e7162f595c81dbe17997d1d0ea77da24a217c9/fonttools-4.56.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cad98c94833465bcf28f51c248aaf07ca022efc6a3eba750ad9c1e0256d278", size = 4963746 },
+ { url = "https://files.pythonhosted.org/packages/ea/e0/46f9600c39c644b54e4420f941f75fa200d9288c9ae171e5d80918b8cbb9/fonttools-4.56.0-cp313-cp313-win32.whl", hash = "sha256:d0cb73ccf7f6d7ca8d0bc7ea8ac0a5b84969a41c56ac3ac3422a24df2680546f", size = 2140927 },
+ { url = "https://files.pythonhosted.org/packages/27/6d/3edda54f98a550a0473f032d8050315fbc8f1b76a0d9f3879b72ebb2cdd6/fonttools-4.56.0-cp313-cp313-win_amd64.whl", hash = "sha256:62cc1253827d1e500fde9dbe981219fea4eb000fd63402283472d38e7d8aa1c6", size = 2186709 },
+ { url = "https://files.pythonhosted.org/packages/bf/ff/44934a031ce5a39125415eb405b9efb76fe7f9586b75291d66ae5cbfc4e6/fonttools-4.56.0-py3-none-any.whl", hash = "sha256:1088182f68c303b50ca4dc0c82d42083d176cba37af1937e1a976a31149d4d14", size = 1089800 },
+]
+
[[package]]
name = "fqdn"
version = "1.5.1"
@@ -895,6 +958,42 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/09/2032e7d15c544a0e3cd831c51d77a8ca57f7555b2e1b2922142eddb02a84/jupyterlab_server-2.27.3-py3-none-any.whl", hash = "sha256:e697488f66c3db49df675158a77b3b017520d772c6e1548c7d9bcc5df7944ee4", size = 59700 },
]
+[[package]]
+name = "kiwisolver"
+version = "1.4.8"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/79/b3/e62464a652f4f8cd9006e13d07abad844a47df1e6537f73ddfbf1bc997ec/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09", size = 124156 },
+ { url = "https://files.pythonhosted.org/packages/8d/2d/f13d06998b546a2ad4f48607a146e045bbe48030774de29f90bdc573df15/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1", size = 66555 },
+ { url = "https://files.pythonhosted.org/packages/59/e3/b8bd14b0a54998a9fd1e8da591c60998dc003618cb19a3f94cb233ec1511/kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c", size = 65071 },
+ { url = "https://files.pythonhosted.org/packages/f0/1c/6c86f6d85ffe4d0ce04228d976f00674f1df5dc893bf2dd4f1928748f187/kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b", size = 1378053 },
+ { url = "https://files.pythonhosted.org/packages/4e/b9/1c6e9f6dcb103ac5cf87cb695845f5fa71379021500153566d8a8a9fc291/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47", size = 1472278 },
+ { url = "https://files.pythonhosted.org/packages/ee/81/aca1eb176de671f8bda479b11acdc42c132b61a2ac861c883907dde6debb/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16", size = 1478139 },
+ { url = "https://files.pythonhosted.org/packages/49/f4/e081522473671c97b2687d380e9e4c26f748a86363ce5af48b4a28e48d06/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc", size = 1413517 },
+ { url = "https://files.pythonhosted.org/packages/8f/e9/6a7d025d8da8c4931522922cd706105aa32b3291d1add8c5427cdcd66e63/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246", size = 1474952 },
+ { url = "https://files.pythonhosted.org/packages/82/13/13fa685ae167bee5d94b415991c4fc7bb0a1b6ebea6e753a87044b209678/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794", size = 2269132 },
+ { url = "https://files.pythonhosted.org/packages/ef/92/bb7c9395489b99a6cb41d502d3686bac692586db2045adc19e45ee64ed23/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b", size = 2425997 },
+ { url = "https://files.pythonhosted.org/packages/ed/12/87f0e9271e2b63d35d0d8524954145837dd1a6c15b62a2d8c1ebe0f182b4/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3", size = 2376060 },
+ { url = "https://files.pythonhosted.org/packages/02/6e/c8af39288edbce8bf0fa35dee427b082758a4b71e9c91ef18fa667782138/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957", size = 2520471 },
+ { url = "https://files.pythonhosted.org/packages/13/78/df381bc7b26e535c91469f77f16adcd073beb3e2dd25042efd064af82323/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb", size = 2338793 },
+ { url = "https://files.pythonhosted.org/packages/d0/dc/c1abe38c37c071d0fc71c9a474fd0b9ede05d42f5a458d584619cfd2371a/kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2", size = 71855 },
+ { url = "https://files.pythonhosted.org/packages/a0/b6/21529d595b126ac298fdd90b705d87d4c5693de60023e0efcb4f387ed99e/kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30", size = 65430 },
+ { url = "https://files.pythonhosted.org/packages/34/bd/b89380b7298e3af9b39f49334e3e2a4af0e04819789f04b43d560516c0c8/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c", size = 126294 },
+ { url = "https://files.pythonhosted.org/packages/83/41/5857dc72e5e4148eaac5aa76e0703e594e4465f8ab7ec0fc60e3a9bb8fea/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc", size = 67736 },
+ { url = "https://files.pythonhosted.org/packages/e1/d1/be059b8db56ac270489fb0b3297fd1e53d195ba76e9bbb30e5401fa6b759/kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712", size = 66194 },
+ { url = "https://files.pythonhosted.org/packages/e1/83/4b73975f149819eb7dcf9299ed467eba068ecb16439a98990dcb12e63fdd/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e", size = 1465942 },
+ { url = "https://files.pythonhosted.org/packages/c7/2c/30a5cdde5102958e602c07466bce058b9d7cb48734aa7a4327261ac8e002/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880", size = 1595341 },
+ { url = "https://files.pythonhosted.org/packages/ff/9b/1e71db1c000385aa069704f5990574b8244cce854ecd83119c19e83c9586/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062", size = 1598455 },
+ { url = "https://files.pythonhosted.org/packages/85/92/c8fec52ddf06231b31cbb779af77e99b8253cd96bd135250b9498144c78b/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7", size = 1522138 },
+ { url = "https://files.pythonhosted.org/packages/0b/51/9eb7e2cd07a15d8bdd976f6190c0164f92ce1904e5c0c79198c4972926b7/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed", size = 1582857 },
+ { url = "https://files.pythonhosted.org/packages/0f/95/c5a00387a5405e68ba32cc64af65ce881a39b98d73cc394b24143bebc5b8/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d", size = 2293129 },
+ { url = "https://files.pythonhosted.org/packages/44/83/eeb7af7d706b8347548313fa3a3a15931f404533cc54fe01f39e830dd231/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165", size = 2421538 },
+ { url = "https://files.pythonhosted.org/packages/05/f9/27e94c1b3eb29e6933b6986ffc5fa1177d2cd1f0c8efc5f02c91c9ac61de/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6", size = 2390661 },
+ { url = "https://files.pythonhosted.org/packages/d9/d4/3c9735faa36ac591a4afcc2980d2691000506050b7a7e80bcfe44048daa7/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90", size = 2546710 },
+ { url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213 },
+]
+
[[package]]
name = "markupsafe"
version = "3.0.2"
@@ -923,6 +1022,37 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 },
]
+[[package]]
+name = "matplotlib"
+version = "3.10.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "contourpy" },
+ { name = "cycler" },
+ { name = "fonttools" },
+ { name = "kiwisolver" },
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "pillow" },
+ { name = "pyparsing" },
+ { name = "python-dateutil" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2f/08/b89867ecea2e305f408fbb417139a8dd941ecf7b23a2e02157c36da546f0/matplotlib-3.10.1.tar.gz", hash = "sha256:e8d2d0e3881b129268585bf4765ad3ee73a4591d77b9a18c214ac7e3a79fb2ba", size = 36743335 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/60/73/6770ff5e5523d00f3bc584acb6031e29ee5c8adc2336b16cd1d003675fe0/matplotlib-3.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c42eee41e1b60fd83ee3292ed83a97a5f2a8239b10c26715d8a6172226988d7b", size = 8176112 },
+ { url = "https://files.pythonhosted.org/packages/08/97/b0ca5da0ed54a3f6599c3ab568bdda65269bc27c21a2c97868c1625e4554/matplotlib-3.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4f0647b17b667ae745c13721602b540f7aadb2a32c5b96e924cd4fea5dcb90f1", size = 8046931 },
+ { url = "https://files.pythonhosted.org/packages/df/9a/1acbdc3b165d4ce2dcd2b1a6d4ffb46a7220ceee960c922c3d50d8514067/matplotlib-3.10.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa3854b5f9473564ef40a41bc922be978fab217776e9ae1545c9b3a5cf2092a3", size = 8453422 },
+ { url = "https://files.pythonhosted.org/packages/51/d0/2bc4368abf766203e548dc7ab57cf7e9c621f1a3c72b516cc7715347b179/matplotlib-3.10.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e496c01441be4c7d5f96d4e40f7fca06e20dcb40e44c8daa2e740e1757ad9e6", size = 8596819 },
+ { url = "https://files.pythonhosted.org/packages/ab/1b/8b350f8a1746c37ab69dda7d7528d1fc696efb06db6ade9727b7887be16d/matplotlib-3.10.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5d45d3f5245be5b469843450617dcad9af75ca50568acf59997bed9311131a0b", size = 9402782 },
+ { url = "https://files.pythonhosted.org/packages/89/06/f570373d24d93503988ba8d04f213a372fa1ce48381c5eb15da985728498/matplotlib-3.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:8e8e25b1209161d20dfe93037c8a7f7ca796ec9aa326e6e4588d8c4a5dd1e473", size = 8063812 },
+ { url = "https://files.pythonhosted.org/packages/fc/e0/8c811a925b5a7ad75135f0e5af46408b78af88bbb02a1df775100ef9bfef/matplotlib-3.10.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:19b06241ad89c3ae9469e07d77efa87041eac65d78df4fcf9cac318028009b01", size = 8214021 },
+ { url = "https://files.pythonhosted.org/packages/4a/34/319ec2139f68ba26da9d00fce2ff9f27679fb799a6c8e7358539801fd629/matplotlib-3.10.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01e63101ebb3014e6e9f80d9cf9ee361a8599ddca2c3e166c563628b39305dbb", size = 8090782 },
+ { url = "https://files.pythonhosted.org/packages/77/ea/9812124ab9a99df5b2eec1110e9b2edc0b8f77039abf4c56e0a376e84a29/matplotlib-3.10.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f06bad951eea6422ac4e8bdebcf3a70c59ea0a03338c5d2b109f57b64eb3972", size = 8478901 },
+ { url = "https://files.pythonhosted.org/packages/c9/db/b05bf463689134789b06dea85828f8ebe506fa1e37593f723b65b86c9582/matplotlib-3.10.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfb036f34873b46978f55e240cff7a239f6c4409eac62d8145bad3fc6ba5a3", size = 8613864 },
+ { url = "https://files.pythonhosted.org/packages/c2/04/41ccec4409f3023a7576df3b5c025f1a8c8b81fbfe922ecfd837ac36e081/matplotlib-3.10.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dc6ab14a7ab3b4d813b88ba957fc05c79493a037f54e246162033591e770de6f", size = 9409487 },
+ { url = "https://files.pythonhosted.org/packages/ac/c2/0d5aae823bdcc42cc99327ecdd4d28585e15ccd5218c453b7bcd827f3421/matplotlib-3.10.1-cp313-cp313t-win_amd64.whl", hash = "sha256:bc411ebd5889a78dabbc457b3fa153203e22248bfa6eedc6797be5df0164dbf9", size = 8134832 },
+]
+
[[package]]
name = "matplotlib-inline"
version = "0.1.7"
@@ -1064,6 +1194,34 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307 },
]
+[[package]]
+name = "numpy"
+version = "2.2.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fb/90/8956572f5c4ae52201fdec7ba2044b2c882832dcec7d5d0922c9e9acf2de/numpy-2.2.3.tar.gz", hash = "sha256:dbdc15f0c81611925f382dfa97b3bd0bc2c1ce19d4fe50482cb0ddc12ba30020", size = 20262700 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0e/8b/88b98ed534d6a03ba8cddb316950fe80842885709b58501233c29dfa24a9/numpy-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bfdb06b395385ea9b91bf55c1adf1b297c9fdb531552845ff1d3ea6e40d5aba", size = 20916001 },
+ { url = "https://files.pythonhosted.org/packages/d9/b4/def6ec32c725cc5fbd8bdf8af80f616acf075fe752d8a23e895da8c67b70/numpy-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23c9f4edbf4c065fddb10a4f6e8b6a244342d95966a48820c614891e5059bb50", size = 14130721 },
+ { url = "https://files.pythonhosted.org/packages/20/60/70af0acc86495b25b672d403e12cb25448d79a2b9658f4fc45e845c397a8/numpy-2.2.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:a0c03b6be48aaf92525cccf393265e02773be8fd9551a2f9adbe7db1fa2b60f1", size = 5130999 },
+ { url = "https://files.pythonhosted.org/packages/2e/69/d96c006fb73c9a47bcb3611417cf178049aae159afae47c48bd66df9c536/numpy-2.2.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:2376e317111daa0a6739e50f7ee2a6353f768489102308b0d98fcf4a04f7f3b5", size = 6665299 },
+ { url = "https://files.pythonhosted.org/packages/5a/3f/d8a877b6e48103733ac224ffa26b30887dc9944ff95dffdfa6c4ce3d7df3/numpy-2.2.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fb62fe3d206d72fe1cfe31c4a1106ad2b136fcc1606093aeab314f02930fdf2", size = 14064096 },
+ { url = "https://files.pythonhosted.org/packages/e4/43/619c2c7a0665aafc80efca465ddb1f260287266bdbdce517396f2f145d49/numpy-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52659ad2534427dffcc36aac76bebdd02b67e3b7a619ac67543bc9bfe6b7cdb1", size = 16114758 },
+ { url = "https://files.pythonhosted.org/packages/d9/79/ee4fe4f60967ccd3897aa71ae14cdee9e3c097e3256975cc9575d393cb42/numpy-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b416af7d0ed3271cad0f0a0d0bee0911ed7eba23e66f8424d9f3dfcdcae1304", size = 15259880 },
+ { url = "https://files.pythonhosted.org/packages/fb/c8/8b55cf05db6d85b7a7d414b3d1bd5a740706df00bfa0824a08bf041e52ee/numpy-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1402da8e0f435991983d0a9708b779f95a8c98c6b18a171b9f1be09005e64d9d", size = 17876721 },
+ { url = "https://files.pythonhosted.org/packages/21/d6/b4c2f0564b7dcc413117b0ffbb818d837e4b29996b9234e38b2025ed24e7/numpy-2.2.3-cp313-cp313-win32.whl", hash = "sha256:136553f123ee2951bfcfbc264acd34a2fc2f29d7cdf610ce7daf672b6fbaa693", size = 6290195 },
+ { url = "https://files.pythonhosted.org/packages/97/e7/7d55a86719d0de7a6a597949f3febefb1009435b79ba510ff32f05a8c1d7/numpy-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5b732c8beef1d7bc2d9e476dbba20aaff6167bf205ad9aa8d30913859e82884b", size = 12619013 },
+ { url = "https://files.pythonhosted.org/packages/a6/1f/0b863d5528b9048fd486a56e0b97c18bf705e88736c8cea7239012119a54/numpy-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:435e7a933b9fda8126130b046975a968cc2d833b505475e588339e09f7672890", size = 20944621 },
+ { url = "https://files.pythonhosted.org/packages/aa/99/b478c384f7a0a2e0736177aafc97dc9152fc036a3fdb13f5a3ab225f1494/numpy-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7678556eeb0152cbd1522b684dcd215250885993dd00adb93679ec3c0e6e091c", size = 14142502 },
+ { url = "https://files.pythonhosted.org/packages/fb/61/2d9a694a0f9cd0a839501d362de2a18de75e3004576a3008e56bdd60fcdb/numpy-2.2.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2e8da03bd561504d9b20e7a12340870dfc206c64ea59b4cfee9fceb95070ee94", size = 5176293 },
+ { url = "https://files.pythonhosted.org/packages/33/35/51e94011b23e753fa33f891f601e5c1c9a3d515448659b06df9d40c0aa6e/numpy-2.2.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:c9aa4496fd0e17e3843399f533d62857cef5900facf93e735ef65aa4bbc90ef0", size = 6691874 },
+ { url = "https://files.pythonhosted.org/packages/ff/cf/06e37619aad98a9d03bd8d65b8e3041c3a639be0f5f6b0a0e2da544538d4/numpy-2.2.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4ca91d61a4bf61b0f2228f24bbfa6a9facd5f8af03759fe2a655c50ae2c6610", size = 14036826 },
+ { url = "https://files.pythonhosted.org/packages/0c/93/5d7d19955abd4d6099ef4a8ee006f9ce258166c38af259f9e5558a172e3e/numpy-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:deaa09cd492e24fd9b15296844c0ad1b3c976da7907e1c1ed3a0ad21dded6f76", size = 16096567 },
+ { url = "https://files.pythonhosted.org/packages/af/53/d1c599acf7732d81f46a93621dab6aa8daad914b502a7a115b3f17288ab2/numpy-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:246535e2f7496b7ac85deffe932896a3577be7af8fb7eebe7146444680297e9a", size = 15242514 },
+ { url = "https://files.pythonhosted.org/packages/53/43/c0f5411c7b3ea90adf341d05ace762dad8cb9819ef26093e27b15dd121ac/numpy-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:daf43a3d1ea699402c5a850e5313680ac355b4adc9770cd5cfc2940e7861f1bf", size = 17872920 },
+ { url = "https://files.pythonhosted.org/packages/5b/57/6dbdd45ab277aff62021cafa1e15f9644a52f5b5fc840bc7591b4079fb58/numpy-2.2.3-cp313-cp313t-win32.whl", hash = "sha256:cf802eef1f0134afb81fef94020351be4fe1d6681aadf9c5e862af6602af64ef", size = 6346584 },
+ { url = "https://files.pythonhosted.org/packages/97/9b/484f7d04b537d0a1202a5ba81c6f53f1846ae6c63c2127f8df869ed31342/numpy-2.2.3-cp313-cp313t-win_amd64.whl", hash = "sha256:aee2512827ceb6d7f517c8b85aa5d3923afe8fc7a57d028cffcd522f1c6fd082", size = 12706784 },
+]
+
[[package]]
name = "oauth2client"
version = "4.1.3"
@@ -1156,6 +1314,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 },
]
+[[package]]
+name = "pillow"
+version = "11.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/af/c097e544e7bd278333db77933e535098c259609c4eb3b85381109602fb5b/pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20", size = 46742715 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/31/9ca79cafdce364fd5c980cd3416c20ce1bebd235b470d262f9d24d810184/pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc", size = 3226640 },
+ { url = "https://files.pythonhosted.org/packages/ac/0f/ff07ad45a1f172a497aa393b13a9d81a32e1477ef0e869d030e3c1532521/pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0", size = 3101437 },
+ { url = "https://files.pythonhosted.org/packages/08/2f/9906fca87a68d29ec4530be1f893149e0cb64a86d1f9f70a7cfcdfe8ae44/pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1", size = 4326605 },
+ { url = "https://files.pythonhosted.org/packages/b0/0f/f3547ee15b145bc5c8b336401b2d4c9d9da67da9dcb572d7c0d4103d2c69/pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec", size = 4411173 },
+ { url = "https://files.pythonhosted.org/packages/b1/df/bf8176aa5db515c5de584c5e00df9bab0713548fd780c82a86cba2c2fedb/pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5", size = 4369145 },
+ { url = "https://files.pythonhosted.org/packages/de/7c/7433122d1cfadc740f577cb55526fdc39129a648ac65ce64db2eb7209277/pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114", size = 4496340 },
+ { url = "https://files.pythonhosted.org/packages/25/46/dd94b93ca6bd555588835f2504bd90c00d5438fe131cf01cfa0c5131a19d/pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352", size = 4296906 },
+ { url = "https://files.pythonhosted.org/packages/a8/28/2f9d32014dfc7753e586db9add35b8a41b7a3b46540e965cb6d6bc607bd2/pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3", size = 4431759 },
+ { url = "https://files.pythonhosted.org/packages/33/48/19c2cbe7403870fbe8b7737d19eb013f46299cdfe4501573367f6396c775/pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9", size = 2291657 },
+ { url = "https://files.pythonhosted.org/packages/3b/ad/285c556747d34c399f332ba7c1a595ba245796ef3e22eae190f5364bb62b/pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c", size = 2626304 },
+ { url = "https://files.pythonhosted.org/packages/e5/7b/ef35a71163bf36db06e9c8729608f78dedf032fc8313d19bd4be5c2588f3/pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65", size = 2375117 },
+ { url = "https://files.pythonhosted.org/packages/79/30/77f54228401e84d6791354888549b45824ab0ffde659bafa67956303a09f/pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861", size = 3230060 },
+ { url = "https://files.pythonhosted.org/packages/ce/b1/56723b74b07dd64c1010fee011951ea9c35a43d8020acd03111f14298225/pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081", size = 3106192 },
+ { url = "https://files.pythonhosted.org/packages/e1/cd/7bf7180e08f80a4dcc6b4c3a0aa9e0b0ae57168562726a05dc8aa8fa66b0/pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c", size = 4446805 },
+ { url = "https://files.pythonhosted.org/packages/97/42/87c856ea30c8ed97e8efbe672b58c8304dee0573f8c7cab62ae9e31db6ae/pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547", size = 4530623 },
+ { url = "https://files.pythonhosted.org/packages/ff/41/026879e90c84a88e33fb00cc6bd915ac2743c67e87a18f80270dfe3c2041/pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab", size = 4465191 },
+ { url = "https://files.pythonhosted.org/packages/e5/fb/a7960e838bc5df57a2ce23183bfd2290d97c33028b96bde332a9057834d3/pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9", size = 2295494 },
+ { url = "https://files.pythonhosted.org/packages/d7/6c/6ec83ee2f6f0fda8d4cf89045c6be4b0373ebfc363ba8538f8c999f63fcd/pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe", size = 2631595 },
+ { url = "https://files.pythonhosted.org/packages/cf/6c/41c21c6c8af92b9fea313aa47c75de49e2f9a467964ee33eb0135d47eb64/pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756", size = 2377651 },
+]
+
[[package]]
name = "platformdirs"
version = "4.3.6"
@@ -1396,6 +1581,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9c/ce/1e4b53c213dce25d6e8b163697fbce2d43799d76fa08eea6ad270451c370/pytest_asyncio-0.21.2-py3-none-any.whl", hash = "sha256:ab664c88bb7998f711d8039cacd4884da6430886ae8bbd4eded552ed2004f16b", size = 13368 },
]
+[[package]]
+name = "pytest-snapshot"
+version = "0.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9b/7b/ab8f1fc1e687218aa66acec1c3674d9c443f6a2dc8cb6a50f464548ffa34/pytest-snapshot-0.9.0.tar.gz", hash = "sha256:c7013c3abc3e860f9feff899f8b4debe3708650d8d8242a61bf2625ff64db7f3", size = 19877 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/29/518f32faf6edad9f56d6e0107217f7de6b79f297a47170414a2bd4be7f01/pytest_snapshot-0.9.0-py3-none-any.whl", hash = "sha256:4b9fe1c21c868fe53a545e4e3184d36bc1c88946e3f5c1d9dd676962a9b3d4ab", size = 10715 },
+]
+
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
@@ -1599,6 +1796,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/49/97/fa78e3d2f65c02c8e1268b9aba606569fe97f6c8f7c2d74394553347c145/rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", size = 34315 },
]
+[[package]]
+name = "ruff"
+version = "0.9.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/20/8e/fafaa6f15c332e73425d9c44ada85360501045d5ab0b81400076aff27cf6/ruff-0.9.10.tar.gz", hash = "sha256:9bacb735d7bada9cfb0f2c227d3658fc443d90a727b47f206fb33f52f3c0eac7", size = 3759776 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/73/b2/af7c2cc9e438cbc19fafeec4f20bfcd72165460fe75b2b6e9a0958c8c62b/ruff-0.9.10-py3-none-linux_armv6l.whl", hash = "sha256:eb4d25532cfd9fe461acc83498361ec2e2252795b4f40b17e80692814329e42d", size = 10049494 },
+ { url = "https://files.pythonhosted.org/packages/6d/12/03f6dfa1b95ddd47e6969f0225d60d9d7437c91938a310835feb27927ca0/ruff-0.9.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:188a6638dab1aa9bb6228a7302387b2c9954e455fb25d6b4470cb0641d16759d", size = 10853584 },
+ { url = "https://files.pythonhosted.org/packages/02/49/1c79e0906b6ff551fb0894168763f705bf980864739572b2815ecd3c9df0/ruff-0.9.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5284dcac6b9dbc2fcb71fdfc26a217b2ca4ede6ccd57476f52a587451ebe450d", size = 10155692 },
+ { url = "https://files.pythonhosted.org/packages/5b/01/85e8082e41585e0e1ceb11e41c054e9e36fed45f4b210991052d8a75089f/ruff-0.9.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47678f39fa2a3da62724851107f438c8229a3470f533894b5568a39b40029c0c", size = 10369760 },
+ { url = "https://files.pythonhosted.org/packages/a1/90/0bc60bd4e5db051f12445046d0c85cc2c617095c0904f1aa81067dc64aea/ruff-0.9.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99713a6e2766b7a17147b309e8c915b32b07a25c9efd12ada79f217c9c778b3e", size = 9912196 },
+ { url = "https://files.pythonhosted.org/packages/66/ea/0b7e8c42b1ec608033c4d5a02939c82097ddcb0b3e393e4238584b7054ab/ruff-0.9.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524ee184d92f7c7304aa568e2db20f50c32d1d0caa235d8ddf10497566ea1a12", size = 11434985 },
+ { url = "https://files.pythonhosted.org/packages/d5/86/3171d1eff893db4f91755175a6e1163c5887be1f1e2f4f6c0c59527c2bfd/ruff-0.9.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:df92aeac30af821f9acf819fc01b4afc3dfb829d2782884f8739fb52a8119a16", size = 12155842 },
+ { url = "https://files.pythonhosted.org/packages/89/9e/700ca289f172a38eb0bca752056d0a42637fa17b81649b9331786cb791d7/ruff-0.9.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de42e4edc296f520bb84954eb992a07a0ec5a02fecb834498415908469854a52", size = 11613804 },
+ { url = "https://files.pythonhosted.org/packages/f2/92/648020b3b5db180f41a931a68b1c8575cca3e63cec86fd26807422a0dbad/ruff-0.9.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d257f95b65806104b6b1ffca0ea53f4ef98454036df65b1eda3693534813ecd1", size = 13823776 },
+ { url = "https://files.pythonhosted.org/packages/5e/a6/cc472161cd04d30a09d5c90698696b70c169eeba2c41030344194242db45/ruff-0.9.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60dec7201c0b10d6d11be00e8f2dbb6f40ef1828ee75ed739923799513db24c", size = 11302673 },
+ { url = "https://files.pythonhosted.org/packages/6c/db/d31c361c4025b1b9102b4d032c70a69adb9ee6fde093f6c3bf29f831c85c/ruff-0.9.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d838b60007da7a39c046fcdd317293d10b845001f38bcb55ba766c3875b01e43", size = 10235358 },
+ { url = "https://files.pythonhosted.org/packages/d1/86/d6374e24a14d4d93ebe120f45edd82ad7dcf3ef999ffc92b197d81cdc2a5/ruff-0.9.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ccaf903108b899beb8e09a63ffae5869057ab649c1e9231c05ae354ebc62066c", size = 9886177 },
+ { url = "https://files.pythonhosted.org/packages/00/62/a61691f6eaaac1e945a1f3f59f1eea9a218513139d5b6c2b8f88b43b5b8f/ruff-0.9.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f9567d135265d46e59d62dc60c0bfad10e9a6822e231f5b24032dba5a55be6b5", size = 10864747 },
+ { url = "https://files.pythonhosted.org/packages/ee/94/2c7065e1d92a8a8a46d46d9c3cf07b0aa7e0a1e0153d74baa5e6620b4102/ruff-0.9.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5f202f0d93738c28a89f8ed9eaba01b7be339e5d8d642c994347eaa81c6d75b8", size = 11360441 },
+ { url = "https://files.pythonhosted.org/packages/a7/8f/1f545ea6f9fcd7bf4368551fb91d2064d8f0577b3079bb3f0ae5779fb773/ruff-0.9.10-py3-none-win32.whl", hash = "sha256:bfb834e87c916521ce46b1788fbb8484966e5113c02df216680102e9eb960029", size = 10247401 },
+ { url = "https://files.pythonhosted.org/packages/4f/18/fb703603ab108e5c165f52f5b86ee2aa9be43bb781703ec87c66a5f5d604/ruff-0.9.10-py3-none-win_amd64.whl", hash = "sha256:f2160eeef3031bf4b17df74e307d4c5fb689a6f3a26a2de3f7ef4044e3c484f1", size = 11366360 },
+ { url = "https://files.pythonhosted.org/packages/35/85/338e603dc68e7d9994d5d84f24adbf69bae760ba5efd3e20f5ff2cec18da/ruff-0.9.10-py3-none-win_arm64.whl", hash = "sha256:5fd804c0327a5e5ea26615550e706942f348b197d5475ff34c19733aee4b2e69", size = 10436892 },
+]
+
[[package]]
name = "send2trash"
version = "1.8.3"