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 @@ + + + + + + + + 2009-02-13T23:31:30+00:00 + image/svg+xml + + + Matplotlib v3.10.1, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + 2009-02-13T23:31:30+00:00 + image/svg+xml + + + Matplotlib v3.10.1, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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"