diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 186739f..0000000 --- a/.dockerignore +++ /dev/null @@ -1,11 +0,0 @@ -.git -.github -.venv -tests -__pycache__ -*.pyc -.ruff_cache -.pytest_cache -.pre-commit-config.yaml -Dockerfile -.dockerignore diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 0899366..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,70 +0,0 @@ -name: Publish container image -on: - release: - types: [published] - workflow_dispatch: -permissions: - contents: read - packages: write -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository_owner }}/prs-utils -jobs: - build-and-push: - runs-on: ubuntu-latest - steps: - - name: Check out repository - uses: actions/checkout@v7 - - - name: Set up QEMU - # Needed to build the linux/arm64 image on an amd64 runner. - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract image metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - # The Nextflow modules pin the full version, e.g. 1.0.0. The major/minor aliases and - # latest exist for convenience only -- do not pin a module against them. - tags: | - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,value=latest,enable=${{ github.event_name == 'release' }} - - - name: Build and push - id: build - uses: docker/build-push-action@v6 - with: - context: . - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Verify the published image reports the release version - # The Nextflow modules emit `prs-utils --version` into their versions topic, so a tag that - # disagrees with the packaged version must not survive a release. - if: github.event_name == 'release' - run: | - set -euo pipefail - image="${REGISTRY}/${IMAGE_NAME}:${GITHUB_REF_NAME#v}" - reported=$(docker run --rm "$image" prs-utils --version) - echo "Image $image reports version: $reported" - if [ "$reported" != "${GITHUB_REF_NAME#v}" ]; then - echo "::error::Image reports '$reported' but the release tag is '${GITHUB_REF_NAME#v}'." - echo "::error::Bump 'version' in pyproject.toml to match the tag." - exit 1 - fi diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index dc552da..0000000 --- a/Dockerfile +++ /dev/null @@ -1,29 +0,0 @@ -# Build the virtualenv from uv.lock so the image ships exactly the dependency set the tests ran -# against, rather than re-resolving at runtime the way `uv run --script` used to. -# The uv tag pins uv itself (pyproject requires >= 0.11.28) and the Python minor; the runtime stage -# must stay on the same Python minor and Debian release, because the venv is copied, not rebuilt. -FROM ghcr.io/astral-sh/uv:0.12.1-python3.13-trixie-slim AS builder - -ENV UV_COMPILE_BYTECODE=1 \ - UV_LINK_MODE=copy - -WORKDIR /app - -# Dependencies first, so this layer caches independently of source changes. -COPY pyproject.toml uv.lock ./ -RUN uv sync --frozen --no-install-project --no-dev - -# README.md is required by the build backend -- pyproject sets `readme = "README.md"`. -COPY README.md ./ -COPY src ./src -# --no-editable copies the package into the venv instead of linking back to /app/src, so the -# runtime stage below only needs the venv. -RUN uv sync --frozen --no-dev --no-editable - - -FROM python:3.13-slim-trixie - -COPY --from=builder /app/.venv /app/.venv -ENV PATH="/app/.venv/bin:$PATH" - -CMD ["prs-utils", "--help"] diff --git a/README.md b/README.md index c1a0763..e53864c 100644 --- a/README.md +++ b/README.md @@ -1,109 +1,46 @@ -# prs_utils +# python_template -![test](https://github.com/UMCUGenetics/prs_utils/actions/workflows/test.yml/badge.svg) -![lint](https://github.com/UMCUGenetics/prs_utils/actions/workflows/lint.yml/badge.svg) +![test](https://github.com/UMCUGenetics/python_template/actions/workflows/test.yml/badge.svg) +![lint](https://github.com/UMCUGenetics/python_template/actions/workflows/lint.yml/badge.svg) -Utility commands for the PRS pipeline, bundled as a single installable package. +Python template project - This repository can be used as a starting point / guide on how to setup a python repository, including automated tests and code formatting. -These utilities previously lived as standalone [PEP 723](https://peps.python.org/pep-0723/) -scripts inside the `prsutils` Nextflow modules -(`NF-Modules/modules/UMCUGenetics/prsutils/*/resources/usr/bin/`), each with its own -`uv run --script` header and lock file. They are now one package with shared dependency -resolution, linting and tests. The behaviour of every command is unchanged. +## GitHub repository creation -## Commands +- Repository name: `Modules should have short, all-lowercase names. Underscores can be used in the module name if it improves readability. Python packages should also have short, all-lowercase names, although the use of underscores is discouraged.` +- Add README +- Add .gitignore: `python` +- Add License: `MIT` -Everything is reachable through the `prs-utils` entry point: +Make sure to perform the following actions, after creating a new github repo: -| Subcommand | Original script | NF module | -| --- | --- | --- | -| `prs-utils pgs-to-vcf` | `pgs_to_vcf.py` | `prsutils/getvcf` | -| `prs-utils merge-prs-mqc` | `merge_prs_mqc.py` | `prsutils/mergeprsmqc` | -| `prs-utils normalise-counts` | `normalise_counts.py` | `prsutils/norm` | -| `prs-utils sample-qc` | `sample_qc.py` | `prsutils/sampleqc` | -| `prs-utils get-snp-list` | `get_snp_list.py` | `prsutils/snplist` | +- Create a develop branch. +- Configure branch protection rules. -Each subcommand keeps the argument parser it was written with, so the flags are identical to -the standalone scripts. Use `prs-utils --help` for the per-command options. +## UV -```sh -uv run prs-utils --help -uv run prs-utils normalise-counts --PRS scores.tsv --mu 0.5 --SD 0.1 -o normalised.tsv -uv run prs-utils get-snp-list --scoring_file PGS000004.txt --prefix PGS000004 --flank 100 -``` - -Note that `merge-prs-mqc` takes its inputs as bare positional paths and always writes -`prs_scores_mqc.tsv` in the working directory. +UV supports packaged and unpackaged applications, this repository show cases a packaged application initiated with the command: `uv init --package .`. To create a simpler unpackaged application use: `uv init .`. Unpackaged applications can be used for single file scripts/tools, while packaged applications are used for (larger) tools requiring multiple files, distribution (pip) and tests separation. -## Container image - -Each release publishes a multi-arch image to -`ghcr.io/umcugenetics/prs-utils:`, built from `uv.lock` so the dependency set is exactly -the one the tests ran against. This is what the `prsutils` Nextflow modules pin their `container` -directive to. +Setup uv package and development dependencies: ```sh -docker run --rm -v "$PWD:/data" -w /data ghcr.io/umcugenetics/prs-utils:1.0.0 \ - prs-utils normalise-counts --PRS scores.tsv --mu 0.5 --SD 0.1 -o normalised.tsv +uv init --package . +uv add --dev ruff +uv add --dev pytest ``` -Singularity pulls the same image directly: +Run pytest and the python-template tool: ```sh -singularity exec docker://ghcr.io/umcugenetics/prs-utils:1.0.0 prs-utils --version -``` - -Build it locally without publishing: - -```sh -docker build -t prs-utils:dev . -``` - -## Releasing - -`prs-utils --version` prints the bare version from `pyproject.toml`, and the Nextflow modules -capture that output into their `versions` topic and snapshot it. Keep the two repositories in step: - -1. Bump `version` in `pyproject.toml`. -2. Run `uv lock` and commit the updated `uv.lock`. -3. Tag and publish a GitHub release. The `publish.yml` workflow builds and pushes the image, then - fails the release if the image does not report the tagged version. -4. Confirm the new tag appears under the repository's Packages. -5. Bump the `container` tag in the five `prsutils` modules in - [UMCUGenetics/NF-Modules](https://github.com/UMCUGenetics/NF-Modules) and re-run their nf-tests. - -The first published version needs one manual step: set the GHCR package visibility to **public** -(Package settings → Change visibility). Otherwise every `singularity pull` in CI and on the HPC -needs registry credentials. - -## Development - -Setup and run the test suite: - -```sh -uv sync uv run pytest tests -``` - -Linting and formatting use [Ruff](https://docs.astral.sh/ruff/): - -```sh -uv run ruff check src tests -uv run ruff format src tests +uv run python-template World +uv run python-template --help ``` ## GitHub Actions -This repository contains two GitHub actions workflows (`.github/workflows/`): `lint.yml` and -`test.yml`. The lint workflow uses the [ruff-action](https://github.com/astral-sh/ruff-action) -action to run ruff. The test workflow uses the -[setup-uv action](https://github.com/astral-sh/setup-uv) to setup uv, install dependencies and -run tests. Both actions are configured to run on each pull request and push to main and develop. +This template project contains two GitHub actions workflows (`.github/workflos/`): `lint.yml` and `test.yml`. The lint workflow uses the [ruff-action](https://github.com/astral-sh/ruff-action) action to run ruff. The test workflow uses the [setup-uv action](https://github.com/astral-sh/setup-uv) to setup uv, install dependencies and run tests. Both actions are configured to run on each pull request and push to main and develop. ## pre-commit -Git pre-commit hooks enable you to run certain commands before each commit and can be used to -check code style before committing. The file `.pre-commit-config.yaml` contains the -[Ruff](https://docs.astral.sh/ruff/) [pre-commit](https://pre-commit.com) hook, which will -automatically run Ruff before each commit. Run the following command to install the git commit -hook: `uvx pre-commit install`. +Git pre-commit hooks enable you to run certain commands before each commit and can be used to check code style before committing. The file `.pre-commit-config.yaml` contains the [Ruff](https://docs.astral.sh/ruff/) [pre-commit](https://pre-commit.com) hook, which will automatically run Ruff before each commit. Run the following command to install the git commit hook: `uvx pre-commit install`. diff --git a/pyproject.toml b/pyproject.toml index 1c9e6de..6f9cf08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,13 @@ [project] -name = "prs-utils" -version = "1.0.0" -description = "Utility commands used by the UMCUGenetics prsutils Nextflow modules." +name = "python-template" +version = "0.1.0" +description = "Python template project." readme = "README.md" authors = [ { name = "Bioinformatica Genetica", email = "bioinformatica-genetica@umcutrecht.nl" } ] -requires-python = ">=3.12" +requires-python = "~=3.14.0" dependencies = [ - "pandas>=2.2", "typer>=0.26.8", ] @@ -20,7 +19,7 @@ dev = [ ] [project.scripts] -prs-utils = "prs_utils.cli:cli" +python-template = "python_template.cli:cli" [build-system] requires = ["uv_build>=0.11.27,<0.12"] diff --git a/src/prs_utils/cli.py b/src/prs_utils/cli.py deleted file mode 100644 index 8a03314..0000000 --- a/src/prs_utils/cli.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Single entry point bundling the PRS pipeline utility scripts as subcommands.""" - -from importlib.metadata import version as package_version -from typing import Annotated - -import typer - -from prs_utils import get_snp_list, merge_prs_mqc, normalise_counts, pgs_to_vcf, sample_qc - -cli = typer.Typer(add_completion=False, help="Utilities for the PRS pipeline.") - -# These subcommands own their own argument parsing (argparse / sys.argv), so Click must not -# touch anything after the subcommand name -- not even --help, which is forwarded verbatim. -_PASSTHROUGH = { - "allow_extra_args": True, - "ignore_unknown_options": True, - "help_option_names": [], -} - - -def _version_callback(show_version: bool) -> None: - # The Nextflow modules capture this output into their `versions` topic, so it must be the - # bare version and nothing else. - if show_version: - typer.echo(package_version("prs-utils")) - raise typer.Exit() - - -@cli.callback() -def _cli_options( - version: Annotated[ - bool, - typer.Option( - "--version", - callback=_version_callback, - is_eager=True, - help="Show the package version and exit.", - ), - ] = False, -) -> None: - """Utilities for the PRS pipeline.""" - - -@cli.command( - "pgs-to-vcf", - context_settings=_PASSTHROUGH, - help="Convert a PGS Catalog scoring file to a VCF (REF=other_allele, ALT=effect_allele).", -) -def _pgs_to_vcf(ctx: typer.Context) -> None: - pgs_to_vcf.main(ctx.args) - - -@cli.command( - "merge-prs-mqc", - context_settings=_PASSTHROUGH, - help="Merge per-sample QC tables into a MultiQC table (writes prs_scores_mqc.tsv).", -) -def _merge_prs_mqc(ctx: typer.Context) -> None: - merge_prs_mqc.main(ctx.args) - - -@cli.command( - "normalise-counts", - context_settings=_PASSTHROUGH, - help="Add a SUM_Z Z-score column to a PRS score table.", -) -def _normalise_counts(ctx: typer.Context) -> None: - normalise_counts.main(ctx.args) - - -@cli.command( - "get-snp-list", - context_settings=_PASSTHROUGH, - help="Write a flanked region list from the positions in a scoring file.", -) -def _get_snp_list(ctx: typer.Context) -> None: - get_snp_list.main(ctx.args) - - -# sample_qc is already a Typer command function -- reuse it as-is so its options are unchanged. -cli.command( - "sample-qc", - help="Flag samples based on PRS Z-score and ancestry thresholds.", -)(sample_qc.main) - - -if __name__ == "__main__": - cli() diff --git a/src/prs_utils/get_snp_list.py b/src/prs_utils/get_snp_list.py deleted file mode 100644 index bf7a64d..0000000 --- a/src/prs_utils/get_snp_list.py +++ /dev/null @@ -1,50 +0,0 @@ -from argparse import ArgumentParser - -import pandas as pd - - -def positions_to_tsv(scoring_file, chr_colname, pos_colname, prefix, flank=100): - df = pd.read_csv(scoring_file, sep="\t", comment="#") - - with open(f"{prefix}_snplist.list", "w") as outfile: - for index, row in df.iterrows(): - chrom = row[chr_colname] - pos = int(row[pos_colname]) - - start = pos - flank - end = pos + flank - - # Skip rows with missing chromosome or position - if not chrom or not pos: - continue - - # Ensure chromosome has 'chr' prefix - chr_prefix = "chr" - if "chr" in str(chrom): - chr_prefix = "" - - tsv_line = f"{chr_prefix}{chrom}:{str(start)}-{str(end)}\n" - - outfile.write(tsv_line) - - -def get_opts(argv=None): - p = ArgumentParser() - p.add_argument("--scoring_file", help="Custom scoring file, alternative for PGSID", required=True) - p.add_argument("--flank", help="Flanking region for each position to call variants in.", default=100, type=int) - p.add_argument("--prefix", help="Prefix for output files", required=True) - - return p.parse_args(argv) - - -def main(argv=None): - args = get_opts(argv) - - chr_colname = "hm_chr" - pos_colname = "hm_pos" - - positions_to_tsv(args.scoring_file, chr_colname, pos_colname, args.prefix, int(args.flank)) - - -if __name__ == "__main__": - main() diff --git a/src/prs_utils/merge_prs_mqc.py b/src/prs_utils/merge_prs_mqc.py deleted file mode 100644 index af256cc..0000000 --- a/src/prs_utils/merge_prs_mqc.py +++ /dev/null @@ -1,75 +0,0 @@ -import sys - -import pandas as pd - -sample_palette = ["#e8f0fe", "#ffffff"] # blue / white — sample stripes - -# Pastel palette — distinct color per model so the same model is recognisable -# across all samples. Wraps after 8 models (rare in practice). -model_palette = [ - "#ffd9b3", # peach - "#b3d9f0", # sky blue - "#fff2a8", # pale yellow - "#d4b8e8", # lavender - "#a8d8c8", # mint teal - "#f5b8c8", # rose pink - "#d9c4a3", # warm sand - "#e0e0e0", # soft grey -] - - -def _emit_bgcols(out, column, mapping): - out.write(f"# {column}:\n") - out.write("# bgcols:\n") - for key, color in mapping.items(): - out.write(f"# '{key}': '{color}'\n") - - -def main(argv=None): - files = list(sys.argv[1:]) if argv is None else list(argv) - - dfs = [] - for f in files: - df = pd.read_csv(f, sep="\t") - dfs.append(df) - - combined = pd.concat(dfs, ignore_index=True) - combined = combined.rename( - columns={ - "SUM_Z": "Z-score", - "QC status": "QC Status", - } - ) - combined = combined[["Sample", "Model", "Z-score", "QC Status", "QC Comment", "Model Alpha"]] - combined.insert(0, "Sample_Model", combined["Sample"] + "__" + combined["Model"]) - combined = combined.sort_values(["Sample", "Model"]).reset_index(drop=True) - - unique_samples = combined["Sample"].drop_duplicates().tolist() - sample_bgcols = {s: sample_palette[i % len(sample_palette)] for i, s in enumerate(unique_samples)} - - # Sample_Model rows align 1:1 with Sample rows — reuse the sample palette, - # keyed on the composite "${Sample}__${Model}" value. - sample_model_bgcols = {sm: sample_bgcols[s] for sm, s in zip(combined["Sample_Model"], combined["Sample"])} - - unique_models = combined["Model"].drop_duplicates().tolist() - model_bgcols = {m: model_palette[i % len(model_palette)] for i, m in enumerate(unique_models)} - - with open("prs_scores_mqc.tsv", "w") as out: - out.write("# id: 'prs-scores'\n") - out.write("# pconfig:\n") - out.write("# only_defined_headers: false\n") - out.write("# headers:\n") - _emit_bgcols(out, "Sample_Model", sample_model_bgcols) - _emit_bgcols(out, "Sample", sample_bgcols) - _emit_bgcols(out, "Model", model_bgcols) - out.write("# Z-score:\n") - out.write("# title: 'Z-score'\n") - out.write("# format: '{:,.3f}'\n") - out.write("# Model Alpha:\n") - out.write("# title: 'Model Alpha'\n") - out.write("# format: '{:,.3f}'\n") - combined.to_csv(out, sep="\t", index=False) - - -if __name__ == "__main__": - main() diff --git a/src/prs_utils/normalise_counts.py b/src/prs_utils/normalise_counts.py deleted file mode 100644 index 26a13f8..0000000 --- a/src/prs_utils/normalise_counts.py +++ /dev/null @@ -1,33 +0,0 @@ -import argparse - -import pandas as pd - - -def get_args(argv=None): - parser = argparse.ArgumentParser() - parser.add_argument("--PRS", help="Table with PRS scores") - parser.add_argument("--mu", help="Population average for calculating Z-scores", required=True, type=float) - parser.add_argument("--SD", help="Population standard deviation for calculating Z-scores", required=True, type=float) - parser.add_argument("-o", "--output", help="Output file", required=True) - - return parser.parse_args(argv) - - -def score_to_z_score(scores, mu, SD): - z_scores = (scores - mu) / SD - - return z_scores - - -def main(argv=None): - args = get_args(argv) - - df = pd.read_csv(args.PRS, sep="\t") - - df["SUM_Z"] = score_to_z_score(df["SCORE1_SUM"], mu=args.mu, SD=args.SD) - - df.to_csv(args.output, sep="\t", index=False) - - -if __name__ == "__main__": - main() diff --git a/src/prs_utils/pgs_to_vcf.py b/src/prs_utils/pgs_to_vcf.py deleted file mode 100644 index a96d782..0000000 --- a/src/prs_utils/pgs_to_vcf.py +++ /dev/null @@ -1,134 +0,0 @@ -import argparse -import csv - - -def parse_args(argv=None): - p = argparse.ArgumentParser( - description="Convert a PGS Catalog scoring CSV/TSV to a VCF (REF=other_allele, ALT=effect_allele)." - ) - p.add_argument("input", help="Input PGS scoring file (tab-delimited)", type=argparse.FileType("r")) - p.add_argument("output", help="Output VCF path ('-' for stdout)", type=argparse.FileType("w", encoding="UTF-8")) - p.add_argument("--pgsid", help="Override PGS ID for header/INFO (falls back to value parsed from metadata)") - p.add_argument("--genome-build", help="Override genome build for header (e.g. GRCh37/GRCh38)") - return p.parse_args(argv) - - -def read_meta(fh): - meta = {} - header = None - body_lines = [] - - for line in fh: - if not line.startswith("#"): # stop at first non-# (the header line) - header = line.rstrip("\n") - break - if line.startswith("###"): # human-readable banner -> ignore - continue - if "=" in line: - key, val = line.strip()[1:].split("=", 1) - meta[key.strip()] = val.strip() - # store the remaining lines (the body) - body_lines = fh.readlines() - - return meta, header, body_lines - - -def build_info_fields(row, pgsid): - # Build INFO - weight = (row.get("effect_weight") or "").strip() - eaf = (row.get("allelefrequency_effect") or "").strip() - hm_source = (row.get("hm_source") or "").replace(" ", "_") - hm_match_chr = (row.get("hm_match_chr") or "").strip() - hm_match_pos = (row.get("hm_match_pos") or "").strip() - info_parts = [f"PGSID={pgsid}"] - if weight: - try: - info_parts.append(f"EFFECT_WEIGHT={float(weight)}") - except ValueError: - pass - if eaf: - try: - info_parts.append(f"EAF={float(eaf)}") - except ValueError: - pass - if hm_source: - info_parts.append(f"HM_SOURCE={hm_source}") - if hm_match_chr and hm_match_pos: - info_parts.append(f"HM_MATCH=chr:{hm_match_chr}|pos:{hm_match_pos}") - - return ";".join(info_parts) - - -def main(argv=None): - args = parse_args(argv) - meta, header, body = read_meta(args.input) - pgsid = args.pgsid or meta.get("pgs_id", "PGS_UNKNOWN") - build = args.genome_build or meta.get("genome_build", "unknown") - - # Open IO - with args.output as oh: - # TSV reader - reader = csv.DictReader([header] + body, delimiter="\t") - - # collect contigs to emit header contig lines later - contigs = set() - chrom_prefix = "" - - # Write VCF header - oh.write("##fileformat=VCFv4.2\n") - oh.write("##source=PGS_Catalog_to_VCF\n") - oh.write(f"##pgs_id={pgsid}\n") - oh.write(f"##reference={build}\n") - oh.write('##INFO=\n') - oh.write('##INFO=\n') - oh.write('##INFO=\n') - oh.write('##INFO=\n') - oh.write('##INFO=\n') - - # Peek through to gather contigs (we need to iterate twice or buffer) - rows = list(reader) - for row in rows: - chrom = (row.get("hm_chr") or "").strip() - if chrom: - if build.lower() == "grch38" and "chr" not in chrom: - chrom_prefix = "chr" - chrom = f"{chrom_prefix}{chrom}" - contigs.add(str(chrom)) - - for contig in sorted(contigs, key=lambda x: x.lstrip("chr") if isinstance(x, str) else x): - oh.write(f"##contig=\n") - - oh.write("#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\n") - - # Emit records - for i, row in enumerate(rows, start=1): - # Choose harmonized coords if available, else original - chrom = (row.get("hm_chr") or "").strip() - pos = (row.get("hm_pos") or "").strip() - if build.lower() == "grch38" and chrom and "chr" not in chrom: - chrom = f"chr{chrom}" - - try: - pos_int = int(pos) - except ValueError: - continue - - ref = (row.get("other_allele") or "").upper().replace(" ", "") - alt = (row.get("effect_allele") or "").upper().replace(" ", "") - - # Basic sanity: skip if missing alleles or REF == ALT - if not ref or not alt or ref == alt: - continue - - rsid = (row.get("hm_rsID") or row.get("rsID") or ".").strip() or "." - qual = "." - filt = "PASS" - - info = build_info_fields(row, pgsid) - - # Write VCF line - oh.write(f"{chrom}\t{pos_int}\t{rsid}\t{ref}\t{alt}\t{qual}\t{filt}\t{info}\n") - - -if __name__ == "__main__": - main() diff --git a/src/prs_utils/sample_qc.py b/src/prs_utils/sample_qc.py deleted file mode 100644 index 7ba58ad..0000000 --- a/src/prs_utils/sample_qc.py +++ /dev/null @@ -1,157 +0,0 @@ -"""Sample QC: flag samples based on PRS Z-score and ancestry thresholds.""" - -from __future__ import annotations - -from pathlib import Path -from typing import Annotated, List - -import pandas as pd -import typer - -app = typer.Typer(add_completion=False, help="Sample QC for PRS pipeline.") - - -def QC_check( - row: pd.Series, expected_ancestry: list[str], conf_threshold: float, model_matchrate: float, matchrate_threshold: float -) -> pd.Series: - """Assign QC status and comments to a single sample row. - - Args: - row: A single row from the merged scores/ancestry DataFrame. - expected_ancestry: Accepted predicted ancestry group labels. - conf_threshold: Minimum KNN confidence score to pass ancestry QC. - model_matchrate: Fraction of model variants found in the sample (0–1). - matchrate_threshold: Minimum required model matchrate to pass QC. - - Returns: - The input row with ``QC status`` and ``QC Comment`` columns added. - """ - ancestry_group_pass = row["pred_group"] in expected_ancestry - ancestry_conf_pass = row["knn_conf"] >= conf_threshold - matchrate_pass = model_matchrate >= matchrate_threshold - - qc_comment = [] - if not ancestry_group_pass: - qc_comment.append("REFERENCE_PANEL_MISMATCH") - if not ancestry_conf_pass: - qc_comment.append("LOW_REFERENCE_PANEL_CONFIDENCE") - if not matchrate_pass: - qc_comment.append("INSUFFICIENT_VARIANT_COVERAGE") - - row["Model matchrate"] = model_matchrate - row["QC status"] = "PASS" if ancestry_group_pass and ancestry_conf_pass and matchrate_pass else "FAIL" - row["QC Comment"] = ";".join(qc_comment) - return row - - -def calc_model_matchrate(model_df: pd.DataFrame) -> float: - """Calculate the fraction of model variants matched in the sample. - - Args: - model_df: Model summary DataFrame with ``match_status`` and ``percent`` columns. - - Returns: - Matchrate as a fraction between 0 and 1. - """ - matched_vars = model_df[model_df["match_status"] == "matched"] - match_rate = matched_vars["percent"].sum() / 100 - return match_rate - - -@app.command() -def main( - scores: Annotated[ - Path, - typer.Option( - "--scores", - "-s", - exists=True, - dir_okay=False, - readable=True, - help="Normalised PRS scores TSV with SUM_Z column.", - ), - ], - ancestry: Annotated[ - Path, - typer.Option( - "--ancestry", - "-a", - exists=True, - dir_okay=False, - readable=True, - help="KNN ancestry TSV with pred_group and knn_conf columns.", - ), - ], - model_summary: Annotated[ - Path, typer.Option("--model-summary", "-m", exists=True, dir_okay=False, readable=True, help="Model summary file") - ], - output: Annotated[ - Path, - typer.Option("--output", "-o", help="Output QC TSV path."), - ], - sample: Annotated[ - str, - typer.Option("--sample", help="Sample ID to tag onto every row."), - ], - model: Annotated[ - str, - typer.Option("--model", help="Model ID to tag onto every row."), - ], - alpha: Annotated[ - float, - typer.Option("--alpha", help="Model alpha value to tag onto every row."), - ], - conf_threshold: Annotated[ - float, - typer.Option("--conf-threshold", help="Minimum ancestry confidence to pass QC. (default: 0.6)"), - ] = 0.6, - expected_ancestry: Annotated[ - List[str], - typer.Option( - "--expected-ancestry", help="Expected superpopulation label(s). Can be passed multiple times. (default: EUR)" - ), - ] = ["EUR"], - matchrate_threshold: Annotated[ - float, - typer.Option( - "--matchrate-threshold", - help="Minimum model matchrate (i.e., percentage of variants in the model that are in the sample. (default: 0.75) )", - ), - ] = 0.75, -) -> None: - """Run sample QC checks and write a MultiQC-compatible table.""" - score_file = pd.read_csv(scores, sep="\t") - ancestry_file = pd.read_csv(ancestry, sep="\t") - model_df = pd.read_csv(model_summary, sep=",") - - combined_df = score_file.merge(ancestry_file, left_on="IID", right_on="#IID") - - model_matchrate = calc_model_matchrate(model_df) - - typer.echo(f"Model matchrate:{model_matchrate}") - qc_df = combined_df.apply(QC_check, args=(expected_ancestry, conf_threshold, model_matchrate, matchrate_threshold), axis=1) - - qc_df["Sample"] = sample - qc_df["Model"] = model - qc_df["Model Alpha"] = alpha - - qc_df = qc_df.filter( - items=[ - "Sample", - "Model", - "SCORE1_SUM", - "SUM_Z", - "Model Matchrate", - "QC status", - "QC Comment", - "Model Alpha", - ] - ) - - qc_df.to_csv(output, sep="\t", index=False) - - typer.echo(f"Wrote {output}") - - -if __name__ == "__main__": - app() diff --git a/src/prs_utils/__init__.py b/src/python_template/__init__.py similarity index 100% rename from src/prs_utils/__init__.py rename to src/python_template/__init__.py diff --git a/src/python_template/cli.py b/src/python_template/cli.py new file mode 100644 index 0000000..1ecc320 --- /dev/null +++ b/src/python_template/cli.py @@ -0,0 +1,30 @@ +import typer + + +def get_hello_msg(name: str = "World"): + """ + Returns a greeting message. + + Args: + name (str): The name to greet. Defaults to "World". + + Returns: + str: A greeting message. + """ + msg = f"Hello {name}!" + + return msg + + +cli = typer.Typer() + + +@cli.command() +def hello(name: str): + """Prints a greeting message.""" + msg = get_hello_msg(name) + print(msg) + + +if __name__ == "__main__": + cli() diff --git a/tests/test_cli.py b/tests/test_cli.py index 5f28bf1..314e76b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,47 +1,6 @@ -from importlib.metadata import version as package_version +from python_template import cli -from typer.testing import CliRunner -from prs_utils.cli import cli - -runner = CliRunner() - - -def test_version_prints_the_bare_version_string(): - """The Nextflow modules snapshot this output, so it must be the version and nothing else.""" - result = runner.invoke(cli, ["--version"]) - - assert result.exit_code == 0 - assert result.output == f"{package_version('prs-utils')}\n" - - -def test_all_subcommands_are_registered(): - result = runner.invoke(cli, ["--help"]) - - assert result.exit_code == 0 - for subcommand in ("pgs-to-vcf", "merge-prs-mqc", "normalise-counts", "sample-qc", "get-snp-list"): - assert subcommand in result.output - - -def test_passthrough_forwards_help_to_the_original_parser(): - """--help after a passthrough subcommand must reach argparse, not Click.""" - result = runner.invoke(cli, ["normalise-counts", "--help"]) - - assert result.exit_code == 0 - # argparse spells these exactly as the standalone script did. - assert "--PRS" in result.output - assert "--SD" in result.output - - -def test_passthrough_forwards_arguments(tmp_path): - scores = tmp_path / "scores.tsv" - scores.write_text("IID\tSCORE1_SUM\nsampleA\t1.5\n") - output = tmp_path / "out.tsv" - - result = runner.invoke( - cli, - ["normalise-counts", "--PRS", str(scores), "--mu", "0.5", "--SD", "0.5", "-o", str(output)], - ) - - assert result.exit_code == 0 - assert output.read_text().splitlines()[1].endswith("2.0") +def test_get_hello_msg(): + assert cli.get_hello_msg() == "Hello World!" + assert cli.get_hello_msg("Bioinformagician") == "Hello Bioinformagician!" diff --git a/tests/test_get_snp_list.py b/tests/test_get_snp_list.py deleted file mode 100644 index a266cd9..0000000 --- a/tests/test_get_snp_list.py +++ /dev/null @@ -1,44 +0,0 @@ -from prs_utils import get_snp_list - -SCORING_FILE = """\ -#pgs_id=PGS000001 -hm_chr\thm_pos -1\t1000 -chr2\t2000 -""" - - -def write_scoring_file(tmp_path): - scoring_file = tmp_path / "scoring.txt" - scoring_file.write_text(SCORING_FILE) - return scoring_file - - -def test_positions_to_tsv_applies_flank_and_chr_prefix(tmp_path): - scoring_file = write_scoring_file(tmp_path) - prefix = tmp_path / "PGS000001" - - get_snp_list.positions_to_tsv(scoring_file, "hm_chr", "hm_pos", str(prefix), flank=100) - - lines = (tmp_path / "PGS000001_snplist.list").read_text().splitlines() - # Row 1 gains a "chr" prefix, row 2 already has one and is left alone. - assert lines == ["chr1:900-1100", "chr2:1900-2100"] - - -def test_positions_to_tsv_default_flank_is_100(tmp_path): - scoring_file = write_scoring_file(tmp_path) - prefix = tmp_path / "default" - - get_snp_list.positions_to_tsv(scoring_file, "hm_chr", "hm_pos", str(prefix)) - - assert (tmp_path / "default_snplist.list").read_text().splitlines()[0] == "chr1:900-1100" - - -def test_main_uses_the_harmonised_columns(tmp_path): - scoring_file = write_scoring_file(tmp_path) - prefix = tmp_path / "PGS000001" - - get_snp_list.main(["--scoring_file", str(scoring_file), "--prefix", str(prefix), "--flank", "10"]) - - lines = (tmp_path / "PGS000001_snplist.list").read_text().splitlines() - assert lines == ["chr1:990-1010", "chr2:1990-2010"] diff --git a/tests/test_merge_prs_mqc.py b/tests/test_merge_prs_mqc.py deleted file mode 100644 index 80ce770..0000000 --- a/tests/test_merge_prs_mqc.py +++ /dev/null @@ -1,65 +0,0 @@ -import pandas as pd - -from prs_utils import merge_prs_mqc - -HEADER = "Sample\tModel\tSCORE1_SUM\tSUM_Z\tQC status\tQC Comment\tModel Alpha\n" - - -def write_qc_tsv(tmp_path, name, rows): - path = tmp_path / name - path.write_text(HEADER + "".join(rows)) - return str(path) - - -def test_main_merges_sorts_and_writes_multiqc_table(tmp_path, monkeypatch): - files = [ - write_qc_tsv(tmp_path, "b.tsv", ["sampleB\tmodel1\t1.0\t0.5\tPASS\tok\t1.0\n"]), - write_qc_tsv(tmp_path, "a.tsv", ["sampleA\tmodel2\t2.0\t1.5\tFAIL\tbad\t0.5\n"]), - write_qc_tsv(tmp_path, "c.tsv", ["sampleA\tmodel1\t3.0\t2.5\tPASS\tok\t0.1\n"]), - ] - monkeypatch.chdir(tmp_path) - - merge_prs_mqc.main(files) - - output = tmp_path / "prs_scores_mqc.tsv" - text = output.read_text() - - # MultiQC configuration is emitted as a comment block ahead of the table. - assert text.startswith("# id: 'prs-scores'\n") - assert "# Z-score:\n" in text - assert "# 'sampleA__model1': '#e8f0fe'\n" in text - assert "# 'sampleB': '#ffffff'\n" in text - - df = pd.read_csv(output, sep="\t", comment="#") - assert list(df.columns) == [ - "Sample_Model", - "Sample", - "Model", - "Z-score", - "QC Status", - "QC Comment", - "Model Alpha", - ] - # Sorted by Sample then Model; SCORE1_SUM is dropped. - assert list(df["Sample_Model"]) == ["sampleA__model1", "sampleA__model2", "sampleB__model1"] - assert list(df["Z-score"]) == [2.5, 1.5, 0.5] - - -def test_main_assigns_a_distinct_colour_per_model(tmp_path, monkeypatch): - files = [ - write_qc_tsv( - tmp_path, - "all.tsv", - [ - "sampleA\tmodel1\t1.0\t0.5\tPASS\tok\t1.0\n", - "sampleA\tmodel2\t1.0\t0.5\tPASS\tok\t1.0\n", - ], - ) - ] - monkeypatch.chdir(tmp_path) - - merge_prs_mqc.main(files) - - text = (tmp_path / "prs_scores_mqc.tsv").read_text() - assert "# 'model1': '#ffd9b3'\n" in text - assert "# 'model2': '#b3d9f0'\n" in text diff --git a/tests/test_normalise_counts.py b/tests/test_normalise_counts.py deleted file mode 100644 index 20b21d6..0000000 --- a/tests/test_normalise_counts.py +++ /dev/null @@ -1,23 +0,0 @@ -import pandas as pd - -from prs_utils import normalise_counts - - -def test_score_to_z_score(): - scores = pd.Series([1.0, 2.0, 3.0]) - - z_scores = normalise_counts.score_to_z_score(scores, mu=2.0, SD=0.5) - - assert list(z_scores) == [-2.0, 0.0, 2.0] - - -def test_main_writes_sum_z_column(tmp_path): - prs = tmp_path / "scores.tsv" - prs.write_text("IID\tSCORE1_SUM\nsampleA\t1.0\nsampleB\t3.0\n") - output = tmp_path / "normalised.tsv" - - normalise_counts.main(["--PRS", str(prs), "--mu", "2.0", "--SD", "0.5", "-o", str(output)]) - - df = pd.read_csv(output, sep="\t") - assert list(df.columns) == ["IID", "SCORE1_SUM", "SUM_Z"] - assert list(df["SUM_Z"]) == [-2.0, 2.0] diff --git a/tests/test_pgs_to_vcf.py b/tests/test_pgs_to_vcf.py deleted file mode 100644 index fb15bb2..0000000 --- a/tests/test_pgs_to_vcf.py +++ /dev/null @@ -1,75 +0,0 @@ -import io - -from prs_utils import pgs_to_vcf - -SCORING_FILE = """\ -### PGS CATALOG SCORING FILE -#pgs_id=PGS000001 -#genome_build=GRCh38 -rsID\teffect_allele\tother_allele\teffect_weight\thm_chr\thm_pos\thm_rsID -rs1\tA\tG\t0.1\t1\t1000\trs1 -rs2\tT\tC\t0.2\t2\t2000\trs2 -""" - - -def test_read_meta_splits_metadata_header_and_body(): - meta, header, body = pgs_to_vcf.read_meta(io.StringIO(SCORING_FILE)) - - assert meta == {"pgs_id": "PGS000001", "genome_build": "GRCh38"} - assert header.startswith("rsID\teffect_allele") - assert len(body) == 2 - - -def test_build_info_fields_includes_numeric_values(): - row = { - "effect_weight": "0.1", - "allelefrequency_effect": "0.25", - "hm_source": "ENSEMBL Variation", - "hm_match_chr": "True", - "hm_match_pos": "True", - } - - info = pgs_to_vcf.build_info_fields(row, "PGS000001") - - assert info == ("PGSID=PGS000001;EFFECT_WEIGHT=0.1;EAF=0.25;HM_SOURCE=ENSEMBL_Variation;HM_MATCH=chr:True|pos:True") - - -def test_build_info_fields_skips_non_numeric_and_missing_values(): - row = {"effect_weight": "NA", "allelefrequency_effect": "", "hm_source": ""} - - assert pgs_to_vcf.build_info_fields(row, "PGS000001") == "PGSID=PGS000001" - - -def test_main_writes_vcf_with_chr_prefixed_contigs(tmp_path): - scoring_file = tmp_path / "scoring.txt" - scoring_file.write_text(SCORING_FILE) - output = tmp_path / "out.vcf" - - pgs_to_vcf.main([str(scoring_file), str(output)]) - - lines = output.read_text().splitlines() - assert lines[0] == "##fileformat=VCFv4.2" - assert "##pgs_id=PGS000001" in lines - # genome_build GRCh38 comes from the metadata, so contigs gain a chr prefix. - assert "##contig=" in lines - assert lines[-2] == "chr1\t1000\trs1\tG\tA\t.\tPASS\tPGSID=PGS000001;EFFECT_WEIGHT=0.1" - assert lines[-1] == "chr2\t2000\trs2\tC\tT\t.\tPASS\tPGSID=PGS000001;EFFECT_WEIGHT=0.2" - - -def test_main_skips_rows_with_equal_or_missing_alleles(tmp_path): - scoring_file = tmp_path / "scoring.txt" - scoring_file.write_text( - "#pgs_id=PGS000002\n" - "rsID\teffect_allele\tother_allele\thm_chr\thm_pos\n" - "rs1\tA\tA\t1\t1000\n" # REF == ALT - "rs2\tA\t\t1\t2000\n" # missing other_allele - "rs3\tA\tG\t1\t\n" # missing position - "rs4\tA\tG\t1\t4000\n" - ) - output = tmp_path / "out.vcf" - - pgs_to_vcf.main([str(scoring_file), str(output)]) - - records = [line for line in output.read_text().splitlines() if not line.startswith("#")] - assert len(records) == 1 - assert records[0].startswith("1\t4000\trs4\tG\tA") diff --git a/tests/test_sample_qc.py b/tests/test_sample_qc.py deleted file mode 100644 index 5f7b00a..0000000 --- a/tests/test_sample_qc.py +++ /dev/null @@ -1,82 +0,0 @@ -import pandas as pd -import pytest - -from prs_utils import sample_qc - - -def make_row(pred_group="EUR", knn_conf=0.9): - return pd.Series({"pred_group": pred_group, "knn_conf": knn_conf}) - - -def test_calc_model_matchrate_sums_only_matched_percentages(): - model_df = pd.DataFrame( - { - "match_status": ["matched", "matched", "unmatched"], - "percent": [60.0, 20.0, 20.0], - } - ) - - assert sample_qc.calc_model_matchrate(model_df) == pytest.approx(0.8) - - -def test_qc_check_passes_when_all_thresholds_are_met(): - row = sample_qc.QC_check(make_row(), ["EUR"], 0.6, 0.9, 0.75) - - assert row["QC status"] == "PASS" - assert row["QC Comment"] == "" - assert row["Model matchrate"] == 0.9 - - -@pytest.mark.parametrize( - ("row", "matchrate", "comment"), - [ - (make_row(pred_group="AFR"), 0.9, "REFERENCE_PANEL_MISMATCH"), - (make_row(knn_conf=0.1), 0.9, "LOW_REFERENCE_PANEL_CONFIDENCE"), - (make_row(), 0.1, "INSUFFICIENT_VARIANT_COVERAGE"), - ], -) -def test_qc_check_reports_each_failure_reason(row, matchrate, comment): - result = sample_qc.QC_check(row, ["EUR"], 0.6, matchrate, 0.75) - - assert result["QC status"] == "FAIL" - assert result["QC Comment"] == comment - - -def test_qc_check_joins_multiple_failure_reasons(): - row = sample_qc.QC_check(make_row(pred_group="AFR", knn_conf=0.1), ["EUR"], 0.6, 0.1, 0.75) - - assert row["QC Comment"] == ("REFERENCE_PANEL_MISMATCH;LOW_REFERENCE_PANEL_CONFIDENCE;INSUFFICIENT_VARIANT_COVERAGE") - - -def test_main_writes_qc_table(tmp_path): - scores = tmp_path / "scores.tsv" - scores.write_text("IID\tSCORE1_SUM\tSUM_Z\nsampleA\t1.5\t0.5\n") - ancestry = tmp_path / "ancestry.tsv" - ancestry.write_text("#IID\tpred_group\tknn_conf\nsampleA\tEUR\t0.9\n") - model_summary = tmp_path / "summary.csv" - model_summary.write_text("match_status,percent\nmatched,90.0\nunmatched,10.0\n") - output = tmp_path / "qc.tsv" - - sample_qc.main( - scores=scores, - ancestry=ancestry, - model_summary=model_summary, - output=output, - sample="sampleA", - model="BCAC_313_PRS", - alpha=1.0, - ) - - df = pd.read_csv(output, sep="\t") - # "Model Matchrate" is intentionally absent: QC_check writes "Model matchrate" (lowercase m) - # while the final filter asks for the capitalised name, so pandas drops it. Preserved as-is. - assert list(df.columns) == [ - "Sample", - "Model", - "SCORE1_SUM", - "SUM_Z", - "QC status", - "QC Comment", - "Model Alpha", - ] - assert df.loc[0, "QC status"] == "PASS" diff --git a/uv.lock b/uv.lock index a3119b1..26d6fb9 100644 --- a/uv.lock +++ b/uv.lock @@ -1,14 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.12" -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] +requires-python = "==3.14.*" [options] exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. @@ -38,45 +30,6 @@ version = "7.11.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d2/59/9698d57a3b11704c7b89b21d69e9d23ecf80d538cabb536c8b63f4a12322/coverage-7.11.3.tar.gz", hash = "sha256:0f59387f5e6edbbffec2281affb71cdc85e0776c1745150a3ab9b6c1d016106b", size = 815210, upload-time = "2025-11-10T00:13:17.18Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/39/af056ec7a27c487e25c7f6b6e51d2ee9821dba1863173ddf4dc2eebef4f7/coverage-7.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5b771b59ac0dfb7f139f70c85b42717ef400a6790abb6475ebac1ecee8de782f", size = 216676, upload-time = "2025-11-10T00:11:11.566Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f8/21126d34b174d037b5d01bea39077725cbb9a0da94a95c5f96929c695433/coverage-7.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:603c4414125fc9ae9000f17912dcfd3d3eb677d4e360b85206539240c96ea76e", size = 217034, upload-time = "2025-11-10T00:11:13.12Z" }, - { url = "https://files.pythonhosted.org/packages/d5/3f/0fd35f35658cdd11f7686303214bd5908225838f374db47f9e457c8d6df8/coverage-7.11.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:77ffb3b7704eb7b9b3298a01fe4509cef70117a52d50bcba29cffc5f53dd326a", size = 248531, upload-time = "2025-11-10T00:11:15.023Z" }, - { url = "https://files.pythonhosted.org/packages/8f/59/0bfc5900fc15ce4fd186e092451de776bef244565c840c9c026fd50857e1/coverage-7.11.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4d4ca49f5ba432b0755ebb0fc3a56be944a19a16bb33802264bbc7311622c0d1", size = 251290, upload-time = "2025-11-10T00:11:16.628Z" }, - { url = "https://files.pythonhosted.org/packages/71/88/d5c184001fa2ac82edf1b8f2cd91894d2230d7c309e937c54c796176e35b/coverage-7.11.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:05fd3fb6edff0c98874d752013588836f458261e5eba587afe4c547bba544afd", size = 252375, upload-time = "2025-11-10T00:11:18.249Z" }, - { url = "https://files.pythonhosted.org/packages/5c/29/f60af9f823bf62c7a00ce1ac88441b9a9a467e499493e5cc65028c8b8dd2/coverage-7.11.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0e920567f8c3a3ce68ae5a42cf7c2dc4bb6cc389f18bff2235dd8c03fa405de5", size = 248946, upload-time = "2025-11-10T00:11:20.202Z" }, - { url = "https://files.pythonhosted.org/packages/67/16/4662790f3b1e03fce5280cad93fd18711c35980beb3c6f28dca41b5230c6/coverage-7.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4bec8c7160688bd5a34e65c82984b25409563134d63285d8943d0599efbc448e", size = 250310, upload-time = "2025-11-10T00:11:21.689Z" }, - { url = "https://files.pythonhosted.org/packages/8f/75/dd6c2e28308a83e5fc1ee602f8204bd3aa5af685c104cb54499230cf56db/coverage-7.11.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:adb9b7b42c802bd8cb3927de8c1c26368ce50c8fdaa83a9d8551384d77537044", size = 248461, upload-time = "2025-11-10T00:11:23.384Z" }, - { url = "https://files.pythonhosted.org/packages/16/fe/b71af12be9f59dc9eb060688fa19a95bf3223f56c5af1e9861dfa2275d2c/coverage-7.11.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c8f563b245b4ddb591e99f28e3cd140b85f114b38b7f95b2e42542f0603eb7d7", size = 248039, upload-time = "2025-11-10T00:11:25.07Z" }, - { url = "https://files.pythonhosted.org/packages/11/b8/023b2003a2cd96bdf607afe03d9b96c763cab6d76e024abe4473707c4eb8/coverage-7.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e2a96fdc7643c9517a317553aca13b5cae9bad9a5f32f4654ce247ae4d321405", size = 249903, upload-time = "2025-11-10T00:11:26.992Z" }, - { url = "https://files.pythonhosted.org/packages/d6/ee/5f1076311aa67b1fa4687a724cc044346380e90ce7d94fec09fd384aa5fd/coverage-7.11.3-cp312-cp312-win32.whl", hash = "sha256:e8feeb5e8705835f0622af0fe7ff8d5cb388948454647086494d6c41ec142c2e", size = 219201, upload-time = "2025-11-10T00:11:28.619Z" }, - { url = "https://files.pythonhosted.org/packages/4f/24/d21688f48fe9fcc778956680fd5aaf69f4e23b245b7c7a4755cbd421d25b/coverage-7.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:abb903ffe46bd319d99979cdba350ae7016759bb69f47882242f7b93f3356055", size = 220012, upload-time = "2025-11-10T00:11:30.234Z" }, - { url = "https://files.pythonhosted.org/packages/4f/9e/d5eb508065f291456378aa9b16698b8417d87cb084c2b597f3beb00a8084/coverage-7.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:1451464fd855d9bd000c19b71bb7dafea9ab815741fb0bd9e813d9b671462d6f", size = 218652, upload-time = "2025-11-10T00:11:32.165Z" }, - { url = "https://files.pythonhosted.org/packages/6d/f6/d8572c058211c7d976f24dab71999a565501fb5b3cdcb59cf782f19c4acb/coverage-7.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84b892e968164b7a0498ddc5746cdf4e985700b902128421bb5cec1080a6ee36", size = 216694, upload-time = "2025-11-10T00:11:34.296Z" }, - { url = "https://files.pythonhosted.org/packages/4a/f6/b6f9764d90c0ce1bce8d995649fa307fff21f4727b8d950fa2843b7b0de5/coverage-7.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f761dbcf45e9416ec4698e1a7649248005f0064ce3523a47402d1bff4af2779e", size = 217065, upload-time = "2025-11-10T00:11:36.281Z" }, - { url = "https://files.pythonhosted.org/packages/a5/8d/a12cb424063019fd077b5be474258a0ed8369b92b6d0058e673f0a945982/coverage-7.11.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1410bac9e98afd9623f53876fae7d8a5db9f5a0ac1c9e7c5188463cb4b3212e2", size = 248062, upload-time = "2025-11-10T00:11:37.903Z" }, - { url = "https://files.pythonhosted.org/packages/7f/9c/dab1a4e8e75ce053d14259d3d7485d68528a662e286e184685ea49e71156/coverage-7.11.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:004cdcea3457c0ea3233622cd3464c1e32ebba9b41578421097402bee6461b63", size = 250657, upload-time = "2025-11-10T00:11:39.509Z" }, - { url = "https://files.pythonhosted.org/packages/3f/89/a14f256438324f33bae36f9a1a7137729bf26b0a43f5eda60b147ec7c8c7/coverage-7.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f067ada2c333609b52835ca4d4868645d3b63ac04fb2b9a658c55bba7f667d3", size = 251900, upload-time = "2025-11-10T00:11:41.372Z" }, - { url = "https://files.pythonhosted.org/packages/04/07/75b0d476eb349f1296486b1418b44f2d8780cc8db47493de3755e5340076/coverage-7.11.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:07bc7745c945a6d95676953e86ba7cebb9f11de7773951c387f4c07dc76d03f5", size = 248254, upload-time = "2025-11-10T00:11:43.27Z" }, - { url = "https://files.pythonhosted.org/packages/5a/4b/0c486581fa72873489ca092c52792d008a17954aa352809a7cbe6cf0bf07/coverage-7.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bba7e4743e37484ae17d5c3b8eb1ce78b564cb91b7ace2e2182b25f0f764cb5", size = 250041, upload-time = "2025-11-10T00:11:45.274Z" }, - { url = "https://files.pythonhosted.org/packages/af/a3/0059dafb240ae3e3291f81b8de00e9c511d3dd41d687a227dd4b529be591/coverage-7.11.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbffc22d80d86fbe456af9abb17f7a7766e7b2101f7edaacc3535501691563f7", size = 248004, upload-time = "2025-11-10T00:11:46.93Z" }, - { url = "https://files.pythonhosted.org/packages/83/93/967d9662b1eb8c7c46917dcc7e4c1875724ac3e73c3cb78e86d7a0ac719d/coverage-7.11.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0dba4da36730e384669e05b765a2c49f39514dd3012fcc0398dd66fba8d746d5", size = 247828, upload-time = "2025-11-10T00:11:48.563Z" }, - { url = "https://files.pythonhosted.org/packages/4c/1c/5077493c03215701e212767e470b794548d817dfc6247a4718832cc71fac/coverage-7.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ae12fe90b00b71a71b69f513773310782ce01d5f58d2ceb2b7c595ab9d222094", size = 249588, upload-time = "2025-11-10T00:11:50.581Z" }, - { url = "https://files.pythonhosted.org/packages/7f/a5/77f64de461016e7da3e05d7d07975c89756fe672753e4cf74417fc9b9052/coverage-7.11.3-cp313-cp313-win32.whl", hash = "sha256:12d821de7408292530b0d241468b698bce18dd12ecaf45316149f53877885f8c", size = 219223, upload-time = "2025-11-10T00:11:52.184Z" }, - { url = "https://files.pythonhosted.org/packages/ed/1c/ec51a3c1a59d225b44bdd3a4d463135b3159a535c2686fac965b698524f4/coverage-7.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:6bb599052a974bb6cedfa114f9778fedfad66854107cf81397ec87cb9b8fbcf2", size = 220033, upload-time = "2025-11-10T00:11:53.871Z" }, - { url = "https://files.pythonhosted.org/packages/01/ec/e0ce39746ed558564c16f2cc25fa95ce6fc9fa8bfb3b9e62855d4386b886/coverage-7.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:bb9d7efdb063903b3fdf77caec7b77c3066885068bdc0d44bc1b0c171033f944", size = 218661, upload-time = "2025-11-10T00:11:55.597Z" }, - { url = "https://files.pythonhosted.org/packages/46/cb/483f130bc56cbbad2638248915d97b185374d58b19e3cc3107359715949f/coverage-7.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:fb58da65e3339b3dbe266b607bb936efb983d86b00b03eb04c4ad5b442c58428", size = 217389, upload-time = "2025-11-10T00:11:57.59Z" }, - { url = "https://files.pythonhosted.org/packages/cb/ae/81f89bae3afef75553cf10e62feb57551535d16fd5859b9ee5a2a97ddd27/coverage-7.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d16bbe566e16a71d123cd66382c1315fcd520c7573652a8074a8fe281b38c6a", size = 217742, upload-time = "2025-11-10T00:11:59.519Z" }, - { url = "https://files.pythonhosted.org/packages/db/6e/a0fb897041949888191a49c36afd5c6f5d9f5fd757e0b0cd99ec198a324b/coverage-7.11.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8258f10059b5ac837232c589a350a2df4a96406d6d5f2a09ec587cbdd539655", size = 259049, upload-time = "2025-11-10T00:12:01.592Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b6/d13acc67eb402d91eb94b9bd60593411799aed09ce176ee8d8c0e39c94ca/coverage-7.11.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4c5627429f7fbff4f4131cfdd6abd530734ef7761116811a707b88b7e205afd7", size = 261113, upload-time = "2025-11-10T00:12:03.639Z" }, - { url = "https://files.pythonhosted.org/packages/ea/07/a6868893c48191d60406df4356aa7f0f74e6de34ef1f03af0d49183e0fa1/coverage-7.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:465695268414e149bab754c54b0c45c8ceda73dd4a5c3ba255500da13984b16d", size = 263546, upload-time = "2025-11-10T00:12:05.485Z" }, - { url = "https://files.pythonhosted.org/packages/24/e5/28598f70b2c1098332bac47925806353b3313511d984841111e6e760c016/coverage-7.11.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4ebcddfcdfb4c614233cff6e9a3967a09484114a8b2e4f2c7a62dc83676ba13f", size = 258260, upload-time = "2025-11-10T00:12:07.137Z" }, - { url = "https://files.pythonhosted.org/packages/0e/58/58e2d9e6455a4ed746a480c4b9cf96dc3cb2a6b8f3efbee5efd33ae24b06/coverage-7.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:13b2066303a1c1833c654d2af0455bb009b6e1727b3883c9964bc5c2f643c1d0", size = 261121, upload-time = "2025-11-10T00:12:09.138Z" }, - { url = "https://files.pythonhosted.org/packages/17/57/38803eefb9b0409934cbc5a14e3978f0c85cb251d2b6f6a369067a7105a0/coverage-7.11.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d8750dd20362a1b80e3cf84f58013d4672f89663aee457ea59336df50fab6739", size = 258736, upload-time = "2025-11-10T00:12:11.195Z" }, - { url = "https://files.pythonhosted.org/packages/a8/f3/f94683167156e93677b3442be1d4ca70cb33718df32a2eea44a5898f04f6/coverage-7.11.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ab6212e62ea0e1006531a2234e209607f360d98d18d532c2fa8e403c1afbdd71", size = 257625, upload-time = "2025-11-10T00:12:12.843Z" }, - { url = "https://files.pythonhosted.org/packages/87/ed/42d0bf1bc6bfa7d65f52299a31daaa866b4c11000855d753857fe78260ac/coverage-7.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a6b17c2b5e0b9bb7702449200f93e2d04cb04b1414c41424c08aa1e5d352da76", size = 259827, upload-time = "2025-11-10T00:12:15.128Z" }, - { url = "https://files.pythonhosted.org/packages/d3/76/5682719f5d5fbedb0c624c9851ef847407cae23362deb941f185f489c54e/coverage-7.11.3-cp313-cp313t-win32.whl", hash = "sha256:426559f105f644b69290ea414e154a0d320c3ad8a2bb75e62884731f69cf8e2c", size = 219897, upload-time = "2025-11-10T00:12:17.274Z" }, - { url = "https://files.pythonhosted.org/packages/10/e0/1da511d0ac3d39e6676fa6cc5ec35320bbf1cebb9b24e9ee7548ee4e931a/coverage-7.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:90a96fcd824564eae6137ec2563bd061d49a32944858d4bdbae5c00fb10e76ac", size = 220959, upload-time = "2025-11-10T00:12:19.292Z" }, - { url = "https://files.pythonhosted.org/packages/e5/9d/e255da6a04e9ec5f7b633c54c0fdfa221a9e03550b67a9c83217de12e96c/coverage-7.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:1e33d0bebf895c7a0905fcfaff2b07ab900885fc78bba2a12291a2cfbab014cc", size = 219234, upload-time = "2025-11-10T00:12:21.251Z" }, { url = "https://files.pythonhosted.org/packages/84/d6/634ec396e45aded1772dccf6c236e3e7c9604bc47b816e928f32ce7987d1/coverage-7.11.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fdc5255eb4815babcdf236fa1a806ccb546724c8a9b129fd1ea4a5448a0bf07c", size = 216746, upload-time = "2025-11-10T00:12:23.089Z" }, { url = "https://files.pythonhosted.org/packages/28/76/1079547f9d46f9c7c7d0dad35b6873c98bc5aa721eeabceafabd722cd5e7/coverage-7.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fe3425dc6021f906c6325d3c415e048e7cdb955505a94f1eb774dafc779ba203", size = 217077, upload-time = "2025-11-10T00:12:24.863Z" }, { url = "https://files.pythonhosted.org/packages/2d/71/6ad80d6ae0d7cb743b9a98df8bb88b1ff3dc54491508a4a97549c2b83400/coverage-7.11.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4ca5f876bf41b24378ee67c41d688155f0e54cdc720de8ef9ad6544005899240", size = 248122, upload-time = "2025-11-10T00:12:26.553Z" }, @@ -136,57 +89,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] -[[package]] -name = "numpy" -version = "2.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/22/fd/89965aa4ac08c74998539fcbf24fa3540f3e15237fbeb6bcf9c908f4aade/numpy-2.5.1.tar.gz", hash = "sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3", size = 20755553, upload-time = "2026-07-04T17:08:00.933Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/7b/14687aa674250e5e546f616f486b0d56d3631cd5b2415739141ce40bdcea/numpy-2.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c889b56fe48b1018f764b0eec8df59ab654e9148aa91faa12596043500de277", size = 16801574, upload-time = "2026-07-04T17:06:12.423Z" }, - { url = "https://files.pythonhosted.org/packages/e1/19/cc5bb2a3f2913d27d6dbb2c78d25921fabaedc6741d4a5a615a11f3c5bf3/numpy-2.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab451b59c5643c570974c43aef780703ef1d3b4965d2be07afd530615a9358d1", size = 11772250, upload-time = "2026-07-04T17:06:15.726Z" }, - { url = "https://files.pythonhosted.org/packages/42/77/fdf34a71dd30f54979b18603bee915e0aaf825b07afe79acd60b04b691e2/numpy-2.5.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:78798bd5b9ad744056af8efa90e3b9ddaa53272a0848a483084a1cc0a13b2dc0", size = 5331516, upload-time = "2026-07-04T17:06:17.913Z" }, - { url = "https://files.pythonhosted.org/packages/ce/e2/eb7efa015b4cce41e2517bf182a7fce0d7d5b9d9ed76a29bfa0f4fe4505c/numpy-2.5.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:2ae0ca40bcb22d6ba59c1dfd5446f49940b0f2d821fde133f10dda11f816b84e", size = 6664863, upload-time = "2026-07-04T17:06:20.02Z" }, - { url = "https://files.pythonhosted.org/packages/a9/4b/a2b32dd94ee9ffbeecb28152240042a3949db33b1c834d44090b80e1b3b8/numpy-2.5.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61ac47e772e6b8ea489e1d2f441a34c5c3ac17327e7ce294cbdf535795ad4e75", size = 15167977, upload-time = "2026-07-04T17:06:21.621Z" }, - { url = "https://files.pythonhosted.org/packages/b8/a9/6e73d68500f80773f65f0654ea932019d6694329a0eb0ed0533de38df376/numpy-2.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca", size = 16672469, upload-time = "2026-07-04T17:06:24.064Z" }, - { url = "https://files.pythonhosted.org/packages/24/7d/ad3e59015135f5261c95fd4cafeff159c955febd83a99a1d9250c4233815/numpy-2.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f7119ebff1a9829e9f431a4f9d28e703023bb6b9fe7c8f724467dbfc27c94ab3", size = 16527531, upload-time = "2026-07-04T17:06:26.69Z" }, - { url = "https://files.pythonhosted.org/packages/83/d0/a39b2fbcde9cb17a1dac678f254b33a6336298af9df338824c685425d5e8/numpy-2.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e824c2acf8862052246be5a44c15da1777940c60d010dd2aab897824d9c430f9", size = 18431940, upload-time = "2026-07-04T17:06:29.521Z" }, - { url = "https://files.pythonhosted.org/packages/04/12/cff070947791c1ed425ff76413189adbdc2fbe215eba7ce7fa454a03c7f8/numpy-2.5.1-cp312-cp312-win32.whl", hash = "sha256:08d60c810432eb83360958dea0999ac4cfb94531ea8efcbf0b7f277c2068aeb2", size = 6066764, upload-time = "2026-07-04T17:06:32.571Z" }, - { url = "https://files.pythonhosted.org/packages/65/66/53f31807a48a750f9d748da273bc3fcedd12b27ff1f3e373bfec55ef2dc0/numpy-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7d60026c0bdb1380e83bfa7a0419c4577ee4b9a08880afcb6dadeb74c649fa2", size = 12430966, upload-time = "2026-07-04T17:06:34.926Z" }, - { url = "https://files.pythonhosted.org/packages/2b/2a/d1a88066b1c14186f5d3c0d18c94f17b064511982bab0578d49ee9d43c29/numpy-2.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:17a25e09640602e10bc8de0e6fa2b3fd68eedd84ba6d7842dc8f32f9ab87bd0b", size = 10350488, upload-time = "2026-07-04T17:06:37.785Z" }, - { url = "https://files.pythonhosted.org/packages/eb/07/ec2a3f0c91761581d4b7104a740791800025983f9a4dc4e73f91a99aeac4/numpy-2.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0bfebd8695f9863592fe744be833a258120b14a9f39da255e8aa8fade2c0ddd1", size = 16796419, upload-time = "2026-07-04T17:06:40.37Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ab/ddb499fc4f8780354395face5b65c7fd107bcd6e1d667a5f07d046956f6f/numpy-2.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30b44a6b53a7ae63c54c089a8726e5563ed302716c5b7ccc85afade40b0e7ff6", size = 11765832, upload-time = "2026-07-04T17:06:42.768Z" }, - { url = "https://files.pythonhosted.org/packages/88/b3/3c28c558a09fc72100c646dac6d2fce8e834c471b0edca01a29996706117/numpy-2.5.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6165343f81b56ef8f514f396989e529b61d9dc709b99421b07e9f3e698e2287d", size = 5325143, upload-time = "2026-07-04T17:06:45.466Z" }, - { url = "https://files.pythonhosted.org/packages/5e/0e/ce19b985bb15c596f4f05954e76cccc77c845083b3b8f938a6c68e523128/numpy-2.5.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4939237038ada79308dda3204ac6462df056b5672b2e25db1149cf873668b3e1", size = 6659749, upload-time = "2026-07-04T17:06:47.288Z" }, - { url = "https://files.pythonhosted.org/packages/2e/20/1ee6614d64332a1bba6411f38e68cb79eec1b2459e20a623777c5c5492a2/numpy-2.5.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c6759f538fb912fc46de0a6b1758ccf7b57bc7c7ebebc23974fdac3de8db0cd", size = 15164716, upload-time = "2026-07-04T17:06:49.494Z" }, - { url = "https://files.pythonhosted.org/packages/ed/a7/2bcd3fdbb87804755c35b729bf8709d62025c5f4cfd7d5b2415997097515/numpy-2.5.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9726558e8db4a5bf7929a70ae50f63abda4daf0efe810e3bfbab95976f75fc1a", size = 16661440, upload-time = "2026-07-04T17:06:52.061Z" }, - { url = "https://files.pythonhosted.org/packages/fc/d7/a41e3310c886fe457d36e670bbf24fae411aca8a7b6ad92a32afd924077c/numpy-2.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3935f3b419b244a02732676fa5317a9193cc596a4c0646db07e5b421229ac9f7", size = 16526305, upload-time = "2026-07-04T17:06:54.605Z" }, - { url = "https://files.pythonhosted.org/packages/53/75/4333a9a707c1edd3a4e1a0c58eca52c0f31e55089fa80db02b5565b24df7/numpy-2.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc932a65ded7ce9013d120845a2514dcccb1a67bfc8deb8d37633762951904a6", size = 18423008, upload-time = "2026-07-04T17:06:57.54Z" }, - { url = "https://files.pythonhosted.org/packages/ee/90/e314a32b1c11a2ffe818ddad3a57b50b4b6e1b6c487192eb50cdef0415d0/numpy-2.5.1-cp313-cp313-win32.whl", hash = "sha256:4b4ff1608417eb7a59da7b967bbb798cacfe071d2caf526a24281cd562072ed9", size = 6063885, upload-time = "2026-07-04T17:07:00.14Z" }, - { url = "https://files.pythonhosted.org/packages/10/70/800b3fca480af32df9e8ea9f3d4a0c8feb4b32d7f195d174eabbda4829ad/numpy-2.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:6c3fe51bc6a16453d452997053454f309e8e0ed7b42d6b361ce4ac8c32913d74", size = 12425674, upload-time = "2026-07-04T17:07:02.387Z" }, - { url = "https://files.pythonhosted.org/packages/8b/0b/196350c122f50f6ca56846f2d71efd5e0d24b7b2e07355e019b2e2c7a11e/numpy-2.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f7feb014281029e628ba2d5a007407443b06e418b6fe451d1e2adcbc8eba0107", size = 10350256, upload-time = "2026-07-04T17:07:04.878Z" }, - { url = "https://files.pythonhosted.org/packages/db/f4/731b6085a83faf6ca843394cbd5e217280c214399f7e8b21b9f552af0ae2/numpy-2.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7c786fe9a5bbe360022e584c5a34cf6b54265c71bd7ec8ac3d8fec38968071f8", size = 16795063, upload-time = "2026-07-04T17:07:07.374Z" }, - { url = "https://files.pythonhosted.org/packages/bf/64/0e215f2048dd11a55bb989ed41b3585ef57452404e638d703a211a3e4157/numpy-2.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32985c896d897419ef8da6917872d80b78ad0ea26d85b23245c7366ffde76d75", size = 11776652, upload-time = "2026-07-04T17:07:09.907Z" }, - { url = "https://files.pythonhosted.org/packages/b5/59/2b844c7a6e9deff69b404a66221e1542937734f65d5e6e39411876053862/numpy-2.5.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:efd736408cc97c79b9e6917338dfc8f06013b2274f992e96b1d9a81a71e2a2c2", size = 5335944, upload-time = "2026-07-04T17:07:12.227Z" }, - { url = "https://files.pythonhosted.org/packages/86/51/9bf7cb2cabcebc9e017e4ec7e6322b378317a542c08b4cb68479c1efc716/numpy-2.5.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:ab84dc6b074fa881cae55bea94cc4f68e285181ba7f32497bf7dee6b1496165b", size = 6656266, upload-time = "2026-07-04T17:07:14.368Z" }, - { url = "https://files.pythonhosted.org/packages/83/3e/fb7615b211b82a32f44d5180a6d421b61f84d4fadd578b48ba4ac34e189f/numpy-2.5.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caf3e317d33d60c37986b452613f4ab51246d0691350c03d0cb4a898627f4a95", size = 15179720, upload-time = "2026-07-04T17:07:16.272Z" }, - { url = "https://files.pythonhosted.org/packages/41/5f/0f992cb24560673496c5d68de61913b57166ce530ffda07c1f280e0cc464/numpy-2.5.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54ad769f17bc2d833b620851989f62054fb9ab93c969d9e1dc3c8e3d56beea21", size = 16664835, upload-time = "2026-07-04T17:07:19.021Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2f/97d6475ee91afe2587797d09446f9d3e475ad4cb681662d824809327b75a/numpy-2.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c12afb53450fa976d4c681c50a7423729a4c51c0465ed9f32b8a9cabbc472373", size = 16539135, upload-time = "2026-07-04T17:07:22.015Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5b/4db81e4ba0be7e2776b1de68c82aa862c7f8ec27e1b4927d4ae075e20678/numpy-2.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c11c405efc5ff6816d5983c96cdfa215bab3428961243af3ff59b228490438", size = 18426684, upload-time = "2026-07-04T17:07:24.941Z" }, - { url = "https://files.pythonhosted.org/packages/1f/64/c0ba2d90724d450279a7df8f32057241070250a26a7e2b5337d77347f481/numpy-2.5.1-cp314-cp314-win32.whl", hash = "sha256:f2479a47f8d5932d1718168a681ad6e536a9df484c83cfcf9de365e164537ace", size = 6116103, upload-time = "2026-07-04T17:07:27.622Z" }, - { url = "https://files.pythonhosted.org/packages/c1/1a/837f9ed7405adcd7a40538792eb169eddd8fa5630c16a1ef49dae71a30f4/numpy-2.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:24d0eb82c0541d3415a33425db64ae439dffccd7b4dbcb30e7c35120205c506a", size = 12562177, upload-time = "2026-07-04T17:07:29.887Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/49707938b6dd0a78a9178dd93227dc89e4c11af47f5c798d70366e8d0483/numpy-2.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:5a4c988b38d261deeeaad9954e3deb091ad905c94e8bb6708654ef1d97f286b0", size = 10627739, upload-time = "2026-07-04T17:07:32.568Z" }, - { url = "https://files.pythonhosted.org/packages/a6/c7/bb4b882cfe7f299cbc8b66e42e7dd78cf9d14e40f9469fc5e3db7e15b3bd/numpy-2.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a33276be12fa045805f477f22482088b66bb758ffbe89a9d21457de863a32e22", size = 11894709, upload-time = "2026-07-04T17:07:34.941Z" }, - { url = "https://files.pythonhosted.org/packages/40/3f/5af7f4a7f6224aef48017aa82bb6174c7a659d724be0c75017b7e64a55b4/numpy-2.5.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f089d7b00756190aacf1f5d34bdf38c3c430ac82b4f868f8cede73380460fce7", size = 5453810, upload-time = "2026-07-04T17:07:37.495Z" }, - { url = "https://files.pythonhosted.org/packages/20/c9/3474309bc94d634d3f9c3eddf03250ecb8c22cd948ef16fef69a77cc5d7b/numpy-2.5.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:09e9bfd8d2cf479c7d174804fb3811c53a8e9f20a37444008606b57d6b7a826d", size = 6761189, upload-time = "2026-07-04T17:07:39.563Z" }, - { url = "https://files.pythonhosted.org/packages/90/8a/558ae39fdd55d7e7f7fef9a84a6e964ac6b23edbd2a07e52bb084500507d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e68d8dd1e7eba712948f2053a29ec86917bc70ba1358df869d9f06649ef9cf09", size = 15225039, upload-time = "2026-07-04T17:07:41.682Z" }, - { url = "https://files.pythonhosted.org/packages/63/27/ca7392b2d030277bdf0273e7d23255b3ee57d57a7c170a6f4fb3981e1e5d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99d5095fa265a0c4152e7bb12759e14381ef5496152f1ce58f44bdf55c44beb4", size = 16701306, upload-time = "2026-07-04T17:07:44.611Z" }, - { url = "https://files.pythonhosted.org/packages/02/42/03d53ae7996c44d4374a8262e9dc41671fd56cbb98f7d47ef85cf5da4c6b/numpy-2.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ab87a91b3cc3382b8956095bd8f95e00cf679bb81554339be1a2ba404a1473c1", size = 16589955, upload-time = "2026-07-04T17:07:47.694Z" }, - { url = "https://files.pythonhosted.org/packages/7b/15/6c1784ae469640e65db111e9a34b3d0f14d91e8a38b9ce34810ced370dbb/numpy-2.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:224ca51130ef7da85bea2191625181cb4f337f9cb64b471f10c1a12aa8b60077", size = 18464252, upload-time = "2026-07-04T17:07:50.684Z" }, - { url = "https://files.pythonhosted.org/packages/94/a8/f98e50356cf167df656c526c2dfeec2d7dde182f2a3da4b458a5938e2776/numpy-2.5.1-cp314-cp314t-win32.whl", hash = "sha256:6eab239876581b2b3c5a242281b6007bbdbcd1c7085d7709bb57c5929b11e6bf", size = 6263298, upload-time = "2026-07-04T17:07:53.445Z" }, - { url = "https://files.pythonhosted.org/packages/72/ac/96ae880cdecad0b3275d9359fcec72667b49a4863c9f12942e43679dda02/numpy-2.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:83ce9c80d5b521b0d77ddcbe5447c218d247929b6cc056ca5351342accfff0af", size = 12748623, upload-time = "2026-07-04T17:07:55.384Z" }, - { url = "https://files.pythonhosted.org/packages/a1/5a/4d2b1601df3602dba7a14f3348ba9bfe94a18adb428e693df6154c293831/numpy-2.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:5a6db61f9aaa57e369905c67d852045d3c4f7126405b29d09b19dec118e9c9cb", size = 10697674, upload-time = "2026-07-04T17:07:58.506Z" }, -] - [[package]] name = "packaging" version = "25.0" @@ -196,52 +98,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] -[[package]] -name = "pandas" -version = "3.0.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "python-dateutil" }, - { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/be/4f/5f3422a2afec5ffc46308b79e53291365a93748b498ac2e58bead0197916/pandas-3.0.5.tar.gz", hash = "sha256:dca3734d6ab7c906e6730f0788b0a1dbb9f2467731f9711f77995c8e9d62d712", size = 4658219, upload-time = "2026-07-22T22:19:28.819Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/54/1dc810ea558d1320b597aa140a514f2fdf1d2ea09c38cf556f13ea712ec9/pandas-3.0.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa290c16964d4963fbfbc358928239cf3bd755b20e988ce944877def2f44471d", size = 10411717, upload-time = "2026-07-22T22:18:08.307Z" }, - { url = "https://files.pythonhosted.org/packages/68/56/fbe81c09195924d8b7b8d4461a20458fe80a6a5ed6b24f0314da684277e1/pandas-3.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2e26bb46934b8a2ca0c3de1d3d606fc5f6746584791b2db264d58cf370e08dc", size = 9957095, upload-time = "2026-07-22T22:18:10.6Z" }, - { url = "https://files.pythonhosted.org/packages/e0/51/fac252f4a913ed5eabf3c11b880a9e8d5a6c10f0b2129d0462212d238b4d/pandas-3.0.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73fa87b08a7ef706f8aafda39ddaccf2a99047bea62d8c88a0361bcafb2237bc", size = 10485458, upload-time = "2026-07-22T22:18:12.834Z" }, - { url = "https://files.pythonhosted.org/packages/12/98/e976540c1addf70442be7842a18cf70884a964abbf69442504f4d2939989/pandas-3.0.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d373ce03ffd84010ed9839fa73672a9c8256990532e158440c0085db7d914b34", size = 10998091, upload-time = "2026-07-22T22:18:15.209Z" }, - { url = "https://files.pythonhosted.org/packages/a4/8c/1f29b5be8d3fc47dd7567eb167fabba2085879b31e0287ce7cba6d3d2ff4/pandas-3.0.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a29c53d85ea98c5e792c59ef82ee9fbe6ca902c0d0adb6b23f45ef894cd7bf6", size = 11499501, upload-time = "2026-07-22T22:18:17.689Z" }, - { url = "https://files.pythonhosted.org/packages/9d/e2/bd9c98ad2df7b38bde002adde4cdf353519da51881634323b126c55997f9/pandas-3.0.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a5ad3b02ed6bc7d7ae9b70804b2c6aa31827489d150f8e623ce82491b82085d7", size = 12060559, upload-time = "2026-07-22T22:18:20.147Z" }, - { url = "https://files.pythonhosted.org/packages/f3/9a/ffbd852d58bd74a617fe2f8ee6a58a96982271ce41cf981eab22190b4a4b/pandas-3.0.5-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:b2acb4650527eec6822c3dadb2b771277b65e7dae7a267d4bccf65fd1bb3fbce", size = 7197652, upload-time = "2026-07-22T22:18:22.502Z" }, - { url = "https://files.pythonhosted.org/packages/70/b5/d2d3e9ae73362ba4229651b0ee1455cf78073a1ce585f6ff693782ce263e/pandas-3.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:80a611068e8a3ac23f7398c6c14eb46dc974e5cc9997f653e2dcfd1da74edd41", size = 9831691, upload-time = "2026-07-22T22:18:24.534Z" }, - { url = "https://files.pythonhosted.org/packages/52/51/dea1e89d6a6796b9c43f85a09b484ee03edb8a4c4842e73e200a8c11301c/pandas-3.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:25ff585b972a18ef1fe9ffa3ac6544d9950508aa76832e5147640b6022821e49", size = 9105796, upload-time = "2026-07-22T22:18:27.064Z" }, - { url = "https://files.pythonhosted.org/packages/bf/09/7b95c4a0025227d6f118c4039b423412ac6a982db02864166185d812fbc7/pandas-3.0.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c1c05a767fe8e5b4fe9e1c29806829c582052eaedb9120a3da83ba3f69e24a5b", size = 10385742, upload-time = "2026-07-22T22:18:29.346Z" }, - { url = "https://files.pythonhosted.org/packages/8d/0c/dc78fd8c4da477b4b5e8ad37295af352190d21ef63a9ee1bc071753074cc/pandas-3.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b86765f268b56f7e665b93bce9d5df69dee7f99e595cf8fb839483ab315942a3", size = 9932067, upload-time = "2026-07-22T22:18:31.833Z" }, - { url = "https://files.pythonhosted.org/packages/3e/71/3592c055cf44df9808550f9368ceda80ff2b224d355ef73fe251dcda1802/pandas-3.0.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c597ecf5616b5c420372c1d4d4c00dbbfba7398bea857dcc984347e1ea48417b", size = 10466756, upload-time = "2026-07-22T22:18:34.195Z" }, - { url = "https://files.pythonhosted.org/packages/e3/70/4363150359f95b4cb4bcbb34ca23572bb5495749a621a8f3d5a1ddfd293c/pandas-3.0.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b11c36e218331d0387cbe3a0a5f75162357a1d92d57b2b08a336ff94b19b2be", size = 10938525, upload-time = "2026-07-22T22:18:36.81Z" }, - { url = "https://files.pythonhosted.org/packages/f7/d0/317e7a0c67c0e69fa905a0161409397a7dc2d46ff611f6ca4803352c042b/pandas-3.0.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf52e1f61d229496da17dc7ab54acdee627357e7008fd4fecba3d0ba2937fa58", size = 11489303, upload-time = "2026-07-22T22:18:39.287Z" }, - { url = "https://files.pythonhosted.org/packages/f1/8d/36dade89b49e4f9d5cbdbe863772581f98c0c6d78fc39ad4c557f6f2e17e/pandas-3.0.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:db172144bb56422bd157812f3b021eacc255451470b31e2c633c349490a1cfee", size = 11989004, upload-time = "2026-07-22T22:18:42.208Z" }, - { url = "https://files.pythonhosted.org/packages/9c/ba/18c4ec8a746e177da05a9e7a7963781d8ea195780724f854601b6ebd6b78/pandas-3.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:0d298e951f23016ce4699951d044ae6418dbc91bf68cefca0f77666fcbb4e5c6", size = 9826896, upload-time = "2026-07-22T22:18:44.539Z" }, - { url = "https://files.pythonhosted.org/packages/de/ec/28a57266b753799a87b8bc79e7887ac6fd981b8c6d2978a0b7e7b6bd708c/pandas-3.0.5-cp313-cp313-win_arm64.whl", hash = "sha256:66266d3442a5e8b3c90274c2b8b230bee42dd1c286bc822cc2f9f2c7e12b883e", size = 9094790, upload-time = "2026-07-22T22:18:47.468Z" }, - { url = "https://files.pythonhosted.org/packages/51/2f/cf6aae281264f4463f0875bcbb15fd2bb6d291cc535187dad1732475e4a9/pandas-3.0.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2f264fc46911cc8131a7322a16199bbf8e353d27c10bb211f5bd0c814324dc36", size = 10390034, upload-time = "2026-07-22T22:18:49.818Z" }, - { url = "https://files.pythonhosted.org/packages/06/ec/5189518c7a7659c4bdcc6b1eb32c46c6f3c86b0661ffd84143d1112c7732/pandas-3.0.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:53730687fcd161883b24e10411c06d6a4c0f2275d2faf3bb2bc25deb4ba8007c", size = 9980065, upload-time = "2026-07-22T22:18:52.249Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f1/598503ce8d7e3c35601e0747ba288c7864baae66380725bc12f13f884dfe/pandas-3.0.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:960d3ebcf249f75206899fcd2c6de53f736b7265759ced0d3e559df0b8b709b0", size = 10545532, upload-time = "2026-07-22T22:18:54.813Z" }, - { url = "https://files.pythonhosted.org/packages/fa/de/ceae2adf7034e07e9910299fe412e1819c4f0dd520700a888bcb03625448/pandas-3.0.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e94c2c5ca43bd3ca32bf64d32308887b65e5f9bfd8023ea52755107a999f93b", size = 10963120, upload-time = "2026-07-22T22:18:57.42Z" }, - { url = "https://files.pythonhosted.org/packages/66/25/86e0f4451874eb79e688deeebe3c451fec4557f8952005818d800ee8ac7e/pandas-3.0.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e819dd5f62966b481a8cb649d3299ebd886a1ea91ed5a99bf7ce77c98d18ab94", size = 11563178, upload-time = "2026-07-22T22:18:59.729Z" }, - { url = "https://files.pythonhosted.org/packages/f3/45/8643daa3b4147e433adfcccefdd0380d3aad79d86b15d8999730fe1944d5/pandas-3.0.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3c5ed2e7c06e91d340dfd091d7934f9bc82e4a36b95f647f090b9d1c9ac649da", size = 12028708, upload-time = "2026-07-22T22:19:02.164Z" }, - { url = "https://files.pythonhosted.org/packages/96/58/ad979ae617615576e8aafd569c9d4b62f1191d896e38f51d66ba06f3b89a/pandas-3.0.5-cp314-cp314-win_amd64.whl", hash = "sha256:cd8f7c6dc98527058ee6264219343f5392240a6f1bfa654fc5d79023020d0c92", size = 9951806, upload-time = "2026-07-22T22:19:04.596Z" }, - { url = "https://files.pythonhosted.org/packages/69/32/7ac03886b304049a9d2625ee88f59af760d8a93bd30ed9239bce7b9869a8/pandas-3.0.5-cp314-cp314-win_arm64.whl", hash = "sha256:5183427f5a8156d480f30333777bc978be93650a49a7c01db26adffe95b31e85", size = 9238297, upload-time = "2026-07-22T22:19:06.836Z" }, - { url = "https://files.pythonhosted.org/packages/be/ed/1d1f2ee5547d5167face2376d11c8b2a4c7bfff5a416ee7a9046891fab1e/pandas-3.0.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:303da736987d481074ca720ada325f8bd80c64ebc2d45ed79b29df3aaa4a26ca", size = 10849690, upload-time = "2026-07-22T22:19:09.391Z" }, - { url = "https://files.pythonhosted.org/packages/57/55/17e17152e98fbb0c4b1e562bc65387a2f20a80db0f4a86bf8d3a0e4248d4/pandas-3.0.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3b2801bbb049d0136f6c213eae02b5fca969384fc2064dd728d8620552aa49da", size = 10509945, upload-time = "2026-07-22T22:19:11.773Z" }, - { url = "https://files.pythonhosted.org/packages/88/90/817d44dbf83facf9556f33576d9af0a241981e7bb5c00606c0bcb5df8dda/pandas-3.0.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cce3a9d11d2b1f82c69a27ec1f4948a170e2c403c4bbfa8cca62e3fdebe2ef3a", size = 10392197, upload-time = "2026-07-22T22:19:14.024Z" }, - { url = "https://files.pythonhosted.org/packages/f1/da/889f00c0a6f5aa1545add70abbf01502dff87ab577adb855bd631c54d2f2/pandas-3.0.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef01af4d8dc6cd2c8d6c7736f149574ef93fe043811eeb5e445f2647154b5040", size = 10862726, upload-time = "2026-07-22T22:19:16.351Z" }, - { url = "https://files.pythonhosted.org/packages/bc/98/f1e934fb3c98fce859c6147c6785816c7b5b9ab7821115c5d8c4de9842b9/pandas-3.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e2759e890db96dfcffdbd9b86c3c2cb6afaf58def482820317e06163ec1066cd", size = 11414864, upload-time = "2026-07-22T22:19:18.981Z" }, - { url = "https://files.pythonhosted.org/packages/fe/be/d448af7d657d82e1888dd8551f79c6d6fb161080b5b9752d84d910ec2319/pandas-3.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b58b1b39d46a5862e3fb18f50d1a201398619d16a0f9f73f57eea5583cf0e63c", size = 11925105, upload-time = "2026-07-22T22:19:21.515Z" }, - { url = "https://files.pythonhosted.org/packages/29/c1/ccb4238212c8c4f496c584f3044d94e0c030ed8e1d68999db46c91c2242f/pandas-3.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:1c10461f6eeb35d8f05b6184c65c8b9991663b66c46b1d559b682cb34ae7c6ea", size = 10387612, upload-time = "2026-07-22T22:19:24.257Z" }, - { url = "https://files.pythonhosted.org/packages/d2/cf/6a51b2c38980e04c279fd2fa908a1b0982064e860444acfca4ec2e2c8359/pandas-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3c5015fd1730fbf883647e88068176c839c102cea883ba1769a6f4593bfc1f8c", size = 9509776, upload-time = "2026-07-22T22:19:26.694Z" }, -] - [[package]] name = "pluggy" version = "1.6.0" @@ -251,35 +107,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] -[[package]] -name = "prs-utils" -version = "1.0.0" -source = { editable = "." } -dependencies = [ - { name = "pandas" }, - { name = "typer" }, -] - -[package.dev-dependencies] -dev = [ - { name = "pytest" }, - { name = "pytest-cov" }, - { name = "ruff" }, -] - -[package.metadata] -requires-dist = [ - { name = "pandas", specifier = ">=2.2" }, - { name = "typer", specifier = ">=0.26.8" }, -] - -[package.metadata.requires-dev] -dev = [ - { name = "pytest", specifier = ">=9.1.1" }, - { name = "pytest-cov", specifier = ">=7.1.0" }, - { name = "ruff", specifier = ">=0.15.20" }, -] - [[package]] name = "pygments" version = "2.19.2" @@ -320,15 +147,28 @@ wheels = [ ] [[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } +name = "python-template" +version = "0.1.0" +source = { editable = "." } dependencies = [ - { name = "six" }, + { name = "typer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [{ name = "typer", specifier = ">=0.26.8" }] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=9.1.1" }, + { name = "pytest-cov", specifier = ">=7.1.0" }, + { name = "ruff", specifier = ">=0.15.20" }, ] [[package]] @@ -378,15 +218,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, ] -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - [[package]] name = "typer" version = "0.26.8" @@ -401,12 +232,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/7c/f7/68adc395201b20b87 wheels = [ { url = "https://files.pythonhosted.org/packages/80/87/b9fd69c92c6102a066e1b86a35243f53e70bd4c709f2a26d9f4fee4f4dc0/typer-0.26.8-py3-none-any.whl", hash = "sha256:3512ca79ac5c11113414b36e80281b872884477722440691c89d1112e321a49c", size = 122564, upload-time = "2026-06-26T09:22:44.72Z" }, ] - -[[package]] -name = "tzdata" -version = "2026.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, -]